From 23bfd23140887ec61c0f36e9bb015f9593229d71 Mon Sep 17 00:00:00 2001 From: franklincf Date: Wed, 7 May 2025 20:33:38 -0300 Subject: [PATCH 1/3] feat: adding codes for attribute inference attack --- src/holisticai/security/attackers/__init__.py | 0 .../attackers/attribute_inference/__init__.py | 13 + .../attackers/attribute_inference/baseline.py | 90 +++++ .../attribute_inference/black_box.py | 146 +++++++ .../attribute_inference/dataset_utils.py | 379 ++++++++++++++++++ .../true_label_baseline.py | 107 +++++ .../attackers/attribute_inference/utils.py | 150 +++++++ .../attribute_inference/white_box.py | 166 ++++++++ .../white_box_lifestyle.py | 168 ++++++++ 9 files changed, 1219 insertions(+) create mode 100644 src/holisticai/security/attackers/__init__.py create mode 100644 src/holisticai/security/attackers/attribute_inference/__init__.py create mode 100644 src/holisticai/security/attackers/attribute_inference/baseline.py create mode 100644 src/holisticai/security/attackers/attribute_inference/black_box.py create mode 100644 src/holisticai/security/attackers/attribute_inference/dataset_utils.py create mode 100644 src/holisticai/security/attackers/attribute_inference/true_label_baseline.py create mode 100644 src/holisticai/security/attackers/attribute_inference/utils.py create mode 100644 src/holisticai/security/attackers/attribute_inference/white_box.py create mode 100644 src/holisticai/security/attackers/attribute_inference/white_box_lifestyle.py diff --git a/src/holisticai/security/attackers/__init__.py b/src/holisticai/security/attackers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/holisticai/security/attackers/attribute_inference/__init__.py b/src/holisticai/security/attackers/attribute_inference/__init__.py new file mode 100644 index 00000000..24f206ea --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/__init__.py @@ -0,0 +1,13 @@ +from holisticai.security.attackers.attribute_inference.baseline import AttributeInferenceBaseline +from holisticai.security.attackers.attribute_inference.black_box import AttributeInferenceBlackBox +from holisticai.security.attackers.attribute_inference.white_box import AttributeInferenceWhiteBoxDecisionTree +from holisticai.security.attackers.attribute_inference.white_box_lifestyle import ( + AttributeInferenceWhiteBoxLifestyleDecisionTree, +) + +__all__ = [ + "AttributeInferenceBaseline", + "AttributeInferenceBlackBox", + "AttributeInferenceWhiteBoxDecisionTree", + "AttributeInferenceWhiteBoxLifestyleDecisionTree", +] diff --git a/src/holisticai/security/attackers/attribute_inference/baseline.py b/src/holisticai/security/attackers/attribute_inference/baseline.py new file mode 100644 index 00000000..c62fa4be --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/baseline.py @@ -0,0 +1,90 @@ +import numpy as np + +from holisticai.security.attackers.attribute_inference.dataset_utils import AttributeInferenceDataPreprocessor +from holisticai.security.attackers.attribute_inference.utils import get_attack_model, get_feature_index + + +class AttributeInferenceBaseline: + """ + Attribute Inference Attack using a baseline model. + + This attack uses a baseline model to infer the value of a specific feature in the dataset. + The attack model is trained on the remaining features in the dataset. + + Parameters + ---------- + attack_model_type : str, default="nn" + The type of model to use for the attack. Options are "nn" for neural network or "tree" for decision tree. + attack_feature : int or slice, default=0 + The index or slice of the feature to attack. If a slice is provided, it should be of size 1. + """ + + def __init__( + self, + attack_model_type="nn", + attack_feature=0, + ): + self._values = None + self._nb_classes = None + + self.attack_model = get_attack_model(attack_model_type) + self.attack_feature = attack_feature + + self._check_params() + self.attack_feature = get_feature_index(self.attack_feature) + self.ai_preprocessor = AttributeInferenceDataPreprocessor(attack_feature=attack_feature) + + def _check_params(self) -> None: + if not isinstance(self.attack_feature, int) and not isinstance(self.attack_feature, slice): + raise TypeError("Attack feature must be either an integer or a slice object.") + + if isinstance(self.attack_feature, int) and self.attack_feature < 0: + raise ValueError("Attack feature index must be non-negative.") + + def fit(self, x: np.ndarray) -> None: + """ + Train the attack model. + + Parameters + ---------- + x : np.ndarray + Input to training process. Includes all features used to train the original model. + """ + # train attack model + attack_x, attack_y = self.ai_preprocessor.fit_transform(x) + self._values = self.ai_preprocessor._values # noqa: SLF001 + self.attack_model.fit(attack_x, attack_y) + + def infer(self, x: np.ndarray, **kwargs) -> np.ndarray: + """ + Infer the attacked feature. + + Parameters + ---------- + x : np.ndarray + Input to attack. Includes all features except the attacked feature. + values : list + Possible values for attacked feature. For a single column feature this should be a simple list containing + all possible values, in increasing order (the smallest value in the 0 index and so on). For a multi-column + feature (for example 1-hot encoded and then scaled), this should be a list of lists, where each internal + list represents a column (in increasing order) and the values represent the possible values for that column + (in increasing order). + + Returns + ------- + np.ndarray + The inferred feature values. + """ + # if values are provided, override the values computed in fit() + values = kwargs.get("values", self._values) + attack_x = self.ai_preprocessor.transform(x) + predictions = self.attack_model.predict_proba(attack_x).astype(np.float32) + + if values is not None: + if isinstance(self.attack_feature, int): + predictions = np.array([values[np.argmax(arr)] for arr in predictions]) + else: + for value, column in zip(values, predictions.T): + for index in range(len(value)): + np.place(column, [column == index], value[index]) + return np.array(predictions) diff --git a/src/holisticai/security/attackers/attribute_inference/black_box.py b/src/holisticai/security/attackers/attribute_inference/black_box.py new file mode 100644 index 00000000..6286675d --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/black_box.py @@ -0,0 +1,146 @@ +""" +This module implements attribute inference attacks. +""" + +from __future__ import annotations + +import logging +from typing import Optional, Union + +import numpy as np + +from holisticai.security.attackers.attribute_inference.dataset_utils import AttributeInferenceDataPreprocessor +from holisticai.security.attackers.attribute_inference.utils import get_attack_model, get_feature_index + +logger = logging.getLogger(__name__) + + +class AttributeInferenceBlackBox: + """ + Implementation of a simple black-box attribute inference attack. + + The idea is to train a simple neural network to learn the attacked feature from the rest of the features and the + model's predictions. Assumes the availability of the attacked model's predictions for the samples under attack, + in addition to the rest of the feature values. If this is not available, the true class label of the samples may be + used as a proxy. + + Parameters + ---------- + estimator : object + Target estimator. + attack_model_type : str + The type of default attack model to train, optional. Should be one of `nn` (for neural network, default) or `rf` + (for random forest). If `attack_model` is supplied, this option will be ignored. + attack_model : object + The attack model to train, optional. If none is provided, a default model will be created. + attack_feature : int or slice + The index of the feature to be attacked or a slice representing multiple indexes in case of a one-hot encoded + feature. + scale_range : tuple + If supplied, the class labels (both true and predicted) will be scaled to the given range. Only applicable when + `estimator` is a regressor. + prediction_normal_factor : float + If supplied, the class labels (both true and predicted) are multiplied by the factor when used as inputs to the + attack-model. Only applicable when `estimator` is a regressor and if `scale_range` is not supplied. + """ + + def __init__( + self, + estimator, + attack_model_type: str = "nn", + attack_model=None, + attack_feature: Union[int, slice] = 0, + scale_range: Optional[tuple[float, float]] = None, + prediction_normal_factor: Optional[float] = 1, + ): + self._values: Optional[list] = None + self._nb_classes: Optional[int] = None + self._attack_model_type = attack_model_type + self._attack_model = attack_model + self.estimator = estimator + self.attack_feature = attack_feature + self.attack_model = get_attack_model(attack_model_type) + + self.prediction_normal_factor = prediction_normal_factor + self.scale_range = scale_range + self._check_params() + self.attack_feature = get_feature_index(self.attack_feature) + self.ai_preprocessor = AttributeInferenceDataPreprocessor( + scale_range=scale_range, + prediction_normal_factor=prediction_normal_factor, + attack_feature=attack_feature, + ) + + def fit(self, x: np.ndarray, y: Optional[np.ndarray] = None, pred: Optional[np.ndarray] = None) -> None: + """ + Train the attack model. + + Parameters + ---------- + x : np.ndarray + Input to training process. Includes all features used to train the original model. + y : np.ndarray + True labels for x. + pred : np.ndarray + Predictions of the original model for x. + """ + + attack_x, attack_y = self.ai_preprocessor.fit_transform(x, y, pred) + self._values = self.ai_preprocessor._values # noqa: SLF001 + self.attack_model.fit(attack_x, attack_y) + + def infer(self, x: np.ndarray, y: np.ndarray, pred: np.ndarray, **kwargs) -> np.ndarray: + """ + Infer the attacked feature. + + Parameters + ---------- + x : np.ndarray + Input to attack. Includes all features except the attacked feature. + y : np.ndarray + True labels for x. + pred : np.ndarray + Original model's predictions for x. + values : list + Possible values for attacked feature. For a single column feature this should be a simple list + containing all possible values, in increasing order (the smallest value in the 0 index and so + on). For a multi-column feature (for example 1-hot encoded and then scaled), this should be a + list of lists, where each internal list represents a column (in increasing order) and the values + represent the possible values for that column (in increasing order). If not provided, is + computed from the training data when calling `fit`. + + Returns + ------- + np.ndarray + The inferred feature values. + """ + + values: Optional[list] = kwargs.get("values") + + # if provided, override the values computed in fit() + if values is not None: + self._values = values + + attack_x = self.ai_preprocessor.transform(x, y, pred) + predictions = self.attack_model.predict_proba(attack_x).astype(np.float32) + + if self._values is not None: + if isinstance(self.attack_feature, int): + predictions = np.array([self._values[np.argmax(arr)] for arr in predictions]) + else: + i = 0 + for column in predictions.T: + for index in range(len(self._values[i])): + np.place(column, [column == index], self._values[i][index]) + i += 1 + return np.array(predictions) + + def _check_params(self) -> None: + if not isinstance(self.attack_feature, int) and not isinstance(self.attack_feature, slice): + raise TypeError("Attack feature must be either an integer or a slice object.") + + if isinstance(self.attack_feature, int) and self.attack_feature < 0: + raise ValueError("Attack feature index must be positive.") + + if self._attack_model_type not in ["nn", "rf"]: + raise ValueError("Illegal value for parameter `attack_model_type`.") diff --git a/src/holisticai/security/attackers/attribute_inference/dataset_utils.py b/src/holisticai/security/attackers/attribute_inference/dataset_utils.py new file mode 100644 index 00000000..fec1696f --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/dataset_utils.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +from typing import Optional + +import numpy as np +from sklearn.preprocessing import minmax_scale + + +def to_categorical(labels, nb_classes) -> np.ndarray: + """ + Convert an array of labels to binary class matrix. + + Parameters + ---------- + labels : np.ndarray + An array of integer labels of shape `(nb_samples,)`. + nb_classes : int + The number of classes (possible labels). + + Returns + ------- + np.ndarray + A binary matrix representation of `y` in the shape `(nb_samples, nb_classes)`. + """ + labels = np.array(labels, dtype=int) + if nb_classes is None: + nb_classes = np.max(labels) + 1 + categorical = np.zeros((labels.shape[0], nb_classes), dtype=np.float32) + categorical[np.arange(labels.shape[0]), np.squeeze(labels)] = 1 + return categorical + + +def check_and_transform_label_format( + labels: np.ndarray, nb_classes: Optional[int], return_one_hot: bool = True +) -> np.ndarray: + """ + Check label format and transform to one-hot-encoded labels if necessary + + Parameters + ---------- + labels : np.ndarray + An array of integer labels of shape `(nb_samples,)`, `(nb_samples, 1)` or `(nb_samples, nb_classes)`. + nb_classes : int + The number of classes. If None the number of classes is determined automatically. + return_one_hot : bool + True if returning one-hot encoded labels, False if returning index labels. + + Returns + ------- + np.ndarray + Labels with shape `(nb_samples, nb_classes)` (one-hot) or `(nb_samples,)` (index). + """ + labels_return = labels + + if len(labels.shape) == 2 and labels.shape[1] > 1: # multi-class, one-hot encoded + if not return_one_hot: + labels_return = np.argmax(labels, axis=1) + labels_return = np.expand_dims(labels_return, axis=1) + elif len(labels.shape) == 2 and labels.shape[1] == 1: + if nb_classes is None: + nb_classes = np.max(labels) + 1 + if nb_classes > 2: # multi-class, index labels + labels_return = to_categorical(labels, nb_classes) if return_one_hot else np.expand_dims(labels, axis=1) + elif nb_classes == 2: # binary, index labels + if return_one_hot: + labels_return = to_categorical(labels, nb_classes) + else: + raise ValueError( + "Shape of labels not recognised." + "Please provide labels in shape (nb_samples,) or (nb_samples, " + "nb_classes)" + ) + elif len(labels.shape) == 1: # index labels + labels_return = to_categorical(labels, nb_classes) if return_one_hot else np.expand_dims(labels, axis=1) + else: + raise ValueError( + "Shape of labels not recognised." + "Please provide labels in shape (nb_samples,) or (nb_samples, " + "nb_classes)" + ) + + return labels_return + + +def get_feature_values(x: np.ndarray, single_index_feature: bool) -> list: + """ + Returns a list of unique values of a given feature. + + Parameters + ---------- + x : np.ndarray + The feature column(s). + single_index_feature : bool + Bool representing whether this is a single-column or multiple-column feature (for + example 1-hot encoded and then scaled). + + Returns + ------- + list + For a single-column feature, a simple list containing all possible values, in increasing order. + For a multi-column feature, a list of lists, where each internal list represents a column and the values + represent the possible values for that column (in increasing order). + """ + values = None + if single_index_feature: + values = np.unique(x).tolist() + else: + for column in x.T: + column_values = np.unique(column) + values = column_values if values is None else np.vstack((values, column_values)) + if values is not None: + values = values.tolist() + return values + + +def floats_to_one_hot(labels: np.ndarray): + """ + Convert a 2D-array of floating point labels to binary class matrix. + + Parameters + ---------- + labels : np.ndarray + A 2D-array of floating point labels of shape `(nb_samples, nb_classes)`. + + Returns + ------- + np.ndarray + A binary matrix representation of `labels` in the shape `(nb_samples, nb_classes)`. + """ + labels = np.array(labels) + for feature in labels.T: # pylint: disable=E1133 + unique = np.unique(feature) + unique.sort() + for index, value in enumerate(unique): + feature[feature == value] = index + return labels.astype(np.float32) + + +def float_to_categorical(labels: np.ndarray, nb_classes: Optional[int] = None): + """ + Convert an array of floating point labels to binary class matrix. + + Parameters + ---------- + labels : np.ndarray + An array of floating point labels of shape `(nb_samples,)`. + nb_classes : int + The number of classes (possible labels). + + Returns + ------- + np.ndarray + A binary matrix representation of `labels` in the shape `(nb_samples, nb_classes)`. + """ + labels = np.array(labels) + unique = np.unique(labels) + unique.sort() + indexes = [np.where(unique == value)[0] for value in labels] + if nb_classes is None: + nb_classes = len(unique) + categorical = np.zeros((labels.shape[0], nb_classes), dtype=np.float32) + categorical[np.arange(labels.shape[0]), np.squeeze(indexes)] = 1 + return categorical + + +class AttackDataset: + """ + Class for splitting data into training and test sets for membership and attribute inference attacks. + + Parameters + ---------- + x : np.ndarray or tuple + Input data. If tuple, the first element is the training data and the second element is the test data. + y : np.ndarray or tuple, optional + Labels. If tuple, the first element is the training labels and the second element is the test labels. + attack_train_ratio : float, optional + The ratio of training data to use for the attack. Default is 0.5. + """ + + def __init__(self, x, y=None, attack_train_ratio: Optional[float] = 0.5): + if type(x) is tuple: + self.x_train, self.x_test = x + self.attack_train_size = int(len(self.x_train) * attack_train_ratio) + self.attack_test_size = int(len(self.x_test) * attack_train_ratio) + else: + self.x_train = x + self.attack_train_size = int(len(self.x_train) * attack_train_ratio) + + self.y_output = y is not None + + if self.y_output: + if type(y) is tuple: + self.y_train, self.y_test = y + else: + self.y_train = y + + def membership_inference_train(self): + """ + Get the training set for the membership inference attack. + + Returns + ------- + tuple + Tuple containing the training data and the membership + """ + x = np.concatenate([self.x_train[: self.attack_train_size :], self.x_test[: self.attack_test_size]]) + train_membership = np.ones(self.attack_train_size) + test_membership = np.zeros(self.attack_test_size) + membership = np.concatenate([train_membership, test_membership]) + + if not self.y_output: + return x, membership + + y = np.concatenate([self.y_train[: self.attack_train_size :], self.y_test[: self.attack_test_size]]) + return x, y, membership + + def membership_inference_test(self): + """ + Get the test set for the membership inference attack. + + Returns + ------- + tuple + Tuple containing the test data and the membership + """ + x = np.concatenate([self.x_train[self.attack_train_size :], self.x_test[self.attack_test_size :]]) + train_membership = np.ones(self.attack_train_size) + test_membership = np.zeros(self.attack_test_size) + membership = np.concatenate([train_membership, test_membership]) + + if not self.y_output: + return x, membership + + y = np.concatenate([self.y_train[self.attack_train_size :], self.y_test[self.attack_test_size :]]) + return x, y, membership + + def attribute_inference_train(self): + """ + Get the training set for the attribute inference attack. + + Returns + ------- + tuple + Tuple containing the training data and the attribute. + """ + attack_x_train = self.x_train[: self.attack_train_size] + + if not self.y_output: + return attack_x_train + + attack_y_train = self.y_train[: self.attack_train_size] + return attack_x_train, attack_y_train + + def attribute_inference_test(self): + """ + Get the test set for the attribute inference attack. + + Returns + ------- + tuple + Tuple containing the test data and the attribute. + """ + attack_x_test = self.x_train[self.attack_train_size :] + + if not self.y_output: + return attack_x_test + + attack_y_test = self.y_train[self.attack_train_size :] + return attack_x_test, attack_y_test + + +class AttributeInferenceDataPreprocessor: + """ + Class for preprocessing data for attribute inference attacks. + + Parameters + ---------- + attack_feature : int or slice + The feature to be attacked. + is_regression : bool, optional + Whether the attack is a regression attack. Default is False. + scale_range : tuple, optional + The range to scale the labels to. Default is None. + prediction_normal_factor : float, optional + The factor to normalize the predictions by. Default is 1. + """ + + def __init__(self, attack_feature, is_regression=None, scale_range=None, prediction_normal_factor=None): + self.is_regression = is_regression # if RegressorMixin in type(self.estimator).__mro__: + self.scale_range = scale_range + self.prediction_normal_factor = prediction_normal_factor + self.attack_feature = attack_feature + + def fit_transform(self, x, y=None, pred=None): + """ + Prepare the data for training the attack model. + + Parameters + ---------- + x : np.ndarray + The input data. + y : np.ndarray, optional + The true labels. + + Returns + ------- + np.ndarray or tuple + The training data for the attack model. If `y` is provided, a tuple is returned with the training data and + the labels. + """ + y_ready = self._get_feature_labels(x) + x_ready = np.delete(x, self.attack_feature, 1) + + # create training set for attack model + if y is not None: + normalized_labels = self._normalized_labels(y) + x_ready = np.c_[x_ready, normalized_labels].astype(np.float32) + + if pred is not None: + normalized_labels = self._normalized_labels(pred) + x_ready = np.c_[x_ready, normalized_labels].astype(np.float32) + + if y_ready is None: + return x_ready + return x_ready, y_ready + + def transform(self, x, y=None, pred=None): + """ + Prepare the data for inference with the attack model. + + Parameters + ---------- + x : np.ndarray + The input data. + y : np.ndarray, optional + The true labels. + + Returns + ------- + np.ndarray or tuple + The data for the attack model. If `y` is provided, a tuple is returned with the data and the labels. + """ + x_ready = x # np.delete(x, self.attack_feature, 1) + + # create training set for attack model + if y is not None: + normalized_labels = self._normalized_labels(y) + x_ready = np.c_[x_ready, normalized_labels].astype(np.float32) + + if pred is not None: + normalized_labels = self._normalized_labels(pred) + x_ready = np.c_[x_ready, normalized_labels].astype(np.float32) + + return x_ready + + def _get_feature_labels(self, x): + attacked_feature = x[:, self.attack_feature] + + self._values = get_feature_values(attacked_feature, isinstance(self.attack_feature, int)) + self._nb_classes = len(self._values) + + if isinstance(self.attack_feature, int): + y_one_hot = float_to_categorical(attacked_feature) + else: + y_one_hot = floats_to_one_hot(attacked_feature) + + y_ready = check_and_transform_label_format(y_one_hot, nb_classes=self._nb_classes, return_one_hot=True) + return y_ready + + def _normalized_labels(self, y): + if self.is_regression: + if self.scale_range is not None: + normalized_labels = minmax_scale(y, feature_range=self.scale_range) + else: + normalized_labels = y * self.prediction_normal_factor + normalized_labels = normalized_labels.reshape(-1, 1) + else: + normalized_labels = check_and_transform_label_format(y, nb_classes=None, return_one_hot=True) + return normalized_labels diff --git a/src/holisticai/security/attackers/attribute_inference/true_label_baseline.py b/src/holisticai/security/attackers/attribute_inference/true_label_baseline.py new file mode 100644 index 00000000..69ccbc0e --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/true_label_baseline.py @@ -0,0 +1,107 @@ +""" +This module implements attribute inference attacks. +""" + +from __future__ import annotations + +import logging +from typing import Optional, Union + +import numpy as np + +from holisticai.security.attackers.attribute_inference.dataset_utils import AttributeInferenceDataPreprocessor +from holisticai.security.attackers.attribute_inference.utils import get_attack_model, get_feature_index + +logger = logging.getLogger(__name__) + + +class AttributeInferenceBaselineTrueLabel: + def __init__( + self, + attack_model_type: str = "nn", + attack_feature: Union[int, slice] = 0, + is_regression: Optional[bool] = False, + scale_range: Optional[tuple[float, float]] = None, + prediction_normal_factor: float = 1, + ): + self._values: Optional[list] = None + self._nb_classes: Optional[int] = None + self.attack_model = get_attack_model(attack_model_type) + self.prediction_normal_factor = prediction_normal_factor + self.scale_range = scale_range + self.is_regression = is_regression + self.attack_feature = attack_feature + self._check_params() + self.attack_feature = get_feature_index(self.attack_feature) + self.ai_preprocessor = AttributeInferenceDataPreprocessor( + is_regression=is_regression, + scale_range=scale_range, + prediction_normal_factor=prediction_normal_factor, + attack_feature=attack_feature, + ) + + def _check_params(self) -> None: + if not isinstance(self.attack_feature, int) and not isinstance(self.attack_feature, slice): + raise TypeError("Attack feature must be either an integer or a slice object.") + + if isinstance(self.attack_feature, int) and self.attack_feature < 0: + raise ValueError("Attack feature index must be positive.") + + def fit(self, x: np.ndarray, y: np.ndarray) -> None: + """ + Train the attack model. + + Parameters + ---------- + x : np.ndarray + Input to training process. Includes all features used to train the original model. + y : np.ndarray + True labels of the features. + """ + + # train attack model + attack_x, attack_y = self.ai_preprocessor.fit_transform(x, y) + self._values = self.ai_preprocessor._values # noqa: SLF001 + self.attack_model.fit(attack_x, attack_y) + + def infer(self, x: np.ndarray, y: np.ndarray, **kwargs) -> np.ndarray: + """ + Infer the attacked feature. + + Parameters + ---------- + x : np.ndarray + Input to attack. Includes all features except the attacked feature. + y : np.ndarray + True labels of the features. + values : list + Possible values for attacked feature. For a single column feature this should be a simple list containing + all possible values, in increasing order (the smallest value in the 0 index and so on). For a multi-column + feature (for example 1-hot encoded and then scaled), this should be a list of lists, where each internal + list represents a column (in increasing order) and the values represent the possible values for that column + (in increasing order). + + Returns + ------- + np.ndarray + The inferred feature values. + """ + values: Optional[list] = kwargs.get("values") + + # if provided, override the values computed in fit() + if values is not None: + self._values = values + + attack_x = self.ai_preprocessor.transform(x, y) + predictions = self.attack_model.predict_proba(attack_x).astype(np.float32) + + if self._values is not None: + if isinstance(self.attack_feature, int): + predictions = np.array([self._values[np.argmax(arr)] for arr in predictions]) + else: + i = 0 + for column in predictions.T: + for index in range(len(self._values[i])): + np.place(column, [column == index], self._values[i][index]) + i += 1 + return np.array(predictions) diff --git a/src/holisticai/security/attackers/attribute_inference/utils.py b/src/holisticai/security/attackers/attribute_inference/utils.py new file mode 100644 index 00000000..8bc7e6d7 --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/utils.py @@ -0,0 +1,150 @@ +def get_attack_model(attack_model_type): + """ + Returns an attack model of the specified type. + + Parameters + ---------- + attack_model_type : str + The type of the attack model. Possible values are 'nn' for neural network and 'rf' for random forest. + + Returns + ------- + object + The attack model. + """ + if attack_model_type == "nn": + from sklearn.neural_network import MLPClassifier + + attack_model = MLPClassifier( + hidden_layer_sizes=(100,), + activation="relu", + solver="adam", + alpha=0.0001, + batch_size="auto", + learning_rate="constant", + learning_rate_init=0.001, + power_t=0.5, + max_iter=2000, + shuffle=True, + random_state=None, + tol=0.0001, + verbose=False, + warm_start=False, + momentum=0.9, + nesterovs_momentum=True, + early_stopping=False, + validation_fraction=0.1, + beta_1=0.9, + beta_2=0.999, + epsilon=1e-08, + n_iter_no_change=10, + max_fun=15000, + ) + elif attack_model_type == "rf": + from sklearn.ensemble import RandomForestClassifier + + attack_model = RandomForestClassifier() + else: + raise ValueError("Illegal value for parameter `attack_model_type`.") + return attack_model + + +def is_pipeline(estimator): + """ + Checks if the given estimator is a pipeline. + + Parameters + ---------- + estimator : object + The estimator to check. + + Returns + ------- + bool + True if the estimator is a pipeline. + """ + from holisticai.pipeline import Pipeline + + if type(estimator) is Pipeline: + return True + return None + + +def model_in_pipeline(estimator): + """ + Returns the model in a pipeline. + + Parameters + ---------- + estimator : object + The estimator to check. + + Returns + ------- + object + The model in the pipeline. + """ + return estimator.estimator_hdl.estimator.obj + + +def is_estimator_valid(estimator, estimator_requirements) -> bool: + """ + Checks if the given estimator satisfies the requirements for this attack. + + Parameters + ---------- + estimator : object + The estimator to check. + estimator_requirements : list + The requirements for the estimator. + + Returns + ------- + bool + True if the estimator is valid for the attack. + """ + + if is_pipeline(estimator): + model = model_in_pipeline(estimator) + return is_estimator_valid(model, estimator_requirements) + + for req in estimator_requirements: + # A requirement is either a class which the estimator must inherit from, or a tuple of classes and the + # estimator is required to inherit from at least one of the classes + if isinstance(req, tuple): + if all(p not in type(estimator).__mro__ for p in req): + return False + elif req not in type(estimator).__mro__: + return False + return True + + +def get_feature_index(feature): + """ + Returns a modified feature index: in case of a slice of size 1, returns the corresponding integer. Otherwise, + returns the same value (integer or slice) as passed. + + Parameters + ---------- + feature : int or slice + The index or slice representing a feature to attack (0-based). + + Returns + ------- + int or slice + An integer representing a single column index or a slice representing a multi-column index. + """ + if isinstance(feature, int): + return feature + + start = feature.start + stop = feature.stop + step = feature.step + if start is None: + start = 0 + if step is None: + step = 1 + if feature.stop is not None and ((stop - start) // step) == 1: + return start + + return feature diff --git a/src/holisticai/security/attackers/attribute_inference/white_box.py b/src/holisticai/security/attackers/attribute_inference/white_box.py new file mode 100644 index 00000000..b087e381 --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/white_box.py @@ -0,0 +1,166 @@ +""" +This module implements attribute inference attacks. +""" + +from __future__ import annotations + +import logging +from typing import Optional + +import numpy as np + +logger = logging.getLogger(__name__) + + +class AttributeInferenceWhiteBoxDecisionTree: + """ + A variation of the method proposed by of Fredrikson et al. in: + https://dl.acm.org/doi/10.1145/2810103.2813677 + + Assumes the availability of the attacked model's predictions for the samples under attack, in addition to access to + the model itself and the rest of the feature values. If this is not available, the true class label of the samples + may be used as a proxy. Also assumes that the attacked feature is discrete or categorical, with limited number of + possible values. For example: a boolean feature. + + Parameters + ---------- + classifier : ScikitlearnDecisionTreeClassifier + Target classifier. + attack_feature : int + The index of the feature to be attacked. + + References + ---------- + .. [1] Fredrikson, M., Jha, S., & Ristenpart, T. (2015, August). Model inversion attacks that exploit confidence + information and basic countermeasures. In Proceedings of the 22nd ACM SIGSAC Conference on Computer and + Communications Security (pp. 1322-1333). + """ + + def __init__(self, classifier, attack_feature: int = 0): + self.attack_feature: int + self.attack_feature = attack_feature + self._check_params() + self.estimator = classifier + + def _check_params(self) -> None: + if self.attack_feature < 0: + raise ValueError("Attack feature must be positive.") + + def infer(self, x: np.ndarray, y: np.ndarray, **kwargs) -> np.ndarray: + """ + Infer the attacked feature. + + If the model's prediction coincides with the real prediction for the sample for a single value, choose it as the + predicted value. If not, fall back to the Fredrikson method (without phi) + + Parameters + ---------- + x : np.ndarray + Input to attack. Includes all features except the attacked feature. + y : np.ndarray + Original model's predictions for x. + values : list + Possible values for attacked feature. + priors : list + Prior distributions of attacked feature values. Same size array as `values`. + + Returns + ------- + np.ndarray + The inferred feature values. + """ + if "priors" not in kwargs: # pragma: no cover + raise ValueError("Missing parameter `priors`.") + if "values" not in kwargs: # pragma: no cover + raise ValueError("Missing parameter `values`.") + priors: Optional[list] = kwargs.get("priors") + values: Optional[list] = kwargs.get("values") + + if priors is None or values is None: # pragma: no cover + raise ValueError("`priors` and `values` are required as inputs.") + if len(priors) != len(values): # pragma: no cover + raise ValueError("Number of priors does not match number of values") + + n_values = len(values) + n_samples = x.shape[0] + + # Will contain the model's predictions for each value + pred_values = [] + # Will contain the probability of each value + prob_values = [] + + for i, value in enumerate(values): + # prepare data with the given value in the attacked feature + v_full = np.full((n_samples, 1), value).astype(x.dtype) + x_value = np.concatenate((x[:, : self.attack_feature], v_full), axis=1) + x_value = np.concatenate((x_value, x[:, self.attack_feature :]), axis=1) + + # Obtain the model's prediction for each possible value of the attacked feature + pred_value = [np.argmax(arr) for arr in self.estimator.predict_proba(x_value)] + pred_values.append(pred_value) + + # find the relative probability of this value for all samples being attacked + prob_value = [ + ((self.get_samples_at_node(self.get_decision_path([row])[-1]) / n_samples) * priors[i]) + for row in x_value + ] + prob_values.append(prob_value) + + # Find the single value that coincides with the real prediction for the sample (if it exists) + pred_rows = zip(*pred_values) + predicted_pred = [] + for row_index, row in enumerate(pred_rows): + if y is not None: + matches = [1 if row[value_index] == y[row_index] else 0 for value_index in range(n_values)] + match_values = [ + values[value_index] if row[value_index] == y[row_index] else 0 for value_index in range(n_values) + ] + else: + matches = [0 for _ in range(n_values)] + match_values = [0 for _ in range(n_values)] + predicted_pred.append(sum(match_values) if sum(matches) == 1 else None) + + # Choose the value with highest probability for each sample + predicted_prob = [np.argmax(list(prob)) for prob in zip(*prob_values)] + + return np.array( + [ + value if value is not None else values[predicted_prob[index]] + for index, value in enumerate(predicted_pred) + ] + ) + + def get_decision_path(self, x: np.ndarray) -> np.ndarray: + """ + Returns the path through nodes in the tree when classifying x. Last one is leaf, first one root node. + + Parameters + ---------- + x : np.ndarray + Input sample. + + Returns + ------- + np.ndarray + The indices of the nodes in the array structure of the tree. + """ + if len(np.shape(x)) == 1: + return self.estimator.decision_path(x.reshape(1, -1)).indices + + return self.estimator.decision_path(x).indices + + def get_samples_at_node(self, node_id: int) -> int: + """ + Returns the number of training samples mapped to a node. + + Parameters + ---------- + node_id : int + Node id. + + Returns + ------- + int + Number of samples mapped this node. + """ + return self.estimator.tree_.n_node_samples[node_id] diff --git a/src/holisticai/security/attackers/attribute_inference/white_box_lifestyle.py b/src/holisticai/security/attackers/attribute_inference/white_box_lifestyle.py new file mode 100644 index 00000000..4ccb4560 --- /dev/null +++ b/src/holisticai/security/attackers/attribute_inference/white_box_lifestyle.py @@ -0,0 +1,168 @@ +""" +This module implements attribute inference attacks. +""" + +from __future__ import annotations + +import logging +from typing import Optional + +import numpy as np + +logger = logging.getLogger(__name__) + + +class AttributeInferenceWhiteBoxLifestyleDecisionTree: + """ + Implementation of Fredrikson et al. white box inference attack for decision trees. + + Assumes that the attacked feature is discrete or categorical, with limited number of possible values. For example: + a boolean feature. + + Parameters + ---------- + estimator : object + Target estimator. + attack_feature : int + The index of the feature to be attacked. + + References + ---------- + .. [1] Fredrikson, M., Jha, S., & Ristenpart, T. (2015, August). Model inversion attacks that exploit confidence\\ + information and basic countermeasures. In Proceedings of the 22nd ACM SIGSAC Conference on Computer and\\ + Communications Security (pp. 1322-1333). + + | Paper link: https://dl.acm.org/doi/10.1145/2810103.2813677 + """ + + def __init__(self, estimator, attack_feature: int = 0): + self.attack_feature: int + self.attack_feature = attack_feature + self._check_params() + self.estimator = estimator + + def _check_params(self) -> None: + if self.attack_feature < 0: + raise ValueError("Attack feature must be positive.") + + def infer(self, x: np.ndarray, **kwargs) -> np.ndarray: + """ + Infer the attacked feature. + + Parameters + ---------- + x : np.ndarray + Input to attack. Includes all features except the attacked feature. + kwargs : dict + Possible values for attacked feature and prior distributions of attacked feature values. + + Returns + ------- + np.ndarray + The inferred feature values. + """ + priors: Optional[list] = kwargs.get("priors") + values: Optional[list] = kwargs.get("values") + + # Checks: + if priors is None or values is None: # pragma: no cover + raise ValueError("`priors` and `values` are required as inputs.") + if len(priors) != len(values): # pragma: no cover + raise ValueError("Number of priors does not match number of values") + + n_samples = x.shape[0] + + # Calculate phi for each possible value of the attacked feature + # phi is the total number of samples in all tree leaves corresponding to this value + phi = self._calculate_phi(x, values, n_samples) + + # Will contain the probability of each value + prob_values = [] + + for i, value in enumerate(values): + # prepare data with the given value in the attacked feature + v_full = np.full((n_samples, 1), value).astype(x.dtype) + x_value = np.concatenate((x[:, : self.attack_feature], v_full), axis=1) + x_value = np.concatenate((x_value, x[:, self.attack_feature :]), axis=1) + + # find the relative probability of this value for all samples being attacked + prob_value = [ + ((self.get_samples_at_node(self.get_decision_path([row])[-1]) / n_samples) * priors[i] / phi[i]) + for row in x_value + ] + prob_values.append(prob_value) + + # Choose the value with highest probability for each sample + return np.array([values[np.argmax(list(prob))] for prob in zip(*prob_values)]) + + def _calculate_phi(self, x, values, n_samples): + phi = [] + for value in values: + v_full = np.full((n_samples, 1), value).astype(x.dtype) + x_value = np.concatenate((x[:, : self.attack_feature], v_full), axis=1) + x_value = np.concatenate((x_value, x[:, self.attack_feature :]), axis=1) + nodes_value = {} + + for row in x_value: + # get leaf ids (no duplicates) + node_id = self.get_decision_path([row])[-1] + nodes_value[node_id] = self.get_samples_at_node(node_id) + # sum sample numbers + num_value = sum(nodes_value.values()) / n_samples + phi.append(num_value) + + return phi + + def get_decision_path(self, x: np.ndarray) -> np.ndarray: + """ + Returns the path through nodes in the tree when classifying x. Last one is leaf, first one root node. + + Parameters + ---------- + x : np.ndarray + Input sample. + + Returns + ------- + np.ndarray + The indices of the nodes in the array structure of the tree. + """ + if len(np.shape(x)) == 1: + return self.estimator.decision_path(x.reshape(1, -1)).indices + + return self.estimator.decision_path(x).indices + + def get_samples_at_node(self, node_id: int) -> int: + """ + Returns the number of training samples mapped to a node. + + Parameters + ---------- + node_id : int + Node id. + + Returns + ------- + int + Number of samples mapped this node. + """ + return self.estimator.tree_.n_node_samples[node_id] + + def _get_input_shape(self, model) -> Optional[tuple[int, ...]]: + _input_shape: Optional[tuple[int, ...]] + if hasattr(model, "n_features_"): + _input_shape = (model.n_features_,) + elif hasattr(model, "n_features_in_"): + _input_shape = (model.n_features_in_,) + elif hasattr(model, "feature_importances_"): + _input_shape = (len(model.feature_importances_),) + elif hasattr(model, "coef_"): + _input_shape = (model.coef_.shape[0],) if len(model.coef_.shape) == 1 else (model.coef_.shape[1],) + elif hasattr(model, "support_vectors_"): + _input_shape = (model.support_vectors_.shape[1],) + elif hasattr(model, "steps"): + _input_shape = self._get_input_shape(model.steps[-1][1].obj) + else: + logger.warning("Input shape not recognised. The model might not have been fitted.") + _input_shape = None + return _input_shape From 6da37e6e954c4f7d4ee08bdfe8f75093a315e1f0 Mon Sep 17 00:00:00 2001 From: franklincf Date: Wed, 7 May 2025 20:34:44 -0300 Subject: [PATCH 2/3] tests: adding tests for attribute inference attack --- ...test_attribute_inference_classification.py | 181 ++++++++++++++++++ .../test_attribute_inference_regression.py | 171 +++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 tests/security/test_attribute_inference_classification.py create mode 100644 tests/security/test_attribute_inference_regression.py diff --git a/tests/security/test_attribute_inference_classification.py b/tests/security/test_attribute_inference_classification.py new file mode 100644 index 00000000..3867afd4 --- /dev/null +++ b/tests/security/test_attribute_inference_classification.py @@ -0,0 +1,181 @@ +import numpy as np +from holisticai.datasets import load_dataset +from holisticai.security.attackers.attribute_inference.baseline import AttributeInferenceBaseline + +from holisticai.security.attackers.attribute_inference.black_box import AttributeInferenceBlackBox +from holisticai.security.attackers.attribute_inference.true_label_baseline import AttributeInferenceBaselineTrueLabel +from holisticai.security.attackers.attribute_inference.white_box_lifestyle import ( + AttributeInferenceWhiteBoxLifestyleDecisionTree +) +from holisticai.security.attackers.attribute_inference.white_box import ( + AttributeInferenceWhiteBoxDecisionTree, +) +from sklearn.tree import DecisionTreeClassifier +from holisticai.efficacy.metrics import classification_efficacy_metrics +import pytest + +np.random.seed(100) + +SHARD_SIZE=60 + +@pytest.fixture +def categorical_dataset(): + dataset = load_dataset("adult", protected_attribute='race') # x,y,group_a,group_b + dataset = dataset.groupby(['y','group_a']).sample(SHARD_SIZE, random_state=42) # 0-ga | 0-gb | 1-ga | 1-gb + train_test = dataset.train_test_split(test_size=0.5, stratify=dataset['y'], random_state=0) + train = train_test['train'] + test = train_test['test'] + correlations = train['X'].corrwith(train['y']).sort_values(ascending=False) + top_10_features = correlations.head(10).index.tolist() + train['X'] = train['X'][top_10_features] + test['X'] = test['X'][top_10_features] + return train, test + + +def test_att_inf_baseline(categorical_dataset): + + train, test = categorical_dataset + + attack_feature = 0 # marital status + + x_train = train['X'].copy().values + y_train = train['y'].copy().values + x_test = test['X'].copy().values + y_test = test['y'].copy().values + + attack = AttributeInferenceBaseline(attack_feature=attack_feature) + attack.fit(x_train) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + feat_pred = attack.infer(attack_x_test, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_black_box(categorical_dataset): + train, test = categorical_dataset + + attack_feature = 0 # marital status + + x_train = train['X'].copy().values + y_train = train['y'].copy().values + x_test = test['X'].copy().values + y_test = test['y'].copy().values + + classifier = DecisionTreeClassifier() + classifier.fit(x_train, y_train) + # classifier = SklearnClassifier(classifier) + + attack = AttributeInferenceBlackBox(estimator=classifier, attack_feature=attack_feature) + + pred = classifier.predict_proba(x_train) + attack.fit(x_train, y_train, pred) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + pred = classifier.predict_proba(x_test) + + feat_true = x_test[:, attack_feature] + + values = [0, 1] + feat_pred = attack.infer(attack_x_test, y_test, pred, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_truelabel(categorical_dataset): + train, test = categorical_dataset + + attack_feature = 0 # marital status + + x_train = train['X'].copy().values + y_train = train['y'].copy().values + x_test = test['X'].copy().values + y_test = test['y'].copy().values + + attack = AttributeInferenceBaselineTrueLabel(attack_feature=attack_feature) + attack.fit(x_train, y_train) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + feat_pred = attack.infer(attack_x_test, y_test, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_white_box_lifestyle(categorical_dataset): + train, test = categorical_dataset + + attack_feature = 0 # marital status + + x_train = train['X'].copy().values + y_train = train['y'].copy().values + x_test = test['X'].copy().values + y_test = test['y'].copy().values + + classifier = DecisionTreeClassifier() + classifier.fit(x_train, y_train) + # classifier = ScikitlearnDecisionTreeClassifier(classifier) + + attack = AttributeInferenceWhiteBoxLifestyleDecisionTree(attack_feature=attack_feature, estimator=classifier) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + priors = [53/120, 67/120] + + feat_pred = attack.infer(attack_x_test, values=values, priors=priors) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_white_box(categorical_dataset): + train, test = categorical_dataset + + attack_feature = 0 # marital status + + x_train = train['X'].copy().values + y_train = train['y'].copy().values + x_test = test['X'].copy().values + y_test = test['y'].copy().values + + classifier = DecisionTreeClassifier() + classifier.fit(x_train, y_train) + # classifier = ScikitlearnDecisionTreeClassifier(classifier) + + attack = AttributeInferenceWhiteBoxDecisionTree(attack_feature=attack_feature, classifier=classifier) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + priors = [53/120, 67/120] + + feat_pred = attack.infer(attack_x_test, y_test, values=values, priors=priors) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 \ No newline at end of file diff --git a/tests/security/test_attribute_inference_regression.py b/tests/security/test_attribute_inference_regression.py new file mode 100644 index 00000000..ff03e481 --- /dev/null +++ b/tests/security/test_attribute_inference_regression.py @@ -0,0 +1,171 @@ +import numpy as np +from holisticai.datasets import load_dataset +from holisticai.security.attackers.attribute_inference.baseline import AttributeInferenceBaseline + +from holisticai.security.attackers.attribute_inference.black_box import AttributeInferenceBlackBox +from holisticai.security.attackers.attribute_inference.true_label_baseline import AttributeInferenceBaselineTrueLabel + +from holisticai.security.attackers.attribute_inference.white_box_lifestyle import ( + AttributeInferenceWhiteBoxLifestyleDecisionTree +) +from holisticai.efficacy.metrics import classification_efficacy_metrics +from holisticai.pipeline import Pipeline +from sklearn.tree import DecisionTreeRegressor +import pytest + +np.random.seed(100) + +SHARD_SIZE = 60 + +@pytest.fixture +def regression_dataset(): + dataset = load_dataset('us_crime', preprocessed=True, protected_attribute='race') + dataset = dataset.sample(SHARD_SIZE, random_state=42) + train_test = dataset.train_test_split(test_size=0.5, random_state=42) + train = train_test['train'] + test = train_test['test'] + return train, test + +def test_att_inf_baseline_regression(regression_dataset): + train, test = regression_dataset + train_data = train['X'].copy() + train_data['group_a'] = train['group_a'].astype(int) + + test_data = test['X'].copy() + test_data['group_a'] = test['group_a'].astype(int) + + x_train = train_data.values + x_test = test_data.values + + y_train = train['y'].values + y_test = test['y'].values + + attack_feature = 101 # last column represents the sensitive attribute + + attack = AttributeInferenceBaseline(attack_feature=attack_feature) + attack.fit(x_train) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + feat_pred = attack.infer(attack_x_test, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + +def test_att_inf_blackbox_regression(regression_dataset): + train, test = regression_dataset + train_data = train['X'].copy() + train_data['group_a'] = train['group_a'].astype(int) + + test_data = test['X'].copy() + test_data['group_a'] = test['group_a'].astype(int) + + x_train = train_data.values + x_test = test_data.values + + y_train = train['y'].values + y_test = test['y'].values + + attack_feature = 101 # last column represents the sensitive attribute + + regressor = Pipeline(steps=[ + ('model', DecisionTreeRegressor()) + ]) + + regressor.fit(x_train, y_train) + + # regressor = train_holisticai_regressor(x_train, y_train) + # regressor = ScikitlearnRegressor(regressor) + pred = regressor.predict(x_train) + + attack = AttributeInferenceBlackBox(estimator=regressor, attack_feature=attack_feature, scale_range=(0,1)) + attack.fit(x_train, y_train, pred) + + pred = regressor.predict(x_test) + attack_x_test = np.delete(x_test, attack_feature, axis=1) + + feat_true = x_test[:, attack_feature] + + values = [False, True] + feat_pred = attack.infer(attack_x_test, y_test, pred, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_baseline_true_label_regression(regression_dataset): + train, test = regression_dataset + train_data = train['X'].copy() + train_data['group_a'] = train['group_a'].astype(int) + + test_data = test['X'].copy() + test_data['group_a'] = test['group_a'].astype(int) + + x_train = train_data.values + x_test = test_data.values + + y_train = train['y'].values + y_test = test['y'].values + + attack_feature = 101 # last column represents the sensitive attribute + + attack = AttributeInferenceBaselineTrueLabel(attack_feature=attack_feature, is_regression=True) + attack.fit(x_train, y_train) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + feat_true = x_test[:, attack_feature] + + values = [0, 1] + feat_pred = attack.infer(attack_x_test, y_test, values=values) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 + + +def test_att_inf_white_box_lifestyle_decision_tree_regression(regression_dataset): + train, test = regression_dataset + train_data = train['X'].copy() + train_data['group_a'] = train['group_a'].astype(int) + + test_data = test['X'].copy() + test_data['group_a'] = test['group_a'].astype(int) + + x_train = train_data.values + x_test = test_data.values + + y_train = train['y'].values + y_test = test['y'].values + + attack_feature = 101 # last column represents the sensitive attribute + + regressor = DecisionTreeRegressor() + regressor.fit(x_train, y_train) + + # regressor = ScikitlearnDecisionTreeRegressor(regressor) + attack = AttributeInferenceWhiteBoxLifestyleDecisionTree(estimator=regressor, attack_feature=attack_feature) + + attack_x_test = np.delete(x_test, attack_feature, axis=1) + + feat_true = x_test[:, attack_feature] + + values = [0, 1] + priors = [2 / 30, 28 / 30] + + feat_pred = attack.infer(attack_x_test, values=values, priors=priors) + + assert len(feat_true) == len(feat_pred) + + df = classification_efficacy_metrics(feat_true, feat_pred) + + assert df.loc['Accuracy']['Value'] > 0.5 \ No newline at end of file From b6b72a19c6524ab2bf45d074225fc42ffd9ce18a Mon Sep 17 00:00:00 2001 From: franklincf Date: Wed, 7 May 2025 20:35:18 -0300 Subject: [PATCH 3/3] chore: adding tutorials for attribute inference attack --- .../classification/baseline.ipynb | 688 +++++++++++++++++ .../classification/black_box.ipynb | 470 ++++++++++++ .../classification/true_label.ipynb | 691 ++++++++++++++++++ .../classification/white_box.ipynb | 480 ++++++++++++ .../classification/white_box_lifestyle.ipynb | 480 ++++++++++++ .../regression/baseline.ipynb | 649 ++++++++++++++++ .../regression/black_box.ipynb | 663 +++++++++++++++++ .../regression/true_label.ipynb | 650 ++++++++++++++++ .../regression/white_box.ipynb | 677 +++++++++++++++++ 9 files changed, 5448 insertions(+) create mode 100644 tutorials/security/attribute_inference_attacks/classification/baseline.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/classification/black_box.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/classification/true_label.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/classification/white_box.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/classification/white_box_lifestyle.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/regression/baseline.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/regression/black_box.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/regression/true_label.ipynb create mode 100644 tutorials/security/attribute_inference_attacks/regression/white_box.ipynb diff --git a/tutorials/security/attribute_inference_attacks/classification/baseline.ipynb b/tutorials/security/attribute_inference_attacks/classification/baseline.ipynb new file mode 100644 index 00000000..9c10a107 --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/classification/baseline.ipynb @@ -0,0 +1,688 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the baseline module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the baseline module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will consider the 'Job' attribute of the German credit dataset as the attribute to be inferred. \n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.baseline import AttributeInferenceBaseline\n", + "\n", + "np.random.seed(100)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 800
Features: X , y , p_attrs
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":800,\"Features\":[\"X , y , p_attrs\"]},\"metadata\":null}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('german_credit', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.2, stratify=dataset['y'], random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
JobCredit amountDurationHousing_freeHousing_ownHousing_rentSaving accounts_quite richSaving accounts_littleSaving accounts_moderateSaving accounts_rich...Checking account_moderateChecking account_richPurpose_'domestic appliances'Purpose_businessPurpose_carPurpose_educationPurpose_furniture/equipmentPurpose_radio/TVPurpose_repairsPurpose_vacation/others
022181300.01.00.00.01.00.00.0...1.00.00.00.01.00.00.00.00.00.0
126148200.01.00.00.00.01.00.0...1.00.00.00.01.00.00.00.00.00.0
222058240.01.00.00.01.00.00.0...0.00.00.00.00.00.00.00.01.00.0
322613360.01.00.00.01.00.00.0...0.00.00.00.00.00.00.00.01.00.0
4173180.01.00.00.01.00.00.0...0.00.00.00.01.00.00.00.00.00.0
\n", + "

