news 2026/10/15 13:54:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从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.5:OpenAI CLIP ViT-L/14,文本特征维度 768;
  • SD 2.1:OpenCLIP ViT-H,文本特征维度 1024;
  • SDXL:双文本编码器,OpenCLIP ViT-bigG(dyhd 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。DDPM(Denoising 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 解码器把潜变量还原成清晰图片。

为什么非要压缩?原因很直接:计算量差异巨大。512x512=26 万像素点,64x64=4096 个特征点,差了 64 倍。如果直接在像素空间做扩散,同样的参数量需要大量显存和更长训练时间。潜空间扩散还能享受感知压缩带来好处:VAE 已经帮模型过滤掉了大量人类不敏感的高频像素噪声,UNet 只需要专注于更高级的语义结构生成。

这也是为什么 video cards 显存不够的人,宁可降低 batch size 或分辨率,也要保留潜空间尺寸设置的原因。分辨率一变,latent 尺寸是跟着变的(8 倍下采样),后续 UNet 的注意力计算也会随之变化。

2.4 采样器、步数与确定性:DDIM 和 DPM++ 在干什么

采样策略对出图效果的影响,经常被新手低估。简单说,网络预测完噪声后,我们其实是在解一个"从带噪状态去噪得到更干净状态"的方程。由于这个方程有近似解,不同采样器就是不同数值解法。

  • DDIM(Denoising 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 拆开,核心流程只有三步:

  1. 用 text encoder 把提示词编码成文本特征(condition);
  2. 初始化随机噪声 latent,在 scheduler 的调度下反复调用 UNet 预测噪声,经过若干步获得干净的 latent;
  3. 用 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_start=0.00085, beta_end=0.012, num_train_timesteps=1000) # 1. 文本编码 prompt = ["a photo of a cute cat sitting on the windowsill"] tokens = tokenizer(prompt, padding="max_length", max_length=77, return_tensors="pt").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, dtype=torch.float16) t_tensor = torch.tensor([t], device=device) # 时间步 embedding 由 unet 内部处理,这里只传标量 tensor noise_pred = unet(latents, t_tensor, encoder_hidden_states=text_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 对应版本。排查步骤我整理成了清单:

  1. 用python -c "import platform; print(platform.platform())"确认 Python 和系统的架构;
  2. 检查 torch 版本是否针对 Apple Silicon 编译。推荐执行pip install torch,系统会自动拉取对应平台版本,如果你之前从其他地方拷贝了 x86_64 的 wheel,就会在动态加载时触发 dlopen 报错;
  3. 确认 PyTorch 的 MPS 后端可用:torch.backends.mps.is_available()返回 True 才能用to("mps")跑;
  4. 依赖版本冲突不能靠肉眼判断,直接用pip check看是否有依赖互相覆盖;
  5. 如果还报错,把完整 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那个更新公式时,之前所有关于"为什么不同采样器出图差别大"的疑惑都会迎刃而解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:53:59

OpenClaw接入iMessage:用Mac云实例打造消息自动化流水线

说实话,把 OpenClaw 这种可以跑规则的智能体接到 iMessage 里,是我最近折腾完觉得最值的一次部署。以前我处理消息全靠手动:看到待办转去提醒自己,收到通知再去查一遍,群里的链接还得复制到别处。现在这套配置跑起来之…

作者头像 李华
网站建设 2026/10/15 13:53:20

手机图片转 PDF!苹果、安卓通用,4 种实用方法,本地、线上都可行

日常工作生活里,经常会遇到需要把相册里的照片、截图批量转为 PDF 的场景,提交材料、发送扫描件都要用到。很多人不知道,不用电脑,仅凭手机就可以轻松完成图片转 PDF。优先推荐手机本地自带功能,处理隐私文件更安全稳妥…

作者头像 李华
网站建设 2026/10/15 13:53:15

汽车后市场数字化转型:微信小程序解决方案

1. 项目背景与核心价值汽车后市场服务领域正在经历数字化转型的关键时期。根据行业调研数据显示,2023年国内汽车后市场规模已突破1.3万亿元,其中线上服务渗透率不足15%,存在巨大的数字化升级空间。这个04816汽车售后服务小程序项目&#xff0…

作者头像 李华
网站建设 2026/10/15 13:52:17

MT4 EA接入国内期货:CTP桥接方案架构与部署实践

简介:MT4CTP 程序化交易软件面向熟悉 MT4 平台、又有国内期货交易需求的量化用户,解决了 EA 策略如何对接 CTP 通道的问题,让已有 MT4 策略可以平滑迁移到国内期货品种上。压缩包共 384 个文件,大小 15.86MB,其中 mq4、…

作者头像 李华
网站建设 2026/10/15 13:51:55

等保2.0计算环境安全测评详解:从身份鉴别到数据备份的实战指南

1. 开头:一次真实测评中,客户对“计算环境”的误读之前接一个三级系统测评项目时,客户信息安全负责人跟我说:“计算环境安全测评,是不是就是看看服务器杀毒软件有没有装、密码有没有改?”我当时笑了笑&…

作者头像 李华
网站建设 2026/10/15 13:51:40

SylixOS嵌入式开发避坑指南:实时性、工具链与系统诊断

简介:《SylixOS应用开发手册1》是一本面向嵌入式开发者与实时系统工程师的权威实践指南,聚焦SylixOS这一广泛应用于航空航天、工业自动化、通信及新能源等关键领域的国产实时操作系统。资源适用于具备C语言基础的中高级开发者,无论是否熟悉Li…

作者头像 李华