deep-river 0.2.6__tar.gz → 0.2.7__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. {deep_river-0.2.6 → deep_river-0.2.7}/PKG-INFO +36 -34
  2. {deep_river-0.2.6 → deep_river-0.2.7}/README.md +3 -2
  3. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/__version__.py +1 -1
  4. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/anomaly/ae.py +55 -27
  5. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/anomaly/probability_weighted_ae.py +14 -10
  6. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/anomaly/rolling_ae.py +16 -23
  7. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/base.py +102 -7
  8. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/classification/classifier.py +100 -148
  9. deep_river-0.2.7/deep_river/classification/rolling_classifier.py +291 -0
  10. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/classification/zoo.py +40 -28
  11. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/regression/multioutput.py +8 -7
  12. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/regression/regressor.py +32 -21
  13. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/regression/rolling_regressor.py +38 -33
  14. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/regression/zoo.py +27 -20
  15. deep_river-0.2.7/deep_river/utils/layer_adaptation.py +239 -0
  16. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/params.py +8 -7
  17. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/tensor_conversion.py +46 -23
  18. deep_river-0.2.7/deep_river/utils/test_layer_adaptation.py +179 -0
  19. deep_river-0.2.7/deep_river/utils/test_tensor_conversion.py +156 -0
  20. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river.egg-info/PKG-INFO +36 -34
  21. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river.egg-info/SOURCES.txt +2 -0
  22. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river.egg-info/requires.txt +32 -31
  23. {deep_river-0.2.6 → deep_river-0.2.7}/setup.py +14 -14
  24. deep_river-0.2.6/deep_river/classification/rolling_classifier.py +0 -605
  25. deep_river-0.2.6/deep_river/utils/test_tensor_conversion.py +0 -117
  26. {deep_river-0.2.6 → deep_river-0.2.7}/LICENSE +0 -0
  27. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/__init__.py +0 -0
  28. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/anomaly/__init__.py +0 -0
  29. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/anomaly/scaler.py +0 -0
  30. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/classification/__init__.py +0 -0
  31. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/regression/__init__.py +0 -0
  32. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/__init__.py +0 -0
  33. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/estimator_checks.py +0 -0
  34. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/hooks.py +0 -0
  35. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river/utils/test_estimators.py +0 -0
  36. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river.egg-info/dependency_links.txt +0 -0
  37. {deep_river-0.2.6 → deep_river-0.2.7}/deep_river.egg-info/top_level.txt +0 -0
  38. {deep_river-0.2.6 → deep_river-0.2.7}/pyproject.toml +0 -0
  39. {deep_river-0.2.6 → deep_river-0.2.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deep_river
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Online Deep Learning for river
5
5
  Home-page: https://online-ml.github.io/deep-river/
6
6
  Author: Cedric Kulbach
@@ -17,42 +17,44 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
17
17
  Requires-Python: >=3.6.0
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: scikit-learn~=1.4.2
20
+ Requires-Dist: scikit-learn~=1.5.0
21
21
  Requires-Dist: torch~=2.2.2
22
22
  Requires-Dist: pandas~=2.2.2
23
23
  Requires-Dist: numpy~=1.26.4
24
- Requires-Dist: river~=0.21.1
25
- Requires-Dist: tqdm~=4.66.4
24
+ Requires-Dist: river~=0.21.2
25
+ Requires-Dist: scipy~=1.13.0
26
+ Requires-Dist: tqdm~=4.66.5
26
27
  Requires-Dist: ordered-set~=4.1.0
27
28
  Requires-Dist: torchviz~=0.0.2
28
29
  Provides-Extra: dev
29
- Requires-Dist: scikit-learn~=1.4.2; extra == "dev"
30
+ Requires-Dist: scikit-learn~=1.5.0; extra == "dev"
30
31
  Requires-Dist: torch~=2.2.2; extra == "dev"
31
32
  Requires-Dist: pandas~=2.2.2; extra == "dev"
32
33
  Requires-Dist: numpy~=1.26.4; extra == "dev"
33
- Requires-Dist: river~=0.21.1; extra == "dev"
34
- Requires-Dist: tqdm~=4.66.4; extra == "dev"
34
+ Requires-Dist: river~=0.21.2; extra == "dev"
35
+ Requires-Dist: scipy~=1.13.0; extra == "dev"
36
+ Requires-Dist: tqdm~=4.66.5; extra == "dev"
35
37
  Requires-Dist: ordered-set~=4.1.0; extra == "dev"
36
38
  Requires-Dist: torchviz~=0.0.2; extra == "dev"
37
- Requires-Dist: graphviz>=0.10.1; extra == "dev"
38
- Requires-Dist: matplotlib>=3.0.2; extra == "dev"
39
- Requires-Dist: mypy>=0.990; extra == "dev"
40
- Requires-Dist: pre-commit>=2.20.0; extra == "dev"
41
- Requires-Dist: pytest>=7.2.0; extra == "dev"
42
- Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
43
- Requires-Dist: scikit-learn>=0.22.1; extra == "dev"
44
- Requires-Dist: black>=22.10.0; extra == "dev"
45
- Requires-Dist: flake8>=5.0.4; extra == "dev"
46
- Requires-Dist: isort>=5.10.1; extra == "dev"
39
+ Requires-Dist: graphviz>=0.20.3; extra == "dev"
40
+ Requires-Dist: matplotlib>=3.9.2; extra == "dev"
41
+ Requires-Dist: mypy>=1.11.1; extra == "dev"
42
+ Requires-Dist: pre-commit>=3.8.0; extra == "dev"
43
+ Requires-Dist: pytest>=8.3.2; extra == "dev"
44
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
45
+ Requires-Dist: black>=24.8.0; extra == "dev"
46
+ Requires-Dist: flake8>=7.1.1; extra == "dev"
47
+ Requires-Dist: isort>=5.13.2; extra == "dev"
47
48
  Requires-Dist: jupyter>=1.0.0; extra == "dev"
48
- Requires-Dist: pyupgrade==3.2.0; extra == "dev"
49
+ Requires-Dist: pyupgrade==3.17.0; extra == "dev"
49
50
  Provides-Extra: docs
50
- Requires-Dist: scikit-learn~=1.4.2; extra == "docs"
51
+ Requires-Dist: scikit-learn~=1.5.0; extra == "docs"
51
52
  Requires-Dist: torch~=2.2.2; extra == "docs"
52
53
  Requires-Dist: pandas~=2.2.2; extra == "docs"
53
54
  Requires-Dist: numpy~=1.26.4; extra == "docs"
54
- Requires-Dist: river~=0.21.1; extra == "docs"
55
- Requires-Dist: tqdm~=4.66.4; extra == "docs"
55
+ Requires-Dist: river~=0.21.2; extra == "docs"
56
+ Requires-Dist: scipy~=1.13.0; extra == "docs"
57
+ Requires-Dist: tqdm~=4.66.5; extra == "docs"
56
58
  Requires-Dist: ordered-set~=4.1.0; extra == "docs"
57
59
  Requires-Dist: torchviz~=0.0.2; extra == "docs"
58
60
  Requires-Dist: flask>=2.0.2; extra == "docs"
@@ -78,18 +80,17 @@ Requires-Dist: mkdocs-charts-plugin; extra == "docs"
78
80
  Requires-Dist: python-slugify; extra == "docs"
79
81
  Requires-Dist: watermark==2.3.1; extra == "docs"
80
82
  Provides-Extra: all
81
- Requires-Dist: graphviz>=0.10.1; extra == "all"
82
- Requires-Dist: matplotlib>=3.0.2; extra == "all"
83
- Requires-Dist: mypy>=0.990; extra == "all"
84
- Requires-Dist: pre-commit>=2.20.0; extra == "all"
85
- Requires-Dist: pytest>=7.2.0; extra == "all"
86
- Requires-Dist: pytest-cov>=4.0.0; extra == "all"
87
- Requires-Dist: scikit-learn>=0.22.1; extra == "all"
88
- Requires-Dist: black>=22.10.0; extra == "all"
89
- Requires-Dist: flake8>=5.0.4; extra == "all"
90
- Requires-Dist: isort>=5.10.1; extra == "all"
83
+ Requires-Dist: graphviz>=0.20.3; extra == "all"
84
+ Requires-Dist: matplotlib>=3.9.2; extra == "all"
85
+ Requires-Dist: mypy>=1.11.1; extra == "all"
86
+ Requires-Dist: pre-commit>=3.8.0; extra == "all"
87
+ Requires-Dist: pytest>=8.3.2; extra == "all"
88
+ Requires-Dist: pytest-cov>=5.0.0; extra == "all"
89
+ Requires-Dist: black>=24.8.0; extra == "all"
90
+ Requires-Dist: flake8>=7.1.1; extra == "all"
91
+ Requires-Dist: isort>=5.13.2; extra == "all"
91
92
  Requires-Dist: jupyter>=1.0.0; extra == "all"
92
- Requires-Dist: pyupgrade==3.2.0; extra == "all"
93
+ Requires-Dist: pyupgrade==3.17.0; extra == "all"
93
94
  Requires-Dist: flask>=2.0.2; extra == "all"
94
95
  Requires-Dist: ipykernel>=6.9.0; extra == "all"
95
96
  Requires-Dist: mike>=0.5.3; extra == "all"
@@ -125,6 +126,7 @@ Requires-Dist: dataclasses; python_version == "3.6"
125
126
  </a>
126
127
  <img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/deep-river">
127
128
  <img alt="GitHub" src="https://img.shields.io/github/license/online-ml/deep-river">
129
+ <a href="https://joss.theoj.org/papers/6a76784f55e8b041d71a7fa776eb386a"><img src="https://joss.theoj.org/papers/6a76784f55e8b041d71a7fa776eb386a/status.svg"></a>
128
130
  </p>
129
131
  <p align="center">
130
132
  deep-river is a Python library for online deep learning.
@@ -194,7 +196,7 @@ For further examples check out the <a href="https://online-ml.github.io/deep-riv
194
196
  ... metric.update(y, y_pred) # update the metric
195
197
  ... model_pipeline.learn_one(x, y) # make the model learn
196
198
  >>> print(f"Accuracy: {metric.get():.4f}")
197
- Accuracy: 0.6736
199
+ Accuracy: 0.7264
198
200
 
199
201
  ```
200
202
  ### Multi Target Regression
@@ -274,7 +276,7 @@ MicroAverage(MAE): 34.31
274
276
  ... metric.update(y, score)
275
277
  ...
276
278
  >>> print(f"ROCAUC: {metric.get():.4f}")
277
- ROCAUC: 0.9017
279
+ ROCAUC: 0.7812
278
280
 
279
281
  ```
280
282
 
@@ -8,6 +8,7 @@
8
8
  </a>
9
9
  <img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/deep-river">
10
10
  <img alt="GitHub" src="https://img.shields.io/github/license/online-ml/deep-river">
11
+ <a href="https://joss.theoj.org/papers/6a76784f55e8b041d71a7fa776eb386a"><img src="https://joss.theoj.org/papers/6a76784f55e8b041d71a7fa776eb386a/status.svg"></a>
11
12
  </p>
12
13
  <p align="center">
13
14
  deep-river is a Python library for online deep learning.
@@ -77,7 +78,7 @@ For further examples check out the <a href="https://online-ml.github.io/deep-riv
77
78
  ... metric.update(y, y_pred) # update the metric
78
79
  ... model_pipeline.learn_one(x, y) # make the model learn
79
80
  >>> print(f"Accuracy: {metric.get():.4f}")
80
- Accuracy: 0.6736
81
+ Accuracy: 0.7264
81
82
 
82
83
  ```
83
84
  ### Multi Target Regression
@@ -157,7 +158,7 @@ MicroAverage(MAE): 34.31
157
158
  ... metric.update(y, score)
158
159
  ...
159
160
  >>> print(f"ROCAUC: {metric.get():.4f}")
160
- ROCAUC: 0.9017
161
+ ROCAUC: 0.7812
161
162
 
162
163
  ```
163
164
 
@@ -1,3 +1,3 @@
1
- VERSION = (0, 2, 6)
1
+ VERSION = (0, 2, 7)
2
2
 
3
3
  __version__ = ".".join(map(str, VERSION)) # noqa: F401
@@ -1,4 +1,4 @@
1
- from typing import Any, Callable, Type, Union
1
+ from typing import Any, Callable, Dict, Type, Union
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
@@ -8,6 +8,7 @@ from torch import nn
8
8
 
9
9
  from deep_river.base import DeepEstimator
10
10
  from deep_river.utils import dict2tensor
11
+ from deep_river.utils.layer_adaptation import expand_layer
11
12
  from deep_river.utils.tensor_conversion import df2tensor
12
13
 
13
14
 
@@ -93,7 +94,7 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
93
94
  ... metric.update(y, score)
94
95
  ...
95
96
  >>> print(f"ROCAUC: {metric.get():.4f}")
96
- ROCAUC: 0.9017
97
+ ROCAUC: 0.7812
97
98
  """
98
99
 
99
100
  def __init__(
@@ -102,6 +103,7 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
102
103
  loss_fn: Union[str, Callable] = "mse",
103
104
  optimizer_fn: Union[str, Callable] = "sgd",
104
105
  lr: float = 1e-3,
106
+ is_feature_incremental: bool = False,
105
107
  device: str = "cpu",
106
108
  seed: int = 42,
107
109
  **kwargs,
@@ -111,10 +113,12 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
111
113
  loss_fn=loss_fn,
112
114
  optimizer_fn=optimizer_fn,
113
115
  lr=lr,
116
+ is_feature_incremental=is_feature_incremental,
114
117
  device=device,
115
118
  seed=seed,
116
119
  **kwargs,
117
120
  )
121
+ self.is_class_incremental = is_feature_incremental
118
122
 
119
123
  @classmethod
120
124
  def _unit_test_params(cls):
@@ -133,6 +137,7 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
133
137
  "module": _TestAutoencoder,
134
138
  "loss_fn": "mse",
135
139
  "optimizer_fn": "sgd",
140
+ "is_feature_incremental": True,
136
141
  }
137
142
 
138
143
  @classmethod
@@ -148,13 +153,7 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
148
153
  set
149
154
  Set of checks to skip during unit testing.
150
155
  """
151
- return {
152
- "check_shuffle_features_no_impact",
153
- "check_emerging_features",
154
- "check_disappearing_features",
155
- "check_predict_proba_one",
156
- "check_predict_proba_one_binary",
157
- }
156
+ return set()
158
157
 
159
158
  def learn_one(self, x: dict, y: Any = None, **kwargs) -> "Autoencoder":
160
159
  """
@@ -173,14 +172,17 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
173
172
  The model itself.
174
173
  """
175
174
  if not self.module_initialized:
176
- self.kwargs["n_features"] = len(x)
177
- self.initialize_module(**self.kwargs)
178
- return self._learn(dict2tensor(x, device=self.device))
175
+ self._update_observed_features(x)
176
+ self.initialize_module(x=x, **self.kwargs)
177
+ self._adapt_input_dim(x)
178
+ return self._learn(
179
+ dict2tensor(x, features=self.observed_features, device=self.device)
180
+ )
179
181
 
180
182
  def _learn(self, x: torch.Tensor) -> "Autoencoder":
181
183
  self.module.train()
182
184
  x_pred = self.module(x)
183
- loss = self.loss_fn(x_pred, x)
185
+ loss = self.loss_func(x_pred, x)
184
186
  loss.backward()
185
187
  self.optimizer.step()
186
188
  self.optimizer.zero_grad()
@@ -205,14 +207,18 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
205
207
  """
206
208
 
207
209
  if not self.module_initialized:
208
- self.kwargs["n_features"] = len(x)
209
- self.initialize_module(**self.kwargs)
210
+ self._update_observed_features(x)
211
+ self.initialize_module(x=x, **self.kwargs)
210
212
 
211
- x_t = dict2tensor(x, device=self.device)
213
+ self._adapt_input_dim(x)
214
+
215
+ x_t = dict2tensor(
216
+ x, features=self.observed_features, device=self.device
217
+ )
212
218
  self.module.eval()
213
219
  with torch.inference_mode():
214
220
  x_pred = self.module(x_t)
215
- loss = self.loss_fn(x_pred, x_t).item()
221
+ loss = self.loss_func(x_pred, x_t).item()
216
222
  return loss
217
223
 
218
224
  def learn_many(self, X: pd.DataFrame) -> "Autoencoder":
@@ -231,10 +237,13 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
231
237
 
232
238
  """
233
239
  if not self.module_initialized:
234
- self.kwargs["n_features"] = len(X.columns)
235
- self.initialize_module(**self.kwargs)
236
- X = df2tensor(X, device=self.device)
237
- return self._learn(X)
240
+
241
+ self._update_observed_features(X)
242
+ self.initialize_module(x=X, **self.kwargs)
243
+
244
+ self._adapt_input_dim(X)
245
+ X_t = df2tensor(X, features=self.observed_features, device=self.device)
246
+ return self._learn(X_t)
238
247
 
239
248
  def score_many(self, X: pd.DataFrame) -> np.ndarray:
240
249
  """
@@ -253,16 +262,35 @@ class Autoencoder(DeepEstimator, AnomalyDetector):
253
262
  indicate more anomalous examples.
254
263
  """
255
264
  if not self.module_initialized:
256
- self.kwargs["n_features"] = len(X.columns)
257
- self.initialize_module(**self.kwargs)
258
- X = df2tensor(X, device=self.device)
265
+ self._update_observed_features(X)
266
+ self.initialize_module(x=X, **self.kwargs)
267
+
268
+ self._adapt_input_dim(X)
269
+ X_t = df2tensor(X, features=self.observed_features, device=self.device)
259
270
 
260
271
  self.module.eval()
261
272
  with torch.inference_mode():
262
- X_pred = self.module(X)
273
+ X_pred = self.module(X_t)
263
274
  loss = torch.mean(
264
- self.loss_fn(X_pred, X, reduction="none"),
265
- dim=list(range(1, X.dim())),
275
+ self.loss_func(X_pred, X_t, reduction="none"),
276
+ dim=list(range(1, X_t.dim())),
266
277
  )
267
278
  score = loss.cpu().detach().numpy()
268
279
  return score
280
+
281
+ def _adapt_input_dim(self, x: Dict | pd.DataFrame):
282
+ has_new_feature = self._update_observed_features(x)
283
+
284
+ if has_new_feature and self.is_feature_incremental:
285
+ expand_layer(
286
+ self.input_layer,
287
+ self.input_expansion_instructions,
288
+ len(self.observed_features),
289
+ output=False,
290
+ )
291
+ expand_layer(
292
+ self.output_layer,
293
+ self.output_expansion_instructions,
294
+ len(self.observed_features),
295
+ output=True,
296
+ )
@@ -8,6 +8,7 @@ from scipy.special import ndtr
8
8
 
9
9
  from deep_river.anomaly import ae
10
10
  from deep_river.utils import dict2tensor
11
+ from deep_river.utils.tensor_conversion import df2tensor
11
12
 
12
13
 
13
14
  class ProbabilityWeightedAutoencoder(ae.Autoencoder):
@@ -87,7 +88,7 @@ class ProbabilityWeightedAutoencoder(ae.Autoencoder):
87
88
  ... metric.update(y, score)
88
89
  ...
89
90
  >>> print(f"ROCAUC: {metric.get():.4f}")
90
- ROCAUC: 0.8599
91
+ ROCAUC: 0.7572
91
92
  """
92
93
 
93
94
  def __init__(
@@ -138,13 +139,15 @@ class ProbabilityWeightedAutoencoder(ae.Autoencoder):
138
139
  The autoencoder itself.
139
140
  """
140
141
  if not self.module_initialized:
141
- self.kwargs["n_features"] = len(x)
142
- self.initialize_module(**self.kwargs)
143
- x_t = dict2tensor(x, device=self.device)
142
+ self._update_observed_features(x)
143
+ self.initialize_module(x=x, **self.kwargs)
144
+
145
+ self._adapt_input_dim(x)
146
+ x_t = dict2tensor(x, self.observed_features, device=self.device)
144
147
 
145
148
  self.module.train()
146
149
  x_pred = self.module(x_t)
147
- loss = self.loss_fn(x_pred, x_t)
150
+ loss = self.loss_func(x_pred, x_t)
148
151
  self._apply_loss(loss)
149
152
  return self
150
153
 
@@ -173,15 +176,16 @@ class ProbabilityWeightedAutoencoder(ae.Autoencoder):
173
176
 
174
177
  def learn_many(self, X: pd.DataFrame) -> "ProbabilityWeightedAutoencoder":
175
178
  if not self.module_initialized:
176
- self.kwargs["n_features"] = len(X.columns)
177
- self.initialize_module(**self.kwargs)
178
- X = dict2tensor(X.to_dict(), device=self.device)
179
+ self._update_observed_features(X)
180
+ self.initialize_module(x=X, **self.kwargs)
181
+ self._adapt_input_dim(X)
182
+ X_t = df2tensor(X, features=self.observed_features, device=self.device)
179
183
 
180
184
  self.module.train()
181
185
  x_pred = self.module(X)
182
186
  loss = torch.mean(
183
- self.loss_fn(x_pred, X, reduction="none"),
184
- dim=list(range(1, X.dim())),
187
+ self.loss_func(x_pred, X_t, reduction="none"),
188
+ dim=list(range(1, X_t.dim())),
185
189
  )
186
190
  self._apply_loss(loss)
187
191
  return self
@@ -26,20 +26,10 @@ class _TestLSTMAutoencoder(nn.Module):
26
26
  num_layers=n_layers,
27
27
  batch_first=batch_first,
28
28
  )
