From 3c79e379a00cb5ff4ce8231e0dc8c34a5a328da6 Mon Sep 17 00:00:00 2001 From: KovalenkoAE Date: Wed, 14 Aug 2024 23:47:37 +0300 Subject: [PATCH] update --- docs/.nojekyll | 0 docs/_images/HI_deviation.png | Bin 112570 -> 0 bytes .../ice/anomaly_detection/datasets.html | 24 + .../ice/anomaly_detection/metrics.html | 45 +- .../anomaly_detection/models/autoencoder.html | 75 +- .../ice/anomaly_detection/models/base.html | 180 +- .../ice/anomaly_detection/models/gnn.html | 607 ++ .../ice/anomaly_detection/models/stgat.html | 733 ++ .../anomaly_detection/models/transformer.html | 778 ++ docs/_modules/ice/base.html | 726 +- .../ice/fault_diagnosis/datasets.html | 14 + .../ice/fault_diagnosis/models/base.html | 104 +- .../ice/fault_diagnosis/models/mlp.html | 20 +- .../ice/fault_diagnosis/models/tcn.html | 20 +- .../ice/health_index_estimation/datasets.html | 22 +- .../ice/health_index_estimation/metrics.html | 13 + .../health_index_estimation/models/base.html | 123 +- .../health_index_estimation/models/mlp.html | 16 +- .../datasets.html | 51 +- .../metrics.html | 2 +- .../models/base.html | 121 +- .../models/lstm.html | 556 ++ .../models/mlp.html | 25 +- docs/_modules/index.html | 4 + docs/_sources/advanced/index.rst.txt | 5 +- .../advanced/optimization_tutorial.ipynb.txt | 881 +++ .../ad_benchmark_autorncodermlp_256.ipynb.txt | 6745 ----------------- .../benchmark/ad_benchmark_gnn.ipynb.txt | 240 + .../benchmark/ad_benchmark_stgat.ipynb.txt | 242 + .../ad_benchmark_transformer.ipynb.txt | 245 + .../benchmark/fd_benchmark_tcn.ipynb.txt | 461 ++ docs/_sources/benchmark/hi_sota.ipynb.txt | 338 + docs/_sources/benchmark/index.rst.txt | 8 +- docs/_sources/benchmark/rul_sota.ipynb.txt | 187 + .../ice.anomaly_detection.models.rst.txt | 23 +- .../ice.fault_diagnosis.models.rst.txt | 3 - ...ice.health_index_estimation.models.rst.txt | 5 +- ...ning_useful_life_estimation.models.rst.txt | 10 +- docs/_sources/reference/ice.rst.txt | 27 - docs/_sources/reference/modules.rst.txt | 7 - docs/_sources/start/datasets.rst.txt | 5 - docs/_sources/start/tasks/task_hi.rst.txt | 12 +- docs/_static/hi/HI_deviation.png | Bin 112570 -> 0 bytes docs/advanced/index.html | 25 +- docs/advanced/optimization_tutorial.html | 995 +++ .../ad_benchmark_autoencodermlp_256.html | 6 +- .../ad_benchmark_gnn.html} | 144 +- .../ad_benchmark_stgat.html} | 164 +- ...256.html => ad_benchmark_transformer.html} | 131 +- docs/benchmark/fd_benchmark_mlp_256.html | 6 +- docs/benchmark/fd_benchmark_tcn.html | 753 ++ docs/benchmark/hi_sota.html | 665 ++ docs/benchmark/index.html | 12 + docs/benchmark/rul_sota.html | 593 ++ docs/genindex.html | 130 +- docs/objects.inv | Bin 1790 -> 2013 bytes docs/py-modindex.html | 15 - .../ice.anomaly_detection.datasets.html | 31 + docs/reference/ice.anomaly_detection.html | 8 +- .../ice.anomaly_detection.metrics.html | 52 +- .../ice.anomaly_detection.models.html | 208 +- docs/reference/ice.base.html | 98 +- .../ice.fault_diagnosis.datasets.html | 31 + docs/reference/ice.fault_diagnosis.html | 1 + .../reference/ice.fault_diagnosis.models.html | 51 +- .../ice.health_index_estimation.html | 3 +- .../ice.health_index_estimation.metrics.html | 21 + .../ice.health_index_estimation.models.html | 51 +- docs/reference/ice.html | 653 -- ...ining_useful_life_estimation.datasets.html | 28 + .../ice.remaining_useful_life_estimation.html | 6 +- ...aining_useful_life_estimation.metrics.html | 38 +- ...maining_useful_life_estimation.models.html | 87 +- docs/reference/index.html | 1 + docs/searchindex.js | 2 +- docs/start/tasks/task_hi.html | 27 - 76 files changed, 10245 insertions(+), 8494 deletions(-) delete mode 100644 docs/.nojekyll delete mode 100644 docs/_images/HI_deviation.png create mode 100644 docs/_modules/ice/anomaly_detection/models/gnn.html create mode 100644 docs/_modules/ice/anomaly_detection/models/stgat.html create mode 100644 docs/_modules/ice/anomaly_detection/models/transformer.html create mode 100644 docs/_modules/ice/remaining_useful_life_estimation/models/lstm.html create mode 100644 docs/_sources/advanced/optimization_tutorial.ipynb.txt delete mode 100644 docs/_sources/benchmark/ad_benchmark_autorncodermlp_256.ipynb.txt create mode 100644 docs/_sources/benchmark/ad_benchmark_gnn.ipynb.txt create mode 100644 docs/_sources/benchmark/ad_benchmark_stgat.ipynb.txt create mode 100644 docs/_sources/benchmark/ad_benchmark_transformer.ipynb.txt create mode 100644 docs/_sources/benchmark/fd_benchmark_tcn.ipynb.txt create mode 100644 docs/_sources/benchmark/hi_sota.ipynb.txt create mode 100644 docs/_sources/benchmark/rul_sota.ipynb.txt delete mode 100644 docs/_sources/reference/ice.rst.txt delete mode 100644 docs/_sources/reference/modules.rst.txt delete mode 100644 docs/_sources/start/datasets.rst.txt delete mode 100644 docs/_static/hi/HI_deviation.png create mode 100644 docs/advanced/optimization_tutorial.html rename docs/{start/datasets.html => benchmark/ad_benchmark_gnn.html} (61%) rename docs/{reference/modules.html => benchmark/ad_benchmark_stgat.html} (57%) rename docs/benchmark/{ad_benchmark_autorncodermlp_256.html => ad_benchmark_transformer.html} (60%) create mode 100644 docs/benchmark/fd_benchmark_tcn.html create mode 100644 docs/benchmark/hi_sota.html create mode 100644 docs/benchmark/rul_sota.html delete mode 100644 docs/reference/ice.html diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/_images/HI_deviation.png b/docs/_images/HI_deviation.png deleted file mode 100644 index aed36e9256ed17c68933447ea038ce208e6d043f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112570 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 P9n5~~J!mz&1Ny=2|GtdNU%mD`!tHl* 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 F{{zEKaF+l8 diff --git a/docs/_modules/ice/anomaly_detection/datasets.html b/docs/_modules/ice/anomaly_detection/datasets.html index da93e55..b6df286 100644 --- a/docs/_modules/ice/anomaly_detection/datasets.html +++ b/docs/_modules/ice/anomaly_detection/datasets.html @@ -414,6 +414,30 @@

    Source code for ice.anomaly_detection.datasets

    + + +
    [docs]class AnomalyDetectionRiethTEP(BaseDataset): + """ + Dataset of Tennessee Eastman Process dataset + Rieth, C. A., Amsel, B. D., Tran, R., & Cook, M. B. (2017). + Additional Tennessee Eastman Process Simulation Data for + Anomaly Detection Evaluation (Version V1) [Computer software]. + Harvard Dataverse. + https://doi.org/10.7910/DVN/6C3JR1. + """ + def __init__(self, num_chunks=None, force_download=False): + self.df = None + self.test_mask = None + self.name = None + self.public_link = None + self.set_name_public_link() + self._load(num_chunks, force_download) + self.target[self.target != 0] = 1 + self.train_mask[self.target != 0] = False + +
    diff --git a/docs/_modules/ice/anomaly_detection/metrics.html b/docs/_modules/ice/anomaly_detection/metrics.html index ac961de..1f94cf9 100644 --- a/docs/_modules/ice/anomaly_detection/metrics.html +++ b/docs/_modules/ice/anomaly_detection/metrics.html @@ -366,19 +366,56 @@

    Source code for ice.anomaly_detection.metrics

    -
    [docs]def accuracy(pred: list, target: list) -> float: +import numpy as np +from sklearn.metrics import confusion_matrix + + +
    [docs]def accuracy(pred: np.ndarray, target: np.ndarray) -> float: """ Accuracy of the classification is the number of true positives divided by the number of examples. Args: - pred (list): predictions. - target (list): target values. + pred (np.ndarray): predictions. + target (np.ndarray): target values. Returns: - float: accuracy + float: accuracy. """ return sum(pred == target) / len(pred)
    + + +
    [docs]def true_positive_rate(pred: np.ndarray, target: np.ndarray) -> np.ndarray[float]: + """ + True Positive Rate is the number of detected faults i divided by the + number of faults i. + + Args: + pred (np.ndarray): predictions. + target (np.ndarray): target values. + + Returns: + list: list of float values with true positive rate for each fault. + """ + cm = confusion_matrix(target, pred, labels=np.arange(target.max() + 1)) + correct = cm[1:, 1:].diagonal() + return list(correct / cm[1:].sum(axis=1))
    + + +
    [docs]def false_positive_rate(pred: np.ndarray, target: np.ndarray) -> np.ndarray[float]: + """ + False Positive Rate, aka False Alarm Rate is the number of false alarms i + divided by the number of normal samples. + + Args: + pred (np.ndarray): predictions. + target (np.ndarray): target values. + + Returns: + list: list of float values with true positive rate for each fault. + """ + cm = confusion_matrix(target, pred, labels=np.arange(target.max() + 1)) + return list(cm[0, 1:] / cm[0].sum())
    diff --git a/docs/_modules/ice/anomaly_detection/models/autoencoder.html b/docs/_modules/ice/anomaly_detection/models/autoencoder.html index 84bb12e..34a334d 100644 --- a/docs/_modules/ice/anomaly_detection/models/autoencoder.html +++ b/docs/_modules/ice/anomaly_detection/models/autoencoder.html @@ -366,8 +366,7 @@

    Source code for ice.anomaly_detection.models.autoencoder

    -from pandas import DataFrame
    -from sklearn.preprocessing import StandardScaler
    +import pandas as pd
     from torch import nn
     
     from ice.anomaly_detection.models.base import BaseAnomalyDetection
    @@ -376,48 +375,41 @@ 

    Source code for ice.anomaly_detection.models.autoencoder

    [docs]class MLP(nn.Module): def __init__( self, - num_sensors: int, + input_dim: int, window_size: int, - num_layers: int, hidden_dims: list, - type: str + decoder: bool = False, ): super().__init__() - self.num_sensors = num_sensors + self.input_dim = input_dim self.window_size = window_size - self.hidden_dims = [num_sensors * window_size] - self.type = type - if hidden_dims is not None and self.type == 'encoder': - self.hidden_dims = self.hidden_dims + hidden_dims - elif hidden_dims is not None and self.type == 'decoder': + self.hidden_dims = [input_dim * window_size] + self.decoder = decoder + if self.decoder: self.hidden_dims = hidden_dims + self.hidden_dims else: - for i in range(num_layers): - dim = self.hidden_dims[i] // 2 - self.hidden_dims.append(dim) - if self.type == 'decoder': - self.hidden_dims.reverse() + self.hidden_dims = self.hidden_dims + hidden_dims self.mlp = nn.Sequential(nn.Flatten()) - for i in range(num_layers): + for i in range(len(hidden_dims)): self.mlp.append(nn.Linear( self.hidden_dims[i], self.hidden_dims[i + 1]) ) - if self.type == 'decoder' and i + 1 == num_layers: + if self.decoder and i + 1 == len(hidden_dims): break self.mlp.append(nn.ReLU())
    [docs] def forward(self, x): output = self.mlp(x) - if self.type == 'decoder': - return output.view(-1, self.window_size, self.num_sensors) + if self.decoder: + return output.view(-1, self.window_size, self.input_dim) return output
    [docs]class AutoEncoderMLP(BaseAnomalyDetection): """ - Autoencoder (AE) consists of encoder and decoder parts. Each + MLP autoencoder consists of MLP encoder and MLP decoder parts. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) for calculations and to a vector (B, L * C) -> (B, L, C) for the output. Where B is the batch size, L is the sequence length, C is the number of sensors. @@ -425,18 +417,24 @@

    Source code for ice.anomaly_detection.models.autoencoder

    def __init__( self, window_size: int, + stride: int = 1, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'ae_anomaly_detection', - threshold: float = 0.95, - hidden_dims: list = [256, 128, 64], + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False, + threshold_level: float = 0.95, + hidden_dims: list = [256, 128, 64] ): """ Args: window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive + sliding windows in training. batch_size (int): The batch size to train the model. lr (float): The larning rate to train the model. num_epochs (float): The number of epochs to train the model. @@ -444,20 +442,19 @@

    Source code for ice.anomaly_detection.models.autoencoder

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. - threshold (float): The boundary for anomaly detection. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + threshold_level (float): Takes a value from 0 to 1. It specifies + the quantile in the distribution of errors on the training + dataset at which the threshold value is set. hidden_dims (list): Dimensions of hidden layers in encoder/decoder. """ super().__init__( - window_size, batch_size, lr, num_epochs, device, verbose, name, threshold + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, + val_ratio, save_checkpoints, threshold_level ) - - self.window_size = window_size - self.num_layers = len(hidden_dims) self.hidden_dims = hidden_dims - self.threshold = threshold - self.loss_fn = nn.MSELoss(reduction='mean') - self.preprocessing = True - self.scaler = StandardScaler() _param_conf_map = dict(BaseAnomalyDetection._param_conf_map, **{ @@ -465,24 +462,20 @@

    Source code for ice.anomaly_detection.models.autoencoder

    } ) - def _create_model(self, df: DataFrame): - num_sensors = df.shape[1] + def _create_model(self, input_dim: int, output_dim: int): self.model = nn.Sequential( MLP( - num_sensors, + input_dim, self.window_size, - self.num_layers, hidden_dims=self.hidden_dims, - type='encoder' ), MLP( - num_sensors, + input_dim, self.window_size, - self.num_layers, hidden_dims=self.hidden_dims[::-1], - type='decoder' + decoder=True ) - )
    + )
    diff --git a/docs/_modules/ice/anomaly_detection/models/base.html b/docs/_modules/ice/anomaly_detection/models/base.html index 714b56e..71c0600 100644 --- a/docs/_modules/ice/anomaly_detection/models/base.html +++ b/docs/_modules/ice/anomaly_detection/models/base.html @@ -369,15 +369,16 @@

    Source code for ice.anomaly_detection.models.base

    from abc import ABC, abstractmethod import numpy as np import pandas as pd -from tqdm.auto import trange, tqdm - +from tqdm.auto import tqdm import torch -from torch.optim import Adam +import torch.nn as nn from torch.utils.data import DataLoader +from torch.optim import Adam +import optuna -from ice.anomaly_detection.utils import SlidingWindowDataset -from ice.base import BaseModel -from ice.anomaly_detection.metrics import accuracy +from ice.base import BaseModel, SlidingWindowDataset +from ice.anomaly_detection.metrics import ( + accuracy, true_positive_rate, false_positive_rate)
    [docs]class BaseAnomalyDetection(BaseModel, ABC): @@ -387,17 +388,23 @@

    Source code for ice.anomaly_detection.models.base

    def __init__( self, window_size: int, + stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, - threshold: float = 0.95 - ): + random_seed: int, + val_ratio: float, + save_checkpoints: bool, + threshold_level: float = 0.95 + ): """ Args: window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive + sliding windows in training. batch_size (int): The batch size to train the model. lr (float): The learning rate to train the model. num_epochs (float): The number of epochs to train the model. @@ -405,105 +412,98 @@

    Source code for ice.anomaly_detection.models.base

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + threshold_level (float): Takes a value from 0 to 1. It specifies + the quantile in the distribution of errors on the training + dataset at which the threshold value is set. """ - super().__init__(batch_size, lr, num_epochs, device, verbose, name) - self._cfg.path_set(["TASK"], "anomaly_detection") + super().__init__(window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints) + self.val_metrics = False + + self.threshold_level = threshold_level + self.threshold_value = None - self.window_size = window_size - self.loss_fn = None - self.preprocessing = False - self.scaler = None - self.threshold = None +
    [docs] def fit(self, df: pd.DataFrame, target: pd.Series = None, + epochs: int = None, save_path: str = None, trial: optuna.Trial = None, + force_model_ctreation: bool = False): + """Fit (train) the model by a given dataset. + + Args: + df (pandas.DataFrame): A dataframe with sensor data. Index has + two columns: `run_id` and `sample`. All other columns a value of + sensors. + target (pandas.Series): A series with target values. Indes has two + columns: `run_id` and `sample`. It is omitted for anomaly + detection task. + epochs (int): The number of epochs for training step. If None, + self.num_epochs parameter is used. + save_path (str): Path to save checkpoints. If None, the path is + created automatically. + """ + if trial: + super().fit(df, target, epochs, save_path, trial=trial, force_model_ctreation=True) + else: + super().fit(df, target, epochs, save_path) + + error = [] + for sample, target in tqdm( + self.dataloader, desc='Steps ...', leave=False, disable=(not self.verbose) + ): + sample = sample.to(self.device) + with torch.no_grad(): + pred = self.model(sample) + error.append(self.loss_no_reduction(pred, sample).mean(dim=(1, 2))) + error = torch.concat(error) + self.threshold_value = torch.quantile(error, self.threshold_level).item() + if self.save_checkpoints: + self.save_checkpoint(save_path)
    _param_conf_map = dict(BaseModel._param_conf_map, **{ - "window_size": ["MODEL", "WINDOW_SIZE"], - "threshold": ["MODEL", "THRESHOLD"] + "threshold_level": ["MODEL", "THRESHOLD_LEVEL"] } ) - -
    [docs] def fit(self, df: pd.DataFrame): - """ Method fit for training anomaly detection models. + +
    [docs] def load_checkpoint(self, checkpoint_path: str): + """Load checkpoint. Args: - df (pd.DataFrame): data without anomaly states + checkpoint_path (str): Path to load checkpoint. """ - assert len(df) >= self.window_size, "window size is larger than the length of df." - if self.preprocessing: - self.scaler.fit(df) - df.loc[:] = self.scaler.transform(df) - self._create_model(df) - self._train_nn(df)
    - - def _fit(self, df: pd.DataFrame): - pass + super().load_checkpoint(checkpoint_path) + self.threshold_value = self._cfg['MODEL']['THRESHOLD_VALUE']
    + + def _prepare_for_training(self, input_dim: int, output_dim: int): + self.optimizer = Adam(self.model.parameters(), lr=self.lr) + self.loss_fn = nn.L1Loss() + self.loss_no_reduction = nn.L1Loss(reduction='none') def _predict(self, sample: torch.Tensor) -> torch.Tensor: input = sample.to(self.device) output = self.model(input) - return output.cpu() - - def _train_nn(self, df: pd.DataFrame): - self.model.train() - self.model.to(self.device) - self.optimizer = Adam(self.model.parameters(), lr=self.lr) - - dataset = SlidingWindowDataset(df, window_size=self.window_size) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - errors = [] - for e in trange(self.num_epochs, desc='Epochs ...', disable=(not self.verbose)): - for sample in tqdm(self.dataloader, desc='Steps ...', leave=False, disable=(not self.verbose)): - input = sample.to(self.device) - output = self.model(input) - loss = self.loss_fn(output, input) - self.optimizer.zero_grad() - loss.backward() - self.optimizer.step() - error = torch.sum(torch.abs(input - output), dim=(1, 2)) - errors = np.append(errors, error.detach().numpy()) - if self.verbose: - print(f'Epoch {e+1}, Loss: {loss.item():.4f}') - self.threshold_value = np.quantile(errors, self.threshold) - -
    [docs] def evaluate(self, df: pd.DataFrame, target: pd.Series) -> dict: - """Evaluate the metrics: accuracy. - - Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of - sensors. - target (pandas.Series): A series with target values. Indes has two - columns: `run_id` and `sample`. + error = self.loss_no_reduction(output, input).mean(dim=(1, 2)) + return (error > self.threshold_value).float().cpu() + + def _validate_inputs(self, df: pd.DataFrame, target: pd.Series): + if target is not None: + assert df.shape[0] == target.shape[0], f"target is incompatible with df by the length: {df.shape[0]} and {target.shape[0]}." + assert np.all(df.index == target.index), "target's index and df's index are not the same." + assert df.index.names == (['run_id', 'sample']), "An index should contain columns `run_id` and `sample`." + assert len(df) >= self.window_size, "window size is larger than the length of df." - Returns: - dict: A dictionary with metrics where keys are names of metrics and - values are values of metrics. - """ - assert df.shape[0] == target.shape[0], f"target is incompatible with df by the length: {df.shape[0]} and {target.shape[0]}." - assert np.all(df.index == target.index), "target's index and df's index are not the same." - assert df.index.names == (['run_id', 'sample']), "An index should contain columns `run_id` and `sample`." - - if self.preprocessing: - df.loc[:] = self.scaler.transform(df) - dataset = SlidingWindowDataset(df, window_size=self.window_size, target=target) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - target, pred = [], [] - for sample, _target in tqdm( - self.dataloader, desc='Steps ...', leave=False, disable=(not self.verbose) - ): - input = sample.to(self.device) - target.append(_target) - with torch.no_grad(): - output = self.predict(input) - error = torch.sum(torch.abs(input - output), dim=(1, 2)) - pred.append((error > self.threshold_value).float()) - target = torch.concat(target).numpy() - pred = torch.concat(pred).numpy() + def _calculate_metrics(self, pred: torch.tensor, target: torch.tensor) -> dict: metrics = { - 'accuracy': accuracy(pred, target) + 'accuracy': accuracy(pred, target), + 'true_positive_rate': true_positive_rate(pred, target), + 'false_positive_rate': false_positive_rate(pred, target), } - self._store_atrifacts_inference(metrics) - return metrics
    + return metrics + + def _set_dims(self, df: pd.DataFrame, target: pd.Series): + self.input_dim = df.shape[1] + self.output_dim = 1
    diff --git a/docs/_modules/ice/anomaly_detection/models/gnn.html b/docs/_modules/ice/anomaly_detection/models/gnn.html new file mode 100644 index 0000000..700432f --- /dev/null +++ b/docs/_modules/ice/anomaly_detection/models/gnn.html @@ -0,0 +1,607 @@ + + + + + + + + + + + ice.anomaly_detection.models.gnn — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for ice.anomaly_detection.models.gnn

    +import torch
    +from torch import nn
    +from torch.nn import functional as F
    +from pandas import DataFrame, Series
    +
    +from ice.anomaly_detection.models.base import BaseAnomalyDetection
    +
    +
    +class GCLayer(nn.Module):
    +    def __init__(
    +            self,
    +            in_dim: int,
    +            out_dim: int
    +            ):
    +        super().__init__()
    +        self.dense = nn.Linear(in_dim, out_dim)
    +    
    +    def forward(self, adj, x):
    +        adj = adj + torch.eye(adj.size(0)).to(x.device)
    +        x = self.dense(x)
    +        norm = adj.sum(1)**(-1/2)
    +        x = norm[None, :] * adj * norm[:, None] @ x
    +
    +        return x
    +
    +
    +class Directed_A(nn.Module):
    +    def __init__(
    +            self,
    +            num_sensors: int,
    +            window_size: int,
    +            alpha: float,
    +            k: int
    +            ):
    +        super().__init__()
    +        self.alpha = alpha
    +        self.k = k
    +
    +        self.e1 = nn.Embedding(num_sensors, window_size)
    +        self.e2 = nn.Embedding(num_sensors, window_size)
    +        self.l1 = nn.Linear(window_size,window_size)
    +        self.l2 = nn.Linear(window_size,window_size)
    +    
    +    def forward(self, idx):
    +        m1 = torch.tanh(self.alpha*self.l1(self.e1(idx)))
    +        m2 = torch.tanh(self.alpha*self.l2(self.e2(idx)))
    +        adj = F.relu(torch.tanh(self.alpha*torch.mm(m1, m2.transpose(1,0))))
    +        
    +        if self.k:
    +            mask = torch.zeros(idx.size(0), idx.size(0)).to(idx.device)
    +            mask.fill_(float('0'))
    +            s1,t1 = (adj + torch.rand_like(adj)*0.01).topk(self.k,1)
    +            mask.scatter_(1,t1,s1.fill_(1))
    +            adj = adj*mask
    +        
    +        return adj
    +
    +
    +class GNNEncoder(nn.Module):
    +    def __init__(
    +            self,
    +            num_sensors: int,
    +            window_size: int,
    +            alpha: float,
    +            k: int
    +            ):
    +        super().__init__()
    +        self.idx = torch.arange(num_sensors)
    +        self.gcl1 = GCLayer(window_size, window_size // 2)
    +        self.gcl2 = GCLayer(window_size // 2, window_size // 8)
    +        self.A = Directed_A(num_sensors, window_size, alpha, k)
    +    
    +    def forward(self, x):
    +        x = torch.transpose(x, 1, 2)
    +        adj = self.A(self.idx.to(x.device))
    +        x = self.gcl1(adj, x).relu()
    +        x = self.gcl2(adj, x).relu()
    +        return x
    +    
    +
    +class Decoder(nn.Module):
    +    def __init__(
    +            self,
    +            num_sensors: int,
    +            window_size: int
    +            ):
    +        super().__init__()
    +        self.num_sensors = num_sensors
    +        self.window_size = window_size
    +        self.decoder = nn.Sequential(
    +            nn.Linear(window_size // 8 * num_sensors, window_size // 2 * num_sensors),
    +            nn.ReLU(),
    +            nn.Linear(window_size // 2 * num_sensors, num_sensors * window_size)
    +        )
    +    
    +    def forward(self, x):
    +        x = torch.flatten(x,1)
    +        x = self.decoder(x)
    +
    +        return x.view(-1, self.window_size, self.num_sensors)
    +
    +
    +
    [docs]class GSL_GNN(BaseAnomalyDetection): + """ + GNN autoencoder consists of encoder with graph convolutional layers + and MLP decoder parts. The graph describing the data is constructed + during the training process using trainable parameters. + """ + def __init__( + self, + window_size: int, + stride: int = 1, + batch_size: int = 128, + lr: float = 0.001, + num_epochs: int = 10, + device: str = 'cpu', + verbose: bool = False, + name: str = 'gnn_anomaly_detection', + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False, + threshold_level: float = 0.95, + alpha: float = 0.2, + k: int = None + ): + """ + Args: + window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive + sliding windows in training. + batch_size (int): The batch size to train the model. + lr (float): The larning rate to train the model. + num_epochs (float): The number of epochs to train the model. + device (str): The name of a device to train the model. `cpu` and + `cuda` are possible. + verbose (bool): If true, show the progress bar in training. + name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + threshold_level (float): Takes a value from 0 to 1. It specifies + the quantile in the distribution of errors on the training + dataset at which the threshold value is set. + alpha (float): Saturation rate for adjacency matrix. + k (int): Limit on the number of edges in the adjacency matrix. + """ + super().__init__( + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, + val_ratio, save_checkpoints, threshold_level + ) + self.alpha = alpha + self.k = k + + _param_conf_map = dict(BaseAnomalyDetection._param_conf_map, + **{ + "alpha": ["MODEL", "ALPHA"] + } + ) + + def _create_model(self, input_dim: int, output_dim: int): + self.model = nn.Sequential( + GNNEncoder( + input_dim, + self.window_size, + self.alpha, + self.k + ), + Decoder(input_dim, self.window_size) + )
    +
    + +
    + + + + + +
    + +
    +
    +
    + +
    + + + + +
    +
    + +
    + +
    +
    +
    + + + + + +
    + + +
    + + \ No newline at end of file diff --git a/docs/_modules/ice/anomaly_detection/models/stgat.html b/docs/_modules/ice/anomaly_detection/models/stgat.html new file mode 100644 index 0000000..a6dacec --- /dev/null +++ b/docs/_modules/ice/anomaly_detection/models/stgat.html @@ -0,0 +1,733 @@ + + + + + + + + + + + ice.anomaly_detection.models.stgat — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for ice.anomaly_detection.models.stgat

    +import pandas as pd
    +import torch
    +import torch.nn as nn
    +from torch.nn import functional as F
    +from torch_geometric.nn import GCNConv, GATConv
    +
    +from ice.anomaly_detection.models.base import BaseAnomalyDetection
    +
    +
    +"""
    +The code of Stgat-Mad is taken from:
    +https://github.com/wagner-d/TimeSeAD
    +"""
    +
    +
    +class InputLayer(nn.Module):
    +    """1-D Convolution layer to extract high-level features of each time-series input
    +    :param n_features: Number of input features/nodes
    +    :param window_size: length of the input sequence
    +    :param kernel_size: size of kernel to use in the convolution operation
    +    """
    +    def __init__(self, n_features, kernel_size=7):
    +        super(InputLayer, self).__init__()
    +        self.padding = nn.ConstantPad1d((kernel_size - 1) // 2, 0.0)
    +        self.conv = nn.Conv1d(in_channels=n_features, out_channels=n_features, kernel_size=kernel_size)
    +        self.relu = nn.ReLU()
    +
    +    def forward(self, x):
    +        x = x.permute(0, 2, 1)
    +        x = self.padding(x)
    +        x = self.relu(self.conv(x))
    +        return x.permute(0, 2, 1)  # Permute back
    +
    +
    +class StgatBlock(nn.Module):
    +    def __init__(self, n_features, window_size, dropout, embed_dim=None):
    +        super(StgatBlock, self).__init__()
    +        self.n_features = n_features
    +        self.window_size = window_size
    +        self.dropout = dropout
    +        self.embed_dim = embed_dim if embed_dim is not None else n_features
    +
    +        self.embed_dim *= 2
    +
    +        self.feature_gat_layers = GATConv(window_size, window_size)
    +        self.temporal_gat_layers = GATConv(n_features, n_features)
    +
    +        self.temporal_gcn_layers = GCNConv(n_features, n_features)
    +
    +    def forward(self, data, fc_edge_index, tc_edge_index):
    +        # x shape (b, n, k): b - batch size, n - window size, k - number of features
    +        x = data.clone().detach()
    +        x = x.permute(0, 2, 1)
    +        batch_num, node_num, all_feature = x.shape
    +
    +        x = x.reshape(-1, all_feature).contiguous()
    +        f_out = self.feature_gat_layers(x, fc_edge_index)
    +        f_out = F.relu(f_out)
    +        f_out = f_out.view(batch_num, node_num, -1)
    +        f_out = f_out.permute(0, 2, 1)
    +        z = f_out.reshape(-1, node_num).contiguous()
    +
    +        t_out = self.temporal_gcn_layers(z, tc_edge_index)
    +        t_out = F.relu(t_out)
    +        t_out = t_out.view(batch_num, node_num, -1)
    +
    +        return t_out.permute(0, 2, 1)
    +
    +
    +class BiLSTMLayer(nn.Module):
    +    def __init__(self, in_dim, hid_dim, n_layers, dropout):
    +        super(BiLSTMLayer, self).__init__()
    +        self.hid_dim = hid_dim
    +        self.n_layers = n_layers
    +        self.dropout = 0.0 if n_layers == 1 else dropout
    +        self.bilstm = nn.LSTM(in_dim, hid_dim, num_layers=n_layers, batch_first=True, dropout=self.dropout, bidirectional=True)
    +
    +    def forward(self, x):
    +        out, h = self.bilstm(x)
    +        out = out.permute(1,0,2)[-1, :, :] # Extracting from last layer
    +        return out
    +
    +
    +class BiLSTMDecoder(nn.Module):
    +    def __init__(self, in_dim, hid_dim, n_layers, dropout):
    +        super(BiLSTMDecoder, self).__init__()
    +        self.in_dim = in_dim
    +        self.dropout = 0.0 if n_layers == 1 else dropout
    +        self.bilstm = nn.LSTM(in_dim, hid_dim, num_layers=n_layers, batch_first=True, dropout=self.dropout, bidirectional=True)
    +
    +    def forward(self, x):
    +        decoder_out, _ = self.bilstm(x)
    +        return decoder_out
    +
    +
    +class ReconstructionModel(nn.Module):
    +    def __init__(self, window_size, in_dim, hid_dim, out_dim, n_layers, dropout):
    +        super(ReconstructionModel, self).__init__()
    +        self.window_size = window_size
    +        self.decoder = BiLSTMDecoder(in_dim, hid_dim, n_layers, dropout)
    +        self.fc = nn.Linear(2 * hid_dim, out_dim)
    +
    +    def forward(self, x):
    +        # x will be last hidden state of the GRU layer
    +        h_end = x
    +        h_end_rep = h_end.repeat_interleave(self.window_size, dim=1).view(x.size(0), self.window_size, -1)
    +        decoder_out = self.decoder(h_end_rep)
    +        out = self.fc(decoder_out)
    +        return out
    +
    +
    +def get_batch_edge_index(org_edge_index, batch_num, node_num):
    +    # org_edge_index:(2, edge_num)
    +    edge_index = org_edge_index.clone().detach()
    +    edge_num = org_edge_index.shape[1]
    +    batch_edge_index = edge_index.repeat(1,batch_num).contiguous()
    +
    +    for i in range(batch_num):
    +        batch_edge_index[:, i*edge_num:(i+1)*edge_num] += i*node_num
    +
    +    return batch_edge_index.long()
    +
    +# graph is 'fully-connect'
    +def get_fc_graph_struc(n_features):
    +    edge_indices = torch.tensor([[i, j] for j in range(n_features) for i in range(n_features) if i != j])
    +    return edge_indices.T.contiguous()
    +
    +
    +def get_tc_graph_struc(temporal_len):
    +    edge_indices = torch.tensor([[i, j] for j in range(temporal_len) for i in range(j)])
    +    return edge_indices.T.contiguous()
    +
    +
    +
    [docs]class STGAT(nn.Module): + def __init__( + self, + n_features, + window_size, + embed_dim, + layer_numb, + lstm_n_layers, + lstm_hid_dim, + recon_n_layers, + recon_hid_dim, + dropout + ): + super(STGAT, self).__init__() + + layers1 = [] + layers2 = [] + layers3 = [] + + self.layer_numb = layer_numb + self.h_temp = [] + + self.input_1 = InputLayer(n_features, 1) + self.input_2 = InputLayer(n_features, 5) + self.input_3 = InputLayer(n_features, 7) + + for i in range(layer_numb): + layers1 += [StgatBlock(n_features, window_size, dropout, embed_dim)] + for i in range(layer_numb): + layers2 += [StgatBlock(n_features, window_size, dropout, embed_dim)] + for i in range(layer_numb): + layers3 += [StgatBlock(n_features, window_size, dropout, embed_dim)] + + self.stgat_1 = nn.Sequential(*layers1) + self.stgat_2 = nn.Sequential(*layers2) + self.stgat_3 = nn.Sequential(*layers3) + + self.bilstm = BiLSTMLayer(n_features * 3, lstm_hid_dim, lstm_n_layers, dropout) + self.recon_model = ReconstructionModel(window_size, 2 * lstm_hid_dim, recon_hid_dim, n_features, recon_n_layers, dropout) + + # Register as buffers so that tensors are moved to the correct device along with the rest of the model + self.register_buffer('fc_edge_index', get_fc_graph_struc(n_features), persistent=False) + self.register_buffer('tc_edge_index', get_tc_graph_struc(window_size), persistent=False) + +
    [docs] def forward(self, x): + # x shape (b, n, k): b - batch size, n - window size, k - number of features + fc_edge_index_sets = get_batch_edge_index(self.fc_edge_index, x.shape[0], x.shape[2]) + tc_edge_index_sets = get_batch_edge_index(self.tc_edge_index, x.shape[0], x.shape[1]) + + x_1 = x + x_2 = self.input_2(x) + x_3 = self.input_3(x) + + for layer in range(self.layer_numb): + if layer==0: + h_cat_1 = x_1 + self.stgat_1[layer](x_1, fc_edge_index_sets, tc_edge_index_sets) + h_cat_2 = x_2 + self.stgat_2[layer](x_2, fc_edge_index_sets, tc_edge_index_sets) + h_cat_3 = x_3 + self.stgat_3[layer](x_3, fc_edge_index_sets, tc_edge_index_sets) + else: + h_cat_1 = h_cat_1 + self.stgat_1[layer](h_cat_1, fc_edge_index_sets, tc_edge_index_sets) + h_cat_2 = h_cat_2 + self.stgat_2[layer](h_cat_2, fc_edge_index_sets, tc_edge_index_sets) + h_cat_3 = h_cat_3 + self.stgat_3[layer](h_cat_3, fc_edge_index_sets, tc_edge_index_sets) + + h_cat = torch.cat([h_cat_1, h_cat_2, h_cat_3], dim=2) + + out_end = self.bilstm(h_cat) + h_end = out_end.view(x.shape[0], -1) # Hidden state for last timestamp + + recons = self.recon_model(h_end) + + return recons
    + + +class STGAT_MAD(BaseAnomalyDetection): + """ + Stgat-Mad was presented at ICASSP 2022: "Stgat-Mad : Spatial-Temporal Graph + Attention Network For Multivariate Time Series Anomaly Detection". + https://ieeexplore.ieee.org/abstract/document/9747274/ + """ + def __init__( + self, + window_size: int, + stride: int = 1, + batch_size: int = 128, + lr: float = 0.001, + num_epochs: int = 10, + device: str = 'cpu', + verbose: bool = False, + name: str = 'stgat_anomaly_detection', + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False, + threshold_level: float = 0.95, + embed_dim: int = None, + layer_numb: int = 2, + lstm_n_layers: int = 1, + lstm_hid_dim: int = 150, + recon_n_layers: int = 1, + recon_hid_dim: int = 150, + dropout: float = 0.2 + ): + """ + Args: + window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive + sliding windows in training. + batch_size (int): The batch size to train the model. + lr (float): The larning rate to train the model. + num_epochs (float): The number of epochs to train the model. + device (str): The name of a device to train the model. `cpu` and + `cuda` are possible. + verbose (bool): If true, show the progress bar in training. + name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + threshold_level (float): Takes a value from 0 to 1. It specifies + the quantile in the distribution of errors on the training + dataset at which the threshold value is set. + embed_dim (int) : Embedding dimension. + layer_numb (int) : Number of layers. + lstm_n_layers (int) : Number of LSTM layers. + lstm_hid_dim (int) : Hidden dimension of LSTM layers. + recon_n_layers (int) : Number of reconstruction layers. + recon_hid_dim (int) : Hidden dimension of reconstruction layers. + dropout (float) : The rate of dropout. + """ + super().__init__( + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, + val_ratio, save_checkpoints, threshold_level + ) + self.embed_dim = embed_dim + self.layer_numb = layer_numb + self.lstm_n_layers = lstm_n_layers + self.lstm_hid_dim = lstm_hid_dim + self.recon_n_layers = recon_n_layers + self.recon_hid_dim = recon_hid_dim + self.dropout = dropout + + _param_conf_map = dict(BaseAnomalyDetection._param_conf_map, + **{ + "layer_numb": ["MODEL", "LAYER_NUMB"], + "lstm_n_layers": ["MODEL", "LSTM_N_LAYERS"], + "lstm_hid_dim": ["MODEL", "LSTM_HID_DIM"], + "recon_n_layers": ["MODEL", "RECON_N_LAYERS"], + "recon_hid_dim": ["MODEL", "RECON_HID_DIM"], + "dropout": ["MODEL", "DROPOUT"] + } + ) + + def _create_model(self, input_dim: int, output_dim: int): + self.model = STGAT( + n_features=input_dim, + window_size=self.window_size, + embed_dim=self.embed_dim, + layer_numb=self.layer_numb, + lstm_n_layers=self.lstm_n_layers, + lstm_hid_dim=self.lstm_hid_dim, + recon_n_layers=self.recon_n_layers, + recon_hid_dim=self.recon_hid_dim, + dropout=self.dropout + ) +
    + +
    + + + + + +
    + +
    +
    +
    + +
    + + + + +
    +
    + +
    + +
    +
    +
    + + + + + +
    + + +
    + + \ No newline at end of file diff --git a/docs/_modules/ice/anomaly_detection/models/transformer.html b/docs/_modules/ice/anomaly_detection/models/transformer.html new file mode 100644 index 0000000..65be837 --- /dev/null +++ b/docs/_modules/ice/anomaly_detection/models/transformer.html @@ -0,0 +1,778 @@ + + + + + + + + + + + ice.anomaly_detection.models.transformer — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for ice.anomaly_detection.models.transformer

    +import torch
    +from torch import nn
    +import torch.nn.functional as F
    +import numpy as np
    +import math
    +
    +from ice.anomaly_detection.models.base import BaseAnomalyDetection
    +
    +
    +"""
    +The code of Anomaly Transformer is taken from official implementation:
    +https://github.com/thuml/Anomaly-Transformer
    +"""
    +class TriangularCausalMask():
    +    def __init__(self, B, L, device="cpu"):
    +        mask_shape = [B, 1, L, L]
    +        with torch.no_grad():
    +            self._mask = torch.triu(torch.ones(mask_shape, dtype=torch.bool), diagonal=1).to(device)
    +
    +    @property
    +    def mask(self):
    +        return self._mask
    +
    +
    +
    [docs]class AnomalyAttention(nn.Module): + def __init__(self, win_size, mask_flag=True, scale=None, attention_dropout=0.0, output_attention=True, device="cpu"): + super(AnomalyAttention, self).__init__() + self.scale = scale + self.mask_flag = mask_flag + self.output_attention = output_attention + self.dropout = nn.Dropout(attention_dropout) + window_size = win_size + self.distances = torch.zeros((window_size, window_size)).to(device) + for i in range(window_size): + for j in range(window_size): + self.distances[i][j] = abs(i - j) + +
    [docs] def forward(self, queries, keys, values, sigma, attn_mask): + B, L, H, E = queries.shape + _, S, _, D = values.shape + scale = self.scale or 1. / math.sqrt(E) + + scores = torch.einsum("blhe,bshe->bhls", queries, keys) + if self.mask_flag: + if attn_mask is None: + attn_mask = TriangularCausalMask(B, L, device=queries.device) + scores.masked_fill_(attn_mask.mask, -np.inf) + attn = scale * scores + + sigma = sigma.transpose(1, 2) # B L H -> B H L + window_size = attn.shape[-1] + sigma = torch.sigmoid(sigma * 5) + 1e-5 + sigma = torch.pow(3, sigma) - 1 + sigma = sigma.unsqueeze(-1).repeat(1, 1, 1, window_size) # B H L L + prior = self.distances.unsqueeze(0).unsqueeze(0).repeat(sigma.shape[0], sigma.shape[1], 1, 1).to(queries.device) + prior = 1.0 / (math.sqrt(2 * math.pi) * sigma) * torch.exp(-prior ** 2 / 2 / (sigma ** 2)) + + series = self.dropout(torch.softmax(attn, dim=-1)) + V = torch.einsum("bhls,bshd->blhd", series, values) + + if self.output_attention: + return (V.contiguous(), series, prior, sigma) + else: + return (V.contiguous(), None)
    + + +
    [docs]class AttentionLayer(nn.Module): + def __init__(self, attention, d_model, n_heads, d_keys=None, + d_values=None): + super(AttentionLayer, self).__init__() + + d_keys = d_keys or (d_model // n_heads) + d_values = d_values or (d_model // n_heads) + self.norm = nn.LayerNorm(d_model) + self.inner_attention = attention + self.query_projection = nn.Linear(d_model, + d_keys * n_heads) + self.key_projection = nn.Linear(d_model, + d_keys * n_heads) + self.value_projection = nn.Linear(d_model, + d_values * n_heads) + self.sigma_projection = nn.Linear(d_model, + n_heads) + self.out_projection = nn.Linear(d_values * n_heads, d_model) + + self.n_heads = n_heads + +
    [docs] def forward(self, queries, keys, values, attn_mask): + B, L, _ = queries.shape + _, S, _ = keys.shape + H = self.n_heads + x = queries + queries = self.query_projection(queries).view(B, L, H, -1) + keys = self.key_projection(keys).view(B, S, H, -1) + values = self.value_projection(values).view(B, S, H, -1) + sigma = self.sigma_projection(x).view(B, L, H) + + out, series, prior, sigma = self.inner_attention( + queries, + keys, + values, + sigma, + attn_mask + ) + out = out.view(B, L, -1) + + return self.out_projection(out), series, prior, sigma
    + + +
    [docs]class PositionalEmbedding(nn.Module): + def __init__(self, d_model, max_len=5000): + super(PositionalEmbedding, self).__init__() + # Compute the positional encodings once in log space. + pe = torch.zeros(max_len, d_model).float() + pe.require_grad = False + + position = torch.arange(0, max_len).float().unsqueeze(1) + div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp() + + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + + pe = pe.unsqueeze(0) + self.register_buffer('pe', pe) + +
    [docs] def forward(self, x): + return self.pe[:, :x.size(1)]
    + + +
    [docs]class TokenEmbedding(nn.Module): + def __init__(self, c_in, d_model): + super(TokenEmbedding, self).__init__() + padding = 1 if torch.__version__ >= '1.5.0' else 2 + self.tokenConv = nn.Conv1d(in_channels=c_in, out_channels=d_model, + kernel_size=3, padding=padding, padding_mode='circular', bias=False) + for m in self.modules(): + if isinstance(m, nn.Conv1d): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + +
    [docs] def forward(self, x): + x = self.tokenConv(x.permute(0, 2, 1)).transpose(1, 2) + return x
    + + +
    [docs]class DataEmbedding(nn.Module): + def __init__(self, c_in, d_model, dropout=0.0): + super(DataEmbedding, self).__init__() + + self.value_embedding = TokenEmbedding(c_in=c_in, d_model=d_model) + self.position_embedding = PositionalEmbedding(d_model=d_model) + + self.dropout = nn.Dropout(p=dropout) + +
    [docs] def forward(self, x): + x = self.value_embedding(x) + self.position_embedding(x) + return self.dropout(x)
    + + +
    [docs]class EncoderLayer(nn.Module): + def __init__(self, attention, d_model, d_ff=None, dropout=0.1, activation="relu"): + super(EncoderLayer, self).__init__() + d_ff = d_ff or 4 * d_model + self.attention = attention + self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1) + self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1) + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(dropout) + self.activation = F.relu if activation == "relu" else F.gelu + +
    [docs] def forward(self, x, attn_mask=None): + new_x, attn, mask, sigma = self.attention( + x, x, x, + attn_mask=attn_mask + ) + x = x + self.dropout(new_x) + y = x = self.norm1(x) + y = self.dropout(self.activation(self.conv1(y.transpose(-1, 1)))) + y = self.dropout(self.conv2(y).transpose(-1, 1)) + + return self.norm2(x + y), attn, mask, sigma
    + + +
    [docs]class Encoder(nn.Module): + def __init__(self, attn_layers, norm_layer=None): + super(Encoder, self).__init__() + self.attn_layers = nn.ModuleList(attn_layers) + self.norm = norm_layer + +
    [docs] def forward(self, x, attn_mask=None): + # x [B, L, D] + series_list = [] + prior_list = [] + sigma_list = [] + for attn_layer in self.attn_layers: + x, series, prior, sigma = attn_layer(x, attn_mask=attn_mask) + series_list.append(series) + prior_list.append(prior) + sigma_list.append(sigma) + + if self.norm is not None: + x = self.norm(x) + + return x, series_list, prior_list, sigma_list
    + + +
    [docs]class AnomalyTransformerModel(nn.Module): + def __init__( + self, + win_size, + enc_in, + c_out, + d_model, + n_heads, + e_layers, + d_ff, + dropout, + activation, + device + ): + super(AnomalyTransformerModel, self).__init__() + + # Encoding + self.embedding = DataEmbedding(enc_in, d_model, dropout) + + # Encoder + self.encoder = Encoder( + [ + EncoderLayer( + AttentionLayer( + AnomalyAttention(win_size, False, attention_dropout=dropout, device=device), + d_model, n_heads), + d_model, + d_ff, + dropout=dropout, + activation=activation + ) for l in range(e_layers) + ], + norm_layer=torch.nn.LayerNorm(d_model) + ) + + self.projection = nn.Linear(d_model, c_out, bias=True) + +
    [docs] def forward(self, x): + enc_out = self.embedding(x) + enc_out, series, prior, sigmas = self.encoder(enc_out) + enc_out = self.projection(enc_out) + + return enc_out # [B, L, D]
    + + +
    [docs]class AnomalyTransformer(BaseAnomalyDetection): + """ + Anomaly Transformer was presented at ICLR 2022: "Anomaly Transformer: + Time Series Anomaly Detection with Association Discrepancy". + https://openreview.net/forum?id=LzQQ89U1qm_ + """ + def __init__( + self, + window_size: int = 100, + stride: int = 1, + batch_size: int = 128, + lr: float = 0.0001, + num_epochs: int = 10, + device: str = 'cpu', + verbose: bool = False, + name: str = 'transformer_anomaly_detection', + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False, + threshold_level: float = 0.95, + d_model: int = 256, + n_heads: int = 8, + e_layers: int = 3, + d_ff: int = 256, + dropout: float = 0.0, + activation: str = 'gelu' + ): + """ + Args: + window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive + sliding windows in training. + batch_size (int): The batch size to train the model. + lr (float): The larning rate to train the model. + num_epochs (float): The number of epochs to train the model. + device (str): The name of a device to train the model. `cpu` and + `cuda` are possible. + verbose (bool): If true, show the progress bar in training. + name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + threshold_level (float): Takes a value from 0 to 1. It specifies + the quantile in the distribution of errors on the training + dataset at which the threshold value is set. + threshold_value (float): Threshold value is calculated after the model is trained. + It sets the error limit above which the data sample defines as anomaly. + d_model (int): Dimension of model. + n_heads (int): Number of heads. + e_layers (int): Number of encoder layers. + d_ff (int): Dimension of MLP. + dropout (float): The rate of dropout. + activation (str): Activation ('relu', 'gelu'). + """ + super().__init__( + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, + val_ratio, save_checkpoints, threshold_level + ) + self.d_model = d_model + self.n_heads = n_heads + self.e_layers = e_layers + self.d_ff = d_ff + self.dropout = dropout + self.activation = activation + + _param_conf_map = dict(BaseAnomalyDetection._param_conf_map, + **{ + "d_model": ["MODEL", "D_MODEL"], + "n_heads": ["MODEL", "N_HEADS"], + "e_layers": ["MODEL", "E_LAYERS"], + "d_ff": ["MODEL", "D_FF"], + "dropout": ["MODEL", "DROPOUT"], + "activation": ["MODEL", "ACTIVATION"] + } + ) + + def _create_model(self, input_dim: int, output_dim: int): + self.model = AnomalyTransformerModel( + win_size=self.window_size, + enc_in=input_dim, + c_out=input_dim, + d_model=self.d_model, + n_heads=self.n_heads, + e_layers=self.e_layers, + d_ff=self.d_ff, + dropout=self.dropout, + activation=self.activation, + device=self.device + )
    +
    + +
    + + + + + +
    + +
    +
    +
    + +
    + + + + +
    +
    + +
    + +
    +
    +
    + + + + + +
    + + +
    + + \ No newline at end of file diff --git a/docs/_modules/ice/base.html b/docs/_modules/ice/base.html index 69f01bd..4870df6 100644 --- a/docs/_modules/ice/base.html +++ b/docs/_modules/ice/base.html @@ -371,17 +371,22 @@

    Source code for ice.base

     import pandas as pd
     import numpy as np
     import torch
    -from tqdm.auto import tqdm
    +from tqdm.auto import tqdm, trange
     import os
     import zipfile
     import requests
     import datetime
     import json
    +import random
     from ice.configs import Config
    +import time
    +from torch.utils.data import DataLoader, Dataset, random_split
    +import optuna
     
     
     
    [docs]class BaseDataset(ABC): """Base class for datasets.""" + def __init__(self, num_chunks=None, force_download=False): """ Args: @@ -397,40 +402,55 @@

    Source code for ice.base

             self.public_link = None
             self.set_name_public_link()
             self._load(num_chunks, force_download)
    -    
    +
     
    -    
    +
         def _load(self, num_chunks, force_download):
             """Load the dataset in self.df and self.target."""
    -        ref_path = f'data/{self.name}/'
    +        ref_path = f"data/{self.name}/"
             if not os.path.exists(ref_path):
                 os.makedirs(ref_path)
    -        zfile_path = f'data/{self.name}.zip'
    +        zfile_path = f"data/{self.name}.zip"
     
             url = self._get_url(self.public_link)
             if not os.path.exists(zfile_path) or force_download:
                 self._download_pgbar(url, zfile_path, self.name, num_chunks)
    -            
    +
             self._extracting_files(zfile_path, ref_path)
    -        self.df = self._read_csv_pgbar(ref_path + 'df.csv', index_col=['run_id', 'sample'])
    -        self.target = self._read_csv_pgbar(ref_path + 'target.csv', index_col=['run_id', 'sample'])['target']
    -        self.train_mask = self._read_csv_pgbar(ref_path + 'train_mask.csv', index_col=['run_id', 'sample'])['train_mask']
    +        self.df = self._read_csv_pgbar(
    +            ref_path + "df.csv", index_col=["run_id", "sample"]
    +        )
    +        self.target = self._read_csv_pgbar(
    +            ref_path + "target.csv", index_col=["run_id", "sample"]
    +        )["target"]
    +        self.train_mask = self._read_csv_pgbar(
    +            ref_path + "train_mask.csv", index_col=["run_id", "sample"]
    +        )["train_mask"]
             self.train_mask = self.train_mask.astype(bool)
             self.test_mask = ~self.train_mask
    -    
    +
         def _get_url(self, public_link):
    -        r = requests.get(f'https://cloud-api.yandex.net/v1/disk/public/resources?public_key={public_link}')
    -        return r.json()['file']
    +        url = ""
    +        r = requests.get(
    +            f"https://cloud-api.yandex.net/v1/disk/public/resources?public_key={public_link}"
    +        )
    +        if r.status_code == 200:
    +            url = r.json()["file"]
    +        else:
    +            raise Exception(r.json()["description"])
    +        return url
     
    -    def _read_csv_pgbar(self, csv_path, index_col, chunk_size=1024*100):
    -        rows = sum(1 for _ in open(csv_path, 'r')) - 1
    +    def _read_csv_pgbar(self, csv_path, index_col, chunk_size=1024 * 100):
    +        rows = sum(1 for _ in open(csv_path, "r")) - 1
             chunk_list = []
    -        with tqdm(total=rows, desc=f'Reading {csv_path}') as pbar:
    -            for chunk in pd.read_csv(csv_path, index_col=index_col, chunksize=chunk_size):
    +        with tqdm(total=rows, desc=f"Reading {csv_path}") as pbar:
    +            for chunk in pd.read_csv(
    +                csv_path, index_col=index_col, chunksize=chunk_size
    +            ):
                     chunk_list.append(chunk)
                     pbar.update(len(chunk))
             df = pd.concat((f for f in chunk_list), axis=0)
    @@ -439,14 +459,15 @@ 

    Source code for ice.base

         def _download_pgbar(self, url, zfile_path, fname, num_chunks):
             resp = requests.get(url, stream=True)
             total = int(resp.headers.get("Content-Length"))
    -        with open(zfile_path, 'wb') as file: 
    +        with open(zfile_path, "wb") as file:
                 with tqdm(
                     total=total,
    -                desc=f'Downloading {fname}',
    -                unit='B',
    +                desc=f"Downloading {fname}",
    +                unit="B",
                     unit_scale=True,
    -                unit_divisor=1024) as pbar:
    -                i = 0    
    +                unit_divisor=1024,
    +            ) as pbar:
    +                i = 0
                     for data in resp.iter_content(chunk_size=1024):
                         if num_chunks is not None and num_chunks == i:
                             break
    @@ -454,20 +475,21 @@ 

    Source code for ice.base

                         pbar.update(len(data))
                         i += 1
     
    -    def _extracting_files(self, zfile_path, ref_path, block_size=1024*10000):
    -        with zipfile.ZipFile(zfile_path, 'r') as zfile:
    +    def _extracting_files(self, zfile_path, ref_path, block_size=1024 * 10000):
    +        with zipfile.ZipFile(zfile_path, "r") as zfile:
                 for entry_info in zfile.infolist():
                     if os.path.exists(ref_path + entry_info.filename):
                         continue
                     input_file = zfile.open(entry_info.filename)
    -                target_file = open(ref_path + entry_info.filename, 'wb')
    +                target_file = open(ref_path + entry_info.filename, "wb")
                     block = input_file.read(block_size)
                     with tqdm(
    -                    total=entry_info.file_size, 
    -                    desc=f'Extracting {entry_info.filename}', 
    -                    unit='B', 
    -                    unit_scale=True, 
    -                    unit_divisor=1024) as pbar:
    +                    total=entry_info.file_size,
    +                    desc=f"Extracting {entry_info.filename}",
    +                    unit="B",
    +                    unit_scale=True,
    +                    unit_divisor=1024,
    +                ) as pbar:
                         while block:
                             target_file.write(block)
                             block = input_file.read(block_size)
    @@ -480,38 +502,57 @@ 

    Source code for ice.base

         """Base class for all models."""
     
         _param_conf_map = {
    -            "batch_size" : ["DATASET", "BATCH_SIZE"],
    -            "lr" : ["OPTIMIZATION", "LR"],
    -            "num_epochs" : ["OPTIMIZATION", "NUM_EPOCHS"],
    -            "verbose" : ["VERBOSE"],
    -            "device" : ["DEVICE"],
    -            "name" : ["EXPERIMENT_NAME"]
    -        }
    +        "batch_size": ["DATASET", "BATCH_SIZE"],
    +        "lr": ["OPTIMIZATION", "LR"],
    +        "num_epochs": ["OPTIMIZATION", "NUM_EPOCHS"],
    +        "verbose": ["VERBOSE"],
    +        "device": ["DEVICE"],
    +        "name": ["EXPERIMENT_NAME"],
    +        "window_size": ["MODEL", "WINDOW_SIZE"],
    +        "stride": ["MODEL", "STRIDE"],
    +        "val_ratio": ["DATASET", "VAL_RATIO"],
    +        "random_seed": ["SEED"],
    +    }
     
         @abstractmethod
         def __init__(
    -            self, 
    -            batch_size: int, 
    -            lr: float, 
    -            num_epochs: int, 
    -            device: str, 
    -            verbose: bool,
    -            name: str
    -        ):
    +        self,
    +        window_size: int,
    +        stride: int,
    +        batch_size: int,
    +        lr: float,
    +        num_epochs: int,
    +        device: str,
    +        verbose: bool,
    +        name: str,
    +        random_seed: int,
    +        val_ratio: float,
    +        save_checkpoints: bool,
    +    ):
             """
             Args:
    +            window_size (int): The window size to train the model.
    +            stride (int): The time interval between first points of consecutive
    +                sliding windows in training.
                 batch_size (int): The batch size to train the model.
                 lr (float): The larning rate to train the model.
                 num_epochs (float): The number of epochs to train the model.
    -            device (str): The name of a device to train the model. `cpu` and 
    +            device (str): The name of a device to train the model. `cpu` and
                     `cuda` are possible.
                 verbose (bool): If true, show the progress bar in training.
                 name (str): The name of the model for artifact storing.
    +            random_seed (int): Seed for random number generation to ensure reproducible results.
    +            val_ratio (float): Proportion of the dataset used for validation, between 0 and 1.
    +            save_checkpoints (bool): If true, store checkpoints.
             """
             self._cfg = Config()
             self._cfg.path_set(["MODEL", "NAME"], self.__class__.__name__)
             self._output_dir = "outputs"
     
    +        self.window_size = window_size
    +        self.val_ratio = val_ratio
    +        self.val_metrics = None
    +        self.stride = stride
             self.batch_size = batch_size
             self.lr = lr
             self.num_epochs = num_epochs
    @@ -519,17 +560,29 @@ 

    Source code for ice.base

             self.verbose = verbose
             self.model = None
             self.name = name
    +        self.random_seed = random_seed
    +        self.save_checkpoints = save_checkpoints
    +        self.input_dim = None
    +        self.output_dim = None
    +        self.train_time = "no date"
    +        self.checkpoint_epoch = 0
    +        self.direction = "minimize"
     
         def __setattr__(self, __name: str, __value: Any):
    -        if __name not in ['cfg', 'param_conf_map'] and __name in self._param_conf_map.keys():
    +        if (
    +            __name not in ["cfg", "param_conf_map"]
    +            and __name in self._param_conf_map.keys()
    +        ):
                 self._cfg.path_set(self._param_conf_map[__name], __value)
     
             super().__setattr__(__name, __value)
             if __name == "name":
    -            self._training_path, self._inference_path = self._initialize_paths()
    -    
    +            self._training_path, self._inference_path, self._checkpoints_path = (
    +                self._initialize_paths()
    +            )
    +
     
    [docs] @classmethod - def from_config(cls, cfg : Config): + def from_config(cls, cfg: Config): """Create instance of the model class with parameters from config. Args: @@ -543,63 +596,48 @@

    Source code for ice.base

     
             for key in cls._param_conf_map.keys():
                 param_dict[key] = cfg.path_get(cls._param_conf_map[key])
    -        
    +
             return cls(**param_dict)
    -
    [docs] def fit(self, df: pd.DataFrame, target: pd.Series): +
    [docs] def fit( + self, + df: pd.DataFrame, + target: pd.Series = None, + epochs: int = None, + save_path: str = None, + trial: optuna.Trial = None, + force_model_ctreation: bool = False, + ): """Fit (train) the model by a given dataset. Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of + df (pandas.DataFrame): A dataframe with sensor data. Index has + two columns: `run_id` and `sample`. All other columns a value of sensors. - target (pandas.Series): A series with target values. Indes has two - columns: `run_id` and `sample`. + target (pandas.Series): A series with target values. Index has two + columns: `run_id` and `sample`. It is omitted for anomaly + detection task. + epochs (int): The number of epochs for training step. If None, + self.num_epochs parameter is used. + save_path (str): Path to save checkpoints. If None, the path is + created automatically. + trial (optuna.Trial, None): optuna.Trial object created by optimize method. + force_model_ctreation (bool): force fit to create model for optimization study. """ - assert df.shape[0] == target.shape[0], f"target is incompatible with df by the length: {df.shape[0]} and {target.shape[0]}." - assert np.all(df.index == target.index), "target's index and df's index are not the same." - assert df.index.names == (['run_id', 'sample']), "An index should contain columns `run_id` and `sample`." - self._create_model(df, target) - assert self.model is not None, "Model creation error." - self._fit(df, target) + self.train_time = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + if epochs is None: + epochs = self.num_epochs + self._validate_inputs(df, target) + if self.model is None or force_model_ctreation: + self._set_dims(df, target) + self._create_model(self.input_dim, self.output_dim) + assert self.model is not None, "Model creation error." + self._prepare_for_training(self.input_dim, self.output_dim) + self._train_nn( + df=df, target=target, epochs=epochs, save_path=save_path, trial=trial + ) self._store_atrifacts_train()
    - def _initialize_paths(self): - artifacts_path = os.path.join(self._output_dir, self.name) - training_path = os.path.join(artifacts_path, 'training') - inference_path = os.path.join(artifacts_path, 'inference') - - return training_path, inference_path - - def _store_atrifacts_train(self): - save_path = os.path.join(self._training_path, datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) - os.makedirs(save_path, exist_ok= True) - self._cfg.to_yaml(os.path.join(save_path, 'config.yaml')) - - def _store_atrifacts_inference(self, metrics): - save_path = os.path.join(self._inference_path, datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) - os.makedirs(save_path, exist_ok= True) - - self._cfg.to_yaml(os.path.join(save_path, 'config.yaml')) - with open(os.path.join(save_path, "metrics.json"), 'w') as json_file: - json.dump(metrics, json_file) - - @abstractmethod - def _create_model(self, df: pd.DataFrame, target: pd.Series): - """ - This method has to be implemented by all children. Create a torch - model for traing and prediction. - """ - pass - - @abstractmethod - def _fit(self, df: pd.DataFrame, target: pd.Series): - """ - This method has to be implemented by all children. Fit (train) the model - by a given dataset. - """ - pass -
    [docs] @torch.no_grad() def predict(self, sample: torch.Tensor) -> torch.Tensor: """Make a prediction for a given batch of samples. @@ -608,7 +646,7 @@

    Source code for ice.base

                 sample (torch.Tensor): A tensor of the shape (B, L, C) where
                     B is the batch size, L is the sequence length, C is the number
                     of sensors.
    -        
    +
             Returns:
                 torch.Tensor: A tensor with predictions of the shape (B,).
             """
    @@ -616,32 +654,492 @@ 

    Source code for ice.base

             self.model.to(self.device)
             return self._predict(sample)
    - @abstractmethod - def _predict(self, sample: torch.Tensor) -> torch.Tensor: - """ - This method has to be implemented by all children. Make a prediction - for a given batch of samples. +
    [docs] def optimize( + self, + df: pd.DataFrame, + target: pd.Series = None, + optimize_parameter: str = "batch_size", + optimize_range: tuple = (128, 256), + direction: str = "minimize", + n_trials: int = 5, + epochs: int = None, + optimize_metric: str = None, + ): + """Make the optuna study to return the best hyperparameter value on validation dataset + + Args: + df (pd.DataFrame): DataFrame to use method fit + optimize_parameter (str, optional): Model parameter to optimize. Defaults to 'batch_size'. + optimize_range (tuple, optional): Model parameter range for optuna trials. Defaults to (128, 256). + n_trials (int, optional): number of trials. Defaults to 5. + target (pd.Series, optional): target pd.Series to use method fit. Defaults to None. + epochs (int, optional): Epoch number to use method fit. Defaults to None. + optimize_metric (str): Metric on validation dataset to use as a target for hyperparameter optimization. + direction (str): "minimize" or "maximize" the target for hyperparameter optimization + """ - pass - -
    [docs] @abstractmethod + param_type = type(self.__dict__[optimize_parameter]) + self.direction = direction + + defaults_torch_backends = ( + torch.backends.cudnn.deterministic, + torch.backends.cudnn.benchmark, + ) + self.dump = self._training_path + + # make torch deterministic behavior + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + torch.use_deterministic_algorithms(True) + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8" + + if not epochs: + epochs = self.num_epochs + + def objective(trial): + + self._training_path = self.dump + """optuna objective + + Args: + trial (optuna.Trial): optuna trial object + + Raises: + AssertionError: Returns if optimize_parameter value is not numerical + + Returns: + float: best validation loss to perform optimization step + """ + if param_type == float: + suggested_param = trial.suggest_float( + optimize_parameter, optimize_range[0], optimize_range[1] + ) + elif param_type == int: + suggested_param = trial.suggest_int( + optimize_parameter, optimize_range[0], optimize_range[1] + ) + elif optimize_parameter == "lr": + suggested_param = trial.suggest_loguniform( + optimize_parameter, optimize_range[0], optimize_range[1] + ) + else: + raise AssertionError(f"{optimize_parameter} is not int or float value") + + setattr(self, optimize_parameter, suggested_param) + + if optimize_metric: + self.val_metrics = True + + self._training_path = ( + self._training_path + f"/parameter_{optimize_parameter} optimization" + ) + os.makedirs(self._training_path, exist_ok=True) + + self.checkpoint_epoch = 0 + print(f"trial step with {optimize_parameter} = {suggested_param}") + self.fit( + df=df, + epochs=epochs, + target=target, + trial=trial, + force_model_ctreation=True, + ) + # use the best key metric on validation dataset from training + # if there is no such metric, use the best validation loss + + if optimize_metric: + return self.best_validation_metrics[ + optimize_metric + ] # dict with all best metric achieved during training -> write it + else: + return self.best_val_loss + + study = optuna.create_study( + direction=direction, + pruner=optuna.pruners.PercentilePruner(25.0), + study_name=f"/parameter_{optimize_parameter} study", + ) + study.optimize(objective, n_trials=n_trials) + self._training_path = self.dump + + print(f"Best hyperparameters: {study.best_params}") + print(f"Best trial: {study.best_trial}") + + df_trials = study.trials_dataframe() + df_trials.to_csv( + self._training_path + + f"/parameter_{optimize_parameter} optimization" + + f"/parameter_{optimize_parameter}.csv", + index=False, + ) + + # restore standard torch deterministic values + torch.backends.cudnn.deterministic, torch.backends.cudnn.benchmark = ( + defaults_torch_backends + ) + del os.environ["CUBLAS_WORKSPACE_CONFIG"] + torch.use_deterministic_algorithms(False)
    + +
    [docs] @torch.no_grad() def evaluate(self, df: pd.DataFrame, target: pd.Series) -> dict: - """This method has to be implemented by all children. Evaluate the - metrics. Docstring has to be rewritten so that all metrics are clearly - described. + """Evaluate the metrics: accuracy. Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of + df (pandas.DataFrame): A dataframe with sensor data. Index has + two columns: `run_id` and `sample`. All other columns a value of sensors. target (pandas.Series): A series with target values. Indes has two columns: `run_id` and `sample`. - + Returns: dict: A dictionary with metrics where keys are names of metrics and values are values of metrics. """ - pass
    + self._validate_inputs(df, target) + dataset = SlidingWindowDataset(df, target, window_size=self.window_size) + self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False) + target, pred = [], [] + for sample, _target in tqdm( + self.dataloader, desc="Steps ...", leave=False, disable=(not self.verbose) + ): + sample = sample.to(self.device) + target.append(_target) + pred.append(self.predict(sample)) + target = torch.concat(target).numpy() + pred = torch.concat(pred).numpy() + metrics = self._calculate_metrics(pred, target) + self._store_atrifacts_inference(metrics) + return metrics
    + +
    [docs] @torch.no_grad() + def model_param_estimation(self): + """Calculate number of self.model parameters, mean and std for inference time + + Returns: + tuple: A tuple containing the number of parameters in the + model and the mean and standard deviation of model inference time. + """ + assert ( + self.model != None + ), "use model.fit() to create fitted model object before" + sample = iter(self.dataloader).__next__() + if len(sample) == 2: + x, y = sample + else: + x = sample + + dummy_input = torch.randn(1, x.shape[1], x.shape[2]).to(self.device) + repetitions = 500 + times = np.zeros((repetitions, 1)) + + if self.device == "cuda": + starter, ender = torch.cuda.Event(enable_timing=True), torch.cuda.Event( + enable_timing=True + ) + + for i in range(repetitions): + starter.record() + _ = self.model(dummy_input) + ender.record() + torch.cuda.synchronize() + curr_time = starter.elapsed_time(ender) + times[i] = curr_time + + else: + for i in range(repetitions): + start_time = time.time() + _ = self.model(dummy_input) + end_time = time.time() + + curr_time = (end_time - start_time) * 1000 # Convert to milliseconds + times[i] = curr_time + + mean_inference_time = np.sum(times) / repetitions + std_inference_time = np.std(times) + + num_params = sum(p.numel() for p in self.model.parameters()) + + return num_params, (mean_inference_time, std_inference_time)
    + + @abstractmethod + def _create_model(self, input_dim: int, output_dim: int): + """ + This method has to be implemented by all children. Create a torch + model for traing and prediction. + """ + pass + + @abstractmethod + def _prepare_for_training(self, input_dim: int, output_dim: int): + """ + This method has to be implemented by all children. Prepare the model + for training by a given dataset. + """ + pass + + @abstractmethod + def _calculate_metrics(self, pred: torch.tensor, target: torch.tensor) -> dict: + """ + This method has to be implemented by all children. Calculate metrics. + """ + pass + + @abstractmethod + def _predict(self, sample: torch.Tensor) -> torch.Tensor: + """ + This method has to be implemented by all children. Make a prediction + for a given batch of samples. + """ + pass + + @abstractmethod + def _set_dims(self, df: pd.DataFrame, target: pd.Series): + """ + This method has to be implemented by all children. Calculate input and + output dimensions of the model by the given dataset. + """ + pass + + def _validate_inputs(self, df: pd.DataFrame, target: pd.Series): + assert ( + df.shape[0] == target.shape[0] + ), f"target is incompatible with df by the length: {df.shape[0]} and {target.shape[0]}." + assert np.all( + df.index == target.index + ), "target's index and df's index are not the same." + assert df.index.names == ( + ["run_id", "sample"] + ), "An index should contain columns `run_id` and `sample`." + assert ( + len(df) >= self.window_size + ), "window size is larger than the length of df." + assert len(df) >= self.stride, "stride is larger than the length of df." + + def _train_nn( + self, + df: pd.DataFrame, + target: pd.Series, + epochs: int, + save_path: str, + trial: optuna.Trial, + ): + self.model.train() + self.model.to(self.device) + self.best_val_loss = ( + float("inf") if self.direction == "minimize" else float("-inf") + ) + self.best_validation_metrics = {} + self._set_seed() + + dataset = SlidingWindowDataset(df, target, window_size=self.window_size, stride=self.stride) + val_size = max(int(len(dataset) * self.val_ratio), 1) + train_size = len(dataset) - val_size + train_dataset, val_dataset = random_split( + dataset, + [train_size, val_size], + generator=torch.Generator().manual_seed(self.random_seed), + ) + + self.dataloader = DataLoader( + train_dataset, batch_size=self.batch_size, shuffle=True + ) + self.val_dataloader = DataLoader( + val_dataset, batch_size=self.batch_size, shuffle=False + ) + for e in trange( + self.checkpoint_epoch, + self.checkpoint_epoch + epochs, + desc="Epochs ...", + disable=(not self.verbose), + ): + for sample, target in tqdm( + self.dataloader, + desc="Steps ...", + leave=False, + disable=(not self.verbose), + ): + sample = sample.to(self.device) + target = target.to(self.device) + logits = self.model(sample) + loss = self.loss_fn(logits, target) + self.optimizer.zero_grad() + loss.backward() + self.optimizer.step() + if self.verbose: + print(f"Epoch {e+1}, Loss: {loss.item():.4f}") + self.checkpoint_epoch = e + 1 + if self.save_checkpoints: + self.save_checkpoint(save_path) + + val_loss, val_metrics = self._validate_nn() + + if self.direction == "minimize": + self.best_val_loss = ( + val_loss if val_loss < self.best_val_loss else self.best_val_loss + ) + else: + self.best_val_loss = ( + val_loss if val_loss > self.best_val_loss else self.best_val_loss + ) + + if self.val_metrics: + for key, value in val_metrics.items(): + if key not in self.best_validation_metrics: + self.best_validation_metrics[key] = value + else: + if self.direction == "minimize": + self.best_validation_metrics[key] = ( + value + if self.best_validation_metrics[key] > value + else self.best_validation_metrics[key] + ) + else: + self.best_validation_metrics[key] = ( + value + if self.best_validation_metrics[key] < value + else self.best_validation_metrics[key] + ) + + if self.verbose: + if self.val_metrics: + print( + f"Epoch {e+1}, Validation Loss: {val_loss:.4f}, Metrics: {val_metrics}" + ) + else: + print(f"Epoch {e+1}, Validation Loss: {val_loss:.4f}") + + if trial: + trial.report(val_loss, self.checkpoint_epoch) + + if trial.should_prune(): + raise optuna.exceptions.TrialPruned() + + def _validate_nn(self): + self.model.eval() + + val_loss = 0 + target_list, pred_list = [], [] + with torch.no_grad(): + for sample, target in self.val_dataloader: + sample = sample.to(self.device) + target = target.to(self.device) + logits = self.model(sample) + loss = self.loss_fn(logits, target) + val_loss += loss.item() + + if self.val_metrics: + pred = self.predict(sample) + target_list.append(target.cpu()) + pred_list.append(pred.cpu()) + + val_loss /= len(self.val_dataloader) + + if self.val_metrics: + target_tensor = torch.cat(target_list).numpy() + pred_tensor = torch.cat(pred_list).numpy() + + val_metrics = self._calculate_metrics(pred_tensor, target_tensor) + else: + val_metrics = None + + self.model.train() + + return val_loss, val_metrics + + def _set_seed(self): + torch.manual_seed(self.random_seed) + random.seed(self.random_seed) + np.random.seed(self.random_seed) + + def _initialize_paths(self): + artifacts_path = os.path.join(self._output_dir, self.name) + training_path = os.path.join(artifacts_path, "training") + inference_path = os.path.join(artifacts_path, "inference") + checkpoints_path = os.path.join(artifacts_path, "checkpoints") + return training_path, inference_path, checkpoints_path + + def _store_atrifacts_train(self): + save_path = os.path.join(self._training_path, self.train_time) + os.makedirs(save_path, exist_ok=True) + self._cfg.to_yaml(os.path.join(save_path, "config.yaml")) + + def _store_atrifacts_inference(self, metrics): + save_path = os.path.join(self._inference_path, self.train_time) + os.makedirs(save_path, exist_ok=True) + + self._cfg.to_yaml(os.path.join(save_path, "config.yaml")) + with open(os.path.join(save_path, "metrics.json"), "w") as json_file: + json.dump(metrics, json_file) + +
    [docs] def save_checkpoint(self, save_path: str = None): + """Save checkpoint. + + Args: + save_path (str): Path to save checkpoint. + """ + if save_path is None: + checkpoints_path = os.path.join(self._checkpoints_path, self.train_time) + os.makedirs(checkpoints_path, exist_ok=True) + file_path = self.name + "_epoch_" + str(self.checkpoint_epoch) + save_path = os.path.join(checkpoints_path, file_path + ".tar") + torch.save( + { + "config": self._cfg, + "epoch": self.checkpoint_epoch, + "input_dim": self.input_dim, + "output_dim": self.output_dim, + "model_state_dict": self.model.state_dict(), + "optimizer_state_dict": self.optimizer.state_dict(), + }, + save_path, + )
    + +
    [docs] def load_checkpoint(self, checkpoint_path: str): + """Load checkpoint. + + Args: + checkpoint_path (str): Path to load checkpoint. + """ + checkpoint = torch.load(checkpoint_path, map_location=self.device) + self._cfg = checkpoint["config"] + self.input_dim = checkpoint["input_dim"] + self.output_dim = checkpoint["output_dim"] + self._create_model(self.input_dim, self.output_dim) + self.model.to(self.device) + assert self.model is not None, "Model creation error." + self._prepare_for_training(self.input_dim, self.output_dim) + self.model.load_state_dict(checkpoint["model_state_dict"]) + self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) + self.checkpoint_epoch = checkpoint["epoch"]
    + + +
    [docs]class SlidingWindowDataset(Dataset): + def __init__( + self, df: pd.DataFrame, target: pd.Series, window_size: int, stride: int = 1 + ): + self.df = df + self.target = target + self.window_size = window_size + + window_end_indices = [] + run_ids = df.index.get_level_values(0).unique() + for run_id in tqdm(run_ids, desc="Creating sequence of samples"): + indices = np.array(df.index.get_locs([run_id])) + indices = indices[self.window_size :: stride] + window_end_indices.extend(indices) + self.window_end_indices = np.array(window_end_indices) + + def __len__(self): + return len(self.window_end_indices) + + def __getitem__(self, idx): + window_index = self.window_end_indices[idx] + sample = self.df.values[window_index - self.window_size : window_index] + if self.target is not None: + target = self.target.values[window_index] + else: + target = sample.astype("float32") + return sample.astype("float32"), target
    diff --git a/docs/_modules/ice/fault_diagnosis/datasets.html b/docs/_modules/ice/fault_diagnosis/datasets.html index 42d66ab..82e834b 100644 --- a/docs/_modules/ice/fault_diagnosis/datasets.html +++ b/docs/_modules/ice/fault_diagnosis/datasets.html @@ -369,6 +369,20 @@

    Source code for ice.fault_diagnosis.datasets

    from ice.base import BaseDataset
     
     
    +
    [docs]class FaultDiagnosisRiethTEP(BaseDataset): + """ + Dataset of Tennessee Eastman Process dataset + Rieth, C. A., Amsel, B. D., Tran, R., & Cook, M. B. (2017). + Additional Tennessee Eastman Process Simulation Data for + Anomaly Detection Evaluation (Version V1) [Computer software]. + Harvard Dataverse. + https://doi.org/10.7910/DVN/6C3JR1. + """ +
    + +
    [docs]class FaultDiagnosisSmallTEP(BaseDataset): """ Cropped version of Tennessee Eastman Process dataset diff --git a/docs/_modules/ice/fault_diagnosis/models/base.html b/docs/_modules/ice/fault_diagnosis/models/base.html index 6380b1c..79bafda 100644 --- a/docs/_modules/ice/fault_diagnosis/models/base.html +++ b/docs/_modules/ice/fault_diagnosis/models/base.html @@ -366,123 +366,43 @@

    Source code for ice.fault_diagnosis.models.base

    -from abc import ABC, abstractmethod
    +from abc import ABC
     import pandas as pd
    -from tqdm.auto import trange, tqdm
    -
     import torch
     from torch import nn
     from torch.optim import Adam
    -from torch.utils.data import DataLoader
     
    -from ice.fault_diagnosis.utils import SlidingWindowDataset
     from ice.base import BaseModel
     from ice.fault_diagnosis.metrics import (
         accuracy, correct_daignosis_rate, true_positive_rate, false_positive_rate)
     
    +
     
    [docs]class BaseFaultDiagnosis(BaseModel, ABC): """Base class for all fault diagnosis models.""" - @abstractmethod - def __init__( - self, - window_size: int, - batch_size: int, - lr: float, - num_epochs: int, - device: str, - verbose: bool, - name: str - ): - """ - Args: - window_size (int): The window size to train the model. - batch_size (int): The batch size to train the model. - lr (float): The learning rate to train the model. - num_epochs (float): The number of epochs to train the model. - device (str): The name of a device to train the model. `cpu` and - `cuda` are possible. - verbose (bool): If true, show the progress bar in training. - name (str): The name of the model for artifact storing. - """ - super().__init__(batch_size, lr, num_epochs, device, verbose, name) - self._cfg.path_set(["TASK"], "fault_diagnosis") - - self.window_size = window_size - self.loss_fn = None - - _param_conf_map = dict(BaseModel._param_conf_map, - **{ - "window_size" : ["MODEL", "WINDOW_SIZE"] - } - ) - - def _fit(self, df: pd.DataFrame, target: pd.Series): - assert len(df) >= self.window_size, "window size is larger than the length of df." - num_classes = len(set(target)) - weight = torch.ones(num_classes, device=self.device) * 0.5 - weight[1:] /= num_classes - 1 + def _prepare_for_training(self, input_dim: int, output_dim: int): + weight = torch.ones(output_dim, device=self.device) * 0.5 + weight[1:] /= output_dim - 1 self.loss_fn = nn.CrossEntropyLoss(weight=weight) - self._train_nn(df, target) + self.optimizer = Adam(self.model.parameters(), lr=self.lr) def _predict(self, sample: torch.Tensor) -> torch.Tensor: sample = sample.to(self.device) logits = self.model(sample) return logits.argmax(axis=1).cpu() - def _train_nn(self, df: pd.DataFrame, target: pd.Series): - self.model.train() - self.model.to(self.device) - self.optimizer = Adam(self.model.parameters(), lr=self.lr) - - dataset = SlidingWindowDataset(df, target, window_size=self.window_size) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - for e in trange(self.num_epochs, desc='Epochs ...', disable=(not self.verbose)): - for sample, target in tqdm(self.dataloader, desc='Steps ...', leave=False, disable=(not self.verbose)): - sample = sample.to(self.device) - target = target.to(self.device) - logits = self.model(sample) - loss = self.loss_fn(logits, target) - self.optimizer.zero_grad() - loss.backward() - self.optimizer.step() - if self.verbose: - print(f'Epoch {e+1}, Loss: {loss.item():.4f}') - -
    [docs] def evaluate(self, df: pd.DataFrame, target: pd.Series) -> dict: - """Evaluate the metrics: accuracy. - - Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of - sensors. - target (pandas.Series): A series with target values. Indes has two - columns: `run_id` and `sample`. - - Returns: - dict: A dictionary with metrics where keys are names of metrics and - values are values of metrics. - """ - dataset = SlidingWindowDataset(df, target, window_size=self.window_size) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - target, pred = [], [] - for sample, _target in tqdm( - self.dataloader, desc='Steps ...', leave=False, disable=(not self.verbose) - ): - sample = sample.to(self.device) - target.append(_target) - with torch.no_grad(): - pred.append(self.predict(sample)) - target = torch.concat(target).numpy() - pred = torch.concat(pred).numpy() + def _calculate_metrics(self, pred: torch.tensor, target: torch.tensor) -> dict: metrics = { 'accuracy': accuracy(pred, target), 'correct_daignosis_rate': correct_daignosis_rate(pred, target), 'true_positive_rate': true_positive_rate(pred, target), 'false_positive_rate': false_positive_rate(pred, target), } - self._store_atrifacts_inference(metrics) - return metrics
    + return metrics + + def _set_dims(self, df: pd.DataFrame, target: pd.Series): + self.input_dim = df.shape[1] + self.output_dim = len(set(target))
    diff --git a/docs/_modules/ice/fault_diagnosis/models/mlp.html b/docs/_modules/ice/fault_diagnosis/models/mlp.html index 736beff..0ade77f 100644 --- a/docs/_modules/ice/fault_diagnosis/models/mlp.html +++ b/docs/_modules/ice/fault_diagnosis/models/mlp.html @@ -380,13 +380,17 @@

    Source code for ice.fault_diagnosis.models.mlp

    def __init__( self, window_size: int, + stride: int = 1, hidden_dim: int=256, batch_size: int=128, lr: float=0.001, num_epochs: int=10, device: str='cpu', verbose: bool=False, - name: str='mlp_fault_diagnosis' + name: str='mlp_fault_diagnosis', + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False ): """ Args: @@ -399,10 +403,14 @@

    Source code for ice.fault_diagnosis.models.mlp

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. """ super().__init__( - window_size, batch_size, lr, num_epochs, device, verbose, name + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints ) + self.val_metrics = True self.hidden_dim = hidden_dim @@ -412,14 +420,12 @@

    Source code for ice.fault_diagnosis.models.mlp

    } ) - def _create_model(self, df: DataFrame, target: Series): - num_sensors = df.shape[1] - num_classes = len(set(target)) + def _create_model(self, input_dim: int, output_dim: int): self.model = nn.Sequential( nn.Flatten(), - nn.Linear(num_sensors * self.window_size, self.hidden_dim), + nn.Linear(input_dim * self.window_size, self.hidden_dim), nn.ReLU(), - nn.Linear(self.hidden_dim, num_classes), + nn.Linear(self.hidden_dim, output_dim), )

    diff --git a/docs/_modules/ice/fault_diagnosis/models/tcn.html b/docs/_modules/ice/fault_diagnosis/models/tcn.html index fa58700..f196d4f 100644 --- a/docs/_modules/ice/fault_diagnosis/models/tcn.html +++ b/docs/_modules/ice/fault_diagnosis/models/tcn.html @@ -493,6 +493,7 @@

    Source code for ice.fault_diagnosis.models.tcn

    def __init__( self, window_size: int, + stride: int = 1, hidden_dim: int=256, kernel_size: int=5, num_layers: int=4, @@ -503,7 +504,10 @@

    Source code for ice.fault_diagnosis.models.tcn

    num_epochs: int=10, device: str='cpu', verbose: bool=False, - name: str='tcn_fault_diagnosis' + name: str='tcn_fault_diagnosis', + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False ): """ Args: @@ -520,10 +524,14 @@

    Source code for ice.fault_diagnosis.models.tcn

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. """ super().__init__( - window_size, batch_size, lr, num_epochs, device, verbose, name + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints ) + self.val_metrics = True self.hidden_dim = hidden_dim self.kernel_size = kernel_size @@ -541,16 +549,14 @@

    Source code for ice.fault_diagnosis.models.tcn

    } ) - def _create_model(self, df: DataFrame, target: Series): - num_sensors = df.shape[1] - num_classes = len(set(target)) + def _create_model(self, input_dim: int, output_dim: int): self.model = _TCNModule( - input_dim=num_sensors, + input_dim=input_dim, kernel_size=self.kernel_size, hidden_dim=self.hidden_dim, num_layers=self.num_layers, dilation_base=self.dilation_base, - output_dim=num_classes, + output_dim=output_dim, dropout=self.dropout, seq_len=self.window_size, )

    diff --git a/docs/_modules/ice/health_index_estimation/datasets.html b/docs/_modules/ice/health_index_estimation/datasets.html index 0ce534d..168321c 100644 --- a/docs/_modules/ice/health_index_estimation/datasets.html +++ b/docs/_modules/ice/health_index_estimation/datasets.html @@ -419,6 +419,7 @@

    Source code for ice.health_index_estimation.datasets

    inter_func = [] for i in range(15): + data[i]["material"] = data[i]["material"].astype("float64") y = data[i].dropna().VB x = data[i].dropna().time @@ -432,21 +433,16 @@

    Source code for ice.health_index_estimation.datasets

    else: data[i] = data[i].fillna(0) - self.df = [data[i].drop(columns=["VB"]) for i in train_nums] + self.df = [ + data[i].drop(columns=["VB", "Unnamed: 0", "case", "run"]) + for i in train_nums + ] self.target = [data[i]["VB"] for i in train_nums] - self.test = [data[i].drop(columns=["VB"]) for i in test_nums] - self.test_target = [data[i]["VB"] for i in test_nums] - - def _read_csv_pgbar(self, csv_path, index_col, chunksize=1024 * 100): - df = pd.read_csv(csv_path) - df.rename(columns={"cut_no": "run_id"}, inplace=True) - df = df.set_index(["run_id", "sample"]).drop( - columns=["Unnamed: 0", "case", "run"] - ) - df["material"] = df["material"].astype("float64") - - return df
    + self.test = [ + data[i].drop(columns=["VB", "Unnamed: 0", "case", "run"]) for i in test_nums + ] + self.test_target = [data[i]["VB"] for i in test_nums]
    diff --git a/docs/_modules/ice/health_index_estimation/metrics.html b/docs/_modules/ice/health_index_estimation/metrics.html index 9dfbdcf..6ded8b2 100644 --- a/docs/_modules/ice/health_index_estimation/metrics.html +++ b/docs/_modules/ice/health_index_estimation/metrics.html @@ -381,6 +381,19 @@

    Source code for ice.health_index_estimation.metrics

    float: rmse """ return float(np.mean((pred - target) ** 2))
    + +
    [docs]def rmse(pred: list, target: list) -> float: + """ + Mean squared error between real and predicted wear. + + Args: + pred (list): numpy prediction values. + target (list): numpy target values. + + Returns: + float: rmse + """ + return float(np.sqrt(np.mean((pred - target) ** 2)))
    diff --git a/docs/_modules/ice/health_index_estimation/models/base.html b/docs/_modules/ice/health_index_estimation/models/base.html index 2fc2116..8f1c574 100644 --- a/docs/_modules/ice/health_index_estimation/models/base.html +++ b/docs/_modules/ice/health_index_estimation/models/base.html @@ -367,134 +367,37 @@

    Source code for ice.health_index_estimation.models.base

     from ice.base import BaseModel
    -from abc import ABC, abstractmethod
    +from abc import ABC
     import pandas as pd
    -from tqdm.auto import trange, tqdm
    -
    -from torch.optim import Adam, AdamW
    -from torch.utils.data import DataLoader
    +from torch.optim import AdamW
     import torch
     from torch import nn
    -from ice.health_index_estimation.utils import SlidingWindowDataset
    -from ice.health_index_estimation.metrics import mse
    +
    +from ice.health_index_estimation.metrics import mse, rmse
     
     
     
    [docs]class BaseHealthIndexEstimation (BaseModel, ABC): """Base class for all HI diagnosis models.""" - @abstractmethod - def __init__( - self, - window_size: int, - stride: int, - batch_size: int, - lr: float, - num_epochs: int, - device: str, - verbose: bool, - name: str, - ): - """ - Args: - window_size (int): The window size to train the model. - stride (int): The time interval between first points of consecutive sliding windows. - batch_size (int): The batch size to train the model. - lr (float): The larning rate to train the model. - num_epochs (float): The number of epochs to train the model. - device (str): The name of a device to train the model. `cpu` and - `cuda` are possible. - verbose (bool): If true, show the progress bar in training. - name (str): The name of the model for artifact storing. - """ - super().__init__(batch_size, lr, num_epochs, device, verbose, name) - - self.window_size = window_size - self.stride = stride - self.loss_fn = None - self.newvalues = None - - self.test_stride = 50 - - _param_conf_map = dict( - BaseModel._param_conf_map, - **{"window_size": ["MODEL", "WINDOW_SIZE"], "stride": ["MODEL", "STRIDE"]}, - ) - - def _fit(self, df: pd.DataFrame, target: pd.Series): - assert ( - len(df) >= self.window_size - ), "window size is larger than the length of df." - assert len(df) >= self.stride, "stride is larger than the length of df." + def _prepare_for_training(self, input_dim: int, output_dim: int): self.loss_fn = nn.L1Loss() - self._train_nn(df, target) + self.optimizer = AdamW(self.model.parameters(), lr=self.lr) def _predict(self, sample: torch.Tensor) -> torch.Tensor: sample = sample.to(self.device) predicted_rul = self.model(sample) return predicted_rul.cpu() - def _train_nn(self, df: pd.DataFrame, target: pd.Series): - self.model.train() - self.model.to(self.device) - self.optimizer = AdamW(self.model.parameters(), lr=self.lr) - - dataset = SlidingWindowDataset( - df, target, window_size=self.window_size, stride=self.stride - ) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - for e in trange(self.num_epochs, desc="Epochs ...", disable=(not self.verbose)): - for sample, target in tqdm( - self.dataloader, - desc="Steps ...", - leave=False, - disable=(not self.verbose), - ): - sample = sample.to(self.device) - target = target.to(self.device) - - logits = self.model(sample) - loss = self.loss_fn(logits, target) - - self.optimizer.zero_grad() - loss.backward() - self.optimizer.step() - self.newvalues.append(loss.item()) - if self.verbose: - print(f"Epoch {e+1}, Loss: {loss.item():.4f}") - -
    [docs] def evaluate(self, df: pd.DataFrame, target: pd.Series) -> dict: - """Evaluate the metrics: mse. - - Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of - sensors. - target (pandas.Series): A series with target values. Indes has two - columns: `run_id` and `sample`. - - Returns: - dict: A dictionary with metrics where keys are names of metrics and - values are values of metrics. - """ - dataset = SlidingWindowDataset( - df, target, window_size=self.window_size, stride=self.test_stride - ) - self.dataloader = DataLoader(dataset, batch_size=1, shuffle=True) - target, pred = [], [] - for sample, _target in tqdm( - self.dataloader, desc="Steps ...", leave=False, disable=(not self.verbose) - ): - sample = sample.to(self.device) - target.append(_target) - with torch.no_grad(): - pred.append(self.predict(sample)) - target = torch.concat(target).numpy() - pred = torch.concat(pred).numpy() + def _calculate_metrics(self, pred: torch.tensor, target: torch.tensor) -> dict: metrics = { "mse": mse(pred, target), + "rmse": rmse(pred, target), } - self._store_atrifacts_inference(metrics) - return metrics
    + return metrics + + def _set_dims(self, df: pd.DataFrame, target: pd.Series): + self.input_dim = df.shape[1] + self.output_dim = 1
    diff --git a/docs/_modules/ice/health_index_estimation/models/mlp.html b/docs/_modules/ice/health_index_estimation/models/mlp.html index 39c637a..188788b 100644 --- a/docs/_modules/ice/health_index_estimation/models/mlp.html +++ b/docs/_modules/ice/health_index_estimation/models/mlp.html @@ -390,6 +390,9 @@

    Source code for ice.health_index_estimation.models.mlp

    device: str = "cpu", verbose: bool = True, name: str = "mlp_fault_diagnosis", + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False ): """ Args: @@ -403,10 +406,14 @@

    Source code for ice.health_index_estimation.models.mlp

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. """ super().__init__( - window_size, stride, batch_size, lr, num_epochs, device, verbose, name + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints ) + self.val_metrics = True self.hidden_dim = hidden_dim self.newvalues = [] @@ -416,12 +423,13 @@

    Source code for ice.health_index_estimation.models.mlp

    **{"hidden_dim": ["MODEL", "HIDDEN_DIM"]} ) - def _create_model(self, df: DataFrame, target: Series): - num_sensors = df.shape[1] + def _create_model(self, input_dim: int, output_dim: int): self.model = nn.Sequential( nn.Flatten(), - nn.Linear(num_sensors * self.window_size, self.hidden_dim), + nn.Dropout(0.5), + nn.Linear(input_dim * self.window_size, self.hidden_dim), nn.ReLU(), + nn.Dropout(0.5), nn.Linear(self.hidden_dim, 1), nn.Flatten(start_dim=0), )
    diff --git a/docs/_modules/ice/remaining_useful_life_estimation/datasets.html b/docs/_modules/ice/remaining_useful_life_estimation/datasets.html index 9787a0f..c7a9020 100644 --- a/docs/_modules/ice/remaining_useful_life_estimation/datasets.html +++ b/docs/_modules/ice/remaining_useful_life_estimation/datasets.html @@ -398,7 +398,7 @@

    Source code for ice.remaining_useful_life_estimation.datasets

    if not os.path.exists(zfile_path) or force_download: self._download_pgbar(url, zfile_path, self.name, num_chunks) - self._extracting_files(zfile_path, ref_path) + self._extracting_files(zfile_path, "data/") self.df = [ self._read_csv_pgbar( ref_path + f"fd{i}_train.csv", index_col=["run_id", "sample"] @@ -422,13 +422,50 @@

    Source code for ice.remaining_useful_life_estimation.datasets

    ref_path + f"fd{i}_test.csv", index_col=["run_id", "sample"] )["rul"] for i in range(1, 5) - ] + ]
    + + +
    [docs]class RulCmapssPaper(BaseDataset): + """ + Preprocessed to piece wise RUL data from the dataset: + Saxena A. et al. Damage propagation modeling for aircraft engine run-to-failure simulation + DOI: 10.1109/PHM.2008.4711414. Target is the minimum rul value for every test device. + + """ + + + + def _load(self, num_chunks, force_download): + """ + Load the test dataset in list obects: self.df, self.target, self.test and self.test_target. + 4 subdatasets fd001-fd004, list index corresponds to a subdataset number + + """ + ref_path = f"data/{self.name}/" + if not os.path.exists(ref_path): + os.makedirs(ref_path) + zfile_path = f"data/{self.name}.zip" - def _read_csv_pgbar(self, csv_path, index_col, chunksize=1024 * 100): - df = pd.read_csv(csv_path) - df.rename(columns={"unit_num": "run_id"}, inplace=True) - df = df.set_index(["run_id", "sample"]) - return df
    + url = self._get_url(self.public_link) + if not os.path.exists(zfile_path) or force_download: + self._download_pgbar(url, zfile_path, self.name, num_chunks) + + self._extracting_files(zfile_path, f"data/{self.name}/") + + self.test = [ + self._read_csv_pgbar( + ref_path + f"fd{i}_test.csv", index_col=["run_id", "sample"] + ).drop(columns=["rul"]) + for i in range(1, 5) + ] + self.test_target = [ + self._read_csv_pgbar( + ref_path + f"/fd{i}_test.csv", index_col=["run_id", "sample"] + )["rul"] + for i in range(1, 5) + ]
    diff --git a/docs/_modules/ice/remaining_useful_life_estimation/metrics.html b/docs/_modules/ice/remaining_useful_life_estimation/metrics.html index 0bf0393..18f3641 100644 --- a/docs/_modules/ice/remaining_useful_life_estimation/metrics.html +++ b/docs/_modules/ice/remaining_useful_life_estimation/metrics.html @@ -397,7 +397,7 @@

    Source code for ice.remaining_useful_life_estimation.metrics

    return float(np.exp((-value / 13)) - 1 if value < 0 else np.exp((value / 10)) - 1)
    -
    [docs]def score(pred: list, target: list) -> float: +
    [docs]def cmapss_score(pred: list, target: list) -> float: """ Non-simmetric metric proposed in the original dataset paper. DOI: 10.1109/PHM.2008.4711414 diff --git a/docs/_modules/ice/remaining_useful_life_estimation/models/base.html b/docs/_modules/ice/remaining_useful_life_estimation/models/base.html index 52b7803..a1814c6 100644 --- a/docs/_modules/ice/remaining_useful_life_estimation/models/base.html +++ b/docs/_modules/ice/remaining_useful_life_estimation/models/base.html @@ -367,132 +367,37 @@

    Source code for ice.remaining_useful_life_estimation.models.base

     from ice.base import BaseModel
    -from abc import ABC, abstractmethod
    +from abc import ABC
     import pandas as pd
    -from tqdm.auto import trange, tqdm
    -
    -from torch.optim import Adam, AdamW
    -from torch.utils.data import DataLoader
    +from torch.optim import AdamW
     import torch
     from torch import nn
    -from ice.remaining_useful_life_estimation.utils import SlidingWindowDataset
    -from ice.remaining_useful_life_estimation.metrics import rmse, score
    +
    +from ice.remaining_useful_life_estimation.metrics import rmse, cmapss_score
     
     
     
    [docs]class BaseRemainingUsefulLifeEstimation(BaseModel, ABC): """Base class for all RUL models.""" - @abstractmethod - def __init__( - self, - window_size: int, - stride: int, - batch_size: int, - lr: float, - num_epochs: int, - device: str, - verbose: bool, - name: str, - ): - """ - Args: - window_size (int): The window size to train the model. - stride (int): The time interval between first points of consecutive sliding windows. - batch_size (int): The batch size to train the model. - lr (float): The learning rate to train the model. - num_epochs (float): The number of epochs to train the model. - device (str): The name of a device to train the model. `cpu` and - `cuda` are possible. - verbose (bool): If true, show the progress bar in training. - name (str): The name of the model for artifact storing. - """ - super().__init__(batch_size, lr, num_epochs, device, verbose, name) - - self.window_size = window_size - self.stride = stride - self.loss_fn = None - self.loss_array = None - - _param_conf_map = dict( - BaseModel._param_conf_map, - **{ - "window_size": ["MODEL", "WINDOW_SIZE"], - "stride": ["MODEL", "STRIDE"], - }, - ) - - def _fit(self, df: pd.DataFrame, target: pd.Series): - assert ( - len(df) >= self.window_size - ), "window size is larger than the length of df." - assert len(df) >= self.stride, "stride is larger than the length of df." + def _prepare_for_training(self, input_dim: int, output_dim: int): self.loss_fn = nn.L1Loss() - self._train_nn(df, target) + self.optimizer = AdamW(self.model.parameters(), lr=self.lr) def _predict(self, sample: torch.Tensor) -> torch.Tensor: sample = sample.to(self.device) predicted_rul = self.model(sample) return predicted_rul.cpu() - def _train_nn(self, df: pd.DataFrame, target: pd.Series): - self.model.train() - self.model.to(self.device) - self.optimizer = AdamW(self.model.parameters(), lr=self.lr) - - dataset = SlidingWindowDataset( - df, target, window_size=self.window_size, stride=self.stride - ) - self.dataloader = DataLoader(dataset, batch_size=self.batch_size, shuffle=True) - for e in trange(self.num_epochs, desc="Epochs ...", disable=(not self.verbose)): - for sample, target in tqdm( - self.dataloader, - desc="Steps ...", - leave=False, - disable=(not self.verbose), - ): - sample = sample.to(self.device) - target = target.to(self.device) - logits = self.model(sample) - loss = self.loss_fn(logits, target) - self.optimizer.zero_grad() - loss.backward() - self.optimizer.step() - self.loss_array.append(loss.item()) - if self.verbose: - print(f"Epoch {e+1}, Loss: {loss.item():.4f}") - -
    [docs] def evaluate(self, df: pd.DataFrame, target: pd.Series) -> dict: - """Evaluate the metrics: rmse, c-mapss score. - - Args: - df (pandas.DataFrame): A dataframe with sensor data. Index has - two columns: `run_id` and `sample`. All other columns a value of - sensors. - target (pandas.Series): A series with target values. Indes has two - columns: `run_id` and `sample`. - - Returns: - dict: A dictionary with metrics where keys are names of metrics and - values are values of metrics. - """ - dataset = SlidingWindowDataset(df, target, window_size=self.window_size) - self.dataloader = DataLoader(dataset, batch_size=1, shuffle=True) - target, pred = [], [] - for sample, _target in tqdm( - self.dataloader, desc="Steps ...", leave=False, disable=(not self.verbose) - ): - sample = sample.to(self.device) - target.append(_target) - with torch.no_grad(): - pred.append(self.predict(sample)) - target = torch.concat(target).numpy() - pred = torch.concat(pred).numpy() + def _calculate_metrics(self, pred: torch.tensor, target: torch.tensor) -> dict: metrics = { "rmse": rmse(pred, target), - "score": score(pred, target), + "cmapss_score": cmapss_score(pred, target), } - self._store_atrifacts_inference(metrics) - return metrics
    + return metrics + + def _set_dims(self, df: pd.DataFrame, target: pd.Series): + self.input_dim = df.shape[1] + self.output_dim = 1
    diff --git a/docs/_modules/ice/remaining_useful_life_estimation/models/lstm.html b/docs/_modules/ice/remaining_useful_life_estimation/models/lstm.html new file mode 100644 index 0000000..2fa856b --- /dev/null +++ b/docs/_modules/ice/remaining_useful_life_estimation/models/lstm.html @@ -0,0 +1,556 @@ + + + + + + + + + + + ice.remaining_useful_life_estimation.models.lstm — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    + + +
    +
    + + + + + +
    + +

    Source code for ice.remaining_useful_life_estimation.models.lstm

    +from torch import nn
    +from ice.remaining_useful_life_estimation.models.base import BaseRemainingUsefulLifeEstimation
    +from pandas import DataFrame, Series
    +import torch
    +
    +
    +
    [docs]class LSTM_model(nn.Module): + """ + Long short-term memory (LSTM) is reccurent neural network type, + pytorch realisation https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html + """ + def __init__( + self, + input_dim, + hidden_size=512, + device="cpu", + num_layers=2,): + """ + Args: + input_dim (int): The dimension size of input data, related to the sensor amount in industry probles. + hidden_size (int): The number of features in the hidden state of the model. + device (str): The name of a device to train the model. `cpu` and `cuda` are possible. + num_layers (int): The number of stacked reccurent layers of the classic LSTM architecture. + """ + super(LSTM_model, self).__init__() + self.input_dim = input_dim + self.hidden_size = hidden_size + self.num_layers = num_layers + self.device = device + + self.lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_size, + num_layers=num_layers, batch_first=True) + +
    [docs] def forward(self, x): + h_0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(self.device) #hidden state + c_0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(self.device) #internal state + + output, (hn, cn) = self.lstm(x, (h_0, c_0)) + return output[:, -1, :]
    + +
    [docs]class LSTM(BaseRemainingUsefulLifeEstimation): + """ + Long short-term memory (LSTM) model consists of the classical LSTM architecture stack and + two-layer MLP with SiLU nonlinearity and dropout to make the final prediction. + + Each sample is moved to LSTM and reshaped to a vector (B, L, C) -> (B, hidden_size, C) + Then the sample is reshaped to a vector (B, hidden_size, C) -> (B, hidden_size * C) + """ + + def __init__( + self, + window_size: int = 32, + stride: int = 1, + hidden_dim: int = 512, + hidden_size: int = 256, + num_layers: int =2, + dropout_value: float = 0.5, + batch_size: int = 64, + lr: float = 1e-4, + num_epochs: int = 35, + device: str = "cpu", + verbose: bool = True, + name: str = "mlp_cmapss_rul", + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False + ): + """ + Args: + window_size (int): The window size to train the model. + stride (int): The time interval between first points of consecutive sliding windows. + hidden_dim (int): The dimensionality of the hidden layer in MLP. + hidden_size (int): The number of features in the hidden state of the model. + num_layers (int): The number of stacked reccurent layers of the classic LSTM architecture. + batch_size (int): The batch size to train the model. + lr (float): The larning rate to train the model. + num_epochs (float): The number of epochs to train the model. + device (str): The name of a device to train the model. `cpu` and + `cuda` are possible. + verbose (bool): If true, show the progress bar in training. + name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. + """ + super().__init__( + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints + ) + self.val_metrics = True + + self.hidden_dim = hidden_dim + self.hidden_size = hidden_size + self.num_layers = num_layers + self.device = device + self.dropout_value = dropout_value + + self.loss_array = [] + + _param_conf_map = dict( + BaseRemainingUsefulLifeEstimation._param_conf_map, + **{"hidden_dim": ["MODEL", "HIDDEN_DIM"], + "hidden_size": ["MODEL", "HIDDEN_SIZE"], + "num_layers": ["MODEL", "NUM_LAYERS"], + "dropout_value": ["MODEL", "DROPOUT"], + } + ) + + def _create_model(self, input_dim: int, output_dim: int): + self.model = nn.Sequential( + nn.Dropout(self.dropout_value), + LSTM_model(input_dim, self.hidden_size, self.device, self.num_layers), + nn.Flatten(), + nn.Linear(self.hidden_size, self.hidden_dim), + nn.SiLU(), + nn.Dropout(self.dropout_value), + nn.Linear(self.hidden_dim, 1), + nn.Flatten(start_dim=0), + )
    +
    + +
    + + + + + +
    + +
    +
    +
    + +
    + + + + +
    +
    + +
    + +
    +
    +
    + + + + + +
    + + +
    + + \ No newline at end of file diff --git a/docs/_modules/ice/remaining_useful_life_estimation/models/mlp.html b/docs/_modules/ice/remaining_useful_life_estimation/models/mlp.html index f5e0c46..7f67280 100644 --- a/docs/_modules/ice/remaining_useful_life_estimation/models/mlp.html +++ b/docs/_modules/ice/remaining_useful_life_estimation/models/mlp.html @@ -383,13 +383,16 @@

    Source code for ice.remaining_useful_life_estimation.models.mlp

    self, window_size: int = 32, stride: int = 1, - hidden_dim: int = 256, - batch_size: int = 256, - lr: float = 5e-5, - num_epochs: int = 50, + hidden_dim: int = 512, + batch_size: int = 64, + lr: float = 1e-4, + num_epochs: int = 15, device: str = "cpu", verbose: bool = True, name: str = "mlp_cmapss_rul", + random_seed: int = 42, + val_ratio: float = 0.15, + save_checkpoints: bool = False ): """ Args: @@ -403,10 +406,14 @@

    Source code for ice.remaining_useful_life_estimation.models.mlp

    `cuda` are possible. verbose (bool): If true, show the progress bar in training. name (str): The name of the model for artifact storing. + random_seed (int): Seed for random number generation to ensure reproducible results. + val_ratio (float): Proportion of the dataset used for validation, between 0 and 1. + save_checkpoints (bool): If true, store checkpoints. """ super().__init__( - window_size, stride, batch_size, lr, num_epochs, device, verbose, name + window_size, stride, batch_size, lr, num_epochs, device, verbose, name, random_seed, val_ratio, save_checkpoints ) + self.val_metrics = True self.hidden_dim = hidden_dim self.loss_array = [] @@ -416,13 +423,13 @@

    Source code for ice.remaining_useful_life_estimation.models.mlp

    **{"hidden_dim": ["MODEL", "HIDDEN_DIM"]} ) - def _create_model(self, df: DataFrame, target: Series): - num_sensors = df.shape[1] - + def _create_model(self, input_dim: int, output_dim: int): self.model = nn.Sequential( + nn.Dropout(0.5), nn.Flatten(), - nn.Linear(num_sensors * self.window_size, self.hidden_dim), + nn.Linear(input_dim * self.window_size, self.hidden_dim), nn.ReLU(), + nn.Dropout(0.5), nn.Linear(self.hidden_dim, 1), nn.Flatten(start_dim=0), )
    diff --git a/docs/_modules/index.html b/docs/_modules/index.html index 962bcad..e9571bf 100644 --- a/docs/_modules/index.html +++ b/docs/_modules/index.html @@ -367,6 +367,9 @@

    All modules for which code is available

  • ice.anomaly_detection.metrics
  • ice.anomaly_detection.models.autoencoder
  • ice.anomaly_detection.models.base
  • +
  • ice.anomaly_detection.models.gnn
  • +
  • ice.anomaly_detection.models.stgat
  • +
  • ice.anomaly_detection.models.transformer
  • ice.anomaly_detection.utils
  • ice.base
  • ice.configs
  • @@ -384,6 +387,7 @@

    All modules for which code is available

  • ice.remaining_useful_life_estimation.datasets
  • ice.remaining_useful_life_estimation.metrics
  • ice.remaining_useful_life_estimation.models.base
  • +
  • ice.remaining_useful_life_estimation.models.lstm
  • ice.remaining_useful_life_estimation.models.mlp
  • ice.remaining_useful_life_estimation.utils
diff --git a/docs/_sources/advanced/index.rst.txt b/docs/_sources/advanced/index.rst.txt index 241fa75..16b47c1 100644 --- a/docs/_sources/advanced/index.rst.txt +++ b/docs/_sources/advanced/index.rst.txt @@ -4,4 +4,7 @@ Advanced materials ################## -2nd project stage problem \ No newline at end of file +.. toctree:: + :maxdepth: 2 + + optimization_tutorial \ No newline at end of file diff --git a/docs/_sources/advanced/optimization_tutorial.ipynb.txt b/docs/_sources/advanced/optimization_tutorial.ipynb.txt new file mode 100644 index 0000000..541fbc1 --- /dev/null +++ b/docs/_sources/advanced/optimization_tutorial.ipynb.txt @@ -0,0 +1,881 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5908e829", + "metadata": {}, + "source": [ + "# Optimization tutorial" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7e7ea27e-aca8-4bdc-bcdb-dda20d6ac045", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\user\\conda\\envs\\ice_testing\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from ice.remaining_useful_life_estimation.datasets import RulCmapss\n", + "from ice.remaining_useful_life_estimation.models import MLP" + ] + }, + { + "cell_type": "markdown", + "id": "f9f612b9-f990-40f8-9191-af341827f7d9", + "metadata": {}, + "source": [ + "Create the MLP model and dataset class." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "81f891c7-de45-448f-a65b-806f2446e6e5", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 517496.66it/s]\n", + "Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 539376.73it/s]\n", + "Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 506176.62it/s]\n", + "Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 539069.75it/s]\n", + "Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 544734.35it/s]\n", + "Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 481589.37it/s]\n", + "Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 427933.68it/s]\n", + "Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 546792.80it/s]\n", + "Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 505412.69it/s]\n", + "Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 478933.98it/s]\n", + "Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 520419.66it/s]\n", + "Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 537036.66it/s]\n", + "Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 515225.24it/s]\n", + "Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 520644.44it/s]\n", + "Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 489809.10it/s]\n", + "Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 537023.31it/s]\n" + ] + } + ], + "source": [ + "dataset_class = RulCmapss()\n", + "data, target = dataset_class.df[0], dataset_class.target[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6fc9029e-7ce9-4cd8-9353-087b66556b17", + "metadata": {}, + "outputs": [], + "source": [ + "model = MLP(device=\"cuda\")" + ] + }, + { + "cell_type": "markdown", + "id": "f8ff1a72-19f4-4389-9362-35a80b8fb851", + "metadata": {}, + "source": [ + "Optimization **without changing the complexity** of the training process. Tune the lr of the training procedure using validation loss as optimization target" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c9e23868-09cb-4693-ad67-f1104245b7df", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-13 09:53:33,784] A new study created in memory with name: /parameter_lr study\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "trial step with lr = 0.00018951382914416393\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 33442.07it/s]\n", + "Epochs ...: 0%| | 0/5 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
FaultTPRFPR
000.96750.0000
110.97380.0000
230.96430.0000
340.95840.0000
450.97310.0000
560.96790.0000
670.96910.0000
790.96510.0000
8100.97880.0000
9110.95260.0000
10120.94180.0001
11130.97800.0000
12150.97520.0000
13160.96080.0000
14170.93570.0000
15180.97170.0000
16190.94820.0000
\n", + "" + ], + "text/plain": [ + " Fault TPR FPR\n", + "0 0 0.9675 0.0000\n", + "1 1 0.9738 0.0000\n", + "2 3 0.9643 0.0000\n", + "3 4 0.9584 0.0000\n", + "4 5 0.9731 0.0000\n", + "5 6 0.9679 0.0000\n", + "6 7 0.9691 0.0000\n", + "7 9 0.9651 0.0000\n", + "8 10 0.9788 0.0000\n", + "9 11 0.9526 0.0000\n", + "10 12 0.9418 0.0001\n", + "11 13 0.9780 0.0000\n", + "12 15 0.9752 0.0000\n", + "13 16 0.9608 0.0000\n", + "14 17 0.9357 0.0000\n", + "15 18 0.9717 0.0000\n", + "16 19 0.9482 0.0000" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "idx = np.array([1, 2, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]) - 1\n", + "pd.DataFrame({\n", + " 'Fault': idx,\n", + " 'TPR': np.array(metrics['true_positive_rate'])[idx],\n", + " 'FPR': np.array(metrics['false_positive_rate'])[idx],\n", + "}).round(4)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "2ad2b5f0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average TPR: 0.96\n" + ] + } + ], + "source": [ + "print(f'Average TPR: {np.array(metrics[\"true_positive_rate\"])[idx].mean():.2f}')" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "c3a334ba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "96.75\n", + "97.38\n", + "96.43\n", + "95.84\n", + "97.31\n", + "96.79\n", + "96.91\n", + "96.51\n", + "97.88\n", + "95.26\n", + "94.18\n", + "97.80\n", + "97.52\n", + "96.08\n", + "93.57\n", + "97.17\n", + "94.82\n" + ] + } + ], + "source": [ + "for i in np.array(metrics[\"true_positive_rate\"])[idx]*100:\n", + " print(f'{i:.2f}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "878cf22c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_sources/benchmark/hi_sota.ipynb.txt b/docs/_sources/benchmark/hi_sota.ipynb.txt new file mode 100644 index 0000000..c0aa68b --- /dev/null +++ b/docs/_sources/benchmark/hi_sota.ipynb.txt @@ -0,0 +1,338 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2da72a3-830f-4a34-b767-230ed0dbf154", + "metadata": {}, + "source": [ + "# Results of HI estimation using Stacked LSTM" + ] + }, + { + "cell_type": "markdown", + "id": "e3de787a-3db3-47c0-a1c6-a2b36cc6586f", + "metadata": {}, + "source": [ + "This notebook presents experimental results of hi estimation on the Milling dataset using the model MLP-256.\n", + "\n", + "Importing libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0d2bf80e-d11f-4c3d-ac75-e4f0cfd418f6", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\user\\conda\\envs\\ice_testing\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from ice.health_index_estimation.datasets import Milling\n", + "from ice.health_index_estimation.models import MLP, TCM, IE_SBiGRU, Stacked_LSTM\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "import torch\n", + "from tqdm.auto import trange\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "41bd9f60-51bf-4bb8-adc8-33a6abf923a1", + "metadata": {}, + "source": [ + "Initializing model class and train/test data split" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "814ba92e-e394-49a4-ac06-7b68361cf078", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Reading data/milling/case_1.csv: 100%|██████████| 153000/153000 [00:00<00:00, 1268689.48it/s]\n", + "Reading data/milling/case_2.csv: 100%|██████████| 117000/117000 [00:00<00:00, 1248873.41it/s]\n", + "Reading data/milling/case_3.csv: 100%|██████████| 126000/126000 [00:00<00:00, 1276983.81it/s]\n", + "Reading data/milling/case_4.csv: 100%|██████████| 63000/63000 [00:00<00:00, 1374151.83it/s]\n", + "Reading data/milling/case_5.csv: 100%|██████████| 54000/54000 [00:00<00:00, 1290003.79it/s]\n", + "Reading data/milling/case_6.csv: 100%|██████████| 9000/9000 [00:00<00:00, 1003395.34it/s]\n", + "Reading data/milling/case_7.csv: 100%|██████████| 72000/72000 [00:00<00:00, 1245529.71it/s]\n", + "Reading data/milling/case_8.csv: 100%|██████████| 54000/54000 [00:00<00:00, 1321487.68it/s]\n", + "Reading data/milling/case_9.csv: 100%|██████████| 81000/81000 [00:00<00:00, 1377467.66it/s]\n", + "Reading data/milling/case_10.csv: 100%|██████████| 90000/90000 [00:00<00:00, 1347769.63it/s]\n", + "Reading data/milling/case_11.csv: 100%|██████████| 207000/207000 [00:00<00:00, 1180064.87it/s]\n", + "Reading data/milling/case_12.csv: 100%|██████████| 126000/126000 [00:00<00:00, 1276980.73it/s]\n", + "Reading data/milling/case_13.csv: 100%|██████████| 135000/135000 [00:00<00:00, 1242669.46it/s]\n", + "Reading data/milling/case_14.csv: 100%|██████████| 81000/81000 [00:00<00:00, 1310826.20it/s]\n", + "Reading data/milling/case_15.csv: 100%|██████████| 63000/63000 [00:00<00:00, 1316886.37it/s]\n", + "Reading data/milling/case_16.csv: 100%|██████████| 18000/18000 [00:00<00:00, 1128731.62it/s]\n", + "C:\\Users\\user\\conda\\envs\\ice_testing\\Lib\\site-packages\\scipy\\interpolate\\_interpolate.py:479: RuntimeWarning: invalid value encountered in divide\n", + " slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None]\n" + ] + } + ], + "source": [ + "dataset_class = Milling()\n", + "\n", + "data, target = pd.concat(dataset_class.df), pd.concat(dataset_class.target) \n", + "test_data, test_target = dataset_class.test[0], dataset_class.test_target[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "75a22b4c-5b1c-4ce0-b74e-6886d1c61c90", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", + "import pandas as pd \n", + "\n", + "scaler = MinMaxScaler()\n", + "trainer_data = scaler.fit_transform(data)\n", + "tester_data = scaler.transform(test_data)\n", + "\n", + "trainer_data = pd.DataFrame(trainer_data, index=data.index, columns=data.columns)\n", + "tester_data = pd.DataFrame(tester_data, index=test_data.index, columns=test_data.columns)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "049500f5-24a0-4076-a2eb-2e15970e3abb", + "metadata": {}, + "outputs": [], + "source": [ + "# path_to_tar = \"hi_sota/\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ac338ba4-ab10-41cc-916b-0413bed0d1c6", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "model_class = Stacked_LSTM(\n", + " window_size=64,\n", + " stride=1024, # 1024\n", + " batch_size=253, # 256\n", + " lr= 0.0031789041005068647, # 0.0004999805761074147,\n", + " num_epochs=55,\n", + " verbose=True,\n", + " device='cuda'\n", + " )\n", + "# model_class.fit(trainer_data, target)\n", + "model_class.load_checkpoint(path_to_tar + \"stack_sota.tar\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1512dcc2-1765-42f6-ab71-b8a44aa699df", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 2809.31it/s]\n", + " \r" + ] + }, + { + "data": { + "text/plain": [ + "{'mse': 0.0022332468596409335, 'rmse': 0.047257241346072384}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_class.evaluate(tester_data, test_target)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "defe310c-5d1e-412d-b68d-447a975ef937", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b58a2a2b-9b73-4ac8-959e-4f93db8cd8eb", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e7638bc7-d161-4266-b2d1-aa3545856853", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "model_class = TCM(\n", + " window_size=64,\n", + " stride=1024, # 1024\n", + " batch_size=253, # 256\n", + " lr= 0.0031789041005068647, # 0.0004999805761074147,\n", + " num_epochs=55,\n", + " verbose=True,\n", + " device='cuda'\n", + " )\n", + "# model_class.fit(trainer_data, target)\n", + "model_class.load_checkpoint(path_to_tar + \"TCM_sota.tar\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "550c5609-a0ec-48d3-9149-a4de69ba8368", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 3511.98it/s]\n", + " \r" + ] + }, + { + "data": { + "text/plain": [ + "{'mse': 0.004014168163365719, 'rmse': 0.06335746335962102}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_class.evaluate(tester_data, test_target)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ede9a8c3-27e5-42df-980e-bcdb1ddcec73", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1ee17ee-09a8-4e7e-a81d-bea529af3f7f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "767c37e2-1750-46d5-9cd7-f628e7322305", + "metadata": {}, + "source": [ + "Training and testing with difference random seed for uncertainty estimation" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1c5e6783-49df-46b5-86a0-6f09ee501d0e", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "model_class = IE_SBiGRU(\n", + " window_size=64,\n", + " stride=1024, # 1024\n", + " batch_size=253, # 256\n", + " lr= 0.0011, # 0.0004999805761074147,\n", + " num_epochs=35,\n", + " verbose=True,\n", + " device='cuda'\n", + " )\n", + "# model_class.fit(trainer_data, target)\n", + "model_class.load_checkpoint(path_to_tar + \"IE_SBiGRU_sota.tar\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6425462a-14a5-4580-b082-92e9165d8984", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 2341.13it/s]\n", + " \r" + ] + }, + { + "data": { + "text/plain": [ + "{'mse': 0.004956771691496658, 'rmse': 0.07040434426579555}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_class.evaluate(tester_data, test_target)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_sources/benchmark/index.rst.txt b/docs/_sources/benchmark/index.rst.txt index e8a7f30..6cae700 100644 --- a/docs/_sources/benchmark/index.rst.txt +++ b/docs/_sources/benchmark/index.rst.txt @@ -9,6 +9,12 @@ Benchmarking :caption: Contents: fd_benchmark_mlp_256 + fd_benchmark_tcn ad_benchmark_autoencodermlp_256 + ad_benchmark_transformer + ad_benchmark_stgat + ad_benchmark_gnn rul_benchmark_lstm_256 - hi_benchmark_mlp_256 \ No newline at end of file + rul_sota + hi_benchmark_mlp_256 + hi_sota \ No newline at end of file diff --git a/docs/_sources/benchmark/rul_sota.ipynb.txt b/docs/_sources/benchmark/rul_sota.ipynb.txt new file mode 100644 index 0000000..4a7e0a9 --- /dev/null +++ b/docs/_sources/benchmark/rul_sota.ipynb.txt @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2da72a3-830f-4a34-b767-230ed0dbf154", + "metadata": {}, + "source": [ + "# Results of RUL estimation using IR" + ] + }, + { + "cell_type": "markdown", + "id": "e3de787a-3db3-47c0-a1c6-a2b36cc6586f", + "metadata": {}, + "source": [ + "This notebook presents experimental results of rul estimation on the CMAPSS dataset using the model lstm-256.\n", + "\n", + "Importing libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0d2bf80e-d11f-4c3d-ac75-e4f0cfd418f6", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\user\\conda\\envs\\ice_testing\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from ice.remaining_useful_life_estimation.datasets import RulCmapss\n", + "from ice.remaining_useful_life_estimation.models import IR \n", + "\n", + "import pandas as pd" + ] + }, + { + "cell_type": "markdown", + "id": "41bd9f60-51bf-4bb8-adc8-33a6abf923a1", + "metadata": {}, + "source": [ + "Initializing model class and train/test data split for fd001 subdataset" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "814ba92e-e394-49a4-ac06-7b68361cf078", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 504872.87it/s]\n", + "Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 533656.45it/s]\n", + "Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 496051.49it/s]\n", + "Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 532245.75it/s]\n", + "Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 547602.44it/s]\n", + "Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 528810.42it/s]\n", + "Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 496134.57it/s]\n", + "Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 529770.68it/s]\n", + "Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 486699.50it/s]\n", + "Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 501537.62it/s]\n", + "Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 512334.66it/s]\n", + "Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 523435.48it/s]\n", + "Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 474171.77it/s]\n", + "Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 501537.62it/s]\n", + "Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 520419.66it/s]\n", + "Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 519167.37it/s]\n" + ] + } + ], + "source": [ + "dataset_class = RulCmapss()\n", + "\n", + "data, target = dataset_class.df[0], dataset_class.target[0]\n", + "test_data, test_target = dataset_class.test[0], dataset_class.test_target[0] " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "717c0b98-a3cb-4cef-bf11-b9274617eddc", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", + "import pandas as pd \n", + "\n", + "scaler = MinMaxScaler()\n", + "trainer_data = scaler.fit_transform(data)\n", + "tester_data = scaler.transform(test_data)\n", + "\n", + "trainer_data = pd.DataFrame(trainer_data, index=data.index, columns=data.columns)\n", + "tester_data = pd.DataFrame(tester_data, index=test_data.index, columns=test_data.columns)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "767c37e2-1750-46d5-9cd7-f628e7322305", + "metadata": {}, + "source": [ + "Training and testing with difference random seed for uncertainty estimation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f9bd5cb4-3cbd-48f2-bb70-8154f2a8e051", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\user\\conda\\envs\\ice_testing\\Lib\\site-packages\\torch\\nn\\modules\\transformer.py:306: UserWarning: enable_nested_tensor is True, but self.use_nested_tensor is False because encoder_layer.activation_relu_or_gelu was not True\n", + " warnings.warn(f\"enable_nested_tensor is True, but self.use_nested_tensor is False because {why_not_sparsity_fast_path}\")\n" + ] + } + ], + "source": [ + "model_class = IR()\n", + "model_class.load_checkpoint(\"rul_sota.tar\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "15689271-81f5-4197-934a-7ac71c32f057", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 11148.24it/s]\n", + " \r" + ] + }, + { + "data": { + "text/plain": [ + "{'rmse': 11.99217470692219, 'cmapss_score': 25394.12755711561}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_class.evaluate(tester_data, test_target)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_sources/reference/ice.anomaly_detection.models.rst.txt b/docs/_sources/reference/ice.anomaly_detection.models.rst.txt index a602716..a7bd8f1 100644 --- a/docs/_sources/reference/ice.anomaly_detection.models.rst.txt +++ b/docs/_sources/reference/ice.anomaly_detection.models.rst.txt @@ -6,15 +6,32 @@ BaseAnomalyDetection .. automodule:: ice.anomaly_detection.models.base :members: - :undoc-members: :show-inheritance: AutoEncoderMLP -------------- -.. automodule:: ice.anomaly_detection.models.autoencoder +.. autoclass:: ice.anomaly_detection.models.autoencoder.AutoEncoderMLP :members: - :undoc-members: :show-inheritance: +AnomalyTransformer +------------------ +.. autoclass:: ice.anomaly_detection.models.transformer.AnomalyTransformer + :members: + :show-inheritance: + +STGAT-MAD +------------------ + +.. autoclass:: ice.anomaly_detection.models.stgat.STGAT_MAD + :members: + :show-inheritance: + +GSL-GNN +------------------ + +.. autoclass:: ice.anomaly_detection.models.gnn.GSL_GNN + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/_sources/reference/ice.fault_diagnosis.models.rst.txt b/docs/_sources/reference/ice.fault_diagnosis.models.rst.txt index 47e43f4..f931a78 100644 --- a/docs/_sources/reference/ice.fault_diagnosis.models.rst.txt +++ b/docs/_sources/reference/ice.fault_diagnosis.models.rst.txt @@ -6,7 +6,6 @@ BaseFaultDiagnosis .. automodule:: ice.fault_diagnosis.models.base :members: - :undoc-members: :show-inheritance: MLP @@ -14,7 +13,6 @@ MLP .. automodule:: ice.fault_diagnosis.models.mlp :members: - :undoc-members: :show-inheritance: TCN @@ -22,6 +20,5 @@ TCN .. automodule:: ice.fault_diagnosis.models.tcn :members: - :undoc-members: :show-inheritance: diff --git a/docs/_sources/reference/ice.health_index_estimation.models.rst.txt b/docs/_sources/reference/ice.health_index_estimation.models.rst.txt index 86f5ce2..ac496e1 100644 --- a/docs/_sources/reference/ice.health_index_estimation.models.rst.txt +++ b/docs/_sources/reference/ice.health_index_estimation.models.rst.txt @@ -6,14 +6,11 @@ BaseRemainingUsefulLifeEstimation .. automodule:: ice.health_index_estimation.models.base :members: - :undoc-members: :show-inheritance: MLP --- -.. automodule:: ice.health_index_estimation.models.mlp +.. autoclass:: ice.health_index_estimation.models.mlp.MLP :members: - :undoc-members: :show-inheritance: - diff --git a/docs/_sources/reference/ice.remaining_useful_life_estimation.models.rst.txt b/docs/_sources/reference/ice.remaining_useful_life_estimation.models.rst.txt index 2ce815c..61d89a0 100644 --- a/docs/_sources/reference/ice.remaining_useful_life_estimation.models.rst.txt +++ b/docs/_sources/reference/ice.remaining_useful_life_estimation.models.rst.txt @@ -6,14 +6,18 @@ BaseHealthIndexEstimation .. automodule:: ice.remaining_useful_life_estimation.models.base :members: - :undoc-members: :show-inheritance: MLP --- -.. automodule:: ice.remaining_useful_life_estimation.models.mlp +.. autoclass:: ice.remaining_useful_life_estimation.models.mlp.MLP :members: - :undoc-members: :show-inheritance: +LSTM +---- + +.. autoclass:: ice.remaining_useful_life_estimation.models.lstm.LSTM + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/_sources/reference/ice.rst.txt b/docs/_sources/reference/ice.rst.txt deleted file mode 100644 index f83e0bb..0000000 --- a/docs/_sources/reference/ice.rst.txt +++ /dev/null @@ -1,27 +0,0 @@ -ice package -=========== - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - :hidden: - - Anomaly detection - ice.configs - ice.fault_diagnosis - ice.health_index_estimation - ice.remaining_useful_life_estimation - -Submodules ----------- - -ice.base module ---------------- - -.. automodule:: ice.base - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/_sources/reference/modules.rst.txt b/docs/_sources/reference/modules.rst.txt deleted file mode 100644 index d4b70e8..0000000 --- a/docs/_sources/reference/modules.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -ice -=== - -.. toctree:: - :maxdepth: 4 - - ice diff --git a/docs/_sources/start/datasets.rst.txt b/docs/_sources/start/datasets.rst.txt deleted file mode 100644 index b178b4f..0000000 --- a/docs/_sources/start/datasets.rst.txt +++ /dev/null @@ -1,5 +0,0 @@ -.. _datasets: - -######## -Datasets -######## \ No newline at end of file diff --git a/docs/_sources/start/tasks/task_hi.rst.txt b/docs/_sources/start/tasks/task_hi.rst.txt index 54693e1..428e3b0 100644 --- a/docs/_sources/start/tasks/task_hi.rst.txt +++ b/docs/_sources/start/tasks/task_hi.rst.txt @@ -85,14 +85,14 @@ equipment :math:`i`, :math:`N` is number of test samples. References """""""""" -The reference results of state-of-the-art papers for the original [1] -dataset are presented in table 1. +.. The reference results of state-of-the-art papers for the original [1] + dataset are presented in table 1. -Since the test dataset involves processes with unknown characteristics, it limits most -related HI-based work approaches, especially similarity-based techniques, which require -information about similar process trajectories. + Since the test dataset involves processes with unknown characteristics, it limits most + related HI-based work approaches, especially similarity-based techniques, which require + information about similar process trajectories. -.. table:: Table 1: References for C-Milling dataset, cuts ‘1’, ‘3’, ‘7’ testing, with the original dataset wear target. + .. table:: Table 1: References for C-Milling dataset, cuts ‘1’, ‘3’, ‘7’ testing, with the original dataset wear target. +--------------------------------+-----------------------+ | Model | RMSE (cycles) | diff --git a/docs/_static/hi/HI_deviation.png b/docs/_static/hi/HI_deviation.png deleted file mode 100644 index aed36e9256ed17c68933447ea038ce208e6d043f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112570 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 P9n5~~J!mz&1Ny=2|GtdNU%mD`!tHl* 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 F{{zEKaF+l8 diff --git a/docs/advanced/index.html b/docs/advanced/index.html index 4247bc8..3fe31ec 100644 --- a/docs/advanced/index.html +++ b/docs/advanced/index.html @@ -49,7 +49,7 @@ - + @@ -244,7 +244,7 @@
    -
    +
    @@ -320,6 +320,17 @@
    + + @@ -371,7 +382,11 @@

    Advanced materials#

    -

    2nd project stage problem

    +
    @@ -394,11 +409,11 @@

    next

    -

    API reference

    +

    Optimization tutorial

    diff --git a/docs/advanced/optimization_tutorial.html b/docs/advanced/optimization_tutorial.html new file mode 100644 index 0000000..530435a --- /dev/null +++ b/docs/advanced/optimization_tutorial.html @@ -0,0 +1,995 @@ + + + + + + + + + + + + Optimization tutorial — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + + + +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    + + + + +
    + +
    + + +
    +
    + + + + + +
    + +
    +

    Optimization tutorial#

    +
    +
    +
    from ice.remaining_useful_life_estimation.datasets import RulCmapss
    +from ice.remaining_useful_life_estimation.models import MLP
    +
    +
    +
    +
    +
    C:\Users\user\conda\envs\ice_testing\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
    +  from .autonotebook import tqdm as notebook_tqdm
    +
    +
    +
    +
    +

    Create the MLP model and dataset class.

    +
    +
    +
    dataset_class = RulCmapss()
    +data, target = dataset_class.df[0], dataset_class.target[0]
    +
    +
    +
    +
    +
    Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 517496.66it/s]
    +Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 539376.73it/s]
    +Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 506176.62it/s]
    +Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 539069.75it/s]
    +Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 544734.35it/s]
    +Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 481589.37it/s]
    +Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 427933.68it/s]
    +Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 546792.80it/s]
    +Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 505412.69it/s]
    +Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 478933.98it/s]
    +Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 520419.66it/s]
    +Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 537036.66it/s]
    +Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 515225.24it/s]
    +Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 520644.44it/s]
    +Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 489809.10it/s]
    +Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 537023.31it/s]
    +
    +
    +
    +
    +
    +
    +
    model = MLP(device="cuda")
    +
    +
    +
    +
    +

    Optimization without changing the complexity of the training process. Tune the lr of the training procedure using validation loss as optimization target

    +
    +
    +
    # model_class.optimize(data, target, optimize_parameter, optimize_range, direction, n_trials, epochs, optimize_metric)
    +model.optimize(data, target, optimize_parameter="lr", optimize_range=(5e-5, 1e-3), direction="minimize", n_trials=3, epochs=5) # if optimize_metric is None, than validation loss is using as optimization target
    +
    +
    +
    +
    +
    [I 2024-08-13 09:53:33,784] A new study created in memory with name: /parameter_lr study
    +
    +
    +
    trial step with lr = 0.00018951382914416393
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 33442.07it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  10%|█         | 24/232 [00:00<00:00, 237.23it/s]
    +Steps ...:  43%|████▎     | 99/232 [00:00<00:00, 535.34it/s]
    +Steps ...:  75%|███████▌  | 174/232 [00:00<00:00, 631.00it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:01,  2.31it/s]    
    +
    +
    +
    Epoch 1, Loss: 39.3309
    +Epoch 1, Validation Loss: 39.3238, Metrics: {'rmse': 50.063830627114406, 'cmapss_score': 1009001.2641988464}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  30%|██▉       | 69/232 [00:00<00:00, 686.55it/s]
    +Steps ...:  62%|██████▏   | 143/232 [00:00<00:00, 715.14it/s]
    +Steps ...:  94%|█████████▍| 218/232 [00:00<00:00, 730.32it/s]
    +Epochs ...:  40%|████      | 2/5 [00:00<00:01,  2.54it/s]    
    +
    +
    +
    Epoch 2, Loss: 36.8534
    +Epoch 2, Validation Loss: 32.2449, Metrics: {'rmse': 40.68462185124231, 'cmapss_score': 326161.11634528585}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  33%|███▎      | 76/232 [00:00<00:00, 751.21it/s]
    +Steps ...:  66%|██████▌   | 152/232 [00:00<00:00, 755.41it/s]
    +Steps ...:  98%|█████████▊| 228/232 [00:00<00:00, 756.70it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:01<00:00,  2.68it/s]    
    +
    +
    +
    Epoch 3, Loss: 22.9493
    +Epoch 3, Validation Loss: 29.7487, Metrics: {'rmse': 38.11920046818511, 'cmapss_score': 251687.82706875}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  32%|███▏      | 75/232 [00:00<00:00, 745.06it/s]
    +Steps ...:  65%|██████▌   | 151/232 [00:00<00:00, 750.90it/s]
    +Steps ...:  98%|█████████▊| 227/232 [00:00<00:00, 753.53it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:01<00:00,  2.74it/s]    
    +
    +
    +
    Epoch 4, Loss: 27.0963
    +Epoch 4, Validation Loss: 28.3393, Metrics: {'rmse': 36.542765927986274, 'cmapss_score': 207206.28175345066}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  32%|███▏      | 74/232 [00:00<00:00, 735.12it/s]
    +Steps ...:  65%|██████▌   | 151/232 [00:00<00:00, 752.66it/s]
    +Steps ...:  98%|█████████▊| 227/232 [00:00<00:00, 755.28it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:01<00:00,  2.70it/s]    
    +[I 2024-08-13 09:53:36,351] Trial 0 finished with value: 26.89373407131288 and parameters: {'lr': 0.00018951382914416393}. Best is trial 0 with value: 26.89373407131288.
    +
    +
    +
    Epoch 5, Loss: 30.0107
    +Epoch 5, Validation Loss: 26.8937, Metrics: {'rmse': 34.96109612817986, 'cmapss_score': 171362.21049284632}
    +trial step with lr = 0.0007874022874638446
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 12542.02it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  17%|█▋        | 39/232 [00:00<00:00, 389.34it/s]
    +Steps ...:  48%|████▊     | 111/232 [00:00<00:00, 581.84it/s]
    +Steps ...:  81%|████████  | 187/232 [00:00<00:00, 661.02it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:01,  2.48it/s]    
    +
    +
    +
    Epoch 1, Loss: 32.6104
    +Epoch 1, Validation Loss: 28.4569, Metrics: {'rmse': 36.641393207296055, 'cmapss_score': 206445.81209835963}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  33%|███▎      | 76/232 [00:00<00:00, 755.00it/s]
    +Steps ...:  66%|██████▌   | 153/232 [00:00<00:00, 760.84it/s]
    +Steps ...:  99%|█████████▉| 230/232 [00:00<00:00, 752.50it/s]
    +Epochs ...:  40%|████      | 2/5 [00:00<00:01,  2.69it/s]    
    +
    +
    +
    Epoch 2, Loss: 29.5834
    +Epoch 2, Validation Loss: 22.3682, Metrics: {'rmse': 29.68363304759782, 'cmapss_score': 98649.45005642212}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  33%|███▎      | 77/232 [00:00<00:00, 764.93it/s]
    +Steps ...:  66%|██████▋   | 154/232 [00:00<00:00, 498.98it/s]
    +Steps ...:  91%|█████████ | 210/232 [00:00<00:00, 435.48it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:01<00:00,  2.10it/s]    
    +
    +
    +
    Epoch 3, Loss: 17.5193
    +Epoch 3, Validation Loss: 17.9444, Metrics: {'rmse': 24.369740561776197, 'cmapss_score': 82673.16229614464}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 37/232 [00:00<00:00, 363.96it/s]
    +Steps ...:  32%|███▏      | 74/232 [00:00<00:00, 359.81it/s]
    +Steps ...:  47%|████▋     | 110/232 [00:00<00:00, 355.65it/s]
    +Steps ...:  63%|██████▎   | 146/232 [00:00<00:00, 352.98it/s]
    +Steps ...:  79%|███████▉  | 183/232 [00:00<00:00, 357.91it/s]
    +Steps ...:  94%|█████████▍| 219/232 [00:00<00:00, 355.44it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:02<00:00,  1.72it/s]    
    +
    +
    +
    Epoch 4, Loss: 16.3577
    +Epoch 4, Validation Loss: 16.1891, Metrics: {'rmse': 22.407311130072234, 'cmapss_score': 90375.15431472883}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 357.63it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 343.62it/s]
    +Steps ...:  47%|████▋     | 108/232 [00:00<00:00, 346.81it/s]
    +Steps ...:  63%|██████▎   | 146/232 [00:00<00:00, 356.10it/s]
    +Steps ...:  79%|███████▉  | 183/232 [00:00<00:00, 360.17it/s]
    +Steps ...:  95%|█████████▍| 220/232 [00:00<00:00, 358.45it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:02<00:00,  1.76it/s]    
    +[I 2024-08-13 09:53:39,216] Trial 1 finished with value: 16.189056094099836 and parameters: {'lr': 0.0007874022874638446}. Best is trial 1 with value: 16.189056094099836.
    +
    +
    +
    Epoch 5, Loss: 18.7968
    +Epoch 5, Validation Loss: 16.4563, Metrics: {'rmse': 22.47353109963666, 'cmapss_score': 73324.81707642713}
    +trial step with lr = 0.00012086132027556038
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 12541.65it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▍        | 34/232 [00:00<00:00, 337.76it/s]
    +Steps ...:  31%|███       | 71/232 [00:00<00:00, 351.34it/s]
    +Steps ...:  46%|████▌     | 107/232 [00:00<00:00, 354.90it/s]
    +Steps ...:  62%|██████▏   | 143/232 [00:00<00:00, 356.58it/s]
    +Steps ...:  78%|███████▊  | 180/232 [00:00<00:00, 357.98it/s]
    +Steps ...:  94%|█████████▎| 217/232 [00:00<00:00, 358.82it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:02,  1.35it/s]    
    +
    +
    +
    Epoch 1, Loss: 42.6350
    +Epoch 1, Validation Loss: 41.5396, Metrics: {'rmse': 53.309621676839306, 'cmapss_score': 1652572.5547515503}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 355.25it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 356.65it/s]
    +Steps ...:  47%|████▋     | 108/232 [00:00<00:00, 357.09it/s]
    +Steps ...:  62%|██████▏   | 144/232 [00:00<00:00, 355.91it/s]
    +Steps ...:  78%|███████▊  | 181/232 [00:00<00:00, 360.10it/s]
    +Steps ...:  94%|█████████▍| 218/232 [00:00<00:00, 359.01it/s]
    +Epochs ...:  40%|████      | 2/5 [00:01<00:02,  1.35it/s]    
    +
    +
    +
    Epoch 2, Loss: 36.4397
    +Epoch 2, Validation Loss: 37.1115, Metrics: {'rmse': 46.95681782704423, 'cmapss_score': 653010.5664245718}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 351.65it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 353.10it/s]
    +Steps ...:  47%|████▋     | 109/232 [00:00<00:00, 356.46it/s]
    +Steps ...:  62%|██████▎   | 145/232 [00:00<00:00, 356.14it/s]
    +Steps ...:  78%|███████▊  | 181/232 [00:00<00:00, 355.91it/s]
    +Steps ...:  94%|█████████▎| 217/232 [00:00<00:00, 354.12it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:02<00:01,  1.34it/s]    
    +
    +
    +
    Epoch 3, Loss: 25.7715
    +Epoch 3, Validation Loss: 32.9065, Metrics: {'rmse': 41.430416515203575, 'cmapss_score': 349818.50039921864}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 37/232 [00:00<00:00, 360.43it/s]
    +Steps ...:  32%|███▏      | 74/232 [00:00<00:00, 359.15it/s]
    +Steps ...:  48%|████▊     | 111/232 [00:00<00:00, 359.73it/s]
    +Steps ...:  64%|██████▍   | 148/232 [00:00<00:00, 360.01it/s]
    +Steps ...:  80%|███████▉  | 185/232 [00:00<00:00, 359.48it/s]
    +Steps ...:  95%|█████████▌| 221/232 [00:00<00:00, 356.30it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:02<00:00,  1.35it/s]    
    +
    +
    +
    Epoch 4, Loss: 29.5199
    +Epoch 4, Validation Loss: 30.6848, Metrics: {'rmse': 39.035223216989905, 'cmapss_score': 274871.78833234054}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 350.68it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 352.70it/s]
    +Steps ...:  47%|████▋     | 108/232 [00:00<00:00, 348.67it/s]
    +Steps ...:  62%|██████▏   | 143/232 [00:00<00:00, 332.92it/s]
    +Steps ...:  77%|███████▋  | 178/232 [00:00<00:00, 338.04it/s]
    +Steps ...:  92%|█████████▏| 214/232 [00:00<00:00, 343.43it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:03<00:00,  1.34it/s]    
    +[I 2024-08-13 09:53:42,980] Trial 2 finished with value: 29.60851450664241 and parameters: {'lr': 0.00012086132027556038}. Best is trial 1 with value: 16.189056094099836.
    +
    +
    +
    Epoch 5, Loss: 33.5629
    +Epoch 5, Validation Loss: 29.6085, Metrics: {'rmse': 37.93360429205696, 'cmapss_score': 242353.786303807}
    +Best hyperparameters: {'lr': 0.0007874022874638446}
    +Best trial: FrozenTrial(number=1, state=1, values=[16.189056094099836], datetime_start=datetime.datetime(2024, 8, 13, 9, 53, 36, 351082), datetime_complete=datetime.datetime(2024, 8, 13, 9, 53, 39, 216093), params={'lr': 0.0007874022874638446}, user_attrs={}, system_attrs={}, intermediate_values={1: 28.456903271558808, 2: 22.368176297443668, 3: 17.944359942180355, 4: 16.189056094099836, 5: 16.456260332247105}, distributions={'lr': FloatDistribution(high=0.001, log=False, low=5e-05, step=None)}, trial_id=1, value=None)
    +
    +
    +
    +
    +

    Optimization with changing the complexity of the training process. Tune the MLP hidden dimension size using MSE metric as optimization target

    +
    +
    +
    # model_class.optimize(data, target, optimize_parameter, optimize_range, direction, n_trials, epochs, optimize_metric)
    +model.optimize(data, target, optimize_parameter="hidden_dim", optimize_range=(256, 1024), direction="minimize", optimize_metric="rmse", n_trials=3, epochs=5)
    +
    +
    +
    +
    +
    [I 2024-08-13 09:53:42,992] A new study created in memory with name: /parameter_hidden_dim study
    +
    +
    +
    trial step with hidden_dim = 702
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 11148.24it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 32/232 [00:00<00:00, 315.23it/s]
    +Steps ...:  29%|██▉       | 67/232 [00:00<00:00, 333.10it/s]
    +Steps ...:  44%|████▍     | 102/232 [00:00<00:00, 338.76it/s]
    +Steps ...:  59%|█████▉    | 137/232 [00:00<00:00, 339.63it/s]
    +Steps ...:  74%|███████▍  | 172/232 [00:00<00:00, 340.10it/s]
    +Steps ...:  89%|████████▉ | 207/232 [00:00<00:00, 341.73it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:03,  1.28it/s]    
    +
    +
    +
    Epoch 1, Loss: 41.7009
    +Epoch 1, Validation Loss: 41.0446, Metrics: {'rmse': 52.46324288978529, 'cmapss_score': 1459642.2102222845}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 350.68it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 354.74it/s]
    +Steps ...:  47%|████▋     | 108/232 [00:00<00:00, 354.46it/s]
    +Steps ...:  62%|██████▏   | 144/232 [00:00<00:00, 354.32it/s]
    +Steps ...:  78%|███████▊  | 181/232 [00:00<00:00, 357.14it/s]
    +Steps ...:  94%|█████████▎| 217/232 [00:00<00:00, 354.29it/s]
    +Epochs ...:  40%|████      | 2/5 [00:01<00:02,  1.31it/s]    
    +
    +
    +
    Epoch 2, Loss: 34.5571
    +Epoch 2, Validation Loss: 35.5294, Metrics: {'rmse': 44.69688917194367, 'cmapss_score': 487095.93532787054}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 33/232 [00:00<00:00, 328.99it/s]
    +Steps ...:  29%|██▉       | 67/232 [00:00<00:00, 335.00it/s]
    +Steps ...:  44%|████▍     | 103/232 [00:00<00:00, 342.81it/s]
    +Steps ...:  59%|█████▉    | 138/232 [00:00<00:00, 342.07it/s]
    +Steps ...:  75%|███████▍  | 173/232 [00:00<00:00, 340.46it/s]
    +Steps ...:  90%|████████▉ | 208/232 [00:00<00:00, 342.89it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:02<00:01,  1.30it/s]    
    +
    +
    +
    Epoch 3, Loss: 24.0595
    +Epoch 3, Validation Loss: 31.7725, Metrics: {'rmse': 40.124605880566875, 'cmapss_score': 305081.3116466362}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▌        | 35/232 [00:00<00:00, 344.29it/s]
    +Steps ...:  30%|███       | 70/232 [00:00<00:00, 344.29it/s]
    +Steps ...:  46%|████▌     | 106/232 [00:00<00:00, 347.96it/s]
    +Steps ...:  61%|██████    | 142/232 [00:00<00:00, 350.98it/s]
    +Steps ...:  77%|███████▋  | 178/232 [00:00<00:00, 350.24it/s]
    +Steps ...:  92%|█████████▏| 214/232 [00:00<00:00, 349.78it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:03<00:00,  1.30it/s]    
    +
    +
    +
    Epoch 4, Loss: 27.7565
    +Epoch 4, Validation Loss: 29.9569, Metrics: {'rmse': 38.28838337636238, 'cmapss_score': 252440.2810893617}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 32/232 [00:00<00:00, 314.78it/s]
    +Steps ...:  28%|██▊       | 66/232 [00:00<00:00, 324.48it/s]
    +Steps ...:  44%|████▍     | 102/232 [00:00<00:00, 337.97it/s]
    +Steps ...:  59%|█████▊    | 136/232 [00:00<00:00, 338.51it/s]
    +Steps ...:  73%|███████▎  | 170/232 [00:00<00:00, 330.41it/s]
    +Steps ...:  88%|████████▊ | 204/232 [00:00<00:00, 324.28it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:03<00:00,  1.29it/s]    
    +[I 2024-08-13 09:53:46,884] Trial 0 finished with value: 37.12774637917532 and parameters: {'hidden_dim': 702}. Best is trial 0 with value: 37.12774637917532.
    +
    +
    +
    Epoch 5, Loss: 31.4343
    +Epoch 5, Validation Loss: 28.8321, Metrics: {'rmse': 37.12774637917532, 'cmapss_score': 219477.3266793877}
    +trial step with hidden_dim = 662
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 12542.40it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▍        | 34/232 [00:00<00:00, 334.45it/s]
    +Steps ...:  30%|██▉       | 69/232 [00:00<00:00, 342.21it/s]
    +Steps ...:  45%|████▍     | 104/232 [00:00<00:00, 337.83it/s]
    +Steps ...:  60%|█████▉    | 139/232 [00:00<00:00, 340.97it/s]
    +Steps ...:  75%|███████▌  | 174/232 [00:00<00:00, 342.74it/s]
    +Steps ...:  90%|█████████ | 209/232 [00:00<00:00, 344.41it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:03,  1.26it/s]    
    +
    +
    +
    Epoch 1, Loss: 41.5648
    +Epoch 1, Validation Loss: 41.1247, Metrics: {'rmse': 52.64622625632633, 'cmapss_score': 1497278.352392366}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 32/232 [00:00<00:00, 319.07it/s]
    +Steps ...:  28%|██▊       | 66/232 [00:00<00:00, 330.08it/s]
    +Steps ...:  44%|████▎     | 101/232 [00:00<00:00, 338.13it/s]
    +Steps ...:  59%|█████▊    | 136/232 [00:00<00:00, 341.91it/s]
    +Steps ...:  74%|███████▍  | 172/232 [00:00<00:00, 346.34it/s]
    +Steps ...:  89%|████████▉ | 207/232 [00:00<00:00, 346.80it/s]
    +Epochs ...:  40%|████      | 2/5 [00:01<00:02,  1.27it/s]    
    +
    +
    +
    Epoch 2, Loss: 37.5466
    +Epoch 2, Validation Loss: 35.6801, Metrics: {'rmse': 45.00773698916855, 'cmapss_score': 511059.8643841665}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 33/232 [00:00<00:00, 321.46it/s]
    +Steps ...:  28%|██▊       | 66/232 [00:00<00:00, 320.47it/s]
    +Steps ...:  43%|████▎     | 99/232 [00:00<00:00, 315.95it/s]
    +Steps ...:  57%|█████▋    | 132/232 [00:00<00:00, 318.65it/s]
    +Steps ...:  71%|███████   | 165/232 [00:00<00:00, 321.90it/s]
    +Steps ...:  86%|████████▌ | 200/232 [00:00<00:00, 330.28it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:02<00:01,  1.25it/s]    
    +
    +
    +
    Epoch 3, Loss: 26.5062
    +Epoch 3, Validation Loss: 31.7830, Metrics: {'rmse': 40.07700663323968, 'cmapss_score': 305080.57834935177}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  14%|█▍        | 32/232 [00:00<00:00, 318.04it/s]
    +Steps ...:  28%|██▊       | 65/232 [00:00<00:00, 323.80it/s]
    +Steps ...:  44%|████▎     | 101/232 [00:00<00:00, 337.28it/s]
    +Steps ...:  59%|█████▊    | 136/232 [00:00<00:00, 341.38it/s]
    +Steps ...:  74%|███████▎  | 171/232 [00:00<00:00, 342.43it/s]
    +Steps ...:  89%|████████▉ | 207/232 [00:00<00:00, 345.26it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:03<00:00,  1.26it/s]    
    +
    +
    +
    Epoch 4, Loss: 28.9049
    +Epoch 4, Validation Loss: 29.9922, Metrics: {'rmse': 38.29525364434387, 'cmapss_score': 252989.93131717102}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▌        | 35/232 [00:00<00:00, 345.28it/s]
    +Steps ...:  31%|███       | 71/232 [00:00<00:00, 350.49it/s]
    +Steps ...:  46%|████▌     | 107/232 [00:00<00:00, 352.15it/s]
    +Steps ...:  62%|██████▏   | 143/232 [00:00<00:00, 349.53it/s]
    +Steps ...:  77%|███████▋  | 179/232 [00:00<00:00, 351.74it/s]
    +Steps ...:  93%|█████████▎| 215/232 [00:00<00:00, 348.30it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:03<00:00,  1.27it/s]    
    +[I 2024-08-13 09:53:50,841] Trial 1 finished with value: 37.207252080735245 and parameters: {'hidden_dim': 662}. Best is trial 0 with value: 37.12774637917532.
    +
    +
    +
    Epoch 5, Loss: 31.7666
    +Epoch 5, Validation Loss: 28.9132, Metrics: {'rmse': 37.207252080735245, 'cmapss_score': 221679.17640028123}
    +trial step with hidden_dim = 879
    +
    +
    +
    Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 11148.54it/s]
    +Epochs ...:   0%|          | 0/5 [00:00<?, ?it/s]
    +Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▌        | 35/232 [00:00<00:00, 340.94it/s]
    +Steps ...:  31%|███       | 71/232 [00:00<00:00, 348.66it/s]
    +Steps ...:  46%|████▌     | 107/232 [00:00<00:00, 351.77it/s]
    +Steps ...:  62%|██████▏   | 143/232 [00:00<00:00, 354.07it/s]
    +Steps ...:  77%|███████▋  | 179/232 [00:00<00:00, 351.60it/s]
    +Steps ...:  93%|█████████▎| 215/232 [00:00<00:00, 353.62it/s]
    +Epochs ...:  20%|██        | 1/5 [00:00<00:03,  1.32it/s]    
    +
    +
    +
    Epoch 1, Loss: 40.2073
    +Epoch 1, Validation Loss: 40.6210, Metrics: {'rmse': 51.9431875404865, 'cmapss_score': 1331357.9658768093}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 350.68it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 348.69it/s]
    +Steps ...:  47%|████▋     | 108/232 [00:00<00:00, 349.60it/s]
    +Steps ...:  62%|██████▎   | 145/232 [00:00<00:00, 354.45it/s]
    +Steps ...:  78%|███████▊  | 181/232 [00:00<00:00, 353.68it/s]
    +Steps ...:  94%|█████████▎| 217/232 [00:00<00:00, 352.66it/s]
    +Epochs ...:  40%|████      | 2/5 [00:01<00:02,  1.32it/s]    
    +
    +
    +
    Epoch 2, Loss: 37.4625
    +Epoch 2, Validation Loss: 34.5596, Metrics: {'rmse': 43.41147259207038, 'cmapss_score': 421283.66798380984}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 358.52it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 356.73it/s]
    +Steps ...:  47%|████▋     | 109/232 [00:00<00:00, 358.43it/s]
    +Steps ...:  62%|██████▎   | 145/232 [00:00<00:00, 355.35it/s]
    +Steps ...:  78%|███████▊  | 182/232 [00:00<00:00, 358.45it/s]
    +Steps ...:  94%|█████████▍| 218/232 [00:00<00:00, 356.98it/s]
    +Epochs ...:  60%|██████    | 3/5 [00:02<00:01,  1.33it/s]    
    +
    +
    +
    Epoch 3, Loss: 23.8398
    +Epoch 3, Validation Loss: 30.9023, Metrics: {'rmse': 39.31126980464057, 'cmapss_score': 286766.446915035}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  16%|█▌        | 36/232 [00:00<00:00, 357.63it/s]
    +Steps ...:  31%|███       | 72/232 [00:00<00:00, 357.63it/s]
    +Steps ...:  47%|████▋     | 109/232 [00:00<00:00, 358.92it/s]
    +Steps ...:  62%|██████▎   | 145/232 [00:00<00:00, 358.41it/s]
    +Steps ...:  78%|███████▊  | 182/232 [00:00<00:00, 359.14it/s]
    +Steps ...:  94%|█████████▍| 218/232 [00:00<00:00, 359.17it/s]
    +Epochs ...:  80%|████████  | 4/5 [00:02<00:00,  1.34it/s]    
    +
    +
    +
    Epoch 4, Loss: 28.2913
    +Epoch 4, Validation Loss: 29.3355, Metrics: {'rmse': 37.68203820452062, 'cmapss_score': 236872.37425031906}
    +
    +
    +
    Steps ...:   0%|          | 0/232 [00:00<?, ?it/s]
    +Steps ...:  15%|█▌        | 35/232 [00:00<00:00, 347.69it/s]
    +Steps ...:  30%|███       | 70/232 [00:00<00:00, 347.69it/s]
    +Steps ...:  45%|████▌     | 105/232 [00:00<00:00, 345.31it/s]
    +Steps ...:  60%|██████    | 140/232 [00:00<00:00, 345.55it/s]
    +Steps ...:  76%|███████▌  | 176/232 [00:00<00:00, 348.00it/s]
    +Steps ...:  91%|█████████▏| 212/232 [00:00<00:00, 351.78it/s]
    +Epochs ...: 100%|██████████| 5/5 [00:03<00:00,  1.33it/s]    
    +[I 2024-08-13 09:53:54,617] Trial 2 finished with value: 36.46475764089671 and parameters: {'hidden_dim': 879}. Best is trial 2 with value: 36.46475764089671.
    +
    +
    +
    Epoch 5, Loss: 31.4287
    +Epoch 5, Validation Loss: 28.2309, Metrics: {'rmse': 36.46475764089671, 'cmapss_score': 203393.37691007284}
    +Best hyperparameters: {'hidden_dim': 879}
    +Best trial: FrozenTrial(number=2, state=1, values=[36.46475764089671], datetime_start=datetime.datetime(2024, 8, 13, 9, 53, 50, 842965), datetime_complete=datetime.datetime(2024, 8, 13, 9, 53, 54, 617905), params={'hidden_dim': 879}, user_attrs={}, system_attrs={}, intermediate_values={1: 40.62102061946218, 2: 34.55964553646925, 3: 30.902342726544635, 4: 29.335512719503264, 5: 28.230895298283276}, distributions={'hidden_dim': IntDistribution(high=1024, log=False, low=256, step=1)}, trial_id=2, value=None)
    +
    +
    +
    +
    +

    The best results are printed at the end of optimization processand saved in the outputs/task_name/traininig/param_name_optimization folder

    +
    + + +
    + + + + + + + +
    + + + +
    + + +
    +
    + +
    + +
    +
    +
    + + + + + +
    + + +
    + + \ No newline at end of file diff --git a/docs/benchmark/ad_benchmark_autoencodermlp_256.html b/docs/benchmark/ad_benchmark_autoencodermlp_256.html index fe0527a..3ec2291 100644 --- a/docs/benchmark/ad_benchmark_autoencodermlp_256.html +++ b/docs/benchmark/ad_benchmark_autoencodermlp_256.html @@ -50,7 +50,7 @@ - + @@ -477,12 +477,12 @@

    Results of anomaly detection using AutoEncoderMLP-256 @@ -396,8 +393,79 @@
    -
    -

    Datasets#

    +
    +

    Results of anomaly detection using GSL-GNN#

    +
    +
    +
    from ice.anomaly_detection.datasets import AnomalyDetectionRiethTEP
    +from ice.anomaly_detection.models import GSL_GNN
    +from sklearn.preprocessing import StandardScaler
    +import numpy as np
    +import pandas as pd
    +
    +
    +
    +
    +

    Download the dataset.

    +
    +
    +
    dataset = AnomalyDetectionRiethTEP()
    +
    +
    +
    +
    +
    +
    +

    Normalize the data.

    +
    +
    +
    scaler = StandardScaler()
    +dataset.df[dataset.train_mask] = scaler.fit_transform(dataset.df[dataset.train_mask])
    +dataset.df[dataset.test_mask] = scaler.transform(dataset.df[dataset.test_mask])
    +
    +
    +
    +
    +

    Create the GNN model.

    +
    +
    +
    model = GSL_GNN(
    +    window_size=32, 
    +    num_epochs=30, 
    +    device='cuda',
    +    verbose=True,
    +    val_ratio=0.1,
    +    save_checkpoints=True,
    +    threshold_level=0.98
    +    )
    +
    +
    +
    +
    +

    Load the checkpoint.

    +
    +
    +
    model.load_checkpoint('gnn_anomaly_detection_epoch_30.tar')
    +
    +
    +
    +
    +

    Evaluate the model on the test data.

    +
    +
    +
    metrics = model.evaluate(dataset.df[dataset.test_mask], dataset.target[dataset.test_mask])
    +metrics
    +
    +
    +
    +
    +
    {'accuracy': 0.8517518472906404,
    + 'true_positive_rate': [0.82365725],
    + 'false_positive_rate': [0.019373853211009175]}
    +
    +
    +
    +
    @@ -411,20 +479,20 @@

    previous

    -

    Installation

    +

    Results of anomaly detection using STGAT-MAD

    next

    -

    Tasks

    +

    Results of RUL estimation using lstm-256

    @@ -439,7 +507,7 @@

