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,38 @@
|
|
|
1
|
+
"""torch-type-nn: type-nn for PyTorch.
|
|
2
|
+
|
|
3
|
+
A type-nn layer maps x to z; output unit k is one And over its Ors r:
|
|
4
|
+
|
|
5
|
+
Or_kr = w_kr . x + b_kr sum type
|
|
6
|
+
A_k = prod_r Or_kr ^ a_kr product type, a_kr >= 1
|
|
7
|
+
z_k = sign(A_k) ln(1 + |A_k|) partition function
|
|
8
|
+
|
|
9
|
+
Reference implementation and design notes:
|
|
10
|
+
https://github.com/hadilq/type-nn, https://hadilq.com/posts/train-the-knowledge/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
14
|
+
|
|
15
|
+
from . import functional
|
|
16
|
+
from .adapters import MLPAdapter, TypeNNAdapter
|
|
17
|
+
from .edits import Edit, follow_structure
|
|
18
|
+
from .functional import and_or, readout
|
|
19
|
+
from .layer import AndOr
|
|
20
|
+
from .mlp import ScalableMLP, TrackedLinear
|
|
21
|
+
from .network import TypeNN, birth_depth
|
|
22
|
+
from .optim import TypeAdam, keep_invariants
|
|
23
|
+
from .protocol import DEGREE, DEPTH, WIDTH, EditContext, Item, Scalable, Trial
|
|
24
|
+
from .scaling import StructureScaler
|
|
25
|
+
from .train import FitResult, fit, mse_loss
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
__version__ = version("torch-type-nn")
|
|
29
|
+
except PackageNotFoundError: # running from a checkout without install
|
|
30
|
+
__version__ = "0.0.0+local"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AndOr", "TypeNN", "TypeAdam", "keep_invariants", "StructureScaler", "fit", "FitResult",
|
|
34
|
+
"mse_loss",
|
|
35
|
+
"birth_depth", "and_or", "readout", "functional", "__version__",
|
|
36
|
+
"Scalable", "Item", "Trial", "EditContext", "WIDTH", "DEGREE", "DEPTH",
|
|
37
|
+
"TypeNNAdapter", "MLPAdapter", "ScalableMLP", "TrackedLinear", "Edit", "follow_structure",
|
|
38
|
+
]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""The MLP adapter: :class:`~torch_type_nn.mlp.ScalableMLP` as a :class:`Scalable`.
|
|
2
|
+
|
|
3
|
+
Addresses:
|
|
4
|
+
|
|
5
|
+
* ``Item(WIDTH, (i, k))``: hidden unit ``k`` of linear ``i`` (its row in
|
|
6
|
+
linear ``i`` and its column in the *consumer*, the next linear that is not
|
|
7
|
+
the depth probe); sites are the non-probe hidden linears ``i``. When the
|
|
8
|
+
depth probe sits between producer and consumer, the unit passes through
|
|
9
|
+
it on an identity entry (weight exactly 1), which keeps the probe square
|
|
10
|
+
and the edit exact, so a depth probe never blocks width growth.
|
|
11
|
+
* ``Item(DEPTH, (i,))``: linear ``i`` (with its ReLU); sites are gaps ``g``
|
|
12
|
+
in front of linear ``g``, counted in the stack without the depth probe.
|
|
13
|
+
Only gaps ``g >= 1`` qualify: they follow a ReLU, so the identity layer is
|
|
14
|
+
exact there.
|
|
15
|
+
* No degree axis.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from ..layer import uniform
|
|
26
|
+
from ..mlp import ScalableMLP, TrackedLinear
|
|
27
|
+
from ..protocol import DEPTH, WIDTH, EditContext, Item, Trial
|
|
28
|
+
|
|
29
|
+
__all__ = ["MLPAdapter"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MLPAdapter:
|
|
33
|
+
""":class:`ScalableMLP` under the :class:`~torch_type_nn.protocol.Scalable` protocol."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, model: ScalableMLP) -> None:
|
|
36
|
+
self.model = model
|
|
37
|
+
self._sink: list | None = None
|
|
38
|
+
self._tracking = False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def L(self):
|
|
42
|
+
return self.model.linears
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------ bookkeeping
|
|
45
|
+
|
|
46
|
+
def num_params(self) -> int:
|
|
47
|
+
return self.model.num_params()
|
|
48
|
+
|
|
49
|
+
def set_tracking(self, on: bool) -> None:
|
|
50
|
+
self._tracking = on
|
|
51
|
+
for lin in self.L:
|
|
52
|
+
lin.track_stats = on
|
|
53
|
+
|
|
54
|
+
def reset_stats(self) -> None:
|
|
55
|
+
for lin in self.L:
|
|
56
|
+
lin.reset_stats()
|
|
57
|
+
|
|
58
|
+
def set_edit_sink(self, sink: list | None) -> None:
|
|
59
|
+
self._sink = sink
|
|
60
|
+
for lin in self.L:
|
|
61
|
+
lin.edit_sink = sink
|
|
62
|
+
|
|
63
|
+
def finalize(self) -> None:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# ----------------------------------------------------------------- probes
|
|
67
|
+
|
|
68
|
+
def _probe_layer(self) -> int | None:
|
|
69
|
+
for i, lin in enumerate(self.L):
|
|
70
|
+
if lin.probe:
|
|
71
|
+
return i
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _probe_unit(lin: TrackedLinear) -> int | None:
|
|
76
|
+
k = lin.unit_probe.nonzero()
|
|
77
|
+
return int(k[0]) if len(k) else None
|
|
78
|
+
|
|
79
|
+
def _consumer(self, i: int) -> TrackedLinear:
|
|
80
|
+
L = self.L
|
|
81
|
+
return L[i + 2] if L[i + 1].probe else L[i + 1]
|
|
82
|
+
|
|
83
|
+
def probe_sites(self, axis: str) -> Sequence:
|
|
84
|
+
if axis == WIDTH:
|
|
85
|
+
L = self.L
|
|
86
|
+
return [i for i in range(len(L) - 1) if not L[i].probe]
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
def probe_at(self, axis: str, site=None) -> Item | None:
|
|
90
|
+
if axis == WIDTH:
|
|
91
|
+
k = self._probe_unit(self.L[site])
|
|
92
|
+
return None if k is None else Item(WIDTH, (site, k))
|
|
93
|
+
if axis == DEPTH:
|
|
94
|
+
p = self._probe_layer()
|
|
95
|
+
return None if p is None else Item(DEPTH, (p,))
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def add_probe(self, axis: str, site, ctx: EditContext) -> None:
|
|
99
|
+
if axis == WIDTH:
|
|
100
|
+
self._add_width_probe(site, ctx)
|
|
101
|
+
elif axis == DEPTH:
|
|
102
|
+
self._insert_depth_probe(site, ctx)
|
|
103
|
+
|
|
104
|
+
def promote(self, item: Item) -> None:
|
|
105
|
+
if item.axis == WIDTH:
|
|
106
|
+
i, k = item.address
|
|
107
|
+
self.L[i].unit_probe[k] = False
|
|
108
|
+
else:
|
|
109
|
+
self.L[item.address[0]].probe = False
|
|
110
|
+
|
|
111
|
+
def born(self, item: Item) -> int:
|
|
112
|
+
if item.axis == WIDTH:
|
|
113
|
+
i, k = item.address
|
|
114
|
+
return int(self.L[i].unit_born[k])
|
|
115
|
+
return self.L[item.address[0]].born
|
|
116
|
+
|
|
117
|
+
def displacement(self, item: Item) -> float:
|
|
118
|
+
if item.axis == WIDTH:
|
|
119
|
+
i, k = item.address
|
|
120
|
+
col = self._consumer(i).weight.detach()[:, k]
|
|
121
|
+
return math.sqrt(float((col * col).mean())) if col.numel() else 0.0
|
|
122
|
+
lin = self.L[item.address[0]]
|
|
123
|
+
if lin.in_features != lin.out_features:
|
|
124
|
+
return math.inf
|
|
125
|
+
W, b = lin.weight.detach(), lin.bias.detach()
|
|
126
|
+
eye = torch.eye(lin.in_features, dtype=W.dtype, device=W.device)
|
|
127
|
+
s = float(((W - eye) ** 2).sum() + (b * b).sum())
|
|
128
|
+
return math.sqrt(s / lin.num_params())
|
|
129
|
+
|
|
130
|
+
def best_site(self, axis: str, moving: Item | None = None):
|
|
131
|
+
"""Loudest gap by mean |dL/dx| at its input, among gaps after a ReLU."""
|
|
132
|
+
probe = None if moving is None else moving.address[0]
|
|
133
|
+
|
|
134
|
+
def loud(lin: TrackedLinear) -> float:
|
|
135
|
+
ns = float(lin.stat_ns)
|
|
136
|
+
return float(lin.stat_gin) / ns if ns else 0.0
|
|
137
|
+
|
|
138
|
+
best_g, best, g = 1, -1.0, 0
|
|
139
|
+
for i, lin in enumerate(self.L):
|
|
140
|
+
if i == probe:
|
|
141
|
+
continue
|
|
142
|
+
if g >= 1:
|
|
143
|
+
v = loud(lin)
|
|
144
|
+
if probe is not None and i == probe + 1:
|
|
145
|
+
v = max(v, loud(self.L[probe]))
|
|
146
|
+
if v > best:
|
|
147
|
+
best, best_g = v, g
|
|
148
|
+
g += 1
|
|
149
|
+
return best_g
|
|
150
|
+
|
|
151
|
+
def site_of(self, item: Item):
|
|
152
|
+
return item.address[0]
|
|
153
|
+
|
|
154
|
+
def remove_probe(self, item: Item) -> None:
|
|
155
|
+
i = item.address[0]
|
|
156
|
+
del self.L[i]
|
|
157
|
+
self.L[i].reset_stats()
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------ structural edits
|
|
160
|
+
|
|
161
|
+
def _add_width_probe(self, i: int, ctx: EditContext) -> None:
|
|
162
|
+
"""Linear i grows a trained hidden unit; the consumer reads it with zeros
|
|
163
|
+
(through an identity entry of the depth probe, if one is in between)."""
|
|
164
|
+
p, c = self.L[i], self._consumer(i)
|
|
165
|
+
bound = 1.0 / math.sqrt(max(p.in_features, 1))
|
|
166
|
+
w = uniform((p.in_features,), bound, ctx.generator, p.weight)
|
|
167
|
+
b = float(uniform((), bound, ctx.generator, p.bias))
|
|
168
|
+
k = p.add_out(w, b, probe=True, born=ctx.step)
|
|
169
|
+
if self.L[i + 1].probe:
|
|
170
|
+
self._pass_through(self.L[i + 1], k)
|
|
171
|
+
c.add_in()
|
|
172
|
+
|
|
173
|
+
@torch.no_grad()
|
|
174
|
+
def _pass_through(self, P: TrackedLinear, k: int) -> None:
|
|
175
|
+
"""Square identity layer P grows input and output k with weight 1."""
|
|
176
|
+
P.add_in()
|
|
177
|
+
e = P.weight.new_zeros(P.in_features)
|
|
178
|
+
e[k] = 1.0
|
|
179
|
+
P.add_out(e, 0.0)
|
|
180
|
+
|
|
181
|
+
def _input_means(self, i: int) -> torch.Tensor | None:
|
|
182
|
+
c = self._consumer(i)
|
|
183
|
+
ns = float(c.stat_ns)
|
|
184
|
+
return (c.stat_sx / ns).clone() if ns > 0 else None
|
|
185
|
+
|
|
186
|
+
@torch.no_grad()
|
|
187
|
+
def _drop_coordinate(self, i: int, k: int, mean: float | None = None) -> None:
|
|
188
|
+
"""Drop hidden unit k of linear i; the consumer keeps the mean of what it
|
|
189
|
+
read (b += w E[a_k]), with E[a_k] taken before any drop on the junction."""
|
|
190
|
+
p, c = self.L[i], self._consumer(i)
|
|
191
|
+
if mean is None:
|
|
192
|
+
means = self._input_means(i)
|
|
193
|
+
mean = None if means is None else means[k]
|
|
194
|
+
if mean is not None:
|
|
195
|
+
c.bias += c.weight[:, k] * mean
|
|
196
|
+
if self.L[i + 1].probe:
|
|
197
|
+
self.L[i + 1].drop_in(k)
|
|
198
|
+
self.L[i + 1].drop_out(k)
|
|
199
|
+
p.drop_out(k)
|
|
200
|
+
c.drop_in(k)
|
|
201
|
+
|
|
202
|
+
@torch.no_grad()
|
|
203
|
+
def _insert_depth_probe(self, g: int, ctx: EditContext) -> None:
|
|
204
|
+
"""ReLU(I a + 0) = a for a >= 0: an identity layer in front of linear g.
|
|
205
|
+
It carries every unit of the junction, width probes included."""
|
|
206
|
+
L = self.L
|
|
207
|
+
c = L[g]
|
|
208
|
+
d = c.in_features
|
|
209
|
+
l = TrackedLinear(d, d, device=c.weight.device, dtype=c.weight.dtype)
|
|
210
|
+
w = uniform((d, d), ctx.noise, ctx.generator, l.weight)
|
|
211
|
+
w += torch.eye(d, dtype=w.dtype, device=w.device)
|
|
212
|
+
l.weight.copy_(w)
|
|
213
|
+
l.bias.copy_(uniform((d,), ctx.noise, ctx.generator, l.bias))
|
|
214
|
+
l.probe, l.born = True, ctx.step
|
|
215
|
+
l.track_stats, l.edit_sink = self._tracking, self._sink
|
|
216
|
+
c.reset_stats()
|
|
217
|
+
L.insert(g, l)
|
|
218
|
+
|
|
219
|
+
# -------------------------------------------------- measurement / pruning
|
|
220
|
+
|
|
221
|
+
def ablate(self, item: Item):
|
|
222
|
+
if item.axis == WIDTH:
|
|
223
|
+
i, k = item.address
|
|
224
|
+
c = self._consumer(i)
|
|
225
|
+
with torch.no_grad():
|
|
226
|
+
s = c.weight[:, k].clone()
|
|
227
|
+
c.weight[:, k] = 0.0
|
|
228
|
+
|
|
229
|
+
def undo() -> None:
|
|
230
|
+
with torch.no_grad():
|
|
231
|
+
c.weight[:, k] = s
|
|
232
|
+
return undo
|
|
233
|
+
i = item.address[0]
|
|
234
|
+
lin = self.L[i]
|
|
235
|
+
if i == 0 or lin.in_features != lin.out_features:
|
|
236
|
+
return None # no exact identity in front of layer 0
|
|
237
|
+
with torch.no_grad():
|
|
238
|
+
W, b = lin.weight.clone(), lin.bias.clone()
|
|
239
|
+
lin.weight.copy_(torch.eye(lin.in_features, dtype=W.dtype, device=W.device))
|
|
240
|
+
lin.bias.zero_()
|
|
241
|
+
|
|
242
|
+
def undo_layer() -> None:
|
|
243
|
+
with torch.no_grad():
|
|
244
|
+
lin.weight.copy_(W)
|
|
245
|
+
lin.bias.copy_(b)
|
|
246
|
+
return undo_layer
|
|
247
|
+
|
|
248
|
+
def cost(self, item: Item) -> int:
|
|
249
|
+
if item.axis == WIDTH:
|
|
250
|
+
i, _ = item.address
|
|
251
|
+
cost = (self.L[i].in_features + 1) + self._consumer(i).out_features
|
|
252
|
+
if self.L[i + 1].probe:
|
|
253
|
+
cost += self.L[i + 1].in_features + 1 + self.L[i + 1].out_features
|
|
254
|
+
return cost
|
|
255
|
+
return self.L[item.address[0]].num_params()
|
|
256
|
+
|
|
257
|
+
def trial_remove(self, item: Item) -> Trial | None:
|
|
258
|
+
i = item.address[0]
|
|
259
|
+
L = self.L
|
|
260
|
+
lin = L[i]
|
|
261
|
+
if i == 0 or i >= len(L) - 1 or lin.in_features != lin.out_features:
|
|
262
|
+
return None
|
|
263
|
+
del L[i]
|
|
264
|
+
|
|
265
|
+
def undo() -> None:
|
|
266
|
+
L.insert(i, lin)
|
|
267
|
+
|
|
268
|
+
return Trial(undo=undo, commit=L[i].reset_stats)
|
|
269
|
+
|
|
270
|
+
def removal_order(self, axis: str) -> Sequence[Item]:
|
|
271
|
+
if axis == DEPTH: # hidden-to-hidden layers only
|
|
272
|
+
return [Item(DEPTH, (i,)) for i in range(1, len(self.L) - 1)]
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
def prune_candidates(self) -> Sequence[Item]:
|
|
276
|
+
return [Item(WIDTH, (i, k)) for i in range(len(self.L) - 1) if not self.L[i].probe
|
|
277
|
+
for k in range(self.L[i].out_features)]
|
|
278
|
+
|
|
279
|
+
def can_remove(self, item: Item, removed: Sequence[Item]) -> bool:
|
|
280
|
+
units = {it.address for it in removed}
|
|
281
|
+
i, k = item.address
|
|
282
|
+
if (i, k) in units:
|
|
283
|
+
return False
|
|
284
|
+
left = self.L[i].out_features - sum(1 for a in units if a[0] == i)
|
|
285
|
+
return left > 1
|
|
286
|
+
|
|
287
|
+
def params_without(self, removed: Sequence[Item]) -> int:
|
|
288
|
+
units = {it.address for it in removed}
|
|
289
|
+
gone = [0] * len(self.L)
|
|
290
|
+
for i, _ in units:
|
|
291
|
+
gone[i] += 1
|
|
292
|
+
if self.L[i + 1].probe: # the pass-through shrinks with it
|
|
293
|
+
gone[i + 1] += 1
|
|
294
|
+
total, n_in = 0, self.L[0].in_features
|
|
295
|
+
for i, lin in enumerate(self.L):
|
|
296
|
+
n_out = lin.out_features - gone[i]
|
|
297
|
+
total += n_out * (n_in + 1)
|
|
298
|
+
n_in = n_out
|
|
299
|
+
return total
|
|
300
|
+
|
|
301
|
+
def commit_removals(self, removed: Sequence[Item],
|
|
302
|
+
axes: Sequence[str] = (WIDTH,)) -> dict[str, int]:
|
|
303
|
+
units = {it.address for it in removed}
|
|
304
|
+
c = {"or_add": 0, "or_drop": 0}
|
|
305
|
+
if WIDTH not in axes:
|
|
306
|
+
return c
|
|
307
|
+
means = {i: self._input_means(i) for i in {a[0] for a in units}}
|
|
308
|
+
for i in reversed(range(len(self.L) - 1)):
|
|
309
|
+
lin = self.L[i]
|
|
310
|
+
if lin.probe:
|
|
311
|
+
continue
|
|
312
|
+
for k in reversed(range(lin.out_features)):
|
|
313
|
+
was_probe = bool(lin.unit_probe[k])
|
|
314
|
+
if (i, k) in units:
|
|
315
|
+
m = means.get(i)
|
|
316
|
+
self._drop_coordinate(i, k, None if m is None else m[k])
|
|
317
|
+
if not was_probe:
|
|
318
|
+
c["or_drop"] += 1
|
|
319
|
+
elif was_probe:
|
|
320
|
+
lin.unit_probe[k] = False
|
|
321
|
+
c["or_add"] += 1
|
|
322
|
+
return c
|