作者选择Dev Color接受捐赠,作为Write for DOnations计划的一部分。
用于动物分类的神经网络会被愚弄吗?愚弄动物分类器可能不会有什么后果,但如果我们的面部验证器可以被愚弄怎么办?或者我们的自动驾驶汽车原型的软件?幸运的是,在我们的移动设备或汽车上,大量工程师和研究人员站在原型计算机视觉模型和生产质量模型之间。尽管如此,这些风险仍然具有重大意义,作为机器学习从业者考虑是很重要的。
在本教程中,您将尝试“愚弄”或欺骗动物分类器。在学习本教程时,您将使用OpenCV
一个计算机视觉库和PyTorch
一个深度学习库。您将在对抗性机器学习的相关领域中涵盖以下主题:
- 创建一个有针对性的对抗示例。选择一张图片,比如一只狗。选择一个目标类,比如一只猫。你的目标是让神经网络相信图中的狗是一只猫。
- 创建对抗性防御。简而言之,在不知道技巧是什么的情况下,保护您的神经网络免受这些棘手的图像的影响。
在本教程结束时,您将拥有一个欺骗神经网络的工具,并了解如何防御欺骗。
先决条件
要完成本教程,您将需要以下内容:
- 具有至少 1GB RAM 的 Python 3 本地开发环境。您可以按照如何为 Python 3 安装和设置本地编程环境来配置您需要的一切。
- 建议您查看Build an Emotion-Based Dog Filter;本教程没有明确使用,但介绍了分类的概念。
第 1 步 – 创建您的项目并安装依赖项
让我们为此项目创建一个工作区并安装您需要的依赖项。您将调用您的工作区AdversarialML
:
- mkdir ~/AdversarialML
导航到AdversarialML
目录:
- cd ~/AdversarialML
创建一个目录来保存您的所有资产:
- mkdir ~/AdversarialML/assets
然后为项目创建一个新的虚拟环境:
- python3 -m venv adversarialml
激活您的环境:
- source adversarialml/bin/activate
然后安装PyTorch,这是您将在本教程中使用的 Python 深度学习框架。
在 macOS 上,使用以下命令安装 Pytorch:
- python -m pip install torch==1.2.0 torchvision==0.4.0
在 Linux 和 Windows 上,对仅 CPU 构建使用以下命令:
- pip install torch==1.2.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
- pip install torchvision
现在为OpenCV
和安装预打包的二进制文件numpy
,它们分别是用于计算机视觉和线性代数的库。OpenCV
提供诸如图像旋转之类的实用程序,并numpy
提供诸如矩阵求逆之类的线性代数实用程序:
- python -m pip install opencv-python==3.4.3.18 numpy==1.14.5
在 Linux 发行版上,您需要安装libSM.so
:
- sudo apt-get install libsm6 libxext6 libxrender-dev
安装依赖项后,让我们运行一个名为 ResNet18 的动物分类器,我们接下来将对其进行描述。
第 2 步 – 运行预训练的动物分类器
该torchvision
库是 PyTorch 的官方计算机视觉库,包含常用计算机视觉神经网络的预训练版本。这些神经网络都在ImageNet 2012上训练,这是一个包含 1000 个类别的 120 万张训练图像的数据集。这些类包括车辆、地点,最重要的是,动物。在这一步中,您将运行这些预训练的神经网络之一,称为 ResNet18。我们将在 ImageNet 上训练的 ResNet18 称为“动物分类器”。
什么是 ResNet18?ResNet18 是由MSR(He 等人)开发的称为残差神经网络的神经网络家族中最小的神经网络。简而言之,他发现神经网络(表示为具有输入和输出的函数)在使用“残差连接”时会表现得更好。这种残差连接在最先进的神经网络中被大量使用,即使在今天也是如此。例如,FBNetV2、FBNetV3。f
x
f(x)
x + f(x)
使用以下命令下载此狗的图像:
- wget -O assets/dog.jpg https://assets.digitalocean.com/articles/trick_neural_network/step2a.png
然后,下载一个 JSON 文件以将神经网络输出转换为人类可读的类名:
- wget -O assets/imagenet_idx_to_label.json https://raw.githubusercontent.com/do-community/tricking-neural-networks/master/utils/imagenet_idx_to_label.json
接下来,创建一个脚本来在狗图像上运行您的预训练模型。创建一个名为 的新文件 step_2_pretrained.py
:
- nano step_2_pretrained.py
首先,通过导入必要的包并声明一个main
函数来添加 Python 样板:
from PIL import Image
import json
import torchvision.models as models
import torchvision.transforms as transforms
import torch
import sys
def main():
pass
if __name__ == '__main__':
main()
接下来,加载从神经网络输出到人类可读类名的映射。在导入语句之后和main
函数之前直接添加:
. . .
def get_idx_to_label():
with open("assets/imagenet_idx_to_label.json") as f:
return json.load(f)
. . .
创建一个图像转换函数,以确保您的输入图像首先具有正确的尺寸,其次被正确归一化。在最后一个之后直接添加以下函数:
. . .
def get_image_transform():
transform = transforms.Compose([
transforms.Resize(224),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
return transform
. . .
在 中get_image_transform
,您定义了许多不同的变换以应用于传递给神经网络的图像:
transforms.Resize(224)
:将图像较小的一侧调整为 224。例如,如果您的图像为 448 x 672,则此操作会将图像下采样到 224 x 336。transforms.CenterCrop(224)
:从图像的中心进行裁剪,尺寸为 224 x 224。transforms.ToTensor()
:将图像转换为 PyTorch 张量。所有 PyTorch 模型都需要 PyTorch 张量作为输入。transforms.Normalize(mean=..., std=...)
:通过减去平均值,然后除以标准偏差来标准化您的输入。torchvision
文档中对此进行了更准确的描述。
添加一个实用程序来预测给定图像的动物类别。此方法使用先前的实用程序来执行动物分类:
. . .
def predict(image):
model = models.resnet18(pretrained=True)
model.eval()
out = model(image)
_, pred = torch.max(out, 1)
idx_to_label = get_idx_to_label()
cls = idx_to_label[str(int(pred))]
return cls
. . .
这里的predict
函数使用预训练的神经网络对提供的图像进行分类:
models.resnet18(pretrained=True)
:加载一个名为 ResNet18 的预训练神经网络。model.eval()
:就地修改模型以在“评估”模式下运行。唯一的其他模式是“训练”模式,但不需要训练模式,因为在本教程中您没有训练模型(即更新模型的参数)。out = model(image)
:在提供的转换图像上运行神经网络。_, pred = torch.max(out, 1)
:神经网络为每个可能的类别输出一个概率。这一步计算概率最高的类的索引。例如,如果out = [0.4, 0.1, 0.2]
,则pred = 0
。idx_to_label = get_idx_to_label()
: 获取从类索引到人类可读类名的映射。例如,映射可以是{0: cat, 1: dog, 2: fish}
。cls = idx_to_label[str(int(pred))]
:将预测的类索引转换为类名。最后两个要点中提供的示例将产生cls = idx_to_label[0] = 'cat'
.
接下来,在最后一个函数之后,添加一个实用程序来加载图像:
. . .
def load_image():
assert len(sys.argv) > 1, 'Need to pass path to image'
image = Image.open(sys.argv[1])
transform = get_image_transform()
image = transform(image)[None]
return image
. . .
这将从脚本的第一个参数中提供的路径加载图像。transform(image)[None]
应用前几行中定义的图像变换序列。
最后,main
使用以下内容填充您的函数,以加载您的图像并对图像中的动物进行分类:
def main():
x = load_image()
print(f'Prediction: {predict(x)}')
仔细检查您的文件是否与我们step_2_pretrained.py
在 GitHub 上的最后一步 2 脚本匹配。保存并退出脚本,然后运行动物分类器:
- python step_2_pretrained.py assets/dog.jpg
这将产生以下输出,显示您的动物分类器按预期工作:
OutputPrediction: Pembroke, Pembroke Welsh corgi
到此结束使用您的预训练模型运行推理。接下来,您将看到一个对抗性示例,它通过使用图像中难以察觉的差异来欺骗神经网络。
第 3 步 – 尝试对抗性示例
现在,您将合成一个对抗性示例,并在该示例上测试神经网络。在本教程中,您将构建表单的对抗性示例x + r
,其中x
是原始图像,r
是一些“扰动”。您最终将r
自己创建扰动,但在此步骤中,您将下载我们事先为您创建的扰动。首先下载扰动r
:
- wget -O assets/adversarial_r.npy https://github.com/do-community/tricking-neural-networks/blob/master/outputs/adversarial_r.npy?raw=true
现在将图像与扰动合成。创建一个名为 的新文件step_3_adversarial.py
:
- nano step_3_adversarial.py
在此文件中,您将执行以下三步过程,以生成对抗性示例:
- 变换图像
- 应用扰动
r
- 逆变换扰动图像
在第 3 步结束时,您将获得一张对抗性图像。首先,导入必要的包并声明一个main
函数:
from PIL import Image
import torchvision.transforms as transforms
import torch
import numpy as np
import os
import sys
from step_2_pretrained import get_idx_to_label, get_image_transform, predict, load_image
def main():
pass
if __name__ == '__main__':
main()
接下来,创建一个“图像转换”来反转之前的图像转换。将其放在导入之后,main
函数之前:
. . .
def get_inverse_transform():
return transforms.Normalize(
mean=[-0.485/0.229, -0.456/0.224, -0.406/0.255], # INVERSE normalize images, according to https://pytorch.org/docs/stable/torchvision/models.html
std=[1/0.229, 1/0.224, 1/0.255])
. . .
和以前一样,该transforms.Normalize
操作减去均值并除以标准差(即,对于原始图像x
,y = transforms.Normalize(mean=u, std=o) = (x - u) / o
)。你做一些代数并定义一个新的操作来反转这个归一化函数 ( transforms.Normalize(mean=-u/o, std=1/o) = (y - -u/o) / 1/o = (y + u/o) o = yo + u = x
)。
作为逆变换的一部分,添加一个将 PyTorch 张量变换回 PIL 图像的方法。在最后一个函数之后添加:
. . .
def tensor_to_image(tensor):
x = tensor.data.numpy().transpose(1, 2, 0) * 255.
x = np.clip(x, 0, 255)
return Image.fromarray(x.astype(np.uint8))
. . .
tensor.data.numpy()
将 PyTorch 张量转换为 NumPy 数组。.transpose(1, 2, 0)
重新排列(channels, width, height)
成(height, width, channels)
。这个 NumPy 数组大约在范围内(0, 1)
。最后,乘以 255 以确保图像现在在 范围内(0, 255)
。np.clip
确保图像中的所有值都在 之间(0, 255)
。x.astype(np.uint8)
确保所有图像值都是整数。最后,Image.fromarray(...)
从 NumPy 数组创建一个 PIL 图像对象。
然后,使用这些实用程序创建具有以下内容的对抗性示例:
. . .
def get_adversarial_example(x, r):
y = x + r
y = get_inverse_transform()(y[0])
image = tensor_to_image(y)
return image
. . .
此函数生成本节开头所述的对抗性示例:
y = x + r
. 将您的扰动r
添加到原始图像中x
。get_inverse_transform
:获取并应用您之前定义的几行反向图像转换。tensor_to_image
:最后,将 PyTorch 张量转换回图像对象。
最后,修改您的main
函数以加载图像,加载对抗性扰动r
,应用扰动,将对抗性示例保存到磁盘,并对对抗性示例运行预测:
def main():
x = load_image()
r = torch.Tensor(np.load('assets/adversarial_r.npy'))
# save perturbed image
os.makedirs('outputs', exist_ok=True)
adversarial = get_adversarial_example(x, r)
adversarial.save('outputs/adversarial.png')
# check prediction is new class
print(f'Old prediction: {predict(x)}')
print(f'New prediction: {predict(x + r)}')
您完成的文件应该step_3_adversarial.py
在 GitHub 上匹配。保存文件,退出编辑器,然后使用以下命令启动脚本:
- python step_3_adversarial.py assets/dog.jpg
你会看到这个输出:
OutputOld prediction: Pembroke, Pembroke Welsh corgi
New prediction: goldfish, Carassius auratus
您现在已经创建了一个对抗性示例:诱使神经网络认为柯基犬是金鱼。在下一步中,您将实际创建r
此处使用的扰动。
第 4 步 – 理解对抗性示例
有关分类的入门知识,请参阅“如何构建基于情感的狗过滤器”。
退一步,回想一下您的分类模型为每个类别输出一个概率。在推理过程中,模型预测概率最高的类别。在训练期间,您更新模型参数t
以最大化正确类的概率y
,给定您的数据x
。
argmax_y P(y|x,t)
但是,要生成对抗性示例,您现在需要修改目标。您现在的目标不是查找类,而是查找新图像x
。选择正确的课程以外的任何课程。让我们称这个新类w
。你的新目标是最大化错误类别的概率。
argmax_x P(w|x)
请注意,t
上述表达式中缺少神经网络权重。这是因为您现在承担了对手的角色:其他人已经训练并部署了一个模型。您只能创建对抗性输入,而不能修改已部署的模型。要生成对抗性示例x
,您可以运行“训练”,但不是更新神经网络权重,而是使用新目标更新输入图像。
提醒一下,对于本教程,您假设对抗样本是 的仿射变换x
。换句话说,您的对抗性示例采用了x + r
some的形式r
。在下一步中,您将编写一个脚本来生成此r
.
第 5 步 – 创建对抗性示例
在这一步中,您将学习扰动r
,因此您的柯基犬被错误分类为金鱼。创建一个名为 的新文件 step_5_perturb.py
:
- nano step_5_perturb.py
导入必要的包并声明一个main
函数:
from torch.autograd import Variable
import torchvision.models as models
import torch.nn as nn
import torch.optim as optim
import numpy as np
import torch
import os
from step_2_pretrained import get_idx_to_label, get_image_transform, predict, load_image
from step_3_adversarial import get_adversarial_example
def main():
pass
if __name__ == '__main__':
main()
直接在您的导入之后和main
函数之前,定义两个常量:
. . .
TARGET_LABEL = 1
EPSILON = 10 / 255.
. . .
第一个常量TARGET_LABEL
是将柯基错误分类为的类。在这种情况下,索引1
对应于“金鱼”。第二个常数EPSILON
是每个图像值允许的最大扰动量。引入此限制是为了使图像不知不觉地发生变化。
在您的两个常量之后,添加一个辅助函数来定义神经网络和扰动参数r
:
. . .
def get_model():
net = models.resnet18(pretrained=True).eval()
r = nn.Parameter(data=torch.zeros(1, 3, 224, 224), requires_grad=True)
return net, r
. . .
model.resnet18(pretrained=True)
像以前一样加载一个名为 ResNet18 的预训练神经网络。也像以前一样,您使用 将模型设置为评估模式.eval
。nn.Parameter(...)
定义了一个新的扰动r
,即输入图像的大小。输入图像的大小也是(1, 3, 224, 224)
。该requires_grad=True
关键字参数可以确保您可以更新这个扰动r
在以后的线路,在这个文件中。
接下来,开始修改您的main
函数。首先加载模型net
,加载输入x
,并定义标签label
:
. . .
def main():
print(f'Target class: {get_idx_to_label()[str(TARGET_LABEL)]}')
net, r = get_model()
x = load_image()
labels = Variable(torch.Tensor([TARGET_LABEL])).long()
. . .
接下来,在您的main
函数中定义标准和优化器。前者告诉 PyTorch 目标是什么——也就是要最小化什么损失。后者告诉 PyTorch 如何训练你的参数r
:
. . .
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD([r], lr=0.1, momentum=0.1)
. . .
紧随其后,为您的参数添加主训练循环r
:
. . .
for i in range(30):
r.data.clamp_(-EPSILON, EPSILON)
optimizer.zero_grad()
outputs = net(x + r)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
_, pred = torch.max(outputs, 1)
if i % 5 == 0:
print(f'Loss: {loss.item():.2f} / Class: {get_idx_to_label()[str(int(pred))]}')
. . .
在此训练循环的每次迭代中,您:
r.data.clamp_(...)
:保证参数r
小,在EPSILON
0以内。optimizer.zero_grad()
:清除您在前一次迭代中计算的任何梯度。model(x + r)
:对修改后的图像运行推理x + r
。- 计算
loss
. - 计算梯度
loss.backward
。 - 采取梯度下降步骤
optimizer.step
。 - 计算预测
pred
。 - 最后,报告损失和预测类别
print(...)
。
接下来,保存最终的扰动r
:
def main():
. . .
for i in range(30):
. . .
. . .
np.save('outputs/adversarial_r.npy', r.data.numpy())
直接下面,还在main
函数中,保存扰动图像:
. . .
os.makedirs('outputs', exist_ok=True)
adversarial = get_adversarial_example(x, r)
最后,对原始图像和对抗样本进行预测:
print(f'Old prediction: {predict(x)}')
print(f'New prediction: {predict(x + r)}')
step_5_perturb.py
在 GitHub 上仔细检查您的脚本是否匹配。保存、退出并运行脚本:
- python step_5_perturb.py assets/dog.jpg
您的脚本将输出以下内容。
OutputTarget class: goldfish, Carassius auratus
Loss: 17.03 / Class: Pembroke, Pembroke Welsh corgi
Loss: 8.19 / Class: Pembroke, Pembroke Welsh corgi
Loss: 5.56 / Class: Pembroke, Pembroke Welsh corgi
Loss: 3.53 / Class: Pembroke, Pembroke Welsh corgi
Loss: 1.99 / Class: Pembroke, Pembroke Welsh corgi
Loss: 1.00 / Class: goldfish, Carassius auratus
Old prediction: Pembroke, Pembroke Welsh corgi
New prediction: goldfish, Carassius auratus
最后两行表示您现在已经从头开始构建了一个对抗性示例。您的神经网络现在将完全合理的柯基犬图像分类为金鱼。
您现在已经证明神经网络很容易被愚弄——而且,对对抗样本缺乏鲁棒性会产生重大后果。一个自然的下一个问题是:你如何对抗对抗样本?包括OpenAI在内的各种组织已经进行了大量研究。在下一节中,您将进行防御以阻止这个对抗性示例。
第 6 步 — 防御对抗性示例
在这一步中,您将实施对抗样本的防御。想法如下:您现在是部署到生产中的动物分类器的所有者。您不知道可能会生成哪些对抗性示例,但您可以修改图像或模型以防止攻击。
在你防守之前,你应该亲眼看看图像处理是多么难以察觉。打开以下两张图片:
assets/dog.jpg
outputs/adversarial.png
在这里,您并排显示两者。您的原始图像将具有不同的纵横比。你能说出哪个是对抗性的例子吗?
请注意,新图像看起来与原始图像相同。事实证明,左边的图像是你的对抗图像。可以肯定的是,下载图像并运行您的评估脚本:
- wget -O assets/adversarial.png https://github.com/alvinwan/fooling-neural-network/blob/master/outputs/adversarial.png?raw=true
- python step_2_pretrained.py assets/adversarial.png
这将输出金鱼类,以证明其对抗性:
OutputPrediction: goldfish, Carassius auratus
您将运行一个相当幼稚但有效的防御:通过写入有损 JPEG 格式来压缩图像。打开 Python 交互式提示:
- python
然后,将对抗性图像加载为 PNG,并将其另存为 JPEG。
- from PIL import Image
- image = Image.open('assets/adversarial.png')
- image.save('outputs/adversarial.jpg')
键入CTRL + D
以离开 Python 交互式提示。接下来,在压缩的对抗性示例上使用您的模型运行推理:
- python step_2_pretrained.py outputs/adversarial.jpg
现在将输出 corgi 类,证明您的天真防御的功效。
OutputPrediction: Pembroke, Pembroke Welsh corgi
你现在已经完成了你的第一个对抗性防御。请注意,这种防御不需要知道对抗性示例是如何生成的。这就是有效防御的原因。还有许多其他形式的防御,其中许多涉及重新训练神经网络。但是,这些再培训过程是它们自己的主题,超出了本教程的范围。至此,您的对抗性机器学习指南到此结束。
结论
要了解您在本教程中的工作的含义,请并排重温这两个图像 – 原始示例和对抗示例。
尽管这两个图像看起来与人眼相同,但第一个图像已被操纵以欺骗您的模型。两个图像都清楚地显示了柯基犬,但模型完全相信第二个模型包含金鱼。这应该与您有关,并且在您结束本教程时,请记住模型的脆弱性。只需应用一个简单的转换,您就可以骗过它。这些是真实的、似是而非的危险,即使是最前沿的研究也无法回避。机器学习安全以外的研究同样容易受到这些缺陷的影响,作为从业者,您有责任安全地应用机器学习。如需更多阅读资料,请查看以下链接:
- 来自 NeurIPS 2018 会议的对抗性机器学习教程。
- 来自 OpenAI 的有关使用对抗性示例攻击机器学习、测试针对看不见的对手的鲁棒性和稳健的对抗性输入的相关博客文章。
有关更多机器学习内容和教程,您可以访问我们的机器学习主题页面。