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...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Learn about the features of 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.

[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.
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.
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.
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
✔
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.
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:
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:
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.
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 .
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:
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.
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:
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.
max_samplesmax_samplesmax_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'.
window_localpadding_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.
from alibi_detect.utils.fetching import fetch_tf_model
model = fetch_tf_model('cifar10', 'resnet32')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:
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:
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.
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)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.
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:
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.
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:
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.
Alibi Detect can be installed from PyPI or conda-forge by following the instructions below.
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:
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:
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:
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.
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.
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:
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.
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.
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.
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:
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:
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.
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.
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:
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:
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.
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 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:
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:
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:
We now generate a batch of data with 10% outliers and detect the outliers in the batch.
Predict outliers:
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:
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:
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.
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:
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
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.
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:
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
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:
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().
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:
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 .
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 .
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)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'.
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.
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.
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.
x_refpredictupdate_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'.
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$
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'.
distance: feature-wise K-S or Chi-Squared statistics between the reference data and the new batch if return_distance equals True.
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:
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:
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:
Predict outliers:
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:
(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).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.
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.
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.
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:
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.
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).
[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.
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.
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:
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.
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.
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.
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:
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.
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).
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 seabornimport 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_tsn_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 = 200plt.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
)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:
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:
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:
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:
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.
pip install alibi-detectParameters:
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:
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:
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.
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
Compute K-S scores and statistics per feature.
x_ref
numpy.ndarray
Reference instances to compare distribution with.
x
numpy.ndarray
Batch of instances.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
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.
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:
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.
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.
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:
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:
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.
CVMDriftInherits from: BaseUnivariateDrift, BaseDetector, ABC, DriftConfigMixin
feature_scorePerforms the two-sample Cramer-von Mises test(s), computing the p-value and test statistic per feature.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
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.
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:
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
loggerInstances 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.
MMDDriftKeopsInherits from: BaseMMDDrift, BaseDetector, ABC
scoreCompute 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.
Returns
Type: Tuple[float, float, float]
FETDriftOnlineInherits from: BaseUniDriftOnline, BaseDetector, StateMixin, ABC, DriftConfigMixin
scoreCompute 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.
Returns
Type: numpy.ndarray
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.
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:
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:
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.
CVMDriftOnlineInherits from: BaseUniDriftOnline, BaseDetector, StateMixin, ABC, DriftConfigMixin
scoreCompute 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.
Returns
Type: numpy.ndarray
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.
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 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).
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-forgemamba install -c conda-forge alibi-detectimport 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 OutlierVAEod = 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) -> Nonefeature_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.
pip install alibi-detect[all]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_refupdate_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.
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.
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.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.
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:
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:
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:
F1 score, accuracy, recall and confusion matrix:
We can also plot the ROC curve based on the instance level outlier scores:
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.
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.
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:
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.
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:
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:
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.
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.
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.:
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.
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:
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.
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.
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.
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:
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.
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).
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:
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:
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:
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:
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) -> Nonefeature_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) -> Nonefeature_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) -> Nonefeature_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) -> Nonescore(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) -> Nonescore(x_t: Union[numpy.ndarray, typing.Any]) -> numpy.ndarrayencoder_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) -> Nonescore(x_t: Union[numpy.ndarray, typing.Any]) -> numpy.ndarray!pip install seabornimport 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.dropoutpreds = 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 alibiimport 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_detectoradult = alibi.datasets.fetch_adult()
X, y = adult.data, adult.target
feature_names = adult.feature_names
category_map = adult.category_map
X.shape, y.shapen_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.shapecategories_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.shapecd = 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}')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.
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.
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
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.
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:
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:
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:
We can again either fetch the pretrained detector from a or train one from scratch:
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:
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:
F1 score, accuracy, precision, recall and confusion matrix:
We can also plot the ROC curve based on the instance level outlier scores:
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.
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:
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.
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.
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.
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
For the related MMDDrift detector.
DEFAULT_METALARGE_ARTEFACTSBuilt-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
BaseDetectorInherits from: ABC
Base class for outlier, adversarial and drift detection algorithms.
predictscoreConfigurableDetectorInherits 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.
from_configget_configReturns
Type: dict
DetectorInherits from: Protocol, Generic
Type Protocol for all detectors.
Used for typing legacy save and load functionality in alibi_detect.saving._tensorflow.saving.py.
predictReturns
Type: typing.Any
DriftConfigMixinA mixin class containing methods related to a drift detector's configuration dictionary.
from_configInstantiate a drift detector from a fully resolved (and validated) config dictionary.
get_configGet the detector's configuration dictionary.
Returns
Type: dict
FitMixinInherits from: ABC
fitReturns
Type: None
NumpyEncoderInherits from: JSONEncoder
defaultStatefulDetectorOnlineInherits 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.
load_statesave_stateThresholdMixinInherits from: ABC
infer_thresholdReturns
Type: None
adversarial_correction_dictadversarial_prediction_dictconcept_drift_dictoutlier_prediction_dictUnder 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 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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
DEFAULT_META: dict = {'name': None, 'online': None, 'data_type': None, 'version': None, 'detector_...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'.
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.
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.
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'.
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.
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]
model_hlmodel_hltemperature: 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.
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.
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 flagged as drift. We also split the corrupted data by corruption type:
We can visualise the same instance for each corruption type:
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:
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.
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 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.
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:
Define number of sampled points and the type of simulated time series. We use TimeSynth to generate sinusoidal signals with noise.
Visualize:
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:
Generate the outliers to detect:
Predict outliers:
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:
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.
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.
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.
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).
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:
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:
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:
Inject outliers in the numerical features. First we need to know the features for each kind:
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:
OHE to train, threshold and outlier sets:
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:
F1 score and confusion matrix:
Plot instance level outlier scores vs. the outlier threshold:
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:
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:
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:
We now generate a batch of data with 10% outliers and detect the outliers in the batch.
Predict outliers:
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:
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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).
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_oodplt.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() -> dictThis 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.AnyDriftConfigMixin(self, /, *args, **kwargs)from_config(config: dict)get_config() -> dictfit(args, kwargs) -> Nonedefault(obj)load_state(filepath: Union[str, os.PathLike])save_state(filepath: Union[str, os.PathLike])infer_threshold(args, kwargs) -> Noneadversarial_correction_dict()adversarial_prediction_dict()concept_drift_dict()outlier_prediction_dict()!pip install torchvisionimport 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 inlineMNIST_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, redx_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') / 255np.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 seabornimport 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_rocn_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 = Falsefilepath = '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 seabornimport 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_scoredef 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_mapset_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 += 1minmax = FalseX_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) / sigmaX_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 = Truefilepath = './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 seabornimport 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) / stdevload_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 += 1n_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')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:
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.
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.
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.
Inherits from: DriftConfigMixin
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
Returns
Type: dict
Predict whether a batch of data has drifted from the reference data.
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]]]
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.
x
Union[numpy.ndarray, list]
Batch of instances.
Returns
Type: Tuple[float, float, float]
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:
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
The following tables list the current state of save/load support for each detector. Adding full support for the remaining detectors is in the .
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.
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:
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.
consists of 60,000 32 by 32 RGB images equally distributed over 10 classes.
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:
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:
Reconstruction of masked images and outlier scores per channel:
Visualize:
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:
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.
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.
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.
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 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:
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.
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.
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.
The method works with both the PyTorch, TensorFlow, and Sklearn frameworks. We will focus exclusively on the Sklearn backend in this notebook.
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.
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);
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.
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 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).
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.
!pip install wilds torch torchvisionfrom 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 inlineWILDS_PATH = './data/wilds'
DOWNLOAD = False # set to True for first run
N = 2000 # size of reference setdef 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-3encoder = 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 thresholdfrom 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 = Truehas_tensorflow: bool = TrueLSDDDrift(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) -> Noneget_config() -> dictpredict(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.

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.
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.
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.
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'.
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 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 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.
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.
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 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 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:
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.
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.
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.
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.
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
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:
We can do the same using TensorFlow instead of PyTorch as backend. We first define the classifier again and then simply run the detector:
CIFAR10 consists of 60,000 32 by 32 RGB images equally distributed over 10 classes.
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:
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:
Reconstruction of masked images and outlier scores per channel:
Visualize:
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:
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.
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 = 50mask_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 atplt.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 alibiimport 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.dataeducation_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') / 255np.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 wildsimport 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 = Truefilepath = '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 = 50mask_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 atplt.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))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
❌
✅
Isolation Forest
✅
❌
Mahalanobis Distance
✅
❌
AE
✅
❌
VAE
Adversarial AE
✅
❌
Model distillation
✅
❌
Kolmogorov-Smirnov
✅
✅
Cramér-von Mises
❌
✅
Fisher's Exact Test
❌
✅
Least-Squares Density Difference
❌
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.
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.
Inherits from: DriftConfigMixin
x_ref
Union[numpy.ndarray, list]
Data used as reference distribution.
c_ref
numpy.ndarray
Context for the reference distribution.
backend
str
'tensorflow'
Predict whether a batch of data has drifted from the reference data, given the provided context.
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]]]
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.
x
Union[numpy.ndarray, list]
Batch of instances.
c
numpy.ndarray
Context associated with batch of instances.
Returns
Type: Tuple[float, float, float, Tuple]
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 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.
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.
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.
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!
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.
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:
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
loggerInstances 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.
ModelDistillationInherits from: BaseDetector, FitMixin, ThresholdMixin, ABC
fitTrain ModelDistillation detector.
Returns
Type: None
infer_thresholdUpdate threshold by a value inferred from the percentage of instances considered to be
adversarial in a sample of the dataset.
Returns
Type: None
predictPredict whether instances are adversarial instances or not.
Returns
Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]
scoreCompute adversarial scores.
Returns
Type: Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]
TYPE_CHECKINGbool(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.
loggerInstances 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.
BaseMultiDriftOnlineInherits from: BaseDetector, StateMixin, ABC
get_thresholdReturn the threshold for timestep t.
Returns
Type: float
predictPredict whether the most recent window of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]
resetDeprecated 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_stateResets the detector to its initial state (t=0). This does not include reconfiguring thresholds.
Returns
Type: None
BaseUniDriftOnlineInherits from: BaseDetector, StateMixin, ABC
get_thresholdReturn the threshold for timestep t.
Returns
Type: numpy.ndarray
predictPredict whether the most recent window(s) of data have drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]
resetDeprecated 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_stateResets the detector to its initial state (t=0). This does not include reconfiguring thresholds.
Returns
Type: None
has_pytorchbool(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_tensorflowbool(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_keopsbool(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.
LearnedKernelDriftInherits from: DriftConfigMixin
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]
LearnedKernelDriftKeopsInherits from: BaseLearnedKernelDrift, BaseDetector, ABC
scoreCompute 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.
Returns
Type: Tuple[float, float, float]
trainerTrain the kernel to maximise an estimate of test power using minibatch gradient descent.
Returns
Type: None
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.
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.
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=2dd.reset_state() # reset to t=0
save_detector(dd, filepath) # save the detector without statehas_pytorch: bool = Truehas_tensorflow: bool = Truelogger: 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) -> Nonepredict(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 = Falsehas_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.
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.
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:
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:
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:
We now generate a batch of data with 10% outliers and detect the outliers in the batch.
Predict outliers:
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:
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.
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:
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:
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:
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:
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.
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:
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:
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.
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:
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).
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:
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:
Apply fit method:
Transform outlier batch to OHE:
Predict outliers:
F1 score and confusion matrix:
Plot instance level outlier scores vs. the outlier threshold:
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') / 255np.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 HiddenOutputModelDistillation(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) -> Nonefit(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) -> Noneinfer_threshold(X: numpy.ndarray, threshold_perc: float = 99.0, margin: float = 0.0, batch_size: int = 10000000000) -> Nonepredict(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) -> Noneget_threshold(t: int) -> floatpredict(x_t: Union[numpy.ndarray, typing.Any], return_test_stat: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]reset() -> Nonereset_state() -> NoneBaseUniDriftOnline(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) -> Noneget_threshold(t: int) -> numpy.ndarraypredict(x_t: Union[numpy.ndarray, typing.Any], return_test_stat: bool = True) -> Dict[Dict[str, str], Dict[str, Union[int, float]]]reset() -> Nonereset_state() -> Nonehas_tensorflow: bool = Truehas_keops: bool = TrueLearnedKernelDrift(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) -> Nonepredict(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) -> Nonescore(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 seabornimport 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) / stdevload_outlier_detector = Truefilepath = '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 featuresdf = 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 = Truefilepath = '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))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.
Inherits from: DriftConfigMixin
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'
Predict whether a batch of data has drifted from the reference data.
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]]]
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.
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:
The in-distribution dataset is Fashion-MNIST and the out-of-distribution dataset we'd like to detect is MNIST.
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.
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:
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:
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:
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:
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:
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.
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 = Truehas_tensorflow: bool = TrueClassifierDrift(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) -> Nonepredict(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 seabornfrom 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_penguinsdata = 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.
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.
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:
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.
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$.
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:
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.
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.
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.
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:
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%.
!pip install seabornimport 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_rocdef 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 = Truefilepath = 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_oodplt.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_oodidx = 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 pykeopsimport 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') / 255X_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_modeldef accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
return (y_true == y_pred).astype(int).sum() / y_true.shape[0]load_pretrained = Truefilepath = '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, percfrom 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] = dfmu_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')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.05from 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.
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.
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.
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.
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:
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:
For convenience, Alibi Detect also pre-registers a number of commonly used utility functions and objects.
*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)=
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:
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.
loggerInstances 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.
AdversarialAEInherits from: BaseDetector, FitMixin, ThresholdMixin, ABC
correctCorrect adversarial instances if the adversarial score is above the threshold.
Returns
Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]
fitTrain Adversarial AE model.
Returns
Type: None
infer_thresholdUpdate threshold by a value inferred from the percentage of instances considered to be
adversarial in a sample of the dataset.
Returns
Type: None
predictPredict whether instances are adversarial instances or not.
Returns
Type: Dict[Dict[str, str], Dict[str, numpy.ndarray]]
scoreCompute adversarial scores.
Returns
Type: Union[numpy.ndarray, Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]]
DenseHiddenInherits from: Model, TensorFlowTrainer, Trainer, Layer, TFLayer, KerasAutoTrackable, AutoTrackable, Trackable, Operation, KerasSaveable
callReturns
Type: tensorflow.python.framework.tensor.Tensor
logger: logging.Logger = <Logger alibi_detect.ad.adversarialae (WARNING)>src✔
✔
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
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'
✔
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)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) -> Nonecorrect(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) -> Noneinfer_threshold(X: numpy.ndarray, threshold_perc: float = 99.0, margin: float = 0.0, batch_size: int = 10000000000) -> Nonepredict(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) -> Nonecall(x: tensorflow.python.framework.tensor.Tensor) -> tensorflow.python.framework.tensor.TensorWe 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.
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.
Binary sentiment classification containing $25,000$ movie reviews for training and $25,000$ for testing. Install the nlp library to fetch the dataset:
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:
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:
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.
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:
Again check the example or the for more information about each of the possible parameters.
H0:
Imbalanced data:
Perturbed data:
We can run the same detector with PyTorch backend for both the preprocessing step and MMD implementation:
H0:
Imbalanced data:
Perturbed data:
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 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.
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.
!pip install nlpimport 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']))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.
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.
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.
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!
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:
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:
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.
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.
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.
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.
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.
Besides alibi-detect, this example notebook also uses and , both of which can be installed via pip/conda.
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.
We borrow code from the PyTorch Geometric to visualize molecules from the graph objects.
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:
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.
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.
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.
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).
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.
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:
Domain or application specific contexts such as the time of day or the activity (e.g. running or resting).
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.
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:
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.
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.
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$).
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:
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.
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.
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.
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.
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:
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.
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.
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.
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).
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') / 255np.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_dimfrom 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 / 10make_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] == Nnp.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 gi = 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_outfrom 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 statsmodelsimport 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}')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.
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 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.
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:
Linear projections
Non-linear projections
Feature maps (from ML model)
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 .
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. :::
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:
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.
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.
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.
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.
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.
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$.
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.
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:
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.
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. :::
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.
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.
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$.
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.
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 .
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.
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.
Standardise the dataset by instance:
Check that the predictions on the test set reach $93.15$% accuracy:
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:
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:
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.
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:
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)

