"> "> 深度学习-图像对抗样本 | Yufei Luo's Blog

深度学习-图像对抗样本

概述

对抗样本指的是在数据集中故意地加入细微的干扰(有时人眼无法识别这样的干扰)所形成的输入样本,这样的样本会导致模型以高置信度给出一个错误的输出。以分类问题为例,对抗样本用数学语言可以描述为,给定输入数据\(x\)和分类器\(f\),对应的分类结果为\(f(x)\),假设存在一个非常小的扰动\(\epsilon\),使得\(f(x+\epsilon)!=f(x)\),那么\(f+\epsilon\)便为一个对抗样本。

下图为对抗样本的一个示例图:

0cefd3e81fbcfb41b6e83b3a55d474e6

按照攻击后的效果,对抗样本分为Targeted Attack和Non-Targeted Attack。前者在攻击之前会预先设定好攻击的目标,例如把红球识别为绿球,也就是说攻击之后的结果是确定的;而后者指的是攻击之前不设定攻击目标,只要攻击之后识别结果发生改变即可。

按照攻击成本,对抗样本分为白盒攻击、黑盒攻击和物理攻击。白盒攻击的难度最低,要求能够完整地获取模型的结构,包括模型的组成以及参数,并且可以完全地控制模型的输入。这一前置条件比较苛刻,通常用作学术研究或者是黑盒攻击和物理攻击的研究基础;黑盒攻击的难度相较于白盒攻击有了很大提高,它完全把要攻击的模型当作一个黑盒,对其结构没有任何了解,只能控制模型的输入。通过对比输入和输出的反馈来进行下一步攻击;物理攻击的难度最大,除了不了解模型的结构,对于模型输入的控制也很弱。以攻击图像分类模型为例,攻击样本要通过相机或者摄像头采集,然后经过一系列预处理之后再送入模型。

本文以计算机视觉相关的深度学习模型为例,对几种白盒攻击算法和黑盒攻击算法,以及常用的防御算法进行简单介绍。

白盒攻击算法

基于优化的对抗样本生成算法

深度学习模型的训练过程为,通过计算样本数据的预测值与真实值之间的损失函数,然后基于反向传播的方法计算损失函数的梯度,基于此不断调整模型的参数,减小损失函数的值,从而迭代计算出模型各层的参数。对抗样本的生成过程可以仿照这一流程,唯一不同的一点是,在迭代训练过程中,将网络的参数固定下来,把对抗样本当成需要训练的参数,然后通过反向传播来调整对抗样本的值。这种生成对抗样本的方法属于定向攻击算法。

下面的代码以训练好的ResNet为例,生成针对于ResNet的对抗样本。

1
2
3
4
5
6
7
8
import torch
from torch.optim import Adam
import torchvision
from torchvision import transforms
import torch.nn.functional as F
import numpy as np
import cv2
import matplotlib.pyplot as plt
1
model=torchvision.models.resnet50(pretrained=True).to('cuda') #预训练模型是基于ImageNet数据集基础之上训练得到的,ImageNet包含的类别可参考https://blog.csdn.net/weixin_41770169/article/details/80482942

接下来,我们选取一张比萨的图片(分类编号为963),然后试图构造对抗样本,使模型将其误分类为拼图(编号611)

1
2
img=cv2.imread('pizza.jpg')[...,::-1]
img=cv2.resize(img,(224,224))
1
2
plt.imshow(img)
plt.axis('off')
output_4_1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def construct_training_img(img):
mean=[0.485,0.456,0.406]
std=[0.229,0.224,0.225]
img=img.astype(np.float32)
img /= 255.0
img=(img-mean)/std
img=img.transpose([2,0,1])
img=np.expand_dims(img,axis=0)
return img

