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/train.py
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# region F I L E
|
|
2
|
+
# <copyright file="mcode-neural/src/mcode_neural/train.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.train'
|
|
9
|
+
@memberof mcode_neural
|
|
10
|
+
@created October 2026
|
|
11
|
+
@author Timothy McGuire, MicroCODE, Inc.
|
|
12
|
+
@description >
|
|
13
|
+
MicroCODE Shared Training 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 training loop for our models, one epoch
|
|
43
|
+
loop for every Model, a single neuron or a PyTorch network, classification,
|
|
44
|
+
regression, and autoencoder: each epoch trains on the model's 'train'
|
|
45
|
+
phase and scores every other phase through the Model's own step, keeping
|
|
46
|
+
the weights of the epoch its kind's rule picks on 'val'.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
REFERENCES:
|
|
50
|
+
-----------
|
|
51
|
+
1. MIT xPRO Course: AI and Deep Learning: Neural Network Applications
|
|
52
|
+
|
|
53
|
+
2. PyTorch -- Transfer Learning for Computer Vision Tutorial (the course's 'train_model()')
|
|
54
|
+
https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html
|
|
55
|
+
|
|
56
|
+
3. PyTorch -- torch.optim (optimizers and learning rate schedulers)
|
|
57
|
+
https://pytorch.org/docs/stable/optim.html
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# endregion
|
|
61
|
+
|
|
62
|
+
# region I M P O R T S
|
|
63
|
+
|
|
64
|
+
import inspect
|
|
65
|
+
import numbers
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
import copy
|
|
69
|
+
import gc
|
|
70
|
+
import itertools
|
|
71
|
+
import os
|
|
72
|
+
import traceback
|
|
73
|
+
|
|
74
|
+
import torch
|
|
75
|
+
|
|
76
|
+
from .data import data_loaders
|
|
77
|
+
|
|
78
|
+
from .model import HISTORY_DEFAULTS, MODEL_KINDS, _plain
|
|
79
|
+
|
|
80
|
+
from .model import _check_model
|
|
81
|
+
|
|
82
|
+
from .network import NETWORK_KINDS
|
|
83
|
+
|
|
84
|
+
from .model import _out_of_memory
|
|
85
|
+
|
|
86
|
+
from .model import _gpu_memory, model_save
|
|
87
|
+
|
|
88
|
+
from .model import _peaks
|
|
89
|
+
|
|
90
|
+
# endregion
|
|
91
|
+
|
|
92
|
+
# region P R I V A T E F U N C T I O N S
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_count(name, count):
|
|
96
|
+
"""
|
|
97
|
+
Raises a ValueError naming 'name' unless 'count' is a whole number of at least 1.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
name: the argument being checked, e.g. "num_epochs".
|
|
101
|
+
count: the value to check; NumPy integers are whole numbers, bools are not.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(count, bool) or not isinstance(count, numbers.Integral) or count < 1:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"'{name}' must be a whole number of at least 1, got {count!r}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_phases(model):
|
|
110
|
+
"""
|
|
111
|
+
Raises a ValueError unless the model's data has "train" first, a "val" phase, and labels in every phase.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
model: the model to train, its data loaded.
|
|
115
|
+
"""
|
|
116
|
+
datasets = model.data.datasets
|
|
117
|
+
phases = list(datasets)
|
|
118
|
+
if not phases or phases[0] != 'train':
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"'model.data' must start with 'train', so each epoch scores the weights it trained; its phases are {phases}"
|
|
121
|
+
)
|
|
122
|
+
if 'val' not in phases:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"'model.data' needs a 'val' phase to pick the best epoch; its phases are {phases}"
|
|
125
|
+
)
|
|
126
|
+
# a phase of new rows without labels can be predicted, but not scored every epoch
|
|
127
|
+
unlabeled = [
|
|
128
|
+
phase
|
|
129
|
+
for phase, dataset in datasets.items()
|
|
130
|
+
if len(dataset) and len(dataset[0]) != 2
|
|
131
|
+
]
|
|
132
|
+
if unlabeled:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"'model.data' phase(s) {unlabeled} have no labels to train or score against"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_keys(name, value, model):
|
|
139
|
+
"""
|
|
140
|
+
Raises a TypeError or ValueError naming 'name' unless 'value' is one item, or a dict keyed by 'model.optimizer_keys'.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
name: the argument being checked, "optimizer" or "scheduler".
|
|
144
|
+
value: the caller's optimizer or scheduler.
|
|
145
|
+
model: the Model to train; its 'optimizer_keys' are () for one optimizer.
|
|
146
|
+
"""
|
|
147
|
+
owner = f"'{type(model).__name__}.optimizer_keys'"
|
|
148
|
+
keys = list(model.optimizer_keys)
|
|
149
|
+
if not keys:
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
raise TypeError(
|
|
152
|
+
f"'{name}' must be one {name} for a {type(model).__name__}, which names no"
|
|
153
|
+
f' {owner}, got a dict of {list(value)}'
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
if not isinstance(value, dict):
|
|
157
|
+
raise TypeError(
|
|
158
|
+
f"'{name}' must be a dict of one {name} per key of {owner} {keys}, got {type(value).__name__}"
|
|
159
|
+
)
|
|
160
|
+
missing = [key for key in keys if key not in value]
|
|
161
|
+
extra = [key for key in value if key not in keys]
|
|
162
|
+
if missing or extra:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"'{name}' must have exactly the keys of {owner} {keys}; missing {missing}, extra {extra}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _each(value, function):
|
|
169
|
+
"""
|
|
170
|
+
Returns function(value) for one item, or {key: function(item)} for a dict of them, e.g. one optimizer per network.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
value: one optimizer or scheduler, or a dict of them keyed by 'optimizer_keys'.
|
|
174
|
+
function: applied to each one.
|
|
175
|
+
"""
|
|
176
|
+
if isinstance(value, dict):
|
|
177
|
+
return {key: function(item) for key, item in value.items()}
|
|
178
|
+
return function(value)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _optimizer_record(optimizer):
|
|
182
|
+
"""
|
|
183
|
+
Returns an optimizer's settings for a "train" event: its name, the constructor's settings given, then its defaults.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
optimizer: a PyTorch optimizer or a NeuronOptimizer; both hold 'defaults' and 'param_groups'.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
{"name", the settings given by the constructor's own names, "defaults": {the rest}}, and
|
|
190
|
+
"param_groups" when a group's setting differs from the constructor's, e.g. a decayed 'lr'.
|
|
191
|
+
"""
|
|
192
|
+
# the constructor's settings by its own names: given, or left at the default its signature names
|
|
193
|
+
signature = inspect.signature(type(optimizer)).parameters
|
|
194
|
+
settings = _plain(optimizer.defaults, 'hyperparams.optimizer')
|
|
195
|
+
given = {
|
|
196
|
+
key: value
|
|
197
|
+
for key, value in settings.items()
|
|
198
|
+
if key in signature and value != signature[key].default
|
|
199
|
+
}
|
|
200
|
+
record = {
|
|
201
|
+
'name': type(optimizer).__name__,
|
|
202
|
+
**given,
|
|
203
|
+
HISTORY_DEFAULTS: {
|
|
204
|
+
key: value for key, value in settings.items() if key not in given
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
# a group's setting only where it differs from the constructor's, e.g. a decayed 'lr'
|
|
208
|
+
groups = [
|
|
209
|
+
{
|
|
210
|
+
key: value
|
|
211
|
+
for key, value in _plain(
|
|
212
|
+
{key: group[key] for key in settings},
|
|
213
|
+
f'hyperparams.optimizer.param_groups[{i}]',
|
|
214
|
+
).items()
|
|
215
|
+
if value != settings[key]
|
|
216
|
+
}
|
|
217
|
+
for i, group in enumerate(optimizer.param_groups)
|
|
218
|
+
]
|
|
219
|
+
if any(groups):
|
|
220
|
+
record['param_groups'] = groups
|
|
221
|
+
return record
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _schedule_record(scheduler):
|
|
225
|
+
"""Returns a scheduler's name and public 'state_dict()' for a "train" event; torch's own '_' keys are its internals."""
|
|
226
|
+
state = scheduler.state_dict()
|
|
227
|
+
return {
|
|
228
|
+
'name': type(scheduler).__name__,
|
|
229
|
+
**{key: value for key, value in state.items() if not key.startswith('_')},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _limit_text(device):
|
|
234
|
+
"""
|
|
235
|
+
Returns the GPU memory this process may use, as a report names it: "the 6.00 GiB cap", or "the GPU's 8.00 GiB".
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
device: a CUDA device.
|
|
239
|
+
"""
|
|
240
|
+
total, cap = _gpu_memory(device)
|
|
241
|
+
return f'the {cap:.2f} GiB cap' if cap is not None else f"the GPU's {total:.2f} GiB"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _peak_text(peaks, device):
|
|
245
|
+
"""
|
|
246
|
+
Returns peaks as one line reads them, e.g. "3.48 GiB allocated, 5.07 GiB reserved of the 6.00 GiB cap".
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
peaks: '_peaks()', or results holding its two keys.
|
|
250
|
+
device: the CUDA device they were measured on.
|
|
251
|
+
"""
|
|
252
|
+
return (
|
|
253
|
+
f'{peaks["peak_allocated_gib"]:.2f} GiB allocated,'
|
|
254
|
+
f' {peaks["peak_reserved_gib"]:.2f} GiB reserved of {_limit_text(device)}'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _memory(device):
|
|
259
|
+
"""
|
|
260
|
+
Returns a network run's memory for its "train" event: its device, its cap, and its peaks, all but the device None on a CPU.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
device: the network's device.
|
|
264
|
+
"""
|
|
265
|
+
if device.type != 'cuda':
|
|
266
|
+
return {
|
|
267
|
+
'device': str(device),
|
|
268
|
+
'cap_gib': None,
|
|
269
|
+
'peak_allocated_gib': None,
|
|
270
|
+
'peak_reserved_gib': None,
|
|
271
|
+
}
|
|
272
|
+
_, cap = _gpu_memory(device)
|
|
273
|
+
return {
|
|
274
|
+
'device': str(device),
|
|
275
|
+
'cap_gib': None if cap is None else round(cap, 3),
|
|
276
|
+
**_peaks(device),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _release(model, device):
|
|
281
|
+
"""
|
|
282
|
+
Gives back the GPU memory a network no longer needs: every parameter's gradient, and PyTorch's cached blocks.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
model: the network.
|
|
286
|
+
device: its device; on a CPU only the gradients are dropped; None, a NumPy neuron's,
|
|
287
|
+
holds nothing to release.
|
|
288
|
+
"""
|
|
289
|
+
if device is None:
|
|
290
|
+
return
|
|
291
|
+
model.zero_grad(set_to_none=True)
|
|
292
|
+
if device.type == 'cuda':
|
|
293
|
+
# a model and its Networks refer to each other, so a discarded one is freed only when collected
|
|
294
|
+
gc.collect()
|
|
295
|
+
torch.cuda.empty_cache()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _clear_frames(error):
|
|
299
|
+
"""
|
|
300
|
+
Clears the local variables of every finished frame in an error's traceback, and in each error it was raised from.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
error: the error a run raised.
|
|
304
|
+
|
|
305
|
+
Note:
|
|
306
|
+
A Notebook keeps the last traceback, and its frames hold the failed batch's tensors, so the
|
|
307
|
+
GPU memory they use is freed only when the frames are cleared; '%debug' then shows no locals.
|
|
308
|
+
"""
|
|
309
|
+
while error is not None:
|
|
310
|
+
traceback.clear_frames(error.__traceback__)
|
|
311
|
+
error = error.__context__
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _save_whole(model, path):
|
|
315
|
+
"""
|
|
316
|
+
Saves 'model' with 'model_save()' to 'path' + ".tmp", then replaces 'path' with it, so a crash mid-write keeps the last file.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
model: the model to save.
|
|
320
|
+
path: the checkpoint file.
|
|
321
|
+
"""
|
|
322
|
+
temporary = f'{os.fspath(path)}.tmp'
|
|
323
|
+
model_save(model, temporary)
|
|
324
|
+
os.replace(temporary, path)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _to_cpu(value):
|
|
328
|
+
"""
|
|
329
|
+
Returns a copy of 'value' with every tensor in it copied to system RAM, e.g. an optimizer's 'state_dict()'.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
value: a tensor, or a dict, list, or tuple holding tensors and plain values.
|
|
333
|
+
"""
|
|
334
|
+
if isinstance(value, torch.Tensor):
|
|
335
|
+
return value.detach().to('cpu', copy=True)
|
|
336
|
+
if isinstance(value, dict):
|
|
337
|
+
return {key: _to_cpu(item) for key, item in value.items()}
|
|
338
|
+
if isinstance(value, (list, tuple)):
|
|
339
|
+
return type(value)(_to_cpu(item) for item in value)
|
|
340
|
+
return copy.deepcopy(value)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _run_phase(model, loader, phase, optimizer):
|
|
344
|
+
"""
|
|
345
|
+
Runs one pass over one phase's loader through the Model's own step, training only in the "train" phase.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
model: the Model, which moves each batch to its own device.
|
|
349
|
+
loader: the phase's DataLoader of (inputs, labels) batches.
|
|
350
|
+
phase: the phase's name, its key in 'model.data.datasets'.
|
|
351
|
+
optimizer: what 'model.train_step()' steps, once per batch in the "train" phase.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
{metric: value}, each metric averaged over every sample the loader served.
|
|
355
|
+
"""
|
|
356
|
+
name = f"'model.data' phase {phase!r}"
|
|
357
|
+
metrics = MODEL_KINDS[model.kind]['metrics']
|
|
358
|
+
totals = dict.fromkeys(metrics, 0.0)
|
|
359
|
+
samples = 0
|
|
360
|
+
|
|
361
|
+
for inputs, labels in loader:
|
|
362
|
+
# S T E P: the Model's own, trained in the "train" phase only, its totals over the batch
|
|
363
|
+
if phase == 'train':
|
|
364
|
+
step = model.train_step(inputs, labels, optimizer, name)
|
|
365
|
+
else:
|
|
366
|
+
step = model.score_step(inputs, labels, name)
|
|
367
|
+
# a step that scores other metrics would chart curves its kind does not name
|
|
368
|
+
if tuple(step) != metrics:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"'{type(model).__name__}' scored {list(step)} in {name};"
|
|
371
|
+
f' its kind {model.kind!r} scores {list(metrics)}, in that order'
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# T A L L Y: each batch's totals, so the phase average weighs every sample alike
|
|
375
|
+
samples += len(inputs)
|
|
376
|
+
for metric, total in step.items():
|
|
377
|
+
totals[metric] += total
|
|
378
|
+
|
|
379
|
+
if not samples:
|
|
380
|
+
raise ValueError(f'{name} served no samples')
|
|
381
|
+
return {metric: total / samples for metric, total in totals.items()}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _train(model, num_epochs, run_phase, hyperparams, device=None, checkpoint=None):
|
|
385
|
+
"""
|
|
386
|
+
Runs the epoch loop every Model shares, then keeps the weights of the val epoch its kind's 'is_better' picks last.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
model: the model, its data loaded and checked by '_check_phases()'.
|
|
390
|
+
num_epochs: the number of epochs, already checked.
|
|
391
|
+
run_phase: run_phase(phase) -> {metric: value}; trains in the "train" phase only.
|
|
392
|
+
hyperparams: the trainer's settings, plain values from '_plain()', for the "train" event.
|
|
393
|
+
device: a network's device, whose memory the run records, and on CUDA prints and
|
|
394
|
+
releases; None for a single neuron, which records none.
|
|
395
|
+
checkpoint: a file the model is saved to at each new best epoch and at the end; None
|
|
396
|
+
saves nothing.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
the same 'model', its best val epoch's weights loaded, with a "train" event appended to
|
|
400
|
+
its history: 'hyperparams', and the results {"curves", "best_epoch",
|
|
401
|
+
f"best_val_{metric}", "seconds", "tests": {}}, so 'model.curves' and 'model.best_epoch'
|
|
402
|
+
read this run; a network's results add "device", "cap_gib", "peak_allocated_gib", and
|
|
403
|
+
"peak_reserved_gib" after "seconds".
|
|
404
|
+
|
|
405
|
+
Note:
|
|
406
|
+
An interrupted or failed run, e.g. Interrupt Kernel or an out-of-memory error, puts back
|
|
407
|
+
the weights the model had when it started, records no event, and raises again. A
|
|
408
|
+
checkpoint written before then keeps its best epoch, its last "train" event the run so
|
|
409
|
+
far, with "status": "in process".
|
|
410
|
+
"""
|
|
411
|
+
# S E T U P: the model kind's metrics, one empty curve per phase and metric, no best epoch yet
|
|
412
|
+
model_kind = MODEL_KINDS[model.kind]
|
|
413
|
+
phases = list(model.data.datasets)
|
|
414
|
+
width = max(len(phase) for phase in phases)
|
|
415
|
+
training_curves = {
|
|
416
|
+
f'{phase}_{metric}': [] for phase in phases for metric in model_kind['metrics']
|
|
417
|
+
}
|
|
418
|
+
best = None
|
|
419
|
+
start = time.perf_counter()
|
|
420
|
+
|
|
421
|
+
# H I S T O R Y: this run's settings and results, one event, so an earlier run's stay as they were
|
|
422
|
+
def results(seconds):
|
|
423
|
+
return {
|
|
424
|
+
'curves': training_curves,
|
|
425
|
+
'best_epoch': best_epoch,
|
|
426
|
+
f'best_val_{model_kind["best"]}': best,
|
|
427
|
+
'seconds': round(seconds, 3),
|
|
428
|
+
**({} if device is None else _memory(device)),
|
|
429
|
+
'tests': {},
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# S T A R T: the weights before this run, in system RAM, put back if the run is interrupted or fails
|
|
433
|
+
start_weights = model.weights()
|
|
434
|
+
try:
|
|
435
|
+
# E P O C H L O O P: train, then score every phase, keeping the best val epoch's weights
|
|
436
|
+
for epoch in range(num_epochs):
|
|
437
|
+
print(f'\nEpoch {epoch}/{num_epochs - 1}')
|
|
438
|
+
print('-' * 10)
|
|
439
|
+
|
|
440
|
+
for phase in phases:
|
|
441
|
+
values = run_phase(phase)
|
|
442
|
+
for metric, value in values.items():
|
|
443
|
+
training_curves[f'{phase}_{metric}'].append(value)
|
|
444
|
+
scores = ' '.join(
|
|
445
|
+
f'{metric.title()}: {value:.4f}' for metric, value in values.items()
|
|
446
|
+
)
|
|
447
|
+
print(f'{phase:{width}} {scores}')
|
|
448
|
+
|
|
449
|
+
if phase == 'val':
|
|
450
|
+
score = values[model_kind['best']]
|
|
451
|
+
if best is None or model_kind['is_better'](score, best):
|
|
452
|
+
best = score
|
|
453
|
+
best_epoch = epoch
|
|
454
|
+
best_weights = model.weights()
|
|
455
|
+
|
|
456
|
+
# C H E C K P O I N T: at an epoch's end, a new best is saved with the run so far, in the file only
|
|
457
|
+
if checkpoint is not None and best_epoch == epoch:
|
|
458
|
+
so_far = results(time.perf_counter() - start)
|
|
459
|
+
model._record('train', hyperparams, so_far | {'status': 'in process'})
|
|
460
|
+
try:
|
|
461
|
+
_save_whole(model, checkpoint)
|
|
462
|
+
finally:
|
|
463
|
+
model.history.pop()
|
|
464
|
+
except BaseException:
|
|
465
|
+
model.load_weights(start_weights)
|
|
466
|
+
raise
|
|
467
|
+
|
|
468
|
+
# B E S T E P O C H: report the run's time and best score, then restore that epoch's weights
|
|
469
|
+
elapsed = time.perf_counter() - start
|
|
470
|
+
minutes, seconds = divmod(round(elapsed), 60)
|
|
471
|
+
model.load_weights(best_weights)
|
|
472
|
+
finished = results(elapsed)
|
|
473
|
+
print(f'\nTraining complete in {minutes}m {seconds}s')
|
|
474
|
+
if device is not None and device.type == 'cuda':
|
|
475
|
+
print(f'Peak GPU memory: {_peak_text(finished, device)}')
|
|
476
|
+
_release(model, device)
|
|
477
|
+
held = torch.cuda.memory_reserved(device) / 2**30
|
|
478
|
+
print(f'GPU memory held: {held:.2f} GiB')
|
|
479
|
+
print(
|
|
480
|
+
f'\nBEST val {model_kind["best"].title()}: {best:.4f} at epoch {best_epoch}\n\n'
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
model._record('train', hyperparams, finished)
|
|
484
|
+
if checkpoint is not None:
|
|
485
|
+
_save_whole(model, checkpoint)
|
|
486
|
+
|
|
487
|
+
return model
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# endregion
|
|
491
|
+
|
|
492
|
+
# region P U B L I C F U N C T I O N S
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def train_model(model, optimizer, scheduler, num_epochs, batch_size, checkpoint=None):
|
|
496
|
+
"""
|
|
497
|
+
Trains a Model on its own data, scoring every phase each epoch, and keeps its best val epoch's weights.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
model: the Model to train, its data loaded, e.g. ClassificationModel,
|
|
501
|
+
BinaryClassificationModel, RegressionModel, AutoencoderModel, or a single
|
|
502
|
+
neuron such as LogisticClassifierNeuronModel; its own 'train_step()' and
|
|
503
|
+
'score_step()' make its losses, step its optimizer, and score each batch.
|
|
504
|
+
optimizer: the optimizer of 'model.parameters()', e.g. torch.optim.Adam; a
|
|
505
|
+
NeuronOptimizer for a single neuron; or, for a Model that names
|
|
506
|
+
'optimizer_keys', a dict of one optimizer per key, e.g.
|
|
507
|
+
{"generator": ..., "discriminator": ...}.
|
|
508
|
+
scheduler: steps once per epoch, after the "train" phase, e.g. ExponentialLR; None for
|
|
509
|
+
no schedule. For a Model that names 'optimizer_keys', a dict of one
|
|
510
|
+
scheduler per key. PyTorch's schedulers take only PyTorch optimizers.
|
|
511
|
+
num_epochs: the number of epochs, a whole number of at least 1.
|
|
512
|
+
batch_size: the number of samples in each batch, a whole number of at least 1.
|
|
513
|
+
checkpoint: a file path, e.g. "../Models/vgg11.pt", that the model is saved to with
|
|
514
|
+
'model_save()' at each epoch whose val score is the best so far, and at the
|
|
515
|
+
end; its folder must exist. None, the default, saves nothing.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
the same 'model', its best val epoch's weights loaded, a network in eval() mode, with
|
|
519
|
+
'model.curves' = {f"{phase}_{metric}": [one float per epoch]} and that epoch's number
|
|
520
|
+
in 'model.best_epoch', as 'show_training_curves(model)' reads them, both from the
|
|
521
|
+
"train" event this run appends to 'model.history'.
|
|
522
|
+
|
|
523
|
+
Note:
|
|
524
|
+
The phases are those of 'model.data': "train" first, then "val", which picks the best
|
|
525
|
+
epoch, and any others, e.g. "test", scored every epoch; each is served by an unshuffled
|
|
526
|
+
DataLoader of 'batch_size', as 'data_loaders()' makes.
|
|
527
|
+
A classifier trains on nn.CrossEntropyLoss of its logits, scores "loss" and "acc", and
|
|
528
|
+
keeps its highest val acc; a regressor trains on nn.MSELoss, scores "loss" only, its
|
|
529
|
+
(N, 1) output flattened to (N,) targets, and keeps its lowest val loss.
|
|
530
|
+
A binary classifier trains on nn.BCEWithLogitsLoss: its labels, (N,) for one output or
|
|
531
|
+
(N, k) for k, must be 0 or 1 and are cast to the model's float dtype, so integer labels
|
|
532
|
+
train as they are; it predicts 'logit > 0', its "acc" is the share of (sample, label)
|
|
533
|
+
answers that are right, and it keeps its highest val acc.
|
|
534
|
+
An autoencoder trains on nn.MSELoss of each sample against itself, scores
|
|
535
|
+
"loss" only, and keeps its lowest val loss; its labels only serve the batches, and every
|
|
536
|
+
input must lie in its output style's 'output_range'.
|
|
537
|
+
A single neuron trains with a NeuronOptimizer on its own loss ('loss()' and
|
|
538
|
+
'loss_gradient()'): each "train" batch is scored, then the neuron steps by the mean of
|
|
539
|
+
its samples' updates, so 'batch_size=1' is the course's per-sample loop. Its labels
|
|
540
|
+
must be one per sample, each one of its 'label_value'. A classifier neuron scores
|
|
541
|
+
"loss" and "acc" and keeps its highest val acc, predicting its higher label where a is
|
|
542
|
+
above its threshold; a regressor neuron scores "loss" only and keeps its lowest val
|
|
543
|
+
loss. A neuron has no device, so its results record no memory.
|
|
544
|
+
A Model of several networks, e.g. a GAN, names its optimizers' keys in
|
|
545
|
+
'optimizer_keys', and a missing or extra key fails naming it; its step returns one
|
|
546
|
+
total per metric of its kind, e.g. "dloss" and "gloss", and a kind whose 'is_better'
|
|
547
|
+
always holds keeps its last epoch.
|
|
548
|
+
The first epoch always counts as the best so far; a later epoch replaces it only by
|
|
549
|
+
scoring strictly better, so the earliest of equal epochs is kept. Each batch is moved
|
|
550
|
+
to the model's own device and shaped as the network reads each sample, its 'in_shape':
|
|
551
|
+
one row for a Linear network, one (channels, height, width) image for a network with
|
|
552
|
+
conv stages. Epochs are numbered from 0 in every printed line.
|
|
553
|
+
The "train" event records the optimizer's name and its constructor's settings by their
|
|
554
|
+
own names, e.g. "lr": the ones given, then those left at their defaults under
|
|
555
|
+
"defaults", which 'show_model_history()' marks "(default)"; a setting the optimizer
|
|
556
|
+
class fixes itself, e.g. AdamW's 'decoupled_weight_decay', reads as a default. A param
|
|
557
|
+
group's setting is recorded, under "param_groups", only where it differs from the
|
|
558
|
+
constructor's when the run starts, e.g. a decayed 'lr'. It also records the scheduler's
|
|
559
|
+
name and public 'state_dict()' (or None), 'num_epochs', and 'batch_size'; training
|
|
560
|
+
again appends another event, so every run is kept. A dict of optimizers is recorded
|
|
561
|
+
by its keys, {key: record}, and a dict of schedulers the same way.
|
|
562
|
+
A batch that runs past the GPU memory this process may use, the cap 'model_device()'
|
|
563
|
+
set or the GPU's whole memory, raises torch.OutOfMemoryError naming the phase and
|
|
564
|
+
'batch_size', e.g. "phase 'train' at batch_size=100 needs more than the 6.00 GiB cap;
|
|
565
|
+
try a smaller batch_size"; PyTorch's own error is its cause.
|
|
566
|
+
The "train" event's results also record the run's "device", e.g. "cuda:0", the cap
|
|
567
|
+
'model_device()' set in GiB ("cap_gib", None with no cap), and its peak GPU memory in
|
|
568
|
+
GiB, "peak_allocated_gib" and "peak_reserved_gib" (None on a CPU). On CUDA it prints,
|
|
569
|
+
after the run's time, "Peak GPU memory: 3.48 GiB allocated, 5.07 GiB reserved of the
|
|
570
|
+
6.00 GiB cap" and, once released, "GPU memory held: 0.55 GiB".
|
|
571
|
+
However the run ends, every parameter's gradient is dropped and PyTorch's cached GPU
|
|
572
|
+
blocks are returned to the driver. An interrupted or failed run, e.g. Interrupt Kernel
|
|
573
|
+
or an out-of-memory error, records no event and puts back the weights the model had when
|
|
574
|
+
it started; its traceback's local variables are cleared, so the failed batch leaves the
|
|
575
|
+
GPU ('%debug' shows no locals). The optimizer's state, e.g. Adam's two moments per
|
|
576
|
+
weight, is the caller's: free it with 'del optimizer', or build a new optimizer for the
|
|
577
|
+
next phase.
|
|
578
|
+
A checkpoint is written to "<checkpoint>.tmp" and then replaces the file, so a crash
|
|
579
|
+
mid-write keeps the last best. Until the run ends its history's last "train" event is
|
|
580
|
+
the run so far, with "status": "in process", which 'show_model_history()' reads "In
|
|
581
|
+
process..."; the finished run rewrites it with the finished event. 'model_load()' reads
|
|
582
|
+
it as any saved model.
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
>>> model.data = data_frame_split(df, input_columns, "color", torch.int64,
|
|
586
|
+
... input_columns, wine_classes, seed=42)
|
|
587
|
+
>>> optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
|
|
588
|
+
>>> scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
|
|
589
|
+
>>> model = train_model(model, optimizer, scheduler, num_epochs=50, batch_size=10)
|
|
590
|
+
>>> show_training_curves(model)
|
|
591
|
+
>>> neuron = train_model(neuron, NeuronOptimizer(lr=0.01), None, num_epochs=10, batch_size=1)
|
|
592
|
+
"""
|
|
593
|
+
# V A L I D A T E: the model, its phases, the counts, and its optimizers fail by name before training
|
|
594
|
+
_check_model(model)
|
|
595
|
+
_check_phases(model)
|
|
596
|
+
_check_count('num_epochs', num_epochs)
|
|
597
|
+
_check_count('batch_size', batch_size)
|
|
598
|
+
_check_keys('optimizer', optimizer, model)
|
|
599
|
+
if scheduler is not None:
|
|
600
|
+
_check_keys('scheduler', scheduler, model)
|
|
601
|
+
if checkpoint is not None:
|
|
602
|
+
if not isinstance(checkpoint, (str, os.PathLike)):
|
|
603
|
+
raise TypeError(
|
|
604
|
+
f'\'checkpoint\' must be a file path, e.g. "../Models/vgg11.pt", got {type(checkpoint).__name__}'
|
|
605
|
+
)
|
|
606
|
+
folder = os.path.dirname(os.path.abspath(checkpoint))
|
|
607
|
+
if not os.path.isdir(folder):
|
|
608
|
+
raise ValueError(
|
|
609
|
+
f"'checkpoint' {os.fspath(checkpoint)!r} is in a folder that does not exist: {folder!r}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# H Y P E R P A R A M E T E R S: this run's settings, made plain now, so one a saved model
|
|
613
|
+
# cannot keep fails by name before training; torch's own '_' keys are its internals
|
|
614
|
+
hyperparams = _plain(
|
|
615
|
+
{
|
|
616
|
+
'trainer': 'train_model',
|
|
617
|
+
'optimizer': _each(optimizer, _optimizer_record),
|
|
618
|
+
'scheduler': None
|
|
619
|
+
if scheduler is None
|
|
620
|
+
else _each(scheduler, _schedule_record),
|
|
621
|
+
'num_epochs': num_epochs,
|
|
622
|
+
'batch_size': batch_size,
|
|
623
|
+
'checkpoint': None if checkpoint is None else os.fspath(checkpoint),
|
|
624
|
+
},
|
|
625
|
+
'hyperparams',
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# S E T U P: one loader per phase, and the device its network kind trains on, None for NumPy
|
|
629
|
+
loaders = data_loaders(model.data.datasets, batch_size)
|
|
630
|
+
device = NETWORK_KINDS[model.network_kind]['device'](model)
|
|
631
|
+
|
|
632
|
+
def run_phase(phase):
|
|
633
|
+
try:
|
|
634
|
+
values = _run_phase(model, loaders[phase], phase, optimizer)
|
|
635
|
+
except torch.OutOfMemoryError as error:
|
|
636
|
+
raise _out_of_memory(phase, batch_size, device) from error
|
|
637
|
+
if phase == 'train' and scheduler is not None:
|
|
638
|
+
_each(scheduler, lambda item: item.step())
|
|
639
|
+
return values
|
|
640
|
+
|
|
641
|
+
# M E M O R Y: the peak is this run's; whatever ends it, its gradients and cached blocks go back
|
|
642
|
+
if device is not None and device.type == 'cuda':
|
|
643
|
+
torch.cuda.reset_peak_memory_stats(device)
|
|
644
|
+
try:
|
|
645
|
+
return _train(model, num_epochs, run_phase, hyperparams, device, checkpoint)
|
|
646
|
+
except BaseException as error:
|
|
647
|
+
_clear_frames(error)
|
|
648
|
+
raise
|
|
649
|
+
finally:
|
|
650
|
+
_release(model, device)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def train_probe(model, optimizer, batch_sizes, num_batches=2):
|
|
654
|
+
"""
|
|
655
|
+
Measures the peak GPU memory of training a network at each batch size, smallest first, and leaves it as it was.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
model: the network to probe, on a CUDA device, its data loaded, e.g. ClassificationModel.
|
|
659
|
+
optimizer: the optimizer the run will use, of 'model.parameters()', or the dict of
|
|
660
|
+
them a Model that names 'optimizer_keys' takes; it steps, so its state,
|
|
661
|
+
e.g. Adam's two moments per weight, counts in each peak.
|
|
662
|
+
batch_sizes: the batch sizes to try, e.g. [32, 64, 100], each a whole number of at least 1
|
|
663
|
+
and at most the size of the "train" phase.
|
|
664
|
+
num_batches: the training batches run at each size, a whole number of at least 1.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
{batch_size: {"peak_allocated_gib", "peak_reserved_gib"}}, smallest first, in GiB to 3
|
|
668
|
+
places; the first size that does not fit maps to None, and no larger size is tried.
|
|
669
|
+
|
|
670
|
+
Note:
|
|
671
|
+
Each size trains 'num_batches' batches of the "train" phase as 'train_model()' does,
|
|
672
|
+
through the Model's own 'train_step()'. Reserved is what the cap of
|
|
673
|
+
'model_device()' bounds. One line is printed per size, e.g. "batch_size 64: 5.00 GiB
|
|
674
|
+
allocated, 5.56 GiB reserved of the 6.00 GiB cap" or "batch_size 100: does not fit the
|
|
675
|
+
6.00 GiB cap". Afterwards the model's weights, the optimizer's state, and torch's random
|
|
676
|
+
state are put back, its gradients and PyTorch's cached GPU blocks are released, and
|
|
677
|
+
nothing is recorded in its history; the scheduler is not used.
|
|
678
|
+
|
|
679
|
+
Example:
|
|
680
|
+
>>> model = ClassificationModel(...).to(model_device())
|
|
681
|
+
>>> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
|
|
682
|
+
>>> train_probe(model, optimizer, [32, 48, 64, 100])
|
|
683
|
+
>>> model = train_model(model, optimizer, None, num_epochs=10, batch_size=64)
|
|
684
|
+
"""
|
|
685
|
+
# V A L I D A T E: the network, its data, the sizes, and its device fail by name before any batch
|
|
686
|
+
_check_model(model)
|
|
687
|
+
_check_phases(model)
|
|
688
|
+
_check_keys('optimizer', optimizer, model)
|
|
689
|
+
if not isinstance(batch_sizes, (list, tuple)) or not batch_sizes:
|
|
690
|
+
raise ValueError(
|
|
691
|
+
f"'batch_sizes' must be a list of whole numbers, e.g. [32, 64, 100], got {batch_sizes!r}"
|
|
692
|
+
)
|
|
693
|
+
dataset = model.data.datasets['train']
|
|
694
|
+
for i, batch_size in enumerate(batch_sizes):
|
|
695
|
+
_check_count(f'batch_sizes[{i}]', batch_size)
|
|
696
|
+
if batch_size > len(dataset):
|
|
697
|
+
raise ValueError(
|
|
698
|
+
f"'batch_sizes[{i}]' is {batch_size}, more than the {len(dataset)} samples of the 'train' phase"
|
|
699
|
+
)
|
|
700
|
+
_check_count('num_batches', num_batches)
|
|
701
|
+
device = NETWORK_KINDS[model.network_kind]['device'](model)
|
|
702
|
+
if device is None:
|
|
703
|
+
raise ValueError(
|
|
704
|
+
f"'model' is a {type(model).__name__}, which has no device; 'train_probe()' measures"
|
|
705
|
+
' the GPU memory of a PyTorch network'
|
|
706
|
+
)
|
|
707
|
+
if device.type != 'cuda':
|
|
708
|
+
raise ValueError(
|
|
709
|
+
f"'model' is on the {device}; 'train_probe()' measures GPU memory, so move it to the GPU"
|
|
710
|
+
" first, e.g. 'model.to(model_device())'"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# S T A R T: what the probe puts back, kept in system RAM so it adds nothing to a peak
|
|
714
|
+
start_weights = model.weights()
|
|
715
|
+
# each optimizer with the state it is put back to
|
|
716
|
+
start_states = _each(optimizer, lambda item: (item, _to_cpu(item.state_dict())))
|
|
717
|
+
was_training = model.training
|
|
718
|
+
width = len(str(max(batch_sizes)))
|
|
719
|
+
peaks = {}
|
|
720
|
+
|
|
721
|
+
# P R O B E: each size from a released GPU, its peak alone; the first that does not fit ends it
|
|
722
|
+
try:
|
|
723
|
+
with torch.random.fork_rng(devices=[device.index]):
|
|
724
|
+
for batch_size in sorted(batch_sizes):
|
|
725
|
+
_release(model, device)
|
|
726
|
+
torch.cuda.reset_peak_memory_stats(device)
|
|
727
|
+
loader = data_loaders({'train': dataset}, batch_size)['train']
|
|
728
|
+
try:
|
|
729
|
+
_run_phase(
|
|
730
|
+
model,
|
|
731
|
+
itertools.islice(loader, num_batches),
|
|
732
|
+
'train',
|
|
733
|
+
optimizer,
|
|
734
|
+
)
|
|
735
|
+
except torch.OutOfMemoryError as error:
|
|
736
|
+
_clear_frames(error)
|
|
737
|
+
peaks[batch_size] = None
|
|
738
|
+
print(
|
|
739
|
+
f'batch_size {batch_size:>{width}}: does not fit {_limit_text(device)}'
|
|
740
|
+
)
|
|
741
|
+
break
|
|
742
|
+
peaks[batch_size] = _peaks(device)
|
|
743
|
+
print(
|
|
744
|
+
f'batch_size {batch_size:>{width}}: {_peak_text(peaks[batch_size], device)}'
|
|
745
|
+
)
|
|
746
|
+
finally:
|
|
747
|
+
model.load_weights(start_weights)
|
|
748
|
+
_each(start_states, lambda start: start[0].load_state_dict(start[1]))
|
|
749
|
+
model.train(was_training)
|
|
750
|
+
_release(model, device)
|
|
751
|
+
return peaks
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# endregion
|
|
755
|
+
# endregion
|
|
756
|
+
# endregion
|