☰
Stable Diffusion 文生图原理精讲:从CLIP文本编码到UNet去噪完整链路
2026/10/26 6:18:56 网站建设 项目流程

我前段时间把 Stable Diffusion 的文生图原理完整过了一遍,从 CLIP 文本编码、扩散模型的正反向过程、UNet 去噪网络,一直到跑通最小文生图代码,做了四集精讲笔记。今天把笔记重新整理出来,希望能帮正在看原理但还没上手的读者打通这条链路。

这套笔记主要适合两类人:一类是已经在用 WebUI 或 ComfyUI 出图、但总感觉在“调参调个寂寞”的玩家;另一类是刚接触生成模型、想搞懂 Stable Diffusion 底层到底怎么工作的开发者。我不会堆一堆公式,而是尽量用可理解的方式把每个模块讲明白,再配上一段能直接复现的代码。看完之后,同一张提示词图,你能知道它为什么长这样,也知道往哪个方向调。

1. 先搭一张全局流程图:从提示词到画面的完整流水线

很多人学 Stable Diffusion 容易一头扎进某个细节里,比如研究采样器,或者纠结某个 LoRA 权重。我的建议是:先别管那些,先把整条文生图流水线装进脑子里。你要知道,你输入一段提示词之后,程序到底在做哪些事。

简单说,整条链路是这样的:提示词先经过一个文本编码器,把自然语言变成一组向量;这组向量作为条件,引导一个名叫 UNet 的去噪网络,在所谓“潜在空间”里,从一个满是噪声的矩阵开始,逐步还原出一个图像矩阵;最后再由一个解码器把矩阵变成你能看到的像素图。

在 Stable Diffusion 里,这个文本编码器用的就是 CLIP 的文本分支。它负责把“a cat sitting on a sofa”这种话转成模型能理解的数字表示。UNet 则是整个扩散过程的核心引擎,它在每一个去噪步骤里接收带噪声的图像和文本条件,预测当前噪声,然后剥掉一部分噪声,反复几十步后得到干净图像。最后一环是 VAE 解码器,它把压缩过的图像潜变量放大还原成 512×512 或 1024×1024 的真实图片。

1.1 三次关键形态转换

整条流程可以看成三次形态转换。第一次转换,是文字变成向量。CLIP 把提示词拆成 token,再映射成一个 token 序列,这相当于把人类的语言“翻译”成了模型能读的数字语言。第二次转换,是随机噪声变成图像潜变量。你在生成时一开始会拿到一张纯随机的高斯噪声图,它在潜在空间里是一个低分辨率的小矩阵,经过 UNet 反复去噪,这个小矩阵逐渐从雪花点变成一张有语义内容的“压缩草图”。第三次转换,是潜变量变成像素图像。VAE 解码器把这张 64×64 的低分辨率草图放大到 512×512 甚至更高,补上颜色、纹理、光影细节。

我当初最大的误区就是以为 Stable Diffusion 是在 512×512 的像素图上直接做扩散。其实不是。它先把训练图压缩到潜在空间,再在压缩后的矩阵上做扩散,这样计算量小非常多,也正因此普通显卡才有机会跑起来。

1.2 四集笔记为什么要按这个顺序讲

我这套四集笔记的顺序不是随手定的。第一集讲 CLIP,是因为它是整条链路的入口,提示词的所有规则、截断问题、风格偏向,都是在这一步产生的影响。如果不先搞懂 CLIP,你会发现很多提示词技巧听起来像玄学:为什么关键词放前面更有效?为什么长提示词容易被忽略?这些都能在 CLIP 的 token 限制里找到解释。

第二集讲扩散模型,因为它回答了最基本的问题:为什么从一张噪声图出发,最终能得到一张有意义的图像。前向加噪、反向去噪、采样器、CFG,这些概念都源于扩散模型本身。第三集讲 UNet,其实是把扩散模型里的“去噪主体”单独拎出来解剖,看它怎么接收噪声图、怎么接收时间步、怎么接收文本条件。最后一集才是代码实战,因为当你理解了前面的原理,代码里的每个参数就不再是魔法数字,而是你能预判结果的操作。

