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/layer.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# region F I L E
|
|
2
|
+
# <copyright file="mcode-neural/src/mcode_neural/layer.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.layer'
|
|
9
|
+
@memberof mcode_neural
|
|
10
|
+
@created October 2026
|
|
11
|
+
@author Timothy McGuire, MicroCODE, Inc.
|
|
12
|
+
@description >
|
|
13
|
+
MicroCODE Shared Layer 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 layer is: 'LAYER_KINDS', each
|
|
43
|
+
weight module's kind, its fields, how it indexes its neurons, and how its
|
|
44
|
+
weights are read; and 'Layer', one weight module of a Network with the modules
|
|
45
|
+
after it, a view of its trained weights holding its 'Neuron's. It also builds
|
|
46
|
+
and checks the networks' stacks: hidden 'Linear' layers, each followed by a
|
|
47
|
+
ReLU (and a Dropout when asked), then a 'Linear' output layer, after any
|
|
48
|
+
convolution stages, 'Conv2d' / 'ReLU' / 'MaxPool2d', and an autoencoder's
|
|
49
|
+
mirrored decoder. The Networks that hold the layers are in 'network.py'; the
|
|
50
|
+
Models, 'FeedforwardModel' and its subclasses and 'AutoencoderModel', are in
|
|
51
|
+
'model.py' with every Model.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
REFERENCES:
|
|
55
|
+
-----------
|
|
56
|
+
1. MIT xPRO Course: AI and Deep Learning: Neural Network Applications
|
|
57
|
+
|
|
58
|
+
2. PyTorch -- torch.nn (Linear, ReLU, Dropout, Sequential, Identity, Sigmoid, Tanh,
|
|
59
|
+
Conv2d, MaxPool2d, Flatten, Unflatten, ConvTranspose2d)
|
|
60
|
+
https://pytorch.org/docs/stable/nn.html
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# endregion
|
|
64
|
+
|
|
65
|
+
# region I M P O R T S
|
|
66
|
+
|
|
67
|
+
import math
|
|
68
|
+
import numbers
|
|
69
|
+
from collections.abc import Sequence
|
|
70
|
+
from itertools import pairwise
|
|
71
|
+
|
|
72
|
+
import numpy as np
|
|
73
|
+
import torch
|
|
74
|
+
|
|
75
|
+
from torch import nn
|
|
76
|
+
|
|
77
|
+
from .data import DATA_KINDS
|
|
78
|
+
from .neuron import NEURON_KINDS, Neuron
|
|
79
|
+
|
|
80
|
+
# endregion
|
|
81
|
+
|
|
82
|
+
# region C O N S T A N T S
|
|
83
|
+
|
|
84
|
+
# the keys of each 'convolution' entry, in the order the "create" event records them:
|
|
85
|
+
# channels: the number of feature maps the stage's Conv2d outputs
|
|
86
|
+
# kernel: the Conv2d's square kernel width, odd, so padding keeps the image's height and width
|
|
87
|
+
# pool: the MaxPool2d's square window, dividing height and width by it; 1 adds no pooling
|
|
88
|
+
CONV_STAGE_KEYS = ('channels', 'kernel', 'pool')
|
|
89
|
+
|
|
90
|
+
# what every layer of one kind shares, keyed by the kind of its weight module:
|
|
91
|
+
# module: the PyTorch module that holds its weights
|
|
92
|
+
# fields: the module's settings, in the order the internals table labels them
|
|
93
|
+
# neuron_axis: the axis of the module's 'weight' that indexes its neurons
|
|
94
|
+
# parameters(module): the module's (weight, bias) tensors
|
|
95
|
+
LAYER_KINDS = {
|
|
96
|
+
'linear': {
|
|
97
|
+
'module': nn.Linear,
|
|
98
|
+
'fields': ('in_features', 'out_features'),
|
|
99
|
+
'neuron_axis': 0,
|
|
100
|
+
'parameters': lambda module: (module.weight, module.bias),
|
|
101
|
+
},
|
|
102
|
+
'conv': {
|
|
103
|
+
'module': nn.Conv2d,
|
|
104
|
+
'fields': ('in_channels', 'out_channels', 'kernel_size', 'stride', 'padding'),
|
|
105
|
+
'neuron_axis': 0,
|
|
106
|
+
'parameters': lambda module: (module.weight, module.bias),
|
|
107
|
+
},
|
|
108
|
+
'deconv': {
|
|
109
|
+
'module': nn.ConvTranspose2d,
|
|
110
|
+
'fields': ('in_channels', 'out_channels', 'kernel_size', 'stride'),
|
|
111
|
+
# a ConvTranspose2d's weight is (in_channels, out_channels, ...), its outputs second
|
|
112
|
+
'neuron_axis': 1,
|
|
113
|
+
'parameters': lambda module: (module.weight, module.bias),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# a name for each of one neuron's inputs, keyed by the length of its 'input_layout()':
|
|
118
|
+
# 1: a row of inputs, "in[k]"
|
|
119
|
+
# 3: channels of rows and columns, a conv filter or an image, "in[c] (r, s)"
|
|
120
|
+
INPUT_NAMES = {
|
|
121
|
+
1: lambda layout: [f'in[{k}]' for k in range(layout[0])],
|
|
122
|
+
3: lambda layout: [f'in[{c}] ({r}, {s})' for c, r, s in np.ndindex(*layout)],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# each weight module's layer kind, and each activation module's neuron kind, by the module's type
|
|
126
|
+
LAYER_MODULES = {row['module']: kind for kind, row in LAYER_KINDS.items()}
|
|
127
|
+
NEURON_MODULES = {row['module']: kind for kind, row in NEURON_KINDS.items()}
|
|
128
|
+
# the modules that reshape a sample without changing a value, so a layer's shape out is read before them
|
|
129
|
+
RESHAPE_MODULES = (nn.Flatten, nn.Unflatten)
|
|
130
|
+
# the modules a layer holds after its weight module that neither weigh nor activate
|
|
131
|
+
LAYER_PASS_MODULES = (nn.Dropout, nn.MaxPool2d, nn.AdaptiveAvgPool2d)
|
|
132
|
+
|
|
133
|
+
# endregion
|
|
134
|
+
|
|
135
|
+
# region C L A S S E S
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Layer:
|
|
139
|
+
"""
|
|
140
|
+
One layer of a Network: a view of one weight module, or a single neuron's 'w', with the modules
|
|
141
|
+
that follow it up to the next weight module. It holds no weights of its own, so it always
|
|
142
|
+
reads the trained values.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
network: the Network it belongs to.
|
|
146
|
+
index: its position in 'network.layer', in the order its weight modules run.
|
|
147
|
+
kind: its weight module's kind, a key of 'LAYER_KINDS': "linear", "conv", or "deconv".
|
|
148
|
+
activation: its neurons' kind, a key of 'NEURON_KINDS', e.g. "relu".
|
|
149
|
+
module: the nn modules in it, its weight module first, e.g. (Linear, ReLU, Dropout); ()
|
|
150
|
+
for a single neuron.
|
|
151
|
+
in_shape: one sample's shape entering its weight module, e.g. (784,) or (1, 28, 28).
|
|
152
|
+
out_shape: one sample's shape leaving it, read before a Flatten or Unflatten, e.g. (64,)
|
|
153
|
+
or (8, 14, 14).
|
|
154
|
+
|
|
155
|
+
Note:
|
|
156
|
+
Networks build their own layers; read them from 'model.network[k].layer', or from
|
|
157
|
+
'model.layer' on a Model of one network. A MaxPool2d, Dropout, Flatten, or Unflatten
|
|
158
|
+
belongs to the layer before it; its 'outputs()' are read before them.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> layer = model.layer[1]
|
|
162
|
+
>>> z, a = layer.outputs(X)
|
|
163
|
+
>>> layer.neuron[5].bias
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
network,
|
|
169
|
+
index,
|
|
170
|
+
kind,
|
|
171
|
+
activation,
|
|
172
|
+
module,
|
|
173
|
+
in_shape,
|
|
174
|
+
out_shape,
|
|
175
|
+
):
|
|
176
|
+
self.network = network
|
|
177
|
+
self.index = index
|
|
178
|
+
self.kind = kind
|
|
179
|
+
self.activation = activation
|
|
180
|
+
self.module = module
|
|
181
|
+
self.in_shape = in_shape
|
|
182
|
+
self.out_shape = out_shape
|
|
183
|
+
# one neuron per output, as its weight module indexes them
|
|
184
|
+
count = self.weight.shape[LAYER_KINDS[kind]['neuron_axis']]
|
|
185
|
+
self.neuron = tuple(Neuron(self, j) for j in range(count))
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def model(self):
|
|
189
|
+
"""The Model its Network belongs to."""
|
|
190
|
+
return self.network.model
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def weight(self):
|
|
194
|
+
"""Its weights, a NumPy array read from its Network on each access, e.g. (out_features, in_features)."""
|
|
195
|
+
return self.network._layer_parameters(self)[0]
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def bias(self):
|
|
199
|
+
"""Its biases, a NumPy array of one per neuron, read from its Network on each access."""
|
|
200
|
+
return self.network._layer_parameters(self)[1]
|
|
201
|
+
|
|
202
|
+
def neuron_weights(self, j):
|
|
203
|
+
"""Returns neuron j's weights, taken on its kind's 'neuron_axis', e.g. one row of a Linear layer."""
|
|
204
|
+
return np.take(self.weight, j, axis=LAYER_KINDS[self.kind]['neuron_axis'])
|
|
205
|
+
|
|
206
|
+
def input_layout(self):
|
|
207
|
+
"""
|
|
208
|
+
Returns the shape one neuron's inputs, and so its weights, are laid out in: a conv filter's
|
|
209
|
+
(in_channels, kh, kw); the first Linear layer of a network that reads the data, when one
|
|
210
|
+
sample of the data is an image, the image's (c, h, w); any other Linear layer's (in_features,).
|
|
211
|
+
"""
|
|
212
|
+
if self._reads_data() and not DATA_KINDS[self.model.data.kind]['has_features']:
|
|
213
|
+
sample = next(iter(self.model.data.datasets.values()))[0][0].shape
|
|
214
|
+
if len(sample) == 3:
|
|
215
|
+
return tuple(sample)
|
|
216
|
+
return tuple(self.neuron[0].weight.shape)
|
|
217
|
+
|
|
218
|
+
def input_names(self):
|
|
219
|
+
"""
|
|
220
|
+
Returns a name for each of one neuron's weights, in its order: the data's feature names for
|
|
221
|
+
the first Linear layer of a network that reads data that names them, else by its
|
|
222
|
+
'input_layout()': "in[k]" for a row, or "in[c] (r, s)" for a conv filter or an image.
|
|
223
|
+
"""
|
|
224
|
+
if self._reads_data() and DATA_KINDS[self.model.data.kind]['has_features']:
|
|
225
|
+
return list(self.model.data.feature)
|
|
226
|
+
layout = self.input_layout()
|
|
227
|
+
return INPUT_NAMES[len(layout)](layout)
|
|
228
|
+
|
|
229
|
+
def inputs(self, X):
|
|
230
|
+
"""
|
|
231
|
+
Returns every sample's input to its weight module, the x its neurons weigh, scored as
|
|
232
|
+
'outputs()' scores, its train / eval mode left as it was.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
X: the input samples, as the model's 'outputs()' or 'classify()' takes them.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
a float64 NumPy array of shape (N, *in_shape), e.g. (N, 3) for a first Linear layer
|
|
239
|
+
of three features, or (N, 8, 14, 14) for a conv layer reading 8 maps.
|
|
240
|
+
"""
|
|
241
|
+
# how a layer's inputs are read is its Network's kind, in NETWORK_KINDS
|
|
242
|
+
return np.asarray(self.network._layer_inputs(self, X), dtype=np.float64)
|
|
243
|
+
|
|
244
|
+
def outputs(self, X):
|
|
245
|
+
"""
|
|
246
|
+
Returns every sample's z, its weight module's output, and a, its activation of z, scored as
|
|
247
|
+
the model scores, its train / eval mode left as it was.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
X: the input samples, as the model's 'outputs()' or 'classify()' takes them.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
(z, a) float64 NumPy arrays of shape (N, neurons, ...), e.g. (N, 64), or (N, 8, 28, 28)
|
|
254
|
+
for a conv layer, whose MaxPool2d follows its activation.
|
|
255
|
+
"""
|
|
256
|
+
z = np.asarray(self.network._layer_z(self, X), dtype=np.float64)
|
|
257
|
+
return z, NEURON_KINDS[self.activation]['activation'](z)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def parameters(self):
|
|
261
|
+
"""Its count of learned values, its weights and biases."""
|
|
262
|
+
return int(self.weight.size + self.bias.size)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def label(self):
|
|
266
|
+
"""Its position, as the code reaches it: "layer[2]" on a Model of one network, else "network[1].layer[0]"."""
|
|
267
|
+
if len(self.model.network) == 1:
|
|
268
|
+
return f'layer[{self.index}]'
|
|
269
|
+
return f'{self.network.label}.layer[{self.index}]'
|
|
270
|
+
|
|
271
|
+
def _reads_data(self):
|
|
272
|
+
"""Returns whether it is the first Linear layer of a network that reads the data."""
|
|
273
|
+
return (
|
|
274
|
+
self.network.source == ('data',)
|
|
275
|
+
and self.index == 0
|
|
276
|
+
and self.kind == 'linear'
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# endregion
|
|
281
|
+
|
|
282
|
+
# region P R I V A T E F U N C T I O N S
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _check_width(name, width):
|
|
286
|
+
"""
|
|
287
|
+
Raises a ValueError naming 'name' unless 'width' is a whole number of at least 1.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
name: the field being checked, as the caller wrote it, e.g. "hidden_size[1]".
|
|
291
|
+
width: the layer width to check; NumPy integers are whole numbers, bools are not.
|
|
292
|
+
"""
|
|
293
|
+
if isinstance(width, bool) or not isinstance(width, numbers.Integral) or width < 1:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"'{name}' must be a whole number of at least 1, got {width!r}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _check_sizes(name, sizes):
|
|
300
|
+
"""
|
|
301
|
+
Raises a TypeError naming 'name' unless 'sizes' is a sequence of widths, each checked by '_check_width()'.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
name: the argument being checked, e.g. "hidden_size".
|
|
305
|
+
sizes: the layer widths, in order, e.g. (64, 32); () is no layers.
|
|
306
|
+
"""
|
|
307
|
+
if not isinstance(sizes, Sequence):
|
|
308
|
+
raise TypeError(
|
|
309
|
+
f"'{name}' must be a sequence of widths, e.g. (64, 32), got {sizes!r}"
|
|
310
|
+
)
|
|
311
|
+
for i, width in enumerate(sizes):
|
|
312
|
+
_check_width(f'{name}[{i}]', width)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _check_dropout(dropout):
|
|
316
|
+
"""
|
|
317
|
+
Raises a ValueError unless 'dropout' is a number at least 0 and below 1.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
dropout: the probability of zeroing each hidden activation while training.
|
|
321
|
+
"""
|
|
322
|
+
if not isinstance(dropout, numbers.Real) or not 0 <= dropout < 1:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
f"'dropout' must be a number at least 0 and below 1, got {dropout!r}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _stack(sizes, dropout):
|
|
329
|
+
"""
|
|
330
|
+
Returns the layers of one stack: Linear, ReLU, and (when asked) Dropout between each pair of
|
|
331
|
+
widths, then a last Linear with no activation.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
sizes: every width in order, input first and output last, e.g. (784, 128, 64, 2).
|
|
335
|
+
dropout: the Dropout rate after each hidden ReLU; 0 adds no Dropout layers.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
a list of layers in the course's order, for 'nn.Sequential(*layers)'.
|
|
339
|
+
"""
|
|
340
|
+
layers = []
|
|
341
|
+
# each hidden layer reads the width of the layer before it
|
|
342
|
+
for size_in, size_out in pairwise(sizes[:-1]):
|
|
343
|
+
layers += [nn.Linear(size_in, size_out), nn.ReLU()]
|
|
344
|
+
if dropout:
|
|
345
|
+
layers.append(nn.Dropout(dropout))
|
|
346
|
+
return layers + [nn.Linear(sizes[-2], sizes[-1])]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_conv(in_features, in_shape, convolution, is_mirrored):
|
|
350
|
+
"""
|
|
351
|
+
Raises a TypeError or ValueError naming the field unless 'convolution' and 'in_shape' build a network that runs.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
in_features: the network's input width, already checked; 'in_shape' must hold as many values.
|
|
355
|
+
in_shape: one image's (channels, height, width); None with no conv stages.
|
|
356
|
+
convolution: each stage a dict of exactly 'CONV_STAGE_KEYS', each value a whole number of
|
|
357
|
+
at least 1, its kernel odd.
|
|
358
|
+
is_mirrored: True for an autoencoder, whose decoder must rebuild every pool exactly, so
|
|
359
|
+
each pool must divide the height and width it pools.
|
|
360
|
+
"""
|
|
361
|
+
if not isinstance(convolution, Sequence):
|
|
362
|
+
raise TypeError(
|
|
363
|
+
f"'convolution' must be a sequence of stages, e.g. ({{'channels': 8, 'kernel': 3, 'pool': 2}},), got {convolution!r}"
|
|
364
|
+
)
|
|
365
|
+
for i, stage in enumerate(convolution):
|
|
366
|
+
if not isinstance(stage, dict):
|
|
367
|
+
raise TypeError(
|
|
368
|
+
f"'convolution[{i}]' must be a dict of {list(CONV_STAGE_KEYS)}, got {stage!r}"
|
|
369
|
+
)
|
|
370
|
+
if set(stage) != set(CONV_STAGE_KEYS):
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"'convolution[{i}]' must have exactly the keys {list(CONV_STAGE_KEYS)}, got {list(stage)}"
|
|
373
|
+
)
|
|
374
|
+
for key in CONV_STAGE_KEYS:
|
|
375
|
+
_check_width(f"convolution[{i}]['{key}']", stage[key])
|
|
376
|
+
# an even kernel cannot be padded evenly, so the stage would change the image's size
|
|
377
|
+
if stage['kernel'] % 2 == 0:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
f"'convolution[{i}]['kernel']' must be odd, so the image keeps its height and width, got {stage['kernel']}"
|
|
380
|
+
)
|
|
381
|
+
if not convolution:
|
|
382
|
+
if in_shape is not None:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"'in_shape' is read only with 'convolution'; got in_shape={in_shape!r} and no stages"
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
if in_shape is None:
|
|
388
|
+
raise ValueError(
|
|
389
|
+
"'convolution' needs 'in_shape', one image's (channels, height, width), e.g. (1, 28, 28)"
|
|
390
|
+
)
|
|
391
|
+
if not isinstance(in_shape, Sequence):
|
|
392
|
+
raise TypeError(
|
|
393
|
+
f"'in_shape' must be a sequence, (channels, height, width), got {in_shape!r}"
|
|
394
|
+
)
|
|
395
|
+
if len(in_shape) != 3:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"'in_shape' must be (channels, height, width), got {tuple(in_shape)}"
|
|
398
|
+
)
|
|
399
|
+
for i, size in enumerate(in_shape):
|
|
400
|
+
_check_width(f'in_shape[{i}]', size)
|
|
401
|
+
if math.prod(in_shape) != in_features:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"'in_shape' {tuple(in_shape)} holds {math.prod(in_shape)} values, not 'in_features' {in_features}"
|
|
404
|
+
)
|
|
405
|
+
shapes = [tuple(in_shape), *_stage_shapes(in_shape, convolution)]
|
|
406
|
+
for i, (before, after) in enumerate(pairwise(shapes)):
|
|
407
|
+
pool = convolution[i]['pool']
|
|
408
|
+
if 0 in after:
|
|
409
|
+
raise ValueError(
|
|
410
|
+
f"'convolution[{i}]['pool']' {pool} pools a {before[1]} x {before[2]} image to nothing"
|
|
411
|
+
)
|
|
412
|
+
# ConvTranspose2d multiplies by the pool, so a floored height or width would come back short
|
|
413
|
+
if is_mirrored and (before[1] % pool or before[2] % pool):
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"'convolution[{i}]['pool']' {pool} must divide the {before[1]} x {before[2]} image it pools,"
|
|
416
|
+
' so the decoder rebuilds it'
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _conv_arguments(in_shape, convolution):
|
|
421
|
+
"""
|
|
422
|
+
Returns the conv arguments as the "create" event records them, already checked by '_check_conv()'.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
in_shape: one image's (channels, height, width), or None.
|
|
426
|
+
convolution: the stages, each a dict of 'CONV_STAGE_KEYS'.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
{"in_shape": a tuple of ints or None, "convolution": a tuple of dicts, each in 'CONV_STAGE_KEYS' order}.
|
|
430
|
+
"""
|
|
431
|
+
return {
|
|
432
|
+
'in_shape': None if in_shape is None else tuple(int(size) for size in in_shape),
|
|
433
|
+
'convolution': tuple(
|
|
434
|
+
{key: int(stage[key]) for key in CONV_STAGE_KEYS} for stage in convolution
|
|
435
|
+
),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _sample_shape(in_features, in_shape):
|
|
440
|
+
"""Returns one sample's shape as the network reads it: 'in_shape', or (in_features,) when it is None."""
|
|
441
|
+
if in_shape is None:
|
|
442
|
+
return (int(in_features),)
|
|
443
|
+
return in_shape
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _stage_shapes(in_shape, convolution):
|
|
447
|
+
"""
|
|
448
|
+
Returns each conv stage's output (channels, height, width), in order; [] with no stages.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
in_shape: one image's (channels, height, width); unread with no stages.
|
|
452
|
+
convolution: the stages; each keeps the height and width through its Conv2d, then its
|
|
453
|
+
MaxPool2d floors them divided by its pool.
|
|
454
|
+
"""
|
|
455
|
+
shapes = []
|
|
456
|
+
shape = in_shape
|
|
457
|
+
for stage in convolution:
|
|
458
|
+
shape = (
|
|
459
|
+
stage['channels'],
|
|
460
|
+
shape[1] // stage['pool'],
|
|
461
|
+
shape[2] // stage['pool'],
|
|
462
|
+
)
|
|
463
|
+
shapes.append(shape)
|
|
464
|
+
return shapes
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _conv_stack(in_shape, convolution):
|
|
468
|
+
"""
|
|
469
|
+
Returns the conv layers: Conv2d, ReLU, and MaxPool2d (none for a pool of 1) per stage, then a Flatten; [] with no stages.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
in_shape: one image's (channels, height, width); its channels feed the first Conv2d.
|
|
473
|
+
convolution: the stages, each a dict of 'CONV_STAGE_KEYS'.
|
|
474
|
+
"""
|
|
475
|
+
if not convolution:
|
|
476
|
+
return []
|
|
477
|
+
layers = []
|
|
478
|
+
channels = in_shape[0]
|
|
479
|
+
for stage in convolution:
|
|
480
|
+
kernel = stage['kernel']
|
|
481
|
+
layers += [
|
|
482
|
+
nn.Conv2d(channels, stage['channels'], kernel, padding=kernel // 2),
|
|
483
|
+
nn.ReLU(),
|
|
484
|
+
]
|
|
485
|
+
if stage['pool'] > 1:
|
|
486
|
+
layers.append(nn.MaxPool2d(stage['pool']))
|
|
487
|
+
channels = stage['channels']
|
|
488
|
+
return layers + [nn.Flatten()]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _deconv_stack(in_shape, convolution):
|
|
492
|
+
"""
|
|
493
|
+
Returns the decoder's conv layers, after its last Linear: a ReLU and an Unflatten to the last
|
|
494
|
+
stage's maps, then one ConvTranspose2d per stage in reverse, its kernel and stride the stage's
|
|
495
|
+
pool, a ReLU between each and none after the last; [] with no stages.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
in_shape: one image's (channels, height, width), which the last ConvTranspose2d outputs.
|
|
499
|
+
convolution: the encoder's stages, each a dict of 'CONV_STAGE_KEYS'.
|
|
500
|
+
"""
|
|
501
|
+
if not convolution:
|
|
502
|
+
return []
|
|
503
|
+
shapes = [in_shape, *_stage_shapes(in_shape, convolution)]
|
|
504
|
+
layers = [nn.ReLU(), nn.Unflatten(1, shapes[-1])]
|
|
505
|
+
for before, stage in zip(shapes[-2::-1], convolution[::-1]):
|
|
506
|
+
pool = stage['pool']
|
|
507
|
+
layers += [
|
|
508
|
+
nn.ConvTranspose2d(stage['channels'], before[0], pool, stride=pool),
|
|
509
|
+
nn.ReLU(),
|
|
510
|
+
]
|
|
511
|
+
# the output style reads the last ConvTranspose2d's values, which a ReLU would clip at 0
|
|
512
|
+
return layers[:-1]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _shaped(inputs, shape, subject):
|
|
516
|
+
"""
|
|
517
|
+
Returns 'inputs' reshaped to (N, *shape), each sample as the network reads it.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
inputs: the samples, a tensor of shape (N, ...), e.g. (N, 1, 28, 28) images or (N, 784) rows.
|
|
521
|
+
shape: one sample's shape as the network reads it, e.g. (1, 28, 28), or (784,) for a
|
|
522
|
+
Linear network.
|
|
523
|
+
subject: what the samples are, for the message, e.g. "'X'".
|
|
524
|
+
"""
|
|
525
|
+
size = math.prod(shape)
|
|
526
|
+
if inputs.shape[1:].numel() != size:
|
|
527
|
+
raise ValueError(
|
|
528
|
+
f'{subject} must hold {size} values per sample, read as {tuple(shape)}, got shape {tuple(inputs.shape)}'
|
|
529
|
+
)
|
|
530
|
+
return inputs.reshape(len(inputs), *shape)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _scored(model, layers, X, name, shape):
|
|
534
|
+
"""
|
|
535
|
+
Returns 'layers(X)' for every sample, scored in eval() without gradients, the model's mode left as it was.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
model: the network 'layers' belong to; its mode is set and restored.
|
|
539
|
+
layers: the module to run, the whole model or one of its stacks, e.g. 'model.encoder'.
|
|
540
|
+
X: the samples, one per row: a tensor or NumPy array of shape (N, ...).
|
|
541
|
+
name: the caller's name for X, e.g. "Z", for the messages.
|
|
542
|
+
shape: one sample's shape as 'layers' read it, e.g. 'model.in_shape', or (latent_size,)
|
|
543
|
+
for a decoder; each sample is reshaped to it, as 'train_model()' feeds the network.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
the outputs tensor, on the CPU.
|
|
547
|
+
|
|
548
|
+
Note:
|
|
549
|
+
X is scored in chunks of the model's scoring batch size, its latest training's
|
|
550
|
+
'batch_size', each chunk moved to the model's device alone and its outputs back to the
|
|
551
|
+
CPU, so a phase of any size fits the GPU memory one training batch did.
|
|
552
|
+
"""
|
|
553
|
+
from .model import _score_batch_size
|
|
554
|
+
|
|
555
|
+
# PyTorch's own errors for these name neither 'X' nor the shape it needs
|
|
556
|
+
if not isinstance(X, (torch.Tensor, np.ndarray)):
|
|
557
|
+
raise TypeError(
|
|
558
|
+
f"'{name}' must be a tensor or NumPy array, got {type(X).__name__}"
|
|
559
|
+
)
|
|
560
|
+
if X.ndim < 2:
|
|
561
|
+
raise ValueError(
|
|
562
|
+
f"'{name}' must hold one row per sample, got shape {tuple(X.shape)}"
|
|
563
|
+
)
|
|
564
|
+
# the samples take the model's own dtype here, and its device one chunk at a time
|
|
565
|
+
parameter = next(model.parameters())
|
|
566
|
+
inputs = torch.as_tensor(X, dtype=parameter.dtype)
|
|
567
|
+
# each sample shaped as train_model() feeds the network; an empty X keeps its 0 rows
|
|
568
|
+
inputs = _shaped(inputs, shape, f"'{name}'")
|
|
569
|
+
is_training = model.training
|
|
570
|
+
# Dropout is off to score; the caller's mode comes back even when the forward pass fails
|
|
571
|
+
model.eval()
|
|
572
|
+
try:
|
|
573
|
+
with torch.no_grad():
|
|
574
|
+
outputs = [
|
|
575
|
+
layers(chunk.to(parameter.device)).cpu()
|
|
576
|
+
for chunk in inputs.split(_score_batch_size(model))
|
|
577
|
+
]
|
|
578
|
+
finally:
|
|
579
|
+
model.train(is_training)
|
|
580
|
+
return torch.cat(outputs)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# endregion
|
|
584
|
+
|
|
585
|
+
# region P U B L I C F U N C T I O N S
|
|
586
|
+
|
|
587
|
+
# endregion
|
|
588
|
+
# endregion
|
|
589
|
+
# endregion
|