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.
- eztrain-0.1.0/.github/workflows/ci.yml +23 -0
- eztrain-0.1.0/.github/workflows/publish.yml +30 -0
- eztrain-0.1.0/.gitignore +11 -0
- eztrain-0.1.0/.python-version +1 -0
- eztrain-0.1.0/LICENSE +21 -0
- eztrain-0.1.0/PKG-INFO +187 -0
- eztrain-0.1.0/README.md +159 -0
- eztrain-0.1.0/pyproject.toml +67 -0
- eztrain-0.1.0/src/eztrain/__init__.py +48 -0
- eztrain-0.1.0/src/eztrain/callbacks.py +125 -0
- eztrain-0.1.0/src/eztrain/checkpoint.py +32 -0
- eztrain-0.1.0/src/eztrain/loggers.py +118 -0
- eztrain-0.1.0/src/eztrain/media.py +25 -0
- eztrain-0.1.0/src/eztrain/metrics.py +61 -0
- eztrain-0.1.0/src/eztrain/py.typed +0 -0
- eztrain-0.1.0/src/eztrain/run.py +58 -0
- eztrain-0.1.0/src/eztrain/trainer.py +227 -0
- eztrain-0.1.0/tests/__init__.py +0 -0
- eztrain-0.1.0/tests/test_callbacks.py +118 -0
- eztrain-0.1.0/tests/test_epoch_trainer.py +96 -0
- eztrain-0.1.0/tests/test_ezconfy_ready.py +41 -0
- eztrain-0.1.0/tests/test_loggers.py +38 -0
- eztrain-0.1.0/tests/test_metrics.py +56 -0
- eztrain-0.1.0/tests/test_run.py +42 -0
- eztrain-0.1.0/tests/test_trainer.py +190 -0
- eztrain-0.1.0/uv.lock +1040 -0
|
@@ -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
|
eztrain-0.1.0/.gitignore
ADDED
|
@@ -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.
|
eztrain-0.1.0/README.md
ADDED
|
@@ -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()
|