mcode-neural 0.5.0__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,447 @@
1
+ # region F I L E
2
+ # <copyright file="mcode-neural/src/mcode_neural/network.py" company="MicroCODE Incorporated">Copyright © 2026 MicroCODE, Inc. Troy, MI</copyright><author>Timothy J. McGuire</author>
3
+ # region M O D U L E
4
+ # region D O C U M E N T A T I O N
5
+ """
6
+ Project: MicroCODE Python Packages
7
+ Customer: Internal + MIT xPRO Course
8
+ @module 'mcode_neural.network'
9
+ @memberof mcode_neural
10
+ @created November 2026
11
+ @author Timothy McGuire, MicroCODE, Inc.
12
+ @description >
13
+ MicroCODE Shared Network Library
14
+
15
+ LICENSE:
16
+ --------
17
+ MIT License: MicroCODE.mcode-neural
18
+
19
+ Copyright (c) 2026 Timothy McGuire, MicroCODE, Inc.
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
38
+
39
+
40
+ DESCRIPTION:
41
+ ------------
42
+ This module implements what every MicroCODE network is: 'NETWORK_KINDS', how
43
+ each kind of network's layers are found and read, a PyTorch module's or our
44
+ NumPy single neuron's; and 'Network', one network of a Model, a view of its
45
+ module holding its 'Layer's. A Model holds one or more Networks, each named
46
+ for the Model attribute that holds it, e.g. an autoencoder's "encoder" and
47
+ "decoder", and each declaring its 'source', what it reads: the data, or an
48
+ earlier network. The Model's own 'forward()' is the one record of how its
49
+ networks combine; building a Model runs it once, with hooks, to find each
50
+ network's layers and shapes, and fails naming the network when its declared
51
+ source does not fit.
52
+
53
+
54
+ REFERENCES:
55
+ -----------
56
+ 1. MIT xPRO Course: AI and Deep Learning: Neural Network Applications
57
+
58
+ 2. PyTorch -- torch.nn.Module.register_forward_hook
59
+ https://pytorch.org/docs/stable/generated/torch.nn.Module.html
60
+ """
61
+
62
+ # endregion
63
+
64
+ # region I M P O R T S
65
+
66
+ import numpy as np
67
+ import torch
68
+
69
+ from .layer import (
70
+ LAYER_KINDS,
71
+ LAYER_MODULES,
72
+ LAYER_PASS_MODULES,
73
+ NEURON_MODULES,
74
+ RESHAPE_MODULES,
75
+ Layer,
76
+ _scored,
77
+ )
78
+
79
+ # endregion
80
+
81
+ # region C O N S T A N T S
82
+
83
+ # how every network of one kind is found and read, keyed by the 'network_kind' its Model declares:
84
+ # networks(model): the Model's Networks, in the order its 'sources' lists them
85
+ # parameters(layer): one of its Layers' (weight, bias), NumPy arrays read live
86
+ # z(layer, X): that Layer's z for every sample of X, float64, (N, neurons, ...)
87
+ # inputs(layer, X): what that Layer's weight module reads for every sample of X, float64, (N, *its in_shape)
88
+ # outputs(network, X, name): what the network's module computes for X, float64, (N, *out_shape);
89
+ # 'name' is the caller's name for X, for the messages
90
+ # count(network): its count of learned values
91
+ # device(model): the device the Model trains on, whose memory a run records; None for NumPy
92
+ NETWORK_KINDS = {
93
+ # any PyTorch module, its layers found by a hooked pass of the Model's 'forward()'
94
+ 'torch': {
95
+ 'networks': lambda model: _probed_networks(model),
96
+ 'parameters': lambda layer: _module_parameters(layer),
97
+ 'z': lambda layer, X: _hooked(layer, X, lambda inputs, output: output),
98
+ 'inputs': lambda layer, X: _hooked(layer, X, lambda inputs, output: inputs[0]),
99
+ 'outputs': lambda network, X, name: _module_outputs(network, X, name),
100
+ 'count': lambda network: sum(p.numel() for p in network.module.parameters()),
101
+ 'device': lambda model: next(model.parameters()).device,
102
+ },
103
+ # our single NumPy neuron: one network of one Layer, 'w' as its one row of weights
104
+ 'numpy': {
105
+ 'networks': lambda model: _neuron_networks(model),
106
+ 'parameters': lambda layer: (
107
+ layer.model.w.reshape(1, -1),
108
+ np.array([layer.model.w_0]),
109
+ ),
110
+ 'z': lambda layer, X: (X @ layer.model.w + layer.model.w_0).reshape(-1, 1),
111
+ 'inputs': lambda layer, X: np.asarray(X, dtype=np.float64),
112
+ 'outputs': lambda network, X, name: network.model.outputs(X).reshape(-1, 1),
113
+ 'count': lambda network: int(network.model.w.size + 1),
114
+ 'device': lambda model: None,
115
+ },
116
+ }
117
+
118
+ # endregion
119
+
120
+ # region C L A S S E S
121
+
122
+
123
+ class Network:
124
+ """
125
+ One network of a Model: a view of the module a Model attribute holds, or a single neuron's
126
+ 'w', with its Layers in the order they run. It holds no weights of its own, so it always
127
+ reads the trained values.
128
+
129
+ Args:
130
+ model: the Model it belongs to.
131
+ index: its position in 'model.network'.
132
+ name: the Model attribute that holds it, e.g. "net", "encoder", or "decoder"; "neuron"
133
+ for a single neuron, which has no module.
134
+ kind: how its layers are found and read, a key of 'NETWORK_KINDS': "torch" or "numpy".
135
+ source: what it reads, a tuple of "data" or an earlier network's name, e.g. ("encoder",).
136
+ module: the nn.Module it views; None for a single neuron.
137
+ in_shape: one sample's shape entering it, e.g. (784,) or (1, 28, 28).
138
+ out_shape: one sample's shape leaving it, e.g. (32,).
139
+ layer_rows: each of its layers' 'kind', 'activation', 'module', 'in_shape', and
140
+ 'out_shape', in the order they run, as 'Layer' takes them.
141
+
142
+ Note:
143
+ Models build their own networks; read them from 'model.network'. The Model's
144
+ 'forward()' is the one record of how its networks combine; 'source' describes it, and
145
+ building the Model checks each network's 'in_shape' against its source's shape.
146
+
147
+ Example:
148
+ >>> decoder = model.network[1]
149
+ >>> decoder.name, decoder.source
150
+ ('decoder', ('encoder',))
151
+ >>> decoder.layer[0].neuron[3].bias
152
+ """
153
+
154
+ def __init__(
155
+ self,
156
+ model,
157
+ index,
158
+ name,
159
+ kind,
160
+ source,
161
+ module,
162
+ in_shape,
163
+ out_shape,
164
+ layer_rows,
165
+ ):
166
+ self.model = model
167
+ self.index = index
168
+ self.name = name
169
+ self.kind = kind
170
+ self.source = source
171
+ self.module = module
172
+ self.in_shape = in_shape
173
+ self.out_shape = out_shape
174
+ self.layer = tuple(Layer(self, i, **row) for i, row in enumerate(layer_rows))
175
+
176
+ @property
177
+ def parameters(self):
178
+ """Its count of learned values, every value of its module's parameters."""
179
+ return NETWORK_KINDS[self.kind]['count'](self)
180
+
181
+ @property
182
+ def label(self):
183
+ """Its position, as the code reaches it, e.g. "network[1]"."""
184
+ return f'network[{self.index}]'
185
+
186
+ def outputs(self, X):
187
+ """
188
+ Returns what it computes for every sample, scored in eval() without gradients, its Model's mode left as it was.
189
+
190
+ Args:
191
+ X: its input samples, one per row: a tensor or NumPy array of shape (N, ...), each
192
+ shaped to 'in_shape', e.g. the data for an encoder, latent codes for a decoder.
193
+
194
+ Returns:
195
+ a float64 NumPy array of shape (N, *out_shape): a network's logits, an encoder's
196
+ latent codes, or a single neuron's a.
197
+ """
198
+ return self._outputs(X, 'X')
199
+
200
+ def _outputs(self, X, name):
201
+ """Returns 'outputs(X)', its messages naming X by the caller's 'name', e.g. "Z"."""
202
+ return NETWORK_KINDS[self.kind]['outputs'](self, X, name)
203
+
204
+ def _layer_parameters(self, layer):
205
+ """Returns one of its Layers' (weight, bias), as NumPy arrays."""
206
+ return NETWORK_KINDS[self.kind]['parameters'](layer)
207
+
208
+ def _layer_z(self, layer, X):
209
+ """Returns one of its Layers' z for every sample of X, as a NumPy array."""
210
+ return NETWORK_KINDS[self.kind]['z'](layer, X)
211
+
212
+ def _layer_inputs(self, layer, X):
213
+ """Returns what one of its Layers' weight module reads for every sample of X, as a NumPy array."""
214
+ return NETWORK_KINDS[self.kind]['inputs'](layer, X)
215
+
216
+
217
+ # endregion
218
+
219
+ # region P R I V A T E F U N C T I O N S
220
+
221
+
222
+ def _recorder(log):
223
+ """Returns a forward hook that appends (module, shape in, shape out) of one sample to 'log'."""
224
+
225
+ def record(module, inputs, output):
226
+ log.append((module, tuple(inputs[0].shape[1:]), tuple(output.shape[1:])))
227
+
228
+ return record
229
+
230
+
231
+ def _probed_networks(model):
232
+ """
233
+ Returns a PyTorch Model's Networks, found by one zero sample run through its 'forward()'.
234
+
235
+ Args:
236
+ model: a PyTorch Model, its networks built and named in 'model.sources'. Hooks on each
237
+ network and each innermost module record what runs, in order; the sample runs in
238
+ eval() without gradients, its mode restored, so no Dropout draws a random number,
239
+ and every hook is removed after.
240
+ """
241
+ parameter = next(model.parameters())
242
+ x = torch.zeros(1, *model.in_shape, dtype=parameter.dtype, device=parameter.device)
243
+ runs = {name: [] for name in model.sources}
244
+ records = {name: [] for name in model.sources}
245
+ handles = []
246
+ for name in model.sources:
247
+ network = getattr(model, name)
248
+ handles.append(network.register_forward_hook(_recorder(runs[name])))
249
+ for module in network.modules():
250
+ if next(module.children(), None) is None:
251
+ handles.append(module.register_forward_hook(_recorder(records[name])))
252
+ is_training = model.training
253
+ model.eval()
254
+ try:
255
+ with torch.no_grad():
256
+ model(x)
257
+ finally:
258
+ model.train(is_training)
259
+ for handle in handles:
260
+ handle.remove()
261
+ for name in model.sources:
262
+ _check_runs(model, name, runs[name], records[name])
263
+ shapes = {name: runs[name][0][1:] for name in model.sources}
264
+ _check_sources(model, shapes)
265
+ last = list(model.sources)[-1]
266
+ return tuple(
267
+ Network(
268
+ model,
269
+ index,
270
+ name,
271
+ 'torch',
272
+ source,
273
+ getattr(model, name),
274
+ *shapes[name],
275
+ _layer_rows(model, name, records[name], is_last=name == last),
276
+ )
277
+ for index, (name, source) in enumerate(model.sources.items())
278
+ )
279
+
280
+
281
+ def _check_runs(model, name, runs, records):
282
+ """
283
+ Raises a ValueError naming the network unless it ran exactly once, each of its modules once.
284
+
285
+ Args:
286
+ model: the Model probed.
287
+ name: the network's name, a key of 'model.sources'.
288
+ runs: the network's own records, one per time it ran.
289
+ records: its innermost modules' records, in the order they ran.
290
+ """
291
+ if not runs:
292
+ raise ValueError(
293
+ f"'model.{name}' is in '{type(model).__name__}.sources' but never runs in 'forward()'"
294
+ )
295
+ modules = [module for module, _, _ in records]
296
+ if len(runs) > 1 or len(set(map(id, modules))) < len(modules):
297
+ raise ValueError(
298
+ f"'model.{name}' runs more than once in one pass; a network or module run twice is not supported yet"
299
+ )
300
+
301
+
302
+ def _check_sources(model, shapes):
303
+ """
304
+ Raises a TypeError or ValueError naming the network unless each declared source fits it.
305
+
306
+ Args:
307
+ model: the Model probed; 'model.sources' maps each network to a tuple of one source,
308
+ "data" or an earlier network's name.
309
+ shapes: each network's (shape in, shape out) of one sample, by name.
310
+ """
311
+ owner = f"'{type(model).__name__}.sources'"
312
+ names = list(model.sources)
313
+ for i, (name, source) in enumerate(model.sources.items()):
314
+ if not isinstance(source, tuple):
315
+ raise TypeError(
316
+ f"{owner}['{name}'] must be a tuple of sources, e.g. ('data',), got {source!r}"
317
+ )
318
+ if len(source) != 1:
319
+ raise ValueError(
320
+ f"{owner} says '{name}' reads {source}; a network of several sources is not supported yet"
321
+ )
322
+ (read,) = source
323
+ if read == 'data':
324
+ expected = tuple(model.in_shape)
325
+ elif read in names[:i]:
326
+ expected = shapes[read][1]
327
+ else:
328
+ raise ValueError(
329
+ f"{owner} says '{name}' reads '{read}', neither 'data' nor a network before it, {names[:i]}"
330
+ )
331
+ if shapes[name][0] != expected:
332
+ raise ValueError(
333
+ f"{owner} says '{name}' reads '{read}', one sample {expected}, but it reads {shapes[name][0]}"
334
+ )
335
+
336
+
337
+ def _layer_rows(model, name, records, is_last):
338
+ """
339
+ Returns one network's layers, as 'Layer' takes them: each weight module with the modules after it.
340
+
341
+ Args:
342
+ model: the Model probed.
343
+ name: the network's name, for the messages.
344
+ records: its innermost modules' (module, shape in, shape out), in the order they ran.
345
+ is_last: True for the Model's last network, whose last layer takes the Model's output
346
+ activation when no module applies it, as the loss does.
347
+ """
348
+ first = type(records[0][0])
349
+ if first not in LAYER_MODULES:
350
+ raise ValueError(
351
+ f"'model.{name}' must start with a weight module, one of"
352
+ f' {[module.__name__ for module in LAYER_MODULES]}, got {first.__name__}'
353
+ )
354
+ rows = []
355
+ for module, in_shape, out_shape in records:
356
+ kind = type(module)
357
+ # a weight module starts a layer; every other module belongs to the layer before it
358
+ if kind in LAYER_MODULES:
359
+ rows.append(
360
+ {
361
+ 'kind': LAYER_MODULES[kind],
362
+ 'activation': None,
363
+ 'module': [],
364
+ 'in_shape': in_shape,
365
+ }
366
+ )
367
+ elif kind in NEURON_MODULES:
368
+ if rows[-1]['activation'] is None:
369
+ rows[-1]['activation'] = NEURON_MODULES[kind]
370
+ elif not isinstance(module, LAYER_PASS_MODULES + RESHAPE_MODULES):
371
+ raise ValueError(
372
+ f"'model.{name}' holds a {kind.__name__}: not a weight module, a 'NEURON_KINDS' activation,"
373
+ ' a pool, a dropout, or a reshape'
374
+ )
375
+ rows[-1]['module'].append(module)
376
+ if not isinstance(module, RESHAPE_MODULES):
377
+ rows[-1]['out_shape'] = out_shape
378
+ # the loss applies a network's output activation, so its last layer has no module for it
379
+ if is_last and rows[-1]['activation'] is None:
380
+ rows[-1]['activation'] = model.output_activation_name
381
+ for row in rows:
382
+ row['module'] = tuple(row['module'])
383
+ if row['activation'] is None:
384
+ row['activation'] = 'linear'
385
+ return rows
386
+
387
+
388
+ def _module_parameters(layer):
389
+ """Returns a PyTorch layer's (weight, bias), its weight module's, as NumPy arrays on the CPU."""
390
+ weight, bias = LAYER_KINDS[layer.kind]['parameters'](layer.module[0])
391
+ return weight.detach().cpu().numpy(), bias.detach().cpu().numpy()
392
+
393
+
394
+ def _hooked(layer, X, pick):
395
+ """
396
+ Returns what a PyTorch layer's weight module reads or computes for every sample, as the
397
+ Model's 'forward()' runs it, scored by '_scored()', as a float64 NumPy array.
398
+
399
+ Args:
400
+ layer: a Layer of a PyTorch Model; the build proved its weight module runs once per pass.
401
+ X: the input samples, as the Model's 'outputs()' takes them.
402
+ pick: 'pick(inputs, output)' returns the tensor to keep from one hooked call, e.g. its
403
+ output, the layer's z, or 'inputs[0]', what it reads.
404
+ """
405
+ model = layer.model
406
+ captured = []
407
+ handle = layer.module[0].register_forward_hook(
408
+ lambda module, inputs, output: captured.append(pick(inputs, output).cpu())
409
+ )
410
+ try:
411
+ _scored(model, model, X, 'X', model.in_shape)
412
+ finally:
413
+ handle.remove()
414
+ return torch.cat(captured).double().numpy()
415
+
416
+
417
+ def _module_outputs(network, X, name):
418
+ """Returns a PyTorch network's module run on X, scored by '_scored()', as a float64 NumPy array."""
419
+ return (
420
+ _scored(network.model, network.module, X, name, network.in_shape)
421
+ .cpu()
422
+ .double()
423
+ .numpy()
424
+ )
425
+
426
+
427
+ def _neuron_networks(model):
428
+ """Returns a single neuron Model's one Network, of one Linear Layer of one Neuron."""
429
+ ((name, source),) = model.sources.items()
430
+ shape = (len(model.w),)
431
+ row = {
432
+ 'kind': 'linear',
433
+ 'activation': model.activation_name,
434
+ 'module': (),
435
+ 'in_shape': shape,
436
+ 'out_shape': (1,),
437
+ }
438
+ return (Network(model, 0, name, 'numpy', source, None, shape, (1,), (row,)),)
439
+
440
+
441
+ # endregion
442
+
443
+ # region P U B L I C F U N C T I O N S
444
+
445
+ # endregion
446
+ # endregion
447
+ # endregion
mcode_neural/neuron.py ADDED
@@ -0,0 +1,233 @@
1
+ # region F I L E
2
+ # <copyright file="mcode-neural/src/mcode_neural/neuron.py" company="MicroCODE Incorporated">Copyright © 2026 MicroCODE, Inc. Troy, MI</copyright><author>Timothy J. McGuire</author>
3
+ # region M O D U L E
4
+ # region D O C U M E N T A T I O N
5
+ """
6
+ Project: MicroCODE Python Packages
7
+ Customer: Internal + MIT xPRO Course
8
+ @module 'mcode_neural.neuron'
9
+ @memberof mcode_neural
10
+ @created October 2026
11
+ @author Timothy McGuire, MicroCODE, Inc.
12
+ @description >
13
+ MicroCODE Shared Neuron Library
14
+
15
+ LICENSE:
16
+ --------
17
+ MIT License: MicroCODE.mcode-neural
18
+
19
+ Copyright (c) 2026 Timothy McGuire, MicroCODE, Inc.
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
38
+
39
+
40
+ DESCRIPTION:
41
+ ------------
42
+ This module implements what every MicroCODE neuron is: 'NEURON_KINDS', each
43
+ activation's label, PyTorch module, range, NumPy activation, and slope; and
44
+ 'Neuron', one output of a model's 'Layer', a view of its trained weights;
45
+ and 'NeuronOptimizer', the gradient descent a single neuron trains with.
46
+ Our single neuron models, 'NeuronModel' and its subclasses, are in 'model.py'
47
+ with every Model.
48
+
49
+
50
+ REFERENCES:
51
+ -----------
52
+ 1. MIT xPRO Course: AI and Deep Learning: Neural Network Applications
53
+
54
+ 2. NumPy -- numpy.ndarray
55
+ https://numpy.org/doc/stable/reference/arrays.ndarray.html
56
+ """
57
+
58
+ # endregion
59
+
60
+ # region I M P O R T S
61
+
62
+ import math
63
+
64
+ import numbers
65
+
66
+ import numpy as np
67
+
68
+ import torch
69
+
70
+ from torch import nn
71
+
72
+ # endregion
73
+
74
+ # region C O N S T A N T S
75
+
76
+ # added to a sigmoid's denominator, so it never divides by 0, as the course's neurons do
77
+ NON_ZERO = 1e-8
78
+ # what every neuron of one kind shares, keyed by its activation's name:
79
+ # label: its name as the views print it; module: the PyTorch module that applies it
80
+ # range: the (low, high) its outputs can reach
81
+ # activation(z): its NumPy activation of a layer's z, (N, neurons, ...); softmax spans the neurons
82
+ # slope(a): da/dz at its activation a, the term every weight's gradient is scaled by
83
+ # has_curve: whether one neuron's a is a curve of its own z alone, so a view can draw it
84
+ NEURON_KINDS = {
85
+ 'linear': {
86
+ 'label': 'Linear',
87
+ 'module': nn.Identity,
88
+ 'range': (-math.inf, math.inf),
89
+ 'activation': lambda z: z,
90
+ 'slope': lambda a: 1.0,
91
+ 'has_curve': True,
92
+ },
93
+ 'sigmoid': {
94
+ 'label': 'Sigmoid',
95
+ 'module': nn.Sigmoid,
96
+ 'range': (0.0, 1.0),
97
+ 'activation': lambda z: 1 / (1 + np.exp(-z) + NON_ZERO),
98
+ 'slope': lambda a: a * (1 - a),
99
+ 'has_curve': True,
100
+ },
101
+ 'tanh': {
102
+ 'label': 'Tanh',
103
+ 'module': nn.Tanh,
104
+ 'range': (-1.0, 1.0),
105
+ 'activation': lambda z: np.tanh(z),
106
+ 'slope': lambda a: 1 - a**2,
107
+ 'has_curve': True,
108
+ },
109
+ 'relu': {
110
+ 'label': 'ReLU',
111
+ 'module': nn.ReLU,
112
+ 'range': (0.0, math.inf),
113
+ 'activation': lambda z: np.maximum(z, 0.0),
114
+ 'slope': lambda a: (a > 0) * 1.0,
115
+ 'has_curve': True,
116
+ },
117
+ 'softmax': {
118
+ 'label': 'Softmax',
119
+ 'module': nn.Softmax,
120
+ 'range': (0.0, 1.0),
121
+ 'activation': lambda z: torch.softmax(torch.as_tensor(z), dim=1).numpy(),
122
+ 'slope': lambda a: a * (1 - a),
123
+ 'has_curve': False,
124
+ },
125
+ }
126
+
127
+ # endregion
128
+
129
+ # region C L A S S E S
130
+
131
+
132
+ class Neuron:
133
+ """
134
+ One neuron of a model's 'Layer': a view of one of its outputs, holding no weights of its own,
135
+ so it always reads the trained values.
136
+
137
+ Args:
138
+ layer: the 'Layer' it belongs to.
139
+ index: its position in 'layer.neuron', its output's row of the layer's weights.
140
+
141
+ Note:
142
+ Layers build their own neurons; read them from 'model.layer[k].neuron[j]', or
143
+ 'model.network[i].layer[k].neuron[j]' on a Model of several networks.
144
+
145
+ Example:
146
+ >>> neuron = model.layer[0].neuron[5]
147
+ >>> z, a = neuron.outputs(X)
148
+ """
149
+
150
+ def __init__(self, layer, index):
151
+ self.layer = layer
152
+ self.index = index
153
+
154
+ @property
155
+ def kind(self):
156
+ """Its activation, its layer's, a key of 'NEURON_KINDS', e.g. "relu"."""
157
+ return self.layer.activation
158
+
159
+ @property
160
+ def weight(self):
161
+ """Its weights: a Linear layer's row, a conv filter (in_channels, h, w), or a single neuron's 'w'."""
162
+ return self.layer.neuron_weights(self.index)
163
+
164
+ @property
165
+ def bias(self):
166
+ """Its bias, a float."""
167
+ return float(self.layer.bias[self.index])
168
+
169
+ @property
170
+ def label(self):
171
+ """Its position, as the code reaches it, e.g. "layer[2].neuron[5]" or "network[1].layer[0].neuron[3]"."""
172
+ return f'{self.layer.label}.neuron[{self.index}]'
173
+
174
+ def outputs(self, X):
175
+ """
176
+ Returns its (z, a) for every sample, its column of 'layer.outputs(X)'.
177
+
178
+ Args:
179
+ X: the input samples, as the model's 'outputs()' or 'classify()' takes them.
180
+
181
+ Returns:
182
+ (z, a) float64 NumPy arrays: (N,) for a Linear neuron, (N, h, w) for a conv neuron's map.
183
+ """
184
+ z, a = self.layer.outputs(X)
185
+ return z[:, self.index], a[:, self.index]
186
+
187
+
188
+ class NeuronOptimizer:
189
+ """
190
+ Plain gradient descent for a single neuron model: the optimizer 'train_model()' takes for a NeuronModel.
191
+
192
+ Args:
193
+ lr: the learning rate, the step size of each update, a number above 0.
194
+
195
+ Note:
196
+ It holds the two attributes a PyTorch optimizer documents, 'defaults' and
197
+ 'param_groups', so a run's "train" event records it as any optimizer:
198
+ {"name": "NeuronOptimizer", "lr": 0.01, "defaults": {}}. Each step moves the neuron
199
+ by the mean of its batch's per-sample updates, so a 'batch_size' of 1 is the course's
200
+ per-sample loop.
201
+
202
+ Example:
203
+ >>> neuron = train_model(neuron, NeuronOptimizer(lr=0.01), None, num_epochs=10, batch_size=1)
204
+ """
205
+
206
+ def __init__(self, lr):
207
+ if isinstance(lr, bool) or not isinstance(lr, numbers.Real) or not lr > 0:
208
+ raise ValueError(f"'lr' must be a number above 0, got {lr!r}")
209
+ self.defaults = {'lr': lr}
210
+ self.param_groups = [{'lr': lr}]
211
+
212
+ def step(self, model, grad_loss):
213
+ """
214
+ Moves the neuron's weights one step down its loss: 'model.update(grad_loss, lr)'.
215
+
216
+ Args:
217
+ model: the NeuronModel, its 'gradient()' cached for this batch.
218
+ grad_loss: dL/da of each sample of the batch, 'model.loss_gradient(a, y)'.
219
+ """
220
+ model.update(grad_loss, self.param_groups[0]['lr'])
221
+
222
+
223
+ # endregion
224
+
225
+ # region P R I V A T E F U N C T I O N S
226
+
227
+ # endregion
228
+
229
+ # region P U B L I C F U N C T I O N S
230
+
231
+ # endregion
232
+ # endregion
233
+ # endregion