File size: 5,724 Bytes
e812d9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env python3
"""Bayesian linear regression reproduction of Table 1 of the SGLRW paper.

Setup taken verbatim from sections/F_experiments.tex of arXiv 2602.15925v1:
N=1000, d=20, theta* ~ N(0,I), sigma^2=1.5, tau=1e-2, 2000 parallel particles,
10000 iterations, decaying schedule delta_t = delta_0 (1+t)^-0.55, minibatch B.

Samplers: SGLD, Clipped-SGLD (R = sqrt(2 delta_t), componentwise, drift only),
SGLRW (Definition 4.2 with the implementation clipping of sqrt(delta_t/2)|grad|
to 1 described at the end of Section 4).

Metric: KL( N(mu,Sigma) || N(mu_hat, Sigma_hat) ) between the analytic posterior
and the empirical Gaussian fit of the 2000 final particles (the paper's
"KL divergence between the true posterior and the empirical Gaussian fit").
"""
from __future__ import annotations
import argparse, json, sys, time
import numpy as np

N, D, SIG2, TAU = 1000, 20, 1.5, 1e-2
PARTICLES, ITERS = 2000, 10000


def make_problem(seed, design="gaussian"):
    rng = np.random.default_rng(1000 + seed)
    if design == "gaussian":
        X = rng.standard_normal((N, D))
    elif design == "uniform":
        X = rng.uniform(-np.sqrt(3.0), np.sqrt(3.0), size=(N, D))
    elif design == "correlated":
        A = rng.standard_normal((D, D)) / np.sqrt(D)
        X = rng.standard_normal((N, D)) @ (np.eye(D) * 0.7 + A * 0.7)
    elif design == "illcond":
        s = np.geomspace(1.0, 0.1, D)
        X = rng.standard_normal((N, D)) * s
    else:
        raise ValueError(design)
    theta_star = rng.standard_normal(D)
    y = X @ theta_star + np.sqrt(SIG2) * rng.standard_normal(N)
    P = X.T @ X / SIG2 + TAU * np.eye(D)
    Sig = np.linalg.inv(P)
    mu = Sig @ (X.T @ y) / SIG2
    return X, y, mu, Sig


def kl_true_vs_fit(mu, Sig, samples):
    """KL( N(mu,Sig) || N(mu_hat,Sig_hat) )."""
    if not np.all(np.isfinite(samples)):
        return float("inf")
    mh = samples.mean(0)
    Sh = np.cov(samples.T)
    sign, ldh = np.linalg.slogdet(Sh)
    if sign <= 0:
        return float("inf")
    ish = np.linalg.inv(Sh)
    diff = mh - mu
    val = 0.5 * (np.trace(ish @ Sig) + diff @ ish @ diff - D + ldh - np.linalg.slogdet(Sig)[1])
    return float(val)


def run(sampler, B, delta0, seed, design="gaussian", iters=ITERS, particles=PARTICLES,
        schedule="decay", init="prior"):
    X, y, mu, Sig = make_problem(seed, design)
    rng = np.random.default_rng(90000 + seed * 17 + hash(sampler) % 1000)
    if init == "prior":
        theta = rng.standard_normal((particles, D))
    elif init == "zero":
        theta = np.zeros((particles, D))
    else:
        theta = mu + rng.standard_normal((particles, D)) * 0.01
    scale = (N / B) / SIG2
    for t in range(iters):
        dt = delta0 * (1.0 + t) ** (-0.55) if schedule == "decay" else delta0
        idx = rng.integers(0, N, size=(particles, B))
        g = TAU * theta
        for b in range(B):
            Xb = X[idx[:, b]]
            r = np.einsum("pd,pd->p", Xb, theta) - y[idx[:, b]]
            g += Xb * (r * scale)[:, None]
        if sampler == "sgld":
            theta = theta - dt * g + np.sqrt(2 * dt) * rng.standard_normal((particles, D))
        elif sampler == "clipped_sgld":
            R = np.sqrt(2 * dt)
            upd = np.clip(dt * g, -R, R)
            theta = theta - upd + R * rng.standard_normal((particles, D))
        elif sampler == "sglrw":
            u = np.clip(np.sqrt(dt / 2.0) * g, -1.0, 1.0)
            pplus = 0.5 - 0.5 * u          # P[S_i = +sqrt(2 dt)]
            s = np.where(rng.random((particles, D)) < pplus, 1.0, -1.0)
            theta = theta + np.sqrt(2 * dt) * s
        elif sampler == "sglrw_unclipped":
            u = np.sqrt(dt / 2.0) * g
            pplus = np.clip(0.5 - 0.5 * u, 0.0, 1.0)
            s = np.where(rng.random((particles, D)) < pplus, 1.0, -1.0)
            theta = theta + np.sqrt(2 * dt) * s
        else:
            raise ValueError(sampler)
        if not np.all(np.isfinite(theta)):
            return {"kl": float("inf"), "diverged_at": t, "sampler": sampler, "B": B,
                    "delta0": delta0, "seed": seed, "design": design}
    kl = kl_true_vs_fit(mu, Sig, theta)
    mh = theta.mean(0)
    Sh = np.cov(theta.T)
    return {"kl": kl, "diverged_at": None, "sampler": sampler, "B": B, "delta0": delta0,
            "seed": seed, "design": design, "schedule": schedule, "init": init,
            "iters": iters, "particles": particles,
            "mean_var_ratio": float(np.mean(np.diag(Sh) / np.diag(Sig))),
            "mean_shift_norm": float(np.linalg.norm(mh - mu)),
            "post_sd_mean": float(np.mean(np.sqrt(np.diag(Sig))))}


def mc_reference(seed, design="gaussian", particles=PARTICLES):
    X, y, mu, Sig = make_problem(seed, design)
    rng = np.random.default_rng(555 + seed)
    L = np.linalg.cholesky(Sig)
    smp = mu + rng.standard_normal((particles, D)) @ L.T
    return kl_true_vs_fit(mu, Sig, smp)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--jobs", required=True, help="json list of job dicts")
    ap.add_argument("--out", required=True)
    a = ap.parse_args()
    jobs = json.loads(a.jobs)
    out = []
    for j in jobs:
        t0 = time.time()
        if j.get("mc_reference"):
            r = {"mc_reference_kl": mc_reference(j["seed"], j.get("design", "gaussian")),
                 "seed": j["seed"], "design": j.get("design", "gaussian")}
        else:
            r = run(**{k: v for k, v in j.items() if k != "tag"})
        r["seconds"] = round(time.time() - t0, 2)
        r["tag"] = j.get("tag", "")
        out.append(r)
        print(json.dumps(r), flush=True)
    with open(a.out, "w") as f:
        json.dump(out, f, indent=1)


if __name__ == "__main__":
    main()