3030
3131import logging
3232from itertools import product
33- from typing import List , Optional , Tuple , Union , TYPE_CHECKING
33+ from typing import List , Optional , Tuple , TYPE_CHECKING
3434
3535import numpy as np
3636
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
4344from six import string_types
4445from scipy ._lib ._util import check_random_state
6061class 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]:
395436class 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:
448495To speed up predictions, the entire parameters array is passed to `self.func`,
449496where a neural network model can batch its computations and execute in parallel
450497Search for `CHANGES` to find all code changes.
451-
452498Dan Kondratyuk 2018
453-
454499Original code adapted from
455500https://github.com/scipy/scipy/blob/70e61dee181de23fdd8d893eaa9491100e2218d7/scipy/optimize/_differentialevolution.py
456501----------
0 commit comments