Skip to content

Commit 4f24c06

Browse files
authored
Merge branch 'dev_1.5.0' into asr_with_defences
2 parents f8f24d1 + 26c8cba commit 4f24c06

File tree

18 files changed

+2005
-156
lines changed

18 files changed

+2005
-156
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ jobs:
3737
tensorflow: 2.2.0
3838
tf_version: v2
3939
keras: 2.3.1
40+
- name: TensorFlow 2.2.0v1 (Keras 2.3.1 Python 3.7)
41+
framework: tensorflow2v1
42+
python: 3.7
43+
tensorflow: 2.2.0
44+
tf_version: v2
45+
keras: 2.3.1
4046
- name: TensorFlow 2.3.1 (Keras 2.4.3 Python 3.7)
4147
framework: tensorflow
4248
python: 3.7

art/attacks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from art.attacks.attack import Attack, EvasionAttack, PoisoningAttack, PoisoningAttackBlackBox, PoisoningAttackWhiteBox
55
from art.attacks.attack import PoisoningAttackTransformer, ExtractionAttack, InferenceAttack, AttributeInferenceAttack
6+
from art.attacks.attack import ReconstructionAttack
67

78
from art.attacks import evasion
89
from art.attacks import extraction

art/attacks/attack.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,37 @@ def set_params(self, **kwargs) -> None:
349349
def _check_params(self) -> None:
350350
if self.attack_feature < 0:
351351
raise ValueError("Attack feature must be positive.")
352+
353+
354+
class ReconstructionAttack(Attack):
355+
"""
356+
Abstract base class for reconstruction attack classes.
357+
"""
358+
359+
attack_params = InferenceAttack.attack_params
360+
361+
def __init__(self, estimator):
362+
"""
363+
:param estimator: A trained estimator targeted for reconstruction attack.
364+
"""
365+
super().__init__(estimator)
366+
367+
@abc.abstractmethod
368+
def reconstruct(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> Tuple[np.ndarray, np.ndarray]:
369+
"""
370+
Reconstruct the training dataset of and from the targeted estimator. This method
371+
should be overridden by all concrete inference attack implementations.
372+
373+
:param x: An array with known records of the training set of `estimator`.
374+
:param y: An array with known labels of the training set of `estimator`, if None predicted labels will be used.
375+
:return: A tuple of two arrays for the reconstructed training input and labels.
376+
"""
377+
raise NotImplementedError
378+
379+
def set_params(self, **kwargs) -> None:
380+
"""
381+
Take in a dictionary of parameters and applies attack-specific checks before saving them as attributes.
382+
"""
383+
# Save attack-specific parameters
384+
super().set_params(**kwargs)
385+
self._check_params()

art/attacks/evasion/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from art.attacks.evasion.fast_gradient import FastGradientMethod
1313
from art.attacks.evasion.hclu import HighConfidenceLowUncertainty
1414
from art.attacks.evasion.hop_skip_jump import HopSkipJump
15+
from art.attacks.evasion.imperceptible_asr.imperceptible_asr import ImperceptibleASR
1516
from art.attacks.evasion.iterative_method import BasicIterativeMethod
1617
from art.attacks.evasion.newtonfool import NewtonFool
1718
from art.attacks.evasion.projected_gradient_descent.projected_gradient_descent import ProjectedGradientDescent

art/attacks/evasion/boundary.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from typing import Optional, Tuple, TYPE_CHECKING
2828

2929
import numpy as np
30-
from tqdm import tqdm
30+
from tqdm import tqdm, trange
3131

3232
from art.attacks.attack import EvasionAttack
3333
from art.config import ART_NUMPY_DTYPE
@@ -75,6 +75,7 @@ def __init__(
7575
num_trial: int = 25,
7676
sample_size: int = 20,
7777
init_size: int = 100,
78+
min_epsilon: Optional[float] = None,
7879
verbose: bool = True,
7980
) -> None:
8081
"""
@@ -89,6 +90,7 @@ def __init__(
8990
:param num_trial: Maximum number of trials per iteration.
9091
:param sample_size: Number of samples per trial.
9192
:param init_size: Maximum number of trials for initial generation of adversarial examples.
93+
:param min_epsilon: Stop attack if perturbation is smaller than `min_epsilon`.
9294
:param verbose: Show progress bars.
9395
"""
9496
super().__init__(estimator=estimator)
@@ -101,10 +103,13 @@ def __init__(
101103
self.num_trial = num_trial
102104
self.sample_size = sample_size
103105
self.init_size = init_size
106+
self.min_epsilon = min_epsilon
104107
self.batch_size = 1
105108
self.verbose = verbose
106109
self._check_params()
107110

111+
self.curr_adv = None
112+
108113
def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
109114
"""
110115
Generate adversarial samples and return them in an array.
@@ -230,8 +235,10 @@ def _attack(
230235
self.curr_delta = initial_delta
231236
self.curr_epsilon = initial_epsilon
232237

238+
self.curr_adv = x_adv
239+
233240
# Main loop to wander around the boundary
234-
for _ in range(self.max_iter):
241+
for _ in trange(self.max_iter, desc="Boundary attack - iterations", disable=not self.verbose):
235242
# Trust region method to adjust delta
236243
for _ in range(self.num_trial):
237244
potential_advs = []
@@ -273,11 +280,15 @@ def _attack(
273280

274281
if epsilon_ratio > 0:
275282
x_adv = potential_advs[np.where(satisfied)[0][0]]
283+
self.curr_adv = x_adv
276284
break
277285
else:
278286
logger.warning("Adversarial example found but not optimal.")
279287
return x_advs[0]
280288

289+
if self.min_epsilon is not None and self.curr_epsilon < self.min_epsilon:
290+
return x_adv
291+
281292
return x_adv
282293

283294
def _orthogonal_perturb(self, delta: float, current_sample: np.ndarray, original_sample: np.ndarray) -> np.ndarray:
@@ -299,20 +310,13 @@ def _orthogonal_perturb(self, delta: float, current_sample: np.ndarray, original
299310
# Project the perturbation onto sphere
300311
direction = original_sample - current_sample
301312

302-
if len(self.estimator.input_shape) == 3:
303-
channel_index = 1 if self.estimator.channels_first else 3
304-
perturb = np.swapaxes(perturb, 0, channel_index - 1)
305-
direction = np.swapaxes(direction, 0, channel_index - 1)
306-
for i in range(direction.shape[0]):
307-
direction[i] /= np.linalg.norm(direction[i])
308-
perturb[i] -= np.dot(np.dot(perturb[i], direction[i].T), direction[i])
309-
310-
perturb = np.swapaxes(perturb, 0, channel_index - 1)
311-
elif len(self.estimator.input_shape) == 1:
312-
direction /= np.linalg.norm(direction)
313-
perturb -= np.dot(perturb, direction.T) * direction
314-
else:
315-
raise ValueError("Input shape not recognised.")
313+
direction_flat = direction.flatten()
314+
perturb_flat = perturb.flatten()
315+
316+
direction_flat /= np.linalg.norm(direction_flat)
317+
perturb_flat -= np.dot(perturb_flat, direction_flat.T) * direction_flat
318+
perturb = perturb_flat.reshape(self.estimator.input_shape)
319+
316320
hypotenuse = np.sqrt(1 + delta ** 2)
317321
perturb = ((1 - hypotenuse) * (current_sample - original_sample) + perturb) / hypotenuse
318322
return perturb
@@ -403,5 +407,8 @@ def _check_params(self) -> None:
403407
if self.step_adapt <= 0 or self.step_adapt >= 1:
404408
raise ValueError("The adaptation factor must be in the range (0, 1).")
405409

410+
if self.min_epsilon is not None and (isinstance(self.min_epsilon, float) or self.min_epsilon <= 0):
411+
raise ValueError("The minimum epsilon must be a positive float.")
412+
406413
if not isinstance(self.verbose, bool):
407414
raise ValueError("The argument `verbose` has to be of type bool.")

0 commit comments

Comments
 (0)