Schur-Riesz Greedy Adapter Expansion / schur_riesz_mvp.py

Failed on benchmark

Raw ⬇ ZIP
  1#!/usr/bin/env python3
  2"""Schur-Riesz Greedy Adapter Expansion numerical MVP."""
  3import json
  4import numpy as np
  5
  6SEED = 2145
  7
  8
  9def weighted_projector(B, y, rtol=1e-11):
 10    y = np.asarray(y, float)
 11    n = len(y)
 12    if B.size == 0:
 13        return np.zeros((n, n)), np.diag(np.sqrt(y))
 14    Y = np.diag(y)
 15    gram = B.T @ Y @ B
 16    P = B @ np.linalg.pinv(gram, rcond=rtol) @ B.T @ Y
 17    return P, np.diag(np.sqrt(y))
 18
 19
 20def residual_response(B, C, y):
 21    P, Ys = weighted_projector(B, y)
 22    Q = C - P @ C
 23    WQ = Ys @ Q
 24    sv = np.linalg.svd(WQ, compute_uv=False)
 25    scale = max(1.0, float(sv[0]) if len(sv) else 0.0)
 26    positive = sv[sv > 1e-9 * scale]
 27    return Q, float(np.sum(WQ * WQ)), positive
 28
 29
 30def fit_loss(X, target, y):
 31    sw = np.sqrt(y)
 32    Xw, tw = sw[:, None] * X, sw * target
 33    residual = tw - Xw @ np.linalg.pinv(Xw) @ tw
 34    return 0.5 * float(residual @ residual)
 35
 36
 37def exact_increment(X, C, target, y):
 38    before = fit_loss(X, target, y)
 39    after = fit_loss(np.column_stack([X, C]), target, y)
 40    Q, _, _ = residual_response(X, C, y)
 41    sw = np.sqrt(y)
 42    Qw, rw = sw[:, None] * Q, sw * target
 43    predicted = 0.5 * float(rw @ Qw @ np.linalg.pinv(Qw.T @ Qw) @ Qw.T @ rw)
 44    return before - after, predicted
 45
 46
 47def make_problem(seed=SEED, n=180, d=3, candidates=18):
 48    r = np.random.default_rng(seed)
 49    y = np.exp(r.normal(0, .35, n))
 50    base = r.normal(size=(n, d))
 51    # Several candidates are redundant, several are useful, and several are noise.
 52    blocks = []
 53    labels = []
 54    for j in range(candidates):
 55        if j < 5:
 56            C = base[:, [j % d]] + .015 * r.normal(size=(n, 1))
 57            labels.append("redundant")
 58        elif j in (5, 7, 11):
 59            C = r.normal(size=(n, 1))
 60            labels.append("useful")
 61        else:
 62            C = .07 * r.normal(size=(n, 1))
 63            labels.append("weak")
 64        blocks.append(C)
 65    target = 1.8 * blocks[5][:, 0] - 1.2 * blocks[7][:, 0] + .25 * base[:, 0] + .35 * r.normal(size=n)
 66    return y, base, blocks, labels, target
 67
 68
 69def math_checks():
 70    r = np.random.default_rng(SEED)
 71    n = 80
 72    y = np.exp(r.normal(size=n))
 73    B = r.normal(size=(n, 3))
 74    C = r.normal(size=(n, 2))
 75    P, Ys = weighted_projector(B, y)
 76    Q, gain, sv = residual_response(B, C, y)
 77    orth = np.linalg.norm(B.T @ (y[:, None] * Q))
 78    idem = np.linalg.norm(P @ P - P)
 79
 80    # Prediction 1: residual gain vanishes as candidate approaches incumbent.
 81    scales = [0.0, .25, .5, 1.0, 2.0]
 82    # candidate = incumbent direction + scale * novel direction
 83    novel = r.normal(size=(n, 1))
 84    gain_scale = []
 85    for a in scales:
 86        _, g, _ = residual_response(B, B[:, [0]] + a * novel, y)
 87        gain_scale.append(g)
 88    # Prediction 2: gain scales quadratically with candidate amplitude.
 89    amps = [.25, .5, 1., 2.]
 90    amp_gain = [residual_response(B, a * novel, y)[1] for a in amps]
 91    ratios = [amp_gain[i] / amp_gain[2] for i in range(4)]
 92    # Prediction 3: exact least-squares improvement equals projected residual energy.
 93    target = r.normal(size=n)
 94    observed, predicted = exact_increment(B, C, target, y)
 95    return {
 96        "projection_orthogonality_abs": orth,
 97        "projector_idempotence_abs": idem,
 98        "scale_sweep": {str(a): g for a, g in zip(scales, gain_scale)},
 99        "amplitude_sweep_gain": {str(a): g for a, g in zip(amps, amp_gain)},
100        "amplitude_ratios_vs_a1": {str(a): ratios[i] for i, a in enumerate(amps)},
101        "expected_amplitude_ratios": {str(a): a*a for a in amps},
102        "exact_loss_drop": observed,
103        "predicted_loss_drop": predicted,
104        "relative_prediction_error": abs(observed-predicted) / max(1e-12, abs(observed)),
105        "positive_singular_values": sv.tolist(),
106    }
107
108
109def greedy_experiment():
110    y, base, blocks, labels, target = make_problem()
111    # Baseline: fixed order; idea: largest projected energy, with a sensible
112    # lower bound that rejects nearly-null blocks. Both use one scalar block.
113    def run(order):
114        X = base.copy()
115        losses = [fit_loss(X, target, y)]
116        selected = []
117        for j in order:
118            Q, g, sv = residual_response(X, blocks[j], y)
119            if len(sv) and sv[0] >= .15 and sv[0] <= 30:
120                selected.append(j)
121                X = np.column_stack([X, blocks[j]])
122                losses.append(fit_loss(X, target, y))
123            if len(selected) == 3: break
124        return selected, losses
125    fixed, fixed_losses = run(list(range(len(blocks))))
126    # Greedy recomputes residual gains each round.
127    remaining = set(range(len(blocks)))
128    X = base.copy(); greedy = [fit_loss(X, target, y)]; accepted = []; gains = []
129    for _ in range(3):
130        scored = []
131        for j in remaining:
132            _, g, sv = residual_response(X, blocks[j], y)
133            if len(sv) and .15 <= sv[0] <= 30:
134                scored.append((g, j, float(sv[0])))
135        if not scored: break
136        g, j, s = max(scored)
137        accepted.append(j); gains.append(g)
138        X = np.column_stack([X, blocks[j]]); remaining.remove(j)
139        greedy.append(fit_loss(X, target, y))
140    return {
141        "fixed_order_selected": fixed,
142        "fixed_order_labels": [labels[j] for j in fixed],
143        "fixed_losses": fixed_losses,
144        "greedy_selected": accepted,
145        "greedy_labels": [labels[j] for j in accepted],
146        "greedy_projected_gains": gains,
147        "greedy_losses": greedy,
148        "final_loss_ratio_greedy_over_fixed": greedy[-1] / fixed_losses[-1],
149    }
150
151
152def main():
153    out = {"seed": SEED, "math_checks": math_checks(), "toy_experiment": greedy_experiment()}
154    print(json.dumps(out, indent=2))
155
156
157if __name__ == "__main__":
158    main()