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
52 changes: 35 additions & 17 deletions deep_river/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class DeepEstimator(base.Estimator):

def __init__(
self,
module: torch.nn.Module,
module: Union[torch.nn.Module, Callable[[], torch.nn.Module]],
loss_fn: Union[str, Callable] = "mse",
optimizer_fn: Union[str, Callable] = "sgd",
lr: float = 1e-3,
Expand All @@ -127,7 +127,11 @@ def __init__(
**kwargs,
):
super().__init__()
self.module = module
self.module = (
module
if isinstance(module, torch.nn.Module)
else self._initialize_module(module, seed)
)
self.lr = lr
self.loss_func = get_loss_fn(loss_fn)
self.loss_fn = loss_fn
Expand Down Expand Up @@ -166,7 +170,14 @@ def __init__(
self.module_input_len = self._get_input_size() if self.input_layer else None
self.observed_features: SortedSet = SortedSet()
self.module.to(self.device)
torch.manual_seed(seed)

@staticmethod
def _initialize_module(
module_fn: Callable[..., torch.nn.Module], seed: int, *args, **kwargs
) -> torch.nn.Module:
with torch.random.fork_rng():
torch.manual_seed(seed)
return module_fn(*args, **kwargs)

@staticmethod
def _extract_candidate_layers(module: torch.nn.Module) -> list[torch.nn.Module]:
Expand Down Expand Up @@ -540,8 +551,10 @@ def load(cls, filepath: Union[str, Path]):
if "module" in init_params and isinstance(init_params["module"], dict):
module_info = init_params.pop("module")
module_cls = cls._import_from_path(module_info["class"])
module = module_cls(
**cls._filter_kwargs(module_cls.__init__, module_info["kwargs"])
module = cls._initialize_module(
module_cls,
init_params.get("seed", 42),
**cls._filter_kwargs(module_cls.__init__, module_info["kwargs"]),
)
if state.get("model_state_dict"):
module.load_state_dict(state["model_state_dict"])
Expand Down Expand Up @@ -585,7 +598,7 @@ def clone(
params = {**self._get_all_init_params(), **new_params}

if "module" not in new_params:
params["module"] = self._rebuild_module()
params["module"] = self._rebuild_module(seed=params.get("seed", self.seed))

new_est = self.__class__(**self._filter_kwargs(self.__class__.__init__, params))

Expand Down Expand Up @@ -670,22 +683,27 @@ def _infer_module_params(self) -> dict:

return params

def _rebuild_module(self):
def _rebuild_module(self, seed: Optional[int] = None):
"""Create a fresh (re‑initialised) copy of the wrapped module."""
seed = self.seed if seed is None else seed
params = self._infer_module_params()
try:
return self.module.__class__(
**self._filter_kwargs(self.module.__class__.__init__, params)
return self._initialize_module(
self.module.__class__,
seed,
**self._filter_kwargs(self.module.__class__.__init__, params),
)
except Exception: # noqa: E722
mod_copy = copy.deepcopy(self.module)
for m in mod_copy.modules():
if hasattr(m, "reset_parameters"):
try:
m.reset_parameters()
except Exception: # noqa: E722
pass
return mod_copy
with torch.random.fork_rng():
torch.manual_seed(seed)
mod_copy = copy.deepcopy(self.module)
for m in mod_copy.modules():
if hasattr(m, "reset_parameters"):
try:
m.reset_parameters()
except Exception: # noqa: E722
pass
return mod_copy

@staticmethod
def _import_from_path(path: str):
Expand Down
19 changes: 14 additions & 5 deletions deep_river/classification/zoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ def __init__(
):
self.n_features = n_features
self.n_init_classes = n_init_classes
module = LogisticRegression.LRModule(
n_features=n_features, n_init_classes=n_init_classes
module = self._initialize_module(
LogisticRegression.LRModule,
seed,
n_features=n_features,
n_init_classes=n_init_classes,
)
if "module" in kwargs:
del kwargs["module"]
Expand Down Expand Up @@ -213,7 +216,9 @@ def __init__(
self.n_width = n_width
self.n_layers = n_layers
self.n_init_classes = n_init_classes
module = MultiLayerPerceptron.MLPModule(
module = self._initialize_module(
MultiLayerPerceptron.MLPModule,
seed,
n_width=n_width,
n_layers=n_layers,
n_features=n_features,
Expand Down Expand Up @@ -354,7 +359,9 @@ def __init__(
self.n_features = n_features
self.hidden_size = hidden_size
self.n_init_classes = n_init_classes
module = LSTMClassifier.LSTMModule(
module = self._initialize_module(
LSTMClassifier.LSTMModule,
seed,
n_features=n_features,
hidden_size=hidden_size,
n_init_classes=n_init_classes,
Expand Down Expand Up @@ -509,7 +516,9 @@ def __init__(
self.num_layers = num_layers
self.nonlinearity = nonlinearity
self.n_init_classes = n_init_classes
module = RNNClassifier.RNNModule(
module = self._initialize_module(
RNNClassifier.RNNModule,
seed,
n_features=n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down
30 changes: 23 additions & 7 deletions deep_river/forecasting/zoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ def __init__(
**kwargs,
):
self.n_features = n_features
module = LinearForecaster.LinearModule(input_size=window_size + n_features)
module = self._initialize_module(
LinearForecaster.LinearModule,
seed,
input_size=window_size + n_features,
)
kwargs.pop("module", None)
super().__init__(
module=module,
Expand Down Expand Up @@ -90,7 +94,9 @@ def __init__(
self.n_features = n_features
self.n_width = n_width
self.n_layers = n_layers
module = MLPForecaster.MLPModule(
module = self._initialize_module(
MLPForecaster.MLPModule,
seed,
input_size=window_size + n_features,
n_width=n_width,
n_layers=n_layers,
Expand Down Expand Up @@ -175,7 +181,9 @@ def __init__(
self.num_layers = num_layers
self.nonlinearity = nonlinearity
self.dropout = dropout
module = RNNForecaster.RNNModule(
module = self._initialize_module(
RNNForecaster.RNNModule,
seed,
input_size=1 + n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down Expand Up @@ -251,7 +259,9 @@ def __init__(
self.hidden_size = hidden_size
self.num_layers = num_layers
self.dropout = dropout
module = GRUForecaster.GRUModule(
module = self._initialize_module(
GRUForecaster.GRUModule,
seed,
input_size=1 + n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down Expand Up @@ -326,7 +336,9 @@ def __init__(
self.hidden_size = hidden_size
self.num_layers = num_layers
self.dropout = dropout
module = LSTMForecaster.LSTMModule(
module = self._initialize_module(
LSTMForecaster.LSTMModule,
seed,
input_size=1 + n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down Expand Up @@ -460,7 +472,9 @@ def __init__(
self.num_layers = num_layers
self.dropout = dropout
self.time_delta = time_delta
module = LiquidForecaster.LiquidModule(
module = self._initialize_module(
LiquidForecaster.LiquidModule,
seed,
input_size=1 + n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down Expand Up @@ -569,7 +583,9 @@ def __init__(
self.n_width = n_width
self.n_layers = n_layers
self.n_blocks = n_blocks
module = NBEATSForecaster.NBEATSModule(
module = self._initialize_module(
NBEATSForecaster.NBEATSModule,
seed,
input_size=window_size + n_features,
n_width=n_width,
n_layers=n_layers,
Expand Down
20 changes: 15 additions & 5 deletions deep_river/regression/zoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def __init__(
**kwargs,
):
self.n_features = n_features
module = LinearRegression.LRModule(n_features=n_features)
module = self._initialize_module(
LinearRegression.LRModule, seed, n_features=n_features
)
if "module" in kwargs:
del kwargs["module"]
super().__init__(
Expand Down Expand Up @@ -192,8 +194,12 @@ def __init__(
self.n_features = n_features
self.n_width = n_width
self.n_layers = n_layers
module = MultiLayerPerceptron.MLPModule(
n_features=n_features, n_layers=n_layers, n_width=n_width
module = self._initialize_module(
MultiLayerPerceptron.MLPModule,
seed,
n_features=n_features,
n_layers=n_layers,
n_width=n_width,
)
if "module" in kwargs:
del kwargs["module"]
Expand Down Expand Up @@ -321,7 +327,9 @@ def __init__(
self.num_layers = num_layers
self.dropout = dropout
self.gradient_clip_value = gradient_clip_value
module = LSTMRegressor.LSTMModule(
module = self._initialize_module(
LSTMRegressor.LSTMModule,
seed,
n_features=n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down Expand Up @@ -456,7 +464,9 @@ def __init__(
self.num_layers = num_layers
self.nonlinearity = nonlinearity
self.dropout = dropout
module = RNNRegressor.RNNModule(
module = self._initialize_module(
RNNRegressor.RNNModule,
seed,
n_features=n_features,
hidden_size=hidden_size,
num_layers=num_layers,
Expand Down
39 changes: 39 additions & 0 deletions deep_river/utils/estimator_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ def check_model_persistence(model, dataset):

# Load the model
try:
rng_state = torch.get_rng_state()
loaded_model = type(model).load(temp_path)
assert torch.equal(torch.get_rng_state(), rng_state)
assert loaded_model is not None, "Loaded model should not be None"
except (AttributeError, TypeError, RuntimeError):
# If loading fails due to module construction issues, skip this check
Expand Down Expand Up @@ -285,6 +287,42 @@ def check_feature_incremental_preservation(model):
Path(temp_path).unlink()


def check_seed_reproducibility(model):
if not hasattr(model, "module") or not hasattr(model, "seed"):
return

with torch.random.fork_rng():
initial_state = torch.get_rng_state()
first = model.clone()
assert torch.equal(torch.get_rng_state(), initial_state)

torch.rand(10)
advanced_state = torch.get_rng_state()
second = model.clone()
assert torch.equal(torch.get_rng_state(), advanced_state)

first_parameters = tuple(first.module.parameters())
second_parameters = tuple(second.module.parameters())
assert len(first_parameters) == len(second_parameters)
assert all(
torch.equal(first_parameter, second_parameter)
for first_parameter, second_parameter in zip(
first_parameters, second_parameters
)
)

different = model.clone({"seed": model.seed + 1})
assert torch.equal(torch.get_rng_state(), advanced_state)
different_parameters = tuple(different.module.parameters())
assert len(first_parameters) == len(different_parameters)
assert any(
not torch.equal(first_parameter, different_parameter)
for first_parameter, different_parameter in zip(
first_parameters, different_parameters
)
)


def yield_deep_checks(model) -> typing.Iterator[typing.Callable]:
"""Generates unit tests for a given model.

Expand All @@ -301,6 +339,7 @@ def yield_deep_checks(model) -> typing.Iterator[typing.Callable]:
yield check_model_persistence_untrained
yield check_model_persistence_with_custom_kwargs
yield check_feature_incremental_preservation
yield check_seed_reproducibility

# Classifier checks
if isinstance(model, base.Classifier) and not isinstance(
Expand Down