☰
Stable Diffusion原理拆解:CLIP、UNet与扩散模型完整链路
2026/10/14 7:23:11 网站建设 项目流程

差不多一年前,我开始正经接触 Stable Diffusion,当时只会对着 WebUI 的界面点鼠标,换个模型试个 prompt,图出了也就出了。直到后来有段时间做项目,需要真正理解每一步在干什么,才把 CLIP、扩散模型、UNet 这几块从头到尾啃了一遍,配合网络上流传度很高的四集精讲视频,加上 diffusers 源码,总算把这套东西串成了完整的链路。这篇笔记就是那段时间的整理,定位是给已经用过 SD 出图、但还没系统梳理过内部原理的人看的。看完你应该能明白:Stable Diffusion 为什么由这几个核心模块组成,CLIP 在里面扮演什么角色,扩散模型的“去噪”到底在学什么,UNet 如何在潜在空间里工作,以及最后怎么用几十行代码从零把一个文生图流程跑通。

1. Stable Diffusion 的整体架构:为什么文生图要做成“多模块协作”

1.1 端到端模型的困境:为什么不能一步从文本直接生成图像

很多人第一次接触 Stable Diffusion 时都会有一个直觉问题:为什么不能像 GPT 那种大模型一样,输入一句话直接输出一张图?实际上最早期的图像生成模型确实走过“一步生成”的路线,典型代表就是生成对抗网络的变体。但你如果玩过那一代模型就会很清楚,它们有致命的问题——训练不稳定,模式坍塌严重,生成的图片类型稍微一变就作废,而且几乎无法做到精确的文本控制。

换个角度想:文本和图像是两种完全不同的模态。语言是离散的、高层次的抽象符号,图像是连续的、低层次的像素阵列。从前者直接跳到后者,中间跨度太大,模型很难同时学会“理解语义”和“生成细节”。Stable Diffusion 的解题思路很明确:拆解任务,每个模块只干一件事。它不尝试一步到位,而是把整个生成过程拆成“语义理解—逐步去噪—像素还原”三个环节,每个环节用不同的网络结构去承担。

1.2 三个核心模块的分工:CLIP、UNet、VAE 各管哪一段

整套 Stable Diffusion 里,主要参与计算的模型有三块,外加一个采样调度器。我用一个通俗的类比来帮助你建立直觉:把生成图像的过程想象成请人画一幅画。

  • CLIP 文本编码器:他是那个听你描述需求的人,负责把你说的话提炼成一份“绘画指令”。只是这份指令不是人话,而是一个高维向量序列。
  • UNet 去噪网络:他是那个对着指令在画布上反复修改的画家。他拿到指令和你给的初始草稿(纯噪声),在你指定的“语义方向”上一次一次修正,每一轮把画面推得更清晰一些。
  • VAE 解码器:他是最后的装裱师,把一幅在紧凑画布上完成的作品放大成最终可以展示的精美成品。
  • 采样调度器(Scheduler):他不是画师,而是给你规定节奏的监工——决定每一步改多少、怎么改,也就是去噪过程中每步的降噪强度。

这套分工下来,文本、图像、表意、细节各自由不同结构负责,训练时也可以针对性地优化某个环节。

1.3 数据流全景:从一段提示词到一张 512×512 图像要经过几步

把上面三个模块串起来,完整的数据流是这样的:

  1. 输入提示词字符串,比如 “a photo of an astronaut riding a horse on mars”。
  2. Tokenizer 把这句话切成若干 token,每个 token 是一个离散数字索引。
  3. CLIP 文本编码器把这串索引映射成 shape 为[1, 77, 768]的特征向量序列,这就是下面所有环节要用的“条件”。
  4. 同时,初始化一个纯高维噪声张量,shape 为[1, 4, 64, 64],这个 64×64 不是最终分辨率,是潜在空间里的“画布”。
  5. UNet 在这个 64×64 的潜在张量上做几十轮去噪,每一轮都参考 CLIP 给出的条件向量。
  6. 去噪完成的潜变量交给 VAE 解码器,从[1, 4, 64, 64]解码成[1, 3, 512, 512]的像素空间图像。

这六步就是文生图最核心的链路。后面所有复杂玩法——换模型、加 LoRA、加 ControlNet——本质上都是在链路的某个节点上做插入或替换。

提示:SD1.5 用的 VAE 下采样倍率是 8,所以 512×512 的像素图,在潜在空间里是 64×64;4 个通道对应 VAE 编码器输出的压缩特征,不是 RGB 那种 3 通道。