29
- self.decoder = nn.LSTM(
30
- input_size=hidden_size,
31
- hidden_size=n_features,
32
- num_layers=n_layers,
33
- batch_first=batch_first,
34
- )
35
29
 
36
30
  def forward(self, x):
37
- _, (h, _) = self.encoder(x)
38
- h = h[-1].view(1, 1, -1)
39
- x_flipped = torch.flip(x[1:], dims=[self.time_axis])
40
- input = torch.cat((h, x_flipped), dim=self.time_axis)
41
- x_hat, _ = self.decoder(input)
42
- return torch.flip(x_hat, dims=[self.time_axis])
31
+ output, (h, c) = self.encoder(x)
32
+ return output
43
33
 
44
34
 
45
35
  class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
@@ -146,7 +136,7 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
146
136
  def _learn(self, x: torch.Tensor):
147
137
  self.module.train()
148
138
  x_pred = self.module(x)
149
- loss = self.loss_fn(x_pred, x)
139
+ loss = self.loss_func(x_pred, x)
150
140
 
151
141
  self.optimizer.zero_grad()
152
142
  loss.backward()
@@ -169,8 +159,8 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
169
159
  The estimator itself.
170
160
  """
171
161
  if not self.module_initialized:
172
- self.kwargs["n_features"] = len(x)
173
- self.initialize_module(**self.kwargs)
162
+ self._update_observed_features(x)
163
+ self.initialize_module(x=x, **self.kwargs)
174
164
 
175
165
  self._x_window.append(list(x.values()))
176
166
 
@@ -197,8 +187,8 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
197
187
  The estimator itself.
198
188
  """
