☰
Stable Diffusion原理精讲:从CLIP文本编码到UNet去噪的全链路解析
2026/10/26 19:57:15 网站建设 项目流程

很多人一说起 Stable Diffusion,脑子里最先蹦出的是“去噪”“梯度”“提示词”这些碎片,真要解释文生图背后整个链条,却常常卡在 CLIP 和 UNet 的关系上。我啃完手头这套四集精讲笔记之后,最大的感受是:SD 的原理并没有想象中那么玄,它本质上是一条清晰的数据流水线——从文本变成向量,从向量去引导噪声消除,再从隐空间还原成像素。这篇笔记我把四集的核心逻辑串起来,结合自己跑代码时的理解,尽量用不绕弯子的方式,把 CLIP、扩散模型、UNet 到最终文生图实战这条链路一次讲透。适合想摆脱“玄学调参”、真正想看懂模型每一步在做什么的读者,也适合准备面试、准备自己微调模型的人。

1. 四集精讲的结构设计与讲解思路

先说这套笔记为什么值得按这个顺序看。Stable Diffusion 看起来是一个整体模型,可它内部其实是多个子模块的协作:一个负责把文字变成机器能理解的向量,一个负责定义“从噪声到图像”的生成过程,一个真正干活去噪的网络,再加最后负责还原图像的编解码器。四集之所以按“CLIP → 扩散模型 → UNet → 代码实战”来分,就是按照数据从输入到输出真正流经的先后顺序拆的。

1.1 为什么从 CLIP 讲起,而不是先讲扩散模型

可能有人觉得扩散模型才是主角,应该先讲数学过程。但从实际推理链路来看,用户输入的第一站就是文本编码,后面的 UNet 注意力机制全靠这组向量提供语义条件。如果不先把 CLIP 处理文本的规则讲清楚,后面看 cross-attention 时就会很困惑,为什么文本会被拆成一个一个 token,为什么超过某长度就失效,为什么同样一句话换个词效果差那么多。我自己的经验是:CLIP 虽然只占整个模型很小一部分参数,但它是整个生成过程的“指挥棒”,先理解它,后面的内容就顺了。

1.2 为什么把 UNet 从扩散模型里单独拆出来

很多入门文章把“扩散模型”和“UNet”混着说,仿佛它们是同一个概念。实际上,扩散模型描述的是一种“逐步加噪再逐步去噪”的生成范式,而 UNet 是在这套范式下负责具体预测工作的网络结构。就好比“出租车司机送你到目的地”是一种流程,而“方向盘和发动机”是具体执行部件。把二者分开讲,概念上就不容易打架。这套笔记里第二集讲扩散模型的数学逻辑,第三集才深入到 UNet 如何实现去噪,恰好避开了新手常见的混淆点。

2. 第一集核心:CLIP 文本编码器与条件控制原理

2.1 CLIP 的双塔对比学习机制

CLIP 的核心思路是在预训练阶段同时训练一个文本编码器和一个图像编码器,然后把配对的文本与图像映射到同一个向量空间里。训练时,模型会学习“这段文字描述的图像应该和这段文字的向量靠近”,反之则推远。这个过程不需要人工标注具体类别,而是直接利用网络上现成的“文字-图像”对,所以规模能做很大。

在 Stable Diffusion 里,我们只用到了 CLIP 的文本编码器部分。文本输入进去,输出的是一组 token 级别的向量表示,也就是说,一句话会被拆成多个 token,每个 token 对应一个向量,整句话最终形成一组向量序列。这个序列会被送到 UNet 的交叉注意力层,去引导生成过程。需要特别提醒的是,这里的 CLIP 并不是一个通用的“语义理解模型”,它的向量更偏向于“能描述图像风格和内容的语义”,因此它对风格词、材质词、构图词非常敏感,而对抽象逻辑、数字、关系词不敏感。

2.2 最长 77 个 token 的限制与分词器细节

CLIP 使用 BPE 分词器,词表大小约 49,000 多。官方实现里,文本编码器会把输入填充或截断到固定长度 77 个 token,这个数字后来被很多社区版本沿用。超过 77 个字后,多余内容会被直接丢弃,这在实际使用中影响非常大。举个例子,如果你把一堆形容词堆在后面,而核心对象放在第 80 个 token 位置,模型根本看不见它。

