1. 先聊点实在的:为什么“对抗样本”盯上了扩散模型
扩散模型这两年是AIGC的顶梁柱,从文生图到视频生成,哪哪儿都有它。尤其是潜在扩散模型(LDM)这类架构,把像素空间压缩到隐空间再做扩散,效率高、效果稳,成了众多线上服务的基础。你可能刷到过“用扩散模型生成动漫头像”这类应用,看起来就是个画图工具,但只要一个输入框暴露在外部,背后就藏着一整套安全攻防问题。
对抗样本这个词不算新鲜了,在分类任务里,给图片加一层肉眼看不见的噪声,就能让模型把猫认成狗。这类攻击手段早就在图像识别、人脸验证这些场景里被大量研究。但扩散模型的输入输出形态和分类器不太一样,它的输出是一张完整生成的图,攻击目的也从“让模型判错”变成了“让模型生成错的东西”。这个差异非常关键,导致很多传统攻击方法不能直接搬过来用,也是“扩散模型对抗样本”能单独成为一个研究方向的原因。
这个内容适合谁参考呢?如果你是做AIGC安全、鲁棒性评测,或者想评估自己的扩散模型部署方案会不会被人“带节奏”,那这套经典baselines值得摸一遍。就算你只是跑开源代码做实验的研究生,了解一下这些baselines的设计思路,也会对你设计自己的评测方案很有帮助。
我曾经把主流baselines挨个复现过一轮,实操中踩了不少坑。这篇东西不是教科书复述,而是把“这些baseline到底在干什么、它们之间的关系、怎么在真实代码里跑起来、以及容易翻车的点”讲清楚。看完之后你能得到一份可以直接索引的baseline清单,还有一套拿来即用的实验框架思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先做思想准备:扩散模型的对抗攻击,到底攻击的是哪个环节
想弄懂baselines,先得弄明白攻击对象是什么。扩散模型不是单块“识别模型”,它是一个包含文本编码器、UNet(或者DiT)、变分自编码器解码器的生成系统。文本进来以后,不是直接被模型“理解”,而是一层层变换,最后才变成图像。攻击者想干扰这个流程,必须搞清楚自己到底在哪个环节下手。
2.1 攻击链路:能下手的地方,比你想的多
- 文本编码器阶段:你对输入的一段文字做改写、插入不可见扰动,攻击权重通常落在CLIP文本编码器上。这种扰动不是直接加在像素上的,而是加在文字embedding层面。
- 隐空间阶段:潜在扩散模型的核心在latent space。如果攻击者能看到(或猜到)编码器输出的隐向量,他可以对隐向量做对抗扰动,称为latent-level攻击。
- 解码器阶段:生成的图像已经是一张RGB图后,再对它做像素级修改。这个更像传统对抗样本,但要考虑“改完之后图看起来还是不是原图”的问题。
- 条件控制阶段:如果是用ControlNet之类的结构控制生成,攻击还可以存在于姿态骨架、线稿等条件输入上。
大多数baseline把火力集中在隐空间和text embedding上,因为这两个地方“性价比”最高——扰动小、影响大,而且跟扩散模型本身的采样过程耦合紧密。纯像素空间攻击往往扰动会很大,因为解码器本身会把部分输入变化给“吸收”掉,导致攻击成功率不佳。
2.2 威胁模型:白盒、黑盒和迁移攻击的差异
威胁模型决定了你能拿到多少信息。白盒场景下,攻击者拥有模型全部信息,包括权重、梯度,可以放心做梯度反传;黑盒场景下,攻击者只能通过查询接口获取生成结果,无法直接算梯度;还有一种“半黑盒”就是迁移攻击,攻击者先攻击一个替代模型,然后把扰动拿到目标模型上去试。
扩散模型的黑盒攻击成本极高:你每查询一次要做一次完整采样,一次采样几十步,如果模型部署在远端,一次查询可能要等好几秒,甚至几天才能跑完一次攻击。所以很多学术baseline只在白盒或者迁移设置下做评测,这是现实约束,不是偷懒。
如果你是工程上做防护评测,我的建议是先把白盒测试跑好,再考虑迁移和黑盒。因为白盒能反映出模型攻击面上限,若白盒都攻不动,那黑盒大概率也稳。
2.3 攻击目标的细分:无目标攻击和有目标攻击
- 无目标攻击:只要让生成结果偏离“正常内容”就算攻击成功。比如本来生成一张猫的图,加了对抗扰动后,生成出来的图看起来像“猫+噪点混合体”或者“扭曲的猫”,语义不再是正常猫。
- 有目标攻击:攻击者希望模型生成某个特定内容,比如说生成一张图,模型认为是“高质量人脸”,但实际内容被定向改成了“狗脸”或“黄色图片”。这种难度更大,因为你需要让对抗扰动的方向与目标语义对齐。
经典评测中,无目标攻击用得更多,因为可量化性好——算FID、LPIPS、CLIP Score就能判断“偏离程度”。有目标攻击需要定义目标Embedding,就像给模型“下命令”,可解释性强但实验设计容易出幺蛾子。
2.4 为什么传统ImageNet对抗攻击方法不通用
传统对抗攻击基于分类器,攻击信号是模型最后一层logits,反传目标非常清晰。扩散模型里没有“logits”这种输出,生成过程包含随机采样噪声,每一帧中间结果都在变化,梯度在整个采样轨迹上不稳定。
所以不能简单把PGD套到UNet上就完事,你需要考虑:
- 采样过程(DDIM、DPM-Solver等)是否可微;
- 随机噪声项应该冻结还是每次都重新采样;
- 攻击目标是中间隐向量的误差,还是最终图像空间的误差。
这些因素都让扩散模型攻击成为一门“既要懂对抗攻击,又要懂生成模型”的交叉活儿。
3. 经典baselines全景图:谁在攻击什么、为什么这么设计
我梳理了目前大家在论文和开源仓库里最常对比的几条baseline路线,并按照攻击层次和更新对象做了分类。把它们搞明白,基本就能看穿大多数论文的方法表格。
3.1 传统对抗攻击硬套扩散模型:PGD和MI-FGSM的变体
最朴素的思路就是沿用PGD,但把攻击目标改成“让直接输出图像的感知质量变差”。做法很简单,输入一张引导图或者文本,然后对输入条件(图像或embedding)做多步梯度上升,每一步根据与干净图片的输出差异计算损失。
这类方法复现很简单,很多新手拿过去就能跑通。问题在于:
- 纯像素空间攻击对潜在扩散模型效果差,因为VAE解码器会将最后一步微扰“抹平”;
- 对采样步数很敏感,采样步数越多,攻击需迭代次数越大;
- 图像级攻击通常会留下高质量痕迹,容易被简单的噪声过滤防御缓解。
不过它依然是很好的第一棒,至少能帮你验证整套梯度链路通不通。
3.2 AdvDM:直接攻击扩散模型的“骨干”
AdvDM是我印象里较早把“扩散模型对抗样本”当成独立问题的思路之一。它的核心洞察是:扩散模型本质上在做“从噪声中还原真实数据分布”的过程。如果攻击者往初始噪声里加一个精心设计的扰动,就能让模型误以为自己的采样终点落在目标图像分布上,从而生成攻击者想要的内容。
在训练好的扩散模型上计算梯度时,需要把整个DDPM/DDIM的反向去噪过程都展开,内存消耗非常大。于是AdvDM用了近似——只选取部分采样时间步去反传梯度,而不是全部展开。这种做法在工程上很有价值,也是后续很多latent攻击方法的雏形。
我当时复现时发现一个坑:AdvDM的梯度和随机种子强相关。在步数较少、采样器固定为DDIM时,它效果还比较稳定。换到DPM-Solver或Euler采样器后,鲁棒性会差一截。所以跑这类baseline一定要固定采样器、固定步数,否则实验结果会飘。
3.3 DiffAttack:用集成方式提升攻击鲁棒性
DiffAttack的设计理念更像是在回答一个问题:单条采样轨迹上的梯度容易陷入局部极值,那如果我们让模型同时沿着多条采样轨迹反传梯度,是不是能获得更稳定的攻击方向?它确实这么干了,采用多步、多噪声集成策略,让每个mini-batch里包含不同初始化噪声的轨迹,并将这些轨迹的梯度做平均。
这种做法的好处很明显:攻击迁移性变好了。同一份对抗样本放到没见过的新采样器、新模型上,攻击成功率依然能维持。坏处是显存压力巨大,毕竟你相当于同时跑了好几个扩散模型的正反向。
实操中如果想复现,建议先在1024×1024或512×512上试小batch,再用梯度累积近似。不要一开始就上高分辨率加高batch,很容易把显卡直接顶爆。我试过用batch=8的高分辨率配置,刚启动两秒就OOM了,最后只能靠梯度累积和换用低分辨率分批计算解决。
3.4 隐空间对抗扰动:Latent Attack及其家族
潜在扩散模型的一大特点是,图像生成过程在低维隐空间里完成。如果攻击者有能力拿到VAE编码器输出的隐向量,他可以直接在隐空间里做对抗扰动,再把扰动后的隐变量交给扩散过程去生成。相较于像素空间攻击,这个玩法噪声更干净、改动幅度更小,对最终图像的影响却可被精确控制。
经典latent attack通常用类似于PGD的更新公式,只不过优化对象是隐向量z。损失函数不是CrossEntropy,而是“让最终生成结果与目标图像的隐向量偏离最大”或“让生成结果落在某个特定的embedding上”。有些baseline会用CLIP embedding作为对齐目标,使得攻击带有语义引导能力,攻击后图还是“像图”,只是内容被改了。
这类方法对工程实现的要求较高,因为你需要保证VAE端到端可微,同时把扩散模型的采样轨迹控制在有限的步数内。我通常会把扩散采样step压缩到10~20步,然后冻结随机噪声,只更新隐向量的对抗扰动。效果和速度之间能找到一个比较实用的平衡点。
3.5 文本引导对抗攻击:针对Prompt的扰动
市面上大量AIGC服务通过文本作为用户输入。针对Prompt的攻击其实是“输入改写”:你在正常提示词附近寻找一个对抗文本邻域,让模型“误解”你的意图,从而生成违反预期的内容。
文本空间的离散性给攻击带来很大麻烦。你不能像图像那样做浮点数级别的梯度更新,只能在词向量连续空间中计算扰动,然后在词表里做映射。通常做法是先用梯度确定哪些词token是“攻击热点”,再对这个token的embedding加可学习的扰动,最后通过近邻检索投影回真实词表。
这个方向目前论文中出现的频率在上升,但baseline还不算完全统一。很多经典评测仍是拿“图像+提示词”联合作为输入,并不是纯文本攻击。如果你只想把baseline跑稳,建议先从图像侧攻击开始,文本侧放在第二优先级。
3.6 各类baselines关系速查表
| Baseline/attack族 | 攻击层 | 核心优化对象 | 需要的模型信息 | 工程难度 | 适合场景 |
|---|---|---|---|---|---|
| PGD/MI-FGSM变体 | 像素空间 | 输入图像 | 白盒梯度 | 低 | 简单评测、基线校准 |
| AdvDM | 噪声/隐变量 | 初始噪声或隐向量 | 白盒梯度 | 中 | 通用无目标攻击 |
| DiffAttack | 隐空间+多轨迹集成 | 对多轨迹联合优化 | 白盒梯度+多步采样 | 高 | 强迁移攻击、鲁棒评测 |
| Latent Attack | 隐变量/embedding | 潜变量扰动 | 白盒梯度 | 中高 | 有目标攻击 |
| 文本引导攻击 | 离散词嵌入 | prompt的embedding | 白盒梯度+替代词映射 | 高 | 线上AIGC服务测试 |
| 迁移攻击 | 可作用在以上任意层 | 替代模型上的扰动 | 替代模型白盒、目标黑盒 | 高 | 真实服务黑盒评估 |
表格里这几类,不是互斥关系。实际研究中经常是混合着用,比如先隐空间攻击,再用集成策略增强迁移性。
4. 实操手记:从零跑通“PGD→AdvDM→DiffAttack”这条链路
光说概念没用,我把自己完整跑过的实验流程分享出来,代码结构以伪代码加逻辑说明为主,你照着思路能还原到自己的项目里。
4.1 技术选型与实验环境准备
我用的环境是PyTorch 2.0以上,CUDA 12.1,单卡A100 80G。模型用的是Stable Diffusion v1.4的pipeline,但你需要把它拆成组件来用——不要直接调StableDiffusionPipeline的傻瓜接口,需要手动拼UNet、CLIP和VAE。
依赖库核心是diffusers、transformers、torch、torchvision、clip、numpy,如果你想省事可以直接在conda里装。
我测试时的基本参数如下:
| 参数 | 数值 | 备注 |
|---|---|---|
| GPU | A100 80GB | 如果显存小,分辨率要降 |
| 图像尺寸 | 512×512 | 默认兼容SD 1.x |
| 采样步数 | 20 | 平衡速度与画质 |
| 采样器 | DDIM | 可复现性较好 |
| 扰动预算epsilon | 0.05~0.1 | 归一化到[0,1] |
这个初始配置不是拍脑袋定的。DDIM采样器是“确定性采样”的代表,攻击者在固定噪声后,梯度能稳定回传;如果换成DPM-Solver,它内部的多步校准会导致反传过程复杂化,不太适合新手复现。
4.2 第一步:把扩散模型拆成“可攻击的积木”
用diffusers搭一套具有手工控制力的流程,分别加载各组件,然后把它们串起来。这一步看上去没有“攻击”,但它是所有baseline的底层底座。我发现很多人在这步偷懒,直接拿着pipeline一把梭,结果后面想要取中间隐变量、冻结某个模块时完全无从下手。
关键操作点:
- 将UNet从pipeline中单独取出来,开启
requires_grad_(False),只对你要攻击的条件输入或隐向量开梯度。 - VAE编码器只在latent attack时需要,提前单独加载。
- CLIP文本编码器默认可以冻结,除非你要做文本embedding攻击,才需要对其开启梯度。
- 固定随机种子,保证后续所有采样过程可复现。
4.3 第二步:用极简PGD验证梯度链路通不通
先别急着上复杂攻击,用一个传统PGD来验证反向传播链路是好的。攻击损失用“输出图像与干净图像在LPIPS上的差异”即可。之所以用LPIPS而不是L2,是因为LPIPS更贴近人眼感知,能反映语义层面的偏移,这也与扩散模型质量评测更合拍。
完整伪代码如下:
python复制for step in range(num_iters):
noise = torch.randn_like(latent).to(device) # 保持固定噪声
latents = noise.clone()
for t in reversed(pipe.scheduler.timesteps):
latent_model_input = torch.cat([latents] * 2)
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_emb).sample
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
latents = scheduler.step(noise_pred, t, latents).prev_sample
image = vae.decode(latents / 0.18215).sample
loss = lpips_loss(image, clean_image)
loss.backward()
# 对输入噪声做PGD更新
noise.data = (noise.data + epsilon * noise.grad.sign()).clamp(-epsilon, epsilon)
noise.grad.zero_()
跑一遍这个脚本,如果loss正常下降,说明整套backward链路OK。如果loss不降或梯度为None,去检查采样器timesteps的数据类型,很多版本问题出在tensor类型Float和Long不一致。
4.4 第三步:复现AdvDM类攻击,做对“时间步采样”
AdvDM的核心在于,不全量展开DDPM的反向过程,而是随机抽一部分时间步做攻击损失计算。这样显存占用小,训练也能较稳。复现时要特别注意它对时间步的采样策略,与去噪长度有关。
关键参数特点如下:
- 通常从总步数里随机抽5~10步用于梯度反传;
- 用累积梯度的方式提高稳定性;
- 对噪声扰动做投影,使其限制在epsilon球内;
- 每轮评估时需要单独重新采样噪声。
我在实际项目里的代码片段可以抽象成这样的逻辑:
python复制timesteps = torch.randint(0, max_timestep, (num_gradient_steps,)).sort(descending=True).values
loss_sum = 0
for t in timesteps:
noisy_latents = scheduler.add_noise(latents, noise, t)
noise_pred = unet(noisy_latents, t, text_emb).sample
loss_t = criterion(noise_pred, target_noise)
loss_sum += loss_t
loss_sum.backward()
这类攻击如果你想迁移到自己的模型上,最关键的是拿到一组合理的噪声预测目标和时间步数量。时间步太少,优化不稳定;太多,显存吃不消。
4.5 第四步:尝试DiffAttack风格的多轨迹梯度集成
DiffAttack的训练身段比较复杂,我做了一个简化实现:在同一个batch内构造多条随机噪声轨迹,分别计算损失后取平均梯度更新。这不算完全严格复现,但在评测中已经能获得足够鲁棒的扰动了。
要注意的是,这条branch会加大显存压力。我在单卡A100上只能把batch撑到4~6条轨迹,再多肯定爆显存。你也可以通过梯度累积来模拟更大batch。实际测试中,多轨迹平均确实让迁移攻击成功率提升了不少,代价就是单轮迭代时间高了接近一个量级。
4.6 手动设计一个对比实验并记录指标
在评测套件中,我建议所有baseline在同一组固定种子上跑,同时使用相同的采样器、步数、分类器和特征提取器。否则你无法判断某个basline的提升是来自算法本身,还是仅仅因为换了随机种子。
记录指标可以分为三组:
- 扰动不显著性:LPIPS、SSIM、PSNR。
- 扰动有效性:目标攻击成功率、无目标攻击成功率。
- 语义一致性:CLIP Score,用于检验攻击后图像在语言描述上是否彻底偏离。
表头可以是“模型 / 攻击方法 / 采样器 / epsilon / 攻击成功率 / CLIP分数 / 时间开销”。最后在评测表格里比较各baseline,这样最有说服力。
5. 复现路上的坑:我替你们先踩一遍的避坑清单
这些坑不是论文里会写的,是你自己跑代码时一定会遇到的。我把最典型的问题整理成了问答式速查,方便你以后出问题直接来翻。
5.1 为什么梯度过了一会儿就消失,死活不更新
最常见的原因是采样器内部的操作把需要梯度的tensor clone了,或者原地操作覆盖了梯度。解决方法是把采样过程中的关键中间量用retain_grad()打开,或者直接手写一个固定步数的迭代采样循环,不要依赖黑盒采样器。
5.2 梯度回传时经常OOM
降低采样步数和时间步采样数量是第一选择。如果显存依然爆炸,就把UNet的attention计算切到slice模式,或在损失函数计算时用checkpointing技术。我不会轻易把batch降到1,那样loss波动会很大,更推荐用梯度累积。
5.3 加了攻击噪声后,生成图像还是“老样子”
如果攻击后生成结果与干净输入相比几乎没有变化,保险起见先用一个简单但“暴力”的测试:把扰动加到肉眼可见的程度,观察输出是否剧烈变化。如果这样还不变化,说明扰动没有作用到模型的有效输入路径上——你攻击的可能只是某个没有被使用到的变量。
5.4 DiffAttack跑得特别慢,怎么优化
- 时间步采样数量减半先跑着看趋势;
- 减少轨迹数量,比如batch=4;
- 图像尺寸从512降到384或者256,验证实验可以肉眼分辨趋势;
- 使用fp16混合精度,能明显提速。
混合精度有个问题:梯度精度可能受损,某些低强度攻击会失效。因此最后确认结论时要用fp32复现一遍关键实验。
5.5 跨模型迁移效果差,如何调参
先确认替代模型和目标模型在结构上是否有足够相似性。如果替代模型是SD 1.4,目标模型是SDXL,迁移性天然就差。尽量选择同架构或同系列模型。再者让集成轨迹更多更杂,让扰动不要过拟合到某条特定采样路径。
5.6 不同论文结果对不上,如何自查
最常见原因在于评测指标不一致。有的文章用FID作为攻击成功标准,有的用CLIP Score,有的甚至用人工打分。这些指标对“攻击成功”的定义差得很远。所以你必须先明确自己的评价标准,再与论文对齐,别盲目追求复现完全相同的数字。
| 现象 | 优先排查方向 |
|---|---|
| 梯度为None或NaN | 数据是否float16、采样器操作是否原地覆盖 |
| 攻击成功但不扰动 | epsilon太小加loss收敛到局部了,增加迭代轮次或换损失 |
| 对采样器特别敏感 | 冻结随机种子、固定DPM采样器参数 |
| OOM | 降低batch、降低分辨率、减少反传时间步 |
6. 写在最后的几句实在话
做对抗样本评测这事,本质上不是为了“攻破谁”,而是为了搞清楚一个系统的安全边界在哪里。我一开始接触这个方向时,也觉得“不就加个噪声吗”,但真去把扩散模型从正向采样到反向梯度完整跑通之后,才发现每一步都是细节。很多看起来高级的模型,你拿最朴素的白盒攻击就能让它生成质量大幅崩塌,反而是一些结构简单的系统因为采样链路短,更不好打。
我的建议是:如果你刚入门,不要直接挑战最复杂的DiffAttack。先把一个简单PGD变体彻底吃透,把扩散模型的采样循环背下来,会改调度器、会换损失函数、能厘清计算图中的依赖关系。这个基本功扎实了以后,后面所有花哨方法对你都只是“优化决策不同”而已。若你还有精力,可以自己去探索结合不同攻击层、不同损失函数的混合策略。对抗样本这一行毕竟是在跟整个生成模型的架构演进赛跑,baselines更新换代速度比想象中快,但底层逻辑和攻防博弈关系,大概率还能再用个几年。