@@ -367,32 +393,78 @@
-
-

ice#

-
- +
+

Results of anomaly detection using STGAT-MAD#

+
+
+
from ice.anomaly_detection.datasets import AnomalyDetectionRiethTEP
+from ice.anomaly_detection.models import STGAT_MAD
+from sklearn.preprocessing import StandardScaler
+import numpy as np
+import pandas as pd
+
+
+
+
+

Download the dataset.

+
+
+
dataset = AnomalyDetectionRiethTEP()
+
+
+
+
+
+
+

Normalize the data.

+
+
+
scaler = StandardScaler()
+dataset.df[dataset.train_mask] = scaler.fit_transform(dataset.df[dataset.train_mask])
+dataset.df[dataset.test_mask] = scaler.transform(dataset.df[dataset.test_mask])
+
+
+
+
+

Create the GNN model.

+
+
+
model = STGAT_MAD(
+    window_size=32, 
+    num_epochs=30, 
+    device='cuda',
+    verbose=True,
+    val_ratio=0.1,
+    save_checkpoints=True,
+    threshold_level=0.98
+    )
+
+
+
+
+

Load the checkpoint.

+
+
+
model.load_checkpoint('stgat_anomaly_detection_epoch_30.tar')
+
+
+
+
+

Evaluate the model on the test data.

