使用 MXNet Gluon 生成对抗样本:基于梯度的 FGSM 攻击完整实战指南
2026/9/21 1:37:59 网站建设 项目流程
  • 深度学习
  • 人工智能
  • 机器学习
  • 分布式训练

【免费下载链接】mxnet

Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more

项目地址:https://gitcode.com/gh_mirrors/mx/mxnet
点击查看免费下载

导读

本文基于 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做了两件事:

  1. x.transpose((2, 0, 1)):把图像通道维从最后一维挪到最前((28, 28, 1)(1, 28, 28)),符合 MXNet GluonNCHW的卷积输入约定;
  2. 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) )

网络结构一览:

类型关键参数作用
1Conv2Dkernel_size=5, channels=20, activation='tanh'提取 20 个低级特征
2MaxPool2Dpool_size=2, strides=22×2 下采样
3Conv2Dkernel_size=5, channels=50, activation='tanh'提取 50 个高级特征
4MaxPool2Dpool_size=2, strides=22×2 下采样
5Flatten展平为向量送入全连接层
6Dense500, activation='tanh'隐层
7Dense10输出 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)))

训练循环的三个要点:

  1. mx.autograd.record()记录前向计算图。其实现见 python/mxnet/autograd.py:进入该上下文后,MXNet 会捕获后续前向运算以构建计算图,为backward提供梯度路径,同时train_mode=True使 Dropout/BatchNorm 等层处于训练模式;
  2. l.backward()反向传播。默认只对"被标记需要梯度"的变量(即通过attach_grad标记的 NDArray 与网络参数)计算梯度并写入其.grad缓冲区——本示例中,网络参数由Trainer自动管理梯度,而输入data的梯度需要我们显式标记(见第五节);
  3. 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 层接口MXAutogradMarkVariablesdata标记为"需要计算梯度的变量"。默认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 Lmx.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 对抗样本实验包括五大步骤:

  1. 加载 MNISTgluon.data.vision.MNIST+DataLoadertransform完成通道重排与归一化;
  2. 构建 CNNHybridSequential+ Conv2D/MaxPool2D/Dense,tanh激活,hybridize()加速;
  3. 训练模型SoftmaxCELoss+ SGD(lr=0.1, momentum=0.95),mx.autograd.record()驱动前向/反向,训练 3 个 epoch 后准确率达 0.97;
  4. 生成对抗样本:取验证批次 →data.attach_grad()record内前向 +backward()得到data.graddata + 0.15 * mx.np.sign(data.grad)
  5. 评估与可视化:对比扰动前后准确率(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

项目地址:https://gitcode.com/gh_mirrors/mx/mxnet
点击查看免费下载

相关推荐

上一篇:3行代码生成高清图像:DiT模型从理论到实战全指南
下一篇:Rancher容器安全基线:符合PCI DSS的容器管理平台配置

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询