def convert_img(img):
mean=[0.485,0.456,0.406]
std=[0.229,0.224,0.225]
img=img.transpose([1,2,0])
img=img*std+mean
img=img*255.0
img=np.clip(img,0,255).astype(np.uint8)
return img
1
2
training_img=construct_training_img(img)
training_img=torch.tensor(training_img,device='cuda',requires_grad=True,dtype=torch.float32)
1
2
3
target_tensor=torch.tensor([611]).to('cuda')
model.eval()
optimizer=Adam([training_img],lr=1e-4)
1
2
3
4
5
6
7
8
9
for i in range(200):
pred=model(training_img)
pred_type=torch.argmax(pred).cpu().numpy()
loss=F.cross_entropy(pred,target_tensor)
print(loss, pred_type)
# if torch.argmax(pred).cpu().numpy()==611: #注意:当类别第一次发生转变时最好再多训练几轮。如果直接停止的话,在数值裁切之后模型的预测分类结果可能会仍然不变
# break
loss.backward()
optimizer.step()
1
2
attack_img=training_img.squeeze().detach().cpu().numpy()
attack_img=convert_img(attack_img)
1
2
attack_img=construct_training_img(attack_img)
attack_img=torch.tensor(attack_img,device='cuda',dtype=torch.float32)
1
torch.argmax(model(attack_img))
tensor(611, device='cuda:0')

FGM/FGSM算法

FGM(Fast Gradient Method),即快速梯度算法,它可以作为无定向攻击和定向攻击算法使用。假设图片的原始数据为\(x\),图片识别的结果为\(y\)。给原始图像加上肉眼难以识别的细微变化\(\eta\),则变换后的图像为\(\tilde{x}=x+\eta\)

将修改后的图像\(\tilde{x}\)输入分类模型中,则线性层的计算结果变为:\(\omega^T \tilde{x}=\omega^T x+\omega^T \eta\)。由于攻击样本的目的是以微小的修改,使得分类结果产生较大的变化,因此如果变化量与梯度的变化方向完全一致,也就是\(\eta=\text{sign}(\omega)\),那么分类结果将会产生较大的变化。

下面的代码为FGM的示例。由于代码其他部分与基于优化的基本类似,因此下面的代码仅仅包括了训练过程:

1
2
3
4
5
6
7
8
9
for i in range(10):
pred=model(training_img)
pred_type=torch.argmax(pred).cpu().numpy()
loss=F.cross_entropy(pred,target_tensor)
print(loss, pred_type)
# if torch.argmax(pred).cpu().numpy()==611: #注意:当类别第一次发生转变时最好再多训练几轮。如果直接停止的话,在数值裁切之后模型的预测分类结果可能会仍然不变
# break
loss.backward()
training_img.data=training_img.data-0.01*torch.sign(training_img.grad.data)

DeepFool算法

DeepFool也是一种基于梯度的白盒攻击算法,它属于无定向攻击算法。相较于FGM算法,它不用指定学习速率,算法本身可以计算出相对与FGM更小的扰动,从而达到攻击目的。

以简单的二分类问题为例,我们假设分类器\(f\)具有线性的分类平面。如果要改变某点\(x_0\)的分类结果,则一定要跨过分割平面。显然最短的移动距离就是垂直于分割平面进行移动。如果将这个距离记为\(r_*(x_0)\)\(f\)的参数设为\(w\),那么存在如下关系: \[ r_*(x_0)=\arg \min ||r||_2=-\frac{f(x_0)}{||w||_2^2}w \] 对于平面来说,\(w\)为平面的法向量,因此这一公式就代表将\(x_0\)沿着垂直于分类平面的方向移动。同时,\(w\)也是\(f\)的梯度方向。

而更加真实的情况则通常是分类器\(f\)具有一个分类曲面,此时就需要重复多次沿着梯度方向移动的过程。算法步骤如下:

img

对于多分类的情况则可以理解为二分类的扩展,相当于是多个One-vs-All分类器的组合。由于DeepFool属于无定向攻击,因此它在每次移动的过程中贪心地选取距离最近的分类超平面,并向这一方向移动。算法过程如下:

img

