扩散模型这两年几乎成了图像生成的默认答案,各种教程、开源实现、产品落地铺天盖地。但如果你真的顺着文献往前翻,会发现在DDPM之前还有一个绕不开的节点——NCSN。它不像后来的连续时间框架那样一上来就摆出随机微分方程,也不像DDPM那样有大量现成代码可以照抄,它更像是把"生成"这件事的视角整个调转了一次:从"估计数据长什么样"改成"估计数据往哪个方向变"。我身边不少同学第一次读NCSN的推导,普遍卡在三个地方:分数函数到底是个什么东西、为什么不能直接拿真数据去拟合分数、以及退火朗之万采样里那一串sigma和步长是怎么定出来的。这篇就按这三个卡点往下捋,把NCSN的理论脉络、多噪声尺度的网络设计、损失权重、采样流程和可复现的实现细节讲透,顺带把它和后来DDPM的对应关系掰开说清楚。适合已经了解生成模型基本概念、正在啃扩散理论、并且想自己动手跑一版的读者。
1. 为什么单独拎出NCSN来讲
1.1 生成模型的三条技术路线与NCSN的位置
把生成模型按"建模对象"分类,大致能分成三类:一类直接建模从噪声到样本的映射,靠对抗博弈或者变分下界来训练;一类显式建模数据的概率密度,比如自回归模型逐像素分解联合分布,或者标准化流用可逆变换把简单分布搬到数据分布上;还有一类不碰密度本身,只关心密度的梯度场,NCSN就是这一支的开山之作。这个分类不是学术上的洁癖,它直接决定了你训练时要用什么损失、采样时要用什么算法、以及会踩什么样的坑。
前两类路线的共同点是都要面对归一化常数。自回归和流模型靠精心设计的结构让归一化常数可计算或者可消掉,代价是结构约束很强,图像上要么采样慢,要么表示能力受限。变异类方法则用一个下界替代真实似然,优化目标和最终指标之间有偏差。NCSN换了个思路:既然归一化常数只和整体缩放有关,而采样需要的是方向信息,那索性把这个常数扔掉,只学对数密度的梯度。这一个转身,把很多原本棘手的计算问题直接绕开了。
我个人的判断是,理解NCSN的意义不在于它当时的生成质量有多高,而在于它确立了"分数匹配 + 多噪声尺度 + 退火采样"这套组合拳。后来的扩散模型,本质上是在这套骨架上把离散的噪声尺度换成连续的时间变量,把朗之万采样换成逆向随机微分方程或者概率流ODE。骨架没变,只是穿的衣服更合身了。
1.2 这篇文章要解决的具体疑问
标题里写的是"理论篇(二)",说明前面已经铺垫过扩散模型的大图景。那这一篇的任务就很明确:把NCSN的数学推导讲清楚,把网络设计的选择讲明白,把采样参数的经验值讲到位。我在实际带人复现的过程中发现,理论推导看懂和代码跑通之间还隔着一段路,这段路上最常见的问题不是公式不会推,而是不知道该把哪个量当成网络输出、损失里的权重为什么要乘sigma平方、采样时的步长和噪声尺度怎么对应。
所以下面的内容会分成几条线并行推进:一条是纯理论线,从分数函数的定义推到去噪分数匹配的目标函数;一条是工程线,讲网络怎么参数化、损失怎么实现、采样怎么循环;还有一条是对照线,把NCSN和后来的扩散模型放在一起比,让你看清楚哪些设计被继承、哪些被替换。三条线交织着走,读完之后你应该能自己写出一个能跑通的NCSN,并且知道每个超参数的来由。
1.3 阅读这篇文章需要的前置知识
不需要你有很深的测度论功底,但几件事最好心里有数:多元高斯分布的对数密度形式、链式法则和分部积分、随机梯度下降的基本流程、以及卷积网络的大致结构。如果对"能量函数""配分函数"这类统计物理词汇有印象,理解起来会更顺,因为分数匹配本质上和能量模型是同一套语言。没有也没关系,文中涉及的地方都会有直白的解释。
另外要提醒一句,理论推导里的期望、梯度、散度这些符号,第一次看容易糊在一起。我的建议是拿张纸,把每个符号是标量、向量还是矩阵标清楚,尤其是雅可比矩阵和它的迹,这是后面推导的关键。很多人在这一步放弃,不是数学难,是符号没理清。
2. 分数匹配:绕开归一化常数的关键一步
2.1 分数函数的定义与几何直觉
给定一个定义在连续空间上的概率密度p(x),它的分数函数定义为对数密度对输入的梯度:s(x) = ∇_x log p(x)。注意这个量是向量,维度和数据维度一样,每个分量表示"在这个方向上稍微动一下,密度的对数会怎么变"。如果把它想象成一张地形图,密度高的地方是山峰,那么分数函数就是每一点上指向"上坡方向"的箭头,箭头的长短反映坡度陡不陡。
这个几何图像非常有用。它告诉你两件事:第一,分数函数不关心山峰的绝对高度,只关心形状,因为对数里如果有常数因子,求梯度就没了;第二,只要你知道了每一点的箭头方向,你就可以顺着箭头往上爬,从任意起点走到高密度区域。生成样本这件事,在分数函数的语言里就变成了"从随机起点出发,沿着分数场往上走"。这比"从概率密度里采样"要具体得多,也更容易用数值方法实现。
需要注意的是,分数函数在密度趋近于零的地方会变得不稳定,因为对数在零点附近发散。这不是小问题,它直接导致了后面必须引入噪声尺度。数据分布往往集中在低维流形附近,流形之外密度极低,那里的分数估计会非常离谱,采样一旦跑偏就回不来。这个坑在单尺度方案里几乎是致命的。
2.2 朗之万动力学:用分数场做采样的经典工具
有了分数场,采样就可以用朗之万动力学来做。离散形式的更新规则是:x_{t+1} = x_t + (ε/2) * s(x_t) + sqrt(ε) * z_t,其中z_t是标准高斯噪声,ε是步长。这个式子的两部分含义很清晰:前半部分沿着分数方向走一小步,把样本往高密度区推;后半部分加一点随机扰动,防止样本全部塌缩到众数上,保证采样的多样性。
这个更新规则的理论保证是:当步长趋于零、迭代次数趋于无穷时,样本的分布会收敛到目标分布p(x)。这是一个很强的结论,它意味着只要分数估计得准,采样就不需要知道归一化常数。但"步长趋于零、迭代无穷"在实际中做不到,步长太大会导致离散化误差甚至发散,步长太小则采样慢得没法用。这个矛盾在后面讲退火采样时还会遇到,是调参的主要战场。
这里有一个容易被忽略的细节:朗之万动力学对分数场的误差非常敏感,尤其在低密度区域。如果分数估计在某个区域偏了一个常数,采样就会系统性地往错误方向漂移,最终分布整体偏移。这种误差不像监督学习里的分类错误那样局部化,它会通过迭代累积放大。所以分数估计的准确性要求比一般回归任务高得多。
2.3 显式分数匹配为什么走不通
最直接的想法是:既然要拟合s(x),那就构造一个网络s_θ(x),最小化它和真实分数之间的平方误差:J(θ) = (1/2) * E_{p(x)} [ ||s_θ(x) - ∇_x log p(x)||^2 ]。这个目标看起来人畜无害,问题是右边那个真实分数根本不知道。你手里只有样本,没有密度的解析形式,∇_x log p(x)算不出来。
有人会想,能不能用核密度估计先估一个密度再求梯度?理论上可以,但高维空间里核密度估计的收敛速度是指数级慢的,几百维的图像数据完全不可行。也有人想用有限差分近似梯度,但那需要知道密度的值,又绕回去了。这条路在实践上是堵死的。
不过这个目标函数不是完全没用。对它做一次展开和分部积分,可以把含有未知分数的项消掉,换成一个只依赖网络本身散度的形式。这是分数匹配的经典技巧,也是整个NCSN理论的第一块基石。下面把这一步推导写清楚,因为它解释了后面所有变体的动机。
2.4 分部积分:把未知量换成可计算的散度
把平方项展开:||s_θ - ∇log p||^2 = ||s_θ||^2 - 2 * s_θ·∇log p + ||∇log p||^2。最后一项和参数无关,优化时可以丢掉。中间那项可以写成-2 * Σ_i s_θ,i * (1/p) * ∂p/∂x_i,注意∇log p = (1/p) * ∇p。把它对p(x)dx积分,得到-2 * Σ_i ∫ s_θ,i * ∂p/∂x_i dx。
对每一项做分部积分,把导数从p挪到s_θ上,边界项在密度衰减到零时消失,于是这一项变成2 * Σ_i ∫ p * ∂s_θ,i/∂x_i dx = 2 * E_{p(x)} [ tr(∇_x s_θ(x)) ]。整理之后,目标函数变成J(θ) = E_{p(x)} [ tr(∇_x s_θ(x)) + (1/2) * ||s_θ(x)||^2 ] + 常数。
这个结果的意义很大:目标函数里只剩网络自己,不需要真实分数。但代价是出现了一个雅可比矩阵的迹,也就是网络输出对输入的散度。在图像这种高维输入下,迹的计算需要反向传播每个输出分量对输入的导数,维度一高就是灾难。所以显式分数匹配虽然理论上优雅,工程上依然跑不动。这个瓶颈直接催生了下一个技巧。
注意:分部积分要求密度在无穷远处衰减得足够快,边界项才能消掉。如果数据分布有紧支撑或者衰减慢,这个假设不成立,推导要打折扣。实际数据基本满足,但心里要有数。
2.5 去噪分数匹配:用加噪绕开雅可比
突破口在于换一个拟合对象。不去拟合原始数据分布p(x)的分数,而是拟合"加了噪声之后"的分布q_σ(x̃) = ∫ p(x) * N(x̃; x, σ^2 I) dx的分数。这个分布是原始分布和高斯核的卷积,它的分数有一个非常好的性质:条件分布q_σ(x̃|x) = N(x̃; x, σ^2 I)的分数是可以解析写出来的,等于-(x̃ - x)/σ^2。
于是可以构造一个新的目标:J(θ) = (1/2) * E_{q_σ(x̃|x) p(x)} [ ||s_θ(x̃) - ∇_{x̃} log q_σ(x̃|x)||^2 ]。这个目标里所有量都是已知的,x从数据里采,x̃用x + σz生成,目标值是-z/σ。不需要雅可比,不需要真实密度,只需要加噪和回归。这个技巧叫去噪分数匹配,它的理论保证是:优化这个目标和优化显式分数匹配目标是等价的(相差一个与参数无关的常数),也就是说你拟合的是加噪分布的分数,而加噪分布的分数在噪声足够小的时候逼近原始分布的分数。
这一步是整个NCSN的发动机。它把一个不可计算的量换成了一个简单的回归任务,而且这个回归任务的标签可以现场生成,连数据标注都省了。我第一次看懂这个推导的时候,感觉像是有人把一扇锁着的门旁边开了个洞——门还是那扇门,但你能过去了。
3. 从单尺度到多尺度:NCSN的网络与损失设计
3.1 单一噪声尺度的两个致命问题
去噪分数匹配解决了可计算性,但留下一个参数:噪声大小σ。这个参数选起来很别扭。选大了,加噪分布和原始分布差太远,学到的分数在场上是"糊"的,采样出来的东西细节全无;选小了,加噪分布大部分区域密度接近零,分数估计极不稳定,而且数据集中在低维流形上,流形外没有样本,网络在那里就是瞎猜。
更根本的问题在流形本身。真实图像数据在高维空间里其实分布在一个低维流形附近,这个流形的体积相对于整个空间可以忽略不计。用高斯噪声扰动时,绝大部分扰动方向是垂直于流形的,这些方向上的密度几乎为零,分数会指向流形内部,量级巨大且方向混乱。单尺度方案在这种区域完全失效,采样一旦跑进去就出不来。
这两个问题指向同一个结论:不同区域需要不同的噪声尺度。远离数据的地方需要大噪声来提供稳定的梯度信号,靠近数据的地方需要小噪声来保留细节。一个固定的σ无法同时满足。这个观察直接导出了多尺度的设计。
3.2 噪声条件网络:让一个模型管多个尺度
解决方案是训练一个以噪声尺度为条件的网络s_θ(x, σ),同时覆盖一组从大到小排列的噪声。网络结构上,σ通过某种嵌入方式注入,常见做法是把它映射成一个向量再拼接到特征图上,或者用条件归一化的方式调制中间层。后者在后来的扩散模型里成了标配,NCSN早期版本更多用拼接。
这里有个关键的参数化选择:网络是直接输出分数,还是输出噪声预测再换算?如果让网络直接回归-z/σ,不同σ下目标值的量级差异巨大,从1/σ_L到1/σ_1可能差两个数量级,训练会非常不稳定。更常见的做法是让网络预测单位噪声z,然后分数用s_θ(x, σ) = -z_θ(x, σ)/σ换算。这样目标值始终是标准正态,量级统一,训练稳定得多。这个技巧在DDPM里被完整继承,成了"预测噪声"这个说法的源头。
心得:如果你的网络直接输出分数,记得在损失里做尺度归一化,否则小噪声尺度的样本会主导梯度。判断方法很简单,打印每个尺度下的损失分量,量级差太多就说明参数化有问题。
3.3 损失函数与权重设计
多尺度下的总损失是各个尺度损失的加权和:J(θ) = (1/2) * Σ_i λ(σ_i) * E [ ||s_θ(x̃, σ_i) + z/σ_i||^2 ]。权重λ(σ)的选取不是拍脑袋,它有一个很自然的来源。把每个尺度的去噪分数匹配目标写开,目标值的平方期望正比于1/σ^2,如果不加权,小σ的尺度会因为目标值大而主导训练。取λ(σ) = σ^2正好把这个量级拉平,让每个尺度的贡献大致相当。
这个权重还有一层解释:它等价于让网络去预测噪声z而不是分数,因为σ^2 * ||s + z/σ||^2 = ||σ s + z||^2 = ||z_θ - z||^2。所以"加权分数匹配"和"噪声预测回归"在数学上是同一件事,只是写法不同。理解这一点之后,你看DDPM的简化损失就不会觉得是凭空冒出来的,它就是NCSN损失换了个变量名。
下面给出损失实现的核心代码,注意网络输出的是分数,标签是负噪声除以尺度:
import torch import torch.nn as nn import math def ncsn_loss(score_net, x, sigmas, device): """ x: [B, C, H, W] 干净样本 sigmas: 从大到小排列的噪声尺度张量 """ B = x.size(0) # 每个样本独立采一个噪声尺度,保证各尺度都被覆盖 idx = torch.randint(0, len(sigmas), (B,), device=device) sigma = sigmas[idx] sigma_b = sigma.view(-1, 1, 1, 1) z = torch.randn_like(x) x_noisy = x + sigma_b * z # 网络输出分数 s_theta(x_noisy, sigma) score = score_net(x_noisy, sigma) target = -z / sigma_b # lambda(sigma) = sigma^2,权重与尺度平方对齐 loss = 0.5 * (sigma_b ** 2) * ((score - target) ** 2) return loss.mean()这段代码里有两个细节值得说。一是尺度的采样方式,每个 batch 里的样本独立采尺度,这样一次前向就能覆盖多个尺度,梯度方差也比整批用同一个尺度要小。二是mean()的位置,它把 batch、通道、空间维度一起平均,所以权重sigma^2在平均之后仍然保留了相对关系,不会被抵消。
3.4 噪声尺度的选取策略
尺度集合通常取几何级数:σ_i = σ_1 * (σ_L/σ_1)^{(i-1)/(L-1)},其中σ_1是最大尺度,σ_L是最小尺度,L是尺度个数。常见配置是σ_1 = 1,σ_L = 0.01,L = 10。为什么这么取?
最大尺度的选择依据是数据点之间的典型距离。如果σ_1太小,加噪之后不同样本的分布还是分离的,网络学到的分数在样本之间是断开的,采样无法从一个模式跳到另一个模式;如果太大,所有样本被糊成一团,细节信息丢失。经验上取能覆盖数据整体尺度的值,图像归一化到[-1, 1]之后取1比较合适。
最小尺度的选择依据是数据本身的精细结构。太小的话,加噪分布和原始分布几乎一样,又回到了流形退化的问题;太大的话,最终生成的样本会偏糊。0.01这个量级在图像上大致对应像素级噪声,再小就没什么收益了。
尺度个数L影响的是采样的平滑程度。L太少,相邻尺度之间的间隔大,退火过程中分布跳变剧烈,采样容易失稳;L太多,计算量线性增长,收益递减。十个左右的尺度在多数图像任务上是够用的,如果分辨率高或者数据复杂,可以加到几十。
| 参数 | 典型取值 | 选取依据 | 调大/调小的后果 |
|---|---|---|---|
| 最大尺度 σ_1 | 1.0 | 覆盖数据整体跨度 | 太小模式间不连通,太大细节丢失 |
| 最小尺度 σ_L | 0.01 | 匹配像素级噪声 | 太小退回流形退化,太大生成偏糊 |
| 尺度个数 L | 10 | 退火平滑度与算力平衡 | 太少跳变失稳,太多收益递减 |
| 损失权重 | σ^2 | 拉平各尺度目标量级 | 不加权则小尺度主导 |
4. 退火朗之万采样:把噪声尺度串成生成链
4.1 从大尺度到小尺度的采样流程
训练完之后,采样不是用一个固定的分数场跑朗之万,而是从最大的尺度开始,依次往小尺度退火。流程是:先用σ_1对应的分数场跑若干步朗之万,得到一个粗糙但全局合理的样本;然后换到σ_2,在刚才的结果上继续跑;一直降到σ_L,最后得到精细样本。
这个流程的直觉是"先定大局,再修细节"。大尺度下分数场平滑,样本能从任意初始点被推到数据的主要区域;小尺度下分数场陡峭,负责把样本精修到流形上。如果直接从最小尺度开始,初始的随机噪声离数据太远,分数场在那里没有定义良好的方向,采样会乱跑。
这个思想和图像处理里的多分辨率金字塔、优化里的模拟退火是一脉相承的。我在调参时发现,退火顺序一旦搞反,生成结果会明显变差,表现为样本整体偏移或者出现大片伪影。顺序不能乱,这是硬约束。
4.2 步长与信噪比的对应关系
每个尺度下的朗之万步长不能固定,要跟着尺度走。常用的设置是ε_i = α * σ_i^2 / σ_L^2,其中α是一个全局系数,典型取值在0.1到0.3之间。这个式子的理由是:步长要和分数场的量级匹配,而分数场的量级大约正比于1/σ,更新项ε * s的量级正比于ε/σ,要让这个量级在不同尺度下保持一致,ε就得正比于σ^2。
更严格的分析要看信噪比。朗之万更新里,漂移项(沿着分数走)和扩散项(随机扰动)的比值决定了采样的行为。信噪比定义为漂移项模长的平方除以扩散项的方差,代入之后大致是ε * ||s||^2 / 4。如果信噪比太低,随机扰动占主导,样本就是在乱走;太高则退化成梯度上升,样本会塌缩到众数。α的取值本质上是在调这个比值。
注意:步长设置不当是采样失败最常见的原因。如果生成结果全是噪点,多半是步长太大导致离散化发散;如果生成结果模糊且重复,多半是步长太小或者迭代步数不够。
4.3 采样实现与关键细节
下面是一个可以直接参考的采样实现,注意网络输出是分数,但内部可能以噪声预测的形式参数化,这里假设网络直接输出分数:
import torch import math @torch.no_grad() def annealed_langevin_sample(score_net, sigmas, shape, steps_per_sigma=100, alpha=0.1, device='cuda'): """ sigmas: 从大到小排列的噪声尺度 shape: (B, C, H, W) """ x = torch.randn(shape, device=device) * sigmas[0] for i, sigma in enumerate(sigmas): # 步长与尺度平方成正比 step = alpha * (sigma ** 2) / (sigmas[-1] ** 2) sigma_batch = torch.full((shape[0],), float(sigma), device=device) for t in range(steps_per_sigma): z = torch.randn_like(x) score = score_net(x, sigma_batch) x = x + 0.5 * step * score + math.sqrt(step) * z return x这段代码里,初始样本用最大尺度的噪声初始化,而不是标准正态。原因是最大尺度本身就对应一个很宽的分布,用它的尺度初始化能让起点落在分数场有效的区域内。如果把初始值设成单位方差,而σ_1远大于一,起点就在太远的地方,前几步的分数估计不可靠。
steps_per_sigma的取值影响质量与速度的权衡。每个尺度跑一百步左右是常见配置,尺度多的时候可以适当减少。实践中可以先用较少的步数看整体轮廓,确认没问题再增加步数精修。
4.4 采样过程中的数值稳定性处理
朗之万迭代对数值误差比较敏感,尤其是分数值在低密度区可能很大。有两个实用技巧:一是对分数做裁剪,把模长限制在一个上限内,防止单步更新过大导致数值爆炸;二是在每个尺度切换时加一点微小的随机扰动,避免样本卡在某个局部结构里出不来。
还有一个细节是最后的输出处理。最小尺度跑完之后,样本里还残留一点噪声,如果直接输出会有颗粒感。可以在最后加几步小步长的更新,或者做一个轻微的平滑。不过要注意别过度处理,否则会把细节抹掉。
5. 横向对照:NCSN与后续扩散模型的关系
5.1 核心概念的对应表
把NCSN和DDPM放在一起看,会发现很多概念是一一对应的,只是换了名字和表述方式。理解这张对应表,能让你在读后续文献时快速定位。
| 概念 | NCSN中的表述 | 扩散模型中的对应 |
|---|---|---|
| 扰动方式 | 加高斯噪声 x + σz | 前向扩散过程,噪声调度 β_t |
| 时间/尺度 | 离散的噪声尺度 σ_i | 连续或离散的时间步 t |
| 网络输出 | 分数 s_θ(x, σ) | 噪声预测 ε_θ(x_t, t) |
| 训练目标 | 加权分数匹配 | 简化噪声回归损失 |
| 采样方法 | 退火朗之万 | 逆向过程 / 概率流ODE |
| 关键权重 | λ(σ) = σ^2 | 损失中的系数,常取 1 |
这张表里最值得琢磨的是"网络输出"那一行。NCSN的原始形式输出分数,但工程上常换成噪声预测;DDPM直接就用噪声预测作为默认参数化。这不是巧合,而是因为噪声预测的数值性质更好,目标值始终是标准正态,训练稳定。后来有些工作又提出预测速度场或者直接预测干净样本,本质都是在同一个框架下换参数化,各有利弊。
5.2 离散尺度与连续时间的差别
NCSN的尺度是离散的一组,采样是分段的。这种设计简单直接,但有两个局限:一是尺度之间的切换是突变的,采样路径不够平滑;二是尺度的数量和间隔需要手动调,缺乏理论指导。连续时间框架把尺度换成时间变量,用一个噪声调度函数β(t)描述噪声如何随时间增长,采样变成求解一个随机微分方程或者常微分方程。
这个推广带来的好处是可以用更成熟的数值方法,比如高阶求解器,用更少的步数达到更好的质量。同时连续框架让理论分析更干净,很多结论可以写成闭式。但代价是概念门槛更高,实现也更复杂。我的建议是先吃透离散版本,再去看连续版本,否则容易被各种符号绕晕。
5.3 实操中容易混淆的几个点
第一个混淆点是"分数"和"噪声"的方向。分数指向高密度区,噪声是从干净样本到加噪样本的位移,两者差一个负号和尺度因子。写代码时符号搞反,训练损失看起来在下降,采样结果却是一团糟。
第二个混淆点是噪声尺度的索引方向。训练时尺度从大到小还是从小到大无所谓,但采样必须从大到小。如果尺度列表的顺序和采样循环的顺序不一致,结果会完全错误。
第三个混淆点是步长公式里的参考尺度。有的实现用σ_L^2做分母,有的用σ_1^2,这会导致步长整体差一个常数倍。两种写法都能工作,但要保证训练和采样用的是同一套约定,混用会出问题。
6. 实操经验与常见坑
6.1 训练阶段的稳定性问题
训练NCSN最常见的现象是损失震荡。原因通常是不同尺度的梯度量级差异大,虽然加了σ^2权重,但如果网络对某个尺度的拟合特别差,那个尺度的损失会持续偏高,把整体梯度带偏。一个实用的做法是记录每个尺度的平均损失,如果某个尺度长期偏高,检查它的噪声水平是否落在数据分布的合理范围内。
另一个问题是批量归一化和小噪声尺度不兼容。批量归一化在训练时用批统计量,推理时用滑动平均,两者不一致会给分数估计引入偏差。在加噪样本上这个偏差会被放大,因为噪声尺度小的时候,样本之间的差异本来就小。用组归一化或者层归一化替代,能缓解这个问题。
还有一个容易被忽略的点是数据归一化。图像如果归一化到[0, 1],最大噪声尺度取1就显得偏大,因为数据的动态范围只有一;归一化到[-1, 1]更合适。这个细节看起来小,但对采样质量影响明显。
6.2 采样阶段的调参顺序
采样调参不要一上来就同时改多个参数,那样你分不清是哪个起的作用。我的习惯顺序是:先固定steps_per_sigma为一个较大的值,比如两百,然后把alpha从0.05试到0.3,找到不发散的最大值;再固定alpha,逐步减少步数,看质量下降到什么程度;最后调尺度个数和范围。
判断采样是否正常,可以看中间结果。把每个尺度结束时的样本保存下来,正常情况下应该看到从模糊色块逐渐变成有结构的图像。如果某个尺度之后突然变成噪点,说明那个尺度的步长太大;如果一直很糊没有细节,说明最小尺度不够小或者步数不够。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 生成结果全是噪点 | 步长过大导致发散 | 把 alpha 降到 0.05 以下重试 |
| 生成结果模糊重复 | 步数不足或最小尺度太大 | 增加步数,把 σ_L 调小 |
| 样本整体偏移 | 分数符号写反 | 检查 target 是 -z/σ 还是 z/σ |
| 某个尺度损失居高不下 | 该尺度噪声与数据尺度不匹配 | 打印各尺度损失,调整尺度范围 |
| 采样中途崩溃 | 低密度区分数值爆炸 | 对分数做模长裁剪 |
| 训练损失不降 | 学习率过大或网络容量不足 | 降学习率,检查参数量 |
6.4 关于评估的一点经验
NCSN这类模型的评估比判别模型麻烦,因为生成质量没有唯一标准。常用的指标包括生成样本的分布距离、以及在一些下游任务上的表现。但这些指标都有局限,尤其是分布距离指标对样本数量敏感,样本少的时候方差很大。
我个人的做法是组合使用:定量指标看趋势,定性观察看细节,再配合中间采样过程的可视化。单看一个数字容易被误导,尤其是当你只跑了几百个样本的时候,指标波动可能比方法差异还大。
还有一个实践中的观察:NCSN在小数据集上容易过拟合,表现为生成的样本和训练集高度相似。这时候增大噪声尺度范围或者增加数据增强会有帮助。大尺度噪声相当于一种正则,它强迫网络学习更全局的结构,而不是记住单个样本。
7. 把理论落到代码时的几个关键决策
7.1 网络骨架的选择
NCSN原始工作用的是类似U-Net的结构,带跳跃连接和归一化层。这个选择不是随意定的:分数场需要同时捕捉全局结构和局部细节,U-Net的多尺度特征正好对应这一点。跳跃连接让浅层的高频信息能直接传到输出,避免细节在深层被抹掉。
如果自己实现,不必完全照搬,但要保证两点:一是网络对输入尺度变化有一定的适应性,二是归一化层的选择要避开批量归一化在小噪声下的问题。组归一化是个稳妥的选择,它在通道维度分组做归一化,不依赖批大小。
7.2 条件注入的方式
噪声尺度怎么注入网络,直接影响条件信息的利用效率。最朴素的做法是把σ映射成一个向量,然后拼接到输入的通道上。这种方式实现简单,但条件信息只在输入层出现一次,深层可能衰减。
更好的做法是在每个分辨率层级都注入条件,用条件归一化的方式调制特征。具体是把σ过一个小的多层感知机,得到缩放和平移系数,作用在归一化之后。这种设计在后来的扩散模型里被广泛采用,效果明显好于单纯的输入拼接。
7.3 训练循环的组织
训练循环本身不复杂,但有几个细节值得注意。尺度的采样要保证每个 batch 内覆盖多个尺度,最好打乱样本和尺度的对应关系。学习率用余弦退火或者带预热的常数学习率都可以,关键是要有梯度裁剪,防止低密度区的大梯度把参数带飞。
日志记录要分尺度统计损失,这是排查问题的主要依据。如果只记总损失,某个尺度的异常会被平均值掩盖。另外建议定期保存采样结果,训练早期就能看出方向对不对,比等到训练结束再发现问题要省时间。
7.4 从理论到实现的落差
理论推导里假设了无限多的样本、无限精确的优化、无限小的步长,实际中这三条都不成立。样本有限导致分数估计有方差,优化不精确导致网络偏差,步长有限导致离散化误差。这三个误差源在采样时会叠加,表现就是生成质量达不到理论预期。
理解这一点很重要,它能让你在调参时不至于怀疑人生。很多看起来是代码 bug 的问题,其实是理论假设和现实条件的差距。遇到问题时先确认实现无误,再去调整那些弥补差距的超参数,比如增加步数、增大尺度个数、延长训练时间。
8. 我在这套理论上踩过的几个坑
第一次复现NCSN的时候,我把网络输出直接当成分数,标签用-z/σ,训练损失降得很好看,采样出来全是灰色噪点。查了很久才发现是权重没加,小尺度样本的梯度淹没了大尺度,网络实际上只学会了最小尺度那一段的分数,而采样初始阶段用的是最大尺度,网络在那里根本没学好。加上σ^2权重之后问题立刻解决。这件事让我意识到,损失函数里的每一个系数都不是装饰。
第二次踩坑是在步长上。我照着某个实现写了ε = α * σ^2,但那个实现的数据归一化到[0, 1],而我的数据归一化到[-1, 1],尺度差了一倍,步长相应也差了一截。结果采样在中间某个尺度突然发散。后来统一了归一化方式,并且把步长公式写成相对于最小尺度的比例形式,才彻底稳定下来。
第三次是关于尺度顺序。我在采样循环里用了reversed(sigmas),但训练时sigmas本身就是从大到小排的,结果等于从小到大采样,生成结果结构混乱。这个错误很低级,但因为代码能跑通、不报错,反而最难发现。后来我养成了一个习惯,在采样函数开头打印一下尺度序列的首尾值,确认方向正确。
这几次经历让我形成一个判断:NCSN这类模型的实现难点不在网络结构,而在那些看起来不起眼的约定——符号方向、尺度顺序、归一化方式、权重系数。这些约定在论文里往往一笔带过,但在代码里错一个就是全盘皆输。所以我现在复现任何生成模型,都会先把这些约定列成一张清单,逐条核对,再开始写网络。
如果你也在啃这一块,我的建议是别急着追求生成质量,先把最小可运行版本跑通,确认采样过程每个尺度的中间结果都符合预期,再去调网络容量和训练时长。理论上的优雅和工程上的稳,中间隔着的就是这些琐碎的约定。把约定理清楚,剩下的就是耐心调参和等待。