Skip to content

Commit c114a49

Browse files
authored
Merge pull request #1416 from Trusted-AI/development_issue_1325
Implement customizable summary writer and indicators of attack failure
2 parents b77725e + 5d493c0 commit c114a49

16 files changed

+804
-202
lines changed

art/attacks/attack.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import numpy as np
2828

2929
from art.exceptions import EstimatorError
30+
from art.summary_writer import SummaryWriter, SummaryWriterDefault
3031

3132
if TYPE_CHECKING:
3233
from art.utils import CLASSIFIER_TYPE
@@ -100,15 +101,17 @@ class Attack(abc.ABC):
100101
def __init__(
101102
self,
102103
estimator,
103-
tensor_board: Union[str, bool] = False,
104+
summary_writer: Union[str, bool, SummaryWriter] = False,
104105
):
105106
"""
106107
:param estimator: An estimator.
107-
:param tensor_board: Activate summary writer for TensorBoard: Default is `False` and deactivated summary writer.
108-
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory. Provide `path` in type
109-
`str` to save in path/CURRENT_DATETIME_HOSTNAME.
110-
Use hierarchical folder structure to compare between runs easily. e.g. pass in ‘runs/exp1’,
111-
‘runs/exp2’, etc. for each new experiment to compare across them.
108+
:param summary_writer: Activate summary writer for TensorBoard.
109+
Default is `False` and deactivated summary writer.
110+
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory.
111+
If of type `str` save in path.
112+
If of type `SummaryWriter` apply provided custom summary writer.
113+
Use hierarchical folder structure to compare between runs easily. e.g. pass in
114+
‘runs/exp1’, ‘runs/exp2’, etc. for each new experiment to compare across them.
112115
"""
113116
super().__init__()
114117

@@ -119,17 +122,13 @@ def __init__(
119122
raise EstimatorError(self.__class__, self.estimator_requirements, estimator)
120123

121124
self._estimator = estimator
122-
self.tensor_board = tensor_board
125+
self._summary_writer_arg = summary_writer
126+
self._summary_writer: Optional[SummaryWriter] = None
123127

124-
if tensor_board: # pragma: no cover
125-
from tensorboardX import SummaryWriter
126-
127-
if isinstance(tensor_board, str):
128-
self.summary_writer = SummaryWriter(tensor_board)
129-
else:
130-
self.summary_writer = SummaryWriter()
131-
else:
132-
self.summary_writer = None
128+
if isinstance(summary_writer, SummaryWriter): # pragma: no cover
129+
self._summary_writer = summary_writer
130+
elif summary_writer:
131+
self._summary_writer = SummaryWriterDefault(summary_writer)
133132

134133
Attack._check_params(self)
135134

@@ -138,6 +137,11 @@ def estimator(self):
138137
"""The estimator."""
139138
return self._estimator
140139

140+
@property
141+
def summary_writer(self):
142+
"""The summary writer."""
143+
return self._summary_writer
144+
141145
@property
142146
def estimator_requirements(self):
143147
"""The estimator requirements."""
@@ -156,8 +160,8 @@ def set_params(self, **kwargs) -> None:
156160

157161
def _check_params(self) -> None:
158162

159-
if not isinstance(self.tensor_board, (bool, str)):
160-
raise ValueError("The argument `tensor_board` has to be either of type bool or str.")
163+
if not isinstance(self._summary_writer_arg, (bool, str, SummaryWriter)):
164+
raise ValueError("The argument `summary_writer` has to be either of type bool or str.")
161165

162166
@staticmethod
163167
def is_estimator_valid(estimator, estimator_requirements) -> bool:

art/attacks/evasion/adversarial_patch/adversarial_patch_pytorch.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from art.estimators.estimator import BaseEstimator, NeuralNetworkMixin
3535
from art.estimators.classification.classifier import ClassifierMixin
3636
from art.utils import check_and_transform_label_format, is_probability, to_categorical
37+
from art.summary_writer import SummaryWriter
3738

3839
if TYPE_CHECKING:
3940
# pylint: disable=C0412
@@ -60,7 +61,7 @@ class AdversarialPatchPyTorch(EvasionAttack):
6061
"max_iter",
6162
"batch_size",
6263
"patch_shape",
63-
"tensor_board",
64+
"summary_writer",
6465
"verbose",
6566
]
6667

