Skip to content

Commit effe6ff

Browse files
authored
Merge pull request #1069 from Trusted-AI/development_issue_1062
Add targeted option to RobustDPatch and fix object detection label formats
2 parents 91a7e73 + 17a18e5 commit effe6ff

File tree

8 files changed

+361
-62
lines changed

8 files changed

+361
-62
lines changed

art/attacks/evasion/dpatch.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def generate( # pylint: disable=W0221
189189

190190
else:
191191

192-
predictions = self.estimator.predict(x=patched_images)
192+
predictions = self.estimator.predict(x=patched_images, standardise_output=True)
193193

194194
for i_image in range(patched_images.shape[0]):
195195
target_dict = dict()
@@ -213,6 +213,7 @@ def generate( # pylint: disable=W0221
213213
gradients = self.estimator.loss_gradient(
214214
x=patched_images[i_batch_start:i_batch_end],
215215
y=patch_target[i_batch_start:i_batch_end],
216+
standardise_output=True,
216217
)
217218

218219
for i_image in range(gradients.shape[0]):

art/attacks/evasion/dpatch_robust.py

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ class RobustDPatch(EvasionAttack):
5959
"learning_rate",
6060
"max_iter",
6161
"batch_size",
62-
"verbose",
6362
"patch_location",
6463
"crop_range",
6564
"brightness_range",
6665
"rotation_weights",
6766
"sample_size",
67+
"targeted",
68+
"verbose",
6869
]
6970

7071
_estimator_requirements = (BaseEstimator, LossGradientsMixin, ObjectDetectorMixin)
@@ -81,6 +82,7 @@ def __init__(
8182
learning_rate: float = 5.0,
8283
max_iter: int = 500,
8384
batch_size: int = 16,
85+
targeted: bool = False,
8486
verbose: bool = True,
8587
):
8688
"""
@@ -96,6 +98,7 @@ def __init__(
9698
:param learning_rate: The learning rate of the optimization.
9799
:param max_iter: The number of optimization steps.
98100
:param batch_size: The size of the training batch.
101+
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
99102
:param verbose: Show progress bars.
100103
"""
101104

@@ -120,9 +123,10 @@ def __init__(
120123
self.brightness_range = brightness_range
121124
self.rotation_weights = rotation_weights
122125
self.sample_size = sample_size
126+
self._targeted = targeted
123127
self._check_params()
124128

125-
def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
129+
def generate(self, x: np.ndarray, y: Optional[List[Dict[str, np.ndarray]]] = None, **kwargs) -> np.ndarray:
126130
"""
127131
Generate RobustDPatch.
128132
@@ -133,7 +137,9 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
133137
channel_index = 1 if self.estimator.channels_first else x.ndim - 1
134138
if x.shape[channel_index] != self.patch_shape[channel_index - 1]:
135139
raise ValueError("The color channel index of the images and the patch have to be identical.")
136-
if y is not None:
140+
if y is None and self.targeted:
141+
raise ValueError("The targeted version of RobustDPatch attack requires target labels provided to `y`.")
142+
if y is not None and not self.targeted:
137143
raise ValueError("The RobustDPatch attack does not use target labels.")
138144
if x.ndim != 4:
139145
raise ValueError("The adversarial patch can only be applied to images.")
@@ -144,6 +150,24 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
144150
else:
145151
image_height, image_width = x.shape[1:3]
146152

153+
if not self.estimator.native_label_is_pytorch_format and y is not None:
154+
from art.estimators.object_detection.utils import convert_tf_to_pt
155+
156+
y = convert_tf_to_pt(y=y, height=x.shape[1], width=x.shape[2])
157+
158+
if y is not None:
159+
for i_image in range(x.shape[0]):
160+
y_i = y[i_image]["boxes"]
161+
for i_box in range(y_i.shape[0]):
162+
x_1, y_1, x_2, y_2 = y_i[i_box]
163+
if (
164+
x_1 < self.crop_range[1]
165+
or y_1 < self.crop_range[0]
166+
or x_2 > image_width - self.crop_range[1] + 1
167+
or y_2 > image_height - self.crop_range[0] + 1
168+
):
169+
raise ValueError("Cropping is intersecting with at least one box, reduce `crop_range`.")
170+
147171
if (
148172
self.patch_location[0] + self.patch_shape[0] > image_height - self.crop_range[0]
149173
or self.patch_location[1] + self.patch_shape[1] > image_width - self.crop_range[1]
@@ -165,14 +189,20 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
165189
i_batch_start = i_batch * self.batch_size
166190
i_batch_end = min((i_batch + 1) * self.batch_size, x.shape[0])
167191

192+
if y is None:
193+
y_batch = y
194+
else:
195+
y_batch = y[i_batch_start:i_batch_end]
196+
168197
# Sample and apply the random transformations:
169198
patched_images, patch_target, transforms = self._augment_images_with_patch(
170-
x[i_batch_start:i_batch_end], self._patch, channels_first=self.estimator.channels_first
199+
x[i_batch_start:i_batch_end], y_batch, self._patch, channels_first=self.estimator.channels_first
171200
)
172201

173202
gradients = self.estimator.loss_gradient(
174203
x=patched_images,
175204
y=patch_target,
205+
standardise_output=True,
176206
)
177207

178208
gradients = self._untransform_gradients(
@@ -187,7 +217,7 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
187217

188218
patch_gradients_old = patch_gradients
189219

190-
self._patch = self._patch + np.sign(patch_gradients) * self.learning_rate
220+
self._patch = self._patch + np.sign(patch_gradients) * (1 - 2 * int(self.targeted)) * self.learning_rate
191221

192222
if self.estimator.clip_values is not None:
193223
self._patch = np.clip(
@@ -199,12 +229,13 @@ def generate(self, x: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> n
199229
return self._patch
200230

201231
def _augment_images_with_patch(
202-
self, x: np.ndarray, patch: np.ndarray, channels_first: bool
232+
self, x: np.ndarray, y: Optional[List[Dict[str, np.ndarray]]], patch: np.ndarray, channels_first: bool
203233
) -> Tuple[np.ndarray, List[Dict[str, np.ndarray]], Dict[str, Union[int, float]]]:
204234
"""
205235
Augment images with patch.
206236
207237
:param x: Sample images.
238+
:param y: Target labels.
208239
:param patch: The patch to be applied.
209240
:param channels_first: Set channels first or last.
210241
"""
@@ -242,17 +273,73 @@ def _augment_images_with_patch(
242273

243274
transformations.update({"rot90": rot90})
244275

276+
if y is not None:
277+
278+
y_copy: List[Dict[str, np.ndarray]] = list()
279+
280+
for i_image in range(x_copy.shape[0]):
281+
y_b = y[i_image]["boxes"].copy()
282+
image_width = x.shape[2]
283+
image_height = x.shape[1]
284+
x_1_arr = y_b[:, 0]
285+
y_1_arr = y_b[:, 1]
286+
x_2_arr = y_b[:, 2]
287+
y_2_arr = y_b[:, 3]
288+
box_width = x_2_arr - x_1_arr
289+
box_height = y_2_arr - y_1_arr
290+
291+
if rot90 == 0:
292+
x_1_new = x_1_arr
293+
y_1_new = y_1_arr
294+
x_2_new = x_2_arr
295+
y_2_new = y_2_arr
296+
297+
if rot90 == 1:
298+
x_1_new = y_1_arr
299+
y_1_new = image_width - x_1_arr - box_width
300+
x_2_new = y_1_arr + box_height
301+
y_2_new = image_width - x_1_arr
302+
303+
if rot90 == 2:
304+
x_1_new = image_width - x_2_arr
305+
y_1_new = image_height - y_2_arr
306+
x_2_new = x_1_new + box_width
307+
y_2_new = y_1_new + box_height
308+
309+
if rot90 == 3:
310+
x_1_new = image_height - y_1_arr - box_height
311+
y_1_new = x_1_arr
312+
x_2_new = image_height - y_1_arr
313+
y_2_new = x_1_arr + box_width
314+
315+
y_i = dict()
316+
y_i["boxes"] = np.zeros_like(y[i_image]["boxes"])
317+
y_i["boxes"][:, 0] = x_1_new
318+
y_i["boxes"][:, 1] = y_1_new
319+
y_i["boxes"][:, 2] = x_2_new
320+
y_i["boxes"][:, 3] = y_2_new
321+
322+
y_i["labels"] = y[i_image]["labels"]
323+
y_i["scores"] = y[i_image]["scores"]
324+
325+
y_copy.append(y_i)
326+
245327
# 3) adjust brightness:
246328
brightness = random.uniform(*self.brightness_range)
247-
x_copy = np.round(brightness * x_copy)
248-
x_patch = np.round(brightness * x_patch)
329+
x_copy = np.round(brightness * x_copy / self.learning_rate) * self.learning_rate
330+
x_patch = np.round(brightness * x_patch / self.learning_rate) * self.learning_rate
249331

250332
transformations.update({"brightness": brightness})
251333

252334
logger.debug("Transformations: %s", str(transformations))
253335

254336
patch_target: List[Dict[str, np.ndarray]] = list()
255-
predictions = self.estimator.predict(x=x_copy)
337+
338+
if self.targeted:
339+
predictions = y_copy
340+
else:
341+
predictions = self.estimator.predict(x=x_copy, standardise_output=True)
342+
256343
for i_image in range(x_copy.shape[0]):
257344
target_dict = dict()
258345
target_dict["boxes"] = predictions[i_image]["boxes"]
@@ -385,8 +472,8 @@ def _check_params(self) -> None:
385472
if len(self.brightness_range) != 2:
386473
raise ValueError("The length of brightness range must be 2.")
387474

388-
if self.brightness_range[0] < 0.0 or self.brightness_range[1] > 1.0:
389-
raise ValueError("The brightness range must be between 0.0 and 1.0.")
475+
if self.brightness_range[0] < 0.0:
476+
raise ValueError("The brightness range must be >= 0.0.")
390477

391478
if self.brightness_range[0] > self.brightness_range[1]:
392479
raise ValueError("The first element of the brightness range must be less or equal to the second one.")
@@ -408,3 +495,6 @@ def _check_params(self) -> None:
408495
raise ValueError("The EOT sample size must be of type int.")
409496
if self.sample_size <= 0:
410497
raise ValueError("The EOT sample size must be greater than 0.")
498+
499+
if not isinstance(self.targeted, bool):
500+
raise ValueError("The argument `targeted` has to be of type bool.")

art/estimators/object_detection/object_detector.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
This module implements mixin abstract base class for all object detectors in ART.
2020
"""
2121

22-
from abc import ABC
22+
from abc import ABC, abstractmethod
2323

2424
from art.estimators.estimator import BaseEstimator
2525
from art.estimators.classification.classifier import LossGradientsMixin
@@ -30,6 +30,14 @@ class ObjectDetectorMixin(ABC):
3030
Mix-in Base class for ART object detectors.
3131
"""
3232

33+
@property
34+
@abstractmethod
35+
def native_label_is_pytorch_format(self) -> bool:
36+
"""
37+
Are the native labels in PyTorch format [x1, y1, x2, y2]?
38+
"""
39+
raise NotImplementedError
40+
3341

3442
class ObjectDetector(ObjectDetectorMixin, LossGradientsMixin, BaseEstimator, ABC):
3543
"""

art/estimators/object_detection/pytorch_faster_rcnn.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ def __init__(
131131
self._model.eval()
132132
self.attack_losses: Tuple[str, ...] = attack_losses
133133

134+
@property
135+
def native_label_is_pytorch_format(self) -> bool:
136+
"""
137+
Are the native labels in PyTorch format [x1, y1, x2, y2]?
138+
"""
139+
return True
140+
134141
@property
135142
def input_shape(self) -> Tuple[int, ...]:
136143
"""
@@ -156,13 +163,12 @@ def loss_gradient(
156163
Compute the gradient of the loss function w.r.t. `x`.
157164
158165
:param x: Samples of shape (nb_samples, height, width, nb_channels).
159-
:param y: Target values of format `List[Dict[Tensor]]`, one for each input image. The
160-
fields of the Dict are as follows:
166+
:param y: Target values of format `List[Dict[Tensor]]`, one for each input image. The fields of the Dict are as
167+
follows:
161168
162-
- boxes (FloatTensor[N, 4]): the predicted boxes in [x1, y1, x2, y2] format, with values \
163-
between 0 and H and 0 and W
164-
- labels (Int64Tensor[N]): the predicted labels for each image
165-
- scores (Tensor[N]): the scores or each prediction.
169+
- boxes (FloatTensor[N, 4]): the boxes in [x1, y1, x2, y2] format, with 0 <= x1 < x2 <= W and
170+
0 <= y1 < y2 <= H.
171+
- labels (Int64Tensor[N]): the labels for each image
166172
:return: Loss gradients of the same shape as `x`.
167173
"""
168174
import torch # lgtm [py/repeated-import]
@@ -181,7 +187,6 @@ def loss_gradient(
181187
y_t = dict()
182188
y_t["boxes"] = torch.from_numpy(y_i["boxes"]).type(torch.float).to(self._device)
183189
y_t["labels"] = torch.from_numpy(y_i["labels"]).type(torch.int64).to(self._device)
184-
y_t["scores"] = torch.from_numpy(y_i["scores"]).to(self._device)
185190
y_tensor.append(y_t)
186191
else:
187192
y_tensor = y
@@ -215,7 +220,6 @@ def loss_gradient(
215220
y_preprocessed_t = dict()
216221
y_preprocessed_t["boxes"] = torch.from_numpy(y_i["boxes"]).type(torch.float).to(self._device)
217222
y_preprocessed_t["labels"] = torch.from_numpy(y_i["labels"]).type(torch.int64).to(self._device)
218-
y_preprocessed_t["scores"] = torch.from_numpy(y_i["scores"]).to(self._device)
219223
y_preprocessed_tensor.append(y_preprocessed_t)
220224
y_preprocessed = y_preprocessed_tensor
221225

@@ -286,12 +290,11 @@ def predict(self, x: np.ndarray, batch_size: int = 128, **kwargs) -> List[Dict[s
286290
287291
:param x: Samples of shape (nb_samples, height, width, nb_channels).
288292
:param batch_size: Batch size.
289-
:return: Predictions of format `List[Dict[str, np.ndarray]]`, one for each input image. The
290-
fields of the Dict are as follows:
293+
:return: Predictions of format `List[Dict[str, np.ndarray]]`, one for each input image. The fields of the Dict
294+
are as follows:
291295
292-
- boxes [N, 4]: the predicted boxes in [x1, y1, x2, y2] format, with values \
293-
between 0 and H and 0 and W
294-
- labels [N]: the predicted labels for each image
296+
- boxes [N, 4]: the boxes in [x1, y1, x2, y2] format, with 0 <= x1 < x2 <= W and 0 <= y1 < y2 <= H.
297+
- labels [N]: the labels for each image
295298
- scores [N]: the scores or each prediction.
296299
"""
297300
import torchvision # lgtm [py/repeated-import]
@@ -330,12 +333,5 @@ def get_activations(
330333
def compute_loss(self, x: np.ndarray, y: np.ndarray, **kwargs) -> np.ndarray:
331334
"""
332335
Compute the loss of the neural network for samples `x`.
333-
334-
:param x: Samples of shape (nb_samples, nb_features) or (nb_samples, nb_pixels_1, nb_pixels_2,
335-
nb_channels) or (nb_samples, nb_channels, nb_pixels_1, nb_pixels_2).
336-
:param y: Target values (class labels) one-hot-encoded of shape `(nb_samples, nb_classes)` or indices
337-
of shape `(nb_samples,)`.
338-
:return: Loss values.
339-
:rtype: Format as expected by the `model`
340336
"""
341337
raise NotImplementedError

0 commit comments

Comments
 (0)