#Stable Diffusion13
Stable Diffusion 在图像生成领域的知名度不亚于对话大模型中的 ChatGPT。其能够在几十秒内为任何给定的输入文本创建逼真图像。由于 Stable Diffusion 的参数量超过 10 亿,并且由于设备上的计算和内存资源有限,因而这种模型主要运行在云端。
在没有精心设计和实施的情况下,在设备上运行这些模型可能会导致延迟增加,这是由于迭代降噪过程和内存消耗过多造成的。
如何在设备端运行 Stable Diffusion 引起了大家的研究兴趣,此前,有研究者开发了一个应用程序,该应用在 iPhone 14 Pro 上使用 Stable Diffusion 生成图片仅需一分钟,使用大约 2GiB 的应用内存。
此前苹果也对此做了一些优化,他们在 iPhone、iPad、Mac 等设备上,半分钟就能生成一张分辨率 512x512 的图像。高通紧随其后,在安卓手机端运行 Stable Diffusion v1.5 ,不到 15 秒生成分辨率 512x512 的图像。
近日,谷歌发表的一篇论文中《 Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations 》,他们实现了在 GPU 驱动的设备上运行 Stable Diffusion 1.4 ,达到 SOTA 推理延迟性能(在三星 S23 Ultra 上,通过 20 次迭代生成 512 × 512 的图像仅需 11.5 秒)。此外,该研究不是只针对一种设备;相反,它是一种通用方法,适用于改进所有潜在扩散模型。
在没有数据连接或云服务器的情况下,这项研究为在手机上本地运行生成 AI 开辟了许多可能性。Stable Diffusion 去年秋天才发布,今天已经可以塞进设备运行,可见这个领域发展速度有多快。
论文地址:https://arxiv.org/pdf/2304.11267.pdf
为了达到这一生成速度,谷歌提出了一些优化建议,下面我们看看谷歌是如何优化的。
方法介绍
该研究旨在提出优化方法来提高大型扩散模型文生图的速度,其中针对 Stable Diffusion 提出一些优化建议,这些优化建议也适用于其他大型扩散模型。
首先来看一下 Stable Diffusion 的主要组成部分,包括:文本嵌入器(text embedder)、噪声生成(noise generation)、去噪神经网络(denoising neural network)和图像解码器(image decoder,如下图 1 所示。
然后我们具体看一下该研究提出的三种优化方法。
专用内核:Group Norm 和 GELU
组归一化(GN)方法的工作原理是将特征图的通道(channel)划分为更小的组,并独立地对每个组进行归一化,从而使 GN 对批大小的依赖性降低,更适合各种批大小和网络架构。该研究没有按顺序执行 reshape、取均值、求方差、归一化这些操作,而是设计了一个独特的 GPU shader 形式的内核,它可以在一个 GPU 命令中执行所有这些操作,而无需任何中间张量(tensor)。
高斯误差线性单元(GELU)作为常用的模型激活函数,包含大量数值计算,例如乘法、加法和高斯误差函数。该研究用一个专用的 shader 来整合这些数值计算及其伴随的 split 和乘法操作,使它们能够在单个 AI 作画调用中执行。
提高注意力模块的效率
Stable Diffusion 中的文本到图像 transformer 有助于对条件分布进行建模,这对于文本到图像生成任务至关重要。然而,由于内存复杂性和时间复杂度,自 / 交叉注意力机制在处理长序列时遇到了困难。基于此,该研究提出两种优化方法,以缓解计算瓶颈。
一方面,为了避免在大矩阵上执行整个 softmax 计算,该研究使用一个 GPU shader 来减少运算操作,大大减少了中间张量的内存占用和整体延迟,具体方法如下图 2 所示。
另一方面,该研究采用 FlashAttention [7] 这种 IO 感知的精确注意力算法,使得高带宽内存(HBM)的访问次数少于标准注意力机制,提高了整体效率
Winograd 卷积
Winograd 卷积将卷积运算转换为一系列矩阵乘法。这种方法可以减少许多乘法运算,提高计算效率。但是,这样一来也会增加内存消耗和数字错误,特别是在使用较大的 tile 时。
Stable Diffusion 的主干在很大程度上依赖于 3×3 卷积层,尤其是在图像解码器中,它们占了 90% 。该研究对这一现象进行了深入分析,以探索在 3 × 3 内核卷积上使用不同 tile 大小的 Winograd 的潜在好处。研究发现 4 × 4 的 tile 大小最佳,因为它在计算效率和内存利用率之间提供了最佳平衡。
实验
该研究在各种设备上进行了基准测试:三星 S23 Ultra(Adreno 740)和 iPhone 14 Pro Max(A16)。基准测试结果如下表 1 所示:
很明显,随着每个优化被激活,延迟逐渐减少(可理解为生成图像时间减少)。具体而言,与基线相比:在三星 S23 Ultra 延迟减少 52.2%;iPhone 14 Pro Max 延迟减少 32.9%。此外,该研究还对三星 S23 Ultra 端到端延迟进行评估,在 20 个去噪迭代 step 内,生成 512 × 512 像素图像,不到 12 秒就达到 SOTA 结果。
小型设备可以运行自己的生成式人工智能模型,这对未来意味着什么?我们可以期待一波。
#Stable Diffusion XL 1.0
之前是0.9
Stable Diffusion XL 1.0 闪亮登场,给你不一样的色彩体验。
在大模型开启的 AIGC 时代,由明星 AI 初创公司 Stability AI 打造的文本到图像生成模型 Stable Diffusion 可谓风靡全球。
虽然从文本到图像的生成模型并不少,但 Stable Diffusion 是最受欢迎的开源模型。各路开发者也基于 Stable Diffusion 模型进行二创,推出各种各样、花式繁多的 AIGC 应用。
刚刚,Stability AI 正式推出了 Stable Diffusion XL(SDXL)1.0。文本到图像生成模型,又完成了进化过程中的一次重要迭代。
这是 Stability AI 最新的旗舰图像模型,也是当前图像生成领域最好的开源模型。
在 SDXL 1.0 版本发布之前,Stability AI 在六月份推出的 SDXL 0.9 仅作研究用途。但从今天起,SDXL 1.0 将通过 Stability AI 的 API 开源开放给开发者,普通人也可以通过消费级应用 Clipdrop 和 DreamStudio 访问。
如何马上体验到 SDXL 1.0?目前有几种渠道:
- Clipdrop:https://clipdrop.co/stable-diffusion
- DreamStudio:https://dreamstudio.ai/generate
- Stability AI Platform:https://platform.stability.ai/
- Github:https://github.com/Stability-AI/generative-models
此外,鉴于与亚马逊的密切合作关系,SDXL 1.0 已经登陆亚马逊云科技的基础模型托管服务 Amazon Bedrock,而且还会登陆 Amazon SageMaker JumpStart。
SDXL 1.0,什么进化了?
1. 生成概念与风格更具挑战的作品
SDXL 1.0 几乎能够生成任何艺术风格的高质量图像,并且是实现逼真效果的最佳开源模型。用户可以在没有任何特定的「灵感」的情况下进行提示,确保风格的绝对自由,生成各具特色的图像。SDXL 1.0 在色彩的鲜艳度和准确度方面做了很好的调整,对比度、光线和阴影都比上一代更好,并全部采用原生 1024x1024 分辨率。
除此之外,SDXL 1.0 可以生成图像模型难以渲染的概念,例如手、文本以及空间的排列。
图源:Stability AI 官网
2. 语言更简洁、更智能
SDXL 1.0 只需几句话就能创建复杂、细致、美观的图像。用户不再需要用华丽的辞藻进行修饰就能够获得想要的高质量图像。
它甚至能够区分「The Red Square」(红场,一个著名景点)和「red square」(红色正方形)。
3. 微调和高级控制
有了 SDXL 1.0 ,根据自定义数据对模型进行微调比以往任何时候都要容易。可生成自定义 LoRA 或检查点,减少数据处理的需要。Stability AI 正在利用专门用于 SDXL 的 T2I / ControlNet 构建下一代任务特定结构、样式和组成控制。这些功能目前处于测试预览阶段,可以随时关注微调的更新。SDXL 1.0 上的图像控制也即将推出。
自 4 月 13 日发布 SDXL beta 版本以来,ClipDrop 用户已经使用该模型生成超过 3500 万张图片,而 Stability AI 的 Discord 社区平均每天生成 2 万张图片。
看看用户使用 SDXL 1.0 的效果吧。
推特:@pratzlowcode
从网友的图片生成效果来看,SDXL 1.0 在光线上把握更加精准,光线效果、色彩对比更加真实。
推特:@foxtrotfrog
可以看到,在色彩的运用上,SDXL 1.0 更加大胆,饱和度较高的情况下光线与阴影的细节依然在线。
推特:@ai_for_success
在这张图片中可以看到,景深较为合适,主体突出,色彩之间的过渡,甚至真实度都十分优秀。
网友制作了 SDXL 0.9 与 SDXL 1.0 的对比图,左边为 SDXL 0.9 生成图像,右边为 SDXL 1.0 生成图像。可以明显看出二者对于色彩运用的差距。SDXL 1.0 的色彩更加丰富且真实,光效表现也更加出色。
也有网友在 SDXL 1.0 中使用 Midjourney 中的旧 prompt,评论中纷纷感叹,它的效果不比 Midjourney 差,甚至有些出乎意料的优秀。
SDXL 1.0 细节:Base 和 Refiner 模型
Stability AI 将 SDXL 1.0 与各种模型进行了对比测试:与 Stable Diffusion1.5/2.1、SDXL 0.9 等模型相比,人们更喜欢由 SDXL 1.0 生成的图像。
SDXL 1.0 也是所有开放式图像模型中参数量最多的模型之一,它建立在一个创新的新架构上,由一个 35 亿参数的基础模型和一个 66 亿参数的细化模型组成。
完整模型包括一个用于潜在扩散的专家混合管道:第一步,基础模型生成(噪声)潜在变量,然后用专门用于最终去噪步骤的细化模型对其进行进一步处理。
基础模型也可以作为独立模块使用。细化模型为基础模型的输出添加更精确的颜色、更高的对比度和更精细的细节。
这种两阶段架构可确保图像生成的稳健性,而且不会影响速度或需要过多的计算资源。SDXL 1.0 可在配备 8GB VRAM 的消费级 GPU 或随时可用的云实例上有效运行。
- 论文地址:https://arxiv.org/pdf/2307.01952.pdf
- 代码地址:https://github.com/Stability-AI/generative-models
#Stable Diffusion XL Turbo
使用一块 A100,出图的延迟只有 200 毫秒。SDXL Turbo、LCM相继发布,AI画图进入实时生成时代:字打多快,出图就有多快
本周二,Stability AI 推出了新一代图像合成模型 Stable Diffusion XL Turbo,引发了一片叫好。人们纷纷表示,图像到文本生成从来没有这么轻松。
你可以不需要其他操作,只用在文本框中输入你的想法,SDXL Turbo 就能够迅速响应,生成对应内容。一边输入,一边生成,内容增加、减少,丝毫不影响它的速度。
你还可以根据已有的图像,更加精细地完成创作。手中只需要拿一张白纸,告诉 SDXL Turbo 你想要一只白猫,字还没打完,小白猫就已经在你的手中了。
你还可以根据已有的图像,更加精细地完成创作。手中只需要拿一张白纸,告诉 SDXL Turbo 你想要一只白猫,字还没打完,小白猫就已经在你的手中了。
SDXL Turbo 模型的速度达到了近乎「实时」的程度,让人不禁开始畅想:图像生成模型是不是可以干些其他事了。
有人直接连着游戏,获得了 2fps 的风格迁移画面:
据官方博客介绍,在 A100 上,SDXL Turbo 可在 207 毫秒内生成 512x512 图像(即时编码 + 单个去噪步骤 + 解码,fp16),其中单个 UNet 前向评估占用了 67 毫秒。
如此,我们可以判断,文生图已经进入「实时」时代。
这样的「即时生成」效率,与前不久爆火的清华 LCM 模型看起来有些相似,但是它们背后的技术内容却有所不同。Stability 在同期发布的一篇研究论文中详细介绍了该模型的内部工作原理。该研究重点提出了一种名为对抗扩散蒸馏(Adversarial Diffusion Distillation,ADD)的技术。SDXL Turbo 声称的优势之一是它与生成对抗网络(GAN)的相似性,特别是在生成单步图像输出方面。
论文地址:https://static1.squarespace.com/static/6213c340453c3f502425776e/t/65663480a92fba51d0e1023f/1701197769659/adversarial_diffusion_distillation.pdf
论文细节
简单来说,对抗扩散蒸馏是一种通用方法,可将预训练扩散模型的推理步数量减少到 1-4 个采样步,同时保持高采样保真度,并有可能进一步提高模型的整体性能。
为此,研究者引入了两个训练目标的组合:(i)对抗损失和(ii)与 SDS 相对应的蒸馏损失。对抗损失迫使模型在每次前向传递时直接生成位于真实图像流形上的样本,避免了其他蒸馏方法中常见的模糊和其他伪影。蒸馏损失使用另一个预训练(且固定)的 扩散模型作为教师,有效利用其广泛知识,并保留在大型扩散模型中观察到的强组合性。在推理过程中,研究者未使用无分类器指导,进一步减少了内存需求。他们保留了模型通过迭代细化来改进结果的能力,这比之前基于 GAN 的单步方法具有优势。
训练步骤如图 2 所示:
表 1 介绍了消融实验的结果,主要结论如下:
接下来是与其他 SOTA 模型的对比,此处研究者没有采用自动化指标,而是选择了更加可靠的用户偏好评估方法,目标是评估 prompt 遵循情况和整体图像。
实验通过使用相同的 prompt 生成输出来比较多个不同的模型变体(StyleGAN-T++、OpenMUSE、IF-XL、SDXL 和 LCM-XL)。在盲测中,SDXL Turbo 以单步击败 LCM-XL 的 4 步配置,并且仅用 4 步击败 SDXL 的 50 步配置。通过这些结果,可以看到 SDXL Turbo 的性能优于最先进的 multi-step 模型,其计算要求显著降低,而无需牺牲图像质量。
图 7 可视化了有关推理速度的 ELO 分数。
表 2 比较了使用相同基础模型的不同 few-step 采样和蒸馏方法。结果显示,ADD 的性能优于所有其他方法,包括 8 步的标准 DPM 求解器。
作为定量实验结果的补充,论文也展示了部分定性实验结果,展示了 ADD-XL 在初始样本基础上的改进能力。图 3 将 ADD-XL(1 step)与 few-step 方案中当前最佳基线进行了比较。图 4 介绍了 ADD-XL 的迭代采样过程。图 8 将 ADD-XL 与其教师模型 SDXL-Base 进行了直接比较。正如用户研究所示,ADD-XL 在质量和 prompt 对齐方面都优于教师模型。
#Stable Video Diffusion
Stability AI 的视频生成模型看来效果不错。
AI 画图的著名公司 Stability AI,终于入局 AI 生成视频了。
本周二,基于 Stable Diffusion 的视频生成模型 Stable Video Diffusion 来了,AI 社区马上开始了热议。
很多人都表示「我们终于等到了」。
项目地址:https://github.com/Stability-AI/generative-models
现在,你可以基于原有的静止图像来生成一段几秒钟的视频。
基于 Stability AI 原有的 Stable Diffusion 文生图模型,Stable Video Diffusion 成为了开源或已商业行列中为数不多的视频生成模型之一。
但目前还不是所有人都可以使用,Stable Video Diffusion 已经开放了用户候补名单注册(https://stability.ai/contact)。
据介绍,Stable Video Diffusion 可以轻松适应各种下游任务,包括通过对多视图数据集进行微调从单个图像进行多视图合成。Stability AI 表示,正在计划建立和扩展这个基础的各种模型,类似于围绕 stable diffusion 建立的生态系统。
Stable Video Diffusion 以两种图像到视频模型的形式发布,能够以每秒 3 到 30 帧之间的可定制帧速率生成 14 和 25 帧的视频。
在外部评估中,Stability AI 证实这些模型超越了用户偏好研究中领先的闭源模型:
Stability AI 强调,Stable Video Diffusion 现阶段不适用于现实世界或直接的商业应用,后续将根据用户对安全和质量的见解和反馈完善该模型。
论文地址:https://stability.ai/research/stable-video-diffusion-scaling-latent-video-diffusion-models-to-large-datasets
Stable Video Diffusion 是 Stability AI 各式各样的开源模型大家族中的一员。现在看来,他们的产品已经横跨图像、语言、音频、三维和代码等多种模态,这是他们致力于提升 AI 最好的证明。
Stable Video Diffusion 的技术层面
Stable Video Diffusion 作为一种高分辨率的视频潜在扩散模型,达到了文本到视频或图像到视频的 SOTA 水平。近期,通过插入时间层并在小型高质量视频数据集上进行微调,为 2D 图像合成训练的潜在扩散模型已转变为生成视频模型。然而,文献中的训练方法千差万别,该领域尚未就视频数据整理的统一策略达成一致。
在 Stable Video Diffusion 的论文中,Stability AI 确定并评估了成功训练视频潜在扩散模型的三个不同阶段:文本到图像预训练、视频预训练和高质量视频微调。他们还证明了精心准备的预训练数据集对于生成高质量视频的重要性,并介绍了训练出一个强大基础模型的系统化策划流程,其中包括了字幕和过滤策略。
Stability AI 在论文中还探讨了在高质量数据上对基础模型进行微调的影响,并训练出一个可与闭源视频生成相媲美的文本到视频模型。该模型为下游任务提供了强大的运动表征,例如图像到视频的生成以及对摄像机运动特定的 LoRA 模块的适应性。除此之外,该模型还能够提供强大的多视图 3D 先验,这可以作为多视图扩散模型的基础,模型以前馈方式生成对象的多个视图,只需要较小的算力需求,性能还优于基于图像的方法。
具体而言,成功训练该模型包括以下三个阶段:
阶段一:图像预训练。本文将图像预训练视为训练 pipeline 的第一阶段,并将初始模型建立在 Stable Diffusion 2.1 的基础上,这样一来为视频模型配备了强大的视觉表示。为了分析图像预训练的效果,本文还训练并比较了两个相同的视频模型。图 3a 结果表明,图像预训练模型在质量和提示跟踪方面都更受青睐。
阶段 2:视频预训练数据集。本文依靠人类偏好作为信号来创建合适的预训练数据集。本文创建的数据集为 LVD(Large Video Dataset ),由 580M 对带注释的视频片段组成。
进一步的研究表明生成的数据集包含可能会降低最终视频模型性能的示例。因此,本文还采用了密集光流来注释数据集。
此外,本文还应用光学字符识别来清除包含大量文本的剪辑。最后,本文使用 CLIP 嵌入来注释每个剪辑的第一帧、中间帧和最后一帧。下表提供了 LVD 数据集的一些统计信息:
阶段 3:高质量微调。为了分析视频预训练对最后阶段的影响,本文对三个模型进行了微调,这些模型仅在初始化方面有所不同。图 4e 为结果。
看起来这是个好的开始。什么时候,我们能用 AI 直接生成一部电影呢?
参考内容:
https://stability.ai/news/stable-video-diffusion-open-ai-video-model
https://news.ycombinator.com/item?id=38368287
#Stable Video Diffusion~2
结构浅析与论文速览
面向之前已经熟悉 Stable Diffusion (SD) 的读者,简要解读 SVD 的论文。
近期,各个科技公司纷纷展示了自己在视频生成模型上的最新成果。虽然不少模型的演示效果都非常惊艳,但其中可供学术界研究的开源模型却少之又少。Stable Video Diffusion (SVD) 算得上是目前开源视频生成模型中的佼佼者,有认真一学的价值。在这篇文章中,我将面向之前已经熟悉 Stable Diffusion (SD) 的读者,简要解读 SVD 的论文。由于 SVD 的部分结构复用了之前的工作,并没有在论文正文中做详细介绍,所以我还会补充介绍一下 SVD 的模型结构、调度器。后续我还会在其他文章中详细介绍 SVD 的代码实现及使用方法。
背景
Stable Video Diffusion 是 Stability 公司于 2023 年 11 月 21 日公布并开源的一套用扩散模型实现的视频生成模型。由于该模型是从 Stability 公司此前发布的著名文生图模型 Stable Diffusion 2.1 微调而成的,因而得名 Stable Video Diffusion。SVD 的技术报告论文与模型同日发布,它对 SVD 的训练过程做了一个详细的分享。由于该论文过分偏向实践,这里我们仅对它的开头及中间模型设计的几处关键部分做解读。
摘要
最近,有许多视频生成模型都是在图像生成模型 SD 的基础上,添加和视频时序相关的模块,并在小规模高质量视频数据集上微调新模型。而 SVD 作者认为,该领域在训练方法及精制数据集的策略上并未达成统一。这篇文章的主要贡献,也正是提出了一套训练方法与精制数据集的方法。具体而言,SVD 的训练由三个阶段组成:文生图预训练、视频预训练、高质量视频微调。同时,SVD 提出了一种系统性的数据精制流程,包含数据的标注与过滤这两部分的策略。论文会分享诸多的实验成果,包括验证精心构建的数据集对生成高质量视频的必要性、探究视频预训练与微调这两步的重要性、展示基础模型如何为图生视频等下游任务提供强大的运动表示、演示模型如何提供多视角三维先验并可以作为微调多视角扩散模型的基础模型在一轮神经网络推理中同时生成多视角的图片。
「构建」一个数据集在论文中通常用动词 curate 及名词 curation 指代。curate 原指展出画作时,选择、组织和呈现艺术品的过程。而现代将这个词用在数据集上时,则转变为表示精心选择、组织和管理数据的过程。中文中并没有完全对应的翻译,我暂时将这个词翻译为「精制」,以区别于随便收集一些数据来构成一个数据集。
总结一下,SVD 并没有强调在模型设计或者采样算法上的创新,而主要宣传了该工作在数据集精制及训练策略上的创新。对于大部分普通研究人员来说,由于没有训练大视频模型的需求,该文章的很多内容都价值不大。我们就只是来大致过一遍这篇文章的主要内容。
SVD 模型架构回顾
Video-LDM 与 SVD
在阅读正文之前,我们先来回顾一下此前视频生成模型的开发历程,并重点探究 SVD 的模型架构——Video LDM 的具体组成。绝大多数工作在训练一个基于扩散模型的视频生成模型时,都是在预训练的 SD 上加入时序模块,如 3D 卷积,并通过微调把一个图像生成模型转换成视频生成模型。由于 SD 是一种 LDM (Latent Diffusion Model),所以这些视频模型都可以归类为 Video-LDM。所谓 LDM,就是一种先生成压缩图像,再用解码模型把压缩图像还原成真实图像的模型。而对于视频,Video-LDM 则会先生成边长压缩过的视频,再把压缩视频还原。
虽然 Video-LDM 严格上来说是一个视频扩散模型的种类,但大家一般会用Video LDM (没有横杠) 来指代 Align your Latents: High-Resolution Video Synthesis with Latent Diffusion Models 这篇工作。这篇论文已在 CVPR 2023 上发布,两个主要作者正是前一年在 CVPR 上发表 SD 论文的主要作者,也是现在这篇 SVD 论文的主要作者。从署名上来看,似乎两个作者在毕业后就加入了 Stability 公司,并将 Video LDM 拓展成了 SVD。论文中也讲到,SVD 完全复用了 Video LDM 的结构。为了了解 SVD 的模型结构,我们再来回顾一下 Video LDM 的结构。
在 SD 的基础上,Video LDM 做对模型结构了两项改动:在扩散模型的去噪模型 U-Net 中加入时序层、在对图像压缩和解压的 VAE 的解码器中加入时序层。
添加时序层
Video LDM 在 U-Net 中加入时序层的方法与多数同期方法相同,是在每个原来处理图像的空间层后面加上处理视频的时序层。Video LDM 加入的时序层包括 3D 卷积层与时序注意力层。这些新模块本身不难理解,但我们需要着重关注这些新模块是怎么与原模型兼容的。
对于之前已有的空间层,只要把数据形状变成 (B T) C H W
就没问题了。而 SVD 又新加入了两种时序层:3D 卷积和时序注意力。我们来看一下数据是怎么经过这些新的时序层的。
2D 卷积会对 B C H W
的数据的后两个高、宽维度做卷积。类似地,3D 卷积会对数据最后三个时间、高、宽维度做卷积。所以,过 3D 卷积前,要把形状从 (B T) C H W
变成 B C T H W
,做完卷积再还原。
接下来我们来看新的时序注意力。这个地方稍微有点难理解,我们从最简单的注意力开始一点一点学习。最早的 NLP 中的注意力层的输入形状为 B L C
,表示数据数、token 长度、token 通道数。L
这一维最为重要,它表示了 L
个 token 之间互相交换信息。如果把其拓展成图像空间注意力,则 token 表示图像的每一个像素。在这种注意力层中,L
是 (H W)
,B C H W
的数据会被转换成 B (H W) C
输入进注意力层。这表示同一组图像中,每个像素两两之间交换信息。而让视频数据过空间注意力层时,只需要把 B
换成 (B T)
即可,即把数据形状从 (B T) C H W
变为 (B T) (H W) C
。这表示同一组、同一帧的图像的每个像素之间,两两交换信息。
在 SVD 新加入的时序注意力层中,token 依旧指代是某一组、某一帧上的一个像素。然而,这次我们不是让同一张图像的像素互相交换信息,而是让不同时刻的像素互相交换信息。因此,这次 token 长度 L
是 T
,它表示要像素在时间维度上交换信息。这样,在视频数据过时序层里的自注意力层时,要把数据形状从 (B T) C H W
变成 (B H W) T C
。这表示每一组、图像每一处的像素独立处理,它们仅与同一位置不同时间的像素进行信息交换。
此处如果你没有理解注意力层的形状变换也不要紧,它只是一个实现细节,不影响后面的阅读。如果感兴趣的话,可以回顾一下 Transformer 论文的代码,看一下注意力运算为什么是对 B L C
的数据做操作的。
微调 VAE 解码器
Video LDM 的另一项改动是修改了图像压缩模型 VAE 的解码器。具体来说,方法先在 VAE 的解码器中加入类似的时序层,并在 VAE 配套的 GAN 的判别器里也加入了时序层,随后开始微调。在微调时,编码器不变,仅训练解码器和判别器。
如果你没听过这套 VAE + GAN 的架构的话,请回顾 Stable Diffusion 论文及与其紧密相关的 VQGAN 论文。
以上就是 Video LDM 的模型结构。SVD 对其没有做任何更改,所以也没有在论文里对模型结构做详细介绍。稍有不同的是,Video LDM 仅微调了新加入的模块,而 SVD 在加入新模块后对模型的所有参数都进行了重新训练。
SVD 训练细节
SVD 分四节介绍了模型训练过程。第一节介绍了数据精制的过程,后三节分别介绍了训练的三个阶段:文生图预训练、视频预训练、高质量视频微调。
获取了一个大规模视频数据集后,SVD 的数据精制主要由预处理和标注这两步组成。由于视频生成模型主要关注生成同一个场景的视频,而不考虑转场的问题,每段训练视频也应该尽量只包含一个场景。为此,预处理主要是在用一些自动化视频剪切工具把收集到的视频进一步切成连续的片段。经切片后,视频片段数变为原来的4倍。标注主要是给视频加上文字描述,以训练一个文生视频的模型。SVD 在添加文字描述时用到了多个标注模型,并使用大语言模型来润色描述。经预处理和标注后,得到的数据集被称作 LVD (Large Video Dataset)。
SVD 数据精制的细节中,比较值得注意的是有关视频帧数的处理。由于开发团队发现视频数据的播放速度快慢不一,于是他们使用光流预测模型来大致估计每段视频的播放速度(以帧率 FPS 表示),并将视频的帧率也作为标注。这样,在训练时,视频的帧率也可以作为一种约束信息。这样的好处是,在我们在生成视频时,可以用该约束来指定视频的播放速度。
之后我们来看 SVD 模型训练的三个阶段。对于第一个文生图预训练阶段,论文没有对模型结构做过多修改,因为他们在这一步使用了之前训练好的 SD 2.1。不过,SVD 在这一步做了一个非常重要的改进:SVD 的噪声调度器从原版的 DDPM 改成了 EDM,采样方法也改成了 EDM 的。
在第三个阶段, 参考以往多阶段训练图像模型的经验, SVD 也在另一个小而精的视频数据集上进行微调。此数据集的获取方法并没有在论文中给出, 大概率是人工手动收集并标注。
SVD 应用
经上述训练后,开发团队得到了一个低分辨率的基础文生视频模型。在实验部分,SVD 论文除了给出视频生成模型在各大公开数据集上的指标外,还分享了几个基于基础模型的应用。
高分辨率文生视频
基础文生视频最直接的应用就是高分辨率文生视频。实现的方法很简单,只要准备一个高分辨率的视频数据集,在此数据集上微调原基础模型即可。SVD 高分辨率文生视频模型能生成 576 x 1024 的视频。
高分辨率图生视频
除了文生视频外,也可以用基础模型来微调出一个图生视频模型。为了把约束从文本换成图像,开发团队将 U-Net 交叉注意力层的约束从文本嵌入变成了约束图像的图像嵌入,并将约束图像与原 U-Net 的噪声输入在通道维度上拼接在一起。特别地,参考以往 Cascaded diffusion models 论文的经验,约束图像在与噪声输入拼接前,会加上一些噪声。除此之外,由于约束机制的变动,像文生图模型一样将约束强度(CFG scale)设成 7.5 会让 SVD 图生视频模型产生瑕疵。因此,SVD 图生视频模型每一帧的约束强度不同,从第一帧到最后一帧以 1.0 到 3.0 线性增长。
参考之前 AnimateDiff 工作,SVD 也成功训练了相机运动 LoRA,使得图生视频模型只会生成平移、缩放等某一种特定相机运动的视频。
视频插帧
Video LDM 曾提出了一种把基础视频模型变成视频插帧模型方法。该方法以视频片段的首末帧为额外约束,在此新约束下把视频生成模型微调成了预测中间帧的视频预测模型。SVD 以同样方式实现了这一应用。
多视角生成
多视角生成是计算机视觉中另一类重要的任务:给定 3D 物体某个视角的图片,需要算法生成物体另外视角的图片,从而还原 3D 物体的原貌。而视频生成模型从数据中学到了物体的平滑变换规律,恰好能帮助到多视角生成任务。SVD 论文用不少篇幅介绍了如何在 3D 数据集上生成视频并微调基础模型,从而得到一个能生成环绕物体旋转的视频的模型。
结语
Stable Video Diffusion 是在文生图模型 Stable Diffusion 2.1 的基础上添加了和 Video LDM 相同的视频模块微调而成的一套视频生成模型。SVD 的论文主要介绍了其精制数据集的细节,并展示了几个微调基础模型能实现的应用。通过微调基础低分辨率文生视频模型,SVD 可以用于高分辨率文生视频、高分辨率图生视频、视频插帧、多视角生成。
对于没有资源与需求训练大视频模型的多数科研人员而言,没有深究这篇文章细节的必要。并且,由于 SVD 只开源了图生视频模型 (3D模型后来是在 SV3D 论文中正式公布的),这篇文章比较有用的只有和图生视频相关的部分。为了彻底搞懂 SVD 的原理,读这篇论文是不够的,我们还需要通过回顾 Video LDM 论文来了解模型结构,学习 EDM 论文来了解训练及采样机制。
这篇文章主要是面向熟悉 Stable Diffusion 的读者的。如果你缺少某些背景知识,欢迎读我之前介绍 Stable Diffusion 的文章。我没有在本文过多介绍 SVD 的实现细节,欢迎阅读我之后发表的 SVD 代码实践文章。
#LoRA6~Stable Diffusion
这里说LoRA 在 Stable Diffusion 中的三种应用
LoRA 是当今深度学习领域中常见的技术。对于 SD,LoRA 则是能够编辑单幅图片、调整整体画风,或者是通过修改训练目标来实现更强大的功能。LoRA 的原理非常简单,它其实就是用两个参数量较少的矩阵来描述一个大参数矩阵在微调中的变化量。Diffusers 库提供了非常便利的 SD LoRA 训练脚本。
如果你一直关注 Stable Diffusion (SD) 社区,那你一定不会对 “LoRA” 这个名词感到陌生。社区用户分享的 SD LoRA 模型能够修改 SD 的画风,使之画出动漫、水墨或像素等风格的图片。但实际上,LoRA 不仅仅能改变 SD 的画风,还有其他的妙用。在这篇文章中,我们会先简单学习 LoRA 的原理,再认识科研中 LoRA 的三种常见应用:1) 还原单幅图像;2)风格调整;3)训练目标调整,最后阅读两个基于 Diffusers 的 SD LoRA 代码实现示例。
LoRA 的原理
在认识 LoRA 之前,我们先来回顾一下迁移学习的有关概念。迁移学习指在一次新的训练中,复用之前已经训练过的模型的知识。如果你自己动手训练过深度学习模型,那你应该不经意间地使用到了迁移学习:比如你一个模型训练了 500 步,测试后发现效果不太理想,于是重新读取该模型的参数,又继续训练了 100 步。之前那个被训练过的模型叫做预训练模型(pre-trained model),继续训练预训练模型的过程叫做微调(fine-tune)。
知道了微调的概念,我们就能来认识 LoRA 了。LoRA 的全称是 Low-Rank Adaptation (低秩适配),它是一种 Parameter-Efficient Fine-Tuning (参数高效微调,PEFT) 方法,即在微调时只训练原模型中的部分参数,以加速微调的过程。相比其他的 PEFT 方法,LoRA 之所以能脱颖而出,是因为它有几个明显的优点:
- 从性能上来看,使用 LoRA 时,只需要存储少量被微调过的参数,而不需要把整个新模型都保存下来。同时,LoRA 的新参数可以和原模型的参数合并到一起,不会增加模型的运算时间。
- 从功能上来看,LoRA 维护了模型在微调中的「变化量」。通过用一个介于 0~1 之间的混合比例乘变化量,我们可以控制模型的修改程度。此外,基于同一个原模型独立训练的多个 LoRA 可以同时使用。
这些优点在 SD LoRA 中的体现为:
- SD LoRA 模型一般都很小,一般只有几十 MB。
- SD LoRA 模型的参数可以合并到 SD 基础模型里,得到一个新的 SD 模型。
- 可以用一个 0~1 之间的比例来控制 SD LoRA 新画风的程度。
- 可以把不同画风的 SD LoRA 模型以不同比例混合。
为什么 LoRA 能有这些优点呢? LoRA 名字中的「低秩」又是什么意思呢? 让我们从 LoRA 的优点入手, 逐步揭示它原。
LoRA 在 SD 中的三种运用
LoRA 在 SD 的科研中有着广泛的应用。按照使用 LoRA 的动机,我们可以把 LoRA 的应用分成:1) 还原单幅图像;2)风格调整;3)训练目标调整。通过学习这些应用,我们能更好地理解 LoRA 的本质。
还原单幅图像
SD 只是一个生成任意图片的模型。为了用 SD 来编辑一张给定的图片,我们一般要让 SD 先学会生成一张一模一样的图片,再在此基础上做修改。可是,由于训练集和输入图片的差异,SD 或许不能生成完全一样的图片。解决这个问题的思路很简单粗暴:我们只用这一张图片来微调 SD,让 SD 在这张图片上过拟合。这样,SD 的输出就会和这张图片非常相似了。
较早介绍这种提高输入图片保真度方法的工作是 Imagic,只不过它采取的是完全微调策略。后续的 DragDiffusion 也用了相同的方法,并使用 LoRA 来代替完全微调。近期的 DiffMorpher 为了实现两幅图像间的插值,不仅对两幅图像单独训练了 LoRA,还通过两个 LoRA 间的插值来平滑图像插值的过程。
风格调整
LoRA 在 SD 社区中最受欢迎的应用就是风格调整了。我们希望 SD 只生成某一画风,或者某一人物的图片。为此,我们只需要在一个符合我们要求的训练集上直接训练 SD LoRA 即可。
由于这种调整 SD 风格的方法非常直接,没有特别介绍这种方法的论文。稍微值得一提的是基于 SD 的视频模型 AnimateDiff,它用 LoRA 来控制输出视频的视角变换,而不是控制画风。
由于 SD 风格化 LoRA 已经被广泛使用,能否兼容 SD 风格化 LoRA 决定了一个工作是否易于在社区中传播。
训练目标调整
最后一个应用就有一点返璞归真了。LoRA 最初的应用就是把一个预训练模型适配到另一任务上。比如 GPT 一开始在大量语料中训练,随后在问答任务上微调。对于 SD 来说,我们也可以修改 U-Net 的训练目标,以提升 SD 的能力。
有不少相关工作用 LoRA 来改进 SD。比如 Smooth Diffusion 通过在训练目标中添加一个约束项并进行 LoRA 微调来使得 SD 的隐空间更加平滑。近期比较火的高速图像生成方法 LCM-LoRA 也是把原本作用于 SD 全参数上的一个模型蒸馏过程用 LoRA 来实现。
SD LoRA 应用总结
尽管上述三种 SD LoRA 应用的设计出发点不同,它们本质上还是在利用微调这一迁移学习技术来调整模型的数据分布或者训练目标。LoRA 只是众多高效微调方法中的一种,只要是微调能实现的功能,LoRA 基本都能实现,只不过 LoRA 更轻便而已。如果你想微调 SD 又担心计算资源不够,那么用 LoRA 准没错。反过来说,你想用 LoRA 在 SD 上设计出一个新应用,就要去思考微调 SD 能够做到哪些事。
Diffusers SD LoRA 代码实战
看完了原理,我们来尝试用 Diffusers 自己训一训 LoRA。我们会先学习 Diffusers 训练 LoRA 的脚本,再学习两个简单的 LoRA 示例:SD 图像插值与 SD 图像风格迁移。
项目网址:https://github.com/SingleZombie/DiffusersExample/tree/main/LoRA
Diffusers 脚本
我们将参考 Diffusers 中的 SD LoRA 文档 https://huggingface.co/docs/diffusers/training/lora ,使用官方脚本 examples/text_to_image/train_text_to_image_lora.py
训练 LoRA。为了使用这个脚本,建议直接克隆官方仓库,并安装根目录和 text_to_image
目录下的依赖文件。本文使用的 Diffusers 版本是 0.26.0,过旧的 Diffusers 的代码可能和本文展示的有所出入。目前,官方文档也描述的是旧版的代码。
git clone https://github.com/huggingface/diffusers
cd diffusers
pip install .
cd examples/text_to_image
pip install -r requirements.txt
这份代码使用 accelerate 库管理 PyTorch 的训练。对同一份代码,只需要修改 accelerate 的配置,就能实现单卡训练或者多卡训练。默认情况下,用 accelerate launch
命令运行 Python 脚本会使用所有显卡。如果你需要修改训练配置,请参考相关文档使用 accelerate config
命令配置环境。
做好准备后,我们来开始阅读 examples/text_to_image/train_text_to_image_lora.py
的代码。这份代码写得十分易懂,复杂的地方都有注释。我们跳过命令行参数部分,直接从 main
函数开始读。
一开始,函数会配置 accelerate 库及日志记录器。
args = parse_args()
logging_dir = Path(args.output_dir, args.logging_dir)
accelerator_project_config = ProjectConfiguration(project_dir=args.output_dir, logging_dir=logging_dir)
accelerator = Accelerator(
gradient_accumulation_steps=args.gradient_accumulation_steps,
mixed_precision=args.mixed_precision,
log_with=args.report_to,
project_config=accelerator_project_config,
)
if args.report_to == "wandb":
if not is_wandb_available():
raise ImportError("Make sure to install wandb if you want to use it for logging during training.")
import wandb
# Make one log on every process with the configuration for debugging.
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
level=logging.INFO,
)
logger.info(accelerator.state, main_process_only=False)
if accelerator.is_local_main_process:
datasets.utils.logging.set_verbosity_warning()
transformers.utils.logging.set_verbosity_warning()
diffusers.utils.logging.set_verbosity_info()
else:
datasets.utils.logging.set_verbosity_error()
transformers.utils.logging.set_verbosity_error()
diffusers.utils.logging.set_verbosity_error()
随后的代码决定是否手动设置随机种子。保持默认即可。
# If passed along, set the training seed now.
if args.seed is not None:
set_seed(args.seed)
接着,函数会创建输出文件夹。如果我们想把模型推送到在线仓库上,函数还会创建一个仓库。我们的项目不必上传,忽略所有 args.push_to_hub
即可。另外,if accelerator.is_main_process:
表示多卡训练时只有主进程会执行这段代码块。
# Handle the repository creation
if accelerator.is_main_process:
if args.output_dir is not None:
os.makedirs(args.output_dir, exist_ok=True)
if args.push_to_hub:
repo_id = create_repo(
repo_id=args.hub_model_id or Path(args.output_dir).name, exist_ok=True, token=args.hub_token
).repo_id
准备完辅助工具后,函数正式开始着手训练。训练前,函数会先实例化好一切处理类,包括用于维护扩散模型中间变量的 DDPMScheduler
,负责编码输入文本的 CLIPTokenizer, CLIPTextModel
,压缩图像的VAE AutoencoderKL
,预测噪声的 U-Net UNet2DConditionModel
。参数 args.pretrained_model_name_or_path
是 Diffusers 在线仓库的地址(如runwayml/stable-diffusion-v1-5
),或者本地的 Diffusers 模型文件夹。
# Load scheduler, tokenizer and models.
noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler")
tokenizer = CLIPTokenizer.from_pretrained(
args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision
)
text_encoder = CLIPTextModel.from_pretrained(
args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision
)
vae = AutoencoderKL.from_pretrained(
args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision, variant=args.variant
)
unet = UNet2DConditionModel.from_pretrained(
args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision, variant=args.variant
)
函数还会设置各个带参数模型是否需要计算梯度。由于我们待会要优化的是新加入的 LoRA 模型,所有预训练模型都不需要计算梯度。另外,函数还会根据 accelerate 配置自动设置这些模型的精度。
# freeze parameters of models to save more memory
unet.requires_grad_(False)
vae.requires_grad_(False)
text_encoder.requires_grad_(False)
# Freeze the unet parameters before adding adapters
for param in unet.parameters():
param.requires_grad_(False)
# For mixed precision training we cast all non-trainable weigths (vae, non-lora text_encoder and non-lora unet) to half-precision
# as these weights are only used for inference, keeping weights in full precision is not required.
weight_dtype = torch.float32
if accelerator.mixed_precision == "fp16":
weight_dtype = torch.float16
elif accelerator.mixed_precision == "bf16":
weight_dtype = torch.bfloat16
# Move unet, vae and text_encoder to device and cast to weight_dtype
unet.to(accelerator.device, dtype=weight_dtype)
vae.to(accelerator.device, dtype=weight_dtype)
text_encoder.to(accelerator.device, dtype=weight_dtype)
把预训练模型都调好了后,函数会配置 LoRA 模块并将其加入 U-Net 模型中。最近,Diffusers 更新了添加 LoRA 的方式。Diffusers 用 Attention 处理器来描述 Attention 的计算。为了把 LoRA 加入到 Attention 模块中,早期的 Diffusers 直接在 Attention 处理器里加入可训练参数。现在,为了和其他 Hugging Face 库统一,Diffusers 使用 PEFT 库来管理 LoRA。我们不需要关注 LoRA 的实现细节,只需要写一个 LoraConfig
就行了。
PEFT 中的 LoRA 文档参见 https://huggingface.co/docs/peft/conceptual_guides/lora
LoraConfig
中有四个主要参数: r, lora_alpha, init_lora_weights, target_modules
。r, lora_alpha
的意义我们已经在前文中见过了,前者决定了 LoRA 矩阵的大小,后者决定了训练速度。默认配置下,它们都等于同一个值 args.rank
。init_lora_weights
表示如何初始化训练参数,gaussian
是论文中使用的方法。target_modules
表示 Attention 模块的哪些层需要添加 LoRA。按照通常的做法,会给所有层,即三个输入变换矩阵 to_k, to_q, to_v
和一个输出变换矩阵 to_out.0
加 LoRA。
创建了配置后,用 unet.add_adapter(unet_lora_config)
就可以创建 LoRA 模块。
unet_lora_config = LoraConfig(
r=args.rank,
lora_alpha=args.rank,
init_lora_weights="gaussian",
target_modules=["to_k", "to_q", "to_v", "to_out.0"],
)
unet.add_adapter(unet_lora_config)
if args.mixed_precision == "fp16":
for param in unet.parameters():
# only upcast trainable parameters (LoRA) into fp32
if param.requires_grad:
param.data = param.to(torch.float32)
更新完了 U-Net 的结构,函数会尝试启用 xformers
来提升 Attention 的效率。PyTorch 在 2.0 版本也加入了类似的 Attention 优化技术。如果你的显卡性能有限,且 PyTorch 版本小于 2.0,可以考虑使用 xformers
。
if args.enable_xformers_memory_efficient_attention:
if is_xformers_available():
import xformers
xformers_version = version.parse(xformers.__version__)
if xformers_version == version.parse("0.0.16"):
logger.warn(
...
)
unet.enable_xformers_memory_efficient_attention()
else:
raise ValueError("xformers is not available. Make sure it is installed correctly")
做完了 U-Net 的处理后,函数会过滤出要优化的模型参数,这些参数稍后会传递给优化器。过滤的原则很简单,如果参数要求梯度,就是待优化参数。
lora_layers = filter(lambda p: p.requires_grad, unet.parameters())
之后是优化器的配置。函数先是配置了一些细枝末节的训练选项,一般可以忽略。
if args.gradient_checkpointing:
unet.enable_gradient_checkpointing()
# Enable TF32 for faster training on Ampere GPUs,
# cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
if args.allow_tf32:
torch.backends.cuda.matmul.allow_tf32 = True
然后是优化器的选择。我们可以忽略其他逻辑,直接用 AdamW
。
# Initialize the optimizer
if args.use_8bit_adam:
try:
import bitsandbytes as bnb
except ImportError:
raise ImportError(
"..."
)
optimizer_cls = bnb.optim.AdamW8bit
else:
optimizer_cls = torch.optim.AdamW
选择了优化器类,就可以实例化优化器了。优化器的第一个参数是之前准备好的待优化 LoRA 参数,其他参数是 Adam 优化器本身的参数。
optimizer = optimizer_cls(
lora_layers,
lr=args.learning_rate,
betas=(args.adam_beta1, args.adam_beta2),
weight_decay=args.adam_weight_decay,
eps=args.adam_epsilon,
)
准备了优化器,之后需要准备训练集。这个脚本用 Hugging Face 的 datasets 库来管理数据集。我们既可以读取在线数据集,也可以读取本地的图片文件夹数据集。在本文的示例项目中,我们将使用图片文件夹数据集。稍后我们再详细学习这样的数据集文件夹该怎么构建。相关的文档可以参考 https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder 。
if args.dataset_name is not None:
# Downloading and loading a dataset from the hub.
dataset = load_dataset(
args.dataset_name,
args.dataset_config_name,
cache_dir=args.cache_dir,
data_dir=args.train_data_dir,
)
else:
data_files = {}
if args.train_data_dir is not None:
data_files["train"] = os.path.join(args.train_data_dir, "**")
dataset = load_dataset(
"imagefolder",
data_files=data_files,
cache_dir=args.cache_dir,
)
# See more about loading custom images at
# https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder
训练 SD 时,每一个数据样本需要包含两项信息:图像数据与对应的文本描述。在数据集 dataset
中,每个数据样本包含了多项属性。下面的代码用于从这些属性中取出图像与文本描述。默认情况下,第一个属性会被当做图像数据,第二个属性会被当做文本。
# Preprocessing the datasets.
# We need to tokenize inputs and targets.
column_names = dataset["train"].column_names
# 6. Get the column names for input/target.
dataset_columns = DATASET_NAME_MAPPING.get(args.dataset_name, None)
if args.image_column is None:
image_column = dataset_columns[0] if dataset_columns is not None else column_names[0]
else:
image_column = args.image_column
if image_column not in column_names:
raise ValueError(
f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}"
)
if args.caption_column is None:
caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1]
else:
caption_column = args.caption_column
if caption_column not in column_names:
raise ValueError(
f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}"
)
准备好了数据集,接下来要定义数据预处理流程以创建 DataLoader
。函数先定义了一个把文本标签预处理成 token ID 的 token 化函数。我们不需要修改它。
def tokenize_captions(examples, is_train=True):
captions = []
for caption in examples[caption_column]:
if isinstance(caption, str):
captions.append(caption)
elif isinstance(caption, (list, np.ndarray)):
# take a random caption if there are multiple
captions.append(random.choice(caption) if is_train else caption[0])
else:
raise ValueError(
f"Caption column `{caption_column}` should contain either strings or lists of strings."
)
inputs = tokenizer(
captions, max_length=tokenizer.model_max_length, padding="max_length", truncation=True, return_tensors="pt"
)
return inputs.input_ids
接着,函数定义了图像数据的预处理流程。该流程是用 torchvision
中的 transforms
实现的。如代码所示,处理流程中包括了 resize 至指定分辨率 args.resolution
、将图像长宽均裁剪至指定分辨率、随机翻转、转换至 tensor 和归一化。
经过这一套预处理后,所有图像的长宽都会被设置为 args.resolution
。统一图像的尺寸,主要的目的是对齐数据,以使多个数据样本能拼接成一个 batch。注意,数据预处理流程中包括了随机裁剪。如果数据集里的多数图片都长宽不一致,模型会倾向于生成被裁剪过的图片。为了解决这一问题,要么自己手动预处理图片,使训练图片都是分辨率至少为 args.resolution
的正方形图片,要么令 batch size 为 1 并取消掉随机裁剪。
# Preprocessing the datasets.
train_transforms = transforms.Compose(
[
transforms.Resize(
args.resolution, interpolation=transforms.InterpolationMode.BILINEAR),
transforms.CenterCrop(
args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution),
transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]),
]
)
定义了预处理流程后,函数对所有数据进行预处理。
def preprocess_train(examples):
images = [image.convert("RGB") for image in examples[image_column]]
examples["pixel_values"] = [
train_transforms(image) for image in images]
examples["input_ids"] = tokenize_captions(examples)
return examples
with accelerator.main_process_first():
if args.max_train_samples is not None:
dataset["train"] = dataset["train"].shuffle(
seed=args.seed).select(range(args.max_train_samples))
# Set the training transforms
train_dataset = dataset["train"].with_transform(preprocess_train)
之后函数用预处理过的数据集创建 DataLoader
。这里要注意的参数是 batch size args.train_batch_size
和读取数据的进程数 args.dataloader_num_workers
。这两个参数的用法和一般的 PyTorch 项目一样。args.train_batch_size
决定了训练速度,一般设置到不爆显存的最大值。如果要读取的数据过多,导致数据读取成为了模型训练的速度瓶颈,则应该提高 args.dataloader_num_workers
。
def collate_fn(examples):
pixel_values = torch.stack([example["pixel_values"]
for example in examples])
pixel_values = pixel_values.to(
memory_format=torch.contiguous_format).float()
input_ids = torch.stack([example["input_ids"] for example in examples])
return {"pixel_values": pixel_values, "input_ids": input_ids}
# DataLoaders creation:
train_dataloader = torch.utils.data.DataLoader(
train_dataset,
shuffle=True,
collate_fn=collate_fn,
batch_size=args.train_batch_size,
num_workers=args.dataloader_num_workers,
)
如果想用更大的 batch size,显存又不够,则可以使用梯度累计技术。使用这项技术时,训练梯度不会每步优化,而是累计了若干步后再优化。args.gradient_accumulation_steps
表示要累计几步再优化模型。实际的 batch size 等于输入 batch size 乘 GPU 数乘梯度累计步数。下面的代码维护了训练步数有关的信息,并创建了学习率调度器。我们按照默认设置使用一个常量学习率即可。
# Scheduler and math around the number of training steps.
overrode_max_train_steps = False
num_update_steps_per_epoch = math.ceil(
len(train_dataloader) / args.gradient_accumulation_steps)
if args.max_train_steps is None:
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
overrode_max_train_steps = True
lr_scheduler = get_scheduler(
args.lr_scheduler,
optimizer=optimizer,
num_warmup_steps=args.lr_warmup_steps * accelerator.num_processes,
num_training_steps=args.max_train_steps * accelerator.num_processes,
)
# Prepare everything with our `accelerator`.
unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
unet, optimizer, train_dataloader, lr_scheduler
)
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
num_update_steps_per_epoch = math.ceil(
len(train_dataloader) / args.gradient_accumulation_steps)
if overrode_max_train_steps:
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
# Afterwards we recalculate our number of training epochs
args.num_train_epochs = math.ceil(
args.max_train_steps / num_update_steps_per_epoch)
在准备工作的最后,函数会用 accelerate 库记录配置信息。
if accelerator.is_main_process:
accelerator.init_trackers("text2image-fine-tune", config=vars(args))
终于,要开始训练了。训练开始前,函数会准备全局变量并记录日志。
# Train!
total_batch_size = args.train_batch_size * \
accelerator.num_processes * args.gradient_accumulation_steps
logger.info("***** Running training *****")
...
global_step = 0
first_epoch = 0
此时,如果设置了 args.resume_from_checkpoint
,则函数会读取之前训练过的权重。一般继续训练时可以把该参数设为 latest
,程序会自动找最新的权重。
# Potentially load in the weights and states from a previous save
if args.resume_from_checkpoint:
if args.resume_from_checkpoint != "latest":
path = ...
else:
# Get the most recent checkpoint
path = ...
if path is None:
args.resume_from_checkpoint = None
initial_global_step = 0
else:
accelerator.load_state(os.path.join(args.output_dir, path))
global_step = int(path.split("-")[1])
initial_global_step = global_step
first_epoch = global_step // num_update_steps_per_epoch
else:
initial_global_step = 0
随后,函数根据总步数和已经训练过的步数设置迭代器,正式进入训练循环。
progress_bar = tqdm(
range(0, args.max_train_steps),
initial=initial_global_step,
desc="Steps",
# Only show the progress bar once on each machine.
disable=not accelerator.is_local_main_process,
)
for epoch in range(first_epoch, args.num_train_epochs):
unet.train()
train_loss = 0.0
for step, batch in enumerate(train_dataloader):
with accelerator.accumulate(unet):
训练的过程基本和 LDM 论文中展示的一致。一开始,要取出图像batch["pixel_values"]
并用 VAE 把它压缩进隐空间。
# Convert images to latent space
latents = vae.encode(batch["pixel_values"].to(
dtype=weight_dtype)).latent_dist.sample()
latents = latents * vae.config.scaling_factor
再随机生成一个噪声。该噪声会套入扩散模型前向过程的公式,和输入图像一起得到 t
时刻的带噪图像。
# Sample noise that we'll add to the latents
noise = torch.randn_like(latents)
下一步,这里插入了一个提升扩散模型训练质量的小技巧,用上它后输出图像的颜色分布会更合理。原理见注释中的链接。args.noise_offset
默认为 0。如果要启用这个特性,一般令 args.noise_offset = 0.1
。
if args.noise_offset:
# https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += args.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
然后是时间戳的随机生成。
bsz = latents.shape[0]
# Sample a random timestep for each image
timesteps = torch.randint(
0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device)
timesteps = timesteps.long()
时间戳和前面随机生成的噪声一起经 DDPM 的前向过程得到带噪图片 noisy_latents
。
# Add noise to the latents according to the noise magnitude at each timestep
# (this is the forward diffusion process)
noisy_latents = noise_scheduler.add_noise(
latents, noise, timesteps)
再把文本 batch["input_ids"]
编码,为之后的 U-Net 前向传播做准备。
# Get the text embedding for conditioning
encoder_hidden_states = text_encoder(batch["input_ids"])[0]
在 U-Net 推理开始前,函数这里做了一个关于 U-Net 输出类型的判断。一般 U-Net 都是输出预测的噪声 epsilon
,可以忽略这段代码。当 U-Net 是想预测噪声时,要拟合的目标是之前随机生成的噪声 noise
。
# Get the target for loss depending on the prediction type
if args.prediction_type is not None:
# set prediction_type of scheduler if defined
noise_scheduler.register_to_config(
prediction_type=args.prediction_type)
if noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif noise_scheduler.config.prediction_type == "v_prediction":
target = noise_scheduler.get_velocity(
latents, noise, timesteps)
else:
raise ValueError(
f"Unknown prediction type {noise_scheduler.config.prediction_type}")
之后把带噪图像、时间戳、文本编码输入进 U-Net,U-Net 输出预测的噪声。
# Predict the noise residual and compute loss
model_pred = unet(noisy_latents, timesteps,
encoder_hidden_states).sample
有了预测值,下一步是算 loss。这里又可以选择是否使用一种加速训练的技术。如果使用,则 args.snr_gamma
推荐设置为 5.0。原 DDPM 的做法是直接算预测噪声和真实噪声的均方误差。
if args.snr_gamma is None:
loss = F.mse_loss(model_pred.float(),
target.float(), reduction="mean")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
...
训练迭代的最后,要用 accelerate 库来完成梯度计算和反向传播。在更新梯度前,可以通过设置 args.max_grad_norm
来裁剪梯度,以防梯度过大。args.max_grad_norm
默认为 1.0。代码中的 if accelerator.sync_gradients:
可以保证所有 GPU 都同步了梯度再执行后续代码。
# Backpropagate
accelerator.backward(loss)
if accelerator.sync_gradients:
params_to_clip = lora_layers
accelerator.clip_grad_norm_(
params_to_clip, args.max_grad_norm)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
一步训练结束后,更新和步数相关的变量。
if accelerator.sync_gradients:
progress_bar.update(1)
global_step += 1
accelerator.log({"train_loss": train_loss}, step=global_step)
train_loss = 0.0
脚本默认每 args.checkpointing_steps
步保存一次中间结果。当需要保存时,函数会清理多余的 checkpoint,再把模型状态和 LoRA 模型分别保存下来。accelerator.save_state(save_path)
负责把模型及优化器等训练用到的所有状态存下来,后面的 StableDiffusionPipeline.save_lora_weights
负责存储 LoRA 模型。
if global_step % args.checkpointing_steps == 0:
if accelerator.is_main_process:
# _before_ saving state, check if this save would set us over the `checkpoints_total_limit`
if args.checkpoints_total_limit is not None:
checkpoints = ...
if len(checkpoints) >= args.checkpoints_total_limit:
# remove ckpt
...
save_path = os.path.join(
args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(save_path)
unwrapped_unet = accelerator.unwrap_model(unet)
unet_lora_state_dict = convert_state_dict_to_diffusers(
get_peft_model_state_dict(unwrapped_unet)
)
StableDiffusionPipeline.save_lora_weights(
save_directory=save_path,
unet_lora_layers=unet_lora_state_dict,
safe_serialization=True,
)
logger.info(f"Saved state to {save_path}")
训练循环的最后,函数会更新进度条上的信息,并根据当前的训练步数决定是否停止训练。
logs = {"step_loss": loss.detach().item(
), "lr": lr_scheduler.get_last_lr()[0]}
progress_bar.set_postfix(**logs)
if global_step >= args.max_train_steps:
break
训完每一个 epoch 后,函数会进行验证。默认的验证方法是新建一个图像生成 pipeline,生成一些图片并保存。如果有其他验证方法,如计算某一指标,可以自行编写这部分的代码。
if accelerator.is_main_process:
if args.validation_prompt is not None and epoch % args.validation_epochs == 0:
logger.info(
f"Running validation... \n Generating {args.num_validation_images} images with prompt:"
f" {args.validation_prompt}."
)
pipeline = DiffusionPipeline.from_pretrained(...)
...
所有训练结束后,函数会再存一次最终的 LoRA 模型权重。
# Save the lora layers
accelerator.wait_for_everyone()
if accelerator.is_main_process:
unet = unet.to(torch.float32)
unwrapped_unet = accelerator.unwrap_model(unet)
unet_lora_state_dict = convert_state_dict_to_diffusers(
get_peft_model_state_dict(unwrapped_unet))
StableDiffusionPipeline.save_lora_weights(
save_directory=args.output_dir,
unet_lora_layers=unet_lora_state_dict,
safe_serialization=True,
)
if args.push_to_hub:
...
函数还会再测试一次模型。具体方法和之前的验证是一样的。
# Final inference
# Load previous pipeline
if args.validation_prompt is not None:
...
运行完了这里,函数也就结束了。
accelerator.end_training()
为了方便使用,我把这个脚本改写了一下:删除了部分不常用的功能,并且配置参数能通过配置文件而不是命令行参数传入。新的脚本为项目根目录下的 train_lora.py
,示例配置文件在 cfg
目录下。以 cfg
中的某个配置文件为例,我们来回顾一下训练脚本主要用到的参数:
{
"log_dir": "log",
"output_dir": "ckpt",
"data_dir": "dataset/mountain",
"ckpt_name": "mountain",
"gradient_accumulation_steps": 1,
"pretrained_model_name_or_path": "runwayml/stable-diffusion-v1-5",
"rank": 8,
"enable_xformers_memory_efficient_attention": true,
"learning_rate": 1e-4,
"adam_beta1": 0.9,
"adam_beta2": 0.999,
"adam_weight_decay": 1e-2,
"adam_epsilon": 1e-08,
"resolution": 512,
"n_epochs": 200,
"checkpointing_steps": 500,
"train_batch_size": 1,
"dataloader_num_workers": 1,
"lr_scheduler_name": "constant",
"resume_from_checkpoint": false,
"noise_offset": 0.1,
"max_grad_norm": 1.0
}
需要关注的参数:output_dir
为输出 checkpoint 的文件夹,ckpt_name
为输出 checkpoint 的文件名。data_dir
是训练数据集所在文件夹。pretrained_model_name_or_path
为 SD 模型文件夹。rank
是决定 LoRA 大小的参数。learning_rate
是学习率。adam
打头的是 AdamW 优化器的参数。resolution
是训练图片的统一分辨率。n_epochs
是训练的轮数。checkpointing_steps
指每过多久存一次 checkpoint。train_batch_size
是 batch size。gradient_accumulation_steps
是梯度累计步数。
要修改这个配置文件,要先把文件夹的路径改对,填上训练时的分辨率,再通过 gradient_accumulation_steps
和 train_batch_size
决定 batch size,接着填 n_epochs
(一般训 10~20 轮就会过拟合)。最后就可以一边改 LoRA 的主要超参数 rank
一边反复训练了。
SD 图像插值
在这个示例中,我们来实现 DiffMorpher 工作的一小部分,完成一个简单的图像插值工具。在此过程中,我们将学会怎么在单张图片上训练 SD LoRA,以验证我们的训练环境。
这个工具的原理很简单:我们对两张图片分别训练一个 LoRA。之后,为了获取两张图片的插值,我们可以对两张图片 DDIM Inversion 的初始隐变量及两个 LoRA 分别插值,用插值过的隐变量在插值过的 SD LoRA 上生成图片就能得到插值图片。
该示例的所有数据和代码都已经在项目文件夹中给出。首先,我们看一下该怎么在单张图片上训 LoRA。训练之前,我们要准备一个数据集文件夹。数据集文件夹及包含所有图片及一个描述文件 metadata.jsonl
。比如单图片的数据集文件夹的结构应如下所示:
├── mountain
│ ├── metadata.jsonl
│ └── mountain.jpg
metadata.jsonl
元数据文件的每一行都是一个 json 结构,包含该图片的路径及文本描述。单图片的元数据文件如下:
{"file_name": "mountain.jpg", "text": "mountain"}
如果是多图片,就应该是:
{"file_name": "mountain.jpg", "text": "mountain"}
{"file_name": "mountain_up.jpg", "text": "mountain"}
...
我们可以运行项目目录下的数据集测试文件 test_dataset.py
来看看 datasets 库的数据集对象包含哪些信息。
from datasets import load_dataset
dataset = load_dataset("imagefolder", data_dir="dataset/mountain")
print(dataset)
print(dataset["train"].column_names)
print(dataset["train"]['image'])
print(dataset["train"]['text'])
其输出大致为:
Generating train split: 1 examples [00:00, 66.12 examples/s]
DatasetDict({
train: Dataset({
features: ['image', 'text'],
num_rows: 1
})
})
['image', 'text']
[<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=512x512 at 0x7F0400246670>]
['mountain']
这说明数据集对象实际上是一个词典。默认情况下,数据集放在词典的 train
键下。数据集的 column_names
属性可以返回每项数据有哪些属性。在我们的数据集里,数据的 image
是图像数据,text
是文本标签。训练脚本默认情况下会把每项数据的第一项属性作为图像,第二项属性作为文本标签。我们的这个数据集定义与训练脚本相符。
认识了数据集,我们可以来训练模型了。用下面的两行命令就可以分别在两张图片上训练 LoRA。
python train_lora.py cfg/mountain.json
python train_lora.py cfg/mountain_up.json
如果要用所有显卡训练,则应该用 accelerate。当然,对于这个简单的单图片训练,不需要用那么多显卡。
accelerate launch train_lora.py cfg/mountain.json
accelerate launch train_lora.py cfg/mountain_up.json
这两个 LoRA 模型的配置文件我们已经在前文见过了。相比普通的风格化 LoRA,这两个 LoRA 的训练轮数非常多,有 200 轮。设置较大的训练轮数能保证模型在单张图片上过拟合。
训练结束后,项目的 ckpt
文件夹下会多出两个 LoRA 权重文件: mountain.safetensor
, mountain_up.safetensor
。我们可以用它们来做图像插值了。
图像插值的脚本为 morph.py
,它的主要内容为:
import torch
from inversion_pipeline import InversionPipeline
lora_path = 'ckpt/mountain.safetensor'
lora_path2 = 'ckpt/mountain_up.safetensor'
sd_path = 'runwayml/stable-diffusion-v1-5'
pipeline: InversionPipeline = InversionPipeline.from_pretrained(
sd_path).to("cuda")
pipeline.load_lora_weights(lora_path, adapter_name='a')
pipeline.load_lora_weights(lora_path2, adapter_name='b')
img1_path = 'dataset/mountain/mountain.jpg'
img2_path = 'dataset/mountain_up/mountain_up.jpg'
prompt = 'mountain'
latent1 = pipeline.inverse(img1_path, prompt, 50, guidance_scale=1)
latent2 = pipeline.inverse(img2_path, prompt, 50, guidance_scale=1)
n_frames = 10
images = []
for i in range(n_frames + 1):
alpha = i / n_frames
pipeline.set_adapters(["a", "b"], adapter_weights=[1 - alpha, alpha])
latent = slerp(latent1, latent2, alpha)
output = pipeline(prompt=prompt, latents=latent,
guidance_scale=1.0).images[0]
images.append(output)
对于每一个 Diffusers 的 Pipeline 类实例,都可以用 pipeline.load_lora_weights
来读取 LoRA 权重。如果我们在同一个模型上使用了多个 LoRA,为了区分它们,我们要加上 adapter_name
参数为每个 LoRA 命名。稍后我们会用到这些名称。
pipeline.load_lora_weights(lora_path, adapter_name='a')
pipeline.load_lora_weights(lora_path2, adapter_name='b')
读好了文件,使用已经写好的 DDIM Inversion 方法来得到两张图片的初始隐变量。
img1_path = 'dataset/mountain/mountain.jpg'
img2_path = 'dataset/mountain_up/mountain_up.jpg'
prompt = 'mountain'
latent1 = pipeline.inverse(img1_path, prompt, 50, guidance_scale=1)
latent2 = pipeline.inverse(img2_path, prompt, 50, guidance_scale=1)
最后开始生成不同插值比例的图片。根据混合比例 alpha
,我们可以用 pipeline.set_adapters(["a", "b"], adapter_weights=[1 - alpha, alpha])
来融合 LoRA 模型的比例。随后,我们再根据 alpha
对隐变量插值。用插值隐变量在插值 SD LoRA 上生成图片即可得到最终的插值图片。
n_frames = 10
images = []
for i in range(n_frames + 1):
alpha = i / n_frames
pipeline.set_adapters(["a", "b"], adapter_weights=[1 - alpha, alpha])
latent = slerp(latent1, latent2, alpha)
output = pipeline(prompt=prompt, latents=latent,
guidance_scale=1.0).images[0]
images.append(output)
下面两段动图中,左图和右图分别是无 LoRA 和有 LoRA 的插值结果。可见,通过 LoRA 权重上的插值,图像插值的过度会更加自然。
图片风格迁移
接下来,我们来实现最流行的 LoRA 应用——风格化 LoRA。当然,训练一个每张随机输出图片都质量很高的模型是很困难的。我们退而求其次,来实现一个能对输入图片做风格迁移的 LoRA 模型。
训练风格化 LoRA 对技术要求不高,其主要难点其实是在数据收集上。大家可以根据自己的需求,准备自己的数据集。我在本文中会分享我的实验结果。我希望把《弹丸论破》的画风——一种颜色渐变较多的动漫画风——应用到一张普通动漫画风的图片上。
由于我的目标是拟合画风而不是某一种特定的物体,我直接选取了 50 张左右的游戏 CG 构成训练数据集,且没有对图片做任何处理。训风格化 LoRA 时,文本标签几乎没用,我把所有数据的文本都设置成了游戏名 danganronpa
。
{"file_name": "1.png", "text": "danganronpa"}
...
{"file_name": "59.png", "text": "danganronpa"}
我的配置文件依然和前文的相同,LoRA rank 设置为 8。我一共训了 100 轮,但发现训练后期模型的过拟合很严重,其实令 n_epochs
为 10 到 20 就能有不错的结果。50 张图片训 10 轮最多几十分钟就训完。
由于训练图片的内容不够多样,且图片预处理时加入了随机裁剪,我的 LoRA 模型随机生成的图片质量较低。于是我决定在图像风格迁移任务上测试该模型。具体来说,我使用了 ControlNet Canny 加上图生图 (SDEdit)技术。相关的代码如下:
from diffusers import StableDiffusionControlNetImg2ImgPipeline, ControlNetModel
from PIL import Image
import cv2
import numpy as np
lora_path = '...'
sd_path = 'runwayml/stable-diffusion-v1-5'
controlnet_canny_path = 'lllyasviel/sd-controlnet-canny'
prompt = '1 man, look at right, side face, Ace Attorney, Phoenix Wright, best quality, danganronpa'
neg_prompt = 'longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, {multiple people}'
img_path = '...'
init_image = Image.open(img_path).convert("RGB")
init_image = init_image.resize((768, 512))
np_image = np.array(init_image)
# get canny image
np_image = cv2.Canny(np_image, 100, 200)
np_image = np_image[:, :, None]
np_image = np.concatenate([np_image, np_image, np_image], axis=2)
canny_image = Image.fromarray(np_image)
canny_image.save('tmp_edge.png')
controlnet = ControlNetModel.from_pretrained(controlnet_canny_path)
pipe = StableDiffusionControlNetImg2ImgPipeline.from_pretrained(
sd_path, controlnet=controlnet
)
pipe.load_lora_weights(lora_path)
output = pipe(
prompt=prompt,
negative_prompt=neg_prompt,
strength=0.5,
guidance_scale=7.5,
controlnet_conditioning_scale=0.5,
num_inference_steps=50,
image=init_image,
cross_attention_kwargs={"scale": 1.0},
control_image=canny_image,
).images[0]
output.save("tmp.png")
StableDiffusionControlNetImg2ImgPipeline
是 Diffusers 中 ControlNet 加图生图的 Pipeline。使用它生成图片的重要参数有:
strength
:0~1 之间重绘比例。越低越接近输入图片。controlnet_conditioning_scale
:0~1 之间的 ControlNet 约束比例。越高越贴近约束。cross_attention_kwargs={"scale": scale}
:此处的scale
是 0~1 之间的 LoRA 混合比例。越高越贴近 LoRA 模型的输出。
这里贴一下输入图片和两张编辑后的图片。
可以看出,输出图片中人物的画风确实得到了修改,颜色渐变更加丰富。我在几乎没有调试 LoRA 参数的情况下得到了这样的结果,可见虽然训练一个高质量的随机生成新画风的 LoRA 难度较高,但只是做风格迁移还是比较容易的。
尽管实验的经历不多,我还是基本上了解了 SD LoRA 风格化的能力边界。LoRA 风格化的本质还是修改输出图片的分布,数据集的质量基本上决定了生成的质量,其他参数的影响不会很大(包括训练图片的文本标签)。数据集最好手动裁剪至 512x512。如果想要生成丰富的风格化内容而不是只生成人物,就要丰富训练数据,减少人物数据的占比。训练时,最容易碰到的机器学习上的问题是过拟合问题。解决此问题的最简单的方式是早停,即不用最终的训练结果而用中间某一步的结果。如果你想实现改变输出数据分布以外的功能,比如精确生成某类物体、向模型中加入一些改变画风的关键词,那你应该使用更加先进的技术,而不仅仅是用最基本的 LoRA 微调。
总结
LoRA 是当今深度学习领域中常见的技术。对于 SD,LoRA 则是能够编辑单幅图片、调整整体画风,或者是通过修改训练目标来实现更强大的功能。LoRA 的原理非常简单,它其实就是用两个参数量较少的矩阵来描述一个大参数矩阵在微调中的变化量。Diffusers 库提供了非常便利的 SD LoRA 训练脚本。相信读完了本文后,我们能知道如何用 Diffusers 训练 LoRA,修改训练中的主要参数,并在简单的单图片 LoRA 编辑任务上验证训练的正确性。利用这些知识,我们也能把 LoRA 拓展到风格化生成及其他应用上。
本文的项目网址:https://github.com/SingleZombie/DiffusersExample/tree/main/LoRA