2. CLIP 文本编码器:一段话如何变成“图像风格指令”

2.1 CLIP 的训练逻辑:对比学习让图文走向同一个语义空间

想要让文本能控制图像,前提是文本和图像必须能映射到同一个向量空间里“对齐”。CLIP(Contrastive Language-Image Pre-training)做的就是这件事。它在训练时拿海量的图文对——图片和描述该图片的句子——用两个编码器分别编码。

  • 图像编码器:把图片映射成一个特征向量。
  • 文本编码器:把描述文字映射成另一个特征向量。

训练目标不复杂:让同一对图文对应的两个向量距离近,让不匹配的图文向量距离远。这种用“拉近正样本、推远负样本”的方式训练的思路,叫对比学习。CLIP 原论文里训练数据是 4 亿个图文对,在如此大规模的数据量下,模型记住了大量的图文对应关系,最终文本编码器输出的向量,天然具备了和图像内容对齐的能力。

这里有一个值得留意的细节:CLIP 的文本编码器不是简单地把每个词揉成向量就完事了,它内部是类似 GPT 的因果注意力结构,最后一个隐藏状态其实已经聚合了整句话的上下文语义。这也是为什么你在代码里拿到的是[1, 77, 768],而不是 77 个独立离散的词向量拼接——你拿到的是一整句话融合之后的语义表示。

2.2 你实际拿到的特征长什么样:77 个 token 与 768 维向量

具体到 Stable Diffusion 1.5,CLIP 文本编码器来自 OpenAI 的 ViT-L/14 模型分支。它有几个硬性参数,你必须搞明白:

  • 最大 token 长度:77。超出会被截断。这就是为什么长 prompt 写到后面可能“不生效”,因为后面的内容根本没进入编码器。
  • 特征维度:768。每个 token 对应一个 768 维的向量,77 个 token 就是 77×768 的矩阵。
  • 文本编码器本身是冻结的:SD 训练时并不是从头训练 CLIP,文字编码部分直接用 OpenAI 预训练好的 CLIP,只在训练 UNet 时把文本 embedding 当固定条件灌入。

你给模型写“a photo of a cat”和“a painting of a cat”,在 768 维空间里它们会落在不同的位置上,UNet 收到不同的条件向量,产生的去噪引导自然不同。这也是为什么换一个 prompt,出来的图风格、布局会跟着变——因为条件向量的方向已经变了。

2.3 为什么“换个说法”图片会差这么多:条件的敏感度来自哪里

很多人会困惑:我只是在提示词里加了个词,为什么画面变化常会超出预期?原因在于 CLIP 是在整个句子上做注意力建模,一个词的变化会同时影响其他所有位置的隐藏状态。也就是说,文本特征并不是 77 个词向量的简单堆叠,而是每个向量都携带了整句话的信息。

比如你在句末加上 “, golden hour”,CLIP 内部在编码“photo”和“astronaut”时,也会参考到“golden hour”的信息。整个 77×768 矩阵都会向右偏移,UNet 在 cross-attention 里接收到的条件整体改变,生成的画面就会大面积变化。

理解这一点对实际调参很有用:想要大改构图,就改句子的主干描述;想要只微调氛围,就尽量维持主干描述,在修饰词上做小幅改动。因为主干描述对应的向量主成分不变,修饰词带来的变化只是叠加在细粒度方向上。

2.4 CLIP 微调和衍生工具:为什么有人要单独动文本编码器

社区里讨论热度很高的“CLIP 模型微调”,本质上是针对特定领域继续训练文本编码器。SD 原版的 CLIP 是通用的,但有些垂直领域的用词它没有很好覆盖,比如特定画师名、特定游戏角色名、圈内特有的艺术风格词汇。微调就是用一批目标领域的图文对,继续拉近特定词汇和图像特征的距离。

而 ComfyUI 里那些和 CLIP 相关的自定义节点——比如“CLIP 询问机”(ClipAsk 类节点)——做的事情是在 CLIP 文本编码前后进行干预:要么组合多个 CLIP 模型的输出,要么把不同提示词分段编码再合并,本质上都是在调整注入 UNet 的条件向量。

注意:如果你打算微调 CLIP 文本编码器,学习率一定要比微调 UNet 小得多,否则很容易灾难性遗忘,把原本通用的语义理解能力弄坏。

3. 扩散模型的核心直觉:用“逐步去噪”代替“一步生成”

3.1 前向加噪:把一张清晰图像一点一点毁掉