199
189
  if not self.module_initialized:
200
- self.kwargs["n_features"] = len(X.columns)
201
- self.initialize_module(**self.kwargs)
190
+ self._update_observed_features(X)
191
+ self.initialize_module(x=X, **self.kwargs)
202
192
 
203
193
  self._x_window.append(X.values.tolist())
204
194
  if len(self._x_window) == self.window_size:
@@ -209,8 +199,10 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
209
199
  def score_one(self, x: dict) -> float:
210
200
  res = 0.0
211
201
  if not self.module_initialized:
212
- self.kwargs["n_features"] = len(x)
213
- self.initialize_module(**self.kwargs)
202
+
203
+ self._update_observed_features(x)
204
+
205
+ self.initialize_module(x=x, **self.kwargs)
214
206
 
215
207
  if len(self._x_window) == self.window_size:
216
208
  x_win = self._x_window.copy()
@@ -219,7 +211,7 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
219
211
  self.module.eval()
220
212
  with torch.inference_mode():
221
213
  x_pred = self.module(x_t)
222
- loss = self.loss_fn(x_pred, x_t)
214
+ loss = self.loss_func(x_pred, x_t)
223
215
  res = loss.item()
224
216
 
225
217
  if self.append_predict:
@@ -228,8 +220,9 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
228
220
 
