news 2026/10/17 15:19:05

PGD对抗训练实战:从对抗样本到鲁棒模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PGD对抗训练实战:从对抗样本到鲁棒模型

1. 从对抗样本说起:为什么模型需要“抗揍”能力

第一次接触对抗攻击这个概念,是在做一个图像分类的小项目时。当时模型在测试集上准确率能到 95% 以上,我信心满满地拿几张自己拍的照片去测,结果只是给图片加了一层肉眼几乎看不出来的噪声,模型就把“熊猫”认成了“长臂猿”,置信度还高达 99%。那一刻我才意识到,深度学习模型看着聪明,其实相当“脆弱”。

这个现象背后的核心问题就是对抗样本(Adversarial Examples)。简单说,就是在原始输入上叠加一个精心设计的、人眼难以察觉的微小扰动,就能让模型产生完全错误的判断。这类扰动不是随机噪声,而是沿着模型梯度方向“故意”构造出来的,所以破坏力极强。

对抗攻击大致分两类:一类是白盒攻击,攻击者知道模型的结构和参数,能直接算梯度;另一类是黑盒攻击,攻击者只能查询模型的输入输出。白盒攻击里最经典、也最常被拿来当基准的方法,就是FGSM(Fast Gradient Sign Method)和它的一系列改进版本。而对抗防御这边,最出名、被引用最多的方法之一,就是PGD(Projected Gradient Descent,投影梯度下降)。

需要先澄清一个容易混淆的点:PGD 这个名字在对抗领域其实有两个身份。一个是攻击方法——PGD 攻击,被认为是“最强的一阶攻击”之一;另一个是防御方法——用 PGD 攻击生成的对抗样本去做对抗训练(Adversarial Training),从而得到对攻击更鲁棒的模型。标题里说的“Towards deep learning models resistant to adversarial attacks”,讲的正是后者:用 PGD 做对抗训练,让模型变得抗揍。

这篇内容我打算把 PGD 这条线从头到尾捋一遍:它到底怎么工作的、为什么有效、对抗训练怎么落地、实操中有哪些坑。适合已经了解基础深度学习、想深入对抗鲁棒性这个方向的朋友,也适合想直接上手复现的工程同学。下面进入正题。

2. 对抗攻击与防御的整体设计思路拆解

2.1 对抗样本的本质:在损失曲面上“爬坡”

要理解 PGD,先得理解对抗样本是怎么造出来的。模型训练的目标是最小化损失函数 $L(x, y; \theta)$,其中 $x$ 是输入,$y$ 是标签,$\theta$ 是参数。正常训练是固定 $x$,调 $\theta$ 让损失变小。

对抗攻击反过来:固定 $\theta$,调 $x$ 让损失变大。也就是说,攻击者想在输入空间里找到一个 $x'$,使得模型在 $x'$ 上的损失尽可能大,从而判断出错。但 $x'$ 不能离原图 $x$ 太远,否则人眼就看出来了,所以通常约束在一个 $\epsilon$ 球内:

$$|x' - x|_\infty \leq \epsilon$$

这里的 $\epsilon$ 就是扰动预算,$\infty$ 范数表示每个像素的改动幅度都不超过 $\epsilon$。用 $\infty$ 范数而不是 $L_2$,是因为它更贴近“每个像素最多改多少”的直觉,也更容易控制视觉不可察觉性。

FGSM 的做法非常粗暴:只沿着梯度的符号方向走一步,步长就是 $\epsilon$:

$$x' = x + \epsilon \cdot \text{sign}(\nabla_x L(x, y; \theta))$$

一步到位,快是快,但不够精细。PGD 则把它变成了迭代版:走很多小步,每步都朝梯度方向爬,走完还要“投影”回 $\epsilon$ 球内。

2.2 为什么选 PGD 作为对抗训练的主力

对抗训练的思路很直接:既然模型怕对抗样本,那我训练的时候就把对抗样本喂给它,让它见多识广,自然就抗揍了。问题在于,用哪种攻击生成的样本去训练效果最好?

早期大家用 FGSM 做对抗训练,发现模型确实能防住 FGSM,但换个攻击方法(比如迭代攻击)就又崩了。这个现象叫梯度掩蔽(Gradient Masking)——模型只是让 FGSM 这种单步攻击失效,并没有真正学到鲁棒特征。

