在前面的章节中,我们已经看到了深度学习模型在解决各种计算机视觉任务方面的强大能力。我们在不同的数据集上训练和测试多个模型。现在,我们将把注意力转向这些模型的健壮性。
在本章中,我们将介绍对抗样本。对抗样本是一种输入数据,它可以显著地改变模型预测,而不被人眼注意到。由于这一事实,对抗样本可能令人担忧,特别是在安全或医疗保健领域等关键任务中。在开始考虑可能的解决方案之前,了解这些攻击是如何工作的将是有益的。
一个对抗样本如下图所示:
左边的图像是原始图像,而右边的图像是同一图像的对抗性版本。
尽管这两张图片看起来一样,分类器的预测概率,显示在标题上,是显著不同的。在本章中,我们将开发这种对抗性攻击。
有两种类型的对抗性攻击:白盒攻击和黑盒攻击。在白盒攻击中,攻击者拥有用于训练模型的模型、输入函数和损失函数的知识。通过使用这些知识,攻击者可以改变输入来破坏预测的输出。输入的变化量通常很小,人眼无法分辨。一种常见的白盒攻击被称为Fast Gradient Sign(FGS)攻击,其工作原理是通过改变输入来使损失最大化。
在FGS攻击中,给定一个输入和一个预先训练的模型,我们计算相对于输入的损失梯度。然后,我们在输入中加入一小部分梯度的绝对值。
在本教程中,您将学习如何为本书“二值图像分类”中介绍的二值分类模型开发FGS攻击。你可能还记得,我们在Histo数据集中开发了一个分类器来分类癌症图像。我们将使用相同的数据集和经过训练的模型。
为了方便,数据集类和模型分别定义在Python文件mydataset.py和mymodel.py中。此外,cnn_weights.pt提取码:123a文件中提供了模型的预训练权值。
mydataset.py文件代码书写
import torch
import os
import numpy as np
import pandas as pd
from PIL import Image
from torch.utils.data import Dataset, Subset, DataLoader
import torchvision.transforms as transforms
np.random.seed(1)
torch.manual_seed(1)
class histoCancerDataset(Dataset):
def __init__(self,data_dir,transform,data_type="train"):
path2data=os.path.join(data_dir,data_type)
self.filenames=os.listdir(path2data)
self.full_filenames=[os.path.join(path2data,f) for f in self.filenames]
csv_filename=data_type+"_labels.csv"
path2csvLabels=os.path.join(data_dir,csv_filename)
labels_df=pd.read_csv(path2csvLabels)
labels_df.set_index("id", inplace=True)
self.labels=[labels_df.loc[filename[:-4]].values[0] for filename in self.filenames]
self.transform=transform
def __len__(self):
return len(self.full_filenames)
def __getitem__(self,idx):
image=Image.open(self.full_filenames[idx])
image=self.transform(image)
return image, self.labels[idx]
data_dir="./data/"
data_transformer=transforms.Compose([transforms.ToTensor()])
hist_ds=histoCancerDataset(data_dir, data_transformer, data_type="train")
test_idex=np.random.randint(hist_ds.__len__(),size=100)
test_ds=Subset(hist_ds,test_index)
test_dl=DataLoader(test_ds,batch_size=1,shuffle=False)
mymodel.py文件代码书写
import torch.nn as nn
import numpy as np
import torch
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets,transforms
def findConv2dOutShape(H_in,w_in,conv,pool=2):
kernel_size=conv.kernel_size
stride=conv.stride
padding=conv.padding
dilation=conv.dilation
H_out=np.floor((H_in+2*padding[0]-dilation[0]*(kernel_size[0]-1)-1)/stride[0]+1)
W_out=np.floor((W_in+2*padding[1]-dilation[1]*(kernel_size[1]-1)-1)/stride[1]+1)
if pool:
H_out/=pool
W_in/=pool
return int(H_out), int(W_out)
class Net(nn.Module):
def __init__(self, params):
super(Net,self).__init__()
C_in,H_in,W_in=params["input_shape"]
init_f=params["initial_filters"]
num_fc1=params["num_fc1"]
num_classes=params["num_classes"]
self.dropout_rate=params["dropout_rate"]
self.conv1=nn.Conv2d(C_in,init_f,kernel_size=3)
h,w=findConv2dOutShape(H_in,W_in,self.conv1)
self.conv2=nn.Conv2d(init_f,init_f*2,kernel_size=3)
h,w=findConv2dOutShape(h,w,self.conv2)
self.conv3=nn.Conv2d(init_f*2,init_f*4,kernel_size=3)
h,w=findConv2dOutShape(h,w,self.conv3)
self.conv4=nn.Conv2d(init_f*4,init_f*8,kernel_size=3)
h,w=findConv2dOutShape(h,w,self.conv4)
self.num_flatten=h*w*8*init_f
self.fc1=nn.Linear(self.num_flatten, num_fc1)
self.fc2=nn.Linear(self.num_fc1,num_classes)
def forward(self,x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x,2,2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x,2,2)
x = F.relu(self.conv3(x))
x = F.max_pool2d(x,2,2)
x = F.relu(self.conv4(x))
x = F.max_pool2d(x,2,2)
x = x.view(-1, self.num_flatten)
x = F.relu(self.fc1(x))
x = F.dropout(x,self.dropout_rate,training=self.training)
x=self.fc2(x)
return F.log_softmax(x,dim=1)
params_model={
"input_shape":(3,96,96),
"initial_filters":8,
"num_fc1":100,
"dropout_rate":0.25,
"num_classes":2,
}
model = Net(params_model)
path2weights="./cnn_weights.pt"
model.load_state_dict(torch.load(path2weights,map_location="cpu")
model.eval()
导入数据集
#1. 导入mydataset
import mydataset
#2. 定义数据加载器对象并且获得一小批数据
test_dl=mydataset.test_dl
for xb,yb in test_dl:
print(xb.shape,yb.shape)
break
# torch.Size([1,3,96,96]) torch.Size([1])
导入预训练权重
我们将加载预先训练的模型,冻结其参数,并在测试数据集上验证其性能:
#1. 导入预训练权重
import mymodel
#2. 定义预训练模型的对象
model=mymodel.model
#3. 将模型移到GPU设备上
import torch
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model=model.to(device)
#4. 定义冻结模型参数的辅助函数
def freeze_model(model):
for child in model.children():
for param in child.parameters():
param.requires_grad=False
print("model frozen")
return model
#5. 调用辅助函数
model=freeze_model(model)
#6. 在测试样本上部署预训练模型
from sklearn.metrics import accuracy_score
def deploy_model(model, test_dl):
y_pred=[]
y_gt=[]
with torch.no_grad():
for x, y in test_dl:
y_gt.append(y.item())
out=model(x.to(device)).cpu().numpy()
out=np.argmax(out,axis=1)[0]
y_pred.append(out)
return y_pred,y_gt
y_pred,y_gt=deploy_model(model,test_dl)
#7. 在测试数据上验证模型的性能
from sklearn.metrics import accuracy_score
acc=accuracy_score(y_pred,y_gt)
print("accuracy:%.2f"%acc)
# accuracy:0.94
实现攻击相关代码
#1. 导入需要的包
from torchvision.transforms.functional import to_pil_image
import torch.nn.functional as F
import matplotlib.pyplot as plt
#2. perturb_input辅助函数定义如下
def perturb_input(xb,yb,model,alfa):
xb=xb.to(device)
xb.requires_grad=True # 输入数据可导
out=model(xb).cpu()
loss=F.nll_loss(out,yb)# 模型结构用F.log_softmax激活函数,此处用F.nll_loss即可
model.zero_grad()
loss.backward()
xb_grad=xb.grad.data # 输入数据的梯度值
xb_p=xb+alfa*xb_grad.sign() # xb_grad.sign()表示输入数据的梯度值的正负号,梯度上升
xb_p=torch.clamp(xb_p,0,1) # 输入数据数值约束在[0,1]
return xb_p, out.detach()
#3. 从数据加载器中获取批数据并调用perturb_input函数
y_pred=[]
y_pred_p=[]
for xb,yb in test_dl: # batch_size为1
xb_p,out=perturb_input(xb,yb,model,alfa=0.005)
#4. 计算扰动前后的预测概率
with torch.no_grad():
pred=out.argmax(dim=1,keepdim=False).item()
y_pred.append(pred)
prob=torch.exp(out[:,1])[0].item()# 模型结构用F.log_softmax激活函数,用torch.exp得到概率
out_p=model(xb_p).cpu()
pred_p=out_p.argmax(dim=1,keepdim=False).item()
y_pred_p.append(pred_p)
prob_p=torch.exp(out_p[:,1])[0].item()
#5. 显示原图和扰动后的图
plt.subplot(1,2,1)
plt.imshow(to_pil_image(xb[0].detach().cpu()))
plt.title(prob)
plt.subplot(1,2,2)
plt.imshow(to_pil_image(xb_p[0].detach().cpu()))
plt.title(prob_p)
plt.show()
#6. 让我们计算扰动数据的模型精度:
acc=accuracy_score(y_pred_p, y_gt)
print("accuracy:%.2f"%acc)
# accuracy:0.31
上面的代码片段将显示扰动前后的测试图像。如以下截图显示:
代码解析:
在加载数据集小节中,我们使用二值图像分类中开发的脚本加载数据集。在步骤1中,我们导入了mydataset.py文件。此文件包含自定义数据集类。在步骤2中,我们定义了一个数据加载器的对象,并提取了一个小批处理。批大小设为1,因此,形状张量(1,3、96、96)。
在加载预训练模型小节中,我们加载了二值图像分类中开发的预训练模型。在第1步中,我们将mymodel.py文件作为包导入。这个文件包含模型类定义。在步骤2中,我们创建了模型类的一个对象。在步骤3中,我们将模型移动到CUDA设备(如果有的话)。在第4步中,我们定义了一个辅助函数来冻结模型参数。这是为了避免对模型参数的任何更新。记住,在对抗性攻击中,我们试图更新输入以更改模型预测。
辅助函数输入:model。在函数中,我们遍历了模型参数,并将requires_grad属性设置为False。在第5步中,我们调用了freeze_model辅助函数。在步骤6中,我们将预先训练好的模型部署到测试数据集上。这一步是为了验证预先训练的模型。在步骤7中,我们在测试数据集上验证了预训练模型的性能。如所见,当前在原始测试数据集上的精度很高。这验证了预先训练的模型是正确加载的。我们将看到在引入对抗性攻击时该性能会受到怎样的影响。
在实现攻击小节中,我们开发了FSG攻击。在步骤1中,我们导入了所需的包。
在步骤2中,我们定义了perturb_input辅助函数。函数输入如下:
- xb:要被扰动的输入,形状为[1,3,height,width]的PyTorch张量
- yb:目标标签,形状为[1]的张量
- model:预训练模型
- alfa:扰动参数,设置为0.005
在辅助函数中,我们首先将输入张量的requires_grad属性设置为True。这样做是为了能够计算出损失相对于输入的梯度。接下来,我们将输入传递给预先训练的模型,并获得它的输出。
接下来,我们通过比较模型输出和目标标签来计算损失值。然后,我们计算了相对于输入张量的损失梯度。最后,我们通过添加部分带符号的梯度来干扰输入。alfa系数定义了添加到输入中的扰动量。更高的alfa值显然会导致更大的扰动。我们把alfa设为0.005,使它足够小,可以改变预测,而我们的眼睛仍然看不见变化。
在步骤3中,我们启动了一个循环来遍历测试数据集。然后我们从数据加载器获得一个小批处理,并将它传递给perturb_input辅助函数。
第4步,对模型进行扰动输入,得到模型预测结果。注意,我们在这一步使用torch.no_grad()方法来停止跟踪梯度。在第5步中,我们显示了原始输入和扰动输入以及预测的概率。正如所见,扰动量不明显的输入,预测却几乎翻转。在步骤6中,我们计算了扰动后数据的模型精度。可以看出,在数据扰动的情况下,模型的精度明显下降。
我们用一个小系数来干扰输入。尝试改变alfa系数,看看它对输入图像和模型预测的影响。您还可以尝试使用FGS攻击来欺骗其他模型。