From 225355258ca311fc4975ce86f58a588f0992a09a Mon Sep 17 00:00:00 2001 From: Shuang Song Date: Tue, 25 Jul 2023 14:49:21 -0700 Subject: [PATCH] Calls epsilon computation in MIA. PiperOrigin-RevId: 551003589 --- .../membership_inference_attack/BUILD | 12 +- .../data_structures.py | 138 +++++++-- .../data_structures_test.py | 266 ++++++++++++++++-- .../membership_inference_attack.py | 43 ++- .../membership_inference_attack_test.py | 128 +++++++++ .../privacy_report_test.py | 27 +- 6 files changed, 568 insertions(+), 46 deletions(-) diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/BUILD b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/BUILD index ff75a51..ef0d824 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/BUILD +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/BUILD @@ -21,7 +21,10 @@ py_test( srcs = ["membership_inference_attack_test.py"], python_version = "PY3", srcs_version = "PY3", - deps = [":membership_inference_attack"], + deps = [ + ":membership_inference_attack", + "//tensorflow_privacy/privacy/privacy_tests:epsilon_lower_bound", + ], ) py_test( @@ -32,6 +35,7 @@ py_test( srcs_version = "PY3", deps = [ ":membership_inference_attack", + "//tensorflow_privacy/privacy/privacy_tests:epsilon_lower_bound", "//tensorflow_privacy/privacy/privacy_tests:utils", ], ) @@ -62,6 +66,7 @@ py_test( deps = [ ":membership_inference_attack", ":privacy_report", + "//tensorflow_privacy/privacy/privacy_tests:epsilon_lower_bound", ], ) @@ -83,7 +88,10 @@ py_library( "seq2seq_mia.py", ], srcs_version = "PY3", - deps = ["//tensorflow_privacy/privacy/privacy_tests:utils"], + deps = [ + "//tensorflow_privacy/privacy/privacy_tests:epsilon_lower_bound", + "//tensorflow_privacy/privacy/privacy_tests:utils", + ], ) py_library( diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures.py b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures.py index 946446d..f10c49a 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures.py +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures.py @@ -20,12 +20,13 @@ import glob import logging import os import pickle -from typing import Any, Dict, Iterable, MutableSequence, Optional, Sequence, Union +from typing import Any, Dict, Iterable, Mapping, MutableSequence, Optional, Sequence, Union import numpy as np import pandas as pd from scipy import special from sklearn import metrics +from tensorflow_privacy.privacy.privacy_tests import epsilon_lower_bound as elb from tensorflow_privacy.privacy.privacy_tests import utils # The minimum TPR or FPR below which they are considered equal. @@ -33,6 +34,11 @@ _ABSOLUTE_TOLERANCE = 1e-3 ENTIRE_DATASET_SLICE_STR = 'Entire dataset' +# Methods for estimation epsilon lower bounds +EPSILON_METHODS = (elb.BoundMethod.BAILEY,) +EPSILON_ALPHA = 0.05 # Level of significance for estimating epsilon lower bound +EPSILON_K = 5 # Will return top-k values for each epsilon lower bound estimate + class SlicingFeature(enum.Enum): """Enum with features by which slicing is available.""" @@ -189,6 +195,7 @@ class PrivacyMetric(enum.Enum): AUC = 'AUC' ATTACKER_ADVANTAGE = 'Attacker advantage' PPV = 'Positive predictive value' + EPSILON_LOWER_BOUND = 'Epsilon lower bound' def __str__(self): """Returns 'AUC' instead of PrivacyMetric.AUC.""" @@ -738,6 +745,27 @@ class RocCurve: ]) +@dataclasses.dataclass +class EpsilonLowerBoundValue: + """Epsilon lower bounds of a membership inference classifier.""" + + # Bounds from different methods + bounds: Mapping[elb.BoundMethod, np.ndarray] + + def get_max_epsilon_bounds(self) -> np.ndarray: + """Returns the bounds with largest average.""" + bounds_val = [bound for bound in self.bounds.values() if bound.size] + if not bounds_val: + return np.array([]) + best_index = np.argmax([bound.mean() for bound in bounds_val]) + return bounds_val[best_index] + + def __str__(self) -> str: + """Returns string showing bounds with largest average.""" + bounds_string = utils.format_number_list(self.get_max_epsilon_bounds()) + return f'EpsilonLowerBoundValue([{bounds_string}])' + + # (no. of training examples, no. of test examples) for the test. DataSize = collections.namedtuple('DataSize', 'ntrain ntest') @@ -761,6 +789,10 @@ class SingleAttackResult: # ROC curve representing the accuracy of the attacker roc_curve: RocCurve + # Lower bound for DP epsilon, derived from tp and fp. For more details, + # see tensorflow_privacy/privacy/privacy_tests/epsilon_lower_bound.py. + epsilon_lower_bound_value: EpsilonLowerBoundValue + # Membership score is some measure of confidence of this attacker that # a particular sample is a member of the training set. # @@ -794,17 +826,23 @@ class SingleAttackResult: def get_auc(self): return self.roc_curve.get_auc() + def get_epsilon_lower_bound(self): + return self.epsilon_lower_bound_value.get_max_epsilon_bounds() + def __str__(self): - """Returns SliceSpec, AttackType, AUC and advantage metrics.""" + """Returns SliceSpec, AttackType, and various MIA metrics.""" return '\n'.join([ 'SingleAttackResult(', ' SliceSpec: %s' % str(self.slice_spec), - ' DataSize: (ntrain=%d, ntest=%d)' % - (self.data_size.ntrain, self.data_size.ntest), + ' DataSize: (ntrain=%d, ntest=%d)' + % (self.data_size.ntrain, self.data_size.ntest), ' AttackType: %s' % str(self.attack_type), ' AUC: %.2f' % self.get_auc(), ' Attacker advantage: %.2f' % self.get_attacker_advantage(), - ' Positive Predictive Value: %.2f' % self.get_ppv(), ')' + ' Positive Predictive Value: %.2f' % self.get_ppv(), + ' Epsilon lower bound: ' + + utils.format_number_list(self.get_epsilon_lower_bound()), + ')', ]) @@ -906,6 +944,21 @@ class SingleMembershipProbabilityResult: ' thresholding on membership probability achieved an advantage of' ' %.2f' % (roc_curve.get_attacker_advantage()) ) + epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds=elb.EpsilonLowerBound( + pos_scores=self.train_membership_probs, + neg_scores=self.test_membership_probs, + alpha=EPSILON_ALPHA, + two_sided_threshold=True, + ).compute_epsilon_lower_bounds(methods=EPSILON_METHODS, k=EPSILON_K) + ) + summary.append( + f' thresholding on membership probability achieved top-{EPSILON_K}' + + ' epsilon lower bound of ' + + utils.format_number_list( + epsilon_lower_bound_value.get_max_epsilon_bounds() + ) + ) return summary @@ -970,6 +1023,9 @@ class AttackResults: advantages = [] ppvs = [] aucs = [] + # Top EPSILON_K epsilon values for each single attack result. + # epsilon_lower_bounds[i][j] is the top-i epsilon value for attack j. + epsilon_lower_bounds = [[] for _ in range(EPSILON_K)] for attack_result in self.single_attack_results: slice_spec = attack_result.slice_spec @@ -985,17 +1041,30 @@ class AttackResults: advantages.append(float(attack_result.get_attacker_advantage())) ppvs.append(float(attack_result.get_ppv())) aucs.append(float(attack_result.get_auc())) + current_elb = attack_result.get_epsilon_lower_bound() + for i in range(EPSILON_K): + if i < len(current_elb): + epsilon_lower_bounds[i].append(current_elb[i]) + else: # If less than EPSILON_K values, use nan. + epsilon_lower_bounds[i].append(np.nan) - df = pd.DataFrame({ - str(AttackResultsDFColumns.SLICE_FEATURE): slice_features, - str(AttackResultsDFColumns.SLICE_VALUE): slice_values, - str(AttackResultsDFColumns.DATA_SIZE_TRAIN): data_size_train, - str(AttackResultsDFColumns.DATA_SIZE_TEST): data_size_test, - str(AttackResultsDFColumns.ATTACK_TYPE): attack_types, - str(PrivacyMetric.ATTACKER_ADVANTAGE): advantages, - str(PrivacyMetric.PPV): ppvs, - str(PrivacyMetric.AUC): aucs - }) + df = pd.DataFrame( + { + str(AttackResultsDFColumns.SLICE_FEATURE): slice_features, + str(AttackResultsDFColumns.SLICE_VALUE): slice_values, + str(AttackResultsDFColumns.DATA_SIZE_TRAIN): data_size_train, + str(AttackResultsDFColumns.DATA_SIZE_TEST): data_size_test, + str(AttackResultsDFColumns.ATTACK_TYPE): attack_types, + str(PrivacyMetric.ATTACKER_ADVANTAGE): advantages, + str(PrivacyMetric.PPV): ppvs, + str(PrivacyMetric.AUC): aucs, + } + | { + str(PrivacyMetric.EPSILON_LOWER_BOUND) + + f'_{i + 1}': epsilon_lower_bounds[i] + for i in range(len(epsilon_lower_bounds)) + } + ) return df def summary(self, by_slices=False) -> str: @@ -1047,6 +1116,22 @@ class AttackResults: max_ppv_result_all.data_size.ntest, max_ppv_result_all.get_ppv(), max_ppv_result_all.slice_spec)) + max_epsilon_lower_bound_all = self.get_result_with_max_epsilon() + summary.append( + ' %s (with %d training and %d test examples) achieved top-%d epsilon ' + 'lower bounds of %s on slice %s' + % ( + max_epsilon_lower_bound_all.attack_type, + max_epsilon_lower_bound_all.data_size.ntrain, + max_epsilon_lower_bound_all.data_size.ntest, + EPSILON_K, + utils.format_number_list( + max_epsilon_lower_bound_all.get_epsilon_lower_bound() + ), + max_epsilon_lower_bound_all.slice_spec, + ) + ) + slice_dict = self._group_results_by_slice() if by_slices and len(slice_dict.keys()) > 1: @@ -1082,7 +1167,20 @@ class AttackResults: 'predictive value of %.2f' % (max_ppv_result.attack_type, max_ppv_result.data_size.ntrain, max_ppv_result.data_size.ntest, max_ppv_result.get_ppv())) - + max_epsilon_lower_bound_all = results.get_result_with_max_epsilon() + summary.append( + ' %s (with %d training and %d test examples) achieved top-%d ' + 'epsilon lower bounds of %s' + % ( + max_epsilon_lower_bound_all.attack_type, + max_epsilon_lower_bound_all.data_size.ntrain, + max_epsilon_lower_bound_all.data_size.ntest, + EPSILON_K, + utils.format_number_list( + max_epsilon_lower_bound_all.get_epsilon_lower_bound() + ), + ) + ) return '\n'.join(summary) def _group_results_by_slice(self): @@ -1124,6 +1222,14 @@ class AttackResults: return self.single_attack_results[np.argmax( [result.get_ppv() for result in self.single_attack_results])] + def get_result_with_max_epsilon(self) -> SingleAttackResult: + """Gets the result with max averaged epsilon lower bound.""" + avg_epsilon_bounds = [ + result.get_epsilon_lower_bound().mean() + for result in self.single_attack_results + ] + return self.single_attack_results[np.argmax(avg_epsilon_bounds)] + def save(self, filepath): """Saves self to a pickle file.""" with open(filepath, 'wb') as out: diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures_test.py b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures_test.py index 4543d15..d929496 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures_test.py +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/data_structures_test.py @@ -20,13 +20,16 @@ from absl.testing import absltest from absl.testing import parameterized import numpy as np import pandas as pd +from tensorflow_privacy.privacy.privacy_tests import epsilon_lower_bound as elb from tensorflow_privacy.privacy.privacy_tests import utils +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack import data_structures from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import _log_value from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackInputData from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackResults from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackResultsCollection from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackType from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import DataSize +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EpsilonLowerBoundValue from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import PrivacyReportMetadata from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import RocCurve from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import SingleAttackResult @@ -578,9 +581,87 @@ class RocCurveTest(parameterized.TestCase): np.testing.assert_allclose(roc.get_ppv(), 0.5, atol=1e-3) + def test_string(self): + roc = RocCurve( + tpr=np.array([0.0, 0.5, 1.0]), + fpr=np.array([0.0, 0.5, 1.0]), + thresholds=np.array([0, 1, 2]), + test_train_ratio=1.0, + ) + self.assertEqual( + str(roc), + ( + 'RocCurve(\n' + ' AUC: 0.50\n' + ' Attacker advantage: 0.00\n' + ' Positive predictive value: 0.50\n' + ')' + ), + ) + + +class EpsilonLowerBoundValueTest(parameterized.TestCase): + """Tests for EpsilonLowerBoundValue class.""" + + def test_epsilon_lower_bound_value(self): + bounds = { + elb.BoundMethod.KATZ_LOG: np.array([2, 2.0]), + elb.BoundMethod.BAILEY: np.array([5, 4.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([10]), + } + elbv = EpsilonLowerBoundValue(bounds=bounds) + self.assertDictEqual(elbv.bounds, bounds) + np.testing.assert_allclose(elbv.get_max_epsilon_bounds(), [10]) + self.assertEqual(str(elbv), 'EpsilonLowerBoundValue([10.0000])') + + def test_epsilon_lower_bound_value_empty(self): + elbv = EpsilonLowerBoundValue(bounds={}) + self.assertDictEqual(elbv.bounds, {}) + self.assertEmpty(elbv.get_max_epsilon_bounds()) + self.assertEqual(str(elbv), 'EpsilonLowerBoundValue([])') + + def test_epsilon_lower_bound_value_one_array_empty(self): + elbv = EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([-2, -2.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([]), + } + ) + np.testing.assert_allclose(elbv.get_max_epsilon_bounds(), [-2, -2.0]) + self.assertEqual(str(elbv), 'EpsilonLowerBoundValue([-2.0000, -2.0000])') + + def test_epsilon_lower_bound_value_all_array_empty(self): + elbv = EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([]), + elb.BoundMethod.ADJUSTED_LOG: np.array([]), + } + ) + self.assertEmpty(elbv.get_max_epsilon_bounds()) + self.assertEqual(str(elbv), 'EpsilonLowerBoundValue([])') + class SingleAttackResultTest(absltest.TestCase): + def setUp(self): + super().setUp() + # Some arbitrary roc. + self.arbitrary_roc = RocCurve( + tpr=np.array([0.0, 0.5, 1.0]), + fpr=np.array([0.0, 0.5, 1.0]), + thresholds=np.array([0, 1, 2]), + test_train_ratio=1.0, + ) + + # Some arbitrary epsilon lower bound. + self.arbitrary_epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([2, 2.0]), + elb.BoundMethod.BAILEY: np.array([5, 4.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([10, 1.0]), + } + ) + # Only a basic test, as this method calls RocCurve which is tested separately. def test_auc_random_classifier(self): roc = RocCurve( @@ -591,9 +672,11 @@ class SingleAttackResultTest(absltest.TestCase): result = SingleAttackResult( roc_curve=roc, + epsilon_lower_bound_value=self.arbitrary_epsilon_lower_bound_value, slice_spec=SingleSliceSpec(None), attack_type=AttackType.THRESHOLD_ATTACK, - data_size=DataSize(ntrain=1, ntest=1)) + data_size=DataSize(ntrain=1, ntest=1), + ) self.assertEqual(result.get_auc(), 0.5) @@ -607,9 +690,11 @@ class SingleAttackResultTest(absltest.TestCase): result = SingleAttackResult( roc_curve=roc, + epsilon_lower_bound_value=self.arbitrary_epsilon_lower_bound_value, slice_spec=SingleSliceSpec(None), attack_type=AttackType.THRESHOLD_ATTACK, - data_size=DataSize(ntrain=1, ntest=1)) + data_size=DataSize(ntrain=1, ntest=1), + ) self.assertEqual(result.get_attacker_advantage(), 0.0) @@ -623,12 +708,59 @@ class SingleAttackResultTest(absltest.TestCase): result = SingleAttackResult( roc_curve=roc, + epsilon_lower_bound_value=self.arbitrary_epsilon_lower_bound_value, slice_spec=SingleSliceSpec(None), attack_type=AttackType.THRESHOLD_ATTACK, - data_size=DataSize(ntrain=1, ntest=1)) + data_size=DataSize(ntrain=1, ntest=1), + ) self.assertEqual(result.get_ppv(), 0.5) + # Only a basic test, as this method calls EpsilonLowerBound which is tested + # separately. + def test_epsilon_lower_bound(self): + epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([2, 2.0]), + elb.BoundMethod.BAILEY: np.array([5, 4.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([10, 1.0]), + } + ) + expected = [10, 1.0] + + result = SingleAttackResult( + roc_curve=self.arbitrary_roc, + epsilon_lower_bound_value=epsilon_lower_bound_value, + slice_spec=SingleSliceSpec(None), + attack_type=AttackType.THRESHOLD_ATTACK, + data_size=DataSize(ntrain=1, ntest=1), + ) + returned_value = result.get_epsilon_lower_bound() + np.testing.assert_allclose(returned_value, expected, atol=1e-7) + + def test_string(self): + result = SingleAttackResult( + roc_curve=self.arbitrary_roc, + epsilon_lower_bound_value=self.arbitrary_epsilon_lower_bound_value, + slice_spec=SingleSliceSpec(None), + attack_type=AttackType.THRESHOLD_ATTACK, + data_size=DataSize(ntrain=1, ntest=1), + ) + self.assertEqual( + str(result), + ( + 'SingleAttackResult(\n' + ' SliceSpec: Entire dataset\n' + ' DataSize: (ntrain=1, ntest=1)\n' + ' AttackType: THRESHOLD_ATTACK\n' + ' AUC: 0.50\n' + ' Attacker advantage: 0.00\n' + ' Positive Predictive Value: 0.50\n' + ' Epsilon lower bound: 10.0000, 1.0000\n' + ')' + ), + ) + class SingleMembershipProbabilityResultTest(absltest.TestCase): @@ -648,6 +780,40 @@ class SingleMembershipProbabilityResultTest(absltest.TestCase): result.attack_with_varied_thresholds( threshold_list=np.array([0.8, 0.7]))[2].tolist(), [0.8, 1]) + @mock.patch.object(data_structures, 'EPSILON_K', 4) + def test_collect_results(self): + result = SingleMembershipProbabilityResult( + slice_spec=SingleSliceSpec(None), + train_membership_probs=np.array([0.91, 1, 0.92, 0.82, 0.75]), + test_membership_probs=np.array([0.81, 0.7, 0.75, 0.25, 0.3]), + ) + summary = result.collect_results( + threshold_list=np.array([0.8, 0.7]), return_roc_results=True + ) + self.assertListEqual( + summary, + [ + '\nMembership probability analysis over slice: "Entire dataset"', + ( + ' with 0.8000 as the threshold on membership probability, the' + ' precision-recall pair is (0.8000, 0.8000)' + ), + ( + ' with 0.7000 as the threshold on membership probability, the' + ' precision-recall pair is (0.6250, 1.0000)' + ), + ' thresholding on membership probability achieved an AUC of 0.94', + ( + ' thresholding on membership probability achieved an advantage' + ' of 0.80' + ), + ( + ' thresholding on membership probability achieved top-4' + ' epsilon lower bound of 0.3719, 0.2765, 0.1207, 0.1207' + ), + ], + ) + class AttackResultsCollectionTest(absltest.TestCase): @@ -661,8 +827,15 @@ class AttackResultsCollectionTest(absltest.TestCase): tpr=np.array([0.0, 0.5, 1.0]), fpr=np.array([0.0, 0.5, 1.0]), thresholds=np.array([0, 1, 2]), - test_train_ratio=1.0), - data_size=DataSize(ntrain=1, ntest=1)) + test_train_ratio=1.0, + ), + epsilon_lower_bound_value=EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([2, 2.0]), + } + ), + data_size=DataSize(ntrain=1, ntest=1), + ) self.results_epoch_10 = AttackResults( single_attack_results=[self.some_attack_result], @@ -722,8 +895,20 @@ class AttackResultsTest(absltest.TestCase): tpr=np.array([0.0, 1.0, 1.0]), fpr=np.array([1.0, 1.0, 0.0]), thresholds=np.array([0, 1, 2]), - test_train_ratio=1.0), - data_size=DataSize(ntrain=1, ntest=1)) + test_train_ratio=1.0, + ), + epsilon_lower_bound_value=EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.INV_SINH: np.array( + [2.65964282, 2.65964282, -0.01648963, -0.01648963] + ), + elb.BoundMethod.CLOPPER_PEARSON: np.array( + [3.28134635, 3.28134635] + ), + } + ), + data_size=DataSize(ntrain=1, ntest=1), + ) # ROC curve of a random classifier self.random_classifier_result = SingleAttackResult( @@ -733,8 +918,18 @@ class AttackResultsTest(absltest.TestCase): tpr=np.array([0.0, 0.5, 1.0]), fpr=np.array([0.0, 0.5, 1.0]), thresholds=np.array([0, 1, 2]), - test_train_ratio=1.0), - data_size=DataSize(ntrain=1, ntest=1)) + test_train_ratio=1.0, + ), + epsilon_lower_bound_value=EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([-0.01648981, -0.01648981]), + elb.BoundMethod.ADJUSTED_LOG: np.array( + [-0.01640757, -0.01640757] + ), + } + ), + data_size=DataSize(ntrain=1, ntest=1), + ) def test_get_result_with_max_auc_first(self): results = AttackResults( @@ -772,34 +967,59 @@ class AttackResultsTest(absltest.TestCase): self.assertEqual(results.get_result_with_max_ppv(), self.perfect_classifier_result) + def test_get_result_with_max_epsilon_first(self): + results = AttackResults( + [self.random_classifier_result, self.perfect_classifier_result] + ) + self.assertEqual( + results.get_result_with_max_epsilon(), self.perfect_classifier_result + ) + + def test_get_result_with_max_epsilon_second(self): + results = AttackResults( + [self.random_classifier_result, self.perfect_classifier_result] + ) + self.assertEqual( + results.get_result_with_max_epsilon(), self.perfect_classifier_result + ) + def test_summary_by_slices(self): results = AttackResults( [self.perfect_classifier_result, self.random_classifier_result]) self.assertSequenceEqual( results.summary(by_slices=True), - 'Best-performing attacks over all slices\n' + - ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' - ' AUC of 1.00 on slice CORRECTLY_CLASSIFIED=True\n' + - ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' + 'Best-performing attacks over all slices\n' + + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' + ' AUC of 1.00 on slice CORRECTLY_CLASSIFIED=True\n' + + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' advantage of 1.00 on slice CORRECTLY_CLASSIFIED=True\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved a' ' positive predictive value of 1.00 on slice CORRECTLY_CLASSIFIED=' - 'True\n\n' + 'True\n' + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved' + ' top-5 epsilon lower bounds of 3.2813, 3.2813 on slice' + ' CORRECTLY_CLASSIFIED=True\n\n' 'Best-performing attacks over slice: "CORRECTLY_CLASSIFIED=True"\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' AUC of 1.00\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' advantage of 1.00\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved a' - ' positive predictive value of 1.00\n\n' + ' positive predictive value of 1.00\n' + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved' + ' top-5 epsilon lower bounds of 3.2813, 3.2813\n\n' 'Best-performing attacks over slice: "Entire dataset"\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' AUC of 0.50\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' advantage of 0.00\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved a' - ' positive predictive value of 0.50') + ' positive predictive value of 0.50\n' + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved' + ' top-5 epsilon lower bounds of -0.0164, -0.0164', + ) + @mock.patch.object(data_structures, 'EPSILON_K', 4) def test_summary_without_slices(self): results = AttackResults( [self.perfect_classifier_result, self.random_classifier_result]) @@ -811,7 +1031,12 @@ class AttackResultsTest(absltest.TestCase): ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved an' ' advantage of 1.00 on slice CORRECTLY_CLASSIFIED=True\n' ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved a' - ' positive predictive value of 1.00 on slice CORRECTLY_CLASSIFIED=True') + ' positive predictive value of 1.00 on slice' + ' CORRECTLY_CLASSIFIED=True\n' + ' THRESHOLD_ATTACK (with 1 training and 1 test examples) achieved' + ' top-4 epsilon lower bounds of 3.2813, 3.2813 on slice' + ' CORRECTLY_CLASSIFIED=True', + ) def test_save_load(self): results = AttackResults( @@ -824,6 +1049,7 @@ class AttackResultsTest(absltest.TestCase): self.assertEqual(repr(results), repr(loaded_results)) + @mock.patch.object(data_structures, 'EPSILON_K', 4) def test_calculate_pd_dataframe(self): single_results = [ self.perfect_classifier_result, self.random_classifier_result @@ -838,7 +1064,11 @@ class AttackResultsTest(absltest.TestCase): 'attack type': ['THRESHOLD_ATTACK', 'THRESHOLD_ATTACK'], 'Attacker advantage': [1.0, 0.0], 'Positive predictive value': [1.0, 0.5], - 'AUC': [1.0, 0.5] + 'AUC': [1.0, 0.5], + 'Epsilon lower bound_1': [3.28134635, -0.01640757], + 'Epsilon lower bound_2': [3.28134635, -0.01640757], + 'Epsilon lower bound_3': [np.nan, np.nan], + 'Epsilon lower bound_4': [np.nan, np.nan], }) pd.testing.assert_frame_equal(df, df_expected) diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack.py b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack.py index ea6bc98..9c21131 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack.py +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack.py @@ -24,12 +24,16 @@ import numpy as np from scipy import special from sklearn import metrics from sklearn import model_selection - +from tensorflow_privacy.privacy.privacy_tests import epsilon_lower_bound as elb from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack import models from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackInputData from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackResults from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackType from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import DataSize +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EPSILON_ALPHA +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EPSILON_K +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EPSILON_METHODS +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EpsilonLowerBoundValue from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import MembershipProbabilityResults from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import PrivacyReportMetadata from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import RocCurve @@ -135,13 +139,23 @@ def _run_trained_attack( test_train_ratio=test_train_ratio) in_train_indices = labels == 1 + epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds=elb.EpsilonLowerBound( + pos_scores=scores[in_train_indices], + neg_scores=scores[~in_train_indices], + alpha=EPSILON_ALPHA, + two_sided_threshold=True, + ).compute_epsilon_lower_bounds(methods=EPSILON_METHODS, k=EPSILON_K) + ) return SingleAttackResult( slice_spec=_get_slice_spec(attack_input), data_size=prepared_attacker_data.data_size, attack_type=attack_type, membership_scores_train=scores[in_train_indices], membership_scores_test=scores[~in_train_indices], - roc_curve=roc_curve) + roc_curve=roc_curve, + epsilon_lower_bound_value=epsilon_lower_bound_value, + ) def _run_threshold_attack(attack_input: AttackInputData): @@ -173,6 +187,14 @@ def _run_threshold_attack(attack_input: AttackInputData): thresholds=-thresholds, # negate because we negated the loss test_train_ratio=test_train_ratio, ) + epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds=elb.EpsilonLowerBound( + pos_scores=loss_train, + neg_scores=loss_test, + alpha=EPSILON_ALPHA, + two_sided_threshold=True, + ).compute_epsilon_lower_bounds(methods=EPSILON_METHODS, k=EPSILON_K) + ) return SingleAttackResult( slice_spec=_get_slice_spec(attack_input), @@ -180,7 +202,9 @@ def _run_threshold_attack(attack_input: AttackInputData): attack_type=AttackType.THRESHOLD_ATTACK, membership_scores_train=attack_input.get_loss_train(), membership_scores_test=attack_input.get_loss_test(), - roc_curve=roc_curve) + roc_curve=roc_curve, + epsilon_lower_bound_value=epsilon_lower_bound_value, + ) def _run_threshold_entropy_attack(attack_input: AttackInputData): @@ -207,14 +231,23 @@ def _run_threshold_entropy_attack(attack_input: AttackInputData): thresholds=-thresholds, # negate because we negated the loss test_train_ratio=test_train_ratio, ) - + epsilon_lower_bound_value = EpsilonLowerBoundValue( + bounds=elb.EpsilonLowerBound( + pos_scores=attack_input.get_entropy_train(), + neg_scores=attack_input.get_entropy_test(), + alpha=EPSILON_ALPHA, + two_sided_threshold=True, + ).compute_epsilon_lower_bounds(methods=EPSILON_METHODS, k=EPSILON_K) + ) return SingleAttackResult( slice_spec=_get_slice_spec(attack_input), data_size=DataSize(ntrain=ntrain, ntest=ntest), attack_type=AttackType.THRESHOLD_ENTROPY_ATTACK, membership_scores_train=-attack_input.get_entropy_train(), membership_scores_test=-attack_input.get_entropy_test(), - roc_curve=roc_curve) + roc_curve=roc_curve, + epsilon_lower_bound_value=epsilon_lower_bound_value, + ) def _run_attack(attack_input: AttackInputData, diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack_test.py b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack_test.py index 73fac0c..4644814 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack_test.py +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/membership_inference_attack_test.py @@ -17,7 +17,9 @@ from unittest import mock from absl.testing import absltest from absl.testing import parameterized import numpy as np +from tensorflow_privacy.privacy.privacy_tests import epsilon_lower_bound as elb from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack import membership_inference_attack as mia +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack import models from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackInputData from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackType from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import DataSize @@ -103,6 +105,21 @@ def get_test_input_logits_only_with_sample_weights(n_train, n_test): sample_weight_test=rng.randn(n_test, 1)) +class MockTrainedAttacker(object): + """Mock for TrainedAttacker.""" + + def __init__(self, backend): + del backend + return + + def train_model(self, input_features, is_training_labels, sample_weight=None): + del input_features, is_training_labels, sample_weight + return + + def predict(self, input_features): + return input_features[:, 0] + + class RunAttacksTest(parameterized.TestCase): def test_run_attacks_size(self): @@ -285,6 +302,117 @@ class RunAttacksTest(parameterized.TestCase): # namely 0.5. np.testing.assert_almost_equal(result.roc_curve.get_ppv(), 0.5, decimal=2) + @parameterized.parameters( + (AttackType.THRESHOLD_ATTACK, 'loss_train', 'loss_test'), + (AttackType.THRESHOLD_ENTROPY_ATTACK, 'entropy_train', 'entropy_test'), + ) + @mock.patch.object(mia, 'EPSILON_K', 4) + def test_run_attack_threshold_calculates_correct_epsilon( + self, attack_type, train_metric_name, test_metric_name + ): + result = mia._run_attack( + AttackInputData( + **{ + train_metric_name: np.array([0.1, 0.2, 1.3, 0.4, 0.5, 0.6]), + test_metric_name: np.array([1.1, 1.2, 1.3, 0.4, 1.5, 1.6]), + } + ), + attack_type, + ) + np.testing.assert_almost_equal( + result.epsilon_lower_bound_value.get_max_epsilon_bounds(), + np.array([0.34695111, 0.34695111, 0.05616349, 0.05616349]), + ) + + @parameterized.product( + ( + dict( + epsilon_methods=tuple(elb.BoundMethod), + expected_max_method=elb.BoundMethod.CLOPPER_PEARSON, + ), + dict( + epsilon_methods=( + elb.BoundMethod.KATZ_LOG, + elb.BoundMethod.CLOPPER_PEARSON, + ), + expected_max_method=elb.BoundMethod.CLOPPER_PEARSON, + ), + dict( + epsilon_methods=(elb.BoundMethod.BAILEY,), + expected_max_method=elb.BoundMethod.BAILEY, + ), + ), + attack_type=[ + AttackType.LOGISTIC_REGRESSION, + AttackType.MULTI_LAYERED_PERCEPTRON, + AttackType.RANDOM_FOREST, + AttackType.K_NEAREST_NEIGHBORS, + ], + ) + @mock.patch.object(models, 'TrainedAttacker', MockTrainedAttacker) + @mock.patch.object(models, 'LogisticRegressionAttacker', MockTrainedAttacker) + @mock.patch.object( + models, 'MultilayerPerceptronAttacker', MockTrainedAttacker + ) + @mock.patch.object(models, 'RandomForestAttacker', MockTrainedAttacker) + @mock.patch.object(models, 'KNearestNeighborsAttacker', MockTrainedAttacker) + def test_run_attack_trained_calculates_correct_epsilon( + self, + epsilon_methods, + expected_max_method, + attack_type, + ): + logits_train = np.ones((1000, 5)) + logits_test = np.zeros((100, 5)) + # The prediction would be all 1 for training and all 0 for test. + expected_bounds = { + elb.BoundMethod.KATZ_LOG: [ + 5.27530977, + 2.97796578, + -0.00720554, + -0.01623037, + ], + elb.BoundMethod.ADJUSTED_LOG: [ + 5.27580935, + 2.98292432, + -0.00717236, + -0.01614769, + -7.62368550, + ], + elb.BoundMethod.BAILEY: [ + 5.87410287, + 3.57903543, + -0.00718316, + -0.01625286, + ], + elb.BoundMethod.INV_SINH: [ + 4.95123977, + 2.65964282, + -0.00720547, + -0.01623030, + ], + elb.BoundMethod.CLOPPER_PEARSON: [ + 5.56738762, + 3.31454626, + ], + } + with mock.patch.object(mia, 'EPSILON_METHODS', epsilon_methods): + result = mia._run_attack( + AttackInputData(logits_train, logits_test), + attack_type, + ) + self.assertCountEqual( + result.epsilon_lower_bound_value.bounds.keys(), epsilon_methods + ) + for key in epsilon_methods: + np.testing.assert_almost_equal( + result.epsilon_lower_bound_value.bounds[key], expected_bounds[key] + ) + np.testing.assert_almost_equal( + result.epsilon_lower_bound_value.get_max_epsilon_bounds(), + expected_bounds[expected_max_method], + ) + def test_run_attack_by_slice(self): result = mia.run_attacks( get_test_input(100, 100), SlicingSpec(by_class=True), diff --git a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/privacy_report_test.py b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/privacy_report_test.py index 19e90d0..b3a1f2d 100644 --- a/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/privacy_report_test.py +++ b/tensorflow_privacy/privacy/privacy_tests/membership_inference_attack/privacy_report_test.py @@ -14,12 +14,13 @@ from absl.testing import absltest import numpy as np - +from tensorflow_privacy.privacy.privacy_tests import epsilon_lower_bound as elb from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack import privacy_report from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackResults from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackResultsCollection from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import AttackType from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import DataSize +from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import EpsilonLowerBoundValue from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import PrivacyReportMetadata from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import RocCurve from tensorflow_privacy.privacy.privacy_tests.membership_inference_attack.data_structures import SingleAttackResult @@ -39,8 +40,16 @@ class PrivacyReportTest(absltest.TestCase): tpr=np.array([0.0, 0.5, 1.0]), fpr=np.array([0.0, 0.5, 1.0]), thresholds=np.array([0, 1, 2]), - test_train_ratio=1.0), - data_size=DataSize(ntrain=1, ntest=1)) + test_train_ratio=1.0, + ), + epsilon_lower_bound_value=EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([-2, -2.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([]), + } + ), + data_size=DataSize(ntrain=1, ntest=1), + ) # Classifier that achieves an AUC of 1.0. self.perfect_classifier_result = SingleAttackResult( @@ -50,8 +59,16 @@ class PrivacyReportTest(absltest.TestCase): tpr=np.array([0.0, 1.0, 1.0]), fpr=np.array([1.0, 1.0, 0.0]), thresholds=np.array([0, 1, 2]), - test_train_ratio=1.0), - data_size=DataSize(ntrain=1, ntest=1)) + test_train_ratio=1.0, + ), + epsilon_lower_bound_value=EpsilonLowerBoundValue( + bounds={ + elb.BoundMethod.KATZ_LOG: np.array([-2, -2.0]), + elb.BoundMethod.ADJUSTED_LOG: np.array([10, 1.0]), + } + ), + data_size=DataSize(ntrain=1, ntest=1), + ) self.results_epoch_0 = AttackResults( single_attack_results=[self.imperfect_classifier_result],