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,360 @@
1
+ # region F I L E
2
+ # <copyright file="mcode-neural/src/mcode_neural/predict.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.predict'
9
+ @memberof mcode_neural
10
+ @created October 2026
11
+ @author Timothy McGuire, MicroCODE, Inc.
12
+ @description >
13
+ MicroCODE Shared Prediction 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 MicroCODE's predictions from our trained models, a
43
+ single neuron or a network: a classifier's labels and class probabilities, a
44
+ regressor's values, and the metrics of either one or of an autoencoder, each scored on one phase of the
45
+ model's own data, without changing the model.
46
+
47
+
48
+ REFERENCES:
49
+ -----------
50
+ 1. MIT xPRO Course: AI and Deep Learning: Neural Network Applications
51
+
52
+ 2. PyTorch -- torch.no_grad and torch.nn.Module.eval
53
+ https://pytorch.org/docs/stable/generated/torch.no_grad.html
54
+ https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval
55
+
56
+ 3. Fawcett, T. -- An introduction to ROC analysis, Pattern Recognition Letters 27 (2006)
57
+ https://doi.org/10.1016/j.patrec.2005.10.010
58
+ """
59
+
60
+ # endregion
61
+
62
+ # region I M P O R T S
63
+
64
+ import numpy as np
65
+
66
+ import torch
67
+ from torch.utils.data import DataLoader
68
+
69
+ from .model import _article, _check_model, _out_of_memory, _score_batch_size
70
+
71
+ # endregion
72
+
73
+ # region P R I V A T E F U N C T I O N S
74
+
75
+
76
+ def _check_kind(model, kind, name):
77
+ """
78
+ Raises a TypeError unless 'model' is a mcode-neural Model, and a ValueError naming 'name' unless it is of 'kind'.
79
+
80
+ Args:
81
+ model: the caller's model.
82
+ kind: the kind the caller needs, "classification", "regression", or "autoencoder".
83
+ name: the calling function, e.g. "predict_classes".
84
+ """
85
+ _check_model(model)
86
+ if model.kind != kind:
87
+ raise ValueError(
88
+ f"'{name}()' needs {_article(kind)} model; 'model' is {_article(model.kind)} model"
89
+ )
90
+
91
+
92
+ def _phase_scores(model, phase, score):
93
+ """
94
+ Returns 'score(X)' for every sample of one phase, scored batch by batch, and the phase's labels.
95
+
96
+ Args:
97
+ model: a Model, its data loaded.
98
+ phase: a phase of 'model.data', e.g. "test".
99
+ score: score(X) -> a NumPy array, or a tuple of them, one row per sample of the batch X,
100
+ a NumPy array, e.g. 'model.classify'.
101
+
102
+ Returns:
103
+ (scores, labels): scores as 'score()' returns them, each array gathered over every batch
104
+ in phase order; labels the phase's labels as a NumPy array, or None for a phase with none.
105
+
106
+ Note:
107
+ The phase is read through a loader of '_score_batch_size(model)' samples, so neither
108
+ system RAM nor the GPU holds it whole. A GPU out-of-memory error names the phase and
109
+ the batch size.
110
+ """
111
+ dataset = model.data._dataset(phase)
112
+ batch_size = _score_batch_size(model)
113
+ parts, labels = [], []
114
+ try:
115
+ for batch in DataLoader(dataset, batch_size=batch_size):
116
+ scores = score(batch[0].numpy())
117
+ parts.append(scores if isinstance(scores, tuple) else (scores,))
118
+ if len(batch) == 2:
119
+ labels.append(batch[1].numpy())
120
+ except torch.OutOfMemoryError as error:
121
+ device = next(model.parameters()).device
122
+ raise _out_of_memory(phase, batch_size, device) from error
123
+ gathered = tuple(np.concatenate(arrays) for arrays in zip(*parts))
124
+ if not isinstance(scores, tuple):
125
+ gathered = gathered[0]
126
+ return gathered, (np.concatenate(labels) if labels else None)
127
+
128
+
129
+ def _roc_curve(is_up, scores):
130
+ """
131
+ Returns a binary classifier's ROC curve: its false and true positive rates at every threshold.
132
+
133
+ Args:
134
+ is_up: every sample's truth as a bool array, True for the higher (positive) class.
135
+ scores: every sample's probability of the higher class.
136
+
137
+ Returns:
138
+ (fpr, tpr) arrays from (0, 0) to (1, 1), one point per distinct score, highest first.
139
+
140
+ Note:
141
+ Fawcett's algorithm (REFERENCES 3): samples tied on a score pass the threshold together,
142
+ so a tie of positives and negatives steps diagonally.
143
+ """
144
+ order = np.argsort(-scores, kind='stable')
145
+ ranked, is_up = scores[order], is_up[order]
146
+ # the last sample of each run of equal scores, where the threshold lets the whole run pass
147
+ last = np.r_[np.flatnonzero(np.diff(ranked)), len(ranked) - 1]
148
+ true_positives = np.cumsum(is_up)[last]
149
+ false_positives = np.cumsum(~is_up)[last]
150
+ fpr = np.r_[0, false_positives / false_positives[-1]]
151
+ tpr = np.r_[0, true_positives / true_positives[-1]]
152
+ return fpr, tpr
153
+
154
+
155
+ def _fit_scores(prediction, actual):
156
+ """
157
+ Returns a regressor's fit: its mean absolute error, root mean squared error, and R².
158
+
159
+ Args:
160
+ prediction: every sample's predicted value.
161
+ actual: every sample's target value, in the same order; not constant.
162
+
163
+ Returns:
164
+ {"mae": float, "rmse": float, "r2": float}
165
+ """
166
+ error = prediction - actual
167
+ squared = error**2
168
+ spread = ((actual - actual.mean()) ** 2).sum()
169
+ return {
170
+ 'mae': float(np.abs(error).mean()),
171
+ 'rmse': float(np.sqrt(squared.mean())),
172
+ 'r2': float(1 - squared.sum() / spread),
173
+ }
174
+
175
+
176
+ def _classifier_scores(model, phase):
177
+ """
178
+ Returns a classifier's accuracy, and its ROC AUC when it has two classes.
179
+
180
+ Args:
181
+ model: a trained classifier with one label per sample.
182
+ phase: a labeled phase of 'model.data'.
183
+ """
184
+ labels = sorted(model.data.classes)
185
+ (columns, probabilities), Y = _phase_scores(model, phase, model.classify)
186
+ actual = np.ravel(Y)
187
+ # k sigmoid outputs are k labels per sample, not one class to score
188
+ if columns.ndim != 1:
189
+ raise ValueError(
190
+ f"'model' has {columns.shape[1]} outputs per sample; its metrics need one label per sample"
191
+ )
192
+ if probabilities.shape[1] != len(labels):
193
+ raise ValueError(
194
+ f"'model' outputs {probabilities.shape[1]} classes; 'model.data.classes' has {len(labels)}"
195
+ )
196
+ scores = {'accuracy': float((np.asarray(labels)[columns] == actual).mean())}
197
+ # a ROC curve ranks one score, the higher class's probability, so only two classes have an AUC
198
+ if len(labels) == 2:
199
+ is_up = actual == labels[1]
200
+ if is_up.all() or not is_up.any():
201
+ raise ValueError(
202
+ "the phase needs both classes for a ROC curve's AUC; it holds only one"
203
+ )
204
+ fpr, tpr = _roc_curve(is_up, probabilities[:, 1])
205
+ scores['auc'] = float(np.trapezoid(tpr, fpr))
206
+ return scores
207
+
208
+
209
+ def _regressor_scores(model, phase):
210
+ """
211
+ Returns a one-target regressor's MAE, RMSE, and R².
212
+
213
+ Args:
214
+ model: a trained regressor with one target.
215
+ phase: a labeled phase of 'model.data'.
216
+ """
217
+ prediction, Y = _phase_scores(model, phase, model.regress)
218
+ actual = np.ravel(Y)
219
+ if prediction.ndim != 1:
220
+ raise ValueError(
221
+ f"'model' has {prediction.shape[1]} outputs; its metrics need one target"
222
+ )
223
+ if np.ptp(actual) == 0:
224
+ raise ValueError("the phase's targets are constant, so R² is undefined")
225
+ return _fit_scores(prediction, actual)
226
+
227
+
228
+ def _reconstruction_errors(model, X):
229
+ """
230
+ Returns an autoencoder's reconstruction of every sample, and each sample's mean squared error.
231
+
232
+ Args:
233
+ model: a trained autoencoder.
234
+ X: the input samples, (N, ...).
235
+
236
+ Returns:
237
+ (rebuilt, errors): rebuilt (N, in_features), each sample encoded and decoded; errors (N,),
238
+ each sample's mean of (rebuilt - input)² over its values.
239
+ """
240
+ rebuilt = model.reconstruct(X)
241
+ errors = ((rebuilt - X.reshape(len(X), -1)) ** 2).mean(axis=1)
242
+ return rebuilt, errors
243
+
244
+
245
+ def _autoencoder_scores(model, phase):
246
+ """
247
+ Returns an autoencoder's mean squared error over every value of every sample, and its root.
248
+
249
+ Args:
250
+ model: a trained autoencoder.
251
+ phase: a labeled phase of 'model.data'; its labels are not read, an autoencoder is
252
+ scored against its input.
253
+ """
254
+ errors, _ = _phase_scores(
255
+ model, phase, lambda X: _reconstruction_errors(model, X)[1]
256
+ )
257
+ # every sample holds the same number of values, so the mean of the means is nn.MSELoss
258
+ mse = float(errors.mean())
259
+ return {'mse': mse, 'rmse': float(np.sqrt(mse))}
260
+
261
+
262
+ # endregion
263
+
264
+ # region P U B L I C F U N C T I O N S
265
+
266
+
267
+ def predict_classes(model, phase):
268
+ """
269
+ Returns a classifier's predicted column and class probabilities for every sample of one phase.
270
+
271
+ Args:
272
+ model: a trained classifier, a single neuron or a network, e.g.
273
+ LogisticClassifierNeuronModel or ClassificationModel, its data loaded.
274
+ phase: the phase of 'model.data' to predict, e.g. "test", or one added by
275
+ 'data_frame_phase()'; its labels, if any, are not read.
276
+
277
+ Returns:
278
+ (columns, probabilities) as NumPy arrays, columns int64 and probabilities float64:
279
+ one label per sample: (N,) columns and (N, k) probabilities, each row summing to 1;
280
+ a binary network's k labels: (N, k) columns, one 0 / 1 per output, and (N, k)
281
+ probabilities, each output's probability of 1.
282
+
283
+ Note:
284
+ A column is a position in the sorted labels of 'model.data.classes', from 0, as the
285
+ model was trained; 'sorted(model.data.classes)[column]' is its label. A network is
286
+ scored in eval() mode, without gradients, on its own device; a neuron does not touch
287
+ its cached z, a, grad_w, or grad_w_0. Neither one's weights or mode change.
288
+
289
+ Example:
290
+ >>> columns, probabilities = predict_classes(model, "test")
291
+ """
292
+ _check_kind(model, 'classification', 'predict_classes')
293
+ (columns, probabilities), _ = _phase_scores(model, phase, model.classify)
294
+ return columns, probabilities
295
+
296
+
297
+ def predict_values(model, phase):
298
+ """
299
+ Returns a regressor's predicted value(s) for every sample of one phase.
300
+
301
+ Args:
302
+ model: a trained regressor, a single neuron or a network, e.g. RegressorNeuronModel
303
+ or RegressionModel, its data loaded.
304
+ phase: the phase of 'model.data' to predict, e.g. "test", or one added by
305
+ 'data_frame_phase()'; its targets, if any, are not read.
306
+
307
+ Returns:
308
+ a float64 NumPy array: (N,) for one target, as the targets are, or (N, k) for k targets.
309
+
310
+ Note:
311
+ A network is scored in eval() mode, without gradients, on its own device; a neuron
312
+ does not touch its cached z, a, grad_w, or grad_w_0. Neither one's weights or mode change.
313
+
314
+ Example:
315
+ >>> values = predict_values(model, "test")
316
+ """
317
+ _check_kind(model, 'regression', 'predict_values')
318
+ values, _ = _phase_scores(model, phase, model.regress)
319
+ return values
320
+
321
+
322
+ def predict_metrics(model, phase):
323
+ """
324
+ Returns a trained model's metrics on one labeled phase of its data, as plain numbers.
325
+
326
+ Args:
327
+ model: a trained model, a single neuron or a network, its data loaded.
328
+ phase: a phase of 'model.data' that has labels, e.g. "test".
329
+
330
+ Returns:
331
+ a classifier's {"accuracy": float}, plus "auc", its ROC AUC, when it has two classes;
332
+ a one-target regressor's {"mae": float, "rmse": float, "r2": float}; an autoencoder's
333
+ {"mse": float, "rmse": float}.
334
+
335
+ Note:
336
+ The same model is scored as 'predict_classes()' or 'predict_values()' score it; the AUC
337
+ ranks each sample's probability of the higher label, as 'show_model_metrics()' charts it.
338
+ An autoencoder's mse is over every value of every reconstructed sample, the loss
339
+ 'train_model()' prints for the phase; its labels are required but not read.
340
+
341
+ Example:
342
+ >>> predict_metrics(model, "test")
343
+ {'accuracy': 0.9615, 'auc': 0.9912}
344
+ """
345
+ scorers = {
346
+ 'classification': _classifier_scores,
347
+ 'regression': _regressor_scores,
348
+ 'autoencoder': _autoencoder_scores,
349
+ }
350
+ _check_model(model)
351
+ if not model.data._is_labeled(phase):
352
+ raise ValueError(
353
+ f"'model.data' phase {phase!r} has no labels to score the model against"
354
+ )
355
+ return scorers[model.kind](model, phase)
356
+
357
+
358
+ # endregion
359
+ # endregion
360
+ # endregion