ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Stable Diffusion原理精讲:从文本编码到UNet去噪全流程

Stable Diffusion原理精讲:从文本编码到UNet去噪全流程

这两年做生成式AI方向的开发,身边的同事问得最多的一个问题就是:Stable Diffusion到底是怎么把一句自然语言提示词变成一张图像的?很多朋友用各类工具调用SD模型已经驾轻就熟,但一碰到CLIP编码器、扩散模型、UNet这些词组合在一起就发怵,总觉得中间隔着一层黑盒。我去年完整梳理了一套四集精讲笔记,按“总览-文本编码-扩散原理-去噪网络-代码实战”的顺序把这条链路彻底捋了一遍,这份笔记就是那次整理出来的成果。它解决的核心问题,是帮你在调API和WebUI之外,真正建立起对文生图全流程的完整认知,适合刚接触扩散模型、想深入理解原理的初学者,也适合那些已经调包很久但一直没补上底层逻辑的开发者。

1. 整体架构拆解:一次文生图是怎么走完全程的

1.1 一套流水线,四个核心组件

Stable Diffusion本质上不是一个单一的神经网络,而是一条由多个组件串联起来的图像生成流水线。整条链路可以这样描述:先是文本提示词经过分词和编码,变成模型能理解的数值向量;然后从一个纯随机噪声出发,由一个去噪网络反复迭代,逐步“雕刻”出图像特征;最后再把这组特征解码成真正的像素图像。

这四个核心组件各司其职:

  • 分词器与CLIP文本编码器:负责把提示词变成文本嵌入向量,也就是模型理解的“语义条件”。
  • 扩散模型调度器:负责定义加噪和去噪的节奏,也就是整个生成过程的时间轴。
  • UNet去噪网络:负责接收带噪声的潜变量,并预测每一步应该去掉多少噪声,是真正干重活的模块。
  • VAE解码器:负责把经过UNet处理后的潜变量还原成RGB图像,相当于把“压缩包”解压出来。

用一个生活化的类比来理解:提示词是菜谱,CLIP是负责读懂菜谱的助手,UNet是掌勺的厨师,它根据菜谱一步一步调整菜品状态,而VAE解码器是最后把半成品装盘的服务员。你吃的不是半成品,而是装盘后的成品,但整个味道对不对,核心在于前面几个环节配合得好不好。

这套流程最大的特点是:扩散过程并没有发生在像素空间,而是发生在一个被压缩的低维潜空间里。所以你在代码里看到的中间变量往往是一个(1, 4, 64, 64)的张量,而不是(1, 3, 512, 512)的图片。这个设计直接决定了SD在显存和速度上的可行性,后面会专门展开讲。

1.2 为什么是“CLIP+扩散模型+UNet”这套组合

很多人会问,为什么偏偏是这三个组件配合,换掉其中一个行不行?答案是:这套组合是在“生成质量、计算效率、可控性”三者之间反复权衡后形成的最优解。

先说CLIP。早期生成模型用文本条件,大多是直接把词向量拼进生成网络,但问题是普通词向量和图像特征不在同一个语义空间里,模型学到“猫”这个词和“猫”的图片之间的关联,需要大量训练数据去硬磨。而CLIP在预训练阶段就通过对比学习把文本和图像映射到了同一个语义空间,简单说,它知道“a photo of a cat”和一张猫的图片在语义上是接近的。直接复用CLIP文本编码器,等于把视觉-文本对齐的成果免费拿过来用,大大降低了文生图的训练难度。

再说扩散模型。相比GAN那种生成器和判别器对抗的训练方式,扩散模型的训练目标更简单,只要求网络学会预测噪声,训练过程稳定得多,不容易出现模式崩塌。同时扩散模型天然适合做条件生成,随时可以在推理时控制生成强度,这也是为什么SD生态里能衍生出ControlNet、局部重绘这些玩法。

最后说UNet。UNet最早是用于医学图像分割的,它的编码器-解码器结构配合跳层连接,天然适合做“输入和输出同尺寸”的预测任务。去噪恰好就是这样的任务:输入一张带噪声的图,输出一张同样尺寸的干净图。UNet的多尺度特征提取能力让它能在低分辨率上把握整体轮廓,在高分辨率上恢复细节纹理,这种特性正好匹配扩散模型从粗到细的生成过程。