我在实操中的习惯是:重要性高的关键词放在句子靠前,从句和修饰成分往后放。因为 BPE 是一次性处理整个序列的,虽然严格来说 token 位置不完全是权重排序,但早期 token 在注意力计算中往往有更强的锚定作用。另外,负面提示词同样要遵循这个规则,不是随便写就能生效的。

2.3 提示词权重语法背后的本质

很多工具支持(word:1.2)这种加权语法。从原理上看,这并非给该词单独加了多大权重,而是相当于把该 token 的向量朝某个方向放大。提示词解析器在预处理时会把注意力权重乘到对应的 embedding 上,比如 1.2 倍。这里的乘数是线性缩放,所以数值过大(比如 1.8 以上)常常造成色彩溢出或画面崩坏,合理的范围一般在 0.8 到 1.4 之间。

3. 第二集核心:扩散模型的前向加噪与反向去噪

3.1 用冰融化成水来理解扩散过程

扩散模型可以类比成一块冰融化成水的过程。前向过程就是不断加噪声,直到一副清晰的图变成完全无法辨认的噪声图;反向过程则是从一滩“随机噪声”里慢慢恢复出清晰的图案。问题的关键是,融化过程是确定的、不可逆的,但如果我们知道每一步融化的规律,是有可能学着逆着做的。扩散模型需要学习的就是这个逆向过程。

3.2 前向过程的数学形式与噪声调度

设原始图像为 x₀,前向过程每一步都按一个预先设计好的系数向它添加高斯噪声。数学上通常写成:

x_t = √(ᾱ_t) · x₀ + √(1 - ᾱ_t) · ε

其中 ε 是标准高斯噪声,ᾱ_t 是从某个噪声调度表里读取的累积系数。在 Stable Diffusion 里,t 一般取到 1000,ᾱ_t 从接近 1 逐渐衰减到接近 0。也就是说,越往后,原始信息的占比越低,噪声占比越高,到最后图像基本就是纯噪声了。这个前向过程在训练时是“直接一步到位”的,不需要真的迭代 1000 次,因为上式可以直接算出任意 t 时刻的噪声图,这点极大提升了训练效率。

3.3 反向过程:预测噪声而不是直接预测图像

到了反向过程,模型要做的事是:给定一张带着噪声的图 z_t 和时间步 t,预测出“上一个时间步的噪声长什么样”,或者说,预测出纯噪声和当前图之间的噪声残差。这里有个关键设计:为什么不直接预测上一时间步的清晰图?因为在噪声占比很大的时候,直接回归到清晰的 x₀ 是非常困难的,网络几乎无从下手;而预测噪声的分布结构相对稳定,损失函数也更平滑。训练时用的损失函数本质上是均方误差,比较的是预测噪声和真实噪声之间的差异,公式可以简化为:

L = E[ || ε - ε_θ(z_t, t, c) ||² ]

注意 c 代表条件向量,也就是 CLIP 编码出的文本向量,它作为额外输入参与整个预测过程。训完之后,推理时不需要再预测每一步的噪声变化,而是从一个纯随机向量出发,交给 UNet 迭代小几十步,逐步把噪声“拧”出来。

3.4 采样步数与调度器的关系

理论上扩散模型需要 1000 步完整去噪,但实际采样器(比如 DDIM、DPM++ 一类的调度器)可以压缩到 20 到 50 步,原因是它们改变了采样过程中的噪声和信号比例,每一步“跳”得更远。步数太少会导致细节粗糙,步数太多也不一定更好,25 到 30 步是性价比最高的区间。我在实测里的感受是:对 SD 1.5 系模型来说,DPM++ 2M Karras 配 25 步比较稳;而对 SDXL 系模型,20 步左右往往足够了。

4. 第三集核心:UNet 架构与条件注入

4.1 从像素空间到隐空间,UNet 到底在干什么

Stable Diffusion 里的 UNet 并不直接处理 512×512 的像素图,它处理的是压缩后的隐空间张量。原始图像会被 VAE 编码器压缩成 64×64×4 的隐向量,UNet 的输入、输出都在这个尺度上。这样做最直接的好处是计算量大幅下降,显存占用骤减,这也是 SD 能在家用显卡上跑起来的关键原因。

