adv-lib 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. adv_lib/__init__.py +1 -0
  2. adv_lib/attacks/__init__.py +13 -0
  3. adv_lib/attacks/augmented_lagrangian.py +243 -0
  4. adv_lib/attacks/auto_pgd.py +523 -0
  5. adv_lib/attacks/boundary_projection_tf.py +170 -0
  6. adv_lib/attacks/carlini_wagner/__init__.py +2 -0
  7. adv_lib/attacks/carlini_wagner/l2.py +151 -0
  8. adv_lib/attacks/carlini_wagner/linf.py +158 -0
  9. adv_lib/attacks/decoupled_direction_norm.py +113 -0
  10. adv_lib/attacks/fast_adaptive_boundary/__init__.py +1 -0
  11. adv_lib/attacks/fast_adaptive_boundary/fast_adaptive_boundary.py +215 -0
  12. adv_lib/attacks/fast_adaptive_boundary/projections.py +164 -0
  13. adv_lib/attacks/fast_minimum_norm.py +218 -0
  14. adv_lib/attacks/perceptual_color_attacks/__init__.py +1 -0
  15. adv_lib/attacks/perceptual_color_attacks/differential_color_functions.py +181 -0
  16. adv_lib/attacks/perceptual_color_attacks/perceptual_color_distance_al.py +128 -0
  17. adv_lib/attacks/primal_dual_gradient_descent.py +379 -0
  18. adv_lib/attacks/projected_gradient_descent.py +109 -0
  19. adv_lib/attacks/segmentation/__init__.py +4 -0
  20. adv_lib/attacks/segmentation/alma_prox.py +283 -0
  21. adv_lib/attacks/segmentation/asma.py +92 -0
  22. adv_lib/attacks/segmentation/dense_adversary.py +83 -0
  23. adv_lib/attacks/segmentation/primal_dual_gradient_descent.py +349 -0
  24. adv_lib/attacks/self_adaptive_norm_update.py +127 -0
  25. adv_lib/attacks/sigma_zero.py +119 -0
  26. adv_lib/attacks/stochastic_sparse_attacks.py +237 -0
  27. adv_lib/attacks/structured_adversarial_attack.py +289 -0
  28. adv_lib/attacks/trust_region.py +153 -0
  29. adv_lib/distances/__init__.py +0 -0
  30. adv_lib/distances/color_difference.py +212 -0
  31. adv_lib/distances/lp_norms.py +18 -0
  32. adv_lib/distances/lpips.py +99 -0
  33. adv_lib/distances/structural_similarity.py +147 -0
  34. adv_lib/utils/__init__.py +1 -0
  35. adv_lib/utils/attack_utils.py +226 -0
  36. adv_lib/utils/color_conversions.py +71 -0
  37. adv_lib/utils/image_selection.py +27 -0
  38. adv_lib/utils/lagrangian_penalties/__init__.py +1 -0
  39. adv_lib/utils/lagrangian_penalties/all_penalties.py +67 -0
  40. adv_lib/utils/lagrangian_penalties/penalty_functions.py +79 -0
  41. adv_lib/utils/lagrangian_penalties/scripts/plot_penalties.py +42 -0
  42. adv_lib/utils/lagrangian_penalties/scripts/plot_univariates.py +32 -0
  43. adv_lib/utils/lagrangian_penalties/univariate_functions.py +299 -0
  44. adv_lib/utils/losses.py +29 -0
  45. adv_lib/utils/projections.py +100 -0
  46. adv_lib/utils/utils.py +58 -0
  47. adv_lib/utils/visdom_logger.py +109 -0
  48. adv_lib-0.2.2.dist-info/LICENSE +29 -0
  49. adv_lib-0.2.2.dist-info/METADATA +170 -0
  50. adv_lib-0.2.2.dist-info/RECORD +52 -0
  51. adv_lib-0.2.2.dist-info/WHEEL +5 -0
  52. adv_lib-0.2.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,164 @@
