mltrainer 0.3.1__tar.gz → 0.3.2__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 (39) hide show
  1. mltrainer-0.3.2/.remember/archive.md +3 -0
  2. mltrainer-0.3.2/.remember/recent.md +8 -0
  3. {mltrainer-0.3.1 → mltrainer-0.3.2}/PKG-INFO +1 -1
  4. {mltrainer-0.3.1 → mltrainer-0.3.2}/pyproject.toml +1 -1
  5. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/trainer.py +34 -5
  6. mltrainer-0.3.2/tests/test_trainer_iteration.py +142 -0
  7. {mltrainer-0.3.1 → mltrainer-0.3.2}/uv.lock +1 -1
  8. mltrainer-0.3.1/.remember/tmp/save-session.pid +0 -1
  9. {mltrainer-0.3.1 → mltrainer-0.3.2}/.claude/settings.local.json +0 -0
  10. {mltrainer-0.3.1 → mltrainer-0.3.2}/.gitignore +0 -0
  11. {mltrainer-0.3.1 → mltrainer-0.3.2}/.lefthook.yml +0 -0
  12. {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/.gitignore +0 -0
  13. {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/now.md +0 -0
  14. {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-ndc.ts +0 -0
  15. {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-save-ts +0 -0
  16. {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-save.json +0 -0
  17. /mltrainer-0.3.1/.remember/today-2026-07-20.md → /mltrainer-0.3.2/.remember/today-2026-07-20.done.md +0 -0
  18. {mltrainer-0.3.1 → mltrainer-0.3.2}/Makefile +0 -0
  19. {mltrainer-0.3.1 → mltrainer-0.3.2}/README.md +0 -0
  20. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/__init__.py +0 -0
  21. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/imagemodels.py +0 -0
  22. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/metrics.py +0 -0
  23. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/preprocessors.py +0 -0
  24. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/rnn_models.py +0 -0
  25. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/settings.py +0 -0
  26. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/transformer.py +0 -0
  27. {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/vae.py +0 -0
  28. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/__init__.py +0 -0
  29. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/conftest.py +0 -0
  30. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_metrics.py +0 -0
  31. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_preprocessors.py +0 -0
  32. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_trainer.py +0 -0
  33. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_trainer_devices.py +0 -0
  34. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/unit/__init__.py +0 -0
  35. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/unit/test_trainersettings.py +0 -0
  36. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/__init__.py +0 -0
  37. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_data.py +0 -0
  38. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_metrics.py +0 -0
  39. {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_model.py +0 -0
@@ -0,0 +1,3 @@
1
+ # Archive
2
+
3
+ ```
@@ -0,0 +1,8 @@
1
+ # Recent
2
+
3
+ ```
4
+
5
+ # Recent
6
+
7
+ ## 2026-07-20
8
+ Refactored mltrainer.report() with Reporter ABC pattern (RayReporter/MLflow/TensorBoard) and added type hints w/ builtin generators, _to_device helper, plus type checking fixes. Cleaned ~115 LOC commented code and updated Makefile (ruff/ty). 47 tests passing—ready v0.3.1.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mltrainer
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: toolkit for training pytorch models
5
5
  Project-URL: GitHub, https://github.com/raoulg/mltrainer
6
6
  Author-email: "R.Grouls" <Raoul.Grouls@han.nl>
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "mltrainer"
4
- version = "0.3.1"
4
+ version = "0.3.2"
5
5
  description = "toolkit for training pytorch models"
6
6
 
7
7
  authors = [
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Callable, Iterator
2
+ from collections.abc import Callable, Iterable, Iterator
3
3
  from datetime import datetime
4
4
  from pathlib import Path
5
5
  from typing import TYPE_CHECKING, TypeVar
@@ -126,8 +126,8 @@ class Trainer:
126
126
  settings: TrainerSettings,
127
127
  loss_fn: Callable,
128
128
  optimizer: type[Optimizer],
129
- traindataloader: Iterator,
130
- validdataloader: Iterator,
129
+ traindataloader: Iterable,
130
+ validdataloader: Iterable,
131
131
  scheduler: Callable | None,
132
132
  device: str | None = None,
133
133
  ) -> None:
@@ -137,6 +137,8 @@ class Trainer:
137
137
  self.loss_fn = loss_fn
138
138
  self.traindataloader = traindataloader
139
139
  self.validdataloader = validdataloader
140
+ self._train_iter: Iterator | None = None
141
+ self._valid_iter: Iterator | None = None
140
142
  self.device = device
141
143
  self.writer: "SummaryWriter | None" = None
142
144
 
@@ -201,6 +203,33 @@ class Trainer:
201
203
  x = x.to(self.device)
202
204
  return x, y.to(self.device)
203
205
 
206
+ def _next_batch(
207
+ self, loader: Iterable, cache: str
208
+ ) -> tuple[BatchTensor, torch.Tensor]:
209
+ """Pull one batch, keeping the iterator alive across steps.
210
+
211
+ `next(iter(loader))` per step is only correct for an *iterator* -- a
212
+ streamer -- because `iter()` on an iterator returns itself. A torch
213
+ DataLoader is an *iterable*: it hands out a fresh iterator (and, with
214
+ num_workers > 0, a fresh worker pool) on every call, so a training
215
+ loader would reshuffle and replay its first batch every step, and a
216
+ validation loader would score the same first batch `valid_steps` times.
217
+
218
+ Holding the iterator and re-`iter`ing it only when it is exhausted
219
+ works for both: an infinite streamer never raises StopIteration, and a
220
+ finite DataLoader wraps around into its next epoch.
221
+ """
222
+ it = getattr(self, cache)
223
+ if it is None:
224
+ it = iter(loader)
225
+ setattr(self, cache, it)
226
+ try:
227
+ return next(it)
228
+ except StopIteration:
229
+ it = iter(loader)
230
+ setattr(self, cache, it)
231
+ return next(it)
232
+
204
233
  def __del__(self):
205
234
  """Cleanup method to ensure proper resource handling"""
206
235
  if hasattr(self, "writer") and self.writer is not None:
@@ -234,7 +263,7 @@ class Trainer:
234
263
  train_loss: float = 0.0
235
264
  train_steps = self.settings.train_steps
236
265
  for _ in tqdm(range(train_steps), colour="#1e4706"):
237
- x, y = next(iter(self.traindataloader))
266
+ x, y = self._next_batch(self.traindataloader, "_train_iter")
238
267
  x, y = self._to_device(x, y)
239
268
  self.optimizer.zero_grad()
240
269
  yhat = self.model(x)
@@ -255,7 +284,7 @@ class Trainer:
255
284
 
256
285
  with torch.no_grad(): # Prevent gradient computation during evaluation
257
286
  for _ in range(valid_steps):
258
- x, y = next(iter(self.validdataloader))
287
+ x, y = self._next_batch(self.validdataloader, "_valid_iter")
259
288
  x, y = self._to_device(x, y)
260
289
 
261
290
  # Forward pass
@@ -0,0 +1,142 @@
1
+ """Regression tests for the per-step iterator (see mltrainer P0.6).
2
+
3
+ `next(iter(loader))` inside the step loop is correct for an *iterator* but not
4
+ for an *iterable* like a torch DataLoader: it rebuilds the iterator every step,
5
+ so training replays a reshuffled first batch and validation scores the same
6
+ first batch `valid_steps` times.
7
+ """
8
+
9
+ import torch
10
+ import torch.optim as optim
11
+ from torch.utils.data import DataLoader, Dataset
12
+
13
+ from mltrainer import Trainer, TrainerSettings
14
+ from tests.utils.dummy_metrics import TestMSE
15
+
16
+
17
+ class CountingDataset(Dataset):
18
+ """Each row is its own index, so a batch is identifiable by its contents."""
19
+
20
+ def __init__(self, size: int = 64) -> None:
21
+ self.size = size
22
+
23
+ def __len__(self) -> int:
24
+ return self.size
25
+
26
+ def __getitem__(self, idx: int):
27
+ x = torch.full((10,), float(idx))
28
+ return x, x
29
+
30
+
31
+ def build_trainer(model, loss_fn, temp_logdir, train_loader, valid_loader, **kwargs):
32
+ settings = TrainerSettings(
33
+ epochs=1,
34
+ metrics=[TestMSE()],
35
+ logdir=temp_logdir,
36
+ reporttypes=[],
37
+ optimizer_kwargs={"lr": 0.01},
38
+ **kwargs,
39
+ )
40
+ return Trainer(
41
+ model=model,
42
+ settings=settings,
43
+ loss_fn=loss_fn,
44
+ optimizer=optim.Adam, # type: ignore
45
+ traindataloader=train_loader,
46
+ validdataloader=valid_loader,
47
+ scheduler=None,
48
+ device="cpu",
49
+ )
50
+
51
+
52
+ def test_valid_loader_walks_the_whole_split(model, loss_fn, temp_logdir):
53
+ """A shuffle=False DataLoader must yield a *different* batch per step."""
54
+ dataset = CountingDataset(size=64)
55
+ valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
56
+ trainer = build_trainer(
57
+ model,
58
+ loss_fn,
59
+ temp_logdir,
60
+ DataLoader(dataset, batch_size=16, shuffle=True),
61
+ valid_loader,
62
+ train_steps=1,
63
+ valid_steps=4,
64
+ )
65
+
66
+ seen = [
67
+ trainer._next_batch(valid_loader, "_valid_iter")[0][0, 0].item()
68
+ for _ in range(4)
69
+ ]
70
+ assert seen == [0.0, 16.0, 32.0, 48.0]
71
+
72
+
73
+ def test_iterator_wraps_around_at_the_end(model, loss_fn, temp_logdir):
74
+ """Asking for more steps than the loader holds restarts it, not crashes."""
75
+ dataset = CountingDataset(size=32)
76
+ valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
77
+ trainer = build_trainer(
78
+ model,
79
+ loss_fn,
80
+ temp_logdir,
81
+ DataLoader(dataset, batch_size=16, shuffle=True),
82
+ valid_loader,
83
+ train_steps=1,
84
+ valid_steps=3,
85
+ )
86
+
87
+ seen = [
88
+ trainer._next_batch(valid_loader, "_valid_iter")[0][0, 0].item()
89
+ for _ in range(3)
90
+ ]
91
+ assert seen == [0.0, 16.0, 0.0]
92
+
93
+
94
+ def test_plain_iterator_still_advances(model, loss_fn, temp_logdir):
95
+ """The old streamer path: an infinite generator, not a DataLoader."""
96
+
97
+ def stream():
98
+ i = 0
99
+ while True:
100
+ yield torch.full((4, 10), float(i)), torch.full((4, 10), float(i))
101
+ i += 1
102
+
103
+ streamer = stream()
104
+ trainer = build_trainer(
105
+ model,
106
+ loss_fn,
107
+ temp_logdir,
108
+ streamer,
109
+ streamer,
110
+ train_steps=1,
111
+ valid_steps=3,
112
+ )
113
+
114
+ seen = [
115
+ trainer._next_batch(streamer, "_valid_iter")[0][0, 0].item() for _ in range(3)
116
+ ]
117
+ assert seen == [0.0, 1.0, 2.0]
118
+
119
+
120
+ def test_validation_metrics_cover_more_than_one_batch(model, loss_fn, temp_logdir):
121
+ """End to end: evalbatches over a loader whose batches differ wildly must
122
+ not report the metric of a single batch."""
123
+ dataset = CountingDataset(size=64)
124
+ valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
125
+ trainer = build_trainer(
126
+ model,
127
+ loss_fn,
128
+ temp_logdir,
129
+ DataLoader(dataset, batch_size=16, shuffle=True),
130
+ valid_loader,
131
+ train_steps=1,
132
+ valid_steps=4,
133
+ )
134
+
135
+ metric_dict, _ = trainer.evalbatches()
136
+
137
+ # what the bug reported: the same model scored on batch 0, four times over
138
+ x, y = next(iter(valid_loader))
139
+ with torch.no_grad():
140
+ first_only = float(TestMSE()(y, trainer.model(x)))
141
+
142
+ assert abs(metric_dict[str(TestMSE())] - first_only) > 1e-3
@@ -944,7 +944,7 @@ wheels = [
944
944
 
945
945
  [[package]]
946
946
  name = "mltrainer"
947
- version = "0.3.0"
947
+ version = "0.3.2"
948
948
  source = { editable = "." }
949
949
  dependencies = [
950
950
  { name = "loguru" },
@@ -1 +0,0 @@
1
- 85305
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes