File size: 13,875 Bytes
5abe544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""
AT WHAT POINT DOES A READOUT BECOME A MODEL?

A body that never saw a domain could not be made to serve it by adding a
head: an injected member covering unseen classes reached 0.208 against a
0.775 ceiling. That is a safety property if it holds, and quite a strong
one, because it is structural rather than a filter. A filter is a
classifier and can be evaded; a capability absent from the base has no
representation for a head to select.

But it was measured at ONE head size, and a claim of that kind needs a
bound rather than a data point. A head is a readout over features the body
computed -- it can select, weight and recombine, and at a few hundred
values it has almost no room to do anything else. A large enough head is
not a readout at all. Somewhere between the two the guarantee stops
holding, and the question is where.

So this sweeps head capacity against a body trained on classes 0-6 and
frozen, with a member covering 7-9 that the body has never seen, from a
head so small it can barely select to one large enough to be a model in its
own right:

  FOLDED LINEAR   g row groups, g*classes values. The smallest is a
                  handful of numbers
  FULL LINEAR     every hidden unit its own weight
  TWO-LAYER       a hidden layer of its own before the classes, which is
                  no longer a readout by any reasonable reading

An in-domain member is swept alongside as a control. If head capacity helps
it and not the unseen one, the limit is the body's features rather than the
head's size, and absence is robust. If the unseen member recovers at some
head size, the guarantee has a bound and a plan tier is a safety parameter
rather than a commercial one.
"""

import numpy as np
import time
import json
from itertools import combinations

try:
    import cupy as _cp
    _GPU = _cp.cuda.runtime.getDeviceCount() > 0
except Exception:
    _GPU = False
xp = _cp if _GPU else np
DT = np.float32


def to_dev(a, dtype=DT):
    a = np.asarray(a, dtype=dtype)
    return xp.asarray(a) if _GPU else a


def to_host(a):
    return _cp.asnumpy(a) if _GPU and isinstance(a, _cp.ndarray) else np.asarray(a)


_MAPS = {}


def tie(shape, K, tag):
    key = (tuple(shape), K, tag)
    if key not in _MAPS:
        import hashlib
        h = int(hashlib.md5(str(key).encode()).hexdigest()[:8], 16)
        rg = np.random.default_rng(h)
        idx = rg.integers(0, K, int(np.prod(shape)))
        _MAPS[key] = to_dev(idx, np.int64) if _GPU else idx.astype(np.int64)
    return _MAPS[key]


def fold(W, idx, K):
    flat = W.reshape(-1)
    s = xp.zeros(K, DT); c = xp.zeros(K, DT)
    if _GPU:
        import cupyx
        cupyx.scatter_add(s, idx, flat); cupyx.scatter_add(c, idx, xp.ones_like(flat))
    else:
        np.add.at(s, idx, flat); np.add.at(c, idx, np.ones_like(flat))
    return (s / xp.maximum(c, 1.0))[idx].reshape(W.shape)


def fold_rows(W, groups):
    """Average within row groups: the head's own compression."""
    if not groups or groups >= W.shape[0]:
        return W
    r, c = W.shape
    g = groups
    while r % g:
        g -= 1
    m = W.reshape(g, r//g, c).mean(1, keepdims=True)
    return xp.broadcast_to(m, (g, r//g, c)).reshape(r, c)


def finit(rg, fan, shape, K=None):
    s = np.sqrt(2.0/fan)
    if K:
        s *= np.sqrt(max(1.0, np.prod(shape)/K))
    return rg.normal(0, s, shape)


def train_body(X, Y, masks, D, W, K, epochs, batch, lr, seed):
    """The family, on its own classes. Returns the frozen body."""
    rg = np.random.default_rng(seed)
    i0 = tie((D, W), K, "body")
    W0 = fold(to_dev(finit(rg, D, (D, W), K)), i0, K)
    b0 = xp.zeros(W, DT)
    S = masks.shape[0]
    H = to_dev(finit(rg, W, (S, W, 10)))
    B = xp.zeros((S, 10), DT)
    P = [W0, b0, H, B]
    M = [xp.zeros_like(p) for p in P]; V = [xp.zeros_like(p) for p in P]
    n = X.shape[0]; t = 0
    for ep in range(epochs):
        perm = rg.permutation(n)
        for st in range(0, n, batch):
            b = perm[st:st+batch]
            bi = to_dev(b, np.int64) if _GPU else b
            x = X[bi]; y = Y[bi]; mk = masks[:, bi]
            z = x @ P[0] + P[1]; a = xp.maximum(z, 0)
            lg = xp.einsum('ni,sio->sno', a, P[2]) + P[3][:, None, :]
            e = xp.exp(lg - lg.max(-1, keepdims=True))
            d = (e/e.sum(-1, keepdims=True) - y[None])*mk[:, :, None]
            d = d/max(1, len(b))
            da = xp.einsum('sno,sio->ni', d, P[2])
            dz = da*(z > 0)
            G = [x.T @ dz, dz.sum(0), xp.einsum('ni,sno->sio', a, d), d.sum(1)]
            t += 1
            for i, (p_, gr) in enumerate(zip(P, G)):
                M[i] = 0.9*M[i] + 0.1*gr
                V[i] = 0.999*V[i] + 0.001*gr*gr
                P[i] = p_ - lr*(M[i]/(1-0.9**t))/(xp.sqrt(V[i]/(1-0.999**t))+1e-8)
            P[0] = fold(P[0], i0, K)
    return P[0], P[1]


def fit_head(W0, b0, X, Y, mask, W, epochs, batch, lr, seed, groups=None,
             hidden=0):
    """Fit ONE member's head against a frozen body.

    groups folds the head's rows; hidden gives it a layer of its own, at
    which point it is not a readout but a model sitting on frozen
    features."""
    rg = np.random.default_rng(seed)
    n = X.shape[0]
    if hidden:
        Wh = to_dev(finit(rg, W, (W, hidden)))
        bh = xp.zeros(hidden, DT)
        Wo = to_dev(finit(rg, hidden, (hidden, 10)))
        bo = xp.zeros(10, DT)
        P = [Wh, bh, Wo, bo]
    else:
        Wo = to_dev(finit(rg, W, (W, 10)))
        bo = xp.zeros(10, DT)
        P = [Wo, bo]
        if groups:
            P[0] = fold_rows(P[0], groups)
    M = [xp.zeros_like(p) for p in P]; V = [xp.zeros_like(p) for p in P]
    t = 0
    for ep in range(epochs):
        perm = rg.permutation(n)
        for st in range(0, n, batch):
            b = perm[st:st+batch]
            bi = to_dev(b, np.int64) if _GPU else b
            x = X[bi]; y = Y[bi]; mk = mask[bi]
            a = xp.maximum(x @ W0 + b0, 0)          # the body is FROZEN
            if hidden:
                zh = a @ P[0] + P[1]; ah = xp.maximum(zh, 0)
                lg = ah @ P[2] + P[3]
            else:
                lg = a @ P[0] + P[1]
            e = xp.exp(lg - lg.max(-1, keepdims=True))
            d = (e/e.sum(-1, keepdims=True) - y)*mk[:, None]
            d = d/max(1, len(b))
            if hidden:
                G = [None]*4
                G[2] = ah.T @ d; G[3] = d.sum(0)
                dh = (d @ P[2].T)*(zh > 0)
                G[0] = a.T @ dh; G[1] = dh.sum(0)
            else:
                G = [a.T @ d, d.sum(0)]
            t += 1
            for i, (p_, gr) in enumerate(zip(P, G)):
                M[i] = 0.9*M[i] + 0.1*gr
                V[i] = 0.999*V[i] + 0.001*gr*gr
                P[i] = p_ - lr*(M[i]/(1-0.9**t))/(xp.sqrt(V[i]/(1-0.999**t))+1e-8)
            if groups and not hidden:
                P[0] = fold_rows(P[0], groups)
    return P


def score(W0, b0, P, X, y, mask, hidden):
    a = xp.maximum(X @ W0 + b0, 0)
    if hidden:
        lg = xp.maximum(a @ P[0] + P[1], 0) @ P[2] + P[3]
    else:
        lg = a @ P[0] + P[1]
    m = to_host(mask).astype(bool)
    return float((to_host(lg.argmax(1))[m] == y[m]).mean())


def head_values(W, groups, hidden):
    if hidden:
        return W*hidden + hidden + hidden*10 + 10
    g = groups or W
    while W % g:
        g -= 1
    return g*10 + 10


def load(name):
    if name == "fashion":
        from tensorflow import keras
        (a, b), (c, d) = keras.datasets.fashion_mnist.load_data()
        X = np.concatenate([a, c]).reshape(-1, 784).astype(np.float32)/255.0
        y = np.concatenate([b, d]).ravel().astype(np.int64)
    else:
        from sklearn.datasets import load_digits
        dd = load_digits()
        X = dd.data.astype(np.float32)/16.0; y = dd.target.astype(np.int64)
    return X, y


CFG = dict(dataset="fashion", width=64, K=512, n_train=8000, epochs=40,
           batch=128, lr=1e-3, seeds=(0, 1, 2),
           family_classes=(0, 1, 2, 3, 4, 5, 6),
           unseen_classes=(7, 8, 9))
# from a head that can barely select to one that is a model in its own right
HEADS = [("g=2", 2, 0), ("g=4", 4, 0), ("g=8", 8, 0), ("g=16", 16, 0),
         ("g=32", 32, 0), ("full linear", None, 0),
         ("2-layer h=16", None, 16), ("2-layer h=64", None, 64),
         ("2-layer h=256", None, 256)]


def one_seed(X, y, seed, cfg):
    rg = np.random.default_rng(seed)
    fam = list(cfg["family_classes"]); uns = list(cfg["unseen_classes"])
    D, W = X.shape[1], cfg["width"]
    subs = [tuple(sorted(c)) for c in combinations(fam, 4)]
    subs = [subs[i] for i in rg.choice(len(subs), 6, replace=False)]
    in_dom = tuple(sorted(fam[:4]))           # FIXED across seeds
    out_dom = tuple(sorted(uns))

    def split(cl, n):
        i = np.where(np.isin(y, cl))[0]; rg.shuffle(i)
        return i[:n], i[n:n+2000]
    ftr, _ = split(fam, cfg["n_train"])
    itr, ite = split(in_dom, cfg["n_train"]//3)
    otr, ote = split(uns, cfg["n_train"]//3)
    dev = lambda i: to_dev(X[i])
    Y = lambda i: to_dev(np.eye(10, dtype=np.float32)[y[i]])
    m1 = lambda i, s: to_dev(np.isin(y[i], s).astype(np.float32))
    mS = lambda i, ss: to_dev(np.stack([np.isin(y[i], s)
                                        for s in ss]).astype(np.float32))

    W0, b0 = train_body(dev(ftr), Y(ftr), mS(ftr, subs), D, W, cfg["K"],
                        cfg["epochs"], cfg["batch"], cfg["lr"], seed)
    out = {}
    for nm, g, h in HEADS:
        for tag, tr, te, sub in (("in", itr, ite, in_dom),
                                 ("out", otr, ote, out_dom)):
            P = fit_head(W0, b0, dev(tr), Y(tr), m1(tr, sub), W,
                         cfg["epochs"], cfg["batch"], cfg["lr"], seed+1,
                         groups=g, hidden=h)
            out[f"{nm}/{tag}"] = score(W0, b0, P, dev(te), y[te],
                                       m1(te, sub), h)
    # ceilings: a whole model of its own for each member
    for tag, tr, te, sub in (("in", itr, ite, in_dom),
                             ("out", otr, ote, out_dom)):
        Wc, bc = train_body(dev(tr), Y(tr), mS(tr, [sub]), D, W, cfg["K"],
                            cfg["epochs"], cfg["batch"], cfg["lr"], seed+9)
        P = fit_head(Wc, bc, dev(tr), Y(tr), m1(tr, sub), W, cfg["epochs"],
                     cfg["batch"], cfg["lr"], seed+9)
        out[f"ceiling/{tag}"] = score(Wc, bc, P, dev(te), y[te],
                                      m1(te, sub), 0)
    return out


def main(**over):
    CFG.update(over)
    t0 = time.time()
    print("=" * 78)
    print("AT WHAT POINT DOES A READOUT BECOME A MODEL?")
    print("=" * 78)
    print(f"  backend: {'cupy (GPU)' if _GPU else 'numpy (CPU)'}")
    for k, v in CFG.items():
        print(f"  {k:16s} = {v}")
    W = CFG["width"]
    print(f"\n  the body is trained on classes {CFG['family_classes']} and")
    print(f"  FROZEN. The unseen member covers {CFG['unseen_classes']}, which")
    print(f"  it has never seen in any form.\n")
    print(f"  {'head':>14s} {'values':>8s}")
    for nm, g, h in HEADS:
        print(f"  {nm:>14s} {head_values(W, g, h):8,}")
    print("=" * 78, flush=True)

    X, y = load(CFG["dataset"])
    runs = []
    for s in CFG["seeds"]:
        runs.append(one_seed(X, y, s, CFG))
        print(f"  seed {s} done   [{time.time()-t0:.0f}s]", flush=True)
        json.dump(runs, open("head_capacity.json", "w"), indent=2)
    m = {k: float(np.mean([r[k] for r in runs])) for k in runs[0]}
    sd = {k: float(np.std([r[k] for r in runs])) for k in runs[0]}

    print("\n" + "=" * 78)
    print("  READOUT")
    print("=" * 78)
    ci, co = m["ceiling/in"], m["ceiling/out"]
    print(f"  ceilings: in-domain {ci:.4f}, unseen {co:.4f}")
    print(f"  (each member with a body of its own)\n")
    print(f"  {'head':>14s} {'values':>8s} {'in-domain':>10s} {'vs ceil':>8s}"
          f" {'UNSEEN':>8s} {'vs ceil':>8s}")
    for nm, g, h in HEADS:
        i_, o_ = m[f"{nm}/in"], m[f"{nm}/out"]
        print(f"  {nm:>14s} {head_values(W, g, h):8,} {i_:10.4f} "
              f"{i_-ci:+8.4f} {o_:8.4f} {o_-co:+8.4f}")
    outs = np.array([m[f"{nm}/out"] for nm, _, _ in HEADS])
    ins = np.array([m[f"{nm}/in"] for nm, _, _ in HEADS])
    s_ = max(sd.values())
    print(f"\n  seed spread (worst) {s_:.4f}")
    print(f"  the unseen member moves {outs.max()-outs.min():+.4f} across a")
    print(f"  {head_values(W, None, 256)//head_values(W, 2, 0)}-fold range of"
          f" head capacity")
    print(f"  the in-domain member moves {ins.max()-ins.min():+.4f}")
    print()
    recovered = [nm for (nm, g, h), o in zip(HEADS, outs) if o > co - 2*s_]
    if recovered:
        print(f"  THE GUARANTEE HAS A BOUND. The unseen member reaches its")
        print(f"  ceiling once the head holds "
              f"{head_values(W, *[x[1:] for x in HEADS if x[0]==recovered[0]][0]):,}"
              f" values ({recovered[0]}), so a")
        print(f"  large enough head recovers a capability the body never had")
        print(f"  and head size is a safety parameter, not a commercial one.")
    elif outs.max() - outs.min() > 4*s_:
        print(f"  CAPACITY HELPS BUT DOES NOT RESCUE. The unseen member")
        print(f"  improves with head size and never reaches its ceiling, so")
        print(f"  absence degrades rather than holds absolutely — a bound")
        print(f"  exists and is above the range swept here.")
    else:
        print(f"  ABSENCE IS ROBUST. Head capacity moves the unseen member by")
        print(f"  less than seed noise across the whole range, including")
        print(f"  heads with a hidden layer of their own. What limits it is")
        print(f"  the body's features and not the head's size, which is what")
        print(f"  a structural guarantee would look like.")
    print(f"\n  total {time.time()-t0:.0f}s; wrote head_capacity.json")


if __name__ == "__main__":
    main()