Stable diffusion 笔记

为什么我炼不出波奇酱?

Preface

新笔记本暗影精灵 9 锐龙版自从 6 月多到手后,换了两次系统 —— 换到 win10 发现驱动没有 win10 支持所以又换了回去 win11。 到了 9 月多时,想着拿这 8g 显卡跑点啥,正好又对 Stable diffusion 感兴趣,于是研究了几天源码,跑了几天训练效果对比。 最终结果还是不尽人意,数据集不全是一方面的问题,还有我的 8g 显卡不支持我对模型继续深入研究了。

原本 9 月底炼的差不多时就想着写一篇文章介绍一下 Stable diffusion,但是不知道为什么忘了(转头像转的)。

此文章旨在用阐述我对 Stable diffusion 源码和架构的一些理解,以及对于 “为什么我炼不出波奇酱?” 这个问题的解答。

脚本

文章用的是 diffusers 提供的示例脚本:

https://github.com/huggingface/diffusers/tree/main/examples

对于略微有点 neural network 知识的人都可以上手。 此外还有面向新手的 HCP-Diffusion 值得推荐,照着它给的 example yaml 改改就能跑。 相对于 diffusers 和 hcpdiff,sd-scripts 是对 diffusers 的脚本进行的拓展,但是提供的脚本复杂难以配置,不建议使用。不过 基于 sd-scripts 的 GUI 可以考虑使用。

SD 模型结构

一个 SD 模型组成:

  • FeatureExtractor (safety checker 专用)
  • Tokenizer, TextEncoder (text)
  • Scheduler (add noise)
  • UNet (denoise)
  • VAE (Variant Auto Encoder)

上面的部件中除了 UNet,其余都是预训练的。 FeatureExtractor, TextEncoder 这些是通用型权重,就和从 resnet 独立出 feature extractor 一个道理。

VAE 也是预训练的,但是有很多人会误解它的方法和作用。 网上查不到具体训练方法,论文里讲的也不多,在 G. Details on Autoencoder Models 那一部分。 我的猜测是和 AE 差不多,img -> latents -> img 的自监督训练。其中的 latents 和 embeddings 有异曲同工之处,区别在于 latents 作为模型输入,有具体形状,embeddings 作为上下文嵌入,一般是 linear features。 VAE 的 encoder 可以实现 img2img 的效果,下面会具体讲讲。

VAE 细节:源码里的 VAE encoder 输出是 8 维的,会使用一个叫做 DiagonalGaussianDistribution 的模型进行变分处理(大概是为了增强重建能力),同时这个变分模型也提供了合并方法输出 4 维,再输入到 UNet。

这里是我根据 diffusers 文档实现的完整 pipeline: https://github.com/iyume/diffusers-test/blob/main/tours/diffusers_tour_pipeline_depart.py

text-to-image

  1. tokenize prompt
  2. 根据 tokens 和 textual inversion weights 进行 token -> vectors 映射
  3. 创建一个长度相同的 uncond_embeddings 并 concat 进去(N+1),旨在使用 CFG Scale (guidance_scale) 控制结果与 prompt 的贴合程度
  4. 创建噪声图,scheduler 循环 timestep 调用 UNet 降噪,embedding 在 UNet 内部处理

UNet 有个 time embedding 我没看懂,不知道为什么要将 text embedding 转换为 time embedding。参照 1 2 3

image-to-image

  1. 利用 VAE encoder 将图像编码为 latents
  2. 根据 step 和 strength 调用 scheduler 对 latents 增加噪声
  3. scheduler 循环 timestep 调用 UNet 降噪,有 prompt 的话就载入 text embedding

Context Embeddings

Context Embeddings 是 Stable diffusion 模型里面一个非常重要的设计,用于接受各种各样的输入,如文本、图像、音乐、语音。 embeddings 并不是多模态,它们有着重要的区别:Stable diffusion 只能接受单一种类的 embedding,除非使用 adapter 模型对各种潜在空间进行相互转换。

具体的嵌入方法就是 Attention 注意力机制。 Attention 是一个单独的层,大部分有着独立的权重,用于改变输出,一般不影响输出的尺寸。

就我个人经验而言,在传统的 CNN 领域里,Attention 一般分为 Channel Attention 和 Spatial Attention,根据维度的不同,计算方差或者其他类似的值,加到结果内。

在 Stable diffusion 则是一个叫做 Transformer2DModel 的模型,比较复杂没细看。

训练方法

基于 RTX 4060 Laptop 8G 测试,只跑得动 textual inversion 和 lora,所以只对这两个训练方法做介绍。

训练图片采用的是 16 张从 TV 截图的波奇酱的图片,每两张在 anythingv5 模型上做了 img2img 风格化处理(对于 lora 似乎影响不大),所以眼睛会略微偏蓝。

Textual Inversion

文本反转,即不是通过改变权重来改变模型效果,而是通过改变输入来改变输出。

训练 textual inversion 时,通过创建一个 tokenizer 不认识的 token,映射至对应的向量,这个向量长度可以由 num_vectors 控制。 向量长度越大,输出的权重文件也就越大,一般 32 vectors 对应 97 kb。

在 tokenizer 中,一个 word 是有可能对应多个 token 的,比如 girls 会被拆分为 girl, s

**结果评价:**训练时间 1 小时。97 KB (32 vectors)。 完全无法训练姿态,只会扭曲模型效果(姿态有关的都只能采用 ControlNet 和 openpose)。动漫人物的发饰、呆毛、眼球等细节完全无法还原,容易产生撕裂现象,还有模糊不清。大概只能训练结合模型已有的物品。非常容易破坏模型风格。

综上,Textual Inversion 是一个很黑盒很 DNN 的操作,大炼丹师或许可以得到非常小的权重和非常完美的结果,或者是大魔法师配合优秀的 prompt 也能得到不错的结果,但是不建议使用这个训练方法。