5 rows × 21 columns

\n", + "
" + ], + "text/plain": [ + " Job Credit amount Duration Housing_free Housing_own Housing_rent \\\n", + "0 2 2181 30 0.0 1.0 0.0 \n", + "1 2 6148 20 0.0 1.0 0.0 \n", + "2 2 2058 24 0.0 1.0 0.0 \n", + "3 2 2613 36 0.0 1.0 0.0 \n", + "4 1 731 8 0.0 1.0 0.0 \n", + "\n", + " Saving accounts_quite rich Saving accounts_little \\\n", + "0 0.0 1.0 \n", + "1 0.0 0.0 \n", + "2 0.0 1.0 \n", + "3 0.0 1.0 \n", + "4 0.0 1.0 \n", + "\n", + " Saving accounts_moderate Saving accounts_rich ... \\\n", + "0 0.0 0.0 ... \n", + "1 1.0 0.0 ... \n", + "2 0.0 0.0 ... \n", + "3 0.0 0.0 ... \n", + "4 0.0 0.0 ... \n", + "\n", + " Checking account_moderate Checking account_rich \\\n", + "0 1.0 0.0 \n", + "1 1.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "4 0.0 0.0 \n", + "\n", + " Purpose_'domestic appliances' Purpose_business Purpose_car \\\n", + "0 0.0 0.0 1.0 \n", + "1 0.0 0.0 1.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "4 0.0 0.0 1.0 \n", + "\n", + " Purpose_education Purpose_furniture/equipment Purpose_radio/TV \\\n", + "0 0.0 0.0 0.0 \n", + "1 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 \n", + "\n", + " Purpose_repairs Purpose_vacation/others \n", + "0 0.0 0.0 \n", + "1 0.0 0.0 \n", + "2 1.0 0.0 \n", + "3 1.0 0.0 \n", + "4 0.0 0.0 \n", + "\n", + "[5 rows x 21 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The German credit dataset is a dataset that contains information about 1000 loan applicants. The dataset has 20 attributes, including the 'Job' attribute, which will be the attribute to be inferred. The 'Job' attribute has four possible values: 'Unskilled', 'Unskilled Resident', 'Skilled', and 'Highly Skilled'. The dataset also contains a binary target attribute, 'Credit', which indicates whether the loan application was approved or not.\n", + "\n", + "For this demonstration, we will transform the 'Job' attribute into a binary attribute by considering only two values: 'Highly Skilled', represented by 1, and 'Not Highly Skilled', represented by 0. The goal of the attack will be to infer the value of the 'Job' attribute of a data record by querying a model trained on the data." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "job_feat_train = train['X']['Job'].values\n", + "job_feat_train[job_feat_train < 2] = 0\n", + "job_feat_train[job_feat_train >= 2] = 1\n", + "\n", + "job_feat_test = test['X']['Job'].values\n", + "job_feat_test[job_feat_test < 2] = 0\n", + "job_feat_test[job_feat_test >= 2] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make this demonstration more simple, we will also remove the 'Credit amount' attribute and the 'Duration' attribute from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "x_test = test['X'].drop(columns=['Credit amount', 'Duration']).values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also replace the transformed 'Job' attribute into the original training data and test data." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 0 # job feature\n", + "\n", + "x_train[:, attack_feature] = job_feat_train\n", + "x_test[:, attack_feature] = job_feat_test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - baseline**\n", + "\n", + "Now, we will perform a baseline attack to infer the selected attribute using the `AttributeInferenceBaseline` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "attack = AttributeInferenceBaseline(attack_feature=attack_feature)\n", + "\n", + "attack.fit(x_train)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "feat_pred = attack.infer(attack_x_test, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.7500001
Balanced Accuracy0.5022591
Precision0.7708331
Recall0.9610391
F1-Score0.8554911
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.750000 1\n", + "Balanced Accuracy 0.502259 1\n", + "Precision 0.770833 1\n", + "Recall 0.961039 1\n", + "F1-Score 0.855491 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.75, which means that the attack was able to infer the 'Job' attribute with an accuracy of 75%." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/classification/black_box.ipynb b/tutorials/security/attribute_inference_attacks/classification/black_box.ipynb new file mode 100644 index 00000000..09775eb7 --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/classification/black_box.ipynb @@ -0,0 +1,470 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the blackbox module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the blackbox module on a classification model. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will consider the 'Job' attribute of the German credit dataset as the attribute to be inferred. \n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.black_box import AttributeInferenceBlackBox" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 800
Features: X , y , p_attrs
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":800,\"Features\":[\"X , y , p_attrs\"]},\"metadata\":null}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('german_credit', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.2, stratify=dataset['y'], random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The German credit dataset is a dataset that contains information about 1000 loan applicants. The dataset has 20 attributes, including the 'Job' attribute, which will be the attribute to be inferred. The 'Job' attribute has four possible values: 'Unskilled', 'Unskilled Resident', 'Skilled', and 'Highly Skilled'. The dataset also contains a binary target attribute, 'Credit', which indicates whether the loan application was approved or not.\n", + "\n", + "For this demonstration, we will transform the 'Job' attribute into a binary attribute by considering only two values: 'Highly Skilled', represented by 1, and 'Not Highly Skilled', represented by 0. The goal of the attack will be to infer the value of the 'Job' attribute of a data record by querying a model trained on the data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "job_feat_train = train['X']['Job'].values\n", + "job_feat_train[job_feat_train < 2] = 0\n", + "job_feat_train[job_feat_train >= 2] = 1\n", + "\n", + "job_feat_test = test['X']['Job'].values\n", + "job_feat_test[job_feat_test < 2] = 0\n", + "job_feat_test[job_feat_test >= 2] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make this demonstration more simple, we will also remove the 'Credit amount' attribute and the 'Duration' attribute from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "x_test = test['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also replace the transformed 'Job' attribute into the original training data and test data." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 0\n", + "x_train[:, attack_feature] = job_feat_train\n", + "x_test[:, attack_feature] = job_feat_test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - blackbox**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceBlackBox` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. This module assumes the availability of the attacked model's predictions for the samples under attack, in addition to the rest of the feature values. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/franklin/.local/share/hatch/env/virtual/holisticai/Rsz3flyg/testing/lib/python3.10/site-packages/sklearn/neural_network/_multilayer_perceptron.py:690: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (2000) reached and the optimization hasn't converged yet.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "from sklearn.tree import DecisionTreeClassifier\n", + "\n", + "classifier = DecisionTreeClassifier()\n", + "classifier.fit(x_train, y_train)\n", + "\n", + "attack = AttributeInferenceBlackBox(estimator=classifier, attack_feature=attack_feature)\n", + "\n", + "pred = classifier.predict_proba(x_train)\n", + "attack.fit(x_train, y_train, pred)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "pred = classifier.predict_proba(x_test)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "feat_pred = attack.infer(attack_x_test, y_test, pred, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.9300001
Balanced Accuracy0.8935631
Precision0.9487181
Recall0.9610391
F1-Score0.9548391
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.930000 1\n", + "Balanced Accuracy 0.893563 1\n", + "Precision 0.948718 1\n", + "Recall 0.961039 1\n", + "F1-Score 0.954839 1" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.915, which means that the attack was able to infer the 'Job' attribute with an accuracy of 91.5%, which is a high accuracy. This demonstrates the vulnerability of the model to attribute inference attacks and the importance of protecting sensitive attributes in the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/classification/true_label.ipynb b/tutorials/security/attribute_inference_attacks/classification/true_label.ipynb new file mode 100644 index 00000000..8fc221a5 --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/classification/true_label.ipynb @@ -0,0 +1,691 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the TrueLabel module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the TrueLabel module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will consider the 'Job' attribute of the German credit dataset as the attribute to be inferred. \n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.true_label_baseline import AttributeInferenceBaselineTrueLabel\n", + "\n", + "np.random.seed(100)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 800
Features: X , y , p_attrs
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":800,\"Features\":[\"X , y , p_attrs\"]},\"metadata\":null}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('german_credit', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.2, stratify=dataset['y'], random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
JobCredit amountDurationHousing_freeHousing_ownHousing_rentSaving accounts_quite richSaving accounts_littleSaving accounts_moderateSaving accounts_rich...Checking account_moderateChecking account_richPurpose_'domestic appliances'Purpose_businessPurpose_carPurpose_educationPurpose_furniture/equipmentPurpose_radio/TVPurpose_repairsPurpose_vacation/others
022181300.01.00.00.01.00.00.0...1.00.00.00.01.00.00.00.00.00.0
126148200.01.00.00.00.01.00.0...1.00.00.00.01.00.00.00.00.00.0
222058240.01.00.00.01.00.00.0...0.00.00.00.00.00.00.00.01.00.0
322613360.01.00.00.01.00.00.0...0.00.00.00.00.00.00.00.01.00.0
4173180.01.00.00.01.00.00.0...0.00.00.00.01.00.00.00.00.00.0
\n", + "

5 rows × 21 columns

\n", + "
" + ], + "text/plain": [ + " Job Credit amount Duration Housing_free Housing_own Housing_rent \\\n", + "0 2 2181 30 0.0 1.0 0.0 \n", + "1 2 6148 20 0.0 1.0 0.0 \n", + "2 2 2058 24 0.0 1.0 0.0 \n", + "3 2 2613 36 0.0 1.0 0.0 \n", + "4 1 731 8 0.0 1.0 0.0 \n", + "\n", + " Saving accounts_quite rich Saving accounts_little \\\n", + "0 0.0 1.0 \n", + "1 0.0 0.0 \n", + "2 0.0 1.0 \n", + "3 0.0 1.0 \n", + "4 0.0 1.0 \n", + "\n", + " Saving accounts_moderate Saving accounts_rich ... \\\n", + "0 0.0 0.0 ... \n", + "1 1.0 0.0 ... \n", + "2 0.0 0.0 ... \n", + "3 0.0 0.0 ... \n", + "4 0.0 0.0 ... \n", + "\n", + " Checking account_moderate Checking account_rich \\\n", + "0 1.0 0.0 \n", + "1 1.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "4 0.0 0.0 \n", + "\n", + " Purpose_'domestic appliances' Purpose_business Purpose_car \\\n", + "0 0.0 0.0 1.0 \n", + "1 0.0 0.0 1.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "4 0.0 0.0 1.0 \n", + "\n", + " Purpose_education Purpose_furniture/equipment Purpose_radio/TV \\\n", + "0 0.0 0.0 0.0 \n", + "1 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 \n", + "\n", + " Purpose_repairs Purpose_vacation/others \n", + "0 0.0 0.0 \n", + "1 0.0 0.0 \n", + "2 1.0 0.0 \n", + "3 1.0 0.0 \n", + "4 0.0 0.0 \n", + "\n", + "[5 rows x 21 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The German credit dataset is a dataset that contains information about 1000 loan applicants. The dataset has 20 attributes, including the 'Job' attribute, which will be the attribute to be inferred. The 'Job' attribute has four possible values: 'Unskilled', 'Unskilled Resident', 'Skilled', and 'Highly Skilled'. The dataset also contains a binary target attribute, 'Credit', which indicates whether the loan application was approved or not.\n", + "\n", + "For this demonstration, we will transform the 'Job' attribute into a binary attribute by considering only two values: 'Highly Skilled', represented by 1, and 'Not Highly Skilled', represented by 0. The goal of the attack will be to infer the value of the 'Job' attribute of a data record by querying a model trained on the data." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "job_feat_train = train['X']['Job'].values\n", + "job_feat_train[job_feat_train < 2] = 0\n", + "job_feat_train[job_feat_train >= 2] = 1\n", + "\n", + "job_feat_test = test['X']['Job'].values\n", + "job_feat_test[job_feat_test < 2] = 0\n", + "job_feat_test[job_feat_test >= 2] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make this demonstration more simple, we will also remove the 'Credit amount' attribute and the 'Duration' attribute from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "x_test = test['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also replace the transformed 'Job' attribute into the original training data and test data." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 0 # job feature\n", + "\n", + "x_train[:, attack_feature] = job_feat_train\n", + "x_test[:, attack_feature] = job_feat_test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - TrueLabel**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceBaselineTrueLabel` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data. This module works similar to the baseline module, but requires the target model's input data to be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "attack = AttributeInferenceBaselineTrueLabel(attack_feature=attack_feature)\n", + "\n", + "attack.fit(x_train, y_train)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "feat_pred = attack.infer(attack_x_test, y_test, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.7300001
Balanced Accuracy0.4892721
Precision0.7659571
Recall0.9350651
F1-Score0.8421051
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.730000 1\n", + "Balanced Accuracy 0.489272 1\n", + "Precision 0.765957 1\n", + "Recall 0.935065 1\n", + "F1-Score 0.842105 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.73, which means that the attack was able to infer the 'Job' attribute with an accuracy of 73%." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/classification/white_box.ipynb b/tutorials/security/attribute_inference_attacks/classification/white_box.ipynb new file mode 100644 index 00000000..0f7ceadf --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/classification/white_box.ipynb @@ -0,0 +1,480 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the WhiteBox module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the WhiteBox module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will consider the 'Job' attribute of the German credit dataset as the attribute to be inferred. \n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.white_box import AttributeInferenceWhiteBoxDecisionTree" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 800
Features: X , y , p_attrs
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":800,\"Features\":[\"X , y , p_attrs\"]},\"metadata\":null}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('german_credit', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.2, stratify=dataset['y'], random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The German credit dataset is a dataset that contains information about 1000 loan applicants. The dataset has 20 attributes, including the 'Job' attribute, which will be the attribute to be inferred. The 'Job' attribute has four possible values: 'Unskilled', 'Unskilled Resident', 'Skilled', and 'Highly Skilled'. The dataset also contains a binary target attribute, 'Credit', which indicates whether the loan application was approved or not.\n", + "\n", + "For this demonstration, we will transform the 'Job' attribute into a binary attribute by considering only two values: 'Highly Skilled', represented by 1, and 'Not Highly Skilled', represented by 0. The goal of the attack will be to infer the value of the 'Job' attribute of a data record by querying a model trained on the data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "job_feat_train = train['X']['Job'].values\n", + "job_feat_train[job_feat_train < 2] = 0\n", + "job_feat_train[job_feat_train >= 2] = 1\n", + "\n", + "job_feat_test = test['X']['Job'].values\n", + "job_feat_test[job_feat_test < 2] = 0\n", + "job_feat_test[job_feat_test >= 2] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((array([0, 1]), array([ 46, 154])), 200)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unique_values = np.unique(job_feat_test, return_counts=True)\n", + "unique_values, len(job_feat_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make this demonstration more simple, we will also remove the 'Credit amount' attribute and the 'Duration' attribute from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "x_test = test['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also replace the transformed 'Job' attribute into the original training data and test data." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 0\n", + "x_train[:, attack_feature] = job_feat_train\n", + "x_test[:, attack_feature] = job_feat_test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - WhiteBox**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceWhiteBoxDecisionTree` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data. This is a variation of the method proposed by [Fredrikson et al.](https://dl.acm.org/doi/10.1145/2810103.2813677), since it assumes the availability of the attacked model's predictions for the samples under attack, in addition to access to the model itself and the rest of the feature values. Unlike the WhiteBoxLifeStyle module, this module requires the target values to pefrom the attack." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.tree import DecisionTreeClassifier\n", + "\n", + "classifier = DecisionTreeClassifier()\n", + "classifier.fit(x_train, y_train)\n", + "\n", + "attack = AttributeInferenceWhiteBoxDecisionTree(classifier=classifier, attack_feature=attack_feature)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "priors = [46/200, 154/200]\n", + "\n", + "feat_pred = attack.infer(attack_x_test, y_test, values=values, priors=priors)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.7150001
Balanced Accuracy0.4795311
Precision0.7621621
Recall0.9155841
F1-Score0.8318581
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.715000 1\n", + "Balanced Accuracy 0.479531 1\n", + "Precision 0.762162 1\n", + "Recall 0.915584 1\n", + "F1-Score 0.831858 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.71, which means that the attack was able to infer the 'Job' attribute with an accuracy of 71%." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/classification/white_box_lifestyle.ipynb b/tutorials/security/attribute_inference_attacks/classification/white_box_lifestyle.ipynb new file mode 100644 index 00000000..089f0a8c --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/classification/white_box_lifestyle.ipynb @@ -0,0 +1,480 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the WhiteBoxLifeStyle module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the WhiteBoxLifeStyle module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will consider the 'Job' attribute of the German credit dataset as the attribute to be inferred. \n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.white_box_lifestyle import AttributeInferenceWhiteBoxLifestyleDecisionTree" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 800
Features: X , y , p_attrs
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":800,\"Features\":[\"X , y , p_attrs\"]},\"metadata\":null}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('german_credit', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.2, stratify=dataset['y'], random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The German credit dataset is a dataset that contains information about 1000 loan applicants. The dataset has 20 attributes, including the 'Job' attribute, which will be the attribute to be inferred. The 'Job' attribute has four possible values: 'Unskilled', 'Unskilled Resident', 'Skilled', and 'Highly Skilled'. The dataset also contains a binary target attribute, 'Credit', which indicates whether the loan application was approved or not.\n", + "\n", + "For this demonstration, we will transform the 'Job' attribute into a binary attribute by considering only two values: 'Highly Skilled', represented by 1, and 'Not Highly Skilled', represented by 0. The goal of the attack will be to infer the value of the 'Job' attribute of a data record by querying a model trained on the data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "job_feat_train = train['X']['Job'].values\n", + "job_feat_train[job_feat_train < 2] = 0\n", + "job_feat_train[job_feat_train >= 2] = 1\n", + "\n", + "job_feat_test = test['X']['Job'].values\n", + "job_feat_test[job_feat_test < 2] = 0\n", + "job_feat_test[job_feat_test >= 2] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((array([0, 1]), array([ 46, 154])), 200)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unique_values = np.unique(job_feat_test, return_counts=True)\n", + "unique_values, len(job_feat_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make this demonstration more simple, we will also remove the 'Credit amount' attribute and the 'Duration' attribute from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "x_test = test['X'].drop(columns=['Credit amount', 'Duration']).values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also replace the transformed 'Job' attribute into the original training data and test data." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 0\n", + "x_train[:, attack_feature] = job_feat_train\n", + "x_test[:, attack_feature] = job_feat_test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - WhiteBoxLifestyle**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceWhiteBoxLifestyleDecisionTree` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data. For our case we will use a implementation of [Fredrikson et al.](https://dl.acm.org/doi/10.1145/2810103.2813677) to perform the attack, specifically for decision trees." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.tree import DecisionTreeClassifier\n", + "\n", + "classifier = DecisionTreeClassifier()\n", + "classifier.fit(x_train, y_train)\n", + "\n", + "attack = AttributeInferenceWhiteBoxLifestyleDecisionTree(estimator=classifier, attack_feature=attack_feature)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "priors = [46/200, 154/200]\n", + "\n", + "feat_pred = attack.infer(attack_x_test, values=values, priors=priors)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.7300001
Balanced Accuracy0.4968941
Precision0.7688171
Recall0.9285711
F1-Score0.8411761
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.730000 1\n", + "Balanced Accuracy 0.496894 1\n", + "Precision 0.768817 1\n", + "Recall 0.928571 1\n", + "F1-Score 0.841176 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.735, which means that the attack was able to infer the 'Job' attribute with an accuracy of 73.5%." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/regression/baseline.ipynb b/tutorials/security/attribute_inference_attacks/regression/baseline.ipynb new file mode 100644 index 00000000..d3b96047 --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/regression/baseline.ipynb @@ -0,0 +1,649 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the baseline module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the baseline module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will work with the 'us_crime' dataset, which contains information about crime rates in the United States, and we will consider the 'race' attribute to perform the attack.\n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.baseline import AttributeInferenceBaseline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 1594
Features: X , y , p_attrs , group_a , group_b
Metadata: race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":1594,\"Features\":[\"X , y , p_attrs , group_a , group_b\"]},\"metadata\":\"race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}\"}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('us_crime', preprocessed=True, protected_attribute='race')\n", + "train_test = dataset.train_test_split(test_size=0.2, random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
statefoldpopulationhouseholdsizeracepctblackracePctAsianracePctHispagePct12t21agePct12t29agePct16t24...NumStreetPctForeignBornPctBornSameStatePctSameHouse85PctSameCity85PctSameState85LandAreaPopDensPctUsePubTransLemasPctOfficDrugUn
04580.140.560.850.090.030.760.830.77...0.090.090.620.330.360.360.340.070.271.0
1620.010.400.020.110.220.390.420.25...0.010.190.660.300.570.780.010.260.020.0
25550.000.330.000.010.010.360.400.24...0.000.030.670.760.770.710.020.150.020.0
33430.010.710.360.210.060.770.650.66...0.000.350.480.660.530.570.010.480.820.0
41380.010.650.080.200.040.480.360.20...0.000.130.220.200.000.000.070.070.010.0
\n", + "

5 rows × 101 columns

\n", + "
" + ], + "text/plain": [ + " state fold population householdsize racepctblack racePctAsian \\\n", + "0 45 8 0.14 0.56 0.85 0.09 \n", + "1 6 2 0.01 0.40 0.02 0.11 \n", + "2 55 5 0.00 0.33 0.00 0.01 \n", + "3 34 3 0.01 0.71 0.36 0.21 \n", + "4 13 8 0.01 0.65 0.08 0.20 \n", + "\n", + " racePctHisp agePct12t21 agePct12t29 agePct16t24 ... NumStreet \\\n", + "0 0.03 0.76 0.83 0.77 ... 0.09 \n", + "1 0.22 0.39 0.42 0.25 ... 0.01 \n", + "2 0.01 0.36 0.40 0.24 ... 0.00 \n", + "3 0.06 0.77 0.65 0.66 ... 0.00 \n", + "4 0.04 0.48 0.36 0.20 ... 0.00 \n", + "\n", + " PctForeignBorn PctBornSameState PctSameHouse85 PctSameCity85 \\\n", + "0 0.09 0.62 0.33 0.36 \n", + "1 0.19 0.66 0.30 0.57 \n", + "2 0.03 0.67 0.76 0.77 \n", + "3 0.35 0.48 0.66 0.53 \n", + "4 0.13 0.22 0.20 0.00 \n", + "\n", + " PctSameState85 LandArea PopDens PctUsePubTrans LemasPctOfficDrugUn \n", + "0 0.36 0.34 0.07 0.27 1.0 \n", + "1 0.78 0.01 0.26 0.02 0.0 \n", + "2 0.71 0.02 0.15 0.02 0.0 \n", + "3 0.57 0.01 0.48 0.82 0.0 \n", + "4 0.00 0.07 0.07 0.01 0.0 \n", + "\n", + "[5 rows x 101 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The 'us_crime' dataset that we will use in this notebook is a processed version that contains 1594 records and 102 attributes, including the protected attribute that we will use in the attack. This protected attribute is a binary attribute that indicates whether the individual is white or non-white." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = train['X'].copy()\n", + "train_data['group_a'] = train['group_a'].astype(int)\n", + "\n", + "test_data = test['X'].copy()\n", + "test_data['group_a'] = test['group_a'].astype(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train_data.values\n", + "x_test = test_data.values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 101 # last column represents the sensitive attribute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - baseline**\n", + "\n", + "Now, we will perform a baseline attack to infer the selected attribute using the `AttributeInferenceBaseline` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "attack = AttributeInferenceBaseline(attack_feature=attack_feature)\n", + "\n", + "attack.fit(x_train)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "feat_pred = attack.infer(attack_x_test, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.9749371
Balanced Accuracy0.9509591
Precision0.9852511
Recall0.9852511
F1-Score0.9852511
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.974937 1\n", + "Balanced Accuracy 0.950959 1\n", + "Precision 0.985251 1\n", + "Recall 0.985251 1\n", + "F1-Score 0.985251 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy around of 0.97, which means that the attack was able to infer the 'group_a' attribute with an accuracy of 97%. This high accuracy indicates that the target model is leaking information about the 'group_a' attribute, which can be a privacy concern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/regression/black_box.ipynb b/tutorials/security/attribute_inference_attacks/regression/black_box.ipynb new file mode 100644 index 00000000..58b90e4b --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/regression/black_box.ipynb @@ -0,0 +1,663 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the BlackBox module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the BlackBox module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will work with the 'us_crime' dataset, which contains information about crime rates in the United States, and we will consider the 'race' attribute to perform the attack.\n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.black_box import AttributeInferenceBlackBox" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 1594
Features: X , y , p_attrs , group_a , group_b
Metadata: race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":1594,\"Features\":[\"X , y , p_attrs , group_a , group_b\"]},\"metadata\":\"race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}\"}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('us_crime', preprocessed=True, protected_attribute='race')\n", + "train_test = dataset.train_test_split(test_size=0.2, random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
statefoldpopulationhouseholdsizeracepctblackracePctAsianracePctHispagePct12t21agePct12t29agePct16t24...NumStreetPctForeignBornPctBornSameStatePctSameHouse85PctSameCity85PctSameState85LandAreaPopDensPctUsePubTransLemasPctOfficDrugUn
04580.140.560.850.090.030.760.830.77...0.090.090.620.330.360.360.340.070.271.0
1620.010.400.020.110.220.390.420.25...0.010.190.660.300.570.780.010.260.020.0
25550.000.330.000.010.010.360.400.24...0.000.030.670.760.770.710.020.150.020.0
33430.010.710.360.210.060.770.650.66...0.000.350.480.660.530.570.010.480.820.0
41380.010.650.080.200.040.480.360.20...0.000.130.220.200.000.000.070.070.010.0
\n", + "

5 rows × 101 columns

\n", + "
" + ], + "text/plain": [ + " state fold population householdsize racepctblack racePctAsian \\\n", + "0 45 8 0.14 0.56 0.85 0.09 \n", + "1 6 2 0.01 0.40 0.02 0.11 \n", + "2 55 5 0.00 0.33 0.00 0.01 \n", + "3 34 3 0.01 0.71 0.36 0.21 \n", + "4 13 8 0.01 0.65 0.08 0.20 \n", + "\n", + " racePctHisp agePct12t21 agePct12t29 agePct16t24 ... NumStreet \\\n", + "0 0.03 0.76 0.83 0.77 ... 0.09 \n", + "1 0.22 0.39 0.42 0.25 ... 0.01 \n", + "2 0.01 0.36 0.40 0.24 ... 0.00 \n", + "3 0.06 0.77 0.65 0.66 ... 0.00 \n", + "4 0.04 0.48 0.36 0.20 ... 0.00 \n", + "\n", + " PctForeignBorn PctBornSameState PctSameHouse85 PctSameCity85 \\\n", + "0 0.09 0.62 0.33 0.36 \n", + "1 0.19 0.66 0.30 0.57 \n", + "2 0.03 0.67 0.76 0.77 \n", + "3 0.35 0.48 0.66 0.53 \n", + "4 0.13 0.22 0.20 0.00 \n", + "\n", + " PctSameState85 LandArea PopDens PctUsePubTrans LemasPctOfficDrugUn \n", + "0 0.36 0.34 0.07 0.27 1.0 \n", + "1 0.78 0.01 0.26 0.02 0.0 \n", + "2 0.71 0.02 0.15 0.02 0.0 \n", + "3 0.57 0.01 0.48 0.82 0.0 \n", + "4 0.00 0.07 0.07 0.01 0.0 \n", + "\n", + "[5 rows x 101 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The 'us_crime' dataset that we will use in this notebook is a processed version that contains 1594 records and 102 attributes, including the protected attribute that we will use in the attack. This protected attribute is a binary attribute that indicates whether the individual is white or non-white." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = train['X'].copy()\n", + "train_data['group_a'] = train['group_a'].astype(int)\n", + "\n", + "test_data = test['X'].copy()\n", + "test_data['group_a'] = test['group_a'].astype(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train_data.values\n", + "x_test = test_data.values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 101 # last column represents the sensitive attribute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - blackbox**\n", + "\n", + "Now, we will perform a attack to infer the selected attribute using the `AttributeInferenceBlackBox` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. This module assumes the availability of the attacked model's predictions for the samples under attack, in addition to the rest of the feature values. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from holisticai.pipeline import Pipeline\n", + "from sklearn.tree import DecisionTreeRegressor\n", + "\n", + "regressor = Pipeline(steps=[\n", + " ('model', DecisionTreeRegressor())\n", + "])\n", + "\n", + "regressor.fit(x_train, y_train)\n", + "\n", + "attack = AttributeInferenceBlackBox(estimator=regressor, attack_feature=attack_feature, scale_range=(0,1))\n", + "\n", + "pred = regressor.predict(x_train)\n", + "\n", + "attack.fit(x_train, y_train, pred)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "\n", + "pred = regressor.predict(x_test)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [False, True]\n", + "feat_pred = attack.infer(attack_x_test, y_test, pred, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.9699251
Balanced Accuracy0.9137171
Precision0.9711821
Recall0.9941001
F1-Score0.9825071
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.969925 1\n", + "Balanced Accuracy 0.913717 1\n", + "Precision 0.971182 1\n", + "Recall 0.994100 1\n", + "F1-Score 0.982507 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.969, which means that the attack was able to infer the 'group_a' attribute with an accuracy of 96.9%, which is a high accuracy. This demonstrates the vulnerability of the model to attribute inference attacks and the importance of protecting sensitive attributes in the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/regression/true_label.ipynb b/tutorials/security/attribute_inference_attacks/regression/true_label.ipynb new file mode 100644 index 00000000..ec588feb --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/regression/true_label.ipynb @@ -0,0 +1,650 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the TrueLabel module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the TrueLabel module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will work with the 'us_crime' dataset, which contains information about crime rates in the United States, and we will consider the 'race' attribute to perform the attack.\n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.true_label_baseline import AttributeInferenceBaselineTrueLabel" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 1594
Features: X , y , p_attrs , group_a , group_b
Metadata: race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":1594,\"Features\":[\"X , y , p_attrs , group_a , group_b\"]},\"metadata\":\"race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}\"}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('us_crime', preprocessed=True, protected_attribute='race')\n", + "train_test = dataset.train_test_split(test_size=0.2, random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
statefoldpopulationhouseholdsizeracepctblackracePctAsianracePctHispagePct12t21agePct12t29agePct16t24...NumStreetPctForeignBornPctBornSameStatePctSameHouse85PctSameCity85PctSameState85LandAreaPopDensPctUsePubTransLemasPctOfficDrugUn
04580.140.560.850.090.030.760.830.77...0.090.090.620.330.360.360.340.070.271.0
1620.010.400.020.110.220.390.420.25...0.010.190.660.300.570.780.010.260.020.0
25550.000.330.000.010.010.360.400.24...0.000.030.670.760.770.710.020.150.020.0
33430.010.710.360.210.060.770.650.66...0.000.350.480.660.530.570.010.480.820.0
41380.010.650.080.200.040.480.360.20...0.000.130.220.200.000.000.070.070.010.0
\n", + "

5 rows × 101 columns

\n", + "
" + ], + "text/plain": [ + " state fold population householdsize racepctblack racePctAsian \\\n", + "0 45 8 0.14 0.56 0.85 0.09 \n", + "1 6 2 0.01 0.40 0.02 0.11 \n", + "2 55 5 0.00 0.33 0.00 0.01 \n", + "3 34 3 0.01 0.71 0.36 0.21 \n", + "4 13 8 0.01 0.65 0.08 0.20 \n", + "\n", + " racePctHisp agePct12t21 agePct12t29 agePct16t24 ... NumStreet \\\n", + "0 0.03 0.76 0.83 0.77 ... 0.09 \n", + "1 0.22 0.39 0.42 0.25 ... 0.01 \n", + "2 0.01 0.36 0.40 0.24 ... 0.00 \n", + "3 0.06 0.77 0.65 0.66 ... 0.00 \n", + "4 0.04 0.48 0.36 0.20 ... 0.00 \n", + "\n", + " PctForeignBorn PctBornSameState PctSameHouse85 PctSameCity85 \\\n", + "0 0.09 0.62 0.33 0.36 \n", + "1 0.19 0.66 0.30 0.57 \n", + "2 0.03 0.67 0.76 0.77 \n", + "3 0.35 0.48 0.66 0.53 \n", + "4 0.13 0.22 0.20 0.00 \n", + "\n", + " PctSameState85 LandArea PopDens PctUsePubTrans LemasPctOfficDrugUn \n", + "0 0.36 0.34 0.07 0.27 1.0 \n", + "1 0.78 0.01 0.26 0.02 0.0 \n", + "2 0.71 0.02 0.15 0.02 0.0 \n", + "3 0.57 0.01 0.48 0.82 0.0 \n", + "4 0.00 0.07 0.07 0.01 0.0 \n", + "\n", + "[5 rows x 101 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The 'us_crime' dataset that we will use in this notebook is a processed version that contains 1594 records and 102 attributes, including the protected attribute that we will use in the attack. This protected attribute is a binary attribute that indicates whether the individual is white or non-white." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = train['X'].copy()\n", + "train_data['group_a'] = train['group_a'].astype(int)\n", + "\n", + "test_data = test['X'].copy()\n", + "test_data['group_a'] = test['group_a'].astype(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train_data.values\n", + "x_test = test_data.values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 101 # last column represents the sensitive attribute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - TrueLabel**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceBaselineTrueLabel` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data. This module works similar to the baseline module, but requires the target model's input data to be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "attack = AttributeInferenceBaselineTrueLabel(attack_feature=attack_feature, is_regression=True)\n", + "\n", + "attack.fit(x_train, y_train)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "feat_pred = attack.infer(attack_x_test, y_test, values=values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.9674191
Balanced Accuracy0.9396761
Precision0.9822491
Recall0.9793511
F1-Score0.9807981
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.967419 1\n", + "Balanced Accuracy 0.939676 1\n", + "Precision 0.982249 1\n", + "Recall 0.979351 1\n", + "F1-Score 0.980798 1" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.967, which means that the attack was able to infer the 'group_a' attribute with an accuracy of 96.7%. This high accuracy indicates that the target model is leaking information about the 'group_a' attribute, which can be a privacy concern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/security/attribute_inference_attacks/regression/white_box.ipynb b/tutorials/security/attribute_inference_attacks/regression/white_box.ipynb new file mode 100644 index 00000000..e52f89dd --- /dev/null +++ b/tutorials/security/attribute_inference_attacks/regression/white_box.ipynb @@ -0,0 +1,677 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Attribute inference attack with the WhiteBox module**\n", + "\n", + "In this notebook, we will demonstrate how to perform an attribute inference attack with the WhiteBox module. The goal of an attribute inference attack is to infer the value of a sensitive attribute of a data record by querying a model trained on the data. In this case, we will work with the 'us_crime' dataset, which contains information about crime rates in the United States, and we will consider the 'race' attribute to perform the attack.\n", + "\n", + "### **Importing the necessary libraries and loading the data** " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from holisticai.datasets import load_dataset\n", + "from holisticai.security.attackers.attribute_inference.white_box_lifestyle import AttributeInferenceWhiteBoxLifestyleDecisionTree" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " [Dataset]\n", + "
\n", + "
\n", + "
Instances: 1594
Features: X , y , p_attrs , group_a , group_b
Metadata: race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}
\n", + " \n", + "
\n", + "
\n", + "
\n", + "
\n", + " \n", + " " + ], + "text/plain": [ + "{\"dtype\":\"Dataset\",\"attributes\":{\"Instances\":1594,\"Features\":[\"X , y , p_attrs , group_a , group_b\"]},\"metadata\":\"race: {'group_a': 'racePctWhite>0.5', 'group_b': 'racePctWhite<=0.5'}\"}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = load_dataset('us_crime', preprocessed=True, protected_attribute='race')\n", + "train_test = dataset.train_test_split(test_size=0.2, random_state=0)\n", + "train = train_test['train']\n", + "test = train_test['test']\n", + "train" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
statefoldpopulationhouseholdsizeracepctblackracePctAsianracePctHispagePct12t21agePct12t29agePct16t24...NumStreetPctForeignBornPctBornSameStatePctSameHouse85PctSameCity85PctSameState85LandAreaPopDensPctUsePubTransLemasPctOfficDrugUn
04580.140.560.850.090.030.760.830.77...0.090.090.620.330.360.360.340.070.271.0
1620.010.400.020.110.220.390.420.25...0.010.190.660.300.570.780.010.260.020.0
25550.000.330.000.010.010.360.400.24...0.000.030.670.760.770.710.020.150.020.0
33430.010.710.360.210.060.770.650.66...0.000.350.480.660.530.570.010.480.820.0
41380.010.650.080.200.040.480.360.20...0.000.130.220.200.000.000.070.070.010.0
\n", + "

5 rows × 101 columns

\n", + "
" + ], + "text/plain": [ + " state fold population householdsize racepctblack racePctAsian \\\n", + "0 45 8 0.14 0.56 0.85 0.09 \n", + "1 6 2 0.01 0.40 0.02 0.11 \n", + "2 55 5 0.00 0.33 0.00 0.01 \n", + "3 34 3 0.01 0.71 0.36 0.21 \n", + "4 13 8 0.01 0.65 0.08 0.20 \n", + "\n", + " racePctHisp agePct12t21 agePct12t29 agePct16t24 ... NumStreet \\\n", + "0 0.03 0.76 0.83 0.77 ... 0.09 \n", + "1 0.22 0.39 0.42 0.25 ... 0.01 \n", + "2 0.01 0.36 0.40 0.24 ... 0.00 \n", + "3 0.06 0.77 0.65 0.66 ... 0.00 \n", + "4 0.04 0.48 0.36 0.20 ... 0.00 \n", + "\n", + " PctForeignBorn PctBornSameState PctSameHouse85 PctSameCity85 \\\n", + "0 0.09 0.62 0.33 0.36 \n", + "1 0.19 0.66 0.30 0.57 \n", + "2 0.03 0.67 0.76 0.77 \n", + "3 0.35 0.48 0.66 0.53 \n", + "4 0.13 0.22 0.20 0.00 \n", + "\n", + " PctSameState85 LandArea PopDens PctUsePubTrans LemasPctOfficDrugUn \n", + "0 0.36 0.34 0.07 0.27 1.0 \n", + "1 0.78 0.01 0.26 0.02 0.0 \n", + "2 0.71 0.02 0.15 0.02 0.0 \n", + "3 0.57 0.01 0.48 0.82 0.0 \n", + "4 0.00 0.07 0.07 0.01 0.0 \n", + "\n", + "[5 rows x 101 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train['X'].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Dataset preprocessing**\n", + "\n", + "The 'us_crime' dataset that we will use in this notebook is a processed version that contains 1594 records and 102 attributes, including the protected attribute that we will use in the attack. This protected attribute is a binary attribute that indicates whether the individual is white or non-white." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data = train['X'].copy()\n", + "train_data['group_a'] = train['group_a'].astype(int)\n", + "\n", + "test_data = test['X'].copy()\n", + "test_data['group_a'] = test['group_a'].astype(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([0, 1]), array([ 60, 339]))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.unique(test['group_a'].astype(int), return_counts=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "x_train = train_data.values\n", + "x_test = test_data.values\n", + "\n", + "y_train = train['y'].values\n", + "y_test = test['y'].values" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "attack_feature = 101 # last column represents the sensitive attribute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Attribute inference attack - WhiteBox**\n", + "\n", + "Now, we will perform an attack to infer the selected attribute using the `AttributeInferenceWhiteBoxDecisionTree` class from the `holisticai` library. This class, creates an object that uses an internal model to perform the attack. The internal model is trained on the same dataset used to train the target model to learn the attacked feature from the remaining features. The attack is performed by querying the internal model with the target model's predictions and the target model's input data. This is a variation of the method proposed by [Fredrikson et al.](https://dl.acm.org/doi/10.1145/2810103.2813677), since it assumes the availability of the attacked model's predictions for the samples under attack, in addition to access to the model itself and the rest of the feature values. Unlike the WhiteBoxLifeStyle module, this module requires the target values to pefrom the attack.\n", + "\n", + "To do this, we will first train a decision tree regressor model on the 'us_crime' dataset and then we will pass this model to the `AttributeInferenceWhiteBoxDecisionTree` class to perform the attack." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.tree import DecisionTreeRegressor\n", + "\n", + "regressor = DecisionTreeRegressor()\n", + "regressor.fit(x_train, y_train)\n", + "\n", + "attack = AttributeInferenceWhiteBoxLifestyleDecisionTree(estimator=regressor, attack_feature=attack_feature)\n", + "\n", + "attack_x_test = np.delete(x_test, attack_feature, axis=1)\n", + "\n", + "feat_true = x_test[:, attack_feature]\n", + "\n", + "values = [0, 1]\n", + "priors = [60 / 399, 339 / 399]\n", + "\n", + "feat_pred = attack.infer(attack_x_test, values=values, priors=priors)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Measuring the attack success**\n", + "\n", + "The success of the attack is measured by the accuracy of the inferred attribute. The accuracy is calculated as the ratio of the correctly inferred attributes to the total number of data records. This can be done by using traditional classification metrics such as accuracy, precision, recall, and F1-score. For our case, we will use the `classification_efficacy_metrics` function from the `holisticai` library to calculate these metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ValueReference
Metric
Accuracy0.8496241
Balanced Accuracy0.5000001
Precision0.8496241
Recall1.0000001
F1-Score0.9186991
\n", + "
" + ], + "text/plain": [ + " Value Reference\n", + "Metric \n", + "Accuracy 0.849624 1\n", + "Balanced Accuracy 0.500000 1\n", + "Precision 0.849624 1\n", + "Recall 1.000000 1\n", + "F1-Score 0.918699 1" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from holisticai.efficacy.metrics import classification_efficacy_metrics\n", + "\n", + "classification_efficacy_metrics(feat_true, feat_pred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our attack achieved an accuracy of 0.849, which means that the attack was able to infer the 'Job' attribute with an accuracy of 84.9%. This is a high accuracy, which indicates that the attack was successful in inferring the sensitive attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "testing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}