ProCreations's picture
Claim 5: train the deep linear UFM and compute the exact Hessian spectrum (was source-reported); outlier count equals K^2 at K=2,3,4 across depths 2-3
2504006 verified
Raw
History Blame Contribute Delete
3.96 kB
"""Claim 5: for a deep linear unconstrained-features model (UFM) with K = 3
classes, the loss Hessian has K^2 = 9 outlier eigenvalues separating from the
bulk.
The logbook states the figures remain source-reported with no independent
training run. A UFM needs no external data -- the features are free variables --
so it is trained here and the exact Hessian spectrum is computed.
Deep linear UFM: logits = W_L ... W_1 H, with H the free feature matrix
(d x N) and W_l the layer weights. Loss = MSE to one-hot targets + weight decay.
The Hessian is formed exactly by autograd over ALL parameters and diagonalised.
"""
import json, numpy as np, torch
RES = {}
torch.set_default_dtype(torch.float64)
def build(K=3, n=8, d=6, depth=2, seed=0):
g = torch.Generator().manual_seed(seed)
N = K*n
Y = torch.zeros(K, N)
for c in range(K): Y[c, c*n:(c+1)*n] = 1.0
H = (torch.randn(d, N, generator=g)*0.3).requires_grad_(True)
Ws = []
dims = [K]+[d]*(depth-1)+[d]
for i in range(depth):
Ws.append((torch.randn(dims[i], dims[i+1], generator=g)*0.3).requires_grad_(True))
return H, Ws, Y
def loss_fn(H, Ws, Y, wd=5e-4):
Z = H
for W in reversed(Ws): Z = W @ Z
l = ((Z-Y)**2).mean()
for W in Ws: l = l+wd*(W**2).sum()
return l+wd*(H**2).sum()
def train(H, Ws, Y, steps=4000, lr=0.05):
params = [H]+Ws
opt = torch.optim.Adam(params, lr=lr)
for _ in range(steps):
opt.zero_grad(); l = loss_fn(H, Ws, Y); l.backward(); opt.step()
return float(l.item())
def hessian_spectrum(H, Ws, Y):
params = [H]+Ws
flat = torch.cat([p.reshape(-1) for p in params]).detach()
n = flat.numel()
def f(v):
i = 0; ps = []
for p in params:
k = p.numel(); ps.append(v[i:i+k].view_as(p)); i += k
Z = ps[0]
for W in reversed(ps[1:]): Z = W @ Z
l = ((Z-Y)**2).mean()
for W in ps[1:]: l = l+5e-4*(W**2).sum()
return l+5e-4*(ps[0]**2).sum()
Hs = torch.autograd.functional.hessian(f, flat)
ev = torch.linalg.eigvalsh((Hs+Hs.T)/2).numpy()
return np.sort(ev)[::-1]
def count_outliers(ev, K):
"""Outliers = eigenvalues above the largest gap in the top of the spectrum."""
top = ev[:4*K*K]
gaps = top[:-1]-top[1:]
j = int(np.argmax(gaps))
return j+1, float(gaps[j]), float(top[j]), float(top[j+1])
def main():
rows = []
for K in (2, 3, 4):
for depth in (2, 3):
H, Ws, Y = build(K=K, depth=depth, seed=K*10+depth)
fin = train(H, Ws, Y)
ev = hessian_spectrum(H, Ws, Y)
n_out, gap, above, below = count_outliers(ev, K)
rows.append({"K": K, "depth": depth, "final_loss": round(fin, 8),
"params": int(sum(p.numel() for p in [H]+Ws)),
"K_squared": K*K, "outliers_found": n_out,
"matches_K2": bool(n_out == K*K),
"gap_size": round(gap, 6),
"last_outlier": round(above, 6), "first_bulk": round(below, 6),
"separation_ratio": round(above/max(below, 1e-12), 2)})
print(" K=%d depth=%d loss=%.2e params=%d K^2=%d outliers found=%d match=%s gap=%.4f (%.4f -> %.4f, ratio %.1fx)"
% (K, depth, fin, rows[-1]["params"], K*K, n_out, n_out == K*K,
gap, above, below, rows[-1]["separation_ratio"]), flush=True)
RES["claim5_ufm_hessian"] = {"rows": rows,
"matches_in": sum(r["matches_K2"] for r in rows), "cells": len(rows),
"min_separation_ratio": min(r["separation_ratio"] for r in rows)}
R = RES["claim5_ufm_hessian"]
print(" outlier count equals K^2 in %d/%d cells; min separation ratio %.1fx"
% (R["matches_in"], R["cells"], R["min_separation_ratio"]), flush=True)
json.dump(RES, open("ufm_results.json", "w"), indent=1)
if __name__ == "__main__":
main(); print("DONE")