Skip to content

Commit 9104486

Browse files
authored
Merge pull request Trusted-AI#990 from shashankkotyan/master
Fixes Bug in Pixel Attack and Threshold Attack.
2 parents eee356f + 61abdaa commit 9104486

File tree

3 files changed

+99
-54
lines changed

3 files changed

+99
-54
lines changed

art/attacks/evasion/pixel_threshold.py

+95-50
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import logging
3232
from itertools import product
33-
from typing import List, Optional, Tuple, Union, TYPE_CHECKING
33+
from typing import List, Optional, Tuple, TYPE_CHECKING
3434

3535
import numpy as np
3636

@@ -39,6 +39,7 @@
3939
# from scipy.optimize import differential_evolution
4040
# In the meantime, the modified implementation is used which is defined in the
4141
# lines `453-1457`.
42+
# Otherwise may use Tensorflow's implementation of DE.
4243

4344
from six import string_types
4445
from scipy._lib._util import check_random_state
@@ -60,31 +61,32 @@
6061
class PixelThreshold(EvasionAttack):
6162
"""
6263
These attacks were originally implemented by Vargas et al. (2019) & Su et al.(2019).
63-
6464
| One Pixel Attack Paper link:
6565
https://ieeexplore.ieee.org/abstract/document/8601309/citations#citations
6666
(arXiv link: https://arxiv.org/pdf/1710.08864.pdf)
6767
| Pixel and Threshold Attack Paper link:
6868
https://arxiv.org/abs/1906.06026
6969
"""
7070

71-
attack_params = EvasionAttack.attack_params + ["th", "es", "targeted", "verbose"]
71+
attack_params = EvasionAttack.attack_params + ["th", "es", "max_iter", "targeted", "verbose", "verbose_es"]
7272
_estimator_requirements = (BaseEstimator, NeuralNetworkMixin, ClassifierMixin)
7373

