Skip to content

Commit 498488c

Browse files
authored
fix: trainer uses best checkpoint (#352)
* fix: trainer uses best checkpoint * fix: trainer
1 parent fa1bddd commit 498488c

3 files changed

Lines changed: 19 additions & 14 deletions

File tree

model2vec/train/trainer.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def _run_validation(
8888
return {key: total / total_samples for key, total in weighted_sums.items()}
8989

9090

91-
def run_training_loop(
91+
def run_training_loop( # noqa: C901
9292
model: nn.Module,
9393
loss_function: nn.Module,
9494
learning_rate: float,
@@ -121,7 +121,7 @@ def run_training_loop(
121121
:param val_check_interval: If set, validate every this many training steps.
122122
:param check_val_every_epoch: If set, validate every this many epochs.
123123
:param compute_metrics: Computes validation metrics from `(head_out, y, loss)`. Defaults to just `val_loss`.
124-
:return: The model's state dict as of the last validation check before training stopped.
124+
:return: The model's state dict from the validation check with the best `val_metric`.
125125
"""
126126
model.to(device)
127127
loss_function.to(device)
@@ -144,17 +144,22 @@ def run_training_loop(
144144

145145
max_epochs = _resolve_max_epochs(max_epochs)
146146

147-
last_checkpoint = copy.deepcopy(model.state_dict())
147+
best_checkpoint = copy.deepcopy(model.state_dict())
148+
best_val_metric = float("inf") if early_stopping_direction == "min" else float("-inf")
148149
current_epoch = 0
149150
global_step = 0
150151
postfix: dict[str, str] = {}
151152
latest_val_loss: float | None = None
152153

153154
def validate_and_checkpoint() -> bool:
154-
nonlocal last_checkpoint, latest_val_loss
155+
nonlocal best_checkpoint, best_val_metric, latest_val_loss
155156
metrics = _run_validation(model, loss_function, compute_metrics, val_loader, device)
156157
latest_val_loss = metrics["val_loss"]
157-
last_checkpoint = copy.deepcopy(model.state_dict())
158+
current = metrics[val_metric]
159+
improved = current < best_val_metric if early_stopping_direction == "min" else current > best_val_metric
160+
if improved:
161+
best_val_metric = current
162+
best_checkpoint = copy.deepcopy(model.state_dict())
158163
postfix.update({key: f"{value:.4f}" for key, value in metrics.items()})
159164
if early_stopper is None:
160165
return False
@@ -178,17 +183,17 @@ def validate_and_checkpoint() -> bool:
178183
should_stop = validate_and_checkpoint()
179184
pbar.set_postfix(postfix)
180185
if should_stop and (min_epochs is None or current_epoch >= min_epochs):
181-
return last_checkpoint
186+
return best_checkpoint
182187

183188
current_epoch += 1
184189

185190
if check_val_every_epoch is not None and current_epoch % check_val_every_epoch == 0:
186191
should_stop = validate_and_checkpoint()
187192
pbar.set_postfix(postfix)
188193
if should_stop and (min_epochs is None or current_epoch >= min_epochs):
189-
return last_checkpoint
194+
return best_checkpoint
190195

191196
_step_plateau_scheduler(scheduler, latest_val_loss)
192197

193198
if current_epoch >= max_epochs:
194-
return last_checkpoint
199+
return best_checkpoint

tests/test_inference.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ def test_init_predict(mock_inference_pipeline: StaticModelPipeline) -> None:
2323
if mock_inference_pipeline.head.activation == Activation.SIGMOID:
2424
assert mock_inference_pipeline.classes_ is not None
2525
if isinstance(mock_inference_pipeline.classes_[0], str):
26-
target = [["a", "b"]]
26+
target = [["b"]]
2727
else:
28-
target = [[0, 1]] # type: ignore
28+
target = [[1]] # type: ignore
2929
else:
3030
assert mock_inference_pipeline.classes_ is not None
3131
if isinstance(mock_inference_pipeline.classes_[0], str):
@@ -82,9 +82,9 @@ def test_roundtrip_save(mock_inference_pipeline: StaticModelPipeline) -> None:
8282
if mock_inference_pipeline.head.activation == Activation.SIGMOID:
8383
assert mock_inference_pipeline.classes_ is not None
8484
if isinstance(mock_inference_pipeline.classes_[0], str):
85-
target = [["a", "b"]]
85+
target = [["b"]]
8686
else:
87-
target = [[0, 1]] # type: ignore
87+
target = [[1]] # type: ignore
8888
else:
8989
assert mock_inference_pipeline.classes_ is not None
9090
if isinstance(mock_inference_pipeline.classes_[0], str):

tests/test_trainable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ def test_predict(mock_trained_pipeline: StaticModelForClassification) -> None:
152152
result = mock_trained_pipeline.predict(["dog cat", "dog"]).tolist()
153153
if mock_trained_pipeline.multilabel:
154154
if type(mock_trained_pipeline.classes_[0]) == str:
155-
assert result == [["a", "b"], ["a", "b"]]
155+
assert result == [["b"], ["b"]]
156156
else:
157-
assert result == [[0, 1], [0, 1]]
157+
assert result == [[1], [1]]
158158
else:
159159
if type(mock_trained_pipeline.classes_[0]) == str:
160160
assert result == ["b", "b"]

0 commit comments

Comments
 (0)