2. CLIP文本编码器:提示词是怎么变成向量,又是怎么控制画面的

2.1 从一句话到一组向量的完整过程

CLIP在Stable Diffusion里只用了它的文本塔部分。流程是这样的:提示词先被一个BPE分词器切成一个个token,SD 1.x系列最多截取77个token,超出部分直接丢弃。每个token对应一个token id,然后经过嵌入层变成向量,再经过多层Transformer编码,最后输出一个[77, 768]的矩阵。

这里有两个细节值得注意。第一,CLIP输出的不是一整句话的单一向量,而是每个token对应一个向量,也就是一组序列。这非常重要,因为UNet在做交叉注意力时,需要按“图像区域”和“文本token”逐一计算相关性,如果只有一个全局向量,就没法实现“这个区域对应那只猫、那个区域对应那片草地”这种精细化控制。

第二,768这个维度来自CLIP ViT-L/14的文本编码器配置。如果你用的是SD 2.x或SDXL,文本编码器就换成了更大的CLIP模型,输出维度会变成[77, 1280],而且SDXL还会同时使用两个文本编码器。但原理是相通的:把文本变成一组带位置信息的语义向量。

我实际测试下来,一个很实用的经验是:提示词开头的前几个token对生成结果影响最大。因为Transformer的自注意力机制会让前面的token在编码时拥有更强的全局信息,后面token的信息更容易被“稀释”。所以写提示词时,最重要的主体、最想要的风格,一定要放在最前面。

还有一个最常见的误区:CLIP不理解否定词。你写“a photo of a cat without hat”,CLIP不会像人类一样先理解“没有帽子”,它只会把“without hat”当作一个附加语义片段,结果可能照样生成戴帽子的猫。正确做法是换成正向描述,比如“a photo of a cat, bare head”,或者配合负面提示词机制,后面代码实战部分会细讲。

2.2 文本条件如何“注入”到生成过程

文本向量准备好之后,接下来要回答的问题就是:它怎么影响UNet的去噪过程?答案是交叉注意力机制。UNet在每一个注意力层里,都会把图像特征作为Query,把CLIP输出的文本向量作为Key和Value,然后通过注意力计算,让图像的每一个位置“查询”它需要关注的文本信息。

公式是Attention(Q, K, V) = softmax(QK^T / sqrt(d)) V,其中Q来自图像特征,K和V来自文本嵌入。这个公式的含义是:先计算图像每个位置与每个文本token之间的相关度,再把相关度变成权重,最后按权重把文本信息融合进图像特征。

理解这个机制之后,就能解释文生图里的一个核心控制参数:CFG,也就是无分类器引导。训练时,UNet有概率被随机“丢条件”,也就是说输入空文本,强迫模型学会在没有任何文本信息的情况下也能去噪。推理时,我们同时跑两次:一次输条件文本,一次输空文本,然后用下面的公式合成:

最终预测 = 无条件预测 + guidance_scale * (有条件预测 - 无条件预测)

这个公式本质上是对文本对齐方向做一个外推。当guidance_scale为7时,会比单纯使用有条件预测的对齐效果更强,画面更贴提示词;但如果调太高,比如超过15,模型会把“对齐文本”这件事做到极端,导致颜色过饱和、画面出现伪影甚至崩坏。我见过很多人刚开始玩SD,觉得数值越大越听话,结果生成一片狼藉,其实就是没理解这个外推公式带来的副作用。

3. 扩散模型原理剖析:加噪与去噪的博弈

3.1 前向过程:让图像一步步淹没在噪声里

扩散模型的思路可以这样理解:我们先把图像一步步加噪,直到它完全变成一个随机噪声图;然后训练一个网络学会逆向操作,从纯噪声一步步还原出图像。

前向加噪过程定义得很简单。原始图像是x_0,总步数是T,每一步都往图像上叠加一点高斯噪声。经过数学推导可以得出一个闭式公式,任意第t步的带噪图像:

x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon

其中epsilon是标准高斯噪声,alpha_bar_t是一个从1衰减到0的系数序列。这个公式带来的好处是:不需要真的从第0步一步一步加噪声到第t步,而是可以直接一步算出任意t时刻的带噪结果。训练的时候,模型只需要随机选一个t,生成一次噪声epsilon,套公式得到x_t,然后让网络预测噪声即可。

