Integral Sparse Dynamics Training / integral_sparse_dynamics.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import os
  4import random
  5import numpy as np
  6from sklearn.metrics import f1_score
  7
  8SEED = 2057
  9rng = np.random.default_rng(SEED)
 10
 11
 12def group_prox(B, lr_lam):
 13    # B shape [outputs, regulators]; one regulator is one regulator-to-state group.
 14    norms = np.linalg.norm(B, axis=0, keepdims=True)
 15    return B * np.maximum(0.0, 1.0 - lr_lam / (norms + 1e-12))
 16
 17
 18def fit_group(X, Y, lam=0.02, epochs=500, lr=None):
 19    # Minimize mean squared multivariate regression loss + group lasso.
 20    X = np.asarray(X); Y = np.asarray(Y)
 21    n, p = X.shape
 22    if lr is None:
 23        lr = 0.8 / (np.linalg.norm(X, 2) ** 2 / max(n, 1) + 1e-8)
 24    W = np.zeros((p, Y.shape[1]))
 25    # Work with rows as regulator groups.
 26    for _ in range(epochs):
 27        G = (X.T @ (X @ W - Y)) / n
 28        W -= lr * G
 29        W = group_prox(W.T, lr * lam).T
 30    return W
 31
 32
 33def make_data(n=5000, d=8, q=12, dt_choices=(0.05, 0.1, 0.2), sigma=0.05,
 34              rho=0.0, seed=0):
 35    rg = np.random.default_rng(seed)
 36    active = np.array([0, 3, 7])
 37    B = np.zeros((q, d))
 38    # correlated regulator features, with a controllable active Gram condition.
 39    z = rg.normal(size=(n, 1))
 40    R = rg.normal(size=(n, q))
 41    for j in active[1:]:
 42        R[:, j] = rho * R[:, 0] + math.sqrt(max(1-rho*rho, 0)) * R[:, j]
 43    B[active] = rg.normal(0, 0.65, size=(len(active), d))
 44    dt = rg.choice(dt_choices, size=n)
 45    # latent increments from the integral equation, noisy observations at endpoints.
 46    clean = (dt[:, None] * (R @ B))
 47    eps0 = rg.normal(0, sigma, size=(n, d))
 48    eps1 = rg.normal(0, sigma, size=(n, d))
 49    y_int = clean + eps1 - eps0
 50    # Derivative target is exactly the same noisy increment divided by dt.
 51    y_fd = y_int / dt[:, None]
 52    return R, dt, y_int, y_fd, B, active
 53
 54
 55def noise_scaling():
 56    # Directly test the two claims: Var(delta x noise)=2 sigma^2 and
 57    # Var((delta x)/dt noise)=2 sigma^2/dt^2.
 58    sigma = 0.07
 59    out = []
 60    for dt in [0.025, 0.05, 0.1, 0.2, 0.4]:
 61        rg = np.random.default_rng(1000 + int(dt * 10000))
 62        e = rg.normal(0, sigma, size=(250000, 1))
 63        f = rg.normal(0, sigma, size=(250000, 1))
 64        inc = (f-e).ravel()
 65        fd = inc / dt
 66        vi, vf = float(np.var(inc)), float(np.var(fd))
 67        out.append({"dt": dt, "integral_var": vi, "integral_pred": 2*sigma*sigma,
 68                    "fd_var": vf, "fd_pred": 2*sigma*sigma/(dt*dt),
 69                    "fd_over_integral": vf/vi, "ratio_pred": 1/(dt*dt)})
 70    return out
 71
 72
 73def support_sweep():
 74    # Prediction: as rho -> 1, lambda_min(active Gram) -> 0 and support recovery degrades.
 75    rows = []
 76    for rho in [0.0, 0.5, 0.8, 0.95, 0.99]:
 77        R, dt, yi, yf, B, active = make_data(n=7000, sigma=0.045, rho=rho, seed=20+int(rho*100))
 78        # Integral formulation uses integrated features dt*r and increments.
 79        Xi = R * dt[:, None]
 80        W = fit_group(Xi, yi, lam=0.018, epochs=350)
 81        # FD formulation uses r and derivative targets; same equal-weight objective.
 82        Wfd = fit_group(R, yf, lam=0.018, epochs=350)
 83        gram = (R[:, active].T @ R[:, active]) / len(R)
 84        eig = float(np.linalg.eigvalsh(gram).min())
 85        def score(w):
 86            pred = np.linalg.norm(w, axis=1) > 0.06
 87            truth = np.zeros(w.shape[0], dtype=bool); truth[active] = True
 88            return float(f1_score(truth, pred)), int(pred.sum())
 89        fi, ni = score(W); ff, nf = score(Wfd)
 90        rows.append({"rho": rho, "lambda_min": eig, "integral_f1": fi,
 91                     "fd_f1": ff, "integral_edges": ni, "fd_edges": nf})
 92    return rows
 93
 94
 95def method_comparison():
 96    # Irregular intervals: integrated loss naturally weights observations by physical
 97    # increment size, while FD noise is amplified on the short intervals.
 98    R, dt, yi, yf, B, active = make_data(n=12000, sigma=0.06,
 99                                          dt_choices=(0.025, 0.05, 0.1, 0.2), rho=0.5, seed=77)
100    Xi = R * dt[:, None]
101    Wi = fit_group(Xi, yi, lam=0.0008, epochs=450)
102    Wf = fit_group(R, yf, lam=0.02, epochs=450)
103    # Evaluate physical increment prediction on fresh noiseless transitions.
104    R2, dt2, y2, yf2, _, _ = make_data(n=4000, sigma=0.0,
105                                         dt_choices=(0.025, 0.05, 0.1, 0.2), rho=0.5, seed=91)
106    true_inc = dt2[:, None] * (R2 @ B)
107    pi = dt2[:, None] * (R2 @ Wi)
108    pf = dt2[:, None] * (R2 @ Wf)
109    def f1(w):
110        pred = np.linalg.norm(w, axis=1) > 0.06
111        truth = np.zeros(w.shape[0], dtype=bool); truth[active] = True
112        return float(f1_score(truth, pred))
113    return {"integral_clean_increment_mse": float(np.mean((pi-true_inc)**2)),
114            "finite_difference_clean_increment_mse": float(np.mean((pf-true_inc)**2)),
115            "integral_support_f1": f1(Wi), "finite_difference_support_f1": f1(Wf),
116            "active_edges": active.tolist()}
117
118
119if __name__ == "__main__":
120    result = {"seed": SEED,
121              "prediction_1_noise_scaling": noise_scaling(),
122              "prediction_2_conditioning_support": support_sweep(),
123              "mini_experiment": method_comparison()}
124    with open("results.json", "w") as f:
125        json.dump(result, f, indent=2)
126    print(json.dumps(result, indent=2))