{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Using ML anonymization to defend against membership inference attacks" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this tutorial we will show how to anonymize models using the ML anonymization module. \n", "\n", "We will demonstrate running inference attacks both on a vanilla model, and then on an anonymized version of the model. We will run a black-box membership inference attack using ART's inference module (https://github.com/Trusted-AI/adversarial-robustness-toolbox/tree/main/art/attacks/inference). \n", "\n", "This will be demonstarted using the Adult dataset (original dataset can be found here: https://archive.ics.uci.edu/ml/datasets/adult). \n", "\n", "For simplicity, we used only the numerical features in the dataset." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load data" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/abigailt/Library/Python/3.9/lib/python/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", " from .autonotebook import tqdm as notebook_tqdm\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[[ 39 13 2174 0 40]\n", " [ 50 13 0 0 13]\n", " [ 38 9 0 0 40]\n", " ...\n", " [ 27 13 0 0 40]\n", " [ 26 11 0 0 48]\n", " [ 27 9 0 0 40]]\n" ] } ], "source": [ "import numpy as np\n", "\n", "import os\n", "import sys\n", "sys.path.insert(0, os.path.abspath('..'))\n", "from apt.utils.dataset_utils import get_adult_dataset_pd\n", "\n", "# requires a folder called 'datasets' in the current directory\n", "(x_train, y_train), (x_test, y_test) = get_adult_dataset_pd()\n", "x_train = x_train.to_numpy()\n", "y_train = y_train.to_numpy().astype(int)\n", "x_test = x_test.to_numpy()\n", "y_test = y_test.to_numpy().astype(int)\n", "\n", "# Use only numeric features (age, education-num, capital-gain, capital-loss, hours-per-week)\n", "x_train = x_train[:, [0, 2, 8, 9, 10]].astype(int)\n", "x_test = x_test[:, [0, 2, 8, 9, 10]].astype(int)\n", "\n", "# get balanced dataset\n", "x_train = x_train[:x_test.shape[0]]\n", "y_train = y_train[:y_test.shape[0]]\n", "\n", "print(x_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Train decision tree model" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Base model accuracy: 0.8087341072415699\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/Users/abigailt/Library/Python/3.9/lib/python/site-packages/sklearn/utils/deprecation.py:103: FutureWarning: The attribute `n_features_` is deprecated in 1.0 and will be removed in 1.2. Use `n_features_in_` instead.\n", " warnings.warn(msg, category=FutureWarning)\n" ] } ], "source": [ "from sklearn.tree import DecisionTreeClassifier\n", "from art.estimators.classification.scikitlearn import ScikitlearnDecisionTreeClassifier\n", "\n", "model = DecisionTreeClassifier()\n", "model.fit(x_train, y_train)\n", "\n", "art_classifier = ScikitlearnDecisionTreeClassifier(model)\n", "\n", "print('Base model accuracy: ', model.score(x_test, y_test))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Attack\n", "The black-box attack basically trains an additional classifier (called the attack model) to predict the membership status of a sample.\n", "#### Train attack model" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from art.attacks.inference.membership_inference import MembershipInferenceBlackBox\n", "\n", "# attack_model_type can be nn (neural network), rf (randon forest) or gb (gradient boosting)\n", "bb_attack = MembershipInferenceBlackBox(art_classifier, attack_model_type='rf')\n", "\n", "# use half of each dataset for training the attack\n", "attack_train_ratio = 0.5\n", "attack_train_size = int(len(x_train) * attack_train_ratio)\n", "attack_test_size = int(len(x_test) * attack_train_ratio)\n", "\n", "# train attack model\n", "bb_attack.fit(x_train[:attack_train_size], y_train[:attack_train_size],\n", " x_test[:attack_test_size], y_test[:attack_test_size])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Infer sensitive feature and check accuracy" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5434836015231544\n" ] } ], "source": [ "# get inferred values for remaining half\n", "inferred_train_bb = bb_attack.infer(x_train[attack_train_size:], y_train[attack_train_size:])\n", "inferred_test_bb = bb_attack.infer(x_test[attack_test_size:], y_test[attack_test_size:])\n", "# check accuracy\n", "train_acc = np.sum(inferred_train_bb) / len(inferred_train_bb)\n", "test_acc = 1 - (np.sum(inferred_test_bb) / len(inferred_test_bb))\n", "acc = (train_acc * len(inferred_train_bb) + test_acc * len(inferred_test_bb)) / (len(inferred_train_bb) + len(inferred_test_bb))\n", "print(acc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This means that for 54% of the data, membership is inferred correctly using this attack." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Anonymized data\n", "## k=100\n", "\n", "Now we will apply the same attacks on an anonymized version of the same dataset (k=100). The data is anonymized on the quasi-identifiers: age, education-num, capital-gain, hours-per-week.\n", "\n", "k=100 means that each record in the anonymized dataset is identical to 99 others on the quasi-identifier values (i.e., when looking only at those features, the records are indistinguishable)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[38 13 0 0 40]\n", " [46 13 0 0 35]\n", " [28 9 0 0 40]\n", " ...\n", " [26 13 0 0 40]\n", " [27 10 0 0 50]\n", " [28 9 0 0 40]]\n" ] } ], "source": [ "from apt.utils.datasets import ArrayDataset\n", "from apt.anonymization import Anonymize\n", "\n", "x_train_predictions = np.array([np.argmax(arr) for arr in art_classifier.predict(x_train)])\n", "\n", "# QI = (age, education-num, capital-gain, hours-per-week)\n", "QI = [0, 1, 2, 4]\n", "anonymizer = Anonymize(100, QI)\n", "anon = anonymizer.anonymize(ArrayDataset(x_train, x_train_predictions))\n", "print(anon)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "6739" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# number of distinct rows in original data\n", "len(np.unique(x_train, axis=0))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "401" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# number of distinct rows in anonymized data\n", "len(np.unique(anon, axis=0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Train decision tree model" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Anonymized model accuracy: 0.8308457711442786\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/Users/abigailt/Library/Python/3.9/lib/python/site-packages/sklearn/utils/deprecation.py:103: FutureWarning: The attribute `n_features_` is deprecated in 1.0 and will be removed in 1.2. Use `n_features_in_` instead.\n", " warnings.warn(msg, category=FutureWarning)\n" ] } ], "source": [ "anon_model = DecisionTreeClassifier()\n", "anon_model.fit(anon, y_train)\n", "\n", "anon_art_classifier = ScikitlearnDecisionTreeClassifier(anon_model)\n", "\n", "print('Anonymized model accuracy: ', anon_model.score(x_test, y_test))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Attack\n", "### Black-box attack" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.4944724235351923\n" ] } ], "source": [ "anon_bb_attack = MembershipInferenceBlackBox(anon_art_classifier, attack_model_type='rf')\n", "\n", "# train attack model\n", "anon_bb_attack.fit(x_train[:attack_train_size], y_train[:attack_train_size],\n", " x_test[:attack_test_size], y_test[:attack_test_size])\n", "\n", "# get inferred values\n", "anon_inferred_train_bb = anon_bb_attack.infer(x_train[attack_train_size:], y_train[attack_train_size:])\n", "anon_inferred_test_bb = anon_bb_attack.infer(x_test[attack_test_size:], y_test[attack_test_size:])\n", "# check accuracy\n", "anon_train_acc = np.sum(anon_inferred_train_bb) / len(anon_inferred_train_bb)\n", "anon_test_acc = 1 - (np.sum(anon_inferred_test_bb) / len(anon_inferred_test_bb))\n", "anon_acc = (anon_train_acc * len(anon_inferred_train_bb) + anon_test_acc * len(anon_inferred_test_bb)) / (len(anon_inferred_train_bb) + len(anon_inferred_test_bb))\n", "print(anon_acc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Attack accuracy is reduced to 50% (eqiuvalent to random guessing)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "without anonymization: (0.5303914835164835, 0.7588748311018303)\n", "with anonymization: (0.49255952380952384, 0.3659255619702739)\n" ] } ], "source": [ "def calc_precision_recall(predicted, actual, positive_value=1):\n", " score = 0 # both predicted and actual are positive\n", " num_positive_predicted = 0 # predicted positive\n", " num_positive_actual = 0 # actual positive\n", " for i in range(len(predicted)):\n", " if predicted[i] == positive_value:\n", " num_positive_predicted += 1\n", " if actual[i] == positive_value:\n", " num_positive_actual += 1\n", " if predicted[i] == actual[i]:\n", " if predicted[i] == positive_value:\n", " score += 1\n", " \n", " if num_positive_predicted == 0:\n", " precision = 1\n", " else:\n", " precision = score / num_positive_predicted # the fraction of predicted “Yes” responses that are correct\n", " if num_positive_actual == 0:\n", " recall = 1\n", " else:\n", " recall = score / num_positive_actual # the fraction of “Yes” responses that are predicted correctly\n", "\n", " return precision, recall\n", "\n", "# regular\n", "print('without anonymization:', calc_precision_recall(np.concatenate((inferred_train_bb, inferred_test_bb)), \n", " np.concatenate((np.ones(len(inferred_train_bb)), np.zeros(len(inferred_test_bb))))))\n", "# anon\n", "print('with anonymization:', calc_precision_recall(np.concatenate((anon_inferred_train_bb, anon_inferred_test_bb)), \n", " np.concatenate((np.ones(len(anon_inferred_train_bb)), np.zeros(len(anon_inferred_test_bb))))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Precision and recall are also reduced." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.9.6" } }, "nbformat": 4, "nbformat_minor": 2 }