1.3 “潜在空间”这个词为什么绕不开

只要学 Stable Diffusion,你一定会看到“潜在扩散模型(Latent Diffusion Model)”这个说法。Stable Diffusion 本质上就是潜在扩散模型,这里的“潜在”指的不是空气里的潜在,而是把图像压缩到低维空间里操作。

你可以把 VAE 编码器想象成一个压缩算法:一张 512×512×3 的图片,经过编码器变成 64×64×4 的潜变量。我们不去像素空间里直接扩散,而是在这个 64×64 的小矩阵上做去噪。你输入提示词之后,扩散模型实际上是在这一小块压缩地图上施工。之所以能这么干,是因为图像的语义信息在低维空间里保留得足够好,同时计算量大幅下降。这也是为什么早期纯像素扩散模型很难普及,而 Stable Diffusion 能用普通消费级显卡生成高分辨率图片。

2. 第一集精讲:CLIP 文本编码器如何把提示词变成特征向量

CLIP 是 OpenAI 提出的一个多模态预训练模型,Stable Diffusion 用的是它的文本编码器部分。这一部分看似简单,其实藏着很多实际入门的坑。你写提示词时遇见的“失效”“权重漂移”“前后不搭”,十有八九都出在 CLIP 身上。

2.1 CLIP 在 Stable Diffusion 里的真实定位:只用文本分支

完整 CLIP 包含一个文本编码器和一个图像编码器,训练时用海量图文对做对比学习,让匹配的文本和图片在向量空间里靠近。但 Stable Diffusion 在推理时并不需要 CLIP 来识别图片,它只把文本编码器拿出来,将提示词转成特征向量。这一点必须先明确:你在 WebUI 里看到“CLIP skip”,调的就是文本编码器的层数;你提到“clip模型应用”,它其实还有很多别的用途,比如零样本分类和图文检索,但在生成模型里它就是一支文本翻译笔。

为什么偏偏用 CLIP,而不是常见的 BERT?因为 Stable Diffusion 需要的条件向量最终要和图像特征对齐。CLIP 在图文配对数据上做对比学习,它输出的文本表示天然贴近图像语义表示。你在提示词里写“a red car”,CLIP 的向量会更靠近实际红车图片的向量空间;而 BERT 更擅长处理通用语言任务,和图像空间的直接对齐性偏弱。这也是为什么后来很多融合模型仍然把 CLIP 作为默认文本编码器。

2.2 77 个 token 的上限:为什么你的提示词后半段经常失效

这是我在笔记里单独标红的地方。标准 Stable Diffusion 1.x 的 CLIP 文本编码器,一次最多处理 77 个 token。如果你输入的提示词超过了这个长度,超出部分会被直接截断,而不是被“理解”后再放进去。很多人写了一大长串英文描述,生成出来的图却只体现了前半段的含义,就是因为后半段在进入网络前就被扔掉了。

实际写提示词时,核心概念一定要放在前面,并用逗号做语义切分。比如你需要“光影、构图、主体、风格”四类因素,尽量把主体和风格放在头部,修饰性内容往后放。另一种常用技巧是拆成多个提示词段,配合一些扩展节点来突破 77 token 的限制,但最终每段编码结果还是要拼进文本序列里。

CLIP 还会做 padding,句子不足 77 个 token 时会用空白符补齐到固定长度。每个 token 会被映射成一个 768 维向量(SD1.x 的情况,SDXL 的文本编码器更大),所以其实你每次给模型的不是一句话,而是一张 77×768 的“文本特征表格”。你可以把这个表格打印出来观察,会发现相同语义的不同表达方式,在向量空间里位置会很接近。

2.3 CLIP 理解的是语义,不是语法

CLIP 是被图文对比信号“喂”出来的,它对词与词组合背后的语法逻辑理解得很浅。它不是真的读了句子,而是把整段话当成一组 token 的特征集合。因此提示词写法更接近“关键词堆叠”而非“完整句子”时,效果往往更好。