PGD 攻击因为是迭代的、每步都重新算梯度,能更充分地探索 $\epsilon$ 球内的“最坏情况”,所以用它生成的对抗样本质量更高、更难防。用 PGD 做对抗训练,模型学到的是对一整类一阶攻击都鲁棒的特征,泛化性明显更好。这也是为什么 PGD 被公认为对抗训练的“标配”攻击方法。

提示:PGD 攻击的强度不是越猛越好。训练时如果 $\epsilon$ 设得过大,模型会为了迁就极端样本而牺牲正常样本的准确率,出现“鲁棒性-准确率”的权衡。这个平衡点需要根据任务调。

2.3 方案选型的几个关键考量

实际做对抗训练时,有几个设计决策绕不开:

  • 扰动预算 $\epsilon$ 怎么定:图像任务常用 8/255(约 0.031),这是经验值,能在“人眼几乎看不出”和“攻击有效”之间取得平衡。MNIST 这种简单数据可以用 0.3 左右。
  • 迭代步数怎么选:PGD 攻击一般 7 到 40 步。步数越多攻击越强,但训练开销线性增长。实践中 7 步或 10 步是常见折中。
  • 步长怎么设:通常取 $\epsilon / 4$ 或 $\epsilon / 10$,保证每步不会跨过整个球。
  • 是否随机初始化:PGD 攻击一般从 $\epsilon$ 球内的随机点出发,而不是从原图出发,这样能探索更多方向,避免陷入局部最优。

这些参数不是拍脑袋定的,背后都有“让攻击尽可能强、同时训练成本可控”的逻辑。下面我会逐个展开。

3. 核心细节解析与实操要点

3.1 PGD 攻击的数学形式与逐步拆解

PGD 攻击的完整迭代公式是这样的:

$$x^{t+1} = \Pi_{x + \mathcal{S}}\left(x^t + \alpha \cdot \text{sign}(\nabla_x L(x^t, y; \theta))\right)$$

拆开看每个符号:

  • $x^t$:第 $t$ 步的对抗样本,$x^0$ 是随机初始化的起点。
  • $\alpha$:步长,每步走多远。
  • $\text{sign}(\nabla_x L)$:损失对输入的梯度符号,指示“往哪个方向改能让损失变大”。
  • $\Pi_{x + \mathcal{S}}$:投影操作,把更新后的点拉回以 $x$ 为中心、半径为 $\epsilon$ 的 $\infty$ 球内。
  • $\mathcal{S}$:允许的扰动集合,$\infty$ 范数下就是 $[-\epsilon, \epsilon]^d$。

投影这一步是 PGD 的灵魂。没有它,迭代几步后 $x'$ 就离原图太远,视觉上完全变了。投影保证了每一步都在“合法”范围内,同时又能持续朝损失增大的方向推进。

用生活化的类比:想象你在一个圆形围栏里想爬到最高的坡顶。FGSM 是直接朝最陡方向冲一大步,可能冲出围栏;PGD 是小步小步走,每走一步如果碰到围栏就贴着围栏继续走,最终能更接近围栏内的最高点。

3.2 对抗训练的目标函数:min-max 博弈

对抗训练本质上是一个min-max 优化问题:

$$\min_\theta \mathbb{E}{(x,y)}\left[\max{|\delta|_\infty \leq \epsilon} L(x + \delta, y; \theta)\right]$$

内层的 $\max$ 就是攻击者:给定当前模型,找一个让损失最大的扰动 $\delta$。外层的 $\min$ 是防御者:调整参数 $\theta$,让这个“最坏情况下的损失”最小。

这个博弈的直觉是:我不追求在“平均情况”下表现好,而是追求在“最坏情况”下也不差。PGD 就是用来近似求解内层 $\max$ 的——因为精确求解内层优化是 NP 难的,只能用迭代方法近似。

注意:内层用 PGD 近似,意味着对抗训练得到的鲁棒性是“针对 PGD 这类一阶攻击”的,不是对所有攻击都绝对鲁棒。这是理论上的局限,实操中要心里有数。

3.3 实操中的关键参数与选择依据

下面这张表是我在多个项目里总结的参数经验,供参考:

参数含义常用取值选择依据
$\epsilon$扰动预算8/255(图像)、0.3(MNIST)视觉不可察觉与攻击有效的平衡
$\alpha$步长$\epsilon/4$ 或 $\epsilon/10$太小收敛慢,太大易震荡
$T$迭代步数7~40步数越多攻击越强,成本越高
初始点起点随机扰动避免局部最优,增强攻击
训练轮数epochs比正常训练多 2~5 倍对抗训练收敛更慢

