mltrainer 0.3.0__tar.gz → 0.3.1__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.
- mltrainer-0.3.1/.claude/settings.local.json +7 -0
- mltrainer-0.3.1/.remember/.gitignore +1 -0
- mltrainer-0.3.1/.remember/tmp/last-ndc.ts +1 -0
- mltrainer-0.3.1/.remember/tmp/last-save-ts +1 -0
- mltrainer-0.3.1/.remember/tmp/last-save.json +1 -0
- mltrainer-0.3.1/.remember/tmp/save-session.pid +1 -0
- mltrainer-0.3.1/.remember/today-2026-07-20.md +2 -0
- mltrainer-0.3.1/Makefile +12 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/PKG-INFO +2 -1
- {mltrainer-0.3.0 → mltrainer-0.3.1}/pyproject.toml +2 -2
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/__init__.py +1 -1
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/metrics.py +7 -2
- mltrainer-0.3.1/src/mltrainer/settings.py +61 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/trainer.py +151 -63
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/transformer.py +3 -0
- mltrainer-0.3.1/tests/utils/dummy_model.py +0 -0
- mltrainer-0.3.0/Makefile +0 -7
- mltrainer-0.3.0/src/mltrainer/settings.py +0 -177
- {mltrainer-0.3.0 → mltrainer-0.3.1}/.gitignore +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/.lefthook.yml +0 -0
- /mltrainer-0.3.0/tests/__init__.py → /mltrainer-0.3.1/.remember/now.md +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/README.md +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/imagemodels.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/preprocessors.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/rnn_models.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/vae.py +0 -0
- {mltrainer-0.3.0/tests/unit → mltrainer-0.3.1/tests}/__init__.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/conftest.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_metrics.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_preprocessors.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_trainer.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_trainer_devices.py +0 -0
- {mltrainer-0.3.0/tests/utils → mltrainer-0.3.1/tests/unit}/__init__.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/unit/test_trainersettings.py +0 -0
- /mltrainer-0.3.0/tests/utils/dummy_data.py → /mltrainer-0.3.1/tests/utils/__init__.py +0 -0
- /mltrainer-0.3.0/tests/utils/dummy_model.py → /mltrainer-0.3.1/tests/utils/dummy_data.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/utils/dummy_metrics.py +0 -0
- {mltrainer-0.3.0 → mltrainer-0.3.1}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1784568708
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1784568694
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"session": "284374ad-dc46-496d-a542-5d141c54654d", "line": 204}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
85305
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
## 19:31 | main
|
|
2
|
+
Refactored mltrainer.report() w/ Reporter ABC (RayReporter/MLflow/TensorBoard); type hints→builtin gen; added _to_device helper; fixed ty checks (transformer.py/trainer.py); Makefile: ruff/ty; cleaned ~115 commented LOC (settings.py); 47 tests pass; ready v0.3.1
|
mltrainer-0.3.1/Makefile
ADDED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mltrainer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: toolkit for training pytorch models
|
|
5
|
+
Project-URL: GitHub, https://github.com/raoulg/mltrainer
|
|
5
6
|
Author-email: "R.Grouls" <Raoul.Grouls@han.nl>
|
|
6
7
|
License: MIT
|
|
7
8
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
[project]
|
|
3
3
|
name = "mltrainer"
|
|
4
|
-
version = "0.3.
|
|
4
|
+
version = "0.3.1"
|
|
5
5
|
description = "toolkit for training pytorch models"
|
|
6
6
|
|
|
7
7
|
authors = [
|
|
@@ -44,7 +44,7 @@ build-backend = "hatchling.build"
|
|
|
44
44
|
[tool.hatch.build.targets.wheel]
|
|
45
45
|
packages = ["src/mltrainer"]
|
|
46
46
|
|
|
47
|
-
[
|
|
47
|
+
[project.urls]
|
|
48
48
|
GitHub = "https://github.com/raoulg/mltrainer"
|
|
49
49
|
|
|
50
50
|
[dependency-groups]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Iterator, Union
|
|
4
|
+
from typing import Iterator, Literal, Union
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import torch
|
|
@@ -86,7 +86,12 @@ class Accuracy(Metric):
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
class F1Score(Metric):
|
|
89
|
-
def __init__(
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
average: Literal["micro", "macro"] = "micro",
|
|
92
|
+
threshold: float = 0.5,
|
|
93
|
+
epsilon: float = 1e-7,
|
|
94
|
+
) -> None:
|
|
90
95
|
self.average = average
|
|
91
96
|
self.threshold = threshold
|
|
92
97
|
self.epsilon = epsilon
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileTypes(Enum):
|
|
12
|
+
JPG = ".jpg"
|
|
13
|
+
PNG = ".png"
|
|
14
|
+
TXT = ".txt"
|
|
15
|
+
ZIP = ".zip"
|
|
16
|
+
TGZ = ".tgz"
|
|
17
|
+
TAR = ".tar.gz"
|
|
18
|
+
GZ = ".gz"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReportTypes(Enum):
|
|
22
|
+
TOML = "TOML"
|
|
23
|
+
TENSORBOARD = "TENSORBOARD"
|
|
24
|
+
MLFLOW = "MLFLOW"
|
|
25
|
+
RAY = "RAY"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FormattedBase(BaseModel):
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return "\n".join(f"{k}: {v}" for k, v in self.__dict__.items())
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return "\n".join(f"{k}: {v}" for k, v in self.__dict__.items())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TrainerSettings(FormattedBase):
|
|
37
|
+
epochs: int
|
|
38
|
+
metrics: List[Callable]
|
|
39
|
+
logdir: Path
|
|
40
|
+
train_steps: int
|
|
41
|
+
valid_steps: int
|
|
42
|
+
reporttypes: List[ReportTypes]
|
|
43
|
+
optimizer_kwargs: Dict[str, Any] = {"lr": 1e-3, "weight_decay": 1e-5}
|
|
44
|
+
scheduler_kwargs: Optional[Dict[str, Any]] = {"factor": 0.1, "patience": 10}
|
|
45
|
+
earlystop_kwargs: Optional[Dict[str, Any]] = {
|
|
46
|
+
"save": False,
|
|
47
|
+
"verbose": True,
|
|
48
|
+
"patience": 10,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
52
|
+
|
|
53
|
+
@field_validator("logdir")
|
|
54
|
+
@classmethod
|
|
55
|
+
def check_path(cls, logdir: Path) -> Path: # noqa: N805
|
|
56
|
+
if isinstance(logdir, str):
|
|
57
|
+
logdir = Path(logdir)
|
|
58
|
+
if not logdir.resolve().exists():
|
|
59
|
+
logdir.mkdir(parents=True)
|
|
60
|
+
logger.info(f"Created logdir {logdir.absolute()}")
|
|
61
|
+
return logdir
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
1
3
|
from datetime import datetime
|
|
2
4
|
from pathlib import Path
|
|
3
|
-
from typing import
|
|
5
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
4
6
|
|
|
5
7
|
import mlflow
|
|
6
8
|
import torch
|
|
@@ -11,10 +13,17 @@ from tqdm import tqdm
|
|
|
11
13
|
|
|
12
14
|
from mltrainer import ReportTypes, TrainerSettings
|
|
13
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from torch.utils.tensorboard.writer import SummaryWriter
|
|
18
|
+
|
|
14
19
|
OptimizerType = TypeVar("OptimizerType", bound=torch.optim.Optimizer)
|
|
15
20
|
|
|
21
|
+
# A batch input is either a single tensor or a tuple of tensors (e.g. packed
|
|
22
|
+
# sequences); the target is always a tensor.
|
|
23
|
+
BatchTensor = torch.Tensor | tuple[torch.Tensor, ...]
|
|
24
|
+
|
|
16
25
|
|
|
17
|
-
def dir_add_timestamp(log_dir:
|
|
26
|
+
def dir_add_timestamp(log_dir: Path | None = None) -> Path:
|
|
18
27
|
if log_dir is None:
|
|
19
28
|
log_dir = Path(".")
|
|
20
29
|
log_dir = Path(log_dir)
|
|
@@ -26,27 +35,110 @@ def dir_add_timestamp(log_dir: Optional[Path] = None) -> Path:
|
|
|
26
35
|
return log_dir
|
|
27
36
|
|
|
28
37
|
|
|
38
|
+
class Reporter(ABC):
|
|
39
|
+
"""Reports training metrics to a single backend.
|
|
40
|
+
|
|
41
|
+
A new backend is added by subclassing (open for extension); the training
|
|
42
|
+
loop that iterates over reporters never changes (closed for modification).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def report(
|
|
47
|
+
self,
|
|
48
|
+
epoch: int,
|
|
49
|
+
train_loss: float,
|
|
50
|
+
test_loss: float,
|
|
51
|
+
metric_dict: dict[str, float],
|
|
52
|
+
) -> None: ...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RayReporter(Reporter):
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
try:
|
|
58
|
+
import ray
|
|
59
|
+
except ModuleNotFoundError:
|
|
60
|
+
raise ModuleNotFoundError(
|
|
61
|
+
"ray is required for RAY reporting. "
|
|
62
|
+
"Install it with: uv add 'mltrainer[tune]'"
|
|
63
|
+
)
|
|
64
|
+
self._ray = ray
|
|
65
|
+
|
|
66
|
+
def report(
|
|
67
|
+
self,
|
|
68
|
+
epoch: int,
|
|
69
|
+
train_loss: float,
|
|
70
|
+
test_loss: float,
|
|
71
|
+
metric_dict: dict[str, float],
|
|
72
|
+
) -> None:
|
|
73
|
+
self._ray.train.report( # ty: ignore[possibly-missing-attribute]
|
|
74
|
+
{
|
|
75
|
+
"iterations": epoch,
|
|
76
|
+
"train_loss": train_loss,
|
|
77
|
+
"test_loss": test_loss,
|
|
78
|
+
**metric_dict,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MLflowReporter(Reporter):
|
|
84
|
+
def __init__(self, optimizer: Optimizer) -> None:
|
|
85
|
+
self.optimizer = optimizer
|
|
86
|
+
|
|
87
|
+
def report(
|
|
88
|
+
self,
|
|
89
|
+
epoch: int,
|
|
90
|
+
train_loss: float,
|
|
91
|
+
test_loss: float,
|
|
92
|
+
metric_dict: dict[str, float],
|
|
93
|
+
) -> None:
|
|
94
|
+
mlflow.log_metric("Loss/train", train_loss, step=epoch)
|
|
95
|
+
mlflow.log_metric("Loss/test", test_loss, step=epoch)
|
|
96
|
+
for m in metric_dict:
|
|
97
|
+
mlflow.log_metric(f"metric/{m}", metric_dict[m], step=epoch)
|
|
98
|
+
lr = self.optimizer.param_groups[0]["lr"]
|
|
99
|
+
mlflow.log_metric("learning_rate", lr, step=epoch)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TensorBoardReporter(Reporter):
|
|
103
|
+
def __init__(self, writer: "SummaryWriter", optimizer: Optimizer) -> None:
|
|
104
|
+
self.writer = writer
|
|
105
|
+
self.optimizer = optimizer
|
|
106
|
+
|
|
107
|
+
def report(
|
|
108
|
+
self,
|
|
109
|
+
epoch: int,
|
|
110
|
+
train_loss: float,
|
|
111
|
+
test_loss: float,
|
|
112
|
+
metric_dict: dict[str, float],
|
|
113
|
+
) -> None:
|
|
114
|
+
self.writer.add_scalar("Loss/train", train_loss, epoch)
|
|
115
|
+
self.writer.add_scalar("Loss/test", test_loss, epoch)
|
|
116
|
+
for m in metric_dict:
|
|
117
|
+
self.writer.add_scalar(f"metric/{m}", metric_dict[m], epoch)
|
|
118
|
+
lr = self.optimizer.param_groups[0]["lr"]
|
|
119
|
+
self.writer.add_scalar("learning_rate", lr, epoch)
|
|
120
|
+
|
|
121
|
+
|
|
29
122
|
class Trainer:
|
|
30
123
|
def __init__(
|
|
31
124
|
self,
|
|
32
125
|
model: torch.nn.Module,
|
|
33
126
|
settings: TrainerSettings,
|
|
34
127
|
loss_fn: Callable,
|
|
35
|
-
optimizer:
|
|
128
|
+
optimizer: type[Optimizer],
|
|
36
129
|
traindataloader: Iterator,
|
|
37
130
|
validdataloader: Iterator,
|
|
38
|
-
scheduler:
|
|
39
|
-
device:
|
|
131
|
+
scheduler: Callable | None,
|
|
132
|
+
device: str | None = None,
|
|
40
133
|
) -> None:
|
|
41
134
|
self.model = model
|
|
42
135
|
self.settings = settings
|
|
43
136
|
self.log_dir = dir_add_timestamp(settings.logdir)
|
|
44
137
|
self.loss_fn = loss_fn
|
|
45
|
-
self.optimizer = optimizer
|
|
46
138
|
self.traindataloader = traindataloader
|
|
47
139
|
self.validdataloader = validdataloader
|
|
48
140
|
self.device = device
|
|
49
|
-
self.writer = None
|
|
141
|
+
self.writer: "SummaryWriter | None" = None
|
|
50
142
|
|
|
51
143
|
if self.device:
|
|
52
144
|
self.model.to(self.device)
|
|
@@ -66,7 +158,7 @@ class Trainer:
|
|
|
66
158
|
"Found earlystop_kwargs in settings."
|
|
67
159
|
"Set to None if you dont want earlystopping."
|
|
68
160
|
)
|
|
69
|
-
self.early_stopping:
|
|
161
|
+
self.early_stopping: EarlyStopping | None = EarlyStopping(
|
|
70
162
|
self.log_dir, **settings.earlystop_kwargs
|
|
71
163
|
)
|
|
72
164
|
else:
|
|
@@ -81,6 +173,34 @@ class Trainer:
|
|
|
81
173
|
TOMLSerializer.save(model, self.log_dir / "model.toml")
|
|
82
174
|
TOMLSerializer.save(settings, self.log_dir / "settings.toml")
|
|
83
175
|
|
|
176
|
+
self.reporters = self._build_reporters()
|
|
177
|
+
|
|
178
|
+
def _build_reporters(self) -> list[Reporter]:
|
|
179
|
+
"""Map the requested report types onto reporter objects once, so the
|
|
180
|
+
per-epoch report loop stays closed for modification."""
|
|
181
|
+
reporters: list[Reporter] = []
|
|
182
|
+
if ReportTypes.RAY in self.settings.reporttypes:
|
|
183
|
+
reporters.append(RayReporter())
|
|
184
|
+
if ReportTypes.MLFLOW in self.settings.reporttypes:
|
|
185
|
+
reporters.append(MLflowReporter(self.optimizer))
|
|
186
|
+
if ReportTypes.TENSORBOARD in self.settings.reporttypes:
|
|
187
|
+
assert self.writer is not None
|
|
188
|
+
reporters.append(TensorBoardReporter(self.writer, self.optimizer))
|
|
189
|
+
return reporters
|
|
190
|
+
|
|
191
|
+
def _to_device(
|
|
192
|
+
self, x: BatchTensor, y: torch.Tensor
|
|
193
|
+
) -> tuple[BatchTensor, torch.Tensor]:
|
|
194
|
+
"""Move a batch onto self.device, handling tuple inputs (e.g. packed
|
|
195
|
+
sequences) as well as single tensors."""
|
|
196
|
+
if not self.device:
|
|
197
|
+
return x, y
|
|
198
|
+
if isinstance(x, tuple):
|
|
199
|
+
x = tuple(xx.to(self.device) for xx in x)
|
|
200
|
+
else:
|
|
201
|
+
x = x.to(self.device)
|
|
202
|
+
return x, y.to(self.device)
|
|
203
|
+
|
|
84
204
|
def __del__(self):
|
|
85
205
|
"""Cleanup method to ensure proper resource handling"""
|
|
86
206
|
if hasattr(self, "writer") and self.writer is not None:
|
|
@@ -115,38 +235,28 @@ class Trainer:
|
|
|
115
235
|
train_steps = self.settings.train_steps
|
|
116
236
|
for _ in tqdm(range(train_steps), colour="#1e4706"):
|
|
117
237
|
x, y = next(iter(self.traindataloader))
|
|
118
|
-
|
|
119
|
-
if isinstance(x, tuple):
|
|
120
|
-
x = tuple(xx.to(self.device) for xx in x)
|
|
121
|
-
y = y.to(self.device)
|
|
122
|
-
else:
|
|
123
|
-
x, y = x.to(self.device), y.to(self.device)
|
|
238
|
+
x, y = self._to_device(x, y)
|
|
124
239
|
self.optimizer.zero_grad()
|
|
125
240
|
yhat = self.model(x)
|
|
126
241
|
loss = self.loss_fn(yhat, y)
|
|
127
242
|
loss.backward()
|
|
128
|
-
self.optimizer.step()
|
|
243
|
+
self.optimizer.step()
|
|
129
244
|
train_loss += loss.cpu().detach().numpy()
|
|
130
245
|
train_loss /= train_steps
|
|
131
246
|
|
|
132
247
|
return train_loss
|
|
133
248
|
|
|
134
|
-
def evalbatches(self) ->
|
|
249
|
+
def evalbatches(self) -> tuple[dict[str, float], float]:
|
|
135
250
|
"""Evaluate model on validation data with proper device handling"""
|
|
136
251
|
self.model.eval()
|
|
137
252
|
valid_steps = self.settings.valid_steps
|
|
138
253
|
test_loss: float = 0.0
|
|
139
|
-
metric_dict:
|
|
254
|
+
metric_dict: dict[str, float] = {}
|
|
140
255
|
|
|
141
256
|
with torch.no_grad(): # Prevent gradient computation during evaluation
|
|
142
257
|
for _ in range(valid_steps):
|
|
143
258
|
x, y = next(iter(self.validdataloader))
|
|
144
|
-
|
|
145
|
-
if isinstance(x, tuple):
|
|
146
|
-
x = tuple(xx.to(self.device) for xx in x)
|
|
147
|
-
y = y.to(self.device)
|
|
148
|
-
else:
|
|
149
|
-
x, y = x.to(self.device), y.to(self.device)
|
|
259
|
+
x, y = self._to_device(x, y)
|
|
150
260
|
|
|
151
261
|
# Forward pass
|
|
152
262
|
yhat = self.model(x)
|
|
@@ -161,7 +271,7 @@ class Trainer:
|
|
|
161
271
|
# Average the results
|
|
162
272
|
test_loss /= valid_steps
|
|
163
273
|
for key in metric_dict:
|
|
164
|
-
metric_dict[
|
|
274
|
+
metric_dict[key] = metric_dict[key] / valid_steps
|
|
165
275
|
|
|
166
276
|
# Handle scheduler
|
|
167
277
|
if self.scheduler:
|
|
@@ -172,54 +282,32 @@ class Trainer:
|
|
|
172
282
|
|
|
173
283
|
return metric_dict, test_loss
|
|
174
284
|
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (self.last_epoch != 0) and (epoch == 0):
|
|
285
|
+
def _resolve_epoch(self, epoch: int) -> int:
|
|
286
|
+
"""Offset the epoch counter when resuming a previous training run."""
|
|
287
|
+
if self.last_epoch != 0 and epoch == 0:
|
|
179
288
|
self.last_epoch += 1
|
|
180
289
|
logger.info(f"Resuming epochs from previous training at {self.last_epoch}")
|
|
181
290
|
if self.last_epoch != 0:
|
|
182
291
|
epoch += self.last_epoch
|
|
183
|
-
|
|
184
|
-
self.test_loss = test_loss
|
|
185
|
-
|
|
186
|
-
if ReportTypes.RAY in reporttypes:
|
|
187
|
-
try:
|
|
188
|
-
import ray
|
|
189
|
-
except ModuleNotFoundError:
|
|
190
|
-
raise ModuleNotFoundError(
|
|
191
|
-
"ray is required for RAY reporting. "
|
|
192
|
-
"Install it with: uv add 'mltrainer[tune]'"
|
|
193
|
-
)
|
|
194
|
-
ray.train.report(
|
|
195
|
-
{
|
|
196
|
-
"iterations": epoch,
|
|
197
|
-
"train_loss": train_loss,
|
|
198
|
-
"test_loss": test_loss,
|
|
199
|
-
**metric_dict,
|
|
200
|
-
}
|
|
201
|
-
)
|
|
292
|
+
return epoch
|
|
202
293
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
294
|
+
def report(
|
|
295
|
+
self,
|
|
296
|
+
epoch: int,
|
|
297
|
+
train_loss: float,
|
|
298
|
+
test_loss: float,
|
|
299
|
+
metric_dict: dict[str, float],
|
|
300
|
+
) -> None:
|
|
301
|
+
epoch = self._resolve_epoch(epoch)
|
|
302
|
+
self.test_loss = test_loss
|
|
210
303
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.writer.add_scalar("Loss/train", train_loss, epoch)
|
|
214
|
-
self.writer.add_scalar("Loss/test", test_loss, epoch)
|
|
215
|
-
for m in metric_dict:
|
|
216
|
-
self.writer.add_scalar(f"metric/{m}", metric_dict[m], epoch)
|
|
217
|
-
lr = [group["lr"] for group in self.optimizer.param_groups][0] # type: ignore
|
|
218
|
-
self.writer.add_scalar("learning_rate", lr, epoch)
|
|
304
|
+
for reporter in self.reporters:
|
|
305
|
+
reporter.report(epoch, train_loss, test_loss, metric_dict)
|
|
219
306
|
|
|
220
307
|
metric_scores = [f"{v:.4f}" for v in metric_dict.values()]
|
|
221
308
|
logger.info(
|
|
222
|
-
f"Epoch {epoch} train {train_loss:.4f} test {test_loss:.4f}
|
|
309
|
+
f"Epoch {epoch} train {train_loss:.4f} test {test_loss:.4f} "
|
|
310
|
+
f"metric {metric_scores}"
|
|
223
311
|
)
|
|
224
312
|
|
|
225
313
|
|
|
@@ -21,6 +21,9 @@ class PositionalEncoding(nn.Module):
|
|
|
21
21
|
It adds positional information to the input embeddings.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
pe: Tensor # registered as a buffer in __init__; annotated so type checkers
|
|
25
|
+
# know it is a Tensor rather than a generic Module attribute.
|
|
26
|
+
|
|
24
27
|
def __init__(self, d_model: int, dropout: float = 0.1, max_seq_len: int = 5000):
|
|
25
28
|
super().__init__()
|
|
26
29
|
self.dropout = nn.Dropout(p=dropout)
|
|
File without changes
|
mltrainer-0.3.0/Makefile
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
-
|
|
7
|
-
from loguru import logger
|
|
8
|
-
from pydantic import BaseModel, ConfigDict, field_validator
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FileTypes(Enum):
|
|
12
|
-
JPG = ".jpg"
|
|
13
|
-
PNG = ".png"
|
|
14
|
-
TXT = ".txt"
|
|
15
|
-
ZIP = ".zip"
|
|
16
|
-
TGZ = ".tgz"
|
|
17
|
-
TAR = ".tar.gz"
|
|
18
|
-
GZ = ".gz"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ReportTypes(Enum):
|
|
22
|
-
TOML = "TOML"
|
|
23
|
-
TENSORBOARD = "TENSORBOARD"
|
|
24
|
-
MLFLOW = "MLFLOW"
|
|
25
|
-
RAY = "RAY"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class FormattedBase(BaseModel):
|
|
29
|
-
def __str__(self) -> str:
|
|
30
|
-
return "\n".join(f"{k}: {v}" for k, v in self.__dict__.items())
|
|
31
|
-
|
|
32
|
-
def __repr__(self) -> str:
|
|
33
|
-
return "\n".join(f"{k}: {v}" for k, v in self.__dict__.items())
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class TrainerSettings(FormattedBase):
|
|
37
|
-
epochs: int
|
|
38
|
-
metrics: List[Callable]
|
|
39
|
-
logdir: Path
|
|
40
|
-
train_steps: int
|
|
41
|
-
valid_steps: int
|
|
42
|
-
reporttypes: List[ReportTypes]
|
|
43
|
-
optimizer_kwargs: Dict[str, Any] = {"lr": 1e-3, "weight_decay": 1e-5}
|
|
44
|
-
scheduler_kwargs: Optional[Dict[str, Any]] = {"factor": 0.1, "patience": 10}
|
|
45
|
-
earlystop_kwargs: Optional[Dict[str, Any]] = {
|
|
46
|
-
"save": False,
|
|
47
|
-
"verbose": True,
|
|
48
|
-
"patience": 10,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
52
|
-
|
|
53
|
-
@field_validator("logdir")
|
|
54
|
-
@classmethod
|
|
55
|
-
def check_path(cls, logdir: Path) -> Path: # noqa: N805
|
|
56
|
-
if isinstance(logdir, str):
|
|
57
|
-
logdir = Path(logdir)
|
|
58
|
-
if not logdir.resolve().exists():
|
|
59
|
-
logdir.mkdir(parents=True)
|
|
60
|
-
logger.info(f"Created logdir {logdir.absolute()}")
|
|
61
|
-
return logdir
|
|
62
|
-
|
|
63
|
-
# def to_toml_dict(self) -> dict:
|
|
64
|
-
# """Convert the model to a TOML-compatible dictionary."""
|
|
65
|
-
# data = self.model_dump(exclude_none=True)
|
|
66
|
-
#
|
|
67
|
-
# # Handle special cases
|
|
68
|
-
# if "metrics" in data:
|
|
69
|
-
# data["metrics"] = [
|
|
70
|
-
# f"{metric.__module__}.{metric.__name__}"
|
|
71
|
-
# if isinstance(metric, Callable) and hasattr(metric, "__name__")
|
|
72
|
-
# else f"{metric.__class__.__module__}.{metric.__class__.__name__}"
|
|
73
|
-
# for metric in data["metrics"]
|
|
74
|
-
# ]
|
|
75
|
-
#
|
|
76
|
-
# # Convert Path objects to strings
|
|
77
|
-
# for key, value in data.items():
|
|
78
|
-
# if isinstance(value, Path):
|
|
79
|
-
# data[key] = str(value)
|
|
80
|
-
#
|
|
81
|
-
# return data
|
|
82
|
-
#
|
|
83
|
-
# def save_toml_example(self, file_path: str | Path) -> None:
|
|
84
|
-
# """Save an example TOML configuration file with comments."""
|
|
85
|
-
# if isinstance(file_path, str):
|
|
86
|
-
# file_path = Path(file_path)
|
|
87
|
-
#
|
|
88
|
-
# data = self.to_toml_dict()
|
|
89
|
-
# lines = []
|
|
90
|
-
#
|
|
91
|
-
# # Start with just the simple scalar values we know work
|
|
92
|
-
# lines.append("epochs = {}\n".format(data["epochs"]))
|
|
93
|
-
# metric_paths = [
|
|
94
|
-
# f"{metric.__module__}.{metric.__name__}" for metric in self.metrics
|
|
95
|
-
# ]
|
|
96
|
-
# lines.append("metrics = {}\n".format(str(metric_paths).replace("'", '"')))
|
|
97
|
-
# lines.append('logdir = "{}"\n'.format(str(data["logdir"])))
|
|
98
|
-
# lines.append("train_steps = {}\n".format(data["train_steps"]))
|
|
99
|
-
# lines.append("valid_steps = {}\n".format(data["valid_steps"]))
|
|
100
|
-
#
|
|
101
|
-
# # Add reporttypes as list of strings (enum names)
|
|
102
|
-
# report_types = [rt.value for rt in data["reporttypes"]]
|
|
103
|
-
# lines.append("reporttypes = {}\n".format(str(report_types).replace("'", '"')))
|
|
104
|
-
#
|
|
105
|
-
# if "optimizer_kwargs" in data:
|
|
106
|
-
# lines.append(
|
|
107
|
-
# self.dict_to_toml("optimizer_kwargs", data["optimizer_kwargs"])
|
|
108
|
-
# )
|
|
109
|
-
#
|
|
110
|
-
# if "scheduler_kwargs" in data:
|
|
111
|
-
# lines.append(
|
|
112
|
-
# self.dict_to_toml("scheduler_kwargs", data["scheduler_kwargs"])
|
|
113
|
-
# )
|
|
114
|
-
#
|
|
115
|
-
# if "earlystop_kwargs" in data:
|
|
116
|
-
# lines.append(
|
|
117
|
-
# self.dict_to_toml("earlystop_kwargs", data["earlystop_kwargs"])
|
|
118
|
-
# )
|
|
119
|
-
#
|
|
120
|
-
# with open(file_path, "w", encoding="utf-8") as f:
|
|
121
|
-
# f.write("".join(lines))
|
|
122
|
-
#
|
|
123
|
-
# @staticmethod
|
|
124
|
-
# def dict_to_toml(name: str, data: dict) -> str:
|
|
125
|
-
# """
|
|
126
|
-
# Convert a dictionary into a TOML table format.
|
|
127
|
-
#
|
|
128
|
-
# Args:
|
|
129
|
-
# name (str): Name of the TOML section.
|
|
130
|
-
# data (dict): Dictionary to convert.
|
|
131
|
-
#
|
|
132
|
-
# Returns:
|
|
133
|
-
# str: TOML formatted table string.
|
|
134
|
-
# """
|
|
135
|
-
# toml_lines = [f"[{name}]"]
|
|
136
|
-
# for k, v in data.items():
|
|
137
|
-
# # Convert booleans to lowercase ('true'/'false')
|
|
138
|
-
# if isinstance(v, bool):
|
|
139
|
-
# v = str(v).lower()
|
|
140
|
-
# toml_lines.append(f"{k} = {v}")
|
|
141
|
-
# return "\n".join(toml_lines) + "\n"
|
|
142
|
-
#
|
|
143
|
-
# @classmethod
|
|
144
|
-
# def from_toml(cls, file_path: str | Path) -> "TrainerSettings":
|
|
145
|
-
# """Load the model from a TOML file."""
|
|
146
|
-
# if isinstance(file_path, str):
|
|
147
|
-
# file_path = Path(file_path)
|
|
148
|
-
#
|
|
149
|
-
# with open(file_path, "rb") as f:
|
|
150
|
-
# data = tomllib.load(f)
|
|
151
|
-
#
|
|
152
|
-
# print(data)
|
|
153
|
-
#
|
|
154
|
-
# # Convert string paths to Path objects
|
|
155
|
-
# if "logdir" in data and isinstance(data["logdir"], str):
|
|
156
|
-
# data["logdir"] = Path(data["logdir"])
|
|
157
|
-
# # Convert reporttype strings back to enums
|
|
158
|
-
# if "reporttypes" in data:
|
|
159
|
-
# data["reporttypes"] = [ReportTypes(rt) for rt in data["reporttypes"]]
|
|
160
|
-
#
|
|
161
|
-
# # Import metrics from their module paths
|
|
162
|
-
# if "metrics" in data:
|
|
163
|
-
# metrics = []
|
|
164
|
-
# for metric_path in data["metrics"]:
|
|
165
|
-
# try:
|
|
166
|
-
# module_path, func_name = metric_path.rsplit(".", 1)
|
|
167
|
-
# module = __import__(module_path, fromlist=[func_name])
|
|
168
|
-
# metric = getattr(module, func_name)
|
|
169
|
-
# metrics.append(metric)
|
|
170
|
-
# except (ImportError, AttributeError) as e:
|
|
171
|
-
# logger.warning(
|
|
172
|
-
# f"Could not load metric {metric_path}. "
|
|
173
|
-
# f"Error: {e}. Using placeholder."
|
|
174
|
-
# )
|
|
175
|
-
# data["metrics"] = metrics
|
|
176
|
-
#
|
|
177
|
-
# return cls(**data)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|