我见过不少新手把提示词写成“In the style of a highly detailed oil painting, a young woman with long flowing hair and bright blue eyes sits beside a window”。这个句子语法完全正确,但 CLIP 会把注意力分散到各个修饰词上,导致主体不够突出。更常用的是“masterpiece, oil painting, 1girl, long hair, blue eyes, sitting beside window”。这不是玄学,是 CLIP 对离散概念的响应方式更直接。

2.4 关于 CLIP 的几个常见误区

第一个误区:以为 CLIP 是识别画面内容的模型。实际上在 Stable Diffusion 流程里,它只负责编码文本,不负责看图像。第二个误区:以为换一个更强的新版 CLIP,画质就能提升。文本编码器影响的是语义理解上限,画风、细节和分辨率主要由 UNet 和 VAE 决定。第三个误区:以为长句一定比短句好。文本越长越容易被截断,也可能稀释关键概念。第四个误区:把“CLIP 询问机”这类调试工具当成必装组件。这类工具更像黑盒检查器,能让你看到某个词对应在哪个 token 附近,但调出满意的图终究要靠提示词结构和参数,不是靠一次编码查询就能解决问题。

3. 第二集精讲:扩散模型的前向加噪与反向去噪

扩散模型是整个 Stable Diffusion 的生成内核。如果你能理解它,采样器、步数、CFG 这些东西你自然而然就懂了。

3.1 先从一杯清水滴墨水说起:前向过程

想象你有一杯清水,你在里面滴一滴墨水,静置一会,墨水开始扩散,水变浅灰;再滴一滴,颜色更深;持续滴很多次之后,整杯水变成完全均匀的黑色,再也看不到原来的透明状态。前向加噪过程就是这个“逐步滴墨水”的过程,只不过加的不是墨水,而是高斯噪声。

数学上,从一张干净图像 x0 出发,经过 t 步加噪,得到带噪图像 x_t,可以直接用一个公式表达:x_t = √(α_t) x0 + √(1 - α_t) ε。这里的 ε 是从标准高斯分布里采样的噪声,α_t 是一个随时间 t 递减的系数,t 越大,噪声占比越高。之所以能用这个公式一步算出任意 t 时刻的结果,是因为每一步加噪都满足高斯噪声的叠加性质,不需要真的一步一步跑。

3.2 从雪花还原到照片:反向去噪的训练目标

前向过程是确定的,反向过程才是模型要学的核心。反向时要从一个几乎全是噪声的 x_T 出发,一步步还原出 x0。如果每一步你都知道前一步加的噪声长什么样,就能把它剥掉。所以训练时我们让 UNet 去预测噪声,目标函数非常简单,就是让模型预测出来的噪声 ε_θ(x_t, t) 和真实噪声 ε 尽可能接近。

在推理时,我们从纯噪声开始,让 UNet 预测出这一步的噪声,然后从当前图像里减掉预测的噪声,再加上一点随机扰动,重复若干次。这就是为什么生成过程能看到画面从“雪花点”逐渐变成轮廓、再到清晰细节。实际代码里每一步的具体更新公式会因为采样器的不同而变化,但底层的“预测噪声—减去噪声”逻辑是一致的。这也解释了为什么模型里往往输出的是噪声图而不是直接输出干净图:训练目标决定了它的角色。

3.3 采样器、步数与 CFG:实际生成里的三大调节旋钮

训练好的扩散模型固定之后,采样器就是“去噪方式”的选择。不同采样器对步数和随机性处理不同,导致同样的模型、同样的提示词,用 Euler、DDIM、DPM++ 出图风格会有差异。我常用的经验是:SD1.5 模型用 DPM++ 2M Karras,步数 20 到 30;SDXL 用 DPM++ 2M SDE Karras,步数 30 上下。步数太少会明显欠采样,画面偏糊;步数太多则边际收益很低,还拉长生成时间。

CFG(Classifier-Free Guidance)是另一个关键参数。它的思路是同时计算无条件噪声和带提示词条件的噪声,然后按 strength 拉大两者差距。CFG 越大,画面越贴提示词,但过高会过饱和、出现伪影。常见范围是 5 到 9。负面提示词可以理解成把“无条件方向”换成了“不希望的方向”,让模型同时避开某些内容。这就是为什么负面提示词能帮你防畸形、防模糊。

3.4 潜在扩散模型 LDM 解决了什么问题