扩散模型的出发点很有意思:想要学会怎么生成一张图,先学会怎么“毁掉”一张图。训练时,我们拿一张真实图像,按照公式逐步往里面加入高斯噪声:

x_t = sqrt(α̅_t) * x_0 + sqrt(1 - α̅_t) * ε

其中 ε 是标准高斯噪声,t 是时间步,α̅_t 是一个预设的单调递减系数。t 越大,原图成分越少、噪声成分越多。当 t 到足够大的时候,原图几乎完全被噪声覆盖,看起来就是一张纯雪花图。

如果把这个过程画出来,就是一个“把墨水滴进清水”的过程:最开始墨滴轮廓清晰,越搅越散,最后整杯水完全变成均匀浑浊的黑色。数学上这套噪声调度方案叫噪声表,每步噪声强度的设置在训练前就定好了,不需要学习。

3.2 反向去噪:让模型学会“倒放”

扩散模型的生成阶段,就是前向过程的“倒放”:盖着从纯噪声开始,一步步去掉噪声,直到恢复出清晰的图像。问题是,纯噪声有无数种“去噪”方式,模型怎么知道每一步该往哪个方向去?

这就是 UNet 要学的东西。训练时我们明确告诉它:给定带噪图像 x_t 和时间步 t,请预测噪声 ε。损失函数是均方误差,公式形式上就是:

L = E_{t, x0, ε} [ || ε - ε_θ(x_t, t) ||² ]

也就是说,模型的目标是“认出来我手上这张带噪图像里,到底掺杂了哪些噪声”。一旦噪声被预测出来,拿当前的带噪图减去噪声,就相当于向原图迈近了一步。反复执行这个过程几十次,就能从噪声里恢复出完整图像。

有一个很容易搞混的地方:你可能会问,模型预测的是纯噪声吗?严格来说是“剩余噪声”。每一步去噪后得到的图像还会残留噪声成分,下一轮继续预测和去除,如此反复迭代。最终第 50 步结束时,噪声被清除得足够干净,才得到最终的潜在表示。

3.3 采样的数学原理:为什么需要迭代多次而不是一步到位

如果扩散过程总共 T 步,理想情况下要从 x_T 一直采样到 x_0 需要执行 T 次去噪循环。DDPM 原文里 T 是 1000 步,这在训练时没问题,但实际跑推理就太慢了。所以后面的工作重点都在“少跑几步还能保证质量”。

DDIM 是第一个里程碑式的改进。它把原本的马尔可夫链推导过程改造成了一个“非马尔可夫”形式,允许跨步采样。原本需要 1000 步的 DDPM,DDIM 可以用 50 步甚至 20 步跑完,生成的图片质量在同等步数下明显更平滑。现代采样器,如 DPM-Solver、DPM++、Euler a,本质上都是用 ODE 求解的数学加速技巧,在不牺牲太多质量的情况下进一步把步数压到 10~30 步。

所以在 diffusers 里你看到的scheduler.set_timesteps(50),意思就是告诉采样器:请在 1000 步的噪声表里均匀挑 50 个时间点,按加速算法在这些时间点上执行跳步采样。

3.4 潜在扩散模型的降维策略:为什么在潜空间扩散,而不是像素空间

Stable Diffusion 全称其实是“潜在扩散模型”(Latent Diffusion Model, LDM)。关键差异在于:原始扩散模型在 512×512×3 的像素空间里做前向加噪、反向去噪,这 78 万个元素的计算量大到不实用。LDM 的聪明之处是先用一个自编码器把图像压缩到潜在空间,再在潜在空间里做扩散。

具体到 SD1.5:VAE 编码器把 512×512×3 的像素图压缩成 64×64×4 的潜变量,体积缩小了 48 倍。扩散过程在这个极紧凑的 4 通道低分辨率张量上运行,UNet 处理的数据量小了一个数量级,训练和推理的速度才变得能被消费级显卡接受。

代价是图像细节会受 VAE 编解码能力上限的约束。这也是为什么大模型社区慢慢开始争论 VAE 是不是瓶颈,后面才出现了各种微调过的 VAE 模型。

4. UNet2DConditionModel:潜空间里的“精细画师”

4.1 为什么选择 UNet 结构:编码器-解码器与跳跃连接

去噪网络的骨干选型用了 UNet,这是很多图像分割任务里验证过的经典结构。它的基本框架是:

  • 编码器部分:逐层下采样,分辨率降低、通道数增加,提取图像的语义特征。
  • 解码器部分:逐层上采样,分辨率恢复、通道数减少,重建出和输入同尺寸的结果。
  • 跳跃连接:每一层下采样后的特征直接拼接送给对应尺度的上采样层,帮助解码器保留局部细节。