:align: center
:alt: 2D drift examplepca = 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: 550pxdef 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: 700pxonline_detector = MMDDriftOnline(X_ref, ert, window_size, backend='tensorflow', n_bootstraps=5000)result = online_detector.predict(X[i]):align: center
:alt: Online detector
:width: 700pximport 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_cifar10cdef 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 = Truefilepath = '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 = Truefilepath = '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()
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:
Domain or application specific contexts such as the time of day or the weather.
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.
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:
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).
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.
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.
We use the which contains about 18,000 newsgroups post across 20 topics, including politics, science sports or religion.
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:
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:
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.
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.
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.
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:
Computers: comp.graphics; comp.os.ms-windows.misc; comp.sys.ibm.pc.hardware; comp.sys.mac.hardware; comp.windows.x
Recreation: rec.autos; rec.motorcycles; rec.sport.baseball; rec.sport.hockey
Science: sci.crypt; sci.electronics; sci.med; sci.space
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.
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.
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).
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).
Politics: talk.politics.misc; talk.politics.guns; talk.politics.mideast
Religion: talk.religion.misc; talk.atheism; soc.religion.christian
!pip install umap-learn torch sentence-transformers statsmodels seaborn datasetsimport 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_namesn = 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 batchdef 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, predsn_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_classesn_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()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.
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
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:
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.
We can create some outliers by applying a random noise mask to the original instances:
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:
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.
The ResNet classification model is trained on data standardized by instance:
Check the predictions on the test:
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:
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.
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.
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!
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 .
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 .
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:
#| 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_cifar10ccorruptions = 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') / 255np.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])