下面为DeepFool的代码实现,同样地,下面只包含训练生成对抗样本过程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
orig_label=604
r=torch.zeros_like(training_img).to('cuda')
for i in range(10):
pred=model(training_img)
pred_type=torch.argmax(pred).cpu().numpy()
print(pred_type)
w=torch.zeros_like(training_img)
f=torch.tensor(0.0).to('cuda')
movement=torch.tensor(np.inf).to('cuda')
cls_f=pred[0,orig_label]
cls_grad=torch.autograd.grad(pred[0,orig_label],training_img,grad_outputs=torch.ones_like(pred[0,orig_label]),retain_graph=True)[0].data
for j in range(1000):
if j==orig_label:
continue
w_temp=torch.autograd.grad(pred[0,j],training_img,grad_outputs=torch.ones_like(pred[0,j]),retain_graph=True)[0].data-cls_grad
f_temp=pred[0,j]-cls_f
movement_temp=torch.abs(f_temp)/torch.norm(w_temp)
if movement_temp < movement:
movement=movement_temp
w=w_temp
f=f_temp
r_i=torch.abs(f)/(torch.norm(w)**2)*w
training_img.data=training_img.data+r_i
r=r+r_i

JSMA算法

JSMA(Jacobian-based Saliency Map Attack)是\(l_0\)范数的白盒定向攻击算法,它追求尽量少地修改像素点。JSMA的特点是引入了Saliency Map的概念,用以表征输入特征对于预测结果的影响程度。

假设一个神经网络的输入为\(x\in \mathbb{R}^M\),输出为\(Y\in \mathbb{R}^N\),隐藏层的个数为\(n\),前向计算的结果为\(f(x)\),其中\(f(x)\)未经过Softmax层的处理。

算法的步骤如下:

  1. 计算前向导数:\(\nabla F(x)=\frac{\partial F(x)}{\partial x}\)

  2. 构造对抗显著图

    分类器对于一个输入\(x\)的分类规则为\(\text{cls}(x)=\arg \max_j F_j(x)\)。假设分类器将\(x\)分类为\(j\),但是我们希望将其分为\(t\),即\(t=\arg\max_j F_j(x)\),以此构造对抗显著图: \[ S(X,t)[i]=\begin{cases} 0 &\text{if}~ \frac{\partial F_t(x)}{\partial x}<0~\text{or}~\sum_{j\ne t} \frac{\partial F_j(x)}{\partial x}>0 \\ (\frac{\partial F_t(x)}{\partial x})|\sum_{j\ne t} \frac{\partial F_j(x)}{\partial x}|~~&\text{otherwise}\\ \end{cases} \] 据此可以得到哪些像素位置的改变对于目标分类\(t\)的影响最大。如果对应导数值为正,则增大该位置像素可以增加目标\(t\)的分数;如果为负值则相反。

  3. 根据上一步构造的显著图,挑选使得\((\frac{\partial F_t(x)}{\partial x})|\sum_{j\ne t}\)的值最大的位置,然后增加或者减小其像素值,对应地就可以增加目标\(t\)的输出,然后重复迭代,直到攻击成功或者达到最大破坏阈值。

下面为JSMA算法的实现,同样只包含了生成对抗样本的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
orig_label=604
target_label=523
max_=3
min_=-3
cnt=0
mask=torch.ones_like(training_img)
for i in range(5000):
pred=model(training_img)
pred_type=torch.argmax(pred).numpy()
print('epoch:',i,' pred type:',pred_type)
if pred_type!=orig_label:
cnt+=1
if cnt>1000: #由于最终会有数值截断,经过尝试,选取500及以下的数字都会使对抗样本的分类仍不变。
break
derivative=torch.autograd.grad(pred[0,target_label],training_img,grad_outputs=torch.ones_like(pred[0,target_label]),retain_graph=True)[0].data
alphas=derivative*mask
betas=-torch.ones_like(alphas)
sal_map=torch.abs(alphas)*torch.abs(betas)*torch.sign(alphas*betas)
id_flatten=torch.argmin(sal_map)
idx=torch.zeros_like(torch.tensor(sal_map.shape))
for i in range(idx.shape[0]):
ind=idx.shape[0]-1-i
idx[ind]=id_flatten%sal_map.shape[ind]
id_flatten=id_flatten//sal_map.shape[ind]
pix_sign=torch.sign(alphas)[idx[0],idx[1],idx[2],idx[3]]
training_img.data[idx[0],idx[1],idx[2],idx[3]]=training_img.data[idx[0],idx[1],idx[2],idx[3]]+pix_sign*0.2*(max_-min_)
if training_img.data[idx[0],idx[1],idx[2],idx[3]]<min_ or training_img.data[idx[0],idx[1],idx[2],idx[3]]>max_:
mask[idx[0],idx[1],idx[2],idx[3]]=0
training_img.data[idx[0],idx[1],idx[2],idx[3]]=torch.clip(training_img.data[idx[0],idx[1],idx[2],idx[3]],min_,max_)