最早期的扩散模型在像素空间上运行,直接对 256×256 的 RGB 图像加噪去噪,显存和计算开销都很大。LDM 的核心思路是先通过 VAE 编码器把图像压到 64×64×4 的潜变量,在潜变量上跑扩散。这里的“潜在”是把高维像素压缩成了低维语义特征,计算量降低一个数量级,同时保留足够信息。

Stable Diffusion 就是 LDM 的一个具体实现。所以你看很多教程里说“去噪空间是 64×64 而不是 512×512”,这不是说最终图像是 64×64,而是去噪过程发生在低维空间,最后由 VAE 解码器拉回高分辨率。理解了这点,你就知道为什么 VAE 对出图效果影响很大:它负责高维还原,如果 VAE 有问题,画面就会灰、糊、色彩不对。

4. 第三集精讲:UNet 凭什么胜任噪声预测器

扩散模型的反向过程需要一个网络来预测噪声。Stable Diffusion 选择的是 UNet,这个选择不是随意的,结构和任务之间高度匹配。

4.1 编码器、解码器和跳跃连接:U 型结构为什么适合图像到图像

UNet 最早来自医学图像分割,结构上是一个 U 字形:左侧编码器一路下采样,提取越来越抽象的语义特征;右侧解码器一路向上采样,把抽象特征逐步还原回原始分辨率;中间通过跳跃连接把编码器的同尺寸特征直接传给解码器。

为什么这种结构适合预测噪声?因为预测噪声本质上是一个图像到图像的任务:输入一张噪声图,输出一张噪声图,输入输出分辨率一样。UNet 的下采样路径可以放大感受野,让网络看到全局结构,知道自己现在处理的是人还是猫;上采样路径可以恢复细节,把边缘、纹理等局部信息补回来。跳跃连接的存在让浅层细节不会因为多轮下采样而丢失。这也是为什么 UNet 能同时抓住“这是一个人”的语义,和“这里手指应该有一根”的细节。

4.2 时间步和文本向量是怎么注入网络的

UNet 在推理时要接收两个额外信息:当前去噪时间步 t,以及提示词的条件向量。

时间步 t 先被编码成一个 embedding,类似 Transformer 里的位置编码,然后在各个 ResNet 块里作为偏置加进去。它的作用是告诉网络:“你现在看到的噪声图,是在第几步的状态,剩下的噪声还占多少比例。”不同时间步对应不同的噪声强度,网络需要据此调整去噪力度。文本向量则是通过交叉注意力注入:图像特征作为 Query,CLIP 输出的 token 序列作为 Key 和 Value。网络在去噪时每次都会“看”一眼提示词,决定当前区域该画什么。

你可以这样理解:时间步像是给画师的进度提醒,文本条件像是放在旁边的参考草图。没有时间步,同一张带噪图像在不同阶段会得到一样处理,这不对;没有文本条件,UNet 就只能生成随机图像。两者缺一不可。

4.3 从结构看手部修复难题:为什么手总画崩

有关“ai绘画手部修复难题”的讨论非常多,网上最常见的梗就是 Stable Diffusion 画手必翻车。从结构上看,手部画崩有几个原因。第一,手在整幅图像里占的面积通常很小,经过 UNet 下采样后,手部区域只剩很少的像素,细节信息自然不够。第二,手部关节非常复杂,手指之间相互遮挡和姿态变化空间极大,训练数据里的手部状态也往往标注噪声很大。第三,交叉注意力机制更容易把注意力分配给面积大、语义强的区域,比如脸、身体,手部容易被忽视。

我见过不少“三招搞定手部修复”的说法,核心大致是:负面提示词排除畸形手指;局部重绘只重画手部区域;ControlNet 用 OpenPose 或深度图锁定手部姿态。具体操作我会在第六节细讲,但先记住一点:手部难题不是单纯加几个提示词能彻底解决的,它是模型结构和数据分布的短板,只能靠工具链弥补。

4.4 不同版本 UNet 的差异有多大