229
221
  def score_many(self, X: pd.DataFrame) -> List[Any]:
230
222
  if not self.module_initialized:
231
- self.kwargs["n_features"] = len(X.columns)
232
- self.initialize_module(**self.kwargs)
223
+
224
+ self._update_observed_features(X)
225
+ self.initialize_module(x=X, **self.kwargs)
233
226
 
234
227
  x_win = self._x_window.copy()
235
228
  x_win.append(X.values.tolist())
@@ -242,7 +235,7 @@ class RollingAutoencoder(RollingDeepEstimator, anomaly.base.AnomalyDetector):
242
235
  with torch.inference_mode():
243
236
  x_pred = self.module(X_t)
244
237
  loss = torch.mean(
245
- self.loss_fn(x_pred, x_pred, reduction="none"),
238
+ self.loss_func(x_pred, x_pred, reduction="none"),
246
239
  dim=list(range(1, x_pred.dim())),
247
240
  )
248
241
  losses = loss.detach().numpy()
@@ -1,11 +1,21 @@
1
1
  import collections
2
2
  import inspect
3
- from typing import Any, Callable, Deque, Type, Union, cast
3
+ import warnings
4
+ from typing import Any, Callable, Deque, Dict, List, Type, Union, cast
4
5
 
6
+ import pandas as pd
5
7
  import torch
