SabaPivot's picture
Upgrade canonical logbook from stronger peer evidence with attribution
ea3a71e verified
Raw
History Blame Contribute Delete
6.43 kB
"""Sharper tests for FedDPO / DecDPO convergence claims: fitted rate exponents
and parameter-free ratio checks, at larger scale than the first pass.
Log-linear DPO: reward r(x) = theta^T phi(x); on a preference pair (w, l) the
loss is -log sigmoid(beta (r_w - r_l)). Clients hold heterogeneous preference
data generated by client-specific ground-truth rewards.
"""
import json
import numpy as np
RESULTS = {}
D, BETA = 32, 1.0
def make_clients(N, kappa, seed, n_per=200):
rng = np.random.default_rng(seed)
base = rng.normal(size=D); base /= np.linalg.norm(base)
cl = []
for i in range(N):
t = base + kappa * rng.normal(size=D) / np.sqrt(D)
t /= np.linalg.norm(t)
W = rng.normal(size=(n_per, D)); L = rng.normal(size=(n_per, D))
flip = (W - L) @ t < 0
W2 = np.where(flip[:, None], L, W); L2 = np.where(flip[:, None], W, L)
cl.append((W2, L2, t))
return cl, base
def grad(th, W, L):
z = BETA * ((W - L) @ th)
s = 1.0 / (1.0 + np.exp(z))
return -BETA * ((W - L) * s[:, None]).mean(axis=0)
def loss(th, cl):
tot = 0.0
for W, L, _ in cl:
z = BETA * ((W - L) @ th)
tot += float(np.mean(np.log1p(np.exp(-z))))
return tot / len(cl)
def fed_dpo(cl, R=200, E=5, S=None, lr=0.5, q_max=0, seed=0):
N = len(cl); S = S or N
rng = np.random.default_rng(seed)
th = np.zeros(D); buf = {}
hist = []
for r in range(R):
sel = rng.choice(N, size=S, replace=False)
deltas = []
for i in sel:
local = th.copy()
for _ in range(E):
local -= lr * grad(local, cl[i][0], cl[i][1])
d = local - th
delay = int(rng.integers(0, q_max + 1)) if q_max else 0
buf.setdefault(r + delay, []).append(d)
for d in buf.pop(r, []):
deltas.append(d)
if deltas:
th = th + np.mean(deltas, axis=0)
hist.append(loss(th, cl))
return np.array(hist)
def metropolis(adj):
n = adj.shape[0]; deg = adj.sum(1)
Wm = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j and adj[i, j]:
Wm[i, j] = 1.0 / (1 + max(deg[i], deg[j]))
Wm[i, i] = 1 - Wm[i].sum()
ev = np.sort(np.abs(np.linalg.eigvals(Wm)))[::-1]
return Wm, float(ev[1])
def dec_dpo(cl, Wm, R=200, E=5, lr=0.5):
N = len(cl)
TH = np.zeros((N, D)); hist = []
for r 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
hist.append(float(np.mean(np.linalg.norm(TH - TH.mean(0), axis=1))))
return np.array(hist)
def claim2_participation():
rows = []
N = 20
cl, _ = make_clients(N, 0.8, seed=1)
for S in (2, 5, 10, 20):
finals = [fed_dpo(cl, R=150, S=S, seed=100 + s)[-20:].mean() for s in range(5)]
var = [np.var(fed_dpo(cl, R=150, S=S, seed=200 + s)[-20:]) for s in range(5)]
rows.append({"S": S, "N": N, "final_loss": float(np.mean(finals)),
"tail_variance": float(np.mean(var)),
"one_over_S": 1.0 / S})
print(" S=%-3d final loss=%.6f tail var=%.3e (1/S=%.3f)" %
(S, rows[-1]["final_loss"], rows[-1]["tail_variance"], 1.0 / S), flush=True)
ls = np.log([r["one_over_S"] for r in rows]); lv = np.log([max(r["tail_variance"], 1e-16) for r in rows])
RESULTS["claim2_participation"] = {
"rows": rows, "loglog_slope_var_vs_1_over_S": round(float(np.polyfit(ls, lv, 1)[0]), 4),
"var_ratio_S2_over_SN": round(rows[0]["tail_variance"] / max(rows[-1]["tail_variance"], 1e-16), 2)}
print(" variance slope vs 1/S = %.3f ; S=2 vs S=N ratio = %.1fx" %
(RESULTS["claim2_participation"]["loglog_slope_var_vs_1_over_S"],
RESULTS["claim2_participation"]["var_ratio_S2_over_SN"]), flush=True)
def claim3_staleness():
rows = []
cl, _ = make_clients(10, 0.8, seed=3)
for q in (0, 1, 2, 5, 10):
f = [fed_dpo(cl, R=150, q_max=q, seed=300 + s)[-20:].mean() for s in range(5)]
rows.append({"q_max": q, "final_loss": float(np.mean(f)),
"sd": float(np.std(f))})
print(" q_max=%-3d final loss=%.6f +- %.6f" % (q, rows[-1]["final_loss"], rows[-1]["sd"]), flush=True)
base = rows[0]["final_loss"]
RESULTS["claim3_staleness"] = {
"rows": rows, "monotone_in_q": all(rows[i+1]["final_loss"] >= rows[i]["final_loss"] - 1e-9
for i in range(len(rows) - 1)),
"penalty_at_qmax10": round(rows[-1]["final_loss"] - base, 6)}
def claim5_topology():
N = 8
cl, _ = make_clients(N, 0.8, seed=5)
tops = {}
ring = np.zeros((N, N), int)
for i in range(N):
ring[i, (i + 1) % N] = ring[(i + 1) % N, i] = 1
tops["ring"] = ring
star = np.zeros((N, N), int); star[0, 1:] = star[1:, 0] = 1
tops["star"] = star
full = np.ones((N, N), int) - np.eye(N, dtype=int)
tops["complete"] = full
path = np.zeros((N, N), int)
for i in range(N - 1):
path[i, i + 1] = path[i + 1, i] = 1
tops["path"] = path
rows = []
for name, adj in tops.items():
Wm, rho = metropolis(adj)
h = dec_dpo(cl, Wm, R=120)
rows.append({"topology": name, "rho": round(rho, 4),
"one_over_1_minus_rho2": round(1.0 / (1 - rho ** 2), 3),
"final_consensus_error": float(h[-1]),
"mean_tail_consensus": float(h[-20:].mean())})
print(" %-9s rho=%.4f 1/(1-rho^2)=%8.2f consensus err=%.4e" %
(name, rho, rows[-1]["one_over_1_minus_rho2"], rows[-1]["mean_tail_consensus"]), flush=True)
x = np.log([r["one_over_1_minus_rho2"] for r in rows])
y = np.log([max(r["mean_tail_consensus"], 1e-16) for r in rows])
sl, ic = np.polyfit(x, y, 1)
r2 = 1 - np.var(y - (sl * x + ic)) / np.var(y)
RESULTS["claim5_topology"] = {"rows": rows,
"loglog_slope_consensus_vs_1_over_1_minus_rho2": round(float(sl), 4),
"r2": round(float(r2), 4)}
print(" consensus error vs 1/(1-rho^2): slope %.3f, R2 %.3f" % (sl, r2), flush=True)
if __name__ == "__main__":
claim2_participation(); claim3_staleness(); claim5_topology()
json.dump(RESULTS, open("dpo_results.json", "w"), indent=1)