ResNet模型的原理以及实现


1 实现MNIST数据集分类
1.1 导入相应库、定义常量以及加载MNIST数据
- 导入库
| 1 | import torch | 
- 定义常量:批处理大小,设备
| 1 | BATCH_SIZE = 256 | 
- 利用torchvision加载MNIST数据集,图像数据一般需要自定义transform(data[0][0]表示第一张图片,data[1][1]表示第一张图片的标签)
| 1 | # 通过transform.Lambda来自定义修改策略 | 
1.2 定义模型
- 本实验使用的三层线性层,模型中需要定义初始化函数以及前向函数
- 线性层的结构定义跟图片维度、类别相关(本实验数据集为MNIST,一般处理为28*28)
| 1 | class Model(nn.Module): | 
1.3 定义训练函数
- 训练函数中需要定义损失函数,然后加载数据,并将数据输入至网络中,损失函数计算损失,将损失反向传播后,利用优化函数更新网络参数,需要注意一点(每一个mini-batch都需要将优化函数中的参数的梯度至零)。可以看出文本分类和图像分类的训练函数差别不大,网络的训练步骤是一样的。
| 1 | def train(model, device, train_loader, optimizer, epoch): | 
1.4 定义测试函数
- 图像分类的测试函数与文本分类并无太大差别,在这里就不详细介绍了。
| 1 | def test(model,device, test_loader): | 
1.5 训练+保存模型+加载模型+测试
- 初始化模型,优化器并定义模型保存路径
| 1 | model = Model() | 
- 迭代训练,保存模型
| 1 | # 训练以及测试 | 
- 训练过程如下图所示  
- 加载模型,测试准确率 
| 1 | best_model = Model() | 
- 测试结果如下图所示  
2 实现FGSM攻击
2.1 导入相应库,定义常量,加载模型
- 需要定义的常量:模型保存路径,要扰动的图片,扰动程度(FGSM论文中的$\varepsilon$)
| 1 | import torch | 
2.2 实现FGSM
- 将图片输入到网络中,求出损失并反向传播
- 根据公式FGSM论文中的扰动表达式,$\varepsilon{sign(\nabla_{x}J(\boldsymbol{\theta},\boldsymbol{x},y))}$,求出扰动 
- 需要注意:当需要用到tensor的梯度时,需要将数据定义为Variable,不明白可以参考 
| 1 | loss_function = nn.CrossEntropyLoss() | 
- 测试攻击效果并展示攻击结果
| 1 | outputs = best_model(x_adversarial) | 

3 实现DeepFool攻击
3.1 导入相应库,定义常量,加载模型
- 需要定义的常量:模型保存路径,需要扰动的图片,迭代次数(DeepFool的扰动需要迭代添加),最大迭代次数,论文中的$\eta$(保证数据点跨越分界面) - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- import torch 
 from ImageClassification.MNISTClassification import Model, testdata
 from torch.autograd import Variable
 import numpy as np
 import copy
 from torch.autograd.gradcheck import zero_gradients
 from torchvision import transforms
 import matplotlib.pyplot as plt
 
 PATH = './mnist_model.pth' # 模型保存路径
 index = 100
 loop_i = 0
 max_iter = 50
 overshoot = 0.02
 
 net = Model()
 net.load_state_dict(torch.load(PATH))
3.2 实现DeepFool
- 获取原始图像的标签,实验中直接利用模型的前向函数得到标签,也可以直接将图片输入至网络中得到标签 - 1 
 2
 3
 4
 5
 6
 7- # 得到原始图片类别 
 image = Variable(testdata[index][0].resize_(1, 784), requires_grad=True)
 label = torch.tensor([testdata[index][1]])
 # flatten() 将多维数组转换为一维数组
 f_image = net.forward(image).data.numpy().flatten() # shape [1, 10] -> [10]
 I = (np.array(f_image)).flatten().argsort()[::-1] # [::-1]从后往前取数
 label = I[0]
- 初始化网络参数,其中包括:公式中需要使用的权重,扰动,分类器输出$w$,$r$,$f$ - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- # 初始化网络参数 
 input_shape = image.data.numpy().shape
 pert_image = copy.deepcopy(image) # 深复制,复制前后两个对象单独存在,不互相影响
 w = np.zeros(input_shape)
 r_tot = np.zeros(input_shape)
 x = Variable(pert_image, requires_grad=True)
 fs = net.forward(x) # shape [1, 10]
 # fs_list = [fs[0][I[k]] for k in range(len(I))] # 按照概率从大到小的顺序将fs值进行排序
 k_i = label