@@ -78,7 +79,7 @@ def __init__(
7879
batch_size: int = 16,
7980
patch_shape: Optional[Tuple[int, int, int]] = None,
8081
patch_type: str = "circle",
81-
tensor_board: Union[str, bool] = False,
82+
summary_writer: Union[str, bool, SummaryWriter] = False,
8283
verbose: bool = True,
8384
):
8485
"""
@@ -98,11 +99,13 @@ def __init__(
9899
:param batch_size: The size of the training batch.
99100
:param patch_shape: The shape of the adversarial patch as a tuple of shape CHW (nb_channels, height, width).
100101
:param patch_type: The patch type, either circle or square.
101-
:param tensor_board: Activate summary writer for TensorBoard: Default is `False` and deactivated summary writer.
102-
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory. Provide `path` in type
103-
`str` to save in path/CURRENT_DATETIME_HOSTNAME.
104-
Use hierarchical folder structure to compare between runs easily. e.g. pass in ‘runs/exp1’,
105-
‘runs/exp2’, etc. for each new experiment to compare across them.
102+
:param summary_writer: Activate summary writer for TensorBoard.
103+
Default is `False` and deactivated summary writer.
104+
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory.
105+
If of type `str` save in path.
106+
If of type `SummaryWriter` apply provided custom summary writer.
107+
Use hierarchical folder structure to compare between runs easily. e.g. pass in
108+
‘runs/exp1’, ‘runs/exp2’, etc. for each new experiment to compare across them.
106109
:param verbose: Show progress bars.
107110
"""
108111
import torch # lgtm [py/repeated-import]
@@ -115,7 +118,7 @@ def __init__(
115118
torchvision_version[0] >= 0 and torchvision_version[1] >= 8
116119
), "AdversarialPatchPyTorch requires torchvision>=0.8.0"
117120

118-
super().__init__(estimator=classifier, tensor_board=tensor_board)
121+
super().__init__(estimator=classifier, summary_writer=summary_writer)
119122
self.rotation_max = rotation_max
120123
self.scale_min = scale_min
121124
self.scale_max = scale_max
@@ -484,25 +487,30 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> T
484487
)
485488
_ = self._train_step(images=images, target=target, mask=mask_i)
486489

