这套Stable Diffusion原理详解是我压了很久的笔记。当初为了跑通一套完整的文生图流程,我把CLIP、UNet、扩散模型这些名词翻来覆去看了好几遍,又对着HuggingFace的diffusers源码一点点比对,才算真正把这条链路理顺。网上讲SD原理的文章不少,但大多是单点讲解:要么纯讲扩散模型数学,要么只给代码不讲为什么;真要把“CLIP怎么把文字变成条件”“UNet在中间到底干了什么”“为什么最后还要加一个VAE解码器”串起来讲清楚的文章,反而很少。所以这份笔记主打一个思路:按数据流的方向,从文本到特征、从特征到噪声、从噪声到图像,一步步拆开看,最后一节直接上代码实战,把前面所有的原理落到几行能跑的脚本里。不管你是刚接触AI绘画的新手,还是想深入源码的开发者,这套拆解思路都能帮你省下不少走弯路的时间。
1. 整体架构先立起来:四个组件各管哪一段
Stable Diffusion这类文生图模型,本质上做的是这么一件事:输入一段文字,输出一张图。但“文字变成图像”这件事不能一步到位,中间必须经过几次形态转换。整套模型就是由负责这些转换的组件拼起来的流水线。我用最直白的方式把这四块“岗位职责”列一下:
| 组件 | 输入 | 输出 | 干的活 |
|---|---|---|---|
| CLIP文本编码器 | 自然语言文本 | 文本特征向量(条件) | 把“一只猫坐在沙发上”变成模型能“听懂”的数字向量 |
| 扩散模型主干 | 随机噪声 + 文本条件 | 去噪后的潜空间特征 | 在潜空间里反复“猜噪声、去噪声”,逐步画出图像轮廓 |
| UNet去噪网络 | 噪声潜变量 + 时间步 + 文本条件 | 预测出的噪声 / 去噪后的潜变量 | 扩散模型内部的核心引擎,负责具体计算 |
| VAE解码器 | 潜空间特征 | 像素级图像 | 把压缩后的特征“放大”还原成一张正常的RGB图片 |
先记住这张表。接下来讲的所有细节,都是在给这四个组件填充“为什么它要这么设计”。很多教程一上来就开始推扩散模型的数学公式,结果读者看了半天也不知道CLIP在哪里发挥作用,其实是因为没先把整体流水线立起来。真正理解SD的捷径,是跟着数据走一遍:文本先进CLIP,变成一个条件向量;随机噪声在UNet的引导下,一步一步去噪;去完噪得到的结果还在潜空间里,必须经VAE解码器还原,肉眼才能看到图。四个部分缺一不可。
我自己当时最大的误区,是以为“扩散模型”就等于Stable Diffusion的全部。看完源码才发现,扩散只是这套系统里的生成策略,真正的计算主力是UNet,而CLIP和VAE分别是入口和出口的“翻译官”。把这一层关系理顺了,后面所有细节都很好理解。
2. 文本入口:CLIP编码器到底做了什么
2.1 CLIP的核心思路:图文对齐
CLIP是OpenAI在2021年提出的模型,全称是Contrastive Language-Image Pre-training,直译过来就是“对比学习的语言-图像预训练”。它最核心的任务,是学会判断“一段文字和一图片是不是匹配的”。训练的时候,模型会同时看到大量图文对,它要做的事情是:把属于同一语义的文字和图片在特征空间里拉近,把不相关的图文对推开。这样练完之后,模型里的文字编码器就具备了一种能力——把一句话变成一串有“语义方向”的向量。
在Stable Diffusion里,我们只用到了CLIP的文本编码器部分。这很关键。以前很多生成模型做条件控制,会把文本简单处理成词向量或者用LSTM编一下,效果都不理想。CLIP的优势在于,它的特征空间经过海量图文对训练后,天然具备“语义对齐”的能力,也就是说,模型知道了文字对应的视觉特征大概长什么样。这让它非常适合给扩散模型当“带路党”。
2.2 从句子到token再到特征向量
CLIP文本编码器的内部结构是Transformer,和GPT那类模型同一个家族。但进入Transformer之前,必须先做tokenize。HuggingFace的CLIPTokenizer会把一句话切分成一个个子词片段,每个片段对应一个ID,然后再把ID映射成词嵌入向量。
这里有个经常被忽略的细节:SD用的CLIP模型,上下文长度只有77。也就是说,不管你的提示词写多长,CLIP只会处理前77个token,后面的内容会被直接截断。这也是为什么很多人发现提示词写到特别长之后,多出来的部分完全不起作用。所以写提示词的时候,真正关键的信息尽量往前面放,后半段基本属于无效区域。
2.3 为什么条件向量这么重要
CLIP编码器最终输出的,是一个维度为768(SD 1.x)或者1280(SD 2.x)的特征向量序列。这个序列会和后续UNet的每一步去噪过程结合,指引模型“该往哪个方向画”。实际调试的时候你会发现,提示词改了之后,生成结果变化往往很显著,就是因为文本特征向量本身发生了方向性偏移,连带着去噪轨迹也跟着变。理解了这一点,你就明白为什么很多人强调“提示词写清楚、写具体”,因为模糊的文本映射出来的条件向量也是模糊的,模型当然不知道该怎么画。
3. 扩散模型:图像是怎么“从无到有”生成的
3.1 前向过程:给图像一点点加噪
讲扩散模型,一定绕不开一个核心思想:把生成过程倒过来看。怎么理解?想象一张正常的图片,你往上面一点一点加随机噪声,加到最后,图片就变成了一团纯高斯噪声,什么都看不出来了。这个过程叫“前向过程”,或者说“加噪过程”。前向过程本身没有学习任何参数,它就是一个定义好的、逐步把数据毁掉的过程。
数学上,这个过程可以用公式表示:每一时间步都往图像上叠加一点高斯噪声,同时让原来的图像信息衰减一点。经过足够多的时间步之后,原图像的信息几乎全部丢失,剩下的就是纯噪声。这个设计的好处是,每一步加噪的公式都是确切可算的,方便后续训练时构造样本。
3.2 反向过程:学习逐步去噪
扩散模型真正要学的,是反向过程——也就是如何从一团纯噪声里,一步步把原来的图像恢复出来。这个反向过程没法直接计算,所以要用神经网络来拟合。UNet就是干这个活的。训练目标很简单:给出一个加了噪的图和时间步t,让网络学会预测加进去的噪声长什么样。预测出噪声之后,把它从加噪图里减掉,就得到更接近原图的结果。反复进行这个“预测噪声、减去噪声”的操作,最后就能从纯噪声还原出一张清晰图像。
这里要特别说明一下直觉。很多初学者会认为UNet像“PS的修复画笔”一样,直接重构原图。但实际不是。它做的是“猜错题”:给它看加了噪声的图,它要告诉你“噪声部分长什么样”。把噪声抽离之后,剩下的自然就是干净图像。这种范式的好处是,噪声分布是固定的高斯分布,比直接预测图像像素要稳定得多。
3.3 采样器和步数:从DDPM到DDIM
有了训练好的UNet,生成图像还需要一个采样器。最初的DDPM采样器需要走上千步才能生成一张不错的图,速度很慢。后来DDIM出现,通过改变采样步长的方式,把步数压缩到50步甚至更少,图像质量和推理速度的平衡一下好了很多。
实际用的时候,采样步数通常设置20到50步。步数太少,去噪不充分,画面会发灰、细节缺失;步数太多,边际收益递减,还白白浪费时间。采样器类型也影响效果,DPM++系列、Euler a这些都是常见选项。我在实践中最开始喜欢用Euler a,因为它速度极快;追求精致度的时候会切到DPM++ 2M Karras。要注意的是,采样器本质上是在“控制反向去噪时的步长策略”,换成不同采样器,同一组提示词和种子出图效果会很不一样,这跟前面的数学模型是一体的,不是玄学。理解这一点之后,你就不会在“为什么换采样器图就变了”这种事上反复困惑了。
4. UNet:真正在干重活的那个画师
4.1 为什么是U形结构
UNet最早是用于医学图像分割的,形状大概是左边逐层下采样提取特征,右边逐层上采样恢复分辨率,中间用跳跃连接把同尺度的特征拼接起来。整个网络从侧面看像个U字,所以叫UNet。Stable Diffusion沿用这种结构,是因为它非常适合这种“既要理解全局语义,又要保留局部细节”的任务。下采样阶段让模型看到更大范围的内容,上采样阶段把细节逐步找回来。
用生活化的类比解释:你可以把它想象成先画草图再描细节。下采样阶段相当于从远处打量整体构图——哪里是天空,哪里是地面,哪里有一个人;上采样阶段则像走进画面,把五官、发丝、衣服褶皱一点点补上。跳跃连接的作用更绝:它把下采样时保留的高频细节直接拷贝到上采样对应层,避免信息在层层压缩中丢失。
4.2 时间步嵌入:让网络知道“现在走到哪一步了”
扩散模型里有时间步t,表示当前的噪声程度。t越大,图像越接近纯噪声,去噪压力也越大;t越小,图像越接近成品,只需要做微小修正。UNet必须知道当前处理的是哪个阶段,才能采取合适策略,因此每个时间步t都会被编码成一个向量,加到网络的各个层里。这个机制叫“时间步嵌入”(time embedding)。做完之后,网络就能根据时间步自适应调整表现:噪声大的时候大胆处理,接近成品的时候小心翼翼。这一点在代码里体现得很直观,后面我会贴出来。
4.3 CrossAttention:文本条件是如何注入的
文本条件要发挥作用,需要在UNet的每个关键层里“渗透”进去,用的机制就是CrossAttention(交叉注意力机制)。简单说,UNet在处理的图像特征会作为Query,CLIP输出的文本特征作为Key和Value。网络每处理一个图像区域,都会问一句:“当前我看到的这块内容,和文字里的哪个词最相关?”然后把相关的文本信息融合进来。这就是提示词能控制画面内容的根本原因。
为什么用CrossAttention而不是别的方式?因为CrossAttention天然适合“两种不同模态之间的对齐”。图像特征来自CNN结构,文本特征来自Transformer结构,两者本质上是不同类型的数据,直接拼接效果很差。CrossAttention通过注意力权重让文本信息动态地、按需地注入到图像生成过程中,灵活性远高于简单的拼接或相加。实际效果也很明显:你写“一条穿着宇航服的狗”,CrossAttention会在狗的躯干区域加大“宇航服”文本特征的权重,生成结果里才会出现宇航服质感。
5. 潜在空间与VAE:显存是怎么省下来的
5.1 为什么要做潜在扩散
Stable Diffusion的全称实际是“潜在扩散模型”(Latent Diffusion Model)。关键就在“潜在”两个字:它不是在像素级空间里直接做扩散,而是先把图片压缩到一个低维潜空间,在潜空间里完成加噪、去噪、生成的全过程,最后再还原回像素图。为什么这么设计?因为像素级空间维度太高了。一张512乘512的RGB图片,直接做扩散模型,每一步的计算量都大到普通显卡根本跑不动。潜空间长宽只有原图的八分之一,通道数也大幅减少,计算量下降得非常明显。这就是为什么Stable Diffusion能在消费级显卡上运行,而同期的很多Pixel空间扩散模型需要超大显存。
5.2 VAE的编码与解码
VAE在这里扮演的角色很像一个“压缩解压器”。编码器把512乘512的图片压缩成64乘64的潜空间特征,解码器再从64乘64的潜空间特征还原成512乘512的图片。整个扩散、去噪过程都发生在64乘64的低维空间中。因为空间变小了,模型参数量不用太大就能获得足够的建模能力,训练和推理都轻快许多。
实际使用中,VAE还有一个很实用的变体。原版VAE有时会让画面色彩显得发灰,很多社区玩家会换成“ema”版本的VAE,或者专门的“kl-f8-anime2”等微调版本。换完之后色彩通常会更鲜艳、对比度更舒服。这也解释了为什么懂行的玩家总说“检查VAE”是排查画面发灰的重要步骤——这个操作几乎零成本,却能极大改善成图观感。
5.3 潜空间里的压缩损失
当然,VAE不是无损压缩。它在压缩过程中会损失一部分高频细节,这也是为什么Stable Diffusion生成的图,放大之后偶尔会出现细节纹理不自然的情况,尤其面部、手部这些精细部位更容易出问题。这也引出了用手部修复、局部重绘等工具的必要性。理解了VAE的原理,你就明白局部分辨率为什么不能只靠加大采样步数解决,根源在潜空间的信息容量上。
6. 文生图代码实战:用diffusers三分钟跑通全流程
6.1 环境准备
代码实战我选了HuggingFace的diffusers库,原因很简单:它把前面讲的那一堆组件都封装成了开箱即用的Pipeline,同时源码结构又足够清晰,能让你自由替换各个组件来加深理解。环境方面,Python 3.10以上,PyTorch 2.x都可以。有NVIDIA显卡最好,显存8G以上就能玩得很舒服;没有显卡也可以用CPU跑,只是会慢很多。
先安装依赖:
pip install diffusers transformers accelerate safetensors如果你的显卡是NVIDIA的,建议再加一个:
pip install xformersxformers能优化注意力计算,出图速度能提升不少。接下来在代码里选择模型,默认用runwayml/stable-diffusion-v1-5,这个模型是社区使用量最大的基座之一,兼容性非常好。
6.2 最小可运行代码
下面是完整可跑的文生图脚本,代码不长,但每一行都对应了前面讲的原理:
import torch from diffusers import StableDiffusionPipeline # 加载整个流水线:CLIP文本编码器 + UNet + VAE + 调度器 pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, # 半精度推理,省显存 ) pipe = pipe.to("cuda") # 模型搬到GPU上 prompt = "a cute cat sitting on a sofa, soft lighting, high detail" # 负向提示词:告诉模型不要出现什么 negative_prompt = "blurry, low quality, bad anatomy" # 设置随机种子,保证结果可复现 generator = torch.Generator(device="cuda").manual_seed(42) image = pipe( prompt=prompt, negative_prompt=negative_prompt, height=512, width=512, num_inference_steps=30, # 去噪步数 guidance_scale=7.5, # 提示词引导强度 generator=generator, ).images[0] image.save("output.png")这段代码,从原理角度拆解一下。from_pretrained加载时,diffusers会在内部装配好“CLIP文本编码器+UNet+VAE”三件套,同时创建一个调度器(scheduler)对象。调度器就是前面说的采样器逻辑所在。prompt先是经过CLIP编码,变成条件向量;然后生成一个随机噪声潜变量,进入UNet的“预测噪声-减去噪声”循环;num_inference_steps=30控制这个循环跑多少遍;最后VAE解码,输出图像。guidance_scale=7.5这个参数特别重要,它控制的是无分类器引导(Classifier-Free Guidance)的强度。简单理解:数值越高,模型越严格遵循提示词,但过高会出现色彩过饱和、画面生硬的问题;数值太低则提示词作用减弱。
6.3 进阶:逐组件替换加深理解
前面说了,diffusers好就好在组件可以单独拆开。比如你想单独看看CLIP编码器输出了什么,可以直接加载CLIP模型试试:
from transformers import CLIPTextModel, CLIPTokenizer tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14") inputs = tokenizer(["a cute cat sitting on a sofa"], return_tensors="pt") outputs = text_encoder(**inputs) print(outputs.last_hidden_state.shape) # torch.Size([1, 17, 768])看,这句话切分成了17个token,每个token对应一个768维的向量。这就是CLIP的产出。你也可以把StableDiffusionPipeline里的UNet单独拿出来,查看CrossAttention层的权重结构,观察文本特征是如何注入的。这一步做完,你对整个链路的理解会比看十篇教程都牢靠。
6.4 从代码反推原理:无分类器引导与种子
再补充一个代码里没明说但极其重要的细节:seed。同一个种子配合同样的提示词和参数,生成的噪声起始点完全一样,结果就会完全一致。这给调试带来极大便利:你可以固定种子去调整提示词,判断变化到底是哪个词引起的;也可以固定提示词而随机种子,用来批量筛选构图。实际工作流里,我通常先用不同种子批量出图,找到满意的构图之后再锁定种子,微调提示词细节。这个小习惯能节省大量反复抽卡的时间。而guidance_scale这个参数之所以有效,依赖的是无分类器引导技术——训练时随机丢弃文本条件,推理时同时跑“有条件”和“无条件”两条去噪路径,用它们的差异来加大提示词对生成方向的影响。整个逻辑都是统一的:要生成一张好图,不是靠单次操作,而是靠一组环节协同控制。
7. ComfyUI实操:把原理搬上可视化工作台
7.1 节点式工作流好在哪
ComfyUI把Stable Diffusion的整个工作流拆成了一个个独立的节点。CLIP加载是一个节点,CLIP文本编码是一个节点,KSampler是去噪采样器,VAE解码又是一个独立节点。所有节点之间用连线串起来。这样做的好处非常明显:每个环节都暴露在你面前,你可以直接看到数据从哪里来、到哪里去。
我第一次用ComfyUI跑通文生图的时候,最大的感觉就是:以前在代码里“看不见摸不着”的中间过程,全变成了一块块可视化的积木。CLIP节点的输出可以直接接一个预览节点查看;UNet的权重可以从加载节点单独替换;甚至采样器的参数在图表上都可以随时调。这种方式特别适合用来验证前面讲的原理——你亲手把“CLIP编码”节点接到“KSampler条件输入”端口上,自然会记住CLIP的输出是作为采样条件存在的。
7.2 从WebUI切换到ComfyUI的取舍
很多用惯了WebUI的人觉得ComfyUI上手有门槛,界面也不如WebUI直观。但我的实际体验是:如果只做简单文生图,WebUI完全够用,而且内置的风格化选项对新手很友好。但一旦涉及局部重绘(inpainting)、ControlNet、多模型串联混合这些复杂玩法,ComfyUI的灵活性就体现出来了。它不需要为每种玩法单独装一个扩展页面,只需要自己连一组节点。从数据结构的角度看,两种工具底层调用的组件完全一致,只是ComfyUI把控制权完整交还给用户了。所以在“原理理解”这件事上,ComfyUI其实是更好的学习工具——它能逼着你想清楚每个节点在数学上到底在做什么。
7.3 一个标准的ComfyUI文生图工作流
不用下载任何预设,从零搭建一个基础工作流只需要这样连:
Load Checkpoint节点加载模型底模(包含UNet、CLIP、VAE三件套);CLIP Text Encode (Prompt)节点输入正向提示词,另加一个节点输入负向提示词;- 两个CLIP节点输出都接到
KSampler的正负条件端口; Empty Latent Image节点设置画布尺寸,输出初始噪声潜变量;- 把潜变量接到KSampler的
latent_image端口; - KSampler设置好步数、CFG、采样器后,输出去噪完成的潜变量;
- 最后接
VAE Decode节点还原成像素图,再接到Save Image节点保存。
看着这套连线,再回头对照代码,你会发现每个节点就是一个代码函数,连线就是函数之间的实参传递。ComfyUI不是玩具,它是把复杂生成逻辑可视化之后的控制台。很多人说ComfyUI出图快,其实并不是模型本身变快了,而是节点流程固定之后省去了WebUI反复加载各类组件的时间开销。
8. 实战中的典型问题与排查技巧
8.1 手部崩坏难题:为什么AI总是画不好手
AI绘画最出名的槽点就是手部崩坏:多一根手指、手指粘连、关节错位之类的问题层出不穷。原因分两层。第一层,数据集里手部的形态变化极其丰富,而且手指的像素占比又很小,模型很难学出稳定的手部特征;第二层,VAE在压缩时损失了高频细节,手部的细微结构到潜空间里已经糊成一团了。修复手法网上流传很广,我实测下来最有效的三招是:
- 第一招,提示词层预防。正向提示词里写
detailed hands, five fingers, perfectly shaped hands,负向提示词里写bad anatomy, extra fingers, fused fingers。这不能百分百避免问题,但能明显减少出错率。原理其实就是加强CLIP引导方向,让UNet在CrossAttention阶段对手部区域多分配注意力。 - 第二招,局部重绘。把崩坏的手部区域用蒙版框出来,单独输入一个包含手部细节描述的提示词,让模型只在蒙版内重绘。因为局部画幅更小、任务更单纯,成功率会高很多。WebUI和ComfyUI都有对应的inpaint节点或功能。
- 第三招,手部修复插件或者专用大模型。社区出现了很多专门训练的手部修复LoRA,以及专门针对精细部位优化的模型。它们本质上是额外告诉UNet“手部应该长什么样”,效果非常直接。
不要指望一个提示词就彻底解决手部问题。最稳定的做法是组合拳:生成时预防 + 崩了之后局部重绘 + 实在不行用修复工具。
8.2 提示词与负向提示词的写法心得
提示词本身的组织也有规律。我的经验是:不要用长句堆砌,而是用一组权重明确的短语去描述视觉要素。什么算视觉要素?主体、动作、环境光、画质修饰词。比如“a red dress woman, standing on the beach, sunset backlight, cinematic composition, highly detailed”这种写法,模型理解起来远比写一大段完整句子要好。权重语法(keyword:1.3)也可以微调某个词的影响力。负向提示词要针对模型容易翻车的点来写,最常见的组合是“lowres, bad anatomy, bad hands, missing fingers, extra digit, blurry, jpeg artifacts, signature, watermark, username”。注意negative里的用词语义要具体,比如直接写extra digits通常比写ugly更有效。
8.3 显存不够与出图慢的排查
显存不足是跑AI绘画最常见的硬件问题。几个立竿见影的优化手段:第一,用torch.float16半精度加载,显存需求直接降低一半;第二,开启attention slicing或者直接用优化库xformers,降低注意力计算时的显存峰值;第三,降低出图分辨率。512乘512不够清晰,可以用低分辨率出图再加放大模型,而不是直接拉高采样分辨率。真正要弄清楚的是:潜空间做扩散的分辨率是64乘64,真正占显存大头的是VAE解码还原像素的这一步,以及UNet层中间特诊图的计算。所以当你想出1024乘1024的大图时,直接在潜空间上加大初始尺寸(也就是修改Empty Latent Image的尺寸),会比硬跑完整像素域高效得多。这又从实操层面印证了潜在扩散的意义。
9. 最后说几句我自己的体会
这套笔记从CLIP一路写到代码实战,内容量不小,但核心只有一条主线:Stable Diffusion不是某个单一模型,而是一条由“文本编码器-扩散引擎-图像解码器”组成的协作流水线。哪一环出了问题,最终图都会受影响。这也是为什么排查问题的时候,我习惯先判断问题出在哪个环节:提示词没生效,先看CLIP编码和CrossAttention;色彩发灰,去查VAE;细节虚糊,考虑采样步数和底模结构。思路对了,做排查就很快。
我个人最受用的一个扩展方向,是动手微调LoRA。LoRA修改的实际上就是UNet里CrossAttention层的权重矩阵,理解了这个底层原理,你再去跑LoRA训练,看到训练完成后出图风格改变,就不会觉得那是黑魔法了。这个过程反过来又加深了对CrossAttention机制的理解。Stable Diffusion这套架构后续衍生出的各种新模型,ControlNet、IPAdapter、SDXL、Sora等等,底层要么沿用这套链路,要么在此基础上做局部改造。把这份基础笔记吃透,后面学任何新东西都会快很多。希望这份拆解笔记对你有用,也欢迎你用代码实际跑一遍,那才是真正属于自己的经验。