这套结构天然适合“输入输出同尺寸”的任务。去噪就是输入一个带噪张量、输出一个同尺寸张量,UNet 既有语义理解能力(下采样),又有细节保留能力(跳跃连接),所以被选作扩散模型的核心网络。

SD 里用的条件版 UNet 全名叫UNet2DConditionModel,意思是加入了额外条件输入(文本特征)的 UNet。它接收三样东西:

  1. 带噪的潜变量张量,shape 为[B, 4, 64, 64]。
  2. 当前时间步 t(一个标量)。
  3. 文本条件encoder_hidden_states,即 CLIP 输出的[B, 77, 768]。

输出是一个与输入同 shape 的预测噪声。

4.2 时间步嵌入:让同一个网络学会不同阶段的去噪

UNet 是“同一个模型处理所有时间步”,但第 5 步和第 40 步去噪时,模型要做的事情显然不同:前期主要是确定大结构,后期是补充细节。模型怎么区分当前处于哪个阶段?

答案是时间步嵌入。t 首先被编码成正弦位置向量,再经过一个小型 MLP,变成和 UNet 内部特征通道数一致的向量。这个时间向量会被加到 UNet 各个层的特征上,相当于一个“进度指示器”,告诉每一层:现在噪声还有多少,你该下手重一点还是轻一点。

如果你写过代码,就会看到unet(sample, timestep, encoder_hidden_states)接口里专门有一个单独的timestep参数。不能省略,也不能填错,否则模型完全不知道此刻应该执行哪种强度的去噪。

4.3 CrossAttention:文本条件真正“管住”画面的地方

UNet 接收文本条件,靠的是 CrossAttention 层把文本信息写入图像特征。原理不复杂:

  • 图像侧的某个中间特征张量经过变换生成 Query。
  • 文本侧的 77×768 向量经过变换生成 Key 和 Value。
  • 让图像区域内每个位置,去文本序列里寻找“哪些文字和这里相关”,再用相关性把对应的文本 Value 加权融合进来。

所以画布上如果一个区域最后生成的是宇航员,那在这个区域的表示中,cross-attention 会重点关注 “astronaut” 对应的那些 token 向量,并吸收它们的信息。这就是文本内容能“指挥”图像内容的空间位置分配的原因。

实际训练时,UNet 里会在多个不同的分辨率尺度上都布置 cross-attention 层,这样文本既能控制宏观布局,也能控制局部材质细节。

4.4 UNet 使用时的几个注意事项

结合社区里大家踩过的坑,我列几条最常见的:

  • UNet 的输入通道数不是 3,而是 4。接入自定义模型时,第一次跑代码看到in_channels=4不用惊讶,这是潜变量的通道数。
  • 注意数据精度:显卡上常用 fp16 加速,但 text encoder 和 VAE 建议保持 fp32 精度或同样 fp16,不能让同一个 pipeline 里精度不统一,否则会在采样过程中出现噪声越来越大的现象。
  • CFG 强度过低时图像容易漂移,过高时会过饱和。SD 里常用的 7.5 属于 1.5 模型的经验值,SDXL 通常推荐 5~6,需要区分模型。
  • 想本地调试但显存不足时,可以缩短侧面尺寸(如 30~40 步),或用少通道的 UNet 变体,先验证逻辑再接回完整模型。

5. 代码实战:用 Diffusers 手写一个文生图循环

前面四节把 CLIP、扩散模型、UNet 的分工讲清楚了,接下来把它们拼进真实代码。我会故意避开StableDiffusionPipeline那种一行出图的方式,因为那一行封装了太多逻辑,不利于建立概念。下面这么做是把 Pipeline 内部拆开,一步步显式调用,你跑通一遍之后再回去看 Pipeline 源码,会豁然开朗。

5.1 环境准备与核心库选型

推荐从 diffusers 入手,它是 HuggingFace 维护的官方库,模型组件的加载和调度器的管理都封装得很干净。需要装的包:

pip install diffusers transformers accelerate torch --upgrade

模型我选runwayml/stable-diffusion-v1-5,这是社区最常用的基础模型。如果你网络不便,可以提前下载到本地再用from_pretrained(本地路径)加载。

5.2 显式拆解的文生图完整代码

下面的代码就是从零手写的简版文生图循环:

