你可能没意识到,你每天都在“看到”扩散模型——揭秘AI作画背后的技术原理

0 阅读13分钟

你有没有刷到过这样的帖子:“Midjourney生成的图片也太逼真了吧!AI是不是已经可以取代设计师了?”要是完全不懂背后的技术,听完只能感慨一句“跪了”。

别担心,这次我们就来彻底搞懂“扩散模型”到底是怎么回事。这是现今火遍全网的Stable Diffusion、Midjourney、DALL·E这些AI绘画软件背后的核心技术。你可能会觉得它高深莫测,但一旦理解了,你会发现它绝对是迄今为止最优雅的AI模型之一。

一句话就能让你抓住精髓:扩散模型的核心非常简单:“先把一张好图一步一步‘糟蹋’成随机雪花点,然后让AI学会把这个过程反过来,‘一点一点’地把图片恢复原样。”

是不是听起来有点像魔法?那我们具体看看它是怎么做到的(文章结尾附完整代码下载)。


1. 扩散 = 一滴墨水 + 倒放录像

你可能不知道,AI绘画的发明灵感,其实来自我们日常生活中再常见不过的现象。

回想一下,你把一滴墨水滴进一杯清水里时发生了什么:墨水刚开始聚成一个小黑点,接着它开始在水中慢慢地散开,变成云雾一样的一团,最终墨水的分子均匀地溶进水里,整杯水都变成了均匀的淡蓝色。

水从“清澈透明”走向“均匀浑浊”的这个过程,在物理学里就叫 “扩散”——秩序走向混沌,清晰的画面被“溶解”了。

扩散模型(Diffusion Models, DM)就是把整个过程搬到电脑里。那AI学会了什么呢?它要学的东西简直太酷了:它要学的是 “倒放” 这个扩散的过程。也就是从一杯已经变成淡蓝色的墨水开始,一步一步地“长”回原来那滴刚刚滴进水里的墨水状态。这几乎不可能,但AI做到了。

Stable Diffusion、DALL·E、Midjourney,所有让全世界惊叹的AI绘画软件,本质上都在做同一件事:从一坨毫无意义的随机“雪花点”噪声里,一点一点地“吸走”噪声,使雪花点慢慢聚拢、成形,最后还原出一张清晰的图片。

这个“糟蹋图片”的过程被称为 “前向扩散” ,而AI学习的那个“倒放”过程,被称为 “反向扩散” 。接下来,我们深入拆解一下这两个步骤。


2. 正向扩散:怎样“毁掉”一张好照片

前向扩散,也就是“糟蹋照片”的过程。其实非常简单粗暴:不停地往照片上撒“雪花点”(专业术语叫高斯噪声)。

2.1 第一步:从高斯分布里“抓一把噪声”

你没必要害怕“高斯分布”这个词。你只要知道,它就是一张“均匀的雪花点”。模型根据一个叫 方差调度计划 的表格,来决定现在该撒多大的雪花。

一开始:用很小的“雪花点”,这样图片只是变模糊一点点而已。
越往后:敢放心地用越来越大的“雪花点”,反正图像已经看不清了。

这两个固定的beta值就是在控制雪花的大小。你看下面这段代码,第一步和最后一步的噪声方差相差了整整200倍:

import torch
 
T = 1000                     # 总时间步数,经典论文设置1000步!
beta_start = 0.0001          # 刚开始,我加点很小很小的噪声,温柔地“糟蹋”
beta_end = 0.02              # 到第1000步,放大招!加更猛的噪声
 
# 下面这一行简单到哭的平均分配,就是“线性方差调度计划”
betas = torch.linspace(beta_start, beta_end, T)
 
print(f"第1步的噪声方差: {betas[0]:.6f}")    # 输出: 0.000100
print(f"第500步的噪声方差: {betas[500]:.6f}") # 输出: 0.010055
print(f"第1000步的噪声方差: {betas[999]:.6f}")# 输出: 0.020000

