Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions deeptab/models/_mixins/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def _emit_event(self, event: str, **kwargs: Any) -> None: ...
# Model construction
# ------------------------------------------------------------------

def _mark_unfitted(self) -> None:
"""Forget a previous ``fit()``: its fitted flag and its best checkpoint."""
if getattr(self, "is_fitted_", False):
self.is_fitted_ = False
self._best_model_path = None

def _build_model(
self,
X,
Expand Down Expand Up @@ -113,6 +119,9 @@ def _build_model(
mode: str | None = None,
):
"""Builds the model using the provided training data."""
# The rebuilt network has untrained weights, so a previous fit() no
# longer describes this estimator until it is fitted again.
self._mark_unfitted()
if dataloader_kwargs is None:
dataloader_kwargs = {}

Expand Down
7 changes: 6 additions & 1 deletion deeptab/models/classifier_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,13 @@ def build_model(
Returns
-------
self : object
The built classifier.
The built classifier. Its weights are untrained, so the estimator
counts as unfitted (``predict()`` raises ``NotFittedError``) until
``fit()`` completes.
"""
# Forget the previous fit before classes_ changes, so that a build that
# fails below doesn't leave a fitted-looking estimator behind.
self._mark_unfitted()

classes = np.unique(y)
num_classes = len(classes)
Expand Down
7 changes: 6 additions & 1 deletion deeptab/models/lss_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,13 @@ def build_model(
Returns
-------
self : object
The built distributional regressor.
The built distributional regressor. Its weights are untrained, so
the estimator counts as unfitted (``predict()`` raises
``NotFittedError``) until ``fit()`` completes.
"""
# The rebuilt network has untrained weights, so a previous fit() no
# longer describes this estimator until it is fitted again.
self._mark_unfitted()
if dataloader_kwargs is None:
dataloader_kwargs = {}
self._configure_distribution(y, family, distributional_kwargs)
Expand Down
4 changes: 3 additions & 1 deletion deeptab/models/regressor_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ def build_model(
Returns
-------
self : object
The built regressor.
The built regressor. Its weights are untrained, so the estimator
counts as unfitted (``predict()`` raises ``NotFittedError``) until
``fit()`` completes.
"""

return super()._build_model(
Expand Down
65 changes: 65 additions & 0 deletions tests/integration/test_build_model_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""build_model() replaces the network with untrained weights, so the estimator
must not keep counting as fitted from an earlier fit()."""

from __future__ import annotations

from typing import Any

import numpy as np
import pandas as pd
import pytest

from deeptab.core.exceptions import NotFittedError
from deeptab.models import MLPLSS, MLPClassifier, MLPRegressor

RANDOM_STATE = 0

FIT_KWARGS: dict[str, Any] = {"max_epochs": 1, "batch_size": 32}


def _make_reg_data():
rng = np.random.default_rng(RANDOM_STATE)
X = rng.standard_normal((120, 4))
y = rng.standard_normal(120)
return pd.DataFrame(X, columns=[f"f{i}" for i in range(4)]), y # type: ignore[call-overload]


def test_regressor_is_unfitted_after_rebuilding(tmp_path):
X, y = _make_reg_data()
reg = MLPRegressor().fit(X, y, random_state=RANDOM_STATE, checkpoint_path=str(tmp_path), **FIT_KWARGS)
assert reg.is_fitted_ is True
assert reg._best_model_path

reg.build_model(X, y)
assert reg.is_fitted_ is False
assert reg._best_model_path is None
with pytest.raises(NotFittedError):
reg.predict(X)

reg.fit(X, y, rebuild=False, checkpoint_path=str(tmp_path), **FIT_KWARGS)
assert reg.is_fitted_ is True


def test_lss_is_unfitted_after_rebuilding(tmp_path):
X, y = _make_reg_data()
model = MLPLSS().fit(X, y, family="normal", random_state=RANDOM_STATE, checkpoint_path=str(tmp_path), **FIT_KWARGS)
assert model.is_fitted_ is True

model.build_model(X, y, family="normal")
assert model.is_fitted_ is False
with pytest.raises(NotFittedError):
model.predict(X)


def test_classifier_is_unfitted_when_rebuilding_fails(tmp_path):
X, _ = _make_reg_data()
y = np.arange(len(X)) % 3
clf = MLPClassifier().fit(X, y, random_state=RANDOM_STATE, checkpoint_path=str(tmp_path), **FIT_KWARGS)
assert clf.is_fitted_ is True

two_classes = y % 2
with pytest.raises(ValueError, match="not seen during fit"):
clf.build_model(X[:100], two_classes[:100], X_val=X[100:], y_val=y[100:])
assert clf.is_fitted_ is False
with pytest.raises(NotFittedError):
clf.predict(X)
Loading