Skip to content

Commit b6fe910

Browse files
committed
Merge branch 'main' into ajb/normalise
# Conflicts: # examples/transformations/preprocessing.ipynb
2 parents 584b589 + 23f42d3 commit b6fe910

36 files changed

+1803
-1470
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ repos:
2929
args: [ "--create", "--python-folders", "aeon" ]
3030

3131
- repo: https://github.com/astral-sh/ruff-pre-commit
32-
rev: v0.7.1
32+
rev: v0.7.2
3333
hooks:
3434
- id: ruff
3535
args: [ "--fix"]

aeon/anomaly_detection/_stray.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,10 @@ class STRAY(BaseAnomalyDetector):
6666
--------
6767
>>> from aeon.anomaly_detection import STRAY
6868
>>> from aeon.datasets import load_airline
69-
>>> from sklearn.preprocessing import MinMaxScaler
7069
>>> import numpy as np
71-
>>> X = load_airline().to_frame().to_numpy()
72-
>>> scaler = MinMaxScaler()
73-
>>> X = scaler.fit_transform(X)
70+
>>> X = load_airline()
7471
>>> detector = STRAY(k=3)
75-
>>> y = detector.fit_predict(X, axis=0)
72+
>>> y = detector.fit_predict(X)
7673
>>> y[:5]
7774
array([False, False, False, False, False])
7875
"""
+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"""Deep learning based clusterers."""
22

3-
__all__ = ["BaseDeepClusterer", "AEFCNClusterer", "AEResNetClusterer"]
3+
__all__ = [
4+
"BaseDeepClusterer",
5+
"AEBiGRUClusterer",
6+
"AEFCNClusterer",
7+
"AEResNetClusterer",
8+
]
9+
from aeon.clustering.deep_learning._ae_bgru import AEBiGRUClusterer
410
from aeon.clustering.deep_learning._ae_fcn import AEFCNClusterer
511
from aeon.clustering.deep_learning._ae_resnet import AEResNetClusterer
612
from aeon.clustering.deep_learning.base import BaseDeepClusterer
+322
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
"""Deep Learning Auto-Encoder using Bidirectional GRU Network."""
2+
3+
__maintainer__ = []
4+
__all__ = ["AEBiGRUClusterer"]
5+
6+
import gc
7+
import os
8+
import time
9+
from copy import deepcopy
10+
11+
from sklearn.utils import check_random_state
12+
13+
from aeon.clustering import DummyClusterer
14+
from aeon.clustering.deep_learning.base import BaseDeepClusterer
15+
from aeon.networks import AEBiGRUNetwork
16+
17+
18+
class AEBiGRUClusterer(BaseDeepClusterer):
19+
"""Auto-Encoder based Bidirectional GRU Network.
20+
21+
Parameters
22+
----------
23+
n_clusters : int, default=None
24+
Number of clusters for the deep learnign model.
25+
clustering_algorithm : str, default="deprecated"
26+
Use 'estimator' parameter instead.
27+
clustering_params : dict, default=None
28+
Use 'estimator' parameter instead.
29+
estimator : aeon clusterer, default=None
30+
An aeon estimator to be built using the transformed data.
31+
Defaults to aeon TimeSeriesKMeans() with euclidean distance
32+
and mean averaging method and n_clusters set to 2.
33+
latent_space_dim : int, default=128
34+
Dimension of the latent space of the auto-encoder.
35+
temporal_latent_space : bool, default = False
36+
Flag to choose whether the latent space is an MTS or Euclidean space.
37+
n_layers : int, default = 2
38+
Number of Bidirectional GRU Layers.
39+
activation : str or list of str, default = "relu"
40+
Activation used after the Bidirectional GRU Layer.
41+
n_epochs : int, default = 2000
42+
The number of epochs to train the model.
43+
batch_size : int, default = 16
44+
The number of samples per gradient update.
45+
use_mini_batch_size : bool, default = True,
46+
Whether or not to use the mini batch size formula.
47+
random_state : int, RandomState instance or None, default=None
48+
If `int`, random_state is the seed used by the random number generator;
49+
If `RandomState` instance, random_state is the random number generator;
50+
If `None`, the random number generator is the `RandomState` instance used
51+
by `np.random`.
52+
Seeded random number generation can only be guaranteed on CPU processing,
53+
GPU processing will be non-deterministic.
54+
verbose : boolean, default = False
55+
Whether to output extra information.
56+
loss : str, default="mean_squared_error"
57+
Fit parameter for the keras model.
58+
metrics : str, default=["mean_squared_error"]
59+
Metrics to evaluate model predictions.
60+
optimizer : keras.optimizers object, default = Adam(lr=0.01)
61+
Specify the optimizer and the learning rate to be used.
62+
file_path : str, default = "./"
63+
File path to save best model.
64+
save_best_model : bool, default = False
65+
Whether or not to save the best model, if the
66+
modelcheckpoint callback is used by default,
67+
this condition, if True, will prevent the
68+
automatic deletion of the best saved model from
69+
file and the user can choose the file name.
70+
save_last_model : bool, default = False
71+
Whether or not to save the last model, last
72+
epoch trained, using the base class method
73+
save_last_model_to_file.
74+
best_file_name : str, default = "best_model"
75+
The name of the file of the best model, if
76+
save_best_model is set to False, this parameter
77+
is discarded.
78+
last_file_name : str, default = "last_model"
79+
The name of the file of the last model, if
80+
save_last_model is set to False, this parameter
81+
is discarded.
82+
callbacks : keras.callbacks, default = None
83+
List of keras callbacks.
84+
85+
86+
Examples
87+
--------
88+
>>> from aeon.clustering.deep_learning import AEBiGRUClusterer
89+
>>> from aeon.clustering import DummyClusterer
90+
>>> from aeon.datasets import load_unit_test
91+
>>> X_train, y_train = load_unit_test(split="train")
92+
>>> X_test, y_test = load_unit_test(split="test")
93+
>>> _clst = DummyClusterer(n_clusters=2)
94+
>>> aebgru=AEBiGRUClusterer( estimator=_clst, n_epochs=20,
95+
... batch_size=4 ) # doctest: +SKIP
96+
>>> aebgru.fit(X_train) # doctest: +SKIP
97+
AEBiGRUClusterer(...)
98+
"""
99+
100+
def __init__(
101+
self,
102+
n_clusters=None,
103+
clustering_algorithm="deprecated",
104+
estimator=None,
105+
clustering_params=None,
106+
latent_space_dim=128,
107+
temporal_latent_space=False,
108+
n_layers=2,
109+
n_units=None,
110+
activation="relu",
111+
n_epochs=2000,
112+
batch_size=32,
113+
use_mini_batch_size=False,
114+
random_state=None,
115+
verbose=False,
116+
loss="mse",
117+
metrics=None,
118+
optimizer="Adam",
119+
file_path="./",
120+
save_best_model=False,
121+
save_last_model=False,
122+
best_file_name="best_model",
123+
last_file_name="last_file",
124+
callbacks=None,
125+
):
126+
self.latent_space_dim = latent_space_dim
127+
self.temporal_latent_space = temporal_latent_space
128+
self.n_layers = n_layers
129+
self.n_units = n_units
130+
self.activation = activation
131+
self.optimizer = optimizer
132+
self.loss = loss
133+
self.metrics = metrics
134+
self.verbose = verbose
135+
self.use_mini_batch_size = use_mini_batch_size
136+
self.callbacks = callbacks
137+
self.file_path = file_path
138+
self.n_epochs = n_epochs
139+
self.save_best_model = save_best_model
140+
self.save_last_model = save_last_model
141+
self.best_file_name = best_file_name
142+
self.random_state = random_state
143+
self.estimator = estimator
144+
145+
super().__init__(
146+
n_clusters=n_clusters,
147+
estimator=estimator,
148+
batch_size=batch_size,
149+
last_file_name=last_file_name,
150+
)
151+
152+
self._network = AEBiGRUNetwork(
153+
latent_space_dim=self.latent_space_dim,
154+
n_layers=self.n_layers,
155+
n_units=self.n_units,
156+
activation=self.activation,
157+
temporal_latent_space=self.temporal_latent_space,
158+
)
159+
160+
def build_model(self, input_shape, **kwargs):
161+
"""Construct a compiled, un-trained, keras model that is ready for training.
162+
163+
In aeon, time series are stored in numpy arrays of shape
164+
(n_channels,n_timepoints). Keras/tensorflow assume
165+
data is in shape (n_timepoints,n_channels). This method also assumes
166+
(n_timepoints,n_channels). Transpose should happen in fit.
167+
168+
Parameters
169+
----------
170+
input_shape : tuple
171+
The shape of the data fed into the input layer, should be
172+
(n_timepoints,n_channels).
173+
174+
Returns
175+
-------
176+
output : a compiled Keras Model.
177+
"""
178+
import numpy as np
179+
import tensorflow as tf
180+
181+
rng = check_random_state(self.random_state)
182+
self.random_state_ = rng.randint(0, np.iinfo(np.int32).max)
183+
tf.keras.utils.set_random_seed(self.random_state_)
184+
encoder, decoder = self._network.build_network(input_shape, **kwargs)
185+
186+
input_layer = tf.keras.layers.Input(input_shape, name="input layer")
187+
encoder_output = encoder(input_layer)
188+
decoder_output = decoder(encoder_output)
189+
output_layer = tf.keras.layers.Reshape(
190+
target_shape=input_shape, name="outputlayer"
191+
)(decoder_output)
192+
193+
model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
194+
195+
self.optimizer_ = (
196+
tf.keras.optimizers.Adam() if self.optimizer is None else self.optimizer
197+
)
198+
199+
if self.metrics is None:
200+
self._metrics = ["mean_squared_error"]
201+
elif isinstance(self.metrics, list):
202+
self._metrics = self.metrics
203+
elif isinstance(self.metrics, str):
204+
self._metrics = [self.metrics]
205+
else:
206+
raise ValueError("Metrics should be a list, string, or None.")
207+
208+
model.compile(optimizer=self.optimizer_, loss=self.loss, metrics=self._metrics)
209+
210+
return model
211+
212+
def _fit(self, X):
213+
"""Fit the classifier on the training set (X, y).
214+
215+
Parameters
216+
----------
217+
X : np.ndarray of shape = (n_cases (n), n_channels (d), n_timepoints (m))
218+
The training input samples.
219+
220+
Returns
221+
-------
222+
self : object
223+
"""
224+
import tensorflow as tf
225+
226+
# Transpose to conform to Keras input style.
227+
X = X.transpose(0, 2, 1)
228+
229+
self.input_shape = X.shape[1:]
230+
self.training_model_ = self.build_model(self.input_shape)
231+
232+
if self.verbose:
233+
self.training_model_.summary()
234+
235+
if self.use_mini_batch_size:
236+
mini_batch_size = min(self.batch_size, X.shape[0] // 10)
237+
else:
238+
mini_batch_size = self.batch_size
239+
240+
self.file_name_ = (
241+
self.best_file_name if self.save_best_model else str(time.time_ns())
242+
)
243+
244+
if self.callbacks is None:
245+
self.callbacks_ = [
246+
tf.keras.callbacks.ReduceLROnPlateau(
247+
monitor="loss", factor=0.5, patience=50, min_lr=0.0001
248+
),
249+
tf.keras.callbacks.ModelCheckpoint(
250+
filepath=self.file_path + self.file_name_ + ".keras",
251+
monitor="loss",
252+
save_best_only=True,
253+
),
254+
]
255+
else:
256+
self.callbacks_ = self._get_model_checkpoint_callback(
257+
callbacks=self.callbacks,
258+
file_path=self.file_path,
259+
file_name=self.file_name_,
260+
)
261+
262+
self.history = self.training_model_.fit(
263+
X,
264+
X,
265+
batch_size=mini_batch_size,
266+
epochs=self.n_epochs,
267+
verbose=self.verbose,
268+
callbacks=self.callbacks_,
269+
)
270+
271+
try:
272+
self.model_ = tf.keras.models.load_model(
273+
self.file_path + self.file_name_ + ".keras", compile=False
274+
)
275+
if not self.save_best_model:
276+
os.remove(self.file_path + self.file_name_ + ".keras")
277+
except FileNotFoundError:
278+
self.model_ = deepcopy(self.training_model_)
279+
280+
self._fit_clustering(X=X)
281+
282+
gc.collect()
283+
284+
return self
285+
286+
def _score(self, X, y=None):
287+
# Transpose to conform to Keras input style.
288+
X = X.transpose(0, 2, 1)
289+
latent_space = self.model_.layers[1].predict(X)
290+
return self._estimator.score(latent_space)
291+
292+
@classmethod
293+
def get_test_params(cls, parameter_set="default"):
294+
"""Return testing parameter settings for the estimator.
295+
296+
Parameters
297+
----------
298+
parameter_set : str, default="default"
299+
Name of the set of test parameters to return, for use in tests. If no
300+
special parameters are defined for a value, will return `"default"` set.
301+
For classifiers, a "default" set of parameters should be provided for
302+
general testing, and a "results_comparison" set for comparing against
303+
previously recorded results if the general set does not produce suitable
304+
probabilities to compare against.
305+
306+
Returns
307+
-------
308+
params : dict or list of dict, default={}
309+
Parameters to create testing instances of the class.
310+
Each dict are parameters to construct an "interesting" test instance, i.e.,
311+
`MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
312+
`create_test_instance` uses the first (or only) dictionary in `params`.
313+
"""
314+
param1 = {
315+
"estimator": DummyClusterer(n_clusters=2),
316+
"n_epochs": 1,
317+
"batch_size": 4,
318+
"n_layers": 1,
319+
"n_units": 2,
320+
}
321+
322+
return [param1]

0 commit comments

Comments
 (0)