2.2 第二步:一步到位的小技巧

你可能在想,想得到第500步的图像,难道真要从第1步加到第500步吗?那多慢啊。

数学家们早就替你想好了,根本不需要这样!他们发现,因为每次加的噪声都是标准的,所以可以用一个一步到位的公式,直接从原始干净图x₀算出第t步的样子x_t:

x_t = sqrt(ᾱ_t) × x₀ + sqrt(1 - ᾱ_t) × ε

这个公式什么意思? 简单说:这张半成品图片x_t = 保留一部分的原始图像信息 + 混入一定量的噪声。

如果你看不懂希腊字母,没关系,你只需要知道:ᾱ_t是一个系数,它随着t增大而减小。这意味着——

  • t很小(刚开始)

    :ᾱ_t≈1,公式前半部分≈1×x₀(原始图),后半部分≈0×ε(几乎没有噪声)。所以早期的x_t跟原图非常像,只是稍微加了一点点噪声。

  • t很大(快结束)

    :ᾱ_t≈0,公式前半部分≈0×x₀(原图几乎没了),后半部分≈1×ε(全是噪声)。所以后期的x_t基本就是纯粹的雪花点了。

用代码实现就会一目了然:

# 提前算好接下来要用的alfa和alfa_bar
alphas = 1 - betas                        # 这是一个长度为T的数组
alpha_bars = torch.cumprod(alphas, dim=0) # 累乘,核心!ᾱ_t = α₁×α₂×...×α_t
 
def add_noise(x_0, t):
    """
    正向加噪:一步到位,直接从原图x_0算加噪后的x_t
    x_0: 原始干净图像,形状 [batch, channels, height, width]
    t:   时间步,例如[3, 5, 10, ...],形状 [batch]
    返回: x_t (带噪图像), noise (所加的真实噪声)
    """
    # 根据t抓取对应ᾱ_t
    t_idx = t - 1
    alpha_bar = alpha_bars[t_idx].reshape(-1, 1, 1, 1)  # 调整形状用于广播
 
    # 从标准正态分布抓一把随机雪花
    noise = torch.randn_like(x_0)
 
    # ✨奇迹发生的地方!一步到位闭式公式✨
    x_t = torch.sqrt(alpha_bar) * x_0 + torch.sqrt(1 - alpha_bar) * noise
    return x_t, noise

现在你是不是已经觉得懂了一半:前向扩散就是个懒人攻略,只要一张原始图和选的噪声方差,轻松破坏一张图,几行代码搞定。


3. 反向扩散:教会AI当“修复大师”

正向破坏容易,逆向修复难。

真正的难点来了:给你x_t,你要怎么还原出x_{t-1}?

你面对的是一幅模糊不清的图像,有无数种可能的上一步状态。这就像你在看一部电影的倒放,要从零散的混乱帧里想象出前一帧的样子。x_t里既有原来图像的信息,也有噪声,而AI的任务就是精准地把噪声部分剔除出去,同时保留图像内容。

3.1 换个思路:不猜原图,只猜噪声

直接让AI预测上一步长什么样太难了。但是注意到我们使用的闭式公式:

x_t = √ᾱ_t × x₀ + √(1−ᾱ_t) × ε

如果你能把噪声ε从x_t里减掉,那你不就知道x₀了嘛!

这就是DDPM史上最聪明的决定:让神经网络学预测噪声ε,而不是直接学怎么画图。 只要预测出ε,代进公式就可以算出x_{t-1}。

3.2 网络结构U-Net:扩散模型的“大脑”

那预测噪声这事,总不能拍脑袋瞎猜吧?谁来做这个“大脑”呢?答案是最适用扩散模型的架构——U-Net

它的U型结构专门为“把一个模糊的东西变得清晰”而生。

