Skip to content

Commit 1db43b4

Browse files
author
Beat Buesser
committed
Merge branch 'dev_1.5.0' of github.com:Trusted-AI/adversarial-robustness-toolbox into dev_1.5.0
2 parents 1594b25 + d536caf commit 1db43b4

30 files changed

+1430
-172
lines changed

art/attacks/evasion/fast_gradient.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -191,27 +191,6 @@ def _minimal_perturbation(self, x: np.ndarray, y: np.ndarray, mask: np.ndarray)
191191

192192
return adv_x
193193

194-
@staticmethod
195-
def _get_mask(x: np.ndarray, **kwargs) -> np.ndarray:
196-
"""
197-
Get the mask from the kwargs.
198-
199-
:param x: An array with the original inputs.
200-
:param mask: An array with a mask to be applied to the adversarial perturbations. Shape needs to be
201-
broadcastable to the shape of x. Any features for which the mask is zero will not be adversarially
202-
perturbed.
203-
:type mask: `np.ndarray`
204-
:return: The mask.
205-
"""
206-
mask = kwargs.get("mask")
207-
208-
if mask is not None:
209-
# Ensure the mask is broadcastable
210-
if len(mask.shape) > len(x.shape) or mask.shape != x.shape[-len(mask.shape) :]:
211-
raise ValueError("Mask shape must be broadcastable to input shape.")
212-
213-
return mask
214-
215194
def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
216195
"""Generate adversarial samples and return them in an array.
217196
@@ -226,9 +205,7 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
226205
:type mask: `np.ndarray`
227206
:return: An array holding the adversarial examples.
228207
"""
229-
mask = kwargs.get("mask")
230-
if mask is not None and mask.ndim > x.ndim:
231-
raise ValueError("Mask shape must be broadcastable to input shape.")
208+
mask = self._get_mask(x, **kwargs)
232209

233210
# Ensure eps is broadcastable
234211
self._check_compatibility_input_and_eps(x=x)
@@ -355,13 +332,19 @@ def _check_params(self) -> None:
355332
if not isinstance(self.minimal, bool):
356333
raise ValueError("The flag `minimal` has to be of type bool.")
357334

358-
def _compute_perturbation(self, batch: np.ndarray, batch_labels: np.ndarray, mask: np.ndarray) -> np.ndarray:
335+
def _compute_perturbation(
336+
self, batch: np.ndarray, batch_labels: np.ndarray, mask: Optional[np.ndarray]
337+
) -> np.ndarray:
359338
# Pick a small scalar to avoid division by 0
360339
tol = 10e-8
361340

362341
# Get gradient wrt loss; invert it if attack is targeted
363342
grad = self.estimator.loss_gradient(batch, batch_labels) * (1 - 2 * int(self.targeted))
364343

344+
# Apply mask
345+
if mask is not None:
346+
grad = np.where(mask == 0.0, 0.0, grad)
347+
365348
# Apply norm bound
366349
def _apply_norm(grad, object_type=False):
367350
if self.norm in [np.inf, "inf"]:
@@ -389,10 +372,7 @@ def _apply_norm(grad, object_type=False):
389372

390373
assert batch.shape == grad.shape
391374

392-
if mask is None:
393-
return grad
394-
else:
395-
return grad * (mask.astype(ART_NUMPY_DTYPE))
375+
return grad
396376

