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.
- mcode_neural/__init__.py +183 -0
- mcode_neural/data.py +773 -0
- mcode_neural/fonts/MicroCODE-Mono/MicroCODE-Mono.ttf +0 -0
- mcode_neural/fonts/MicroCODE-Mono/OFL.txt +128 -0
- mcode_neural/fonts/RobotoMono/OFL.txt +93 -0
- mcode_neural/fonts/RobotoMono/RobotoMono-Bold.ttf +0 -0
- mcode_neural/fonts/RobotoMono/RobotoMono-Bold.woff2 +0 -0
- mcode_neural/fonts/RobotoMono/RobotoMono-Regular.ttf +0 -0
- mcode_neural/fonts/RobotoMono/RobotoMono-Regular.woff2 +0 -0
- mcode_neural/layer.py +589 -0
- mcode_neural/model.py +2092 -0
- mcode_neural/network.py +447 -0
- mcode_neural/neuron.py +233 -0
- mcode_neural/predict.py +360 -0
- mcode_neural/show.py +6076 -0
- mcode_neural/train.py +756 -0
- mcode_neural-0.5.0.dist-info/METADATA +767 -0
- mcode_neural-0.5.0.dist-info/RECORD +22 -0
- mcode_neural-0.5.0.dist-info/WHEEL +4 -0
- mcode_neural-0.5.0.dist-info/licenses/LICENSE +21 -0
- mcode_neural-0.5.0.dist-info/licenses/src/mcode_neural/fonts/MicroCODE-Mono/OFL.txt +128 -0
- mcode_neural-0.5.0.dist-info/licenses/src/mcode_neural/fonts/RobotoMono/OFL.txt +93 -0
mcode_neural/network.py
ADDED
|
@@ -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
|