8
+ from ordered_set import OrderedSet
6
9
  from river import base
10
+ from torch.utils.hooks import RemovableHandle
7
11
 
8
12
  from deep_river.utils import get_loss_fn, get_optim_fn
13
+ from deep_river.utils.hooks import ForwardOrderTracker, apply_hooks
14
+ from deep_river.utils.layer_adaptation import (
15
+ SUPPORTED_LAYERS,
16
+ expand_layer,
17
+ load_instructions,
18
+ )
9
19
 
10
20
  try:
11
21
  from graphviz import Digraph
@@ -53,19 +63,29 @@ class DeepEstimator(base.Estimator):
53
63
  loss_fn: Union[str, Callable] = "mse",
54
64
  optimizer_fn: Union[str, Callable] = "sgd",
55
65
  lr: float = 1e-3,
66
+ is_feature_incremental: bool = False,
56
67
  device: str = "cpu",
57
68
  seed: int = 42,
58
69
  **kwargs,
59
70
  ):
60
71
  super().__init__()
61
72
  self.module_cls = module
62
- self.module: torch.nn.Module = cast(torch.nn.Module, None) # cleaner
63
- self.loss_fn = get_loss_fn(loss_fn)
64
- self.optimizer_fn = get_optim_fn(optimizer_fn)
73
+ self.module: torch.nn.Module = cast(torch.nn.Module, None)
74
+ self.loss_func = get_loss_fn(loss_fn)
75
+ self.loss_fn = loss_fn
76
+ self.optimizer_func = get_optim_fn(optimizer_fn)
77
+ self.optimizer_fn = optimizer_fn
78
+ self.is_feature_incremental = is_feature_incremental
79
+ self.is_class_incremental: bool = False
80
+ self.observed_features: OrderedSet[str] = OrderedSet([])
65
81
  self.lr = lr