这里alpha_bar_t的衰减节奏就是常说的noise schedule。有的调度器用线性衰减,有的用cosine衰减,不同调度器会影响模型对不同噪声级别的敏感度。简单理解:如果某个时间段的alpha_bar_t衰减太快,图像会在很早期就被噪声淹没,模型就很难学到那段区间的有效信息。这也是为什么不同版本的模型要搭配不同调度器,不能随便乱换。

用生活类比来理解前向过程:一滴墨汁滴进清水,随着时间推移慢慢扩散,最终整杯水都变成均匀的灰色。前向过程就是这段“从清晰到混沌”的录像,而我们事先录好了这段录像,接下来要教一个人学会倒放。

3.2 反向过程:网络学会从噪声里“捞”图像

反向过程的目标,是从一个纯噪声x_T出发,一步步还原出清晰的x_0。UNet在这个过程里扮演的角色是“噪声预测器”:给定当前带噪图像x_t、当前时间步t和文本条件c,输出一个预测噪声epsilon_theta。然后采样器按照一定规则,把预测出的噪声从x_t里“减”掉一部分,得到更清晰的x_{t-1}。

训练目标也因此变得极其简洁,就是一个MSE损失:

L = E[ || epsilon - epsilon_theta(x_t, t, c) ||^2 ]

也就是让网络预测出的噪声尽量接近真实添加的噪声。这里有个值得琢磨的点:为什么不直接让网络预测x_0?因为直接预测干净的图像,在高噪声状态下难度极高,早期噪声几乎完全淹没了图像内容,网络基本无从下手。而预测噪声在数学上更平滑、更稳定,并且可以通过调度器公式间接推导出x_0,所以几乎主流扩散模型都采用噪声预测目标。

反向过程中的另一个核心角色是采样器。DDPM原始方案是一步步按马尔可夫链去噪,速度慢;后来学界发现这个过程可以看作一个连续时间上的微分方程,于是出现了DDIM、Euler、DPM-Solver等采样器,它们能用更大的步长、更少的迭代次数逼近相同结果。

我自己的经验是:步数20到30是大多数场景下的甜点区间。少于20步,细节容易丢失;超过50步,画面改善微乎其微,纯粹浪费计算时间。采样器方面,Euler或DPM++系列在质量和速度的平衡上表现最好,是我日常的首选。

3.3 训练和推理为什么长得不一样

很多人在初学扩散模型时会疑惑:训练时模型只做一次前向预测,为什么推理时要循环几十次?

原因在于训练和推理的目标完全不同。训练阶段,我们追求的是“让网络学会在任意噪声级别下预测噪声”,所以每次迭代只需要随机抽取一个时间步t,算一次损失,反向传播一次。模型在训练中见过所有t下的情况,但从来没有真正走过一遍完整的去噪链条。

推理阶段,我们要从t=T一路走到t=0,每走一步都依赖前一步的输出。这是一个迭代过程,有点像雕刻:第一刀只能砍出大致的轮廓,后面每一刀都在之前的成果上继续精修。因为每一步都依赖上一步的结果,任何一步的误差都会累积到后续步骤,所以采样器的数值精度和步长安排很大程度上决定了最终画质。

理解这个差异后,你会明白为什么采样器可以“跳步”:因为ODE的解可以通过大步长离散化来近似,不需要严格按照训练时的T步走。训练时T可以设到1000步,是为了让加噪过程足够平滑,让模型有充足的中间状态可学;推理时用30步走完,相当于在一条已知轨迹上做稀疏采样,反而效率更高。

4. UNet骨干网络:去噪任务里的主力架构

4.1 从图像分割到图像生成,UNet凭什么胜任

UNet作为一种经典的编码器-解码器结构,最显著的特征是左右对称:左边一路下采样,把特征图越变越小、通道数越来越多;右边一路上采样,把特征图逐步恢复到原始尺寸;中间通过跳层连接把下采样过程中的特征直接拼接到对应层级的上采样特征上。

这个结构在图像分割任务里被验证得非常成功,原因是它兼顾了全局语义和局部细节:下采样路径让网络拥有足够大的感受野,能够“看到”整张图的布局;上采样路径配合跳层连接,让网络在恢复分辨率时能够利用前层的边缘纹理信息,不至于把细节全丢光。

