开始前,会这些就够了
- 知道独立随机量的方差可以相加
- 会平方、开方与均方误差
- 能区分训练目标和模型输入
- 需要补基础时:先补期望与方差
- 需要补基础时:再分清预测值、误差与单位
遇到不熟悉的词,先看这 2 个解释
打开完整术语小抄 ↗第一步加入方差 0.1 的噪声,第二步再加入方差 0.2 的噪声,累计方差并不是简单的 0.3。因为第二步在加入新噪声前,还把已有信号连同旧噪声一起缩小了。
这笔账会继续影响训练目标。同样把噪声预测错 0.2,在不同噪声强度下,换算成原信号的误差可能差很多。我们先用一个数把整个过程算清楚,再看为什么 ε、x₀、v 不能只改变量名就互换。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- diffusion_targets.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B diffusion_targets.py解压到独立练习文件夹,在含 diffusion_targets.py 的目录打开终端运行。固定标量的累计方差、逐步与闭式加噪、ε/x₀/v 目标及误差权重恒等式;未训练模型、生成图像或运行反向采样器。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
先用两步算出累计方差
把干净数据记为 x₀。第 1 步取 β₁=0.1,先乘 √0.9,再加 √0.1×z₁;第 2 步取 β₂=0.2,先把第 1 步结果乘 √0.8,再加 √0.2×z₂。z₁、z₂ 在概率模型中是互相独立的标准正态噪声。
展开后,原数据系数是 √(0.9×0.8)=√0.72;旧噪声系数是 √(0.1×0.8),新噪声系数是 √0.2。因此给定 x₀ 时的噪声方差是 0.08+0.2=0.28,正好等于 1−0.72。这里讨论条件噪声方差,不把 x₀ 在整个数据集上的变动也算进去。
一般记 αₜ=1−βₜ,ᾱₜ=α₁×…×αₜ。于是直接生成第 t 步输入的式子是 xₜ=√ᾱₜx₀+√(1−ᾱₜ)ε。横线表示从第 1 步累乘到第 t 步,不能拿单步 αₜ 代替。
同分布的一次加噪,不是拿最后一份噪声重放
本例把数学时刻 t=0 留给完全干净的输入,ᾱ₀=1;β 列表的第 0 项代表数学第 1 步。β=[0.1,0.2,0.25] 对应 ᾱ=[1,0.9,0.72,0.54]。框架的数组索引可能从 0 对应第一次加噪,接入时需要显式对齐。
为了核对同一条轨迹,程序固定 x₀=2、z=[0.3,−0.4,0.2]。第 2 步逐次计算得到 x₂≈1.603024;把前两份噪声按展开系数组合,再除以 √0.28,得到等效 ε≈−0.177705,用闭式公式重建可得到同一个 x₂。
若错误地直接拿最后一份 z₂=−0.4 当 ε,闭式结果约为 1.485396,无法重放原轨迹。如果只需要从 q(xₜ|x₀) 重新采样,则可以独立抽一份新的标准正态 ε;它与逐步加噪具有相同分布,但不是同一条随机轨迹。程序使用固定数字,不把三份数字当作验证高斯分布的样本。
同样错 0.2,换算回原数据的影响不同
设 a=√ᾱₜ、b=√(1−ᾱₜ),于是 xₜ=ax₀+bε。已知 xₜ,如果预测噪声 ε̂,就可用 x̂₀=(xₜ−bε̂)/a 换算原数据预测。把 ε̂=ε+δ 代进去,得到 x̂₀−x₀=−(b/a)δ。
令 δ=0.2,噪声平方误差一直是 0.04。ᾱₜ 分别为 0.9、0.5、0.1 时,原数据平方误差却分别约为 0.004444、0.04、0.36。噪声越重、a 越小,同样的噪声预测误差换算成原数据误差越大。
定义 SNR=ᾱₜ/(1−ᾱₜ),即这套标准化加噪系数的信噪比。在同一 xₜ、同一预测经上述公式换算、相同维度归约下,Lε=SNR×Lx₀。它是误差恒等式;不是说任意两个分别训练的网络,其损失都自动满足这个等式。
v 是另一种组合,解码规则也要跟着改
这里采用 v=aε−bx₀。与 xₜ=ax₀+bε 联立,可解得 x₀=axₜ−bv、ε=bxₜ+av,因为 a²+b²=1。v 在这里是这个线性组合的名称,不能看见变量名就把它当成刚才的 ε。
取 ᾱ=0.5、x₀=2、ε=0.5,程序用准确的 v 按正确公式恢复 x₀=2;若误把这个 v 放进预测噪声的公式,得到约 3.560660。数字形状相同、没有异常,也不代表目标解释正确。
对同一份 x̂₀ 换算出的 v̂,有 Lv=(SNR+1)×Lx₀。本例 ᾱ=0.5 时,Lε=0.04、Lx₀=0.04、Lv=0.08。三个目标的无权重损失对应不同的时刻权重,不能把它们的数值大小直接作为模型优劣。
要保留同一个目标,权重要一起换算
如果原目标是在每个时刻最小化 wₜ×Lx₀,把输出改为 ε 后,等价的数值目标是 (wₜ/SNR)×Lε;改为 v 后则是 (wₜ/(SNR+1))×Lv。前提是比较的预测通过相同公式对应,而且每个样本的时刻权重在批次归约前应用。
反过来,直接使用无权重的 Lε,相当于在 x₀ 误差上乘 SNR。这样改变了不同噪声强度在目标里的分量,并不是单纯换一种显示单位。DDPM 的简化噪声目标和后续 Min-SNR 研究都需要明确目标与权重;本文只核对这种换算,没有复现实验中的收敛速度或生成质量。
以上相除限定 0<ᾱₜ<1。干净端 ᾱ=1 没有噪声可供同样方式反解;完全噪声端 ᾱ=0 不能用除以 a 恢复 x₀。程序拒绝这两个转换边界,不用随便加一个小数把定义问题隐藏起来。
一次运行核对三笔不同的账
保存为 diffusion_targets.py,运行 python -B diffusion_targets.py。它核对逐步噪声方差、同轨迹的闭式重建,以及三个预测目标的误差关系;另用 135 组内部点计算核对恒等式,并写入 report.json。
这里没有训练去噪网络、加载图片、构造反向采样器或生成图像。固定标量算术能发现累乘、索引、目标和权重错误;完整扩散训练还需要时间条件、数据尺度、模型参数化和实际采样评估。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Cumulative Gaussian noise and target-loss identities; no model training."""
import json
import math
from pathlib import Path
def cumulative_schedule(betas):
result = [1.0] # mathematical time t=0 is clean input
for beta in betas:
if not math.isfinite(beta) or not 0 < beta < 1:
raise ValueError('each beta must be finite and in (0, 1)')
result.append(result[-1] * (1 - beta))
return result
def coefficients(alpha_bar):
if not math.isfinite(alpha_bar) or not 0 < alpha_bar < 1:
raise ValueError('target conversion requires 0 < alpha_bar < 1')
return math.sqrt(alpha_bar), math.sqrt(1-alpha_bar)
def target_case(alpha_bar, x0, epsilon, delta):
a, b = coefficients(alpha_bar)
xt = a*x0 + b*epsilon
epsilon_hat = epsilon + delta
x0_hat = (xt - b*epsilon_hat) / a
velocity = a*epsilon - b*x0
# Derive v_hat from the SAME x0_hat, rather than another independent error.
velocity_hat = (a*xt - x0_hat) / b
loss_epsilon = (epsilon_hat-epsilon)**2
loss_x0 = (x0_hat-x0)**2
loss_velocity = (velocity_hat-velocity)**2
snr = alpha_bar/(1-alpha_bar)
assert math.isclose(loss_epsilon, snr*loss_x0, rel_tol=1e-11, abs_tol=1e-12)
assert math.isclose(loss_velocity, (snr+1)*loss_x0, rel_tol=1e-11, abs_tol=1e-12)
assert math.isclose(a*xt-b*velocity, x0, abs_tol=1e-12)
assert math.isclose(b*xt+a*velocity, epsilon, abs_tol=1e-12)
# Intentionally interpret the exact v as epsilon to demonstrate mismatch.
wrong_x0 = (xt-b*velocity)/a
return {'alpha_bar':alpha_bar,'a':a,'b':b,'xt':xt,'epsilon':epsilon,
'velocity':velocity,'x0_hat':x0_hat,'loss_epsilon':loss_epsilon,
'loss_x0':loss_x0,'loss_velocity':loss_velocity,'snr':snr,
'wrong_x0_using_v_as_epsilon':wrong_x0}
def main():
betas, noises, x0 = [0.1, 0.2, 0.25], [0.3, -0.4, 0.2], 2.0
bars = cumulative_schedule(betas)
current, variance, weights = x0, 0.0, []
rows = []
for time, (beta, z) in enumerate(zip(betas,noises), start=1):
alpha = 1-beta
current = math.sqrt(alpha)*current + math.sqrt(beta)*z
variance = alpha*variance + beta
weights = [math.sqrt(alpha)*weight for weight in weights] + [math.sqrt(beta)]
residual = sum(weight*noise for weight,noise in zip(weights,noises[:time]))
a, b = coefficients(bars[time])
effective_epsilon = residual/b
direct = a*x0+b*effective_epsilon
wrong_last_noise = a*x0+b*z
assert math.isclose(current,direct,abs_tol=1e-12)
assert math.isclose(variance,1-bars[time],abs_tol=1e-12)
assert math.isclose(sum(w*w for w in weights),variance,abs_tol=1e-12)
rows.append({'t':time,'beta':beta,'alpha_bar':bars[time],
'variance':variance,'noise_weights':weights,'effective_epsilon':effective_epsilon,
'sequential_xt':current,'direct_xt':direct,'wrong_xt_using_last_noise':wrong_last_noise})
cases = [target_case(bar,x0,0.5,0.2) for bar in [0.9,0.5,0.1]]
# Cross-check identities over a grid of finite interior schedules and targets.
grid_checks = 0
for bar in [0.01,0.1,0.5,0.9,0.99]:
for original in [-2.0,0.0,2.0]:
for epsilon in [-0.7,0.0,0.5]:
for delta in [-0.3,0.0,0.2]:
target_case(bar,original,epsilon,delta)
grid_checks += 1
rejected = 0
for bad in [0,1,-0.1,float('nan')]:
try:
coefficients(bad)
except ValueError:
rejected += 1
assert rejected == 4
report={'scope':'fixed scalar Gaussian algebra; no images, reverse sampler or training',
'betas':betas,'alpha_bar_including_clean_t0':bars,'sequential':rows,
'target_cases':cases,'identity_grid_cases':grid_checks,
'rejected_conversion_boundaries':rejected,'checks_passed':True}
Path('report.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
print('alpha_bar[t=0..3] =', [round(x,6) for x in bars])
for row in rows:
print('t={} variance={:.6f} xt={:.6f} effective_eps={:.6f} last-noise-xt={:.6f}'.format(
row['t'],row['variance'],row['sequential_xt'],row['effective_epsilon'],row['wrong_xt_using_last_noise']))
for row in cases:
print('alpha_bar={:.1f} SNR={:.6f} L_eps={:.6f} L_x0={:.6f} L_v={:.6f}'.format(
row['alpha_bar'],row['snr'],row['loss_epsilon'],row['loss_x0'],row['loss_velocity']))
print('same exact v decoded as epsilon at alpha_bar=0.5: x0={:.6f}'.format(cases[1]['wrong_x0_using_v_as_epsilon']))
print('identity_grid_cases={} boundaries_rejected={}'.format(grid_checks,rejected))
print('checks=PASS; report.json written')
if __name__ == '__main__':
main()
怎样运行:在独立练习文件夹中执行 python -B diffusion_targets.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。固定标量的累计方差、逐步与闭式加噪、ε/x₀/v 目标及误差权重恒等式;未训练模型、生成图像或运行反向采样器。
运行后对照结果
alpha_bar[t=0..3] = [1.0, 0.9, 0.72, 0.54]
t=1 variance=0.100000 xt=1.992235 effective_eps=0.300000 last-noise-xt=1.992235
t=2 variance=0.280000 xt=1.603024 effective_eps=-0.177705 last-noise-xt=1.485396
t=3 variance=0.460000 xt=1.488259 effective_eps=0.027373 last-noise-xt=1.605340
alpha_bar=0.9 SNR=9.000000 L_eps=0.040000 L_x0=0.004444 L_v=0.044444
alpha_bar=0.5 SNR=1.000000 L_eps=0.040000 L_x0=0.040000 L_v=0.080000
alpha_bar=0.1 SNR=0.111111 L_eps=0.040000 L_x0=0.360000 L_v=0.400000
same exact v decoded as epsilon at alpha_bar=0.5: x0=3.560660
identity_grid_cases=135 boundaries_rejected=4
checks=PASS; report.json written记住这些要点
- 累计噪声由 ᾱ 的连乘决定,不能简单加 β。
- 重放同一轨迹要组合旧噪声,重新采样只要求分布相同。
- ε、x₀、v 对应不同解码公式与误差尺度。
- 换预测目标时,想保持相同损失就必须换时刻权重。
别把例子用过头
- 把 αₜ 当成累计 ᾱₜ。
- 混淆干净 t=0 与框架数组的第 0 个加噪位置。
- 用独立预测的两组误差检查只对同一映射成立的恒等式。
- 把预测 v 的输出交给预测 ε 的恢复公式。
自己试一小步
把标量改成二维数据,并让两个训练样本处于不同噪声时刻,怎样保留同一个加权目标?
- 为两个样本分别指定 ᾱ、原数据、噪声和预测偏差。
- 逐样本换算三种目标,再分别核对平方误差关系。
- 分别计算先按时刻加权再平均,以及错误地先平均后使用一个权重。
- 逐渐把 ᾱ 推近两个端点,记录数值敏感性。
做到这些,就算完成
- 清楚说明维度求和还是平均,以及批次如何归约。
- 所有比较使用通过同一公式对应的预测。
- 端点行为与实际训练质量分开报告。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Denoising Diffusion Probabilistic Models (新标签页)↗
前向加噪、累计 ᾱ、直接采样和简化噪声训练目标。
- Efficient Diffusion Training via Min-SNR Weighting Strategy (新标签页)↗
同一预测转换下 ε、x₀、v 误差与 SNR 权重的关系。
- Diffusers v0.35.1:DDPMScheduler 源码 (新标签页)↗
cumprod、
add_noise、get_velocity与三种prediction_type的具体语义。