编码器“下采样”:你想象一下,AI第一次看到一张噪声图,它先“退后几步”,整体扫描一遍。这一步是为了把握图片的全局结构,看看大概是个什么东西——猫?狗?还是人脸?
解码器“上采样”:搞懂了大概轮廓后,AI就“走近几步”,开始关注局部细节——这里是猫耳朵,那里是胡须尖。
跳跃连接:这就是U-Net最神奇的设计!在缩小(编码)的各个阶段,U-Net会偷偷“copy”一份当时的特征图(比如带着耳朵、眼睛位置的粗略信息)。在扩大(解码)的阶段,它会把这些旧信息“抄”回来和当前新信息拼在一起。这就好比修图师手里同时有模糊参考和清晰素描,细节不易丢。

为了让你看到U-Net是怎样一步步把噪声图片“修”干净的,下面用一个简化的PyTorch实现来演示完整的反向去噪过程:

def denoise_step(model, x, t):
    """
    反向去噪干一件事:从x_t算出更干净的x_{t-1}
    model: 训练好的U-Net神经网络
    x:     当前的带噪声图像
    t:     当前时间步(例如[500,500,...])
    """
    # 拿到当前这一步的alpha_t和alpha_bar_t
    idx = t - 1
    alpha = alphas[idx].reshape(-1, 1, 1, 1)
    alpha_bar = alpha_bars[idx].reshape(-1, 1, 1, 1)
 
    # 关键一步:让U-Net预测当前噪声εθ
    with torch.no_grad():
        eps_pred = model(x, t)      # 看!这就是U-Net的魔力!它猜出来噪声长啥样!
 
    # 套公式算出均值mu(最有可能的干净图像)
    mu = (x - ((1 - alpha) / torch.sqrt(1 - alpha_bar)) * eps_pred) / torch.sqrt(alpha)
 
    # 除了最后一步之外,再加一点小小的随机扰动,保持多样性
    if t > 1:
        noise = torch.randn_like(x)
        sigma = torch.sqrt(betas[idx])
        return mu + sigma * noise
    return mu

4. 时间步编码:告诉模型“这是第几步”

前面的代码里有一个细节:U-Net不仅接受噪声图片x_t,还接受时间步t作为输入。

思考:为什么必须告诉模型现在是第几步?

t

接近1000时,你的图片几乎都是噪声,没什么内容可挖。这时模型应该着重去猜图像的“整体骨架”——它是猫还是狗?它该朝哪个方向?

t

接近0时,图片的轮廓已经很清晰了,只是一些微小噪点还残留在画面上。这时模型应该关注的不是“猫的轮廓”,而是细节“消噪”——把毛发的颗粒感修掉。

如果模型不知道t,它就不清楚自己当前是该“勾勒大局”还是“精修细节”,最终效果会一塌糊涂。为了让模型能理解这一点,AI用了一种巧妙的 “时间步编码”(正弦位置编码) 技术(灵感来自Transformer)。它把整数t变成一个长长的向量,向量中不同维度的值按照不同频率振荡,类似于给AI一个“进度条”,告诉它现在是去噪的哪个阶段。

时间步编码的代码如下。其中最关键的是第20-21行:将MLP映射后的时间编码通过“广播加法”加到每个像素点上,让模型在每个空间位置都能感知当前的时间阶段:

def pos_encoding(timesteps, output_dim, device):
    """
    将离散的时间步t[例如:1,2...]转译成一段连续的向量,供神经网络咀嚼消化
    """
    batch_size = len(timesteps)
    v = torch.zeros(batch_size, output_dim, device=device)
    for i in range(batch_size):
        t = timesteps[i]
        D = output_dim
        vec = torch.zeros(D, device=device)
        positions = torch.arange(0, D, device=device)
        freqs = 10000 ** (positions / D)      # 不同维度对应不同频率
        vec[0::2] = torch.sin(t / freqs[0::2])  # 偶数位置用sin
        vec[1::2] = torch.cos(t / freqs[1::2])  # 奇数位置用cos
        v[i] = vec
    return v
 
