tekkmaven commited on
Commit
64c6923
·
verified ·
1 Parent(s): 68fb45b

Upload representation_tracker.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. representation_tracker.py +279 -0
representation_tracker.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Representation Tracking Toolkit
3
+ ================================
4
+ Tools for measuring how neural network internal representations change during training.
5
+ Implements CKA, SVCCA, subspace angles, gradient alignment, attention entropy,
6
+ and representation variance explained — all GPU-accelerated.
7
+
8
+ Based on:
9
+ - Kornblith et al. 2019 (CKA): arxiv.org/abs/1905.00414
10
+ - Raghu et al. 2017 (SVCCA): arxiv.org/abs/1706.05806
11
+ - Laitinen 2026 (mechanistic forgetting): arxiv.org/abs/2601.18699
12
+ - Lampinen et al. 2024 (representation bias): arxiv.org/abs/2405.05847
13
+ """
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+ import numpy as np
18
+ from typing import Dict, List, Optional, Tuple
19
+ from collections import defaultdict
20
+
21
+
22
+ # ============================================================
23
+ # CKA — Centered Kernel Alignment
24
+ # ============================================================
25
+
26
+ def centering(K: torch.Tensor) -> torch.Tensor:
27
+ """Apply centering matrix H = I - (1/n)·11^T to kernel matrix K."""
28
+ n = K.shape[0]
29
+ unit = torch.ones(n, n, device=K.device, dtype=K.dtype) / n
30
+ return K - unit @ K - K @ unit + unit @ K @ unit
31
+
32
+
33
+ def linear_HSIC(X: torch.Tensor, Y: torch.Tensor) -> torch.Tensor:
34
+ """Hilbert-Schmidt Independence Criterion with linear kernel."""
35
+ n = X.shape[0]
36
+ K = X @ X.T
37
+ L = Y @ Y.T
38
+ Kc = centering(K)
39
+ Lc = centering(L)
40
+ return (Kc * Lc).sum() / ((n - 1) ** 2)
41
+
42
+
43
+ def linear_CKA(X: torch.Tensor, Y: torch.Tensor) -> float:
44
+ """
45
+ Linear CKA between activation matrices X [n_samples, d1] and Y [n_samples, d2].
46
+ Returns scalar in [0, 1]; 1 = identical representational structure.
47
+ """
48
+ hsic_xy = linear_HSIC(X, Y)
49
+ hsic_xx = linear_HSIC(X, X)
50
+ hsic_yy = linear_HSIC(Y, Y)
51
+ denom = (hsic_xx.sqrt() * hsic_yy.sqrt()).clamp(min=1e-10)
52
+ return (hsic_xy / denom).item()
53
+
54
+
55
+ def cka_heatmap(hidden_states_a: List[torch.Tensor],
56
+ hidden_states_b: List[torch.Tensor]) -> np.ndarray:
57
+ """
58
+ Compute CKA between all layer pairs of two model states.
59
+ hidden_states_a/b: list of [n_samples, d_model] tensors per layer.
60
+ Returns: [n_layers, n_layers] numpy array.
61
+ """
62
+ n = len(hidden_states_a)
63
+ m = len(hidden_states_b)
64
+ heatmap = np.zeros((n, m))
65
+ for i in range(n):
66
+ for j in range(m):
67
+ heatmap[i, j] = linear_CKA(hidden_states_a[i], hidden_states_b[j])
68
+ return heatmap
69
+
70
+
71
+ # ============================================================
72
+ # SVCCA — Singular Vector CCA
73
+ # ============================================================
74
+
75
+ def svcca(X: torch.Tensor, Y: torch.Tensor, threshold: float = 0.99) -> float:
76
+ """
77
+ SVCCA similarity. SVD to truncate dimensions, then CCA.
78
+ Returns mean canonical correlation in [0, 1].
79
+ """
80
+ def truncate_svd(Z, thr):
81
+ Z_c = Z - Z.mean(0)
82
+ U, S, Vh = torch.linalg.svd(Z_c, full_matrices=False)
83
+ var_explained = (S ** 2).cumsum(0) / (S ** 2).sum()
84
+ k = max(1, (var_explained < thr).sum().item() + 1)
85
+ return U[:, :k] * S[:k]
86
+
87
+ Xr = truncate_svd(X, threshold)
88
+ Yr = truncate_svd(Y, threshold)
89
+
90
+ n = Xr.shape[0]
91
+ eps = 1e-6
92
+ Cxx = Xr.T @ Xr / (n - 1) + eps * torch.eye(Xr.shape[1], device=X.device)
93
+ Cyy = Yr.T @ Yr / (n - 1) + eps * torch.eye(Yr.shape[1], device=Y.device)
94
+ Cxy = Xr.T @ Yr / (n - 1)
95
+
96
+ try:
97
+ Cxx_inv_sqrt = torch.linalg.inv(torch.linalg.cholesky(Cxx))
98
+ Cyy_inv_sqrt = torch.linalg.inv(torch.linalg.cholesky(Cyy))
99
+ M = Cxx_inv_sqrt.T @ Cxy @ Cyy_inv_sqrt
100
+ S = torch.linalg.svdvals(M)
101
+ return S.clamp(0, 1).mean().item()
102
+ except Exception:
103
+ # Fallback: just use CKA
104
+ return linear_CKA(X, Y)
105
+
106
+
107
+ # ============================================================
108
+ # Principal Subspace Angles
109
+ # ============================================================
110
+
111
+ def subspace_angles(X: torch.Tensor, Y: torch.Tensor,
112
+ k: int = 10) -> torch.Tensor:
113
+ """
114
+ Principal angles between top-k PCA subspaces of X and Y.
115
+ Returns angles in radians, shape [min(k, available_dims)].
116
+ 0 = identical subspaces, π/2 = orthogonal.
117
+ """
118
+ def top_k_basis(Z, k):
119
+ Z_c = Z - Z.mean(0)
120
+ _, _, Vh = torch.linalg.svd(Z_c, full_matrices=False)
121
+ actual_k = min(k, Vh.shape[0])
122
+ return Vh[:actual_k].T # [d, actual_k]
123
+
124
+ Qx = top_k_basis(X, k)
125
+ Qy = top_k_basis(Y, k)
126
+ # Ensure compatible dimensions
127
+ min_k = min(Qx.shape[1], Qy.shape[1])
128
+ Qx = Qx[:, :min_k]
129
+ Qy = Qy[:, :min_k]
130
+
131
+ M = Qx.T @ Qy
132
+ svals = torch.linalg.svdvals(M).clamp(-1, 1)
133
+ return torch.arccos(svals)
134
+
135
+
136
+ def mean_subspace_angle_degrees(X: torch.Tensor, Y: torch.Tensor,
137
+ k: int = 10) -> float:
138
+ """Mean principal subspace angle in degrees."""
139
+ angles = subspace_angles(X, Y, k)
140
+ return (angles.mean() * 180 / torch.pi).item()
141
+
142
+
143
+ # ============================================================
144
+ # Gradient Alignment
145
+ # ============================================================
146
+
147
+ def gradient_alignment(model, batch_a, batch_b, loss_fn) -> float:
148
+ """
149
+ Cosine similarity between gradient vectors for two different batches.
150
+ Positive = cooperative gradients, Negative = interfering gradients.
151
+ From Laitinen 2026: r=0.87 correlation with forgetting severity.
152
+ """
153
+ model.zero_grad()
154
+ loss_a = loss_fn(model, batch_a)
155
+ loss_a.backward()
156
+ grad_a = torch.cat([p.grad.flatten() for p in model.parameters()
157
+ if p.grad is not None]).clone()
158
+
159
+ model.zero_grad()
160
+ loss_b = loss_fn(model, batch_b)
161
+ loss_b.backward()
162
+ grad_b = torch.cat([p.grad.flatten() for p in model.parameters()
163
+ if p.grad is not None]).clone()
164
+
165
+ model.zero_grad()
166
+ return F.cosine_similarity(grad_a.unsqueeze(0), grad_b.unsqueeze(0)).item()
167
+
168
+
169
+ # ============================================================
170
+ # Attention Entropy
171
+ # ============================================================
172
+
173
+ def attention_entropy(attn_weights: torch.Tensor) -> Dict[str, object]:
174
+ """
175
+ Compute Shannon entropy of attention distributions.
176
+ attn_weights: [batch, n_heads, seq_len, seq_len] — softmaxed attention patterns.
177
+ Returns per-head entropy and summary statistics.
178
+ """
179
+ eps = 1e-9
180
+ H = -(attn_weights * (attn_weights + eps).log2()).sum(-1) # [B, H, T]
181
+ return {
182
+ 'mean_entropy': H.mean().item(),
183
+ 'per_head_entropy': H.mean(dim=(0, 2)).cpu().tolist(),
184
+ 'entropy_std': H.std().item(),
185
+ }
186
+
187
+
188
+ # ============================================================
189
+ # Representation Variance Explained by Task
190
+ # ============================================================
191
+
192
+ def task_variance_explained(acts: torch.Tensor,
193
+ task_labels: torch.Tensor,
194
+ n_components: int = 20) -> Dict:
195
+ """
196
+ How much of the top-k PCA variance is predictable from task labels?
197
+ Based on Lampinen et al. 2024 — features learned first dominate top PCs.
198
+
199
+ Returns R² of linear regression: task_label → PC scores.
200
+ """
201
+ X = acts.cpu().float().numpy()
202
+ y = task_labels.cpu().float().numpy()
203
+
204
+ # Center
205
+ X = X - X.mean(0)
206
+ # PCA via SVD
207
+ U, S, Vh = np.linalg.svd(X, full_matrices=False)
208
+ n_comp = min(n_components, len(S))
209
+ scores = U[:, :n_comp] * S[:n_comp]
210
+ explained_var = (S[:n_comp] ** 2) / (S ** 2).sum()
211
+
212
+ # Per-PC R² via simple correlation
213
+ r2_per_pc = []
214
+ for i in range(n_comp):
215
+ corr = np.corrcoef(y, scores[:, i])[0, 1]
216
+ r2_per_pc.append(corr ** 2 if not np.isnan(corr) else 0.0)
217
+
218
+ # Weighted total
219
+ weighted_r2 = sum(explained_var[i] * r2_per_pc[i] for i in range(n_comp))
220
+
221
+ return {
222
+ 'weighted_r2': float(weighted_r2),
223
+ 'per_pc_r2': r2_per_pc,
224
+ 'explained_variance_ratio': explained_var.tolist(),
225
+ }
226
+
227
+
228
+ # ============================================================
229
+ # Parameter-space metrics
230
+ # ============================================================
231
+
232
+ def parameter_delta_cosine(params_init: List[torch.Tensor],
233
+ params_a: List[torch.Tensor],
234
+ params_b: List[torch.Tensor]) -> float:
235
+ """
236
+ Cosine similarity between parameter change vectors.
237
+ Measures whether two training runs moved parameters in the same direction.
238
+ """
239
+ delta_a = torch.cat([(a - i).flatten() for i, a in zip(params_init, params_a)])
240
+ delta_b = torch.cat([(b - i).flatten() for i, b in zip(params_init, params_b)])
241
+ return F.cosine_similarity(delta_a.unsqueeze(0), delta_b.unsqueeze(0)).item()
242
+
243
+
244
+ def weight_change_magnitude_per_layer(
245
+ model_init_state: Dict[str, torch.Tensor],
246
+ model_current_state: Dict[str, torch.Tensor]
247
+ ) -> Dict[str, float]:
248
+ """L2 norm of weight change per named parameter."""
249
+ results = {}
250
+ for name in model_init_state:
251
+ if name in model_current_state:
252
+ delta = (model_current_state[name].float() -
253
+ model_init_state[name].float())
254
+ results[name] = delta.norm().item()
255
+ return results
256
+
257
+
258
+ # ============================================================
259
+ # Probing Classifier
260
+ # ============================================================
261
+
262
+ def linear_probe_accuracy(acts: torch.Tensor, labels: np.ndarray,
263
+ n_splits: int = 5) -> float:
264
+ """
265
+ Linear probe on layer activations. Cross-validated accuracy.
266
+ acts: [n_samples, d_hidden]. labels: [n_samples] integer class labels.
267
+ """
268
+ from sklearn.linear_model import LogisticRegression
269
+ from sklearn.preprocessing import StandardScaler
270
+ from sklearn.model_selection import cross_val_score
271
+
272
+ X = acts.cpu().float().numpy()
273
+ X = StandardScaler().fit_transform(X)
274
+
275
+ clf = LogisticRegression(max_iter=1000, C=1.0, solver='lbfgs',
276
+ multi_class='multinomial')
277
+ scores = cross_val_score(clf, X, labels, cv=min(n_splits, len(set(labels))),
278
+ scoring='accuracy')
279
+ return scores.mean()