关于步长,有个细节值得说:如果 $\alpha$ 取 $\epsilon$,那就退化成 FGSM 了(一步走满)。取 $\epsilon/4$ 是常见做法,保证 4 步左右能覆盖整个球。步数 $T$ 和步长 $\alpha$ 要配合,一般 $T \cdot \alpha \geq \epsilon$,否则走不满整个球,攻击不够充分。

3.4 一个容易踩的坑:标签泄漏与梯度掩蔽

做对抗训练时,有个隐蔽的坑叫标签泄漏(Label Leaking)。如果你在生成对抗样本时用了真实标签 $y$,而训练时又用同一个 $y$ 算损失,模型可能会“记住”这个标签,导致鲁棒性虚高。正确做法是生成对抗样本和训练用同一套标签,但要确保评估时用的是独立的攻击。

另一个坑是梯度掩蔽。有些模型看起来能防住 PGD,其实是因为它的梯度变得很小或很乱,导致攻击算不出有效方向。判断方法很简单:换一个不依赖梯度的攻击(比如基于迁移的攻击、基于分数的攻击)再测一遍,如果还是鲁棒,那才是真鲁棒。

4. 完整实操流程与核心环节实现

4.1 环境准备与依赖

我用的是 PyTorch,版本 1.13 以上都行。核心依赖就几个:

pip install torch torchvision numpy matplotlib

不需要额外的对抗库,PGD 攻击和对抗训练都可以手写,几十行代码的事。自己写的好处是能完全掌控每个细节,调试起来也方便。

4.2 PGD 攻击的代码实现

先实现攻击部分。核心就是一个循环,每步算梯度、更新、投影:

import torch import torch.nn as nn def pgd_attack(model, x, y, epsilon, alpha, num_steps, device): """ PGD 攻击实现 model: 目标模型 x: 原始输入 [B, C, H, W] y: 真实标签 [B] epsilon: 扰动预算 alpha: 步长 num_steps: 迭代步数 """ model.eval() # 随机初始化起点,在 epsilon 球内 x_adv = x + torch.empty_like(x).uniform_(-epsilon, epsilon) x_adv = torch.clamp(x_adv, 0, 1) # 保证像素值合法 x_adv = x_adv.detach() for _ in range(num_steps): x_adv.requires_grad = True output = model(x_adv) loss = nn.CrossEntropyLoss()(output, y) grad = torch.autograd.grad(loss, x_adv)[0] # 沿梯度符号方向走一步 x_adv = x_adv.detach() + alpha * grad.sign() # 投影回 epsilon 球 delta = torch.clamp(x_adv - x, -epsilon, epsilon) x_adv = torch.clamp(x + delta, 0, 1).detach() return x_adv

几个关键点解释一下:

  • 随机初始化:x + uniform(-epsilon, epsilon)让每次攻击的起点不同,避免固定起点导致的局部最优。
  • torch.clamp(x_adv, 0, 1):图像像素必须在 [0,1] 之间,投影时不能越界。
  • delta = clamp(x_adv - x, -epsilon, epsilon):这是 $\infty$ 范数投影的核心,把每个像素的偏移限制在 $[-\epsilon, \epsilon]$。
  • detach():切断计算图,避免梯度累积到原图上。

4.3 对抗训练的完整训练循环

有了攻击,对抗训练就是在每个 batch 上先生成对抗样本,再用它更新模型参数:

def adversarial_train(model, train_loader, optimizer, epochs, epsilon=8/255, alpha=2/255, num_steps=7, device='cuda'): model.train() criterion = nn.CrossEntropyLoss() for epoch in range(epochs): total_loss, correct, total = 0, 0, 0 for x, y in train_loader: x, y = x.to(device), y.to(device) # 第一步:生成对抗样本 x_adv = pgd_attack(model, x, y, epsilon, alpha, num_steps, device) # 第二步:用对抗样本训练 model.train() optimizer.zero_grad() output = model(x_adv) loss = criterion(output, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) correct += (output.argmax(1) == y).sum().item() total += x.size(0) print(f"Epoch {epoch+1}: loss={total_loss/total:.4f}, " f"acc={correct/total:.4f}")

这里有个细节:pgd_attack里调用了model.eval(),但训练时模型应该是train()模式(因为 BatchNorm、Dropout 等层的行为不同)。所以生成完对抗样本后要切回model.train()。这个切换很容易忘,忘了会导致训练不稳定。