class TimeAwareConvBlock(nn.Module):
    ...
    def forward(self, x, v):
        N, C, _, _ = x.shape
        v = self.mlp(v)                       # 形状 [N, C]
        v = v.view(N, C, 1, 1)                # 变形成N, C, 1, 1,用于广播
        return self.convs(x + v)              # 将时间步编码以加法融入特征图

最终,时间编码融入U-Net的每一个卷积层,让模型在每一步都知道自己正处在去噪流程的哪个阶段,从而采取最合适的“修复策略”。


5. 训练目标:不再需要复杂的数学,只看MSE

把前向和反向搭建起来之后,最关键的问题来了。

怎么训练U-Net?让目标简单到极致——只用“猜噪声”

因为你已经知道每一步加的真实噪声是啥(从正态分布里抓的),而U-Net预测的是εθ(xt, t),所以训练目标就简化为最小化预测噪声和真实噪声的差异,这称为MSE损失

# 训练循环的核心!在每个batch:
x0 = images.to(device)            # 从数据集取出一张原图
t = torch.randint(1, 1001, ...)   # 随机抽一个时间步
x_t, noise = diffuser.add_noise(x0, t)  # 前向加噪,得到噪声和带噪图
noise_pred = unet(x_t, t)         # U-Net猜测噪声值
loss = F.mse_loss(noise_pred, noise)  # 计算预测噪声与真实噪声的差距

经过成千上万次迭代训练,U-Net会逐渐成为一个“噪声专家”——给它任何x_t和t,它都能精准地预测出当初加进去的噪声长什么样。


6. 从推理到生成:千步蜕变成画

训练完成之后,从随机噪声生成新图片的过程就是“倒放”:

  • 生成一个纯噪声样本x_T。

  • for t = T down to 1

    • 让U-Net预测噪声εθ。

    • 用公式x_{t-1} = (x_t - 一个东西) / sqrt(α_t) + σ_t*z得到一个更干净的图片。

  • 最后输出那张清晰的x_0。


7. 潜在扩散模型Stable Diffusion:让画质飞跃的升级版

你可能有疑问:上面跑MNIST可以,但换成高清大图(1024×1024),计算量得有多大?

这正是潜在扩散模型的创意所在!它不在原始的几百万像素点里加噪声,而是先让一个叫“VAE”(变分自编码器)的模型把图片压缩成一个超小图的“核心摘要”(潜在空间),然后在那个超小尺寸的空间里做加噪和去噪,最后再把摘要解码回原始尺寸。

效果:原本要用200亿次计算变成只要几亿次,快得不是一点点!


8. 扩散模型正在重塑AI创作的版图

现在,扩散模型已经不是实验室的玩具,而是各行各业的生产力工具:

  • DALL·E

    系列是扩张模型的代表作。OpenAI已经在逐步推动未来替代模型;

  • Midjourney v7

    仍然是很多人心目中的“美学之王”;

  • Stable Diffusion 3

    等开源模型实现了消费级GPU高分辨率生成;

  • Seedance 2.0

    走向视频生成,用扩散变压器架构,实现音频+视频的同时生成;

  • 音频合成

    3D生成科研等领域也在大规模采用扩散模型。


9. 从理论到实践:工程总结

从原理到代码,我们走完扩散模型的完整旅程,这里是几个最值得记住的要点:

  • 反向扩散的核心是预测“噪声”

    ,而不是预测原图。

  • 闭式采样

    让正向加噪一步到位,训练飞速。

  • U-Net + 时间步编码 → 模型能感知去噪到了哪个阶段

  • MSE损失

    稳定可靠。

  • 潜在扩散

    把计算量从像素层推向潜在空间,加快生成速度。

  • 训练和推理结合**DDIM、DPM++**等加速采样可以数倍到数十倍提速。

扩散模型极富诗意的设计方案(破坏→学习修复)和惊人的实际效果,给了全球AI开发者新的启发。从今天开始,你也可以把笔墨挥洒出来,设计和训练属于你自己的AI绘画模型了!


完整代码下载:pan.baidu.com/s/1E09Vyocq…