+
+
+
metrics = model.evaluate(dataset.df[dataset.test_mask], dataset.target[dataset.test_mask])
+metrics
+
+
+
+
+
{'accuracy': 0.861279659277504,
+ 'true_positive_rate': [0.8352755],
+ 'false_positive_rate': [0.019435206422018347]}
+
+
+
@@ -406,6 +478,24 @@

ice# @@ -417,7 +507,7 @@

ice# @@ -367,7 +376,7 @@ - +

@@ -384,79 +393,80 @@
-
-

Results of anomaly detection using AutoEncoderMLP-256#

-

This notebook presents experimental results of anomaly detection on the Tennessee Eastman Process dataset using the model AutoEncoderMLP-256.

-

Importing libraries.

+
+

Results of anomaly detection using AnomalyTransformer#

-
import numpy as np
-import torch
+
from ice.anomaly_detection.datasets import AnomalyDetectionRiethTEP
+from ice.anomaly_detection.models import AnomalyTransformer
 from sklearn.preprocessing import StandardScaler
-from tqdm.auto import trange
-from ice.anomaly_detection.datasets import AnomalyDetectionReinartzTEP, AnomalyDetectionSmallTEP
-from ice.anomaly_detection.models import AutoEncoderMLP
+import numpy as np
+import pandas as pd
 
