30
30
31
31
import logging
32
32
from itertools import product
33
- from typing import List , Optional , Tuple , Union , TYPE_CHECKING
33
+ from typing import List , Optional , Tuple , TYPE_CHECKING
34
34
35
35
import numpy as np
36
36
39
39
# from scipy.optimize import differential_evolution
40
40
# In the meantime, the modified implementation is used which is defined in the
41
41
# lines `453-1457`.
42
+ # Otherwise may use Tensorflow's implementation of DE.
42
43
43
44
from six import string_types
44
45
from scipy ._lib ._util import check_random_state
60
61
class PixelThreshold (EvasionAttack ):
61
62
"""
62
63
These attacks were originally implemented by Vargas et al. (2019) & Su et al.(2019).
63
-
64
64
| One Pixel Attack Paper link:
65
65
https://ieeexplore.ieee.org/abstract/document/8601309/citations#citations
66
66
(arXiv link: https://arxiv.org/pdf/1710.08864.pdf)
67
67
| Pixel and Threshold Attack Paper link:
68
68
https://arxiv.org/abs/1906.06026
69
69
"""
70
70
71
- attack_params = EvasionAttack .attack_params + ["th" , "es" , "targeted" , "verbose" ]
71
+ attack_params = EvasionAttack .attack_params + ["th" , "es" , "max_iter" , " targeted" , "verbose" , "verbose_es " ]
72
72
_estimator_requirements = (BaseEstimator , NeuralNetworkMixin , ClassifierMixin )
73
73
74
74
def __init__ (
75
75
self ,
76
76
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 ,
80
81
verbose : bool = True ,
82
+ verbose_es : bool = False ,
81
83
) -> None :
82
84
"""
83
85
Create a :class:`.PixelThreshold` instance.
84
-
85
86
:param classifier: A trained classifier.
86
87
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
87
88
: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.
88
90
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
89
91
:param verbose: Print verbose messages of ES and show progress bars.
90
92
"""
@@ -94,8 +96,10 @@ def __init__(
94
96
self .type_attack = - 1
95
97
self .th = th # pylint: disable=C0103
96
98
self .es = es # pylint: disable=C0103
99
+ self .max_iter = max_iter
97
100
self ._targeted = targeted
98
101
self .verbose = verbose
102
+ self .verbose_es = verbose_es
99
103
PixelThreshold ._check_params (self )
100
104
101
105
if self .estimator .channels_first :
@@ -121,15 +125,20 @@ def _check_params(self) -> None:
121
125
if not isinstance (self .verbose , bool ):
122
126
raise ValueError ("The flag `verbose` has to be of type bool." )
123
127
124
- if not isinstance (self .verbose , bool ):
128
+ if not isinstance (self .verbose_es , bool ):
125
129
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." )
126
132
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 :
130
140
"""
131
141
Generate adversarial samples and return them in an array.
132
-
133
142
:param x: An array with the original inputs.
134
143
:param y: Target values (class labels) one-hot-encoded of shape (nb_samples, nb_classes) or indices of shape
135
144
(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
149
158
y = np .argmax (y , axis = 1 )
150
159
151
160
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
+ )
153
166
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].
155
169
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 ])
157
175
x = x * 255.0
158
176
177
+ x = x .astype (np .uint8 )
178
+
159
179
adv_x_best = []
180
+ self .adv_th = []
160
181
for image , target_class in tqdm (zip (x , y ), desc = "Pixel threshold" , disable = not self .verbose ):
182
+
161
183
if self .th is None :
162
- self .min_th = 127
184
+
185
+ min_th = - 1
163
186
start , end = 1 , 127
187
+
188
+ image_result = image
189
+
164
190
while True :
165
- image_result : Union [ List [ np . ndarray ], np . ndarray ] = []
191
+
166
192
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
+
170
195
if success :
196
+ image_result = trial_image_result
171
197
end = threshold - 1
198
+ min_th = threshold
172
199
else :
173
200
start = threshold + 1
174
- if success :
175
- self .min_th = threshold
201
+
176
202
if end < start :
177
- if isinstance (image_result , list ) and not image_result :
178
- # success = False
179
- image_result = image
180
203
break
204
+
205
+ self .adv_th = [min_th ]
206
+
181
207
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
+
183
214
adv_x_best += [image_result ]
184
215
185
216
adv_x_best_array = np .array (adv_x_best )
186
217
187
- if scale_input :
188
- adv_x_best_array = adv_x_best_array / 255.0
189
-
190
218
if y is not None :
191
219
y = to_categorical (y , self .estimator .nb_classes )
192
220
221
+ if self .rescale :
222
+ x = self .rescale_input (x )
223
+ adv_x_best_array = self .rescale_input (adv_x_best_array )
224
+
193
225
logger .info (
194
226
"Success rate of Attack: %.2f%%" ,
195
227
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):
230
262
"""
231
263
Checks whether the given perturbation `adv_x` for the image `img` is successful.
232
264
"""
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 ])
234
271
return bool (
235
272
(self .targeted and predicted_class == target_class )
236
273
or (not self .targeted and predicted_class != target_class )
237
274
)
238
275
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 ]:
242
277
"""
243
278
Attack the given image `image` with the threshold `limit` for the `target_class` which is true label for
244
279
untargeted attack and targeted label for targeted attack.
245
280
"""
246
281
bounds , initial = self ._get_bounds (image , limit )
247
282
248
283
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 ]
250
290
return predictions if not self .targeted else 1 - predictions
251
291
252
292
def callback_fn (x , convergence = None ): # pylint: disable=R1710,W0613
253
293
if self .es == 0 :
254
294
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" )
256
296
else :
257
297
return self ._attack_success (x , image , target_class )
258
298
259
299
if self .es == 0 :
260
300
from cma import CMAOptions
261
301
262
302
opts = CMAOptions ()
263
- if not self .verbose :
303
+ if not self .verbose_es :
264
304
opts .set ("verbose" , - 9 )
265
305
opts .set ("verb_disp" , 40000 )
266
306
opts .set ("verb_log" , 40000 )
@@ -282,18 +322,19 @@ def callback_fn(x, convergence=None): # pylint: disable=R1710,W0613
282
322
predict_fn ,
283
323
maxfun = max (1 , 400 // len (bounds )) * len (bounds ) * 100 ,
284
324
callback = callback_fn ,
285
- iterations = max_iter ,
325
+ iterations = self . max_iter ,
286
326
)
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 )
289
330
290
331
adv_x = strategy .result [0 ]
291
332
else :
292
333
strategy = differential_evolution (
293
334
predict_fn ,
294
335
bounds ,
295
- disp = self .verbose ,
296
- maxiter = max_iter ,
336
+ disp = self .verbose_es ,
337
+ maxiter = self . max_iter ,
297
338
popsize = max (1 , 400 // len (bounds )),
298
339
recombination = 1 ,
299
340
atol = - 1 ,
@@ -312,7 +353,6 @@ class PixelAttack(PixelThreshold):
312
353
"""
313
354
This attack was originally implemented by Vargas et al. (2019). It is generalisation of One Pixel Attack originally
314
355
implemented by Su et al. (2019).
315
-
316
356
| One Pixel Attack Paper link:
317
357
https://ieeexplore.ieee.org/abstract/document/8601309/citations#citations
318
358
(arXiv link: https://arxiv.org/pdf/1710.08864.pdf)
@@ -324,20 +364,21 @@ def __init__(
324
364
self ,
325
365
classifier : "CLASSIFIER_NEURALNETWORK_TYPE" ,
326
366
th : Optional [int ] = None ,
327
- es : int = 0 ,
367
+ es : int = 1 ,
368
+ max_iter : int = 100 ,
328
369
targeted : bool = False ,
329
370
verbose : bool = False ,
330
371
) -> None :
331
372
"""
332
373
Create a :class:`.PixelAttack` instance.
333
-
334
374
:param classifier: A trained classifier.
335
375
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
336
376
: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.
337
378
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
338
379
:param verbose: Indicates whether to print verbose messages of ES used.
339
380
"""
340
- super ().__init__ (classifier , th , es , targeted , verbose )
381
+ super ().__init__ (classifier , th , es , max_iter , targeted , verbose )
341
382
self .type_attack = 0
342
383
343
384
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]:
395
436
class ThresholdAttack (PixelThreshold ):
396
437
"""
397
438
This attack was originally implemented by Vargas et al. (2019).
398
-
399
439
| Paper link:
400
440
https://arxiv.org/abs/1906.06026
401
441
"""
@@ -405,19 +445,20 @@ def __init__(
405
445
classifier : "CLASSIFIER_NEURALNETWORK_TYPE" ,
406
446
th : Optional [int ] = None ,
407
447
es : int = 0 ,
448
+ max_iter : int = 100 ,
408
449
targeted : bool = False ,
409
450
verbose : bool = False ,
410
451
) -> None :
411
452
"""
412
453
Create a :class:`.PixelThreshold` instance.
413
-
414
454
:param classifier: A trained classifier.
415
455
:param th: threshold value of the Pixel/ Threshold attack. th=None indicates finding a minimum threshold.
416
456
: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.
417
458
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
418
459
:param verbose: Indicates whether to print verbose messages of ES used.
419
460
"""
420
- super ().__init__ (classifier , th , es , targeted , verbose )
461
+ super ().__init__ (classifier , th , es , max_iter , targeted , verbose )
421
462
self .type_attack = 1
422
463
423
464
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:
440
481
return imgs
441
482
442
483
484
+ class CMAEarlyStoppingException (Exception ):
485
+ """Raised when CMA is stopping early after successful optimisation."""
486
+
487
+ pass
488
+
489
+
443
490
# TODO: Make the attack compatible with current version of SciPy Optimize
444
491
# Differential Evolution
445
492
# pylint: disable=W0105
@@ -448,9 +495,7 @@ def _perturb_image(self, x: np.ndarray, img: np.ndarray) -> np.ndarray:
448
495
To speed up predictions, the entire parameters array is passed to `self.func`,
449
496
where a neural network model can batch its computations and execute in parallel
450
497
Search for `CHANGES` to find all code changes.
451
-
452
498
Dan Kondratyuk 2018
453
-
454
499
Original code adapted from
455
500
https://github.com/scipy/scipy/blob/70e61dee181de23fdd8d893eaa9491100e2218d7/scipy/optimize/_differentialevolution.py
456
501
----------
0 commit comments