66
82
  self.device = device
67
83
  self.kwargs = kwargs
68
84
  self.seed = seed
85
+ self.input_layer = cast(torch.nn.Module, None)
86
+ self.input_expansion_instructions = cast(Dict, None)
87
+ self.output_layer = cast(torch.nn.Module, None)
88
+ self.output_expansion_instructions = cast(Dict, None)
69
89
  self.module_initialized = False
70
90
  torch.manual_seed(seed)
71
91
 
@@ -103,7 +123,7 @@ class DeepEstimator(base.Estimator):
103
123
  y_pred.mean(), params=dict(self.module.named_parameters())
104
124
  )
105
125
 
106
- def initialize_module(self, **kwargs):
126
+ def initialize_module(self, x: dict | pd.DataFrame, **kwargs):
107
127
  """
108
128
  Parameters
109
129
  ----------
@@ -118,17 +138,26 @@ class DeepEstimator(base.Estimator):
118
138
  instance
119
139
  The initialized component.
120
140
  """
141
+ torch.manual_seed(self.seed)
142
+ if isinstance(x, Dict):
143
+ n_features = len(x)
144
+ elif isinstance(x, pd.DataFrame):
145
+ n_features = len(x.columns)
146
+
121
147
  if not isinstance(self.module_cls, torch.nn.Module):
