Spectral quadrature features / spectral_quadrature_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3from pathlib import Path
  4
  5import numpy as np
  6
  7
  8SEED = 2977
  9
 10
 11def gaussian_kernel(X, Y=None):
 12    if Y is None:
 13        Y = X
 14    d2 = ((X[:, None, :] - Y[None, :, :]) ** 2).sum(axis=2)
 15    return np.exp(-0.5 * d2)
 16
 17
 18def hermite_rule_2d(q):
 19    # Gauss-Hermite integrates exp(-t^2) f(t); t=sqrt(2) w gives
 20    # standard-normal integration, with weights normalized to sum to one.
 21    roots, weights = np.polynomial.hermite.hermgauss(q)
 22    nodes_1d = np.sqrt(2.0) * roots
 23    weights_1d = weights / np.sqrt(np.pi)
 24    nodes = np.array([(a, b) for a in nodes_1d for b in nodes_1d])
 25    weights_2d = np.array([wa * wb for wa in weights_1d for wb in weights_1d])
 26    weights_2d /= weights_2d.sum()
 27    return nodes, weights_2d
 28
 29
 30def quadrature_features(X, q):
 31    W, a = hermite_rule_2d(q)
 32    # G(w,x)=exp(i w^T x), Z_mi=sqrt(a_i)G(w_i,x_m).
 33    Z = np.exp(1j * (X @ W.T)) * np.sqrt(a)[None, :]
 34    return Z
 35
 36
 37def random_features(X, n, rng):
 38    W = rng.normal(size=(n, X.shape[1]))
 39    Z = np.exp(1j * (X @ W.T)) / np.sqrt(n)
 40    return Z
 41
 42
 43def eig_metrics(K, Kh, top=20):
 44    lam = np.linalg.eigvalsh(K)[::-1]
 45    lhat = np.linalg.eigvalsh((Kh + Kh.conj().T) / 2.0)[::-1]
 46    j = min(top, len(lam))
 47    top_abs = float(np.mean(np.abs(lam[:j] - lhat[:j])))
 48    top_rel = float(np.mean(np.abs(lam[:j] - lhat[:j]) / np.maximum(lam[:j], 1e-12)))
 49    op = float(np.linalg.norm(K - Kh, ord=2))
 50    # Weyl's inequality is checked for every ordered eigenvalue.
 51    weyl_max = float(np.max(np.abs(lam - lhat)))
 52    return {"top20_abs_eigen_error": top_abs, "top20_relative_eigen_error": top_rel,
 53            "operator_error": op, "weyl_bound_holds": bool(weyl_max <= op + 2e-8),
 54            "largest_exact_eigenvalue": float(lam[0]), "largest_approx_eigenvalue": float(lhat[0])}
 55
 56
 57def krr_mse(Xtr, ytr, Xte, yte, Ztr, Zte, ridge=1e-3):
 58    # Complex feature-space ridge regression; same representation for both methods.
 59    A = Ztr.conj().T @ Ztr + ridge * np.eye(Ztr.shape[1])
 60    theta = np.linalg.solve(A, Ztr.conj().T @ ytr)
 61    pred = np.real(Zte @ theta)
 62    return float(np.mean((pred - yte) ** 2))
 63
 64
 65def main():
 66    rng = np.random.default_rng(SEED)
 67    # Fixed points make all methods directly comparable. A smooth target is
 68    # deliberately compatible with the RBF kernel used by the Fourier integral.
 69    X = rng.normal(size=(120, 2))
 70    K = gaussian_kernel(X)
 71    rows = []
 72    for q in (3, 4, 6, 8):
 73        Zq = quadrature_features(X, q)
 74        mq = eig_metrics(K, Zq @ Zq.conj().T)
 75        random_runs = []
 76        for s in range(20):
 77            Zr = random_features(X, q * q, np.random.default_rng(SEED + 1000 * q + s))
 78            random_runs.append(eig_metrics(K, Zr @ Zr.conj().T))
 79        rr = {key: float(np.mean([r[key] for r in random_runs]))
 80              for key in ("top20_abs_eigen_error", "top20_relative_eigen_error", "operator_error",
 81                          "largest_approximate_eigenvalue") if key in random_runs[0]}
 82        rows.append({"features": q * q, "quadrature": mq, "random_mean": rr,
 83                     "random_std_top20_relative": float(np.std([r["top20_relative_eigen_error"] for r in random_runs]))})
 84
 85    # Equal-width learning check at 64 features, with a held-out split.
 86    Xtr, Xte = X[:80], X[80:]
 87    ytr = np.sin(Xtr[:, 0]) + 0.25 * np.cos(1.5 * Xtr[:, 1])
 88    yte = np.sin(Xte[:, 0]) + 0.25 * np.cos(1.5 * Xte[:, 1])
 89    Zqtr, Zqte = quadrature_features(Xtr, 8), quadrature_features(Xte, 8)
 90    q_mse = krr_mse(Xtr, ytr, Xte, yte, Zqtr, Zqte)
 91    random_mses = []
 92    for s in range(20):
 93        # Generate the same frequencies for train and test in each replicate.
 94        W = np.random.default_rng(SEED + 7000 + s).normal(size=(64, 2))
 95        Ztr = np.exp(1j * (Xtr @ W.T)) / 8.0
 96        Zte = np.exp(1j * (Xte @ W.T)) / 8.0
 97        random_mses.append(krr_mse(Xtr, ytr, Xte, yte, Ztr, Zte))
 98
 99    # Independent cheap algebra sanity check of Z Z* and the probability rule.
100    W, a = hermite_rule_2d(4)
101    Z = np.exp(1j * (X[:12] @ W.T)) * np.sqrt(a)[None, :]
102    # Independent matrix-form evaluation of the same quadrature sum.
103    direct = np.exp(1j * (X[:12] @ W.T)) @ np.diag(a) @ np.exp(-1j * (W @ X[:12].T))
104    algebra_error = float(np.max(np.abs(Z @ Z.conj().T - direct)))
105
106    result = {"seed": SEED, "kernel": "exp(-||x-y||^2/2)", "rows": rows,
107              "learning_64_features": {"quadrature_mse": q_mse,
108                                        "random_mean_mse": float(np.mean(random_mses)),
109                                        "random_std_mse": float(np.std(random_mses))},
110              "sanity": {"weights_sum": float(a.sum()), "weights_nonnegative": bool(np.all(a >= 0)),
111                         "ZZh_identity_max_error": algebra_error}}
112    Path("results.json").write_text(json.dumps(result, indent=2))
113    print(json.dumps(result, indent=2))
114
115
116if __name__ == "__main__":
117    main()