SD1.5 的 UNet 参数规模在 860M 左右,输入输出是 64×64 的潜变量;SDXL 用了更大的 UNet,并额外集成一个文本编码器,支持原生 1024×1024 出图,结构上增加了更多 Transformer 块。你换模型时,其实很大程度上就是换 UNet 的权重,不同权重的 UNet 训练数据和迭代方式不同,画风、概念理解能力自然差异巨大。

使用 UNet 有个重要注意事项:它是去噪网络,不是图像分类器。不要在推理时指望它直接告诉你“这张图是什么”;它只负责在给定噪声图和条件时输出预测噪声。另外,不同底模型之间混用 UNet 权重很容易出现通道不匹配或语义错乱,所以下载模型时最好配套使用对应的 text encoder 和 VAE。

5. 第四集精讲:最小文生图代码实战

原理看得再多,不如亲手跑一段代码。这一集我会带你从零搭好环境,写一个最小可用的文生图脚本。你不需要 WebUI,只需要一个 Python 环境。

5.1 环境准备:diffusers 全家桶怎么装

我用的是 Hugging Face 的 diffusers 库,它把管线封装得相当干净,适合学习过程。建议 Python 3.10,有 NVIDIA 显卡且装好 CUDA 驱动。安装依赖时,在终端执行:

pip install --upgrade diffusers transformers accelerate torch Pillow

如果已经装过 torch,可以先检查一下版本是不是支持你的 CUDA。第一次运行脚本时会从模型仓库下载权重,SD1.5 的基础模型大概 4GB,所以要预留好磁盘空间。网络慢的话可以提前用模型下载工具把权重放到本地,之后直接把本地路径传给 from_pretrained。这一步和显卡驱动一样,属于环境问题,我建议先处理顺再说生成。

5.2 一段直接能跑的生成脚本

下面这段代码是完整的,复制保存成generate.py,然后运行,就能得到一张 512×512 的图:

import torch from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16 ) pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) pipe = pipe.to("cuda") prompt = "a beautiful painting of a girl with cat ears, soft lighting, highly detailed" negative_prompt = "ugly, blurry, low quality, bad hands" generator = torch.Generator("cuda").manual_seed(42) image = pipe( prompt=prompt, negative_prompt=negative_prompt, num_inference_steps=25, guidance_scale=7.5, generator=generator ).images[0] image.save("output.png")

这里我从StableDiffusionPipeline加载模型,并把默认调度器换成了 DPM++。torch_dtype=torch.float16能让显存占用减半,大部分显卡都建议开。negative_prompt传入文本,模型会把这些词当作“不希望出现的内容”来避开。num_inference_steps是去噪步数,25 是一个比较稳的中间值;guidance_scale是 CFG 强度,7.5 是我常用的默认值。生成结果是一张 PIL.Image 对象,save一下就能用。

5.3 显存不够怎么办:float16、切片注意力与 offload

如果你的显卡只有 6GB 显存,直接跑上面的脚本可能报 CUDA out of memory。先不要急着换卡,有几个办法依次尝试。

第一,确认已经用torch_dtype=torch.float16,这通常能省一半显存。第二,在pipe.to("cuda")之前加上pipe.enable_attention_slicing(),它会将注意力计算切成小块,显著减少峰值显存,代价是速度稍慢。第三,用pipe.enable_model_cpu_offload()替代pipe.to("cuda"),这个模式会自动把部分模型留在 CPU,用的时候再搬到 GPU,显存占用更低,但第一次生成会有额外延迟。

还不行就把分辨率降到 384×384 来测试,脚本里通过height和width参数控制。实在不够,社区里有“stable diffusion cpp”这类量化解方案,适合低配环境,但我个人还是建议先把标准 diffusers 流程跑通,再考虑这些进阶玩法。

5.4 固定随机种子与负面提示词:让结果可控

代码里generator = torch.Generator("cuda").manual_seed(42)的作用是固定随机种子。这样一来,同样提示词、同样参数,每次生成的结果都一致。这个能力很重要,因为很多调参实验必须控制变量:你先固定种子,改 one 个参数,才看得出是哪个变量导致变化。

