Skip to content

Commit 86baa09

Browse files
authored
Merge pull request #1118 from TrojAISec/dev_1.7.0
Support for Binary Classification with PGD
2 parents 9104486 + c941a95 commit 86baa09

File tree

16 files changed

+212
-55
lines changed

16 files changed

+212
-55
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
- AGH University of Science and Technology
1515
- Rensselaer Polytechnic Institute (RPI)
1616
- IMT Atlantique
17+
- Troj.AI

art/attacks/evasion/fast_gradient.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
231231
# Use model predictions as correct outputs
232232
logger.info("Using model predictions as correct labels for FGM.")
233233
y = get_labels_np_array(self.estimator.predict(x, batch_size=self.batch_size)) # type: ignore
234-
y = y / np.sum(y, axis=1, keepdims=True)
234+
235+
if self.estimator.nb_classes > 2:
236+
y = y / np.sum(y, axis=1, keepdims=True)
235237

236238
# Return adversarial examples computed with minimal perturbation if option is active
237239
rate_best: Optional[float]

art/attacks/inference/attribute_inference/meminf_based.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
Create an AttributeInferenceMembership attack instance.
5757
5858
:param classifier: Target classifier.
59-
:param membership_attack: The membership inference attack to use. Should be fit/callibrated in advance, and
59+
:param membership_attack: The membership inference attack to use. Should be fit/calibrated in advance, and
6060
should support returning probabilities.
6161
:param attack_feature: The index of the feature to be attacked or a slice representing multiple indexes in
6262
case of a one-hot encoded feature.
@@ -106,10 +106,10 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
106106

107107
predicted = self.membership_attack.infer(x_value, y, probabilities=True)
108108
if first:
109-
probabilities = predicted[:, 1].reshape(-1, 1)
109+
probabilities = predicted
110110
first = False
111111
else:
112-
probabilities = np.hstack((probabilities, predicted[:, 1].reshape(-1, 1)))
112+
probabilities = np.hstack((probabilities, predicted))
113113

114114
# needs to be of type float so we can later replace back the actual values
115115
value_indexes = np.argmax(probabilities, axis=1).astype(np.float32)
@@ -130,9 +130,9 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
130130

131131
predicted = self.membership_attack.infer(x_value, y, probabilities=True)
132132
if first:
133-
probabilities = predicted[:, 1].reshape(-1, 1)
133+
probabilities = predicted
134134
else:
135-
probabilities = np.hstack((probabilities, predicted[:, 1].reshape(-1, 1)))
135+
probabilities = np.hstack((probabilities, predicted))
136136
first = False
137137
value_indexes = np.argmax(probabilities, axis=1).astype(np.float32)
138138
pred_values = np.zeros_like(probabilities)

art/attacks/inference/membership_inference/black_box.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,9 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
292292