下图是使用单 keyword (gotou_hitori) 不搭配任何 prompt 生成的图像:

下图是使用 keyword 搭配 prompt 生成的图像:

使用了类似 [[gotou_hitori:0.2]::0.8] 的 prompt 魔法。

text-to-image

最原始的方法,旨在训练整个 UNet。

对降噪误差做 backward,比如 DDIM 有 50 timestep,对 20 至 21 的降噪步骤相减并 backward。

我这里只用了 text-to-image lora 脚本,没有记录训练时间,但是我记得和 textual inversion 的训练时长差不多,且模型效果比 textual inversion 还差,大概率是因为数据集不完善。diffusers 的官方脚本没更新,只能用它老一套的 datasets 载入,比较麻烦。

评价为不好用,要求数据集完善,可以被 dreambooth 完美取代。

Dreambooth

参照官方的 Dreambooth 讲解。 Dreambooth 也是训练整个 UNet 的方法,但是速度快,要求数据集小。

我这里只训练了 dreambooth lora 脚本。训练的关键参数是 rank,类似于 textual inversion 中的 num_vectors。 rank 越高表示控制的维度越深,同时权重文件也会越大,也更容易过拟合。

防止过拟合的方法最简单的就是用 tensorboard 看看每一百步生成的测试图像。

**结果评价:**训练时间 10 分钟,可以关掉 validation。3 MB (rank 1)。 对动漫人物的学习(建议 rank>10)可以达到略胜于 Textual Inversion 的效果,主要是不容易影响模型自带的风格,模型兼容性也比 TI 更好。但对于波奇酱的方块发饰则很难达到好的效果,生成出来要么是圆的,要么一大一小,只有极小概率会生成出不错的发饰。

参考生成图:

Dreambooth lora test

第一张的效果最佳,不过呆毛偏上了点。相比于 TI 来说,lora 不需要搭配太多 prompt,只要 <lora:gotou_hitori> 载入 lora 即可达到不错的效果。

高清生成

最后放一张使用 anime6k 配合 prompt 咒语生成的高清图:

波奇酱高清

里面应该是包含了 generation data 的。话说现在居然还没有一个便利的查看/修改 generation data 的工具,打算自己写一个了。

笔记

随手记的一个笔记

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CFG Scale(classifier-free guidance): 提示词和生成图像的贴合程度
Outpainting: 向外延展画布
Inpainting: 修复图像内部空缺
Color Sketch: 为素描画填充颜色
Prompt Matrix: 提示词矩阵,用 | 分割提示词的排列组合进行扩散
Pormpt S/R: 提示词 Search/Replace,搜索替换并矩阵扩散
Prompt Editing: [from:to:when] 控制每次 inference step 的 prompt
(()) 给予更多权重,(word:1.111) 权重分配
X/Y/Z plot: 同 X/Y plot,根据不同输入参数(提示词,CFG scale,step 等)创建输出矩阵
Textual Inversion: 将特定提示词和相关图像嵌入模型进行训练
Tiling: 将图像周期平铺放大分辨率,比如把花的数量增加
Styles: 保存常用提示词
Variations: 图像微调,与 img2img 不同在于,微调的提示词是可选的,构图通常有很大变化
High-res fix: 防止生成高分辨率(大于 512x512)时出现图像扭曲,在生成 512x512 时设置 upscale 参数 1.5 则输出 768x768,step 设置为 0 时会使用 sampling step 的值
Upscaling: 将图像进行放大
Alternating Words: 交叉使用不同的 prompt,是 Prompt Editing 的自动版

tcmalloc: 对显存分配提升巨大,见 https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/6722
将 generation data 粘贴到 prompt 然后点击 "Generate" 下方第一个蓝色按钮即可自动应用参数

https://www.youtube.com/watch?v=dVjMiJsuR5o
Dreambooth: 训练整个模型最有效的方法,也只能训练整个模型,占用空间大
Textual Inversion: 在 text encoder 进行训练,对某种特定的单词创建特定的 embeddings (vector) 再输入模型,只改变描述,不改变模型效果
LoRA: 在模型中插入低秩权重,对这个权重进行训练,同时会减慢生成速度,数个 LoRA 可以同时使用,但很容易过拟合(容易记住数据集)

Tricks:
(1) num_train_epochs = max_steps * batch_size / len(train_dataloader)
max_steps = num_train_epochs * len(train_dataloader) / batch_size
假如有 14 张训练图片,此时 len(train_dataloader)=1400,则 epochs = max_steps * 4 / 1400,max_steps=1050 时 epochs=3,max_steps=1051 时 epochs=4
(2) Inpainting Mask blur 是进行高斯模糊的参数,0 代表完全改变,越高代表越少的改变
(3) [[gotou_hitori:0.2]::0.8] add gotou_hitori from 0.2 to 0.8

后记

Stable diffusion 太强了,生成美少女也很好玩,源码太复杂没能全部读完,将来要是有兴趣了再读吧。

虽然我对 AI 感兴趣,从最开始的 CS231n 到后面发论文,期间读了很多 segmentation(deeplabv3,bisenetv2,unet)、fusion(多尺度、曝光、聚焦等各种融合) 等领域的优秀论文,也从中学了很多。 但我毕竟是个末流一本,也不是搞算法和数学原理的,对我而言沉没成本很大。 AI 我是打算只当作兴趣学学的。Web3 也差不多,后面可能会写一篇介绍 go-ethereum 的文章。

之前居然还有一个人开了 issue 和我讨论,挺惊讶的:https://github.com/iyume/image-fusion/issues/2

文章花了一整天的时间来写,太久没写文章感觉很生疏。

CC BY-NC-SA 4.0 License