扩散模型这两年的存在感,强到几乎成了“生成式AI”的代名词。从2022年Stable Diffusion开源那天起,文生图、图生视频、新视角合成、3D生成一股脑涌进大众视野,但很多人并不清楚,扩散模型并不是2022年才冒出来的。它从最初一个略带物理味道的想法,到今天变成整个AIGC领域的地基,经历了将近十年的演化。这篇博文我打算按自己的理解,把这条路完整捋一遍:从2015年那篇奠基论文讲起,讲score-based模型、DDPM、SDE统一视角、引导机制、潜在扩散模型,再延伸到新视角合成和3D内容生成,最后附上我在实际训练和部署中踩过的坑。
如果你正在学扩散模型、想在项目里用它生成图像,或者只是好奇为什么AI绘图这么好用,这篇内容应该能帮你建立一条很清晰的技术时间线。读完你会明白:为什么加噪会被叫做“扩散”,为什么现在的生成模型都跑到“潜空间”里干活,以及你在各类Demo里看到的新视角合成、3D生成效果,到底是怎么从扩散模型里长出来的。
1. 一切从一个热力学比喻开始:扩散模型的思想原点
1.1 “扩散”两个字到底在说什么
不用公式解释扩散模型的话,我通常会打一个比方:你有一杯滴进墨水的清水,墨水会慢慢晕开,最后整杯水变成均匀的灰色,这个过程就是物理扩散。它的逆过程,是把灰色水重新变回“墨水分明”的状态,几乎不可能自然发生。但如果有一个“神之手”,每一步都能预判颗粒往哪走,理论上也能把水复原。
2015年之前,生成模型界的主流思路并不是这个。GAN当时已经火了好几年,VAE也有一批拥趸。扩散模型的起点,是2015年Sohl-Dickstein等人发表的那篇标题很长的论文《Deep Unsupervised Learning using Nonequilibrium Thermodynamics》。这篇论文最核心的贡献,是把热力学里的“扩散”翻译成了机器学习语言:定义一条前向路径,把数据逐步加噪直到变成纯噪声;再训练一个神经网络,去学习这条路径的逆向过程。训练完成之后,你只要从纯噪声出发,顺着逆向过程一步一步走,就能得到一张新图。
这个思想今天看很自然,但在当时相当超前。它不是去“直接生成数据”,而是去“学一个破坏的反过程”。这等于换了一个提问方式:不要问模型怎么凭空画出一只猫,而是问它怎么把一堆噪声一步步“擦”成一只猫。
1.2 前向过程和反向过程的数学骨架
具体到数学上,前向过程定义一个马尔可夫链。给定一张干净图像x0,每一步按一个预先设计好的方差往里面加一点高斯噪声,经过T步之后得到xT。当步数T足够大,xT基本就是标准高斯噪声,原始信息彻底被淹没。反向过程是训练一个带参数的神经网络,每一步根据当前带噪图像和时间步t,预测这一步该“去掉”多少噪声,逐步把图像还原。
2015年这篇论文里,训练目标写得非常复杂,用了变分下界的框架,形式上类似VAE。作者推导了ELBO,并设计了一系列近似。但问题在于,当时大家发现直接优化这个目标很困难,需要非常大的采样步数和算力,生成质量和同时期的GAN比有明显差距。所以这篇论文虽然把整个框架搭起来了,但它在图像生成上展现出统治力,还要再等五年。
1.3 为什么它“起了个大早,赶了个晚集”
我2019年回头看这篇2015年的论文时,心里最大的疑问就是:为什么这么漂亮的想法没有立刻引爆?原因其实很现实。第一是算力,2015年大规模训练图像生成模型还很吃力。第二是优化目标太复杂,当时的实现包含一连串近似,训练不稳定。第三是高分辨率生成效果差,在ImageNet和LSUN这种大规模数据集上,它的表现远不如GAN那种眼花缭乱的结果。
公平地说,没有2015年这篇奠基之作,就不会有后面一系列“做减法”式的改进。后来的工作几乎都在干同一件事:把复杂的训练目标简化,把采样过程加速,把控制条件加进来。DDPM之所以能成功,本质上就是因为它把2015年那套复杂目标,化简成了“预测噪声”四个字。
2. 现代扩散模型的三个坐标:NCSN、DDPM与SDE统一
2.1 NCSN:用score matching估计“数据分布的梯度”
2019年,Yang Song和Stefano Ermon发表了NCSN(Noise Conditional Score Network)。他们的思路和DDPM那条线不同,不直接建模数据分布p(x),而是估计数据分布的“分数”——也就是对数似然关于输入的梯度,∇x log p(x)。这相当于告诉模型:从当前样本出发,往哪个方向调整,能更快走到真实数据的高密度区域。有了这个分数,就可以用郎之万采样(Langevin dynamics),从随机噪声开始,沿着梯度方向一步步“爬”到真实样本附近。
这里面有个关键工程细节:直接估计高维空间里的分数非常困难,因为低密度区域几乎没有数据,估计方差会爆炸。他们的解法是引入多重噪声扰动。用一系列不同幅度的噪声把数据“抹糊”,在每个噪声级别下分别估计分数,训练时随机抽一个级别。这样做的效果是,模型先在“糊掉”的数据分布上找到大致方向,再逐步收紧到真实分布,这就是annealed Langevin dynamics的思路,也是“噪声条件得分网络”这个名字的来源。
NCSN与DDPM看起来是两套完全不同的理论,但后来证明它们其实是同一个硬币的两面。这个“统一”是2021年才完成的,但对理解扩散模型至关重要。
2.2 DDPM:把一切简化到“预测噪声”
2020年,Jonathan Ho等人的DDPM(Denoising Diffusion Probabilistic Models)是整个扩散模型蜕变的分水岭。我到现在还记得第一次跑通DDPM训练脚本时的感受:训练逻辑简单到难以置信。从数据里抽一张图,随机抽一个时间步t,随机抽一个高斯噪声,把它加到图上,然后让U-Net把这个噪声预测出来,算L2损失。
E_t || ε - ε_θ(x_t, t)||²
意思就是,给定t时刻的噪声图x_t,让网络去预测“刚刚加进去的那个噪声”本身,而不是预测均值、方差。整个训练目标不再需要复杂的ELBO近似,一句话就能说清楚。网络结构用了U-Net,时间步t通过正弦位置编码输入,配合EMA和混合精度训练。结果是在CIFAR-10和LSUN数据集上,FID追平甚至反超同时期的GAN。
这件事的意义非常大。它意味着扩散模型不再是一个“看起来很理论”的存在,而是一个任何有PyTorch基础的人都能在几天内复现的算法。我记得当时社区里很快出现了各种基于DDPM的改进工作,训练脚本也从一行行难懂的推导变成了一套标准流水线。从这时起,扩散模型正式进入了主流实践。
2.3 SDE统一:一转眼,两个门派被收进同一个框架
2021年,Song等人的《Score-Based Generative Modeling through Stochastic Differential Equations》把NCSN和DDPM统一到了一个连续时间视角下。这篇论文我反复读过很多遍,它的核心洞见是:前向加噪过程可以看作一个从数据分布漂移到先验分布的随机微分方程,而逆向过程是另一个“时间反转”的SDE,只依赖于每个时刻的分数。
从这个视角看,NCSN对应的是一类方差爆发的SDE(VE-SDE,variance exploding),DDPM对应的则是方差保持的SDE(VP-SDE,variance preserving)。换句话说,两个看起来理论渊源不同的模型,只是同一个连续过程的两种不同离散化方式。这个统一带来的实际价值有两层。一是理论工具更强了:可以用SDE求解器和概率流ODE来加速采样,DDIM采样器可以看作概率流ODE的一种确定性离散化。二是后续很多改进都建立在这个框架上,“扩散模型”从此不再特指某一篇论文,而是一整个研究范式的名字。
2.1、2.2和2.3这三条线合在一起,构成了现代扩散模型的三个坐标。理解它们之间的异同,再看后面的LDM和Stable Diffusion,会轻松很多。
3. 从“能生成”到“生成得好”:引导机制与工程技巧
3.1 classifier guidance:让扩散模型学会“指哪打哪”
DDPM虽然能生成高质量图像,但有一个致命短板:不好控制内容。你告诉它“我要一只猫”,它不会听你的。2021年Dhariwal和Nichol那篇《Diffusion Models Beat GANs on Image Synthesis》戳中了这个痛点。他们在采样时引入一个分类器的梯度:每一步除了按去噪网络给出的方向走,还额外叠加分类器关于输入的梯度,把样本往“属于目标类别”的方向推一步。
这个操作等价于在采样过程中注入目标类别的对数似然梯度。超参数guidance scale通常取1到10之间,越大类别越一致,但太大的话图像会出现过饱和、不自然的伪影。这篇论文的另一个贡献是证明,只要把模型规模和训练策略做到位,扩散模型在FID这类指标上能全面超过GAN。那年之后,很多做生成的研究团队都开始扭头看扩散方向。
不过classifier guidance有个烦恼:需要额外训练一个能在噪声图上工作的分类器,训练成本不小。我当时试过在ImageNet上训这个分类器,前处理要处理不同噪声级别的输入,工程上相当啰嗦。所以没过多久,无分类器引导就把它取代了。
3.2 classifier-free guidance:把分类器扔掉,条件反而更灵活
Ho和Salimans提出的classifier-free guidance,是扩散模型历史上最优雅的一个小技巧。它的做法简单得令人吃惊:训练条件扩散模型时,以一定概率(比如10%)把条件置空,让同一个模型既学会“看着条件去噪”,又学会“不看条件去噪”。采样时对两种预测做外推:
ε̃ = (1 + w) * ε_cond − w * ε_uncond
其中ε_cond是有条件时的预测,ε_uncond是条件置空时的预测,w是引导强度。这个式子用起来极其灵活,可以和文本、类别、布局、深度图等任意条件组合,而且不需要训练额外的分类器。后来几乎所有文生图模型——DALL-E 2、Imagen、Stable Diffusion——的默认方案都是它。
我自己的体会是,classifier-free guidance让“条件控制”这件事从“专门训练一个判别模型”变成了“同一个模型里抠出一部分参数”。工程上省了一大截,效果还更好。唯一要注意的是训练时条件dropout的比例不能太大,否则模型会过度忽略条件;太小则无条件的预测不靠谱,外推会失效。10%是一个普遍好用的默认值。
3.3 采样提速与训练稳定性:从“1000步”到“几十步”
我最早用DDPM生成一张512x512图像,要跑满1000步采样,一张图要几十秒,放到服务里完全不可用。后来换用DDIM,在50步以内就能得到几乎不损失质量的结果,推理速度提升了一个量级。原理上,DDIM对应概率流ODE的确定性离散化,去掉了朗之万采样中的随机项,因此同样步数下更稳。
再往后,DPM-Solver和DPM++这类基于ODE的采样器进一步把步数压到10到20步。现在你打开diffusers库,scheduler列表里有一堆选项,最常用的就是PNDM、DDIM、DPM-Solver和Euler。如果你在部署文生图服务,我的建议是优先用DPM-Solver配10到20步,质量、速度、稳定性综合最优。
训练稳定方面,EMA几乎必须开。EMA相当于把历史权重做了平滑,会让训练过程中的样本质量震荡明显下降。我自己在训练UNet时见过太多次“上一轮loss在降,这一轮突然崩了”的情况,开EMA之后这种现象少很多。混合精度fp16也是标配,但要注意梯度缩放和某些层在fp16下的数值稳定性,后面第6节我会把坑写细。
4. 压缩进潜空间:LDM与Stable Diffusion的“破圈”之路
4.1 像素域扩散为什么撑不起大规模应用
DDPM在64x64、128x128上很惊艳,但要上512x512甚至1024x1024就非常吃力。原因很直接:扩散模型每一步都在处理“整个图像张量”,像素域维度高,U-Net每一层的计算极其沉重,显存和算力都吃不消。而且文本条件要注入到每一层,到了高分辨率,计算量更是雪上加霜。
一个自然的想法是:不要直接在图像空间做扩散,先拿一个自编码器把图像压缩到低维潜空间,只在潜空间里做去噪。这就是Latent Diffusion Models(LDM,潜在扩散模型)的核心思想。它来自Rombach等人2021年底提交的论文,2022年在CVPR发表。这条路线直接促成了Stable Diffusion的出现。
4.2 LDM三个组件与那个容易忽视的缩放系数
LDM由三块组成:第一块是感知压缩自编码器,把512x512x3的图像编码成64x64x4的潜变量;第二块是一个U-Net扩散模型,在潜空间里完成加噪去噪;第三块是条件注入机制,文本或其他模态通过交叉注意力(cross-attention)与U-Net的中间特征交互。训练分两个阶段:先训自编码器,再固定编码器训潜空间扩散模型。
因为潜空间维度只有原来的差不多1/48,扩散模型跑起来轻松非常多,也更容易扩展到高分辨率。但我必须提醒一个很容易踩的坑:潜空间的scale。训练LDM时一定要让潜变量的标准差保持在一个合适范围,否则扩散训练会不稳定。官方实现里会对编码器输出乘一个缩放系数,例如Stable Diffusion v1.x里大约是0.18215,目的就是让潜空间分布接近标准正态。很多复现LDM的人忘了这一层,loss虽然能降,但生成的图总是发糊。
4.3 从LDM到Stable Diffusion,再到整个AIGC生态
Stable Diffusion其实就是LDM在文本生成图像场景下的开放版本。2022年8月开源之后,扩散模型的能力被释放到了整个社区,后面长出来一整棵技术树:LoRA用少量数据微调风格,ControlNet用深度图、边缘图做结构控制,DreamBooth做个性化定制,以及各种基于SD的插件式工作流。可以说,没有LDM把扩散模型从像素域搬到潜空间,以当时的GPU算力,全球用户“人人能跑文生图”根本不可能。
印象最深的是,SD刚开源那阵,一台消费级显卡用fp16就能出图,这在一年前根本不敢想。这背后是潜空间压缩、U-Net降采样、classifier-free guidance三者合力的结果。你要理解现在的扩散模型生态,LDM这条线索是必须吃透的。
5. 扩散模型的新战场:新视角合成与3D内容生成
5.1 新视角合成是什么任务,为什么需要扩散模型
新视角合成,粗浅来说就是“给你一张或几张同一个物体的照片,你帮我画出从另一个角度看它是什么样子”。传统做法依赖多视图几何和NeRF这类神经渲染,需要采集大量多视角照片,再用体渲染显式重建3D场景。但如果只给你一张图,怎么猜出背面长什么样?这是一个极度不适定的问题,必须依靠先验知识。
扩散模型的价值正在于它是一个极其强大的图像先验。2022年底的Zero-1-to-3做了一件非常直观的事:把输入图像和一个相对相机位姿变化(旋转矩阵加平移向量)一起作为条件,训练扩散模型去生成新视角下的画面。推理时,给定一张图和一个目标姿态,模型直接输出对应视角图像,不需要测试时的任何优化,也不需要场景重建。扩展到广义场景时,同一个物体只要见过足够多角度的训练数据,它就能生硬地“脑补”出背后的样子。
我把这个例子放在这里,是因为它是理解“扩散模型不只是文生图”的最好入口。它的条件不是文本,而是“相机变换”;它的输出不是“符合文本的图”,而是“符合视角变化的图”。同一个生成底座,换一个条件编码方式,就变成另一个任务。
5.2 从2D扩散到3D生成:SDS loss与可微渲染结合
比新视角合成更进一步的,是直接从文本或单张图生成完整3D模型。2022年的DreamFusion提出了Score Distillation Sampling(SDS)。它的思路是:用一个预训练2D扩散模型当“视觉先验”,监督一个可微渲染的3D表示(通常是NeRF一类)。每次迭代,从当前3D表示渲染一幅图,把图加噪后丢给扩散模型估计分数,得到“渲染图和目标语义之间差异”的梯度,再反向传播去优化3D参数。
本质上,扩散模型被当成一个无所不知的图像先验,用它把2D生成能力“蒸馏”进3D表达。后来很多工作都沿着这条路走:用SD做先验、用LoRA控制风格、用SDS加各种正则约束,几分钟就能从文本生成一个可以环绕查看的3D模型。虽然它在几何精度上不如传统三维重建,但那种“从无到有创造3D资产”的能力,对游戏、影视、电商行业的影响是实打实的。
5.3 视角合成与3D生成其实是同一套零件
值得玩味的是,这两个方向底层用的是同一套零件:预训练扩散模型、相机姿态表示,以及把扩散先验和可微渲染结合的数学框架。区别只在于任务是“预测图像”还是“优化三维表示”。这种“换任务不换框架”的特性,让扩散模型成为通用生成底座。如果你已经会用diffusers、理解了U-Net和cross-attention,那再往新视角合成、SDS训练走,并没有想象中那么高的门槛。
6. 动手实践:把扩散模型跑起来,以及我踩过的那些坑
6.1 环境准备:diffusers一行安装
理论聊完,落地才是硬道理。Hugging Face的diffusers是目前最省事的起点。建议环境是Python 3.10、PyTorch 2.x、CUDA 11.8以上。安装就一行:
pip install diffusers transformers accelerate想直接体验Stable Diffusion的话,官方代码少得感人:
from diffusers import StableDiffusionPipeline import torch pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16 ).to("cuda") image = pipe("a cat sitting on a windowsill, golden hour").images[0] image.save("cat.png")第一次运行会下载权重,建议保留好本地缓存。需要注意的是,fp16推理会比fp32快不少,显存占用也少一大截,但某些显卡上输出会偶发黑色图片,多半是精度或缓存问题。
6.2 自己训练一个最小DDPM
如果不想只当使用者,可以跑一个极小的DDPM训练。diffusers官方示例脚本train_unconditional.py就是干这个的:
accelerate launch train_unconditional.py \ --dataset_name hf-internal-testing/demo_image_dataset \ --output_dir ddpm-demo \ --train_batch_size 16 \ --num_epochs 50 \ --mixed_precision fp16 \ --learning_rate 1e-4脚本会自动把图片缩放并归一化到[-1,1]。默认用的线性beta schedule是从0.0001到0.02,共1000步。这个区间为什么这么选?太小的话前向加噪不够,反向学不到东西;太大则一步噪声就把信号盖掉,网络难以还原。这组数值是DDPM论文里反复试出来的,后来模型喜欢用cosine schedule,曲线更平缓,实测更稳。
如果你想加classifier-free guidance,需要在训练脚本里做两件事:第一,把条件用一个embedding层编码;第二,在喂条件时按照10%的概率随机置零。采样时再按那个外推公式算最终预测。我自己踩过一次坑:条件dropout概率设了30%,模型学出来的无条件分支过于强势,导致带条件分支被压制,生成图像和文本对不上。这个比例不是越大越好。
6.3 常见问题排查速查表
我把训练和部署扩散模型时最常见的几个问题整理成了一张表:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| loss发散或出现NaN | 学习率过大,或fp16未正确使用梯度缩放 | 把学习率降到1e-4以下,开启默认的GradScaler |
| 生成图像全灰或全是噪声 | 未开EMA,或训练步数太少 | 开启EMA,拉长训练时间 |
| 样本质量高但多样性差 | guidance scale过高,或采样步数太少 | 把w降到7.5附近,用DDIM或DPM-Solver跑20步 |
| 图像出现紫绿噪点伪影 | fp16下U-Net的GroupNorm精度不稳 | 关键层回退fp32,或直接使用bf16 |
| 512x512训练显存溢出 | batch size过大 | 减小batchsize,用梯度累积,或改用LDM潜空间路线 |
还有一个非常实用的经验:训练的时候,最好每次验证采样都使用EMA权重。带EMA的权重通常比当前step的权重稳定得多,很多官方checkpoint其实都是EMA版本。我自己跑实验时,会在每个epoch末尾用同一组随机种子对比“当前权重”和“EMA权重”的输出,效果差距肉眼可见。
6.4 一点实操心得
我在刚接触扩散模型时,最痛苦的不是看不懂公式,而是“理论框架”和“训练手感”之间总是对不上。后来发现最好的学习路径是倒着来的:先跑通一个最小demo,再一步步把加噪、去噪、引导、潜空间这些概念对应到代码里。等你把DDPM训练脚本从外到内改过一遍,再回头看LDM、SDS这些进阶方向,基本就是顺水推舟。
如果你只是做效果演示,我劝你别一上来就复刻完整LDM训练——那需要多阶段训练和大算力。合适的路径是先在小数据集上把DDPM训通,再用diffusers里的SD做微调。扩散模型这十年走过的路,其实就是在反复证明一件事:把一个简单的想法做到极致,比一开始就追求复杂更管用。