293293
if inferred is not None:
294294
if not probabilities:
295-
inferred_return = inferred.reshape(-1).astype(np.int)
295+
inferred_return = np.round(inferred)
296296
else:
297-
inferred = inferred.reshape(-1)
298-
prob_0 = np.ones_like(inferred) - inferred
299-
inferred_return = np.stack((prob_0, inferred), axis=1)
297+
inferred_return = inferred
300298
else:
301299
raise ValueError("No data available.")
302300
elif not self.default_model:
@@ -305,13 +303,13 @@ def infer(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.n
305303
if probabilities:
306304
inferred_return = pred
307305
else:
308-
inferred_return = np.array([np.argmax(arr) for arr in pred])
306+
inferred_return = np.round(pred)
309307
else:
310308
pred = self.attack_model.predict_proba(np.c_[features, y]) # type: ignore
311309
if probabilities:
312-
inferred_return = pred
310+
inferred_return = pred[:, [1]]
313311
else:
314-
inferred_return = np.array([np.argmax(arr) for arr in pred])
312+
inferred_return = np.round(pred[:, [1]])
315313

316314
return inferred_return
317315

art/estimators/classification/classifier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def nb_classes(self) -> int:
105105
106106
:return: Number of classes in the data.
107107
"""
108+
if self._nb_classes < 2:
109+
raise ValueError("nb_classes must be greater than or equal to 2.")
108110
return self._nb_classes # type: ignore
109111

110112

art/estimators/classification/keras.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ def _initialize_params(
178178
self._output_layer = 0
179179

180180
_, self._nb_classes = k.int_shape(self._output)
181+
# Check for binary classification
182+
if self._nb_classes == 1:
183+
self._nb_classes = 2
181184
self._input_shape = k.int_shape(self._input)[1:]
182185
logger.debug(
183186
"Inferred %i classes and %s as input shape for Keras classifier.",

art/estimators/classification/pytorch.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from art.utils import check_and_transform_label_format
4141

4242
if TYPE_CHECKING:
43-
# pylint: disable=C0412
43+
# pylint: disable=C0412, C0302
4444
import torch
4545

4646
from art.utils import CLIP_VALUES_TYPE, PREPROCESSING_TYPE
@@ -266,21 +266,26 @@ def reduce_labels(self, y: Union[np.ndarray, "torch.Tensor"]) -> Union[np.ndarra
266266
"""
267267
Reduce labels from one-hot encoded to index labels.
268268
"""
269+
# pylint: disable=R0911
269270
import torch # lgtm [py/repeated-import]
270271

271272
# Check if the loss function requires as input index labels instead of one-hot-encoded labels
272-
if self._reduce_labels and self._int_labels:
273-
if isinstance(y, torch.Tensor):
274-
return torch.argmax(y, dim=1)
275-
return np.argmax(y, axis=1)
276-
277-
if self._reduce_labels: # float labels
273+
# Checking for exactly 2 classes to support binary classification
274+
if self.nb_classes > 2:
275+
if self._reduce_labels and self._int_labels:
276+
if isinstance(y, torch.Tensor):
277+
return torch.argmax(y, dim=1)
278+
return np.argmax(y, axis=1)
279+
if self._reduce_labels: # float labels
280+
if isinstance(y, torch.Tensor):
281+
return torch.argmax(y, dim=1).type("torch.FloatTensor")
282+
y_index = np.argmax(y, axis=1).astype(np.float32)
283+
y_index = np.expand_dims(y_index, axis=1)
284+
return y_index
285+
else:
278286
if isinstance(y, torch.Tensor):
279-
return torch.argmax(y, dim=1).type("torch.FloatTensor")
280-
y_index = np.argmax(y, axis=1).astype(np.float32)
281-
y_index = np.expand_dims(y_index, axis=1)
282-
return y_index
283-
287+
return y.float()
288+
return y.astype(np.float32)
284289
return y
285290

286291
def predict( # pylint: disable=W0221
@@ -302,8 +307,9 @@ def predict( # pylint: disable=W0221
302307
# Apply preprocessing
303308
x_preprocessed, _ = self._apply_preprocessing(x, y=None, fit=False)
304309

310+
results_list = []
311+
305312
# Run prediction with batch processing
306-
results = np.zeros((x_preprocessed.shape[0], self.nb_classes), dtype=np.float32)
307313
num_batch = int(np.ceil(len(x_preprocessed) / float(batch_size)))
308314
for m in range(num_batch):
309315
# Batch indexes
@@ -315,8 +321,13 @@ def predict( # pylint: disable=W0221
315321
with torch.no_grad():
316322
model_outputs = self._model(torch.from_numpy(x_preprocessed[begin:end]).to(self._device))
317323
output = model_outputs[-1]
318-
results[begin:end] = output.detach().cpu().numpy()
324+
output = output.detach().cpu().numpy().astype(np.float32)
325+
if len(output.shape) == 1:
326+
output = np.expand_dims(output.detach().cpu().numpy(), axis=1).astype(np.float32)
327+
328+
results_list.append(output)
319329

330+
results = np.vstack(results_list)
320331
# Apply postprocessing
321332
predictions = self._apply_postprocessing(preds=results, fit=False)
322333

@@ -577,7 +588,12 @@ def hook(grad):
577588

578589
self._model.zero_grad()
579590
if label is None:
580-
for i in range(self.nb_classes):
591+
if len(preds.shape) == 1 or preds.shape[1] == 1:
592+
num_outputs = 1
593+
else:
594+
num_outputs = self.nb_classes
595+
596+
for i in range(num_outputs):
581597
torch.autograd.backward(
582598
preds[:, i],
583599
torch.tensor([1.0] * len(preds[:, 0])).to(self._device),

art/estimators/classification/tensorflow.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,9 @@ def class_gradient( # pylint: disable=W0221
10371037

10381038
class_gradient = tape.gradient(prediction, x_input).numpy()
10391039
class_gradients.append(class_gradient)
1040+
# Break after 1 iteration for binary classification case
1041+
if len(predictions.shape) == 1 or predictions.shape[1] == 1:
1042+
break
10401043

10411044
gradients = np.swapaxes(np.array(class_gradients), 0, 1)
10421045

art/utils.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -528,13 +528,18 @@ def check_and_transform_label_format(
528528
if len(labels.shape) == 2 and labels.shape[1] > 1:
529529
if not return_one_hot:
530530
labels = np.argmax(labels, axis=1)
531-
elif len(labels.shape) == 2 and labels.shape[1] == 1:
531+
elif len(labels.shape) == 2 and labels.shape[1] == 1 and nb_classes is not None and nb_classes > 2:
532532
labels = np.squeeze(labels)
533533
if return_one_hot:
534534
labels = to_categorical(labels, nb_classes)
535+
elif len(labels.shape) == 2 and labels.shape[1] == 1 and nb_classes is not None and nb_classes == 2:
536+
pass
535537
elif len(labels.shape) == 1:
536538
if return_one_hot:
537-
labels = to_categorical(labels, nb_classes)
539+
if nb_classes == 2:
540+
labels = np.expand_dims(labels, axis=1)
541+
else:
542+
labels = to_categorical(labels, nb_classes)
538543
else:
539544
raise ValueError(
540545
"Shape of labels not recognised."
@@ -616,7 +621,10 @@ def get_labels_np_array(preds: np.ndarray) -> np.ndarray:
616621
:param preds: Array of class confidences, nb of instances as first dimension.
617622
:return: Labels.
618623
"""
619-
preds_max = np.amax(preds, axis=1, keepdims=True)
624+
if len(preds.shape) >= 2:
625+
preds_max = np.amax(preds, axis=1, keepdims=True)
626+
else:
627+
preds_max = np.round(preds)
620628
y = preds == preds_max
621629
y = y.astype(np.uint8)
622630
return y
@@ -642,11 +650,19 @@ def compute_success_array(
642650
:param batch_size: Batch size.
643651
:return: Percentage of successful adversarial samples.
644652
"""
645-
adv_preds = np.argmax(classifier.predict(x_adv, batch_size=batch_size), axis=1)
653+
adv_preds = classifier.predict(x_adv, batch_size=batch_size)
654+
if len(adv_preds.shape) >= 2:
655+
adv_preds = np.argmax(adv_preds, axis=1)
656+
else:
657+
adv_preds = np.round(adv_preds)
646658
if targeted:
647659
attack_success = adv_preds == np.argmax(labels, axis=1)
648660
else:
649-
preds = np.argmax(classifier.predict(x_clean, batch_size=batch_size), axis=1)
661+
preds = classifier.predict(x_clean, batch_size=batch_size)
662+
if len(preds.shape) >= 2:
663+
preds = np.argmax(preds, axis=1)
664+
else:
665+
preds = np.round(preds)
650666
attack_success = adv_preds != preds
651667

652668
return attack_success

tests/attacks/inference/attribute_inference/test_meminf_based.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ def transform_feature(x):
187187
inferred_train = attack.infer(x_train_for_attack, y_train_iris, values=values)
188188
inferred_test = attack.infer(x_test_for_attack, y_test_iris, values=values)
189189
# check accuracy
190-
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
191-
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
190+
train_acc = np.sum(inferred_train == x_train_feature) / len(inferred_train)
191+
test_acc = np.sum(inferred_test == x_test_feature) / len(inferred_test)
192192
assert 0.1 <= train_acc
193193
assert 0.1 <= test_acc
194194

@@ -325,18 +325,18 @@ def transform_feature(x):
325325
attack_train_ratio = 0.5
326326
attack_train_size = int(len(x_train) * attack_train_ratio)
327327
attack_test_size = int(len(x_test) * attack_train_ratio)
328-
# attack without callibration
328+
# attack without calibration
329329
attack = AttributeInferenceMembership(classifier, meminf_attack, attack_feature=attack_feature)
330330
# infer attacked feature
331331
inferred_train = attack.infer(x_train_for_attack, y_train_iris, values=values)
332332
inferred_test = attack.infer(x_test_for_attack, y_test_iris, values=values)
333333
# check accuracy
334-
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
335-
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
334+
train_acc = np.sum(inferred_train == x_train_feature) / len(inferred_train)
335+
test_acc = np.sum(inferred_test == x_test_feature) / len(inferred_test)
336336
assert 0.5 <= train_acc
337337
assert 0.5 <= test_acc
338338

339-
# attack with callibration
339+
# attack with calibration
340340
meminf_attack.calibrate_distance_threshold(
341341
x_train[:attack_train_size],
342342
y_train_iris[:attack_train_size],
@@ -349,8 +349,8 @@ def transform_feature(x):
349349
inferred_train = attack.infer(x_train_for_attack, y_train_iris, values=values)
350350
inferred_test = attack.infer(x_test_for_attack, y_test_iris, values=values)
351351
# check accuracy
352-
train_acc = np.sum(inferred_train == x_train_feature.reshape(1, -1)) / len(inferred_train)
353-
test_acc = np.sum(inferred_test == x_test_feature.reshape(1, -1)) / len(inferred_test)
352+
train_acc = np.sum(inferred_train == x_train_feature) / len(inferred_train)
353+
test_acc = np.sum(inferred_test == x_test_feature) / len(inferred_test)
354354
assert 0.1 <= train_acc
355355
assert 0.1 <= test_acc
356356

0 commit comments

Comments
 (0)