CW算法

CW算法通常被认为是攻击能力最强的白盒攻击算法之一,同时也是一种基于优化的对抗样本生成算法。它的创新之处在于对损失函数的定义上。在定向攻击中,损失函数常常使用交叉熵,优化过程就是不断地减小目标函数的过程。

假设在原始数据\(x\)上增加扰动,生成对抗样本\(x+\delta\),对抗样本和原始数据之间的距离定义为\(D(x,x+\delta)\),那么整个优化函数可以定义为: \[ \min D(x,x+\delta)+c\cdot f(x+\delta) \] 其中,\(f\)为自定义的损失函数。参数\(c\)决定了扰动的大小,\(c\)越大则攻击成功率和扰动都会变大,通常使用二分查找的方式来选择尽可能小的\(c\)值。对于定向的\(l_2\)攻击,假设攻击目标的标签为\(t\),被攻击模型的输出为\(Z\),那么目标函数定义为: \[ f(x)=\max(\max\{Z(x)_i:i\ne t\}-Z(x)_t,-k) \] CW算法的另一个特点是对数据截断的处理。它使用了变换变量的方法,具体可以描述为,引入一个新的变量\(w\),将对抗样本\(x+\delta\)表示为\(x+\delta=\frac{1}{2}(\tanh (w)+1)\)。这样既保证了对抗样本的取值范围不溢出,也不会因为数据截断而导致梯度消失。这样,整个优化目标便可以写为: \[ \min ||\frac{1}{2}(\tanh (w)+1)-x||_2^2+c\cdot f(\tanh(w)+1) \] 下面为CW算法的实现,只包含了关键参数的定义和对抗样本的生成过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
min_=-3.0
max_=3.0
boxmul=(max_-min_)/2.0
boxplus=(max_+min_)/2.0
orig_label=604
target_label=532
target=torch.tensor(np.zeros((1,1000)),dtype=torch.float32)
target[0,target_label]=1.0
target=target.to('cuda')
lower_bound=0
confidence=1e-3
upper_bound=1e10
o_best_l2=1e10
o_best_score=-1
o_best_attack=None
k=40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
for outer_step in range(20): #二分法寻找最合适的参数c
timg=torch.tensor(training_img)
timg=torch.arctanh((timg-boxplus) / boxmul *0.999999)

modifier=torch.zeros_like(timg,requires_grad=True)
optimizer=Adam([modifier],lr=1e-4)
for iteration in range(100): # 对于每个参数c,迭代优化100次
optimizer.zero_grad()
newimg=torch.tanh(modifier+timg)*boxmul+boxplus
output=model(newimg)

loss2=torch.dist(newimg,(torch.tanh(timg)*boxmul+boxplus),p=2)
real=torch.max(output*target)
other=torch.max((1-target)*output)
loss1=other-real+k
loss1=confidence*loss1
loss=loss1+loss2
loss.backward(retain_graph=True)
optimizer.step()

l2=loss2
sc=output.data.cpu().numpy()

if (l2.item()<o_best_l2) and (np.argmax(sc)==target_label):
o_best_l2=l2
o_best_score=np.argmax(sc)
o_best_attack=newimg.data.cpu().numpy()

print('Confidence: ',confidence, ' l2: ',l2, ' class: ', np.argmax(sc))

confidence_old=-1
if (o_best_score==torch.argmax(target)) and (o_best_score!=-1):
upper_bound=min(upper_bound,confidence)
if upper_bound<1e9:
confidence_old=confidence
confidence=(lower_bound+upper_bound)/2
else:
lower_bound=max(lower_bound,confidence)
confidence_old=confidence
if upper_bound<1e9:
confidence=(lower_bound+upper_bound)/2
else:
confidence*=10

黑盒攻击算法

单像素攻击算法