490+
# Write summary
487491
if self.summary_writer is not None: # pragma: no cover
488-
self.summary_writer.add_image(
489-
"patch",
490-
self._patch,
492+
x_patched = (
493+
self._random_overlay(
494+
images=torch.from_numpy(x).to(self.estimator.device), patch=self._patch, mask=mask
495+
)
496+
.detach()
497+
.cpu()
498+
.numpy()
499+
)
500+
501+
self.summary_writer.update(
502+
batch_id=0,
491503
global_step=i_iter,
504+
grad=None,
505+
patch=self._patch,
506+
estimator=self.estimator,
507+
x=x_patched,
508+
y=y,
509+
targeted=self.targeted,
492510
)
493511

494-
if hasattr(self.estimator, "compute_losses"):
495-
x_patched = self._random_overlay(
496-
images=torch.from_numpy(x).to(self.estimator.device), patch=self._patch, mask=mask
497-
)
498-
losses = self.estimator.compute_losses(x=x_patched, y=torch.from_numpy(y).to(self.estimator.device))
499-
500-
for key, value in losses.items():
501-
self.summary_writer.add_scalar(
502-
"loss/{}".format(key),
503-
np.mean(value.detach().cpu().numpy()),
504-
global_step=i_iter,
505-
)
512+
if self.summary_writer is not None:
513+
self.summary_writer.reset()
506514

507515
return (
508516
self._patch.detach().cpu().numpy(),

art/attacks/evasion/adversarial_patch/adversarial_patch_tensorflow.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from art.estimators.estimator import BaseEstimator, NeuralNetworkMixin
3636
from art.estimators.classification.classifier import ClassifierMixin
3737
from art.utils import check_and_transform_label_format, is_probability, to_categorical
38+
from art.summary_writer import SummaryWriter
3839

3940
if TYPE_CHECKING:
4041
# pylint: disable=C0412
@@ -60,7 +61,7 @@ class AdversarialPatchTensorFlowV2(EvasionAttack):
6061
"max_iter",
6162
"batch_size",
6263
"patch_shape",
63-
"tensor_board",
64+
"summary_writer",
6465
"verbose",
6566
]
6667

@@ -76,7 +77,7 @@ def __init__(
7677
max_iter: int = 500,
7778
batch_size: int = 16,
7879
patch_shape: Optional[Tuple[int, int, int]] = None,
79-
tensor_board: Union[str, bool] = False,
80+
summary_writer: Union[str, bool, SummaryWriter] = False,
8081
verbose: bool = True,
8182
):
8283
"""
@@ -93,16 +94,18 @@ def __init__(
9394
:param max_iter: The number of optimization steps.
9495
:param batch_size: The size of the training batch.
9596
:param patch_shape: The shape of the adversarial patch as a tuple of shape HWC (width, height, nb_channels).
96-
:param tensor_board: Activate summary writer for TensorBoard: Default is `False` and deactivated summary writer.
97-
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory. Provide `path` in type
98-
`str` to save in path/CURRENT_DATETIME_HOSTNAME.
99-
Use hierarchical folder structure to compare between runs easily. e.g. pass in ‘runs/exp1’,
100-
‘runs/exp2’, etc. for each new experiment to compare across them.
97+
:param summary_writer: Activate summary writer for TensorBoard.
98+
Default is `False` and deactivated summary writer.
99+
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory.
100+
If of type `str` save in path.
101+
If of type `SummaryWriter` apply provided custom summary writer.
102+
Use hierarchical folder structure to compare between runs easily. e.g. pass in
103+
‘runs/exp1’, ‘runs/exp2’, etc. for each new experiment to compare across them.
101104
:param verbose: Show progress bars.
102105
"""
103106
import tensorflow as tf # lgtm [py/repeated-import]
104107

105-
super().__init__(estimator=classifier, tensor_board=tensor_board)
108+
super().__init__(estimator=classifier, summary_writer=summary_writer)
106109
self.rotation_max = rotation_max
107110
self.scale_min = scale_min
108111
self.scale_max = scale_max
@@ -458,23 +461,23 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> T
458461
for images, target, mask_i in dataset:
459462
_ = self._train_step(images=images, target=target, mask=mask_i)
460463

464+
# Write summary
461465
if self.summary_writer is not None: # pragma: no cover
462-
self.summary_writer.add_image(
463-
"patch",
464-
self._patch.numpy().transpose((2, 0, 1)),
466+
x_patched = self._random_overlay(images=x, patch=self._patch, mask=mask)
467+
468+
self.summary_writer.update(
469+
batch_id=0,
465470
global_step=i_iter,
471+
grad=None,
472+
patch=self._patch.numpy().transpose((2, 0, 1)),
473+
estimator=self.estimator,
474+
x=x_patched,
475+
y=y,
476+
targeted=self.targeted,
466477
)
467478

468-
if hasattr(self.estimator, "compute_losses"):
469-
x_patched = self._random_overlay(images=x, patch=self._patch, mask=mask)
470-
losses = self.estimator.compute_losses(x=x_patched, y=y)
471-
472-
for key, value in losses.items():
473-
self.summary_writer.add_scalar(
474-
"loss/{}".format(key),
475-
np.mean(value),
476-
global_step=i_iter,
477-
)
479+
if self.summary_writer is not None:
480+
self.summary_writer.reset()
478481

479482
return (
480483
self._patch.numpy(),

art/attacks/evasion/adversarial_texture/adversarial_texture_pytorch.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from art.attacks.attack import EvasionAttack
3030
from art.estimators.estimator import BaseEstimator, LossGradientsMixin
3131
from art.estimators.object_tracking.object_tracker import ObjectTrackerMixin
32+
from art.summary_writer import SummaryWriter
3233

3334
if TYPE_CHECKING:
3435
# pylint: disable=C0412
@@ -67,6 +68,7 @@ def __init__(
6768
step_size: float = 1.0 / 255.0,
6869
max_iter: int = 500,
6970
batch_size: int = 16,
71+
summary_writer: Union[str, bool, SummaryWriter] = False,
7072
verbose: bool = True,
7173
):
7274
"""
@@ -80,11 +82,18 @@ def __init__(
8082
:param step_size: The step size.
8183
:param max_iter: The number of optimization steps.
8284
:param batch_size: The size of the training batch.
85+
:param summary_writer: Activate summary writer for TensorBoard.
86+
Default is `False` and deactivated summary writer.
87+
If `True` save runs/CURRENT_DATETIME_HOSTNAME in current directory.
88+
If of type `str` save in path.
89+
If of type `SummaryWriter` apply provided custom summary writer.
90+
Use hierarchical folder structure to compare between runs easily. e.g. pass in
91+
‘runs/exp1’, ‘runs/exp2’, etc. for each new experiment to compare across them.
8392
:param verbose: Show progress bars.
8493
"""
8594
import torch # lgtm [py/repeated-import]
8695

87-
super().__init__(estimator=estimator)
96+
super().__init__(estimator=estimator, summary_writer=summary_writer)
8897
self.patch_height = patch_height
8998
self.patch_width = patch_width
9099
self.x_min = x_min
@@ -95,6 +104,9 @@ def __init__(
95104
self.verbose = verbose
96105
self._check_params()
97106

107+
self._batch_id = 0
108+
self._i_max_iter = 0
109+
98110
self.patch_shape = (self.patch_height, self.patch_width, 3)
99111

100112
if self.estimator.channels_first:
@@ -142,6 +154,18 @@ def _train_step(
142154

143155
gradients = self._patch.grad.sign() * self.step_size
144156

157+
# Write summary
158+
if self.summary_writer is not None: # pragma: no cover
159+
self.summary_writer.update(
160+
batch_id=self._batch_id,
161+
global_step=self._i_max_iter,
162+
grad=np.expand_dims(self._patch.grad.detach().cpu().numpy(), axis=0),
163+
patch=None,
164+
estimator=None,
165+
x=None,
166+
y=None,
167+
)
168+
145169
with torch.no_grad():
146170
self._patch[:] = torch.clamp(
147171
self._patch + gradients, min=self.estimator.clip_values[0], max=self.estimator.clip_values[1]
@@ -356,8 +380,13 @@ def __getitem__(self, idx):
356380
drop_last=False,
357381
)
358382

359-
for _ in trange(self.max_iter, desc="Adversarial Texture PyTorch", disable=not self.verbose):
383+
for i_max_iter in trange(self.max_iter, desc="Adversarial Texture PyTorch", disable=not self.verbose):
384+
385+
self._i_max_iter = i_max_iter
386+
self._batch_id = 0
387+
360388
for videos_i, target_i, y_init_i, foreground_i in data_loader:
389+
self._batch_id += 1
361390
videos_i = videos_i.to(self.estimator.device)
362391
y_init_i = y_init_i.to(self.estimator.device)
363392
foreground_i = foreground_i.to(self.estimator.device)
@@ -367,6 +396,21 @@ def __getitem__(self, idx):
367396

368397
_ = self._train_step(videos=videos_i, target=target_i_list, y_init=y_init_i, foreground=foreground_i)
369398

399+
# Write summary
400+
if self.summary_writer is not None: # pragma: no cover
401+
self.summary_writer.update(
402+
batch_id=self._batch_id,
403+
global_step=self._i_max_iter,
404+
grad=None,
405+
patch=self._patch.detach().cpu().numpy(),
406+
estimator=self.estimator,
407+
x=videos_i.detach().cpu().numpy(),
408+
y=target_i_list,
409+
)
410+
411+
if self.summary_writer is not None:
412+
self.summary_writer.reset()
413+
370414
return self.apply_patch(x=x, foreground=foreground)
371415

372416
def apply_patch(

0 commit comments

Comments
 (0)