torchabc 0.1.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.
- torchabc/__init__.py +415 -0
- torchabc/cli.py +91 -0
- torchabc-0.1.0.dist-info/LICENSE +21 -0
- torchabc-0.1.0.dist-info/METADATA +234 -0
- torchabc-0.1.0.dist-info/RECORD +7 -0
- torchabc-0.1.0.dist-info/WHEEL +4 -0
- torchabc-0.1.0.dist-info/entry_points.txt +3 -0
torchabc/__init__.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import torch
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Union, Dict, List, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TorchABC(abc.ABC):
|
|
9
|
+
"""
|
|
10
|
+
A minimal abstract class for training, evaluation, and inference of pytorch models.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, device: Union[str, torch.device] = None, logger: Callable = print, **hparams):
|
|
14
|
+
"""Initialize the model.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
device : str or torch.device, optional
|
|
19
|
+
The device to use. Defaults to None, which will try CUDA, then MPS, and
|
|
20
|
+
finally fall back to CPU.
|
|
21
|
+
logger : Callable, optional
|
|
22
|
+
A logging function that takes a dictionary in input. Defaults to print.
|
|
23
|
+
**hparams :
|
|
24
|
+
Arbitrary keyword arguments that will be stored in the `self.hparams` namespace.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
device : torch.device
|
|
29
|
+
The device the model will operate on.
|
|
30
|
+
logger : Callable
|
|
31
|
+
The function used for logging.
|
|
32
|
+
hparams : SimpleNamespace
|
|
33
|
+
A namespace containing the hyperparameters.
|
|
34
|
+
epoch : int
|
|
35
|
+
The last epoch seen during training.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__()
|
|
38
|
+
if device is not None:
|
|
39
|
+
self.device = torch.device(device)
|
|
40
|
+
elif torch.cuda.is_available():
|
|
41
|
+
self.device = torch.device("cuda")
|
|
42
|
+
elif torch.backends.mps.is_available():
|
|
43
|
+
self.device = torch.device("mps")
|
|
44
|
+
else:
|
|
45
|
+
self.device = torch.device("cpu")
|
|
46
|
+
self.logger = logger
|
|
47
|
+
self.hparams = SimpleNamespace(**hparams)
|
|
48
|
+
self.epoch = 0
|
|
49
|
+
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
@cached_property
|
|
52
|
+
def network(self) -> torch.nn.Module:
|
|
53
|
+
"""The neural network.
|
|
54
|
+
|
|
55
|
+
Returns a `torch.nn.Module` whose input and output tensors assume the
|
|
56
|
+
batch size is the first dimension: (batch_size, ...).
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
@cached_property
|
|
62
|
+
def optimizer(self) -> torch.optim.Optimizer:
|
|
63
|
+
"""The optimizer for training the network.
|
|
64
|
+
|
|
65
|
+
Returns a `torch.optim.Optimizer` configured for `self.network.parameters()`.
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
@cached_property
|
|
71
|
+
def scheduler(self) -> Union[None, torch.optim.lr_scheduler.LRScheduler, torch.optim.lr_scheduler.ReduceLROnPlateau]:
|
|
72
|
+
"""The learning rate scheduler for the optimizer.
|
|
73
|
+
|
|
74
|
+
Returns a `torch.optim.lr_scheduler.LRScheduler` or `torch.optim.lr_scheduler.ReduceLROnPlateau`
|
|
75
|
+
configured for `self.optimizer`.
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@abc.abstractmethod
|
|
80
|
+
@cached_property
|
|
81
|
+
def dataloaders(self) -> Dict[str, torch.utils.data.DataLoader]:
|
|
82
|
+
"""The dataloaders for training and evaluation.
|
|
83
|
+
|
|
84
|
+
This method defines and returns a dictionary containing the `DataLoader` instances
|
|
85
|
+
for the training, validation, and testing datasets. The keys of the dictionary
|
|
86
|
+
should correspond to the names of the datasets (e.g., 'train', 'val', 'test'),
|
|
87
|
+
and the values should be their respective `torch.utils.data.DataLoader` objects.
|
|
88
|
+
|
|
89
|
+
Any transformation of the raw input data for each dataset should be implemented
|
|
90
|
+
within the `preprocess` method of this class. The `preprocess` method should
|
|
91
|
+
then be passed as the `transform` argument of the `Dataset` instances.
|
|
92
|
+
|
|
93
|
+
If you require custom collation logic (i.e., a specific way to merge a list of
|
|
94
|
+
samples into a batch beyond the default behavior), you should implement this
|
|
95
|
+
logic in the `collate` method of this class. The `collate` method should then be
|
|
96
|
+
passed to the `collate_fn` argument when creating the `DataLoader` instances.
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abc.abstractmethod
|
|
101
|
+
def preprocess(self, data: Any, flag: str = '') -> Any:
|
|
102
|
+
"""Prepare the raw data for the network.
|
|
103
|
+
|
|
104
|
+
The way this method processes the `data` depends on the `flag`.
|
|
105
|
+
When `flag` is empty (the default), the `data` are assumed to represent the
|
|
106
|
+
model's input that is used for inference. When `flag` has a specific value,
|
|
107
|
+
the method may perform different preprocessing steps such as transforming
|
|
108
|
+
the target or augmenting the input for training.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
data : Any
|
|
113
|
+
The raw input data to be processed.
|
|
114
|
+
flag : str, optional
|
|
115
|
+
A string indicating the purpose of the preprocessing. The default
|
|
116
|
+
is an empty string, meaning preprocess the model's input for inference.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Any
|
|
121
|
+
The preprocessed data.
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abc.abstractmethod
|
|
126
|
+
def postprocess(self, outputs: torch.Tensor) -> Any:
|
|
127
|
+
"""Postprocess the model's outputs.
|
|
128
|
+
|
|
129
|
+
This method transforms the outputs of the neural network to
|
|
130
|
+
generate the final predictions.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
outputs : torch.Tensor
|
|
135
|
+
The output tensor from `self.network`.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
Any
|
|
140
|
+
The postprocessed outputs.
|
|
141
|
+
"""
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
@abc.abstractmethod
|
|
145
|
+
def loss(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
146
|
+
"""Loss function.
|
|
147
|
+
|
|
148
|
+
This method defines the loss function that quantifies the discrepancy
|
|
149
|
+
between the neural network `outputs` and the corresponding `targets`.
|
|
150
|
+
The loss function should be differentiable to enable backpropagation.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
outputs : torch.Tensor
|
|
155
|
+
The tensor containing the network's output.
|
|
156
|
+
targets : torch.Tensor
|
|
157
|
+
The targets corresponding to the outputs.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
torch.Tensor
|
|
162
|
+
A scalar tensor representing the computed loss value.
|
|
163
|
+
"""
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
@abc.abstractmethod
|
|
167
|
+
def metrics(self, outputs: torch.Tensor, targets: torch.Tensor) -> Dict[str, float]:
|
|
168
|
+
"""Evaluation metrics.
|
|
169
|
+
|
|
170
|
+
This method calculates various metrics that quantify the discrepancy
|
|
171
|
+
between the neural network `outputs` and the corresponding `targets`.
|
|
172
|
+
Unlike `self.loss`, which is primarily used for training, these metrics
|
|
173
|
+
are only used for evaluation and they do not need to be differentiable.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
outputs : torch.Tensor
|
|
178
|
+
The tensor containing the network's output.
|
|
179
|
+
targets : torch.Tensor
|
|
180
|
+
The targets corresponding to the outputs.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
Dict[str, float]
|
|
185
|
+
A dictionary where the keys are the names of the metrics and the
|
|
186
|
+
values are the corresponding metric scores.
|
|
187
|
+
"""
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def collate(self, batch: Any) -> Any:
|
|
191
|
+
"""Collate a batch of data.
|
|
192
|
+
|
|
193
|
+
This method applies the `torch.utils.data.default_collate` function, which is
|
|
194
|
+
used as the default function for collation in dataloaders. For custom data types,
|
|
195
|
+
overwrite this function and pass it as the `collate_fn` argument to the dataloader.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
batch : Any
|
|
200
|
+
The batch of data to collate.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
Any
|
|
205
|
+
The collated batch of data.
|
|
206
|
+
"""
|
|
207
|
+
return torch.utils.data.default_collate(batch)
|
|
208
|
+
|
|
209
|
+
def move(self, data: Any) -> Any:
|
|
210
|
+
"""Move data to the current device.
|
|
211
|
+
|
|
212
|
+
This method moves the data to the device specified by `self.device`. It supports
|
|
213
|
+
moving tensors, lists, tuples, and dictionaries. For custom data types, overwrite
|
|
214
|
+
this function to implement the necessary logic for moving the data to the device.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
data : Any
|
|
219
|
+
The data to move to the current device.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
Any
|
|
224
|
+
The data moved to the current device.
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(data, torch.Tensor):
|
|
227
|
+
return data.to(self.device)
|
|
228
|
+
elif isinstance(data, list):
|
|
229
|
+
return [self.move(item) for item in data]
|
|
230
|
+
elif isinstance(data, tuple):
|
|
231
|
+
return tuple(self.move(item) for item in data)
|
|
232
|
+
elif isinstance(data, dict):
|
|
233
|
+
return {key: self.move(value) for key, value in data.items()}
|
|
234
|
+
else:
|
|
235
|
+
raise TypeError(
|
|
236
|
+
f"Unsupported data type: {type(data)}. "
|
|
237
|
+
"Please implement the move method for custom data types."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def train(self, epochs: int, on: str = 'train', val: str = 'val', gas: int = 1, callback: Callable = None) -> List[dict]:
|
|
241
|
+
"""Train the model.
|
|
242
|
+
|
|
243
|
+
This method sets the network to training mode, iterates through the training dataloader
|
|
244
|
+
for the given number of epochs, performs forward and backward passes, optimizes the
|
|
245
|
+
model parameters, and logs the training loss and metrics. It optionally performs validation
|
|
246
|
+
after each epoch.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
epochs : int
|
|
251
|
+
The number of training epochs to perform.
|
|
252
|
+
on : str, optional
|
|
253
|
+
The name of the training dataloader. Defaults to 'train'.
|
|
254
|
+
val : str, optional
|
|
255
|
+
The name of the validation dataloader. Defaults to 'val'.
|
|
256
|
+
gas : int, optional
|
|
257
|
+
The number of gradient accumulation steps. Defaults to 1 (no gradient accumulation).
|
|
258
|
+
callback : Callable, optional
|
|
259
|
+
A callback function that is called after each epoch. It should accept two arguments:
|
|
260
|
+
the instance itself and a list of dictionaries containing the loss and evaluation metrics.
|
|
261
|
+
When this function returns True, training stops.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
list
|
|
266
|
+
A list of dictionaries containing the loss and evaluation metrics.
|
|
267
|
+
"""
|
|
268
|
+
logs, log_batch, log_epoch = [], {}, {}
|
|
269
|
+
for epoch in range(self.epoch + 1, self.epoch + 1 + epochs):
|
|
270
|
+
self.epoch = epoch
|
|
271
|
+
self.network.train()
|
|
272
|
+
self.network.to(self.device)
|
|
273
|
+
self.optimizer.zero_grad()
|
|
274
|
+
loss_gas = 0
|
|
275
|
+
for batch, (inputs, targets) in enumerate(self.dataloaders[on], start=1):
|
|
276
|
+
inputs, targets = self.move((inputs, targets))
|
|
277
|
+
outputs = self.network(inputs)
|
|
278
|
+
loss = self.loss(outputs, targets)
|
|
279
|
+
loss = loss / gas
|
|
280
|
+
loss.backward()
|
|
281
|
+
loss_gas += loss.item()
|
|
282
|
+
if batch % gas == 0:
|
|
283
|
+
self.optimizer.step()
|
|
284
|
+
self.optimizer.zero_grad()
|
|
285
|
+
log_batch.update({on + "/epoch": epoch, on + "/batch": batch, on + "/loss": loss_gas})
|
|
286
|
+
log_batch.update({on + "/" + k: v for k, v in self.metrics(outputs, targets).items()})
|
|
287
|
+
self.logger(log_batch)
|
|
288
|
+
logs.append(log_batch.copy())
|
|
289
|
+
loss_gas = 0
|
|
290
|
+
if val:
|
|
291
|
+
log_epoch.update({val + "/epoch": epoch})
|
|
292
|
+
log_epoch.update({val + "/" + k: v for k, v in self.eval(on=val).items()})
|
|
293
|
+
if self.scheduler:
|
|
294
|
+
if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
295
|
+
if not val:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
"ReduceLROnPlateau scheduler requires validation metrics. "
|
|
298
|
+
"Please provide a validation dataloader. "
|
|
299
|
+
)
|
|
300
|
+
if not hasattr(self.hparams, "plateau"):
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"ReduceLROnPlateau scheduler requires a metric to monitor. "
|
|
303
|
+
"Please specify the metric name during initialization by "
|
|
304
|
+
f"using {self.__class__.__name__}(plateau='name'), "
|
|
305
|
+
"where 'name' is either 'loss' or a key returned by `self.metrics`."
|
|
306
|
+
)
|
|
307
|
+
self.scheduler.step(log_epoch[val + "/" + self.hparams.plateau])
|
|
308
|
+
log_epoch.update({val + "/lr": self.scheduler.get_last_lr()})
|
|
309
|
+
else:
|
|
310
|
+
self.scheduler.step()
|
|
311
|
+
log_epoch.update({val + "/lr": self.scheduler.get_last_lr()})
|
|
312
|
+
if log_epoch:
|
|
313
|
+
self.logger(log_epoch)
|
|
314
|
+
logs.append(log_epoch.copy())
|
|
315
|
+
if callback:
|
|
316
|
+
stop = callback(self, logs)
|
|
317
|
+
if stop:
|
|
318
|
+
break
|
|
319
|
+
return logs
|
|
320
|
+
|
|
321
|
+
def eval(self, on: str) -> Dict[str, float]:
|
|
322
|
+
"""Evaluate the model.
|
|
323
|
+
|
|
324
|
+
This method sets the network to evaluation mode, iterates through the
|
|
325
|
+
given dataloader, calculates the loss and metrics, and returns
|
|
326
|
+
the results. No gradients are computed during this process.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
on : str
|
|
331
|
+
The name of the dataloader to evaluate on. This should be one of
|
|
332
|
+
the keys in `self.dataloaders`.
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
dict
|
|
337
|
+
A dictionary containing the loss and evaluation metrics.
|
|
338
|
+
"""
|
|
339
|
+
self.network.eval()
|
|
340
|
+
tot_loss, num_batches = 0, 0
|
|
341
|
+
all_outputs, all_targets = [], []
|
|
342
|
+
with torch.no_grad():
|
|
343
|
+
self.network.to(self.device)
|
|
344
|
+
for inputs, targets in self.dataloaders[on]:
|
|
345
|
+
inputs, targets = self.move((inputs, targets))
|
|
346
|
+
outputs = self.network(inputs)
|
|
347
|
+
tot_loss += self.loss(outputs, targets).item()
|
|
348
|
+
all_outputs.append(outputs)
|
|
349
|
+
all_targets.append(targets)
|
|
350
|
+
num_batches += 1
|
|
351
|
+
metrics = self.metrics(torch.cat(all_outputs), torch.cat(all_targets))
|
|
352
|
+
metrics["loss"] = tot_loss / num_batches
|
|
353
|
+
return metrics
|
|
354
|
+
|
|
355
|
+
def predict(self, data: Any) -> Any:
|
|
356
|
+
"""Predict the raw data.
|
|
357
|
+
|
|
358
|
+
This method sets the network to evaluation mode, preprocesses and
|
|
359
|
+
collates the input data into a batch of size 1, performs a forward pass
|
|
360
|
+
without tracking gradients, and then postprocesses the output to
|
|
361
|
+
return the final prediction.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
data : Any
|
|
366
|
+
The raw input data to predict.
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
Any
|
|
371
|
+
The postprocessed prediction.
|
|
372
|
+
"""
|
|
373
|
+
self.network.eval()
|
|
374
|
+
self.network.to(self.device)
|
|
375
|
+
with torch.no_grad():
|
|
376
|
+
preprocessed = self.preprocess(data)
|
|
377
|
+
batch = [preprocessed]
|
|
378
|
+
collated = self.collate(batch)
|
|
379
|
+
inputs = self.move(collated)
|
|
380
|
+
outputs = self.network(inputs)
|
|
381
|
+
predictions = self.postprocess(outputs)
|
|
382
|
+
return predictions[0]
|
|
383
|
+
|
|
384
|
+
def save(self, checkpoint: str) -> None:
|
|
385
|
+
"""Save checkpoint.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
checkpoint : str
|
|
390
|
+
The path where to save the checkpoint.
|
|
391
|
+
"""
|
|
392
|
+
torch.save({
|
|
393
|
+
'hparams': self.hparams.__dict__,
|
|
394
|
+
'epoch': self.epoch,
|
|
395
|
+
'network_state_dict': self.network.state_dict(),
|
|
396
|
+
'optimizer_state_dict': self.optimizer.state_dict(),
|
|
397
|
+
'scheduler_state_dict': self.scheduler.state_dict() if self.scheduler else None,
|
|
398
|
+
|
|
399
|
+
}, checkpoint)
|
|
400
|
+
|
|
401
|
+
def load(self, checkpoint: str) -> None:
|
|
402
|
+
"""Load checkpoint.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
checkpoint : str
|
|
407
|
+
The path from where to load the checkpoint.
|
|
408
|
+
"""
|
|
409
|
+
checkpoint = torch.load(checkpoint, map_location=self.device)
|
|
410
|
+
self.hparams = SimpleNamespace(**checkpoint['hparams'])
|
|
411
|
+
self.epoch = checkpoint['epoch']
|
|
412
|
+
self.network.load_state_dict(checkpoint['network_state_dict'])
|
|
413
|
+
self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
|
|
414
|
+
if checkpoint['scheduler_state_dict']:
|
|
415
|
+
self.scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
|
torchabc/cli.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from . import TorchABC
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(description="Generate template.")
|
|
9
|
+
parser.add_argument('--create', type=str, required=True, help='The path to create the template file.')
|
|
10
|
+
parser.add_argument('--minimal', action='store_true', help='Generate a minimal template without docstrings.')
|
|
11
|
+
args = parser.parse_args()
|
|
12
|
+
|
|
13
|
+
cached_properties = {}
|
|
14
|
+
methods = {}
|
|
15
|
+
defaults = {
|
|
16
|
+
'scheduler': 'return None',
|
|
17
|
+
'metrics': 'return {}',
|
|
18
|
+
'postprocess': 'return outputs',
|
|
19
|
+
'preprocess': 'return data',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for name, member in inspect.getmembers(TorchABC):
|
|
23
|
+
if hasattr(member, '__isabstractmethod__'):
|
|
24
|
+
if isinstance(TorchABC.__dict__.get(name), cached_property):
|
|
25
|
+
cached_properties[name] = member.__doc__ or ""
|
|
26
|
+
elif callable(member):
|
|
27
|
+
sig = inspect.signature(member)
|
|
28
|
+
methods[name] = (sig, member.__doc__ or "")
|
|
29
|
+
|
|
30
|
+
template = """
|
|
31
|
+
import torch
|
|
32
|
+
from torchabc import TorchABC
|
|
33
|
+
from functools import cached_property
|
|
34
|
+
from typing import Any, Dict
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ClassName(TorchABC):"""
|
|
38
|
+
|
|
39
|
+
if not args.minimal:
|
|
40
|
+
template += """
|
|
41
|
+
\"\"\"A concrete implementation of the TorchABC abstract class.
|
|
42
|
+
|
|
43
|
+
Use this template to implement your own model by following these steps:
|
|
44
|
+
- replace ClassName with the name of your model,
|
|
45
|
+
- replace this docstring with a description of your model,
|
|
46
|
+
- implement the methods below to define the core logic of your model,
|
|
47
|
+
- access the hyperparameters passed during initialization with `self.hparams`.
|
|
48
|
+
\"\"\""""
|
|
49
|
+
|
|
50
|
+
template += """
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
for name in ('dataloaders', 'preprocess', 'network', 'optimizer', 'scheduler', 'loss', 'metrics', 'postprocess'):
|
|
54
|
+
if name in cached_properties:
|
|
55
|
+
doc = cached_properties[name]
|
|
56
|
+
template += f"""
|
|
57
|
+
@cached_property
|
|
58
|
+
def {name}(self):"""
|
|
59
|
+
if not args.minimal:
|
|
60
|
+
template += f"""
|
|
61
|
+
\"\"\"{doc}\"\"\""""
|
|
62
|
+
template += f"""
|
|
63
|
+
{defaults[name] if name in defaults else 'raise NotImplementedError'}
|
|
64
|
+
"""
|
|
65
|
+
elif name in methods:
|
|
66
|
+
sig, doc = methods[name]
|
|
67
|
+
template += f"""
|
|
68
|
+
def {name}{sig}:"""
|
|
69
|
+
if not args.minimal:
|
|
70
|
+
template += f"""
|
|
71
|
+
\"\"\"{doc}\"\"\""""
|
|
72
|
+
template += f"""
|
|
73
|
+
{defaults[name] if name in defaults else 'raise NotImplementedError'}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if not args.minimal:
|
|
77
|
+
template += f"""
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
# Example usage
|
|
81
|
+
model = ClassName()
|
|
82
|
+
model.train(epochs=1)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
with open(args.create, "x") as f:
|
|
86
|
+
f.write(template.lstrip())
|
|
87
|
+
|
|
88
|
+
print(f"Template generated at: {args.create}")
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Emanuele Guidotti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: torchabc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal abstract class for training, evaluation, and inference of pytorch models.
|
|
5
|
+
Home-page: https://github.com/eguidotti/torchabc
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Emanuele Guidotti
|
|
8
|
+
Author-email: emanuele.guidotti@usi.ch
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Dist: torch (>=2.6.0,<3.0.0)
|
|
16
|
+
Project-URL: Repository, https://github.com/eguidotti/torchabc
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# TorchABC
|
|
20
|
+
|
|
21
|
+
[`TorchABC`](https://github.com/eguidotti/torchabc) is a minimal abstract class for training, evaluation, and inference of pytorch models that helps you keep your code organized. It depends on [`torch`](https://pypi.org/project/torch/) only and it consists of a simple self-contained [file](https://github.com/eguidotti/torchabc/blob/main/torchabc/__init__.py).
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
`TorchABC` implements the workflow illustrated above. The workflow begins with raw **DATA**, which undergoes a **preprocess** step. This preprocessing step transforms the raw data into **INPUT** samples and their corresponding **TARGET** labels.
|
|
28
|
+
|
|
29
|
+
Next, the individual **INPUT** samples are grouped into batches called **INPUTS** using a **collate** function. Similarly, the **TARGET** labels are batched into **TARGETS**. The **INPUTS** are then fed into the neural **network**, which produces **OUTPUTS**.
|
|
30
|
+
|
|
31
|
+
The **OUTPUTS** of the network are compared to the **TARGETS** using a **LOSS** function. The calculated loss quantifies the error between the model's predictions and the actual targets. This **LOSS** is then used by the **optimizer / scheduler** to update the parameters of the **network** to minimize the loss. The optimizer controls how the parameters are adjusted, while the scheduler can dynamically change the learning rate of the optimizer during training.
|
|
32
|
+
|
|
33
|
+
Finally, the raw **OUTPUTS** from the network undergo a **postprocess** step to generate the final **PREDICTIONS**. This could involve converting probabilities to class labels, applying thresholds, or other task-specific transformations.
|
|
34
|
+
|
|
35
|
+
**The core logic blocks** are abstract. You define their specific behavior with maximum flexibility.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
Install the package.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install torchabc
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Generate a template using the command line interface.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
torchabc --create template.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The template is structured as follows.
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
class ClassName(TorchABC):
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def dataloaders(self):
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def preprocess(self, data: Any, flag: str = '') -> Any:
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def network(self):
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def optimizer(self):
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
@cached_property
|
|
72
|
+
def scheduler(self):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def loss(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def metrics(self, outputs: torch.Tensor, targets: torch.Tensor) -> Dict[str, float]:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
def postprocess(self, outputs: torch.Tensor) -> Any:
|
|
82
|
+
return outputs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Fill out the template with the dataloaders, preprocessing and postprocessing steps, the neural network, optimizer, scheduler, loss, and evaluation metrics.
|
|
86
|
+
|
|
87
|
+
#### `dataloaders`: the dataloaders for training and evaluation
|
|
88
|
+
|
|
89
|
+
This method defines and returns a dictionary containing the [`DataLoader`](https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader) instances for the training, validation, and testing datasets. The dictionary's keys should correspond to the names of the datasets (e.g., 'train', 'val', 'test'), and the values should be their respective `DataLoader` objects. Any transformation of the raw input data for each dataset should be implemented within the `preprocess` method of this class. The `preprocess` method should then be passed as the `transform` argument of the [`Dataset`](https://pytorch.org/docs/stable/data.html#torch.utils.data.Dataset) instances.
|
|
90
|
+
|
|
91
|
+
#### `preprocess`: preprocessing
|
|
92
|
+
|
|
93
|
+
This method processes the data differently depending on a `flag`. When `flag` is empty (the default), the data are assumed to represent the model's input used for inference. When `flag` has a specific value, the method may perform different preprocessing steps, such as transforming the target or augmenting the input for training.
|
|
94
|
+
|
|
95
|
+
#### `network`: the neural network
|
|
96
|
+
|
|
97
|
+
Returns a [`Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) whose input and output tensors assume the batch size is the first dimension: (batch_size, ...).
|
|
98
|
+
|
|
99
|
+
#### `optimizer`: the optimizer for training the network
|
|
100
|
+
|
|
101
|
+
Returns an [`Optimizer`](https://pytorch.org/docs/main/optim.html#torch.optim.Optimizer) configured for the `network`.
|
|
102
|
+
|
|
103
|
+
#### `scheduler`: the learning rate scheduler for the optimizer
|
|
104
|
+
|
|
105
|
+
Returns a [`LRScheduler`](https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.LRScheduler.html) or [`ReduceLROnPlateau`](https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.ReduceLROnPlateau.html) configured for the `optimizer`.
|
|
106
|
+
|
|
107
|
+
#### `loss`: loss function
|
|
108
|
+
|
|
109
|
+
This method defines the loss function that quantifies the discrepancy between the neural network `outputs` and the corresponding `targets`. The loss function should be differentiable to enable backpropagation.
|
|
110
|
+
|
|
111
|
+
#### `metrics`: evaluation metrics
|
|
112
|
+
|
|
113
|
+
This method calculates various metrics that quantify the discrepancy between the neural network `outputs` and the corresponding `targets`. Unlike `loss`, which is primarily used for training, these metrics are only used for evaluation and do not need to be differentiable.
|
|
114
|
+
|
|
115
|
+
#### `postprocess`: postprocess
|
|
116
|
+
|
|
117
|
+
This method transforms the neural network outputs to generate the final predictions.
|
|
118
|
+
|
|
119
|
+
## Usage
|
|
120
|
+
|
|
121
|
+
After filling out the template above, you can use your class as follows.
|
|
122
|
+
|
|
123
|
+
### Initialization
|
|
124
|
+
|
|
125
|
+
Initialize the class with
|
|
126
|
+
|
|
127
|
+
```py
|
|
128
|
+
model = ClassName(
|
|
129
|
+
device: Union[str, torch.device] = None,
|
|
130
|
+
logger: Callable = print,
|
|
131
|
+
**hparams
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Device
|
|
136
|
+
|
|
137
|
+
The `device` is the [`torch.device`](https://pytorch.org/docs/stable/tensor_attributes.html#torch.device) to use. Defaults to `None`, which will try CUDA, then MPS, and finally fall back to CPU.
|
|
138
|
+
|
|
139
|
+
#### Logger
|
|
140
|
+
|
|
141
|
+
A logging function that takes a dictionary in input. The default prints to standard output. You can can easily log with [wandb](https://pypi.org/project/wandb/)
|
|
142
|
+
|
|
143
|
+
```py
|
|
144
|
+
import wandb
|
|
145
|
+
model = ClassName(logger=wandb.log)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
or with any other custom logger.
|
|
149
|
+
|
|
150
|
+
#### Hyperparameters
|
|
151
|
+
|
|
152
|
+
You will typically use several parameters to control the behavior of `ClassName`, such as the learning rate or batch size. These parameters should be passed during the initialization
|
|
153
|
+
|
|
154
|
+
```py
|
|
155
|
+
model = ClassName(lr=0.001, batch_size=64)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
and are stored in the attribute `hparams` of the model. For instance, use `hparams.lr` to access the `lr` value.
|
|
159
|
+
|
|
160
|
+
### Training
|
|
161
|
+
|
|
162
|
+
Train the model with
|
|
163
|
+
|
|
164
|
+
```py
|
|
165
|
+
model.train(
|
|
166
|
+
epochs: int,
|
|
167
|
+
on: str = 'train',
|
|
168
|
+
val: str = 'val',
|
|
169
|
+
gas: int = 1,
|
|
170
|
+
callback: Callable = None
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
where
|
|
175
|
+
|
|
176
|
+
- `epochs` is the number of training epochs to perform.
|
|
177
|
+
- `on` is the name of the training dataloader. Defaults to 'train'.
|
|
178
|
+
- `val` is the name of the validation dataloader. Defaults to 'val'.
|
|
179
|
+
- `gas` is the number of gradient accumulation steps. Defaults to 1 (no gradient accumulation).
|
|
180
|
+
- `callback` is a function that is called after each epoch. It should accept two arguments: the instance itself and a list of dictionaries containing the loss and evaluation metrics. When this function returns `True`, training stops.
|
|
181
|
+
|
|
182
|
+
This method returns a list of dictionaries containing the loss and evaluation metrics.
|
|
183
|
+
|
|
184
|
+
### Checkpoints
|
|
185
|
+
|
|
186
|
+
Save the model to a checkpoint.
|
|
187
|
+
|
|
188
|
+
```py
|
|
189
|
+
model.save("checkpoint.pth")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Load the model from a checkpoint.
|
|
193
|
+
|
|
194
|
+
```py
|
|
195
|
+
model.load("checkpoint.pth")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
You can also use the `callback` function to implement a custom checkpointing strategy. For instance, the following example saves a checkpoint after each training epoch.
|
|
199
|
+
|
|
200
|
+
```py
|
|
201
|
+
callback = lambda self, logs: self.save(f"epoch_{logs[-1]['val/epoch']}.pth")
|
|
202
|
+
model.train(epochs=10, val='val', callback=callback)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Evaluation
|
|
206
|
+
|
|
207
|
+
Evaluate the model with
|
|
208
|
+
|
|
209
|
+
```py
|
|
210
|
+
model.eval(on='test')
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
where `on` is the name of the dataloader to evaluate on. This should be one of the keys in the `dataloaders`. This method returns a dictionary containing the evaluation metrics.
|
|
214
|
+
|
|
215
|
+
### Inference
|
|
216
|
+
|
|
217
|
+
Predict raw data with
|
|
218
|
+
|
|
219
|
+
```py
|
|
220
|
+
model.predict(data)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
where `data` is the raw input data. This method returns the postprocessed prediction.
|
|
224
|
+
|
|
225
|
+
## Examples
|
|
226
|
+
|
|
227
|
+
Get started with simple self-contained examples:
|
|
228
|
+
|
|
229
|
+
- [MNIST classification](https://github.com/eguidotti/torchabc/blob/main/examples/mnist.py)
|
|
230
|
+
|
|
231
|
+
## Contribute
|
|
232
|
+
|
|
233
|
+
Contributions are welcome! Submit pull requests with new [examples](https://github.com/eguidotti/torchabc/tree/main/examples) or improvements to the core [`TorchABC`](https://github.com/eguidotti/torchabc/blob/main/torchabc/__init__.py) class itself.
|
|
234
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
torchabc/__init__.py,sha256=bkJ7wt6jKM5nOvAVSE1KKRzOk3HqMBX57G2eBK5Jezk,15994
|
|
2
|
+
torchabc/cli.py,sha256=RRvhivA2JhOHnxy_TG3wAT9W8ar6n32a-OeDokKFcT4,2850
|
|
3
|
+
torchabc-0.1.0.dist-info/LICENSE,sha256=DUdVdCoS4OrUDYtUZHIVx1gX82IqTif3uII-1m5IlIg,1074
|
|
4
|
+
torchabc-0.1.0.dist-info/METADATA,sha256=d530_tiQiYPmsOjlyG4QnqoLhXusWxuoHVFxFxiunlQ,9329
|
|
5
|
+
torchabc-0.1.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
6
|
+
torchabc-0.1.0.dist-info/entry_points.txt,sha256=57CzcZChCA8OC9RaYv9LEcLakXplqJWLXvxvDKDU0NQ,46
|
|
7
|
+
torchabc-0.1.0.dist-info/RECORD,,
|