资讯动态

Stable Diffusion原理精讲:CLIP、UNet与扩散模型全解析

发布时间:2026/10/26 14:39:09 来源:尧图企业网站定制
很多把 Stable Diffusion 当画图工具用的人第一篇想学的是怎么调提示词、怎么换模型但真到了要解决实际问题的时候比如手部画崩、主体漂移、画面风格没法控制你绕不开 CLIP、扩散模型、UNet 这几个底层模块。我说的 Stable Diffusion特指潜在扩散模型这套架构CLIP 负责把文字翻译成向量UNet 负责在潜在空间里一步步去噪扩散模型负责给整个“从噪声到图像”的过程提供理论框架最后由 VAE 把结果从压缩空间还原成像素。这篇文章是我四集精讲笔记的整理版把原理和代码放在一起说适合刚接触文生图、以及已经能用 WebUI 出图但还想把原理搞清楚的人。先泼一盆冷水很多人把 Stable Diffusion 的原理想复杂了总觉得要读懂一堆公式才能上手。其实你只需要理解四个模块怎么协作、每一步输入输出是什么形状就可以在生产环境里排错、调优甚至自己改网络结构了。我之前带团队做 AI 绘画工具的时候新人最容易卡在同一个困惑上为什么模型能理解“一只橘色猫坐在窗台上”这句话答案就在 CLIP 和 UNet 的交叉注意力上。借着这次写四集精讲笔记的整理稿我把这条链路掰开揉碎从整体架构讲到代码实现还附带我踩过的几个典型坑。1. 从整体链路看 Stable Diffusion先建立一个全局认知文生图不是“文字直接变成图”而是一条流水线。文字先进 CLIP变成一组向量随机噪声在 UNet 里被反复“清洗”清洗的过程里不断吸收文字向量的指引最后被 VAE 解码成真实像素。这条链路任何一环出问题最终图片都会出问题。1.1 文生图的“四段式”逻辑我用四个阶段来理解整个管线这也是四集精讲的核心骨架。第一段是文本编码你输入的 prompt 会先经过 tokenizer 切分成 token再由 CLIP 文本编码器转成向量通常是一个 [77, 768] 或者 [77, 1280] 的矩阵不同模型有差异。这段向量就是图画的“文字灵魂”后续 UNet 全程都在参考它。第二段是空间压缩图片不是直接操作 512×512 像素而是先用 VAE 编码器把它压到潜在空间比如 64×64×4 的 latent。这样做不仅省显存更重要的是把高频细节和低级语义先分离开UNet 可以在一个更稳定的语义空间里做去噪。第三段是扩散去噪从纯随机噪声开始UNet 根据当前的 latent、步骤数 time step、还有 CLIP 的文本向量预测出这一步的噪声调度器scheduler负责把预测出的噪声和当前 latent 结合一步步去掉噪声。等步数走完我们得到一个干净的 latent。第四段是 VAE 解码把干净的 latent 交给 VAE 解码器还原成 512×512 RGB 图像。这一步也是你看到的“出图瞬间”。这四段分别对应代码里的四个主要对象CLIPTextModel、VAE、UNet、Scheduler。所以当你写 pipeline 的时候哪怕是封装得很高级的 diffusers底层也不过是这四个组件按顺序跑一遍。1.2 为什么是“潜在”扩散模型“潜在”这个词很多人听过但没理解透。普通扩散模型比如 DDPM是在像素空间里直接做加噪去噪假设你生成一张 512×512 的图每一步都要处理 512×512×3 的数据计算量很大训练和推理都慢。潜在扩散模型 LDM 的思路是先用自编码器把图像压缩到一个更小的 latent 空间宽度和高度缩小 8 倍通道数变成 4整个数据量直接降到几十分之一。UNet 在这个压缩空间里做扩散最后再用 VAE 解码回像素。我这么类比像素空间是电影的原始胶片潜在空间是剪辑好的成片模型处理的是有语义压缩的版本而不是每一帧原始画面。这也是为什么 Stable Diffusion 能在普通消费级显卡上跑起来。还有一个容易忽视的点为什么压缩后信息没丢因为 VAE 不是普通的有损压缩它在训练时会约束 latent 符合高斯分布同时解码器有能力从这种分布里恢复出高频细节。实际上 VAE 的 latent 在 UNet 迭代时会被不断“修正”所以 final decoded 图像反而比直接吃像素的模型更稳。2. CLIP把文字变成机器能懂的向量CLIP 这一步是 Stable Diffusion 能“听懂”文字的关键。我遇到过不少人直接在 WebUI 里写提示词从来没想过为什么顺序变化会影响出图更没想过为什么提示词太长会被截断。这些现象背后全在 CLIP 的细节里。2.1 CLIP 在文生图管线里的位置与作用CLIP 原本是 OpenAI 做图文对比训练出来的模型。它有 image encoder 和 text encoder 两个分支训练时让匹配的图文向量靠近、不匹配的远离。Stable Diffusion 这里只用它的 text encoder也就是拿它当“文字编码器”。作用是把你写的自然语言句子编码成语义向量方便 UNet 参考。要注意的是CLIP 文本编码器的输入长度不是无限的。Stable Diffusion 1.x 系列默认最多 77 个 token超出部分会被直接忽略。这也是“提示词堆太满没用”的根本原因。很多新手写了一段 200 词的环境描写实际作用等于被截断到第 77 个 token后面全白写。我自己的习惯是核心主体词放最前面就是因为它会优先参与注意力计算。2.2 动手看看 CLIP 的输出长什么样理论讲完我直接点开一段代码验证 CLIP 输出。终端跑 Python加载 transformers 的 CLIP 模型试试from transformers import CLIPTokenizer, CLIPTextModel import torch model_id openai/clip-vit-large-patch14 tokenizer CLIPTokenizer.from_pretrained(model_id) text_encoder CLIPTextModel.from_pretrained(model_id) prompt a cat on windowsill, warm light inputs tokenizer(prompt, return_tensorspt) with torch.no_grad(): outputs text_encoder(**inputs) print(outputs.last_hidden_state.shape) # torch.Size([1, 9, 768]) 9 是 token 数768 是 hidden size这里 last_hidden_state 就是 UNet 拿去做交叉注意力的那组向量。它每一个 token 对应注意力里的一个“键”和“值”。而 outputs.pooler_output 是一个 [1, 768] 的句子级向量常用于分类任务但文生图里反而主要用 last_hidden_state因为 UNet 需要逐 token 的细粒度信息不能只靠一句的整体语义。有个很容易踩的坑Stable Diffusion 1.5 的 text encoder 是 CLIP ViT-L/14但它在 diffusers 里使用的版本并不是通用 CLIP checkpoint而是专门微调过的 openai/clip-vit-large-patch14 和相关权重。有些人图省事随便加载一个空模型导致文本向量全是噪声出图效果当然崩。2.3 CLIP 的限制和微调思路CLIP 模型应用很广但它不是为文生图量身定做的。它的限制很直接对精细物体结构、数量关系、空间位置理解有限。你说“三只猫站在窗台上”CLIP 很可能只学到“猫”和“窗台”的概念对“三”这个数量信息把握不强。这也是为什么后来出现 ControlNet、区域控制、以及各种微调方案。想突破这些限制常用的路线有两条一是微调 CLIP 文本编码器。你可以用低秩适配 LoRA 在特定风格或者特定领域数据上微调让它对专属词汇更敏感。比如我在做室内设计数据集时发现模型对“奶油风”“侘寂风”这类词理解很差就在几千张标注图上微调了 CLIP出来效果明显提升。二是绕过文本编码局限改用额外的控制信号。ControlNet 就是直接把姿势、深度图、边缘图灌进 UNet不和 CLIP 抢语义理解。CLIP 还有一个“层数选择”的隐藏参数。在 WebUI 或者 ComfyUI 里有些高级插件允许你选 CLIP 的 skip 层。默认使用最后一层 hidden state但学过注意力的人会知道不同层关注的信息粒度不一样浅层关注词法深层关注语义。实际测试中SD 1.5 有时候用倒数第二层反而没那么“油”。这种调试可以理解为文字向量是素材用第几层等于选素材的滤镜没有绝对对错只有适不适合当前人物。3. 扩散模型正向加噪反向去噪扩散模型是 Stable Diffusion 的理论内核。很多人一听到数学公式就头疼但核心概念并不复杂。你可以把生成过程想象成“倒放一段视频”正常视频是从清晰图像变成噪声的渐进过程而生成就是把这段视频倒着放一遍。3.1 训练阶段前向加噪过程训练阶段我们不断往图像中加入高斯噪声。设原始图像为 x₀加入噪声后的图像为 xₜt 越大噪声越强。数学上来写就是x_t sqrt(alpha_t) * x_0 sqrt(1 - alpha_t) * epsilon其中 epsilon 是标准高斯噪声alpha_t 是随 t 变化的衰减系数。训练的目标就是让 UNet 学会预测“噪声 epsilon”而不是预测图像本身。这听起来像是绕远路其实好处是预测噪声更容易而且能避免模型收敛到模糊解。加噪过程还有个显著的工程价值前向过程可以在任意 t 时刻直接从 x₀ 算出来不需要一步一步迭代。所以训练时可以随机采样 t让模型应对各种噪声强度这大大提高了训练效率。我在实际微调的时候也沿用这个思路随机采样 t 比固定步数训练效果更稳。3.2 生成阶段反向去噪也就是采样训练完之后生成就走“反向过程”从纯高斯噪声 x_T 出发每一步用 UNet 预测当前噪声然后按调度器公式“减去”一部分噪声。在不同时间步不断重复最终得到一张图像。这里一个关键参数叫“步数”也就是 num_inference_steps。步数越多理论上清得越干净但也不是越多越好。我用 Euler a 采样器测过20 步就能出基本稳定的图30 步后细节更丰富40 步后提升很微小但速度明显变慢。某些调度器比如 DPM-Solver 和 UniPC可以用更少的步数达到类似效果20 步就够。所以别迷信 50 步。更加合理的方法是先固定 20 步左右找到构图想要更多细节再开高步数精修。调度器还决定了噪声减小的方式。欧拉、DDIM、DPM、Heun 这些名字其实是在选择“数值求解器”的近似策略。调试时你可以记住一个规律越“能跳步”的采样器收敛越快但每一步跑飞的风险也越大越“保守”的采样器比如 Heun细节更稳但慢。实际工程里我常用 DPM 2M Karras 做常规出图用 Euler a 做风格探索用 Heun 做精修重绘。3.3 CFG无分类器引导再说一个稳定出图绕不开的参数CFG也就是 Classifier-Free Guidance。它控制提示词对生成图的“约束强度”。具体实现时UNet 会跑两次一次带文本条件一次不带文本条件或带负面条件。最终的噪声预测为noise_pred unconditional_noise guidance_scale * (conditioned_noise - unconditional_noise)guidance_scale 一般取 7 左右。太低了模型自由发挥主体跑偏太高了颜色过饱和、物体会“过度挤压”手部崩溃概率飙升。我在代码里复现这一步时发现很多人误以为 CFG 越高越“听话”其实过高时图像会失真。因为它在拉大与无条件生成的差距差距过大就不再是自然图像分布里的点了。4. UNet真正干活的去噪网络CLIP 负责理解文字扩散模型负责定义过程真正在每一步里把像素往“符合文字描述的图像分布”推的是 UNet。如果你只记一个重点那就是 UNet 的核心不是“生成”而是“预测噪声”。4.1 UNet 的网络结构编码器-瓶颈-解码器UNet 经典结构呈现“U 形”左边是编码器负责逐步下采样提取特征中间是瓶颈层右边是解码器负责上采样恢复分辨率。每一级之间还有跳跃连接把编码器的浅层特征直接拼到解码器的对应层避免深层特征丢失细节。Stable Diffusion 的 UNet 和医疗影像领域的 UNet 略有不同在残差块之间加入了自注意力和交叉注意力模块。下采样后latent 尺寸变小但通道数增多相当于把空间信息映射到语义特征。比如 64×64 latent 输入下采样到 32×32、16×16、8×8每个尺度分别做注意力。每个尺度关注的信息不一样浅层关注边缘纹理深层关注物体整体构成。UNet 里还有一个额外输入time step embedding。为什么需要它因为不同去噪阶段的“任务”不同。早期噪声大模型要恢复的是整体轮廓后期噪声小模型要做的是微调细节。time embedding 让同一套权重对不同阶段产生不同行为。我在调试时偶尔会打印不同 time step 下的特征图能看到早期多为大块结构后期高频纹理更明显这就验证了为什么扩散模型能在单模型内完成从布局到细节的完整生成。4.2 交叉注意力文本向量是怎么“画”进图片的看明白 UNet 结构之后最实用的一个概念是交叉注意力。在 UNet 的每个注意力层latent 经过处理后作为 Query查询CLIP 文本向量作为 Key 和 Value。公式简化成Attention(Q, K, V) softmax(Q * K^T / sqrt(d)) * V这有点像一个数据库查询图像特征“问”每个文字 token“我应该关注哪些关键词”然后从文本向量里“取”对应内容。所以当提示词里某个词权重越高它占据的注意力比例越高出图就越突出。这也是为什么写提示词时把核心名词放前面、减少无关修饰会直接改变画面内容分布。网上有很多人做“注意力热力图可视化”把生成的中间注意力矩阵叠加在原图上可以看到每个 token“猫”“窗台”“阳光”各自控制画面哪个区域。当你发现想控制某个区域但提示词老不生效时通常就是交叉注意力没有对应到正确 token这时候可以考虑用 attention 控制插件或 region control 模块去强制划定权重区域。4.3 UNet 使用时要注意的坑用完一段时间的 UNet我总结了几条实操经验。第一数据类型要统一。SD 1.5 的 UNet 默认权重是 fp16你用 fp32 加载推理不是不行但显存占用翻倍速度也慢。反过来如果你在 CPU 上做推理fp16 反而部分平台不支持容易报错。建议显存够就 fp16CPU 就用 fp32。第二text encoder 最后一个隐藏层的输出不能乱改维度。有人想用 SDXL 的两阶段文本编码器直接喂给 SD1.5 的 UNet会发现维度对不上直接炸。SDXL 的 UNet 用的是 open_clip ViT-bigG 和 CLIP ViT-L 的组合输出上下文长度也不一样。第三显存优化要看时机。diffusers 提供 enable_attention_slicing 和 enable_vae_slicing前者把注意力矩阵切块算特别适合 4GB 显存的设备。但开启后会慢我一般只在显存不够时才开。另外 enable_model_cpu_offload 可以动态把 CLIP、UNet、VAE 在 CPU 和 GPU 之间搬运但多了一次迁移速度也会下降。第四UNet 不是越多层越好。有一些改进版 UNet 加深了网络层数如增加 transformer blocks 数量确实能提升细节但训练难度和显存消耗成倍增长。普通用户没必要自己魔改直接使用社区成熟的 checkpoint 或者 LoRA 更划算。5. 文生图代码实战从零跑通一个 pipeline前面讲的都是原理最终还是要落到代码上。我用的开发环境是 Python diffusers PyTorch模型以 SD1.5 为例。下面这一段是能直接跑的最小文生图代码。5.1 环境准备与依赖我建议你创建一个干净的 Python 3.10 环境pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install diffusers transformers accelerate safetensors版本上注意diffusers 更新很快API 出现过多次迁移。旧代码里 DiffusionPipeline 虽然还能用但官方现在推荐 AutoPipelineForText2Image因为会自动判断模型类型。我个人习惯固定 diffusers0.27.0 这种版本跑生产脚本避免新版把参数名改了搞出兼容问题。5.2 核心代码像拆积木一样拆解每一步先来一个最简洁版本from diffusers import AutoPipelineForText2Image import torch pipe AutoPipelineForText2Image.from_pretrained( runwayml/stable-diffusion-v1-5, torch_dtypetorch.float16, safety_checkerNone ) pipe pipe.to(cuda) g torch.Generator(devicecuda).manual_seed(42) image pipe( a cat on windowsill, warm light, photorealistic, negative_promptlowres, bad anatomy, bad hands, text, watermark, num_inference_steps30, guidance_scale7.0, generatorg, ).images[0] image.save(cat.png)这段代码看着简单但 pipeline 内部帮你完成了前面讲的所有阶段。如果你想完全掌控原理可以把四个组件手动拆开跑from transformers import CLIPTokenizer, CLIPTextModel from diffusers import AutoencoderKL, UNet2DConditionModel, DDPMScheduler import torch # 加载组件 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, subfolderunet) scheduler DDPMScheduler.from_pretrained(runwayml/stable-diffusion-v1-5, subfolderscheduler) # 1. 文本编码 prompt a cat on windowsill, warm light tokens tokenizer(prompt, paddingmax_length, max_length77, return_tensorspt) text_embeddings text_encoder(tokens.input_ids.to(cuda)).last_hidden_state # [1, 77, 768] # 2. 随机噪声 latents torch.randn((1, 4, 64, 64), devicecuda) # 3. 去噪循环 scheduler.set_timesteps(30) for t in scheduler.timesteps: noise_pred unet(latents, t, encoder_hidden_statestext_embeddings).sample latents scheduler.step(noise_pred, t, latents).prev_sample # 4. VAE 解码 with torch.no_grad(): image vae.decode(latents / vae.config.scaling_factor).sample手动版本有几个容易出错的地方。第一padding 必须 max_length77否则文本向量长度不够。第二scheduler 的 step 返回 prev_sample不能直接拿 noise_pred 当图。第三latents 要除以 scaling_factor否则还原出来的图偏暗或者偏亮。5.3 手部修复三招经典难题的工程解法提到文生图实战绕不开那个被吐槽最多的问题手部修复。为什么手这么容易崩因为手的结构复杂、指节多、在图像中占比小交叉注意力很难把每个指节的语义精确分配到像素区域。我在实践里总结了三个招数。第一招提示词层面。正向提示词加 “perfect hands, detailed fingers, five fingers”反向提示词加 “bad hands, missing fingers, extra fingers, deformed hands”。有作用但不要期望太高。它只是给模型一个语义倾向复杂姿势下仍然会崩。第二招ControlNet 约束。先用 OpenPose 骨架或 DWPose 提取手部关键点把姿势图作为条件输入 UNet。这一步能强制模型的注意力集中在给定的手部结构上。实际效果立竿见影特别是固定手势的时候。不过要确保骨架本身合理如果关键点混乱反而会画出畸形的扭曲手。第三招局部重绘 inpaint。先用普通文生图跑出大图如果只有手区域崩了就做一个手部 mask然后调用 inpaint pipeline 单独对手部重绘from diffusers import AutoPipelineForInpainting import numpy as np from PIL import Image pipe AutoPipelineForInpainting.from_pretrained( runwayml/stable-diffusion-inpainting, torch_dtypetorch.float16, ).to(cuda) init_image Image.open(cat.png).convert(RGB).resize((512, 512)) mask_image Image.new(L, (512, 512), 0) # 手动把手部区域在 mask 上画成白色其余黑色 mask_np np.array(mask_image) mask_np[300:400, 200:300] 255 mask_image Image.fromarray(mask_np) result pipe( promptperfect hands, detailed fingers, negative_promptbad hands, deformed hands, missing fingers, imageinit_image, mask_imagemask_image, strength0.6, ).images[0]这里 strength 控制重绘强度0.6 左右表示保留原图大部分纹理只修改 mask 内手部区域的细节。太高了会连手周围的衣服一起重画太低了重绘效果不明显我一般从 0.5 起步慢慢调。6. 常见问题与排查技巧实录最后这部分我把平时收集的高频问题整理成速查表也给一些我自己的排错习惯。这不是理论是真正跑生产环境跑出来的。6.1 显存不足与图像质量问题排查我做了个简短排查表遇到问题直接对着查现象可能原因解决方向CUDA Out of Memory显存不够刘海模式下仍爆开 enable_attention_slicing或缩小分辨率或换 bf16 模型图像全黑VAE 的 scaling_factor 计算错误手动调用时除以 scaling_factorfp16 时换 fp32 跑 VAE图像灰蒙蒙采样器步数过低或 CFG 太低提高 steps 到 30或把 CFG 升到 7~8手部崩坏结构复杂注意力纠缠用尾部 inpaint、ControlNet OpenPose 或负面词约束提示词后半段不生效上下文超过 77 token 被截断只保留前 77 token核心词前置或用 SDXL 的长标签模型出图速度突然变慢开启了 CPU offload 或 attention slicing确认显存占用必要时关闭对应优化这个表是我排查问题时最常翻的。很多小白出问题第一反应是重装环境其实大部分在参数和数据类型层面就解决了。6.2 提示词与负面词的关键技巧提示词这门功夫核心是理解 CLIP 的语义表达。它不像搜索引擎那样“关键词全塞进去就好”更像“用最精确的措辞描述主体”。我建议把提示词拆成四个部分写主体 环境 风格 质量词。例如主体a persian cat 环境on a wooden windowsill, soft morning light 风格photorealistic, shallow depth of field 质量词highly detailed, sharp focus, 8k负面词也分两类一类是画质类如 lowres、blurry、jpeg artifacts另一类是解剖类如 bad anatomy、bad hands、mutated limbs。负面词主要是给无分类器引导提供反方向参考让模型尽量远离这些特征。还有一个小技巧在 WebUI 或 ComfyUI 里你可以用“clip 询问机”的方式单独调试提示词。本质就是把 CLIP 文本编码器输出拉出来对比两个提示词在向量空间里的距离。如果距离很近说明画出来会差不多如果很远说明画面差距会很大。这样比反复抽卡更高效。ComfyUI 里通过 CLIP Text Encode 节点就能看到结果不需要写代码。6.3 从 WebUI 到代码、ComfyUI怎么选很多人会问我已经会用 WebUI 了为什么还要学代码我的观点是如果只是日常出图WebUI 完全够用但要做批量生成、接入业务系统、复现研究结果、或者自定义采样流程代码比 GUI 可靠得多。ComfyUI 的优点是可以可视化流程编排把模型加载、节点连接、参数调整都变成可复用的工作流尤其适合复杂重绘和多模型串联。还有个趋势是 lightweight 部署比如 Stable Diffusion cpp 这类项目可以把模型量化为 int4 或 int8 跑在 CPU 或者低显存设备上适合没有 GPU 的服务端场景。但代价是画质和速度都有折损。我看过它的开源实现PPM 思路其实还是那套 pipeline只是底层算子改变了精度策略。最后聊一点个人的工程习惯。我出图不会立刻用默认参数而是先固定 seed 和 20 步低 CFG 跑一轮草稿看构图与主体是否对确认后再开 30 步、拉高 CFG 精修。改提示词时只改一个词不动其他参数不然变量太多你根本不知道是什么起了作用。手部问题除非确实必要我一般不硬修直接局部重绘或者在生成时加 OpenPose 约束比抽卡碰运气稳定太多。这套四集笔记整理下来核心就一句话Stable Diffusion 不是黑盒只要把 CLIP、扩散模型、UNet、VAE 各自的分工搞清楚绝大多数问题你都能在组件层面找到答案。碰到一张图怎么调都不对就回到链路上问自己是文字没被理解CLIP还是去噪步数不够扩散模型还是结构约束不到位UNet还是还原出的像素不对VAE。锁定问题模块再对症下药比盲目换提示词有效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