import torch from transformers import CLIPTokenizer, CLIPTextModel from diffusers import AutoencoderKL, UNet2DConditionModel, PNDMScheduler from PIL import Image model_id = "runwayml/stable-diffusion-v1-5" device = "cuda" if torch.cuda.is_available() else "cpu" dtype = torch.float16 if device == "cuda" else torch.float32 # 1. 加载四个核心组件 tokenizer = CLIPTokenizer.from_pretrained(model_id, subfolder="tokenizer") text_encoder = CLIPTextModel.from_pretrained(model_id, subfolder="text_encoder").to(device=device, dtype=dtype) vae = AutoencoderKL.from_pretrained(model_id, subfolder="vae").to(device=device, dtype=dtype) unet = UNet2DConditionModel.from_pretrained(model_id, subfolder="unet").to(device=device, dtype=dtype) scheduler = PNDMScheduler.from_pretrained(model_id, subfolder="scheduler") prompt = "a photo of an astronaut riding a horse on mars" # 2. 文本编码 text_input = tokenizer( prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt", ) with torch.no_grad(): text_embeddings = text_encoder(text_input.input_ids.to(device))[0] # 此时 text_embeddings shape: [1, 77, 768] # 3. 无条件编码(CFG 用) uncond_input = tokenizer( [""], padding="max_length", max_length=tokenizer.model_max_length, return_tensors="pt", ) with torch.no_grad(): uncond_embeddings = text_encoder(uncond_input.input_ids.to(device))[0] # 拼接成 [2, 77, 768] text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) # 4. 初始化潜变量 generator = torch.manual_seed(42) latents = torch.randn( (1, unet.in_channels, 64, 64), generator=generator, device=device, dtype=dtype, ) latents = latents * scheduler.init_noise_sigma # 5. 迭代去噪 scheduler.set_timesteps(30) for i, t in enumerate(scheduler.timesteps): latent_model_input = torch.cat([latents] * 2) latent_model_input = scheduler.scale_model_input(latent_model_input, t) with torch.no_grad(): noise_pred = unet( latent_model_input, t, encoder_hidden_states=text_embeddings, ).sample # 分类器自由引导 noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) noise_pred = noise_pred_uncond + 7.5 * (noise_pred_text - noise_pred_uncond) latents = scheduler.step(noise_pred, t, latents).prev_sample # 6. VAE 解码回像素空间 latents = latents / vae.config.scaling_factor with torch.no_grad(): image = vae.decode(latents).sample # 7. 数值范围调整并保存 image = (image / 2 + 0.5).clamp(0, 1) image = image.cpu().permute(0, 2, 3, 1).numpy() img = (image[0] * 255).astype("uint8") Image.fromarray(img).save("astronaut.png")

5.3 逐段解释:每一步对应什么原理

  • 第 2 步:tokenizer 把句子按 BPE 切词表切成 token 索引,max_length=77截断。如果把 tokenizer 输出的input_ids还原成文本,你就会看到 “a photo of ” 这类子词。
  • 第 3 步:无条件 CLIP 编码生成的[1, 77, 768]向量,专门用于 CFG 计算。在 UNet 每次前向之前,条件向量和无条件向量拼接成 batch size 2,一次算出两组预测。
  • 第 5 步:噪声预测结果被.chunk(2)拆成两份,然后按noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)组合,这是 CFG 引导的核心表达式。guidance_scale 越大,图像越“听话”,但过大会产生伪影和色彩过艳。
  • 第 5 步末尾的scheduler.step是采样器在起作用:根据预测噪声和当前时间步 t,计算出更清晰的下一步潜变量。
  • 第 6 步:VAE decode 之前必须要除scaling_factor。这个值是为了让 VAE 编码器输出的潜变量方差保持约等于 1,方便扩散过程使用标准高斯分布初始化。忘了除,生成的图会发灰发暗。
  • 第 7 步:VAE 输出的像素值范围大约在[-1, 1],所以要做(x / 2 + 0.5),映射回[0, 1]后转成 uint8 保存。

5.4 运行过程中的典型报错与解决办法

跑这套代码最常见的两类问题:

一类是环境问题,尤其是 Mac 上容易出现类似ImportError: dlopen ...的报错。这通常是因为本机缺少某些原生库,或库版本不兼容。常见对策:优先使用 conda 创建干净环境,再安装 PyTorch 全家桶;如果仍然报错,检查是否是编译工具链问题,换用预编译的 wheel 版本往往能解决。苹果芯片用户注意,mps后端在部分算子上的行为与 CUDA 不完全一致,特别复杂的模型推理建议先用 CPU 跑通验证逻辑,再切 MPS 提速。

