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 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,7 @@
1
+ from abc import ABC, abstractmethod
2
+ from ..pipelines.pipeline import Pipeline
3
+
4
+
5
+ class Callback(ABC):
6
+ @abstractmethod
7
+ def __call__(self, pipeline: Pipeline) -> None: ...
@@ -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()