介绍
对抗鲁棒性 (adversarial robustness):我们能否开发出对输入的(测试时)扰动鲁棒的分类器,而这些扰动是由意图欺骗分类器的敌人产生的。
准备工作
Python 3.7
- pytorch 1.0
- cvxpy 1.0
- numpy/scipy/PIL/etc
需要的背景知识
深入
首先,我们使用 PyTorch 中(预训练的)ResNet50 模型来分类猪的这张图片。
PyTorch 中正常的图像分类策略是首先使用torchvision.transforms
模块对图像进行变换(至近似 0 均值,单位方差)。然而,因为我们想要在原来的(非标准化的)图像空间制造扰动,我们会用一个稍微不同的方法,实际上在 PyTorch 层上构建变换,以便我们可以直接输入图像。首先,让我们加载这张图像并调整大小为 224x224,即大多数 ImageNet 图像用作输入的默认大小(因此是预训练分类器)。
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
# 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 uses HWC whereas Pytorch uses CHW, so we need to convert)
plt.imshow(pig_tensor[0].numpy().transpose(1, 2, 0))
现在让我们在必要的变换后加载预训练的 ResNet50 模型并将它应用到图像上(这里奇怪的索引只是用于遵循 PyTorch 标准,模块的所有输入应该是batch_size x num_channels x height x weight
的形式)。
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 维的向量,包含 1000 个 imagenet 类别的 logit 值(即如果你想要把它转换成一个概率向量,你应该对这个向量使用 softmax 运算)。为了找到最大似然的类,我们简单地取这个向量中最大值的索引,并且在 imagenet 类的列表中查找该索引来找到对应的标签。
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
成功识别出该图像是猪。
一些介绍的符号
现在我们尝试欺骗这个分类器把这张图像识别为其他东西。为了解释这一过程,我们要介绍一些符号。具体来说,我们会定义模型,或假设函数, 为从输入空间(上例中是一个三维的张量)到输出空间的映射。输出空间是一个 维的向量,其中 是正被预测的类的数量。注意像我们上面的模型,输出对应于 logit 空间,所以这些实数可正可负。 向量表示所有定义这个模型的参数(即所有的卷积滤波器,全连接层权重矩阵,偏差等等), 参数是当我们训练一个神将网络的时候通常去优化的。最后,注意这个 恰好对应于上面 Python 代码的model
对象。
其次,我们定义一个损失函数 为一个从模型预测和真实标签到一个非负数的映射。这个损失函数的语义是第一个自变量是模型的输出(logits 可正可负),第二个自变量是真实类别的索引(即一个从 1 到 的数来表示真实类别的索引)。因此,对于输入 和真实类别 ,这个符号
表示假定真实标签为 的情况下分类器对 的预测取得的损失。到目前为止深度学习中使用的最常见的损失形式是交叉熵损失(有时也叫 softmax 损失),定义为
其中 表示向量 的第 个元素。
另:对于不熟悉上面惯例的人,注意这个损失函数的形式来自于典型的 softmax 激活函数。定义 softmax 运算 应用于一个向量
为一个从由 返回的类别 logits 到一个概率分布的映射。那么典型的训练网络的目标是最大化真实类别标签的概率。由于概率本身接近 0 很小,更加常见的是去最大化真实类别标签概率的 log 值,即
由于惯例是想要最小化损失(而不是最大化概率),我们使用这个值的负数作为我们的损失函数。我们可以在 PyTorch 中使用下面的指令来求这个损失。
# 341 is the class index corresponding to "hog"
print(nn.CrossEntropyLoss()(model(norm(pig_tensor)), torch.LongTensor([341])).item())
0.003882253309711814
0.0039 的损失非常小,由上面的惯例,对应于分类器认为这是一只猪的概率为 。
创建对抗样本
所以如何操作这张图像才能使分类器把它认作别的东西呢?为了回答这一问题,注意通常训练分类器的方法是优化参数 ,以便最小化在某个训练集 上的平均损失,我们可以写作最优化问题
我们通常用(随机)梯度下降解该问题。即,对于某个小批量 ,我们计算损失关于参数 的梯度并对 在相反方向上做一个小的调整
其中 是某个步长,我们对不同的小批量重复这个过程直至覆盖整个训练集,直至参数收敛。
这里最重要的一项是梯度 ,计算对每个参数 的小调整会怎样影响损失函数。对于深度神经网络,这个梯度可以通过反向传播来高效地计算。然而,自动微分(构成反向传播的基础的数学技术)的优美在于我们不仅局限于对损失关于 求微分,我们可以很容易地计算损失关于输入 本身的梯度。这个值可以告诉我们图像本身的小变化会怎样影响损失函数。
这恰好就是为了生成对抗样本我们要去做的事。但是不是像我们在优化网络参数时做的去调整图像来最小化损失,我们要做的是调整图像来最大化损失。就是说,我们想要解决这个优化问题
其中 表示我们试图最大化损失的对抗样本。当然,我们不能只是去随意地优化 (毕竟,确实存在不是猪的图像,如果我们完全地改变了图像,比如说一只狗,那么我们可以“欺骗”分类器把它认作不是猪,这就不是特别令人印象深刻了)。所以相反我们需要确保 和我们的原始输入 很接近。习惯上说,我们通常通过优化对 的扰动来实现,这个扰动我们记为 ,那么通过优化
其中 表示允许的扰动集合。描述“正确的”允许的扰动集合实际上非常难:理论上,我们想要 捕捉人类视觉上觉得和原始输入 “一样”的一切。它可以包括从添加微量噪声到旋转,平移,缩放,或在底层模型上执行一些 3D 转换,甚至完全改变“非猪”位置的图像的一切扰动。不用说,给出一个数学上严密的所有应当允许的扰动的定义是不可能的,但是对抗样本背后的基本原理是我们可以考虑允许的扰动的可能空间的某个子集,以至于根据任何“适当的”定义,图像的实际语义内容都不会在这个扰动下改变。
尽管绝非是唯一合理的选择,一个常用的扰动集是 球,定义为集合
其中向量 的 范数定义为
即我们允许在每个分量上有大小在 之间的扰动(稍微更加复杂,因为我们也应当确保 也在 之间以便它仍是一张有效的图像)。我们之后会回来讨论通常把 球或者范数球作为扰动集是否合理。但我们现在只能说 球的优点在于对于小的 ,它创造出的扰动,给图像中的每个像素添加如此小的一个成分,以至于它们和原来的图像视觉上不可辨别,并且因此给我们提供了一个“必要但绝对不充分的”条件来认为一个对扰动鲁棒的分类器。并且深度网络的现实是它们很容易被恰好是这种类型的操作欺骗。
接下来的例子使用 PyTorch 的SGD
优化器来调整我们对输入的扰动以最大化损失。尽管名字如此,由于这里没有训练集或者小批量的概念,实际上这里不是随机梯度下降,而仅仅是梯度下降。因为我们每一步后面都有一个投影回 球的操作(通过简单地裁剪超出 大小的值到 ),这实际上是一个被称为投影梯度下降(PGD)的过程。我们很快会考虑稍微复杂的版本(其中我们需要详细地操作而非使用 PyTorch 的优化类),但我们现在先考虑简单的情况。
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(f"{t} {loss.item()}")
opt.zero_grad()
loss.backward()
opt.step()
delta.data.clamp_(-epsilon, epsilon)
print(f"True class probability {nn.Softmax(dim=1)(pred)[0, 341].item()}")
0 -0.003882253309711814 5 -0.006934622768312693 10 -0.015797464177012444 15 -0.08087033778429031 20 -12.66297721862793 25 -16.00203514099121 True class probability 6.450456453421793e-07
在 30 个梯度步后,ResNet50 认为图像有小于 的机会是一只猪。(注:我们也应该裁剪 在 范围内,但这对于上面界限中的 已经满足了,所以我们不需要在这里显式地去做了。)相反,结果是分类器非常确信这张图像是一只袋熊,正如我们下面代码所见,计算最大类别和它的概率。
max_class = pred.max(dim=1)[1].item()
print(f"Predicted class: {imagenet_classes[max_class]}")
print(f"Predicted probability: {nn.Softmax(dim=1)(pred)[0, max_class].item()}")
Predicted class: wombat Predicted probability: 0.9996782541275024
所以这个袋熊猪长什么样子呢?不幸的是,和我们原来的猪极其相似。
plt.imshow((pig_tensor + delta)[0].detach().numpy().transpose(1, 2, 0))
下面这事实上是我们加到图像上的delta
,很大程度地以 50 的因数放大了的样子,因为不然的话不可能看到。
plt.imshow((50*delta + 0.5)[0].detach().numpy().transpose(1, 2, 0))
所以本质上,通过添加这种随机样的噪声的很小的倍数,我们可以创造出一张看起来和原来图像一样的图像,然而会被完全错误地分类。当然,为了更加正确地完成这些,我们应该把噪声数值定为图像的可允许的级别(即,在 1/255 的步幅之内),但像这样的技术性细节很容易解决,我们确实可以创建和原始图像相比用人眼无法区分的有效图像,但分类器会错误地进行分类。
定向攻击
可能你会说,这确实令人印象深刻,但袋熊确实和猪没有那么不一样,所以也许这个问题没有那么糟糕。但结果是同样的技术可以被用来创造出可以被分类成几乎任何我们想要的类别的图像。这被称为“定向攻击”,唯一区别是不是仅仅最大化正确类别的损失,我们在最大化正确类别的损失的时候也要最小化目标类别的损失。即,我们要解这个优化问题
其中表达式简化是因为 这一项从每个损失中消掉了,剩余的只有线性项。这就是它的样子。注意我们调整了一点步幅大小使它在这种情况下起效果,但我们很快就会讨论稍有不同的投影梯度下降的缩放方法,这点就不再需要了。
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(f"{t} {loss.item()}")
opt.zero_grad()
loss.backward()
opt.step()
delta.data.clamp_(-epsilon, epsilon)
0 24.006046295166016 10 -0.3006401062011719 20 -7.818149566650391 30 -14.145238876342773 40 -19.913776397705078 50 -26.795330047607422 60 -28.212621688842773 70 -33.595985412597656 80 -35.872467041015625 90 -35.97798156738281
max_class = pred.max(dim=1)[1].item()
print(f"Predicted class: {imagenet_classes[max_class]}")
print(f"Predicted probability: {nn.Softmax(dim=1)(pred)[0, max_class].item()}")
Predicted class: airliner Predicted probability: 0.9099140763282776
和前面一样,这里是我们的飞机猪,看起来非常像一只正常的猪(代码中的目标类别 404 的确是飞机,所以我们的定向攻击起效了)。
plt.imshow((pig_tensor + delta)[0].detach().numpy().transpose(1, 2, 0))
下面是我们的飞机噪声。
plt.imshow((50*delta + 0.5)[0].detach().numpy().transpose(1, 2, 0))
当然,结论是使用对抗攻击和深度学习,你可以让猪飞。
我们稍后会讨论这些攻击引发的实际问题,但这样的攻击的容易性引发了一个显然的问题:我们可以训练出在某种程度上抵抗这种攻击的深度学习分类器吗?这个问题的简短回答是“是的”,但我们(作为一个领域)距离真正实现这样的训练或者几乎达到我们用“标准”深度学习方法获得的性能还有很长的路。这个教程会详尽地包含攻击和防御侧,并希望到它结束时你会对目前发展状况和我们仍要取得大量进展的方向有一个了解。
对抗鲁棒性的简短(不完整)历史
- 起源(鲁棒优化,robust optimization)
- 支持向量机
- 对抗分类(例如 Domingos 2004)
- 不同类型鲁棒性间的差别(测试时,训练时等等)
- Szgegy 等人 2003,Goodfellow 等人 2004
- 许多提出的防御方法
- 许多提出的攻击方法
- 准确验证方法
- 凸上界方法
- 最近趋势
对抗鲁棒性与训练
现在更加正式地探讨攻击深度学习分类器(这里的意思是,构造对抗样本来欺骗分类器)的挑战,和以一种使它们更能抵抗这种攻击的方式训练或以某种方式修改现有的分类器的挑战。
简短回顾:风险,训练与测试集
首先,我们更正式地如在机器学习中被使用的一样讨论风险的传统概念。分类器的风险是它在样本的真实分布下的期望损失,即
其中 表示样本的真实分布。当然,实际上我们不知道实际数据之下的分布,所以我们通过考虑一个从 中独立同分布地抽取的有限样本集合来近似这个值,
下面我们讨论经验风险
如上面提到的,传统的训练机器学习算法的过程是找到在某个记为 的训练集上最小化经验风险(或者可能是这个目标的某个正则化版本)
当然,一旦参数 已经基于训练集 被选择了,这个数据集就不再能给予我们得到的分类器的风险的无偏估计了,所以通常有另一个数据集 (也包含从真正下面的分布 中独立同分布地采样的点),我们使用 作为代理来估计真正的风险 。
对抗风险
作为对传统风险的代替,我们也来探讨对抗风险。它很像传统风险,除了不是去经受在每个样本点上的损失 ,我们经受在样本点附近某个区域内最差情况的损失,即
其中为了完整性,我们显式地允许扰动域 可取决于样本点自己,使用前面小节的例子,为了确保扰动遵守最终的图像边界这一点很有必要,但它也可能潜在地编码大量关于每个图像允许什么样的扰动的语义信息。
自然地,也有对抗风险的经验类比,看起来就和我们在前面的小节中讨论的很像
这个值实质上度量了如果我们能够对抗地在样本的允许集 内操作数据集中每个输入,分类器的最差情况经验误差。
为什么我们更喜欢去用对抗风险而非传统风险?如果我们真的在对抗环境中运行,其中敌人可以在有分类器的全部知识的情况下操作输入,那么对抗风险会提供分类器期望性能的更准确的估计。这在实践中似乎不太可能,但一些分类任务(尤其是那些关于计算机安全的)例如垃圾邮件分类,恶意软件检测,网络侵入检测等等的确是对抗的,其中攻击者有直接的动机去欺骗分类器。或者即使我们不希望环境总是对抗的,一些机器学习的应用看似风险足够高以致我们想要了解分类器的“最差”性能,即使这是不太可能发生的事。这种逻辑是对自动驾驶等领域的对抗样本感兴趣的基础,例如,有人在研究如何操作停止标志来故意欺骗分类器。
然而,还有一个合理的理由是我们可能比起传统的经验风险更喜欢经验对抗风险,即使我们最终想要最小化传统风险。这一点的原因是从真实的下面的分布中独立同分布地抽取样本是非常困难的。相反,我们使用的任何收集数据的过程都是接近真实的下面的分布的一个经验尝试,也许会忽略某些维度,尤其是那些对人类来说“显而易见”的维度。即使在前面的图像分类示例中,这一点也很明显。最近有许多这样的说法,在图像分类方面,算法已经“超越了人类的表现”,使用像我们前面看到示例的分类器。但是,如上面的例子所示,算法与人类的表现相去甚远,如果它们甚至不能识别出一张从任何视觉定义来看与原始图像完全相同的图像,实际上属于同一类。有些人可能会争辩说,这些情况“不应该计算在内”,因为它们是专门设计来欺骗相关算法的,并且可能与实际看到的图像不相对应,但更简单的扰动,如平移和旋转也可以作为对抗样本。
最基本的问题是当有声称 ML 系统达到“人类水平”的性能时,它们真正的意思是“就是在这个实验中使用的采样机制下生成的数据上的人类水平”。但是人类不会仅在一个采样分布上表现良好,人类对环境变化的适应能力惊人。因此,当人们被告知机器学习算法“超越了人类的表现”(特别是当它们与相关的深度学习算法“像人脑一样工作”的说法结合在一起时),通常会导致一个隐含的假设,即算法也将具有类似的适应力。但它们并没有,深度学习算法难以置信地脆弱,对抗样本以非常明显和直观的方式揭示了这一事实。换句话说,难道我们不能至少同意,对于那些就像相信第一个图像是猪一样相信第二张图像是飞机的系统,为“人类层面”和“像人脑一样工作”这样的言论降降温?
f, ax = plt.subplots(1, 2, figsize=(10, 5))
ax[0].imshow(pig_tensor[0].detach().numpy().transpose(1, 2, 0))
ax[1].imshow((pig_tensor + delta)[0].detach().numpy().transpose(1, 2, 0))
训练对抗鲁棒的分类器
带着这个动机,我们现在探讨训练对对抗攻击鲁棒的分类器(或者相当于,最小化经验对抗风险的分类器)的任务。与传统训练的情况相似,可以被写为优化问题
我们将其称为对抗学习的最小最大或鲁棒优化形式化表述,在本教程的过程中我们会多次回到这个形式化表述。
与传统训练一样,我们在实践中解决这个优化问题的方法是通过 上的随机梯度下降。即,我们会重复地选择一个小批量 ,并根据它的梯度更新
但是,考虑到内部函数自己包含着一个最大化问题,我们现在如何计算内部项的梯度呢?幸运的是,实践中答案非常简单,由 Danskin 定理给出。为了我们的讨论,这个定理指出,包含最大化项的内部函数的梯度简单地由在这个最大值处求值的函数的梯度给出。换句话说,令 表示内部最优化问题的最优
我们需要的梯度简单地由下式给出
(其中在右侧,我们把 视作一个固定值,即我们不用担心它对 的依赖)。这看起来可能是“显然的”,但实际上是很微妙的一点,要证明这一点成立并不容易(毕竟得到的 的值依赖于 ,所以为什么在求梯度时可以把它视作独立于 并不清晰)。我们在这里不会去证明 Danskin 定理,只是简单地指出这个性质会是我们的工作变得更简单。
给定这个框架,在寻找对抗样本和训练鲁棒的分类器的过程之间有一个很好的相互作用。具体来说,在经验对抗风险上梯度下降的过程看起来和下面的很像
- 对于每个 ,解内部最大化问题(即,计算一个对抗样本)
- 计算经验对抗风险的梯度,并更新
换句话说,我们反复地计算对抗样本,然后不仅基于原始数据点,而且基于这些对抗样本来更新分类器。这个过程已在深度学习文献中被称为“对抗训练”,并且(如果合适,稍后会详细介绍)这是我们用于训练对抗鲁棒模型的最有效的经验方法之一,尽管有一些注意事项值得一提。
首先,我们应该注意到实际上,我们从来没有对真正的经验对抗风险进行梯度下降,正是因为我们通常不能最优地解决内部最大化问题。具体来说,如果像我们上面那样做通过梯度下降去做,内部最大化是一个非凸优化问题,当我们使用例如梯度下降这样的技术时,我们最多只能够找到局部最优。举例来说,由于只有当内部最大化问题被精确地解出时 Danskin 定理才在理论上使用,这似乎会给这样的方法带来问题。然而,在实践中,通常的情况是,如果内部优化问题解决得“足够好”,那么这个策略就会表现很好。不过,这在很大程度上取决于内部优化问题实际上的解决程度,如果只使用一个糟糕的近似策略来解决内部最大化问题,那么一个稍微更详尽的内部优化策略将被证明是一个很有效的攻击。这就是为什么当前最好的策略是尽可能明确地解出这个内部优化问题(甚至是近似地),使得后续的策略简单地“优化得更好”训练好的鲁棒性尽可能困难(尽管不是不可能)。
其次,虽然在理论上,我们可以将最坏情况的扰动作为计算梯度的点,但在实践中,这可能会导致训练过程的振荡,通常最好将多个具有不同随机初始化的扰动以及可能基于未扰动初始点的梯度结合起来使用。
最后,我们应该注意到,一些鲁棒训练方法(特别是基于内部最大化问题上界的方法)实际上不需要反复寻找对抗样本点,然后再进行优化;相反,这些方法会产生一个内部最大化的闭式界限,可以通过非迭代方法解决。我们将在后续章节中更详细地讨论这些方法。
最后的注释
在继续之前,我们想再补充一下关于对抗鲁棒性的鲁棒优化形式化表述的价值的评论。重要的是要强调,每个对抗攻击和防御方法都分别是近似解决内部最大化和/或外部最小化问题的方法。即使是没有明确表达这一点的论文,也在尝试解决这些问题(尽管可能存在一些潜在的差异,例如直接考虑不同的损失函数,如 0/1 损失而不是交叉熵损失)。
在我们看来(这段话应该被理解为 Zico 和 Aleksander 的观点),该领域的一个显著挑战是许多论文从使用的方法的角度提出一种攻击或防御,而不是解决的问题(即优化问题)。这就是为什么我们会得到许多不同名称的策略,它们都考虑了上述优化的某些小变体,例如在 项中考虑不同的范数界限,在解决内部最大化问题时使用不同的优化程序,或使用看似非常奢侈的技术来抵御攻击,这些技术通常似乎与优化形式化表述根本没有明显的关系。虽然有可能证明这种方法比我们已知的最佳策略更有效,但更为启发式的攻击和防御策略的历史并不乐观。
考虑到所有这些,本教程的下一章计划应该是清晰的。在第二章中,我们首先会稍微偏离主题,展示所有这些问题在线性模型的情况下是如何运作的;也许并不令人惊讶的是,在线性情况下,我们所讨论的内部最大化问题可以被准确地解决(或非常接近上界),我们可以对这些模型在对抗设定中的性能做出非常强有力的陈述。接下来,在第三章中,我们将回到深度网络的世界,研究内部最大化问题,重点关注可以应用的三类一般方法:1)下界(即构造对抗样本),2)精确解(通过组合优化),3)上界(通常使用一些更易处理的策略)。在第四章中,我们将解决训练对抗模型的问题,这通常涉及使用下界进行对抗训练,或者使用上界进行“证实的”鲁棒训练(使用精确组合解的对抗训练尚未被证明可行)。最后,在第五章中,我们回到本章中的一些更大的问题,并讨论更多内容:在这里,我们讨论对抗鲁棒性超越典型“安全”理由的价值;相反,我们考虑在正则化、泛化和学习到的表示的意义上,对抗鲁棒性的价值。