UNet 本身的整体结构是编码器-解码器对称结构,包含多级下采样、中间瓶颈层、再逐级上采样。下采样使用卷积配合残差块,把空间分辨率逐步降低,特征通道逐步增加,让网络在不同的感受野下理解图像内容。上采样部分则逐步恢复分辨率,最终输出和输入同尺度的张量,也就是“预测出来的噪声”。

4.2 时间步 embedding 与残差块的交互

UNet 怎么知道当前去噪进行到哪一步?答案是时间步 t 会先被编码成一个 embedding 向量,然后通过全连接层映射,并加到每一个残差块上。这么做的好处是网络内部可以自适应地调整特征激活程度——早去噪阶段更关注整体轮廓,晚去噪阶段更关注细节纹理。如果没有这个时间条件,网络在噪声很浓和噪声很淡的情况下都会用同一套权重,很难兼顾极端情形。

4.3 Cross-Attention:文本条件如何进入图像特征

这是整个 SD 原理中最值得深挖的一环。UNet 内部既有标准的自注意力,也有交叉注意力。交叉注意力层的 Q 来自图像侧的隐空间特征,K 和 V 来自文本侧的 CLIP token 向量。计算逻辑和注意力公式一致:

Attention(Q, K, V) = softmax(QKᵀ / √d) · V

这个过程相当于让图像中的每个局部块,根据它和文本各 token 的相关程度,去“读取”文本语义信息。如果一个图块和“猫”这个词高度相关,它的注意力权重就会集中在这个 token 上,然后融合其向量。这就是为什么提示词能强有力地控制生成内容的原因——不是“施加魔法”式的控制,而是通过注意力软对齐逐层影响特征。

4.4 无分类器指导(CFG)与文本条件的配合

CFG 是推理阶段放大条件控制强度的关键机制。训练时,模型会以一定概率随机丢弃文本条件,让网络学会“不带条件也能去噪”。推理时,模型分别预测一次带条件的结果和一次不带条件的结果,再把两者差放大后叠加,公式大致是:

ε_final = ε_uncond + cfg_scale · (ε_cond - ε_uncond)

cfg_scale 就是常说的 CFG 值。值越高,生成结果越贴近提示词,但超过一定数值后,画面会过饱和、出现伪影,甚至直接崩坏。1.5 系推荐 7 左右,SDXL 推荐 5 到 6,这只是经验值,最终需要按模型调配。

5. 第四集核心:文生图全流程与代码实战

5.1 从文本到像素的完整推理链

把前三集的内容连起来,一条完整链路就出来了:用户输入提示词后,CLIP 文本编码器把它编码成 77×768 的向量序列;初始化一个 64×64×4 的纯噪声隐张量;接着进入去噪循环,每一步把当前隐张量、时间步、文本向量一起送入 UNet,U-Net 输出预测噪声,调度器根据噪声更新隐张量;循环结束后,VQ-VAE 或 KL-VAE 解码器把 64×64×4 的隐张量还原成 512×512 的像素图。每一步各司其职,缺一环都跑不通。

5.2 不用现成 Pipeline 时,核心代码长什么样

不少读者直接用现成 Pipeline,几行代码就能生成图片,但这对理解原理帮助不大。我当时是照着四集笔记里给的最小复现逻辑,把 Pipeline 拆开手写了一遍,核心流程如下:

