From a9b4a400f46ffb331de2fac638ef9ce38831a900 Mon Sep 17 00:00:00 2001 From: franklincf Date: Fri, 25 Apr 2025 19:42:07 -0300 Subject: [PATCH 1/9] feature: adding source code and tutorial for MIA attacker ml-leaks --- src/holisticai/security/attackers/__init__.py | 3 + .../membership_inference/__init__.py | 0 .../membership_inference/ml_leaks.py | 184 ++++++++++++++++++ .../ml_leaks.ipynb | 182 +++++++++++++++++ 4 files changed, 369 insertions(+) create mode 100644 src/holisticai/security/attackers/__init__.py create mode 100644 src/holisticai/security/attackers/membership_inference/__init__.py create mode 100644 src/holisticai/security/attackers/membership_inference/ml_leaks.py create mode 100644 tutorials/security/membership_inference_attack/ml_leaks.ipynb 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..dd41ef17 --- /dev/null +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -0,0 +1,184 @@ +import numpy as np +from sklearn.base import BaseEstimator, clone + + +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. + task : str, optional + The type of task to perform. It can be either "binary" or "multiclass". \ + Default is "binary". + + 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, task: str = "binary"): + 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).") + + if task not in ["binary", "multiclass"]: + raise ValueError("task must be either 'binary' or 'multiclass'.") + + self.task = task + self.target_model = target_model + self.target_dataset = target_dataset + self.shadow_dataset = shadow_dataset + + def fit(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 + 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) + X_mia_test, y_mia_test = self._create_attacker_dataset(target_train_preds, target_test_preds) + return (X_mia_train, y_mia_train), (X_mia_test, y_mia_test) + + 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 _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": + raise NotImplementedError("Multiclass MIA is not implemented yet.") + 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/tutorials/security/membership_inference_attack/ml_leaks.ipynb b/tutorials/security/membership_inference_attack/ml_leaks.ipynb new file mode 100644 index 00000000..c79e898e --- /dev/null +++ b/tutorials/security/membership_inference_attack/ml_leaks.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "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": "code", + "execution_count": 3, + "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": 4, + "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": "code", + "execution_count": 5, + "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": "code", + "execution_count": 6, + "id": "1744b648", + "metadata": {}, + "outputs": [], + "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)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "97ebb163", + "metadata": {}, + "outputs": [], + "source": [ + "train_attacker_data, test_attacker_data = mia_attacker.fit()\n", + "\n", + "X_mia_train, y_mia_train = train_attacker_data\n", + "X_mia_test, y_mia_test = test_attacker_data" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9e0d0a24", + "metadata": {}, + "outputs": [], + "source": [ + "# Train the Attacker Model\n", + "attacker_model = mia_attacker.train_model(X_mia_train, y_mia_train)\n", + "y_mia_train_pred = attacker_model.predict(X_mia_train)\n", + "y_mia_test_pred = attacker_model.predict(X_mia_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bcb13897", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train Precision: 0.5735756082537727\n", + "Train Recall: 0.8237063246351172\n", + "Train F1 Score: 0.6762527233115468\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(\"Train Precision:\", precision)\n", + "print(\"Train Recall:\", recall)\n", + "print(\"Train F1 Score:\", f1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48c31878", + "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": 5 +} From edeacb56d542a6e2e3b4fe4c7a800d56b7340e75 Mon Sep 17 00:00:00 2001 From: franklincf Date: Mon, 28 Apr 2025 08:55:24 -0300 Subject: [PATCH 2/9] refactor: updating codes for better understanding --- .../membership_inference/ml_leaks.py | 47 ++++++++++++++- .../ml_leaks.ipynb | 58 ++++++++++++------- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/holisticai/security/attackers/membership_inference/ml_leaks.py b/src/holisticai/security/attackers/membership_inference/ml_leaks.py index dd41ef17..9ab003a3 100644 --- a/src/holisticai/security/attackers/membership_inference/ml_leaks.py +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -28,6 +28,8 @@ class MLleaks: task : str, optional The type of task to perform. It can be either "binary" or "multiclass". \ Default is "binary". + seed : int, optional + Random seed for reproducibility. Default is 42. References ---------- @@ -36,7 +38,14 @@ class MLleaks: Membership Inference Attacks and Defenses on Machine Learning Models. """ - def __init__(self, target_model: BaseEstimator, target_dataset: tuple, shadow_dataset: tuple, task: str = "binary"): + def __init__( + self, + target_model: BaseEstimator, + target_dataset: tuple, + shadow_dataset: tuple, + task: str = "binary", + seed: int = 42, + ): if not isinstance(target_model, BaseEstimator): raise TypeError("target_model must be an instance of BaseEstimator.") @@ -53,8 +62,10 @@ def __init__(self, target_model: BaseEstimator, target_dataset: tuple, shadow_da 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) - def fit(self) -> tuple: + def generate_attack_dataset(self) -> tuple: """ Trains the shadow model and generates the membership inference dataset to train the attacker model. @@ -68,10 +79,40 @@ def fit(self) -> tuple: target_train, target_test = self.target_dataset X_target_train, _ = target_train X_target_test, _ = target_test + print("Training shadow model...") 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) + print("Creating attacker dataset...") X_mia_test, y_mia_test = self._create_attacker_dataset(target_train_preds, target_test_preds) - return (X_mia_train, y_mia_train), (X_mia_test, y_mia_test) + 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 + X_mia_test, y_mia_test = self.test_attacker_data + # Here you would typically train your attacker model using X_mia_train and y_mia_train + # and evaluate it using X_mia_test and y_mia_test. + # For now, we just return the datasets. + 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: """ diff --git a/tutorials/security/membership_inference_attack/ml_leaks.ipynb b/tutorials/security/membership_inference_attack/ml_leaks.ipynb index c79e898e..fcd761c6 100644 --- a/tutorials/security/membership_inference_attack/ml_leaks.ipynb +++ b/tutorials/security/membership_inference_attack/ml_leaks.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "e7d2e610", "metadata": {}, "outputs": [], @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "5a5c0dae", "metadata": {}, "outputs": [], @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "b16eb921", "metadata": {}, "outputs": [ @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "194cf6d2", "metadata": {}, "outputs": [ @@ -87,46 +87,64 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "1744b648", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training shadow model...\n", + "Creating attacker dataset...\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)" + "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": 7, - "id": "97ebb163", + "execution_count": 6, + "id": "e423c0b6", "metadata": {}, "outputs": [], "source": [ - "train_attacker_data, test_attacker_data = mia_attacker.fit()\n", - "\n", "X_mia_train, y_mia_train = train_attacker_data\n", "X_mia_test, y_mia_test = test_attacker_data" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "9e0d0a24", "metadata": {}, - "outputs": [], + "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.train_model(X_mia_train, y_mia_train)\n", - "y_mia_train_pred = attacker_model.predict(X_mia_train)\n", - "y_mia_test_pred = attacker_model.predict(X_mia_test)" + "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": 9, + "execution_count": 8, "id": "bcb13897", "metadata": {}, "outputs": [ @@ -134,9 +152,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Train Precision: 0.5735756082537727\n", - "Train Recall: 0.8237063246351172\n", - "Train F1 Score: 0.6762527233115468\n" + "Train Precision: 0.5752705182560274\n", + "Train Recall: 0.8041574524546661\n", + "Train F1 Score: 0.6707245093699277\n" ] } ], From 24bd614fc783f4d73f1d572d8a8dc5cde444f263 Mon Sep 17 00:00:00 2001 From: franklincf Date: Mon, 28 Apr 2025 16:52:10 -0300 Subject: [PATCH 3/9] refactor: updating codes --- .../membership_inference/ml_leaks.py | 11 +-- .../ml_leaks.ipynb | 73 ++++++++++++++----- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/holisticai/security/attackers/membership_inference/ml_leaks.py b/src/holisticai/security/attackers/membership_inference/ml_leaks.py index 9ab003a3..2b2175a9 100644 --- a/src/holisticai/security/attackers/membership_inference/ml_leaks.py +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -1,6 +1,10 @@ +import logging + import numpy as np from sklearn.base import BaseEstimator, clone +logger = logging.getLogger(__name__) + class MLleaks: """ @@ -79,10 +83,10 @@ def generate_attack_dataset(self) -> tuple: target_train, target_test = self.target_dataset X_target_train, _ = target_train X_target_test, _ = target_test - print("Training shadow model...") + logger.info("Training shadow model...") 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) - print("Creating attacker dataset...") + 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 @@ -109,9 +113,6 @@ def fit(self): raise RuntimeError("You must call `generate_attack_dataset` before `fit`.") X_mia_train, y_mia_train = self.train_attacker_data X_mia_test, y_mia_test = self.test_attacker_data - # Here you would typically train your attacker model using X_mia_train and y_mia_train - # and evaluate it using X_mia_test and y_mia_test. - # For now, we just return the datasets. 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: diff --git a/tutorials/security/membership_inference_attack/ml_leaks.ipynb b/tutorials/security/membership_inference_attack/ml_leaks.ipynb index fcd761c6..dad3f9c2 100644 --- a/tutorials/security/membership_inference_attack/ml_leaks.ipynb +++ b/tutorials/security/membership_inference_attack/ml_leaks.ipynb @@ -1,5 +1,15 @@ { "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, @@ -14,6 +24,18 @@ "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, @@ -62,6 +84,14 @@ "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, @@ -85,21 +115,22 @@ "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": [ - "Training shadow model...\n", - "Creating attacker dataset...\n" - ] - } - ], + "outputs": [], "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", @@ -144,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "bcb13897", "metadata": {}, "outputs": [ @@ -152,9 +183,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Train Precision: 0.5752705182560274\n", - "Train Recall: 0.8041574524546661\n", - "Train F1 Score: 0.6707245093699277\n" + "Attacker Precision: 0.5752705182560274\n", + "Attacker Recall: 0.8041574524546661\n", + "Attacker F1 Score: 0.6707245093699277\n" ] } ], @@ -162,9 +193,17 @@ "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(\"Train Precision:\", precision)\n", - "print(\"Train Recall:\", recall)\n", - "print(\"Train F1 Score:\", f1)" + "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. " ] }, { From 47abf7a7d9d5ed178e80f11d0a3ad45096ddf063 Mon Sep 17 00:00:00 2001 From: franklincf Date: Mon, 28 Apr 2025 18:31:03 -0300 Subject: [PATCH 4/9] tests: adding tests for mia ml-leaks module --- tests/security/test_ml_leaks.py | 83 +++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/security/test_ml_leaks.py diff --git a/tests/security/test_ml_leaks.py b/tests/security/test_ml_leaks.py new file mode 100644 index 00000000..a4f73d6e --- /dev/null +++ b/tests/security/test_ml_leaks.py @@ -0,0 +1,83 @@ +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) + + +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 From dc9984db856564d48c1d9a185c40ad6f020d4b0a Mon Sep 17 00:00:00 2001 From: franklincf Date: Mon, 28 Apr 2025 18:33:49 -0300 Subject: [PATCH 5/9] docs: adding documentation for mia ml-leaks module --- docs/source/gallery/tutorials/security.rst | 4 + .../security/attackers/ml_leaks.ipynb | 239 ++++++++++++++++++ docs/source/reference/index.rst | 2 + docs/source/reference/security/attackers.rst | 15 ++ 4 files changed, 260 insertions(+) create mode 100644 docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb create mode 100644 docs/source/reference/security/attackers.rst 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..dad3f9c2 --- /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": [], + "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": 9, + "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. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48c31878", + "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": 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 From 7aea68a38ca6d5c1db47526c2ad86cd19047b152 Mon Sep 17 00:00:00 2001 From: franklincf Date: Tue, 29 Apr 2025 20:49:10 -0300 Subject: [PATCH 6/9] feat: adding multiclass support for mia ml-leaks attacker --- .../membership_inference/ml_leaks.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/holisticai/security/attackers/membership_inference/ml_leaks.py b/src/holisticai/security/attackers/membership_inference/ml_leaks.py index 2b2175a9..6c61773f 100644 --- a/src/holisticai/security/attackers/membership_inference/ml_leaks.py +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -29,9 +29,6 @@ class MLleaks: 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. - task : str, optional - The type of task to perform. It can be either "binary" or "multiclass". \ - Default is "binary". seed : int, optional Random seed for reproducibility. Default is 42. @@ -47,7 +44,6 @@ def __init__( target_model: BaseEstimator, target_dataset: tuple, shadow_dataset: tuple, - task: str = "binary", seed: int = 42, ): if not isinstance(target_model, BaseEstimator): @@ -59,10 +55,12 @@ def __init__( 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).") - if task not in ["binary", "multiclass"]: - raise ValueError("task must be either 'binary' or 'multiclass'.") - - self.task = task + 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 @@ -112,7 +110,6 @@ def fit(self): 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 - X_mia_test, y_mia_test = self.test_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: @@ -217,7 +214,12 @@ def _create_attacker_dataset(self, train_preds: np.ndarray, test_preds: np.ndarr 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": - raise NotImplementedError("Multiclass MIA is not implemented yet.") + 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) From 6f47d4499c2c0e7463560b0d804afdfcbf5da000 Mon Sep 17 00:00:00 2001 From: franklincf Date: Wed, 30 Apr 2025 20:23:59 -0300 Subject: [PATCH 7/9] chore: updating tutorials --- .../security/attackers/ml_leaks.ipynb | 20 +++++++++---------- .../ml_leaks.ipynb | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb b/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb index dad3f9c2..5fb7f19f 100644 --- a/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb +++ b/docs/source/gallery/tutorials/security/attackers/ml_leaks.ipynb @@ -130,7 +130,15 @@ "execution_count": 5, "id": "1744b648", "metadata": {}, - "outputs": [], + "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", @@ -175,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "bcb13897", "metadata": {}, "outputs": [ @@ -205,14 +213,6 @@ "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. " ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48c31878", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/tutorials/security/membership_inference_attack/ml_leaks.ipynb b/tutorials/security/membership_inference_attack/ml_leaks.ipynb index dad3f9c2..5fb7f19f 100644 --- a/tutorials/security/membership_inference_attack/ml_leaks.ipynb +++ b/tutorials/security/membership_inference_attack/ml_leaks.ipynb @@ -130,7 +130,15 @@ "execution_count": 5, "id": "1744b648", "metadata": {}, - "outputs": [], + "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", @@ -175,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "bcb13897", "metadata": {}, "outputs": [ @@ -205,14 +213,6 @@ "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. " ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48c31878", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From fe69df3e0f1549c8d79f98c2ed4609b725a0a39c Mon Sep 17 00:00:00 2001 From: franklincf Date: Thu, 8 May 2025 18:28:36 -0300 Subject: [PATCH 8/9] feat: adding support for shadow models when no target architecture is known --- .../membership_inference/ml_leaks.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/holisticai/security/attackers/membership_inference/ml_leaks.py b/src/holisticai/security/attackers/membership_inference/ml_leaks.py index 6c61773f..a6dc6b1f 100644 --- a/src/holisticai/security/attackers/membership_inference/ml_leaks.py +++ b/src/holisticai/security/attackers/membership_inference/ml_leaks.py @@ -29,6 +29,8 @@ class MLleaks: 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. @@ -44,6 +46,7 @@ def __init__( target_model: BaseEstimator, target_dataset: tuple, shadow_dataset: tuple, + clone_model: bool = False, seed: int = 42, ): if not isinstance(target_model, BaseEstimator): @@ -66,6 +69,7 @@ def __init__( 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: """ @@ -82,7 +86,10 @@ def generate_attack_dataset(self) -> tuple: X_target_train, _ = target_train X_target_test, _ = target_test logger.info("Training shadow model...") - X_mia_train, y_mia_train = self._train_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) @@ -180,6 +187,55 @@ def _train_shadow_model(self) -> tuple: 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. From 7ad874dac495db73efe156d9c2b8c1c4f5b941c9 Mon Sep 17 00:00:00 2001 From: franklincf Date: Thu, 8 May 2025 18:40:28 -0300 Subject: [PATCH 9/9] tests: updating tests for non clone shadow model --- tests/security/test_ml_leaks.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/security/test_ml_leaks.py b/tests/security/test_ml_leaks.py index a4f73d6e..2436e105 100644 --- a/tests/security/test_ml_leaks.py +++ b/tests/security/test_ml_leaks.py @@ -49,6 +49,15 @@ def ml_leaks_instance(tgt_shw_dataset): 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 @@ -81,3 +90,13 @@ def test_create_attacker_dataset(ml_leaks_instance): 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