summaryrefslogtreecommitdiff
path: root/src/stats_decomp.py
blob: f82e720538b6291cc29bc3e18037614bd47f8d21 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""Decomposition + robustness statistics for the committed results (results.md).

Computes, from the saved jlens_v3 artifacts and the base checkpoint:
  1. W_U row-norm decomposition: r(||W_U[k]||, freq), r vs log-frequency,
     Spearman, and per-layer partial correlation of faithful norm with
     frequency after regressing out ||W_U[k]||.
  2. Pearson p-values (normal-approx two-sided) and Spearman per layer.
  3. Synthetic-pair ratio with across-seed mean/SD and bootstrap CI.

Run inside the meru container where artifacts exist:
    python3 src/stats_decomp.py
"""
import os
import pickle
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np
import torch

V = 65
TRAIN_BIN = 'data/shakespeare_char/train.bin'
CKPT = 'out-shakespeare-char/ckpt.pt'


def rankdata(a):
    """Average ranks (ties averaged) — minimal scipy-free rankdata."""
    sorter = np.argsort(a, kind='mergesort')
    ranks = np.empty(len(a), dtype=float)
    ranks[sorter] = np.arange(1, len(a) + 1)
    i = 0
    while i < len(a):
        j = i
        while j + 1 < len(a) and a[sorter[j + 1]] == a[sorter[i]]:
            j += 1
        if j > i:
            ranks[sorter[i:j + 1]] = (i + j) / 2.0 + 1
        i = j + 1
    return ranks


def spearman(x, y):
    return np.corrcoef(rankdata(x), rankdata(y))[0, 1]


def pearson_p(r, n):
    """Two-sided p for Pearson r via normal approx on Fisher's z (fine for n=65)."""
    import math
    from math import erf, sqrt
    z = abs(r) * math.sqrt((n - 2) / max(1 - r * r, 1e-12))
    return 2 * (1 - 0.5 * (1 + erf(z / sqrt(2))))


def load_layer(l):
    for p in (f'outputs/jlens_v3/layer{l}.pt', f'outputs/jlens_v3_layer{l}.pt'):
        if os.path.exists(p):
            return torch.load(p, map_location='cpu')
    raise FileNotFoundError(f'no jlens artifact for layer {l}')


def main():
    train = np.memmap(TRAIN_BIN, dtype=np.uint16, mode='r')
    counts = np.bincount(train, minlength=V).astype(float)
    freq = counts / counts.sum() * 100
    logf = np.log10(freq)

    ckpt = torch.load(CKPT, map_location='cpu')
    wu = ckpt['model']['lm_head.weight'].float().numpy()
    wu_norms = np.linalg.norm(wu, axis=1)

    print("=== W_U row-norm decomposition ===")
    print(f"r(||W_U[k]||, freq)        = {np.corrcoef(wu_norms, freq)[0, 1]:+.3f}")
    print(f"r(||W_U[k]||, log10 freq)  = {np.corrcoef(wu_norms, logf)[0, 1]:+.3f}")
    print(f"Spearman(||W_U[k]||, freq) = {spearman(wu_norms, freq):+.3f}")

    print("\n=== per-layer decomposition ===")
    print(f"{'layer':<6}{'r(faith,freq)':>14}{'p':>10}{'r(faith,WU)':>13}"
          f"{'Spear(faith)':>14}{'partial|WU':>13}")
    for l in range(6):
        d = load_layer(l)
        fn = np.array([d['faithful_norms'][k] for k in range(V)])
        rf = np.corrcoef(fn, freq)[0, 1]
        rfw = np.corrcoef(fn, wu_norms)[0, 1]
        sf = spearman(fn, freq)
        A = np.vstack([wu_norms, np.ones(V)]).T
        resid = fn - A @ np.linalg.lstsq(A, fn, rcond=None)[0]
        r_part = np.corrcoef(resid, freq)[0, 1]
        print(f"L{l:<5}{rf:>+14.3f}{pearson_p(rf, V):>10.1e}{rfw:>+13.3f}"
              f"{sf:>+14.3f}{r_part:>+13.3f}")

    print("\n=== synthetic pair ratio ===")
    with open('data/synth_pair/meta.pkl', 'rb') as f:
        meta = pickle.load(f)
    iid_s = meta['stoi']['@']
    iid_n = meta['stoi']['#']
    ratios = []
    for s in range(3):
        vals_s, vals_n = [], []
        for l in (2, 3, 4):
            d = torch.load(f'outputs/synth_pair/seed{s}/layer{l}.pt',
                           map_location='cpu')
            fn = d['faithful_norms']
            vals_s.append(fn[iid_s])
            vals_n.append(fn[iid_n])
        ratios.append(np.mean(vals_s) / max(np.mean(vals_n), 1e-9))
    r_arr = np.array(ratios)
    rng = np.random.RandomState(0)
    boot = [np.mean(rng.choice(r_arr, 3, replace=True)) for _ in range(10000)]
    print(f"ratio per seed: {[f'{r:.3f}' for r in ratios]}")
    print(f"mean +/- SD: {r_arr.mean():.3f} +/- {r_arr.std(ddof=1):.3f}")
    print(f"bootstrap 95% CI: [{np.percentile(boot, 2.5):.3f}, "
          f"{np.percentile(boot, 97.5):.3f}]")


if __name__ == '__main__':
    main()