论文链接:https://arxiv.org/abs/1607.02533
源码出处:https://github.com/Harry24k/adversarial-attacks-pytorch/tree/master
源码
import torch
import torch.nn as nn
from ..attack import Attack
class BIM(Attack):
r"""
BIM or iterative-FGSM in the paper 'Adversarial Examples in the Physical World'
[https://arxiv.org/abs/1607.02533]
Distance Measure : Linf
Arguments:
model (nn.Module): model to attack.
eps (float): maximum perturbation. (Default: 8/255)
alpha (float): step size. (Default: 2/255)
steps (int): number of steps. (Default: 10)
.. note:: If steps set to 0, steps will be automatically decided following the paper.
Shape:
- images: :math:`(N, C, H, W)` where `N = number of batches`, `C = number of channels`, `H = height` and `W = width`. It must have a range [0, 1].
- labels: :math:`(N)` where each value :math:`y_i` is :math:`0 \leq y_i \leq` `number of labels`.
- output: :math:`(N, C, H, W)`.
Examples::
>>> attack = torchattacks.BIM(model, eps=8/255, alpha=2/255, steps=10)
>>> adv_images = attack(images, labels)
"""
def __init__(self, model, eps=8/255, alpha=2/255, steps=10):
super().__init__("BIM", model)
self.eps = eps
self.alpha = alpha
if steps == 0:
self.steps = int(min(eps*255 + 4, 1.25*eps*255))
else:
self.steps = steps
self.supported_mode = ['default', 'targeted']
def forward(self, images, labels):
r"""
Overridden.
"""
self._check_inputs(images)
images = images.clone().detach().to(self.device)
labels = labels.clone().detach().to(self.device)
if self.targeted:
target_labels = self.get_target_label(images, labels)
loss = nn.CrossEntropyLoss()
ori_images = images.clone().detach()
for _ in range(self.steps):
images.requires_grad = True
outputs = self.get_logits(images)
# Calculate loss
if self.targeted:
cost = -loss(outputs, target_labels)
else:
cost = loss(outputs, labels)
# Update adversarial images
grad = torch.autograd.grad(cost, images,
retain_graph=False,
create_graph=False)[0]
adv_images = images + self.alpha*grad.sign()
a = torch.clamp(ori_images - self.eps, min=0)
b = (adv_images >= a).float()*adv_images + (adv_images < a).float()*a
c = (b > ori_images+self.eps).float()*(ori_images+self.eps) + (b <= ori_images + self.eps).float()*b
images = torch.clamp(c, max=1).detach()
return images
解析
BIM算法(Basic Iterative Method)又叫迭代FGSM算法(I-FGSM),FGSM算法假设目标损失函数
J
(
x
,
y
)
J(x,y)
J(x,y)与
x
x
x之间是近似线性的,即
J
(
x
,
y
)
≈
w
T
x
J(x ,y)≈w^Tx
J(x,y)≈wTx,所以沿着梯度上升的方向改变输入
x
x
x可以增大损失,从而达到使模型分类错误的目的。但是这个假设不一定正确,即
J
(
x
,
y
)
J(x,y)
J(x,y)与
x
x
x之间可能不是线性的,也就是说改变在
(
0
,
ϵ
s
i
g
n
(
▽
x
J
(
θ
,
x
,
y
)
)
)
(0,\epsilon sign(\bigtriangledown_{x}J(\theta,x,y)))
(0,ϵsign(▽xJ(θ,x,y)))之间可能存在某个扰动,使得
J
J
J增加得更多。于是本篇论文就提出迭代的方式来找各个像素点的扰动,而不是一次性所有像素都改那么多,即迭代式的FGSM。公式如下所示:
X
0
a
d
v
=
X
,
X
N
+
1
a
d
v
=
C
l
i
p
X
,
ϵ
{
X
N
a
d
v
+
α
s
i
g
n
(
▽
x
J
(
X
N
a
d
v
,
y
t
r
u
e
)
)
}
X^{adv}_0=X,X^{adv}_{N+1}=Clip_{X,\epsilon}\{X^{adv}_N+\alpha sign(\triangledown_{x}J(X^{adv}_N,y_{true}))\}
X0adv=X,XN+1adv=ClipX,ϵ{XNadv+αsign(▽xJ(XNadv,ytrue))}
迭代的含义:每次在上一步的对抗样本的基础上,各个像素增长
α
\alpha
α(或者减少),然后再执行裁剪,保证新样本的各个像素都在
x
x
x的
ϵ
\epsilon
ϵ邻域内。这种迭代的方法是有可能在各个像素变化小于
ϵ
\epsilon
ϵ的情况下找到对抗样本的,如果找不到,最差的效果就跟原始的FGSM一样。
裁剪(
C
l
i
p
Clip
Clip)的作用:在迭代更新过程中,随着增加扰动的次数的增加,样本的部分像素值可能会溢出,比如超出0到1的范围,这时需将这些值用0或1代替,最后才能生成有效的图像。
论文还提出了ILLC算法(Iterative Least Likely Class Attack),即使用类似的迭代的方法进行有目标攻击,公式如下所示:
X
0
a
d
v
=
X
,
X
N
+
1
a
d
v
=
C
l
i
p
X
,
ϵ
{
X
N
a
d
v
−
α
s
i
g
n
(
▽
x
J
(
X
N
a
d
v
,
y
t
a
r
g
e
t
)
)
}
X^{adv}_0=X,X^{adv}_{N+1}=Clip_{X,\epsilon}\{X^{adv}_N-\alpha sign(\triangledown_{x}J(X^{adv}_N,y_{target}))\}
X0adv=X,XN+1adv=ClipX,ϵ{XNadv−αsign(▽xJ(XNadv,ytarget))}其中
y
t
a
r
g
e
t
=
arg min
y
(
y
∣
X
)
y_{target}=\argmin\limits_y(y\mid X)
ytarget=yargmin(y∣X)也就是与样本
X
X
X偏离最远的错误类。
eps
:即
ϵ
\epsilon
ϵ,表示最大扰动。
alpha
:即
α
\alpha
α,表示每次迭代中扰动的增加量(或减少量)。
steps
:表示迭代次数,论文中将其设为
m
i
n
(
ϵ
+
4
,
1.25
ϵ
)
min(\epsilon+4,1.25\epsilon)
min(ϵ+4,1.25ϵ),因为论文中认为这足够对抗性示例到达最大范数球的边缘,同时也保证实验的计算成本可控。由于代码中图像已经被归一化为
[
0
,
1
]
[0,1]
[0,1],
ϵ
\epsilon
ϵ也在该范围内,所以steps
即为
m
i
n
(
ϵ
×
255
+
4
,
1.25
ϵ
×
255
)
min(\epsilon\times255+4,1.25\epsilon\times255)
min(ϵ×255+4,1.25ϵ×255)。
images = images.clone().detach().to(self.device)
:clone()
将图像克隆到一块新的内存区(pytorch默认同样的tensor共享一块内存区);detach()
是将克隆的新的tensor从当前计算图中分离下来,作为叶节点,从而可以计算其梯度;to()
作用就是将其载入设备。
target_labels = self.get_target_label(images, labels)
:若是有目标攻击的情况,获取目标标签。目标标签的选取有多种方式,例如可以选择与真实标签相差最大的标签,也可以随机选择除真实标签外的标签。
loss = nn.CrossEntropyLoss()
:设置损失函数为交叉熵损失。
ori_images = images.clone().detach()
:保存原始图像,用于裁剪过程。
outputs = self.get_logits(images)
:获得图像的在模型中的输出值。
cost = -loss(outputs, target_labels)
:有目标情况下计算损失
cost = loss(outputs, labels)
:无目标情况下计算损失
grad = torch.autograd.grad(cost, images, retain_graph=False, create_graph=False)[0]
:cost
对images
求导,得到梯度grad
。
adv_images = images + self.alpha*grad.sign()
:根据公式在图像上沿着梯度上升方向以步长为
α
\alpha
α增加扰动。
a = torch.clamp(ori_images - self.eps, min=0) # a为图像最小值,即减去最大扰动值
b =(adv_images >= a).float()*adv_images + (adv_images < a).float()*a # 将对抗图像小于a的部分设为a
c = (b > ori_images+self.eps).float()*(ori_images+self.eps) + (b <= ori_images + self.eps).float()*b # 将b中超出扰动范围的值设为最大值
images = torch.clamp(c, max=1).detach() # 将c中超出1的值设为1
以上四行代码为裁剪( C l i p Clip Clip)过程,得到有效的图像。