扩散模型的去噪任务恰好也需要这种“先看全局、再补细节”的能力。低噪声状态下的图像只是略微模糊,网络需要有足够强的局部细节感知能力去恢复纹理;高噪声状态下的图像几乎全是噪点,网络反而需要依靠低分辨率的全局特征去判断画面内容到底是什么。UNet的多尺度特性让它在这两种极端状态之间都能工作得不错。

UNet内部的重复单元一般是ResNetBlock加注意力模块的组合。ResNetBlock是基础的特征提取单元,通过残差连接让深层网络训练更稳定;注意力模块则分为自注意力和交叉注意力两种。自注意力负责捕捉图像内部的长距离依赖,让远处区域之间能够互相“商量”;交叉注意力负责与文本信息融合,也就是上一节提到的文本注入点。

4.2 时间步条件:一张给网络看的“进度条”

扩散模型的UNet和普通分割UNet最大的不同,是它多了一个输入:当前时间步t。这个t必须告诉网络“现在噪声水平大概是什么程度”,网络才能决定此时应该优先恢复轮廓还是优先细化纹理。

t不是直接作为整数喂给网络的,而是先经过一个正弦位置编码,变换成一个向量,再经过几层全连接映射成时间嵌入向量。这个时间嵌入向量会被注入到每个ResNetBlock里,通常是通过“缩放加平移”的方式调制中间的归一化层参数。这样每个卷积层都能感知到“当前处于去噪过程的哪个阶段”,从而灵活调整自己的行为。

可以这样理解:t就像导航里的进度条,告诉司机“你已经走了百分之多少的路程,接下来该进入什么路况”。如果没有这个进度信息,网络在低噪声阶段和高噪声阶段用同一套策略处理图像,生成结果必然混乱。

实际调试模型时,如果你留意UNet的输入,会发现除了sample和encoder_hidden_states之外,还有一个timestep参数。这个参数虽然是一个标量,但重要性一点不亚于文本条件。动手实践时不妨试着把timestep随便改成一个固定值,比如一直设成t=500,你会看到网络完全失去节奏感,输出一团模糊或者过度锐化的噪声图,这个实验能非常直观地体会到时间步条件的作用。

4.3 交叉注意力:图文信息融合的“命门”

SD的文本控制能力,最终都体现在UNet内部的交叉注意力模块上。这里Query来自图像特征图,Key和Value来自CLIP文本嵌入。具体过程是:图像特征图先被展平成一组位置向量,每个位置向量分别与所有文本token计算相似度,得到一组权重,再用这组权重把文本向量加权求和后融合回图像特征。

公式里那个除以sqrt(d)很容易被忽略,但它非常关键。当向量维度d很大时,点积结果会非常大,导致softmax输出的分布接近one-hot,梯度很陡。除以sqrt(d)等于把数值拉回合理范围,让注意力分布更平滑,训练更稳定。

交叉注意力的一个实际影响是显存开销。注意力计算复杂度是序列长度的平方,图像特征图分辨率越高,序列长度越长,开销增长得非常快。这也是为什么SD要坚持在潜空间里做生成:64x64的特征图在注意力计算上的代价远低于512x512的像素图,两者差了64倍。如果直接在像素空间做扩散,显存会爆炸,这也是“潜空间扩散”这个概念存在的根本原因。

想直观感受交叉注意力,可以做一个小实验:把文本嵌入里“cat”对应的token向量置零,其他token保持不变,再生成一次图片,观察猫的对象是否消失或者变形。我在复现SD原理时做过这个消融实验,效果非常明显。这种动手验证的方式,比重看十遍论文都有用。

5. 文生图代码实战:从最小管线到手动接管每一步

5.1 用扩散模型库搭一条最小可用管线

现在开始进入最实际的环节:写代码。虽然很多项目已经封装好了高层调用接口,但既然目标是理解原理,我建议至少从管线级代码开始跑一遍。

环境准备很简单,主要依赖深度学习框架和一个扩散模型工具库。安装好依赖后,最简洁的写法是这样:

