- 深度学习
- 人工智能
- 机器学习
- 分布式训练
【免费下载链接】mxnet
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more
导读
本文基于 Apache MXNet 仓库中的 example/adversary/README.md 及其配套的 adversary_generation.ipynb 示例,系统讲解"对抗样本"(Adversarial Examples)这一机器学习安全核心概念,以及如何利用 MXNet Gluon 的自动求导能力,用Fast Gradient Sign Method(FGSM,快速梯度符号法)攻击一个训练良好的 CNN 模型。读完本文,你将掌握:对抗样本的数学原理、利用mx.autograd求取损失对输入的梯度、用梯度符号对输入做微小扰动使模型误判,以及如何用可视化验证攻击效果——并能在本地基于 MXNet 完整复现这一流程。
一、什么是对抗样本:让训练良好的模型"自信地犯错"
example/adversary/README.md 开篇即给出对抗样本的精确定义:
Adversarial examples are samples where the input has been manipulated to confuse a model (i.e. confident in an incorrect prediction) but where the correct answer still appears obvious to a human.
即:对抗样本是一类经过人为篡改的输入。篡改的目的一是让模型"自信地给出错误预测"(confident in an incorrect prediction),二是这种篡改对肉眼几乎不可察觉——正确答案对人类依然显而易见。例如,一张手写数字图片被加上了人眼几乎看不出差异的微小噪声,模型却从"1"错判成"3"。
这个概念最早由 Goodfellow、Shlens 与 Szegedy 在论文"Explaining and Harnessing Adversarial Examples"(arXiv:1412.6572,2014)中系统阐述,README 与 Notebook 均将其标注为参考文献 [1]。论文的核心洞见在于:深度学习模型的决策边界在高维空间中呈现"线性"的脆弱性,沿着损失函数上升最快的方向(即梯度方向)对输入做微小扰动,就足以把样本推过决策边界。
该 README 同时指出,本示例所用的对抗样本生成方法正是利用损失函数对输入的梯度(the gradient of the loss with respect to the input)来构造对抗样本——这就是下文要实现的 FGSM 方法。
二、方法原理:Fast Gradient Sign Method(FGSM)
本示例采用的攻击算法是 Goodfellow 等人在上述论文中提出的 FGSM。给定输入x、真实标签y与损失函数L,对抗样本的构造公式为:
x_adv = x + ε · sign(∇_x L(x, y))其中:
∇_x L(x, y)是损失对输入(而非网络权重)的梯度,它指明在当前输入点上,往哪个方向修改像素会让损失增长最快;sign(·)对梯度逐元素取符号(+1 / 0 / -1),将扰动方向离散化,得到一个"最经济"的扰动方向;ε(epsilon)是扰动强度系数,控制噪声的可见程度与攻击强度,本示例中取0.15。
在 Notebook 的 "Perturbation" 一节中,代码正是这条公式的直接落地:
data_perturbated = data + 0.15 * mx.np.sign(data.grad)对比论文原文的公式:x_adv = x + ε · sign(∇_x J(x, y)),二者完全一致——区别只在于 MXNet 中用data.grad保存了已由反向传播计算好的梯度∇_x L。
这一方法之所以有效,是因为它抓住了"模型在输入空间中存在线性盲区"这一本质:即使在 MNIST 这样简单的任务上,训练精度已达 0.97 的 CNN,仅加一步ε=0.15的符号扰动,验证批次的准确率就从0.96875 骤降到 0.40625(见 Notebook 的运行输出)。这正是"用梯度方向做微小扰动"的威力。
三、环境准备与数据加载
3.1 运行环境
Notebook 需要以下依赖:
import mxnet as mx import numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm from mxnet import gluon其中matplotlib仅用于对抗样本的可视化展示;核心计算完全依赖 MXNet。上下文与批大小按如下方式自动选择 CPU/GPU:
ctx = mx.gpu() if mx.device.num_gpus() else mx.cpu() batch_size = 128- 若机器有可用 GPU(
mx.device.num_gpus()返回非 0),则使用mx.gpu(),否则回退到mx.cpu(); - 批大小固定为
128。
3.2 加载 MNIST 数据集
MNIST 手写数字数据集由 Gluon 内置的gluon.data.vision.MNIST提供,源码位于 python/mxnet/gluon/data/vision/datasets.py。每个样本是形状为(28, 28, 1)的 3D 图像,train=True加载训练集(60,000 张),train=False加载测试集(10,000 张)。首次使用时数据会自动下载并缓存到$MXNET_HOME/datasets/mnist目录。
数据预处理与装载代码如下:
transform = lambda x, y: (x.transpose((2, 0, 1)).astype('float32') / 255., y) train_dataset = gluon.data.vision.MNIST(train=True).transform(transform) test_dataset = gluon.data.vision.MNIST(train=False).transform(transform) train_data = gluon.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=5) test_data = gluon.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)这里的transform做了两件事:
x.transpose((2, 0, 1)):把图像通道维从最后一维挪到最前((28, 28, 1)→(1, 28, 28)),符合 MXNet GluonNCHW的卷积输入约定;x.astype('float32') / 255.:将像素值从[0, 255]归一化到[0, 1],使梯度计算与扰动幅度在同一量纲下进行。
DataLoader负责按批产出数据:训练集打乱顺序(shuffle=True)并使用 5 个 worker 并行取数,测试集不打乱(shuffle=False),便于对照同批样本在扰动前后的预测变化。
四、构建并训练一个简单的 CNN 分类器
4.1 网络结构
Notebook 使用gluon.nn.HybridSequential构建了一个用于 MNIST 数字识别的经典小型 CNN(LeNet 风格):
net = gluon.nn.HybridSequential() with net.name_scope(): net.add( gluon.nn.Conv2D(kernel_size=5, channels=20, activation='tanh'), gluon.nn.MaxPool2D(pool_size=2, strides=2), gluon.nn.Conv2D(kernel_size=5, channels=50, activation='tanh'), gluon.nn.MaxPool2D(pool_size=2, strides=2), gluon.nn.Flatten(), gluon.nn.Dense(500, activation='tanh'), gluon.nn.Dense(10) )网络结构一览:
| 层 | 类型 | 关键参数 | 作用 |
|---|---|---|---|
| 1 | Conv2D | kernel_size=5, channels=20, activation='tanh' | 提取 20 个低级特征 |
| 2 | MaxPool2D | pool_size=2, strides=2 | 2×2 下采样 |
| 3 | Conv2D | kernel_size=5, channels=50, activation='tanh' | 提取 50 个高级特征 |
| 4 | MaxPool2D | pool_size=2, strides=2 | 2×2 下采样 |
| 5 | Flatten | — | 展平为向量送入全连接层 |
| 6 | Dense | 500, activation='tanh' | 隐层 |
| 7 | Dense | 10 | 输出 10 类(数字 0-9)logits |
注意这里卷积与全连接层均使用tanh激活,最后一层Dense(10)不带激活函数,直接输出 10 个类别的 logits。选择HybridSequential而非Sequential,是为了调用net.hybridize()将网络编译为符号图(symbolic graph),提升训练与推理速度。
4.2 初始化、损失函数与优化器
net.initialize(mx.initializer.Uniform(), ctx=ctx) net.hybridize() loss = gluon.loss.SoftmaxCELoss() trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1, 'momentum': 0.95})net.initialize(mx.initializer.Uniform(), ctx=ctx):用均匀分布初始化所有参数并绑定到ctx指定的设备;net.hybridize():启用混合编程,将网络转为符号执行路径;gluon.loss.SoftmaxCELoss():Softmax 交叉熵损失。其定义在 python/mxnet/gluon/loss.py(SoftmaxCELoss = SoftmaxCrossEntropyLoss):当from_logits=False(默认)时先对 logits 做npx.log_softmax,再通过-npx.pick(pred, label)取出真实标签对应的负对数概率,最后按batch_axis=0求批次均值。这正是后续对抗梯度∇_x L的"源头损失";gluon.Trainer(net.collect_params(), 'sgd', {...}):使用带动量的 SGD,学习率 0.1、动量 0.95。
4.3 训练循环
epoch = 3 for e in range(epoch): train_loss = 0. acc = mx.gluon.metric.Accuracy() for i, (data, label) in enumerate(train_data): data = data.as_in_context(ctx) label = label.as_in_context(ctx) with mx.autograd.record(): output = net(data) l = loss(output, label) l.backward() trainer.update(data.shape[0]) train_loss += l.mean().item() acc.update(label, output) print("Train Accuracy: %.2f\t Train Loss: %.5f" % (acc.get()[1], train_loss/(i+1)))训练循环的三个要点:
mx.autograd.record()记录前向计算图。其实现见 python/mxnet/autograd.py:进入该上下文后,MXNet 会捕获后续前向运算以构建计算图,为backward提供梯度路径,同时train_mode=True使 Dropout/BatchNorm 等层处于训练模式;l.backward()反向传播。默认只对"被标记需要梯度"的变量(即通过attach_grad标记的 NDArray 与网络参数)计算梯度并写入其.grad缓冲区——本示例中,网络参数由Trainer自动管理梯度,而输入data的梯度需要我们显式标记(见第五节);trainer.update(data.shape[0]):按批次样本数缩放梯度后更新权重,符合 SGD 动量更新的惯例。
Notebook 训练 3 个 epoch 后的运行输出为:
Train Accuracy: 0.92 Train Loss: 0.32142 Train Accuracy: 0.97 Train Loss: 0.16773 Train Accuracy: 0.97 Train Loss: 0.14660即训练 3 个 epoch 后,该 CNN 在训练集上的准确率达到0.97,是一个"训练良好"的模型——这正是后续攻击实验的前提:我们要证明即便是高精度的模型也容易被对抗样本欺骗。
五、核心环节:计算损失对输入的梯度并施加扰动
5.1 先测基线:扰动前的验证集准确率
攻击前,先从测试集中取一个批次(128 张图),计算该批次在干净输入下的准确率作为基线:
# Get a batch from the testing set for data, label in test_data: data = data.as_in_context(ctx) label = label.as_in_context(ctx) break # Attach gradient to it to get the gradient of the loss with respect to the input data.attach_grad() with mx.autograd.record(): output = net(data) l = loss(output, label) l.backward() acc = mx.gluon.metric.Accuracy() acc.update(label, output) print("Validation batch accuracy {}".format(acc.get()[1]))其中最关键的一行是data.attach_grad()。其实现位于 python/mxnet/ndarray/ndarray.py:它为该 NDArray 分配一个初始化为零的梯度缓冲区,并调用 C 层接口MXAutogradMarkVariables将data标记为"需要计算梯度的变量"。默认grad_req='write',即每次backward都覆写该缓冲区。此后在mx.autograd.record()上下文中执行l.backward()时,梯度就会沿着计算图传播到data.grad,得到∇_x L。
运行结果是:
Validation batch accuracy 0.96875即 128 张干净测试图片中约 96.9% 被正确分类——基线准确率很高,为后续攻击效果对比提供了清晰的参照。
5.2 施加 FGSM 扰动:验证准确率骤降
现在执行对抗攻击——沿损失对输入的梯度符号方向修改输入:
data_perturbated = data + 0.15 * mx.np.sign(data.grad) output = net(data_perturbated) acc = mx.gluon.metric.Accuracy() acc.update(label, output) print("Validation batch accuracy after perturbation {}".format(acc.get()[1]))这条语句即 FGSM 的完整实现:data.grad是我们上一步计算好的∇_x L,mx.np.sign逐元素取符号得到方向,0.15是扰动强度ε,相加后得到对抗样本data_perturbated。随后用同一个net对扰动后的输入重新推理,并再次统计准确率:
Validation batch accuracy after perturbation 0.40625同一个批次,准确率从 96.9% 暴跌至 40.6%——仅凭"梯度符号 + 0.15 幅度"的微小扰动,就让超过一半原本预测正确的图片被判错。这正是对抗样本威胁的直观体现,也验证了 Goodfellow 论文中"模型对输入的线性敏感性"论断。
5.3 攻击流程的技术要点小结
| 步骤 | 代码 | 底层机制 |
|---|---|---|
| 标记输入需要梯度 | data.attach_grad() | MXAutogradMarkVariables分配梯度缓冲区(python/mxnet/ndarray/ndarray.py) |
| 记录前向图并反传 | with mx.autograd.record(): ... l.backward() | 记录计算图并反向传播,梯度写入data.grad(python/mxnet/autograd.py) |
| 构造对抗样本 | data + 0.15 * mx.np.sign(data.grad) | FGSM:x + ε·sign(∇_x L) |
| 重新推理评估 | net(data_perturbated) | 与训练共用同一网络权重 |
六、可视化验证:肉眼难以察觉、模型却已误判
最后,Notebook 随机抽取批次中的一张对抗样本进行可视化,展示"人眼看来仍是原数字、模型却已错判"的效果:
from random import randint idx = randint(0, batch_size - 1) plt.imshow(data_perturbated[idx, :].asnumpy().reshape(28, 28), cmap=cm.Greys_r) print("true label: %d" % label.asnumpy()[idx]) print("predicted: %d" % np.argmax(output.asnumpy(), axis=1)[idx])data_perturbated[idx, :]取对抗批次中的第idx张图,.asnumpy()转回 NumPy 数组后reshape(28, 28)恢复为二维灰度图;plt.imshow(..., cmap=cm.Greys_r)用灰度 colormap 显示;- 同时打印真实标签
label[idx]与模型预测np.argmax(output, axis=1)[idx]。
Notebook 的一次运行输出为:
true label: 1 predicted: 3即人眼看到的是清晰的数字"1",模型却预测为"3"。由于ε=0.15的扰动是在[0,1]归一化空间中叠加的,单像素变化至多 0.15,视觉上几乎不可分辨,但足以跨越模型的决策边界。
七、完整代码串联与实验结论
将上述各节整合,一个完整的 MXNet Gluon FGSM 对抗样本实验包括五大步骤:
- 加载 MNIST:
gluon.data.vision.MNIST+DataLoader,transform完成通道重排与归一化; - 构建 CNN:
HybridSequential+ Conv2D/MaxPool2D/Dense,tanh激活,hybridize()加速; - 训练模型:
SoftmaxCELoss+ SGD(lr=0.1, momentum=0.95),mx.autograd.record()驱动前向/反向,训练 3 个 epoch 后准确率达 0.97; - 生成对抗样本:取验证批次 →
data.attach_grad()→record内前向 +backward()得到data.grad→data + 0.15 * mx.np.sign(data.grad); - 评估与可视化:对比扰动前后准确率(0.96875 → 0.40625),随机抽图展示"真实标签 1、预测 3"的误判。
实验结论:使用 MXNet Gluon 的自动求导机制,仅需一个批次的梯度信息即可高效构造对抗样本;一个在 MNIST 上训练良好(准确率 0.97)的 CNN 在ε=0.15的 FGSM 攻击下,验证批准确率从 96.9% 降至 40.6%,充分说明深度学习模型对基于梯度的输入扰动高度脆弱——这也正是对抗训练、输入防御等方法研究的出发点。
完整可运行代码见仓库中的 adversary_generation.ipynb,其原理依据为 Goodfellow et al.,"Explaining and Harnessing Adversarial Examples"(arXiv:1412.6572)。若要进一步研究 MXNet 中梯度与自动求导的底层实现,可深入阅读 python/mxnet/autograd.py、python/mxnet/ndarray/ndarray.py 与 python/mxnet/gluon/loss.py 中的对应实现。
- 深度学习
- 人工智能
- 机器学习
- 分布式训练
【免费下载链接】mxnet
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more
相关推荐
对抗样本生成终极指南:从FGSM到PGD的完整教程
对抗样本生成终极指南:从FGSM到PGD的完整教程 在当今AI安全领域,对抗样本攻击已成为机器学习模型面临的最严峻威胁之一。通过精心设计的微小扰动,攻击者能够误
人工智能机器学习模型安全红蓝对抗CleverHans对抗攻击完全手册:从FGSM到CW攻击的深度解析
CleverHans对抗攻击完全手册:从FGSM到CW攻击的深度解析 在人工智能安全领域, 对抗攻击 已成为评估机器学习模型鲁棒性的关键测试方法。CleverH
人工智能模型安全AI评测深度学习Kotlin Explorer 项目教程
Kotlin Explorer 项目教程 1. 项目的目录结构及介绍 Kotlin Explorer 项目的目录结构如下: kotlin explorer/ ├
人工智能模型安全AI评测深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考