-

Downloading the TEP dataset.

+

Download the dataset.

-
dataset = AnomalyDetectionReinartzTEP()
+
dataset = AnomalyDetectionRiethTEP()
 
-
+
-

Training the model and calculation of metrics.

+

Normalize the data.

-
metrics = []
-for i in trange(5):
-    torch.random.manual_seed(i)
-    model = AutoEncoderMLP(
-        window_size=32,
-        batch_size=512,
-        lr=0.001,
-        num_epochs=20,
-        verbose=False,
-        device='cuda'
+
scaler = StandardScaler()
+dataset.df[dataset.train_mask] = scaler.fit_transform(dataset.df[dataset.train_mask])
+dataset.df[dataset.test_mask] = scaler.transform(dataset.df[dataset.test_mask])
+
+
+
+
+

Create the AnomalyTransformer model.

+
+
+
model = AnomalyTransformer(
+    window_size=32, 
+    lr=0.001, 
+    num_epochs=30, 
+    device='cuda', 
+    verbose=True, 
+    val_ratio=0.1,
+    save_checkpoints=True,
+    threshold_level=0.98,
+    d_model=32, 
+    e_layers=1,
+    d_ff=32, 
+    dropout=0.0
     )
-    model.fit(
-        dataset.df[dataset.train_mask])
-    metrics.append(
-        model.evaluate(
-            dataset.df[dataset.test_mask], dataset.target[dataset.test_mask]))
 
-
-
-

Printing metrics.

+

Load the checkpoint.

+
+
+
model.load_checkpoint('transformer_anomaly_detection_epoch_30.tar')
+
+
+
+
+

Evaluate the model on the test data.

-
acc = []
-tpr = []
-fpr = []
-for metrics_i in metrics:
-    acc.append(metrics_i["accuracy"])
-    tpr.append(metrics_i["true_positive_rate"])
-    fpr.append(metrics_i["false_positive_rate"])
-tpr_mean, tpr_std = np.mean(tpr, axis=0), np.std(tpr, axis=0)
-fpr_mean, fpr_std = np.mean(fpr, axis=0), np.std(fpr, axis=0)
-
-print(f'Accuracy: {np.mean(acc):.4f} ± {2*np.std(acc):.4f}')
-for i in range(len(tpr_mean)):
-    print(f'TPR/FPR: {tpr_mean[i]:.4f} ± {2*tpr_std[i]:.4f} / {fpr_mean[i]:.4f} ± {2*fpr_std[i]:.4f}')
+
metrics = model.evaluate(dataset.df[dataset.test_mask], dataset.target[dataset.test_mask])
+metrics
 
-
Accuracy: 0.8028 ± 0.0007
-TPR/FPR: 0.7378 ± 0.0022 / 0.0366 ± 0.0034
+
{'accuracy': 0.8588669950738916,
+ 'true_positive_rate': [0.830817625],
+ 'false_positive_rate': [0.012466169724770642]}
 
@@ -474,13 +484,22 @@

Results of anomaly detection using AutoEncoderMLP-256 @@ -493,7 +512,7 @@

Results of anomaly detection using AutoEncoderMLP-256 diff --git a/docs/benchmark/fd_benchmark_mlp_256.html b/docs/benchmark/fd_benchmark_mlp_256.html index 0fe9d50..5df2dd1 100644 --- a/docs/benchmark/fd_benchmark_mlp_256.html +++ b/docs/benchmark/fd_benchmark_mlp_256.html @@ -49,7 +49,7 @@ - + @@ -528,11 +528,11 @@

Results of fault diagnosis using MLP-256

next

-

Results of anomaly detection using AutoEncoderMLP-256

+

<no title>

diff --git a/docs/benchmark/fd_benchmark_tcn.html b/docs/benchmark/fd_benchmark_tcn.html new file mode 100644 index 0000000..0c2cc60 --- /dev/null +++ b/docs/benchmark/fd_benchmark_tcn.html @@ -0,0 +1,753 @@ + + + + + + + + + + + + Results of fault diagnosis using TCN — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +
+

Results of fault diagnosis using TCN#

+
+
+
from ice.fault_diagnosis.datasets import FaultDiagnosisRiethTEP
+from ice.fault_diagnosis.models import TCN
+from sklearn.preprocessing import StandardScaler
+import numpy as np
+import pandas as pd
+
+
+
+
+

Download the dataset.

+
+
+
dataset = FaultDiagnosisRiethTEP()
+
+
+
+
+
+
+

Normalize the data.

+
+
+
scaler = StandardScaler()
+dataset.df[dataset.train_mask] = scaler.fit_transform(dataset.df[dataset.train_mask])
+dataset.df[dataset.test_mask] = scaler.transform(dataset.df[dataset.test_mask])
+
+
+
+
+

Create the TCN model.

+
+
+
model = TCN(
+    window_size=60,
+    batch_size=128,
+    num_layers=1,
+    kernel_size=3,
+    hidden_dim=32,
+    lr=1e-4,
+    num_epochs=30,
+    verbose=True,
+    device='cpu',
+    save_checkpoints=True,
+    val_ratio=0.1,
+)
+
+
+
+
+

Load the checkpoint.

+
+
+
model.load_checkpoint('tcn_fault_diagnosis_epoch_30.tar')
+
+
+
+
+

Evaluate the model on the test data.

+
+
+
metrics = model.evaluate(
+    dataset.df[dataset.test_mask],
+    dataset.target[dataset.test_mask]
+)
+
+
+
+
+
+
+
+
+
idx = np.array([1, 2, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]) - 1
+pd.DataFrame({
+    'Fault': idx,
+    'TPR': np.array(metrics['true_positive_rate'])[idx],
+    'FPR': np.array(metrics['false_positive_rate'])[idx],
+}).round(4)
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaultTPRFPR
000.96750.0000
110.97380.0000
230.96430.0000
340.95840.0000
450.97310.0000
560.96790.0000
670.96910.0000
790.96510.0000
8100.97880.0000
9110.95260.0000
10120.94180.0001
11130.97800.0000
12150.97520.0000
13160.96080.0000
14170.93570.0000
15180.97170.0000
16190.94820.0000
+
+
+
+
+
print(f'Average TPR: {np.array(metrics["true_positive_rate"])[idx].mean():.2f}')
+
+
+
+
+
Average TPR: 0.96
+
+
+
+
+
+
+
for i in np.array(metrics["true_positive_rate"])[idx]*100:
+    print(f'{i:.2f}')
+
+
+
+
+
96.75
+97.38
+96.43
+95.84
+97.31
+96.79
+96.91
+96.51
+97.88
+95.26
+94.18
+97.80
+97.52
+96.08
+93.57
+97.17
+94.82
+
+
+
+
+
+ + +
+ + + + + + + +
+ + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/benchmark/hi_sota.html b/docs/benchmark/hi_sota.html new file mode 100644 index 0000000..1c9ad6f --- /dev/null +++ b/docs/benchmark/hi_sota.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + Results of HI estimation using Stacked LSTM — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +
+

Results of HI estimation using Stacked LSTM#

+

This notebook presents experimental results of hi estimation on the Milling dataset using the model MLP-256.

+

Importing libraries.

+
+
+
from ice.health_index_estimation.datasets import Milling
+from ice.health_index_estimation.models import MLP, TCM, IE_SBiGRU, Stacked_LSTM
+
+import pandas as pd
+import numpy as np
+import torch
+from tqdm.auto import trange
+    
+
+
+
+
+
C:\Users\user\conda\envs\ice_testing\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
+  from .autonotebook import tqdm as notebook_tqdm
+
+
+
+
+

Initializing model class and train/test data split

+
+
+
dataset_class = Milling()
+
+data, target = pd.concat(dataset_class.df), pd.concat(dataset_class.target) 
+test_data, test_target = dataset_class.test[0], dataset_class.test_target[0]
+
+
+
+
+
Reading data/milling/case_1.csv: 100%|██████████| 153000/153000 [00:00<00:00, 1268689.48it/s]
+Reading data/milling/case_2.csv: 100%|██████████| 117000/117000 [00:00<00:00, 1248873.41it/s]
+Reading data/milling/case_3.csv: 100%|██████████| 126000/126000 [00:00<00:00, 1276983.81it/s]
+Reading data/milling/case_4.csv: 100%|██████████| 63000/63000 [00:00<00:00, 1374151.83it/s]
+Reading data/milling/case_5.csv: 100%|██████████| 54000/54000 [00:00<00:00, 1290003.79it/s]
+Reading data/milling/case_6.csv: 100%|██████████| 9000/9000 [00:00<00:00, 1003395.34it/s]
+Reading data/milling/case_7.csv: 100%|██████████| 72000/72000 [00:00<00:00, 1245529.71it/s]
+Reading data/milling/case_8.csv: 100%|██████████| 54000/54000 [00:00<00:00, 1321487.68it/s]
+Reading data/milling/case_9.csv: 100%|██████████| 81000/81000 [00:00<00:00, 1377467.66it/s]
+Reading data/milling/case_10.csv: 100%|██████████| 90000/90000 [00:00<00:00, 1347769.63it/s]
+Reading data/milling/case_11.csv: 100%|██████████| 207000/207000 [00:00<00:00, 1180064.87it/s]
+Reading data/milling/case_12.csv: 100%|██████████| 126000/126000 [00:00<00:00, 1276980.73it/s]
+Reading data/milling/case_13.csv: 100%|██████████| 135000/135000 [00:00<00:00, 1242669.46it/s]
+Reading data/milling/case_14.csv: 100%|██████████| 81000/81000 [00:00<00:00, 1310826.20it/s]
+Reading data/milling/case_15.csv: 100%|██████████| 63000/63000 [00:00<00:00, 1316886.37it/s]
+Reading data/milling/case_16.csv: 100%|██████████| 18000/18000 [00:00<00:00, 1128731.62it/s]
+C:\Users\user\conda\envs\ice_testing\Lib\site-packages\scipy\interpolate\_interpolate.py:479: RuntimeWarning: invalid value encountered in divide
+  slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None]
+
+
+
+
+
+
+
from sklearn.preprocessing import StandardScaler, MinMaxScaler
+import pandas as pd 
+
+scaler = MinMaxScaler()
+trainer_data = scaler.fit_transform(data)
+tester_data = scaler.transform(test_data)
+
+trainer_data = pd.DataFrame(trainer_data, index=data.index, columns=data.columns)
+tester_data = pd.DataFrame(tester_data, index=test_data.index, columns=test_data.columns)
+
+
+
+
+
+
+
# path_to_tar = "hi_sota/"
+
+
+
+
+
+
+
model_class = Stacked_LSTM(
+        window_size=64,
+        stride=1024, # 1024
+        batch_size=253, # 256
+        lr= 0.0031789041005068647, # 0.0004999805761074147,
+        num_epochs=55,
+        verbose=True,
+        device='cuda'
+    )
+# model_class.fit(trainer_data, target)
+model_class.load_checkpoint(path_to_tar + "stack_sota.tar")
+
+
+
+
+
+
+
model_class.evaluate(tester_data, test_target)
+
+
+
+
+
Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 2809.31it/s]
+                                                             
+
+
+
{'mse': 0.0022332468596409335, 'rmse': 0.047257241346072384}
+
+
+
+
+
+
+
model_class = TCM(
+        window_size=64,
+        stride=1024, # 1024
+        batch_size=253, # 256
+        lr= 0.0031789041005068647, # 0.0004999805761074147,
+        num_epochs=55,
+        verbose=True,
+        device='cuda'
+    )
+# model_class.fit(trainer_data, target)
+model_class.load_checkpoint(path_to_tar + "TCM_sota.tar")
+
+
+
+
+
+
+
model_class.evaluate(tester_data, test_target)
+
+
+
+
+
Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 3511.98it/s]
+                                                             
+
+
+
{'mse': 0.004014168163365719, 'rmse': 0.06335746335962102}
+
+
+
+
+