122
148
  self.module = self.module_cls(
123
- **self._filter_kwargs(self.module_cls, kwargs)
149
+ n_features=n_features,
150
+ **self._filter_kwargs(self.module_cls, kwargs),
124
151
  )
125
152
 
126
153
  self.module.to(self.device)
127
- self.optimizer = self.optimizer_fn(
154
+ self.optimizer = self.optimizer_func(
128
155
  self.module.parameters(), lr=self.lr
129
156
  )
130
157
  self.module_initialized = True
131
158
 
159
+ self._get_input_output_layers(n_features=n_features)
160
+
132
161
  def clone(
133
162
  self,
134
163
  new_params: dict[Any, Any] | None = None,
@@ -160,6 +189,72 @@ class DeepEstimator(base.Estimator):
160
189
  clone.__dict__.update(self.__dict__)
161
190
  return clone
162
191
 
192
+ def _adapt_input_dim(self, x: Dict | pd.DataFrame):
193
+ has_new_feature = self._update_observed_features(x)
194
+
195
+ if has_new_feature and self.is_feature_incremental:
196
+ expand_layer(
197
+ self.input_layer,
198
+ self.input_expansion_instructions,
199
+ len(self.observed_features),
200
+ output=False,
201
+ )
202
+
203
+ def _update_observed_features(self, x):
204
+ n_existing_features = len(self.observed_features)
205
+ if isinstance(x, Dict):
206
+ self.observed_features |= x.keys()
207
+ else:
208
+ self.observed_features |= x.columns
209
+
210
+ if len(self.observed_features) > n_existing_features:
211
+ self.observed_features = OrderedSet(sorted(self.observed_features))
212
+ return True
213
+ else:
214
+ return False
215
+
216
+ def _get_input_output_layers(self, n_features: int):
217
+ handles: List[RemovableHandle] = []
218
+ tracker = ForwardOrderTracker()
219
+ apply_hooks(module=self.module, hook=tracker, handles=handles)
220
+
221
+ x_dummy = torch.empty((1, n_features), device=self.device)
222
+ self.module(x_dummy)
223
+
224
+ for h in handles:
225
+ h.remove()
226
+
227
+ if self.is_class_incremental:
228
+ if tracker.ordered_modules and isinstance(
229
+ tracker.ordered_modules[-1], SUPPORTED_LAYERS
230
+ ):
231
+
232
+ self.output_layer = tracker.ordered_modules[-1]
233
+ self.output_expansion_instructions = load_instructions(
234
+ self.output_layer
235
+ )
236
+ else:
237
+ warnings.warn(
238
+ "The model will not be able to adapt its output to new "
239
+ "classes since no supported output layer was found."
240
+ )
241
+ self.is_class_incremental = False
242
+
243
+ if self.is_feature_incremental:
244
+ if tracker.ordered_modules and isinstance(
245
+ tracker.ordered_modules[0], SUPPORTED_LAYERS
246
+ ):
247
+ self.input_layer = tracker.ordered_modules[0]
248
+ self.input_expansion_instructions = load_instructions(
249
+ self.input_layer
250
+ )
251
+ else:
252
+ warnings.warn(
253
+ "The model will not be able to adapt its input layer to "
254
+ "new features since no supported input layer was found."
255
+ )
256
+ self.is_feature_incremental = False
257
+
163
258
 
164
259
  class RollingDeepEstimator(DeepEstimator):
165
260
  """