资讯详情

从GAN到扩散模型:DDPM原理详解与PyTorch实现实践

📅 2026/10/13 8:01:17 | 华诺云谱 👁 阅读
从GAN到扩散模型:DDPM原理详解与PyTorch实现实践
生成式模型这两年的风向变化圈内感知其实比外界看到的还要剧烈。曾经提到生成式AI几乎所有人第一反应都是GAN从人脸生成到超分辨率对抗训练几乎是唯一解。但如今Diffusion扩散模型成了绝对主角无论是图像、音频还是多模态生成范式已经从“生成对抗博弈”转为“逐步去噪重建”。这篇文章不端着我从最直观的类比讲起把扩散模型的数学原理掰开揉碎再用PyTorch一步步搭一个能跑的DDPMDenoising Diffusion Probabilistic Models实现最后把我在实际训练中踩过的坑、调参的经验一并交代清楚。不管你是刚接触生成式模型的新手还是想从GAN切到扩散模型的老手读完应该都能建立一套清晰的操作认知。1. 范式之争GAN的软肋与扩散模型的接棒逻辑1.1 GAN的三大难解之痛GANGenerative Adversarial Network的核心思路是用一个生成器G和一个判别器D互相博弈G试图生成以假乱真的样本D则努力分辨真假。理论上这个min-max博弈的均衡点就是生成分布完全等于真实分布现实中的操作却远没那么理想。第一痛是训练稳定性。判别器和生成器的能力稍有失衡损失就会剧烈震荡甚至直接模式崩塌Mode Collapse生成出来的样本千篇一律。我早年调GAN做图像生成时最头疼的就是反复调整判别器结构、学习率、标签平滑这些超参数每次训练都像开盲盒同一个配置换个随机种子结果可能天差地别。第二痛是多样性缺失。GAN的生成质量往往不错但多样性明显不足。原因在于训练目标本质上是一个分布匹配过程生成器只要找到一条能“骗过”判别器的路径就会固守己见导致生成结果在低维流形上反复打转。用FIDFréchet Inception Distance指标来评价GAN在真实分布覆盖度上天然吃亏。第三痛是缺乏可控性。GAN的隐空间Latent Space虽然能通过插值做一些语义操作但隐空间的几何结构并不是我们期望的“均匀、连续、有语义”需要做大量额外工作去约束。这也直接导致后续很多可控生成任务比如把文本变成图像、把姿态变成人像GAN做起来极其吃力。1.2 扩散模型赢在哪里扩散模型跟GAN完全不是一个思路。它不搞对抗而是把生成过程定义成“加噪再除噪”两段流程前向过程给真实图像逐步叠加高斯噪声直到完全变成一张纯噪声图反向过程则训练一个神经网络从纯噪声出发一步步把图像“恢复”出来。生成问题就从一个博弈问题变成了一个去噪优化问题核心是让网络学会“如何预测每一步的噪声”。这个转变带来的直接好处很明显。第一是训练稳定因为损失函数就是简单的MSE均方误差没有判别器没有对抗平衡只要数据没问题训练过程基本不会发散。第二是多样性好扩散模型的目标是拟合完整的数据分布样本覆盖度明显优于GAN这意味着FID、IS等指标上的表现更好。第三是可控性自然因为前向过程本身就是逐步加噪的马尔可夫链我们可以通过类别条件、文本条件来引导反向生成过程做条件生成就像给网络多喂一个输入那么简单。从GAN到扩散模型本质上是把“制造逼真样本”这个目标换成了“理解数据结构”这个目标。生成器不再只盯着怎么骗过判别器而是学会观察一张图像在不同噪声水平下是什么样子这种更本质的建模方式让扩散模型在很多场景下都能“降维打击”GAN。1.3 不只是画图扩散模型的应用版图虽然扩散模型最出圈的是图像生成但它的应用范围远不止于此。文本到图像生成是大家最熟知的场景从早期的低分辨率生成到现在的超高清大图背后都是扩散模型的能力升级。除此之外图像编辑、图像修复、超分辨率、风格迁移、视频生成、音频合成甚至蛋白质结构预测扩散模型都有完整的落地路径。音频领域尤其值得一提。扩散模型可以对声谱图逐步去噪生成自然流畅的语音也能用于音乐生成、语音克隆。视频生成则是把扩散过程从二维图像扩展到三维时空模型需要同时去噪空间帧和时序帧复杂度骤增但效果也是传统方法难以比拟的。还有一个被低估的方向是科学计算领域比如蛋白质折叠。扩散模型通过预测三维结构的残基坐标逐步去噪能够生成以前只能在实验室里费力推断的蛋白质空间结构这在生物医药领域的想象空间非常大。所以不要以为扩散模型只是“AI画图”的底层技术它本质上是一种通用的数据生成框架凡是能定义“加噪过程”的数据类型都可以套用这套范式。2. 先别看公式扩散模型的直觉与类比理解2.1 一个墨滴入水的局面前向加噪怎么理解想象你往一杯清水里滴入一滴墨水。一开始墨水是集中在一个小区域里轮廓清晰信息密度极高。随着时间推移墨水不断扩散颜色逐渐均匀地分布到整杯水里最终整杯水变成淡淡的灰色你再也看不出当初那滴墨水的任何形状。这个过程完全不可逆原始位置信息被彻底破坏。扩散模型的前向过程就是这个“墨水扩散”的数学版。给定一张清晰图像x₀我们在每一步t给它叠加一点高斯噪声经过T步之后图像的原始信息被噪声稀释得无影无踪得到的x_T近似服从标准正态分布也就是一张纯噪声图。这里有个极其重要的细节每一步的加噪是个马尔可夫过程也就是当前状态只依赖于上一步状态。但因为我们每一步加的都是高斯噪声整条链可以一步到位计算即从x₀直接算出任意t时刻的加噪结果x_t而不需要真的一步步迭代。这个闭式解的数学基础源自高斯分布的叠加性质也是后面代码里能高效实现前向加噪的关键。2.2 逆时间而行反向去噪的直觉去噪的直觉前向过程是做“破坏”反向过程就是学习“修复”。如果我们可以估计出每一步加噪散落的噪声ε就能从x_t中把它减掉恢复出一个接近x_{t-1}的图像。把这个操作从T步一直执行到第0步就得到了生成的图像。可以这么想前向过程把图像推下了悬崖反向过程是让网络一步步把图像拉回山顶。每走一步只负责最小的修正累积起来就能完成一个极其精细的重建任务。这与GAN“一步到位生成图像”的设计形成了鲜明对比——扩散模型把困难任务拆成了上千个简单子任务每个子任务只是去除一点噪声网络学起来自然压力小很多。还有一种更好的直觉一张真实图像在各类噪声水平下的形态你是能观察到的。网络上对图像加噪去噪的研究早已有之但扩散模型的关键革命在于训练时它让网络看遍了所有程度的噪声采样时它从纯噪声开始反复运用这个去噪能力。所以模型的“想象力”来自对图像结构的大量观察而不是对抗博弈中的投机取巧。2.3 为什么偏偏是UNet当去噪网络去噪网络需要同时满足几个条件第一输入输出都是图像所以得是像素级别的映射第二要能感知“当前在第几步”同一个去噪操作在第100步和第900步本质上应该完全不同第三要有足够的感受野因为去噪过程既需要局部纹理细节也需要全局结构信息。UNet天然满足这些需求。它的编码器-解码器结构配合跳跃连接能把浅层的空间细节直接传递到深层解码器避免信息在压缩过程中丢失。同时UNet的不同层天然形成了多尺度特征既能处理局部噪声也能把握全局结构。而时间步信息通过正弦位置编码Sinusoidal Embedding注入到每个残差块中让网络清楚知道自己处于哪个去噪阶段。后来很多改进的扩散模型也尝试过用Transformer结构比如直接基于ViT做去噪器效果依然不错但UNet因为参数效率更高、归纳偏置更匹配图像任务在很长一段时间里都是扩散模型的主力骨架。做复现的时候先用UNet打底再逐步换骨干是比较稳妥的路线。3. 数学原理拆解KL散度、重参数化与噪声预测损失3.1 前向过程的参数化一套优雅的闭式解前向过程定义为一个固定的马尔可夫链。设原始图像为x₀每一步按照预先设定的方差βₜ向x₍ₜ₋₁₎添加高斯噪声q(xₜ | xₜ₋₁) N(xₜ; √(1-βₜ) xₜ₋₁, βₜ I)这里βₜ是一个从β₁递增到β_T的方差调度表DDPM原文用的是线性调度从1e-4逐渐增加到0.02。因为每一步都是高斯分布累积效果依旧高斯我们可以直接表示出q(xₜ | x₀)。令αₜ 1 - βₜᾱₜ ∏ₛ₌₁ᵗ αₛ则q(xₜ | x₀) N(xₜ; √(ᾱₜ) x₀, (1 - ᾱₜ) I)这个闭式解的推导只要反复用到“两个高斯随机变量相加仍是高斯均值相加、方差相加”这条性质。代码里通常用重参数化Reparameterization Trick来采样xₜ √(ᾱₜ) x₀ √(1 - ᾱₜ) ε其中 ε ~ N(0, I)有了这条公式训练时不必真的循环T步去逐步加噪直接从x₀跳到任意xₜ效率极高。这也是扩散模型能被高效训练的关键之一。3.2 反向过程与变分下界为什么要最小化KL散度反向过程同样定义为一个马尔可夫链但它的转移核要交给神经网络去学习p_θ(x₍ₜ₋₁₎ | xₜ) N(x₍ₜ₋₁₎; μ_θ(xₜ, t), Σ_θ(xₜ, t))训练目标是最小化反向分布与真实前向后验分布之间的差异。由于真实后验q(x₍ₜ₋₁₎ | xₜ, x₀)是有闭式解的只要我们额外知道x₀所以每个去噪步骤的KL散度都是可以计算的。理论上我们要优化的是负对数似然的变分下界VLB也就是L_VLB E_q [ Σₜ D_KL( q(x₍ₜ₋₁₎ | xₜ, x₀) || p_θ(x₍ₜ₋₁₎ | xₜ) ) ]这个形式很直观网络要学着逼近“完美的去噪分布”。变分推断的框架在这里起的作用是把不能直接优化的对数似然替换成一组可以逐项优化的KL散度。3.3 简化损失函数一切收敛到噪声预测MSE推导的另一种路线是直接把目标简化为预测噪声。因为真实后验的均值可以写成μ̃ₜ (1/√(αₜ)) ( xₜ - (βₜ/√(1-ᾱₜ)) ε )也就是说如果网络能预测出混合在xₜ中的噪声ε就能还原出x₍ₜ₋₁₎的均值。把μ_θ参数化为μ_θ(xₜ, t) (1/√(αₜ)) ( xₜ - (βₜ/√(1-ᾱₜ)) ε_θ(xₜ, t) )代入KL散度并忽略掉权重项最终所有目标都归结为让网络预测的噪声越来越接近真实混入的噪声L_simple E_{t, x₀, ε} [ || ε - ε_θ( √(ᾱₜ) x₀ √(1-ᾱₜ) ε, t ) ||² ]这就是大家在代码里看到的那个极其简单的MSE损失。它背后的逻辑其实十分惊人一个看似复杂的生成模型最终只需要学习“这张被噪声污染的图里面噪声到底是什么”这一个任务。这个简化过程我第一次读到的时候非常震撼因为GAN里一堆对抗损失、梯度惩罚、谱归一化到这里几乎全部消失了。3.4 推导中容易被忽略的关键细节有几个细节在推导过程中容易忽略但直接影响实现的理解。第一前向过程的方差βₜ是固定不学习的整个训练过程只更新反向网络参数这跟VAE把先验方差也学出来的做法不同少了一半的优化难度。第二KL散度中除了去噪项还有一个先验匹配项和一个重建项在实际训练时先验匹配项几乎没有梯度贡献重建项在T很大时也被淡化所以简化损失足以覆盖训练信号。第三Σ_θ(xₜ, t)在DDPM原文中被固定为βₜ不参与学习但在后续改进工作中可以单独学习。理解这些细节至少能帮你调试扩散模型时快速判断异常出在哪一个环节。4. PyTorch从零复现数据、UNet、训练与采样全流程4.1 环境与依赖准备复现DDPM不需要太重的依赖PyTorch 1.13或2.x版本都行。我这里使用MNIST手写数字数据集做演示因为28×28图像分辨率低CPU能跑GPU训练更是几分钟就能出效果。如果你想直接挑战高分辨率数据建议先确认显存大于8GB再把batch size调小。代码的组织结构我分为四个模块调度参数与加噪函数、时间嵌入、UNet骨干、训练与采样脚本。调度参数本质上只是预先算好的一堆常数放在类里方便全局引用。import torch import torch.nn as nn import torch.nn.functional as F import math T 1000 beta_start, beta_end 0.0001, 0.02 betas torch.linspace(beta_start, beta_end, T) alphas 1.0 - betas alphas_cumprod torch.cumprod(alphas, dim0) sqrt_alphas_cumprod torch.sqrt(alphas_cumprod) sqrt_one_minus_alphas_cumprod torch.sqrt(1.0 - alphas_cumprod)这段代码计算了整个前向过程的累积产品。为什么要累积乘积因为ᾱₜ表示从x₀到xₜ的信息保留比例。当t越来越大ᾱₜ趋向于0xₜ里几乎不再含有原始图像信息。这是整个去噪链条里的“时间表”。4.2 数据准备与前向加噪函数数据部分直接用torchvision加载MNIST归一化到[-1, 1]区间。这个细节非常重要因为最终的纯噪声x_T服从标准正态分布如果数据本身不在零均值范围网络从纯噪声出发生成的样本也会有整体偏置效果会打折扣。from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) dataset datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) dataloader torch.utils.data.DataLoader(dataset, batch_size128, shuffleTrue)前向加噪函数直接利用闭式解一步到位完成def q_sample(x0, t, noiseNone): if noise is None: noise torch.randn_like(x0) sqrt_alpha sqrt_alphas_cumprod[t].view(-1, 1, 1, 1) sqrt_one_minus_alpha sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1) return sqrt_alpha * x0 sqrt_one_minus_alpha * noise这里的t是长度为batch size的随机时间步向量每个样本对应一个随机时刻。让网络一次训练看到不同噪声水平的样本是整个训练策略的核心“课程”。4.3 UNet核心模块实现UNet实现我分为三个部分时间嵌入模块、残差块、骨架结构。时间嵌入借鉴Transformer的位置编码思路把时间步t映射成一个高维向量再通过MLP注入到特征图。class SinusoidalPositionEmbeddings(nn.Module): def __init__(self, dim): super().__init__() self.dim dim def forward(self, time): half_dim self.dim // 2 emb math.log(10000) / (half_dim - 1) emb torch.exp(torch.arange(half_dim, devicetime.device) * -emb) emb time[:, None] * emb[None, :] return torch.cat([emb.sin(), emb.cos()], dim-1) class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, time_emb_dim): super().__init__() self.time_mlp nn.Sequential(nn.SiLU(), nn.Linear(time_emb_dim, out_channels)) self.norm1 nn.GroupNorm(8, in_channels) self.conv1 nn.Conv2d(in_channels, out_channels, 3, padding1) self.norm2 nn.GroupNorm(8, out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, 3, padding1) self.shortcut nn.Conv2d(in_channels, out_channels, 1) if in_channels ! out_channels else nn.Identity() def forward(self, x, t): h F.silu(self.norm1(x)) h self.conv1(h) h h self.time_mlp(t)[:, :, None, None] h F.silu(self.norm2(h)) h self.conv2(h) return self.shortcut(x) h为什么要GroupNorm而不是BatchNorm因为每个样本的噪声水平不同BatchNorm用在纯噪声上去统计batch统计量并不合适容易引入跨样本干扰。GroupNorm对单个样本做归一化在这个场景下稳定得多。UNet骨架采用经典的编码器-解码器结构。编码器逐层下采样提取语义特征解码器通过跳跃连接融合编码器各级特征恢复空间细节。class UNet(nn.Module): def __init__(self, in_channels1, out_channels1, time_emb_dim128): super().__init__() self.time_embedding SinusoidalPositionEmbeddings(time_emb_dim) self.inc nn.Conv2d(in_channels, 64, 3, padding1) self.down1 ResidualBlock(64, 64, time_emb_dim) self.down2 ResidualBlock(64, 128, time_emb_dim) self.down3 ResidualBlock(128, 256, time_emb_dim) self.pool nn.MaxPool2d(2) self.mid ResidualBlock(256, 256, time_emb_dim) self.up1 ResidualBlock(512, 128, time_emb_dim) self.up2 ResidualBlock(256, 64, time_emb_dim) self.up3 ResidualBlock(128, 64, time_emb_dim) self.upsample nn.Upsample(scale_factor2, modenearest) self.outc nn.Conv2d(64, out_channels, 3, padding1) def forward(self, x, t): t_emb self.time_embedding(t) x1 self.inc(x) h1 self.down1(x1, t_emb) h self.pool(h1) h2 self.down2(h, t_emb) h self.pool(h2) h3 self.down3(h, t_emb) h self.mid(h3, t_emb) h self.upsample(h) h torch.cat([h, h2], dim1) h self.up1(h, t_emb) h self.upsample(h) h torch.cat([h, h1], dim1) h self.up2(h, t_emb) h self.up3(h, t_emb) return self.outc(h)通道拼接的时候需要注意维度对齐。编码器向下采样一次分辨率减半解码器上采样一次恢复分辨率所以cat操作发生在同一分辨率层级上。这里的down1、down2、down3输出顺序是h1、h2、h3解码方向与之对应的是h2、h1代码里已经对齐。4.4 训练循环几行代码背后的深层逻辑训练循环看起来非常简单几乎只有几行代码。每次迭代随机采样t随机采样噪声ε用闭式解构造xₜ让模型预测噪声ε然后计算MSE损失。def train(model, dataloader, optimizer, epochs, device): model.train() for epoch in range(epochs): total_loss 0.0 for x, _ in dataloader: x x.to(device) t torch.randint(0, T, (x.shape[0],), devicedevice).long() noise torch.randn_like(x) x_noisy q_sample(x, t, noise) pred_noise model(x_noisy, t) loss F.mse_loss(pred_noise, noise) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, loss: {total_loss / len(dataloader):.6f})为什么训练这么简单核心在于闭式解把“漫长马尔可夫链的观测”压缩成了单步计算。我们不是在训练网络模拟整条链而是在任意锚点让网络学会局部去噪不同t叠加起来就是完整的反向过程。这也是一个相当反直觉的学习策略反复训练网络做“只需去噪一步”的小任务最后采样时却能走完整条链。训练时还有几个隐藏但非常重要的细节。第一t是随机采样的这样模型在每个噪声水平上都能获得足够监督信号。第二损失并没有对不同t做加权因为噪声预测本身在所有t上都是等量纲的MSE网络会自动平衡各阶段的难度。第三优化器一般用Adam或AdamW学习率建议2e-4左右不需要太复杂的学习率调度。4.5 采样推理流程从纯噪声一步步还原训练完成之后生成过程是反向的。先从标准正态分布采样一个x_T然后从T-1循环到0每一步使用网络预测的噪声计算x₍ₜ₋₁₎的均值并加上一个对应方差的高斯扰动。torch.no_grad() def p_sample(model, x, t, device): t_tensor torch.full((x.shape[0],), t, devicedevice, dtypetorch.long) pred_noise model(x, t_tensor) sqrt_recip_alphas torch.sqrt(1.0 / alphas).to(device) pred_mean sqrt_recip_alphas[t] * (x - betas[t] * pred_noise / sqrt_one_minus_alphas_cumprod[t]) if t 0: posterior_variance betas[t] * (1.0 - alphas_cumprod[t - 1]) / (1.0 - alphas_cumprod[t]) noise torch.randn_like(x) return pred_mean torch.sqrt(posterior_variance) * noise else: return pred_mean def sample(model, shape, device): x torch.randn(shape, devicedevice) for t in reversed(range(0, T)): x p_sample(model, x, t, device) return x最后一步不加噪声的原因很直观如果最后一步还注入随机扰动生成的样本会带着额外的高频噪声看起来脏脏的。而在中间步骤中加噪声是必要的因为去噪网络的训练数据天然包含了这种随机性采样过程必须模拟训练分布才能让网络发挥正常。我见过不少人把最后一步也加了噪声导致生成图里总有一层“雪花”——原因就在这。4.6 条件生成让扩散模型听懂指令前面的实现是标准的无条件生成模型只负责随机生成数字。实际项目里大量需求是条件生成比如“给我一张数字8的图”。实现条件生成的改动非常小只要把类别标签嵌入到时间步嵌入附近即可。常见的做法有两类一是把label embedding加到time embedding上二是作为额外通道拼接到输入图像上。MNIST demo里用第一种就足够class ConditionalUNet(nn.Module): def __init__(self, num_classes10, **kwargs): super().__init__() self.label_embedding nn.Embedding(num_classes, 128) self.unet UNet(**kwargs) def forward(self, x, t, labels): t_emb self.unet.time_embedding(t) label_emb self.label_embedding(labels) ...训练时随机丢弃标签的做法叫“Classifier-Free Guidance”简单说就是让模型同时学会有条件和无条件生成采样时再通过一个引导强度把条件信号放大。这个技巧是目前所有高分扩散模型生成效果惊艳的核心原因值得单独展开研究。这篇主要聚焦基础实现条件生成就先开个头后续可以单独写一篇。5. 实战避坑训练不稳定、采样慢与显存不足的根源5.1 训练常见问题与排查清单扩散模型的训练比GAN稳定很多但这不代表没有坑。我自己跑MNIST和CIFAR-10时遇到过几类问题每次都能靠排查清单快速定位。如果loss始终不下降大概率是数据归一化出了问题。图像没有归一化到[-1, 1]时前向过程计算出的xₜ和标准噪声差距过大模型学不到有效信号。在MNIST里用ToTensor后像素范围是[0,1]必须再用Normalize((0.5,),(0.5,))转换到[-1,1]这个步骤忘了哪怕训练十轮loss也不会有明显下降。如果生成的图像模糊或结构混乱第一看看训练步骤是否太少。DDPM在MNIST上通常需要20到50个epoch才能出现清晰数字少于这个数量模型还没学够高频细节。第二个可能原因是采样时步长使用不当比如直接拿训练时的完整1000步来采样没问题但如果你自己写了加速采样逻辑步数少了而噪声注入没同步调整就容易糊。如果训练爆显存优先检查batch size和图像分辨率其次是网络通道数。64×64以上分辨率建议先把channel缩到64或32毕竟目标只是验证流程没必要一上来就堆大模型。现象常见原因处理建议loss不降数据未归一化到[-1,1]检查transform确保Normalize生成全为噪声训练步数不足增加epoch或检查t采样范围生成图像有网格状伪影上采样方式不合适改用F.interpolate配合bilinear或反卷积采样结果偏暗数据均值未对齐检查归一化是否包含均值偏移显存不足网络过宽或batch过大减小channel、batch、或使用混合精度5.2 采样速度与步数权衡DDIM思路速览DDPM的完整采样需要1000次网络推理这在真实场景里太慢了。解决思路是引入DDIMDenoising Diffusion Implicit Models它允许跳步采样比如从1000步压缩到50步效果几乎不损。直觉上的解释是DDPM的采样链本质是一条扩散反向链而DDIM把这条链“垂直到更短”的序列上每一步的更新策略改成确定性去噪再加一个可控噪声。这意味着不再受原始马尔可夫链的步数约束可以任意跨步。如果你想在项目里快速出图DDIM几乎是必学的加速方案。基础实现里的p_sample改成p_sample_ddim主要是换成DDIM专属的更新公式。torch.no_grad() def ddim_step(model, x, t, prev_t, eta0.0, devicecpu): t_tensor torch.full((x.shape[0],), t, devicedevice, dtypetorch.long) pred_noise model(x, t_tensor) alpha_t alphas_cumprod[t].to(device) alpha_prev alphas_cumprod[prev_t].to(device) if prev_t 0 else torch.tensor(1.0, devicedevice) std_dev eta * torch.sqrt((1 - alpha_prev) / (1 - alpha_t) * (1 - alpha_t / alpha_prev)) pred_x0 (x - torch.sqrt(1 - alpha_t) * pred_noise) / torch.sqrt(alpha_t) mean torch.sqrt(alpha_prev) * pred_x0 torch.sqrt(1 - alpha_prev) * pred_noise if eta 0 and t 0: mean mean std_dev * torch.randn_like(x) return mean这里的eta控制随机性eta0时采样变成完全确定性生成的样本集合内部一致性高适合做编辑任务eta0时会保留部分随机性生成结果更多样。实际项目里通常先试eta0需要多样性再加到0.5左右。5.3 混合精度与EMA大规模训练的两个必备技巧如果你把MNIST demo往更大规模的数据集上扩展两个技巧必须尽快安排。第一是混合精度训练AMP把网络参数前向和反向的精度降到FP16损失计算用FP32能同时降低显存占用并提速。PyTorch的torch.cuda.amp包直接支持改动量极小。第二是EMAExponential Moving Average也就是对模型参数做指数滑动平均。训练过程中维护一份参数副本它比当前参数更平滑生成的图像细节往往更好。用GAN的术语来说这相当于给生成器一个“记忆”避免最后几步优化动荡导致模型回退。很多论文中的效果对比实际上都是用了EMA以后的版本所以你自己复现时不要漏掉这个细节。torch.no_grad() def update_ema(ema_model, model, decay0.9999): for ema_param, param in zip(ema_model.parameters(), model.parameters()): ema_param.data.mul_(decay).add_(param.data, alpha1 - decay)采样时用ema_model代替model效果会有肉眼可见的提升。MNIST下可能差距不大但到了人脸生成这类精细任务EMA几乎是决定清晰度的关键。5.4 评估生成效果不只是看“像不像”生成模型做完了怎么判断好坏业内常用两个指标FID和IS。IS衡量生成样本的清晰度和多样性但计算依赖一个分类网络比较局限。FID则比较真实样本与生成样本在特征空间里的分布距离越小说明两组分布越接近。实操经验是FID在训练初期下降很快后期会进入平台期这时候可以考虑增大采样步数、增加EMA decay来继续压指标。另外如果你是在小数据集上做实验FID波动会比较大多跑几次采样取平均才可靠。还有一个容易被忽视的点贝斯扩散模型生成的样本质量对随机种子有一定依赖。同一个checkpoint换一个采样种子FID可能差一两个点。评估时用固定种子序列采样才方便对比版本差异。5.5 扩散模型的局限性不能只会吹捧尽管扩散模型很强大但它也不是没有软肋。最大问题是采样速度慢即便有DDIM加速距离实时生成还有距离工业级落地下一步的生成往往要通过蒸馏技术把大模型压成一步或几步采样的小模型。另一个问题是可控性仍有限文本引导虽然效果不错但在复杂组合场景下依然会出现语义丢失或属性混淆。此外扩散模型往往是自回归式地从纯噪声重建如果掌握不好生成过程的中间状态很难天然支持“半途继续改图”这类交互式创作。理解这些局限不是为了泼冷水而是让你在做技术选型时更理性。当项目需要高精度的可控生成时可能用扩散模型加条件控制网络更合适当项目预算紧张、要求实时出图时GAN或者蒸馏后的扩散模型仍是值得考虑的方向。结语从GAN到扩散模型变化的只是范式不变的是对分布的理解我在实际项目里用GAN和扩散模型都搭过生成pipeline最大的体会是扩散模型把“生成”这个抽象任务具象成了一个可分解的、可解释的工程问题调试起来舒服太多了。它的数学和实现门槛并不比GAN高反而因为损失函数简单更容易让初学者把精力放到数据、网络结构和采样细节上。最后再分享一个小技巧如果你在MNIST或CIFAR-10上训练扩散模型把训练过程中的中间检查点保存下来观察不同epoch的生成效果变化你会看到模型从“只会生成模糊色块”到“清晰地写出手写数字”的完整演化过程。这个过程对理解生成式模型的范式变革特别有价值。别急着追求大模型先用小规模把原理和代码吃透收益远大于跟风堆资源。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