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.
Files changed (38) hide show
  1. mltrainer-0.3.1/.claude/settings.local.json +7 -0
  2. mltrainer-0.3.1/.remember/.gitignore +1 -0
  3. mltrainer-0.3.1/.remember/tmp/last-ndc.ts +1 -0
  4. mltrainer-0.3.1/.remember/tmp/last-save-ts +1 -0
  5. mltrainer-0.3.1/.remember/tmp/last-save.json +1 -0
  6. mltrainer-0.3.1/.remember/tmp/save-session.pid +1 -0
  7. mltrainer-0.3.1/.remember/today-2026-07-20.md +2 -0
  8. mltrainer-0.3.1/Makefile +12 -0
  9. {mltrainer-0.3.0 → mltrainer-0.3.1}/PKG-INFO +2 -1
  10. {mltrainer-0.3.0 → mltrainer-0.3.1}/pyproject.toml +2 -2
  11. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/__init__.py +1 -1
  12. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/metrics.py +7 -2
  13. mltrainer-0.3.1/src/mltrainer/settings.py +61 -0
  14. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/trainer.py +151 -63
  15. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/transformer.py +3 -0
  16. mltrainer-0.3.1/tests/utils/dummy_model.py +0 -0
  17. mltrainer-0.3.0/Makefile +0 -7
  18. mltrainer-0.3.0/src/mltrainer/settings.py +0 -177
  19. {mltrainer-0.3.0 → mltrainer-0.3.1}/.gitignore +0 -0
  20. {mltrainer-0.3.0 → mltrainer-0.3.1}/.lefthook.yml +0 -0
  21. /mltrainer-0.3.0/tests/__init__.py → /mltrainer-0.3.1/.remember/now.md +0 -0
  22. {mltrainer-0.3.0 → mltrainer-0.3.1}/README.md +0 -0
  23. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/imagemodels.py +0 -0
  24. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/preprocessors.py +0 -0
  25. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/rnn_models.py +0 -0
  26. {mltrainer-0.3.0 → mltrainer-0.3.1}/src/mltrainer/vae.py +0 -0
  27. {mltrainer-0.3.0/tests/unit → mltrainer-0.3.1/tests}/__init__.py +0 -0
  28. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/conftest.py +0 -0
  29. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_metrics.py +0 -0
  30. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_preprocessors.py +0 -0
  31. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_trainer.py +0 -0
  32. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/test_trainer_devices.py +0 -0
  33. {mltrainer-0.3.0/tests/utils → mltrainer-0.3.1/tests/unit}/__init__.py +0 -0
  34. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/unit/test_trainersettings.py +0 -0
  35. /mltrainer-0.3.0/tests/utils/dummy_data.py → /mltrainer-0.3.1/tests/utils/__init__.py +0 -0
  36. /mltrainer-0.3.0/tests/utils/dummy_model.py → /mltrainer-0.3.1/tests/utils/dummy_data.py +0 -0
  37. {mltrainer-0.3.0 → mltrainer-0.3.1}/tests/utils/dummy_metrics.py +0 -0
  38. {mltrainer-0.3.0 → mltrainer-0.3.1}/uv.lock +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run *)"
5
+ ]
6
+ }
7
+ }
@@ -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
@@ -0,0 +1,12 @@
1
+ format:
2
+ ruff format src
3
+ ruff check --fix src
4
+
5
+ lint:
6
+ ruff check src
7
+ ty check src
8
+
9
+ test:
10
+ pytest
11
+
12
+ .PHONY: format lint test
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mltrainer
3
- Version: 0.3.0
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.0"
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
- [tools.urls]
47
+ [project.urls]
48
48
  GitHub = "https://github.com/raoulg/mltrainer"
49
49
 
50
50
  [dependency-groups]
@@ -3,4 +3,4 @@ from mltrainer.trainer import Trainer
3
3
 
4
4
  __all__ = ["Trainer", "TrainerSettings", "ReportTypes"]
5
5
 
6
- __version__ = "0.2.7"
6
+ __version__ = "0.3.1"
@@ -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__(self, average="micro", threshold=0.5, epsilon=1e-7):
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 Callable, Dict, Iterator, Optional, Tuple, Type, TypeVar
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: Optional[Path] = None) -> Path:
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: Type[Optimizer],
128
+ optimizer: type[Optimizer],
36
129
  traindataloader: Iterator,
37
130
  validdataloader: Iterator,
38
- scheduler: Optional[Callable],
39
- device: Optional[str] = None,
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: Optional[EarlyStopping] = EarlyStopping(
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
- if self.device:
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() # type: ignore
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) -> Tuple[Dict[str, float], float]:
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: Dict[str, float] = {}
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
- if self.device:
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[str(key)] = metric_dict[str(key)] / valid_steps
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 report(
176
- self, epoch: int, train_loss: float, test_loss: float, metric_dict: Dict
177
- ) -> None:
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
- reporttypes = self.settings.reporttypes
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
- if ReportTypes.MLFLOW in reporttypes:
204
- mlflow.log_metric("Loss/train", train_loss, step=epoch)
205
- mlflow.log_metric("Loss/test", test_loss, step=epoch)
206
- for m in metric_dict:
207
- mlflow.log_metric(f"metric/{m}", metric_dict[m], step=epoch)
208
- lr = [group["lr"] for group in self.optimizer.param_groups][0] # type: ignore
209
- mlflow.log_metric("learning_rate", lr, step=epoch)
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
- if ReportTypes.TENSORBOARD in reporttypes:
212
- assert self.writer is not None
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} metric {metric_scores}" # noqa E501
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,7 +0,0 @@
1
- format:
2
- black mltrainer
3
- isort mltrainer
4
-
5
- lint:
6
- flake8 mltrainer
7
- mypy mltrainer
@@ -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