Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 207 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

docs-master

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

API Reference

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Overview

Learn about the features of Alibi Detect

Alibi Detect Logo

Alibi Detect

Alibi Detect is a source-available Python library focused on outlier, adversarial and drift detection. The package aims to cover both online and offline detectors for tabular data, text, images and time series. Both TensorFlow and PyTorch backends are supported for drift detection.

For more background on the importance of monitoring outliers and distributions in a production setting, check out this talk from the Challenges in Deploying and Monitoring Machine Learning Systems ICML 2020 workshop, based on the paper Monitoring and explainability of models in production and referencing Alibi Detect.

For a thorough introduction to drift detection, check out the talk below titled, . The talk covers what drift is and why it pays to detect it, the different types of drift, how it can be detected in a principled manner and also describes the anatomy of a drift detector.

Methods

Protecting Your Machine Learning Against Drift: An Introduction

Outlier Detection

Methods

Bibliography

  • [RA12] Gordon J. Ross and Niall M. Adams. Two nonparametric control charts for detecting arbitrary distribution changes. Journal of Quality Technology, 44(2):102–116, 2012. doi:10.1080/00224065.2012.11917887.

  • [RTA12] Gordon J. Ross, Dimitris K. Tasoulis, and Niall M. Adams. Sequential monitoring of a bernoulli sequence when the pre-change parameter is unknown. Computational Statistics, 28(2):463–479, March 2012. arXiv:1212.6020, doi:10.1007/s00180-012-0311-7.

  • [HHG20] Allison Marie Horst, Alison Presmanes Hill, and Kristen B Gorman. palmerpenguins: Palmer Archipelago (Antarctica) penguin data. 2020. R package version 0.1.0. doi:10.5281/zenodo.3960218.

  • [GBR+12] Arthur Gretton, Karsten M. Borgwardt, Malte J. Rasch, Bernhard Schölkopf, and Alexander Smola. A kernel two-sample test. J. Mach. Learn. Res., 13(1):723–773, mar 2012. URL: https://dl.acm.org/doi/10.5555/2188385.2188410.

Examples

Methods

alibi_detect.cd.keops

Examples

Examples

alibi_detect.ad

alibi_detect.cd

Algorithm Overview

The following tables summarize the advised use cases for the current algorithms. Please consult the method specific pages for a more detailed breakdown of each method. The column Feature Level indicates whether the detection can be done and returned at the feature level, e.g. per pixel for an image.

Outlier Detection

Detector
Tabular
Image
Time Series
Text
Categorical Features
Online
Feature Level

Adversarial Detection

Detector
Tabular
Image
Time Series
Text
Categorical Features
Online
Feature Level

Drift Detection

Detector
Tabular
Image
Time Series
Text
Categorical Features
Online
Feature Level

All drift detectors and built-in preprocessing methods support both PyTorch and TensorFlow backends. The preprocessing steps include randomly initialized encoders, pretrained text embeddings to detect drift on using the library and extraction of hidden layers from machine learning models. The preprocessing steps allow to detect different types of drift such as covariate and predicted distribution shift.

Online

Adversarial Detection

Offline

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

✔

Isolation Forest

✔

✔

Mahalanobis Distance

✔

✔

✔

AE

Adversarial AE

✔

✔

Kolmogorov-Smirnov

✔

✔

✔

✔

transformers

✔

✔

Isolation Forest

source

Isolation Forest

Overview

Isolation forests (IF) are tree based models specifically used for outlier detection. The IF isolates observations by randomly selecting a feature and then randomly selecting a split value between the maximum and minimum values of the selected feature. The number of splittings required to isolate a sample is equivalent to the path length from the root node to the terminating node. This path length, averaged over a forest of random trees, is a measure of normality and is used to define an anomaly score. Outliers can typically be isolated quicker, leading to shorter paths. The algorithm is suitable for low to medium dimensional tabular data.

Usage

Initialize

Parameters:

  • threshold: threshold value for the outlier score above which the instance is flagged as an outlier.

  • n_estimators: number of base estimators in the ensemble. Defaults to 100.

  • max_samples: number of samples to draw from the training data to train each base estimator. If int, draw max_samples samples. If float

Initialized outlier detector example:

Fit

We then need to train the outlier detector. The following parameters can be specified:

  • X: training batch as a numpy array.

  • sample_weight: array with shape (batch size,) used to assign different weights to each instance during training. Defaults to None.

It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

Detect

We detect outliers by simply calling predict on a batch of instances X to compute the instance level outlier scores. We can also return the instance level outlier score by setting return_instance_score to True.

The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

  • is_outlier: boolean whether instances are above the threshold and therefore outlier instances. The array is of shape (batch size,).

  • instance_score: contains instance level scores if return_instance_score equals True.

Examples

Tabular

Spectral Residual

source

Spectral Residual

Overview

The Spectral Residual outlier detector is based on the paper Time-Series Anomaly Detection Service at Microsoft and is suitable for unsupervised online anomaly detection in univariate time series data. The algorithm first computes the of the original data. Then it computes the spectral residual of the log amplitude of the transformed signal before applying the Inverse Fourier Transform to map the sequence back from the frequency to the time domain. This sequence is called the saliency map. The anomaly score is then computed as the relative difference between the saliency map values and their moving averages. If the score is above a threshold, the value at a specific timestep is flagged as an outlier. For more details, please check out the .

Usage

Initialize

Parameters:

  • threshold: Threshold used to classify outliers. Relative saliency map distance from the moving average.

  • window_amp: Window used for the moving average in the spectral residual computation. The spectral residual is the difference between the log amplitude of the Fourier Transform and a convolution of the log amplitude over window_amp.

  • window_local: Window used for the moving average in the outlier score computation. The outlier score computes the relative difference between the saliency map and a moving average of the saliency map over

Initialized outlier detector example:

It is often hard to find a good threshold value. If we have a time series containing both normal and outlier data and we know approximately the percentage of normal data in the time series, we can infer a suitable threshold:

Detect

We detect outliers by simply calling predict on a time series X to compute the outlier scores and flag the anomalies. We can also return the instance (timestep) level outlier score by setting return_instance_score to True.

The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

  • is_outlier: boolean whether instances are above the threshold and therefore outlier instances. The array is of shape (timesteps,).

  • instance_score: contains instance level scores if return_instance_score equals True.

Examples

Models

Models and/or building blocks that can be useful outside of outlier, adversarial or drift detection can be found under alibi_detect.models. Main implementations:

  • PixelCNN++: from alibi_detect.models.tensorflow import PixelCNN

  • Variational Autoencoder: from alibi_detect.models.tensorflow import VAE

  • Sequence-to-sequence model: from alibi_detect.models.tensorflow import Seq2Seq

  • ResNet: from alibi_detect.models.tensorflow import resnet

Pre-trained TensorFlow ResNet-20/32/44 models on CIFAR-10 can be found on our and can be fetched as follows:

Auto-Encoder

Overview

The Auto-Encoder (AE) outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The AE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

Kolmogorov-Smirnov

Kolmogorov-Smirnov

Overview

The drift detector applies feature-wise two-sample (K-S) tests. For multivariate data, the obtained p-values for each feature are aggregated either via the

Chi-Squared

Chi-Squared

Overview

The drift detector applies feature-wise tests for the categorical features. For multivariate data, the obtained p-values for each feature are aggregated either via the

VAE
AEGMM
VAEGMM
Likelihood Ratios
Prophet
Spectral Residual
Seq2Seq
Model distillation
Cramér-von Mises
Fisher's Exact Test
Least-Squares Density Difference
Maximum Mean Discrepancy (MMD)
Learned Kernel MMD
Context-aware MMD
Chi-Squared
Mixed-type tabular
Classifier
Spot-the-diff
Classifier Uncertainty
Regressor Uncertainty
, draw
max_samples
times number of features
samples. If
'auto'
,
max_samples
= min(256, number of samples).
  • max_features: number of features to draw from the training data to train each base estimator. If int, draw max_features features. If float, draw max_features times number of features features.

  • bootstrap: whether to fit individual trees on random subsets of the training data, sampled with replacement.

  • n_jobs: number of jobs to run in parallel for fit and predict.

  • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

  • Outlier detection on KDD Cup 99
    window_local
    timesteps.
  • padding_amp_method: Padding method to be used prior to each convolution over log amplitude. Possible values: constant | replicate | reflect. Default value: replicate.

    • constant - padding with constant 0.

    • replicate - repeats the last/extreme value.

    • reflect - reflects the time series.

  • padding_local_method: Padding method to be used prior to each convolution over saliency map. Possible values: constant | replicate | reflect. Default value: replicate.

    • constant - padding with constant 0.

    • replicate - repeats the last/extreme value.

    • reflect - reflects the time series.

  • padding_amp_side: Whether to pad the amplitudes on both sides or only on one side. Possible values: bilateral | left | right.

  • n_est_points: Number of estimated points padded to the end of the sequence.

  • n_grad_points: Number of points used for the gradient estimation of the additional points padded to the end of the sequence. The paper sets this value to 5.

  • Fourier Transform
    paper
    Time series outlier detection with Spectral Residuals on synthetic data
    from alibi_detect.utils.fetching import fetch_tf_model
    
    model = fetch_tf_model('cifar10', 'resnet32')
    Google Cloud Bucket
    Usage

    Initialize

    Parameters:

    • threshold: threshold value above which the instance is flagged as an outlier.

    • encoder_net: tf.keras.Sequential instance containing the encoder network. Example:

    • decoder_net: tf.keras.Sequential instance containing the decoder network. Example:

    • ae: instead of using a separate encoder and decoder, the AE can also be passed as a tf.keras.Model.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The following parameters can be specified:

    • X: training batch as a numpy array of preferably normal data.

    • loss_fn: loss function used for training. Defaults to the Mean Squared Error loss.

    • optimizer: optimizer used for training. Defaults to Adam with learning rate 1e-3.

    • epochs: number of training epochs.

    • batch_size: batch size used during training.

    • verbose: boolean whether to print training progress.

    • log_metric: additional metrics whose progress will be displayed if verbose equals True.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Detect

    We detect outliers by simply calling predict on a batch of instances X. Detection can be customized via the following parameters:

    • outlier_type: either 'instance' or 'feature'. If the outlier type equals 'instance', the outlier score at the instance level will be used to classify the instance as an outlier or not. If 'feature' is selected, outlier detection happens at the feature level (e.g. by pixel in images).

    • outlier_perc: percentage of the sorted (descending) feature level outlier scores. We might for instance want to flag an image as an outlier if at least 20% of the pixel values are on average above the threshold. In this case, we set outlier_perc to 20. The default value is 100 (using all the features).

    • return_feature_score: boolean whether to return the feature level outlier scores.

    • return_instance_score: boolean whether to return the instance level outlier scores.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances or features are above the threshold and therefore outliers. If outlier_type equals 'instance', then the array is of shape (batch size,). If it equals 'feature', then the array is of shape (batch size, instance shape).

    • feature_score: contains feature level scores if return_feature_score equals True.

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Image

    Outlier detection on CIFAR10

    from alibi_detect.od import IForest
    
    od = IForest(
        threshold=0.,
        n_estimators=100
    )
    od.fit(
        X_train
    )
    od.infer_threshold(
        X, 
        threshold_perc=95
    )
    preds = od.predict(
        X,
        return_instance_score=True
    )
    from alibi_detect.od import SpectralResidual
    
    od = SpectralResidual(
        threshold=1.,
        window_amp=20,
        window_local=20,
        padding_amp_method='reflect',
        padding_local_method='reflect',
        padding_amp_side='bilateral',
        n_est_points=10,
        n_grad_points=5
    )
    od.infer_threshold(
        X,
        t=t,  # array with timesteps, assumes dt=1 between observations if omitted
        threshold_perc=95
    )
    preds = od.predict(
        X,
        t=t,  # array with timesteps, assumes dt=1 between observations if omitted
        return_instance_score=True
    )
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu)
      ])
    decoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(1024,)),
          Dense(4*4*128),
          Reshape(target_shape=(4, 4, 128)),
          Conv2DTranspose(256, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2DTranspose(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2DTranspose(3, 4, strides=2, padding='same', activation='sigmoid')
      ])
    from alibi_detect.od import OutlierAE
    
    od = OutlierAE(threshold=0.1,
                   encoder_net=encoder_net,
                   decoder_net=decoder_net)
    od.fit(X_train, epochs=50)
    od.infer_threshold(X, threshold_perc=95)
    preds = od.predict(X,
                       outlier_type='instance',
                       outlier_perc=75,
                       return_feature_score=True,
                       return_instance_score=True)
    or the
    (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur.

    For high-dimensional data, we typically want to reduce the dimensionality before computing the feature-wise univariate K-S tests and aggregating those via the chosen correction method. Following suggestions in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift, we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs (BBSDs) as out-of-the box preprocessing methods and note that PCA can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift. The adversarial detector which is part of the library can also be transformed into a drift detector picking up drift that reduces the performance of the classification model. We can therefore combine different preprocessing techniques to figure out if there is drift which hurts the model performance, and whether this drift can be classified as input drift or label shift.

    Detecting input data drift (covariate shift) $\Delta p(x)$ for text data requires a custom preprocessing step. We can pick up changes in the semantics of the input by extracting (contextual) embeddings and detect drift on those. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract. The library contains functionality to leverage pre-trained embeddings from HuggingFace's transformer package but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in the Text drift detection on IMDB movie reviews notebook.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • p_val: p-value used for significance of the K-S test. If the FDR correction method is used, this corresponds to the acceptable q-value.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to True. It is possible that it needs to be set to False if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.

    • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

    • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

    • correction: Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    • alternative: Defines the alternative hypothesis. Options are 'two-sided' (default), 'less' or 'greater'.

    • n_features: Number of features used in the K-S test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    • input_shape: Shape of input data.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized drift detector example:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the feature-wise p-values before the multivariate correction by setting return_p_val to True. The drift can also be detected at the feature level by setting drift_type to 'feature'. No multivariate correction will take place since we return the output of n_features univariate tests. For drift detection on all the features combined with the correction, use 'batch'. return_p_val equal to True will also return the threshold used by the detector (either for the univariate case or after the multivariate correction).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains feature-level p-values if return_p_val equals True.

    • threshold: for feature-level drift detection the threshold equals the p-value used for the significance of the K-S test. Otherwise the threshold after the multivariate correction (either bonferroni or fdr) is returned.

    • distance: feature-wise K-S statistics between the reference data and the new batch if return_distance equals True.

    Examples

    Graph

    Drift detection on molecular graphs

    Image

    Drift detection on CIFAR10

    Text

    Text drift detection on IMDB movie reviews

    source
    Kolmogorov-Smirnov
    Bonferroni
    False Discovery Rate
    or the
    (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur. Similarly to the other drift detectors, a preprocessing steps could be applied, but the output features need to be categorical.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • p_val: p-value used for significance of the Chi-Squared test for. If the FDR correction method is used, this corresponds to the acceptable q-value.

    • categories_per_feature: Optional dictionary with as keys the feature column index and as values the number of possible categorical values for that feature or a list with the possible values. If you know how many categories are present for a given feature you could pass this in the categories_per_feature dict in the Dict[int, int] format, e.g. {0: 3, 3: 2}. If you pass N categories this will assume the possible values for the feature are [0, ..., N-1]. You can also explicitly pass the possible categories in the Dict[int, List[int]] format, e.g. {0: [0, 1, 2], 3: [0, 55]}. Note that the categories can be arbitrary int values. If it is not specified, categories_per_feature is inferred from x_ref.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to True. It is possible that it needs to be set to False if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.

    • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

    • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique. Needs to return categorical features for the Chi-Squared detector.

    • correction: Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    • n_features: Number of features used in the Chi-Squared test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    • data_type: can specify data type added to metadata. E.g. 'tabular'.

    Initialized drift detector example:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the feature-wise p-values before the multivariate correction by setting return_p_val to True. The drift can also be detected at the feature level by setting drift_type to 'feature'. No multivariate correction will take place since we return the output of n_features univariate tests. For drift detection on all the features combined with the correction, use 'batch'. return_p_val equal to True will also return the threshold used by the detector (either for the univariate case or after the multivariate correction).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains feature-level p-values if return_p_val equals True.

    • threshold: for feature-level drift detection the threshold equals the p-value used for the significance of the Chi-Square test. Otherwise the threshold after the multivariate correction (either bonferroni or fdr) is returned.

    • distance: feature-wise Chi-Square test statistics between the reference data and the new batch if return_distance equals True.

    Examples

    Drift detection on income prediction

    source
    Chi-Squared
    Bonferroni
    False Discovery Rate

    Getting Started

    Installation

    Alibi Detect can be installed from PyPI or conda-forge by following the instructions below.

    PyPI

    Alibi Detect can be installed from with pip. We provide optional dependency buckets for several modules that are large or sometimes tricky to install. Many detectors are supported out of the box with the default install but some detectors require a specific optional dependency installation to use. For instance, the OutlierProphet detector requires the prophet installation. Other detectors have a choice of backend. For instance, the LSDDDrift detector has a choice of tensorflow or pytorch backends. The tabs below list the full set of detector functionality provided by each optional dependency.

    Default installation.

    The default installation provides out the box support for the following detectors:

    conda-forge

    To install the conda-forge version it is recommended to use , which can be installed to the baseconda enviroment with:

    mamba can then be used to install alibi-detect in a conda enviroment:

    Features

    is an open source Python library focused on outlier, adversarial and drift detection. The package aims to cover both online and offline detectors for tabular data, text, images and time series. TensorFlow, PyTorch and (where applicable) backends are supported for drift detection. Alibi-Detect does not install these as default. See for more details.

    To get a list of respectively the latest outlier, adversarial and drift detection algorithms, you can type:

    Summary tables highlighting the practical use cases for all the algorithms can be found .

    For detailed information on the outlier detectors:

    Similar for adversarial detection:

    And data drift:

    Basic Usage

    We will use the to illustrate the usage of outlier and adversarial detectors in alibi-detect.

    First, we import the detector:

    Then we initialize it by passing it the necessary arguments:

    Some detectors require an additional .fit step using training data:

    The detectors can be saved or loaded as described in . Finally, we can make predictions on test data and detect outliers or adversarial examples.

    The predictions are returned in a dictionary with as keys meta and data. meta contains the detector's metadata while data is in itself a dictionary with the actual predictions (and other relevant values). It has either is_outlier, is_adversarial or is_drift (filled with 0's and 1's) as well as optional instance_score, feature_score or p_value as keys with numpy arrays as values.

    The exact details will vary slightly from method to method, so we encourage the reader to become familiar with the in alibi-detect.

    Mahalanobis Distance

    source

    Mahalanobis Distance

    Overview

    The Mahalanobis online outlier detector aims to predict anomalies in tabular data. The algorithm calculates an outlier score, which is a measure of distance from the center of the features distribution (). If this outlier score is higher than a user-defined threshold, the observation is flagged as an outlier. The algorithm is online, which means that it starts without knowledge about the distribution of the features and learns as requests arrive. Consequently you should expect the output to be bad at the start and to improve over time. The algorithm is suitable for low to medium dimensional tabular data.

    The algorithm is also able to include categorical variables. The fit step first computes pairwise distances between the categories of each categorical variable. The pairwise distances are based on either the model predictions (MVDM method) or the context provided by the other variables in the dataset (ABDM method). For MVDM, we use the difference between the conditional model prediction probabilities of each category. This method is based on the Modified Value Difference Metric (MVDM) by . ABDM stands for Association-Based Distance Metric, a categorical distance measure introduced by . ABDM infers context from the presence of other variables in the data and computes a dissimilarity measure based on the . Both methods can also be combined as ABDM-MVDM. We can then apply multidimensional scaling to project the pairwise distances into Euclidean space.

    Usage

    Initialize

    Parameters:

    • threshold: Mahalanobis distance threshold above which the instance is flagged as an outlier.

    • n_components: number of principal components used.

    • std_clip: feature-wise standard deviation used to clip the observations before updating the mean and covariance matrix.

    Initialized outlier detector example:

    Fit

    We only need to fit the outlier detector if there are categorical variables present in the data. The following parameters can be specified:

    • X: training batch as a numpy array.

    • y: model class predictions or ground truth labels for X. Used for 'mvdm' and 'abdm-mvdm' pairwise distance metrics. Not needed for 'abdm'.

    • d_type: pairwise distance metric used for categorical variables. Currently,

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Beware though that the outlier detector is stateful and every call to the score function will update the mean and covariance matrix, even when inferring the threshold.

    Detect

    We detect outliers by simply calling predict on a batch of instances X to compute the instance level Mahalanobis distances. We can also return the instance level outlier score by setting return_instance_score to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances are above the threshold and therefore outlier instances. The array is of shape (batch size,).

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Tabular

    Auto-Encoding Gaussian Mixture Model

    source

    Auto-Encoding Gaussian Mixture Model

    Overview

    The Auto-Encoding Gaussian Mixture Model (AEGMM) Outlier Detector follows the paper. The encoder compresses the data while the reconstructed instances generated by the decoder are used to create additional features based on the reconstruction error between the input and the reconstructions. These features are combined with encodings and fed into a Gaussian Mixture Model (). The AEGMM outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised or semi-supervised training is desirable since labeled data is often scarce. The sample energy of the GMM can then be used to determine whether an instance is an outlier (high sample energy) or not (low sample energy). The algorithm is suitable for tabular and image data.

    Usage

    Initialize

    Parameters:

    • threshold: threshold value for the sample energy above which the instance is flagged as an outlier.

    • n_gmm: number of components in the GMM.

    • encoder_net: tf.keras.Sequential instance containing the encoder network. Example:

    • decoder_net: tf.keras.Sequential instance containing the decoder network. Example:

    • gmm_density_net: layers for the GMM network wrapped in a tf.keras.Sequential class. Example:

    • aegmm: instead of using a separate encoder, decoder and GMM density net, the AEGMM can also be passed as a tf.keras.Model.

    • recon_features: function to extract features from the reconstructed instance by the decoder. Defaults to a combination of the mean squared reconstruction error and the cosine similarity between the original and reconstructed instances by the AE.

    • data_type: can specify data type added to metadata. E.g.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The following parameters can be specified:

    • X: training batch as a numpy array of preferably normal data.

    • loss_fn: loss function used for training. Defaults to the custom AEGMM loss which is a combination of the mean squared reconstruction error, the sample energy of the GMM and a loss term penalizing small values on the diagonals of the covariance matrices in the GMM to avoid trivial solutions. It is important to balance the loss weights below so no single loss term dominates during the optimization.

    • w_energy: weight on sample energy loss term. Defaults to 0.1.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Detect

    We detect outliers by simply calling predict on a batch of instances X to compute the instance level sample energies. We can also return the instance level outlier score by setting return_instance_score to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances are above the threshold and therefore outlier instances. The array is of shape (batch size,).

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Tabular

    Sequence-to-Sequence (Seq2Seq)

    source

    Sequence-to-Sequence (Seq2Seq)

    Overview

    The Sequence-to-Sequence (Seq2Seq) outlier detector consists of 2 main building blocks: an encoder and a decoder. The encoder consists of a which processes the input sequence and initializes the decoder. The LSTM decoder then makes sequential predictions for the output sequence. In our case, the decoder aims to reconstruct the input sequence. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

    Since even for normal data the reconstruction error can be state-dependent, we add an outlier threshold estimator network to the Seq2Seq model. This network takes in the hidden state of the decoder at each timestep and predicts the estimated reconstruction error for normal data. As a result, the outlier threshold is not static and becomes a function of the model state. This is similar to , but while they train the threshold estimator separately from the Seq2Seq model with a Support-Vector Regressor, we train a neural net regression network end-to-end with the Seq2Seq model.

    The detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The Seq2Seq outlier detector is suitable for both univariate and multivariate time series.

    Usage

    Initialize

    Parameters:

    • n_features: number of features in the time series.

    • seq_len: sequence length fed into the Seq2Seq model.

    • threshold: threshold used for outlier detection. Can be a float or feature-wise array.

    • latent_dim: latent dimension of the encoder and decoder.

    • output_activation: activation used in the Dense output layer of the decoder.

    • beta: weight on the threshold estimation mean-squared error (MSE) loss term.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The following parameters can be specified:

    • X: univariate or multivariate time series array with preferably normal data used for training. Shape equals (batch, n_features) or (batch, seq_len, n_features).

    • loss_fn: loss function used for training. Defaults to the MSE loss.

    • optimizer: optimizer used for training. Defaults to with learning rate 1e-3.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold. We can either set the threshold over both features combined or determine a feature-wise threshold. Here we opt for the feature-wise threshold. This is for instance useful when different features have different variance or sensitivity to outliers. The snippet assumes there are about 5% outliers in the first feature and 10% in the second:

    Detect

    We detect outliers by simply calling predict on a batch of instances X. Detection can be customized via the following parameters:

    • outlier_type: either 'instance' or 'feature'. If the outlier type equals 'instance', the outlier score at the instance level will be used to classify the instance as an outlier or not. If 'feature' is selected, outlier detection happens at the feature level. It is important to distinguish 2 use cases:

      • X has shape (batch, n_features):

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances or features are above the threshold and therefore outliers. If outlier_type equals 'instance', then the array is of shape (batch,). If it equals 'feature', then the array is of shape (batch, seq_len, n_features) or (batch, n_features), depending on the shape of X.

    • feature_score: contains feature level scores if return_feature_score equals True.

    Examples

    Isolation Forest outlier detection on KDD Cup ‘99 dataset

    Method

    Isolation forests (IF) are tree based models specifically used for outlier detection. The IF isolates observations by randomly selecting a feature and then randomly selecting a split value between the maximum and minimum values of the selected feature. The number of splittings required to isolate a sample is equivalent to the path length from the root node to the terminating node. This path length, averaged over a forest of random trees, is a measure of normality and is used to define an anomaly score. Outliers can typically be isolated quicker, leading to shorter paths.

    Dataset

    The outlier detector needs to detect computer network intrusions using TCP dump data for a local-area network (LAN) simulating a typical U.S. Air Force LAN. A connection is a sequence of TCP packets starting and ending at some well defined times, between which data flows to and from a source IP address to a target IP address under some well defined protocol. Each connection is labeled as either normal, or as an attack.

    There are 4 types of attacks in the dataset:

    • DOS: denial-of-service, e.g. syn flood;

    • R2L: unauthorized access from a remote machine, e.g. guessing password;

    • U2R: unauthorized access to local superuser (root) privileges;

    • probing: surveillance and other probing, e.g., port scanning.

    The dataset contains about 5 million connection records.

    There are 3 types of features:

    • basic features of individual connections, e.g. duration of connection

    • content features within a connection, e.g. number of failed log in attempts

    • traffic features within a 2 second window, e.g. number of connections to the same host as the current connection

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load dataset

    We only keep a number of continuous (18 out of 41) features.

    Assume that a model is trained on normal instances of the dataset (not outliers) and standardization is applied:

    Apply standardization:

    Define outlier detector

    We train an outlier detector from scratch:

    The warning tells us we still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have some data which we know contains around 5% outliers. The percentage of outliers can be set with perc_outlier in the create_outlier_batch function.

    Let's save the outlier detector with updated threshold:

    Detect outliers

    We now generate a batch of data with 10% outliers and detect the outliers in the batch.

    Predict outliers:

    Display results

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    We can see that the isolation forest does not do a good job at detecting 1 type of outliers with an outlier score around 0. This makes inferring a good threshold without explicit knowledge about the outliers hard. Setting the threshold just below 0 would lead to significantly better detector performance for the outliers in the dataset. This is also reflected by the ROC curve:

    Fisher’s Exact Test

    source

    Fisher's Exact Test

    Overview

    The FET drift detector is a non-parametric drift detector. It applies Fisher's Exact Test (FET) to each feature, and is intended for application to , with binary univariate data consisting of either (True, False) or (0, 1). This detector is ideal for use in a supervised setting, monitoring drift in a model's instance level accuracy (i.e. correct prediction = 0, and incorrect prediction = 1).

    The detector is primarily intended for univariate data, but can also be used in a multivariate setting. For multivariate data, the obtained p-values for each feature are aggregated either via the or the (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur. As with other univariate detectors such as the detector, for high-dimensional data, we typically want to reduce the dimensionality before computing the feature-wise univariate FET tests and aggregating those via the chosen correction method. See for more guidance on this.

    For the $j^{th}$ feature, the FET detector considers the 2x2 contingency table between the reference data $x_j^{ref}$ and test data $x_j$ for that feature:

    True (1)
    False (0)

    where $N^{ref}_1$ represents the number of 1's in the reference data (for the $j^{th}$ feature), $N^{ref}_0$ the number of 0's, and so on. These values can be used to define an odds ratio:

    The null hypothesis is $H_0: \widehat{OR}=1$. In other words, the proportion of 1's to 0's is unchanged between the test and reference distributions, such that the odds of 1's vs 0's is independent of whether the data is drawn from the reference or test distribution. The offline FET detector can perform one-sided or two-sided tests, with the alternative hypothesis set by the alternative keyword argument:

    • If alternative='greater', the alternative hypothesis is $H_a: \widehat{OR}>1$ i.e. proportion of 1's versus 0's has increased compared to the reference distribution.

    • If alternative='less', the alternative hypothesis is $H_a: \widehat{OR}<1$ i.e. the proportion of 1's versus 0's has decreased compared to the reference distribution.

    • If alternative='two-sided', the alternative hypothesis is $H_a: \widehat{OR} \ne 1$ i.e. the proportion of 1's versus 0's has changed compared to the reference distribution.

    The p-value returned by the detector is then the probability of obtaining an odds ratio at least as extreme as that observed (in the direction specified by alternative), assuming the null hypothesis is true.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution. Note this should be the raw data, for example np.array([0, 0, 1, 0, 0, 0]), not the 2x2 contingency table.

    Keyword arguments:

    • p_val: p-value used for significance of the FET test. If the FDR correction method is used, this corresponds to the acceptable q-value.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to True. It is possible that it needs to be set to False if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.

    • x_ref_preprocessed

    Initialized drift detector example:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the feature-wise p-values before the multivariate correction by setting return_p_val to True. The drift can also be detected at the feature level by setting drift_type to 'feature'. No multivariate correction will take place since we return the output of n_features univariate tests. For drift detection on all the features combined with the correction, use 'batch'. return_p_val equal to True will also return the threshold used by the detector (either for the univariate case or after the multivariate correction).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains feature-level p-values if return_p_val equals True.

    • threshold: for feature-level drift detection the threshold equals the p-value used for the significance of the FET test. Otherwise the threshold after the multivariate correction (either

    Examples

    Mixed-type tabular data

    source

    Mixed-type tabular data

    Overview

    The drift detector applies feature-wise two-sample Kolmogorov-Smirnov (K-S) tests for the continuous numerical features and tests for the categorical features. For multivariate data, the obtained p-values for each feature are aggregated either via the or the (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur. Similarly to the other drift detectors, a preprocessing steps could be applied, but the output features need to be categorical.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • p_val: p-value used for significance of the K-S and Chi-Squared test across all features. If the FDR correction method is used, this corresponds to the acceptable q-value.

    • categories_per_feature: Dictionary with as keys the column indices of the categorical features and optionally as values the number of possible categorical values for that feature or a list with the possible values. If you know which features are categorical and simply want to infer the possible values of the categorical feature from the reference data you can pass a Dict[int, NoneType] such as {0: None, 3: None} if features 0 and 3 are categorical. If you also know how many categories are present for a given feature you could pass this in the categories_per_feature dict in the Dict[int, int] format, e.g. {0: 3, 3: 2}. If you pass N categories this will assume the possible values for the feature are [0, ..., N-1]. You can also explicitly pass the possible categories in the Dict[int, List[int]]

    Initialized drift detector example:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the feature-wise p-values before the multivariate correction by setting return_p_val to True. The drift can also be detected at the feature level by setting drift_type to 'feature'. No multivariate correction will take place since we return the output of n_features univariate tests. For drift detection on all the features combined with the correction, use 'batch'. return_p_val equal to True will also return the threshold used by the detector (either for the univariate case or after the multivariate correction).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains feature-level p-values if return_p_val equals True.

    • threshold: for feature-level drift detection the threshold equals the p-value used for the significance of the K-S and Chi-Squared tests. Otherwise the threshold after the multivariate correction (either

    Examples

    Datasets

    source

    Overview

    The package also contains functionality in alibi_detect.datasets to easily fetch a number of datasets for different modalities. For each dataset either the data and labels or a Bunch object with the data, labels and optional metadata are returned. Example:

    Sequential Data and Time Series

    Genome Dataset: fetch_genome

    • Bacteria genomics dataset for out-of-distribution detection, released as part of . From the original TL;DR: The dataset contains genomic sequences of 250 base pairs from 10 in-distribution bacteria classes for training, 60 OOD bacteria classes for validation, and another 60 different OOD bacteria classes for test. There are respectively 1, 7 and again 7 million sequences in the training, validation and test sets. For detailed info on the dataset check the .

    ECG 5000: fetch_ecg

    • 5000 ECG's, originally obtained from .

    NAB: fetch_nab

    • Any univariate time series in a DataFrame from the . A list with the available time series can be retrieved using alibi_detect.datasets.get_list_nab().

    Images

    CIFAR-10-C: fetch_cifar10c

    • CIFAR-10-C () contains the test set of CIFAR-10, but corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in a classification model's performance trained on CIFAR-10. fetch_cifar10c allows you to pick any severity level or corruption type. The list with available corruption types can be retrieved with alibi_detect.datasets.corruption_types_cifar10c(). The dataset can be used in research on robustness and drift. The original data can be found . Example:

    Adversarial CIFAR-10: fetch_attack

    • Load adversarial instances on a ResNet-56 classifier trained on CIFAR-10. Available attacks: ('cw') and ('slide'). Example:

    Tabular

    KDD Cup '99: fetch_kdd

    • Dataset with different types of computer network intrusions. fetch_kdd allows you to select a subset of network intrusions as targets or pick only specified features. The original data can be found .

    Time series outlier detection with Spectral Residuals on synthetic data

    Method

    The Spectral Residual outlier detector is based on the paper and is suitable for unsupervised online anomaly detection in univariate time series data. The algorithm first computes the of the original data. Then it computes the spectral residual of the log amplitude of the transformed signal before applying the Inverse Fourier Transform to map the sequence back from the frequency to the time domain. This sequence is called the saliency map. The anomaly score is then computed as the relative difference between the saliency map values and their moving averages. If this score is above a threshold, the value at a specific timestep is flagged as an outlier. For more details, please check out the .

    Variational Auto-Encoding Gaussian Mixture Model

    Variational Auto-Encoding Gaussian Mixture Model

    Overview

    The Variational Auto-Encoding Gaussian Mixture Model (VAEGMM) Outlier Detector follows the

    Online Fisher’s Exact Test

    Online Fisher's Exact Test

    Overview

    The online detector is a non-parametric method for online drift detection. Like the

    Model Distillation

    Model distillation

    Overview

    is a technique that is used to transfer knowledge from a large network to a smaller network. Typically, it consists of training a second model with a simplified architecture on soft targets (the output distributions or the logits) obtained from the original model.

    alibi_detect.cd.ks

    KSDrift

    Inherits from: BaseUnivariateDrift, BaseDetector, ABC, DriftConfigMixin

    Cramér-von Mises

    Cramér-von Mises

    Overview

    The CVM drift detector is a non-parametric drift detector, which applies feature-wise two-sample

    Likelihood Ratios for Outlier Detection

    Likelihood Ratios for Outlier Detection

    Overview

    The outlier detector described by

    Prophet Detector

    Prophet Detector

    Overview

    The Prophet outlier detector uses the time series forecasting package explained in

    from alibi_detect.cd import KSDrift
    
    cd = KSDrift(x_ref, p_val=0.05)
    preds = cd.predict(x, drift_type='batch', return_p_val=True, return_distance=True)
    from alibi_detect.cd import ChiSquareDrift
    
    cd = ChiSquareDrift(x_ref, p_val=0.05)
    preds = cd.predict(x, drift_type='batch', return_p_val=True, return_distance=True)
    from alibi_detect.datasets import fetch_ecg
    
    (X_train, y_train), (X_test, y_test) = fetch_ecg(return_X_y=True)
    reservoir sampling
    reservoir sampling

    start_clip: number of observations before clipping is applied.

  • max_n: algorithm behaves as if it has seen at most max_n points.

  • cat_vars: dictionary with as keys the categorical columns and as values the number of categories per categorical variable. Only needed if categorical variables are present.

  • ohe: boolean whether the categorical variables are one-hot encoded (OHE) or not. If not OHE, they are assumed to have ordinal encodings.

  • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

  • 'abdm'
    ,
    'mvdm'
    and
    'abdm-mvdm'
    are supported.
    'abdm'
    infers context from the other variables while
    'mvdm'
    uses the model predictions.
    'abdm-mvdm'
    is a weighted combination of the two metrics.
  • w: weight on 'abdm' (between 0. and 1.) distance if d_type equals 'abdm-mvdm'.

  • disc_perc: list with percentiles used in binning of numerical features used for the 'abdm' and 'abdm-mvdm' pairwise distance measures.

  • standardize_cat_vars: standardize numerical values of categorical variables if True.

  • feature_range: tuple with min and max ranges to allow for numerical values of categorical variables. Min and max ranges can be floats or numpy arrays with dimension (1, number of features) for feature-wise ranges.

  • smooth: smoothing exponent between 0 and 1 for the distances. Lower values will smooth the difference in distance metric between different features.

  • center: whether to center the scaled distance measures. If False, the min distance for each feature except for the feature with the highest raw max distance will be the lower bound of the feature range, but the upper bound will be below the max feature range.

  • Mahalanobis distance
    Cost et al (1993)
    Le et al (2005)
    Kullback-Leibler divergence
    Outlier detection on KDD Cup 99
    'tabular'
    or
    'image'
    .

    w_cov_diag: weight on covariance diagonals. Defaults to 0.005.

  • optimizer: optimizer used for training. Defaults to Adam with learning rate 1e-4.

  • epochs: number of training epochs.

  • batch_size: batch size used during training.

  • verbose: boolean whether to print training progress.

  • log_metric: additional metrics whose progress will be displayed if verbose equals True.

  • Deep Autoencoding Gaussian Mixture Model for Unsupervised Anomaly Detection
    GMM
    Outlier detection on KDD Cup 99
    seq2seq: optionally pass an already defined or pretrained Seq2Seq model to the outlier detector as a tf.keras.Model.
  • threshold_net: optionally pass the layers for the threshold estimation network wrapped in a tf.keras.Sequential instance. Example:

  • epochs: number of training epochs.

  • batch_size: batch size used during training.

  • verbose: boolean whether to print training progress.

  • log_metric: additional metrics whose progress will be displayed if verbose equals True.

  • There are batch instances with n_features features per instance.

  • X has shape (batch, seq_len, n_features)

    • Now there are batch instances with seq_len x n_features features per instance.

  • outlier_perc: percentage of the sorted (descending) feature level outlier scores. We might for instance want to flag a multivariate time series as an outlier at a specific timestamp if at least 75% of the feature values are on average above the threshold. In this case, we set outlier_perc to 75. The default value is 100 (using all the features).

  • return_feature_score: boolean whether to return the feature level outlier scores.

  • return_instance_score: boolean whether to return the instance level outlier scores.

  • instance_score: contains instance level scores if return_instance_score equals True.

    Bidirectional
    LSTM
    Park et al. (2017)
    Adam
    Time series outlier detection with Seq2Seq models on synthetic data
    Seq2Seq time series outlier detection on ECG data
    : Whether or not the reference data
    x_ref
    has already been preprocessed. If
    True
    , the reference data will be skipped and preprocessing will only be applied to the test data passed to
    predict
    .
  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

  • correction: Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

  • alternative: Defines the alternative hypothesis. Options are 'greater' (default), 'less' or 'two-sided'.

  • n_features: Number of features used in the FET test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

  • input_shape: Shape of input data.

  • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

  • bonferroni
    or
    fdr
    ) is returned.
  • distance: Feature-wise test statistics between the reference data and the new batch if return_distance equals True. In this case, the test statistics correspond to the odds ratios.

  • $x_j$

    $N_1$

    $N_0$

    $x_j^{ref}$

    $N^{ref}_1$

    $N^{ref}_0$

    OR^=N1N0N1refN0ref\widehat{OR} = \frac{\frac{N_1}{N_0}}{\frac{N^{ref}_1}{N^{ref}_0}}OR=N0ref​N1ref​​N0​N1​​​
    Bernoulli distributions
    Bonferroni
    False Discovery Rate
    Kolmogorov-Smirnov
    Dimension Reduction
    Supervised drift detection on the penguins dataset
    format, e.g.
    {0: [0, 1, 2], 3: [0, 55]}
    . Note that the categories can be arbitrary
    int
    values.
  • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to True. It is possible that it needs to be set to False if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.

  • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

  • correction: Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

  • alternative: Defines the alternative hypothesis for the K-S tests. Options are 'two-sided' (default), 'less' or 'greater'. Make sure to use 'two-sided' when mixing categorical and numerical features.

  • n_features: Number of features used in the K-S and Chi-Squared tests. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

  • data_type: can specify data type added to metadata. E.g. 'tabular'.

  • bonferroni
    or
    fdr
    ) is returned.
  • distance: feature-wise K-S or Chi-Squared statistics between the reference data and the new batch if return_distance equals True.

  • Chi-Squared
    Bonferroni
    False Discovery Rate
    Drift detection on income prediction
    Likelihood Ratios for Out-of-Distribution Detection
    README
    Physionet
    Numenta Anomaly Benchmark
    Hendrycks & Dietterich, 2019
    here
    Carlini-Wagner
    SLIDE
    here
    Dataset

    We test the outlier detector on a synthetic dataset generated with the TimeSynth package. It allows you to generate a wide range of time series (e.g. pseudo-periodic, autoregressive or Gaussian Process generated signals) and noise types (white or red noise). It can be installed as follows:

    Additionally, this notebook requires the seaborn package for visualization which can be installed via pip:

    Create univariate time series

    Define number of sampled points and the type of simulated time series. We use TimeSynth to generate a sinusoidal signal with Gaussian noise.

    We can inject noise in the time series via inject_outlier_ts. The noise can be regulated via the percentage of outliers (perc_outlier), the strength of the perturbation (n_std) and the minimum size of the noise perturbation (min_std):

    Visualize part of the original and perturbed time series:

    Perturbed data:

    Define Spectral Residual outlier detector

    Note that for the local convolution we pad the signal internally only on the left, following the paper's recommendation.

    The warning tells us that we need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have some data which we know contains around 10% outliers:

    Let's infer the threshold:

    Let's save the outlier detector with the updated threshold:

    We can load the same detector via load_detector:

    Detect outliers

    Predict outliers:

    Display results

    F1 score, accuracy, recall and confusion matrix:

    Plot the outlier scores of the time series vs. the outlier threshold. :

    Let's zoom in on a smaller time scale to have a clear picture:

    Time-Series Anomaly Detection Service at Microsoft
    Fourier Transform
    paper
    detector, it applies an Fisher's Exact Test (FET) to each feature. It is intended for application to
    streams, with binary data consisting of either (True, False) or (0, 1). This detector is ideal for use in a supervised setting, monitoring drift in a model's instance level accuracy (i.e. correct prediction = 0, and incorrect prediction = 1).

    Threshold configuration

    Online detectors assume the reference data is large and fixed and operate on single data points at a time (rather than batches). These data points are passed into the test-windows, and a two-sample test-statistic (in this case $F=1-\hat{p}$) between the reference data and test-window is computed at each time-step. When the test-statistic exceeds a preconfigured threshold, drift is detected. Configuration of the thresholds requires specification of the expected run-time (ERT) which specifies how many time-steps that the detector, on average, should run for in the absence of drift before making a false detection.

    In a similar manner to that proposed in this paper by Ross et al. , thresholds are configured by simulating n_bootstraps Bernoulli streams. The length of streams can be set with the t_max parameter. Since the thresholds are expected to converge after t_max = 2*max(window_sizes) - 1 time steps, we only need to simulate trajectories and estimate thresholds up to this point, and t_max is set to this value by default. Following [1], the test statistics are smoothed using an exponential moving average to remove their discreteness, allowing more precise quantiles to be targeted:

    For a window size of $W$, at time $t$ the value of the statistic $F_t$ depends on more than just the previous $W$ values. If $\lambda$, set by lam, is too small, thresholds may keep decreasing well past $2W - 1$ timesteps. To avoid this, the default lam is set to a high value of $\lambda=0.99$, meaning that discreteness is still broken, but the value of the test statistic depends (almost) solely on the last $W$ observations. If more smoothing is desired, the t_max parameter can be manually set at a larger value.

    Note

    The detector must configure thresholds for each window size and each feature. This can be a time consuming process if the number of features is high. For high-dimensional data users are recommended to apply a dimension reduction step via preprocess_fn.

    Window sizes

    Specification of test-window sizes (the detector accepts multiple windows of different size $W$) is also required, with smaller windows allowing faster response to severe drift and larger windows allowing more power to detect slight drift. Since this detector requires a window to be full to function, the ERT is measured from t = min(window_sizes)-1.

    Multivariate data

    Although this detector is primarly intended for univariate data, it can also be applied to multivariate data. In this case, the detector makes a correction similar to the Bonferroni correction used for the offline detector. Given $d$ features, the detector configures thresholds by targeting the $1-\beta$ quantile of test statistics over the simulated streams, where $\beta = 1 - (1-(1/ERT))^{(1/d)}$. For the univariate case, this simplifies to $\beta = 1/ERT$. At prediction time, drift is flagged if the test statistic of any feature stream exceed the thresholds.

    Note

    In the multivariate case, for the ERT to be accurately targeted the feature streams must be independent.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • ert: The expected run-time in the absence of drift, starting from t=min(windows_sizes).

    • window_sizes: The sizes of the sliding test-windows used to compute the test-statistics. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    Keyword arguments:

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • n_bootstraps: The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    • t_max: Length of streams to simulate when configuring thresholds. If None, this is set to 2 * max(window_sizes) - 1.

    • alternative: Defines the alternative hypothesis. Options are 'greater' (default) or 'less', corresponding to an increase or decrease in the mean of the Bernoulli stream.

    • lam: Smoothing coefficient used for exponential moving average. If heavy smoothing is applied (lam<<1), a larger t_max may be necessary in order to ensure the thresholds have converged.

    • n_features: Number of features used in the FET test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    • verbose: Whether or not to print progress during configuration.

    • input_shape: Shape of input data.

    • data_type: Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Initialized drift detector example:

    Detect Drift

    We detect data drift by sequentially calling predict on single instances x_t (no batch dimension) as they each arrive. We can return the test-statistic and the threshold by setting return_test_stat to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if any of the test-windows have drifted from the reference data and 0 otherwise.

    • time: The number of observations that have been so far passed to the detector as test instances.

    • ert: The expected run-time the detector was configured to run at in the absence of drift.

    • test_stat: FET test-statistics (1-p_val) between the reference data and the test_windows if return_test_stat equals True.

    • threshold: The values the test-statsitics are required to exceed for drift to be detected if return_test_stat equals True.

    Managing State

    The detector's state may be saved with the save_state method:

    The previously saved state may then be loaded via the load_state method:

    At any point, the state may be reset to t=0 with the reset_state method. When saving the detector with save_detector, the state will be saved, unless t=0 (see here).

    References

    [1] Ross, G.J., Tasoulis, D.K. & Adams, N.M. Sequential monitoring of a Bernoulli sequence when the pre-change parameter is unknown. Comput Stat 28, 463–479 (2013). doi: 10.1007/s00180-012-0311-7. arXiv: 1212.6020.

    source
    Fisher's Exact Test
    Ft=(1−λ)Ft−1+λFtF_t = (1-\lambda)F_{t-1} + \lambda F_tFt​=(1−λ)Ft−1​+λFt​
    offline Fisher's Exact Test
    Bernoulli

    Here, we apply model distillation to obtain harmfulness scores, by comparing the output distributions of the original model with the output distributions of the distilled model, in order to detect adversarial data, malicious data drift or data corruption. We use the following definition of harmful and harmless data points:

    • Harmful data points are defined as inputs for which the model's predictions on the uncorrupted data are correct while the model's predictions on the corrupted data are wrong.

    • Harmless data points are defined as inputs for which the model's predictions on the uncorrupted data are correct and the model's predictions on the corrupted data remain correct.

    Analogously to the adversarial AE detector, which is also part of the library, the model distillation detector picks up drift that reduces the performance of the classification model.

    The detector can be used as follows:

    • Given an input $x,$ an adversarial score $S(x)$ is computed. $S(x)$ equals the value loss function employed for distillation calculated between the original model's output and the distilled model's output on $x$.

    • If $S(x)$ is above a threshold (explicitly defined or inferred from training data), the instance is flagged as adversarial.

    Usage

    Initialize

    Parameters:

    • threshold: threshold value above which the instance is flagged as an adversarial instance.

    • distilled_model: tf.keras.Sequential instance containing the model used for distillation. Example:

    • model: the classifier as a tf.keras.Model. Example:

    • loss_type: type of loss used for distillation. Supported losses: 'kld', 'xent'.

    • temperature: Temperature used for model prediction scaling. Temperature <1 sharpens the prediction probability distribution which can be beneficial for prediction distributions with high entropy.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized detector example:

    Fit

    We then need to train the detector. The following parameters can be specified:

    • X: training batch as a numpy array.

    • loss_fn: loss function used for training. Defaults to the custom model distillation loss.

    • optimizer: optimizer used for training. Defaults to Adam with learning rate 1e-3.

    • epochs: number of training epochs.

    • batch_size: batch size used during training.

    • verbose: boolean whether to print training progress.

    • log_metric: additional metrics whose progress will be displayed if verbose equals True.

    • preprocess_fn: optional data preprocessing function applied per batch during training.

    The threshold for the adversarial / harmfulness score can be set via infer_threshold. We need to pass a batch of instances $X$ and specify what percentage of those we consider to be normal via threshold_perc. Even if we only have normal instances in the batch, it might be best to set the threshold value a bit lower (e.g. $95$%) since the model could have misclassified training instances.

    Detect

    We detect adversarial / harmful instances by simply calling predict on a batch of instances X. We can also return the instance level score by setting return_instance_score to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_adversarial: boolean whether instances are above the threshold and therefore adversarial instances. The array is of shape (batch size,).

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Image

    Harmful drift detection through model distillation on CIFAR10

    source
    Model distillation
    . The underlying Prophet model is a decomposable univariate time series model combining trend, seasonality and holiday effects. The model forecast also includes an uncertainty interval around the estimated trend component using the
    of the extrapolated model. Alternatively, full Bayesian inference can be done at the expense of increased compute. The upper and lower values of the uncertainty interval can then be used as outlier thresholds for each point in time. First, the distance from the observed value to the nearest uncertainty boundary (upper or lower) is computed. If the observation is within the boundaries, the outlier score equals the negative distance. As a result, the outlier score is the lowest when the observation equals the model prediction. If the observation is outside of the boundaries, the score equals the distance measure and the observation is flagged as an outlier. One of the main drawbacks of the method however is that you need to refit the model as new data comes in. This is undesirable for applications with high throughput and real-time detection.

    Note

    To use this detector, first install Prophet by running:

    This will install Prophet, and its major dependency PyStan. PyStan is currently only partly supported on Windows. If this detector is to be used on a Windows system, it is recommended to manually install (and test) PyStan before running the command above.

    Usage

    Initialize

    Parameters:

    • threshold: width of the uncertainty intervals of the forecast, used as outlier threshold. Equivalent to interval_width. If the instance lies outside of the uncertainty intervals, it is flagged as an outlier. If mcmc_samples equals 0, it is the uncertainty in the trend using the MAP estimate of the extrapolated model. If mcmc_samples >0, then uncertainty over all parameters is used.

    • growth: 'linear' or 'logistic' to specify a linear or logistic trend.

    • cap: growth cap in case growth equals 'logistic'.

    • holidays: pandas DataFrame with columns 'holiday' (string) and 'ds' (dates) and optionally columns 'lower_window' and 'upper_window' which specify a range of days around the date to be included as holidays.

    • holidays_prior_scale: parameter controlling the strength of the holiday components model. Higher values imply a more flexible trend, more prone to more overfitting.

    • country_holidays: include country-specific holidays via country abbreviations. The holidays for each country are provided by the holidays package in Python. A list of available countries and the country name to use is available on: https://github.com/dr-prodigy/python-holidays. Additionally, Prophet includes holidays for: Brazil (BR), Indonesia (ID), India (IN), Malaysia (MY), Vietnam (VN), Thailand (TH), Philippines (PH), Turkey (TU), Pakistan (PK), Bangladesh (BD), Egypt (EG), China (CN) and Russian (RU).

    • changepoint_prior_scale: parameter controlling the flexibility of the automatic changepoint selection. Large values will allow many changepoints, potentially leading to overfitting.

    • changepoint_range: proportion of history in which trend changepoints will be estimated. Higher values means more changepoints, potentially leading to overfitting.

    • seasonality_mode: either 'additive' or 'multiplicative'.

    • daily_seasonality: can be 'auto', True, False, or a number of Fourier terms to generate.

    • weekly_seasonality: can be 'auto', True, False, or a number of Fourier terms to generate.

    • yearly_seasonality: can be 'auto', True, False, or a number of Fourier terms to generate.

    • add_seasonality: manually add one or more seasonality components. Pass a list of dicts containing the keys 'name', 'period', 'fourier_order' (obligatory), 'prior_scale' and 'mode' (optional).

    • seasonality_prior_scale: parameter controlling the strength of the seasonality model. Larger values allow the model to fit larger seasonal fluctuations, potentially leading to overfitting.

    • uncertainty_samples: number of simulated draws used to estimate uncertainty intervals.

    • mcmc_samples: If > 0, will do full Bayesian inference with the specified number of MCMC samples. If 0, will do MAP estimation.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The fit method takes a pandas DataFrame df with as columns 'ds' containing the dates or timestamps and 'y' for the time series being investigated. The date format is ideally YYYY-MM-DD and timestamp format YYYY-MM-DD HH:MM:SS.

    Detect

    We detect outliers by simply calling predict on a DataFrame df, again with columns 'ds' and 'y' to compute the instance level outlier scores. We can also return the instance level outlier score or the raw Prophet model forecast by setting respectively return_instance_score or return_forecast to True. It is important that the dates or timestamps of the test data follow the training data.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: DataFrame with columns 'ds' containing the dates or timestamps and 'is_outlier' a boolean whether instances are above the threshold and therefore outlier instances.

    • instance_score: DataFrame with 'ds' and 'instance_score' which contains instance level scores if return_instance_score equals True.

    • forecast: DataFrame with the raw model predictions if return_forecast equals True. The DataFrame contains columns with the upper and lower boundaries ('yhat_upper' and 'yhat_lower'), the model predictions ('yhat'), and the decomposition of the prediction in the different components (trend, seasonality, holiday).

    Examples

    Time-series outlier detection using Prophet on weather data

    source
    Prophet
    this excellent paper
    MAP estimate
    from alibi_detect.od import Mahalanobis
    
    od = Mahalanobis(
        threshold=10.,
        n_components=2,
        std_clip=3,
        start_clip=100
    )
    od.fit(
        X_train,
        d_type='abdm',
        disc_perc=[25, 50, 75]
    )
    od.infer_threshold(
        X, 
        threshold_perc=95
    )
    preds = od.predict(
        X,
        return_instance_score=True
    )
    encoder_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(n_features,)),
        Dense(60, activation=tf.nn.tanh),
        Dense(30, activation=tf.nn.tanh),
        Dense(10, activation=tf.nn.tanh),
        Dense(latent_dim, activation=None)
    ])
    decoder_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(latent_dim,)),
        Dense(10, activation=tf.nn.tanh),
        Dense(30, activation=tf.nn.tanh),
        Dense(60, activation=tf.nn.tanh),
        Dense(n_features, activation=None)
    ])
    gmm_density_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(latent_dim + 2,)),
        Dense(10, activation=tf.nn.tanh),
        Dense(n_gmm, activation=tf.nn.softmax)
    ])
    from alibi_detect.od import OutlierAEGMM
    
    od = OutlierAEGMM(
        threshold=7.5,
        encoder_net=encoder_net,
        decoder_net=decoder_net,
        gmm_density_net=gmm_density_net,
        n_gmm=2
    )
    od.fit(
        X_train,
        epochs=10,
        batch_size=1024
    )
    od.infer_threshold(
        X, 
        threshold_perc=95
    )
    preds = od.predict(
        X,
        return_instance_score=True
    )
    threshold_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(seq_len, latent_dim)),
            Dense(64, activation=tf.nn.relu),
            Dense(64, activation=tf.nn.relu),
        ])
    from alibi_detect.od import OutlierSeq2Seq
    
    n_features = 2
    seq_len = 50
    
    od = OutlierSeq2Seq(n_features,
                        seq_len,
                        threshold=None,
                        threshold_net=threshold_net,
                        latent_dim=100)
    od.fit(X_train, epochs=20)
    od.infer_threshold(X, threshold_perc=np.array([95, 90]))
    preds = od.predict(X,
                       outlier_type='instance',
                       outlier_perc=100,
                       return_feature_score=True,
                       return_instance_score=True)
    !pip install seaborn
    #| tags: []
    import matplotlib
    %matplotlib inline
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix, f1_score
    
    from alibi_detect.od import IForest
    from alibi_detect.datasets import fetch_kdd
    from alibi_detect.utils.data import create_outlier_batch
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_roc
    #| tags: []
    kddcup = fetch_kdd(percent10=True)  # only load 10% of the dataset
    print(kddcup.data.shape, kddcup.target.shape)
    #| tags: []
    np.random.seed(0)
    normal_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=400000, perc_outlier=0)
    X_train, y_train = normal_batch.data.astype('float'), normal_batch.target
    print(X_train.shape, y_train.shape)
    print('{}% outliers'.format(100 * y_train.mean()))
    #| tags: []
    mean, stdev = X_train.mean(axis=0), X_train.std(axis=0)
    #| tags: []
    X_train = (X_train - mean) / stdev
    #| tags: []
    filepath = 'my_path'  # change to directory where model is saved
    detector_name = 'IForest'
    filepath = os.path.join(filepath, detector_name)
    
    # initialize outlier detector
    od = IForest(threshold=None,  # threshold for outlier score
                 n_estimators=100)
    
    # train
    od.fit(X_train)
    
    # save the trained outlier detector
    save_detector(od, filepath)
    #| tags: []
    np.random.seed(0)
    perc_outlier = 5
    threshold_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=perc_outlier)
    X_threshold, y_threshold = threshold_batch.data.astype('float'), threshold_batch.target
    X_threshold = (X_threshold - mean) / stdev
    print('{}% outliers'.format(100 * y_threshold.mean()))
    #| tags: []
    od.infer_threshold(X_threshold, threshold_perc=100-perc_outlier)
    print('New threshold: {}'.format(od.threshold))
    #| tags: []
    save_detector(od, filepath)
    #| tags: []
    np.random.seed(1)
    outlier_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=10)
    X_outlier, y_outlier = outlier_batch.data.astype('float'), outlier_batch.target
    X_outlier = (X_outlier - mean) / stdev
    print(X_outlier.shape, y_outlier.shape)
    print('{}% outliers'.format(100 * y_outlier.mean()))
    #| tags: []
    od_preds = od.predict(X_outlier, return_instance_score=True)
    #| tags: []
    labels = outlier_batch.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {:.4f}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    #| tags: []
    plot_instance_score(od_preds, y_outlier, labels, od.threshold)
    #| tags: []
    roc_data = {'IF': {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}}
    plot_roc(roc_data)
    from alibi_detect.cd import FETDrift
    
    cd = FETDrift(x_ref, p_val=0.05)
    preds = cd.predict(x, drift_type='batch', return_p_val=True)
    from alibi_detect.cd import TabularDrift
    
    cd = TabularDrift(x_ref, p_val=0.05, categories_per_feature={0: None, 3: None})
    preds = cd.predict(x, drift_type='batch', return_p_val=True, return_distance=True)
    from alibi_detect.datasets import fetch_genome
    
    (X_train, y_train), (X_val, y_val), (X_test, y_test) = fetch_genome(return_X_y=True)
    from alibi_detect.datasets import fetch_cifar10c
    
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X, y = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    from alibi_detect.datasets import fetch_attack
    
    (X_train, y_train), (X_test, y_test) = fetch_attack('cifar10', 'resnet56', 'cw', return_X_y=True)
    !pip install git+https://github.com/TimeSynth/TimeSynth.git
    !pip install seaborn
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, recall_score
    import timesynth as ts
    
    from alibi_detect.od import SpectralResidual
    from alibi_detect.utils.perturbation import inject_outlier_ts
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_ts
    n_points = 100000
    # timestamps
    time_sampler = ts.TimeSampler(stop_time=n_points // 4)
    time_samples = time_sampler.sample_regular_time(num_points=n_points)
    
    # harmonic time series with Gaussian noise
    sinusoid = ts.signals.Sinusoidal(frequency=0.25)
    white_noise = ts.noise.GaussianNoise(std=0.1)
    ts_harm = ts.TimeSeries(signal_generator=sinusoid, noise_generator=white_noise)
    samples, signals, errors = ts_harm.sample(time_samples)
    X = samples.reshape(-1, 1).astype(np.float32)
    print(X.shape)
    data = inject_outlier_ts(X, perc_outlier=10, perc_window=10, n_std=2., min_std=1.)
    X_outlier, y_outlier, labels = data.data, data.target.astype(int), data.target_names
    print(X_outlier.shape, y_outlier.shape)
    n_plot = 200
    plt.plot(time_samples[:n_plot], X[:n_plot], marker='o', markersize=4, label='sample')
    plt.plot(time_samples[:n_plot], signals[:n_plot], marker='*', markersize=4, label='signal')
    plt.plot(time_samples[:n_plot], errors[:n_plot], marker='.', markersize=4, label='noise')
    plt.xlabel('Time')
    plt.ylabel('Magnitude')
    plt.title('Original sinusoid with noise')
    plt.legend()
    plt.show();
    plt.plot(time_samples[:n_plot], X[:n_plot], marker='o', markersize=4, label='original')
    plt.plot(time_samples[:n_plot], X_outlier[:n_plot], marker='*', markersize=4, label='perturbed')
    plt.xlabel('Time')
    plt.ylabel('Magnitude')
    plt.title('Original vs. perturbed data')
    plt.legend()
    plt.show();
    od = SpectralResidual(
        threshold=None,                  # threshold for outlier score
        window_amp=20,                   # window for the average log amplitude
        window_local=20,                 # window for the average saliency map
        n_est_points=20,                 # nb of estimated points padded to the end of the sequence
        padding_amp_method='reflect',    # padding method to be used prior to each convolution over log amplitude.
        padding_local_method='reflect',  # padding method to be used prior to each convolution over saliency map.
        padding_amp_side='bilateral'     # whether to pad the amplitudes on both sides or only on one side. 
    )
    X_threshold = X_outlier[:10000, :]
    od.infer_threshold(X_threshold, time_samples[:10000], threshold_perc=90)
    print('New threshold: {:.4f}'.format(od.threshold))
    filepath = 'my_path'
    save_detector(od, filepath)
    od = load_detector(filepath)
    od_preds = od.predict(X_outlier, time_samples, return_instance_score=True)
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    acc = accuracy_score(y_outlier, y_pred)
    rec = recall_score(y_outlier, y_pred)
    print('F1 score: {} -- Accuracy: {} -- Recall: {}'.format(f1, acc, rec))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_instance_score(od_preds, y_outlier, labels, od.threshold)
    plot_feature_outlier_ts(od_preds, 
                            X_outlier, 
                            od.threshold,
                            window=(1000, 1050),
                            t=time_samples,
                            X_orig=X)
    from alibi_detect.cd import FETDriftOnline
    
    ert = 150
    window_sizes = [20,40]
    cd = FETDriftOnline(x_ref, ert, window_sizes)
    preds = cd.predict(x_t, return_test_stat=True)
    cd = FETDriftOnline(x_ref, ert, window_sizes)  # Instantiate detector at t=0
    cd.predict(x_1)  # t=1
    cd.save_state('checkpoint_t1')  # Save state at t=1
    cd.predict(x_2)  # t=2
    # Load state at t=1
    cd.load_state('checkpoint_t1')
    distilled_model = tf.keras.Sequential(
        [
            tf.keras.InputLayer(input_shape=(input_dim,)),
            tf.keras.layers.Dense(output_dim, activation=tf.nn.softmax)
        ]
    )
    inputs = tf.keras.Input(shape=(input_dim,))
    hidden = tf.keras.layers.Dense(hidden_dim)(inputs)
    outputs = tf.keras.layers.Dense(output_dim, activation=tf.nn.softmax)(hidden)
    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    from alibi_detect.ad import ModelDistillation
    
    ad = ModelDistillation(
        distilled_model=distilled_model,
        model=model,
        temperature=0.5
    )
    ad.fit(X_train, epochs=50)
    ad.infer_threshold(X_train, threshold_perc=95, batch_size=64)
    preds_detect = ad.predict(X, batch_size=64, return_instance_score=True)
    pip install alibi-detect[prophet]
    from alibi_detect.od import OutlierProphet
    
    od = OutlierProphet(
        threshold=0.9,
        growth='linear'
    )
    od.fit(df)
    preds = od.predict(
        df,
        return_instance_score=True,
        return_forecast=True
    )

    KSDrift

  • TabularDrift

  • Mahalanobis

  • SpectralResidual

  • If you are unsure which detector to use, or wish to have access to as many as possible the recommended installation is:

    pip install alibi-detect[tensorflow,prophet]

    If you would rather use pytorch backends then you can use:

    pip install alibi-detect[torch,prophet]

    However, the following detectors do not have pytorch backend support:

    • OutlierAE

    • OutlierAEGMM

    Alternatively you can install all the dependencies using (this will include both tensorflow and pytorch):

    Note

    If you wish to use the GPU version of PyTorch, or are installing on Windows, it is recommended to prior to installing alibi-detect.

    Note

    If using torch version 2.0.0 or 2.0.1 along with some versions of tensorflow you may experience hanging depending on the order you import each of these libraries. This is fixed in torch 2.1.0 onwards.

    Installation with PyTorch backend.

    The PyTorch installation is required to use the PyTorch backend for the following detectors:

    • ClassifierDrift

    • LearnedKernelDrift

    • LSDDDrift

    Note

    If you wish to use the GPU version of PyTorch, or are installing on Windows, it is recommended to prior to installing alibi-detect.

    Note

    If using torch version 2.0.0 or 2.0.1 along with some versions of tensorflow you may experience hanging depending on the order you import each of these libraries. This is fixed in torch 2.1.0 onwards.

    Installation with TensorFlow backend.

    The TensorFlow installation is required to use the TensorFlow backend for the following detectors:

    • ClassifierDrift

    • LearnedKernelDrift

    • LSDDDrift

    The TensorFlow installation is required to use the following detectors:

    Installation with KeOps backend.

    The KeOps installation is required to use the KeOps backend for the following detectors:

    • MMDDrift

    Note

    KeOps requires a C++ compiler compatible with std=c++11, for example g++ >=7 or clang++ >=8, and aCuda toolkit installation. For more detailed version requirements and testing instructions for KeOps, see the KeOps docs. Currently, the KeOps backend is only officially supported on Linux.

    Installation with Prophet support.

    Provides support for the OutlierProphet time series outlier detector.

    Auto-Encoding Gaussian Mixture Model (AEGMM)
  • Variational Auto-Encoding Gaussian Mixture Model (VAEGMM)

  • Likelihood Ratios

  • Prophet Detector

  • Spectral Residual Detector

  • Sequence-to-Sequence (Seq2Seq) Detector

  • Cramér-von Mises Drift Detector
  • Fisher's Exact Test Drift Detector

  • Kolmogorov-Smirnov Drift Detector

  • Learned Kernel MMD Drift Detector

  • Least-Squares Density Difference Drift Detector

  • Online Least-Squares Density Difference Drift Detector

  • Maximum Mean Discrepancy (MMD) Drift Detector

  • Online Maximum Mean Discrepancy Drift Detector

  • Spot-the-diff Drift Detector

  • Mixed-type Tabular Data Drift Detector

  • PyPI
    pip install alibi-detect
    ChiSquareDrift
    CVMDrift
    FETDrift
    mamba
    Alibi Detect
    KeOps
    installation options
    here
    Isolation Forest
    Mahalanobis Distance
    Auto-Encoder (AE)
    Variational Auto-Encoder (VAE)
    Adversarial AE Detector
    Model Distillation Detector
    Chi-Squared Drift Detector
    Classifier Drift Detector
    Classifier and Regressor Uncertainty Drift Detectors
    Context-aware Drift Detector
    VAE outlier detector
    Saving and loading
    types of algorithms supported
    paper but with a
    instead of a regular Auto-Encoder. The encoder compresses the data while the reconstructed instances generated by the decoder are used to create additional features based on the reconstruction error between the input and the reconstructions. These features are combined with encodings and fed into a Gaussian Mixture Model (
    ). The VAEGMM outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised or semi-supervised training is desirable since labeled data is often scarce. The sample energy of the GMM can then be used to determine whether an instance is an outlier (high sample energy) or not (low sample energy). The algorithm is suitable for tabular and image data.

    Usage

    Initialize

    Parameters:

    • threshold: threshold value for the sample energy above which the instance is flagged as an outlier.

    • latent_dim: latent dimension of the VAE.

    • n_gmm: number of components in the GMM.

    • encoder_net: tf.keras.Sequential instance containing the encoder network. Example:

    • decoder_net: tf.keras.Sequential instance containing the decoder network. Example:

    • gmm_density_net: layers for the GMM network wrapped in a tf.keras.Sequential class. Example:

    • vaegmm: instead of using a separate encoder, decoder and GMM density net, the VAEGMM can also be passed as a tf.keras.Model.

    • samples: number of samples drawn during detection for each instance to detect.

    • beta: weight on the KL-divergence loss term following the $\beta$-VAE framework. Default equals 1.

    • recon_features: function to extract features from the reconstructed instance by the decoder. Defaults to a combination of the mean squared reconstruction error and the cosine similarity between the original and reconstructed instances by the VAE.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The following parameters can be specified:

    • X: training batch as a numpy array of preferably normal data.

    • loss_fn: loss function used for training. Defaults to the custom VAEGMM loss which is a combination of the elbo loss, sample energy of the GMM and a loss term penalizing small values on the diagonals of the covariance matrices in the GMM to avoid trivial solutions. It is important to balance the loss weights below so no single loss term dominates during the optimization.

    • w_recon: weight on elbo loss term. Defaults to 1e-7.

    • w_energy: weight on sample energy loss term. Defaults to 0.1.

    • w_cov_diag: weight on covariance diagonals. Defaults to 0.005.

    • optimizer: optimizer used for training. Defaults to with learning rate 1e-4.

    • cov_elbo: dictionary with covariance matrix options in case the elbo loss function is used. Either use the full covariance matrix inferred from X (dict(cov_full=None)), only the variance (dict(cov_diag=None)) or a float representing the same standard deviation for each feature (e.g. dict(sim=.05)) which is the default.

    • epochs: number of training epochs.

    • batch_size: batch size used during training.

    • verbose: boolean whether to print training progress.

    • log_metric: additional metrics whose progress will be displayed if verbose equals True.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Detect

    We detect outliers by simply calling predict on a batch of instances X to compute the instance level sample energies. We can also return the instance level outlier score by setting return_instance_score to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances are above the threshold and therefore outlier instances. The array is of shape (batch size,).

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Tabular

    Outlier detection on KDD Cup 99

    source
    Deep Autoencoding Gaussian Mixture Model for Unsupervised Anomaly Detection
    VAE
    GMM
    Constructor
    Name
    Type
    Default
    Description

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for significance of the K-S test for each feature. If the FDR correction method is used, this corresponds to the acceptable q-value.

    x_ref_preprocessed

    bool

    False

    Methods

    feature_score

    Compute K-S scores and statistics per feature.

    Name
    Type
    Default
    Description

    x_ref

    numpy.ndarray

    Reference instances to compare distribution with.

    x

    numpy.ndarray

    Batch of instances.

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    (CVM) tests. For two empirical distributions $F(z)$ and $F_{ref}(z)$, the CVM test statistic is defined as

    where $k$ is the joint sample. The CVM test is an alternative to the Kolmogorov-Smirnov (K-S) two-sample test, which uses the maximum distance between two emphirical distributions $F(z)$ and $F_{ref}(z)$. By using the full joint sample, the CVM can exhibit greater power against shifts in higher moments, such as variance changes.

    For multivariate data, the detector applies a separate CVM test to each feature, and the p-values obtained for each feature are aggregated either via the Bonferroni or the False Discovery Rate (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur. As with other univariate detectors such as the Kolmogorov-Smirnov detector, for high-dimensional data, we typically want to reduce the dimensionality before computing the feature-wise univariate FET tests and aggregating those via the chosen correction method. See Dimension Reduction for more guidance on this.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • p_val: p-value used for significance of the CVM test. If the FDR correction method is used, this corresponds to the acceptable q-value.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to True. It is possible that it needs to be set to False if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.

    • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

    • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • correction: Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    • n_features: Number of features used in the CVM test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    • input_shape: Shape of input data.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized drift detector example:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the feature-wise p-values before the multivariate correction by setting return_p_val to True. The drift can also be detected at the feature level by setting drift_type to 'feature'. No multivariate correction will take place since we return the output of n_features univariate tests. For drift detection on all the features combined with the correction, use 'batch'. return_p_val equal to True will also return the threshold used by the detector (either for the univariate case or after the multivariate correction).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains feature-level p-values if return_p_val equals True.

    • threshold: for feature-level drift detection the threshold equals the p-value used for the significance of the CVM test. Otherwise the threshold after the multivariate correction (either bonferroni or fdr) is returned.

    • distance: feature-wise CVM statistics between the reference data and the new batch if return_distance equals True.

    Examples

    Supervised drift detection on the penguins dataset

    source
    Cramér-von Mises
    W=∑z∈k∣F(z)−Fref(z)∣2,W = \sum_{z\in k} \left| F(z) - F_{ref}(z) \right|^2,W=z∈k∑​∣F(z)−Fref​(z)∣2,
    in
    uses the likelihood ratio (LLR) between 2 generative models as the outlier score. One model is trained on the original data while the other is trained on a perturbed version of the dataset. This is based on the observation that the log likelihood for an instance under a generative model can be heavily affected by population level background statistics. The second generative model is therefore trained to capture the background statistics still present in the perturbed data while the semantic features have been erased by the perturbations.

    The perturbations are added using an independent and identical Bernoulli distribution with rate $\mu$ which substitutes a feature with one of the other possible feature values with equal probability. For images, this means for instance changing a pixel with a different pixel value randomly sampled within the $0$ to $255$ pixel range. The package also contains a PixelCNN++ implementation adapted from the official TensorFlow Probability version, and available as a standalone model in alibi_detect.models.tensorflow.pixelcnn.

    Usage

    Initialize

    Parameters:

    • threshold: outlier threshold value used for the negative likelihood ratio. Scores above the threshold are flagged as outliers.

    • model: a generative model, either as a tf.keras.Model, TensorFlow Probability distribution or built-in PixelCNN++ model.

    • model_background: optional separate model fit on the perturbed background data. If this is not specified, a copy of model will be used.

    • log_prob: if the model does not have a log_prob function like e.g. a TensorFlow Probability distribution, a function needs to be passed that evaluates the log likelihood.

    • sequential: flag whether the data is sequential or not. Used to create targets during training. Defaults to False.

    • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    Initialized outlier detector example:

    Fit

    We then need to train the 2 generative models in sequence. The following parameters can be specified:

    • X: training batch as a numpy array of preferably normal data.

    • mutate_fn: function used to create the perturbations. Defaults to an independent and identical Bernoulli distribution with rate $\mu$

    • mutate_fn_kwargs: kwargs for mutate_fn. For the default function, the mutation rate and feature range needs to be specified, e.g. dict(rate=.2, feature_range=(0,255)).

    • loss_fn: loss function used for the generative models.

    • loss_fn_kwargs: kwargs for the loss function.

    • optimizer: optimizer used for training. Defaults to with learning rate 1e-3.

    • epochs: number of training epochs.

    • batch_size: batch size used during training.

    • log_metric: additional metrics whose progress will be displayed if verbose equals True.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Detect

    We detect outliers by simply calling predict on a batch of instances X. Detection can be customized via the following parameters:

    • outlier_type: either 'instance' or 'feature'. If the outlier type equals 'instance', the outlier score at the instance level will be used to classify the instance as an outlier or not. If 'feature' is selected, outlier detection happens at the feature level (e.g. by pixel in images).

    • batch_size: batch size used for model prediction calls.

    • return_feature_score: boolean whether to return the feature level outlier scores.

    • return_instance_score: boolean whether to return the instance level outlier scores.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances or features are above the threshold and therefore outliers. If outlier_type equals 'instance', then the array is of shape (batch size,). If it equals 'feature', then the array is of shape (batch size, instance shape).

    • feature_score: contains feature level scores if return_feature_score equals True.

    • instance_score: contains instance level scores if return_instance_score equals True.

    Examples

    Image

    Likelihood Ratio Outlier Detection with PixelCNN++

    Sequential Data

    Likelihood Ratio Outlier Detection on Genomic Sequences

    source
    Ren et al. (2019)
    Likelihood Ratios for Out-of-Distribution Detection

    alibi_detect.cd.fet

    FETDrift

    Inherits from: BaseUnivariateDrift, BaseDetector, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    feature_score

    Performs Fisher exact test(s), computing the p-value per feature.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    alibi_detect.cd.cvm

    CVMDrift

    Inherits from: BaseUnivariateDrift, BaseDetector, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    feature_score

    Performs the two-sample Cramer-von Mises test(s), computing the p-value and test statistic per feature.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    Context-Aware Maximum Mean Discrepancy

    source

    Context-Aware Maximum Mean Discrepancy

    Overview

    The context-aware maximum mean discrepancy drift detector () is a kernel based method for detecting drift in a manner that can take relevant context into account. A normal drift detector detects when the distributions underlying two sets of samples ${x^0_i}{i=1}^{n_0}$ and ${x^1_i}{i=1}^{n_1}$ differ. A context-aware drift detector only detects differences that can not be attributed to a corresponding difference between sets of associated context variables ${c^0_i}{i=1}^{n_0}$ and ${c^1_i}{i=1}^{n_1}$.

    Context-aware drift detectors afford practitioners the flexibility to specify their desired context variable. It could be a transformation of the data, such as a subset of features, or an unrelated indexing quantity, such as the time or weather. Everything that the practitioner wishes to allow to change between the reference window and test window should be captured within the context variable.

    On a technical level, the method operates in a manner similar to the . However, instead of using an estimate of the squared difference between kernel mean embeddings of $X_{\text{ref}}$ and $X_{\text{test}}$ as the test statistic, we now use an estimate of the expected squared difference between the kernel of $X_{\text{ref}}|C$ and $X_{\text{test}}|C$. As well as the kernel defined on the space of data $X$ required to define the test statistic, estimating the statistic additionally requires a kernel defined on the space of the context variable $C$. For any given realisation of the test statistic an associated p-value is then computed using a .

    The detector is designed for cases where the training data contains a rich variety of contexts and individual test windows may cover a much more limited subset. It is assumed that the test contexts remain within the support of those observed in the reference set.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • c_ref: Context for the reference distribution.

    Keyword arguments:

    • backend: Both TensorFlow and PyTorch implementations of the context-aware MMD detector as well as various preprocessing steps are available. Specify the backend (tensorflow or pytorch). Defaults to tensorflow.

    • p_val: p-value used for significance of the permutation test.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data

    Additional PyTorch keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    Initialized drift detector example with the PyTorch backend:

    The same detector in TensorFlow:

    Detect Drift

    We detect data drift by simply calling predict on a batch of test or deployment instances x and contexts c. We can return the p-value and the threshold of the permutation test by setting return_p_val to True and the context-aware maximum mean discrepancy metric and threshold by setting return_distance to True. We can also set return_coupling to True which additionally returns the coupling matrices $W_\text{ref,test}$, $W_\text{ref,ref}$ and $W_\text{test,test}$. As illustrated in the examples (, ) this can provide deep insights into where the reference and test distributions are similar and where they differ.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains the p-value if return_p_val equals True.

    • threshold: p-value threshold if return_p_val

    Examples

    Text

    Time series

    alibi_detect.cd.chisquare

    ChiSquareDrift

    Inherits from: BaseUnivariateDrift, BaseDetector, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    feature_score

    Compute Chi-Squared test statistic and p-values per feature.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    alibi_detect.cd.keops.mmd

    Constants

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    MMDDriftKeops

    Inherits from: BaseMMDDrift, BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    score

    Compute the p-value resulting from a permutation test using the maximum mean discrepancy

    as a distance measure between the reference data and the data to be tested.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float]

    alibi_detect.cd.fet_online

    FETDriftOnline

    Inherits from: BaseUniDriftOnline, BaseDetector, StateMixin, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    score

    Compute the test-statistic (FET) between the reference window(s) and test window.

    If a given test-window is not yet full then a test-statistic of np.nan is returned for that window.

    Name
    Type
    Default
    Description

    Returns

    • Type: numpy.ndarray

    Variational Auto-Encoder

    source

    Variational Auto-Encoder

    Overview

    The Variational Auto-Encoder (VAE) outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised or semi-supervised training is desirable since labeled data is often scarce. The VAE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is either measured as the mean squared error (MSE) between the input and the reconstructed instance or as the probability that both the input and the reconstructed instance are generated by the same process. The algorithm is suitable for tabular and image data.

    Usage

    Initialize

    Parameters:

    • threshold: threshold value above which the instance is flagged as an outlier.

    • score_type: scoring method used to detect outliers. Currently only the default 'mse' supported.

    • latent_dim: latent dimension of the VAE.

    • decoder_net: tf.keras.Sequential instance containing the decoder network. Example:

    • vae: instead of using a separate encoder and decoder, the VAE can also be passed as a tf.keras.Model.

    • samples: number of samples drawn during detection for each instance to detect.

    • beta: weight on the KL-divergence loss term following the $\beta$- framework. Default equals 1.

    Initialized outlier detector example:

    Fit

    We then need to train the outlier detector. The following parameters can be specified:

    • X: training batch as a numpy array of preferably normal data.

    • loss_fn: loss function used for training. Defaults to the loss.

    • optimizer: optimizer used for training. Defaults to with learning rate 1e-3.

    It is often hard to find a good threshold value. If we have a batch of normal and outlier data and we know approximately the percentage of normal data in the batch, we can infer a suitable threshold:

    Detect

    We detect outliers by simply calling predict on a batch of instances X. Detection can be customized via the following parameters:

    • outlier_type: either 'instance' or 'feature'. If the outlier type equals 'instance', the outlier score at the instance level will be used to classify the instance as an outlier or not. If 'feature' is selected, outlier detection happens at the feature level (e.g. by pixel in images).

    • outlier_perc: percentage of the sorted (descending) feature level outlier scores. We might for instance want to flag an image as an outlier if at least 20% of the pixel values are on average above the threshold. In this case, we set outlier_perc to 20. The default value is 100 (using all the features).

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_outlier: boolean whether instances or features are above the threshold and therefore outliers. If outlier_type equals 'instance', then the array is of shape (batch size,). If it equals 'feature', then the array is of shape (batch size, instance shape).

    • feature_score: contains feature level scores if return_feature_score equals True.

    Examples

    Image

    Tabular

    alibi_detect.cd.cvm_online

    CVMDriftOnline

    Inherits from: BaseUniDriftOnline, BaseDetector, StateMixin, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    score

    Compute the test-statistic (CVM) between the reference window(s) and test window.

    If a given test-window is not yet full then a test-statistic of np.nan is returned for that window.

    Name
    Type
    Default
    Description

    Returns

    • Type: numpy.ndarray

    Seq2Seq time series outlier detection on ECG data

    Method

    The (Seq2Seq) outlier detector consists of 2 main building blocks: an encoder and a decoder. The encoder consists of a which processes the input sequence and initializes the decoder. The LSTM decoder then makes sequential predictions for the output sequence. In our case, the decoder aims to reconstruct the input sequence. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

    Since even for normal data the reconstruction error can be state-dependent, we add an outlier threshold estimator network to the Seq2Seq model. This network takes in the hidden state of the decoder at each timestep and predicts the estimated reconstruction error for normal data. As a result, the outlier threshold is not static and becomes a function of the model state. This is similar to , but while they train the threshold estimator separately from the Seq2Seq model with a Support-Vector Regressor, we train a neural net regression network end-to-end with the Seq2Seq model.

    Time-series outlier detection using Prophet on weather data

    Method

    The Prophet outlier detector uses the time series forecasting package explained in . The underlying Prophet model is a decomposable univariate time series model combining trend, seasonality and holiday effects. The model forecast also includes an uncertainty interval around the estimated trend component using the of the extrapolated model. Alternatively, full Bayesian inference can be done at the expense of increased compute. The upper and lower values of the uncertainty interval can then be used as outlier thresholds for each point in time. First, the distance from the observed value to the nearest uncertainty boundary (upper or lower) is computed. If the observation is within the boundaries, the outlier score equals the negative distance. As a result, the outlier score is the lowest when the observation equals the model prediction. If the observation is outside of the boundaries, the score equals the distance measure and the observation is flagged as an outlier. One of the main drawbacks of the method however is that you need to refit the model as new data comes in. This is undesirable for applications with high throughput and real-time detection.

    Note

    Model Uncertainty

    Model Uncertainty

    Overview

    Model-uncertainty drift detectors aim to directly detect drift that's likely to effect the performance of a model of interest. The approach is to test for change in the number of instances falling into regions of the input space on which the model is uncertain in its predictions. For each instance in the reference set the detector obtains the model's prediction and some associated notion of uncertainty. For example for a classifier this may be the entropy of the predicted label probabilities or for a regressor with dropout layers dropout Monte Carlo can be used to provide a notion of uncertainty. The same is done for the test set and if significant differences in uncertainty are detected (via a Kolmogorov-Smirnoff test) then drift is flagged. The detector's reference set should be disjoint from the model's training set (on which the model's confidence may be higher).

    Online Cramér-von Mises

    Online Cramér-von Mises

    Overview

    The online detector is a non-parametric method for online drift detection on continuous data. Like the

    Categorical and mixed type data drift detection on income prediction

    Method

    The drift detector applies feature-wise two-sample (K-S) tests for the continuous numerical features and tests for the categorical features. For multivariate data, the obtained p-values for each feature are aggregated either via the or the (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur.

    pip install alibi-detect[torch]
    pip install alibi-detect[tensorflow]
    pip install alibi-detect[keops]
    pip install alibi-detect[prophet]
    conda install mamba -n base -c conda-forge
    mamba install -c conda-forge alibi-detect
    import alibi_detect
    # View all the Outlier Detection (od) algorithms available
    alibi_detect.od.__all__
    ['OutlierAEGMM',
     'IForest',
     'Mahalanobis',
     'OutlierAE',
     'OutlierVAE',
     'OutlierVAEGMM',
     'OutlierProphet',
     'OutlierSeq2Seq',
     'SpectralResidual',
     'LLR']
    # View all the Adversarial Detection (ad) algorithms available
    alibi_detect.ad.__all__
    ['AdversarialAE',
    'ModelDistillation']
    # View all the Concept Drift (cd) detection algorithms available
    alibi_detect.cd.__all__
    ['ChiSquareDrift',
     'ClassifierDrift',
     'ClassifierUncertaintyDrift',
     'ContextMMDDrift',
     'CVMDrift',
     'FETDrift',
     'KSDrift',
     'LearnedKernelDrift',
     'LSDDDrift',
     'LSDDDriftOnline',
     'MMDDrift',
     'MMDDriftOnline',
     'RegressorUncertaintyDrift',
     'SpotTheDiffDrift',
     'TabularDrift']
    from alibi_detect.od import OutlierVAE
    od = OutlierVAE(
        threshold=0.1,
        encoder_net=encoder_net,
        decoder_net=decoder_net,
        latent_dim=1024
    )
    od.fit(X_train)
    preds = od.predict(X_test)
    encoder_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(n_features,)),
        Dense(60, activation=tf.nn.tanh),
        Dense(30, activation=tf.nn.tanh),
        Dense(10, activation=tf.nn.tanh),
        Dense(latent_dim, activation=None)
    ])
    decoder_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(latent_dim,)),
        Dense(10, activation=tf.nn.tanh),
        Dense(30, activation=tf.nn.tanh),
        Dense(60, activation=tf.nn.tanh),
        Dense(n_features, activation=None)
    ])
    gmm_density_net = tf.keras.Sequential(
    [
        InputLayer(input_shape=(latent_dim + 2,)),
        Dense(10, activation=tf.nn.tanh),
        Dense(n_gmm, activation=tf.nn.softmax)
    ])
    from alibi_detect.od import OutlierVAEGMM
    
    od = OutlierVAEGMM(
        threshold=7.5,
        encoder_net=encoder_net,
        decoder_net=decoder_net,
        gmm_density_net=gmm_density_net,
        latent_dim=4,
        n_gmm=2,
        samples=10
    )
    od.fit(
        X_train,
        epochs=10,
        batch_size=1024
    )
    od.infer_threshold(
        X, 
        threshold_perc=95
    )
    preds = od.predict(
        X,
        return_instance_score=True
    )
    KSDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, correction: str = 'bonferroni', alternative: str = 'two-sided', n_features: Optional[int] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    feature_score(x_ref: numpy.ndarray, x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]
    from alibi_detect.cd import CVMDrift
    
    cd = CVMDrift(x_ref, p_val=0.05)
    preds = cd.predict(x, drift_type='batch', return_p_val=True, return_distance=True)
    from alibi_detect.od import LLR
    from alibi_detect.models.tensorflow import PixelCNN
    
    image_shape = (28, 28, 1)
    model = PixelCNN(image_shape)
    od = LLR(threshold=-100, model=model)
    od.fit(X_train, epochs=10, batch_size=32)
    od.infer_threshold(X, threshold_perc=95, batch_size=32)
    preds = od.predict(X, outlier_type='instance', batch_size=32)
    logger: logging.Logger = <Logger alibi_detect.cd.keops.mmd (WARNING)>

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

    correction

    str

    'bonferroni'

    Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    alternative

    str

    'two-sided'

    Defines the alternative hypothesis. Options are 'two-sided', 'less' or 'greater'.

    n_features

    Optional[int]

    None

    Number of features used in the K-S test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    OutlierVAE

  • OutlierVAEGMM

  • AdversarialAE

  • ModelDistillation

  • LLR
    OutlierSeq2Seq
    OutlierVAE
    OutlierVAEGMM
    AdversarialAE
    ModelDistillation
    pip install alibi-detect[all]
    install and test PyTorch
    LSDDDriftOnline
    MMDDrift
    MMDDriftOnline
    SpotTheDiffDrift
    ContextMMDDrift
    install and test PyTorch
    LSDDDriftOnline
    MMDDrift
    MMDDriftOnline
    SpotTheDiffDrift
    ContextMMDDrift
    OutlierAE
    OutlierAEGMM
    LLR
    OutlierSeq2Seq
    Adam
    reservoir sampling
    Adam

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    correction

    str

    'bonferroni'

    Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    alternative

    str

    'greater'

    Defines the alternative hypothesis. Options are 'greater', 'less' or two-sided. These correspond to an increase, decrease, or any change in the mean of the Bernoulli data.

    n_features

    Optional[int]

    None

    Number of features used in the FET test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution. Data must consist of either [True, False]'s, or [0, 1]'s.

    p_val

    float

    0.05

    p-value used for significance of the FET test. If the FDR correction method is used, this corresponds to the acceptable q-value.

    x_ref_preprocessed

    bool

    False

    x_ref

    numpy.ndarray

    Reference instances to compare distribution with. Data must consist of either [True, False]'s, or [0, 1]'s.

    x

    numpy.ndarray

    Batch of instances. Data must consist of either [True, False]'s, or [0, 1]'s.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    correction

    str

    'bonferroni'

    Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    n_features

    Optional[int]

    None

    Number of features used in the CVM test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for significance of the CVM test. If the FDR correction method is used, this corresponds to the acceptable q-value.

    x_ref_preprocessed

    bool

    False

    x_ref

    numpy.ndarray

    Reference instances to compare distribution with.

    x

    numpy.ndarray

    Batch of instances.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    x_ref
    at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to
    True
    . It is possible that it needs to be set to
    False
    if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.
  • update_ref: Reference data can optionally be updated to the last N instances seen by the detector. The parameter should be passed as a dictionary {'last': N}.

  • preprocess_fn: Function to preprocess the data (x_ref and x) before computing the data drift metrics. Typically a dimensionality reduction technique. NOTE: Preprocessing is not applied to the context data.

  • x_kernel: Kernel defined on the data x_*. Defaults to a Gaussian RBF kernel (from alibi_detect.utils.pytorch import GaussianRBF or from alibi_detect.utils.tensorflow import GaussianRBF dependent on the backend used).

  • c_kernel: Kernel defined on the context c_*. Defaults to a Gaussian RBF kernel (from alibi_detect.utils.pytorch import GaussianRBF or from alibi_detect.utils.tensorflow import GaussianRBF dependent on the backend used).

  • n_permutations: Number of permutations used in the conditional permutation test.

  • prop_c_held: Proportion of contexts held out to condition on.

  • n_folds: Number of cross-validation folds used when tuning the regularisation parameters.

  • batch_size: If not None, then compute batches of MMDs at a time rather than all at once which could lead to memory issues.

  • input_shape: Optionally pass the shape of the input data.

  • data_type: can specify data type added to the metadata. E.g. 'tabular' or 'image'.

  • verbose: Whether or not to print progress during configuration.

  • equals
    True
    .
  • distance: conditional MMD^2 metric between the reference data and the new batch if return_distance equals True.

  • distance_threshold: conditional MMD^2 metric value from the permutation test which corresponds to the the p-value threshold.

  • coupling_xx: coupling matrix $W_\text{ref,ref}$ for the reference data.

  • coupling_yy: coupling matrix $W_\text{test,test}$ for the test data.

  • coupling_xy: coupling matrix $W_\text{ref,test}$ between the reference and test data.

  • Cobb and Van Looveren, 2022
    maximum mean discrepancy detector
    conditional mean embeddings
    conditional permutation test
    text
    ECGs
    Context-aware drift detection on news articles
    Context-aware drift detection on ECGs

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

    correction

    str

    'bonferroni'

    Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    n_features

    Optional[int]

    None

    Number of features used in the Chi-Squared test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for significance of the Chi-Squared test for each feature. If the FDR correction method is used, this corresponds to the acceptable q-value.

    categories_per_feature

    Optional[Dict[int, int]]

    None

    x_ref

    numpy.ndarray

    Reference instances to compare distribution with.

    x

    numpy.ndarray

    Batch of instances.

    Optional dictionary with as keys the feature column index and as values the number of possible categorical values for that feature or a list with the possible values. If you know how many categories are present for a given feature you could pass this in the categories_per_feature dict in the Dict[int, int] format, e.g. {0: 3, 3: 2}. If you pass N categories this will assume the possible values for the feature are [0, ..., N-1]. You can also explicitly pass the possible categories in the Dict[int, List[int]] format, e.g. {0: [0, 1, 2], 3: [0, 55]}. Note that the categories can be arbitrary int values. If it is not specified, categories_per_feature is inferred from x_ref.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    kernel

    Callable

    <class 'alibi_detect.utils.keops.kernels.GaussianRBF'>

    Kernel used for the MMD computation, defaults to Gaussian RBF kernel.

    sigma

    Optional[numpy.ndarray]

    None

    Optionally set the GaussianRBF kernel bandwidth. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths.

    configure_kernel_from_x_ref

    bool

    True

    Whether to already configure the kernel bandwidth from the reference data.

    n_permutations

    int

    100

    Number of permutations used in the permutation test.

    batch_size_permutations

    int

    1000000

    KeOps computes the n_permutations of the MMD^2 statistics in chunks of batch_size_permutations.

    device

    Union[Literal[cuda, gpu, cpu], torch.device, None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for the significance of the permutation test.

    x_ref_preprocessed

    bool

    False

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    n_bootstraps

    int

    10000

    The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    t_max

    Optional[int]

    None

    Length of the streams to simulate when configuring thresholds. If None, this is set to 2 * max(window_sizes) - 1.

    alternative

    str

    'greater'

    Defines the alternative hypothesis. Options are 'greater' or 'less', which correspond to an increase or decrease in the mean of the Bernoulli stream.

    lam

    float

    0.99

    Smoothing coefficient used for exponential moving average.

    n_features

    Optional[int]

    None

    Number of features used in the statistical test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    verbose

    bool

    True

    Whether or not to print progress during configuration.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    ert

    float

    The expected run-time (ERT) in the absence of drift. For the univariate detectors, the ERT is defined as the expected run-time after the smallest window is full i.e. the run-time from t=min(windows_sizes).

    window_sizes

    List[int]

    x_t

    Union[numpy.ndarray, typing.Any]

    A single instance.

    window sizes for the sliding test-windows used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    encoder_net: tf.keras.Sequential instance containing the encoder network. Example:

    data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

    cov_elbo: dictionary with covariance matrix options in case the elbo loss function is used. Either use the full covariance matrix inferred from X (dict(cov_full=None)), only the variance (dict(cov_diag=None)) or a float representing the same standard deviation for each feature (e.g. dict(sim=.05)) which is the default.

  • epochs: number of training epochs.

  • batch_size: batch size used during training.

  • verbose: boolean whether to print training progress.

  • log_metric: additional metrics whose progress will be displayed if verbose equals True.

  • return_feature_score: boolean whether to return the feature level outlier scores.

  • return_instance_score: boolean whether to return the instance level outlier scores.

  • instance_score: contains instance level scores if return_instance_score equals True.
    VAE
    elbo
    Adam
    Outlier detection on CIFAR10
    Outlier detection on KDD Cup 99
    Outlier detection on Adult dataset

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    n_bootstraps

    int

    10000

    The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    batch_size

    int

    64

    The maximum number of bootstrap simulations to compute in each batch when configuring thresholds. A smaller batch size reduces memory requirements, but can result in a longer configuration run time.

    n_features

    Optional[int]

    None

    Number of features used in the statistical test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    verbose

    bool

    True

    Whether or not to print progress during configuration.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    ert

    float

    The expected run-time (ERT) in the absence of drift. For the univariate detectors, the ERT is defined as the expected run-time after the smallest window is full i.e. the run-time from t=min(windows_sizes).

    window_sizes

    List[int]

    x_t

    Union[numpy.ndarray, typing.Any]

    A single instance.

    window sizes for the sliding test-windows used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    The detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The Seq2Seq outlier detector is suitable for both univariate and multivariate time series.

    Dataset

    The outlier detector needs to spot anomalies in electrocardiograms (ECG's). The dataset contains 5000 ECG's, originally obtained from Physionet under the name BIDMC Congestive Heart Failure Database(chfdb), record chf07. The data has been pre-processed in 2 steps: first each heartbeat is extracted, and then each beat is made equal length via interpolation. The data is labeled and contains 5 classes. The first class which contains almost 60% of the observations is seen as normal while the others are outliers. The detector is trained on heartbeats from the first class and needs to flag the other classes as anomalies.

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load dataset

    Flip train and test data because there are only 500 ECG's in the original training set and 4500 in the test set:

    Since we treat the first class as the normal, inlier data and the rest of X_train as outliers, we need to adjust the training (inlier) data and the labels of the test set.

    Some of the outliers in X_train are used in combination with some of the inlier instances to infer the threshold level:

    Apply min-max scaling between 0 and 1 to the observations using the inlier data:

    Reshape the observations to (batch size, sequence length, features) for the detector:

    We can now visualize scaled instances from each class:

    Load or define Seq2Seq outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found here. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    Let's inspect how well the sequence-to-sequence model can predict the ECG's of the inlier and outlier classes. The predictions in the charts below are made on ECG's from the test set:

    It is clear that the model can reconstruct the inlier class but struggles with the outliers.

    If we trained a model from scratch, the warning thrown when we initialized the model tells us that we need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a time series of instances and specify what percentage of those we consider to be normal via threshold_perc, equal to the percentage of Class 1 in X_threshold. The outlier_perc parameter defines the percentage of features used to define the outlier threshold. In this example, the number of features considered per instance equals 140 (1 for each timestep). We set the outlier_perc at 95, which means that we will use the 95% features with highest reconstruction error, adjusted for by the threshold estimate.

    Let's save the outlier detector with the updated threshold:

    We can load the same detector via load_detector:

    Detect outliers

    Display results

    F1 score, accuracy, recall and confusion matrix:

    We can also plot the ROC curve based on the instance level outlier scores:

    Sequence-to-Sequence
    Bidirectional
    LSTM
    Park et al. (2017)

    To use this detector, first install Prophet by running:

    This will install Prophet, and its major dependency PyStan. PyStan is currently only partly supported on Windows. If this detector is to be used on a Windows system, it is recommended to manually install (and test) PyStan before running the command above.

    Dataset

    The example uses a weather time series dataset recorded by the Max-Planck-Institute for Biogeochemistry. The dataset contains 14 different features such as air temperature, atmospheric pressure, and humidity. These were collected every 10 minutes, beginning in 2003. Like the TensorFlow time-series tutorial, we only use data collected between 2009 and 2016.

    Load dataset

    Select subset to test Prophet model on:

    Prophet model expects a DataFrame with 2 columns: one named ds with the timestamps and one named y with the time series to be evaluated. We will just look at the temperature data:

    Define outlier detector

    We train an outlier detector from scratch:

    Please check out the documentation as well as the original Prophet documentation on how to customize the Prophet-based outlier detector and add seasonalities, holidays, opt for a saturating logistic growth model or apply parameter regularization.

    Predict outliers on test data

    Define the test data. It is important that the timestamps of the test data follow the training data. We check this below by comparing the first few rows of the test DataFrame with the last few of the training DataFrame:

    Predict outliers on test data:

    Visualize results

    We can first visualize our predictions with Prophet's built in plotting functionality. This also allows us to include historical predictions:

    We can also plot the breakdown of the different components in the forecast. Since we did not do full Bayesian inference with mcmc_samples, the uncertaintly intervals of the forecast are determined by the MAP estimate of the extrapolated trend.

    It is clear that the further we predict in the future, the wider the uncertainty intervals which determine the outlier threshold.

    Let's overlay the actual data with the upper and lower outlier thresholds predictions and check where we predicted outliers:

    Outlier scores and predictions:

    The outlier scores naturally trend down as uncertainty increases when we predict further in the future.

    Let's look at some individual outliers:

    Prophet
    this excellent paper
    MAP estimate

    ClassifierUncertaintyDrift should be used with classification models whereas RegressorUncertaintyDrift should be used with regression models. They are used in much the same way.

    By default ClassifierUncertaintyDrift uses uncertainty_type='entropy' as the notion of uncertainty for classifier predictions and a Kolmogorov-Smirnov two-sample test is performed on these continuous values. However uncertainty_type='margin' can also be specified to deem the classifier's prediction uncertain if they fall within a margin (e.g. in [0.45,0.55] for binary classifier probabilities) (similar to Sethi and Kantardzic (2017)) and a Chi-Squared two-sample test is performed on these 0-1 flags of uncertainty.

    By default RegressorUncertaintyDrift uses uncertainty_type='mc_dropout' and assumes a PyTorch or TensorFlow model with dropout layers as the regressor. This evaluates the model under multiple dropout configurations and uses the variation as the notion of uncertainty. Alternatively a model that outputs (for each instance) a vector of independent model predictions can be passed and uncertainty_type='ensemble' can be specified. Again the variation is taken as the notion of uncertainty and in both cases a Kolmogorov-Smirnov two-sample test is performed on the continuous notions of uncertainty.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution. Should be disjoint from the model's training set

    • model: The model of interest whose performance we'd like to remain constant.

    Keyword arguments:

    • p_val: p-value used for the significance of the test.

    • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

    • input_shape: Optionally pass the shape of the input data.

    • data_type: Optionally specify the data type (e.g. tabular, image or time-series). Added to metadata.

    ClassifierUncertaintyDrift-specific keyword arguments:

    • preds_type: Type of prediction output by the model. Options are 'probs' (in [0,1]) or 'logits' (in [-inf,inf]).

    • uncertainty_type: Method for determining the model's uncertainty for a given instance. Options are 'entropy' or 'margin'.

    • margin_width: Width of the margin if uncertainty_type = 'margin'. The model is considered uncertain on an instance if the highest two class probabilities it assigns to the instance differ by less than this.

    RegressorUncertaintyDrift-specific keyword arguments:

    • uncertainty_type: Method for determining the model's uncertainty for a given instance. Options are 'mc_dropout' or 'ensemble'. For the former the model should have dropout layers and output a scalar per instance. For the latter the model should output a vector of predictions per instance.

    • n_evals: The number of times to evaluate the model under different dropout configurations. Only relavent when using the 'mc_dropout' uncertainty type.

    Additional arguments if batch prediction required:

    • backend: Framework that was used to define model. Options are 'tensorflow' or 'pytorch'.

    • batch_size: Batch size to use to evaluate model. Defaults to 32.

    • device: Device type to use. The default None tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu' or 'cpu'. Only relevant for 'pytorch' backend.

    Additional arguments for NLP models

    • tokenizer: Tokenizer to use before passing data to model.

    • max_len: Max length to be used by tokenizer.

    Examples

    Drift detector for a TensorFlow classifier outputting probabilities:

    Drift detector for a PyTorch regressor (with dropout layers) outputting scalars:

    Note that for the PyTorch RegressorUncertaintyDrift detector the dropout layers need to be defined within the nn.Module init to be able to set them to train mode when computing the uncertainty estimates, e.g.:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. return_p_val equal to True will also return the p-value of the test and return_distance equal to True will return the test-statistic.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • threshold: the user-defined threshold defining the significance of the test.

    • p_val: the p-value of the test if return_p_val equals True.

    • distance: the test-statistic if return_distance equals True.

    Examples

    Graph

    Drift detection on molecular graphs

    Image and tabular

    Drift detection on CIFAR10 and Wine Quality Data Set

    source
    detector, it applies a univariate Cramér-von Mises (CVM) test to each feature. This detector is an adaptation of that proposed in
    by Ross et al. .

    Warning

    This detector is multi-threaded, with Numba used to parallelise over the simulated streams. There is a known issue on MacOS, where Numba's default OpenMP threading layer causes segfaults. A workaround is to use the slightly less performant workqueue threading layer on MacOS by setting the NUMBA_THREADING_LAYER enviroment variable or running:

    Threshold configuration

    Online detectors assume the reference data is large and fixed and operate on single data points at a time (rather than batches). These data points are passed into the test-windows, and a two-sample test-statistic between the reference data and test-window is computed at each time-step. When the test-statistic exceeds a preconfigured threshold, drift is detected. Configuration of the thresholds requires specification of the expected run-time (ERT) which specifies how many time-steps that the detector, on average, should run for in the absence of drift before making a false detection. Thresholds are then configured to target this ERT by simulating n_bootstraps number of streams of length t_max = 2*max(window_sizes) - 1. Conveniently, the non-parametric nature of the detector means that thresholds depend only on $M$, the length of the reference data set. Therefore, for multivariate data, configuration is only as costly as the univariate case.

    Note

    In order to reduce the memory requirements of the threshold configuration process, streams are simulated in batches of size $N_{batch}$, set with the batch_size keyword argument. However, the memory requirements still scale with $O(M^2N_{batch})$. If configuration is requiring too much memory (or time), then consider subsampling the reference data. The quadratic growth of the cost with respect to the number of reference instances $M$, combined with the diminishing increase in test power, often makes this a worthwhile tradeoff.

    Window sizes

    Specification of test-window sizes (the detector accepts multiple windows of different size $W$) is also required, with smaller windows allowing faster response to severe drift and larger windows allowing more power to detect slight drift. Since this detector requires the windows to be full to function, the ERT is measured from t = min(window_sizes)-1.

    Multivariate data

    Although this detector is primarly intended for univariate data, it can also be applied to multivariate data. In this case, the detector makes a correction similar to the Bonferroni correction used for the offline detector. Given $d$ features, the detector configures thresholds by targeting the $1-\beta$ quantile of test statistics over the simulated streams, where $\beta = 1 - (1-(1/ERT))^{(1/d)}$. For the univariate case, this simplifies to $\beta = 1/ERT$. At prediction time, drift is flagged if the test statistic of any feature stream exceed the thresholds.

    Note

    In the multivariate case, for the ERT's upper bound to be accurate, the feature streams must be independent. Regardless of independence, the ERT will still be properly lower bounded.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • ert: The expected run-time in the absence of drift, starting from t=min(windows_sizes).

    • window_sizes: The sizes of the sliding test-windows used to compute the test-statistics. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    Keyword arguments:

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • n_bootstraps: The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    • batch_size: The maximum number of bootstrap simulations to compute in each batch when configuring thresholds. A smaller batch size reduces memory requirements, but can result in a longer configuration run time.

    • n_features: Number of features used in the FET test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    • verbose: Whether or not to print progress during configuration.

    • input_shape: Shape of input data.

    • data_type: Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Initialized drift detector example:

    Detect Drift

    We detect data drift by sequentially calling predict on single instances x_t (no batch dimension) as they each arrive. We can return the test-statistic and the threshold by setting return_test_stat to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if any of the test-windows have drifted from the reference data and 0 otherwise.

    • time: The number of observations that have been so far passed to the detector as test instances.

    • ert: The expected run-time the detector was configured to run at in the absence of drift.

    • test_stat: CVM test-statistics between the reference data and the test_windows if return_test_stat equals True.

    • threshold: The values the test-statsitics are required to exceed for drift to be detected if return_test_stat equals True.

    Managing State

    The detector's state may be saved with the save_state method:

    The previously saved state may then be loaded via the load_state method:

    At any point, the state may be reset to t=0 with the reset_state method. When saving the detector with save_detector, the state will be saved, unless t=0 (see here).

    source
    Cramér-von Mises
    offline Cramér-von Mises
    this paper
    Dataset

    The instances contain a person's characteristics like age, marital status or education while the label represents whether the person makes more or less than $50k per year. The dataset consists of a mixture of numerical and categorical features. It is fetched using the Alibi library, which can be installed with pip:

    Load income prediction dataset

    The fetch_adult function returns a Bunch object containing the instances, the targets, the feature names and a dictionary with as keys the column indices of the categorical features and as values the possible categories for each categorical variable.

    We split the data in a reference set and 2 test sets on which we test the data drift:

    Detect drift

    We need to provide the drift detector with the columns which contain categorical features so it knows which features require the Chi-Squared and which ones require the K-S univariate test. We can either provide a dict with as keys the column indices and as values the number of possible categories or just set the values to None and let the detector infer the number of categories from the reference data as in the example below:

    Initialize the detector:

    We can also save/load an initialised detector:

    Now we can check whether the 2 test sets are drifting from the reference data:

    Let's take a closer look at each of the features. The preds dictionary also returns the K-S or Chi-Squared test statistics and p-value for each feature:

    None of the feature-level p-values are below the threshold:

    If you are interested in individual feature-wise drift, this is also possible:

    What about the second test set?

    We can again investigate the individual features:

    It seems like there is little divergence in the distributions of the features between the reference and test set. Let's visualize this:

    Categorical data drift

    While the TabularDrift detector works fine with numerical or categorical features only, we can also directly use a categorical drift detector. In this case, we don't need to specify the categorical feature columns. First we construct a categorical-only dataset and then use the ChiSquareDrift detector:

    Kolmogorov-Smirnov
    Chi-Squared
    Bonferroni
    False Discovery Rate
    FETDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, correction: str = 'bonferroni', alternative: str = 'greater', n_features: Optional[int] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    feature_score(x_ref: numpy.ndarray, x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]
    CVMDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, correction: str = 'bonferroni', n_features: Optional[int] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    feature_score(x_ref: numpy.ndarray, x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]
    from alibi_detect.cd import ContextMMDDrift
    
    cd = ContextMMDDrift(x_ref, c_ref, p_val=.05, backend='pytorch')
    from alibi_detect.cd import ContextMMDDrift
    
    cd = ContextMMDDrift(x_ref, c_ref, p_val=.05, backend='tensorflow')
    preds = cd.predict(x, c, return_p_val=True, return_distance=True, return_coupling=True)
    ChiSquareDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, categories_per_feature: Optional[Dict[int, int]] = None, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, correction: str = 'bonferroni', n_features: Optional[int] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    feature_score(x_ref: numpy.ndarray, x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]
    MMDDriftKeops(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, kernel: Callable = <class 'alibi_detect.utils.keops.kernels.GaussianRBF'>, sigma: Optional[numpy.ndarray] = None, configure_kernel_from_x_ref: bool = True, n_permutations: int = 100, batch_size_permutations: int = 1000000, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    FETDriftOnline(self, x_ref: Union[numpy.ndarray, list], ert: float, window_sizes: List[int], preprocess_fn: Optional[Callable] = None, x_ref_preprocessed: bool = False, n_bootstraps: int = 10000, t_max: Optional[int] = None, alternative: str = 'greater', lam: float = 0.99, n_features: Optional[int] = None, verbose: bool = True, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    score(x_t: Union[numpy.ndarray, typing.Any]) -> numpy.ndarray
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu)
      ])
    decoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(latent_dim,)),
          Dense(4*4*128),
          Reshape(target_shape=(4, 4, 128)),
          Conv2DTranspose(256, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2DTranspose(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2DTranspose(3, 4, strides=2, padding='same', activation='sigmoid')
      ])
    from alibi_detect.od import OutlierVAE
    
    od = OutlierVAE(
        threshold=0.1,
        encoder_net=encoder_net,
        decoder_net=decoder_net,
        latent_dim=1024,
        samples=10
    )
    od.fit(
        X_train,
        epochs=50
    )
    od.infer_threshold(
        X, 
        threshold_perc=95
    )
    preds = od.predict(
        X,
        outlier_type='instance',
        outlier_perc=75,
        return_feature_score=True,
        return_instance_score=True
    )
    CVMDriftOnline(self, x_ref: Union[numpy.ndarray, list], ert: float, window_sizes: List[int], preprocess_fn: Optional[Callable] = None, x_ref_preprocessed: bool = False, n_bootstraps: int = 10000, batch_size: int = 64, n_features: Optional[int] = None, verbose: bool = True, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    score(x_t: Union[numpy.ndarray, typing.Any]) -> numpy.ndarray
    !pip install seaborn
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np
    import os
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, precision_score, recall_score
    
    from alibi_detect.od import OutlierSeq2Seq
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.datasets import fetch_ecg
    from alibi_detect.utils.visualize import plot_roc
    (X_test, y_test), (X_train, y_train) = fetch_ecg(return_X_y=True)
    print(X_train.shape, y_train.shape)
    print(X_test.shape, y_test.shape)
    inlier_idx = np.where(y_train == 1)[0]
    X_inlier, y_inlier = X_train[inlier_idx], np.zeros_like(y_train[inlier_idx])
    outlier_idx = np.where(y_train != 1)[0]
    X_outlier, y_outlier = X_train[outlier_idx], y_train[outlier_idx]
    y_test[y_test == 1] = 0  # class 1 represent the inliers
    y_test[y_test != 0] = 1
    print(X_inlier.shape, X_outlier.shape)
    n_threshold = 1000
    perc_inlier = 60
    n_inlier = int(perc_inlier * .01 * n_threshold)
    n_outlier = int((100 - perc_inlier) * .01 * n_threshold)
    idx_thr_in = np.random.choice(X_inlier.shape[0], n_inlier, replace=False)
    idx_thr_out = np.random.choice(X_outlier.shape[0], n_outlier, replace=False)
    X_threshold = np.concatenate([X_inlier[idx_thr_in], X_outlier[idx_thr_out]], axis=0)
    y_threshold = np.zeros(n_threshold).astype(int)
    y_threshold[-n_outlier:] = 1
    print(X_threshold.shape, y_threshold.shape)
    xmin, xmax = X_inlier.min(), X_inlier.max()
    rng = (0, 1)
    X_inlier = ((X_inlier - xmin) / (xmax - xmin)) * (rng[1] - rng[0]) + rng[0]
    X_threshold = ((X_threshold - xmin) / (xmax - xmin)) * (rng[1] - rng[0]) + rng[0]
    X_test = ((X_test - xmin) / (xmax - xmin)) * (rng[1] - rng[0]) + rng[0]
    X_outlier = ((X_outlier - xmin) / (xmax - xmin)) * (rng[1] - rng[0]) + rng[0]
    print('Inlier: min {:.2f} --- max {:.2f}'.format(X_inlier.min(), X_inlier.max()))
    print('Threshold: min {:.2f} --- max {:.2f}'.format(X_threshold.min(), X_threshold.max()))
    print('Test: min {:.2f} --- max {:.2f}'.format(X_test.min(), X_test.max()))
    shape = (-1, X_inlier.shape[1], 1)
    X_inlier = X_inlier.reshape(shape)
    X_threshold = X_threshold.reshape(shape)
    X_test = X_test.reshape(shape)
    X_outlier = X_outlier.reshape(shape)
    print(X_inlier.shape, X_threshold.shape, X_test.shape)
    idx_plt = [np.where(y_outlier == i)[0][0] for i in list(np.unique(y_outlier))]
    X_plt = np.concatenate([X_inlier[0:1], X_outlier[idx_plt]], axis=0)
    
    for i in range(X_plt.shape[0]):
        plt.plot(X_plt[i], label='Class ' + str(i+1))
    plt.title('ECGs of Different Classes')
    plt.xlabel('Time step')
    plt.legend()
    plt.show()
    load_outlier_detector = True
    #| scrolled: true
    filepath = 'my_path'  # change to (absolute) directory where model is downloaded
    detector_type = 'outlier'
    dataset = 'ecg'
    detector_name = 'OutlierSeq2Seq'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
    
        # initialize outlier detector
        od = OutlierSeq2Seq(1,
                            X_inlier.shape[1],  # sequence length
                            threshold=None,
                            latent_dim=40)
    
        # train
        od.fit(X_inlier,
               epochs=100,
               verbose=False)
    
        # save the trained outlier detector
        save_detector(od, filepath)
    ecg_pred = od.seq2seq.decode_seq(X_test)[0]
    i_normal = np.where(y_test == 0)[0][0]
    plt.plot(ecg_pred[i_normal], label='Prediction')
    plt.plot(X_test[i_normal], label='Original')
    plt.title('Predicted vs. Original ECG of Inlier Class 1')
    plt.legend()
    plt.show()
    
    i_outlier = np.where(y_test == 1)[0][0]
    plt.plot(ecg_pred[i_outlier], label='Prediction')
    plt.plot(X_test[i_outlier], label='Original')
    plt.title('Predicted vs. Original ECG of Outlier')
    plt.legend()
    plt.show()
    od.infer_threshold(X_threshold, outlier_perc=95, threshold_perc=perc_inlier)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    od = load_detector(filepath)
    od_preds = od.predict(X_test,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    y_pred = od_preds['data']['is_outlier']
    labels = ['normal', 'outlier']
    f1 = f1_score(y_test, y_pred)
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    print('F1 score: {:.3f} -- Accuracy: {:.3f} -- Precision: {:.3f} -- Recall: {:.3f}'.format(f1, acc, prec, rec))
    cm = confusion_matrix(y_test, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    roc_data = {'S2S': {'scores': od_preds['data']['instance_score'], 'labels': y_test}}
    plot_roc(roc_data)
    pip install alibi-detect[prophet]
    #| tags: []
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import pandas as pd
    import tensorflow as tf
    
    from alibi_detect.od import OutlierProphet
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    #| tags: []
    zip_path = tf.keras.utils.get_file(
        origin='https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip',
        fname='jena_climate_2009_2016.csv.zip',
        extract=True
    )
    csv_path, _ = os.path.splitext(zip_path)
    df = pd.read_csv(csv_path)
    df['Date Time'] = pd.to_datetime(df['Date Time'], format='%d.%m.%Y %H:%M:%S')
    print(df.shape)
    df.head()
    #| tags: []
    n_prophet = 10000
    #| tags: []
    d = {'ds': df['Date Time'][:n_prophet], 'y': df['T (degC)'][:n_prophet]}
    df_T = pd.DataFrame(data=d)
    print(df_T.shape)
    df_T.head()
    #| tags: []
    plt.plot(df_T['ds'], df_T['y'])
    plt.title('T (in °C) over time')
    plt.xlabel('Time')
    plt.ylabel('T (in °C)')
    plt.show()
    #| tags: []
    filepath = 'my_path'  # change to directory where model is saved
    detector_name = 'OutlierProphet'
    filepath = os.path.join(filepath, detector_name)    
    
    # initialize, fit and save outlier detector
    od = OutlierProphet(threshold=.9)
    od.fit(df_T)
    save_detector(od, filepath)
    #| tags: []
    n_periods = 1000
    d = {'ds': df['Date Time'][n_prophet:n_prophet+n_periods], 
         'y': df['T (degC)'][n_prophet:n_prophet+n_periods]}
    df_T_test = pd.DataFrame(data=d)
    df_T_test.head()
    #| tags: []
    df_T.tail()
    #| tags: []
    od_preds = od.predict(
        df_T_test, 
        return_instance_score=True,
        return_forecast=True
    )
    #| tags: []
    future = od.model.make_future_dataframe(periods=n_periods, freq='10T', include_history=True)
    forecast = od.model.predict(future)
    fig = od.model.plot(forecast)
    #| tags: []
    fig = od.model.plot_components(forecast)
    #| tags: []
    forecast['y'] = df['T (degC)'][:n_prophet+n_periods]
    #| tags: []
    pd.plotting.register_matplotlib_converters()  # needed to plot timestamps
    forecast[-n_periods:].plot(x='ds', y=['y', 'yhat', 'yhat_upper', 'yhat_lower'])
    plt.title('Predicted T (in °C) over time')
    plt.xlabel('Time')
    plt.ylabel('T (in °C)')
    plt.show()
    #| tags: []
    od_preds['data']['forecast']['threshold'] = np.zeros(n_periods)
    od_preds['data']['forecast'][-n_periods:].plot(x='ds', y=['score', 'threshold'])
    plt.title('Outlier score over time')
    plt.xlabel('Time')
    plt.ylabel('Outlier score')
    plt.show()
    #| tags: []
    df_fcst = od_preds['data']['forecast']
    df_outlier = df_fcst.loc[df_fcst['score'] > 0]
    #| tags: []
    print('Number of outliers: {}'.format(df_outlier.shape[0]))
    df_outlier[['ds', 'yhat', 'yhat_lower', 'yhat_upper', 'y']]
    from alibi_detect.cd import ClassifierUncertaintyDrift
    
    clf =  # tensorflow classifier model
    cd = ClassifierUncertaintyDetector(x_ref, clf, backend='tensorflow', p_val=.05, preds_type='probs')
    from alibi_detect.cd import RegressorUncertaintyDrift
    
    reg =  # pytorch regression model with at least 1 dropout layer
    cd = RegressorUncertaintyDrift(x_ref, reg, backend='pytorch', p_val=.05, uncertainty_type='mc_dropout')
    class Model(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            # define model
            self.dropout = nn.Dropout(p=.5)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
            # do forward pass which includes self.dropout
    preds = cd.predict(x)
    from numba import config
    config.THREADING_LAYER = 'workqueue'
    from alibi_detect.cd import CVMDriftOnline
    
    ert = 150
    window_sizes = [20,40]
    cd = CVMDriftOnline(x_ref, ert, window_sizes)
    preds = cd.predict(x_t, return_test_stat=True)
    cd = CVMDriftOnline(x_ref, ert, window_sizes)  # Instantiate detector at t=0
    cd.predict(x_1)  # t=1
    cd.save_state('checkpoint_t1')  # Save state at t=1
    cd.predict(x_2)  # t=2
    # Load state at t=1
    cd.load_state('checkpoint_t1')
    !pip install alibi
    import alibi
    import matplotlib.pyplot as plt
    import numpy as np
    
    from alibi_detect.cd import ChiSquareDrift, TabularDrift
    from alibi_detect.saving import save_detector, load_detector
    adult = alibi.datasets.fetch_adult()
    X, y = adult.data, adult.target
    feature_names = adult.feature_names
    category_map = adult.category_map
    X.shape, y.shape
    n_ref = 10000
    n_test = 10000
    
    X_ref, X_t0, X_t1 = X[:n_ref], X[n_ref:n_ref + n_test], X[n_ref + n_test:n_ref + 2 * n_test]
    X_ref.shape, X_t0.shape, X_t1.shape
    categories_per_feature = {f: None for f in list(category_map.keys())}
    cd = TabularDrift(X_ref, p_val=.05, categories_per_feature=categories_per_feature)
    filepath = 'my_path'  # change to directory where detector is saved
    save_detector(cd, filepath)
    cd = load_detector(filepath)
    preds = cd.predict(X_t0)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds['data']['is_drift']]))
    for f in range(cd.n_features):
        stat = 'Chi2' if f in list(categories_per_feature.keys()) else 'K-S'
        fname = feature_names[f]
        stat_val, p_val = preds['data']['distance'][f], preds['data']['p_val'][f]
        print(f'{fname} -- {stat} {stat_val:.3f} -- p-value {p_val:.3f}')
    preds['data']['threshold']
    fpreds = cd.predict(X_t0, drift_type='feature')
    for f in range(cd.n_features):
        stat = 'Chi2' if f in list(categories_per_feature.keys()) else 'K-S'
        fname = feature_names[f]
        is_drift = fpreds['data']['is_drift'][f]
        stat_val, p_val = fpreds['data']['distance'][f], fpreds['data']['p_val'][f]
        print(f'{fname} -- Drift? {labels[is_drift]} -- {stat} {stat_val:.3f} -- p-value {p_val:.3f}')
    preds = cd.predict(X_t1)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds['data']['is_drift']]))
    for f in range(cd.n_features):
        stat = 'Chi2' if f in list(categories_per_feature.keys()) else 'K-S'
        fname = feature_names[f]
        is_drift = (preds['data']['p_val'][f] < preds['data']['threshold']).astype(int)
        stat_val, p_val = preds['data']['distance'][f], preds['data']['p_val'][f]
        print(f'{fname} -- Drift? {labels[is_drift]} -- {stat} {stat_val:.3f} -- p-value {p_val:.3f}')
    def plot_categories(idx: int) -> None:
        # reference data
        x_ref_count = {f: [(X_ref[:, f] == v).sum() for v in vals] 
                       for f, vals in cd.x_ref_categories.items()}
        fref_drift = {cat: x_ref_count[idx][i] for i, cat in enumerate(category_map[idx])}
        
        # test set
        cats = {f: list(np.unique(X_t1[:, f])) for f in categories_per_feature.keys()}
        X_count = {f: [(X_t1[:, f] == v).sum() for v in vals] for f, vals in cats.items()}
        fxt1_drift = {cat: X_count[idx][i] for i, cat in enumerate(category_map[idx])}
        
        # plot bar chart
        plot_labels = list(fxt1_drift.keys())
        ind = np.arange(len(plot_labels))
        width = .35
        fig, ax = plt.subplots()
        p1 = ax.bar(ind, list(fref_drift.values()), width)
        p2 = ax.bar(ind + width, list(fxt1_drift.values()), width)
        ax.set_title(f'Counts per category for {feature_names[idx]} feature')
        ax.set_xticks(ind + width / 2)
        ax.set_xticklabels(plot_labels)
        ax.legend((p1[0], p2[0]), ('Reference', 'Test'), loc='upper right', ncol=2)
        ax.set_ylabel('Counts')
        ax.set_xlabel('Categories')
        plt.xticks(list(np.arange(len(plot_labels))), plot_labels, rotation='vertical')
        plt.show()
    plot_categories(2)
    plot_categories(3)
    plot_categories(4)
    cols = list(category_map.keys())
    cat_names = [feature_names[_] for _ in list(category_map.keys())]
    X_ref_cat, X_t0_cat = X_ref[:, cols], X_t0[:, cols]
    X_ref_cat.shape, X_t0_cat.shape
    cd = ChiSquareDrift(X_ref_cat, p_val=.05)
    preds = cd.predict(X_t0_cat)
    print('Drift? {}'.format(labels[preds['data']['is_drift']]))
    print(f"Threshold {preds['data']['threshold']}")
    for f in range(cd.n_features):
        fname = cat_names[f]
        is_drift = (preds['data']['p_val'][f] < preds['data']['threshold']).astype(int)
        stat_val, p_val = preds['data']['distance'][f], preds['data']['p_val'][f]
        print(f'{fname} -- Drift? {labels[is_drift]} -- {stat} {stat_val:.3f} -- p-value {p_val:.3f}')

    Maximum Mean Discrepancy

    source

    Maximum Mean Discrepancy

    Overview

    The Maximum Mean Discrepancy (MMD) detector is a kernel-based method for multivariate 2 sample testing. The MMD is a distance-based measure between 2 distributions p and q based on the mean embeddings $\mu_{p}$ and $\mu_{q}$ in a reproducing kernel Hilbert space $F$:

    We can compute unbiased estimates of $MMD^2$ from the samples of the 2 distributions after applying the kernel trick. We use by default a , but users are free to pass their own kernel of preference to the detector. We obtain a $p$-value via a on the values of $MMD^2$.

    For high-dimensional data, we typically want to reduce the dimensionality before computing the permutation test. Following suggestions in , we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs () as out-of-the box preprocessing methods and note that can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift.

    Detecting input data drift (covariate shift) $\Delta p(x)$ for text data requires a custom preprocessing step. We can pick up changes in the semantics of the input by extracting (contextual) embeddings and detect drift on those. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract. The library contains functionality to leverage pre-trained embeddings from but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in the notebook.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • backend: TensorFlow, PyTorch and implementations of the MMD detector are available. Specify the backend (tensorflow, pytorch or keops). Defaults to tensorflow.

    • p_val: p-value used for significance of the permutation test.

    • preprocess_at_init

    Additional PyTorch keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    Additional KeOps keyword arguments:

    • batch_size_permutations: KeOps computes the n_permutations of the MMD^2 statistics in chunks of batch_size_permutations. Defaults to 1,000,000.

    Initialized drift detector examples for each of the available backends:

    We can also easily add preprocessing functions for the TensorFlow and PyTorch frameworks. Note that we can also combine for instance a PyTorch preprocessing step with a KeOps detector. The following example uses a randomly initialized image encoder in PyTorch:

    The same functionality is supported in TensorFlow and the main difference is that you would import from alibi_detect.cd.tensorflow import preprocess_drift. Other preprocessing steps such as the output of hidden layers of a model or extracted text embeddings using transformer models can be used in a similar way in both frameworks. TensorFlow example for the hidden layer output:

    Check out the example for more details.

    Alibi Detect also includes custom text preprocessing steps in both TensorFlow and PyTorch based on Huggingface's package:

    Again the same functionality is supported in TensorFlow but with from alibi_detect.cd.tensorflow import preprocess_drift and from alibi_detect.models.tensorflow import TransformerEmbedding imports. Check out the example for more information.

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the p-value and the threshold of the permutation test by setting return_p_val to True and the maximum mean discrepancy metric and threshold by setting return_distance to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains the p-value if return_p_val equals True.

    • threshold: p-value threshold if return_p_val

    Examples

    Graph

    Image

    Tabular

    Text

    Likelihood Ratio Outlier Detection on Genomic Sequences

    Method

    The outlier detector described by Ren et al. (2019) in Likelihood Ratios for Out-of-Distribution Detection uses the likelihood ratio between 2 generative models as the outlier score. One model is trained on the original data while the other is trained on a perturbed version of the dataset. This is based on the observation that the likelihood score for an instance under a generative model can be heavily affected by population level background statistics. The second generative model is therefore trained to capture the background statistics still present in the perturbed data while the semantic features have been erased by the perturbations.

    The perturbations are added using an independent and identical Bernoulli distribution with rate $\mu$ which substitutes a feature with one of the other possible feature values with equal probability. Each feature in the genome dataset can take 4 values (one of the ACGT nucleobases). This means that a perturbed feature is swapped with one of the other nucleobases. The generative model used in the example is a simple LSTM network.

    Dataset

    The bacteria genomics dataset for out-of-distribution detection was released as part of the paper. From the original TL;DR: The dataset contains genomic sequences of 250 base pairs from 10 in-distribution bacteria classes for training, 60 OOD bacteria classes for validation, and another 60 different OOD bacteria classes for test. There are respectively 1, 7 and again 7 million sequences in the training, validation and test sets. For detailed info on the dataset check the .

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load genome data

    X represents the genome sequences and y whether they are outliers ($1$) or not ($0$).

    There are no outliers in the training set and a majority of outliers (compared to the training data) in the validation and test sets:

    Define model

    We need to define a generative model which models the genome sequences. We follow the paper and opt for a simple LSTM. Note that we don't actually need to define the model below if we simply load the pretrained detector later on:

    We also need to define our loss function which we can utilize to evaluate the log-likelihood for the outlier detector:

    Load or train the outlier detector

    We can again either fetch the pretrained detector from a or train one from scratch:

    Compare the log likelihoods

    Let's compare the log likelihoods of the inliers vs. the outlier test set data under the semantic and background models. We randomly sample $100,000$ instances from both distributions since the full test set contains $7,000,000$ genomic sequences. The histograms show that the generative model does not distinguish well between inliers and outliers.

    This is because of the background-effect which is in this case the GC-content in the genomic sequences. This effect is partially reduced when taking the likelihood ratio:

    Detect outliers

    We follow the same procedure with the outlier detector. First we need to set an outlier threshold with infer_threshold. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have a small batch of data with roughly $30$% outliers but we don't know exactly which ones.

    Let's save the outlier detector with updated threshold:

    Let'spredict outliers on a sample of the test set:

    Display results

    F1 score, accuracy, precision, recall and confusion matrix:

    We can also plot the ROC curve based on the instance level outlier scores:

    Classifier

    source

    Classifier

    Overview

    The classifier-based drift detector Lopez-Paz and Oquab, 2017 simply tries to correctly distinguish instances from the reference set vs. the test set. The classifier is trained to output the probability that a given instance belongs to the test set. If the probabilities it assigns to unseen test instances are significantly higher (as determined by a Kolmogorov-Smirnov test) to those it assigns to unseen reference instances then the test set must differ from the reference set and drift is flagged. Alternatively, the detector also allows to binarize the classifier predictions (0 or 1) and apply a binomial test on the binarized predictions of the reference vs. the test data. To leverage all the available reference and test data, stratified cross-validation can be applied and the out-of-fold predictions are used for the significance test. Note that a new classifier is trained for each test set or even each fold within the test set.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • model: Binary classification model used for drift detection. TensorFlow, PyTorch and Sklearn models are supported.

    Keyword arguments:

    • backend: Specify the backend (tensorflow, pytorch or sklearn). This depends on the framework of the model. Defaults to tensorflow.

    • p_val: p-value threshold used for the significance of the test.

    • preprocess_at_init

    Additional PyTorch keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    • dataloader: Dataloader object used during training of the model. Defaults to torch.utils.data.DataLoader. The dataloader is not initialized yet, this is done during init off the detector using the batch_size. Custom dataloaders can be passed as well, e.g. for graph data we can use torch_geometric.data.DataLoader.

    Additional Sklearn keyword arguments:

    • use_calibration : Whether to use calibration. Calibration can be used on top of any model. Only relevant for 'sklearn' backend.

    • calibration_kwargs : Optional additional kwargs for calibration. Only relevant for 'sklearn' backend. See https://scikit-learn.org/stable/modules/generated/sklearn.calibration.CalibratedClassifierCV.html for more details.

    • use_oob : Whether to use out-of-bag(OOB) predictions. Supported only for RandomForestClassifier

    Initialized TensorFlow drift detector example:

    A similar detector using PyTorch:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. return_p_val equal to True will also return the p-value of the test, return_distance equal to True will return a notion of strength of the drift and return_probs equals True also returns the out-of-fold classifier model prediction probabilities on the reference and test data (0 = reference data, 1 = test data) as well as the associated out-of-fold reference and test instances.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • threshold: the user-defined threshold defining the significance of the test

    • p_val: the p-value of the test if return_p_val equals True.

    Examples

    Least-Squares Density Difference

    source

    Least-Squares Density Difference

    Overview

    The least-squares density difference detector is a method for multivariate 2 sample testing. The LSDD between two distributions $p$ and $q$ on $\mathcal{X}$ is defined as

    Given two samples we can compute an estimate of the $LSDD$ between the two underlying distributions and use it as a test statistic. We then obtain a $p$-value via a on the values of the $LSDD$ estimates. In practice we actually estimate the LSDD scaled by a factor that maintains numerical stability when dimensionality is high.

    Note

    $LSDD$ is based on the assumption that a probability density exists for both distributions and hence is only suitable for continuous data. If you are working with tabular data containing categorical variables, we recommend using the instead.

    For high-dimensional data, we typically want to reduce the dimensionality before computing the permutation test. Following suggestions in , we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs () as out-of-the box preprocessing methods and note that can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift.

    Detecting input data drift (covariate shift) $\Delta p(x)$ for text data requires a custom preprocessing step. We can pick up changes in the semantics of the input by extracting (contextual) embeddings and detect drift on those. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract. The library contains functionality to leverage pre-trained embeddings from but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in the notebook.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • backend: Both TensorFlow and PyTorch implementations of the LSD detector as well as various preprocessing steps are available. Specify the backend (tensorflow or pytorch). Defaults to tensorflow.

    • p_val: p-value used for significance of the permutation test.

    • preprocess_at_init: Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to

    Additional PyTorch keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    Initialized drift detector example:

    The same detector in PyTorch:

    We can also easily add preprocessing functions for both frameworks. The following example uses a randomly initialized image encoder in PyTorch:

    The same functionality is supported in TensorFlow and the main difference is that you would import from alibi_detect.cd.tensorflow import preprocess_drift. Other preprocessing steps such as the output of hidden layers of a model or extracted text embeddings using transformer models can be used in a similar way in both frameworks. TensorFlow example for the hidden layer output:

    The LSDDDrift detector can be used in exactly the same way as the MMDDrift detector which is further demonstrated in the example.

    Alibi Detect also includes custom text preprocessing steps in both TensorFlow and PyTorch based on Huggingface's package:

    Again the same functionality is supported in TensorFlow but with from alibi_detect.cd.tensorflow import preprocess_drift and from alibi_detect.models.tensorflow import TransformerEmbedding imports. Check out the example for more information.

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. We can return the p-value and the threshold of the permutation test by setting return_p_val to True and the maximum mean discrepancy metric and threshold by setting return_distance to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • p_val: contains the p-value if return_p_val equals True.

    • threshold: p-value threshold if return_p_val

    Examples

    For the related MMDDrift detector.

    Image

    Text

    alibi_detect.base

    Constants

    DEFAULT_META

    LARGE_ARTEFACTS

    Built-in mutable sequence.

    If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

    BaseDetector

    Inherits from: ABC

    Base class for outlier, adversarial and drift detection algorithms.

    Constructor

    Properties

    Property
    Type
    Description

    Methods

    predict

    Name
    Type
    Default
    Description

    score

    Name
    Type
    Default
    Description

    ConfigurableDetector

    Inherits from: Detector, Protocol, Generic

    Type Protocol for detectors that have support for saving via config.

    Used for typing save and load functionality in alibi_detect.saving.saving.

    Methods

    from_config

    Name
    Type
    Default
    Description

    get_config

    Returns

    • Type: dict

    Detector

    Inherits from: Protocol, Generic

    Type Protocol for all detectors.

    Used for typing legacy save and load functionality in alibi_detect.saving._tensorflow.saving.py.

    Note

    Constructor

    Methods

    predict

    Returns

    • Type: typing.Any

    DriftConfigMixin

    A mixin class containing methods related to a drift detector's configuration dictionary.

    Constructor

    Methods

    from_config

    Instantiate a drift detector from a fully resolved (and validated) config dictionary.

    Name
    Type
    Default
    Description

    get_config

    Get the detector's configuration dictionary.

    Returns

    • Type: dict

    FitMixin

    Inherits from: ABC

    Methods

    fit

    Returns

    • Type: None

    NumpyEncoder

    Inherits from: JSONEncoder

    Methods

    default

    Name
    Type
    Default
    Description

    StatefulDetectorOnline

    Inherits from: ConfigurableDetector, Detector, Protocol, Generic

    Type Protocol for detectors that have support for save/loading of online state.

    Used for typing save and load functionality in alibi_detect.saving.saving.

    Methods

    load_state

    Name
    Type
    Default
    Description

    save_state

    Name
    Type
    Default
    Description

    ThresholdMixin

    Inherits from: ABC

    Methods

    infer_threshold

    Returns

    • Type: None

    Functions

    adversarial_correction_dict

    adversarial_prediction_dict

    concept_drift_dict

    outlier_prediction_dict

    Interpretable drift detection with the spot-the-diff detector on MNIST and Wine-Quality datasets

    Under the hood drift detectors leverage a function of the data that is expected to be large when drift has occured and small when it hasn't. In the Learned drift detectors on CIFAR-10 example notebook we note that we can learn a function satisfying this property by training a classifer to distinguish reference and test samples. However we now additionally note that if the classifier is specified in a certain way then when drift is detected we can inspect the weights of the classifier to shine light on exactly which features of the data were used to distinguish reference from test samples and therefore caused drift to be detected.

    The SpotTheDiffDrift detector is designed to make this process straightforward. Like the ClassifierDrift detector, it uses a portion of the available data to train a classifier to discriminate between reference and test instances. Letting $\hat{p}_T(x)$ represent the probability assigned by the classifier that the instance $x$ is from the test set rather than reference set, the difference here is that we use a classifier of the form logit(p^T)=b0+b1k(x,w1)+...+bJk(x,wJ),\text{logit}(\hat{p}_T) = b_0 + b_1 k(x,w_1) + ... + b_Jk(x,w_J),logit(p^​T​)=b0​+b1​k(x,w1​)+...+bJ​k(x,wJ​), where $k(\cdot,\cdot)$ is a kernel specifying a notion of similarity between instances, $w_i$ are learnable test locations and $b_i$ are learnable regression coefficients.

    The idea here is that if the detector flags drift and $b_i >0$ then we know that it reached its decision by considering how similar each instance is to the instance $w_i$, with those being more similar being more likely to be test instances than reference instances. Alternatively if $b_i < 0$ then instances more similar to $w_i$ were deemed more likely to be reference instances.

    In order to provide less noisy and therefore more interpretable results, we define each test location as where $\bar{x}$ is the mean reference instance. We may then interpret $d_i$ as the additive transformation deemed to make the average reference more ($b_i>0$) or less ($b_i<0$) similar to a test instance. Defining the test locations in this way allows us to instead learn the difference $d_i$ and apply regularisation such that non-zero values must be justified by improved classification performance. This allows us to more clearly identify which features any detected drift should be attributed to.

    This approach to interpretable drift detection is inspired by the work of , however several major adaptations have been made.

    Backend

    The method works with both the PyTorch and TensorFlow frameworks. Alibi Detect does however not install PyTorch for you. Check the PyTorch docs how to do this.

    Dataset

    We start with an image example in order to provide a visual illustration of how the detector works. For this prupose we use the of 28 by 28 grayscale handwritten digits. To represent the common problem of new classes emerging during the deployment phase we consider a reference set of ~9,000 instances containing only the digits 1-9 and a test set of 10,000 instances containing all of the digits 0-9. We would like drift to be detected in this scenario because a model trained of the reference instances will not know how to process instances from the new class.

    This notebook requires the torchvision package which can be installed via pip:

    When instantiating the detector we should specify the number of "diffs" we would like it to use to discriminate reference from test instances. Here there is a trade off. Using n_diffs=1 is the simplest to interpret and seems to work well in practice. Using more diffs may result in stronger detection power but the diffs may be harder to interpret due to intereactions and conditional dependencies.

    The strength of the regularisation (l1_reg) to apply to the diffs should also be specified. Stronger regularisation results in sparser diffs as the classifier is encouraged to discriminate using fewer features. This may make the diff more interpretable but may again come at the cost of detection power.

    We should also specify how the classifier should be trained with standard arguments such as learning_rate, epochs and batch_size. By default a is used for the kernel but alternatives can be specified via the kernel kwarg. Additionally the classifier can be initialised with any desired diffs by passing them with the initial_diffs kwarg -- by default they are initialised with Gaussian noise with standard deviation equal to that observed in the reference data.

    When we then call the detector to detect drift on the deployment/test set it trains the classifier (thereby learning the diffs) and the usual is_drift and p_val properties can be inspected in the usual way:

    As expected, the drift was detected. However we may now additionally look at the learned diffs and corresponding coefficients to determine how the detector reached this decision.

    The detector has identified the zero that was missing from the reference data -- it realised that test instances were on average more (coefficient > 0) simmilar to an instance with below average middle pixel values and above average zero-region pixel values than reference instances were. It used this information to determine that drift had occured.

    Interpretable Drift Detection on the Wine Quality Dataset

    To provide an example on tabular data we consider the consisting of 4898 and 1599 samples of white and red wine respectively. Each sample has an associated quality (as determined by experts) and 11 numeric features indicating its acidity, density, pH etc. To represent the problem of a model being trained on one distribution and deployed on a subtly different one, we take as a reference set the samples of white wine and consider the red wine samples to form a 'corrupted' deployment set.

    We can see that the data for both red and white wine samples take the same format.

    We extract the features and shuffle and normalise them such that they take values in [0,1].

    We then split off half of the reference set to act as an unseen sample from the same underlying distribution for which drift should not be detected.

    We instantiate our detector in the same way as we do above, but this time using the Pytorch backend for the sake of variety. We then get the predictions of the detector on both the undrifted and corrupted test sets.

    As expected drift is detected on the red wine samples but not the held out white wine samples from the same distribution. Now we can inspect the returned diff to determine how the detector reached its decision

    We see that the detector was able to discriminate the corrupted (red) wine samples from the reference (white) samples by noting that on average reference samples (coeff < 0) typically contain more sulfur dioxide and residual sugars but have less sulphates and chlorides and have lower pH and volatile and fixed acidity.

    Adversarial Auto-Encoder

    source

    Adversarial Auto-Encoder

    Overview

    The adversarial detector follows the method explained in the Adversarial Detection and Correction by Matching Prediction Distributions paper. Usually, autoencoders are trained to find a transformation $T$ that reconstructs the input instance $x$ as accurately as possible with loss functions that are suited to capture the similarities between x and $x'$ such as the mean squared reconstruction error. The novelty of the adversarial autoencoder (AE) detector relies on the use of a classification model-dependent loss function based on a distance metric in the output space of the model to train the autoencoder network. Given a classification model $M$ we optimise the weights of the autoencoder such that the between the model predictions on $x$ and on $x'$ is minimised. Without the presence of a reconstruction loss term $x'$ simply tries to make sure that the prediction probabilities $M(x')$ and $M(x)$ match without caring about the proximity of $x'$ to $x$. As a result, $x'$ is allowed to live in different areas of the input feature space than $x$ with different decision boundary shapes with respect to the model $M$. The carefully crafted adversarial perturbation which is effective around x does not transfer to the new location of $x'$ in the feature space, and the attack is therefore neutralised. Training of the autoencoder is unsupervised since we only need access to the model prediction probabilities and the normal training instances. We do not require any knowledge about the underlying adversarial attack and the classifier weights are frozen during training.

    The detector can be used as follows:

    • An adversarial score $S$ is computed. $S$ equals the K-L divergence between the model predictions on $x$ and $x'$.

    • If $S$ is above a threshold (explicitly defined or inferred from training data), the instance is flagged as adversarial.

    • For adversarial instances, the model $M$ uses the reconstructed instance $x'$ to make a prediction. If the adversarial score is below the threshold, the model makes a prediction on the original instance $x$.

    This procedure is illustrated in the diagram below:

    The method is very flexible and can also be used to detect common data corruptions and perturbations which negatively impact the model performance. The algorithm works well on tabular and image data.

    Usage

    Initialize

    Parameters:

    • threshold: threshold value above which the instance is flagged as an adversarial instance.

    • encoder_net: tf.keras.Sequential instance containing the encoder network. Example:

    • decoder_net: tf.keras.Sequential instance containing the decoder network. Example:

    • ae: instead of using a separate encoder and decoder, the AE can also be passed as a tf.keras.Model.

    • model: the classifier as a tf.keras.Model. Example:

    • hidden_layer_kld: dictionary with as keys the number of the hidden layer(s) in the classification model which are extracted and used during training of the adversarial AE, and as values the output dimension for the hidden layer. Extending the training methodology to the hidden layers is optional and can further improve the adversarial correction mechanism.

    • model_hl: instead of passing a dictionary to hidden_layer_kld, a list with tf.keras models for the hidden layer K-L divergence computation can be passed directly.

    • w_model_hl

    Initialized adversarial detector example:

    Fit

    We then need to train the adversarial detector. The following parameters can be specified:

    • X: training batch as a numpy array.

    • loss_fn: loss function used for training. Defaults to the custom adversarial loss.

    • w_model: weight on the loss term minimizing the K-L divergence between model prediction probabilities on the original and reconstructed instance. Defaults to 1.

    The threshold for the adversarial score can be set via infer_threshold. We need to pass a batch of instances $X$ and specify what percentage of those we consider to be normal via threshold_perc. Even if we only have normal instances in the batch, it might be best to set the threshold value a bit lower (e.g. $95$%) since the the model could have misclassified training instances leading to a higher score if the reconstruction picked up features from the correct class or some instances might look adversarial in the first place.

    Detect

    We detect adversarial instances by simply calling predict on a batch of instances X. We can also return the instance level adversarial score by setting return_instance_score to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_adversarial: boolean whether instances are above the threshold and therefore adversarial instances. The array is of shape (batch size,).

    • instance_score: contains instance level scores if return_instance_score equals True.

    Correct

    We can immediately apply the procedure sketched out in the above diagram via correct. The method also returns a dictionary with meta and data keys. On top of the information returned by detect, 3 additional fields are returned under data:

    • corrected: model predictions by following the adversarial detection and correction procedure.

    • no_defense: model predictions without the adversarial correction.

    • defense: model predictions where each instance is corrected by the defense, regardless of the adversarial score.

    Examples

    Image

    Learned drift detectors on CIFAR-10

    Under the hood drift detectors leverage a function (also known as a test-statistic) that is expected to take a large value if drift has occurred and a low value if not. The power of the detector is partly determined by how well the function satisfies this property. However, specifying such a function in advance can be very difficult. In this example notebook we consider two ways in which a portion of the available data may be used to learn such a function before then applying it on the held out portion of the data to test for drift.

    Detecting drift with a learned classifier

    The classifier-based drift detector simply tries to correctly distinguish instances from the reference data vs. the test set. The classifier is trained to output the probability that a given instance belongs to the test set. If the probabilities it assigns to unseen tests instances are significantly higher (as determined by a Kolmogorov-Smirnov test) to those it assigns to unseen reference instances then the test set must differ from the reference set and drift is flagged. To leverage all the available reference and test data, stratified cross-validation can be applied and the out-of-fold predictions are used for the significance test. Note that a new classifier is trained for each test set or even each fold within the test set.

    Time series outlier detection with Seq2Seq models on synthetic data

    Method

    The (Seq2Seq) outlier detector consists of 2 main building blocks: an encoder and a decoder. The encoder consists of a which processes the input sequence and initializes the decoder. The LSTM decoder then makes sequential predictions for the output sequence. In our case, the decoder aims to reconstruct the input sequence. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

    Since even for normal data the reconstruction error can be state-dependent, we add an outlier threshold estimator network to the Seq2Seq model. This network takes in the hidden state of the decoder at each timestep and predicts the estimated reconstruction error for normal data. As a result, the outlier threshold is not static and becomes a function of the model state. This is similar to , but while they train the threshold estimator separately from the Seq2Seq model with a Support-Vector Regressor, we train a neural net regression network end-to-end with the Seq2Seq model.

    Online drift detection for Camelyon17 medical imaging dataset

    This notebook demonstrates a typical workflow for applying online drift detectors to streams of image data. For those unfamiliar with how the online drift detectors operate in alibi_detect we recommend first checking out the more introductory example where online drift detection is performed for the wine quality dataset.

    This notebook requires the wilds, torch and torchvision packages which can be installed via pip:

    Online Maximum Mean Discrepancy

    Online Maximum Mean Discrepancy

    Overview

    The online detector is a kernel-based method for online drift detection. The MMD is a distance-based measure between 2 distributions

    VAE outlier detection for income prediction

    Method

    The Variational Auto-Encoder () outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The VAE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

    VAE outlier detection on KDD Cup ‘99 dataset

    Method

    The Variational Auto-Encoder () outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The VAE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is either measured as the mean squared error (MSE) between the input and the reconstructed instance or as the probability that both the input and the reconstructed instance are generated by the same process.

    Online Drift Detection on the Wine Quality Dataset

    In the context of deployed models, data (model queries) usually arrive sequentially and we wish to detect it as soon as possible after its occurence. One approach is to perform a test for drift every $W$ time-steps, using the $W$ samples that have arrived since the last test. Such a strategy could be implemented using any of the offline detectors implemented in alibi-detect, but being both sensitive to slight drift and responsive to severe drift is difficult. If the window size $W$ is too small then slight drift will be undetectable. If it is too large then the delay between test-points hampers responsiveness to severe drift.

    An alternative strategy is to perform a test each time data arrives. However the usual offline methods are not applicable because the process for computing p-values is too expensive and doesn't account for correlated test outcomes when using overlapping windows of test data.

    Online detectors instead work by computing the test-statistic once using the first $W$ data points and then updating the test-statistic sequentially at low cost. When no drift has occured the test-statistic fluctuates around its expected value and once drift occurs the test-statistic starts to drift upwards. When it exceeds some preconfigured threshold value, drift is detected.

    Unlike offline detectors which require the specification of a threshold p-value (a false positive rate), the online detectors in alibi-detect require the specification of an expected run-time (ERT) (an inverted FPR). This is the number of time-steps that we insist our detectors, on average, should run for in the absense of drift before making a false detection. Usually we would like the ERT to be large, however this results in insensitive detectors which are slow to respond when drift does occur. There is a tradeoff between the expected run time and the expected detection delay.

    alibi_detect.cd.lsdd

    Constants

    has_pytorch

    bool(x) -> bool

    Online Least-Squares Density Difference

    Online Least-Squares Density Difference

    Overview

    The online Least Squares Density Difference detector is a non-parametric method for online drift detection. The LSDD between two distributions $p$ and $q$ on $\mathcal{X}$ is defined as

    DEFAULT_META: dict = {'name': None, 'online': None, 'data_type': None, 'version': None, 'detector_...
    : Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to
    True
    . It is possible that it needs to be set to
    False
    if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.
  • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

  • kernel: Kernel used when computing the MMD. Defaults to a Gaussian RBF kernel (from alibi_detect.utils.pytorch import GaussianRBF, from alibi_detect.utils.tensorflow import GaussianRBF or from alibi_detect.utils.keops import GaussianRBF dependent on the backend used). Note that for the KeOps backend, the diagonal entries of the kernel matrices kernel(x_ref, x_ref) and kernel(x_test, x_test) should be equal to 1. This is compliant with the default Gaussian RBF kernel.

  • sigma: Optional bandwidth for the kernel as a np.ndarray. We can also average over a number of different bandwidths, e.g. np.array([.5, 1., 1.5]).

  • configure_kernel_from_x_ref: If sigma is not specified, the detector can infer it via a heuristic and set sigma to the median (TensorFlow and PyTorch) or the mean pairwise distance between 2 samples (KeOps) by default. If configure_kernel_from_x_ref is True, we can already set sigma at initialization of the detector by inferring it from x_ref, speeding up the prediction step. If set to False, sigma is computed separately for each test batch at prediction time.

  • n_permutations: Number of permutations used in the permutation test.

  • input_shape: Optionally pass the shape of the input data.

  • data_type: can specify data type added to the metadata. E.g. 'tabular' or 'image'.

  • equals
    True
    .
  • distance: MMD^2 metric between the reference data and the new batch if return_distance equals True.

  • distance_threshold: MMD^2 metric value from the permutation test which corresponds to the the p-value threshold.

  • MMD(F,p,q)=∣∣μp−μq∣∣F2MMD(F, p, q) = || \mu_{p} - \mu_{q} ||^2_{F}MMD(F,p,q)=∣∣μp​−μq​∣∣F2​
    radial basis function kernel
    permutation test
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    BBSDs
    PCA
    HuggingFace's transformer package
    Text drift detection on IMDB movie reviews
    KeOps
    Drift detection on CIFAR10
    transformers
    Text drift detection on IMDB movie reviews
    Drift detection on molecular graphs
    Drift detection on CIFAR10
    Scaling up drift detection with KeOps
    Text drift detection on IMDB movie reviews
    Likelihood Ratios for Out-of-Distribution Detection
    README
    Google Cloud Bucket
    : Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to
    True
    . It is possible that it needs to be set to
    False
    if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.
  • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed. If the input data type is of type List[Any] then update_x_ref needs to be set to None and the reference set remains fixed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

  • preds_type: Whether the model outputs 'probs' (probabilities - for 'tensorflow', 'pytorch', 'sklearn' models), 'logits' (for 'pytorch', 'tensorflow' models), 'scores' (for 'sklearn' models if decision_function is supported).

  • binarize_preds: Whether to test for discrepancy on soft (e.g. probs/logits/scores) model predictions directly with a K-S test or binarise to 0-1 prediction errors and apply a binomial test. Defaults to False and therefore applies the K-S test.

  • train_size: Optional fraction (float between 0 and 1) of the dataset used to train the classifier. The drift is detected on 1 - train_size. Cannot be used in combination with n_folds.

  • n_folds: Optional number of stratified folds used for training. The model preds are then calculated on all the out-of-fold predictions. This allows to leverage all the reference and test data for drift detection at the expense of longer computation. If both train_size and n_folds are specified, n_folds is prioritized.

  • seed: Optional random seed for fold selection.

  • optimizer: Optimizer used during training of the classifier. From torch.optim for PyTorch and tf.keras.optimizers for TensorFlow.

  • learning_rate: Learning rate for the optimizer. Only relevant for tensorflow and pytorch backends.

  • batch_size: Batch size used during training of the classifier.Only relevant for tensorflow and pytorch backends.

  • epochs: Number of training epochs for the classifier. Applies to each fold if n_folds is specified. Only relevant for tensorflow and pytorch backends.

  • verbose: Verbosity level during the training of the classifier. 0 is silent and 1 prints a progress bar. Only relevant for tensorflow and pytorch backends.

  • train_kwargs: Optional additional kwargs for the built-in TensorFlow (from alibi_detect.models.tensorflow import trainer) or PyTorch (from alibi_detect.models.pytorch import trainer) trainer functions.

  • dataset: Dataset object used during training of the classifier. Defaults to alibi_detect.utils.pytorch.TorchDataset (an instance of torch.utils.data.Dataset) for the PyTorch backend and alibi_detect.utils.tensorflow.TFDataset (an instance of tf.keras.utils.Sequence) for the TensorFlow backend. For PyTorch, the dataset should only take the data x and the array of labels y as input, so when e.g. TorchDataset is passed to the detector at initialisation, during training TorchDataset(x, y) is used. For TensorFlow, the dataset is an instance of tf.keras.utils.Sequence, so when e.g. TFDataset is passed to the detector at initialisation, during training TFDataset(x, y, batch_size=batch_size, shuffle=True) is used. x can be of type np.ndarray or List[Any] while y is of type np.ndarray.

  • input_shape: Shape of input data.

  • data_type: Optionally specify the data type (e.g. tabular, image or time-series). Added to metadata.

  • .

    distance: a notion of strength of the drift if return_distance equals True. Equal to the K-S test statistic assuming binarize_preds equals False or the relative error reduction over the baseline error expected under the null if binarize_preds equals True.

  • probs_ref: the instance level prediction probability for the reference data x_ref (0 = reference data, 1 = test data) if return_probs is True.

  • probs_test: the instance level prediction probability for the test data x if return_probs is true.

  • x_ref_oof: the instances associated with probs_ref if return_probs equals True.

  • x_test_oof: the instances associated with probs_test if return_probs equals True.

  • Drift detection on CIFAR10
    Drift detection on Adult Census
    Drift detection on Amazon reviews
    True
    . It is possible that it needs to be set to
    False
    if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.
  • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

  • sigma: Optionally set the bandwidth of the Gaussian kernel used in estimating the LSDD. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths. If sigma is not specified, the 'median heuristic' is adopted whereby sigma is set as the median pairwise distance between reference samples.

  • n_permutations: Number of permutations used in the permutation test.

  • n_kernel_centers: The number of reference samples to use as centers in the Gaussian kernel model used to estimate LSDD. Defaults to 1/20th of the reference data.

  • lambda_rd_max: The maximum relative difference between two estimates of LSDD that the regularization parameter lambda is allowed to cause. Defaults to 0.2 as in the paper.

  • input_shape: Optionally pass the shape of the input data.

  • data_type: can specify data type added to the metadata. E.g. 'tabular' or 'image'.

  • equals
    True
    .
  • distance: LSDD metric between the reference data and the new batch if return_distance equals True.

  • distance_threshold: LSDD metric value from the permutation test which corresponds to the the p-value threshold.

  • LSDD(p,q)=∫X(p(x)−q(x))2 dx.LSDD(p,q) = \int_{\mathcal{X}} (p(x)-q(x))^2 \,dx.LSDD(p,q)=∫X​(p(x)−q(x))2dx.
    permutation test
    TabularDrift detector
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    BBSDs
    PCA
    HuggingFace's transformer package
    Text drift detection on IMDB movie reviews
    Drift detection on CIFAR10
    transformers
    Text drift detection on IMDB movie reviews
    Drift detection on CIFAR10
    Text drift detection on IMDB movie reviews

    meta

    Dict

    X

    numpy.ndarray

    X

    numpy.ndarray

    config

    dict

    config

    dict

    A config dictionary matching the schema's in :class:~alibi_detect.saving.schemas.

    obj

    filepath

    Union[str, os.PathLike]

    filepath

    Union[str, os.PathLike]

    wi=xˉ+diw_i = \bar{x}+d_iwi​=xˉ+di​
    Jitkrittum et al. (2016)
    MNIST dataset
    Gaussian RBF
    Wine Quality Data Set
    : Weights assigned to the loss of each model in
    model_hl
    . Also used to weight the K-L divergence contribution for each model in
    model_hl
    when computing the adversarial score.
  • temperature: Temperature used for model prediction scaling. Temperature <1 sharpens the prediction probability distribution which can be beneficial for prediction distributions with high entropy.

  • data_type: can specify data type added to metadata. E.g. 'tabular' or 'image'.

  • w_recon: weight on the mean squared error reconstruction loss term. Defaults to 0.

  • optimizer: optimizer used for training. Defaults to Adam with learning rate 1e-3.

  • epochs: number of training epochs.

  • batch_size: batch size used during training.

  • verbose: boolean whether to print training progress.

  • log_metric: additional metrics whose progress will be displayed if verbose equals True.

  • preprocess_fn: optional data preprocessing function applied per batch during training.

  • KL-divergence
    Adversarial detection on CIFAR10

    Backend

    The method works with both the PyTorch and TensorFlow frameworks. Alibi Detect does however not install PyTorch for you. Check the PyTorch docs how to do this.

    Dataset

    CIFAR10 consists of 60,000 32 by 32 RGB images equally distributed over 10 classes. We evaluate the drift detector on the CIFAR-10-C dataset (Hendrycks & Dietterich, 2019). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance. We also check for drift against the original test set with class imbalances.

    Load data

    Original CIFAR-10 data:

    For CIFAR-10-C, we can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the original test set in a reference dataset and a dataset which should not be flagged as drift. We also split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    Detect drift with a TensorFlow classifier

    Single fold

    We use a simple classification model and try to distinguish between the reference data and the corrupted test sets. The detector defaults to binarize=False which means a Kolmogorov-Smirnov test will be used to test for significant disparity between continuous model predictions (e.g. probabilities or logits). Initially we'll test at a significance level of $p=0.05$, use $75$% of the shuffled reference and test data for training and evaluate the detector on the remaining $25$%. We only train for 1 epoch.

    If needed, the detector can be saved and loaded with save_detector and load_detector:

    Let's check whether the detector thinks drift occurred on the different test sets and time the prediction calls:

    As expected, drift was only detected on the corrupted datasets and the classifier could easily distinguish the corrupted from the reference data.

    Use all the available data via cross-validation

    So far we've only used $25$% of the data to detect the drift since $75$% is used for training purposes. At the cost of additional training time we can however leverage all the data via stratified cross-validation. We just need to set the number of folds and keep everything else the same. So for each test set n_folds models are trained, and the out-of-fold predictions combined for the significance test:

    Detecting drift with a learned kernel

    An alternative to training a classifier to output high probabilities for instances from the test window and low probabilities for instances from the reference window is to learn a kernel that outputs high similarities between instances from the same window and low similarities between instances from different windows. The kernel may then be used within an MMD-test for drift. Liu et al. (2020) propose this learned approach and note that it is in fact a generalisation of the above classifier-based method. However, in this case we can train the kernel to directly optimise an estimate of the detector's power, which can result in superior performance.

    Detect drift with a learned PyTorch kernel

    Any differentiable Pytorch or TensorFlow module that takes as input two instances and outputs a scalar (representing similarity) can be used as the kernel for this drift detector. However, in order to ensure that MMD=0 implies no-drift the kernel should satify a characteristic property. This can be guarenteed by defining a kernel as k(x,y)=(1−ϵ)∗ka(Φ(x),Φ(y))+ϵ∗kb(x,y),k(x,y)=(1-\epsilon)*k_a(\Phi(x), \Phi(y)) + \epsilon*k_b(x,y),k(x,y)=(1−ϵ)∗ka​(Φ(x),Φ(y))+ϵ∗kb​(x,y), where $\Phi$ is a learnable projection, $k_a$ and $k_b$ are simple characteristic kernels (such as a Gaussian RBF, and $\epsilon>0$ is a small constant. By letting $\Phi$ be very flexible we can learn powerful kernels in this manner.

    This can be implemented as shown below. We use Pytorch instead of TensorFlow this time for the sake of variety. Because we are dealing with images we give our projection $\Phi$ a convolutional architecture.

    We may then specify a DeepKernel in the following manner. By default GaussianRBF kernels are used for $k_a$ and $k_b$ and here we specify $\epsilon=0.01$, but we could alternatively set eps='trainable'.

    Since our PyTorch encoder expects the images in a (batch size, channels, height, width) format, we transpose the data. Note that this step could also be passed to the drift detector via the preprocess_fn kwarg:

    We then pass the kernel to the LearnedKernelDrift detector. By default $75%$ of the data is used to train the kernel and the MMD-test is performed on the other $25%$.

    Again, the detector can be saved and loaded:

    Finally, lets make some predictions with the detector:

    The detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The Seq2Seq outlier detector is suitable for both univariate and multivariate time series.

    Dataset

    We test the outlier detector on a synthetic dataset generated with the TimeSynth package. It allows you to generate a wide range of time series (e.g. pseudo-periodic, autoregressive or Gaussian Process generated signals) and noise types (white or red noise). It can be installed as follows:

    Additionally, this notebook requires the seaborn package for visualization which can be installed via pip:

    Create multivariate time series

    Define number of sampled points and the type of simulated time series. We use TimeSynth to generate sinusoidal signals with noise.

    Visualize:

    Load or define Seq2Seq outlier detector

    We still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a time series of instances and specify what percentage of those we consider to be normal via threshold_perc. First we create outliers by injecting noise in the time series via inject_outlier_ts. The noise can be regulated via the percentage of outliers (perc_outlier), the strength of the perturbation (n_std) and the minimum size of the noise perturbation (min_std). Let's assume we have some data which we know contains around 10% outliers in either of the features:

    Visualize outlier data used to determine the threshold:

    Let's infer the threshold. The inject_outlier_ts method distributes perturbations evenly across features. As a result, each feature contains about 5% outliers. We can either set the threshold over both features combined or determine a feature-wise threshold. Here we opt for the feature-wise threshold. This is for instance useful when different features have different variance or sensitivity to outliers. We also manually decrease the threshold a bit to increase the sensitivity of our detector:

    Let's save the outlier detector with the updated threshold:

    We can load the same detector via load_detector:

    Detect outliers

    Generate the outliers to detect:

    Predict outliers:

    Display results

    F1 score, accuracy, recall and confusion matrix:

    Plot the feature-wise outlier scores of the time series for each timestep vs. the outlier threshold:

    We can also plot the ROC curve using the instance level outlier scores:

    Sequence-to-Sequence
    Bidirectional
    LSTM
    Park et al. (2017)
    p
    and
    q
    based on the mean embeddings $\mu_{p}$ and $\mu_{q}$ in a reproducing kernel Hilbert space $F$:

    Given reference samples ${X_i}{i=1}^{N}$ and test samples ${Y_i}{i=t}^{t+W}$ we may compute an unbiased estimate $\widehat{MMD}^2(F, {X_i}{i=1}^N, {Y_i}{i=t}^{t+W})$ of the squared MMD between the two underlying distributions. The estimate can be updated at low-cost as new data points enter into the test-window. We use by default a radial basis function kernel, but users are free to pass their own kernel of preference to the detector.

    Online detectors assume the reference data is large and fixed and operate on single data points at a time (rather than batches). These data points are passed into the test-window and a two-sample test-statistic (in this case squared MMD) between the reference data and test-window is computed at each time-step. When the test-statistic exceeds a preconfigured threshold, drift is detected. Configuration of the thresholds requires specification of the expected run-time (ERT) which specifies how many time-steps that the detector, on average, should run for in the absence of drift before making a false detection. It also requires specification of a test-window size, with smaller windows allowing faster response to severe drift and larger windows allowing more power to detect slight drift.

    For high-dimensional data, we typically want to reduce the dimensionality before passing it to the detector. Following suggestions in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift, we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs (BBSDs) as out-of-the box preprocessing methods and note that PCA can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift.

    Detecting input data drift (covariate shift) $\Delta p(x)$ for text data requires a custom preprocessing step. We can pick up changes in the semantics of the input by extracting (contextual) embeddings and detect drift on those. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract. The library contains functionality to leverage pre-trained embeddings from HuggingFace's transformer package but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in the Text drift detection on IMDB movie reviews notebook.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • ert: The expected run-time in the absence of drift, starting from t=0.

    • window_size: The size of the sliding test-window used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    Keyword arguments:

    • backend: Backend used for the MMD implementation and configuration.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • kernel: Kernel used for the MMD computation, defaults to Gaussian RBF kernel.

    • sigma: Optionally set the GaussianRBF kernel bandwidth. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths. If sigma is not specified, the 'median heuristic' is adopted whereby sigma is set as the median pairwise distance between reference samples.

    • n_bootstraps: The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    • verbose: Whether or not to print progress during configuration.

    • input_shape: Shape of input data.

    • data_type: Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Additional PyTorch keyword arguments:

    • device: Device type used. The default None tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu' or 'cpu'. Only relevant for 'pytorch' backend.

    Initialized drift detector example:

    The same detector in PyTorch:

    We can also easily add preprocessing functions for both frameworks. The following example uses a randomly initialized image encoder in PyTorch:

    The same functionality is supported in TensorFlow and the main difference is that you would import from alibi_detect.cd.tensorflow import preprocess_drift. Other preprocessing steps such as the output of hidden layers of a model or extracted text embeddings using transformer models can be used in a similar way in both frameworks. TensorFlow example for the hidden layer output:

    Check out the Online Drift Detection on the Wine Quality Dataset example for more details.

    Alibi Detect also includes custom text preprocessing steps in both TensorFlow and PyTorch based on Huggingface's transformers package:

    Again the same functionality is supported in TensorFlow but with from alibi_detect.cd.tensorflow import preprocess_drift and from alibi_detect.models.tensorflow import TransformerEmbedding imports.

    Detect Drift

    We detect data drift by sequentially calling predict on single instances x_t (no batch dimension) as they each arrive. We can return the test-statistic and the threshold by setting return_test_stat to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the test-window (of the most recent window_size observations) has drifted from the reference data and 0 otherwise.

    • time: The number of observations that have been so far passed to the detector as test instances.

    • ert: The expected run-time the detector was configured to run at in the absence of drift.

    • test_stat: MMD^2 metric between the reference data and the test_window if return_test_stat equals True.

    • threshold: The value the test-statsitic is required to exceed for drift to be detected if return_test_stat equals True.

    Managing State

    The detector's state may be saved with the save_state method:

    The previously saved state may then be loaded via the load_state method:

    At any point, the state may be reset to t=0 with the reset_state method. When saving the detector with save_detector, the state will be saved, unless t=0 (see here).

    Examples

    Online Drift Detection on the Wine Quality Dataset

    Online Drift Detection on the Camelyon medical imaging dataset

    source
    Maximum Mean Discrepancy (MMD)
    MMD(F,p,q)=∣∣μp−μq∣∣F2MMD(F, p, q) = || \mu_{p} - \mu_{q} ||^2_{F}MMD(F,p,q)=∣∣μp​−μq​∣∣F2​
    Dataset

    The instances contain a person's characteristics like age, marital status or education while the label represents whether the person makes more or less than $50k per year. The dataset consists of a mixture of numerical and categorical features. It is originally not an outlier detection dataset so we will inject artificial outliers. It is fetched using the Alibi library, which can be installed with pip. We also use seaborn to visualize the data:

    Load adult dataset

    The fetch_adult function returns a Bunch object containing the features, the targets, the feature names and a mapping of the categories in each categorical variable.

    Shuffle data:

    Reorganize data so categorical features come first, remove some features and adjust feature_names and category_map accordingly:

    Preprocess data

    Normalize numerical features or scale numerical between -1 and 1:

    Fit OHE to categorical variables:

    Combine numerical and categorical data:

    Define train, validation (to find outlier threshold) and test set:

    Create outliers

    Inject outliers in the numerical features. First we need to know the features for each kind:

    Numerical

    Now we can add outliers to the validation (or threshold) and test sets. For the numerical data, we need to specify the numerical columns (cols), the percentage of outliers (perc_outlier), the strength (n_std) and the minimum size of the perturbation (min_std). The outliers are distributed evenly across the numerical features:

    Let's inspect an instance that was changed:

    Same thing for the test set:

    Apply one-hot encoding

    OHE to train, threshold and outlier sets:

    Load or define outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found here. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    The warning tells us we still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc.

    Let’s save the outlier detector with updated threshold:

    Detect outliers

    Display results

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    VAE
    Dataset

    The outlier detector needs to detect computer network intrusions using TCP dump data for a local-area network (LAN) simulating a typical U.S. Air Force LAN. A connection is a sequence of TCP packets starting and ending at some well defined times, between which data flows to and from a source IP address to a target IP address under some well defined protocol. Each connection is labeled as either normal, or as an attack.

    There are 4 types of attacks in the dataset:

    • DOS: denial-of-service, e.g. syn flood;

    • R2L: unauthorized access from a remote machine, e.g. guessing password;

    • U2R: unauthorized access to local superuser (root) privileges;

    • probing: surveillance and other probing, e.g., port scanning.

    The dataset contains about 5 million connection records.

    There are 3 types of features:

    • basic features of individual connections, e.g. duration of connection

    • content features within a connection, e.g. number of failed log in attempts

    • traffic features within a 2 second window, e.g. number of connections to the same host as the current connection

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load dataset

    We only keep a number of continuous (18 out of 41) features.

    Assume that a model is trained on normal instances of the dataset (not outliers) and standardization is applied:

    Apply standardization:

    Load or define outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found here. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    The warning tells us we still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have some data which we know contains around 5% outliers. The percentage of outliers can be set with perc_outlier in the create_outlier_batch function.

    We could have also inferred the threshold from the normal training data by setting threshold_perc e.g. at 99 and adding a bit of margin on top of the inferred threshold. Let's save the outlier detector with updated threshold:

    Detect outliers

    We now generate a batch of data with 10% outliers and detect the outliers in the batch.

    Predict outliers:

    Display results

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    We can clearly see that some outliers are very easy to detect while others have outlier scores closer to the normal data. We can also plot the ROC curve for the outlier scores of the detector:

    Investigate instance level outlier

    We can now take a closer look at some of the individual predictions on X_outlier.

    The srv_count feature is responsible for a lot of the displayed outliers.

    VAE

    To target the desired ERT, thresholds are configured during an initial configuration phase via simulation. This configuration process is only suitable when the amount reference data (most likely the training data of the model of interest) is relatively large (ideally around an order of magnitude larger than the desired ERT). Configuration can be expensive (less so with a GPU) but allows the detector to operate at low-cost during deployment.

    This notebook demonstrates online drift detection using two different two-sample distance metrics for the test-statistic, the maximum mean discrepency (MMD) and least-squared density difference (LSDD), both of which can be updated sequentially at low cost.

    Backend

    The online detectors are implemented in both the PyTorch and TensorFlow frameworks with support for CPU and GPU. Various preprocessing steps are also supported out-of-the box in Alibi Detect for both frameworks and an example will be given in this notebook. Alibi Detect does however not install PyTorch for you. Check the PyTorch docs how to do this.

    Dataset

    The Wine Quality Data Set consists of 4898 and 1599 samples of white and red wine respectively. Each sample has an associated quality (as determined by experts) and 11 numeric features indicating its acidity, density, pH etc. We consider the regression problem of tring to predict the quality of white wine samples given these features. We will then consider whether the model remains suitable for predicting the quality of red wine samples or whether the associated change in the underlying distribution should be considered as drift.

    Online detection with MMD and Pytorch

    The Maximum Mean Discepency (MMD) is a distance-based measure between 2 distributions p and q based on the mean embeddings $\mu_{p}$ and $\mu_{q}$ in a reproducing kernel Hilbert space $F$:

    Given reference samples ${X_i}{i=1}^{N}$ and test samples ${Y_i}{i=t}^{t+W}$ we may compute an unbiased estimate $\widehat{MMD}^2(F, {X_i}{i=1}^N, {Y_i}{i=t}^{t+W})$ of the squared MMD between the two underlying distributions. Depending on the size of the reference and test windows, $N$ and $W$ respectively, this can be relatively expensive. However, once computed it is possible to update the statistic to estimate to the squared MMD between the distributions underlying ${X_i}{i=1}^{N}$ and ${Y_i}{i=t+1}^{t+1+W}$ at a very low cost, making it suitable for online drift detection.

    By default we use a radial basis function kernel, but users are free to pass their own kernel of preference to the detector.

    Load data

    First we load in the data:

    We can see that the data for both red and white wine samples take the same format.

    We shuffle and normalise the data such that each feature takes a value in [0,1], as does the quality we seek to predict. We assue that our model was trained on white wine samples, which therefore forms the reference distribution, and that red wine samples can be considered to be drawn from a drifted distribution.

    Although it may not be necessary on this relatively low-dimensional data for which individual features are semantically meaningful, we demonstrate how principle component analysis (PCA) can be performed as a preprocessing stage to project raw data onto a lower dimensional representation which more concisely captures the factors of variation in the data. As not to bias the detector it is necessary to fit the projection using a split of the data which isn't then passed as reference data. We additionally split off some white wine samples to act as undrifted data during deployment.

    Now we define a PCA object to be used as a preprocessing function to project the 11-D data onto a 2-D representation. We learn the first 2 principal components on the training split of the reference data.

    Hopefully the learned preprocessing step has learned a projection such that in the lower dimensional space the two samples are distinguishable.

    Now we can define our online drift detector. We specify an expected run-time (in the absence of drift) of 50 time-steps, and a window size of 10 time-steps. Upon initialising the detector thresholds will be computed using 2500 boostrap samples. These values of ert, window_size and n_bootstraps are lower than a typical use-case in order to demonstrate the average behaviour of the detector over a large number of runs in a reasonable time.

    We now define a function which will simulate a single run and return the run-time. Note how the detector acts on single instances at a time, the run-time is considered as the time elapsed after the test-window has been filled, and that the detector is stateful and must be reset between detections.

    Now we look at the distribution of run-times when operating on the held-out data from the reference distribution of white wine samples. We report the average run-time, however note that the targeted run-time distribution, a Geometric distribution with mean ert, is very high variance so the empirical average may not be that close to ert over a relatively small number of runs. We can see that the detector accurately targets the desired Geometric distribution however by inspecting the linearity of a Q-Q plot.

    If we run the detector in an identical manner but on data from the drifted distribution of red wine samples the average run-time is much lower.

    Online detection with LSDD and TensorFlow

    Here we address the same problem but using the least squares density difference (LSDD) as the two-sample distance in a manner similar to Bu et al. (2017). The LSDD between two distributions $p$ and $q$ on $\mathcal{X}$ is defined as LSDD(p,q)=∫X(p(x)−q(x))2 dxLSDD(p,q) = \int_{\mathcal{X}} (p(x)-q(x))^2 \,dxLSDD(p,q)=∫X​(p(x)−q(x))2dx and also has an empirical estimate $\widehat{LSDD}({X_i}{i=1}^N, {Y_i}{i=t}^{t+W})$ that can be updated at low cost as the test window is updated to ${Y_i}_{i=t+1}^{t+1+W}$.

    We additionally show that TensorFlow can also be used as the backend and that sometimes it is not necessary to perform preprocessing, making definition of the drift detector simpler. Moreover, in the absence of a learned preprocessing stage we may use all of the reference data available.

    And now we define the LSDD-based online drift detector, again with an ert of 50 and window_size of 10.

    We run this new detector on the held out reference data and again see that in the absence of drift the distribution of run-times follows a Geometric distribution with mean ert.

    And when drift has occured the detector is very fast to respond.

    MMD(F,p,q)=∣∣μp−μq∣∣F2MMD(F, p, q) = || \mu_{p} - \mu_{q} ||^2_{F}MMD(F,p,q)=∣∣μp​−μq​∣∣F2​
    and also has an empirical estimate $\widehat{LSDD}({X_i}{i=1}^N, {Y_i}{i=t}^{t+W})$ that can be updated at low cost as the test window is updated to ${Y_i}_{i=t+1}^{t+1+W}$. The detector is motivated by, but is a modified version of,
    .

    Online detectors assume the reference data is large and fixed and operate on single data points at a time (rather than batches). These data points are passed into the test-window and a two-sample test-statistic (in this case an estimate of LSDD) between the reference data and test-window is computed at each time-step. When the test-statistic exceeds a preconfigured threshold, drift is detected. Configuration of the thresholds requires specification of the expected run-time (ERT) which specifies how many time-steps that the detector, on average, should run for in the absence of drift before making a false detection. It also requires specification of a test-window size, with smaller windows allowing faster response to severe drift and larger windows allowing more power to detect slight drift.

    For high-dimensional data, we typically want to reduce the dimensionality before passing it to the detector. Following suggestions in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift, we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs (BBSDs) as out-of-the box preprocessing methods and note that PCA can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift.

    Detecting input data drift (covariate shift) $\Delta p(x)$ for text data requires a custom preprocessing step. We can pick up changes in the semantics of the input by extracting (contextual) embeddings and detect drift on those. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract. The library contains functionality to leverage pre-trained embeddings from HuggingFace's transformer package but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in the Text drift detection on IMDB movie reviews notebook.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • ert: The expected run-time in the absence of drift, starting from t=0.

    • window_size: The size of the sliding test-window used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    Keyword arguments:

    • backend: Backend used for the LSDD implementation and configuration.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • sigma: Optionally set the bandwidth of the Gaussian kernel used in estimating the LSDD. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths. If sigma is not specified, the 'median heuristic' is adopted whereby sigma is set as the median pairwise distance between reference samples.

    • n_bootstraps: The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ERT.

    • n_kernel_centers: The number of reference samples to use as centers in the Gaussian kernel model used to estimate LSDD. Defaults to 2*window_size.

    • lambda_rd_max: The maximum relative difference between two estimates of LSDD that the regularization parameter lambda is allowed to cause. Defaults to 0.2 as in the paper.

    • verbose: Whether or not to print progress during configuration.

    • input_shape: Shape of input data.

    • data_type: Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Additional PyTorch keyword arguments:

    • device: Device type used. The default None tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu' or 'cpu'. Only relevant for 'pytorch' backend.

    Initialized drift detector example:

    The same detector in PyTorch:

    We can also easily add preprocessing functions for both frameworks. The following example uses a randomly initialized image encoder in PyTorch:

    The same functionality is supported in TensorFlow and the main difference is that you would import from alibi_detect.cd.tensorflow import preprocess_drift. Other preprocessing steps such as the output of hidden layers of a model or extracted text embeddings using transformer models can be used in a similar way in both frameworks. TensorFlow example for the hidden layer output:

    Check out the Online Drift Detection on the Wine Quality Dataset example for more details.

    Alibi Detect also includes custom text preprocessing steps in both TensorFlow and PyTorch based on Huggingface's transformers package:

    Again the same functionality is supported in TensorFlow but with from alibi_detect.cd.tensorflow import preprocess_drift and from alibi_detect.models.tensorflow import TransformerEmbedding imports.

    Detect Drift

    We detect data drift by sequentially calling predict on single instances x_t (no batch dimension) as they each arrive. We can return the test-statistic and the threshold by setting return_test_stat to True.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the test-window (of the most recent window_size observations) has drifted from the reference data and 0 otherwise.

    • time: The number of observations that have been so far passed to the detector as test instances.

    • ert: The expected run-time the detector was configured to run at in the absence of drift.

    • test_stat: LSDD metric between the reference data and the test_window if return_test_stat equals True.

    • threshold: The value the test-statsitic is required to exceed for drift to be detected if return_test_stat equals True.

    Managing State

    The detector's state may be saved with the save_state method:

    The previously saved state may then be loaded via the load_state method:

    At any point, the state may be reset to t=0 with the reset_state method. When saving the detector with save_detector, the state will be saved, unless t=0 (see here).

    Examples

    Online Drift Detection on the Wine Quality Dataset

    source
    LSDD(p,q)=∫X(p(x)−q(x))2 dxLSDD(p,q) = \int_{\mathcal{X}} (p(x)-q(x))^2 \,dxLSDD(p,q)=∫X​(p(x)−q(x))2dx
    Bu et al. (2017)
    from alibi_detect.cd import MMDDrift
    
    cd_tf = MMDDrift(x_ref, backend='tensorflow', p_val=.05)
    cd_torch = MMDDrift(x_ref, backend='pytorch', p_val=.05)
    cd_keops = MMDDrift(x_ref, backend='keops', p_val=.05)
    from functools import partial
    import torch
    import torch.nn as nn
    from alibi_detect.cd.pytorch import preprocess_drift
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # define encoder
    encoder_net = nn.Sequential(
        nn.Conv2d(3, 64, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(64, 128, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(128, 512, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(2048, 32)
    ).to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, device=device, batch_size=512)
    
    cd = MMDDrift(x_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn)
    from alibi_detect.cd.tensorflow import HiddenOutput, preprocess_drift
    
    model = # TensorFlow model; tf.keras.Model or tf.keras.Sequential
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(model, layer=-1), batch_size=128)
    
    cd = MMDDrift(x_ref, backend='tensorflow', p_val=.05, preprocess_fn=preprocess_fn)
    import torch
    import torch.nn as nn
    from transformers import AutoTokenizer
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    model_name = 'bert-base-cased'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    embedding_type = 'hidden_state'
    layers = [5, 6, 7]
    embed = TransformerEmbedding(model_name, embedding_type, layers)
    model = nn.Sequential(embed, nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, enc_dim)).to(device).eval()
    preprocess_fn = partial(preprocess_drift, model=model, tokenizer=tokenizer, max_len=512, batch_size=32)
    
    # initialise drift detector
    cd = MMDDrift(x_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn)
    preds = cd.predict(X, return_p_val=True, return_distance=True)
    !pip install seaborn
    #| scrolled: true
    import os
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix
    from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
    import tensorflow as tf
    from tensorflow.keras.layers import Dense, Input, LSTM
    
    from alibi_detect.od import LLR
    from alibi_detect.datasets import fetch_genome
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_roc
    (X_train, y_train), (X_val, y_val), (X_test, y_test) = \
            fetch_genome(return_X_y=True, return_labels=False)
    print(X_train.shape, y_train.shape, X_val.shape, y_val.shape, X_test.shape, y_test.shape)
    print('Fraction of outliers in train, val and test sets: '
          '{:.2f}, {:.2f} and {:.2f}'.format(y_train.mean(), y_val.mean(), y_test.mean()))
    genome_dim = 249  # not 250 b/c we use 1->249 as input and 2->250 as target
    input_dim = 4  # ACGT nucleobases
    hidden_dim = 2000
    
    inputs = Input(shape=(genome_dim,), dtype=tf.int8)
    x = tf.one_hot(tf.cast(inputs, tf.int32), input_dim)
    x = LSTM(hidden_dim, return_sequences=True)(x)
    logits = Dense(input_dim, activation=None)(x)
    model = tf.keras.Model(inputs=inputs, outputs=logits, name='LlrLSTM')
    def loss_fn(y, x):
        y = tf.one_hot(tf.cast(y, tf.int32), 4)  # ACGT on-hot encoding
        return tf.nn.softmax_cross_entropy_with_logits(y, x, axis=-1)
    def likelihood_fn(y, x):
        return -loss_fn(y, x)
    load_pretrained = True
    #| scrolled: false
    filepath = os.path.join(os.getcwd(), 'my_path')  # change to download directory
    detector_type = 'outlier'
    dataset = 'genome'
    detector_name = 'LLR'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:
        # initialize detector
        od = LLR(threshold=None, model=model, log_prob=likelihood_fn, sequential=True)
        
        # train
        od.fit(
            X_train,
            mutate_fn_kwargs=dict(rate=.2, feature_range=(0,3)),
            mutate_batch_size=1000,
            loss_fn=loss_fn,
            optimizer=tf.keras.optimizers.Adam(learning_rate=5e-4),
            epochs=20,
            batch_size=100,
            verbose=False
        )
        
        # save the trained outlier detector
        save_detector(od, filepath)
    idx_in, idx_ood = np.where(y_test == 0)[0], np.where(y_test == 1)[0]
    n_in, n_ood = idx_in.shape[0], idx_ood.shape[0]
    n_sample = 100000  # sample 100k inliers and outliers each
    sample_in = np.random.choice(n_in, size=n_sample, replace=False)
    sample_ood = np.random.choice(n_ood, size=n_sample, replace=False)
    X_test_in, X_test_ood = X_test[idx_in[sample_in]], X_test[idx_ood[sample_ood]]
    y_test_in, y_test_ood = y_test[idx_in[sample_in]], y_test[idx_ood[sample_ood]]
    X_test_sample = np.concatenate([X_test_in, X_test_ood])
    y_test_sample = np.concatenate([y_test_in, y_test_ood])
    print(X_test_in.shape, X_test_ood.shape)
    # semantic model
    logp_s_in = od.logp_alt(od.dist_s, X_test_in, batch_size=100)
    logp_s_ood = od.logp_alt(od.dist_s, X_test_ood, batch_size=100)
    logp_s = np.concatenate([logp_s_in, logp_s_ood])
    # background model
    logp_b_in = od.logp_alt(od.dist_b, X_test_in, batch_size=100)
    logp_b_ood = od.logp_alt(od.dist_b, X_test_ood, batch_size=100)
    logp_b = np.concatenate([logp_b_in, logp_b_ood])
    # show histograms
    plt.hist(logp_s_in, bins=100, label='in');
    plt.hist(logp_s_ood, bins=100, label='ood');
    plt.title('Semantic Log Probabilities')
    plt.legend()
    plt.show()
    
    plt.hist(logp_b_in, bins=100, label='in');
    plt.hist(logp_b_ood, bins=100, label='ood');
    plt.title('Background Log Probabilities')
    plt.legend()
    plt.show()
    llr_in = logp_s_in - logp_b_in
    llr_ood = logp_s_ood - logp_b_ood
    plt.hist(llr_in, bins=100, label='in');
    plt.hist(llr_ood, bins=100, label='ood');
    plt.title('Likelihood Ratio')
    plt.legend()
    plt.show()
    llr = np.concatenate([llr_in, llr_ood])
    roc_data = {'LLR': {'scores': -llr, 'labels': y_test_sample}}
    plot_roc(roc_data)
    n, frac_outlier = 1000, .3
    perc_outlier = 100 * frac_outlier
    n_sample_in, n_sample_ood = int(n * (1 - frac_outlier)), int(n * frac_outlier)
    idx_in, idx_ood = np.where(y_val == 0)[0], np.where(y_val == 1)[0]
    n_in, n_ood = idx_in.shape[0], idx_ood.shape[0]
    sample_in = np.random.choice(n_in, size=n_sample_in, replace=False)
    sample_ood = np.random.choice(n_ood, size=n_sample_ood, replace=False)
    X_thr_in, X_thr_ood = X_val[idx_in[sample_in]], X_val[idx_ood[sample_ood]]
    X_threshold = np.concatenate([X_thr_in, X_thr_ood])
    print(X_threshold.shape)
    od.infer_threshold(X_threshold, threshold_perc=perc_outlier, batch_size=100)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    od_preds = od.predict(X_test_sample, batch_size=100)
    y_pred = od_preds['data']['is_outlier']
    labels = ['normal', 'outlier']
    f1 = f1_score(y_test_sample, y_pred)
    acc = accuracy_score(y_test_sample, y_pred)
    prec = precision_score(y_test_sample, y_pred)
    rec = recall_score(y_test_sample, y_pred)
    print('F1 score: {:.3f} -- Accuracy: {:.3f} -- Precision: {:.3f} '
          '-- Recall: {:.3f}'.format(f1, acc, prec, rec))
    cm = confusion_matrix(y_test_sample, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    roc_data = {'LLR': {'scores': od_preds['data']['instance_score'], 'labels': y_test_sample}}
    plot_roc(roc_data)
    import tensorflow as tf
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, Input
    from alibi_detect.cd import ClassifierDrift
    
    model = tf.keras.Sequential(
      [
          Input(shape=(32, 32, 3)),
          Conv2D(8, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(16, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(32, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(2, activation='softmax')
      ]
    )
    
    cd = ClassifierDrift(x_ref, model, p_val=.05, preds_type='probs', n_folds=5, epochs=2)
    import torch.nn as nn
    
    model = nn.Sequential(
        nn.Conv2d(3, 8, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(8, 16, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(16, 32, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(128, 2)
    )
    
    cd = ClassifierDrift(x_ref, model, backend='pytorch', p_val=.05, preds_type='logits')
    preds = cd.predict(x)
    from alibi_detect.cd import LSDDDrift
    
    cd = LSDDDrift(x_ref, backend='tensorflow', p_val=.05)
    cd = LSDDDrift(x_ref, backend='pytorch', p_val=.05)
    from functools import partial
    import torch
    import torch.nn as nn
    from alibi_detect.cd.pytorch import preprocess_drift
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # define encoder
    encoder_net = nn.Sequential(
        nn.Conv2d(3, 64, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(64, 128, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(128, 512, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(2048, 32)
    ).to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, device=device, batch_size=512)
    
    cd = LSDDDrift(x_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn)
    from alibi_detect.cd.tensorflow import HiddenOutput, preprocess_drift
    
    model = # TensorFlow model; tf.keras.Model or tf.keras.Sequential
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(model, layer=-1), batch_size=128)
    
    cd = LSDDDrift(x_ref, backend='tensorflow', p_val=.05, preprocess_fn=preprocess_fn)
    import torch
    import torch.nn as nn
    from transformers import AutoTokenizer
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    model_name = 'bert-base-cased'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    embedding_type = 'hidden_state'
    layers = [5, 6, 7]
    embed = TransformerEmbedding(model_name, embedding_type, layers)
    model = nn.Sequential(embed, nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, enc_dim)).to(device).eval()
    preprocess_fn = partial(preprocess_drift, model=model, tokenizer=tokenizer, max_len=512, batch_size=32)
    
    # initialise drift detector
    cd = LSDDDrift(x_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn)
    preds = cd.predict(X, return_p_val=True, return_distance=True)
    LARGE_ARTEFACTS: list = ['x_ref', 'c_ref', 'preprocess_fn']
    BaseDetector(self)
    predict(X: numpy.ndarray)
    score(X: numpy.ndarray)
    from_config(config: dict)
    get_config() -> dict
    This exists to distinguish between detectors with and without support for config saving and loading. Once all
    detector support this then this protocol will be removed.
    Detector(self, *args, **kwargs)
    predict() -> typing.Any
    DriftConfigMixin(self, /, *args, **kwargs)
    from_config(config: dict)
    get_config() -> dict
    fit(args, kwargs) -> None
    default(obj)
    load_state(filepath: Union[str, os.PathLike])
    save_state(filepath: Union[str, os.PathLike])
    infer_threshold(args, kwargs) -> None
    adversarial_correction_dict()
    adversarial_prediction_dict()
    concept_drift_dict()
    outlier_prediction_dict()
    !pip install torchvision
    import torch
    import tensorflow as tf
    import torchvision
    import numpy as np
    import matplotlib.pyplot as plt
    from alibi_detect.cd import SpotTheDiffDrift
    
    np.random.seed(0)
    torch.manual_seed(0)
    tf.random.set_seed(0)
    %matplotlib inline
    MNIST_PATH = 'my_path'
    DOWNLOAD = True
    MISSING_NUMBER = 0
    N = 10000
    
    # Load and shuffle data
    mnist_train_ds = torchvision.datasets.MNIST(MNIST_PATH, train=True, download=DOWNLOAD)
    all_x, all_y = mnist_train_ds.data, mnist_train_ds.targets
    perm = np.random.permutation(len(all_x))
    all_x, all_y = all_x[perm], all_y[perm]
    all_x = all_x[:, None, : , :].numpy().astype(np.float32)/255.
    
    # Create a reference and test set
    x_ref = all_x[:N]
    x = all_x[N:2*N]
    
    # Remove a class from reference set
    x_ref = x_ref[all_y[:10000] != MISSING_NUMBER]
    cd = SpotTheDiffDrift(
        x_ref,
        n_diffs=1,
        l1_reg=1e-4,
        backend='tensorflow',
        verbose=1,
        learning_rate=1e-2, 
        epochs=5, 
        batch_size=64,
    )
    preds = cd.predict(x)
    
    print(f"Drift? {'Yes' if preds['data']['is_drift'] else 'No'}")
    print(f"p-value: {preds['data']['p_val']}")
    print(f"Diff coeff: {preds['data']['diff_coeffs']}")
    diff = preds['data']['diffs'][0,0]
    plt.imshow(diff, cmap='RdBu', vmin=-np.max(np.abs(diff)), vmax=np.max(np.abs(diff)))
    plt.colorbar()
    import pandas as pd
    red_df = pd.read_csv(
        "https://storage.googleapis.com/seldon-datasets/wine_quality/winequality-red.csv", sep=';'
    )
    white_df = pd.read_csv(
        "https://storage.googleapis.com/seldon-datasets/wine_quality/winequality-white.csv", sep=';'
    )
    white_df.describe()
    red_df.describe()
    white, red = np.asarray(white_df, np.float32)[:, :-1], np.asarray(red_df, np.float32)[:, :-1]
    n_white, n_red = white.shape[0], red.shape[0]
    
    col_maxes = white.max(axis=0)
    white, red = white / col_maxes, red / col_maxes
    white, red = white[np.random.permutation(n_white)], red[np.random.permutation(n_red)]
    x, x_corr = white, red
    x_ref = x[:len(x)//2]
    x_h0 = x[len(x)//2:]
    cd = SpotTheDiffDrift(
        x_ref,
        n_diffs=1,
        l1_reg=1e-4,
        backend='pytorch',
        verbose=1,
        learning_rate=1e-2, 
        epochs=5, 
        batch_size=64,
    )
    
    preds_h0 = cd.predict(x_h0)
    preds_corr = cd.predict(x_corr)
    print(f"Drift on h0? {'Yes' if preds_h0['data']['is_drift'] else 'No'}")
    print(f"p-value on h0: {preds_h0['data']['p_val']}")
    print(f"Drift on corrupted? {'Yes' if preds_corr['data']['is_drift'] else 'No'}")
    print(f"p-value on corrupted:: {preds_corr['data']['p_val']}")
    diff = preds_corr['data']['diffs'][0]
    print(f"Diff coeff: {preds_corr['data']['diff_coeffs']}")
    plt.barh(white_df.columns[:-1], diff)
    plt.xlim((-1.1*np.max(np.abs(diff)), 1.1*np.max(np.abs(diff))))
    plt.axvline(0, linestyle='--', color='black')
    plt.show()
    encoder_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(32, 32, 3)),
            Conv2D(32, 4, strides=2, padding='same', 
                   activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
            Conv2D(64, 4, strides=2, padding='same', 
                   activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
            Conv2D(256, 4, strides=2, padding='same', 
                   activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
            Flatten(),
            Dense(40)
        ]
    )
    decoder_net = tf.keras.Sequential(
    [
            InputLayer(input_shape=(40,)),
            Dense(4 * 4 * 128, activation=tf.nn.relu),
            Reshape(target_shape=(4, 4, 128)),
            Conv2DTranspose(256, 4, strides=2, padding='same', 
                            activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
            Conv2DTranspose(64, 4, strides=2, padding='same', 
                            activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
            Conv2DTranspose(3, 4, strides=2, padding='same', 
                            activation=None, kernel_regularizer=l1(1e-5))
        ]
    )
    inputs = tf.keras.Input(shape=(input_dim,))
    outputs = tf.keras.layers.Dense(output_dim, activation=tf.nn.softmax)(inputs)
    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    from alibi_detect.ad import AdversarialAE
    
    ad = AdversarialAE(
        encoder_net=encoder_net, 
        decoder_net=decoder_net, 
        model=model,
        temperature=0.5
    )
    ad.fit(X_train, epochs=50)
    ad.infer_threshold(X_train, threshold_perc=95, batch_size=64)
    preds_detect = ad.predict(X, batch_size=64, return_instance_score=True)
    preds_correct = ad.correct(X, batch_size=64, return_instance_score=True)
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    
    from alibi_detect.cd import ClassifierDrift
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    np.random.seed(0)
    n_test = X_test.shape[0]
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    idx_h0 = np.delete(np.arange(n_test), idx, axis=0)
    X_ref,y_ref = X_test[idx], y_test[idx]
    X_h0, y_h0 = X_test[idx_h0], y_test[idx_h0]
    print(X_ref.shape, X_h0.shape)
    n_corr = len(corruption)
    X_c = [X_corr[i * n_test:(i + 1) * n_test] for i in range(n_corr)]
    i = 6
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow(X_test[i])
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, Input
    
    tf.random.set_seed(0)
    
    model = tf.keras.Sequential(
      [
          Input(shape=(32, 32, 3)),
          Conv2D(8, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(16, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(32, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(2, activation='softmax')
      ]
    )
    
    cd = ClassifierDrift(X_ref, model, p_val=.05, train_size=.75, epochs=1)
    from alibi_detect.saving import save_detector, load_detector
    # Save detector
    filepath = 'tf_detector'
    save_detector(cd, filepath)
    
    # Load detector
    cd = load_detector(filepath)
    from timeit import default_timer as timer
    
    labels = ['No!', 'Yes!']
    
    def make_predictions(cd, x_h0, x_corr, corruption):
        t = timer()
        preds = cd.predict(x_h0)
        dt = timer() - t
        print('No corruption')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print(f'p-value: {preds["data"]["p_val"]:.3f}')
        print(f'Time (s) {dt:.3f}')
        
        if isinstance(x_corr, list):
            for x, c in zip(x_corr, corruption):
                t = timer()
                preds = cd.predict(x)
                dt = timer() - t
                print('')
                print(f'Corruption type: {c}')
                print('Drift? {}'.format(labels[preds['data']['is_drift']]))
                print(f'p-value: {preds["data"]["p_val"]:.3f}')
                print(f'Time (s) {dt:.3f}')
    make_predictions(cd, X_h0, X_c, corruption)
    cd = ClassifierDrift(X_ref, model, p_val=.05, n_folds=5, epochs=1)
    #| scrolled: true
    make_predictions(cd, X_h0, X_c, corruption)
    import torch
    import torch.nn as nn
    
    # set random seed and device
    seed = 0
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # define the projection
    proj = nn.Sequential(
        nn.Conv2d(3, 8, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(8, 16, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(16, 32, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
    ).to(device)
    from alibi_detect.utils.pytorch.kernels import DeepKernel
    kernel = DeepKernel(proj, eps=0.01)
    def permute_c(x):
        return np.transpose(x.astype(np.float32), (0, 3, 1, 2))
    
    X_ref_pt = permute_c(X_ref)
    X_h0_pt = permute_c(X_h0)
    X_c_pt = [permute_c(xc) for xc in X_c]
    print(X_ref_pt.shape, X_h0_pt.shape, X_c_pt[0].shape)
    from alibi_detect.cd import LearnedKernelDrift
    cd = LearnedKernelDrift(X_ref_pt, kernel, backend='pytorch', p_val=.05, epochs=1)
    from alibi_detect.saving import save_detector, load_detector
    # Save detector
    filepath = 'torch_detector'
    save_detector(cd, filepath)
    
    # Load detector
    cd = load_detector(filepath)
    make_predictions(cd, X_h0_pt, X_c_pt, corruption)
    !pip install git+https://github.com/TimeSynth/TimeSynth.git
    !pip install seaborn
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, recall_score
    import tensorflow as tf
    import timesynth as ts
    
    from alibi_detect.od import OutlierSeq2Seq
    from alibi_detect.utils.perturbation import inject_outlier_ts
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_feature_outlier_ts, plot_roc
    n_points = int(1e6)  # number of timesteps
    perc_train = 80  # percentage of instances used for training
    perc_threshold = 10  # percentage of instances used to determine threshold
    n_train = int(n_points * perc_train * .01)
    n_threshold = int(n_points * perc_threshold * .01)
    n_features = 2  # number of features in the time series
    seq_len = 50  # sequence length
    # set random seed
    np.random.seed(0)
    
    # timestamps
    time_sampler = ts.TimeSampler(stop_time=n_points // 4)
    time_samples = time_sampler.sample_regular_time(num_points=n_points)
    
    # create time series
    ts1 = ts.TimeSeries(
        signal_generator=ts.signals.Sinusoidal(frequency=0.25),
        noise_generator=ts.noise.GaussianNoise(std=0.1)
    )
    samples1 = ts1.sample(time_samples)[0].reshape(-1, 1)
    
    ts2 = ts.TimeSeries(
        signal_generator=ts.signals.Sinusoidal(frequency=0.15),
        noise_generator=ts.noise.RedNoise(std=.7, tau=0.5)
    )
    samples2 = ts2.sample(time_samples)[0].reshape(-1, 1)
    
    # combine signals
    X = np.concatenate([samples1, samples2], axis=1).astype(np.float32)
    
    # split dataset into train, infer threshold and outlier detection sets
    X_train = X[:n_train]
    X_threshold = X[n_train:n_train+n_threshold]
    X_outlier = X[n_train+n_threshold:]
    
    # scale using the normal training data
    mu, sigma = X_train.mean(axis=0), X_train.std(axis=0)
    X_train = (X_train - mu) / sigma
    X_threshold = (X_threshold - mu) / sigma
    X_outlier = (X_outlier - mu) / sigma
    print(X_train.shape, X_threshold.shape, X_outlier.shape)
    n_features = X.shape[-1]
    istart, istop = 50, 100
    for f in range(n_features):
        plt.plot(X_train[istart:istop, f], label='X_train')
        plt.title('Feature {}'.format(f))
        plt.xlabel('Time')
        plt.ylabel('Feature value')
        plt.legend()
        plt.show()
    load_outlier_detector = False
    filepath = 'my_path'  # change to directory where model is saved
    if load_outlier_detector:  # load pretrained outlier detector
        od = load_detector(filepath)
    else:  # define model, initialize, train and save outlier detector
        
        # initialize outlier detector
        od = OutlierSeq2Seq(n_features,
                            seq_len,
                            threshold=None,
                            latent_dim=100)
        
        # train
        od.fit(X_train,
               epochs=10,
               verbose=False)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    np.random.seed(0)
    
    X_thr = X_threshold.copy()
    data = inject_outlier_ts(X_threshold, perc_outlier=10, perc_window=10, n_std=2., min_std=1.)
    X_threshold = data.data
    print(X_threshold.shape)
    istart, istop = 0, 50
    for f in range(n_features):
        plt.plot(X_threshold[istart:istop, f], label='outliers')
        plt.plot(X_thr[istart:istop, f], label='original')
        plt.title('Feature {}'.format(f))
        plt.xlabel('Time')
        plt.ylabel('Feature value')
        plt.legend()
        plt.show()
    od.infer_threshold(X_threshold, threshold_perc=[95, 95])
    od.threshold -= .15
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    od = load_detector(filepath)
    np.random.seed(1)
    
    X_out = X_outlier.copy()
    data = inject_outlier_ts(X_outlier, perc_outlier=10, perc_window=10, n_std=2., min_std=1.)
    X_outlier, y_outlier, labels = data.data, data.target.astype(int), data.target_names
    print(X_outlier.shape, y_outlier.shape)
    od_preds = od.predict(X_outlier,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    acc = accuracy_score(y_outlier, y_pred)
    rec = recall_score(y_outlier, y_pred)
    print('F1 score: {:.3f} -- Accuracy: {:.3f} -- Recall: {:.3f}'.format(f1, acc, rec))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_feature_outlier_ts(od_preds,
                            X_outlier, 
                            od.threshold[0],
                            window=(150, 200),
                            t=time_samples,
                            X_orig=X_out)
    roc_data = {'S2S': {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}}
    plot_roc(roc_data)
    from alibi_detect.cd import MMDDriftOnline
    
    cd = MMDDriftOnline(x_ref, ert, window_size, backend='tensorflow')
    cd = MMDDriftOnline(x_ref, ert, window_size, backend='pytorch')
    from functools import partial
    import torch
    import torch.nn as nn
    from alibi_detect.cd.pytorch import preprocess_drift
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # define encoder
    encoder_net = nn.Sequential(
        nn.Conv2d(3, 64, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(64, 128, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(128, 512, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(2048, 32)
    ).to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, device=device, batch_size=512)
    
    cd = MMDDriftOnline(x_ref, ert, window_size, backend='pytorch', preprocess_fn=preprocess_fn)
    from alibi_detect.cd.tensorflow import HiddenOutput, preprocess_drift
    
    model = # TensorFlow model; tf.keras.Model or tf.keras.Sequential
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(model, layer=-1), batch_size=128)
    
    cd = MMDDriftOnline(x_ref, ert, window_size, backend='tensorflow', preprocess_fn=preprocess_fn)
    import torch
    import torch.nn as nn
    from transformers import AutoTokenizer
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    model_name = 'bert-base-cased'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    embedding_type = 'hidden_state'
    layers = [5, 6, 7]
    embed = TransformerEmbedding(model_name, embedding_type, layers)
    model = nn.Sequential(embed, nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, enc_dim)).to(device).eval()
    preprocess_fn = partial(preprocess_drift, model=model, tokenizer=tokenizer, max_len=512, batch_size=32)
    
    # initialise drift detector
    cd = MMDDriftOnline(x_ref, ert, window_size, backend='pytorch', preprocess_fn=preprocess_fn)
    preds = cd.predict(x_t, return_test_stat=True)
    cd = MMDDriftOnline(x_ref, ert, window_size)  # Instantiate detector at t=0
    cd.predict(x_1)  # t=1
    cd.save_state('checkpoint_t1')  # Save state at t=1
    cd.predict(x_2)  # t=2
    # Load state at t=1
    cd.load_state('checkpoint_t1')
    !pip install alibi seaborn
    import os
    import alibi
    import matplotlib
    %matplotlib inline
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, precision_score, recall_score
    from sklearn.preprocessing import OneHotEncoder
    import tensorflow as tf
    tf.keras.backend.clear_session()
    from tensorflow.keras.layers import Dense, InputLayer
    
    from alibi_detect.od import OutlierVAE
    from alibi_detect.utils.perturbation import inject_outlier_tabular
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score
    def set_seed(s=0):
        np.random.seed(s)
        tf.random.set_seed(s)
    adult = alibi.datasets.fetch_adult()
    X, y = adult.data, adult.target
    feature_names = adult.feature_names
    category_map_tmp = adult.category_map
    set_seed(0)
    Xy_perm = np.random.permutation(np.c_[X, y])
    X, y = Xy_perm[:,:-1], Xy_perm[:,-1]
    keep_cols = [2, 3, 5, 0, 8, 9, 10]
    feature_names = feature_names[2:4] + feature_names[5:6] + feature_names[0:1] + feature_names[8:11]
    print(feature_names)
    X = X[:, keep_cols]
    print(X.shape)
    category_map = {}
    i = 0
    for k, v in category_map_tmp.items():
        if k in keep_cols:
            category_map[i] = v
            i += 1
    minmax = False
    X_num = X[:, -4:].astype(np.float32, copy=False)
    if minmax:
        xmin, xmax = X_num.min(axis=0), X_num.max(axis=0)
        rng = (-1., 1.)
        X_num_scaled = (X_num - xmin) / (xmax - xmin) * (rng[1] - rng[0]) + rng[0]
    else:  # normalize
        mu, sigma = X_num.mean(axis=0), X_num.std(axis=0)
        X_num_scaled = (X_num - mu) / sigma
    X_cat = X[:, :-4].copy()
    ohe = OneHotEncoder(categories='auto')
    ohe.fit(X_cat)
    X = np.c_[X_cat, X_num_scaled].astype(np.float32, copy=False)
    n_train = 25000
    n_valid = 5000
    X_train, y_train = X[:n_train,:], y[:n_train]
    X_valid, y_valid = X[n_train:n_train+n_valid,:], y[n_train:n_train+n_valid]
    X_test, y_test = X[n_train+n_valid:,:], y[n_train+n_valid:]
    print(X_train.shape, y_train.shape,
          X_valid.shape, y_valid.shape,
          X_test.shape, y_test.shape)
    cat_cols = list(category_map.keys())
    num_cols = [col for col in range(X.shape[1]) if col not in cat_cols]
    print(cat_cols, num_cols)
    perc_outlier = 10
    data = inject_outlier_tabular(X_valid, num_cols, perc_outlier, n_std=8., min_std=6.)
    X_threshold, y_threshold = data.data, data.target
    X_threshold_, y_threshold_ = X_threshold.copy(), y_threshold.copy()  # store for comparison later
    outlier_perc = 100 * y_threshold.sum() / len(y_threshold)
    print('{:.2f}% outliers'.format(outlier_perc))
    outlier_idx = np.where(y_threshold != 0)[0]
    vdiff = X_threshold[outlier_idx[0]] - X_valid[outlier_idx[0]]
    fdiff = np.where(vdiff != 0)[0]
    print('{} changed by {:.2f}.'.format(feature_names[fdiff[0]], vdiff[fdiff[0]]))
    data = inject_outlier_tabular(X_test, num_cols, perc_outlier, n_std=8., min_std=6.)
    X_outlier, y_outlier = data.data, data.target
    print('{:.2f}% outliers'.format(100 * y_outlier.sum() / len(y_outlier)))
    X_train_ohe = ohe.transform(X_train[:, :-4].copy())
    X_threshold_ohe = ohe.transform(X_threshold[:, :-4].copy())
    X_outlier_ohe = ohe.transform(X_outlier[:, :-4].copy())
    print(X_train_ohe.shape, X_threshold_ohe.shape, X_outlier_ohe.shape)
    X_train = np.c_[X_train_ohe.toarray(), X_train[:, -4:]].astype(np.float32, copy=False)
    X_threshold = np.c_[X_threshold_ohe.toarray(), X_threshold[:, -4:]].astype(np.float32, copy=False)
    X_outlier = np.c_[X_outlier_ohe.toarray(), X_outlier[:, -4:]].astype(np.float32, copy=False)
    print(X_train.shape, X_threshold.shape, X_outlier.shape)
    load_outlier_detector = True
    filepath = './models/'  # change to directory where model is downloaded
    if load_outlier_detector:  # load pretrained outlier detector
        detector_type = 'outlier'
        dataset = 'adult'
        detector_name = 'OutlierVAE'
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        n_features = X_train.shape[1]
        latent_dim = 2
    
        encoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(n_features,)),
              Dense(25, activation=tf.nn.relu),
              Dense(10, activation=tf.nn.relu),
              Dense(5, activation=tf.nn.relu)
          ])
    
        decoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(latent_dim,)),
              Dense(5, activation=tf.nn.relu),
              Dense(10, activation=tf.nn.relu),
              Dense(25, activation=tf.nn.relu),
              Dense(n_features, activation=None)
          ])
    
        # initialize outlier detector
        od = OutlierVAE(threshold=None,  # threshold for outlier score
                        score_type='mse',  # use MSE of reconstruction error for outlier detection
                        encoder_net=encoder_net,  # can also pass VAE model instead
                        decoder_net=decoder_net,  # of separate encoder and decoder
                        latent_dim=latent_dim,
                        samples=5)
    
        # train
        od.fit(X_train,
               loss_fn=tf.keras.losses.mse,
               epochs=5,
               verbose=True)
    
        # save the trained outlier detector
        save_detector(od, filepath)
    od.infer_threshold(X_threshold, threshold_perc=100-outlier_perc, outlier_perc=100)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    od_preds = od.predict(X_outlier,
                          outlier_type='instance',
                          return_feature_score=True,
                          return_instance_score=True)
    labels = data.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    acc = accuracy_score(y_outlier, y_pred)
    prec = precision_score(y_outlier, y_pred)
    rec = recall_score(y_outlier, y_pred)
    print('F1 score: {:.2f} -- Accuracy: {:.2f} -- Precision: {:.2f} -- Recall: {:.2f}'.format(f1, acc, prec, rec))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_instance_score(od_preds, y_outlier.astype(int), labels, od.threshold, ylim=(0, 25))
    !pip install seaborn
    import os
    import logging
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix, f1_score
    import tensorflow as tf
    tf.keras.backend.clear_session()
    from tensorflow.keras.layers import Dense, InputLayer
    
    from alibi_detect.datasets import fetch_kdd
    from alibi_detect.models.tensorflow import elbo
    from alibi_detect.od import OutlierVAE
    from alibi_detect.utils.data import create_outlier_batch
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_tabular, plot_roc
    
    logger = tf.get_logger()
    logger.setLevel(logging.ERROR)
    kddcup = fetch_kdd(percent10=True)  # only load 10% of the dataset
    print(kddcup.data.shape, kddcup.target.shape)
    np.random.seed(0)
    normal_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=400000, perc_outlier=0)
    X_train, y_train = normal_batch.data.astype('float'), normal_batch.target
    print(X_train.shape, y_train.shape)
    print('{}% outliers'.format(100 * y_train.mean()))
    mean, stdev = X_train.mean(axis=0), X_train.std(axis=0)
    X_train = (X_train - mean) / stdev
    load_outlier_detector = True
    #| scrolled: true
    filepath = 'my_dir'  # change to directory (absolute path) where model is downloaded
    detector_type = 'outlier'
    dataset = 'kddcup'
    detector_name = 'OutlierVAE'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        n_features = X_train.shape[1]
        latent_dim = 2
        
        encoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(n_features,)),
              Dense(20, activation=tf.nn.relu),
              Dense(15, activation=tf.nn.relu),
              Dense(7, activation=tf.nn.relu)
          ])
    
        decoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(latent_dim,)),
              Dense(7, activation=tf.nn.relu),
              Dense(15, activation=tf.nn.relu),
              Dense(20, activation=tf.nn.relu),
              Dense(n_features, activation=None)
          ])
        
        # initialize outlier detector
        od = OutlierVAE(threshold=None,  # threshold for outlier score
                        score_type='mse',  # use MSE of reconstruction error for outlier detection
                        encoder_net=encoder_net,  # can also pass VAE model instead
                        decoder_net=decoder_net,  # of separate encoder and decoder
                        latent_dim=latent_dim,
                        samples=5)
        # train
        od.fit(X_train,
               loss_fn=elbo,
               cov_elbo=dict(sim=.01),
               epochs=30,
               verbose=True)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    np.random.seed(0)
    perc_outlier = 5
    threshold_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=perc_outlier)
    X_threshold, y_threshold = threshold_batch.data.astype('float'), threshold_batch.target
    X_threshold = (X_threshold - mean) / stdev
    print('{}% outliers'.format(100 * y_threshold.mean()))
    od.infer_threshold(X_threshold, threshold_perc=100-perc_outlier)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    np.random.seed(1)
    outlier_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=10)
    X_outlier, y_outlier = outlier_batch.data.astype('float'), outlier_batch.target
    X_outlier = (X_outlier - mean) / stdev
    print(X_outlier.shape, y_outlier.shape)
    print('{}% outliers'.format(100 * y_outlier.mean()))
    od_preds = od.predict(X_outlier,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    print(list(od_preds['data'].keys()))
    labels = outlier_batch.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {:.4f}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_instance_score(od_preds, y_outlier, labels, od.threshold)
    roc_data = {'VAE': {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}}
    plot_roc(roc_data)
    X_recon = od.vae(X_outlier).numpy()  # reconstructed instances by the VAE
    #| scrolled: false
    plot_feature_outlier_tabular(od_preds,
                                 X_outlier,
                                 X_recon=X_recon,
                                 threshold=od.threshold,
                                 instance_ids=None,  # pass a list with indices of instances to display
                                 max_instances=5,  # max nb of instances to display
                                 top_n=5,  # only show top_n features ordered by outlier score
                                 outliers_only=False,  # only show outlier predictions
                                 feature_names=kddcup.feature_names,  # add feature names
                                 figsize=(20, 30))
    import matplotlib.pyplot as plt
    import numpy as np
    import torch
    import tensorflow as tf
    import pandas as pd
    import scipy
    from sklearn.decomposition import PCA
    
    np.random.seed(0)
    torch.manual_seed(0)
    tf.random.set_seed(0)
    red = pd.read_csv(
        "https://storage.googleapis.com/seldon-datasets/wine_quality/winequality-red.csv", sep=';'
    )
    white = pd.read_csv(
        "https://storage.googleapis.com/seldon-datasets/wine_quality/winequality-white.csv", sep=';'
    )
    white.describe()
    red.describe()
    white, red = np.asarray(white, np.float32), np.asarray(red, np.float32)
    n_white, n_red = white.shape[0], red.shape[0]
    
    col_maxes = white.max(axis=0)
    white, red = white / col_maxes, red / col_maxes
    white, red = white[np.random.permutation(n_white)], red[np.random.permutation(n_red)]
    X = white[:, :-1]
    X_corr = red[:, :-1]
    X_train = X[:(n_white//2)]
    X_ref = X[(n_white//2):(3*n_white//4)]
    X_h0 = X[(3*n_white//4):]
    pca = PCA(2)
    pca.fit(X_train)
    enc_h0 = pca.transform(X_h0)
    enc_h1 = pca.transform(X_corr)
    
    plt.scatter(enc_h0[:,0], enc_h0[:,1], alpha=0.2, color='green', label='white wine')
    plt.scatter(enc_h1[:,0], enc_h1[:,1], alpha=0.2, color='red', label='red wine')
    plt.legend(loc='upper right')
    plt.show()
    from alibi_detect.cd import MMDDriftOnline
    
    ert = 50
    window_size = 10
    
    cd = MMDDriftOnline(
        X_ref, ert, window_size, backend='pytorch', preprocess_fn=pca.transform, n_bootstraps=2500
    )
    def time_run(cd, X, window_size):
        n = X.shape[0]
        perm = np.random.permutation(n)
        t = 0
        cd.reset_state()
        while True:
            pred = cd.predict(X[perm[t%n]])
            if pred['data']['is_drift'] == 1:
                return t
            else:
                t += 1
    n_runs = 250
    times_h0 = [time_run(cd, X_h0, window_size) for _ in range(n_runs)]
    print(f"Average run-time under no-drift: {np.mean(times_h0)}")
    _ = scipy.stats.probplot(np.array(times_h0), dist=scipy.stats.geom, sparams=1/ert, plot=plt)
    n_runs = 250
    times_h1 = [time_run(cd, X_corr, window_size) for _ in range(n_runs)]
    print(f"Average run-time under drift: {np.mean(times_h1)}")
    X_ref = np.concatenate([X_train, X_ref], axis=0)
    from alibi_detect.cd import LSDDDriftOnline
    
    cd = LSDDDriftOnline(
        X_ref, ert, window_size, backend='tensorflow', n_bootstraps=2500,
    )
    n_runs = 250
    times_h0 = [time_run(cd, X_h0, window_size) for _ in range(n_runs)]
    print(f"Average run-time under no-drift: {np.mean(times_h0)}")
    _ = scipy.stats.probplot(np.array(times_h0), dist=scipy.stats.geom, sparams=1/ert, plot=plt)
    n_runs = 250
    times_h1 = [time_run(cd, X_corr, window_size) for _ in range(n_runs)]
    print(f"Average run-time under drift: {np.mean(times_h1)}")
    from alibi_detect.cd import LSDDDriftOnline
    
    cd = LSDDDriftOnline(x_ref, ert, window_size, backend='tensorflow')
    cd = LSDDDriftOnline(x_ref, ert, window_size, backend='pytorch')
    from functools import partial
    import torch
    import torch.nn as nn
    from alibi_detect.cd.pytorch import preprocess_drift
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # define encoder
    encoder_net = nn.Sequential(
        nn.Conv2d(3, 64, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(64, 128, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(128, 512, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(2048, 32)
    ).to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, device=device, batch_size=512)
    
    cd = LSDDDriftOnline(x_ref, ert, window_size, backend='pytorch', preprocess_fn=preprocess_fn)
    from alibi_detect.cd.tensorflow import HiddenOutput, preprocess_drift
    
    model = # TensorFlow model; tf.keras.Model or tf.keras.Sequential
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(model, layer=-1), batch_size=128)
    
    cd = LSDDDriftOnline(x_ref, ert, window_size, backend='tensorflow', preprocess_fn=preprocess_fn)
    import torch
    import torch.nn as nn
    from transformers import AutoTokenizer
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    model_name = 'bert-base-cased'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    embedding_type = 'hidden_state'
    layers = [5, 6, 7]
    embed = TransformerEmbedding(model_name, embedding_type, layers)
    model = nn.Sequential(embed, nn.Linear(768, 256), nn.ReLU(), nn.Linear(256, enc_dim)).to(device).eval()
    preprocess_fn = partial(preprocess_drift, model=model, tokenizer=tokenizer, max_len=512, batch_size=32)
    
    # initialise drift detector
    cd = LSDDDriftOnline(x_ref, ert, window_size, backend='pytorch', preprocess_fn=preprocess_fn)
    preds = cd.predict(x_t, return_test_stat=True)
    cd = LSDDDriftOnline(x_ref, ert, window_size)  # Instantiate detector at t=0
    cd.predict(x_1)  # t=1
    cd.save_state('checkpoint_t1')  # Save state at t=1
    cd.predict(x_2)  # t=2
    # Load state at t=1
    cd.load_state('checkpoint_t1')
    Data

    We will use the Camelyon17 dataset, one of the WILDS datasets of Koh et al, (2020) that represent "in-the-wild" distribution shifts for various data modalities. It contains tissue scans to be classificatied as benign or cancerous. The pre-change distribution corresponds to scans from across three hospitals and the post-change distribution corresponds to scans from a new fourth hospital.

    Koh et al, (2020) show that models trained on scans from the pre-change distribution achieve an accuracy of 93.2% on unseen scans from same distribution, but only 70.3% accuracy on scans from the post-change distribution.

    First we create a function that converts the Camelyon dataset to a stream in order to simulate a live deployment environment. We extract N instances to act as the reference set on which a model of interest was trained. We then consider a stream of images from the pre-change (same) distribution and a stream of images from the post-change (drifted) distribution.

    The following cell will download the Camelyon dataset (if DOWNLOAD=True). The download size is ~10GB and size on disk is ~15GB.

    Shown below are samples from the pre-change distribution:

    And samples from the post-change distribution:

    Kernel Projection

    The images are of dimension 96x96x3. We train an autoencoder in order to define a more structured representational space of lower dimension. This projection can be thought of as an extension of the kernel. It is important that trained preprocessing components are trained on a split of data that doesn't then form part of the reference data passed to the drift detector.

    We can train the autoencoder using a helper function provided for convenience in alibi-detect.

    The preprocessing/projection functions are expected to map numpy arrays to numpy array, so we wrap the encoder within the function below.

    Drift Detection

    alibi-detect's online drift detectors window the stream of data in an 'overlapping window' manner such that a test is performed at every time step. We will use an estimator of MMD as the test statistic. The estimate is updated incrementally at low cost. The thresholds are configured via simulation in an initial configuration phase to target the desired expected runtime (ERT) in the absence of change. For a detailed description of this calibration procedure see Cobb et al, 2021.

    We define a function which will apply the detector to the streams and return the time at which drift was detected.

    First we apply the detector multiple times to the pre-change stream where the distribution is unchanged.

    We see that the average runtime in the absence of change is close to the desired ERT, as expected. We can inspect the detector's test_stats and thresholds properties to see how the test statistic varied over time and how close it got to exceeding the threshold.

    Now we apply it to the post-change stream where the images are from a drifted distribution.

    We see that the detector is quick to flag drift when it has occured.

    Online Drift Detection on the Wine Quality Dataset
    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_tensorflow

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    LSDDDrift

    Inherits from: DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    backend

    str

    'tensorflow'

    Backend used for the LSDD implementation.

    p_val

    float

    0.05

    Methods

    get_config

    Returns

    • Type: dict

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    score

    Compute the p-value resulting from a permutation test using the least-squares density

    difference as a distance measure between the reference data and the data to be tested.

    Name
    Type
    Default
    Description

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    Returns

    • Type: Tuple[float, float, float]

    Saving and Loading

    Alibi Detect includes support for saving and loading detectors to disk. To save a detector, simply call the save_detector method and provide a path to a directory (a new one will be created if it doesn't exist):

    To load a previously saved detector, use the load_detector method and provide it with the path to the detector's directory:

    Note: When loading a saved detector, a warning will be issued if the runtime alibi-detect version is different from the version used to save the detector. It is highly recommended to use the same alibi-detect, Python and dependency versions as were used to save the detector to avoid potential bugs and incompatibilities.

    Formats

    Detectors can be saved using two formats:

    • Config format: For drift detectors, by default save_detector serializes the detector via a config file named config.toml, stored in filepath. The format is human-readable, which makes the config files useful for record keeping, and allows a detector to be edited before it is reloaded. For more details, see .

    • Legacy format: Outlier and adversarial detectors are saved to files stored within filepath. Drift detectors can also be saved in this legacy format by running save_detector with legacy=True

    Note: If you save a detector with legacy=True, or load one that was saved with legacy=True, and you are using TensorFlow>2.15, then you must set the environment variable TF_USE_LEGACY_KERAS=1. This is in order to tell TensorFlow to use the legacy Keras 2 implementation to save and load TensorFlow models. See TensorFlow + Keras 2 backwards compatibility section of the for more details.

    Supported detectors

    The following tables list the current state of save/load support for each detector. Adding full support for the remaining detectors is in the .

    Detector
    Legacy save/load
    Config save/load

    Supported ML models

    Alibi Detect drift detectors offer the option to perform with user-defined machine learning models:

    Additionally, some detectors are built upon models directly, for example the drift detector requires a model to be passed as an argument:

    In order for a detector to be saveable and loadable, any models contained within it (or referenced within a ) must fall within the family of supported models:

    Alibi Detect supports serialization of any TensorFlow model that can be serialized to the format. Custom objects should be pre-registered with .

    PyTorch models are serialized by saving the using the module. Therefore, Alibi Detect should support any PyTorch model that can be saved and loaded with torch.save(..., pickle_module=dill) and torch.load(..., pickle_module=dill).

    Scikit-learn models are serialized using . Any scikit-learn model that is a subclass of is supported, including models following the scikit-learn API.

    Online detectors

    are stateful, with their state updated each timestep t (each time .predict() is called). {func}~alibi_detect.saving.save_detector will save the state of online detectors to disk if t > 0. At load time, {func}~alibi_detect.saving.load_detector will load this state. For example:

    To save a clean (stateless) detector, it should be reset before saving:

    AE outlier detection on CIFAR10

    Method

    The Auto-Encoder (AE) outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The AE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is measured as the mean squared error (MSE) between the input and the reconstructed instance.

    Dataset

    consists of 60,000 32 by 32 RGB images equally distributed over 10 classes.

    Load CIFAR10 data

    Load or define outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found . You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    Check quality AE model

    Check outliers on original CIFAR images

    Plot instance level outlier scores

    Visualize predictions

    Predict outliers on perturbed CIFAR images

    We perturb CIFAR images by adding random noise to patches (masks) of the image. For each mask size in n_mask_sizes, sample n_masks and apply those to each of the n_imgs images. Then we predict outliers on the masked instances:

    Define masks and get images:

    Calculate instance level outlier scores:

    Visualize outlier scores vs. mask sizes

    Investigate instance level outlier

    Reconstruction of masked images and outlier scores per channel:

    Visualize:

    Predict outliers on a subset of features

    The sensitivity of the outlier detector can not only be controlled via the threshold, but also by selecting the percentage of the features used for the instance level outlier score computation. For instance, we might want to flag outliers if 40% of the features (pixels for images) have an average outlier score above the threshold. This is possible via the outlier_perc argument in the predict function. It specifies the percentage of the features that are used for outlier detection, sorted in descending outlier score order.

    Visualize outlier scores vs. mask sizes and percentage of features used:

    Infer outlier threshold value

    Finding good threshold values can be tricky since they are typically not easy to interpret. The infer_threshold method helps finding a sensible value. We need to pass a batch of instances X and specify what percentage of those we consider to be normal via threshold_perc.

    Learned Kernel

    source

    Learned Kernel

    Overview

    The learned-kernel drift detector (Liu et al., 2020) is an extension of the Maximum Mean Discrepancy drift detector where the kernel used to define the MMD is trained using a portion of the data to maximise an estimate of the resulting test power. Once the kernel has been learned a permutation test is performed in the usual way on the value of the MMD.

    This method is closely related to the which trains a classifier to discriminate between instances from the reference window and instances from the test window. The difference here is that we train a kernel to output high similarity on instances from the same window and low similarity between instances from different windows. If this is possible in a generalisable manner then drift must have occured.

    As with the classifier-based approach, we should specify the proportion of data to use for training and testing respectively as well as training arguments such as the learning rate and batch size. Note that a new kernel is trained for each test set that is passed for detection.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    • kernel: A differentiable TensorFlow or PyTorch module that takes two sets of instances as inputs and returns a kernel similarity matrix as output.

    Keyword arguments:

    • backend: TensorFlow, PyTorch and implementations of the learned kernel detector are available. The backend can be specified as tensorflow, pytorch or keops. Defaults to tensorflow.

    • p_val: p-value threshold used for the significance of the test.

    • preprocess_at_init

    Additional PyTorch and KeOps keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    • dataloader: Dataloader object used during training of the kernel. Defaults to torch.utils.data.DataLoader. The dataloader is not initialized yet, this is done during init off the detector using the batch_size. Custom dataloaders can be passed as well, e.g. for graph data we can use torch_geometric.data.DataLoader.

    Additional KeOps only keyword arguments:

    • batch_size_permutations: KeOps computes the n_permutations of the MMD^2 statistics in chunks of batch_size_permutations. Defaults to 1,000,000.

    Defining the kernel

    Any differentiable Pytorch or TensorFlow module that takes as input two instances and outputs a scalar (representing similarity) can be used as the kernel for this drift detector. However, in order to ensure that MMD=0 implies no-drift the kernel should satify a characteristic property. This can be guaranteed by defining a kernel as where $\Phi$ is a learnable projection, $k_a$ and $k_b$ are simple characteristic kernels (such as a ), and $\epsilon>0$ is a small constant. By letting $\Phi$ be very flexible we can learn powerful kernels in this manner.

    This is easily implemented using the DeepKernel class provided in alibi_detect. We demonstrate below how we might define a convolutional kernel for images using Pytorch. By default GaussianRBF kernels are used for $k_a$ and $k_b$ and here we specify $\epsilon=0.01$, but we could alternatively set eps='trainable'.

    It is important to note that, if retrain_from_scratch=True and we have not initialised the kernel bandwidth sigma for the default GaussianRBF kernel $k_a$ and optionally also for $k_b$, we will initialise sigma using a median (PyTorch and TensorFlow) or mean (KeOps) bandwidth heuristic for every detector prediction. For KeOps detectors specifically, this could form a computational bottleneck and should be avoided by already specifying a bandwidth in advance. To do this, we can leverage the library's built-in heuristics:

    Instantiating the detector

    Instantiating the detector is then as simple as passing the reference data and the kernel as follows:

    We could have alternatively defined the kernel and instantiated the detector using KeOps:

    Or by using TensorFlow as the backend:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. return_p_val equal to True will also return the p-value of the test, return_distance equal to True will return a notion of strength of the drift and return_kernel equals True will also return the trained kernel.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • threshold: the user-defined p-value threshold defining the significance of the test

    • p_val: the p-value of the test if return_p_val equals True.

    Examples

    Graph

    Image

    Tabular

    Learned drift detectors on Adult Census

    Under the hood, drift detectors leverage a function (also known as a test-statistic) that is expected to take a large value if drift has occurred and a low value if not. The power of the detector is partly determined by how well the function satisfies this property. However, specifying such a function in advance can be very difficult.

    Detecting drift with a learned classifier

    The classifier-based drift detector simply tries to correctly distinguish instances from the reference data vs. the test set. The classifier is trained to output the probability that a given instance belongs to the test set. If the probabilities it assigns to unseen tests instances are significantly higher (as determined by a Kolmogorov-Smirnov test) than those it assigns to unseen reference instances then the test set must differ from the reference set and drift is flagged. To leverage all the available reference and test data, stratified cross-validation can be applied and the out-of-fold predictions are used for the significance test. Note that a new classifier is trained for each test set or even each fold within the test set.

    Backend

    The method works with both the PyTorch, TensorFlow, and Sklearn frameworks. We will focus exclusively on the Sklearn backend in this notebook.

    Dataset

    Adult dataset consists of 32,561 distributed over 2 classes based on whether the annual income is >50K. We evaluate drift on particular subsets of the data which are constructed based on the education level. As we will further discuss, our reference dataset will consist of people having a low education level, while our test dataset will consist of people having a high education level.

    Note: we need to install alibi to fetch the adult dataset.

    Load Adult Census Dataset

    We split the dataset in two based on the education level. We define a low_education level consisting of: 'Dropout', 'High School grad', 'Bachelors', and a high_education level consisting of: 'Bachelors', 'Masters', 'Doctorate'. Intentionally we included an overlap between the two distributions consisting of people that have a Bachelors degree. Our goal is to detect that the two distributions are different.

    We sample our reference dataset from the low_education level. In addition, we sample two other datasets:

    • x_h0 - sampled from the low_education level to support the null hypothesis (i.e., the two distributions are identical);

    • x_h1 - sampled from the high_education level to support the alternative hypothesis (i.e., the two distributions are different);

    Define dataset pre-processor

    Utils

    Drift detection

    We perform a binomial test using a RandomForestClassifier.

    As expected, when testing against x_h0, we fail to reject $H_0$, while for the second case there is enough evidence to reject $H_0$ and flag that the data has drifted.

    For the classifiers that do not support predict_proba but offer support for decision_function, we can perform a K-S test on the scores by setting preds_type='scores'.

    Some models can return a poor estimate of the class label probability or some might not even support probability predictions. We can add calibration on top of each classifier to obtain better probability estimates and perform a K-S test. For demonstrative purposes, we will calibrate a LinearSVC which does not support predict_proba, but any other classifier would work.

    Speeding things up

    In order to use the entire dataset and obtain unbiased predictions required to perform the statistical test, the ClassifierDrift detector has the option to perform a n_folds split. Although appealing due to its data efficiency, this method can be slow since it is required to train a number of n_folds classifiers.

    For the RandomForestClassifier we can avoid retraining n_folds classifiers by using the out-of-bag predictions. In a RandomForestClassifier each tree is trained on a separate dataset obtained by sampling with replacement the original training set, a method known as bagging. On average, only 63% unique samples from the original dataset are used to train each tree (). Thus, for each tree, we can obtain predictions for the remaining out-of-bag samples (i.e., the rest of 37%). By cumulating the out-of-bag predictions across all the trees we can eventually obtain a prediction for each sample in the original dataset. Note that we used the word 'eventually' because if the number of trees is too small, covering the entire original dataset might be unlikely.

    For demonstrative purposes, we will compare the running time of the ClassifierDrift detector when using a RandomForestClassifier in two setups: n_folds=5, use_oob=False and use_oob=True.

    We can observe that in this particular setting, using the out-of-bag prediction can speed up the procedure up to almost x4.

    Model uncertainty based drift detection on CIFAR-10 and Wine-Quality datasets

    Method

    Model-uncertainty drift detectors aim to directly detect drift that's likely to effect the performance of a model of interest. The approach is to test for change in the number of instances falling into regions of the input space on which the model is uncertain in its predictions. For each instance in the reference set the detector obtains the model's prediction and some associated notion of uncertainty. For example for a classifier this may be the entropy of the predicted label probabilities or for a regressor with dropout layers can be used to provide a notion of uncertainty. The same is done for the test set and if significant differences in uncertainty are detected (via a Kolmogorov-Smirnoff test) then drift is flagged.

    It is important that the detector uses a reference set that is disjoint from the model's training set (on which the model's confidence may be higher).

    Spot-the-diff

    Spot-the-diff

    Overview

    The spot-the-diff drift detector is an extension of the drift detector where the classifier is specified in a manner that makes detections interpretable at the feature level when they occur. The detector is inspired by the work of

    Drift detection on Amazon reviews

    Methods

    We illustrate drift detection on text data using the following detectors:

    • using to flag drift in the embedding space.

    VAE outlier detection on CIFAR10

    Method

    The Variational Auto-Encoder () outlier detector is first trained on a batch of unlabeled, but normal (inlier) data. Unsupervised training is desireable since labeled data is often scarce. The VAE detector tries to reconstruct the input it receives. If the input data cannot be reconstructed well, the reconstruction error is high and the data can be flagged as an outlier. The reconstruction error is either measured as the mean squared error (MSE) between the input and the reconstructed instance or as the probability that both the input and the reconstructed instance are generated by the same process.

    alibi_detect.cd.context_aware

    Constants

    has_pytorch

    bool(x) -> bool

    !pip install wilds torch torchvision
    from typing import Tuple, Generator, Callable, Optional
    import numpy as np
    import matplotlib.pyplot as plt
    import torch
    import torch.nn as nn
    from torch.utils.data import TensorDataset, DataLoader
    import torchvision.transforms as transforms
    from wilds.common.data_loaders import get_train_loader
    from wilds import get_dataset
    
    torch.manual_seed(0)
    np.random.seed(0)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    %matplotlib inline
    WILDS_PATH = './data/wilds'
    DOWNLOAD = False  # set to True for first run
    N = 2000  # size of reference set
    def stream_camelyon(
        split: str='train', 
        img_size: Tuple[int]=(96,96), 
        root_dir: str=None, 
        download: bool=False
    ) -> Generator:
    
        camelyon = get_dataset('camelyon17', root_dir=root_dir, download=download)
        ds = camelyon.get_subset(
            split, 
            transform=transforms.Compose([transforms.Resize(img_size), transforms.ToTensor()])
        )
        ds_iter = iter(get_train_loader('standard', ds, batch_size=1))
    
        while True:
            try:
                img = next(ds_iter)[0][0]
            except Exception:
                ds_iter = iter(get_train_loader('standard', ds, batch_size=1))
                img = next(ds_iter)[0][0]
            yield img.numpy()
    
    stream_p = stream_camelyon(split='train', root_dir=WILDS_PATH, download=DOWNLOAD)
    x_ref = np.stack([next(stream_p) for _ in range(N)], axis=0)
    
    stream_q_h0 = stream_camelyon(split='id_val', root_dir=WILDS_PATH, download=DOWNLOAD)
    stream_q_h1 = stream_camelyon(split='test', root_dir=WILDS_PATH, download=DOWNLOAD)
    #| scrolled: true
    fig, axs = plt.subplots(nrows=1, ncols=6, figsize=(15,4))
    for i in range(6):
        axs[i].imshow(np.transpose(next(stream_p), (1,2,0)))
        axs[i].axis('off')
    fig, axs = plt.subplots(nrows=1, ncols=6, figsize=(15,4))
    for i in range(6):
        axs[i].imshow(np.transpose(next(stream_q_h1), (1,2,0)))
        axs[i].axis('off')
    ENC_DIM = 32
    BATCH_SIZE = 32
    EPOCHS = 5
    LEARNING_RATE = 1e-3
    encoder = nn.Sequential(
        nn.Conv2d(3, 8, 5, stride=3, padding=1),    # [batch, 8, 32, 32]
        nn.ReLU(),
        nn.Conv2d(8, 12, 4, stride=2, padding=1),   # [batch, 12, 16, 16]
        nn.ReLU(),
        nn.Conv2d(12, 16, 4, stride=2, padding=1),   # [batch, 16, 8, 8]
        nn.ReLU(),
        nn.Conv2d(16, 20, 4, stride=2, padding=1),   # [batch, 20, 4, 4]
        nn.ReLU(),
        nn.Conv2d(20, ENC_DIM, 4, stride=1, padding=0),   # [batch, enc_dim, 1, 1]
        nn.Flatten(), 
    )
    decoder = nn.Sequential(
        nn.Unflatten(1, (ENC_DIM, 1, 1)),
        nn.ConvTranspose2d(ENC_DIM, 20, 4, stride=1, padding=0),  # [batch, 20, 4, 4]
        nn.ReLU(),
        nn.ConvTranspose2d(20, 16, 4, stride=2, padding=1),  # [batch, 16, 8, 8]
        nn.ReLU(),
        nn.ConvTranspose2d(16, 12, 4, stride=2, padding=1),  # [batch, 12, 16, 16]
        nn.ReLU(),
        nn.ConvTranspose2d(12, 8, 4, stride=2, padding=1),  # [batch, 8, 32, 32]
        nn.ReLU(),
        nn.ConvTranspose2d(8, 3, 5, stride=3, padding=1),   # [batch, 3, 96, 96]
        nn.Sigmoid(),
    )
    ae = nn.Sequential(encoder, decoder).to(device)
    
    x_fit, x_ref = np.split(x_ref, [len(x_ref)//2])
    x_fit = torch.as_tensor(x_fit)
    x_fit_dl = DataLoader(TensorDataset(x_fit, x_fit), BATCH_SIZE, shuffle=True)
    from alibi_detect.models.pytorch import trainer
    
    trainer(ae, nn.MSELoss(), x_fit_dl, device, learning_rate=LEARNING_RATE, epochs=EPOCHS)
    def encoder_fn(x: np.ndarray) -> np.ndarray:
        x = torch.as_tensor(x).to(device)
        with torch.no_grad():
            x_proj = encoder(x)
        return x_proj.cpu().numpy()
    ERT = 150  # expected run-time in absence of change
    W = 20  # size of test window
    B = 50_000  # number of simulations to configure threshold
    from alibi_detect.cd import MMDDriftOnline
    
    dd = MMDDriftOnline(x_ref, ERT, W, backend='pytorch', preprocess_fn=encoder_fn)
    def compute_runtime(detector: Callable, stream: Generator) -> int:
    
        t = 0
        detector.reset_state()
        detected = False
    
        while not detected:
            t += 1
            z = next(stream)
            pred = detector.predict(z)
            detected = pred['data']['is_drift']
        print(t)
        return t
    #| scrolled: true
    times_h0 = [compute_runtime(dd, stream_p) for i in range(15)]
    print(f"Average runtime in absence of change: {np.array(times_h0).mean()}")
    ts = np.arange(dd.t)
    plt.plot(ts, dd.test_stats, label='Test statistic')
    plt.plot(ts, dd.thresholds, label='Thresholds')
    plt.xlabel('t', fontsize=16)
    plt.ylabel('$T_t$', fontsize=16)
    plt.legend(loc='upper right', fontsize=14)
    plt.show()
    times_h1 = [compute_runtime(dd, stream_q_h1) for i in range(15)]
    print(f"Average detection delay following change: {np.array(times_h1).mean()}")
    ts = np.arange(dd.t)
    plt.plot(ts, dd.test_stats, label='Test statistic')
    plt.plot(ts, dd.thresholds, label='Thresholds')
    plt.xlabel('t', fontsize=16)
    plt.ylabel('$T_t$', fontsize=16)
    plt.legend(loc='upper right', fontsize=14)
    plt.show()
    has_pytorch: bool = True
    has_tensorflow: bool = True
    LSDDDrift(self, x_ref: Union[numpy.ndarray, list], backend: str = 'tensorflow', p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, sigma: Optional[numpy.ndarray] = None, n_permutations: int = 100, n_kernel_centers: Optional[int] = None, lambda_rd_max: float = 0.2, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    get_config() -> dict
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    from alibi_detect.od import OutlierVAE
    from alibi_detect.saving import save_detector
    
    od = OutlierVAE(...) 
    
    filepath = './my_detector/'
    save_detector(od, filepath)
    from alibi_detect.saving import load_detector
    
    filepath = './my_detector/'
    od = load_detector(filepath)

    p-value used for the significance of the permutation test.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    sigma

    Optional[numpy.ndarray]

    None

    Optionally set the bandwidth of the Gaussian kernel used in estimating the LSDD. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths. If sigma is not specified, the 'median heuristic' is adopted whereby sigma is set as the median pairwise distance between reference samples.

    n_permutations

    int

    100

    Number of permutations used in the permutation test.

    n_kernel_centers

    Optional[int]

    None

    The number of reference samples to use as centers in the Gaussian kernel model used to estimate LSDD. Defaults to 1/20th of the reference data.

    lambda_rd_max

    float

    0.2

    The maximum relative difference between two estimates of LSDD that the regularization parameter lambda is allowed to cause. Defaults to 0.2 as in the paper.

    device

    Union[Literal[cuda, gpu, cpu], ForwardRef('torch.device'), None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device. Only relevant for 'pytorch' backend.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Whether to return the LSDD metric between the new batch and reference data.

    CIFAR10
    here
    : Whether to already apply the (optional) preprocessing step to the reference data at initialization and store the preprocessed data. Dependent on the preprocessing step, this can reduce the computation time for the predict step significantly, especially when the reference dataset is large. Defaults to
    True
    . It is possible that it needs to be set to
    False
    if the preprocessing step requires statistics from both the reference and test data, such as the mean or standard deviation.
  • x_ref_preprocessed: Whether or not the reference data x_ref has already been preprocessed. If True, the reference data will be skipped and preprocessing will only be applied to the test data passed to predict.

  • update_x_ref: Reference data can optionally be updated to the last N instances seen by the detector or via reservoir sampling with size N. For the former, the parameter equals {'last': N} while for reservoir sampling {'reservoir_sampling': N} is passed. If the input data type is of type List[Any] then update_x_ref needs to be set to None and the reference set remains fixed.

  • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

  • n_permutations: The number of permutations to use in the permutation test once the MMD has been computed.

  • var_reg: Constant added to the estimated variance of the MMD for stability.

  • reg_loss_fn: The regularisation term reg_loss_fn(kernel) is added to the loss function being optimized.

  • train_size: Optional fraction (float between 0 and 1) of the dataset used to train the classifier. The drift is detected on 1 - train_size.

  • retrain_from_scratch: Whether the kernel should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set. Defaults to True.

  • optimizer: Optimizer used during training of the kernel. From torch.optim for PyTorch and tf.keras.optimizers for TensorFlow.

  • learning_rate: Learning rate for the optimizer.

  • batch_size: Batch size used during training of the kernel.

  • batch_size_predict: Batch size used for the trained drift detector predictions.

  • preprocess_batch_fn: Optional batch preprocessing function. For example to convert a list of generic objects to a tensor which can be processed by the kernel.

  • epochs: Number of training epochs for the kernel.

  • verbose: Verbosity level during the training of the kernel. 0 is silent and 1 prints a progress bar.

  • train_kwargs: Optional additional kwargs for the built-in TensorFlow (from alibi_detect.models.tensorflow import trainer) or PyTorch (from alibi_detect.models.pytorch import trainer) trainer functions.

  • dataset: Dataset object used during training of the kernel. Defaults to alibi_detect.utils.pytorch.TorchDataset (an instance of torch.utils.data.Dataset) for the PyTorch and KeOps backends and alibi_detect.utils.tensorflow.TFDataset (an instance of tf.keras.utils.Sequence) for the TensorFlow backend. For PyTorch or KeOps, the dataset should only take the windows x_ref and x_test as input, so when e.g. TorchDataset is passed to the detector at initialisation, during training TorchDataset(x_ref, x_test) is used. For TensorFlow, the dataset is an instance of tf.keras.utils.Sequence, so when e.g. TFDataset is passed to the detector at initialisation, during training TFDataset(x_ref, x_test, batch_size=batch_size, shuffle=True) is used. x_ref and x_test can be of type np.ndarray or List[Any].

  • input_shape: Shape of input data.

  • data_type: Optionally specify the data type (e.g. tabular, image or time-series). Added to metadata.

  • num_workers: The number of workers used by the DataLoader. The default (num_workers=0) means multi-process data loading is disabled. Setting num_workers>0 may be unreliable on Windows.

    distance: MMD^2 metric between the reference data and the new batch if return_distance equals True.

  • distance_threshold: MMD^2 metric value from the permutation test which corresponds to the the p-value threshold if return_distance equals True.

  • kernel: The trained kernel if return_kernel equals True.

  • k(x,y)=(1−ϵ)∗ka(Φ(x),Φ(y))+ϵ∗kb(x,y),k(x,y)=(1-\epsilon)*k_a(\Phi(x), \Phi(y)) + \epsilon*k_b(x,y),k(x,y)=(1−ϵ)∗ka​(Φ(x),Φ(y))+ϵ∗kb​(x,y),
    classifier drift detector
    KeOps
    Gaussian RBF
    Drift detection on molecular graphs
    Drift detection on CIFAR10
    Scaling up drift detection with KeOps
    Bostrom
    Backend

    For models that require batch evaluation both PyTorch and TensorFlow frameworks are supported. Alibi Detect does however not install PyTorch for you. Check the PyTorch docs how to do this.

    Classifier uncertainty based drift detection

    We start by demonstrating how to leverage model uncertainty to detect malicious drift when the model of interest is a classifer.

    Dataset

    CIFAR10 consists of 60,000 32 by 32 RGB images equally distributed over 10 classes. We evaluate the drift detector on the CIFAR-10-C dataset (Hendrycks & Dietterich, 2019). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance. We also check for drift against the original test set with class imbalances.

    Original CIFAR-10 data:

    For CIFAR-10-C, we can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the original test set in a reference dataset and a dataset which should not be rejected under the no-change null H0. We also split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    We can also verify that the performance of a classification model on CIFAR-10 drops significantly on this perturbed dataset:

    Given the drop in performance, it is important that we detect the harmful data drift!

    Detect drift

    Unlike many other approaches we needn't specify a dimension-reducing preprocessing step as the detector operates directly on the data as it is input to the model of interest. In fact, the two-stage projection input -> prediction -> uncertainty can be thought of as the projection from the input space onto the real line, ready to perform the test.

    We simply pass the model to the detector and inform it that the predictions should be interpreted as 'probs' rather than 'logits' (i.e. a softmax has already been applied). By default uncertainty_type='entropy' is used as the notion of uncertainty for classifier predictions, however uncertainty_type='margin' can be specified to deem the classifier's prediction uncertain if they fall within a margin (e.g. in [0.45,0.55] for binary classifier probabilities) (similar to Sethi and Kantardzic (2017)).

    Let's check whether the detector thinks drift occurred on the different test sets and time the prediction calls:

    Note here how drift is only detected for the corrupted datasets on which the model's performance is significantly degraded. For the 'brightness' corruption, for which the model maintains 89% classification accuracy, the change in model uncertainty is not deemed significant (p-value 0.11, above the 0.05 threshold). For the other corruptions which signficiantly hamper model performance, the malicious drift is detected.

    Regressor uncertainty based drift detection

    We now demonstrate how to leverage model uncertainty to detect malicious drift when the model of interest is a regressor. This is a less general approach as regressors often make point-predictions with no associated notion of uncertainty. However, if the model makes its predictions by ensembling the predicitons of sub-models then we can consider the variation in the sub-model predictions as a notion of uncertainty. RegressorUncertaintyDetector facilitates models that output a vector of such sub-model predictions (uncertainty_type='ensemble') or deep learning models that include dropout layers and can therefore (as noted by Gal and Ghahramani 2016) be considered as an ensemble (uncertainty_type='mc_dropout', the default option).

    Dataset

    The Wine Quality Data Set consists of 1599 and 4898 samples of red and white wine respectively. Each sample has an associated quality (as determined by experts) and 11 numeric features indicating its acidity, density, pH etc. We consider the regression problem of tring to predict the quality of red wine sample given these features. We will then consider whether the model remains suitable for predicting the quality of white wine samples or whether the associated change in the underlying distribution should be considered as malicious drift.

    First we load in the data.

    We can see that the data for both red and white wine samples take the same format.

    We shuffle and normalise the data such that each feature takes a value in [0,1], as does the quality we seek to predict.

    We split the red wine data into a set on which to train the model, a reference set with which to instantiate the detector and a set which the detector should not flag drift. We then instantiate a DataLoader to pass the training data to a PyTorch model in batches.

    Regression model

    We now define the regression model that we'll train to predict the quality from the features. The exact details aren't important other than the presence of at least one dropout layer. We then train the model for 20 epochs to optimise the mean square error on the training data.

    We now evaluate the trained model on both unseen samples of red wine and white wine. We see that, unsurprisingly, the model is better able to predict the quality of unseen red wine samples.

    Detect drift

    We now look at whether a regressor-uncertainty detector would have picked up on this malicious drift. We instantiate the detector and obtain drift predictions on both the held-out red-wine samples and the white-wine samples. We specify uncertainty_type='mc_dropout' in this case, but alternatively we could have trained an ensemble model that for each instance outputs a vector of multiple independent predictions and specified uncertainty_type='ensemble'.

    dropout Monte Carlo
    but various major adaptations have been made.

    As with the usual classifier-based approach, a portion of the available data is used to train a classifier that can disciminate reference instances from test instances. If the classifier can learn to discriminate in a generalisable manner then drift must have occured. Here we additionally enforce that the classifier takes the form logit(p^T)=b0+b1k(x,w1)+...+bJk(x,wJ),\text{logit}(\hat{p}_T) = b_0 + b_1 k(x,w_1) + ... + b_Jk(x,w_J),logit(p^​T​)=b0​+b1​k(x,w1​)+...+bJ​k(x,wJ​), where $\hat{p}_T$ is the predicted probability that instance $x$ is from the test window (rather than reference), $k(\cdot,\cdot)$ is a kernel specifying a notion of similarity between instances, $w_i$ are learnable test locations and $b_i$ are learnable regression coefficients.

    If the detector flags drift and $b_i >0$ then we know that it reached its decision by considering how similar each instance is to the instance $w_i$, with those being more similar being more likely to be test instances than reference instances. Alternatively if $b_i < 0$ then instances more similar to $w_i$ were deemed more likely to be reference instances.

    In order to provide less noisy and therefore more interpretable results, we define each test location as wi=xˉ+diw_i = \bar{x}+d_iwi​=xˉ+di​ where $\bar{x}$ is the mean reference instance. We may then interpret $d_i$ as the additive transformation deemed to make the average reference more ($b_i>0$) or less ($b_i<0$) similar to a test instance. Defining the test locations in this way allows us to instead learn the difference $d_i$ and apply regularisation such that non-zero values must be justified by improved classification performance. This allows us to more clearly identify which features any detected drift should be attributed to.

    As with the standard classifier-based approach, we should specify the proportion of data to use for training and testing respectively as well as training arguments such as the learning rate and batch size. Note that classifier is trained for each test set that is passed for detection.

    Usage

    Initialize

    Arguments:

    • x_ref: Data used as reference distribution.

    Keyword arguments:

    • backend: Specify the backend (tensorflow or pytorch) to use for defining the kernel and training the test locations/differences.

    • p_val: p-value threshold used for the significance of the test.

    • preprocess_fn: Function to preprocess the data before computing the data drift metrics.

    • kernel: A differentiable TensorFlow or PyTorch module that takes two instances as input and returns a scalar notion of similarity os output. Defaults to a Gaussian radial basis function.

    • n_diffs: The number of test locations to use, each corresponding to an interpretable difference.

    • initial_diffs: Array used to initialise the diffs that will be learned. Defaults to Gaussian for each feature with equal variance to that of reference data.

    • l1_reg: Strength of l1 regularisation to apply to the differences.

    • binarize_preds: Whether to test for discrepency on soft (e.g. probs/logits) model predictions directly with a K-S test or binarise to 0-1 prediction errors and apply a binomial test.

    • train_size: Optional fraction (float between 0 and 1) of the dataset used to train the classifier. The drift is detected on 1 - train_size. Cannot be used in combination with n_folds.

    • n_folds: Optional number of stratified folds used for training. The model preds are then calculated on all the out-of-fold instances. This allows to leverage all the reference and test data for drift detection at the expense of longer computation. If both train_size and n_folds are specified, n_folds is prioritized.

    • retrain_from_scratch: Whether the classifier should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    • seed: Optional random seed for fold selection.

    • optimizer: Optimizer used during training of the kernel. From torch.optim for PyTorch and tf.keras.optimizers for TensorFlow.

    • learning_rate: Learning rate for the optimizer.

    • batch_size: Batch size used during training of the kernel.

    • preprocess_batch_fn: Optional batch preprocessing function. For example to convert a list of generic objects to a tensor which can be processed by the kernel.

    • epochs: Number of training epochs for the kernel.

    • verbose: Verbosity level during the training of the kernel. 0 is silent and 1 prints a progress bar.

    • train_kwargs: Optional additional kwargs for the built-in TensorFlow (from alibi_detect.models.tensorflow import trainer) or PyTorch (from alibi_detect.models.pytorch import trainer) trainer functions.

    • dataset: Dataset object used during training of the classifier. Defaults to alibi_detect.utils.pytorch.TorchDataset (an instance of torch.utils.data.Dataset) for the PyTorch backend and alibi_detect.utils.tensorflow.TFDataset (an instance of tf.keras.utils.Sequence) for the TensorFlow backend. For PyTorch, the dataset should only take the data x and the array of labels y as input, so when e.g. TorchDataset is passed to the detector at initialisation, during training TorchDataset(x, y) is used. For TensorFlow, the dataset is an instance of tf.keras.utils.Sequence, so when e.g. TFDataset is passed to the detector at initialisation, during training TFDataset(x, y, batch_size=batch_size, shuffle=True) is used. x can be of type np.ndarray or List[Any] while y is of type np.ndarray.

    • input_shape: Shape of input data.

    • data_type: Optionally specify the data type (e.g. tabular, image or time-series). Added to metadata.

    Additional PyTorch keyword arguments:

    • device: cuda or gpu to use the GPU and cpu for the CPU. If the device is not specified, the detector will try to leverage the GPU if possible and otherwise fall back on CPU.

    • dataloader: Dataloader object used during training of the classifier. Defaults to torch.utils.data.DataLoader. The dataloader is not initialized yet, this is done during init off the detector using the batch_size. Custom dataloaders can be passed as well, e.g. for graph data we can use torch_geometric.data.DataLoader.

    Defining the kernel

    Any differentiable Pytorch or TensorFlow module that takes as input two instances and outputs a scalar (representing similarity) can be used as the kernel for this drift detector. By default a simple Gaussian RBF kernel is used. Keeping the kernel simple can aid interpretability, but alternatively a "deep kernel" of the form k(x,y)=(1−ϵ)∗ka(Φ(x),Φ(y))+ϵ∗kb(x,y),k(x,y)=(1-\epsilon)*k_a(\Phi(x), \Phi(y)) + \epsilon*k_b(x,y),k(x,y)=(1−ϵ)∗ka​(Φ(x),Φ(y))+ϵ∗kb​(x,y), where $\Phi$ is a (differentiable) projection, $k_a$ and $k_b$ are simple kernels (such as a Gaussian RBF) and $\epsilon>0$ a small constant can be used. The DeepKernel class found in either alibi_detect.utils.tensorflow or alibi_detect.utils.pytorch aims to make defining such kernels straightforward. You should not allow too many learnable parameters however as we would like the classifier to discriminate using the test locations rather than kernel parameters.

    Instantiating the detector

    Instantiating the detector is as simple as passing your reference data and selecting a backend, but you should also consider the number of "diffs" you would like your model to use to discriminate reference from test instances and the strength of regularisation you would like to apply to them.

    Using n_diffs=1 is the simplest to interpret and seems to work well in practice. Using more diffs may result in stronger detection power but the diffs may be harder to interpret due to intereactions and conditional dependencies.

    The strength of the regularisation (l1_reg) to apply to the diffs should also be specified. Stronger regularisation results in sparser diffs as the classifier is encouraged to discriminate using fewer features. This may make the diff more interpretable but may again come at the cost of detection power.

    Alternatively we could have used the TensorFlow backend and defined a deep kernel with a convolutional structure:

    Detect Drift

    We detect data drift by simply calling predict on a batch of instances x. return_p_val equal to True will also return the p-value of the test, return_distance equal to True will return a notion of strength of the drift, return_probs equals True returns the out-of-fold classifier model prediction probabilities on the reference and test data (0 = reference data, 1 = test data) as well as the associated out-of-fold reference and test instances, and return_kernel equals True will also return the trained kernel.

    The prediction takes the form of a dictionary with meta and data keys. meta contains the detector's metadata while data is also a dictionary which contains the actual predictions stored in the following keys:

    • is_drift: 1 if the sample tested has drifted from the reference data and 0 otherwise.

    • diffs: a numpy array containing the diffs used to discriminate reference from test instances.

    • diff_coeffs a coefficient correspond to each diff where a coeffient greater than zero implies that the corresponding diff makes the average reference instances more similar to a test instance on average and less than zero implies less similar.

    • threshold: the user-defined p-value threshold defining the significance of the test

    • p_val: the p-value of the test if return_p_val equals True.

    • distance: a notion of strength of the drift if return_distance equals True. Equal to the K-S test statistic assuming binarize_preds equals False or the relative error reduction over the baseline error expected under the null if binarize_preds equals True.

    • probs_ref: the instance level prediction probability for the reference data x_ref (0 = reference data, 1 = test data) if return_probs is True.

    • probs_test: the instance level prediction probability for the test data x if return_probs is true.

    • x_ref_oof: the instances associated with probs_ref if return_probs equals True.

    • x_test_oof: the instances associated with probs_test if return_probs equals True.

    • kernel: The trained kernel if return_kernel equals True.

    Examples

    Interpretable Drift detection on MNIST and the Wine Quality dataset

    source
    Classifier
    Jitkrittum et al. (2016)
    Classifier drift detector to detect drift in the input space.

    Dataset

    The Amazon dataset contains product reviews with a star rating. We will test whether drift can be detected if the ratings start to drift. For more information, check the WILDS documentation page.

    Dependencies

    Besides alibi-detect, this example notebook also uses the Amazon dataset through the WILDS package. WILDS is a curated collection of benchmark datasets that represent distribution shifts faced in the wild and can be installed via pip:

    Throughout the notebook we use detectors with both PyTorch and TensorFlow backends.

    Load and prepare data

    We first load the dataset and create reference data, data which should not be rejected under the null of the test (H0) and data which should exhibit drift (H1). The drift is introduced later by specifying a specific star rating for the test instances.

    The following cell will download the Amazon dataset (if DOWNLOAD=True). The download size is ~7GB and size on disk is ~7GB.

    Detect drift

    MMD detector on transformer embeddings

    First we embed instances using a pretrained transformer model and detect data drift using the MMD detector on the embeddings.

    Helper functions:

    Define the transformer embedding preprocessing step:

    Define a function which will for a specified number of iterations (n_sample):

    • Configure the MMDDrift detector with a new reference data sample

    • Detect drift on the H0 and H1 splits

    Classifier drift detector

    Now we will use the ClassifierDrift detector which uses a binary classification model to try and distinguish the reference from the test (H0 or H1) data. Drift is then detected on the difference between the prediction distributions on out-of-fold reference vs. test instances using a Kolmogorov-Smirnov 2 sample test on the prediction probabilities or via a binomial test on the binarized predictions. We use a pretrained transformer model but freeze its weights and only train the head which consists of 2 dense layers with a leaky ReLU non-linearity:

    TensorFlow drift detector

    We can do the same using TensorFlow instead of PyTorch as backend. We first define the classifier again and then simply run the detector:

    Maximum Mean Discrepancy (MMD) detector
    pre-trained transformers
    Dataset

    CIFAR10 consists of 60,000 32 by 32 RGB images equally distributed over 10 classes.

    Load CIFAR10 data

    Load or define outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found here. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    Check quality VAE model

    Check outliers on original CIFAR images

    Plot instance level outlier scores

    Visualize predictions

    Predict outliers on perturbed CIFAR images

    We perturb CIFAR images by adding random noise to patches (masks) of the image. For each mask size in n_mask_sizes, sample n_masks and apply those to each of the n_imgs images. Then we predict outliers on the masked instances:

    Define masks and get images:

    Calculate instance level outlier scores:

    Visualize outlier scores vs. mask sizes

    Investigate instance level outlier

    Reconstruction of masked images and outlier scores per channel:

    Visualize:

    Predict outliers on a subset of features

    The sensitivity of the outlier detector can not only be controlled via the threshold, but also by selecting the percentage of the features used for the instance level outlier score computation. For instance, we might want to flag outliers if 40% of the features (pixels for images) have an average outlier score above the threshold. This is possible via the outlier_perc argument in the predict function. It specifies the percentage of the features that are used for outlier detection, sorted in descending outlier score order.

    Visualize outlier scores vs. mask sizes and percentage of features used:

    Infer outlier threshold value

    Finding good threshold values can be tricky since they are typically not easy to interpret. The infer_threshold method helps finding a sensible value. We need to pass a batch of instances X and specify what percentage of those we consider to be normal via threshold_perc.

    VAE
    import logging
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import tensorflow as tf
    tf.keras.backend.clear_session()
    from tensorflow.keras.layers import Conv2D, Conv2DTranspose, \
        Dense, Layer, Reshape, InputLayer, Flatten
    from tqdm import tqdm
    
    from alibi_detect.od import OutlierAE
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.utils.perturbation import apply_mask
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_image
    
    logger = tf.get_logger()
    logger.setLevel(logging.ERROR)
    train, test = tf.keras.datasets.cifar10.load_data()
    X_train, y_train = train
    X_test, y_test = test
    
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    print(X_train.shape, y_train.shape, X_test.shape, y_test.shape)
    load_outlier_detector = True
    #| scrolled: true
    filepath = 'my_path'  # change to (absolute) directory where model is downloaded
    detector_type = 'outlier'
    dataset = 'cifar10'
    detector_name = 'OutlierAE'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        encoding_dim = 1024
        
        encoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(32, 32, 3)),
              Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu),
              Flatten(),
              Dense(encoding_dim,)
          ])
    
        decoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(encoding_dim,)),
              Dense(4*4*128),
              Reshape(target_shape=(4, 4, 128)),
              Conv2DTranspose(256, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(3, 4, strides=2, padding='same', activation='sigmoid')
          ])
        
        # initialize outlier detector
        od = OutlierAE(threshold=.015,  # threshold for outlier score
                        encoder_net=encoder_net,  # can also pass AE model instead
                        decoder_net=decoder_net,  # of separate encoder and decoder
                        )
        # train
        od.fit(X_train,
               epochs=50,
               verbose=True)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    idx = 8
    X = X_train[idx].reshape(1, 32, 32, 3)
    X_recon = od.ae(X)
    plt.imshow(X.reshape(32, 32, 3))
    plt.axis('off')
    plt.show()
    plt.imshow(X_recon.numpy().reshape(32, 32, 3))
    plt.axis('off')
    plt.show()
    X = X_train[:500]
    print(X.shape)
    od_preds = od.predict(X,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    print(list(od_preds['data'].keys()))
    target = np.zeros(X.shape[0],).astype(int)  # all normal CIFAR10 training instances
    labels = ['normal', 'outlier']
    plot_instance_score(od_preds, target, labels, od.threshold)
    X_recon = od.ae(X).numpy()
    plot_feature_outlier_image(od_preds, 
                               X, 
                               X_recon=X_recon,
                               instance_ids=[8, 60, 100, 330],  # pass a list with indices of instances to display
                               max_instances=5,  # max nb of instances to display
                               outliers_only=False)  # only show outlier predictions
    # nb of predictions per image: n_masks * n_mask_sizes 
    n_mask_sizes = 10
    n_masks = 20
    n_imgs = 50
    mask_sizes = [(2*n,2*n) for n in range(1,n_mask_sizes+1)]
    print(mask_sizes)
    img_ids = np.arange(n_imgs)
    X_orig = X[img_ids].reshape(img_ids.shape[0], 32, 32, 3)
    print(X_orig.shape)
    #| scrolled: true
    all_img_scores = []
    for i in tqdm(range(X_orig.shape[0])):
        img_scores = np.zeros((len(mask_sizes),))
        for j, mask_size in enumerate(mask_sizes):
            # create masked instances
            X_mask, mask = apply_mask(X_orig[i].reshape(1, 32, 32, 3),
                                      mask_size=mask_size,
                                      n_masks=n_masks,
                                      channels=[0,1,2],
                                      mask_type='normal',
                                      noise_distr=(0,1),
                                      clip_rng=(0,1))
            # predict outliers
            od_preds_mask = od.predict(X_mask)
            score = od_preds_mask['data']['instance_score']
            # store average score over `n_masks` for a given mask size
            img_scores[j] = np.mean(score)
        all_img_scores.append(img_scores)
    x_plt = [mask[0] for mask in mask_sizes]
    for ais in all_img_scores:
        plt.plot(x_plt, ais)
        plt.xticks(x_plt)
    plt.title('Outlier Score All Images for Increasing Mask Size')
    plt.xlabel('Mask size')
    plt.ylabel('Outlier Score')
    plt.show()
    ais_np = np.zeros((len(all_img_scores), all_img_scores[0].shape[0]))
    for i, ais in enumerate(all_img_scores):
        ais_np[i, :] = ais
    ais_mean = np.mean(ais_np, axis=0)
    plt.title('Mean Outlier Score All Images for Increasing Mask Size')
    plt.xlabel('Mask size')
    plt.ylabel('Outlier score')
    plt.plot(x_plt, ais_mean)
    plt.xticks(x_plt)
    plt.show()
    i = 8  # index of instance to look at
    plt.plot(x_plt, all_img_scores[i])
    plt.xticks(x_plt)
    plt.title('Outlier Scores Image {} for Increasing Mask Size'.format(i))
    plt.xlabel('Mask size')
    plt.ylabel('Outlier score')
    plt.show()
    #| scrolled: true
    all_X_mask = []
    X_i = X_orig[i].reshape(1, 32, 32, 3)
    all_X_mask.append(X_i)
    # apply masks
    for j, mask_size in enumerate(mask_sizes):
        # create masked instances
        X_mask, mask = apply_mask(X_i,
                                  mask_size=mask_size,
                                  n_masks=1,  # just 1 for visualization purposes
                                  channels=[0,1,2],
                                  mask_type='normal',
                                  noise_distr=(0,1),
                                  clip_rng=(0,1))
        all_X_mask.append(X_mask)
    all_X_mask = np.concatenate(all_X_mask, axis=0)
    all_X_recon = od.ae(all_X_mask).numpy()
    od_preds = od.predict(all_X_mask)
    plot_feature_outlier_image(od_preds, 
                               all_X_mask, 
                               X_recon=all_X_recon, 
                               max_instances=all_X_mask.shape[0], 
                               n_channels=3)
    perc_list = [20, 40, 60, 80, 100]
    
    all_perc_scores = []
    for perc in perc_list:
        od_preds_perc = od.predict(all_X_mask, outlier_perc=perc)
        iscore = od_preds_perc['data']['instance_score']
        all_perc_scores.append(iscore)
    x_plt = [0] + x_plt
    for aps in all_perc_scores:
        plt.plot(x_plt, aps)
        plt.xticks(x_plt)
    plt.legend(perc_list)
    plt.title('Outlier Score for Increasing Mask Size and Different Feature Subsets')
    plt.xlabel('Mask Size')
    plt.ylabel('Outlier Score')
    plt.show()
    print('Current threshold: {}'.format(od.threshold))
    od.infer_threshold(X, threshold_perc=99)  # assume 1% of the training data are outliers
    print('New threshold: {}'.format(od.threshold))
    from torch import nn
    from alibi_detect.utils.pytorch import DeepKernel
    
    # define the projection phi
    proj = nn.Sequential(
        nn.Conv2d(3, 8, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(8, 16, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(16, 32, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
    )
    
    # define the kernel
    kernel = DeepKernel(proj, eps=0.01)
    from alibi_detect.utils.pytorch.kernels import sigma_median, GaussianRBF
    
    # example usage
    x, y = torch.randn(*shape), torch.randn(*shape)
    dist = ((x[:, None, :] - y[None, :, :]) ** 2).sum(-1)  # distance used for the GaussianRBF kernel
    sigma = sigma_median(x, y, dist)
    kernel_b = GaussianRBF(sigma=sigma, trainable=True)
    
    # equivalent TensorFlow and KeOps functions
    from alibi_detect.utils.tensorflow.kernels import sigma_median
    from alibi_detect.utils.keops.kernels import sigma_mean
    # instantiate the detector
    from alibi_detect.cd import LearnedKernelDrift
    
    cd = LearnedKernelDrift(x_ref, kernel, backend='pytorch', p_val=.05, epochs=10, batch_size=32)
    from alibi_detect.utils.keops import DeepKernel
    
    kernel = DeepKernel(proj, eps=0.01)
    cd = LearnedKernelDrift(x_ref, kernel, backend='keops', p_val=.05, epochs=10, batch_size=32)
    import tensorflow as tf
    from tensorflow.keras.layers import Conv2D, Flatten, Input
    from alibi_detect.utils.tensorflow import DeepKernel
    
    # define the projection phi
    proj = tf.keras.Sequential(
      [
          Input(shape=(32, 32, 3)),
          Conv2D(8, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(16, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(32, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
      ]
    )
    
    # define the kernel
    kernel = DeepKernel(proj, eps=0.01)
    
    # instantiate the detector
    cd = LearnedKernelDrift(x_ref, kernel, backend='tensorflow', p_val=.05, epochs=10, batch_size=32)
    preds = cd.predict(X, return_p_val=True, return_distance=True)
    !pip install alibi
    import numpy as np
    import pandas as pd
    from typing import List, Tuple, Dict, Callable
    
    from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
    from sklearn.svm import LinearSVC
    
    from sklearn.compose import ColumnTransformer
    from sklearn.preprocessing import StandardScaler, OneHotEncoder
    from sklearn.model_selection import train_test_split
    
    from alibi.datasets import fetch_adult
    from alibi_detect.cd import ClassifierDrift
    # fetch adult dataset
    adult = fetch_adult()
    
    # separate columns in numerical and categorical.
    categorical_names = [adult.feature_names[i] for i in adult.category_map.keys()]
    categorical_ids = list(adult.category_map.keys())
    
    numerical_names = [name for i, name in enumerate(adult.feature_names) if i not in adult.category_map.keys()]
    numerical_ids = [i for i in range(len(adult.feature_names)) if i not in adult.category_map.keys()]
    
    X = adult.data
    education_col = adult.feature_names.index('Education')
    education = adult.category_map[education_col]
    print(education)
    # define low education
    low_education = [
        education.index('Dropout'),
        education.index('High School grad'),
        education.index('Bachelors')
        
    ]
    # define high education
    high_education = [
        education.index('Bachelors'),
        education.index('Masters'),
        education.index('Doctorate')
    ]
    print("Low education:", [education[i] for i in low_education])
    print("High education:", [education[i] for i in high_education])
    # select instances for low and high education
    low_education_mask = pd.Series(X[:, education_col]).isin(low_education).to_numpy()
    high_education_mask = pd.Series(X[:, education_col]).isin(high_education).to_numpy()
    X_low, X_high = X[low_education_mask], X[high_education_mask]
    size = 1000
    np.random.seed(0)
    
    # define reference and H0 dataset
    idx_low = np.random.choice(np.arange(X_low.shape[0]), size=2*size, replace=False)
    x_ref, x_h0 = train_test_split(X_low[idx_low], test_size=0.5, random_state=5, shuffle=True)
    
    # define reference and H1 dataset
    idx_high = np.random.choice(np.arange(X_high.shape[0]), size=size, replace=False)
    x_h1 = X_high[idx_high]
    # define numerical standard scaler.
    num_transf = StandardScaler()
    
    # define categorical one-hot encoder.
    cat_transf = OneHotEncoder(
        categories=[range(len(x)) for x in adult.category_map.values()],
        handle_unknown="ignore"
    )
    
    # Define column transformer
    preprocessor = ColumnTransformer(
        transformers=[
            ("cat", cat_transf, categorical_ids),
            ("num", num_transf, numerical_ids),
        ],
        sparse_threshold=0
    )
    
    # fit preprocessor.
    preprocessor = preprocessor.fit(np.concatenate([x_ref, x_h0, x_h1]))
    labels = ['No!', 'Yes!']
    
    def print_preds(preds: dict, preds_name: str) -> None:
        print(preds_name)
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print(f'p-value: {preds["data"]["p_val"]:.3f}')
        print('')
    # define classifier
    model = RandomForestClassifier()
    
    # define drift detector with binarize prediction
    detector = ClassifierDrift(
        x_ref=x_ref,
        model=model,
        backend='sklearn',
        preprocess_fn=preprocessor.transform,
        binarize_preds=True,
        n_folds=2,
    )
    
    # print results
    print_preds(detector.predict(x=x_h0), "H0")
    print_preds(detector.predict(x=x_h1), "H1")
    # define model
    model = GradientBoostingClassifier()
    
    
    # define drift detector
    detector = ClassifierDrift(
        x_ref=x_ref,
        model=model,
        backend='sklearn',
        preprocess_fn=preprocessor.transform,
        preds_type='scores',
        binarize_preds=False,
        n_folds=2,
    )
    
    # print results
    print_preds(detector.predict(x=x_h0), "H0")
    print_preds(detector.predict(x=x_h1), "H1")
    # define model - does not support predict_proba
    model = LinearSVC(max_iter=10000)
    
    # define drift detector
    detector = ClassifierDrift(
        x_ref=x_ref,
        model=model,
        backend='sklearn',
        preprocess_fn=preprocessor.transform,
        binarize_preds=False,
        n_folds=2,
        use_calibration=True,
        calibration_kwargs={'method': 'isotonic'}
    )
    
    # print results
    print_preds(detector.predict(x=x_h0), "H0")
    print_preds(detector.predict(x=x_h1), "H1")
    n_estimators = 400
    n_folds = 5
    %%time
    # define drift detector
    detector_rf = ClassifierDrift(
        x_ref=x_ref,
        model=RandomForestClassifier(n_estimators=n_estimators),
        backend='sklearn',
        preprocess_fn=preprocessor.transform,
        binarize_preds=False,
        n_folds=n_folds
    )
    
    # print results
    print_preds(detector_rf.predict(x=x_h0), "H0")
    print_preds(detector_rf.predict(x=x_h1), "H1")
    %%time
    # define drift detector
    detector_rf_oob = ClassifierDrift(
        x_ref=x_ref,
        model=RandomForestClassifier(n_estimators=n_estimators),
        backend='sklearn',
        preprocess_fn=preprocessor.transform,
        binarize_preds=False,
        use_oob=True
    )
    
    # print results
    print_preds(detector_rf_oob.predict(x=x_h0), "H0")
    print_preds(detector_rf_oob.predict(x=x_h1), "H1")
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import os
    import tensorflow as tf
    import torch
    from torch import nn
    
    from alibi_detect.cd import ClassifierUncertaintyDrift, RegressorUncertaintyDrift
    from alibi_detect.models.tensorflow import scale_by_instance
    from alibi_detect.utils.fetching import fetch_tf_model, fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    from alibi_detect.models.pytorch import trainer
    from alibi_detect.cd.utils import encompass_batching
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    np.random.seed(0)
    n_test = X_test.shape[0]
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    idx_h0 = np.delete(np.arange(n_test), idx, axis=0)
    X_ref,y_ref = X_test[idx], y_test[idx]
    X_h0, y_h0 = X_test[idx_h0], y_test[idx_h0]
    print(X_ref.shape, X_h0.shape)
    # check that the classes are more or less balanced
    classes, counts_ref = np.unique(y_ref, return_counts=True)
    counts_h0 = np.unique(y_h0, return_counts=True)[1]
    print('Class Ref H0')
    for cl, cref, ch0 in zip(classes, counts_ref, counts_h0):
        assert cref + ch0 == n_test // 10
        print('{}     {} {}'.format(cl, cref, ch0))
    n_corr = len(corruption)
    X_c = [X_corr[i * n_test:(i + 1) * n_test] for i in range(n_corr)]
    #| tags: [hide_input]
    i = 1
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow(X_test[i])
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    dataset = 'cifar10'
    model = 'resnet32'
    clf = fetch_tf_model(dataset, model)
    acc = clf.evaluate(scale_by_instance(X_test), y_test, batch_size=128, verbose=0)[1]
    print('Test set accuracy:')
    print('Original {:.4f}'.format(acc))
    clf_accuracy = {'original': acc}
    for _ in range(len(corruption)):
        acc = clf.evaluate(scale_by_instance(X_c[_]), y_test, batch_size=128, verbose=0)[1]
        clf_accuracy[corruption[_]] = acc
        print('{} {:.4f}'.format(corruption[_], acc))
    #| scrolled: false
    cd = ClassifierUncertaintyDrift(
      X_ref, model=clf, backend='tensorflow', p_val=0.05, preds_type='probs'
    )
    from timeit import default_timer as timer
    
    labels = ['No!', 'Yes!']
    
    def make_predictions(cd, x_h0, x_corr, corruption):
        t = timer()
        preds = cd.predict(x_h0)
        dt = timer() - t
        print('No corruption')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('Feature-wise p-values:')
        print(preds['data']['p_val'])
        print(f'Time (s) {dt:.3f}')
        
        if isinstance(x_corr, list):
            for x, c in zip(x_corr, corruption):
                t = timer()
                preds = cd.predict(x)
                dt = timer() - t
                print('')
                print(f'Corruption type: {c}')
                print('Drift? {}'.format(labels[preds['data']['is_drift']]))
                print('Feature-wise p-values:')
                print(preds['data']['p_val'])
                print(f'Time (s) {dt:.3f}')
    make_predictions(cd, X_h0, X_c, corruption)
    red = pd.read_csv(
        "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv", sep=';'
    )
    white = pd.read_csv(
        "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv", sep=';'
    )
    red.describe()
    white.describe()
    red, white = np.asarray(red, np.float32), np.asarray(white, np.float32)
    n_red, n_white = red.shape[0], white.shape[0]
    
    col_maxes = red.max(axis=0)
    red, white = red / col_maxes, white / col_maxes
    red, white = red[np.random.permutation(n_red)], white[np.random.permutation(n_white)]
    X, y = red[:, :-1], red[:, -1:]
    X_corr, y_corr = white[:, :-1], white[:, -1:]
    X_train, y_train = X[:(n_red//2)], y[:(n_red//2)]
    X_ref, y_ref = X[(n_red//2):(3*n_red//4)], y[(n_red//2):(3*n_red//4)]
    X_h0, y_h0 = X[(3*n_red//4):], y[(3*n_red//4):]
    
    X_train_ds = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train))
    X_train_dl = torch.utils.data.DataLoader(X_train_ds, batch_size=32, shuffle=True, drop_last=True)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    reg = nn.Sequential(
        nn.Linear(11, 16),
        nn.ReLU(),
        nn.Dropout(0.5),
        nn.Linear(16, 32),
        nn.ReLU(),
        nn.Dropout(0.5),
        nn.Linear(32, 1)
    ).to(device)
    
    trainer(reg, nn.MSELoss(), X_train_dl, device, torch.optim.Adam, learning_rate=0.001, epochs=30)
    reg = reg.eval()
    reg_fn = encompass_batching(reg, backend='pytorch', batch_size=32)
    preds_ref = reg_fn(X_ref)
    preds_corr = reg_fn(X_corr)
    
    ref_mse = np.square(preds_ref - y_ref).mean()
    corr_mse = np.square(preds_corr - y_corr).mean()
    
    print(f'MSE when predicting the quality of unseen red wine samples: {ref_mse}')
    print(f'MSE when predicting the quality of unseen white wine samples: {corr_mse}')
    cd = RegressorUncertaintyDrift(
        X_ref, model=reg, backend='pytorch', p_val=0.05, uncertainty_type='mc_dropout', n_evals=100
    )
    preds_h0 = cd.predict(X_h0)
    preds_h1 = cd.predict(X_corr)
    
    print(f"Drift detected on unseen red wine samples? {'yes' if preds_h0['data']['is_drift']==1 else 'no'}")
    print(f"Drift detected on white wine samples? {'yes' if preds_h1['data']['is_drift']==1 else 'no'}")
    
    print(f"p-value on unseen red wine samples? {preds_h0['data']['p_val']}")
    print(f"p-value on white wine samples? {preds_h1['data']['p_val']}")
    from alibi_detect.cd import SpotTheDiffDrift
    
    cd = SpotTheDiffDrift(
        x_ref,
        backend='pytorch',
        p_val=.05,
        n_diffs=1,
        l1_reg=1e-3,
        epochs=10,
        batch_size=32
    )
    
    import tensorflow as tf
    from tensorflow.keras.layers import Conv2D, Flatten, Input
    from alibi_detect.utils.tensorflow import DeepKernel
    
    # define the projection phi with not too much flexability
    proj = tf.keras.Sequential(
      [
          Input(shape=(32, 32, 3)),
          Conv2D(8, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(16, 4, strides=2, padding='same', activation=tf.nn.relu, trainable=False),
          Conv2D(32, 4, strides=2, padding='same', activation=tf.nn.relu, trainable=False),
          Flatten(),
      ]
    )
    
    # define the kernel
    kernel = DeepKernel(proj, eps=0.01)
    
    # instantiate the detector
    cd = SpotTheDiffDrift(
        x_ref,
        backend='tensorflow',
        p_val=.05,
        kernel=kernel,
        n_diffs=1,
        l1_reg=1e-3,
        epochs=10,
        batch_size=32
    )
    preds = cd.predict(X, return_p_val=True, return_distance=True)
    !pip install wilds
    import numpy as np
    import torch
    
    def set_seed(seed: int) -> None:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        np.random.seed(seed)
    
    seed = 1234
    set_seed(seed)
    AMAZON_PATH = './data/amazon' # path to save data
    DOWNLOAD = False  # set to True for first run
    #| scrolled: true
    from functools import partial
    from sklearn.model_selection import train_test_split
    from torch.utils.data import DataLoader, Subset
    from wilds import get_dataset
    from wilds.common.data_loaders import get_train_loader
    
    ds = get_dataset(dataset='amazon', root_dir=AMAZON_PATH, download=DOWNLOAD)
    ds_tr = ds.get_subset('train')
    idx_ref, idx_h0 = train_test_split(np.arange(len(ds_tr)), train_size=.5, random_state=seed, shuffle=True)
    ds_ref = Subset(ds_tr, idx_ref)
    ds_h0 = Subset(ds_tr, idx_h0)
    ds_h1 = ds.get_subset('test')
    dl = partial(DataLoader, shuffle=True, batch_size=100, collate_fn=ds.collate, num_workers=2)
    dl_ref, dl_h0, dl_h1 = dl(ds_ref), dl(ds_h0), dl(ds_h1)
    from typing import List
    
    
    def update_flat_list(x: List[list]):
        return [item for sublist in x for item in sublist]
    
    
    def accumulate_sample(dataloader: DataLoader, sample_size: int, stars: int = None):
        """ Create batches of data from dataloaders. """
        batch_count, stars_count = 0, 0
        x_out, y_out, meta_out = [], [], []
        for x, y, meta in dataloader:
            y, meta = y.numpy(), meta.numpy()
            if isinstance(stars, int):
                idx_stars = np.where(y == stars)[0]
                y, meta = y[idx_stars], meta[idx_stars]
                x = tuple([x[idx] for idx in idx_stars])
            n_batch = y.shape[0]
            idx = min(sample_size - batch_count, n_batch)
            batch_count += n_batch
            x_out += [x[:idx]]
            y_out += [y[:idx]]
            meta_out += [meta[:idx]]
            if batch_count >= sample_size:
                break
        x_out = update_flat_list(x_out)
        y_out = np.concatenate(y_out, axis=0)
        meta_out = np.concatenate(meta_out, axis=0)
        return x_out, y_out, meta_out
    #| scrolled: true
    from alibi_detect.cd import MMDDrift
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    from functools import partial
    from transformers import AutoTokenizer
    
    emb_type = 'hidden_state'  # pooler_output, last_hidden_state or hidden_state
    # layers to extract hidden states from for the embedding used in drift detection
    # only relevant for emb_type = 'hidden_state'
    n_layers = 8
    layers = [-_ for _ in range(1, n_layers + 1)]
    max_len = 100  # max length for the tokenizer
    
    model_name = 'bert-base-cased'  # a model supported by the transformers library
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    embedding = TransformerEmbedding(model_name, emb_type, layers).to(device).eval()
    preprocess_fn = partial(preprocess_drift, model=embedding, tokenizer=tokenizer, max_len=max_len, batch_size=32)
    labels = ['No!', 'Yes!']
    
    
    def print_preds(preds: dict, preds_name: str) -> None:
        print(preds_name)
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print(f'p-value: {preds["data"]["p_val"]:.3f}')
        print('') 
        
    
    def make_predictions(ref_size: int, test_size: int, n_sample: int, stars_h1: int = 4) -> None:
        """ Create drift MMD detector, init, sample data and make predictions. """
        for _ in range(n_sample):
            # sample data
            x_ref, y_ref, meta_ref = accumulate_sample(dl_ref, ref_size)
            x_h0, y_h0, meta_h0 = accumulate_sample(dl_h0, test_size)
            x_h1, y_h1, meta_h1 = accumulate_sample(dl_h1, test_size, stars=stars_h1)
            # init and run detector
            dd = MMDDrift(x_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn, n_permutations=1000)
            preds_h0 = dd.predict(x_h0)
            preds_h1 = dd.predict(x_h1)
            print_preds(preds_h0, 'H0')
            print_preds(preds_h1, 'H1')
    #| scrolled: false
    make_predictions(ref_size=1000, test_size=1000, n_sample=2, stars_h1=4)
    import torch.nn as nn
    from transformers import DistilBertModel
    
    model_name = 'distilbert-base-uncased'
    
    class Classifier(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.lm = DistilBertModel.from_pretrained(model_name)
            for param in self.lm.parameters():  # freeze language model weights
                param.requires_grad = False
            self.head = nn.Sequential(nn.Linear(768, 512), nn.LeakyReLU(.1), nn.Linear(512, 2))
        
        def forward(self, tokens) -> torch.Tensor:
            h = self.lm(**tokens).last_hidden_state
            h = nn.MaxPool1d(kernel_size=100)(h.permute(0, 2, 1)).squeeze(-1)
            return self.head(h)
    
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = Classifier()
    from alibi_detect.cd import ClassifierDrift
    from alibi_detect.utils.prediction import tokenize_transformer
    
    
    def make_predictions(model, backend: str, ref_size: int, test_size: int, n_sample: int, stars_h1: int = 4) -> None:
        """ Create drift Classifier detector, init, sample data and make predictions. """
        
        # batch_fn tokenizes each batch of instances of the reference and test set during training
        b = 'pt' if backend == 'pytorch' else 'tf'
        batch_fn = partial(tokenize_transformer, tokenizer=tokenizer, max_len=max_len, backend=b)
        
        for _ in range(n_sample):
            # sample data
            x_ref, y_ref, meta_ref = accumulate_sample(dl_ref, ref_size)
            x_h0, y_h0, meta_h0 = accumulate_sample(dl_h0, test_size)
            x_h1, y_h1, meta_h1 = accumulate_sample(dl_h1, test_size, stars=stars_h1)
            # init and run detector
            # since our classifier returns logits, we set preds_type to 'logits'
            # n_folds determines the number of folds used for cross-validation, this makes sure all 
            #   test data is used but only out-of-fold predictions taken into account for the drift detection
            #   alternatively we can set train_size to a fraction between 0 and 1 and not apply cross-validation
            # epochs specifies how many epochs the classifier will be trained for each sample or fold
            # preprocess_batch_fn is applied to each batch of instances and translates the text into tokens
            dd = ClassifierDrift(x_ref, model, backend=backend, p_val=.05, preds_type='logits', 
                                 n_folds=3, epochs=2, preprocess_batch_fn=batch_fn, train_size=None)
            preds_h0 = dd.predict(x_h0)
            preds_h1 = dd.predict(x_h1)
            print_preds(preds_h0, 'H0')
            print_preds(preds_h1, 'H1')
    #| scrolled: true
    make_predictions(model, 'pytorch', ref_size=1000, test_size=1000, n_sample=2, stars_h1=4)
    import tensorflow as tf
    from tensorflow.keras.layers import Dense, LeakyReLU, MaxPool1D
    from transformers import TFDistilBertModel
    
    class ClassifierTF(tf.keras.Model):
        def __init__(self) -> None:
            super(ClassifierTF, self).__init__()
            self.lm = TFDistilBertModel.from_pretrained(model_name)
            self.lm.trainable = False  # freeze language model weights
            self.head = tf.keras.Sequential([Dense(512), LeakyReLU(alpha=.1), Dense(2)])
        
        def call(self, tokens) -> tf.Tensor:
            h = self.lm(**tokens).last_hidden_state
            h = tf.squeeze(MaxPool1D(pool_size=100)(h), axis=1)
            return self.head(h)
        
        @classmethod
        def from_config(cls, config):  # not needed for sequential/functional API models
            return cls(**config)
    
    model = ClassifierTF()
    #| scrolled: false
    make_predictions(model, 'tensorflow', ref_size=1000, test_size=1000, n_sample=2, stars_h1=4)
    import os
    import logging
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    tf.keras.backend.clear_session()
    from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Dense, Layer, Reshape, InputLayer
    from tqdm import tqdm
    
    from alibi_detect.models.tensorflow import elbo
    from alibi_detect.od import OutlierVAE
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.utils.perturbation import apply_mask
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_image
    
    logger = tf.get_logger()
    logger.setLevel(logging.ERROR)
    train, test = tf.keras.datasets.cifar10.load_data()
    X_train, y_train = train
    X_test, y_test = test
    
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    print(X_train.shape, y_train.shape, X_test.shape, y_test.shape)
    load_outlier_detector = True
    filepath = 'my_path'  # change to directory where model is downloaded
    detector_type = 'outlier'
    dataset = 'cifar10'
    detector_name = 'OutlierVAE'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        latent_dim = 1024
        
        encoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(32, 32, 3)),
              Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu)
          ])
    
        decoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(latent_dim,)),
              Dense(4*4*128),
              Reshape(target_shape=(4, 4, 128)),
              Conv2DTranspose(256, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(3, 4, strides=2, padding='same', activation='sigmoid')
          ])
        
        # initialize outlier detector
        od = OutlierVAE(threshold=.015,  # threshold for outlier score
                        score_type='mse',  # use MSE of reconstruction error for outlier detection
                        encoder_net=encoder_net,  # can also pass VAE model instead
                        decoder_net=decoder_net,  # of separate encoder and decoder
                        latent_dim=latent_dim,
                        samples=2)
        # train
        od.fit(X_train, 
               loss_fn=elbo,
               cov_elbo=dict(sim=.05),
               epochs=50,
               verbose=False)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    idx = 8
    X = X_train[idx].reshape(1, 32, 32, 3)
    X_recon = od.vae(X)
    plt.imshow(X.reshape(32, 32, 3))
    plt.axis('off')
    plt.show()
    plt.imshow(X_recon.numpy().reshape(32, 32, 3))
    plt.axis('off')
    plt.show()
    X = X_train[:500]
    print(X.shape)
    od_preds = od.predict(X,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    print(list(od_preds['data'].keys()))
    target = np.zeros(X.shape[0],).astype(int)  # all normal CIFAR10 training instances
    labels = ['normal', 'outlier']
    plot_instance_score(od_preds, target, labels, od.threshold)
    X_recon = od.vae(X).numpy()
    plot_feature_outlier_image(od_preds, 
                               X, 
                               X_recon=X_recon,
                               instance_ids=[8, 60, 100, 330],  # pass a list with indices of instances to display
                               max_instances=5,  # max nb of instances to display
                               outliers_only=False)  # only show outlier predictions
    # nb of predictions per image: n_masks * n_mask_sizes 
    n_mask_sizes = 10
    n_masks = 20
    n_imgs = 50
    mask_sizes = [(2*n,2*n) for n in range(1,n_mask_sizes+1)]
    print(mask_sizes)
    img_ids = np.arange(n_imgs)
    X_orig = X[img_ids].reshape(img_ids.shape[0], 32, 32, 3)
    print(X_orig.shape)
    #| scrolled: true
    all_img_scores = []
    for i in tqdm(range(X_orig.shape[0])):
        img_scores = np.zeros((len(mask_sizes),))
        for j, mask_size in enumerate(mask_sizes):
            # create masked instances
            X_mask, mask = apply_mask(X_orig[i].reshape(1, 32, 32, 3),
                                      mask_size=mask_size,
                                      n_masks=n_masks,
                                      channels=[0,1,2],
                                      mask_type='normal',
                                      noise_distr=(0,1),
                                      clip_rng=(0,1))
            # predict outliers
            od_preds_mask = od.predict(X_mask)
            score = od_preds_mask['data']['instance_score']
            # store average score over `n_masks` for a given mask size
            img_scores[j] = np.mean(score)
        all_img_scores.append(img_scores)
    x_plt = [mask[0] for mask in mask_sizes]
    for ais in all_img_scores:
        plt.plot(x_plt, ais)
        plt.xticks(x_plt)
    plt.title('Outlier Score All Images for Increasing Mask Size')
    plt.xlabel('Mask size')
    plt.ylabel('Outlier Score')
    plt.show()
    ais_np = np.zeros((len(all_img_scores), all_img_scores[0].shape[0]))
    for i, ais in enumerate(all_img_scores):
        ais_np[i, :] = ais
    ais_mean = np.mean(ais_np, axis=0)
    plt.title('Mean Outlier Score All Images for Increasing Mask Size')
    plt.xlabel('Mask size')
    plt.ylabel('Outlier score')
    plt.plot(x_plt, ais_mean)
    plt.xticks(x_plt)
    plt.show()
    i = 8  # index of instance to look at
    plt.plot(x_plt, all_img_scores[i])
    plt.xticks(x_plt)
    plt.title('Outlier Scores Image {} for Increasing Mask Size'.format(i))
    plt.xlabel('Mask size')
    plt.ylabel('Outlier score')
    plt.show()
    all_X_mask = []
    X_i = X_orig[i].reshape(1, 32, 32, 3)
    all_X_mask.append(X_i)
    # apply masks
    for j, mask_size in enumerate(mask_sizes):
        # create masked instances
        X_mask, mask = apply_mask(X_i,
                                  mask_size=mask_size,
                                  n_masks=1,  # just 1 for visualization purposes
                                  channels=[0,1,2],
                                  mask_type='normal',
                                  noise_distr=(0,1),
                                  clip_rng=(0,1))
        all_X_mask.append(X_mask)
    all_X_mask = np.concatenate(all_X_mask, axis=0)
    all_X_recon = od.vae(all_X_mask).numpy()
    od_preds = od.predict(all_X_mask)
    plot_feature_outlier_image(od_preds, 
                               all_X_mask, 
                               X_recon=all_X_recon, 
                               max_instances=all_X_mask.shape[0], 
                               n_channels=3)
    perc_list = [20, 40, 60, 80, 100]
    
    all_perc_scores = []
    for perc in perc_list:
        od_preds_perc = od.predict(all_X_mask, outlier_perc=perc)
        iscore = od_preds_perc['data']['instance_score']
        all_perc_scores.append(iscore)
    x_plt = [0] + x_plt
    for aps in all_perc_scores:
        plt.plot(x_plt, aps)
        plt.xticks(x_plt)
    plt.legend(perc_list)
    plt.title('Outlier Score for Increasing Mask Size and Different Feature Subsets')
    plt.xlabel('Mask Size')
    plt.ylabel('Outlier Score')
    plt.show()
    print('Current threshold: {}'.format(od.threshold))
    od.infer_threshold(X, threshold_perc=99)  # assume 1% of the training data are outliers
    print('New threshold: {}'.format(od.threshold))
    . Loading is performed in the same way, by simply running
    load_detector(filepath)
    .

    ✅

    Maximum Mean Discrepancy

    ✅

    ✅

    Learned Kernel MMD

    ❌

    ✅

    Chi-Squared

    ✅

    ✅

    Mixed-type tabular

    ✅

    ✅

    Classifier

    ✅

    ✅

    Spot-the-diff

    ❌

    ✅

    Classifier Uncertainty

    ❌

    ✅

    Regressor Uncertainty

    ❌

    ✅

    Online Cramér-von Mises

    ❌

    ✅

    Online Fisher's Exact Test

    ❌

    ✅

    Online Least-Squares Density Difference

    ❌

    ✅

    Online Maximum Mean Discrepancy

    ❌

    ✅

    Detector
    Legacy save/load
    Config save/load

    Isolation Forest

    ✅

    ❌

    Mahalanobis Distance

    ✅

    ❌

    AE

    ✅

    ❌

    VAE

    Detector
    Legacy save/load
    Config save/load

    Adversarial AE

    ✅

    ❌

    Model distillation

    ✅

    ❌

    Kolmogorov-Smirnov

    ✅

    ✅

    Cramér-von Mises

    ❌

    ✅

    Fisher's Exact Test

    ❌

    ✅

    Least-Squares Density Difference

    TOML
    Detector Configuration Files
    dill
    Getting Started docs for Keras
    Roadmap
    preprocessing
    Classifier
    detector configuration file
    HDF5
    register_keras_serializable
    entire model
    dill
    joblib
    sklearn.base.BaseEstimator
    xgboost
    Online drift detectors

    ❌

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_tensorflow

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    ContextMMDDrift

    Inherits from: DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    c_ref

    numpy.ndarray

    Context for the reference distribution.

    backend

    str

    'tensorflow'

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data, given the provided context.

    Name
    Type
    Default
    Description

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    c

    numpy.ndarray

    Context associated with batch of instances.

    return_p_val

    bool

    True

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    score

    Compute the MMD based conditional test statistic, and perform a conditional permutation test to obtain a

    p-value representing the test statistic's extremity under the null hypothesis.

    Name
    Type
    Default
    Description

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    c

    numpy.ndarray

    Context associated with batch of instances.

    Returns

    • Type: Tuple[float, float, float, Tuple]

    Maximum Mean Discrepancy drift detector on CIFAR-10

    Method

    The Maximum Mean Discrepancy (MMD) detector is a kernel-based method for multivariate 2 sample testing. The MMD is a distance-based measure between 2 distributions p and q based on the mean embeddings $\mu_{p}$ and $\mu_{q}$ in a reproducing kernel Hilbert space $F$:

    MMD(F,p,q)=∣∣μp−μq∣∣F2MMD(F, p, q) = || \mu_{p} - \mu_{q} ||^2_{F}MMD(F,p,q)=∣∣μp​−μq​∣∣F2​

    We can compute unbiased estimates of $MMD^2$ from the samples of the 2 distributions after applying the kernel trick. We use by default a radial basis function kernel, but users are free to pass their own kernel of preference to the detector. We obtain a $p$-value via a permutation test on the values of $MMD^2$. This method is also described in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift.

    Backend

    The method is implemented in both the PyTorch and TensorFlow frameworks with support for CPU and GPU. Various preprocessing steps are also supported out-of-the box in Alibi Detect for both frameworks and illustrated throughout the notebook. Alibi Detect does however not install PyTorch for you. Check the how to do this.

    Dataset

    consists of 60,000 32 by 32 RGB images equally distributed over 10 classes. We evaluate the drift detector on the CIFAR-10-C dataset (). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance. We also check for drift against the original test set with class imbalances.

    Load data

    Original CIFAR-10 data:

    For CIFAR-10-C, we can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the original test set in a reference dataset and a dataset which should not be rejected under the H0 of the MMD test. We also split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    We can also verify that the performance of a classification model on CIFAR-10 drops significantly on this perturbed dataset:

    Given the drop in performance, it is important that we detect the harmful data drift!

    Detect drift with TensorFlow backend

    First we try a drift detector using the TensorFlow framework for both the preprocessing and the MMD computation steps.

    We are trying to detect data drift on high-dimensional (32x32x3) data using a multivariate MMD permutation test. It therefore makes sense to apply dimensionality reduction first. Some dimensionality reduction methods also used in are readily available: a randomly initialized encoder (UAE or Untrained AutoEncoder in the paper), BBSDs (black-box shift detection using the classifier's softmax outputs) and PCA (using scikit-learn).

    Random encoder

    First we try the randomly initialized encoder:

    Let's check whether the detector thinks drift occurred on the different test sets and time the prediction calls:

    As expected, drift was only detected on the corrupted datasets.

    BBSDs

    For BBSDs, we use the classifier's softmax outputs for black-box shift detection. This method is based on . The ResNet classifier is trained on data standardised by instance so we need to rescale the data.

    Initialisation of the drift detector. Here we use the output of the softmax layer to detect the drift, but other hidden layers can be extracted as well by setting 'layer' to the index of the desired hidden layer in the model:

    Again drift is only flagged on the perturbed data.

    Detect drift with PyTorch backend

    We can do the same thing using the PyTorch backend. We illustrate this using the randomly initialized encoder as preprocessing step:

    Since our PyTorch encoder expects the images in a (batch size, channels, height, width) format, we transpose the data:

    The drift detector will attempt to use the GPU if available and otherwise falls back on the CPU. We can also explicitly specify the device. Let's compare the GPU speed up with the CPU implementation:

    Notice the over 30x acceleration provided by the GPU.

    Similar to the TensorFlow implementation, PyTorch can also use the hidden layer output from a pretrained model for the preprocessing step via:

    Supervised drift detection on the penguins dataset

    Method

    When true outputs/labels are available, we can perform supervised drift detection; monitoring the model's performance directly in order to check for harmful drift. Two detectors ideal for this application are the Fisher’s Exact Test (FET) detector and Cramér-von Mises (CVM) detector detectors.

    The FET detector is designed for use on binary data, such as the instance level performance indicators from a classifier (i.e. 0/1 for each incorrect/correct classification). The CVM detector is designed use on continuous data, such as a regressor's instance level loss or error scores.

    In this example we will use the offline versions of these detectors, which are suitable for use on batches of data. In many cases data may arrive sequentially, and the user may wish to perform drift detection as the data arrives to ensure it is detected as soon as possible. In this case, the online versions of the FET and CVM detectors can be used, as will be explored in a future example.

    Dataset

    The dataset consists of data on 344 penguins from 3 islands in the Palmer Archipelago, Antarctica. There are 3 different species of penguin in the dataset, and a common task is to classify the the species of each penguin based upon two features, the length and depth of the peguin's bill, or beak.

    Artwork by

    This notebook requires the seaborn package for visualization and the palmerpenguins package to load data. Thse can be installed via pip:

    Load data

    To download the dataset we use the package:

    The data consists of 333 rows (one row is removed as contains a NaN), one for each penguin. There are 8 features describing the peguins' physical characteristics, their species and sex, the island each resides on, and the year measurements were taken.

    Classification example

    For our first example use case, we will perform the popular species classification task. Here we wish the classify the species based on only bill_length_mm and bill_depth_mm. To start we remove the other features and visualise those that remain.

    The above plot shows that the Adeilie species can primarily be identified by looking at bill length. Then to further distinguish between Gentoo and Chinstrap, we can look at the bill depth.

    Next we separate the data into inputs and outputs, and encoder the species data to integers. Finally, we now split into three data sets; one to train the classifier, one to act a reference set when testing for drift, and one to test for drift on.

    Train a classifier

    For this dataset, a relatively shallow decision tree classifier should be sufficient, and so we train an sklearn one on the training data.

    As expected, the decision tree is able to give acceptable classification accuracy on the train and test sets.

    Shift data

    In order to demonstrate use of the drift detectors, we first need to add some artificial drift to the test data X_test. We add two types of drift here; to create covariate drift we subtract 5mm from the bill length of all the Gentoo penguins. $P(y|\mathbf{X})$ is unchanged here, but clearly we have introduced a delta $\Delta P(\mathbf{X})$. To create concept drift, we switch the labels of the Gentoo and Chinstrap penguins, so that the underlying process $P(y|\mathbf{X})$ is changed.

    We now define a utility function to plot the classifier's decision boundaries, and we use this to visualise the reference data set, the test set, and the two new data sets where drift is present.

    These plots serve as a visualisation of the differences between covariate drift and concept drift. Importantly, the model accuracies shown above also highlight the fact that not all drift is necessarily malicious, in the sense that even relatively significant drift does not always lead to degradation in a model's performance indicators. For example, the model actually gives a slightly higher accuracy on the covariate drift data set than on the no drift set in this case. Conversely, the concept drift unsuprisingly leads to severely degraded model performance.

    Unsupervised drift detection

    Before getting to the main task in this example, monitoring malicious drift with a supervised drift detector, we will first use the to check for covariate drift. To do this we initialise it in an unsupervised manner by passing it the input data X_ref.

    Applying this detector on the no drift, covariate drift and concept drift data sets, we see that the detector only detects drift in the covariate drift case. Not detecting drift in the no drift case is desirable, but not detecting drift in the concept drift case is potentially problematic.

    Supervised drift detection

    The fact that the unsupervised detector above does not detect the severe concept drift demonstrates the motivation for using supervised drift detectors that directly check for malicious drift, which can include malicious concept drift.

    To perform supervised drift detection we first need to compute the model's performance indicators. Since this is a classification task, a suitable performance indicator is the instance level binary losses, which are computed below.

    As seen above, these losses are binary data, where 0 represents an incorrect classification for each instance, and 1 represents a correct classification.

    Since this is binary data, the FET detector is chosen, and initialised on the reference loss data. The alternative hypothesis is set to less, meaning we will only flag drift if the proportion of 1s to 0s is reduced compared to the reference data. In other words, we only flag drift if the model's performance has degraded.

    Applying this detector to the same three data sets, we see that malicious drift isn't detected in the no drift or covariate drift cases, which is unsurprising since the model performance isn't degraded in these cases. However, with this supervised detector, we now detect the malicious concept drift as desired.

    Regression example

    To provide a short example of supervised detection in a regression setting, we now rework the dataset into a regression task, and use the detector on the model's squared error.

    Warning: Must have scipy >= 1.7.0 installed for this example.

    Load data and train model

    For a regression task, we take the penguins' flipper length and sex as inputs, and aim to predict the penguins' body mass. Looking at a scatter plot of these features, we can see there is substantial correlation between the chosen inputs and outputs.

    Again, we split the dataset into the same three sets; a training set, reference set and test set.

    This time we train a linear regressor on the training data, and find that it gives acceptable training and test accuracy.

    Shift data

    To generate a copy of the test data with concept drift added, we use the model to create new output data, with a multiplicative factor and some Gaussian noise added. The quality of our synthetic output data is of course affected by the accuracy of the model, but it serves to demonstrate the behavior of the model (and detector) when $P(y|\mathbf{X})$ is changed.

    Unsurprisingly, the covariate drift leads to degradation in the model accuracy.

    Supervised drift detection

    As in the classification example, in order to perform supervised drift detection we need to compute the models performance indicators. For this regression example, the instance level squared errors are used.

    The CVM detector is trained on the reference losses:

    As desired, the CVM detector does not detect drift on the no drift data, but does on covariate drift data.

    alibi_detect.ad.model_distillation

    Constants

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    ModelDistillation

    Inherits from: BaseDetector, FitMixin, ThresholdMixin, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    fit

    Train ModelDistillation detector.

    Name
    Type
    Default
    Description

    Returns

    • Type: None

    infer_threshold

    Update threshold by a value inferred from the percentage of instances considered to be

    adversarial in a sample of the dataset.

    Name
    Type
    Default
    Description

    Returns

    • Type: None

    predict

    Predict whether instances are adversarial instances or not.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]

    score

    Compute adversarial scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]

    alibi_detect.cd.base_online

    Constants

    TYPE_CHECKING

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    BaseMultiDriftOnline

    Inherits from: BaseDetector, StateMixin, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    get_threshold

    Return the threshold for timestep t.

    Name
    Type
    Default
    Description

    Returns

    • Type: float

    predict

    Predict whether the most recent window of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    reset

    Deprecated reset method. This method will be repurposed or removed in the future. To reset the detector to

    its initial state (t=0) use :meth:reset_state.

    Returns

    • Type: None

    reset_state

    Resets the detector to its initial state (t=0). This does not include reconfiguring thresholds.

    Returns

    • Type: None

    BaseUniDriftOnline

    Inherits from: BaseDetector, StateMixin, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    get_threshold

    Return the threshold for timestep t.

    Name
    Type
    Default
    Description

    Returns

    • Type: numpy.ndarray

    predict

    Predict whether the most recent window(s) of data have drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    reset

    Deprecated reset method. This method will be repurposed or removed in the future. To reset the detector to

    its initial state (t=0) use :meth:reset_state.

    Returns

    • Type: None

    reset_state

    Resets the detector to its initial state (t=0). This does not include reconfiguring thresholds.

    Returns

    • Type: None

    alibi_detect.cd.learned_kernel

    Constants

    has_pytorch

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_tensorflow

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_keops

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    LearnedKernelDrift

    Inherits from: DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]

    alibi_detect.cd.keops.learned_kernel

    LearnedKernelDriftKeops

    Inherits from: BaseLearnedKernelDrift, BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    score

    Compute the p-value resulting from a permutation test using the maximum mean discrepancy

    as a distance measure between the reference data and the data to be tested. The kernel used within the MMD is first trained to maximise an estimate of the resulting test power.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float]

    trainer

    Train the kernel to maximise an estimate of test power using minibatch gradient descent.

    Name
    Type
    Default
    Description

    Returns

    • Type: None

    AEGMM and VAEGMM outlier detection on KDD Cup ‘99 dataset

    Method

    The AEGMM method follows the ICLR 2018 paper. The encoder compresses the data while the reconstructed instances generated by the decoder are used to create additional features based on the reconstruction error between the input and the reconstructions. These features are combined with encodings and fed into a Gaussian Mixture Model (GMM). Training of the AEGMM model is unsupervised on normal (inlier) data. The sample energy of the GMM can then be used to determine whether an instance is an outlier (high sample energy) or not (low sample energy). VAEGMM on the other hand uses a instead of a plain autoencoder.

    Mahalanobis outlier detection on KDD Cup ‘99 dataset

    Method

    The Mahalanobis online outlier detector aims to predict anomalies in tabular data. The algorithm calculates an outlier score, which is a measure of distance from the center of the features distribution (). If this outlier score is higher than a user-defined threshold, the observation is flagged as an outlier. The algorithm is online, which means that it starts without knowledge about the distribution of the features and learns as requests arrive. Consequently you should expect the output to be bad at the start and to improve over time.

    alibi_detect.cd.classifier

    Constants

    has_pytorch

    bool(x) -> bool

    model = ... # A TensorFlow model
    preprocess_fn = partial(preprocess_drift, model=model, batch_size=128)
    cd = MMDDrift(x_ref, backend='tensorflow', p_val=.05, preprocess_fn=preprocess_fn)
    cd = ClassifierDrift(x_ref, model, backend='sklearn', p_val=.05, preds_type='probs')
    from alibi_detect.cd import LSDDDriftOnline
    from alibi_detect.saving import save_detector, load_detector
    
    # Init detector (t=0)
    dd = LSDDDriftOnline(x_ref, window_size=10, ert=50)
    
    # Run 2 predictions
    pred_1 = dd.predict(x_1)  # t=1 
    pred_2 = dd.predict(x_2)  # t=2
    
    # Save detector (state will be saved since t>0)
    save_detector(dd, filepath)
    
    # Load detector
    dd_new = load_detector(filepath)  # detector will start at t=2
    dd.reset_state()  # reset to t=0
    save_detector(dd, filepath)  # save the detector without state
    has_pytorch: bool = True
    has_tensorflow: bool = True
    logger: logging.Logger = <Logger alibi_detect.cd.context_aware (WARNING)>
    ContextMMDDrift(self, x_ref: Union[numpy.ndarray, list], c_ref: numpy.ndarray, backend: str = 'tensorflow', p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, x_kernel: Callable = None, c_kernel: Callable = None, n_permutations: int = 1000, prop_c_held: float = 0.25, n_folds: int = 5, batch_size: Optional[int] = 256, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None, verbose: bool = False) -> None
    predict(x: Union[numpy.ndarray, list], c: numpy.ndarray, return_p_val: bool = True, return_distance: bool = True, return_coupling: bool = False) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    score(x: Union[numpy.ndarray, list], c: numpy.ndarray) -> Tuple[float, float, float, Tuple]
    logger: logging.Logger = <Logger alibi_detect.ad.model_distillation (WARNING)>
    TYPE_CHECKING: bool = False
    has_pytorch: bool = True

    ✅

    ❌

    AEGMM

    ✅

    ❌

    VAEGMM

    ✅

    ❌

    Likelihood Ratios

    ✅

    ❌

    Prophet

    ✅

    ❌

    Spectral Residual

    ✅

    ❌

    Seq2Seq

    ✅

    ❌

    Backend used for the MMD implementation.

    p_val

    float

    0.05

    p-value used for the significance of the permutation test.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last N instances seen by the detector. The parameter should be passed as a dictionary {'last': N}.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_kernel

    Callable

    None

    Kernel defined on the input data, defaults to Gaussian RBF kernel.

    c_kernel

    Callable

    None

    Kernel defined on the context data, defaults to Gaussian RBF kernel.

    n_permutations

    int

    1000

    Number of permutations used in the permutation test.

    prop_c_held

    float

    0.25

    Proportion of contexts held out to condition on.

    n_folds

    int

    5

    Number of cross-validation folds used when tuning the regularisation parameters.

    batch_size

    Optional[int]

    256

    If not None, then compute batches of MMDs at a time (rather than all at once).

    device

    Union[Literal[cuda, gpu, cpu], ForwardRef('torch.device'), None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device. Only relevant for 'pytorch' backend.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    verbose

    bool

    False

    Whether to print progress messages.

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    Whether to return the conditional MMD test statistic between the new batch and reference data.

    return_coupling

    bool

    False

    Whether to return the coupling matrices.

    PyTorch docs
    CIFAR10
    Hendrycks & Dietterich, 2019
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    Detecting and Correcting for Label Shift with Black Box Predictors

    loss_type

    str

    'kld'

    Loss for distillation. Supported: 'kld', 'xent'

    temperature

    float

    1.0

    Temperature used for model prediction scaling. Temperature <1 sharpens the prediction probability distribution.

    data_type

    Optional[str]

    None

    Optionally specifiy the data type (tabular, image or time-series). Added to metadata.

    epochs

    int

    20

    Number of training epochs.

    batch_size

    int

    128

    Batch size used for training.

    verbose

    bool

    True

    Whether to print training progress.

    log_metric

    Tuple[str, ForwardRef('tf.keras.metrics')]

    None

    Additional metrics whose progress will be displayed if verbose equals True.

    callbacks

    .tensorflow.keras.callbacks

    None

    Callbacks used during training.

    preprocess_fn

    Callable

    None

    Preprocessing function applied to each training batch.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    threshold

    Optional[float]

    None

    Threshold used for score to determine adversarial instances.

    distilled_model

    Optional[keras.src.models.model.Model]

    None

    A tf.keras model to distill.

    model

    Optional[keras.src.models.model.Model]

    None

    X

    numpy.ndarray

    Training batch.

    loss_fn

    .tensorflow.keras.losses

    <function loss_distillation at 0x28ee8c4c0>

    Loss function used for training.

    optimizer

    Union[ForwardRef('tf.keras.optimizers.Optimizer'), ForwardRef('tf.keras.optimizers.legacy.Optimizer'), type[ForwardRef('tf.keras.optimizers.Optimizer')], type[ForwardRef('tf.keras.optimizers.legacy.Optimizer')]]

    <class 'keras.src.optimizers.adam.Adam'>

    X

    numpy.ndarray

    Batch of instances.

    threshold_perc

    float

    99.0

    Percentage of X considered to be normal based on the adversarial score.

    margin

    float

    0.0

    X

    numpy.ndarray

    Batch of instances.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    return_instance_score

    bool

    True

    X

    numpy.ndarray

    Batch of instances to analyze.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    return_predictions

    bool

    False

    A trained tf.keras classification model.

    Optimizer used for training.

    Add margin to threshold. Useful if adversarial instances have significantly higher scores and there is no adversarial instance in X.

    Whether to return instance level adversarial scores.

    Whether to return the predictions of the classifier on the original and reconstructed instances.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    n_bootstraps

    int

    1000

    The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ert.

    verbose

    bool

    True

    Whether or not to print progress during configuration.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    n_bootstraps

    int

    1000

    The number of bootstrap simulations used to configure the thresholds. The larger this is the more accurately the desired ERT will be targeted. Should ideally be at least an order of magnitude larger than the ert.

    n_features

    Optional[int]

    None

    Number of features used in the statistical test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    verbose

    bool

    True

    Whether or not to print progress during configuration.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    ert

    float

    The expected run-time (ERT) in the absence of drift. For the multivariate detectors, the ERT is defined as the expected run-time from t=0.

    window_size

    int

    t

    int

    The timestep to return a threshold for.

    x_t

    Union[numpy.ndarray, typing.Any]

    A single instance to be added to the test-window.

    return_test_stat

    bool

    True

    Whether to return the test statistic and threshold.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    ert

    float

    The expected run-time (ERT) in the absence of drift. For the univariate detectors, the ERT is defined as the expected run-time after the smallest window is full i.e. the run-time from t=min(windows_sizes)-1.

    window_sizes

    List[int]

    t

    int

    The timestep to return a threshold for.

    x_t

    Union[numpy.ndarray, typing.Any]

    A single instance to be added to the test-window(s).

    return_test_stat

    bool

    True

    Whether to return the test statistic and threshold.

    The size of the sliding test-window used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    The sizes of the sliding test-windows used to compute the test-statistic. Smaller windows focus on responding quickly to severe drift, larger windows focus on ability to detect slight drift.

    p_val

    float

    0.05

    p-value used for the significance of the test.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before applying the kernel.

    n_permutations

    int

    100

    The number of permutations to use in the permutation test once the MMD has been computed.

    batch_size_permutations

    int

    1000000

    KeOps computes the n_permutations of the MMD^2 statistics in chunks of batch_size_permutations. Only relevant for 'keops' backend.

    var_reg

    float

    1e-05

    Constant added to the estimated variance of the MMD for stability.

    reg_loss_fn

    Callable

    <function LearnedKernelDrift.<lambda> at 0x28fe7ea60>

    The regularisation term reg_loss_fn(kernel) is added to the loss function being optimized.

    train_size

    Optional[float]

    0.75

    Optional fraction (float between 0 and 1) of the dataset used to train the kernel. The drift is detected on 1 - train_size.

    retrain_from_scratch

    bool

    True

    Whether the kernel should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    optimizer

    Optional[Callable]

    None

    Optimizer used during training of the kernel.

    learning_rate

    float

    0.001

    Learning rate used by optimizer.

    batch_size

    int

    32

    Batch size used during training of the kernel.

    batch_size_predict

    int

    32

    Batch size used for the trained drift detector predictions.

    preprocess_batch_fn

    Optional[Callable]

    None

    Optional batch preprocessing function. For example to convert a list of objects to a batch which can be processed by the kernel.

    epochs

    int

    3

    Number of training epochs for the kernel. Corresponds to the smaller of the reference and test sets.

    num_workers

    int

    0

    Number of workers for the dataloader. The default (num_workers=0) means multi-process data loading is disabled. Setting num_workers>0 may be unreliable on Windows.

    verbose

    int

    0

    Verbosity level during the training of the kernel. 0 is silent, 1 a progress bar.

    train_kwargs

    Optional[dict]

    None

    Optional additional kwargs when training the kernel.

    device

    Union[Literal[cuda, gpu, cpu], ForwardRef('torch.device'), None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device. Relevant for 'pytorch' and 'keops' backends.

    dataset

    Optional[Callable]

    None

    Dataset object used during training.

    dataloader

    Optional[Callable]

    None

    Dataloader object used during training. Relevant for 'pytorch' and 'keops' backends.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    return_kernel

    bool

    True

    Whether to return the updated kernel trained to discriminate reference and test instances.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    kernel

    Callable

    Trainable PyTorch or TensorFlow module that returns a similarity between two instances.

    backend

    str

    'tensorflow'

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    Backend used by the kernel and training loop.

    Whether to return the MMD metric between the new batch and reference data.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before applying the kernel.

    n_permutations

    int

    100

    The number of permutations to use in the permutation test once the MMD has been computed.

    batch_size_permutations

    int

    1000000

    KeOps computes the n_permutations of the MMD^2 statistics in chunks of batch_size_permutations.

    var_reg

    float

    1e-05

    Constant added to the estimated variance of the MMD for stability.

    reg_loss_fn

    Callable

    <function LearnedKernelDriftKeops.<lambda> at 0x28fe7e5e0>

    The regularisation term reg_loss_fn(kernel) is added to the loss function being optimized.

    train_size

    Optional[float]

    0.75

    Optional fraction (float between 0 and 1) of the dataset used to train the kernel. The drift is detected on 1 - train_size.

    retrain_from_scratch

    bool

    True

    Whether the kernel should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    optimizer

    torch.optim.optimizer.Optimizer

    <class 'torch.optim.adam.Adam'>

    Optimizer used during training of the kernel.

    learning_rate

    float

    0.001

    Learning rate used by optimizer.

    batch_size

    int

    32

    Batch size used during training of the kernel.

    batch_size_predict

    int

    1000000

    Batch size used for the trained drift detector predictions.

    preprocess_batch_fn

    Optional[Callable]

    None

    Optional batch preprocessing function. For example to convert a list of objects to a batch which can be processed by the kernel.

    epochs

    int

    3

    Number of training epochs for the kernel. Corresponds to the smaller of the reference and test sets.

    num_workers

    int

    0

    Number of workers for the dataloader. The default (num_workers=0) means multi-process data loading is disabled. Setting num_workers>0 may be unreliable on Windows.

    verbose

    int

    0

    Verbosity level during the training of the kernel. 0 is silent, 1 a progress bar.

    train_kwargs

    Optional[dict]

    None

    Optional additional kwargs when training the kernel.

    device

    Union[Literal[cuda, gpu, cpu], torch.device, None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device. Relevant for 'pytorch' and 'keops' backends.

    dataset

    Callable

    <class 'alibi_detect.utils.pytorch.data.TorchDataset'>

    Dataset object used during training.

    dataloader

    Callable

    <class 'torch.utils.data.dataloader.DataLoader'>

    Dataloader object used during training. Only relevant for 'pytorch' backend.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    optimizer

    Callable

    <class 'torch.optim.adam.Adam'>

    learning_rate

    float

    0.001

    preprocess_fn

    Optional[Callable]

    None

    epochs

    int

    20

    reg_loss_fn

    Callable

    <function LearnedKernelDriftKeops.<lambda> at 0x28fe7e940>

    verbose

    int

    1

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    kernel

    Union[torch.nn.modules.module.Module, torch.nn.modules.container.Sequential]

    Trainable PyTorch module that returns a similarity between two instances.

    p_val

    float

    0.05

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    j_hat

    alibi_detect.cd.keops.learned_kernel.LearnedKernelDriftKeops.JHat

    dataloaders

    Tuple[torch.utils.data.dataloader.DataLoader, torch.utils.data.dataloader.DataLoader]

    device

    torch.device

    p-value used for the significance of the test.

    Dataset

    The outlier detector needs to detect computer network intrusions using TCP dump data for a local-area network (LAN) simulating a typical U.S. Air Force LAN. A connection is a sequence of TCP packets starting and ending at some well defined times, between which data flows to and from a source IP address to a target IP address under some well defined protocol. Each connection is labeled as either normal, or as an attack.

    There are 4 types of attacks in the dataset:

    • DOS: denial-of-service, e.g. syn flood;

    • R2L: unauthorized access from a remote machine, e.g. guessing password;

    • U2R: unauthorized access to local superuser (root) privileges;

    • probing: surveillance and other probing, e.g., port scanning.

    The dataset contains about 5 million connection records.

    There are 3 types of features:

    • basic features of individual connections, e.g. duration of connection

    • content features within a connection, e.g. number of failed log in attempts

    • traffic features within a 2 second window, e.g. number of connections to the same host as the current connection

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load dataset

    We only keep a number of continuous (18 out of 41) features.

    Assume that a model is trained on normal instances of the dataset (not outliers) and standardization is applied:

    Apply standardization:

    Load or define AEGMM outlier detector

    The pretrained outlier and adversarial detectors used in the example notebooks can be found here. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    The warning tells us we still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have some data which we know contains around 5% outliers. The percentage of outliers can be set with perc_outlier in the create_outlier_batch function.

    Save outlier detector with updated threshold:

    Detect outliers

    We now generate a batch of data with 10% outliers and detect the outliers in the batch.

    Predict outliers:

    Display results

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    We can also plot the ROC curve for the outlier scores of the detector:

    Investigate results

    We can visualize the encodings of the instances in the latent space and the features derived from the instance reconstructions by the decoder. The encodings and features are then fed into the GMM density network.

    A lot of the outliers are already separated well in the latent space.

    Use VAEGMM outlier detector

    We can again instantiate the pretrained VAEGMM detector from the Google Cloud Bucket. You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    Need to infer the threshold again:

    Save outlier detector with updated threshold:

    Detect outliers and display results

    Predict:

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    You can zoom in by adjusting the min and max values in ylim. We can also compare the VAEGMM ROC curve with AEGMM:

    Deep Autoencoding Gaussian Mixture Model for Unsupervised Anomaly Detection
    variational autoencoder
    Dataset

    The outlier detector needs to detect computer network intrusions using TCP dump data for a local-area network (LAN) simulating a typical U.S. Air Force LAN. A connection is a sequence of TCP packets starting and ending at some well defined times, between which data flows to and from a source IP address to a target IP address under some well defined protocol. Each connection is labeled as either normal, or as an attack.

    There are 4 types of attacks in the dataset:

    • DOS: denial-of-service, e.g. syn flood;

    • R2L: unauthorized access from a remote machine, e.g. guessing password;

    • U2R: unauthorized access to local superuser (root) privileges;

    • probing: surveillance and other probing, e.g., port scanning.

    The dataset contains about 5 million connection records.

    There are 3 types of features:

    • basic features of individual connections, e.g. duration of connection

    • content features within a connection, e.g. number of failed log in attempts

    • traffic features within a 2 second window, e.g. number of connections to the same host as the current connection

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Load dataset

    We only keep a number of continuous (18 out of 41) features.

    Assume that a machine learning model is trained on normal instances of the dataset (not outliers) and standardization is applied:

    Define outlier detector

    We train an outlier detector from scratch.

    Be aware that Mahalanobis is an online, stateful outlier detector. Saving or loading a Mahalanobis detector therefore also saves and loads the state of the detector. This allows the user to warm up the detector before deploying it into production.

    The warning tells us we still need to set the outlier threshold. This can be done with the infer_threshold method. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have some data which we know contains around 5% outliers. The percentage of outliers can be set with perc_outlier in the create_outlier_batch function.

    Detect outliers

    We now generate a batch of data with 10% outliers, standardize those with the mean and stdev values obtained from the normal data (inliers) and detect the outliers in the batch.

    Predict outliers:

    We can now save the warmed up outlier detector:

    Display results

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    We can also plot the ROC curve for the outlier scores of the detector:

    Include categorical variables

    So far we only tracked continuous variables. We can however also include categorical variables. The fit step first computes pairwise distances between the categories of each categorical variable. The pairwise distances are based on either the model predictions (MVDM method) or the context provided by the other variables in the dataset (ABDM method). For MVDM, we use the difference between the conditional model prediction probabilities of each category. This method is based on the Modified Value Difference Metric (MVDM) by Cost et al (1993). ABDM stands for Association-Based Distance Metric, a categorical distance measure introduced by Le et al (2005). ABDM infers context from the presence of other variables in the data and computes a dissimilarity measure based on the Kullback-Leibler divergence. Both methods can also be combined as ABDM-MVDM. We can then apply multidimensional scaling to project the pairwise distances into Euclidean space.

    Load and transform data

    Create a dictionary with as keys the categorical columns and values the number of categories for each variable in the dataset. This dictionary will later be used in the fit step of the outlier detector.

    Fit an ordinal encoder on the categorical data:

    Combine scaled numerical and ordinal features. X_fit will be used to infer distances between categorical features later. To make it easy, we will already transform the whole dataset, including the outliers that need to be detected later. This is for illustrative purposes:

    Initialize and fit outlier detector

    We use the same threshold as for the continuous data. This will likely not result in optimal performance. Alternatively, you can infer the threshold again.

    Set fit parameters:

    Apply fit method to find numerical values for categorical variables:

    The numerical values for the categorical features are stored in the attribute od.d_abs. This is a dictionary with as keys the columns for the categorical features and as values the numerical equivalent of the category:

    Another option would be to set d_type to 'mvdm' and y to kddcup.target to infer the numerical values for categorical variables from the model labels (or alternatively the predictions).

    Run outlier detector and display results

    Generate batch of data with 10% outliers:

    Preprocess the outlier batch:

    Predict outliers:

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    Use OHE instead of ordinal encoding for the categorical variables

    Since we will apply one-hot encoding (OHE) on the categorical variables, we convert cat_vars_ord from the ordinal to OHE format. alibi_detect.utils.mapping contains utility functions to do this. The keys in cat_vars_ohe now represent the first column index for each one-hot encoded categorical variable. This dictionary will later be used in the counterfactual explanation.

    Fit a one-hot encoder on the categorical data:

    Transform X_fit to OHE:

    Initialize and fit outlier detector

    Initialize:

    Apply fit method:

    Run outlier detector and display results

    Transform outlier batch to OHE:

    Predict outliers:

    F1 score and confusion matrix:

    Plot instance level outlier scores vs. the outlier threshold:

    Mahalanobis distance
    from functools import partial
    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    
    from alibi_detect.cd import MMDDrift
    from alibi_detect.models.tensorflow import scale_by_instance
    from alibi_detect.utils.fetching import fetch_tf_model
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    np.random.seed(0)
    n_test = X_test.shape[0]
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    idx_h0 = np.delete(np.arange(n_test), idx, axis=0)
    X_ref,y_ref = X_test[idx], y_test[idx]
    X_h0, y_h0 = X_test[idx_h0], y_test[idx_h0]
    print(X_ref.shape, X_h0.shape)
    # check that the classes are more or less balanced
    classes, counts_ref = np.unique(y_ref, return_counts=True)
    counts_h0 = np.unique(y_h0, return_counts=True)[1]
    print('Class Ref H0')
    for cl, cref, ch0 in zip(classes, counts_ref, counts_h0):
        assert cref + ch0 == n_test // 10
        print('{}     {} {}'.format(cl, cref, ch0))
    n_corr = len(corruption)
    X_c = [X_corr[i * n_test:(i + 1) * n_test] for i in range(n_corr)]
    #| tags: [hide_input]
    i = 4
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow(X_test[i])
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    dataset = 'cifar10'
    model = 'resnet32'
    clf = fetch_tf_model(dataset, model)
    acc = clf.evaluate(scale_by_instance(X_test), y_test, batch_size=128, verbose=0)[1]
    print('Test set accuracy:')
    print('Original {:.4f}'.format(acc))
    clf_accuracy = {'original': acc}
    for _ in range(len(corruption)):
        acc = clf.evaluate(scale_by_instance(X_c[_]), y_test, batch_size=128, verbose=0)[1]
        clf_accuracy[corruption[_]] = acc
        print('{} {:.4f}'.format(corruption[_], acc))
    #| scrolled: false
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, InputLayer, Reshape
    from alibi_detect.cd.tensorflow import preprocess_drift
    
    tf.random.set_seed(0)
    
    # define encoder
    encoding_dim = 32
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(encoding_dim,)
      ]
    )
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, batch_size=512)
    
    # initialise drift detector
    cd = MMDDrift(X_ref, backend='tensorflow', p_val=.05, 
                  preprocess_fn=preprocess_fn, n_permutations=100)
    
    # we can also save/load an initialised detector
    filepath = 'detector_tf'  # change to directory where detector is saved
    save_detector(cd, filepath)
    cd = load_detector(filepath)
    from timeit import default_timer as timer
    
    labels = ['No!', 'Yes!']
    
    def make_predictions(cd, x_h0, x_corr, corruption):
        t = timer()
        preds = cd.predict(x_h0)
        dt = timer() - t
        print('No corruption')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print(f'p-value: {preds["data"]["p_val"]:.3f}')
        print(f'Time (s) {dt:.3f}')
        
        if isinstance(x_corr, list):
            for x, c in zip(x_corr, corruption):
                t = timer()
                preds = cd.predict(x)
                dt = timer() - t
                print('')
                print(f'Corruption type: {c}')
                print('Drift? {}'.format(labels[preds['data']['is_drift']]))
                print(f'p-value: {preds["data"]["p_val"]:.3f}')
                print(f'Time (s) {dt:.3f}')
    #| scrolled: false
    make_predictions(cd, X_h0, X_c, corruption)
    X_ref_bbsds = scale_by_instance(X_ref)
    X_h0_bbsds = scale_by_instance(X_h0)
    X_c_bbsds = [scale_by_instance(X_c[i]) for i in range(n_corr)]
    from alibi_detect.cd.tensorflow import HiddenOutput
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(clf, layer=-1), batch_size=128)
    
    # initialise drift detector
    cd = MMDDrift(X_ref_bbsds, backend='tensorflow', p_val=.05, 
                  preprocess_fn=preprocess_fn, n_permutations=100)
    make_predictions(cd, X_h0_bbsds, X_c_bbsds, corruption)
    import torch
    import torch.nn as nn
    
    # set random seed and device
    seed = 0
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(device)
    def permute_c(x):
        return np.transpose(x.astype(np.float32), (0, 3, 1, 2))
    
    X_ref_pt = permute_c(X_ref)
    X_h0_pt = permute_c(X_h0)
    X_c_pt = [permute_c(xc) for xc in X_c]
    print(X_ref_pt.shape, X_h0_pt.shape, X_c_pt[0].shape)
    from alibi_detect.cd.pytorch import preprocess_drift
    
    # define encoder
    encoder_net = nn.Sequential(
        nn.Conv2d(3, 64, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(64, 128, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Conv2d(128, 512, 4, stride=2, padding=0),
        nn.ReLU(),
        nn.Flatten(),
        nn.Linear(2048, encoding_dim)
    ).to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, device=device, batch_size=512)
    
    # initialise drift detector
    cd = MMDDrift(X_ref_pt, backend='pytorch', p_val=.05, 
                  preprocess_fn=preprocess_fn, n_permutations=100)
    
    # we can also save/load an initialised PyTorch based detector
    filepath = 'detector_pt'  # change to directory where detector is saved
    save_detector(cd, filepath)
    cd = load_detector(filepath)
    make_predictions(cd, X_h0_pt, X_c_pt, corruption)
    device = torch.device('cpu')
    preprocess_fn = partial(preprocess_drift, model=encoder_net.to(device), 
                            device=device, batch_size=512)
    
    cd = MMDDrift(X_ref_pt, backend='pytorch', preprocess_fn=preprocess_fn, device='cpu')
    make_predictions(cd, X_h0_pt, X_c_pt, corruption)
    from alibi_detect.cd.pytorch import HiddenOutput
    ModelDistillation(self, threshold: float = None, distilled_model: keras.src.models.model.Model = None, model: keras.src.models.model.Model = None, loss_type: str = 'kld', temperature: float = 1.0, data_type: str = None) -> None
    fit(X: numpy.ndarray, loss_fn: .tensorflow.keras.losses = <function loss_distillation at 0x28ee8c4c0>, optimizer: Union[ForwardRef('tf.keras.optimizers.Optimizer'), ForwardRef('tf.keras.optimizers.legacy.Optimizer'), type[ForwardRef('tf.keras.optimizers.Optimizer')], type[ForwardRef('tf.keras.optimizers.legacy.Optimizer')]] = <class 'keras.src.optimizers.adam.Adam'>, epochs: int = 20, batch_size: int = 128, verbose: bool = True, log_metric: Tuple[str, ForwardRef('tf.keras.metrics')] = None, callbacks: .tensorflow.keras.callbacks = None, preprocess_fn: Callable = None) -> None
    infer_threshold(X: numpy.ndarray, threshold_perc: float = 99.0, margin: float = 0.0, batch_size: int = 10000000000) -> None
    predict(X: numpy.ndarray, batch_size: int = 10000000000, return_instance_score: bool = True) -> Dict[Dict[str, str], Dict[str, numpy.ndarray]]
    score(X: numpy.ndarray, batch_size: int = 10000000000, return_predictions: bool = False) -> Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]
    logger: logging.Logger = <Logger alibi_detect.cd.base_online (WARNING)>
    BaseMultiDriftOnline(self, x_ref: Union[numpy.ndarray, list], ert: float, window_size: int, preprocess_fn: Optional[Callable] = None, x_ref_preprocessed: bool = False, n_bootstraps: int = 1000, verbose: bool = True, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    get_threshold(t: int) -> float
    predict(x_t: Union[numpy.ndarray, typing.Any], return_test_stat: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    reset() -> None
    reset_state() -> None
    BaseUniDriftOnline(self, x_ref: Union[numpy.ndarray, list], ert: float, window_sizes: List[int], preprocess_fn: Optional[Callable] = None, x_ref_preprocessed: bool = False, n_bootstraps: int = 1000, n_features: Optional[int] = None, verbose: bool = True, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    get_threshold(t: int) -> numpy.ndarray
    predict(x_t: Union[numpy.ndarray, typing.Any], return_test_stat: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    reset() -> None
    reset_state() -> None
    has_tensorflow: bool = True
    has_keops: bool = True
    LearnedKernelDrift(self, x_ref: Union[numpy.ndarray, list], kernel: Callable, backend: str = 'tensorflow', p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, n_permutations: int = 100, batch_size_permutations: int = 1000000, var_reg: float = 1e-05, reg_loss_fn: Callable = <function LearnedKernelDrift.<lambda> at 0x28fe7ea60>, train_size: Optional[float] = 0.75, retrain_from_scratch: bool = True, optimizer: Optional[Callable] = None, learning_rate: float = 0.001, batch_size: int = 32, batch_size_predict: int = 32, preprocess_batch_fn: Optional[Callable] = None, epochs: int = 3, num_workers: int = 0, verbose: int = 0, train_kwargs: Optional[dict] = None, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, dataset: Optional[Callable] = None, dataloader: Optional[Callable] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True, return_kernel: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]
    LearnedKernelDriftKeops(self, x_ref: Union[numpy.ndarray, list], kernel: Union[torch.nn.modules.module.Module, torch.nn.modules.container.Sequential], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, n_permutations: int = 100, batch_size_permutations: int = 1000000, var_reg: float = 1e-05, reg_loss_fn: Callable = <function LearnedKernelDriftKeops.<lambda> at 0x28fe7e5e0>, train_size: Optional[float] = 0.75, retrain_from_scratch: bool = True, optimizer: torch.optim.optimizer.Optimizer = <class 'torch.optim.adam.Adam'>, learning_rate: float = 0.001, batch_size: int = 32, batch_size_predict: int = 1000000, preprocess_batch_fn: Optional[Callable] = None, epochs: int = 3, num_workers: int = 0, verbose: int = 0, train_kwargs: Optional[dict] = None, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, dataset: Callable = <class 'alibi_detect.utils.pytorch.data.TorchDataset'>, dataloader: Callable = <class 'torch.utils.data.dataloader.DataLoader'>, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    trainer(j_hat: alibi_detect.cd.keops.learned_kernel.LearnedKernelDriftKeops.JHat, dataloaders: Tuple[torch.utils.data.dataloader.DataLoader, torch.utils.data.dataloader.DataLoader], device: torch.device, optimizer: Callable = <class 'torch.optim.adam.Adam'>, learning_rate: float = 0.001, preprocess_fn: Optional[Callable] = None, epochs: int = 20, reg_loss_fn: Callable = <function LearnedKernelDriftKeops.<lambda> at 0x28fe7e940>, verbose: int = 1) -> None
    !pip install seaborn
    import os
    import logging
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix, f1_score
    import tensorflow as tf
    tf.keras.backend.clear_session()
    from tensorflow.keras.layers import Dense, InputLayer
    
    from alibi_detect.datasets import fetch_kdd
    from alibi_detect.models.tensorflow import eucl_cosim_features
    from alibi_detect.od import OutlierAEGMM, OutlierVAEGMM
    from alibi_detect.utils.data import create_outlier_batch
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_tabular, plot_roc
    
    logger = tf.get_logger()
    logger.setLevel(logging.ERROR)
    kddcup = fetch_kdd(percent10=True)  # only load 10% of the dataset
    print(kddcup.data.shape, kddcup.target.shape)
    np.random.seed(0)
    normal_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=400000, perc_outlier=0)
    X_train, y_train = normal_batch.data.astype('float32'), normal_batch.target
    print(X_train.shape, y_train.shape)
    print('{}% outliers'.format(100 * y_train.mean()))
    mean, stdev = X_train.mean(axis=0), X_train.std(axis=0)
    X_train = (X_train - mean) / stdev
    load_outlier_detector = True
    filepath = 'my_path'  # change to directory (absolute path) where model is downloaded
    detector_type = 'outlier'
    dataset = 'kddcup'
    detector_name = 'OutlierAEGMM'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        # the model defined here is similar to the one defined in the original paper
        n_features = X_train.shape[1]
        latent_dim = 1
        n_gmm = 2  # nb of components in GMM
    
        encoder_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(n_features,)),
            Dense(60, activation=tf.nn.tanh),
            Dense(30, activation=tf.nn.tanh),
            Dense(10, activation=tf.nn.tanh),
            Dense(latent_dim, activation=None)
        ])
    
        decoder_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(latent_dim,)),
            Dense(10, activation=tf.nn.tanh),
            Dense(30, activation=tf.nn.tanh),
            Dense(60, activation=tf.nn.tanh),
            Dense(n_features, activation=None)
        ])
    
        gmm_density_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(latent_dim + 2,)),
            Dense(10, activation=tf.nn.tanh),
            Dense(n_gmm, activation=tf.nn.softmax)
        ])
        
        # initialize outlier detector
        od = OutlierAEGMM(threshold=None,  # threshold for outlier score
                          encoder_net=encoder_net,         # can also pass AEGMM model instead
                          decoder_net=decoder_net,         # of separate encoder, decoder
                          gmm_density_net=gmm_density_net, # and gmm density net
                          n_gmm=n_gmm,
                          recon_features=eucl_cosim_features)  # fn used to derive features
                                                               # from the reconstructed
                                                               # instances based on cosine 
                                                               # similarity and Eucl distance 
        
        # train
        od.fit(X_train,
               epochs=50,
               batch_size=1024,
               verbose=True)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    np.random.seed(0)
    perc_outlier = 5
    threshold_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=perc_outlier)
    X_threshold, y_threshold = threshold_batch.data.astype('float32'), threshold_batch.target
    X_threshold = (X_threshold - mean) / stdev
    print('{}% outliers'.format(100 * y_threshold.mean()))
    od.infer_threshold(X_threshold, threshold_perc=100-perc_outlier)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    np.random.seed(1)
    outlier_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=10)
    X_outlier, y_outlier = outlier_batch.data.astype('float32'), outlier_batch.target
    X_outlier = (X_outlier - mean) / stdev
    print(X_outlier.shape, y_outlier.shape)
    print('{}% outliers'.format(100 * y_outlier.mean()))
    od_preds = od.predict(X_outlier, return_instance_score=True)
    labels = outlier_batch.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {:.4f}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_instance_score(od_preds, y_outlier, labels, od.threshold, ylim=(None, None))
    roc_data = {'AEGMM': {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}}
    plot_roc(roc_data)
    enc = od.aegmm.encoder(X_outlier)  # encoding
    X_recon = od.aegmm.decoder(enc)  # reconstructed instances
    recon_features = od.aegmm.recon_features(X_outlier, X_recon)  # reconstructed features
    df = pd.DataFrame(dict(enc=enc[:, 0].numpy(), 
                           cos=recon_features[:, 0].numpy(), 
                           eucl=recon_features[:, 1].numpy(), 
                           label=y_outlier))
    
    groups = df.groupby('label')
    fig, ax = plt.subplots()
    for name, group in groups:
        ax.plot(group.enc, group.cos, marker='o', 
                linestyle='', ms=6, label=labels[name])
    plt.title('Encoding vs. Cosine Similarity')
    plt.xlabel('Encoding')
    plt.ylabel('Cosine Similarity')
    ax.legend()
    plt.show()
    fig, ax = plt.subplots()
    for name, group in groups:
        ax.plot(group.enc, group.eucl, marker='o', 
                linestyle='', ms=6, label=labels[name])
    plt.title('Encoding vs. Relative Euclidean Distance')
    plt.xlabel('Encoding')
    plt.ylabel('Relative Euclidean Distance')
    ax.legend()
    plt.show()
    load_outlier_detector = True
    filepath = 'my_path'  # change to directory (absolute path) where model is downloaded
    detector_type = 'outlier'
    dataset = 'kddcup'
    detector_name = 'OutlierVAEGMM'
    filepath = os.path.join(filepath, detector_name)
    if load_outlier_detector:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        # the model defined here is similar to the one defined in
        # the OutlierVAE notebook
        n_features = X_train.shape[1]
        latent_dim = 2
        n_gmm = 2
    
        encoder_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(n_features,)),
            Dense(20, activation=tf.nn.relu),
            Dense(15, activation=tf.nn.relu),
            Dense(7, activation=tf.nn.relu)
        ])
    
        decoder_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(latent_dim,)),
            Dense(7, activation=tf.nn.relu),
            Dense(15, activation=tf.nn.relu),
            Dense(20, activation=tf.nn.relu),
            Dense(n_features, activation=None)
        ])
    
        gmm_density_net = tf.keras.Sequential(
        [
            InputLayer(input_shape=(latent_dim + 2,)),
            Dense(10, activation=tf.nn.relu),
            Dense(n_gmm, activation=tf.nn.softmax)
        ])
        
        
        # initialize outlier detector
        od = OutlierVAEGMM(threshold=None,
                           encoder_net=encoder_net,
                           decoder_net=decoder_net,
                           gmm_density_net=gmm_density_net,
                           n_gmm=n_gmm,
                           latent_dim=latent_dim,
                           samples=10,
                           recon_features=eucl_cosim_features)
        
        # train
        od.fit(X_train,
               epochs=50,
               batch_size=1024,
               cov_elbo=dict(sim=.0025),  # standard deviation assumption
               verbose=True)           # for elbo training
        
        # save the trained outlier detector
        save_detector(od, filepath)
    od.infer_threshold(X_threshold, threshold_perc=100-perc_outlier)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    od_preds = od.predict(X_outlier, return_instance_score=True)
    labels = outlier_batch.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {:.4f}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    plot_instance_score(od_preds, y_outlier, labels, od.threshold, ylim=(None, None))
    roc_data['VAEGMM'] = {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}
    plot_roc(roc_data)
    !pip install seaborn
    #| scrolled: true
    #| tags: []
    import matplotlib
    %matplotlib inline
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix, f1_score
    from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
    
    from alibi_detect.od import Mahalanobis
    from alibi_detect.datasets import fetch_kdd
    from alibi_detect.utils.data import create_outlier_batch
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.utils.mapping import ord2ohe
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_roc
    #| tags: []
    kddcup = fetch_kdd(percent10=True)  # only load 10% of the dataset
    print(kddcup.data.shape, kddcup.target.shape)
    #| tags: []
    np.random.seed(0)
    normal_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=100000, perc_outlier=0)
    X_train, y_train = normal_batch.data.astype('float'), normal_batch.target
    print(X_train.shape, y_train.shape)
    print('{}% outliers'.format(100 * y_train.mean()))
    #| tags: []
    mean, stdev = X_train.mean(axis=0), X_train.std(axis=0)
    #| tags: []
    filepath = 'my_path'  # change to directory where model is saved
    detector_name = 'Mahalanobis'
    filepath = os.path.join(filepath, detector_name)
    
    # initialize and save outlier detector
    threshold = None  # scores above threshold are classified as outliers   
    n_components = 2  # nb of components used in PCA
    std_clip = 3  # clip values used to compute mean and cov above "std_clip" standard deviations
    start_clip = 20  # start clipping values after "start_clip" instances
    
    od = Mahalanobis(threshold, 
                     n_components=n_components,
                     std_clip=std_clip,
                     start_clip=start_clip)
    
    save_detector(od, filepath)  # save outlier detector
    #| tags: []
    np.random.seed(0)
    perc_outlier = 5
    threshold_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=perc_outlier)
    X_threshold, y_threshold = threshold_batch.data.astype('float'), threshold_batch.target
    X_threshold = (X_threshold - mean) / stdev
    print('{}% outliers'.format(100 * y_threshold.mean()))
    #| tags: []
    od.infer_threshold(X_threshold, threshold_perc=100-perc_outlier)
    print('New threshold: {}'.format(od.threshold))
    threshold = od.threshold
    #| tags: []
    np.random.seed(1)
    outlier_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=10)
    X_outlier, y_outlier = outlier_batch.data.astype('float'), outlier_batch.target
    X_outlier = (X_outlier - mean) / stdev
    print(X_outlier.shape, y_outlier.shape)
    print('{}% outliers'.format(100 * y_outlier.mean()))
    #| tags: []
    od_preds = od.predict(X_outlier, return_instance_score=True)
    #| tags: []
    save_detector(od, filepath)
    #| tags: []
    labels = outlier_batch.target_names
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    #| tags: []
    plot_instance_score(od_preds, y_outlier, labels, od.threshold, ylim=(0,50))
    #| tags: []
    roc_data = {'MD': {'scores': od_preds['data']['instance_score'], 'labels': y_outlier}}
    plot_roc(roc_data)
    #| tags: []
    cat_cols = ['protocol_type', 'service', 'flag']
    num_cols = ['srv_count', 'serror_rate', 'srv_serror_rate',
                'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 
                'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 
                'dst_host_srv_count', 'dst_host_same_srv_rate', 
                'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate',
                'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 
                'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 
                'dst_host_srv_rerror_rate']
    cols = cat_cols + num_cols
    #| tags: []
    np.random.seed(0)
    kddcup = fetch_kdd(keep_cols=cols, percent10=True)
    print(kddcup.data.shape, kddcup.target.shape)
    #| tags: []
    cat_vars_ord = {}
    n_categories = len(cat_cols)
    for i in range(n_categories):
        cat_vars_ord[i] = len(np.unique(kddcup.data[:, i]))
    print(cat_vars_ord)
    #| tags: []
    enc = OrdinalEncoder()
    enc.fit(kddcup.data[:, :n_categories])
    #| tags: []
    X_num = (kddcup.data[:, n_categories:] - mean) / stdev  # standardize numerical features
    X_ord = enc.transform(kddcup.data[:, :n_categories])  # apply ordinal encoding to categorical features
    X_fit = np.c_[X_ord, X_num].astype(np.float32, copy=False)  # combine numerical and categorical features
    print(X_fit.shape)
    #| tags: []
    n_components = 2
    std_clip = 3
    start_clip = 20
        
    od = Mahalanobis(threshold,
                     n_components=n_components, 
                     std_clip=std_clip, 
                     start_clip=start_clip,
                     cat_vars=cat_vars_ord,
                     ohe=False)  # True if one-hot encoding (OHE) is used
    #| tags: []
    d_type = 'abdm'  # pairwise distance type, 'abdm' infers context from other variables
    disc_perc = [25, 50, 75]  # percentiles used to bin numerical values; used in 'abdm' calculations
    standardize_cat_vars = True  # standardize numerical values of categorical variables
    #| tags: []
    od.fit(X_fit,
           d_type=d_type,
           disc_perc=disc_perc,
           standardize_cat_vars=standardize_cat_vars)
    #| tags: []
    cat = 0  # categorical variable to plot numerical values for
    #| tags: []
    plt.bar(np.arange(len(od.d_abs[cat])), od.d_abs[cat])
    plt.xticks(np.arange(len(od.d_abs[cat])))
    plt.title('Numerical values for categories in categorical variable {}'.format(cat))
    plt.xlabel('Category')
    plt.ylabel('Numerical value')
    plt.show()
    #| tags: []
    np.random.seed(1)
    outlier_batch = create_outlier_batch(kddcup.data, kddcup.target, n_samples=1000, perc_outlier=10)
    data, y_outlier = outlier_batch.data, outlier_batch.target
    print(data.shape, y_outlier.shape)
    print('{}% outliers'.format(100 * y_outlier.mean()))
    #| tags: []
    X_num = (data[:, n_categories:] - mean) / stdev
    X_ord = enc.transform(data[:, :n_categories])
    X_outlier = np.c_[X_ord, X_num].astype(np.float32, copy=False)
    print(X_outlier.shape)
    #| tags: []
    od_preds = od.predict(X_outlier, return_instance_score=True)
    #| tags: []
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    #| tags: []
    plot_instance_score(od_preds, y_outlier, labels, od.threshold, ylim=(0, 150))
    #| tags: []
    cat_vars_ohe = ord2ohe(X_fit, cat_vars_ord)[1]
    print(cat_vars_ohe)
    #| tags: []
    enc = OneHotEncoder(categories='auto')
    enc.fit(X_fit[:, :n_categories])
    #| tags: []
    X_ohe = enc.transform(X_fit[:, :n_categories])
    X_fit = np.array(np.c_[X_ohe.todense(), X_fit[:, n_categories:]].astype(np.float32, copy=False))
    print(X_fit.shape)
    #| tags: []
    od = Mahalanobis(threshold,
                     n_components=n_components, 
                     std_clip=std_clip, 
                     start_clip=start_clip,
                     cat_vars=cat_vars_ohe,
                     ohe=True)
    #| tags: []
    od.fit(X_fit,
           d_type=d_type,
           disc_perc=disc_perc,
           standardize_cat_vars=standardize_cat_vars)
    #| tags: []
    X_ohe = enc.transform(X_ord)
    X_outlier = np.array(np.c_[X_ohe.todense(), X_num].astype(np.float32, copy=False))
    print(X_outlier.shape)
    #| tags: []
    od_preds = od.predict(X_outlier, return_instance_score=True)
    #| tags: []
    y_pred = od_preds['data']['is_outlier']
    f1 = f1_score(y_outlier, y_pred)
    print('F1 score: {}'.format(f1))
    cm = confusion_matrix(y_outlier, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    #| tags: []
    plot_instance_score(od_preds, y_outlier, labels, od.threshold, ylim=(0,200))
    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_tensorflow

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    ClassifierDrift

    Inherits from: DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    model

    Union[sklearn.base.ClassifierMixin, Callable]

    PyTorch, TensorFlow or Sklearn classification model used for drift detection.

    backend

    str

    'tensorflow'

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the test.

    return_distance

    bool

    True

    Returns

    • Type: Dict[str, Dict[str, Union[str, int, float, Callable]]]

    palmerpenguins
    Allison Horst
    palmerpenguins
    MMD detector
    CVM

    Likelihood Ratio Outlier Detection with PixelCNN++

    Method

    The outlier detector described by Ren et al. (2019) in Likelihood Ratios for Out-of-Distribution Detection uses the likelihood ratio between 2 generative models as the outlier score. One model is trained on the original data while the other is trained on a perturbed version of the dataset. This is based on the observation that the likelihood score for an instance under a generative model can be heavily affected by population level background statistics. The second generative model is therefore trained to capture the background statistics still present in the perturbed data while the semantic features have been erased by the perturbations.

    The perturbations are added using an independent and identical Bernoulli distribution with rate $\mu$ which substitutes a feature with one of the other possible feature values with equal probability. For images, this means changing a pixel with a different pixel randomly sampled within the $0$ to $255$ pixel range.

    The generative model used in the example is a PixelCNN++, adapted from the official TensorFlow Probability , and available as a standalone model in from alibi_detect.models.tensorflow import PixelCNN.

    Dataset

    The training set consists of 60,000 28 by 28 grayscale images distributed over 10 classes. The classes represent items of clothing such as shirts or trousers. At test time, we want to distinguish the Fashion-MNIST test set from MNIST, which represents 28 by 28 grayscale numbers from 0 to 9.

    This notebook requires the seaborn package for visualization which can be installed via pip:

    Utility Functions

    Load data

    The in-distribution dataset is Fashion-MNIST and the out-of-distribution dataset we'd like to detect is MNIST.

    Define PixelCNN++ model

    We now need to define our generative model. This is not necessary if the pretrained detector is later loaded from the Google Bucket.

    Key PixelCNN++ arguments in a nutshell:

    • num_resnet: number of layers () within each hierarchical block ().

    • num_hierarchies: number of blocks separated by expansions or contractions of dimensions. See .

    • num_filters: number of convolutional filters.

    • num_logistic_mix: number of components in the logistic mixture distribution.

    Optionally, a different model can be passed to the detector with argument model_background. The mentions that additional $L2$-regularization (l2_weight) for the background model could improve detection performance.

    Load or train the outlier detector

    We can again either fetch the pretrained detector from a or train one from scratch:

    We can load our saved detector again by defining the PixelCNN architectures for the semantic and background models as well as providing the shape of the input data:

    Let's sample some instances from the semantic model to check how good our generative model is:

    Most of the instances look like they represent the dataset well. When we do the same thing for our background model, we see that there is some background noise injected:

    Compare the log likelihoods

    Let's compare the log likelihoods of the inliers vs. the outlier data under the semantic and background models. Although MNIST data looks very distinct from Fashion-MNIST, the generative model does not distinguish well between the 2 datasets as shown by the histograms of the log likelihoods:

    This is due to the dominance of the background which is similar (basically lots of $0$'s for both datasets). If we however take the likelihood ratio, the MNIST data are detected as outliers. And this is exactly what the outlier detector does as well:

    Detect outliers

    We follow the same procedure with the outlier detector. First we need to set an outlier threshold with infer_threshold. We need to pass a batch of instances and specify what percentage of those we consider to be normal via threshold_perc. Let's assume we have a small batch of data with roughly $50$% outliers but we don't know exactly which ones.

    Let's save the outlier detector with updated threshold:

    Let's now predict outliers on the combined Fashion-MNIST and MNIST datasets:

    Display results

    F1 score, accuracy, precision, recall and confusion matrix:

    We can also plot the ROC curve based on the instance level outlier scores and compare it with the likelihood of only the semantic model:

    Analyse feature scores

    To understand why the likelihood ratio works to detect outliers but the raw log likelihoods don't, it is helpful to look at the pixel-wise log likelihoods of both the semantic and background models.

    Plot in-distribution instances:

    It is clear that both the semantic and background model attach high probabilities to the background pixels. This effect is cancelled out in the likelihood ratio in the last column. The same applies to the out-of-distribution instances:

    Scaling up drift detection with KeOps

    Introduction

    A number of convenient and powerful kernel-based drift detectors such as the () or the () do not scale favourably with increasing dataset size $n$, leading to quadratic complexity $\mathcal{O}(n^2)$ for naive implementations. As a result, we can quickly run into memory issues by having to store the $[N_\text{ref} + N_\text{test}, N_\text{ref} + N_\text{test}]$ kernel matrix (on the GPU if applicable) used for an efficient implementation of the permutation test. Note that $N_\text{ref}$ is the reference data size and $N_\text{test}$ the test data size.

    We can however drastically speed up and scale up kernel-based drift detectors to large dataset sizes by working with symbolic kernel matrices instead and leverage the library to do so. For the user of $\texttt{Alibi Detect}$ the only thing that changes is the specification of the detector's backend, e.g. for the MMD detector:

    In this notebook we will run a few simple benchmarks to illustrate the speed and memory improvements from using KeOps over vanilla PyTorch on the GPU (1x RTX 2080 Ti) for both the standard MMD and learned kernel MMD detectors.

    Model Distillation drift detector on CIFAR-10

    Method

    is a technique that is used to transfer knowledge from a large network to a smaller network. Typically, it consists of training a second model with a simplified architecture on soft targets (the output distributions or the logits) obtained from the original model.

    Here, we apply model distillation to obtain harmfulness scores, by comparing the output distributions of the original model with the output distributions of the distilled model, in order to detect adversarial data, malicious data drift or data corruption. We use the following definition of harmful and harmless data points:

    has_pytorch: bool = True
    has_tensorflow: bool = True
    ClassifierDrift(self, x_ref: Union[numpy.ndarray, list], model: Union[sklearn.base.ClassifierMixin, Callable], backend: str = 'tensorflow', p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, preds_type: str = 'probs', binarize_preds: bool = False, reg_loss_fn: Callable = <function ClassifierDrift.<lambda> at 0x28fe6e9d0>, train_size: Optional[float] = 0.75, n_folds: Optional[int] = None, retrain_from_scratch: bool = True, seed: int = 0, optimizer: Optional[Callable] = None, learning_rate: float = 0.001, batch_size: int = 32, preprocess_batch_fn: Optional[Callable] = None, epochs: int = 3, verbose: int = 0, train_kwargs: Optional[dict] = None, device: Union[typing_extensions.Literal['cuda', 'gpu', 'cpu'], ForwardRef('torch.device'), NoneType] = None, dataset: Optional[Callable] = None, dataloader: Optional[Callable] = None, input_shape: Optional[tuple] = None, use_calibration: bool = False, calibration_kwargs: Optional[dict] = None, use_oob: bool = False, data_type: Optional[str] = None) -> None
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True, return_probs: bool = True, return_model: bool = True) -> Dict[str, Dict[str, Union[str, int, float, Callable]]]
    !pip install palmerpenguins
    !pip install seaborn
    from functools import partial
    import pandas as pd
    import numpy as np
    
    import matplotlib.pyplot as plt
    from matplotlib.colors import ListedColormap
    import seaborn as sns 
    
    # construct cmap
    sns.set_style('whitegrid')
    sns.set(font_scale = 1.2)
    
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.linear_model import LinearRegression
    from sklearn.model_selection import train_test_split
    from alibi_detect.cd import MMDDrift, FETDrift, CVMDrift
    
    # Set color pallette to match palmerpenguins
    mypalette = sns.color_palette(["#ff7300","#008b8b", "#c15bcb"], as_cmap=True)
    sns.set_palette(mypalette)
    my_cmap = ListedColormap(mypalette)
    from palmerpenguins import load_penguins
    data = load_penguins().dropna()
    data.head()
    data = data.drop(['island', 'flipper_length_mm', 'body_mass_g', 'sex', 'year'], axis=1)
    y = data['species']
    pairplot_figure = sns.pairplot(data, hue='species')
    pairplot_figure.fig.set_size_inches(9, 6.5)
    X = data[['bill_length_mm', 'bill_depth_mm']]
    y = data['species']
    mymap = {'Adelie':0, 'Gentoo':1, 'Chinstrap':2}
    y = y.map(mymap)
    
    X_train, X_ref, y_train, y_ref = train_test_split(X.to_numpy(), y.to_numpy(), train_size=60, random_state=42)
    X_ref, X_test, y_ref, y_test = train_test_split(X_ref, y_ref, train_size=0.5, random_state=42)
    clf = DecisionTreeClassifier(max_depth=5, random_state=42)
    clf = clf.fit(X_train, y_train)
    print('Training accuracy = %.1f %%' % (100*clf.score(X_train, y_train)))
    print('Test accuracy = %.1f %%' % (100*clf.score(X_test, y_test)))
    X_covar, y_covar = X_test.copy(), y_test.copy()
    X_concept, y_concept = X_test.copy(), y_test.copy()
    
    # Apply covariate drift by altering the bill depth of the Gentoo species
    idx1 = np.argwhere(y_test==1)
    X_covar[idx1,1] -= 5
    
    # Apply concept drift by switching two species
    idx2 = np.argwhere(y_test==2)
    y_concept[idx1] = 2
    y_concept[idx2] = 1
    
    Xs = {'No drift': X_test, 'Covariate drift': X_covar, 'Concept drift': X_concept}
    def plot_decision_boundaries(X, y, clf, ax=None, title=None):
        """
        Helper function to visualize a classifier's decision boundaries. 
        """
        if ax is None:
            f, ax = plt.subplots(figsize=(6, 6))
        ax.set_xlabel('Bill length (mm)')
        ax.set_ylabel('Bill Depth (mm)')
        
        # Plotting decision regions
        x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
        y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
        xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1))
    
        Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
    
        ax.contourf(xx, yy, Z, alpha=0.2, cmap=my_cmap)
        ax.scatter(X[:, 0], X[:, 1], c=y, s=80, edgecolor="k", cmap=my_cmap)
        
        ax.text(0.02, 0.98, 'Model accuracy = %.1f %%' % (100*clf.score(X, y)),
               ha='left', va='top', transform=ax.transAxes, fontweight='bold')
        
        if title is not None:
            ax.set_title(title)
    fig, axs = plt.subplots(2, 2, figsize=(12,12))
    plot_decision_boundaries(X_ref, y_ref, clf, ax=axs[0,0], title='Reference data')
    plot_decision_boundaries(X_test, y_test, clf, ax=axs[0,1], title='No drift')
    plot_decision_boundaries(X_covar, y_covar, clf, ax=axs[1,0], title='Covariate drift')
    plot_decision_boundaries(X_concept, y_concept, clf, ax=axs[1,1], title='Concept drift')
    plt.subplots_adjust(wspace=0.3, hspace=0.3)
    cd_mmd = MMDDrift(X_ref, p_val = 0.05)
    labels = ['No!', 'Yes!']
    for name, Xarr in Xs.items():
        print('\n%s' % name)
        np.random.seed(0)  # Set the seed used in the MMD permutation test (only for notebook reproducibility)
        preds = cd_mmd.predict(Xarr)
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val']))
    loss_ref = (clf.predict(X_ref) == y_ref).astype(int)
    loss_test = (clf.predict(X_test) == y_test).astype(int)
    loss_covar = (clf.predict(X_covar) == y_covar).astype(int)
    loss_concept = (clf.predict(X_concept) == y_concept).astype(int)
    losses = {'No drift': loss_test, 'Covariate drift': loss_covar, 'Concept drift': loss_concept}
    
    print(loss_ref)
    cd_fet = FETDrift(loss_ref, p_val=0.05, alternative='less')
    labels = ['No!', 'Yes!']
    for name, loss_arr in losses.items():
        print('\n%s' % name)
        preds = cd_fet.predict(loss_arr)
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val'][0]))
    data_r = load_penguins().dropna()
    Xr = data_r[['flipper_length_mm', 'sex']].replace({'sex': {'female': 1, 'male': 0}})
    yr = data_r['body_mass_g']
    
    _ = sns.scatterplot(data=data_r, x='flipper_length_mm', y='body_mass_g', hue='sex')
    Xr_train, Xr_ref, yr_train, yr_ref = train_test_split(Xr.to_numpy(), yr.to_numpy(), 
                                                              train_size=60, random_state=42)
    Xr_ref, Xr_test, yr_ref, yr_test = train_test_split(Xr_ref, yr_ref, train_size=0.5, random_state=42)
    reg = LinearRegression()
    reg.fit(Xr_train, yr_train)
    print('Training RMS error = %.3f' % np.sqrt(np.mean((reg.predict(Xr_train)-yr_train)**2)))
    print('Test RMS error = %.3f' % np.sqrt(np.mean((reg.predict(Xr_test)-yr_test)**2)))
    Xr_concept = Xr_test.copy()
    yr_concept = reg.predict(Xr_concept)*1.1 + np.random.normal(0, 100, size=len(yr_test))
    reg.score(Xr_concept, yr_concept)
    print('Test RMS error = %.3f' % np.sqrt(np.mean((reg.predict(Xr_concept)-yr_concept)**2)))
    lossr_ref = (reg.predict(Xr_ref) - yr_ref)**2
    lossr_test = (reg.predict(Xr_test) - yr_test)**2
    lossr_concept = (reg.predict(Xr_concept) - yr_concept)**2
    
    lossesr = {'No drift': lossr_test, 'Concept drift': lossr_concept}
    cd_cvm = CVMDrift(lossr_ref, p_val=0.05)
    labels = ['No!', 'Yes!']
    for name, loss_arr in lossesr.items():
        print('\n%s' % name)
        preds = cd_cvm.predict(loss_arr)
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val'][0]))

    Backend used for the training loop implementation. Supported: 'tensorflow'

    p_val

    float

    0.05

    p-value used for the significance of the test.

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    preds_type

    str

    'probs'

    Whether the model outputs 'probs' (probabilities - for 'tensorflow', 'pytorch', 'sklearn' models), 'logits' (for 'pytorch', 'tensorflow' models), 'scores' (for 'sklearn' models if decision_function is supported).

    binarize_preds

    bool

    False

    Whether to test for discrepancy on soft (e.g. probs/logits/scores) model predictions directly with a K-S test or binarise to 0-1 prediction errors and apply a binomial test.

    reg_loss_fn

    Callable

    <function ClassifierDrift.<lambda> at 0x28fe6e9d0>

    The regularisation term reg_loss_fn(model) is added to the loss function being optimized. Only relevant for 'tensorflow` and 'pytorch' backends.

    train_size

    Optional[float]

    0.75

    Optional fraction (float between 0 and 1) of the dataset used to train the classifier. The drift is detected on 1 - train_size. Cannot be used in combination with n_folds.

    n_folds

    Optional[int]

    None

    Optional number of stratified folds used for training. The model preds are then calculated on all the out-of-fold instances. This allows to leverage all the reference and test data for drift detection at the expense of longer computation. If both train_size and n_folds are specified, n_folds is prioritized.

    retrain_from_scratch

    bool

    True

    Whether the classifier should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    seed

    int

    0

    Optional random seed for fold selection.

    optimizer

    Optional[Callable]

    None

    Optimizer used during training of the classifier. Only relevant for 'tensorflow' and 'pytorch' backends.

    learning_rate

    float

    0.001

    Learning rate used by optimizer. Only relevant for 'tensorflow' and 'pytorch' backends.

    batch_size

    int

    32

    Batch size used during training of the classifier. Only relevant for 'tensorflow' and 'pytorch' backends.

    preprocess_batch_fn

    Optional[Callable]

    None

    Optional batch preprocessing function. For example to convert a list of objects to a batch which can be processed by the model. Only relevant for 'tensorflow' and 'pytorch' backends.

    epochs

    int

    3

    Number of training epochs for the classifier for each (optional) fold. Only relevant for 'tensorflow' and 'pytorch' backends.

    verbose

    int

    0

    Verbosity level during the training of the classifier. 0 is silent, 1 a progress bar. Only relevant for 'tensorflow' and 'pytorch' backends.

    train_kwargs

    Optional[dict]

    None

    Optional additional kwargs when fitting the classifier. Only relevant for 'tensorflow' and 'pytorch' backends.

    device

    Union[Literal[cuda, gpu, cpu], ForwardRef('torch.device'), None]

    None

    Device type used. The default tries to use the GPU and falls back on CPU if needed. Can be specified by passing either 'cuda', 'gpu', 'cpu' or an instance of torch.device. Only relevant for 'pytorch' backend.

    dataset

    Optional[Callable]

    None

    Dataset object used during training. Only relevant for 'tensorflow' and 'pytorch' backends.

    dataloader

    Optional[Callable]

    None

    Dataloader object used during training. Only relevant for 'pytorch' backend.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    use_calibration

    bool

    False

    Whether to use calibration. Calibration can be used on top of any model. Only relevant for 'sklearn' backend.

    calibration_kwargs

    Optional[dict]

    None

    Optional additional kwargs for calibration. Only relevant for 'sklearn' backend. See https://scikit-learn.org/stable/modules/generated/sklearn.calibration.CalibratedClassifierCV.html for more details.

    use_oob

    bool

    False

    Whether to use out-of-bag(OOB) predictions. Supported only for RandomForestClassifier.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    Whether to return a notion of strength of the drift. K-S test stat if binarize_preds=False, otherwise relative error reduction.

    return_probs

    bool

    True

    Whether to return the instance level classifier probabilities for the reference and test data (0=reference data, 1=test data).

    return_model

    bool

    True

    Whether to return the updated model trained to discriminate reference and test instances.

  • receptive_field_dims: height and width in pixels of the receptive field above and to the left of a given pixel.

  • implementation
    Fashion-MNIST
    Fig.2 PixelCNN
    Fig.2 PixelCNN++
    Fig.2 PixelCNN++
    Likelihood Ratio paper
    Google Cloud Bucket

    Data

    We randomly sample points from the standard normal distribution and run the detectors with PyTorch and KeOps backends for the following settings:

    • $N_\text{ref}, N_\text{test} = [2, 5, 10, 20, 50, 100]$ (batch sizes in '000s)

    • $D = [2, 10, 50]$

    Where $D$ denotes the number of features.

    Requirements

    The notebook requires PyTorch and KeOps to be installed. Once PyTorch is installed, KeOps can be installed via pip:

    Before we start let’s fix the random seeds for reproducibility:

    Vanilla PyTorch vs. KeOps comparison

    Utility functions

    First we define some utility functions to run the experiments:

    As detailed earlier, we will compare the PyTorch with the KeOps implementation of the MMD and learned kernel MMD detectors for a variety of reference and test data batch sizes as well as different feature dimensions. Note that for the PyTorch implementation, the portion of the kernel matrix for the reference data itself can already be computed at initialisation of the detector. This computation will not be included when we record the detector's prediction time. Since use cases where $N_\text{ref} >> N_\text{test}$ are quite common, we will also test for this specific setting. The key reason is that we cannot amortise this computation for the KeOps detector since we are working with lazily evaluated symbolic matrices.

    MMD detector

    1. $N_\text{ref} = N_\text{test}$

    Note that for KeOps we could further increase the number of instances in the reference and test sets (e.g. to 500,000) without running into memory issues.

    Below we visualise the runtimes of the different experiments. We can make the following observations:

    • The relative speed improvements of KeOps over vanilla PyTorch increase with increasing batch size.

    • Due to the explicit kernel computation and storage, the PyTorch detector runs out-of-memory after a little over 10,000 instances in each of the reference and test sets while KeOps keeps scaling up without any issues.

    • The relative speed improvements decline with growing feature dimension. Note however that we would not recommend using a (untrained) MMD detector on very high-dimensional data in the first place.

    The plots show both the absolute and relative (PyTorch / KeOps) mean prediction times for the MMD drift detector for different feature dimensions $[2, 10, 50]$.

    The difference between KeOps and PyTorch is even more striking when we only look at $[2, 10]$ features:

    2. $N_\text{ref} >> N_\text{test}$

    Now we check whether the speed improvements still hold when $N_\text{ref} >> N_\text{test}$ ($N_\text{ref} / N_\text{test} = 10$) and a large part of the kernel can already be computed at initialisation time of the PyTorch (but not the KeOps) detector.

    The below plots illustrate that KeOps indeed still provides large speed ups over PyTorch. The x-axis shows the reference batch size $N_\text{ref}$. Note that $N_\text{ref} / N_\text{test} = 10$.

    Learned kernel MMD detector

    We conduct similar experiments as for the MMD detector for $N_\text{ref} = N_\text{test}$ and n_features=50. We use a deep learned kernel with an MLP followed by Gaussian RBF kernels and project the input features on a d_out=2-dimensional space. Since the learned kernel detector computes the kernel matrix in a batch-wise manner, we can also scale up the number of instances for the PyTorch backend without running out-of-memory.

    We again plot the absolute and relative (PyTorch / KeOps) mean prediction times for the learned kernel MMD drift detector for different feature dimensions:

    Conclusion

    As illustrated in the experiments, KeOps allows you to drastically speed up and scale up drift detection to larger datasets without running into memory issues. The speed benefit of KeOps over the PyTorch (or TensorFlow) MMD detectors decrease as the number of features increases. Note though that it is not advised to apply the (untrained) MMD detector to very high-dimensional data in the first place and that we can apply dimensionality reduction via the deep kernel for the learned kernel MMD detector.

    MMD detector
    Gretton et al., 2012
    learned kernel MMD detector
    Liu et al., 2020
    KeOps
    Harmful data points are defined as inputs for which the model's predictions on the uncorrupted data are correct while the model's predictions on the corrupted data are wrong.
  • Harmless data points are defined as inputs for which the model's predictions on the uncorrupted data are correct and the model's predictions on the corrupted data remain correct.

  • Analogously to the adversarial AE detector, which is also part of the library, the model distillation detector picks up drift that reduces the performance of the classification model.

    Moreover, in this example a drift detector that applies two-sample Kolmogorov-Smirnov (K-S) tests to the scores is employed. The p-values obtained are used to assess the harmfulness of the data.

    Dataset

    CIFAR10 consists of 60,000 32 by 32 RGB images equally distributed over 10 classes. We evaluate the drift detector on the CIFAR-10-C dataset (Hendrycks & Dietterich, 2019). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance.

    Load data

    Original CIFAR-10 data:

    For CIFAR-10-C, we can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    We can also verify that the performance of a classification model on CIFAR-10 drops significantly on this perturbed dataset:

    Model distillation as a malicious drift detector

    Analogously to the adversarial AE detector, which uses an autoencoder to reproduce the output distribution of a classifier and produce adversarial scores, the model distillation detector achieves the same goal by using a simple classifier in place of the autoencoder. This approach is more flexible since it bypasses the instance's generation step, and it can be applied in a straightforward way to a variety of data sets such as text or time series.

    We can use the adversarial scores produced by the Model Distillation detector in the context of drift detection. The score function of the detector becomes the preprocessing function for the drift detector. The K-S test is then a simple univariate test between the adversarial scores of the reference batch and the test data. Higher adversarial scores indicate more harmful drift. Importantly, a harmfulness detector flags malicious data drift. We can fetch the pretrained model distillation detector from a Google Cloud Bucket or train one from scratch:

    Definition and training of the distilled model

    Scores and p-values calculation

    Here we initialize the K-S drift detector using the harmfulness scores as a preprocessing function. The KS test is performed on these scores.

    Initialise the drift detector:

    Calculate scores. We split the corrupted data into harmful and harmless data and visualize the harmfulness scores for various values of corruption severity.

    Plot scores

    We now plot the mean scores and standard deviations per severity level. The plot shows the mean harmfulness scores (lhs) and ResNet-32 accuracies (rhs) for increasing data corruption severity levels. Level 0 corresponds to the original test set. Harmful scores are scores from instances which have been flipped from the correct to an incorrect prediction because of the corruption. Not harmful means that a correct prediction was unchanged after the corruption.

    Plot p-values for contaminated batches

    In order to simulate a realistic scenario, we perform a K-S test on batches of instance which are increasingly contaminated with corrupted data. The following steps are implemented:

    • We randomly pick n_ref=1000 samples from the non-currupted test set to be used as a reference set in the initialization of the K-S drift detector.

    • We sample batches of data of size batch_size=100 contaminated with an increasing number of harmful corrupted data and harmless corrupted data.

    • The K-S detector predicts whether drift occurs between the contaminated batches and the reference data and returns the p-values of the test.

    • We observe that contamination of the batches with harmful data reduces the p-values much faster than contamination with harmless data. In the latter case, the p-values remain above the detection threshold even when the batch is heavily contaminated

    We repeat the test for 100 randomly sampled batches and we plot the mean and the maximum p-values for each level of severity and contamination below. We can see from the plot that the detector is able to clearly detect a batch contaminated with harmful data compared to a batch contaminated with harmless data when the percentage of currupted data reaches 20%-30%.

    Model distillation
    !pip install seaborn
    import os
    from functools import partial
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import seaborn as sns
    from sklearn.metrics import confusion_matrix
    from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
    import tensorflow as tf
    
    from alibi_detect.od import LLR
    from alibi_detect.models.tensorflow import PixelCNN
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.tensorflow import predict_batch
    from alibi_detect.utils.visualize import plot_roc
    def load_data(dataset: str) -> tuple:
        if dataset == 'mnist':
            (X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
        elif dataset == 'fashion_mnist':
            (X_train, y_train), (X_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()
        else:
            raise NotImplementedError
        X_train = X_train.astype('float32')
        X_test = X_test.astype('float32')
        y_train = y_train.astype('int64').reshape(-1,)
        y_test = y_test.astype('int64').reshape(-1,)
        if len(X_train.shape) == 3:
            shape = (-1,) + X_train.shape[1:] + (1,)
            X_train = X_train.reshape(shape)
            X_test = X_test.reshape(shape)
        return (X_train, y_train), (X_test, y_test)
    
    
    def plot_grid_img(X: np.ndarray, figsize: tuple = (10, 6)) -> None:
        n = X.shape[0]
        nrows = int(n**.5)
        ncols = int(np.ceil(n / nrows))
        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize)
        n_subplot = 1
        for r in range(nrows):
            for c in range(ncols):
                plt.subplot(nrows, ncols, n_subplot)
                plt.axis('off')
                plt.imshow(X[n_subplot-1, :, :, 0])
                n_subplot += 1
                
    
    def plot_grid_logp(idx: list, X: np.ndarray, logp_s: np.ndarray, 
                       logp_b: np.ndarray, figsize: tuple = (10, 6)) -> None:
        nrows, ncols = len(idx), 4
        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize)
        n_subplot = 1
        for r in range(nrows):
            plt.subplot(nrows, ncols, n_subplot)
            plt.imshow(X[idx[r], :, :, 0])
            plt.colorbar()
            plt.axis('off')
            if r == 0:
                plt.title('Image')
            n_subplot += 1
    
            plt.subplot(nrows, ncols, n_subplot)
            plt.imshow(logp_s[idx[r], :, :])
            plt.colorbar()
            plt.axis('off')
            if r == 0:
                plt.title('Semantic Logp')
            n_subplot += 1
    
            plt.subplot(nrows, ncols, n_subplot)
            plt.imshow(logp_b[idx[r], :, :])
            plt.colorbar()
            plt.axis('off')
            if r == 0:
                plt.title('Background Logp')
            n_subplot += 1
    
            plt.subplot(nrows, ncols, n_subplot)
            plt.imshow(logp_s[idx[r], :, :] - logp_b[idx[r], :, :])
            plt.colorbar()
            plt.axis('off')
            if r == 0:
                plt.title('LLR')
            n_subplot += 1
    (X_train_in, y_train_in), (X_test_in, y_test_in) = load_data('fashion_mnist')
    X_test_ood, y_test_ood = load_data('mnist')[1]
    input_shape = X_train_in.shape[1:]
    print(X_train_in.shape, X_test_in.shape, X_test_ood.shape)
    i = 0
    plt.imshow(X_train_in[i].reshape(input_shape[:-1]))
    plt.title('Fashion-MNIST')
    plt.axis('off')
    plt.show();
    plt.imshow(X_test_ood[i].reshape(input_shape[:-1]))
    plt.title('MNIST')
    plt.axis('off')
    plt.show();
    model = PixelCNN(
        image_shape=input_shape,
        num_resnet=5,
        num_hierarchies=2,
        num_filters=32,
        num_logistic_mix=1,
        receptive_field_dims=(3, 3),
        dropout_p=.3,
        l2_weight=0.
    )
    load_pretrained = True
    filepath = os.path.join(os.getcwd(), 'my_path')  # change to download directory
    detector_type = 'outlier'
    dataset = 'fashion_mnist'
    detector_name = 'LLR'
    filepath = os.path.join(filepath, detector_name)    
    if load_pretrained:  # load pretrained outlier detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:
        # initialize detector
        od = LLR(threshold=None, model=model)
        
        # train
        od.fit(
            X_train_in,
            mutate_fn_kwargs=dict(rate=.2),
            mutate_batch_size=1000,
            optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),
            epochs=20,
            batch_size=32,
            verbose=False
        )
        
        # save the trained outlier detector
        save_detector(od, filepath)
    kwargs = {'dist_s': model, 'dist_b': model.copy(), 'input_shape': input_shape}
    od = load_detector(filepath, **kwargs)
    n_sample = 16
    X_sample = od.dist_s.sample(n_sample).numpy()
    plot_grid_img(X_sample)
    X_sample = od.dist_b.sample(n_sample).numpy()
    plot_grid_img(X_sample)
    shape_in, shape_ood = X_test_in.shape[0], X_test_ood.shape[0]
    # semantic model
    logp_s_in = predict_batch(X_test_in, od.dist_s.log_prob, batch_size=32, shape=shape_in)
    logp_s_ood = predict_batch(X_test_ood, od.dist_s.log_prob, batch_size=32, shape=shape_ood)
    logp_s = np.concatenate([logp_s_in, logp_s_ood])
    # background model
    logp_b_in = predict_batch(X_test_in, od.dist_b.log_prob, batch_size=32, shape=shape_in)
    logp_b_ood = predict_batch(X_test_ood, od.dist_b.log_prob, batch_size=32, shape=shape_ood)
    # show histograms
    plt.hist(logp_s_in, bins=100, label='in');
    plt.hist(logp_s_ood, bins=100, label='ood');
    plt.title('Semantic Log Probabilities')
    plt.legend()
    plt.show()
    
    plt.hist(logp_b_in, bins=100, label='in');
    plt.hist(logp_b_ood, bins=100, label='ood');
    plt.title('Background Log Probabilities')
    plt.legend()
    plt.show()
    llr_in = logp_s_in - logp_b_in
    llr_ood = logp_s_ood - logp_b_ood
    plt.hist(llr_in, bins=100, label='in');
    plt.hist(llr_ood, bins=100, label='ood');
    plt.title('Likelihood Ratio')
    plt.legend()
    plt.show()
    n, frac_outlier = 500, .5
    perc_outlier = 100 * frac_outlier
    n_in, n_ood = int(n * (1 - frac_outlier)), int(n * frac_outlier)
    idx_in = np.random.choice(shape_in, size=n_in, replace=False)
    idx_ood = np.random.choice(shape_ood, size=n_ood, replace=False)
    X_threshold = np.concatenate([X_test_in[idx_in], X_test_ood[idx_ood]])
    #| scrolled: false
    od.infer_threshold(X_threshold, threshold_perc=perc_outlier, batch_size=32)
    print('New threshold: {}'.format(od.threshold))
    save_detector(od, filepath)
    X_test = np.concatenate([X_test_in, X_test_ood])
    y_test = np.concatenate([np.zeros(X_test_in.shape[0]), np.ones(X_test_ood.shape[0])])
    print(X_test.shape, y_test.shape)
    od_preds = od.predict(X_test,
                          batch_size=32,
                          outlier_type='instance',    # use 'feature' or 'instance' level
                          return_feature_score=True,  # scores used to determine outliers
                          return_instance_score=True)
    y_pred = od_preds['data']['is_outlier']
    labels = ['normal', 'outlier']
    f1 = f1_score(y_test, y_pred)
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    print('F1 score: {:.3f} -- Accuracy: {:.3f} -- Precision: {:.3f} '
          '-- Recall: {:.3f}'.format(f1, acc, prec, rec))
    cm = confusion_matrix(y_test, y_pred)
    df_cm = pd.DataFrame(cm, index=labels, columns=labels)
    sns.heatmap(df_cm, annot=True, cbar=True, linewidths=.5)
    plt.show()
    roc_data = {
        'LLR': {'scores': od_preds['data']['instance_score'], 'labels': y_test},
        'Likelihood': {'scores': -logp_s, 'labels': y_test}  # negative b/c outlier score
    }
    plot_roc(roc_data)
    n_plot = 5
    # semantic model
    logp_fn_s = partial(od.dist_s.log_prob, return_per_feature=True)
    logp_s_pixel_in = predict_batch(X_test_in[:n_plot], logp_fn_s, batch_size=32)
    logp_s_pixel_ood = predict_batch(X_test_ood[:n_plot], logp_fn_s, batch_size=32)
    
    # background model
    logp_fn_b = partial(od.dist_b.log_prob, return_per_feature=True)
    logp_b_pixel_in = predict_batch(X_test_in[:n_plot], logp_fn_b, batch_size=32)
    logp_b_pixel_ood = predict_batch(X_test_ood[:n_plot], logp_fn_b, batch_size=32)
    
    # pixel-wise likelihood ratios
    llr_pixel_in = logp_s_pixel_in - logp_b_pixel_in
    llr_pixel_ood = logp_s_pixel_ood - logp_b_pixel_ood
    idx = list(np.arange(n_plot))
    plot_grid_logp(idx, X_test_in, logp_s_pixel_in, logp_b_pixel_in, figsize=(14,14))
    idx = list(np.arange(n_plot))
    plot_grid_logp(idx, X_test_ood, logp_s_pixel_ood, logp_b_pixel_ood, figsize=(14,14))
    from alibi_detect.cd import MMDDrift
    
    detector_torch = MMDDrift(x_ref, backend='pytorch')
    detector_keops = MMDDrift(x_ref, backend='keops')
    !pip install pykeops
    import numpy as np
    import torch
    
    def set_seed(seed: int) -> None:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        np.random.seed(seed)
    
    set_seed(2022)
    from alibi_detect.cd import MMDDrift, LearnedKernelDrift
    from alibi_detect.utils.keops.kernels import DeepKernel as DeepKernelKeops
    from alibi_detect.utils.keops.kernels import GaussianRBF as GaussianRBFKeops
    from alibi_detect.utils.pytorch.kernels import DeepKernel as DeepKernelTorch
    from alibi_detect.utils.pytorch.kernels import GaussianRBF as GaussianRBFTorch
    import matplotlib.pyplot as plt
    from scipy.stats import kstest
    from timeit import default_timer as timer
    import torch.nn as nn
    import torch.nn.functional as F
    
    
    class Projection(nn.Module):
        def __init__(self, d_in: int, d_out: int = 2):
            super().__init__()
            self.lin1 = nn.Linear(d_in, d_out)
            self.lin2 = nn.Linear(d_out, d_out)
        
        def forward(self, x):
            return self.lin2(F.relu(self.lin1(x)))
        
    
    def eval_detector(p_vals: np.ndarray, threshold: float, is_drift: bool, t_mean: float, t_std: float) -> dict:
        """ In case of drifted data (ground truth) it returns the detector's power.
        In case of no drift, it computes the false positive rate (FPR) and whether the p-values
        are uniformly distributed U[0,1] which is checked via a KS test. """
        results = {'power': None, 'fpr': None, 'ks': None}
        below_p_val_threshold = (p_vals <= threshold).mean()
        if is_drift:
            results['power'] = below_p_val_threshold
        else:
            results['fpr'] = below_p_val_threshold
            stat_ks, p_val_ks = kstest(p_vals, 'uniform')
            results['ks'] = {'p_val': p_val_ks, 'stat': stat_ks}
        results['p_vals'] = p_vals
        results['time'] = {'mean': t_mean, 'stdev': t_std}
        return results
    
    
    def experiment(detector: str, backend: str, n_runs: int, n_ref: int, n_test: int, n_features: int, 
                   mu: float = 0.) -> dict:
        """ Runs the experiment n_runs times, each time with newly sampled reference and test data.
        Returns the p-values for each test as well as the mean and standard deviations of the runtimes. """
        p_vals, t_detect = [], []
        for _ in range(n_runs):
            # Sample reference and test data
            x_ref = np.random.randn(*(n_ref, n_features)).astype(np.float32)
            x_test = np.random.randn(*(n_test, n_features)).astype(np.float32) + mu
            
            # Initialise detector, make and log predictions
            p_val = .05
            dd_kwargs = dict(p_val=p_val, backend=backend, n_permutations=100)
            if detector == 'mmd':
                dd = MMDDrift(x_ref, **dd_kwargs)
            elif detector == 'learned_kernel':
                d_out, sigma = 2, .1
                proj = Projection(n_features, d_out)
                Kernel = GaussianRBFKeops if backend == 'keops' else GaussianRBFTorch
                kernel_a = Kernel(trainable=True, sigma = torch.Tensor([sigma]))
                kernel_b = Kernel(trainable=True, sigma = torch.Tensor([sigma]))
                device = torch.device('cuda')
                DeepKernel = DeepKernelKeops if backend == 'keops' else DeepKernelTorch
                deep_kernel = DeepKernel(proj, kernel_a, kernel_b, eps=.01).to(device)
                if backend == 'pytorch' and n_ref + n_test > 20000:
                    batch_size = 10000
                    batch_size_predict = 10000 
                else:
                    batch_size = 1000000
                    batch_size_predict = 1000000
                dd_kwargs.update(
                    dict(
                        epochs=2, train_size=.75, batch_size=batch_size, batch_size_predict=batch_size_predict
                    )
                )
                dd = LearnedKernelDrift(x_ref, deep_kernel, **dd_kwargs)
            start = timer()
            pred = dd.predict(x_test)
            end = timer()
            
            if _ > 0:  # first run reserved for KeOps compilation
                t_detect.append(end - start)
                p_vals.append(pred['data']['p_val'])
                
            del dd, x_ref, x_test
            torch.cuda.empty_cache()
        
        p_vals = np.array(p_vals)
        t_mean, t_std = np.array(t_detect).mean(), np.array(t_detect).std()
        results = eval_detector(p_vals, p_val, mu != 0., t_mean, t_std)
        return results
    
    
    def format_results(experiments: dict, n_features: list, backends: list, max_batch_size: int = 1e10) -> dict:
        T = {'batch_size': None, 'keops': None, 'pytorch': None}
        T['batch_size'] = np.unique([experiments['keops'][_]['n_ref'] for _ in experiments['keops'].keys()])
        T['batch_size'] = list(T['batch_size'][T['batch_size'] <= max_batch_size])
        T['keops'] = {f: [] for f in n_features}
        T['pytorch'] = {f: [] for f in n_features}
    
        for backend in backends:
            for f in T[backend].keys():
                for bs in T['batch_size']:
                    for k, v in experiments[backend].items():
                        if f == v['n_features'] and bs == v['n_ref']:
                            T[backend][f].append(results[backend][k]['time']['mean'])
    
        for k, v in T['keops'].items():  # apply padding
            n_pad = len(v) - len(T['pytorch'][k])
            T['pytorch'][k] += [np.nan for _ in range(n_pad)]
        return T
    
    
    def plot_absolute_time(experiments: dict, results: dict, n_features: list, y_scale: str = 'linear', 
                           detector: str = 'MMD', max_batch_size: int = 1e10):
        T = format_results(experiments, n_features, ['keops', 'pytorch'], max_batch_size)
        colors = ['b', 'g', 'r', 'c', 'm', 'y', 'b']
        legend, n_c = [], 0
        for f in n_features:
            plt.plot(T['batch_size'], T['keops'][f], linestyle='solid', color=colors[n_c]);
            legend.append(f'keops - {f}')
            plt.plot(T['batch_size'], T['pytorch'][f], linestyle='dashed', color=colors[n_c]);
            legend.append(f'pytorch - {f}')
            n_c += 1
        plt.title(f'{detector} drift detection time for 100 permutations')
        plt.legend(legend, loc=(1.1,.1));
        plt.xlabel('Batch size');
        plt.ylabel('Time (s)');
        plt.yscale(y_scale);
        plt.show();
    
    
    def plot_relative_time(experiments: dict, results: dict, n_features: list, y_scale: str = 'linear',
                           detector: str = 'MMD', max_batch_size: int = 1e10):
        T = format_results(experiments, n_features, ['keops', 'pytorch'], max_batch_size)
        colors = ['b', 'g', 'r', 'c', 'm', 'y', 'b']
        legend, n_c = [], 0
        for f in n_features:
            t_keops, t_torch = T['keops'][f], T['pytorch'][f]
            ratio = [tt / tk for tt, tk in zip(t_torch, t_keops)]
            plt.plot(T['batch_size'], ratio, linestyle='solid', color=colors[n_c]);
            legend.append(f'pytorch/keops - {f}')
            n_c += 1
        plt.title(f'{detector} drift detection pytorch/keops time ratio for 100 permutations')
        plt.legend(legend, loc=(1.1,.1));
        plt.xlabel('Batch size');
        plt.ylabel('time pytorch / keops');
        plt.yscale(y_scale);
        plt.show();
    experiments_eq = {
        'keops': {
            0: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 2},
            1: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 2},
            2: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 2},
            3: {'n_ref': 20000, 'n_test': 20000, 'n_runs': 5, 'n_features': 2},
            4: {'n_ref': 50000, 'n_test': 50000, 'n_runs': 5, 'n_features': 2},
            5: {'n_ref': 100000, 'n_test': 100000, 'n_runs': 5, 'n_features': 2},
            6: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 10},
            7: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 10},
            8: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 10},
            9: {'n_ref': 20000, 'n_test': 20000, 'n_runs': 5, 'n_features': 10},
            10: {'n_ref': 50000, 'n_test': 50000, 'n_runs': 5, 'n_features': 10},
            11: {'n_ref': 100000, 'n_test': 100000, 'n_runs': 5, 'n_features': 10},
            12: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 50},
            13: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 50},
            14: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 50},
            15: {'n_ref': 20000, 'n_test': 20000, 'n_runs': 5, 'n_features': 50},
            16: {'n_ref': 50000, 'n_test': 50000, 'n_runs': 5, 'n_features': 50},
            17: {'n_ref': 100000, 'n_test': 100000, 'n_runs': 5, 'n_features': 50}
        },
        'pytorch': {  # runs OOM after 10k instances in ref and test sets
            0: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 2},
            1: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 2},
            2: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 2},
            3: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 10},
            4: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 10},
            5: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 10},
            6: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 5, 'n_features': 50},
            7: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 5, 'n_features': 50},
            8: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 5, 'n_features': 50}
        }
    }
    #| scrolled: true
    backends = ['keops', 'pytorch']
    results = {backend: {} for backend in backends}
    
    for backend in backends:
        exps = experiments_eq[backend]
        for i, exp in exps.items():
            results[backend][i] = experiment(
                'mmd', backend, exp['n_runs'], exp['n_ref'], exp['n_test'], exp['n_features']
            )
    n_features = [2, 10, 50]
    max_batch_size = 100000
    
    plot_absolute_time(experiments_eq, results, n_features, max_batch_size=max_batch_size)
    plot_relative_time(experiments_eq, results, n_features, max_batch_size=max_batch_size)
    plot_absolute_time(experiments_eq, results, [2, 10], max_batch_size=max_batch_size)
    experiments_neq = {
        'keops': {
            0: {'n_ref': 2000, 'n_test': 200, 'n_runs': 10, 'n_features': 2},
            1: {'n_ref': 5000, 'n_test': 500, 'n_runs': 10, 'n_features': 2},
            2: {'n_ref': 10000, 'n_test': 1000, 'n_runs': 10, 'n_features': 2},
            3: {'n_ref': 20000, 'n_test': 2000, 'n_runs': 10, 'n_features': 2},
            4: {'n_ref': 50000, 'n_test': 5000, 'n_runs': 10, 'n_features': 2},
            5: {'n_ref': 100000, 'n_test': 10000, 'n_runs': 10, 'n_features': 2}
        },
        'pytorch': {
            0: {'n_ref': 2000, 'n_test': 200, 'n_runs': 10, 'n_features': 2},
            1: {'n_ref': 5000, 'n_test': 500, 'n_runs': 10, 'n_features': 2},
            2: {'n_ref': 10000, 'n_test': 1000, 'n_runs': 10, 'n_features': 2}
        }
    }
    results = {backend: {} for backend in backends}
    
    for backend in backends:
        exps = experiments_neq[backend]
        for i, exp in exps.items():
            results[backend][i] = experiment(
                'mmd', backend, exp['n_runs'], exp['n_ref'], exp['n_test'], exp['n_features']
            )
    plot_absolute_time(experiments_neq, results, [2], max_batch_size=max_batch_size)
    plot_relative_time(experiments_neq, results, [2], max_batch_size=max_batch_size)
    experiments_eq = {
        'keops': {
            0: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 3, 'n_features': 50},
            1: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 3, 'n_features': 50},
            2: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 3, 'n_features': 50},
            3: {'n_ref': 20000, 'n_test': 20000, 'n_runs': 3, 'n_features': 50},
            4: {'n_ref': 50000, 'n_test': 50000, 'n_runs': 3, 'n_features': 50},
            5: {'n_ref': 100000, 'n_test': 100000, 'n_runs': 3, 'n_features': 50}
        },
        'pytorch': {
            0: {'n_ref': 2000, 'n_test': 2000, 'n_runs': 3, 'n_features': 50},
            1: {'n_ref': 5000, 'n_test': 5000, 'n_runs': 3, 'n_features': 50},
            2: {'n_ref': 10000, 'n_test': 10000, 'n_runs': 3, 'n_features': 50},
            3: {'n_ref': 20000, 'n_test': 20000, 'n_runs': 3, 'n_features': 50},
            4: {'n_ref': 50000, 'n_test': 50000, 'n_runs': 3, 'n_features': 50},
            5: {'n_ref': 100000, 'n_test': 100000, 'n_runs': 3, 'n_features': 50}
        }
    }
    #| scrolled: true
    results = {backend: {} for backend in backends}
    
    for backend in backends:
        exps = experiments_eq[backend]
        for i, exp in exps.items():
            results[backend][i] = experiment(
                'learned_kernel', backend, exp['n_runs'], exp['n_ref'], exp['n_test'], exp['n_features']
            )
    max_batch_size = 100000
    
    plot_absolute_time(experiments_eq, results, [50], max_batch_size=max_batch_size)
    plot_relative_time(experiments_eq, results, [50], max_batch_size=max_batch_size)
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import os
    import tensorflow as tf
    from alibi_detect.cd import KSDrift
    from alibi_detect.ad import ModelDistillation
    
    from alibi_detect.models.tensorflow import scale_by_instance
    from alibi_detect.utils.fetching import fetch_tf_model, fetch_detector
    from alibi_detect.utils.tensorflow import predict_batch
    from alibi_detect.saving import save_detector
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_train = scale_by_instance(X_train)
    y_train = y_train.astype('int64').reshape(-1,)
    X_test = X_test.astype('float32') / 255
    y_test = y_test.astype('int64').reshape(-1,)
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    X_c = []
    n_corr = len(corruption)
    n_test = X_test.shape[0]
    for i in range(n_corr):
        X_c.append(X_corr[i * n_test:(i + 1) * n_test])
    #| tags: [hide_input]
    i = 1
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow(X_test[i])
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    dataset = 'cifar10'
    model = 'resnet32'
    clf = fetch_tf_model(dataset, model)
    acc = clf.evaluate(scale_by_instance(X_test), y_test, batch_size=128, verbose=0)[1]
    print('Test set accuracy:')
    print('Original {:.4f}'.format(acc))
    clf_accuracy = {'original': acc}
    for _ in range(len(corruption)):
        acc = clf.evaluate(scale_by_instance(X_c[_]), y_test, batch_size=128, verbose=0)[1]
        clf_accuracy[corruption[_]] = acc
        print('{} {:.4f}'.format(corruption[_], acc))
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, InputLayer
    from tensorflow.keras.regularizers import l1
    
    def distilled_model_cifar10(clf, nb_conv_layers=3, nb_filters1=256, nb_dense=40,
                                kernel1=4, kernel2=4, kernel3=4, ae_arch=False):
        print('Define distilled model')
        nb_filters1 = int(nb_filters1)
        nb_filters2 = int(nb_filters1 / 2)
        nb_filters3 = int(nb_filters1 / 4)
        layers = [InputLayer(input_shape=(32, 32, 3)),
                  Conv2D(nb_filters1, kernel1, strides=2, padding='same')]
        if nb_conv_layers > 1:
            layers.append(Conv2D(nb_filters2, kernel2, strides=2, padding='same',
                                 activation=tf.nn.relu, kernel_regularizer=l1(1e-5)))
        if nb_conv_layers > 2:
            layers.append(Conv2D(nb_filters3, kernel3, strides=2, padding='same',
                                 activation=tf.nn.relu, kernel_regularizer=l1(1e-5)))
        layers.append(Flatten())
        layers.append(Dense(nb_dense))
        layers.append(Dense(clf.output_shape[1], activation='softmax'))
        distilled_model = tf.keras.Sequential(layers)
        return distilled_model
    def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
        return (y_true == y_pred).astype(int).sum() / y_true.shape[0]
    load_pretrained = True
    filepath = 'my_path' # change to (absolute) directory where model is downloaded
    detector_type = 'adversarial'
    detector_name = 'model_distillation'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:
        distilled_model = distilled_model_cifar10(clf)
        print(distilled_model.summary())
        ad = ModelDistillation(distilled_model=distilled_model, model=clf)
        ad.fit(X_train, epochs=50, batch_size=128, verbose=True)
        save_detector(ad, filepath)
    batch_size = 100
    nb_batches = 100
    severities = [1, 2, 3, 4, 5]
    def sample_batch(x_orig, x_corr, batch_size, p):
        nb_orig = int(batch_size * (1 - p))
        nb_corr = batch_size - nb_orig
        perc = np.round(nb_corr / batch_size, 2)
        
        idx_orig = np.random.choice(range(x_orig.shape[0]), nb_orig)
        x_sample_orig = x_orig[idx_orig]    
        
        idx_corr = np.random.choice(range(x_corr.shape[0]), nb_corr)
        x_sample_corr = x_corr[idx_corr]
        
        x_batch = np.concatenate([x_sample_orig, x_sample_corr])
        return x_batch, perc
    from functools import partial
    
    np.random.seed(0)
    n_ref = 1000
    idx_ref = np.random.choice(range(X_test.shape[0]), n_ref)
    X_test = scale_by_instance(X_test)
    X_ref = X_test[idx_ref]
    labels = ['No!', 'Yes!']
    
    # adversarial score fn = preprocess step
    preprocess_fn = partial(ad.score, batch_size=128)
    
    # initialize the drift detector
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn)
    dfs = {}
    score_drift = {
        1: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        2: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        3: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        4: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        5: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
    }
    y_pred = predict_batch(X_test, clf, batch_size=256).argmax(axis=1)
    score_x = ad.score(X_test, batch_size=256)
    
    for s in severities:
        print('Loading corrupted data. Severity = {}'.format(s))
        X_corr, y_corr = fetch_cifar10c(corruption=corruptions, severity=s, return_X_y=True)
        print('Preprocess data...')
        X_corr = X_corr.astype('float32') / 255
        X_corr = scale_by_instance(X_corr)
        
        print('Make predictions on corrupted dataset...')
        y_pred_corr = predict_batch(X_corr, clf, batch_size=1000).argmax(axis=1)
        
        print('Compute adversarial scores on corrupted dataset...')
        score_corr = ad.score(X_corr, batch_size=256)
        
        labels_corr = np.zeros(score_corr.shape[0])
        repeat = y_corr.shape[0] // y_test.shape[0]
        y_pred_repeat = np.tile(y_pred, (repeat,))
        
        # malicious/harmful corruption: original prediction correct but
        # prediction on corrupted data incorrect
        idx_orig_right = np.where(y_pred_repeat == y_corr)[0]
        idx_corr_wrong = np.where(y_pred_corr != y_corr)[0]
        idx_harmful = np.intersect1d(idx_orig_right, idx_corr_wrong)
        
        # harmless corruption: original prediction correct and prediction
        # on corrupted data correct
        labels_corr[idx_harmful] = 1
        labels = np.concatenate([np.zeros(X_test.shape[0]), labels_corr]).astype(int)
        idx_corr_right = np.where(y_pred_corr == y_corr)[0]
        idx_harmless = np.intersect1d(idx_orig_right, idx_corr_right)
        
        # Split corrupted inputs in harmful and harmless
        X_corr_harm = X_corr[idx_harmful]
        X_corr_noharm = X_corr[idx_harmless]
    
        # Store adversarial scores for harmful and harmless data
        score_drift[s]['all'] = score_corr
        score_drift[s]['harm'] = score_corr[idx_harmful]
        score_drift[s]['noharm'] = score_corr[idx_harmless]
        score_drift[s]['acc'] = accuracy(y_corr, y_pred_corr)
        
        print('Compute p-values')
        for j in range(nb_batches):
            ps = []
            pvs_harm = []
            pvs_noharm = []
            for p in np.arange(0, 1, 0.1):
                # Sampling a batch of size `batch_size` where a fraction p of the data
                # is corrupted harmful data and a fraction 1 - p is non-corrupted data
                X_batch_harm, _ = sample_batch(X_test, X_corr_harm, batch_size, p)
                
                # Sampling a batch of size `batch_size` where a fraction p of the data
                # is corrupted harmless data and a fraction 1 - p is non-corrupted data
                X_batch_noharm, perc = sample_batch(X_test, X_corr_noharm, batch_size, p)
                
                # Calculating p-values for the harmful and harmless data by applying
                # K-S test on the adversarial scores
                pv_harm = cd.score(X_batch_harm)
                pv_noharm = cd.score(X_batch_noharm)
                ps.append(perc * 100)
                pvs_harm.append(pv_harm[0])
                pvs_noharm.append(pv_noharm[0])
            if j == 0:
                df = pd.DataFrame({'p': ps})
            df['pvalue_harm_{}'.format(j)] = pvs_harm
            df['pvalue_noharm_{}'.format(j)] = pvs_noharm 
    
        for name in ['pvalue_harm', 'pvalue_noharm']:
            df[name + '_mean'] = df[[col for col in df.columns if name in col]].mean(axis=1)
            df[name + '_std'] = df[[col for col in df.columns if name in col]].std(axis=1)
            df[name + '_max'] = df[[col for col in df.columns if name in col]].max(axis=1)
            df[name + '_min'] = df[[col for col in df.columns if name in col]].min(axis=1)
        df.set_index('p', inplace=True)
        dfs[s] = df
    mu_noharm, std_noharm = [], []
    mu_harm, std_harm = [], []
    acc = [clf_accuracy['original']]
    for k, v in score_drift.items():
        mu_noharm.append(v['noharm'].mean())
        std_noharm.append(v['noharm'].std())
        mu_harm.append(v['harm'].mean())
        std_harm.append(v['harm'].std())
        acc.append(v['acc'])
    plot_labels = ['0', '1', '2', '3', '4', '5']
    
    N = 6
    ind = np.arange(N)
    width = .35
    
    fig_bar_cd, ax = plt.subplots()
    ax2 = ax.twinx()
    
    p0 = ax.bar(ind[0], score_x.mean(), yerr=score_x.std(), capsize=2)
    p1 = ax.bar(ind[1:], mu_noharm, width, yerr=std_noharm, capsize=2)
    p2 = ax.bar(ind[1:] + width, mu_harm, width, yerr=std_harm, capsize=2)
    
    ax.set_title('Harmfullness Scores and Accuracy by Corruption Severity')
    ax.set_xticks(ind + width / 2)
    ax.set_xticklabels(plot_labels)
    ax.set_ylim((-2))
    ax.legend((p1[0], p2[0]), ('Not Harmful', 'Harmful'), loc='upper right', ncol=2)
    ax.set_ylabel('Score')
    ax.set_xlabel('Corruption Severity')
    
    color = 'tab:red'
    ax2.set_ylabel('Accuracy', color=color)
    ax2.plot(acc, color=color)
    ax2.tick_params(axis='y', labelcolor=color)
    
    plt.show()
    #| scrolled: false
    for s in severities:
        nrows = 1
        ncols = 2
        figsize = (15, 8)
        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize)
        title0 = ('Mean p-values for various percentages of corrupted data. \n' 
                 ' Nb of batches = {}, batch size = {}, severity = {}'.format(
                     nb_batches, batch_size, s))
        title1 = ('Maximum p-values for various  percentages of corrupted data. \n' 
                 ' Nb of batches = {}, batch size = {}, severity = {}'.format(
                     nb_batches, batch_size, s))
        dfs[s][['pvalue_harm_mean', 'pvalue_noharm_mean']].plot(ax=ax[0], title=title0)
        dfs[s][['pvalue_harm_max', 'pvalue_noharm_max']].plot(ax=ax[1], title=title1)
        for a in ax:
            a.set_xlabel('Percentage of corrupted data')
            a.set_ylabel('p-value')

    Detector Configuration Files

    For advanced use cases, Alibi Detect features powerful configuration file based functionality. As shown below, Drift detectors can be specified with a configuration file named config.toml (adversarial and outlier detectors coming soon!), which can then be passed to {func}~alibi_detect.saving.load_detector:

    import numpy as np
    from alibi_detect.cd import MMDDrift
    
    x_ref = np.load('detector_directory/x_ref.npy')
    detector = MMDDrift(x_ref, p_val=0.05)
    
    name = "MMDDrift"
    x_ref = "x_ref.npy"
    p_val = 0.05
    from alibi_detect.saving import load_detector
    filepath = 'detector_directory/'
    detector = load_detector(filepath)

    Compared to standard instantiation, config-driven instantiation has a number of advantages:

    • Human readable: The config.toml files are human-readable (and editable!), providing a readily accessible record of previously created detectors.

    • Flexible artefact specification: Artefacts such as datasets and models can be specified as locally serialized objects, or as runtime registered objects (see ). Multiple detectors can share the same artefacts, and they can be easily swapped.

    • Inbuilt validation: The {func}~alibi_detect.saving.load_detector function uses to validate detector configurations.

    To get a general idea of the expected layout of a config file, see the . Alternatively, to obtain a fully populated config file for reference, users can run one of the and generate a config file by passing an instantiated detector to {func}~alibi_detect.saving.save_detector.

    Configuration file layout

    All detector configuration files follow a consistent layout, simplifying the process of writing simple config files by hand. For example, a {class}~alibi_detect.cd.KSDrift detector with a serialized function to preprocess reference and test data can be specified as:

    The name field should always be the name of the detector, for example KSDrift or SpotTheDiffDrift. The remaining fields are the args/kwargs to pass to the detector (see the {mod}alibi_detect.cd docs for a full list of permissible args/kwargs for each detector). All config fields follow this convention, however as discussed in , some fields can be more complex than others.

    Note:In the above example config.toml, x_ref and preprocess_fn are stored in detector_directory/, but this directory isn't included in the config file. This is because in the config file, relative directories are relative to the location of the config.toml file. Filepaths may be absolute, or include nested directories, but must be POSIX paths i.e. use / path separators instead of \.

    Note:In the above example config.toml, x_ref and preprocess_fn are stored in detector_directory/, but this directory isn't incSometimes, fields representing kwargs need to be set to None. However, unspecified fields are set to a detector's default kwargs (or for , the defaults shown in the tables). To set fields as None, specify them as the string "None".

    Specifying artefacts

    When specifying a detector via a config.toml file, the locally stored reference data x_ref must be specified. In addition, many detectors also require (or allow) additional artefacts, such as kernels, functions and models. Depending on their type, artefacts can be specified in config.toml in a number of ways:

    • Local files: Simple functions and/or models can be specified as locally stored files, whilst data arrays are specified as locally stored numpy files.

    • Function/object registry: As discussed in , functions and other objects defined at runtime can be registered using {func}alibi_detect.saving.registry, allowing them to be specified in the config file without having to serialise them. For convenience a number of Alibi Detect functions such as {func}~alibi_detect.cd.tensorflow.preprocess.preprocess_drift are also pre-registered.

    • Dictionaries

    The following table shows the allowable formats for all possible config file artefacts.

    All Artefacts Table

    Artefact dictionaries

    Simple artefacts, for example a simple preprocessing function serialized in a dill file, can be specified directly: preprocess_fn = "function.dill". However, if more complex, they can be specified as an artefact dictionary:

    config.toml (excerpt)

    Here, the preprocess_fn field is a {class}~alibi_detect.saving.schemas.PreprocessConfig artefact dictionary. In this example, specifying the preprocess_fn function as a dictionary allows us to specify additional kwarg's to be passed to the function upon loading. This example also demonstrates the flexibility of the TOML format, with dictionaries able to be specified with {} brackets or by sections demarcated with [] brackets (see the for more details on the TOML format).

    Other config fields in the {ref}all-artefacts-table table can be specified via artefact dictionaries in a similar way. For example, the model and proj fields can be set as TensorFlow or PyTorch models via the {class}~alibi_detect.saving.schemas.ModelConfig dictionary. Often an artefact dictionary may itself contain nested artefact dictionaries, as is the case in in the following example, where a preprocess_fn is specified with a TensorFlow model.

    config.toml (excerpt)

    Each artefact dictionary has an associated pydantic model which is used for . The for these pydantic models provides a description of the permissible fields for each artefact dictionary. For examples of how the artefact dictionaries can be used in practice, see {ref}examples.

    Registering artefacts

    Custom artefacts defined in Python code may be specified in the config file without the need to serialise them, by first adding them to the Alibi Detect artefact registry using the {mod}alibi_detect.saving.registry submodule. This submodule harnesses the library to allow functions to be registered with a decorator syntax:

    Once the custom function has been registered, it can be specified in config.toml files via its reference string (with @ prepended), for example "@my_function.v1" in this case. Other objects, such as custom tensorflow or pytorch models, can also be registered by using the register function directly. For example, to register a tensorflow encoder model:

    Examining registered artefacts

    A registered object's metadata can be obtained with registry.find(), and all currently registered objects can be listed with registry.get_all(). For example, registry.find("my_function.v1") returns the following:

    Pre-registered utility functions/objects

    For convenience, Alibi Detect also pre-registers a number of commonly used utility functions and objects.

    Function/Class
    Registry reference*
    Tensorflow
    Pytorch

    *For backend-specific functions/classes, [backend] should be replaced the desired backend e.g. tensorflow or pytorch.

    These can be used in config.toml files. Of particular importance are the preprocess_drift utility functions, which allows models, tokenizers and embeddings to be easily specified for preprocessing, as demonstrated in the .

    (examples)=

    Example config files

    Drift detection on text data

    This example presents a configuration for the {class}~alibi_detect.cd.MMDDrift detector used in . The detector will pass the input text data through a preprocess_fn step consisting of a tokenizer, embedding and model. An model is included in order to reduce the dimensionality of the embedding space, which consists of a 768-dimensional vector for each instance. The config.toml is:

    Validating config files

    When {func}~alibi_detect.saving.load_detector is called, the {func}~alibi_detect.saving.validate_config utility function is used internally to validate the given detector configuration. This allows any problems with the configuration to be detected prior to sometimes time-consuming operations of loading artefacts and instantiating the detector. {func}~alibi_detect.saving.validate_config can also be used by devs working with Alibi Detect config dictionaries.

    Under-the-hood, {func}~alibi_detect.saving.load_detector parses the config.toml file into a unresolved config dictionary. It then passes this dict through {func}~alibi_detect.saving.validate_config, to check for errors such as incorrectly named fields, and incorrect types. If working directly with config dictionaries, the same process can be done explicitly, for example:

    This will return a ValidationError because p_val is expected to be float not a list, and bad_field isn't a recognised field for the MMDDrift detector:

    Validating at this stage is useful as errors can be caught before the sometimes time-consuming operation of resolving the config dictionary, which involves loading each artefact in the dictionary ({func}~alibi_detect.saving.read_config and {func}~alibi_detect.saving.resolve_config can be used to manually read and resolve a config for debugging). The resolved config dictionary is then also passed through {func}~alibi_detect.saving.validate_config, and this second validation can also be done explicitly:

    Note that since resolved=True, {func}~alibi_detect.saving.validate_config is now expecting x_ref to be a Numpy ndarray instead of a string. This second level of validation can be useful as it helps detect problems with loaded artefacts before attempting the sometimes time-consuming operation of instantiating the detector.

    alibi_detect.ad.adversarialae

    Constants

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    AdversarialAE

    Inherits from: BaseDetector, FitMixin, ThresholdMixin, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    correct

    Correct adversarial instances if the adversarial score is above the threshold.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]

    fit

    Train Adversarial AE model.

    Name
    Type
    Default
    Description

    Returns

    • Type: None

    infer_threshold

    Update threshold by a value inferred from the percentage of instances considered to be

    adversarial in a sample of the dataset.

    Name
    Type
    Default
    Description

    Returns

    • Type: None

    predict

    Predict whether instances are adversarial instances or not.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]

    score

    Compute adversarial scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]

    DenseHidden

    Inherits from: Model, TensorFlowTrainer, Trainer, Layer, TFLayer, KerasAutoTrackable, AutoTrackable, Trackable, Operation, KerasSaveable

    Constructor

    Name
    Type
    Default
    Description

    Methods

    call

    Name
    Type
    Default
    Description

    Returns

    • Type: tensorflow.python.framework.tensor.Tensor

    logger: logging.Logger = <Logger alibi_detect.ad.adversarialae (WARNING)>
    : More complex artefacts are specified via nested dictionaries, usually containing a
    src
    field and additional option/setting fields. Sometimes these fields may be nested artefact dictionaries themselves. See
    for further details.

    ✔

    ✔

    dataset

    ✔

    ✔

    initial_diffs

    ✔

    model / proj

    ✔

    alibi_detect.saving.schemas.ModelConfig

    preprocess_fn

    ✔

    ✔

    alibi_detect.saving.schemas.PreprocessConfig

    preprocess_batch_fn

    ✔

    ✔

    embedding

    ✔

    alibi_detect.saving.schemas.EmbeddingConfig

    tokenizer

    ✔

    alibi_detect.saving.schemas.TokenizerConfig

    kernel

    ✔

    alibi_detect.saving.schemas.KernelConfig or alibi_detect.saving.schemas.DeepKernelConfig

    kernel_a / kernel_b

    ✔

    alibi_detect.saving.schemas.KernelConfig

    optimizer

    ✔

    ✔

    alibi_detect.saving.schemas.OptimizerConfig

    Field

    .npy file

    .dill file

    Registry

    Artefact Dictionary

    x_ref

    ✔

    c_ref

    ✔

    reg_loss_fn

    {func}~alibi_detect.cd.tensorflow.preprocess.preprocess_drift

    '@cd.[backend].preprocess.preprocess_drift'

    ✔

    ✔

    {class}~alibi_detect.utils.tensorflow.kernels.GaussianRBF

    '@utils.[backend].kernels.GaussianRBF'

    ✔

    ✔

    {class}~alibi_detect.utils.tensorflow.data.TFDataset

    '@utils.tensorflow.data.TFDataset'

    ✔

    Specifying complex fields
    pydantic
    Example config files
    example notebooks
    dill
    Specifying artefacts
    Artefact dictionaries
    dill
    npy
    Registering artefacts
    TOML documentation
    validation of config files
    documentation
    catalogue
    IMDB example
    Text drift detection on IMDB movie reviews
    Untrained AutoEncoder (UAE)
    name = "KSDrift"
    x_ref = "x_ref.npy"
    p_val = 0.05
    preprocess_fn = "function.dill"
    from alibi_detect.saving import load_detector
    detector = load_detector('detector_directory/')
    import numpy as np
    from alibi_detect.cd import KSDrift
    
    x_ref = np.load('detector_directory/x_ref.npy')
    preprocess_fn = dill.load('detector_directory/function.dill')
    detector = MMDDrift(x_ref, p_val=0.05, preprocess_fn=preprocess_fn)

    Artefact dictionaries

    encoder_net

    Optional[keras.src.models.model.Model]

    None

    Layers for the encoder wrapped in a tf.keras.Sequential class if no 'ae' is specified.

    decoder_net

    Optional[keras.src.models.model.Model]

    None

    Layers for the decoder wrapped in a tf.keras.Sequential class if no 'ae' is specified.

    model_hl

    Optional[List[keras.src.models.model.Model]]

    None

    List with tf.keras models for the hidden layer K-L divergence computation.

    hidden_layer_kld

    Optional[dict]

    None

    Dictionary with as keys the hidden layer(s) of the model which are extracted and used during training of the AE, and as values the output dimension for the hidden layer.

    w_model_hl

    Optional[list]

    None

    Weights assigned to the loss of each model in model_hl.

    temperature

    float

    1.0

    Temperature used for model prediction scaling. Temperature <1 sharpens the prediction probability distribution.

    data_type

    Optional[str]

    None

    Optionally specifiy the data type (tabular, image or time-series). Added to metadata.

    return_all_predictions

    bool

    True

    Whether to return the predictions on the original and the reconstructed data.

    w_recon

    float

    0.0

    Weight on MSE reconstruction error loss term.

    optimizer

    Union[ForwardRef('tf.keras.optimizers.Optimizer'), ForwardRef('tf.keras.optimizers.legacy.Optimizer'), type[ForwardRef('tf.keras.optimizers.Optimizer')], type[ForwardRef('tf.keras.optimizers.legacy.Optimizer')]]

    <class 'keras.src.optimizers.adam.Adam'>

    Optimizer used for training.

    epochs

    int

    20

    Number of training epochs.

    batch_size

    int

    128

    Batch size used for training.

    verbose

    bool

    True

    Whether to print training progress.

    log_metric

    Tuple[str, ForwardRef('tf.keras.metrics')]

    None

    Additional metrics whose progress will be displayed if verbose equals True.

    callbacks

    .tensorflow.keras.callbacks

    None

    Callbacks used during training.

    preprocess_fn

    Callable

    None

    Preprocessing function applied to each training batch.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    hidden_dim

    Optional[int]

    None

    Dimension of optional additional dense layer.

    threshold

    Optional[float]

    None

    Threshold used for adversarial score to determine adversarial instances.

    ae

    Optional[keras.src.models.model.Model]

    None

    A trained tf.keras autoencoder model if available.

    model

    Optional[keras.src.models.model.Model]

    None

    X

    numpy.ndarray

    Batch of instances.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    return_instance_score

    bool

    True

    X

    numpy.ndarray

    Training batch.

    loss_fn

    .tensorflow.keras.losses

    <function loss_adv_ae at 0x28ee8c430>

    Loss function used for training.

    w_model

    float

    1.0

    X

    numpy.ndarray

    Batch of instances.

    threshold_perc

    float

    99.0

    Percentage of X considered to be normal based on the adversarial score.

    margin

    float

    0.0

    X

    numpy.ndarray

    Batch of instances.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    return_instance_score

    bool

    True

    X

    numpy.ndarray

    Batch of instances to analyze.

    batch_size

    int

    10000000000

    Batch size used when computing scores.

    return_predictions

    bool

    False

    model

    keras.src.models.model.Model

    tf.keras classification model.

    hidden_layer

    int

    Hidden layer from model where feature map is extracted from.

    output_dim

    int

    x

    tensorflow.python.framework.tensor.Tensor

    A trained tf.keras classification model.

    Whether to return instance level adversarial scores.

    Weight on model prediction loss term.

    Add margin to threshold. Useful if adversarial instances have significantly higher scores and there is no adversarial instance in X.

    Whether to return instance level adversarial scores.

    Whether to return the predictions of the classifier on the original and reconstructed instances.

    Output dimension for softmax layer.

    [preprocess_fn]
    src = "function.dill"
    kwargs = {'kwarg1'=42, 'kwarg2'=false}
    [preprocess_fn]
    src = "@cd.tensorflow.preprocess.preprocess_drift"
    batch_size = 32
    
    [preprocess_fn.model]
    src = "model/"
    import numpy as np
    from alibi_detect.saving import registry, load_detector
    
    # Register a simple function
    @registry.register('my_function.v1')
    def my_function(x: np.ndarray) -> np.ndarray:
        "A custom function to normalise input data."
        return (x - x.mean()) / x.std()
    
    # Load detector with config.toml file referencing "@my_function.v1"    
    detector = load_detector(filepath)
    name = "MMDDrift"
    x_ref = "x_ref.npy"
    preprocess_fn = "@my_function.v1"
    import tensorflow as tf
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, InputLayer
    from alibi_detect.saving import registry
    
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(32,)
      ]
    )
    registry.register("my_encoder.v1", func=encoder_net)
    {'module': '__main__', 'file': 'test.py', 'line_no': 3, 'docstring': 'A custom function to normalise input data.'}
    x_ref = "x_ref.npy"
    name = "MMDDrift"
    
    [preprocess_fn]
    src = "@cd.tensorflow.preprocess.preprocess_drift"
    batch_size = 32
    max_len = 100
    tokenizer.src = "tokenizer/"
    
    [preprocess_fn.model]
    src = "model/"
    
    [preprocess_fn.embedding]
    src = "embedding/"
    type = "hidden_state"
    layers = [-1, -2, -3, -4, -5, -6, -7, -8]
    from alibi_detect.saving import validate_config
    
    # Define a simple config dict
    cfg = {
        'name': 'MMDDrift',
        'x_ref': 'x_ref.npy',
        'p_val': [0.05],
        'bad_field': 'oops!'
    }
    
    # Validate the config
    validate_config(cfg)
    ValidationError: 2 validation errors for MMDDriftConfig
    p_val
      value is not a valid float (type=type_error.float)
    bad_field
      extra fields not permitted (type=value_error.extra)
    import numpy as np
    from alibi_detect.saving import validate_config
    
    # Create some reference data
    x_ref = np.random.normal(size=(100,5))
    
    # Define a simple config dict
    cfg = {
        'name': 'MMDDrift',
        'x_ref': x_ref,
        'p_val': 0.05
    }
    
    # Validate the config
    validate_config(cfg, resolved=True)
    AdversarialAE(self, threshold: float = None, ae: keras.src.models.model.Model = None, model: keras.src.models.model.Model = None, encoder_net: keras.src.models.model.Model = None, decoder_net: keras.src.models.model.Model = None, model_hl: List[keras.src.models.model.Model] = None, hidden_layer_kld: dict = None, w_model_hl: list = None, temperature: float = 1.0, data_type: str = None) -> None
    correct(X: numpy.ndarray, batch_size: int = 10000000000, return_instance_score: bool = True, return_all_predictions: bool = True) -> Dict[Dict[str, str], Dict[str, numpy.ndarray]]
    fit(X: numpy.ndarray, loss_fn: .tensorflow.keras.losses = <function loss_adv_ae at 0x28ee8c430>, w_model: float = 1.0, w_recon: float = 0.0, optimizer: Union[ForwardRef('tf.keras.optimizers.Optimizer'), ForwardRef('tf.keras.optimizers.legacy.Optimizer'), type[ForwardRef('tf.keras.optimizers.Optimizer')], type[ForwardRef('tf.keras.optimizers.legacy.Optimizer')]] = <class 'keras.src.optimizers.adam.Adam'>, epochs: int = 20, batch_size: int = 128, verbose: bool = True, log_metric: Tuple[str, ForwardRef('tf.keras.metrics')] = None, callbacks: .tensorflow.keras.callbacks = None, preprocess_fn: Callable = None) -> None
    infer_threshold(X: numpy.ndarray, threshold_perc: float = 99.0, margin: float = 0.0, batch_size: int = 10000000000) -> None
    predict(X: numpy.ndarray, batch_size: int = 10000000000, return_instance_score: bool = True) -> Dict[Dict[str, str], Dict[str, numpy.ndarray]]
    score(X: numpy.ndarray, batch_size: int = 10000000000, return_predictions: bool = False) -> Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]
    DenseHidden(self, model: keras.src.models.model.Model, hidden_layer: int, output_dim: int, hidden_dim: int = None) -> None
    call(x: tensorflow.python.framework.tensor.Tensor) -> tensorflow.python.framework.tensor.Tensor

    Text drift detection on IMDB movie reviews

    Method

    We detect drift on text data using both the Maximum Mean Discrepancy and Kolmogorov-Smirnov (K-S) detectors. In this example notebook we will focus on detecting covariate shift $\Delta p(x)$ as detecting predicted label distribution drift does not differ from other modalities (check K-S and MMD drift on CIFAR-10).

    It becomes however a little bit more involved when we want to pick up input data drift $\Delta p(x)$. When we deal with tabular or image data, we can either directly apply the two sample hypothesis test on the input or do the test after a preprocessing step with for instance a randomly initialized encoder as proposed in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift (they call it an Untrained AutoEncoder or UAE). It is not as straightforward when dealing with text, both in string or tokenized format as they don't directly represent the semantics of the input.

    As a result, we extract (contextual) embeddings for the text and detect drift on those. This procedure has a significant impact on the type of drift we detect. Strictly speaking we are not detecting $\Delta p(x)$ anymore since the whole training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract.

    The library contains functionality to leverage pre-trained embeddings from but also allows you to easily use your own embeddings of choice. Both options are illustrated with examples in this notebook.

    Note

    As is done in this example, it is recommended to pass text data to detectors as a list of strings (List[str]). This allows for seamless integration with HuggingFace's transformers library.

    One exception to the above is when custom embeddings are used. Here, it is important to ensure that the data is passed to the custom embedding model in a compatible format. In , a preprocess_batch_fn is defined in order to convert list's to the np.ndarray's expected by the custom TensorFlow embedding.

    Backend

    The method works with both the PyTorch and TensorFlow frameworks for the statistical tests and preprocessing steps. Alibi Detect does however not install PyTorch for you. Check the how to do this.

    Dataset

    Binary sentiment classification containing $25,000$ movie reviews for training and $25,000$ for testing. Install the nlp library to fetch the dataset:

    Load tokenizer

    Load data

    Let's take a look at respectively a negative and positive review:

    We split the original test set in a reference dataset and a dataset which should not be rejected under the H0 of the statistical test. We also create imbalanced datasets and inject selected words in the reference set.

    Reference, H0 and imbalanced data:

    Inject words in reference data:

    Preprocessing

    First we need to specify the type of embedding we want to extract from the BERT model. We can extract embeddings from the ...

    • pooler_output: Last layer hidden-state of the first token of the sequence (classification token; CLS) further processed by a Linear layer and a Tanh activation function. The Linear layer weights are trained from the next sentence prediction (classification) objective during pre-training. Note: this output is usually not a good summary of the semantic content of the input, you’re often better with averaging or pooling the sequence of hidden-states for the whole input sequence.

    • last_hidden_state: Sequence of hidden states at the output of the last layer of the model, averaged over the tokens.

    • hidden_state: Hidden states of the model at the output of each layer, averaged over the tokens.

    If hidden_state or hidden_state_cls is used as embedding type, you also need to pass the layer numbers used to extract the embedding from. As an example we extract embeddings from the last 8 hidden states.

    Let's check what an embedding looks like:

    So the BERT model's embedding space used by the drift detector consists of a $768$-dimensional vector for each instance. We will therefore first apply a dimensionality reduction step with an Untrained AutoEncoder (UAE) before conducting the statistical hypothesis test. We use the embedding model as the input for the UAE which then projects the embedding on a lower dimensional space.

    Let's test this again:

    K-S detector

    Initialize

    We proceed to initialize the drift detector. From here on the detector works the same as for other modalities such as images. Please check the example or the for more information about each of the possible parameters.

    Detect drift

    Let’s first check if drift occurs on a similar sample from the training set as the reference data.

    Detect drift on imbalanced and perturbed datasets:

    MMD TensorFlow detector

    Initialize

    Again check the example or the for more information about each of the possible parameters.

    Detect drift

    H0:

    Imbalanced data:

    Perturbed data:

    MMD PyTorch detector

    Initialize

    We can run the same detector with PyTorch backend for both the preprocessing step and MMD implementation:

    Detect drift

    H0:

    Imbalanced data:

    Perturbed data:

    Train embeddings from scratch

    So far we used pre-trained embeddings from a BERT model. We can however also use embeddings from a model trained from scratch. First we define and train a simple classification model consisting of an embedding and LSTM layer in TensorFlow.

    Load data and train model

    Load and tokenize data:

    Let's check out an instance:

    Define and train a simple model:

    Extract the embedding layer from the trained model and combine with UAE preprocessing step:

    Again, create reference, H0 and perturbed datasets. Also test against the Reuters news topic classification dataset.

    Initialize detector and detect drift

    H0:

    Perturbed data:

    The detector is not as sensitive as the Transformer-based K-S drift detector. The embeddings trained from scratch only trained on a small dataset and a simple model with cross-entropy loss function for 2 epochs. The pre-trained BERT model on the other hand captures semantics of the data better.

    Sample from the Reuters dataset:

    hidden_state_cls: See hidden_state but use the CLS token output.

    HuggingFace's transformer package
    the final example
    PyTorch docs
    dataset
    images
    K-S detector documentation
    images
    MMD detector documentation
    !pip install nlp
    import nlp
    import numpy as np
    import os
    import tensorflow as tf
    from transformers import AutoTokenizer
    from alibi_detect.cd import KSDrift, MMDDrift
    from alibi_detect.saving import save_detector, load_detector
    #| scrolled: true
    model_name = 'bert-base-cased'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    def load_dataset(dataset: str, split: str = 'test'):
        data = nlp.load_dataset(dataset)
        X, y = [], []
        for x in data[split]:
            X.append(x['text'])
            y.append(x['label'])
        X = np.array(X)
        y = np.array(y)
        return X, y
    #| colab: {base_uri: 'https://localhost:8080/'}
    #| scrolled: true
    X, y = load_dataset('imdb', split='train')
    print(X.shape, y.shape)
    #| colab: {base_uri: 'https://localhost:8080/'}
    labels = ['Negative', 'Positive']
    print(labels[y[-1]])
    print(X[-1])
    #| colab: {base_uri: 'https://localhost:8080/'}
    print(labels[y[2]])
    print(X[2])
    def random_sample(X: np.ndarray, y: np.ndarray, proba_zero: float, n: int):
        if len(y.shape) == 1:
            idx_0 = np.where(y == 0)[0]
            idx_1 = np.where(y == 1)[0]
        else:
            idx_0 = np.where(y[:, 0] == 1)[0]
            idx_1 = np.where(y[:, 1] == 1)[0]
        n_0, n_1 = int(n * proba_zero), int(n * (1 - proba_zero))
        idx_0_out = np.random.choice(idx_0, n_0, replace=False)
        idx_1_out = np.random.choice(idx_1, n_1, replace=False)
        X_out = np.concatenate([X[idx_0_out], X[idx_1_out]])
        y_out = np.concatenate([y[idx_0_out], y[idx_1_out]])
        return X_out.tolist(), y_out.tolist()
    
    
    def padding_last(x: np.ndarray, seq_len: int) -> np.ndarray:
        try:  # try not to replace padding token
            last_token = np.where(x == 0)[0][0]
        except:  # no padding
            last_token = seq_len - 1
        return 1, last_token
    
    
    def padding_first(x: np.ndarray, seq_len: int) -> np.ndarray:
        try:  # try not to replace padding token
            first_token = np.where(x == 0)[0][-1] + 2
        except:  # no padding
            first_token = 0
        return first_token, seq_len - 1
    
    
    def inject_word(token: int, X: np.ndarray, perc_chg: float, padding: str = 'last'):
        seq_len = X.shape[1]
        n_chg = int(perc_chg * .01 * seq_len)
        X_cp = X.copy()
        for _ in range(X.shape[0]):
            if padding == 'last':
                first_token, last_token = padding_last(X_cp[_, :], seq_len)
            else:
                first_token, last_token = padding_first(X_cp[_, :], seq_len)
            if last_token <= n_chg:
                choice_len = seq_len
            else:
                choice_len = last_token
            idx = np.random.choice(np.arange(first_token, choice_len), n_chg, replace=False)
            X_cp[_, idx] = token
        return X_cp.tolist()
    # proba_zero = fraction with label 0 (=negative sentiment)
    n_sample = 1000
    X_ref = random_sample(X, y, proba_zero=.5, n=n_sample)[0]
    X_h0 = random_sample(X, y, proba_zero=.5, n=n_sample)[0]
    n_imb = [.1, .9]
    X_imb = {_: random_sample(X, y, proba_zero=_, n=n_sample)[0] for _ in n_imb}
    words = ['fantastic', 'good', 'bad', 'horrible']
    perc_chg = [1., 5.]  # % of tokens to change in an instance
    
    words_tf = tokenizer(words)['input_ids']
    words_tf = [token[1:-1][0] for token in words_tf]
    max_len = 100
    tokens = tokenizer(X_ref, pad_to_max_length=True, 
                       max_length=max_len, return_tensors='tf')
    X_word = {}
    for i, w in enumerate(words_tf):
        X_word[words[i]] = {}
        for p in perc_chg:
            x = inject_word(w, tokens['input_ids'].numpy(), p)
            dec = tokenizer.batch_decode(x, **dict(skip_special_tokens=True))
            X_word[words[i]][p] = dec
    #| colab: {base_uri: 'https://localhost:8080/'}
    tokens['input_ids']
    #| scrolled: true
    from alibi_detect.models.tensorflow import TransformerEmbedding
    
    emb_type = 'hidden_state'
    n_layers = 8
    layers = [-_ for _ in range(1, n_layers + 1)]
    
    embedding = TransformerEmbedding(model_name, emb_type, layers)
    #| scrolled: false
    tokens = tokenizer(list(X[:5]), pad_to_max_length=True, 
                       max_length=max_len, return_tensors='tf')
    x_emb = embedding(tokens)
    print(x_emb.shape)
    tf.random.set_seed(0)
    from alibi_detect.cd.tensorflow import UAE
    
    enc_dim = 32
    shape = (x_emb.shape[1],)
    
    uae = UAE(input_layer=embedding, shape=shape, enc_dim=enc_dim)
    #| colab: {base_uri: 'https://localhost:8080/'}
    emb_uae = uae(tokens)
    print(emb_uae.shape)
    #| scrolled: true
    from functools import partial
    from alibi_detect.cd.tensorflow import preprocess_drift
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=uae, tokenizer=tokenizer, 
                            max_len=max_len, batch_size=32)
    
    # initialize detector
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn, input_shape=(max_len,))
    
    # we can also save/load an initialised detector
    filepath = 'my_path'  # change to directory where detector is saved
    save_detector(cd, filepath)
    cd = load_detector(filepath)
    #| colab: {base_uri: 'https://localhost:8080/'}
    preds_h0 = cd.predict(X_h0)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    print('p-value: {}'.format(preds_h0['data']['p_val']))
    #| colab: {base_uri: 'https://localhost:8080/'}
    for k, v in X_imb.items():
        preds = cd.predict(v)
        print('% negative sentiment {}'.format(k * 100))
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val']))
        print('')
    #| colab: {base_uri: 'https://localhost:8080/'}
    #| scrolled: false
    for w, probas in X_word.items():
        for p, v in probas.items():
            preds = cd.predict(v)
            print('Word: {} -- % perturbed: {}'.format(w, p))
            print('Drift? {}'.format(labels[preds['data']['is_drift']]))
            print('p-value: {}'.format(preds['data']['p_val']))
            print('')
    cd = MMDDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn, 
                  n_permutations=100, input_shape=(max_len,))
    #| colab: {base_uri: 'https://localhost:8080/'}
    preds_h0 = cd.predict(X_h0)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    print('p-value: {}'.format(preds_h0['data']['p_val']))
    #| colab: {base_uri: 'https://localhost:8080/'}
    for k, v in X_imb.items():
        preds = cd.predict(v)
        print('% negative sentiment {}'.format(k * 100))
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val']))
        print('')
    #| colab: {base_uri: 'https://localhost:8080/'}
    #| scrolled: false
    for w, probas in X_word.items():
        for p, v in probas.items():
            preds = cd.predict(v)
            print('Word: {} -- % perturbed: {}'.format(w, p))
            print('Drift? {}'.format(labels[preds['data']['is_drift']]))
            print('p-value: {}'.format(preds['data']['p_val']))
            print('')
    #| colab: {base_uri: 'https://localhost:8080/'}
    import torch
    import torch.nn as nn
    
    # set random seed and device
    seed = 0
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(device)
    #| scrolled: true
    from alibi_detect.cd.pytorch import preprocess_drift
    from alibi_detect.models.pytorch import TransformerEmbedding
    from alibi_detect.cd.pytorch import UAE
    
    # Embedding model
    embedding_pt = TransformerEmbedding(model_name, emb_type, layers)
    
    # PyTorch untrained autoencoder
    uae = UAE(input_layer=embedding_pt, shape=shape, enc_dim=enc_dim)
    model = uae.to(device).eval()
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=model, tokenizer=tokenizer, 
                            max_len=max_len, batch_size=32, device=device)
    
    # initialise drift detector
    cd = MMDDrift(X_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn, 
                  n_permutations=100, input_shape=(max_len,))
    #| colab: {base_uri: 'https://localhost:8080/'}
    preds_h0 = cd.predict(X_h0)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    print('p-value: {}'.format(preds_h0['data']['p_val']))
    #| colab: {base_uri: 'https://localhost:8080/'}
    for k, v in X_imb.items():
        preds = cd.predict(v)
        print('% negative sentiment {}'.format(k * 100))
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('p-value: {}'.format(preds['data']['p_val']))
        print('')
    #| colab: {base_uri: 'https://localhost:8080/'}
    for w, probas in X_word.items():
        for p, v in probas.items():
            preds = cd.predict(v)
            print('Word: {} -- % perturbed: {}'.format(w, p))
            print('Drift? {}'.format(labels[preds['data']['is_drift']]))
            print('p-value: {}'.format(preds['data']['p_val']))
            print('')
    from tensorflow.keras.datasets import imdb, reuters
    from tensorflow.keras.layers import Dense, Embedding, Input, LSTM
    from tensorflow.keras.preprocessing import sequence
    from tensorflow.keras.utils import to_categorical
    
    INDEX_FROM = 3
    NUM_WORDS = 10000
    
    
    def print_sentence(tokenized_sentence: str, id2w: dict):
        print(' '.join(id2w[_] for _ in tokenized_sentence))
        print('')
        print(tokenized_sentence)
    
    
    def mapping_word_id(data):
        w2id = data.get_word_index()
        w2id = {k: (v + INDEX_FROM) for k, v in w2id.items()}
        w2id["<PAD>"] = 0
        w2id["<START>"] = 1
        w2id["<UNK>"] = 2
        w2id["<UNUSED>"] = 3
        id2w = {v: k for k, v in w2id.items()}
        return w2id, id2w
    
    
    def get_dataset(dataset: str = 'imdb', max_len: int = 100):
        if dataset == 'imdb':
            data = imdb
        elif dataset == 'reuters':
            data = reuters
        else:
            raise NotImplementedError
    
        w2id, id2w = mapping_word_id(data)
    
        (X_train, y_train), (X_test, y_test) = data.load_data(
            num_words=NUM_WORDS, index_from=INDEX_FROM)
        X_train = sequence.pad_sequences(X_train, maxlen=max_len)
        X_test = sequence.pad_sequences(X_test, maxlen=max_len)
        y_train, y_test = to_categorical(y_train), to_categorical(y_test)
    
        return (X_train, y_train), (X_test, y_test), (w2id, id2w)
    
    
    def imdb_model(X: np.ndarray, num_words: int = 100, emb_dim: int = 128,
                   lstm_dim: int = 128, output_dim: int = 2) -> tf.keras.Model:
        X = np.array(X)
        inputs = Input(shape=(X.shape[1:]), dtype=tf.float32)
        x = Embedding(num_words, emb_dim)(inputs)
        x = LSTM(lstm_dim, dropout=.5)(x)
        outputs = Dense(output_dim, activation=tf.nn.softmax)(x)
        model = tf.keras.Model(inputs=inputs, outputs=outputs)
        model.compile(
            loss='categorical_crossentropy',
            optimizer='adam',
            metrics=['accuracy']
        )
        return model
    #| scrolled: false
    (X_train, y_train), (X_test, y_test), (word2token, token2word) = \
        get_dataset(dataset='imdb', max_len=max_len)
    #| colab: {base_uri: 'https://localhost:8080/'}
    print_sentence(X_train[0], token2word)
    #| colab: {base_uri: 'https://localhost:8080/'}
    model = imdb_model(X=X_train, num_words=NUM_WORDS, emb_dim=256, lstm_dim=128, output_dim=2)
    model.fit(X_train, y_train, batch_size=32, epochs=2, 
              shuffle=True, validation_data=(X_test, y_test))
    #| colab: {base_uri: 'https://localhost:8080/'}
    embedding = tf.keras.Model(inputs=model.inputs, outputs=model.layers[1].output)
    x_emb = embedding(X_train[:5])
    print(x_emb.shape)
    tf.random.set_seed(0)
    
    shape = tuple(x_emb.shape[1:])
    uae = UAE(input_layer=embedding, shape=shape, enc_dim=enc_dim)
    X_ref, y_ref = random_sample(X_test, y_test, proba_zero=.5, n=n_sample)
    X_h0, y_h0 = random_sample(X_test, y_test, proba_zero=.5, n=n_sample)
    tokens = [word2token[w] for w in words]
    X_word = {}
    for i, t in enumerate(tokens):
        X_word[words[i]] = {}
        for p in perc_chg:
            X_word[words[i]][p] = inject_word(t, np.array(X_ref), p, padding='first')
    #| scrolled: true
    # load and tokenize Reuters dataset
    (X_reut, y_reut), (w2t_reut, t2w_reut) = \
        get_dataset(dataset='reuters', max_len=max_len)[1:]
    
    # sample random instances
    idx = np.random.choice(X_reut.shape[0], n_sample, replace=False)
    X_ood = X_reut[idx]
    #| colab: {base_uri: 'https://localhost:8080/'}
    from alibi_detect.cd.tensorflow import preprocess_drift
    
    # define preprocess_batch_fn to convert list of str's to np.ndarray to be processed by `model`
    def convert_list(X: list):
        return np.array(X)
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=uae, batch_size=128, preprocess_batch_fn=convert_list)
    
    # initialize detector
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn)
    #| colab: {base_uri: 'https://localhost:8080/'}
    preds_h0 = cd.predict(X_h0)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    print('p-value: {}'.format(preds_h0['data']['p_val']))
    #| colab: {base_uri: 'https://localhost:8080/'}
    #| scrolled: false
    for w, probas in X_word.items():
        for p, v in probas.items():
            preds = cd.predict(v)
            print('Word: {} -- % perturbed: {}'.format(w, p))
            print('Drift? {}'.format(labels[preds['data']['is_drift']]))
            print('p-value: {}'.format(preds['data']['p_val']))
            print('')
    #| colab: {base_uri: 'https://localhost:8080/'}
    preds_ood = cd.predict(X_ood)
    labels = ['No!', 'Yes!']
    print('Drift? {}'.format(labels[preds_ood['data']['is_drift']]))
    print('p-value: {}'.format(preds_ood['data']['p_val']))

    Kolmogorov-Smirnov data drift detector on CIFAR-10

    Method

    The drift detector applies feature-wise two-sample Kolmogorov-Smirnov (K-S) tests. For multivariate data, the obtained p-values for each feature are aggregated either via the Bonferroni or the False Discovery Rate (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur.

    For high-dimensional data, we typically want to reduce the dimensionality before computing the feature-wise univariate K-S tests and aggregating those via the chosen correction method. Following suggestions in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift, we incorporate Untrained AutoEncoders (UAE) and black-box shift detection using the classifier's softmax outputs (BBSDs) as out-of-the box preprocessing methods and note that PCA can also be easily implemented using scikit-learn. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift. The which is part of the library can also be transformed into a drift detector picking up drift that reduces the performance of the classification model. We can therefore combine different preprocessing techniques to figure out if there is drift which hurts the model performance, and whether this drift can be classified as input drift or label shift.

    Backend

    The method works with both the PyTorch and TensorFlow frameworks for the optional preprocessing step. Alibi Detect does however not install PyTorch for you. Check the how to do this.

    Dataset

    consists of 60,000 32 by 32 RGB images equally distributed over 10 classes. We evaluate the drift detector on the CIFAR-10-C dataset (). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance. We also check for drift against the original test set with class imbalances.

    Load data

    Original CIFAR-10 data:

    For CIFAR-10-C, we can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the original test set in a reference dataset and a dataset which should not be rejected under the H0 of the K-S test. We also split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    We can also verify that the performance of a classification model on CIFAR-10 drops significantly on this perturbed dataset:

    Given the drop in performance, it is important that we detect the harmful data drift!

    Detect drift

    First we try a drift detector using the TensorFlow framework for the preprocessing step. We are trying to detect data drift on high-dimensional (32x32x3) data using feature-wise univariate tests. It therefore makes sense to apply dimensionality reduction first. Some dimensionality reduction methods also used in are readily available: a randomly initialized encoder (UAE or Untrained AutoEncoder in the paper), BBSDs (black-box shift detection using the classifier's softmax outputs) and PCA.

    Random encoder

    First we try the randomly initialized encoder:

    The p-value used by the detector for the multivariate data with encoding_dim features is equal to p_val / encoding_dim because of the .

    Let's check whether the detector thinks drift occurred on the different test sets and time the prediction calls:

    As expected, drift was only detected on the corrupted datasets. The feature-wise p-values for each univariate K-S test per (encoded) feature before multivariate correction show that most of them are well above the $0.05$ threshold for H0 and below for the corrupted datasets.

    BBSDs

    For BBSDs, we use the classifier's softmax outputs for black-box shift detection. This method is based on . The ResNet classifier is trained on data standardised by instance so we need to rescale the data.

    Now we initialize the detector. Here we use the output of the softmax layer to detect the drift, but other hidden layers can be extracted as well by setting 'layer' to the index of the desired hidden layer in the model:

    Again we can see that the p-value used by the detector for the multivariate data with 10 features (number of CIFAR-10 classes) is equal to p_val / 10 because of the .

    There is no drift on the original held out test set:

    Label drift

    We can also check what happens when we introduce class imbalances between the reference data X_ref and the tested data X_imb. The reference data will use $75$% of the instances of the first 5 classes and only $25$% of the last 5. The data used for drift testing then uses respectively $25$% and $75$% of the test instances for the first and last 5 classes.

    Update reference dataset for the detector and make predictions. Note that we store the preprocessed reference data since the preprocess_at_init kwarg is by default True:

    Update reference data

    So far we have kept the reference data the same throughout the experiments. It is possible however that we want to test a new batch against the last N instances or against a batch of instances of fixed size where we give each instance we have seen up until now the same chance of being in the reference batch (). The update_x_ref argument allows you to change the reference data update rule. It is a Dict which takes as key the update rule ('last' for last N instances or 'reservoir_sampling') and as value the batch size N of the reference data. You can also save the detector after the prediction calls to save the updated reference data.

    The reference data is now updated with each predict call. Say we start with our imbalanced reference set and make a prediction on the remaining test set data X_imb, then the drift detector will figure out data drift has occurred.

    We can now see that the reference data consists of N instances, obtained through reservoir sampling.

    We then draw a random sample from the training set and compare it with the updated reference data. This still highlights that there is data drift but will update the reference data again:

    When we draw a new sample from the training set, it highlights that it is not drifting anymore against the reservoir in X_ref.

    Multivariate correction mechanism

    Instead of the Bonferroni correction for multivariate data, we can also use the less conservative (FDR) correction. See or for nice explanations. While the Bonferroni correction controls the probability of at least one false positive, the FDR correction controls for an expected amount of false positives. The p_val argument at initialisation time can be interpreted as the acceptable q-value when the FDR correction is applied.

    Adversarial autoencoder as a malicious drift detector

    We can leverage the adversarial scores obtained from an trained on normal data and transform it into a data drift detector. The score function of the adversarial autoencoder becomes the preprocessing function for the drift detector. The K-S test is then a simple univariate test on the adversarial scores. Importantly, an adversarial drift detector flags malicious data drift. We can fetch the pretrained adversarial detector from a or train one from scratch:

    Initialise the drift detector:

    Make drift predictions on the original test set and corrupted data:

    While X_imb clearly exhibits input data drift due to the introduced class imbalances, it is not flagged by the adversarial drift detector since the performance of the classifier is not affected and the drift is not malicious. We can visualise this by plotting the adversarial scores together with the harmfulness of the data corruption as reflected by the drop in classifier accuracy:

    We can therefore use the scores of the detector itself to quantify the harmfulness of the drift! We can generalise this to all the corruptions at each severity level in CIFAR-10-C:

    We now compute mean scores and standard deviations per severity level and plot the results. The plot shows the mean adversarial scores (lhs) and ResNet-32 accuracies (rhs) for increasing data corruption severity levels. Level 0 corresponds to the original test set. Harmful scores are scores from instances which have been flipped from the correct to an incorrect prediction because of the corruption. Not harmful means that the prediction was unchanged after the corruption.

    Drift detection on molecular graphs

    Methods

    We illustrate drift detection on molecular graphs using a variety of detectors:

    • Kolmogorov-Smirnov detector on the output of the binary classification Graph Isomorphism Network to detect prediction distribution shift.

    • which leverages a measure of uncertainty on the model predictions (in this case ) to detect drift which could lead to degradation of model performance.

    • on graph embeddings to flag drift in the input data.

    • which flags drift in the input data using a (deep) learned kernel. The method trains a (deep) kernel on part of the data to maximise an estimate of the test power. Once the kernel is learned a permutation test is performed in the usual way on the value of the Maximum Mean Discrepancy (MMD) on the held out test set.

    • to see if drift occurred on graph level statistics such as the number of nodes, edges and the average clustering coefficient.

    Dataset

    We will train a classification model and detect drift on the ogbg-molhiv dataset. The dataset contains molecular graphs with both atom features (atomic number-1, chirality, node degree, formal charge, number of H bonds, number of radical electrons, hybridization, aromatic?, in a ring?) and bond level properties (bond type (e.g. single or double), bond stereo code, conjugated?). The goal is to predict whether a molecule inhibits HIV virus replication or not, so the task is binary classification.

    The dataset is split using the scaffold splitting procedure. This means that the molecules are split based on their 2D structural framework. Structurally different molecules are grouped into different subsets (train, validation, test) which could mean that there is drift between the splits.

    The dataset is retrieved from the dataset collection.

    Dependencies

    Besides alibi-detect, this example notebook also uses and , both of which can be installed via pip/conda.

    Load and analyze data

    We set some samples apart to serve as the reference data for our drift detectors. Note that the allowed format of the reference data is very flexible and can be np.ndarray or List[Any]:

    Let's plot some graph summary statistics such as the distribution of the node degrees, number of nodes and edges as well as the clustering coefficients:

    While the average number of nodes and edges are similar across the splits, the histograms show that the tails are slightly heavier for the training graphs.

    Plot molecules

    We borrow code from the PyTorch Geometric to visualize molecules from the graph objects.

    Train and evaluate a GNN classification model

    As our classifier we use a variation of a incorporating edge (bond) as well as node (atom) features.

    Train and evaluate the model. Evaluation is done using . If you already have a trained model saved, you can directly load it by specifying the load_path:

    Detect drift

    Prediction distribution drift

    We will first detect drift on the prediction distribution of the GIN model. Since the binary classification model returns continuous numerical univariate predictions, we use the . First we define some utility functions:

    Because we pass lists with torch_geometric.data.Data objects to the detector, we need to preprocess the data using the batch_fn into torch_geometric.data.Batch objects which can be fed to the model. Then we detect drift on the model prediction distribution.

    Since the dataset is heavily imbalanced, we will test the detectors on a sample which oversamples from the minority class (molecules which inhibit HIV virus replication):

    As expected, prediction distribution shift is detected for the imbalanced sample but not for the random test sample with similar label distribution as the reference data.

    Prediction uncertainty drift

    The can pick up when the model predictions drift into areas of changed uncertainty compared to the reference data. This can be a good proxy for drift which results in model performance degradation. The uncertainty is estimated via a Monte Carlo estimate (). We use the since our binary classification model returns 1D logits.

    Although we didn't pick up drift in the GIN model prediction distribution for the test sample, we can see that the model is less certain about the predictions on the test set, illustrated by the lower ROC-AUC.

    Input data drift using the MMD detector

    We can also more detect drift on the input data by encoding the data with a randomly initialized GNN to extract graph embeddings. Then we apply our detector of choice, e.g. the on the extracted embeddings.

    Input data drift using a learned kernel

    Instead of applying the MMD detector on the pooling output of a randomly initialized GNN encoder, we use the which trains the encoder and kernel on part of the data to maximise an estimate of the detector's test power. Once the kernel is learned a permutation test is performed in the usual way on the value of the MMD on the held out test set.

    Since the molecular scaffolds are different across the train, validation and test sets, we expect that this type of data shift is picked up in the input data (technically not the input but the graph embedding).

    Drift on graph statistics

    We could also compute graph-level statistics such as the number of nodes, edges and clustering coefficient and detect drift on those statistics using the Kolmogorov-Smirnov test with multivariate correction (e.g. ). First we define a preprocessing step to extract the summary statistics from the graphs:

    The 3 returned p-values correspond to respectively the p-values for the number of nodes, edges and clustering coefficient. We already saw in the EDA that the distributions of the node, edge and clustering coefficients look similar across the train, validation and test sets except for the tails. This is confirmed by running the drift detector on the graph statistics which cannot seem to pick up on the differences in molecular scaffolds between the datasets, unless we heavily oversample from the minority class where the number of nodes and edges but not the clustering coefficient significantly differ.

    Context-aware drift detection on ECGs

    Introduction

    In this notebook we show how to detect drift on ECG data given a specific context using the context-aware MMD detector (Cobb and Van Looveren, 2022). Consider the following simple example: we have a heatbeat monitoring system which is trained on a wide variety of heartbeats sampled from people of all ages across a variety of activities (e.g. rest or running). Then we deploy the system to monitor individual people during certain activities. The distribution of the heartbeats monitored during deployment will then be drifting against the reference data which resembles the full training distribution, simply because only individual people in a specific setting are being tracked. However, this does not mean that the system is not working and requires re-training. We are instead interested in flagging drift given the relevant context such as the person's characteristics (e.g. age or medical history) and the activity. Traditional drift detectors cannot flexibly deal with this setting since they rely on the i.i.d. assumption when sampling the reference and test sets. The context-aware detector however allows us to pass this context to the detector and flag drift appropriately. More generally, the context-aware drift detector detects changes in the data distribution which cannot be attributed to a permissible change in the context variable. On top of that, the detector allows you to understand which subpopulations are present in both the reference and test data which provides deeper insights into the distribution underlying the test data.

    Useful context (or conditioning) variables for the context-aware drift detector include but are not limited to:

    1. Domain or application specific contexts such as the time of day or the activity (e.g. running or resting).

    2. Conditioning on the relative prevalences of known subpopulations, such as the frequency of different types of heartbeats. It is important to note that while the relative frequency of each subpopulation (e.g. the different heartbeat types) might change, the distribution underlying each individual subpopulation (e.g. each specific type of heartbeat) cannot change.

    3. Conditioning on model predictions. Assume we trained a classifier which detects arrhythmia, then we can provide the classifier model predictions as context and understand if, given the model prediction, the data comes from the same underlying distribution as the reference data or not.

    The following settings will be showcased throughout the notebook:

    1. A change in the prevalences of subpopulations (i.e. different types of heartbeats as determined by an unsupervised clustering model or an ECG classifier) which are also present in the reference data is observed. Contrary to traditional drift detection approaches, the context-aware detector does not flag drift as this change in frequency of various heartbeats is permissible given the context provided.

    2. A change in the underlying distribution underlying one or more subpopulations takes place. While we allow changes in the prevalences of the subpopulations accounted for by the context variable, we do not allow changes of the subpopulations themselves. If for instance the ECGs are corrupted by noise on the sensor measurements, we want to flag drift.

    We also show how to condition the detector on different context variables such as the ECG classifier model predictions, cluster membership by an unsupervised clustering algorithm and timestamps.

    Under setting 1. we want our detector to be well-calibrated (a controlled False Positive Rate (FPR) and more generally a p-value which is uniformly distributed between 0 and 1) while under setting 2. we want our detector to be powerful and flag drift. Lastly, we show how the detector can help you to understand the connection between the reference and test data distributions better.

    Data

    The dataset contains 5000 ECG’s, originally obtained from Physionet from the , record chf07. The data has been pre-processed in 2 steps: first each heartbeat is extracted, and then each beat is made equal length via interpolation. The data is labeled and contains 5 classes. The first class $N$ which contains almost 60% of the observations is seen as normal while the others are supraventricular ectopic beats ($S$), ventricular ectopic beats ($V$), fusion beats ($F$) and unknown beats ($Q$).

    Requirements

    The notebook requires the torch and statsmodels packages to be installed, which can be done via pip:

    Before we start let's fix the random seeds for reproducibility:

    Load data

    First we load the data, show the distribution across the ECG classes and visualise some ECGs from each class.

    We can see that most heartbeats can be classified as normal, followed by the unknown class. We will now sample 500 heartbeats to train a simple ECG classifier. Importantly, we leave out the $F$ and $V$ classes which are used to detect drift. First we define a helper function to sample data.

    We use a prop_train fraction of all samples to train the classifier and then remove instances from the $F$ and $V$ classes. The rest of the data is used by our drift detectors.

    Train an ECG classifier

    Now we define and train our classifier on the training set.

    Let's evaluate out classifier on both the training and drift portions of the datasets.

    Detector calibration under no change

    We start with an example where no drift occurs and the reference and test data are both sampled randomly from all classes present in the reference data (classes 0, 1 and 3). Under this scenario, we expect no drift to be detected by either a normal MMD detector or by the context-aware MMD detector.

    Before we can start using the context-aware drift detector, first we need to define our context variable. In our experiments we allow the relative prevalences of subpopulations (i.e. the relative frequency of different types of hearbeats also present in the reference data) to vary while the distributions underlying each of the subpopulations remain unchanged. To achieve this we condition on the prediction probabilities of the classifier we trained earlier to distinguish the different types of ECGs. We can do this because the prediction probabilities can account for the frequency of occurrence of each of the heartbeat types (be it imperfectly given our classifier makes the occasional mistake).

    The below figure of the of a random sample from the uniform distribution U[0,1] against the obtained p-values from the vanilla and context-aware MMD detectors illustrate how well both detectors are calibrated. A perfectly calibrated detector should have a Q-Q plot which closely follows the diagonal. Only the middle plot in the grid shows the detector's p-values. The other plots correspond to n_runs p-values actually sampled from U[0,1] to contextualise how well the central plot follows the diagonal given the limited number of samples.

    As expected we can see that both the normal MMD and the context-aware MMD detectors are well-calibrated.

    Changing the relative subpopulation prevalences

    We now focus our attention on a more realistic problem where the relative frequency of one or more subpopulations (i.e. types of hearbeats) is changing while the underlying subpopulation distribution stays the same. This would be the expected setting when we monitor the heartbeat of a specific person (e.g. only normal heartbeats) and we don't want to flag drift.

    While the usual MMD detector only returns very low p-values (mostly 0), the context-aware MMD detector remains calibrated.

    Changing the subpopulation distribution

    In the following example we change the distribution of one or more of the underlying subpopulations (i.e. the different types of heartbeats). Notice that now we do want to flag drift since our context variable, which permits changes in relative subpopulation prevalences, can no longer explain the change in distribution.

    We will again sample from the normal heartbeats, but now we will add random noise to a fraction of the extracted heartbeats to change the distribution. This could be the result of an error with some of the sensors. The perturbation is illustrated below:

    As we can see from the Q-Q and power of the detector, the changes in the subpopulation are easily detected:

    Changing the context variable

    We now use the cluster membership probabilities of a Gaussian mixture model which is fit on the training instances as context variables instead of the model predictions. We will test both the calibration when the frequency of the subpopulations (the cluster memberships) changes as well as the power when the $F$ and $V$ heartbeats are included.

    Interpretability of the context-aware detector

    The test statistic $\hat{t}$ of the context-aware MMD detector can be formulated as follows: $\hat{t} = \langle K_{0,0}, W_{0,0} \rangle + \langle K_{1,1}, W_{1,1} \rangle -2\langle K_{0,1}, W_{0,1}\rangle$ where $0$ refers to the reference data, $1$ to the test data, and $W_{.,.}$ and $K_{.,.}$ are the weight and kernel matrices, respectively. The weight matrices $W_{.,.}$ allow us to focus on the distribution's subpopulations of interest. Reference instances which have similar contexts as the test data will have higher values for their entries in $W_{0,1}$ than instances with dissimilar contexts. We can therefore interpret $W_{0,1}$ as the coupling matrix between instances in the reference and the test sets. This allows us to investigate which subpopulations from the reference set are present and which are missing in the test data. If we also have a good understanding of the model performance on various subpopulations of the reference data, we could even try and use this coupling matrix to roughly proxy model performance on the unlabeled test instances. Note that in this case we would require labels from the reference data and make sure the reference instances come from the validation, not the training set.

    In the following example we only pick 1 type of heartbeat (the normal one) to be present in the test set while 3 types are present in the reference set. We can then investigate via the coupling matrix whether the test statistic $\hat{t}$ focused on the right types of heartbeats in the reference data via $W_{0,1}$. More concretely, we can sum over the columns (the test instances) of $W_{0,1}$ and check which reference instances obtained the highest weights.

    As expected no drift was detected since the test set only contains normal heartbeats. We now sort the weights of w_ref in descending order. We expect the top 400 entries to be fairly high and consistent since these represent the normal heartbeats in the reference set. Afterwards, the weight attribution to the other instances in the reference set should be low. The plot below confirms that this is indeed what happens.

    Time conditioning

    The dataset consists of nicely extracted and aligned ECGs of 140 data points for each observation. However in reality it is likely that we will continuously or periodically observe instances which are not nicely aligned. We could however assign a timestamp to the data (e.g. starting from a peak) and use time as the context variable. This is illustrated in the example below.

    First we create a new dataset where we split each instance in slices of non-overlapping ECG segments. Each of the segments will have an associated timestamp as context variable. Then we can check the calibration under no change (besides the time-varying behaviour which is accounted for) as well as the power for ECG segments where we add incorrect time stamps to some of the segments.

    adversarial detector
    PyTorch docs
    CIFAR10
    Hendrycks & Dietterich, 2019
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    Bonferroni correction
    Detecting and Correcting for Label Shift with Black Box Predictors
    Bonferroni correction
    reservoir sampling
    False Discovery Rate
    here
    here
    adversarial autoencoder
    Google Cloud Bucket
    Model Uncertainty detector
    MC dropout
    Maximum Mean Discrepancy detector
    Learned Kernel detector
    Kolmogorov-Smirnov detector
    Open Graph Benchmark
    PyTorch Geometric
    OGB
    GNN explanation example
    Graph Isomorphism Network
    ROC-AUC
    Kolmogorov-Smirnov drift detector
    model uncertainty drift detector
    MC dropout
    RegressorUncertaintyDrift detector
    MMD detector
    Learned Kernel detector
    Bonferroni

    Conditioning on model uncertainties which would allow increases in model uncertainty due to drift into familiar regions of high aleatoric uncertainty (often fine) to be distinguished from that into unfamiliar regions of high epistemic uncertainty (often problematic).

    BIDMC Congestive Heart Failure Database
    Q-Q (Quantile-Quantile) plots
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import tensorflow as tf
    
    from alibi_detect.cd import KSDrift
    from alibi_detect.models.tensorflow import scale_by_instance
    from alibi_detect.utils.fetching import fetch_tf_model, fetch_detector
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    np.random.seed(0)
    n_test = X_test.shape[0]
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    idx_h0 = np.delete(np.arange(n_test), idx, axis=0)
    X_ref,y_ref = X_test[idx], y_test[idx]
    X_h0, y_h0 = X_test[idx_h0], y_test[idx_h0]
    print(X_ref.shape, X_h0.shape)
    # check that the classes are more or less balanced
    classes, counts_ref = np.unique(y_ref, return_counts=True)
    counts_h0 = np.unique(y_h0, return_counts=True)[1]
    print('Class Ref H0')
    for cl, cref, ch0 in zip(classes, counts_ref, counts_h0):
        assert cref + ch0 == n_test // 10
        print('{}     {} {}'.format(cl, cref, ch0))
    n_corr = len(corruption)
    X_c = [X_corr[i * n_test:(i + 1) * n_test] for i in range(n_corr)]
    #| tags: [hide_input]
    i = 1
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow(X_test[i])
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    dataset = 'cifar10'
    model = 'resnet32'
    clf = fetch_tf_model(dataset, model)
    acc = clf.evaluate(scale_by_instance(X_test), y_test, batch_size=128, verbose=0)[1]
    print('Test set accuracy:')
    print('Original {:.4f}'.format(acc))
    clf_accuracy = {'original': acc}
    for _ in range(len(corruption)):
        acc = clf.evaluate(scale_by_instance(X_c[_]), y_test, batch_size=128, verbose=0)[1]
        clf_accuracy[corruption[_]] = acc
        print('{} {:.4f}'.format(corruption[_], acc))
    #| scrolled: false
    from functools import partial
    from tensorflow.keras.layers import Conv2D, Dense, Flatten, InputLayer, Reshape
    from alibi_detect.cd.tensorflow import preprocess_drift
    
    tf.random.set_seed(0)
    
    # define encoder
    encoding_dim = 32
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(encoding_dim,)
      ]
    )
    
    # define preprocessing function
    preprocess_fn = partial(preprocess_drift, model=encoder_net, batch_size=512)
    
    # initialise drift detector
    p_val = .05
    cd = KSDrift(X_ref, p_val=p_val, preprocess_fn=preprocess_fn)
    
    # we can also save/load an initialised detector
    filepath = 'my_path'  # change to directory where detector is saved
    save_detector(cd, filepath)
    cd = load_detector(filepath)
    assert cd.p_val / cd.n_features == p_val / encoding_dim
    from timeit import default_timer as timer
    
    labels = ['No!', 'Yes!']
    
    def make_predictions(cd, x_h0, x_corr, corruption):
        t = timer()
        preds = cd.predict(x_h0)
        dt = timer() - t
        print('No corruption')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('Feature-wise p-values:')
        print(preds['data']['p_val'])
        print(f'Time (s) {dt:.3f}')
        
        if isinstance(x_corr, list):
            for x, c in zip(x_corr, corruption):
                t = timer()
                preds = cd.predict(x)
                dt = timer() - t
                print('')
                print(f'Corruption type: {c}')
                print('Drift? {}'.format(labels[preds['data']['is_drift']]))
                print('Feature-wise p-values:')
                print(preds['data']['p_val'])
                print(f'Time (s) {dt:.3f}')
    make_predictions(cd, X_h0, X_c, corruption)
    X_train = scale_by_instance(X_train)
    X_test = scale_by_instance(X_test)
    X_ref = scale_by_instance(X_ref)
    X_h0 = scale_by_instance(X_h0)
    X_c = [scale_by_instance(X_c[i]) for i in range(n_corr)]
    from alibi_detect.cd.tensorflow import HiddenOutput
    
    # define preprocessing function, we use the
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(clf, layer=-1), batch_size=128)
    
    cd = KSDrift(X_ref, p_val=p_val, preprocess_fn=preprocess_fn)
    assert cd.p_val / cd.n_features == p_val / 10
    make_predictions(cd, X_h0, X_c, corruption)
    np.random.seed(0)
    # get index for each class in the test set
    num_classes = len(np.unique(y_test))
    idx_by_class = [np.where(y_test == c)[0] for c in range(num_classes)]
    # sample imbalanced data for different classes for X_ref and X_imb
    perc_ref = .75
    perc_ref_by_class = [perc_ref if c < 5 else 1 - perc_ref for c in range(num_classes)]
    n_by_class = n_test // num_classes
    X_ref = []
    X_imb, y_imb = [], []
    for _ in range(num_classes):
        idx_class_ref = np.random.choice(n_by_class, size=int(perc_ref_by_class[_] * n_by_class), replace=False)
        idx_ref = idx_by_class[_][idx_class_ref]
        idx_class_imb = np.delete(np.arange(n_by_class), idx_class_ref, axis=0)
        idx_imb = idx_by_class[_][idx_class_imb]
        assert not np.array_equal(idx_ref, idx_imb)
        X_ref.append(X_test[idx_ref])
        X_imb.append(X_test[idx_imb])
        y_imb.append(y_test[idx_imb])
    X_ref = np.concatenate(X_ref)
    X_imb = np.concatenate(X_imb)
    y_imb = np.concatenate(y_imb)
    print(X_ref.shape, X_imb.shape, y_imb.shape)
    cd.x_ref = cd.preprocess_fn(X_ref)
    preds_imb = cd.predict(X_imb)
    print('Drift? {}'.format(labels[preds_imb['data']['is_drift']]))
    print(preds_imb['data']['p_val'])
    N = 7500
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn, update_x_ref={'reservoir_sampling': N})
    preds_imb = cd.predict(X_imb)
    print('Drift? {}'.format(labels[preds_imb['data']['is_drift']]))
    assert cd.x_ref.shape[0] == N
    np.random.seed(0)
    perc_train = .5
    n_train = X_train.shape[0]
    idx_train = np.random.choice(n_train, size=int(perc_train * n_train), replace=False)
    preds_train = cd.predict(X_train[idx_train])
    print('Drift? {}'.format(labels[preds_train['data']['is_drift']]))
    np.random.seed(1)
    perc_train = .1
    idx_train = np.random.choice(n_train, size=int(perc_train * n_train), replace=False)
    preds_train = cd.predict(X_train[idx_train])
    print('Drift? {}'.format(labels[preds_train['data']['is_drift']]))
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn, correction='fdr')
    
    preds_imb = cd.predict(X_imb)
    print('Drift? {}'.format(labels[preds_imb['data']['is_drift']]))
    load_pretrained = True
    #| scrolled: true
    from tensorflow.keras.regularizers import l1
    from tensorflow.keras.layers import Conv2DTranspose
    from alibi_detect.ad import AdversarialAE
    
    # change filepath to (absolute) directory where model is downloaded
    filepath = os.path.join(os.getcwd(), 'my_path')
    detector_type = 'adversarial'
    detector_name = 'base'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:  # train detector from scratch
        # define encoder and decoder networks
        encoder_net = tf.keras.Sequential(
                [
                    InputLayer(input_shape=(32, 32, 3)),
                    Conv2D(32, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(64, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(256, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Flatten(),
                    Dense(40)
                ]
            )
        
        decoder_net = tf.keras.Sequential(
            [
                    InputLayer(input_shape=(40,)),
                    Dense(4 * 4 * 128, activation=tf.nn.relu),
                    Reshape(target_shape=(4, 4, 128)),
                    Conv2DTranspose(256, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(64, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(3, 4, strides=2, padding='same', 
                                    activation=None, kernel_regularizer=l1(1e-5))
                ]
            )
        
        # initialise and train detector
        ad = AdversarialAE(encoder_net=encoder_net, decoder_net=decoder_net, model=clf)
        ad.fit(X_train, epochs=50, batch_size=128, verbose=True)
        
        # save the trained adversarial detector
        save_detector(ad, filepath)
    np.random.seed(0)
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    X_ref = scale_by_instance(X_test[idx])
    
    # adversarial score fn = preprocess step
    preprocess_fn = partial(ad.score, batch_size=128)
    
    cd = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn)
    clf_accuracy['h0'] = clf.evaluate(X_h0, y_h0, batch_size=128, verbose=0)[1]
    preds_h0 = cd.predict(X_h0)
    print('H0: Accuracy {:.4f} -- Drift? {}'.format(
        clf_accuracy['h0'], labels[preds_h0['data']['is_drift']]))
    clf_accuracy['imb'] = clf.evaluate(X_imb, y_imb, batch_size=128, verbose=0)[1]
    preds_imb = cd.predict(X_imb)
    print('imbalance: Accuracy {:.4f} -- Drift? {}'.format(
        clf_accuracy['imb'], labels[preds_imb['data']['is_drift']]))
    for x, c in zip(X_c, corruption):
        preds = cd.predict(x)
        print('{}: Accuracy {:.4f} -- Drift? {}'.format(
            c, clf_accuracy[c],labels[preds['data']['is_drift']]))
    adv_scores = {}
    score = ad.score(X_ref, batch_size=128)
    adv_scores['original'] = {'mean': score.mean(), 'std': score.std()}
    score = ad.score(X_h0, batch_size=128)
    adv_scores['h0'] = {'mean': score.mean(), 'std': score.std()}
    score = ad.score(X_imb, batch_size=128)
    adv_scores['imb'] = {'mean': score.mean(), 'std': score.std()}
    
    for x, c in zip(X_c, corruption):
        score_x = ad.score(x, batch_size=128)
        adv_scores[c] = {'mean': score_x.mean(), 'std': score_x.std()}
    mu = [v['mean'] for _, v in adv_scores.items()]
    stdev = [v['std'] for _, v in adv_scores.items()]
    xlabels = list(adv_scores.keys())
    acc = [clf_accuracy[label] for label in xlabels]
    xticks = np.arange(len(mu))
    
    width = .35
    
    fig, ax = plt.subplots()
    ax2 = ax.twinx()
    
    p1 = ax.bar(xticks, mu, width, yerr=stdev, capsize=2)
    color = 'tab:red'
    p2 = ax2.bar(xticks + width, acc, width, color=color)
    
    ax.set_title('Adversarial Scores and Accuracy by Corruption Type')
    ax.set_xticks(xticks + width / 2)
    ax.set_xticklabels(xlabels, rotation=45)
    ax.legend((p1[0], p2[0]), ('Score', 'Accuracy'), loc='upper right', ncol=2)
    ax.set_ylabel('Adversarial Score')
    
    color = 'tab:red'
    ax2.set_ylabel('Accuracy')
    ax2.set_ylim((-.26,1.2))
    ax.set_ylim((-2,9))
    
    plt.show()
    def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
        return (y_true == y_pred).astype(int).sum() / y_true.shape[0]
    from alibi_detect.utils.tensorflow import predict_batch
    
    severities = [1, 2, 3, 4, 5]
    
    score_drift = {
        1: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        2: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        3: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        4: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        5: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
    }
    
    y_pred = predict_batch(X_test, clf, batch_size=256).argmax(axis=1)
    score_x = ad.score(X_test, batch_size=256)
    
    for s in severities:
        print('\nSeverity: {} of {}'.format(s, len(severities)))
        
        print('Loading corrupted dataset...')
        X_corr, y_corr = fetch_cifar10c(corruption=corruptions, severity=s, return_X_y=True)
        X_corr = X_corr.astype('float32')
        
        print('Preprocess data...')
        X_corr = scale_by_instance(X_corr)
        
        print('Make predictions on corrupted dataset...')
        y_pred_corr = predict_batch(X_corr, clf, batch_size=256).argmax(axis=1)
        
        print('Compute adversarial scores on corrupted dataset...')
        score_corr = ad.score(X_corr, batch_size=256)
        
        print('Get labels for malicious corruptions...')
        labels_corr = np.zeros(score_corr.shape[0])
        repeat = y_corr.shape[0] // y_test.shape[0]
        y_pred_repeat = np.tile(y_pred, (repeat,))
        # malicious/harmful corruption: original prediction correct but 
        # prediction on corrupted data incorrect
        idx_orig_right = np.where(y_pred_repeat == y_corr)[0]
        idx_corr_wrong = np.where(y_pred_corr != y_corr)[0]
        idx_harmful = np.intersect1d(idx_orig_right, idx_corr_wrong)
        labels_corr[idx_harmful] = 1
        labels = np.concatenate([np.zeros(X_test.shape[0]), labels_corr]).astype(int)
        # harmless corruption: original prediction correct and prediction
        # on corrupted data correct
        idx_corr_right = np.where(y_pred_corr == y_corr)[0]
        idx_harmless = np.intersect1d(idx_orig_right, idx_corr_right)
        
        score_drift[s]['all'] = score_corr
        score_drift[s]['harm'] = score_corr[idx_harmful]
        score_drift[s]['noharm'] = score_corr[idx_harmless]
        score_drift[s]['acc'] = accuracy(y_corr, y_pred_corr)
    mu_noharm, std_noharm = [], []
    mu_harm, std_harm = [], []
    acc = [clf_accuracy['original']]
    for k, v in score_drift.items():
        mu_noharm.append(v['noharm'].mean())
        std_noharm.append(v['noharm'].std())
        mu_harm.append(v['harm'].mean())
        std_harm.append(v['harm'].std())
        acc.append(v['acc'])
    plot_labels = ['0', '1', '2', '3', '4', '5']
    
    N = 6
    ind = np.arange(N)
    width = .35
    
    fig_bar_cd, ax = plt.subplots()
    ax2 = ax.twinx()
    
    p0 = ax.bar(ind[0], score_x.mean(), yerr=score_x.std(), capsize=2)
    p1 = ax.bar(ind[1:], mu_noharm, width, yerr=std_noharm, capsize=2)
    p2 = ax.bar(ind[1:] + width, mu_harm, width, yerr=std_harm, capsize=2)
    
    ax.set_title('Adversarial Scores and Accuracy by Corruption Severity')
    ax.set_xticks(ind + width / 2)
    ax.set_xticklabels(plot_labels)
    ax.set_ylim((-1,6))
    ax.legend((p1[0], p2[0]), ('Not Harmful', 'Harmful'), loc='upper right', ncol=2)
    ax.set_ylabel('Score')
    ax.set_xlabel('Corruption Severity')
    
    color = 'tab:red'
    ax2.set_ylabel('Accuracy', color=color)
    ax2.plot(acc, color=color)
    ax2.tick_params(axis='y', labelcolor=color)
    
    plt.show()
    import numpy as np
    import os
    import torch
    
    def set_seed(seed: int) -> None:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        np.random.seed(seed)
    
    set_seed(0)
    from ogb.graphproppred import PygGraphPropPredDataset
    from torch_geometric.data import DataLoader
    #| scrolled: true
    dataset_name = 'ogbg-molhiv'
    batch_size = 32
    dataset = PygGraphPropPredDataset(name=dataset_name)
    split_idx = dataset.get_idx_split()
    n_ref = 1000
    n_h0 = 500
    
    idx_tr = split_idx['train']
    idx_sample = np.random.choice(idx_tr.numpy(), size=n_ref + n_h0, replace=False)
    idx_ref, idx_h0 = idx_sample[:n_ref], idx_sample[n_ref:]
    x_ref = [dataset[i] for i in idx_ref]
    x_h0 = [dataset[i] for i in idx_h0]
    idx_tr = torch.from_numpy(np.setdiff1d(idx_tr, idx_sample))
    print(f'Number of reference instances: {len(x_ref)}')
    print(f'Number of H0 instances: {len(x_h0)}')
    dl_tr = DataLoader(dataset[idx_tr], batch_size=batch_size, shuffle=True)
    dl_val = DataLoader(dataset[split_idx['valid']], batch_size=batch_size, shuffle=False)
    dl_te = DataLoader(dataset[split_idx['test']], batch_size=batch_size, shuffle=False)
    print(f'Number of train, val and test batches: {len(dl_tr)}, {len(dl_val)} and {len(dl_te)}')
    ds = dataset
    print()
    print(f'Dataset: {ds}:')
    print('=============================================================')
    print(f'Number of graphs: {len(ds)}')
    print(f'Number of node features: {ds.num_node_features}')
    print(f'Number of edge features: {ds.num_edge_features}')
    print(f'Number of classes: {ds.num_classes}')
    
    i = 0
    d = ds[i]
    
    print(f'\nExample: {d}')
    print('=============================================================')
    
    print(f'Number of nodes: {d.num_nodes}')
    print(f'Number of edges: {d.num_edges}')
    print(f'Average node degree: {d.num_edges / d.num_nodes:.2f}')
    print(f'Contains isolated nodes: {d.contains_isolated_nodes()}')
    print(f'Contains self-loops: {d.contains_self_loops()}')
    print(f'Is undirected: {d.is_undirected()}')
    #| scrolled: true
    import matplotlib.pyplot as plt
    import networkx as nx
    from networkx.algorithms.cluster import clustering
    from torch_geometric.utils import degree, to_networkx
    from tqdm import tqdm
    from typing import Tuple
    
    
    def degrees_and_clustering(loader: DataLoader) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        degrees, c_coeff, num_nodes, num_edges = [], [], [], []
        for data in tqdm(loader):
            row, col = data.edge_index
            deg = degree(row, data.x.size(0), dtype=data.x.dtype)
            degrees.append(deg.numpy())
            g = to_networkx(data, node_attrs=['x'], edge_attrs=['edge_attr'], to_undirected=True)
            c = list(clustering(g).values())
            c_coeff.append(c)
            num_nodes += [d.num_nodes for d in data.to_data_list()]
            num_edges += [d.num_edges for d in data.to_data_list()]
        degrees = np.concatenate(degrees, axis=0)
        c_coeff = np.concatenate(c_coeff, axis=0)
        return degrees, c_coeff, np.array(num_nodes), np.array(num_edges)
    
    
    # x: nodes, edges, degree, cluster
    def plot_histogram(x: str, bins: int = None, log: bool = True) -> None:
        if x == 'nodes':
            vals = [num_nodes_tr, num_nodes_val, num_nodes_te]
        elif x == 'edges':
            vals = [num_edges_tr, num_edges_val, num_edges_te]
        elif x == 'degree':
            vals = [degree_tr, degree_val, degree_te]
        elif x == 'cluster':
            vals = [cluster_tr, cluster_val, cluster_te]
        labels = ['train', 'val', 'test']
        for v, l in zip(vals, labels):
            plt.hist(v, density=True, log=log, label=l, bins=bins)
        plt.title(f'{x} distribution')
        plt.legend()
        plt.show()
    degree_tr, cluster_tr, num_nodes_tr, num_edges_tr = degrees_and_clustering(dl_tr)
    degree_val, cluster_val, num_nodes_val, num_edges_val = degrees_and_clustering(dl_val)
    degree_te, cluster_te, num_nodes_te, num_edges_te = degrees_and_clustering(dl_te)
    print('Average number and stdev of nodes, edges, degree and clustering coefficients:')
    print('\nTrain...')
    print(f'Nodes: {num_nodes_tr.mean():.1f} +- {num_nodes_tr.std():.1f}')
    print(f'Edges: {num_edges_tr.mean():.1f} +- {num_edges_tr.std():.1f}')
    print(f'Degree: {degree_tr.mean():.1f} +- {degree_tr.std():.1f}')
    print(f'Clustering: {cluster_tr.mean():.3f} +- {cluster_tr.std():.3f}')
    
    print('\nValidation...')
    print(f'Nodes: {num_nodes_val.mean():.1f} +- {num_nodes_val.std():.1f}')
    print(f'Edges: {num_edges_val.mean():.1f} +- {num_edges_val.std():.1f}')
    print(f'Degree: {degree_val.mean():.1f} +- {degree_val.std():.1f}')
    print(f'Clustering: {cluster_val.mean():.3f} +- {cluster_val.std():.3f}')
    
    print('\nTest...')
    print(f'Nodes: {num_nodes_te.mean():.1f} +- {num_nodes_te.std():.1f}')
    print(f'Edges: {num_edges_te.mean():.1f} +- {num_edges_te.std():.1f}')
    print(f'Degree: {degree_te.mean():.1f} +- {degree_te.std():.1f}')
    print(f'Clustering: {cluster_te.mean():.3f} +- {cluster_te.std():.3f}')
    plot_histogram('nodes', bins=50)
    plot_histogram('edges', bins=50)
    plot_histogram('degree')
    plot_histogram('cluster')
    def draw_molecule(g, edge_mask=None, draw_edge_labels=False):
        g = g.copy().to_undirected()
        node_labels = {}
        for u, data in g.nodes(data=True):
            node_labels[u] = data['name']
        pos = nx.planar_layout(g)
        pos = nx.spring_layout(g, pos=pos)
        if edge_mask is None:
            edge_color = 'black'
            widths = None
        else:
            edge_color = [edge_mask[(u, v)] for u, v in g.edges()]
            widths = [x * 10 for x in edge_color]
        nx.draw(g, pos=pos, labels=node_labels, width=widths,
                edge_color=edge_color, edge_cmap=plt.cm.Blues,
                node_color='azure')
        
        if draw_edge_labels and edge_mask is not None:
            edge_labels = {k: ('%.2f' % v) for k, v in edge_mask.items()}    
            nx.draw_networkx_edge_labels(g, pos, edge_labels=edge_labels,
                                        font_color='red')
        plt.show()
    
    
    def to_molecule(data):
        ATOM_MAP = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al', 'Si', 'P',
                    'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn',
                    'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y']
        g = to_networkx(data, node_attrs=['x'])
        for u, data in g.nodes(data=True):
            data['name'] = ATOM_MAP[data['x'][0]]
            del data['x']
        return g
    i = 0
    mol = to_molecule(dataset[i])
    plt.figure(figsize=(10, 5))
    draw_molecule(mol)
    import torch.nn as nn
    import torch.nn.functional as F
    from torch_geometric.data.batch import Batch
    from torch_geometric.nn import MessagePassing, global_add_pool, global_max_pool, global_mean_pool, LayerNorm
    from ogb.graphproppred.mol_encoder import AtomEncoder, BondEncoder
    
    
    class GINConv(MessagePassing):
        def __init__(self, emb_dim: int) -> None:
            super().__init__(aggr='add')
            self.mlp = nn.Sequential(
                nn.Linear(emb_dim, 2 * emb_dim),
                nn.BatchNorm1d(2 * emb_dim),
                nn.ReLU(),
                nn.Linear(2 * emb_dim, emb_dim)
            )
            self.eps = nn.Parameter(torch.Tensor([0.]))
            self.bond_encoder = BondEncoder(emb_dim=emb_dim)  # encode edge features
    
        def forward(self, x: torch.Tensor, edge_index: torch.Tensor, edge_attr: torch.Tensor) -> torch.Tensor:
            edge_emb = self.bond_encoder(edge_attr)
            return self.mlp((1 + self.eps) * x + self.propagate(edge_index, x=x, edge_attr=edge_emb))
    
        def message(self, x_j: torch.Tensor, edge_attr: torch.Tensor) -> torch.Tensor:
            return x_j + edge_attr
    
        def update(self, aggr_out: torch.Tensor) -> torch.Tensor:
            return aggr_out
    
    
    class GIN(nn.Module):
        def __init__(self, n_layer: int = 5, emb_dim: int = 64, n_out: int = 2, dropout: float = .5,
                     jk: bool = True, residual: bool = True, pool: str = 'add', norm: str = 'batch') -> None:
            super().__init__()
            self.n_layer = n_layer
            self.jk = jk  # jumping-knowledge
            self.residual = residual  # residual/skip connections
            self.atom_encoder = AtomEncoder(emb_dim=emb_dim)  # encode node features
            self.convs = nn.ModuleList([GINConv(emb_dim) for _ in range(n_layer)])
            norm = nn.BatchNorm1d if norm == 'batch' else LayerNorm
            self.bns = nn.ModuleList([norm(emb_dim) for _ in range(n_layer)])
            if pool == 'mean':
                self.pool = global_mean_pool
            elif pool == 'add':
                self.pool = global_add_pool
            elif pool == 'max':
                self.pool = global_max_pool
            pool_dim = (n_layer + 1) * emb_dim if jk else emb_dim
            self.linear = nn.Linear(pool_dim, n_out)
            self.dropout = nn.Dropout(p=dropout)
    
        def forward(self, data: Batch) -> torch.Tensor:
            x, edge_index, edge_attr, batch = data.x, data.edge_index, data.edge_attr, data.batch
            # node embeddings
            hs = [self.atom_encoder(x)]
            for layer in range(self.n_layer):
                h = self.convs[layer](hs[layer], edge_index, edge_attr)
                h = self.bns[layer](h)
                if layer < self.n_layer - 1:
                    h = F.relu(h)
                if self.residual:
                    h += hs[layer]
                hs += [h]
            # graph embedding and prediction
            if self.jk:
                h = torch.cat([h for h in hs], -1)
            h_pool = self.pool(h, batch)
            h_drop = self.dropout(h_pool)
            h_out = self.linear(h_drop)
            return h_out
    #| scrolled: true
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'device: {device}')
    
    n_layer = 5
    emb_dim = 300
    n_out = 1
    dropout = .5
    jk = True
    residual = False
    pool = 'mean'
    norm = 'batch'
    
    model = GIN(n_layer, emb_dim, n_out, dropout, jk, residual, pool, norm).to(device)
    load_path = 'gnn'  # set to None if no pretrained model available
    #| scrolled: true
    from ogb.graphproppred import Evaluator
    from tqdm import tqdm
    
    criterion = nn.BCEWithLogitsLoss()
    optim = torch.optim.Adam(model.parameters(), lr=.001)
    evaluator = Evaluator(name=dataset_name)  # ROC-AUC for ogbg-molhiv
    
    
    def train(loader: DataLoader, verbose: bool = False) -> None:
        dl = tqdm(loader, total=len(loader)) if verbose else loader
        model.train()
        for data in dl:
            data = data.to(device)
            optim.zero_grad()
            y_hat = model(data)
            is_labeled = data.y == data.y
            loss = criterion(y_hat[is_labeled], data.y[is_labeled].float())
            loss.backward()
            optim.step()
            if verbose:
                dl.set_postfix(dict(loss=loss.item()))
    
    
    def evaluate(loader: DataLoader, split: str, verbose: bool = False) -> float:
        dl = tqdm(loader, total=len(loader)) if verbose else loader
        model.eval()
        y_pred, y_true = [], []
        for data in dl:
            data = data.to(device)
            with torch.no_grad():
                y_hat = model(data)
            y_pred.append(y_hat.cpu())
            y_true.append(data.y.float().cpu())
        y_true = torch.cat(y_true, dim=0)
        y_pred = torch.cat(y_pred, dim=0)
        loss = criterion(y_pred, y_true)
        input_dict = dict(y_true=y_true, y_pred=y_pred)
        result_dict = evaluator.eval(input_dict)
        print(f'{split} ROC-AUC: {result_dict["rocauc"]:.3f} -- loss: {loss:.3f}')
        return result_dict["rocauc"]
    
    
    if load_path is None or not os.path.isdir(load_path):
        epochs = 150
        rocauc_best = 0.
        save_path = 'gnn'
        for epoch in range(epochs):
            print(f'\nEpoch {epoch + 1} / {epochs}')
            train(dl_tr)
            _ = evaluate(dl_tr, 'train')
            rocauc = evaluate(dl_val, 'val')
            if rocauc > rocauc_best and os.path.isdir(save_path):
                print('Saving new best model.')
                rocauc_best = rocauc
                torch.save(model.state_dict(), os.path.join(save_path, 'model.dict'))
            _ = evaluate(dl_te, 'test')
        load_path = save_path
    
    
    # load (best) model
    model.load_state_dict(torch.load(os.path.join(load_path, 'model.dict')))
    _ = evaluate(dl_tr, 'train')
    _ = evaluate(dl_val, 'val')
    _ = evaluate(dl_te, 'test')
    from torch_geometric.data import Batch, Data
    from typing import Dict, List, Union
    
    
    labels = ['No!', 'Yes!']
    def make_predictions(dd, xs: Dict[str, List[Data]]) -> None:
        for split, x in xs.items():
            preds = dd.predict(x)
            dl = DataLoader(x, batch_size=32, shuffle=False)
            _ = evaluate(dl, split)
            print('Drift? {}'.format(labels[preds['data']['is_drift']]))
            if isinstance(preds["data"]["p_val"], (list, np.ndarray)):
                print(f'p-value: {preds["data"]["p_val"]}')
            else:
                print(f'p-value: {preds["data"]["p_val"]:.3f}')
            print('')
            
    
    def sample(split: str, n: int, ) -> List[Data]:
        idx = np.random.choice(split_idx[split].numpy(), size=n, replace=False)
        return [dataset[i] for i in idx]
    from alibi_detect.cd import KSDrift
    from alibi_detect.utils.pytorch import predict_batch
    from functools import partial
    
    
    def batch_fn(data: Union[List[Data], Batch]) -> Batch:
        if isinstance(data, Batch):
            return data
        else:
            return Batch().from_data_list(data)
    
    
    preprocess_fn = partial(predict_batch, model=model, device=device, preprocess_fn=batch_fn, batch_size=32)
    dd = KSDrift(x_ref, p_val=.05, preprocess_fn=preprocess_fn)
    split = 'test'
    x_imb = sample(split, 500)
    n = 0
    for i in split_idx[split]:
        if dataset[i].y[0].item() == 1:
            x_imb.append(dataset[i])
            n += 1
    print(f'# instances: {len(x_imb)} -- # class 1: {n}')
    xs = {'H0': x_h0, 'test sample': sample('test', 500), 'imbalanced sample': x_imb}
    make_predictions(dd, xs)
    #| scrolled: false
    from alibi_detect.cd import RegressorUncertaintyDrift
    
    dd = RegressorUncertaintyDrift(x_ref, model=model, backend='pytorch', p_val=.05, n_evals=100,
                                   uncertainty_type='mc_dropout', preprocess_batch_fn=batch_fn)
    make_predictions(dd, xs)
    class Encoder(nn.Module):
        def __init__(self, n_layer: int = 1, emb_dim: int = 64, jk: bool = True, 
                     residual: bool = True, pool: str = 'add', norm: str = 'batch') -> None:
            super().__init__()
            self.n_layer = n_layer
            self.jk = jk  # jumping-knowledge
            self.residual = residual  # residual/skip connections
            self.atom_encoder = AtomEncoder(emb_dim=emb_dim)  # encode node features
            self.convs = nn.ModuleList([GINConv(emb_dim) for _ in range(n_layer)])
            norm = nn.BatchNorm1d if norm == 'batch' else LayerNorm
            self.bns = nn.ModuleList([norm(emb_dim) for _ in range(n_layer)])
            self.pool = global_add_pool
    
        def forward(self, data: Batch) -> torch.Tensor:
            x, edge_index, edge_attr, batch = data.x, data.edge_index, data.edge_attr, data.batch
            # node embeddings
            hs = [self.atom_encoder(x)]
            for layer in range(self.n_layer):
                h = self.convs[layer](hs[layer], edge_index, edge_attr)
                h = self.bns[layer](h)
                if layer < self.n_layer - 1:
                    h = F.relu(h)
                if self.residual:
                    h += hs[layer]
                hs += [h]
            # graph embedding and prediction
            if self.jk:
                h = torch.cat([h for h in hs], -1)
            h_out = self.pool(h, batch)
            return h_out
    from alibi_detect.cd import MMDDrift
    
    enc = Encoder(n_layer=1).to(device)
    preprocess_fn = partial(predict_batch, model=enc, device=device, preprocess_fn=batch_fn, batch_size=32)
    dd = MMDDrift(x_ref, backend='pytorch', p_val=.05, n_permutations=1000, preprocess_fn=preprocess_fn)
    make_predictions(dd, xs)
    from alibi_detect.cd import LearnedKernelDrift
    from alibi_detect.utils.pytorch import DeepKernel
    
    kernel = DeepKernel(enc, kernel_b=None)  # use the already defined random encoder in the deep kernel
    dd = LearnedKernelDrift(x_ref, kernel, backend='pytorch', p_val=.05, dataloader=DataLoader, 
                            preprocess_batch_fn=batch_fn, epochs=2)
    make_predictions(dd, xs)
    # return number of nodes, edges and average clustering coefficient per graph
    def graph_stats(data: List[Data]) -> np.ndarray:
        num_nodes = np.array([d.num_nodes for d in data])
        num_edges = np.array([d.num_edges for d in data])
        c = np.array([np.array(list(clustering(to_networkx(d)).values())).mean() for d in data])
        return np.concatenate([num_nodes[:, None], num_edges[:, None], c[:, None]], axis=-1)
    dd = KSDrift(x_ref, p_val=.05, preprocess_fn=graph_stats)
    make_predictions(dd, xs)
    !pip install torch statsmodels
    import numpy as np
    import torch
    
    def set_seed(seed: int) -> None:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        np.random.seed(seed)
    
    set_seed(2022)
    from alibi_detect.datasets import fetch_ecg
    import matplotlib.pyplot as plt
    
    (x_train, y_train), (x_test, y_test) = fetch_ecg(return_X_y=True)
    y_train -= 1  # classes start at 1
    y_test -= 1
    x_all = np.concatenate([x_train, x_test], 0)
    y_all = np.concatenate([y_train, y_test], 0)
    
    n_total = x_train.shape[0] + x_test.shape[0]
    n_class = len(np.unique(y_test))
    x_by_class = {c: [] for c in range(n_class)}
    
    # check number of instances per class
    for c in range(n_class):
        idx_tr, idx_te = np.where(y_train == c)[0], np.where(y_test == c)[0]
        x_c = np.concatenate([x_train[idx_tr], x_test[idx_te]], axis=0)
        x_by_class[c] = x_c
    
    # plot breakdown of all instances
    plt.figure(figsize=(14,7))
    labels = ['N','Q','V','S','F']
    plt.pie([v.shape[0] for v in x_by_class.values()], labels=labels, 
             colors=['red','green','blue','skyblue','orange'], autopct='%1.1f%%')
    p = plt.gcf()
    p.gca().add_artist(plt.Circle((0,0), 0.7, color='white'))
    plt.title(f'Breakdown of all {n_total} instances by type of heartbeat')
    plt.show()
    
    # visualise an instance from each class
    for k, v in x_by_class.items():
        plt.plot(v[0], label=labels[k])
    plt.title('ECGs of Different Classes')
    plt.xlabel('Time step')
    plt.legend()
    plt.show()
    def split_data(x, y, n1, n2, seed=None):
            
        if seed:
            np.random.seed(seed)
        
        # split data by class
        cs = np.unique(y)
        n_c = len(np.unique(y))
        idx_c = {_: np.where(y == _)[0] for _ in cs}
    
        # convert nb instances per class to a list if needed
        n1_c = [n1] * n_c if isinstance(n1, int) else n1
        n2_c = [n2] * n_c if isinstance(n2, int) else n2
        
        # sample reference, test and held out data
        idx1, idx2 = [], []
        for _, c in enumerate(cs):
            idx = np.random.choice(idx_c[c], size=len(idx_c[c]), replace=False)
            idx1.append(idx[:n1_c[_]])
            idx2.append(idx[n1_c[_]:n1_c[_] + n2_c[_]])
        idx1 = np.concatenate(idx1)
        idx2 = np.concatenate(idx2)    
        x1, y1 = x[idx1], y[idx1]
        x2, y2 = x[idx2], y[idx2]
        return (x1, y1), (x2, y2)
    prop_train = .15
    n_train_c = [int(prop_train * len(v)) for v in x_by_class.values()]
    n_train_c[2], n_train_c[4] = 0, 0  # remove F and V classes from the training data
    # the remainder of the data is used by the drift detectors
    n_drift_c = [len(v) - n_train_c[_] for _, v in enumerate(x_by_class.values())]
    (x_train, y_train), (x_drift, y_drift) = split_data(x_all, y_all, n_train_c, n_drift_c, seed=0)
    print('train:', x_train.shape, 'drift detection:', x_drift.shape)
    import torch.nn as nn
    import torch.nn.functional as F
    
    
    class Classifier(nn.Module):
        def __init__(self, dim_in: int = 140, dim_hidden: int = 128, dim_out: int = 5) -> None:
            super().__init__()
            self.lin_in = nn.Linear(dim_in, dim_hidden)
            self.bn1 = nn.BatchNorm1d(dim_hidden)
            self.lin_hidden = nn.Linear(dim_hidden, dim_hidden)
            self.bn2 = nn.BatchNorm1d(dim_hidden)
            self.lin_out = nn.Linear(dim_hidden, dim_out)
        
        def forward(self, x: torch.Tensor) -> torch.Tensor:
            x = F.leaky_relu(self.bn1(self.lin_in(x)))
            x = F.leaky_relu(self.bn2(self.lin_hidden(x)))
            return self.lin_out(x)
    from torch.utils.data import TensorDataset, DataLoader
    from alibi_detect.models.pytorch import trainer
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    ds_train = TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train).long())
    dl_train = DataLoader(ds_train, batch_size=32, shuffle=True, drop_last=True)
    model = Classifier().to(device)
    trainer(model, nn.CrossEntropyLoss(), dl_train, device, torch.optim.Adam, learning_rate=.001, epochs=5)
    model.eval()
    with torch.no_grad():
        y_pred_train = model(torch.from_numpy(x_train).to(device)).argmax(-1).cpu().numpy()
        y_pred_drift = model(torch.from_numpy(x_drift).to(device)).argmax(-1).cpu().numpy()
    acc_train = (y_pred_train == y_train).mean()
    acc_drift = (y_pred_drift == y_drift).mean()
    print(f'Model accuracy: train {acc_train:.2f} - drift {acc_drift:.2f}')
    from scipy.special import softmax
    
    
    def context(x: np.ndarray) -> np.ndarray:
        """ Condition on classifier prediction probabilities. """
        model.eval()
        with torch.no_grad():
            logits = model(torch.from_numpy(x).to(device)).cpu().numpy()
        return softmax(logits, -1)
    from alibi_detect.cd import MMDDrift, ContextMMDDrift
    from tqdm import tqdm
    
    n_ref, n_test = 200, 200
    n_drift = x_drift.shape[0]
    
    # filter out classes not in training set
    idx_filter = np.concatenate([np.where(y_drift == _)[0] for _ in np.unique(y_train)])
    
    n_runs = 300  # number of drift detection runs, each with a different reference and test sample
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        idx = np.random.choice(idx_filter, size=len(idx_filter), replace=False)
        idx_ref, idx_test = idx[:n_ref], idx[n_ref:n_ref+n_test]
        x_ref = x_drift[idx_ref]
        x_test = x_drift[idx_test]
        
        # mmd drift detector
        dd_mmd = MMDDrift(x_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(x_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector 
        c_ref = context(x_ref)
        c_test = context(x_test)
        dd_cad = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(x_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    import statsmodels.api as sm
    from scipy.stats import uniform
    
    
    def plot_p_val_qq(p_vals: np.ndarray, title: str) -> None:
        fig, axes = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(12,10))
        fig.suptitle(title)
        n = len(p_vals)
        for i in range(9):
            unifs = p_vals if i==4 else np.random.rand(n)
            sm.qqplot(unifs, uniform(), line='45', ax=axes[i//3,i%3])
            if i//3 < 2:
                axes[i//3,i%3].set_xlabel('')
            if i%3 != 0:
                axes[i//3,i%3].set_ylabel('')
    plot_p_val_qq(p_vals_mmd, 'Q-Q plot MMD detector')
    plot_p_val_qq(p_vals_cad, 'Q-Q plot Context-Aware MMD detector')
    n_ref_c = 400
    # only 3 classes in train set and class 0 contains the normal heartbeats
    n_test_c = [200, 0, 0]
    
    x_c_train, y_c_train = x_drift[idx_filter], y_drift[idx_filter]
    
    n_runs = 300
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        (x_ref, y_ref), (x_test, y_test) = split_data(x_c_train, y_c_train, n_ref_c, n_test_c, seed=_)
        
        # mmd drift detector
        dd_mmd = MMDDrift(x_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(x_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector 
        c_ref = context(x_ref)
        c_test = context(x_test)
        dd_cad = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(x_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    plot_p_val_qq(p_vals_mmd, 'Q-Q plot MMD detector')
    plot_p_val_qq(p_vals_cad, 'Q-Q plot Context-Aware MMD detector')
    i = 0
    
    plt.plot(x_train[i], label='original')
    plt.plot(x_train[i] + np.random.normal(size=140), label='noise')
    plt.title('Original vs. perturbed ECG')
    plt.xlabel('Time step')
    plt.legend()
    plt.show()
    noise_frac = .5  # 50% of the test set samples are corrupted, the rest stays in-distribution
    
    n_runs = 300
    
    p_vals_cad = []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        (x_ref, y_ref), (x_test, y_test) = split_data(x_c_train, y_c_train, n_ref_c, n_test_c, seed=_)
        
        # perturb a fraction of the test data
        n_test, n_features = x_test.shape
        n_noise = int(noise_frac * n_test)
        x_noise = np.random.normal(size=n_noise * n_features).reshape(n_noise, n_features)
        idx_noise = np.random.choice(n_test, size=n_noise, replace=False)
        x_test[idx_noise] += x_noise
        
        # cad drift detector 
        c_ref = context(x_ref)
        c_test = context(x_test)
        dd_cad = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(x_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_cad = np.array(p_vals_cad)
    threshold = .05
    print(f'Power at {threshold * 100}% significance level')
    print(f'Context-aware MMD: {(p_vals_cad < threshold).mean():.3f}')
    
    plot_p_val_qq(p_vals_cad, 'Q-Q plot Context-Aware MMD detector')
    from sklearn.mixture import GaussianMixture
    
    n_clusters = 2  # normal heartbeats + S/Q which look fairly similar as illustrated earlier
    gmm = GaussianMixture(n_components=n_clusters, covariance_type='full', random_state=2022)
    gmm.fit(x_train)
    # compute all contexts
    c_all_proba = gmm.predict_proba(x_drift)
    c_all_class = gmm.predict(x_drift)
    n_ref_c = [200, 200]
    n_test_c = [100, 25]
    
    def sample_from_clusters():
        idx_ref, idx_test = [], []
        for _, (i_ref, i_test) in enumerate(zip(n_ref_c, n_test_c)):
            idx_c = np.where(c_all_class == _)[0]
            idx_shuffle = np.random.choice(idx_c, size=len(idx_c), replace=False)
            idx_ref.append(idx_shuffle[:i_ref])
            idx_test.append(idx_shuffle[i_ref:i_ref+i_test])
        idx_ref = np.concatenate(idx_ref, 0)
        idx_test = np.concatenate(idx_test, 0)
        c_ref = c_all_proba[idx_ref]
        c_test = c_all_proba[idx_test]
        x_ref = x_drift[idx_ref]
        x_test = x_drift[idx_test]
        return c_ref, c_test, x_ref, x_test
    #| scrolled: true
    n_runs = 300
    
    p_vals_null, p_vals_new = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        c_ref, c_test_null, x_ref, x_test_null = sample_from_clusters()
            
        # previously unseen classes
        x_test_new = np.concatenate([x_drift[y_drift == 2], x_drift[y_drift == 4]], 0)
        c_test_new = gmm.predict_proba(x_test_new)
        
        # detect drift
        dd = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_null = dd.predict(x_test_null, c_test_null)
        preds_new = dd.predict(x_test_new, c_test_new)
        p_vals_null.append(preds_null['data']['p_val'])
        p_vals_new.append(preds_new['data']['p_val'])
        
    p_vals_null = np.array(p_vals_null)
    p_vals_new = np.array(p_vals_new)
    plot_p_val_qq(p_vals_null, 'Q-Q plot Context-Aware MMD detector when changing the subpopulation prevalence')
    threshold = .05
    print(f'Power at {threshold * 100}% significance level')
    print(f'Context-aware MMD on F and V classes: {(p_vals_new < threshold).mean():.3f}')
    n_ref_c = 400
    n_test_c = [200, 0, 0]
    
    (x_ref, y_ref), (x_test, y_test) = split_data(x_c_train, y_c_train, n_ref_c, n_test_c)
    
    # condition using the model pred
    c_ref = context(x_ref)
    c_test = context(x_test)
    
    # initialise detector and make predictions
    dd = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
    preds = dd.predict(x_test, c_test, return_coupling=True)
    
    # no drift is detected since the distribution of 
    # the subpopulations in the test set remain the same
    print(f'p-value: {preds["data"]["p_val"]:.3f}')
    
    # extract coupling matrix between reference and test data
    W_01 = preds['data']['coupling_xy']
    
    # sum over test instances
    w_ref = W_01.sum(1)
    inds_ref_sort = np.argsort(w_ref)[::-1]
    plt.plot(w_ref[inds_ref_sort]);
    plt.title('Sorted reference weights from the coupling matrix W_01');
    plt.ylabel('Reference instance weight in W_01');
    plt.xlabel('Instances sorted by weight in W_01');
    plt.show()
    # filter out normal heartbeats
    idx_normal = np.where(y_drift == 0)[0]
    x_normal, y_normal = x_drift[idx_normal], y_drift[idx_normal]
    n_normal = len(x_normal)
    
    # determine segment length and starting points in each original ECG
    segment_len = 40
    n_segments = 3
    max_start = n_features - n_segments * segment_len
    idx_start = np.random.choice(max_start, size=n_normal, replace=True)
    
    # split original ECGs in segments
    x_split = np.concatenate(
        [
            np.concatenate(
                [x_normal[_, idx+i*segment_len:idx+(i+1)*segment_len][None, :] for i in range(n_segments)], 0
            ) for _, idx in enumerate(idx_start)
        ], 0
    )
    
    # time-varying context, standardised
    c_split = np.repeat(idx_start, n_segments).astype(np.float32)
    c_add = np.tile(np.array([i*segment_len for i in range(n_segments)]), len(idx_start)).astype(np.float32)
    c_split += c_add
    c_split = (c_split - c_split.mean()) / c_split.std()
    c_split = c_split[:, None]
    n_ref = 500
    n_test = 500
    
    mismatch_frac = .4  # fraction of instances where the time stamps are incorrect given the segment
    n_mismatch = int(mismatch_frac * n_test)
    
    n_runs = 300
    
    p_vals_null, p_vals_alt = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        # no change
        idx = np.random.choice(n_normal, size=n_normal, replace=False)    
        idx_ref, idx_test = idx[:n_ref], idx[n_ref:n_ref+n_test]
        x_ref = x_split[idx_ref]
        x_test_null = x_split[idx_test]
        x_test_alt = x_test_null
        
        # context
        c_ref, c_test_null = c_split[idx_ref], c_split[idx_test]
        
        # mismatched time stamps
        c_test_alt = c_test_null.copy()
        idx_mismatch = np.random.choice(n_test-1, size=n_mismatch, replace=False)
        c_test_alt[idx_mismatch] = c_test_alt[idx_mismatch+1]  # shift 1 spot to the right
        
        # detect drift
        dd = ContextMMDDrift(x_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_null = dd.predict(x_test_null, c_test_null)
        preds_alt = dd.predict(x_test_alt, c_test_alt)
        p_vals_null.append(preds_null['data']['p_val'])
        p_vals_alt.append(preds_alt['data']['p_val'])
        
    p_vals_null = np.array(p_vals_null)
    p_vals_alt = np.array(p_vals_alt)
    #| scrolled: false
    plot_p_val_qq(p_vals_null, 'Q-Q plot Context-Aware MMD detector under no change')
    threshold = .05
    print(f'Power at {threshold * 100}% significance level')
    print(f'Context-aware MMD with mismatched time stamps: {(p_vals_alt < threshold).mean():.3f}')

    Drift Detection

    1. What is drift?

    Although powerful, modern machine learning models can be sensitive. Seemingly subtle changes in a data distribution can destroy the performance of otherwise state-of-the art models, which can be especially problematic when ML models are deployed in production. Typically, ML models are tested on held out data in order to estimate their future performance. Crucially, this assumes that the process underlying the input data $\mathbf{X}$ and output data $\mathbf{Y}$ remains constant.

    Drift is said to occur when the process underlying $\mathbf{X}$ and $\mathbf{Y}$ at test time differs from the process that generated the training data. In this case, we can no longer expect the model’s performance on test data to match that observed on held out training data. At test time we always observe features $\mathbf{X}$, and the ground truth then refers to a corresponding label $\mathbf{Y}$. If ground truths are available at test time, supervised drift detection can be performed, with the model’s predictive performance monitored directly. However, in many scenarios, such as the binary classification example below, ground truths are not available and unsupervised drift detection methods are required.

    To explore the different types of drift, consider the common scenario where we deploy a model $f: \boldsymbol{x} \mapsto y$ on input data $\mathbf{X}$ and output data $\mathbf{Y}$, jointly distributed according to $P(\mathbf{X},\mathbf{Y})$. The model is trained on training data drawn from a distribution $P_{ref}(\mathbf{X},\mathbf{Y})$. Drift is said to have occurred when $P(\mathbf{X},\mathbf{Y}) \ne P_{ref}(\mathbf{X},\mathbf{Y})$. Writing the joint distribution as

    we can classify drift under a number of types:

    • Covariate drift: Also referred to as input drift, this occurs when the distribution of the input data has shifted $P(\mathbf{X}) \ne P_{ref}(\mathbf{X})$, whilst $P(\mathbf{Y}|\mathbf{X})$ = $P_{ref}(\mathbf{Y}|\mathbf{X})$. This may result in the model giving unreliable predictions.

    • Prior drift: Also referred to as label drift, this occurs when the distribution of the outputs has shifted $P(\mathbf{Y}) \ne P_{ref}(\mathbf{Y})$, whilst $P(\mathbf{X}|\mathbf{Y})=P_{ref}(\mathbf{X}|\mathbf{Y})$. This can affect the model's decision boundary, as well as the model's performance metrics.

    • Concept drift: This occurs when the process generating $y$ from $x$ has changed, such that $P(\mathbf{Y}|\mathbf{X}) \ne P_{ref}(\mathbf{Y}|\mathbf{X})$. It is possible that the model might no longer give a suitable approximation of the true process.

    Note that a change in one of the conditional probabilities $P(\mathbf{X}|\mathbf{Y})$ and $P(\mathbf{Y}|\mathbf{X})$ does not necessarily imply a change in the other. For example, consider the pneumonia prediction example from , whereby a classification model $f$ is trained to predict $y$, the occurrence (or not) of pneumonia, given a list of symptoms $\boldsymbol{x}$. During a pneumonia outbreak, $P(\mathbf{Y}|\mathbf{X})$ (e.g. pneumonia given cough) might rise, but the manifestations of the disease $P(\mathbf{X}|\mathbf{Y})$ might not change. In many cases, knowledge of underlying causal structure of the problem can be used to deduce that one of the conditionals will remain unchanged.

    Below, the different types of drift are visualised for a simple two-dimensional classification problem. It is possible for a drift to fall under more than one category, for example the prior drift below also happens to be a case of covariate drift.

    It is relatively easy to spot drift by eyeballing these figures here. However, the task becomes considerably harder for high-dimensional real problems, especially since real-time ground truths are not typically available. Some types of drift, such as prior and concept drift, are especially difficult to detect without access to ground truths. As a workaround proxies are required, for example a model’s predictions can be monitored to check for prior drift.

    2. Detecting drift

    offers a wide array of methods for detecting drift (see ), some of which are examined in the NeurIPS 2019 paper . Generally, these aim to determine whether the distribution $P(\mathbf{z})$ has drifted from a reference distribution $P_{ref}(\mathbf{z})$, where $\mathbf{z}$ may represent input data $\mathbf{X}$, true output data $\mathbf{Y}$, or some form of model output, depending on what type of drift we wish to detect.

    Due to natural randomness in the process being modelled, we don’t necessarily expect observations $\mathbf{z}1,\dots,\mathbf{z}N$ drawn from $P(\mathbf{z})$ to be identical to $\mathbf{z}^{ref}1,\dots,\mathbf{z}^{ref}M$ drawn from $P{ref}(\mathbf{z})$. To decide whether differences between $P(\mathbf{z})$ and $P{ref}(\mathbf{z})$ are due to drift or just natural randomness in the data, statistical two-sample hypothesis testing is used, with the null hypothesis $P(\mathbf{z})=P{ref}(\mathbf{z})$. If the $p$-value obtained is below a given threshold, the null is rejected and the alternative hypothesis $P(\mathbf{z}) \ne P{ref}(\mathbf{z})$ is accepted, suggesting drift is occurring.

    Since $\mathbf{z}$ is often high-dimensional (even a 200 x 200 greyscale image has 40k dimensions!), performing hypothesis testing in the full-space is often either computationally intractable, or unsuitable for the chosen statistical test. Instead, the pipeline below is often used, with dimension reduction as a pre-processing step.

    :::{figure} images/drift_pipeline.png :align: center :alt: Drift detection pipeline

    Figure inspired by Figure 1 in Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift. :::

    Hypothesis testing

    Hypothesis testing involves first choosing a test statistic $S(\mathbf{z})$, which is expected to be small if the null hypothesis $H_0$ is true, and large if the alternative $H_a$ is true. For observed data $\mathbf{z}$, $S(\mathbf{z})$ is computed, followed by a $p$-value $\hat{p} = P(\text{such an extreme } S(\mathbf{z}) | H_0)$. In other words, $\hat{p}$ represents the probability of such an extreme value of $S(\mathbf{z})$ occurring given that $H_0$ is true. When $\hat{p}\le \alpha$, results are said to be statistically significant, and the null $P(\mathbf{z})=P_{ref}(\mathbf{z})$ is rejected. Conveniently, the threshold $\alpha$ represents the desired false positive rate.

    The test statistics available in can be broadly split into two categories; univariate and multivariate tests:

    • Univariate:

      • (for categorical data)

    When applied to multidimensional data with dimension $d$, the univariate tests are applied in a feature-wise manner. The obtained $p$-values for each feature are aggregated either via the or the (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur. If the tests (i.e. each feature dimension) are independent, these corrections preserve the desired false positive rate (FPR). However, usually this is not the case, resulting in FPR's up to $d$-times lower than desired, which becomes especially problematic when $d$ is large. Additionally, since the univariate tests examine the feature-wise marginal distributions, they may miss drift in cases where the joint distribution over all $d$ features has changed, but the marginals have not. The multivariate tests avoid these problems, at the cost of greater complexity.

    Dimension reduction

    Given an input dataset $\mathbf{X}\in \mathbb{R}^{N\times d}$, where $N$ is the number of observations and $d$ the number of dimensions, the aim is to reduce the data dimensionality from $d$ to $K$, where $K\ll d$. A drift detector can then be applied to the lower dimensional data $\hat{\mathbf{X}}\in \mathbb{R}^{N\times K}$, where distances more meaningfully capture notions of similarity/dissimilarity between instances. Dimension reduction approaches can be broadly categorised under:

    1. Linear projections

    2. Non-linear projections

    3. Feature maps (from ML model)

    4. Model uncertainty

    allows for a high degree of flexibility here, with a user’s chosen dimension reduction technique able to be incorporated into their chosen detector via the preprocess_fn argument (and sometimes preprocess_batch_fn and preprocess_at_init, depending on the detector). In the following sections, the three categories of techniques are briefly introduced. Alibi Detect offers the following functionality using either or backends and preprocessing utilities. For more details, see the .

    Linear projections

    This includes dimension reduction techniques such as and . These techniques involve using a transformation or projection matrix $\mathbf{R}$ to reduce the dimensionality of a given data matrix $\mathbf{X}$, such that $\hat{\mathbf{X}} = \mathbf{XR}$. A straightforward way to include such techniques as a pre-processing stage is to pass them to the detectors via the preprocess_fn argument, for example for the scikit-learn library’s PCA class:

    :::{admonition} Note 1: Disjoint training and reference data sets

    Astute readers may have noticed that in the code snippet above, the data X_train is used to “train” the PCA model, but the MMDDrift detector is initialised with X_ref. This is a subtle yet important point. If a detector’s preprocessor (a or other step) is trained on the reference data (X_ref), any over-fitting to this data may make the resulting detector overly sensitive to differences between the reference and test data sets.

    To avoid an overly discriminative detector, it is customary to draw two disjoint datasets from $P_{ref}(\mathbf{z})$, a training set and a held-out reference set. The training data is used to train any input preprocessing steps, and the detector is then initialised on the reference set, and used to detect drift between the reference and test set. This also applies to the , which should be trained on the training set not the reference set. :::

    Non-linear projections

    A common strategy for obtaining non-linear dimension reducing representations is to use an autoencoder, but other can also be used. Autoencoders consist of an encoder function $\phi : \mathcal{X} \mapsto \mathcal{H}$ and a decoder function $\psi : \mathcal{H} \mapsto \mathcal{X}$, where the latent space $\mathcal{H}$ has lower dimensionality than the input space $\mathcal{X}$. The output of the encoder $\hat{\mathbf{X}} \in \mathcal{H}$ can then be monitored by the drift detector. Training involves learning both the encoding function $\phi$ and the decoding function $\psi$, in order to reduce the reconstruction loss, e.g. if MSE is used: $\phi, \psi = \text{arg} \min_{\phi, \psi}, \lVert \mathbf{X}-(\phi \circ \psi)\mathbf{X}\rVert^2$. However, untrained (randomly initialised) autoencoders can also be used. For an example, a pytorch autoencoder can be incorporated into a detector by packaging it as a callable function using {func}~alibi_detect.cd.pytorch.preprocess.preprocess_drift and {func}~functools.partial:

    Feature maps

    Following , feature maps can be extracted from existing pre-trained black-box models such as the image classifier shown below. Instead of using the latent space as the dimensionality-reducing representation, other layers of the model such as the softmax outputs or predicted class-labels can also be extracted and monitored. Since different layers yield different output dimensions, different hypothesis tests are required for each.

    :::{figure} images/BBSD.png :align: center :alt: Black box shift detection

    Figure inspired by this MNIST classification example from the timeserio package. :::

    shows that extracting feature maps from existing models can be an effective technique, which is encouraging since this allows the user to repurpose existing black-box models for use as drift detectors. The syntax for incorporating existing models into drift detectors is similar to the previous autoencoder example, with the added step of using {class}~alibi_detect.cd.tensorflow.preprocess.HiddenOutput to select the model’s network layer to extract outputs from. The code snippet below is borrowed from , where the softmax layer of the well-known model is fed into an MMDDrift detector.

    Model uncertainty

    The -based drift detector uses the ML model of interest itself to detect drift. These detectors aim to directly detect drift that’s likely to affect the performance of a model of interest. The approach is to test for change in the number of instances falling into regions of the input space on which the model is uncertain in its predictions. For each instance in the reference set the detector obtains the model’s prediction and some associated notion of uncertainty. The same is done for the test set and if significant differences in uncertainty are detected (via a Kolmogorov-Smirnoff test) then drift is flagged. The model’s notion of uncertainty depends on the type of model. For a classifier this may be the entropy of the predicted label probabilities. For a regressor with dropout layers, can be used to provide a notion of uncertainty.

    The model uncertainty-based detectors are classed under the dimension reduction category since a model's uncertainty is by definition one-dimensional. However, the syntax for the uncertainty-based detectors is different to the other detectors. Instead of passing a pre-processing step to a detector via a preprocess_fn (or similar) argument, the dimension reduction (in this case computing a notion of uncertainty) is performed internally by these detectors.

    Input preprocessing

    Dimension reduction is a common preprocessing task (e.g. for covariate drift detection on tabular or image data), but some modalities of data (e.g. text and graph data) require other forms of preprocessing in order for drift detection to be performed effectively.

    Text data

    When dealing with text data, performing drift detection on raw strings or tokenized data is not effective since they don’t represent the semantics of the input. Instead, we extract contextual embeddings from language transformer models and detect drift on those. This procedure has a significant impact on the type of drift we detect. Strictly speaking we are not detecting covariate/input drift anymore since the entire training procedure (objective function, training data etc) for the (pre)trained embeddings has an impact on the embeddings we extract.

    :::{figure} images/BERT.png :align: center :alt: The DistilBERT language representation model

    Figure based on Jay Alammar’s excellent to the BERT model :::

    Alibi Detect contains functionality to leverage pre-trained embeddings from HuggingFace’s package. Popular models such as or (shown above) can be used, but Alibi Detect also allows you to easily use your own embeddings of choice. A subsequent dimension reduction step can also be applied if necessary, as is done in the example, where the 768-dimensional embeddings from the BERT model are passed through an untrained AutoEncoder to reduce their dimensionality. Alibi Detect allows various types of embeddings to be extracted from transformer models, using {class}~alibi_detect.models.tensorflow.embedding.TransformerEmbedding.

    Graph data

    In a similar manner to text data, graph data requires preprocessing before drift detection can be performed. This can be done by extracting graph embeddings from graph neural network (GNN) encoders, as shown below, and demonstrated in the example.

    Simple example

    For a simple example, we’ll use the to check for drift on the two-dimensional binary classification problem shown previously (see ). The MMD detector is a kernel-based method for multivariate two sample testing. Since the number of dimensions is already low, dimension reduction step is not necessary here here. For a more advanced example using the with dimension reduction, check out the example.

    The true model/process is defined as:

    where the slope $s$ is set as $s=-1$.

    The reference distribution is defined as a mixture of two Normal distributions:

    with the standard deviation set at $\sigma=0.8$, and the weights set to $\phi_1=\phi_2=0.5$. Reference data $\mathbf{X}^{ref}$ and training data $\mathbf{X}^{train}$ (see Note 1) can be generated by sampling from this distribution. The corresponding labels $\mathbf{Y}^{ref}$ and $\mathbf{Y}^{train}$ are obtained by evaluating true_model().

    For a model, we choose the well-known decision tree classifier. As well as training the model, this is a good time to initialise the MMD detector with the held-out reference data $\mathbf{X}^{ref}$ by calling:

    The significance threshold is set at $\alpha=0.05$, meaning the detector will flag results as drift detected when the computed $p$-value is less than this i.e. $\hat{p}< \alpha$.

    No drift

    Before introducing drift, we first examine the case where no drift is present. We resample from the same mixture of Gaussian distributions to generate test data $\mathbf{X}$. The individual data observations are different, but the underlying distributions are unchanged, hence no true drift is present.

    Unsurprisingly, the model’s mean test accuracy is relatively high. To run the detector on test data the .predict() is used:

    For the test statistic $S(\mathbf{X})$ (we write $S(\mathbf{X})$ instead of $S(\mathbf{z})$ since the detector is operating on input data), the MMD detector uses the to compute unbiased estimates of $\text{MMD}^2$. The $\text{MMD}$ is a distance-based measure between the two distributions $P$ and $P_{ref}$, based on the mean embeddings $\mu$ and $\mu_{ref}$ in a reproducing kernel Hilbert space $F$:

    A $p$-value is then obtained via a on the estimates of $\text{MMD}^2$. As expected, since we are sampling from the reference distribution $P_{ref}(\mathbf{X})$, the detector’s prediction is 'is_drift':0 here, indicating that drift is not detected. More specifically, the detector’s $p$-value (p_val) is above the threshold of $\alpha=0.05$ (threshold), indicating that no statistically significant drift has been detected. The .predict() method also returns $\hat{S}(\mathbf{X})$ (distance_threshold), which is the threshold in terms of the test statistic $S(\mathbf{X})$ i.e. when $S(\mathbf{X})\ge \hat{S}(\mathbf{X})$ statistically significant drift has been detected.

    Covariate and prior drift

    To impose covariate drift, we apply a shift to the mean of one of the normal distributions:

    The test data has drifted into a previously unseen region of feature space, and the model is now misclassifying a number of test observations. If true test labels are available, this is easily detectable by monitoring the test accuracy. However, labels are not always available at test time, in which case a drift detector monitoring the covariates comes in handy. In this case, the MMD detector successfully detects the covariate drift.

    In a similar manner, a proxy for prior drift can be monitored by initialising a detector on labels from the reference set, and then feeding it a model’s predicted labels:

    3. Learned drift detection

    It can often be challenging to specify a test statistic $S(\mathbf{z})$ that is large when drift is present and small otherwise. Alibi Detect offers a number of learned detectors, which try to explicitly learn a test statistic which satisfies this property:

    • (erence)

    These detectors can be highly effective, but require training hence potentially increasing data requirements and set-up time. Similarly to when training preprocessing steps, it is important that the learned detectors are trained on training data which is held-out from the reference data set (see Note 1). A brief overview of these detectors is given below. For more details, see the detectors’ respective pages.

    Learned kernel

    The uses a kernel $k(\mathbf{z},\mathbf{z}^{ref})$ to compute unbiased estimates of $\text{MMD}^2$. The user is free to provide their own kernel, but by default a is used. The drift detector () extends this approach by training a kernel to maximise an estimate of the resulting test power. The learned kernel is defined as

    where $\Phi$ is a learnable projection, $k_a$ and $k_b$ are simple characteristic kernels (such as a Gaussian RBF), and $\epsilon>0$ is a small constant. By letting $\Phi$ be very flexible we can learn powerful kernels in this manner.

    The figure below compares the use of a Gaussian and a learned kernel for identifying differences between two distributions $P$ and $P_{ref}$. The distributions are each equal mixtures of nine Gaussians with the same modes, but each component of $P_{ref}$ is an isotropic Gaussian, whereas the covariance of $P$ differs in each component. The Gaussian kernel (c) treats points isotropically throughout the space, based upon $\lVert \mathbf{z} - \mathbf{z}^{ref} \rVert$ only. The learned kernel (d) behaves differently in different regions of the space, adapting to local structure and therefore allowing better detection of differences between $P$ and $P_{ref}$.

    :::{figure} images/deep_kernel.png :align: center :alt: Gaussian and deep kernels

    Original image source: Liu et al., 2020. Captions modified to match notation used elsewhere on this page. :::

    Classifier

    The -based drift detector () attempts to detect drift by explicitly training a classifier to discriminate between data from the reference and test sets. The statistical test used depends on whether the classifier outputs probabilities or binarized (0 or 1) predictions, but the general idea is to determine whether the classifiers performance is statistically different from random chance. If the classifier can learn to discriminate better than randomly (in a generalisable manner) then drift must have occurred.

    show that a classifier-based drift detector is actually a special case of the . An important difference is that to train a classifier we maximise its accuracy (or a cross-entropy proxy), while for a learned kernel we maximise the test power directly. Liu et al. show that the latter approach is empirically superior.

    Spot-the-diff

    The (erence) drift detector is an extension of the drift detector, where the classifier is specified in a manner that makes detections interpretable at the feature level when they occur. The detector is inspired by the work of but various major adaptations have been made.

    As with the usual classifier-based approach, a portion of the available data is used to train a classifier that can discriminate reference instances from test instances. However, the spot-the-diff detector is specified such that when drift is detected, we can inspect the weights of the classifier to shine light on exactly which features of the data were used to distinguish reference from test samples, and therefore caused drift to be detected. The example demonstrates this capability.

    4. Online drift detection

    So far, we have discussed drift detection in an offline context, with the entire test set ${\mathbf{z}i}{i=1}^{N}$ compared to the reference dataset ${\mathbf{z}^{ref}i}{i=1}^{M}$. However, at test time, data sometimes arrives sequentially. Here it is desirable to detect drift in an online fashion, allowing us to respond as quickly as possible and limit the damage it might cause.

    One approach is to perform a test for drift every $W$ time-steps, using the $W$ samples that have arrived since the last test. In other words, that is to compare ${\mathbf{z}i}{i=t-W+1}^{t}$ to ${\mathbf{z}^{ref}i}{i=1}^{M}$. Such a strategy could be implemented using any of the offline detectors implemented in alibi-detect, but being both sensitive to slight drift and responsive to severe drift is difficult. If the window size $W$ is too large the delay between consecutive statistical tests hampers responsiveness to severe drift, but an overly small window is unreliable. This is demonstrated below, where the offline is used to monitor drift in data $\mathbf{X}$ sampled from a normal distribution $\mathcal{N}\left(\mu,\sigma^2 \right)$ over time $t$, with the mean starting to drift from $\mu=0$ to $\mu=0.5$ at $t=40$.

    Online drift detectors

    An alternative strategy is to perform a test each time data arrives. However the usual offline methods are not applicable because the process for computing $p$-values is too expensive. Additionally, they don’t account for correlated test outcomes when using overlapping windows of test data, leading to miscalibrated detectors operating at an unknown False Positive Rate (FPR). Well-calibrated FPR’s are crucial for judging the significance of a drift detection. In the absence of calibration, drift detection can be useless since there is no way of knowing what fraction of detections are false positives. To tackle this problem, offers specialist online drift detectors:

    These detectors leverage the calibration method introduced by in order to ensure they are well well-calibrated when used in a sequential manner. The detectors compute a test statistic $S(\mathbf{z})$ during the configuration phase. Then, at test time, the test statistic is updated sequentially at a low cost. When no drift has occurred the test statistic fluctuates around its expected value, and once drift occurs the test statistic starts to drift upwards. When it exceeds some preconfigured threshold value, drift is detected. The online detectors are constructed in a similar manner to the offline detectors, for example for the online MMD detector:

    But, in addition to providing the detector with reference data, the expected run-time (see below), and size of the sliding window must also be defined. Another important difference is that the online detectors make predictions on single data instances:

    This can be seen in the animation below, where the online detector considers each incoming observation/sample individually, instead of considering a batch of observations like the offline detectors.

    Expected run-time and detection delay

    Unlike offline detectors which require the specification of a threshold $p$-value, which is equivalent to a false positive rate (FPR), the online detectors in alibi-detect require the specification of an expected run-time (ERT) (an inverted FPR). This is the number of time-steps that we insist our detectors, on average, should run for in the absence of drift, before making a false detection.

    Usually we would like the ERT to be large, however this results in insensitive detectors which are slow to respond when drift does occur. Hence, there is a tradeoff between the expected run time and the expected detection delay (the time taken for the detector to respond to drift in the data). To target the desired ERT, thresholds are configured during an initial configuration phase via simulation (n_bootstraps sets the number of boostrap simulations used here). This configuration process is only suitable when the amount of reference data is relatively large (ideally around an order of magnitude larger than the desired ERT). Configuration can be expensive (less so with a GPU), but allows the detector to operate at a low-cost at test time. For a more in-depth explanation, see .

    Adversarial AE detection and correction on CIFAR-10

    Method

    The adversarial detector is based on Adversarial Detection and Correction by Matching Prediction Distributions. Usually, autoencoders are trained to find a transformation $T$ that reconstructs the input instance $x$ as accurately as possible with loss functions that are suited to capture the similarities between x and $x'$ such as the mean squared reconstruction error. The novelty of the adversarial autoencoder (AE) detector relies on the use of a classification model-dependent loss function based on a distance metric in the output space of the model to train the autoencoder network. Given a classification model $M$ we optimise the weights of the autoencoder such that the KL-divergence between the model predictions on $x$ and on $x'$ is minimised. Without the presence of a reconstruction loss term $x'$ simply tries to make sure that the prediction probabilities $M(x')$ and $M(x)$ match without caring about the proximity of $x'$ to $x$. As a result, $x'$ is allowed to live in different areas of the input feature space than $x$ with different decision boundary shapes with respect to the model $M$. The carefully crafted adversarial perturbation which is effective around x does not transfer to the new location of $x'$ in the feature space, and the attack is therefore neutralised. Training of the autoencoder is unsupervised since we only need access to the model prediction probabilities and the normal training instances. We do not require any knowledge about the underlying adversarial attack and the classifier weights are frozen during training.

    The detector can be used as follows:

    • An adversarial score $S$ is computed. $S$ equals the K-L divergence between the model predictions on $x$ and $x'$.

    • If $S$ is above a threshold (explicitly defined or inferred from training data), the instance is flagged as adversarial.

    • For adversarial instances, the model $M$ uses the reconstructed instance $x'$ to make a prediction. If the adversarial score is below the threshold, the model makes a prediction on the original instance $x$.

    This procedure is illustrated in the diagram below:

    The method is very flexible and can also be used to detect common data corruptions and perturbations which negatively impact the model performance.

    Dataset

    consists of 60,000 32 by 32 RGB images equally distributed over 10 classes.

    Note: in order to run this notebook, it is adviced to use Python 3.7 and have a GPU enabled.

    Utility functions

    Load data

    Standardise the dataset by instance:

    Load classifier

    Check that the predictions on the test set reach $93.15$% accuracy:

    Adversarial Attack

    We investigate both and attacks. You can simply load previously found adversarial instances on the pretrained ResNet-56 model. The attacks are generated by using :

    Check if the prediction accuracy of the model on the adversarial instances is close to $0$%.

    Let's visualise some adversarial instances:

    Load or train and evaluate the adversarial detectors

    We can again either fetch the pretrained detector from a or train one from scratch:

    The detector first reconstructs the input instances which can be adversarial. The reconstructed input is then fed to the classifier if the adversarial score for the instance is above the threshold. Let's investigate what happens when we reconstruct attacked instances and make predictions on them:

    Accuracy on attacked vs. reconstructed instances:

    The detector restores the accuracy after the attacks from almost $0$% to well over $80$%! We can compute the adversarial scores and inspect some of the reconstructed instances:

    The ROC curves and AUC values show the effectiveness of the adversarial score to detect adversarial instances:

    The threshold for the adversarial score can be set via infer_threshold. We need to pass a batch of instances $X$ and specify what percentage of those we consider to be normal via threshold_perc. Assume we have only normal instances some of which the model has misclassified leading to a higher score if the reconstruction picked up features from the correct class or some might look adversarial in the first place. As a result, we set our threshold at $95$%:

    Let's save the updated detector:

    We can also load it easily as follows:

    The correct method of the detector executes the diagram in Figure 1. First the adversarial scores is computed. For instances where the score is above the threshold, the classifier prediction on the reconstructed instance is returned. Otherwise the original prediction is kept. The method returns a dictionary containing the metadata of the detector, whether the instances in the batch are adversarial (above the threshold) or not, the classifier predictions using the correction mechanism and both the original and reconstructed predictions. Let's illustrate this on a batch containing some adversarial (C&W) and original test set instances:

    Let's check the model performance:

    This can be improved with the correction mechanism:

    Temperature Scaling

    We can further improve the correction performance by applying temperature scaling on the original model predictions $M(x)$ during both training and inference when computing the adversarial scores. We can again load a pretrained detector or train one from scratch:

    Applying temperature scaling to CIFAR-10 improves the ROC curve and AUC values.

    Hidden Layer K-L Divergence

    The performance of the correction mechanism can also be improved by extending the training methodology to one of the hidden layers of the classification model. We extract a flattened feature map from the hidden layer, feed it into a linear layer and apply the softmax function. The K-L divergence between predictions on the hidden layer for $x$ and $x'$ is optimised and included in the adversarial score during inference:

    Malicious Data Drift

    The adversarial detector proves to be very flexible and can be used to measure the harmfulness of the data drift on the classifier. We evaluate the detector on the CIFAR-10-C dataset (). The instances in CIFAR-10-C have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in model performance.

    We can select from the following corruption types:

    Fetch the CIFAR-10-C data for a list of corruptions at each severity level (from 1 to 5), make classifier predictions on the corrupted data, compute adversarial scores and identify which perturbations where malicious or harmful and which weren't. We can then store and visualise the adversarial scores for the harmful and harmless corruption. The score for the harmful perturbations is significantly higher than for the harmless ones. As a result, the adversarial detector also functions as a data drift detector.

    Compute mean scores and standard deviation per severity level and plot:

    Fisher's Exact Test (for binary data)

  • Multivariate:

    • Least-Squares Density Difference (LSDD)

    • Maximum Mean Discrepancy (MMD)

  • P(X,Y)=P(Y∣X)P(X)=P(X∣Y)P(Y),P(\mathbf{X},\mathbf{Y}) = P(\mathbf{Y}|\mathbf{X})P(\mathbf{X}) = P(\mathbf{X}|\mathbf{Y})P(\mathbf{Y}),P(X,Y)=P(Y∣X)P(X)=P(X∣Y)P(Y),
    y={1if x2>sx10otherwisey = \begin{cases} 1 & \text{if } x_2 > sx_1 \\ 0 & \text{otherwise} \end{cases}y={10​if x2​>sx1​otherwise​
    Pref(X)=ϕ1N([−1,−1]T,σ2I)+ϕ2N([1,1]T,σ2I)P_{ref}(\mathbf{X}) = \phi_1 \mathcal{N}\left(\left[-1,-1\right]^T, \sigma^2\mathbf{I} \right) + \phi_2 \mathcal{N}\left(\left[1,1\right]^T, \sigma^2\mathbf{I} \right)Pref​(X)=ϕ1​N([−1,−1]T,σ2I)+ϕ2​N([1,1]T,σ2I)
    MMD2(F,P,Pref)=∥μ−μref∥F2\text{MMD}^2\left(F, P, P_{ref}\right) = \lVert \mu - \mu_{ref} \rVert^2_{F}MMD2(F,P,Pref​)=∥μ−μref​∥F2​
    P(X)=ϕ1N([−1+3,−1−3]T,σ2I)+ϕ2N([1,1]T,σ2I)P(\mathbf{X}) = \phi_1 \mathcal{N}\left(\left[-1\color{red}{+3},-1\color{red}{-3}\right]^T, \sigma^2\mathbf{I} \right) + \phi_2 \mathcal{N}\left(\left[1,1\right]^T, \sigma^2\mathbf{I} \right)P(X)=ϕ1​N([−1+3,−1−3]T,σ2I)+ϕ2​N([1,1]T,σ2I)
    k(z,zref)=(1−ϵ) ka ⁣(Φ(z),Φ(zref))+ϵ kb ⁣(z,zref),k(\mathbf{z},\mathbf{z}^{ref}) = \left(1-\epsilon\right)\, k_a\!\left(\Phi(\mathbf{z}),\Phi(\mathbf{z}^{ref}) \right) + \epsilon \, k_b\!\left(\mathbf{z},\mathbf{z}^{ref}\right),k(z,zref)=(1−ϵ)ka​(Φ(z),Φ(zref))+ϵkb​(z,zref),
    Lipton et al.
    Alibi Detect
    here
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    Alibi Detect
    Chi-Squared
    Kolmogorov-Smirnov
    Cramér-von Mises
    Bonferroni
    False Discovery Rate
    Alibi Detect
    TensorFlow
    PyTorch
    examples
    principal component analysis (PCA)
    sparse random projections (SRP)
    dimension reduction
    input preprocessing
    learned drift detectors
    non-linear techniques
    Detecting and Correcting for Label Shift with Black Box Predictors
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    Maximum Mean Discrepancy drift detector on CIFAR-10
    ResNet-32
    model uncertainty
    dropout Monte Carlo
    visual guide
    transformer
    BERT
    DistilBERT
    Text drift detection on IMDB movie reviews
    Drift detection on molecular graphs
    MMD detector
    notebook
    MMD detector
    Maximum Mean Discrepancy drift detector on CIFAR-10
    kernel trick
    permutation test
    Learned kernel
    Classifier
    Spot-the-diff
    MMD detector
    Gaussian RBF kernel
    Learned kernel
    Liu et al., 2020
    classifier
    Lopez-Paz and Oquab, 2017
    Liu et al.
    learned kernel
    spot-the-diff
    Classifier
    Jitkrittum et al. (2016)
    Interpretable drift detection with the spot-the-diff detector on MNIST and Wine-Quality datasets
    MMD detector
    Alibi Detect
    Online Maximum Mean Discrepancy
    Online Least-Squares Density Difference
    Online Cramér-von Mises
    Online Fisher's Exact Test
    Cobb et al. (2021)
    Drift Detection: An Introduction with Seldon
    Drift in deployment
    :align: center
    :alt: 2D drift example
    pca = PCA(2)
    pca.fit(X_train)
    detector = MMDDrift(X_ref, backend='tensorflow', p_val=.05, preprocess_fn=pca.transform)
    encoder_net = torch.nn.Sequential(...)
    preprocess_fn = partial(preprocess_drift, model=encoder_net, batch_size=512)
    detector = MMDDrift(X_ref, backend='pytorch', p_val=.05, preprocess_fn=preprocess_fn)
    clf = fetch_tf_model('cifar10', 'resnet32')
    preprocess_fn = partial(preprocess_drift, model=HiddenOutput(clf, layer=-1), batch_size=128)
    detector = MMDDrift(X_ref, backend='tensorflow', p_val=.05,preprocess_fn=preprocess_fn)
    reg =  # pytorch regression model with at least 1 dropout layer
    detector = RegressorUncertaintyDrift(x_ref, reg, backend='pytorch', 
                                         p_val=.05, uncertainty_type='mc_dropout')
    :align: center
    :alt: A graph embedding
    :width: 550px
    def true_model(X,slope=-1):
        z = slope*X[:,0]
        idx = np.argwhere(X[:,1]>z)
        y = np.zeros(X.shape[0])
        y[idx] = 1
        return y
        
    true_slope = -1 
    # Reference distribution
    sigma = 0.8
    phi1 = 0.5
    phi2 = 0.5
    ref_norm_0 = multivariate_normal([-1,-1], np.eye(2)*sigma**2)
    ref_norm_1 = multivariate_normal([ 1, 1], np.eye(2)*sigma**2)
    
    # Reference data (to initialise the detectors)
    N_ref = 240
    X_0 = ref_norm_0.rvs(size=int(N_ref*phi1),random_state=1)
    X_1 = ref_norm_1.rvs(size=int(N_ref*phi2),random_state=1)
    X_ref = np.vstack([X_0, X_1])
    y_ref = true_model(X_ref,true_slope)
    
    # Training data (to train the classifer)
    N_train = 240
    X_0 = ref_norm_0.rvs(size=int(N_train*phi1),random_state=0)
    X_1 = ref_norm_1.rvs(size=int(N_train*phi2),random_state=0)
    X_train = np.vstack([X_0, X_1])
    y_train = true_model(X_train,true_slope)
    detector = MMDDrift(X_ref, backend='pytorch', p_val=.05)
    # Fit decision tree classifier
    from sklearn.tree import DecisionTreeClassifier
    clf = DecisionTreeClassifier(max_depth=20)
    clf.fit(X_train, y_train)
    
    # Plot with a pre-defined helper function
    plot(X_ref,y_ref,true_slope,clf=clf)
    
    # Classifier accuracy
    print('Mean training accuracy %.2f%%' %(100*clf.score(X_ref,y_ref)))
    
    # Fit a drift detector to the training data
    from alibi_detect.cd import MMDDrift
    detector = MMDDrift(X_ref, backend='pytorch', p_val=.05)
    .. parsed-literal::
    
        Mean training accuracy 99.17%
        No GPU detected, fall back on CPU.
    N_test = 120
    X_0 = ref_norm_0.rvs(size=int(N_test*phi1),random_state=2)
    X_1 = ref_norm_1.rvs(size=int(N_test*phi2),random_state=2)
    X_test = np.vstack([X_0, X_1])
    
    # Plot
    y_test = true_model(X_test,true_slope)
    plot(X_test,y_test,true_slope,clf=clf)
    
    # Classifier accuracy
    print('Mean test accuracy %.2f%%' %(100*clf.score(X_test,y_test)))
    .. parsed-literal::
    
        Mean test accuracy 95.00%
    detector.predict(X_test)
    .. parsed-literal::
    
        {'data': {'is_drift': 0,
          'distance': 0.0023595122654528344,
          'p_val': 0.30000001192092896,
          'threshold': 0.05,
          'distance_threshold': 0.008109889},
         'meta': {'name': 'MMDDriftTorch',
          'detector_type': 'offline',
          'data_type': None,
          'backend': 'pytorch'}}
    shift_norm_0 = multivariate_normal([2, -4], np.eye(2)*sigma**2)
    X_0 = shift_norm_0.rvs(size=N_test*phi1,random_state=2)
    X_1 = ref_norm_1.rvs(size=N_test*phi2,random_state=2)
    X_test = np.vstack([X_0, X_1])
    
    # Plot
    y_test = true_model(X_test,slope)
    plot(X_test,y_test,slope,clf=clf)
    
    # Classifier accuracy
    print('Mean test accuracy %.2f%%' %(100*clf.score(X_test,y_test)))
    
    # Check for drift in covariates
    pred = detector.predict(X_test)
    labels = ['No','Yes']
    print('Is drift? %s!' %labels[pred['data']['is_drift']])
    .. parsed-literal::
    
        Mean test accuracy 66.67%
        Is drift? Yes!
    label_detector = MMDDrift(y_ref.reshape(-1,1), backend='tensorflow', p_val=.05)
    y_pred = clf.predict(X_test)
    label_detector.predict(y_pred.reshape(-1,1))
    :align: center
    :alt: Online drift detection
    :width: 350px
    :align: center
    :alt: Offline detector with W=2
    :width: 700px
    :align: center
    :alt: Offline detector with W=20
    :width: 700px
    online_detector = MMDDriftOnline(X_ref, ert, window_size, backend='tensorflow', n_bootstraps=5000)
    result = online_detector.predict(X[i])
    :align: center
    :alt: Online detector
    :width: 700px
    CIFAR10
    Carlini-Wagner (C&W)
    SLIDE
    Foolbox
    Google Cloud Bucket
    Hendrycks & Dietterich, 2019
    adversarialae.png
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    from sklearn.metrics import roc_curve, auc
    import tensorflow as tf
    from tensorflow.keras.layers import (Conv2D, Conv2DTranspose, Dense, Flatten, 
                                         InputLayer, Reshape)
    from tensorflow.keras.regularizers import l1
    
    from alibi_detect.ad import AdversarialAE
    from alibi_detect.utils.fetching import fetch_detector, fetch_tf_model
    from alibi_detect.utils.tensorflow import predict_batch
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.datasets import fetch_attack, fetch_cifar10c, corruption_types_cifar10c
    def scale_by_instance(X: np.ndarray) -> np.ndarray:
        mean_ = X.mean(axis=(1, 2, 3)).reshape(-1, 1, 1, 1)
        std_ = X.std(axis=(1, 2, 3)).reshape(-1, 1, 1, 1)
        return (X - mean_) / std_, mean_, std_
    
    
    def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
        return (y_true == y_pred).astype(int).sum() / y_true.shape[0]
    
    
    def plot_adversarial(idx: list,
                         X: np.ndarray,
                         y: np.ndarray,
                         X_adv: np.ndarray, 
                         y_adv: np.ndarray,
                         mean: np.ndarray, 
                         std: np.ndarray, 
                         score_x: np.ndarray = None,
                         score_x_adv: np.ndarray = None,
                         X_recon: np.ndarray = None,
                         y_recon: np.ndarray = None,
                         figsize: tuple = (10, 5)) -> None:
        
        # category map from class numbers to names
        cifar10_map = {0: 'airplane', 1: 'automobile', 2: 'bird', 3: 'cat', 4: 'deer', 5: 'dog',
                       6: 'frog', 7: 'horse', 8: 'ship', 9: 'truck'}
        
        nrows = len(idx)
        ncols = 3 if isinstance(X_recon, np.ndarray) else 2
        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize)
        
        n_subplot = 1
        for i in idx:
            
            # rescale images in [0, 1]
            X_adj = (X[i] * std[i] + mean[i]) / 255
            X_adv_adj = (X_adv[i] * std[i] + mean[i]) / 255
            if isinstance(X_recon, np.ndarray):
                X_recon_adj = (X_recon[i] * std[i] + mean[i]) / 255
            
            # original image
            plt.subplot(nrows, ncols, n_subplot)
            plt.axis('off')
            if i == idx[0]:
                if isinstance(score_x, np.ndarray):
                    plt.title('CIFAR-10 Image \n{}: {:.3f}'.format(cifar10_map[y[i]], score_x[i]))
                else:
                    plt.title('CIFAR-10 Image \n{}'.format(cifar10_map[y[i]]))
            else:
                if isinstance(score_x, np.ndarray):
                    plt.title('{}: {:.3f}'.format(cifar10_map[y[i]], score_x[i]))
                else:
                    plt.title('{}'.format(cifar10_map[y[i]]))
            plt.imshow(X_adj)
            n_subplot += 1
            
            # adversarial image
            plt.subplot(nrows, ncols, n_subplot)
            plt.axis('off')
            if i == idx[0]:
                if isinstance(score_x_adv, np.ndarray):
                    plt.title('Adversarial \n{}: {:.3f}'.format(cifar10_map[y_adv[i]], score_x_adv[i]))
                else:
                    plt.title('Adversarial \n{}'.format(cifar10_map[y_adv[i]]))
            else:
                if isinstance(score_x_adv, np.ndarray):
                    plt.title('{}: {:.3f}'.format(cifar10_map[y_adv[i]], score_x_adv[i]))
                else:
                    plt.title('{}'.format(cifar10_map[y_adv[i]]))
            plt.imshow(X_adv_adj)
            n_subplot += 1
         
            # reconstructed image
            if isinstance(X_recon, np.ndarray):
                plt.subplot(nrows, ncols, n_subplot)
                plt.axis('off')
                if i == idx[0]:
                    plt.title('AE Reconstruction \n{}'.format(cifar10_map[y_recon[i]]))
                else:
                    plt.title('{}'.format(cifar10_map[y_recon[i]]))
                plt.imshow(X_recon_adj)
                n_subplot += 1
        
        plt.show()
    
        
    def plot_roc(roc_data: dict, figsize: tuple = (10,5)):
        plot_labels = []
        scores_attacks = []
        labels_attacks = []
        for k, v in roc_data.items():
            if 'original' in k:
                continue
            score_x = roc_data[v['normal']]['scores']
            y_pred = roc_data[v['normal']]['predictions']
            score_v = v['scores']
            y_pred_v = v['predictions']
            labels_v = np.ones(score_x.shape[0])
            idx_remove = np.where(y_pred == y_pred_v)[0]
            labels_v = np.delete(labels_v, idx_remove)
            score_v = np.delete(score_v, idx_remove)
            scores = np.concatenate([score_x, score_v])
            labels = np.concatenate([np.zeros(y_pred.shape[0]), labels_v]).astype(int)
            scores_attacks.append(scores)
            labels_attacks.append(labels)
            plot_labels.append(k)
        
        for sc_att, la_att, plt_la in zip(scores_attacks, labels_attacks, plot_labels):
            fpr, tpr, thresholds = roc_curve(la_att, sc_att)
            roc_auc = auc(fpr, tpr)
            label = str('{}: AUC = {:.2f}'.format(plt_la, roc_auc))
            plt.plot(fpr, tpr, lw=1, label='{}: AUC={:.4f}'.format(plt_la, roc_auc))
    
        plt.plot([0, 1], [0, 1], color='black', lw=1, linestyle='--')
        plt.xlim([0.0, 1.0])
        plt.ylim([0.0, 1.05])
        plt.xlabel('False Positive Rate')
        plt.ylabel('True Positive Rate')
        plt.title('{}'.format('ROC curve'))
        plt.legend(loc="lower right", ncol=1)
        plt.grid()
        plt.show()
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32')
    X_test = X_test.astype('float32')
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    X_train, mean_train, std_train = scale_by_instance(X_train)
    X_test, mean_test, std_test = scale_by_instance(X_test)
    scale = (mean_train, std_train), (mean_test, std_test)
    dataset = 'cifar10'
    model = 'resnet56'
    clf = fetch_tf_model(dataset, model)
    #| scrolled: true
    y_pred = predict_batch(X_test, clf, batch_size=32).argmax(axis=1)
    acc_y_pred = accuracy(y_test, y_pred)
    print('Accuracy: {:.4f}'.format(acc_y_pred))
    # C&W attack
    data_cw = fetch_attack(dataset, model, 'cw')
    X_train_cw, X_test_cw = data_cw['data_train'], data_cw['data_test']
    meta_cw = data_cw['meta'] # metadata with hyperparameters of the attack
    # SLIDE attack
    data_slide = fetch_attack(dataset, model, 'slide')
    X_train_slide, X_test_slide = data_slide['data_train'], data_slide['data_test']
    meta_slide = data_slide['meta']
    print(X_test_cw.shape, X_test_slide.shape)
    y_pred_cw = predict_batch(X_test_cw, clf, batch_size=32).argmax(axis=1)
    y_pred_slide = predict_batch(X_test_slide, clf, batch_size=32).argmax(axis=1)
    acc_y_pred_cw = accuracy(y_test, y_pred_cw)
    acc_y_pred_slide = accuracy(y_test, y_pred_slide)
    print('Accuracy: cw {:.4f} -- SLIDE {:.4f}'.format(acc_y_pred_cw, acc_y_pred_slide))
    idx = [3, 4]
    print('C&W attack...')
    plot_adversarial(idx, X_test, y_pred, X_test_cw, y_pred_cw, 
                     mean_test, std_test, figsize=(10, 10))
    print('SLIDE attack...')
    plot_adversarial(idx, X_test, y_pred, X_test_slide, y_pred_slide, 
                     mean_test, std_test, figsize=(10, 10))
    load_pretrained = True
    #| scrolled: true
    filepath = 'my_path'  # change to (absolute) directory where model is downloaded
    detector_type = 'adversarial'
    detector_name = 'base'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:  # train detector from scratch
        # define encoder and decoder networks
        encoder_net = tf.keras.Sequential(
                [
                    InputLayer(input_shape=(32, 32, 3)),
                    Conv2D(32, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(64, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(256, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Flatten(),
                    Dense(40)
                ]
            )
        
        decoder_net = tf.keras.Sequential(
            [
                    InputLayer(input_shape=(40,)),
                    Dense(4 * 4 * 128, activation=tf.nn.relu),
                    Reshape(target_shape=(4, 4, 128)),
                    Conv2DTranspose(256, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(64, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(3, 4, strides=2, padding='same', 
                                    activation=None, kernel_regularizer=l1(1e-5))
                ]
            )
        
        # initialise and train detector
        ad = AdversarialAE(
            encoder_net=encoder_net, 
            decoder_net=decoder_net, 
            model=clf
        )
        ad.fit(X_train, epochs=40, batch_size=64, verbose=True)
        
        # save the trained adversarial detector
        save_detector(ad, filepath)
    X_recon_cw = predict_batch(X_test_cw, ad.ae, batch_size=32)
    X_recon_slide = predict_batch(X_test_slide, ad.ae, batch_size=32)
    y_recon_cw = predict_batch(X_recon_cw, clf, batch_size=32).argmax(axis=1)
    y_recon_slide = predict_batch(X_recon_slide, clf, batch_size=32).argmax(axis=1)
    acc_y_recon_cw = accuracy(y_test, y_recon_cw)
    acc_y_recon_slide = accuracy(y_test, y_recon_slide)
    print('Accuracy after C&W attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_cw, acc_y_recon_cw))
    print('Accuracy after SLIDE attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_slide, acc_y_recon_slide))
    score_x = ad.score(X_test, batch_size=32)
    score_cw = ad.score(X_test_cw, batch_size=32)
    score_slide = ad.score(X_test_slide, batch_size=32)
    #| scrolled: false
    print('C&W attack...')
    idx = [10, 13, 14, 16, 17]
    plot_adversarial(idx, X_test, y_pred, X_test_cw, y_pred_cw, mean_test, std_test, 
                     score_x=score_x, score_x_adv=score_cw, X_recon=X_recon_cw, 
                     y_recon=y_recon_cw, figsize=(10, 15))
    print('SLIDE attack...')
    idx = [23, 25, 27, 29, 34]
    plot_adversarial(idx, X_test, y_pred, X_test_slide, y_pred_slide, mean_test, std_test, 
                     score_x=score_x, score_x_adv=score_slide, X_recon=X_recon_slide, 
                     y_recon=y_recon_slide, figsize=(10, 15))
    roc_data = {
        'original': {'scores': score_x, 'predictions': y_pred},
        'C&W': {'scores': score_cw, 'predictions': y_pred_cw, 'normal': 'original'},
        'SLIDE': {'scores': score_slide, 'predictions': y_pred_slide, 'normal': 'original'}
    }
    
    plot_roc(roc_data)
    ad.infer_threshold(X_test, threshold_perc=95, margin=0., batch_size=32)
    print('Adversarial threshold: {:.4f}'.format(ad.threshold))
    save_detector(ad, filepath)
    ad = load_detector(filepath)
    n_test = X_test.shape[0]
    np.random.seed(0)
    idx_normal = np.random.choice(n_test, size=1600, replace=False)
    idx_cw = np.random.choice(n_test, size=400, replace=False)
    
    X_mix = np.concatenate([X_test[idx_normal], X_test_cw[idx_cw]])
    y_mix = np.concatenate([y_test[idx_normal], y_test[idx_cw]])
    print(X_mix.shape, y_mix.shape)
    y_pred_mix = predict_batch(X_mix, clf, batch_size=32).argmax(axis=1)
    acc_y_pred_mix = accuracy(y_mix, y_pred_mix)
    print('Accuracy {:.4f}'.format(acc_y_pred_mix))
    preds = ad.correct(X_mix, batch_size=32)
    acc_y_corr_mix = accuracy(y_mix, preds['data']['corrected'])
    print('Accuracy {:.4f}'.format(acc_y_corr_mix))
    load_pretrained = True
    filepath = 'my_path'  # change to (absolute) directory where model is downloaded
    detector_name = 'temperature'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad_t = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:  # train detector from scratch
        # define encoder and decoder networks
        encoder_net = tf.keras.Sequential(
                [
                    InputLayer(input_shape=(32, 32, 3)),
                    Conv2D(32, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(64, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(256, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Flatten(),
                    Dense(40)
                ]
            )
        
        decoder_net = tf.keras.Sequential(
            [
                    InputLayer(input_shape=(40,)),
                    Dense(4 * 4 * 128, activation=tf.nn.relu),
                    Reshape(target_shape=(4, 4, 128)),
                    Conv2DTranspose(256, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(64, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(3, 4, strides=2, padding='same', 
                                    activation=None, kernel_regularizer=l1(1e-5))
                ]
            )
        
        # initialise and train detector
        ad_t = AdversarialAE(
            encoder_net=encoder_net, 
            decoder_net=decoder_net, 
            model=clf,
            temperature=0.5
        )
        ad_t.fit(X_train, epochs=40, batch_size=64, verbose=True)
        
        # save the trained adversarial detector
        save_detector(ad_t, filepath)
    # reconstructed adversarial instances
    X_recon_cw_t = predict_batch(X_test_cw, ad_t.ae, batch_size=32)
    X_recon_slide_t = predict_batch(X_test_slide, ad_t.ae, batch_size=32)
    
    # make predictions on reconstructed instances and compute accuracy
    y_recon_cw_t = predict_batch(X_recon_cw_t, clf, batch_size=32).argmax(axis=1)
    y_recon_slide_t = predict_batch(X_recon_slide_t, clf, batch_size=32).argmax(axis=1)
    acc_y_recon_cw_t = accuracy(y_test, y_recon_cw_t)
    acc_y_recon_slide_t = accuracy(y_test, y_recon_slide_t)
    print('Accuracy after C&W attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_cw, acc_y_recon_cw_t))
    print('Accuracy after SLIDE attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_slide, 
                                                                               acc_y_recon_slide_t))
    score_x_t = ad_t.score(X_test, batch_size=32)
    score_cw_t = ad_t.score(X_test_cw, batch_size=32)
    score_slide_t = ad_t.score(X_test_slide, batch_size=32)
    roc_data['original_t'] = {'scores': score_x_t, 'predictions': y_pred}
    roc_data['C&W T=0.5'] = {'scores': score_cw_t, 'predictions': y_pred_cw, 'normal': 'original_t'}
    roc_data['SLIDE T=0.5'] = {'scores': score_slide_t, 'predictions': y_pred_slide, 'normal': 'original_t'}
    
    plot_roc(roc_data)
    load_pretrained = True
    filepath = 'my_path'  # change to (absolute) directory where model is downloaded
    detector_name = 'hiddenkld'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad_hl = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:  # train detector from scratch
        # define encoder and decoder networks
        encoder_net = tf.keras.Sequential(
                [
                    InputLayer(input_shape=(32, 32, 3)),
                    Conv2D(32, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(64, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(256, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Flatten(),
                    Dense(40)
                ]
            )
        
        decoder_net = tf.keras.Sequential(
            [
                    InputLayer(input_shape=(40,)),
                    Dense(4 * 4 * 128, activation=tf.nn.relu),
                    Reshape(target_shape=(4, 4, 128)),
                    Conv2DTranspose(256, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(64, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(3, 4, strides=2, padding='same', 
                                    activation=None, kernel_regularizer=l1(1e-5))
                ]
            )
        
        # initialise and train detector
        ad_hl = AdversarialAE(
            encoder_net=encoder_net, 
            decoder_net=decoder_net, 
            model=clf,
            hidden_layer_kld={200: 20},  # extract feature map from hidden layer 200
            temperature=1                # predict softmax with output dim=20
        )
        ad_hl.fit(X_train, epochs=40, batch_size=64, verbose=True)
        
        # save the trained adversarial detector
        save_detector(ad_hl, filepath)
    # reconstructed adversarial instances
    X_recon_cw_hl = predict_batch(ad_hl.ae, X_test_cw, batch_size=32)
    X_recon_slide_hl = predict_batch(ad_hl.ae, X_test_slide, batch_size=32)
    
    # make predictions on reconstructed instances and compute accuracy
    y_recon_cw_hl = predict_batch(X_recon_cw_hl, clf, batch_size=32).argmax(axis=1)
    y_recon_slide_hl = predict_batch(X_recon_slide_hl, clf, batch_size=32).argmax(axis=1)
    acc_y_recon_cw_hl = accuracy(y_test, y_recon_cw_hl)
    acc_y_recon_slide_hl = accuracy(y_test, y_recon_slide_hl)
    print('Accuracy after C&W attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_cw, acc_y_recon_cw_hl))
    print('Accuracy after SLIDE attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_slide, 
                                                                               acc_y_recon_slide_hl))
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    severities = [1,2,3,4,5]
    
    score_drift = {
        1: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        2: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        3: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        4: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
        5: {'all': [], 'harm': [], 'noharm': [], 'acc': 0},
    }
    
    for s in severities:
        print('\nSeverity: {} of {}'.format(s, len(severities)))
        
        print('Loading corrupted dataset...')
        X_corr, y_corr = fetch_cifar10c(corruption=corruptions, severity=s, return_X_y=True)
        X_corr = X_corr.astype('float32')
        
        print('Preprocess data...')
        X_corr, mean_test, std_test = scale_by_instance(X_corr)
        
        print('Make predictions on corrupted dataset...')
        y_pred_corr = predict_batch(X_corr, clf, batch_size=32).argmax(axis=1)
        
        print('Compute adversarial scores on corrupted dataset...')
        score_corr = ad_t.score(X_corr, batch_size=32)
        scores = np.concatenate([score_x_t, score_corr])
        
        print('Get labels for malicious corruptions...')
        labels_corr = np.zeros(score_corr.shape[0])    
        repeat = y_corr.shape[0] // y_test.shape[0]
        y_pred_repeat = np.tile(y_pred, (repeat,))
        # malicious/harmful corruption: original prediction correct but 
        # prediction on corrupted data incorrect
        idx_orig_right = np.where(y_pred_repeat == y_corr)[0]
        idx_corr_wrong = np.where(y_pred_corr != y_corr)[0]
        idx_harmful = np.intersect1d(idx_orig_right, idx_corr_wrong)
        labels_corr[idx_harmful] = 1
        labels = np.concatenate([np.zeros(X_test.shape[0]), labels_corr]).astype(int)
        # harmless corruption: original prediction correct and prediction
        # on corrupted data correct
        idx_corr_right = np.where(y_pred_corr == y_corr)[0]
        idx_harmless = np.intersect1d(idx_orig_right, idx_corr_right)
        
        score_drift[s]['all'] = score_corr
        score_drift[s]['harm'] = score_corr[idx_harmful]
        score_drift[s]['noharm'] = score_corr[idx_harmless]
        score_drift[s]['acc'] = accuracy(y_corr, y_pred_corr)
    mu_noharm, std_noharm = [], []
    mu_harm, std_harm = [], []
    acc = [acc_y_pred]
    for k, v in score_drift.items():
        mu_noharm.append(v['noharm'].mean())
        std_noharm.append(v['noharm'].std())
        mu_harm.append(v['harm'].mean())
        std_harm.append(v['harm'].std())
        acc.append(v['acc'])
    plot_labels = ['0', '1', '2', '3', '4', '5']
    
    N = 6
    ind = np.arange(N)
    width = .35
    
    fig_bar_cd, ax = plt.subplots()
    ax2 = ax.twinx()
    
    p0 = ax.bar(ind[0], score_x_t.mean(), yerr=score_x_t.std(), capsize=2)
    p1 = ax.bar(ind[1:], mu_noharm, width, yerr=std_noharm, capsize=2)
    p2 = ax.bar(ind[1:] + width, mu_harm, width, yerr=std_harm, capsize=2)
    
    ax.set_title('Adversarial Scores and Accuracy by Corruption Severity')
    ax.set_xticks(ind + width / 2)
    ax.set_xticklabels(plot_labels)
    ax.set_ylim((-1,6))
    ax.legend((p1[0], p2[0]), ('Not Harmful', 'Harmful'), loc='upper right', ncol=2)
    ax.set_ylabel('Score')
    ax.set_xlabel('Corruption Severity')
    
    color = 'tab:red'
    ax2.set_ylabel('Accuracy', color=color)
    ax2.plot(acc, color=color)
    ax2.tick_params(axis='y', labelcolor=color)
    
    plt.show()

    Context-aware drift detection on news articles

    Introduction

    In this notebook we show how to detect drift on text data given a specific context using the context-aware MMD detector (Cobb and Van Looveren, 2022). Consider the following simple example: the upcoming elections result in an increase of political news articles compared to other topics such as sports or science. Given the context (the elections), it is however not surprising that we observe this uptick. Moreover, assume we have a machine learning model which is trained to classify news topics, and this model performs well on political articles. So given that we fully expect this uptick to occur given the context, and that our model performs fine on the political news articles, we do not want to flag this type of drift in the data. This setting corresponds more closely to many real-life settings than traditional drift detection where we make the assumption that both the reference and test data are i.i.d. samples from their underlying distributions.

    In our news topics example, each different topic such as politics, sports or weather represents a subpopulation of the data. Our context-aware drift detector can then detect changes in the data distribution which cannot be attributed to a change in the relative prevalences of these subpopulations, which we deem permissible. As a cherry on the cake, the context-aware detector allows you to understand which subpopulations are present in both the reference and test data. This allows you to obtain deep insights into the distribution underlying the test data.

    Useful context (or conditioning) variables for the context-aware drift detector include but are not limited to:

    1. Domain or application specific contexts such as the time of day or the weather.

    2. Conditioning on the relative prevalences of known subpopulations, such as the frequency of political articles. It is important to note that while the relative frequency of each subpopulation might change, the distribution underlying each subpopulation cannot change.

    3. Conditioning on model predictions. Assume we trained a classifier which tries to figure out which news topic an article belongs to. Given our model predictions we then want to understand whether our test data follows the same underlying distribution as reference instances with similar model predictions. This conditioning would also be useful in case of trending news topics which cause the model prediction distribution to shift but not necessarily the distribution within each of the news topics.

    The following settings will be illustrated throughout the notebook:

    1. A change in the prevalences of subpopulations (i.e. news topics) relative to their prevalences in the training data. Contrary to traditional drift detection approaches, the context-aware detector does not flag drift as this change in frequency of news topics is permissible given the context provided (e.g. more political news articles around elections).

    2. A change in the underlying distribution of one or more subpopulations takes place. While we allow changes in the prevalence of the subpopulations accounted for by the context variable, we do not allow changes of the subpopulations themselves. Let's assume that a newspaper usually has a certain tone (e.g. more conservative) when it comes to politics. If this tone changes (to less conservative) around elections (increased frequency of political news articles), then we want to flag it as drift since the change cannot be attributed to the context given to the detector.

    3. A change in the distribution as we observe a previously unseen news topic

    Under setting 1. we want our detector to be well-calibrated (a controlled False Positive Rate (FPR) and more generally a p-value which is uniformly distributed between 0 and 1) while under settings 2. and 3. we want our detector to be powerful and flag the drift. Lastly, we show how the detector can help you to understand the connection between the reference and test data distributions better.

    Data

    We use the which contains about 18,000 newsgroups post across 20 topics, including politics, science sports or religion.

    Requirements

    The notebook requires the umap-learn, torch, sentence-transformers, statsmodels, seaborn and datasets packages to be installed, which can be done via pip:

    Before we start let's fix the random seeds for reproducibility:

    Load data

    First we load the data, show which classes (news topics) are present and what an instance looks like.

    Let's take a look at an instance from the dataset:

    Define models and train a classifier

    We embed the news posts using pre-trained embeddings and optionally add a dimensionality reduction step with . UMAP also allows to leverage reference data labels.

    We define respectively a generic clustering model using UMAP, a model to embed the text input using pre-trained SentenceTransformers embeddings, a text classifier and a utility function to place the data on the right device.

    First we train a classifier on a small subset of the data. The aim of the classifier is to predict the news topic of each instance. Below we define a few simple training and evaluation functions.

    We now split the data in 2 sets. The first set (x_train) we will use to train our text classifier, and the second set (x_drift) is held out to test our drift detector on.

    Let's train our classifier. The classifier consists of a simple MLP head on top of a pre-trained SentenceTransformer model as the backbone. The SentenceTransformer remains frozen during training and only the MLP head is finetuned.

    Detector calibration under no change

    We start with an example where no drift occurs and the reference and test data are both sampled randomly from all news topics. Under this scenario, we expect no drift to be detected by either a normal MMD detector or by the context-aware MMD detector.

    First we define some helper functions. The first one visualises the clustered text data while the second function samples disjoint reference and test sets with a specified number of instances per class (i.e. per news topic).

    We first define the embedding model using the pre-trained SentenceTransformer embeddings and then embed both the reference and test sets.

    By applying UMAP clustering on the SentenceTransformer embeddings, we can visually inspect the various news topic clusters. Note that we fit the clustering model on the held out data first, and then make predictions on the reference and test sets.

    We can visually see that the reference and test set are made up of similar clusters of data, grouped by news topic. As a result, we would not expect drift to be flagged. If the data distribution did not change, we can expect the p-value distribution of our statistical test to be uniformly distributed between 0 and 1. So let's see if this assumption holds.

    Importantly, first we need to define our context variable for the context-aware MMD detector. In our experiments we allow the relative prevalences of subpopulations to vary while the distributions underlying each of the subpopulations remain unchanged. To achieve this we condition on the prediction probabilities of the classifier we trained earlier to distinguish each of the 20 different news topics. We can do this because the prediction probabilities can account for the frequency of occurrence of each of the topics (be it imperfectly given our classifier makes the occasional mistake).

    Before we set off our experiments, we embed all the instances in x_drift and compute all contexts c_drift so we don't have to call our transformer model every single pass in the for loop.

    The below figure of the of a random sample from the uniform distribution U[0,1] against the obtained p-values from the vanilla and context-aware MMD detectors illustrate how well both detectors are calibrated. A perfectly calibrated detector should have a Q-Q plot which closely follows the diagonal. Only the middle plot in the grid shows the detector's p-values. The other plots correspond to n_runs p-values actually sampled from U[0,1] to contextualise how well the central plot follows the diagonal given the limited number of samples.

    As expected we can see that both the normal MMD and the context-aware MMD detectors are well-calibrated.

    Changing the relative subpopulation prevalence

    We now focus our attention on a more realistic problem where the relative frequency of one or more subpopulations (i.e. news topics) is changing in a way which can be attributed to external events. Importantly, the distribution underlying each subpopulation (e.g. the distribution of hockey news itself) remains unchanged, only its frequency changes.

    In our example we assume that the World Series and Stanley Cup coincide on the calendar leading to a spike in news articles on respectively baseball and hockey. Furthermore, there is not too much news on Mac or Windows since there are no new releases or products planned anytime soon.

    While the context-aware detector remains well calibrated, the MMD detector consistently flags drift (low p-values). Note that this is the expected behaviour since the vanilla MMD detector cannot take any external context into account and correctly detects that the reference and test data do not follow the same underlying distribution.

    We can also easily see this on the plot below where the p-values of the context-aware detector are uniformly distributed while the MMD detector's p-values are consistently close to 0. Note that we limited the y-axis range to make the plot easier to read.

    Changing the subpopulation distribution

    In the following example we change the distribution of one or more of the underlying subpopulations. Notice that now we do want to flag drift since our context variable, which permits changes in relative subpopulation prevalences, can no longer explain the change in distribution.

    Imagine our news topic classification model is not as granular as before and instead of the 20 categories only predicts the 6 super classes, organised by subject matter:

    1. Computers: comp.graphics; comp.os.ms-windows.misc; comp.sys.ibm.pc.hardware; comp.sys.mac.hardware; comp.windows.x

    2. Recreation: rec.autos; rec.motorcycles; rec.sport.baseball; rec.sport.hockey

    3. Science: sci.crypt; sci.electronics; sci.med; sci.space

    4. Miscellaneous: misc.forsale

    What if baseball and hockey become less popular and the distribution underlying the Recreation class changes? We will want to detect this as the change in distributions of the subpopulations (the 6 super classes) cannot be explained anymore by the context variable.

    In order to reuse our pretrained classifier for the super classes, we add the following helper function to map the predictions on the super classes and return one-hot encoded predictions over the 6 super classes. Note that our context variable now changes from a probability distribution over the 20 news topics to a one-hot encoded representation over the 6 super classes.

    We can see that the context-aware detector is powerful to detect changes in the distributions of the subpopulations.

    Detect unseen topics

    Next we illustrate the effectiveness of the context-aware detector to detect new topics which are not present in the reference data. Obviously we also want to flag drift in this case. As an example we introduce movie reviews in the test data.

    Changing the context variable

    So far we have conditioned the context-aware detector on the model predictions. There are however many other useful contexts possible. One such example would be to condition on the predictions of an unsupervised clustering algorithm. To facilitate this, we first apply kernel PCA on the embedding vectors, followed by a Gaussian mixture model which clusters the data into 6 classes (same as the super classes). We will test both the calibration under the null hypothesis (no distribution change) as well as the power when a new topic (movie reviews) is injected.

    Next we change the number of instances in each cluster between the reference and test sets. Note that we do not alter the underlying distribution of each of the clusters, just the frequency.

    Now we run the experiment and show the context-aware detector's calibration when changing the cluster frequencies. We also show how the usual MMD detector will consistently flag drift. Furthermore, we inject instances from the movie reviews dataset and illustrate that the context-aware detector remains powerful when the underlying cluster distribution changes (by including a previously unseen topic).

    Interpretability of the context-aware detector

    The test statistic $\hat{t}$ of the context-aware MMD detector can be formulated as follows: $\hat{t} = \langle K_{0,0}, W_{0,0} \rangle + \langle K_{1,1}, W_{1,1} \rangle -2\langle K_{0,1}, W_{0,1}\rangle$ where $0$ refers to the reference data, $1$ to the test data, and $W_{.,.}$ and $K_{.,.}$ are the weight and kernel matrices, respectively. The weight matrices $W_{.,.}$ allow us to focus on the distribution's subpopulations of interest. Reference instances which have similar contexts as the test data will have higher values for their entries in $W_{0,1}$ than instances with dissimilar contexts. We can therefore interpret $W_{0,1}$ as the coupling matrix between instances in the reference and the test sets. This allows us to investigate which subpopulations from the reference set are present and which are missing in the test data. If we also have a good understanding of the model performance on various subpopulations of the reference data, we could even try and use this coupling matrix to roughly proxy model performance on the unlabeled test instances. Note that in this case we would require labels from the reference data and make sure the reference instances come from the validation, not the training set.

    In the following example we only pick 2 classes to be present in the test set while all 20 are present in the reference set. We can then investigate via the coupling matrix whether the test statistic $\hat{t}$ focused on the right classes in the reference data via $W_{0,1}$. More concretely, we can sum over the columns (the test instances) of $W_{0,1}$ and check which reference instances obtained the highest weights.

    Conditioning on model uncertainties which would allow increases in model uncertainty due to drift into familiar regions of high aleatoric uncertainty (often fine) to be distinguished from that into unfamiliar regions of high epistemic uncertainty (often problematic).

    . A newspaper might for instance add a classified ads section, which was not present in the reference data.

    Politics: talk.politics.misc; talk.politics.guns; talk.politics.mideast

  • Religion: talk.religion.misc; talk.atheism; soc.religion.christian

  • 20 newsgroup dataset
    SentenceTransformers
    UMAP
    Q-Q (Quantile-Quantile) plots
    !pip install umap-learn torch sentence-transformers statsmodels seaborn datasets
    import numpy as np
    import torch
    
    def set_seed(seed: int) -> None:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        np.random.seed(seed)
    
    set_seed(2022)
    from sklearn.datasets import fetch_20newsgroups
    
    dataset = fetch_20newsgroups(subset='all', shuffle=True, random_state=42)
    print(f'{len(dataset.data)} documents')
    print(f'{len(dataset.target_names)} categories:')
    dataset.target_names
    n = 1
    for _, document in enumerate(dataset.data[:n]):
        category = dataset.target_names[dataset.target[_]]
        print(f'{_}. Category: {category}')
        print('---------------------------')
        print(document[:1000])
        print('---------------------------')
    from sentence_transformers import SentenceTransformer
    import torch.nn as nn
    import umap
    
    
    class UMAPModel:
        def __init__(
            self, 
            n_neighbors: int = 10,
            n_components: int = 2,
            metric: str = 'euclidean',
            min_dist: float = .1,
            **kwargs: dict
        ) -> None:
            super().__init__()
            kwargs = kwargs if isinstance(kwargs, dict) else dict()
            kwargs.update(
                n_neighbors=n_neighbors,
                n_components=n_components,
                metric=metric,
                min_dist=min_dist
            )
            self.model = umap.UMAP(**kwargs)
        
        def fit(self, x: np.ndarray, y: np.ndarray = None) -> None:
            """ Fit UMAP embedding. A combination of labeled and unlabeled data
            can be passed. Unlabeled instances are equal to -1. """
            self.model.fit(x, y=y)
        
        def predict(self, x: np.ndarray) -> np.ndarray:
            """ Transform the input x to the embedding space. """
            return self.model.transform(x)
    
    
    class EmbeddingModel:
        def __init__(
            self,
            model_name: str = 'paraphrase-MiniLM-L6-v2',  # https://www.sbert.net/docs/pretrained_models.html
            max_seq_length: int = 200,
            batch_size: int = 32,
            device: torch.device = None
        ) -> None:
            if not isinstance(device, torch.device):
                device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
            self.encode_text = SentenceTransformer(model_name).to(device)
            self.encode_text.max_seq_length = max_seq_length
            self.batch_size = batch_size
        
        def __call__(self, x: np.ndarray) -> np.ndarray:
            return self.encode_text.encode(x, convert_to_numpy=True, batch_size=self.batch_size,
                                           show_progress_bar=False)
    
        
    class Classifier(nn.Module):
        def __init__(
            self, 
            model_name: str = 'paraphrase-MiniLM-L6-v2',
            max_seq_length: int = 200,
            n_classes: int = 20
        ) -> None:
            """ Text classification model. Note that we do not train the embedding backbone. """
            super().__init__()
            self.encode_text = SentenceTransformer(model_name)
            self.encode_text.max_seq_length = max_seq_length
            for param in self.encode_text.parameters():
                param.requires_grad = False
            self.head = nn.Sequential(nn.Linear(384, 256), nn.LeakyReLU(.1), nn.Dropout(.5), nn.Linear(256, 20))
            
        def forward(self, tokens) -> torch.Tensor:
            return self.head(self.encode_text(tokens)['sentence_embedding'])
    
        
    def batch_to_device(batch: dict, target_device: torch.device):
        """ Send a pytorch batch to a device (CPU/GPU). """
        for key in batch:
            if isinstance(batch[key], torch.Tensor):
                batch[key] = batch[key].to(target_device)
        return batch
    def train_model(model, loader, epochs=3, lr=1e-3):
        optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
        criterion = nn.CrossEntropyLoss()
        for epoch in range(epochs):
            for x, y in tqdm(loader):
                tokens, y = tokenize(x), y.to(device)
                y_hat = clf(tokens)
                optimizer.zero_grad()
                loss = criterion(y_hat, y)
                loss.backward()
                optimizer.step()
    
                
    def eval_model(model, loader, verbose=1):
        model.eval()
        logits, labels = [], []
        with torch.no_grad():
            if verbose == 1:
                loader = tqdm(loader)
            for x, y in loader:
                tokens = tokenize(x)
                y_hat = model(tokens)
                logits += [y_hat.cpu().numpy()]
                labels += [y.cpu().numpy()]
        logits = np.concatenate(logits, 0)
        preds = np.argmax(logits, 1)
        labels = np.concatenate(labels, 0)
        if verbose == 1:
            accuracy = (preds == labels).mean()
            print(f'Accuracy: {accuracy:.3f}')
        return logits, preds
    n_all = len(dataset.data)
    n_train = 5000  # nb of instances to train news topic classifier on
    idx_train = np.random.choice(n_all, size=n_train, replace=False)
    idx_keep = np.setdiff1d(np.arange(n_all), idx_train)
    # data used for model training
    x_train, y_train = [dataset.data[_] for _ in idx_train], dataset.target[idx_train]
    # data used for drift detection
    x_drift, y_drift = [dataset.data[_] for _ in idx_keep], dataset.target[idx_keep]
    n_drift = len(x_drift)
    from alibi_detect.utils.pytorch import TorchDataset
    from torch.utils.data import DataLoader
    from tqdm import tqdm
    from typing import Dict, List
    
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    clf = Classifier().to(device)
    train_loader = DataLoader(TorchDataset(x_train, y_train), batch_size=32, shuffle=True)
    drift_loader = DataLoader(TorchDataset(x_drift, y_drift), batch_size=32, shuffle=False)
    
    def tokenize(x: List[str]) -> Dict[str, torch.Tensor]:
        tokens = clf.encode_text.tokenize(x)
        return batch_to_device(tokens, device)
    
    train_model(clf, train_loader, epochs=5)
    clf.eval()
    _, _ = eval_model(clf, train_loader)
    _, _ = eval_model(clf, drift_loader)
    import matplotlib.pyplot as plt
    
    
    def plot_clusters(x: np.ndarray, y: np.ndarray, classes: list, title: str = None) -> None:
        fig, ax = plt.subplots(1, figsize=(14, 10))
        plt.scatter(*x.T, s=0.3, c=y, cmap='Spectral', alpha=1.0)
        plt.setp(ax, xticks=[], yticks=[])
        nc = len(classes)
        cbar = plt.colorbar(boundaries=np.arange(nc+1)-0.5)
        cbar.set_ticks(np.arange(nc))
        cbar.set_ticklabels(classes)
        if title:
            plt.title(title);
            
    
    def split_data(x, y, n_ref_c, n_test_c, seed=None, y2=None, return_idx=False):
        
        if seed:
            np.random.seed(seed)
        
        # split data by class
        n_c = len(np.unique(y))
        idx_c = {_: np.where(y == _)[0] for _ in range(n_c)}
        
        # convert nb instances per class to a list if needed
        n_ref_c = [n_ref_c] * n_c if isinstance(n_ref_c, int) else n_ref_c
        n_test_c = [n_test_c] * n_c if isinstance(n_test_c, int) else n_test_c
        
        # sample reference, test and held out data
        idx_ref, idx_test, idx_held = [], [], []
        for _ in range(n_c):
            idx = np.random.choice(idx_c[_], size=len(idx_c[_]), replace=False)
            idx_ref.append(idx[:n_ref_c[_]])
            idx_test.append(idx[n_ref_c[_]:n_ref_c[_] + n_test_c[_]])
            idx_held.append(idx[n_ref_c[_] + n_test_c[_]:])
        idx_ref = np.concatenate(idx_ref)
        idx_test = np.concatenate(idx_test)
        idx_held = np.concatenate(idx_held)
        x_ref, y_ref = [x[_] for _ in idx_ref], y[idx_ref]
        x_test, y_test = [x[_] for _ in idx_test], y[idx_test]
        x_held, y_held = [x[_] for _ in idx_held], y[idx_held]
        if y2 is not None:
            y_ref2, y_test2, y_held2 = y2[idx_ref], y2[idx_test], y2[idx_held]
            return (x_ref, y_ref, y_ref2), (x_test, y_test, y_test2), (x_held, y_held, y_held2)
        elif not return_idx:
            return (x_ref, y_ref), (x_test, y_test), (x_held, y_held)
        else:
            return idx_ref, idx_test, idx_held
    # initially assume equal distribution of topics in the reference data
    n_ref, n_test = 2000, 2000
    classes = dataset.target_names
    n_classes = len(classes)
    n_ref_c = n_ref // n_classes
    n_test_c = n_test // n_classes
    
    (x_ref, y_ref), (x_test, y_test), (x_held, y_held) = split_data(x_drift, y_drift, n_ref_c, n_test_c)
    model = EmbeddingModel()
    emb_ref = model(x_ref)
    emb_test = model(x_test)
    print(f'Shape of embedded reference and test data: {emb_ref.shape} - {emb_test.shape}')
    umap_model = UMAPModel()
    emb_held = model(x_held)
    umap_model.fit(emb_held, y=y_held)
    cluster_ref = umap_model.predict(emb_ref)
    cluster_test = umap_model.predict(emb_test)
    plot_clusters(cluster_ref, y_ref, classes, title='Reference data: clustered news topics')
    plot_clusters(cluster_test, y_test, classes, title='Test data: clustered news topics')
    from scipy.special import softmax
    
    def context(x: List[str], y: np.ndarray):  # y only needed for the data loader
        """ Condition on classifier prediction probabilities. """
        loader = DataLoader(TorchDataset(x, y), batch_size=32, shuffle=False)
        logits = eval_model(clf.eval(), loader, verbose=0)[0]
        return softmax(logits, -1)
    #| code_folding: []
    emb_drift = model(x_drift)
    c_drift = context(x_drift, y_drift)
    #| scrolled: true
    from alibi_detect.cd import MMDDrift, ContextMMDDrift
    
    n_runs = 50  # number of drift detection runs, each with a different reference and test sample
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        idx = np.random.choice(n_drift, size=n_drift, replace=False)
        idx_ref, idx_test = idx[:n_ref], idx[n_ref:n_ref+n_test]
        emb_ref, c_ref = emb_drift[idx_ref], c_drift[idx_ref]
        emb_test, c_test = emb_drift[idx_test], c_drift[idx_test]
        
        # mmd drift detector
        dd_mmd = MMDDrift(emb_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(emb_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector 
        dd_cad = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(emb_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    import statsmodels.api as sm
    from scipy.stats import uniform
    
    
    def plot_p_val_qq(p_vals: np.ndarray, title: str) -> None:
        fig, axes = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(12,10))
        fig.suptitle(title)
        n = len(p_vals)
        for i in range(9):
            unifs = p_vals if i==4 else np.random.rand(n)
            sm.qqplot(unifs, uniform(), line='45', ax=axes[i//3,i%3])
            if i//3 < 2:
                axes[i//3,i%3].set_xlabel('')
            if i%3 != 0:
                axes[i//3,i%3].set_ylabel('')
    plot_p_val_qq(p_vals_mmd, 'Q-Q plot MMD detector')
    plot_p_val_qq(p_vals_cad, 'Q-Q plot Context-Aware MMD detector')
    n_ref_c = 2000 // n_classes
    n_test_c = [100] * n_classes
    n_test_c[4], n_test_c[5] = 50, 50  # few stories on Mac/Windows
    n_test_c[9], n_test_c[10] = 150, 150  # more stories on baseball/hockey
    #| scrolled: true
    n_runs = 50
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        idx_ref, idx_test, _ = split_data(x_drift, y_drift, n_ref_c, n_test_c, return_idx=True)
        emb_ref, c_ref = emb_drift[idx_ref], c_drift[idx_ref]
        emb_test, c_test = emb_drift[idx_test], c_drift[idx_test]
        
        # mmd drift detector
        dd_mmd = MMDDrift(emb_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(emb_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector 
        dd_cad = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(emb_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    plot_p_val_qq(p_vals_mmd, 'Q-Q plot MMD detector')
    plot_p_val_qq(p_vals_cad, 'Q-Q plot Context-Aware MMD detector')
    import seaborn as sns
    
    def plot_hist(
        p_vals: List[np.ndarray],
        title: str,
        colors: List[str] = ['salmon', 'turquoise'],
        methods: List[str] = ['MMD', 'CA-MMD']
    ):
        for p_val, method, color in zip(p_vals, methods, colors):
            sns.distplot(p_val, color=color, norm_hist=True, kde=True, label=f'{method}', hist=True)
            plt.legend(loc='upper right')
        plt.xlim(0, 1)
        plt.ylim(0, 20)
        plt.ylabel('Density')
        plt.xlabel('p-values')
        plt.title(title)
        plt.show();
        
    p_vals = [p_vals_mmd, p_vals_cad]
    title = 'p-value distribution for a change in subpopulation prevalence'
    plot_hist(p_vals, title)
    # map the original target labels to super classes
    class_map = {
        0: [1, 2, 3, 4, 5],
        1: [7, 8, 9, 10],
        2: [11, 12, 13, 14],
        3: [6],
        4: [16, 17, 18],
        5: [0, 15, 19]
    }
    
    def map_to_super(y: np.ndarray):
        y_super = np.zeros_like(y)
        for k, v in class_map.items():
            for _ in v:
                idx_chg = np.where(y == _)[0]
                y_super[idx_chg] = k
        return y_super
    
    y_drift_super = map_to_super(y_drift)
    n_super = len(list(class_map.keys()))
    def ohe_super_preds(x: List[str], y: np.ndarray):
        classes = np.argmax(context(x, y), -1)  # class predictions
        classes_super = map_to_super(classes)  # map to super classes
        return np.eye(n_super, dtype=np.float32)[classes_super]  # return OHE
    #| scrolled: true
    n_ref_c, n_test_c = 1000 // n_super, 1000 // n_super
    n_runs = 50
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        (x_ref, y_ref, y_ref2), (x_test, y_test, y_test2), (x_held, y_held, y_held2) = \
            split_data(x_drift, y_drift_super, n_ref_c, n_test_c, y2=y_drift)
    
        # remove baseball and hockey from the recreation super class in the test set
        idx_bb, idx_hock = np.where(y_test2 == 9)[0], np.where(y_test2 == 10)[0]
        idx_remove = np.concatenate([idx_bb, idx_hock], 0)
        x_test = [x_test[_] for _ in np.arange(len(x_test)) if _ not in idx_remove]
        y_test = np.delete(y_test, idx_remove)
        
        # embed text
        emb_ref = model(x_ref)
        emb_test = model(x_test)
        
        # mmd drift detector
        dd_mmd = MMDDrift(emb_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(emb_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector 
        c_ref = ohe_super_preds(x_ref, y_ref)
        c_test = ohe_super_preds(x_test, y_test)
        dd_cad = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(emb_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    threshold = .05
    print(f'Power at {threshold * 100}% significance level')
    print(f'MMD: {(p_vals_mmd < threshold).mean():.3f}')
    print(f'Context-aware MMD: {(p_vals_cad < threshold).mean():.3f}')
    
    p_vals = [p_vals_mmd, p_vals_cad]
    title = 'p-value distribution for a change in subpopulation distribution'
    plot_hist(p_vals, title)
    #| scrolled: true
    from datasets import load_dataset
    
    dataset = load_dataset("imdb")
    x_imdb = dataset['train']['text']
    n_imdb = len(x_imdb)
    
    n_test_imdb = 100
    n_ref_c = 1000 // n_classes
    n_test_c = 1000 // n_classes
    n_runs = 50
    
    p_vals_mmd, p_vals_cad = [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        idx_ref, idx_test, _ = split_data(x_drift, y_drift, n_ref_c, n_test_c, return_idx=True)
        emb_ref, c_ref = emb_drift[idx_ref], c_drift[idx_ref]
        emb_test, c_test = emb_drift[idx_test], c_drift[idx_test]
        
        # add random imdb reviews to the test data
        idx_imdb = np.random.choice(n_imdb, n_test_imdb, replace=False)
        x_imdb_sample = [x_imdb[_] for _ in idx_imdb]
        emb_imdb = model(x_imdb_sample)
        c_imdb = context(x_imdb_sample, np.zeros(len(x_imdb_sample)))  # value second arg does not matter
        emb_test = np.concatenate([emb_test, emb_imdb], 0)
        c_test = np.concatenate([c_test, c_imdb], 0)
        
        # mmd drift detector
        dd_mmd = MMDDrift(emb_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(emb_test)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
        
        # context-aware mmd drift detector
        dd_cad = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_cad = dd_cad.predict(emb_test, c_test)
        p_vals_cad.append(preds_cad['data']['p_val'])
        
    p_vals_mmd = np.array(p_vals_mmd)
    p_vals_cad = np.array(p_vals_cad)
    threshold = .05
    print(f'Power at {threshold * 100}% significance level')
    print(f'MMD: {(p_vals_mmd < threshold).mean():.3f}')
    print(f'Context-aware MMD: {(p_vals_cad < threshold).mean():.3f}')
    from sklearn.decomposition import KernelPCA
    from sklearn.mixture import GaussianMixture
    
    # embed training data
    emb_train = model(x_train)
    
    # apply kernel PCA to reduce dimensionality
    kernel_pca = KernelPCA(n_components=10, kernel='linear')
    kernel_pca.fit(emb_train)
    emb_train_pca = kernel_pca.transform(emb_train)
    emb_drift_pca = kernel_pca.transform(emb_drift)
    
    # cluster the data
    y_train_super = map_to_super(y_train)
    n_clusters = len(np.unique(y_train_super))
    gmm = GaussianMixture(n_components=n_clusters, covariance_type='full', random_state=2022)
    gmm.fit(emb_train_pca)
    c_all_proba = gmm.predict_proba(emb_drift_pca)
    c_all_class = gmm.predict(emb_drift_pca)
    # determine cluster proportions for the reference and test samples
    n_ref_c = [100, 100, 100, 100, 100, 100]
    n_test_c = [50, 50, 100, 25, 75, 25]
    
    def sample_from_clusters():
        idx_ref, idx_test = [], []
        for _, (i_ref, i_test) in enumerate(zip(n_ref_c, n_test_c)):
            idx_c = np.where(c_all_class == _)[0]
            idx_shuffle = np.random.choice(idx_c, size=len(idx_c), replace=False)
            idx_ref.append(idx_shuffle[:i_ref])
            idx_test.append(idx_shuffle[i_ref:i_ref+i_test])
        idx_ref = np.concatenate(idx_ref, 0)
        idx_test = np.concatenate(idx_test, 0)
        c_ref = c_all_proba[idx_ref]
        c_test = c_all_proba[idx_test]
        emb_ref = emb_drift[idx_ref]
        emb_test = emb_drift[idx_test]
        return c_ref, c_test, emb_ref, emb_test
    #| scrolled: true
    n_test_imdb = 100  # number of imdb instances for each run
    
    n_runs = 50
    
    p_vals_null, p_vals_alt, p_vals_mmd = [], [], []
    for _ in tqdm(range(n_runs)):
        
        # sample data
        c_ref, c_test_null, emb_ref, emb_test_null = sample_from_clusters()
        
        # sample random imdb reviews
        idx_imdb = np.random.choice(n_imdb, n_test_imdb, replace=False)
        x_imdb_sample = [x_imdb[_] for _ in idx_imdb]
        emb_imdb = model(x_imdb_sample)
        c_imdb = gmm.predict_proba(kernel_pca.transform(emb_imdb))
        
        # now we mix in-distribution instances with the imdb reviews
        emb_alt = np.concatenate([emb_test_null[:n_test_imdb], emb_imdb], 0)
        c_alt = np.concatenate([c_test_null[:n_test_imdb], c_imdb], 0)
        
        # mmd drift detector
        dd_mmd = MMDDrift(emb_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_mmd = dd_mmd.predict(emb_test_null)
        p_vals_mmd.append(preds_mmd['data']['p_val'])
    
        # context-aware mmd drift detector
        dd = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
        preds_null = dd.predict(emb_test_null, c_test_null)
        preds_alt = dd.predict(emb_alt, c_alt)
        p_vals_null.append(preds_null['data']['p_val'])
        p_vals_alt.append(preds_alt['data']['p_val'])
    
    p_vals_null = np.array(p_vals_null)
    p_vals_alt = np.array(p_vals_alt)
    p_vals_mmd = np.array(p_vals_mmd)
    print(f'Power at {threshold * 100}% significance level')
    print(f'Context-aware MMD: {(p_vals_alt < threshold).mean():.3f}')
    plot_p_val_qq(p_vals_mmd, 'Q-Q plot MMD detector when changing the cluster frequencies')
    plot_p_val_qq(p_vals_null, 'Q-Q plot Context-Aware MMD detector when changing the cluster frequencies')
    n_ref_c = 2000 // n_classes
    n_test_c = [0] * n_classes
    n_test_c[9], n_test_c[10] = 200, 200  # only stories on baseball/hockey
    (x_ref, y_ref), (x_test, y_test), _ = split_data(x_drift, y_drift, n_ref_c, n_test_c)
    
    # embed data
    emb_ref = model(x_ref)
    emb_test = model(x_test)
    
    # condition using the classifier predictions
    c_ref = context(x_ref, y_ref)
    c_test = context(x_test, y_test)
    
    # initialise detector and make predictions
    dd = ContextMMDDrift(emb_ref, c_ref, p_val=.05, n_permutations=100, backend='pytorch')
    preds = dd.predict(emb_test, c_test, return_coupling=True)
    
    # no drift is detected since the distribution of 
    # the subpopulations in the test set remain the same
    print(f'p-value: {preds["data"]["p_val"]:.3f}')
    
    # extract coupling matrix between reference and test data
    W_01 = preds['data']['coupling_xy']
    
    # sum over test instances
    w_ref = W_01.sum(1)
    # Map the top assigned reference weights to the associated instance labels
    # and select top 2 * n_ref_c. This tells us what the labels were of the reference 
    # instances with the highest weights in the coupling matrix W_01.
    # Ideally this would correspond to instances from the baseball and hockey 
    # classes in the reference set (labels 9 and 10).
    inds_ref_sort = np.argsort(w_ref)[::-1]
    y_sort = y_ref[inds_ref_sort][:2 * n_ref_c]
    
    # And indeed, we can see that we mainly matched with the correct reference instances!
    correct_match = np.array([y in [9, 10] for y in y_sort]).mean()
    print(f'The top {100 * correct_match:.2f}% couplings from the top coupled {2 * n_ref_c} instances '
          'come from the baseball and hockey classes!')
    
    # We can also easily see from the sorted coupling weights that the test statistic 
    # focuses on just the baseball and hockey classes in the reference set and then
    # the weights in the coupling matrix W_01 fall of a cliff.
    plt.plot(w_ref[inds_ref_sort]);
    plt.title('Sorted reference weights from the coupling matrix W_01');
    plt.ylabel('Reference instance weight in W_01');
    plt.xlabel('Instances sorted by weight in W_01');
    plt.show()

    Deployment

    • 1. Dataset

    • 2. Outlier detection with a variational autoencoder (VAE)

    • 3. Adversarial detection by matching prediction probabilities

    • 4. Drift detection with Kolmogorov-Smirnov

    1. Dataset

    consists of 60,000 32 by 32 RGB images equally distributed over 10 classes: airplane, automobile, bird, cat, deer, dog, frog, horse, ship and truck.

    2. Outlier detection with a variational autoencoder (VAE)

    Method

    In a nutshell:

    • Train a VAE on normal data so it can reconstruct inliers well

    • If the VAE cannot reconstruct the incoming requests well? Outlier!

    More resources on VAE: ,

    Image source: https://lilianweng.github.io/lil-log/2018/08/12/from-autoencoder-to-beta-vae.html

    Load detector or train from scratch

    The pretrained outlier and adversarial detectors used in the notebook can be found . You can use the built-in fetch_detector function which saves the pre-trained models in a local directory filepath and loads the detector. Alternatively, you can train a detector from scratch:

    Let's check whether the model manages to reconstruct the in-distribution training data:

    Setting the threshold

    Finding good threshold values can be tricky since they are typically not easy to interpret. The infer_threshold method helps finding a sensible value. We need to pass a batch of instances X and specify what percentage of those we consider to be normal via threshold_perc.

    Create and detect outliers

    We can create some outliers by applying a random noise mask to the original instances:

    Deploy the detector

    For this example we use the open source deployment platform and eventing based project which allows serverless components to be connected to event streams. The Seldon Core payload logger sends events containing model requests to the Knative broker which can farm these out to serverless components such as the outlier, drift or adversarial detection modules. Further eventing components can be added to feed off events produced by these components to send onwards to, for example, alerting or storage modules. This happens asynchronously.

    We already configured a cluster on DigitalOcean with Seldon Core installed. The configuration steps to set everything up from scratch are detailed in .

    First we get the IP address of the Istio Ingress Gateway. This assumes Istio is installed with a LoadBalancer.

    We define some utility functions for the prediction of the deployed model.

    Let's make a prediction on the original instance:

    Let's check the message dumper for the output of the outlier detector:

    We then make a prediction on the perturbed instance:

    Although the prediction is still correct, the instance is clearly an outlier:

    3. Adversarial detection by matching prediction probabilities

    Method

    The adversarial detector is based on . Usually, autoencoders are trained to find a transformation $T$ that reconstructs the input instance $x$ as accurately as possible with loss functions that are suited to capture the similarities between x and $x'$ such as the mean squared reconstruction error. The novelty of the adversarial autoencoder (AE) detector relies on the use of a classification model-dependent loss function based on a distance metric in the output space of the model to train the autoencoder network. Given a classification model $M$ we optimise the weights of the autoencoder such that the between the model predictions on $x$ and on $x'$ is minimised. Without the presence of a reconstruction loss term $x'$ simply tries to make sure that the prediction probabilities $M(x')$ and $M(x)$ match without caring about the proximity of $x'$ to $x$. As a result, $x'$ is allowed to live in different areas of the input feature space than $x$ with different decision boundary shapes with respect to the model $M$. The carefully crafted adversarial perturbation which is effective around x does not transfer to the new location of $x'$ in the feature space, and the attack is therefore neutralised. Training of the autoencoder is unsupervised since we only need access to the model prediction probabilities and the normal training instances. We do not require any knowledge about the underlying adversarial attack and the classifier weights are frozen during training.

    The detector can be used as follows:

    • An adversarial score $S$ is computed. $S$ equals the K-L divergence between the model predictions on $x$ and $x'$.

    • If $S$ is above a threshold (explicitly defined or inferred from training data), the instance is flagged as adversarial.

    • For adversarial instances, the model $M$ uses the reconstructed instance $x'$ to make a prediction. If the adversarial score is below the threshold, the model makes a prediction on the original instance $x$.

    This procedure is illustrated in the diagram below:

    The method is very flexible and can also be used to detect common data corruptions and perturbations which negatively impact the model performance.

    Utility functions

    Rescale data

    The ResNet classification model is trained on data standardized by instance:

    Load pre-trained classifier

    Check the predictions on the test:

    Adversarial attack

    We investigate both and attacks. You can simply load previously found adversarial instances on the pretrained ResNet-56 model. The attacks are generated by using :

    We can verify that the accuracy of the classifier drops to almost $0$%:

    Let's visualise some adversarial instances:

    Load or train and evaluate the adversarial detector

    We can again either fetch the pretrained detector from a or train one from scratch:

    The detector first reconstructs the input instances which can be adversarial. The reconstructed input is then fed to the classifier to compute the adversarial score. If the score is above a threshold, the instance is classified as adversarial and the detector tries to correct the attack. Let's investigate what happens when we reconstruct attacked instances and make predictions on them:

    Accuracy on attacked vs. reconstructed instances:

    The detector restores the accuracy after the attacks from almost $0$% to well over $80$%! We can compute the adversarial scores and inspect some of the reconstructed instances:

    The ROC curves and AUC values show the effectiveness of the adversarial score to detect adversarial instances:

    The threshold for the adversarial score can be set via infer_threshold. We need to pass a batch of instances $X$ and specify what percentage of those we consider to be normal via threshold_perc. Assume we have only normal instances some of which the model has misclassified leading to a higher score if the reconstruction picked up features from the correct class or some might look adversarial in the first place. As a result, we set our threshold at $95$%:

    The correct method of the detector executes the diagram in Figure 1. First the adversarial scores is computed. For instances where the score is above the threshold, the classifier prediction on the reconstructed instance is returned. Otherwise the original prediction is kept. The method returns a dictionary containing the metadata of the detector, whether the instances in the batch are adversarial (above the threshold) or not, the classifier predictions using the correction mechanism and both the original and reconstructed predictions. Let's illustrate this on a batch containing some adversarial (C&W) and original test set instances:

    Let's check the model performance:

    This can be improved with the correction mechanism:

    There are a few other tricks highlighted in the (temperature scaling and hidden layer K-L divergence) and implemented in Alibi Detect which can further boost the adversarial detector's performance. Check for more details.

    4. Drift detection with Kolmogorov-Smirnov

    Method

    The drift detector applies feature-wise two-sample (K-S) tests. For multivariate data, the obtained p-values for each feature are aggregated either via the or the (FDR) correction. The Bonferroni correction is more conservative and controls for the probability of at least one false positive. The FDR correction on the other hand allows for an expected fraction of false positives to occur.

    For high-dimensional data, we typically want to reduce the dimensionality before computing the feature-wise univariate K-S tests and aggregating those via the chosen correction method. Following suggestions in , we incorporate Untrained AutoEncoders (UAE), black-box shift detection using the classifier's softmax outputs () and as out-of-the box preprocessing methods. Preprocessing methods which do not rely on the classifier will usually pick up drift in the input data, while BBSDs focuses on label shift. The which is part of the library can also be transformed into a drift detector picking up drift that reduces the performance of the classification model. We can therefore combine different preprocessing techniques to figure out if there is drift which hurts the model performance, and whether this drift can be classified as input drift or label shift.

    Note that the library also has a drift detector based on the and contains as well.

    Dataset

    We will use the CIFAR-10-C dataset () to evaluate the drift detector. The instances in CIFAR-10-C come from the test set in CIFAR-10 but have been corrupted and perturbed by various types of noise, blur, brightness etc. at different levels of severity, leading to a gradual decline in the classification model performance. We also check for drift against the original test set with class imbalances.

    We can select from the following corruption types at 5 severity levels:

    Let's pick a subset of the corruptions at corruption level 5. Each corruption type consists of perturbations on all of the original test set images.

    We split the original test set in a reference dataset and a dataset which should not be rejected under the H0 of the K-S test. We also split the corrupted data by corruption type:

    We can visualise the same instance for each corruption type:

    We can also verify that the performance of a ResNet-32 classification model on CIFAR-10 drops significantly on this perturbed dataset:

    Given the drop in performance, it is important that we detect the harmful data drift!

    Detect drift

    We are trying to detect data drift on high-dimensional (32x32x3) data using an aggregation of univariate K-S tests. It therefore makes sense to apply dimensionality reduction first. Some dimensionality reduction methods also used in are readily available: UAE (Untrained AutoEncoder), BBSDs (black-box shift detection using the classifier's softmax outputs) and PCA (using scikit-learn).

    Untrained AutoEncoder

    First we try UAE:

    Let's check whether the detector thinks drift occurred within the original test set:

    As expected, no drift occurred. We can also inspect the feature-wise K-S statistics, threshold value and p-values for each univariate K-S test by (encoded) feature before the multivariate correction. Most of them are well above the $0.05$ threshold:

    Let's now check the predictions on the perturbed data:

    BBSDs

    For BBSDs, we use the classifier's softmax outputs for black-box shift detection. This method is based on .

    Here we use the output of the softmax layer to detect the drift, but other hidden layers can be extracted as well by setting 'layer' to the index of the desired hidden layer in the model:

    There is again no drift on the original held out test set:

    We compare this with the perturbed data:

    For more functionality and examples, such as updating the reference data with reservoir sampling or picking another multivariate correction mechanism, check out .

    Leveraging the adversarial detector for malicious drift detection

    While monitoring covariate and predicted label shift is all very interesting and exciting, at the end of the day we are mainly interested in whether the drift actually hurt the model performance significantly. To this end, we can leverage the adversarial detector and measure univiariate drift on the adversarial scores!

    Make drift predictions on the original test set and corrupted data:

    We can therefore use the scores of the detector itself to quantify the harmfulness of the drift! We can generalise this to all the corruptions at each severity level in CIFAR-10-C.

    On the plot below we show the mean values and standard deviations of the adversarial scores per severity level. The plot shows the mean adversarial scores (lhs) and ResNet-32 accuracies (rhs) for increasing data corruption severity levels. Level 0 corresponds to the original test set. Harmful scores are scores from instances which have been flipped from the correct to an incorrect prediction because of the corruption. Not harmful means that the prediction was unchanged after the corruption. The chart can be reproduced in .

    Deploy

    We can deploy the drift detector in a similar fashion as the . For a more detailed step-by-step overview of the deployment process, check .

    The deployed drift detector accumulates requests until a predefined drift_batch_size is reached, in our case $5000$ which is defined in the and set in the . After $5000$ instances, the batch is cleared and fills up again.

    We now run the same test on some corrupted data:

    CIFAR10
    paper
    excellent blog post
    here
    Seldon Core
    Knative
    this example notebook
    Adversarial Detection and Correction by Matching Prediction Distributions
    KL-divergence
    Carlini-Wagner (C&W)
    SLIDE
    Foolbox
    Google Cloud Bucket
    paper
    this example notebook
    Kolmogorov-Smirnov
    Bonferroni
    False Discovery Rate
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    BBSDs
    PCA
    adversarial detector
    Maximum Mean Discrepancy
    drift on text functionality
    Hendrycks & Dietterich, 2019
    Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift
    Detecting and Correcting for Label Shift with Black Box Predictors
    this example notebook
    this notebook
    outlier detector
    this notebook
    yaml for the deployment
    drift detector wrapper
    vae-lillog.png
    deploy-diagram.png
    adversarialae.png
    adversarialscores.png
    #| code_folding: [0]
    # imports and plot examples
    import matplotlib.pyplot as plt
    %matplotlib inline
    import tensorflow as tf
    
    (X_train, y_train), (X_test, y_test) = tf.keras.datasets.cifar10.load_data()
    X_train = X_train.astype('float32') / 255
    X_test = X_test.astype('float32') / 255
    y_train = y_train.astype('int64').reshape(-1,)
    y_test = y_test.astype('int64').reshape(-1,)
    print('Train: ', X_train.shape, y_train.shape)
    print('Test: ', X_test.shape, y_test.shape)
    
    plt.figure(figsize=(10, 10))
    n = 4
    for i in range(n ** 2):
        plt.subplot(n, n, i + 1)
        plt.imshow(X_train[i])
        plt.axis('off')
    plt.show();
    #| code_folding: [0]
    # more imports
    import logging
    import numpy as np
    import os
    
    from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Dense
    from tensorflow.keras.layers import Flatten, Layer, Reshape, InputLayer
    from tensorflow.keras.regularizers import l1
    
    from alibi_detect.od import OutlierVAE
    from alibi_detect.utils.fetching import fetch_detector
    from alibi_detect.utils.perturbation import apply_mask
    from alibi_detect.saving import save_detector, load_detector
    from alibi_detect.utils.visualize import plot_instance_score, plot_feature_outlier_image
    
    logger = tf.get_logger()
    logger.setLevel(logging.ERROR)
    load_pretrained = True
    #| scrolled: true
    filepath = os.path.join(os.getcwd(), 'outlier')
    detector_type = 'outlier'
    dataset = 'cifar10'
    detector_name = 'OutlierVAE'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:  # load pre-trained detector
        od = fetch_detector(filepath, detector_type, dataset, detector_name)
    else:  # define model, initialize, train and save outlier detector
        
        # define encoder and decoder networks
        latent_dim = 1024
        encoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(32, 32, 3)),
              Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu)
          ]
        )
    
        decoder_net = tf.keras.Sequential(
          [
              InputLayer(input_shape=(latent_dim,)),
              Dense(4*4*128),
              Reshape(target_shape=(4, 4, 128)),
              Conv2DTranspose(256, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(64, 4, strides=2, padding='same', activation=tf.nn.relu),
              Conv2DTranspose(3, 4, strides=2, padding='same', activation='sigmoid')
          ]
        )
        
        # initialize outlier detector
        od = OutlierVAE(
            threshold=.015,  # threshold for outlier score
            encoder_net=encoder_net,  # can also pass VAE model instead
            decoder_net=decoder_net,  # of separate encoder and decoder
            latent_dim=latent_dim
        )
        
        # train
        od.fit(X_train, epochs=50, verbose=False)
        
        # save the trained outlier detector
        save_detector(od, filepath)
    #| code_folding: [0]
    # plot original and reconstructed instance
    idx = 8
    X = X_train[idx].reshape(1, 32, 32, 3)
    X_recon = od.vae(X)
    plt.imshow(X.reshape(32, 32, 3)); plt.axis('off'); plt.show()
    plt.imshow(X_recon.numpy().reshape(32, 32, 3)); plt.axis('off'); plt.show()
    print('Current threshold: {}'.format(od.threshold))
    od.infer_threshold(X_train, threshold_perc=99, batch_size=128)  # assume 1% of the training data are outliers
    print('New threshold: {}'.format(od.threshold))
    np.random.seed(0)
    
    i = 1
    
    # create masked instance
    x = X_test[i].reshape(1, 32, 32, 3)
    x_mask, mask = apply_mask(
        x,
        mask_size=(8,8),
        n_masks=1,
        channels=[0,1,2],
        mask_type='normal',
        noise_distr=(0,1),
        clip_rng=(0,1)
    )
    
    # predict outliers and reconstructions
    sample = np.concatenate([x_mask, x])
    preds = od.predict(sample)
    x_recon = od.vae(sample).numpy()
    #| code_folding: [0]
    # check if outlier and visualize outlier scores
    labels = ['No!', 'Yes!']
    print(f"Is original outlier? {labels[preds['data']['is_outlier'][1]]}")
    print(f"Is perturbed outlier? {labels[preds['data']['is_outlier'][0]]}")
    plot_feature_outlier_image(preds, sample, x_recon, max_instances=1)
    CLUSTER_IPS=!(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    CLUSTER_IP=CLUSTER_IPS[0]
    print(CLUSTER_IP)
    SERVICE_HOSTNAMES=!(kubectl get ksvc vae-outlier -o jsonpath='{.status.url}' | cut -d "/" -f 3)
    SERVICE_HOSTNAME_VAEOD=SERVICE_HOSTNAMES[0]
    print(SERVICE_HOSTNAME_VAEOD)
    #| code_folding: []
    import json
    import requests
    from typing import Union
    
    classes = ('plane', 'car', 'bird', 'cat', 'deer', 
               'dog', 'frog', 'horse', 'ship', 'truck')
    
    def predict(x: np.ndarray) -> Union[str, list]:
        """ Model prediction. """
        formData = {
        'instances': x.tolist()
        }
        headers = {}
        res = requests.post(
            'http://'+CLUSTER_IP+'/seldon/default/tfserving-cifar10/v1/models/resnet32/:predict', 
            json=formData, 
            headers=headers
        )
        if res.status_code == 200:
            return classes[np.array(res.json()["predictions"])[0].argmax()]
        else:
            print("Failed with ",res.status_code)
            return []
        
        
    def outlier(x: np.ndarray) -> Union[dict, list]:
        """ Outlier prediction. """
        formData = {
        'instances': x.tolist()
        }
        headers = {
            "Alibi-Detect-Return-Feature-Score": "true",
            "Alibi-Detect-Return-Instance-Score": "true"
        }
        headers["Host"] = SERVICE_HOSTNAME_VAEOD
        res = requests.post('http://'+CLUSTER_IP+'/', json=formData, headers=headers)
        if res.status_code == 200:
            od = res.json()
            od["data"]["feature_score"] = np.array(od["data"]["feature_score"])
            od["data"]["instance_score"] = np.array(od["data"]["instance_score"])
            return od
        else:
            print("Failed with ",res.status_code)
            return []
        
        
    def show(x: np.ndarray) -> None:
        plt.imshow(x.reshape(32, 32, 3))
        plt.axis('off')
        plt.show()
    show(x)
    predict(x)
    res=!kubectl logs $(kubectl get pod -l serving.knative.dev/configuration=message-dumper -o jsonpath='{.items[0].metadata.name}') user-container
    data = []
    for i in range(0,len(res)):
        if res[i] == 'Data,':
            data.append(res[i+1])
    j = json.loads(json.loads(data[0]))
    print("Outlier?",labels[j["data"]["is_outlier"]==[1]])
    show(x_mask)
    predict(x_mask)
    res=!kubectl logs $(kubectl get pod -l serving.knative.dev/configuration=message-dumper -o jsonpath='{.items[0].metadata.name}') user-container
    data= []
    for i in range(0,len(res)):
        if res[i] == 'Data,':
            data.append(res[i+1])
    j = json.loads(json.loads(data[1]))
    print("Outlier?",labels[j["data"]["is_outlier"]==[1]])
    preds = outlier(x_mask)
    plot_feature_outlier_image(preds, x_mask, X_recon=None)
    #| code_folding: [0]
    # more imports
    from sklearn.metrics import roc_curve, auc
    from alibi_detect.ad import AdversarialAE
    from alibi_detect.datasets import fetch_attack
    from alibi_detect.utils.fetching import fetch_tf_model
    from alibi_detect.utils.tensorflow import predict_batch
    #| code_folding: [0]
    # instance scaling and plotting utility functions
    def scale_by_instance(X: np.ndarray) -> np.ndarray:
        mean_ = X.mean(axis=(1, 2, 3)).reshape(-1, 1, 1, 1)
        std_ = X.std(axis=(1, 2, 3)).reshape(-1, 1, 1, 1)
        return (X - mean_) / std_, mean_, std_
    
    
    def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
        return (y_true == y_pred).astype(int).sum() / y_true.shape[0]
    
    
    def plot_adversarial(idx: list,
                         X: np.ndarray,
                         y: np.ndarray,
                         X_adv: np.ndarray, 
                         y_adv: np.ndarray,
                         mean: np.ndarray, 
                         std: np.ndarray, 
                         score_x: np.ndarray = None,
                         score_x_adv: np.ndarray = None,
                         X_recon: np.ndarray = None,
                         y_recon: np.ndarray = None,
                         figsize: tuple = (10, 5)) -> None:
        
        # category map from class numbers to names
        cifar10_map = {0: 'airplane', 1: 'automobile', 2: 'bird', 3: 'cat', 4: 'deer', 5: 'dog',
                       6: 'frog', 7: 'horse', 8: 'ship', 9: 'truck'}
        
        nrows = len(idx)
        ncols = 3 if isinstance(X_recon, np.ndarray) else 2
        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=figsize)
        
        n_subplot = 1
        for i in idx:
            
            # rescale images in [0, 1]
            X_adj = (X[i] * std[i] + mean[i]) / 255
            X_adv_adj = (X_adv[i] * std[i] + mean[i]) / 255
            if isinstance(X_recon, np.ndarray):
                X_recon_adj = (X_recon[i] * std[i] + mean[i]) / 255
            
            # original image
            plt.subplot(nrows, ncols, n_subplot)
            plt.axis('off')
            if i == idx[0]:
                if isinstance(score_x, np.ndarray):
                    plt.title('CIFAR-10 Image \n{}: {:.3f}'.format(cifar10_map[y[i]], score_x[i]))
                else:
                    plt.title('CIFAR-10 Image \n{}'.format(cifar10_map[y[i]]))
            else:
                if isinstance(score_x, np.ndarray):
                    plt.title('{}: {:.3f}'.format(cifar10_map[y[i]], score_x[i]))
                else:
                    plt.title('{}'.format(cifar10_map[y[i]]))
            plt.imshow(X_adj)
            n_subplot += 1
            
            # adversarial image
            plt.subplot(nrows, ncols, n_subplot)
            plt.axis('off')
            if i == idx[0]:
                if isinstance(score_x_adv, np.ndarray):
                    plt.title('Adversarial \n{}: {:.3f}'.format(cifar10_map[y_adv[i]], score_x_adv[i]))
                else:
                    plt.title('Adversarial \n{}'.format(cifar10_map[y_adv[i]]))
            else:
                if isinstance(score_x_adv, np.ndarray):
                    plt.title('{}: {:.3f}'.format(cifar10_map[y_adv[i]], score_x_adv[i]))
                else:
                    plt.title('{}'.format(cifar10_map[y_adv[i]]))
            plt.imshow(X_adv_adj)
            n_subplot += 1
         
            # reconstructed image
            if isinstance(X_recon, np.ndarray):
                plt.subplot(nrows, ncols, n_subplot)
                plt.axis('off')
                if i == idx[0]:
                    plt.title('AE Reconstruction \n{}'.format(cifar10_map[y_recon[i]]))
                else:
                    plt.title('{}'.format(cifar10_map[y_recon[i]]))
                plt.imshow(X_recon_adj)
                n_subplot += 1
        
        plt.show()
    
        
    def plot_roc(roc_data: dict, figsize: tuple = (10,5)):
        plot_labels = []
        scores_attacks = []
        labels_attacks = []
        for k, v in roc_data.items():
            if 'original' in k:
                continue
            score_x = roc_data[v['normal']]['scores']
            y_pred = roc_data[v['normal']]['predictions']
            score_v = v['scores']
            y_pred_v = v['predictions']
            labels_v = np.ones(score_x.shape[0])
            idx_remove = np.where(y_pred == y_pred_v)[0]
            labels_v = np.delete(labels_v, idx_remove)
            score_v = np.delete(score_v, idx_remove)
            scores = np.concatenate([score_x, score_v])
            labels = np.concatenate([np.zeros(y_pred.shape[0]), labels_v]).astype(int)
            scores_attacks.append(scores)
            labels_attacks.append(labels)
            plot_labels.append(k)
        
        for sc_att, la_att, plt_la in zip(scores_attacks, labels_attacks, plot_labels):
            fpr, tpr, thresholds = roc_curve(la_att, sc_att)
            roc_auc = auc(fpr, tpr)
            label = str('{}: AUC = {:.2f}'.format(plt_la, roc_auc))
            plt.plot(fpr, tpr, lw=1, label='{}: AUC={:.4f}'.format(plt_la, roc_auc))
    
        plt.plot([0, 1], [0, 1], color='black', lw=1, linestyle='--')
        plt.xlim([0.0, 1.0])
        plt.ylim([0.0, 1.05])
        plt.xlabel('False Positive Rate')
        plt.ylabel('True Positive Rate')
        plt.title('{}'.format('ROC curve'))
        plt.legend(loc="lower right", ncol=1)
        plt.grid()
        plt.show()
    #| code_folding: [0]
    # rescale data
    X_train, mean_train, std_train = scale_by_instance(X_train * 255.)
    X_test, mean_test, std_test = scale_by_instance(X_test * 255.)
    scale = (mean_train, std_train), (mean_test, std_test)
    dataset = 'cifar10'
    model = 'resnet56'
    clf = fetch_tf_model(dataset, model)
    y_pred = predict_batch(X_test, clf, batch_size=32).argmax(axis=1)
    acc_y_pred = accuracy(y_test, y_pred)
    print('Accuracy: {:.4f}'.format(acc_y_pred))
    #| code_folding: []
    # C&W attack
    data_cw = fetch_attack(dataset, model, 'cw')
    X_train_cw, X_test_cw = data_cw['data_train'], data_cw['data_test']
    meta_cw = data_cw['meta'] # metadata with hyperparameters of the attack
    # SLIDE attack
    data_slide = fetch_attack(dataset, model, 'slide')
    X_train_slide, X_test_slide = data_slide['data_train'], data_slide['data_test']
    meta_slide = data_slide['meta']
    y_pred_cw = predict_batch(X_test_cw, clf, batch_size=32).argmax(axis=1)
    y_pred_slide = predict_batch(X_test_slide, clf, batch_size=32).argmax(axis=1)
    acc_y_pred_cw = accuracy(y_test, y_pred_cw)
    acc_y_pred_slide = accuracy(y_test, y_pred_slide)
    print('Accuracy: cw {:.4f} -- SLIDE {:.4f}'.format(acc_y_pred_cw, acc_y_pred_slide))
    #| code_folding: [0]
    # plot attacked instances
    idx = [3, 4]
    print('C&W attack...')
    plot_adversarial(idx, X_test, y_pred, X_test_cw, y_pred_cw, 
                     mean_test, std_test, figsize=(10, 10))
    print('SLIDE attack...')
    plot_adversarial(idx, X_test, y_pred, X_test_slide, y_pred_slide, 
                     mean_test, std_test, figsize=(10, 10))
    load_pretrained = True
    #| scrolled: true
    filepath = os.path.join(os.getcwd(), 'adversarial')
    detector_type = 'adversarial'
    detector_name = 'base'
    filepath = os.path.join(filepath, detector_name)
    if load_pretrained:
        ad = fetch_detector(filepath, detector_type, dataset, detector_name, model=model)
    else:  # train detector from scratch
        
        # define encoder and decoder networks
        encoder_net = tf.keras.Sequential(
                [
                    InputLayer(input_shape=(32, 32, 3)),
                    Conv2D(32, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(64, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2D(256, 4, strides=2, padding='same', 
                           activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Flatten(),
                    Dense(40)
                ]
            )
        
        decoder_net = tf.keras.Sequential(
            [
                    InputLayer(input_shape=(40,)),
                    Dense(4 * 4 * 128, activation=tf.nn.relu),
                    Reshape(target_shape=(4, 4, 128)),
                    Conv2DTranspose(256, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(64, 4, strides=2, padding='same', 
                                    activation=tf.nn.relu, kernel_regularizer=l1(1e-5)),
                    Conv2DTranspose(3, 4, strides=2, padding='same', 
                                    activation=None, kernel_regularizer=l1(1e-5))
                ]
            )
        
        # initialise and train detector
        ad = AdversarialAE(
            encoder_net=encoder_net, 
            decoder_net=decoder_net, 
            model=clf
        )
        ad.fit(X_train, epochs=40, batch_size=64, verbose=True)
        
        # save the trained adversarial detector
        save_detector(ad, filepath)
    X_recon_cw = predict_batch(X_test_cw, ad.ae, batch_size=32)
    X_recon_slide = predict_batch(X_test_slide, ad.ae, batch_size=32)
    y_recon_cw = predict_batch(X_recon_cw, clf, batch_size=32).argmax(axis=1)
    y_recon_slide = predict_batch(X_recon_slide, clf, batch_size=32).argmax(axis=1)
    acc_y_recon_cw = accuracy(y_test, y_recon_cw)
    acc_y_recon_slide = accuracy(y_test, y_recon_slide)
    print('Accuracy after C&W attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_cw, acc_y_recon_cw))
    print('Accuracy after SLIDE attack {:.4f} -- reconstruction {:.4f}'.format(acc_y_pred_slide, acc_y_recon_slide))
    score_x = ad.score(X_test, batch_size=32)
    score_cw = ad.score(X_test_cw, batch_size=32)
    score_slide = ad.score(X_test_slide, batch_size=32)
    #| code_folding: [0]
    #| scrolled: false
    # visualize original, attacked and reconstructed instances with adversarial scores
    print('C&W attack...')
    idx = [10, 13, 14, 16, 17]
    plot_adversarial(idx, X_test, y_pred, X_test_cw, y_pred_cw, mean_test, std_test, 
                     score_x=score_x, score_x_adv=score_cw, X_recon=X_recon_cw, 
                     y_recon=y_recon_cw, figsize=(10, 15))
    print('SLIDE attack...')
    idx = [23, 25, 27, 29, 34]
    plot_adversarial(idx, X_test, y_pred, X_test_slide, y_pred_slide, mean_test, std_test, 
                     score_x=score_x, score_x_adv=score_slide, X_recon=X_recon_slide, 
                     y_recon=y_recon_slide, figsize=(10, 15))
    #| code_folding: [0]
    # plot roc curve
    roc_data = {
        'original': {'scores': score_x, 'predictions': y_pred},
        'C&W': {'scores': score_cw, 'predictions': y_pred_cw, 'normal': 'original'},
        'SLIDE': {'scores': score_slide, 'predictions': y_pred_slide, 'normal': 'original'}
    }
    
    plot_roc(roc_data)
    ad.infer_threshold(X_test, threshold_perc=95, margin=0., batch_size=32)
    print('Adversarial threshold: {:.4f}'.format(ad.threshold))
    n_test = X_test.shape[0]
    np.random.seed(0)
    idx_normal = np.random.choice(n_test, size=1600, replace=False)
    idx_cw = np.random.choice(n_test, size=400, replace=False)
    
    X_mix = np.concatenate([X_test[idx_normal], X_test_cw[idx_cw]])
    y_mix = np.concatenate([y_test[idx_normal], y_test[idx_cw]])
    print(X_mix.shape, y_mix.shape)
    y_pred_mix = predict_batch(X_mix, clf, batch_size=32).argmax(axis=1)
    acc_y_pred_mix = accuracy(y_mix, y_pred_mix)
    print('Accuracy {:.4f}'.format(acc_y_pred_mix))
    preds = ad.correct(X_mix, batch_size=32)
    acc_y_corr_mix = accuracy(y_mix, preds['data']['corrected'])
    print('Accuracy {:.4f}'.format(acc_y_corr_mix))
    #| code_folding: [0]
    # yet again import stuff
    from alibi_detect.cd import KSDrift
    from alibi_detect.cd.preprocess import UAE, HiddenOutput
    from alibi_detect.datasets import fetch_cifar10c, corruption_types_cifar10c
    corruptions = corruption_types_cifar10c()
    print(corruptions)
    corruption = ['gaussian_noise', 'motion_blur', 'brightness', 'pixelate']
    X_corr, y_corr = fetch_cifar10c(corruption=corruption, severity=5, return_X_y=True)
    X_corr = X_corr.astype('float32') / 255
    np.random.seed(0)
    n_test = X_test.shape[0]
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    idx_h0 = np.delete(np.arange(n_test), idx, axis=0)
    X_ref,y_ref = X_test[idx], y_test[idx]
    X_h0, y_h0 = X_test[idx_h0], y_test[idx_h0]
    print(X_ref.shape, X_h0.shape)
    
    X_c = []
    n_corr = len(corruption)
    for i in range(n_corr):
        X_c.append(scale_by_instance(X_corr[i * n_test:(i + 1) * n_test])[0])
    #| code_folding: [0]
    # plot original and corrupted images
    i = 1
    
    n_test = X_test.shape[0]
    plt.title('Original')
    plt.axis('off')
    plt.imshow((X_test[i] * std_test[i] + mean_test[i]) / 255.)
    plt.show()
    for _ in range(len(corruption)):
        plt.title(corruption[_])
        plt.axis('off')
        plt.imshow(X_corr[n_test * _+ i])
        plt.show()
    dataset = 'cifar10'
    model = 'resnet32'
    clf = fetch_tf_model(dataset, model)
    acc = clf.evaluate(X_test, y_test, batch_size=128, verbose=0)[1]
    print('Test set accuracy:')
    print('Original {:.4f}'.format(acc))
    clf_accuracy = {'original': acc}
    for _ in range(len(corruption)):
        acc = clf.evaluate(X_c[_], y_test, batch_size=128, verbose=0)[1]
        clf_accuracy[corruption[_]] = acc
        print('{} {:.4f}'.format(corruption[_], acc))
    tf.random.set_seed(0)
    
    # define encoder
    encoding_dim = 32
    encoder_net = tf.keras.Sequential(
      [
          InputLayer(input_shape=(32, 32, 3)),
          Conv2D(64, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(128, 4, strides=2, padding='same', activation=tf.nn.relu),
          Conv2D(512, 4, strides=2, padding='same', activation=tf.nn.relu),
          Flatten(),
          Dense(encoding_dim,)
      ]
    )
    uae = UAE(encoder_net=encoder_net)
    preprocess_kwargs = {'model': uae, 'batch_size': 128}
    
    # initialise drift detector
    p_val = .05
    cd = KSDrift(
        p_val=p_val,        # p-value for K-S test 
        X_ref=X_ref,       # test against original test set
        preprocess_kwargs=preprocess_kwargs
    )
    preds_h0 = cd.predict(X_h0, return_p_val=True)
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    #| code_folding: [0]
    # print stats for H0
    print('K-S statistics:')
    print(preds_h0['data']['distance'])
    print(f"\nK-S statistic threshold: {preds_h0['data']['threshold']}")
    print('\np-values:')
    print(preds_h0['data']['p_val'])
    #| code_folding: [0]
    # print stats for corrupted data
    for x, c in zip(X_c, corruption):
        preds = cd.predict(x, return_p_val=True)
        print(f'Corruption type: {c}')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('Feature-wise p-values:')
        print(preds['data']['p_val'])
        print('')
    # use output softmax layer
    preprocess_kwargs = {'model': HiddenOutput(model=clf, layer=-1), 'batch_size': 128}
    
    cd = KSDrift(
        p_val=p_val,
        X_ref=X_ref,
        preprocess_kwargs=preprocess_kwargs
    )
    #| code_folding: []
    preds_h0 = cd.predict(X_h0)
    print('Drift? {}'.format(labels[preds_h0['data']['is_drift']]))
    print('\np-values:')
    print(preds_h0['data']['p_val'])
    #| code_folding: []
    for x, c in zip(X_c, corruption):
        preds = cd.predict(x)
        print(f'Corruption type: {c}')
        print('Drift? {}'.format(labels[preds['data']['is_drift']]))
        print('Feature-wise p-values:')
        print(preds['data']['p_val'])
        print('')
    np.random.seed(0)
    idx = np.random.choice(n_test, size=n_test // 2, replace=False)
    X_ref = scale_by_instance(X_test[idx])[0]
    
    cd = KSDrift(
        p_val=.05,
        X_ref=X_ref,
        preprocess_fn=ad.score,  # adversarial score fn = preprocess step
        preprocess_kwargs={'batch_size': 128}
    )
    #| code_folding: [0]
    # evaluate classifier on different datasets
    clf_accuracy['h0'] = clf.evaluate(X_h0, y_h0, batch_size=128, verbose=0)[1]
    preds_h0 = cd.predict(X_h0)
    print('H0: Accuracy {:.4f} -- Drift? {}'.format(
        clf_accuracy['h0'], labels[preds_h0['data']['is_drift']]))
    for x, c in zip(X_c, corruption):
        preds = cd.predict(x)
        print('{}: Accuracy {:.4f} -- Drift? {}'.format(
            c, clf_accuracy[c],labels[preds['data']['is_drift']]))
    SERVICE_HOSTNAMES=!(kubectl get ksvc drift-detector -o jsonpath='{.status.url}' | cut -d "/" -f 3)
    SERVICE_HOSTNAME_CD=SERVICE_HOSTNAMES[0]
    print(SERVICE_HOSTNAME_CD)
    from tqdm.notebook import tqdm
    
    drift_batch_size = 5000
    
    # accumulate batches
    for i in tqdm(range(0, drift_batch_size, 100)):
        x = X_h0[i:i+100]
        predict(x)
    
    # check message dumper
    res=!kubectl logs $(kubectl get pod -l serving.knative.dev/configuration=message-dumper-drift -o jsonpath='{.items[0].metadata.name}') user-container
    data= []
    for i in range(0,len(res)):
        if res[i] == 'Data,':
            data.append(res[i+1])
    j = json.loads(json.loads(data[0]))
    print("Drift?", labels[j["data"]["is_drift"]==1])
    c = 0
    
    print(f'Corruption: {corruption[c]}')
    
    # accumulate batches
    for i in tqdm(range(0, drift_batch_size, 100)):
        x = X_c[c][i:i+100]
        predict(x)
    
    # check message dumper
    res=!kubectl logs $(kubectl get pod -l serving.knative.dev/configuration=message-dumper-drift -o jsonpath='{.items[0].metadata.name}') user-container
    data= []
    for i in range(0,len(res)):
        if res[i] == 'Data,':
            data.append(res[i+1])
    j = json.loads(json.loads(data[1]))
    print("Drift?", labels[j["data"]["is_drift"]==1])

    alibi_detect.cd.base

    Constants

    has_pytorch

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    has_tensorflow

    bool(x) -> bool

    Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

    logger

    Instances of the Logger class represent a single logging channel. A "logging channel" indicates an area of an application. Exactly how an "area" is defined is up to the application developer. Since an application can have any number of areas, logging channels are identified by a unique string. Application areas can be nested (e.g. an area of "input processing" might include sub-areas "read CSV files", "read XLS files" and "read Gnumeric files"). To cater for this natural nesting, channel names are organized into a namespace hierarchy where levels are separated by periods, much like the Java or Python package namespace. So in the instance given above, channel names might be "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels. There is no arbitrary limit to the depth of nesting.

    BaseClassifierDrift

    Inherits from: BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    get_splits

    Split reference and test data in train and test folds used by the classifier.

    Name
    Type
    Default
    Description

    Returns

    • Type: Union[Tuple[Union[numpy.ndarray, list], numpy.ndarray], Tuple[Union[numpy.ndarray, list], numpy.ndarray, Optional[List[Tuple[numpy.ndarray, numpy.ndarray]]]]]

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[str, Dict[str, Union[str, int, float, Callable]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]

    score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, numpy.ndarray, numpy.ndarray, Union[numpy.ndarray, list], Union[numpy.ndarray, list]]

    test_probs

    Perform a statistical test of the probabilities predicted by the model against

    what we'd expect under the no-change null.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float]

    BaseContextMMDDrift

    Inherits from: BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data, given the provided context.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float, Tuple]

    BaseLSDDDrift

    Inherits from: BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float]

    BaseLearnedKernelDrift

    Inherits from: BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    get_splits

    Split reference and test data into two splits -- one of which to learn test locations

    and parameters and one to use for tests.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]], Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]]

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]

    score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float]

    BaseMMDDrift

    Inherits from: BaseDetector, ABC

    Constructor

    Name
    Type
    Default
    Description

    Methods

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[float, float, float]

    BaseUnivariateDrift

    Inherits from: BaseDetector, ABC, DriftConfigMixin

    Constructor

    Name
    Type
    Default
    Description

    Methods

    feature_score

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    predict

    Predict whether a batch of data has drifted from the reference data.

    Name
    Type
    Default
    Description

    Returns

    • Type: Dict[Dict[str, str], Dict[str, Union[numpy.ndarray, int, float]]]

    preprocess

    Data preprocessing before computing the drift scores.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    score

    Compute the feature-wise drift score which is the p-value of the

    statistical test and the test statistic.

    Name
    Type
    Default
    Description

    Returns

    • Type: Tuple[numpy.ndarray, numpy.ndarray]

    has_pytorch: bool = True

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    preds_type

    str

    'probs'

    Whether the model outputs probabilities or logits

    binarize_preds

    bool

    False

    Whether to test for discrepency on soft (e.g. probs/logits) model predictions directly with a K-S test or binarise to 0-1 prediction errors and apply a binomial test.

    train_size

    Optional[float]

    0.75

    Optional fraction (float between 0 and 1) of the dataset used to train the classifier. The drift is detected on 1 - train_size. Cannot be used in combination with n_folds.

    n_folds

    Optional[int]

    None

    Optional number of stratified folds used for training. The model preds are then calculated on all the out-of-fold predictions. This allows to leverage all the reference and test data for drift detection at the expense of longer computation. If both train_size and n_folds are specified, n_folds is prioritized.

    retrain_from_scratch

    bool

    True

    Whether the classifier should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    seed

    int

    0

    Optional random seed for fold selection.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    return_probs

    bool

    True

    Whether to return the instance level classifier probabilities for the reference and test data (0=reference data, 1=test data). The reference and test instances of the associated probabilities are also returned.

    return_model

    bool

    True

    Whether to return the updated model trained to discriminate reference and test instances.

    n_cur

    int

    Size of current window used in training model

    x_ref_preprocessed

    bool

    False

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last N instances seen by the detector. The parameter should be passed as a dictionary {'last': N}.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    x_kernel

    Optional[Callable]

    None

    Kernel defined on the input data, defaults to Gaussian RBF kernel.

    c_kernel

    Optional[Callable]

    None

    Kernel defined on the context data, defaults to Gaussian RBF kernel.

    n_permutations

    int

    1000

    Number of permutations used in the permutation test.

    prop_c_held

    float

    0.25

    Proportion of contexts held out to condition on.

    n_folds

    int

    5

    Number of cross-validation folds used when tuning the regularisation parameters.

    batch_size

    Optional[int]

    256

    If not None, then compute batches of MMDs at a time (rather than all at once).

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    verbose

    bool

    False

    Whether or not to print progress during configuration.

    return_distance

    bool

    True

    Whether to return the conditional MMD test statistic between the new batch and reference data.

    return_coupling

    bool

    False

    Whether to return the coupling matrices.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    sigma

    Optional[numpy.ndarray]

    None

    Optionally set the bandwidth of the Gaussian kernel used in estimating the LSDD. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths. If sigma is not specified, the 'median heuristic' is adopted whereby sigma is set as the median pairwise distance between reference samples.

    n_permutations

    int

    100

    Number of permutations used in the permutation test.

    n_kernel_centers

    Optional[int]

    None

    The number of reference samples to use as centers in the Gaussian kernel model used to estimate LSDD. Defaults to 1/20th of the reference data.

    lambda_rd_max

    float

    0.2

    The maximum relative difference between two estimates of LSDD that the regularization parameter lambda is allowed to cause. Defaults to 0.2 as in the paper.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    n_permutations

    int

    100

    The number of permutations to use in the permutation test once the MMD has been computed.

    train_size

    Optional[float]

    0.75

    Optional fraction (float between 0 and 1) of the dataset used to train the kernel. The drift is detected on 1 - train_size. Cannot be used in combination with n_folds.

    retrain_from_scratch

    bool

    True

    Whether the kernel should be retrained from scratch for each set of test data or whether it should instead continue training from where it left off on the previous set.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    return_kernel

    bool

    True

    Whether to return the updated kernel trained to discriminate reference and test instances.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics.

    sigma

    Optional[numpy.ndarray]

    None

    Optionally set the Gaussian RBF kernel bandwidth. Can also pass multiple bandwidth values as an array. The kernel evaluation is then averaged over those bandwidths.

    configure_kernel_from_x_ref

    bool

    True

    Whether to already configure the kernel bandwidth from the reference data.

    n_permutations

    int

    100

    Number of permutations used in the permutation test.

    input_shape

    Optional[tuple]

    None

    Shape of input data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    preprocess_at_init

    bool

    True

    Whether to preprocess the reference data when the detector is instantiated. Otherwise, the reference data will be preprocessed at prediction time. Only applies if x_ref_preprocessed=False.

    update_x_ref

    Optional[Dict[str, int]]

    None

    Reference data can optionally be updated to the last n instances seen by the detector or via reservoir sampling with size n. For the former, the parameter equals {'last': n} while for reservoir sampling {'reservoir_sampling': n} is passed.

    preprocess_fn

    Optional[Callable]

    None

    Function to preprocess the data before computing the data drift metrics. Typically a dimensionality reduction technique.

    correction

    str

    'bonferroni'

    Correction type for multivariate data. Either 'bonferroni' or 'fdr' (False Discovery Rate).

    n_features

    Optional[int]

    None

    Number of features used in the statistical test. No need to pass it if no preprocessing takes place. In case of a preprocessing step, this can also be inferred automatically but could be more expensive to compute.

    input_shape

    Optional[tuple]

    None

    Shape of input data. Needs to be provided for text data.

    data_type

    Optional[str]

    None

    Optionally specify the data type (tabular, image or time-series). Added to metadata.

    return_distance

    bool

    True

    Whether to return the test statistic between the features of the new batch and reference data.

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for the significance of the test.

    x_ref_preprocessed

    bool

    False

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_splits

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the test.

    return_distance

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    y_oof

    numpy.ndarray

    Out of fold targets (0 ref, 1 cur)

    probs_oof

    numpy.ndarray

    Probabilities predicted by the model

    n_ref

    int

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    c_ref

    numpy.ndarray

    Context for the reference distribution.

    p_val

    float

    0.05

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    c

    numpy.ndarray

    Context associated with batch of instances.

    return_p_val

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    c

    numpy.ndarray

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for the significance of the permutation test.

    x_ref_preprocessed

    bool

    False

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for the significance of the test.

    x_ref_preprocessed

    bool

    False

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for the significance of the permutation test.

    x_ref_preprocessed

    bool

    False

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    return_p_val

    bool

    True

    Whether to return the p-value of the permutation test.

    return_distance

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    x_ref

    Union[numpy.ndarray, list]

    Data used as reference distribution.

    p_val

    float

    0.05

    p-value used for significance of the statistical test for each feature. If the FDR correction method is used, this corresponds to the acceptable q-value.

    x_ref_preprocessed

    bool

    False

    x_ref

    numpy.ndarray

    x

    numpy.ndarray

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    drift_type

    str

    'batch'

    Predict drift at the 'feature' or 'batch' level. For 'batch', the test statistics for each feature are aggregated using the Bonferroni or False Discovery Rate correction (if n_features>1).

    return_p_val

    bool

    True

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    x

    Union[numpy.ndarray, list]

    Batch of instances.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    Whether to return the splits.

    Whether to return a notion of strength of the drift. K-S test stat if binarize_preds=False, otherwise relative error reduction.

    Size of reference window used in training model

    p-value used for the significance of the permutation test.

    Whether to return the p-value of the permutation test.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    Whether to return the LSDD metric between the new batch and reference data.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    Whether to return the MMD metric between the new batch and reference data.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    Whether to return the MMD metric between the new batch and reference data.

    Whether the given reference data x_ref has been preprocessed yet. If x_ref_preprocessed=True, only the test data x will be preprocessed at prediction time. If x_ref_preprocessed=False, the reference data will also be preprocessed.

    Whether to return feature level p-values.

    has_tensorflow: bool = True
    logger: logging.Logger = <Logger alibi_detect.cd.base (WARNING)>
    BaseClassifierDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, preds_type: str = 'probs', binarize_preds: bool = False, train_size: Optional[float] = 0.75, n_folds: Optional[int] = None, retrain_from_scratch: bool = True, seed: int = 0, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    get_splits(x_ref: Union[numpy.ndarray, list], x: Union[numpy.ndarray, list], return_splits: bool = True) -> Union[Tuple[Union[numpy.ndarray, list], numpy.ndarray], Tuple[Union[numpy.ndarray, list], numpy.ndarray, Optional[List[Tuple[numpy.ndarray, numpy.ndarray]]]]]
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True, return_probs: bool = True, return_model: bool = True) -> Dict[str, Dict[str, Union[str, int, float, Callable]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, numpy.ndarray, numpy.ndarray, Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
    test_probs(y_oof: numpy.ndarray, probs_oof: numpy.ndarray, n_ref: int, n_cur: int) -> Tuple[float, float]
    BaseContextMMDDrift(self, x_ref: Union[numpy.ndarray, list], c_ref: numpy.ndarray, p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, x_kernel: Callable = None, c_kernel: Callable = None, n_permutations: int = 1000, prop_c_held: float = 0.25, n_folds: int = 5, batch_size: Optional[int] = 256, input_shape: Optional[tuple] = None, data_type: Optional[str] = None, verbose: bool = False) -> None
    predict(x: Union[numpy.ndarray, list], c: numpy.ndarray, return_p_val: bool = True, return_distance: bool = True, return_coupling: bool = False) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[numpy.ndarray, numpy.ndarray]
    score(x: Union[numpy.ndarray, list], c: numpy.ndarray) -> Tuple[float, float, float, Tuple]
    BaseLSDDDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, sigma: Optional[numpy.ndarray] = None, n_permutations: int = 100, n_kernel_centers: Optional[int] = None, lambda_rd_max: float = 0.2, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[numpy.ndarray, numpy.ndarray]
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    BaseLearnedKernelDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, n_permutations: int = 100, train_size: Optional[float] = 0.75, retrain_from_scratch: bool = True, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    get_splits(x_ref: Union[numpy.ndarray, list], x: Union[numpy.ndarray, list]) -> Tuple[Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]], Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]]
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True, return_kernel: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    BaseMMDDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, sigma: Optional[numpy.ndarray] = None, configure_kernel_from_x_ref: bool = True, n_permutations: int = 100, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    predict(x: Union[numpy.ndarray, list], return_p_val: bool = True, return_distance: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[numpy.ndarray, numpy.ndarray]
    score(x: Union[numpy.ndarray, list]) -> Tuple[float, float, float]
    BaseUnivariateDrift(self, x_ref: Union[numpy.ndarray, list], p_val: float = 0.05, x_ref_preprocessed: bool = False, preprocess_at_init: bool = True, update_x_ref: Optional[Dict[str, int]] = None, preprocess_fn: Optional[Callable] = None, correction: str = 'bonferroni', n_features: Optional[int] = None, input_shape: Optional[tuple] = None, data_type: Optional[str] = None) -> None
    feature_score(x_ref: numpy.ndarray, x: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]
    predict(x: Union[numpy.ndarray, list], drift_type: str = 'batch', return_p_val: bool = True, return_distance: bool = True) -> Dict[Dict[str, str], Dict[str, Union[numpy.ndarray, int, float]]]
    preprocess(x: Union[numpy.ndarray, list]) -> Tuple[numpy.ndarray, numpy.ndarray]
    score(x: Union[numpy.ndarray, list]) -> Tuple[numpy.ndarray, numpy.ndarray]