作为 Write for DOnations 计划的一部分,作者选择了 Dev Color 来接受捐赠。
用于动物分类的神经网络会被愚弄吗? 愚弄动物分类器可能没有什么后果,但如果我们的面部验证器被愚弄了怎么办? 还是我们的自动驾驶汽车原型的软件? 幸运的是,大量工程师和研究人员站在我们的移动设备或汽车上的原型计算机视觉模型和生产质量模型之间。 尽管如此,这些风险仍具有重大影响,并且对于作为机器学习从业者来说很重要。
在本教程中,您将尝试“愚弄”或欺骗动物分类器。 在学习本教程时,您将使用计算机视觉库 OpenCV 和深度学习库 PyTorch。 您将在 对抗机器学习 的相关领域涵盖以下主题:
- 创建一个以 为目标的对抗样本 。 例如,选择一张狗的图像。 选择一个 target 类,比如一只猫。 你的目标是让神经网络相信图中的狗是一只猫。
- 创建 对抗性防御 。 简而言之,保护你的神经网络免受这些棘手的图像的影响,而不知道其中的诀窍是什么。
在本教程结束时,您将拥有一个欺骗神经网络的工具,并了解如何防御欺骗。
先决条件
要完成本教程,您将需要以下内容:
- 具有至少 1GB RAM 的 Python 3 本地开发环境。 您可以按照如何为Python 3安装和设置本地编程环境来配置您需要的一切。
- 建议您查看构建基于情感的狗过滤器; 本教程没有明确使用,但介绍了分类的概念。
第 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 上进行训练,该数据集包含 120 万张训练图像和 1000 个类别。 这些类别包括车辆、地点,最重要的是,动物。 在这一步中,您将运行这些预训练的神经网络之一,称为 ResNet18。 我们将在 ImageNet 上训练的 ResNet18 称为“动物分类器”。
什么是 ResNet18? ResNet18 是由 MSR(He 等人)开发的称为 残差神经网络 的神经网络家族中最小的神经网络。 简而言之,他发现神经网络(表示为函数 f
,输入 x
,输出 f(x)
)在“残差连接”[X169X ]。 这种残差连接在最先进的神经网络中被大量使用,即使在今天也是如此。 例如,FBNetV2、FBNetV3。
使用以下命令下载这张狗的图像:
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 样板:
step_2_pretrained.py
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
函数之前直接添加:
step_2_pretrained.py
. . . def get_idx_to_label(): with open("assets/imagenet_idx_to_label.json") as f: return json.load(f) . . .
创建一个图像转换函数,以确保您的输入图像首先具有正确的尺寸,其次是正确规范化。 在最后一个之后直接添加以下函数:
step_2_pretrained.py
. . . 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 文档 中有更准确的描述。
给定图像,添加一个实用程序来预测动物类别。 此方法使用之前的两个实用程序来执行动物分类:
step_2_pretrained.py
. . . 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'
。
接下来,在最后一个函数之后,添加一个实用程序来加载图像:
step_2_pretrained.py
. . . 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
函数,以加载图像并对图像中的动物进行分类:
step_2_pretrained.py
def main(): x = load_image() print(f'Prediction: {predict(x)}')
仔细检查您的文件是否与我们在 GitHub 上的 step_2_pretrained.py 中的最后一步 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
函数:
step_3_adversarial.py
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
函数之前:
step_3_adversarial.py
. . . 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 图像的方法。 在最后一个函数之后添加:
step_3_adversarial.py
. . . 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 图像对象。
然后,使用这些实用程序创建具有以下内容的对抗性示例:
step_3_adversarial.py
. . . 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
,应用扰动,将对抗性示例保存到磁盘,并对对抗性示例运行预测:
step_3_adversarial.py
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)}')
您完成的文件应与 GitHub 上的 step_3_adversarial.py 匹配。 保存文件,退出编辑器,然后使用以下命令启动脚本:
python step_3_adversarial.py assets/dog.jpg
你会看到这个输出:
OutputOld prediction: Pembroke, Pembroke Welsh corgi New prediction: goldfish, Carassius auratus
您现在已经创建了一个对抗性示例:诱使神经网络认为柯基犬是金鱼。 在下一步中,您将实际创建您在此处使用的扰动 r
。
第 4 步 — 了解对抗性示例
有关分类的入门知识,请参阅 “如何构建基于情感的狗过滤器”。
退后一步,回想一下您的分类模型为每个类别输出一个概率。 在推理过程中,模型预测概率最高的类别。 在训练期间,在给定数据 x
的情况下,更新模型参数 t
以最大化正确类别 y
的概率。
argmax_y P(y|x,t)
但是,要生成对抗性示例,您现在需要修改目标。 您现在的目标不是查找类,而是查找新图像 x
。 参加除正确课程之外的任何课程。 让我们称这个新类为 w
。 你的新目标是最大化错误分类的概率。
argmax_x P(w|x)
请注意,上述表达式中缺少神经网络权重 t
。 这是因为您现在假设了对手的角色:其他人已经训练并部署了一个模型。 您只能创建对抗性输入,不能修改已部署的模型。 要生成对抗性示例 x
,您可以运行“训练”,但不是更新神经网络权重,而是使用新目标更新输入图像。
提醒一下,对于本教程,您假设对抗性示例是 x
的仿射变换。 换句话说,您的对抗性示例对某些 r
采用 x + r
的形式。 在下一步中,您将编写一个脚本来生成这个 r
。
第 5 步 - 创建对抗性示例
在这一步中,您将学习一个扰动 r
,从而将您的柯基犬误分类为金鱼。 创建一个名为 step_5_perturb.py
的新文件:
nano step_5_perturb.py
导入必要的包并声明一个 main
函数:
step_5_perturb.py
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
函数之前,定义两个常量:
step_5_perturb.py
. . . TARGET_LABEL = 1 EPSILON = 10 / 255. . . .
第一个常量 TARGET_LABEL
是将柯基误分类为的类。 在这种情况下,索引 1
对应于“goldfish”。 第二个常数 EPSILON
是每个图像值允许的最大扰动量。 引入了这个限制,以便在不知不觉中改变图像。
在您的两个常数之后,添加一个辅助函数来定义神经网络和扰动参数 r
:
step_5_perturb.py
. . . 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
:
step_5_perturb.py
. . . 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
:
step_5_perturb.py
. . . criterion = nn.CrossEntropyLoss() optimizer = optim.SGD([r], lr=0.1, momentum=0.1) . . .
紧接着,为您的参数 r
添加主训练循环:
step_5_perturb.py
. . . 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
:
step_5_perturb.py
def main(): . . . for i in range(30): . . . . . . np.save('outputs/adversarial_r.npy', r.data.numpy())
直接下面,还是在main
函数中,保存扰动图像:
step_5_perturb.py
. . . os.makedirs('outputs', exist_ok=True) adversarial = get_adversarial_example(x, r)
最后,对原始图像和对抗样本运行预测:
step_5_perturb.py
print(f'Old prediction: {predict(x)}') print(f'New prediction: {predict(x + r)}')
仔细检查您的脚本是否与 GitHub 上的 step_5_perturb.py 匹配。 保存、退出并运行脚本:
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 的有关 Attacking Machine Learning with Adversarial Examples、Testing Robustness against Unseen Adversaries 和 Robust Adversarial Inputs 的相关博文。
更多机器学习内容和教程,可以访问我们的【X67X】机器学习专题页面【X98X】。