负面提示词同样重要。它利用 CFG 机制把模型往反方向推,能帮你避开低质量、畸形的结果。但负面提示词不是越长越好,我曾经堆了一长串负面词,结果画面构图反而越来越僵硬。比较通用的是“ugly, blurry, low quality, bad anatomy, bad hands, extra fingers”。具体项目可以再结合题材调整。

6. 四集笔记之外的实战经验:手部修复与翻车排障

原理和代码都跑通之后,真正进入日常使用你会遇到各种玄学问题。这一节我把自己踩过的坑和常用解法整理出来,尤其是手部修复和常见翻车场景。

6.1 手部修复三招:提示词、局部重绘、姿态控制

先说第一招:提示词控制。在负面提示词里加“bad hands, extra fingers, fused fingers, deformed hands”,能明显降低手指畸形的概率。注意不要把所有负面词都堆在一起,每条用逗号分开,并且不要加入太抽象的词,CLIP 对这些短语义的反应最直接。

第二招是局部重绘。如果整张图只有手部崩了,你可以用图生图模式把手部区域圈出来,然后把 mask 区域内重新生成。关键是重绘步数不要太高,模糊度和原始图要接近,否则手部区域会跟周围风格不一致。WebUI 里直接用“局部重绘”上传图片,画一下 mask 就行;diffusers 里则对应StableDiffusionInpaintPipeline。

第三招是姿态控制。使用 ControlNet 的 OpenPose 模型,你可以提供一个手部骨架图,让 UNet 在去噪时被“强制”按这个姿态来画手。这个办法对复杂手势特别有效,代价是你要有一个像样的骨架图,而且 ControlNet 权重参数要调适度,太强会压制画风。三招可以组合使用:先加负面提示词降低概率,再用 ControlNet 锁定姿势,最后用局部重绘修复明显的残余瑕疵。

6.2 出图灰蒙蒙、噪点多:先查 VAE 和采样参数

如果你发现生成的图整体灰蒙蒙,或者颜色饱和度很低,大概率是 VAE 的问题。Stable Diffusion 1.x 默认自带的 VAE 在某些版本上色彩偏淡,社区常用的vae-ft-mse-840000会明显改善色彩。你可以把 VAE 文件下载到本地,然后在 WebUI 的设置里加载,或者写成StableDiffusionPipeline.from_pretrained(..., vae=...)。SDXL 则一般自带新 VAE,问题少一些。

如果画面噪点多、细节碎,先检查 CFG。CFG 太高会导致画面过饱和、不自然;CFG 太低则容易糊。另一个常见原因是采样步数太低,SD1.5 用 DPM++ 时尽量不低于 20 步。还有一个容易被忽略的点:负向提示词里如果写了太多“detailed texture, sharp focus”之类的正向词,会干扰模型,反而让细节变得杂乱。

6.3 显存溢出的排障顺序

很多人在生成中断时只会看到 CUDA out of memory,但原因可能不一样。我的排障顺序是:先看分辨率,512×512 已经能跑还溢出再说显存;再看是否开了 fp16;然后确认有没有开启切片注意力或 CPU offload;最后检查批量大小是不是大于 1。通常到这步问题就解决了。

如果是在 WebUI 里运行,还可以加启动参数--medvram或--lowvram,它们会把模型调度得更保守。但要记住,显存优化往往会降低速度,所以如果显存够用,尽量不要同时开太多优化。另一个容易忽视的问题是后台进程占显存,比如浏览器硬件加速、另一个正在运行的生成进程,都会挤占显存空间。

6.4 一点学习路线的个人建议

如果你正准备按四集笔记的顺序学习,我的建议是:先跑通第 5 节的代码,看到一张图真的从文字里生成出来,再回头消化第 2 到第 4 节的原理。原因很简单,没有经过实践反馈,很多概念很难真正记住。跑完代码后,你可以故意改参数、换提示词、换采样器,去观察每一步变化。

公式不需要全推,但建议把第 3 节提到的加噪公式和损失函数抄下来,自己手写一遍。写的时候你会意识到噪声预测和图像生成之间的直接关系。最后,每个失败案例都值得记录:图像崩溃、手部畸形、过曝、模糊,把这些样本和当时的提示词、参数、种子放一起,你很快就能积累出属于自己的“调参直觉”。

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

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

立即咨询