- 参照论文中的伪代码,写出迭代攻击算法,其中外循环控制扰动是否成功以及迭代次数是否超过最大限制,内循环控制找出图像点和k个分界面中距离最近的分界面,找到最近分界面后就可以求出需要扰动的距离。  - 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- while k_i == label and loop_i < max_iter: 
 pert = np.inf
 fs[0][I[0]].backward(retain_graph=True) # 多次反向传播(多层监督)时,梯度是累加的
 orig_grad = x.grad.data.numpy().copy() # 赋值,等号左右占用同一个地址
 # 从k个扰动距离中选择最近的距离
 for k in range(len(I)):
 zero_gradients(x)
 fs[0][I[k]].backward(retain_graph=True)
 cur_grad = x.grad.data.numpy().copy()
 w_k = cur_grad - orig_grad # shape [1, 784]
 f_k = (fs[0][I[k]] - fs[0][I[0]]).data.numpy() # shape [1]
 pert_k = abs(f_k) / np.linalg.norm(w_k.flatten())
 if pert_k < pert:
 pert = pert_k
 w = w_k
 r_i = (pert + 1e-4) * w / np.linalg.norm(w)
 r_tot = np.float32(r_tot + r_i) # 由于决策面是非线性的,因此需要叠加扰动
 pert_image = image + (1 + overshoot) * torch.from_numpy(r_tot)
 # x = Variable(pert_image, requires_grad=True)
 fs = net.forward(pert_image)
 k_i = np.argmax(fs.data.numpy().flatten()) # 扰动后的类别
 loop_i += 1
 r_tot += (1 + overshoot) * r_tot
- 测试攻击效果并展示攻击结果(代码与FGSM基本一致) - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19- outputs = net(pert_image.data.resize_(1, 784)) 
 predicted = torch.max(outputs.data, 1)[1]
 print("original predicata is : {}".format(label))
 print("attacked predicate is : {}".format(predicted[0]))
 pert_image = pert_image.data
 pert_image = pert_image.resize(28, 28)
 img_adv = transforms.ToPILImage()(pert_image)
 plt.subplot(1, 2, 1)
 plt.title("img_adv")
 plt.imshow(img_adv)
 img = testdata[index][0]
 img = transforms.ToPILImage()(img.resize_(28, 28))
 plt.subplot(1, 2, 2)
 plt.title("img_org")
 plt.imshow(img)
 plt.show() 
4 参考文献
- https://github.com/chaoge123456/MLsecurity/blob/master/blog/FGSM%20and%20DeepFool/adversary_example.ipynb
- https://blog.csdn.net/u011630575/article/details/78604226
- https://blog.csdn.net/xiaoxifei/article/details/87797935
- https://www.cv-foundation.org/openaccess/content_cvpr_2016/html/Moosavi-Dezfooli_DeepFool_A_Simple_CVPR_2016_paper.html
- https://arxiv.org/abs/1412.6572
附录
- 知识点1:copy()、deepcopy()与赋值的区别 - deepcopy() ,深复制,即将被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出来的新对象产生影响。(复制前后的两个对象占用不同的地址) - = ,赋值,并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。(是同一个对象,占用相同的地址,只是名称不同) - copy() ,浅复制,分为两种情况 - 1)当浅复制的值是不可变对象(数值,字符串,元组)时和“等于赋值”的情况一样,对象的id值与浅复制原来的值相同。 - 2)当浅复制的值是可变对象(列表和元组)时会产生一个“是那么独立的对象”存在。有两种情况: - 第一种情况:复制的 对象中无 复杂 子对象,原来值的改变并不会影响浅复制的值,同时浅复制的值改变也并不会影响原来的值。原来值的id值与浅复制原来的值不同。 - 第二种情况复制的对象中有 复杂 子对象 (例如列表中的一个子元素是一个列表),如果不改变其中复杂子对象,浅复制的值改变并不会影响原来的值。但是改变原来的值 中的复杂子对象的值会影响浅复制的值。 
- 知识点2:optimizer.step() 和loss.backward() - 1 
 2
 3
 4
 5
 6
 7
 8- optimizer = Adam(model.parameters(), lr=0.01) 
 loss_function = nn.CrossEntropyLoss()
 for mini-batch:
 optimizer.zero_grad()
 根据输入求出loss
 loss.backward()
 optimizer.step()- 一般情况下,模型的训练代码都是这种结构的,优化器的作用是优化模型的参数,因此初始化时需要将模型的参数交给优化。优化器还需要损失反向传播的梯度信息,因此在loss.backward()之后接上optimizer.step()。切记,每一个mini-batch都需要将优化器中的梯度信息清零,以免影响后续的优化。 
- 保存网络与加载网络 - 1 
 2
 3
 4
 5
 6
 7
 8- # 保存整个网络 
 torch.save(net, 'mnist_net_all.pkl')
 # 保存网络中的参数, 速度快,占空间少
 torch.save(net.state_dict(),'mnist_net_param.pkl')
 # 针对上面一般的保存方法,加载的方法分别是:
 model_dict=torch.load(PATH)
 model_dict=model.load_state_dict(torch.load(PATH))



