AbstractPhil commited on
Commit
6824384
·
verified ·
1 Parent(s): 4dff406

Create 111m_prototype_v4_200x24_svd_eigh_kl_div.py

Browse files
111m_prototype_v4_200x24_svd_eigh_kl_div.py ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SVAE - V=1024, D=24 (Validated Binding Constant)
3
+ ==================================================
4
+ V=1024, D=24 -> CV=0.2916 (from sweep, confirmed)
5
+
6
+ Deep encoder/decoder for 1024x24 = 24,576 matrix.
7
+ Light KL on spectral shape (don't constrain magnitude).
8
+ Row CV should be ~0.29 by dimensional law.
9
+
10
+ pip install "git+https://github.com/AbstractEyes/geolip-core.git"
11
+ """
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+ import torchvision
17
+ import torchvision.transforms as T
18
+ import math
19
+
20
+ try:
21
+ from geolip_core.linalg import svd as geolip_svd
22
+ from geolip_core.linalg.eigh import FLEigh
23
+ HAS_GEOLIP = True
24
+ print("Using geolip-core FLEigh (Faddeev-LeVerrier pipeline)")
25
+ except ImportError:
26
+ HAS_GEOLIP = False
27
+ print("geolip-core not found, fallback to torch.svd_lowrank")
28
+
29
+
30
+ # -- CM monitoring --
31
+
32
+ def cayley_menger_vol2(points):
33
+ B, N, D = points.shape
34
+ gram = torch.bmm(points, points.transpose(1, 2))
35
+ norms = torch.diagonal(gram, dim1=1, dim2=2)
36
+ d2 = F.relu(norms.unsqueeze(2) + norms.unsqueeze(1) - 2 * gram)
37
+ cm = torch.zeros(B, N + 1, N + 1, device=points.device, dtype=points.dtype)
38
+ cm[:, 0, 1:] = 1.0
39
+ cm[:, 1:, 0] = 1.0
40
+ cm[:, 1:, 1:] = d2
41
+ k = N - 1
42
+ sign = (-1.0) ** (k + 1)
43
+ fact = math.factorial(k)
44
+ return sign * torch.linalg.det(cm.float()).to(points.dtype) / ((2 ** k) * (fact ** 2))
45
+
46
+
47
+ def cv_of(emb, n_samples=200):
48
+ if emb.dim() != 2 or emb.shape[0] < 5:
49
+ return 0.0
50
+ N, D = emb.shape
51
+ pool = min(N, 512)
52
+ indices = torch.stack([torch.randperm(pool, device=emb.device)[:5] for _ in range(n_samples)])
53
+ vol2 = cayley_menger_vol2(emb[:pool][indices])
54
+ valid = vol2 > 1e-20
55
+ if valid.sum() < 10:
56
+ return 0.0
57
+ vols = vol2[valid].sqrt()
58
+ return (vols.std() / (vols.mean() + 1e-8)).item()
59
+
60
+
61
+ def safe_gram_svd(M, use_fast=False):
62
+ """
63
+ use_fast=False: torch.svd_lowrank (randomized, always converges, slower)
64
+ use_fast=True: Gram + eigh in fp64 (deterministic, fast, needs conditioning)
65
+ """
66
+ orig_dtype = M.dtype
67
+ if use_fast:
68
+ # Optimized: Gram + eigh in fp64
69
+ A = M.double()
70
+ G = torch.bmm(A.transpose(1, 2), A)
71
+ eigenvalues, V = torch.linalg.eigh(G)
72
+ eigenvalues = eigenvalues.flip(-1)
73
+ V = V.flip(-1)
74
+ S = torch.sqrt(eigenvalues.clamp(min=1e-24))
75
+ U = torch.bmm(A, V) / S.unsqueeze(1).clamp(min=1e-16)
76
+ Vh = V.transpose(-2, -1).contiguous()
77
+ return U.to(orig_dtype), S.to(orig_dtype), Vh.to(orig_dtype)
78
+ else:
79
+ # Stable: randomized, always converges
80
+ U, S, V = torch.svd_lowrank(M.float(), q=M.shape[-1], niter=4)
81
+ Vh = V.transpose(1, 2)
82
+ return U.to(orig_dtype), S.to(orig_dtype), Vh.to(orig_dtype)
83
+
84
+
85
+ BINDING_CONSTANT = 0.29154
86
+
87
+
88
+ # -- Data --
89
+
90
+ def get_cifar10(batch_size=256):
91
+ transform = T.Compose([
92
+ T.ToTensor(),
93
+ T.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
94
+ ])
95
+ train_ds = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
96
+ test_ds = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
97
+ train_loader = torch.utils.data.DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2)
98
+ test_loader = torch.utils.data.DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=2)
99
+ return train_loader, test_loader
100
+
101
+
102
+ # -- SVAE --
103
+
104
+ class SVAE(nn.Module):
105
+ def __init__(self, matrix_v=200, D=24):
106
+ super().__init__()
107
+ self.matrix_v = matrix_v
108
+ self.D = D
109
+ self.img_dim = 3 * 32 * 32
110
+ self.mat_dim = matrix_v * D # 200*24 = 4,800
111
+
112
+ # Gradual expansion: 3072 -> 512 -> 1024 -> 4800 (max 4.7x per step)
113
+ self.encoder = nn.Sequential(
114
+ nn.Linear(self.img_dim, 512),
115
+ nn.GELU(),
116
+ nn.Linear(512, 1024),
117
+ nn.GELU(),
118
+ nn.Linear(1024, self.mat_dim),
119
+ )
120
+ self.decoder = nn.Sequential(
121
+ nn.Linear(self.mat_dim, 1024),
122
+ nn.GELU(),
123
+ nn.Linear(1024, 512),
124
+ nn.GELU(),
125
+ nn.Linear(512, self.img_dim),
126
+ )
127
+
128
+ # Spectral log-variance (shape regularization only)
129
+ self.logvar_head = nn.Sequential(
130
+ nn.Linear(1024, 128), # tap from encoder hidden layer
131
+ nn.GELU(),
132
+ nn.Linear(128, D),
133
+ )
134
+ # Init logvar to small values so reparameterization starts gentle
135
+ nn.init.zeros_(self.logvar_head[-1].weight)
136
+ nn.init.constant_(self.logvar_head[-1].bias, -5.0)
137
+
138
+ # Prior: SHAPE only, not magnitude
139
+ # Normalized decay from 1.0 to ~0.14 in log space
140
+ # The prior says "S should decay smoothly" not "S should be small"
141
+ self.register_buffer('prior_log_mu', torch.linspace(0, -2, D))
142
+ self.register_buffer('prior_log_var', torch.ones(D)) # wide prior (var=e^1 ~2.7)
143
+
144
+ # Orthogonal init on last encoder layer for well-conditioned initial matrices
145
+ nn.init.orthogonal_(self.encoder[-1].weight)
146
+
147
+ self.use_fast_svd = False # switched during training
148
+
149
+ def encode(self, images):
150
+ B = images.shape[0]
151
+ flat = images.reshape(B, -1)
152
+
153
+ # Run encoder with hidden tap for logvar
154
+ h1 = F.gelu(self.encoder[0](flat)) # 3072 -> 512
155
+ h2 = F.gelu(self.encoder[2](h1)) # 512 -> 1024
156
+ mat_flat = self.encoder[4](h2) # 1024 -> mat_dim
157
+ M = mat_flat.reshape(B, self.matrix_v, self.D)
158
+
159
+ U, S, Vh = safe_gram_svd(M, use_fast=self.use_fast_svd)
160
+
161
+ # Log-variance from hidden (not full mat_dim - too expensive)
162
+ log_var = self.logvar_head(h2)
163
+
164
+ # Reparameterize on NORMALIZED spectrum (shape, not magnitude)
165
+ if self.training:
166
+ S_norm = S / (S[:, 0:1] + 1e-8) # normalize by S[0]
167
+ log_S_norm = torch.log(S_norm.clamp(min=1e-8))
168
+ std = torch.exp(0.5 * log_var)
169
+ eps = torch.randn_like(std)
170
+ S_norm_sampled = torch.exp(log_S_norm + std * eps)
171
+ S_norm_sampled, _ = S_norm_sampled.sort(dim=-1, descending=True)
172
+ # Denormalize back
173
+ S_sampled = S_norm_sampled * S[:, 0:1]
174
+ else:
175
+ S_sampled = S
176
+ S_norm = S / (S[:, 0:1] + 1e-8)
177
+
178
+ return {
179
+ 'U': U, 'S': S, 'S_sampled': S_sampled, 'Vt': Vh,
180
+ 'S_norm': S / (S[:, 0:1] + 1e-8),
181
+ 'M': M, 'log_var': log_var,
182
+ }
183
+
184
+ def decode_from_svd(self, U, S, Vt):
185
+ B = U.shape[0]
186
+ M_hat = torch.bmm(U * S.unsqueeze(1), Vt)
187
+ return self.decoder(M_hat.reshape(B, -1)).reshape(B, 3, 32, 32)
188
+
189
+ def spectral_kl(self, S_norm, log_var):
190
+ """KL on NORMALIZED spectrum shape. Magnitude-free."""
191
+ log_S = torch.log(S_norm.clamp(min=1e-8))
192
+ mu_q = log_S
193
+ var_q = torch.exp(log_var)
194
+ mu_p = self.prior_log_mu.unsqueeze(0)
195
+ var_p = torch.exp(self.prior_log_var).unsqueeze(0)
196
+ kl = 0.5 * (var_q / var_p + (mu_p - mu_q).pow(2) / var_p
197
+ - 1 + torch.log(var_p / (var_q + 1e-8)))
198
+ return kl.sum(dim=-1).mean()
199
+
200
+ def forward(self, images):
201
+ svd = self.encode(images)
202
+ if self.use_fast_svd:
203
+ # Post-warmup: decode from sampled S, compute KL
204
+ recon = self.decode_from_svd(svd['U'], svd['S_sampled'], svd['Vt'])
205
+ kl = self.spectral_kl(svd['S_norm'], svd['log_var'])
206
+ else:
207
+ # Warmup: pure reconstruction, no KL noise
208
+ recon = self.decode_from_svd(svd['U'], svd['S'], svd['Vt'])
209
+ kl = torch.tensor(0.0, device=images.device)
210
+ return {'recon': recon, 'svd': svd, 'kl': kl}
211
+
212
+ @staticmethod
213
+ def effective_rank(S):
214
+ p = S / (S.sum(-1, keepdim=True) + 1e-8)
215
+ p = p.clamp(min=1e-8)
216
+ return (-(p * p.log()).sum(-1)).exp()
217
+
218
+
219
+ # -- Training --
220
+
221
+ def train(epochs=50, lr=1e-3, kl_weight=0.001, warmup_epochs=5, device='cuda'):
222
+ device = torch.device(device if torch.cuda.is_available() else 'cpu')
223
+ train_loader, test_loader = get_cifar10(batch_size=256)
224
+
225
+ model = SVAE(matrix_v=200, D=24).to(device)
226
+ opt = torch.optim.Adam(model.parameters(), lr=lr)
227
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
228
+
229
+ total_params = sum(p.numel() for p in model.parameters())
230
+ print(f"SVAE - V=200, D=24 (Validated: CV=0.2914)")
231
+ print(f" Matrix: (200, 24) = 4,800 elements")
232
+ print(f" Phase 1 (ep 1-{warmup_epochs}): randomized SVD, recon only")
233
+ print(f" Phase 2 (ep {warmup_epochs+1}+): fp64 Gram+eigh, recon + KL(w={kl_weight})")
234
+ print(f" Params: {total_params:,}")
235
+ print("=" * 95)
236
+ print(f"{'ep':>3} | {'loss':>7} {'recon':>7} {'kl':>7} {'t/ep':>5} | "
237
+ f"{'t_rec':>7} | "
238
+ f"{'S0':>7} {'SD':>6} {'ratio':>5} {'erank':>5} | "
239
+ f"{'row_cv':>7} {'svd':>5}")
240
+ print("-" * 95)
241
+
242
+ import time
243
+
244
+ for epoch in range(1, epochs + 1):
245
+ # SVD switch: randomized warmup -> optimized
246
+ if epoch == warmup_epochs + 1:
247
+ model.use_fast_svd = True
248
+ print(f" >>> Switching to fp64 Gram+eigh SVD <<<")
249
+
250
+ model.train()
251
+ total_loss, total_recon, total_kl, n = 0, 0, 0, 0
252
+ t0 = time.time()
253
+
254
+ for images, labels in train_loader:
255
+ images = images.to(device)
256
+ opt.zero_grad()
257
+ out = model(images)
258
+
259
+ recon_loss = F.mse_loss(out['recon'], images)
260
+ kl = out['kl']
261
+ loss = recon_loss + kl_weight * kl
262
+ loss.backward()
263
+ opt.step()
264
+
265
+ total_loss += loss.item() * len(images)
266
+ total_recon += recon_loss.item() * len(images)
267
+ total_kl += kl.item() * len(images)
268
+ n += len(images)
269
+
270
+ sched.step()
271
+ epoch_time = time.time() - t0
272
+
273
+ if epoch % 2 == 0 or epoch <= 3 or epoch == warmup_epochs + 1:
274
+ model.eval()
275
+ test_recon, test_n = 0, 0
276
+ test_S, test_erank = None, 0
277
+ row_cvs = []
278
+ nb = 0
279
+
280
+ with torch.no_grad():
281
+ for images, labels in test_loader:
282
+ images = images.to(device)
283
+ out = model(images)
284
+ test_recon += F.mse_loss(out['recon'], images).item() * len(images)
285
+ test_n += len(images)
286
+ test_erank += model.effective_rank(out['svd']['S']).mean().item()
287
+
288
+ if nb < 3:
289
+ for b in range(min(4, len(images))):
290
+ row_cvs.append(cv_of(out['svd']['M'][b]))
291
+
292
+ if test_S is None:
293
+ test_S = out['svd']['S'].mean(0).cpu()
294
+ else:
295
+ test_S += out['svd']['S'].mean(0).cpu()
296
+ nb += 1
297
+
298
+ test_erank /= nb
299
+ test_S /= nb
300
+ ratio = (test_S[0] / (test_S[-1] + 1e-8)).item()
301
+ mean_cv = sum(row_cvs) / len(row_cvs) if row_cvs else 0
302
+ svd_tag = "FAST" if model.use_fast_svd else "rand"
303
+
304
+ print(f"{epoch:3d} | {total_loss/n:7.4f} {total_recon/n:7.4f} "
305
+ f"{total_kl/n:7.3f} {epoch_time:5.1f} | "
306
+ f"{test_recon/test_n:7.4f} | "
307
+ f"{test_S[0]:7.2f} {test_S[-1]:6.3f} {ratio:5.2f} "
308
+ f"{test_erank:5.2f} | "
309
+ f"{mean_cv:7.4f} {svd_tag:>5}")
310
+
311
+ # -- Final Analysis --
312
+ print()
313
+ print("=" * 90)
314
+ print("FINAL ANALYSIS")
315
+ print("=" * 90)
316
+
317
+ model.eval()
318
+ all_S, all_recon_err, all_labels = [], [], []
319
+ all_row_cvs = []
320
+
321
+ with torch.no_grad():
322
+ for images, labels in test_loader:
323
+ images = images.to(device)
324
+ out = model(images)
325
+ all_S.append(out['svd']['S'].cpu())
326
+ all_recon_err.append(
327
+ F.mse_loss(out['recon'], images, reduction='none')
328
+ .mean(dim=(1, 2, 3)).cpu())
329
+ all_labels.append(labels.cpu())
330
+ for b in range(min(4, len(images))):
331
+ all_row_cvs.append(cv_of(out['svd']['M'][b]))
332
+
333
+ all_S = torch.cat(all_S)
334
+ all_recon_err = torch.cat(all_recon_err)
335
+ all_labels = torch.cat(all_labels)
336
+ erank = model.effective_rank(all_S)
337
+ mean_cv = sum(all_row_cvs) / len(all_row_cvs)
338
+
339
+ print(f"\n V=1024, D=24 (validated CV=0.2916)")
340
+ print(f" Recon MSE: {all_recon_err.mean():.6f} +/- {all_recon_err.std():.6f}")
341
+ print(f" Effective rank: {erank.mean():.2f} +/- {erank.std():.2f}")
342
+ print(f" Row CV: {mean_cv:.4f} (target: {BINDING_CONSTANT}, delta: {abs(mean_cv - BINDING_CONSTANT):.4f})")
343
+
344
+ # Spectrum
345
+ S_mean = all_S.mean(0)
346
+ S_norm = S_mean / (S_mean[0] + 1e-8)
347
+ total_energy = (S_mean ** 2).sum()
348
+ print(f"\n Singular value profile (raw and normalized):")
349
+ cumulative = 0
350
+ for i in range(len(S_mean)):
351
+ e = (S_mean[i] ** 2).item()
352
+ cumulative += e
353
+ pct = cumulative / total_energy * 100
354
+ bar = "#" * int(S_norm[i].item() * 30)
355
+ print(f" S[{i:2d}]: {S_mean[i]:8.3f} norm={S_norm[i]:.4f} cum={pct:5.1f}% {bar}")
356
+
357
+ # Per-class
358
+ cifar_names = ['plane', 'car', 'bird', 'cat', 'deer',
359
+ 'dog', 'frog', 'horse', 'ship', 'truck']
360
+ print(f"\n Per-class:")
361
+ print(f" {'cls':>6} {'recon':>8} {'erank':>6} {'S0':>7} {'SD':>7} {'ratio':>6}")
362
+ for c in range(10):
363
+ mask = all_labels == c
364
+ rc = all_recon_err[mask].mean().item()
365
+ er = erank[mask].mean().item()
366
+ s0 = all_S[mask, 0].mean().item()
367
+ sd = all_S[mask, -1].mean().item()
368
+ r = s0 / (sd + 1e-8)
369
+ print(f" {cifar_names[c]:>6} {rc:8.6f} {er:6.2f} {s0:7.3f} {sd:7.3f} {r:6.2f}")
370
+
371
+ # -- Recon grid --
372
+ print(f"\n Saving reconstruction grid...")
373
+ import matplotlib
374
+ matplotlib.use('Agg')
375
+ import matplotlib.pyplot as plt
376
+
377
+ mean_t = torch.tensor([0.4914, 0.4822, 0.4465]).reshape(1, 3, 1, 1).to(device)
378
+ std_t = torch.tensor([0.2470, 0.2435, 0.2616]).reshape(1, 3, 1, 1).to(device)
379
+
380
+ model.eval()
381
+ with torch.no_grad():
382
+ images, labels = next(iter(test_loader))
383
+ images = images.to(device)
384
+ out = model(images)
385
+
386
+ selected_idx = []
387
+ for c in range(10):
388
+ class_idx = (labels == c).nonzero(as_tuple=True)[0]
389
+ selected_idx.extend(class_idx[:2].tolist())
390
+
391
+ orig = images[selected_idx]
392
+ U = out['svd']['U'][selected_idx]
393
+ S = out['svd']['S'][selected_idx]
394
+ Vt = out['svd']['Vt'][selected_idx]
395
+
396
+ mode_counts = [1, 4, 8, 16, 24]
397
+ prog_recons = []
398
+ for nm in mode_counts:
399
+ r = model.decode_from_svd(U[:, :, :nm], S[:, :nm], Vt[:, :nm, :])
400
+ prog_recons.append(r)
401
+
402
+ def denorm(t):
403
+ return (t * std_t + mean_t).clamp(0, 1).cpu()
404
+
405
+ n_samples = len(selected_idx)
406
+ n_cols = 2 + len(mode_counts)
407
+ fig, axes = plt.subplots(n_samples, n_cols, figsize=(n_cols * 1.5, n_samples * 1.5))
408
+ col_titles = ['Original'] + [f'{m} modes' for m in mode_counts] + ['|Err|x5']
409
+
410
+ for i in range(n_samples):
411
+ axes[i, 0].imshow(denorm(orig[i:i+1])[0].permute(1, 2, 0).numpy())
412
+ for j, r in enumerate(prog_recons):
413
+ axes[i, j+1].imshow(denorm(r[i:i+1])[0].permute(1, 2, 0).numpy())
414
+ err_col = 1 + len(prog_recons)
415
+ diff = (denorm(orig[i:i+1]) - denorm(prog_recons[-1][i:i+1])).abs() * 5
416
+ axes[i, err_col].imshow(diff.clamp(0, 1)[0].permute(1, 2, 0).numpy())
417
+ c = labels[selected_idx[i]].item()
418
+ axes[i, 0].set_ylabel(cifar_names[c], fontsize=8, rotation=0, labelpad=35)
419
+
420
+ for j, title in enumerate(col_titles):
421
+ axes[0, j].set_title(title, fontsize=8)
422
+ for ax in axes.flat:
423
+ ax.axis('off')
424
+
425
+ plt.tight_layout()
426
+ plt.savefig('/content/svae_recon_grid.png', dpi=200, bbox_inches='tight')
427
+ print(f" Saved to /content/svae_recon_grid.png")
428
+ try:
429
+ plt.show()
430
+ except:
431
+ pass
432
+ plt.close()
433
+
434
+
435
+ if __name__ == "__main__":
436
+ train()