Python-稳定扩散使用指南-全-
Python 稳定扩散使用指南(全)
原文:
zh.annas-archive.org/md5/317b14a0dd360d5c03f7015069fb4860
译者:飞龙
前言
当稳定扩散于2022年8月22日发布时,这个基于扩散的图像生成模型迅速吸引了全世界的关注。其模型和源代码都是完全开源的,托管在GitHub上。随着数百万社区参与者和用户的加入,发布了众多新的混合模型。创建了诸如稳定扩散WebUI和InvokeAI等工具。
虽然稳定扩散WebUI工具可以生成由扩散模型驱动的奇妙图像,但其可用性有限。来自Hugging Face的开源Diffusers包允许用户使用Python完全控制稳定扩散。然而,它缺少许多关键特性,例如加载自定义LoRA模型和文本反转,利用社区共享的模型/检查点,调度和加权提示,无限提示令牌,修复图像分辨率,以及放大。本书将帮助您克服Diffusers的限制,实现高级特性,创建一个完全定制和工业级的稳定扩散应用。
在本书结束时,您不仅将能够使用Python生成和编辑图像,还能利用本书中提供的解决方案构建适用于您业务和用户的稳定扩散应用。
本书面向的对象
本书面向那些希望全面了解图像生成和扩散模型工作原理的AI图像和艺术生成爱好者。
本书也适合对全面理解AI图像生成和精确控制扩散模型感兴趣的艺术家。
针对那些旨在基于稳定扩散创建AI图像生成应用的Python应用开发者,本书也将提供帮助。
最后,本书旨在帮助数据科学家、机器学习工程师和研究人员,他们希望通过编程方式控制稳定扩散过程、自动化流水线、构建自定义流水线,并使用Python进行测试和验证。
本书涵盖的内容
第1章,介绍稳定扩散,提供了对AI图像生成技术稳定扩散的介绍。
第2章,设置稳定扩散的环境,介绍了如何设置CUDA和Python环境以运行稳定扩散模型。
第3章,使用稳定扩散生成图像,是一个快速入门章节,帮助您开始使用Python通过稳定扩散生成图像。
第4章,理解扩散模型背后的理论,深入探讨了扩散模型的内部机制。
第5章,理解稳定扩散的工作原理,涵盖了稳定扩散背后的理论。
第6章, 使用稳定扩散模型,涵盖了模型数据处理以及模型文件的转换和加载。
第7章, 优化性能和VRAM使用,教你如何提高性能并减少VRAM使用。
第8章, 使用社区共享的LoRAs,展示了如何使用社区共享的LoRAs与稳定扩散检查点模型。
第9章, 使用文本反转,使用社区共享的文本反转与稳定扩散检查点模型。
第10章, 解锁77个令牌限制并启用提示权重,涵盖了如何构建自定义提示处理代码以使用具有加权重要性分数的无限制大小提示。具体来说,我们将探讨如何为单个提示或令牌分配不同的权重,从而微调模型注意力并生成更准确的结果。
第11章, 图像修复与超分辨率,展示了如何使用稳定扩散修复和提升图像。
第12章, 计划提示解析,展示了如何构建自定义管道以支持计划提示。
第13章, 使用ControlNet生成图像,涵盖了如何使用ControlNet与稳定扩散检查点模型。
第14章, 使用稳定扩散生成视频,展示了如何使用AnimateDiff与稳定扩散一起生成短视频剪辑,并理解视频生成的理论。
第15章, 使用BLIP-2和LLaVA生成图像描述,介绍了如何使用大型语言模型(LLMs)从图像中提取描述。
第16章, 探索稳定扩散XL,展示了如何开始使用更新、更好的稳定扩散模型——稳定扩散XL。
第17章, 构建优化提示以用于稳定扩散,讨论了编写稳定扩散提示以生成更好图像的技术,以及利用LLMs自动生成提示。
第18章, 应用:对象编辑和风格迁移,涵盖了如何使用稳定扩散和相关机器学习模型编辑图像以及将风格从一个图像转移到另一个图像。
第19章, 生成数据持久化,展示了如何将图像生成提示和参数保存到生成的PNG图像中。
第20章, 创建交互式用户界面,展示了如何使用开源框架Gradio构建稳定扩散WebUI。
第21章,扩散模型迁移学习,介绍了如何从头开始训练稳定扩散LoRA。
第22章,探索稳定扩散之外的内容,提供了有关稳定扩散、AI以及如何了解最新发展的更多信息。
要充分利用本书
您需要具备一些Python编程语言的经验。熟悉神经网络和PyTorch将有助于阅读和运行本书中的代码。
免责声明:
这本书的编写考虑了道德规范和法规。请避免将在这里获得的知识用于任何不道德的目的。请参阅第22章以深入了解使用AI的伦理问题。
本书涵盖的软件/硬件 | 操作系统要求 |
---|---|
Python 3.10+ | Linux、Windows或macOS |
Nvidia GPU(Apple M芯片可能可行,但强烈推荐Nvidia GPU) | |
Hugging Face Diffusers |
请参阅第2章以获取设置环境的详细步骤。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的GitHub仓库(下一节中提供链接)获取代码。这样做将有助于您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从GitHub(https://github.com/PacktPublishing/Using-Stable-Diffusion-with-Python)下载本书的示例代码文件。如果代码有更新,它将在GitHub仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在https://github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter昵称。以下是一个示例:“在这里,让我们使用controlnet-openpose-sdxl-1.0
open pose ControlNet for SDXL。”
代码块设置为以下格式:
import torch
from diffusers import StableDiffusionPipeline
# load model
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16
).to("cuda:0")
任何命令行输入或输出都按以下方式编写:
$ pip install pandas
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“点击运行按钮后,进度条将出现在输出文本框的位置。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们欢迎读者的反馈。
customercare@packtpub.com
并在邮件主题中提及书名。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
copyright@packt.com
并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《使用Python进行稳定扩散》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费PDF副本
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
请放心,现在购买每一本Packt书籍,您都可以免费获得该书的DRM免费PDF版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接
https://packt.link/free-ebook/9781835086377
-
提交您的购买证明
-
就这些!我们将直接将您的免费PDF和其他好处发送到您的电子邮件中。
第1部分 – 稳定扩散的旋风
欢迎来到稳定扩散的迷人世界,这是一个快速发展的领域,它彻底改变了我们处理图像生成和编辑的方式。在我们旅程的第一部分,我们将全面探索基础知识,为深入理解这项强大的技术打下基础。
在接下来的六章中,我们将深入探讨稳定扩散的核心概念、原则和应用,为进一步的实验和创新提供坚实的基础。我们将从介绍稳定扩散的基础知识开始,然后提供设置成功环境的实战指南。您将学习如何使用稳定扩散生成令人惊叹的图像,然后更深入地探讨扩散模型的理论基础以及稳定扩散如何施展其魔法的复杂性。
到本部分结束时,您将全面了解稳定扩散,从其底层机制到实际应用,这将使您能够利用其潜力并创作出令人瞩目的视觉内容。那么,让我们深入探索稳定扩散的奇妙之处吧!
本部分包含以下章节:
第一章:介绍 Stable Diffusion
Stable Diffusion 是一个深度学习模型,它利用扩散过程从引导指令和图像中生成高质量的美术作品。
在本章中,我们将向您介绍 AI 图像生成技术,即 Stable Diffusion,并了解它是如何发展到现在的。
与其他深度学习图像生成模型不同,例如 OpenAI 的 DALL-E 2,Stable Diffusion 通过从随机的噪声潜在张量开始,然后逐渐向其中添加详细信息来工作。添加的细节量由一个扩散过程决定,该过程受数学方程式控制(我们将在第五章中深入探讨细节)。在最终阶段,模型将潜在张量解码成像素图像。
自 2022 年创建以来,Stable Diffusion 已被广泛用于生成令人印象深刻的图像。例如,它可以生成与真实照片难以区分的人物、动物、物体和场景的图像。图像是通过特定的指令生成的,例如 “在月球表面奔跑的猫” 或 “一位宇航员骑马的照片”。
这里是一个用于与 Stable Diffusion 一起生成图像的提示示例:
"一位宇航员骑马的照片"。
Stable Diffusion 将生成以下图像:
图 1.1:由 Stable Diffusion 生成的宇航员骑马的图片
在我按下 Enter 按钮之前,这张图片并不存在。它是通过我和 Stable Diffusion 的协作创建的。Stable Diffusion 不仅理解我们给出的描述,还会为图像添加更多细节。
除了文本到图像的生成之外,Stable Diffusion 还通过自然语言促进照片的编辑。为了说明这一点,再次考虑前面的图像。我们可以使用自动生成的蒙版和提示将太空背景替换为蓝天和山脉。
可以使用 background
提示生成背景蒙版,而 blue sky and mountains
提示用于指导 Stable Diffusion 将初始图像转换为以下形式:
图 1.2:将背景替换为蓝天和山脉
无需鼠标点击或拖动,也不需要额外的付费软件,如 Photoshop。您可以使用纯 Python 和 Stable Diffusion 实现这一点。Stable Diffusion 可以仅使用 Python 代码执行许多其他任务,这些内容将在本书的后续章节中介绍。
稳定扩散是一个强大的工具,有可能彻底改变我们创建和交互图像的方式。它可以用于创建电影、视频游戏和其他应用中的逼真图像。它还可以用于生成用于营销、广告和装饰的个性化图像。
这里是稳定扩散的一些关键特性:
-
它可以从文本描述中生成高质量的图像
-
它基于扩散过程,这是一种比其他方法更稳定、更可靠的生成图像的方式
-
现在有大量可公开访问的预训练模型(10,000+),并且还在不断增长
-
新的研究和应用正在建立在稳定扩散的基础上
-
它是开源的,任何人都可以使用
在我们继续之前,让我简要介绍一下近年来扩散模型的演变。
扩散模型的演变
扩散模型并非一直可用,正如罗马不是一天建成的。为了从高层次上了解这项技术,在本节中,我们将讨论近年来扩散模型的整体演变。
在Transformer和Attention之前
不久前,卷积神经网络(CNNs)和残差神经网络(ResNets)主导了机器学习中的计算机视觉领域。
卷积神经网络(CNNs)和残差网络(ResNets)在引导对象检测和面部识别等任务中已被证明是非常有效的。这些模型已在各个行业得到广泛应用,包括自动驾驶汽车和人工智能驱动的农业。
然而,CNNs和ResNets存在一个显著的缺点:它们只能识别其训练集中的对象。要检测一个完全新的对象,必须将新的类别标签添加到训练数据集中,然后重新训练或微调预训练模型。
这种限制源于模型本身,以及当时硬件的限制和训练数据的可用性。
Transformer改变了机器学习
由谷歌开发的Transformer模型彻底改变了计算机视觉领域,其影响始于对自然语言处理(NLP)的影响。
与依赖于预定义标签来计算损失并通过反向传播更新神经网络权重的传统方法不同,Transformer模型以及注意力机制引入了一个开创性的概念。它们利用训练数据本身进行训练和标记。
让我们以下面的句子为例:
“稳定扩散可以使用文本来生成图像”
假设我们将单词序列输入到神经网络中,但不包括最后一个单词 text:
“稳定扩散可以使用 来生成图像”
使用这个提示,模型可以根据其当前权重预测下一个单词。比如说它预测了 apple。单词 apple 的编码嵌入在向量空间中与 text 有显著差异,就像两个之间有很大差距的数字一样。这个差距值可以用作损失值,然后通过反向传播更新权重。
通过在训练和更新过程中重复这个过程数百万次甚至数十亿次,模型的权重逐渐学会在句子中产生下一个合理的单词。
机器学习模型现在可以通过设计适当的损失函数学习各种任务。
OpenAI 的 CLIP 发生了重大变化
研究人员和工程师迅速认识到 Transformer 模型的潜力,正如著名机器学习论文《Attention Is All You Need》的结论部分所提到的。作者陈述如下:
我们对基于注意力的模型未来的发展感到兴奋,并计划将其应用于其他任务。我们计划将 Transformer 扩展到涉及除文本之外的其他输入和输出模态的问题,并研究局部、受限的注意力机制,以有效地处理图像、音频、以及视频*等大型输入和输出。
如果你已经阅读了这篇论文并掌握了基于 Transformer 和注意力机制的模型的卓越能力,你可能会受到启发,重新构想自己的工作并利用这种非凡的力量。
OpenAI 的研究人员抓住了这种力量,创建了一个名为 CLIP [1] 的模型,该模型使用注意力机制和 Transformer 模型架构来训练图像分类模型。该模型能够无需标记数据对广泛的图像进行分类。它是第一个在从互联网上提取的 4 亿个图像-文本对上训练的大型规模图像分类模型。
尽管在 OpenAI 的 CLIP 模型之前已有类似的研究,但根据 CLIP 论文作者 [1] 的观点,这些结果并不令人满意:
这些弱监督模型与最近直接从自然语言学习图像表示的探索之间的一个关键区别是规模。
事实上,规模在解锁通用图像识别的非凡超级能力中起着关键作用。当其他模型使用了 20 万张图像时,CLIP 团队使用令人敬畏的 4 亿张图像与来自公共互联网的文本数据一起训练了他们的模型。
结果令人震惊。CLIP 使图像识别和分割摆脱了预定义标签的限制。它可以检测到先前模型难以处理的对象。CLIP 通过其大规模模型带来了重大变革。鉴于 CLIP 的巨大影响力,研究人员在思考它是否也可以用于从文本生成图像。
生成图像
仅使用CLIP,我们仍然无法根据文本描述生成逼真的图像。例如,如果我们要求CLIP画一个苹果,模型会合并各种类型的苹果,不同的形状、颜色、背景等等。CLIP可能会生成一个一半绿色一半红色的苹果,这可能不是我们想要的。
你可能熟悉生成对抗网络(GANs),它能够生成高度逼真的图像。然而,在生成过程中无法使用文本提示。GANs已经成为图像处理任务(如人脸修复和图像上采样)的复杂解决方案。尽管如此,仍需要一种新的创新方法来利用基于引导描述或提示的图像生成模型。
2020年6月,Jonathan Ho等人发表了一篇题为《基于扩散的概率模型去噪》的论文[3],介绍了一种基于扩散的图像生成概率模型。术语扩散借自热力学。扩散的原始含义是粒子从高浓度区域向低浓度区域的运动。这种扩散的想法启发了机器学习研究人员将其应用于去噪和采样过程。换句话说,我们可以从一个噪声图像开始,通过去除噪声逐渐细化它。去噪过程逐渐将高噪声水平的图像转换成原始图像的更清晰版本。因此,这种生成模型被称为去噪扩散****概率模型。
这种方法背后的想法是巧妙的。对于任何给定的图像,我们向原始图像添加有限数量的正态分布的噪声图像,有效地将其转换为一个完全噪声的图像。如果我们训练一个模型,该模型可以在CLIP模型的引导下逆转这种扩散过程,会怎样呢?令人惊讶的是,这种方法是有效的[4]。
DALL-E 2和Stable Diffusion
2022年4月,OpenAI发布了DALL-E 2,并附带了一篇题为《基于CLIP潜力的分层文本条件图像生成》的论文[4]。DALL-E 2在全球范围内引起了广泛关注。它生成了一大批令人惊叹的图像,在社交网络和主流媒体中广泛传播。人们不仅对生成的图像质量感到惊讶,也对它能够创造从未存在过的图像的能力感到惊讶。DALL-E 2实际上在创作艺术作品。
可能是巧合,2022年4月,CompVis发表了一篇题为《基于潜在扩散模型的超高分辨率图像合成》的论文[5],介绍了一种基于扩散的文本引导图像生成模型。在CompVis的工作基础上,CompVis、Stability AI和LAION的研究人员和工程师合作,于2022年8月发布了DALL-E 2的开源版本,名为Stable Diffusion。
为什么选择Stable Diffusion
虽然 DALL-E 2 和其他商业图像生成模型如Midjourney可以在不要求复杂的环境设置或硬件准备的情况下生成令人瞩目的图像,但这些模型是闭源的。因此,用户对生成过程有限制,无法使用自己的定制模型,也无法向平台添加自定义功能。
另一方面,Stable Diffusion是一个在CreativeML Open RAIL-M许可下发布的开源模型。用户不仅有权使用该模型,还可以阅读源代码,添加功能,并从社区共享的无数自定义模型中受益。
使用哪个Stable Diffusion
当我们说Stable Diffusion时,我们真正指的是哪个Stable Diffusion?以下是一个不同Stable Diffusion工具及其差异的列表:
-
Stable Diffusion GitHub仓库 (https://github.com/CompVis/stable-diffusion): 这是CompVis提供的Stable Diffusion的原始实现,由许多杰出的工程师和研究人员贡献。这是一个PyTorch实现,可用于训练和生成图像、文本和其他创意内容。截至2023年写作时,该库的活跃度较低。其README页面还建议用户使用Hugging Face的Diffusers来使用和训练扩散模型。
-
Hugging Face的Diffusers: Diffusers是由Hugging Face开发的用于训练和使用扩散模型的库。它是生成图像、音频甚至分子3D结构的最新、预训练扩散模型的首选库。截至写作时,该库得到了良好的维护,并正在积极开发。几乎每天都有新代码添加到其GitHub仓库中。
-
AUTOMATIC1111的Stable Diffusion WebUI: 这可能是目前最受欢迎的基于Web的应用程序,允许用户使用Stable Diffusion生成图像和文本。它提供了一个GUI界面,使得用户可以轻松地实验不同的设置和参数。
-
InvokeAI: InvokeAI最初是作为Stable Diffusion项目的分支开发的,但后来已经发展成为一个独特的平台。InvokeAI提供了一系列功能,使其成为创意人士的强大工具。
-
ComfyUI: ComfyUI是一个基于节点的UI,利用Stable Diffusion。它允许用户构建定制的流程,包括图像后处理和转换。它是一个强大且灵活的图形用户界面,用于Stable Diffusion,以其基于节点的设计为特点。
在这本书中,当我提到Stable Diffusion时,我指的是Stable Diffusion模型,而不是刚刚列出的GUI工具。本书的重点将集中在使用纯Python的Stable Diffusion。我们的示例代码将使用Diffusers的管道,并利用来自Stable Diffusion WebUI和学术论文开源代码等。
为什么选择这本书
尽管Stable Diffusion GUI工具可以由扩散模型驱动生成令人惊叹的图像,但其可用性有限。存在数十个调节旋钮(正在添加更多滑块和按钮)和特定术语,有时使得生成高质量图像变成了一场猜谜游戏。另一方面,来自Hugging Face的开源Diffusers包使用Python让用户对Stable Diffusion拥有完全控制权。然而,它缺少许多关键特性,如加载自定义LoRA和文本反转,利用社区共享的模型/检查点,调度,加权提示,无限提示令牌,以及高分辨率图像修复和放大(然而,Diffusers包仍在不断改进)。
本书旨在帮助您从扩散模型的内部视角理解所有复杂的术语和调节旋钮。本书还将协助您克服扩散器的局限性,实现缺失的功能和高级特性,以创建一个完全定制的Stable Diffusion应用程序。
考虑到人工智能技术发展的快速步伐,本书还旨在使您能够快速适应即将到来的变化。
到本书结束时,您不仅能够使用Python生成和编辑图像,还能利用本书中提供的解决方案构建适合您业务和用户的Stable Diffusion应用程序。
让我们开始这段旅程。
参考文献
-
从自然语言监督学习可迁移的视觉模型:https://arxiv.org/abs/2103.00020
-
去噪扩散概率模型:https://arxiv.org/abs/2006.11239
-
使用CLIP潜变量进行分层文本条件图像生成:https://arxiv.org/abs/2204.06125v1
-
使用潜在扩散模型进行高分辨率图像合成:https://arxiv.org/abs/2112.10752
-
DALL-E 2:https://openai.com/dall-e-2
第二章:设置稳定扩散的环境
欢迎来到第二章。在本章中,我们将专注于设置运行稳定扩散的环境。我们将涵盖所有必要的步骤和方面,以确保与稳定扩散模型一起工作时体验顺畅。我们的主要目标是帮助您了解每个组件的重要性以及它们如何对整个过程做出贡献。
本章内容如下:
-
稳定扩散运行所需的硬件要求介绍
-
安装所需软件依赖项的详细步骤:NVIDIA 的 CUDA、Python、Python 虚拟环境(可选但推荐),以及 PyTorch
-
没有 GPU 的用户的其他替代选项,例如 Google Colab 和配备硅 CPU(M 系列的)的 Apple MacBook
-
设置过程中常见问题的故障排除
-
维护稳定环境的技巧和最佳实践
我们将首先概述稳定扩散(Stable Diffusion)的概念、其重要性以及在各个领域的应用。这将帮助您更好地理解核心概念及其重要性。
接下来,我们将详细介绍每个依赖项的安装步骤,包括 CUDA、Python 和 PyTorch。我们还将讨论使用 Python 虚拟环境的优点,并指导您如何设置一个。
对于没有访问带有 GPU 的机器的用户,我们将探讨替代选项,例如 Google Colab。我们将提供使用这些服务的全面指南,并讨论与之相关的权衡。
最后,我们将解决在设置过程中可能出现的常见问题,并提供故障排除技巧。此外,我们将分享维护稳定环境的最佳实践,以确保与稳定扩散模型一起工作时体验顺畅。
到本章结束时,您将拥有为稳定扩散设置和维护定制的环境的基础,让您能够高效地构建和实验您的模型。
运行稳定扩散所需的硬件要求
本节将讨论运行稳定扩散模型所需的硬件要求。本书将涵盖稳定扩散 v1.5和稳定扩散 XL(SDXL)版本。这两个版本也是本书撰写时最常用的模型。
2022 年 10 月发布的稳定扩散 v1.5 被视为通用模型,可以与 v1.4 互换使用。另一方面,2023 年 7 月发布的 SDXL 以其更有效地处理更高分辨率的能力而闻名,与稳定扩散 v1.5 相比。它可以在不牺牲质量的情况下生成更大尺寸的图像。
实质上,稳定扩散是一组包括以下内容的模型:
-
标记器(Tokenizer):将文本提示转换为一系列标记。
-
文本编码器:Stable Diffusion文本编码器是一个特殊的Transformer语言模型——具体来说,是CLIP模型的文本编码器。在SDXL中,还使用了更大尺寸的OpenCLIP [6] 文本编码器,将标记编码成文本嵌入。
-
变分自编码器(VAE):这会将图像编码到潜在空间,并将其解码回图像。
-
UNet:这是去噪过程发生的地方。UNet结构被用来理解噪声/去噪循环中的步骤。它接受某些元素,如噪声、时间步数据和一个条件信号(例如,文本描述的表示),并预测可用于去噪过程的噪声残差。
Stable Diffusion的组件提供神经网络权重数据,除了分词器。虽然理论上CPU可以处理训练和推理,但带有GPU或并行计算设备的物理机器可以提供学习并运行Stable Diffusion模型的最佳体验。
GPU
理论上,Stable Diffusion模型可以在GPU和CPU上运行。实际上,基于PyTorch的模型在NVIDIA GPU上使用CUDA运行效果最佳。
Stable Diffusion需要至少4 GB VRAM的GPU。根据我的经验,4 GB VRAM的GPU只能让你生成512x512的图像,但生成它们可能需要很长时间。至少8 GB VRAM的GPU可以提供相对愉悦的学习和使用体验。VRAM的大小越大越好。
本书中的代码在8 GB VRAM的NVIDIA RTX 3070Ti和24 GB VRAM的RTX 3090上进行了测试。
系统内存
GPU和CPU之间将进行大量的数据传输,一些Stable Diffusion模型可能会轻松占用高达6 GB的RAM。请至少准备16 GB的系统RAM;32 GB RAM会更好——越多越好,尤其是对于多个模型。
存储
请准备一个大硬盘。默认情况下,Hugging Face包会将模型数据下载到系统驱动器中的缓存文件夹。如果你只有256 GB或512 GB的存储空间,你会发现它很快就会用完。建议准备1 TB的NVME SSD,尽管2 TB或更多会更好。
软件需求
现在我们已经准备好了硬件,Stable Diffusion需要额外的软件来支持其执行并提供使用Python的更好控制。本节将为你提供准备软件环境的步骤。
CUDA安装
如果你使用的是Microsoft Windows,请首先安装Microsoft Visual Studio (VS) [5]。VS将安装CUDA所需的所有其他依赖包和二进制文件。你可以简单地选择VS的最新社区版本,这是免费的。
现在,前往NVIDIA CUDA下载页面 [1] 下载CUDA安装文件。以下截图显示了选择Windows 11的CUDA示例:
图2.1:选择Windows的CUDA安装下载文件
下载CUDA安装文件,然后双击此文件,就像安装任何其他Windows应用程序一样安装CUDA。
如果您使用的是Linux操作系统,安装CUDA的过程略有不同。您可以执行NVIDIA提供的Bash脚本来自动化安装。以下是详细步骤:
-
为了确保最小化错误,最好首先卸载所有NVIDIA驱动程序,所以如果您已经安装了NVIDIA的驱动程序,请使用以下命令来卸载它:
sudo apt-get purge nvidia*
sudo apt-get autoremove
然后,重新启动您的系统:
sudo reboot
-
安装GCC。GNU编译器集合(GCC)是一组用于各种编程语言的编译器,如C、C++、Objective-C、Fortran、Ada等。它是由GNU项目开发的开源项目,并且在Unix-like操作系统(包括Linux)上广泛用于编译和构建软件。如果没有安装GCC,我们在CUDA安装过程中会遇到错误。使用以下命令安装它:
sudo apt install gcc
-
在CUDA下载页面[2]上为您的系统选择正确的CUDA版本。以下截图显示了为Ubuntu 22.04选择CUDA的示例:
图2.2:选择Linux的CUDA安装下载文件
在您选择之后,页面将显示处理整个安装过程的命令脚本。以下是一个示例:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin
sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/12.1.1/local_installers/cuda-repo-ubuntu2204-12-1-local_12.1.1-530.30.02-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu2204-12-1-local_12.1.1-530.30.02-1_amd64.deb
sudo cp /var/cuda-repo-ubuntu2204-12-1-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda
注意
在您阅读这本书的时候,脚本可能已经被更新。为了避免错误和潜在的安装失败,我建议打开页面并使用反映您选择的脚本。
安装Windows、Linux和macOS上的Python
我们首先将为Windows安装Python。
安装Windows上的Python
您可以访问https://www.python.org/并下载Python 3.9或Python 3.10进行安装。
经过多年的手动下载和点击安装过程后,我发现使用包管理器来自动化安装非常有用。使用包管理器,您只需编写一次脚本,保存它,然后下次需要安装软件时,您只需在终端窗口中运行相同的脚本即可。Windows上最好的包管理器之一是Chocolatey (https://chocolatey.org/)。
一旦您安装了Chocolatey,可以使用以下命令安装Python 3.10.6:
choco install python --version=3.10.6
创建Python虚拟环境:
pip install --upgrade --user pip
pip install virtualenv
python -m virtualenv venv_win_p310
venv_win_p310\Scripts\activate
python -m ensurepip
python -m pip install --upgrade pip
我们将继续安装Linux上Python的步骤。
安装Linux上的Python
现在我们将为Linux(Ubuntu)安装Python。按照以下步骤操作:
-
安装所需的软件包:
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.10
sudo apt-get install python3.10-dev
sudo apt-get install python3.10-distutils
-
安装
pip
:curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.10 get-pip.py
-
创建Python虚拟环境:
python3.10 -m pip install --user virtualenv
python3.10 -m virtualenv venv_ubuntu_p310
. venv_ubuntu_p310/bin/activate
安装macOS上的Python
如果您使用的是内置硅芯片的Mac(带有Apple Mx CPU),您很可能已经安装了Python。您可以使用以下命令测试您的Mac上是否已安装Python:
python3 --version
如果您的机器还没有 Python 解释器,您可以使用 Homebrew [7] 通过一条简单的命令来安装它,如下所示:
brew install python
请记住,Python 版本通常会定期更新,通常每年更新一次。您可以通过更改版本号来安装特定的 Python 版本。例如,您可以将 python3.10
更改为 python3.11
。
安装 PyTorch
Hugging Face Diffusers 包依赖于 PyTorch 包,因此我们需要安装 PyTorch 包。访问 PyTorch 入门页面 (https://pytorch.org/get-started/locally/) 并选择适合您系统的适当 PyTorch 版本。以下是在 Windows 上的 PyTorch 截图:
图 2.3:安装 PyTorch
接下来,使用动态生成的命令来安装 PyTorch:
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117
除了 CUDA 11.7 之外,还有 CUDA 11.8。版本的选择将取决于您机器上安装的 CUDA 版本。
您可以使用以下命令来查找您的 CUDA 版本:
nvcc --version
您也可以使用以下命令:
nvidia-smi
您机器的 CUDA 版本可能高于列出的 11.7 或 11.8 版本,例如 12.1。通常,某些模型或包需要特定的版本。对于 Stable Diffusion,只需安装最新版本即可。
如果您正在使用 Mac,请选择 Mac 选项来安装适用于 macOS 的 PyTorch。
如果您正在使用 Python 虚拟环境,请确保在激活的虚拟环境中安装 PyTorch。否则,如果您意外地在虚拟环境外安装 PyTorch,然后在虚拟环境中运行 Python 代码,您可能会遇到 PyTorch 没有正确安装的问题。
运行 Stable Diffusion 管道
现在您已经安装了所有依赖项,是时候运行一个 Stable Diffusion 管道来测试环境是否正确设置了。您可以使用任何 Python 编辑工具,例如 VS Code 或 Jupyter Notebook,来编辑和执行 Python 代码。按照以下步骤操作:
-
安装 Hugging Face Diffusers 的包:
pip install diffusers
pip install transformers scipy ftfy accelerate
-
启动 Stable Diffusion 管道:
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16)
pipe.to("cuda") # mps for mac
如果您正在使用 Mac,将
cuda
更改为mps
。尽管 macOS 受支持并且可以使用 Diffusers 包生成图像,但其性能相对较慢。作为比较,使用 Stable Diffusion V1.5 生成一个 512x512 的图像,NVIDIA RTX 3090 可以达到大约每秒 20 次迭代,而 M3 Max CPU 在默认设置下只能达到大约每秒 5 次迭代。 -
生成图像:
prompt = "a photo of an astronaut riding a horse on mars,blazing fast, wind and sand moving back"
image = pipe(
prompt, num_inference_steps=30
).images[0]
image
如果您看到一位宇航员骑马的图像,那么您在物理机器上已经正确设置了所有环境。
使用 Google Colaboratory
Google Colaboratory(或 Google Colab)是 Google 提供的在线计算服务。本质上,Google Colab 是一个具有 GPU/CUDA 功能的在线 Jupyter Notebook。
它的免费笔记本可以提供与NVIDIA RTX 3050或RTX 3060相当的15 GB VRAM的CUDA计算。如果你没有现成的独立GPU,性能还算不错。
让我们来看看使用Google Colab的优缺点:
-
pip
和资源下载都很快速 -
缺点:
-
每个笔记本都有一个磁盘限制
-
你无法完全控制后端服务器;终端访问需要Colab Pro订阅
-
性能无法保证,因此在高峰时段你可能会遇到慢速GPU推理,并且可能会因为长时间计算而被断开连接。
-
每次重启笔记本时,Colab笔记本的计算环境都会被重置;换句话说,每次启动笔记本时,你都需要重新安装所有包和下载模型文件。
-
使用Google Colab运行稳定扩散管道
下面是开始使用Google Colab的详细步骤:
-
从 https://colab.research.google.com/ 创建一个新的实例。
-
点击 运行 | 更改运行时类型 并选择 T4 GPU,如图 图2.4 所示:
图2.4:在Google Colab笔记本中选择GPU
-
创建一个新的单元格,并使用以下命令检查GPU和CUDA是否工作:
!nvidia-smi
-
安装Hugging Face Diffusers的包:
!pip install diffusers
!pip install transformers scipy ftfy accelerate ipywidgets
-
开始启动稳定的扩散管道:
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16)
pipe.to("cuda")
-
生成图像:
prompt = "a photo of an astronaut riding a horse on mars,blazing fast, wind and sand moving back"
image = pipe(
prompt, num_inference_steps=30
).images[0]
image
几秒钟后,你应该能看到如图 图2.5 所示的结果:
图2.5:在Google Colab中运行稳定扩散管道
如果你看到生成的图像如图 图2.5 所示,那么你已经成功设置了Diffusers包,在Google Colab中运行稳定扩散模型。
摘要
有人说,开始训练机器学习模型最具挑战性的部分不是数学或其内部逻辑。通常,最大的障碍是设置一个合适的运行环境来运行模型。工程师和教授们花整个周末试图在他们的实验室机器上安装CUDA的情况并不少见。这可能是由于缺少依赖项、跳过的步骤或版本不兼容造成的。
我专门用了一章来介绍安装过程,希望这些详细的步骤能帮助你避免常见的陷阱。通过遵循这些步骤,你将能够深入了解稳定扩散模型,并开始图像生成,障碍最小化。
此外,你安装的软件和包也将适用于基于Transformer的大型语言模型。
在下一章中,我们将开始使用稳定扩散来生成图像。
参考文献
-
Microsoft Windows的CUDA安装指南:https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/index.html
-
NVIDIA CUDA 下载: https://developer.nvidia.com/cuda-downloads
-
Google Colab: https://colab.research.google.com/
-
Hugging Face Diffusers 安装: https://huggingface.co/docs/diffusers/installation
-
Visual Studio Community 下载: https://visualstudio.microsoft.com/vs/community/
-
OpenCLIP GitHub 仓库: https://github.com/mlfoundations/open_clip
-
Homebrew: https://brew.sh/
第三章:使用Stable Diffusion生成图像
在本章中,我们将通过利用Hugging Face Diffusers包(https://github.com/huggingface/diffusers)和开源包来开始使用常见的Stable Diffusion功能。正如我们在 第1章 中提到的,Stable Diffusion简介,Hugging Face Diffusers是目前最广泛使用的Stable Diffusion的Python实现。在我们探索图像生成时,我们将介绍常用的术语。
假设你已经安装了所有包和依赖项;如果你看到错误消息说找不到GPU或需要CUDA,请参考 第2章 来设置运行Stable Diffusion的环境。
通过本章,我旨在通过使用Hugging Face的Diffusers包使你熟悉Stable Diffusion。我们将在下一章深入探讨Stable Diffusion的内部机制。
在本章中,我们将涵盖以下主题:
-
如何使用Hugging Face令牌登录Hugging Face
-
使用Stable Diffusion生成图像
-
使用生成种子来重现图像
-
使用Stable Diffusion调度器
-
交换或更改Stable Diffusion模型
-
使用指导尺度
让我们开始吧。
登录Hugging Face
你可以使用 huggingface_hub
库中的 login()
函数,如下所示:
from huggingface_hub import login
login()
通过这样做,你正在通过Hugging Face Hub进行身份验证。这允许你下载托管在Hub上的预训练扩散模型。不登录,你可能无法使用模型ID(如 runwayml/stable-diffusion-v1-5
)下载这些模型。
当你运行前面的代码时,你正在提供你的Hugging Face令牌。你可能想知道访问令牌的步骤,但不用担心。令牌输入对话框将提供链接和信息来 访问 令牌。
登录后,你可以使用Diffusers包中的 from_pretrained()
函数下载预训练的扩散模型。例如,以下代码将从Hugging Face Hub下载 stable-diffusion-v1-5
模型:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
).to("cuda:0")
注意
你可能已经注意到,我使用 to("cuda:0")
而不是 to("cuda")
,因为在多GPU场景下,你可以更改CUDA索引来告诉Diffusers使用指定的GPU。例如,你可以使用 to("cuda:1")
来使用第二个启用了CUDA的GPU生成Stable Diffusion图像。
下载模型后,是时候使用Stable Diffusion生成图像了。
生成图像
现在我们已经将Stable Diffusion模型加载到GPU上,让我们生成一个图像。text2img_pipe
包含了管道对象;我们只需要提供一个 prompt
字符串,使用自然语言描述我们想要生成的图像,如下面的代码所示:
# generate an image
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt
).images[0]
image
阅读本文时,您完全可以根据自己的想法更改提示内容,例如,高分辨率,一只猫在火星表面奔跑的照片
或4k,一只猫驾驶飞机的高质量图像
。令人惊叹的是,Stable Diffusion可以根据纯自然语言的描述生成图像。
如果您在不更改代码的情况下运行前面的代码,您可能会看到这样的图像出现:
图3.1:一位宇航员骑马的图像
我说“您可能会看到这样的图像”,因为您几乎有99.99%的几率不会看到相同的图像;相反,您会看到外观和感觉相似的图像。为了使生成过程保持一致,我们需要另一个参数,称为generator
。
生成种子
在Stable Diffusion中,种子是一个用于初始化生成过程的随机数。种子用于创建一个噪声张量,然后由扩散模型用于生成图像。相同的种子、相同的提示和设置通常会产生相同的图像。
生成种子有两个原因:
-
可重复性:通过使用相同的种子,您可以使用相同的设置和提示一致地生成相同的图像。
-
探索:通过改变种子数字,您可以发现各种图像变体。这通常会导致新颖且引人入胜的图像出现。
当未提供种子数字时,Diffusers包会自动为每个图像创建过程生成一个随机数。然而,您可以选择指定您首选的种子数字,如下面的Python代码所示:
my_seed = 1234
generator = torch.Generator("cuda:0").manual_seed(my_seed)
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = generator
).images[0]
display(image)
在前面的代码中,我们使用torch
创建了一个带有手动种子的torch.Generator
对象。我们专门使用这个生成器进行图像生成。通过这样做,我们可以反复生成相同的图像。
生成种子是控制Stable Diffusion图像生成的一种方法。接下来,让我们进一步探讨调度器以进行自定义。
采样调度器
在讨论了生成种子之后,现在让我们深入探讨Stable Diffusion图像生成的另一个重要方面:采样调度器。
原始的扩散模型在生成图像方面已经展示了令人印象深刻的结果。然而,一个缺点是缓慢的反向去噪过程,这通常需要1,000步将随机噪声数据空间转换为连贯的图像(具体来说,是潜在数据空间,我们将在第4章中进一步探讨这个概念)。这个过程非常耗时。
为了缩短图像生成过程,研究人员已经提出了几种解决方案。这个想法很简单:如果我们能够只对样本执行关键步骤,而不是进行1,000步去噪,那会怎么样?这个想法是可行的。采样器或调度器使扩散模型能够在仅仅20步内生成图像!
在Hugging Face Diffusers包中,这些有用的组件被称为调度器。然而,您在其他资源中也可能遇到采样器这个术语。您可以查看Diffusers Schedulers [2]页面以获取最新的支持调度器。
默认情况下,Diffusers包使用PNDMScheduler
。我们可以通过运行以下代码找到它:
# Check out the current scheduler
text2img_pipe.scheduler
代码将返回如下对象:
PNDMScheduler {
"_class_name": "PNDMScheduler",
"_diffusers_version": "0.17.1",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"num_train_timesteps": 1000,
"prediction_type": "epsilon",
"set_alpha_to_one": false,
"skip_prk_steps": true,
"steps_offset": 1,
"trained_betas": null
}
初看起来,PNDMScheduler
对象的字段可能看起来复杂且不熟悉。然而,随着您深入了解第4章和第5章中Stable Diffusion模型的内部机制,这些字段将变得更加熟悉和易于理解。未来的学习之旅将揭示Stable Diffusion模型的复杂性,并阐明PNDMScheduler
对象中每个字段的用途和重要性。
许多列表调度器可以在20到50步内生成图像。根据我的经验,Euler
调度器是最佳选择之一。让我们应用Euler
调度器来生成一个图像:
from diffusers import EulerDiscreteScheduler
text2img_pipe.scheduler = EulerDiscreteScheduler.from_config(
text2img_pipe.scheduler.config)
generator = torch.Generator("cuda:0").manual_seed(1234)
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = generator
).images[0]
display(image)
您可以通过使用num_inference_steps
参数来自定义去噪步骤的数量。更高的步数通常会导致更好的图像质量。在这里,我们将调度步骤设置为20
,并比较了默认的PNDMScheduler
和EulerDiscreteScheduler
的结果:
# Euler scheduler with 20 steps
from diffusers import EulerDiscreteScheduler
text2img_pipe.scheduler = EulerDiscreteScheduler.from_config(
text2img_pipe.scheduler.config)
generator = torch.Generator("cuda:0").manual_seed(1234)
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = generator,
num_inference_steps = 20
).images[0]
display(image)
下图显示了两个调度器之间的差异:
图3.2:左:20步的Euler调度器;右:20步的PNDMScheduler
在这个比较中,Euler调度器正确地生成了一张包含所有四条马腿的图像,而PNDM调度器提供了更多细节但遗漏了一条马腿。这些调度器表现出色,将整个图像生成过程从1,000步减少到仅20步,使得在家庭电脑上运行Stable Diffusion成为可能。
注意,每个调度器都有其优缺点。您可能需要尝试不同的调度器以找到最适合您的。
接下来,让我们探索用社区贡献的、微调过的替代模型替换原始Stable Diffusion模型的过程。
更改模型
在撰写本章时,有大量基于V1.5 Stable Diffusion模型微调的模型可供选择,这些模型由蓬勃发展的用户社区贡献。如果模型文件托管在Hugging Face,您可以通过更改其标识符轻松切换到不同的模型,如下面的代码片段所示:
# Change model to "stablediffusionapi/deliberate-v2"
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16
).to("cuda:0")
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt
).images[0]
display(image)
此外,您还可以使用从civitai.com下载的ckpt/safetensors
模型(http://civitai.com)。在这里,我们通过以下代码演示如何加载deliberate-v2
模型:
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_single_file(
"path/to/deliberate-v2.safetensors",
torch_dtype = torch.float16
).to("cuda:0")
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt
).images[0]
display(image)
当从本地文件加载模型时,主要区别在于使用from_single_file
函数而不是from_pretrained
。可以使用前面的代码加载一个ckpt
模型文件。
在本书的第6章中,我们将专门关注模型加载,涵盖Hugging Face和本地存储方法。通过实验各种模型,你可以发现改进、独特的艺术风格或更好的特定用例兼容性。
我们已经提到了生成种子、调度器和模型使用。另一个扮演关键角色的参数是guidance_scale
。让我们接下来看看它。
指导比例
指导比例或无分类器指导(CFG)是一个控制生成的图像对文本提示的遵守程度的参数。较高的指导比例将迫使图像更符合提示,而较低的指导比例将给Stable Diffusion更多空间来决定图像中要放入的内容。
这里是一个在保持其他参数不变的同时应用不同指导比例的示例:
import torch
generator = torch.Generator("cuda:0").manual_seed(123)
prompt = """high resolution, a photograph of an astronaut riding a horse on mars"""
image_3_gs = text2img_pipe(
prompt = prompt,
num_inference_steps = 30,
guidance_scale = 3,
generator = generator
).images[0]
image_7_gs = text2img_pipe(
prompt = prompt,
num_inference_steps = 30,
guidance_scale = 7,
generator = generator
).images[0]
image_10_gs = text2img_pipe(
prompt = prompt,
num_inference_steps = 30,
guidance_scale = 10,
generator = generator
).images[0]
from diffusers.utils import make_image_grid
images = [image_3_gs,image_7_gs,image_10_gs]
make_image_grid(images,rows=1,cols=3)
图3.3提供了并排比较:
图3.3:左:guidance_scale = 3;中:guidance_scale = 7;右:guidance_scale = 10
在实践中,除了遵守提示外,我们还可以注意到高指导比例设置有以下影响:
-
增加颜色饱和度
-
增加对比度
-
如果设置得太高可能会导致图像模糊
guidance_scale
参数通常设置在7
到8.5
之间。7.5
是一个很好的默认值。
摘要
在本章中,我们通过Hugging Face Diffusers包探讨了使用Stable Diffusion的基本要素。我们实现了以下目标:
-
登录Hugging Face以启用自动模型下载
-
使用生成器生成图像
-
使用调度器进行高效的图像创建
-
调整了指导比例以适应期望的图像质量
仅用几行代码,我们就成功地创建了图像,展示了Diffusers包的非凡能力。本章没有涵盖每个功能和选项;请记住,该包正在不断进化,新功能和增强功能定期添加。
对于渴望解锁Diffusers包全部潜力的人来说,我鼓励您探索其源代码。深入了解其内部工作原理,发现隐藏的宝藏,并从头开始构建Stable Diffusion管道。一段值得的旅程在等待着你!
git clone https://github.com/huggingface/diffusers
在下一章中,我们将深入了解包的内部结构,并学习如何构建一个针对您独特需求和偏好的自定义Stable Diffusion管道。
参考文献
-
使用潜在扩散模型进行高分辨率图像合成:https://arxiv.org/abs/2112.10752
-
Hugging Face Diffusers调度器:https://huggingface.co/docs/diffusers/api/schedulers/overview
第四章:理解扩散模型背后的理论
本章将深入探讨驱动扩散模型的理论,并了解系统的内部工作原理。神经网络模型是如何生成如此逼真的图像的呢?好奇心强的人想要揭开面纱,看看内部的工作机制。
我们将触及扩散模型的基础,旨在弄清楚其内部工作原理,并为下一章实现可行的流程奠定基础。
通过理解扩散模型的复杂性,我们不仅增强了我们对高级稳定扩散(也称为潜在扩散模型(LDMs))的理解,而且还能更有效地导航Diffusers包的源代码。
这项知识将使我们能够根据新兴需求扩展包的功能。
具体来说,我们将探讨以下主题:
-
理解图像到噪声的过程
-
更高效的正向****扩散过程
-
噪声到图像的训练过程
-
噪声到图像的采样过程
-
理解分类器引导去噪
到本章结束时,我们将深入探讨由Jonathan Ho等人最初提出的扩散模型的内部工作原理。我们将理解扩散模型的基础理念,并学习正向扩散过程。我们将了解扩散模型训练和采样的反向扩散过程,并学会启用文本引导的扩散模型。
让我们开始吧。
理解图像到噪声的过程
扩散模型的想法受到了热力学中扩散概念的启发。将一张图像视为一杯水,并向图像(水)中添加足够的噪声(墨水),最终将图像(水)变成完整的噪声图像(墨水水)。
如图4**.1所示,图像x 0可以被转换为一个几乎高斯(正态分布)的噪声图像x T。
图4.1:正向扩散和反向去噪
我们采用一个预定的正向扩散过程,表示为q,该过程系统地给图像引入高斯噪声,直到最终变成纯噪声。这个过程表示为q(x t | x t-1)。请注意,反向过程p θ(x t-1 | x t)仍然未知。
正向扩散过程的一步可以表示如下:
q(x t | x t-1) ≔ 𝒩(x t; √ _ 1 − β t x t-1 , β t I)
让我从左到右一点一点解释这个公式:
-
符号q(x t | x t-1)用来表示条件概率分布。在这种情况下,分布q表示在给定先前图像x t−1的情况下观察到噪声图像x t的概率。
-
公式中使用定义符号 := 而不是波浪符号 (∼) 是因为扩散前向过程是一个确定性过程。波浪符号 (∼) 通常用于表示分布。在这种情况下,如果我们使用波浪符号,公式将表示噪声图像是一个完整的高斯分布。然而,情况并非如此。t 步的噪声图像是由前一个图像和添加的噪声的确定性函数定义的。
-
那么为什么这里使用 𝒩 符号呢?𝒩 符号用于表示高斯分布。然而,在这种情况下,𝒩 符号被用来表示噪声图像的函数形式。
-
在右侧,分号之前,x_t 是我们希望在正态分布中的东西。分号之后,那些是分布的参数。分号通常用于分隔输出和参数。
-
βt 是第 t 步的噪声方差。√_1 − βt x_t−1 是新分布的均值。
-
为什么公式中使用大写的 I 呢?因为 RGB 图像可以有多个通道,而单位矩阵可以将噪声方差独立地应用于不同的通道。
使用 Python 添加高斯噪声到图像相当简单:
import numpy as np
import matplotlib.pyplot as plt
import ipyplot
from PIL import Image
# Load an image
img_path = r"dog.png"
image = plt.imread(img_path)
# Parameters
num_iterations = 16
beta = 0.1 # noise_variance
images = []
steps = ["Step:"+str(i) for i in range(num_iterations)]
# Forward diffusion process
for i in range(num_iterations):
mean = np.sqrt(1 - beta) * image
image = np.random.normal(mean, beta, image.shape)
# convert image to PIL image object
pil_image = Image.fromarray((image * 255).astype('uint8'), 'RGB')
# add to image list
images.append(pil_image)
ipyplot.plot_images(images, labels=steps, img_width=120)
要执行前面的代码,您还需要通过 pip install ipyplot
安装 ipyplot
包。提供的代码在图像上执行前向扩散过程的模拟,然后可视化这个过程在多次迭代中的进展。以下是代码每个部分所做操作的逐步解释:
-
导入库:
-
ipyplot
是一个库,用于以更交互的方式在 Jupyter 笔记本中绘制图像。 -
PIL
(代表Image
模块,用于图像处理)。
-
-
加载图像:
-
img_path
被定义为image
文件dog.png
的路径。 -
使用
plt.imread(img_path)
加载image
。
-
-
设置参数:
-
num_iterations
定义了扩散过程将被模拟的次数。 -
beta
是一个参数,用于模拟扩散过程中的噪声方差。
-
-
初始化列表:
-
images
被初始化为一个空列表,它将随后保存扩散过程每次迭代产生的 PIL 图像对象。 -
steps
是一个字符串列表,当图像被绘制时将作为标签使用,指示每个图像的步骤编号。
-
-
前向扩散过程:
-
一个
for
循环运行num_iterations
次,每次执行一个扩散步骤。mean
通过将图像乘以sqrt(1 - beta)
的因子来计算。 -
通过向均值添加高斯噪声生成新的图像,其中噪声的标准差为
beta
。这是通过np.random.normal
实现的。 -
结果的图像数组值被缩放到 0-255 范围,并转换为 8 位无符号整数格式,这是图像的常见格式。
-
pil_image
通过将图像数组转换为 RGB 模式的 PIL 图像对象来创建。
-
-
使用
ipyplot
在网格中绘制图像,如图 图 4.2 所示。
图 4.2:向图像添加噪声
从结果中,我们可以看到,尽管每个图像都来自正态分布函数,但并非每个图像都是完整的高斯分布,或者更严格地说,是 1000
,后来在稳定扩散中,步骤数减少到 20
到 50
之间。
如果 图 4.2 的最后一幅图像是各向同性的高斯分布,其 2D 分布可视化将呈现为一个圆圈;它以所有维度具有相等的方差为特征。换句话说,分布的扩散或宽度沿所有轴都是相同的。
让我们在添加 16 倍高斯噪声后绘制图像像素分布:
sample_img = image # take the last image from the diffusion process
plt.scatter(sample_img[:, 0], sample_img[:, 1], alpha=0.5)
plt.title("2D Isotropic Gaussian Distribution")
plt.xlabel("X")
plt.ylabel("Y")
plt.axis("equal")
plt.show()
结果如图 图 4.3 所示。
图 4.3:几乎各向同性的正态分布噪声图像
该图显示了代码如何仅用 16 步高效地将图像转换为几乎各向同性的正态分布噪声图像,如图 图 4.2 的最后一幅图像所示。
更高效的正向扩散过程
如果我们使用链式过程在 t 步计算一个噪声图像,它首先需要计算从 1 到 t - 1 步的噪声图像,这并不高效。我们可以利用一种称为重参数化[10]的技巧将原始链式过程转换为一步过程。这个技巧看起来是这样的。
如果我们有一个以 μ 为均值、σ² 为方差的高斯分布 z:
z ∼ 𝒩(μ, σ²)
然后,我们可以将分布重写如下:
ϵ ∼ 𝒩(0,1)
z = μ + σϵ
这个技巧带来的好处是,我们现在可以一步计算计算任何步骤的图像,这将大大提高训练性能:
x_t = √(1 − β_t) x_{t−1} + √(β_t) ϵ_{t−1}
现在,假设我们定义以下内容:
α_t = 1 − β_t
现在我们有以下内容:
_α_t = ∏(i=1 to t) α_i
这里没有魔法;定义 α_t 和 α_‾_t 只是为了方便,这样我们就可以在 t 步计算一个噪声图像,并使用以下方程从源未噪声图像 x_0 生成 x_t:
x_t = √(1 − β_t) x_{t−1} + √(β_t) ϵ_{t−1}
α_t 和 α_‾_t 看起来是什么样子?这里是一个简化的示例 (图 4.4)。
图 4.4:重参数化实现
在 图 4.4 中,我们所有的 α - 0.1 和 β - 0.9 都是相同的。现在,每当我们需要生成一个噪声图像 x_t 时,我们可以快速从已知数字中计算出 α_‾t;线条显示了用于计算 α‾_t 的数字。
以下代码可以在任何步骤生成一个噪声图像:
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from itertools import accumulate
def get_product_accumulate(numbers):
product_list = list(accumulate(numbers, lambda x, y: x * y))
return product_list
# Load an image
img_path = r"dog.png"
image = plt.imread(img_path)
image = image * 2 - 1 # [0,1] to [-1,1]
# Parameters
num_iterations = 16
beta = 0.05 # noise_variance
betas = [beta]*num_iterations
alpha_list = [1 - beta for beta in betas]
alpha_bar_list = get_product_accumulate(alpha_list)
target_index = 5
x_target = (
np.sqrt(alpha_bar_list[target_index]) * image
+ np.sqrt(1 - alpha_bar_list[target_index]) *
np.random.normal(0,1,image.shape)
)
x_target = (x_target+1)/2
x_target = Image.fromarray((x_target * 255).astype('uint8'), 'RGB')
display(x_target)
这段代码是之前展示的数学公式的实现。我在这里展示代码是为了帮助大家建立数学公式与实际实现之间的关联理解。如果你熟悉 Python,可能会发现这段代码使得底层细节更容易理解。该代码可以生成如图 图 4.5 所示的带噪声的图像。
图 4.5:重参数化实现
现在,让我们思考如何利用神经网络恢复图像。
噪声到图像的训练过程
我们已经有了向图像添加噪声的解决方案,这被称为正向扩散,如图 图 4.6 所示。要从噪声中恢复图像,或进行反向扩散,如图 图 4.6 所示,我们需要找到一种方法来实现反向步骤 pθ(xt−1|xt)。然而,没有额外的帮助,这一步是无法处理的或无法计算的。
考虑到我们已经有最终的高斯噪声数据,以及所有噪声步骤数据在手。如果我们能够训练一个能够逆转过程的神经网络会怎样?我们可以使用神经网络来提供噪声图像的均值和方差,然后从之前图像数据中移除生成的噪声。通过这样做,我们应该能够使用这一步来表示 pθ(xt−1|xt),从而恢复图像。
图 4.6:正向扩散和反向过程
你可能会问我们应该如何计算损失并更新权重。最终图像 (xT) 移除了之前添加的噪声,并将提供真实数据。毕竟,我们可以在正向扩散过程中实时生成噪声数据。接下来,将其与神经网络(通常是 UNet)的输出数据进行比较。我们得到损失数据,可用于计算梯度下降数据并更新神经网络权重。
DDPM 论文 [4] 提供了一种简化的损失计算方法:
Lsimple(θ) := 𝔼t, x0∈[||∈ − ∈θ(√αt xt0 + √1 − αt ϵ, t) ||²]
由于 xt = √αt xt0 + √1 − αt,我们可以进一步简化公式为以下形式:
Lsimple(θ) ≔ 𝔼t,x0,ϵ[||ϵ − ϵθ(xt, t) ||²]
UNet 将以带噪声的图像数据 x_t 和时间步数据 t 作为输入,如图 图 4.7 所示。为什么以 t 作为输入?因为所有去噪过程都共享相同的神经网络权重,输入 t 将帮助训练一个考虑时间步的 UNet。
图 4.7:UNet 训练输入和损失计算
当我们说训练一个神经网络来预测将被从图像中移除的噪声分布,从而得到更清晰的图像时,神经网络预测的是什么?在DDPM论文[4]中,原始的扩散模型使用一个固定的方差θ,并将高斯分布的均值- μ作为唯一需要通过神经网络学习的参数。
在PyTorch实现中,损失数据可以计算如下:
import torch
import torch.nn as nn
# code prepare the model object, image and timestep
# ...
# noise is the Ɛ ~ N(0,1) with the shape of the image x_t.
noise = torch.randn_like(x_t)
# x_t is the noised image at step "t", together with the time_step value
predicted_noise = model(x_t, time_step)
loss = nn.MSELoss(noise, predicted_noise)
# backward weight propagation
# ...
现在,我们应该能够训练一个扩散模型,并且该模型应该能够从随机高斯分布的噪声中恢复图像。接下来,让我们看看推理或采样的工作原理。
噪声到图像的采样过程
这里是从模型中采样图像的步骤,或者换句话说,通过反向扩散过程生成图像:
- 生成一个均值为0,方差为1的完整高斯噪声:
x T ∼ 𝒩(0,1)
我们将使用这个噪声作为起始图像。
- 从t = T循环到t = 1。在每一步中,如果t > 1,则生成另一个高斯噪声图像z:
z ∼ 𝒩(0,1)
如果t = 1,则以下情况发生:
z = 0
然后,从UNet模型生成噪声,并从输入的噪声图像x t中移除生成的噪声:
x t-1 = 1 _ √ _ α t (x t − 1 − α t _ √ _ 1 − _ α t ϵ θ(x t, t)) + √ _ 1 − α t z
如果我们看一下前面的方程,所有的α t和α ‾ t都是来自β t的已知数字。我们唯一需要从UNet得到的是ϵ θ(x t, t),这是UNet产生的噪声,如图4**.8所示。8*。
图4.8:从UNet采样
在这里添加的√ _ 1 − α t z看起来有点神秘。为什么要添加这个过程?原始论文没有解释这个添加的噪声,但研究人员发现,在去噪过程中添加的噪声将显著提高生成的图像质量!
- 循环结束,返回最终生成的图像x 0。
现在,让我们谈谈图像生成引导。
理解分类器引导去噪
到目前为止,我们还没有讨论文本引导。图像生成过程将以随机高斯噪声作为唯一输入,然后根据训练数据集随机生成图像。但我们需要一个引导的图像生成;例如,输入“dog”来要求扩散模型生成包含“dog”的图像。
在2021年,来自OpenAI的Dhariwal和Nichol在他们题为扩散模型在图像 合成 [12]的论文中提出了分类器引导。
根据提出的方法,我们可以在训练阶段提供分类标签来实现分类器引导去噪。除了图像或时间步长嵌入之外,我们还提供了文本描述嵌入,如图4**.9所示。
图4.9:使用条件文本训练扩散模型
在图4**.7中,有两个输入,而在图4**.9中,有一个额外的输入 – 文本嵌入;这是由OpenAI的CLIP模型生成的嵌入数据。我们将在下一章讨论更强大的CLIP模型引导的扩散模型。
摘要
在本章中,我们深入探讨了由约翰逊·霍等最初提出的扩散模型的内部工作原理。[4]。我们了解了扩散模型的基础思想,并学习了正向扩散过程。我们还了解了扩散模型训练和采样的反向扩散过程,并探讨了如何实现文本引导的扩散模型。
通过本章,我们旨在解释扩散模型的核心思想。如果你想自己实现扩散模型,我建议直接阅读原始DDPM论文。
DDPM扩散模型可以生成逼真的图像,但其中一个问题是其性能。不仅训练模型速度慢,图像采样也慢。在下一章中,我们将讨论Stable Diffusion模型,它将以天才的方式提高速度。
参考文献
)
)
-
约翰逊·霍等,去噪扩散概率模型 – https://arxiv.org/abs/2006.11239
-
斯蒂恩斯,扩散模型清晰解释! – https://medium.com/@steinsfu/diffusion-model-clearly-explained-cd331bd41166
-
斯蒂恩斯,Stable Diffusion清晰解释! – https://medium.com/@steinsfu/stable-diffusion-clearly-explained-ed008044e07e
-
DeepFindr,从零开始使用PyTorch构建扩散模型 – https://www.youtube.com/watch?v=a4Yfz2FxXiY&t=5s&ab_channel=DeepFindr
)
- 阿里·塞夫,什么是扩散模型? – https://www.youtube.com/watch?v=fbLgFrlTnGU&ab_channel=AriSeff
)
-
Prafulla Dhariwal, Alex Nichol*,扩散模型在图像合成上击败了GANs – https://arxiv.org/abs/2105.05233
-
Diederik P Kingma, Max Welling,自动编码变分贝叶斯 – https://arxiv.org/abs/1312.6114
-
Lilian Weng,什么是扩散模型? – https://lilianweng.github.io/posts/2021-07-11-diffusion-models/
-
Prafulla Dhariwal, Alex Nichol,扩散模型在图像合成上击败了GANs – https://arxiv.org/abs/2105.05233
第五章:理解Stable Diffusion的工作原理
在第4章中,我们通过一些数学公式深入了解了扩散模型的内部工作原理。如果你不习惯每天阅读公式,可能会感到害怕,但一旦你熟悉了那些符号和希腊字母,完全理解这些公式的益处是巨大的。数学公式和方程不仅以精确和简洁的形式帮助我们理解过程的本质,而且使我们能够阅读更多他人的论文和作品。
虽然原始的扩散模型更像是一个概念证明,但它展示了多步扩散模型与单次传递神经网络相比的巨大潜力。然而,原始扩散模型(去噪扩散概率模型(DDPM)[1])和一些后续的分类器指导去噪存在一些缺点。让我列举两个:
-
要使用分类器指导训练扩散模型,需要训练一个新的分类器,而且我们不能重用预训练的分类器。此外,在扩散模型训练中,训练一个包含1,000个类别的分类器已经很不容易了。
-
在像素空间中使用预训练模型进行推理计算成本很高,更不用说训练模型了。在没有内存优化的情况下,使用预训练模型在具有8 GB VRAM的家用电脑上生成512x512像素的图像是不可能的。
在2022年,研究人员提出了潜在扩散模型,由Robin等人[2]提出。该模型很好地解决了分类问题和性能问题。后来,这种潜在扩散模型被称为Stable Diffusion。
在本章中,我们将探讨Stable Diffusion如何解决前面的问题,并引领图像生成领域的最先进发展。我们将具体涵盖以下主题:
-
潜在空间中的Stable Diffusion
-
使用Diffusers生成潜在向量
-
使用CLIP生成文本嵌入
-
生成时间步嵌入
-
初始化Stable Diffusion UNet
-
实现文本到图像的Stable Diffusion推理管道
-
实现文本引导的图像到图像的Stable Diffusion推理管道
-
将所有代码合并在一起
让我们深入探讨Stable Diffusion的核心。
本章的示例代码使用Diffusers包的0.20.0版本进行测试。为确保代码运行顺畅,请使用Diffusers v0.20.0。你可以使用以下命令进行安装:
pip install diffusers==0.20.0
潜在空间中的Stable Diffusion
与在像素空间中处理扩散不同,Stable Diffusion使用潜在空间来表示图像。什么是潜在空间?简而言之,潜在空间是对象的向量表示。为了打个比方,在你去参加相亲之前,媒人可以以向量的形式提供给你对方的身高、体重、年龄、爱好等信息:
[height, weight, age, hobbies,...]
你可以将这个向量视为你盲约对象的潜在空间。一个真实人的真实属性维度几乎是无限的(你可以为一个人写一篇传记)。潜在空间可以用来表示一个真实的人,只需要有限数量的特征,如身高、体重和年龄。
在Stable Diffusion训练阶段,使用一个训练好的编码器模型,通常表示为ℇ (E),将输入图像编码为潜在向量表示。在反向扩散过程之后,潜在空间由像素空间的解码器解码。解码器通常表示为D (D)。
训练和采样都在潜在空间中进行。训练过程在图5.1中展示:
图5.1:在潜在空间中训练Stable Diffusion模型
图5.1展示了Stable Diffusion模型的训练过程。它展示了模型是如何被训练的概述。
下面是这个过程的一步一步分解:
-
输入: 模型使用图像、标题文本和时间步嵌入(指定去噪发生的步骤)进行训练。
-
图像编码器: 输入图像通过编码器传递。编码器是一个神经网络,它处理输入图像并将其转换为更抽象和压缩的表示。这种表示通常被称为“潜在空间”,因为它捕捉了图像的基本特征,但不是像素级细节。
-
潜在空间: 编码器输出一个向量,代表潜在空间中的输入图像。潜在空间通常比输入空间(图像的像素空间)低维,这使得处理更快,并且更有效地表示输入数据。整个训练过程都在潜在空间中进行。
-
迭代N步: 训练过程涉及在潜在空间中多次迭代(N步)。这个迭代过程是模型学习细化潜在空间表示并做出小调整以匹配期望输出图像的地方。
-
UNet: 在每次迭代后,模型使用UNet根据当前的潜在空间向量生成输出图像。UNet生成预测的噪声,并整合输入文本嵌入、步骤信息以及可能的其他嵌入。
-
损失函数: 模型的训练过程还涉及一个损失函数。这个函数衡量输出图像与期望输出图像之间的差异。随着模型的迭代,损失不断计算,模型通过调整其权重来最小化这个损失。这就是模型如何从错误中学习并随着时间的推移而改进。
请参考第21章获取更多关于模型训练的详细步骤。
从UNet进行推理的过程在图5.2中展示:
图5.2:稳定扩散在潜在空间中的推理
稳定扩散不仅支持文本引导的图像生成;它还支持图像引导的生成。
在图5.2中,从左侧开始,我们可以看到文本和图像都被用来引导图像生成。
当我们提供文本输入时,稳定扩散使用 CLIP [3] 生成嵌入向量,该向量将被输入到 UNet 中,并使用注意力机制。
当我们提供一个图像作为引导信号时,输入图像将被编码到潜在空间,然后与随机生成的高斯噪声进行拼接。
一切都取决于我们提供的指导;我们可以提供文本、图像或两者兼而有之。我们甚至可以在不提供任何图像的情况下生成图像;在这种情况下,“空”的引导案例中,UNet模型将根据随机初始化的噪声决定生成什么。
提供了两个基本输入文本嵌入和初始图像潜在噪声(是否包含初始图像在潜在空间中的编码向量),UNet开始从潜在空间中的初始图像中去除噪声。经过几个去噪步骤后,在解码器的帮助下,稳定扩散可以在像素空间中输出一个生动的图像。
该过程与训练过程类似,但不需要将损失值发送回更新权重。相反,在经过几个去噪步骤(N步骤)后,潜在解码器(变分自动编码器(VAE)[4])将图像从潜在空间转换为可见像素空间。
接下来,让我们看看那些组件(文本编码器、图像编码器、UNet和图像解码器)的样子,然后我们将一步一步地构建我们自己的稳定扩散管道。
使用 diffusers 生成潜在向量
在本节中,我们将使用预训练的稳定扩散模型将图像编码到潜在空间,以便我们有一个关于潜在向量看起来和感觉如何的具体印象。然后,我们将潜在向量解码回图像。此操作还将为构建图像到图像的定制管道奠定基础:
-
load_image
函数从diffusers
中加载图像,从本地存储或URL加载图像。在下面的代码中,我们从当前程序的同一目录中加载了一个名为dog.png
的图像:from diffusers.utils import load_image
image = load_image("dog.png")
display(image)
-
预处理图像:加载的每个像素都由一个介于0到255之间的数字表示。稳定扩散过程中的图像编码器处理介于-1.0到1.0之间的图像数据。因此,我们首先需要进行数据范围转换:
import numpy as np
# convert image object to array and
# convert pixel data from 0 ~ 255 to 0 ~ 1
image_array = np.array(image).astype(np.float32)/255.0
# convert the number from 0 ~ 1 to -1 ~ 1
image_array = image_array * 2.0 - 1.0
现在,如果我们使用 Python 代码
image_array.shape
来检查image_array
数据形状,我们将看到图像数据的形状为 –(512,512,3)
,排列为(width, height, channel)
,而不是常用的(channel, width, height)
。在这里,我们需要使用transpose()
函数将图像数据形状转换为(channel, width, height)
或(3,512,512)
:# transform the image array from width,height,
# channel to channel,width,height
image_array_cwh = image_array.transpose(2,0,1)
2
位于2, 0, 1
的第一个位置,这意味着将原始的第三维度(索引为2
)移动到第一个维度。相同的逻辑适用于0
和1
。原始的0
维度现在转换为第二个位置,原始的1
现在位于第三个维度。通过这个转置操作,NumPy 数组
image_array_cwh
现在具有(``3,512,512)
的形状。稳定扩散图像编码器以批处理方式处理图像数据,在这个例子中是具有四个维度的数据,批处理维度位于第一个位置;我们需要在这里添加批处理维度:
# add batch dimension
image_array_cwh = np.expand_dims(image_array_cwh, axis = 0)
-
torch
并移动到 CUDA:我们将使用 CUDA 将图像数据转换为潜在空间。为了实现这一点,我们需要在将其传递给下一步模型之前将数据加载到 CUDA VRAM 中:# load image with torch
import torch
image_array_cwh = torch.from_numpy(image_array_cwh)
image_array_cwh_cuda = image_array_cwh.to(
"cuda",
dtype=torch.float16
)
-
加载稳定扩散图像编码器 VAE:这个 VAE 模型用于将图像从像素空间转换为潜在空间:
# Initialize VAE model
import torch
from diffusers import AutoencoderKL
vae_model = AutoencoderKL.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder = "vae",
torch_dtype=torch.float16
).to("cuda")
-
将图像编码为潜在向量:现在一切准备就绪,我们可以将任何图像编码为潜在向量,作为 PyTorch 张量:
latents = vae_model.encode(
image_array_cwh_cuda).latent_dist.sample()
检查潜在数据的数据和形状:
print(latents[0])
print(latents[0].shape)
我们可以看到潜在数据具有
(4, 64, 64)
的形状,每个元素的范围在-1.0
到1.0
之间。稳定扩散对所有去噪步骤都在一个 64x64 张量上进行,该张量具有 4 个通道,用于生成 512x512 的图像。数据大小远小于其原始图像大小,512x512 的三个颜色通道。
-
将潜在数据解码为图像(可选):你可能想知道,我能将潜在数据转换回像素图像吗?是的,我们可以通过以下代码行来实现:
import numpy as np
from PIL import Image
def latent_to_img(latents_input, scale_rate = 1):
latents_2 = (1 / scale_rate) * latents_input
# decode image
with torch.no_grad():
decode_image = vae_model.decode(
latents_input,
return_dict = False
)[0][0]
decode_image = (decode_image / 2 + 0.5).clamp(0, 1)
# move latent data from cuda to cpu
decode_image = decode_image.to("cpu")
# convert torch tensor to numpy array
numpy_img = decode_image.detach().numpy()
# covert image array from (width, height, channel)
# to (channel, width, height)
numpy_img_t = numpy_img.transpose(1,2,0)
# map image data to 0, 255, and convert to int number
numpy_img_t_01_255 = \
(numpy_img_t*255).round().astype("uint8")
# shape the pillow image object from the numpy array
return Image.fromarray(numpy_img_t_01_255)
pil_img = latent_to_img(latents_input)
pil_img
diffusers
稳定扩散管道最终将生成一个潜在张量。我们将遵循类似的步骤,在本章的后半部分恢复图像的降噪潜在。
使用 CLIP 生成文本嵌入
要生成文本嵌入(嵌入包含图像特征),我们首先需要标记输入文本或提示,然后将标记 ID 编码为嵌入。以下是实现此目的的步骤:
-
获取提示标记 ID:
input_prompt = "a running dog"
# input tokenizer and clip embedding model
import torch
from transformers import CLIPTokenizer,CLIPTextModel
# initialize tokenizer
clip_tokenizer = CLIPTokenizer.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder = "tokenizer",
dtype = torch.float16
)
input_tokens = clip_tokenizer(
input_prompt,
return_tensors = "pt"
)["input_ids"]
input_tokens
上述代码将
a running dog
文本提示转换为标记 ID 列表,作为一个torch
张量对象 –tensor([[49406, 320, 2761, 1929, 49407]])
。 -
将标记 ID 编码为嵌入:
# initialize CLIP text encoder model
clip_text_encoder = CLIPTextModel.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder="text_encoder",
# dtype=torch.float16
).to("cuda")
# encode token ids to embeddings
prompt_embeds = clip_text_encoder(
input_tokens.to("cuda")
)[0]
-
检查嵌入数据:
print(prompt_embeds)
print(prompt_embeds.shape)
现在,我们可以看到
prompt_embeds
的数据如下:tensor([[[-0.3884,0.0229, -0.0522,..., -0.4899, -0.3066,0.0675],
[ 0.0290, -1.3258, 0.3085,..., -0.5257,0.9768,0.6652],
[ 1.4642,0.2696,0.7703,..., -1.7454, -0.3677,0.5046],
[-1.2369,0.4149,1.6844,..., -2.8617, -1.3217,0.3220],
[-1.0182,0.7156,0.4969,..., -1.4992, -1.1128, -0.2895]]],
device='cuda:0', grad_fn=<NativeLayerNormBackward0>)
其形状为
torch.Size([1, 5, 768])
。每个标记 ID 被编码为一个 768 维向量。 -
prompt
和prompt
/negative
prompt
情况:# prepare neg prompt embeddings
uncond_tokens = "blur"
# get the prompt embedding length
max_length = prompt_embeds.shape[1]
# generate negative prompt tokens with the same length of prompt
uncond_input_tokens = clip_tokenizer(
uncond_tokens,
padding = "max_length",
max_length = max_length,
truncation = True,
return_tensors = "pt"
)["input_ids"]
# generate the negative embeddings
with torch.no_grad():
negative_prompt_embeds = clip_text_encoder(
uncond_input_tokens.to("cuda")
)[0]
-
torch
向量:prompt_embeds = torch.cat([negative_prompt_embeds,
prompt_embeds])
接下来,我们将初始化时间步数据。
初始化时间步嵌入
我们在第3章中介绍了调度器。通过使用调度器,我们可以为图像生成采样关键步骤。与在原始扩散模型(DDPM)中通过去噪1,000步来生成图像相比,使用调度器我们只需20步就能生成图像。
在本节中,我们将使用欧拉调度器生成时间步嵌入,然后我们将看看时间步嵌入看起来是什么样子。不管试图绘制过程的图表有多好,我们只能通过阅读实际数据和代码来理解它的工作原理:
-
从模型调度器配置初始化一个调度器:
from diffusers import EulerDiscreteScheduler as Euler
# initialize scheduler from a pretrained checkpoint
scheduler = Euler.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder = "scheduler"
)
上述代码将从检查点的调度器配置文件初始化一个调度器。请注意,你还可以创建一个调度器,正如我们在第3章中讨论的那样,如下所示:
import torch
from diffusers import StableDiffusionPipeline
from diffusers import EulerDiscreteScheduler as Euler
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
).to("cuda:0")
scheduler = Euler.from_config(text2img_pipe.scheduler.config)
然而,这需要你首先加载一个模型,这不仅速度慢,而且不必要;我们需要的只是模型的调度器。
-
为图像扩散过程采样步骤:
inference_steps = 20
scheduler.set_timesteps(inference_steps, device = "cuda")
timesteps = scheduler.timesteps
for t in timesteps:
print(t)
我们将看到20步的值如下:
...
tensor(999., device='cuda:0', dtype=torch.float64)
tensor(946.4211, device='cuda:0', dtype=torch.float64)
tensor(893.8421, device='cuda:0', dtype=torch.float64)
tensor(841.2632, device='cuda:0', dtype=torch.float64)
tensor(788.6842, device='cuda:0', dtype=torch.float64)
tensor(736.1053, device='cuda:0', dtype=torch.float64)
tensor(683.5263, device='cuda:0', dtype=torch.float64)
tensor(630.9474, device='cuda:0', dtype=torch.float64)
tensor(578.3684, device='cuda:0', dtype=torch.float64)
tensor(525.7895, device='cuda:0', dtype=torch.float64)
tensor(473.2105, device='cuda:0', dtype=torch.float64)
tensor(420.6316, device='cuda:0', dtype=torch.float64)
tensor(368.0526, device='cuda:0', dtype=torch.float64)
tensor(315.4737, device='cuda:0', dtype=torch.float64)
tensor(262.8947, device='cuda:0', dtype=torch.float64)
tensor(210.3158, device='cuda:0', dtype=torch.float64)
tensor(157.7368, device='cuda:0', dtype=torch.float64)
tensor(105.1579, device='cuda:0', dtype=torch.float64)
tensor(52.5789, device='cuda:0', dtype=torch.float64)
tensor(0., device='cuda:0', dtype=torch.float64)
在这里,调度器从1,000步中抽取了20步,这20步可能足以对图像生成中的完整高斯分布进行去噪。这种步骤采样技术也有助于提高Stable Diffusion的性能。
初始化Stable Diffusion UNet
UNet架构[5]由Ronneberger等人为了生物医学图像分割目的而引入。在UNet架构之前,卷积网络通常用于图像分类任务。当使用卷积网络时,输出是一个单一类别的标签。然而,在许多视觉任务中,所需的输出还应包括定位信息,而UNet模型解决了这个问题。
UNet的U形架构使得在不同尺度上高效学习特征成为可能。UNet的跳过连接直接将不同阶段的特征图结合起来,使模型能够有效地在不同尺度间传播信息。这对于去噪至关重要,因为它确保模型在去噪过程中既保留了精细的细节,也保留了全局上下文。这些特性使UNet成为去噪模型的良好候选。
在Diffuser
库中,有一个名为UNet2DconditionalModel
的类;这是一个用于图像生成和相关任务的2D条件UNet模型。它是扩散模型的关键组件,在图像生成过程中发挥着至关重要的作用。我们只需几行代码就可以加载一个UNet模型,如下所示:
import torch
from diffusers import UNet2DConditionModel
unet = UNet2DConditionModel.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder ="unet",
torch_dtype = torch.float16
).to("cuda")
与我们刚刚加载的UNet模型一起,我们拥有了Stable Diffusion所需的所有组件。这并不难,对吧?接下来,我们将使用这些构建块来构建两个Stable Diffusion管道——一个文本到图像和一个图像到图像。
实现文本到图像的Stable Diffusion推理管道
到目前为止,我们已经初始化并加载了所有文本编码器、图像VAE和去噪UNet模型到CUDA VRAM中。以下步骤将它们链接在一起,形成最简单且可工作的Stable Diffusion文本到图像管道:
-
初始化潜在噪声:在*图5**.2中,推理的起点是随机初始化的高斯潜在噪声。我们可以使用以下代码创建一个潜在噪声:
# prepare noise latents
shape = torch.Size([1, 4, 64, 64])
device = "cuda"
noise_tensor = torch.randn(
shape,
generator = None,
dtype = torch.float16
).to("cuda")
在训练阶段,使用初始噪声sigma来帮助防止扩散过程陷入局部最小值。当扩散过程开始时,它很可能处于一个非常接近局部最小值的状态。
init_noise_sigma = 14.6146
用于帮助避免这种情况。因此,在推理过程中,我们也将使用init_noise_sigma
来塑造初始潜在表示。# scale the initial noise by the standard deviation required by
# the scheduler
latents = noise_tensor * scheduler.init_noise_sigma
-
遍历UNet:在准备所有这些组件后,我们最终到达将初始潜在表示输入UNet以生成我们想要的目标潜在表示的阶段:
guidance_scale = 7.5
latents_sd = torch.clone(latents)
for i,t in enumerate(timesteps):
# expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents_sd] * 2)
latent_model_input = scheduler.scale_model_input(
latent_model_input, t)
# predict the noise residual
with torch.no_grad():
noise_pred = unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
return_dict = False,
)[0]
# perform guidance
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale *
(noise_pred_text - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
latents_sd = scheduler.step(noise_pred, t,
latents_sd, return_dict=False)[0]
前面的代码是
diffusers
包中DiffusionPipeline
的简化去噪循环,移除了所有边缘情况,仅保留了推理的核心。该算法通过迭代地向图像的潜在表示中添加噪声来工作。在每次迭代中,噪声由文本提示引导,这有助于模型生成与提示更相似的图像。
前面的代码首先定义了一些变量:
-
guidance_scale
变量控制应用于噪声的引导程度。 -
latents_sd
变量存储生成的图像的潜在表示。时间步变量存储一个列表,其中包含添加噪声的时间步。
代码的主循环遍历时间步。在每次迭代中,代码首先将潜在表示扩展以包含其自身的两份副本。这是因为在Stable Diffusion算法中,使用的是无分类器引导机制,它需要两个潜在表示的副本。
然后代码调用
unet
函数来预测当前时间步的噪声残差。然后代码对噪声残差进行引导。这涉及到将文本条件噪声残差的缩放版本添加到无条件噪声残差中。应用引导的程度由
guidance_scale
变量控制。最后,代码调用
scheduler
函数来更新图像的潜在表示。scheduler
函数是一个函数,它控制在每个时间步添加到潜在表示中的噪声量。如前所述,前面的代码是Stable Diffusion算法的简化版本。在实际应用中,该算法要复杂得多,并融合了多种其他技术来提高生成图像的质量。
-
-
使用
latent_to_img
函数从潜在空间恢复图像:import numpy as np
from PIL import Image
def latent_to_img(latents_input):
# decode image
with torch.no_grad():
decode_image = vae_model.decode(
latents_input,
return_dict = False
)[0][0]
decode_image = (decode_image / 2 + 0.5).clamp(0, 1)
# move latent data from cuda to cpu
decode_image = decode_image.to("cpu")
# convert torch tensor to numpy array
numpy_img = decode_image.detach().numpy()
# covert image array from (channel, width, height)
# to (width, height, channel)
numpy_img_t = numpy_img.transpose(1,2,0)
# map image data to 0, 255, and convert to int number
numpy_img_t_01_255 = \
(numpy_img_t*255).round().astype("uint8")
# shape the pillow image object from the numpy array
return Image.fromarray(numpy_img_t_01_255)
latents_2 = (1 / 0.18215) * latents_sd
pil_img = latent_to_img(latents_2)
latent_to_img
函数按照以下顺序执行操作:-
它调用
vae_model.decode
函数将潜在向量解码为图像。vae_model.decode
函数是在图像数据集上训练的函数。它可以用来生成与数据集中图像相似的新图像。 -
将图像数据归一化到
0
到1
的范围内。这样做是因为Image.fromarray
函数期望图像数据处于这个范围内。 -
将图像数据从GPU移动到CPU。然后,它将图像数据从torch张量转换为NumPy数组。这样做是因为
Image.fromarray
函数只接受NumPy数组作为输入。 -
将图像数组的维度翻转,使其处于(宽度,高度,通道)格式,这是
Image.fromarray
函数期望的格式。 -
将图像数据映射到
0
到255
的范围内,并将其转换为整型。 -
调用
Image.fromarray
函数从图像数据创建Python图像库(PIL)图像对象。
在解码潜在图像时,需要使用
latents_2 = (1 / 0.18215) * latents_sd
这一行代码,因为在训练过程中潜在被0.18215
这个因子缩放。这种缩放是为了确保潜在空间具有单位方差。在解码时,需要将潜在缩放回其原始尺度以重建原始图像。如果一切顺利,你应该能看到类似以下内容:
-
图5.3:一只正在奔跑的狗,由定制的Stable Diffusion管道生成
在下一节中,我们将实现一个图像到图像的Stable Diffusion管道。
实现文本引导的图像到图像Stable Diffusion推理管道
现在我们需要做的只是将起始图像与起始潜在噪声连接起来。latents_input
Torch张量是我们在本章早期从狗图像中编码的潜在:
strength = 0.7
# scale the initial noise by the standard deviation required by the
# scheduler
latents = latents_input*(1-strength) +
noise_tensor*scheduler.init_noise_sigma
这就是所有必要的步骤;使用与文本到图像管道相同的代码,你应该会生成类似图5.4的内容:
图5.4:一只正在奔跑的狗,由定制的图像到图像Stable Diffusion管道生成
注意,前面的代码使用strength = 0.7
;强度表示原始潜在噪声的权重。如果你想得到与初始图像(你提供给图像到图像管道的图像)更相似的图像,请使用较小的强度数字;否则,增加它。
摘要
在本章中,我们从原始扩散模型DDPM过渡到,并解释了什么是Stable Diffusion以及为什么它比DDPM模型更快、更好。
如论文《使用潜在扩散模型进行高分辨率图像合成》[6] 所建议的,稳定扩散与其前辈最大的区别特征是“潜在”。本章解释了潜在空间是什么以及稳定扩散的训练和推理是如何在内部工作的。
为了全面理解,我们使用了诸如将初始图像编码为潜在数据、将输入提示转换为令牌 ID 并使用 CLIP 文本模型将其嵌入到文本嵌入中、使用稳定扩散调度器采样推理的详细步骤、创建初始噪声潜在值、将初始噪声潜在值与初始图像潜在值连接、将所有组件组合起来构建自定义文本到图像稳定扩散管道,以及扩展管道以启用文本引导的图像到图像稳定扩散管道等方法创建了组件。我们逐一创建了这些组件,最终构建了两个稳定扩散管道——一个文本到图像管道和一个扩展的文本引导图像到图像管道。
通过完成本章,你应该不仅对稳定扩散有一个一般性的理解,而且能够自由地构建自己的管道以满足特定需求。
在下一章中,我们将介绍加载稳定扩散模型的方法。
参考文献
-
Jonathan Ho,Ajay Jain,Pieter Abbeel,去噪扩散概率模型:https://arxiv.org/abs/2006.11239
-
Robin 等人,使用潜在扩散模型进行高分辨率图像合成:https://arxiv.org/abs/2112.10752
-
Alec 等人,从自然语言监督中学习可迁移的视觉模型:https://arxiv.org/abs/2103.00020
-
Hugging Face 的 UNet2DConditionModel 文档:https://huggingface.co/docs/diffusers/api/models/unet2d-cond
-
Robin 等人,使用潜在扩散模型进行高分辨率图像合成:https://arxiv.org/abs/2112.10752
阅读材料
Jonathan Ho,Tim Salimans,无分类器扩散引导:https://arxiv.org/abs/2207.12598
使用 Diffusers 的稳定扩散:https://huggingface.co/blog/stable_diffusion
Olaf Ronneberger,Philipp Fischer,Thomas Brox,UNet:用于生物医学图像分割的卷积网络:https://arxiv.org/abs/1505.04597
第六章:使用 Stable Diffusion 模型
当我们开始使用 Stable Diffusion 模型时,我们会立即遇到不同种类的模型文件,并需要知道如何将模型文件转换为所需的格式。
在本章中,我们将更熟悉 Stable Diffusion 模型文件,包括如何使用模型 ID 从 Hugging Face 仓库加载模型。我们还将提供示例代码来加载开源社区共享的 safetensors
和 .ckpt
模型文件。
在本章中,我们将涵盖以下主题:
-
加载 Diffusers 模型
-
从 safetensors 和 ckpt 文件加载模型检查点
-
使用 CKPT 和 safetensors 文件与 Diffusers
-
模型安全检查器
-
将检查点模型文件转换为 Diffusers 格式
-
使用 Stable Diffusion XL
到本章结束时,你将了解 Stable Diffusion 模型文件类型以及如何将模型文件转换为可以由 Diffusers 加载的格式。
技术要求
在开始之前,请确保你已经安装了 safetensors
包:
pip install safetensors
safetensors
Python 包提供了一种简单高效的方式来安全地访问、存储和共享张量。
加载 Diffusers 模型
而不是手动下载模型文件,Hugging Face Diffusers 包提供了一个方便的方法,可以通过字符串类型的模型 ID 访问开源模型文件,如下所示:
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
)
当执行前面的代码时,如果 Diffusers 找不到由模型 ID 指示的模型文件,该包将自动联系 Hugging Face 仓库下载模型文件,并将它们存储在缓存文件夹中以便下次使用。
默认情况下,缓存文件将存储在以下位置:
Windows:
C:\Users\user_name\.cache\huggingface\hub
Linux:
\``home\user_name\.cache\huggingface\hub
使用默认的缓存路径在开始时是可行的,然而,如果你的系统驱动器小于 512 GB,你很快会发现那些模型文件正在消耗存储空间。为了避免存储空间不足,我们可能需要提前规划模型存储。Diffusers 提供了一个参数,允许我们指定存储缓存权重文件的自定义路径。
以下是在前一个示例代码中添加了一个更多参数,cache_dir
:
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16,
cache_dir = r"D:\my_model_folder"
)
通过指定此 cache_dir
参数,所有自动下载的模型和配置文件都将存储在新位置,而不是占用系统磁盘驱动器。
你可能还会注意到示例代码指定了一个 torch_dtytpe
参数,告诉 Diffusers 使用 torch.float16
。默认情况下,PyTorch 使用 torch.float32
进行矩阵乘法。对于模型推理,换句话说,在用 Stable Diffusion 生成图像的阶段,我们可以使用 float16
类型,这不仅可以将速度提高约 100%,还可以几乎不引人注目的方式节省 GPU 内存。
通常,使用 Hugging Face 的模型既容易又安全。Hugging Face 实现了一个安全检查器,以确保上传的模型文件不包含可能损害您的计算机的恶意代码。
尽管如此,我们仍然可以使用手动下载的模型文件与 Diffusers 一起使用。接下来,我们将从本地磁盘加载各种模型文件。
从 safetensors 和 ckpt 文件加载模型检查点
完整的模型文件也称为 检查点 数据。如果您阅读了一篇或一份关于下载检查点的文章或文档,他们谈论的是 Stable Diffusion 模型文件。
存在许多类型的检查点,如 .ckpt
文件、safetensors
文件和 diffusers
文件:
-
.ckpt
是最基础的文件格式,与大多数 Stable Diffusion 模型兼容。然而,它们也是最易受到恶意攻击的。 -
safetensors
是一种较新的文件格式,旨在比.ckpt
文件更安全。与.ckpt
相比,safetensors
格式在安全性、速度和可用性方面表现更佳。Safetensors 有几个特性来防止代码执行:-
限制数据类型:只允许存储特定的数据类型,例如整数和张量。这消除了在保存的数据中包含代码的可能性。
-
哈希:每个数据块都会进行哈希处理,并将哈希值存储在数据旁边。对数据的任何修改都会改变哈希值,使其立即可检测。
-
隔离:数据存储在隔离的环境中,防止与其他程序交互,并保护您的系统免受潜在的漏洞攻击。
-
-
Diffusers 文件是专为与
Diffusers
库无缝集成而特别设计的最新文件格式。该格式具有顶级的加密功能,并确保与所有 Stable Diffusion 模型兼容。与将数据压缩成单个文件的传统方法不同,Diffusers 格式采用文件夹的形式,包含权重和配置文件。此外,这些文件夹中包含的模型文件遵循safetensors
格式。
当我们使用 Diffusers 自动下载功能时,Diffusers 会将文件存储为 Diffusers 格式。
接下来,我们将加载 ckpt
或 safetensors
格式的 Stable Diffusion 模型。
使用 ckpt 和 safetensors 文件与 Diffusers
Diffusers 社区正在积极增强其功能。在撰写本文时,我们可以轻松地使用 Diffusers
包加载 .ckpt
或 safetensors
检查点文件。
以下代码可以用来加载和使用 safetensors
或 .ckpt
检查点文件。
加载 safetensors
模型:
import torch
from diffusers import StableDiffusionPipeline
model_path = r"model/path/path/model_name.safetensors"
pipe = StableDiffusionPipeline.from_single_file(
model_path,
torch_dtype = torch.float16
)
使用以下代码加载 .ckpt
模型:
import torch
from diffusers import StableDiffusionPipeline
model_path = r"model/path/path/model_name.ckpt"
pipe = StableDiffusionPipeline.from_single_file(
model_path,
torch_dtype = torch.float16
)
您没有阅读错误的代码;我们可以使用相同的函数 from_single_file
加载 safetensors
和 .ckpt
模型文件。接下来,让我们看看安全检查器。
关闭模型安全检查器
默认情况下,Diffusers 管道将使用安全检查器模型来检查输出结果,以确保生成的结果不包含任何 NSFW、暴力或不安全的内容。在某些情况下,安全检查器可能会触发误报并生成空图像(完全黑色的图像)。关于安全检查器有几个 GitHub 问题讨论 [11]。在测试阶段,我们可以暂时关闭安全检查器。
当使用模型 ID 加载模型时关闭安全检查器,请运行以下代码:
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16,
safety_checker = None # or load_safety_checker = False
)
注意,当我们从 safetensors
或 .ckpt
文件加载模型时,关闭安全检查器的参数是不同的。我们不应使用 safety_checker
,而应使用如下示例代码中所示的 load_safety_checker
:
import torch
from diffusers import StableDiffusionPipeline
model_path = r"model/path/path/model_name.ckpt"
pipe = StableDiffusionPipeline.from_single_file(
model_path,
torch_dtype = torch.float16,
load_safety_checker = False
)
你应该能够在 from_pretrained
函数中使用 load_safety_checker = False
来禁用安全检查器。
安全检查器是来自 CompVis – 计算机视觉和学习 LMU 慕尼黑(https://github.com/CompVis)的开源机器学习模型,基于 CLIP [9][10] 构建,称为 Stable Diffusion 安全检查器 [3]。
虽然我们可以将模型加载到一个单独的文件中,但在某些情况下,我们需要将 .ckpt
或 safetensors
模型文件转换为 Diffusers 文件夹结构。接下来,让我们看看如何将模型文件转换为 Diffusers 格式。
将检查点模型文件转换为 Diffusers 格式
与 Diffusers 格式相比,从 .ckpt
或 safetensors
文件加载检查点模型数据较慢,因为每次我们加载 .ckpt
或 safetensors
文件时,Diffusers 都会解包并将其转换为 Diffusers 格式。为了每次加载模型文件时都保存转换,我们可能需要考虑将检查点文件转换为 Diffusers 格式。
我们可以使用以下代码将 .ckpt
文件转换为 Diffusers 格式:
ckpt_checkpoint_path = r"D:\temp\anythingV3_fp16.ckpt"
target_part = r"D:\temp\anythingV3_fp16"
pipe = download_from_original_stable_diffusion_ckpt(
ckpt_checkpoint_path,
from_safetensors = False,
device = "cuda:0"
)
pipe.save_pretrained(target_part)
要将 safetensors
文件转换为 Diffusers 格式,只需将 from_safetensors
参数更改为 True
,如下面的示例代码所示:
from diffusers.pipelines.stable_diffusion.convert_from_ckpt import \
download_from_original_stable_diffusion_ckpt
safetensors_checkpoint_path = \
r"D:\temp\deliberate_v2.safetensors"
target_part = r"D:\temp\deliberate_v2"
pipe = download_from_original_stable_diffusion_ckpt(
safetensors_checkpoint_path,
from_safetensors = True,
device = "cuda:0"
)
pipe.save_pretrained(target_part)
如果你尝试通过搜索引擎寻找解决方案来进行转换,你可能会在互联网的某些角落看到使用名为 convert_original_stable_diffusion_to_diffusers.py
的脚本的解决方案。该脚本位于 Diffusers GitHub 仓库中:https://github.com/huggingface/diffusers/tree/main/scripts。该脚本运行良好。如果你查看脚本的代码,该脚本使用了之前展示的相同代码。
要使用转换后的模型文件,这次只需使用 from_pretrained
函数加载 local
文件夹(而不是模型 ID):
# load local diffusers model files using from_pretrained function
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
r"D:\temp\deliberate_v2",
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
image = pipe("a cute puppy").images[0]
image
你应该会看到由前面的代码生成的可爱小狗图像。接下来,让我们加载 Stable Diffusion XL 模型。
使用 Stable Diffusion XL
Stable Diffusion XL (SDXL)是Stability AI的一个模型。与之前的模型略有不同,SDXL被设计为双阶段模型。我们需要基础模型来生成图像,并可以利用第二个细化模型来细化图像,如图图6.1所示。细化模型是可选的:
图6.1:SDXL,双模型管道
图6.1显示,要从SDXL模型生成最佳质量的图像,我们需要使用基础模型生成原始图像,输出为128x128的潜在图像,然后使用细化模型对其进行增强。
在尝试SDXL模型之前,请确保您至少有15 GB的VRAM,否则,您可能会在细化模型输出图像之前看到CUDA out of memory
错误。您还可以使用第5章中的优化方法,构建一个自定义管道,在可能的情况下将模型移出VRAM。
这里是加载SDXL模型的步骤:
-
下载基础模型
safetensors
文件[6]。您不需要从模型仓库下载所有文件。在撰写本文时,检查点的名称是sd_xl_base_1.0.safetensors
。 -
下载细化模型
safetensors
文件[7]。我们还可以通过提供模型ID让Diffusers管道为我们下载safetensors
文件。 -
接下来,我们将从
safetensors
文件初始化基础和细化模型:import torch
from diffusers import (
StableDiffusionXLPipeline, StableDiffusionXLImg2ImgPipeline)
# load base model
base_model_checkpoint_path = \
r"path/to/sd_xl_base_1.0.safetensors"
base_pipe = StableDiffusionXLPipeline.from_single_file(
base_model_checkpoint_path,
torch_dtype = torch.float16,
use_safetensors = True
)
# load refiner model
refiner_model_checkpoint_path = \
r"path/to/sd_xl_refiner_1.0.safetensors"
refiner_pipe = \
StableDiffusionXLImg2ImgPipeline.from_single_file(
refiner_model_checkpoint_path,
torch_dtype = torch.float16,
use_safetensors = True
)
或者,我们可以使用模型ID初始化基础和细化模型:
import torch
from diffusers import (
StableDiffusionXLPipeline,
StableDiffusionXLImg2ImgPipeline
)
# load base model
base_model_id = "stabilityai/stable-diffusion-xl-base-1.0"
base_pipe = StableDiffusionXLPipeline.from_pretrained(
base_model_id,
torch_dtype = torch.float16
)
# load refiner model
refiner_model_id = "stabilityai/stable-diffusion-xl-refiner-1.0"
refiner_pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(
refiner_model_id,
torch_dtype = torch.float16
)
-
让我们生成潜在空间中的基础图像(4x128x128的中间层潜在图像):
# move model to cuda and generate base image latent
from diffusers import EulerDiscreteScheduler
prompt = """
analog photograph of a cat in a spacesuit taken inside the cockpit of a stealth fighter jet,
Fujifilm, Kodak Portra 400, vintage photography
"""
neg_prompt = """
paint, watermark, 3D render, illustration, drawing,worst quality, low quality
"""
base_pipe.to("cuda")
base_pipe.scheduler = EulerDiscreteScheduler.from_config(
base_pipe.scheduler.config)
with torch.no_grad():
base_latents = base_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
output_type = "latent"
).images[0]
base_pipe.to("cpu")
torch.cuda.empty_cache()
注意,在前面代码的末尾,我们通过使用
base_pipe.to("cpu")
和torch.cuda.empty_cache()
将base_pipe
从VRAM中移除。 -
将细化模型加载到VRAM中,并使用潜在空间中的基础图像生成最终图像:
# refine the image
refiner_pipe.to("cuda")
refiner_pipe.scheduler = EulerDiscreteScheduler.from_config(
refiner_pipe.scheduler.config)
with torch.no_grad():
image = refiner_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
image = [base_latents]
).images[0]
refiner_pipe.to("cpu")
torch.cuda.empty_cache()
image
结果将与图6.2中显示的类似:
图6.2:SDXL生成的图像——穿着宇航服的猫
详细信息和质量远远优于Stable Diffusion 1.5渲染的版本。虽然这个模型在撰写本文时相对较新,但在不久的将来,将会有更多混合检查点模型和低秩适配器(LoRAs)可用。
摘要
本章主要关注Stable Diffusion模型的用法。我们可以通过使用其模型ID来利用Hugging Face的模型。此外,广泛分布的开源模型可以在社区网站如CIVITAI [4]上找到,您可以在那里下载大量的模型资源。这些模型文件通常是.ckpt
或safetensors
文件格式。
本章涵盖了这些模型文件的区别以及直接从Diffusers
包中使用检查点模型文件。此外,它还提供了一种解决方案,将独立模型检查点文件转换为Diffusers格式,以便更快地加载模型。
最后,本章还介绍了如何加载和使用 SDXL 的双模型管道。
参考文献
-
Hugging Face Load safetensors:https://huggingface.co/docs/diffusers/using-diffusers/using_safetensors
-
pickle — Python 对象序列化:https://docs.python.org/3/library/pickle.html
-
稳定扩散安全检查器:https://huggingface.co/CompVis/stable-diffusion-safety-checker
)
- civitai:https://www.civitai.com
)
-
stability.ai:https://stability.ai/
-
stable-diffusion-xl-base-1.0:https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0
)
- stable-diffusion-xl-refiner-1.0:https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0
)
-
safetensors GitHub 仓库:https://github.com/huggingface/safetensors
-
Alec Radford 等人,从自然语言监督中学习可迁移的视觉模型:https://arxiv.org/abs/2103.00020
-
OpenAI CLIP GitHub 仓库:https://github.com/openai/CLIP
-
安全检查器问题:https://github.com/huggingface/diffusers/issues/845, https://github.com/huggingface/diffusers/issues/3422
第二部分 – 使用定制功能改进扩散器
在第一部分,我们探讨了扩散器背后的基本概念和技术,为它们在各个领域的应用奠定了基础。现在,是时候通过深入研究可以显著增强这些强大模型功能的高级定制选项,将我们的理解提升到下一个层次。
本节中的章节旨在为你提供优化和扩展扩散器所需的知识和技能,解锁新的创意表达和问题解决的可能性。从优化性能和管理 VRAM 使用到利用社区驱动的资源,以及探索文本反转等创新技术,我们将涵盖一系列有助于你推动扩散器可能性的边界的话题。
在接下来的章节中,你将学习如何克服限制,利用社区的集体智慧,并解锁将提升你使用扩散器工作的新功能。无论你是寻求提高效率、探索新的艺术途径,还是仅仅保持创新的前沿,本书的这一部分所提供的定制功能和技巧将为你提供成功所需的工具和灵感。
本部分包含以下章节:
第七章:优化性能和VRAM使用
在前面的章节中,我们介绍了Stable Diffusion模型背后的理论,介绍了Stable Diffusion模型的数据格式,并讨论了转换和模型加载。尽管Stable Diffusion模型在潜在空间中进行去噪,但默认情况下,模型的数据和执行仍然需要大量资源,并且可能会不时抛出CUDA Out of memory
错误。
为了使用Stable Diffusion快速平滑地生成图像,有一些技术可以优化整个过程,提高推理速度,并减少VRAM使用。在本章中,我们将介绍以下优化解决方案,并讨论这些解决方案在实际应用中的效果:
-
使用float16或bfloat16数据类型
-
启用VAE分块
-
启用Xformers或使用PyTorch 2.0
-
启用顺序CPU卸载
-
启用模型CPU卸载
-
Token 合并 (ToMe)
通过使用这些解决方案中的一些,你可以让你的GPU即使只有4 GB RAM也能顺畅地运行Stable Diffusion模型。请参阅第2章以获取运行Stable Diffusion模型所需的详细软件和硬件要求。
设置基线
在探讨优化解决方案之前,让我们看看默认设置下的速度和VRAM使用情况,这样我们就可以知道在应用优化解决方案后VRAM使用量减少了多少,或者速度提高了多少。
让我们使用一个非精选的数字1
作为生成器的种子,以排除随机生成的种子的影响。测试是在运行Windows 11的RTX 3090(24 GB VRAM)上进行的,还有一个GPU用于渲染所有其他窗口和UI,这样RTX 3090就可以专门用于Stable Diffusion管道:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5"
).to("cuda:0")
# generate an image
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
默认情况下,PyTorch为卷积启用TensorFloat32 (TF32)模式[4],为矩阵乘法启用float32 (FP32)模式。前面的代码使用8.4 GB VRAM生成一个512x512的图像,生成速度为7.51次/秒。在接下来的章节中,我们将测量采用优化解决方案后的VRAM使用量和生成速度的提升。
优化方案1 – 使用float16或bfloat16数据类型
在PyTorch中,默认以FP32精度创建浮点张量。TF32数据格式是为Nvidia Ampere和后续CUDA设备开发的。TF32可以通过略微降低计算精度来实现更快的矩阵乘法和卷积[5]。FP32和TF32都是历史遗留设置,对于训练是必需的,但网络很少需要如此高的数值精度来进行推理。
我们可以不使用TF32和FP32数据类型,而是以float16或bfloat16精度加载和运行Stable Diffusion模型的权重,以节省VRAM使用并提高速度。但float16和bfloat16之间有什么区别,我们应该使用哪一个?
bfloat16和float16都是半精度浮点数据格式,但它们有一些区别:
-
值范围:bfloat16的正值范围比float16大。bfloat16的最大正值约为3.39e38,而float16约为6.55e4。这使得bfloat16更适合需要大动态范围模型的场景。
-
精度:bfloat16和float16都具有3位指数和10位尾数(分数)。然而,bfloat16使用最高位作为符号位,而float16将其用作尾数的一部分。这意味着bfloat16的相对精度比float16小,特别是对于小数。
bfloat16通常对深度神经网络很有用。它在范围、精度和内存使用之间提供了良好的平衡。它被许多现代GPU支持,与使用单精度(FP32)相比,可以显著减少内存使用并提高训练速度。
在Stable Diffusion中,我们可以使用bfloat16或float16来提高推理速度并同时减少VRAM使用。以下是一些使用bfloat16加载Stable Diffusion模型的代码:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.bfloat16 # <- load float16 version weight
).to("cuda:0")
我们使用text2img_pipe
管道对象生成一个仅使用4.7 GB VRAM的图像,每秒进行19.1次去噪迭代。
注意,如果您使用的是CPU,则不应使用torch.float16
,因为CPU没有对float16的硬件支持。
优化方案2 – 启用VAE分块
Stable Diffusion VAE分块是一种可以用来生成大图像的技术。它通过将图像分割成小块,然后分别生成每个块来实现。这项技术允许在不使用太多VRAM的情况下生成大图像。
注意,分块编码和解码的结果与非分块版本几乎无差别。Diffusers对VAE分块的实现使用重叠的块来混合边缘,从而形成更平滑的输出。
您可以在推理之前添加一行代码text2img_pipe.enable_vae_tiling()
来启用VAE分块:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16 # <- load float16 version weight
).to("cuda:0")
text2img_pipe.enable_vae_tiling() # < Enable VAE Tiling
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1),
width = 1024,
height= 1024
).images[0]
image
打开或关闭VAE分块似乎对生成的图像影响不大。唯一的区别是,没有VAE分块时,VRAM使用量生成一个1024x1024的图像需要7.6 GB VRAM。另一方面,打开VAE分块将VRAM使用量降低到5.1 GB。
VAE分块发生在图像像素空间和潜在空间之间,整个过程对去噪循环的影响最小。测试表明,在生成少于4张图像的情况下,对性能的影响不明显,可以减少20%到30%的VRAM使用量。始终开启它是个好主意。
优化方案3 – 启用Xformers或使用PyTorch 2.0
当我们提供文本或提示来生成图像时,编码的文本嵌入将被馈送到扩散UNet的Transformer多头注意力组件。
在Transformer块内部,自注意力和交叉注意力头将尝试计算注意力分数(通过QKV
操作)。这是计算密集型的,并且也会使用大量的内存。
Meta Research的开源Xformers
[2]软件包旨在优化此过程。简而言之,Xformers与标准Transformers之间的主要区别如下:
-
分层注意力机制:Xformers使用分层注意力机制,它由两层注意力组成:粗层和细层。粗层在高层次上关注输入序列,而细层在低层次上关注输入序列。这使得Xformers能够在学习输入序列中的长距离依赖关系的同时,也能关注局部细节。
-
减少头数:Xformers使用的头数比标准Transformers少。头是注意力机制中的计算单元。Xformers使用4个头,而标准Transformers使用12个头。这种头数的减少使得Xformers能够在保持性能的同时减少内存需求。
使用Diffusers
软件包为Stable Diffusion启用Xformers非常简单。只需添加一行代码,如下面的代码片段所示:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16 # <- load float16 version weight
).to("cuda:0")
text2img_pipe.enable_xformers_memory_efficient_attention() # < Enable
# xformers
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
如果你使用的是PyTorch 2.0+,你可能不会注意到性能提升或VRAM使用量下降。这是因为PyTorch 2.0包含一个类似于Xformers实现的本地构建的注意力优化功能。如果正在使用版本2.0之前的PyTorch历史版本,启用Xformers将明显提高推理速度并减少VRAM使用。
优化方案4 – 启用顺序CPU卸载
正如我们在第五章中讨论的那样,一个管道包括多个子模型:
-
用于将文本编码为嵌入的文本嵌入模型
-
用于编码输入引导图像和解码潜在空间到像素图像的图像潜在编码器/解码器
-
UNet 将循环推理去噪步骤
-
安全检查模型检查生成内容的安全性
顺序CPU卸载的想法是在完成其任务并空闲时将空闲子模型卸载到CPU RAM。
这里是一个逐步工作的示例:
-
将CLIP文本模型加载到GPU VRAM,并将输入提示编码为嵌入。
-
将CLIP文本模型卸载到CPU RAM。
-
将VAE模型(图像到潜在空间的编码器和解码器)加载到GPU VRAM,并在当前任务是图像到图像管道时编码起始图像。
-
将VAE卸载到CPU RAM。
-
将UNet加载到循环遍历去噪步骤(同时加载和卸载未使用的子模块权重数据)。
-
将UNet卸载到CPU RAM。
-
将VAE模型从CPU RAM加载到GPU VRAM以执行潜在空间到图像的解码。
在前面的步骤中,我们可以看到在整个过程中,只有一个子模型会留在 VRAM 中,这可以有效地减少 VRAM 的使用。然而,加载和卸载会显著降低推理速度。
启用顺序 CPU 卸载就像以下代码片段中的一行一样简单:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
).to("cuda:0")
# generate an image
text2img_pipe.enable_sequential_cpu_offload() # <- Enable sequential
# CPU offload
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
想象一下创建一个定制管道,该管道能够有效地利用 VRAM 来进行 UNet 的去噪。通过在空闲期间将文本编码器/解码器、VAE 模型和安全检查器模型策略性地转移到 CPU 上,同时保持 UNet 模型在 VRAM 中,可以实现显著的速度提升。这种方法在书中提供的自定义实现中得到了证实,它将 VRAM 使用量显著降低到低至 3.2 GB(即使是生成 512x512 的图像),同时保持可比的处理速度,性能没有明显下降!
本章提供的自定义管道代码几乎与 enable_sequential_cpu_offload()
做的是同一件事。唯一的区别是保持 UNet 在 VRAM 中直到去噪结束。这就是为什么推理速度保持快速的原因。
通过适当的模型加载和卸载管理,我们可以将 VRAM 使用量从 4.7 GB 降低到 3.2 GB,同时保持与未进行模型卸载时相同的推理速度。
优化方案 5 – 启用模型 CPU 卸载
完整模型卸载将整个模型数据移动到和从 GPU 上,而不是只移动权重。如果不启用此功能,所有模型数据在正向推理前后都将留在 GPU 上;清除 CUDA 缓存也不会释放 VRAM。如果你正在加载其他模型,例如,一个上采样模型以进一步处理图像,这可能会导致 CUDA Out of memory
错误。模型到 CPU 卸载方法可以缓解 CUDA Out of
memory
问题。
根据这种方法背后的理念,在 CPU RAM 和 GPU VRAM 之间移动模型时,将额外花费一到两秒钟。
要启用此方法,请删除 pipe.to("cuda")
并添加 pipe.enable_model_cpu_offload()
:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
) # .to("cuda") is removed here
# generate an image
text2img_pipe.enable_model_cpu_offload() # <- enable model offload
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
在卸载模型时,GPU 承载单个主要管道组件,通常是文本编码器、UNet 或 VAE,而其余组件在 CPU 内存中处于空闲状态。像 UNet 这样的组件,在经过多次迭代后,会留在 GPU 上,直到它们的利用率不再需要。
模型 CPU 卸载方法可以将 VRAM 使用量降低到 3.6 GB,并保持相对较好的推理速度。如果你对前面的代码进行测试运行,你会发现推理速度最初相对较慢,然后逐渐加快到其正常迭代速度。
在图像生成结束时,我们可以使用以下代码手动将模型权重数据从 VRAM 移动到 CPU RAM:
pipe.to("cpu")
torch.cuda.empty_cache()
执行前面的代码后,你会发现你的 GPU VRAM 使用量水平显著降低。
接下来,让我们来看看标记合并。
优化方案6 – 标记合并(ToMe)
标记合并(ToMe)最初由Daniel等人提出[3]。这是一种可以用来加快稳定扩散模型推理时间的技术。ToMe通过合并模型中的冗余标记来工作,这意味着与未合并的模型相比,模型需要做的工作更少。这可以在不牺牲图像质量的情况下带来明显的速度提升。
ToMe通过首先识别模型中的冗余标记来工作。这是通过查看标记之间的相似性来完成的。如果两个标记非常相似,那么它们可能是冗余的。一旦识别出冗余标记,它们就会被合并。这是通过平均两个标记的值来完成的。
例如,如果一个模型有100个标记,其中50个标记是冗余的,那么合并冗余标记可以将模型需要处理的标记数量减少50%。
ToMe可以与任何稳定扩散模型一起使用。它不需要任何额外的训练。要使用ToMe,我们首先需要从其原始发明者那里安装以下包:
pip install tomesd
然后,导入ToMe
包以启用它:
import torch
from diffusers import StableDiffusionPipeline
import tomesd
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
).to("cuda:0")
tomesd.apply_patch(text2img_pipe, ratio=0.5)
# generate an image
prompt ="high resolution, a photograph of an astronaut riding a horse"
image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
性能提升取决于找到多少冗余标记。在前面的代码中,ToMe
包将迭代速度从大约每秒19次提高到20次。
值得注意的是,ToMe
包可能会产生略微改变后的图像输出,尽管这种差异对图像质量没有可察觉的影响。这是因为ToMe合并了标记,这可能会影响条件嵌入。
摘要
在本章中,我们介绍了六种技术来增强稳定扩散的性能并最小化VRAM的使用。VRAM的量往往是运行稳定扩散模型时最显著的障碍,其中“CUDA内存不足”是一个常见问题。我们讨论的技术可以大幅减少VRAM的使用,同时保持相同的推理速度。
启用float16数据类型可以将VRAM使用量减半,并将推理速度提高近一倍。VAE分块允许在不使用过多VRAM的情况下生成大图像。Xformers通过实现智能的两层注意力机制,可以进一步减少VRAM使用量并提高推理速度。PyTorch 2.0提供了原生功能,如Xformers,并自动启用它们。
通过将子模型及其子模块卸载到CPU RAM,顺序CPU卸载可以显著减少VRAM的使用,尽管这会以较慢的推理速度为代价。然而,我们可以使用相同的概念来实现我们的顺序卸载机制,以节省VRAM使用量,同时保持推理速度几乎不变。模型CPU卸载可以将整个模型卸载到CPU,为其他任务释放VRAM,并在必要时才将模型重新加载回VRAM。标记合并(或ToMe)减少了冗余标记并提高了推理速度。
通过应用这些解决方案,你可能会运行一个性能优于世界上任何其他模型的流水线。人工智能领域正在不断演变,在你阅读这段文字的时候,可能会有新的解决方案出现。然而,理解其内部工作原理使我们能够根据你的需求调整和优化图像生成过程。
在下一章中,我们将探讨最激动人心的主题之一,即社区共享的LoRAs。
参考文献
-
Hugging Face、内存和速度:https://huggingface.co/docs/diffusers/optimization/fp16
-
facebookresearch, xformers:https://github.com/facebookresearch/xformers
-
Daniel Bolya, Judy Hoffman;快速稳定扩散的Token合并:https://arxiv.org/abs/2303.17604
-
每个用户都应该了解的PyTorch混合精度训练:https://pytorch.org/blog/what-every-user-should-know-about-mixed-precision-training-in-pytorch/#picking-the-right-approach
)
- 使用NVIDIA TF32 Tensor Cores加速AI训练:https://developer.nvidia.com/blog/accelerating-ai-training-with-tf32-tensor-cores/
第八章:使用社区共享的LoRA
为了满足特定需求并生成更高保真的图像,我们可能需要微调预训练的Stable Diffusion模型,但没有强大的GPU,微调过程会非常缓慢。即使你有所有硬件或资源,微调后的模型仍然很大,通常与原始模型文件大小相同。
幸运的是,来自大型语言模型(LLM)邻域社区的研究人员开发了一种高效的微调方法,低秩适配(LoRA —— “低”是为什么“o”是小写的原因)[1]。使用LoRA,原始检查点保持冻结状态,没有任何修改,而微调权重更改存储在一个独立的文件中,我们通常称之为LoRA文件。此外,在CIVITAI [4]和HuggingFace等网站上还有无数社区共享的LoRA。
在本章中,我们将深入探讨LoRA的理论,然后介绍将LoRA加载到Stable Diffusion模型中的Python方法。我们还将剖析LoRA模型,以了解LoRA模型的结构,并创建一个自定义函数来加载Stable Diffusion V1.5 LoRA。
本章将涵盖以下主题:
-
LoRA是如何工作的?
-
使用Diffusers与LoRA
-
在加载过程中应用LoRA权重
-
深入了解LoRA
-
创建一个用于加载LoRA的函数
-
为什么LoRA有效
到本章结束时,我们将能够程序化地使用任何社区LoRA,并了解LoRA在Stable Diffusion中是如何以及为什么工作的。
技术要求
如果你已经在你的计算机上运行了Diffusers
包,你应该能够执行本章中的所有代码,以及用于使用Diffusers加载LoRA的代码。
Diffusers使用PEFT(参数高效微调)[10]来管理LoRA的加载和卸载。PEFT是由Hugging Face开发的库,它提供了参数高效的适应大型预训练模型的方法,以适应特定的下游应用。PEFT背后的关键思想是仅微调模型参数的一小部分,而不是全部微调,从而在计算和内存使用方面节省了大量资源。这使得即使在资源有限的消费级硬件上也能微调非常大的模型。有关LoRA的更多信息,请参阅第21章。
我们需要安装PEFT包来启用Diffusers的PEFT LoRA加载:
pip install PEFT
如果你在代码中遇到其他执行错误,也可以参考第2章。
LoRA是如何工作的?
LoRA是一种快速微调扩散模型的技术,最初由微软研究人员在Edward J. Hu等人的论文中提出[1]。它通过创建一个针对特定概念进行适配的小型、低秩模型来实现。这个小模型可以与主检查点模型合并,以生成与用于训练LoRA的图像相似的图像。
让我们用W表示原始UNet注意力权重(Q,K,V),用ΔW表示LoRA的微调权重,用W′表示合并后的权重。将LoRA添加到模型的过程可以表示如下:
W′= W + ΔW
如果我们想控制LoRA权重的比例,我们用α表示这个比例。现在,将LoRA添加到模型可以表示如下:
W′= W + αΔW
α的范围可以从0
到1.0
[2]。如果我们把α设置得略大于1.0
,应该没问题。LoRA之所以如此小,是因为ΔW可以用两个小的矩阵A和B来表示,使得:
ΔW = A B T
其中A ∈ ℝ n×d是一个n × d的矩阵,B ∈ ℝ m×d是一个m × d的矩阵。B的转置,记作B T,是一个d × m的矩阵。
例如,如果ΔW是一个6 × 8的矩阵,总共有48
个权重数。现在,在LoRA文件中,6 × 8的矩阵可以表示为两个矩阵——一个6 × 2的矩阵,总共12
个数,另一个2 × 8的矩阵,总共16
个数。
权重的总数从48
减少到28
。这就是为什么与检查点模型相比,LoRA文件可以如此小。
使用Diffusers的LoRA
由于开源社区的贡献,使用Python加载LoRA从未如此简单。在本节中,我们将介绍如何使用Diffusers加载LoRA模型。
在以下步骤中,我们将首先加载基础Stable Diffusion V1.5,生成不带LoRA的图像,然后加载一个名为MoXinV1
的LoRA模型到基础模型中。我们将清楚地看到带和不带LoRA模型之间的差异:
-
准备Stable Diffusion管道:以下代码将加载Stable Diffusion管道并将管道实例移动到VRAM:
import torch
from diffusers import StableDiffusionPipeline
pipeline = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.float16
).to("cuda:0")
-
生成不带LoRA的图像:现在,让我们生成一个不带LoRA加载的图像。这里,我将使用Stable Diffusion默认v1.5模型以“传统中国水墨画”风格生成“一枝花”:
prompt = """
shukezouma, shuimobysim, a branch of flower, traditional chinese ink painting
"""
image = pipeline(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
display(image)
前面的代码使用了一个非精选的生成器,
默认种子:1
。结果如图图8**.1所示:
图8.1:未使用LoRA的花枝
坦白说,前面的图像并不那么好,而且“花”更像是一团黑墨点。
-
使用默认设置生成带LoRA的图像:接下来,让我们将LoRA模型加载到管道中,看看MoXin LoRA能对图像生成带来什么帮助。使用默认设置加载LoRA只需一行代码:
# load LoRA to the pipeline
pipeline.load_lora_weights(
"andrewzhu/MoXinV1",
weight_name = "MoXinV1.safetensors",
adapter_name = "MoXinV1"
)
如果模型不存在于你的模型缓存中,Diffusers会自动下载LoRA模型文件。
现在,再次运行以下代码进行推理(与步骤2中使用的相同代码):
image = pipeline(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
display(image)
我们将得到一个包含更好“花”的水墨画风格的新图像,如图图8**.2所示:
图8.2:使用默认设置下的LoRA的花枝
这次,“花朵”更像是一朵花,总体上比没有应用 LoRA 的那朵花要好。然而,本节中的代码在加载 LoRA 时没有应用“权重”。在下一节中,我们将加载一个具有任意权重(或 α)的 LoRA 模型。
在加载过程中应用 LoRA 权重
在 LoRA 如何工作? 部分中,我们提到了用于定义添加到主模型中 LoRA 权重部分的 α 值。我们可以使用带有 PEFT [10] 的 Diffusers 容易地实现这一点。
什么是 PEFT?PEFT 是 Hugging Face 开发的一个库,用于高效地适应预训练模型,例如 大型语言模型(LLMs)和 Stable Diffusion 模型,而无需对整个模型进行微调。PEFT 是一个更广泛的概念,代表了一组旨在高效微调 LLMs 的方法。LoRA,相反,是 PEFT 范畴下的一种特定技术。
在集成 PEFT 之前,Diffusers 中加载和管理 LoRAs 需要大量的自定义代码和破解。为了更轻松地管理具有加载和卸载权重的多个 LoRAs,Diffusers 使用 PEFT 库来帮助管理推理的不同适配器。在 PEFT 中,微调的参数被称为适配器,这就是为什么你会看到一些参数被命名为 adapters
。LoRA 是主要的适配器技术之一;在本章中,你可以将 LoRA 和适配器视为同一事物。
加载具有权重的 LoRA 模型很简单,如下面的代码所示:
pipeline.set_adapters(
["MoXinV1"],
adapter_weights=[0.5]
)
image = pipeline(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
display(image)
在前面的代码中,我们将 LoRA 权重设置为 0.5
以替换默认的 1.0
。现在,你将看到如图 图 8.3 所示生成的图像。
图 8.3:通过应用 0.5 LoRA 权重添加的 LoRA 花枝
从 图 8.3 中,我们可以观察到应用 0.5
权重到 LoRA 模型后的差异。
集成 PEFT 的 Diffusers 也可以通过重用我们用于加载第一个 LoRA 模型的相同代码来加载另一个 LoRA:
# load another LoRA to the pipeline
pipeline.load_lora_weights(
"andrewzhu/civitai-light-shadow-lora",
weight_name = "light_and_shadow.safetensors",
adapter_name = "light_and_shadow"
)
然后,通过调用 set_adapters
函数添加第二个 LoRA 模型的权重:
pipeline.set_adapters(
["MoXinV1", "light_and_shadow"],
adapter_weights=[0.5,1.0]
)
prompt = """
shukezouma, shuimobysim ,a branch of flower, traditional chinese ink painting,STRRY LIGHT,COLORFUL
"""
image = pipeline(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
display(image)
我们将得到一个新的图像,其中添加了来自第二个 LoRA 的样式,如图 图 8.4 所示:
图 8.4:具有两个 LoRA 模型的花枝
我们也可以使用相同的代码为 Stable Diffusion XL 管道加载 LoRA。
使用 PEFT,我们不需要重新启动管道来禁用 LoRA;我们可以通过一行代码简单地禁用所有 LoRAs:
pipeline.disable_lora()
注意,LoRA 加载的实现与其他工具(如 A1111 Stable Diffusion WebUI)略有不同。使用相同的提示、相同的设置和相同的 LoRA 权重,你可能会得到不同的结果。
别担心——在下一节中,我们将深入探讨 LoRA 模型的内部结构,并使用 A1111 Stable Diffusion WebUI 等工具实现一个使用 LoRA 输出相同结果的解决方案。
深入探讨LoRA的内部结构
理解LoRA内部工作原理将帮助我们根据具体需求实现自己的LoRA相关功能。在本节中,我们将深入探讨LoRA的结构和权重模式,然后逐步手动将LoRA模型加载到Stable Diffusion模型中。
如我们在本章开头所讨论的,应用LoRA就像以下这样简单:
W′= W + αΔW
ΔW可以分解为A和B:
ΔW = A B T
因此,将LoRA权重合并到检查点模型的整体思路是这样的:
-
从LoRA文件中找到A和B权重矩阵。
-
将LoRA模块层名与检查点模块层名匹配,以便我们知道要合并哪个矩阵。
-
生成ΔW = A B T。
-
更新检查点模型的权重。
如果你之前有训练LoRA模型的经验,你可能知道可以设置一个超参数alpha
,其值大于1
,例如4
。这通常与将另一个参数rank
也设置为4
一起进行。然而,在此上下文中使用的α通常小于1。α的实际值通常使用以下公式计算:
α = alpha _ rank
在训练阶段,将alpha
和rank
都设置为4
将产生α值为1
。如果不正确理解,这个概念可能会让人感到困惑。
接下来,让我们一步一步地探索LoRA模型的内部结构。
从LoRA文件中找到A和B权重矩阵
在开始探索LoRA结构的内部结构之前,你需要下载一个LoRA文件。你可以从以下URL下载MoXinV1.safetensors
:https://huggingface.co/andrewzhu/MoXinV1/resolve/main/MoXinV1.safetensors。
在.safetensors
格式中设置好LoRA文件后,使用以下代码加载它:
# load lora file
from safetensors.torch import load_file
lora_path = "MoXinV1.safetensors"
state_dict = load_file(lora_path)
for key in state_dict:
print(key)
当LoRA权重应用于文本编码器时,键名以lora_te_
开头:
...
lora_te_text_model_encoder_layers_7_mlp_fc1.alpha
lora_te_text_model_encoder_layers_7_mlp_fc1.lora_down.weight
lora_te_text_model_encoder_layers_7_mlp_fc1.lora_up.weight
...
当LoRA权重应用于UNet时,键名以lora_unet_
开头:
...
lora_unet_down_blocks_0_attentions_1_proj_in.alpha
lora_unet_down_blocks_0_attentions_1_proj_in.lora_down.weight
lora_unet_down_blocks_0_attentions_1_proj_in.lora_up.weight
...
输出是string
类型。以下是输出LoRA权重键中出现过的术语的含义:
-
lora_te_
前缀表示权重应用于文本编码器;lora_unet_
表示权重旨在更新Stable Diffusionunet
模块的层。 -
down_blocks_0_attentions_1_proj_in
是层名,这个层名应该存在于检查点模型的unet
模块中。 -
.alpha
是训练好的权重,用来表示将有多少LoRA权重应用到主检查点模型中。它持有表示α的浮点值,在W′= W + αΔW中。由于这个值将由用户输入替换,我们可以跳过这个值。 -
lora_down.weight
表示代表A的这个层的值。 -
lora_up.weight
表示代表B的这个层的值。 -
注意,
down
在down_blocks
中表示unet
模型的下方(UNet的左侧)。
以下Python代码将获取LoRA层信息,并具有模型对象处理器:
# find the layer name
LORA_PREFIX_UNET = 'lora_unet'
LORA_PREFIX_TEXT_ENCODER = 'lora_te'
for key in state_dict:
if 'text' in key:
layer_infos = key.split('.')[0].split(
LORA_PREFIX_TEXT_ENCODER+'_')[-1].split('_')
curr_layer = pipeline.text_encoder
else:
layer_infos = key.split('.')[0].split(
LORA_PREFIX_UNET+'_')[-1].split('_')
curr_layer = pipeline.unet
key
持有LoRA模块层名称,而layer_infos
持有从LoRA层中提取的检查点模型层名称。我们这样做的原因是检查点模型中并非所有层都有LoRA权重进行调整,因此我们需要获取将要更新的层的列表。
找到相应的检查点模型层名称
打印出检查点模型unet
结构:
unet = pipeline.unet
modules = unet.named_modules()
for child_name, child_module in modules:
print("child_module:",child_module)
我们可以看到模块是以这样的树状结构存储的:
...
(down_blocks): ModuleList(
(0): CrossAttnDownBlock2D(
(attentions): ModuleList(
(0-1): 2 x Transformer2DModel(
(norm): GroupNorm(32, 320, eps=1e-06, affine=True)
(proj_in): Conv2d(320, 320, kernel_size=(1, 1), stride=(1, 1))
(transformer_blocks): ModuleList(
(0): BasicTransformerBlock(
(norm1): LayerNorm((320,), eps=1e-05, elementwise_affine=True)
(attn1): Attention(
(to_q): Linear(in_features=320, out_features=320, bias=False)
(to_k): Linear(in_features=320, out_features=320, bias=False)
(to_v): Linear(in_features=320, out_features=320, bias=False)
(to_out): ModuleList(
(0): Linear(in_features=320, out_features=320, bias=True)
(1): Dropout(p=0.0, inplace=False)
)
...
每行由一个模块名称(down_blocks
)组成,模块内容可以是ModuleList
或特定的神经网络层,Conv2d
。这些都是UNet的组成部分。目前,将LoRA应用于特定的UNet模块不是必需的。然而,了解UNet的内部结构很重要:
# find the layer name
for key in state_dict:
# find the LoRA layer name (the same code shown above)
for key in state_dict:
if 'text' in key:
layer_infos = key.split('.')[0].split(
"lora_unet_")[-1].split('_')
curr_layer = pipeline.text_encoder
else:
layer_infos = key.split('.')[0].split(
"lora_te_")[-1].split('_')
curr_layer = pipeline.unet
# loop through the layers to find the target layer
temp_name = layer_infos.pop(0)
while len(layer_infos) > -1:
try:
curr_layer = curr_layer.__getattr__(temp_name)
# no exception means the layer is found
if len(layer_infos) > 0:
temp_name = layer_infos.pop(0)
# all names are pop out, break out from the loop
elif len(layer_infos) == 0:
break
except Exception:
# no such layer exist, pop next name and try again
if len(temp_name) > 0:
temp_name += '_'+layer_infos.pop(0)
else:
# temp_name is empty
temp_name = layer_infos.pop(0)
循环部分有点棘手。当回顾检查点模型结构,它以分层的形式作为树时,我们不能简单地使用for
循环来遍历列表。相反,我们需要使用while
循环来导航树的每个叶子。整个过程如下:
-
layer_infos.pop(0)
将返回列表中的第一个名称,并将其从列表中移除,例如从layer_infos
列表中移除up
–['up', 'blocks', '3', 'attentions', '2', 'transformer', 'blocks', '0', 'ff', '``net', '2']
-
使用
curr_layer.__getattr__(temp_name)
来检查层是否存在。如果不存在,将抛出异常,程序将移动到exception
部分继续输出layer_infos
列表中的名称,并再次检查。 -
如果找到了层,但
layer_infos
列表中仍有剩余的名称,它们将继续弹出。 -
名称将继续出现,直到没有抛出异常,并且我们遇到
len(layer_infos) == 0
条件,这意味着层已完全匹配。
在这一点上,curr_layer
对象指向检查点模型权重数据,可以在下一步中进行引用。
更新检查点模型权重
为了便于键值引用,让我们创建一个pair_keys = []
列表,其中pair_keys[0]
返回A矩阵,pair_keys[1]
返回B矩阵:
# ensure the sequence of lora_up(A) then lora_down(B)
pair_keys = []
if 'lora_down' in key:
pair_keys.append(key.replace('lora_down', 'lora_up'))
pair_keys.append(key)
else:
pair_keys.append(key)
pair_keys.append(key.replace('lora_up', 'lora_down'))
然后,我们更新权重:
alpha = 0.5
# update weight
if len(state_dict[pair_keys[0]].shape) == 4:
# squeeze(3) and squeeze(2) remove dimensions of size 1
#from the tensor to make the tensor more compact
weight_up = state_dict[pair_keys[0]].squeeze(3).squeeze(2).\
to(torch.float32)
weight_down = state_dict[pair_keys[1]].squeeze(3).squeeze(2).\
to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up,
weight_down).unsqueeze(2).unsqueeze(3)
else:
weight_up = state_dict[pair_keys[0]].to(torch.float32)
weight_down = state_dict[pair_keys[1]].to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down)
alpha * torch.mm(weight_up, weight_down)
代码是用于实现αA B T的核心代码。
就这样!现在,管道的文本编码器和unet
模型权重已经通过LoRA更新。接下来,让我们将所有部分组合起来,创建一个功能齐全的函数,可以将LoRA模型加载到Stable Diffusion管道中。
编写一个加载LoRA的函数
让我们再添加一个列表来存储已访问的键,并将所有前面的代码组合到一个名为load_lora
的函数中:
def load_lora(
pipeline,
lora_path,
lora_weight = 0.5,
device = 'cpu'
):
state_dict = load_file(lora_path, device=device)
LORA_PREFIX_UNET = 'lora_unet'
LORA_PREFIX_TEXT_ENCODER = 'lora_te'
alpha = lora_weight
visited = []
# directly update weight in diffusers model
for key in state_dict:
# as we have set the alpha beforehand, so just skip
if '.alpha' in key or key in visited:
continue
if 'text' in key:
layer_infos = key.split('.')[0].split(
LORA_PREFIX_TEXT_ENCODER+'_')[-1].split('_')
curr_layer = pipeline.text_encoder
else:
layer_infos = key.split('.')[0].split(
LORA_PREFIX_UNET+'_')[-1].split('_')
curr_layer = pipeline.unet
# find the target layer
# loop through the layers to find the target layer
temp_name = layer_infos.pop(0)
while len(layer_infos) > -1:
try:
curr_layer = curr_layer.__getattr__(temp_name)
# no exception means the layer is found
if len(layer_infos) > 0:
temp_name = layer_infos.pop(0)
# layer found but length is 0,
# break the loop and curr_layer keep point to the
# current layer
elif len(layer_infos) == 0:
break
except Exception:
# no such layer exist, pop next name and try again
if len(temp_name) > 0:
temp_name += '_'+layer_infos.pop(0)
else:
# temp_name is empty
temp_name = layer_infos.pop(0)
# org_forward(x) + lora_up(lora_down(x)) * multiplier
# ensure the sequence of lora_up(A) then lora_down(B)
pair_keys = []
if 'lora_down' in key:
pair_keys.append(key.replace('lora_down', 'lora_up'))
pair_keys.append(key)
else:
pair_keys.append(key)
pair_keys.append(key.replace('lora_up', 'lora_down'))
# update weight
if len(state_dict[pair_keys[0]].shape) == 4:
# squeeze(3) and squeeze(2) remove dimensions of size 1
# from the tensor to make the tensor more compact
weight_up = state_dict[pair_keys[0]].squeeze(3).\
squeeze(2).to(torch.float32)
weight_down = state_dict[pair_keys[1]].squeeze(3).\
squeeze(2).to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up,
weight_down).unsqueeze(2).unsqueeze(3)
else:
weight_up = state_dict[pair_keys[0]].to(torch.float32)
weight_down = state_dict[pair_keys[1]].to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up,
weight_down)
# update visited list, ensure no duplicated weight is
# processed.
for item in pair_keys:
visited.append(item)
使用该函数很简单;只需提供pipeline
对象、LoRA路径lora_path
和LoRA权重编号lora_weight
,如下所示:
pipeline = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.bfloat16
).to("cuda:0")
lora_path = r"MoXinV1.safetensors"
load_lora(
pipeline = pipeline,
lora_path = lora_path,
lora_weight = 0.5,
device = "cuda:0"
)
现在,让我们试试看:
prompt = """
shukezouma, shuimobysim ,a branch of flower, traditional chinese ink painting
"""
image = pipeline(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
display(image)
它确实有效,效果很好;请参见图8**.5中所示的结果:
图8.5:使用自定义LoRA加载器的花朵分支
你可能会想知道,“为什么一个小的LoRA文件具有如此强大的能力?”让我们深入探讨LoRA模型有效的原因。
为什么LoRA有效
Armen等人撰写的论文内禀维度解释了语言模型微调的有效性 [8]发现,预训练表示的内禀维度远低于预期,他们如下所述:
“我们通过实验表明,在预训练表示的上下文中,常见的NLP任务具有比完整参数化低几个数量级的内禀维度。”
矩阵的内禀维度是一个用于确定表示该矩阵中包含的重要信息所需的有效维数的概念。
假设我们有一个矩阵M
,有五行三列,如下所示:
M = 1 2 3
4 5 6
7 8 9
10 11 12
13 14 15
这个矩阵的每一行代表一个包含三个值的数据点或向量。我们可以将这些向量视为三维空间中的点。然而,如果我们可视化这些点,我们可能会发现它们大约位于一个二维平面上,而不是占据整个三维空间。
在这种情况下,矩阵M
的内禀维度将是2
,这意味着可以使用两个维度有效地捕捉数据的本质结构。第三个维度没有提供太多额外的信息。
一个低内禀维度的矩阵可以通过两个低秩矩阵来表示,因为矩阵中的数据可以被压缩到几个关键特征。然后,这些特征可以通过两个较小的矩阵来表示,每个矩阵的秩等于原始矩阵的内禀维度。
Edward J. Hu等人撰写的论文LoRA:大型语言模型的低秩自适应 [1]更进一步,引入了LoRA的概念,利用低内禀维度的特性,通过将权重差分解为两个低秩部分来加速微调过程,ΔW = A B T。
很快发现LoRA的有效性不仅限于LLM模型,还与扩散模型结合产生了良好的结果。Simo Ryu发布了LoRA [2]代码,并成为第一个尝试对Stable Diffusion进行LoRA训练的人。那是在2023年7月,现在在https://www.civitai.com上共享了超过40,000个LoRA模型。
摘要
在本章中,我们讨论了如何使用LoRA增强Stable Diffusion模型,理解了LoRA是什么,以及为什么它对微调和推理有益。
然后,我们开始使用Diffusers
包中的实验函数加载LoRA,并通过自定义实现提供LoRA权重。我们使用简单的代码快速了解LoRA能带来什么。
然后,我们深入研究了LoRA模型的内部结构,详细介绍了提取LoRA权重的步骤,并了解了如何将这些权重合并到检查点模型中。
此外,我们实现了一个Python函数,可以加载LoRA safetensors文件并执行权重合并。
最后,我们简要探讨了LoRA为何有效,基于研究人员最新的论文。
在下一章中,我们将探索另一种强大的技术——文本反转——来教模型新的“单词”,然后使用预训练的“单词”向生成的图像添加新概念。
参考文献
-
Edward J.等人,LoRA:大型语言模型的低秩自适应:https://arxiv.org/abs/2106.09685
-
Simo Ryu (cloneofsimo),
lora
: https://github.com/cloneofsimo/lora -
kohya_lora_loader
: https://gist.github.com/takuma104/e38d683d72b1e448b8d9b3835f7cfa44 -
CIVITAI:https://www.civitai.com
-
Rinon Gal等人,一张图片胜过千言万语:使用文本反转个性化文本到图像生成:https://textual-inversion.github.io/
-
Diffusers的
lora_state_dict
函数:https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/modeling_utils.py -
Andrew Zhu,改进Diffusers包以实现高质量图像生成:https://towardsdatascience.com/improving-diffusers-package-for-high-quality-image-generation-a50fff04bdd4
-
Armen等人,内在维度解释了语言模型微调的有效性:https://arxiv.org/abs/2012.13255
-
Hugging Face, LoRA: https://huggingface.co/docs/diffusers/training/lora
-
Hugging Face, PEFT: https://huggingface.co/docs/peft/en/index
第九章:使用文本反转
文本反转(TI)是向预训练模型提供额外功能的一种方式。与在第8章中讨论的低秩自适应(LoRA)不同,LoRA是一种应用于文本编码器和UNet注意力权重的微调技术,TI是一种基于训练数据添加新嵌入空间的技术。
在Stable Diffusion的上下文中,文本嵌入指的是将文本数据表示为高维空间中的数值向量,以便通过机器学习算法进行操作和处理。具体来说,在Stable Diffusion的情况下,文本嵌入通常使用对比语言-图像预训练(CLIP)[6]模型创建。
要训练一个TI模型,你只需要三到五张最小图像集,结果是一个紧凑的pt
或bin
文件,通常只有几KB大小。这使得TI成为将新元素、概念或风格融入预训练检查点模型的同时保持卓越便携性的高效方法。
在本章中,我们将首先使用来自diffusers
包的TI加载器使用TI,然后深入TI的核心以揭示其内部工作原理,最后构建一个自定义TI加载器以将TI权重应用于图像生成。
下面是我们将要讨论的主题:
-
使用TI进行扩散器推理
-
TI是如何工作的
-
构建自定义TI加载器
到本章结束时,你将能够开始使用社区共享的任何类型的TI,并构建你的应用程序以加载TI。
让我们开始利用Stable Diffusion TI的力量。
使用TI进行扩散器推理
在深入了解TI内部工作原理之前,让我们看看如何使用Diffusers来使用TI。
在Hugging Face的Stable Diffusion概念库[3]和CIVITAI[4]中共享了无数预训练的TI。例如,从Stable Diffusion概念库中最受欢迎的TI之一是sd-concepts-library/midjourney-style
[5]。我们可以通过在代码中简单地引用此名称来开始使用它;Diffusers将自动下载模型数据:
-
让我们初始化一个Stable Diffusion管道:
# initialize model
from diffusers import StableDiffusionPipeline
import torch
model_id = "stablediffusionapi/deliberate-v2"
pipe = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16
).to("cuda")
-
不使用TI生成图像:
# without using TI
prompt = "a high quality photo of a futuristic city in deep \
space, midjourney-style"
image = pipe(
prompt,
num_inference_steps = 50,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
在提示中,使用了
midjourney-style
,这将是TI的名称。如果没有应用名称,我们将看到生成的图像,如图图9.1所示:
图9.1:没有TI的深空未来城市
-
使用TI生成图像。
现在,让我们将TI加载到Stable Diffusion管道中,并给它命名为
midjourney-style
,以表示新添加的嵌入:pipe.load_textual_inversion(
"sd-concepts-library/midjourney-style",
token = "midjourney-style"
)
上述代码将自动下载TI并将其添加到管道模型中。再次执行相同的提示和管道,我们将得到一个全新的图像,如图图9.2所示:
图9.2:深空中带有TI的未来城市
是的,它看起来和感觉就像Midjourney生成的图像,但实际上是由Stable Diffusion生成的。TI名称中的“反转”表示我们可以将任何新名称逆转换为新嵌入,例如,如果我们给一个新标记命名为colorful-magic-style
:
pipe.load_textual_inversion(
"sd-concepts-library/midjourney-style",
token = "colorful-magic-style"
)
由于我们使用 midjourney-style
作为TI的名称,我们将得到相同的图像。这次,我们将 colorful-magic-style
“反转”到新嵌入中。然而,Diffusers提供的 load_textual_inversion
函数没有为用户提供 weight
参数来加载具有特定权重的TI。我们将在本章后面添加加权TI到我们自己的TI加载器中。
在此之前,让我们深入TI的核心,看看它是如何内部工作的。
TI是如何工作的
简而言之,训练TI就是找到一个与目标图像最佳匹配的文本嵌入,例如其风格、物体或面部。关键是找到一个在当前文本编码器中从未存在的新嵌入。正如图9**.3,从其原始论文[1]所示:
图9.3:文本嵌入和反转过程的概述
训练的唯一任务是找到一个由 v * 表示的新嵌入,并使用 S * 作为标记字符串占位符;字符串可以替换为任何在分词器中不存在的字符串。一旦找到新的对应嵌入向量,训练就完成了。训练的输出通常是一个包含768个数字的向量。这就是为什么TI文件如此小巧;它只是几千字节。
就像预训练的UNet是一堆矩阵魔法盒子,一个密钥(嵌入)可以解锁一个盒子,以获得一个图案、一种风格或一个物体。盒子的数量远远多于文本编码器提供的有限密钥。TI的训练是通过提供一个新密钥来解锁未知的魔法盒子来完成的。在整个训练和推理过程中,原始检查点模型保持不变。
精确地说,新嵌入的寻找可以定义为以下:
v * = arg v min E z∼E(x),y,ϵ∼N(0,1),t[||ϵ − ϵ θ(z t, t, c θ(y))|| 2 2]
让我们逐个从左到右解释公式:
-
v * 表示我们正在寻找的新嵌入。
-
arg min 符号常用于统计学和优化,表示最小化一个函数的值集合。这是一个有用的符号,因为它允许我们讨论函数的最小值,而无需指定最小值的实际值。
-
(E) 是损失期望。
-
z ∼ E(x) 表示输入图像将被编码到潜在空间。
-
y 是输入文本。
-
e ∼ N(0,1) 表示初始噪声潜在值是一个具有
0
均值和1
方差的严格高斯分布。 -
c θ(y) 代表一个将输入文本字符串 y 映射到嵌入向量的文本编码器模型。
-
ϵ θ(z t, t, c θ(y))表示我们在t步骤提供带噪声的潜在图像z,t本身和文本嵌入c θ(y),然后从UNet模型生成噪声向量。
-
2在|| 2中表示欧几里得距离的平方。2在|| 2中表示数据在2维。
一起,公式显示了我们可以如何使用Stable Diffusion的训练过程来近似一个新嵌入v *,它生成最小损失。
接下来,让我们构建一个自定义TI加载器函数。
构建自定义TI加载器
在本节中,我们将通过将前面的理解转化为代码,并为加载函数提供一个TI权重参数来构建一个TI加载器。
在编写函数代码之前,我们先来了解一下TI的内部结构。在运行以下代码之前,您需要先将TI文件下载到您的存储设备中。
pt文件格式的TI
以pt
文件格式加载TI:
# load a pt TI
import torch
loaded_learned_embeds = torch.load("badhandsv5-neg.pt",
map_location="cpu")
keys = list(loaded_learned_embeds.keys())
for key in keys:
print(key,":",loaded_learned_embeds[key])
我们可以清楚地看到TI文件中的键和配对值:
string_to_token : {'*': 265}
string_to_param : {'*': tensor([[ 0.0399, -0.2473, 0.1252, ..., 0.0455, 0.0845, -0.1463],
[-0.1385, -0.0922, -0.0481, ..., 0.1766, -0.1868, 0.3851]],
requires_grad=True)}
name : bad-hands-5
step : 1364
sd_checkpoint : 7ab762a7
sd_checkpoint_name : blossom-extract
最重要的值是具有string_to_param
键的tensor
对象。我们可以使用以下代码从中提取张量值:
string_to_token = loaded_learned_embeds['string_to_token']
string_to_param = loaded_learned_embeds['string_to_param']
# separate token and the embeds
trained_token = list(string_to_token.keys())[0]
embeds = string_to_param[trained_token]
bin文件格式的TI
Hugging Face概念库中的大多数TI都是bin
格式。bin
结构比pt
结构更简单:
import torch
loaded_learned_embeds = torch.load("midjourney_style.bin",
map_location="cpu")
keys = list(loaded_learned_embeds.keys())
for key in keys:
print(key,":",loaded_learned_embeds[key])
我们将看到这个——只有一个键和一个值的字典:
<midjourney-style> : tensor([-5.9785e-02, -3.8523e-02, 5.1913e-02, 8.0925e-03, -6.2018e-02,
1.3361e-01, 1.3679e-01, 8.2224e-02, -2.0598e-01, 1.8543e-02,
1.9180e-01, -1.5537e-01, -1.5216e-01, -1.2607e-01, -1.9420e-01,
1.0445e-01, 1.6942e-01, 4.2150e-02, -2.7406e-01, 1.8115e-01,
...
])
提取张量对象就像执行以下操作一样简单:
keys = list(loaded_learned_embeds.keys())
embeds = loaded_learned_embeds[keys[0]] * weight
构建TI加载器的详细步骤
这里是加载带有权重的TI的详细步骤:
-
emb_params
键用于存储嵌入张量。使用此函数在模型初始化阶段或图像生成阶段加载TI:
def load_textual_inversion(
learned_embeds_path,
token,
text_encoder,
tokenizer,
weight = 0.5,
device = "cpu"
):
loaded_learned_embeds = \
torch.load(learned_embeds_path, map_location=device)
if "string_to_token" in loaded_learned_embeds:
string_to_token = \
loaded_learned_embeds['string_to_token']
string_to_param = \
loaded_learned_embeds['string_to_param']
# separate token and the embeds
trained_token = list(string_to_token.keys())[0]
embeds = string_to_param[trained_token]
embeds = embeds[0] * weight
elif "emb_params" in loaded_learned_embeds:
embeds = loaded_learned_embeds["emb_params"][0] * weight
else:
keys = list(loaded_learned_embeds.keys())
embeds = loaded_learned_embeds[keys[0]] * weight
# ...
让我们分析前面的代码:
-
torch.load(learned_embeds_path, map_location=device)
使用PyTorch的torch.load
函数从指定的文件加载学习嵌入 -
if "string_to_token" in loaded_learned_embeds
检查一个特定的文件结构,其中嵌入存储在一个具有string_to_token
和string_to_param
键的字典中,并从这个结构中提取令牌和嵌入 -
elif "emb_params" in loaded_learned_embeds
则处理一个不同的结构,其中嵌入直接存储在emb_params
键下 -
else:
处理一个通用结构,假设嵌入存储在字典的第一个键下
实质上,权重作为嵌入向量的每个元素的乘数,微调TI效果强度。例如,权重值为
1.0
将应用TI的全强度,而值为0.5
将应用半强度。 -
-
将数据转换为与Stable Diffusion文本编码器相同的类型:
dtype = text_encoder.get_input_embeddings().weight.dtype
embeds.to(dtype)
-
将令牌添加到分词器中:
token = token if token is not None else trained_token
num_added_tokens = tokenizer.add_tokens(token)
if num_added_tokens == 0:
raise ValueError(
f"""The tokenizer already contains the token {token}.
Please pass a different `token` that is not already in
the tokenizer."""
)
如果添加的令牌已存在,代码将引发异常以防止覆盖现有令牌。
-
获取令牌ID并将新的嵌入添加到文本编码器中:
# resize the token embeddings
text_encoder.resize_token_embeddings(len(tokenizer))
# get the id for the token and assign the embeds
token_id = tokenizer.convert_tokens_to_ids(token)
text_encoder.get_input_embeddings().weight.data[token_id] = embeds
这就是加载大多数现有TI(来自Hugging Face和Civitai)所需的全部代码。
将所有代码放在一起
让我们把所有的代码块合并到一个函数中——load_textual_inversion
:
def load_textual_inversion(
learned_embeds_path,
token,
text_encoder,
tokenizer,
weight = 0.5,
device = "cpu"
):
loaded_learned_embeds = torch.load(learned_embeds_path,
map_location=device)
if "string_to_token" in loaded_learned_embeds:
string_to_token = loaded_learned_embeds['string_to_token']
string_to_param = loaded_learned_embeds['string_to_param']
# separate token and the embeds
trained_token = list(string_to_token.keys())[0]
embeds = string_to_param[trained_token]
embeds = embeds[0] * weight
elif "emb_params" in loaded_learned_embeds:
embeds = loaded_learned_embeds["emb_params"][0] * weight
else:
keys = list(loaded_learned_embeds.keys())
embeds = loaded_learned_embeds[keys[0]] * weight
# cast to dtype of text_encoder
dtype = text_encoder.get_input_embeddings().weight.dtype
embeds.to(dtype)
# add the token in tokenizer
token = token if token is not None else trained_token
num_added_tokens = tokenizer.add_tokens(token)
if num_added_tokens == 0:
raise ValueError(
f"""The tokenizer already contains the token {token}.
Please pass a different `token` that is not already in the
tokenizer."""
)
# resize the token embeddings
text_encoder.resize_token_embeddings(len(tokenizer))
# get the id for the token and assign the embeds
token_id = tokenizer.convert_tokens_to_ids(token)
text_encoder.get_input_embeddings().weight.data[token_id] = embeds
return (tokenizer,text_encoder)
要使用它,我们需要从管道对象中获取tokenizer
和text_encoder
:
text_encoder = pipe.text_encoder
tokenizer = pipe.tokenizer
然后通过调用新创建的函数来加载它:
load_textual_inversion(
learned_embeds_path = "learned_embeds.bin",
token = "colorful-magic-style",
text_encoder = text_encoder,
tokenizer = tokenizer,
weight = 0.5,
device = "cuda"
)
现在,使用相同的推理代码生成图像。请注意,这次我们使用的是权重为0.5
的TI,让我们看看与原始权重为1.0
的图像相比是否有任何不同:
prompt = "a high quality photo of a futuristic city in deep space, colorful-magic-style"
image = pipe(
prompt,
num_inference_steps = 50,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
结果看起来相当不错(见图9.4):
图9.4:通过自定义函数加载TI的深空未来城市
结果似乎比使用Diffusers的单行TI加载器更好。我们自定义加载器的另一个优点是,我们现在可以自由地为加载的模型分配权重。
摘要
本章讨论了稳定扩散TI是什么以及它与LoRA之间的区别。然后,我们介绍了一种快速将任何TI加载到Diffusers中的方法,以便在生成管道中应用新的图案、风格或对象。
然后,我们深入TI的核心,了解了它是如何训练和工作的。基于对它工作原理的理解,我们进一步实现了一个具有接受TI权重的TI加载器。
最后,我们提供了一段示例代码,用于调用自定义TI加载器,然后以0.5
的权重生成图像。
在下一章中,我们将探讨如何最大化提示词的力量并解锁它们的全部潜力。
参考文献
- Rinon 等人,一张图片胜过千言万语:使用文本反转个性化文本到图像生成:https://arxiv.org/abs/2208.01618 和 https://textual-inversion.github.io/
)
-
Hugging Face,文本反转:https://huggingface.co/docs/diffusers/main/en/training/text_inversion#how-it-works
-
Civitai:[https://civitai.com](https://civitai.com
)
- 在稳定扩散上应用Midjourney风格:[https://huggingface.co/sd-concepts-library/midjourney-style](https://huggingface.co/sd-concepts-library/midjourney-style
)
- OpenAI的CLIP:https://github.com/openai/CLIP
第十章:克服 77 个标记限制并启用提示加权
从 第 5 章,我们知道稳定扩散利用 OpenAI 的 CLIP 模型作为其文本编码器。根据源代码 [6],CLIP 模型的标记化实现具有 77 个标记的上下文长度。
CLIP 模型中的这个 77 个标记限制扩展到 Hugging Face Diffusers,限制了最大输入提示为 77 个标记。不幸的是,由于这个限制,无法在不进行一些修改的情况下在这些输入提示中分配关键词权重。
例如,假设你给出一个产生超过 77 个标记的提示字符串,如下所示:
from diffusers import StableDiffusionPipeline
import torch
pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype=torch.float16).to("cuda")
prompt = "a photo of a cat and a dog driving an aircraft "*20
image = pipe(prompt = prompt).images[0]
image
Diffusers 将显示一个警告消息,如下所示:
The following part of your input was truncated because CLIP can only handle sequences up to 77 tokens…
你不能通过提供权重来突出显示猫,如下所示:
a photo (cat:1.5) and a dog driving an aircraft
默认情况下,Diffusers
包不包括克服 77 个标记限制或为单个标记分配权重的功能,正如其文档所述。这是因为 Diffusers 旨在作为一个通用的工具箱,提供可以在各种项目中使用的必要功能。
尽管如此,通过使用 Diffusers 提供的核心功能,我们可以开发一个自定义提示解析器。这个解析器将帮助我们绕过 77 个标记的限制并为每个标记分配权重。在本章中,我们将深入探讨文本嵌入的结构,并概述一种方法来超越 77 个标记的限制,同时为每个标记分配权重值。
在本章中,我们将涵盖以下内容:
-
理解 77 个标记限制
-
克服 77 个标记限制
-
启用带权重的长提示
-
使用社区管道克服 77 个标记限制
如果你想要开始使用支持长提示加权的完整功能管道,请参阅 *使用社区 pipelines 部分克服 77 个标记限制。
到本章结束时,你将能够使用无大小限制的加权提示,并了解如何使用 Python 实现它们。
理解 77 个标记限制
稳定扩散(v1.5)文本编码器使用 OpenAI 的 CLIP 编码器 [2]。CLIP 文本编码器有一个 77 个标记的限制,这个限制传播到下游的稳定扩散。我们可以通过以下步骤重现 77 个标记的限制:
-
我们可以从稳定扩散中取出编码器并验证它。假设我们有提示
一张猫和狗驾驶飞机的照片
并将其乘以 20 以使提示的标记大小超过 77:prompt = "a photo of a cat and a dog driving an aircraft "*20
-
重新使用本章开头初始化的管道,并取出
tokenizer
和text_encoder
:tokenizer = pipe.tokenizer
text_encoder = pipe.text_encoder
-
使用
tokenizer
从提示中获取标记 ID:tokens = tokenizer(
prompt,
truncation = False,
return_tensors = 'pt'
)["input_ids"]
print(len(tokens[0]))
-
由于我们设置了
truncation = False
,tokenizer
将将任何长度的字符串转换为标记 ID。前面的代码将输出一个长度为 181 的标记列表。return_tensors = 'pt'
将告诉函数以[1,181]
张量对象的形式返回结果。尝试将标记 ID 编码为
embeddings
:embeddings = pipe.text_encoder(tokens.to("cuda"))[0]
我们将看到一个
RuntimeError
错误消息,内容如下:RuntimeError: The size of tensor a (181) must match the size of tensor b (77) at non-singleton dimension 1
从前面的步骤中,我们可以看到CLIP的文本编码器一次只能接受77个标记。
-
现在,让我们看看第一个和最后一个标记。如果我们去掉
*20
,只对提示a photo cat and dog driving an aircraft
进行分词,当我们打印出标记ID时,我们将看到10个标记ID而不是8个:tensor([49406, 320, 1125, 2368, 537, 1929, 4161, 550, 7706, 49407])
-
在前面的标记ID中,第一个(
49406
)和最后一个(49407
)是自动添加的。我们可以使用tokenizer._convert_id_to_token
将标记ID转换为字符串:print(tokenizer._convert_id_to_token(49406))
print(tokenizer._convert_id_to_token(49407))
我们可以看到两个额外的标记被添加到提示中:
<|startoftext|>
<|endoftext|>
为什么我们需要检查这个?因为当我们连接标记时,我们需要移除自动添加的开始和结束标记。接下来,让我们继续进行克服77个标记限制的步骤。
克服77个标记的限制
幸运的是,Stable Diffusion UNet不强制执行这个77个标记的限制。如果我们能够分批获取嵌入,将那些分块的嵌入连接成一个张量,并将其提供给UNet,我们应该能够克服77个标记的限制。以下是这个过程的大致概述:
-
从Stable Diffusion管道中提取文本分词器和文本编码器。
-
不论其大小如何,对输入提示进行分词。
-
消除添加的开始和结束标记。
-
提取前77个标记并将它们编码成嵌入。
-
将嵌入堆叠成一个大小为
[1, x, 768]
的张量。
现在,让我们使用Python代码来实现这个想法:
-
提取文本分词器和文本编码器:
# step 1\. take out the tokenizer and text encoder
tokenizer = pipe.tokenizer
text_encoder = pipe.text_encoder
我们可以重用Stable Diffusion管道中的分词器和文本编码器。
-
分词任何大小的输入提示:
# step 2\. encode whatever size prompt to tokens by setting
# truncation = False.
tokens = tokenizer(
prompt,
truncation = False
)["input_ids"]
print("token length:", len(tokens))
# step 2.2\. encode whatever size neg_prompt,
# padding it to the size of prompt.
negative_ids = pipe.tokenizer(
neg_prompt,
truncation = False,
padding = "max_length",
max_length = len(tokens)
).input_ids
print("neg_token length:", len(negative_ids))
在前面的代码中,我们做了以下操作:
-
我们将
truncation = False
设置为允许分词超过默认的77个标记限制。这确保了无论提示的大小如何,整个提示都会被分词。 -
标记作为Python列表返回,而不是torch张量。Python列表中的标记将使我们更容易添加额外的元素。请注意,在提供给文本编码器之前,标记列表将被转换为torch张量。
-
有两个额外的参数,
padding = "max_length"
和max_length = len(tokens)
。我们使用这些参数确保提示标记和负提示标记的大小相同。
-
-
移除开始和结束标记。
分词器将自动添加两个额外的标记:开始标记(
49406
)和结束标记(49407
)。在后续步骤中,我们将分割标记序列并将分块标记输入到文本编码器中。每个块将有自己的开始和结束标记。但在那之前,我们需要从原始的长标记列表中最初排除它们:
tokens = tokens[1:-1]
negative_ids = negative_ids[1:-1]
然后将这些开始和结束标记添加回分块标记中,每个块的大小为
75
。我们将在第4步将开始和结束标记添加回去。 -
将77个大小的分块标记编码成嵌入:
# step 4\. Pop out the head 77 tokens,
# and encode the 77 tokens to embeddings.
embeds,neg_embeds = [],[]
chunk_size = 75
bos = pipe.tokenizer.bos_token_id
eos = pipe.tokenizer.eos_token_id
for i in range(0, len(tokens), chunk_size):
# Add the beginning and end token to the 75 chunked tokens to
# make a 77-token list
sub_tokens = [bos] + tokens[i:i + chunk_size] + [eos]
# text_encoder support torch.Size([1,x]) input tensor
# that is why use [sub_tokens],
# instead of simply give sub_tokens.
tensor_tokens = torch.tensor(
[sub_tokens],
dtype = torch.long,
device = pipe.device
)
chunk_embeds = text_encoder(tensor_tokens)[0]
embeds.append(chunk_embeds)
# Add the begin and end token to the 75 chunked neg tokens to
# make a 77 token list
sub_neg_tokens = [bos] + negative_ids[i:i + chunk_size] + \
[eos]
tensor_neg_tokens = torch.tensor(
[sub_neg_tokens],
dtype = torch.long,
device = pipe.device
)
neg_chunk_embeds= text_encoder(tensor_neg_tokens)[0]
neg_embeds.append(neg_chunk_embeds)
前面的代码通过token列表循环,每次取出75个token。然后,它将起始和结束token添加到75个token的列表中,以创建一个77个token的列表。为什么是77个token?因为文本编码器一次可以编码77个token到嵌入中。
在
for
循环内部,第一部分处理提示嵌入,第二部分处理负嵌入。尽管我们提供了一个空的负提示,为了启用无分类指导扩散,我们仍然需要一个与正提示嵌入大小相同的负嵌入列表(在去噪循环中,条件潜在将减去由无提示生成的无条件潜在)。 -
将嵌入堆叠到
[1,x,768]
大小的torch张量。在这一步之前,
embeds
列表包含如下数据:[tensor1, tensor2...]
Stable Diffusion流水线的嵌入参数接受大小为
torch.Size([1,x,768])
的张量。我们仍然需要使用这两行代码将这些列表转换为三维张量:
# step 5\. Stack the embeddings to a [1,x,768] size torch tensor.
prompt_embeds = torch.cat(embeds, dim = 1)
prompt_neg_embeds = torch.cat(neg_embeds, dim = 1)
在前面的代码中,我们有以下内容:
-
embeds
和neg_embeds
是PyTorch张量的列表。torch.cat()
函数用于沿着由dim
指定的维度连接这些张量。在这种情况下,我们有dim=1
,这意味着张量是在它们的第二个维度上连接的(因为Python使用0基于索引)。 -
prompt_embeds
是一个包含embeds
中所有嵌入的张量。同样,prompt_neg_embeds
包含neg_embeds
中所有嵌入的张量。
-
到目前为止,我们已经有一个可以转换任何长度提示到嵌入的文本编码器,这些嵌入可以被Stable Diffusion流水线使用。接下来,让我们将所有代码放在一起。
将所有代码组合到一个函数中
让我们更进一步,将所有之前的代码放入一个打包的函数中:
def long_prompt_encoding(
pipe:StableDiffusionPipeline,
prompt,
neg_prompt = ""
):
bos = pipe.tokenizer.bos_token_id
eos = pipe.tokenizer.eos_token_id
chunk_size = 75
# step 1\. take out the tokenizer and text encoder
tokenizer = pipe.tokenizer
text_encoder = pipe.text_encoder
# step 2.1\. encode whatever size prompt to tokens by setting
# truncation = False.
tokens = tokenizer(
prompt.
truncation = False,
# return_tensors = 'pt'
)["input_ids"]
# step 2.2\. encode whatever size neg_prompt,
# padding it to the size of prompt.
negative_ids = pipe.tokenizer(
neg_prompt,
truncation = False,
# return_tensors = "pt",
Padding = "max_length",
max_length = len(tokens)
).input_ids
# Step 3\. remove begin and end tokens
tokens = tokens[1:-1]
negative_ids = negative_ids[1:-1]
# step 4\. Pop out the head 77 tokens,
# and encode the 77 tokens to embeddings.
embeds,neg_embeds = [],[]
for i in range(0, len(tokens), chunk_size):
# Add the beginning and end tokens to the 75 chunked tokens to make a
# 77-token list
sub_tokens = [bos] + tokens[i:i + chunk_size] + [eos]
# text_encoder support torch.Size([1,x]) input tensor
# that is why use [sub_tokens], instead of simply give sub_tokens.
tensor_tokens = torch.tensor(
[sub_tokens],
dtype = torch.long,
device = pipe.device
)
chunk_embeds = text_encoder(tensor_tokens)[0]
embeds.append(chunk_embeds)
# Add beginning and end token to the 75 chunked neg tokens to make a
# 77-token list
sub_neg_tokens = [bos] + negative_ids[i:i + chunk_size] + \
[eos]
tensor_neg_tokens = torch.tensor(
[sub_neg_tokens],
dtype = torch.long,
device = pipe.device
)
neg_chunk_embeds = text_encoder(tensor_neg_tokens)[0]
neg_embeds.append(neg_chunk_embeds)
# step 5\. Stack the embeddings to a [1,x,768] size torch tensor.
prompt_embeds = torch.cat(embeds, dim = 1)
prompt_neg_embeds = torch.cat(neg_embeds, dim = 1)
return prompt_embeds, prompt_neg_embeds
让我们创建一个长提示来测试前面的函数是否工作:
prompt = "photo, cute cat running on the grass" * 10 #<- long prompt
prompt_embeds, prompt_neg_embeds = long_prompt_encoding(
pipe, prompt, neg_prompt="low resolution, bad anatomy"
)
print(prompt_embeds.shape)
image = pipe(
prompt_embeds = prompt_embeds,
negative_prompt_embeds = prompt_neg_embeds,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
结果如图10.1所示:
图10.1:可爱的小猫在草地上奔跑,使用了长提示
如果我们的新函数对长提示有效,生成的图像应该反映附加的提示信息。让我们将提示信息扩展到以下内容:
prompt = "photo, cute cat running on the grass" * 10
prompt = prompt + ",pure white cat" * 10
新的提示信息将生成如图10.2所示的图像:
图10.2:可爱的小猫在草地上奔跑,附加了纯白色猫的提示
如您所见,新添加的提示信息工作正常,并且为猫添加了更多白色元素;然而,它仍然不是提示信息中要求的纯白色。我们将通过提示权重来解决此问题,我们将在下一节中介绍。
启用带权重的长提示
我们刚刚为基于Stable Diffusion管道(v1.5版)构建了任意大小的文本编码器。所有这些步骤都是为构建带有加权文本编码器的长提示铺路。
加权Stable Diffusion提示是指为用于通过Stable Diffusion算法生成图像的文本提示中的特定单词或短语分配不同的重要性级别。通过调整这些权重,我们可以控制某些概念对生成输出的影响程度,从而实现图像的更大定制和细化。
该过程通常涉及放大或缩小与提示中每个概念相关的文本嵌入向量。例如,如果您想使Stable Diffusion模型强调某个特定主题,同时降低另一个主题的强调,您将增加前者的权重并减少后者的权重。加权提示使我们能够更好地引导图像生成,以达到期望的结果。
为提示添加权重的核心仅仅是向量乘法:
加权 _ 嵌入 = [embedding1, embedding2, ..., embedding768] × 权重
在此之前,我们仍需要进行一些准备工作以创建加权提示嵌入,如下所示:
-
将
a (white) cat
转换成如下列表:[['a ', 1.0], ['white', 1.1], ['cat', 1.0]]
。我们将采用在Automatic1111 Stable Diffusion (SD) WebUI中广泛使用的提示格式,如开源SD WebUI[4]中定义的那样。 -
标记和权重提取:将标记ID及其对应的权重分别放入两个不同的列表中。
-
提示和负提示填充:确保提示和负提示标记具有相同的最大长度。如果提示比负提示长,则将负提示填充到与提示相同的长度。否则,将提示填充以与负提示的长度对齐。
关于注意力和强调(权重),我们将实现以下权重格式[4]:
a (word) - increase attention to word by a factor of 1.1
a ((word)) - increase attention to word by a factor of 1.21 (= 1.1 * 1.1)
a [word] - decrease attention to word by a factor of 1.1
a (word:1.5) - increase attention to word by a factor of 1.5
a (word:0.25) - decrease attention to word by a factor of 4 (= 1 / 0.25)
a \(word\) - use literal () characters in prompt
让我们更详细地了解这些步骤:
-
构建名为
parse_prompt_attention
的函数。为了确保提示格式与Automatic1111的SD WebUI完全兼容,我们将从开源的
parse_prompt_attention
函数[3]中提取并重用该函数:def parse_prompt_attention(text):
import re
re_attention = re.compile(
r"""
\\\(|\\\)|\\\[|\\]|\\\\|\\|\(|\[|:([+-]?[.\d]+)\)|
\)|]|[^\\()\[\]:]+|:
"""
, re.X
)
re_break = re.compile(r"\s*\bBREAK\b\s*", re.S)
res = []
round_brackets = []
square_brackets = []
round_bracket_multiplier = 1.1
square_bracket_multiplier = 1 / 1.1
def multiply_range(start_position, multiplier):
for p in range(start_position, len(res)):
res[p][1] *= multiplier
for m in re_attention.finditer(text):
text = m.group(0)
weight = m.group(1)
if text.startswith('\\'):
res.append([text[1:], 1.0])
elif text == '(':
round_brackets.append(len(res))
elif text == '[':
square_brackets.append(len(res))
elif weight is not None and len(round_brackets) > 0:
multiply_range(round_brackets.pop(), float(weight))
elif text == ')' and len(round_brackets) > 0:
multiply_range(round_brackets.pop(), \
round_bracket_multiplier)
elif text == ']' and len(square_brackets) > 0:
multiply_range(square_brackets.pop(), \
square_bracket_multiplier)
else:
parts = re.split(re_break, text)
for i, part in enumerate(parts):
if i > 0:
res.append(["BREAK", -1])
res.append([part, 1.0])
for pos in round_brackets:
multiply_range(pos, round_bracket_multiplier)
for pos in square_brackets:
multiply_range(pos, square_bracket_multiplier)
if len(res) == 0:
res = [["", 1.0]]
# merge runs of identical weights
i = 0
while i + 1 < len(res):
if res[i][1] == res[i + 1][1]:
res[i][0] += res[i + 1][0]
res.pop(i + 1)
else:
i += 1
return res
使用以下方式调用先前创建的函数:
parse_prompt_attention("a (white) cat")
这将返回以下内容:
[['a ', 1.0], ['white', 1.1], [' cat', 1.0]]
-
获取带有权重的提示。
在前述函数的帮助下,我们可以得到一组提示和权重对的列表。文本编码器将仅对提示中的标记进行编码(不需要将权重作为输入提供给文本编码器)。我们需要进一步处理提示-权重对,将其转换为两个大小相同的独立列表,一个用于标记ID,一个用于权重,如下所示:
tokens: [1,2,3...]
weights: [1.0, 1.0, 1.0...]
这可以通过以下函数来完成:
# step 2\. get prompts with weights
# this function works for both prompt and negative prompt
def get_prompts_tokens_with_weights(
pipe: StableDiffusionPipeline,
prompt: str
):
texts_and_weights = parse_prompt_attention(prompt)
text_tokens,text_weights = [],[]
for word, weight in texts_and_weights:
# tokenize and discard the starting and the ending token
token = pipe.tokenizer(
word,
# so that tokenize whatever length prompt
truncation = False
).input_ids[1:-1]
# the returned token is a 1d list: [320, 1125, 539, 320]
# use merge the new tokens to the all tokens holder:
# text_tokens
text_tokens = [*text_tokens,*token]
# each token chunk will come with one weight, like ['red
# cat', 2.0]
# need to expand the weight for each token.
chunk_weights = [weight] * len(token)
# append the weight back to the weight holder: text_
# weights
text_weights = [*text_weights, *chunk_weights]
return text_tokens,text_weights
前述函数接受两个参数:SD管道和提示字符串。输入字符串可以是正提示或负提示。
在函数体内,我们首先调用
parse_prompt_attention
函数,以最小的粒度(权重应用于单个标记级别)关联带有权重的提示。然后,我们遍历列表,对文本进行标记化,并使用索引操作[1:-1]
移除标记器添加的开始和结束标记ID。将新的标记ID合并回包含所有标记ID的列表。同时,扩展权重数量并将其合并回包含所有权重数字的列表。
让我们重用“一只(白色)猫”的提示并调用该函数:
prompt = "a (white) cat"
tokens, weights = get_prompts_tokens_with_weights(pipe, prompt)
print(tokens,weights)
前面的代码将返回以下内容:
[320, 1579, 2368] [1.0, 1.1, 1.0]
注意到“白色”的第二个标记ID现在权重为
1.1
而不是1.0
。 -
填充标记。
在这一步,我们将进一步将标记ID列表及其权重转换为分块列表。
假设我们有一个包含超过77个元素的标记ID列表:
[``1,2,3,...,100]
我们需要将其转换为包含分块的列表,每个块包含最多77个(最大)标记:
[[``49406,1,2...75,49407],[49406,76,77,...,100,49407]]
这样做是为了在下一步中,我们可以遍历列表的外层,并逐个编码77个标记的列表。
现在,你可能想知道为什么我们需要一次向文本编码器提供最多77个标记。如果我们简单地循环每个元素并逐个编码一个标记会怎样?这是一个好问题,但我们不能这样做,因为单独编码“白色”然后编码“猫”将产生与一次一起编码“白色猫”不同的嵌入。
我们可以通过快速测试来找出差异。首先,让我们只编码“白色”:
# encode "white" only
white_token = 1579
white_token_tensor = torch.tensor(
[[white_token]],
dtype = torch.long,
device = pipe.device
)
white_embed = pipe.text_encoder(white_token_tensor)[0]
print(white_embed[0][0])
然后,一起编码“白色”和“猫”:
# encode "white cat"
white_token, cat_token = 1579, 2369
white_cat_token_tensor = torch.tensor(
[[white_token, cat_token]],
dtype = torch.long,
device = pipe.device
)
white_cat_embeds = pipe.text_encoder(white_cat_token_tensor)[0]
print(white_cat_embeds[0][0])
尝试运行前面的代码;你会发现相同的“白色”会导致不同的嵌入。根本原因是什么?标记和嵌入不是一对一的映射;嵌入是基于自注意力机制[5]生成的。单个“白色”可以代表颜色或姓氏,而“白色猫”中的“白色”显然是在说这是一个描述猫的颜色。
让我们回到填充工作。以下代码将检查标记列表的长度。如果标记ID列表长度大于75,则取前75个标记并循环此操作,剩余的标记少于75个,将由单独的逻辑处理:
# step 3\. padding tokens
def pad_tokens_and_weights(
token_ids: list,
weights: list
):
bos,eos = 49406,49407
# this will be a 2d list
new_token_ids = []
new_weights = []
while len(token_ids) >= 75:
# get the first 75 tokens
head_75_tokens = [token_ids.pop(0) for _ in range(75)]
head_75_weights = [weights.pop(0) for _ in range(75)]
# extract token ids and weights
temp_77_token_ids = [bos] + head_75_tokens + [eos]
temp_77_weights = [1.0] + head_75_weights + [1.0]
# add 77 tokens and weights chunks to the holder list
new_token_ids.append(temp_77_token_ids)
new_weights.append(temp_77_weights)
# padding the left
if len(token_ids) > 0:
padding_len = 75 - len(token_ids)
padding_len = 0
temp_77_token_ids = [bos] + token_ids + [eos] * \
padding_len + [eos]
new_token_ids.append(temp_77_token_ids)
temp_77_weights = [1.0] + weights + [1.0] * \
padding_len + [1.0]
new_weights.append(temp_77_weights)
# return
return new_token_ids, new_weights
接下来,使用以下函数:
t,w = pad_tokens_and_weights(tokens.copy(), weights.copy())
print(t)
print(w)
前面的函数接受以下先前生成的
tokens
和weights
列表:[320, 1579, 2368] [1.0, 1.1, 1.0]
它将其转换为以下形式:
[[49406, 320, 1579, 2368, 49407]]
[[1.0, 1.0, 1.1, 1.0, 1.0]]
-
获取加权嵌入。
这是最后一步,我们将得到没有标记大小限制的与Automatic1111兼容的嵌入:
def get_weighted_text_embeddings(
pipe: StableDiffusionPipeline,
prompt : str = "",
neg_prompt: str = ""
):
eos = pipe.tokenizer.eos_token_id
prompt_tokens, prompt_weights = \
get_prompts_tokens_with_weights(
pipe, prompt
)
neg_prompt_tokens, neg_prompt_weights = \
get_prompts_tokens_with_weights(pipe, neg_prompt)
# padding the shorter one
prompt_token_len = len(prompt_tokens)
neg_prompt_token_len = len(neg_prompt_tokens)
if prompt_token_len > neg_prompt_token_len:
# padding the neg_prompt with eos token
neg_prompt_tokens = (
neg_prompt_tokens + \
[eos] * abs(prompt_token_len - neg_prompt_token_len)
)
neg_prompt_weights = (
neg_prompt_weights +
[1.0] * abs(prompt_token_len - neg_prompt_token_len)
)
else:
# padding the prompt
prompt_tokens = (
prompt_tokens \
+ [eos] * abs(prompt_token_len - \
neg_prompt_token_len)
)
prompt_weights = (
prompt_weights \
+ [1.0] * abs(prompt_token_len - \
neg_prompt_token_len)
)
embeds = []
neg_embeds = []
prompt_token_groups ,prompt_weight_groups = \
pad_tokens_and_weights(
prompt_tokens.copy(),
prompt_weights.copy()
)
neg_prompt_token_groups, neg_prompt_weight_groups = \
pad_tokens_and_weights(
neg_prompt_tokens.copy(),
neg_prompt_weights.copy()
)
# get prompt embeddings one by one is not working.
for i in range(len(prompt_token_groups)):
# get positive prompt embeddings with weights
token_tensor = torch.tensor(
[prompt_token_groups[i]],
dtype = torch.long, device = pipe.device
)
weight_tensor = torch.tensor(
prompt_weight_groups[i],
dtype = torch.float16,
device = pipe.device
)
token_embedding = \
pipe.text_encoder(token_tensor)[0].squeeze(0)
for j in range(len(weight_tensor)):
token_embedding[j] = token_embedding[j] *
weight_tensor[j]
token_embedding = token_embedding.unsqueeze(0)
embeds.append(token_embedding)
# get negative prompt embeddings with weights
neg_token_tensor = torch.tensor(
[neg_prompt_token_groups[i]],
dtype = torch.long, device = pipe.device
)
neg_weight_tensor = torch.tensor(
neg_prompt_weight_groups[i],
dtype = torch.float16,
device = pipe.device
)
neg_token_embedding = \
pipe.text_encoder(neg_token_tensor)[0].squeeze(0)
for z in range(len(neg_weight_tensor)):
neg_token_embedding[z] = (
neg_token_embedding[z] * neg_weight_tensor[z]
)
neg_token_embedding = neg_token_embedding.unsqueeze(0)
neg_embeds.append(neg_token_embedding)
prompt_embeds = torch.cat(embeds, dim = 1)
neg_prompt_embeds = torch.cat(neg_embeds, dim = 1)
return prompt_embeds, neg_prompt_embeds
函数看起来有点长,但逻辑很简单。让我分段解释:
-
在填充较短的提示部分,逻辑会将较短的提示填充到结束标记(
eos
),这样提示和负提示标记列表就具有相同的大小(这样生成的潜在变量可以进行减法操作)。 -
我们调用
pad_tokens_and_weights
函数将所有标记和权重分割成块,每个块包含77个元素。 -
我们遍历块列表,并在一步中将77个标记编码为嵌入。
-
我们使用
token_embedding = pipe.text_encoder(token_tensor)[0].squeeze(0)
来移除空维度,这样我们就可以将每个元素与其权重相乘。注意,现在,每个标记都由一个768个元素的向量表示。 -
最后,我们退出循环,并使用
prompt_embeds = torch.cat(embeds, dim = 1)
将张量列表堆叠成一个更高维度的张量。
-
验证工作
在编写了不那么多的代码之后,我们终于准备好了所有逻辑,现在让我们测试一下代码。
在长提示编码器的简单版本中,我们仍然得到一只猫,身体上有一些图案,而不是我们在提示中给出的纯白色
。现在,让我们给white
关键词添加权重,看看会发生什么:
prompt = "photo, cute cat running on the grass" * 10
prompt = prompt + ",pure (white:1.5) cat" * 10
neg_prompt = "low resolution, bad anatomy"
prompt_embeds, prompt_neg_embeds = get_weighted_text_embeddings(
pipe, prompt = prompt, neg_prompt = neg_prompt
)
image = pipe(
prompt_embeds = prompt_embeds,
negative_prompt_embeds = prompt_neg_embeds,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
我们新的嵌入函数神奇地使我们能够生成一只纯白色的猫,因为我们给“白色”关键词赋予了1.5
的权重。
图10.3:一只可爱的纯白色猫在草地上奔跑,对“白色”一词的权重为1.5
就这些!现在,我们可以重用或扩展这个函数来构建我们想要的任何自定义提示解析器。但如果你不想自己构建函数来实现,有没有办法开始使用无限加权提示?是的,接下来我们将介绍两个由开源社区贡献并集成到Diffusers中的管道。
使用社区管道克服77个标记的限制
从零开始实现支持长提示加权的管道可能具有挑战性。通常,我们只是希望利用Diffusers使用详细和细微的提示来生成图片。幸运的是,开源社区已经为SD v1.5和SDXL提供了实现。SDXL的实现最初由本书的作者Andrew Zhu初始化,并由社区大幅改进。
我现在将提供两个示例,说明如何使用社区管道来处理SD v1.5和SDXL:
-
这个例子使用了SD v1.5的
lpw_stable_diffusion
管道。使用以下代码启动一个长提示加权管道:
from diffusers import DiffusionPipeline
import torch
model_id_or_path = "stablediffusionapi/deliberate-v2"
pipe = DiffusionPipeline.from_pretrained(
model_id_or_path,
torch_dtype = torch.float16,
custom_pipeline = "lpw_stable_diffusion"
).to("cuda:0")
在前面的代码中,
custom_pipeline = "lpw_stable_diffusion"
实际上会从Hugging Face服务器下载lpw_stable_diffusion
文件,并在DiffusionPipeline
管道内部调用。 -
让我们使用这个管道生成一张图片:
prompt = "photo, cute cat running on the grass" * 10
prompt = prompt + ",pure (white:1.5) cat" * 10
neg_prompt = "low resolution, bad anatomy"
image = pipe(
prompt = prompt,
negative_prompt = neg_prompt,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
你将看到与图10.3相同的图片。
-
现在让我们通过使用
lpw_stable_diffusion
管道来为SDXL举一个例子。使用方法几乎与我们在 SD v1.5 中使用的方法相同。唯一的区别是我们正在加载一个 SDXL 模型,并且我们使用了一个不同的自定义管道名称:
lpw_stable_diffusion_xl
。请看以下代码:from diffusers import DiffusionPipeline
import torch
model_id_or_path = "stabilityai/stable-diffusion-xl-base-1.0"
pipe = DiffusionPipeline.from_pretrained(
model_id_or_path,
torch_dtype = torch.float16,
custom_pipeline = "lpw_stable_diffusion_xl",
).to("cuda:0")
图像生成代码与我们用于 SD v1.5 的代码完全相同:
prompt = "photo, cute cat running on the grass" * 10
prompt = prompt + ",pure (white:1.5) cat" * 10
neg_prompt = "low resolution, bad anatomy"
image = pipe(
prompt = prompt,
negative_prompt = neg_prompt,
generator = torch.Generator("cuda").manual_seed(7)
).images[0]
image
我们将看到如图 图 10.4 所示的图像:
图 10.4:一只可爱的纯白色猫在草地上奔跑,对“白色”一词的权重为 1.5,使用 lpw_stable_diffusion_xl
从图像中,我们可以清楚地看到 pure (white:1.5) cat
带入图像中的内容:证明该管道可以使用长加权提示生成图像。
摘要
本章试图解决最热门讨论的话题之一:使用 Diffusers
包克服 77 个标记限制并为 Stable Diffusion 管道添加提示权重。Automatic1111 的 Stable Diffusion WebUI 提供了一个灵活的用户界面,并且现在(在我写这篇文章的时候)是最流行的提示权重和关注格式。然而,如果我们查看 Automatic1111 的代码,我们可能会很快迷失方向;它的代码很长,没有清晰的文档。
本章从了解 77 个标记限制的根本原因开始,进而探讨了 Stable Diffusion 管道如何使用提示嵌入。我们实现了两个函数来克服 77 个标记限制。
实现了一个不带权重的简单函数,以展示如何绕过 77 个标记限制。我们还构建了另一个具有完整长提示使用功能(无长度限制)的函数,并实现了提示加权。
通过理解和实现这两个函数,我们可以利用这个想法,不仅可以使用 Diffuser 生成与使用 Automatic1111 的 WebUI 相同的高质量图像,还可以进一步扩展它以添加更多强大的功能。至于要添加哪个功能,现在取决于你。在下一章中,我们将开始另一个令人兴奋的主题:使用 Stable Diffusion 修复和放大图像。
参考文献
-
Hugging Face,加权提示:https://huggingface.co/docs/diffusers/main/en/using-diffusers/weighted_prompts
-
OpenAI CLIP,连接文本和图像:https://openai.com/research/clip
-
Automatic1111,Stable Diffusion WebUI 提示解析器:https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/modules/prompt_parser.py#L345C19-L345C19
)
- Automatic1111,关注/强调:https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#attentionemphasis
)
-
Ashish 等人,Attention Is All You Need: https://arxiv.org/abs/1706.03762
-
77 个 token 大小限制的来源:https://github.com/openai/CLIP/blob/4d120f3ec35b30bd0f992f5d8af2d793aad98d2a/clip/clip.py#L206
第十一章:图像恢复和超分辨率
虽然稳定扩散V1.5和稳定扩散XL都展示了生成图像的能力,但我们的初始创作可能尚未展现出它们的最优质量。本章旨在探索各种技术和策略,以提高图像恢复、增强图像分辨率并向生成的视觉作品中引入复杂细节。
本章的主要重点是利用稳定扩散作为有效工具来增强和提升图像的潜力。此外,我们将简要介绍一些互补的尖端人工智能(AI)方法,以提升图像分辨率,这些方法与基于扩散的过程不同。
在本章中,我们将涵盖以下主题:
-
理解术语
-
使用图像到图像扩散管道提升图像
-
使用ControlNet Tile提升图像
让我们开始吧!
理解术语
在我们开始使用稳定扩散来提升图像质量之前,了解与此过程相关的常见术语是有益的。在关于稳定扩散的文章或书籍中,你可能会遇到三个相关术语:图像插值、图像上采样和图像超分辨率。这些技术旨在提高图像的分辨率,但它们在方法和结果上有所不同。熟悉这些术语将帮助你更好地理解稳定扩散和其他图像增强工具的工作原理:
-
图像插值是提升图像最简单且最普遍的方法。它通过基于图像中现有像素来近似新的像素值来实现。存在多种插值方法,每种方法都有其优势和劣势。最常用的插值方法包括最近邻插值[6]、双线性插值[7]、双三次插值[8]和Lanczos重采样[9]。
-
图像上采样是一个更广泛的术语,包括任何增强图像分辨率的技巧。这个类别包括插值方法,以及更复杂的方法,如超分辨率。
-
图像超分辨率代表一种特定的图像上采样子集,旨在在不增加原始尺寸的情况下提高图像的分辨率和更精细的细节,同时最小化质量损失并防止出现伪影。与依赖于基本插值的传统图像上采样方法不同,图像超分辨率采用先进的算法,通常基于深度学习技术。这些算法从高分辨率图像数据集中学习高频模式和细节。随后,这些学习到的模式被用于提升低分辨率图像,产生高质量的成果。
解决上述任务(图像插值、图像上采样和图像超分辨率)的解决方案通常被称为上采样器或高分辨率修复器。在本章中,我们将使用术语upscaler。
在基于深度学习的超分辨率解决方案中,存在各种类型的上采样器。超分辨率解决方案可以大致分为三种类型:
-
基于 GAN 的解决方案,例如 ESRGAN [10]
-
基于 Swin Transformer 的解决方案,例如 SwinIR [11],
-
基于 Stable Diffusion 的解决方案
在本章中,我们的主要关注点将是 Stable Diffusion 上采样器。这一选择不仅受到本书对扩散模型的强调,还受到 Stable Diffusion 提供增强上采样结果和优越控制灵活性的潜力驱动。例如,它允许我们通过提示来指导超分辨率过程并填充额外的细节。我们将在本章的后半部分使用 Python 代码实现这一点。
你可能急于开始使用来自 Diffusers 的 Latent Upscaler 和 Stable Diffusion Upscale 管道 [1]。然而,值得注意的是,当前的 Latent Upscaler 和 Upscale 管道并非最优。两者都严重依赖于特定的预训练模型,消耗大量的 VRAM,并且表现出相对较慢的性能。
本章将介绍基于 Stable Diffusion 方法两种替代解决方案:
-
StableDiffusionPipeline
类。它还保留了支持长提示并引入了在前面章节中介绍过的文本反转。 -
ControlNet
模型,这使得通过显著增强细节来实现图像超分辨率成为可能。
在这个背景下,让我们深入了解这两种超分辨率方法的复杂细节。
使用 Img2img 扩散进行图像上采样
正如我们在第五章中讨论的那样,Stable Diffusion 并不仅仅依赖于文本作为其初始指导;它还能够利用图像作为起点。我们实现了一个自定义的管道,该管道以图像作为图像生成的基石。
通过将去噪强度降低到某个阈值,例如 0.3
,初始图像的特征和风格将保留在最终生成的图像中。这一特性可以用来将 Stable Diffusion 作为图像上采样器,从而实现图像超分辨率。让我们一步一步地探索这个过程。
我们将首先介绍一步超分辨率的概念,然后探讨多步超分辨率。
一步超分辨率
在本节中,我们将介绍一种使用图像到图像扩散一次上采样图像的解决方案。以下是实现它的步骤指南:
-
让我们首先使用Stable Diffusion生成一个256x256的起始图像。而不是从互联网下载图像或使用外部图像作为输入,让我们利用Stable Diffusion生成一个。毕竟,这是Stable Diffusion擅长的领域:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16
).to("cuda:0")
prompt = "a realistic photo of beautiful woman face"
neg_prompt = "NSFW, bad anatomy"
raw_image = text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
generator = torch.Generator("cuda").manual_seed(3)
).images[0]
display(raw_image)
前面的代码将生成如图图11**.1所示的图像:
图11.1:由Stable Diffusion生成的256x256大小的女性面部照片
如果你以打印形式(例如,一本书)查看图像,可能看不到图像中的噪声和模糊。然而,如果你运行前面的代码并放大图像,你很容易注意到生成的图像中的模糊和噪声。
让我们保存图像以进行进一步处理:
image_name = "woman_face"
file_name_256x256 =f"input_images/{image_name}_256x256.png"
raw_image.save(file_name_256x256)
-
将图像调整到目标大小。最初,我们需要建立一个图像调整函数,确保图像的宽度和高度都能被
8
整除:def get_width_height(width, height):
width = (width//8)*8
height = (height//8)*8
return width,height
接下来,使用图像插值将其调整到目标大小:
from diffusers.utils import load_image
from PIL import Image
def resize_img(img_path,upscale_times):
img = load_image(img_path)
if upscale_times <=0:
return img
width,height = img.size
width = width * upscale_times
height = height * upscale_times
width,height = get_width_height(int(width),int(height))
img = img.resize(
(width,height),
resample = Image.LANCZOS if upscale_times > 1 \
else Image.AREA
)
return img Image.LANCZOS interpolation method.
以下代码使用
resize_img
函数将图像放大三倍:resized_raw_image = resize_img(file_name_256x256, 3.0)
你可以将任何大于
1.0
的浮点数输入到函数中。 -
创建一个img-to-img管道作为上采样器。为了启用引导图像超分辨率,我们需要提供一个引导提示,如下所示:
sr_prompt = """8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo, """
prompt = """
a realistic photo of beautiful woman face
"""
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
sr_prompt
表示超分辨率提示,可以在任何超分辨率任务中重用,无需更改。接下来,调用管道以提升图像:prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
img2image_3x = img2img_pipe(
image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.3,
num_inference_steps = 80,
guidance_scale = 8,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
img2image_3x
注意,
strength
参数设置为0.3
,这意味着每个去噪步骤将对潜在图像应用30%的高斯噪声。当使用纯文本到图像的管道时,默认将强度设置为1.0
。通过增加这里的strength
值,将向初始图像引入更多的新
元素。根据我的测试,0.3
似乎是一个很好的平衡点。然而,你可以将其调整为0.25
或提高到0.4
。
对于Diffusers的img-to-img管道,实际的去噪步骤将是num_inference_steps
乘以strength
。总去噪步骤将是80 × 0.3 = 24。这不是Stable Diffusion强制执行的规则;它来自Diffusers的Stable Diffusion管道的实现。
如第3章所述,guidance_scale
参数控制结果与提供的prompt
和neg_prompt
的匹配程度。在实践中,较高的guidance_scale
会产生稍微清晰一些的图像,但可能会更多地改变图像元素,而较低的guidance_scale
会导致图像更加模糊,同时保留更多原始图像元素。如果你不确定值,可以选择介于7到8之间的某个值。
一旦运行前面的代码,你会观察到原始图像的大小不仅升级到768x768,图像质量也经历了显著的提升。
然而,这并不是终点;我们可以重用前面的过程来进一步提高图像分辨率和质量。
让我们保存图像以供进一步使用:
file_name_768x768 = f"input_images/{image_name}_768x768.png"
img2image_3x.save(file_name_768x768)
接下来,让我们使用多图像到图像步骤来提高图像分辨率。
多步超分辨率
使用一步分辨率,代码将图像从 256x256 超分辨率到 768x768。在本节中,我们将过程进一步推进,将图像大小增加到当前尺寸的两倍。
注意,在进步到更高分辨率的图像之前,您需要确保 VRAM 的使用可能需要超过 8 GB。
我们将主要重用一步超分辨率过程中的代码:
-
将图像大小加倍:
resized_raw_image = resize_img(file_name_768x768, 2.0)
display(resized_raw_image)
-
进一步的图像超分辨率代码可以将图像分辨率提高六倍(256x256 到 1,536x1,536),这可以显著提高图像的清晰度和细节:
sr_prompt = "8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo,"
prompt = """
a realistic photo of beautiful woman face
"""
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
img2image_6x = img2img_pipe(
image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.3,
num_inference_steps = 80,
guidance_scale = 7.5,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
img2image_6x
上述代码将生成一个比原始图像大六倍的图像,极大地提高了其质量。
超分辨率结果比较
现在,让我们检查六倍超分辨率图像,并将其与原始图像进行比较,以查看图像质量提高了多少。
图11.2 提供了原始图像和六倍超分辨率图像的并排比较:
图11.2:左 – 原始原始图像,右 – 六倍超分辨率图像
请查看电子书版本,以便轻松辨别更细微的改进。图11.3 清晰地展示了嘴巴区域的改进:
图11.3:左 – 原始原始图像中的嘴巴,右 – 六倍超分辨率图像中的嘴巴
图11.4 展示了眼睛的改进:
图11.4:上方 – 原始原始图像中的眼睛,下方 – 六倍超分辨率图像中的眼睛
稳定扩散增强了图像的几乎所有方面 – 从眉毛和睫毛到瞳孔 – 与原始原始图像相比,带来了显著的改进。
Img-to-Img 限制
deliberate-v2
稳定扩散模型是一个基于 SD v1.5 的检查点模型,它使用 512x512 的图像进行训练。因此,img-to-img 管道继承了该模型的所有约束。当尝试将图像从 1,024x1,024 超分辨率到更高的分辨率时,该模型可能不如处理低分辨率图像时高效。
然而,img-to-img 并非生成高质量图像的唯一解决方案。接下来,我们将探讨另一种可以以更高细节提升图像的技术。
ControlNet Tile 图像提升
Stable Diffusion ControlNet 是一种神经网络架构,通过引入额外的条件来增强扩散模型。这个模型背后的概念源于一篇题为 Adding Conditional Control to Text-to-Image Diffusion Models [3] 的论文,由 Zhang Lvmin 和 Maneesh Agrawala 在 2023 年撰写。有关 ControlNet 的更多详细信息,请参阅第 13 章。
ControlNet 与图像到图像的 Stable Diffusion 流水线相似,但具有显著更强的功能。
当使用 img2img 流水线时,我们输入初始图像以及条件文本,以生成与起始引导图像相似的画面。相比之下,ControlNet 使用一个或多个辅助 UNet 模型,这些模型与 Stable Diffusion 模型协同工作。这些 UNet 模型同时处理输入提示和图像,并在 UNet 上一个阶段中的每个步骤将结果合并。ControlNet 的全面探索可以在第 13 章中找到。
与图像到图像的流水线相比,ControlNet 产生了更优越的结果。在 ControlNet 模型中,ControlNet Tile 因其能够通过向原始图像引入大量细节信息来提升图像的能力而脱颖而出。
在后续的代码中,我们将使用最新的 ControlNet 版本,1.1。论文和模型的作者确认,他们将保持架构的一致性,直到 ControlNet V1.5。在阅读时,最新的 ControlNet 迭代可能超过 v1.1。有可能将 v1.1 的代码用于 ControlNet 的后续版本。
使用 ControlNet Tile 提升图像的步骤
接下来,让我们一步一步地使用 ControlNet Tile 来提升图像:
-
初始化 ControlNet Tile 模型。以下代码将启动一个 ControlNet v1.1 模型。请注意,当 ControlNet 从 v1.1 开始时,ControlNet 的子类型由
subfolder = 'control_v11f1e_sd15_tile'
指定:import torch
from diffusers import ControlNetModel
controlnet = ControlNetModel.from_pretrained(
'takuma104/control_v11',
subfolder = 'control_v11f1e_sd15_tile',
torch_dtype = torch.float16
)
我们不能简单地使用 ControlNet 本身来做任何事情;我们需要启动一个 Stable Diffusion V1.5 流水线,以便与 ControlNet 模型协同工作。
-
初始化一个 Stable Diffusion v1.5 模型流水线。使用 ControlNet 的主要优势在于其与任何从 Stable Diffusion 基础模型微调过的检查点模型的兼容性。我们将继续使用基于 Stable Diffusion V1.5 的模型,因为它具有卓越的质量和相对较低的 VRAM 要求。鉴于这些属性,预计 Stable Diffusion v1.5 将在相当长的一段时间内保持其相关性:
# load controlnet tile
from diffusers import StableDiffusionControlNetImg2ImgPipeline
# load checkpoint model with controlnet
pipeline = StableDiffusionControlNetImg2ImgPipeline. \
from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
controlnet = controlnet
)
在提供的代码中,我们提供了
controlnet
,这是我们按照 步骤1 初始化的,作为StableDiffusionControlNetImg2ImgPipeline
管道的参数。此外,代码与标准 Stable Diffusion 管道非常相似。 -
调整图像大小。这是我们在图像到图像管道中采取的相同步骤;我们需要将图像放大到目标大小:
image_name = "woman_face"
file_name_256x256 = f"input_images/{image_name}_256x256.png"
resized_raw_image = resize_img(file_name_256x256, 3.0)
resized_raw_image
之前的代码使用
LANCZOS
插值将图像放大三次:Image super-resolution using ControlNet Tile
# upscale
sr_prompt = "8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo,"
prompt = """
a realistic photo of beautiful woman face
"""
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
pipeline.to("cuda")
cn_tile_upscale_img = pipeline(
image = resized_raw_image,
control_image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.8,
guidance_scale = 7,
generator = torch.Generator("cuda"),
num_inference_steps = 50
).images[0]
cn_tile_upscale_img
我们重用了图像到图像上采样器中的正提示和负提示。区别在此概述:
-
我们将原始上采样图像分配给初始扩散图像,标记为
image = resized_raw_image
,以及 ControlNet 起始图像,标记为control_image = resized_raw_image
。 -
为了利用 ControlNet 对去噪的影响,从而增强生成过程,强度被配置为
0.8
。
-
注意,我们可以降低强度参数以尽可能保留原始图像。
ControlNet Tile 上采样结果
仅通过一轮三倍超分辨率,我们就可以通过引入大量复杂细节来显著提高我们的图像:
图11.5:左 – 原始原始图像,右 – ControlNet Tile 三倍上采样超分辨率
与图像到图像上采样器相比,ControlNet Tile 包含了更多的细节。在放大图像时,你可以观察到单个发丝的添加,从而整体提高了图像质量。
为了达到类似的效果,图像到图像方法需要多个步骤将图像放大六倍。相比之下,ControlNet Tile 通过一轮三倍上采样就实现了相同的效果。
此外,与图像到图像解决方案相比,ControlNet Tile 提供了相对较低的 VRAM 使用优势。
其他 ControlNet Tile 上采样样本
ControlNet Tile 超分辨率可以为各种照片和图像产生显著的结果。以下是一些通过仅使用几行代码生成、调整大小和上采样图像,捕捉到复杂细节的额外样本:
-
人脸:生成、调整大小和上采样此图像的代码如下:
# step 1\. generate an image
prompt = """
Raw, analog a portrait of an 43 y.o. man ,
beautiful photo with highly detailed face by greg rutkowski and magali villanueve
"""
neg_prompt = "NSFW, bad anatomy"
text2img_pipe.to("cuda")
raw_image = text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
generator = torch.Generator("cuda").manual_seed(3)
).images[0]
display(raw_image)
image_name = "man"
file_name_256x256 = f"input_images/{image_name}_256x256.png"
raw_image.save(file_name_256x256)
# step 2\. resize image
resized_raw_image = resize_img(file_name_256x256, 3.0)
display(resized_raw_image)
# step 3\. upscale image
sr_prompt = "8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo,"
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
pipeline.to("cuda")
cn_tile_upscale_img = pipeline(
image = resized_raw_image,
control_image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.8,
guidance_scale = 7,
generator = torch.Generator("cuda"),
num_inference_steps = 50,
# controlnet_conditioning_scale = 0.8
).images[0]
display(cn_tile_upscale_img)
结果在 图11.6 中展示:
图11.6:左 – 原始原始图像,右 – ControlNet Tile 三倍上采样超分辨率
-
老人:以下是生成、调整大小和上采样图像的代码:
# step 1\. generate an image
prompt = """
A realistic photo of an old man, standing in the garden, flower and green trees around, face view
"""
neg_prompt = "NSFW, bad anatomy"
text2img_pipe.to("cuda")
raw_image = text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
generator = torch.Generator("cuda").manual_seed(3)
).images[0]
display(raw_image)
image_name = "man"
file_name_256x256 = f"input_images/{image_name}_256x256.png"
raw_image.save(file_name_256x256)
# step 2\. resize image
resized_raw_image = resize_img(file_name_256x256, 4.0)
display(resized_raw_image)
# step 3\. upscale image
sr_prompt = "8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo,"
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
pipeline.to("cuda")
cn_tile_upscale_img = pipeline(
image = resized_raw_image,
control_image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.8,
guidance_scale = 7,
generator = torch.Generator("cuda"),
num_inference_steps = 50,
# controlnet_conditioning_scale = 0.8
).images[0]
display(cn_tile_upscale_img)
结果在 图11.7 中展示:
图 11.7:左 – 老人的原始图像,右 – ControlNet 瓦片四倍上采样超分辨率
-
皇家女性:以下是生成、调整大小和上采样图像的代码:
# step 1\. generate an image
prompt = """
upper body photo of royal female, elegant, pretty face, majestic dress,
sitting on a majestic chair, in a grand fantasy castle hall, shallow depth of field, cinematic lighting, Nikon D850,
film still, HDR, 8k
"""
neg_prompt = "NSFW, bad anatomy"
text2img_pipe.to("cuda")
raw_image = text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
generator = torch.Generator("cuda").manual_seed(7)
).images[0]
display(raw_image)
image_name = "man"
file_name_256x256 = f"input_images/{image_name}_256x256.png"
raw_image.save(file_name_256x256)
# step 2\. resize image
resized_raw_image = resize_img(file_name_256x256, 4.0)
display(resized_raw_image)
# step 3\. upscale image
sr_prompt = "8k, best quality, masterpiece, realistic, photo-realistic, ultra detailed, sharp focus, raw photo,"
prompt = f"{sr_prompt}{prompt}"
neg_prompt = "worst quality, low quality, lowres, bad anatomy"
pipeline.to("cuda")
cn_tile_upscale_img = pipeline(
image = resized_raw_image,
control_image = resized_raw_image,
prompt = prompt,
negative_prompt = neg_prompt,
strength = 0.8,
guidance_scale = 7,
generator = torch.Generator("cuda"),
num_inference_steps = 50,
# controlnet_conditioning_scale = 0.8
).images[0]
display(cn_tile_upscale_img)
结果展示在 图 11.8 中:
图 11.8:左 – 原始的皇家女性图像,右 – ControlNet 瓦片四倍上采样超分辨率
摘要
本章提供了当代图像上采样和超分辨率方法的总结,强调了它们的独特特性。本章的主要重点是两种利用稳定扩散能力的超分辨率技术:
-
利用稳定扩散图像到图像管道
-
实现ControlNet瓦片以在上采样图像的同时增强细节
此外,我们还展示了 ControlNet 瓦片超分辨率技术的更多示例。
如果你的目标是尽可能保留原始图像的上采样方面的各个方面,我们建议使用图像到图像管道。相反,如果你更喜欢一种由 AI 驱动的生成丰富细节的方法,ControlNet 瓦片是更合适的选择。
在 第 12 章 中,我们将开发一个计划提示解析器,以便我们能够更精确地控制图像生成。
参考文献
- Hugging Face – 超分辨率: https://huggingface.co/docs/diffusers/v0.13.0/en/api/pipelines/stable_diffusion/upscale
)
- Hugging Face – 带有扩散器的超快速控制网络: https://huggingface.co/blog/controlnet
)
- Lvmin Zhang, Maneesh Agrawala, 向文本到图像扩散模型添加条件控制: https://arxiv.org/abs/2302.05543
)
- Lvmin Zhang, ControlNet 原始实现代码: https://github.com/lllyasviel
)
- Lvmin Zhang, ControlNet 1.1 瓦片: https://github.com/lllyasviel/ControlNet-v1-1-nightly#controlnet-11-tile
)
)
)
)
- Lanczos 重采样: https://en.wikipedia.org/wiki/Lanczos_resampling
)
- ESRGAN: 增强型超分辨率生成对抗网络: https://arxiv.org/abs/1809.00219
)
- SwinIR:使用 Swin Transformer 进行图像恢复:https://arxiv.org/abs/2108.10257
)
- Python Pillow 包:https://github.com/python-pillow/Pillow
第十二章:定时提示解析
在第10章中,我们讨论了如何解锁77个令牌的提示限制以及启用提示权重的解决方案,这为本章铺平了道路。凭借第10章中的知识,我们可以利用自然语言和权重格式生成各种类型的图像。然而,Hugging Face Diffusers包的现成代码中存在一些固有的限制。
例如,我们不能编写一个提示来要求Stable Diffusion在前五步生成一只猫,然后在接下来的五步生成一只狗。同样,我们也不能编写一个提示来要求Stable Diffusion通过交替去噪两个概念来实现两个概念的混合。
在本章中,我们将探讨以下主题中的两种解决方案:
-
使用Compel包
-
构建自定义定时提示管道
技术要求
要开始本章中的代码,您需要安装运行Stable Diffusion所需的必要包。有关如何设置这些包的详细说明,请参阅第2章。
除了Stable Diffusion所需的包之外,您还需要为使用Compel包部分安装Compel
包,以及为构建自定义定时提示 管道部分安装lark
包。
我将提供在每个部分中安装和使用这些包的逐步说明。
使用Compel包
Compel [1] 是由Damian Stewart开发和维护的一个开源文本提示权重和混合库。它是使Diffusers中的提示混合变得最容易的方法之一。此包还具有对提示应用权重的功能,类似于我们在第10章中实现的解决方案,但具有不同的权重语法。在本章中,我将介绍可以帮助我们编写生成具有两个或更多概念混合的图像的提示的混合功能。
假设我们想要创建一张半猫半狗的照片。我们如何使用提示来完成?让我们假设我们只是给Stable Diffusion以下提示:
A photo with half cat and half dog
这里是Python代码行(不使用Compel):
import torch
from diffusers import StableDiffusionPipeline
pipeline = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
image = pipeline(
prompt = "A photo with half cat and half dog",
generator = torch.Generator("cuda:0").manual_seed(3)
).images[0]
image
您将看到图12.1所示的结果:
图12.1:半猫半狗照片提示的结果
单词half
应该应用于照片本身,而不是图像。在这种情况下,我们可以使用Compel来帮助生成一个混合猫和狗的文本嵌入。
在导入Compel
包之前,我们需要安装该包:
pip install compel
注意,Compel
包之所以能与 Diffusers 一起使用,是因为该包使用 tokenizer
(类型:transformers.models.clip.tokenization_clip.CLIPTokenizer
)和 text_encoder
(类型:transformers.models.clip.modeling_clip.CLIPTextModel
)从 Stable Diffusion 模型文件生成文本嵌入。我们也应该在初始化 Compel
对象时意识到这一点:
from comp
compel = Compel(
tokenizer = pipeline.tokenizer,
text_encoder = pipeline.text_encoder
)
pipeline
(类型:StableDiffusionPipeline
)是我们刚刚创建的 Stable Diffusion 管道。接下来,使用以下格式创建一个混合提示:
prompt = '("A photo of cat", "A photo of dog").blend(0.5, 0.5)'
prompt_embeds = compel(prompt)
然后,通过 prompt_embeds
参数将文本嵌入输入到 Stable Diffusion 管道中:
image = pipeline(
prompt_embeds = prompt_embeds,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
image
我们将看到一只看起来像猫也像狗的宠物,如图 图 12.2 所示:
图 12.2:使用 Compel 混合的图片 – 一半猫,一半狗
我们可以改变混合的命题数量,以获得更多 猫
或更多 狗
。让我们将提示更改为以下内容:
prompt = '("A photo of cat", "A photo of dog").blend(0.7, 0.3)'
我们将得到一张稍微更像猫的图片,如图 图 12.3 所示:
图 12.3:使用 Compel 混合猫和狗的图片 – 70% 猫,30% 狗
Compel 可以做的不仅仅是提示混合;它还可以提供 加权
和 和
连接提示。您可以在其语法功能 [2] 文档中探索更多使用示例和功能。
虽然使用 Compel 混合提示很容易,就像我们在前面的例子中看到的那样,但以下这种混合提示在日常使用中既奇怪又难以理解:
prompt = '("A photo of cat", "A photo of dog").blend(0.7, 0.3)'
在我对 Compel 仓库中的示例代码进行初步审查时,我对以下行感到好奇:("A photo of cat", "A photo of dog").blend(0.7, 0.3)
。这行代码引发了一些问题,例如 blend()
函数是如何调用的?然而,很明显,blend()
是提示字符串的一部分,而不是可以在 Python 代码中调用的函数。
相比之下,Stable Diffusion WebUI [3] 的提示混合或计划功能相对更易于用户使用。语法允许我们使用如下提示语法达到相同的混合效果:
[A photo of cat:A photo of dog:0.5]
在 Stable Diffusion WebUI 中的此计划提示将在步骤的前 50% 生成一张猫的图片,在步骤的最后 50% 生成一张狗的图片。
或者,您可以使用 |
运算符来交替提示:
[A photo of cat|A photo of dog]
上述计划提示将在渲染猫和狗的图片之间交替。换句话说,它将在一个步骤中渲染一张猫的图片,在下一个步骤中渲染一张狗的图片,并继续这种模式,直到整个渲染过程的结束。
这两个调度功能也可以通过 Diffusers 实现。在下一节中,我们将探讨如何为 Diffusers 实现这两个高级提示调度功能。
构建自定义计划提示管道
正如我们在 第五章 中讨论的那样,生成过程利用输入提示嵌入在每个步骤中去噪图像。默认情况下,每个去噪步骤都使用完全相同的嵌入。然而,为了获得更精确的生成控制,我们可以修改管道代码以为每个去噪步骤提供唯一的嵌入。
以以下提示为例:
[A photo of cat:A photo of dog:0.5]
在总共 10 个去噪步骤中,我们希望管道能在前 5 步去除噪声以揭示 A photo of cat
,在接下来的 5 步去除噪声以揭示 A photo of dog
。为了实现这一点,我们需要实现以下组件:
-
一个能够从提示中提取调度号的提示解析器
-
一种将提示嵌入并创建与步骤数量相匹配的提示嵌入列表的方法
-
一个从 Diffusers 管道派生的新
pipeline
类,使我们能够将我们的新功能集成到管道中,同时保留 Diffusers 管道的所有现有功能
接下来,让我们实现格式化的提示解析器。
一个计划好的提示解析器
开源 Stable Diffusion WebUI 项目的源代码显示,我们可以使用 lark
[4] – 一个 Python 的解析工具包。我们还将使用 lark
包来解析我们自己的提示解析器的计划提示。
要安装 lark
,请运行以下命令:
pip install -U lark
与 Stable Diffusion WebUI 兼容的提示定义在以下代码中:
import lark
schedule_parser = lark.Lark(r"""
!start: (prompt | /[][():]/+)*
prompt: (emphasized | scheduled | alternate | plain | WHITESPACE)*
!emphasized: "(" prompt ")"
| "(" prompt ":" prompt ")"
| "[" prompt "]"
scheduled: "[" [prompt ":"] prompt ":" [WHITESPACE] NUMBER "]"
alternate: "[" prompt ("|" prompt)+ "]"
WHITESPACE: /\s+/
plain: /([^\\\[\]():|]|\\.)+/
%import common.SIGNED_NUMBER -> NUMBER
""")
如果你决定深入语法沼泽,完全理解定义中的每一行,那么 lark
文档 [5] 是你该去的地方。
接下来,我们将使用来自 SD WebUI 代码库的 Python 函数。此函数使用 Lark schedule_parser
语法定义来解析输入提示:
def get_learned_conditioning_prompt_schedules(prompts, steps):
def collect_steps(steps, tree):
l = [steps]
class CollectSteps(lark.Visitor):
def scheduled(self, tree):
tree.children[-1] = float(tree.children[-1])
if tree.children[-1] < 1:
tree.children[-1] *= steps
tree.children[-1] = min(steps, int(tree.children[-1]))
l.append(tree.children[-1])
def alternate(self, tree):
l.extend(range(1, steps+1))
CollectSteps().visit(tree)
return sorted(set(l))
def at_step(step, tree):
class AtStep(lark.Transformer):
def scheduled(self, args):
before, after, _, when = args
yield before or () if step <= when else after
def alternate(self, args):
yield next(args[(step - 1)%len(args)])
def start(self, args):
def flatten(x):
if type(x) == str:
yield x
else:
for gen in x:
yield from flatten(gen)
return ''.join(flatten(args))
def plain(self, args):
yield args[0].value
def __default__(self, data, children, meta):
for child in children:
yield child
return AtStep().transform(tree)
def get_schedule(prompt):
try:
tree = schedule_parser.parse(prompt)
except lark.exceptions.LarkError as e:
if 0:
import traceback
traceback.print_exc()
return [[steps, prompt]]
return [[t, at_step(t, tree)] for t in collect_steps(steps,
tree)]
promptdict = {prompt: get_schedule(prompt) for prompt in
set(prompts)}
return [promptdict[prompt] for prompt in prompts]
将总去噪步骤数设置为 10,并为该函数提供一个更短的名字,g
:
steps = 10
g = lambda p: get_learned_conditioning_prompt_schedules([p], steps)[0]
现在,让我们向函数投掷一些提示以查看解析结果:
-
测试 #1:
cat
:g("cat")
之前的代码将解析
cat
输入文本为以下字符串:[[10, 'cat']]
结果表明所有 10 个步骤都将使用
cat
生成图像。 -
测试 #2:
[cat:dog:0.5]
:将提示更改为
[cat:dog:0.5]
:g('[cat:dog:0.5]')
函数将生成以下结果:
[[5, 'cat'], [10, 'dog']]
结果意味着前 5 步使用
cat
,最后 5 步使用dog
。 -
测试 #3:
[cat|dog]
:该函数还支持替代调度。将提示更改为
[cat | dog]
,在两个名称中间有一个“或”|
操作符:g('[cat|dog]')
提示解析器将生成以下结果:
[[1, 'cat'],
[2, 'dog'],
[3, 'cat'],
[4, 'dog'],
[5, 'cat'],
[6, 'dog'],
[7, 'cat'],
[8, 'dog'],
[9, 'cat'],
[10, 'dog']]
换句话说,它为每个去噪步骤交替提示。
到目前为止,它在解析方面工作得很好。然而,在将其馈送到管道之前,还需要做一些额外的工作。
填补缺失的步骤
在 测试 #2 中,生成的结果只包含两个元素。我们需要扩展结果列表以涵盖每个步骤:
def parse_scheduled_prompts(text, steps=10):
text = text.strip()
parse_result = None
try:
parse_result = get_learned_conditioning_prompt_schedules(
[text],
steps = steps
)[0]
except Exception as e:
print(e)
if len(parse_result) == 1:
return parse_result
prompts_list = []
for i in range(steps):
current_prompt_step, current_prompt_content = \
parse_result[0][0],parse_result[0][1]
step = i + 1
if step < current_prompt_step:
prompts_list.append(current_prompt_content)
continue
if step == current_prompt_step:
prompts_list.append(current_prompt_content)
parse_result.pop(0)
return prompts_list
这个 Python 函数 parse_scheduled_prompts
接受两个参数:text
和 steps
(默认值为 10)。该函数处理给定的文本,根据学习到的条件调度生成提示列表。
下面是对函数功能的逐步解释:
-
使用
try-except
块调用get_learned_conditioning_prompt_schedules
函数,传入处理后的文本和指定的步数。结果存储在parse_result
中。如果有异常——比如语法错误,它将被捕获并打印。 -
如果
parse_result
的长度为 1,则将parse_result
作为最终输出返回。 -
循环遍历步数的范围并执行以下操作:
-
从
parse_result
获取当前的提示步骤和内容。 -
将循环计数器
i
增加1
并将其存储在变量 step 中。 -
如果
step
小于当前的提示步骤,则将当前提示内容添加到prompts_list
并继续到下一个迭代。 -
如果
step
等于当前的提示步骤,则将当前提示内容添加到prompts_list
并从parse_result
中移除第一个元素。
-
-
将
prompts_list
作为最终输出返回。
函数本质上基于学习到的条件调度生成提示列表,每个提示根据指定的步数添加到列表中。
让我们调用这个函数来测试它:
prompt_list = parse_scheduled_prompts("[cat:dog:0.5]")
prompt_list
我们将得到如下所示的提示列表:
['cat',
'cat',
'cat',
'cat',
'cat',
'dog',
'dog',
'dog',
'dog',
'dog']
五个针对 cat
的提示和五个针对 dog
的提示——每个去噪步骤将使用其中一个提示。
支持计划提示的 Stable Diffusion 管道
到目前为止,所有提示都仍然是纯文本形式。我们需要使用自定义嵌入代码将无限和加权提示编码为嵌入,或者我们可以简单地使用 Diffusers 的默认编码器生成嵌入,但有一个 77 个标记的限制。
为了使逻辑更容易和更简洁地跟踪,我们将在此部分使用默认文本编码器。一旦我们弄清楚它是如何工作的,就很容易将其与我们在 第 10 章 中构建的更强大的编码器交换。
由于我们将对原始 Diffusers Stable Diffusion 管道进行小手术以支持此嵌入列表,操作包括创建一个新的继承自 Diffusers 管道的管道类。我们可以通过以下代码直接重用初始化管道中的分词器和文本编码器:
...
prompt_embeds = self._encode_prompt(
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt,
negative_prompt_embeds=negative_prompt_embeds,
)
...
我将在下一部分进一步解释前面的代码。我们将在 scheduler_call
函数中实现整个逻辑(类似于 StableDiffusionPipeline
的 __call__
函数):
from typing import List, Callable, Dict, Any
from torch import Generator,FloatTensor
from diffusers.pipelines.stable_diffusion import (
StableDiffusionPipelineOutput)
from diffusers import (
StableDiffusionPipeline,EulerDiscreteScheduler)
class StableDiffusionPipeline_EXT(StableDiffusionPipeline):
@torch.no_grad()
def scheduler_call(
self,
prompt: str | List[str] = None,
height: int | None = 512,
width: int | None = 512,
num_inference_steps: int = 50,
guidance_scale: float = 7.5,
negative_prompt: str | List[str] | None = None,
num_images_per_prompt: int | None = 1,
eta: float = 0,
generator: Generator | List[Generator] | None = None,
latents: FloatTensor | None = None,
prompt_embeds: FloatTensor | None = None,
negative_prompt_embeds: FloatTensor | None = None,
output_type: str | None = "pil",
callback: Callable[[int, int, FloatTensor], None] | None = None,
callback_steps: int = 1,
cross_attention_kwargs: Dict[str, Any] | None = None,
):
...
# 6\. Prepare extra step kwargs. TODO: Logic should ideally
# just be moved out of the pipeline
extra_step_kwargs = self.prepare_extra_step_kwargs(
generator, eta)
# 7\. Denoising loop
num_warmup_steps = len(timesteps) - num_inference_steps * \
self.scheduler.order
with self.progress_bar(total=num_inference_steps) as \
progress_bar:
for i, t in enumerate(timesteps):
# AZ code to enable Prompt Scheduling,
# will only function when
# when there is a prompt_embeds_l provided.
prompt_embeds_l_len = len(embedding_list)
if prompt_embeds_l_len > 0:
# ensure no None prompt will be used
pe_index = (i)%prompt_embeds_l_len
prompt_embeds = embedding_list[pe_index]
# expand the latents if we are doing classifier
#free guidance
latent_model_input = torch.cat([latents] * 2) \
if do_classifier_free_guidance else latents
latent_model_input = self.scheduler. \
scale_model_input(latent_model_input, t)
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
cross_attention_kwargs=cross_attention_kwargs,
).sample
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = \
noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * \
(noise_pred_text - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents,
**extra_step_kwargs).prev_sample
# call the callback, if provided
if i == len(timesteps) - 1 or ((i + 1) > \
num_warmup_steps and (i + 1) % \
self.scheduler.order == 0):
progress_bar.update()
if callback is not None and i % callback_steps== 0:
callback(i, t, latents)
if output_type == "latent":
image = latents
elif output_type == "pil":
# 8\. Post-processing
image = self.decode_latents(latents)
image = self.numpy_to_pil(image)
else:
# 8\. Post-processing
image = self.decode_latents(latents)
if hasattr(self, "final_offload_hook") and \
self.final_offload_hook is not None:
self.final_offload_hook.offload()
return StableDiffusionPipelineOutput(images=image)
这个 Python 函数 scheduler_call
是 StableDiffusionPipeline_EXT
类的一个方法,该类是 StableDiffusionPipeline
的子类。
实现整个逻辑的步骤如下:
-
将默认调度器设置为
EulerDiscreteScheduler
以获得更好的生成结果:if self.scheduler._class_name == "PNDMScheduler":
self.scheduler = EulerDiscreteScheduler.from_config(
self.scheduler.config
)
-
准备
device
和do_classifier_free_guidance
参数:device = self._execution_device
do_classifier_free_guidance = guidance_scale > 1.0
-
调用
parse_scheduled_prompts
函数以获取prompt_list
提示列表。这是我们在此章节前一部分构建的函数:prompt_list = parse_scheduled_prompts(prompt)
-
如果没有找到计划中的提示,则使用正常的单提示逻辑:
embedding_list = []
if len(prompt_list) == 1:
prompt_embeds = self._encode_prompt(
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt,
negative_prompt_embeds=negative_prompt_embeds,
)
else:
for prompt in prompt_list:
prompt_embeds = self._encode_prompt(
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt,
negative_prompt_embeds=negative_prompt_embeds,
)
embedding_list.append(prompt_embeds)
在步骤4中,函数处理输入提示以生成提示嵌入。输入提示可以是一个字符串或字符串列表。函数首先将输入提示(s)解析为名为
prompt_list
的列表。如果列表中只有一个提示,则函数直接使用_encode_prompt
方法编码提示并将其结果存储在prompt_embeds
中。如果有多个提示,则函数遍历prompt_list
并使用_encode_prompt
方法分别编码每个提示。生成的嵌入存储在embedding_list
中。 -
准备扩散过程的timesteps:
self.scheduler.set_timesteps(num_inference_steps, device=device)
timesteps = self.scheduler.timesteps
-
准备潜在变量以初始化
latents
张量(这是一个PyTorch张量):num_channels_latents = self.unet.in_channels
batch_size = 1
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)
-
实现去噪循环:
num_warmup_steps = len(timesteps) - num_inference_steps * \
self.scheduler.order
with self.progress_bar(total=num_inference_steps) as \
progress_bar:
for i, t in enumerate(timesteps):
# custom code to enable Prompt Scheduling,
# will only function when
# when there is a prompt_embeds_l provided.
prompt_embeds_l_len = len(embedding_list)
if prompt_embeds_l_len > 0:
# ensure no None prompt will be used
pe_index = (i)%prompt_embeds_l_len
prompt_embeds = embedding_list[pe_index]
# expand the latents if we are doing
# classifier free guidance
latent_model_input = torch.cat([latents] * 2)
if do_classifier_free_guidance else latents
latent_model_input =
self.scheduler.scale_model_input(
latent_model_input, t)
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
cross_attention_kwargs=cross_attention_kwargs,
).sample
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = \
noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * \
(noise_pred_text - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t,
latents).prev_sample
# call the callback, if provided
if i == len(timesteps) - 1 or ((i + 1) >
num_warmup_steps and (i + 1) %
self.scheduler.order == 0):
progress_bar.update()
if callback is not None and i % callback_steps == 0:
callback(i, t, latents)
在步骤7中,去噪循环遍历扩散过程的timesteps。如果启用了提示调度(即
embedding_list
中有多个提示嵌入),则函数选择当前timestep的适当提示嵌入。embedding_list
的长度存储在prompt_embeds_l_len
中。如果prompt_embeds_l_len
大于0
,则表示启用了提示调度。函数使用模运算符(%
)计算当前timestepi
的pe_index
索引。这确保了索引在embedding_list
的长度上循环,并选择当前timestep的适当提示嵌入。选定的提示嵌入随后分配给prompt_embeds
。 -
最后一步是去噪后处理:
image = self.decode_latents(latents)
image = self.numpy_to_pil(image)
return StableDiffusionPipelineOutput(images=image,
nsfw_content_detected=None)
在最后一步,我们通过调用
decode_latents()
函数将图像数据从潜在空间转换为像素空间。在这里使用StableDiffusionPipelineOutput
类是为了在从管道返回输出时保持一致的结构。我们在这里使用它来使我们的管道与Diffusers管道兼容。你还可以在与此章节相关的代码文件中找到完整的代码。如果你还在这里,恭喜你!让我们执行它以见证结果:
pipeline = StableDiffusionPipeline_EXT.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
prompt = "high quality, 4k, details, A realistic photo of cute \ [cat:dog:0.6]"
neg_prompt = "paint, oil paint, animation, blur, low quality, \ bad glasses"
image = pipeline.scheduler_call(
prompt = prompt,
negative_prompt = neg_prompt,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
image
我们应该看到图12.4中所示的照片:
图12.4:使用自定义计划提示管道混合了60%的猫和40%的狗的照片
这里还有一个例子,使用替代提示[猫|狗]
:
prompt = "high quality, 4k, details, A realistic photo of white \
[cat|dog]"
neg_prompt = "paint, oil paint, animation, blur, low quality, bad \
glasses"
image = pipeline.scheduler_call(
prompt = prompt,
negative_prompt = neg_prompt,
generator = torch.Generator("cuda").manual_seed(3)
).images[0]
image
我们的替代提示给出了类似于图12.5的照片:
图12.5:使用我们自定义计划提示管道的替代提示调度混合猫和狗的照片
如果您看到了如图12.4和图12.5所示的半猫半狗的图像生成,那么您已经成功构建了您自定义的提示调度器。
摘要
在本章中,我们介绍了两种进行计划提示图像生成的解决方案。第一种解决方案,Compel
包,是使用起来最简单的一个。只需安装该包,您就可以使用其提示混合功能在一个提示中混合两个或更多概念。
第二种解决方案是一个定制的管道,它首先解析提示字符串,并为每个去噪步骤准备一个提示列表。自定义管道遍历提示列表以创建一个嵌入列表。最后,scheduler_call
函数使用嵌入列表中的提示嵌入来生成具有精确控制的图像。
如果您成功实现了自定义的计划管道,您就可以以更精确的方式控制生成。说到控制,在第13章中,我们将探讨另一种控制图像生成的方法——ControlNet。
参考文献
-
Compel语法特性:https://github.com/damian0815/compel/blob/main/doc/syntax.md
-
稳定扩散WebUI提示编辑:https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#prompt-editing
)
-
Lark - Python的解析工具包:https://github.com/lark-parser/lark
第3部分 - 高级主题
在第1部分和第2部分中,我们为稳定扩散奠定了坚实的基础,涵盖了其基础知识、定制选项和优化技术。现在,是时候探索更高级的领域了,我们将探讨尖端应用、创新模型和专家级策略来生成卓越的视觉内容。
本部分中的章节将带您踏上一段激动人心的旅程,探索稳定扩散的最新发展。您将学习如何使用ControlNet以前所未有的控制力生成图像,使用AnimateDiff制作吸引人的视频,以及使用BLIP-2和LLaVA等强大的视觉语言模型从图像中提取有洞察力的描述。此外,您还将了解稳定扩散XL,这是稳定扩散模型的一个更新、更强大的迭代版本。
为了锦上添花,我们将深入探讨为稳定的扩散制作优化提示的艺术,包括编写有效提示的技术以及利用大型语言模型来自动化流程。在本部分结束时,你将具备处理复杂项目、拓展稳定的扩散边界和开启新的创意可能性的专业知识。准备好释放你的全部潜力,创造出令人叹为观止的结果!
本部分包含以下章节:
第十三章:使用 ControlNet 生成图像
Stable Diffusion 的 ControlNet 是一个神经网络插件,允许你通过添加额外条件来控制扩散模型。它首次在 2023 年由 Zhang Lvmin 和 Maneesh Agrawala 发表的论文《Adding Conditional Control to Text-to-Image Diffusion Models [1]》中介绍。
本章将涵盖以下主题:
-
什么是 ControlNet 以及它与其他方法有何不同?
-
ControlNet 的使用方法
-
在一个管道中使用多个 ControlNet
-
ControlNet 的工作原理
-
更多 ControlNet 使用方法
到本章结束时,你将了解 ControlNet 的工作原理以及如何使用 Stable Diffusion V1.5 和 Stable Diffusion XL ControlNet 模型。
什么是 ControlNet 以及它与其他方法有何不同?
在“控制”方面,你可能还记得文本嵌入、LoRA 和图像到图像的扩散管道。但是什么让 ControlNet 不同且有用?
与其他解决方案不同,ControlNet 是一个直接在 UNet 扩散过程中工作的模型。我们在 表 13.1 中比较了这些解决方案:
控制方法 | 工作阶段 | 使用场景 |
---|---|---|
文本嵌入 | 文本编码器 | 添加新的风格、新的概念或新的面孔 |
LoRA | 将 LoRA 权重合并到 UNet 模型(以及可选的 CLIP 文本编码器) | 添加一组风格、概念并生成内容 |
图像到图像 | 提供初始潜在图像 | 修复图像,或向图像添加风格和概念 |
ControlNet | ControlNet 参与者与检查点模型 UNet 一起去噪 | 控制形状、姿势、内容细节 |
表 13.1:文本嵌入、LoRA、图像到图像和 ControlNet 的比较
在许多方面,ControlNet 与我们讨论过的图像到图像管道类似,如 第 11 章。图像到图像和 ControlNet 都可以用来增强图像。
然而,ControlNet 可以以更精确的方式“控制”图像。想象一下,你想要生成一个使用另一张图像中的特定姿势或完美对齐场景中的物体到特定参考点的图像。这种精度是使用现成的 Stable Diffusion 模型无法实现的。ControlNet 是帮助你实现这些目标的工具。
此外,ControlNet 模型与其他所有开源检查点模型兼容,而一些其他解决方案仅与作者提供的单个基础模型兼容。创建 ControlNet 的团队不仅开源了模型,还开源了训练新模型的代码。换句话说,我们可以训练一个 ControlNet 模型,使其与其他任何模型一起工作。这正是原始论文 [1] 中所说的:
由于 Stable Diffusion 是一个典型的 UNet 结构,这种 ControlNet 架构可能适用于其他模型。
注意,ControlNet 模型只能与使用相同基础模型的模型一起工作。一个 Stable Diffusion (SD) v1.5 ControlNet 模型可以与所有其他 SD v1.5 模型一起工作。对于 Stable Diffusion XL (SDXL) 模型,我们需要一个经过 SDXL 训练的 ControlNet 模型。这是因为 SDXL 模型使用不同的架构,比 SD v1.5 的 UNet 更大。没有额外的工作,一个 ControlNet 模型只能与这种类型的模型一起工作。
我使用“没有额外工作”是因为在 2023 年 12 月,为了弥合这一差距,Lingmin Ran 等人发表了一篇名为 X-Adapter: Adding Universal Compatibility of Plugins for Upgraded Diffusion Model 的论文 [8]。这篇论文详细介绍了适配器,使我们能够在新 SDXL 模型中使用 SD V1.5 LoRA 和 ControlNet。
接下来,让我们开始使用 SD 模型结合 ControlNet。
使用 ControlNet 的方法
在深入探讨 ControlNet 的后端之前,在本节中,我们将开始使用 ControlNet 来帮助控制图像生成。
在以下示例中,我们将首先使用 SD 生成一个图像,获取对象的 Canny 形状,然后使用 Canny 形状在 ControlNet 的帮助下生成一个新的图像。
注意
Canny 图像指的是经过 Canny 边缘检测的图像,这是一种流行的边缘检测算法。它由 John F. Canny 在 1986 年开发。[7]
让我们使用以下代码使用 SD 生成一个图像:
-
使用 SD 生成样本图像:
import torch
from diffusers import StableDiffusionPipeline
# load model
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16
).to("cuda:0")
# generate sample image
prompt = """
high resolution photo,best quality, masterpiece, 8k
A cute cat stand on the tree branch, depth of field, detailed body
"""
neg_prompt = """
paintings,ketches, worst quality, low quality, normal quality, lowres,
monochrome, grayscale
"""
image = text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
generator = torch.Generator("cuda").manual_seed(7)
).images[0]
image
我们将看到一只猫的图像,如图 13.1* 所示:
图 13.1:由 SD 生成的猫
-
然后,我们将得到样本图像的 Canny 形状。
我们需要另一个包,
controlnet_aux
,从图像创建 Canny 图像。只需执行以下两行pip
命令来安装controlnet_aux
:pip install opencv-contrib-python
pip install controlnet_aux
我们可以用三行代码生成图像的 Canny 边缘形状:
from controlnet_aux import CannyDetector
canny = CannyDetector()
image_canny = canny(image, 30, 100)
以下是代码的分解:
-
from controlnet_aux import CannyDetector
:这一行从controlnet_aux
模块导入CannyDetector
类。有许多其他检测器。 -
image_canny = canny(image, 30, 100)
:这一行调用CannyDetector
类(实现为一个可调用对象)的__call__
方法,并以下列参数:-
image
:这是将要应用 Canny 边缘检测算法的输入图像。 -
30
:这是边缘的下限阈值值。任何强度梯度低于此值的边缘将被丢弃。 -
100
:这是边缘的上限阈值值。任何强度梯度高于此值的边缘将被视为强边缘。
-
上述代码将生成如图 13.2* 所示的 Canny 图像:
-
图 13.2:猫的 Canny 图像
-
现在,我们将使用 ControlNet 模型根据这个 Canny 图像生成一个新的图像。首先,让我们加载 ControlNet 模型:
from diffusers import ControlNetModel
canny_controlnet = ControlNetModel.from_pretrained(
'takuma104/control_v11',
subfolder='control_v11p_sd15_canny',
torch_dtype=torch.float16
)
首次运行时,代码将自动从 Hugging Face 下载 ControlNet 模型。如果您存储中有 ControlNet
safetensors
模型并想使用自己的模型,您首先需要将文件转换为 diffuser 格式。您可以在第 6 章中找到转换代码。然后,将takuma104/control_v11
替换为 ControlNet 模型的路径。 -
初始化一个 ControlNet 管道:
from diffusers import StableDiffusionControlNetImg2ImgPipeline
cn_pipe = \
StableDiffusionControlNetImg2ImgPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
controlnet = canny_controlnet
)
注意,您可以自由地将
stablediffusionapi/deliberate-v2
与社区中的任何其他 SD v1.5 模型交换。 -
使用 ControlNet 管道生成新的图像。在以下示例中,我们将用狗替换猫:
prompt = """
high resolution photo,best quality, masterpiece, 8k
A cute dog stand on the tree branch, depth of field, detailed body
"""
neg_prompt = """
paintings,ketches, worst quality, low quality, normal quality, lowres,
monochrome, grayscale
"""
image_from_canny = single_cn_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
image = canny_image,
generator = torch.Generator("cuda").manual_seed(2),
num_inference_steps = 30,
guidance_scale = 6.0
).images[0]
image_from_canny
这些代码行将生成一个遵循 Canny 边缘的新图像,但现在小猫变成了一只狗,如图 图 13.3 所示:
图 13.3:使用猫的 Canny 图像和 ControlNet 生成的狗
猫的身体结构和形状得到了保留。您可以随意更改提示和设置,以探索模型的惊人能力。需要注意的是,如果您不向 ControlNet 管道提供提示,管道仍然会输出有意义的图像,可能是另一种风格的猫,这意味着 ControlNet 模型学会了某种 Canny 边缘的潜在含义。
在此示例中,我们只使用了一个 ControlNet 模型,但我们也可以向一个管道提供多个 ControlNet 模型。
在一个管道中使用多个 ControlNets
在本节中,我们将初始化另一个 ControlNet,NormalBAE,然后将 Canny 和 NormalBAE ControlNet 模型一起输入以形成一个管道。
让我们生成一个作为额外控制图像的正常 BAE。Normal BAE 是一个使用 Bae 等人提出的正常不确定性方法 [4] 估计正常图的模型:
from controlnet_aux import NormalBaeDetector
normal_bae = \
NormalBaeDetector.from_pretrained("lllyasviel/Annotators")
image_canny = normal_bae(image)
image_canny
此代码将生成原始图像的正常 BAE 映射,如图 图 13.4 所示:
图 13.4:生成的猫的正常 BAE 图像
现在,让我们为单个管道初始化两个 ControlNet 模型:一个 Canny ControlNet 模型,另一个 NormalBae ControlNet 模型:
from diffusers import ControlNetModel
canny_controlnet = ControlNetModel.from_pretrained(
'takuma104/control_v11',
subfolder='control_v11p_sd15_canny',
torch_dtype=torch.float16
)
bae_controlnet = ControlNetModel.from_pretrained(
'takuma104/control_v11',
subfolder='control_v11p_sd15_normalbae',
torch_dtype=torch.float16
)
controlnets = [canny_controlnet, bae_controlnet]
从代码中,我们可以轻松地看出所有 ControlNet 模型都共享相同的架构。要加载不同的 ControlNet 模型,我们只需更改模型名称。此外,请注意,两个 ControlNet 模型位于 Python controlnets
列表
中。我们可以直接将这些 ControlNet 模型提供给管道,如下所示:
from diffusers import StableDiffusionControlNetPipeline
two_cn_pipe = StableDiffusionControlNetPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
controlnet = controlnets
).to("cuda")
在推理阶段,使用一个额外的参数,controlnet_conditioning_scale
,来控制每个 ControlNet 的影响范围:
prompt = """
high resolution photo,best quality, masterpiece, 8k
A cute dog on the tree branch, depth of field, detailed body,
"""
neg_prompt = """
paintings,ketches, worst quality, low quality, normal quality, lowres,
monochrome, grayscale
"""
image_from_2cn = two_cn_pipe(
prompt = prompt,
image = [canny_image,bae_image],
controlnet_conditioning_scale = [0.5,0.5],
generator = torch.Generator("cuda").manual_seed(2),
num_inference_steps = 30,
guidance_scale = 5.5
).images[0]
image_from_2cn
此代码将给我们另一张图像,如图 图 13.5 所示:
图 13.5:由 Canny ControlNet 和正常 BAE ControlNet 生成的狗
在 controlnet_conditioning_scale = [0.5,0.5]
中,我为每个 ControlNet 模型赋予一个 0.5
的缩放值。这两个缩放值加起来为 1.0
。我们应该赋予的总权重不超过 2
。过高的值会导致不期望的图像。例如,如果你给每个 ControlNet 模型赋予 1.2
和 1.3
的权重,如 controlnet_conditioning_scale = [1.2,1.3]
,你可能会得到一个不期望的图像。
如果我们成功使用 ControlNet 模型生成图像,我们就共同见证了 ControlNet 的力量。在下一节中,我们将讨论 ControlNet 的工作原理。
ControlNet 的工作原理
在本节中,我们将深入探讨 ControlNet 的结构,并了解 ControlNet 内部是如何工作的。
ControlNet 通过向神经网络块注入额外的条件来工作。如图 图 13**.6 所示,可训练的副本是添加额外指导到原始 SD UNet 块的 ControlNet 块:
图 13.6:添加 ControlNet 组件
在训练阶段,我们取目标层块的一个副本作为 ControlNet 块。在 图 13**.6 中,它被标记为 可训练的副本。与所有参数使用高斯分布进行典型神经网络初始化不同,ControlNet 使用来自稳定扩散基础模型的预训练权重。这些基础模型参数中的大多数都是冻结的(有选项在以后解冻它们),只有额外的 ControlNet 组件是从头开始训练的。
在训练和推理过程中,输入 x 通常是一个三维向量,x ∈ ℝ h×w×c,其中 h、w、c 分别是高度、宽度和通道数。c 是一个条件向量,我们将将其传递到 SD UNet 和 ControlNet 模型网络中。
零卷积在这个过程中起着关键作用。零卷积是权重和偏差初始化为零的 1D 卷积。零卷积的优势在于,即使没有进行单个训练步骤,从 ControlNet 注入的值也不会对图像生成产生影响。这确保了辅助网络在任何阶段都不会对图像生成产生负面影响。
你可能会想:如果卷积层的权重为零,梯度不也会为零吗?这难道不会使网络无法学习吗?然而,正如论文的作者解释[5]的那样,实际情况更为复杂。
让我们考虑一个简单的例子:
y = wx + b
然后,我们还有以下情况:
∂ y / ∂ w = x, ∂ y / ∂ x = w, ∂ y / ∂ b = 1
如果 w = 0 且 x ≠ 0,那么我们就有以下情况:
∂ y / ∂ w ≠ 0, ∂ y / ∂ x = 0, ∂ y / ∂ b ≠ 0
这意味着只要 x ≠ 0,一次梯度下降迭代就会使 w 非零。然后,我们有:
∂ y / ∂ x ≠ 0
因此,零卷积将逐渐变成具有非零权重的普通卷积层。多么天才的设计啊!
SD UNet仅在编码器块和中间块与ControlNet连接。可训练的蓝色块和白色零卷积层被添加来构建ControlNet。它简单而有效。
在原始论文——张吕民等人撰写的《向文本到图像扩散模型添加条件控制》[1]中——其作者还提供了一种消融研究,并讨论了许多不同的情况,例如将零卷积层替换为传统卷积层,并比较了差异。这是一篇优秀的论文,值得一读。
进一步使用
在本节中,我们将介绍更多关于ControlNet的使用,涵盖SD V1.5和SDXL。
更多带有SD的ControlNets
ControlNet-v1-1-nightly存储库[3]的作者列出了目前可用的所有V1.1 ControlNet模型。在我撰写这一章的时候,列表如下:
control_v11p_sd15_canny
control_v11p_sd15_mlsd
control_v11f1p_sd15_depth
control_v11p_sd15_normalbae
control_v11p_sd15_seg
control_v11p_sd15_inpaint
control_v11p_sd15_lineart
control_v11p_sd15s2_lineart_anime
control_v11p_sd15_openpose
control_v11p_sd15_scribble
control_v11p_sd15_softedge
control_v11e_sd15_shuffle
control_v11e_sd15_ip2p
control_v11f1e_sd15_tile
您可以简单地用列表中的一个模型名称替换ControlNet模型名称,然后开始使用它。使用开源ControlNet辅助模型[6]中的一个注释器生成控制图像。
考虑到人工智能领域的发展速度,当你阅读这段内容时,版本可能已增加到v1.1+。然而,底层机制应该是相同的。
SDXL ControlNets
当我撰写这一章时,SDXL刚刚发布,这个新模型生成的图像质量优秀,且比之前需要更短的提示词。Hugging Face Diffusers团队为XL模型训练并提供了几个ControlNet模型。其使用方法几乎与之前版本相同。在这里,我们将使用controlnet-openpose-sdxl-1.0
开放姿态ControlNet为SDXL。
注意,您将需要一个具有超过15 GB VRAM的专用GPU来运行以下示例。
使用以下代码初始化一个SDXL管道:
import torch
from diffusers import StableDiffusionXLPipeline
sdxl_pipe = StableDiffusionXLPipeline.from_pretrained(
"RunDiffusion/RunDiffusion-XL-Beta",
torch_dtype = torch.float16,
load_safety_checker = False
)
sdxl_pipe.watermark = None
然后,生成一个包含男士的图像:
from diffusers import EulerDiscreteScheduler
prompt = """
full body photo of young man, arms spread
white blank background,
glamour photography,
upper body wears shirt,
wears suit pants,
wears leather shoes
"""
neg_prompt = """
worst quality,low quality, paint, cg, spots, bad hands,
three hands, noise, blur, bad anatomy, low resolution, blur face, bad face
"""
sdxl_pipe.to("cuda")
sdxl_pipe.scheduler = EulerDiscreteScheduler.from_config(
sdxl_pipe.scheduler.config)
image = sdxl_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
width = 832,
height = 1216
).images[0]
sdxl_pipe.to("cpu")
torch.cuda.empty_cache()
image
代码将生成一个图像,如图图13.7所示:
图13.7:由SDXL生成的穿着西装的男士
我们可以使用controlnet_aux
中的OpenposeDetector
[6]来提取姿态:
from controlnet_aux import OpenposeDetector
open_pose = \
OpenposeDetector.from_pretrained("lllyasviel/Annotators")
pose = open_pose(image)
pose
我们将获得如图图13.8所示的姿态图像:
图13.8:穿着西装的男士姿态图像
现在,让我们使用SDXL ControlNet开放姿态模型启动SDXL管道:
from diffusers import StableDiffusionXLControlNetPipeline
from diffusers import ControlNetModel
sdxl_pose_controlnet = ControlNetModel.from_pretrained(
"thibaud/controlnet-openpose-sdxl-1.0",
torch_dtype=torch.float16,
)
sdxl_cn_pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
"RunDiffusion/RunDiffusion-XL-Beta",
torch_dtype = torch.float16,
load_safety_checker = False,
add_watermarker = False,
controlnet = sdxl_pose_controlnet
)
sdxl_cn_pipe.watermark = None
现在,我们可以使用新的ControlNet管道从姿态图像生成具有相同风格的新的图像。我们将重用提示词,但将man替换为woman。我们的目标是生成一个穿着西装的女士的新图像,但与之前男士的图像具有相同的姿态:
from diffusers import EulerDiscreteScheduler
prompt = """
full body photo of young woman, arms spread
white blank background,
glamour photography,
wear sunglass,
upper body wears shirt,
wears suit pants,
wears leather shoes
"""
neg_prompt = """
worst quality,low quality, paint, cg, spots, bad hands,
three hands, noise, blur, bad anatomy, low resolution,
blur face, bad face
"""
sdxl_cn_pipe.to("cuda")
sdxl_cn_pipe.scheduler = EulerDiscreteScheduler.from_config(
sdxl_cn_pipe.scheduler.config)
generator = torch.Generator("cuda").manual_seed(2)
image = sdxl_cn_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
width = 832,
height = 1216,
image = pose,
generator = generator,
controlnet_conditioning_scale = 0.5,
num_inference_steps = 30,
guidance_scale = 6.0
).images[0]
sdxl_cn_pipe.to("cpu")
torch.cuda.empty_cache()
image
代码生成一个具有相同姿态的新图像,与预期完全匹配,如图图13.9所示:
图13.9:使用SDXL ControlNet生成的穿着西装的女士
我们将在第16章中进一步讨论Stable Diffusion XL。
摘要
在本章中,我们介绍了一种使用SD ControlNets精确控制图像生成的方法。从我们提供的详细示例中,你可以开始使用一个或多个ControlNet模型与SD v1.5以及SDXL一起使用。
我们还深入探讨了ControlNet的内部机制,简要解释了它是如何工作的。
我们可以在许多应用中使用ControlNet,包括将风格应用于图像、将形状应用于图像、将两个图像合并为一个,以及使用摆姿势的图像生成人体。它在许多方面都非常强大且非常有用。我们的想象力是唯一的限制。
然而,还有一个限制:很难在两个生成(具有不同的种子)之间对齐背景和整体上下文。你可能想使用ControlNet从源视频提取的帧生成视频,但结果仍然不理想。
在下一章中,我们将介绍使用SD生成视频和动画的解决方案。
参考文献
-
向文本到图像扩散模型添加条件控制: https://arxiv.org/abs/2302.05543
-
ControlNet v1.0 GitHub仓库: https://github.com/lllyasviel/ControlNet
-
ControlNet v1.1 GitHub仓库: https://github.com/lllyasviel/ControlNet-v1-1-nightly
-
surface_normal_uncertainty
: https://github.com/baegwangbin/surface_normal_uncertainty -
零卷积常见问题解答: https://github.com/lllyasviel/ControlNet/blob/main/docs/faq.md
-
ControlNet AUX: https://github.com/patrickvonplaten/controlnet_aux
-
Canny边缘检测器: https://en.wikipedia.org/wiki/Canny_edge_detector
-
X-Adapter:为升级的扩散模型添加插件通用兼容性: https://showlab.github.io/X-Adapter/
第十四章:使用稳定扩散生成视频
利用稳定扩散模型的力量,我们可以通过LoRA、文本嵌入和控制网等技术生成高质量的图像。从静态图像的自然发展是动态内容,即视频。我们能否使用稳定扩散模型生成一致的视频?
稳定扩散模型的 UNet 架构虽然对单图处理有效,但在处理多图时缺乏上下文感知能力。因此,使用相同的提示和参数但不同的种子生成相同或持续相关的图像具有挑战性。由于模型本身的随机性引入,生成的图像在颜色、形状或风格上可能存在显著差异。
一个人可能会考虑图像到图像的管道或ControlNet方法,其中视频片段被分割成单个图像,每个图像依次处理。然而,在整个序列中保持一致性,尤其是在应用重大变化(如将真实视频转换为卡通)时,仍然是一个挑战。即使有姿态对齐,输出视频仍可能表现出明显的闪烁。
随着Yuwei Gao及其同事发表的 AnimateDiff: Animating Your Personalized Text-to-Image Diffusion Models without Specific Tuning [1] 的出版,取得了突破。这项工作为从文本生成一致图像铺平了道路,从而使得创建短视频成为可能。
本章将探讨以下内容:
-
文本到视频生成的原理
-
AnimateDiff 的实际应用
-
利用 Motion LoRA 控制动画运动
到本章结束时,你将理解视频生成的理论方面、AnimateDiff 的内部工作原理以及为什么这种方法在创建一致和连贯的图像方面是有效的。通过提供的示例代码,你将能够生成一个 16 帧的视频。然后你可以应用 Motion LoRA 来操纵动画的运动。
请注意,本章的结果无法在静态格式如论文或PDF中完全欣赏。为了获得最佳体验,我们鼓励你参与相关的示例代码,运行它,并观察生成的视频。
技术要求
本章中,我们将使用 Diffusers
库中可用的 AnimateDiffPipeline
生成视频。你不需要安装任何额外的工具或包,因为 Diffusers(从版本 0.23.0 开始)提供了所有必需的组件和类。在整个章节中,我将指导你使用这些功能。
要以 MP4 视频格式导出结果,你还需要安装 opencv-python
包:
pip install opencv-python
此外,请注意,AnimateDiffPipeline
生成一个 16 帧 256x256 的视频片段至少需要 8 GB 的 VRAM。
文本到视频生成的原理
Stable Diffusion UNet虽然有效于生成单个图像,但由于其缺乏上下文意识,在生成一致图像方面存在不足。研究人员已经提出了克服这一局限性的解决方案,例如从前一或两个帧中结合时间信息。然而,这种方法仍然无法确保像素级一致性,导致连续图像之间存在明显差异,并在生成的视频中产生闪烁。
为了解决这个不一致性问题,AnimateDiff的作者训练了一个分离的运动模型——一个类似于ControlNet模型的零初始化卷积侧模型。进一步地,而不是控制一个图像,运动模型被应用于一系列连续的帧,如图图14.1所示:
图14.1:AnimatedDiff的架构
该过程涉及在视频数据集上训练运动建模模块以提取运动先验,同时保持基础Stable Diffusion模型冻结。运动先验是关于运动的知识,以便指导视频的生成或定制。在训练阶段,一个运动模块(也称为运动UNet)被添加到Stable Diffusion UNet中。与正常的Stable Diffusion V1.5 UNet训练类似,这个运动UNet将同时处理所有帧。我们可以将它们视为来自同一视频片段的一批图像。
例如,如果我们输入一个包含16帧的视频,带有注意力头的运动模块将被训练以考虑所有16帧。如果我们查看实现源代码,TransformerTemporalModel
[4]是MotionModules
[3]的核心组件。
在推理过程中,当我们想要生成视频时,将加载运动模块并将其权重合并到Stable Diffusion UNet中。当我们想要生成一个包含16帧的视频时,管道将首先使用高斯噪声——𝒩(0,1)初始化16个随机潜在值。如果没有运动模块,Stable Diffusion UNet将去除噪声并生成16个独立的图像。然而,借助内置的Transformer注意力头的运动模块的帮助,运动UNet试图创建16个相关的帧。你可能想知道,为什么这些图像是相关的?那是因为训练视频中的帧是相关的。在去噪阶段之后,VAE中的解码器D将把16个潜在值转换为像素图像。
运动UNet负责在生成的视频中引入连续帧之间的相关性。它类似于一个图像中不同区域的相关性。这是因为注意力头关注图像的不同部分,并且在训练阶段学习了这些知识。同样,在视频生成过程中,模型在训练阶段学习了帧之间的相关性。
在核心上,这种方法涉及设计一个在连续图像序列上操作的注意力机制。通过学习帧之间的关系,AnimateDiff可以从文本生成更一致和连贯的图像。此外,由于基础Stable Diffusion模型保持锁定状态,各种Stable Diffusion扩展技术,如LoRA、文本嵌入、ControlNet和图像到图像生成,也可以应用于AnimateDiff。
理论上,任何适用于标准Stable Diffusion的东西,也应该适用于AnimateDiff到AnimateDiff。
在进入下一节之前,请注意,AnimateDiff模型生成一个16帧、256x256的视频片段至少需要12 GB的VRAM。为了真正理解这个概念,编写代码来利用AnimateDiff是非常推荐的。现在,让我们使用AnimateDiff生成一个短视频(GIF和MP4格式)。
AnimateDiff的实际应用
原始的AnimateDiff代码和模型作为一个独立的GitHub仓库[2]发布。虽然作者提供了示例代码和Google Colab来展示结果,但用户仍然需要手动拉取代码并下载模型文件来使用,同时要小心包的版本。
到2023年11月,Dhruv Nair [9] 将AnimateDiff Pipeline合并到Diffusers中,使用户能够在不离开Diffusers
包的情况下,使用AnimateDiff持久化模型生成视频片段。以下是使用Diffusers中的AnimatedDiff管道的方法:
-
使用集成的AnimateDiff代码安装这个特定的Diffusers版本:
pip install diffusers==0.23.0
在撰写本章时,包含最新AnimateDiff代码的Diffusers版本是0.23.0。通过指定这个版本号,您可以确保示例代码能够顺利且无错误地运行,因为它已经针对这个特定版本进行了测试。
您还可以尝试安装Diffusers的最新版本,因为到您阅读这篇文档的时候,它可能已经增加了更多功能:
pip install -U diffusers
-
加载运动适配器。我们将使用原始论文作者的预训练运动适配器模型:
from diffusers import MotionAdapter
adapter = MotionAdapter.from_pretrained(
"guoyww/animatediff-motion-adapter-v1-5-2"
)
-
从基于Stable Diffusion v1.5的检查点模型加载一个AnimateDiff管道:
from diffusers import AnimateDiffPipeline
pipe = AnimateDiffPipeline.from_pretrained(
"digiplay/majicMIX_realistic_v6",
motion_adapter = adapterm,
safety_checker = None
)
-
使用合适的调度器。调度器在生成连贯图像的过程中扮演着重要的角色。作者进行的一项比较研究表明,不同的调度器可以导致不同的结果。实验表明,具有以下设置的
EulerAncestralDiscreteScheduler
调度器可以生成相对较好的结果:from diffusers import EulerAncestralDiscreteScheduler
scheduler = EulerAncestralDiscreteScheduler.from_pretrained(
model_path,
subfolder = "scheduler",
clip_sample = False,
timestep_spacing = "linspace",
steps_offset = 1
)
pipe.scheduler = scheduler
pipe.enable_vae_slicing()
pipe.enable_model_cpu_offload()
为了优化VRAM使用,您可以采用两种策略。首先,使用
pipe.enable_vae_slicing()
配置VAE一次解码一帧,从而减少内存消耗。此外,利用pipe.enable_model_cpu_offload()
将空闲子模型卸载到CPU,进一步降低VRAM使用。 -
生成连贯的图像:
import torch
from diffusers.utils import export_to_gif, export_to_video
prompt = """photorealistic, 1girl, dramatic lighting"""
neg_prompt = """worst quality, low quality, normal quality, lowres, bad anatomy, bad hands, monochrome, grayscale watermark, moles"""
#pipe.to("cuda:0")
output = pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
num_frames = 16,
num_inference_steps = 30,
guidance_scale= 8.5,
generator = torch.Generator("cuda").manual_seed(7)
)
frames = output.frames[0]
torch.cuda.empty_cache()
export_to_gif(frames, "animation_origin_256_wo_lora.gif")
export_to_video(frames, "animation_origin_256_wo_lora.mp4")
现在,你应该能够看到使用AnimateDiff生成的16帧产生的GIF文件。这个GIF使用了16张256x256的图像。你可以应用在第11章中介绍的图像超分辨率技术来提升图像并创建一个512x512的GIF。我不会在本章中重复代码。强烈建议利用在第11章中学到的技能来进一步提高视频生成的质量。
利用运动LoRA控制动画运动
除了运动适配器模型之外,论文的作者还介绍了运动LoRA来控制运动风格。运动LoRA与我们第8章中介绍的相同的LoRA适配器。如前所述,AnimateDiff管道支持所有其他社区共享的LoRA。你可以在作者的Hugging Face仓库 [8] 中找到这些运动LoRA。
这些运动LoRA可以用来控制摄像机视角。在这里,我们将使用放大LoRA – guoyww/animatediff-motion-lora-zoom-in
– 作为示例。放大将引导模型生成带有放大运动的视频。
使用方法简单,只需额外一行代码:
pipe.load_lora_weights("guoyww/animatediff-motion-lora-zoom-in",
adapter_name="zoom-in")
这里是完整的生成代码。我们主要重用了上一节中的代码:
import torch
from diffusers.utils import export_to_gif, export_to_video
prompt = """
photorealistic, 1girl, dramatic lighting
"""
neg_prompt = """
worst quality, low quality, normal quality, lowres, bad anatomy, bad hands
, monochrome, grayscale watermark, moles
"""
pipe.to("cuda:0")
pipe.load_lora_weights("guoyww/animatediff-motion-lora-zoom-in", adapter_name="zoom-in")
output = pipe(
prompt = prompt,
negative_prompt = neg_prompt,
height = 256,
width = 256,
num_frames = 16,
num_inference_steps = 40,
guidance_scale = 8.5,
generator = torch.Generator("cuda").manual_seed(123)
)
frames = output.frames[0]
pipe.to("cpu")
torch.cuda.empty_cache()
export_to_gif(frames, "animation_origin_256_w_lora_zoom_in.gif")
export_to_video(frames, "animation_origin_256_w_lora_zoom_in.mp4")
你应该在同一文件夹下看到一个名为animation_origin_256_w_lora_zoom_in.gif
的放大GIF剪辑和一个名为animation_origin_256_w_lora_zoom_in.mp4
的MP4视频剪辑被生成。
摘要
每天在社交网络上流传的文本到视频样本的质量和持续时间都在不断提高。很可能在你阅读这一章的时候,本章中提到的技术的功能已经超过了这里所描述的内容。然而,一个不变的是训练模型将注意力机制应用于一系列图像的概念。
在撰写本文时,OpenAI的Sora [9]刚刚发布。这项技术可以根据Transformer Diffusion架构生成高质量的视频。这与AnimatedDiff中使用的方法类似,它结合了Transformer和扩散模型。
AnimatedDiff与众不同的地方在于其开放性和适应性。它可以应用于任何具有相同基础检查点版本的社区模型,这是目前任何其他解决方案都没有提供的功能。此外,论文的作者已经完全开源了代码和模型。
本章主要讨论了文本到图像生成的挑战,然后介绍了AnimatedDiff,解释了它是如何以及为什么能工作的。我们还提供了一个示例代码,展示如何使用Diffusers包中的AnimatedDiff管道在自己的GPU上从16张连贯的图像生成GIF剪辑。
在下一章中,我们将探讨从图像生成文本描述的解决方案。
参考文献
-
郭宇伟,杨策源,饶安毅,王耀辉,乔宇,林大华,戴博,AnimateDiff:无需特定 调整 的个性化文本到图像扩散模型:https://arxiv.org/abs/2307.04725
-
原始 AnimateDiff 代码仓库:https://github.com/guoyww/AnimateDiff
-
Diffusers 动作模块实现:https://github.com/huggingface/diffusers/blob/3dd4168d4c96c429d2b74c2baaee0678c57578da/src/diffusers/models/unets/unet_motion_model.py#L50
)
-
Hugging Face Diffusers TransformerTemporalModel 实现:https://github.com/huggingface/diffusers/blob/3dd4168d4c96c429d2b74c2baaee0678c57578da/src/diffusers/models/transformers/transformer_temporal.py#L41
-
[4] Dhruv Nair,https://github.com/DN6
-
AnimateDiff 提案拉取请求:https://github.com/huggingface/diffusers/pull/5413
-
animatediff-motion-adapter-v1-5-2:https://huggingface.co/guoyww/animatediff-motion-adapter-v1-5-2
-
郭宇伟的 Hugging Face 仓库:https://huggingface.co/guoyww
-
视频生成模型作为世界模拟器:https://openai.com/research/video-generation-models-as-world-simulators
第十五章:使用BLIP-2和LLaVA生成图像描述
想象一下,你手中有一张图像,需要将其放大或基于它生成新的图像,但你没有与之相关的提示或描述。你可能会说,“好吧,我可以为它编写一个新的提示。”对于一张图像来说,这是可以接受的,但如果有成千上万甚至数百万张没有描述的图像呢?手动编写它们是不可能的。
幸运的是,我们可以使用人工智能(AI)来帮助我们生成描述。有许多预训练模型可以实现这一目标,而且数量总是在增加。在本章中,我将介绍两种AI解决方案来生成图像的标题、描述或提示,全部自动化:
-
BLIP-2:使用冻结图像编码器和大型语言模型进行语言-图像预训练的引导 [1]
-
LLaVA:大型语言和视觉助手 [3]
BLIP-2 [1] 运行速度快,对硬件要求相对较低,而LLaVA [3](其llava-v1.5-13b
模型)在撰写本文时是最新的、功能最强大的模型。
到本章结束时,你将能够做到以下事情:
-
通常了解BLIP-2和LLaVA的工作原理
-
编写Python代码以使用BLIP-2和LLaVA从图像生成描述
技术要求
在深入探讨BLIP-2和LLaVA之前,让我们使用Stable Diffusion生成一张用于测试的图像。
首先,加载一个deliberate-v2
模型,但不将其发送到CUDA:
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16
)
接下来,在以下代码中,我们首先将模型发送到CUDA并生成图像,然后我们将模型卸载到CPU RAM中,并从CUDA中清除模型:
text2img_pipe.to("cuda:0")
prompt ="high resolution, a photograph of an astronaut riding a horse"
input_image = text2img_pipe(
prompt = prompt,
generator = torch.Generator("cuda:0").manual_seed(100),
height = 512,
width = 768
).images[0]
text2img_pipe.to("cpu")
torch.cuda.empty_cache()
input_image
上述代码将为我们生成与以下图中类似的图像,该图像将在以下章节中使用:
图15.1:由SD v1.5生成的宇航员骑马的图像
现在,让我们开始吧。
BLIP-2 – 语言-图像预训练的引导
在BLIP:为统一视觉-语言理解和生成进行语言-图像预训练的引导论文[4]中,李俊南等人提出了一种弥合自然语言和视觉模态之间差距的解决方案。值得注意的是,BLIP模型在生成高质量图像描述方面表现出卓越的能力,在发布时超越了现有的基准。
其高质量背后的原因是李俊南等人使用了一种创新的技术,从他们的第一个预训练模型中构建了两个模型:
-
过滤模型
-
标题生成模型
过滤模型可以过滤掉低质量的文本-图像对,从而提高训练数据质量,同时其标题生成模型可以为图像生成令人惊讶的好、简短的描述。借助这两个模型,论文的作者不仅提高了训练数据质量,还自动扩大了其规模。然后,他们使用增强的训练数据重新训练 BLIP 模型,结果令人印象深刻。但这是 2022 年的故事。
到 2023 年 6 月,Salesforce 的同一团队推出了新的 BLIP-2。
BLIP-2 的工作原理
在当时,BLIP 表现良好,但其模型的语言部分仍然相对较弱。大型语言模型(LLMs)如 OpenAI 的 GPT 和 Meta 的 LLaMA 强大,但训练成本极高。因此,BLIP 团队通过问自己提出了挑战:我们能否使用现成的、预训练的冻结图像编码器和冻结 LLM 进行视觉语言预训练,同时保留它们学习到的表示?
答案是肯定的。BLIP-2 通过引入查询转换器来解决这个问题,该转换器有助于生成与文本标题对应的视觉表示,然后将其输入到冻结的 LLM 中以解码文本描述。
查询转换器,通常被称为 Q-Former [2],是 BLIP-2 模型的一个关键组件。它充当连接冻结图像编码器和冻结语言大模型(LLM)的桥梁。Q-Former 的主要功能是将一组“查询标记”映射到查询嵌入。这些查询嵌入有助于从图像编码器中提取与给定文本指令最相关的视觉特征。
在 BLIP-2 模型的训练过程中,图像编码器和 LLM 的权重保持冻结。同时,Q-Former 进行训练,使其能够根据特定任务要求进行适应和优化性能。通过使用一组可学习的查询向量,Q-Former 有效地从图像编码器中提炼出有价值的信息,使得语言大模型(LLM)能够基于视觉内容生成准确且上下文适当的响应。
类似的概念也应用于 LLaVA,我们将在后面讨论。BLIP 的核心思想是重用有效的视觉和语言组件,并且只训练一个中间模型来将它们连接起来。
接下来,让我们开始使用 BLIP-2。
使用 BLIP-2 生成描述
在 Hugging Face transformers [5] 包的帮助下,使用 BLIP-2 既简单又干净。如果您还没有安装该包,只需运行以下命令来安装或更新到最新版本:
pip install -U transformer
然后使用以下代码加载 BLIP-2 模型数据:
from transformers import AutoProcessor, Blip2ForConditionalGeneration
import torch
processor = AutoProcessor.from_pretrained("Salesforce/blip2-opt-2.7b")
# by default `from_pretrained` loads the weights in float32
# we load in float16 instead to save memory
device = "cuda" if torch.cuda.is_available() else "cpu"
model = Blip2ForConditionalGeneration.from_pretrained(
"Salesforce/blip2-opt-2.7b",
torch_dtype=torch.float16
).to(device)
您的第一次运行将自动从 Hugging Face 模型存储库下载模型权重数据。这可能需要一些时间,所以请耐心等待。下载完成后,运行以下代码以询问 BLIP-2 我们提供的图像:
prompt = "describe the content of the image:"
inputs = processor(
input_image,
text=prompt,
return_tensors="pt"
).to(device, torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=768)
generated_text = processor.batch_decode(
generated_ids,
skip_special_tokens=True
)[0].strip()
print(generated_text)
该代码返回描述“太空中的骑马宇航员”,这是好的且准确的。如果我们问背景中有多少颗行星呢?让我们将提示改为“背景中有多少颗行星:”。然后它返回“宇宙比你想象的要大”。这次不够好。
因此,BLIP-2擅长快速生成整个图像的简短描述。然而,要生成更详细的描述或甚至与图像交互,我们可以利用LLaVA的力量。
LLaVA – 大型语言和视觉助手
如其名称LLaVA[3]所示,这个模型不仅在名称上,而且在内部结构上与LLaMA非常接近。LLaVA使用LLaMA作为其语言部分,这使得在需要时可以更换语言模型。这绝对是一个许多场景的杀手特性。Stable Diffusion的一个关键特性是其对模型更换和微调的开放性。与Stable Diffusion类似,LLaVA被设计为利用开源的LLM模型。
接下来,让我们看看LLaVA是如何工作的。
LLaVA的工作原理
LLaVA的作者,刘浩天等人[3],展示了一个精美、准确的图表,展示了该模型如何在架构中利用预训练的CLIP和LLaMA模型,如下所示:
图15.2:LLaVA的架构
让我们从下往上阅读这个图表。在推理过程中,我们提供一个表示为Xv的图像和一个表示为Xq的语言指令。视觉编码器是CLIP视觉编码器ViT-L/14[6]。与Stable Diffusion v1.5一样,CLIP也用作其文本编码器。
CLIP模型将图像编码为Zv,投影W是LLaVA提供的模型数据。投影模型将编码后的图像嵌入Zv投影到 Hv 如下:
Hv = W ⋅ Zv
在另一方面,语言指令被编码为CLIP的512维嵌入块。图像和语言嵌入具有相同的维度性。
以这种方式,语言模型fϕ既了解图像又了解语言!这种方法与Stable Diffusion的文本反转技术有一些相似之处,既轻量又强大。
接下来,让我们编写一些代码来指导LLaVA与图像交互。
安装LLaVA
为了获得最佳体验,强烈建议在Linux机器上使用LLaVA。在Windows上使用可能会导致意外缺失组件。还建议为使用LLaVA建立Python虚拟环境。在第2章中提供了设置Python虚拟环境的详细步骤和命令。
将LLaVA仓库克隆到本地文件夹:
git clone https://github.com/haotian-liu/LLaVA.git
cd LLaVA
然后通过运行以下命令安装LLaVA:
pip install -U .
接下来,从Hugging Face模型仓库下载模型文件:
# Make sure you have git-lfs installed (https://git-lfs.com)
git lfs install
git clone https://huggingface.co/liuhaotian/llava-v1.5-7b
请注意,模型文件很大,下载需要一些时间。在编写本章时,您也可以通过将前面代码片段中的 7
更改为 13
来下载 13B 模型,以使用 13B LLaVA 模型。
设置到此结束。现在,让我们继续编写 Python 代码。
使用 LLaVA 生成图像描述
由于我们刚刚安装了 LLaVA,我们可以参考以下相关模块:
from llava.constants import (
IMAGE_TOKEN_INDEX,
DEFAULT_IMAGE_TOKEN,
DEFAULT_IM_START_TOKEN,
DEFAULT_IM_END_TOKEN
)
from llava.conversation import (
conv_templates, SeparatorStyle
)
from llava.model.builder import load_pretrained_model
from llava.mm_utils import (
process_images,
tokenizer_image_token,
get_model_name_from_path,
KeywordsStoppingCriteria
)
加载 tokenizer
、image_processor
和 model
组件。tokenizer
将文本转换为标记 ID,image_processor
将图像转换为张量,而 model
是我们将用于生成输出的管道:
# load up tokenizer, model, image_processor
model_path = "/path/to/llava-v1.5-7b"
model_name = get_model_name_from_path(model_path)
conv_mode = "llava_v1"
tokenizer, model, image_processor, _ = load_pretrained_model(
model_path = model_path,
model_base = None,
model_name = model_name,
load_4bit = True,
device = "cuda",
device_map = {'':torch.cuda.current_device()}
)
以下是对前面代码的分解:
-
model_path
:此路径指向存储预训练模型的文件夹。 -
model_base
:此参数设置为None
,表示未指定特定的父架构。 -
model_name
:预训练模型的名称(llava-v1.5
)。 -
load_4bit
:如果设置为True
,则在推理过程中启用 4 位量化。这可以减少内存使用并提高速度,但可能会略微影响结果的品质。 -
device
:此参数指定 CUDA 作为计算发生的设备。 -
device_map
:此参数用于将 GPU 设备映射到模型的各个部分,如果您想将工作负载分配到多个 GPU 上。由于此处只映射了一个设备,这意味着将执行单 GPU 运行。
现在,让我们创建图像描述:
-
创建一个
conv
对象以保存对话历史:# start a new conversation
user_input = """Analyze the image in a comprehensive and detailed manner"""
conv = conv_templates[conv_mode].copy()
-
将图像转换为张量:
# process image to tensor
image_tensor = process_images(
[input_image],
image_processor,
{"image_aspect_ratio":"pad"}
).to(model.device, dtype=torch.float16)
-
将图像占位符添加到对话中:
if model.config.mm_use_im_start_end:
inp = DEFAULT_IM_START_TOKEN + DEFAULT_IMAGE_TOKEN + \
DEFAULT_IM_END_TOKEN + '\n' + user_input
else:
inp = DEFAULT_IMAGE_TOKEN + '\n' + user_input
conv.append_message(conv.roles[0], inp)
-
获取提示并将其转换为推理的标记:
# get the prompt for inference
conv.append_message(conv.roles[1], None)
prompt = conv.get_prompt()
# convert prompt to token ids
input_ids = tokenizer_image_token(
prompt,
tokenizer,
IMAGE_TOKEN_INDEX,
return_tensors='pt'
).unsqueeze(0).cuda()
-
准备停止条件:
stop_str = conv.sep if conv.sep_style != \
SeparatorStyle.TWO else conv.sep2
keywords = [stop_str]
stopping_criteria = KeywordsStoppingCriteria(keywords,
tokenizer, input_ids)
-
最后,从 LLaVA 获取输出:
# output the data
with torch.inference_mode():
output_ids = model.generate(
input_ids,
images =image_tensor,
do_sample = True,
temperature = 0.2,
max_new_tokens = 1024,
streamer = None,
use_cache = True,
stopping_criteria = [stopping_criteria]
)
outputs = tokenizer.decode(output_ids[0,
input_ids.shape[1]:]).strip()
# make sure the conv object holds all the output
conv.messages[-1][-1] = outputs
print(outputs)
如以下输出所示,LLaVA 可以生成令人惊叹的描述:
The image features a man dressed in a white space suit, riding a horse in a desert-like environment. The man appears to be a space traveler, possibly on a mission or exploring the area. The horse is galloping, and the man is skillfully riding it.
In the background, there are two moons visible, adding to the sense of a space-themed setting. The combination of the man in a space suit, the horse, and the moons creates a captivating and imaginative scene.</s>
我已经尽可能地精简了代码,但它仍然很长。它需要仔细复制或转录到您的代码编辑器中。我建议复制粘贴此书附带的存储库中提供的代码。您可以在单个单元格中执行上述代码,以观察 LLaVA 从图像中生成描述的有效性。
摘要
在本章中,我们的主要重点是两种旨在生成图像描述的 AI 解决方案。第一个是 BLIP-2,这是一种生成图像简洁标题的有效且高效的解决方案。第二个是 LLaVA 解决方案,它能够从图像中生成更详细和准确的描述信息。
在 LLaVA 的帮助下,我们甚至可以与图像互动以从中提取更多信息。
视觉和语言能力的集成也为开发更强大的多模态模型奠定了基础,其潜力我们只能开始想象。
在下一章中,我们将开始使用 Stable Diffusion XL。
参考文献
-
李俊南,李东旭,西尔维奥·萨瓦雷塞,何思婷,BLIP-2:使用冻结图像编码器和大型语言模型进行语言-图像预训练的引导:https://arxiv.org/abs/2301.12597
-
BLIP-2 Hugging Face 文档:https://huggingface.co/docs/transformers/main/model_doc/blip-2
-
刘浩天,李春远,吴庆阳,李庸宰,LLaVA:大型语言和视觉 助手:https://llava-vl.github.io/
-
李俊南,李东旭,熊才明,何思婷,BLIP:为统一视觉语言理解和生成进行语言-图像预训练的引导:https://arxiv.org/abs/2201.12086
-
Hugging Face Transformers GitHub 仓库:https://github.com/huggingface/transformers
-
Alec Radford,金钟宇,克里斯·霍拉西,阿迪亚·拉梅什,加布里埃尔·高,桑迪尼·阿加瓦尔,吉里什·萨斯特里,阿曼达·阿斯凯尔,帕梅拉·米什金,杰克·克拉克,格雷琴·克鲁格,伊利亚·苏茨克维,从自然语言监督中学习可迁移的视觉模型:https://arxiv.org/abs/2103.00020
-
LLaVA GitHub 仓库:https://github.com/haotian-liu/LLaVA
第十六章:探索 Stable Diffusion XL
在不太成功的 Stable Diffusion 2.0 和 Stable Diffusion 2.1 之后,2023 年 7 月 Stability AI 发布了其最新版本,Stable Diffusion XL(SDXL)[1]。注册一开放,我就迫不及待地应用了模型权重数据。我的测试和社区进行的测试都表明,SDXL 取得了显著的进步。它现在允许我们以更高的分辨率生成更高质量的图像,远远超过了 Stable Diffusion V1.5 基础模型。另一个显著的改进是能够使用更直观的“自然语言”提示来生成图像,消除了需要拼凑大量“单词”来形成一个有意义的提示的需求。此外,我们现在可以用更简洁的提示生成所需的图像。
与之前的版本相比,SDXL 在几乎每个方面都有所改进,值得花费时间和精力开始使用它以实现更好的稳定图像生成。在本章中,我们将详细讨论 SDXL 的新特性,并解释上述变化为何导致了其改进。例如,我们将探讨与 Stable Diffusion V1.5 相比,变分自编码器(VAE)、UNet 和 TextEncoder 设计中有什么新内容。简而言之,本章将涵盖以下内容:
-
SDXL 的新特性是什么?
-
使用 SDXL
然后,我们将使用 Python 代码演示最新的 SDXL 基础模型和社区模型在实际中的应用。我们将涵盖基本用法,以及高级用法,例如加载多个 LoRA 模型和使用无限加权提示。
让我们开始吧。
SDXL 的新特性是什么?
SDXL 仍然是一个潜在扩散模型,保持了与 Stable Diffusion v1.5 相同的整体架构。根据 SDXL 背后的原始论文 [2],SDXL 扩展了每个组件,使它们更宽、更大。SDXL 的骨干 UNet 大了三倍,SDXL 基础模型中有两个文本编码器,还包括了一个基于扩散的细化模型。整体架构如图 16.1 所示:
图 16.1:SDXL 架构
注意,细化器是可选的;我们可以决定是否使用细化器模型。接下来,让我们逐一深入到每个组件。
SDXL 的 VAE
VAE 是一对编码器和解码器神经网络。VAE 编码器将图像编码到潜在空间中,其配对的解码器可以将潜在图像解码为像素图像。许多网络文章告诉我们 VAE 是一种用于提高图像质量的技巧;然而,这并不是全部。在 Stable Diffusion 中,VAE 的核心责任是将像素图像转换为潜在空间,并将其从潜在空间转换回来。当然,一个好的 VAE 可以通过添加高频细节来提高图像质量。
SDXL中使用的VAE是一个重新训练的版本,它使用相同的自动编码器架构,但批量大小有所增加(256与9相比),并且还使用指数移动平均跟踪权重[2]。新的VAE在所有评估指标上都优于原始模型。
由于这些实现差异,如果我们决定独立使用VAE,而不是重用第5章中引入的VAE代码,我们需要编写新的代码。在这里,我们将提供一个SDXL VAE的一些常见用法的示例:
-
初始化一个VAE模型:
import torch
from diffusers.models import AutoencoderKL
vae_model = AutoencoderKL.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "vae"
).to("cuda:0")
-
使用VAE模型编码图像。在执行以下代码之前,将
cat.png
文件替换为验证的可访问图像路径:from diffusers.utils import load_image
from diffusers.image_processor import VaeImageProcessor
image = load_image("/path/to/cat.png")
image_processor = VaeImageProcessor()
prep_image = image_processor.preprocess(image)
prep_image = prep_image.to("cuda:0")
with torch.no_grad():
image_latent = vae_model.encode(prep_image
).latent_dist.sample()
image_latent.shape
-
从潜在空间解码图像:
with torch.no_grad():
decode_image = vae_model.decode(
image_latent,
return_dict = False
)[0]
image = image_processor.postprocess(image = decode_image)[0]
image
在前面的代码中,你首先将图像编码到潜在空间。潜在空间中的图像对我们来说是不可见的,但它捕捉了图像在潜在空间中的特征(换句话说,在高维向量空间中)。然后,代码的解码部分将潜在空间中的图像解码到像素空间。从前面的代码中,我们可以知道VAE的核心功能是什么。
你可能好奇为什么了解VAE的知识是必要的。它有众多应用。例如,它允许你将生成的潜在图像保存在数据库中,仅在需要时解码。这种方法可以将图像存储减少多达90%,而信息损失很小。
SDXL的UNet
UNet模型是SDXL的骨干神经网络。SDXL中的UNet比之前的Stable Diffusion模型大近三倍。SDXL的UNet是一个26GB的亿参数神经网络,而Stable Diffusion V1.5的UNet有8.6亿参数。尽管当前的开源LLM模型在神经网络大小方面要大得多,但截至写作时(2023年10月),SDXL的UNet是开源Diffusion模型中最大的,这直接导致了更高的VRAM需求。8GB的VRAM在大多数使用SD V1.5的情况下可以满足需求;对于SDXL,通常需要15GB的VRAM;否则,我们需要降低图像分辨率。
除了模型大小扩展外,SDXL还重新排列了其Transformer块的顺序,这对于更好的、更精确的自然语言到图像指导至关重要。
SDXL中的两个文本编码器
SDXL中最显著的变化之一是文本编码器。SDXL使用两个文本编码器一起,CLIP ViT-L [5] 和 OpenCLIP ViT-bigG(也称为OpenCLIP G/14)。此外,SDXL使用OpenCLIP ViT-bigG的池化嵌入。
CLIP ViT-L是OpenAI最广泛使用的模型之一,也是Stable Diffusion V1.5中使用的文本编码器或嵌入模型。OpenCLIP ViT-bigG模型是什么?OpenCLIP是CLIP(Contrastive Language-Image Pre-Training)的开源实现。OpenCLIP G/14是在LAION-2B数据集[9]上训练的最大和最好的OpenClip模型,该数据集包含2亿张图片,总大小为100 TB。虽然OpenAI CLIP模型生成一个768维度的嵌入向量,但OpenClip G/14输出一个1,280维度的嵌入。通过连接两个嵌入(长度相同),输出一个2,048维度的嵌入。这比Stable Diffusion v1.5之前的768维度嵌入大得多。
为了说明文本编码过程,让我们以句子a running dog
作为输入;普通的文本标记器首先将句子转换为标记,如下面的代码所示:
input_prompt = "a running dog"
from transformers import CLIPTokenizer,CLIPTextModel
import torch
# initialize tokenizer 1
clip_tokenizer = CLIPTokenizer.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "tokenizer",
dtype = torch.float16
)
input_tokens = clip_tokenizer(
input_prompt,
return_tensors="pt"
)["input_ids"]
print(input_tokens)
clip_tokenizer_2 = CLIPTokenizer.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "tokenizer_2",
dtype = torch.float16
)
input_tokens_2 = clip_tokenizer_2(
input_prompt,
return_tensors="pt"
)["input_ids"]
print(input_tokens_2)
前面的代码将返回以下结果:
tensor([[49406, 320, 2761, 1929, 49407]])
tensor([[49406, 320, 2761, 1929, 49407]])
在前面的结果中,49406
是起始标记,而49407
是结束标记。
接下来,下面的代码使用CLIP文本编码器将标记转换为嵌入向量:
clip_text_encoder = CLIPTextModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "text_encoder",
torch_dtype =torch.float16
).to("cuda")
# encode token ids to embeddings
with torch.no_grad():
prompt_embeds = clip_text_encoder(
input_tokens.to("cuda")
)[0]
print(prompt_embeds.shape)
结果嵌入张量包括五个768维度的向量:
torch.Size([1, 5, 768])
之前的代码使用了OpenAI的CLIP将提示文本转换为768维度的嵌入。下面的代码使用OpenClip G/14模型将标记编码为五个1,280维度的嵌入:
clip_text_encoder_2 = CLIPTextModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "text_encoder_2",
torch_dtype =torch.float16
).to("cuda")
# encode token ids to embeddings
with torch.no_grad():
prompt_embeds_2 = clip_text_encoder_2(input_tokens.to("cuda"))[0]
print(prompt_embeds_2.shape)
结果嵌入张量包括五个1,280维度的向量:
torch.Size([1, 5, 1280])
现在,下一个问题是,什么是池化嵌入?嵌入池化是将一系列标记转换为单个嵌入向量的过程。换句话说,池化嵌入是信息的有损压缩。
与我们之前使用的嵌入过程不同,该过程将每个标记编码为一个嵌入向量,池化嵌入是一个代表整个输入文本的向量。我们可以使用以下Python代码从OpenClip生成池化嵌入:
from transformers import CLIPTextModelWithProjection
clip_text_encoder_2 = CLIPTextModelWithProjection.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
subfolder = "text_encoder_2",
torch_dtype =torch.float16
).to("cuda")
# encode token ids to embeddings
with torch.no_grad():
pool_embed = clip_text_encoder_2(input_tokens.to("cuda"))[0]
print(pool_embed.shape)
前面的代码将从文本编码器返回一个torch.Size([1, 1280])
的池化嵌入向量。池化嵌入的最大标记大小为77
。在SDXL中,池化嵌入与来自CLIP和OpenCLIP的标记级别嵌入一起提供给UNet,以指导图像生成。
别担心——在使用SDXL之前,您不需要手动提供这些嵌入。Diffusers
包中的StableDiffusionXLPipeline
会为我们做所有事情。我们只需要提供提示和负面提示文本。我们将在使用 SDXL部分提供示例代码。
两阶段设计
SDXL的另一个设计增加是其精细模型。根据SDXL论文[2],精细模型用于通过添加更多细节并使其更好来增强图像,尤其是在最后10步。
精细模型只是另一个图像到图像的模型,可以帮助修复损坏的图像,并为基模型生成的图像添加更多元素。
根据我的观察,对于社区共享的检查点模型,精炼器模型可能不是必需的。
接下来,我们将使用SDXL进行常见用例。
使用SDXL
我们在第6章中简要介绍了SDXL模型的加载,在第13章中介绍了SDXL ControlNet的使用。您可以在那里找到示例代码。在本节中,我们将介绍更多常见的SDXL用法,包括加载社区共享的SDXL模型以及如何使用图像到图像的管道来增强模型,使用SDXL与社区共享的LoRA模型,以及来自本书作者的Diffuser的无限制长度提示管道。
使用SDXL社区模型
仅在SDXL发布几个月后,开源社区就基于Stability AI的基础模型发布了无数经过微调的SDXL模型。我们可以在Hugging Face和CIVITAI(https://civitai.com/)上找到这些模型,数量还在不断增加。
在这里,我们将从HuggingFace加载一个模型,使用SDXL模型ID:
import torch
from diffusers import StableDiffusionXLPipeline
base_pipe = StableDiffusionXLPipeline.from_pretrained(
"RunDiffusion/RunDiffusion-XL-Beta",
torch_dtype = torch.float16
)
base_pipe.watermark = None
注意,在前面的代码中,base_pipe.watermark = None
将移除生成的图像中的不可见水印。
接下来,将模型移动到CUDA,生成图像,然后从CUDA卸载模型:
prompt = "realistic photo of astronaut cat in fighter cockpit, detailed, 8k"
sdxl_pipe.to("cuda")
image = sdxl_pipe(
prompt = prompt,
width = 768,
height = 1024,
generator = torch.Generator("cuda").manual_seed(1)
).images[0]
sdxl_pipe.to("cpu")
torch.cuda.empty_cache()
image
仅需一行提示,无需提供任何负面提示,SDXL就能生成令人惊叹的图像,如图图16**.2所示:
图16.2:由SDXL生成的猫飞行员
您可能想使用精炼器模型来增强图像,但精炼器模型并没有带来显著的变化。相反,我们将使用具有相同模型数据的图像到图像管道来提高图像的分辨率。
使用SDXL图像到图像增强图像
让我们先提高图像到两倍:
from diffusers.image_processor import VaeImageProcessor
img_processor = VaeImageProcessor()
# get the size of the image
(width, height) = image.size
# upscale image
image_x = img_processor.resize(
image = image,
width = int(width * 1.5),
height = int(height * 1.5)
)
image_x
然后,通过重用前一个文本到图像管道中的模型数据来启动图像到图像管道,从而节省RAM和VRAM的使用:
from diffusers import StableDiffusionXLImg2ImgPipeline
img2img_pipe = StableDiffusionXLImg2ImgPipeline(
vae = sdxl_pipe.vae,
text_encoder = sdxl_pipe.text_encoder,
text_encoder_2 = sdxl_pipe.text_encoder_2,
tokenizer = sdxl_pipe.tokenizer,
tokenizer_2 = sdxl_pipe.tokenizer_2,
unet = sdxl_pipe.unet,
scheduler = sdxl_pipe.scheduler,
add_watermarker = None
)
img2img_pipe.watermark = None
现在,是时候调用管道来进一步增强图像:
img2img_pipe.to("cuda")
refine_image_2x = img2img_pipe(
image = image_x,
prompt = prompt,
strength = 0.3,
num_inference_steps = 30,
guidance_scale = 4.0
).images[0]
img2img_pipe.to("cpu")
torch.cuda.empty_cache()
refine_image_2x
注意,我们将强度设置为0.3
以保留大部分原始输入图像信息。我们将得到一个新、更好的图像,如图图16**.3所示:
图16.3:图像到图像管道中精炼的猫飞行员图像
尽管您可能一开始在书中看不到太多差异,但仔细检查计算机屏幕上的图像,您会发现许多额外的细节。
现在,让我们探讨如何利用LoRA与Diffusers结合使用。如果您对LoRA不熟悉,我建议您回顾第8章,其中更详细地介绍了Stable Diffusion LoRA的用法,以及第21章,其中全面介绍了LoRA的训练。
使用SDXL LoRA模型
不久前,使用Diffusers加载LoRA是不可能的,更不用说将多个LoRA模型加载到一个管道中。随着Diffusers团队和社区贡献者的巨大工作,我们现在可以将多个LoRA模型加载到SDXL管道中,并指定LoRA缩放数值。
其使用也非常简单。只需两行代码即可将一个LoRA添加到管道中:
sdxl_pipe.load_lora_weights("path/to/lora.safetensors")
sdxl_pipe.fuse_lora(lora_scale = 0.5)
要添加两个LoRA模型:
sdxl_pipe.load_lora_weights("path/to/lora1.safetensors")
sdxl_pipe.fuse_lora(lora_scale = 0.5)
sdxl_pipe.load_lora_weights("path/to/lora2.safetensors")
sdxl_pipe.fuse_lora(lora_scale = 0.5)
如我们在第8章中讨论的,使用LoRA有两种方式——一种是将它与骨干模型权重合并,另一种是动态猴子补丁。在这里,对于SDXL,方法是模型合并,这意味着从管道中卸载一个LoRA。为了卸载一个LoRA模型,我们需要再次加载LoRA,但带有负的lora_scale
。例如,如果我们想从管道中卸载lora2.safetensors
,我们可以使用以下代码来实现:
sdxl_pipe.load_lora_weights("path/to/lora2.safetensors")
sdxl_pipe.fuse_lora(lora_scale = -0.5)
除了使用fuse_lora
加载LoRA模型外,我们还可以使用PEFT集成LoRA加载。代码与我们刚刚使用的代码非常相似,但我们添加了一个名为adapter_name
的额外参数,如下所示:
sdxl_pipe.load_lora_weights("path/to/lora1.safetensors",
adapter_name="lora1")
sdxl_pipe.load_lora_weights("path/to/lora2.safetensors", ,
adapter_name="lora2")
我们可以使用以下代码动态调整LoRA缩放:
sdxl_pipe.set_adapters(["lora1", "lora2"], adapter_weights=[0.5, 1.0])
我们还可以禁用LoRA,如下所示:
sdxl_pipe.disable_lora()
或者,我们可以禁用两个加载的LoRA模型中的一个,如下所示:
sdxl_pipe.set_adapters(["lora1", "lora2"], adapter_weights=[0.0, 1.0])
在上述代码中,我们禁用了lora1
,同时继续使用lora2
。
通过适当的LoRA管理代码,你可以使用SDXL和无限数量的LoRA模型。说到“无限”,接下来,我们将介绍SDXL的“无限”长度提示词。
使用SDXL和无限制提示词
默认情况下,SDXL,像之前的版本一样,一次图像生成只支持最多77个token。在第10章中,我们深入探讨了实现支持无长度限制的加权提示词的文本嵌入编码器。对于SDXL,想法类似但更复杂,实现起来也稍微困难一些;毕竟,现在有两个文本编码器。
我构建了一个长权重SDXL管道,lpw_stable_diffusion_xl
,它与官方的Diffusers
包合并。在本节中,我将介绍如何使用此管道以长权重和无限制的方式启用管道。
确保您已使用以下命令更新了您的Diffusers
包到最新版本:
pip install -U diffusers
然后,使用以下代码使用管道:
from diffusers import DiffusionPipeline
import torch
pipe = DiffusionPipeline.from_pretrained(
"RunDiffusion/RunDiffusion-XL-Beta",
torch_dtype = torch.float16,
use_safetensors = True,
variant = "fp16",
custom_pipeline = "lpw_stable_diffusion_xl",
)
prompt = """
glamour photography, (full body:1.5) photo of young man,
white blank background,
wear sweater, with scarf,
wear jean pant,
wear nike run shoes,
wear sun glass,
wear leather shoes,
holding a umbrella in hand
""" * 2
prompt = prompt + " a (cute cat:1.5) aside"
neg_prompt = """
(worst quality:1.5),(low quality:1.5), paint, cg, spots, bad hands,
three hands, noise, blur
"""
pipe.to("cuda")
image = pipe(
prompt = prompt,
negative_prompt = neg_prompt,
width = 832,
height = 1216,
generator = torch.Generator("cuda").manual_seed(7)
).images[0]
pipe.to("cpu")
torch.cuda.empty_cache()
image
上述代码使用DiffusionPipeline
加载一个由开源社区成员(即我)贡献的自定义管道lpw_stable_diffusion_xl
。
注意,在代码中,提示词被乘以2,使其长度肯定超过77个token。在提示词的末尾,附加了a (cute cat:1.5) aside
。如果管道支持超过77个token的提示词,生成的结果中应该有一只猫。
上述代码生成的图像显示在图16**.4中:
图16.4:使用无限提示长度管道生成的一男一猫,lpw_stable_diffusion_xl
从图像中,我们可以看到提示中的所有元素都得到了反映,现在有一只可爱的小猫坐在男人的旁边。
摘要
本章介绍了最新和最好的稳定扩散模型——SDXL。我们首先介绍了SDXL的基本知识以及为什么它强大且高效,然后深入研究了新发布模型的每个组件,包括VAE、UNet、文本编码器和新的两阶段设计。
我们为每个组件提供了示例代码,以帮助您深入了解SDXL。这些代码示例也可以用来利用各个组件的力量。例如,我们可以使用VAE来压缩图像,并使用文本编码器为图像生成文本嵌入。
在本章的后半部分,我们介绍了SDXL的一些常见用例,例如加载社区共享的检查点模型,使用图像到图像的管道增强和放大图像,以及介绍一种简单有效的解决方案来将多个LoRA模型加载到一个管道中。最后,我们提供了一个端到端解决方案,用于使用无限长度的加权提示来使用SDXL。
在SDXL的帮助下,我们可以用简短的提示生成令人惊叹的图像,并实现更好的结果。
在下一章中,我们将讨论如何编写稳定扩散提示,并利用LLM自动生成和增强提示。
参考文献
-
SDXL:改进潜在扩散模型以实现高分辨率图像合成:https://arxiv.org/abs/2307.01952
-
稳定扩散XL扩散器:https://huggingface.co/docs/diffusers/main/en/using-diffusers/sdxl
-
CLIP from OpenAI: https://openai.com/research/clip
-
CLIP VIT Large模型:https://huggingface.co/openai/clip-vit-large-patch14
-
使用OPENCLIP达到80%的无监督准确率:在LAION-2B上训练的VIT-G/14:https://laion.ai/blog/giant-openclip/
-
CLIP-ViT-bigG-14-laion2B-39B-b160k:https://huggingface.co/laion/CLIP-ViT-bigG-14-laion2B-39B-b160k
-
OpenCLIP GitHub仓库:https://github.com/mlfoundations/open_clip
-
LAION-5B:开放大规模多模态数据集的新时代:https://laion.ai/blog/laion-5b/
第十七章:为Stable Diffusion构建优化提示
在Stable Diffusion V1.5(SD V1.5)中,制作提示以生成理想的图像可能具有挑战性。看到由复杂和不寻常的词组合产生的令人印象深刻的图像并不罕见。这主要归因于Stable Diffusion V1.5中使用的语言文本编码器——OpenAI的CLIP模型。CLIP使用来自互联网的带标题图像进行训练,其中许多是标签而不是结构化句子。
当使用SD v1.5时,我们不仅要记住大量的“魔法”关键词,还要有效地组合这些标签词。对于SDXL,其双语言编码器CLIP和OpenCLIP比之前SD v1.5中的要先进和智能得多。然而,我们仍然需要遵循某些指南来编写有效的提示。
在本章中,我们将介绍创建专用提示的基本原则,然后探讨强大的大型语言模型(LLM)技术,以帮助我们自动生成提示。以下是本章将要涉及的主题:
-
什么是一个好的提示?
-
使用LLM作为提示生成器
让我们开始吧。
什么是一个好的提示?
有人说使用Stable Diffusion就像是一个魔术师,微小的技巧和改动就能产生巨大的影响。为Stable Diffusion编写好的提示对于充分利用这个强大的文本到图像AI模型至关重要。让我介绍一些最佳实践,这些实践将使你的提示更加有效。
从长远来看,AI模型将更好地理解自然语言,但就目前而言,让我们付出额外的努力,让我们的提示工作得更好。
在与本章相关的代码文件中,你会发现Stable Diffusion v1.5对提示非常敏感,不同的提示将显著影响图像质量。同时,Stable Diffusion XL得到了很大改进,对提示不太敏感。换句话说,为Stable Diffusion XL编写简短的提示描述将生成相对稳定的图像质量。
你也可以在本章附带代码库中找到生成所有图像的代码。
清晰具体
你的提示越具体,从Stable Diffusion获得的图像就越准确。
这里有一个原始提示:
A painting of cool sci-fi.
从Stable Diffusion V1.5开始,我们可能会得到如图图17.1所示的图像:
图17.1:使用SD V1.5从提示“一幅酷炫的科幻画”生成的图像
它给我们带来了带有先进设备的动画人脸,但它离我们可能想要的“科幻”概念还远。
从Stable Diffusion XL开始,“科幻”概念得到了更丰富的体现,如图图17.2所示:
图 17.2:使用 SDXL 从提示“一幅酷科幻画”生成的图像
这些画作确实很酷,但简短的提示生成的图像要么不是我们想要的,要么控制度不够。
现在我们重写提示,添加更多具体元素:
A photorealistic painting of a futuristic cityscape with towering skyscrapers, neon lights, and flying vehicles, Science Fiction Artwork
使用改进的提示,SD V1.5 给出的结果比原始结果更准确,如图 图 17.3 所示:
图 17.3:使用 SD V1.5 从添加特定元素的提示生成的图像
SDXL 也改进了其输出,反映了给定的提示,如图 图 17.4 所示:
图 17.4:使用 SDXL 从添加特定元素的提示生成的图像
除非你故意让 Stable Diffusion 做出自己的决定,一个好的提示清楚地定义了期望的结果,几乎没有模糊的空间。它应指定主题、风格以及任何描述你想象中的图像的额外细节。
描述性描述
描述性地描述主题。这与 清晰具体 规则类似;不仅应该具体,而且我们提供给 SD 模型的输入和细节越多,我们得到的结果就越好。这对于生成肖像图像尤其有效。
假设我们想要生成一张以下提示的女性肖像:
A beautiful woman
这里是我们从 SD V1.5 得到的结果:
图 17.5:使用 SD V1.5 从提示“一位美丽的女士”生成的图像
整体来说,这张图片不错,但细节不足,看起来像是半画半照片。SDXL 使用这个简短的提示生成了更好的图像,如图 图 17.6 所示:
图 17.6:使用 SDXL 从提示“一位美丽的女士”生成的图像
但结果却是随机的:有时是全身图像,有时是专注于脸部。为了更好地控制结果,让我们改进提示如下:
Masterpiece, A stunning realistic photo of a woman with long, flowing brown hair, piercing emerald eyes, and a gentle smile, set against a backdrop of vibrant autumn foliage.
使用这个提示,SD V1.5 返回了更好、更一致的图像,如图 图 17.7 所示:
图 17.7:使用 SD V1.5 从增强描述性提示生成的图像
类似地,SDXL 也提供了由提示范围限定的图像,而不是生成野性、失控的图像,如图 图 17.8 所示:
图 17.8:使用 SDXL 从增强描述性提示生成的图像
提及细节,如服装、配饰、面部特征和周围环境;越多越好。描述性对于引导Stable Diffusion生成期望的图像至关重要。使用描述性语言在Stable Diffusion模型的“心中”描绘一幅生动的画面。
使用一致的术语
确保在整个上下文中提示的一致性。除非你愿意被Stable Diffusion的惊喜所吸引,否则术语的矛盾将导致意外的结果。
假设我们给出以下提示,想要生成一个穿着蓝色西装的男人,但我们也将彩色布料
作为关键词的一部分:
A man wears blue suit, he wears colorful cloth
这种描述是矛盾的,SD模型将不清楚要生成什么:蓝色西装还是彩色西装?结果是未知的。使用这个提示,SDXL生成了图17.9中显示的两个图像:
图17.9:使用SD V1.5从提示“一个男人穿着蓝色西装,他穿着彩色布料”生成的图像
一张穿着蓝色西装的图像,另一张穿着彩色西装的图像。让我们改进提示,告诉Stable Diffusion我们想要一件蓝色西装搭配彩色围巾:
A man in a sharp, tailored blue suit is adorned with a vibrant, colorful scarf, adding a touch of personality and flair to his professional attire
现在,结果要好得多,更一致,如图17.10所示:
图17.10:使用SD V1.5从经过细化的统一提示生成的图像
在你使用的术语中保持一致性,以避免混淆模型。如果你在提示的第一部分提到了一个关键概念,不要突然在后面部分改变到另一个概念。
参考艺术作品和风格
参考特定的艺术作品或艺术风格以引导AI复制期望的美学。提及该风格显著的特征,如笔触、色彩搭配或构图元素,这些将严重影响生成结果。
让我们生成一张不提梵高的星夜的夜空图像:
A vibrant, swirling painting of a starry night sky with a crescent moon illuminating a quaint village nestled among rolling hills."
Stable Diffusion V1.5生成具有卡通风格的图像,如图17.11所示:
图17.11:使用SD V1.5从未指定风格或参考作品的提示生成的图像
让我们在提示中添加梵高的星夜
:
A vibrant, swirling painting of a starry night sky reminiscent of Van Gogh's Starry Night, with a crescent moon illuminating a quaint village nestled among rolling hills.
如图17.12所示,梵高的旋转风格在画作中更为突出:
图17.12:使用SD V1.5从指定了风格和参考作品的提示生成的图像
结合负面提示
Stable Diffusion 还提供了一个负面提示输入,以便我们可以定义不希望添加到图像中的元素。负面提示在许多情况下都表现良好。
我们将使用以下提示,不应用负面提示:
1 girl, cute, adorable, lovely
Stable Diffusion 将生成如图 图 17.13 所示的图像:
图 17.13:使用 SD V1.5 从不带负面提示的提示中生成的图像
这并不算太糟糕,但离好还差得远。让我们假设我们提供以下一些负面提示:
paintings, sketches, worst quality, low quality, normal quality, lowres,
monochrome, grayscale, skin spots, acne, skin blemishes, age spots, extra fingers,
fewer fingers,broken fingers
生成的图像有了很大的改进,如图 图 17.14 所示:
图 17.14:使用 SD V1.5 从带有负面提示的提示中生成的图像
正面提示会增加 Stable Diffusion 模型的 UNet 对目标对象的关注,而负面提示则减少了显示对象的“关注”。有时,简单地添加适当的负面提示可以极大地提高图像质量。
迭代和细化
不要害怕尝试不同的提示并看看哪个效果最好。通常需要一些尝试和错误才能得到完美的结果。
然而,手动创建满足这些要求的提示很困难,更不用说包含主题、风格、艺术家、分辨率、细节、颜色和照明信息的提示了。
接下来,我们将使用 LLM 作为提示生成助手。
使用 LLM 生成更好的提示
所有的上述规则或技巧都有助于更好地理解 Stable Diffusion 如何与提示一起工作。由于这是一本关于使用 Python 与 Stable Diffusion 一起使用的书,我们不希望手动处理这些任务;最终目标是自动化整个过程。
Stable Diffusion 发展迅速,其近亲 LLM 和多模态社区也毫不逊色。在本节中,我们将利用 LLM 帮助我们根据一些关键词输入生成提示。以下提示适用于各种类型的 LLM:ChatGPT、GPT-4、Google Bard 或任何其他有能力的开源 LLM。
首先,让我们告诉 LLM 它将要做什么:
You will take a given subject or input keywords, and output a more creative, specific, descriptive, and enhanced version of the idea in the form of a fully working Stable Diffusion prompt. You will make all prompts advanced, and highly enhanced. Prompts you output will always have two parts, the "Positive Prompt" and "Negative prompt".
在前面的提示下,LLM 知道如何处理输入;接下来,让我们教它一些关于 Stable Diffusion 的知识。没有这些,LLM 可能对 Stable Diffusion 一无所知:
Here is the Stable Diffusion document you need to know:
* Good prompts needs to be clear and specific, detailed and descriptive.
* Good prompts are always consistent from beginning to end, no contradictory terminology is included.
* Good prompts reference to artworks and style keywords, you are art and style experts, and know how to add artwork and style names to the prompt.
IMPORTANT:You will look through a list of keyword categories and decide whether you want to use any of them. You must never use these keyword category names as keywords in the prompt itself as literal keywords at all, so always omit the keywords categories listed below:
Subject
Medium
Style
Artist
Website
Resolution
Additional details
Color
Lighting
Treat the above keywords as a checklist to remind you what could be used and what would best serve to make the best image possible.
我们还需要告诉 LLM 一些术语的定义:
About each of these keyword categories so you can understand them better:
(Subject:)
The subject is what you want to see in the image.
(Resolution:)
The Resolution represents how sharp and detailed the image is. Let's add keywords with highly detailed and sharp focus.
(Additional details:)
Any Additional details are sweeteners added to modify an image, such as sci-fi, stunningly beautiful and dystopian to add some vibe to the image.
(Color:)
color keywords can be used to control the overall color of the image. The colors you specified may appear as a tone or in objects, such as metallic, golden, red hue, etc.
(Lighting:)
Lighting is a key factor in creating successful images (especially in photography). Lighting keywords can have a huge effect on how the image looks, such as cinematic lighting or dark to the prompt.
(Medium:)
The Medium is the material used to make artwork. Some examples are illustration, oil painting, 3D rendering, and photography.
(Style:)
The style refers to the artistic style of the image. Examples include impressionist, surrealist, pop art, etc.
(Artist:)
Artist names are strong modifiers. They allow you to dial in the exact style using a particular artist as a reference. It is also common to use multiple artist names to blend their styles, for example, Stanley Artgerm Lau, a superhero comic artist, and Alphonse Mucha, a portrait painter in the 19th century could be used for an image, by adding this to the end of the prompt:
by Stanley Artgerm Lau and Alphonse Mucha
(Website:)
The Website could be Niche graphic websites such as Artstation and Deviant Art, or any other website which aggregates many images of distinct genres. Using them in a prompt is a sure way to steer the image toward these styles.
根据前面的定义,我们正在教 LLM 关于 什么是一个好的提示? 部分的指南:
CRITICAL IMPORTANT: Your final prompt will not mention the category names at all, but will be formatted entirely with these articles omitted (A', 'the', 'there',) do not use the word 'no' in the Negative prompt area. Never respond with the text, "The image is a", or "by artist", just use "by [actual artist name]" in the last example replacing [actual artist name] with the actual artist name when it's an artist and not a photograph style image.
For any images that are using the medium of Anime, you will always use these literal keywords at the start of the prompt as the first keywords (include the parenthesis):
"masterpiece, best quality, (Anime:1.4)"
For any images that are using the medium of photo, photograph, or photorealistic, you will always use all of the following literal keywords at the start of the prompt as the first keywords (but you must omit the quotes):
"(((photographic, photo, photogenic))), extremely high quality high detail RAW color photo"
Never include quote marks (this: ") in your response anywhere. Never include, 'the image' or 'the image is' in the response anywhere.
Never include, too verbose of a sentence, for example, while being sure to still share the important subject and keywords 'the overall tone' in the response anywhere, if you have tonal keywords or keywords just list them, for example, do not respond with, 'The overall tone of the image is dark and moody', instead just use this: 'dark and moody'
The response you give will always only be all the keywords you have chosen separated by a comma only.
排除任何涉及性或裸露的提示:
IMPORTANT:
If the image includes any nudity at all, mention nude in the keywords explicitly and do NOT provide these as keywords in the keyword prompt area. You should always provide tasteful and respectful keywords.
为 LLM 提供一个少样本学习 [1] 材料的示例:
Here is an EXAMPLE (this is an example only):
I request: "A beautiful white sands beach"
You respond with this keyword prompt paragraph and Negative prompt paragraph:
Positive Prompt: Serene white sands beach with crystal clear waters, and lush green palm trees, Beach is secluded, with no crowds or buildings, Small shells scattered across sand, Two seagulls flying overhead. Water is calm and inviting, with small waves lapping at shore, Palm trees provide shade, Soft, fluffy clouds in the sky, soft and dreamy, with hues of pale blue, aqua, and white for water and sky, and shades of green and brown for palm trees and sand, Digital illustration, Realistic with a touch of fantasy, Highly detailed and sharp focus, warm and golden lighting, with sun setting on horizon, casting soft glow over the entire scene, by James Jean and Alphonse Mucha, Artstation
Negative Prompt: low quality, people, man-made structures, trash, debris, storm clouds, bad weather, harsh shadows, overexposure
现在,教 LLM 如何输出负面提示:
IMPORTANT: Negative Keyword prompts
Using negative keyword prompts is another great way to steer the image, but instead of putting in what you want, you put in what you don't want. They don't need to be objects. They can also be styles and unwanted attributes. (e.g. ugly, deformed, low quality, etc.), these negatives should be chosen to improve the overall quality of the image, avoid bad quality, and make sense to avoid possible issues based on the context of the image being generated, (considering its setting and subject of the image being generated.), for example, if the image is a person holding something, that means the hands will likely be visible, so using 'poorly drawn hands' is wise in that case.
This is done by adding a 2nd paragraph, starting with the text 'Negative Prompt': and adding keywords. Here is a full example that does not contain all possible options, but always use only what best fits the image requested, as well as new negative keywords that would best fit the image requested:
tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, bad anatomy, watermark, signature, cut off, low contrast, underexposed, overexposed, bad art, beginner, amateur, distorted face, blurry, draft, grainy
IMPORTANT:
Negative keywords should always make sense in context to the image subject and medium format of the image being requested. Don't add any negative keywords to your response in the negative prompt keyword area where it makes no contextual sense or contradicts, for example, if I request: 'A vampire princess, anime image', then do NOT add these keywords to the Negative prompt area: 'anime, scary, Man-made structures, Trash, Debris, Storm clouds', and so forth. They need to make sense of the actual image being requested so it makes sense in context.
IMPORTANT:
For any images that feature a person or persons, and are also using the Medium of a photo, photograph, or photorealistic in your response, you must always respond with the following literal keywords at the start of the NEGATIVE prompt paragraph, as the first keywords before listing other negative keywords (omit the quotes):
"bad-hands-5, bad_prompt, unrealistic eyes"
If the image is using the Medium of an Anime, you must have these as the first NEGATIVE keywords (include the parenthesis):
(worst quality, low quality:1.4)
提醒LLM注意存在标记限制;在这里,你可以将150
改为其他数字。本章相关的示例代码使用了由SkyTNT [3]创建的lpw_stable_diffusion
,以及由本书作者Andrew Zhu创建的lpw_stable_diffusion_xl
:
IMPORTANT: Prompt token limit:
The total prompt token limit (per prompt) is 150 tokens. Are you ready for my first subject?
将所有提示合并到一个块中:
You will take a given subject or input keywords, and output a more creative, specific, descriptive, and enhanced version of the idea in the form of a fully working Stable Diffusion prompt. You will make all prompts advanced, and highly enhanced. Prompts you output will always have two parts, the "Positive Prompt" and "Negative prompt".
Here is the Stable Diffusion document you need to know:
* Good prompts needs to be clear and specific, detailed and descriptive.
* Good prompts are always consistent from beginning to end, no contradictory terminology is included.
* Good prompts reference to artworks and style keywords, you are art and style experts, and know how to add artwork and style names to the prompt.
IMPORTANT:You will look through a list of keyword categories and decide whether you want to use any of them. You must never use these keyword category names as keywords in the prompt itself as literal keywords at all, so always omit the keywords categories listed below:
Subject
Medium
Style
Artist
Website
Resolution
Additional details
Color
Lighting
About each of these keyword categories so you can understand them better:
(Subject:)
The subject is what you want to see in the image.
(Resolution:)
The Resolution represents how sharp and detailed the image is. Let's add keywords highly detailed and sharp focus.
(Additional details:)
Any Additional details are sweeteners added to modify an image, such as sci-fi, stunningly beautiful and dystopian to add some vibe to the image.
(Color:)
color keywords can be used to control the overall color of the image. The colors you specified may appear as a tone or in objects, such as metallic, golden, red hue, etc.
(Lighting:)
Lighting is a key factor in creating successful images (especially in photography). Lighting keywords can have a huge effect on how the image looks, such as cinematic lighting or dark to the prompt.
(Medium:)
The Medium is the material used to make artwork. Some examples are illustration, oil painting, 3D rendering, and photography.
(Style:)
The style refers to the artistic style of the image. Examples include impressionist, surrealist, pop art, etc.
(Artist:)
Artist names are strong modifiers. They allow you to dial in the exact style using a particular artist as a reference. It is also common to use multiple artist names to blend their styles, for example, Stanley Artgerm Lau, a superhero comic artist, and Alphonse Mucha, a portrait painter in the 19th century could be used for an image, by adding this to the end of the prompt:
by Stanley Artgerm Lau and Alphonse Mucha
(Website:)
The Website could be Niche graphic websites such as Artstation and Deviant Art, or any other website which aggregates many images of distinct genres. Using them in a prompt is a sure way to steer the image toward these styles.
Treat the above keywords as a checklist to remind you what could be used and what would best serve to make the best image possible.
CRITICAL IMPORTANT: Your final prompt will not mention the category names at all, but will be formatted entirely with these articles omitted (A', 'the', 'there',) do not use the word 'no' in the Negative prompt area. Never respond with the text, "The image is a", or "by artist", just use "by [actual artist name]" in the last example replacing [actual artist name] with the actual artist name when it's an artist and not a photograph style image.
For any images that are using the medium of Anime, you will always use these literal keywords at the start of the prompt as the first keywords (include the parenthesis):
"masterpiece, best quality, (Anime:1.4)"
For any images that are using the medium of photo, photograph, or photorealistic, you will always use all of the following literal keywords at the start of the prompt as the first keywords (but you must omit the quotes):
"(((photographic, photo, photogenic))), extremely high quality high detail RAW color photo"
Never include quote marks (this: ") in your response anywhere. Never include, 'the image' or 'the image is' in the response anywhere.
Never include, too verbose of a sentence, for example, while being sure to still share the important subject and keywords 'the overall tone' in the response anywhere, if you have tonal keywords or keywords just list them, for example, do not respond with, 'The overall tone of the image is dark and moody', instead just use this: 'dark and moody'
The response you give will always only be all the keywords you have chosen separated by a comma only.
IMPORTANT:
If the image includes any nudity at all, mention nude in the keywords explicitly and do NOT provide these as keywords in the keyword prompt area. You should always provide tasteful and respectful keywords.
Here is an EXAMPLE (this is an example only):
I request: "A beautiful white sands beach"
You respond with this keyword prompt paragraph and Negative prompt paragraph:
Positive Prompt: Serene white sands beach with crystal clear waters, and lush green palm trees, Beach is secluded, with no crowds or buildings, Small shells scattered across sand, Two seagulls flying overhead. Water is calm and inviting, with small waves lapping at shore, Palm trees provide shade, Soft, fluffy clouds in the sky, soft and dreamy, with hues of pale blue, aqua, and white for water and sky, and shades of green and brown for palm trees and sand, Digital illustration, Realistic with a touch of fantasy, Highly detailed and sharp focus, warm and golden lighting, with sun setting on horizon, casting soft glow over the entire scene, by James Jean and Alphonse Mucha, Artstation
Negative Prompt: low quality, people, man-made structures, trash, debris, storm clouds, bad weather, harsh shadows, overexposure
IMPORTANT: Negative Keyword prompts
Using negative keyword prompts is another great way to steer the image, but instead of putting in what you want, you put in what you don't want. They don't need to be objects. They can also be styles and unwanted attributes. (e.g. ugly, deformed, low quality, etc.), these negatives should be chosen to improve the overall quality of the image, avoid bad quality, and make sense to avoid possible issues based on the context of the image being generated, (considering its setting and subject of the image being generated.), for example, if the image is a person holding something, that means the hands will likely be visible, so using 'poorly drawn hands' is wise in that case.
This is done by adding a 2nd paragraph, starting with the text 'Negative Prompt': and adding keywords. Here is a full example that does not contain all possible options, but always use only what best fits the image requested, as well as new negative keywords that would best fit the image requested:
tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, bad anatomy, watermark, signature, cut off, low contrast, underexposed, overexposed, bad art, beginner, amateur, distorted face, blurry, draft, grainy
IMPORTANT:
Negative keywords should always make sense in context to the image subject and medium format of the image being requested. Don't add any negative keywords to your response in the negative prompt keyword area where it makes no contextual sense or contradicts, for example, if I request: 'A vampire princess, anime image', then do NOT add these keywords to the Negative prompt area: 'anime, scary, Man-made structures, Trash, Debris, Storm clouds', and so forth. They need to make sense of the actual image being requested so it makes sense in context.
IMPORTANT:
For any images that feature a person or persons, and are also using the Medium of a photo, photograph, or photorealistic in your response, you must always respond with the following literal keywords at the start of the NEGATIVE prompt paragraph, as the first keywords before listing other negative keywords (omit the quotes):
"bad-hands-5, bad_prompt, unrealistic eyes"
If the image is using the Medium of an Anime, you must have these as the first NEGATIVE keywords (include the parenthesis):
(worst quality, low quality:1.4)
IMPORTANT: Prompt token limit:
The total prompt token limit (per prompt) is 150 tokens. Are you ready for my first subject?
简单地将前面的提示复制粘贴到ChatGPT、Bard或其他任何可用的LLM中。然后,输入你想要Stable Diffusion生成的任何主题:
a realistic photo of a woman standing by the side of a river
LLM(在本例中为Google Bard)将为我们提供一个新的提示:
Positive Prompt:
(((photographic, photo, photogenic))), extremely high quality high detail RAW color photo, A serene portrait of a woman standing by the edge of a tranquil river, her gaze lost in the gentle flow of the water. Her sun-kissed skin glows against the backdrop of lush greenery, while her flowing hair cascades down her shoulders like a waterfall. The warm sunlight filters through the trees, casting dappled patterns on her face and the riverbank. The air is filled with the sweet scent of wildflowers, and the gentle sound of birdsong fills the air. The woman's expression is one of peace and tranquility, as if she is completely at one with nature.
Negative Prompt:
bad-hands-5, bad_prompt, unrealistic eyes, blurry, low-resolution, pixelated, noisy, grainy, overexposed, underexposed, harsh shadows, bad lighting, out of focus, poorly composed, distracting elements, low contrast, washed out, unrealistic anatomy, distorted face,
使用原始提示一位女士站在河边
,Stable Diffusion V1.5生成了图17.15中所示的图像:
图17.15:使用SD V1.5从原始提示“一位女士站在河边”生成的图像
使用LLM生成的新的正面和负面提示,SD V1.5生成了图17.16中所示的图像:
图17.16:使用SD V1.5从LLM生成的提示生成的图像
这些改进也适用于SDXL。使用原始提示,SDXL生成了图17.17中所示的图像:
图17.17:使用SDXL从原始提示“一位女士站在河边”生成的图像
使用LLM生成的正面和负面提示,SDXL生成了图17.18中所示的图像:
图17.18:使用SDXL从LLM生成的提示生成的图像
这些图像无疑比原始提示生成的图像要好,证明了LLM生成的提示可以提高生成图像的质量。
摘要
在本章中,我们首先讨论了为Stable Diffusion编写提示以生成高质量图像的挑战。然后,我们介绍了一些编写有效Stable Diffusion提示的基本规则。
进一步来说,我们总结了提示编写的规则,并将它们纳入LLM提示中。这种方法不仅适用于ChatGPT [4],也适用于其他LLM。
在预定义提示和LLM的帮助下,我们可以完全自动化图像生成过程。无需手动仔细编写和调整提示;只需告诉AI你想要生成的内容,LLM就会提供复杂的提示和负面提示。如果设置正确,Stable Diffusion可以自动执行提示并交付结果,无需任何人为干预。
我们理解AI的发展速度非常快。在不久的将来,你将能够添加更多自己的LLM提示,使过程更加智能和强大。这将进一步增强Stable Diffusion和LLM的功能,让你能够以最小的努力生成令人惊叹的图像。
在下一章中,我们将利用前几章学到的知识,使用Stable Diffusion构建有用的应用。
参考文献
-
语言模型是少样本学习者: https://arxiv.org/abs/2005.14165
-
通过ChatGPT或本地LLM模型创建Stable diffusion提示的最佳文本是什么?你使用的是什么更好的?: https://www.reddit.com/r/StableDiffusion/comments/14tol5n/best_text_prompt_for_creating_stable_diffusion/
)
- ChatGPT: https://chat.openai.com/
第4部分 – 将Stable Diffusion构建到应用中
在本书中,我们探讨了Stable Diffusion的巨大潜力,从其基本概念到高级应用和定制技术。现在,是时候将所有内容整合起来,将Stable Diffusion应用于现实世界,使其力量对用户可用,并解锁新的创意表达和解决问题的可能性。
在本最终部分,我们将专注于构建展示Stable Diffusion多功能性和影响力的实用应用。你将学习如何开发创新解决方案,如对象编辑和风格迁移,使用户能够以前所未有的方式操作图像。我们还将讨论数据持久性的重要性,展示如何直接在生成的PNG图像中保存图像生成提示和参数。
此外,你将发现如何使用Gradio等流行框架创建交互式用户界面,使用户能够轻松地与Stable Diffusion模型互动。此外,我们还将深入探讨迁移学习领域,指导你从头开始训练Stable Diffusion LoRA。最后,我们将对Stable Diffusion、AI的未来以及关注这个快速发展的领域最新发展的必要性进行更广泛的讨论。
到本部分结束时,你将具备将Stable Diffusion集成到各种应用中的知识和技能,从创意工具到提高生产力的软件。可能性是无限的,现在是时候释放Stable Diffusion的全部潜力了!
本部分包含以下章节:
第十八章:应用 - 对象编辑和风格迁移
Stable Diffusion(SD)不仅能够生成各种图像,还可以用于图像编辑和风格从一个图像到另一个图像的迁移。在本章中,我们将探讨图像编辑和风格迁移的解决方案。
在此过程中,我们还将介绍使我们能够实现这些目标的工具:CLIPSeg,用于检测图像内容;Rembg,这是一个能够完美去除图像背景的工具;以及 IP-Adapter,用于将风格从一个图像转移到另一个图像。
在本章中,我们将涵盖以下主题:
-
使用 Stable Diffusion 编辑图像
-
对象和风格迁移
让我们开始。
使用 Stable Diffusion 编辑图像
你还记得我们在 第 1 章 中讨论的背景交换示例吗?在本节中,我们将介绍一个可以帮助您编辑图像内容的解决方案。
在我们能够编辑任何东西之前,我们需要识别我们想要编辑的对象的边界。在我们的例子中,为了获取背景掩码,我们将使用 CLIPSeg [1] 模型。CLIPSeg,代表基于 CLIP 的图像分割,是一个训练有素以基于文本提示或参考图像分割图像的模型。与需要大量标记数据的传统分割模型不同,CLIPSeg 可以在少量甚至没有训练数据的情况下实现令人印象深刻的结果。
CLIPSeg 建立在 CLIP 的成功之上,CLIP 是 SD 所使用的相同模型。CLIP 是一个强大的预训练模型,它学会了将文本和图像连接起来。CLIPSeg 模型在 CLIP 的基础上添加了一个小的解码器模块,使其能够将学习到的关系转换为像素级分割。这意味着我们可以向 CLIPSeg 提供一个简单的描述,例如“这张图片的背景”,CLIPSeg 将返回目标对象的掩码。
现在,让我们看看如何使用 CLIPSeg 完成一些任务。
替换图像背景内容
我们首先将加载 CLIPSeg 处理器和模型,然后向模型提供提示和图像以生成掩码数据,最后使用 SD 修复管道重新绘制背景。让我们一步一步来做:
-
加载 CLIPSeg 模型。
以下代码将加载
CLIPSegProcessor
处理器和CLIPSegForImageSegmentation
模型:from transformers import(
CLIPSegProcessor,CLIPSegForImageSegmentation)
processor = CLIPSegProcessor.from_pretrained(
"CIDAS/clipseg-rd64-refined"
)
model = CLIPSegForImageSegmentation.from_pretrained(
"CIDAS/clipseg-rd64-refined"
)
processor
将用于预处理提示和图像输入。model
将负责模型推理。 -
生成灰度掩码。
默认情况下,CLIPSeg 模型将返回其结果的 logits。通过应用
torch.sigmoid()
函数,我们就可以得到图像中目标对象的灰度掩码。灰度掩码可以让我们生成二值掩码,该掩码将在 SD 修复管道中使用:from diffusers.utils import load_image
from diffusers.utils.pil_utils import numpy_to_pil
import torch
source_image = load_image("./images/clipseg_source_image.png")
prompts = ['the background']
inputs = processor(
text = prompts,
images = [source_image] * len(prompts),
padding = True,
return_tensors = "pt"
)
with torch.no_grad():
outputs = model(**inputs)
preds = outputs.logits
mask_data = torch.sigmoid(preds)
mask_data_numpy = mask_data.detach().unsqueeze(-1).numpy()
mask_pil = numpy_to_pil(
mask_data_numpy)[0].resize(source_image.size)
上述代码将生成一个突出显示背景的灰度掩码图像,如图 图 18**.1 所示:
图 18.1:背景灰度掩码
这个掩码还不是我们想要的;我们需要一个二值掩码。为什么我们需要二值掩码?因为 SD v1.5 修复模型与二值掩码相比,与灰度掩码配合得更好。你也可以将灰度掩码添加到 SD 管道中查看结果;尝试不同的组合和输入不会有任何损失。
-
生成二值掩码。
我们将使用以下代码将灰度掩码转换为 0-1 二值掩码图像:
bw_thresh = 100
bw_fn = lambda x : 255 if x > bw_thresh else 0
bw_mask_pil = mask_pil.convert("L").point(bw_fn, mode="1")
让我解释一下我们在前面的代码中展示的关键元素:
-
bw_thresh
:这定义了将像素视为黑色或白色的阈值。在前面代码中,任何大于 100 的灰度像素值将被视为白色高光。 -
mask_pil.convert("L")
:这一行将mask_pil
图像转换为灰度模式。灰度图像只有一个通道,表示像素强度值从 0(黑色)到 255(白色)。 -
.point(bw_fn, mode="1")
:这一行将bw_fn
阈值函数应用于灰度图像的每个像素。mode="1"
参数确保输出图像是一个 1 位二值图像(只有黑白)。
我们将看到 图 18.2 中显示的结果:
-
图 18.2:背景二值掩码
-
使用 SD 修复模型重新绘制背景:
from diffusers import(StableDiffusionInpaintPipeline,
EulerDiscreteScheduler)
inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(
"CompVis/stable-diffusion-v1-4",
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
sd_prompt = "blue sky and mountains"
out_image = inpaint_pipe(
prompt = sd_prompt,
image = source_image,
mask_image = bw_mask_pil,
strength = 0.9,
generator = torch.Generator("cuda:0").manual_seed(7)
).images[0]
out_image
在前面的代码中,我们使用 SD v1.4 模型作为修复模型,因为它生成的结果比 SD v1.5 模型更好。如果你执行它,你会看到我们在第一章中展示的确切结果。现在背景不再是广阔的行星宇宙,而是蓝天和山脉。
同样的技术可以用于许多其他目的,例如编辑照片中的衣物和向照片中添加物品。
移除图像背景
许多时候,我们只想移除图像的背景。有了二值掩码在手,移除背景根本不难。我们可以使用以下代码来完成:
from PIL import Image, ImageOps
output_image = Image.new("RGBA", source_image.size,
(255,255,255,255))
inverse_bw_mask_pil = ImageOps.invert(bw_mask_pil)
r = Image.composite(source_image ,output_image,
inverse_bw_mask_pil)
这里是每行所做事情的分解:
-
from PIL import Image, ImageOps
:这一行从 PIL 导入了Image
和ImageOps
模块。Image
模块提供了一个同名的类,用于表示 PIL 图像。ImageOps
模块包含了许多“现成”的图像处理操作。 -
output_image = Image.new("RGBA", source_image.size, (255,255,255,255))
:这一行创建了一个与source_image
大小相同的新的图像。新的图像将以 RGBA 模式存在,这意味着它包括红色、绿色、蓝色和 alpha(透明度)通道。图像中所有像素的初始颜色被设置为白色(255,255,255)
,具有全不透明度(255)
。 -
inverse_bw_mask_pil = ImageOps.invert(bw_mask_pil)
: 这行代码使用ImageOps中的invert
函数反转bw_mask_pil
图像的颜色。如果bw_mask_pil
是一个黑白图像,则结果将是原始图像的负片,即黑色变成白色,白色变成黑色。 -
r = Image.composite(source_image, output_image, inverse_bw_mask_pil)
: 这行代码通过inverse_bw_mask_pil
掩码图像将source_image
和output_image
混合创建一个新的图像。其中掩码图像为白色(或灰色阴影),则使用source_image
中的相应像素,而掩码图像为黑色,则使用output_image
中的相应像素。结果被分配给r
。
简单的四行代码就能实现将背景替换为纯白色,如图图18.3所示:
图18.3:使用CLIPSeg去除背景
但是,我们会看到锯齿边缘;这并不好,而且不能通过CLIPSeg完美解决。如果你打算再次将此图像输入到扩散管道中,SD将使用另一个图像到图像的管道来帮助修复锯齿边缘问题。根据扩散模型的本性,背景边缘将被模糊或用其他像素重新渲染。为了干净地去除背景,我们需要其他工具的帮助,例如,Rembg项目[2]。它的使用也很简单:
-
安装包:
pip install rembg
-
使用两行代码去除背景:
from rembg import remove
remove(source_image)
我们可以看到背景被完全去除,如图图18.4所示:
图18.4:使用Rembg去除背景
要将背景设置为白色,使用以下三行代码,如下所示:
from rembg import remove
from PIL import Image
white_bg = Image.new("RGBA", source_image.size, (255,255,255))
image_wo_bg = remove(source_image)
Image.alpha_composite(white_bg, image_wo_bg)
我们可以看到背景被完全替换成了白色背景。具有纯白色背景的物体在某些情况下可能很有用;例如,我们打算将这个物体用作指导嵌入。不,你没有看错;我们可以将图像作为输入提示。让我们在下一节中探讨这个问题。
物体和风格迁移
当我们在第4章和第5章中介绍SD的理论时,我们了解到在UNet扩散过程中只涉及文本嵌入。即使我们提供一个初始图像作为起点,初始图像也仅仅被用作起始噪声或与初始噪声连接。它对扩散过程的步骤没有任何影响。
直到IP-Adapter项目[3]的出现。IP-Adapter是一个工具,它允许您使用现有的图像作为文本提示的参考。换句话说,我们可以将图像作为另一份提示工作,与文本指导一起生成图像。与通常对某些概念或风格效果良好的文本反转不同,IP-Adapter可以与任何图像一起工作。
在IP-Adapter的帮助下,我们可以神奇地将一个物体从一个图像转移到另一个完全不同的图像中。
接下来,让我们开始使用 IP-Adapter 将一个对象从一个图像转移到另一个图像。
加载带有 IP-Adapter 的 Stable Diffusion 流程
在 Diffusers 中使用 IP-Adapter 简单到不需要安装任何额外的包或手动下载任何模型文件:
-
加载图像编码器。正是这个专门图像编码器在将图像转换为引导提示嵌入中扮演了关键角色:
import torch
from transformers import CLIPVisionModelWithProjection
image_encoder = CLIPVisionModelWithProjection.from_pretrained(
"h94/IP-Adapter",
subfolder = "models/image_encoder",
torch_dtype = torch.float16,
).to("cuda:0")
-
加载一个普通的 SD 流程,但增加一个额外的
image_encoder
参数:from diffusers import StableDiffusionImg2ImgPipeline
pipeline = StableDiffusionImg2ImgPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
image_encoder = image_encoder,
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
注意
即使加载 SDXL 流程,我们也将使用 models/image_encoder
中的图像编码器模型,而不是 sdxl_models/image_encoder
;否则,将抛出错误消息。您还可以用任何其他社区共享模型替换 SD v1.5 基础模型。
-
将 IP-Adapter 应用到 UNet 流程:
pipeline.load_ip_adapter(
"h94/IP-Adapter",
Subfolder = "models",
weight_name = "ip-adapter_sd15.bin"
)
如果您使用 SDXL 流程,请将
models
替换为sdxl_models
,并将ip-adapter_sd15.bin
替换为ip-adapter_sdxl.bin
。
就这些了;现在我们可以像使用任何其他流程一样使用这个流程。如果不存在 IP-Adapter 模型,Diffusers 将帮助您自动下载模型文件。在下一节中,我们将使用 IP-Adapter 模型将一个图像的风格转移到另一个图像。
转移风格
在本节中,我们将编写代码将著名画家约翰内斯·维梅尔的 《戴珍珠耳环的少女》(见 图 18**.5)转移到 宇航员骑马 的图像:
图 18.5:约翰内斯·维梅尔的《戴珍珠耳环的少女》
这里,让我们启动流程以转换风格:
from diffusers.utils import load_image
source_image = load_image("./images/clipseg_source_image.png")
ip_image = load_image("./images/vermeer.png")
pipeline.to("cuda:0")
image = pipeline(
prompt = 'best quality, high quality',
negative_prompt = "monochrome,lowres, bad anatomy,low quality" ,
image = source_image,
ip_adapter_image = ip_image ,
num_images_per_prompt = 1 ,
num_inference_steps = 50,
strength = 0.5,
generator = torch.Generator("cuda:0").manual_seed(1)
).images[0]
pipeline.to("cpu")
torch.cuda.empty_cache()
image
在前面的代码中,我们使用了原始宇航员图像 – source_image
– 作为基础,并将油画图像作为 IP-Adapter 图像提示 – ip_image
(我们想要其风格)。令人惊讶的是,我们得到了 图 18**.6 中所示的结果:
图 18.6:宇航员骑着一匹马的新风格
《戴珍珠耳环的少女》 图像的风格和感觉已成功应用于另一张图像。
IP-Adapter 的潜力巨大。我们甚至可以将一个图像中的服装和面部特征转移到另一个图像中。更多使用示例可以在原始 IP-Adapter 仓库 [3] 和 Diffusers PR 页面 [5] 中找到。
摘要
在本章中,重点是使用 SD 进行图像编辑和风格转换。本章介绍了 CLIPSeg 用于图像内容检测、Rembg 用于背景移除以及 IP-Adapter 用于在图像之间转移风格等工具。
第一部分涵盖了图像编辑,特别是替换或移除背景。CLIPSeg 用于生成背景的掩码,然后将其转换为二值掩码。背景要么使用 SD 替换,要么移除,后者会显示锯齿状边缘。Rembg 被介绍为更平滑的背景移除解决方案。
第二部分探讨了使用IP-Adapter进行对象和风格转换。这个过程包括加载图像编码器,将其集成到SD管道中,并将IP-Adapter应用于管道的UNet。章节以将维梅尔的戴珍珠耳环的少女的风格转移到骑马的宇航员图像上的一个示例结束。
在下一章中,我们将探讨如何保存和读取从生成的图像文件中保存和读取参数和提示信息的方法。
参考文献
-
CLIPSeg GitHub仓库:https://github.com/timojl/clipseg
-
Timo Lüddecke 和 Alexander S. Ecker,使用文本和图像提示进行图像分割:https://arxiv.org/abs/2112.10003
-
IP-Adapter GitHub仓库:https://github.com/tencent-ailab/IP-Adapter
-
Rembg,一个用于去除图像背景的工具:https://github.com/danielgatis/rembg
-
IP-Adapters原始样本:https://github.com/huggingface/diffusers/pull/5713
第十九章:生成数据持久化
想象一个 Python 程序生成图像,但当你回到图像希望进行改进或简单地根据原始提示生成新图像时,你找不到确切的提示、推理步骤、指导比例以及其他实际上生成图像的东西!
解决此问题的一个方案是将所有元数据保存到生成的图像文件中。便携式网络图形(PNG)[1] 图像格式为我们提供了一个机制,可以存储与图像像素数据一起的元数据。我们将探讨这个解决方案。
在本章中,我们将探讨以下内容:
-
探索和理解 PNG 文件结构
-
在 PNG 文件中存储稳定扩散生成元数据
-
从 PNG 文件中提取稳定扩散生成元数据
通过采用本章提供的解决方案,您将能够保持图像文件中的生成提示和参数,并提取元信息以供进一步使用。
让我们开始。
探索和理解 PNG 文件结构
在保存图像元数据和稳定扩散生成参数之前,我们最好对为什么选择 PNG 作为输出图像格式以保存稳定扩散的输出有一个全面的了解,以及为什么 PNG 可以支持无限定制的元数据,这对于将大量数据写入图像非常有用。
通过理解 PNG 格式,我们可以自信地将数据写入 PNG 文件,因为我们打算将数据持久化到图像中。
PNG 是一种光栅图形文件格式,是稳定扩散生成的理想图像格式。PNG 文件格式被创建为一个改进的、非专利的无损图像压缩格式,现在在互联网上广泛使用。
除了 PNG,还有其他几种图像格式也支持保存自定义图像元数据,例如 JPEG、TIFF、RAW、DNG 和 BMP。然而,这些格式都有它们的问题和限制。JPEG 文件可以包含自定义的 Exif 元数据,但 JPEG 是一种有损压缩图像格式,通过牺牲图像质量达到高压缩率。DNG 是 Adobe 拥有的专有格式。与 PNG 相比,BMP 的自定义元数据大小有限。
对于 PNG 格式,除了存储额外元数据的能力外,还有很多优点使其成为理想的格式 [1]:
-
无损压缩:PNG 使用无损压缩,这意味着在压缩过程中图像质量不会降低
-
透明度支持:PNG 支持透明度(alpha 通道),允许图像具有透明背景或半透明元素
-
宽色域:PNG 支持 24 位 RGB 颜色,32 位 RGBA 颜色和灰度图像,提供广泛的颜色选项
-
伽玛校正:PNG 支持伽玛校正,有助于在不同设备和平台之间保持一致的色彩
-
渐进显示:PNG 支持交错,允许图像在下载过程中逐步显示。
我们还需要意识到,在某些情况下,PNG 可能不是最佳选择。以下是一些例子:
-
更大的文件大小:与 JPEG 等其他格式相比,PNG 文件可能更大,因为其无损压缩。
-
不支持动画的原生支持:与 GIF 不同,PNG 不支持原生的动画。
-
不适用于高分辨率照片:由于其无损压缩,PNG 不是高分辨率照片的最佳选择,因为文件大小可能比使用有损压缩的 JPEG 等格式大得多。
尽管存在这些限制,PNG 仍然是一种可行的图像格式选择,尤其是对于 Stable Diffusion 的原始图像。
PNG 文件的内部数据结构基于基于块的架构。每个块是一个自包含的单元,它存储有关图像或元数据的特定信息。这种结构允许 PNG 文件存储附加信息,如文本、版权或其他元数据,而不会影响图像数据本身。
PNG 文件由一个签名后跟一系列块组成。以下是 PNG 文件主要组件的简要概述:
-
签名:PNG 文件的前 8 个字节是一个固定的签名(十六进制中的 89 50 4E 47 0D 0A 1A 0A),用于标识文件为 PNG。
-
length
字段。 -
CRC(4 字节):用于错误检测的循环冗余检查(CRC)值,基于块的类型和数据字段计算。
这种结构提供了灵活性和可扩展性,因为它允许在不破坏现有 PNG 解码器兼容性的情况下添加新的块类型。此外,这种 PNG 数据结构使得将几乎无限量的附加元数据插入到图像中成为可能。
接下来,我们将使用 Python 将一些文本数据插入到 PNG 图像文件中。
在 PNG 图像文件中保存额外的文本数据。
首先,让我们使用 Stable Diffusion 生成一个用于测试的图像。与我们在前几章中使用的代码不同,这次我们将使用 JSON 对象来存储生成参数。
加载模型:
import torch
from diffusers import StableDiffusionPipeline
model_id = "stablediffusionapi/deliberate-v2"
text2img_pipe = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype = torch.float16
)
# Then, we define all the parameters that will be used to generate an
# image in a JSON object:
gen_meta = {
"model_id": model_id,
"prompt": "high resolution,
a photograph of an astronaut riding a horse",
"seed": 123,
"inference_steps": 30,
"height": 512,
"width": 768,
"guidance_scale": 7.5
}
现在,让我们使用 Python 的 dict
类型中的 gen_meta
:
text2img_pipe.to("cuda:0")
input_image = text2img_pipe(
prompt = gen_meta["prompt"],
generator = \
torch.Generator("cuda:0").manual_seed(gen_meta["seed"]),
guidance_scale = gen_meta["guidance_scale"],
height = gen_meta["height"],
width = gen_meta["width"]
).images[0]
text2img_pipe.to("cpu")
torch.cuda.empty_cache()
input_image
我们应该有一个使用 input_image
处理生成的图像 – 在 Python 上下文中对图像对象的引用。
接下来,让我们逐步将 gen_meta
数据存储在 PNG 文件中:
-
如果您还没有安装,请安装
pillow
库 [2]:pip install pillow
-
使用以下代码添加一个存储文本信息的块:
from PIL import Image
from PIL import PngImagePlugin
import json
# Open the original image
image = Image.open("input_image.png")
# Define the metadata you want to add
metadata = PngImagePlugin.PngInfo()
gen_meta_str = json.dumps(gen_meta)
metadata.add_text("my_sd_gen_meta", gen_meta_str)
# Save the image with the added metadata
image.save("output_image_with_metadata.png", "PNG",
pnginfo=metadata)
现在,字符串化的
gen_meta
已存储在output_image_with_metadata.png
文件中。请注意,我们首先需要使用json.dumps(gen_meta)
将gen_data
从对象转换为字符串。上述代码向 PNG 文件添加了一个数据块。正如我们在本章开头所学的,PNG 文件是按块堆叠的,这意味着我们应该能够向 PNG 文件中添加任意数量的文本块。在下面的示例中,我们添加了两个块而不是一个:
from PIL import Image
from PIL import PngImagePlugin
import json
# Open the original image
image = input_image#Image.open("input_image.png")
# Define the metadata you want to add
metadata = PngImagePlugin.PngInfo()
gen_meta_str = json.dumps(gen_meta)
metadata.add_text("my_sd_gen_meta", gen_meta_str)
# add a copy right json object
copyright_meta = {
"author":"Andrew Zhu",
"license":"free use"
}
copyright_meta_str = json.dumps(copyright_meta)
metadata.add_text("copy_right", copyright_meta_str)
# Save the image with the added metadata
image.save("output_image_with_metadata.png", "PNG",
pnginfo=metadata)
只需调用另一个
add_text()
函数,我们就可以向 PNG 文件中添加第二个文本块。接下来,让我们从 PNG 图像中提取添加的数据。 -
从 PNG 图像中提取文本数据是直接的。我们将再次使用
pillow
包来完成提取任务:from PIL import Image
image = Image.open("output_image_with_metadata.png")
metadata = image.info
# print the meta
for key, value in metadata.items():
print(f"{key}: {value}")
我们应该看到如下输出:
my_sd_gen_meta: {"model_id": "stablediffusionapi/deliberate-v2", "prompt": "high resolution, a photograph of an astronaut riding a horse", "seed": 123, "inference_steps": 30, "height": 512, "width": 768, "guidance_scale": 7.5}
copy_right: {"author": "Andrew Zhu", "license": "free use"}
通过本节提供的代码,我们应该能够将自定义数据保存到 PNG 图像文件中,并从中检索。
PNG 额外数据存储限制
您可能会想知道文本数据大小是否有任何限制。写入 PNG 文件的元数据量没有具体的限制。然而,基于 PNG 文件结构和用于读取和写入元数据的软件或库的限制,存在实际上的约束。
如我们在第一部分所讨论的,PNG 文件是按块存储的。每个块的最大大小为 2^31 - 1 字节(约 2 GB)。虽然理论上可以在单个 PNG 文件中包含多个元数据块,但在这些块中存储过多或过大的数据可能导致使用其他软件打开图像时出现错误或加载时间变慢。
在实践中,PNG 文件中的元数据通常很小,包含诸如版权、作者、描述或用于创建图像的软件等信息。在我们的案例中,是用于生成图像的稳定扩散参数。不建议在 PNG 元数据中存储大量数据,因为这可能会导致性能问题和与某些软件的兼容性问题。
概述
在本章中,我们介绍了一种将图像生成提示和相对参数存储在 PNG 图像文件中的解决方案,这样生成数据就会随着文件移动,我们可以使用稳定扩散提取参数,以增强或扩展提示以供其他用途使用。
本章介绍了 PNG 文件的文件结构,并提供了示例代码,用于在 PNG 文件中存储多个文本数据块,然后使用 Python 代码从 PNG 文件中提取元数据。
通过解决方案的示例代码,您也将能够从 A1111 的稳定扩散网页界面生成的图像中提取元数据。
在下一章中,我们将为稳定扩散应用程序构建一个交互式网页界面。
参考文献
-
可移植网络图形 (PNG) 规范:https://www.w3.org/TR/png/
第二十章:创建交互式用户界面
在前面的章节中,我们仅使用 Python 代码和 Jupyter Notebook 实现了使用 Stable Diffusion 的各种任务。在某些场景中,我们不仅需要交互式用户界面以便更容易测试,还需要更好的用户体验。
假设我们已经使用 Stable Diffusion 构建了一个应用程序。我们如何将其发布给公众或非技术用户以尝试它?在本章中,我们将使用一个开源的交互式 UI 框架,Gradio [1],来封装 diffusers 代码,并仅使用 Python 提供基于网络的 UI。
本章不会深入探讨 Gradio 的所有使用方面。相反,我们将专注于提供一个高级概述,介绍其基本构建块,所有这些都有特定的目标:展示如何使用 Gradio 构建一个 Stable Diffusion 文本到图像的管道。
在本章中,我们将涵盖以下主题:
-
介绍 Gradio
-
Gradio 基础知识
-
使用 Gradio 构建 Stable Diffusion 文本到图像管道
让我们开始。
介绍 Gradio
Gradio 是一个 Python 库,它使构建机器学习模型和数据科学工作流程的美丽、交互式网络界面变得容易。它是一个高级库,它抽象化了网络开发的细节,这样你就可以专注于构建你的模型和界面。
我们在前面几章中多次提到的 A1111 Stable Diffusion Web UI 使用 Gradio 作为用户界面,许多研究人员使用这个框架来快速展示他们最新的工作。以下是 Gradio 成为主流用户界面的几个原因:
-
易于使用:Gradio 的简单 API 使你只需几行代码就能创建交互式网络界面
-
灵活:Gradio 可以用来创建各种交互式网络界面,从简单的滑块到复杂的聊天机器人
-
可扩展:Gradio 是可扩展的,因此你可以自定义界面的外观和感觉或添加新功能
-
开源:Gradio 是开源的,因此你可以为项目做出贡献或在项目中使用它
Gradio 的另一个特性是其他类似框架中不存在的,即 Gradio 界面可以嵌入到 Python 笔记本中,或作为独立的网页展示(当你看到这个笔记本嵌入功能时,你会知道为什么这个特性很酷)。
如果你已经使用 diffusers 运行过 Stable Diffusion,你的 Python 环境应该已经为 Gradio 准备就绪。如果这是你阅读旅程的第一章,请确保你的机器上已安装 Python 3.8 或更高版本。
现在我们已经了解了 Gradio 是什么,让我们学习如何设置它。
开始使用 Gradio
在本节中,我们将了解启动 Gradio 应用程序所需的最小设置。
-
使用
pip
安装 Gradio:pip install gradio
请确保您更新以下两个包到最新版本:
click
和uvicorn
:pip install -U click
pip install -U uvicorn
-
在 Jupyter Notebook 单元中创建一个单元格,并在单元格中编写或复制以下代码:
import gradio
def greet(name):
return "Hello " + name + "!"
demo = gradio.Interface(
fn = greet,
inputs = "text",
outputs = "text"
)
demo.launch()
执行它不会弹出一个新的网络浏览器窗口。相反,UI 将嵌入到 Jupyter Notebook 的结果面板中。
当然,你可以复制并粘贴本地 URL – http://127.0.0.1:7860
– 到任何本地浏览器中查看。
注意,下次你在另一个 Jupyter Notebook 的单元格中执行代码时,将分配一个新的服务器端口,例如 7861
。Gradio 不会自动回收分配的服务器端口。我们可以使用一行额外的代码 – gr.close_all()
– 来确保在启动之前释放所有活动端口。按照以下代码更新:
import gradio
def greet(name):
return "Hello " + name + "!"
demo = gradio.Interface(
fn = greet,
inputs = "text",
outputs = "text"
)
gradio.close_all()
demo.launch(
server_port = 7860
)
代码和嵌入的 Gradio 界面都将显示在 图 20.1 中:
图 20.1:Gradio UI 嵌入 Jupyter Notebook 单元格
注意,Jupyter Notebook 正在 Visual Studio Code 中运行。它也适用于 Google Colab 或独立安装的 Jupyter Notebook。
或者,我们可以从终端启动 Gradio 应用程序。
在 Jupyter Notebook 中启动网络应用程序对于测试和概念验证演示很有用。当部署应用程序时,我们最好从终端启动它。
创建一个名为 gradio_app.py
的新文件,并使用我们在 步骤 2 中使用的相同代码。使用一个新的端口号,例如 7861
,以避免与已使用的 7860
冲突。然后从终端启动应用程序:
python gradio_app.py
这样就设置好了。接下来,让我们熟悉一下 Gradio 的基本构建块。
Gradio 基础知识
上述示例代码是从 Gradio 官方快速入门教程中改编的。当我们查看代码时,很多细节都被隐藏了。我们不知道 Clear
按钮在哪里,我们没有指定 Submit
按钮,也不知道 Flag
按钮是什么。
在使用 Gradio 进行任何严肃的应用之前,我们需要理解每一行代码,并确保每个元素都在控制之下。
与使用 Interface
函数自动提供布局不同,Blocks
可能为我们提供了更好的方法来使用显式声明添加界面元素。
Gradio Blocks
Interface
函数提供了一个抽象层,可以轻松创建快速演示,但有一个抽象层。简单是有代价的。另一方面,Blocks
是一种低级方法,用于布局元素和定义数据流。借助 Blocks
,我们可以精确控制以下内容:
-
组件的布局
-
触发动作的事件
-
数据流的方向
一个例子将更好地解释它:
import gradio
gradio.close_all()
def greet(name):
return f"hello {name} !"
with gradio.Blocks() as demo:
name_input = gradio.Textbox(label = "Name")
output = gradio.Textbox(label = "output box")
diffusion_btn = gradio.Button("Generate")
diffusion_btn.click(
fn = greet,
inputs = name_input,
outputs = output
)
demo.launch(server_port = 7860)
上一段代码将生成如图 图 20.2 所示的界面:
图 20.2:使用 Blocks 构建 Gradio UI
在 Blocks
下的所有元素都将显示在 UI 中。UI 元素的文本也是由我们定义的。在 click
事件中,我们定义了 fn
事件函数、inputs
和 outputs
。最后,使用 demo.launch(server_port = '7860')
启动应用程序。
遵循 Python 的一个指导原则:“明确优于隐晦”,我们努力使代码清晰简洁。
输入和输出
在 Gradio Blocks 部分的代码中,只使用了一个输入和一个输出。我们可以提供多个输入和输出,如下面的代码所示:
import gradio
gradio.close_all()
def greet(name, age):
return f"hello {name} !", f"You age is {age}"
with gradio.Blocks() as demo:
name_input = gradio.Textbox(label = "Name")
age_input = gradio.Slider(minimum =0,maximum =100,
label ="age slider")
name_output = gradio.Textbox(label = "name output box")
age_output = gradio.Textbox(label = "age output")
diffusion_btn = gradio.Button("Generate")
diffusion_btn.click(
fn = greet,
inputs = [name_input, age_input],
outputs = [name_output, age_output]
)
demo.launch(server_port = 7860)
结果显示在 图 20**.3 中:
图 20.3:带有多个输入和输出的 Gradio UI
简单地将元素堆叠在 with gradio.Blocks() as demo:
之下,并在 list
中提供输入和输出。Gradio 将自动从输入中获取值并将它们转发到 greet
绑定函数。输出将采用相关函数返回的元组值。
接下来,用提示和输出图像组件替换元素。这种方法可以应用于构建基于网页的 Stable Diffusion 管道。然而,在继续之前,我们需要探索如何将进度条集成到我们的界面中。
构建进度条
在 Gradio 中使用进度条,我们可以在相关的事件函数中添加一个 progress
参数。Progress
对象将用于跟踪函数的进度,并以进度条的形式显示给用户。
下面是 Gradio 中使用进度条的示例。
import gradio, time
gradio.close_all()
def my_function(text, progress=gradio.Progress()):
for i in range(10):
time.sleep(1)
progress(i/10, desc=f"{i}")
return text
with gradio.Blocks() as demo:
input = gradio.Textbox()
output = gradio.Textbox()
btn = gradio.Button()
btn.click(
fn = my_function,
inputs = input,
outputs = output
)
demo.queue().launch(server_port=7860)
在前面的代码中,我们使用 progress(i/10, desc=f"{i}")
手动更新进度条。每次休眠后,进度条将前进 10%。
点击 运行 按钮后,进度条将出现在输出文本框的位置。我们将使用类似的方法在下一节中应用 Stable Diffusion 管道的进度条。
使用 Gradio 构建稳定的扩散文本到图像管道
准备就绪后,现在让我们使用 Gradio 构建一个稳定的扩散文本到图像管道。UI 界面将包括以下内容:
-
一个提示输入框
-
一个负提示输入框
-
一个带有
Generate
标签的按钮 -
点击 生成 按钮时的进度条
-
一个输出图像
下面是实现这五个元素的代码:
import gradio
gradio.close_all(verbose = True)
import torch
from diffusers import StableDiffusionPipeline
text2img_pipe = StableDiffusionPipeline.from_pretrained(
"stablediffusionapi/deliberate-v2",
torch_dtype = torch.float16,
safety_checker = None
).to("cuda:0")
def text2img(
prompt:str,
neg_prompt:str,
progress_bar = gradio.Progress()
):
return text2img_pipe(
prompt = prompt,
negative_prompt = neg_prompt,
callback = (
lambda step,
timestep,
latents: progress_bar(step/50,desc="denoising")
)
).images[0]
with gradio.Blocks(
theme = gradio.themes.Monochrome()
) as sd_app:
gradio.Markdown("# Stable Diffusion in Gradio")
prompt = gradio.Textbox(label="Prompt", lines = 4)
neg_prompt = gradio.Textbox(label="Negative Prompt", lines = 2)
sd_gen_btn = gradio.Button("Generate Image")
output_image = gradio.Image()
sd_gen_btn.click(
fn = text2img,
inputs = [prompt, neg_prompt],
outputs = output_image
)
sd_app.queue().launch(server_port = 7861)
在前面的代码中,我们首先启动 text2img_pipe
管道到 VRAM,然后创建一个 text2img
函数,该函数将由 Gradio 事件按钮调用。注意 lambda
表达式:
callback = (
lambda step, timestep, latents:
progress_bar(step/50, desc="denoising")
)
我们将进度条传递到 diffusers 的去噪循环中。然后,每个去噪步骤将更新进度条。
代码的最后部分是 Gradio 元素 Block
堆栈。代码还给了 Gradio 一个新的主题:
...
with gradio.Blocks(
theme = gradio.themes.Monochrome()
) as sd_app:
...
现在,你应该能够在 Jupyter Notebook 和任何本地网页浏览器中运行代码并生成一些图像。
进度条和结果显示在 图 20**.4 中:
图 20.4:带有进度条的 Gradio UI
你可以向这个示例应用程序添加更多元素和功能。
概述
在撰写本章(2023年12月)时,关于使用 Gradio 与 diffusers 开始的信息或示例代码并不多。我们编写本章是为了帮助快速构建一个 Web UI 的 Stable Diffusion 应用程序,这样我们就可以在几分钟内与他人分享结果,而不需要接触一行 HTML、CSS 或 JavaScript,在整个构建过程中使用纯 Python。
本章介绍了 Gradio,它所能做到的事情以及它为何如此受欢迎。我们没有详细讨论 Gradio 的每一个功能;我们相信它的官方文档[1]在这方面做得更好。相反,我们用一个简单的例子来解释 Gradio 的核心以及构建一个使用 Gradio 的 Stable Diffusion Web UI 所需准备的内容。
最后,我们一次性介绍了 Blocks
、inputs
、outputs
、进度条和事件绑定,并在 Gradio 中构建了一个虽小但功能齐全的 Stable Diffusion 管道。
在下一章中,我们将深入探讨一个相对复杂的话题:模型微调和 LoRA 训练。
参考文献
-
Gradio:使用 Python 构建机器学习 Web 应用 — https://github.com/gradio-app/gradio
-
Gradio 快速入门:https://www.gradio.app/guides/quickstart
第二十一章:扩散模型迁移学习
本书主要关注使用Python进行Stable Diffusion,这样做时,我们需要针对我们的特定需求微调模型。正如我们在前面的章节中讨论的那样,有许多方法可以定制模型,例如以下方法:
-
解锁UNet以微调所有参数
-
训练一个文本反转以添加新的关键词嵌入
-
锁定UNet并训练一个LoRA模型以定制样式
-
训练一个ControlNet模型以使用控制引导来指导图像生成
-
训练一个适配器,将图像作为指导嵌入之一
在一个简单的章节中涵盖所有模型训练主题是不可能的。需要另一本书来讨论模型训练的细节。
尽管如此,我们仍然希望利用本章深入探讨模型训练的核心概念。我们不想列出如何微调扩散模型的示例代码,或者使用Diffusers
软件包中的脚本,而是想介绍训练的核心概念,以便你完全理解常见的训练过程。在本章中,我们将涵盖以下主题:
-
通过使用PyTorch从头开始训练线性模型来介绍模型训练的基础知识
-
介绍Hugging Face Accelerate软件包以在多个GPU上训练模型
-
逐步构建使用PyTorch和Accelerator训练Stable Diffusion V1.5 LoRA模型的代码
到本章结束时,你将熟悉整体训练过程和关键概念,并且能够阅读来自其他存储库的示例代码,构建自己的训练代码以从预训练模型定制模型。
编写训练一个模型的代码是学习如何训练模型的最佳方式。让我们开始工作吧。
技术要求
训练模型比模型推理需要更多的GPU功率和VRAM。准备一个至少有8GB VRAM的GPU – 越多越好。你也可以使用多个GPU来训练模型。
建议安装以下软件包的最新版本:
pip install torch torchvision torchaudio
pip install bitsandbytes
pip install transformers
pip install accelerate
pip install diffusers
pip install peft
pip install datasets
这里列出了我编写代码示例时使用的指定软件包及其版本:
pip install torch==2.1.2 torchvision==0.16.1 torchaudio==2.1.1
pip install bitsandbytes==0.41.0
pip install transformers==4.36.1
pip install accelerate==0.24.1
pip install diffusers==0.26.0
pip install peft==0.6.2
pip install datasets==2.16.0
训练代码在Ubuntu 22.04 x64版本上进行了测试。
使用PyTorch训练神经网络模型
本节的目标是使用PyTorch构建和训练一个简单的神经网络模型。这个模型将是一个单层模型,没有额外的复杂层。它很简单,但包含了训练Stable Diffusion LoRA所需的所有元素,正如我们将在本章后面看到的那样。
如果你熟悉PyTorch模型训练,可以跳过这一节。如果你是第一次开始训练模型,这个简单的模型训练将帮助你彻底理解模型训练的过程。
在开始之前,请确保你已经安装了技术要求部分中提到的所有必需的软件包。
准备训练数据
假设我们想要训练一个具有四个权重并输出一个数字结果的模型,如下所示:
y = w1 × x1 + w2 × x2 + w3 × x3 + w4 × x4
四个权重w1、w2、w3、w4是我们希望从训练数据中获得的模型权重(将这些权重视为Stable Diffusion模型的权重)。由于我们需要一些真实数据来训练模型,我将使用权重[2,3,4,7]
来生成一些样本数据:
import numpy as np
w_list = np.array([2,3,4,7])
让我们创建10组输入样本数据x_sample
;每个x_sample
是一个包含四个元素的数组,与权重的长度相同:
import random
x_list = []
for _ in range(10):
x_sample = np.array([random.randint(1,100) for _ in range(
len(w_list))])
x_list.append(x_sample)
在以下部分,我们将使用神经网络模型来预测一系列权重;为了训练的目的,让我们假设在生成训练数据后,真实的权重是未知的。
在前面的代码片段中,我们利用numpy
利用其点积运算符@
来计算输出y
。现在,让我们生成包含10
个元素的y_list
:
y_list = []
for x_sample in x_list:
y_temp = x_sample@w_list
y_list.append(y_temp)
您可以打印x_list
和y_list
来查看训练数据。
我们的训练数据已经准备好了;不需要下载其他任何东西。接下来,让我们定义模型本身并准备训练。
准备训练
我们的模式可能是世界上最简单的模型,一个简单的线性点积,如下面的代码所示:
import torch
import torch.nn as nn
class MyLinear(nn.Module):
def __init__(self):
super().__init__()
self.w = nn.Parameter(torch.randn(4))
def forward(self, x:torch.Tensor):
return self.w @ x
torch.randn(4)
代码是用来生成一个包含四个权重数字的张量。不需要其他代码;我们的NN模型现在准备好了,命名为MyLinear
。
要训练一个模型,我们需要初始化它,类似于在LLM或扩散模型中初始化随机权重:
model = MyLinear()
几乎所有神经网络模型训练都遵循以下步骤:
-
前向传递以预测结果。
-
计算预测结果和真实结果之间的差异,即损失值。
-
执行反向传播以计算梯度损失值。
-
更新模型参数。
在开始训练之前,定义一个损失函数和一个优化器。损失函数loss_fn
将帮助根据预测结果和真实结果计算损失值。optimizer
将用于更新权重。
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.00001)
lr
代表学习率,这是一个关键的超参数需要设置。确定最佳学习率(lr)通常涉及试错,这取决于您模型的特性、数据集和问题。为了找到一个合理的学习率,您需要执行以下操作:
-
从较小的学习率开始:一种常见的做法是从较小的学习率开始,例如0.001,并根据观察到的收敛行为逐渐增加或减少它。
-
使用学习率调度器:您可以在训练过程中动态调整学习率。一种常见的方法是步长衰减,即在固定数量的epoch后学习率降低。另一种流行的方法是指数衰减,其中学习率随时间指数下降。(我们不会在世界上最简单的模型中使用它。)
此外,别忘了将输入和输出转换为torch Tensor对象:
x_input = torch.tensor(x_list, dtype=torch.float32)
y_output = torch.tensor(y_list, dtype=torch.float32)
所有准备工作都已完成,让我们开始训练一个模型。
训练模型
我们将设置迭代次数为100,这意味着将训练数据循环100次:
# start train model
num_epochs = 100
for epoch in range(num_epochs):
for i, x in enumerate(x_input):
# forward
y_pred = model(x)
# calculate loss
loss = loss_fn(y_pred,y_output[i])
# zero out the cached parameter.
optimizer.zero_grad()
# backward
loss.backward()
# update paramters
optimizer.step()
if (epoch+1) % 10 == 0:
print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch+1,
num_epochs, loss.item()))
print("train done")
让我们分解前面的代码:
-
y_pred = model(x)
: 这一行将模型应用于当前输入数据样本x
,生成预测y_pred
。 -
loss = loss_fn(y_pred,y_output[i])
: 这一行通过比较预测输出y_pred
与实际输出y_output[i]
,使用指定的损失函数loss_fn
来计算损失(也称为误差或成本)。 -
optimizer.zero_grad()
: 这一行将反向传播期间计算的梯度重置为零。这很重要,因为它防止梯度值在不同样本之间传递。 -
loss.backward()
: 这一行执行反向传播算法,计算所有参数相对于损失的梯度。 -
optimizer.step()
: 这一行根据计算出的梯度和选择的优化方法更新模型的参数。
将所有代码合并并运行,我们将看到以下输出:
Epoch [10/100], Loss: 201.5572
Epoch [20/100], Loss: 10.8380
Epoch [30/100], Loss: 3.5255
Epoch [40/100], Loss: 1.7397
Epoch [50/100], Loss: 0.9160
Epoch [60/100], Loss: 0.4882
Epoch [70/100], Loss: 0.2607
Epoch [80/100], Loss: 0.1393
Epoch [90/100], Loss: 0.0745
Epoch [100/100], Loss: 0.0398
train done
损失值快速收敛并接近0
,经过100
个迭代周期后。执行以下代码以查看当前权重:
model.w
你可以看到权重更新如下:
Parameter containing:
tensor([1.9761, 3.0063, 4.0219, 6.9869], requires_grad=True)
这非常接近 [2,3,4,7]
!模型成功训练以找到正确的权重数字。
在Stable Diffusion和多GPU训练的情况下,我们可以从Hugging Face Accelerate包[4]中获得帮助。让我们开始使用Accelerate
。
使用Hugging Face的Accelerate训练模型
Hugging Face的Accelerate
是一个库,它提供了对不同的PyTorch分布式框架的高级API,旨在简化分布式和混合精度训练的过程。它旨在将训练循环中的更改保持在最低限度,并允许相同的函数适用于任何分布式设置。让我们看看Accelerate
能带来什么。
应用Hugging Face的Accelerate
让我们将Accelerate
应用于我们简单但有效的模型。Accelerate旨在与PyTorch一起使用,因此我们不需要更改太多代码。以下是使用Accelerate
训练模型的步骤:
-
生成默认配置文件:
from accelerate import utils
utils.write_basic_config()
-
初始化一个
Accelerate
实例,并将模型实例和数据发送到Accelerate管理的设备:from accelerate import Accelerator
accelerator = Accelerator()
device = accelerator.device
x_input.to(device)
y_output.to(device)
model.to(device)
-
将
loss.backward
替换为accelerator.backward(loss)
:# loss.backward
accelerator.backward(loss)
接下来,我们将使用Accelerate更新训练代码。
合并代码
我们将保持所有其他代码不变;以下是完整的训练代码,除了数据准备和模型初始化:
# start train model using Accelerate
from accelerate import utils
utils.write_basic_config()
from accelerate import Accelerator
accelerator = Accelerator()
device = accelerator.device
x_input.to(device)
y_output.to(device)
model.to(device)
model, optimizer = accelerator.prepare(
model, optimizer
)
num_epochs = 100
for epoch in range(num_epochs):
for i, x in enumerate(x_input):
# forward
y_pred = model(x)
# calculate loss
loss = loss_fn(y_pred,y_output[i])
# zero out the cached parameter.
optimizer.zero_grad()
# backward
#loss.backward()
accelerator.backward(loss)
# update paramters
optimizer.step()
if (epoch+1) % 10 == 0:
print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch+1,
num_epochs, loss.item()))
print("train done")
运行前面的代码,我们应该得到与运行不带Hugging Face Accelerate
库的训练模型相同的输出。并且损失值也会收敛。
使用Accelerate在多个GPU上训练模型
有许多种多 GPU 训练的方式;在我们的案例中,我们将使用数据并行风格 [1]。简单来说,我们将整个模型数据加载到每个 GPU 中,并将训练数据分配到多个 GPU 上。
在 PyTorch 中,我们可以使用以下代码实现:
import torch.nn as nn
from torch.nn.parallel import DistributedDataParallel
model = MyLinear()
ddp_model = DistributedDataParallel(model)
# Hugging Face Accelerate wraps this operation automatically using the prepare() function like this:
from accelerate import Accelerator
accelerator = Accelerator()
model = MyLinear()
model = accelerator.prepare(model)
对于世界上最简单的模型,我们将整个模型加载到每个 GPU 中,并将 10 组训练数据分成每组 5 组。每个 GPU 将同时处理五组数据。在每一步之后,所有损失梯度数值将通过 allreduce
操作合并。allreduce
操作简单地将所有 GPU 的损失数据相加,然后将其发送回每个 GPU 以更新权重,如下面的 Python 代码所示:
def allreduce(data):
for i in range(1, len(data)):
data[0][:] += data[i].to(data[0].device)
for i in range(1, len(data)):
data[i][:] = data[0].to(data[i].device)
Accelerate 将启动两个独立的过程来训练。为了避免创建两个训练数据集,让我们生成一个数据集,并使用 pickle
包将其保存到本地存储:
import numpy as np
w_list = np.array([2,3,4,7])
import random
x_list = []
for _ in range(10):
x_sample = np.array([random.randint(1,100)
for _ in range(len(w_list))]
)
x_list.append(x_sample)
y_list = []
for x_sample in x_list:
y_temp = x_sample@w_list
y_list.append(y_temp)
train_obj = {
'w_list':w_list.tolist(),
'input':x_list,
'output':y_list
}
import pickle
with open('train_data.pkl','wb') as f:
pickle.dump(train_obj,f)
然后,将整个模型和训练代码包裹在一个 main
函数中,并将其保存到一个名为 train_model_in_2gpus.py
的新 Python 文件中:
import torch
import torch.nn as nn
from accelerate import utils
from accelerate import Accelerator
# start a accelerate instance
utils.write_basic_config()
accelerator = Accelerator()
device = accelerator.device
def main():
# define the model
class MyLinear(nn.Module):
def __init__(self):
super().__init__()
self.w = nn.Parameter(torch.randn(len(w_list)))
def forward(self, x:torch.Tensor):
return self.w @ x
# load training data
import pickle
with open("train_data.pkl",'rb') as f:
loaded_object = pickle.load(f)
w_list = loaded_object['w_list']
x_list = loaded_object['input']
y_list = loaded_object['output']
# convert data to torch tensor
x_input = torch.tensor(x_list, dtype=torch.float32).to(device)
y_output = torch.tensor(y_list, dtype=torch.float32).to(device)
# initialize model, loss function, and optimizer
Model = MyLinear().to(device)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.00001)
# wrap model and optimizer using accelerate
model, optimizer = accelerator.prepare(
model, optimizer
)
num_epochs = 100
for epoch in range(num_epochs):
for i, x in enumerate(x_input):
# forward
y_pred = model(x)
# calculate loss
loss = loss_fn(y_pred,y_output[i])
# zero out the cached parameter.
optimizer.zero_grad()
# backward
#loss.backward()
accelerator.backward(loss)
# update paramters
optimizer.step()
if (epoch+1) % 10 == 0:
print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch+1,
num_epochs, loss.item()))
# take a look at the model weights after trainning
model = accelerator.unwrap_model(model)
print(model.w)
if __name__ == "__main__":
main()
然后,使用以下命令开始训练:
accelerate launch --num_processes=2 train_model_in_2gpus.py
你应该看到类似这样的内容:
Parameter containing:
tensor([1.9875, 3.0020, 4.0159, 6.9961], device='cuda:0', requires_grad=True)
如果是这样,恭喜你!你刚刚在两个 GPU 上成功训练了一个 AI 模型。现在,让我们利用你所学到的知识开始训练一个稳定的扩散 V1.5 LoRA。
训练一个稳定的扩散 V1.5 LoRA
Hugging Face 文档提供了通过调用 Diffusers 提供的预定义脚本 [2] 来训练 LoRA 的完整指导。然而,我们不想仅仅停留在“使用”脚本上。Diffusers 的训练代码中包含了许多边缘情况处理和难以阅读和学习的额外代码。在本节中,我们将逐行编写训练代码,以全面了解每个步骤中发生的情况。
在下面的示例中,我们将使用八张带有相关标题的图片来训练一个 LoRA。图片和图片标题包含在本章代码的 train_data
文件夹中。
我们的训练代码结构将如下所示:
# import packages
import torch
from accelerate import utils
from accelerate import Accelerator
from diffusers import DDPMScheduler,StableDiffusionPipeline
from peft import LoraConfig
from peft.utils import get_peft_model_state_dict
from datasets import load_dataset
from torchvision import transforms
import math
from diffusers.optimization import get_scheduler
from tqdm.auto import tqdm
import torch.nn.functional as F
from diffusers.utils import convert_state_dict_to_diffusers
# train code
def main():
accelerator = Accelerator(
gradient_accumulation_steps = gradient_accumulation_steps,
mixed_precision = "fp16"
)
Device = accelerator.device
...
# almost all training code will be land inside of this main function.
if __name__ == "__main__":
main()
在 main()
函数下方,我们初始化 accelerate
实例。Accelerator
实例使用两个超参数进行初始化:
-
gradient_accumulation_steps
:这是在更新模型参数之前要累积梯度的训练步数。梯度累积允许你使用比单个 GPU 可能实现的更大的批大小进行有效训练,同时仍然将模型参数拟合到内存中。 -
mixed_precision
:这指定了训练期间要使用的精度。"fp16"
值表示将使用半精度浮点值进行中间计算,这可以导致更快的训练时间和更低的内存使用。
Accelerator
实例还有一个属性 device,它是在模型将要训练的设备(GPU 或 CPU)。device 属性可以在训练之前将模型和张量移动到适当的设备。
现在,让我们开始定义超参数。
定义训练超参数
超参数是那些不是从数据中学习,而是在学习过程开始之前设置的参数。它们是用户定义的设置,用于控制机器学习算法的训练过程。在我们的 LoRA 训练案例中,我们将有以下设置:
# hyperparameters
output_dir = "."
pretrained_model_name_or_path = "runwayml/stable-diffusion-v1-5"
lora_rank = 4
lora_alpha = 4
learning_rate = 1e-4
adam_beta1, adam_beta2 = 0.9, 0.999
adam_weight_decay = 1e-2
adam_epsilon = 1e-08
dataset_name = None
train_data_dir = "./train_data"
top_rows = 4
output_dir = "output_dir"
resolution = 768
center_crop = True
random_flip = True
train_batch_size = 4
gradient_accumulation_steps = 1
num_train_epochs = 200
# The scheduler type to use. Choose between ["linear", "cosine", # "cosine_with_restarts", "polynomial","constant", "constant_with_
# warmup"]
lr_scheduler_name = "constant" #"cosine"#
max_grad_norm = 1.0
diffusion_scheduler = DDPMScheduler
让我们分解前面的设置:
-
output_dir
:这是模型输出将被保存的目录。 -
pretrained_model_name_or_path
:这是用作训练起始点的预训练模型的名称或路径。 -
lora_rank
:这是32
层可能不足以有效,而高于256
的 Rank 对于大多数任务可能过于冗余。在我们的案例中,因为我们只使用八个图像来训练 LoRA,将 Rank 设置为4
就足够了。 -
lora_alpha
:相反,这控制了在微调过程中对预训练模型权重进行的更新强度。具体来说,微调期间生成的权重变化乘以一个缩放因子,该因子等于 Alpha 除以 Rank,然后将其添加回原始模型权重。因此,相对于 Rank 增加Alpha。将 Alpha 设置为 Rank 是一个常见的起始实践。 -
learning_rate
:此参数控制模型在训练过程中从错误中学习的速度。具体来说,它设置了每次迭代的步长,决定了模型调整其参数以最小化loss
函数的积极性。 -
adam_beta1
和adam_beta2
:这些是在 Adam 优化器中使用的参数,分别用于控制梯度移动平均和平方梯度的衰减率。 -
adam_weight_decay
:这是 Adam 优化器中使用的权重衰减,用于防止过拟合。 -
adam_epsilon
:这是在 Adam 优化器中添加到分母的小值,以增加数值稳定性。 -
dataset_name
:这是用于训练的数据集的名称。特别是,这是 Hugging Face 数据集 ID,例如lambdalabs/pokemon-blip-captions
。 -
train_data_dir
:这是存储训练数据的目录。 -
top_rows
:这是用于训练的行数。它用于选择训练的顶部行;如果你有一个包含 1,000 行的数据集,将其设置为 8 以训练顶部 8 行的训练代码。 -
output_dir
:这是在训练过程中保存输出的目录。 -
resolution
:这是输入图像的分辨率。 -
center_crop
:这是一个布尔标志,表示是否对输入图像执行中心裁剪。 -
random_flip
:这是一个布尔标志,表示是否对输入图像执行随机水平翻转。 -
train_batch_size
:这是训练过程中使用的批量大小。 -
gradient_accumulation_steps
:这是在更新模型参数之前需要累积梯度的训练步数。 -
num_train_epochs
:这是要执行的训练轮数。 -
lr_scheduler_name
:这是要使用的学习率调度器的名称。 -
max_grad_norm
:这是要剪裁以防止梯度爆炸的最大梯度范数。 -
diffusion_scheduler
:这是要使用的扩散调度器的名称。
准备 Stable Diffusion 组件
当训练 LoRA 时,涉及推理、添加损失值和反向传播的过程——这是一个类似于推理过程的过程。为了便于此,让我们使用来自 Diffusers
包的 StableDiffusionPipeline
来获取 tokenizer
、text_encoder
、vae
和 unet
:
noise_scheduler = DDPMScheduler.from_pretrained(
pretrained_model_name_or_path, subfolder="scheduler")
weight_dtype = torch.float16
pipe = StableDiffusionPipeline.from_pretrained(
pretrained_model_name_or_path,
torch_dtype = weight_dtype
).to(device)
tokenizer, text_encoder = pipe.tokenizer, pipe.text_encoder
vae, unet = pipe.vae, pipe.unet
在 LoRA 训练期间,这些组件将促进前向传递,但它们的权重在反向传播期间不会更新,因此我们需要将 requires_grad_
设置为 False
,如下所示:
# freeze parameters of models, we just want to train a LoRA only
unet.requires_grad_(False)
vae.requires_grad_(False)
text_encoder.requires_grad_(False)
LoRA 权重是我们想要训练的部分;让我们使用 PEFT 的 [3] LoraConfig
来初始化 LoRA 配置。
PEFT
是由 Hugging Face 开发的库,它提供了参数高效的途径来适应大型预训练模型到特定的下游应用。PEFT 背后的关键思想是只微调模型参数的一小部分,而不是全部微调,从而在计算和内存使用方面节省了大量资源。这使得即使在资源有限的消费级硬件上也能微调非常大的模型。
LoRA 是 PEFT 库支持的 PEFT 方法之一。使用 LoRA,在微调过程中,不是更新给定层的所有权重,而是学习权重更新的低秩近似,从而减少了每层所需的额外参数数量。这种方法允许你仅微调模型总参数的 0.16%,同时实现与完全微调相似的性能。
要使用 LoRA 与预训练的 Transformer 模型,你需要实例化一个 LoraConfig
对象并将其传递到模型适当的组件中。LoraConfig
类有几个属性来控制其行为,包括分解的维度/秩、dropout 率和其他超参数。一旦配置完成,你就可以使用标准技术,如梯度下降来训练你的模型。以下是创建 LoRA 配置对象的代码:
# configure LoRA parameters use PEFT
unet_lora_config = LoraConfig(
r = lora_rank,
lora_alpha = lora_alpha,
init_lora_weights = "gaussian",
target_modules = ["to_k", "to_q", "to_v", "to_out.0"]
)
接下来,让我们使用 unet_lora_config
配置将 LoRA 适配器添加到 UNet 模型中:
# Add adapter and make sure the trainable params are in float32.
unet.add_adapter(unet_lora_config)
for param in unet.parameters():
# only upcast trainable parameters (LoRA) into fp32
if param.requires_grad:
param.data = param.to(torch.float32)
在 for
循环内部,如果参数需要梯度(即它们是可训练的),它们的数据类型将被显式转换为 torch.float32
。这确保了只有可训练的参数以 float32
格式存在,以便于高效训练。
加载训练数据
让我们使用以下代码加载数据:
if dataset_name:
# Downloading and loading a dataset from the hub. data will be
# saved to ~/.cache/huggingface/datasets by default
dataset = load_dataset(dataset_name)
else:
dataset = load_dataset(
"imagefolder",
data_dir = train_data_dir
)
train_data = dataset["train"]
dataset["train"] = train_data.select(range(top_rows))
# Preprocessing the datasets. We need to tokenize inputs and targets.
dataset_columns = list(dataset["train"].features.keys())
image_column, caption_column = dataset_columns[0],dataset_columns[1]
让我们分解前面的代码:
-
if dataset_name:
:如果提供了dataset_name
,代码将尝试使用load_dataset
函数从 Hugging Face 的数据集库中加载数据集。如果没有提供dataset_name
,它假定数据集是本地存储的,并使用imagefolder
数据集类型来加载它。 -
train_data = dataset["train"]
: 将数据集的训练部分分配给train_data
变量。 -
dataset["train"] = train_data.select(range(top_rows))
: 选择训练数据集的前top行并将其分配回数据集的训练部分。当需要快速实验时,与数据集的小子集一起使用时很有用。 -
dataset_columns = list(dataset["train"].features.keys())
: 从dataset["train"]
特征字典中提取键并将其分配给dataset_columns
变量。这些键代表数据集中的图像和标题列。 -
image_column, caption_column = dataset_columns[0], dataset_columns[1]
: 将第一列和第二列分别分配给image_column
和caption_column
变量。这假设数据集恰好有两列——第一列用于图像,第二列用于标题。
我们需要一个函数将输入文本转换为token ID;我们定义这个函数如下:
def tokenize_captions(examples, is_train=True):
'''Preprocessing the datasets.We need to tokenize input captions and transform the images.'''
captions = []
for caption in examples[caption_column]:
if isinstance(caption, str):
captions.append(caption)
inputs = tokenizer(
captions,
max_length = tokenizer.model_max_length,
padding = "max_length",
truncation = True,
return_tensors = "pt"
)
return inputs.input_ids
然后,我们训练数据转换管道:
# Preprocessing the datasets.
train_transforms = transforms.Compose(
[
transforms.Resize(
resolution,
interpolation=transforms.InterpolationMode.BILINEAR
),
transforms.CenterCrop(resolution) if center_crop else
transforms.RandomCrop(resolution),
transforms.RandomHorizontalFlip() if random_flip else
transforms.Lambda(lambda x: x),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]) # [0,1] -> [-1,1]
]
)
上述代码定义了一组图像转换,这些转换将在训练机器学习或深度学习模型时应用于训练数据集。这些转换是通过PyTorch
库中的transforms
模块定义的。
下面是每行代码的作用说明:
-
transforms.Compose()
: 这是一个将多个转换“链式连接”起来的函数。它接受一个转换函数列表作为输入,并按顺序应用它们。 -
transforms.Resize(resolution, interpolation=transforms.InterpolationMode.BILINEAR)
: 这行代码将图像调整到指定的分辨率像素,同时保持宽高比。使用的插值方法是双线性插值。 -
transforms.CenterCrop(resolution) if center_crop else transforms.RandomCrop(resolution)
: 这行代码将图像裁剪为分辨率x分辨率的正方形。如果center_crop
为True
,则从图像中心进行裁剪。如果center_crop
为False
,则随机裁剪。 -
transforms.RandomHorizontalFlip() if random_flip else transforms.Lambda(lambda x: x)
: 这行代码以0.5的概率随机水平翻转图像。如果random_flip
为False
,则保持图像不变。 -
transforms.ToTensor()
: 这行代码将图像从PIL图像或NumPy数组转换为PyTorch张量。 -
transforms.Normalize([0.5], [0.5])
: 这行代码将图像的像素值缩放到-1和1之间。在将图像数据传递给神经网络之前,通常用于归一化图像数据。
通过使用transforms.Compose
将这些转换链式连接起来,你可以轻松地预处理图像数据并对数据集应用多个转换。
我们需要以下代码来使用链式转换对象:
def preprocess_train(examples):
'''prepare the train data'''
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
# only do this in the main process
with accelerator.main_process_first():
# Set the training transforms
train_dataset = dataset["train"].with_transform(preprocess_train)
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 = train_batch_size
)
之前的代码首先定义了一个名为 preprocess_train
的函数,该函数用于预处理训练数据。它首先将图像转换为 RGB 格式,然后使用 train_transforms
对象对这些图像应用一系列图像变换(调整大小、居中/随机裁剪、随机水平翻转和归一化)。接着,使用 tokenize_captions
函数对输入的标题进行标记化。处理后的预训练数据被添加到 examples
字典中,作为 pixel_values
和 input_ids
键。
使用 accelerator.main_process_first()
行是为了确保代码块内的代码仅在主进程中执行。在这种情况下,它设置了 train_dataset
的训练变换。
collate_fn
函数用于将数据集示例收集到一个批次中,以便输入模型。它接受一个示例列表,并将 pixel_values
和 input_ids
堆叠在一起。然后,得到的张量被转换为 float32
格式,并作为字典返回。
最后,使用 torch.utils.data.DataLoader
类创建了 train_dataloader
,它以指定的批量大小、打乱和收集函数加载 train_dataset
。
在 PyTorch 中,DataLoader 是一个实用类,它抽象了在训练或评估时批量加载数据的过程。它用于批量加载数据,这些数据点是用于训练机器学习模型的序列数据点。
在提供的代码中,train_dataloader
是 PyTorch 的 DataLoader
类的一个实例。它用于批量加载数据。更具体地说,它以预定义的批量大小从 train_dataset
中批量加载数据,在每个训练周期中打乱数据,并在将数据输入模型之前应用用户定义的 collate_fn
函数来预处理数据。
train_dataloader
对于模型的效率训练是必要的。通过批量加载数据,它允许模型并行处理多个数据点,这可以显著减少训练时间。此外,在每个训练周期中打乱数据有助于通过确保模型在每个周期中看到不同的数据点来防止过拟合。
在提供的代码中,collate_fn
函数用于在将数据输入模型之前对其进行预处理。它接受一个示例列表,并返回一个包含每个示例的像素值和输入 ID 的字典。collate_fn
函数在 DataLoader
将数据输入模型之前应用于每个数据批次。这允许通过将相同的预处理步骤应用于每个数据批次来更有效地处理数据。
定义训练组件
为了准备和定义训练组件,我们首先初始化一个 AdamW
优化器。AdamW
是一种用于训练机器学习模型的优化算法。它是流行的 Adam
优化器的一个变体,为每个模型参数使用自适应学习率。AdamW
优化器与 Adam
优化器类似,但在梯度更新步骤中包含一个额外的权重衰减项。这个权重衰减项在优化过程中添加到损失函数的梯度中,通过向损失函数添加正则化项来帮助防止过拟合。
我们可以使用以下代码初始化一个 AdamW
优化器:
# initialize optimizer
lora_layers = filter(lambda p: p.requires_grad, unet.parameters())
optimizer = torch.optim.AdamW(
lora_layers,
lr = learning_rate,
betas = (adam_beta1, adam_beta2),
weight_decay = adam_weight_decay,
eps = adam_epsilon
)
filter
函数用于遍历 unet
模型的所有参数,并仅选择那些需要梯度计算的参数。filter
函数返回一个包含需要梯度计算参数的生成器对象。这个生成器对象被分配给 lora_layers
变量,该变量将在训练过程中用于优化模型参数。
AdamW
优化器使用以下超参数初始化:
-
lr
:学习率,它控制每次迭代中向损失函数最小值移动的步长 -
betas
:一个包含梯度移动平均的指数衰减率(β1)和平方梯度的指数衰减率(β2)的元组 -
weight_decay
:在优化过程中添加到损失函数梯度的权重衰减项 -
eps
:添加到分母中的一个小值,以提高数值稳定性
其次,我们定义一个学习率调度器 - lr_scheduler
。我们不必手动定义,可以使用 Diffusers
包提供的 get_scheduler
函数(from diffusers.optimization import get_scheduler
):
# learn rate scheduler from diffusers's get_scheduler
lr_scheduler = get_scheduler(
lr_scheduler_name,
optimizer = optimizer
)
此代码使用 Diffusers
库中的 get_scheduler
函数创建一个学习率调度器对象。学习率调度器确定学习率(即梯度下降中的步长)在训练期间如何变化。
get_scheduler
函数接受两个参数:
-
lr_scheduler_name
:要使用的学习率调度器算法的名称。在我们的示例中,名称是constant
,在代码开头定义。 -
optimizer
:学习率调度器将应用到的 PyTorch 优化器对象。这是我们刚刚初始化的AdamW
优化器。
我们已经准备好了启动训练的所有元素,并且已经编写了大量代码来准备数据集,尽管实际的训练代码并不长。接下来,让我们编写训练代码。
训练 Stable Diffusion V1.5 LoRA
训练 LoRA 通常需要一段时间,我们最好创建一个进度条来跟踪训练进度:
# set step count and progress bar
max_train_steps = num_train_epochs*len(train_dataloader)
progress_bar = tqdm(
range(0, max_train_steps),
initial = 0,
desc = "Steps",
# Only show the progress bar once on each machine.
Disable = not accelerator.is_local_main_process,
)
下面是核心训练代码:
# start train
for epoch in range(num_train_epochs):
unet.train()
train_loss = 0.0
for step, batch in enumerate(train_dataloader):
# step 1\. Convert images to latent space
# latents = vae.encode(batch["pixel_values"].to(
dtype=weight_dtype)).latent_dist.sample()
latents = latents * vae.config.scaling_factor
# step 2\. Sample noise that we'll add to the latents,
latents provide the shape info.
noise = torch.randn_like(latents)
# step 3\. Sample a random timestep for each image
batch_size = latents.shape[0]
timesteps = torch.randint(
low = 0,
high = noise_scheduler.config.num_train_timesteps,
size = (batch_size,),
device = latents.device
)
timesteps = timesteps.long()
# step 4\. Get the text embedding for conditioning
encoder_hidden_states = text_encoder(batch["input_ids"])[0]
# step 5\. Add noise to the latents according to the noise
# magnitude at each timestep
# (this is the forward diffusion process),
# provide to unet to get the prediction result
noisy_latents = noise_scheduler.add_noise(
latents, noise, timesteps)
# step 6\. Get the target for loss depend on the 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}")
# step 7\. Predict the noise residual and compute loss
model_pred = unet(noisy_latents, timesteps,
encoder_hidden_states).sample
# step 8\. Calculate loss
loss = F.mse_loss(model_pred.float(), target.float(),
reduction="mean")
# step 9\. Gather the losses across all processes for logging
# (if we use distributed training).
avg_loss = accelerator.gather(loss.repeat(
train_batch_size)).mean()
train_loss += avg_loss.item() / gradient_accumulation_steps
# step 10\. Backpropagate
accelerator.backward(loss)
if accelerator.sync_gradients:
params_to_clip = lora_layers
accelerator.clip_grad_norm_(params_to_clip, max_grad_norm)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
# step 11\. check optimization step and update progress bar
if accelerator.sync_gradients:
progress_bar.update(1)
train_loss = 0.0
logs = {"epoch": epoch,"step_loss": loss.detach().item(),
"lr": lr_scheduler.get_last_lr()[0]}
progress_bar.set_postfix(**logs)
上述代码是 Stable Diffusion 模型训练的典型训练循环。以下是代码各部分功能的分解:
-
外层循环(
for epoch in range(num_train_epochs)
)遍历训练的轮数。一个轮次是对整个训练数据集的一次完整遍历。 -
unet.train()
将模型设置为训练模式。这很重要,因为一些层,如dropout和批归一化,在训练和测试期间表现不同。在训练阶段,这些层的表现与评估阶段不同。例如,dropout层在训练期间将以一定概率丢弃节点以防止过拟合,但在评估期间不会丢弃任何节点。同样,BatchNorm
层在训练期间将使用批统计信息,但在评估期间将使用累积统计信息。因此,如果您不调用unet.train()
,这些层在训练阶段将不会正确表现,这可能导致结果不正确。 -
内层循环(
for step, batch in enumerate(train_dataloader)
)遍历训练数据。train_dataloader
是一个DataLoader
对象,它提供训练数据的批次。 -
在步骤 1中,模型使用
latents
将输入图像编码到潜在空间中,这些潜在变量通过一个因子进行缩放。 -
在步骤 2中,向潜在向量添加随机噪声。这种噪声是从标准正态分布中采样的,并且与潜在向量的形状相同。
-
在步骤 3中,为批处理中的每个图像随机采样时间步长。这是时间相关噪声添加过程的一部分。
-
在步骤 4中,使用文本编码器获取用于条件的文本嵌入。
-
在步骤 5中,根据每个时间步长的噪声幅度向潜在向量添加噪声。
-
在步骤 6中,根据预测类型确定损失计算的目标。它可以是噪声或噪声的速度。
-
在步骤 7和步骤 8中,模型使用带噪声的潜在向量、时间步长和文本嵌入进行预测。然后计算损失作为模型预测与目标之间的均方误差。
-
在步骤 9中,收集所有进程中的损失以进行日志记录。在分布式训练的情况下,这是必要的,其中模型在多个GPU上训练。这样我们可以在训练过程中看到损失值的变化。
-
在步骤 10中,计算损失相对于模型参数的梯度(
accelerator.backward(loss)
),如果需要,则对梯度进行裁剪。这是为了防止梯度变得过大,这可能导致数值不稳定性。优化器根据梯度更新模型参数(optimizer.step()
),学习率调度器更新学习率(lr_scheduler.step()
)。然后梯度被重置为零(optimizer.zero_grad()
)。 -
在步骤 11中,如果梯度已同步,则将训练损失重置为零,并更新进度条。
-
最后,训练损失、学习率和当前epoch 被记录下来以监控训练过程。进度条会根据这些日志更新。
一旦你理解了前面的步骤,你不仅可以训练 Stable Diffusion LoRA,还可以训练任何其他模型。
最后,我们需要保存我们刚刚训练的 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))
weight_name = f"""lora_{pretrained_model_name_or_path.split('/')[-1]}_rank{lora_rank}_s{max_train_steps}_r{resolution}_{diffusion_scheduler.__name__}_{formatted_date}.safetensors"""
StableDiffusionPipeline.save_lora_weights(
save_directory = output_dir,
unet_lora_layers = unet_lora_state_dict,
safe_serialization = True,
weight_name = weight_name
)
accelerator.end_training()
让我们分解前面的代码:
-
accelerator.wait_for_everyone()
:这一行用于分布式训练,以确保所有进程都已达到代码中的这一点。这是一个同步点。 -
if accelerator.is_main_process:
:这一行检查当前进程是否为主进程。在分布式训练中,你通常只想保存一次模型,而不是为每个进程保存一次。 -
unet = unet.to(torch.float32)
:这一行将模型权重的数据类型转换为float32
。这通常是为了节省内存,因为float32
比使用float64
使用更少的内存,但仍然为大多数深度学习任务提供足够的精度。 -
unwrapped_unet = accelerator.unwrap_model(unet)
:这一行将模型从加速器中解包,加速器是一个用于分布式训练的包装器。 -
unet_lora_state_dict = convert_state_dict_to_diffusers(get_peft_model_state_dict(unwrapped_unet))
:这一行获取模型的权重状态字典,其中包含模型的权重,并将其转换为适合 Diffusers 的格式。 -
weight_name = f"lora_{pretrained_model_name_or_path.split('/')[-1]}_rank{lora_rank}_s{max_train_steps}_r{resolution}_{diffusion_scheduler.__name__}_{formatted_date}.safetensors"
:这一行创建了一个用于保存权重的文件名。该名称包含了关于训练过程的各种详细信息。 -
StableDiffusionPipeline.save_lora_weights(...)
:这一行将模型的权重保存到文件中。save_directory
参数指定了文件将保存的目录,unet_lora_layers
是模型的权重状态字典,safe_serialization
表示权重应以安全的方式保存,以便以后加载,weight_name
是文件的名称。 -
accelerator.end_training()
:这一行表示训练过程的结束。这通常用于清理训练过程中使用的资源。
我们在本章的关联代码文件夹中提供了完整的训练代码,文件名为 train_sd16_lora.py
。我们还没有完成;我们仍然需要使用 accelerator
命令来启动训练,而不是直接输入 python py_file.py
。
启动训练
如果你只有一个 GPU,只需运行以下命令:
accelerate launch --num_processes=1 ./train_sd16_lora.py
对于两个 GPU,将 --num_processes
增加到 2
,如下所示:
accelerate launch --num_processes=2 ./train_sd16_lora.py
如果你有多于两个 GPU,并且想在指定的 GPU 上进行训练(例如,你有三个 GPU,并且想让训练代码在第二个和第三个 GPU 上运行),请使用以下命令:
CUDA_VISIBLE_DEVICES=1,2 accelerate launch --num_processes=2 ./train_sd16_lora.py
要使用第一个和第三个 GPU,只需更新 CUDA_VISIBLE_DEVICES
设置为 0,2
,如下所示:
CUDA_VISIBLE_DEVICES=0,2 accelerate launch --num_processes=2 ./train_sd16_lora.py
验证结果
这是见证模型训练力量的最激动人心的时刻。首先,让我们加载LoRA,但将其权重设置为0.0
,使用adapter_weights = [0.0]
:
from diffusers import StableDiffusionPipeline
import torch
from diffusers.utils import make_image_grid
from diffusers import EulerDiscreteScheduler
lora_name = "lora_file_name.safetensors"
lora_model_path = f"./output_dir/{lora_name}"
device = "cuda:0"
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype = torch.bfloat16
).to(device)
pipe.load_lora_weights(
pretrained_model_name_or_path_or_dict=lora_model_path,
adapter_name = "az_lora"
)
prompt = "a toy bike. macro photo. 3d game asset"
nagtive_prompt = "low quality, blur, watermark, words, name"
pipe.set_adapters(
["az_lora"],
adapter_weights = [0.0]
)
pipe.scheduler = EulerDiscreteScheduler.from_config(
pipe.scheduler.config)
images = pipe(
prompt = prompt,
nagtive_prompt = nagtive_prompt,
num_images_per_prompt = 4,
generator = torch.Generator(device).manual_seed(12),
width = 768,
height = 768,
guidance_scale = 8.5
).images
pipe.to("cpu")
torch.cuda.empty_cache()
make_image_grid(images, cols = 2, rows = 2)
运行前面的代码,我们将得到图21.1中显示的图像:
图21.1:玩具自行车、宏观照片、3D游戏资产以及未使用LoRA生成的图像
结果并不那么好。现在,让我们启用训练好的LoRA,使用adapter_weights = [1.0]
。再次运行代码,你应该会看到图21.2中显示的图像:
图21.2:玩具自行车、宏观照片、3D游戏资产以及使用八张图像进行LoRA训练生成的图像
结果比未使用LoRA的图像好得多!如果你看到类似的结果,恭喜你!
摘要
这是一章内容丰富的章节,但了解模型训练的强大功能是值得这一长度的。一旦我们掌握了训练技能,我们就可以根据我们的需求训练任何模型。整个训练过程并不容易,因为有太多细节和琐碎的事情要处理。然而,编写训练代码是全面理解模型训练工作原理的唯一方法;考虑到丰硕的成果,花时间从底层弄清楚它是值得的。
由于章节长度的限制,我只能说整个LoRA训练过程,但一旦你在LoRA训练中取得成功,你可以在Diffusers中找到更多的训练样本,根据你的具体需求更改代码,或者简单地编写你的训练代码,尤其是如果你正在开发一个新模型的架构。
在本章中,我们首先训练了一个简单的模型;模型本身并不那么有趣,但它帮助你理解了使用PyTorch进行模型训练的核心步骤。然后,我们转向利用Accelerator包在多个GPU上训练模型。最后,我们触及了真实的Stable Diffusion模型,并使用仅八张图像训练了一个全功能的LoRA。
在下一章和最后一章中,我们将讨论一些不那么技术性的内容,即人工智能及其与我们、隐私的关系,以及如何跟上其快速变化的进步。
参考文献
-
什么是分布式数据并行(DDP):https://pytorch.org/tutorials/beginner/ddp_series_theory.html
-
启动LoRA训练脚本:https://huggingface.co/docs/diffusers/en/training/lora#launch-the-script
)
-
Hugging Face PEFT:https://huggingface.co/docs/peft/en/index
-
Hugging Face Accelerate:https://huggingface.co/docs/accelerate/en/index
)
第二十二章:探索超越稳定扩散
稳定扩散的领域处于不断变化的状态,每天都有创新模型、方法和研究论文出现。在整个撰写本书的过程中,稳定扩散社区经历了显著的增长。鉴于这一领域的动态性,不可避免地,一些发展没有包含在这些页面中。
在撰写本书并深入研究稳定扩散的复杂性过程中,我经常被问到,“你是如何开始理解这个复杂主题的?”在本章的结尾,我旨在分享我的学习之旅,并提供见解,帮助您跟上稳定扩散和AI的最新发展。
在本章中,我们将讨论以下内容:
-
区分这一AI浪潮的特点:理解当前AI革命的独特特征
-
数学和编程的持久价值:强调在快速变化的AI领域中核心技能的重要性
-
跟上AI创新:保持与最新AI突破同步的技巧和策略
-
培养负责任、道德、隐私和安全的人工智能:探讨开发符合社会价值观和安全标准的人工智能的最佳实践
-
我们与AI的关系演变:反思AI对个人、组织和整个社会的影响
我希望这一章能为那些渴望扩展他们对稳定扩散和AI知识的人提供宝贵的资源。好奇心是解锁这个激动人心领域更深层次理解和探索的关键。
区分这一AI浪潮的特点
2016年3月,AlphaGo [1] 在一场五局比赛中击败了世界著名的围棋选手李世石,创造了历史。这是一个重大事件,因为围棋是一种需要战略思维和直觉的游戏,由于其复杂性,被认为计算机无法掌握。AlphaGo的胜利是对AI和机器学习进步的证明。
AlphaGo的成功基于深度神经网络和蒙特卡洛树搜索技术的结合。它通过学习成千上万的专业围棋游戏来学习模式和策略。然后,它通过自我对弈来提高技能和对游戏的理解。
这一成就标志着AI发展中的一个重大里程碑,证明了机器现在可以在需要深度理解和战略决策的任务中超越人类。
我在观看这些游戏直播时,对机器的力量感到震惊。然而,驱动AlphaGo的模型并非没有局限性。以下是一些:
-
针对围棋的特定性:AlphaGo专门设计来玩围棋。它没有能力将知识转移到其他游戏或领域。如果我们给棋盘增加一行,AlphaGo将无法正常运作。
-
可解释性:理解 AlphaGo 为什么会做出某些决定很困难,这可能会使其在关键情况下难以信任或依赖其输出。
AlphaGo 仅使用 GoPlay 数据进行训练,因此其数据范围相当有限,这是其“特定”功能的根本原因。它类似于基于卷积神经网络(CNN)的图像分类模型。这些模型通过一组预定义的数据进行训练;因此,它们只能在特定输入数据范围内执行。
在 2017 年,论文“Attention Is All You Need” [2] 介绍了变换器模型。作者展示了变换器模型在一种特定的无监督任务——机器翻译中的有效性。他们训练模型将句子从一种语言翻译成另一种语言,而不需要任何对齐的句子对或显式监督。相反,他们使用编码器-解码器结构以概率预测下一个单词。换句话说,下一个“单词”或“标记”是输入的训练标签,因此模型试图学习数据中存在的模式或结构,而不需要关于学习内容的任何明确指导。
变换器模型本身无疑仍然很重要(至少在撰写本文时)。但训练一个没有预定义标签的模型的想法和实现是天才的。
近年来,一些模型仅使用解码器来训练模型。值得注意的是,GPT-3 使用仅解码器架构来生成文本。其他一些视觉模型使用注意力机制来替代 CNN 结构——例如,视觉变换器(ViT) [3] 和 Swin 变换器 [4]。
在稳定扩散的情况下,本书中提出的模型在其 UNet 架构中集成了注意力机制,如在第 4章 和 5章 中所述。稳定扩散可以接受任何图像和标题对进行训练,其数据范围没有限制。如果我们有足够的硬件能力,我们可以提供世界上所有图像及其相关描述文本来训练一个超级扩散模型。
如 OpenAI 的 Sora 模型所示,只要有足够的视频数据、相关描述、强大的 GPU 力量和基于扩散变换器的模型,就可以生成一个视频,在一定程度上模仿现实世界。
在撰写本文时,我们尚不清楚这种基于注意力的自动学习架构的局限性是什么。
数学与编程的持久价值
随着我们见证 AI 展示的力量,有些人可能会争论,在未来,我们可能不需要学习编程或数学,因为我们可以将任何任务委托给 AI。然而,这远远不是事实。这场 AI 革命浪潮为未来打开了一扇新的大门,但本质上,AI 仍然是在硅芯片上运行的程序,它需要人类提供智慧和知识。
当前的AI技术是基于概率论、统计学和线性代数等数学模型开发的,这些对于AI算法至关重要。例如,基于潜在扩散模型(稳定扩散)的算法是基于神经网络的,其灵感来源于人脑的结构。深度学习最重要的部分是反向传播,它本质上就是微积分。因此,没有数学,AI无法存在。
关于编程技能,GPT和扩散模型并不会使编程技能变得多余;相反——编程发现了新的征服领域。那些为AI发展做出贡献的人也是编写最多代码的人。
假设你被某些自封的专家误导,放弃了追求你的编程能力。几年后,当你打开任何GitHub AI项目时,你不仅无法做出任何贡献,甚至完全无法阅读代码。
数学知识和编程技能永远不会过时。它们可能会随着时间改变形式,但核心概念保持不变。
当你正在阅读一本关于使用Python进行稳定扩散的书时,我敢打赌你不会满足于仅仅能够使用该模型,还需要了解它是如何内部工作的。为了理解它,最好的方式是创建它,正如理查德·费曼[7]在他的黑板上[5]所说的,如图图22.1所示:
图22.1 – 理查德·费曼的黑板 – “我不能创造的,我就不能理解”
这也取决于个人经验;我只能在实现之后才能完全理解一个主题。正如古老的谚语所说,我们不能通过阅读关于游泳的书来学会游泳。
然而,实现一个模型将需要理解理论,这需要理解相关的数学知识以及将复杂公式转换为可执行位元的编程技能。
假设我们已经下定决心要更多地了解AI——我们应该从哪里开始?
跟踪AI创新
由于转换器模型在很大程度上改变了格局,我们无法从亚马逊书店找到最新的学习材料,尤其是2022年之前出版的书籍。找到最相关的高质量信息非常重要。以下是一些可能有用的渠道:
-
跟随知名论文作者:通常,一个富有成效或聪明的论文作者可能会创建或贡献另一个模型。他们的GitHub账户、X(以前称为Twitter)账户或其他渠道更新是了解他们最新工作的好方法。
-
使用 X:当浏览我的 X(以前称为 Twitter)动态时,我经常遇到各种内容,包括幽默的视频和图片,这些内容可以轻易地消耗我的早晨。为了最大化平台的有用性,我学会了利用 不感兴趣此帖子 功能来调整我的动态,使其显示更多相关信息。这需要自律,因为我需要抵制参与娱乐帖子的诱惑,而是与提供有价值的见解的 AI 相关帖子互动。通过持续应用此策略,X 成为了跟踪该领域最新发展的宝贵资源。
-
git pull
命令是了解相关领域最新进展的有用方式。例如,当我们打开
diffusers
GitHub 仓库,并输入git pull origin main
命令时,它会列出主分支的最新更改,我可以找出哪些代码被合并到了主分支。通过简单地 ctrl + 点击文件名,我可以打开包含最新更改的文件。通常,
git pull
命令可以提供关于稳定扩散最新进展的最有价值信息。通常,代码中也会包含论文的网址。 -
有用网站:除了 GitHub 仓库,还有三个网站或工具对于查找论文和模型非常有用:
-
Papers with Code (https://paperswithcode.com/):这个网站是跟踪最新研究的绝佳资源。它提供了一个包含相关学术论文及其相关代码实现的综合列表,使得理解和重现结果变得容易。你可以通过研究领域、任务和数据集筛选论文,该网站还展示了各种任务中最先进模型的排行榜。
-
GitHub 趋势:GitHub 是一个开发者和研究人员分享代码的平台,趋势 部分可以发现新模型和实现的宝库。通过根据你的兴趣领域(例如,机器学习、深度学习或自然语言处理)筛选结果,你可以找到最受欢迎和最近更新的存储库。这可以帮助你了解该领域的最新进展和最佳实践。
-
Hugging Face 模型库:Hugging Face 是一个知名的构建和分享 自然语言处理(NLP)模型的平台。他们的模型库是一个可搜索的预训练模型存储库,你可以通过任务、语言和框架进行筛选。通过探索模型库,你可以找到广泛 NLP 任务的前沿模型,并轻松地将它们集成到自己的项目中。此外,Hugging Face 提供详细的文档和教程,使其成为初学者和经验丰富的实践者都极佳的资源。
-
在跟进最新发展时,我发现保持专注和好奇也很重要:
-
保持专注:对我们来说,保持对当前任务或项目的专注,而不被最新的AI进步过分分散注意力,是至关重要的。虽然跟上行业趋势很重要,但不断转移注意力可能导致项目不完整或对理解缺乏深度。以下是一些保持专注的策略:
-
优先学习:确定你现在需要为项目或职业目标学习的内容,并集中精力学习。
-
设定具体的学习目标:有明确的目标可以帮助你保持方向。
-
为探索分配时间:每周留出特定的时间来探索新的进步。这样,你可以在不分散注意力的同时满足好奇心。
-
-
保持好奇,避免感到不知所措:保持对AI中新的想法和技术的好奇和开放态度是至关重要的,但同样重要的是管理这种好奇心,以避免感到不知所措。以下有四个小贴士可以帮助你做到这一点:
-
拥抱成长心态:理解学习是一个过程,现在不知道所有的事情是完全可以接受的。
-
使用多种学习方法:如果一种方法让你感到不知所措,就尝试另一种方法。例如,如果阅读研究论文过于密集,可以尝试观看视频讲座或参加在线课程。或者,简单地请求像ChatGPT这样的大型语言模型提供帮助。
-
创建学习计划:规划你的学习,确保你有时间消化新信息。不要立即克隆仓库,而是记下安排一个时间来处理它。
-
寻求支持:加入AI社区、论坛或讨论组,在那里你可以提问并与他人分享你的学习之旅。这可以使学习过程不那么令人畏惧。
-
在学习AI时,平衡是关键。这关乎找到适合你的专注和好奇的正确混合。
培养负责任、道德、私密和安全的AI
随着我们向前发展,AI将成为我们生活的一个基本组成部分,渗透到我们存在的几乎每一个方面,就像电和互联网一样。最初,这些技术被视为新奇事物,甚至可能是有潜在危险的。高压交流电构成了致命威胁,而互联网则成为虚假信息的渠道。尽管有这些最初的恐惧,我们还是设法减轻了这些技术的负面影响,利用它们的潜力为更大的利益服务。
当突破性的技术出现时,几乎不可能抑制其传播。我们不应该限制对这些进步的访问,而应该努力负责任地将它们整合到我们的生活中。
人工智能也不例外。人工智能被用于欺骗或欺诈目的的例子并不少见。随着人工智能的民主化,强大的AI工具将更容易被每个人获取。挑战在于管理人工智能的潜在滥用。就像一把刀,既可以用来准备食物,也可以造成伤害,人工智能的影响在很大程度上取决于用户。
为了准备进入一个由人工智能驱动的世界,我们需要增加我们对人工智能的理解,包括它的能力和局限性。我们必须学会恰当地使用它,并在我们自己和后代中灌输道德和伦理价值观。如果可能的话,我们还应该建立规范其使用的法律。
作为人工智能开发者,保持人工智能技术的透明度至关重要。当一家大公司发布一个可以生成有害内容的模型,如极端言论或过度政治正确性时,一个开放的社区可以表达担忧并采取纠正措施。相同水平的人工智能技术可以用来对抗这些问题,本质上是以火攻火。
在Stable Diffusion v1.5中,有一个基于OpenAI CLIP(对比语言-图像预训练)的安全检查模型可用,以确保输出内容无害。此模型可以自动且有效地执行此任务。Stable Diffusion XL还包括一个水印模块,可以在图像背景中嵌入难以察觉的水印。此功能可以通过添加特定的隐藏信息来保护图像版权。如果我们保持人工智能技术的开放性,我们总能找到平衡权力和确保人工智能用于更大福祉的方法。
我们正处于一个新时代的黎明,人工智能将改变我们的生活方式、工作方式以及我们彼此互动的方式。我们必须确保这一变革带来积极增长和发展,通过拥抱人工智能并共同努力。但有一件事一直在困扰着我们——如果人工智能夺走了我们的工作怎么办?
我们与人工智能的关系正在演变
1907年4月24日,纽约市的提灯工举行了罢工[6],导致许多街道没有照明。尽管市民有投诉,警察也做出了努力,但由于各种挑战,成功点亮的路灯寥寥无几。这一事件标志着向电灯街道的重大转变,电灯比煤气灯更容易维护,并且自19世纪末引入以来已经开始取代煤气灯。
到1927年,电灯已经完全取代了煤气灯,导致提灯工这一职业和提灯工联盟的消失。电气化进程是不可阻挡的,无论公众和提灯工多么不愿意接受它。
人工智能是新型的电灯;它可以富有创造力,它可以完全自动化,它可以很好地处理一个或多个任务,并且可能超越人类的能力。是的,人工智能电灯将取代我们如此习惯并精心维护的旧式煤气灯。那么,提灯工的工作会被“人工智能”电灯所取代吗?
嗯,恐怕这次从煤气灯维护转向电灯维护并不那么容易。然而,“AI”智能路灯不仅仅是取代工作,还在创造更多的就业机会。最重要的是,由AI创造的这些工作比之前的工作更有趣、更有意义。说实话,我们真的喜欢那些涉及重复、枯燥任务的工作吗?比如那些曾经由提灯工完成的工作?AI将取代无聊的工作,并释放我们更多的脑力去探索以前没有人做过的新奇、激动人心的领域。让我们拥抱变化,欢迎AI智能路灯,并一起开始我们的旅程!
摘要
本章讨论了超出稳定扩散范围的话题,重点关注AI发展的更广泛背景及其对社会的影响。以下是主要观点的简要总结:
-
当前的AI浪潮是独特的,因为它利用基于注意力的自动学习架构,使模型能够在不同领域之间转移知识
-
数学能力和编程技能对于AI开发仍然是必不可少的,因为它们构成了AI算法的基础,并使研究人员能够建立在对现有知识的构建之上
-
您需要通过关注论文作者、使用X(Twitter)、执行GitHub
pull
命令和访问有用网站等渠道了解最新的AI发展动态 -
通过促进透明度、解决潜在滥用问题以及教育用户关于AI的能力和局限性,开发负责任、道德、隐私保护和安全的AI
-
拥抱AI的变革力量,认识到它可能会取代一些工作,但也将为更有趣、更有意义的工作创造新的机会
通过探讨这些话题,我们可以更好地理解AI在我们生活中的作用,并为所有人的利益做出其负责任发展的贡献。
参考文献
-
一图胜千言:大规模图像识别的Transformer:https://arxiv.org/abs/2010.11929
-
Swin Transformer:使用平移窗口的层次视觉Transformer:https://arxiv.org/abs/2103.14030
-
理查德·费曼去世时的黑板:https://digital.archives.caltech.edu/collections/Images/1.10-29/
-
灯夫罢工;城市某些地方变暗;警察预备队在哈莱姆出动,以点燃煤气灯。工会只召集了400名男子,不久后,煤气公司开始裁员:https://www.nytimes.com/1907/04/25/archives/lamplighters-quit-city-dark-in-spots-police-reserves-out-in-harlem.html