扩散模型分类器引导:从原理到采样实践全解析
做生成模型的同学基本都绕不开一个痛点模型生成的质量上去了但结果不受控。你用扩散模型随手一抓能出很漂亮的图但想让它稳定生成“一只猫坐在沙发上”问题就来了。分类器引导Classifier Guidance就是为了解决这类条件生成问题提出的思路之一。它的核心想法很直接既然扩散模型只是一个无条件生成器那我就在采样过程中额外拿一个分类器去判断当前结果离目标类别有多远再把“误差方向的梯度”塞回采样步骤里把生成轨迹往目标类别拐一把。这个方案在扩散模型早期是非常关键的一步。它不需要改动生成模型的训练过程不改变扩散模型的任何权重只需要在推理采样时额外挂一个训练好的分类器。它也是后来被广泛使用的无分类器引导Classifier-Free GuidanceCFG的前置思路——理解了它你再看CFG、条件生成、图像编辑里的各种引导手段都会通透很多。这篇文章我打算从问题背景、数学原理、分类器训练细节、完整采样流程到常见坑位把这条链路摊开讲一遍。适合刚接触扩散模型、想搞懂“引导”到底在干什么的同学也适合已经会跑几行代码但对其中的系数和梯度为什么要这么处理摸不着头脑的人。1. 为什么需要分类器引导扩散模型的条件生成困境1.1 扩散模型天然是“无条件”的扩散模型的基本玩法可以理解成两步先把真实图像逐步加噪直到变成纯高斯噪声然后训练一个神经网络去逆转这个过程。所谓“逆转”本质上是在学习条件分布 p(x_{t-1} | x_t)——给定一个稍微干净一点的噪声图预测更干净的那个图上一步长什么样。这个条件分布里没有类别标签的位置。你给它一个对象它不会问“这东西是什么”它的任务仅仅是“让图像变得更清晰、更真实”。所以标准的DDPM、DDIM在完成训练之后其实是一个纯正的无条件生成器。你采样出来的图像是什么内容完全看一开始抽的随机噪声落在分布里的哪个区域。这个特性不是缺陷它保证了模型学习的是一个完整的、高覆盖度的数据分布。但在实际应用中我们几乎总是需要“条件”要人脸就出人脸要猫就出猫要某一风格的图就出某一风格。无条件生成器没法直接满足这类需求这就是条件生成问题的起点。1.2 条件生成的两条路线训练时注入与采样时引导给扩散模型加条件信号业界基本有两条路线。一条是在训练时就带上条件。把类别标签、文本向量、深度图等额外信息输入到主干网络里让网络在每次去噪时都参考这个条件学一个条件噪声预测器 p(x_{t-1} | x_t, y)。这种方式后来发展成了主流像Stable Diffusion里的文本条件就是用交叉注意力把文本向量送进UNet的。它的好处是条件信息能被模型深度融合生成质量和一致性上限很高坏处是你要重新训练或微调模型成本大而且一旦模型训完条件模态基本就固定了。另一条路线就是这里要讲的分类器引导。模型照旧无条件训练但在采样时用一个“外挂”分类器提供条件信号。它的逻辑是反向去噪的每一步除了按照噪声预测网络的方向走我再额外加一个“让分类器更确信目标类别”的梯度偏移。生成模型自身没有变变的只是采样轨迹。这个思路有点像开车去市中心。原本扩散模型只知道“怎么把路走顺、走稳”但不知道目的地。分类器引导相当于在旁边放了一个导航它不重新教你开车只是在每个路口提示一句“往左打一点方向”。你知道大方向但每一步的小修正来自外部信号。1.3 分类器引导的核心定位零训练成本的“外挂条件”分类器引导能火除了思路直观更重要的是它在工程上有巨大吸引力生成模型不用动分类器可以随便换。你训练一个通用的分类器用来判断“图里是猫还是狗”下次想生成猫挂上猫那一类的梯度想生成狗换一个目标类就行。同样的生成模型可以接十个、二十个不同的分类器各管一个属性维度这比每个条件重训一个生成模型经济得多。有研究者在早期实验中发现分类器引导还能明显改善FID、IS这类生成质量指标尤其是对ImageNet这种类间差异大的数据集。原理上可以这么理解分类器梯度把采样轨迹推向了类别分布中“更典型”的区域相当于降低了样本落入类间模糊地带的概率也让生成的每一类特征更集中、更容易被识别。所以这个引导不只是“把结果导向某类”在某种程度上也是在帮生成模型“修正自己不太准的类别边界”。当然后来的Classifier-Free Guidance在文本生成等大规模场景里取代了它的位置因为CFG根本不需要训练分类器只需要一次条件模型和一次无条件模型的输出就能达到甚至超过分类器引导的效果。但分类器引导的概念、数学框架和“用外部梯度修正采样方向”的思路是理解整个引导机制家族的基础。2. 分类器引导的数学原理一次贝叶斯分解2.1 从条件分数函数说起要想把分类器梯度塞进扩散采样得先找到扩散模型和分类器的共同语言。这个共同语言就是分数函数也就是概率分布对输入数据 x 的梯度。扩散模型的去噪网络在不少推导里会被解释成在估计分数函数 ∇_x log p(x_t)。用噪声预测网络 ε_θ(x_t, t) 来表示时两者就差一个和噪声方差相关的缩放系数。直观地说分数函数告诉我们在当前的噪声图 x_t 下往哪个方向移动能让它更像来自真实数据分布的样本。现在我们要的条件分布是 p(x_t | y)它表示“在已知目标类别为 y 的前提下当前噪声图 x_t 的似然”。如果我能知道这个条件分布的分数函数 ∇_{x_t} log p(x_t | y)那采样时就照着这个分数走生成的样本自然满足条件。这里用到了最经典的贝叶斯分解p(x_t | y) ∝ p(x_t) · p(y | x_t)两边取对数再对 x_t 求梯度得到∇_{x_t} log p(x_t | y) ∇_{x_t} log p(x_t) ∇_{x_t} log p(y | x_t)注意等式右边第一项正是无条件扩散模型已经在估计的分数第二项正是分类器网络 p(y | x_t) 对输入图像求梯度。这两项相加就得到了一个完整的条件分数函数。这意味着我完全不需要训练条件生成模型——无条件分数网络加一个分类器梯度就凑出了条件分数函数的完整表达。2.2 DDPM采样公式里怎么塞进一个分类器在DDPM的实际采样中我们并不直接操作分数函数而是操作噪声预测网络 ε_θ。核心更新大致是x_{t-1} ← 均值项 随机噪声项其中的均值项 μ_θ 可以表示成μ_θ(x_t, t) (1 / √α_t) · (x_t - (β_t / √(1 - ᾱ_t)) · ε_θ(x_t, t))加入分类器梯度后均值项被修正为μ_guided μ_θ s · σ_t² · ∇_{x_t} log p_φ(y | x_t)这里的 σ_t² 对应的就是当前步的方差比如 β_t 或对应的高斯扩散过程方差s 是引导强度系数p_φ 是那个外挂分类器。分类器梯度的方向正是“让目标类别的对数概率变大”的方向把均值朝这个方向拉每一步都往“更像目标类”的方向走累积起来就能把最终生成的样本拉进目标类别。很多初学者会困惑“梯度符号到底是加还是减”。有个很实用的判断方法你想让什么变大就往它的梯度方向加。分类器对目标类的对数概率求梯度这个梯度的方向就是该类别概率上升最快的方向所以采样更新里是加上这个梯度不是减去。不过要注意具体实现库里的换算系数经常不一样有的代码会直接把梯度加到 x_t 上有的加到预测噪声上有的代入均值公式。这些在数学上是等价的但具体缩放系数不同你没搞清楚就瞎抄代码出来的结果从“几乎没引导”到“完全崩坏”都有可能。这正是下一节要讲的 guidance scale 存在的意义。2.3 guidance scale 到底是干什么的看到公式里的 s 了吗它叫引导强度或者叫guidance scale。它控制的是“分类器梯度的可信度和作用幅度”。当 s 0 时采样退化成纯无条件生成分类器完全不参与s 越大每次采样步骤里分类器对轨迹的拉扯越强生成结果越贴合目标类别。但s不是越大越好。你把这个系数调大的时候所有样本都会往目标类别在高维空间中的“峰顶”方向集中。生成结果的类别一致性上去了但多样性会明显下降甚至可能出现“每个结果几乎长得一样”的典型故障术语叫模式坍缩。更极端的情况下生成图像会出现伪影、色彩失真、饱和度爆炸。一个很有参考价值的观察是早期的一些实验里中等偏大的引导强度比如 s 在 1.0 到 3.0 之间能在保持类别准确性的同时显著改善图像质量。但这个范围跟具体数据集、采样步数、分类器训练质量都相关不是一个可以无脑复制的参数。需要你在自己的场景里跑一个简单的收缩实验从 s 0 开始逐步加码观察分类精度和FID的交叉变化来定。2.4 一个直觉理解采样中的“拉力”与“随机力”你还可以从物理图像上理解这件事。扩散模型的采样过程就像是一个粒子在能量景观里做朗之万动力学运动均值项相当于“向下滚”的力随机噪声项相当于“布朗运动”的热扰动它保证粒子不会总是掉进同一个局部最小。分类器梯度相当于额外增加了一个“向目标类别低势能区域滚”的定向力。你把这个力加大粒子当然更容易到达目标区域但同时也可能碾掉原本由噪声项维持的多样性。理解了这层关系你就能够解释为什么引导强度增大后样本多样性会下降——因为定向力压过了热扰动粒子全部滚进了一个比较窄的峰。3. 分类器设计与训练真正的工程难点3.1 普通分类器为什么不行有人会觉得分类器引导嘛拿一个现成的图像分类网络比如在干净图像上训练好的ResNet一类直接挂上去不就行了吗实际试过就知道完全不行。原因很简单扩散模型采样过程中每一步喂给分类器的输入 x_t 根本不是干净图像而是带噪的中间状态而且这个噪声量在不同时间步差异巨大。在干净的ImageNet图像上训练的分类器遇到带有大量高斯噪声的 x_t输出的类别概率基本就是随机噪声附近的均匀分布梯度方向毫无意义。用这种分类器去引导采样就跟让一个只看得见白天的无人机在大雾天导航一样处处失灵。所以分类器引导里真正的主角不是那个“判断最终图像类别”的网络而是一个“判断带噪图像所属类别”的噪声分类器。它必须被专门训练才能在不同噪声强度下都给出可靠的梯度。3.2 噪声分类器的训练细节训练噪声分类器的方法不难听一遍就能理解。你拿一个预训练好的扩散模型把干净图像按某个时间步 t 加噪得到 x_t √(ᾱ_t) x_0 √(1 - ᾱ_t) ε然后把 (x_t, t) 一起喂给分类器让分类器输出这张带噪图像属于哪个类别。损失函数就是普通的多分类交叉熵。这里有两个细节决定分类器是否好用。第一时间步 t 必须作为条件打进分类器。因为同一张图加噪到不同程度时分类难度完全不一样。t 很小的图接近干净图像分类器很容易判断t 很大的图几乎完全是噪声分类器只能瞎猜。如果分类器不知道当前噪声有多大它就无法合理校准输出置信度。给 t 做一个类似于Transformer位置编码的正弦嵌入然后和图像特征拼接是最常见的做法。如果分类器主干是UNet也可以像扩散模型一样在残差块里以尺度偏移的方式注入时间步信息。第二时间步 t 的采样分布很关键。训练时如果只在随机均匀的 t 上采样分类器在极端噪声级别下的表现会非常差。有个实用技巧是让训练数据的时间步分布跟扩散模型采样的实际轨迹对齐或者至少在训练时略微重加权让分类器在中等和高噪声区间花更多精力学习。否则你采样时走到后面几步分类器的置信度直接掉到底梯度消失引导等于白加。3.3 网络结构与条件注入用UNet还是普通CNN分类器主干选什么会直接影响引导效果。如果你只是做低分辨率实验一个ResNet级别的分类器加上时间嵌入就能跑起来。但如果你想做高分辨率、需要高质量梯度引导的场景更可靠的是采用和扩散模型去噪网络相近的结构——比如UNet。原因在于引导梯度的计算是逐像素的输入尺寸和通道变化都发生在多维张量上UNet本身的多尺度特征提取和跳跃连接能提供更稳定的梯度场ResNet虽然也能出梯度但容易出现梯度集中在局部区域的问题导致生成结果局部细节奇怪。实操上的一个折中方案是用UNet骨架做分类器输出端接一个全局池化和线性分类头。UNet既提供时间步条件注入的位置又保留空间结构特征整体训练成本也不算很高。早期一些经典工作里的噪声分类器就这么设计的效果远好于直接拿普通分类网络。3.4 分类器训练中的几个坑第一是分类器对输入分布的匹配。扩散模型的图像输入一般被归一化到 [-1, 1]分类器训练和推理时也要保持同样的归一化方式否则梯度的尺度就不对。很多人训练分类器时直接用 ImageNet 统计量做归一化跟扩散模型的 [-1, 1] 空间不匹配梯度出来歪得离谱。第二是分类器过拟合。噪声分类器的参数量如果太大训练数据又不够它会记住训练集中某几个带噪样本的类别特征导致在采样时对未见过的中间状态过于自信。一个可靠信号是分类器在验证噪声图上的准确率很高但引导出来的生成图像质量反而下降。这基本就是分类器泛化出问题了。第三是分类器的数据和扩散模型的训练数据必须来自同一个分布。生成模型在某个类别上生成效果差分类器又是在另一批数据上训练的这两者根本不匹配引导方向自然无效。实际项目中最好用同一份数据源分别训练生成模型和分类器。4. 完整采样流水线手把手跑通一个带引导的生成4.1 准备阶段在进入采样代码之前你需要准备好三样东西一个训练好的无条件扩散模型一个能够在任意噪声级别下输出类别概率的噪声分类器以及一个明确的目标类别标签 y。这是一切引导采样的起点。我们的目标其实很纯粹在每一个时间步 t 上调用扩散模型预测噪声调用噪声分类器计算目标类别的梯度把两者按特定系数融合然后按 DDPM 的更新公式推进到 t-1。下面这段代码我用伪代码加 PyTorch 风格来写思路不绑定任何具体框架。其中 diff_model 是去噪网络classifier 是噪声分类器。关键点不在于用什么工具而在于梯度的计算方式和系数配比。import torch def sample_with_guidance( diff_model, classifier, x_T, y, betas, alpha_bars, guidance_scale2.0, ): x x_T # 初始纯噪声 for t in reversed(range(len(betas))): alpha_bar_t alpha_bars[t] beta_t betas[t] alpha_t 1 - beta_t sigma_t beta_t ** 0.5 # 1. 扩散模型预测当前噪声 with torch.no_grad(): eps_pred diff_model(x, t) # 2. 分类器计算梯度目标是增大 log p(y | x_t) x.requires_grad_(True) logits classifier(x, t) log_prob torch.log_softmax(logits, dim1)[0, y] classifier_grad torch.autograd.grad(log_prob, x)[0] x.requires_grad_(False) # 3. 融合引导梯度与噪声预测 # 数学上等价于对条件分数函数做组合 eps_guided eps_pred - guidance_scale * sigma_t * classifier_grad # 4. DDPM 采样更新 mean (1 / alpha_t**0.5) * ( x - (beta_t / (1 - alpha_bar_t) ** 0.5) * eps_guided ) if t 0: noise torch.randn_like(x) * sigma_t else: noise 0 x mean noise return x代码逻辑一共四段每一段都有讲究。第一段预测噪声量这个不解释跟普通采样完全一样。第二段是分类器引导的核心你需要注意x.requires_grad_(True)这一句它让输入张量有梯度然后通过torch.autograd.grad(log_prob, x)直接求出目标类别的对数概率对输入噪声图的梯度。这一步是用 PyTorch 做任何自定义引导的核心操作熟了这个套路以后做图像编辑、属性操控都能用上。第三段把两个信号融合。注意eps_guided eps_pred - guidance_scale * sigma_t * classifier_grad中的减号它跟前面公式的推导是一体的。扩散模型预测的是噪声 ε和分数函数的符号正好相反所以当我们要在分数函数里加分类器梯度时反映到噪声预测上就要减掉对应的梯度项。这个方向搞反生成结果会往目标类别的反方向漂。第四段就是 DDP M的更新公式甚至连随机噪声的加入都和普通采样一致。你会发现带不带引导采样的基本骨架没有变变的就是第三步怎么融合额外的梯度信号。4.2 参数怎么调从零开始定 guidance scale简化版的代码跑起来之后你需要面对的第一个问题就是 guidance scale 到底给多少。我的经验是不要指望一次成功而是做一个小范围的扫描分别取 s 0.5、1.0、2.0、4.0、8.0每组固定随机种子采样几十张图同时观察两个指标——生成图像的分类置信度用另一个干净的验证分类器去评估和图像的主观质量。s 太小时比如 0.5图像质量和无条件生成差别不大分类准确率提升有限因为引导力度太弱。s 在 2 到 4 之间时通常是“质量提升明显但还没怎么牺牲多样性”的甜点区。s 超过 8 之后图像会出现明显的伪影、色块和过度锐化同时多样性暴跌——不同随机种子生成的结果越来越相似。还有个被忽视的规律采样的总步数越少每步的引导强度对最终结果的影响就越大。如果你用的是 20 步快速采样那么同样的 s2.0 可能就比 1000 步采样时表现出更强的引导效果。这是因为更少步骤意味着每一步的方向修正权重更高。从快速实验起步时我建议先用较多样步数把引导行为摸清楚再逐步降采样步数并重新校准 s。4.3 分类器梯度裁剪稳定性的一层保险玩过一段时间后你会遇到一个恼人的现象梯度在某些时间步上突然爆掉导致生成的图像直接变成乱七八糟的噪声块。原因在分类器在极端噪声下输出的梯度幅值可能非常不稳定或者某个特定类别在分类器的决策边界上梯度异常陡峭。一个有效且几乎零成本的解决办法是对分类器梯度做裁剪。具体操作是这样的先算出梯度在整个批量上的范数如果范数超过某个阈值就按比例缩放。我常用的策略是把梯度范数限制在固定大小附近如果超过限制就把梯度整体缩回去。加了这个约束后引导过程的稳定性会明显提升代价只是稍微降低一点极端步数的引导强度。grad_norm classifier_grad.flatten().norm() max_norm 1.0 if grad_norm max_norm: classifier_grad classifier_grad * (max_norm / grad_norm)实际项目里这个阈值本身也可以调。你把阈值设得越紧生成越稳定但引导效果也越弱设得太松等于没加保护。从 1.0 起步根据生成质量做一到两轮微调基本就能找到一个合适的平衡点。4.4 从DDPM扩展到DDIM一个容易踩的坑DDPM 采样需要很多步速度慢。DDIM 可以大大减少采样步数很多人会想把分类器引导直接架在 DDIM 上。这里有个细节值得说清楚。在 DDIM 的更新公式中随机噪声项被移除或大幅减小它所对应的方差项 σ_t 就等于 0。而前面我们看到DDPM 里分类器梯度是通过 mean 加上一个与 σ_t² 成正比的项参与采样的。DDIM 采样器的方差归零直接套用同一个公式分类器引导就失效了。正确的做法是重新推导 DDIM 版本的引导项。一种常见的实现思路是把分类器梯度直接加在从当前 x_t 推导出的预测干净图像 x_0_pred 上或者把它当作一个独立的偏移合并进去噪方向而不是沿用 DDPM 那套与采样方差挂钩的配比。很多框架的实现里都会有一个专门针对采样器类型的引导分支你换采样器的时候不要只改循环体要连引导项的系数一起检查。这个坑的教训是在做引导相关的工作时不要理所当然地认为“换个采样器只是换个更新公式”分类器引导的系数和采样器的方差定义是耦合在一起的。最稳妥的做法是把你使用的采样器公式在纸上推导一遍找到分类器梯度应该放在哪个位置、乘什么系数再动手改代码。5. 常见问题与排查技巧实录5.1 生成图像变模糊、局部崩坏怎么办症状是这样的加了分类器引导之后图像整体结构还是对的但细节很差边缘发毛某些区域有明显的伪影。如果你确认采样器本身没问题那八成就是 guidance scale 太大分类器梯度在局部区域作用过强把像素值推到了不合理范围。处理办法很简单降低 s或者给分类器梯度增加一个时变的衰减权重。我自己的做法是让引导强度随采样进度衰减——前面几步用较强的引导快速锁定大类别方向后面几步逐步降低引导权重让扩散模型有更多自由去恢复细节。这样做出来的图类别保真度和细节质量都能兼顾。5.2 分类器概率饱和导致梯度消失分类器训练得越好它可能在带噪图上越早地对某些类别给出高置信度。一旦 log p(y|x_t) 接近 0它的梯度幅度就会变得非常小分类器引导等于自动关闭了。这就是概率饱和问题。一个实用的对策是给分类器的对数概率加一个温度系数。比如把 logits 除以一个大于 1 的温度再算 log softmax这样即使分类器很自信梯度也不会过早消失。温度系数的选择和引导强度类似需要做很小范围的实验。另一个思路是保证分类器的时间步条件注入是有效的——分类器必须能从时间步信息判断当前噪声级别并据此调整置信度否则它在高噪声区也保持高置信度那同样会出问题。5.3 分类器过拟合到特定噪声级别训练噪声分类器时如果时间步采样分布不均匀比如某个时间区间采得太多分类器就会在那个噪声级别上表现得体在其他噪声级别上近乎失效。表现出来就是你用某个采样步数配置引导效果良好换一个采样步数配置就完全失控。解决思路是训练时对时间步做全局采样同时给每个 t 设定一个最小和最大的采样概率。我还会刻意在验证阶段观察分类器在不同噪声级别下的准确率曲线。如果这条曲线在中高噪声段下跌得比预期快就该调整训练数据里对应的噪声区间占比了。5.4 与Classifier-Free Guidance怎么选分类器引导和CFG并不是替代关系而是同一思想的两个变体。分类器引导用外部分类器提供梯度CFG则用同一个条件模型在有无条件提示下的输出差异来构造梯度方向。CFG的最大优点是不用额外训练分类器因此在文本生成的大规模场景里成了默认做法。但分类器引导并没有过时。如果你是做属性控制手头已经有一个很成熟的分类器或者你只有无条件的扩散模型没有资金和时间去微调条件版本那分类器引导就是性价比极高的方案。另外像多模态引导、医疗图像分割引导这类场景外部网络提供的梯度往往比文本嵌入更容易得到分类器引导的思路依然适用。我个人在实际操作中的一个体会是分类器引导的工程门槛在于“训练一个能在噪声域工作的可靠分类器”一旦这一步解决了整个系统非常灵活你可以随时换分类器、随时改目标类别甚至可以用多个分类器梯度叠加实现多属性约束。如果只是想要稳、想要快又正好有条件模型那直接上CFG绝对更省心。最后再分享一个实操小技巧如果你打算长期在某个领域使用分类器引导一定要把分类器和生成模型的数据分布对齐问题纳入标准检查项并且给每个实验记录当前使用的采样器、步数、引导系数和分类器训练分布。我踩过几次坑之后发现这类引导类方法的失败绝大多数不是数学错而是输入分布、系数缩放、时间步对齐这些“看似不起眼”的细节没对上。把这些变量管好剩下的事情就简单了。