单像素攻击(Single Pixel Attack)是一种典型的黑盒攻击算法,它的基本思想是,通过修改原始数据上的一个像素的值,让模型产生分类错误。

以图像分类模型\(f\)为例,假设图像为\(I\),它的通道数为\(l\)。分类标签为\(c(I)\),分类标签的集合表示为\(\{1,2,\dots,C\}\)。用\((b,x,y)\)代表坐标为\((x,y)\)的像素点的\(b\)号通道。设图像存在某个关键像素点\((*,x,y)\),通过修改它的值使得图像\(I\)变为\(I_p\),此时\(f\)的分类结果产生错误,即\(f(I_p)!=c(I)\)

图像的扰动可以通过扰动函数\(\text{PERT}(I,p,x,y)=p\cdot \text{sign}(I(*,x,y))\)来实现,其中\(p\)为扰动系数,需要手动设置。则单像素攻击算法可以简单描述为,每次随机选择一个像素点,然后在对原图的基础上对该点施加扰动。如果扰动后的图像出现分类错误,则说明该点为关键像素点,否则则不是关键像素点。

下面为单像素攻击算法的代码示例。根据实际尝试,对于较高维度的输入(例如alexnet的3*224*224的输入),仅修改一个像素点根本无法实现攻击,需要随机选取多个像素同时修改才能攻击成功:

1
2
3
4
5
6
7
8
import torch
from torch.optim import Adam
import torchvision
from torchvision import transforms
import torch.nn.functional as F
import numpy as np
import cv2
import matplotlib.pyplot as plt
1
model=torchvision.models.alexnet(pretrained=True).to('cuda')#预训练模型是基于ImageNet数据集基础之上训练得到的,ImageNet包含的类别可参考https://blog.csdn.net/weixin_41770169/article/details/80482942

接下来,我们选取一张沙漏的图片(分类编号为604),然后试图构造对抗样本

1
2
img=cv2.imread('hourglass.jpg')[...,::-1]
img=cv2.resize(img,(224,224))
1
2
plt.imshow(img)
plt.axis('off')
hourglass
1
2
3
4
5
6
7
8
def construct_training_img(img):
mean=torch.tensor([0.485,0.456,0.406]).reshape(1,1,3)
std=torch.tensor([0.229,0.224,0.225]).reshape(1,1,3)
img /= torch.tensor(255.0)
img=(img-mean)/std
img=img.permute([2,0,1])
img=img.unsqueeze(0)
return img
1
model.eval()
1
2
3
4
5
6
7
8
9
10
11
12
13
p=10.0
img=torch.tensor(construct_training_img(img),dtype=torch.float32,device='cuda')
label=torch.argmax(model(img))
critical_pixels=list()
for trial in range(20000):
perturbed_img=torch.tensor(img)
perturbed_pixel=torch.randint(0,224,(60,2)) #经过尝试,只改变一个像素点对于224*224的图片不足以改变图片的分类结果,甚至同时修改50个像素点都很难成功
for x,y in perturbed_pixel:
perturbed_img[0,:,x,y]=perturbed_img[0,:,x,y]+p*torch.sign(perturbed_img[0,:,x,y])
perturbed_img=torch.clip(perturbed_img,-3.0,3.0)
if label!=torch.argmax(model(perturbed_img)):
critical_pixels.append(perturbed_pixel)
break
1
print(torch.argmax(model(perturbed_img)))
tensor(707, device='cuda:0')

迁移学习攻击

在黑盒攻击中,迁移学习攻击算法是一种非常重要的攻击算法。它的基本思想是,对于结构类似的神经网络,在面对相同对抗样本的攻击时,具有类似的表现。也就是说,如果一个样本可以攻击模型A,那么有一定的概率它也可以攻击模型B。

下面为使用ResNet50,用基于优化的对抗样本生成方法生成对抗样本,然后去攻击ResNet18的代码:

1
2
3
4
5
6
7
8
import torch
from torch.optim import Adam
import torchvision
from torchvision import transforms
import torch.nn.functional as F
import numpy as np
import cv2
import matplotlib.pyplot as plt
1
model=torchvision.models.resnet50(pretrained=True).to('cuda') #预训练模型是基于ImageNet数据集基础之上训练得到的,ImageNet包含的类别可参考https://blog.csdn.net/weixin_41770169/article/details/80482942