4.4 参数计算与选择过程

假设做 CIFAR-10 任务,图像是 32x32,像素范围 [0,1]。选 $\epsilon = 8/255 \approx 0.031$,意味着每个像素最多改 0.031,视觉上几乎看不出。

步长 $\alpha$ 取 $\epsilon/4 = 2/255 \approx 0.0078$。步数 $T=7$,那么总移动距离 $7 \times 0.0078 = 0.055 > 0.031$,足够覆盖整个球。如果 $T \cdot \alpha < \epsilon$,攻击走不满,强度不够。

训练轮数方面,正常训练 CIFAR-10 大概 100 轮收敛,对抗训练要 150 到 200 轮。因为每个 batch 多了一次 PGD 前向反向,训练时间大概是正常训练的 7 到 10 倍(取决于步数)。这是对抗训练最大的成本,要有心理准备。

4.5 评估:怎么判断模型真的鲁棒了

评估不能只看“PGD 攻击下的准确率”,因为可能被梯度掩蔽骗了。我的做法是:

  1. 正常准确率:干净样本上的准确率,看鲁棒性有没有牺牲太多。
  2. PGD 攻击准确率:用不同 $\epsilon$、不同步数测,看曲线是否平滑。
  3. 迁移攻击准确率:用一个独立训练的模型生成对抗样本,测目标模型,看是否还鲁棒。
  4. 不同攻击对比:FGSM、PGD、CW 都测一遍,看是否一致。

如果 PGD 下准确率高,但迁移攻击下崩了,那大概率是梯度掩蔽,不是真鲁棒。

5. 常见问题与排查技巧实录

5.1 训练不收敛或准确率暴跌

这是对抗训练最常见的问题。原因通常有几个:

  • $\epsilon$ 太大:模型为了迁就极端样本,牺牲了正常样本。解决方法是先从小 $\epsilon$ 开始,比如 4/255,稳定后再加到 8/255。
  • 学习率太高:对抗训练本身就不稳定,学习率要比正常训练低。正常用 0.1 的话,对抗训练可以降到 0.01 或 0.05。
  • BatchNorm 统计量问题:对抗样本的分布和干净样本不同,BatchNorm 的 running mean/var 会被污染。可以试试用干净样本更新 BatchNorm 统计量,或者用 GroupNorm 替代。

我踩过最深的坑就是 BatchNorm。当时模型训练 loss 一直震荡,后来发现是对抗样本把 BatchNorm 的统计量带偏了。改成用干净样本更新统计量后,训练立刻稳定了。

5.2 鲁棒性虚高:梯度掩蔽的识别

前面提过梯度掩蔽,这里给个具体的排查方法。用两个模型 A 和 B,A 是你要评估的模型,B 是另一个独立训练的模型。用 B 生成对抗样本,去攻击 A。如果 A 在 B 的对抗样本下准确率很低,但在自己生成的对抗样本下准确率很高,那 A 就是梯度掩蔽了。

另一个信号是:攻击的 loss 不随步数增加而上升。正常情况 PGD 步数越多,攻击越强,loss 越高。如果 loss 卡住不动,说明梯度信息没用上,模型在“装死”。

5.3 训练太慢的优化技巧

对抗训练慢是硬伤,但有几个优化方向:

  • 减少 PGD 步数:7 步通常够用,实在慢可以降到 5 步,鲁棒性损失不大。
  • 混合训练:不是每个 batch 都用对抗样本,可以一半干净一半对抗,速度能快近一倍。
  • Free 对抗训练:复用前向传播的梯度来生成对抗样本,避免重复前向。这个方法能把训练成本降到接近正常训练,但实现复杂一些。
  • YOPO 类方法:只对第一层做攻击,减少计算量,适合大模型。

5.4 常见问题速查表

问题现象可能原因排查方向解决方法
训练 loss 震荡学习率过高 / BatchNorm 污染看 loss 曲线降学习率 / 干净样本更新 BN
鲁棒性虚高梯度掩蔽迁移攻击测试换攻击方法验证
正常准确率暴跌$\epsilon$ 过大对比不同 $\epsilon$减小 $\epsilon$
训练极慢PGD 步数多计时各环节减步数 / Free 训练
攻击 loss 不升梯度消失看梯度范数检查模型结构

提示:对抗训练不是万能的。它提升的是对特定 $\epsilon$ 范围内一阶攻击的鲁棒性,对更大扰动、其他类型攻击(如物理攻击、语义攻击)效果有限。别指望一个 PGD 对抗训练解决所有安全问题。