Training and testing with difference random seed for uncertainty estimation

+
+
+
model_class = IE_SBiGRU(
+        window_size=64,
+        stride=1024, # 1024
+        batch_size=253, # 256
+        lr= 0.0011, # 0.0004999805761074147,
+        num_epochs=35,
+        verbose=True,
+        device='cuda'
+    )
+# model_class.fit(trainer_data, target)
+model_class.load_checkpoint(path_to_tar + "IE_SBiGRU_sota.tar")
+
+
+
+
+
+
+
model_class.evaluate(tester_data, test_target)
+
+
+
+
+
Creating sequence of samples: 100%|██████████| 14/14 [00:00<00:00, 2341.13it/s]
+                                                            
+
+
+
{'mse': 0.004956771691496658, 'rmse': 0.07040434426579555}
+
+
+
+
+
+ + +
+ + + + + + + +
+ + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/benchmark/index.html b/docs/benchmark/index.html index c25bc45..f47e1c3 100644 --- a/docs/benchmark/index.html +++ b/docs/benchmark/index.html @@ -327,9 +327,15 @@

@@ -390,9 +396,15 @@

Contents:

diff --git a/docs/benchmark/rul_sota.html b/docs/benchmark/rul_sota.html new file mode 100644 index 0000000..a26a94c --- /dev/null +++ b/docs/benchmark/rul_sota.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + Results of RUL estimation using IR — ICE documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +
+

