Skip to content

Commit 31fafff

Browse files
authored
Merge pull request #2384 from abigailgold/dev_1.18.0_scaling
Support scaling input features to attacks
2 parents 3597228 + 290519d commit 31fafff

File tree

4 files changed

+196
-7
lines changed

4 files changed

+196
-7
lines changed

art/attacks/inference/attribute_inference/black_box.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from sklearn.svm import SVC, SVR
3333
from sklearn.preprocessing import minmax_scale, OneHotEncoder, OrdinalEncoder
3434
from sklearn.compose import ColumnTransformer
35+
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
3536

3637
from art.estimators.estimator import BaseEstimator
3738
from art.estimators.classification.classifier import ClassifierMixin
@@ -80,6 +81,7 @@ def __init__(
8081
is_continuous: Optional[bool] = False,
8182
scale_range: Optional[Tuple[float, float]] = None,
8283
prediction_normal_factor: Optional[float] = 1,
84+
scaler_type: Optional[str] = "standard",
8385
non_numerical_features: Optional[List[int]] = None,
8486
encoder: Optional[Union[OrdinalEncoder, OneHotEncoder, ColumnTransformer]] = None,
8587
nn_model_epochs: int = 100,
@@ -109,7 +111,11 @@ def __init__(
109111
Only applicable when `estimator` is a regressor.
110112
:param prediction_normal_factor: If supplied, the class labels (both true and predicted) are multiplied by the
111113
factor when used as inputs to the attack-model. Only applicable when
112-
`estimator` is a regressor and if `scale_range` is not supplied
114+
`estimator` is a regressor and if `scale_range` is not supplied.
115+
:param scaler_type: The type of scaling to apply to all input features to the attack. Can be one of: "standard",
116+
"minmax", "robust" or None. If not None, the appropriate scaler from scikit-learn will be
117+
applied. If None, no scaling will be applied. This is in addition to any specific scaling
118+
performed on the class labels based on the params scale_range or prediction_normal_factor.
113119
:param non_numerical_features: a list of feature indexes that require encoding in order to feed into an ML model
114120
(i.e., strings), not including the attacked feature. Should only be supplied if
115121
non-numeric features exist in the input data not including the attacked feature,
@@ -130,6 +136,8 @@ def __init__(
130136
self.attack_model: Optional[Any] = None
131137
self.prediction_normal_factor = prediction_normal_factor
132138
self.scale_range = scale_range
139+
self.scaler_type = scaler_type
140+
self.scaler: Optional[Any] = None
133141
self.epochs = nn_model_epochs
134142
self.batch_size = nn_model_batch_size
135143
self.learning_rate = nn_model_learning_rate
@@ -252,6 +260,19 @@ def fit(self, x: np.ndarray, y: Optional[np.ndarray] = None) -> None:
252260
if y is not None:
253261
x_train = np.concatenate((x_train, y), axis=1)
254262

263+
if self.scaler_type:
264+
if self.scaler_type == "standard":
265+
self.scaler = StandardScaler()
266+
elif self.scaler_type == "minmax":
267+
self.scaler = MinMaxScaler()
268+
elif self.scaler_type == "robust":
269+
self.scaler = RobustScaler()
270+
else:
271+
raise ValueError("Illegal scaler_type: ", self.scaler_type)
272+
if self.scaler:
273+
self.scaler.fit(x_train)
274+
x_train = self.scaler.transform(x_train)
275+
255276
# train attack model
256277
if self._attack_model_type == "nn":
257278
import torch
@@ -407,6 +428,9 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
407428
if y is not None:
408429
x_test = np.concatenate((x_test, y), axis=1)
409430

431+
if self.scaler:
432+
x_test = self.scaler.transform(x_test)
433+
410434
if self._attack_model_type == "nn":
411435
from torch.utils.data import DataLoader
412436
from art.utils import to_cuda, from_cuda

art/attacks/inference/membership_inference/black_box.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from sklearn.neighbors import KNeighborsClassifier
3232
from sklearn.tree import DecisionTreeClassifier
3333
from sklearn.svm import SVC
34+
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
3435

3536
from art.attacks.attack import MembershipInferenceAttack
3637
from art.estimators.estimator import BaseEstimator
@@ -56,6 +57,10 @@ class MembershipInferenceBlackBox(MembershipInferenceAttack):
5657
"input_type",
5758
"attack_model_type",
5859
"attack_model",
60+
"scaler_type",
61+
"nn_model_epochs",
62+
"nn_model_batch_size",
63+
"nn_model_learning_rate",
5964
]
6065
_estimator_requirements = (BaseEstimator, (ClassifierMixin, RegressorMixin))
6166

@@ -65,6 +70,7 @@ def __init__(
6570
input_type: str = "prediction",
6671
attack_model_type: str = "nn",
6772
attack_model: Optional[Any] = None,
73+
scaler_type: Optional[str] = "standard",
6874
nn_model_epochs: int = 100,
6975
nn_model_batch_size: int = 100,
7076
nn_model_learning_rate: float = 0.0001,
@@ -73,6 +79,9 @@ def __init__(
7379
Create a MembershipInferenceBlackBox attack instance.
7480
7581
:param estimator: Target estimator.
82+
:param input_type: the type of input to train the attack on. Can be one of: 'prediction' or 'loss'. Default is
83+
`prediction`. Predictions can be either probabilities or logits, depending on the return type
84+
of the model. If the model is a regressor, only `loss` can be used.
7685
:param attack_model_type: the type of default attack model to train, optional. Should be one of:
7786
`nn` (neural network, default),
7887
`rf` (random forest),
@@ -82,10 +91,10 @@ def __init__(
8291
`knn` (k nearest neighbors),
8392
`svm` (support vector machine).
8493
If `attack_model` is supplied, this option will be ignored.
85-
:param input_type: the type of input to train the attack on. Can be one of: 'prediction' or 'loss'. Default is
86-
`prediction`. Predictions can be either probabilities or logits, depending on the return type
87-
of the model. If the model is a regressor, only `loss` can be used.
8894
:param attack_model: The attack model to train, optional. If none is provided, a default model will be created.
95+
:param scaler_type: The type of scaling to apply to the input features to the attack. Can be one of: "standard",
96+
"minmax", "robust" or None. If not None, the appropriate scaler from scikit-learn will be
97+
applied. If None, no scaling will be applied.
8998
:param nn_model_epochs: the number of epochs to use when training a nn attack model
9099
:param nn_model_batch_size: the batch size to use when training a nn attack model
91100
:param nn_model_learning_rate: the learning rate to use when training a nn attack model
@@ -95,6 +104,8 @@ def __init__(
95104
self.input_type = input_type
96105
self.attack_model_type = attack_model_type
97106
self.attack_model = attack_model
107+
self.scaler_type = scaler_type
108+
self.scaler: Optional[Any] = None
98109
self.epochs = nn_model_epochs
99110
self.batch_size = nn_model_batch_size
100111
self.learning_rate = nn_model_learning_rate
@@ -245,13 +256,27 @@ def fit( # pylint: disable=W0613
245256
if x_2 is None:
246257
self.use_label = False
247258

259+
if self.scaler_type:
260+
if self.scaler_type == "standard":
261+
self.scaler = StandardScaler()
262+
elif self.scaler_type == "minmax":
263+
self.scaler = MinMaxScaler()
264+
elif self.scaler_type == "robust":
265+
self.scaler = RobustScaler()
266+
else:
267+
raise ValueError("Illegal scaler_type: ", self.scaler_type)
268+
248269
if self.default_model and self.attack_model_type == "nn":
249270
import torch
250271
from torch import nn
251272
from torch import optim
252273
from torch.utils.data import DataLoader
253274
from art.utils import to_cuda
254275

276+
if self.scaler:
277+
self.scaler.fit(x_1)
278+
x_1 = self.scaler.transform(x_1)
279+
255280
if x_2 is not None:
256281

257282
class MembershipInferenceAttackModel(nn.Module):
@@ -393,8 +418,15 @@ def forward(self, x_1):
393418
else: # not nn
394419
y_ready = check_and_transform_label_format(y_new, nb_classes=2, return_one_hot=False)
395420
if x_2 is not None:
396-
self.attack_model.fit(np.c_[x_1, x_2], y_ready.ravel()) # type: ignore
421+
x = np.c_[x_1, x_2]
422+
if self.scaler:
423+
self.scaler.fit(x)
424+
x = self.scaler.transform(x)
425+
self.attack_model.fit(x, y_ready.ravel()) # type: ignore
397426
else:
427+
if self.scaler:
428+
self.scaler.fit(x_1)
429+
x_1 = self.scaler.transform(x_1)
398430
self.attack_model.fit(x_1, y_ready.ravel()) # type: ignore
399431

400432
def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
@@ -467,6 +499,9 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
467499
from torch.utils.data import DataLoader
468500
from art.utils import to_cuda, from_cuda
469501

502+
if self.scaler:
503+
features = self.scaler.transform(features)
504+
470505
self.attack_model.eval() # type: ignore
471506
predictions: Optional[np.ndarray] = None
472507

@@ -512,17 +547,27 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
512547
elif not self.default_model:
513548
# assumes the predict method of the supplied model returns probabilities
514549
if y is not None and self.use_label:
515-
inferred = self.attack_model.predict(np.c_[features, y]) # type: ignore
550+
features = np.c_[features, y]
551+
if self.scaler:
552+
features = self.scaler.transform(features)
553+
inferred = self.attack_model.predict(features) # type: ignore
516554
else:
555+
if self.scaler:
556+
features = self.scaler.transform(features)
517557
inferred = self.attack_model.predict(features) # type: ignore
518558
if probabilities:
519559
inferred_return = inferred
520560
else:
521561
inferred_return = np.round(inferred)
522562
else:
523563
if y is not None and self.use_label:
524-
inferred = self.attack_model.predict_proba(np.c_[features, y]) # type: ignore
564+
features = np.c_[features, y]
565+
if self.scaler:
566+
features = self.scaler.transform(features)
567+
inferred = self.attack_model.predict_proba(features) # type: ignore
525568
else:
569+
if self.scaler:
570+
features = self.scaler.transform(features)
526571
inferred = self.attack_model.predict_proba(features) # type: ignore
527572
if probabilities:
528573
inferred_return = inferred[:, [1]]

tests/attacks/inference/attribute_inference/test_black_box.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,107 @@ def transform_feature(x):
9494
art_warning(e)
9595

9696

97+
@pytest.mark.skip_framework("dl_frameworks")
98+
@pytest.mark.parametrize("scaler_type", ["standard", "robust", "minmax"])
99+
def test_black_box_scalers(art_warning, scaler_type, decision_tree_estimator, get_iris_dataset):
100+
try:
101+
attack_feature = 2 # petal length
102+
103+
# need to transform attacked feature into categorical
104+
def transform_feature(x):
105+
x[x > 0.5] = 2.0
106+
x[(x > 0.2) & (x <= 0.5)] = 1.0
107+
x[x <= 0.2] = 0.0
108+
109+
values = [0.0, 1.0, 2.0]
110+
111+
(x_train_iris, y_train_iris), (x_test_iris, y_test_iris) = get_iris_dataset
112+
# training data without attacked feature
113+
x_train_for_attack = np.delete(x_train_iris, attack_feature, 1)
114+
# only attacked feature
115+
x_train_feature = x_train_iris[:, attack_feature].copy().reshape(-1, 1)
116+
transform_feature(x_train_feature)
117+
# training data with attacked feature (after transformation)
118+
x_train = np.concatenate((x_train_for_attack[:, :attack_feature], x_train_feature), axis=1)
119+
x_train = np.concatenate((x_train, x_train_for_attack[:, attack_feature:]), axis=1)
120+
121+
# test data without attacked feature
122+
x_test_for_attack = np.delete(x_test_iris, attack_feature, 1)
123+
# only attacked feature
124+
x_test_feature = x_test_iris[:, attack_feature].copy().reshape(-1, 1)
125+
transform_feature(x_test_feature)
126+
127+
classifier = decision_tree_estimator()
128+
129+
attack = AttributeInferenceBlackBox(classifier, attack_feature=attack_feature, scaler_type=scaler_type)
130+
# get original model's predictions
131+
x_train_predictions = np.array([np.argmax(arr) for arr in classifier.predict(x_train_iris)]).reshape(-1, 1)
132+
x_test_predictions = np.array([np.argmax(arr) for arr in classifier.predict(x_test_iris)]).reshape(-1, 1)
133+
# train attack model
134+
attack.fit(x_train)
135+
# infer attacked feature
136+
inferred_train = attack.infer(x_train_for_attack, pred=x_train_predictions, values=values)
137+
inferred_test = attack.infer(x_test_for_attack, pred=x_test_predictions, values=values)
138+
# check accuracy
139+
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
140+
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
141+
assert pytest.approx(0.8285, abs=0.3) == train_acc
142+
assert pytest.approx(0.8888, abs=0.3) == test_acc
143+
144+
except ARTTestException as e:
145+
art_warning(e)
146+
147+
148+
@pytest.mark.skip_framework("dl_frameworks")
149+
def test_black_box_tabular_no_scaler(art_warning, decision_tree_estimator, get_iris_dataset):
150+
try:
151+
attack_feature = 2 # petal length
152+
153+
# need to transform attacked feature into categorical
154+
def transform_feature(x):
155+
x[x > 0.5] = 2.0
156+
x[(x > 0.2) & (x <= 0.5)] = 1.0
157+
x[x <= 0.2] = 0.0
158+
159+
values = [0.0, 1.0, 2.0]
160+
161+
(x_train_iris, y_train_iris), (x_test_iris, y_test_iris) = get_iris_dataset
162+
# training data without attacked feature
163+
x_train_for_attack = np.delete(x_train_iris, attack_feature, 1)
164+
# only attacked feature
165+
x_train_feature = x_train_iris[:, attack_feature].copy().reshape(-1, 1)
166+
transform_feature(x_train_feature)
167+
# training data with attacked feature (after transformation)
168+
x_train = np.concatenate((x_train_for_attack[:, :attack_feature], x_train_feature), axis=1)
169+
x_train = np.concatenate((x_train, x_train_for_attack[:, attack_feature:]), axis=1)
170+
171+
# test data without attacked feature
172+
x_test_for_attack = np.delete(x_test_iris, attack_feature, 1)
173+
# only attacked feature
174+
x_test_feature = x_test_iris[:, attack_feature].copy().reshape(-1, 1)
175+
transform_feature(x_test_feature)
176+
177+
classifier = decision_tree_estimator()
178+
179+
attack = AttributeInferenceBlackBox(classifier, attack_feature=attack_feature, scaler_type=None)
180+
# get original model's predictions
181+
x_train_predictions = np.array([np.argmax(arr) for arr in classifier.predict(x_train_iris)]).reshape(-1, 1)
182+
x_test_predictions = np.array([np.argmax(arr) for arr in classifier.predict(x_test_iris)]).reshape(-1, 1)
183+
# train attack model
184+
attack.fit(x_train)
185+
# infer attacked feature
186+
inferred_train = attack.infer(x_train_for_attack, pred=x_train_predictions, values=values)
187+
inferred_test = attack.infer(x_test_for_attack, pred=x_test_predictions, values=values)
188+
# check accuracy
189+
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
190+
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
191+
assert pytest.approx(0.8285, abs=0.3) == train_acc
192+
assert pytest.approx(0.8888, abs=0.3) == test_acc
193+
194+
except ARTTestException as e:
195+
art_warning(e)
196+
197+
97198
@pytest.mark.skip_framework("dl_frameworks")
98199
@pytest.mark.parametrize("model_type", ["nn", "rf", "gb", "lr", "dt", "knn", "svm"])
99200
def test_black_box_continuous(art_warning, decision_tree_estimator, get_iris_dataset, model_type):

tests/attacks/inference/membership_inference/test_black_box.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ def test_black_box_tabular(art_warning, model_type, decision_tree_estimator, get
5757
art_warning(e)
5858

5959

60+
@pytest.mark.parametrize("scaler_type", ["standard", "robust", "minmax"])
61+
def test_black_box_tabular_scalers(art_warning, scaler_type, decision_tree_estimator, get_iris_dataset):
62+
try:
63+
classifier = decision_tree_estimator()
64+
attack = MembershipInferenceBlackBox(classifier, scaler_type=scaler_type)
65+
backend_check_membership_accuracy(attack, get_iris_dataset, attack_train_ratio, 0.25)
66+
except ARTTestException as e:
67+
art_warning(e)
68+
69+
70+
def test_black_box_tabular_no_scaler(art_warning, decision_tree_estimator, get_iris_dataset):
71+
try:
72+
classifier = decision_tree_estimator()
73+
attack = MembershipInferenceBlackBox(classifier, scaler_type=None)
74+
backend_check_membership_accuracy(attack, get_iris_dataset, attack_train_ratio, 0.25)
75+
except ARTTestException as e:
76+
art_warning(e)
77+
78+
6079
@pytest.mark.parametrize("model_type", ["nn", "rf", "gb", "lr", "dt", "knn", "svm"])
6180
def test_black_box_tabular_no_label(art_warning, model_type, decision_tree_estimator, get_iris_dataset):
6281
try:

0 commit comments

Comments
 (0)