另一类是显存不足。把torch.randn的 batch 调小、采样步数降下来,或者统一用torch.float16加载模型(注意不要让 VAE 和 UNet 精度混用)。再不行就把 latent 的尺寸往下调,比如 32×32,虽然出图分辨率低一些,但流程完全可以验证。

6. 四集精讲课之外的延伸:采样器、加速模型与工具链

6.1 采样器与步数的实际关系:为什么同一个模型换个采样器就不同

前面 5.2 用了 PNDMScheduler。PNDM 是 SD1.5 默认的采样器,稳定但不算快。如果你把scheduler换成DPMSolverMultistepScheduler,同样 30 步出图质量明显更好,甚至 15 步都和原来 30 步差不多。原因是它在数学上用高阶 ODE 求解器逼近去噪轨迹。

社区里说的“换采样器”本质上是换调度器中的数值求解策略。DDIM 偏可复现,Euler 系列简单直观,DPM++ 带额外的二阶校正,每一步的计算量稍高但每一步的信息量也更足。实践建议:写代码时把采样器作为可插拔选项,跑同一组 prompt 对比不同采样器的效果,再做选择。

6.2 Turbo 与 LCM:为什么能把步数压到 8 步甚至 4 步

如果你的机器算力有限,社区这些年专门发展出一条“减少推理步数”的路径。比如 SDXL Turbo、LCM(Latent Consistency Model),它们都在训练阶段做了蒸馏——把教师模型(普通 SD)多步采样的结果,压缩成学生模型一两次采样就能逼近的程度。

这类模型的原理不复杂:让一个学生网络模仿教师网络从不同时间步出发的最终输出结果,学到的是一条“捷径”。使用时它们往往不支持很高的 CFG 值(经常建议 1~2),这会让你的代码里无条件文本编码部分可以直接省略。需要留意的是速度和质量的平衡,提速的同时大动态范围画面的控制力确实会下降,这是这类模型的固有取舍。

6.3 从代码到 ComfyUI:模块化工作流的逻辑沉淀

如果你写一遍上面的显式循环,再打开 ComfyUI 看它的节点图,会发现非常熟悉:加载 CLIP、加载 UNet、加载 VAE、设置采样器、设置步数和 CFG——这些节点对应的就是你代码里的每个步骤。ComfyUI 把这段代码链路图形化之后,允许你自由换节点、组合临时分支,这也是为什么它被社区称为“CLIP 询问机”之类高级节点的温床。

理解代码链路还有一个隐藏收益:调试 ComfyUI 工作流出问题时,你能准确判断问题出在哪一层——是文本编码阶段,还是采样阶段,还是 VAE 解码阶段。比如出图发灰,优先检查 VAE 的 scaling 和 dtype;出图结构混乱,多半在 CFG 和采样步数;提示词完全不生效,则要检查文本向量是否真的接入了 UNet 的 cross-attention。

6.4 进阶方向:双编码器模型、ControlNet 与 LoRA 的联动逻辑

当你把这套核心链路完全吃透,再看 SDXL 就很容易。SDXL 的文本部分用了两个 CLIP 编码器,分别输出 768 维和 1280 维特征,两个塔的特征拼成一个综合条件向量,在一定程度上拓宽了语义细节。ControlNet 则是在 UNet 的输入侧再接一个可训练的副本,把额外条件(如边缘图、骨骼图)注入到 UNet 各层。LoRA 更轻量,它并不是单独的大模型,而是在 UNet 和文本编码器中插入少量低秩矩阵,以极小的参数量微调生成风格。

社区热词“unet 模型改进”,讨论的也大多集中在 cross-attention 层的位置、通道注意力机制、多尺度特征融合这些设计上。一旦你对 4.3 讲的 cross-attention 理解到位,再读这类论文就会很快抓住它们的改动到底改了哪里。

我个人学下来的体会是,Stable Diffusion 的“原理”其实并不高不可攀。最难的不是某个具体的数学推导,而是先建立起整个模块化设计的直觉。建议按这条路径走:先把上面的代码跑通,然后去 diffusers 源码里读StableDiffusionPipeline,把代码中每一行和这里的显式步骤对应起来;读完代码再回头翻 DDPM、DDIM、LDM 三篇论文,你会忽然发现,很多花费大量篇幅讲的细节其实在代码里只是几行参数配置。到那个时候,你就已经完全具备自己改模型、做微调、控制生成过程的能力了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询