很多朋友在学习 Stable Diffusion 时,容易被一堆名词吓住:CLIP 是什么?UNet 为什么长得像 U 形?扩散模型又是怎么把一张图从纯噪声里“变”出来的?更别提网上那么多代码片段,抄下来却跑不通。这篇笔记是我在完整跟进四集精讲课程后,结合自己的复现踩坑经历整理出来的,尝试把原理和代码放在一起讲清楚,争取做到“看完能理解,照着能跑通”。
整个 Stable Diffusion 的链路其实可以拆成三个核心部件:CLIP 负责“听懂”你的文字,UNet 负责“决定”画面的结构细节,而扩散模型本身则是从噪声一步步“雕刻”出图像的过程。这三个部件配合调度器(Scheduler)做多步去噪,最终再由 VAE 解码器把压缩的特征还原成高清图片。下文我会从整体架构开始,逐一拆解每个模块的工作原理和关键代码,最后给出一份可以直接运行的文生图实战脚本,并附上我在实际调试中总结的常见问题和参数建议。
1. 整体架构:Stable Diffusion 究竟做了什么
1.1 三阶段 pipeline 的核心思路
Stable Diffusion 并不是直接在一个巨大的像素空间里做扩散,而是先在潜空间(Latent Space)里完成大部分计算,最后才解码回像素。这种设计直接降低了显存占用和计算量,也是它和早期扩散模型最大的区别。整个流程可以粗暴地分为三块:文本编码器把提示词变成条件向量,UNet 在潜空间里预测噪声并逐步去噪,VAE 解码器把潜变量还原成图片。
文本提示词 → CLIP 文本编码器 → 条件向量 text_emb 随机噪声 → VAE 编码器 → 潜变量 latents text_emb + latents → UNet 迭代去噪 → 干净潜变量 干净潜变量 → VAE 解码器 → 最终图像从上到下看,就像一条流水线。CLIP 相当于“翻译官”,把人的语言翻译成模型能理解的数学向量;UNet 是整个流程的“施工队”,在每一步去噪时参考文本条件,对噪声进行预测和剥离;VAE 则是“画师”,把压缩的潜变量放大还原为图片。理解这条链路,后面的代码就不会一头雾水。
1.2 为什么要在潜空间而不是像素空间计算
直接在高分辨率像素空间做扩散,最大的问题是计算量呈指数级膨胀。一张 512x512 的 RGB 图像有 78 万多个数值,要对这样的张量进行几十次前向推理,显存会瞬间爆掉。Stable Diffusion 的做法是先通过 VAE 的编码器把图像压缩到 64x64 的潜空间(通道数变为 4),相当于用 4 个通道的抽象特征表示整张图的信息。这个压缩比例让 UNet 的计算量降低了一个数量级,普通消费级显卡也能在可接受时间内出图。
这里其实藏着一个很关键的细节:潜空间并不是简单的下采样,而是通过自编码器学到的语义压缩。它保留了图像的纹理、结构、颜色等核心信息,只是丢弃了高频的、肉眼难以察觉的细节。所以最后一步 VAE 解码器的作用不仅仅是放大分辨率,而是基于压缩特征重建出完整的图像。理解了这一点,你就明白了为什么很多人调整“潜空间通道数”或者“VAE 版本”会对出图风格产生那么大的影响。
1.3 四集精讲的知识点分布与学习路线
四集精讲的安排非常合理,每集都聚焦一个模块:第一集讲 CLIP 文本编码与条件注入,第二集讲扩散模型的数学原理与调度器,第三集讲 UNet 结构与注意力机制,第四集则聚焦文生图的完整代码实战。我自己学习时的感受是,如果跳过前两集直接看代码,很多参数的含义根本搞不懂,比如为什么 guidance_scale 是 7.5,为什么总步数设成 20 而不是 100。建议按顺序看,代码部分尽量亲手敲一遍,不要只用现成的 WebUI 按钮。
提示:学原理时不要陷入数学公式的泥潭,关键是理解输入输出关系。CLIP 输入一段文字,输出一个矩阵;UNet 输入带噪潜变量和条件,输出预测噪声;调度器决定每步怎么更新。把这三个“输入-输出”关系刻在脑子里,后续看任何代码都会觉得眼熟。
2. CLIP 文本编码器:模型怎么听懂人话
2.1 从 token 到 embedding 的转化流程
CLIP(Contrastive Language-Image Pre-training)虽然名字里有“对比学习”,但在 Stable Diffusion 里它只承担文本编码的工作。流程本身分成 4 步:
- 分词器把句子按词元拆分,例如“a cute cat”可能拆成几个 token,每个 token 对应一个 ID。
- 每个 token ID 通过词嵌入表查到一个初始向量。
- 加上位置编码,让模型知道每个词在句子里的位置。
- Transformer 层做多层自注意力计算,输出一个上下文相关的向量序列。
最终我们拿到的 text_emb 形状通常是 [77, 768] 或者 [77, 1280],取决于用的是哪个 CLIP 版本。77 这个数字代表最大 token 长度,超过的部分会被截断,这也是为什么超长提示词往往不起作用的原因之一。
2.2 为什么 77 个 token 限制如此关键
CLIP 的文本编码器固定了最大序列长度 77,这直接决定了出图时提示词的信息量。很多用户会遇到“提示词写了一大段,但生成结果却不尽如人意”的情况,大概率就是关键信息被截断在了第 77 个 token 之外。实际测试中,重要的主体描述和风格词应该放在句子的前段,让 CLIP 在有限的窗口中优先编码这些核心信息。
如果你需要表达更多内容,有两个常见的处理思路:一是使用支持更长上下文的扩展版本,二是把提示词拆分成正面和负面两部分,让模型在加权机制下更关注你指定的内容。但归根结底,CLIP 的 77 token 限制是架构层面的硬约束,理解了这一点,你才能合理规划提示词的写法,而不是盲目堆砌关键词。
2.3 条件注入:cross-attention 的桥接作用
CLIP 输出的 text_emb 并不是直接“拼”到 UNet 里的,而是通过交叉注意力机制(Cross-Attention)注入到 UNet 的每一层。简单说,UNet 在处理图像特征时,会产生一组查询向量(Query),而 text_emb 提供键(Key)和值(Value)。通过注意力得分,模型可以动态决定“当前这块区域应该更关注文本中的哪个词”。
这个过程非常直观:比如你输入“一只戴帽子的猫”,UNet 在绘制帽子区域时,会对“帽子”这个词产生较高的注意力权重;在绘制脸部时,则更关注“猫”。这种动态匹配能力就是文生图模型能理解复杂语义的关键。想要直观感受这种机制,可以用注意力热力图工具查看生成过程中每个 token 对应哪些像素区域,非常有意思。
3. 扩散模型:从纯噪声到清晰图像的核心机制
3.1 前向过程:给图像逐步加噪
扩散模型的前向过程(Forward Process)不涉及任何神经网络,它只是一个固定的、逐步添加噪声的过程。给定一张真实图像 x0,我们按时间步 t 逐步向其中加入高斯噪声,得到越来越模糊的带噪图像 xt。公式上看,xt 是由 x0 和噪声 ε 按一定比例混合而成的,混合比例由超参数 α_t 控制。
训练时,网络要做的事情其实非常简单:给它输入 xt 和时间步 t,让它预测当时加入的噪声 ε。这个过程被反复执行,网络逐渐学会了“如果给我一张带噪图和对应的时间步,我能猜出里面混了多少噪声以及是什么样的噪声”。这是最核心的训练任务,也是扩散模型和 GAN 最大的区别:GAN 直接生成图像,而扩散模型学习的是“去噪”这个动作。
3.2 逆向过程:多步去噪的“雕刻”艺术
生成图像时,我们从一张纯噪声 x_T 出发,通过逆向过程(Reverse Process)一步一步去噪,最终得到干净图像 x0。每一步并不是彻底去掉所有噪声,而是根据 UNet 预测的噪声,在调度器的控制下减去一部分,再加回一点点随机噪声(这一步叫做重参数化,能避免生成过程走向确定性崩溃)。这个“多步小步走”的策略,让每一步的修正都足够精细,最终才能形成稳定、清晰的画面。
很多人不理解为什么不能一步到位,非要迭代几十次。直观地说,一步预测噪声的误差往往很大,直接减去全部噪声会让画面出现奇怪伪影。而几十步迭代中,每步只修正一点,可以让模型充分“思考”当前状态,结合文本条件做精细化调整。这也是为什么步数从 20 提到 50,画面细节和稳定性通常会有明显提升,但超过 100 步后收益就非常有限了。
3.3 调度器(Scheduler)的作用与参数影响
调度器决定了逆向过程中每一步怎么从预测噪声更新当前潜变量。常见的调度器有 DDIM、DPM-Solver、Euler a 等,它们在每一步的更新公式和是否需要额外随机噪声上有所不同。DDIM 的优势是步数少、确定性强;DPM-Solver 则在低步数下也能保持较好的采样质量。我自己在频繁试验后的经验是:想要快速预览效果,用 DPM-Solver 或 Euler a,步数设 20 到 30;想要追求极致细节,可以用 DDIM,步数设 50 左右。调度器本身不会彻底改变画面风格,但会影响细节保留和光影过渡的均匀性。
4. UNet 结构:图像特征提取与噪声预测主力军
4.1 编码器、瓶颈层、解码器的三明治设计
UNet 因结构形似 U 而得名。它由三大部分组成:编码器(左侧,逐步下采样)、瓶颈层(底部)、解码器(右侧,逐步上采样)。编码器的每一层都包含残差块和自注意力模块,一边提取特征一边降低空间分辨率;瓶颈层则以最紧凑的特征形式处理全局信息;解码器再逐步恢复分辨率,并通过跳跃连接(Skip Connection)把编码器对应层的特征直接拼接过来。
跳跃连接是 UNet 最精妙的设计。它把浅层的高分辨率细节直接传递给深层,让解码器在还原图像时既能利用深层的语义信息,又保留浅层的纹理细节。你可以把它想象成画师先在草稿纸上画出轮廓,然后不断参考轮廓细节来填充颜色,而不是凭空猜测。
4.2 自注意力与交叉注意力的双通道
UNet 内部有两种注意力机制:自注意力(Self-Attention)处理图像特征内部的关系,让远处的像素也能相互参考,解决卷积核感受野有限的问题;交叉注意力则把文本条件引入,与 CLIP 输出的 embedding 做交互。这两个注意力模块的配合,让 UNet 既懂得“上下文”又懂得“指令”。
这种设计的直接效果是:自注意力保证了图像的一致性和连贯性,比如当你生成一只长颈鹿时,脖子和身体的纹理能够自然衔接;交叉注意力则保证了语义的精准控制,比如你要求“红色的椅子”,模型会在相应区域强调这个颜色概念。二者缺一不可。
4.3 时间步嵌入:让模型知道当前做到哪一步
UNet 在每层处理时,除了输入带噪潜变量,还需要知道当前处于整个去噪过程的哪一步。这个信息通过时间步嵌入(time embedding)提供。模型把时间步 t 映射为一个特定维度的向量,并加到每一层的特征中。听起来有点抽象,实际理解起来不难:就像做菜时的“计时器”,锅里炖了 5 分钟和 30 分钟,处理方式显然不同。噪声程度高的阶段需要大刀阔斧地重构,噪声程度低的阶段则需要精雕细琢。
5. 文生图完整流程:从零写一个可运行脚本
5.1 环境准备与依赖安装
在开始写代码之前,需要先准备一个可用的 Python 环境。我使用的是 PyTorch 生态,依赖项包括diffusers、transformers、accelerate、safetensors和torch。为了让代码尽量简洁,我直接基于 HuggingFace 的diffusers库做封装,你不需要手写 UNet 或 CLIP 的完整实现,只需要关注调用流程即可。
pip install diffusers transformers accelerate safetensors torch装好之后,先检查本机有没有可用的 GPU。如果没有 NVIDIA 显卡,代码会退回 CPU 模式,但生成速度会非常慢,一张图可能要几分钟甚至更久。所以还是建议在有 CUDA 的显卡上运行,例如常见的 8GB 显存显卡就能在 20 步左右出图。
5.2 加载模型 Pipeline:STableDiffusionPipeline 的用法
在 diffusers 库里,最便捷的方式是使用StableDiffusionPipeline。这个 Pipeline 已经把 CLIP、UNet、VAE 和调度器全部封装好了,你只需要指定模型 ID 和运行设备。
import torch from diffusers import StableDiffusionPipeline # 指定一个基础模型 ID,例如某开源社区中的通用模型 model_id = "your-model-id-here" pipe = StableDiffusionPipeline.from_pretrained( model_id, torch_dtype=torch.float16, safety_checker=None, ) pipe = pipe.to("cuda")这里有几个参数需要解释一下:torch_dtype=torch.float16表示使用半精度运行,能显著降低显存占用并提升速度;safety_checker=None表示跳过内置的安全检查器,能减少推理时间,但请确保你生成的内容遵循相关规范。
注意:加载过程会下载模型文件,如果网络状况不佳,建议提前设置好镜像源,或者从本地路径加载。我自己踩过坑,首次加载时因为网络中断导致模型文件损坏,后来下载到本地再加载才稳定下来。
5.3 提示词预处理与张量准备
加载好 Pipeline 之后,下一步是准备输入提示词,并把它转成模型需要的张量格式。diffusers提供了encode_prompt的内部方法,但我们直接用pipeline的调用接口时,它会帮我们自动完成这些处理。如果你需要手动查看中间结果,可以这样操作:
prompt = "a beautiful landscape, mountains, lake, sunset, highly detailed" negative_prompt = "blurry, low quality, distorted" # 预先将文本转成模型需要的张量 text_embeddings = pipe._encode_prompt( prompt=prompt, device=pipe.device, num_images_per_prompt=1, do_classifier_free_guidance=True, negative_prompt=negative_prompt, ) print(text_embeddings.shape)这个过程中,CLIP 会把提示词和负面提示词分别编码为条件向量。负面提示词并不是从生成结果中“去掉”某些东西,而是通过无分类器引导(Classifier-Free Guidance)机制,让模型更清晰地理解“你不想要什么特征”。理论上,负面提示词的存在会拉大正反向条件的差异,从而增强生成结果的语义控制力。
5.4 设置调度器与推理参数
在正式生成前,我们还要配置调度器和推理超参。调度器负责定义采样方式,diffusers 默认使用 PNDMScheduler,但更推荐替换为 DPMSolverMultistepScheduler,它在较少的步数下就能得到不错的效果。
from diffusers import DPMSolverMultistepScheduler pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) generator = torch.Generator(device="cuda").manual_seed(1024) with torch.autocast("cuda"): image = pipe( prompt=prompt, negative_prompt=negative_prompt, num_inference_steps=25, guidance_scale=7.5, width=512, height=512, generator=generator, ).images[0]这段代码里有几个参数值得仔细解释一下:
num_inference_steps:去噪步数。太少会显得粗糙,太多则会白白增加计算时间。25 对于 DPM-Solver 来说已经足够生成相对干净的图像。guidance_scale:无分类器引导的强度。值越高,图像越贴近提示词的语义,但过高会导致色彩过饱和、对比度异常,通常设置在 7 到 9 之间。generator:固定随机种子,可以让你复现完全相同的结果,方便调试参数。width和height:生成图像的尺寸。如果设置为 768x768,需要确保显存足够,且模型本身支持该分辨率。
5.5 保存与展示生成结果
最后一步很简单,把生成的 PIL 图像保存到本地。
image.save("output.png")如果想在 Jupyter Notebook 里直接预览结果,也可以用display(image)。到这里,一个基础版文生图脚本就完成了。整个流程封装得很干净,核心代码量不超过 30 行,真正复杂的 UNet 和 CLIP 都被库隐藏在了内部。
6. 常见问题与排查技巧实录
6.1 显存不足怎么办:降低分辨率与步数优先
CUDA out of memory是最常见的报错之一。如果你的显卡显存有限,比如只有 6GB,最直接的调整是把生成尺寸改为 512x512,并把 batch size 设为 1。还可以考虑开启pipe.enable_attention_slicing(),这个操作会对注意力计算进行分片,减少中间激活值的显存占用。
另外,把步数从 30 降到 20 也能略微减少显存压力,毕竟每一步都要做一次完整的 UNet 前向推理。如果还是爆显存,可以尝试使用 CPU 上的部分层计算,但速度会明显下降,我的建议是优先降低尺寸,不出意外能解决 90% 的显存问题。
6.2 生成图像杂乱或色彩失真:调整分类器引导强度
如果你发现生成结果结构混乱、物体形状扭曲,或者颜色过于饱和浓重,大概率是guidance_scale设置得过高。我在多次实验中发现,7.5 是很多模型和提示词的“甜点区”;超过 12 后,图像会开始出现伪影。但调得太低也不行,比如低于 3,出图会和提示词关联度很差。建议以 7.5 为基准,每次增减 0.5,直到找到最适合当前模型的数值。
6.3 提示词不生效或细节缺失:检查 token 截断与词序
前面提到,CLIP 最大只处理 77 个 token,所以长提示词里的关键信息很容易被截断。解决办法是调整描述的顺序,把主体、风格、构图这些核心词放在句首,修饰词放在后面。另外,同一个词汇的不同表达方式也可能导致差异,例如“big house”和“spacious house”,模型响应不同。如果你反复调整提示词都不生效,建议去可视化工具里看一眼 CLIP 对每个 token 的注意力权重,非常直观。
6.4 生成结果不稳定,同一提示词每次不同:固定种子与调整采样偏差
扩散模型的生成过程带有随机性,即便你提供相同的提示词,也能得到完全不同的图。为了可控地调试参数,必须在每次生成时固定随机种子。如果你发现固定种子后结果仍然波动,可能是调度器在采样时引入了额外的随机噪声(比如某些采样器为了增加多样性会故意加入微小噪声)。可以考虑换用确定性更强的采样器,比如 DDIM,或者在pipe()参数中设置更低的eta值。
6.5 代码测试速查表
| 现象 | 可能原因 | 调试建议 |
|---|---|---|
| 显存不足,程序崩溃 | 输入尺寸过大 / 步数过多 | 降至 512x512,开启 attention slicing,减步数 |
| 画面杂乱、物体扭曲 | 引导系数过高 | 将 guidance_scale 降至 5-7 之间 |
| 长提示词内容缺失 | 超过 77 token 被截断 | 关键描述提前,删减修饰词 |
| 相同种子但结果不同 | 采样过程引入了额外随机性 | 换用 DDIM 调度器或设置 eta=0 |
| 出图速度极慢 | CPU 推理 / 未启用半精度 | 开启 CUDA,使用 torch.float16 |
7. 实测经验:调度器、步数与提示措辞的协同调整
7.1 如何根据目标风格选择调度器
调度器不仅影响采样速度,还会对最终画面的质感产生微妙影响。我的个人经验如下:如果你追求的是“视觉冲击力强、边缘锐利”的作品,DPM++ 2M Karras 会是个好选择;如果你想让画面更柔和自然,Euler a 可能更适合;而想要高度可复现、过程稳定的输出,DDIM 是稳妥的选项。在跑实验时,我会针对同一提示词做几个调度器的横向对比,挑出最符合预期的作为固定配置。
7.2 步数设置与细节呈现的关系
步数并不是越多越好,而是存在一个“边际收益递减”的拐点。我用同一张图测试过步数从 10 到 100 的变化,发现 10 步左右画面结构已经基本成型,但粗糙感明显;20 到 30 步时细节逐渐丰富;超过 60 步后,肉眼几乎看不出区别。对于大多数场景,25 到 30 步是性价比最高的区间。如果你时间充裕且想追求极致细节,设置 50 步也完全没问题。
7.3 实用的提示词书写模板分享
这里给你一套我常用的提示词书写结构,能有效提高出图成功率:
主体 + 动作 + 环境 + 构图方式 + 风格 + 细节修饰比如“a young woman reading a book in a cozy library, soft window light, portrait composition, oil painting style, intricate details”。这种从主体到细节的逻辑顺序符合 CLIP 编码特征,在生成时能够最大化利用有限的 token 窗口。负面提示词方面,我通常会加上“lowres, bad anatomy, bad hands, extra fingers, blurry”等常见问题词汇,能显著提高成图质量。
8. 实战案例:从素描写生到数字绘画风格切换
为了让你更直观地理解上述参数的作用,我用同一张构图做了一组对比测试。提示词的主体是“一只坐着的柯基犬”,分别选用三种风格修饰:“pencil sketch”、“watercolor painting”、“cyberpunk style”。
在固定种子和采样器不变的前提下,只修改风格词,最终结果在构图和姿势上保持了高度一致,但色彩、材质和光感呈现出完全不同的面貌。这表明 CLIP 对风格词汇的编码是相当稳定的,它不会改变主体本身,而是通过交叉注意力控制纹理和色调的重建方向。这个特性对艺术家很有用:你可以先确定构图和主体,然后批量尝试不同风格词,快速产出系列作品。
我个人的体会是:想要真正驾驭 Stable Diffusion,不能只依赖 WebUI 的滑块操作。潜入代码层面,理解每个参数如何影响和调度模型内部的张量流动,才是关键。当你能够熟练拿捏调度器、步数和引导系数时,那些看起来“玄学”的出图差异,都会变成可控的、可预期的变量。
最后分享一个小技巧:在你找到一组满意的参数后,务必记录下来。我自己的习惯是维护一个表格,记录提示词前半段、风格词、负面提示词、采样器、步数、种子和最终得分。慢慢积累起来,你会发现那就是你最宝贵的私人配方库。后续无论在哪个平台复现结果,还是帮朋友排查问题,都能迅速定位到有效配置。