SabaPivot's picture
Upgrade canonical logbook from stronger peer evidence with attribution
ea3a71e verified
Raw
History Blame Contribute Delete
4.65 kB
"""Redesigned tests: remove the two confounds in the first pass.
Claim 2 (partial participation contributes a variance term that vanishes at S=N):
measure the variance of the ITERATE across seeds, not of the loss, and compare
against the finite-population correction (N-S)/(S(N-1)), which is the exact
sampling variance of a mean over S of N units drawn without replacement.
S=N is reported separately as an exact-zero check (no fit).
Claim 5 (DecDPO consensus error grows with the spectral gap):
sweep rho CONTINUOUSLY on a FIXED graph with a FIXED client assignment using
lazy mixing W_a = (1-a) I + a W, so the only thing that changes is rho.
Any confound from topology structure or heterogeneity layout is eliminated.
"""
import json
import numpy as np
from dpo_exp import make_clients, grad, metropolis, D
RESULTS = {}
def fed_iterate(cl, R, S, lr=0.5, E=5, seed=0):
N = len(cl); rng = np.random.default_rng(seed)
th = np.zeros(D)
for _ in range(R):
sel = rng.choice(N, size=S, replace=False)
ds = []
for i in sel:
loc = th.copy()
for _ in range(E):
loc -= lr * grad(loc, cl[i][0], cl[i][1])
ds.append(loc - th)
th = th + np.mean(ds, axis=0)
return th
def claim2():
N, R = 20, 120
cl, _ = make_clients(N, 0.8, seed=1)
rows = []
for S in (2, 4, 5, 10, 20):
TH = np.array([fed_iterate(cl, R, S, seed=1000 + s) for s in range(24)])
var = float(np.trace(np.cov(TH.T)))
fpc = (N - S) / (S * (N - 1))
rows.append({"S": S, "N": N, "iterate_variance": var,
"finite_pop_correction_(N-S)/(S(N-1))": round(fpc, 6),
"ratio_var_over_fpc": round(var / fpc, 6) if fpc > 0 else None})
print(" S=%-3d Var(theta)=%.4e (N-S)/(S(N-1))=%.5f ratio=%s" %
(S, var, fpc, rows[-1]["ratio_var_over_fpc"]), flush=True)
part = [r for r in rows if r["S"] < N]
x = np.log([r["finite_pop_correction_(N-S)/(S(N-1))"] for r in part])
y = np.log([r["iterate_variance"] for r in part])
sl, ic = np.polyfit(x, y, 1)
r2 = 1 - np.var(y - (sl * x + ic)) / np.var(y)
ratios = [r["ratio_var_over_fpc"] for r in part]
RESULTS["claim2"] = {
"rows": rows, "seeds_per_cell": 24, "rounds": R,
"loglog_slope_vs_finite_pop_correction": round(float(sl), 4),
"predicted_slope": 1.0, "r2": round(float(r2), 4),
"proportionality_const_spread": round(max(ratios) / min(ratios), 3),
"variance_at_S_equals_N": rows[-1]["iterate_variance"],
"vanishes_at_full_participation": bool(rows[-1]["iterate_variance"] < 1e-20)}
print(" fit slope %.3f (predicted 1.0), R2 %.3f ; const spread %.2fx ; Var(S=N)=%.2e" %
(sl, r2, RESULTS["claim2"]["proportionality_const_spread"],
rows[-1]["iterate_variance"]), flush=True)
def dec_consensus(cl, Wm, R=200, E=5, lr=0.5):
N = len(cl); TH = np.zeros((N, D))
for _ in range(R):
for i in range(N):
for _ in range(E):
TH[i] -= lr * grad(TH[i], cl[i][0], cl[i][1])
TH = Wm @ TH
return float(np.mean(np.linalg.norm(TH - TH.mean(0), axis=1)))
def claim5():
N = 8
cl, _ = make_clients(N, 0.8, seed=5)
ring = np.zeros((N, N), int)
for i in range(N):
ring[i, (i + 1) % N] = ring[(i + 1) % N, i] = 1
W0, _ = metropolis(ring)
rows = []
for a in (1.0, 0.8, 0.6, 0.45, 0.3, 0.2, 0.12):
Wm = (1 - a) * np.eye(N) + a * W0
rho = float(np.sort(np.abs(np.linalg.eigvals(Wm)))[::-1][1])
err = dec_consensus(cl, Wm, R=250)
rows.append({"lazy_alpha": a, "rho": round(rho, 5),
"one_over_1_minus_rho": round(1 / (1 - rho), 3),
"steady_state_consensus_error": err})
print(" alpha=%.2f rho=%.5f 1/(1-rho)=%7.2f consensus err=%.5e" %
(a, rho, rows[-1]["one_over_1_minus_rho"], err), flush=True)
x = np.log([r["one_over_1_minus_rho"] for r in rows])
y = np.log([r["steady_state_consensus_error"] for r in rows])
sl, ic = np.polyfit(x, y, 1)
r2 = 1 - np.var(y - (sl * x + ic)) / np.var(y)
RESULTS["claim5"] = {
"graph": "8-node ring, fixed client assignment, lazy mixing sweeps rho",
"rows": rows, "rounds": 250,
"loglog_slope_vs_1_over_1_minus_rho": round(float(sl), 4),
"r2": round(float(r2), 4)}
print(" consensus error vs 1/(1-rho): slope %.3f, R2 %.4f" % (sl, r2), flush=True)
if __name__ == "__main__":
claim2(); claim5()
json.dump(RESULTS, open("dpo_results2.json", "w"), indent=1)