1
+ import math
2
+
3
+ import torch
4
+ from torch import Tensor
5
+ from torch.nn import functional as F
6
+
7
+
8
+ def projection_l1(points_to_project: Tensor, w_hyperplane: Tensor, b_hyperplane: Tensor) -> Tensor:
9
+ device = points_to_project.device
10
+ t, w, b = points_to_project, w_hyperplane.clone(), b_hyperplane
11
+
12
+ c = (w * t).sum(1).sub_(b)
13
+ ind2 = (c >= 0).float().mul_(2).sub_(1)
14
+ w.mul_(ind2.unsqueeze(1))
15
+ c.mul_(ind2)
16
+
17
+ w_abs = w.abs()
18
+ r = (1 / w_abs).clamp_(max=1e12)
19
+ indr = torch.argsort(r, dim=1)
20
+ indr_rev = torch.argsort(indr)
21
+
22
+ d = (w < 0).float().sub_(t).mul_(w != 0)
23
+ ds = torch.min(-w * t, (1 - t).mul_(w)).gather(1, indr)
24
+ ds2 = torch.cat((c.unsqueeze(-1), ds), 1)
25
+ s = torch.cumsum(ds2, dim=1)
26
+
27
+ c2 = s[:, -1] < 0
28
+
29
+ lb = torch.zeros(c2.sum(), device=device)
30
+ ub = torch.full_like(lb, s.shape[1])
31
+ nitermax = math.ceil(math.log2(w.shape[1]))
32
+
33
+ s_ = s[c2]
34
+ for counter in range(nitermax):
35
+ counter4 = (lb + ub).mul_(0.5).floor_()
36
+ counter2 = counter4.long().unsqueeze(1)
37
+ c3 = s_.gather(1, counter2).squeeze(1) > 0
38
+ lb = torch.where(c3, counter4, lb)
39
+ ub = torch.where(c3, ub, counter4)
40
+
41
+ lb2 = lb.long()
42
+
43
+ if c2.any():
44
+ indr = indr[c2].gather(1, lb2.unsqueeze(1)).squeeze(1)
45
+ u = torch.arange(0, w.shape[0], device=device).unsqueeze(1)
46
+ u2 = torch.arange(0, w.shape[1], device=device, dtype=torch.float).unsqueeze(0)
47
+ alpha = s[c2, lb2].neg().div_(w[c2, indr])
48
+ c5 = u2 < lb.unsqueeze(-1)
49
+ u3 = c5[u[:c5.shape[0]], indr_rev[c2]]
50
+ d[c2] *= u3
51
+ d[c2, indr] = alpha
52
+
53
+ return d.mul_(w_abs > 1e-8)
54
+
55
+
56
+ def projection_l2(points_to_project: Tensor, w_hyperplane: Tensor, b_hyperplane: Tensor) -> Tensor:
57
+ device = points_to_project.device
58
+ t, w, b = points_to_project, w_hyperplane.clone(), b_hyperplane
59
+
60
+ c = (w * t).sum(1).sub_(b)
61
+ ind2 = (c >= 0).float().mul_(2).sub_(1)
62
+ w.mul_(ind2.unsqueeze(1))
63
+ w_nonzero = w.abs() > 1e-8
64
+ c.mul_(ind2)
65
+
66
+ r = torch.maximum(t / w, (t - 1).div_(w)).clamp_(min=-1e12, max=1e12)
67
+ r.masked_fill_(~w_nonzero, 1e12)
68
+ r[r == -1e12] *= -1
69
+ rs, indr = torch.sort(r, dim=1)
70
+ rs2 = F.pad(rs[:, 1:], (0, 1))
71
+ rs.masked_fill_(rs == 1e12, 0)
72
+ rs2.masked_fill_(rs2 == 1e12, 0)
73
+
74
+ w3s = w.square().gather(1, indr)
75
+ w5 = w3s.sum(dim=1, keepdim=True)
76
+ ws = w5 - torch.cumsum(w3s, dim=1)
77
+ d = (r * w).neg_()
78
+ d.mul_(w_nonzero)
79
+ s = torch.cat((-w5 * rs[:, 0:1], torch.cumsum((rs - rs2).mul_(ws), dim=1).sub_(w5 * rs[:, 0:1])), 1)
80
+
81
+ c4 = s[:, 0] + c < 0
82
+ c3 = (d * w).sum(dim=1).add_(c) > 0
83
+ c2 = ~(c4 | c3)
84
+
85
+ lb = torch.zeros(c2.sum(), device=device)
86
+ ub = torch.full_like(lb, w.shape[1] - 1)
87
+ nitermax = math.ceil(math.log2(w.shape[1]))
88
+
89
+ s_, c_ = s[c2], c[c2]
90
+ for counter in range(nitermax):
91
+ counter4 = (lb + ub).mul_(0.5).floor_()
92
+ counter2 = counter4.long().unsqueeze(1)
93
+ c3 = s_.gather(1, counter2).squeeze(1).add_(c_) > 0
94
+ lb = torch.where(c3, counter4, lb)
95
+ ub = torch.where(c3, ub, counter4)
96
+
97
+ lb = lb.long()
98
+
99
+ if c4.any():
100
+ alpha = c[c4] / w5[c4].squeeze(-1)
101
+ d[c4] = -alpha.unsqueeze(-1) * w[c4]
102
+
103
+ if c2.any():
104
+ alpha = (s[c2, lb] + c[c2]).div_(ws[c2, lb]).add_(rs[c2, lb])
105
+ alpha[ws[c2, lb] == 0] = 0
106
+ c5 = alpha.unsqueeze(-1) > r[c2]
107
+ d[c2] = (d[c2] * c5).sub_((~c5).float().mul_(alpha.unsqueeze(-1)).mul_(w[c2]))
108
+
109
+ return d.mul_(w_nonzero)
110
+
111
+
112
+ def projection_linf(points_to_project: Tensor, w_hyperplane: Tensor, b_hyperplane: Tensor) -> Tensor:
113
+ device = points_to_project.device
114
+ t, w, b = points_to_project, w_hyperplane.clone(), b_hyperplane.clone()
115
+
116
+ sign = ((w * t).sum(1).sub_(b) >= 0).float().mul_(2).sub_(1)
117
+ w.mul_(sign.unsqueeze(1))
118
+ b.mul_(sign)
119
+
120
+ a = (w < 0).float()
121
+ d = (a - t).mul_(w != 0)
122
+
123
+ p = (2 * a).sub_(1).mul_(t).neg_().add_(a)
124
+ indp = torch.argsort(p, dim=1)
125
+
126
+ b.sub_((w * t).sum(1))
127
+ b0 = (w * d).sum(1)
128
+
129
+ indp2 = indp.flip((1,))
130
+ ws = w.gather(1, indp2)
131
+ bs2 = -ws * d.gather(1, indp2)
132
+
133
+ s = torch.cumsum(ws.abs_(), dim=1)
134
+ sb = torch.cumsum(bs2, dim=1).add_(b0.unsqueeze(1))
135
+
136
+ b2 = sb[:, -1] - s[:, -1] * p.gather(1, indp[:, 0:1]).squeeze(1)
137
+ c_l = b - b2 > 0
138
+ c2 = (b - b0 > 0) & (~c_l)
139
+ lb = torch.zeros(c2.sum(), device=device)
140
+ ub = torch.full_like(lb, w.shape[1] - 1)
141
+ nitermax = math.ceil(math.log2(w.shape[1]))
142
+
143
+ indp_, sb_, s_, p_, b_ = indp[c2], sb[c2], s[c2], p[c2], b[c2]
144
+ for counter in range(nitermax):
145
+ counter4 = (lb + ub).mul_(0.5).floor_()
146
+
147
+ counter2 = counter4.long().unsqueeze(1)
148
+ indcurr = indp_.gather(1, indp_.size(1) - 1 - counter2)
149
+ b2 = sb_.gather(1, counter2).sub_(s_.gather(1, counter2).mul_(p_.gather(1, indcurr))).squeeze(1)
150
+ c = b_ - b2 > 0
151
+
152
+ lb = torch.where(c, counter4, lb)
153
+ ub = torch.where(c, ub, counter4)
154
+
155
+ lb = lb.long()
156
+
157
+ if c_l.any():
158
+ lmbd_opt = (b[c_l] - sb[c_l, -1]).div_(-s[c_l, -1]).clamp_(min=0).unsqueeze_(-1)
159
+ d[c_l] = (2 * a[c_l]).sub_(1).mul_(lmbd_opt)
160
+
161
+ lmbd_opt = (b[c2] - sb[c2, lb]).div_(-s[c2, lb]).clamp_(min=0).unsqueeze_(-1)
162
+ d[c2] = torch.minimum(lmbd_opt, d[c2]).mul_(a[c2]).add_(torch.maximum(-lmbd_opt, d[c2]).mul_(1 - a[c2]))
163
+
164
+ return d.mul_(w != 0)
@@ -0,0 +1,218 @@
1
+ # Adapted from https://github.com/maurapintor/Fast-Minimum-Norm-FMN-Attack
2
+
3
+ import math
4
+ from functools import partial
5
+ from typing import Optional
6
+
7
+ import torch
8
+ from torch import Tensor, nn
9
+ from torch.autograd import grad
10
+
11
+ from adv_lib.utils.losses import difference_of_logits
12
+ from adv_lib.utils.projections import clamp_, l1_ball_euclidean_projection
13
+
14
+
15
+ def l0_projection_(δ: Tensor, ε: Tensor) -> Tensor:
16
+ """In-place l0 projection"""
17
+ δ = δ.flatten(1)
18
+ δ_abs = δ.abs()
19
+ thresholds = δ_abs.topk(k=ε.long().max(), dim=1).values.gather(1, (ε.long().unsqueeze(1) - 1).clamp_(min=0))
20
+ δ[δ_abs < thresholds] = 0
21
+ return δ
22
+
23
+
24
+ def l1_projection_(δ: Tensor, ε: Tensor) -> Tensor:
25
+ """In-place l1 projection"""
26
+ δ = l1_ball_euclidean_projection(x=δ.flatten(1), ε=ε, inplace=True)
27
+ return δ
28
+
29
+
30
+ def l2_projection_(δ: Tensor, ε: Tensor) -> Tensor:
31
+ """In-place l2 projection"""
32
+ δ = δ.flatten(1)
33
+ l2_norms = δ.norm(p=2, dim=1, keepdim=True).clamp_(min=1e-12)
34
+ δ.mul_(ε.unsqueeze(1) / l2_norms).clamp_(max=1)
35
+ return δ
36
+
37
+
38
+ def linf_projection_(δ: Tensor, ε: Tensor) -> Tensor:
39
+ """In-place linf projection"""
40
+ δ, ε = δ.flatten(1), ε.unsqueeze(1)
41
+ δ = clamp_(δ, lower=-ε, upper=ε)
42
+ return δ
43
+
44
+
45
+ def l0_mid_points(x0: Tensor, x1: Tensor, ε: Tensor) -> Tensor:
46
+ n_features = x0[0].numel()
47
+ δ = l0_projection_(δ=x1 - x0, ε=n_features * ε)
48
+ return δ.view_as(x0).add_(x0)
49
+
50
+
51
+ def l1_mid_points(x0: Tensor, x1: Tensor, ε: Tensor) -> Tensor:
52
+ threshold = (1 - ε).unsqueeze(1)
53
+ δ = (x1 - x0).flatten(1)
54
+ δ_abs = δ.abs()
55
+ mask = δ_abs <= threshold
56
+ mid_points = δ_abs.sub_(threshold).copysign_(δ)
57
+ mid_points[mask] = 0
58
+ return mid_points.view_as(x0).add_(x0)
59
+
60
+
61
+ def l2_mid_points(x0: Tensor, x1: Tensor, ε: Tensor) -> Tensor:
62
+ ε = ε.unsqueeze(1)
63
+ return x0.flatten(1).mul(1 - ε).addcmul_(ε, x1.flatten(1)).view_as(x0)
64
+
65
+
66
+ def linf_mid_points(x0: Tensor, x1: Tensor, ε: Tensor) -> Tensor:
67
+ ε = ε.unsqueeze(1)
68
+ δ = clamp_((x1 - x0).flatten(1), lower=-ε, upper=ε)
69
+ return δ.view_as(x0).add_(x0)
70
+
71
+
72
+ def fmn(model: nn.Module,
73
+ inputs: Tensor,
74
+ labels: Tensor,
75
+ norm: float,
76
+ targeted: bool = False,
77
+ steps: int = 10,
78
+ α_init: float = 1.0,
79
+ α_final: Optional[float] = None,
80
+ γ_init: float = 0.05,
81
+ γ_final: float = 0.001,
82
+ starting_points: Optional[Tensor] = None,
83
+ binary_search_steps: int = 10) -> Tensor:
84
+ """
85
+ Fast Minimum-Norm attack from https://arxiv.org/abs/2102.12827.
86
+
87
+ Parameters
88
+ ----------
89
+ model : nn.Module
90
+ Model to attack.
91
+ inputs : Tensor
92
+ Inputs to attack. Should be in [0, 1].
93
+ labels : Tensor
94
+ Labels corresponding to the inputs if untargeted, else target labels.
95
+ norm : float
96
+ Norm to minimize in {0, 1, 2 ,float('inf')}.
97
+ targeted : bool
98
+ Whether to perform a targeted attack or not.
99
+ steps : int
100
+ Number of optimization steps.
101
+ α_init : float
102
+ Initial step size.
103
+ α_final : float
104
+ Final step size after cosine annealing.
105
+ γ_init : float
106
+ Initial factor by which ε is modified: ε = ε * (1 + or - γ).
107
+ γ_final : float
108
+ Final factor, after cosine annealing, by which ε is modified.
109
+ starting_points : Tensor
110
+ Optional warm-start for the attack.
111
+ binary_search_steps : int
112
+ Number of binary search steps to find the decision boundary between inputs and starting_points.
113
+
114
+ Returns
115
+ -------
116
+ adv_inputs : Tensor
117
+ Modified inputs to be adversarial to the model.
118
+
119
+ """
120
+ _dual_projection_mid_points = {
121
+ 0: (None, l0_projection_, l0_mid_points),
122
+ 1: (float('inf'), l1_projection_, l1_mid_points),
123
+ 2: (2, l2_projection_, l2_mid_points),
124
+ float('inf'): (1, linf_projection_, linf_mid_points),
125
+ }
126
+ if inputs.min() < 0 or inputs.max() > 1: raise ValueError('Input values should be in the [0, 1] range.')
127
+ device = inputs.device
128
+ batch_size = len(inputs)
129
+ batch_view = lambda tensor: tensor.view(batch_size, *[1] * (inputs.ndim - 1))
130
+ dual, projection, mid_point = _dual_projection_mid_points[norm]
131
+ α_final = α_init / 100 if α_final is None else α_final
132
+ multiplier = 1 if targeted else -1
133
+
134
+ # If starting_points is provided, search for the boundary
135
+ if starting_points is not None:
136
+ start_preds = model(starting_points).argmax(dim=1)
137
+ is_adv = (start_preds == labels) if targeted else (start_preds != labels)
138
+ if not is_adv.all():
139
+ raise ValueError('Starting points are not all adversarial.')
140
+ lower_bound = torch.zeros(batch_size, device=device)
141
+ upper_bound = torch.ones(batch_size, device=device)
142
+ for _ in range(binary_search_steps):
143
+ ε = (lower_bound + upper_bound) / 2
144
+ mid_points = mid_point(x0=inputs, x1=starting_points, ε=ε)
145
+ pred_labels = model(mid_points).argmax(dim=1)
146
+ is_adv = (pred_labels == labels) if targeted else (pred_labels != labels)
147
+ lower_bound = torch.where(is_adv, lower_bound, ε)
148
+ upper_bound = torch.where(is_adv, ε, upper_bound)
149
+
150
+ δ = mid_point(x0=inputs, x1=starting_points, ε=upper_bound).sub_(inputs)
151
+ else:
152
+ δ = torch.zeros_like(inputs)
153
+ δ.requires_grad_(True)
154
+
155
+ if norm == 0:
156
+ ε = torch.ones(batch_size, device=device) if starting_points is None else δ.flatten(1).norm(p=0, dim=1)
157
+ else:
158
+ ε = torch.full((batch_size,), float('inf'), device=device)
159
+
160
+ # Init trackers
161
+ worst_norm = torch.maximum(inputs, 1 - inputs).flatten(1).norm(p=norm, dim=1)
162
+ best_norm = worst_norm.clone()
163
+ best_adv = inputs.clone()
164
+ adv_found = torch.zeros(batch_size, dtype=torch.bool, device=device)
165
+
166
+ for i in range(steps):
167
+ cosine = (1 + math.cos(math.pi * i / steps)) / 2
168
+ α = α_final + (α_init - α_final) * cosine
169
+ γ = γ_final + (γ_init - γ_final) * cosine
170
+
171
+ δ_norm = δ.data.flatten(1).norm(p=norm, dim=1)
172
+ adv_inputs = inputs + δ
173
+ logits = model(adv_inputs)
174
+ pred_labels = logits.argmax(dim=1)
175
+
176
+ if i == 0:
177
+ labels_infhot = torch.zeros_like(logits).scatter_(1, labels.unsqueeze(1), float('inf'))
178
+ logit_diff_func = partial(difference_of_logits, labels=labels, labels_infhot=labels_infhot)
179
+
180
+ logit_diffs = logit_diff_func(logits=logits)
181
+ loss = multiplier * logit_diffs
182
+ δ_grad = grad(loss.sum(), δ, only_inputs=True)[0]
183
+
184
+ is_adv = (pred_labels == labels) if targeted else (pred_labels != labels)
185
+ is_smaller = δ_norm < best_norm
186
+ is_both = is_adv & is_smaller
187
+ adv_found.logical_or_(is_adv)
188
+ best_norm = torch.where(is_both, δ_norm, best_norm)
189
+ best_adv = torch.where(batch_view(is_both), adv_inputs.detach(), best_adv)
190
+
191
+ if norm == 0:
192
+ ε = torch.where(is_adv,
193
+ torch.minimum(torch.minimum(ε - 1, (ε * (1 - γ)).floor_()), best_norm),
194
+ torch.maximum(ε + 1, (ε * (1 + γ)).floor_()))
195
+ ε.clamp_(min=0)
196
+ else:
197
+ distance_to_boundary = loss.detach().abs() / δ_grad.flatten(1).norm(p=dual, dim=1).clamp_(min=1e-12)
198
+ ε = torch.where(is_adv,
199
+ torch.minimum(ε * (1 - γ), best_norm),
200
+ torch.where(adv_found, ε * (1 + γ), δ_norm + distance_to_boundary))
201
+
202
+ # clip ε
203
+ ε = torch.minimum(ε, worst_norm)
204
+
205
+ # normalize gradient
206
+ grad_l2_norms = δ_grad.flatten(1).norm(p=2, dim=1).clamp_(min=1e-12)
207
+ δ_grad.div_(batch_view(grad_l2_norms))
208
+
209
+ # gradient ascent step
210
+ δ.data.add_(δ_grad, alpha=α)
211
+
212
+ # project in place
213
+ projection(δ=δ.data, ε=ε)
214
+
215
+ # clamp
216
+ δ.data.add_(inputs).clamp_(min=0, max=1).sub_(inputs)
217
+
218
+ return best_adv
@@ -0,0 +1 @@
1
+ from .perceptual_color_distance_al import perc_al
@@ -0,0 +1,181 @@
1
+ # Adapted from https://github.com/ZhengyuZhao/PerC-Adversarial
2
+ import numpy as np
3
+ import torch
4
+ from torch import Tensor
5
+
6
+
7
+ def rgb2xyz(rgb_image):
8
+ device = rgb_image.device
9
+ mt = torch.tensor([[0.4124, 0.3576, 0.1805],
10
+ [0.2126, 0.7152, 0.0722],
11
+ [0.0193, 0.1192, 0.9504]], device=device)
12
+ mask1 = (rgb_image > 0.0405).float()
13
+ mask1_no = 1 - mask1
14
+ temp_img = mask1 * (((rgb_image + 0.055) / 1.055) ** 2.4)
15
+ temp_img = temp_img + mask1_no * (rgb_image / 12.92)
16
+ temp_img = 100 * temp_img
17
+
18
+ res = torch.matmul(mt, temp_img.permute(1, 0, 2, 3).contiguous().view(3, -1)).view(
19
+ 3, rgb_image.size(0), rgb_image.size(2), rgb_image.size(3)).permute(1, 0, 2, 3)
20
+ return res
21
+
22
+
23
+ def xyz_lab(xyz_image):
24
+ mask_value_0 = (xyz_image == 0).float()
25
+ mask_value_0_no = 1 - mask_value_0
26
+ xyz_image = xyz_image + 0.0001 * mask_value_0
27
+ mask1 = (xyz_image > 0.008856).float()
28
+ mask1_no = 1 - mask1
29
+ res = mask1 * (xyz_image) ** (1 / 3)
30
+ res = res + mask1_no * ((7.787 * xyz_image) + (16 / 116))
31
+ res = res * mask_value_0_no
32
+ return res
33
+
34
+
35
+ def rgb2lab_diff(rgb_image):
36
+ """
37
+ Function to convert a batch of image tensors from RGB space to CIELAB space.
38
+ parameters: xn, yn, zn are the CIE XYZ tristimulus values of the reference white point.
39
+ Here use the standard Illuminant D65 with normalization Y = 100.
40
+ """
41
+ rgb_image = rgb_image
42
+ res = torch.zeros_like(rgb_image)
43
+ xyz_image = rgb2xyz(rgb_image)
44
+
45
+ xn = 95.0489
46
+ yn = 100
47
+ zn = 108.8840
48
+
49
+ x = xyz_image[:, 0, :, :]
50
+ y = xyz_image[:, 1, :, :]
51
+ z = xyz_image[:, 2, :, :]
52
+
53
+ L = 116 * xyz_lab(y / yn) - 16
54
+ a = 500 * (xyz_lab(x / xn) - xyz_lab(y / yn))
55
+ b = 200 * (xyz_lab(y / yn) - xyz_lab(z / zn))
56
+ res[:, 0, :, :] = L
57
+ res[:, 1, :, :] = a
58
+ res[:, 2, :, :] = b
59
+
60
+ return res
61
+
62
+
63
+ def degrees(n): return n * (180. / np.pi)
64
+
65
+
66
+ def radians(n): return n * (np.pi / 180.)
67
+
68
+
69
+ def hpf_diff(x, y):
70
+ mask1 = ((x == 0) * (y == 0)).float()
71
+ mask1_no = 1 - mask1
72
+
73
+ tmphp = degrees(torch.atan2(x * mask1_no, y * mask1_no))
74
+ tmphp1 = tmphp * (tmphp >= 0).float()
75
+ tmphp2 = (360 + tmphp) * (tmphp < 0).float()
76
+
77
+ return tmphp1 + tmphp2
78
+
79
+
80
+ def dhpf_diff(c1, c2, h1p, h2p):
81
+ mask1 = ((c1 * c2) == 0).float()
82
+ mask1_no = 1 - mask1
83
+ res1 = (h2p - h1p) * mask1_no * (torch.abs(h2p - h1p) <= 180).float()
84
+ res2 = ((h2p - h1p) - 360) * ((h2p - h1p) > 180).float() * mask1_no
85
+ res3 = ((h2p - h1p) + 360) * ((h2p - h1p) < -180).float() * mask1_no
86
+
87
+ return res1 + res2 + res3
88
+
89
+
90
+ def ahpf_diff(c1, c2, h1p, h2p):
91
+ mask1 = ((c1 * c2) == 0).float()
92
+ mask1_no = 1 - mask1
93
+ mask2 = (torch.abs(h2p - h1p) <= 180).float()
94
+ mask2_no = 1 - mask2
95
+ mask3 = (torch.abs(h2p + h1p) < 360).float()
96
+ mask3_no = 1 - mask3
97
+
98
+ res1 = (h1p + h2p) * mask1_no * mask2
99
+ res2 = (h1p + h2p + 360.) * mask1_no * mask2_no * mask3
100
+ res3 = (h1p + h2p - 360.) * mask1_no * mask2_no * mask3_no
101
+ res = (res1 + res2 + res3) + (res1 + res2 + res3) * mask1
102
+ return res * 0.5
103
+
104
+
105
+ def ciede2000_diff(lab1, lab2):
106
+ """
107
+ CIEDE2000 metric to claculate the color distance map for a batch of image tensors defined in CIELAB space
108
+
109
+ This version contains errors:
110
+ - Typo in the formula for T: "- 39" should be "- 30"
111
+ - 0.0001s added for numerical stability change the conditional values
112
+
113
+ """
114
+ L1 = lab1[:, 0, :, :]
115
+ A1 = lab1[:, 1, :, :]
116
+ B1 = lab1[:, 2, :, :]
117
+ L2 = lab2[:, 0, :, :]
118
+ A2 = lab2[:, 1, :, :]
119
+ B2 = lab2[:, 2, :, :]
120
+ kL = 1
121
+ kC = 1
122
+ kH = 1
123
+
124
+ mask_value_0_input1 = ((A1 == 0) * (B1 == 0)).float()
125
+ mask_value_0_input2 = ((A2 == 0) * (B2 == 0)).float()
126
+ mask_value_0_input1_no = 1 - mask_value_0_input1
127
+ mask_value_0_input2_no = 1 - mask_value_0_input2
128
+ B1 = B1 + 0.0001 * mask_value_0_input1
129
+ B2 = B2 + 0.0001 * mask_value_0_input2
130
+
131
+ C1 = torch.sqrt((A1 ** 2.) + (B1 ** 2.))
132
+ C2 = torch.sqrt((A2 ** 2.) + (B2 ** 2.))
133
+
134
+ aC1C2 = (C1 + C2) / 2.
135
+ G = 0.5 * (1. - torch.sqrt((aC1C2 ** 7.) / ((aC1C2 ** 7.) + (25 ** 7.))))
136
+ a1P = (1. + G) * A1
137
+ a2P = (1. + G) * A2
138
+ c1P = torch.sqrt((a1P ** 2.) + (B1 ** 2.))
139
+ c2P = torch.sqrt((a2P ** 2.) + (B2 ** 2.))
140
+
141
+ h1P = hpf_diff(B1, a1P)
142
+ h2P = hpf_diff(B2, a2P)
143
+ h1P = h1P * mask_value_0_input1_no
144
+ h2P = h2P * mask_value_0_input2_no
145
+
146
+ dLP = L2 - L1
147
+ dCP = c2P - c1P
148
+ dhP = dhpf_diff(C1, C2, h1P, h2P)
149
+ dHP = 2. * torch.sqrt(c1P * c2P) * torch.sin(radians(dhP) / 2.)
150
+ mask_0_no = 1 - torch.max(mask_value_0_input1, mask_value_0_input2)
151
+ dHP = dHP * mask_0_no
152
+
153
+ aL = (L1 + L2) / 2.
154
+ aCP = (c1P + c2P) / 2.
155
+ aHP = ahpf_diff(C1, C2, h1P, h2P)
156
+ T = 1. - 0.17 * torch.cos(radians(aHP - 39)) + 0.24 * torch.cos(radians(2. * aHP)) + 0.32 * torch.cos(
157
+ radians(3. * aHP + 6.)) - 0.2 * torch.cos(radians(4. * aHP - 63.))
158
+ dRO = 30. * torch.exp(-1. * (((aHP - 275.) / 25.) ** 2.))
159
+ rC = torch.sqrt((aCP ** 7.) / ((aCP ** 7.) + (25. ** 7.)))
160
+ sL = 1. + ((0.015 * ((aL - 50.) ** 2.)) / torch.sqrt(20. + ((aL - 50.) ** 2.)))
161
+
162
+ sC = 1. + 0.045 * aCP
163
+ sH = 1. + 0.015 * aCP * T
164
+ rT = -2. * rC * torch.sin(radians(2. * dRO))
165
+
166
+ # res_square=((dLP / (sL * kL)) ** 2.) + ((dCP / (sC * kC)) ** 2.) + ((dHP / (sH * kH)) ** 2.) + rT * (dCP / (sC * kC)) * (dHP / (sH * kH))
167
+
168
+ res_square = ((dLP / (sL * kL)) ** 2.) + ((dCP / (sC * kC)) ** 2.) * mask_0_no + (
169
+ (dHP / (sH * kH)) ** 2.) * mask_0_no + rT * (dCP / (sC * kC)) * (dHP / (sH * kH)) * mask_0_no
170
+ mask_0 = (res_square <= 0).float()
171
+ mask_0_no = 1 - mask_0
172
+ res_square = res_square + 0.0001 * mask_0
173
+ res = torch.sqrt(res_square)
174
+ res = res * mask_0_no
175
+
176
+ return res
177
+
178
+
179
+ def ciede2000_loss(input: Tensor, target: Tensor) -> Tensor:
180
+ Lab_input, Lab_target = map(rgb2lab_diff, (input, target))
181
+ return ciede2000_diff(Lab_input, Lab_target).flatten(1).norm(p=2, dim=1)
@@ -0,0 +1,128 @@
1
+ # Adapted from https://github.com/ZhengyuZhao/PerC-Adversarial
2
+
3
+ from math import pi, cos
4
+
5
+ import torch
6
+ from torch import nn, Tensor
7
+ from torch.autograd import grad
8
+
9
+ from .differential_color_functions import rgb2lab_diff, ciede2000_diff
10
+
11
+
12
+ def quantization(x):
13
+ """quantize the continus image tensors into 255 levels (8 bit encoding)"""
14
+ x_quan = torch.round(x * 255) / 255
15
+ return x_quan
16
+
17
+
18
+ def perc_al(model: nn.Module,
19
+ images: Tensor,
20
+ labels: Tensor,
21
+ num_classes: int,
22
+ targeted: bool = False,
23
+ max_iterations: int = 1000,
24
+ alpha_l_init: float = 1.,
25
+ alpha_c_init: float = 0.5,
26
+ confidence: float = 0, **kwargs) -> Tensor:
27
+ """
28
+ PerC_AL: Alternating Loss of Classification and Color Differences to achieve imperceptibile perturbations with few
29
+ iterations. Adapted from https://github.com/ZhengyuZhao/PerC-Adversarial.
30
+
31
+ Parameters
32
+ ----------
33
+ model : nn.Module
34
+ Model to fool.
35
+ images : Tensor
36
+ Batch of image examples in the range of [0,1].
37
+ labels : Tensor
38
+ Original labels if untargeted, else labels of targets.
39
+ targeted : bool, optional
40
+ Whether to perform a targeted adversary or not.
41
+ max_iterations : int
42
+ Number of iterations for the optimization.
43
+ alpha_l_init: float
44
+ step size for updating perturbations with respect to classification loss
45
+ alpha_c_init: float
46
+ step size for updating perturbations with respect to perceptual color differences. for relatively easy
47
+ untargeted case, alpha_c_init is adjusted to a smaller value (e.g., 0.1 is used in the paper)
48
+ confidence : float, optional
49
+ Confidence of the adversary for Carlini's loss, in term of distance between logits.
50
+ Note that this approach only supports confidence setting in an untargeted case
51
+
52
+ Returns
53
+ -------
54
+ Tensor
55
+ Batch of image samples modified to be adversarial
56
+ """
57
+
58
+ if images.min() < 0 or images.max() > 1: raise ValueError('Input values should be in the [0, 1] range.')
59
+ device = images.device
60
+
61
+ alpha_l_min = alpha_l_init / 100
62
+ alpha_c_min = alpha_c_init / 10
63
+ multiplier = -1 if targeted else 1
64
+
65
+ X_adv_round_best = images.clone()
66
+ inputs_LAB = rgb2lab_diff(images)
67
+ batch_size = images.shape[0]
68
+ delta = torch.zeros_like(images, requires_grad=True)
69
+ mask_isadv = torch.zeros(batch_size, dtype=torch.bool, device=device)
70
+ color_l2_delta_bound_best = torch.full((batch_size,), 100000, dtype=torch.float, device=device)
71
+
72
+ if (targeted == False) and confidence != 0:
73
+ labels_onehot = torch.zeros(labels.size(0), num_classes, device=device)
74
+ labels_onehot.scatter_(1, labels.unsqueeze(1), 1)
75
+ labels_infhot = torch.zeros_like(labels_onehot).scatter_(1, labels.unsqueeze(1), float('inf'))
76
+ if (targeted == True) and confidence != 0:
77
+ print('Only support setting confidence in untargeted case!')
78
+ return
79
+
80
+ # check if some images are already adversarial
81
+ if (targeted == False) and confidence != 0:
82
+ logits = model(images)
83
+ real = logits.gather(1, labels.unsqueeze(1)).squeeze(1)
84
+ other = (logits - labels_infhot).amax(dim=1)
85
+ mask_isadv = (real - other) <= -40
86
+ elif confidence == 0:
87
+ if targeted:
88
+ mask_isadv = model(images).argmax(1) == labels
89
+ else:
90
+ mask_isadv = model(images).argmax(1) != labels
91
+ color_l2_delta_bound_best[mask_isadv] = 0
92
+ X_adv_round_best[mask_isadv] = images[mask_isadv]
93
+
94
+ for i in range(max_iterations):
95
+ # cosine annealing for alpha_l_init and alpha_c_init
96
+ alpha_c = alpha_c_min + 0.5 * (alpha_c_init - alpha_c_min) * (1 + cos(i / max_iterations * pi))
97
+ alpha_l = alpha_l_min + 0.5 * (alpha_l_init - alpha_l_min) * (1 + cos(i / max_iterations * pi))
98
+
99
+ loss = multiplier * nn.CrossEntropyLoss(reduction='sum')(model(images + delta), labels)
100
+ grad_a = grad(loss, delta, only_inputs=True)[0]
101
+ delta.data[~mask_isadv] = delta.data[~mask_isadv] + alpha_l * (grad_a.permute(1, 2, 3, 0) / torch.norm(
102
+ grad_a.flatten(1), dim=1)).permute(3, 0, 1, 2)[~mask_isadv]
103
+
104
+ d_map = ciede2000_diff(inputs_LAB, rgb2lab_diff(images + delta)).unsqueeze(1)
105
+ color_dis = torch.norm(d_map.flatten(1), dim=1)
106
+ grad_color = grad(color_dis.sum(), delta, only_inputs=True)[0]
107
+ delta.data[mask_isadv] = delta.data[mask_isadv] - alpha_c * (grad_color.permute(1, 2, 3, 0) / torch.norm(
108
+ grad_color.flatten(1), dim=1)).permute(3, 0, 1, 2)[mask_isadv]
109
+
110
+ delta.data = (images + delta.data).clamp_(min=0, max=1) - images
111
+ X_adv_round = quantization(images + delta.data)
112
+
113
+ if (targeted == False) and confidence != 0:
114
+ logits = model(X_adv_round)
115
+ real = logits.gather(1, labels.unsqueeze(1)).squeeze(1)
116
+ other = (logits - labels_infhot).amax(dim=1)
117
+ mask_isadv = (real - other) <= -40
118
+ elif confidence == 0:
119
+ if targeted:
120
+ mask_isadv = model(X_adv_round).argmax(1) == labels
121
+ else:
122
+ mask_isadv = model(X_adv_round).argmax(1) != labels
123
+ mask_best = (color_dis.data < color_l2_delta_bound_best)
124
+ mask = mask_best * mask_isadv
125
+ color_l2_delta_bound_best[mask] = color_dis.data[mask]
126
+ X_adv_round_best[mask] = X_adv_round[mask]
127
+
128
+ return X_adv_round_best