import torch from diffusers import StableDiffusionPipeline pipe = StableDiffusionPipeline.from_pretrained( "./local_sd_weights", # 换成你自己下载好的权重目录 torch_dtype=torch.float16 ).to("cuda") prompt = "a photo of a cute cat, soft lighting, highly detailed" image = pipe( prompt, num_inference_steps=30, guidance_scale=7.5 ).images[0] image.save("cat.png")

就这么几行,一条完整的文生图流程就走完了。但请注意这里发生了很多事情:from_pretrained自动加载了分词器、CLIP文本编码器、UNet、VAE解码器和默认调度器;pipe(prompt, ...)这行代码内部经历了“文本编码-初始化随机噪声-循环去噪-VAE解码”的全过程。

如果你的机器显卡显存不是很大,可以加上pipe.enable_attention_slicing()和pipe.enable_vae_slicing(),这两个操作会把注意力计算和VAE解码切成小块,略微牺牲一点速度来换显存。我用一块较老的显卡测试过,开启后能把512分辨率生成的显存占用压到8GB以内。

5.2 提示词写法与核心参数的经验值

代码能跑起来之后,真正影响出图质量的往往是那些“看不见的参数”。我整理过一张自己常用的参数速查表:

参数推荐范围说明
num_inference_steps20~30太少细节不足,太多收益递减
guidance_scale6~9低于5画面松散,高于12容易过饱和
height / width64的倍数512或768最佳,模型在这个分辨率附近训练
seed固定整数固定后结果可复现
negative_prompt描述不想要的内容比在正向提示词里写否定词更有效

提示词方面,我习惯按“主体+材质+环境+风格”的结构组织内容,例如:“a cute cat, fluffy fur, in a cozy living room, golden hour lighting, photorealistic, 8k”。重要的主体词放在最前面,逗号分隔不一定要用严格语法,CLIP对短语序列的理解效果不错。

一个容易被忽略的细节是分辨率必须是64的倍数。因为VAE的下采样倍数是8,UNet内部还有多层下采样,最终潜变量尺寸必须是整数。设成513这类非对齐尺寸,轻则报错,重则边缘模糊,我最初踩过好几次这个坑。

如果生成结果经常“不听话”,先看是不是提示词里塞了太多相互冲突的概念。比如“cat”和“dog”同时出现在主体位置,模型只会试图生成一个四不像的融合体。这也是交叉注意力机制决定的:权重会在两个概念之间分散,而不是像人类一样做明确的二选一。

5.3 手动接管每一步:看清管线内部发生了什么

高层管线的代码太顺滑,反而容易让人忽略内部流程。为了让原理和代码对应起来,我把管线拆开,手动控制每一步:

# 1. 文本编码 tokens = pipe.tokenizer( prompt, padding="max_length", max_length=77, truncation=True, return_tensors="pt" ).to("cuda") text_emb = pipe.text_encoder(**tokens).last_hidden_state # 2. 无条件文本(用于CFG) uncond_tokens = pipe.tokenizer( "", padding="max_length", max_length=77, truncation=True, return_tensors="pt" ).to("cuda") uncond_emb = pipe.text_encoder(**uncond_tokens).last_hidden_state # 3. 随机初始噪声 latents = torch.randn((1, 4, 64, 64), device="cuda", dtype=torch.float16) # 4. 设置采样时间步 pipe.scheduler.set_timesteps(30) # 5. 循环去噪 for t in pipe.scheduler.timesteps: latent_input = pipe.scheduler.scale_model_input(latents, t) with torch.no_grad(): noise_pred_uncond = pipe.unet( latent_input, t, encoder_hidden_states=uncond_emb ).sample noise_pred_cond = pipe.unet( latent_input, t, encoder_hidden_states=text_emb ).sample # CFG合成 noise_pred = noise_pred_uncond + 7.5 * (noise_pred_cond - noise_pred_uncond) latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample # 6. VAE解码 image = pipe.vae.decode(latents / pipe.vae.config.scaling_factor).sample

这段代码里latents的形状是(1, 4, 64, 64),其中4表示潜变量的通道数,64是压缩后的空间尺寸。pipe.vae.config.scaling_factor是VAE在训练时为了稳定数值引入的缩放系数,解码前要先按这个系数缩放回去,不然颜色会整体偏移。

手动接管的好处是,你能清楚地看到CFG原来是这么做的:同一个UNet跑了两次,一次带文本条件,一次空条件,然后按比例外推。你还能尝试在循环中间打印某一层的特征图,观察不同t阶段网络关注的重点。这个视角是高层API完全给不了的。

6. 常见问题与排查技巧实录

6.1 生成结果总是不对劲?先按这个顺序排查

做文生图久了,你会发现大多数失败案例的成因非常集中。我第一次复现时也经历过一段“怎么调都不对”的时期,后来总结了排查顺序:

先看提示词。前面说了,重要词放前面,避免冲突概念,不要写否定句。这个排在第一位,因为文本条件是整个流程的起点,起点错了后面全错。

再看步数和引导系数。步数低于15时,细节通常不够;引导系数高于12时,画面容易过饱和、出现噪点。这两个参数是出图风格剧烈变化的最大来源。

再看分辨率。非64倍数会导致特征图边缘被裁切,画面看起来发虚、构图奇怪。

基本完成前三个检查后,绝大多数“不对劲”的图都能找到原因。如果仍然失控,才需要考虑是不是权重文件本身和代码版本不匹配,或者采样器类型不兼容。

6.2 显存不够和速度太慢怎么办

显存问题最直观的表现就是CUDA out of memory。解决办法优先级如下:先把精度降到半精度,例如torch_dtype=torch.float16,这一步通常能省一半显存;然后开启注意力切片,能进一步压内存峰值;最后才是降低分辨率或减小批大小。

如果以上方案仍然不够,可以考虑使用模型卸载功能,把暂时不用的模块从显存搬到内存,用到时再搬回来。代价是速度会慢不少,但至少能跑起来。

速度优化方面,最有价值的点是采样器选择。同样的30步,某些采样器比另一些快30%以上;而同样的画质,用更先进的采样器配合20步就能达到普通采样器30步的效果。所以遇到“慢”的问题,优先检查采样器而不是盲目缩减步数。

6.3 为什么两次生成结果不一样,怎么做到复现

扩散模型的生成起点是随机噪声,而随机噪声的生成依赖随机种子。如果你想复现一张图,必须固定三点:随机种子、同样的采样器、同样的步数和引导系数。

这里有一个隐蔽的坑:即使你固定了seed,如果换了显卡型号,或者CUDA版本不同,某些底层原子操作的顺序可能变化,最终结果仍旧会有微小差异。跨机器完全复现是不可能的,这是一个需要接受的事实。我自己的做法是,在需要复现的实验里固定seed,并明确记录运行环境,而不是天真地以为seed就是万能钥匙。

还有一个容易被忽略的细节:VAE是有随机性的吗?默认情况下VAE解码是确定性的,但如果你没有固定seed,PyTorch的整体随机状态本身就在变,所以任何随机性都会影响结果。调试时建议先torch.manual_seed(0),把所有变量控制住。

6.4 采样器到底怎么选,别被选项绕晕

很多初学者面对采样器列表一头雾水,我来做个简化总结。决定采样器表现的两大因素是:数值求解器的精度和步长策略。前者决定每一步的近似质量,后者决定能不能用大步长减少总步数。

我的经验法则:想快速出图,选Euler或DPM++系列,20到25步画质已足够;追求极致稳定,选DDIM,但需要适当增加步数;做动画或批量生成时,优先选速度快、每步结果稳定的采样器,而不是画质极限最强的那个。

采样器之间不存在绝对的优劣,关键是和步数搭配。我在跑实验时经常做一个小对比:固定seed、提示词和引导系数,只换采样器,记下各自在10步、20步、30步下的结果,这样很快就能找到自己手头模型的最佳组合。这种消融对比的方法,比看任何测评文章都更能建立直观体感。

写在最后:一个值得坚持的学习习惯

这套四集精讲笔记梳理完之后,我自己最大的收获其实不是记住了某个公式,而是建立了一个习惯:任何生成结果异常时,先顺着推理链路排查一遍,而不是盲目换参数碰运气。

我特别推荐你也做一次“拆管线”练习,把高层调用换成手动流程,在每一步打印张量形状,遇到底层报错就顺着报错信息倒推是哪个组件出了问题。这个习惯帮助我很快适应了后续SDXL、多种ControlNet插件等新东西,因为底层原理始终没变:文本先编码成条件向量,UNet按时间步去噪,调度器控制节奏,VAE负责解码。

另外一个小技巧是,给每个关键概念准备一个“最小实验”。比如想看CFG的作用,就固定其他条件,只调guidance_scale,从1到15各生成一张图,观察变化趋势;想看交叉注意力机制到底融合了什么,就手动修改文本嵌入的某个token向量再生成。这类实验成本很低,但带来的理解深度远超单纯看文档和教程,也是我后来逐渐能自己改采样器逻辑、写自定义推理流程的基础。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进