397377
def _apply_perturbation(
398378
self, batch: np.ndarray, perturbation: np.ndarray, eps_step: Union[int, float, np.ndarray]
@@ -487,3 +467,35 @@ def _compute(
487467
x_adv[batch_index_1:batch_index_2] = x_init[batch_index_1:batch_index_2] + perturbation
488468

489469
return x_adv
470+
471+
@staticmethod
472+
def _get_mask(x: np.ndarray, **kwargs) -> np.ndarray:
473+
"""
474+
Get the mask from the kwargs.
475+
476+
:param x: An array with the original inputs.
477+
:param mask: An array with a mask to be applied to the adversarial perturbations. Shape needs to be
478+
broadcastable to the shape of x. Any features for which the mask is zero will not be adversarially
479+
perturbed.
480+
:type mask: `np.ndarray`
481+
:return: The mask.
482+
"""
483+
mask = kwargs.get("mask")
484+
485+
if mask is not None:
486+
if mask.ndim > x.ndim:
487+
raise ValueError("Mask shape must be broadcastable to input shape.")
488+
489+
if not (np.issubdtype(mask.dtype, np.floating) or mask.dtype == np.bool):
490+
raise ValueError(
491+
"The `mask` has to be either of type np.float32, np.float64 or np.bool. The provided"
492+
"`mask` is of type {}.".format(mask.dtype)
493+
)
494+
495+
if np.issubdtype(mask.dtype, np.floating) and np.amin(mask) < 0.0:
496+
raise ValueError(
497+
"The `mask` of type np.float32 or np.float64 requires all elements to be either zero"
498+
"or positive values."
499+
)
500+
501+
return mask

art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_numpy.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,21 +246,14 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
246246
:type mask: `np.ndarray`
247247
:return: An array holding the adversarial examples.
248248
"""
249-
mask = kwargs.get("mask")
250-
251-
# Check the mask
252-
if mask is not None and mask.ndim > x.ndim:
253-
raise ValueError("Mask shape must be broadcastable to input shape.")
249+
mask = self._get_mask(x, **kwargs)
254250

255251
# Ensure eps is broadcastable
256252
self._check_compatibility_input_and_eps(x=x)
257253

258254
# Check whether random eps is enabled
259255
self._random_eps()
260256

261-
# Get the mask
262-
mask = self._get_mask(x, **kwargs)
263-
264257
if isinstance(self.estimator, ClassifierMixin):
265258
# Set up targets
266259
targets = self._set_targets(x, y)

art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_pytorch.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from tqdm import trange, tqdm
3333

3434
from art.config import ART_NUMPY_DTYPE
35+
from art.estimators.estimator import BaseEstimator, LossGradientsMixin
36+
from art.estimators.classification.classifier import ClassifierMixin
3537
from art.attacks.evasion.projected_gradient_descent.projected_gradient_descent_numpy import (
3638
ProjectedGradientDescentCommon,
3739
)
@@ -40,7 +42,6 @@
4042
if TYPE_CHECKING:
4143
import torch
4244
from art.estimators.classification.pytorch import PyTorchClassifier
43-
from art.estimators.object_detection.pytorch_faster_rcnn import PyTorchFasterRCNN
4445

4546
logger = logging.getLogger(__name__)
4647

@@ -54,9 +55,11 @@ class ProjectedGradientDescentPyTorch(ProjectedGradientDescentCommon):
5455
| Paper link: https://arxiv.org/abs/1706.06083
5556
"""
5657

58+
_estimator_requirements = (BaseEstimator, LossGradientsMixin, ClassifierMixin)
59+
5760
def __init__(
5861
self,
59-
estimator: Union["PyTorchClassifier", "PyTorchFasterRCNN"],
62+
estimator: Union["PyTorchClassifier"],
6063
norm: Union[int, float, str] = np.inf,
6164
eps: Union[int, float, np.ndarray] = 0.3,
6265
eps_step: Union[int, float, np.ndarray] = 0.1,
@@ -120,9 +123,7 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
120123
"""
121124
import torch # lgtm [py/repeated-import]
122125

123-
mask = kwargs.get("mask")
124-
if mask is not None and mask.ndim > x.ndim:
125-
raise ValueError("Mask shape must be broadcastable to input shape.")
126+
mask = self._get_mask(x, **kwargs)
126127

127128
# Ensure eps is broadcastable
128129
self._check_compatibility_input_and_eps(x=x)
@@ -249,7 +250,9 @@ def _generate_batch(
249250

250251
return adv_x.cpu().detach().numpy()
251252

252-
def _compute_perturbation(self, x: "torch.Tensor", y: "torch.Tensor", mask: "torch.Tensor") -> "torch.Tensor":
253+
def _compute_perturbation(
254+
self, x: "torch.Tensor", y: "torch.Tensor", mask: Optional["torch.Tensor"]
255+
) -> "torch.Tensor":
253256
"""
254257
Compute perturbations.
255258
@@ -271,6 +274,10 @@ def _compute_perturbation(self, x: "torch.Tensor", y: "torch.Tensor", mask: "tor
271274
# Get gradient wrt loss; invert it if attack is targeted
272275
grad = self.estimator.loss_gradient(x=x, y=y) * (1 - 2 * int(self.targeted))
273276

277+
# Apply mask
278+
if mask is not None:
279+
grad = torch.where(mask == 0.0, torch.tensor(0.0), grad)
280+
274281
# Apply norm bound
275282
if self.norm in ["inf", np.inf]:
276283
grad = grad.sign()
@@ -285,10 +292,7 @@ def _compute_perturbation(self, x: "torch.Tensor", y: "torch.Tensor", mask: "tor
285292

286293
assert x.shape == grad.shape
287294

288-
if mask is None:
289-
return grad
290-
else:
291-
return grad * mask
295+
return grad
292296

293297
def _apply_perturbation(
294298
self, x: "torch.Tensor", perturbation: "torch.Tensor", eps_step: Union[int, float, np.ndarray]

art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_tensorflow_v2.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from tqdm import trange, tqdm
3333

3434
from art.config import ART_NUMPY_DTYPE
35+
from art.estimators.estimator import BaseEstimator, LossGradientsMixin
36+
from art.estimators.classification.classifier import ClassifierMixin
3537
from art.attacks.evasion.projected_gradient_descent.projected_gradient_descent_numpy import (
3638
ProjectedGradientDescentCommon,
3739
)
@@ -53,6 +55,8 @@ class ProjectedGradientDescentTensorFlowV2(ProjectedGradientDescentCommon):
5355
| Paper link: https://arxiv.org/abs/1706.06083
5456
"""
5557

58+
_estimator_requirements = (BaseEstimator, LossGradientsMixin, ClassifierMixin)
59+
5660
def __init__(
5761
self,
5862
estimator: "TensorFlowV2Classifier",
@@ -119,9 +123,7 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
119123
"""
120124
import tensorflow as tf # lgtm [py/repeated-import]
121125

122-
mask = kwargs.get("mask")
123-
if mask is not None and mask.ndim > x.ndim:
124-
raise ValueError("Mask shape must be broadcastable to input shape.")
126+
mask = self._get_mask(x, **kwargs)
125127

126128
# Ensure eps is broadcastable
127129
self._check_compatibility_input_and_eps(x=x)
@@ -224,17 +226,11 @@ def _generate_batch(
224226
225227
:param x: An array with the original inputs.
226228
:param targets: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)`.
227-
<<<<<<< HEAD
228-
:param mask: An array with a mask to be applied to the adversarial perturbations. Shape needs to be
229-
broadcastable to the shape of x. Any features for which the mask is zero will not be adversarially
230-
perturbed.
231-
:param eps: Maximum perturbation that the attacker can introduce.
232-
:param eps_step: Attack step size (input variation) at each iteration.
233-
=======
234229
:param mask: An array with a mask broadcastable to input `x` defining where to apply adversarial perturbations.
235230
Shape needs to be broadcastable to the shape of x and can also be of the same shape as `x`. Any
236231
features for which the mask is zero will not be adversarially perturbed.
237-
>>>>>>> origin/dev_1.5.0
232+
:param eps: Maximum perturbation that the attacker can introduce.
233+
:param eps_step: Attack step size (input variation) at each iteration.
238234
:return: Adversarial examples.
239235
"""
240236
adv_x = x
@@ -269,6 +265,10 @@ def _compute_perturbation(self, x: "tf.Tensor", y: "tf.Tensor", mask: Optional["
269265
1 - 2 * int(self.targeted), dtype=ART_NUMPY_DTYPE
270266
)
271267

268+
# Apply mask
269+
if mask is not None:
270+
grad = tf.where(mask == 0.0, 0.0, grad)
271+
272272
# Apply norm bound
273273
if self.norm == np.inf:
274274
grad = tf.sign(grad)
@@ -285,10 +285,7 @@ def _compute_perturbation(self, x: "tf.Tensor", y: "tf.Tensor", mask: Optional["
285285

286286
assert x.shape == grad.shape
287287

288-
if mask is None:
289-
return grad
290-
else:
291-
return grad * mask
288+
return grad
292289

293290
def _apply_perturbation(
294291
self, x: "tf.Tensor", perturbation: "tf.Tensor", eps_step: Union[int, float, np.ndarray]

art/attacks/evasion/zoo.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,10 @@ def compare(object1, object2):
344344
else:
345345
x_orig = x_batch
346346
self._reset_adam(np.prod(self.estimator.input_shape).item())
347-
self._current_noise.fill(0)
347+
if x_batch.shape == self._current_noise.shape:
348+
self._current_noise.fill(0)
349+
else:
350+
self._current_noise = np.zeros(x_batch.shape, dtype=ART_NUMPY_DTYPE)
348351
x_adv = x_orig.copy()
349352

350353
# Initialize best distortions, best changed labels and best attacks
@@ -537,7 +540,10 @@ def _resize_image(self, x: np.ndarray, size_x: int, size_y: int, reset: bool = F
537540
# Reset variables to original size and value
538541
if dims == x.shape:
539542
resized_x = x
540-
self._current_noise.fill(0)
543+
if x.shape == self._current_noise.shape:
544+
self._current_noise.fill(0)
545+
else:
546+
self._current_noise = np.zeros(x.shape, dtype=ART_NUMPY_DTYPE)
541547
else:
542548
resized_x = zoom(x, (1, dims[1] / x.shape[1], dims[2] / x.shape[2], dims[3] / x.shape[3],),)
543549
self._current_noise = np.zeros(dims, dtype=ART_NUMPY_DTYPE)

art/defences/preprocessor/preprocessor.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,16 @@ def get_gradient(x, grad):
205205

206206
return x_grad
207207

208-
if x.shape == grad.shape:
208+
if x.dtype == np.object:
209+
x_grad_list = list()
210+
for i, x_i in enumerate(x):
211+
x_grad_list.append(get_gradient(x=x_i, grad=grad[i]))
212+
x_grad = np.empty(x.shape[0], dtype=object)
213+
x_grad[:] = list(x_grad_list)
214+
elif x.shape == grad.shape:
209215
x_grad = get_gradient(x=x, grad=grad)
210216
else:
211-
# Special case for lass gradients
217+
# Special case for loss gradients
212218
x_grad = np.zeros_like(grad)
213219
for i in range(grad.shape[1]):
214220
x_grad[:, i, ...] = get_gradient(x=x, grad=grad[:, i, ...])
@@ -268,35 +274,41 @@ def __call__(self, x: np.ndarray, y: Optional[np.ndarray] = None) -> Tuple[np.nd
268274
return result, y
269275

270276
# Backward compatibility.
271-
def _get_gradient(self, x: np.ndarray, grad: np.ndarray) -> np.ndarray:
272-
"""
273-
Helper function for estimate_gradient
274-
"""
277+
def estimate_gradient(self, x: np.ndarray, grad: np.ndarray) -> np.ndarray:
275278
import tensorflow as tf # lgtm [py/repeated-import]
276279

277-
with tf.GradientTape() as tape:
278-
x = tf.convert_to_tensor(x, dtype=config.ART_NUMPY_DTYPE)
279-
tape.watch(x)
280-
grad = tf.convert_to_tensor(grad, dtype=config.ART_NUMPY_DTYPE)
280+
def get_gradient(x: np.ndarray, grad: np.ndarray) -> np.ndarray:
281+
"""
282+
Helper function for estimate_gradient
283+
"""
281284

282-
x_prime = self.estimate_forward(x)
285+
with tf.GradientTape() as tape:
286+
x = tf.convert_to_tensor(x, dtype=config.ART_NUMPY_DTYPE)
287+
tape.watch(x)
288+
grad = tf.convert_to_tensor(grad, dtype=config.ART_NUMPY_DTYPE)
283289

284-
x_grad = tape.gradient(target=x_prime, sources=x, output_gradients=grad)
290+
x_prime = self.estimate_forward(x)
285291

286-
x_grad = x_grad.numpy()
287-
if x_grad.shape != x.shape:
288-
raise ValueError("The input shape is {} while the gradient shape is {}".format(x.shape, x_grad.shape))
292+
x_grad = tape.gradient(target=x_prime, sources=x, output_gradients=grad)
289293

290-
return x_grad
294+
x_grad = x_grad.numpy()
295+
if x_grad.shape != x.shape:
296+
raise ValueError("The input shape is {} while the gradient shape is {}".format(x.shape, x_grad.shape))
291297

292-
# Backward compatibility.
293-
def estimate_gradient(self, x: np.ndarray, grad: np.ndarray) -> np.ndarray:
294-
if x.shape == grad.shape:
295-
x_grad = self._get_gradient(x=x, grad=grad)
298+
return x_grad
299+
300+
if x.dtype == np.object:
301+
x_grad_list = list()
302+
for i, x_i in enumerate(x):
303+
x_grad_list.append(get_gradient(x=x_i, grad=grad[i]))
304+
x_grad = np.empty(x.shape[0], dtype=object)
305+
x_grad[:] = list(x_grad_list)
306+
elif x.shape == grad.shape:
307+
x_grad = get_gradient(x=x, grad=grad)
296308
else:
297-
# Special case for lass gradients
309+
# Special case for loss gradients
298310
x_grad = np.zeros_like(grad)
299311
for i in range(grad.shape[1]):
300-
x_grad[:, i, ...] = self._get_gradient(x=x, grad=grad[:, i, ...])
312+
x_grad[:, i, ...] = get_gradient(x=x, grad=grad[:, i, ...])
301313

302314
return x_grad

art/defences/preprocessor/spatial_smoothing_pytorch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def __init__(
6363
"""
6464
Create an instance of local spatial smoothing.
6565
66-
:window_size: Size of spatial smoothing window.
66+
:param window_size: Size of spatial smoothing window.
6767
:param channels_first: Set channels first or last.
6868
:param clip_values: Tuple of the form `(min, max)` representing the minimum and maximum values allowed
6969
for features.

0 commit comments

Comments
 (0)