eztrain 0.1.0__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.
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: uv sync
21
+ - run: uv run ruff check .
22
+ - run: uv run mypy
23
+ - run: uv run pytest
@@ -0,0 +1,30 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - run: uv build
14
+ - uses: actions/upload-artifact@v4
15
+ with:
16
+ name: dist
17
+ path: dist/
18
+
19
+ publish:
20
+ needs: build
21
+ runs-on: ubuntu-latest
22
+ environment: pypi
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/download-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
@@ -0,0 +1 @@
1
+ 3.12
eztrain-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alessio Arcara
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.
eztrain-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.5
2
+ Name: eztrain
3
+ Version: 0.1.0
4
+ Summary: Small, framework-agnostic training-loop library. The trainer companion to EzConfy.
5
+ Project-URL: Homepage, https://github.com/alessioarcara/EzTrain
6
+ Project-URL: Repository, https://github.com/alessioarcara/EzTrain
7
+ Project-URL: Issues, https://github.com/alessioarcara/EzTrain/issues
8
+ Author: Alessio Arcara
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: deep-learning,ezconfy,machine-learning,trainer,training
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: loguru>=0.7
24
+ Requires-Dist: tqdm>=4.66
25
+ Provides-Extra: wandb
26
+ Requires-Dist: wandb>=0.17; extra == 'wandb'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # EzTrain
30
+
31
+ A small, framework-agnostic training-loop library. The trainer companion to
32
+ [EzConfy](https://github.com/alessioarcara/EzConfy).
33
+
34
+ Every ML project rewrites the same trainer: a loop over epochs or updates,
35
+ periodic evaluation, callbacks, early stopping, checkpoint scheduling, metric
36
+ logging, graceful `Ctrl+C`. **EzTrain** extracts exactly that skeleton and
37
+ nothing else:
38
+
39
+ - **Core never imports torch or jax.** It abstracts the *iteration*, not the
40
+ tensor: an iteration is anything that returns a `Mapping[str, Any]` of
41
+ metrics — an epoch over a dataloader, an RL collect→update cycle, a
42
+ world-model phase.
43
+ - **Run lifecycle built in.** A run id (`<name>_<timestamp>`) is shared
44
+ between checkpoints and the experiment tracker, with three resume modes:
45
+ **FRESH** (from scratch), **CONTINUE** (same run: weights + optimizer +
46
+ tracker), **FORK** (new run seeded with old weights).
47
+ - **Composition over inheritance** where it matters: `Logger`, `Metric` and
48
+ `Checkpointer` are Protocols you inject; `Callback` hooks are dispatched
49
+ dynamically by name so trainers can invent their own hooks.
50
+ - **No hidden defaults.** No optimizer/scheduler construction, no concrete
51
+ metrics, no config system (that's EzConfy's job), no distributed magic.
52
+
53
+ Core dependencies: `tqdm`, `loguru`. That's it.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ uv add eztrain # core
59
+ uv add "eztrain[wandb]" # + Weights & Biases logger
60
+ ```
61
+
62
+ ## Supervised (epoch-based)
63
+
64
+ ```python
65
+ from eztrain import EpochTrainer, EarlyStopping, MetricCollection, WandbLogger
66
+
67
+ class MyTrainer(EpochTrainer):
68
+ def __init__(self, *, model, optimizer, **kwargs):
69
+ super().__init__(**kwargs)
70
+ self.model, self.optimizer = model, optimizer
71
+
72
+ def train_step(self, batch):
73
+ loss = ... # your forward/backward/step
74
+ return {"loss": loss.item()} # averaged over the epoch -> "train/loss"
75
+
76
+ def eval_step(self, batch):
77
+ preds, loss = ...
78
+ self.metrics.update(preds, batch.y) # your metrics, your signature
79
+ return {"loss": loss.item()} # -> "val/loss"
80
+
81
+ trainer = MyTrainer(
82
+ model=model,
83
+ optimizer=optimizer,
84
+ train_loader=train_loader,
85
+ val_loader=val_loader,
86
+ metrics=MetricCollection([MyAccuracy()]),
87
+ max_iterations=100, # epochs
88
+ eval_freq=1,
89
+ callbacks=[EarlyStopping(monitor="val/loss", patience=10)],
90
+ logger=WandbLogger(project="my-project"),
91
+ run_name="baseline",
92
+ )
93
+ trainer.fit()
94
+ ```
95
+
96
+ ## RL (update-based)
97
+
98
+ Subclass `Trainer` directly — one iteration is one update:
99
+
100
+ ```python
101
+ from eztrain import Trainer
102
+
103
+ class PPOTrainer(Trainer):
104
+ def __init__(self, *, env, agent, num_steps, **kwargs):
105
+ super().__init__(unit="update", **kwargs)
106
+ self.env, self.agent, self.num_steps = env, agent, num_steps
107
+ self.obs = env.reset()
108
+
109
+ def train_iteration(self, update):
110
+ segment, self.obs = collect_rollouts(self.env, self.agent, self.num_steps, self.obs)
111
+ advantages, returns = compute_gae(segment)
112
+ return self.agent.learn_from(segment, advantages, returns)
113
+
114
+ def evaluate(self):
115
+ return {"eval/reward": evaluate(self.agent)}
116
+
117
+ def log_step(self): # log by env steps, not updates
118
+ return self.iteration * self.num_steps * self.env.num_envs
119
+ ```
120
+
121
+ ## Resume and fork
122
+
123
+ ```python
124
+ Trainer(run_name="exp-1") # FRESH
125
+ Trainer(run_name="exp-1", resume_from="exp-1_20260530_051406") # CONTINUE
126
+ Trainer(run_name="exp-2", resume_from="exp-1_20260530_051406") # FORK
127
+ ```
128
+
129
+ `trainer.run` carries `run_id`, `run_type` and `restore_dir`. A
130
+ `CheckpointCallback` restores in `on_train_start` (setting
131
+ `trainer.start_iteration`) and saves on schedule; the checkpoint *mechanics*
132
+ live in a `Checkpointer` implementation you inject (torch/orbax
133
+ implementations ship as extras — coming next). `WandbLogger` reuses the run
134
+ id, so a CONTINUE run resumes the same wandb run.
135
+
136
+ ## Callbacks
137
+
138
+ ```python
139
+ from eztrain import Callback
140
+
141
+ class MyCallback(Callback):
142
+ def on_train_start(self, trainer): ...
143
+ def on_iteration_end(self, trainer, iteration): ...
144
+ def on_eval_end(self, trainer): ... # trainer.history has fresh metrics
145
+ def on_train_end(self, trainer): ... # always runs, even on Ctrl+C
146
+ ```
147
+
148
+ The stable surface callbacks can rely on: `trainer.run`, `trainer.iteration`,
149
+ `trainer.start_iteration` (writable), `trainer.history`,
150
+ `trainer.should_stop` (writable), `trainer.checkpointables`,
151
+ `trainer.logger`, `trainer.call_hook`. Custom hooks compose freely:
152
+ `self.call_hook("on_rollout_end", num_steps=n)` inside your trainer reaches
153
+ any callback that defines it.
154
+
155
+ ## With EzConfy
156
+
157
+ Every public class takes plain keyword arguments, so it can be instantiated
158
+ straight from YAML:
159
+
160
+ ```yaml
161
+ # schema.yaml
162
+ types:
163
+ Callback: eztrain.callbacks:Callback
164
+ Logger: eztrain.loggers:Logger
165
+ schema:
166
+ trainer:
167
+ callbacks: list[Callback]
168
+ logger: Logger
169
+ ```
170
+
171
+ ```yaml
172
+ # config.yaml
173
+ trainer:
174
+ logger:
175
+ _target_type_: eztrain.loggers:WandbLogger
176
+ _init_args_: { project: my-project, entity: me }
177
+ callbacks:
178
+ - _target_type_: eztrain.callbacks:EarlyStopping
179
+ _init_args_: { monitor: val/loss, mode: min, patience: 10 }
180
+ ```
181
+
182
+ ## What's deliberately *not* here
183
+
184
+ Concrete train steps, models, losses, optimizers/schedulers (inject your
185
+ own — a library default becomes a cage), metrics implementations, config
186
+ loading, distributed training. Keep those in your project; eztrain only
187
+ owns the loop around them.
@@ -0,0 +1,159 @@
1
+ # EzTrain
2
+
3
+ A small, framework-agnostic training-loop library. The trainer companion to
4
+ [EzConfy](https://github.com/alessioarcara/EzConfy).
5
+
6
+ Every ML project rewrites the same trainer: a loop over epochs or updates,
7
+ periodic evaluation, callbacks, early stopping, checkpoint scheduling, metric
8
+ logging, graceful `Ctrl+C`. **EzTrain** extracts exactly that skeleton and
9
+ nothing else:
10
+
11
+ - **Core never imports torch or jax.** It abstracts the *iteration*, not the
12
+ tensor: an iteration is anything that returns a `Mapping[str, Any]` of
13
+ metrics — an epoch over a dataloader, an RL collect→update cycle, a
14
+ world-model phase.
15
+ - **Run lifecycle built in.** A run id (`<name>_<timestamp>`) is shared
16
+ between checkpoints and the experiment tracker, with three resume modes:
17
+ **FRESH** (from scratch), **CONTINUE** (same run: weights + optimizer +
18
+ tracker), **FORK** (new run seeded with old weights).
19
+ - **Composition over inheritance** where it matters: `Logger`, `Metric` and
20
+ `Checkpointer` are Protocols you inject; `Callback` hooks are dispatched
21
+ dynamically by name so trainers can invent their own hooks.
22
+ - **No hidden defaults.** No optimizer/scheduler construction, no concrete
23
+ metrics, no config system (that's EzConfy's job), no distributed magic.
24
+
25
+ Core dependencies: `tqdm`, `loguru`. That's it.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ uv add eztrain # core
31
+ uv add "eztrain[wandb]" # + Weights & Biases logger
32
+ ```
33
+
34
+ ## Supervised (epoch-based)
35
+
36
+ ```python
37
+ from eztrain import EpochTrainer, EarlyStopping, MetricCollection, WandbLogger
38
+
39
+ class MyTrainer(EpochTrainer):
40
+ def __init__(self, *, model, optimizer, **kwargs):
41
+ super().__init__(**kwargs)
42
+ self.model, self.optimizer = model, optimizer
43
+
44
+ def train_step(self, batch):
45
+ loss = ... # your forward/backward/step
46
+ return {"loss": loss.item()} # averaged over the epoch -> "train/loss"
47
+
48
+ def eval_step(self, batch):
49
+ preds, loss = ...
50
+ self.metrics.update(preds, batch.y) # your metrics, your signature
51
+ return {"loss": loss.item()} # -> "val/loss"
52
+
53
+ trainer = MyTrainer(
54
+ model=model,
55
+ optimizer=optimizer,
56
+ train_loader=train_loader,
57
+ val_loader=val_loader,
58
+ metrics=MetricCollection([MyAccuracy()]),
59
+ max_iterations=100, # epochs
60
+ eval_freq=1,
61
+ callbacks=[EarlyStopping(monitor="val/loss", patience=10)],
62
+ logger=WandbLogger(project="my-project"),
63
+ run_name="baseline",
64
+ )
65
+ trainer.fit()
66
+ ```
67
+
68
+ ## RL (update-based)
69
+
70
+ Subclass `Trainer` directly — one iteration is one update:
71
+
72
+ ```python
73
+ from eztrain import Trainer
74
+
75
+ class PPOTrainer(Trainer):
76
+ def __init__(self, *, env, agent, num_steps, **kwargs):
77
+ super().__init__(unit="update", **kwargs)
78
+ self.env, self.agent, self.num_steps = env, agent, num_steps
79
+ self.obs = env.reset()
80
+
81
+ def train_iteration(self, update):
82
+ segment, self.obs = collect_rollouts(self.env, self.agent, self.num_steps, self.obs)
83
+ advantages, returns = compute_gae(segment)
84
+ return self.agent.learn_from(segment, advantages, returns)
85
+
86
+ def evaluate(self):
87
+ return {"eval/reward": evaluate(self.agent)}
88
+
89
+ def log_step(self): # log by env steps, not updates
90
+ return self.iteration * self.num_steps * self.env.num_envs
91
+ ```
92
+
93
+ ## Resume and fork
94
+
95
+ ```python
96
+ Trainer(run_name="exp-1") # FRESH
97
+ Trainer(run_name="exp-1", resume_from="exp-1_20260530_051406") # CONTINUE
98
+ Trainer(run_name="exp-2", resume_from="exp-1_20260530_051406") # FORK
99
+ ```
100
+
101
+ `trainer.run` carries `run_id`, `run_type` and `restore_dir`. A
102
+ `CheckpointCallback` restores in `on_train_start` (setting
103
+ `trainer.start_iteration`) and saves on schedule; the checkpoint *mechanics*
104
+ live in a `Checkpointer` implementation you inject (torch/orbax
105
+ implementations ship as extras — coming next). `WandbLogger` reuses the run
106
+ id, so a CONTINUE run resumes the same wandb run.
107
+
108
+ ## Callbacks
109
+
110
+ ```python
111
+ from eztrain import Callback
112
+
113
+ class MyCallback(Callback):
114
+ def on_train_start(self, trainer): ...
115
+ def on_iteration_end(self, trainer, iteration): ...
116
+ def on_eval_end(self, trainer): ... # trainer.history has fresh metrics
117
+ def on_train_end(self, trainer): ... # always runs, even on Ctrl+C
118
+ ```
119
+
120
+ The stable surface callbacks can rely on: `trainer.run`, `trainer.iteration`,
121
+ `trainer.start_iteration` (writable), `trainer.history`,
122
+ `trainer.should_stop` (writable), `trainer.checkpointables`,
123
+ `trainer.logger`, `trainer.call_hook`. Custom hooks compose freely:
124
+ `self.call_hook("on_rollout_end", num_steps=n)` inside your trainer reaches
125
+ any callback that defines it.
126
+
127
+ ## With EzConfy
128
+
129
+ Every public class takes plain keyword arguments, so it can be instantiated
130
+ straight from YAML:
131
+
132
+ ```yaml
133
+ # schema.yaml
134
+ types:
135
+ Callback: eztrain.callbacks:Callback
136
+ Logger: eztrain.loggers:Logger
137
+ schema:
138
+ trainer:
139
+ callbacks: list[Callback]
140
+ logger: Logger
141
+ ```
142
+
143
+ ```yaml
144
+ # config.yaml
145
+ trainer:
146
+ logger:
147
+ _target_type_: eztrain.loggers:WandbLogger
148
+ _init_args_: { project: my-project, entity: me }
149
+ callbacks:
150
+ - _target_type_: eztrain.callbacks:EarlyStopping
151
+ _init_args_: { monitor: val/loss, mode: min, patience: 10 }
152
+ ```
153
+
154
+ ## What's deliberately *not* here
155
+
156
+ Concrete train steps, models, losses, optimizers/schedulers (inject your
157
+ own — a library default becomes a cage), metrics implementations, config
158
+ loading, distributed training. Keep those in your project; eztrain only
159
+ owns the loop around them.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "eztrain"
3
+ version = "0.1.0"
4
+ description = "Small, framework-agnostic training-loop library. The trainer companion to EzConfy."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [{ name = "Alessio Arcara" }]
9
+ keywords = ["training", "trainer", "machine-learning", "deep-learning", "ezconfy"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Science/Research",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "tqdm>=4.66",
24
+ "loguru>=0.7",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/alessioarcara/EzTrain"
29
+ Repository = "https://github.com/alessioarcara/EzTrain"
30
+ Issues = "https://github.com/alessioarcara/EzTrain/issues"
31
+
32
+ [project.optional-dependencies]
33
+ wandb = ["wandb>=0.17"]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "ruff>=0.6",
39
+ "mypy>=1.10",
40
+ "types-tqdm",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/eztrain"]
49
+
50
+ [tool.ruff]
51
+ line-length = 88
52
+ target-version = "py310"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "UP", "B"]
56
+
57
+ [tool.mypy]
58
+ python_version = "3.10"
59
+ strict = true
60
+ files = ["src"]
61
+
62
+ [[tool.mypy.overrides]]
63
+ module = "wandb.*"
64
+ ignore_missing_imports = true
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
@@ -0,0 +1,48 @@
1
+ """eztrain: a small, framework-agnostic training-loop library.
2
+
3
+ The trainer companion to EzConfy: every public class is instantiable from
4
+ plain keyword arguments, so it can be built straight from YAML.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ from eztrain.callbacks import (
10
+ Callback,
11
+ CheckpointCallback,
12
+ EarlyStopping,
13
+ MonitorCallback,
14
+ )
15
+ from eztrain.checkpoint import Checkpointer
16
+ from eztrain.loggers import Logger, NullLogger, RecordingLogger, WandbLogger
17
+ from eztrain.media import Image, Video
18
+ from eztrain.metrics import Metric, MetricCollection
19
+ from eztrain.run import RunInfo, RunType, generate_run_id, resolve_run, run_id_base
20
+ from eztrain.trainer import EpochTrainer, Trainer
21
+
22
+ try:
23
+ __version__ = version("eztrain")
24
+ except PackageNotFoundError: # pragma: no cover - package not installed
25
+ __version__ = "0.0.0"
26
+
27
+ __all__ = [
28
+ "Callback",
29
+ "CheckpointCallback",
30
+ "Checkpointer",
31
+ "EarlyStopping",
32
+ "EpochTrainer",
33
+ "Image",
34
+ "Logger",
35
+ "Metric",
36
+ "MetricCollection",
37
+ "MonitorCallback",
38
+ "NullLogger",
39
+ "RecordingLogger",
40
+ "RunInfo",
41
+ "RunType",
42
+ "Trainer",
43
+ "Video",
44
+ "WandbLogger",
45
+ "generate_run_id",
46
+ "resolve_run",
47
+ "run_id_base",
48
+ ]
@@ -0,0 +1,125 @@
1
+ """Callbacks: observe and steer a trainer through named hooks.
2
+
3
+ ``Callback`` is a nominal base class (so config systems like EzConfy can
4
+ type a polymorphic ``list[Callback]``), but dispatch is dynamic by hook
5
+ name — see ``Trainer.call_hook`` — so callbacks may also implement custom
6
+ hooks emitted by project-specific trainers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Literal
12
+
13
+ from loguru import logger as log
14
+
15
+ from eztrain.checkpoint import Checkpointer
16
+
17
+ if TYPE_CHECKING:
18
+ from eztrain.trainer import Trainer
19
+
20
+
21
+ class Callback:
22
+ def on_train_start(self, trainer: Trainer) -> None:
23
+ pass
24
+
25
+ def on_iteration_end(self, trainer: Trainer, iteration: int) -> None:
26
+ pass
27
+
28
+ def on_eval_end(self, trainer: Trainer) -> None:
29
+ pass
30
+
31
+ def on_train_end(self, trainer: Trainer) -> None:
32
+ pass
33
+
34
+
35
+ class MonitorCallback(Callback):
36
+ """Tracks the best value of ``trainer.history[monitor]``.
37
+
38
+ Base for anything that reacts to "the metric improved" (early stopping,
39
+ best-model checkpointing). Subclasses call :meth:`improved`.
40
+ """
41
+
42
+ def __init__(self, *, monitor: str, mode: Literal["min", "max"] = "min") -> None:
43
+ self.monitor = monitor
44
+ self.mode = mode
45
+ self.best: float | None = None
46
+
47
+ def improved(self, trainer: Trainer) -> bool:
48
+ value = trainer.history.get(self.monitor)
49
+ if value is None:
50
+ log.warning("'{}' not found in trainer history; skipping.", self.monitor)
51
+ return False
52
+ try:
53
+ value = float(value)
54
+ except (TypeError, ValueError):
55
+ log.warning(
56
+ "'{}' value {!r} is not a number; skipping.", self.monitor, value
57
+ )
58
+ return False
59
+
60
+ if self.best is None or (
61
+ value < self.best if self.mode == "min" else value > self.best
62
+ ):
63
+ self.best = value
64
+ return True
65
+ return False
66
+
67
+
68
+ class EarlyStopping(MonitorCallback):
69
+ """Sets ``trainer.should_stop`` after ``patience`` evaluations without
70
+ improvement of ``monitor``."""
71
+
72
+ def __init__(
73
+ self,
74
+ *,
75
+ monitor: str,
76
+ mode: Literal["min", "max"] = "min",
77
+ patience: int = 10,
78
+ ) -> None:
79
+ super().__init__(monitor=monitor, mode=mode)
80
+ self.patience = patience
81
+ self.counter = 0
82
+
83
+ def on_eval_end(self, trainer: Trainer) -> None:
84
+ if self.improved(trainer):
85
+ self.counter = 0
86
+ return
87
+ self.counter += 1
88
+ log.info(
89
+ "No improvement in '{}' for {}/{} evaluations.",
90
+ self.monitor,
91
+ self.counter,
92
+ self.patience,
93
+ )
94
+ if self.counter >= self.patience:
95
+ log.info("Early stopping triggered.")
96
+ trainer.should_stop = True
97
+
98
+
99
+ class CheckpointCallback(Callback):
100
+ """Checkpoint *schedule*: restore on train start, save every
101
+ ``save_freq`` iterations and once more at the end if needed.
102
+
103
+ The *mechanics* (file formats, best/latest policies, what CONTINUE vs
104
+ FORK restores) belong to the injected
105
+ :class:`~eztrain.checkpoint.Checkpointer`.
106
+ """
107
+
108
+ def __init__(self, *, checkpointer: Checkpointer, save_freq: int = 1) -> None:
109
+ self.checkpointer = checkpointer
110
+ self.save_freq = save_freq
111
+ self._last_saved: int | None = None
112
+
113
+ def on_train_start(self, trainer: Trainer) -> None:
114
+ self.checkpointer.setup(trainer)
115
+
116
+ def on_iteration_end(self, trainer: Trainer, iteration: int) -> None:
117
+ if iteration % self.save_freq == 0:
118
+ self.checkpointer.save(trainer, iteration, trainer.history)
119
+ self._last_saved = iteration
120
+
121
+ def on_train_end(self, trainer: Trainer) -> None:
122
+ if trainer.iteration > 0 and self._last_saved != trainer.iteration:
123
+ self.checkpointer.save(trainer, trainer.iteration, trainer.history)
124
+ self._last_saved = trainer.iteration
125
+ self.checkpointer.close()