has_pytorchbool(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_tensorflowbool(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.
loggerInstances 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.
BaseClassifierDriftInherits from: BaseDetector, ABC
get_splitsSplit reference and test data in train and test folds used by the classifier.
Returns
Type: Union[Tuple[Union[numpy.ndarray, list], numpy.ndarray], Tuple[Union[numpy.ndarray, list], numpy.ndarray, Optional[List[Tuple[numpy.ndarray, numpy.ndarray]]]]]
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[str, Dict[str, Union[str, int, float, Callable]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
scoreReturns
Type: Tuple[float, float, numpy.ndarray, numpy.ndarray, Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
test_probsPerform a statistical test of the probabilities predicted by the model against
what we'd expect under the no-change null.
Returns
Type: Tuple[float, float]
BaseContextMMDDriftInherits from: BaseDetector, ABC
predictPredict whether a batch of data has drifted from the reference data, given the provided context.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
scoreReturns
Type: Tuple[float, float, float, Tuple]
BaseLSDDDriftInherits from: BaseDetector, ABC
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
scoreReturns
Type: Tuple[float, float, float]
BaseLearnedKernelDriftInherits from: BaseDetector, ABC
get_splitsSplit reference and test data into two splits -- one of which to learn test locations
and parameters and one to use for tests.
Returns
Type: Tuple[Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]], Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]]
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float, Callable]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[Union[numpy.ndarray, list], Union[numpy.ndarray, list]]
scoreReturns
Type: Tuple[float, float, float]
BaseMMDDriftInherits from: BaseDetector, ABC
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[int, float]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
scoreReturns
Type: Tuple[float, float, float]
BaseUnivariateDriftInherits from: BaseDetector, ABC, DriftConfigMixin
feature_scoreReturns
Type: Tuple[numpy.ndarray, numpy.ndarray]
predictPredict whether a batch of data has drifted from the reference data.
Returns
Type: Dict[Dict[str, str], Dict[str, Union[numpy.ndarray, int, float]]]
preprocessData preprocessing before computing the drift scores.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
scoreCompute the feature-wise drift score which is the p-value of the
statistical test and the test statistic.
Returns
Type: Tuple[numpy.ndarray, numpy.ndarray]
has_pytorch: bool = Truepreprocess_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 = Truelogger: 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) -> Noneget_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) -> Nonepredict(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) -> Nonepredict(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) -> Noneget_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) -> Nonepredict(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) -> Nonefeature_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]