Results of RUL estimation using IR#

+

This notebook presents experimental results of rul estimation on the CMAPSS dataset using the model lstm-256.

+

Importing libraries.

+
+
+
from ice.remaining_useful_life_estimation.datasets import RulCmapss
+from ice.remaining_useful_life_estimation.models import IR 
+
+import pandas as pd
+
+
+
+
+
C:\Users\user\conda\envs\ice_testing\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
+  from .autonotebook import tqdm as notebook_tqdm
+
+
+
+
+

Initializing model class and train/test data split for fd001 subdataset

+
+
+
dataset_class = RulCmapss()
+
+data, target = dataset_class.df[0], dataset_class.target[0]
+test_data, test_target = dataset_class.test[0], dataset_class.test_target[0]  
+
+
+
+
+
Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 504872.87it/s]
+Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 533656.45it/s]
+Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 496051.49it/s]
+Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 532245.75it/s]
+Reading data/C-MAPSS/fd1_train.csv: 100%|██████████| 20631/20631 [00:00<00:00, 547602.44it/s]
+Reading data/C-MAPSS/fd2_train.csv: 100%|██████████| 53759/53759 [00:00<00:00, 528810.42it/s]
+Reading data/C-MAPSS/fd3_train.csv: 100%|██████████| 24720/24720 [00:00<00:00, 496134.57it/s]
+Reading data/C-MAPSS/fd4_train.csv: 100%|██████████| 61249/61249 [00:00<00:00, 529770.68it/s]
+Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 486699.50it/s]
+Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 501537.62it/s]
+Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 512334.66it/s]
+Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 523435.48it/s]
+Reading data/C-MAPSS/fd1_test.csv: 100%|██████████| 13097/13097 [00:00<00:00, 474171.77it/s]
+Reading data/C-MAPSS/fd2_test.csv: 100%|██████████| 33991/33991 [00:00<00:00, 501537.62it/s]
+Reading data/C-MAPSS/fd3_test.csv: 100%|██████████| 16598/16598 [00:00<00:00, 520419.66it/s]
+Reading data/C-MAPSS/fd4_test.csv: 100%|██████████| 41214/41214 [00:00<00:00, 519167.37it/s]
+
+
+
+
+
+
+
from sklearn.preprocessing import StandardScaler, MinMaxScaler
+import pandas as pd 
+
+scaler = MinMaxScaler()
+trainer_data = scaler.fit_transform(data)
+tester_data = scaler.transform(test_data)
+
+trainer_data = pd.DataFrame(trainer_data, index=data.index, columns=data.columns)
+tester_data = pd.DataFrame(tester_data, index=test_data.index, columns=test_data.columns)
+
+
+
+
+

