From 2c5c872af46166a2aada0fb9b1446cc4b9996bd8 Mon Sep 17 00:00:00 2001 From: Jad-yehya Date: Tue, 19 Nov 2024 10:53:57 +0100 Subject: [PATCH] Cleaned solvers code and added comments --- solvers/AR.py | 5 ++++- solvers/abod.py | 14 ++++++++++++-- solvers/cblof.py | 11 ++++++++++- solvers/dif.py | 24 +++++++++++++----------- solvers/isolation-forest.py | 6 ++++++ solvers/lof.py | 6 ++++++ solvers/lstm.py | 5 ++--- solvers/ocsvm.py | 9 ++++++++- solvers/vanilla-transformer.py | 16 +++++++--------- 9 files changed, 68 insertions(+), 28 deletions(-) diff --git a/solvers/AR.py b/solvers/AR.py index a1d81c7..ff2d547 100644 --- a/solvers/AR.py +++ b/solvers/AR.py @@ -11,7 +11,7 @@ class Solver(BaseSolver): - name = "AR" + name = "AR" # AutoRegressive Linear model install_cmd = "conda" requirements = ["pip:torch", "tqdm"] @@ -130,11 +130,13 @@ def run(self, _): xw_hat, 1 ) + # Calculating the percentile value for the threshold percentile_value = np.percentile( np.abs(self.X_test[self.window_size:] - x_hat[self.window_size:]), self.percentile ) + # Thresholding predictions = np.zeros_like(x_hat)-1 predictions[self.window_size:] = np.where( np.abs(self.X_test[self.window_size:] - @@ -143,6 +145,7 @@ def run(self, _): self.predictions = np.max(predictions, axis=1) + # Skipping the solver call if a condition is met def skip(self, X_train, X_test, y_test): if X_train.shape[0] < self.window_size + self.horizon: return True, "No enough training samples" diff --git a/solvers/abod.py b/solvers/abod.py index 2c22ba6..6ff02ae 100644 --- a/solvers/abod.py +++ b/solvers/abod.py @@ -9,7 +9,7 @@ class Solver(BaseSolver): - name = "ABOD" + name = "ABOD" # Angle-Based Outlier Detection install_cmd = "conda" requirements = ["pip:pyod"] @@ -34,8 +34,10 @@ def set_objective(self, X_train, y_test, X_test): ) def run(self, _): + # Using only windowed data, parameter used only for consistency if self.window: - # We need to transform the data to have a rolling window + + # Transofrming the data into rolling windowed data if self.X_train is not None: self.Xw_train = np.lib.stride_tricks.sliding_window_view( self.X_train, window_shape=self.window_size, axis=0 @@ -51,6 +53,7 @@ def run(self, _): self.y_test, window_shape=self.window_size, axis=0 )[::self.stride] + # Flattening the data for the model flatrain = self.Xw_train.reshape(self.Xw_train.shape[0], -1) flatest = self.Xw_test.reshape(self.Xw_test.shape[0], -1) @@ -64,18 +67,25 @@ def run(self, _): (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score ) + # Function used to skip a solver call when n_neighbors >= window_size def skip(self, X_train, X_test, y_test): if self.n_neighbors >= self.window_size: return True, "Number of neighbors greater than number of samples." diff --git a/solvers/cblof.py b/solvers/cblof.py index fd9454f..3e44432 100644 --- a/solvers/cblof.py +++ b/solvers/cblof.py @@ -33,8 +33,9 @@ def set_objective(self, X_train, y_test, X_test): ) def run(self, _): - + # Using only windowed data, parameter used only for consistency if self.window: + # We need to transform the data to have a rolling window if self.X_train is not None: self.Xw_train = np.lib.stride_tricks.sliding_window_view( @@ -51,6 +52,7 @@ def run(self, _): self.y_test, window_shape=self.window_size, axis=0 )[::self.stride] + # Flattening the data for the model flatrain = self.Xw_train.reshape(self.Xw_train.shape[0], -1) flatest = self.Xw_test.reshape(self.Xw_test.shape[0], -1) @@ -63,18 +65,25 @@ def run(self, _): (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score ) + # Skipping the solver call if a condition is met def skip(self, X_train, X_test, y_test): if X_train.shape[0] < self.window_size: return True, "No enough samples to create a window" diff --git a/solvers/dif.py b/solvers/dif.py index 3f0421b..6aeef8e 100644 --- a/solvers/dif.py +++ b/solvers/dif.py @@ -3,7 +3,6 @@ from benchopt import safe_import_context with safe_import_context() as import_ctx: - from benchopt.utils.sys_info import get_cuda_version from pyod.models.dif import DIF import numpy as np @@ -26,15 +25,15 @@ class Solver(BaseSolver): def set_objective(self, X_train, y_test, X_test): self.X_train = X_train self.X_test, self.y_test = X_test, y_test - if get_cuda_version() is None: - self.clf = DIF(contamination=self.contamination) - else: - self.clf = DIF(contamination=self.contamination, device="cuda") + # Device is automatically selected by the model + # if device=None + self.clf = DIF(contamination=self.contamination, device=None) def run(self, _): - + # Using only windowed data, parameter used only for consistency if self.window: - # We need to transform the data to have a rolling window + + # Transofrming the data into rolling windowed data if self.X_train is not None: self.Xw_train = np.lib.stride_tricks.sliding_window_view( self.X_train, window_shape=self.window_size, axis=0 @@ -50,6 +49,7 @@ def run(self, _): self.y_test, window_shape=self.window_size, axis=0 )[::self.stride] + # Flattening the data for the model flatrain = self.Xw_train.reshape(self.Xw_train.shape[0], -1) flatest = self.Xw_test.reshape(self.Xw_test.shape[0], -1) @@ -62,23 +62,25 @@ def run(self, _): (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score ) def skip(self, X_train, X_test, y_test): - # If cuda is not available, we skip the test because deep method - # from benchopt.utils.sys_info import get_cuda_version - # if get_cuda_version() is None: - # return True, "Cuda is not available" if X_train.shape[0] < self.window_size: return True, "Not enough samples to create a window" return False, None diff --git a/solvers/isolation-forest.py b/solvers/isolation-forest.py index 0372e9c..dac03e3 100644 --- a/solvers/isolation-forest.py +++ b/solvers/isolation-forest.py @@ -58,13 +58,19 @@ def run(self, _): (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score diff --git a/solvers/lof.py b/solvers/lof.py index 5bd2829..1ce2058 100644 --- a/solvers/lof.py +++ b/solvers/lof.py @@ -63,13 +63,19 @@ def run(self, _): (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score diff --git a/solvers/lstm.py b/solvers/lstm.py index fd5683e..b3e128f 100644 --- a/solvers/lstm.py +++ b/solvers/lstm.py @@ -101,6 +101,7 @@ def run(self, _): ti = tqdm(range(self.n_epochs), desc="epoch", leave=True) + # Training loop for epoch in ti: self.model.train() train_loss = 0 @@ -122,6 +123,7 @@ def run(self, _): # Saving the model torch.save(self.model.state_dict(), "model.pth") + # Test loop self.model.eval() raw_reconstruction = [] for x in self.test_loader: @@ -147,9 +149,6 @@ def run(self, _): ) def skip(self, X_train, X_test, y_test): - # from benchopt.utils.sys_info import get_cuda_version - # if get_cuda_version() is None: - # return True, "CUDA is not available. Skipping this solver." if X_train.shape[0] < self.window_size: return True, "Not enough samples to create a window." return False, None diff --git a/solvers/ocsvm.py b/solvers/ocsvm.py index 214230f..268e57c 100644 --- a/solvers/ocsvm.py +++ b/solvers/ocsvm.py @@ -56,17 +56,24 @@ def run(self, _): raw_y_hat = self.clf.predict(self.flatest) raw_anomaly_score = self.clf.decision_function(self.flatest) + # The results we get has a shape of result_shape = ( (self.X_train.shape[0] - self.window_size) // self.stride ) + 1 + # Mapping the binary output from {-1, 1} to {1, 0} + # For consistency with the other solvers self.raw_y_hat = np.array(raw_y_hat) + + # Adding -1 for the non predicted samples + # The first window_size samples are not predicted by the model self.raw_y_hat = np.where(self.raw_y_hat == -1, 1, 0) self.raw_y_hat = np.append( np.full(self.X_train.shape[0] - result_shape, -1), self.raw_y_hat ) + # Anomaly scores (Not used but allows finer thresholding) self.raw_anomaly_score = np.array(raw_anomaly_score) self.raw_anomaly_score = np.append( np.full(result_shape, -1), self.raw_anomaly_score @@ -74,7 +81,7 @@ def run(self, _): def skip(self, X_train, X_test, y_test): if X_train.shape[0] < self.window_size: - return True, "Window size is larger than dataset size. Skipping." + return True, "Window size is larger than dataset size." return False, None def get_result(self): diff --git a/solvers/vanilla-transformer.py b/solvers/vanilla-transformer.py index 88a55a2..677cfca 100644 --- a/solvers/vanilla-transformer.py +++ b/solvers/vanilla-transformer.py @@ -57,6 +57,7 @@ def set_objective(self, X_train, y_test, X_test): self.optimizer, mode='min', factor=0.5, patience=5 ) + # Using only windowed data, parameter used only for consistency if self.window: if self.X_train is not None: self.Xw_train = np.lib.stride_tricks.sliding_window_view( @@ -91,6 +92,7 @@ def run(self, _): patience = 20 no_improve = 0 + # Training loop for epoch in ti: self.model.train() total_loss = 0 @@ -115,7 +117,7 @@ def run(self, _): total_loss += loss.item() avg_loss = total_loss / (len(self.Xw_train) // self.batch_size) - ti.set_description(f"Epoch {epoch} (loss={avg_loss:.5e})") # noqa + ti.set_description(f"Epoch {epoch} (loss={avg_loss:.5e})") # Learning rate scheduling self.scheduler.step(avg_loss) @@ -128,13 +130,11 @@ def run(self, _): else: no_improve += 1 if no_improve == patience: - # print("Early stopping!") break - # self.model.load_state_dict(torch.load('best_model.pth')) - + # Test loop self.model.eval() - batch_size = 1024 # Adjust this based on your GPU memory + batch_size = 1024 all_predictions = [] with torch.no_grad(): @@ -161,11 +161,13 @@ def run(self, _): x_hat[self.window_size+self.horizon:] = mean_overlaping_pred( xw_hat, 1) + # Calculating the percentile value for the threshold percentile_value = np.percentile( np.abs(self.X_test[self.window_size:] - x_hat[self.window_size:]), self.percentile ) + # Thresholding predictions = np.zeros_like(x_hat)-1 predictions[self.window_size:] = np.where( np.abs(self.X_test[self.window_size:] - @@ -177,10 +179,6 @@ def run(self, _): def skip(self, X_train, X_test, y_test): if X_train.shape[0] < self.window_size + self.horizon: return True, "No enough training samples" - - if X_test.shape[0] < self.window_size + self.horizon: - return True, "No enough testing samples" - return False, None def get_result(self):