“从数据造噪声很容易;从噪声造数据,才叫生成模型。”
这是 Song 等人那篇经典论文(arXiv:2011.13456)的开场第一句,我认为它是关于扩散模型最好的一句话总结。
你大概听过这样的解释:“扩散模型就是先给图片加噪声,再训练模型把噪声去掉。“这句话没错,但它解释不了最关键的一件事——去噪听起来只是把脏图擦干净,那它怎么能”擦”出一张世界上从来不存在的图?
这篇就死磕这一个问题。全程不需要你会随机微分方程,最后会给你一个不用装任何库、复制粘贴就能跑的小例子,让你亲手看着噪声变成数据。
名词速查
| 术语 | 一句话解释 |
|---|---|
| 生成模型 | 会”造”新样本的模型(造图、造文),相对于只会”判断”的模型(这是猫还是狗) |
| 前向过程(加噪) | 把干净数据一步步弄脏,直到变成纯噪声。这一步不需要学,是人为规定的 |
| 反向过程(去噪) | 从纯噪声一步步还原成数据。这一步才是神经网络要学的东西 |
| 去噪器 | 那个神经网络。输入”脏数据 + 有多脏”,输出”我猜干净的原样是什么” |
| 采样步数 | 从噪声走到成品用了几步。步数是速度和质量的旋钮 |
| 分数(score) | 专业叫法,直觉就是”往哪个方向走更像真实数据”的箭头 |
一、先说清楚”生成”到底难在哪
要理解扩散的聪明之处,得先感受一下它绕过的那个难题。
“生成一张图片”用数学讲就是:从一个概率分布里随机抽一个样本出来。 这个分布是”所有看起来像真实照片的图片”的分布。
难点在于这个分布长得极其古怪。想象一张 256×256 的彩色图,它有 256×256×3 ≈ 20 万个数字。这 20 万个数字的所有可能组合构成一个巨大的空间,而”看起来像真实照片”的那些点,在这个空间里稀薄得可怕——你随机填 20 万个数字,得到的一定是雪花噪点,不可能碰巧是一只猫。
所以问题变成了:这个分布你既写不出公式,又没法直接采样。怎么从里面抽样本?
二、核心花招:把一个难问题拆成一千个简单问题
这就是扩散模型的整个思想,一句话:
让你一笔画出蒙娜丽莎,你画不出来;让你画一千笔、每笔只要求比上一笔清楚一点点,你就能画出来。
“从纯噪声直接跳到一张猫”是个难到没法学的任务。但换成这个任务:
这里有一张稍微有噪点的猫,请把噪点去掉一点。
这个任务简单得多——因为图里已经有猫的轮廓了,模型只需要做局部的、小幅的修补。
扩散模型就是把生成拆成了一千个这样的小任务。每一步都不需要聪明,只需要”稍微变清楚一点”。 一千步小聪明叠起来,就完成了那个原本学不会的大任务。
这个”拆步”的思路最早由 Sohl-Dickstein 等人在 2015 年提出(arXiv:1503.03585,标题里那个”非平衡热力学”听着吓人,思想就是上面这段),五年后被 Ho 等人的 DDPM(arXiv:2006.11239)真正跑成了能出高质量图的方法。
三、训练:为什么”加噪声”是个免费的老师
现在问题来了:怎么训练一个”会去掉一点噪点”的模型?训练需要标准答案,标准答案从哪来?
这是扩散最漂亮的地方,也是最容易被讲漏的一步:
标准答案是你自己造的。
流程是这样的:
- 拿一张干净的真实照片(这个有,互联网上一堆)
- 你自己往上加一定量的噪声——加多少你说了算,所以你完全知道加了什么
- 把加噪后的图喂给模型,让它猜”原来那张干净的长什么样”
- 你手里有原图,所以能立刻算出它猜得对不对,然后修正它
看出来了吗?第 2 步是免费的、无限量的标签生成器。 你不需要人工标注,不需要另一个模型当裁判,你想要多少训练样本就有多少——同一张图配不同的噪声量,就是无数个不同的训练样本。
这也解释了扩散模型为什么比 GAN 好训练。GAN 要让两个网络互相博弈,容易崩;扩散的训练目标就是个老老实实的”猜得准不准”,是标准的监督学习。
四、最小的例子:一维世界里的扩散
抽象讲到这儿就够了。现在把整件事缩小到你能用计算器验算的规模。
我们的世界只有一个数字。 数据分布简单到极致:真实数据只有两种,+3 和 -3,各占一半。你可以把 +3 想成”猫”,-3 想成”狗”——这就是我们这个小世界里全部的”真实照片”。
前向加噪:给真实值加上一个随机数,噪声的大小用 σ 表示。σ 很大时,+3 和 -3 被彻底搅浑,看不出原来是哪个。
去噪器要做的事:给你一个脏数字(比如 0.5)和当前的噪声量 σ,猜原来是 +3 还是 -3。
这个玩具世界的好处是,最优去噪器能直接写出来(不用训练神经网络):
不用管 tanh 是什么,只要知道它把任何数字压到 −1 和 +1 之间就行。我把这个公式和”暴力枚举两种可能算加权平均”的结果对照验算过:
| σ(噪声量) | x_t(看到的脏数字) | 公式算的 | 暴力算的 | 差 |
|---|---|---|---|---|
| 3.0 | 0.5 | 0.4954 | 0.4954 | 1e-16 |
| 3.0 | 2.0 | 1.7483 | 1.7483 | 0 |
| 3.0 | −1.0 | −0.9645 | −0.9645 | 3e-16 |
| 1.0 | 0.5 | 2.7154 | 2.7154 | 9e-16 |
| 1.0 | 2.0 | 3.0000 | 3.0000 | 4e-16 |
| 1.0 | −1.0 | −2.9852 | −2.9852 | 4e-16 |
两列完全一致(差异只是浮点误差),说明公式没写错。
现在读懂这张表,你就读懂了去噪器在干什么:
- 噪声大(σ=3)时,看到 0.5,它只敢猜 0.495——“我看不太清,感觉稍微偏正”
- 噪声小(σ=1)时,同样看到 0.5,它就敢猜 2.72——“这么清楚了,肯定是 +3”
去噪器不是在擦噪点,它是在表态:“根据我看到的这点线索,真相更可能偏哪边。” 噪声越小,它越敢下重注。这就是那个”箭头”、那个专业叫法里的”分数(score)“。
五、跑一趟:噪声真的变成了数据
把去噪器接成一条链就能采样了:从纯噪声出发,每一步让去噪器猜一次原样,然后按下一档(更小的)噪声量重新加一点噪,再猜。σ 一档档降下去,猜测就一步步收紧。
这是我实际跑出来的一条轨迹(σ 每步减半,随机种子 7):
| 步 | σ | 当前的脏数字 x_t | 模型猜的原样 |
|---|---|---|---|
| 0 | 8.0 | −2.047 | −0.287 |
| 1 | 4.0 | 1.759 | 0.955 |
| 2 | 2.0 | 0.503 | 1.080 |
| 3 | 1.0 | 0.765 | 2.940 |
| 4 | 0.5 | 2.475 | 3.000 |
| 5 | 0.25 | 2.947 | 3.000 |
最终落点:+3.000。
从一个毫无意义的随机数 −2.047 出发,六步之后,它精确地落在了我们这个小世界的”真实数据”上。前三步模型都在含糊其辞(−0.287、0.955、1.080,都是”我不确定”),第 4 步开始态度突然坚决(2.940 → 3.000)。这就是”从噪声造出数据”,没有任何魔法。
跑两万次(种子 123),落到 +3 的比例是 0.5028,距离 ±3 的平均偏差在 0.0001 量级。也就是说,它不只是造出了合法数据,还准确还原了”两种各一半”这个真实分布——生成模型要的就是这个。(不设种子每次会在 0.5 附近小幅波动,我另一次跑出 0.4932。)
六、回答开头那个问题:为什么”擦”能造出新东西
现在可以正面回答了。你可能会想:“模型不就是学会了把脏数字推回 ±3 吗?那它只是在复现训练数据,哪来的’新’?”
关键在于:去噪器学到的不是”答案是什么”,而是一张”往哪边走更像真实数据”的方向图。 在我们的小世界里,这张图说的是”正数往 +3 推,负数往 −3 推,越接近越用力”。放到图片上,这张图说的是”往’更像真实照片’的方向推一点”——而”像真实照片”的地方有无穷多个,不只是训练集里那几张。
新东西就是这么来的:方向图是连续的,起点是随机的,所以你会落到训练集里没有的、但同样”合法”的点上。 就像你学会了”往山谷低处走”这条规则,就能从任何位置出发走到谷底——包括你从没走过的路线。
而”随机性到底从哪儿进来的”,我原本以为答案很显然:起点不同,落点就不同。跑了个对照实验之后发现不对。
| 采样方式 | 落到 +3 的比例 | 终点与起点同号的比例 |
|---|---|---|
| 每步重新加噪(标准做法) | 0.5028 | 0.5538 |
| 不加噪,纯确定性去噪 | 0.5053 | 1.0000 |
(两万次采样,同一组起点,种子 123)
看第二列:标准做法下,终点和起点同号的比例只有 55%——几乎等于瞎猜。 起点是正的,终点很可能是负的,我上面那条轨迹就是活例子(起点 −2.047,终点 +3)。真正决定结果的,是去噪途中每一步注入的那点新噪声,不是起点。
而关掉途中的加噪之后,同号比例变成 100%——起点完全决定终点。
两种做法都还原出了正确的分布(0.5028 和 0.5053,都在 0.5 附近)。所以结论是:
多样性可以来自起点,也可以来自途中,两条路都能到达正确的分布。
这不是纯理论趣味,它对应一个你可能遇到过的现象:用 DDIM(arXiv:2010.02502,那篇让采样快 10–50 倍的论文)这类不注入随机性的采样器时,固定种子就能复现出同一张图;而随机性更强的采样器,同样的种子也可能给你不同的结果。原因就在上面这张表里。
七、从图片到文字:为什么文本要换个玩法
到这里你已经理解了图片扩散。那扩散语言模型是怎么回事?
卡点只有一个:上面全部的推理都依赖”可以加一点点噪声”。
像素值是连续的,128 加一点噪声变成 131.7,完全合理。但文字是离散的 token——“猫”加一点噪声等于什么?没有”半个猫”这种东西。 那条从数据到噪声的连续通道,在文字上不存在。
解决办法是换一种”弄脏”的定义:不加噪声,改成挖空。 加噪 = 按一定比例把 token 换成 [MASK];去噪 = 把空填回来。噪声越大就是挖得越多,纯噪声就是整句全挖空。
这个思路来自 D3PM(arXiv:2107.03006)对离散数据加噪方式的系统研究,其中”挖空”这种选择叫吸收态。于是整套框架平移过来了:
| 图片扩散 | 文本扩散 | |
|---|---|---|
| 数据 | 连续的像素值 | 离散的 token |
| 怎么弄脏 | 加高斯噪声 | 换成 [MASK] |
| 脏到极致 | 纯雪花噪点 | 整句全是 [MASK] |
| 去噪器干什么 | 猜干净的像素 | 猜被挖掉的词 |
| 步数是什么 | 去噪迭代次数 | 分几批把空填完 |
想清楚这一层,你会发现文本扩散忽然不神秘了——它本质就是一个”挖空比例可以任意变化”的完形填空模型。 挖 15% 是 BERT,挖 0% 到 100% 随机变化并学会在任意比例下填空,就是扩散语言模型。
这条路已经跑到 8B 规模并验证可行(LLaDA,arXiv:2502.09992)。它省下的步数从哪儿来、代价是什么、为什么在代码任务上行而在知识推理上掉队——那是另一篇的内容:自回归不是物理定律。这篇讲扩散是什么,那篇讲扩散在语言上划不划算。
八、四个常见误解
预设一下你读到这儿可能还存着的疑问:
误解一:“扩散就是个高级降噪滤镜。” 滤镜的输入是一张有内容的脏图。扩散采样的输入是纯噪声,里面没有任何内容。它不是在恢复已有信息,是在无中生有——第五节那条轨迹从 −2.047 开始,那个数字里没有”3”的任何信息。
误解二:“步数越多质量一定越好。” 步数多确实让每步的近似误差更小,但收益会饱和,而时间是线性涨的。DDIM 那篇论文的整个卖点就是用远少于原来的步数拿到可比的质量(摘要报的是 10–50× 加速)。步数是个旋钮,不是”越大越好”的指标。
误解三:“起点的随机种子决定了结果。” 这个我上面用实验反驳了——在标准的随机采样里,起点只贡献 55% 的同号率,接近无关。只有关掉途中噪声(确定性采样)时,种子才完全决定结果。
误解四:“扩散必须一步步来,所以注定慢。” “拆成很多步”是训练时的思想,采样时步数可以压缩,甚至可以把多步蒸馏进一步。图像领域已经有几步出图的模型了。慢不是扩散的宿命,是早期实现的状态。
九、小白自测
能用自己的话答出来,这篇就算读进去了:
- 为什么”从纯噪声一步生成图片”学不会,“去掉一点噪声”却学得会?
- 扩散训练的标准答案是从哪来的?为什么说它是免费的?
- 第四节那张表里,同样看到 0.5,为什么 σ=3 时猜 0.495、σ=1 时猜 2.72?
- 去噪器学到的到底是”答案”还是”方向”?这个区别为什么决定了它能造出新东西?
- 标准采样里,多样性主要来自起点还是来自途中?确定性采样呢?
- 文字为什么不能像像素那样”加一点点噪声”?换成了什么?
带得走的东西
如果只留一句:扩散模型的本质,是把”直接从分布里采样”这个学不会的难题,换成了”反复回答’往哪边走更像真实数据‘“这个学得会的简单问题。
这个换法的适用范围比生成图片大得多。以后你看到任何”迭代精修”的方法——反复改写、反复投票、反复自我修正——都可以拿同一把尺子去量:它是在一步里要求一个聪明的答案,还是把难题拆成了很多步不需要多聪明的小改进?
诚实的提醒
这篇里的两个玩具实验(第四节的对照表、第五、六节的轨迹与统计)都是我亲手跑出来的,脚本就在下面,你可以复现或推翻。论文的编号、标题和引用的加速倍数是当场核实过的(DDPM、DDIM、Sohl-Dickstein、Song 的 SDE 那篇、D3PM、LLaDA 六篇的 arXiv 页面都打开确认过)。
需要说明的简化,免得你拿这个玩具去对照真实实现时困惑:
- 我用的加噪写法是
x_t = x_0 + σ·ε(只加噪、不缩放原数据),这是所谓”方差爆炸”的简单写法。真实的 DDPM 会同时把原数据按比例缩小,多一个系数。 - 我的采样器是”猜原样 → 按下一档噪声重新加噪”的简化退火版本,抓的是主干思想,与 DDPM 的原始采样公式在细节上不同。
- 那个
tanh公式只对”数据只有 ±3 两个值”这个玩具分布成立,它是解析解,不是训练出来的。真实模型是用神经网络逼近同一个东西。 - DDIM 的”确定性采样”对应它论文里不注入随机性的那个特例;摘要本身只强调 10–50× 加速,没有用”确定性”这个词,这一层是我的理解。
最低成本的亲手验证实验(不需要 GPU、不需要 pip install,标准库就够,我跑通过):
import math, random
a = 3.0 # 真实数据只有 +3 和 -3
den = lambda x, s: a * math.tanh(a*x/s**2) # 最优去噪器
sigmas = [8.0, 4.0, 2.0, 1.0, 0.5, 0.25] # 噪声一档档降
def sample(stochastic=True):
x = sigmas[0] * random.gauss(0, 1) # 起点:纯噪声
for i, s in enumerate(sigmas):
x0 = den(x, s) # 猜原样
nxt = sigmas[i+1] if i+1 < len(sigmas) else None
x = x0 + (nxt*random.gauss(0,1) if (stochastic and nxt) else 0.0)
return x
hits = [sample() for _ in range(20000)]
print("落到 +3 的比例:", sum(v > 0 for v in hits)/len(hits))
print("距 ±3 的平均偏差:", sum(abs(abs(v)-a) for v in hits)/len(hits))
三个值得你自己动手改的地方:把 sigmas 改短(比如只留 [8.0, 0.25]),看质量怎么崩——那就是”步数不够”的样子;把 stochastic 改成 False,验证一下我第六节那张表;把数据分布从 ±3 改成三个值(比如 −3、0、+3,去噪器公式要跟着改),看看它是否还能还原出各三分之一。
参考与继续读
论文(按时间顺序,也是这个领域的脉络)
- arXiv:1503.03585 — Deep Unsupervised Learning using Nonequilibrium Thermodynamics(Sohl-Dickstein 等,2015):把生成拆成一串小步骤的原始想法
- arXiv:2006.11239 — Denoising Diffusion Probabilistic Models(DDPM,Ho 等,2020):让这个想法真正出高质量图,CIFAR-10 上 FID 3.17
- arXiv:2010.02502 — Denoising Diffusion Implicit Models(DDIM,ICLR 2021):同样的训练目标,采样快 10–50 倍
- arXiv:2011.13456 — Score-Based Generative Modeling through SDEs(Song 等,ICLR 2021 Oral):本文开头那句话的出处,把分数匹配和扩散统一进同一个框架
- arXiv:2107.03006 — D3PM(NeurIPS 2021):离散数据怎么加噪,“挖空”作为吸收态
- arXiv:2502.09992 — LLaDA:掩码扩散撑到 8B 规模的语言模型
站内相关
- 自回归不是物理定律:扩散语言模型买到了什么,亏在哪里 — 本文的进阶篇:扩散用在语言上的账怎么算
- 梯度下降入门 — 第六节”往山谷低处走”那个直觉的正式版本
- 为什么交叉熵偏偏要用 log — 训练目标怎么衡量”猜得准不准”
- AI 数学从算术到前沿 — 想继续深挖扩散的数学,这里有完整的前置依赖链(扩散/分数匹配在 L8)