Skip to content

Commit 3862abe

Browse files
authored
Add Lp-bounded patch adversary for object detection (#242)
* Create a folder for attack.composer. * Add composer modules for unbounded patch adversary. * Add config of Adam optimizer. * Add LoadCoords for patch adversary. * Add a config of unbounded patch adversary. * Add a datamodule config for carla patch adversary. * Fix the simple Linf projection. * Add composer module PertImageBase for Lp bounded patch adversary. * Add config of lp-bounded patch adversary. * Formatting
1 parent 1360ab0 commit 3862abe

File tree

6 files changed

+114
-5
lines changed

6 files changed

+114
-5
lines changed

mart/attack/composer/patch.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
import torch
1010
import torchvision.transforms.functional as F
11+
from torchvision.io import read_image
1112

1213
__all__ = [
1314
"PertRectSize",
1415
"PertExtractRect",
1516
"PertRectPerspective",
17+
"PertImageBase",
1618
]
1719

1820

@@ -71,3 +73,44 @@ def forward(self, perturbation, input, coords):
7173
)
7274

7375
return perturbation
76+
77+
78+
class PertImageBase(torch.nn.Module):
79+
"""Resize an image and add to perturbation."""
80+
81+
def __init__(self, fpath):
82+
super().__init__()
83+
# RGBA -> RGB
84+
self.image_orig = read_image(fpath)[:3, :, :]
85+
self.image = None
86+
87+
# Project the result with the pixel value constraint.
88+
self.image_clamp = FakeClamp(min=0, max=255)
89+
90+
def forward(self, perturbation):
91+
# Initialize the image with new shape of perturbation.
92+
if self.image is None or self.image.shape[-2:] != perturbation.shape[-2:]:
93+
height, width = perturbation.shape[-2:]
94+
self.image = F.resize(self.image_orig, size=[height, width])
95+
self.image = self.image.to(device=perturbation.device)
96+
97+
perturbation = self.image + perturbation
98+
perturbation = self.image_clamp(perturbation)
99+
100+
return perturbation
101+
102+
103+
class FakeClamp(torch.nn.Module):
104+
"""Clamp the data, but keep the gradient."""
105+
106+
def __init__(self, *, min, max):
107+
super().__init__()
108+
self.min = min
109+
self.max = max
110+
111+
def forward(self, a):
112+
with torch.no_grad():
113+
delta = a.clamp(min=self.min, max=self.max) - a
114+
115+
a = a + delta
116+
return a

mart/attack/projector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ def __init__(self, eps: int | float, p: int | float = torch.inf):
131131
@torch.no_grad()
132132
def project_(self, perturbation, *, input, target):
133133
pert_norm = perturbation.norm(p=self.p)
134-
if pert_norm > self.eps:
134+
if self.p == torch.inf:
135+
perturbation.clamp_(-self.eps, self.eps)
136+
elif pert_norm > self.eps:
135137
# We only upper-bound the norm.
136138
perturbation.mul_(self.eps / pert_norm)
137139

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pert_image_base:
2+
_target_: mart.attack.composer.PertImageBase
3+
fpath: ???
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
_target_: mart.attack.projector.Lp
2+
# p is actually torch.inf by default.
3+
p:
4+
_target_: builtins.float
5+
_args_: ["inf"]
6+
eps: ???
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defaults:
2+
- adversary
3+
- /optimizer@optimizer: adam
4+
- enforcer: default
5+
- composer: default
6+
- composer/perturber/initializer: uniform
7+
- composer/perturber/projector: linf
8+
- composer/modules:
9+
[
10+
pert_rect_size,
11+
pert_extract_rect,
12+
pert_image_base,
13+
pert_rect_perspective,
14+
overlay,
15+
]
16+
- gradient_modifier: sign
17+
- gain: rcnn_training_loss
18+
- objective: zero_ap
19+
- override /callbacks@callbacks: [progress_bar, image_visualizer]
20+
21+
max_iters: ???
22+
lr: ???
23+
eps: ???
24+
25+
optimizer:
26+
maximize: True
27+
lr: ${..lr}
28+
29+
enforcer:
30+
# No constraints with complex renderer in the pipeline.
31+
# TODO: Constraint on digital perturbation?
32+
constraints: {}
33+
34+
composer:
35+
perturber:
36+
initializer:
37+
min: ${negate:${....eps}}
38+
max: ${....eps}
39+
projector:
40+
eps: ${....eps}
41+
modules:
42+
pert_image_base:
43+
fpath: ???
44+
sequence:
45+
seq010:
46+
pert_rect_size: ["target.coords"]
47+
seq020:
48+
pert_extract_rect:
49+
["perturbation", "pert_rect_size.height", "pert_rect_size.width"]
50+
seq030:
51+
pert_image_base: ["pert_extract_rect"]
52+
seq040:
53+
pert_rect_perspective: ["pert_image_base", "input", "target.coords"]
54+
seq050:
55+
overlay: ["pert_rect_perspective", "input", "target.perturbable_mask"]

tests/test_projector.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_compose(input_data, target_data):
158158
tensor.norm.return_value = 10
159159
compose(tensor, input=input_data, target=target_data)
160160

161-
# RangeProjector, RangeAdditiveProjector, and LinfAdditiveRangeProjector calls `clamp_`
162-
assert tensor.clamp_.call_count == 3
163-
# LpProjector and MaskProjector calls `mul_`
164-
assert tensor.mul_.call_count == 2
161+
# RangeProjector, RangeAdditiveProjector, LpProjector_inf, and LinfAdditiveRangeProjector calls `clamp_`
162+
assert tensor.clamp_.call_count == 4
163+
# MaskProjector calls `mul_`
164+
assert tensor.mul_.call_count == 1

0 commit comments

Comments
 (0)