接下来,我们选取一张图片(分类编号为963),然后试图构造对抗样本,使模型将其误分类为拼图(编号611)

1
2
img=cv2.imread('pizza.jpg')[...,::-1]
img=cv2.resize(img,(224,224))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def construct_training_img(img):
mean=[0.485,0.456,0.406]
std=[0.229,0.224,0.225]
img=img.astype(np.float32)
img /= 255.0
img=(img-mean)/std
img=img.transpose([2,0,1])
img=np.expand_dims(img,axis=0)
return img

def convert_img(img):
mean=[0.485,0.456,0.406]
std=[0.229,0.224,0.225]
img=img.transpose([1,2,0])
img=img*std+mean
img=img*255.0
img=np.clip(img,0,255).astype(np.uint8)
return img
1
2
training_img=construct_training_img(img)
training_img=torch.tensor(training_img,device='cuda',requires_grad=True,dtype=torch.float32)
1
2
3
target_tensor=torch.tensor([611]).to('cuda')
model.eval()
optimizer=Adam([training_img],lr=1e-4)
1
2
3
4
5
6
7
8
9
for i in range(500): # 训练500轮使得对抗样本具有较强的鲁棒性
pred=model(training_img)
pred_type=torch.argmax(pred).cpu().numpy()
loss=F.cross_entropy(pred,target_tensor)
print(loss, pred_type)
# if torch.argmax(pred).cpu().numpy()==611: #注意:当类别第一次发生转变时最好再多训练几轮。如果直接停止的话,在数值裁切之后模型的预测分类结果可能会仍然不变
# break
loss.backward()
optimizer.step()
1
2
attack_img=construct_training_img(attack_img)
attack_img=torch.tensor(attack_img,device='cuda',dtype=torch.float32)
1
torch.argmax(model(attack_img))
tensor(611, device='cuda:0')

使用上一步生成的对抗样本去攻击ResNet18,可以成功攻击:

1
2
model2=torchvision.models.resnet18(pretrained=True).to('cuda')
torch.argmax(model2(attack_img))
tensor(463, device='cuda:0')

试图使用ResNet50的对抗样本攻击AlexNet,无法让AlexNet出现识别错误:

1
2
model2=torchvision.models.alexnet(pretrained=True).to('cuda')
torch.argmax(model2(attack_img))
tensor(963, device='cuda:0')

对抗样本防御方法

常用的抵御对抗样本的方法包括:

  1. 对抗样本自身也存在着鲁棒性问题,也就是说对抗样本在经过旋转、滤波、亮度或对比度调整、加入噪声之后,可能会使得对抗样本失效,使得对抗样本仍可以正确分类。因此在不影响正常样本的分类这一前提下,可以加入一些图像预处理步骤,具体参数例如旋转角度、噪声添加量等需要根据模型来进行调整。
  2. 对抗训练:使用常见的攻击算法生成一系列的对抗样本,然后将对抗样本与正常样本一起送入模型中训练。
  3. 高斯数据增强:对抗训练的方法难以穷尽所有的对抗样本,而对抗样本的思路相当于是在正常样本中加入噪声,因此在训练中可以用高斯噪声去模拟这种噪声。在模型的训练过程中,在原始数据的基础上叠加高斯噪声,然后对模型进行训练,即可对模型进行加固。
  4. 使用去噪编码器,将数据经过去噪自编码器处理之后再送入模型中进行预测。

常用对抗样本工具箱

  • AdvBox:由百度安全实验室研发的AI模型安全工具箱,https://github.com/advboxes/AdvBox
  • ART:Welcome to the Adversarial Robustness Toolbox — Adversarial Robustness Toolbox 1.7.2 documentation
  • FoolBox:bethgelab/foolbox: A Python toolbox to create adversarial examples that fool neural networks in PyTorch, TensorFlow, and JAX

参考

  1. AI安全之对抗样本入门
  2. DeepFool - 知乎 (zhihu.com)
  3. 对抗样本生成算法之JSMA算法_ilalaaa的博客-CSDN博客