共计 7114 个字符,预计需要花费 18 分钟才能阅读完成。
对抗训练介绍——尝试欺骗一个模型
作者:支广达
当我们要实际部署一个机器学习系统的时候,一件非常重要的事情就是系统的鲁棒性,我们希望系统不仅能够对大多数的例子有效,而且要真正的可靠,例如能够识别出别人的攻击(欺骗你的分类模型)。因此近几年对抗鲁棒性(Adversarial Robustness)这个话题引发了广泛的关注。要先改进模型,我们必须知道模型的问题在哪,今天我们就来感受下我们的模型是如何被欺骗的
加载模型和样例图片
深度学习的魅力之处在于你可以很容易的开始实践然后看到一些在数据上的实际结果。下面我们就来构造我们第一个欺骗模型的例子。
在开始前我们先用 pytorch 加载一个训练好的 Resnet50 的模型和一张猪的图片用来测试。
我们将图片大小改成 224X224, 并将其转成 tensor:
from PIL import Image
from torchvision import transforms
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
# read the image, resize to 224 and convert to PyTorch Tensor
pig_img = Image.open("pig.jpg")
preprocess = transforms.Compose([transforms.Resize(224),
transforms.ToTensor(),])
pig_tensor = preprocess(pig_img)[None,:,:,:]
# plot image (note that numpy using HWC whereas Pytorch user CHW, so we need to convert)
plt.imshow(pig_tensor[0].numpy().transpose(1,2,0))
<matplotlib.image.AxesImage at 0x7f14d3fd3550>
下面加载在 imagenet 数据集上训练好的 ResNet50 模型,并将图片输入查看结果。下面图片处理成 batch_size x num_channels x height x width
的形式是 pytorch 统一的输入格式
import torch
import torch.nn as nn
from torchvision.models import resnet50
# simple Module to normalize an image
class Normalize(nn.Module):
def __init__(self, mean, std):
super(Normalize, self).__init__()
self.mean = torch.Tensor(mean)
self.std = torch.Tensor(std)
def forward(self, x):
return (x - self.mean.type_as(x)[None,:,None,None]) / self.std.type_as(x)[None,:,None,None]
# values are standard normalization for ImageNet images,
# from https://github.com/pytorch/examples/blob/master/imagenet/main.py
norm = Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
# load pre-trained ResNet50, and put into evaluation mode (necessary to e.g. turn off batchnorm)
model = resnet50(pretrained=True)
model.eval();
# form predictions
pred = model(norm(pig_tensor))
现在模型输出结果 pred 是一个 1000 维的向量,代表了 imagenet 的 1000 类图片。要找到这个结果预测的是哪个类别,最简单的可以找向量中最大的那个值,然后找出所对应的类别:
import json
with open("imagenet_class_index.json") as f:
imagenet_classes = {int(i):x[1] for i,x in json.load(f).items()}
print(imagenet_classes[pred.max(dim=1)[1].item()])
hog
预测是正确的!(pig 在 imagenet 数据集中标签是 hog)
一些基础概念
为了下面解释如何欺骗模型,我们需要先介绍一些基础概念。
第一步:我们将模型定义为一个函数:
$$
h_{\theta}: \mathcal{X} \rightarrow \mathbb{R}^{k}
$$
代表了将输入空间映射到 k 维输出空间的函数,$k$ 就是分类的数量,$\theta$ 代表了模型中的所有训练参数,所以 $h_{\theta}$ 就代表我们的模型.
第二步:我们定义损失函数:$\ell\left(h_{\theta}(x), y\right)$ , 其中 $x$ 为输入样本 $y$ 是对应的正确的 label,具体的我们用 cross entropy 损失函数:
$$
\ell\left(h_{\theta}(x), y\right)=\log \left(\sum_{j=1}^{k} \exp \left(h_{\theta}(x)_{j}\right)\right)-h_{\theta}(x)_{y}
$$
这里 $h_{\theta}(x)_{j}$ 代表 $h_{\theta}(x)$ 中第 $j$ 个元素.
# 341 is the class index corresponding to "hog"
print(nn.CrossEntropyLoss()(model(norm(pig_tensor)),torch.LongTensor([341])).item())
0.003882253309711814
0.0039 的损失已经非常小了,我们的模型会以 $\exp (-0.0039) \approx 0.996$ 的概率认为这张图片是一头猪。
创建一张对抗图片
那么,我们如何处理该图像以使得能够欺骗这个模型,让它认为是其他东西呢?在回答这个问题前,我们先来看看模型是怎么训练的,训练分类器的常用方法是优化参数 $\theta$,以最大程度地减少某些训练集的平均损失 $\left\{x_{i} \in \mathcal{X}, y_{i} \in \mathbb{Z}\right\}, i=1, \ldots, m$ , 我们将其写为优化问题
$$
\operatorname{minimize}_{\theta} \frac{1}{m} \sum_{i=1}^{m} \ell\left(h_{\theta}\left(x_{i}\right), y_{i}\right)
$$
我们通常通过(随机)梯度下降来解决这个优化问题,即:
$$
\theta:=\theta-\frac{\alpha}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \nabla_{\theta} \ell\left(h_{\theta}\left(x_{i}\right), y_{i}\right)
$$
这里 $\alpha$ 是步长,$\mathcal{B}$ 是一个 batch。对于深度神经网络,可以通过反向传播有效地计算此梯度。但是反向传播的还有一个优点在于,我们不仅可以让 loss 对 θ 求导,我们还可以让 loss 对于输入本身进行求导!!这正是我们要生成的对抗例子的方法。我们通过反向传播调整图像使得损失最大化,也就是说我们要解决下面的优化问题
$$
\underset{\hat{x}}{\operatorname{maximize}} \ell\left(h_{\theta}(\hat{x}), y\right)
$$
这里的 $\hat{\boldsymbol{x}}$ 代表我们的对抗图片,它的目的就是最大化对应的损失。当然,我们不能仅仅通过任意优化 $\hat{\boldsymbol{x}}$ (毕竟,确实存在一些不是猪的图像,比如我们完全改变图像,让图像变成狗,那么分类器将他判别为不是猪这就是很正常的了)因此,我们需要确保 $\hat{\boldsymbol{x}}$ 接近我们的原始输入 $x$。所以我们将优化问题写成:
$$
\operatorname{maximize} \ell\left(h_{\theta}(x+\delta), y\right)
$$
这里的 $\Delta$ 是表示对图像进行改变的范围,从理论上讲,我们希望 $\Delta$ 能够包括让人类视觉上认为改动后的图片与原来输入的图片是相同的任何改动。这个可能包括添加少量噪声到旋转,平移,缩放或对基础模型执行某些 3D 转换,甚至包括这头猪的另一个拍摄角度。但在数学上,是不可能给出一个严格的定义的。所以我们只能定义这样一个空间使得,对图片的最大扰动不会让图片的内容含义改变:
$$
\Delta=\left\{\delta:\|\delta\|_{\infty} \leq \epsilon\right\}
$$
这里 $\|\delta\|_{\alpha}$ 定义为
$$
\|\delta\|_{\infty}=\max _{i}\left| \delta_{i}\right|
$$
好接下来让我们来实际看下这个方法的效果,以下示例使用 PyTorch 的 SGD 优化器将输入的扰动调整为最大,以最大程度地减少损失。
import torch.optim as optim
epsilon = 2./255
delta = torch.zeros_like(pig_tensor, requires_grad=True)
opt = optim.SGD([delta], lr=1e-1)
for t in range(30):
pred = model(norm(pig_tensor + delta))
loss = -nn.CrossEntropyLoss()(pred, torch.LongTensor([341]))
if t % 5 == 0:
print(t, loss.item())
opt.zero_grad()
loss.backward()
opt.step()
delta.data.clamp_(-epsilon, epsilon)
print("True class probability:", nn.Softmax(dim=1)(pred)[0,341].item())
0 -0.003882253309711814
5 -0.0069345044903457165
10 -0.01582527346909046
15 -0.08056001365184784
20 -11.751323699951172
25 -16.78317642211914
True class probability: 1.3113177601553616e-06
经过了 30 步的梯度下降,我们的 Resnet50 认为这张图片是猪的可能性已经小于 了,下面我们查看一下现在模型认为这张图片是什么。
max_class = pred.max(dim=1)[1].item()
print("Predicted class:", imagenet_classes[max_class])
print("Predicted probability:", nn.Softmax(dim=1)(pred)[0,max_class].item())
Predicted class: wombat
Predicted probability: 0.9999175071716309
现在这个模型认为我们的输入是一只毛鼻袋熊,很有趣吧!我们再看看我们实际输入的图片是长什么样子的:
plt.imshow((pig_tensor + delta)[0].detach().numpy().transpose(1,2,0))
<matplotlib.image.AxesImage at 0x7f14d0dcb358>
很遗憾,我们对图片的改变人的肉眼完全看不出来。
现在将我们的 delta
放大 50 倍,看看我们做了什么改变
plt.imshow((50*delta+0.5)[0].detach().numpy().transpose(1,2,0))
<matplotlib.image.AxesImage at 0x7f14cc22a908>
因此,通过添加这种看起来很随机的噪声的微小倍数,我们可以创建看起来与原始图像相同但被错误分类的图像。
针对性的欺骗
利用这个原理,更进一步的,我们可以设置欺骗的目标对象,比如下面我们让模型认为这只猪是一架飞机。
与上述方法不同的是,我们不仅要最大化模型输出与正确对象的 loss,我们还要最小化输出与我们目标对象的 loss:
$$
\underset{\delta \in \Delta}{\operatorname{maximize}}\left(\ell\left(h_{\theta}(x+\delta), y\right)-\ell\left(h_{\theta}(x+\delta), y_{\mathrm{target}}\right)\right) \equiv \underset{\delta \in \Delta}{\operatorname{maximize}}\left(h_{\theta}(x+\delta)_{y_{\mathrm{target}}}-h_{\theta}(x+\delta)_{y}\right)
$$
delta = torch.zeros_like(pig_tensor, requires_grad=True)
opt = optim.SGD([delta], lr=5e-3)
for t in range(100):
pred = model(norm(pig_tensor + delta))
loss = (-nn.CrossEntropyLoss()(pred, torch.LongTensor([341])) +
nn.CrossEntropyLoss()(pred, torch.LongTensor([404])))
if t % 10 == 0:
print(t, loss.item())
opt.zero_grad()
loss.backward()
opt.step()
delta.data.clamp_(-epsilon, epsilon)
0 24.006044387817383
10 -0.24818801879882812
20 -8.039923667907715
30 -15.460402488708496
40 -21.939563751220703
50 -26.95309066772461
60 -31.754430770874023
70 -33.13744354248047
80 -37.07537841796875
90 -34.388519287109375
max_class = pred.max(dim=1)[1].item()
print("Predicted class:", imagenet_classes[max_class])
print("Predicted probability:", nn.Softmax(dim=1)(pred)[0,max_class].item())
Predicted class: airliner
Predicted probability: 0.8934944868087769
现在我们的模型已经认为这只猪已经是一架飞机了!同样的,我们的飞机猪看起来还是和原来的图片一模一样:
plt.imshow((pig_tensor + delta)[0].detach().numpy().transpose(1,2,0))
<matplotlib.image.AxesImage at 0x7f14cc1ccd68>
下面是我们添加的噪声
plt.imshow((50*delta+0.5)[0].detach().numpy().transpose(1,2,0))
<matplotlib.image.AxesImage at 0x7f14cc1f37b8>
以上就是我们这次分享的利用对抗训练中构建一个欺骗模型的例子,这只是一个有趣的小例子,如果您对对抗训练感兴趣,可以查找相关论文,进行进一步的学习
项目地址:https://momodel.cn/explore/5eba60c9d99e51afef3bfebd?type=app(推荐在电脑端使用 Google Chrome 浏览器进行打开)
引用
- seq2seq:https://blog.csdn.net/rxm1989/article/details/79459739
- attention:https://zhuanlan.zhihu.com/p/47063917
- 代码来源:https://github.com/EuphoriaYan/ChatRobot-For-Keras2
关于我们
Mo(网址:https://momodel.cn)是一个支持 Python 的人工智能在线建模平台,能帮助你快速开发、训练并部署模型。
近期 Mo 也在持续进行机器学习相关的入门课程和论文分享活动,欢迎大家关注我们的公众号获取最新资讯!