赞
踩
首先需要指出的是,代码是从李宏毅老师的课程中下载的,并不是我自己码的。这篇文章主要是在原代码中加了一些讲解和注释,以及将繁体字改成了简体字。
今天的内容,说实在话,有点像是教咱们如何成为一名“特工”(哈哈~~碰巧最近在回顾马特达蒙的谍影重重,感觉这一部分还挺有趣)。
首先我们来介绍一下反向攻击这个词以及代码中涉及的算法。反向攻击就是在原图片中加一些极其微小的杂讯(这些杂讯有时候人眼根本看不出来),以使得我们之前训练出来的很不错的神经网络失效。听起来是不是很酷,以后提前偷摸下班的时候就可以不被摄像头认出来了(开个玩笑~)。
那我们可以采取的攻击有哪几种呢?李宏毅老师在课上教了这两种攻击的形式。其中第一张被叫做 non-targeted attack,顾名思义,这种攻击并没有想让网络把原图里面的虎斑猫误认为是某一种特定的物种,而是只要求其认错就行,至于认错成什么是无所谓的。这种情况下,我们的目标函数就是要求预测出来的结构与真实的结构越远越好,即最终采用交叉熵损失的负数。第二种攻击方式叫做 targeted-attack,顾名思义,就是要让网络把原来的虎斑猫误认为是某种特定的物种(比如ppt提到的鱼)。这时我们的损失函数就是让预测物种和原物种(虎斑猫)尽可能远,同时使得预测物种和特定物种(鱼)尽可能近。我们在这次代码中提到FGSM攻击方法属于第一种non-targeted attack。
注意:1. 我们找出来的用来攻击的图片要和原图片相差较近,需要加一个constraint(因为差别太大本来就会认成另一种物种,没有攻击的意义)2. 在攻击的时候我们的因变量是图片
x
′
x^{'}
x′ 而不是模型的参数
θ
\theta
θ,因为模型的参数
θ
\theta
θ 其实是已经训练出来的,我们这一步是要找到最好的攻击图像。
FGSM是怎么做的呢?从上面的讨论我们可以看出,我们要做的其实就是下面ppt中给出的第一个公式(我们要通过这个公式去寻找最佳的 x)。因为有constraint的存在,我们不能直接做梯度下降。FGSM选择了一种较为简单的改进,即直接向着梯度的方向移动一个
ϵ
\epsilon
ϵ,并且只移动这一步。同时,我们可以看到,这次移动是符合我们的constraint的。这样,我们就做到了攻击。
其他还有很多更高级的攻击方法,这里就不介绍了。
下面通过代码来看FGSM是怎么做的,其中我们选取的预训练的模型(也就是我们要攻击的模型)是著名的VGG16模型。VGG16是由Simonyan 和Zisserman在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组(Visual Geometry Group)的缩写。该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。它的结构如下图所示(其实并不是很复杂哈~主要是一些卷积层、池化层和全连接层组成):
# 下载资料
!gdown --id '14CqX3OfY9aUbhGp4OpdSHLvq2321fUB7' --output data.zip
# 解压
!unzip -qq -u data.zip
# 确认目前的档案
!ls
Downloading...
From: https://drive.google.com/uc?id=14CqX3OfY9aUbhGp4OpdSHLvq2321fUB7
To: /content/data.zip
17.9MB [00:00, 109MB/s]
data data.zip sample_data
import os # 读取 label.csv import pandas as pd # 读取图片 from PIL import Image import numpy as np import torch # Loss function import torch.nn.functional as F # 读取资料 import torchvision.datasets as datasets from torch.utils.data import Dataset, DataLoader # 载入预训练的网络模型 import torchvision.models as models # 将资料转换为符合预训练模型的形式 import torchvision.transforms as transforms # 展示图片 import matplotlib.pyplot as plt device = torch.device("cuda")
class Adverdataset(Dataset): def __init__(self, root, label, transforms): # 图片所在资料夹 self.root = root # 导入图片的label(注意这里要转换为longtensor格式) self.label = torch.from_numpy(label).long() # 将输入的图片转换为符合预训练模型的格式 self.transforms = transforms # 图片档案名称的 list self.fnames = [] for i in range(200): self.fnames.append("{:03d}".format(i)) def __getitem__(self, idx): # 利用路径读取图片 img = Image.open(os.path.join(self.root, self.fnames[idx] + '.png')) # 将输入的图片转换为符合预训练模型的形式 img = self.transforms(img) # 图片相对应的 label label = self.label[idx] return img, label def __len__(self): # 由于自己已经知道图片库里面有 200 张图片, 所以回传 200 return 200
class Attacker: def __init__(self, img_dir, label): # 读入预训练模型 vgg16 self.model = models.vgg16(pretrained = True)#保留预训练模型的参数 self.model.cuda() self.model.eval()#因为模型已经预训练好了,所以这里就不用像之前一样设置.train了,而是直接.eval #因为我们之后要对图片进行标准化,所以我们在这里设置一下标准化的均值和方差,三维的数据分别对应的RGB三原色的三个通道 self.mean = [0.485, 0.456, 0.406] self.std = [0.229, 0.224, 0.225] # 把图片 normalize 到 0~1 之间 mean 0 variance 1 self.normalize = transforms.Normalize(self.mean, self.std, inplace=False) transform = transforms.Compose([ transforms.Resize((224, 224), interpolation=3),#参数interpolation表示选择的插值方式,这里选择的是双线性插值 transforms.ToTensor(), self.normalize ]) # 利用 Adverdataset 这个 class 读取资料 self.dataset = Adverdataset('./data/images', label, transform) self.loader = torch.utils.data.DataLoader( self.dataset, batch_size = 1, shuffle = False) # FGSM 攻击 def fgsm_attack(self, image, epsilon, data_grad): # 找出 gradient 的方向 sign_data_grad = data_grad.sign() # 将图片加上 gradient 方向乘上 epsilon 的 noise perturbed_image = image + epsilon * sign_data_grad # 将图片超过 1 或是小于 0 的部分 clip 掉 # perturbed_image = torch.clamp(perturbed_image, 0, 1) return perturbed_image def attack(self, epsilon): # 存下一些成功攻击后的图片 以便之后展示,本程序选择存不超过5个图片展示 adv_examples = [] wrong, fail, success = 0, 0, 0 for (data, target) in self.loader: data, target = data.to(device), target.to(device) data_raw = data; data.requires_grad = True #我们需要将数据的gradient也计算,因为之后FGSM攻击的时候要用 # 将图片丟入 model 进行测试 得出相对应的 class output = self.model(data) #找出还没被攻击的模型预测出来的 class,通过比较相应概率的大小来决定判断成哪个 class init_pred = output.max(1, keepdim=True)[1] # 如果原始预测的 class 就错误,就不再多此一举的攻击了 if init_pred.item() != target.item(): wrong += 1 continue # 如果 class 正确 就开始计算 gradient 以进行 FGSM 攻击 loss = F.nll_loss(output, target) #交叉熵损失 self.model.zero_grad() #先清空gradient loss.backward() #后向传播计算data的gradient data_grad = data.grad.data perturbed_data = self.fgsm_attack(data, epsilon, data_grad) #用上一步编写的FGSM实施攻击 # 再将加入 noise 的图片丟入 model 进行调试 得出相对应的 class output = self.model(perturbed_data) final_pred = output.max(1, keepdim=True)[1] if final_pred.item() == target.item(): # 辨识结构是正确的 攻击失败 fail += 1 else: # 辨识结构失败 攻击成功 success += 1 # 将攻击成功的前5个图片存入 #注意因为之前做了标准化,这里要反标准化一下 if len(adv_examples) < 5: adv_ex = perturbed_data * torch.tensor(self.std, device = device).view(3, 1, 1) + torch.tensor(self.mean, device = device).view(3, 1, 1) adv_ex = adv_ex.squeeze().detach().cpu().numpy() data_raw = data_raw * torch.tensor(self.std, device = device).view(3, 1, 1) + torch.tensor(self.mean, device = device).view(3, 1, 1) data_raw = data_raw.squeeze().detach().cpu().numpy() adv_examples.append( (init_pred.item(), final_pred.item(), data_raw , adv_ex) ) final_acc = (fail / (wrong + success + fail)) print("Epsilon: {}\tTest Accuracy = {} / {} = {}\n".format(epsilon, fail, len(self.loader), final_acc)) return adv_examples, final_acc
可以看到的是,攻击之后VGG16的预测成功率极低。虽然FGSM并不是很复杂,但是攻击效果不错。
if __name__ == '__main__': # 读入图片相对应的 label df = pd.read_csv("./data/labels.csv") df = df.loc[:, 'TrueLabel'].to_numpy() label_name = pd.read_csv("./data/categories.csv") label_name = label_name.loc[:, 'CategoryName'].to_numpy() # 新建一个 Attacker 类 attacker = Attacker('./data/images', df) # 要尝试的 epsilon epsilons = [0.1, 0.01] accuracies, examples = [], [] # 进行攻击并存放正确率和攻击成功的图片 for eps in epsilons: ex, acc = attacker.attack(eps) accuracies.append(acc) examples.append(ex)
Epsilon: 0.1 Test Accuracy = 6 / 200 = 0.03
Epsilon: 0.01 Test Accuracy = 54 / 200 = 0.27
cnt = 0 plt.figure(figsize=(30, 30)) for i in range(len(epsilons)): for j in range(len(examples[i])): cnt += 1 plt.subplot(len(epsilons),len(examples[0]) * 2,cnt) plt.xticks([], []) plt.yticks([], []) if j == 0: plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14) orig,adv,orig_img, ex = examples[i][j] # plt.title("{} -> {}".format(orig, adv)) plt.title("original: {}".format(label_name[orig].split(',')[0])) orig_img = np.transpose(orig_img, (1, 2, 0)) plt.imshow(orig_img) cnt += 1 plt.subplot(len(epsilons),len(examples[0]) * 2,cnt) plt.title("adversarial: {}".format(label_name[adv].split(',')[0])) ex = np.transpose(ex, (1, 2, 0)) plt.imshow(ex) plt.tight_layout() plt.show()
我们来看一下,究竟是什么样子的改变让我们的VGG16认不出来原来的物种了。下面给出了5组图片,其中左边是原来被VGG16成功预测的图片,而右边是加了一些杂讯后被VGG16误分类的图片。从图可以看出,这些杂讯极其微小,人眼一般是看不出来的。也正因如此,我们的攻击是十分有效的。我们给原图里面混入一些这样的图片,会使得原有的模型出现不小的差错。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。