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/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