7474
def __init__(
7575
self,
7676
classifier: "CLASSIFIER_NEURALNETWORK_TYPE",
77-
th: Optional[int],
78-
es: int,
79-
targeted: bool,
77+
th: Optional[int] = None,
78+
es: int = 0,
79+
max_iter: int = 100,
80+
targeted: bool = False,
8081
verbose: bool = True,
82+
verbose_es: bool = False,
8183
) -> None:
8284
"""
8385
Create a :class:`.PixelThreshold` instance.
84-
8586
:param classifier: A trained classifier.
8687
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
8788
:param es: Indicates whether the attack uses CMAES (0) or DE (1) as Evolutionary Strategy.
89+
:param max_iter: Sets the Maximum iterations to run the Evolutionary Strategies for optimisation.
8890
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
8991
:param verbose: Print verbose messages of ES and show progress bars.
9092
"""
@@ -94,8 +96,10 @@ def __init__(
9496
self.type_attack = -1
9597
self.th = th # pylint: disable=C0103
9698
self.es = es # pylint: disable=C0103
99+
self.max_iter = max_iter
97100
self._targeted = targeted
98101
self.verbose = verbose
102+
self.verbose_es = verbose_es
99103
PixelThreshold._check_params(self)
100104

101105
if self.estimator.channels_first:
@@ -121,15 +125,20 @@ def _check_params(self) -> None:
121125
if not isinstance(self.verbose, bool):
122126
raise ValueError("The flag `verbose` has to be of type bool.")
123127

124-
if not isinstance(self.verbose, bool):
128+
if not isinstance(self.verbose_es, bool):
125129
raise ValueError("The argument `verbose` has to be of type bool.")
130+
if self.estimator.clip_values is None:
131+
raise ValueError("This attack requires estimator clip values to be defined.")
126132

127-
def generate( # pylint: disable=W0221
128-
self, x: np.ndarray, y: Optional[np.ndarray] = None, max_iter: int = 100, **kwargs
129-
) -> np.ndarray:
133+
def rescale_input(self, x):
134+
"""Rescale inputs"""
135+
x = x.astype(np.float32) / 255.0
136+
x = (x * (self.estimator.clip_values[1] - self.estimator.clip_values[0])) + self.estimator.clip_values[0]
137+
return x
138+
139+
def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
130140
"""
131141
Generate adversarial samples and return them in an array.
132-
133142
:param x: An array with the original inputs.
134143
:param y: Target values (class labels) one-hot-encoded of shape (nb_samples, nb_classes) or indices of shape
135144
(nb_samples,). Only provide this parameter if you'd like to use true labels when crafting adversarial
@@ -149,47 +158,70 @@ def generate( # pylint: disable=W0221
149158
y = np.argmax(y, axis=1)
150159

151160
if self.th is None:
152-
logger.info("Performing minimal perturbation Attack.")
161+
logger.info(
162+
"Performing minimal perturbation Attack. \
163+
This takes substainally long time to process. \
164+
For sanity check, pass th=10 to the Attack instance."
165+
)
153166

154-
scale_input = bool(np.max(x) <= 1)
167+
# NOTE: Pixel and Threshold Attacks are well defined for unprocessed images where the pixel values are,
168+
# 8-Bit color i.e., the pixel values are np.uint8 in range [0, 255].
155169

156-
if scale_input:
170+
# TO-DO: Better checking of input image.
171+
# All other cases not tested needs the images to be rescaled to [0, 255].
172+
if self.estimator.clip_values[1] != 255.0:
173+
self.rescale = True
174+
x = (x - self.estimator.clip_values[0]) / (self.estimator.clip_values[1] - self.estimator.clip_values[0])
157175
x = x * 255.0
158176

177+
x = x.astype(np.uint8)
178+
159179
adv_x_best = []
180+
self.adv_th = []
160181
for image, target_class in tqdm(zip(x, y), desc="Pixel threshold", disable=not self.verbose):
182+
161183
if self.th is None:
162-
self.min_th = 127
184+
185+
min_th = -1
163186
start, end = 1, 127
187+
188+
image_result = image
189+
164190
while True:
165-
image_result: Union[List[np.ndarray], np.ndarray] = []
191+
166192
threshold = (start + end) // 2
167-
success, trial_image_result = self._attack(image, target_class, threshold, max_iter)
168-
if image_result or success:
169-
image_result = trial_image_result
193+
success, trial_image_result = self._attack(image, target_class, threshold)
194+
170195
if success:
196+
image_result = trial_image_result
171197
end = threshold - 1
198+
min_th = threshold
172199
else:
173200
start = threshold + 1
174-
if success:
175-
self.min_th = threshold
201+
176202
if end < start:
177-
if isinstance(image_result, list) and not image_result:
178-
# success = False
179-
image_result = image
180203
break
204+
205+
self.adv_th = [min_th]
206+
181207
else:
182-
success, image_result = self._attack(image, target_class, self.th, max_iter)
208+
209+
success, image_result = self._attack(image, target_class, self.th)
210+
211+
if not success:
212+
image_result = image
213+
183214
adv_x_best += [image_result]
184215

185216
adv_x_best_array = np.array(adv_x_best)
186217

187-
if scale_input:
188-
adv_x_best_array = adv_x_best_array / 255.0
189-
190218
if y is not None:
191219
y = to_categorical(y, self.estimator.nb_classes)
192220

221+
if self.rescale:
222+
x = self.rescale_input(x)
223+
adv_x_best_array = self.rescale_input(adv_x_best_array)
224+
193225
logger.info(
194226
"Success rate of Attack: %.2f%%",
195227
100 * compute_success(self.estimator, x, y, adv_x_best_array, self.targeted, 1),
@@ -230,37 +262,45 @@ def _attack_success(self, adv_x, x, target_class):
230262
"""
231263
Checks whether the given perturbation `adv_x` for the image `img` is successful.
232264
"""
233-
predicted_class = np.argmax(self.estimator.predict(self._perturb_image(adv_x, x))[0])
265+
adv = self._perturb_image(adv_x, x)
266+
267+
if self.rescale:
268+
adv = self.rescale_input(adv)
269+
270+
predicted_class = np.argmax(self.estimator.predict(adv)[0])
234271
return bool(
235272
(self.targeted and predicted_class == target_class)
236273
or (not self.targeted and predicted_class != target_class)
237274
)
238275

239-
def _attack(
240-
self, image: np.ndarray, target_class: np.ndarray, limit: int, max_iter: int
241-
) -> Tuple[bool, np.ndarray]:
276+
def _attack(self, image: np.ndarray, target_class: np.ndarray, limit: int) -> Tuple[bool, np.ndarray]:
242277
"""
243278
Attack the given image `image` with the threshold `limit` for the `target_class` which is true label for
244279
untargeted attack and targeted label for targeted attack.
245280
"""
246281
bounds, initial = self._get_bounds(image, limit)
247282

248283
def predict_fn(x):
249-
predictions = self.estimator.predict(self._perturb_image(x, image))[:, target_class]
284+
adv = self._perturb_image(x, image)
285+
286+
if self.rescale:
287+
adv = self.rescale_input(adv)
288+
289+
predictions = self.estimator.predict(adv)[:, target_class]
250290
return predictions if not self.targeted else 1 - predictions
251291

252292
def callback_fn(x, convergence=None): # pylint: disable=R1710,W0613
253293
if self.es == 0:
254294
if self._attack_success(x.result[0], image, target_class):
255-
raise Exception("Attack Completed :) Earlier than expected")
295+
raise CMAEarlyStoppingException("Attack Completed :) Earlier than expected")
256296
else:
257297
return self._attack_success(x, image, target_class)
258298

259299
if self.es == 0:
260300
from cma import CMAOptions
261301

262302
opts = CMAOptions()
263-
if not self.verbose:
303+
if not self.verbose_es:
264304
opts.set("verbose", -9)
265305
opts.set("verb_disp", 40000)
266306
opts.set("verb_log", 40000)
@@ -282,18 +322,19 @@ def callback_fn(x, convergence=None): # pylint: disable=R1710,W0613
282322
predict_fn,
283323
maxfun=max(1, 400 // len(bounds)) * len(bounds) * 100,
284324
callback=callback_fn,
285-
iterations=max_iter,
325+
iterations=self.max_iter,
286326
)
287-
except Exception as exception: # pylint: disable=W0703
288-
logger.info(exception)
327+
except CMAEarlyStoppingException as err:
328+
if self.verbose_es:
329+
logger.info(err)
289330

290331
adv_x = strategy.result[0]
291332
else:
292333
strategy = differential_evolution(
293334
predict_fn,
294335
bounds,
295-
disp=self.verbose,
296-
maxiter=max_iter,
336+
disp=self.verbose_es,
337+
maxiter=self.max_iter,
297338
popsize=max(1, 400 // len(bounds)),
298339
recombination=1,
299340
atol=-1,
@@ -312,7 +353,6 @@ class PixelAttack(PixelThreshold):
312353
"""
313354
This attack was originally implemented by Vargas et al. (2019). It is generalisation of One Pixel Attack originally
314355
implemented by Su et al. (2019).
315-
316356
| One Pixel Attack Paper link:
317357
https://ieeexplore.ieee.org/abstract/document/8601309/citations#citations
318358
(arXiv link: https://arxiv.org/pdf/1710.08864.pdf)
@@ -324,20 +364,21 @@ def __init__(
324364
self,
325365
classifier: "CLASSIFIER_NEURALNETWORK_TYPE",
326366
th: Optional[int] = None,
327-
es: int = 0,
367+
es: int = 1,
368+
max_iter: int = 100,
328369
targeted: bool = False,
329370
verbose: bool = False,
330371
) -> None:
331372
"""
332373
Create a :class:`.PixelAttack` instance.
333-
334374
:param classifier: A trained classifier.
335375
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
336376
:param es: Indicates whether the attack uses CMAES (0) or DE (1) as Evolutionary Strategy.
377+
:param max_iter: Sets the Maximum iterations to run the Evolutionary Strategies for optimisation.
337378
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
338379
:param verbose: Indicates whether to print verbose messages of ES used.
339380
"""
340-
super().__init__(classifier, th, es, targeted, verbose)
381+
super().__init__(classifier, th, es, max_iter, targeted, verbose)
341382
self.type_attack = 0
342383

343384
def _perturb_image(self, x: np.ndarray, img: np.ndarray) -> np.ndarray:
@@ -395,7 +436,6 @@ def _get_bounds(self, img: np.ndarray, limit) -> Tuple[List[list], list]:
395436
class ThresholdAttack(PixelThreshold):
396437
"""
397438
This attack was originally implemented by Vargas et al. (2019).
398-
399439
| Paper link:
400440
https://arxiv.org/abs/1906.06026
401441
"""
@@ -405,19 +445,20 @@ def __init__(
405445
classifier: "CLASSIFIER_NEURALNETWORK_TYPE",
406446
th: Optional[int] = None,
407447
es: int = 0,
448+
max_iter: int = 100,
408449
targeted: bool = False,
409450
verbose: bool = False,
410451
) -> None:
411452
"""
412453
Create a :class:`.PixelThreshold` instance.
413-
414454
:param classifier: A trained classifier.
415455
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
416456
:param es: Indicates whether the attack uses CMAES (0) or DE (1) as Evolutionary Strategy.
457+
:param max_iter: Sets the Maximum iterations to run the Evolutionary Strategies for optimisation.
417458
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
418459
:param verbose: Indicates whether to print verbose messages of ES used.
419460
"""
420-
super().__init__(classifier, th, es, targeted, verbose)
461+
super().__init__(classifier, th, es, max_iter, targeted, verbose)
421462
self.type_attack = 1
422463

423464
def _perturb_image(self, x: np.ndarray, img: np.ndarray) -> np.ndarray:
@@ -440,6 +481,12 @@ def _perturb_image(self, x: np.ndarray, img: np.ndarray) -> np.ndarray:
440481
return imgs
441482

442483

484+
class CMAEarlyStoppingException(Exception):
485+
"""Raised when CMA is stopping early after successful optimisation."""
486+
487+
pass
488+
489+
443490
# TODO: Make the attack compatible with current version of SciPy Optimize
444491
# Differential Evolution
445492
# pylint: disable=W0105
@@ -448,9 +495,7 @@ def _perturb_image(self, x: np.ndarray, img: np.ndarray) -> np.ndarray:
448495
To speed up predictions, the entire parameters array is passed to `self.func`,
449496
where a neural network model can batch its computations and execute in parallel
450497
Search for `CHANGES` to find all code changes.
451-
452498
Dan Kondratyuk 2018
453-
454499
Original code adapted from
455500
https://github.com/scipy/scipy/blob/70e61dee181de23fdd8d893eaa9491100e2218d7/scipy/optimize/_differentialevolution.py
456501
----------

tests/attacks/test_pixel_attack.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def _test_attack(self, classifier, x_test, y_test, targeted):
136136
targets = y_test
137137

138138
for es in [1]: # Option 0 is not easy to reproduce reliably, we should consider it at a later time
139-
df = PixelAttack(classifier, th=64, es=es, targeted=targeted, verbose=False)
140-
x_test_adv = df.generate(x_test_original, targets, max_iter=10)
139+
df = PixelAttack(classifier, th=64, es=es, max_iter=10, targeted=targeted, verbose=False)
140+
x_test_adv = df.generate(x_test_original, targets)
141141

142142
np.testing.assert_raises(AssertionError, np.testing.assert_array_equal, x_test, x_test_adv)
143143
self.assertFalse((0.0 == x_test_adv).all())

tests/attacks/test_threshold_attack.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ def _test_attack(self, classifier, x_test, y_test, targeted):
127127
targets = y_test
128128

129129
for es in [1]: # Option 0 is not easy to reproduce reliably, we should consider it at a later time
130-
df = ThresholdAttack(classifier, th=128, es=es, targeted=targeted, verbose=False)
131-
x_test_adv = df.generate(x_test_original, targets, max_iter=10)
130+
df = ThresholdAttack(classifier, th=128, es=es, max_iter=10, targeted=targeted, verbose=False)
131+
x_test_adv = df.generate(x_test_original, targets)
132132

133133
np.testing.assert_raises(AssertionError, np.testing.assert_array_equal, x_test, x_test_adv)
134134
self.assertFalse((0.0 == x_test_adv).all())

0 commit comments

Comments
 (0)