Training and testing with difference random seed for uncertainty estimation

+
+
+
model_class = IR()
+model_class.load_checkpoint("rul_sota.tar")
+
+
+
+
+
C:\Users\user\conda\envs\ice_testing\Lib\site-packages\torch\nn\modules\transformer.py:306: UserWarning: enable_nested_tensor is True, but self.use_nested_tensor is False because encoder_layer.activation_relu_or_gelu was not True
+  warnings.warn(f"enable_nested_tensor is True, but self.use_nested_tensor is False because {why_not_sparsity_fast_path}")
+
+
+
+
+
+
+
model_class.evaluate(tester_data, test_target)
+
+
+
+
+
Creating sequence of samples: 100%|██████████| 100/100 [00:00<00:00, 11148.24it/s]
+                                                          
+
+
+
{'rmse': 11.99217470692219, 'cmapss_score': 25394.12755711561}
+
+
+
+
+
+ + +
+ + + + + + + +
+ + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html index 3002e7b..4964942 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -345,9 +345,12 @@

Index

| C | E | F + | G | I + | L | M | N + | O | P | R | S @@ -363,11 +366,15 @@

A

  • (in module ice.fault_diagnosis.metrics)
  • +
  • AnomalyDetectionReinartzTEP (class in ice.anomaly_detection.datasets) +
  • +
  • AnomalyDetectionRiethTEP +
  • AnomalyDetectionSmallTEP diff --git a/docs/reference/ice.anomaly_detection.html b/docs/reference/ice.anomaly_detection.html index 608e9bf..fea602c 100644 --- a/docs/reference/ice.anomaly_detection.html +++ b/docs/reference/ice.anomaly_detection.html @@ -418,16 +418,22 @@

    Anomaly detectionModels

  • Datasets
  • Metrics
  • Utils
      diff --git a/docs/reference/ice.anomaly_detection.metrics.html b/docs/reference/ice.anomaly_detection.metrics.html index b7d3359..cc3036b 100644 --- a/docs/reference/ice.anomaly_detection.metrics.html +++ b/docs/reference/ice.anomaly_detection.metrics.html @@ -419,18 +419,18 @@

      Anomaly detection metrics#

      -ice.anomaly_detection.metrics.accuracy(pred: list, target: list) float[source]#
      +ice.anomaly_detection.metrics.accuracy(pred: ndarray, target: ndarray) float[source]#

      Accuracy of the classification is the number of true positives divided by the number of examples.

      Parameters:
        -
      • pred (list) – predictions.

      • -
      • target (list) – target values.

      • +
      • pred (np.ndarray) – predictions.

      • +
      • target (np.ndarray) – target values.

      Returns:
      -

      accuracy

      +

      accuracy.

      Return type:

      float

      @@ -438,6 +438,48 @@
      +
      +
      +ice.anomaly_detection.metrics.false_positive_rate(pred: ndarray, target: ndarray) ndarray[float][source]#
      +

      False Positive Rate, aka False Alarm Rate is the number of false alarms i +divided by the number of normal samples.

      +
      +
      Parameters:
      +
        +
      • pred (np.ndarray) – predictions.

      • +
      • target (np.ndarray) – target values.

      • +
      +
      +
      Returns:
      +

      list of float values with true positive rate for each fault.

      +
      +
      Return type:
      +

      list

      +
      +
      +
      + +
      +
      +ice.anomaly_detection.metrics.true_positive_rate(pred: ndarray, target: ndarray) ndarray[float][source]#
      +

      True Positive Rate is the number of detected faults i divided by the +number of faults i.

      +
      +
      Parameters:
      +
        +
      • pred (np.ndarray) – predictions.

      • +
      • target (np.ndarray) – target values.

      • +
      +
      +
      Returns:
      +

      list of float values with true positive rate for each fault.

      +
      +
      Return type:
      +

      list

      +
      +
      +
      +
  • @@ -484,6 +526,8 @@ diff --git a/docs/reference/ice.anomaly_detection.models.html b/docs/reference/ice.anomaly_detection.models.html index 2142208..f35abb4 100644 --- a/docs/reference/ice.anomaly_detection.models.html +++ b/docs/reference/ice.anomaly_detection.models.html @@ -421,13 +421,15 @@

    Anomaly detection models

    BaseAnomalyDetection#

    -class ice.anomaly_detection.models.base.BaseAnomalyDetection(window_size: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, threshold: float = 0.95)[source]#
    +class ice.anomaly_detection.models.base.BaseAnomalyDetection(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, random_seed: int, val_ratio: float, save_checkpoints: bool, threshold_level: float = 0.95)[source]#

    Bases: BaseModel, ABC

    Base class for all anomaly detection models.

    Parameters:
    -
    -fit(df: DataFrame)[source]#
    -

    Method fit for training anomaly detection models.

    +
    +load_checkpoint(checkpoint_path: str)[source]#
    +

    Load checkpoint.

    Parameters:
    -

    df (pd.DataFrame) – data without anomaly states

    +

    checkpoint_path (str) – Path to load checkpoint.

    @@ -479,13 +482,13 @@

    Anomaly detection models -

    AutoEncoderMLP#

    +
    +

    AutoEncoderMLP#

    -class ice.anomaly_detection.models.autoencoder.AutoEncoderMLP(window_size: int, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'ae_anomaly_detection', threshold: float = 0.95, hidden_dims: list = [256, 128, 64])[source]#
    +class ice.anomaly_detection.models.autoencoder.AutoEncoderMLP(window_size: int, stride: int = 1, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'ae_anomaly_detection', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False, threshold_level: float = 0.95, hidden_dims: list = [256, 128, 64])[source]#

    Bases: BaseAnomalyDetection

    -

    Autoencoder (AE) consists of encoder and decoder parts. Each +

    MLP autoencoder consists of MLP encoder and MLP decoder parts. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) for calculations and to a vector (B, L * C) -> (B, L, C) for the output. Where B is the batch size, L is the sequence length, C is the number of sensors.

    @@ -493,6 +496,8 @@

    Anomaly detection modelsParameters:
    • window_size (int) – The window size to train the model.

    • +
    • stride (int) – The time interval between first points of consecutive +sliding windows in training.

    • batch_size (int) – The batch size to train the model.

    • lr (float) – The larning rate to train the model.

    • num_epochs (float) – The number of epochs to train the model.

    • @@ -500,37 +505,135 @@

      Anomaly detection models +

      AnomalyTransformer#

      -
      -class ice.anomaly_detection.models.autoencoder.MLP(num_sensors: int, window_size: int, num_layers: int, hidden_dims: list, type: str)[source]#
      -

      Bases: Module

      -

      Initializes internal Module state, shared by both nn.Module and ScriptModule.

      -
      -
      -forward(x)[source]#
      -

      Defines the computation performed at every call.

      -

      Should be overridden by all subclasses.

      -
      -

      Note

      -

      Although the recipe for forward pass needs to be defined within -this function, one should call the Module instance afterwards -instead of this since the former takes care of running the -registered hooks while the latter silently ignores them.

      -
      +
      +class ice.anomaly_detection.models.transformer.AnomalyTransformer(window_size: int = 100, stride: int = 1, batch_size: int = 128, lr: float = 0.0001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'transformer_anomaly_detection', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False, threshold_level: float = 0.95, d_model: int = 256, n_heads: int = 8, e_layers: int = 3, d_ff: int = 256, dropout: float = 0.0, activation: str = 'gelu')[source]#
      +

      Bases: BaseAnomalyDetection

      +

      Anomaly Transformer was presented at ICLR 2022: “Anomaly Transformer: +Time Series Anomaly Detection with Association Discrepancy”. +https://openreview.net/forum?id=LzQQ89U1qm_

      +
      +
      Parameters:
      +
        +
      • window_size (int) – The window size to train the model.

      • +
      • stride (int) – The time interval between first points of consecutive +sliding windows in training.

      • +
      • batch_size (int) – The batch size to train the model.

      • +
      • lr (float) – The larning rate to train the model.

      • +
      • num_epochs (float) – The number of epochs to train the model.

      • +
      • device (str) – The name of a device to train the model. cpu and +cuda are possible.

      • +
      • verbose (bool) – If true, show the progress bar in training.

      • +
      • name (str) – The name of the model for artifact storing.

      • +
      • random_seed (int) – Seed for random number generation to ensure reproducible results.

      • +
      • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

      • +
      • save_checkpoints (bool) – If true, store checkpoints.

      • +
      • threshold_level (float) – Takes a value from 0 to 1. It specifies +the quantile in the distribution of errors on the training +dataset at which the threshold value is set.

      • +
      • threshold_value (float) – Threshold value is calculated after the model is trained. +It sets the error limit above which the data sample defines as anomaly.

      • +
      • d_model (int) – Dimension of model.

      • +
      • n_heads (int) – Number of heads.

      • +
      • e_layers (int) – Number of encoder layers.

      • +
      • d_ff (int) – Dimension of MLP.

      • +
      • dropout (float) – The rate of dropout.

      • +
      • activation (str) – Activation (‘relu’, ‘gelu’).

      • +
      +
      +
      -
      -
      -training: bool#
      -
      +

    +
    +

    STGAT-MAD#

    +
    +
    +class ice.anomaly_detection.models.stgat.STGAT_MAD(window_size: int, stride: int = 1, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'stgat_anomaly_detection', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False, threshold_level: float = 0.95, embed_dim: Optional[int] = None, layer_numb: int = 2, lstm_n_layers: int = 1, lstm_hid_dim: int = 150, recon_n_layers: int = 1, recon_hid_dim: int = 150, dropout: float = 0.2)[source]#
    +

    Bases: BaseAnomalyDetection

    +

    Stgat-Mad was presented at ICASSP 2022: “Stgat-Mad : Spatial-Temporal Graph +Attention Network For Multivariate Time Series Anomaly Detection”. +https://ieeexplore.ieee.org/abstract/document/9747274/

    +
    +
    Parameters:
    +
      +
    • window_size (int) – The window size to train the model.

    • +
    • stride (int) – The time interval between first points of consecutive +sliding windows in training.

    • +
    • batch_size (int) – The batch size to train the model.

    • +
    • lr (float) – The larning rate to train the model.

    • +
    • num_epochs (float) – The number of epochs to train the model.

    • +
    • device (str) – The name of a device to train the model. cpu and +cuda are possible.

    • +
    • verbose (bool) – If true, show the progress bar in training.

    • +
    • name (str) – The name of the model for artifact storing.

    • +
    • random_seed (int) – Seed for random number generation to ensure reproducible results.

    • +
    • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

    • +
    • save_checkpoints (bool) – If true, store checkpoints.

    • +
    • threshold_level (float) – Takes a value from 0 to 1. It specifies +the quantile in the distribution of errors on the training +dataset at which the threshold value is set.

    • +
    • embed_dim (int) – Embedding dimension.

    • +
    • layer_numb (int) – Number of layers.

    • +
    • lstm_n_layers (int) – Number of LSTM layers.

    • +
    • lstm_hid_dim (int) – Hidden dimension of LSTM layers.

    • +
    • recon_n_layers (int) – Number of reconstruction layers.

    • +
    • recon_hid_dim (int) – Hidden dimension of reconstruction layers.

    • +
    • dropout (float) – The rate of dropout.

    • +
    +
    +
    +
    +
    +
    +

    GSL-GNN#

    +
    +
    +class ice.anomaly_detection.models.gnn.GSL_GNN(window_size: int, stride: int = 1, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'gnn_anomaly_detection', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False, threshold_level: float = 0.95, alpha: float = 0.2, k: Optional[int] = None)[source]#
    +

    Bases: BaseAnomalyDetection

    +

    GNN autoencoder consists of encoder with graph convolutional layers +and MLP decoder parts. The graph describing the data is constructed +during the training process using trainable parameters.

    +
    +
    Parameters:
    +
      +
    • window_size (int) – The window size to train the model.

    • +
    • stride (int) – The time interval between first points of consecutive +sliding windows in training.

    • +
    • batch_size (int) – The batch size to train the model.

    • +
    • lr (float) – The larning rate to train the model.

    • +
    • num_epochs (float) – The number of epochs to train the model.

    • +
    • device (str) – The name of a device to train the model. cpu and +cuda are possible.

    • +
    • verbose (bool) – If true, show the progress bar in training.

    • +
    • name (str) – The name of the model for artifact storing.

    • +
    • random_seed (int) – Seed for random number generation to ensure reproducible results.

    • +
    • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

    • +
    • save_checkpoints (bool) – If true, store checkpoints.

    • +
    • threshold_level (float) – Takes a value from 0 to 1. It specifies +the quantile in the distribution of errors on the training +dataset at which the threshold value is set.

    • +
    • alpha (float) – Saturation rate for adjacency matrix.

    • +
    • k (int) – Limit on the number of edges in the adjacency matrix.

    • +
    +
    +
    @@ -581,19 +684,26 @@

    Anomaly detection models
  • BaseAnomalyDetection
  • -
  • AutoEncoderMLP
  • +
    +
    +save_checkpoint(save_path: Optional[str] = None)[source]#
    +

    Save checkpoint.

    +
    +
    Parameters:
    +

    save_path (str) – Path to save checkpoint.

    +
    +
    +
    + + + +
    +
    +class ice.base.SlidingWindowDataset(df: DataFrame, target: Series, window_size: int, stride: int = 1)[source]#
    +

    Bases: Dataset

    @@ -591,9 +668,14 @@
  • BaseModel.evaluate()
  • BaseModel.fit()
  • BaseModel.from_config()
  • +
  • BaseModel.load_checkpoint()
  • +
  • BaseModel.model_param_estimation()
  • +
  • BaseModel.optimize()
  • BaseModel.predict()
  • +
  • BaseModel.save_checkpoint()
  • +
  • SlidingWindowDataset
  • diff --git a/docs/reference/ice.fault_diagnosis.datasets.html b/docs/reference/ice.fault_diagnosis.datasets.html index 97bb8eb..98aa1d8 100644 --- a/docs/reference/ice.fault_diagnosis.datasets.html +++ b/docs/reference/ice.fault_diagnosis.datasets.html @@ -443,6 +443,33 @@ +
    +
    +class ice.fault_diagnosis.datasets.FaultDiagnosisRiethTEP(num_chunks=None, force_download=False)[source]#
    +

    Bases: BaseDataset

    +

    Dataset of Tennessee Eastman Process dataset +Rieth, C. A., Amsel, B. D., Tran, R., & Cook, M. B. (2017). +Additional Tennessee Eastman Process Simulation Data for +Anomaly Detection Evaluation (Version V1) [Computer software]. +Harvard Dataverse. +https://doi.org/10.7910/DVN/6C3JR1.

    +
    +
    Parameters:
    +
      +
    • num_chunks (int) – If given, download only num_chunks chunks of data. +Used for testing purposes.

    • +
    • force_download (bool) – If True, download the dataset even if it exists.

    • +
    +
    +
    +
    + +

    This method has to be implemented by all children. Set name and public link.

    +
    + +
    +
    class ice.fault_diagnosis.datasets.FaultDiagnosisSmallTEP(num_chunks=None, force_download=False)[source]#
    @@ -519,6 +546,10 @@
  • FaultDiagnosisReinartzTEP.set_name_public_link()
  • +
  • FaultDiagnosisRiethTEP +
  • FaultDiagnosisSmallTEP diff --git a/docs/reference/ice.fault_diagnosis.html b/docs/reference/ice.fault_diagnosis.html index 2930859..dd93d63 100644 --- a/docs/reference/ice.fault_diagnosis.html +++ b/docs/reference/ice.fault_diagnosis.html @@ -424,6 +424,7 @@

    Fault diagnosisDatasets

  • diff --git a/docs/reference/ice.fault_diagnosis.models.html b/docs/reference/ice.fault_diagnosis.models.html index 3d82033..08d1893 100644 --- a/docs/reference/ice.fault_diagnosis.models.html +++ b/docs/reference/ice.fault_diagnosis.models.html @@ -421,50 +421,28 @@

    Fault diagnosis models

    BaseFaultDiagnosis#

    -class ice.fault_diagnosis.models.base.BaseFaultDiagnosis(window_size: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str)[source]#
    +class ice.fault_diagnosis.models.base.BaseFaultDiagnosis(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, random_seed: int, val_ratio: float, save_checkpoints: bool)[source]#

    Bases: BaseModel, ABC

    Base class for all fault diagnosis models.

    Parameters:
    • window_size (int) – The window size to train the model.

    • +
    • stride (int) – The time interval between first points of consecutive +sliding windows in training.

    • batch_size (int) – The batch size to train the model.

    • -
    • lr (float) – The learning rate to train the model.

    • +
    • lr (float) – The larning rate to train the model.

    • num_epochs (float) – The number of epochs to train the model.

    • device (str) – The name of a device to train the model. cpu and cuda are possible.

    • verbose (bool) – If true, show the progress bar in training.

    • name (str) – The name of the model for artifact storing.

    • +
    • random_seed (int) – Seed for random number generation to ensure reproducible results.

    • +
    • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

    • +
    • save_checkpoints (bool) – If true, store checkpoints.

    -
    -
    -evaluate(df: DataFrame, target: Series) dict[source]#
    -

    Evaluate the metrics: accuracy.

    -
    -
    Parameters:
    -
      -
    • df (pandas.DataFrame) – A dataframe with sensor data. Index has -two columns: run_id and sample. All other columns a value of -sensors.

    • -
    • target (pandas.Series) – A series with target values. Indes has two -columns: run_id and sample.

    • -
    -
    -
    Returns:
    -

    -
    A dictionary with metrics where keys are names of metrics and

    values are values of metrics.

    -
    -
    -

    -
    -
    Return type:
    -

    dict

    -
    -
    -
    -
    @@ -472,7 +450,7 @@

    Fault diagnosis models

    MLP#

    -class ice.fault_diagnosis.models.mlp.MLP(window_size: int, hidden_dim: int = 256, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'mlp_fault_diagnosis')[source]#
    +class ice.fault_diagnosis.models.mlp.MLP(window_size: int, stride: int = 1, hidden_dim: int = 256, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'mlp_fault_diagnosis', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False)[source]#

    Bases: BaseFaultDiagnosis

    Multilayer Perceptron (MLP) consists of input, hidden, output layers and ReLU activation. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) @@ -490,6 +468,9 @@

    Fault diagnosis modelscuda are possible.

  • verbose (bool) – If true, show the progress bar in training.

  • name (str) – The name of the model for artifact storing.

  • +
  • random_seed (int) – Seed for random number generation to ensure reproducible results.

  • +
  • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

  • +
  • save_checkpoints (bool) – If true, store checkpoints.

  • @@ -500,7 +481,7 @@

    Fault diagnosis models

    TCN#

    -class ice.fault_diagnosis.models.tcn.TCN(window_size: int, hidden_dim: int = 256, kernel_size: int = 5, num_layers: int = 4, dilation_base: int = 2, dropout: float = 0.2, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'tcn_fault_diagnosis')[source]#
    +class ice.fault_diagnosis.models.tcn.TCN(window_size: int, stride: int = 1, hidden_dim: int = 256, kernel_size: int = 5, num_layers: int = 4, dilation_base: int = 2, dropout: float = 0.2, batch_size: int = 128, lr: float = 0.001, num_epochs: int = 10, device: str = 'cpu', verbose: bool = False, name: str = 'tcn_fault_diagnosis', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False)[source]#

    Bases: BaseFaultDiagnosis

    Temporal Convolutional Network (TCN)-based Fault Diagnosis method. The implementation is based on the paper Lomov, Ildar, et al. “Fault detection @@ -522,6 +503,9 @@

    Fault diagnosis modelscuda are possible.

  • verbose (bool) – If true, show the progress bar in training.

  • name (str) – The name of the model for artifact storing.

  • +
  • random_seed (int) – Seed for random number generation to ensure reproducible results.

  • +
  • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

  • +
  • save_checkpoints (bool) – If true, store checkpoints.

  • @@ -574,10 +558,7 @@

    Fault diagnosis models

    +
    +
    +ice.health_index_estimation.metrics.rmse(pred: list, target: list) float[source]#
    +

    Mean squared error between real and predicted wear.

    +
    +
    Parameters:
    +
      +
    • pred (list) – numpy prediction values.

    • +
    • target (list) – numpy target values.

    • +
    +
    +
    Returns:
    +

    rmse

    +
    +
    Return type:
    +

    float

    +
    +
    +
    + @@ -483,6 +503,7 @@ diff --git a/docs/reference/ice.health_index_estimation.models.html b/docs/reference/ice.health_index_estimation.models.html index 306c5db..9a28761 100644 --- a/docs/reference/ice.health_index_estimation.models.html +++ b/docs/reference/ice.health_index_estimation.models.html @@ -421,14 +421,15 @@

    Remaining useful life models

    BaseRemainingUsefulLifeEstimation#

    -class ice.health_index_estimation.models.base.BaseHealthIndexEstimation(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str)[source]#
    +class ice.health_index_estimation.models.base.BaseHealthIndexEstimation(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, random_seed: int, val_ratio: float, save_checkpoints: bool)[source]#

    Bases: BaseModel, ABC

    Base class for all HI diagnosis models.

    Parameters:
    • window_size (int) – The window size to train the model.

    • -
    • stride (int) – The time interval between first points of consecutive sliding windows.

    • +
    • stride (int) – The time interval between first points of consecutive +sliding windows in training.

    • batch_size (int) – The batch size to train the model.

    • lr (float) – The larning rate to train the model.

    • num_epochs (float) – The number of epochs to train the model.

    • @@ -436,44 +437,20 @@

      Remaining useful life models -
      -evaluate(df: DataFrame, target: Series) dict[source]#
      -

      Evaluate the metrics: mse.

      -
      -
      Parameters:
      -
        -
      • df (pandas.DataFrame) – A dataframe with sensor data. Index has -two columns: run_id and sample. All other columns a value of -sensors.

      • -
      • target (pandas.Series) – A series with target values. Indes has two -columns: run_id and sample.

      • -
      -
      -
      Returns:
      -

      -
      A dictionary with metrics where keys are names of metrics and

      values are values of metrics.

      -
      -
      -

      -
      -
      Return type:
      -

      dict

      -
      -
      -

    -
    -
    -

    MLP#

    +
    +

    MLP#

    -class ice.health_index_estimation.models.mlp.MLP(window_size: int = 1024, stride: int = 300, hidden_dim: int = 256, batch_size: int = 64, lr: float = 5e-05, num_epochs: int = 50, device: str = 'cpu', verbose: bool = True, name: str = 'mlp_fault_diagnosis')[source]#
    +class ice.health_index_estimation.models.mlp.MLP(window_size: int = 1024, stride: int = 300, hidden_dim: int = 256, batch_size: int = 64, lr: float = 5e-05, num_epochs: int = 50, device: str = 'cpu', verbose: bool = True, name: str = 'mlp_fault_diagnosis', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False)[source]#

    Bases: BaseHealthIndexEstimation

    Multilayer Perceptron (MLP) consists of input, hidden, output layers and ReLU activation. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) @@ -492,6 +469,9 @@

    Remaining useful life models

    +
    +
    +class ice.remaining_useful_life_estimation.datasets.RulCmapssPaper(num_chunks=None, force_download=False)[source]#
    +

    Bases: BaseDataset

    +

    Preprocessed to piece wise RUL data from the dataset: +Saxena A. et al. Damage propagation modeling for aircraft engine run-to-failure simulation +DOI: 10.1109/PHM.2008.4711414. Target is the minimum rul value for every test device.

    +
    +
    Parameters:
    +
      +
    • num_chunks (int) – If given, download only num_chunks chunks of data. +Used for testing purposes.

    • +
    • force_download (bool) – If True, download the dataset even if it exists.

    • +
    +
    +
    +
    + +

    This method has to be implemented by all children. Set name and public link.

    +
    + +
    +
    @@ -490,6 +514,10 @@
  • RulCmapss.set_name_public_link()
  • +
  • RulCmapssPaper +
  • diff --git a/docs/reference/ice.remaining_useful_life_estimation.html b/docs/reference/ice.remaining_useful_life_estimation.html index e79d574..b9d9d88 100644 --- a/docs/reference/ice.remaining_useful_life_estimation.html +++ b/docs/reference/ice.remaining_useful_life_estimation.html @@ -418,17 +418,19 @@

    Remaining useful life
  • Models
  • Datasets
  • Metrics
  • Utils
      diff --git a/docs/reference/ice.remaining_useful_life_estimation.metrics.html b/docs/reference/ice.remaining_useful_life_estimation.metrics.html index 379992a..a2889da 100644 --- a/docs/reference/ice.remaining_useful_life_estimation.metrics.html +++ b/docs/reference/ice.remaining_useful_life_estimation.metrics.html @@ -418,15 +418,19 @@

      Remaining useful life metrics#

      -
      -ice.remaining_useful_life_estimation.metrics.nonsimmetric_function(value)[source]#
      -

      Exponent calculation function depending on the relative deviation from the true value

      +
      +ice.remaining_useful_life_estimation.metrics.cmapss_score(pred: list, target: list) float[source]#
      +

      Non-simmetric metric proposed in the original dataset paper. +DOI: 10.1109/PHM.2008.4711414

      Parameters:
      -

      value (float) – division result between predicted and real values.

      +
        +
      • pred (list) – numpy prediction values.

      • +
      • target (list) – numpy target values.

      • +
      Returns:
      -

      calculation result

      +

      cmapss score function

      Return type:

      float

      @@ -435,18 +439,15 @@
      -
      -ice.remaining_useful_life_estimation.metrics.rmse(pred: list, target: list) float[source]#
      -

      Root mean squared error between real and predicted remaining useful life values.

      +
      +ice.remaining_useful_life_estimation.metrics.nonsimmetric_function(value)[source]#
      +

      Exponent calculation function depending on the relative deviation from the true value

      Parameters:
      -
        -
      • pred (list) – numpy prediction values.

      • -
      • target (list) – numpy target values.

      • -
      +

      value (float) – division result between predicted and real values.

      Returns:
      -

      rmse

      +

      calculation result

      Return type:

      float

      @@ -455,10 +456,9 @@
      -
      -ice.remaining_useful_life_estimation.metrics.score(pred: list, target: list) float[source]#
      -

      Non-simmetric metric proposed in the original dataset paper. -DOI: 10.1109/PHM.2008.4711414

      +
      +ice.remaining_useful_life_estimation.metrics.rmse(pred: list, target: list) float[source]#
      +

      Root mean squared error between real and predicted remaining useful life values.

      Parameters:
        @@ -467,7 +467,7 @@
      Returns:
      -

      cmapss score function

      +

      rmse

      Return type:

      float

      @@ -520,9 +520,9 @@ diff --git a/docs/reference/ice.remaining_useful_life_estimation.models.html b/docs/reference/ice.remaining_useful_life_estimation.models.html index 07b2d5b..aeba09f 100644 --- a/docs/reference/ice.remaining_useful_life_estimation.models.html +++ b/docs/reference/ice.remaining_useful_life_estimation.models.html @@ -421,70 +421,81 @@

      Health index models

      BaseHealthIndexEstimation#

      -class ice.remaining_useful_life_estimation.models.base.BaseRemainingUsefulLifeEstimation(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str)[source]#
      +class ice.remaining_useful_life_estimation.models.base.BaseRemainingUsefulLifeEstimation(window_size: int, stride: int, batch_size: int, lr: float, num_epochs: int, device: str, verbose: bool, name: str, random_seed: int, val_ratio: float, save_checkpoints: bool)[source]#

      Bases: BaseModel, ABC

      Base class for all RUL models.

      Parameters:
      • window_size (int) – The window size to train the model.

      • -
      • stride (int) – The time interval between first points of consecutive sliding windows.

      • +
      • stride (int) – The time interval between first points of consecutive +sliding windows in training.

      • batch_size (int) – The batch size to train the model.

      • -
      • lr (float) – The learning rate to train the model.

      • +
      • lr (float) – The larning rate to train the model.

      • num_epochs (float) – The number of epochs to train the model.

      • device (str) – The name of a device to train the model. cpu and cuda are possible.

      • verbose (bool) – If true, show the progress bar in training.

      • name (str) – The name of the model for artifact storing.

      • +
      • random_seed (int) – Seed for random number generation to ensure reproducible results.

      • +
      • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

      • +
      • save_checkpoints (bool) – If true, store checkpoints.

      -
      -
      -evaluate(df: DataFrame, target: Series) dict[source]#
      -

      Evaluate the metrics: rmse, c-mapss score.

      +
      + +

      +
      +

      MLP#

      +
      +
      +class ice.remaining_useful_life_estimation.models.mlp.MLP(window_size: int = 32, stride: int = 1, hidden_dim: int = 512, batch_size: int = 64, lr: float = 0.0001, num_epochs: int = 15, device: str = 'cpu', verbose: bool = True, name: str = 'mlp_cmapss_rul', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False)[source]#
      +

      Bases: BaseRemainingUsefulLifeEstimation

      +

      Multilayer Perceptron (MLP) consists of input, hidden, output layers and +ReLU activation. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) +where B is the batch size, L is the sequence length, C is the number of +sensors.

      Parameters:
        -
      • df (pandas.DataFrame) – A dataframe with sensor data. Index has -two columns: run_id and sample. All other columns a value of -sensors.

      • -
      • target (pandas.Series) – A series with target values. Indes has two -columns: run_id and sample.

      • +
      • window_size (int) – The window size to train the model.

      • +
      • stride (int) – The time interval between first points of consecutive sliding windows.

      • +
      • hidden_dim (int) – The dimensionality of the hidden layer in MLP.

      • +
      • batch_size (int) – The batch size to train the model.

      • +
      • lr (float) – The larning rate to train the model.

      • +
      • num_epochs (float) – The number of epochs to train the model.

      • +
      • device (str) – The name of a device to train the model. cpu and +cuda are possible.

      • +
      • verbose (bool) – If true, show the progress bar in training.

      • +
      • name (str) – The name of the model for artifact storing.

      • +
      • random_seed (int) – Seed for random number generation to ensure reproducible results.

      • +
      • val_ratio (float) – Proportion of the dataset used for validation, between 0 and 1.

      • +
      • save_checkpoints (bool) – If true, store checkpoints.

      -
      Returns:
      -

      -
      A dictionary with metrics where keys are names of metrics and

      values are values of metrics.

      -
      -
      -

      -
      -
      Return type:
      -

      dict

      -
      - -
      -
      -

      MLP#

      +
      +

      LSTM#

      -
      -class ice.remaining_useful_life_estimation.models.mlp.MLP(window_size: int = 32, stride: int = 1, hidden_dim: int = 256, batch_size: int = 256, lr: float = 5e-05, num_epochs: int = 50, device: str = 'cpu', verbose: bool = True, name: str = 'mlp_cmapss_rul')[source]#
      +
      +class ice.remaining_useful_life_estimation.models.lstm.LSTM(window_size: int = 32, stride: int = 1, hidden_dim: int = 512, hidden_size: int = 256, num_layers: int = 2, dropout_value: float = 0.5, batch_size: int = 64, lr: float = 0.0001, num_epochs: int = 35, device: str = 'cpu', verbose: bool = True, name: str = 'mlp_cmapss_rul', random_seed: int = 42, val_ratio: float = 0.15, save_checkpoints: bool = False)[source]#

      Bases: BaseRemainingUsefulLifeEstimation

      -

      Multilayer Perceptron (MLP) consists of input, hidden, output layers and -ReLU activation. Each sample is reshaped to a vector (B, L, C) -> (B, L * C) -where B is the batch size, L is the sequence length, C is the number of -sensors.

      +

      Long short-term memory (LSTM) model consists of the classical LSTM architecture stack and +two-layer MLP with SiLU nonlinearity and dropout to make the final prediction.

      +

      Each sample is moved to LSTM and reshaped to a vector (B, L, C) -> (B, hidden_size, C) +Then the sample is reshaped to a vector (B, hidden_size, C) -> (B, hidden_size * C)

      Parameters:
      • window_size (int) – The window size to train the model.

      • stride (int) – The time interval between first points of consecutive sliding windows.

      • hidden_dim (int) – The dimensionality of the hidden layer in MLP.

      • +
      • hidden_size (int) – The number of features in the hidden state of the model.

      • +
      • num_layers (int) – The number of stacked reccurent layers of the classic LSTM architecture.

      • batch_size (int) – The batch size to train the model.

      • lr (float) – The larning rate to train the model.

      • num_epochs (float) – The number of epochs to train the model.

      • @@ -492,6 +503,9 @@

        Health index models