import torch from transformers import CLIPTokenizer, CLIPTextModel from diffusers import AutoencoderKL, UNet2DConditionModel, DPMSolverMultistepScheduler # 1. 加载各子模块 tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14") vae = AutoencoderKL.from_pretrained("stabilityai/sd-vae-ft-mse") unet = UNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="unet") scheduler = DPMSolverMultistepScheduler.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="scheduler") # 2. 文本编码 prompt = "a cat sitting on a windowsill, soft light, detailed fur" tokens = tokenizer(prompt, padding="max_length", max_length=77, truncation=True, return_tensors="pt") text_embeddings = text_encoder(tokens.input_ids.to("cuda"))[0] # [1, 77, 768] # 3. 初始化随机噪声隐张量 latents = torch.randn((1, 4, 64, 64), device="cuda") latents = latents * scheduler.init_noise_sigma # 4. 循环去噪 scheduler.set_timesteps(25) for t in scheduler.timesteps: latent_input = torch.cat([latents] * 2) # 同时跑带条件与无条件 with torch.no_grad(): noise_pred = unet(latent_input, t, encoder_hidden_states=text_embeddings).sample noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2) noise_pred = noise_pred_uncond + 7.0 * (noise_pred_cond - noise_pred_uncond) latents = scheduler.step(noise_pred, t, latents).prev_sample # 5. VAE 解码还原像素图 latents = 1 / 0.18215 * latents with torch.no_grad(): image = vae.decode(latents).sample image = (image / 2 + 0.5).clamp(0, 1).permute(0, 2, 3, 1).cpu().numpy()

这段代码最大的价值在于让你亲眼看到:文本在哪一步进去、噪声在哪一步被预测、CFG 在哪一步作用、VAE 在哪一步解码。每步剥开看,SD 的神秘感就没了。

5.3 关键参数与采样器组合的经验值

实操中参数组合的影响很大。分辨率最好基于模型原生分辨率,SD 1.5 一般是 512×512,SDXL 是 1024×1024。如果要放大,先按原生分辨率生成再外挂放大模型,比直接调高分辨率更稳定。步数方面,25 步左右即可,CFG 调太高画面会“塑料感”严重。随机种子固定后,其他参数不变,生成结果可复现,这也是调试物体的基础。

6. 常见问题与排查技巧

6.1 显存溢出与半精度陷阱

入门最常见的报错就是 CUDA out of memory。解决思路无非三条:缩小批量大小、降低分辨率、启用切片注意力或 xformers。另外,很多人为了省显存直接用半精度,但 fp16 在 UNet 内部偶尔会因为数值溢出导致黑图或噪点。保险做法是关键模块用 fp32,或者启用调度器自带的抗溢出处理。

6.2 生成图像崩坏与画面质量问题

生成出来的图有几种典型问题,排查方向各不相同。全黑或全白通常是 VAE 解码或归一化出错;颜色过饱和往往是 CFG 过高;画面结构混乱、肢体崩坏多半是模型能力或采样步数不足;高频噪点过多则需要检查潜空间后处理。我的经验是,先把 CFG 降到 5,步数提到 40,如果问题依旧,再去动提示词描述。

6.3 微调和训练中的隐藏坑

不少人会在自己数据上微调 UNet,最容易遇到的坑是过拟合导致“提示词失效”。原因通常是数据集太小或学习率太高,网络把图像和某些隐藏特征耦合了。我踩过几次坑之后的经验是:微调阶段先冻结文本编码器和 VAE,只调 UNet,学习率定在 1e-5 左右,并配合强正则化,效果会稳定很多。另外,每次训练完一定要做一次“文本条件消融测试”——换不同的 prompt 看同一批生成结果,如果结果几乎不变,说明条件控制已经失效。

注意:以上参数和代码都基于常见开源实现,实际环境不同需要微调。但理解原理本身比换参数更重要,一条条链路捋清楚之后,出问题也知道去哪找原因。

7. 一点实操体会

整个四集笔记跟下来,我最明显的感觉是:Stable Diffusion 不再是一个“黑盒咒语生成器”,而是一台可以拆开检修的机器。最值得花时间的部分不是去背 CLIP 的 tokenizer 细节,也不是手动推导每一行数学公式,而是亲手跑一遍最小代码,把 5.2 节里那几十行流程在脑海里反复过几遍。真正落地的那一瞬间,你会突然明白为什么社区里那么多人强调 CFG 要设在 7 而不是 10,为什么步数超过 30 后收益急剧下降,为什么提示词里最重要的词要放在前面。这些经验,单靠看别人写结论是记不牢的,必须自己在生成结果上对比过才有感觉。希望这篇笔记能帮你从“会用”跨到“理解”,下一次再看到生成崩坏的图,你不只会重新抽卡,还会下意识想一下:是文本条件没传进去,还是调度器参数不合理,又或者是 VAE 解码出了问题。这就是理解原理带来的最大红利。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询