torch-type-nn 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torch_type_nn/__init__.py +38 -0
- torch_type_nn/adapters/__init__.py +6 -0
- torch_type_nn/adapters/mlp.py +322 -0
- torch_type_nn/adapters/typenn.py +546 -0
- torch_type_nn/edits.py +99 -0
- torch_type_nn/functional.py +126 -0
- torch_type_nn/layer.py +354 -0
- torch_type_nn/mlp.py +251 -0
- torch_type_nn/network.py +115 -0
- torch_type_nn/optim.py +122 -0
- torch_type_nn/protocol.py +148 -0
- torch_type_nn/py.typed +0 -0
- torch_type_nn/scaling.py +523 -0
- torch_type_nn/train.py +97 -0
- torch_type_nn-0.1.0.dev0.dist-info/METADATA +302 -0
- torch_type_nn-0.1.0.dev0.dist-info/RECORD +18 -0
- torch_type_nn-0.1.0.dev0.dist-info/WHEEL +4 -0
- torch_type_nn-0.1.0.dev0.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""The type-nn adapter: :class:`~torch_type_nn.network.TypeNN` as a :class:`Scalable`.
|
|
2
|
+
|
|
3
|
+
Addresses:
|
|
4
|
+
|
|
5
|
+
* ``Item(WIDTH, (i, k))``: output coordinate ``k`` of layer ``i`` (the unit
|
|
6
|
+
and the column of layer ``i + 1`` that reads it); sites are junctions ``i``.
|
|
7
|
+
* ``Item(DEGREE, (i, k, r))``: Or slot ``r`` of unit ``k`` of layer ``i``;
|
|
8
|
+
sites are units ``(i, k)``.
|
|
9
|
+
* ``Item(DEPTH, (i,))``: layer ``i``; sites are gaps ``g`` (in front of layer
|
|
10
|
+
``g``), counted in the stack without the depth probe.
|
|
11
|
+
|
|
12
|
+
Width grows on *every* junction (``width_through_depth_probe=True``, the
|
|
13
|
+
default): a width probe on a junction that crosses the depth probe passes
|
|
14
|
+
through the probe layer on a carrier Or (w = e_k, b = 0, a = 1) and the real
|
|
15
|
+
consumer beyond it reads it with weights of exactly 0. The C reference skips
|
|
16
|
+
such junctions, which leaves a network born with depth <= 2 no width site
|
|
17
|
+
while a depth probe exists (see docs/DIFFERENCES.md);
|
|
18
|
+
``width_through_depth_probe=False`` reproduces it.
|
|
19
|
+
|
|
20
|
+
The numerical content is ``type_nn_scale.c``: distances from the identity,
|
|
21
|
+
exact ablations, affine folds for depth edits and the exact parameter count
|
|
22
|
+
of a trial model (see docs/DIFFERENCES.md).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from ..layer import AndOr, uniform
|
|
33
|
+
from ..network import TypeNN
|
|
34
|
+
from ..protocol import DEGREE, DEPTH, WIDTH, EditContext, Item, Trial
|
|
35
|
+
|
|
36
|
+
__all__ = ["TypeNNAdapter", "dev_or", "dev_column", "dev_layer"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------- distances from identity
|
|
40
|
+
|
|
41
|
+
def dev_or(layer: AndOr, k: int, r: int) -> float:
|
|
42
|
+
"""RMS distance of an Or from the unit identity (w = 0, b = 1, a = 1)."""
|
|
43
|
+
w, b, a = (layer.weight.detach()[k, r], layer.bias.detach()[k, r],
|
|
44
|
+
layer.assembly.detach()[k, r])
|
|
45
|
+
s = float((w * w).sum() + (b - 1) ** 2 + (a - 1) ** 2)
|
|
46
|
+
return math.sqrt(s / (layer.in_features + 2))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dev_column(consumer: AndOr, j: int) -> float:
|
|
50
|
+
"""RMS of the weights with which ``consumer`` reads coordinate ``j``."""
|
|
51
|
+
live = consumer.mask
|
|
52
|
+
c = int(live.sum())
|
|
53
|
+
if not c:
|
|
54
|
+
return 0.0
|
|
55
|
+
w = consumer.weight.detach()[..., j][live]
|
|
56
|
+
return math.sqrt(float((w * w).sum()) / c)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sq_one(layer: AndOr) -> torch.Tensor:
|
|
60
|
+
w, b, a = layer.weight.detach(), layer.bias.detach(), layer.assembly.detach()
|
|
61
|
+
return (w * w).sum(-1) + (b - 1) ** 2 + (a - 1) ** 2 # (m, R)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _sq_carrier(layer: AndOr) -> torch.Tensor:
|
|
65
|
+
# carrier of coordinate k: w = e_k, b = 0, a = 1 (the Or equals x_k)
|
|
66
|
+
m = layer.out_features
|
|
67
|
+
eye = torch.eye(m, layer.in_features, dtype=layer.weight.dtype,
|
|
68
|
+
device=layer.weight.device).unsqueeze(1) # (m, 1, n)
|
|
69
|
+
d = layer.weight.detach() - eye
|
|
70
|
+
return (d * d).sum(-1) + layer.bias.detach() ** 2 + (layer.assembly.detach() - 1) ** 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dev_layer(layer: AndOr) -> float:
|
|
74
|
+
"""RMS distance from the identity layer; inf if the layer is not square."""
|
|
75
|
+
if layer.in_features != layer.out_features:
|
|
76
|
+
return math.inf
|
|
77
|
+
live = layer.mask
|
|
78
|
+
one, car = _sq_one(layer), _sq_carrier(layer)
|
|
79
|
+
all_one = torch.where(live, one, torch.zeros_like(one)).sum(1, keepdim=True)
|
|
80
|
+
v = torch.where(live, all_one - one + car, torch.full_like(one, math.inf))
|
|
81
|
+
s = float(v.min(1).values.sum()) if layer.degree else math.inf
|
|
82
|
+
c = int(live.sum()) * (layer.in_features + 2)
|
|
83
|
+
return math.sqrt(s / c) if c else math.inf
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------- exact ablations
|
|
87
|
+
|
|
88
|
+
def _snap(layer: AndOr, *names: str) -> dict:
|
|
89
|
+
return {n: getattr(layer, n).detach().clone() for n in names}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@torch.no_grad()
|
|
93
|
+
def _restore(layer: AndOr, snap: dict) -> None:
|
|
94
|
+
for n, v in snap.items():
|
|
95
|
+
getattr(layer, n).copy_(v)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@torch.no_grad()
|
|
99
|
+
def _ablate_or(layer: AndOr, k: int, r: int):
|
|
100
|
+
s = (layer.weight[k, r].clone(), layer.bias[k, r].clone(), layer.assembly[k, r].clone())
|
|
101
|
+
layer.weight[k, r] = 0.0
|
|
102
|
+
layer.bias[k, r] = 1.0
|
|
103
|
+
layer.assembly[k, r] = 1.0
|
|
104
|
+
|
|
105
|
+
def undo() -> None:
|
|
106
|
+
with torch.no_grad():
|
|
107
|
+
layer.weight[k, r], layer.bias[k, r], layer.assembly[k, r] = s
|
|
108
|
+
return undo
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@torch.no_grad()
|
|
112
|
+
def _ablate_col(consumer: AndOr, j: int):
|
|
113
|
+
s = consumer.weight[..., j].clone()
|
|
114
|
+
consumer.weight[..., j] = 0.0
|
|
115
|
+
|
|
116
|
+
def undo() -> None:
|
|
117
|
+
with torch.no_grad():
|
|
118
|
+
consumer.weight[..., j] = s
|
|
119
|
+
return undo
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@torch.no_grad()
|
|
123
|
+
def _ablate_layer(layer: AndOr):
|
|
124
|
+
"""Reset a square layer to its identity: per unit the live Or nearest e_k
|
|
125
|
+
becomes the carrier, every other Or the unit identity."""
|
|
126
|
+
snap = _snap(layer, "weight", "bias", "assembly")
|
|
127
|
+
live = layer.mask
|
|
128
|
+
d = torch.where(live, _sq_carrier(layer) - _sq_one(layer),
|
|
129
|
+
torch.full_like(layer.bias, math.inf))
|
|
130
|
+
best = d.argmin(1) # (m,)
|
|
131
|
+
layer.weight.masked_fill_(live.unsqueeze(-1), 0.0)
|
|
132
|
+
layer.bias.masked_fill_(live, 1.0)
|
|
133
|
+
layer.assembly.fill_(1.0)
|
|
134
|
+
ks = torch.arange(layer.out_features, device=best.device)
|
|
135
|
+
has = live[ks, best]
|
|
136
|
+
ks, best = ks[has], best[has]
|
|
137
|
+
layer.weight[ks, best, ks] = 1.0
|
|
138
|
+
layer.bias[ks, best] = 0.0
|
|
139
|
+
return lambda: _restore(layer, snap)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------- folds
|
|
143
|
+
|
|
144
|
+
def fold_from_stats(at: AndOr, u_to_x: bool) -> tuple[torch.Tensor, torch.Tensor]:
|
|
145
|
+
"""A type-identity layer emits u = F(x), which is x only to first order.
|
|
146
|
+
Insert (u_to_x): x ~ alpha u + beta. Remove: u ~ alpha x + beta.
|
|
147
|
+
Measured on the epoch that just ran."""
|
|
148
|
+
n = at.in_features
|
|
149
|
+
one, zero = at.weight.new_ones(n), at.weight.new_zeros(n)
|
|
150
|
+
ns = float(at.stat_ns)
|
|
151
|
+
if ns < 2:
|
|
152
|
+
return one, zero
|
|
153
|
+
mx, mu = at.stat_sx / ns, at.stat_su / ns
|
|
154
|
+
vxx = at.stat_sxx / ns - mx * mx
|
|
155
|
+
vuu = at.stat_suu / ns - mu * mu
|
|
156
|
+
vxu = at.stat_sxu / ns - mx * mu
|
|
157
|
+
if u_to_x:
|
|
158
|
+
alpha = torch.where(vuu > 0, vxu / torch.where(vuu > 0, vuu, one), one)
|
|
159
|
+
beta = mx - alpha * mu
|
|
160
|
+
else:
|
|
161
|
+
alpha = torch.where(vxx > 0, vxu / torch.where(vxx > 0, vxx, one), one)
|
|
162
|
+
beta = mu - alpha * mx
|
|
163
|
+
bad = ~(alpha > 0) | ~torch.isfinite(alpha)
|
|
164
|
+
return torch.where(bad, one, alpha), torch.where(bad, zero, beta)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@torch.no_grad()
|
|
168
|
+
def fold_apply(c: AndOr, alpha: torch.Tensor, beta: torch.Tensor) -> None:
|
|
169
|
+
J = min(alpha.shape[0], c.in_features)
|
|
170
|
+
a, b = alpha[:J], beta[:J]
|
|
171
|
+
c.bias += (c.weight[..., :J] * b).sum(-1)
|
|
172
|
+
c.weight[..., :J] *= a
|
|
173
|
+
c.adam_m_weight[..., :J] /= a
|
|
174
|
+
c.adam_v_weight[..., :J] /= a * a
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------- the adapter
|
|
178
|
+
|
|
179
|
+
class TypeNNAdapter:
|
|
180
|
+
""":class:`TypeNN` under the :class:`~torch_type_nn.protocol.Scalable` protocol."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, model: TypeNN, *, width_through_depth_probe: bool = True) -> None:
|
|
183
|
+
self.model = model
|
|
184
|
+
self.through = bool(width_through_depth_probe)
|
|
185
|
+
self._sink: list | None = None
|
|
186
|
+
self._tracking = False
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def layers(self):
|
|
190
|
+
return self.model.layers
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------ bookkeeping
|
|
193
|
+
|
|
194
|
+
def num_params(self) -> int:
|
|
195
|
+
return self.model.num_params()
|
|
196
|
+
|
|
197
|
+
def set_tracking(self, on: bool) -> None:
|
|
198
|
+
self._tracking = on
|
|
199
|
+
for layer in self.layers:
|
|
200
|
+
layer.track_stats = on
|
|
201
|
+
|
|
202
|
+
def reset_stats(self) -> None:
|
|
203
|
+
for layer in self.layers:
|
|
204
|
+
layer.reset_stats()
|
|
205
|
+
|
|
206
|
+
def set_edit_sink(self, sink: list | None) -> None:
|
|
207
|
+
self._sink = sink
|
|
208
|
+
for layer in self.layers:
|
|
209
|
+
layer.edit_sink = sink
|
|
210
|
+
|
|
211
|
+
def finalize(self) -> None:
|
|
212
|
+
for layer in self.layers:
|
|
213
|
+
layer.compact()
|
|
214
|
+
|
|
215
|
+
# ----------------------------------------------------------------- probes
|
|
216
|
+
|
|
217
|
+
def _probe_layer(self) -> int | None:
|
|
218
|
+
for i, layer in enumerate(self.layers):
|
|
219
|
+
if layer.probe:
|
|
220
|
+
return i
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _probe_unit(layer: AndOr) -> int | None:
|
|
225
|
+
k = layer.unit_probe.nonzero()
|
|
226
|
+
return int(k[0]) if len(k) else None
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _probe_or(layer: AndOr, k: int) -> int | None:
|
|
230
|
+
r = (layer.or_probe[k] & layer.mask[k]).nonzero()
|
|
231
|
+
return int(r[0]) if len(r) else None
|
|
232
|
+
|
|
233
|
+
def _crosses_probe(self, i: int) -> bool:
|
|
234
|
+
return self.through and self.layers[i + 1].probe
|
|
235
|
+
|
|
236
|
+
def _consumer(self, i: int) -> AndOr:
|
|
237
|
+
"""The layer that reads the output coordinates of layer i."""
|
|
238
|
+
return self.layers[i + 2] if self._crosses_probe(i) else self.layers[i + 1]
|
|
239
|
+
|
|
240
|
+
def probe_sites(self, axis: str) -> Sequence:
|
|
241
|
+
L = self.layers
|
|
242
|
+
if axis == WIDTH:
|
|
243
|
+
if self.through: # every non-probe producer; the probe passes units on
|
|
244
|
+
return [i for i in range(len(L) - 1) if not L[i].probe]
|
|
245
|
+
# the reference rule: junctions not touching the depth probe
|
|
246
|
+
return [i for i in range(len(L) - 1) if not (L[i].probe or L[i + 1].probe)]
|
|
247
|
+
if axis == DEGREE:
|
|
248
|
+
return [(i, k) for i, l in enumerate(L) for k in range(l.out_features)]
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
def probe_at(self, axis: str, site=None) -> Item | None:
|
|
252
|
+
if axis == WIDTH:
|
|
253
|
+
k = self._probe_unit(self.layers[site])
|
|
254
|
+
return None if k is None else Item(WIDTH, (site, k))
|
|
255
|
+
if axis == DEGREE:
|
|
256
|
+
i, k = site
|
|
257
|
+
r = self._probe_or(self.layers[i], k)
|
|
258
|
+
return None if r is None else Item(DEGREE, (i, k, r))
|
|
259
|
+
p = self._probe_layer()
|
|
260
|
+
return None if p is None else Item(DEPTH, (p,))
|
|
261
|
+
|
|
262
|
+
def add_probe(self, axis: str, site, ctx: EditContext) -> None:
|
|
263
|
+
if axis == WIDTH:
|
|
264
|
+
self._add_width_probe(site, ctx)
|
|
265
|
+
elif axis == DEGREE:
|
|
266
|
+
i, k = site
|
|
267
|
+
layer = self.layers[i]
|
|
268
|
+
n = layer.in_features
|
|
269
|
+
w = uniform((n,), ctx.noise, ctx.generator, layer.weight)
|
|
270
|
+
b = 1.0 + float(uniform((), ctx.noise, ctx.generator, layer.bias))
|
|
271
|
+
layer.add_or(k, w, b, 1.0, probe=True, born=ctx.step)
|
|
272
|
+
else:
|
|
273
|
+
self._insert_depth_probe(site, ctx)
|
|
274
|
+
|
|
275
|
+
def promote(self, item: Item) -> None:
|
|
276
|
+
if item.axis == WIDTH:
|
|
277
|
+
i, k = item.address
|
|
278
|
+
self.layers[i].unit_probe[k] = False
|
|
279
|
+
elif item.axis == DEGREE:
|
|
280
|
+
i, k, r = item.address
|
|
281
|
+
self.layers[i].or_probe[k, r] = False
|
|
282
|
+
else:
|
|
283
|
+
self.layers[item.address[0]].probe = False
|
|
284
|
+
|
|
285
|
+
def born(self, item: Item) -> int:
|
|
286
|
+
if item.axis == WIDTH:
|
|
287
|
+
i, k = item.address
|
|
288
|
+
return int(self.layers[i].unit_born[k])
|
|
289
|
+
if item.axis == DEGREE:
|
|
290
|
+
i, k, r = item.address
|
|
291
|
+
return int(self.layers[i].or_born[k, r])
|
|
292
|
+
return self.layers[item.address[0]].born
|
|
293
|
+
|
|
294
|
+
def displacement(self, item: Item) -> float:
|
|
295
|
+
if item.axis == WIDTH:
|
|
296
|
+
i, k = item.address
|
|
297
|
+
return dev_column(self._consumer(i), k)
|
|
298
|
+
if item.axis == DEGREE:
|
|
299
|
+
i, k, r = item.address
|
|
300
|
+
return dev_or(self.layers[i], k, r)
|
|
301
|
+
return dev_layer(self.layers[item.address[0]])
|
|
302
|
+
|
|
303
|
+
def best_site(self, axis: str, moving: Item | None = None):
|
|
304
|
+
"""Gap with the largest mean |dL/dx| over the epoch, counted in the
|
|
305
|
+
stack without the depth probe. The gap behind the last layer has no
|
|
306
|
+
consumer to absorb a fold and is not a candidate."""
|
|
307
|
+
probe = None if moving is None else moving.address[0]
|
|
308
|
+
|
|
309
|
+
def loud(layer: AndOr) -> float:
|
|
310
|
+
ns = float(layer.stat_ns)
|
|
311
|
+
return float(layer.stat_gin) / ns if ns else 0.0
|
|
312
|
+
|
|
313
|
+
best_g, best, g = 0, -1.0, 0
|
|
314
|
+
for i, layer in enumerate(self.layers):
|
|
315
|
+
if i == probe:
|
|
316
|
+
continue
|
|
317
|
+
v = loud(layer)
|
|
318
|
+
if probe is not None and i == probe + 1:
|
|
319
|
+
v = max(v, loud(self.layers[probe]))
|
|
320
|
+
if v > best:
|
|
321
|
+
best, best_g = v, g
|
|
322
|
+
g += 1
|
|
323
|
+
return best_g
|
|
324
|
+
|
|
325
|
+
def site_of(self, item: Item):
|
|
326
|
+
return item.address[0]
|
|
327
|
+
|
|
328
|
+
def remove_probe(self, item: Item) -> None:
|
|
329
|
+
self._remove_identity_layer(item.address[0])
|
|
330
|
+
|
|
331
|
+
# ------------------------------------------------------ structural edits
|
|
332
|
+
|
|
333
|
+
def _add_width_probe(self, i: int, ctx: EditContext) -> None:
|
|
334
|
+
"""The producing layer grows a new, trained output unit; the consuming
|
|
335
|
+
layer meets it with weights born at exactly 0."""
|
|
336
|
+
p, c = self.layers[i], self._consumer(i)
|
|
337
|
+
k = p.add_unit()
|
|
338
|
+
bound = 1.0 / math.sqrt(max(p.in_features, 1))
|
|
339
|
+
w = uniform((p.in_features,), bound, ctx.generator, p.weight)
|
|
340
|
+
b = float(uniform((), bound, ctx.generator, p.bias))
|
|
341
|
+
p.add_or(k, w, b, 1.0, born=ctx.step)
|
|
342
|
+
p.unit_probe[k] = True
|
|
343
|
+
p.unit_born[k] = ctx.step
|
|
344
|
+
if self._crosses_probe(i):
|
|
345
|
+
# the depth probe carries it: a new input and a unit whose one Or
|
|
346
|
+
# is the carrier of that input (w = e_k, b = 0, a = 1)
|
|
347
|
+
P = self.layers[i + 1]
|
|
348
|
+
P.add_input()
|
|
349
|
+
kp = P.add_unit()
|
|
350
|
+
e = P.weight.new_zeros(P.in_features)
|
|
351
|
+
e[kp] = 1.0
|
|
352
|
+
P.add_or(kp, e, 0.0, 1.0, born=ctx.step)
|
|
353
|
+
c.add_input()
|
|
354
|
+
|
|
355
|
+
def _input_means(self, i: int) -> torch.Tensor | None:
|
|
356
|
+
"""E[x] of the consumer of junction i over the epoch (None without data)."""
|
|
357
|
+
c = self._consumer(i)
|
|
358
|
+
ns = float(c.stat_ns)
|
|
359
|
+
return (c.stat_sx / ns).clone() if ns > 0 else None
|
|
360
|
+
|
|
361
|
+
@torch.no_grad()
|
|
362
|
+
def _drop_coordinate(self, i: int, k: int, mean: float | None = None) -> None:
|
|
363
|
+
"""Drop output coordinate k of layer i; the consumer keeps the mean of
|
|
364
|
+
what it contributed (b += w E[x_k]). ``mean`` is E[x_k] taken before
|
|
365
|
+
any other drop on the junction (a drop resets the consumer's statistics)."""
|
|
366
|
+
p, c = self.layers[i], self._consumer(i)
|
|
367
|
+
if mean is None:
|
|
368
|
+
means = self._input_means(i)
|
|
369
|
+
mean = None if means is None else means[k]
|
|
370
|
+
if mean is not None:
|
|
371
|
+
c.bias += c.weight[..., k] * mean
|
|
372
|
+
if self._crosses_probe(i):
|
|
373
|
+
P = self.layers[i + 1]
|
|
374
|
+
P.drop_unit(k)
|
|
375
|
+
P.drop_input(k)
|
|
376
|
+
p.drop_unit(k)
|
|
377
|
+
c.drop_input(k)
|
|
378
|
+
|
|
379
|
+
@torch.no_grad()
|
|
380
|
+
def _insert_depth_probe(self, g: int, ctx: EditContext) -> None:
|
|
381
|
+
"""Identity layer of width d at gap g (in front of layer g)."""
|
|
382
|
+
layers = self.layers
|
|
383
|
+
c = layers[g]
|
|
384
|
+
alpha, beta = fold_from_stats(c, u_to_x=True)
|
|
385
|
+
# A width probe on the junction in front of c would be a coordinate
|
|
386
|
+
# the identity layer has to carry; the reference retires it first.
|
|
387
|
+
# (Passing units through the probe, the identity carries it.)
|
|
388
|
+
if g > 0 and not layers[g - 1].probe and not self.through:
|
|
389
|
+
k = self._probe_unit(layers[g - 1])
|
|
390
|
+
if k is not None:
|
|
391
|
+
self._drop_coordinate(g - 1, k)
|
|
392
|
+
keep = [j for j in range(alpha.shape[0]) if j != k]
|
|
393
|
+
alpha, beta = alpha[keep], beta[keep]
|
|
394
|
+
d = c.in_features
|
|
395
|
+
l = AndOr(d, d, 1, device=c.weight.device, dtype=c.weight.dtype)
|
|
396
|
+
w = uniform((d, 1, d), ctx.noise, ctx.generator, l.weight)
|
|
397
|
+
w[:, 0, :] += torch.eye(d, dtype=w.dtype, device=w.device)
|
|
398
|
+
l.weight.copy_(w)
|
|
399
|
+
l.bias.copy_(uniform((d, 1), ctx.noise, ctx.generator, l.bias))
|
|
400
|
+
l.assembly.fill_(1.0)
|
|
401
|
+
l.or_born.fill_(ctx.step)
|
|
402
|
+
l.probe, l.born, l.track_stats = True, ctx.step, self._tracking
|
|
403
|
+
l.edit_sink = self._sink
|
|
404
|
+
fold_apply(c, alpha, beta)
|
|
405
|
+
c.reset_stats()
|
|
406
|
+
self.model.insert_layer(g, l)
|
|
407
|
+
|
|
408
|
+
def _remove_identity_layer(self, i: int) -> None:
|
|
409
|
+
"""Remove a (near-)identity layer; layer i+1 absorbs it."""
|
|
410
|
+
l, c = self.layers[i], self.layers[i + 1]
|
|
411
|
+
alpha, beta = fold_from_stats(l, u_to_x=False)
|
|
412
|
+
fold_apply(c, alpha, beta)
|
|
413
|
+
c.reset_stats()
|
|
414
|
+
self.model.remove_layer(i)
|
|
415
|
+
|
|
416
|
+
# -------------------------------------------------- measurement / pruning
|
|
417
|
+
|
|
418
|
+
def ablate(self, item: Item):
|
|
419
|
+
if item.axis == WIDTH:
|
|
420
|
+
i, k = item.address
|
|
421
|
+
return _ablate_col(self._consumer(i), k)
|
|
422
|
+
if item.axis == DEGREE:
|
|
423
|
+
i, k, r = item.address
|
|
424
|
+
return _ablate_or(self.layers[i], k, r)
|
|
425
|
+
layer = self.layers[item.address[0]]
|
|
426
|
+
if layer.in_features != layer.out_features:
|
|
427
|
+
return None
|
|
428
|
+
return _ablate_layer(layer)
|
|
429
|
+
|
|
430
|
+
def cost(self, item: Item) -> int:
|
|
431
|
+
if item.axis == WIDTH:
|
|
432
|
+
i, j = item.address
|
|
433
|
+
p, c = self.layers[i], self._consumer(i)
|
|
434
|
+
cost = int(p.mask[j].sum()) * (p.in_features + 2) + int(c.mask.sum())
|
|
435
|
+
if self._crosses_probe(i): # its carrier Or and its probe column
|
|
436
|
+
P = self.layers[i + 1]
|
|
437
|
+
cost += (P.in_features + 2) + int(P.mask.sum())
|
|
438
|
+
return cost
|
|
439
|
+
if item.axis == DEGREE:
|
|
440
|
+
return self.layers[item.address[0]].in_features + 2
|
|
441
|
+
return self.layers[item.address[0]].num_params()
|
|
442
|
+
|
|
443
|
+
def trial_remove(self, item: Item) -> Trial | None:
|
|
444
|
+
"""Remove square layer i; layer i+1 absorbs the fold."""
|
|
445
|
+
i = item.address[0]
|
|
446
|
+
l, c = self.layers[i], self.layers[i + 1]
|
|
447
|
+
if l.in_features != l.out_features:
|
|
448
|
+
return None
|
|
449
|
+
snap = _snap(c, "weight", "bias", "adam_m_weight", "adam_v_weight")
|
|
450
|
+
alpha, beta = fold_from_stats(l, u_to_x=False)
|
|
451
|
+
fold_apply(c, alpha, beta)
|
|
452
|
+
self.model.remove_layer(i)
|
|
453
|
+
|
|
454
|
+
def undo() -> None:
|
|
455
|
+
self.model.insert_layer(i, l)
|
|
456
|
+
_restore(c, snap)
|
|
457
|
+
|
|
458
|
+
return Trial(undo=undo, commit=c.reset_stats)
|
|
459
|
+
|
|
460
|
+
def removal_order(self, axis: str) -> Sequence[Item]:
|
|
461
|
+
if axis == DEPTH: # never the output layer
|
|
462
|
+
return [Item(DEPTH, (i,)) for i in range(len(self.layers) - 1)]
|
|
463
|
+
return []
|
|
464
|
+
|
|
465
|
+
def prune_candidates(self) -> Sequence[Item]:
|
|
466
|
+
out = []
|
|
467
|
+
L = self.layers
|
|
468
|
+
for i, l in enumerate(L):
|
|
469
|
+
out += [Item(DEGREE, (i, k, r)) for k, r in l.mask.nonzero().tolist()]
|
|
470
|
+
if i + 1 < len(L):
|
|
471
|
+
out += [Item(WIDTH, (i, k)) for k in range(l.out_features)]
|
|
472
|
+
return out
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def _gone(removed: Sequence[Item]):
|
|
476
|
+
units = {it.address for it in removed if it.axis == WIDTH}
|
|
477
|
+
ors = {it.address for it in removed if it.axis == DEGREE}
|
|
478
|
+
return units, ors
|
|
479
|
+
|
|
480
|
+
def can_remove(self, item: Item, removed: Sequence[Item]) -> bool:
|
|
481
|
+
units, ors = self._gone(removed)
|
|
482
|
+
i, k = item.address[:2]
|
|
483
|
+
if (i, k) in units:
|
|
484
|
+
return False
|
|
485
|
+
if item.axis == DEGREE: # keep at least one Or per unit
|
|
486
|
+
left = int(self.layers[i].mask[k].sum()) - sum(
|
|
487
|
+
1 for a in ors if a[0] == i and a[1] == k)
|
|
488
|
+
return left > 1
|
|
489
|
+
left = self.layers[i].out_features - sum(1 for a in units if a[0] == i)
|
|
490
|
+
return left > 1 # keep at least one coordinate per junction
|
|
491
|
+
|
|
492
|
+
def params_without(self, removed: Sequence[Item]) -> int:
|
|
493
|
+
"""(live Ors) x (live inputs + 2) per surviving unit. (The C reference
|
|
494
|
+
sums a per-item tally that can count an Or twice; see DIFFERENCES.md.)"""
|
|
495
|
+
units, ors = self._gone(removed)
|
|
496
|
+
total, n_in = 0, self.layers[0].in_features
|
|
497
|
+
for i, l in enumerate(self.layers):
|
|
498
|
+
n_out = 0
|
|
499
|
+
for k, n_or in enumerate(l.num_ors().tolist()):
|
|
500
|
+
if (i, k) in units:
|
|
501
|
+
continue
|
|
502
|
+
n_out += 1
|
|
503
|
+
gone = sum(1 for a in ors if a[0] == i and a[1] == k)
|
|
504
|
+
total += (n_or - gone) * (n_in + 2)
|
|
505
|
+
n_in = n_out
|
|
506
|
+
return total
|
|
507
|
+
|
|
508
|
+
def commit_removals(self, removed: Sequence[Item],
|
|
509
|
+
axes: Sequence[str] = (WIDTH, DEGREE)) -> dict[str, int]:
|
|
510
|
+
"""Remove the chosen items, then clear the probe flags of what stays, on
|
|
511
|
+
the given axes only. Under the BIC rule the items were ablated (an
|
|
512
|
+
identity: exact); under the threshold rule they sit inside the band."""
|
|
513
|
+
units, ors = self._gone(removed)
|
|
514
|
+
c = {"or_add": 0, "or_drop": 0, "and_add": 0, "and_drop": 0}
|
|
515
|
+
layers = self.layers
|
|
516
|
+
do_width, do_degree = WIDTH in axes, DEGREE in axes
|
|
517
|
+
means = {i: self._input_means(i) for i in range(len(layers) - 1)
|
|
518
|
+
if any(a[0] == i for a in units)}
|
|
519
|
+
for i in reversed(range(len(layers))):
|
|
520
|
+
l = layers[i]
|
|
521
|
+
for k in reversed(range(l.out_features)):
|
|
522
|
+
gone = (i, k) in units
|
|
523
|
+
for r in reversed(range(l.degree if do_degree else 0)):
|
|
524
|
+
if not bool(l.mask[k, r]):
|
|
525
|
+
continue
|
|
526
|
+
was_probe = bool(l.or_probe[k, r])
|
|
527
|
+
if (i, k, r) in ors and not gone:
|
|
528
|
+
l.drop_or(k, r)
|
|
529
|
+
if not was_probe:
|
|
530
|
+
c["and_drop"] += 1
|
|
531
|
+
elif was_probe and not gone:
|
|
532
|
+
l.or_probe[k, r] = False
|
|
533
|
+
c["and_add"] += 1
|
|
534
|
+
if do_width and i + 1 < len(layers):
|
|
535
|
+
was_probe = bool(l.unit_probe[k])
|
|
536
|
+
if gone:
|
|
537
|
+
m = means.get(i)
|
|
538
|
+
self._drop_coordinate(i, k, None if m is None else m[k])
|
|
539
|
+
if not was_probe:
|
|
540
|
+
c["or_drop"] += 1
|
|
541
|
+
elif was_probe:
|
|
542
|
+
l.unit_probe[k] = False
|
|
543
|
+
c["or_add"] += 1
|
|
544
|
+
for l in layers:
|
|
545
|
+
l.compact()
|
|
546
|
+
return c
|
torch_type_nn/edits.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Keep a stock ``torch.optim`` optimizer in step with structural edits.
|
|
2
|
+
|
|
3
|
+
PyTorch optimizers key their state by the ``Parameter`` object, and
|
|
4
|
+
structural edits replace parameters with tensors of a new shape. An
|
|
5
|
+
:class:`Edit` records one such replacement together with an *index map*
|
|
6
|
+
along one dimension: ``index[j]`` is the position in the old tensor that
|
|
7
|
+
slice ``j`` of the new tensor came from, or ``-1`` for a new slice.
|
|
8
|
+
|
|
9
|
+
:func:`follow_structure` applies a list of edits to an optimizer. Every state
|
|
10
|
+
tensor with the old parameter's shape (Adam's ``exp_avg``/``exp_avg_sq``,
|
|
11
|
+
SGD's ``momentum_buffer``, ...) is remapped with the same index map, new
|
|
12
|
+
slices starting at 0. Scalar state (Adam's ``step``) is kept as is, so a new
|
|
13
|
+
slice inherits the tensor's step count; :class:`~torch_type_nn.optim.TypeAdam`
|
|
14
|
+
avoids that with per-Or step counts. Parameters that appeared or disappeared
|
|
15
|
+
without an edit (a new or removed layer) are added to or dropped from the
|
|
16
|
+
optimizer.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
from torch import nn
|
|
25
|
+
|
|
26
|
+
__all__ = ["Edit", "follow_structure", "grow_index", "keep_index"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Edit:
|
|
31
|
+
"""``old`` was replaced by ``new``; along ``dim``, ``new[j]`` came from ``old[index[j]]``."""
|
|
32
|
+
|
|
33
|
+
old: nn.Parameter
|
|
34
|
+
new: nn.Parameter
|
|
35
|
+
dim: int
|
|
36
|
+
index: torch.Tensor
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def grow_index(n: int, device=None) -> torch.Tensor:
|
|
40
|
+
"""Index map of ``n`` kept slices followed by one new slice."""
|
|
41
|
+
return torch.cat([torch.arange(n, device=device), torch.full((1,), -1, device=device)])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def keep_index(keep: torch.Tensor) -> torch.Tensor:
|
|
45
|
+
"""Index map of a selection (``index_select`` with ``keep``)."""
|
|
46
|
+
return keep.clone()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def remap_tensor(t: torch.Tensor, new_shape: torch.Size, dim: int,
|
|
50
|
+
index: torch.Tensor) -> torch.Tensor:
|
|
51
|
+
"""``t`` rearranged by the index map, new slices zero."""
|
|
52
|
+
out = t.new_zeros(new_shape)
|
|
53
|
+
index = index.to(t.device)
|
|
54
|
+
src = index >= 0
|
|
55
|
+
if bool(src.any()):
|
|
56
|
+
pos = src.nonzero().flatten()
|
|
57
|
+
out.index_copy_(dim, pos, t.index_select(dim, index[src]))
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@torch.no_grad()
|
|
62
|
+
def follow_structure(optimizer: torch.optim.Optimizer, model: nn.Module,
|
|
63
|
+
edits: list[Edit]) -> None:
|
|
64
|
+
"""Bring ``optimizer`` in line with ``model`` after structural edits.
|
|
65
|
+
|
|
66
|
+
1. Each edit, in order: the old parameter is replaced by the new one in
|
|
67
|
+
its param group, and its state is remapped along the edit's index map.
|
|
68
|
+
2. Parameters of ``model`` the optimizer does not know get added to the
|
|
69
|
+
first param group, with fresh state.
|
|
70
|
+
3. Parameters the optimizer holds that are no longer in ``model`` are
|
|
71
|
+
dropped with their state.
|
|
72
|
+
"""
|
|
73
|
+
for e in edits:
|
|
74
|
+
for group in optimizer.param_groups:
|
|
75
|
+
for i, p in enumerate(group["params"]):
|
|
76
|
+
if p is e.old:
|
|
77
|
+
group["params"][i] = e.new
|
|
78
|
+
st = optimizer.state.pop(e.old, None)
|
|
79
|
+
if st:
|
|
80
|
+
optimizer.state[e.new] = {
|
|
81
|
+
k: (remap_tensor(v, e.new.shape, e.dim, e.index)
|
|
82
|
+
if isinstance(v, torch.Tensor) and v.shape == e.old.shape and v.dim() > 0
|
|
83
|
+
else v)
|
|
84
|
+
for k, v in st.items()}
|
|
85
|
+
live = list(model.parameters())
|
|
86
|
+
live_ids = {id(p) for p in live}
|
|
87
|
+
held = set()
|
|
88
|
+
for group in optimizer.param_groups:
|
|
89
|
+
kept = []
|
|
90
|
+
for p in group["params"]:
|
|
91
|
+
if id(p) in live_ids and id(p) not in held:
|
|
92
|
+
kept.append(p)
|
|
93
|
+
held.add(id(p))
|
|
94
|
+
else:
|
|
95
|
+
optimizer.state.pop(p, None)
|
|
96
|
+
group["params"] = kept
|
|
97
|
+
missing = [p for p in live if id(p) not in held]
|
|
98
|
+
if missing:
|
|
99
|
+
optimizer.param_groups[0]["params"].extend(missing)
|