substrax 0.1.0__py3-none-any.whl
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.
- substrax/__init__.py +13 -0
- substrax/callbacks/__init__.py +18 -0
- substrax/callbacks/base.py +158 -0
- substrax/callbacks/early_stopping.py +116 -0
- substrax/callbacks/plateau.py +121 -0
- substrax/checkpoint/__init__.py +6 -0
- substrax/checkpoint/checkpoint_store.py +437 -0
- substrax/devices/__init__.py +24 -0
- substrax/devices/info.py +96 -0
- substrax/devices/placement.py +466 -0
- substrax/mesh/__init__.py +38 -0
- substrax/mesh/device_mesh.py +161 -0
- substrax/mesh/rules.py +130 -0
- substrax/mesh/strategies.py +513 -0
- substrax/py.typed +0 -0
- substrax/spmd/__init__.py +38 -0
- substrax/spmd/collectives.py +251 -0
- substrax/spmd/data_parallel.py +142 -0
- substrax/tracking/__init__.py +19 -0
- substrax/tracking/_optional.py +27 -0
- substrax/tracking/_plots.py +72 -0
- substrax/tracking/logger.py +373 -0
- substrax/tracking/mlflow.py +215 -0
- substrax/tracking/wandb.py +170 -0
- substrax/typing.py +12 -0
- substrax-0.1.0.dist-info/METADATA +322 -0
- substrax-0.1.0.dist-info/RECORD +29 -0
- substrax-0.1.0.dist-info/WHEEL +4 -0
- substrax-0.1.0.dist-info/licenses/LICENSE +21 -0
substrax/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Substrax: shared training and hardware infrastructure for the Avitai JAX stack.
|
|
2
|
+
|
|
3
|
+
The package sits below calibrax, datarax, artifex and opifex and holds the code those
|
|
4
|
+
packages used to carry separately: device information and placement (``devices``), device
|
|
5
|
+
meshes and sharding strategies (``mesh``), data-parallel training helpers (``spmd``), one
|
|
6
|
+
Orbax checkpoint store (``checkpoint``), training callbacks (``callbacks``) and step-wise
|
|
7
|
+
experiment tracking (``tracking``). Import from the subpackages; this module exposes only
|
|
8
|
+
the version.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Training callbacks, best-metric tracking and early stopping."""
|
|
2
|
+
|
|
3
|
+
from substrax.callbacks.base import BaseCallback, CallbackList, TrainerLike, TrainingCallback
|
|
4
|
+
from substrax.callbacks.early_stopping import EarlyStoppingCallback, EarlyStoppingConfig
|
|
5
|
+
from substrax.callbacks.plateau import BestMetricTracker, EarlyStopping, PlateauMode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseCallback",
|
|
10
|
+
"BestMetricTracker",
|
|
11
|
+
"CallbackList",
|
|
12
|
+
"EarlyStopping",
|
|
13
|
+
"EarlyStoppingCallback",
|
|
14
|
+
"EarlyStoppingConfig",
|
|
15
|
+
"PlateauMode",
|
|
16
|
+
"TrainerLike",
|
|
17
|
+
"TrainingCallback",
|
|
18
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""The training-callback protocol, its no-op base and the dispatching list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from flax import nnx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class TrainerLike(Protocol):
|
|
13
|
+
"""What a callback may ask of the trainer that drives it."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def model(self) -> nnx.Module:
|
|
17
|
+
"""The model being trained."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class TrainingCallback(Protocol):
|
|
23
|
+
"""Lifecycle hooks a training loop fires; implement any subset via ``BaseCallback``.
|
|
24
|
+
|
|
25
|
+
Parameters are positional-only so implementations may name them as they like.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def on_train_begin(self, trainer: TrainerLike, /) -> None:
|
|
29
|
+
"""Called once before the first epoch."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def on_train_end(self, trainer: TrainerLike, /) -> None:
|
|
33
|
+
"""Called once after the last epoch."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def on_epoch_begin(self, trainer: TrainerLike, epoch: int, /) -> None:
|
|
37
|
+
"""Called before each epoch."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def on_epoch_end(self, trainer: TrainerLike, epoch: int, logs: dict[str, Any], /) -> None:
|
|
41
|
+
"""Called after each epoch with that epoch's metrics."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def on_batch_begin(self, trainer: TrainerLike, batch: int, /) -> None:
|
|
45
|
+
"""Called before each batch."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def on_batch_end(self, trainer: TrainerLike, batch: int, logs: dict[str, Any], /) -> None:
|
|
49
|
+
"""Called after each batch with that batch's metrics."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def on_validation_begin(self, trainer: TrainerLike, /) -> None:
|
|
53
|
+
"""Called before a validation pass."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def on_validation_end(self, trainer: TrainerLike, logs: dict[str, Any], /) -> None:
|
|
57
|
+
"""Called after a validation pass with its metrics."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BaseCallback:
|
|
62
|
+
"""No-op implementation of every hook; subclasses override what they need."""
|
|
63
|
+
|
|
64
|
+
__slots__ = ()
|
|
65
|
+
|
|
66
|
+
def on_train_begin(self, _trainer: TrainerLike) -> None:
|
|
67
|
+
"""Called once before the first epoch."""
|
|
68
|
+
|
|
69
|
+
def on_train_end(self, _trainer: TrainerLike) -> None:
|
|
70
|
+
"""Called once after the last epoch."""
|
|
71
|
+
|
|
72
|
+
def on_epoch_begin(self, _trainer: TrainerLike, _epoch: int) -> None:
|
|
73
|
+
"""Called before each epoch."""
|
|
74
|
+
|
|
75
|
+
def on_epoch_end(self, _trainer: TrainerLike, _epoch: int, _logs: dict[str, Any]) -> None:
|
|
76
|
+
"""Called after each epoch with that epoch's metrics."""
|
|
77
|
+
|
|
78
|
+
def on_batch_begin(self, _trainer: TrainerLike, _batch: int) -> None:
|
|
79
|
+
"""Called before each batch."""
|
|
80
|
+
|
|
81
|
+
def on_batch_end(self, _trainer: TrainerLike, _batch: int, _logs: dict[str, Any]) -> None:
|
|
82
|
+
"""Called after each batch with that batch's metrics."""
|
|
83
|
+
|
|
84
|
+
def on_validation_begin(self, _trainer: TrainerLike) -> None:
|
|
85
|
+
"""Called before a validation pass."""
|
|
86
|
+
|
|
87
|
+
def on_validation_end(self, _trainer: TrainerLike, _logs: dict[str, Any]) -> None:
|
|
88
|
+
"""Called after a validation pass with its metrics."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CallbackList:
|
|
92
|
+
"""Dispatches every hook to its callbacks in registration order."""
|
|
93
|
+
|
|
94
|
+
__slots__ = ("_callbacks",)
|
|
95
|
+
|
|
96
|
+
def __init__(self, callbacks: Iterable[TrainingCallback] | None = None) -> None:
|
|
97
|
+
"""Wrap an initial set of callbacks.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
callbacks: The callbacks to start with; each receives every hook, in this order.
|
|
101
|
+
"""
|
|
102
|
+
self._callbacks: list[TrainingCallback] = list(callbacks) if callbacks else []
|
|
103
|
+
|
|
104
|
+
def add(self, callback: TrainingCallback) -> None:
|
|
105
|
+
"""Append a callback."""
|
|
106
|
+
self._callbacks.append(callback)
|
|
107
|
+
|
|
108
|
+
def remove(self, callback: TrainingCallback) -> None:
|
|
109
|
+
"""Remove a callback; raises ``ValueError`` if it was never added."""
|
|
110
|
+
self._callbacks.remove(callback)
|
|
111
|
+
|
|
112
|
+
def __len__(self) -> int:
|
|
113
|
+
"""The number of callbacks."""
|
|
114
|
+
return len(self._callbacks)
|
|
115
|
+
|
|
116
|
+
def __iter__(self) -> Iterator[TrainingCallback]:
|
|
117
|
+
"""Iterate over the callbacks in dispatch order."""
|
|
118
|
+
return iter(self._callbacks)
|
|
119
|
+
|
|
120
|
+
def on_train_begin(self, trainer: TrainerLike) -> None:
|
|
121
|
+
"""Dispatch ``on_train_begin``."""
|
|
122
|
+
for callback in self._callbacks:
|
|
123
|
+
callback.on_train_begin(trainer)
|
|
124
|
+
|
|
125
|
+
def on_train_end(self, trainer: TrainerLike) -> None:
|
|
126
|
+
"""Dispatch ``on_train_end``."""
|
|
127
|
+
for callback in self._callbacks:
|
|
128
|
+
callback.on_train_end(trainer)
|
|
129
|
+
|
|
130
|
+
def on_epoch_begin(self, trainer: TrainerLike, epoch: int) -> None:
|
|
131
|
+
"""Dispatch ``on_epoch_begin``."""
|
|
132
|
+
for callback in self._callbacks:
|
|
133
|
+
callback.on_epoch_begin(trainer, epoch)
|
|
134
|
+
|
|
135
|
+
def on_epoch_end(self, trainer: TrainerLike, epoch: int, logs: dict[str, Any]) -> None:
|
|
136
|
+
"""Dispatch ``on_epoch_end``."""
|
|
137
|
+
for callback in self._callbacks:
|
|
138
|
+
callback.on_epoch_end(trainer, epoch, logs)
|
|
139
|
+
|
|
140
|
+
def on_batch_begin(self, trainer: TrainerLike, batch: int) -> None:
|
|
141
|
+
"""Dispatch ``on_batch_begin``."""
|
|
142
|
+
for callback in self._callbacks:
|
|
143
|
+
callback.on_batch_begin(trainer, batch)
|
|
144
|
+
|
|
145
|
+
def on_batch_end(self, trainer: TrainerLike, batch: int, logs: dict[str, Any]) -> None:
|
|
146
|
+
"""Dispatch ``on_batch_end``."""
|
|
147
|
+
for callback in self._callbacks:
|
|
148
|
+
callback.on_batch_end(trainer, batch, logs)
|
|
149
|
+
|
|
150
|
+
def on_validation_begin(self, trainer: TrainerLike) -> None:
|
|
151
|
+
"""Dispatch ``on_validation_begin``."""
|
|
152
|
+
for callback in self._callbacks:
|
|
153
|
+
callback.on_validation_begin(trainer)
|
|
154
|
+
|
|
155
|
+
def on_validation_end(self, trainer: TrainerLike, logs: dict[str, Any]) -> None:
|
|
156
|
+
"""Dispatch ``on_validation_end``."""
|
|
157
|
+
for callback in self._callbacks:
|
|
158
|
+
callback.on_validation_end(trainer, logs)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Early stopping callback driven by epoch logs.
|
|
2
|
+
|
|
3
|
+
Monitors a metric in the epoch logs and stops training when it stops improving, reaches a
|
|
4
|
+
goal, diverges, or becomes non-finite. The best-so-far and stagnation bookkeeping is
|
|
5
|
+
``BestMetricTracker``; this class adds the log lookup, the thresholds and the epoch record.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from substrax.callbacks.base import BaseCallback, TrainerLike
|
|
15
|
+
from substrax.callbacks.plateau import BestMetricTracker, PlateauMode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
19
|
+
class EarlyStoppingConfig:
|
|
20
|
+
"""Configuration for early stopping.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
monitor: Metric name to monitor (e.g., "val_loss", "accuracy").
|
|
24
|
+
min_delta: Minimum change to qualify as an improvement.
|
|
25
|
+
patience: Number of epochs with no improvement before stopping.
|
|
26
|
+
mode: "min" if lower is better, "max" if higher is better.
|
|
27
|
+
check_finite: If True, stop when metric becomes NaN or Inf.
|
|
28
|
+
stopping_threshold: Stop immediately when metric reaches this value.
|
|
29
|
+
divergence_threshold: Stop if metric exceeds this value (min mode only).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
monitor: str = "val_loss"
|
|
33
|
+
min_delta: float = 0.0
|
|
34
|
+
patience: int = 10
|
|
35
|
+
mode: Literal["min", "max"] = "min"
|
|
36
|
+
check_finite: bool = True
|
|
37
|
+
stopping_threshold: float | None = None
|
|
38
|
+
divergence_threshold: float | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EarlyStoppingCallback(BaseCallback):
|
|
42
|
+
"""Stop training when a monitored metric stops improving."""
|
|
43
|
+
|
|
44
|
+
__slots__ = ("_stopped_epoch", "_tracker", "config")
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: EarlyStoppingConfig) -> None:
|
|
47
|
+
"""Initialize early stopping callback.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Early stopping configuration.
|
|
51
|
+
"""
|
|
52
|
+
self.config = config
|
|
53
|
+
self._tracker = BestMetricTracker(mode=config.mode, min_delta=config.min_delta)
|
|
54
|
+
self._stopped_epoch: int | None = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def wait_count(self) -> int:
|
|
58
|
+
"""Epochs since the last improvement."""
|
|
59
|
+
return self._tracker.num_bad_epochs
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def best_score(self) -> float | None:
|
|
63
|
+
"""The best monitored value so far, or ``None`` before the metric first appears."""
|
|
64
|
+
best = self._tracker.best
|
|
65
|
+
return None if math.isinf(best) else best
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def stopped_epoch(self) -> int | None:
|
|
69
|
+
"""The epoch at which stopping was decided, if it has been."""
|
|
70
|
+
return self._stopped_epoch
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def should_stop(self) -> bool:
|
|
74
|
+
"""Whether training should stop."""
|
|
75
|
+
return self._stopped_epoch is not None
|
|
76
|
+
|
|
77
|
+
def on_epoch_end(self, _trainer: TrainerLike, epoch: int, logs: dict[str, Any]) -> None:
|
|
78
|
+
"""Read the monitored metric, if present, and decide whether to stop.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
_trainer: The trainer instance (unused).
|
|
82
|
+
epoch: Current epoch number.
|
|
83
|
+
logs: Dictionary of metrics from this epoch.
|
|
84
|
+
"""
|
|
85
|
+
value = logs.get(self.config.monitor)
|
|
86
|
+
if value is None:
|
|
87
|
+
return
|
|
88
|
+
current = float(value)
|
|
89
|
+
if self._stops_immediately(current):
|
|
90
|
+
self._stopped_epoch = epoch
|
|
91
|
+
return
|
|
92
|
+
self._tracker.register(current)
|
|
93
|
+
if self._tracker.num_bad_epochs >= self.config.patience:
|
|
94
|
+
self._stopped_epoch = epoch
|
|
95
|
+
|
|
96
|
+
def _stops_immediately(self, current: float) -> bool:
|
|
97
|
+
"""Whether ``current`` ends training before patience is considered."""
|
|
98
|
+
if self.config.check_finite and not math.isfinite(current):
|
|
99
|
+
return True
|
|
100
|
+
return self._meets_threshold(current) or self._diverged(current)
|
|
101
|
+
|
|
102
|
+
def _meets_threshold(self, current: float) -> bool:
|
|
103
|
+
"""Whether ``current`` reaches the stopping threshold (the training goal)."""
|
|
104
|
+
threshold = self.config.stopping_threshold
|
|
105
|
+
if threshold is None:
|
|
106
|
+
return False
|
|
107
|
+
if self._tracker.mode is PlateauMode.MIN:
|
|
108
|
+
return current <= threshold
|
|
109
|
+
return current >= threshold
|
|
110
|
+
|
|
111
|
+
def _diverged(self, current: float) -> bool:
|
|
112
|
+
"""Whether ``current`` exceeds the divergence threshold in min mode."""
|
|
113
|
+
threshold = self.config.divergence_threshold
|
|
114
|
+
return (
|
|
115
|
+
threshold is not None and self._tracker.mode is PlateauMode.MIN and current > threshold
|
|
116
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Metric-driven early stopping.
|
|
2
|
+
|
|
3
|
+
Reacts to a stream of per-epoch validation metrics. Framework agnostic (pure-Python
|
|
4
|
+
scalar bookkeeping, no JAX state), so it composes with any training loop.
|
|
5
|
+
|
|
6
|
+
Semantics follow the established reference: :class:`EarlyStopping` mirrors the
|
|
7
|
+
Keras / Lightning callback (stop after ``patience`` epochs without a ``min_delta``
|
|
8
|
+
improvement). Learning-rate plateau decay is ``optax.contrib.reduce_on_plateau``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlateauMode(StrEnum):
|
|
17
|
+
"""Whether a monitored metric improves by decreasing or increasing."""
|
|
18
|
+
|
|
19
|
+
MIN = "min"
|
|
20
|
+
MAX = "max"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BestMetricTracker:
|
|
24
|
+
"""Shared best-so-far + stagnation bookkeeping for the plateau callbacks.
|
|
25
|
+
|
|
26
|
+
Tracks the best monitored value and the number of consecutive updates without
|
|
27
|
+
a ``min_delta`` improvement, in either ``"min"`` or ``"max"`` mode.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *, mode: PlateauMode | str, min_delta: float) -> None:
|
|
31
|
+
"""Initialise the tracker.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mode: ``"min"`` (lower is better) or ``"max"`` (higher is better).
|
|
35
|
+
min_delta: Minimum absolute change counted as an improvement.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: If ``min_delta`` is negative.
|
|
39
|
+
"""
|
|
40
|
+
if min_delta < 0.0:
|
|
41
|
+
raise ValueError(f"min_delta must be non-negative, got {min_delta}.")
|
|
42
|
+
self._mode = PlateauMode(mode)
|
|
43
|
+
self._min_delta = float(min_delta)
|
|
44
|
+
self._best = float("inf") if self._mode is PlateauMode.MIN else float("-inf")
|
|
45
|
+
self._num_bad_epochs = 0
|
|
46
|
+
|
|
47
|
+
def _is_improvement(self, value: float) -> bool:
|
|
48
|
+
"""Return whether ``value`` beats the best by at least ``min_delta``."""
|
|
49
|
+
if self._mode is PlateauMode.MIN:
|
|
50
|
+
return value < self._best - self._min_delta
|
|
51
|
+
return value > self._best + self._min_delta
|
|
52
|
+
|
|
53
|
+
def register(self, value: float) -> bool:
|
|
54
|
+
"""Record ``value``; return ``True`` if it improves on the best so far.
|
|
55
|
+
|
|
56
|
+
Updates the best value and resets the stagnation counter on an
|
|
57
|
+
improvement; otherwise increments the stagnation counter.
|
|
58
|
+
"""
|
|
59
|
+
if self._is_improvement(value):
|
|
60
|
+
self._best = float(value)
|
|
61
|
+
self._num_bad_epochs = 0
|
|
62
|
+
return True
|
|
63
|
+
self._num_bad_epochs += 1
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def mode(self) -> PlateauMode:
|
|
68
|
+
"""The improvement direction."""
|
|
69
|
+
return self._mode
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def best(self) -> float:
|
|
73
|
+
"""The best monitored value seen so far."""
|
|
74
|
+
return self._best
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def num_bad_epochs(self) -> int:
|
|
78
|
+
"""Consecutive updates without a ``min_delta`` improvement."""
|
|
79
|
+
return self._num_bad_epochs
|
|
80
|
+
|
|
81
|
+
def _reset_stagnation(self) -> None:
|
|
82
|
+
"""Clear the stagnation counter (e.g. after an LR reduction)."""
|
|
83
|
+
self._num_bad_epochs = 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class EarlyStopping(BestMetricTracker):
|
|
87
|
+
"""Signal to stop once a monitored metric stops improving."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
patience: int,
|
|
93
|
+
min_delta: float = 0.0,
|
|
94
|
+
mode: PlateauMode | str = PlateauMode.MIN,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Initialise the stopper.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
patience: Epochs without a ``min_delta`` improvement before stopping.
|
|
100
|
+
min_delta: Minimum absolute change counted as an improvement.
|
|
101
|
+
mode: ``"min"`` (lower is better) or ``"max"`` (higher is better).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If ``patience`` is not positive.
|
|
105
|
+
"""
|
|
106
|
+
super().__init__(mode=mode, min_delta=min_delta)
|
|
107
|
+
if patience < 1:
|
|
108
|
+
raise ValueError(f"patience must be >= 1, got {patience}.")
|
|
109
|
+
self._patience = patience
|
|
110
|
+
|
|
111
|
+
def update(self, value: float) -> bool:
|
|
112
|
+
"""Record the latest metric; return ``True`` if it improved on the best."""
|
|
113
|
+
return self.register(value)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def should_stop(self) -> bool:
|
|
117
|
+
"""Whether the metric has stagnated for ``patience`` epochs."""
|
|
118
|
+
return self._num_bad_epochs >= self._patience
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = ["BestMetricTracker", "EarlyStopping", "PlateauMode"]
|