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()
|