frogbox 0.5.3__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.
- frogbox/__init__.py +12 -0
- frogbox/callbacks/__init__.py +22 -0
- frogbox/callbacks/callback.py +7 -0
- frogbox/callbacks/image_logger.py +231 -0
- frogbox/cli.py +147 -0
- frogbox/config.py +332 -0
- frogbox/data/__init__.py +1 -0
- frogbox/data/dataset.py +12 -0
- frogbox/data/model.py +9 -0
- frogbox/data/train_supervised.py +42 -0
- frogbox/engines/__init__.py +0 -0
- frogbox/engines/engine.py +170 -0
- frogbox/engines/events.py +64 -0
- frogbox/engines/gan.py +125 -0
- frogbox/engines/supervised.py +106 -0
- frogbox/handlers/__init__.py +0 -0
- frogbox/handlers/checkpoint.py +165 -0
- frogbox/handlers/composite_loss_logger.py +30 -0
- frogbox/handlers/metric_logger.py +43 -0
- frogbox/handlers/optimizer_logger.py +37 -0
- frogbox/handlers/output_logger.py +21 -0
- frogbox/metrics/__init__.py +4 -0
- frogbox/pipelines/__init__.py +14 -0
- frogbox/pipelines/composite_loss.py +50 -0
- frogbox/pipelines/gan.py +366 -0
- frogbox/pipelines/lr_scheduler.py +45 -0
- frogbox/pipelines/name_generation.py +457 -0
- frogbox/pipelines/pipeline.py +234 -0
- frogbox/pipelines/supervised.py +283 -0
- frogbox/utils.py +119 -0
- frogbox-0.5.3.dist-info/METADATA +48 -0
- frogbox-0.5.3.dist-info/RECORD +36 -0
- frogbox-0.5.3.dist-info/WHEEL +5 -0
- frogbox-0.5.3.dist-info/entry_points.txt +2 -0
- frogbox-0.5.3.dist-info/licenses/LICENSE +21 -0
- frogbox-0.5.3.dist-info/top_level.txt +1 -0
frogbox/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. include:: ./intro.md
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.5.3"
|
|
6
|
+
|
|
7
|
+
from accelerate.utils import set_seed # noqa: F401
|
|
8
|
+
from .config import read_json_config, SupervisedConfig, GANConfig # noqa: F401
|
|
9
|
+
from .engines.events import Event # noqa: F401
|
|
10
|
+
from .pipelines.supervised import SupervisedPipeline # noqa: F401
|
|
11
|
+
from .pipelines.gan import GANPipeline # noqa: F401
|
|
12
|
+
from .utils import load_model_checkpoint # noqa: F401
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Custom callbacks
|
|
3
|
+
|
|
4
|
+
Custom callbacks can be created by implementing a function that accepts the pipeline as its first argument.
|
|
5
|
+
|
|
6
|
+
For instance, in the following example a callback is added to unfreeze the model's encoder after 20 epochs:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from frogbox import Event
|
|
10
|
+
|
|
11
|
+
def unfreeze_encoder(pipeline)
|
|
12
|
+
model = pipeline.model
|
|
13
|
+
model.encoder.requires_grad_(True)
|
|
14
|
+
|
|
15
|
+
pipeline.install_callback(
|
|
16
|
+
event=Event("epoch_started", first=20, last=20),
|
|
17
|
+
callback=unfreeze_encoder,
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
""" # noqa: E501
|
|
21
|
+
|
|
22
|
+
from .image_logger import ImageLogger # noqa: F401
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Logging images
|
|
3
|
+
|
|
4
|
+
The simplest way to log images during training is to create a callback with
|
|
5
|
+
`frogbox.callbacks.image_logger.ImageLogger`:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from frogbox import Events
|
|
9
|
+
from frogbox.callbacks import ImageLogger
|
|
10
|
+
|
|
11
|
+
image_logger = ImageLogger()
|
|
12
|
+
|
|
13
|
+
pipeline.install_callback(
|
|
14
|
+
event="epoch_completed",
|
|
15
|
+
callback=image_logger,
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Images can automatically be denormalized by setting `denormalize_input`/`denormalize_output`
|
|
20
|
+
and providing the mean and standard deviation used for normalization.
|
|
21
|
+
|
|
22
|
+
For instance, if input images are normalized with ImageNet parameters and outputs are in [0, 1]:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
image_logger = ImageLogger(
|
|
26
|
+
normalize_mean=[0.485, 0.456, 0.406],
|
|
27
|
+
normalize_std=[0.229, 0.224, 0.225],
|
|
28
|
+
denormalize_input=True,
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
More advanced transformations can be made by overriding `input_transform`, `model_transform`, or `output_transform`:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from torchvision.transforms.functional import hflip
|
|
36
|
+
|
|
37
|
+
def flip_input(x, y, y_pred):
|
|
38
|
+
x = hflip(x)
|
|
39
|
+
return x, y_pred, y
|
|
40
|
+
|
|
41
|
+
image_logger = ImageLogger(
|
|
42
|
+
output_transform=flip_input,
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
""" # noqa: E501
|
|
46
|
+
|
|
47
|
+
from typing import Sequence, Callable, Any, Optional
|
|
48
|
+
import torch
|
|
49
|
+
from torchvision.transforms.functional import (
|
|
50
|
+
center_crop,
|
|
51
|
+
resize,
|
|
52
|
+
InterpolationMode,
|
|
53
|
+
to_pil_image,
|
|
54
|
+
)
|
|
55
|
+
from torchvision.utils import make_grid
|
|
56
|
+
import tqdm
|
|
57
|
+
import wandb
|
|
58
|
+
from .callback import Callback
|
|
59
|
+
from ..pipelines.pipeline import Pipeline
|
|
60
|
+
from ..utils import convert_tensor
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ImageLogger(Callback):
|
|
64
|
+
"""Callback for logging images."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
split: str = "test",
|
|
69
|
+
log_label: str = "test/images",
|
|
70
|
+
resize_to_fit: bool = True,
|
|
71
|
+
interpolation: str | InterpolationMode = "nearest",
|
|
72
|
+
num_cols: Optional[int] = None,
|
|
73
|
+
denormalize_input: bool = False,
|
|
74
|
+
denormalize_target: bool = False,
|
|
75
|
+
normalize_mean: Sequence[float] = (0.0, 0.0, 0.0),
|
|
76
|
+
normalize_std: Sequence[float] = (1.0, 1.0, 1.0),
|
|
77
|
+
show_progress: bool = False,
|
|
78
|
+
input_transform: Callable[[Any, Any], Any] = lambda x, y: (x, y),
|
|
79
|
+
model_transform: Callable[[Any], Any] = lambda output: output,
|
|
80
|
+
output_transform: Callable[
|
|
81
|
+
[Any, Any, Any], Any
|
|
82
|
+
] = lambda x, y, y_pred: (x, y_pred, y),
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Create ImageLogger.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
split : str
|
|
90
|
+
Dataset split to evaluate on. Defaults to "test".
|
|
91
|
+
log_label : str
|
|
92
|
+
Label to log images under in Weights & Biases.
|
|
93
|
+
resize_to_fit : bool
|
|
94
|
+
If `true` smaller images are resized to fit canvas.
|
|
95
|
+
interpolation : torchvision.transforms.functional.InterpolationMode
|
|
96
|
+
Interpolation to use for resizing images.
|
|
97
|
+
num_cols : int
|
|
98
|
+
Number of columns in image grid.
|
|
99
|
+
Defaults to number of elements in returned tuple.
|
|
100
|
+
denormalize_input : bool
|
|
101
|
+
If `true` input images (x) a denormalized after inference.
|
|
102
|
+
denormalize_target : bool
|
|
103
|
+
If `true` target images (y and y_pred) are denormalized after
|
|
104
|
+
inference.
|
|
105
|
+
normalize_mean : (float, float, float)
|
|
106
|
+
RGB mean values used in image normalization.
|
|
107
|
+
normalize_std : (float, float, float)
|
|
108
|
+
RGB std.dev. values used in image normalization.
|
|
109
|
+
show_progress : bool
|
|
110
|
+
Show progress bar.
|
|
111
|
+
input_transform : Callable
|
|
112
|
+
Function that receives tensors `y` and `y` and outputs tuple of
|
|
113
|
+
tensors `(x, y)`.
|
|
114
|
+
model_transform : Callable
|
|
115
|
+
Function that receives the output from the model during evaluation
|
|
116
|
+
and converts it into the predictions:
|
|
117
|
+
`y_pred = model_transform(model(x))`.
|
|
118
|
+
output_transform : Callable
|
|
119
|
+
Function that receives `x`, `y`, `y_pred` and returns tensors to be
|
|
120
|
+
logged as images. Default is returning `(x, y_pred, y)`.
|
|
121
|
+
"""
|
|
122
|
+
self._split = split
|
|
123
|
+
self._log_label = log_label
|
|
124
|
+
self._resize_to_fit = resize_to_fit
|
|
125
|
+
self._interpolation = InterpolationMode(interpolation)
|
|
126
|
+
self._num_cols = num_cols
|
|
127
|
+
self._denormalize_input = denormalize_input
|
|
128
|
+
self._denormalize_target = denormalize_target
|
|
129
|
+
self._normalize_mean = normalize_mean
|
|
130
|
+
self._normalize_std = normalize_std
|
|
131
|
+
self._show_progress = show_progress
|
|
132
|
+
self._input_transform = input_transform
|
|
133
|
+
self._model_transform = model_transform
|
|
134
|
+
self._output_transform = output_transform
|
|
135
|
+
|
|
136
|
+
def _denormalize(self, x: torch.Tensor) -> torch.Tensor:
|
|
137
|
+
mean = torch.as_tensor(
|
|
138
|
+
self._normalize_mean, device=x.device, dtype=x.dtype
|
|
139
|
+
).reshape(1, -1, 1, 1)
|
|
140
|
+
|
|
141
|
+
std = torch.as_tensor(
|
|
142
|
+
self._normalize_std, device=x.device, dtype=x.dtype
|
|
143
|
+
).reshape(1, -1, 1, 1)
|
|
144
|
+
|
|
145
|
+
return (x * std) + mean
|
|
146
|
+
|
|
147
|
+
def __call__(self, pipeline: Pipeline) -> None:
|
|
148
|
+
if not hasattr(pipeline, "model") or not hasattr(pipeline, "loaders"):
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
f"ImageLogger not compatible with pipeline {pipeline}."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
model = pipeline.model
|
|
154
|
+
loaders = pipeline.loaders
|
|
155
|
+
accelerator = pipeline.accelerator
|
|
156
|
+
|
|
157
|
+
model.eval()
|
|
158
|
+
|
|
159
|
+
data_iter = loaders[self._split]
|
|
160
|
+
if self._show_progress:
|
|
161
|
+
data_iter = tqdm.tqdm(
|
|
162
|
+
data_iter,
|
|
163
|
+
desc="Images",
|
|
164
|
+
ncols=80,
|
|
165
|
+
leave=False,
|
|
166
|
+
total=len(data_iter),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
images = []
|
|
170
|
+
for batch in data_iter:
|
|
171
|
+
x, y = batch
|
|
172
|
+
x, y = self._input_transform(x, y)
|
|
173
|
+
|
|
174
|
+
with torch.inference_mode():
|
|
175
|
+
y_pred = self._model_transform(model(x))
|
|
176
|
+
|
|
177
|
+
x, y, y_pred = accelerator.gather_for_metrics((x, y, y_pred))
|
|
178
|
+
|
|
179
|
+
x = convert_tensor(x, device=torch.device("cpu"))
|
|
180
|
+
y = convert_tensor(y, device=torch.device("cpu"))
|
|
181
|
+
y_pred = convert_tensor(y_pred, device=torch.device("cpu"))
|
|
182
|
+
|
|
183
|
+
if self._denormalize_input:
|
|
184
|
+
x = self._denormalize(x)
|
|
185
|
+
if self._denormalize_target:
|
|
186
|
+
y = self._denormalize(y)
|
|
187
|
+
y_pred = self._denormalize(y_pred)
|
|
188
|
+
|
|
189
|
+
output = self._output_transform(x, y, y_pred)
|
|
190
|
+
|
|
191
|
+
batch_sizes = [len(e) for e in output]
|
|
192
|
+
assert all(s == batch_sizes[0] for s in batch_sizes)
|
|
193
|
+
for i in range(batch_sizes[0]):
|
|
194
|
+
grid = self._combine_test_images([e[i] for e in output])
|
|
195
|
+
images.append(grid)
|
|
196
|
+
|
|
197
|
+
wandb_images = [wandb.Image(to_pil_image(image)) for image in images]
|
|
198
|
+
pipeline.log({self._log_label: wandb_images})
|
|
199
|
+
|
|
200
|
+
def _combine_test_images(
|
|
201
|
+
self, images: Sequence[torch.Tensor]
|
|
202
|
+
) -> torch.Tensor:
|
|
203
|
+
for image in images:
|
|
204
|
+
assert len(image.shape) == 3
|
|
205
|
+
assert image.size(0) in (1, 3)
|
|
206
|
+
|
|
207
|
+
max_h = max(image.size(1) for image in images)
|
|
208
|
+
max_w = max(image.size(2) for image in images)
|
|
209
|
+
|
|
210
|
+
transformed = []
|
|
211
|
+
for image in images:
|
|
212
|
+
c, h, w = image.shape
|
|
213
|
+
if (h, w) != (max_h, max_w):
|
|
214
|
+
if self._resize_to_fit:
|
|
215
|
+
image = resize(
|
|
216
|
+
image,
|
|
217
|
+
size=(max_h, max_w),
|
|
218
|
+
interpolation=self._interpolation,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
image = center_crop(image, output_size=(max_h, max_w))
|
|
222
|
+
if c == 1:
|
|
223
|
+
image = image.repeat((3, 1, 1))
|
|
224
|
+
image = image.clamp(0.0, 1.0)
|
|
225
|
+
transformed.append(image)
|
|
226
|
+
|
|
227
|
+
return make_grid(
|
|
228
|
+
tensor=transformed,
|
|
229
|
+
normalize=False,
|
|
230
|
+
nrow=self._num_cols or len(transformed),
|
|
231
|
+
)
|
frogbox/cli.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""@private"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import importlib
|
|
7
|
+
import subprocess
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cli():
|
|
13
|
+
"""
|
|
14
|
+
An opinionated machine learning framework.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.command(
|
|
19
|
+
context_settings=dict(
|
|
20
|
+
ignore_unknown_options=True,
|
|
21
|
+
help_option_names=[],
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
25
|
+
def launch(args):
|
|
26
|
+
"""
|
|
27
|
+
Launch script.
|
|
28
|
+
|
|
29
|
+
Alias for `accelerate launch`.
|
|
30
|
+
"""
|
|
31
|
+
exec_path = shutil.which("accelerate")
|
|
32
|
+
cmd = [exec_path, "launch"] + list(args)
|
|
33
|
+
env = os.environ.copy()
|
|
34
|
+
subprocess.run(cmd, env=env, check=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cli.command(
|
|
38
|
+
context_settings=dict(
|
|
39
|
+
ignore_unknown_options=True,
|
|
40
|
+
help_option_names=[],
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
44
|
+
def config(args):
|
|
45
|
+
"""
|
|
46
|
+
Configure training system.
|
|
47
|
+
|
|
48
|
+
Alias for `accelerate config`.
|
|
49
|
+
"""
|
|
50
|
+
exec_path = shutil.which("accelerate")
|
|
51
|
+
cmd = [exec_path, "config"] + list(args)
|
|
52
|
+
env = os.environ.copy()
|
|
53
|
+
subprocess.run(cmd, env=env, check=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@cli.group()
|
|
57
|
+
def project():
|
|
58
|
+
"""Manage project."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@project.command(name="new")
|
|
62
|
+
@click.option(
|
|
63
|
+
"--type",
|
|
64
|
+
"-t",
|
|
65
|
+
"type_",
|
|
66
|
+
type=click.Choice(["supervised"]),
|
|
67
|
+
default="supervised",
|
|
68
|
+
help="Pipeline type.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--dir",
|
|
72
|
+
"-d",
|
|
73
|
+
"dir_",
|
|
74
|
+
type=click.Path(
|
|
75
|
+
exists=False,
|
|
76
|
+
file_okay=False,
|
|
77
|
+
dir_okay=True,
|
|
78
|
+
path_type=Path,
|
|
79
|
+
),
|
|
80
|
+
default=Path("."),
|
|
81
|
+
help="Project root directory.",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--overwrite",
|
|
85
|
+
is_flag=True,
|
|
86
|
+
help="Overwrite existing files if present.",
|
|
87
|
+
)
|
|
88
|
+
def new_project(type_: str, dir_: Path, overwrite: bool = False):
|
|
89
|
+
"""Create a new project from template."""
|
|
90
|
+
|
|
91
|
+
template_inputs = [
|
|
92
|
+
f"train_{type_}.py",
|
|
93
|
+
"model.py",
|
|
94
|
+
"dataset.py",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
template_outputs = [
|
|
98
|
+
dir_ / "train.py",
|
|
99
|
+
dir_ / "models" / "example.py",
|
|
100
|
+
dir_ / "datasets" / "example.py",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
overwrite_checks = template_outputs + [dir_ / "configs" / "example.json"]
|
|
104
|
+
|
|
105
|
+
# Check if files already exist
|
|
106
|
+
if not overwrite:
|
|
107
|
+
for path in overwrite_checks:
|
|
108
|
+
if path.exists():
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
f"File '{path}' already exists."
|
|
111
|
+
" Use flag --overwrite to overwrite."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Create folders and copy template files
|
|
115
|
+
resource_files = importlib.resources.files("frogbox.data")
|
|
116
|
+
for input_resource, output_path in zip(template_inputs, template_outputs):
|
|
117
|
+
file_data = resource_files.joinpath(input_resource).read_text()
|
|
118
|
+
output_path.parent.mkdir(exist_ok=True, parents=True)
|
|
119
|
+
output_path.write_text(file_data)
|
|
120
|
+
|
|
121
|
+
# Create config template
|
|
122
|
+
if type_ == "supervised":
|
|
123
|
+
from .config import SupervisedConfig, ObjectDefinition
|
|
124
|
+
|
|
125
|
+
config_json = SupervisedConfig(
|
|
126
|
+
type="supervised",
|
|
127
|
+
project="example",
|
|
128
|
+
model=ObjectDefinition(class_name="models.example.ExampleModel"),
|
|
129
|
+
datasets={
|
|
130
|
+
"train": ObjectDefinition(
|
|
131
|
+
class_name="datasets.example.ExampleDataset"
|
|
132
|
+
),
|
|
133
|
+
"val": ObjectDefinition(
|
|
134
|
+
class_name="datasets.example.ExampleDataset"
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
).model_dump_json(indent=4, exclude_none=True)
|
|
138
|
+
else:
|
|
139
|
+
raise RuntimeError(f"Unknown pipeline type {type_}.")
|
|
140
|
+
|
|
141
|
+
output_path = dir_ / "configs" / "example.json"
|
|
142
|
+
output_path.parent.mkdir(exist_ok=True, parents=True)
|
|
143
|
+
output_path.write_text(config_json)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
cli()
|