Wide-tree invariant alignment layer / tree_alignment.py

Failed on benchmark

Raw ⬇ ZIP
 1import json
 2import time
 3from pathlib import Path
 4import numpy as np
 5from scipy.optimize import linear_sum_assignment
 6
 7
 8def standardize_features(F):
 9    F = np.asarray(F, dtype=np.float64)
10    F = F - F.mean(axis=0, keepdims=True)
11    s = F.std(axis=0, keepdims=True)
12    s[s < 1e-12] = 1.0
13    return F / s
14
15
16def cosine_score(X, Y):
17    Xn = X / np.maximum(np.linalg.norm(X, axis=1, keepdims=True), 1e-12)
18    Yn = Y / np.maximum(np.linalg.norm(Y, axis=1, keepdims=True), 1e-12)
19    return Xn @ Yn.T
20
21
22def tree_features(X, max_depth=5, branches=(2, 3, 4), normalize=True):
23    """Finite rooted-tree contractions computed from the sample Gram matrix.
24
25    A coordinate vertex of degree two contracts to A=XX^T/d. Repeated A
26    multiplication gives rooted path contractions; powers of A1 represent
27    wide rooted trees with repeated branches. Thus all features are exactly
28    invariant under right multiplication X -> XQ for orthogonal Q.
29    """
30    X = np.asarray(X, dtype=np.float64)
31    A = X @ X.T / max(X.shape[1], 1)
32    one = np.ones((X.shape[0], 1), dtype=np.float64)
33    messages, v = [], one
34    for _ in range(max_depth):
35        v = A @ v
36        messages.append(v[:, 0])
37    root = (A @ one)[:, 0]
38    for b in branches:
39        messages.append(root ** b)
40    F = np.stack(messages, axis=1)
41    return standardize_features(F) if normalize else F
42
43
44def tree_score(X, Y):
45    FX, FY = tree_features(X), tree_features(Y)
46    return FX @ FY.T / FX.shape[1]
47
48
49def hungarian_accuracy(score, truth=None):
50    r, c = linear_sum_assignment(-score)
51    if truth is None:
52        truth = np.arange(score.shape[0])
53    return float(np.mean(c == np.asarray(truth)[r]))
54
55
56def random_orthogonal(d, rng):
57    q, r = np.linalg.qr(rng.normal(size=(d, d)))
58    q *= np.sign(np.diag(r))[None, :]
59    return q
60
61
62def make_pair(n, d, rho, rng):
63    X = rng.normal(size=(n, d))
64    Q = random_orthogonal(d, rng)
65    # Y has row-wise correlation rho with the rotated matched X.
66    Y = rho * (X @ Q) + np.sqrt(max(1 - rho*rho, 0.0)) * rng.normal(size=(n, d))
67    return X, Y
68
69
70def run(seed=7, n=96, d=64, rhos=(0.55, 0.65, 0.75, 0.85), repeats=8):
71    rng = np.random.default_rng(seed)
72    invariant_errors, rows = [], []
73    X = rng.normal(size=(n, d)); Q = random_orthogonal(d, rng)
74    f0 = tree_features(X, normalize=False)
75    f1 = tree_features(X @ Q, normalize=False)
76    invariant_errors.append(float(np.max(np.abs(f0-f1)) / max(np.max(np.abs(f0)), 1e-12)))
77    for rho in rhos:
78        ca, ta = [], []
79        for _ in range(repeats):
80            X, Y = make_pair(n, d, rho, rng)
81            t = time.perf_counter(); cs = cosine_score(X, Y); tc = time.perf_counter()-t
82            t = time.perf_counter(); ts = tree_score(X, Y); tt = time.perf_counter()-t
83            ca.append(hungarian_accuracy(cs)); ta.append(hungarian_accuracy(ts))
84        rows.append({'rho': rho, 'cosine_mean': float(np.mean(ca)), 'cosine_std': float(np.std(ca)),
85                     'tree_mean': float(np.mean(ta)), 'tree_std': float(np.std(ta)),
86                     'cosine_sec': tc, 'tree_sec': tt})
87    out = {'invariance_relative_max_error': invariant_errors[0], 'rows': rows}
88    Path('results.json').write_text(json.dumps(out, indent=2))
89    print(json.dumps(out, indent=2))
90    return out
91
92
93if __name__ == '__main__':
94    run()