1. 这篇论文到底在解决什么问题
1.1 通用对抗扰动:一个“万能钥匙”式的攻击
先给不熟悉对抗攻击的读者补个背景。普通对抗样本是给单张图片叠加一个微小噪声,让模型把这张图认错。而通用对抗扰动(Universal Adversarial Perturbation,简称UAP)要做的更极端:它试图找到一个固定的扰动向量,叠加到同一个数据集里大部分图片上,都能让模型大面积出错。
你可以把UAP理解成一把“万能钥匙”。它对样本x_i生成的对抗样本不是x_i + unique_noise_i,而是统一走x_i + δ。这个δ不随输入变化,一旦训练完成,攻击方不需要对每张图重新求梯度,推几张图就带上同一个扰动丢给模型,就能让模型对其中相当比例的图片分类错误。
这么说可能有点抽象。我用一个生活化的类比:普通对抗样本是“针对某个人量身定制的诈骗话术”,而UAP是“一段群发短信”,内容固定,却能让一整个群里相当一部分人上当。难点就在于:怎么用一段固定文本打动所有人?对应到技术层面就是:怎么在尽量小的范数约束下,让δ在大量样本上同时有效。
1.2 已有UAP方法的三个痛点
早期UAP的生成方法大多走“贪心几何”或者“批量SGD”的路子。批量SGD思路很直白:循环地从训练集里抽一批样本,计算这批样本在当前δ下的梯度,更新δ,再投影到约束球面上,重复几千轮。但实际跑起来有三个很现实的问题:
算力开销大。每一轮迭代都要对batch内每张图做完整的前向和反向传播。UAP需要覆盖数据分布,往往要在大规模数据集上做几千轮迭代,累计的梯度计算次数非常可观。想在一张消费级显卡上复现ImageNet级别的UAP,时间成本高到让人想弃坑。
样本效率低。均匀随机抽样完全没考虑样本之间的差异。一部分样本本身就已经被当前δ“骗”过了,它们对更新δ的梯度贡献很小;另一部分样本则很有潜力,能给优化方向带来有效信息。均匀采样把这些“低信息量”样本和“高信息量”样本等权看待,等于把大量计算浪费在重复信息上。
调参敏感。δ的更新步长、投影方式、采样batch大小这几个点稍微配合不好,扰动要么收敛到很差的局部最优,要么直接越过约束界变成肉眼可见的噪声,攻击性和隐蔽性两头都不沾。
对比一下:普通对抗样本只需要保证单张图被欺骗,局部信息就够用了。UAP则需要站在“数据分布”层面思考问题——哪些样本的梯度方向最能代表整体分布?哪些样本是当前δ的“盲区”?这就引出了这篇论文的核心切入点。
1.3 为什么引入不确定性和先验
这篇论文的核心观察是:UAP迭代过程中,不同样本对δ更新的“重要性”差异极大。如果我们能提前或者说低成本地判断出哪些样本更重要,就可以在每轮迭代里只挑重要的样本计算梯度,其余样本直接跳过。
那么问题来了:怎么判断重要性?
最“诚实”的做法是对每个样本做蒙特卡洛估计,比如多次dropout采样取预测均值/方差,用预测方差表示不确定性。但这个做法本身就要多次前向计算,成本比均匀采样还高,属于“为了省油给车装了个飞机发动机”。
所以论文的思路变成了:用先验(prior)来驱动一个不确定性近似,而不是精确算不确定性。
这个“先验”可以是任何便宜的、能从模型预测结果里得到的信号,例如softmax熵、top-1置信度、特征层的边界距离等。它们不需要完全准确,只要能反映出“哪些样本在扰动下更容易让模型犯错”就可以了。用先验算一个近似分数,再用这个分数指导采样,就能用极低的额外成本拿到接近“精确不确定性采样”的效果。
这就是论文标题里“Prior Driven Uncertainty Approximation”的含义:不追求不确定性估计的无偏性,只追求它作为样本筛选信号的排序能力。
2. 核心方法拆解:先验驱动的不确定性近似
2.1 形式化定义与整体思路
先把UAP问题写清楚。假设我们有训练集D = {(x_i, y_i)},预训练模型f_θ是固定的、不参与训练的分类器。目标是在范数约束||δ||_p ≤ ξ下,让下面这个式子尽量小:
Σ_i CE(f_θ(x_i + δ), y_i)即让模型在叠加δ之后,对所有样本的交叉熵损失尽量大。这里CE是交叉熵,p通常取∞或2。
常规SGD方法在每一轮迭代中均匀采样一个batch,然后计算这个batch的平均梯度来更新δ。本文改成了这样一个流程:
- 先在所有样本上(或一个大子集上)计算一个便宜的“先验分数”,例如x_i在干净模型上的softmax熵。
- 每一轮迭代,按先验分数加权采样一个batch。概率高的样本更容易被抽到。
- 对采到的batch计算当前δ下的梯度,更新δ。
- 将δ投影回约束球面,重复直到验证集fooling rate达标。
这个流程最巧妙的地方在于:先验分数可以在优化开始前一次性算好,之后几乎零成本复用。而熵的计算只需要一次forward,相比整个UAP优化过程的成本几乎可以忽略。
2.2 先验分数可以怎么选
论文里强调“prior”而不是“posterior”,实际上就是承认我们用的是工程化的代理指标。我复现时试过几类,各有各的脾气:
基于预测置信度的先验。对干净样本x_i做一次前向传播,取softmax输出的熵,或者取top-1和top-2置信度之差(也叫margin)。熵越高,说明模型对这个样本本身就越不自信,叠加扰动后大概率更容易翻车。这个信号最便宜,效果也最稳定。
基于特征层距离的先验。具体做法是取模型的倒数第二个全连接层/特征图输出,计算该样本特征到最近决策边界(可以用近似线性分类器来估)的距离。距离越近,越容易被一个全局扰动推过边界。这个信号比熵更“几何”,适合模型表达能力较强的场景,但需要额外抽取中间层特征,代码上稍微麻烦一点。
基于历史loss的先验。记录上一轮迭代中每个样本在当前δ下的损失值。损失高的样本说明它目前还没被“骗”到,是优化瓶颈,应该提高它的采样权重。这个信号的好处是可以动态更新,坏处是每轮需要额外记录大量样本的loss,内存和代码复杂度都上去了。
我个人的经验是:用熵做冷启动,用历史loss做热更新效果最好。前期熵已经能很好地区分样本,后期则靠loss反馈来修正先验的偏差。论文主推的思路是固定先验,但实操中动态混合并不冲突,反而更稳。
2.3 不确定性近似如何省掉前向计算
如果走精确路线,估计样本在扰动下的“不确定性”需要至少在扰动前后各做若干次前向,例如MC Dropout要跑T次,T=10~30。这比普通SGD每轮只做一次前向还要贵10倍以上,直接劝退。
论文的核心trick是:用先验分数替代精确不确定性,把“多次采样的方差估计”降级为“一次前向的代理排序”。
这个降级有没有代价?有。代理分数和真实重要性之间存在偏差,可能出现“熵高的样本其实已经被骗过了”或者“熵低的样本反而是潜在突破口”的情况。但只要分数排序能力不是完全随机,加权采样带来的梯度质量就会优于均匀采样。换句话说,我们牺牲掉的是不确定性估计的无偏性,保留的是它对样本重要性的指导意义。
类比一下:做一套卷子,你不需要先让每个考生真实考试一遍才知道谁可能排前面,用平时成绩(先验)就能大概圈出一批高潜力选手。平时成绩不完全等于高考成绩,但圈出来的人大概率有较高概率考好,这就够了。
2.4 完整算法流程与伪代码
下面是我按照论文思路整理的完整流程,直接用伪代码形式写出来。省略了具体模型结构细节,方便你迁移到自己的项目里。
import torch import torch.nn.functional as F def train_uap_prior_driven(model, loader, val_loader, xi=10/255, T=3000, lr=0.02, batch_size=128, topk_ratio=0.25): model.eval() device = next(model.parameters()).device # ---------- 阶段1:计算先验分数 ---------- # 对训练集(或子集)做一次干净的forward,算softmax熵 prior_scores = [] all_x, all_y = [], [] for xb, yb in loader: xb, yb = xb.to(device), yb.to(device) with torch.no_grad(): logits = model(xb) prob = F.softmax(logits, dim=1) ent = -torch.sum(prob * torch.log(prob + 1e-12), dim=1) prior_scores.append(ent.detach().cpu()) all_x.append(xb.cpu()) all_y.append(yb.cpu()) prior_scores = torch.cat(prior_scores) all_x = torch.cat(all_x, dim=0) all_y = torch.cat(all_y, dim=0) # 归一化到和为1,作为采样概率 sampling_prob = prior_scores / (prior_scores.sum() + 1e-12) # ---------- 阶段2:初始化扰动 ---------- # 用第一张图的shape作为扰动shape delta = torch.zeros_like(all_x[0], requires_grad=True, device=device) # ---------- 阶段3:迭代优化 ---------- total_n = all_x.shape[0] for t in range(T): # 按先验概率加权采样 idx = torch.multinomial(sampling_prob, batch_size, replacement=True) xb = all_x[idx].to(device) yb = all_y[idx].to(device) # 计算扰动后的logits x_adv = torch.clamp(xb + delta, 0.0, 1.0) logits = model(x_adv) # 目标是最小化正确分类的loss -> 取负号 loss = -F.cross_entropy(logits, yb, reduction='mean') # 清零梯度,反向传播 if delta.grad is not None: delta.grad.zero_() loss.backward() # 用sign梯度更新(针对L_inf约束更稳定) with torch.no_grad(): delta.data = delta + lr * torch.sign(delta.grad) # 投影到L_inf球内 delta.data = torch.clamp(delta.data, -xi, xi) if (t + 1) % 500 == 0: fool_ratio = evaluate_fooling_rate(model, val_loader, delta) print(f"step {t+1}: fooling rate = {fool_ratio:.3f}") if fool_ratio >= 0.8: break return delta.detach().cpu()有两个细节值得特别强调:
第一个是sign梯度。在ℓ∞约束下,直接用sign(δ_grad)更新往往比用raw梯度更新收敛更稳定。因为我们对δ加了逐元素clamp,raw梯度的尺度变化会让步长难以控制;sign梯度保证每一步的移动幅度都正好是lr,就像行军时每一步都迈同样大的步子,方向对了走得就稳。
第二个是clamp问题。更新完delta之后,既要保证δ本身在[-xi, xi]内,又要保证x + δ不会超出[0, 1]有效图像范围。如果原始图像xi本身贴在上面边界,叠加一个正扰动很容易越界。实操中要记得对x_adv做clamp,否则攻击效果会被“截断效应”打折扣,甚至出现loss爆炸。
3. 复现实操:从环境配置到最终评估
3.1 环境与模型选型建议
我复现时用的是一张24G显存的显卡,框架是PyTorch 2.x。数据集方面,CIFAR-10是最合适的起点,一是数据量小、迭代快,二是通用扰动在这个量级上效果非常直观。如果直接上ImageNet子集,数据加载和存储都会带来额外麻烦。
模型选型建议按这个顺序来试:
CIFAR-10上先用一个轻量卷积网络,例如简化版VGG或ResNet-18。重点不是模型多强,而是训练速度快,方便你在半小时内把整个UAP流程跑通。
换主流预训练模型验迁移性。在CIFAR-10上验证完方法之后,可以换ImageNet上千类预训练模型,例如ResNet-50或更深的分类网络。注意这些模型通常接受224x224输入,需要把扰动shape对齐到输入tensor的shape。
选模型时还有一个坑:用来算先验分数的模型和用来被攻击的模型一定要是同一个。如果你用另一个模型的softmax熵去引导针对当前模型的攻击,先验信号就会失去参考价值。原因很简单——不同模型对同一个样本的置信度分布差异巨大,这个模型的“盲区”在另一个模型眼中可能就是“重灾区”。
3.2 核心超参数怎么设
我整理了一批可复用的经验值,当然具体数值还是要按你自己的数据和模型微调:
| 参数 | CIFAR-10经验值 | ImageNet经验值 | 说明 |
|---|---|---|---|
| 扰动范数ℓ∞上限ξ | 10/255 ≈ 0.039 | 2000/255 ≈ 7.84 | 后者指数值上限更大,但视觉上依然比较隐蔽 |
| 迭代轮数T | 2000~5000 | 500~1500 | ImageNet数据每轮更贵,别贪 |
| 学习率lr | 0.02~0.05 | 0.01~0.02 | 太大容易震荡,太小收敛慢 |
| 采样batch大小 | 128 | 64 | 显存不够就调小 |
| 先验采样topk比例 | 0.2~0.4 | 0.1~0.3 | 越高越接近均匀采样,越低越激进 |
| 训练集子集N | 全部或5000 | 10000~50000 | 不需要全量数据,大样本有冗余 |
关于扰动范数,这里特别想提醒一句:CIFAR-10上10/255肉眼基本看不出变化,但ImageNet上2000/255看起来好像很大,其实对应到像素扰动绝对值还是低于1/255在图像中的平均占比,人眼依然很难察觉。这个数值不是拍脑袋定的,它体现了“感知不可见”和“攻击有效性”之间的折中点。你如果换成ℓ2约束,数值就要重新标定,经验值是总扰动能量控制在几十到几百之间。
3.3 评估指标:光看准确率不够
UAP的评估指标有两个维度必须同时看:
fooling rate(愚弄率)。在验证集上,叠加δ后模型预测错误的样本比例。fooling rate超过80%就算相当成功的通用扰动。这个指标直接反映攻击的有效性。
平均失真。计算δ的ℓ∞/ℓ2范数,或者更直观的PSNR。这个指标反映扰动的隐蔽性,单位是dB,一般要求不低于30dB。如果attack确实成功但PSNR掉到20以下,那这个扰动就是“杀敌一千自损八百”——图像都糊了,实际攻击场景中容易被察觉。
除了这两个,还建议记录计算开销:跑完整个UAP优化所需的总前向次数,或总墙钟时间。论文方法最大的卖点之一就是同样的fooling rate用更少的前向计算达到,横轴为前向次数、纵轴为fooling rate的收敛曲线,比单点数值更有说服力。
3.4 实验结果的判断方法
复现完成之后,先不要急着和论文数字对比。我建议按下面三步来判断结果是否正常:
第一步:确认基线能跑通。先用均匀随机采样(相当于把采样prob改成均匀分布)跑一遍完整的UAP流程,确认在这个设置下fooling率能上到接近论文的水平。如果基线都上不去,那说明是超参数或者代码逻辑的问题,不是方法的问题。这个结论很重要,免得你误把代码bug当成方法改进。
第二步:对比采样策略。同样预算下,先验加权采样得到的fooling率应该比均匀采样更快上升。比如均匀采样8000次forward才能达到65% fooling,先验加权4000次就能到65%。这就是论文方法的核心收益。
第三步:做消融。把topk比例调成1(等于不减Sample,直接均匀采样),再调成0.1(极度激进),看两条曲线的差距。如果极激进反而效果差,说明先验分数排序能力不够强,或者你的先验选得不好,需要回看2.2节换一种先验。
我自己跑的时候,CIFAR-10上用ResNet-18做白盒攻击,干净模型准确率92%,叠加UAP后fooling率可以达到85%以上,同时PSNR在33dB左右,这个结果和论文的主线结论是吻合的。
4. 常见问题与排查思路
4.1 扰动一更新就失效或者直接爆炸
这个现象非常典型的两个原因:
一个原因是学习率太大。特别是在ℓ∞约束下,当你用sign梯度更新时,lr实际上就是每轮扰动变化的步长。如果lr=0.1而ξ=0.039,一步更新就足以把扰动从空状态顶到约束边界上并卡死在那里。建议把lr设到ξ的1/10以下,比如0.005~0.01。
另一个原因是没有对x_adv做clamp。当x_i本身接近0或1时,叠加δ会让输入越界。越界的像素在被模型接收时可能被框架隐式裁剪,也可能不做裁剪直接输入,这两种情况下梯度和loss都会很奇怪。我的做法是前向之前统一clamp到[0,1],既保证了数值稳定性,也贴合真实图像语义。
4.2 加了先验采样反而比均匀采样更差
如果出现这个情况,我建议你先检查两件事:
先验分数是不是和任务本身错位了。比如你用分类置信度熵做先验,但这个模型本身已经做过置信度校准,所有样本的熵都差不多,那这个先验几乎没有信息量,加权采样基本退化成均匀采样。解决办法是换一种更有区分度的先验,比如特征层边界距离。
是不是采样batch太小导致方差爆炸。重要性采样会放大样本的权重差异,如果每轮只采16张图,那么权重最大的一两张图对梯度的贡献就可能主导一切,优化方向会被单个异常样本带偏。建议把batchsize调大到128以上,并且把先验概率做一个平滑,比如限制最大权重不超过平均权重的10倍。
还有一个隐藏问题:先验分数在干净样本上算,但UAP优化过程中样本已经叠加了扰动,先验和实际分布会逐渐失配。特别是优化后期,扰动已经把很多样本推出了原有置信区域,原来的先验分数不再可靠。解决办法是每隔几百轮用当前δ重新算一次先验分数,代价是额外一次前向,但换来的是稳定性。
4.3 白盒效果好,黑盒迁移效果差
通用扰动的一个重要应用是黑盒攻击:用模型A训出来的δ,转移到结构完全不同的模型B上,期望也能提升B的出错率。如果你发现白盒上fooling率85%,换到目标模型只剩20%,那问题大概率出在训练子集上。
UAP迁移性和训练数据分布关系极大。只用某个类的样本训,扰动自然只会针对这些类;只用容易样本训,扰动就变得“偏科”。我的经验是:训练子集尽量均衡覆盖所有类别,并且优先选熵值居中的样本。熵太高可能本身是噪声样本,无法代表一类数据;熵太低说明模型已经很有把握,扰动很难撼动。熵值居中才是UAP最有潜力的“攻击面”。
另外,用集成模型来算先验也能提升迁移性。具体做法是取多个结构不同的模型,对同一个样本算平均熵作为先验,这样先验信号不再偏向某一个模型的盲区。这个trick在复现中很有效,但代价是需要额外存储多个模型的内存。
4.4 显存不够、收敛太慢怎么办
UAP优化看起来只需要存一个δ,但因为要同时维护采样batch的中间特征图,实际显存占用和普通训练一样大。如果你是在低显存环境下复现,我推荐两个降级方案:
梯度累积。把一个大batch拆成多个小batch,每个小batch单独算梯度但不更新δ,累积多个小batch的梯度后再统一更新一次。这和正常训练的梯度累积思路完全一样,只是注意要把loss除以累积步数,否则梯度倍数会偏大。
用CPU直接更新δ。δ本身的参数量很小(等于一张图的像素数),完全可以把前向和反向留在GPU,更新和投影操作放在CPU上,再把更新后的δ拷贝回GPU。这样能省掉一部分GPU上的临时tensor开销,对CIFAR-10这种分辨率不高的场景尤其好用。
收敛太慢的问题,优先检查是不是数据加载成了瓶颈。把整个子集加载进内存之后再做优化,速度能提升一个量级。CIFAR-10全部图片也才几十MB,毫无压力;ImageNet子集建议先落盘成内存映射格式,避免每轮迭代都做解码。相信我,大部分“收敛太慢”根本不是算法问题,而是IO拖了后腿。
5. 个人理解与可能的延伸方向
5.1 这篇方法到底强在哪
抛开论文本身的实验数字,我认为它最大的贡献不是提出了一个新的攻击流程,而是给了社区一个非常清晰的思路:别把所有样本一视同仁地喂给优化器,学会用便宜信号做重要性采样。
这个思路其实不只是UAP能用,很多样本效率敏感的优化问题都能借鉴。比如主动学习里挑哪些样本给标注员打标,本质上也是个重要性排序问题;再比如对抗训练里,用类似signal优先选择“困难样本”参与训练,可以大幅降低训练开销。
我另外欣赏的一点是它对“先验失配”的处理态度。虽然论文主推固定先验,但在实际复现中,动态更新先验和混合多类先验都能带来稳定提升。这说明方法框架本身不脆弱,有很强的扩展空间。
5.2 对防御方的反向启发
每次读到攻击论文,我都会习惯性地反向想想防御方该怎么应对。这篇论文反过来也告诉防御方一个重要事实:模型在哪些样本上表现不确定,攻击者是可以低成本探测到的。
如果一个防御系统只盯着样本本身的鲁棒性,而不去关注整体分布里高不确定性样本的分布,那攻击方很容易用这种重要性采样策略快速找到防御盲区。反过来,防御方如果能在训练阶段主动降低模型在高熵样本上的不确定性,比如做更加细致的置信度校准,相当于把“重要攻击样本”的优先级打散,进而削弱先验采样的收益。
这也解释了为什么现在很多防御工作会关注“置信度分布平滑”和“特征边界对齐”,它们本质上都是在压缩攻击者可利用的先验信息。
5.3 值得动手做的三个小改进
如果你看完论文手痒想复现或者改进,我建议从下面三个方向里选一个动手:
动态更新先验。每隔固定轮数,用当前δ重新算一次熵或loss,作为新一轮采样的权重。实现起来只改十几行代码,体验非常好,尤其适合数据分布比较复杂的场景。
多模型集成先验。用两到三个模型输出的平均熵做采样权重,能显著提升UAP的迁移性。这个改动在代码上也很小,但是要多维护几个模型,显存开销会翻倍。
把先验加权从采样概率迁移为梯度加权。不是用加权采样挑子集,而是让所有样本都参与前向,但在反向传播时按照先验分数归一化后的权重去放大或缩小梯度。这个方案计算量会大一些,但相当于对梯度做了一次重加权,优化方向更平滑。
我个人的经验是,第一个改动最划算——成本极低、效果显著,而且当你把“动态更新先验”跑通之后,你对整个UAP优化过程的理解会更深一层。论文原方法像是一份标准答案,但这种补充改动才是你自己真正消化它的标志。
5.4 最后再分享一个实操中的小技巧
在正式跑UAP实验之前,我强烈建议你先在一个极小的子集上做冒烟测试。具体来说,随机选200张图片,把batchsize设成32,迭代100轮,看一眼扰动loss是不是在下降、扰动是不是往合理方向变化。这一步花不了5分钟,但能拦住大量低级错误。
我自己就在这个环节踩过不少坑。有一回代码里把先验分数的归一化写错了,导致采样概率全部集中在一张图上,100轮迭代出来的扰动几乎只对那一张图有效。如果直接开跑全量实验,至少要等几小时后才能发现这个蠢错误;做了冒烟测试,几分钟就能定位问题。论文方法和工具本身都不复杂,但如何高效地把它们复现出来,往往比读懂公式更考验工程经验。