frogbox 0.5.3__tar.gz

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.
Files changed (44) hide show
  1. frogbox-0.5.3/LICENSE +21 -0
  2. frogbox-0.5.3/MANIFEST.in +1 -0
  3. frogbox-0.5.3/PKG-INFO +48 -0
  4. frogbox-0.5.3/README.md +26 -0
  5. frogbox-0.5.3/frogbox/__init__.py +12 -0
  6. frogbox-0.5.3/frogbox/callbacks/__init__.py +22 -0
  7. frogbox-0.5.3/frogbox/callbacks/callback.py +7 -0
  8. frogbox-0.5.3/frogbox/callbacks/image_logger.py +231 -0
  9. frogbox-0.5.3/frogbox/cli.py +147 -0
  10. frogbox-0.5.3/frogbox/config.py +332 -0
  11. frogbox-0.5.3/frogbox/data/__init__.py +1 -0
  12. frogbox-0.5.3/frogbox/data/dataset.py +12 -0
  13. frogbox-0.5.3/frogbox/data/model.py +9 -0
  14. frogbox-0.5.3/frogbox/data/train_supervised.py +42 -0
  15. frogbox-0.5.3/frogbox/engines/__init__.py +0 -0
  16. frogbox-0.5.3/frogbox/engines/engine.py +170 -0
  17. frogbox-0.5.3/frogbox/engines/events.py +64 -0
  18. frogbox-0.5.3/frogbox/engines/gan.py +125 -0
  19. frogbox-0.5.3/frogbox/engines/supervised.py +106 -0
  20. frogbox-0.5.3/frogbox/handlers/__init__.py +0 -0
  21. frogbox-0.5.3/frogbox/handlers/checkpoint.py +165 -0
  22. frogbox-0.5.3/frogbox/handlers/composite_loss_logger.py +30 -0
  23. frogbox-0.5.3/frogbox/handlers/metric_logger.py +43 -0
  24. frogbox-0.5.3/frogbox/handlers/optimizer_logger.py +37 -0
  25. frogbox-0.5.3/frogbox/handlers/output_logger.py +21 -0
  26. frogbox-0.5.3/frogbox/metrics/__init__.py +4 -0
  27. frogbox-0.5.3/frogbox/pipelines/__init__.py +14 -0
  28. frogbox-0.5.3/frogbox/pipelines/composite_loss.py +50 -0
  29. frogbox-0.5.3/frogbox/pipelines/gan.py +366 -0
  30. frogbox-0.5.3/frogbox/pipelines/lr_scheduler.py +45 -0
  31. frogbox-0.5.3/frogbox/pipelines/name_generation.py +457 -0
  32. frogbox-0.5.3/frogbox/pipelines/pipeline.py +234 -0
  33. frogbox-0.5.3/frogbox/pipelines/supervised.py +283 -0
  34. frogbox-0.5.3/frogbox/utils.py +119 -0
  35. frogbox-0.5.3/frogbox.egg-info/PKG-INFO +48 -0
  36. frogbox-0.5.3/frogbox.egg-info/SOURCES.txt +42 -0
  37. frogbox-0.5.3/frogbox.egg-info/dependency_links.txt +1 -0
  38. frogbox-0.5.3/frogbox.egg-info/entry_points.txt +2 -0
  39. frogbox-0.5.3/frogbox.egg-info/requires.txt +9 -0
  40. frogbox-0.5.3/frogbox.egg-info/top_level.txt +1 -0
  41. frogbox-0.5.3/pyproject.toml +36 -0
  42. frogbox-0.5.3/setup.cfg +4 -0
  43. frogbox-0.5.3/tests/test_config.py +12 -0
  44. frogbox-0.5.3/tests/test_package.py +12 -0
frogbox-0.5.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Simon Larsen
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 @@
1
+ include frogbox/data/*.py
frogbox-0.5.3/PKG-INFO ADDED
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: frogbox
3
+ Version: 0.5.3
4
+ Summary: An opinionated machine learning framework
5
+ Author-email: "Simon J. Larsen" <simonhffh@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://hisimon.dk/frogbox
8
+ Project-URL: Issues, https://github.com/SimonLarsen/frogbox/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: torch>=2.5.1
13
+ Requires-Dist: torchvision>=0.20.1
14
+ Requires-Dist: accelerate>=1.2.0
15
+ Requires-Dist: torchmetrics>=1.6.0
16
+ Requires-Dist: pydantic>=2.10.3
17
+ Requires-Dist: tqdm>=4.67.1
18
+ Requires-Dist: wandb>=0.18.7
19
+ Requires-Dist: jinja2>=3.1.4
20
+ Requires-Dist: click>=8.1.7
21
+ Dynamic: license-file
22
+
23
+ <p align="center">
24
+ <img src="https://simonlarsen.github.io/frogbox/logo.png" width="224">
25
+ </p>
26
+ <h1 align="center">
27
+ frogbox
28
+ </h1>
29
+
30
+ Frogbox is an opinionated PyTorch machine learning framework built for rapid prototyping and research.
31
+
32
+ ## Features
33
+
34
+ * Experiments are defined using JSON files and support [jinja2](https://jinja.palletsprojects.com) templates.
35
+ * Flexible event system inspired by [Ignite](https://pytorch.org/ignite).
36
+ * Automatic experiment tracking. Currently only [Weights & Biases](https://wandb.ai/) is supported with other platforms planned.
37
+ * CLI tool for easy project management. Just type `frogbox project new -t supervised` to get started.
38
+ * Integrates [Accelerate](https://huggingface.co/docs/accelerate/index) to support automatic mixed precision (AMP) and distributed training.
39
+
40
+ ## Installation
41
+
42
+ ```sh
43
+ pip install git+https://SimonLarsen@github.com/SimonLarsen/frogbox.git@v0.5.3
44
+ ```
45
+
46
+ ## Getting started
47
+
48
+ See [documentation](https://simonlarsen.github.io/frogbox).
@@ -0,0 +1,26 @@
1
+ <p align="center">
2
+ <img src="https://simonlarsen.github.io/frogbox/logo.png" width="224">
3
+ </p>
4
+ <h1 align="center">
5
+ frogbox
6
+ </h1>
7
+
8
+ Frogbox is an opinionated PyTorch machine learning framework built for rapid prototyping and research.
9
+
10
+ ## Features
11
+
12
+ * Experiments are defined using JSON files and support [jinja2](https://jinja.palletsprojects.com) templates.
13
+ * Flexible event system inspired by [Ignite](https://pytorch.org/ignite).
14
+ * Automatic experiment tracking. Currently only [Weights & Biases](https://wandb.ai/) is supported with other platforms planned.
15
+ * CLI tool for easy project management. Just type `frogbox project new -t supervised` to get started.
16
+ * Integrates [Accelerate](https://huggingface.co/docs/accelerate/index) to support automatic mixed precision (AMP) and distributed training.
17
+
18
+ ## Installation
19
+
20
+ ```sh
21
+ pip install git+https://SimonLarsen@github.com/SimonLarsen/frogbox.git@v0.5.3
22
+ ```
23
+
24
+ ## Getting started
25
+
26
+ See [documentation](https://simonlarsen.github.io/frogbox).
@@ -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
+ )
@@ -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()