diff --git a/docs/source/gallery/tutorials/security.rst b/docs/source/gallery/tutorials/security.rst index 0909efe4..cb9438ac 100644 --- a/docs/source/gallery/tutorials/security.rst +++ b/docs/source/gallery/tutorials/security.rst @@ -22,3 +22,7 @@ Privacy Risk Score Mitigation ---------- - `Anonymization `_ + +Attackers +--------- +- `Membership Inference ML-leaks `_ \ No newline at end of file diff --git a/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb b/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb new file mode 100644 index 00000000..5fb7f19f --- /dev/null +++ b/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8f36a4eb", + "metadata": {}, + "source": [ + "# Membership inference attack of a binary classification Model\n", + "\n", + "In this notebook, we will evaluate the security of a binary classification model trained on the Adult dataset. We will use the ML-leaks method to perform membership inference attack (MIA) to train an attacker model and measure the model's security against this attacker using traditional metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e7d2e610", + "metadata": {}, + "outputs": [], + "source": [ + "from holisticai.security.attackers import MLleaks\n", + "from holisticai.datasets import load_dataset\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.metrics import accuracy_score\n", + "from sklearn import metrics" + ] + }, + { + "cell_type": "markdown", + "id": "a7148d07", + "metadata": {}, + "source": [ + "## Loading the dataset\n", + "\n", + "We will use the Adult dataset, which contains census data. The target variable is whether a person's income exceeds $50K/year, and the protected attribute we will consider is 'sex'. For time constraints, we will only use a small subset of the data for testing the model and use the attackers.\n", + "\n", + "Following the MIA pipeline of this method, we will assume that the attacker has a dataset that comes from the same distribution as the training dataset. This dataset will be used to train a shadow model that will be used to mimic the target model. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5a5c0dae", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = load_dataset('adult', protected_attribute='sex', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.5, random_state=42)\n", + "target = train_test['train'].train_test_split(test_size=0.5, random_state=42)\n", + "shadow = train_test['test'].train_test_split(test_size=0.5, random_state=42)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b16eb921", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training set size: 11305\n", + "Test set size: 11306\n", + "Shadow Training set size: 11305\n", + "Shadow Test set size: 11306\n" + ] + } + ], + "source": [ + "X_target_train = target['train']['X']\n", + "y_target_train = target['train']['y']\n", + "X_target_test = target['test']['X']\n", + "y_target_test = target['test']['y']\n", + "\n", + "X_shadow_train = shadow['train']['X']\n", + "y_shadow_train = shadow['train']['y']\n", + "X_shadow_test = shadow['test']['X']\n", + "y_shadow_test = shadow['test']['y']\n", + "\n", + "print(\"Training set size:\", X_target_train.shape[0])\n", + "print(\"Test set size:\", X_target_test.shape[0])\n", + "\n", + "print(\"Shadow Training set size:\", X_shadow_train.shape[0])\n", + "print(\"Shadow Test set size:\", X_shadow_test.shape[0])" + ] + }, + { + "cell_type": "markdown", + "id": "def203ae", + "metadata": {}, + "source": [ + "## Training the target model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "194cf6d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target Model Performance:\n", + "Accuracy: 0.8439766495666018\n" + ] + } + ], + "source": [ + "target_model = RandomForestClassifier(random_state=42)\n", + "target_model.fit(X_target_train, y_target_train)\n", + "y_target_pred = target_model.predict(X_target_test)\n", + "print(\"Target Model Performance:\")\n", + "print(\"Accuracy:\", accuracy_score(y_target_test, y_target_pred))" + ] + }, + { + "cell_type": "markdown", + "id": "2a4c09d1", + "metadata": {}, + "source": [ + "## Setting up the membership inference attack\n", + "\n", + "We will use the ML-leaks method to perform membership inference attack (MIA) presented by Salem et al. in 2018. The method relaxes the assumption that the attacker uses multiple shadow models to train the attacker model. Instead, it uses a single shadow model to get the confidence scores, generate and attacker dataset and use it to train the attacker model." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1744b648", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Task: binary\n" + ] + } + ], + "source": [ + "tgt_dataset = ((X_target_train, y_target_train), (X_target_test, y_target_test))\n", + "sdw_dataset = ((X_shadow_train, y_shadow_train), (X_shadow_test, y_shadow_test))\n", + "\n", + "mia_attacker = MLleaks(target_model, tgt_dataset, sdw_dataset)\n", + "train_attacker_data, test_attacker_data = mia_attacker.generate_attack_dataset()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e423c0b6", + "metadata": {}, + "outputs": [], + "source": [ + "X_mia_train, y_mia_train = train_attacker_data\n", + "X_mia_test, y_mia_test = test_attacker_data" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9e0d0a24", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attacker Model Performance:\n", + "Accuracy: 0.6052363893680067\n" + ] + } + ], + "source": [ + "# Train the Attacker Model\n", + "attacker_model = mia_attacker.fit()\n", + "y_mia_test_pred = attacker_model.predict(X_mia_test)\n", + "print(\"Attacker Model Performance:\")\n", + "print(\"Accuracy:\", accuracy_score(y_mia_test, y_mia_test_pred))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bcb13897", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attacker Precision: 0.5752705182560274\n", + "Attacker Recall: 0.8041574524546661\n", + "Attacker F1 Score: 0.6707245093699277\n" + ] + } + ], + "source": [ + "precision = metrics.precision_score(y_mia_test, y_mia_test_pred)\n", + "recall = metrics.recall_score(y_mia_test, y_mia_test_pred)\n", + "f1 = metrics.f1_score(y_mia_test, y_mia_test_pred)\n", + "print(\"Attacker Precision:\", precision)\n", + "print(\"Attacker Recall:\", recall)\n", + "print(\"Attacker F1 Score:\", f1)" + ] + }, + { + "cell_type": "markdown", + "id": "a63fd318", + "metadata": {}, + "source": [ + "From these results, we can see that the attacker model achieves a precision of 0.575 and a recall of 0.805. The F1 score is 0.67, which indicates that the attacker model is able to identify the members of the target model with a reasonable accuracy. " + ] + } + ], + "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": 5 +} diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 24e8f3bf..e593c231 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -67,6 +67,8 @@ Security in machine learning involves practices and measures to protect AI model - Metrics for evaluating security in classification and regression tasks. * - `Mitigation `_ - Strategies for enhancing security and robustness in learning tasks. + * - `Attackers `_ + - Techniques and strategies to simulate attacks and test model security. Robustness ---------- diff --git a/docs/source/reference/security/attackers.rst b/docs/source/reference/security/attackers.rst new file mode 100644 index 00000000..0c05fa1f --- /dev/null +++ b/docs/source/reference/security/attackers.rst @@ -0,0 +1,15 @@ +:py:mod:`holisticai.security.attackers` +========================================= + +.. automodule:: holisticai.security.attackers + :no-members: + :no-inherited-members: + +**Membership inference attack** + +.. autosummary:: + :nosignatures: + :template: function.rst + :toctree: .generated/ + + MLleaks \ No newline at end of file diff --git a/src/holisticai/security/attackers/__init__.py b/src/holisticai/security/attackers/__init__.py new file mode 100644 index 00000000..580b94ce --- /dev/null +++ b/src/holisticai/security/attackers/__init__.py @@ -0,0 +1,3 @@ +from holisticai.security.attackers.membership_inference.ml_leaks import MLleaks + +__all__ = ["MLleaks"] diff --git a/src/holisticai/security/attackers/membership_inference/__init__.py b/src/holisticai/security/attackers/membership_inference/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/holisticai/security/attackers/membership_inference/ml_leaks.py b/src/holisticai/security/attackers/membership_inference/ml_leaks.py new file mode 100644 index 00000000..a6dc6b1f --- /dev/null +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -0,0 +1,284 @@ +import logging + +import numpy as np +from sklearn.base import BaseEstimator, clone + +logger = logging.getLogger(__name__) + + +class MLleaks: + """ + Class for performing Membership Inference Attacks (MIA) using shadow models. + + This class is designed to train a shadow model on a shadow dataset and then \ + use the shadow model to create a membership inference dataset. The class \ + supports only binary classification task. + + The procedure follows the implementation of the MIA attack as described in \ + "ML-Leaks: Model and Data Independent Membership Inference Attacks and \ + Defenses on Machine Learning Models" by Salem et al. (2018). + + Parameters + ---------- + target_model : BaseEstimator + The target model to be attacked. It should be an instance of a scikit-learn \ + estimator that implements the `predict_proba` method. + target_dataset : tuple + A tuple containing the target training and testing datasets. Each dataset \ + should be a tuple of (X, y), where X is the feature matrix and y is the target vector. + shadow_dataset : tuple + A tuple containing the shadow training and testing datasets. Each dataset \ + should be a tuple of (X, y), where X is the feature matrix and y is the target vector. + clone_model : bool, optional + If True, the target model architecture will be cloned before training. + seed : int, optional + Random seed for reproducibility. Default is 42. + + References + ---------- + + .. [1] Salem, A. et al. (2018). ML-Leaks: Model and Data Independent \ + Membership Inference Attacks and Defenses on Machine Learning Models. + """ + + def __init__( + self, + target_model: BaseEstimator, + target_dataset: tuple, + shadow_dataset: tuple, + clone_model: bool = False, + seed: int = 42, + ): + if not isinstance(target_model, BaseEstimator): + raise TypeError("target_model must be an instance of BaseEstimator.") + + if not isinstance(target_dataset, tuple) or len(target_dataset) != 2: + raise ValueError("target_dataset must be a tuple with two elements (train and test datasets).") + + if not isinstance(shadow_dataset, tuple) or len(shadow_dataset) != 2: + raise ValueError("shadow_dataset must be a tuple with two elements (train and test datasets).") + + classes_tgt = np.unique(target_dataset[0][1]) + classes_sdw = np.unique(shadow_dataset[0][1]) + if max(len(classes_tgt), len(classes_sdw)) > 2: + self.task = "multiclass" + else: + self.task = "binary" + self.target_model = target_model + self.target_dataset = target_dataset + self.shadow_dataset = shadow_dataset + # set random seed for reproducibility + np.random.seed(seed=seed) + self.clone_model = clone_model + + def generate_attack_dataset(self) -> tuple: + """ + Trains the shadow model and generates the membership inference dataset to train the attacker model. + + Returns + ------- + tuple + A tuple containing two elements: + - (X_mia_train, y_mia_train): Training dataset for the attacker model. + - (X_mia_test, y_mia_test): Testing dataset for the attacker model. + """ + target_train, target_test = self.target_dataset + X_target_train, _ = target_train + X_target_test, _ = target_test + logger.info("Training shadow model...") + if self.clone_model: + X_mia_train, y_mia_train = self._train_shadow_models() + else: + X_mia_train, y_mia_train = self._train_shadow_model() + target_train_preds, target_test_preds = self._get_probs(self.target_model, X_target_train, X_target_test) + logger.info("Creating attacker dataset...") + X_mia_test, y_mia_test = self._create_attacker_dataset(target_train_preds, target_test_preds) + self.train_attacker_data, self.test_attacker_data = (X_mia_train, y_mia_train), (X_mia_test, y_mia_test) + return self.train_attacker_data, self.test_attacker_data + + def fit(self): + """ + Trains the attacker model using the generated membership inference dataset. + + This method should be called after `generate_attack_dataset` to train the \ + attacker model on the generated dataset. The attacker model is trained using \ + the training data and evaluated on the testing data. + + Returns + ------- + BaseEstimator + A trained instance of the attacker model. + + Raises + ------- + RuntimeError + If `generate_attack_dataset` has not been called before this method. + """ + if not hasattr(self, "train_attacker_data") or not hasattr(self, "test_attacker_data"): + raise RuntimeError("You must call `generate_attack_dataset` before `fit`.") + X_mia_train, y_mia_train = self.train_attacker_data + return self.train_model(X_mia_train, y_mia_train) + + def _get_probs(self, model: BaseEstimator, X_train: np.ndarray, X_test: np.ndarray) -> tuple: + """ + Computes the predicted probabilities for the training and testing datasets + using the provided model. + + Parameters + ---------- + model : BaseEstimator + A trained model that implements the `predict_proba` method. + X_train : array-like of shape (n_samples_train, n_features) + The training data for which probabilities are to be predicted. + X_test : array-like of shape (n_samples_test, n_features) + The testing data for which probabilities are to be predicted. + + Returns + ------- + tuple of ndarray + A tuple containing: + - y_train_pred : ndarray of shape (n_samples_train, n_classes) + Predicted probabilities for the training data. + - y_test_pred : ndarray of shape (n_samples_test, n_classes) + Predicted probabilities for the testing data. + """ + y_train_pred = model.predict_proba(X_train) + y_test_pred = model.predict_proba(X_test) + return y_train_pred, y_test_pred + + def train_model(self, X_data: np.ndarray, y_data: np.ndarray) -> BaseEstimator: + """ + Trains a clone of the target model using the provided data. + + Parameters + ---------- + X_data : np.ndarray + The input features for training the model. + y_data : np.ndarray + The target labels corresponding to the input features. + + Returns + ------- + BaseEstimator + A trained instance of the target model. + """ + model = clone(self.target_model) + model.fit(X_data, y_data) + return model + + def _train_shadow_model(self) -> tuple: + """ + Trains the shadow model and creates the attacker dataset. + + This method trains a shadow model using the shadow dataset, obtains \ + predictions for both the shadow training and testing data, and then \ + creates a dataset for the attacker model based on these predictions. + + Returns + ------- + tuple + A tuple containing the attacker dataset created from the shadow \ + model's predictions on the shadow training and testing data. + """ + shadow_train, shadow_test = self.shadow_dataset + X_shadow_train, y_shadow_train = shadow_train + X_shadow_test, _ = shadow_test + self.shadow_model = self.train_model(X_shadow_train, y_shadow_train) + shadow_train_preds, shadow_test_preds = self._get_probs(self.shadow_model, X_shadow_train, X_shadow_test) + return self._create_attacker_dataset(shadow_train_preds, shadow_test_preds) + + def _train_shadow_models(self) -> tuple: + """ + Trains multiple shadow models and creates the attacker dataset. + This method trains multiple shadow models (MLP, Random Forest, and Logistic Regression) \ + using the shadow dataset to avoid clone the target model. + + Returns + ------- + tuple + A tuple containing the attacker dataset created from the shadow \ + models' predictions on the shadow training and testing data. + """ + # import mlp classifier + # import random forest classifier + from sklearn.ensemble import RandomForestClassifier + + # import logistic regression + from sklearn.linear_model import LogisticRegression + from sklearn.neural_network import MLPClassifier + + shadow_train, shadow_test = self.shadow_dataset + X_shadow_train, y_shadow_train = shadow_train + X_shadow_test, _ = shadow_test + # train mlp classifier with shadow dataset + mlp_model = MLPClassifier(random_state=42) + mlp_model.fit(X_shadow_train, y_shadow_train) + mlp_train_preds, mlp_test_preds = self._get_probs(mlp_model, X_shadow_train, X_shadow_test) + mlp_train_preds = np.array(mlp_train_preds) + mlp_test_preds = np.array(mlp_test_preds) + + # train random forest classifier with shadow dataset + rf_model = RandomForestClassifier(random_state=42) + rf_model.fit(X_shadow_train, y_shadow_train) + rf_train_preds, rf_test_preds = self._get_probs(rf_model, X_shadow_train, X_shadow_test) + rf_train_preds = np.array(rf_train_preds) + rf_test_preds = np.array(rf_test_preds) + + # train logistic regression with shadow dataset + lr_model = LogisticRegression(random_state=42) + lr_model.fit(X_shadow_train, y_shadow_train) + lr_train_preds, lr_test_preds = self._get_probs(lr_model, X_shadow_train, X_shadow_test) + lr_train_preds = np.array(lr_train_preds) + lr_test_preds = np.array(lr_test_preds) + + training_preds = np.concatenate((mlp_train_preds, rf_train_preds, lr_train_preds), axis=0) + testing_preds = np.concatenate((mlp_test_preds, rf_test_preds, lr_test_preds), axis=0) + + return self._create_attacker_dataset(training_preds, testing_preds) + + def _create_attacker_dataset(self, train_preds: np.ndarray, test_preds: np.ndarray, shuffle: bool = True) -> tuple: + """ + Creates a dataset for training a membership inference attacker model. + + This function combines predictions from the training and test datasets\ + to create features (`X_mia`) and labels (`y_mia`) for the attacker model.\ + Labels are assigned as 1 for training predictions and 0 for test predictions.\ + Optionally, the resulting dataset can be shuffled. + + Parameters + ---------- + train_preds : np.ndarray + Predictions from the training dataset. Shape should match the model's output. + test_preds : np.ndarray + Predictions from the test dataset. Shape should match the model's output. + shuffle : bool, optional + Whether to shuffle the resulting dataset, by default True. + + Returns + ------- + tuple + A tuple containing: + - X_mia (np.ndarray): Combined predictions from training and test datasets. + - y_mia (np.ndarray): Labels indicating membership (1 for training, 0 for test). + + Raises + ------ + NotImplementedError + If the task is "multiclass", as this functionality is not implemented yet. + """ + if self.task == "binary": + X_mia = np.concatenate((train_preds, test_preds), axis=0) + y_mia = np.concatenate((np.ones(len(train_preds)), np.zeros(len(test_preds))), axis=0) + elif self.task == "multiclass": + train_preds = [sorted(s, reverse=True)[0:3] for s in train_preds] + train_preds = np.array(train_preds) + test_preds = [sorted(s, reverse=True)[0:3] for s in test_preds] + test_preds = np.array(test_preds) + X_mia = np.concatenate((train_preds, test_preds), axis=0) + y_mia = np.concatenate((np.ones(len(train_preds)), np.zeros(len(test_preds))), axis=0) + if shuffle: + indices = np.arange(len(X_mia)) + np.random.shuffle(indices) + X_mia = X_mia[indices] + y_mia = y_mia[indices] + return X_mia, y_mia diff --git a/tests/security/test_ml_leaks.py b/tests/security/test_ml_leaks.py new file mode 100644 index 00000000..2436e105 --- /dev/null +++ b/tests/security/test_ml_leaks.py @@ -0,0 +1,102 @@ +import pytest +import numpy as np +from sklearn.base import BaseEstimator +from holisticai.security.attackers import MLleaks +from sklearn.ensemble import RandomForestClassifier + +from holisticai.datasets import load_dataset + + +SHARD_SIZE=100 + +@pytest.fixture +def tgt_shw_dataset(): + dataset = load_dataset("adult", protected_attribute='sex') # 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) + target = train_test['train'].train_test_split(test_size=0.5, random_state=0) + shadow = train_test['test'].train_test_split(test_size=0.5, random_state=0) + X_target_train = target['train']['X'] + y_target_train = target['train']['y'] + X_target_test = target['test']['X'] + y_target_test = target['test']['y'] + + X_shadow_train = shadow['train']['X'] + y_shadow_train = shadow['train']['y'] + X_shadow_test = shadow['test']['X'] + y_shadow_test = shadow['test']['y'] + + tgt_dataset = ((X_target_train, y_target_train), (X_target_test, y_target_test)) + sdw_dataset = ((X_shadow_train, y_shadow_train), (X_shadow_test, y_shadow_test)) + return tgt_dataset, sdw_dataset + + + +class MockModel(BaseEstimator): + def fit(self, X, y): + pass + + def predict_proba(self, X): + return np.random.rand(len(X), 2) + + +@pytest.fixture +def ml_leaks_instance(tgt_shw_dataset): + target_dataset, shadow_dataset = tgt_shw_dataset + X_target_train, y_target_train = target_dataset[0] + target_model = RandomForestClassifier(random_state=42) + target_model.fit(X_target_train, y_target_train) + return MLleaks(target_model, target_dataset, shadow_dataset) + + +@pytest.fixture +def ml_leaks_no_clone_instance(tgt_shw_dataset): + target_dataset, shadow_dataset = tgt_shw_dataset + X_target_train, y_target_train = target_dataset[0] + target_model = RandomForestClassifier(random_state=42) + target_model.fit(X_target_train, y_target_train) + return MLleaks(target_model, target_dataset, shadow_dataset, clone_model=True) + + +def test_generate_attack_dataset(ml_leaks_instance): + train_data, test_data = ml_leaks_instance.generate_attack_dataset() + assert len(train_data) == 2 + assert len(test_data) == 2 + assert train_data[0].shape[0] == train_data[1].shape[0] + assert test_data[0].shape[0] == test_data[1].shape[0] + + +def test_fit(ml_leaks_instance): + ml_leaks_instance.generate_attack_dataset() + attacker_model = ml_leaks_instance.fit() + assert attacker_model is not None + + +def test_get_probs(ml_leaks_instance, tgt_shw_dataset): + target_dataset, _ = tgt_shw_dataset + model = ml_leaks_instance.target_model + X_train, _ = target_dataset[0] + X_test, _ = target_dataset[1] + train_probs, test_probs = ml_leaks_instance._get_probs(model, X_train, X_test) + assert train_probs.shape[0] == X_train.shape[0] + assert test_probs.shape[0] == X_test.shape[0] + + +def test_create_attacker_dataset(ml_leaks_instance): + train_data, test_data = ml_leaks_instance.generate_attack_dataset() + X_mia_train, y_mia_train = train_data + X_mia_test, y_mia_test = test_data + assert X_mia_train.shape[0] == 200 + assert X_mia_test.shape[0] == 200 + assert np.sum(y_mia_train) == 100 # 100 training samples labeled as 1 + assert np.sum(y_mia_test) == 100 # 100 testing samples labeled as 1 + + +def test_create_attacker_dataset_no_clone(ml_leaks_no_clone_instance): + train_data, test_data = ml_leaks_no_clone_instance.generate_attack_dataset() + X_mia_train, y_mia_train = train_data + X_mia_test, y_mia_test = test_data + assert X_mia_train.shape[0] == 600 + assert X_mia_test.shape[0] == 200 + assert np.sum(y_mia_train) == 300 # 100 training samples labeled as 1 + assert np.sum(y_mia_test) == 100 # 100 testing samples labeled as 1 diff --git a/tutorials/security/membership_inference_attack/ml_leaks.ipynb b/tutorials/security/membership_inference_attack/ml_leaks.ipynb new file mode 100644 index 00000000..5fb7f19f --- /dev/null +++ b/tutorials/security/membership_inference_attack/ml_leaks.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8f36a4eb", + "metadata": {}, + "source": [ + "# Membership inference attack of a binary classification Model\n", + "\n", + "In this notebook, we will evaluate the security of a binary classification model trained on the Adult dataset. We will use the ML-leaks method to perform membership inference attack (MIA) to train an attacker model and measure the model's security against this attacker using traditional metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e7d2e610", + "metadata": {}, + "outputs": [], + "source": [ + "from holisticai.security.attackers import MLleaks\n", + "from holisticai.datasets import load_dataset\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.metrics import accuracy_score\n", + "from sklearn import metrics" + ] + }, + { + "cell_type": "markdown", + "id": "a7148d07", + "metadata": {}, + "source": [ + "## Loading the dataset\n", + "\n", + "We will use the Adult dataset, which contains census data. The target variable is whether a person's income exceeds $50K/year, and the protected attribute we will consider is 'sex'. For time constraints, we will only use a small subset of the data for testing the model and use the attackers.\n", + "\n", + "Following the MIA pipeline of this method, we will assume that the attacker has a dataset that comes from the same distribution as the training dataset. This dataset will be used to train a shadow model that will be used to mimic the target model. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5a5c0dae", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = load_dataset('adult', protected_attribute='sex', preprocessed=True)\n", + "train_test = dataset.train_test_split(test_size=0.5, random_state=42)\n", + "target = train_test['train'].train_test_split(test_size=0.5, random_state=42)\n", + "shadow = train_test['test'].train_test_split(test_size=0.5, random_state=42)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b16eb921", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training set size: 11305\n", + "Test set size: 11306\n", + "Shadow Training set size: 11305\n", + "Shadow Test set size: 11306\n" + ] + } + ], + "source": [ + "X_target_train = target['train']['X']\n", + "y_target_train = target['train']['y']\n", + "X_target_test = target['test']['X']\n", + "y_target_test = target['test']['y']\n", + "\n", + "X_shadow_train = shadow['train']['X']\n", + "y_shadow_train = shadow['train']['y']\n", + "X_shadow_test = shadow['test']['X']\n", + "y_shadow_test = shadow['test']['y']\n", + "\n", + "print(\"Training set size:\", X_target_train.shape[0])\n", + "print(\"Test set size:\", X_target_test.shape[0])\n", + "\n", + "print(\"Shadow Training set size:\", X_shadow_train.shape[0])\n", + "print(\"Shadow Test set size:\", X_shadow_test.shape[0])" + ] + }, + { + "cell_type": "markdown", + "id": "def203ae", + "metadata": {}, + "source": [ + "## Training the target model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "194cf6d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target Model Performance:\n", + "Accuracy: 0.8439766495666018\n" + ] + } + ], + "source": [ + "target_model = RandomForestClassifier(random_state=42)\n", + "target_model.fit(X_target_train, y_target_train)\n", + "y_target_pred = target_model.predict(X_target_test)\n", + "print(\"Target Model Performance:\")\n", + "print(\"Accuracy:\", accuracy_score(y_target_test, y_target_pred))" + ] + }, + { + "cell_type": "markdown", + "id": "2a4c09d1", + "metadata": {}, + "source": [ + "## Setting up the membership inference attack\n", + "\n", + "We will use the ML-leaks method to perform membership inference attack (MIA) presented by Salem et al. in 2018. The method relaxes the assumption that the attacker uses multiple shadow models to train the attacker model. Instead, it uses a single shadow model to get the confidence scores, generate and attacker dataset and use it to train the attacker model." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1744b648", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Task: binary\n" + ] + } + ], + "source": [ + "tgt_dataset = ((X_target_train, y_target_train), (X_target_test, y_target_test))\n", + "sdw_dataset = ((X_shadow_train, y_shadow_train), (X_shadow_test, y_shadow_test))\n", + "\n", + "mia_attacker = MLleaks(target_model, tgt_dataset, sdw_dataset)\n", + "train_attacker_data, test_attacker_data = mia_attacker.generate_attack_dataset()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e423c0b6", + "metadata": {}, + "outputs": [], + "source": [ + "X_mia_train, y_mia_train = train_attacker_data\n", + "X_mia_test, y_mia_test = test_attacker_data" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9e0d0a24", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attacker Model Performance:\n", + "Accuracy: 0.6052363893680067\n" + ] + } + ], + "source": [ + "# Train the Attacker Model\n", + "attacker_model = mia_attacker.fit()\n", + "y_mia_test_pred = attacker_model.predict(X_mia_test)\n", + "print(\"Attacker Model Performance:\")\n", + "print(\"Accuracy:\", accuracy_score(y_mia_test, y_mia_test_pred))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bcb13897", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attacker Precision: 0.5752705182560274\n", + "Attacker Recall: 0.8041574524546661\n", + "Attacker F1 Score: 0.6707245093699277\n" + ] + } + ], + "source": [ + "precision = metrics.precision_score(y_mia_test, y_mia_test_pred)\n", + "recall = metrics.recall_score(y_mia_test, y_mia_test_pred)\n", + "f1 = metrics.f1_score(y_mia_test, y_mia_test_pred)\n", + "print(\"Attacker Precision:\", precision)\n", + "print(\"Attacker Recall:\", recall)\n", + "print(\"Attacker F1 Score:\", f1)" + ] + }, + { + "cell_type": "markdown", + "id": "a63fd318", + "metadata": {}, + "source": [ + "From these results, we can see that the attacker model achieves a precision of 0.575 and a recall of 0.805. The F1 score is 0.67, which indicates that the attacker model is able to identify the members of the target model with a reasonable accuracy. " + ] + } + ], + "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": 5 +}