Skip to content

Commit 88639a6

Browse files
authored
Merge pull request #1120 from Trusted-AI/dev_1.6.2
Update to ART 1.6.2
2 parents acd5e04 + 2b9cd8e commit 88639a6

File tree

61 files changed

+877
-381
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+877
-381
lines changed

.github/workflows/ci-mxnet.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI MXNet
1+
name: CI MXNet + Legacy
22
on:
33
# Run on manual trigger
44
workflow_dispatch:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CI Style Checks
2+
on:
3+
# Run on manual trigger
4+
workflow_dispatch:
5+
6+
# Run on pull requests
7+
pull_request:
8+
paths-ignore:
9+
- '*.md'
10+
11+
# Run when pushing to main or dev branches
12+
push:
13+
branches:
14+
- main
15+
- dev*
16+
17+
# Run scheduled CI flow daily
18+
schedule:
19+
- cron: '0 8 * * 0'
20+
21+
jobs:
22+
style:
23+
name: Style Check
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout Repo
27+
uses: actions/checkout@v2
28+
- name: Setup Python
29+
uses: actions/setup-python@v2
30+
with:
31+
python-version: 3.7
32+
- name: Pre-install
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get -y -q install ffmpeg libavcodec-extra
36+
pip install tensorflow==2.4.1
37+
pip install keras==2.4.3
38+
- name: Install Dependencies
39+
run: |
40+
python -m pip install --upgrade pip setuptools wheel
41+
pip install -q pylint==2.7.4 mypy==0.812 pycodestyle==2.7.0 black==20.8b1
42+
pip install -q -r requirements.txt
43+
pip list
44+
- name: pycodestyle
45+
run: pycodestyle --ignore=C0330,C0415,E203,E231,W503 --max-line-length=120 art
46+
- name: pylint
47+
if: ${{ always() }}
48+
run: pylint --disable=C0330,C0415,E203,E1136,E0401,E1102 -rn art
49+
- name: mypy
50+
if: ${{ always() }}
51+
run: mypy art
52+
- name: pytest-flake8
53+
if: ${{ always() }}
54+
run: pytest --flake8 -v -m flake8
55+
- name: black
56+
if: ${{ always() }}
57+
run: |
58+
black --line-length 120 --check art/
59+
black --line-length 120 --check tests/
60+
black --line-length 120 --check examples/

.github/workflows/ci.yml

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -134,36 +134,3 @@ jobs:
134134
run: ./run_tests.sh ${{ matrix.framework }}
135135
- name: Upload coverage to Codecov
136136
uses: codecov/[email protected]
137-
style:
138-
name: Style Check
139-
runs-on: ubuntu-latest
140-
steps:
141-
- name: Checkout Repo
142-
uses: actions/[email protected]
143-
- name: Setup Python
144-
uses: actions/[email protected]
145-
with:
146-
python-version: 3.7
147-
- name: Pre-install
148-
run: |
149-
sudo apt-get update
150-
sudo apt-get -y -q install ffmpeg libavcodec-extra
151-
pip install tensorflow==2.4.1
152-
pip install keras==2.4.3
153-
- name: Install Dependencies
154-
run: |
155-
python -m pip install --upgrade pip setuptools wheel
156-
pip install -q pylint==2.7.4 pycodestyle==2.7.0
157-
pip install -q -r requirements.txt
158-
pip list
159-
- name: pycodestyle
160-
run: pycodestyle --ignore=C0330,C0415,E203,E231,W503 --max-line-length=120 art
161-
- name: pylint
162-
if: ${{ always() }}
163-
run: pylint --disable=C0330,C0415,E203,E1136,E0401,E1102 -rn art
164-
- name: mypy
165-
if: ${{ always() }}
166-
run: mypy art
167-
- name: pytest-flake8
168-
if: ${{ always() }}
169-
run: pytest --flake8 -v -m flake8

art/attacks/evasion/adversarial_patch/adversarial_patch_pytorch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ def __init__(
100100
import torch # lgtm [py/repeated-import]
101101
import torchvision
102102

103-
torch_version = list(map(int, torch.__version__.lower().split(".")))
104-
torchvision_version = list(map(int, torchvision.__version__.lower().split(".")))
103+
torch_version = list(map(int, torch.__version__.lower().split("+")[0].split(".")))
104+
torchvision_version = list(map(int, torchvision.__version__.lower().split("+")[0].split(".")))
105105
assert torch_version[0] >= 1 and torch_version[1] >= 7, "AdversarialPatchPyTorch requires torch>=1.7.0"
106106
assert (
107107
torchvision_version[0] >= 0 and torchvision_version[1] >= 8

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/attacks/evasion/iterative_method.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def __init__(
5353
max_iter: int = 100,
5454
targeted: bool = False,
5555
batch_size: int = 32,
56+
verbose: bool = True,
5657
) -> None:
5758
"""
5859
Create a :class:`.ProjectedGradientDescent` instance.
@@ -63,6 +64,7 @@ def __init__(
6364
:param max_iter: The maximum number of iterations.
6465
:param targeted: Indicates whether the attack is targeted (True) or untargeted (False).
6566
:param batch_size: Size of the batch on which adversarial samples are generated.
67+
:param verbose: Show progress bars.
6668
"""
6769
super().__init__(
6870
estimator=estimator,
@@ -73,4 +75,5 @@ def __init__(
7375
targeted=targeted,
7476
num_random_init=0,
7577
batch_size=batch_size,
78+
verbose=verbose,
7679
)

art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_pytorch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def _compute_perturbation( # pylint: disable=W0221
293293

294294
# Apply mask
295295
if mask is not None:
296-
grad = torch.where(mask == 0.0, torch.tensor(0.0), grad)
296+
grad = torch.where(mask == 0.0, torch.tensor(0.0).to(self.estimator.device), grad)
297297

298298
# Apply norm bound
299299
if self.norm in ["inf", np.inf]:

art/defences/detector/poison/ground_truth_evaluator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def analyze_correctness(
9595
dic_json.update({key_i: matrix_i})
9696
all_errors_by_class.append(errors)
9797

98-
all_errors_by_class = np.asarray(all_errors_by_class)
98+
all_errors_by_class = np.asarray(all_errors_by_class, dtype=object)
9999
conf_matrix_json = json.dumps(dic_json)
100100

101101
return all_errors_by_class, conf_matrix_json

0 commit comments

Comments
 (0)