
1. 从一张图说起为什么 Stable Diffusion 值得每个技术人拆开看第一次跑通 Stable Diffusion 文生图的时候我盯着屏幕上那张“一只穿宇航服的柴犬在月球上遛弯”的图看了很久。不是因为它多惊艳而是因为我知道这背后是一条极其精巧的流水线一段文字先被 CLIP 编码成向量然后在潜空间里被 UNet 一步步“去噪”最后 VAE 把潜变量解码成像素。整条链路里没有任何一步是魔法全是可拆解、可复现的工程。这篇文章就是我把这条链路彻底拆开后的笔记。核心关键词围绕Stable Diffusion、CLIP、扩散模型、UNet、文生图展开我会从原理讲到代码从数学直觉讲到实操踩坑。适合三类人一是想搞懂扩散模型到底怎么工作的算法同学二是想自己写一版文生图 demo 的工程师三是用 ComfyUI 或 WebUI 出图、但想弄明白节点背后发生了什么的重度用户。不管你是哪一类读完你应该能自己用 Python 搭出一个最小可用的文生图流程并且知道每一步为什么这么设计。我写这篇的初衷很简单网上讲 Stable Diffusion 的文章要么只讲概念不讲代码要么直接甩一堆公式劝退。我想做的是把“为什么”讲透——为什么用潜空间而不是像素空间为什么 UNet 要预测噪声而不是直接预测图像为什么 CLIP 的文本编码器不能随便换。这些问题的答案决定了你在实际调参、换模型、做微调时的每一个决策。2. 整体架构拆解Stable Diffusion 到底由哪几块拼成2.1 三大组件各司其职CLIP、UNet、VAEStable Diffusion 不是一个单一模型而是三个网络协同工作的系统。理解这一点非常关键因为很多人调参调不明白就是因为把三者混为一谈。CLIP 文本编码器负责把自然语言变成机器能理解的向量。你输入的“a cat sitting on a chair”它不会逐字理解而是把整句话映射到一个 768 维SD 1.5或 1024 维SDXL的语义空间里。这个向量就是后续所有生成的“指挥棒”。UNet是真正干活的主力。它接收带噪的潜变量和 CLIP 给的文本条件预测出“这一步应该去掉多少噪声”。注意它预测的是噪声不是图像本身。这个设计选择后面会详细讲。VAE分两部分编码器把像素图压缩成潜变量解码器把潜变量还原成像素图。文生图流程里主要用解码器因为我们是凭空生成潜变量不需要编码。三者串起来的流程是文本 → CLIP → 文本向量 → 和随机噪声一起喂给 UNet → 迭代去噪 → 得到干净的潜变量 → VAE 解码 → 输出图像。2.2 为什么要在潜空间做扩散算力账算给你看这是整个架构里最聪明的一步。假设直接在 512×512 的 RGB 像素空间做扩散每一步去噪都要处理 512×512×3 ≈ 78 万个数值。扩散模型通常要跑 20 到 50 步每步都要过一遍 UNet这个计算量对消费级显卡是灾难。Stable Diffusion 的做法是先训练一个 VAE把 512×512×3 的图像压缩成 64×64×4 的潜变量。数据量从 78 万降到 1.6 万压缩了大约 48 倍。UNet 只在这个小得多的潜空间里工作显存占用和计算时间都大幅下降。代价是 VAE 的压缩是有损的重建图像会有轻微细节损失。但实测下来这个损失在视觉上几乎察觉不到换来的是能在 8GB 显存的卡上跑 512 图这笔账非常划算。这也是“潜在扩散模型”Latent Diffusion Model这个名字的由来。2.3 训练与推理的对称性理解这个才能理解采样训练时我们拿一张真实图像加不同强度的噪声让 UNet 学会“给定带噪图和噪声强度预测出加的噪声是什么”。推理时反过来从纯噪声出发让 UNet 一步步预测噪声并减掉最终得到干净图像。这个对称性是理解采样器的基础。不同的采样器Euler、DPM、DDIM本质上是在用不同的数学方法解同一个去噪方程。有的步数少但质量一般有的步数多但更精细。理解了训练目标你就知道为什么采样步数不是越多越好——超过某个点收益递减甚至可能因为累积误差反而变差。3. CLIP 文本编码器文字是怎么变成“指挥棒”的3.1 CLIP 的核心思想对比学习拉近图文距离CLIP 全称 Contrastive Language-Image Pre-training是 OpenAI 提出的图文对齐模型。它的训练方式很直观拿 4 亿对图文数据让配对的图文向量在空间里靠近不配对的推远。训练完之后一张猫的图片和“a photo of a cat”这句话的向量会非常接近。Stable Diffusion 只用了 CLIP 的文本编码器部分。它把文本 token 化后经过 Transformer 编码输出每个 token 的向量表示。SD 1.5 用的是 CLIP ViT-L/14 的文本塔输出 77 个 token 的 768 维向量序列。这里有个细节值得注意77 是最大 token 长度包括起始和结束标记。所以你的提示词实际能用的长度是 75 个 token 左右。超长提示词会被截断这就是为什么有些词写了没效果——它根本没进模型。3.2 文本向量的两种用法全局池化与序列条件CLIP 输出的是 77×768 的序列但 UNet 需要的是条件信息。这里有两种处理方式一种是取全局池化向量通常是结束标记对应的那个得到一个 768 维的全局语义另一种是把整个 77×768 序列通过交叉注意力注入 UNet。Stable Diffusion 主要用后者。UNet 的每个注意力层都会用文本序列做 cross-attention让图像特征去“查询”文本信息。这就是为什么提示词里不同位置的词会影响图像不同区域——注意力机制在起作用。理解这一点对写提示词很有帮助。词序和权重确实会影响结果因为注意力分布会变。但也不用过度迷信“魔法咒语”CLIP 的语义空间是连续的相近意思的词效果差异没那么大。3.3 实操用 Python 提取 CLIP 文本向量下面这段代码可以直接跑需要装 transformers 和 torchfrom transformers import CLIPTextModel, CLIPTokenizer 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 sitting on a chair tokens tokenizer( prompt, paddingmax_length, max_length77, truncationTrue, return_tensorspt ) with torch.no_grad(): outputs text_encoder(tokens.input_ids) # last_hidden_state: [1, 77, 768] embeddings outputs.last_hidden_state print(embeddings.shape)跑完你会看到输出是[1, 77, 768]。这个张量就是后续喂给 UNet 的条件。注意paddingmax_length和truncationTrue这两个参数前者保证长度固定为 77后者防止超长提示词报错。提示如果你在做 ComfyUI 的 CLIP 询问机节点调试本质上就是在看这个 77×768 的向量。不同 token 位置的向量差异决定了图像不同区域的生成倾向。4. 扩散模型从加噪到去噪的数学直觉4.1 前向过程把图像一步步“糊掉”扩散模型的前向过程极其简单对一张干净图像 x₀逐步加高斯噪声经过 T 步后变成纯噪声 x_T。每一步的公式是x_t √(α_t) · x_{t-1} √(1-α_t) · ε其中 ε 是标准高斯噪声α_t 是预设的系数。这个公式有个漂亮的性质可以一步到位地从 x₀ 算出任意时刻的 x_t不需要真的迭代 T 次。这让训练变得高效。α_t 的调度策略schedule决定了加噪速度。常见的有线性调度和余弦调度。余弦调度在后期加噪更慢保留更多结构信息通常效果更好。Stable Diffusion 用的就是类似余弦的缩放线性调度。4.2 反向过程UNet 学会“猜噪声”反向过程是扩散模型的核心难点。理论上如果我们知道每一步噪声的分布就能从纯噪声还原出图像。但这个分布太复杂没法直接算。于是我们用神经网络去逼近它。训练目标很直接给 UNet 一张带噪图 x_t 和时间步 t让它预测出当初加的噪声 ε。损失函数就是预测噪声和真实噪声的均方误差。这个设计比直接预测 x₀ 更稳定因为噪声的分布是标准高斯的数值范围可控。为什么预测噪声比预测图像好我自己的理解是预测图像需要网络输出符合自然图像统计规律的复杂结构而预测噪声只需要输出近似高斯的随机场任务难度低得多。网络把“理解图像结构”这件事隐式地编码在了预测噪声的能力里。4.3 采样从纯噪声一步步“雕刻”出图像推理时我们从 x_T ~ N(0, I) 出发用 UNet 预测噪声然后按公式反推 x_{t-1}。重复这个过程直到 x₀再送进 VAE 解码。这里有个关键参数CFG scaleClassifier-Free Guidance。它控制文本条件的影响强度。公式是ε_final ε_uncond scale · (ε_cond - ε_uncond)scale 为 1 时就是纯条件预测scale 越大文本影响越强但过大超过 15会导致图像过饱和、色彩失真。实测 7 到 12 是甜区具体看模型和提示词。采样步数方面DDIM 通常 20 到 50 步就够DPM 2M Karras 在 20 到 30 步就能出好图。步数再多收益很小反而浪费时间。我一般固定 25 步配合 DPM 2M Karras速度和质量的平衡点。5. UNet 网络结构去噪主力的内部构造5.1 编码器-解码器加跳跃连接经典但有效UNet 的结构名字就来自它的形状左边编码器逐层下采样右边解码器逐层上采样中间用跳跃连接把同层特征拼起来。这个设计最初用于医学图像分割在扩散模型里同样好用。编码器负责提取多尺度特征从细节到语义。解码器负责重建空间分辨率。跳跃连接让解码器能直接拿到编码器的细节特征避免上采样过程中丢失信息。对去噪任务来说这很关键——既要理解全局结构又要保留局部纹理。Stable Diffusion 的 UNet 在此基础上加了两个东西时间步嵌入和交叉注意力。时间步嵌入告诉网络当前是第几步去噪因为不同步数的去噪难度不同。交叉注意力让文本条件注入到每一层。5.2 时间步嵌入让网络知道“现在多吵”时间步 t 是一个标量但网络需要的是向量。做法是用正弦位置编码把它映射成高维向量再通过几层全连接。这个思路和 Transformer 的位置编码一样目的是让网络能区分不同的 t。为什么需要这个因为去噪任务在不同步数下差异很大。早期步数噪声强网络应该关注大结构后期步数噪声弱网络应该精修细节。没有时间步信息网络就没法自适应调整。5.3 交叉注意力文本条件怎么注入图像特征UNet 的每个注意力层里图像特征作为 Query文本序列作为 Key 和 Value做标准的 cross-attention。这样图像特征就能“查询”文本信息决定哪些区域应该生成什么内容。这个机制解释了为什么提示词里不同词会影响不同区域。比如“red car on green grass”红色会倾向于出现在车的位置绿色出现在草的位置。注意力权重在空间上的分布就是这种对应关系的来源。注意UNet 使用时的注意事项里最常见的就是显存问题。交叉注意力的计算量随分辨率平方增长所以高分辨率生成比如 1024显存需求会陡增。解决办法是用 VAE 分块解码或者用 tiled diffusion 这类技术。6. 文生图代码实战从零搭一个最小可用流程6.1 环境准备与依赖安装我用的环境是 Python 3.10 PyTorch 2.0 diffusers。diffusers 是 Hugging Face 出的扩散模型库封装得很好适合快速上手。pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install diffusers transformers accelerate safetensors如果你在 Mac 上跑可能会遇到ImportError: dlopen这类问题通常是 PyTorch 版本和系统架构不匹配。解决办法是装对应架构的 wheelM 系列芯片用--index-url https://download.pytorch.org/whl/cpu或者对应的 MPS 版本。6.2 核心代码20 行实现文生图import torch from diffusers import StableDiffusionPipeline model_id runwayml/stable-diffusion-v1-5 pipe StableDiffusionPipeline.from_pretrained( model_id, torch_dtypetorch.float16 ).to(cuda) prompt a cat sitting on a chair, oil painting style image pipe( prompt, num_inference_steps25, guidance_scale7.5 ).images[0] image.save(output.png)这段代码跑通你就有了一个完整的文生图流程。num_inference_steps是采样步数guidance_scale是 CFG scale。第一次跑会下载模型权重大概 4GB 左右。6.3 参数调优步数、CFG、种子怎么选步数不是越多越好。我实测下来DPM 2M Karras 采样器在 20 步时质量已经很好25 步是边际收益的临界点。超过 30 步基本看不出差别纯属浪费算力。CFG scale 的甜区在 7 到 12。低于 5 文本约束太弱图像会跑偏高于 15 色彩会过饱和出现“烧焦”感。我一般用 7.5 作为默认值需要更强文本遵循时调到 10。种子seed决定初始噪声。固定种子可以复现同一张图方便对比不同参数的效果。做实验时一定要固定种子否则你分不清变化是参数带来的还是随机性带来的。6.4 显存优化低配显卡也能跑8GB 显存跑 512 图基本够用但跑 768 或 1024 会爆。几个实用技巧用torch.float16半精度显存减半质量损失很小开启pipe.enable_attention_slicing()用时间换显存用pipe.enable_vae_slicing()分块解码 VAE极端情况下用pipe.enable_sequential_cpu_offload()把不用的层挪到内存这些开关的代价是速度变慢但能让低配卡跑起来。我自己的经验是attention slicing 对速度影响最小优先开这个。7. 常见问题与排查技巧实录7.1 出图全黑或全灰多半是 VAE 问题这是新手最常遇到的坑。生成出来一片黑或者一片灰通常是 VAE 解码出了问题。原因可能是用了 fp16 的 VAE数值溢出。解决办法是强制 VAE 用 fp32pipe.vae.to(torch.float32)或者换一个更稳定的 VAE 权重。SD 1.5 自带的 VAE 在 fp16 下确实容易出问题社区有专门的修复版 VAE 可以用。7.2 提示词不生效检查 token 长度和权重写了很长的提示词但某些词没效果先检查是不是超过 77 token 被截断了。可以用 tokenizer 打印 token 数量确认。另外提示词权重语法如(word:1.3)在不同前端里支持程度不一样WebUI 和 ComfyUI 的解析规则有差异跨平台迁移时要注意。7.3 显存溢出从分辨率和批大小入手CUDA out of memory是最常见的报错。排查顺序先降分辨率512 到 384 能省不少再降批大小一次生成一张然后开 attention slicing最后考虑换更小的模型或用量化版本。7.4 常见问题速查表问题现象可能原因解决办法全黑/全灰图VAE fp16 溢出VAE 转 fp32 或换 VAE提示词无效超 77 token 截断精简提示词或分段显存溢出分辨率/批大小过高降分辨率、开 slicing图像过饱和CFG scale 过高降到 7-10生成速度慢步数过多或没用半精度降步数、用 fp16Mac 启动报错架构不匹配装对应架构 wheel7.5 几个我踩过的坑第一个坑是盲目追求高步数。刚开始我以为步数越多越好设了 100 步结果等了半天图还不如 25 步的。后来才明白采样器的收敛速度不同选对采样器比堆步数重要。第二个坑是忽视种子。做参数对比时没固定种子结果每次图都不一样根本没法判断参数效果。固定种子后对比实验才有意义。第三个坑是乱换 VAE。不同 VAE 对色彩和细节影响很大有些 VAE 会让画面偏灰或偏艳。换 VAE 后一定要重新调 CFG否则效果可能更差。8. 从原理到扩展这套知识还能怎么用搞懂这条链路之后很多扩展方向就自然打开了。比如图生图本质是把输入图 VAE 编码成潜变量加部分噪声后走同样的去噪流程噪声强度由 denoising strength 控制。再比如 ControlNet是在 UNet 里额外注入结构条件原理和文本交叉注意力类似只是条件换成了边缘图、深度图等。模型微调方面LoRA 是在 UNet 的注意力层里插入低秩矩阵用少量数据微调出特定风格。DreamBooth 则是用少量图像让模型记住特定主体。这些技术的底层都建立在对 CLIP、UNet、扩散过程的理解之上。CLIP 模型微调也是个热门方向。如果你想让模型理解特定领域的术语比如医学或工业词汇微调 CLIP 文本编码器比重新训练整个模型划算得多。不过要注意微调后的 CLIP 和原版 UNet 可能不匹配需要联合微调或做适配。最后分享一个我个人的习惯每次调参前先固定种子和采样器只改一个变量记录结果。这样积累下来的参数直觉比看任何教程都管用。扩散模型的参数空间很大但真正影响大的就那么几个摸清它们的脾气出图就稳了。