原文 https://www.felixsanz.dev/articles/how-stable-diffusion-works
引言(Introduction)
使用 Stable Diffusion 并不一定要理解它的工作原理,但理解之后会有帮助。许多可配置参数,比如 Seed(种子)、Sampler(采样器)、Steps(步数)、CFG Scale 或 Denoising strength(去噪强度),对于生成高质量图像至关重要。
本文将从宏观角度、尽量用通俗方式(不写一行代码、面向几乎所有人),讲解 Stable Diffusion 1.x 版本的内部机制,以及它如何仅凭一句文本就能生成如此有创意的图像。
扩散模型如何工作(How a diffusion model works)
在机器学习中,有多种 生成模型(能够生成数据的模型,这里是图像)。其中一类是 扩散模型(diffusion models),之所以这么叫,是因为它们模仿分子扩散的行为。我们都经历过这种现象:喷空气清新剂,或者往水杯里倒冰块,颗粒会移动并扩散开来。
正向扩散(Forward diffusion)
Stable Diffusion 的训练从互联网上收集的几十万张照片开始,并在一系列步骤中逐步向图像添加高斯噪声(Gaussian noise),直到图像完全失去意义。换句话说,噪声像空气清新剂在房间里扩散粒子那样扩散到图像中。这个过程称为正向扩散(因为它是“向前”的过程)。
反向扩散(Reverse diffusion)
当我们让 Stable Diffusion 生成图像时,就会发生所谓的“魔法”——反向扩散(reverse diffusion),它会把前面那个过程反过来。
也叫(Aka)
把模型在训练中学到的东西应用到实际生成上,也常被称为 推理(inference)。在 Stable Diffusion 里,这个术语可以用来指代反向扩散过程。
反向扩散的第一步,是生成一张 512×512 像素的纯随机噪声图,这张图没有任何意义。生成这张噪声图时,我们可以通过 seed(种子) 参数控制,默认值是 -1(随机)。例如,如果使用 seed 数字 4376845645,每次都会生成同一份噪声,因此只要知道 seed,就可以复现生成结果。
接着,要把一张全是噪声的图变成“一只穿着蝙蝠侠服装的狗”,并把过程反过来,你需要知道图像里“还剩多少噪声”。这通过训练一个 卷积神经网络(convolutional neural network)来实现,它能预测图像中的噪声水平。这个模型叫 U-Net(或去噪 U-Net),而在 Stable Diffusion 中通常称它为 噪声预测器(noise predictor)。
训练这个模型的方法,是使用正向扩散过程中产生的图像,并告诉它我们加了多少噪声,让它学会识别噪声水平。就像我告诉你:第一张图噪声是 0%,中间那张 50%,最后一张 100%……你很可能也能在新照片中判断噪声大概是多少,因为你已经理解了规律。也许不完美,但嘿,你又不是神经网络!
(示意:0%、50%、100%、75%、13%)
采样(Sampling)
有了最初这张“纯噪声”的图,以及一种能计算“图里还剩多少噪声”的方法,**采样过程(sampling process)**就开始了。
噪声预测器会估计图像里还剩多少噪声。然后名为 采样器(sampler) 的算法会生成一张包含“该噪声量”的图,并将其从原始图像中减去。这个过程会重复若干次,重复次数由 steps(步数) 或 sampling steps(采样步数) 指定。
采样算法的例子包括:Euler、Euler Ancestral、DDIM、DPM、DPM2、DPM++ 2M Karras。它们的差异在于:有的更快、有的更具创造性、有的更擅长细化小细节等。关于这些算法的更多信息、用途与差异,可参考文章 Complete guide to samplers in Stable Diffusion(Stable Diffusion 采样器完全指南)。
重复足够多次后,我们就能得到最终图像。
(示意:图像 / 估计噪声:100% → 75% → 50% → 25%)
采样里还有一个重要组件叫 噪声调度器(noise scheduler),它负责管理每一步应该去除多少噪声。如果噪声减少是线性的,那么图像每一步变化幅度都一样,会产生比较突兀的变化。一个“负斜率”的噪声调度器可以在一开始去除大量噪声以便更快推进,然后逐步减少每一步的去噪量,从而微调图像中的小细节。这个算法可配置,也可以在多种选项中选择。
(图示:Noise predictor (U-Net)、Seed、带噪张量/去噪张量等)
但是……模型怎么知道噪声背后是狗?(But... how does the model know that there is a dog behind the noise?)
答案是:它并不知道。到目前为止,我们只是生成了一张无条件图像(unconditioned image)。结果可能是汽车或任何东西,因为我们的 prompt(提示词) 并没有对结果施加约束。
条件控制(Conditioning)
当然,真正有趣的是:不是生成随机无意义的图,而是在每一步都对结果施加条件约束(conditioning)。
回忆一下,我们训练噪声预测器时给了它两样东西:图像 和 噪声水平。因此,我们可以把噪声预测器“引导”到想要的结果上。就像问:“这张穿着蝙蝠侠服装的狗的照片里噪声水平是多少?”而不是只问“这张照片的噪声水平是多少?”。前一种问法就是在对结果施加条件。
(“不要想粉红色大象!”的例子)
使用文本(prompt)并不是唯一的条件控制方式。可以同时使用多种条件。下面从最重要的一种开始。
文生图(Text-to-Image)
把文本转换成条件,是 Stable Diffusion 最基础的功能,叫 Text-to-Image。它由多个部分组成。
分词器(Tokenizer)
因为计算机不理解字母,第一步需要用 分词器(tokenizer) 把每个词转换成数字,称为 符号(symbol)/ token(令牌)。
在 Stable Diffusion 中,分词依赖 CLIP(Contrastive Language-Image Pre-Training) 模型。这个由 OpenAI 创建的模型能把图像转换成详细描述,不过在这里仅使用其内置分词器来做这一步。
示例:dog in batman costume → CLIP tokenizer → 25 56 97 12
注意:tokens ≠ words。分词器不一定把每个单词都变成一个 token;可能存在复合词,空格也会影响 token 数量。
你可能见过“prompt 最多 77 个词”的说法。这个限制之所以存在,是因为 token 存在一个长度为 77 的向量里(1×77)。所以限制其实是 token 数量,不是单词数。确实,超过 77 个 token 后其余会被丢弃;但现在也有拼接与反馈等技术,可以用多个 77-token 的块来条件控制噪声预测器,直到用完全部 token。
通用组件(Common components)
上面这一步只属于 Text-to-Image。从这里开始,后续各种条件控制都会用到下面这些层。
嵌入(Embedding)
来做个想象练习:我们有 10 张人物照片,要按两个参数把他们放到二维坐标图里:年龄 和 头发数量。如果做得对,就会得到某种分布。
这其实就是一种 嵌入(embedding):用向量把术语和概念分类存储。Stable Diffusion 使用的是 CLIP 的一个版本 ViT-L,其嵌入维度是 768 维。我们无法直接知道每一维代表什么,因为它取决于训练数据,但可以想象:某一维表示颜色、另一维表示物体大小、另一维表示纹理、另一维表示面部表情、另一维表示亮度、另一维表示物体间的空间距离……一直到 768 维。
每个 token 都有 768 维。也就是说,如果 prompt 里用到 “car”,对应的 token 会被转换成一个 768 维向量。对全部 token 做完之后,会得到大小为 1×77×768 的嵌入。
采用这种方式后,模型可以计算这些概念之间的距离:例如“服装”概念的维度,通常会比“窗户”“门”(房间部件)更接近“男人”“女人”等概念。这个例子很简化,真实情况更抽象。
Transformer(变换器)
这是条件控制的最后一步:嵌入会被一个 CLIP Transformer 处理。该网络由多层组成,负责在每一步中引导噪声预测器,让生成结果朝向能表达这些嵌入信息的图像。
在 Text-to-Image 中这些嵌入叫 文本嵌入(text embeddings),但 transformer 也可以处理来自其他条件的嵌入(后面会提到)。
Transformer 里有个关键点:当所有嵌入权重相同、对结果影响一样时,称为 自注意力(self-attention)。除了这种结构,Stable Diffusion 还使用 交叉注意力(cross-attention):它会计算嵌入之间的依赖关系,输出一个表示这些关系的张量;这一步会用到前面提到的“距离”。
例如 prompt:a red wall with a wooden door
如果只有 self-attention,模型可能生成“木墙配红门”,这也合理但可能不符合你的意思。
借助 cross-attention,模型会计算 wood/door 和 wood/wall 的接近程度,知道前者更可能,于是更倾向于生成“红墙 + 木门”。
我们人类也类似:如果我说“红色的车”,你很可能想到跑车(比如法拉利)而不是丰田普锐斯,因为在你的脑中“car”和“red”更接近“Ferrari”而非“Prius”。这就是“注意力”这个名字的来源:它模拟了我们关注某些细节的认知过程。
利用这些信息,噪声预测器会在每一步被引导,逐渐逼近一个在给定条件下最可能的结构。
类标签与 CFG Scale(Class labels and the CFG Scale value)
另一个重要的条件控制来自 类标签(class labels),它能帮助 transformer。
训练 Stable Diffusion 时,会用一个外部分类器(CG - classifier guidance)给训练图像分配标签。例如,狗的照片会带有 animal、dogs 等标签,猫的照片会带有 animal、cats 等(还有很多)。
当我们把 CG scale 设得很高时,相当于让 transformer 更强烈地区分“不相似”的标签、聚焦于“相似”的标签;当值较低时,标签间更“接近”,于是你让它生成猫时,可能得到任意其他动物(或附近标签)。
为了避免训练时必须依赖外部分类器,后来引入了 classifier-free guidance(CFG)。这意味着引导不再需要一个分类器给图像打标签;训练过程中直接使用图像描述来自动调整类别。
通过调节 CFG Scale,我们可以更强或更弱地“划定标签的领地”。因此常说它会增加或减少生成自由度:
值越高:更忠实(fidelity),标签选择更谨慎;
值越低:更自由(freedom),不那么受提示词约束,有时反而能产生更高质量(限制更少)。
作者经验:最佳范围通常在 5.5 到 7.5,但也需要视情况调高或调低(尤其当 prompt 没被遵守时,往往需要提高)。
图生图(Image-to-Image)
如果用一张图像来作为条件控制,就进入 Image-to-Image。
例如,我们随便画一张很潦草的房子草图,然后把这张图和 prompt 一起交给 Stable Diffusion:
a realistic photograph of a wooden house with a red door in the middle of the forest
两种条件会同时作用,得到结果。
这是怎么做到的?记得 Stable Diffusion 一开始会创建一张“纯噪声图”吗?在这里不是从纯噪声开始,而是对你的起始图像添加一定量的噪声,作为基础。
通过 Denoising strength(去噪强度) 可以控制加多少噪声:
值为 0:不加噪声 → 结果与草图几乎相同;
值为 1:把条件几乎变成纯噪声 → 草图对结果影响不再明显(相当于回到 Text-to-Image);
介于两者之间:通常是理想区间,取决于你希望模型多大程度尊重输入结构。
(示例说明:0、0.5、0.75、0.9、1 不同强度下结构保持与“真实感”的权衡。)
局部重绘(Inpainting)
Inpainting 指对图像某一部分重新生成,本质上是应用于特定区域的 Image-to-Image。
外延扩图(Outpainting)
Outpainting 指扩展图像边界、生成原本不存在的新区域。它通常会用多层条件控制:新区域通常用噪声(因为是空白),再加 prompt,以及原图的一部分(或全部)一起作为条件。
深度引导(Depth-to-Image)
如果要利用图像的三维信息,会用 深度图(depth map),这称为 Depth-to-Image。
像 MiDaS 这样的模型会处理图像以提取深度图。随后根据 Denoising strength 添加噪声,把这些信息转换成 embedding,并与其他 embedding 一起进行条件控制。
示例:只用一张图来提取深度图(原图仅用于提取深度),再配合 prompt:two astronauts dancing in a club on the moon
其他(Other)
ControlNet 是一种神经网络,借助一系列模型以多种方式对输出施加条件控制,能应用深度图、姿势等多种条件。
示例:用 prompt astronaut on the moon 进行姿势控制。
创建姿势(Creating poses)
你不一定要用 OpenPose 从图像中提取姿势,也可以从零开始创建,比如通过一个在线编辑器。
潜空间(The latent space)
这里还有一块拼图:没有它,上述过程都无法高效完成。此前我们一直在图像的像素上工作,这叫 图像空间(image space)。对一张 512×512 的 RGB 图像来说,空间维度是 786,432(3×512×512),这对家用电脑来说太大。
解决方案是使用 潜空间(latent space),因此 Stable Diffusion 属于 潜扩散模型(latent diffusion model, LDM)。它不是在巨大的像素空间中工作,而是先压缩到一个小 64 倍的空间(示意为 3×64×64)。
你可能会问:压缩 64 倍怎么还能不丢细节?关键在于:我们不需要全部信息。比如人像通常都有脸、眼睛、鼻子、嘴等共同结构。模型不存储每个像素的全部信息,而是以最高效方式用数字表示常见且重要的特征,存成一个大小为 3×64×64 的张量。
变分自编码器(VAE) 是一种神经网络:
编码器(encoder):把图像 → 潜空间张量
解码器(decoder):把潜空间张量 → 图像
反向扩散中我们看到的所有过程,实际上都发生在潜空间里。这会带来两点变化:
开始时,不是生成“像素噪声图”,而是生成**潜噪声(latent noise)**并存在张量中。对于 inpainting,会先把图像送入 VAE encoder 得到潜空间张量,再对其加上所需噪声。
结束时,当 Stable Diffusion 在潜空间得到最终结果后,张量会经过 VAE decoder 变成 512×512 图像。
Stable Diffusion 也允许你选择不同的 VAE。不同任务上不同 VAE 效果可能更好,因为潜空间会丢失一部分信息,而 VAE 负责把信息“恢复/重建”回来。
Stable Diffusion 及其版本(Stable Diffusion and its versions)
本文讲的是 Stable Diffusion 1.x。1.4 到 1.5 的差别主要在训练时间,因此 1.5 的出图质量更高。
Stable Diffusion 2.x 变化更大:1.x 使用 OpenAI 的 CLIP ViT-L,而 2.x 使用 LAION 的 OpenCLIP ViT-H。它更大,并且用**无版权(copyright-free)**图像训练。2.x 训练使用的图像最高到 768×768(而 1.x 为 512×512),因此生成图像在质量/分辨率上更高。
关于“无版权图像”,对社区是好事,但也带来批评:在某些情况下,使用专有风格或名人脸生成时,效果比前一版更差。2.x 也不允许 NSFW 图像。
2.1 版本在训练中稍微放松了 NSFW 过滤,因此这类图像现在可以生成。
结论(Conclusion)
回顾 Stable Diffusion 的推理(生成)主要步骤:
基于固定或随机 seed 生成一个带潜噪声的张量(inpainting 时会以图像为基础)。
各种条件(文本、深度图、类标签等)被转换为嵌入向量,在多维中存储其特征。
这些向量进入 CLIP transformer,利用**交叉注意力(cross-attention)**计算嵌入及其特征之间的关系。
噪声预测器(U-Net) 在 transformer 的引导下开始去噪。去噪采用采样机制,并重复指定的步数。
在每一步中,选定的 sampler 生成噪声并从初始张量中减去,得到更“干净”的张量作为下一步基础;noise scheduler 控制每一步进度,使其非线性。
去噪完成后,张量通过 VAE decoder 离开潜空间,转换成图像,从而生成最终结果。