从CLIP到扩散模型:Stable Diffusion原理与代码实战笔记

📅 发布时间:2026/10/15 13:54:01
从CLIP到扩散模型:Stable Diffusion原理与代码实战笔记
前阵子为了把Stable Diffusion的原理彻底吃透我把一套从 CLIP、扩散模型、UNet 到文生图代码实战的四集精讲反复看了三遍期间对着 diffusers 源码、各种采样器公式踩了不少坑最后干脆跟着视频把所有代码重写了一遍才算把这套东西的逻辑线理顺。如果你也处于能用 webui 或 ComfyUI 出图但一打开教程里关于 CLIP、UNet 的讲解就犯困的状态这篇笔记就是写给同样经历的你。这套四集精讲的顺序安排得很巧妙先讲CLIP怎么把文字和图像映射到同一个空间再讲扩散模型为什么能凭空生成图像接着拆解UNet在降噪循环里到底做了什么最后用一段能跑起来的文生图代码把前面所有概念串成闭环。下面我按这个顺序把笔记里我认为最具有信息量的内容整理出来重点包括为什么要这样设计和实际用的时候会踩到什么坑。1. 第一集CLIP 是文生图里的翻译官把文字和图像装进同一个坐标系1.1 CLIP 训练的核心思路图文对比学习CLIP全称 Contrastive Language-Image Pre-training是 OpenAI 早在 2021 年提出的模型。它做的不是常规的给图片分类或者给文字打标签而是让一整个句子和一张图片在语义上对齐。采用的训练方式是对比学习从互联网上抓取大量图片-文字描述对每批数据里既有正样本对正确的图片和描述也有大量负样本对这张图配了不相关的句子。训练目标是让正样本对在向量空间里距离更近、负样本对距离更远。具体实现上CLIP 有两个编码器图像编码器把图片转成一个向量文本编码器把句子转成另一个向量然后通过对比损失InfoNCE 这类形式把特征空间拉齐。这个过程的关键不是某个单点分类任务而是对比关系——模型必须理解这只猫蹲在窗台上和一张对应的猫图有多匹配。训练完成后针对一个句子和数百张图片只要算各自的向量再做余弦相似度就能给所有图片按文本相关性排序。这一套在 Stable Diffusion 里的角色很清晰它充当了翻译官。你输入的英文提示词经过 CLIP 文本编码器被转换成一系列语义向量后续网络只能理解这些向量不理解原文字符本身。1.2 在 SD 里我们只用 CLIP 的文本编码器很多第一次看源码的读者会困惑既然 CLIP 包含图像编码器和文本编码器为什么 SD 加载权重时只导入了 text encoder原因在于 SD 在生成阶段不需要判断图片与文字是否匹配它只需要一份已经被语义化、能被其他模块使用的文本特征。具体来说Stable Diffusion 1.x 使用的是 OpenAI 的 CLIP ViT-L/14 模型中的文本编码器输出维度为 768。流程大致是提示词先被切分成 token比如 a photo of a cat 会被当作 4 个 token每个 token 通过词表查找得到 ID再映射成初始 embedding形状是 [seq_len, 768]embedding 经过若干层 Transformer 编码最后输出的特征再被后面 UNet 的 cross-attention 使用。这里有个经常被忽略的细节很多讲解会把CLIP 输出一个文本向量说得很笼统实际上 SD 用的是序列特征而不是只取最后一层向量。每个 token 位置都有独立的语义向量UNet 的注意力层可以看到整个句子的顺序信息。这也是为什么提示词前后顺序会影响生成结果。1.3 OpenAI CLIP 和 OpenCLIP为什么模型版本总是对不上实际动手时最容易踩坑的就是你下载的权重文件来自哪个 CLIP 版本。SD 1.x 的官方权重用的是 OpenAI 的 CLIP而后续很多社区模型尤其是 SD 2.x 和 SDXL改用 OpenCLIP 作为文本编码器两者的 tokenizer 词表、hidden size、层数都不一样。SD 1.5OpenAI CLIP ViT-L/14文本特征维度 768SD 2.1OpenCLIP ViT-H文本特征维度 1024SDXL双文本编码器OpenCLIP ViT-bigGdyhd size 1280加 OpenAI CLIP ViT-L。所以你在部署模型时如果发现加载后出图效果完全不对提示词似乎不起作用第一件事就应该检查text encoder 的配置是否与权重匹配。很多网上下载的精简版权重只保留了 UNet 部分加载时如果照搬原配置文件就可能在文本编码器这一步报尺寸不匹配。1.4 为什么不能把文本直接丢给 UNet经常有人问既然 UNet 接收条件信息为什么不直接把 token 序列丢进去非要过 CLIP这里有两个层面。第一表示形式不统一UNet 处理的是图像特征二维空间结构而原始 token 是离散 ID没法直接做空间层级的信息融合。第二语义密度不够CLIP 经过在大量图文对上的训练能提取出猫、窗台、阳光这种高层次的视觉语义直接泛化到具体生成的画面里。如果没有这一步模型就得从零学文本语言这对参数量和训练数据的要求会大得多。对于做CLIP 模型微调的人来说改动文本编码器会有连锁影响改 text encoder 需要同时微调 UNet否则文本特征分布变了cross-attention 的 Q/K/V 矩阵没法正常对齐。这个经验在后面讲代码实战时还会提到。2. 第二集扩散模型的正向与逆向说白了就是先弄脏再学会收拾干净2.1 前向过程噪声一点点把图像吃掉扩散模型的核心思想很多人第一次听都会觉得绕其实可以类比为一个逆向的污染过程。假设你有一张干净的图像 x0我们定义一个前向过程每一步往图像里加入少量高斯噪声连续加 T 步之后图像会彻底变成一张纯噪声图。如果用公式表达前向过程每次采样x_t sqrt(1 - beta_t) * x_{t-1} sqrt(beta_t) * epsilon其中 epsilon 是从标准高斯分布采样出的噪声beta_t 是预先定义好的噪声调度表scheduler。因为高斯分布具有可加性我们可以直接从 x0 一步算到任意步 x_t不需要真的逐轮迭代。这样训练的时候网络每次只需要拿某个随机时刻 t 的带噪图像 x_t、时间步 t、条件 c作为输入预测未来要减去的噪声残差即可。在前向过程里t 越大图像越接近纯噪声t 越小原始结构保留得越完整。模型专注于预测这一幅带噪图和前一幅之间的噪声增量而不是直接回归原图。这个设计最大的优势是让整个训练目标稳定且容易监督。2.2 反向过程让网络去猜每一步加入了什么噪声生成阶段就是反向过程从一张随机噪声图 x_T 出发逐步去噪每一步用训练好的模型预测当前噪声然后根据调度器更新公式往更干净的方向前进一步反复执行直到得到 x0。DDPMDenoising Diffusion Probabilistic Models使用的是一个很直接的重建式变分下界目标但后来被简化成更直观的噪声预测目标最小化预测噪声 epsilon_theta(x_t, t) 与真实噪声 epsilon 之间的均方误差。这里有个关键模型并不是一次性地画出一整张图而是像雕刻一样每一轮只负责把当前状态往更清晰的方向推一小步。生成质量取决于去噪步数和每步的算法精度。步数少速度上去了但可能丢失高频细节步数多质量高但计算开销也成倍增加。在实际项目中采样步数普遍设置为 20-50 步。你如果打开 ComfyUI 或 Automatic1111 的采样器面板会看到一堆采样方案比如 Euler、DDIM、DPM 2M、UniPC这些就是在如何根据噪声预测结果更新 x_{t-1}上做了不同的近似数学处理。2.3 潜空间扩散为什么 SD 不在像素空间里做Stable Diffusion 严格来说应该叫Latent Diffusion Model潜在扩散模型。所谓潜在意思是它没有在 512x512x3 的像素空间里做扩散而是先通过一个 VAE变分自编码器把图像压缩成 64x64x4 的潜向量特征在这个小得多的空间里完成扩散过程最后再由 VAE 解码器把潜变量还原成清晰图片。为什么非要压缩原因很直接计算量差异巨大。512x51226 万像素点64x644096 个特征点差了 64 倍。如果直接在像素空间做扩散同样的参数量需要大量显存和更长训练时间。潜空间扩散还能享受感知压缩带来好处VAE 已经帮模型过滤掉了大量人类不敏感的高频像素噪声UNet 只需要专注于更高级的语义结构生成。这也是为什么 video cards 显存不够的人宁可降低 batch size 或分辨率也要保留潜空间尺寸设置的原因。分辨率一变latent 尺寸是跟着变的8 倍下采样后续 UNet 的注意力计算也会随之变化。2.4 采样器、步数与确定性DDIM 和 DPM 在干什么采样策略对出图效果的影响经常被新手低估。简单说网络预测完噪声后我们其实是在解一个从带噪状态去噪得到更干净状态的方程。由于这个方程有近似解不同采样器就是不同数值解法。DDIMDenoising Diffusion Implicit Models去掉了原始 DDPM 的随机性每一步都是确定性更新。好处是只要初始噪声固定生成的图可以复现坏处是步数太少时细节损失明显。DPM 2M基于微分方程的更高阶 solver用两步历史信息逼近更好结果在 20-30 步时效果通常优于 DDIM适合追求质量且不想等太久的情况。Euler 采样器最简单的一阶解法速度最快默认步数下细节偏少适合快速出草稿。UniPC统一预测校正框架兼顾速度与质量社区使用也比较多。实际经验如果你在做批量出图且要求可复现建议用 DDIM 或者固定 seed如果追求 8-10 步内快速出效果优先考虑 LCM、Turbo 这类结合了蒸馏的专门采样器。千万别拿默认 50 步的配置配 LCM 模型反而会过增强制修正。3. 第三集UNet 的每一层在做什么以及使用时的常见误区3.1 骨架结构下采样、上采样、跳跃连接继续深入第三集主角是UNet。它最早是医学图像分割领域提出的网络结构核心特点是编码-解码不对称 跳跃连接。编码器部分逐步下采样分辨率减半通道数翻倍把输入图像特征压缩成一系列低分辨率但高通道数的特征图解码器部分逐步上采样把特征还原到原分辨率。跳跃连接的作用非常关键它把编码器某一层的特征直接拼接到解码器相同分辨率的层上。下采样确实能提取抽象语义但是同样会丢失边缘、纹理等高频细节跳跃连接让解码器可以从编码器抄近路拿回这些信息。在 SD 的生成任务里这种设计让 UNet 在理解整体结构和修复局部细节之间取得了平衡。3.2 时间步和文本条件是怎么进来的UNet 是一个条件扩散模型它需要同时知道三个东西当前带噪的 latent、当前噪声强度对应的时间步 t、以及 CLIP 输出的文本条件。时间步的注入方式通常是加在残差块里把整数 t 做正弦位置编码映射成 embedding 向量再通过 MLP 网络处理最后加入每个卷积块的特征中。这样网络能在不同噪声强度下学到不同行为——早期去噪阶段重点是恢复整体结构后期去噪阶段重点是补充细节。文本条件则通过cross-attention注入图像侧特征作为 Query文本侧 token 特征作为 Key 和 Value。这种设计的聪明之处是每个图像位置的 patch 都能根据文本内容选取它需要关注的语义信息。比如画面里有猫这个 token和猫相关的图像区域会在这个注意力过程中与文本特征发生更强的交互。3.3 cross-attention 如何把 CLIP 向量揉进图像特征这一集里建议反复理解的是 cross-attention 的维度变化。假设当前 UNet 的 feature map 是 64x64x320 形状会先拉平为 4096 个位置、每个位置 320 维特征。文本特征序列是 77x768。多头注意力机制里图像特征经过投影得到 Q文本特征经过投影得到 K 和 V然后计算 attention score再用得分加权求和文本特征 V最终把文本信息揉回图像特征。这个交叉注意力是在不同尺度层分别发生多次的所以一句话里的不同 token 能影响画面不同区域。这也是为什么科研和工程界都关注注意力图可视化——可以直接看到提示词里的 cat 激活了 latent 空间的哪些 patch。如果你调出了 attention map 工具会发现提示词权重失衡、易混淆的词相互覆盖等现象都可以直观看到。3.4 使用 UNet 的注意事项输入尺寸、通道约定、dtype结合这次笔记里实际踩到的坑我有几条关于 UNet 使用的经验输入通道数必须是 4 的整数倍关系latent 通常是 4 通道如果从自己的 VAE 或外部 latent 传入必须保证形状与模型训练时一致UNet 的 dtype 要与 VAE、text encoder 一致。fp16 训练权重加载到 CPU 设备时经常因为缺少自动混合精度而报错不要随意改变 UNet 输入分辨率与训练分辨率差距过大的尺度虽然 UNet 对尺寸有一定泛化能力但 attention 在超长序列上会内存爆炸也容易导致生成大图时结构崩坏t 的步数范围必须与 scheduler 一致如果你加载的是 1000 步训练的权重却把采样调度器配成 2000 步时间步编码会失真。做UNet 模型改进的人改动时尤其要注意精度。很多社区训练 LoRA 或微调模型时只改了 UNet却没有同步调整 CLIP text encoder 的导出逻辑最后效果不理想往往就是条件注入不对齐。4. 第四集把原理连成一条线的文生图代码实战4.1 最小结构VAE 编码、UNet 去噪、VAE 解码四集里前三分之一都在讲概念最后才是真正跑代码。用 diffusers 库能把整条链路写得非常短。最省事的示例是直接用DiffusionPipeline.from_pretrained几行代码就能出图。但为了理解内部过程我建议大家在 Jupyter Notebook 里手动把 pipeline 拆开核心流程只有三步用 text encoder 把提示词编码成文本特征condition初始化随机噪声 latent在 scheduler 的调度下反复调用 UNet 预测噪声经过若干步获得干净的 latent用 VAE decoder 把 latent 解码成像素图像。整体结构可以用以下代码骨架表示4.2 核心采样循环的代码走读以下是我跟着精讲做过的简化版采样循环去掉了很多无关细节只保留从随机噪声到最终图像的完整逻辑。实际使用时用 diffusers 官方 pipe 更方便这里拿手写循环的目的是对照概念。import torch import torch.nn.functional as F from diffusers import AutoencoderKL, UNet2DConditionModel, PNDMScheduler from transformers import CLIPTextModel, CLIPTokenizer # 加载组件 tokenizer CLIPTokenizer.from_pretrained(openai/clip-vit-large-patch14) text_encoder CLIPTextModel.from_pretrained(openai/clip-vit-large-patch14) vae AutoencoderKL.from_pretrained(path/to/vae) unet UNet2DConditionModel.from_pretrained(path/to/unet) scheduler PNDMScheduler(beta_start0.00085, beta_end0.012, num_train_timesteps1000) # 1. 文本编码 prompt [a photo of a cute cat sitting on the windowsill] tokens tokenizer(prompt, paddingmax_length, max_length77, return_tensorspt).input_ids text_embeddings text_encoder(tokens).last_hidden_state # 2. 初始噪声 latent latents torch.randn((1, 4, 64, 64)) # 3. 采样循环 scheduler.set_timesteps(50) for t in scheduler.timesteps: latents latents.to(device, dtypetorch.float16) t_tensor torch.tensor([t], devicedevice) # 时间步 embedding 由 unet 内部处理这里只传标量 tensor noise_pred unet(latents, t_tensor, encoder_hidden_statestext_embeddings).sample latents scheduler.step(noise_pred, t, latents).prev_sample # 4. VAE 解码 image vae.decode(latents / 0.18215).sample image (image / 2 0.5).clamp(0, 1) # 从 [-1,1] 转到 [0,1]这段代码里有几个新手容易犯的错误值得注意潜变量缩放系数 0.18215。VAE 训练时 latent 的方差经过标准化处理采样结束后需要除以这个系数再送入 decoder很多手写循环出图结果一片糊就是因为少了这一步text embeddings 要 padding 到 77 长度。CLIP 的输入定长是 77 个 token长提示词需要截断短提示词需要填充否则 UNet 的 cross-attention 序列维度对不上scheduler.step 返回 prev_sample而不是直接替换 latents。有人写成latents noise_pred那么生成结果就完全不对了因为去噪更新涉及一步差分数值更新不能直接拿预测值当下一状态。4.3 部署踩坑实录Mac 上 dlopen 报错与显存不足排查这套精讲评论区里被问得最多的是Mac 版 Stable Diffusion 无法启动报错 ImportError: dlopen。这个错普遍发生在 macOS 环境安装 torch 时没有正确选择 arm64 对应版本。排查步骤我整理成了清单用python -c import platform; print(platform.platform())确认 Python 和系统的架构检查 torch 版本是否针对 Apple Silicon 编译。推荐执行pip install torch系统会自动拉取对应平台版本如果你之前从其他地方拷贝了 x86_64 的 wheel就会在动态加载时触发 dlopen 报错确认 PyTorch 的 MPS 后端可用torch.backends.mps.is_available()返回 True 才能用to(mps)跑依赖版本冲突不能靠肉眼判断直接用pip check看是否有依赖互相覆盖如果还报错把完整 traceback 贴出来看是哪个 so/dylib 加载失败通常是 libomp 或 libtorch_cpu.dylib 缺失。显存不足是另一个高频问题。桌面显卡跑 512x512 图最低建议 6GB 以上显存如果你是 4GB 也要硬跑可以降低 batch size 或者开启 attention slicing。diffusers 提供了pipe.enable_attention_slicing()和pipe.enable_vae_slicing()这两招能大幅降低峰值显存。Automatic1111 WebUI 里则是 --medvram 参数与 enable_attention_slicing 原理类似。4.4 从 GitHub 找模型和微调资源的经验顺着 GitHub 关键词stable diffusion能找到的项目非常多但要学会分辨什么值得深看。我常用的筛选标准是看 README 里是否明确写清楚了模型架构权重来源、license、训练数据组成看 issue 里是否频繁出现加载失败类问题如果项目长期不更新且 issue 无人回应别采用看是否提供完整的model_index.json和子文件夹目录结构这决定了 diffusers 能否无缝加载。想训练或者微调自己的文生图模型最有价值的资源不是某段生成代码而是官方训练脚本比如 diffusers 仓库里的train_text_to_image.py和train_text_to_image_lora.py。读这类文件你会真正理解如何用 CLIP 文本条件联合训练 UNet的数据组织方式。至于CLIP 模型应用的扩展方向社区里很多是把 CLIP 的能力拆出来单独用比如让 CLIP 给一组候选图做排序、或者结合 YOLO 一起做检测视觉编码器做区域语义筛选、CLIP 做全局图文匹配。这种组合思路在需要找图-描述-过滤的应用链路上特别好用。4.5 往后还能玩的方向CLIP 微调、Turbo/LCM 加速、ComfyUI 节点化如果基本原理已经通了接下来的进阶路线大概有三条微调 text encoder。对特定画风或者自定义概念微调 CLIP 文本编码器能改变提示词理解但精度要求比较高务必用低学习率和少量数据试跑否则会把通用语义破坏用 z-image-turbo 这类快速模型。它们本质是蒸馏后的 LoRA 或小模型配合低步数采样器可以做到不到 10 步出图。我们做了测试8 步和 30 步之间肉眼差距已经很小非常适合做实时生成和动画ComfyUI 节点化改造。ComfyUI 把 CLIP、UNet、VAE、采样器全部暴露成可视化节点很适合验证不同模块的相互影响。如果你在找CLIP 询问机这类自定义节点工具本质也是对 CLIP 特征能力的再封装配合工作流可以做到语义控制更精准。我对这套精讲唯一的补充建议是别急着直接套用现成 pipe先用手写循环跑通一次再对照官方教程对比差异你会在代码里获得比视频更多的直觉。尤其当你看懂了scheduler.step那个更新公式时之前所有关于为什么不同采样器出图差别大的疑惑都会迎刃而解。