Skip to content

Commit 030dc49

Browse files
authored
Merge pull request #1645 from Trusted-AI/dev_1.10.1
Update to ART 1.10.1
2 parents 16443ae + 6ff0ea1 commit 030dc49

File tree

10 files changed

+178
-26
lines changed

10 files changed

+178
-26
lines changed

.github/workflows/ci-style-checks.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ jobs:
4141
pip install pluggy==0.13.1
4242
pip install tensorflow==2.7.0
4343
pip install keras==2.7.0
44-
python -m pip install types-six
45-
python -m pip install types-PyYAML
46-
python3 -m pip install types-setuptools
44+
pip install types-six
45+
pip install types-PyYAML
46+
pip install types-setuptools
4747
pip install click==8.0.2
4848
pip list
4949
- name: pycodestyle

art/attacks/evasion/adversarial_patch/adversarial_patch_pytorch.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,10 @@ def apply_patch(
683683
mask = mask.copy()
684684
mask = self._check_mask(mask=mask, x=x)
685685
x_tensor = torch.Tensor(x)
686-
mask_tensor = torch.Tensor(mask)
686+
if mask is not None:
687+
mask_tensor = torch.Tensor(mask)
688+
else:
689+
mask_tensor = None
687690
if isinstance(patch_external, np.ndarray):
688691
patch_tensor = torch.Tensor(patch_external)
689692
else:

art/attacks/inference/attribute_inference/black_box.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def fit(self, x: np.ndarray, y: Optional[np.ndarray] = None) -> None:
153153
# get model's predictions for x
154154
if ClassifierMixin in type(self.estimator).__mro__:
155155
predictions = np.array([np.argmax(arr) for arr in self.estimator.predict(x)]).reshape(-1, 1)
156+
if y is not None:
157+
y = check_and_transform_label_format(y, return_one_hot=True)
156158
else: # Regression model
157159
if self.scale_range is not None:
158160
predictions = minmax_scale(self.estimator.predict(x).reshape(-1, 1), feature_range=self.scale_range)
@@ -162,6 +164,8 @@ def fit(self, x: np.ndarray, y: Optional[np.ndarray] = None) -> None:
162164
predictions = self.estimator.predict(x).reshape(-1, 1) * self.prediction_normal_factor
163165
if y is not None:
164166
y = y * self.prediction_normal_factor
167+
if y is not None:
168+
y = y.reshape(-1, 1)
165169

166170
# get vector of attacked feature
167171
y_attack = x[:, self.attack_feature]
@@ -176,7 +180,6 @@ def fit(self, x: np.ndarray, y: Optional[np.ndarray] = None) -> None:
176180
x_train = np.concatenate((np.delete(x, self.attack_feature, 1), predictions), axis=1).astype(np.float32)
177181

178182
if y is not None:
179-
y = check_and_transform_label_format(y, return_one_hot=True)
180183
x_train = np.concatenate((x_train, y), axis=1)
181184

182185
# train attack model
@@ -227,11 +230,14 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
227230
x_test = np.concatenate((x, pred * self.prediction_normal_factor), axis=1).astype(np.float32)
228231
if y is not None:
229232
y = y * self.prediction_normal_factor
233+
if y is not None:
234+
y = y.reshape(-1, 1)
230235
else:
231236
x_test = np.concatenate((x, pred), axis=1).astype(np.float32)
237+
if y is not None:
238+
y = check_and_transform_label_format(y, return_one_hot=True)
232239

233240
if y is not None:
234-
y = check_and_transform_label_format(y, return_one_hot=True)
235241
x_test = np.concatenate((x_test, y), axis=1)
236242

237243
predictions = self.attack_model.predict(x_test).astype(np.float32)

art/attacks/inference/attribute_inference/true_label_baseline.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __init__(
5959
attack_model_type: str = "nn",
6060
attack_model: Optional["CLASSIFIER_TYPE"] = None,
6161
attack_feature: Union[int, slice] = 0,
62+
is_regression: Optional[bool] = False,
6263
scale_range: Optional[slice] = None,
6364
prediction_normal_factor: float = 1,
6465
):
@@ -72,11 +73,12 @@ def __init__(
7273
:param attack_feature: The index of the feature to be attacked or a slice representing multiple indexes in
7374
case of a one-hot encoded feature.
7475
case of a one-hot encoded feature.
76+
:param is_regression: Whether the model is a regression model. Default is False (classification).
7577
:param scale_range: If supplied, the class labels (both true and predicted) will be scaled to the given range.
76-
Only applicable when `estimator` is a regressor.
78+
Only applicable when `is_regression` is True.
7779
:param prediction_normal_factor: If supplied, the class labels (both true and predicted) are multiplied by the
7880
factor when used as inputs to the attack-model. Only applicable when
79-
`estimator` is a regressor and if `scale_range` is not supplied.
81+
`is_regression` is True and if `scale_range` is not supplied.
8082
"""
8183
super().__init__(estimator=None, attack_feature=attack_feature)
8284

@@ -119,6 +121,7 @@ def __init__(
119121

120122
self.prediction_normal_factor = prediction_normal_factor
121123
self.scale_range = scale_range
124+
self.is_regression = is_regression
122125
self._check_params()
123126
self.attack_feature = get_feature_index(self.attack_feature)
124127

@@ -146,11 +149,14 @@ def fit(self, x: np.ndarray, y: np.ndarray) -> None:
146149
raise ValueError("None value detected.")
147150

148151
# create training set for attack model
149-
if self.scale_range is not None:
150-
normalized_labels = minmax_scale(y, feature_range=self.scale_range)
152+
if self.is_regression:
153+
if self.scale_range is not None:
154+
normalized_labels = minmax_scale(y, feature_range=self.scale_range)
155+
else:
156+
normalized_labels = y * self.prediction_normal_factor
157+
normalized_labels = normalized_labels.reshape(-1, 1)
151158
else:
152-
normalized_labels = y * self.prediction_normal_factor
153-
normalized_labels = check_and_transform_label_format(normalized_labels, return_one_hot=True)
159+
normalized_labels = check_and_transform_label_format(y, return_one_hot=True)
154160
x_train = np.concatenate((np.delete(x, self.attack_feature, 1), normalized_labels), axis=1).astype(np.float32)
155161

156162
# train attack model
@@ -179,11 +185,14 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
179185
if values is not None:
180186
self._values = values
181187

182-
if self.scale_range is not None:
183-
normalized_labels = minmax_scale(y, feature_range=self.scale_range)
188+
if self.is_regression:
189+
if self.scale_range is not None:
190+
normalized_labels = minmax_scale(y, feature_range=self.scale_range)
191+
else:
192+
normalized_labels = y * self.prediction_normal_factor
193+
normalized_labels = normalized_labels.reshape(-1, 1)
184194
else:
185-
normalized_labels = y * self.prediction_normal_factor
186-
normalized_labels = check_and_transform_label_format(normalized_labels, return_one_hot=True)
195+
normalized_labels = check_and_transform_label_format(y, return_one_hot=True)
187196
x_test = np.concatenate((x, normalized_labels), axis=1).astype(np.float32)
188197

189198
predictions = self.attack_model.predict(x_test).astype(np.float32)

art/attacks/poisoning/gradient_matching_attack.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ def __initialize_poison_tensorflow(
151151
152152
:param x_trigger: A list of samples to use as triggers.
153153
:param y_trigger: A list of target classes to classify the triggers into.
154-
:param x_train: A list of training data to poison a portion of.
155-
:param y_train: A list of labels for x_train.
154+
:param x_poison: A list of training data to poison a portion of.
155+
:param y_poison: A list of true labels for x_poison.
156156
"""
157157
# pylint: disable=no-name-in-module
158158
from tensorflow.keras import backend as K
@@ -190,7 +190,7 @@ def _weight_grad(classifier: TensorFlowV2Classifier, x: tf.Tensor, target: tf.Te
190190
y_true_poison = Input(shape=np.shape(y_poison)[1:])
191191
embedding_layer = Embedding(
192192
len(x_poison),
193-
np.prod(input_poison.shape[1:]),
193+
np.prod(x_poison.shape[1:]),
194194
embeddings_initializer=tf.keras.initializers.RandomNormal(stddev=self.epsilon * 0.01),
195195
)
196196
embeddings = embedding_layer(input_indices)

art/defences/trainer/adversarial_trainer_madry_pgd.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ class AdversarialTrainerMadryPGD(Trainer):
5454
def __init__(
5555
self,
5656
classifier: "CLASSIFIER_LOSS_GRADIENTS_TYPE",
57-
nb_epochs: int = 391,
58-
batch_size: int = 128,
57+
nb_epochs: Optional[int] = 391,
58+
batch_size: Optional[int] = 128,
5959
eps: Union[int, float] = 8,
6060
eps_step: Union[int, float] = 2,
6161
max_iter: int = 7,
@@ -91,18 +91,41 @@ def __init__(
9191
self.trainer = AdversarialTrainer(classifier, self.attack, ratio=1.0) # type: ignore
9292

9393
def fit( # pylint: disable=W0221
94-
self, x: np.ndarray, y: np.ndarray, validation_data: Optional[np.ndarray] = None, **kwargs
94+
self,
95+
x: np.ndarray,
96+
y: np.ndarray,
97+
validation_data: Optional[np.ndarray] = None,
98+
batch_size: Optional[int] = None,
99+
nb_epochs: Optional[int] = None,
100+
**kwargs
95101
) -> None:
96102
"""
97103
Train a model adversarially. See class documentation for more information on the exact procedure.
98104
99105
:param x: Training data.
100106
:param y: Labels for the training data.
101107
:param validation_data: Validation data.
108+
:param batch_size: Size of batches. Overwrites batch_size defined in __init__ if not None.
109+
:param nb_epochs: Number of epochs to use for trainings. Overwrites nb_epochs defined in __init__ if not None.
102110
:param kwargs: Dictionary of framework-specific arguments.
103111
"""
112+
batch_size_fit: int
113+
if batch_size is not None:
114+
batch_size_fit = batch_size
115+
elif self.batch_size is not None:
116+
batch_size_fit = self.batch_size
117+
else:
118+
raise ValueError("Please provide value for `batch_size`.")
119+
120+
if nb_epochs is not None:
121+
nb_epochs_fit: int = nb_epochs
122+
elif self.nb_epochs is not None:
123+
nb_epochs_fit = self.nb_epochs
124+
else:
125+
raise ValueError("Please provide value for `nb_epochs`.")
126+
104127
self.trainer.fit(
105-
x, y, validation_data=validation_data, nb_epochs=self.nb_epochs, batch_size=self.batch_size, **kwargs
128+
x, y, validation_data=validation_data, nb_epochs=nb_epochs_fit, batch_size=batch_size_fit, **kwargs
106129
)
107130

108131
def get_classifier(self) -> "CLASSIFIER_LOSS_GRADIENTS_TYPE":

art/estimators/object_detection/python_object_detector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init__(
7474
maximum values allowed for features. If floats are provided, these will be used as the range of all
7575
features. If arrays are provided, each value will be considered the bound for a feature, thus
7676
the shape of clip values needs to match the total number of features.
77-
:param channels_first: Set channels first or last.
77+
:param channels_first: [Currently unused] Set channels first or last.
7878
:param preprocessing_defences: Preprocessing defence(s) to be applied by the classifier.
7979
:param postprocessing_defences: Postprocessing defence(s) to be applied by the classifier.
8080
:param preprocessing: Tuple of the form `(subtrahend, divisor)` of floats or `np.ndarray` of values to be
@@ -214,7 +214,7 @@ def _get_losses(
214214
x_grad.requires_grad = True
215215
else:
216216
x_grad = x[i].to(self.device)
217-
if x_grad.shape[-1] in [1, 3]:
217+
if x_grad.shape[2] < x_grad.shape[0] and x_grad.shape[2] < x_grad.shape[1]:
218218
x_grad = torch.permute(x_grad, (2, 0, 1))
219219

220220
image_tensor_list_grad.append(x_grad)

art/estimators/object_detection/pytorch_faster_rcnn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(
7070
maximum values allowed for features. If floats are provided, these will be used as the range of all
7171
features. If arrays are provided, each value will be considered the bound for a feature, thus
7272
the shape of clip values needs to match the total number of features.
73-
:param channels_first: Set channels first or last.
73+
:param channels_first: [Currently unused] Set channels first or last.
7474
:param preprocessing_defences: Preprocessing defence(s) to be applied by the classifier.
7575
:param postprocessing_defences: Postprocessing defence(s) to be applied by the classifier.
7676
:param preprocessing: Tuple of the form `(subtrahend, divisor)` of floats or `np.ndarray` of values to be

tests/attacks/inference/attribute_inference/test_black_box.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,77 @@ def transform_feature(x):
314314
art_warning(e)
315315

316316

317+
@pytest.mark.skip_framework("dl_frameworks")
318+
@pytest.mark.parametrize("model_type", ["nn", "rf"])
319+
def test_black_box_regressor_label(art_warning, get_diabetes_dataset, model_type):
320+
try:
321+
attack_feature = 0 # age
322+
323+
bins = [
324+
-0.96838121,
325+
-0.77154309,
326+
-0.57470497,
327+
-0.37786684,
328+
-0.18102872,
329+
0.0158094,
330+
0.21264752,
331+
0.40948564,
332+
0.60632376,
333+
0.80316188,
334+
1.0,
335+
]
336+
337+
# need to transform attacked feature into categorical
338+
def transform_feature(x):
339+
for i in range(len(bins) - 1):
340+
x[(x >= bins[i]) & (x <= bins[i + 1])] = i
341+
342+
values = list(range(len(bins) - 1))
343+
344+
(x_train_diabetes, y_train_diabetes), (x_test_diabetes, y_test_diabetes) = get_diabetes_dataset
345+
# training data without attacked feature
346+
x_train_for_attack = np.delete(x_train_diabetes, attack_feature, 1)
347+
# only attacked feature
348+
x_train_feature = x_train_diabetes[:, attack_feature].copy().reshape(-1, 1)
349+
transform_feature(x_train_feature)
350+
# training data with attacked feature (after transformation)
351+
x_train = np.concatenate((x_train_for_attack[:, :attack_feature], x_train_feature), axis=1)
352+
x_train = np.concatenate((x_train, x_train_for_attack[:, attack_feature:]), axis=1)
353+
354+
# test data without attacked feature
355+
x_test_for_attack = np.delete(x_test_diabetes, attack_feature, 1)
356+
# only attacked feature
357+
x_test_feature = x_test_diabetes[:, attack_feature].copy().reshape(-1, 1)
358+
transform_feature(x_test_feature)
359+
360+
from sklearn import linear_model
361+
362+
regr_model = linear_model.LinearRegression()
363+
regr_model.fit(x_train_diabetes, y_train_diabetes)
364+
regressor = ScikitlearnRegressor(regr_model)
365+
366+
attack = AttributeInferenceBlackBox(
367+
regressor, attack_feature=attack_feature, prediction_normal_factor=1 / 250, attack_model_type=model_type
368+
)
369+
# get original model's predictions
370+
x_train_predictions = regressor.predict(x_train_diabetes).reshape(-1, 1)
371+
x_test_predictions = regressor.predict(x_test_diabetes).reshape(-1, 1)
372+
# train attack model
373+
attack.fit(x_train, y=y_train_diabetes)
374+
# infer attacked feature
375+
inferred_train = attack.infer(x_train_for_attack, pred=x_train_predictions, values=values, y=y_train_diabetes)
376+
inferred_test = attack.infer(x_test_for_attack, pred=x_test_predictions, values=values, y=y_test_diabetes)
377+
# check accuracy
378+
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
379+
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
380+
381+
assert pytest.approx(0.0258, abs=0.12) == train_acc
382+
assert pytest.approx(0.0375, abs=0.12) == test_acc
383+
384+
except ARTTestException as e:
385+
art_warning(e)
386+
387+
317388
@pytest.mark.skip_framework("dl_frameworks")
318389
def test_black_box_with_model(art_warning, decision_tree_estimator, get_iris_dataset):
319390
try:

tests/attacks/inference/attribute_inference/test_true_label_baseline.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,46 @@ def transform_feature(x):
183183
art_warning(e)
184184

185185

186+
@pytest.mark.skip_framework("dl_frameworks")
187+
@pytest.mark.parametrize("model_type", ["nn", "rf"])
188+
def test_true_label_baseline_regression(art_warning, get_diabetes_dataset, model_type):
189+
try:
190+
attack_feature = 1 # sex
191+
192+
(x_train, y_train), (x_test, y_test) = get_diabetes_dataset
193+
# training data without attacked feature
194+
x_train_for_attack = np.delete(x_train, attack_feature, 1)
195+
# only attacked feature
196+
x_train_feature = x_train[:, attack_feature].copy().reshape(-1, 1)
197+
198+
# test data without attacked feature
199+
x_test_for_attack = np.delete(x_test, attack_feature, 1)
200+
# only attacked feature
201+
x_test_feature = x_test[:, attack_feature].copy().reshape(-1, 1)
202+
203+
baseline_attack = AttributeInferenceBaselineTrueLabel(
204+
attack_feature=attack_feature, attack_model_type=model_type, is_regression=True
205+
)
206+
# train attack model
207+
baseline_attack.fit(x_train, y_train)
208+
# infer attacked feature
209+
baseline_inferred_train = baseline_attack.infer(x_train_for_attack, y=y_train)
210+
baseline_inferred_test = baseline_attack.infer(x_test_for_attack, y=y_test)
211+
# check accuracy
212+
baseline_train_acc = np.sum(baseline_inferred_train == x_train_feature.reshape(1, -1)) / len(
213+
baseline_inferred_train
214+
)
215+
baseline_test_acc = np.sum(baseline_inferred_test == x_test_feature.reshape(1, -1)) / len(
216+
baseline_inferred_test
217+
)
218+
219+
assert 0.6 <= baseline_train_acc
220+
assert 0.6 <= baseline_test_acc
221+
222+
except ARTTestException as e:
223+
art_warning(e)
224+
225+
186226
def test_check_params(art_warning):
187227
try:
188228
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)