Rank-Adaptive Tensor-Train MLP / tt_mlp_experiment.py
Mechanism failed
1import json, math, random, time
2import numpy as np
3import torch
4import torch.nn as nn
5import torch.nn.functional as F
6
7SEED = 605
8np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
9
10
11def tt_svd_matrix(W, modes, rel_tol=0.0, max_rank=None):
12 """TT-SVD of W with C-order input/output mode ordering."""
13 m, n = modes
14 d = len(m)
15 T = W.reshape(*(list(m) + list(n)))
16 # Convert to interleaved physical ordering (m1,n1,m2,n2,...).
17 perm = [k for pair in zip(range(d), range(d, 2*d)) for k in pair]
18 T = T.transpose(perm)
19 norm = np.linalg.norm(W)
20 eps2 = (rel_tol * norm) ** 2
21 # Equal per-bond budget gives the standard global error bound.
22 budget = eps2 / max(1, d - 1)
23 cores, ranks, tails = [], [1], []
24 left = T
25 rprev = 1
26 for k in range(d - 1):
27 left = left.reshape(rprev * m[k] * n[k], -1)
28 u, s, vh = np.linalg.svd(left, full_matrices=False)
29 tail = np.cumsum(s[::-1] ** 2)[::-1]
30 rank = len(s)
31 if rel_tol > 0:
32 valid = np.where(np.r_[tail[1:], 0.0] <= budget + 1e-14)[0]
33 if len(valid): rank = int(valid[0] + 1)
34 if max_rank is not None: rank = min(rank, max_rank)
35 rank = max(1, rank)
36 discarded = float(np.sum(s[rank:] ** 2))
37 tails.append(discarded)
38 cores.append(u[:, :rank].reshape(rprev, m[k], n[k], rank))
39 left = (s[:rank, None] * vh[:rank])
40 rprev = rank; ranks.append(rank)
41 cores.append(left.reshape(rprev, m[-1], n[-1], 1))
42 ranks.append(1)
43 return cores, ranks, tails, norm
44
45
46def tt_reconstruct(cores):
47 x = cores[0]
48 for c in cores[1:]:
49 x = np.tensordot(x, c, axes=([-1], [0]))
50 # x: m1,n1,m2,n2,..., with singleton bond ends removed
51 d = len(cores)
52 x = np.squeeze(x, axis=(0, -1))
53 shape = x.shape
54 out_modes = [shape[2*k] for k in range(d)]
55 in_modes = [shape[2*k+1] for k in range(d)]
56 perm = list(range(0, 2*d, 2)) + list(range(1, 2*d, 2))
57 return x.transpose(perm).reshape(int(np.prod(out_modes)), int(np.prod(in_modes)))
58
59
60def tt_param_count(modes, ranks):
61 m, n = modes
62 return sum(ranks[k] * m[k] * n[k] * ranks[k+1] for k in range(len(m)))
63
64
65class TTLinear(nn.Module):
66 def __init__(self, W, modes, rel_tol=0.0, max_rank=None, bias=True):
67 super().__init__(); self.modes = modes; self.max_rank = max_rank
68 cores, ranks, _, _ = tt_svd_matrix(W, modes, rel_tol, max_rank)
69 self.cores = nn.ParameterList([nn.Parameter(torch.tensor(c, dtype=torch.float32)) for c in cores])
70 self.ranks = ranks
71 self.bias = nn.Parameter(torch.zeros(W.shape[0])) if bias else None
72
73 def forward(self, x):
74 # Direct tensor-network contraction: batch, all input modes, TT cores.
75 d = len(self.modes[0])
76 letters = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
77 out = letters[1:1+d]; inn = letters[1+d:1+2*d]
78 bond = letters[1+2*d:1+3*d+1] # includes both singleton boundary bonds
79 xsub = letters[0] + ''.join(inn)
80 terms = [xsub]
81 for k, c in enumerate(self.cores):
82 terms.append(bond[k] + out[k] + inn[k] + bond[k+1])
83 equation = ','.join(terms) + '->' + letters[0] + ''.join(out)
84 operands = [x.reshape(x.shape[0], *self.modes[1])] + list(self.cores)
85 y = torch.einsum(equation, *operands).reshape(x.shape[0], -1)
86 return y + self.bias if self.bias is not None else y
87
88 @torch.no_grad()
89 def round(self, rel_tol):
90 W = tt_reconstruct([c.detach().cpu().numpy() for c in self.cores])
91 cores, ranks, _, _ = tt_svd_matrix(W, self.modes, rel_tol, self.max_rank)
92 self.cores = nn.ParameterList([nn.Parameter(torch.tensor(c, device=self.cores[0].device, dtype=self.cores[0].dtype)) for c in cores])
93 self.ranks = ranks
94
95
96def verify():
97 modes = ([4,4,4], [4,4,4]); W = np.random.randn(64,64)
98 cores, ranks, tails, norm = tt_svd_matrix(W, modes, rel_tol=0.08, max_rank=8)
99 Wa = tt_reconstruct(cores); err = np.linalg.norm(W-Wa)
100 # Uncapped TT-SVD tests the stated tolerance theorem directly.
101 exact_cores, exact_ranks, exact_tails, _ = tt_svd_matrix(W, modes, rel_tol=0.08, max_rank=None)
102 exact_err = np.linalg.norm(W - tt_reconstruct(exact_cores))
103 bound = 0.08 * norm
104 # contraction equality check on a random vector
105 layer = TTLinear(W, modes, rel_tol=0.08, max_rank=8, bias=False)
106 x = torch.randn(3,64)
107 direct = layer(x).detach().numpy(); expected = x.numpy() @ Wa.T
108 return {'relative_fro_error': float(err/norm), 'requested_bound': float(bound/norm),
109 'bound_holds_with_cap': bool(err <= bound + 1e-6),
110 'uncapped_relative_fro_error': float(exact_err/norm),
111 'uncapped_bound_holds': bool(exact_err <= bound + 1e-6),
112 'contraction_max_abs': float(np.max(np.abs(direct-expected))),
113 'ranks': ranks, 'uncapped_ranks': exact_ranks, 'dense_params': 4096,
114 'tt_params': tt_param_count(modes, ranks), 'uncapped_tt_params': tt_param_count(modes, exact_ranks),
115 'tail_squared_sum': float(sum(tails))}
116
117
118def train_compare():
119 device = 'cuda' if torch.cuda.is_available() else 'cpu'
120 try:
121 torch.manual_seed(SEED)
122 modes = ([4,4,4], [4,4,4]); dim=64
123 # Teacher is genuinely TT-low-rank, making this a representation test.
124 teacher = TTLinear(np.random.randn(dim,dim)*0.25, modes, rel_tol=0, max_rank=2, bias=True).to(device)
125 with torch.no_grad(): teacher.bias.normal_(0, .1)
126 X = torch.randn(1024, dim, device=device); Y = torch.tanh(teacher(X)).detach()
127 results = {}
128 for name, model in [('dense', nn.Sequential(nn.Linear(dim,dim), nn.Tanh()).to(device)),
129 ('tt', None)]:
130 if model is None:
131 model = nn.Sequential(TTLinear(np.random.randn(dim,dim)*.05, modes, rel_tol=0, max_rank=4).to(device), nn.Tanh())
132 opt = torch.optim.Adam(model.parameters(), lr=3e-3)
133 t0=time.perf_counter(); losses=[]
134 for step in range(250):
135 pred=model(X); loss=F.mse_loss(pred,Y); opt.zero_grad(); loss.backward(); opt.step()
136 if name=='tt' and (step+1)%50==0: model[0].round(0.03)
137 losses.append(float(loss.detach().cpu()))
138 results[name]={'final_mse':losses[-1], 'mse_step_50':losses[49], 'seconds':time.perf_counter()-t0,
139 'params':sum(p.numel() for p in model.parameters()),
140 'ranks': getattr(model[0], 'ranks', None) if name=='tt' else None}
141 return {'device':device, 'results':results}
142 except Exception as e:
143 if device == 'cuda':
144 torch.cuda.empty_cache(); return train_compare_cpu()
145 raise
146
147def train_compare_cpu():
148 torch.cuda.is_available=lambda: False
149 return train_compare()
150
151if __name__ == '__main__':
152 print(json.dumps({'verification':verify(), 'training':train_compare()}, indent=2))