Dyadic Resolvent Filter Layer / experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math, time
  2import numpy as np
  3from scipy.linalg import eigh
  4
  5SEED = 1226
  6
  7
  8def unitary(A, t):
  9    w, V = eigh(A)
 10    return (V * np.exp(-1j * t * w)) @ V.T
 11
 12
 13def dyadic_exact(A, lam, K):
 14    n = A.shape[0]
 15    I = np.eye(n, dtype=complex)
 16    R = 1j * np.linalg.inv(I - math.exp(-lam) * unitary(A, 1.0))
 17    for k in range(1, K + 1):
 18        t = 2.0 ** (-k)
 19        R -= 1j * t * np.linalg.inv(I + math.exp(-lam * t) * unitary(A, t))
 20    return R
 21
 22
 23def dyadic_neumann(A, X, lam, K, J):
 24    Y = np.zeros_like(X, dtype=complex)
 25    U = unitary(A, 1.0)
 26    Z = X.astype(complex)
 27    for j in range(J + 1):
 28        Y += 1j * math.exp(-j * lam) * Z
 29        Z = U @ Z
 30    for k in range(1, K + 1):
 31        t = 2.0 ** (-k)
 32        c = math.exp(-lam * t)
 33        U = unitary(A, t)
 34        Z = X.astype(complex)
 35        for j in range(J + 1):
 36            Y -= 1j * t * ((-c) ** j) * Z
 37            Z = U @ Z
 38    return Y
 39
 40
 41def scalar_verification():
 42    vals = np.linspace(-2.0, 2.0, 17)
 43    A = np.diag(vals)
 44    out = {}
 45    for lam in [0.2, 0.5, 1.0, 2.0]:
 46        target = np.diag(1.0 / (vals - 1j * lam))
 47        errs = []
 48        for K in [2, 4, 6, 8, 10, 12]:
 49            errs.append(float(np.max(np.abs(dyadic_exact(A, lam, K) - target))))
 50        # Exact resolvent tail is bounded by sum_{k>K} 2^-k = 2^-K,
 51        # since ||(I+cU)^-1|| <= 1/(1-c) is not uniformly useful; empirically
 52        # the measured tail should decrease with K and remain below a constant*2^-K.
 53        ratios = [errs[i] / errs[i + 1] for i in range(len(errs)-1)]
 54        # Neumann prediction: at scale t, remainder is at most c^(J+1)/(1-c).
 55        nerrs = []
 56        for J in [1, 2, 4, 8, 12, 16]:
 57            X = np.eye(len(vals))
 58            nerrs.append(float(np.max(np.abs(dyadic_neumann(A, X, lam, 10, J) - dyadic_exact(A, lam, 10)))))
 59        # Unitary norm prediction over random vectors and times.
 60        x = np.random.default_rng(7).normal(size=len(vals))
 61        norm_errors = [abs(np.linalg.norm(unitary(A, t) @ x) / np.linalg.norm(x) - 1) for t in [1, .5, .125, .01]]
 62        out[str(lam)] = {"K": [2,4,6,8,10,12], "exact_errors": errs,
 63                         "K_error_ratios": ratios, "J": [1,2,4,8,12,16],
 64                         "neumann_errors": nerrs, "unitary_relative_norm_errors": norm_errors}
 65    return out
 66
 67
 68def graph_data(n=80, p_in=.16, p_out=.035, d=8):
 69    rng = np.random.default_rng(SEED)
 70    y = np.repeat([0, 1], n // 2)
 71    G = np.zeros((n, n), float)
 72    for i in range(n):
 73        for j in range(i+1, n):
 74            p = p_in if y[i] == y[j] else p_out
 75            if rng.random() < p: G[i,j] = G[j,i] = 1
 76    deg = G.sum(1)
 77    L = np.eye(n) - (G / np.sqrt(np.maximum(deg, 1)[:,None] * np.maximum(deg, 1)[None,:]))
 78    # Features include weak signal; held-out nodes use same fixed feature matrix.
 79    X = rng.normal(0, 1, (n, d))
 80    X[:, 0] += (2*y-1) * .65
 81    perm = rng.permutation(n)
 82    tr, va = perm[:n//2], perm[n//2:]
 83    return L, X, y, tr, va
 84
 85
 86def train_models():
 87    # Tiny ridge classifier compares standard low-degree polynomial filtering to dyadic filtering.
 88    L, X, y, tr, va = graph_data()
 89    # Rescale normalized Laplacian to [-1,1] (spectrum is approximately [0,2]).
 90    A = L - np.eye(len(L))
 91    rng = np.random.default_rng(SEED)
 92    results = {}
 93    configs = [("baseline_poly", 6), ("dyadic_K2_J2", 6), ("dyadic_K4_J2", 6)]
 94    for name, budget in configs:
 95        t0 = time.perf_counter()
 96        if name == "baseline_poly":
 97            Z = X.astype(complex); F = [Z]
 98            U = A @ Z
 99            for _ in range(budget):
100                F.append(U); U = A @ U
101            H = np.concatenate([z.real for z in F], axis=1)
102        else:
103            K = int(name.split('K')[1].split('_')[0]); J = int(name.split('J')[1])
104            Hc = dyadic_neumann(A, X, .8, K, J)
105            H = np.concatenate([X, Hc.real, Hc.imag], axis=1)
106        # closed-form ridge, no training randomness
107        lam = 1.0
108        W = np.linalg.solve(H[tr].T @ H[tr] + lam*np.eye(H.shape[1]), H[tr].T @ (2*y[tr]-1))
109        pred = (H @ W > 0).astype(int)
110        acc = float(np.mean(pred[va] == y[va]))
111        norm = float(np.linalg.norm(H) / np.linalg.norm(X))
112        results[name] = {"val_accuracy": acc, "feature_norm_ratio": norm,
113                         "seconds": time.perf_counter()-t0, "sparse_op_budget": budget}
114    return results
115
116
117def main():
118    np.random.seed(SEED)
119    report = {"seed": SEED, "verification": scalar_verification(), "mini_experiment": train_models()}
120    with open("results.json", "w") as f: json.dump(report, f, indent=2)
121    print(json.dumps(report, indent=2))
122
123if __name__ == "__main__": main()