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.
@@ -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,6 @@
1
+ """Adapters that put model families under the scaling protocol."""
2
+
3
+ from .mlp import MLPAdapter
4
+ from .typenn import TypeNNAdapter
5
+
6
+ __all__ = ["TypeNNAdapter", "MLPAdapter"]
@@ -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