Spanning-Tree Connectivity Loss / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, time
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6import torch.nn.functional as F
  7
  8SEEDS = [0, 1, 2]
  9N = 12
 10
 11def laplacian(w):
 12    return torch.diag(w.sum(-1)) - w
 13
 14def tree_loss(w):
 15    n = w.shape[-1]
 16    L = laplacian(w)
 17    sign, ld = torch.linalg.slogdet(L + torch.ones_like(L) / n)
 18    if sign.item() <= 0:
 19        raise RuntimeError('non-positive shifted Laplacian')
 20    return -ld + math.log(n)
 21
 22def spectral_metrics(w):
 23    a = np.asarray(w)
 24    L = np.diag(a.sum(1)) - a
 25    ev = np.linalg.eigvalsh(L)
 26    return {
 27        'pseudo_det': float(np.prod(ev[1:])),
 28        'fiedler': float(ev[1]),
 29        'components': int(np.sum(ev < 1e-7)),
 30        'eigenvalues': ev.tolist(),
 31    }
 32
 33def math_check():
 34    rng = np.random.default_rng(4)
 35    x = rng.uniform(.1, 1.2, (N, N))
 36    w = (x + x.T) / 2
 37    np.fill_diagonal(w, 0)
 38    L = np.diag(w.sum(1)) - w
 39    ev = np.linalg.eigvalsh(L)
 40    pdet = float(np.prod(ev[1:]))
 41    rankone_det = float(np.linalg.det(L + np.ones((N, N)) / N))
 42    cofactor = float(np.linalg.det(L[:-1, :-1]))
 43    eps = 1e-4
 44    derivative = (np.linalg.det(L + eps*np.eye(N)) - np.linalg.det(L - eps*np.eye(N))) / (2*eps)
 45    wt = torch.tensor(w, dtype=torch.double, requires_grad=True)
 46    tl = tree_loss(wt)
 47    tl.backward()
 48    return {
 49        'pseudo_det_vs_rank_one_det_relerr': abs(pdet-rankone_det)/pdet,
 50        'matrix_tree_pdet_over_N_vs_cofactor_relerr': abs(pdet/N-cofactor)/cofactor,
 51        'derivative_formula_relerr': abs(derivative-pdet)/pdet,
 52        'tree_loss_vs_stated_formula_abs': abs(float(tl)-(-math.log(pdet)+math.log(N))),
 53        'autograd_gradient_norm': float(wt.grad.norm()),
 54    }
 55
 56class EdgeLearner(nn.Module):
 57    def __init__(self, n):
 58        super().__init__()
 59        self.logits = nn.Parameter(torch.randn(n, n) * .15)
 60    def weights(self):
 61        s = (self.logits + self.logits.T) / 2
 62        w = F.softplus(s)
 63        return w * (1 - torch.eye(w.shape[0], device=w.device))
 64
 65def make_target(seed):
 66    rng = np.random.default_rng(seed)
 67    # Two-community target: the learning objective must preserve task-relevant structure.
 68    y = np.zeros((N, N), dtype=np.float32)
 69    groups = np.arange(N) < N//2
 70    for i in range(N):
 71        for j in range(i):
 72            base = 1.0 if groups[i] == groups[j] else .08
 73            y[i,j] = y[j,i] = base + rng.normal(0, .025)
 74    np.fill_diagonal(y, 0)
 75    return torch.tensor(y)
 76
 77def run_one(seed, alpha, device):
 78    torch.manual_seed(seed); np.random.seed(seed)
 79    target = make_target(seed).to(device)
 80    model = EdgeLearner(N).to(device)
 81    opt = torch.optim.Adam(model.parameters(), lr=.08)
 82    losses=[]; t0=time.perf_counter()
 83    for step in range(250):
 84        w = model.weights()
 85        fit = ((w-target)**2).mean()
 86        reg = tree_loss(w)
 87        loss = fit + alpha*reg
 88        opt.zero_grad(); loss.backward(); opt.step()
 89        losses.append(float(loss.detach().cpu()))
 90    elapsed=time.perf_counter()-t0
 91    w=model.weights().detach().cpu().numpy()
 92    met=spectral_metrics(w)
 93    met.update({'fit_mse': float(((torch.tensor(w)-target.cpu())**2).mean()),
 94                'final_loss': losses[-1], 'loss_std_last50': float(np.std(losses[-50:])),
 95                'seconds': elapsed})
 96    return met
 97
 98def main():
 99    device='cuda' if torch.cuda.is_available() else 'cpu'
100    try:
101        # Probe CUDA and fall back on any runtime/device error.
102        if device == 'cuda': torch.zeros(1, device='cuda').sum().item()
103    except Exception:
104        device='cpu'
105    result={'device':device, 'math_check':math_check(), 'runs':{}}
106    for alpha in [0.0, 1e-3, 1e-2]:
107        key='baseline' if alpha==0 else 'tree_alpha_'+str(alpha)
108        vals=[]
109        for seed in SEEDS:
110            try: vals.append(run_one(seed, alpha, device))
111            except Exception:
112                if device=='cuda':
113                    device='cpu'; result['device']='cpu'; vals.append(run_one(seed, alpha, device))
114                else: raise
115        result['runs'][key]=vals
116    for key, vals in result['runs'].items():
117        result['summary_'+key]={k:float(np.mean([v[k] for v in vals])) for k in ['fit_mse','fiedler','components','pseudo_det','loss_std_last50','seconds']}
118    Path('results.json').write_text(json.dumps(result, indent=2))
119    print(json.dumps(result, indent=2))
120
121if __name__=='__main__': main()