6. 一些延伸思考与个人经验

PGD 对抗训练这套东西,我从最早跑通 demo 到真正用在项目里,中间隔了大半年。最大的体会是:理论上的优雅和工程上的可用之间,隔着无数细节。

比如论文里说“用 PGD 生成对抗样本训练”,但没告诉你 BatchNorm 会出问题、学习率要调、训练轮数要翻倍。这些都得自己踩坑。我现在的习惯是,任何对抗训练的实验,先跑一个小规模(比如 CIFAR-10 子集)验证流程,确认能收敛、鲁棒性有提升,再上全量数据。

另一个体会是评估的重要性。对抗鲁棒性这个领域,虚高的结果太多了。很多论文报的鲁棒准确率,换个攻击方法就掉一大截。所以我现在评估模型,至少用三种攻击交叉验证,宁可报低一点,也不报虚高。

最后分享一个实用技巧:如果你只是想快速验证一个模型是否鲁棒,不用完整跑对抗训练。可以先用 PGD 攻击测一下现有模型的鲁棒准确率,如果干净准确率 95%、PGD 下只有 5%,那说明模型完全没防御能力,需要从头做对抗训练。这个快速测试能帮你判断值不值得投入训练成本。

对抗鲁棒性这个方向还在快速发展,PGD 只是其中一块基石。理解了它的原理和实操,再看其他方法(比如 TRADES、MART、以及各种 certified defense)会容易很多。希望这些内容能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:19:02

机器学习三要素与模型评估:构建可靠模型的底层逻辑

1. 从三要素到模型评估&#xff1a;先建立整体认知框架如果你刚接触机器学习&#xff0c;大概率会被各种术语砸得晕头转向。神经网络、正则化、交叉验证、AUC、梯度下降……每个词单独看都认识&#xff0c;放在一起却不知道从哪下手。我在带新人入门时经常说一句话&#xff1a;…

作者头像 李华
网站建设 2026/10/17 15:16:46

从零实现一个Android简单计算器:布局、逻辑与状态管理详解

不少刚接触Android开发的朋友&#xff0c;想找个能顺手练完整个基础链路的项目&#xff0c;第一反应多半是“做个计算器”。它没有复杂的网络请求&#xff0c;也不需要设计数据库表&#xff0c;但要真正把一个能用的计算器做出来&#xff0c;你需要把界面布局、按钮事件、状态切…

作者头像 李华
网站建设 2026/10/17 15:16:38

Unity如何自动找到并调用Start/Update?内部机制解析

有人问了一个看起来特基础、仔细想又说不清的问题&#xff1a;Unity引擎到底是怎么知道某个MonoBehaviour子类里有Start方法&#xff0c;然后在正确时机自动调用的&#xff1f;刚入门的同学觉得这事挺玄学——脚本里写了个Start&#xff0c;引擎就跟约定好了一样&#xff0c;在…

作者头像 李华
网站建设 2026/10/17 15:16:16

Unity安卓串口通信实战:JNI桥接、设备扫描与避坑指南

简介&#xff1a;Unity安卓串口收发代码是一份面向Unity开发者的原生串口通信实现&#xff0c;解决Unity应用与Arduino、传感器等硬件设备的数据交互问题。资源包内含基于JNI的Java层串口封装、C/C本地接口以及用于编译so库的Android.mk构建脚本&#xff0c;可直接接入Unity C#…

作者头像 李华
网站建设 2026/10/17 15:15:30

IFRS S1可持续披露准则落地指南:从条款拆解到数据治理

简介&#xff1a;这份PDF是安永对国际可持续准则理事会&#xff08;ISSB&#xff09;发布的《国际财务报告可持续披露准则第1号——可持续相关财务信息披露一般要求》&#xff08;IFRS S1&#xff09;的逐项深入解读&#xff0c;面向财务报告编制人员、ESG信息披露负责人、审计…

作者头像 李华
网站建设 2026/10/17 15:11:44

单卡跑70B大模型:airllm逐层加载推理实战与调优

1. 单卡跑大模型这件事&#xff0c;到底卡在哪第一次看到 airllm 这个项目的时候&#xff0c;我正在一台只有单张消费级显卡的机器上折腾一个 70B 级别的模型。显存不够&#xff0c;量化到 4bit 依然爆&#xff0c;换更小的模型又达不到想要的效果。airllm 给出的思路很直接&am…

作者头像 李华