qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

扩散训练换成预测原图,为什么损失不能照搬?

从两步加噪的方差账目出发,区分单步与累计噪声,再核对噪声、原信号和 v 三种目标的误差换算。

展开本文目录
  1. 下载完整练习包
  2. 先用两步算出累计方差
  3. 同分布的一次加噪,不是拿最后一份噪声重放
  4. 同样错 0.2,换算回原数据的影响不同
  5. v 是另一种组合,解码规则也要跟着改
  6. 要保留同一个目标,权重要一起换算
  7. 一次运行核对三笔不同的账
  8. 记住这些要点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
累计 ᾱ
把每一步保留旧信号的系数依次相乘,记录从干净数据走到当前时刻一共保留多少。
预测目标的误差换算
同一份预测换成噪声、原数据或 v 的表达后,平方误差要乘不同的时刻系数。
打开完整术语小抄 ↗

第一步加入方差 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。

这里没有训练去噪网络、加载图片、构造反向采样器或生成图像。固定标量算术能发现累乘、索引、目标和权重错误;完整扩散训练还需要时间条件、数据尺度、模型参数化和实际采样评估。

展开收起代码 · 小实验python · 101 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""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 的输出交给预测 ε 的恢复公式。

自己试一小步

把标量改成二维数据,并让两个训练样本处于不同噪声时刻,怎样保留同一个加权目标?

  1. 为两个样本分别指定 ᾱ、原数据、噪声和预测偏差。
  2. 逐样本换算三种目标,再分别核对平方误差关系。
  3. 分别计算先按时刻加权再平均,以及错误地先平均后使用一个权重。
  4. 逐渐把 ᾱ 推近两个端点,记录数值敏感性。

做到这些,就算完成

  • 清楚说明维度求和还是平均,以及批次如何归约。
  • 所有比较使用通过同一公式对应的预测。
  • 端点行为与实际训练质量分开报告。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。