Stable Diffusion文生图全解析:从CLIP到UNet的扩散模型原理与实战
1. 先搞清楚这套系统到底在做什么:从一张噪点到一幅画
Stable Diffusion 这套系统,你拆开来看,核心其实就三块:一个负责理解文字的 CLIP,一个负责"去噪"的 UNet,还有一个负责调度整个去噪节奏的采样器。很多人一上来就被"扩散模型""潜在空间""交叉注意力"这些词吓住,但本质上,它干的事情很简单:从一个随机噪点出发,一步步把噪点擦干净,最终显露出你想要的图像。就像一个盲人在一块大理石上雕刻,先敲掉大块,再精修细节,最后呈现出一尊雕像。我写这套四集精讲笔记的初衷,就是把这条链路上每个环节的"为什么"讲透,而不是丢一堆 Python 代码让你照着抄。
这套方法适合谁?说实话,只要你对"文生图"这个词感过兴趣,无论你是搞设计的、做算法的、还是纯粹想玩玩本地部署的爱好者,都能从这套笔记里找到自己需要的部分。设计师可能只需要理解 CLIP 为什么能听懂人话、CFG 参数调太高为什么图会发黑;算法工程师可以深入去看 UNet 里的时间步嵌入和注意力权重;而动手型玩家,我希望你能照着最后一集的代码,在自己电脑上跑通一个最小流程——那才是真正产生"原来如此"感受的时刻。
我见过太多人拿着别人训练好的模型埋头调参数,却完全不知道 PD 和 DDIM 有什么区别,也不知道为什么换了采样器出图风格会变。这不是你的错,是现有教程普遍在"原理"和"代码"之间那道鸿沟上跳过去了。这套笔记就是来填这条沟的:我会从整体流程开始,一步步拆解 CLIP 的编码逻辑、扩散模型的正向加噪与逆向去噪、UNet 的结构与注意力机制,最后给出一段真正能跑通的文生图代码,并附上我在实际部署中踩过的坑。如果你按顺序读下来,再回头去看那些"调参数玄学",你会发现大部分问题都有了合理的解释。
1.1 文生图的本质与整体流程
先不要管任何数学公式,用一句话概括整个 Stable Diffusion 的推理流程:你输入一段文字,然后系统在纯随机的噪声图上,按照文字给出的"方向感",一步步将噪声抹去。每一步都基于两个信息——当前这张图长什么样,以及文字希望它长什么样。两个信息被拼接在一起,输入到 UNet 这个"雕刻家"里,UNet 预测出"下一步应该往哪个方向修",然后采样器就据此更新图片。
这个流程里的关键点在于,UNet 不是一次性预测出最终图像,而是"每步只预测一小步"。为什么这样?因为一步到位太难了,神经网络在复杂的图像分布上直接映射,结果会非常粗糙。而迭代式的去噪,把一个大难题拆成了几百个小步骤,每一步的预测任务都相对简单,误差也就更可控。扩散模型的名字就来源于这个"拆步"的思路——它受热力学中"扩散"过程的启发:如果你把一滴墨水滴入清水,它会不可逆地扩散均匀;扩散模型就是学习这个扩散过程的"逆过程",从均匀混沌的噪声中找回原本墨滴的形状。
1.2 为什么是潜在空间:不是画布太大,是计算太贵
讲到 Stable Diffusion,就绕不开"潜在扩散模型"(Latent Diffusion Model)这个说法。为什么不直接在像素上做扩散?原因特别现实:一张 512x512 的 RGB 图片,有 78 万个像素点,每个点有 3 个通道。你在这么大尺寸上迭代 50 步,每一步都要让神经网络处理几十万维的数据,训练和推理的计算量都大到离谱。
所以 Stable Diffusion 采用了一个很聪明的前置操作:先通过一个叫 VAE(变分自编码器)的模型,把图片压缩到一个很小的"潜在空间"里。这个压缩不是简单的缩放,而是把图片的语义信息编码成一组低维向量——你可以理解为,它在 8x 到几十倍的压缩率下,仍然保留了"画面里有什么、布局如何"这样的高层信息。扩散和去噪过程都在这个潜在空间里进行,最后再用 VAE 的解码器把去噪后的潜在向量还原成像素图像。这就是"潜在"两个字的核心含义。
我刚接触的时候把潜在空间想象成"草稿",还挺贴切的:模型在草稿本上反复修改构图和物体,改满意了再誊到正式画布上。这个设计的直接好处是,训练成本大幅下降,普通显卡也跑得动;后来各种"加速 LoRA 微调"的思路,也都是建立在潜在空间这个底座上的。
2. CLIP:让机器把文字和图像对齐的翻译官
Stable Diffusion 里接收你文字的那个模块,就是 CLIP。CLIP 的全称是 Contrastive Language-Image Pre-training,由 OpenAI 在 2021 年提出。它的核心思想非常直白:训练一个模型,让它在同一个向量空间里,把相关的图片和文字放在相近的位置。相关性越高,向量距离越近;不相关,就推远。
这个概念听着简单,但它是整个文生图体系的基石。没有 CLIP,你的提示词对模型来说就是一堆无意义的符号;有了 CLIP,模型才能在每一步去噪时,"读懂"你应该往哪个方向修图。
2.1 CLIP 的核心设计:双塔编码器与对比学习
CLIP 的网络结构一般叫"双塔结构":左边一个图像编码器,右边一个文本编码器。图像编码器可以是 ResNet 或者 ViT,文本编码器通常是 Transformer。这两个编码器各干各的,但它们的输出被映射到同一个多维向量空间里。
训练方式用的是对比学习。想象你有一批图片,每张图片配了一段描述文字。模型的目标是:让"正确的图片-文字对"的相似度分数尽可能高,让"错误的图片-文字对"的分数尽可能低。这里的相似度通常用余弦相似度计算。因为训练数据足够大(OpenAI 用了将近 4 亿个图文对),CLIP 最终学到了一种"通用对齐关系":不只是记住训练集里的配对,而是能理解"一只戴着帽子的狗"这种在训练数据里可能没见过的组合。
这个特性对 Stable Diffusion 至关重要。因为用户输入的提示词千奇百怪,模型必须有能力把"没见过的新组合"映射到视觉概念上。CLIP 的文本编码器就像一个经验丰富的翻译官,不仅懂语法,还懂画面感。
2.2 在 Stable Diffusion 中 CLIP 到底扮演什么角色
在 Stable Diffusion 的 UNet 里,CLIP 的文本编码器被用来把提示词转成一组 embedding 向量。注意,这里用的是 CLIP 的文本编码器部分,图像编码器在推理阶段基本不用。这组向量会通过一种叫"交叉注意力"的机制,注入到 UNet 的各个层中,指导去噪过程。
具体来说,UNet 在处理每一张噪声图时,会同时关注两个东西:一个是当前图像自身的特征,另一个是用 CLIP 提取的文本特征。两者通过注意力层融合。你可以把交叉注意力想象成"在作画过程中,每画一笔都要抬头看一眼前人给的草稿,对照一下方向对不对"。草稿就是提示词编码,画布就是潜在空间里的噪声图。
CLIP 在这里还有一层特殊作用:它的 text encoder 输出的是 77 个 token 的序列,超出部分会被截断或者通过其他方式扩展。这也是为什么你在输入超长提示词时,会发现后半段的影响变弱——因为 CLIP 的文本编码器输入端就是这么设计的。了解这个限制,你就能理解为什么有些插件(比如长提示词增强)能提升表现力,本质是绕开了这个 77 token 的瓶颈。
2.3 动手看 CLIP 的输入输出:一段 Python 实测
光说原理太空,我建议你亲手跑一下这段代码,直观感受 CLIP 文本编码器的输入输出。下面用 transformers 库加载最常用的 CLIP 文本编码器,看看它对一句话的编码结果长什么样。
from transformers import CLIPProcessor, CLIPTextModel import torch model_id = "openai/clip-vit-large-patch14" processor = CLIPProcessor.from_pretrained(model_id) text_model = CLIPTextModel.from_pretrained(model_id) text = "a portrait of a cat, detailed, photorealistic" inputs = processor(text=text, return_tensors="pt", padding=True) outputs = text_model(**inputs) # 输出是一个 [batch_size, seq_len, hidden_dim] 的张量 # hidden_dim 是 768,seq_len 是 77(CLIP 固定长度) last_hidden_state = outputs.last_hidden_state print(last_hidden_state.shape) # 拿第一个 token(对应 [CLS])的向量作为整句的全局表示 sentence_embedding = last_hidden_state[:, 0, :] print(sentence_embedding[:5]) # 打印前 5 个数字看看维度值你跑完会看到,张量形状是[1, 77, 768]。这就是 CLIP 给 Stable Diffusion 的文本特征。其中 77 是它固定的序列长度,768 是隐藏维度。在 Stable Diffusion 内部,这个原始向量还会经过一个线性层投影到 UNet 使用的维度。这一步实测的意义在于,解掉你对 CLIP 的"黑盒感"——它没有魔法,就是一组高维数值,后续所有关于提示词、CFG 的讨论,都是建立在这组数值之上的。
另外一个常见疑问是:CLIP 的文本编码器能不能微调?当然能。我在笔记里专门讲过 CLIP 微调对文生图风格的影响。从我的实验看,微调 CLIP 比微调 UNet 影响更"全局",因为它改变的是对提示词的整体理解方式,就像换了翻译官的母语习惯。但这也意味着风险:微调不当会让原本能理解的提示词变"笨",所以新手不建议轻易动 CLIP。
3. 扩散模型与 UNet:一步步去噪的雕刻家
CLIP 帮你"读懂了要求",接下来要执行去噪工作的,就是 UNet。UNet 这个名字听起来像某种网络协议的缩写,其实它源自 2015 年一篇医学图像分割论文,因为网络结构呈 U 形而得名:左边逐步下采样提取特征,右边逐步上采样恢复尺寸,中间有跳跃连接保留细节。这个结构最初是给分割任务设计的,没想到在扩散模型时代成了主力工具。
要理解 UNet 在 Stable Diffusion 里干了什么,得先搞清楚扩散模型的两个阶段:前向过程(加噪)和逆向过程(去噪)。
3.1 前向扩散:给图片慢慢加噪的过程
扩散模型的想法是:如果我们能把一张清晰的图片,一步步"破坏"成纯噪声,那么反过来,能不能从纯噪声一步步"重建"回清晰图片?前向过程就是那个逐步破坏的动作。每一步,我们在图片上叠加一点高斯噪声,并且用当前步数 t 来控制噪声强度。这个过程的数学形式是:
x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon
其中epsilon是从标准高斯分布采样的噪声,alpha_bar_t是递减的系数,控制 t 越大时原图保留越少、噪声越多。到足够大的 T 步时,x_T基本就是一张纯噪声图。
这个公式看着简单,但它有一个漂亮的性质:给定 x_0,你可以直接跳到任意第 t 步的加噪结果,而不必一步一步模拟。这一下子就让训练变得高效了。训练目标是让 UNet 学会预测出这个噪声epsilon,而不是预测原图或者预测分数。
我估计有读者会问:为什么预测噪声,不直接预测原图?实验发现,预测噪声在训练和采样过程中稳定得更好。你把它理解成"雕刻家只需要知道自己下一步该凿掉哪块废料"就顺了:找到废料,去掉它,剩下的自然就是雕像。
3.2 逆向去噪:UNet 是如何学会"猜"噪声的
真正生成图像时,我们做的是逆向过程。先从纯噪声x_T出发,然后需要模型告诉我们:当前这幅图里,包含了什么样的"可去除噪声"。UNet 输入两个东西:当前噪声图x_t,以及一个时间步的信号t。输出是一张预测的噪声图epsilon_hat。然后采样器根据epsilon_hat和某种调度规则,算出下一步的图像x_{t-1}。如此反复,直到x_0。
这听起来像一个自证过程:噪声图里的"噪声"和"内容"是混合在一起的,模型怎么知道哪些该去掉?答案来自训练。训练阶段,我们给 UNet 看了海量的"第 t 步噪声图 + 对应的真实噪声"配对,UNet 就是在学这样一个映射:给定当前图,它里面藏着的那团噪声长什么样。它不需要知道图里的物体叫什么,只需要能从噪声图里分离出噪声成分。
我在笔记里画过一张对比图:前向过程是"图片 → 加噪 → 噪声",逆向过程是"噪声 → 去噪 → 图片"。你会发现,这两个过程使用的网络权重其实是同一个 UNet,但它们对应的场景不同,所以模型中大量用到了条件信息——这就是为什么 UNet 里会有那么多注意力层,它需要整理来自文本、图像和时间步三方面的信号。
3.3 UNet 里的注意力机制:为什么它能理解提示词
UNet 不是那种简单的卷积堆叠。Stable Diffusion 用的 UNet 加入了多个注意力模块,其中最重要的是交叉注意力(cross-attention)。交叉注意力的查询来自图像特征,键值来自文本特征。这样网络在做局部操作时,可以"查询"提示词中的相关部分。
用个生活例子:你在画一只"戴帽子的猫",UNet 的每个卷积块就像你在挥舞画笔,而交叉注意力就是"你时不时低头看一眼草稿上画的帽子应该在哪个位置、什么形状"。它让噪声图里每个区域,都能从文本序列里挑出最相关的单词来指导自己的绘制。这也解释了为什么"cat" 和 "hat" 的 embedding 之间的关系会对画面产生那么大的影响——交叉注意力机制本质上建立了文本和图像区域之间的软关联。
时间步 t 的处理也有讲究。UNet 会通过一个正弦位置编码将 t 变成向量,然后加到所有层的特征上。这样同一个网络,在处理"噪点很多的前期步骤"和"细节丰富的后期步骤"时,会主动调整自己的行为方式:前期负责铺大色块,后期负责描细节。
3.4 训练与推理:两个阶段完全不同的逻辑
训练阶段和推理阶段有一个非常容易混淆的地方,我单独拎出来说。训练扩散模型时,我们需要的是完整的加噪-去噪循环:随机选一张真实图,随机选一个步数 t,对它加噪,让 UNet 预测噪声,然后用均方误差损失更新权重。
而推理阶段,也就是你实际生成图片时,根本没有"真实图"作为参照,就是从噪声出发,靠 UNet 的预测驱动整个去噪链条。你可能会觉得奇怪:没有真实图,模型怎么会越去噪越像真实图?这恰恰是扩散模型训练生效的结果——网络在训练中学到了真实图片的分布特征,所以它在去噪时总是倾向于生成"看起来像真实图像"的结构,而不是随机图案。
推理阶段还有两个关键参数:步数 steps 和引导强度 guidance_scale。步数越多,去噪过程越精细,但速度越慢;guidance_scale 控制的是"文本条件"的权重,越高代表越忠实提示词,但过高会导致色彩过饱和甚至崩坏。这两者我在下一部分的代码实战里都会用到。
4. 文生图代码实战:从零跑通一个最小流程
原理聊到这儿,不写代码等于没讲。这一节我带你把 Stable Diffusion 的文生图跑通。其实现在用 diffusers 库,代码量少得惊人,但我更希望你在跑通之后,能回头对应到前面讲的每个部件——注意观察哪里是 VAE 编码,哪里是 CLIP 编码,哪里是 UNet 去噪,哪里是 VAE 解码。
4.1 环境准备与依赖安装
我以当前比较稳定的 diffusers 版本为例。建议你用一个干净的 Python 3.10 环境,先装 PyTorch(带 CUDA 的版本),再装几个库:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install diffusers transformers accelerate safetensors装完之后可以用一个小脚本确认一切正常:
import torch from diffusers import StableDiffusionPipeline print(torch.cuda.is_available()) print(torch.__version__)如果你没有独立显卡,内存大于 8GB 也能硬跑,但速度会慢到怀疑人生。我个人的建议是:至少一块 6GB 显存的显卡,或者直接用在线服务试水。本地环境的坑,最常见的就是 CUDA 版本和 PyTorch 对不上,所以我用--index-url明确指定了 cu118 这个版本号,省得后面到处报错。
4.2 用 Transformers 和 Diffusers 实现文生图
核心代码就下面这一段。我用的是 Stable Diffusion 2.1 基础版,模型文件会自动从 Hugging Face 下载到本地缓存,第一次跑会要些时间。
from diffusers import StableDiffusionPipeline import torch # 加载管线:这里会自动拉取 text encoder、VAE 和 UNet 三个组件 pipe = StableDiffusionPipeline.from_pretrained( "stabilityai/stable-diffusion-2-1-base", torch_dtype=torch.float16 ) pipe = pipe.to("cuda") # 建议开启这个,能省不少显存 pipe.enable_attention_slicing() prompt = "a cinematic photo of a cat astronaut, detailed fur, nebula background" image = pipe( prompt, num_inference_steps=50, guidance_scale=7.5, seed=42 ).images[0] image.save("cat_astronaut.png")注意我传了seed=42,这是为了结果可复现。diffusers 库的新版本用的是generator参数,我在笔记里用的是旧接口,其实两者等价。你看,短短几行就生成了图片。但如果你只满足于这个 Demo,那这套笔记就白看了。我们拆解一下这段代码背后发生了什么:
pipe对象内部其实是一个StableDiffusionPipeline,它按顺序调用 text encoder → UNet → VAE decoder 三个组件。image是一张 PIL 图片,它最终由 VAE decoder 把潜在空间的向量解码回像素。num_inference_steps=50意味着 UNet 会被调用 50 次,一次一步地修正潜在向量。
4.3 核心参数逐个调一遍:steps、guidance_scale、scheduler
参数调参是玄学这种说法,本质是不理解各参数在流程里的作用。我用自己的实验数据给你列一张表,方便你看着调:
| 参数 | 取值范围 | 作用 | 调高了会怎样 |
|---|---|---|---|
steps | 20~100 | 控制 UNet 迭代次数 | 细节更丰富,但时间更长,超过 50 收益递减 |
guidance_scale | 5~15 | 控制文本条件强度 | 过于贴近提示词,色彩易饱和、出现伪影 |
seed | 任意 | 控制初始噪声 | 固定种子换提示词,能观察到风格稳定变化 |
关于 steps 我有一个实测体会:从 20 步提高到 50 步,效果提升非常明显;从 50 提到 100,肉眼几乎看不出差别,但耗时翻倍。所以日常生成用 30~50 步是性价比最高的区间。
guidance_scale这个参数对应原理部分说的"文本条件权重"。它的实现方式是采样时做两次 UNet 前向:一次是有文本条件的预测,一次是无文本条件的预测,最后用noise_pred = noise_uncond + guidance_scale * (noise_cond - noise_uncond)融合。这个公式解释了为什么过高的值会导致生成结果"过于用力"——它放大了文本的影响,同时也放大了误差。
另外,你还可以换采样器(scheduler)。默认的PNDMScheduler是确定性采样,DDIMScheduler步数更少但质量相近,DPMSolverMultistepScheduler则几乎成为当下默认选项,因为它在保证质量的同时能把步数压到 20 左右。换采样器的代码就一行:
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)我在笔记里强烈推荐新手直接上 DPM++ 这类采样器,因为它的 20 步效果接近默认的 50 步,视觉上没有明显退化。这个结论不是玄学,DEIS 和 DPM 这类采样器本身就是在数学上做了更高效的离散化逼近。
5. 实际踩过的坑与调优心得
最后一章,我把自己跑这套流程时真实踩过的坑整理出来。这些经验不是教科书上会写的,但每一个都耽误过我至少半小时到一天的时间。
5.1 显存不够怎么办:优化选项清单
很多人问,6GB 显存能不能跑 512x512 的 Stable Diffusion?能,但你必须开优化。diffusers 提供了一系列开关,我从最有效到最辅助排序:
enable_attention_slicing():把注意力计算切块,显著降低峰值显存。enable_vae_slicing():将 VAE 解码过程拆成小块,省 30% 左右显存。- 采样分辨率降到 512x512 以下:潜在空间是直接按比例缩放的,分辨率降低,显存占用是近似平方下降。
- 开启
torch.compile(如果你的 PyTorch 版本支持):不仅是加速,有时候也能减少部分临时张量的开销。
我自己的经验是,三行优化全开之后,5GB 显存跑 512x512 的 20 步迭代没有压力。如果还是爆显存,建议考虑用 SD 1.5 的官方低分辨率模型,效果差异不大,但显存友好很多。
5.2 出图质量不稳定:种子、步数与 CFG 的平衡
新手常见抱怨是"同一句话,每次生成都不一样,是不是出 bug 了"。记住,只要把seed固定住,生成结果就是完全可复现的。所以做比较实验时,请永远固定种子。种子不同,初始噪声就不同,最后画面构图不同是正常现象,这也是发散灵感的好工具——你甚至可以用固定提示词 + 随机种子当"抽卡"玩。
不稳定感的另一个来源是 CFG(guidance_scale)设置过高。比如你把 guidance_scale 调到 20,它虽然会在语义上更贴近提示词,但图像会变得"油腻",背景画风杂乱。我的建议是先调到你喜欢的步数,再逐渐减小guidance_scale,直到出现你能接受的最自然风格。通常 6~8 是比较稳的甜点区间。
5.3 CLIP 微调到底有用吗:个人经验分享
关于 CLIP 微调这件事,我在笔记里单独开过一节。很多人误以为微调 CLIP 能让自己的模型更好地"理解某种画风",比如把脸画得更美、更二次元。但这其实是个误区——CLIP 负责的是"文-图对齐",不是"画风渲染"。画风主要通过 UNet 和微调 UNet(比如 LoRA、DreamBooth)来改变。
那 CLIP 微调有什么用?有,但作用很"底层"。它可以让你的模型在训练数据有限的领域里,遇到更贴合的语义特征,比如某个特定的角色名、产品或艺术流派专属词汇。但你必须清醒地认识到:CLIP 是文生图链路里的"总指挥",动它一下,所有提示词的响应都会变,所以它更像是"让模型听得懂更专业的话",而不是"让模型画得更像谁"。如果你想让模型输出特定角色,去微调 UNet 和 VAE decoder 才更靠谱。我做过一个邪道实验:给 CLIP 文本编码器做了几天 LoRA,结果对普通画风的促进微乎其微,反而让一些常规词汇的理解出现了位移,最后只好回滚。
5.4 从笔记到实践:完成四集精讲的学习路径
这套四集笔记我整理过一段时间,它的原版相当于是"从零到部署"的完整视频课,每个章节都配了代码和检查点。我提炼出最重要的部分,做成现在的文字版:第一集确认整体架构与 CLIP,第二集深入扩散模型与 UNet,第三集是拉通跑一个最小流程,第四集是各种部署优化与质量调优。你现在看到的这篇文字,基本是前三集的浓缩加第四集的心得。
如果你想真正学会,我强烈建议你按这个顺序来:先用自己的话把这篇文章的流程图画一遍;然后用我给的代码跑通一张图,哪怕是粗糙的;接着试着换一个提示词、换一个采样器、换一组 steps,观察画面变化。只要你能解释清楚"为什么换采样器后 20 步效果反而更好",你就已经走出了纯玄学调参的怪圈。
最后再分享一个小技巧:我习惯把每次实验的提示词、参数、种子、生成结果截图一起放进一个 Markdown 表格里。当你积累了 50 组记录之后,你再回头看参数之间的相互作用,就会发现很多规律是肉眼扫一眼根本看不出来的。这套笔记的价值不在于让你背下所有概念,而是让你知道去哪里找答案、怎么看参数之间的关系。这一行就是这样——没有一劳永逸的完美参数,只有不断摸索中沉淀出的感觉。