Stable Diffusion原理拆解:CLIP、UNet与扩散模型实战指南
先声明一下,这篇笔记来得很不容易。过去一周我把手头关于Stable Diffusion的资料全部重新过了一遍,结合四集精讲视频的内容,把CLIP、扩散模型、UNet和文生图代码实战这条线彻底梳理了一遍。网上讲Stable Diffusion原理的文章不少,但大多数要么只给结论不给推导过程,要么甩一堆公式让人劝退。这篇笔记的目标很明确:让一个会用Stable Diffusion出图、但不理解背后机制的人,读完能真正明白"提示词为什么能控制画面""为什么需要那么多步去噪""UNet在里面到底干了什么活",并且能动手写代码跑通一次完整的文生图流程。
如果你平时只用WebUI或者ComfyUI出图,从来没想过提示词是怎么"进入"模型的,也不知道所谓的"模型"其实不是一个整体,那这篇笔记非常适合你。如果你正准备学diffusers库或者想深入理解ControlNet、LoRA的作用原理,这篇笔记会给你打下很扎实的地基。
1. 先看全局:Stable Diffusion其实是"三驾马车"在协同工作
1.1 为什么必须拆开看CLIP、UNet和扩散调度器
很多人第一次接触Stable Diffusion的时候,以为它是一个"端到端的大模型":输入一句话,输出一张图。但实际拆开看,它是一条流水线,由若干个独立组件协同完成。就好比一家餐厅,端上来一道菜,背后有采购、切配、掌勺、摆盘不同岗位的人各司其职,你不能说"这道菜是某一个人做出来的"。
Stable Diffusion最核心的三个组件是CLIP文本编码器、UNet图像生成器、VAE图像解码器,外加一个调度器(scheduler)来控制每一步去噪的策略。这三个组件分开训练、各管一段,使用的时候再拼装成完整的pipeline。
把架构拆开理解,最大的好处是出了问题你能判断是哪个环节的锅。我见过太多人在社区里提问:"提示词明明写了red dress,为什么出图是蓝色裙子?"这种问题如果理解架构,第一反应就应该是去检查CLIP对文本的理解方式,而不是怀疑显卡坏了。反过来,如果生成的图像有雾感、结构崩坏、肢体畸形,问题多半出在UNet与调度器的配合上,而不是提示词写错了。还有一类问题——画面发灰、颜色暗淡,这种时候大概率是VAE没选对。换VAE改善色彩这个社区里流传的操作,背后其实就在调整解码器的行为。
1.2 VAE在流程中承担的角色
这里单独把VAE拎出来说,是因为很多讲原理的文章把它一笔带过,但它在Stable Diffusion的技术栈里非常关键。
Stable Diffusion的全名其实是Latent Diffusion Model,也就是潜在扩散模型。"潜在"二字指的就是它不在原始像素空间干活,而是在一个压缩后的潜在空间里干活。VAE(Variational Autoencoder,变分自编码器)负责这种压缩与还原。
具体来说,一张512x512x3的RGB图像,直接让模型去处理的话,计算量非常大——光是单张图就有近80万个像素值。VAE的编码器会把这张图压缩成一个64x64x4的潜在张量,相当于把图像的大致信息装进一个体积只有原来约1/48的"压缩包"里。扩散过程在这个压缩包里进行,等到去噪接近完成,再由VAE的解码器把这个潜在张量还原成512x512x3的像素图像。
理解这个机制之后,很多操作就说得通了。比如社区里流传"换VAE让画面更鲜艳",本质上就是换了一个解码器。不同VAE解码器在某些颜色分布上有不同的倾向,有的善于还原高饱和色彩,有的倾向于清淡柔和。再比如为什么Stable Diffusion能在消费级显卡上跑起来?因为整个扩散过程是在64x64的小分辨率上完成的,而不是在512x512上完成,计算量天差地别。
1.3 一次文生图的完整数据流
把这条流水线从头到尾走一遍,大概是这样:
提示词 → CLIP文本编码器 → 文本条件向量(conditioning embedding) 随机噪声 → 初始化潜在张量(latent) 循环:UNet基于当前潜在张量和文本条件预测噪声 → 调度器更新潜在张量 → 重复N步 去噪完成 → VAE解码器 → 输出RGB图像这个流程里最容易被忽略的是随机噪声的初始化。你每次生成不同的图,本质上是随机噪声的起点不同;你设置随机种子,本质上是锁定了这个噪声起点,让整个过程可复现。生成同一张图的"变体"与"重roll",差别就在于噪声起点是否改变。
CLIP提供的文本条件是贯穿整个去噪循环的。它会以交叉注意力的形式,在每一步都影响UNet对噪声的预测方向。所以提示词不是"开头用一下就没了",它会持续引导整个去噪过程的走向。
提示:如果你用的是ComfyUI,它的节点式界面其实就是这条数据流的可视化。CLIP Text Encode节点负责文本处理,KSampler节点内部就是"UNet+调度器"的循环,VAE Decode节点负责还原图像。与其说ComfyUI难用,不如说它逼着你理解这套架构。
2. CLIP不只是"文本编码器":条件注入才是关键
2.1 CLIP的文本侧到底在做什么
CLIP全称是Contrastive Language-Image Pre-training,是OpenAI发布的对比学习模型。它的训练思路很直接:把图片和文字成对喂进去,让匹配的图文对在向量空间里靠近,不匹配的远离。训练完成后,CLIP就得到了两个编码器:一个把图像映射成向量,一个把文本映射成向量,而且这两个向量在同一个语义空间里可以互相比较。
在Stable Diffusion里,我们只用它文本编码器的那一半,也就是把提示词变成一组向量。这里有个常见的误解:以为CLIP输出的是一个"整体语义向量",用来表示这句话的总体意思。实际上,CLIP输出的是一组token级的向量序列。
打个比方:一句话进来,CLIP先把句子切成token,比如"a red dress on a girl"被切成若干个token,然后每个token经过Transformer层后都对应一个向量。模型拿到的不是"这句话的意思浓缩成一个向量",而是"每个词在上下文里的语义表示"。这一点非常重要——因为UNet做CrossAttention的时候,需要让图像的每个区域有机会与文本序列里的特定token建立联系。如果CLIP只给一个整体语义向量,那"红色的裙子"和"红色的苹果"在文本向量上没有区别,因为整体语义被压平了。
这也解释了为什么社区里老说提示词顺序很重要。CLIP对句子的处理不是简单的"词袋",它会让前面token的语义表征受后面token的影响,但靠前的token通常在整个序列里承担着更基础的语义锚点作用。把主体词放在前面,CLIP对它的表征会更加突出,CrossAttention分配给它的注意力权重更容易占上风。
2.2 "CLIP limit"导致的截断问题
CLIP上下文长度是有限的,这在原理上有个小知识点:SD 1.5时代的文本编码器最长支持75个token,SDXL扩充到了77个。很多人提示词写了一大段英文,写到后面发现完全不起作用,其实就是后面一段被截断了,模型根本没看到。
我第一次意识到这个问题,是因为一段提示词里前面写环境,后面写人物细节,结果出图人物面部一塌糊涂。后来把人物细节挪到前面,效果立刻改善。原因就在于超出CLIP limit之后,靠后的部分会被截断或压缩,等于白写。
所以提示词的基本功,第一条就是把最重要的关键词放在前75个token内。你可以在理解CLIP机制后,慢慢积累一套自己的精简提示词模板,把风格词、主体词、细节词按优先级排序,而不是一股脑往里面堆。
2.3 CrossAttention注入:从提示词到图像特征
文本向量不能直接修改UNet里的图像特征,中间必须有个交互机制。这个机制就是CrossAttention(交叉注意力)。
简单理解:UNet在处理带噪图像特征时,会把图像特征的每个位置当作"查询者";CLIP产生的文本token向量序列充当"被查询的档案库"。图像特征根据自身的当前状态,计算出对每个文本token的注意力权重,然后把相关文本信息加权融合进自身。这样,图像特征每一个位置都"读取"了自己最需要的文本信息。
举个具体的例子。去噪进行到中间步骤时,某个区域看起来像一团模糊的圆形,UNet不知道该把它细化成太阳、皮球还是柠檬。这时候CrossAttention上场:这个位置的图像特征会去文本序列里找有没有"sun""round""bright"这类词,找到之后把相关信息融合进来,下一步去噪就倾向于把这个圆形细化成太阳。
这整个"查询-匹配"过程会在每一步反复发生。所以提示词越具体,CLIP提供的token向量区分度越高,UNet在做判断时信息越充足。反过来,提示词写得模糊而空泛,CrossAttention找到的对应关系也模糊,画面自然就缺乏细节。
这还引出一个关于**负面提示词(negative prompt)**的深层理解。负面提示词被CLIP编码后,在采样时通过CFG(Classifer-Free Guidance)机制反向起作用:正向提示词负责"拉近"注意力,负面提示词负责"推开"注意力。比如你写"deformed hands, extra fingers",UNet在预测噪声时会同时参考正向和负向条件,向着远离负面特征的区域移动。所以负面提示词不是玄学,它本质上是给模型提供了一个"避让目标"。
2.4 实操里值得留意的CLIP细节
加权语法。像(keyword:1.2)这种写法,本质是把对应token的向量乘以一个权重系数,系数越大,CrossAttention里它的影响力越强。但注意,权重不是无限可加的。超过1.5之后,token对应的图像区域容易出现过饱和、颜色发灰,甚至直接崩坏。我平时把大部分权重控制在1.1到1.4之间,非必要时干脆不开权重,而是通过换措辞、加前置描述来达到强调效果。
选用不同的CLIP层输出。这个操作在ComfyUI里被叫做"CLIP询问机",因为你可以选择CLIP的最后一层输出或倒数第二层输出作为文本条件。不同层的语义粒度不同:最后一层更接近整体语义,倒数第二层往往保留更多的局部句法信息。SDXL时代有些模型对CLIP层的选择更敏感,实测中换一层输出,画风和文字遵循度都可能变化。
CLIP模型微调与替换。Stable Diffusion社区的模型生态里,有很多换用不同CLIP的尝试,比如用OpenCLIP或中文CLIP变体来增强对中文提示词的理解。换CLIP本质上是改变"文本→语义向量"的映射函数。如果你在生成特定语言或特定风格内容时总觉得提示词"带不动",可以考虑是不是CLIP和当前UNet的搭配不理想,尝试替换一个更强或更适配的CLIP变体。
3. 扩散模型的核心矛盾:前向加噪与反向去噪如何练成
3.1 前向过程的本质
Diffusion Model的核心思想可以一句话概括:先把一张清晰图像逐步加入高斯噪声,一直加到变成纯随机噪声;然后训练一个网络学会"倒放"这个加噪过程,从纯噪声逐步还原出清晰图像。这个过程里包含一个前向过程和一个反向过程。
前向过程用的是噪声调度表(noise schedule)。它规定了每一步加多少噪声:前期加得快、后期加得慢,按一个预定义函数衰减。常用的是线性调度和cosine调度,两者区别在于让噪声强度随步数变化的曲线形状不同。SD的调度器(scheduler)负责在推理时决定在哪一步用多强的噪声预测更新潜在张量。
为什么特意提到噪声调度?因为训练时用的调度和推理时用的调度不一定一致,但大家都希望它们越接近越好。调度器不同,即使同一个UNet权重,出图风格也有差异。这也是为什么在WebUI里换scheduler,图像会出现明显变化的内层原因。
从训练数据角度看,前向过程的作用是提供"中间样本"。训练时,给定一张几乎干净的训练图像,我们随机抽取一个时间步t,按照该步预设的噪声强度,往图像上添加对应幅度的噪声,得到一张带噪图。然后告诉模型:"这是一张在t时刻加噪后的图,请告诉我我加进去的噪声长什么样。"
好,关键来了。模型的目标是预测噪声本身,而不是预测干净图像。为什么?
如果把目标设为从带噪图中预测干净图像,每个时间步t的"输入→输出"映射差异很大:一个加了重噪声的图,要还原的干净版本有海量可能性,回归目标太分散,模型很难收敛。改成预测噪声之后,优化目标就变成了一个很干净的L2损失回归问题——只需要让预测噪声逼近真实添加的噪声张量即可。这比逆推干净图像要简单得多,也稳定得多。
还有一个值得说的点:为什么扩散模型生成时不能一步到位,非要一步一步去噪?因为从纯噪声到清晰图像,这个映射实在太复杂了,网络学不动。把它拆成上千个微小的步骤,每一步只做少量修正,相当于把一个"高难度任务"分解成上千个"简单子任务"。每一步只需要回答"当前噪声里应该减去一小部分",网络要做的事就明确多了。
3.2 反向去噪的逐步过程
生成阶段,我们手里只有一个训练好的噪声预测网络(UNet)。没有"反向去噪网络",只有"噪声预测网络",所以反向去噪的具体操作是:从一个纯噪声张量出发,在每一步让UNet基于当前带噪图和当前步数t预测噪声;然后从当前潜在张量中减去一部分预测噪声,再根据调度器的规则增加一点随机扰动,得到下一步更干净的张量。如此反复执行预定的步数,最终得到一张去噪完成的潜在图。
这里有一个很经典的直觉图:去噪过程就像一个雕塑家在大块石头上逐步雕刻。一开始面对的是杂乱无章的噪声石块,每步去噪就像是削掉一些多余的部分,让大形先出来;中间步数里,人物的轮廓、场景布局逐渐显现;最后几步里,细节、纹理甚至光照才确定下来。如果你只从中期开始看生成的中间变量,会发现早期的形状很模糊,晚期才逐步锐利。
这个一步步微调的特性也决定了为什么步数不能太少。步数太少,相当于每一步削掉的噪声幅度太大,细节还没成型就匆匆定稿,容易出现结构性问题。步数太多,每一步的修正幅度越来越小,收益递减,而耗时线性增长。DDIM这类采样器可以在20步左右出图,是因为它走了一套更高效的离散化路径;DPM++在高步数下能得到更干净的细节保留。我自己常用的是:探索阶段20到25步,最终出图30到40步,超过40步感觉得到的收益就不明显了。
3.3 训练目标与生成目标的联系
训练目标和生成目标之间有一个巧妙的衔接:训练时模型学习的是"在任意时间步t下,给定带噪输入,预测当前添加的噪声张量"。生成时我们从t=最大噪声步开始,每一步用UNet预测噪声,进而还原出t-1时刻的更干净输入。因为训练数据覆盖了从几乎干净到完全噪声的所有状态,所以模型在生成时从纯噪声起步也不会慌。
这里顺带说清楚一个概念,叫潜在扩散模型(Latent Diffusion Model)。扩散过程目标网络增加了一层VAE:不直接在像素上做加噪和去噪,而是在VAE压缩后的潜在空间里做。为什么这个设计很重要?因为像素空间的计算成本太高,且加噪后的图像往往包含大量人眼本来看不到的高频冗余信息;压缩到潜空间后,计算量大幅下降,扩散过程也更容易专注于"语义层面的去噪"而非"像素层面的去噪"。Stable Diffusion之所以能高效训练、能跑在日常显卡上,关键就是这一层压缩。
训练扩散模型本身也有工程细节。数据集的图像会被先经过CLIP预处理吗?不,CLIP只服务文本侧,图像数据走的是VAE编码器。每个batch里随机采样不同的时间步t,让模型在同一batch里看到不同噪声强度的样本,这样训练效率更高。损失函数就是预测噪声和真实噪声之间的MSE(均方误差)。看似简单,但这个目标配合大规模数据和足够的训练步数,可以训练出非常强的生成能力。
LoRA微调之所以有效,正是因为它在这个"噪声预测"目标上做了局部干预。LoRA冻结了UNet的绝大部分参数,只微调一小部分低秩矩阵,效果是让模型在指定风格或指定物体上做出更精准的噪声预测。比如手部LoRA,本质上是让模型遇到"手势复杂的带噪图像"时,预测出的去噪方向更倾向于恢复成结构正常的手型,而不是糊成一团。
4. UNet在生成过程中的真实工作方式
4.1 U型结构设计逻辑
UNet(U-Net)是一种最初用于医学图像分割的对称编码器-解码器结构,因形似字母U而得名。在Stable Diffusion里,它的任务是庞大的:接收带噪声的潜在张量,预测每个位置的噪声。这个任务既需要理解全局结构,又需要保留局部细节,U型结构恰好兼顾这两点。
UNet具体分三部分:
- 左边的下采样路径(编码器):把特征图分辨率一步步降低,通道数一步步增加。下采样层数和倍数决定了UNet能捕捉多宏观的上下文。这部分网络学会的是"看懂图像在表达什么",提炼出高层语义特征。
- 中间的瓶颈层:最深的特征表示,此时分辨率最小、通道数最高。瓶颈层负责处理全局级别的信息。
- 右边的上采样路径(解码器):把特征图一步步放大回原始输入分辨率。这部分负责把抽象的语义信息还原成具体的结构细节。
这还不是关键,真正的关键是跳跃连接(skip connections)。下采样加深语义的同时不可避免地丢掉细节,为了不让细节信息丢失,UNet把左边每一层下采样前的特征图直接接到右边对应上采样层之后的位置,让解码路径在恢复分辨率时可以同时参考"高层语义"和"浅层细节"。用大白话说,跳跃连接像一个备忘录,让生成器在努力恢复分辨率的同时,不会忘记输入图像原本的纹理细节。
在Stable Diffusion里,UNet的输入是VAE压缩后的潜在张量,尺寸通常是64x64x4。很多人会惊讶:原来它从来没有直接处理512x512的图像?对,它处理的是远小得多的64x64潜在空间表示。在这个小分辨率上完成所有扩散步骤,这也是为什么Stable Diffusion能够相对高效地在消费级显卡上推理。
4.2 Attention模块嵌入的位置与原因
纯卷积结构有个短板:局部感受野有限。卷积核一次只能看图像中间一个小区域,虽然有下采样可以扩大感受野,但要建立"左上方有一个太阳,右下方的地面应该被照得亮一些"这种远距离依赖,纯卷积结构是不擅长的。生成任务恰恰特别依赖全局协调——构图、光影、多个物体之间的关系,都需要跨区域的语义协同。
所以Stable Diffusion的UNet在下采样和上采样的多个阶段中,嵌入了Attention(注意力)模块,包括Self-Attention和CrossAttention两类。
Self-Attention是图像特征内部的自我查询。每个位置的图像特征会去查询其他位置的图像特征,计算它们之间的关联权重。它的作用是让图像区域之间互相"通气"。比如模型在某块区域判断"这里是一只手",那么相邻的肤色、指甲、手腕区域都可以参考这个判断来生成,手部的整体协调性就会变好。扩散模型为什么能生成整体结构协调的图像,很大程度靠Self-Attention的全局建模能力。
CrossAttention则是图像特征与文本特征之间的交互。图像位置作为查询方,从CLIP文本token序列里查找到自己最需要的文本信息。这决定了画面内容如何与提示词对齐。两者在UNet里交替工作:先用CrossAttention吸收文本条件,再用Self-Attention协调各区域之间的关系,逐步逼近最终画面。
这些Attention模块之所以安排在多个层级,是因为不同层级的特征分辨率不同,需要的语义粒度也不同。浅层特征对应的图像区域比较局部,深层特征比较整体。所以在浅层加Attention和深层加Attention带来的效果是不同的——有人尝试调整注意力比例以获得特殊风格,原理就在这里。
4.3 和真实出图质量直接相关的UNet细节
出图质量最直观的影响因素,就是UNet的参数规模与结构设计。SD 1.5的UNet参数量约860M,SDXL的UNet接近2.6B,参数多了两倍以上,而且新增了更多Attention头。参数量的提升让模型有了更强的细节建模能力,生成的图在纹理复杂度、光影写实度上都有明显提升。代价就是显存和推理时间上涨。市面上的"大模型"之争,某种意义上就是不同规模、不同设计的UNet生态之争。
实际使用里还有一个概念叫"UNet层输出"。像ControlNet这类结构控制工具,它的做法是旁路一个可训练的网络,把输入图像的条件(比如canny边缘、深度图、姿态骨架)提取出控制特征,注入到UNet的特定层的Attention模块附近。ControlNet注入在不同层位,产生的控制效果不同。如果发现在某一任务下"控制力不够"或"控制过度",可以试试调整注入层位和权重。
最后聊聊UNet使用时的实际注意事项。
- 先搞清楚显存瓶颈在哪里。很多人为了省显存,上来就把UNet切到fp16甚至int8,结果出图发灰、残缺、细节丢失。其实对消费级显卡来说,优先开启Attention优化(xformers或sdp)和VAE tiling,往往就能把显存压下来,完全不需要动UNet精度。
- 再就是设备分配。如果UNet在GPU而VAE在CPU,出图速度会非常慢。这种问题网上每天都有新手问,本质就是图里没有把组件装在同一设备上。
- 注意Hugging Face生态里不同版本的UNet权重会有差异。有些第三方模型虽然名字相似,但UNet的Attention头数、层数并不完全一样,如果你使用自定义pipeline代码,要确保入参和模型结构匹配,否则会直接报错或者生成怪异结果。
5. 文生图代码实战:跑通Diffusers管线并解决手部崩坏
5.1 最小可运行代码与关键参数解释
原理聊完,还是得上手跑一次。我用Hugging Face的diffusers库演示最简单的文生图pipeline,这是目前社区最主流的Stable Diffusion推理库。
from diffusers import StableDiffusionPipeline import torch pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, variant="fp16", ) pipe = pipe.to("cuda") prompt = "a girl playing guitar on the beach, detailed hands, masterpiece" negative_prompt = "bad hands, deformed fingers, extra fingers, blurry, low quality" image = pipe( prompt=prompt, negative_prompt=negative_prompt, height=512, width=512, num_inference_steps=30, guidance_scale=7.5, ).images[0] image.save("output.png")这段代码会做这些事:
- 加载SD 1.5的完整pipeline(包含CLIP、UNet、VAE和默认调度器)。
- CLIP把prompt和negative_prompt编码成文本条件。
- 初始化随机潜在张量。
- 进入去噪循环,UNet预测噪声,调度器更新潜在张量,重复30步。
- VAE解码器把去噪完成的潜在张量还原成512x512 RGB图。
- 保存结果。
几个参数值得解释:
guidance_scale=7.5:这是CFG的强度。生成时除了计算"有文本条件"的噪声预测,还会计算"无文本条件"的噪声预测,然后把两者差放大7.5倍再叠加。数值太低,图像和提示词关联弱;数值太高,颜色过饱和、出现塑料质感。我个人在5到9之间尝试,细节丰富的图我会降到6左右。num_inference_steps=30:去噪循环步数。对着SD 1.5和默认调度器,25到30步已经很稳定。如果换Euler或者DPM++,步数可以进一步调整,具体看调度器表现。height=512, width=512:SD 1.5在512分辨率训练,直接提高输出分辨率很容易出现重复构图、结构崩坏。想要大图,正确做法是先生成512图,再用图生图或超分模型放大。negative_prompt:为CFG提供反向引导,让UNet在去噪过程中避开"坏手、多余手指、模糊、低质量"这些特征。
跑通这段代码,意味着你已经亲手执行了一遍完整的"CLIP + 扩散模型 + UNet + VAE"pipeline。之后无论是调参还是换模型,思路都清晰很多。
5.2 手部修复三招:画骨、修图、模型微调
AI绘画最经典的老大难问题就是手部结构崩坏:手指多一根、少一根、弯曲成诡异角度、指节粘连。网传"Stable Diffusion仅用三招就能搞定手部修复",其实归纳下来确实就是这三招。
第一招:画骨,用ControlNet锁定手部姿态。
先准备一张参考图或直接指定手部姿态,用OpenPose或Depth这些预处理器提取手部骨骼点,然后通过ControlNet把骨骼图作为控制条件注入UNet。这样去噪过程中,UNet生成手部区域时就有了骨骼约束,手指的数量、弯曲方向会被强制对齐到骨骼点上。生成出来的手即便不是特别精细,至少结构不会崩得太离谱。
这招的注意点是:ControlNet对手部骨骼的检测精度很敏感,如果原图本身手指就重叠模糊,提取出来的骨骼点也不准,反而会把错误结构固化。所以画骨的前提是有一张清晰可靠的手部参考。
第二招:修图,用局部重绘补救。
如果第一招没拦住,手还是崩了,第二招是针对局部区域的后处理。把出图放进图生图,配合mask圈出手部区域,输入正向词"hand, five fingers, correct anatomy"和负面词"deformed fingers, extra fingers",用较小的重绘幅度重新生成这个区域。重绘幅度(denoising strength)建议控制在0.3到0.45之间。太低了修的幅度不够,手指细节改不动;太高了会把整个画面风格带跑,甚至改变肤色光影。
这招的好处是不需要重新生成整张图,节省时间,适合出图后挽救。缺点是mask圈定的区域如果太小,模型容易重复生成类似的结构问题,因为UNet的整体语义已经习惯了那个区域"就该那么长"。
第三招:模型微调,训练手部LoRA。
如果你经常画人物,手部频繁崩坏,那最根治的方法是训练一个手部LoRA。原理前面说过:LoRA是一组低秩矩阵权重,冻结UNet大部分参数,训练时会"教会"UNet在面对手部区域时,更倾向于预测出结构合理的去噪方向。核心在于数据:几百张经过标注的手部图片即可,但数据质量很重要——遮挡严重、手指重叠过多、风格太单一的样本,反而会带偏模型。
三招怎么选?出图前预防用第一招,出图后挽救用第二招,天花板受限用第三招。我自己的习惯是:日常出图先用第三招打好底子,遇到特别想要的姿态再加第一招做控制,出图后偶尔用第二招救急。三种手段配合,手部崩坏的频率能压得很低。
5.3 显存不足、步数与CFG的调整经验
跑代码时最常见的问题就是显存不足(OOM)。我按顺序推荐这些优化方案,从影响最小到影响最大:
- 开启Attention优化。在diffusers里可以调用
pipe.enable_xformers_memory_efficient_attention()或使用sdp加速,这类优化能在几乎不损失画质的情况下减少显存占用。 - 开启VAE tiling。
pipe.enable_vae_tiling()让VAE解码时分块进行,大幅降低解码阶段的峰值显存。 - 降低输出分辨率。把512x512改成448x512之类,显存立刻下降。
- 切换成fp16甚至int8,或者开启CPU offload。但要注意,int8不一定能无缝兼容所有自定义节点和采样器,会带来额外的工程问题,不建议新手一上来就做。
最后聊聊步数与CFG。扩散模型生成时可以看成一条"去噪路径"。步数和CFG的关系大致是:步数决定路径的细致程度,CFG决定路径是否贴合文本条件内容。CFG太低,路径会更自由但内容漂移;CFG太高,路径被文本条件拉得过紧,色彩和质感会发腻。两者都受模型本身的影响,换一个底模后,原来调好的参数往往需要重调。
给出一个比较保险的起手经验:SD 1.5配Euler或DPM++的SDE采样器,30步、CFG 7.5;SDXL配DPM++ 2M,30步、CFG 5.5。在此基础上微调。判断方法很简单:出图太"素"、元素对不上,说明CFG低,往8到9加;出图太"油"、颜色发腻,说明CFG高,往5到6降。这些没有绝对正确,更多的还是要按当前图片的表现做判断。
最后提醒:跑本地推理时,注意UNet与VAE是否在同一设备上。如果你只把UNet放GPU而VAE还在CPU,出图速度会非常慢。这个坑虽然简单,但社区里一半的"速度异常慢"问题都是这种设备分配不当引起的。
6. 这条主线之外,你还需要在脑子里留下的几个结论
从CLIP怎么把文字变成条件向量,到扩散模型怎么一步步把噪声"雕刻"成图像,再到UNet内部如何处理细节与语义,最后到代码里每个参数的含义——这条线走完,Stable Diffusion在你眼里就不只是一个"出图工具",而是一套逻辑闭环的生成系统。
我总结的最重要一条经验是:不要上来就微调参数,先理解它为什么会起作用。比如CFG调大,你要知道这意味着文本条件更强的"拉力",随之而来的不是简单的"更听话",还可能是色彩过饱和、画面发腻。再比如换采样器,你要明白它改的是噪声去除的"路线",所以在不同采样器下,同样步数的结果差别会很大。理解了机制之后,调参就不是碰运气,而是尝试时心里有数。
第二,学会通过中间变量排查问题。如果你能拿到去噪过程中的中间潜在张量,观察它是从什么阶段开始跑偏的,就能定位是早期构图出了问题,还是后期细节出了问题。官方没有提供一个很方便的界面来做这件事,但社区里的许多工具,比如ComfyUI的采样器和Latent预览节点,能让你直观地看到去噪过程中的中间蛋糕。以后遇到奇怪的结果,用这个方式排查会比瞎猜高效得多。
第三,扩散模型的"可解释性"本身就有限,不用把所有希望寄托在完全可控上。当前Stable Diffusion仍然存在很多不可控之处,比如手部难点、复杂物体纠缠、文字嵌入错误。了解了原理,会让你对这些限制有个合理的预期,而不是因为它不能完美执行你想要的内容而沮丧。工具的边界本身也是使用工具的一部分。
这个内容后续还可以这样扩展:沿着CLIP这条线深入,可以研究更多文本条件注入方式,比如双向注意力、更长的上下文;沿着UNet这条线,可以探索结构改进,比如SDXL、SD3、Flux中采用的类似DiT的架构替换UNet骨干;沿着数据集和训练目标这条线,可以研究Classifier-free guidance的变体、动态CFG、对抗式扩散蒸馏等进阶技巧。原理永远是那块最坚实的地基。