介绍
本文将介绍以下内容:
数据集
收集数据的渠道(TODO) —— # 1.1.1
如何解决数据集的疑难杂症? —— # 1.1.2
应对数据缺失
应对画面损伤
应对图像重复:高效的图像去重算法
标注有哪些类型?如何挑选高质量的数据标注? —— # 1.2
训练
训练器是如何工作的? —— # 2.1
有什么创新的策略或算法能够优化训练? —— # 2.2
数据加权和配比算法 —— # 2.2.1
自动宽高比分桶算法 —— # 2.2.2
潜变量缓存算法 —— # 2.2.3
标注随机化算法(TODO) —— # 2.2.4
高级训练参数讲解 —— # 2.3
社区技术
提示词加权算法 —— # 3.1
为什么能实时预览 webui 的生成过程? —— # 3.2
文本嵌入
到底什么是文本嵌入 embedding? —— # 4
如何训练像 badhandv4 一样的负面文本嵌入? —— # 4.1
模型融合
模型融合的底层原理 —— # 5.1
模型融合算法介绍 —— # 5.2
模型融合的经验法则(TODO) —— # 5.3
如何设计你自己的模型融合算法?(TODO) —— # 5.4
注意:本文处在长期施工状态,各节内容将与时俱进。
1 数据集
1.1 数据收集
数据是训练的核心。训练的难点不是训练,而是如何获取并利用高质量的数据。
1.1.1 数据集的获取渠道
数据集的获取渠道很多,核心目的都是得到 图片+标注 的图文对。
收集数据取决于需求。几百张图的数据集常用于训练 LoRA、embedding 等小模型,可直接一张张下载;几千到几万张图的数据集用于轻量微调大模型、训练 ControlNet、大型 LoRA 等任务,可使用数据集获取工具收集或下载现成数据集;几十万几百万图的大型数据集用于全参数微调大模型,需要使用爬虫大规模爬取,或使用现成的大型数据集;再往上是从零训练的专业范畴,本文作者做不起。
注:本节下文所作推荐为个人向。
数据集工具
(推荐)Waifuc:方便、快捷的数据集收集和处理工具,专门服务于 AI 绘画领域,上手门槛低,功能多样。能做到一键爬取+处理数据。适合动漫二次元数据收集。
(推荐)Gallery-DL:朴素、老牌的图像爬虫项目。支持大量图像网站,使用稍复杂,可 DIY 性强、灵活。
(推荐)Powerful Pixiv Downloader:Pixiv 爬虫,浏览器插件,便捷,高效,五脏俱全。
Waifuset:作者的项目。带 UI 的数据集处理,同样适用于大数据集。
Hakubooru:Danbooru 数据库信息查询工具。
数据集来源
(推荐)HuggingFace:世界最大的机器学习社区之一,存储了大量的开源模型和数据集。
(推荐)Danbooru:动漫图像数据库。大部分动漫大模型数据获取的第一选择。图像多,数据搜索和获取非常容易,且每张图都带有详细的标签。适合大量收集数据。
Pixiv:动漫插画网站。
Artstation:概念艺术,作品质量很高。
Pinterest:图像共享网站,数据质量参差不齐。
Twitter:部分画师会在其账号下上传作品。
Unsplash:真人摄影网站,图片质量高。
(待补充)
开源数据集
nyanko7/danbooru2023:集合了 Danbooru 至 2023 年底的所有图像及其元数据。
deepghs/danbooru_newest-webp-4Mpixel:集合了 Danbooru 2023 年以后的数据,通常与前者搭配使用。
KBlueLeaf/danbooru2023-florence2-caption:社区使用 microsoft/Florence-2-large 模型对Danbooru 图像的自然语言标注数据集。
Nahrawy/VIDIT-Depth-ControlNet:ControlNet 数据集,3D 模型深度图带标注,1024x1024 分辨率。
(待补充)
1.1.2 数据的疑难杂症
收集数据时,我们常会遇到各种疑难杂症,本节将讨论应对各种问题的解决方式和思路。
数据缺失
在训练某种概念,特别是风格训练时,往往会出现训练图像数量不足的情况。在训练集有限的情况下,我们常考虑用以下方法扩充训练集:
在训练中启用 随机水平翻转。这将在训练中随机水平翻转训练图像。该参数应当总是对训练有帮助的。
特写裁切。将分辨率过大的图像裁切出各种分辨率下的特写。例如,从远到近,可将一张图像加工为整图、人物特写、半身特写和面部特写。
其中,尽量保证裁切后分辨率不小于训练时的分辨率,且裁切出的内容能独立成图。
该方法在训练图充足时也是有效的,特别是对于风格学习。推测是因为越贴近原图分辨率的训练图所带有的风格信息越丰富。
裁切图像的有用工具如网站 Birme, Fotor;软件则如 Microsoft Photos, Photoshop。
相似图补充。添加类似风格的其他图像。如用写实填充写实,用油画填充油画。
您也可以使用高质量 AI 图像去填充。
但必须与原图具有相当的相似度,否则将画蛇添足。
一种道听途说的方法是将这些补充图像用作正则化图像参与训练,而不与真正的训练集混淆,即以一个更低的学习率对待它们,而与原风格越不相似的图像,学习率应该越低。
画面损伤
在训练中,SD 模型会敏感地学习到图像上的无用信息,例如污渍、水印、不完整的剑等等,为成品模型带来缺陷。如果您坚持使用这些图像,那么学习如何正确应对这些情况对您至关重要。
除了裁切和手绘修补,我们还可以采用内补绘制来修复图像损伤。现存的内补绘制模型能应对大部分情况。以下是一些常用内补绘制方法和模型:
PS 内容识别填充:PS 自带的的内容识别填充功能将能解决大部分的小范围图像损伤,如去除小型的水印或多余的物体等。该功能识别选区附近的内容来填补所选区域的内容。如果填补的区域周围为纯色或为具有规律的图案纹理的效果更佳;
PS 魔术橡皮擦:同上,为内补绘制算法。更擅长“去除”某个物体。较低版本的 PS 中似乎无此功能;
PS AI:与 PS 内容识别填冲相比,AI 识别填充更胜一筹,效果是所有方法中最好的,且能够修补大范围损伤。缺点是需要付费,且速度较慢;
SD 内补绘制模型:众所周知。速度慢,效果一般,未来可期;
OpenCV:速度快,效果差,需要一定的编程能力。模型对应 Python 的 cv2 库。使用参考见:OpenCV: Image Inpainting
Lama Inpainter:速度适中,效果一般,需要较强的编程能力。推荐用于 批量地重绘某个区域,在给定一批图像时,该区域可设为某个固定区域,或由某个目标检测(如 YOLOv5)或分割模型选择。
除了正确地处理图像损伤,在大量图像中检测它们也非常重要。以下提供一些检测图像内容损伤的思路:
利用脚本检查图像标注:要求一定的编程基础。如果对数据集进行了标注,则可利用脚本检测图像标注中是否存在与内容损伤有关的关键词,例如“watermark”, "signature"等。
利用 AI 技术检测图像内容:要求较强的编程基础。可以使用一些 AI 模型检测图像是否包含内容损伤。如 YOLOv5 等。
图像重复
重复的图像对训练几乎没有影响。可如果您执意想要去重,且具有较强的编程基础,则可以考虑使用本节将介绍的算法思路。
考虑到训练集图像较多,若使用常规的相似度比较算法则会变得复杂且耗时。因此,本节使用 感知哈希算法(Perceptual Hashing Algorithm)。感知哈希算法为每个图像计算一个哈希值,称为 感知哈希(Perceptual Hash),作为图像的“身份证”。相似图像的感知哈希非常接近。不同感知哈希值的差异大小通过汉明距离(Hamming Distance)衡量。缩放图像几乎不会改变感知哈希,而裁切图像会。
一个感知哈希通常可以表示为一个 16 进制的字符串,该字符串可作为哈希表(字典)的键。下文照此思路,提供一个简单的感知哈希去重算法,用于去重一些图像。对于重复图像,该算法删除分辨率较低的一者。
感知哈希去重算法
输入:一些图像
建立一个空的哈希表(字典)
为训练集中的每个图像计算一个感知哈希,以其作为键,图像(或图像文件的路径)作为值,添加到哈希表中。在构造过程中,键冲突的两个图像即为完全重复的图像,保留分辨率高者即可。
注:无需通过计算汉明距离判断图像相似。因为感知哈希的精度受哈希尺寸的影响,所以,我们只需将感知哈希的尺寸由原先的 8 减小至 6,即可模糊化地去重。
1.2 数据标注
标注描述图像的内容。精细、准确、巧妙的标注将极大地助推训练。
一个优秀的标注通常应具有以下标准:
准确:标注用词准确,没有拼写错误;
精细:不多不少,恰好能描述图像中的所有主要对象;
巧妙:标注的方式适配特定任务(细节见下文)。
1.2.1 标注在训练中的作用
文生图模型训练的每张图像对应一个标注。在训练中,使用标注作为提示词让模型生成一张图像,这张生成的图像与训练的图像相似度越低,即误差越大,则模型会朝着减小误差的方向去学习。训练中,模型会去关联提示词和画面中的物体。
基于上面的描述,让我们看看错误的标注对训练的危害。
当标注过于简单,未能涵盖画面中的主要内容时,模型将尝试使用这段简单的标注作为提示词生成图像。但是,由于提示词简单,这张图像的内容将与原图像相差甚远。但是,模型必须强迫自己用简单提示词生成的图像与训练图像对齐。因此,这样训练出的模型将具有使用简单提示词生成复杂图像的能力,可这也容易导致模型生成出多于提示词描述的内容,且在较长提示词下的标签较差。
反之,当标注过于复杂,训练时,生成图像的内容将少于训练图像,多余的标注难以被准确地分配到画面中的特定物体,从而导致语义和内容的偏差过大。
1.2.2 标注的种类
目前社区中的标注大致可分为两类:自然语言标注 和 标签标注。
常见打标器
自然语言打标器
BLIP:传统的打标器,速度快,但内容细节欠缺严重;
GPT3.5:待测试……
(推荐)GPT4V:质量最高,价格最昂贵的打标器。
(推荐)CogVLM:GPT4V的平替,质量高;
标签打标器
(推荐)WaifuTagger:基于 Danbooru 训练的打标模型,适合动漫二次元图像。质量高,标签准确,速度快。
DeepDanbooru:质量一般,速度很快;
自然语言标注常能描述物体的位置关系,而标签标注名词密度大,造价便宜,容易修改和优化。
结合二者的优点,可采用自然语言+标签的标注策略。
1.2.3 如何正确选择标注形式?
包括 Stable Diffusion (SD) 在内的文生图扩散模型有一个臭名昭著的特性——模型会过度服从所训练的标注。举个例子,如果您在模型训练前,在所有数据标注前面添加一个 “$” 符号。那么,当您使用训练出的模型做推理时,如果提示词的第一个字符不是 “$” 符号的话,生成效果将大打折扣。
因此,训练和推理所使用标注的形式都应尽量保持一致。这里,标注的形式例如语法习惯,排布规律,标签的顺序,分隔词、句的方式等等。
在讨论训练应使用何种标注前,理解标注选择的前提是至关重要的。
基于上面的原则,就容易理解目前存在的数据标注误区了。目前,常见的数据标注误区是:
i) “使用下划线分隔标注标签中的单词”。该习惯可能常源自于 Danbooru 的标签习惯。例如,使用 ganyu_(genshin_impact)
而不是 ganyu (genshin impact)
。这是因为,文本编码器 CLIP 模型和训练的基础模型是在空格形式的标注中训练的,而不是下划线形式。因此,空格形式标注的效果通常要优于下划线形式。
ii) “在训练数据的标注中应用括号转义”。转义括号即在标注中出现的括号前添加反斜杠 “\”,以防止括号被解释为加权括号。例如,ganyu \(genshin impact\)
就是一个带转移括号的标注。转移括号应当被用在生成中,而不是在训练中。这是因为,训练脚本通常并不会将训练数据标注中的括号解释为加权,也就不需要转义括号。而如果对括号进行了转义,那么反斜杠和括号就会一同被视为标注的一部分,从而导致训练与预期不匹配。因此,当训练器不将括号解释为提示词加权时,训练数据应使用未经过转义的括号。
下面一个例子,帮助读者更好地理解括号转义的影响。当某炼丹师X使用带转义括号的标注,比如说 ganyu \(genshin impact\)
训练模型时,炼丹师X期望标注送到模型眼力是 ganyu (genshin impact)
,但模型看到的实际上是 ganyu \(genshin impact\)
,即与原标注相同,比预期多了括号。在推理时,炼丹师X使用提示词 ganyu \(genshin impact\)
生成图像,他通过使用转义括号避免加权,他所期望模型看到的提示词和模型实际看到的提示词一致,都是 ganyu (genshin impact)
。然而,模型从训练中学到的正确形式应该是 ganyu \(genshin impact\)
。因此,炼丹师X的提示词并不能正确地发挥作用。
iii) “标签标注总是优于自然语言标注,或反之”。选用自然语言作为标注还是标签作为标注没有绝对的优劣。在对使用自然语言标注的模型进行推理时,应尽量使用自然语言作为标注,反之亦然。
上面的结论只管但有效。除此之外,在训练中,具体标注形式的选择还应该考虑基础模型的影响。这是因为,在训练时,我们衡量训练进度的好坏是通过将数据标注作为提示词输入基础模型后,对比模型输出与原图的差异。换句话说,训练会引导基础模型向数据(图像+标注)的方向靠拢。这么一来,如果数据标注与基础模型习惯的标注形式不同,那么模型会持续认为自己与数据的期望不符。这种情况下的微调不仅会把基础模型的语言习惯“拽”向少量的数据,同时还妨碍了基础模型利用已有知识来学习和关联标注和数据。
因此,训练的标注类型应尽量服从基础模型的提示词习惯。例如,当基础模型偏好自然语言标注时,所选标注也应当是自然语言;当基础模型偏好用下划线分隔标签时,数据标注也应当用下划线分隔标签。然而,当训练数据较多,且训练的是大模型时,则可以使用与基础模型不同的提示词习惯。这是因为,大规模的微调能够大幅调整基础模型的知识,“掰弯”基础模型。
2 训练
2.1 训练器基本
模型训练通过 训练器(trainer)完成,如 kohya-ss 的 sd-scripts。训练器通过用户传入的训练配置(config),加载数据和模型,并服从配置完成模型训练。
这一部分为选读的前置知识,面向刨根问底的读者。业余的模型训练不需要过度学习模型权重更新背后的算法逻辑,只需要对训练器运行的工序有大致认知即可。
大模型训练器包含以下基本组件:
模型(model):所有模型。例如 UNet,文本编码器,VAE等。
数据集(dataset):包含所有训练数据及每个数据的信息。
数据加载器(dataloader):负责在每一步训练时,从数据集中逐个加载数据。掌管 (i) 一次加载多少数据,(ii) 每次加载数据时,对数据做何种后处理,(iii) 以什么样的顺序加载数据。
学习率调度器(learning rate scheduler):在训练中控制学习率的高低。
优化器(optimizer):最重要的组件之一,训练器的心脏。负责更新所训练模型的权重。优化器的选择决定了模型权重更新的底层策略。常见的优化器有 AdamW 和 AdaFactor 等。
训练状态(train state):所有记录训练进度的变量都可以被称作训练状态。训练状态所记录的信息一般包括 loss、当前训练步(step)数、当前训练期(epoch)数等等。
噪声采样器(noise scheduler):负责向训练图像中添加噪声。
2.1.1 训练器的运行步骤
为了方便讲解,我们将一次训练划分为两个阶段,准备阶段和训练阶段。在准备阶段,训练器加载模型和数据集,并为数据集创建加载器,创建学习率调度器、优化器和噪声采样器,最后初始化训练状态。训练阶段是一个重复若干次的循环,每次循环从数据加载器中加载一批数据,并对比模型的生成结果和真实数据,计算误差(loss),朝着真实数据的方向更新一次模型权重。
2.1.2 训练器的组成结构
要准备一个数据集一共有三步,(i) 读取数据,(ii) 数据预处理 和 (iii) 数据分批。
读取数据不仅读取本地(或云端)的图像和标注文本。数据加载还会获取数据的其他信息,或称为元数据(metadata),例如包括图像的分辨率等有用信息。以常见的训练器 sd-scripts 为例,以下数据信息将会被读取或计算:
图像的路径:`/path/to/image.jpg`
图像的标注:`three apples on a white table`
图像的分辨率:`1024x1024`
图像的重复次数:`4`
图像缓存文件的路径:`/path/to/image.npz`
图像的缓存
……
这里的数据预处理是广义的,常见的数据预处理包括按宽高比为图像分桶、移除低分辨率的图像等改善训练的操作。
数据分批即将数据分组,每组的数据数量,即批次大小,相同。另外,数据加载器会通过缩放或裁切的处理,保证每个批次内的图像分辨率相同。
学习率调度器和优化器
学习率调度器负责控制学习率的在训练中的变化,例如,常数调度器保持基准学习率在训练中不变,以配合优化器的自适应功能;余弦调度器让学习率在高低之间来回变换,低学习率时防止模型在最优点之间来回跳动,高学习率时避免陷入局部最小值;而启用预热功能时,学习率会从训练开始线性增长,以防止训练开始时的不稳定。
另外,训练模型学习率绝非固定不变的,其所应该设定的高于低取决于很多因素,例如批次大小、优化器的类型、优化器的超参数等等。
优化器在训练中追踪训练状态和模型参数,并为各个模型参数配置最优的更新策略。不同优化器的显存占用不同,例如 Adam 系列优化器需要保存模型每个参数自训练开始以来的指数移动平均(ema)loss,故显存消耗大。
噪声采样器
噪声采样器记录的是扩散模型中的超参数 beta。在正向扩散中,向图像中添加的噪声只取决于随机高斯噪声、时间步和这些超参数。其中的随机高斯噪声和时间步能直接调用算法生成。因此,只要给定图像、纯噪声、时间步和噪声采样器,就能得到一幅嘈杂的图像。
2.1.3 模型训练
当一切准备就绪时,模型训练即可开始。模型训练循环重复单步训练,直到训练步数达到所设定的最大步数或用户终止训练。
图:模型单步训练流程图。
每一步训练,数据加载器从数据集中加载一批数据。获取图像时,它将这批数据中的所有图像缩放为相同分辨率。
随后,准备所有 UNet 模型的输入—— (i) 随机抽取一个 0~1000 范围内的整数,作为时间步 t;(ii) 随机获取一个标准高斯噪声 ϵ;(ii) VAE 将图像 x_0 编码为潜变量 z_0,噪声采样器基于 t,向该潜变量中添加噪声,得到加噪后的嘈杂潜变量 z_t;(iii) 分词器(tokenizer)将标注文本打散编码为词元,文本编码器将词元编码为词嵌入向量。将时间步 t、嘈杂潜变量 z_t 和词嵌入向量输入 UNet,得到模型预测的纯噪声,ϵ'(或 ϵ_hat),表示图像所对应的纯噪声。
得到模型预测后,将它与刚才获取的标准高斯噪声 ϵ 比较,做 MSE Loss,即对两个矩阵的对应位置作平方差后取平均值,作为 loss。
最后,用该 loss 去更新模型权重,让模型向生成数据图像的方向学习。
2.2 训练器优化
上节,我们介绍了训练器的基本构造和运行过程。真实的训练会采用许多从各方面优化训练的策略,例如,混合精度等等。然而,在社区文章中讨论太过底层的机器学习参数没有意义,因为读者可以在任何地方学习到他们。因此,本文将侧重讨论更加直观且具有启发式的训练优化策略。例如,数据加权、宽高比分桶、缓存潜变量等。
2.2.1 数据加权
在训练中,我们通常不愿意平等对待每一个数据。让我们考虑以下几种情况:
使用正则集:社区对正则集的使用可以追溯到 Dreambooth。为了在已训练的模型上用新数据训练而不让模型遗忘先前训练的知识,需要将旧的数据集作为正则集加入到训练中。正则集帮助模型复习旧知识,在训练中出现的频率应低于训练集,否则训练会过于缓慢,模型也容易过拟合到旧数据集中。
按质量加权:一张高质量的图像理应在训练中被更充分地学习,而一张低质量的图像则相反,甚至应该被排除在数据集之外,这是非常直观的。
对数据配比:假如我们希望同时教授模型多个角色,我们希望图片数量少和图片数量多的角色能在训练后达到相同的拟合程度,那么就应该提高少图角色的权重并削减多图角色的权重。该方法被应用于多概念模型的训练中,例如 Anime Illust Diffusion 等。
这三种情况的宗旨都是对训练数据的合理利用,将计算资源更多地留给那些我们真正希望模型学习的数据。
数据加权是一种直观、简单、高效的利用训练数据的策略。在这里,我们定义“加权”为改变某些数据在训练中的重复次数(number of repeats),从而让他们更多或更少地在学习过程中出现。因此,对一个数据加权的本质其实是拷贝其若干次。
遗憾的是,现有的训练脚本并未实现细化到每个数据的加权策略,而是允许使用者将数据集划分为多个子集,并各自设定重复次数。
本节的剩余部分将启发式地给出三种情况的数据加权算法。
正则集加权算法
正则集加权算法最简单,只需要在训练前将数据集划分为训练集和正则集,并提高训练集数据的重复次数即可。算法:
输入:训练集和正则集
对于每个数据,如果这个数据在训练集中,则将他的重复次数乘以 n,例如 10。
质量加权算法
按质量加权要求在训练前为每个数据指定它的质量,并在训练前根据每个数据的质量提高或降低其重复次数。一种更直观的方式是,提前在数据库中指定每个数据的重复次数。算法:
输入:训练集
对于每个数据,根据其质量设定其重复次数。
数据配比算法
对数据配比要更加复杂,但非常有效,特别是对于那些数据集中的冷门概念的均衡学习。
为了直观地引入数据配比算法,我们举一个具体地例子,假设有一个这样的数据集:数据集中有一共 100 个的不同角色的图片,每个角色的图片数量从 20 到 120 不等。我们计划训练 1 个 epoch。当正常训练时,所有数据的重复次数都是 1,那么每个角色在这个 epoch 内被模型学习的次数等于它的数据数量。
现在,为了保证每个角色的图片在训练中被学习到相同次数,我们规定一个每个角色在一个 epoch 内应当出现次数的基准,例如 100。假设数据最少的角色是 A,有 20 张图,最多的角色是 B,有 120 张图,介于二者之间的是角色 C,有 60 张图。那么,我们设定角色 A 所有数据的重复次数为 100 / 20 = 5 次,角色 B 为 100 / 120 = 5/6 = 0.83 次,角色 C 为 100 / 60 = 5/3 = 1.67 次。
设定完毕后,我们拷贝角色 A 的每个数据 5 份(注意,实际实现不会直接拷贝文件)用于训练。对于角色 B,因为它的重复次数出现了小数,因此我们用概率对待它,即在数据加载器加载到角色 B 的数据时,有 100% - 83% = 17% 的概率将角色 B 的触发词从数据标注中移除。对于角色 C 的小数情况,我们的处理方式为,首先将角色 C 所有数据的重复次数设为 1,而对于每个角色 C 的数据,我们有 67% 的概率将它的重复次数提高 1。
该例子启发式地介绍了数据配比的简单策略,但也存在诸多问题。例如,(i) 使用概率确定重复次数的策略存在偏差,不能精准控制数据重复。其次,(ii) 当一个数据有多个角色时,重复次数计算会出错。
广义的数据配比算法需要统计数据集的信息,并额外(人为地)提供一个概念量基准 n。算法的核心思想是让每个待学习概念在一个 epoch 内被模型学习大约 n 次。对于数据量本身已经超过 n 的数据,则可根据需要,要么保留它,要么丢弃它,要么无效化数据中的概念,例如从它的标注中移除这个概念的触发词(具体方法见第 1.2.4 节)。数据配比算法简述如下:
输入:(i) 训练集,其中有多个待学习的概念(如角色);(ii) 数据基准 n。
对于每个待学习的概念 ,统计其数据数量;
对于每个数据,设其最低重复次数为 1。对于数据中的每个待学习概念,提高其重复次数
n / 这一概念的数据数量
次。当计算结果不是整数或小于 1 时,特殊处理之。
2.2.2 分桶图像
自动宽高比分桶,Aspect Ratio Bucket,简称 ARB,是一项 允许不同宽高比分辨率的图像存在于同一数据集中进行训练 的技术。
在机器学习中,模型每批输入的图像必须具有相同的分辨率,所以训练器必须实现一个能将不同分辨率图像各自分组的算法,即自动宽高比分桶算法。算法将宽高比接近的图像分组到一个被称为“桶(bucket)”的结构中,各桶分别训练。
至今为止,ARB 百利而无一害。它使图像无需经过裁切而得以完整地投入训练,不仅缓解了由于图像裁剪导致的畸形问题(例如断剑、人体畸形、裁切不良等),还节省了裁图的工程量。目前为止无明显缺点。
图 1:SDXL1.0 模型训练中所使用的 ARB 分桶表。他们获得了很棒的结果。
SDXL 架构的模型将训练图像的分辨率作为额外的特征来训练模型,其中使用了位置编码(positional encoding)技术。这导致 SDXL 在生成陌生分辨率的图像时表现较差。因此,在使用 SDXL 架构的模型进行推理时,需要尽量使用模型所支持的分辨率。关于位置编码的介绍请参见后文。
分桶算法
分桶算法的基本思想是,在不过度破坏原图比例的前提下,将原图缩小至某个特定的像素量。这里定义:
图像的像素量 = 图像的宽 x 图像的高
在 ARB 中,每个“桶”是一个 特定的分辨率,例如 512x768,所有训练集中分辨率最接近 512x768 的图像都会被扔进该桶。下文将从易到难地介绍分桶算法的实现。
分桶算法的设计必须遵从以下几个前提:
控制桶数:桶的数量不能过多,也不能过少;
比例不变:缩放后的图像与原图比例差距不能过大;
尺寸一致:所有缩放图像的像素量需要相近;
不能放大:不能放大图像。
对于前提1,桶过多,每个桶中图像少,其中多余不足一个批次的图像将以更低的批次大小训练,从而干扰训练且拖慢训练速度;桶过少,每个图像可供选择的宽高比变少,有的图像不得不经过不良缩放或裁切以适应桶的尺寸。经验法则是,训练图像越多,桶可以越多,反之。
分桶算法一
输入:最大分辨率 (max_w x max_h),一些图像
对于每张图像,将图像 等比缩放 至像素量恰好不超过预设的最大分辨率后,按照将缩放后图像的分辨率,将它归入桶中。
分桶算法一简单地把图像缩放到像素量恰好不超过预设的最大分辨率。它不会损伤原图的比例,但需要大量的桶,来承载各种比例的图像,这违背了前提1。
对此,我们优化分桶算法一,减少桶的数量。我们让输出图像的宽和高均能被某个整数 d 整除。这里,d 常为 32 或 64。
分桶算法二
输入:最大分辨率 (max_w x max_h),一些图像,除数 d
对于每张图像:(i) 首先计算出像素量恰好等于最大分辨率,且宽高比等于该图像分辨率的尺寸,记为宽 w 和高 h。(ii) 之后,逐渐减小 w 和 h 至恰好能被 d 整除,记该衰减后的宽和高为 w' 和 h'。(ii) 最后,将图像缩放至 w' x h',并放入桶 w' x h' 中。
分桶算法二将宽高比相近而非相同的图像划入同一个桶。
在此基础上,我们添加不可放大图像的约束,即分桶算法三。
分桶算法三
输入:最大分辨率 (max_w x max_h),一些图像,除数 d
对于每张图像,在分桶算法二的基础上,额外判断原始图像的像素量是否已经小于最大分辨率,若是,则跳过算法二的步骤 (i),直接将 w 和 h 设为原始图像的尺寸。
在特定情况下,例如训练使用了位置编码的模型,或是想要复刻某个模型的分桶,我们希望图像仅被分入固定的桶中,以此提出分桶算法四。
分桶算法四
输入:一些桶,一些图像,除数 d
定义宽高比为 宽除高,则每张图像和桶均具有一个宽高比。分桶时,对于每张图像,为它匹配 宽高比最接近的桶 并放入。如果图像分辨率不匹配,则将图像缩放为桶的分辨率后再放入。
Kohya 系训练脚本是一种稍微更复杂的分桶算法。博主 @青龙圣者 和 @秋叶aaaki 的训练脚本均属此类。这里将介绍帮助读者手算的简化算法,原版的算法在计算细节上有优化。
Kohya 的分桶算法 - 简化版
输入:最大分辨率 (max_w x max_h),一些图像,除数 d
记 max_area = max_w x max_h。
对于每张图像,如果该图像的宽 img_w 乘高 img_h 大于 max_area,记 img_area = img_w x img_h,那么该图像将 能被 d 整除地 被分入桶 (
img_w × (max_area/img_area)
xheight × (max_area/area)
) 之中。如果该图像的像素量小于 max_area,则该图像将 能被 d 整除地分入桶
img_w
×img_h
之中。
注:此处的分桶算法为禁用 upscale 的情况。这种情况下,另两个分桶算法的输入,即最大分辨率 max_resolution 和最小分辨率 min_resolution,其实不会产生实际效果。
有代码基础的读者可尝试用类似的算法模拟自己所用脚本的分桶方式,以提前得知图像经过处理后的状态,检查是否有不良缩放和裁切。
2.2.3 缓存潜变量
缓存潜变量,cache latents,是以内存换时间的训练提速方式。SD 模型最终训练的是潜变量,潜变量由 VAE 编码图像而得到的,可以简单地认为潜变量为 缩小原始图像至原来的 1/8 得到的压缩图像(具体缩放倍数取决于 VAE)。
在不缓存潜变量时,每一步训练都需要 VAE 解码训练图像来获得潜变量图像,非常耗时。将解码后的潜变量预先存储起来,方便训练期间调用即缓存潜变量。存储这些潜变量需要额外的显存开销,因此这一技术是以内存换取时间。
普通的缓存潜变量仅在运行期间单次有效。因此,将潜变量保存为本地文件后,后续训练中便无需再次缓存。
注:缓存潜变量的优化策略
sd-scripts 似乎缺少高效的潜变量缓存策略,例如,多卡并行的缓存。以下是一些似乎可以优化的方案:
支持 多卡并行 缓存;
支持 单独地 缓存潜变量,而非在训练脚本开始后才缓存——不需要将整个模型全部加载;
直接将缓存文件直接 视为图像数据 加载,而不是先加载图像,再加载潜变量缓存;
在缓存潜变量时,将缓存结果 异步地 写入硬盘;
将潜变量的加载后移至数据 第一次被需要时,而非训练开始时全部加载——防止在花费长时间去加载大数据集的潜变量后,训练一开始就报错重来。
2.2.4 标注随机化算法
当一个数据被数据加载器加载时,训练器可以对数据做一些小修改。这么做的好处是向数据中添加随机性,数据每次被加载时都会与往次产生略微不同,从而多样化整个数据集,提高模型的鲁棒性(即抗干扰能力)。常见的数据随机化策略有如随机水平翻转图像、随机丢弃若干标签、标签洗牌等。
在实际训练中,能对图像做的随机化十分有限,因为图像有其严格对应的标注。但图像的标注却有很大操作空间。这是因为语言对图像的描述是多样的。例如,原神角色甘雨不仅能用 ganyu (genshin impact)
表示,也能用 1girl, blue hair, horns
描述(如果你不希望显式地描述甘雨的话)。而当你希望仅仅使用 ganyu (genshin impact)
来生成甘雨时,保留 blue hair, horns
则显得多余,因为描述甘雨时,其实已经包含了蓝发和角的意思。因此,训练角色的经验法则认为,训练角色时应该用一个角色标识符(例如角色名)来表示角色,而删除标注中直接描述角色核心特征的标注。然而,在训练中,如果二者同时存在,那么图像中有关甘雨的信息会被分散地关联到 ganyu (genshin impact)
、 blue hair
和 horns
中,导致模型若要生成甘雨,就需要再提示词中同时加上 ganyu (genshin impact), blue hair, horns
。而要是训练时只用 ganyu (genshin impact)
而完全去除 blue hair, horns
,那当生成甘雨时加上了 blue hair, horns
反而会不像。
在一些特定情况中,标注随机化的好处不仅是提高模型鲁棒性,还能配合其他训练策略来动态地处理数据。例如,对于第 2.2.1 节中需要辍学太热门角色的情况,则可以通过移除标注中的角色标识符实现。
本节将讨论各种标签随机化算法及其影响,旨在抛砖引玉地为读者带来启发式思考。
TODO
2.3 高级参数
请转至文章:[硬核] 扩散模型训练和微调的高级参数讲解 | Civitai
3 社区技术
3.1 提示词加权
提示词加权(weighted prompt)即使用扩号来提高某段提示词的权重。例如 male focus of (ironman:1.3)
将 ironman
的权重提高到了原本的 1.3 倍。不过,加权一段提示词也相当于变相地降权其他提示词。
提示词加权的发生在提示词处理时。通常,提示词从文本在输入模型前会经过分词器和文本编码器的处理。具体地,一段文本提示词会首先被分词器翻译为词元(tokens),词元再被文本编码器转换为文本嵌入(text embedding)以输入模型。
提示词加权方式作用于词元转文本嵌入阶段。原先,所有词元具有相同的权重(1.0),而加权算法会将解析出需要加权的词元们乘以对应的权重(例如,ironman
的权重由1.0变为1.3),之后再通过计算,保证加权前后所有词元权重的平均值相等。
3.2 生成时的实时预览
在图像生成过程中,我们无法直接观测到潜变量。只有在经过 VAE 的解码后,我们才能查看图像内容。VAE 的解码大约需要 2~3 个迭代步数的时间,每步迭代都直接使用 VAE 去解码潜变量显然是不合适的。
WebUI 预览生成图像的过程使用了近似 VAE 解码技术。该技术使用一个微型的 VAE,称为 VAE-Approx,快速解码生成过程的潜变量为质量较低的图像以供预览。
4 文本嵌入
一个文本嵌入(英文名 text embedding,下文简称 emb)模型装载了某个文本概念。它诱导 SD 模型生成该概念的内容。如上所述,一段文本通常会经过分词器变为词元,再经过文本编码器变为文本嵌入,最后输入模型网络,即UNET。一个 emb 文件装在的就是输入模型前的那段文本嵌入。因此,emb 无法生成那些 SD 模型本身不知道的内容。
本节将介绍以下有关文本嵌入(Text Embedding)的内容:
负面文本嵌入
.pt 和 .safetensors 的相互转换
SD1.5 和 SDXL 文本嵌入的相互转换
为了方便,以下文本嵌入简称为 emb。
4.1 负面文本嵌入
负面文本嵌入,简称 负面 emb 或 neg emb,是正常 emb 的逆向使用。但如今,负面 emb 的意义已凌驾于正常 emb 之上。广为人知的负面 emb 如 EasyNegative 和 badhandv4 等。
负面 emb 的思路非常直观,虽然人们难以教会模型关于“好”的概念,因为“好”的定义相对模糊,但“坏”的定义更为具体且容易获取,例如畸变、伪影、低分辨率等。因此,负面 emb 的职责就是告诉模型什么是糟糕的——类似施肥。
本节将介绍负面 emb 的炼制过程和基于经验的推荐参数。
4.1.1 生成数据集:挑选工业原料
既然要教会模型什么是“糟糕透顶”的,就需要准备丑陋至极,不堪入目的训练集。那么,什么才是所谓的“丑陋至极,不堪入目”呢?根据经验法则,满足以下条件的图像/训练集最有利于训练:
为了避免干扰正常概念,图像应基本不具备任何正常的概念。例如,大量的 画面损毁,人体畸变 等。
为了不改变风格,且使 emb 模型有足够的的泛化能力,训练集图像应具有所训练概念之下的各种风格、色彩、对比度、亮度、细节复杂程度、构图、人物动作。
训练图像越多越好。通常来说,应多于 500 张。
训练图像可直接由 SD 模型生成。为了使生成的图像尽可能“难看”,可以选用 SD1.5 类型下较差的模型,并在提示词中填写如“low quality, worst quality, deformity, bad anatomy, distorted fingers”等提示词,同时调低采样步数,换用效果差的采样器等。
为了使训练集多样,您可以在不同的提示词、采样步数、CFG 等参数下生成尽量多的图像。
您可以人工地“破坏图像”,例如,常用的生成分辨率是 512x512。可若要训练“低分辨率”的概念,则可生成低于 512x512 分辨率的图像后再放大为 512x512,即可制作低分图。
发挥您的作图技巧,生成最丑陋,最不堪入目的图像吧!
4.1.2 训练模型:制作化肥
训练负面 emb 模型与训练一般 emb 模型类似,可以为数据集打上标签。向量数设置为 2~16 左右。初始化词元可以置空,也可以填写为所训练概念,但要注意所填写的内容所占用的词元(token)数应与 emb 模型的向量数匹配,您可以通过一些工具来查看某一句话的词元数,例如 WebUI 的 tokenizer 扩展等。
按照经验,负面 emb 应以更高的学习率训练至过拟合,以免泛化过多而误伤其他概念。通常,起始学习率应介于 1e-3~1e-4 之间(即 0.001~0.0001),终止学习率则介于 1e-5~1e-6 之间。训练至少 1000 步,直至 loss 收敛。
4.1.3 模型评估:肥料的好坏
一个好的负面 emb 的应做到如下几点:
有效:修复了所训练的负面概念;
不越俎代庖:不误伤其他正常的概念;
小而美:不占用过多词元数;
泛化:在各种 SD 模型上均有不错的泛化能力(较难实现);
4.2 模型格式的转换
TODO
4.3 模型种类的转换
如今,基于 SDXL 的 emb 模型(简称为 embXL)数量较少,而 SD1.5 具有很多优秀的 emb 模型(简称为 emb1.5)。一个 embXL 包括 CLIP_G 权重和 CLIP_L 权重两部分,其中,CLIP_L 的权重与 emb1.5 具有相同的形状。也就是说,embXL 的其中一部分是 emb1.5 的结构。这使得二者之间相互转化成为可能。
emb1.5 转 embXL
由于 SDXL 具有两个文本编码器,无法完美地将 SD1.5 的 emb 模型转移到 SDXL 上。但是,转移相同部分是可行的。方法是,拷贝 emb1.5 中的权重到 embXL 的 CLIP_L 权重上,并用零来补全 CLIP_G 的权重。
embXL 转 emb1.5
embXL 理论上能够无损地转换为 emb1.5。方法是,提取 embXL 中 CLIP_L 上的权重,将其原封不动地拷贝到 emb1.5 上,即可完成转换。
5 模型融合
5.1 模型权重的储存
AI 模型的权重通常储存在一个哈希表(字典) state_dict 中。state_dict 的键为权重名称,值为权重。按照某一比例混合 state_dict 各层的模型权重即为模型融合。
SD 1.5 模型权重储存格式
在 state_dict 中,文本编码器的权重储存在含有 cond_stage_model.transformer.text_model
的键之中,共197个这样的键。
UNET 的权重储存在含有 model.diffusion_model
的键之中,共有686个这样的键。其中,IN 层为 model.diffusion_model.input_blocks
,共248个;OUT 层为model.diffusion_model.output_blocks
,共384个;MID 层为 ,model.diffusion_model.middle_block
共46个。
VAE 储存在含有 first_stage_model
的键之中,共有248个这样的键。
SD XL 模型权重储存格式
对于 SDXL,含有 conditioner.embedders.0.transformer.text_model
的键对应文本编码器 CLIP_L,即 SD1.5 中的文本编码器,共有197个这样的键。含有 conditioner.embedders.1.model.transformer
的键对应 CLIP_G,共有 385 个这样的键。
UNET 的权重的储存格式与 SD1.5 模型相同,IN, OUT, MID 层各有 574, 868, 226 个键。
VAE 储存在含有 first_stage_model
的键之中,共有248个这样的键。
5.1.1 模型的加载和保存
本节将演示在本地使用python简单地加载和保存一个.safetensors模型。鉴于目前大部分模型都被封装为 .ckpt 或 .safetensors 格式,本节将不针对散装pytorch模型。
模型的加载和保存非常容易,只需使用safetensors包即可。您可以通过在终端输入 pip install safetensors
来安装。以 safetensors 格式的模型为例,以下代码实现了模型权重的加载和保存:
# 模型的加载和保存
from safetensors.torch import load_file, save_file
model_path = "Path/to/your/model.safetensors" # 设置模型路径
state_dict = load_file(model_path) # 加载模型文件为 state dict
save_file(state_dict, "Path/to/save/your/model.safetensors") # 保存模
若有需要,您亦可将模型储存为其他易读形式,例如,将模型的键储存为 json 格式以观察模型结构。
5.2 模型融合
模型融合分为三个步骤:(i) 加载模型的 state_dict;(ii) 融合对应键的值,即权重;(iii) 保存模型。本节将详细介绍如何实现模型融合。
5.2.2 分层融合算法
给定需要融合的层,在加载两个模型的 state_dict 后,我们只需按照对应层的键值,依次提取出模型在该层的权重,并进行融合即可。以下模型算法实现了将两个模型 A 和 B 按照一定比例融合。比例越大,融合结果越偏向于模型 B。
模型融合算法一
输入:(i) 两个模型的 state_dict
sd_A
和sd_B
;(ii) 融合比例 alpha 和 (iii) 待融合的部分;定义融合后的新模型为 sd_M,首先将模型 A 的权重复制到 M 上。
对于每个待融合的层,以它为键,在 sd_A 和 sd_B 中找出对应层的权重 w_A 和 w_B,计算 w_M = w_A × (1-alpha) + w_B × alpha。之后,将 w_M 赋值给模型 sd_M 的对应层。
输出 sd_M。
这样,我们就完成了简单的分层融合。下文将介绍一些进阶的模型融合算法,例如 SuperMerger 插件中的 cosine_A/B、TrainDifference 等。这些融合算法有助于平滑融合曲线,使结果模型的权重不过度颠簸。
5.2.3 复杂融合算法
TODO
5.3 模型融合的经验法则
无论是哪种模型,融合哪一层,对模型出图的影响都是牵一发而动全身的。扩散模型的生成方式是一步步地去除图像中的噪声,每一步都依赖于前一步的输出,类似蝴蝶效应。扩散开始时,图像嘈杂不堪,仅具有图像的模糊结构。但模型会尽可能地利用这些含糊不清的嘈杂信息去推测具体的图像。例如,将直线解释为剑,将圆圈解释为篮球。
5.3.1 SDXL
首先,让我们复习 SDXL 的结构。SDXL 为 UNet 架构,搭配两个 CLIP 文本编码器。UNet 共九个 IN 层(IN00~IN08),一个 MID 层(MID)和九个 OUT 层(OUT08~OUT00)。IN 和 OUT 层每三个分为一组。即:
IN 层
上层 IN00~IN02
中层 IN03~IN05
下层 IN06~IN08
OUT 层(IN 层顺序相反)
下层 OUT00~OUT02
中层 OUT03~OUT05
上层 OUT06~OUT08
当图像输入 UNet 时,它依次经过层的顺序为:IN 的上 -> 中 -> 下层 -> MID 层 -> OUT 的下 -> 中 -> 上层。IN 层也被称为下采样层,因为图像每经过一(大)层,尺寸就会被缩小为原来的一半,一共缩小三次,变为原来的 1/8;OUT 层则被称为上采样层,图像没经过其一(大)层,尺寸会被放大为原来的两倍,一共放大三次,尺寸从原来的 1/8 放大到原始尺寸。
此外,每一个 IN 层的输出会输入到对应 OUT 层,例如,IN00 的输出会成为将来 OUT08 的输入。
根据社区经验,SDXL 模型的 IN 层与图像构图关系较大,OUT 层与图像渲染关系较大。这一行为类似于用 IN 层搭建画面的基本结构,再用 OUT 层临摹上色。另外,不管是 IN 还是 OUT 层,层数越向下,该层对画面的影响越宏观,反之。例如 IN 上层对构图影响更细致,例如线条结构;而 IN 下层影响整体构图,例如人物姿势、动作等。
TODO