原文:
zh.annas-archive.org/md5/ea442d0490aad3d193551ec97eaf414e译者:飞龙
第四部分:应用和新发展
本部分涵盖了早期章节中生成模型的某些应用,以及生成 AI 领域的一些新发展。
在第十三章和第十四章中,你将学习两种生成音乐的方法:MuseGAN,它将音乐作品视为类似于图像的多维对象,以及 Music Transformer,它将音乐作品视为一系列音乐事件。第十五章介绍了扩散模型,这些模型构成了所有领先的文本到图像 Transformer(如 DALL-E 2 或 Imagen)的基础。第十六章使用 LangChain 库将预训练的大型语言模型与 Wolfram Alpha 和 Wikipedia API 结合,创建了一个零样本的全知全能个人助手。
第十三章:使用 MuseGAN 进行音乐生成
本章涵盖
-
使用乐器数字接口进行音乐表示
-
将音乐生成视为与图像生成类似的对象创建问题
-
构建和训练生成对抗网络以生成音乐
-
使用训练好的 MuseGAN 模型生成音乐
到目前为止,我们已经成功生成了形状、数字、图像和文本。在本章和下一章中,我们将探讨两种不同的生成逼真音乐的方法。本章将应用图像 GANs 的技术,将音乐视为类似于图像的多维对象。生成器将生成一首完整的音乐作品并将其提交给评论家(作为判别器,因为我们使用 Wasserstein 距离和梯度惩罚,如第五章所述)进行评估。然后,生成器将根据评论家的反馈修改音乐,直到它与训练数据集中的真实音乐非常相似。在下一章中,我们将音乐视为一系列音乐事件,采用自然语言处理(NLP)技术。我们将使用 GPT 风格的 Transformer 来根据先前的事件预测序列中最可能的音乐事件。这个 Transformer 将生成一系列音乐事件,这些事件可以转换为听起来逼真的音乐。
使用人工智能进行音乐生成的领域已经引起了广泛关注;MuseGAN 是一个突出的模型,由 Dong、Hsiao、Yang 和 Yang 在 2017 年提出。1 MuseGAN 是一个深度神经网络,利用生成对抗网络(GANs)来创建多轨音乐,其中的“Muse”一词象征着音乐背后的创造性灵感。该模型擅长理解代表不同乐器或不同声音的不同轨道之间的复杂交互(这在我们的训练数据中是这种情况)。因此,MuseGAN 可以生成和谐且统一的乐曲。
与其他 GAN 模型类似,MuseGAN 由两个主要组件组成:生成器和评论家(评论家提供对样本真实性的连续度量,而不是将样本分类为真实或虚假)。生成器的任务是生成音乐,而评论家评估音乐的质量并向生成器提供反馈。这种对抗性交互使得生成器能够逐步改进,从而创造出更真实、更具吸引力的音乐。
假设你是一位热衷于约翰·塞巴斯蒂安·巴赫的粉丝,并且已经听过他所有的作品。你可能想知道是否可以使用 MuseGAN 创建模仿他风格的合成音乐。答案是肯定的,你将在本章中学习如何做到这一点。
具体来说,你将首先探索如何将一段多轨音乐表示为一个多维对象。一个轨道本质上是一行音乐或声音,可以是不同的乐器,如钢琴、贝斯或鼓,或者不同的声音,如女高音、女低音、男高音或男低音。在创作电子音乐中的轨道时,你通常将其组织成小节(时间段),然后将每个小节细分为步骤以更好地控制节奏,接着为每个步骤分配一个特定的音符来创作旋律和节奏。因此,我们训练集中的每首音乐都是以(4,2,16,84)的形状结构化的:这意味着有四个音乐轨道,每个轨道由 2 个小节组成,每个小节包含 16 个步骤,每个步骤可以播放 84 种不同音符中的任意一种。
我们生成的 MuseGAN 音乐的风格将受到训练数据的影响。由于你对巴赫的作品感兴趣,你将使用《JSB 圣歌集》数据集来训练 MuseGAN,这是一个由巴赫创作的圣歌集合,为四个轨道编排。这些圣歌已被转换为钢琴卷表示,这是一种用于可视化和编码音乐的方法,特别是用于数字处理目的。你将学习如何将形状为(4,2,16,84)的音乐作品转换为音乐乐器数字接口(MIDI)文件,然后可以在你的电脑上播放。
虽然在前面章节中,生成器仅使用来自潜在空间的一个单一噪声向量来生成不同格式的内容,如形状、数字和图像,但 MuseGAN 中的生成器在生成音乐时会使用四个噪声向量。使用四个独立的噪声向量(和弦、风格、旋律和节奏,我将在本章后面详细解释)是设计选择,它允许在音乐生成过程中有更大的控制和多样性。这些噪声向量中的每一个都代表音乐的不同方面,通过单独操纵它们,模型可以生成更复杂和细腻的作品。
一旦模型被训练,我们将丢弃批评网络,这是 GAN 模型中的常见做法。然后,我们将利用训练好的生成器,通过输入来自潜在空间的四个噪声向量来生成音乐作品。以这种方式生成的音乐与巴赫的风格非常相似。
13.1 数字音乐表示
我们的目标是掌握从头开始构建和训练用于音乐生成的 GAN 模型的艺术。为了实现这一目标,我们需要从音乐理论的基础知识开始,包括理解音符、八度和音高数字。随后,我们将深入研究数字音乐的内部工作原理,特别是关注 MIDI 文件。
根据我们用于音乐生成的机器学习模型的类型,音乐作品的数字表示可能会有所不同。例如,在本章中,我们将音乐表示为一个多维对象,而在下一章中,我们将使用不同的格式:一系列索引。
在本节中,我们将介绍基本的音乐理论,然后转向使用钢琴卷来数字化表示音乐。你将学习如何在电脑上加载和播放一个示例 MIDI 文件。我们还将介绍 music21 Python 库,你将安装并使用它来可视化与音乐作品相关的乐谱音符。最后,你将学习将一首音乐作品表示为一个具有形状(4,2,16,84)的多维对象。
13.1.1 音乐音符、八度和音高
在本章中,我们将处理一个表示音乐作品为 4D 对象的训练数据集。要理解训练数据中的音乐作品的意义,首先熟悉音乐理论中的某些基本概念是至关重要的,例如音乐音符、八度和音高。这些概念相互关联,对于理解数据集至关重要。
图 13.1 说明了这些概念之间的关系。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F01_Liu.png
图 13.1 音乐音符、八度和音高的关系(也称为音符编号)。第一列显示了 11 个八度(范围从-1 到 9),代表不同的音乐声音级别。每个八度被细分为 12 个半音,这些半音列在图的最上面一行:C、C#、D、D#、…、B。在每个八度内,每个音符都被分配一个特定的音高编号,范围从 0 到 127,如图所示。
音乐音符是代表音乐中特定声音的符号。这些音符是音乐的基础元素,用于创作旋律、和弦和节奏。每个音符都被分配一个名称(如 A、B、C、D、E、F、G)并对应一个特定的频率,这决定了它的音高:音符听起来是高还是低。例如,中 C(C4)通常的频率约为 262 赫兹,这意味着其声波每秒振动 262 次。
你可能想知道“中 C(C4)”这个术语的含义。C4 中的数字 4 指的是八度,它是从一个音乐音高级别到下一个级别的距离。在图 13.1 中,最左边的列显示了 11 个八度级别,范围从-1 到 9。当你从一个八度级别移动到下一个时,声音的频率会翻倍。例如,A4 音符通常调校为 440 赫兹,而比 A4 高一个八度的 A5,则调校为 880 赫兹。
在西方音乐中,一个八度被分为 12 个半音,每个半音对应一个特定的音符。图 13.1 的顶部行列出了这 12 个半音:C、C#、D、D#、…、B。上下移动 12 个半音会到达相同的音符名称,但处于更高的或更低的八度。如前所述,A5 比 A4 高一个八度。
每个特定八度内的音符被分配一个音高数字,范围从 0 到 127,如图 13.1 所示。例如,C4 音符的音高数字为 60,而 F3 的音高数字为 53。音高数字是表示音乐音符的一种更有效的方式,因为它指定了八度水平和半音。你将在本章中使用的训练数据就是用音高数字编码的,正是出于这个原因。
13.1.2 多轨音乐简介
首先,让我们谈谈多轨音乐是如何工作的以及它是如何以数字形式表示的。在电子音乐制作中,“轨”通常指的是音乐的一个单独的层次或组成部分,例如鼓轨、贝斯轨或旋律轨。在古典音乐中,轨可能代表不同的声乐部分,如女高音、女低音、男高音和男低音。例如,我们在这章中使用的训练数据集,JSB Chorales 数据集,包含四个轨,对应四个声乐部分。在音乐制作中,每个轨都可以在数字音频工作站(DAW)中单独编辑和处理。这些轨由各种音乐元素组成,包括小节、步骤和音符。
小节(或度量)是由特定数量的拍子定义的时间段,每个拍子有特定的音符持续时间。在许多流行的音乐流派中,小节通常包含四个拍子,尽管这可以根据作品的拍号而变化。一个轨中的小节数量由轨的长度和结构决定。例如,在我们的训练数据集中,每个轨由两个小节组成。
在步骤序列的上下文中,这是一种在电子音乐中编程节奏和旋律的常用技术,一个“步骤”代表一小节的一个细分。在一个标准的 4/4 拍(一小节有四个拍子,每个拍子有四个步骤)中,你可能会发现每小节有 16 个步骤,每个步骤对应一小节的十六分之一。
最后,每个步骤包含一个音符。在我们的数据集中,我们限制范围为最常用的 84 个音符(音高数字从 0 到 83)。因此,步骤中的音符被编码为一个包含 84 个值的 one-hot 向量。
为了用实际例子说明这些概念,请从本书的 GitHub 仓库github.com/markhliu/DGAI下载文件 example.midi,并将其保存在你电脑的/files/目录下。具有.midi 扩展名的文件是 MIDI 文件。MIDI 是一种技术标准,概述了协议、数字接口和连接器,以使电子乐器、计算机和其他相关设备能够相互连接和通信。
MIDI 文件可以在您计算机上的大多数音乐播放器上播放。为了了解我们训练数据中的音乐类型,请使用您计算机上的音乐播放器打开您刚刚下载的文件 example.midi。它应该听起来像我在我的网站上放置的这个音乐文件:mng.bz/lrJB。文件 example.midi 是从本章训练数据集中的某个音乐作品转换而来的。稍后您将学习如何将形状为(4, 2, 16, 84)的音乐作品转换为可以在您计算机上播放的 MIDI 文件。
我们将使用 music21 Python 库,这是一个专为音乐分析、创作和操作设计的强大且全面的工具包,来可视化各种音乐概念是如何工作的。因此,请在您的计算机上的 Jupyter Notebook 应用程序的新单元中运行以下代码行:
!pip install music21
music21 库允许您将音乐以乐谱形式可视化,以便更好地理解轨道、小节、音级和音符。为了实现这一点,您必须首先在您的计算机上安装 MuseScore 应用程序。访问musescore.org/en/download并下载适用于您操作系统的最新版本的 MuseScore 应用程序。截至本文撰写时,最新版本是 MuseScore 4,我们将以此为例。确保您知道 MuseScore 应用程序在您计算机上的文件路径。例如,在 Windows 上,路径是 C:\Program Files\MuseScore 4\bin\MuseScore4.exe。运行以下列表中的代码单元以可视化文件 example.midi 的乐谱。
列表 13.1 使用 music21 库可视化乐谱
%matplotlib inline ①
from music21 import midi, environment
mf = midi.MidiFile()
mf.open("files/example.midi") ②
mf.read()
mf.close()
stream = midi.translate.midiFileToStream(mf)
us = environment.Environment()
path = r'C:\Program Files\MuseScore 4\bin\MuseScore4.exe'
us['musescoreDirectPNGPath'] = path ③
stream.show() ④
① 在 Jupyter 笔记本中显示图像,而不是在原始应用程序中
② 打开 MIDI 文件
③ 定义 MuseScore 应用程序的路径
④ 显示乐谱
对于 macOS 操作系统的用户,将前面代码单元中的路径更改为/Applications/MuseScore 4.app/Contents/MacOS/mscore。对于 Linux 用户,将路径更改为/home/[用户名]/.local/bin/mscore4portable,将[用户名]替换为您实际的用户名。例如,我的用户名是mark,所以路径是/home/mark/.local/bin/mscore4portable。
执行前面的代码单元将显示类似于图 13.2 所示的乐谱。请注意,图中的注释是我添加的,所以您将只看到乐谱而没有任何注释。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F02_Liu.png
图 13.2 JSB Chorales 数据集中一首音乐作品的乐谱。音乐有四个轨道,代表合唱中的四个声部:女高音、女低音、男高音和男低音。乐谱结构为每个轨道两个小节,左右两侧分别代表第一和第二小节。每个小节由 16 个步骤组成,与 4/4 拍号相匹配,其中一小节包含四个节拍,每个节拍细分为四个十六分音符。总共可能有 84 个不同的音高,每个音符都表示为一个包含 84 个值的 one-hot 向量。
JSB Chorales 数据集,由约翰·塞巴斯蒂安·巴赫的合唱作品组成,常用于音乐生成任务中训练机器学习模型。数据集中每首音乐作品的形状(4, 2, 16, 84)可以这样解释。数字 4 代表合唱中的四个声部:女高音、女低音、男高音和男低音。每个声部在数据集中被视为一个单独的轨道。每首作品分为两个小节(也称为乐句)。数据集以这种方式格式化,以便标准化音乐作品的长度,用于训练目的。数字 16 代表每个小节中的步骤数(或细分)。最后,音符以 84 个值进行 one-hot 编码,表示每个步骤中可以演奏的可能音高(或音符)的数量。
13.1.3 数字化音乐表示:钢琴卷轴
钢琴卷轴是音乐的一种视觉表示,常用于 MIDI 编曲软件和 DAWs 中。它以传统钢琴卷轴命名,这种卷轴在自动演奏钢琴中使用,其中包含带有孔的物理卷纸,以表示音符。在数字环境中,钢琴卷轴发挥着类似的功能,但以虚拟格式存在。
钢琴卷轴以网格形式显示,时间水平表示(从左到右),音高垂直表示(从下到上)。每一行对应一个特定的音符,高音在顶部,低音在底部,类似于钢琴键盘的布局。
音符以网格上的条形或块表示。音符块沿垂直轴的位置表示其音高,而沿水平轴的位置表示其在音乐中的时间。音符块的长度表示音符的持续时间。
让我们使用 music21 库来展示钢琴卷轴的样子。在您的 Jupyter Notebook 应用的新单元格中运行以下代码行:
stream.plot()
输出结果如图 13.3 所示。
music21 库还允许您查看与前面钢琴卷轴对应的量化音符:
for n in stream.recurse().notes:
print(n.offset, n.pitches)
输出结果
0.0 (<music21.pitch.Pitch E4>,)
0.25 (<music21.pitch.Pitch A4>,)
0.5 (<music21.pitch.Pitch G4>,)
0.75 (<music21.pitch.Pitch F4>,)
1.0 (<music21.pitch.Pitch E4>,)
1.25 (<music21.pitch.Pitch D4>,)
1.75 (<music21.pitch.Pitch E4>,)
2.0 (<music21.pitch.Pitch E4>,)
2.5 (<music21.pitch.Pitch D4>,)
3.0 (<music21.pitch.Pitch C4>,)
3.25 (<music21.pitch.Pitch A3>,)
3.75 (<music21.pitch.Pitch B3>,)
0.0 (<music21.pitch.Pitch G3>,)
0.25 (<music21.pitch.Pitch A3>,)
0.5 (<music21.pitch.Pitch B3>,)
…
3.25 (<music21.pitch.Pitch F2>,)
3.75 (<music21.pitch.Pitch E2>,)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F03_Liu.png
图 13.3 一段音乐的钢琴卷。钢琴卷是音乐作品的图形表示,以网格形式呈现,时间从左到右水平进展,音高从下到上垂直表示。网格上的每一行对应一个独特的音乐音符,排列方式类似于钢琴键盘,高音符位于顶部,低音符位于底部。这首特定的音乐由两个小节组成,因此在图中可以看到两个不同的部分。音符块的垂直位置表示其音高,而其水平位置表示音符在作品中的演奏时间。此外,音符块的长度反映了音符持续的时长。
我省略了大部分输出。前一个输出中每行的第一个值代表时间。在大多数情况下,每行之后时间增加 0.25 秒。如果下一行的时间增加超过 0.25 秒,这意味着音符持续的时间超过 0.25 秒。正如你所见,起始音符是 E4。0.25 秒后,音符变为 A4,然后是 G4,以此类推。这解释了图 13.3 中(最左侧)的前三个块,它们分别具有 E、A 和 G 的值。
你可能好奇如何将音乐音符序列转换成形状为(4, 2, 16, 84)的对象。为了理解这一点,让我们检查音乐音符中每个时间步的音高数字:
for n in stream.recurse().notes:
print(n.offset,n.pitches[0].midi)
输出结果为
0.0 64
0.25 69
0.5 67
0.75 65
1.0 64
1.25 62
1.75 64
2.0 64
2.5 62
3.0 60
3.25 57
3.75 59
0.0 55
0.25 57
0.5 59
…
3.25 41
3.75 40
前面的代码块已将每个时间步的音乐音符转换为基于图 13.1 中使用的映射的音高数字,范围在 0 到 83 之间。然后,每个音高数字被转换为具有 84 个值的 one-hot 变量,其中所有值均为-1,只有一个位置为 1。我们使用-1 和 1 而不是 0 和 1 进行 one-hot 编码,因为将值放置在-1 和 1 之间可以将数据围绕 0 中心化,这可以使训练更加稳定和快速。许多激活函数和权重初始化方法都假设输入数据围绕 0 中心化。图 13.4 说明了如何将一段 MIDI 音乐编码成形状为(4, 2, 16, 84)的对象。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F04_Liu.png
图 13.4 如何使用 4D 对象表示一段音乐。在我们的训练数据中,每段音乐都由形状为(4, 2, 16, 84)的 4D 对象表示。第一个维度代表四个音乐轨道,即音乐中的四个声部(女高音、女低音、男高音和男低音)。每个音乐轨道分为两个小节。每个小节有四个节拍,每个节拍有四个音符;因此,每个小节有 16 个音符。最后,每个音符由一个具有 84 个值的 one-hot 变量表示,其中所有值均为-1,只有一个位置为 1。
图 13.4 解释了音乐对象形状的维度(4, 2, 16, 84)。本质上,每首音乐作品包含四个轨道,每个轨道包含两个小节。每个小节被细分为 16 个音符。鉴于我们的训练集中音高数字从 0 到 83,每个音符由一个包含 84 个值的 one-hot 向量表示。
在后续关于准备训练数据的讨论中,我们将探讨如何将形状为(4, 2, 16, 84)的对象转换回 MIDI 格式的音乐作品,以便在计算机上播放。
13.2 音乐生成的蓝图
在创作音乐时,我们需要融入更详细的输入以增强控制和多样性。与仅从潜在空间中利用单个噪声向量来生成形状、数字和图像的方法不同,我们在音乐生成过程中将使用四个不同的噪声向量。由于每首音乐作品包含四个轨道和两个小节,我们将使用四个向量来管理这种结构。我们将使用一个向量来控制所有轨道和小节,另一个向量来控制所有轨道中的每个小节,第三个向量来监督所有轨道跨越小节,第四个向量来管理每个轨道中的每个单独小节。本节将向您介绍和弦、风格、旋律和节奏的概念,并解释它们如何影响音乐生成的各个方面。之后,我们将讨论构建和训练 MuseGAN 模型所涉及的步骤。
13.2.1 使用和弦、风格、旋律和节奏构建音乐
在音乐生成阶段稍后,我们从潜在空间中获取四个噪声向量(和弦、风格、旋律和节奏),并将它们输入到生成器中,以创建一首音乐作品。你可能想知道这四条信息的含义。在音乐中,和弦、风格、旋律和节奏是构成作品整体声音和感觉的关键元素。接下来,我将简要解释每个元素。
风格指的是音乐创作、表演和体验的特征方式。它包括音乐类型(如爵士、古典、摇滚等)、音乐创作的时代以及作曲家或表演者的独特方法。风格受文化、历史和个人因素的影响,有助于定义音乐的个性。
节奏感是音乐中的节奏感或摇摆,尤其是在放克、爵士和灵魂乐等风格中。这是让你想要脚打拍子或跳舞的原因。节奏感是通过强调模式、节奏部分(如鼓、贝斯等)之间的互动和速度来创造的。它是赋予音乐运动感和流畅感的关键元素。
和弦是由两个或更多音符同时演奏的组合。它们为音乐提供了和声基础。和弦建立在音阶之上,用于创建音乐的结构和情感深度。不同的和弦类型(大调、小调、减调、增调等)及其编排可以唤起听众的各种情绪和感受。
最后,旋律是音乐作品中最容易识别的音符序列。它是你可能吹口哨或跟着唱的部分。旋律通常由音阶构成,以音高、节奏和轮廓(音高的上升和下降模式)为特征。一首好的旋律易于记忆且富有表现力,传达作品的主要音乐和情感主题。
这些元素共同协作,和谐地创造出音乐作品的整体声音和体验。每个元素都有其作用,但它们都相互影响,共同产生最终的音乐作品。具体来说,一首音乐作品由四个轨道组成,每个轨道有两个小节,从而产生八个小节/轨道组合。我们将使用一个噪声向量用于风格,应用于所有八个小节。我们将使用八个不同的噪声向量用于旋律,每个向量用于一个独特的小节。有四个噪声向量用于节奏,每个向量应用于不同的轨道,两个小节都保持相同。两个噪声向量将用于和弦,每个小节一个。图 13.5 展示了这四个元素如何共同贡献于完整音乐作品的创作。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F05_Liu.png
图 13.5 使用和弦、风格、旋律和节奏生成音乐。每一首音乐作品由四个轨道组成,跨越两个小节。为此,我们将从潜在空间中提取四个噪声向量。第一个向量,代表和弦,其维度为(1,32)。这个向量将通过一个时间网络进行处理,将和弦扩展为两个(1,32)向量,对应两个小节,所有轨道上的值都相同。第二个向量,表示风格,其维度也为(1,32),在所有轨道和小节中保持不变。第三个向量,旋律,形状为(4,32)。它将通过一个时间网络拉伸成两个(4,32)向量,从而产生八个(1,32)向量,每个向量代表一个独特的轨道和小节组合。最后,第四个向量,节奏,其维度为(4,32),将应用于四个轨道,两个小节都保持相同的值。
生成器通过一次生成一个轨道的一小节来创建音乐作品。为此,它需要四个形状为(1, 32)的噪声向量作为输入。这些向量代表和弦、风格、旋律和节奏,每个都控制音乐的一个独特方面,如前所述。由于音乐作品由四个轨道组成,每个轨道有两个小节,因此总共有八个轨道/小节组合。因此,我们需要八组和弦、风格、旋律和节奏来生成音乐作品的全部部分。
我们将从潜在空间中获取四个与和弦、风格、旋律和节奏对应的噪声向量。我们还将后来引入一个时间网络,其作用是沿着小节维度扩展输入。对于两个小节,这意味着输入大小的加倍。音乐本质上是时间的,具有随时间展开的模式和结构。MuseGAN 中的时间网络旨在捕捉这些时间依赖关系,确保生成的音乐具有连贯和逻辑的进展。
和弦的噪声向量形状为(1, 32)。在通过时间网络处理后,我们获得两个(1, 32)大小的向量。第一个向量用于第一小节中的所有四个轨道,而第二个向量用于第二小节中的所有四个轨道。
风格的噪声向量,同样形状为(1, 32),被均匀地应用于所有八个轨道/小节组合。请注意,我们不会将风格向量通过时间网络传递,因为风格向量被设计为在小节间保持相同。
旋律的噪声向量形状为(4, 32)。当通过时间网络传递时,它会产生两个(4, 32)大小的向量,这些向量进一步分解为八个(1, 32)大小的向量。每个向量都用于独特的轨道/小节组合。
最后,节奏的噪声向量形状为(4, 32),其应用方式是每个(1, 32)大小的向量应用于不同的轨道,在小节间保持不变。我们不会将节奏向量通过时间网络传递,因为节奏向量被设计为在小节间保持相同。
在为八个轨道/小节组合中的每个组合生成一小节音乐后,我们将它们合并以创建一个完整的音乐作品,由四个不同的轨道组成,每个轨道包含两个独特的小节。
13.2.2 训练 MuseGAN 的蓝图
第一章提供了 GANs 背后基础概念的概述。在第三章到第五章中,你探索了用于生成形状、数字和图像的 GANs 的创建和训练。本小节将总结构建和训练 MuseGAN 的步骤,突出与之前章节的不同之处。
MuseGAN 生成的音乐风格受训练数据风格的影响。因此,你应该首先收集一个适合训练的巴赫作品数据集。接下来,你将创建一个 MuseGAN 模型,该模型由一个生成器和评论家组成。生成器网络接收四个随机噪声向量作为输入(和弦、风格、旋律和节奏),并输出一首音乐。评论家网络评估一首音乐并分配一个评分,对真实音乐(来自训练集)给予高评分,对由生成器产生的假音乐给予低评分。生成器和评论家网络都利用深度卷积层来捕捉输入的空间特征。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH13_F06_Liu.png
图 13.6 展示了训练 MuseGAN 生成音乐的步骤图。生成器通过从潜在空间中抽取四个随机噪声向量(图的上左角)来生成假音乐作品,并将其展示给评论家(中间)。评论家评估作品并分配一个评分。高评分表明该作品很可能是来自训练数据集的,而低评分则表明该作品很可能是假的(由生成器生成的)。此外,还向评论家展示了一个由真实和假样本混合而成的插值音乐作品(图的上左角)。训练过程结合了基于评论家对这一插值作品评分的梯度惩罚,并将其添加到总损失中。然后将评分与真实值进行比较,使评论家和生成器都能从这些评估中学习。经过多次训练迭代后,生成器变得擅长生成与真实样本几乎无法区分的音乐作品。
图 13.6 展示了 MuseGAN 的训练过程。生成器(图的下左角)接收四个随机噪声向量(和弦、风格、旋律和节奏)作为输入,并生成假音乐作品(图 13.6 中的步骤 1)。这些噪声向量来自潜在空间,它代表了 GAN 可以生成的潜在输出范围,从而能够创建多样化的数据样本。然后,这些假音乐作品(右上角来自训练集的真实音乐作品)被评论家(步骤 3)评估。评论家(图的底部中央)对所有音乐作品进行评分,目的是给予真实音乐高评分,而给予假音乐低评分(步骤 4)。
为了指导模型参数的调整,必须为生成器和批评家选择适当的损失函数。生成器的损失函数旨在鼓励生成与训练数据集中的数据点非常相似的数据点。具体来说,生成器的损失函数是批评家评分的负值。通过最小化这个损失函数,生成器努力创建能够从批评家那里获得高评分的音乐作品。另一方面,批评家的损失函数被制定为鼓励对真实和生成数据点进行准确评估。因此,如果音乐作品来自训练集,批评家的损失函数是评分本身;如果它是生成器生成的,则是评分的负值。本质上,批评家的目标是给真实音乐作品赋予高评分,给假音乐作品赋予低评分。
此外,我们像第五章中那样,将 Wasserstein 距离和梯度惩罚纳入损失函数,以增强 GAN 模型的训练稳定性和性能。为此,一个混合了真实和假音乐的插值音乐作品(如图 13.6 左上角所示)由批评家进行评估。然后,基于批评家对这一插值作品的评分,梯度惩罚在训练过程中被添加到总损失中。
在整个训练循环中,我们在训练批评家和生成器之间交替。在每个训练迭代中,我们从训练集中采样一批真实音乐作品和一批由生成器生成的假音乐作品。我们通过比较批评家的评分(即分数)与真实值(音乐作品是真是假)来计算总损失。然后,我们稍微调整生成器和批评家网络中的权重,以便在后续迭代中,生成器产生更逼真的音乐作品,批评家对真实音乐赋予更高的分数,对假音乐赋予更低的分数。
一旦 MuseGAN 完全训练完成,可以通过输入四个随机噪声向量到训练好的生成器来创建音乐。
13.3 准备 MuseGAN 的训练数据
我们将使用约翰·塞巴斯蒂安·巴赫的合唱作品作为我们的训练数据集,期望生成的音乐类似于巴赫的风格。如果你更喜欢其他音乐家的风格,你可以使用他们的作品作为训练数据。在本节中,我们将首先下载训练数据,并将其组织成批次以供后续训练。
此外,我们还了解到训练集中的音乐作品将被表示为 4D 对象。在本节中,你还将学习如何将这些多维对象转换为计算机上可播放的音乐作品。这种转换是必不可少的,因为 MuseGAN 生成的多维对象与训练集中的类似。在本章的后面部分,我们将 MuseGAN 产生的多维对象转换为 MIDI 文件,使你能够在计算机上收听生成的音乐。
13.3.1 下载训练数据
我们将使用 JSB Chorales 钢琴卷数据集作为我们的训练集。访问 Cheng-Zhi Anna Huang 的 GitHub 仓库 (github.com/czhuang/JSB-Chorales-dataset) 并下载音乐文件 Jsb16thSeparated.npz。将文件保存在您电脑上的 /files/ 目录中。
然后,从本书的 GitHub 仓库 (github.com/markhliu/DGAI) 下载两个实用模块 midi_util.py 和 MuseGAN_util.py,并将它们保存在您电脑上的 /utils/ 目录中。本章中的代码改编自 Azamat Kanametov 的优秀 GitHub 仓库 (github.com/akanametov/musegan)。有了这些文件,我们现在可以加载音乐文件并将它们组织成批次以进行处理:
from torch.utils.data import DataLoader
from utils.midi_util import MidiDataset
dataset = MidiDataset('files/Jsb16thSeparated.npz')
first_song=dataset[0]
print(first_song.shape)
loader = DataLoader(dataset, batch_size=64,
shuffle=True, drop_last=True)
我们将您刚刚下载的数据集加载到 Python 中,然后提取第一首歌曲并将其命名为 first_song。由于歌曲表示为多维对象,我们打印出第一首歌曲的形状。最后,我们将训练数据放入 64 个批次的批次中,以便在章节的后续部分使用。
前一个代码块的输出是
torch.Size([4, 2, 16, 84])
数据集中的每首歌曲的形状为 (4, 2, 16, 84),如前一个输出所示。这表明每首歌曲由四个曲目组成,每个曲目有两小节。每小节包含 16 个时间步,在每个时间步,音符由一个包含 84 个值的独热向量表示。在每个独热向量中,所有值都设置为 -1,除了一个位置,其值设置为 1,表示音符的存在。您可以如下验证数据集中的值范围:
flat=first_song.reshape(-1,)
print(set(flat.tolist()))
输出是
{1.0, -1.0}
前一个输出显示,每个音乐作品中的值要么是 -1,要么是 1。
13.3.2 将多维对象转换为音乐作品
目前,歌曲格式为 PyTorch 张量,并准备好输入到 MuseGAN 模型中。然而,在我们继续之前,了解如何将这些多维对象转换为电脑上可播放的音乐作品非常重要。这将帮助我们稍后把生成的音乐作品转换为可播放的文件。
首先,我们将所有 84 个值的独热变量转换为介于 0 到 83 之间的音高数:
import numpy as np
from music21 import note, stream, duration, tempo
parts = stream.Score()
parts.append(tempo.MetronomeMark(number=66))
max_pitches = np.argmax(first_song, axis=-1) ①
midi_note_score = max_pitches.reshape([2 * 16, 4]) ②
print(midi_note_score)
① 将 84 个值的独热向量转换为介于 0 到 83 之间的数字
② 将结果重塑为 (32, 4)
输出是
tensor([[74, 74, 74, 74],
…
[70, 70, 69, 69],
[67, 67, 69, 69],
[70, 70, 70, 70],
[69, 69, 69, 69],
[69, 69, 69, 69],
[65, 65, 65, 65],
[58, 58, 60, 60],
…
[53, 53, 53, 53]])
在这里显示的输出中,每一列代表一个音乐曲目,数值范围从 0 到 83。这些数字对应于音高数,正如您在图 13.1 中之前看到的。
现在,我们将把前一个代码块中的张量 midi_note_score 转换为实际的 MIDI 文件,让您可以在电脑上播放它。
列表 13.2 将音高数转换为 MIDI 文件
for i in range(4): ①
last_x = int(midi_note_score[:, i][0])
s = stream.Part()
dur = 0
for idx, x in enumerate(midi_note_score[:, i]): ②
x = int(x)
if (x != last_x or idx % 4 == 0) and idx > 0:
n = note.Note(last_x)
n.duration = duration.Duration(dur)
s.append(n)
dur = 0
last_x = x
dur = dur + 0.25 ③
n = note.Note(last_x)
n.duration = duration.Duration(dur)
s.append(n) ④
parts.append(s)
parts.write("midi","files/first_song.midi")
① 遍历四个音乐曲目
② 遍历每个曲目中的所有音符
③ 将 0.25 秒添加到每个时间步
④ 将音符添加到音乐流中
运行前面的代码单元后,您将在电脑上看到一个 MIDI 文件,first_song.midi。使用您电脑上的音乐播放器播放它,以了解我们用来训练 MuseGAN 的音乐类型。
练习 13.1
将训练数据集中的第二首歌曲转换为 MIDI 文件。将其保存为second_song.midi,并使用您电脑上的音乐播放器播放。
13.4 构建 MuseGAN
实质上,我们将音乐作品视为一个具有多个维度的对象。利用第四章至第六章中的技术,我们将使用深度卷积神经网络来处理这项任务,因为它们能够有效地从多维对象中提取空间特征。在 MuseGAN 中,我们将构建一个生成器和评判器,类似于图像创建中的生成器根据评判器的反馈来细化图像。生成器将生成一个作为 4D 对象的音乐作品。
我们将真实音乐(来自我们的训练集)和生成器生成的假音乐都展示给评判器。评判器将对每首作品从负无穷大到正无穷大进行评分,分数越高表示音乐更可能是真实的。评判器的目标是给真实音乐高分数,给假音乐低分数。相反,生成器的目标是生成与真实音乐难以区分的音乐,从而从评判器那里获得高分数。
在本节中,我们将构建一个 MuseGAN 模型,该模型包括一个生成网络和一个评判网络。评判网络使用深度卷积层从多维对象中提取独特特征,从而增强其评估音乐作品的能力。另一方面,生成网络利用深度转置卷积层生成旨在生成逼真音乐作品的特征图。稍后,我们将使用训练集中的音乐作品来训练 MuseGAN 模型。
13.4.1 MuseGAN 中的评判器
如第五章所述,将 Wasserstein 距离纳入损失函数可以帮助稳定训练。因此,在 MuseGAN 中,我们采用类似的方法,并使用评判器而不是判别器。评判器不是一个二元分类器;相反,它评估生成器的输出(在这种情况下,是一首音乐作品),并分配一个从-∞到∞的分数。更高的分数表示音乐更可能是真实的(即来自训练集)。
我们构建了一个如以下列表所示的音乐评判神经网络,其定义可以在您之前下载的文件 MuseGAN_util.py 中找到。
列表 13.3 MuseGAN 中的评判器网络
class MuseCritic(nn.Module):
def __init__(self,hid_channels=128,hid_features=1024,
out_features=1,n_tracks=4,n_bars=2,n_steps_per_bar=16,
n_pitches=84):
super().__init__()
self.n_tracks = n_tracks
self.n_bars = n_bars
self.n_steps_per_bar = n_steps_per_bar
self.n_pitches = n_pitches
in_features = 4 * hid_channels if n_bars == 2\
else 12 * hid_channels
self.seq = nn.Sequential(
nn.Conv3d(self.n_tracks, hid_channels,
(2, 1, 1), (1, 1, 1), padding=0), ①
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, hid_channels,
(self.n_bars - 1, 1, 1), (1, 1, 1), padding=0),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, hid_channels,
(1, 1, 12), (1, 1, 12), padding=0),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, hid_channels,
(1, 1, 7), (1, 1, 7), padding=0),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, hid_channels,
(1, 2, 1), (1, 2, 1), padding=0),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, hid_channels,
(1, 2, 1), (1, 2, 1), padding=0),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(hid_channels, 2 * hid_channels,
(1, 4, 1), (1, 2, 1), padding=(0, 1, 0)),
nn.LeakyReLU(0.3, inplace=True),
nn.Conv3d(2 * hid_channels, 4 * hid_channels,
(1, 3, 1), (1, 2, 1), padding=(0, 1, 0)),
nn.LeakyReLU(0.3, inplace=True),
nn.Flatten(), ②
nn.Linear(in_features, hid_features),
nn.LeakyReLU(0.3, inplace=True),
nn.Linear(hid_features, out_features)) ③
def forward(self, x):
return self.seq(x)
① 将输入通过几个 Conv3d 层
② 将输出扁平化
③ 将输出通过两个线性层
评论网络的输入是一个维度为(4,2,16,84)的音乐作品。该网络主要由几个 Conv3d 层组成。这些层将音乐作品中的每个轨道视为一个 3D 对象,并应用过滤器来提取空间特征。Conv3d 层的操作与前面章节中讨论的用于图像生成的 Conv2d 层类似。
需要注意的是,评论模型的最后一层是线性的,我们没有对其输出应用任何激活函数。因此,评论模型的输出是一个从-∞到∞的值,这可以解释为评论家对音乐作品的评价。
13.4.2 MuseGAN 中的生成器
如本章前面所述,生成器将一次生成一段音乐,然后我们将这八段音乐组合成一首完整的乐曲。
MuseGAN 中的生成器不是只使用一个噪声向量,而是使用四个独立的噪声向量作为输入来控制生成的音乐的各个方面。其中两个向量将通过时间网络进行处理,以扩展它们在小节维度上的长度。而风格和节奏向量被设计为在小节间保持不变,和弦和旋律向量被设计为在小节间变化。因此,我们首先建立一个时间网络来扩展和弦和旋律向量跨越两个小节,确保生成的音乐在时间上有连贯和逻辑的进展。
在您之前下载的本地模块MuseGAN_util中,我们定义了TemporalNetwork()类如下:
class TemporalNetwork(nn.Module):
def __init__(self,z_dimension=32,hid_channels=1024,n_bars=2):
super().__init__()
self.n_bars = n_bars
self.net = nn.Sequential(
Reshape(shape=[z_dimension, 1, 1]), ①
nn.ConvTranspose2d(z_dimension,hid_channels,
kernel_size=(2, 1),stride=(1, 1),padding=0,),
nn.BatchNorm2d(hid_channels),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(hid_channels,z_dimension,
kernel_size=(self.n_bars - 1, 1),stride=(1, 1),
padding=0,),
nn.BatchNorm2d(z_dimension),
nn.ReLU(inplace=True),
Reshape(shape=[z_dimension, self.n_bars]),) ②
def forward(self, x):
return self.net(x)
① TemporalNetwork()类的输入维度是(1,32)。
② 输出维度是(2,32)。
这里描述的TemporalNetwork()类使用两个 ConvTranspose2d 层将单个噪声向量扩展为两个不同的噪声向量,每个向量对应两个小节中的一个。正如我们在第四章中提到的,转置卷积层用于上采样和生成特征图。在这种情况下,它们被用来在不同的小节间扩展噪声向量。
我们不会一次性生成所有轨道的所有小节,而是逐个小节地生成音乐。这样做可以让 MuseGAN 在计算效率、灵活性和音乐连贯性之间取得平衡,从而产生更有结构和吸引力的音乐作品。因此,我们继续构建一个负责生成乐曲片段的生成器:轨道中的一小节。我们在本地的MuseGAN_util模块中引入了BarGenerator()类:
class BarGenerator(nn.Module):
def __init__(self,z_dimension=32,hid_features=1024,hid_channels=512,
out_channels=1,n_steps_per_bar=16,n_pitches=84):
super().__init__()
self.n_steps_per_bar = n_steps_per_bar
self.n_pitches = n_pitches
self.net = nn.Sequential(
nn.Linear(4 * z_dimension, hid_features), ①
nn.BatchNorm1d(hid_features),
nn.ReLU(inplace=True),
Reshape(shape=[hid_channels,hid_features//hid_channels,1]),
nn.ConvTranspose2d(hid_channels,hid_channels,
kernel_size=(2, 1),stride=(2, 1),padding=0), ②
nn.BatchNorm2d(hid_channels),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(hid_channels,hid_channels // 2,
kernel_size=(2, 1),stride=(2, 1),padding=0),
nn.BatchNorm2d(hid_channels // 2),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(hid_channels // 2,hid_channels // 2,
kernel_size=(2, 1),stride=(2, 1),padding=0),
nn.BatchNorm2d(hid_channels // 2),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(hid_channels // 2,hid_channels // 2,
kernel_size=(1, 7),stride=(1, 7),padding=0),
nn.BatchNorm2d(hid_channels // 2),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(hid_channels // 2,out_channels,
kernel_size=(1, 12),stride=(1, 12),padding=0),
Reshape([1, 1, self.n_steps_per_bar, self.n_pitches])) ③
def forward(self, x):
return self.net(x)
① 我们将和弦、风格、旋律和节奏合并成一个向量,大小为 4 * 32。
② 然后将输入重塑为二维,我们使用几个 ConvTranspose2d 层进行上采样和音乐特征生成。
③ 输出的形状为(1,1,16,84):1 个轨道,1 个小节,16 个音符,每个音符由一个 84 值的向量表示。
BarGenerator()类接受四个噪声向量作为输入,每个向量代表不同轨道中特定节拍的和弦、风格、旋律和节奏,所有这些向量的形状都是(1,32)。这些向量在输入到BarGenerator()类之前被连接成一个单一的 128 值向量。BarGenerator()类的输出是一个音乐节拍,维度为(1,1,16,84),表示 1 个轨道,1 个节拍,16 个音符,每个音符由一个 84 值的向量表示。
最后,我们将使用MuseGenerator()类生成一个完整的音乐作品,包含四个轨道,每个轨道有两个节拍。每个节拍都是使用之前定义的BarGenerator()类构建的。为了实现这一点,我们在本地MuseGAN_util模块中定义了MuseGenerator()类。
列表 13.4 MuseGAN 中的音乐生成器
class MuseGenerator(nn.Module):
def __init__(self,z_dimension=32,hid_channels=1024,
hid_features=1024,out_channels=1,n_tracks=4,
n_bars=2,n_steps_per_bar=16,n_pitches=84):
super().__init__()
self.n_tracks = n_tracks
self.n_bars = n_bars
self.n_steps_per_bar = n_steps_per_bar
self.n_pitches = n_pitches
self.chords_network=TemporalNetwork(z_dimension,
hid_channels, n_bars=n_bars)
self.melody_networks = nn.ModuleDict({})
for n in range(self.n_tracks):
self.melody_networks.add_module(
"melodygen_" + str(n),
TemporalNetwork(z_dimension,
hid_channels, n_bars=n_bars))
self.bar_generators = nn.ModuleDict({})
for n in range(self.n_tracks):
self.bar_generators.add_module(
„bargen_" + str(n),BarGenerator(z_dimension,
hid_features,hid_channels // 2,out_channels,
n_steps_per_bar=n_steps_per_bar,n_pitches=n_pitches))
def forward(self,chords,style,melody,groove):
chord_outs = self.chords_network(chords)
bar_outs = []
for bar in range(self.n_bars): ①
track_outs = []
chord_out = chord_outs[:, :, bar]
style_out = style
for track in range(self.n_tracks): ②
melody_in = melody[:, track, :]
melody_out = self.melody_networks"melodygen_"\
+ str(track)[:, :, bar]
groove_out = groove[:, track, :]
z = torch.cat([chord_out, style_out, melody_out,\
groove_out], dim=1) ③
track_outs.append(self.bar_generators"bargen_"\
+ str(track)) ④
track_out = torch.cat(track_outs, dim=1)
bar_outs.append(track_out)
out = torch.cat(bar_outs, dim=2) ⑤
return out
① 遍历两个节拍
② 遍历四个轨道
③ 将和弦、风格、旋律和节奏合并为一个输入
④ 使用轨道生成器生成一个节拍
⑤ 将八个节拍合并成一个完整的音乐作品
生成器接受四个噪声向量作为输入。它遍历四个轨道和两个节拍。在每次迭代中,它使用轨道生成器创建一个音乐节拍。完成所有迭代后,MuseGenerator()类将八个节拍合并成一个连贯的音乐作品,其维度为(4,2,16,84)。
13.4.3 优化器和损失函数
我们基于本地模块中的MuseGenerator()和MuseCritic()类创建一个生成器和批评家:
import torch
from utils.MuseGAN_util import (init_weights, MuseGenerator, MuseCritic)
device = "cuda" if torch.cuda.is_available() else "cpu"
generator = MuseGenerator(z_dimension=32, hid_channels=1024,
hid_features=1024, out_channels=1).to(device)
critic = MuseCritic(hid_channels=128,
hid_features=1024,
out_features=1).to(device)
generator = generator.apply(init_weights)
critic = critic.apply(init_weights)
正如我们在第五章中讨论的,批评家生成一个评分而不是分类,因此损失函数定义为预测和目标之间的乘积的负平均值。因此,我们在本地模块MuseGAN_util中定义以下loss_fn()函数:
def loss_fn(pred, target):
return -torch.mean(pred*target)
在训练过程中,对于生成器,我们将loss_fn()函数中的目标参数赋值为 1。这种设置旨在引导生成器产生能够获得最高评分(即loss_fn()函数中的变量 pred)的音乐。对于批评家,我们在损失函数中将目标设置为 1 用于真实音乐,-1 用于假音乐。这种设置引导批评家对真实音乐给予高评分,对假音乐给予低评分。
与第五章中的方法类似,我们将 Wasserstein 距离和梯度惩罚结合到批评家的损失函数中,以确保训练稳定性。梯度惩罚在MuseGAN_util.py文件中定义如下:
class GradientPenalty(nn.Module):
def __init__(self):
super().__init__()
def forward(self, inputs, outputs):
grad = torch.autograd.grad(
inputs=inputs,
outputs=outputs,
grad_outputs=torch.ones_like(outputs),
create_graph=True,
retain_graph=True,
)[0]
grad_=torch.norm(grad.view(grad.size(0),-1),p=2,dim=1)
penalty = torch.mean((1\. - grad_) ** 2)
return penalty
GradientPenalty() 类需要两个输入:插值音乐,这是真实和假音乐的结合,以及批评家网络分配给这种插值音乐的评分。该类计算批评家评分关于插值音乐的梯度。然后,梯度惩罚被计算为这些梯度的范数的平方差与目标值 1 之间的平方差,这与我们在第五章中采取的方法类似。
如往常一样,我们将使用 Adam 优化器来训练批评家和生成器:
lr = 0.001
g_optimizer = torch.optim.Adam(generator.parameters(),
lr=lr, betas=(0.5, 0.9))
c_optimizer = torch.optim.Adam(critic.parameters(),
lr=lr, betas=(0.5, 0.9))
有了这些,我们已经成功构建了一个 MuseGAN,现在可以使用本章前面准备的数据进行训练。
13.5 训练 MuseGAN 以生成音乐
现在我们已经有了 MuseGAN 模型和训练数据,我们将继续在本节中训练模型。
与第三章和第四章中的方法类似,在训练 GAN 时,我们将交替训练批评家和生成器。在每个训练迭代中,我们将从训练数据集中采样一批真实音乐和从生成器中生成的一批音乐,并将它们呈现给批评家进行评估。在批评家训练期间,我们将批评家的评分与真实值进行比较,并稍微调整批评家网络的权重,以便在下一个迭代中,评分对真实音乐尽可能高,对生成音乐尽可能低。在生成器训练期间,我们将生成音乐输入到批评模型中,以获得评分,然后稍微调整生成器网络的权重,以便在下一个迭代中,评分更高(因为生成器旨在创建能够欺骗批评家的音乐作品)。我们重复这个过程多次迭代,逐渐使生成器网络能够创建更逼真的音乐作品。
一旦模型训练完成,我们将丢弃批评家网络,并使用训练好的生成器通过输入四个噪声向量(和弦、风格、旋律和节奏)来创建音乐作品。
13.5.1 训练 MuseGAN
在我们开始训练 MuseGAN 模型的训练循环之前,我们首先定义了一些超参数和辅助函数。超参数 repeat 控制我们在每次迭代中训练批评家的次数,display_step 指定我们显示输出的频率,而 epochs 是我们训练模型的时代数。
列表 13.5 超参数和辅助函数
from utils.MuseGAN_util import loss_fn, GradientPenalty
batch_size=64
repeat=5
display_step=10
epochs=500 ①
alpha=torch.rand((batch_size,1,1,1,1)).requires_grad_().to(device) ②
gp=GradientPenalty() ③
def noise(): ④
chords = torch.randn(batch_size, 32).to(device)
style = torch.randn(batch_size, 32).to(device)
melody = torch.randn(batch_size, 4, 32).to(device)
groove = torch.randn(batch_size, 4, 32).to(device)
return chords,style,melody,groove
① 定义了一些超参数
② 定义了 alpha 以创建插值音乐
③ 定义了一个 gp() 函数来计算梯度惩罚
④ 定义了一个 noise() 函数来检索四个随机噪声向量
批处理大小设置为 64,这有助于我们确定需要检索多少组随机噪声向量来创建一个假音乐批次。我们将在每个训练循环中训练批评家五次,生成器只训练一次,因为一个有效的批评家对于训练生成器至关重要。我们将在每个 10 个 epoch 后显示训练损失。我们将对模型进行 500 个 epoch 的训练。
我们在本地模块中实例化GradientPenalty()类,以创建一个gp()函数来计算梯度惩罚。我们还定义了一个noise()函数来生成四个随机噪声向量,以输入到生成器中。
接下来,我们定义以下函数,train_epoch(),用于训练模型一个 epoch。
列表 13.6 在一个 epoch 中训练 MuseGAN 模型
def train_epoch():
e_gloss = 0
e_closs = 0
for real in loader: ①
real = real.to(device)
for _ in range(repeat): ②
chords,style,melody,groove=noise()
c_optimizer.zero_grad()
with torch.no_grad():
fake = generator(chords, style, melody,groove).detach()
realfake = alpha * real + (1 - alpha) * fake
fake_pred = critic(fake)
real_pred = critic(real)
realfake_pred = critic(realfake)
fake_loss = loss_fn(fake_pred,-torch.ones_like(fake_pred))
real_loss = loss_fn(real_pred,torch.ones_like(real_pred))
penalty = gp(realfake, realfake_pred)
closs = fake_loss + real_loss + 10 * penalty ③
closs.backward(retain_graph=True)
c_optimizer.step()
e_closs += closs.item() / (repeat*len(loader))
g_optimizer.zero_grad()
chords,style,melody,groove=noise()
fake = generator(chords, style, melody, groove)
fake_pred = critic(fake)
gloss = loss_fn(fake_pred, torch.ones_like(fake_pred)) ④
gloss.backward()
g_optimizer.step()
e_gloss += gloss.item() / len(loader)
return e_gloss, e_closs
① 遍历所有批次
② 在每个迭代中训练批评家五次
③ 批评家的总损失有三个组成部分:评估真实音乐的损失、评估假音乐的损失以及梯度惩罚损失。
④ 训练生成器
训练过程与我们第五章训练带有梯度惩罚的条件 GAN 时使用的非常相似。
我们现在对模型进行 500 个 epoch 的训练:
for epoch in range(1,epochs+1):
e_gloss, e_closs = train_epoch()
if epoch % display_step == 0:
print(f"Epoch {epoch}, G loss {e_gloss} C loss {e_closs}")
如果你使用 GPU 进行训练,大约需要一个小时。否则,可能需要几个小时。一旦完成,你可以按照以下方式将训练好的生成器保存到本地文件夹:
torch.save(generator.state_dict(),'files/MuseGAN_G.pth')
或者,你可以从我的网站上下载训练好的生成器:mng.bz/Bglr。
接下来,我们将丢弃批评家网络,并使用训练好的生成器来创建模仿巴赫风格的音乐。
13.5.2 使用训练好的 MuseGAN 生成音乐
要使用训练好的生成器生成音乐,我们将从潜在空间中提取四个噪声向量输入到生成器中。请注意,我们可以在同一时间生成多个音乐对象并将它们一起解码,形成一个连续的音乐作品。你将在本小节中学习如何做到这一点。
我们首先加载生成器中的训练权重:
generator.load_state_dict(torch.load('files/MuseGAN_G.pth',
map_location=device))
我们不仅能够生成一个 4D 音乐对象,还可以同时生成多个 4D 音乐对象,并在之后将它们转换成一个连续的音乐作品。例如,如果我们旨在创建五个音乐对象,我们首先从潜在空间中采样五组噪声向量。每组包含四个向量:和弦、风格、旋律和节奏,如下所示:
num_pieces = 5
chords = torch.rand(num_pieces, 32).to(device)
style = torch.rand(num_pieces, 32).to(device)
melody = torch.rand(num_pieces, 4, 32).to(device)
groove = torch.rand(num_pieces, 4, 32).to(device)
每个生成的音乐对象可以转换成一个大约持续 8 秒的音乐作品。在这种情况下,我们选择生成五个音乐对象,并在之后将它们解码成一个单一的音乐作品,结果持续大约 40 秒。你可以根据你的喜好调整变量num_pieces的值,以适应你希望的音乐作品长度。
接下来,我们向生成器提供五组潜在变量,以生成一组音乐对象:
preds = generator(chords, style, melody, groove).detach()
输出,preds,包含五个音乐对象。接下来,我们将这些对象解码成一个单一的音乐作品,表示为一个 MIDI 文件:
from utils.midi_util import convert_to_midi
music_data = convert_to_midi(preds.cpu().numpy())
music_data.write('midi', 'files/MuseGAN_song.midi')
我们从本地模块 midi_util 中导入 convert_to_midi() 函数。打开你之前下载的文件 midi_util.py 并回顾 convert_to_midi() 函数的定义。这个过程与我们本章前面将训练集中的第一个音乐对象转换为文件 first_song.midi 时所做的是类似的。由于 MIDI 文件表示随时间变化的音符序列,我们只需将对应于五个音乐对象的五个音乐片段连接成一个扩展的音符序列。然后将这个组合序列保存为 MuseGAN_song.midi 到你的电脑上。
在你的电脑上找到生成的音乐片段 MuseGAN_song.midi。用你选择的音乐播放器打开它并聆听,看它是否与训练集中的音乐片段相似。为了比较,你可以在我网站上听一听由训练模型生成的音乐片段,网址是 mng.bz/dZJv。请注意,由于生成器的输入,即噪声向量,是从潜在空间中随机抽取的,因此你生成的音乐片段听起来会有所不同。
练习 13.2
从潜在空间中获得三组随机的噪声向量(每组应包含和弦、风格、旋律和节奏)。将它们输入到训练好的生成器中,以获得三个音乐对象。将它们解码成一首单独的音乐,以 MIDI 文件的形式。将其保存为 generated_song.midi 到你的电脑上,并使用音乐播放器播放。
在本章中,你学习了如何构建和训练 MuseGAN 以生成巴赫风格的音乐。具体来说,你将一首音乐视为一个 4D 对象,并应用了第四章中关于深度卷积层的技巧来开发 GAN 模型。在下一章中,你将探索生成音乐的不同方式:将一首音乐视为一系列索引,并利用 NLP 技术通过逐个预测一个索引来生成音乐片段。
摘要
-
MuseGAN 将一首音乐视为一个类似于图像的多维对象。生成器生成一首音乐并将其提交,连同训练集中的真实音乐片段一起,供评论家评估。然后根据评论家的反馈修改音乐,直到它与训练数据集中的真实音乐非常相似。
-
音乐音符、八度和音高是音乐理论中的基本概念。八度代表不同的音乐声音水平。每个八度分为 12 个半音:C、C#、D、D#、E、F、F#、G、G#、A、A#、B。在八度内,一个音符被分配一个特定的音高数字。
-
在电子音乐制作中,一个轨道通常指的是音乐的一个单独的层或组件。每个轨道包含多个小节(或度量)。小节进一步分为多个步骤。
-
为了将一段音乐表示为一个多维对象,我们用(4, 2, 16, 84)的形状来结构化它:4 个音乐轨道,每个轨道包含 2 个小节,每个小节包含 16 个步骤,每个步骤可以演奏 84 种不同音符中的任意一种。
-
在音乐创作中,为了实现更大的控制和多样性,引入更详细的输入是至关重要的。与之前章节中用来自潜在空间的单个噪声向量生成形状、数字和图像不同,我们在音乐生成过程中使用了四个不同的噪声向量。鉴于每首音乐作品由四个轨道和两个小节组成,我们使用这四个向量来有效地管理这种结构。一个向量控制所有轨道和小节,另一个控制所有轨道中的每个小节,第三个监督所有轨道跨越小节的情况,第四个管理每个轨道中的每个小节。
^(1) 邓浩文,萧文仪,杨立嘉,杨宜璇,2017,“MuseGAN:用于符号音乐生成和伴奏的多轨道序列生成对抗网络。” arxiv.org/abs/1709.06298.
第十四章:构建和训练音乐 Transformer
本章涵盖了
-
使用控制信息和速度值来表示音乐
-
将音乐标记化为一系列索引
-
构建和训练音乐 Transformer
-
使用训练好的 Transformer 生成音乐事件
-
将音乐事件转换回可播放的 MIDI 文件
为你最喜欢的音乐家不再与我们同在而感到难过?不再难过:生成式 AI 可以将他们带回舞台!
以 Layered Reality 为例,这是一家位于伦敦的公司,正在开发名为《猫王进化》的项目。1 目标?使用 AI 复活传奇的猫王艾维斯·普雷斯利。通过将艾维斯的大量官方档案材料,包括视频剪辑、照片和音乐,输入到一个复杂的计算机模型中,这个 AI 猫王学会了以惊人的相似度模仿他的唱歌、说话、跳舞和走路。结果?一场数字表演,捕捉了已故国王本人的精髓。
《猫王进化》项目是生成式 AI 在各个行业产生变革性影响的杰出例子。在前一章中,你探讨了使用 MuseGAN 创建可以以多轨音乐作品为假的音乐的方法。MuseGAN 将一首音乐视为一个多维对象,类似于图像,并生成与训练数据集中的音乐相似的音乐作品。然后,由评论家评估真实和 AI 生成的音乐,这有助于改进 AI 生成的音乐,直到它与真实音乐无法区分。
在本章中,你将采用一种不同的方法来处理 AI 音乐创作,将其视为一系列音乐事件。我们将应用第十一章和第十二章中讨论的文本生成技术,来预测序列中的下一个元素。具体来说,你将开发一个类似 GPT 风格的模型,根据序列中所有先前事件来预测下一个音乐事件。由于 GPT 风格的 Transformer 具有可扩展性和自注意力机制,这些机制有助于它们捕捉长距离依赖关系并理解上下文,因此它们非常适合这项任务。你将创建的音乐 Transformer 具有 2016 万个参数,足够捕捉音乐作品中不同音符的长期关系,但同时也足够小,可以在合理的时间内进行训练。
我们将使用来自谷歌 Magenta 团队的 Maestro 钢琴音乐作为我们的训练数据。你将学习如何首先将音乐乐器数字接口(MIDI)文件转换为音乐音符序列,类似于自然语言处理(NLP)中的原始文本数据。然后你将把音乐音符分解成称为音乐事件的小片段,类似于 NLP 中的标记。由于神经网络只能接受数值输入,因此你需要将每个独特的事件标记映射到一个索引。有了这个,训练数据中的音乐作品就被转换成了索引序列,准备好输入到神经网络中。
为了训练音乐 Transformer 以预测序列中下一个标记,基于当前标记和序列中所有之前的标记,我们将创建长度为 2,048 的索引序列作为输入(特征 x)。然后我们将序列向右移动一个索引,并使用它们作为输出(目标 y)。我们将 (x, y) 对输入到音乐 Transformer 中以训练模型。一旦训练完成,我们将使用一个短索引序列作为提示并将其输入到音乐 Transformer 中以预测下一个标记,然后将该标记附加到提示中形成一个新的序列。这个新序列被反馈到模型中进行进一步的预测,这个过程会重复进行,直到序列达到期望的长度。
你将看到训练好的音乐 Transformer 可以生成模仿训练数据集中风格的逼真音乐。此外,与第十三章中生成的音乐不同,你将学习如何控制音乐作品的艺术性。你将通过调整预测的 logits 与温度参数的比例来实现这一点,就像你在前几章中控制生成文本的艺术性时做的那样。
14.1 音乐 Transformer 简介
音乐 Transformer 的概念于 2018 年提出.^(2) 这种创新方法扩展了最初为 NLP 任务设计的 Transformer 架构,应用于音乐生成领域。正如前几章所讨论的,Transformers 使用自注意力机制来有效地把握上下文,并捕捉序列中元素之间的长距离依赖关系。
类似地,音乐 Transformer 被设计成通过学习大量现有音乐数据集来生成音乐序列。该模型被训练成根据先前的音乐事件来预测序列中的下一个音乐事件,通过理解训练数据中不同音乐元素之间的模式、结构和关系。
训练音乐 Transformer 的关键步骤在于找出如何将音乐表示为一系列独特的音乐事件,类似于 NLP 中的标记。在上一章中,你学习了如何将一首音乐表示为 4D 对象。在本章中,你将探索一种替代的音乐表示方法,即通过控制信息和速度值实现的基于性能的音乐表示.^(3) 基于此,你将把一首音乐转换为四种类型的音乐事件:音符开启、音符关闭、时间移动和速度。
音符开启信号表示一个音符的开始演奏,指定音符的音高。音符关闭表示音符的结束,告诉乐器停止演奏该音符。时间移动表示两个音乐事件之间经过的时间量。速度衡量演奏音符的力量或速度,较高的值对应更强的、更响亮的声音。每种类型的音乐事件都有许多不同的值。每个独特的事件将被映射到不同的索引,有效地将一首音乐转换为一个索引序列。然后,你将应用第十一章和第十二章中讨论的 GPT 模型,创建一个仅具有解码器的音乐 Transformer,以预测序列中的下一个音乐事件。
在本节中,你将首先通过控制信息和速度值了解基于性能的音乐表示。然后,你将探索如何将音乐作品表示为一系列音乐事件。最后,你将学习构建和训练 Transformer 以生成音乐的步骤。
14.1.1 基于性能的音乐表示
基于性能的音乐表示通常使用 MIDI 格式实现,该格式通过控制信息和速度值捕捉音乐表演的细微差别。在 MIDI 中,音符通过音符开启和音符关闭消息表示,这些消息包含每个音符的音高和速度信息。
正如我们在第十三章中讨论的,音高值范围从 0 到 127,每个值对应于八度中的一个半音。例如,音高值 60 对应于 C4 音符,而音高值 74 对应于 D5 音符。速度值,同样范围从 0 到 127,表示音符的动态,较高的值表示更响亮或更有力的演奏。通过结合这些控制信息和速度值,MIDI 序列可以捕捉现场表演的表达细节,允许通过兼容 MIDI 的乐器和软件进行表达性的回放。
为了给你一个具体的例子,说明如何通过控制信息和速度值表示一首音乐,请考虑以下列表中显示的五个音符。
列表 14.1 基于性能的音乐表示中的示例音符
<[SNote] time: 1.0325520833333333 type: note_on, value: 74, velocity: 86>
<[SNote] time: 1.0442708333333333 type: note_on, value: 38, velocity: 77>
<[SNote] time: 1.2265625 type: note_off, value: 74, velocity: None>
<[SNote] time: 1.2395833333333333 type: note_on, value: 73, velocity: 69>
<[SNote] time: 1.2408854166666665 type: note_on, value: 37, velocity: 64>
这些是训练数据集中你将在本章使用的音乐作品中的前五个音符。第一个音符的大致时间戳为 1.03 秒,音高值为 74(D5)的音符以 86 的速度开始演奏。观察第二个音符,你可以推断出大约 0.01 秒后(因为时间戳现在是 1.04 秒),一个音高值为 38 的音符以 77 的速度开始演奏,以此类推。
这些音乐符号类似于自然语言处理中的原始文本;我们不能直接将它们输入到音乐 Transformer 中训练模型。我们首先需要将音符“令牌化”,然后将令牌转换为索引,再输入到模型中。
为了令牌化音乐音符,我们将使用 0.01 秒的增量来表示音乐,以减少音乐作品中的时间步数。此外,我们将控制消息与速度值分开,并将它们视为音乐作品的独立元素。具体来说,我们将使用音符开启、音符关闭、时间偏移和速度事件来表示音乐。一旦这样做,前五个音符可以表示为以下事件(为了简洁,省略了一些事件)。
列表 14.2 音乐作品的令牌化表示
<Event type: time_shift, value: 99>,
<Event type: time_shift, value: 2>,
<Event type: velocity, value: 21>,
<Event type: note_on, value: 74>,
<Event type: time_shift, value: 0>,
<Event type: velocity, value: 19>,
<Event type: note_on, value: 38>,
<Event type: time_shift, value: 17>,
<Event type: note_off, value: 74>,
<Event type: time_shift, value: 0>,
<Event type: velocity, value: 17>,
<Event type: note_on, value: 73>,
<Event type: velocity, value: 16>,
<Event type: note_on, value: 37>,
<Event type: time_shift, value: 0>
…
我们将以 0.01 秒的增量计算时间偏移,并将从 0.01 秒到 1 秒的时间偏移以 100 个不同的值进行令牌化。因此,时间偏移事件被令牌化为 100 个独特的事件令牌:值为 0 表示 0.01 秒的时间间隔,值为 1 表示 0.02 秒的时间间隔,以此类推,直到 99,表示 1 秒的时间间隔。如果一个时间偏移超过 1 秒,你可以使用多个时间偏移令牌来表示它。例如,列表 14.2 中的前两个令牌都是时间偏移令牌,值分别为 99 和 2,分别表示 1 秒和 0.03 秒的时间间隔。这与列表 14.1 中第一个音乐音符的时间戳相匹配:1.0326 秒。
列表 14.2 也显示了速度是音乐事件的一种独立类型。我们将速度值放入 32 个等间隔的箱子中,将原始的速度值(范围从 0 到 127)转换为 32 个值中的一个,范围从 0 到 31。这就是为什么列表 14.1 中第一个音符的原始速度值 86 现在在列表 14.2 中表示为速度事件,值为 21(因为 86 落在第 22 个箱子中,Python 使用零基索引)。
表 14.1 显示了四种不同令牌化事件的意义、它们的值范围以及每个事件令牌的意义。
表 14.1 不同事件令牌的意义
| 事件令牌类型 | 事件令牌值范围 | 事件令牌的意义 |
|---|---|---|
note_on | 0–127 | 在某个音高值处开始演奏。例如,值为 74 的note_on表示开始演奏 D5 音符。 |
note_off | 0–127 | 释放某个音符。例如,值为 60 的note_off表示停止演奏 C4 音符。 |
time_shift | 0–99 | time_shift值是 0.01 秒的增量。例如,0 表示 0.01 秒,2 表示 0.03 秒,99 表示 1 秒。 |
velocity | 0–31 | 原始速度值被放入 32 个箱子中。使用箱子值。例如,原始速度值为 86 现在有一个标记化值为 21。 |
与 NLP 中采用的方法类似,我们将每个唯一的标记转换为索引,以便我们可以将数据输入到神经网络中。根据表 14.1,有 128 个唯一的音符开启事件标记,128 个音符关闭事件标记,32 个速度事件标记和 100 个时间偏移事件标记。这导致总共有 128 + 128 + 32 + 100 = 388 个唯一标记。因此,我们根据表 14.2 中提供的映射将这些 388 个唯一标记转换为从 0 到 387 的索引。
表 14.2 事件标记到索引和索引到事件标记的映射
| 标记类型 | 索引范围 | 事件标记到索引 | 索引到事件标记 |
|---|---|---|---|
note_on | 0–127 | note_on标记的值。例如,具有 74 个值的note_on标记被分配一个索引值为 74。 | 如果索引范围是 0 到 127,将标记类型设置为note_on并将值设置为索引值。例如,索引值 63 映射到具有 63 个值的note_on标记。 |
note_off | 128–255 | 128 加上note_off标记的值。例如,具有 60 个值的note_off标记被分配一个索引值为 188(因为 128+60=188)。 | 如果索引范围是 128 到 255,将标记类型设置为note_off并将值设置为索引减去 128。例如,索引 180 映射到具有 52 个值的note_off标记。 |
time_shift | 256–355 | 256 加上time_shift标记的值。例如,具有 16 个值的time_shift标记被分配一个索引值为 272(因为 256+16=272)。 | 如果索引范围是 256 到 355,将标记类型设置为time_shift并将值设置为索引减去 256。例如,索引 288 映射到具有 32 个值的time_shift标记。 |
velocity | 356–387 | 356 加上速度标记的值。例如,具有 21 个值的速度标记被分配一个索引值为 377(因为 356+21=377)。 | 如果索引范围是 356 到 387,将标记类型设置为velocity并将值设置为索引减去 356。例如,索引 380 映射到具有 24 个值的velocity标记。 |
表 14.2 概述了事件标记到索引的转换。音符开启标记被分配从 0 到 127 的索引值,其中索引值对应于标记中的音高数。音符关闭标记被分配从 128 到 255 的索引值,索引值是 128 加上音高数。时间偏移标记被分配从 256 到 355 的索引值,索引值是 256 加上时间偏移值。最后,速度标记被分配从 356 到 387 的索引值,索引值是 356 加上速度箱子数。
使用这种标记到索引的映射,我们将每首音乐转换成一系列索引。我们将对此训练数据集中的所有音乐作品应用此转换,并使用生成的序列来训练我们的音乐 Transformer(其细节将在后面解释)。一旦训练完成,我们将使用 Transformer 以序列的形式生成音乐。最后一步是将此序列转换回 MIDI 格式,以便我们可以在计算机上播放和欣赏音乐。
表 14.2 的最后一列提供了将索引转换回事件标记的指导。我们首先根据索引所在的范围确定标记类型。表 14.2 的第二列中的四个范围对应于表的第一列中的四种标记类型。为了获得每种标记类型的值,我们将索引值分别减去 0、128、256 和 356,分别对应四种类型的标记。然后,这些标记化的事件被转换为 MIDI 格式的音符,准备好在计算机上播放。
14.1.2 音乐 Transformer 架构
在第九章中,我们构建了一个编码器-解码器 Transformer,在第十一章和第十二章中,我们专注于仅解码器 Transformer。与编码器捕获源语言含义并将其传递给解码器以生成翻译的语言翻译任务不同,音乐生成不需要编码器理解不同的语言。相反,模型根据音乐序列中的先前事件标记生成后续事件标记。因此,我们将为我们的音乐生成任务构建一个仅解码器的 Transformer。
我们的音乐 Transformer,与其他 Transformer 模型一样,利用自注意力机制来捕捉音乐作品中不同音乐事件之间的长距离依赖关系,从而生成连贯且逼真的音乐。尽管我们的音乐 Transformer 在大小上与我们在第十一章和第十二章中构建的 GPT 模型不同,但它具有相同的核心架构。它遵循与 GPT-2 模型相同的结构设计,但尺寸显著更小,这使得在没有超级计算设施的情况下进行训练成为可能。
具体来说,我们的音乐 Transformer 由 6 个解码器层组成,嵌入维度为 512,这意味着每个标记在词嵌入后都由一个 512 维的向量表示。与原始 2017 年论文“Attention Is All You Need”中使用正弦和余弦函数进行位置编码不同,我们使用嵌入层来学习序列中不同位置的位置编码。因此,序列中的每个位置也由一个 512 维的向量表示。为了计算因果自注意力,我们使用 8 个并行注意力头来捕捉序列中标记的不同含义,每个注意力头的维度为 64(512/8)。
与 GPT-2 模型中 50,257 的词汇量相比,我们的模型具有更小的词汇量,为 390(388 个不同的事件标记,加上一个表示序列结束的标记和一个填充较短的序列的标记;我将在后面解释为什么需要填充)。这使得我们可以在音乐 Transformer 中将最大序列长度设置为 2,048,这比 GPT-2 模型中的最大序列长度 1,024 长得多。这种选择是必要的,以便捕捉序列中音乐音符的长期关系。具有这些超参数值,我们的音乐 Transformer 具有 1,016 万参数。
图 14.1 展示了本章我们将创建的音乐 Transformer 的架构。它与你在第十一章和第十二章中构建的 GPT 模型的架构相似。图 14.1 还显示了训练过程中数据通过模型时的大小。
我们构建的音乐 Transformer 的输入包括输入嵌入,如图 14.1 底部所示。输入嵌入是输入序列的词嵌入和位置编码的总和。然后,这个输入嵌入依次通过六个解码器块。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH14_F01_Liu.png
图 14.1 展示了音乐 Transformer 的架构。MIDI 格式的音乐文件首先被转换为音乐事件的序列。这些事件随后被标记化并转换为索引。我们将这些索引组织成 2,048 个元素的序列,每个批次包含 2 个这样的序列。输入序列首先进行词嵌入和位置编码;输入嵌入是这两个组件的总和。然后,这个输入嵌入通过六个解码器层进行处理,每个层都利用自注意力机制来捕捉序列中不同音乐事件之间的关系。经过解码器层后,输出经过层归一化以确保训练过程中的稳定性。然后,它通过一个线性层,输出大小为 390,这对应于词汇表中的独特标记数量。这个最终输出代表了序列中下一个音乐事件的预测对数几率。
如第十一章和第十二章所述,每个解码器层由两个子层组成:一个因果自注意力层和一个前馈网络。此外,我们对每个子层应用层归一化和残差连接,以增强模型稳定性和学习能力。
经过解码器层后,输出经过层归一化,然后输入到一个线性层。我们模型中的输出数量对应于词汇表中的独特音乐事件标记的数量,即 390。模型的输出是下一个音乐事件标记的对数几率。
之后,我们将应用 softmax 函数到这些 logits 上,以获得所有可能事件标记的概率分布。该模型被设计用来根据当前标记和音乐序列中所有之前的标记来预测下一个事件标记,使其能够生成连贯且音乐上合理的序列。
14.1.3 训练音乐 Transformer
既然我们已经了解了如何构建用于音乐生成的音乐 Transformer,那么让我们概述一下音乐 Transformer 的训练过程。
模型生成的音乐风格受用于训练的音乐作品的影响。我们将使用 Google 的 Magenta 团队的钢琴表演来训练我们的模型。图 14.2 说明了训练音乐生成 Transformer 所涉及的步骤。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH14_F02_Liu.png
图 14.2 音乐 Transformer 生成音乐的训练过程
与我们在 NLP 任务中采取的方法类似,我们音乐 Transformer 训练过程中的第一步是将原始训练数据转换为数值形式,以便将其输入到模型中。具体来说,我们首先将训练集中的 MIDI 文件转换为音乐音符序列。然后,我们将这些音符进一步标记化,通过将它们转换为 388 个独特的事件/标记中的 1 个。标记化后,我们为每个标记分配一个唯一的索引(即一个整数),将训练集中的音乐作品转换为整数序列(见图 14.2 中的步骤 1)。
接下来,我们将整数序列转换为训练数据,通过将此序列划分为等长的序列(见图 14.2 中的步骤 2)。我们允许每个序列中最多有 2,048 个索引。选择 2,048 允许我们捕捉音乐序列中音乐事件之间的长距离依赖关系,以创建逼真的音乐。这些序列形成我们模型的特征(x 变量)。正如我们在前几章中训练 GPT 模型生成文本时所做的,我们将输入序列窗口向右滑动一个索引,并将其用作训练数据中的输出(y 变量;见图 14.2 中的步骤 3)。这样做迫使我们的模型根据音乐序列中的当前标记和所有之前的标记来预测序列中的下一个音乐标记。
输入和输出对作为音乐 Transformer 的训练数据(x, y)。在训练过程中,你将遍历训练数据。在前向传递中,你将输入序列 x 通过音乐 Transformer(步骤 4)。音乐 Transformer 然后根据模型中的当前参数进行预测(步骤 5)。你通过比较预测的下一个标记与步骤 3 获得的输出来计算交叉熵损失。换句话说,你将模型的预测与真实值(步骤 6)进行比较。最后,你将调整音乐 Transformer 中的参数,以便在下一个迭代中,模型的预测更接近实际输出,最小化交叉熵损失(步骤 7)。该模型本质上是在执行一个多类别分类问题:它从词汇表中的所有独特音乐标记中预测下一个标记。
你将通过多次迭代重复步骤 3 到 7。在每次迭代后,模型参数都会调整以改善下一个标记的预测。这个过程将重复进行 50 个 epoch。
要使用训练好的模型生成新的音乐作品,我们从测试集中获取一个音乐作品,对其进行标记化,并将其转换为一系列长索引。我们将使用前 250 个索引作为提示(200 或 300 个索引将产生类似的结果)。然后,我们要求训练好的音乐 Transformer 生成新的索引,直到序列达到一定长度(例如,1,000 个索引)。然后,我们将索引序列转换回 MIDI 文件,以便在您的计算机上播放。
14.2 音乐作品的标记化
在掌握了音乐 Transformer 的结构和其训练方法之后,我们将从第一步开始:对训练数据集中的音乐作品进行标记化和索引。
我们将首先使用基于性能的表示(如第一部分所述)来表示音乐作品为音符,类似于自然语言处理中的原始文本。之后,我们将这些音符划分为一系列事件,类似于自然语言处理中的标记。每个独特的事件将被分配一个不同的索引。利用这个映射,我们将训练数据集中的所有音乐作品转换为索引序列。
接下来,我们将这些索引序列标准化为固定长度,具体为 2,048 个索引的序列,并将它们用作特征输入(x)。通过将窗口向右移动一个索引,我们将生成相应的输出序列(y)。然后,我们将输入和输出(x, y)对分组成批次,为章节后面的音乐 Transformer 训练做准备。
由于我们需要 pretty_midi 和 music21 库来处理 MIDI 文件,请在 Jupyter Notebook 应用程序的新单元格中执行以下代码行:
!pip install pretty_midi music21
14.2.1 下载训练数据
我们将从由谷歌 Magenta 团队提供的 MAESTRO 数据集中获取钢琴演奏,该数据集可在storage.googleapis.com/magentadata/datasets/maestro/v2.0.0/maestro-v2.0.0-midi.zip获得,并下载 ZIP 文件。下载后,解压缩它,并将生成的文件夹/maestro-v2.0.0/移动到您计算机上的/files/目录中。
确保/maestro-v2.0.0/文件夹包含 4 个文件(其中一个应命名为“maestro-v2.0.0.json”)和 10 个子文件夹。每个子文件夹应包含超过 100 个 MIDI 文件。为了熟悉训练数据中音乐片段的声音,尝试使用您喜欢的音乐播放器打开一些 MIDI 文件。
接下来,我们将 MIDI 文件分成训练、验证和测试子集。首先,在/files/maestro-v2.0.0/目录下创建三个子文件夹:
import os
os.makedirs("files/maestro-v2.0.0/train", exist_ok=True)
os.makedirs("files/maestro-v2.0.0/val", exist_ok=True)
os.makedirs("files/maestro-v2.0.0/test", exist_ok=True)
为了方便处理 MIDI 文件,访问凯文·杨的 GitHub 仓库github.com/jason9693/midi-neural-processor,下载 processor.py 文件,并将其放置在您计算机上的/utils/文件夹中。或者,您也可以从本书的 GitHub 仓库github.com/markhliu/DGAI中获取该文件。我们将使用此文件作为本地模块,将 MIDI 文件转换为一系列索引,反之亦然。这种方法使我们能够专注于开发、训练和利用音乐 Transformer,而无需陷入音乐格式转换的细节。同时,我将提供一个简单的示例,说明这个过程是如何工作的,这样您就可以使用该模块自己将 MIDI 文件和一系列索引之间进行转换。
此外,您还需要从本书的 GitHub 仓库下载 ch14util.py 文件,并将其放置在您计算机上的/utils/目录中。我们将使用 ch14util.py 文件作为另一个本地模块来定义音乐 Transformer 模型。
/maestro-v2.0.0/文件夹中的 maestro-v2.0.0.json 文件包含所有 MIDI 文件及其指定的子集(训练、验证或测试)。基于这些信息,我们将 MIDI 文件分类到三个相应的子文件夹中。
列表 14.3 将训练数据分割为训练、验证和测试子集
import json
import pickle
from utils.processor import encode_midi
file="files/maestro-v2.0.0/maestro-v2.0.0.json"
with open(file,"r") as fb:
maestro_json=json.load(fb) ①
for x in maestro_json: ②
mid=rf'files/maestro-v2.0.0/{x["midi_filename"]}'
split_type = x["split"] ③
f_name = mid.split("/")[-1] + ".pickle"
if(split_type == "train"):
o_file = rf'files/maestro-v2.0.0/train/{f_name}'
elif(split_type == "validation"):
o_file = rf'files/maestro-v2.0.0/val/{f_name}'
elif(split_type == "test"):
o_file = rf'files/maestro-v2.0.0/test/{f_name}'
prepped = encode_midi(mid)
with open(o_file,"wb") as f:
pickle.dump(prepped, f)
① 加载 JSON 文件
② 遍历训练数据中的所有文件
根据 JSON 文件中的说明,将文件放置在训练、验证或测试子文件夹中
您下载的 JavaScript 对象表示法(JSON)文件将训练数据集中的每个文件分类到三个子集之一:训练、验证和测试。在执行前面的代码列表后,如果您在计算机上的/train/、/val/和/test/文件夹中查看,您应该在每个文件夹中找到许多文件。为了验证这三个文件夹中每个文件夹的文件数量,您可以执行以下检查:
train_size=len(os.listdir('files/maestro-v2.0.0/train'))
print(f"there are {train_size} files in the train set")
val_size=len(os.listdir('files/maestro-v2.0.0/val'))
print(f"there are {val_size} files in the validation set")
test_size=len(os.listdir('files/maestro-v2.0.0/test'))
print(f"there are {test_size} files in the test set")
前一个代码块输出的结果是
there are 967 files in the train set
there are 137 files in the validation set
there are 178 files in the test set
结果显示,训练、验证和测试子集中分别有 967、137 和 178 首音乐作品。
14.2.2 MIDI 文件标记化
接下来,我们将每个 MIDI 文件表示为一串音乐音符。
列表 14.4 将 MIDI 文件转换为音乐音符序列
import pickle
from utils.processor import encode_midi
import pretty_midi
from utils.processor import (_control_preprocess,
_note_preprocess,_divide_note,
_make_time_sift_events,_snote2events)
file='MIDI-Unprocessed_Chamber1_MID--AUDIO_07_R3_2018_wav--2'
name=rf'files/maestro-v2.0.0/2018/{file}.midi' ①
events=[]
notes=[]
song=pretty_midi.PrettyMIDI(name)
for inst in song.instruments:
inst_notes=inst.notes
ctrls=_control_preprocess([ctrl for ctrl in
inst.control_changes if ctrl.number == 64])
notes += _note_preprocess(ctrls, inst_notes) ②
dnotes = _divide_note(notes) ③
dnotes.sort(key=lambda x: x.time)
for i in range(5):
print(dnotes[i])
① 从训练数据集中选择一个 MIDI 文件
② 从音乐中提取音乐事件
③ 将所有音乐事件放入列表 dnotes 中
我们已经从训练数据集中选择了一个 MIDI 文件,并使用 processor.py 本地模块将其转换为音乐音符序列。前面代码列表的输出如下:
<[SNote] time: 1.0325520833333333 type: note_on, value: 74, velocity: 86>
<[SNote] time: 1.0442708333333333 type: note_on, value: 38, velocity: 77>
<[SNote] time: 1.2265625 type: note_off, value: 74, velocity: None>
<[SNote] time: 1.2395833333333333 type: note_on, value: 73, velocity: 69>
<[SNote] time: 1.2408854166666665 type: note_on, value: 37, velocity: 64>
这里显示的输出显示了 MIDI 文件中的前五个音符。你可能已经注意到输出中的时间表示是连续的。某些音符同时包含note_on和velocity属性,由于时间表示的连续性,这会导致大量独特的音乐事件,从而复杂化了标记过程。此外,不同的note_on和velocity值的组合很大(每个都可以假设 128 个不同的值,范围从 0 到 127),导致词汇表大小过大。这反过来又会使得训练变得不切实际。
为了减轻这个问题并减小词汇表大小,我们进一步将这些音乐音符转换为标记化事件:
cur_time = 0
cur_vel = 0
for snote in dnotes:
events += _make_time_sift_events(prev_time=cur_time, ①
post_time=snote.time)
events += _snote2events(snote=snote, prev_vel=cur_vel) ②
cur_time = snote.time
cur_vel = snote.velocity
indexes=[e.to_int() for e in events]
for i in range(15): ③
print(events[i])
① 将时间离散化以减少独特事件的数目
② 将音乐音符转换为事件
③ 打印出前 15 个事件
输出如下:
<Event type: time_shift, value: 99>
<Event type: time_shift, value: 2>
<Event type: velocity, value: 21>
<Event type: note_on, value: 74>
<Event type: time_shift, value: 0>
<Event type: velocity, value: 19>
<Event type: note_on, value: 38>
<Event type: time_shift, value: 17>
<Event type: note_off, value: 74>
<Event type: time_shift, value: 0>
<Event type: velocity, value: 17>
<Event type: note_on, value: 73>
<Event type: velocity, value: 16>
<Event type: note_on, value: 37>
<Event type: time_shift, value: 0>
音乐作品现在由四种类型的事件表示:音符开启、音符关闭、时间移动和速度。每种事件类型包含不同的值,总共产生 388 个独特的事件,如前面表格 14.2 中详细说明。将 MIDI 文件转换为这种独特事件序列的具体细节对于构建和训练音乐 Transformer 不是必要的。因此,我们不会深入探讨这个话题;感兴趣的读者可以参考前面提到的 Huang 等人(2018)的研究。你需要知道的是如何使用 processor.py 模块将 MIDI 文件转换为索引序列,反之亦然。在下面的子节中,你将学习如何完成这个任务。
14.2.3 准备训练数据
我们已经学会了如何将音乐作品转换为标记,然后转换为索引。下一步涉及准备训练数据,以便我们可以在本章后面利用它来训练音乐 Transformer。为了实现这一点,我们定义了以下列表中所示的create_xys()函数。
列表 14.5 创建训练数据
import torch,os,pickle
max_seq=2048
def create_xys(folder):
files=[os.path.join(folder,f) for f in os.listdir(folder)]
xys=[]
for f in files:
with open(f,"rb") as fb:
music=pickle.load(fb)
music=torch.LongTensor(music)
x=torch.full((max_seq,),389, dtype=torch.long)
y=torch.full((max_seq,),389, dtype=torch.long) ①
length=len(music)
if length<=max_seq:
print(length)
x[:length]=music ②
y[:length-1]=music[1:] ③
y[length-1]=388 ④
else:
x=music[:max_seq]
y=music[1:max_seq+1]
xys.append((x,y))
return xys
① 创建长度为 2,048 个索引的(x, y)序列,并将索引 399 设置为填充索引
② 使用最多 2,048 个索引的序列作为输入
③ 将窗口向右滑动一个索引,并使用它作为输出
④ 设置结束索引为 388
正如我们在整本书中反复看到的那样,在序列预测任务中,我们使用一个序列 x 作为输入。然后我们将序列向右移动一个位置以创建输出序列。这种方法迫使模型根据序列中的当前元素和所有前面的元素来预测下一个元素。为了为我们的音乐 Transformer 准备训练数据,我们将构建(x, y)对,其中 x 是输入,y 是输出。x 和 y 都包含 2,048 个索引——足够长以捕捉序列中音乐音符的长期关系,但又不至于太长而阻碍训练过程。
我们将遍历下载的训练数据集中的所有音乐作品。如果一个音乐作品的长度超过 2,048 个索引,我们将使用前 2,048 个索引作为输入 x。对于输出 y,我们将使用从第二个位置到第 2,049 个位置的索引。在音乐作品长度小于或等于 2,048 个索引的罕见情况下,我们将使用索引 389 填充序列,以确保 x 和 y 的长度都是 2,048 个索引。此外,我们使用索引 388 来表示序列 y 的结束。
如第一部分所述,总共有 388 个独特的事件标记,索引从 0 到 387。由于我们使用 388 来表示 y 序列的结束,并使用 389 来填充序列,因此总共有 390 个独特的索引,范围从 0 到 389。
我们现在可以将create_xys()函数应用于训练子集:
trainfolder='files/maestro-v2.0.0/train'
train=create_xys(trainfolder)
输出如下
15
5
1643
1771
586
这表明在训练子集中的 967 首音乐作品中,只有 5 首的长度小于 2,048 个索引。它们的长度在之前的输出中显示。
我们还把create_xys()函数应用于验证和测试子集:
valfolder='files/maestro-v2.0.0/val'
testfolder='files/maestro-v2.0.0/test'
print("processing the validation set")
val=create_xys(valfolder)
print("processing the test set")
test=create_xys(testfolder)
输出如下
processing the validation set
processing the test set
1837
这表明验证子集中的所有音乐作品长度都超过 2,048 个索引。测试子集中只有一首音乐作品的长度小于 2,048 个索引。
让我们打印出验证子集中的一份文件,看看它是什么样子:
val1, _ = val[0]
print(val1.shape)
print(val1)
输出如下:
torch.Size([2048])
tensor([324, 366, 67, ..., 60, 264, 369])
验证集第一对中的 x 序列长度为 2,048 个索引,具有诸如 324、367 等值。让我们使用processor.py模块来解码序列到一个 MIDI 文件,这样你就可以听到它的声音:
from utils.processor import decode_midi
file_path="files/val1.midi"
decode_midi(val1.cpu().numpy(), file_path=file_path)
decode_midi()函数将索引序列转换为 MIDI 文件,可以在你的电脑上播放。在运行前面的代码块后,用电脑上的音乐播放器打开 val1.midi 文件,听听它的声音。
练习 14.1
使用processor.py本地模块中的decode_midi()函数将训练子集中的第一首音乐作品转换为 MIDI 文件。将其保存为 train1.midi 到你的电脑上。用电脑上的音乐播放器打开它,感受一下我们用于训练数据类型的音乐。
最后,我们创建一个数据加载器,以便数据以批次的格式进行训练:
from torch.utils.data import DataLoader
batch_size=2
trainloader=DataLoader(train,batch_size=batch_size,
shuffle=True)
为了防止您的 GPU 内存耗尽,我们将使用 2 个批处理大小,因为我们创建了非常长的序列,每个序列包含 2,048 个索引。如果需要,可以将批处理大小减少到 1 或切换到 CPU 训练。
有了这些,我们的训练数据已经准备好了。在接下来的两个部分中,我们将从头开始构建一个音乐 Transformer,然后使用我们刚刚准备好的训练数据进行训练。
14.3 构建用于生成音乐的 GPT
现在我们已经准备好了训练数据,我们将从头开始构建一个用于音乐生成的 GPT 模型。这个模型的架构将与我们在第十一章中开发的 GPT-2XL 模型和第十二章中的文本生成器相似。然而,由于我们选择的特定超参数,我们的音乐 Transformer 的大小将有所不同。
为了节省空间,我们将模型构建放在本地模块 ch14util.py 中。在这里,我们的重点是音乐 Transformer 选择使用的超参数。具体来说,我们将决定n_layer的值,即模型中解码器的层数;n_head,用于计算因果自注意力的并行头数;n_embd,嵌入维度;以及block_size,输入序列中的标记数。
14.3.1 音乐 Transformer 中的超参数
打开您之前从本书的 GitHub 仓库下载的文件 ch14util.py。在里面,您会发现几个函数和类,它们与第十二章中定义的完全相同。
正如本书中我们看到的所有 GPT 模型一样,解码器块中的前馈网络使用高斯误差线性单元(GELU)激活函数。因此,我们在 ch14util.py 中定义了一个 GELU 类,这与我们在第十二章中做的一样。
我们使用一个Config()类来存储音乐 Transformer 中使用的所有超参数:
from torch import nn
class Config():
def __init__(self):
self.n_layer = 6
self.n_head = 8
self.n_embd = 512
self.vocab_size = 390
self.block_size = 2048
self.embd_pdrop = 0.1
self.resid_pdrop = 0.1
self.attn_pdrop = 0.1
config=Config()
device="cuda" if torch.cuda.is_available() else "cpu"
Config()类中的属性作为我们音乐 Transformer 的超参数。我们将n_layer属性的值设置为 6,表示我们的音乐 Transformer 由 6 个解码器层组成。这比我们在第十二章中构建的 GPT 模型中的解码器层数要多。每个解码器层处理输入序列并引入一个抽象或表示的层次。随着信息穿越更多层,模型能够捕捉到数据中更复杂的模式和关系。这种深度对于我们的音乐 Transformer 理解并生成复杂的音乐作品至关重要。
n_head属性设置为 8,表示在计算因果自注意力时,我们将查询 Q、键 K 和值 V 向量分为八个并行头。n_embd属性设置为 512,表示嵌入维度为 512:每个事件标记将由一个包含 512 个值的向量表示。vocab_size属性由词汇表中的唯一标记数量确定,为 390。如前所述,有 388 个唯一的事件标记,我们添加了 1 个标记来表示序列的结束,并添加了另一个标记来填充较短的序列,以便所有序列的长度都为 2,048。block_size属性设置为 2,048,表示输入序列最多包含 2,048 个标记。我们将 dropout 比率设置为 0.1,与第十一章和第十二章相同。
与所有 Transformer 一样,我们的音乐 Transformer 使用自注意力机制来捕捉序列中不同元素之间的关系。因此,我们在本地模块 ch14util 中定义了一个CausalSelfAttention()类,它与第十二章中定义的CausalSelfAttention()类相同。
14.3.2 构建音乐 Transformer
我们将前馈网络与因果自注意力子层结合形成一个解码块(即解码层)。我们对每个子层应用层归一化和残差连接以提高稳定性和性能。为此,我们在本地模块中定义了一个Block()类来创建解码块,它与我们在第十二章中定义的Block()类相同。
然后,我们在音乐 Transformer 的上方堆叠六个解码块,形成其主体。为了实现这一点,我们在本地模块中定义了一个Model()类。正如我们在本书中看到的所有 GPT 模型一样,我们使用通过 PyTorch 中的Embedding()类学习到的位置编码,而不是原始 2017 年论文“Attention Is All You Need”中的固定位置编码。有关两种位置编码方法之间的差异,请参阅第十一章。
模型的输入由对应于词汇表中音乐事件标记的索引序列组成。我们将输入通过词嵌入和位置编码传递,并将两者相加以形成输入嵌入。然后,输入嵌入通过六个解码层。之后,我们对输出应用层归一化,并将其连接到一个线性头,以便输出的数量为 390,即词汇表的大小。输出是词汇表中 390 个标记的 logits。稍后,我们将对 logits 应用 softmax 激活函数,以获得生成音乐时词汇表中唯一音乐标记的概率分布。
接下来,我们将通过实例化我们在本地模块中定义的Model()类来创建我们的音乐 Transformer:
from utils.ch14util import Model
model=Model(config)
model.to(device)
num=sum(p.numel() for p in model.transformer.parameters())
print("number of parameters: %.2fM" % (num/1e6,))
print(model)
输出是
number of parameters: 20.16M
Model(
(transformer): ModuleDict(
(wte): Embedding(390, 512)
(wpe): Embedding(2048, 512)
(drop): Dropout(p=0.1, inplace=False)
(h): ModuleList(
(0-5): 6 x Block(
(ln_1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
(attn): CausalSelfAttention(
(c_attn): Linear(in_features=512, out_features=1536, bias=True)
(c_proj): Linear(in_features=512, out_features=512, bias=True)
(attn_dropout): Dropout(p=0.1, inplace=False)
(resid_dropout): Dropout(p=0.1, inplace=False)
)
(ln_2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
(mlp): ModuleDict(
(c_fc): Linear(in_features=512, out_features=2048, bias=True)
(c_proj): Linear(in_features=2048, out_features=512, bias=True)
(act): GELU()
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
(ln_f): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
)
(lm_head): Linear(in_features=512, out_features=390, bias=False)
)
我们的音乐 Transformer 由 2016 万个参数组成,这个数字比拥有超过 15 亿个参数的 GPT-2XL 小得多。尽管如此,我们的音乐 Transformer 的规模超过了我们在第十二章中构建的仅包含 512 万个参数的文本生成器。尽管存在这些差异,所有三个模型都是基于仅解码器 Transformer 架构。差异仅在于超参数,如嵌入维度、解码器层数、词汇量大小等。
14.4 训练和使用音乐 Transformer
在本节中,您将使用本章前期准备好的训练数据批次来训练您刚刚构建的音乐 Transformer。为了加快过程,我们将对模型进行 100 个周期的训练,然后停止训练过程。对于感兴趣的人来说,您可以使用验证集来确定何时停止训练,根据模型在验证集上的性能,就像我们在第二章中所做的那样。
一旦模型训练完成,我们将以一系列索引的形式提供给它一个提示。然后,我们将请求训练好的音乐 Transformer 生成下一个索引。这个新的索引被附加到提示中,更新的提示被送回模型进行另一个预测。这个过程会迭代重复,直到序列达到一定的长度。
与第十三章中生成的音乐不同,我们可以通过应用不同的温度来控制音乐作品的创造性。
14.4.1 训练音乐 Transformer
和往常一样,我们将使用 Adam 优化器进行训练。鉴于我们的音乐 Transformer 实质上执行的是一个多类别分类任务,我们将使用交叉熵损失作为我们的损失函数:
lr=0.0001
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
loss_func=torch.nn.CrossEntropyLoss(ignore_index=389)
在之前的损失函数中,ignore_index=389 参数指示程序在目标序列(即序列 y)中遇到索引 389 时忽略它,因为这个索引仅用于填充目的,并不代表音乐作品中的任何特定事件标记。
我们将接着对模型进行 100 个周期的训练。
列表 14.6 训练音乐 Transformer 生成音乐
model.train()
for i in range(1,101):
tloss = 0.
for idx, (x,y) in enumerate(trainloader): ①
x,y=x.to(device),y.to(device)
output = model(x)
loss=loss_func(output.view(-1,output.size(-1)),
y.view(-1)) ②
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(),1) ③
optimizer.step() ④
print(f'epoch {i} loss {tloss/(idx+1)}')
torch.save(model.state_dict(),f'files/musicTrans.pth') ⑤
① 遍历所有训练数据批次
② 将模型预测与实际输出进行比较
③ 将梯度范数裁剪到 1
④ 调整模型参数以最小化损失
⑤ 训练后保存模型
在训练过程中,我们将所有输入序列 x 在一个批次中通过模型来获得预测。然后,我们将这些预测与批次中的相应输出序列 y 进行比较,并计算交叉熵损失。之后,我们调整模型参数以最小化这个损失。需要注意的是,我们已经将梯度范数裁剪到 1,以防止潜在的梯度爆炸问题。
如果您有 CUDA 支持的 GPU,上述训练过程大约需要 3 小时。训练完成后,训练好的模型权重 musicTrans.pth 将保存在您的计算机上。或者,您可以从我的网站mng.bz/V2pW下载训练好的权重。
14.4.2 使用训练好的 Transformer 进行音乐生成
现在我们已经训练了一个音乐 Transformer,我们可以进行音乐生成了。
与文本生成过程类似,音乐生成始于将一系列索引(代表事件标记)作为提示输入到模型中。我们将从测试集中选择一首音乐作品,并使用前 250 个音乐事件作为提示:
from utils.processor import decode_midi
prompt, _ = test[42]
prompt = prompt.to(device)
len_prompt=250
file_path = "files/prompt.midi"
decode_midi(prompt[:len_prompt].cpu().numpy(),
file_path=file_path)
我们随机选择了一个索引(在我们的例子中是 42)并使用它从测试子集中检索一首歌曲。我们只保留前 250 个音乐事件,稍后我们将这些事件输入到训练好的模型中以预测下一个音乐事件。为了比较,我们将提示保存为 MIDI 文件 prompt.midi 到本地文件夹中。
练习 14.2
使用decode_midi()函数将测试集中第二首音乐的第一个 250 个音乐事件转换为 MIDI 文件。将其保存在您的计算机上的 prompt2.midi。
为了简化音乐生成过程,我们将定义一个sample()函数。该函数接受一个索引序列作为输入,代表一小段音乐。然后它迭代地预测并追加新的索引到序列中,直到达到指定的长度seq_length。实现方式如下所示。
列表 14.7 音乐生成中的sample()函数
softmax=torch.nn.Softmax(dim=-1)
def sample(prompt,seq_length=1000,temperature=1):
gen_seq=torch.full((1,seq_length),389,dtype=torch.long).to(device)
idx=len(prompt)
gen_seq[..., :idx]=prompt.type(torch.long).to(device)
while(idx < seq_length): ①
y=softmax(model(gen_seq[..., :idx])/temperature)[...,:388] ②
probs=y[:, idx-1, :]
distrib=torch.distributions.categorical.Categorical(probs=probs)
next_token=distrib.sample() ③
gen_seq[:, idx]=next_token
idx+=1
return gen_seq[:, :idx] ④
① 生成新的索引直到序列达到一定长度
② 将预测值除以温度,然后在 logits 上应用 softmax 函数
③ 从预测的概率分布中采样以生成新的索引
④ 输出整个序列
sample()函数的一个参数是温度,它调节生成音乐的创造性。如有需要,请参考第八章了解其工作原理。由于我们可以仅通过温度参数调整生成音乐的原创性和多样性,因此在此实例中省略了top-K采样以简化过程。正如我们在本书中之前三次讨论过top-K采样(在第 8、11 和 12 章),感兴趣的读者可以尝试将top-K采样结合到sample()函数中。
接下来,我们将加载训练好的权重到模型中:
model.load_state_dict(torch.load("files/musicTrans.pth",
map_location=device))
model.eval()
然后,我们调用sample()函数来生成一段音乐:
from utils.processor import encode_midi
file_path = "files/prompt.midi"
prompt = torch.tensor(encode_midi(file_path))
generated_music=sample(prompt, seq_length=1000)
首先,我们利用处理器模块中的encode_midi()函数将 MIDI 文件 prompt.midi 转换为索引序列。然后我们使用这个序列作为sample()函数中的提示来生成由 1,000 个索引组成的音乐作品。
最后,我们将生成的索引序列转换为 MIDI 格式:
music_data = generated_music[0].cpu().numpy()
file_path = 'files/musicTrans.midi'
decode_midi(music_data, file_path=file_path)
我们在 processor.py 模块中使用了decode_midi()函数,将生成的索引序列转换成您电脑上的 MIDI 文件,即 musicTrans.midi。在您的电脑上打开这两个文件,prompt.midi 和 musicTrans.midi,并聆听它们。prompt.midi 中的音乐大约持续 10 秒。musicTrans.midi 中的音乐大约持续 40 秒,最后的 30 秒是由音乐变换器生成的新音乐。生成的音乐应该听起来像我网站上的音乐作品:mng.bz/x6dg。
上述代码块可能产生类似于以下输出的结果:
info removed pitch: 52
info removed pitch: 83
info removed pitch: 55
info removed pitch: 68
在生成的音乐中,可能会有一些音符需要被移除。例如,如果生成的音乐作品尝试关闭音符 52,但音符 52 最初从未被开启,那么我们就不能关闭它。因此,我们需要移除这样的音符。
练习 14.3
使用训练好的音乐变换器模型生成包含 1,200 个音符的音乐作品,保持温度参数为 1。使用您在 14.2 练习中生成的 prompt2.midi 文件中的索引序列作为提示。将生成的音乐保存到您电脑上的名为 musicTrans2.midi 的文件中。
您可以通过将温度参数设置为大于 1 的值来提高音乐的创意,如下所示:
file_path = "files/prompt.midi"
prompt = torch.tensor(encode_midi(file_path))
generated_music=sample(prompt, seq_length=1000,temperature=1.5)
music_data = generated_music[0].cpu().numpy()
file_path = 'files/musicHiTemp.midi'
decode_midi(music_data, file_path=file_path)
我们将温度设置为 1.5。生成的音乐保存为 musicHiTemp.midi 文件在您的电脑上。打开该文件并聆听,看看您是否能辨别出与 musicTrans.midi 文件中的音乐相比有任何差异。
练习 14.4
使用训练好的音乐变换器模型生成包含 1,000 个索引的音乐作品,将温度参数设置为 0.7。使用 prompt.midi 文件中的索引序列作为提示。将生成的音乐保存到您电脑上的名为 musicLowTemp.midi 的文件中。打开此文件聆听生成的音乐,看看新作品与 musicTrans.midi 文件中的音乐之间是否有可辨别的差异。
在本章中,您已经学习了如何从头开始构建和训练音乐变换器,基于您在前面章节中使用的仅解码器变换器架构。在下一章中,您将探索基于扩散的模型,这些模型是像 OpenAI 的 DALL-E 2 和 Google 的 Imagen 这样的文本到图像变换器的核心。
摘要
-
音乐的性能表示使我们能够将音乐作品表示为一串音符,这些音符包括控制信息和速度值。这些音符可以进一步简化为四种音乐事件:音符开启、音符关闭、时间移动和速度。每种事件类型可以假设各种值。因此,我们可以将音乐作品转换为一串标记,然后转换为索引。
-
音乐 Transformer 架构是对最初为 NLP 任务设计的 Transformer 架构进行适配,用于音乐生成。该模型旨在通过学习大量现有音乐数据集来生成音乐音符序列。它通过识别训练数据中各种音乐元素之间的模式、结构和关系,被训练来根据前面的音符预测序列中的下一个音符。
-
正如文本生成一样,我们可以使用温度来调节生成音乐的创造力。
^(1) Chloe Veltman,2024 年 3 月 15 日。“仅仅因为你的最爱歌手已经去世,并不意味着你不能看到他们‘现场’。” mng.bz/r1de。
^(2) Cheng-Zhi Anna Huang, Ashish Vaswani, Jakob Uszkoreit, Noam Shazeer, Ian Simon, Curtis Hawthorne, Andrew M. Dai, Matthew D. Hoffman, Monica Dinculescu, 和 Douglas Eck,2018 年,“Music Transformer。” arxiv.org/abs/1809.04281。
^(3) 例如,参见 Hawthorne 等人,2018 年,“使用 MAESTRO 数据集实现分解钢琴音乐建模和生成。” arxiv.org/abs/1810.12247。
第十五章:种扩散模型和文本到图像 Transformer
本章涵盖
-
前向扩散和反向扩散是如何工作的
-
如何构建和训练去噪 U-Net 模型
-
使用训练好的 U-Net 生成花卉图像
-
文本到图像 Transformer 背后的概念
-
编写 Python 程序通过 DALL-E 2 使用文本生成图像
近年来,多模态大型语言模型(LLMs)因其处理各种内容格式的能力而受到广泛关注,例如文本、图像、视频、音频和代码。这一领域的显著例子包括 OpenAI 的 DALL-E 2、Google 的 Imagen 和 Stability AI 的 Stable Diffusion 等文本到图像 Transformer。这些模型能够根据文本描述生成高质量的图像。
这些文本到图像模型包括三个基本组件:一个文本编码器,它将文本压缩成潜在表示;一种将文本信息融入图像生成过程的方法;以及一种扩散机制,用于逐步细化图像以产生逼真的输出。理解扩散机制对于理解文本到图像 Transformer 尤其关键,因为扩散模型构成了所有领先文本到图像 Transformer 的基础。因此,你将首先在本章中构建和训练一个扩散模型来生成花卉图像。这将帮助你深入理解前向扩散过程,其中噪声逐步添加到图像中,直到它们变成随机噪声。随后,你将训练一个模型通过逐步从图像中移除噪声来逆转扩散过程,直到模型可以从随机噪声中生成一个新、干净的图像,类似于训练数据集中的图像。
扩散模型已成为生成高分辨率图像的首选选择。扩散模型的成功在于它们能够模拟和逆转复杂的噪声添加过程,这模仿了对图像结构和如何从抽象模式中构建图像的深入理解。这种方法不仅确保了高质量,而且在生成的图像中保持了多样性和准确性的平衡。
之后,我们将解释文本到图像 Transformer 在概念上的工作方式。我们将重点关注由 OpenAI 开发的对比语言-图像预训练(CLIP)模型,该模型旨在理解和关联视觉和文本信息。CLIP 处理两种类型的输入:图像和文本(通常是标题或描述的形式)。这些输入通过模型中的两个编码器分别处理。
CLIP 的图像分支使用视觉 Transformer (ViT)将图像编码到高维向量空间中,在这个过程中提取视觉特征。同时,文本分支使用基于 Transformer 的语言模型将文本描述编码到相同的向量空间中,从文本中捕获语义特征。CLIP 已经在许多匹配图像和文本描述的配对上进行了训练,以使向量空间中匹配对的表示紧密对齐。
OpenAI 的文本到图像的 Transformers,如 DALL-E 2,将 CLIP 作为一个核心组件。在本章中,你将学习如何获取 OpenAI API 密钥,并编写一个 Python 程序,根据文本描述使用 DALL-E 2 生成图像。
15.1 去噪扩散模型简介
使用以下示例可以说明基于扩散的模型的概念。考虑使用基于扩散的模型生成高分辨率花卉图像的目标。为此,你首先需要获取一组高质量的花卉图像用于训练。然后,指导模型逐步将这些图像中引入少量随机噪声,这个过程被称为正向扩散。经过多次添加噪声的步骤后,训练图像最终变成随机噪声。下一阶段涉及训练模型逆转这个过程,从纯噪声图像开始,逐步减少噪声,直到图像与原始训练集中的图像无法区分。
一旦训练完成,模型将得到随机噪声图像进行处理。它通过多次迭代系统地消除图像中的噪声,直到生成一个与训练集中相似的、高分辨率的花卉图像。这就是基于扩散模型的底层原理。1
在本节中,你将首先探索基于扩散的模型的数学基础。然后,你将深入了解 U-Nets 的架构,这是一种用于去噪图像和生成高分辨率花卉图像的模型类型。具体来说,U-Net 采用缩放点积注意力(SDPA)机制,类似于你在第九章到第十二章中看到的 Transformer 模型。最后,你将学习基于扩散的模型的训练过程以及训练模型的图像生成过程。
15.1.1 正向扩散过程
几篇论文提出了具有类似底层机制的基于扩散的模型。2 让我们以花卉图像作为一个具体的例子来解释去噪扩散模型背后的思想。图 15.1 展示了正向扩散过程的工作原理。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu.png
图 15.1 正向扩散过程的示意图。我们从训练集中的干净图像x[0]开始,向其添加噪声є[0],形成噪声图像x[1] = √(1 – β[1])x[0] + √(β[1])є[0]。我们重复这个过程 1000 次,直到图像x[1000]变成随机噪声。
假设花朵图像 x[0](如图 15.1 左侧图像所示)遵循 q(x) 分布。在正向扩散过程中,我们将向每个 T = 1,000 步中的图像添加少量噪声。噪声张量是正态分布的,其形状与花朵图像相同:(3, 64, 64),表示三个颜色通道,高度和宽度为 64 像素。
扩散模型中的时间步
在扩散模型中,时间步指的是在逐渐向数据添加噪声并随后逆转此过程以生成样本的过程中所经历的离散阶段。扩散模型的前向阶段在一系列时间步中逐步添加噪声,将数据从其原始、干净的状态转换为噪声分布。在反向阶段,模型在类似的一系列时间步中操作,但顺序相反。它系统地从数据中去除噪声以重建原始数据或生成新的、高保真度的样本。在此反向过程的每个时间步中,都涉及预测在相应的正向步骤中添加的噪声并将其减去,从而逐渐去除数据直到达到干净状态。
在时间步 1 中,我们将噪声 є[0] 添加到图像 x[0],从而获得一个噪声图像 x[1]:
|
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ01.png
| (15.1) |
|---|
即,x[1] 是 x[0] 和 є[0] 的加权求和,其中 β[1] 衡量了噪声的权重。β 的值在不同时间步中会变化——因此 β[1] 中的下标。如果我们假设 x[0] 和 є[0] 之间相互独立,并且遵循标准正态分布(即,均值为 0,方差为 1),则噪声图像 x[1] 也将遵循标准正态分布。这很容易证明,因为
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ02.png
和
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ03.png
我们可以在接下来的 T-1 个时间步中继续向图像添加噪声,以便
|
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ04.png
| (15.2) |
|---|
我们可以使用重新参数化技巧并定义 α[t] = 1 − β[t] 和
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ05.png
以便我们可以在任意时间步 t 对 x[t] 进行采样,其中 t 可以取 [1, 2, . . ., T−1, T] 中的任何值。然后我们有
|
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F01_Liu-EQ06.png
| (15.3) |
|---|
其中 є 是 є[0]、є[1]、…、є*[t]*[–1] 的组合,利用我们可以将两个正态分布相加以获得一个新的正态分布这一事实。例如,请参阅 Lilian Weng 的博客 mng.bz/Aalg 以获取证明。
图 15.1 的左侧显示了来自训练集的干净花朵 x[0]。在第一步中,我们向其注入噪声 є[0],以形成噪声图像 x[1](图 15.1 中的第二个图像)。我们重复此过程 1,000 个时间步,直到图像变成随机噪声(最右侧的图像)。
15.1.2 使用 U-Net 模型进行图像去噪
现在你已经理解了正向扩散过程,让我们来讨论反向扩散过程(即去噪过程)。如果我们能够训练一个模型来逆转正向扩散过程,我们可以向模型输入随机噪声并要求它生成一个带噪声的花朵图像。然后我们可以将这个带噪声的图像再次输入到训练好的模型中,生成一个更清晰、但仍然带噪声的图像。我们可以迭代重复这个过程多次,直到我们获得一个干净的图像,与训练集中的图像无法区分。在反向扩散过程中使用多个推理步骤,而不是仅仅一个步骤,对于逐渐从噪声分布中重建高质量数据至关重要。它允许更可控、稳定且高质量地生成数据。
为了达到这个目的,我们将创建一个去噪 U-Net 模型。U-Net 架构最初是为生物医学图像分割设计的,其特点是具有对称形状,包括收缩路径(编码器)和扩展路径(解码器),通过瓶颈层连接。在去噪的背景下,U-Net 模型被调整为从图像中去除噪声,同时保留重要细节。U-Net 在去噪任务中优于简单的卷积网络,因为它们能够有效地捕捉图像中的局部和全局特征。
图 15.2 是本章中使用的去噪 U-Net 结构图。
该模型以一个带噪声的图像和带噪声图像的时间步长(x[t] 和方程 15.3 中的 t)作为输入,并预测图像中的噪声(即 є)。由于带噪声的图像是原始干净图像和噪声的加权总和(参见方程 15.3),了解噪声使我们能够推断并重建原始图像。
收缩路径(即编码器;图 15.2 的左侧)由多个卷积层和池化层组成。它逐步下采样图像,在不同抽象级别提取和编码特征。这一部分网络学会识别与去噪相关的模式和特征。
瓶颈层(图 15.2 的底部)连接编码器和解码器路径。它由卷积层组成,负责捕捉图像的最抽象表示。
扩展路径(即解码器;图 15.2 的右侧)由上采样层和卷积层组成。它逐步上采样特征图,在通过跳跃连接结合编码器特征的同时重建图像。跳跃连接(图 15.2 中用虚线表示)在 U-Net 模型中至关重要,因为它们允许模型通过结合低级和高级特征来保留输入图像的精细细节。接下来,我将简要解释跳跃连接是如何工作的。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F02_Liu.png
图 15.2 去噪 U-Net 模型架构。U-Net 架构以其对称形状为特征,包括收缩路径(编码器)和扩展路径(解码器),通过瓶颈层连接。该模型旨在去除图像中的噪声,同时保留重要细节。模型的输入是一个噪声图像,以及图像所在的时间步,输出是图像中的预测噪声。
在 U-Net 模型中,跳过连接是通过将编码路径的特征图与解码路径中的相应特征图连接起来实现的。这些特征图通常具有相同的空间维度,但由于它们经过的路径不同,可能已经以不同的方式处理过。在编码过程中,输入图像逐渐下采样,一些空间信息(如边缘和纹理)可能会丢失。跳过连接通过直接从编码器传递特征图到解码器,绕过信息瓶颈,帮助保留这些信息。
例如,图 15.2 顶部的虚线表示模型将编码器中的 Conv2D 层的输出(形状为(128,64,64))与解码器中的 Conv2D 层的输入(形状也为(128,64,64))连接起来。因此,解码器中 Conv2D 层的最终输入形状为(256,64,64)。
通过将解码器中的高级、抽象特征与编码器中的低级、详细特征相结合,跳过连接使模型能够更好地重建去噪图像中的细微细节。这在去噪任务中尤为重要,因为保留细微的图像细节至关重要。
在我们的去噪 U-Net 模型中,缩放点积注意力(SDPA)机制在收缩路径的最后一个块和扩展路径的最后一个块中实现,伴随着层归一化和残差连接(如图 15.2 中标记为 Attn/Norm/Add)。这个 SDPA 机制本质上与我们第九章中开发的相同;关键区别在于它应用于图像像素而不是文本标记。
跳过连接的使用和模型的大小导致我们的去噪 U-Net 中存在冗余的特征提取,确保在去噪过程中不会丢失任何重要特征。然而,模型的大尺寸也使得识别相关特征变得复杂,就像在干草堆里找针一样。注意力机制赋予模型强调显著特征并忽略不相关特征的能力,从而增强了学习过程的有效性。
15.1.3 训练去噪 U-Net 模型的蓝图
去噪 U-Net 的输出是注入到噪声图像中的噪声。该模型经过训练以最小化输出(预测噪声)与真实噪声之间的差异。
去噪 U-Net 模型利用 U-Net 架构捕捉局部和全局上下文的能力,使其在去除噪声的同时保留重要细节(如边缘和纹理)变得有效。这些模型在各种应用中得到了广泛应用,包括医学图像去噪、摄影图像恢复等。图 15.3 展示了我们去噪 U-Net 模型的训练过程。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F03_Liu.png
图 15.3 去噪 U-Net 模型的训练过程。我们首先获取干净的花卉图像作为我们的训练集。我们向干净的花卉图像添加噪声,并将其呈现给 U-Net 模型。模型预测噪声图像中的噪声。我们将预测的噪声与注入花卉图像的实际噪声进行比较,并调整模型权重以最小化平均绝对误差。
第一步是收集花卉图像数据集。我们将使用牛津 102 花卉数据集作为我们的训练集。我们将所有图像调整到 64 × 64 像素的固定分辨率,并将像素值归一化到范围[–1, 1]。对于去噪,我们需要成对的干净和噪声图像。我们将根据方程 15.3 中指定的公式,对干净的花卉图像添加噪声,以创建噪声对应图像(图 15.3 中的步骤 2)。
然后,我们将根据图 15.2 中概述的结构构建一个去噪 U-Net 模型。在训练的每个 epoch 中,我们以批处理的方式遍历数据集。我们向花卉图像添加噪声,并将噪声图像及其在噪声图像中的时间步长 t 呈现给 U-Net 模型(步骤 3)。U-Net 模型根据模型中的当前参数预测噪声图像中的噪声(步骤 4)。
我们将预测的噪声与实际噪声进行比较,并在像素级别计算 L1 损失(即平均绝对误差)(步骤 5)。在这种情况下,L1 损失通常比 L2 损失(均方误差)对异常值更不敏感,因此更受欢迎。然后,我们调整模型参数以最小化 L1 损失(步骤 6),以便在下一轮迭代中,模型能做出更好的预测。我们将重复此过程多次,直到模型参数收敛。
15.2 准备训练数据
我们将使用牛津 102 花卉数据集,该数据集可在 Hugging Face 上免费获取,作为我们的训练数据。该数据集包含大约 8,000 张花卉图像,您可以使用之前安装的datasets库直接下载。
为了节省空间,我们将大多数辅助函数和类放在两个本地模块中,ch15util.py 和 unet_util.py。从本书的 GitHub 仓库(github.com/markhliu/DGAI)下载这两个文件,并将它们放在您计算机上的/utils/文件夹中。本章中的 Python 程序是从 Hugging Face 的 GitHub 仓库(github.com/huggingface/diffusers)和 Filip Basara 的 GitHub 仓库(github.com/filipbasara0/simple-diffusion)改编的。
您将使用 Python 将数据集下载到您的计算机上。之后,我们将通过逐渐向训练数据集中的干净图像添加噪声,直到它们变成随机噪声,来演示前向扩散过程。最后,您将批量放置训练数据,以便我们可以在本章后面使用它们来训练去噪 U-Net 模型。
您在本章中将使用以下 Python 库:datasets、einops、diffusers 和 openai。要安装这些库,请在您的计算机上的 Jupyter Notebook 应用程序的新单元格中执行以下代码行:
!pip install datasets einops diffusers openai
按照屏幕上的说明完成安装。
15.2.1 将花卉图像作为训练数据
您之前安装的datasets库中的load_dataset()方法允许您直接从 Hugging Face 下载牛津 102 花卉数据集。然后我们将使用matplotlib库显示数据集中的某些花卉图像,以便我们了解训练数据集中的图像是什么样的。
在 Jupyter Notebook 的单元格中运行以下列表中的代码行。
列表 15.1 下载和可视化花卉图像
from datasets import load_dataset
from utils.ch15util import transforms
dataset = load_dataset("huggan/flowers-102-categories",
split="train",) ①
dataset.set_transform(transforms)
import matplotlib.pyplot as plt
from torchvision.utils import make_grid
# Plot all the images of the 1st batch in grid
grid = make_grid(dataset[:16]["input"], 8, 2) ②
plt.figure(figsize=(8,2),dpi=300)
plt.imshow(grid.numpy().transpose((1,2,0)))
plt.axis("off")
plt.show()
① 从 Hugging Face 下载图像
② 绘制前 16 张图像
在运行前面的代码列表后,您将看到数据集中的前 16 张花卉图像,如图 15.4 所示。这些是各种类型花卉的高分辨率彩色图像。我们已经将每张图像的大小标准化为(3,64,64)。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F04_Liu.png
图 15.4 来自牛津 102 花卉数据集的前 16 张图像。
我们将数据集分成每批 4 个,以便我们可以在本章后面使用它们来训练去噪 U-Net 模型。我们选择批大小为 4,以保持内存大小足够小,以便在训练过程中适合 GPU。如果您的 GPU 内存较小,请将批大小调整为 2 甚至 1:
import torch
resolution=64
batch_size=4
train_dataloader=torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True)
接下来,我们将编写代码并可视化前向扩散过程。
15.2.2 可视化前向扩散过程
我们在本地模块 ch15util.py 中定义了一个 DDIMScheduler() 类,您刚刚下载。请查看该文件中的类;我们将使用它向图像添加噪声。我们还将使用该类来生成干净的图像,以及训练好的去噪 U-Net 模型。DDIMScheduler() 类管理步长大小和去噪步骤的顺序,通过去噪过程实现确定性推理,从而可以生成高质量的样本。
我们首先从训练集中选择四张干净图像,并生成与这些图像形状相同的噪声张量:
clean_images=next(iter(train_dataloader))["input"]*2-1 ①
print(clean_images.shape)
nums=clean_images.shape[0]
noise=torch.randn(clean_images.shape) ②
print(noise.shape)
① 获取四张干净图像
② 生成一个与干净图像形状相同的张量,噪声;噪声中的每个值都遵循独立的标准正态分布。
前一个代码块输出的结果是
torch.Size([4, 3, 64, 64])
torch.Size([4, 3, 64, 64])
图像和噪声张量的形状都是 (4, 3, 64, 64),这意味着批处理中有 4 张图像,每张图像有 3 个颜色通道,图像的高度和宽度为 64 像素。
在正向扩散过程中,在干净图像 (x[0] 如我们在第一部分中解释的) 和随机噪声 (x[T]) 之间有 999 个过渡噪声图像。过渡噪声图像是干净图像和噪声的加权总和。随着 t 从 0 到 1,000 的变化,干净图像的权重逐渐降低,噪声的权重逐渐增加,如方程式 15.3 所述。
接下来,我们生成并可视化一些过渡噪声图像。
列表 15.2 可视化正向扩散过程
from utils.ch15util import DDIMScheduler
noise_scheduler=DDIMScheduler(num_train_timesteps=1000) ①
allimgs=clean_images
for step in range(200,1001,200): ②
timesteps=torch.tensor([step-1]*4).long()
noisy_images=noise_scheduler.add_noise(clean_images,
noise, timesteps) ③
allimgs=torch.cat((allimgs,noisy_images)) ④
import torchvision
imgs=torchvision.utils.make_grid(allimgs,4,6)
fig = plt.figure(dpi=300)
plt.imshow((imgs.permute(2,1,0)+1)/2) ⑤
plt.axis("off")
plt.show()
① 使用 1,000 个时间步创建 DDIMScheduler() 类实例
② 查看时间步 200、400、600、800 和 1,000
③ 在这些时间步创建噪声图像
④ 将噪声图像与干净图像连接
⑤ 显示所有图像
DDIMScheduler() 类中的 add_noise() 方法接受三个参数:clean_images、noise 和 timesteps。它产生干净图像和噪声的加权总和,即噪声图像。此外,权重是时间步 t 的函数。随着时间步 t 从 0 到 1,000 的变化,干净图像的权重逐渐降低,噪声的权重逐渐增加。如果您运行前面的代码列表,您将看到类似于图 15.5 的图像。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F05_Liu.png
图 15.5 正向扩散过程。第一列中的四张图像是训练数据集中的干净图像。然后我们从时间步 1 到时间步 1,000 逐渐向这些图像添加噪声。随着时间步的增加,图像中注入的噪声越来越多。第二列中的四张图像是在 200 个时间步之后的图像。第三列包含 400 个时间步之后的图像,它们的噪声比第二列中的图像更多。最后一列包含 1,000 个时间步之后的图像,它们是 100% 的随机噪声。
第一列包含没有噪声的四个干净图像。随着向右移动,我们逐渐向图像添加越来越多的噪声。最后一列包含纯随机噪声。
15.3 构建噪声去除 U-Net 模型
在本章的早期部分,我们讨论了噪声去除 U-Net 模型的架构。在本节中,我将指导您使用 Python 和 PyTorch 来实现它。
我们将要构建的 U-Net 模型相当大,包含超过 1.33 亿个参数,反映了其预期任务的复杂性。它通过下采样和上采样输入来捕捉图像中的局部和全局特征。该模型使用多个通过跳跃连接相互连接的卷积层,这些层结合了网络不同级别的特征。这种架构有助于保持空间信息,从而促进更有效的学习。
由于噪声去除 U-Net 模型规模庞大且特征提取冗余,我们采用了 SDPA 注意力机制,使模型能够专注于当前任务中最相关的输入方面。为了计算 SDPA 注意力,我们将图像展平并将像素视为一个序列。然后我们将使用 SDPA 以类似于在第九章中学习文本中不同标记之间依赖关系的方式,学习图像中不同像素之间的依赖关系。
15.3.1 噪声去除 U-Net 模型中的注意力机制
为了实现注意力机制,我们在本地模块 ch15util.py 中定义了一个Attention()类,如下面的代码列表所示。
列表 15.3 噪声去除 U-Net 模型中的注意力机制
import torch
from torch import nn, einsum
from einops import rearrange
class Attention(nn.Module):
def __init__(self, dim, heads=4, dim_head=32):
super().__init__()
self.scale = dim_head**-0.5
self.heads = heads
hidden_dim = dim_head * heads
self.to_qkv = nn.Conv2d(dim, hidden_dim * 3, 1, bias=False)
self.to_out = nn.Conv2d(hidden_dim, dim, 1)
def forward(self, x):
b, c, h, w = x.shape
qkv = self.to_qkv(x).chunk(3, dim=1) ①
q, k, v = map(
lambda t: rearrange(t, 'b (h c) x y -> b h c (x y)', h=self.heads),
qkv) ②
q = q * self.scale
sim = einsum('b h d i, b h d j -> b h i j', q, k)
attn = sim.softmax(dim=-1) ③
out = einsum('b h i j, b h d j -> b h i d', attn, v) ④
out = rearrange(out, 'b h (x y) d -> b (h d) x y', x=h, y=w)
return self.to_out(out) ⑤
attn=Attention(128)
x=torch.rand(1,128,64,64)
out=attn(x)
print(out.shape)
① 将输入通过三个线性层传递以获得查询、键和值
② 将查询、键和值分成四个头
③ 计算注意力权重
④ 计算每个头中的注意力向量
⑤ 将四个注意力向量连接成一个
运行上述代码列表后的输出是
torch.Size([1, 128, 64, 64])
这里使用的注意力机制 SDPA 与我们在第九章中使用的相同,当时我们将 SDPA 应用于表示文本中标记的索引序列。在这里,我们将它应用于图像中的像素。我们将图像的展平像素视为一个序列,并使用 SDPA 提取输入图像不同区域之间的依赖关系,从而提高去噪过程的效率。
列表 15.3 展示了 SDPA 在我们上下文中的操作方式。为了给您一个具体的例子,我们创建了一个假设的图像,x,其维度为(1,128,64,64),表示批次中的一个图像,128 个特征通道,每个通道的大小为 64 × 64 像素。然后输入 x 通过注意力层进行处理。具体来说,图像中的每个特征通道被展平成一个 64 × 64 = 4,096 像素的序列。然后这个序列通过三个不同的神经网络层传递,以产生查询 Q、键 K 和值 V,随后将它们分成四个头。每个头中的注意力向量计算如下:
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F05_Liu-EQ07.png
其中 d[k] 代表关键向量 K 的维度。来自四个头的注意力向量被连接回一个单独的注意力向量。
15.3.2 去噪 U-Net 模型
在您刚刚下载的本地模块 unet_util.py 中,我们定义了一个 UNet() 类来表示去噪 U-Net 模型。请查看文件中的定义,稍后我会简要解释其工作原理。以下代码列表展示了 UNet() 类的一部分。
列表 15.4 定义 UNet() 类
class UNet(nn.Module):
…
def forward(self, sample, timesteps): ①
if not torch.is_tensor(timesteps):
timesteps = torch.tensor([timesteps],
dtype=torch.long,
device=sample.device)
timesteps = torch.flatten(timesteps)
timesteps = timesteps.broadcast_to(sample.shape[0])
t_emb = sinusoidal_embedding(timesteps, self.hidden_dims[0])
t_emb = self.time_embedding(t_emb) ②
x = self.init_conv(sample)
r = x.clone()
skips = []
for block1, block2, attn, downsample in self.down_blocks: ③
x = block1(x, t_emb)
skips.append(x)
x = block2(x, t_emb)
x = attn(x)
skips.append(x)
x = downsample(x)
x = self.mid_block1(x, t_emb)
x = self.mid_attn(x)
x = self.mid_block2(x, t_emb) ④
for block1, block2, attn, upsample in self.up_blocks:
x = torch.cat((x, skips.pop()), dim=1) ⑤
x = block1(x, t_emb)
x = torch.cat((x, skips.pop()), dim=1)
x = block2(x, t_emb)
x = attn(x)
x = upsample(x)
x = self.out_block(torch.cat((x, r), dim=1), t_emb)
out = self.conv_out(x)
return {"sample": out} ⑥
① 模型接受一批噪声图像和时间步长作为输入。
② 将嵌入的时间步长作为输入添加到图像的各个阶段。
③ 将输入通过收缩路径传递
④ 通过瓶颈路径传递输入
⑤ 通过具有跳跃连接的扩张路径传递输入
⑥ 输出是输入图像中预测的噪声。
去噪 U-Net 的任务是根据这些图像所在的时间步长预测输入图像中的噪声。如方程 15.3 所述,任何时间步长 t 的噪声图像 x[t] 可以表示为干净图像 x[o] 和标准正态分布的随机噪声 є 的加权总和。分配给干净图像的权重随时间步长 t 从 0 到 T 的进展而减少,分配给随机噪声的权重增加。因此,为了推断噪声图像中的噪声,去噪 U-Net 需要知道噪声图像所在的时间步长。
时间步长使用类似于 Transformer(在第九章和第十章中讨论)中的位置编码的正弦和余弦函数进行嵌入,结果得到一个 128 值的向量。然后这些嵌入被扩展以匹配模型中各个层级的图像特征维度。例如,在第一个下采样块中,时间嵌入被广播到形状为(128,64,64),然后添加到图像特征中,这些特征也有(128,64,64)的维度。
接下来,我们通过在本地模块中实例化 UNet() 类来创建一个去噪 U-Net 模型:
from utils.unet_util import UNet
device="cuda" if torch.cuda.is_available() else "cpu"
resolution=64
model=UNet(3,hidden_dims=[128,256,512,1024],
image_size=resolution).to(device)
num=sum(p.numel() for p in model.parameters())
print("number of parameters: %.2fM" % (num/1e6,))
print(model)
输出是
number of parameters: 133.42M
模型有超过 1.33 亿个参数,您可以从之前的输出中看到。鉴于参数数量庞大,本章中的训练过程将耗时,大约需要 3 到 4 小时的 GPU 训练时间。然而,对于那些无法访问 GPU 训练的人,训练好的权重也存在于我的网站上。这些权重的链接将在下一节提供。
15.4 训练和使用去噪 U-Net 模型
现在我们已经拥有了训练数据和去噪 U-Net 模型,我们可以准备使用训练数据来训练模型了。
在每个训练周期中,我们将遍历训练数据中的所有批次。对于每张图像,我们将随机选择一个时间步长,并根据这个时间步长值在训练数据中的干净图像上添加噪声,从而得到一个噪声图像。然后,我们将这些噪声图像及其对应的时间步长值输入到去噪 U-Net 模型中,以预测每张图像中的噪声。我们将预测的噪声与真实值(实际添加到图像中的噪声)进行比较,并调整模型参数以最小化预测噪声与实际噪声之间的平均绝对误差。
训练完成后,我们将使用训练好的模型生成花朵图像。我们将通过 50 次推理步骤(即,我们将时间步长值设置为 980、960、……、20 和 0)来完成这一生成。从随机噪声开始,我们将将其输入到训练好的模型中,以获得一个噪声图像。然后,我们将这个噪声图像反馈到训练好的模型中,以对其进行去噪。我们将重复这个过程 50 次推理步骤,最终得到一个与训练集中花朵不可区分的图像。
15.4.1 训练去噪 U-Net 模型
接下来,我们首先定义训练过程中的优化器和学习率调度器。
我们将使用 AdamW 优化器,这是我们在整本书中一直在使用的 Adam 优化器的一个变体。AdamW 优化器最初由 Ilya Loshchilov 和 Frank Hutter 提出,它将权重衰减(一种正则化形式)从优化步骤中分离出来.^(3) AdamW 优化器不是直接将权重衰减应用于梯度,而是在优化步骤之后直接将权重衰减应用于参数(权重)。这种修改有助于通过防止衰减率与学习率一起调整来实现更好的泛化性能。感兴趣的读者可以在 Loshchilov 和 Hutter 的原始论文中了解更多关于 AdamW 优化器的内容。
我们还将使用 diffusers 库中的学习率调度器来调整训练过程中的学习率。最初使用较高的学习率可以帮助模型逃离局部最小值,而在训练的后期阶段逐渐降低学习率可以帮助模型更稳定、更准确地收敛到全局最小值。学习率调度器定义如下所示。
列表 15.5 在训练中选择优化器和学习率
from diffusers.optimization import get_scheduler
num_epochs=100 ①
optimizer=torch.optim.AdamW(model.parameters(),lr=0.0001,
betas=(0.95,0.999),weight_decay=0.00001,eps=1e-8) ②
lr_scheduler=get_scheduler( ③
"cosine",
optimizer=optimizer,
num_warmup_steps=300,
num_training_steps=(len(train_dataloader) * num_epochs))
① 将训练模型 100 个周期
② 使用 AdamW 优化器
③ 使用 diffusers 库中的学习率调度器来控制学习率
get_scheduler() 函数的精确定义由 Hugging Face 在 GitHub 上定义:mng.bz/ZVo5。在前 300 个训练步骤(预热步骤)中,学习率从 0 线性增加到 0.0001(我们在 AdamW 优化器中设置的学习率)。在 300 步之后,学习率按照 0.0001 和 0 之间的余弦函数值递减。在下面的列表中,我们训练模型 100 个周期。
列表 15.6 训练去噪 U-Net 模型
for epoch in range(num_epochs):
model.train()
tloss = 0
print(f"start epoch {epoch}")
for step, batch in enumerate(train_dataloader):
clean_images = batch["input"].to(device)*2-1
nums = clean_images.shape[0]
noise = torch.randn(clean_images.shape).to(device)
timesteps = torch.randint(0,
noise_scheduler.num_train_timesteps,
(nums, ),
device=device).long()
noisy_images = noise_scheduler.add_noise(clean_images,
noise, timesteps) ①
noise_pred = model(noisy_images,
timesteps)["sample"] ②
loss=torch.nn.functional.l1_loss(noise_pred, noise) ③
loss.backward()
optimizer.step() ④
lr_scheduler.step()
optimizer.zero_grad()
tloss += loss.detach().item()
if step%100==0:
print(f"step {step}, average loss {tloss/(step+1)}")
torch.save(model.state_dict(),'files/diffusion.pth')
① 在训练集中向干净图像添加噪声
② 使用去噪 U-Net 预测噪声图像中的噪声
③ 将预测的噪声与实际噪声进行比较以计算损失
④ 调整模型参数以最小化平均绝对误差
在每个周期中,我们遍历训练集中所有干净的花朵图像的批次。我们向这些干净的图像添加噪声,并将它们输入到去噪 U-Net 中以预测这些图像中的噪声。然后我们将预测的噪声与实际噪声进行比较,并调整模型参数以最小化两者之间的平均绝对误差(像素级)。
这里描述的训练过程需要几个小时,使用 GPU 训练。训练后,训练好的模型权重将保存在您的计算机上。或者,您可以从我的网站 mng.bz/RNlD 下载训练好的权重。下载后请解压文件。
15.4.2 使用训练好的模型生成花朵图像
为了生成花朵图像,我们将使用 50 个推理步骤。这意味着我们将查看 t = 0 和 t = T 之间等间隔的 50 个时间步,在我们的情况下 T = 1,000。因此,50 个推理时间步是 t = 980, 960, 940, . . . , 20, 和 0。我们将从纯随机噪声开始,这对应于 t = 1000 的图像。我们使用训练好的去噪 U-Net 模型对其进行去噪,并在 t = 980 创建一个噪声图像。然后我们将 t = 980 的噪声图像呈现给训练好的模型进行去噪,以获得 t = 960 的噪声图像。我们重复这个过程多次,直到获得 t = 0 的图像,这是一个干净的图像。这个过程通过在本地模块 ch15util.py 中的 DDIMScheduler() 类的 generate() 方法实现。
列表 15.7 在 DDIMScheduler() 类中定义 generate() 方法
@torch.no_grad()
def generate(self,model,device,batch_size=1,generator=None,
eta=1.0,use_clipped_model_output=True,num_inference_steps=50):
imgs=[]
image=torch.randn((batch_size,model.in_channels,model.sample_size,
model.sample_size),
generator=generator).to(device) ①
self.set_timesteps(num_inference_steps)
for t in tqdm(self.timesteps): ②
model_output = model(image, t)["sample"] ③
image = self.step(model_output,t,image,eta,
use_clipped_model_output=\
use_clipped_model_output) ④
img = unnormalize_to_zero_to_one(image)
img = img.cpu().permute(0, 2, 3, 1).numpy()
imgs.append(img) ⑤
image = unnormalize_to_zero_to_one(image)
image = image.cpu().permute(0, 2, 3, 1).numpy()
return {"sample": image}, imgs
① 使用随机噪声作为起点(即 t = 1,000 的图像)
② 使用 50 个推理时间步(t = 980, 960, 940, . . , 20, 0)
③ 使用训练好的去噪 U-Net 模型来预测噪声
④ 根据预测的噪声创建图像
⑤ 将中间图像保存在列表,imgs 中
在这个 generate() 方法中,我们同样创建了一个列表,imgs,用于存储所有在时间步 t = 980, 960,. . . , 20, 和 0 时的中间图像。我们将使用它们来可视化去噪过程。generate() 方法返回一个包含生成的图像和列表,imgs 的字典。
接下来,我们将使用之前的 generate() 方法创建 10 张干净的图像。
列表 15.8 使用训练好的去噪 U-Net 模型生成图像
sd=torch.load('files/diffusion.pth',map_location=device)
model.load_state_dict(sd)
with torch.no_grad():
generator = torch.manual_seed(1) ①
generated_images,imgs = noise_scheduler.generate(
model,device,
num_inference_steps=50,
generator=generator,
eta=1.0,
use_clipped_model_output=True,
batch_size=10) ②
imgnp=generated_images["sample"]
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4),dpi=300)
for i in range(10): ③
ax = plt.subplot(2,5, i + 1)
plt.imshow(imgnp[i])
plt.xticks([])
plt.yticks([])
plt.tight_layout()
plt.show()
① 设置随机种子为 1,以便结果可重复
② 使用定义的 generate()方法创建 10 张干净的图像
③ 绘制生成的图像
我们将随机种子设置为 1。因此,如果您使用我网站上的训练模型,您将得到与图 15.6 中显示的相同的结果。我们使用之前定义的generate()方法,使用 50 次推理步骤创建 10 张干净的图像。然后我们将这 10 张图像以 2×5 的网格形式绘制出来,如图 15.6 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F06_Liu.png
图 15.6 由训练好的去噪 U-Net 模型创建的花朵图像。
如您从图 15.6 中看到的那样,生成的花朵图像看起来很真实,类似于训练数据集中的图像。
练习 15.1
修改代码列表 15.8 并将随机种子改为 2。保持其余代码不变。重新运行代码列表,看看生成的图像是什么样子。
generate()方法还返回一个包含 50 个中间步骤中所有图像的列表 imgs。我们将使用它们来可视化去噪过程。
列表 15.9 可视化去噪过程
steps=imgs[9::10] ①
imgs20=[]
for j in [1,3,6,9]:
for i in range(5):
imgs20.append(steps[i][j]) ②
plt.figure(figsize=(10,8),dpi=300)
for i in range(20): ③
k=i%5
ax = plt.subplot(4,5, i + 1)
plt.imshow(imgs20[i])
plt.xticks([])
plt.yticks([])
plt.tight_layout()
plt.title(f't={800-200*k}',fontsize=15,c="r")
plt.show()
① 保持时间步 800、600、400、200 和 0
② 从 10 组花朵中选择 4 组
③ 以 4×5 的网格绘制 20 张图像
列表中的 imgs 包含所有 50 个推理步骤中的 10 组图像,t = 980, 960, …,20, 0。因此,列表中共有 500 张图像。我们选择了五个时间步(t = 800, 600, 400, 200 和 0)用于四种不同的花朵(图 15.6 中的第 2、4、7 和 10 张图像)。然后我们将这 20 张图像以 4×5 的网格形式绘制出来,如图 15.7 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F07_Liu.png
图 15.7 训练好的去噪 U-Net 模型如何逐步将随机噪声转换为干净的鲜花图像。我们将随机噪声输入到训练好的模型中,以获得时间步 980 的图像。然后我们将 t = 980 的噪声图像输入到模型中,以获得 t = 960 的图像。我们重复这个过程 50 次推理步骤,直到我们获得 t = 0 的图像。该图的第一列显示了 t = 800 时的四个花朵;第二列显示了相同的四个花朵在 t = 600 时…;最后一列显示了 t = 0(即干净的鲜花图像)的四个花朵。
图 15.7 的第一列显示了 t = 800 时的四个花朵图像。它们接近随机噪声。第二列显示了 t = 600 时的花朵,它们开始看起来像花朵。随着我们向右移动,图像变得越来越清晰。最右边的一列显示了 t = 0 时的四个干净花朵图像。
现在您已经了解了扩散模型的工作原理,我们将讨论文本到图像的生成。文本到图像的 Transformers,如 DALL-E 2、Imagen 和 Stable Diffusion 的图像生成过程与我们在本章前面讨论的逆向扩散过程非常相似,只是在生成图像时,模型将文本嵌入作为条件信号。
15.5 文本到图像的 Transformers
如 OpenAI 的 DALL-E 2、Google 的 Imagen 和 Stability AI 的 Stable Diffusion 这样的文本到图像 Transformer 使用扩散模型从文本描述生成图像。这些文本到图像 Transformer 的一个重要组成部分是扩散模型。文本到图像生成的过程涉及将文本输入编码成一个潜在表示,然后将其用作扩散模型的条件信号。这些 Transformer 通过迭代地去除随机噪声向量来学习生成与文本描述相对应的逼真图像,这个过程由编码的文本引导。
所有这些文本到图像的 Transformer 的关键在于一个能够理解不同模态内容的模型。在这种情况下,该模型必须理解文本描述并将它们与图像以及反之联系起来。
在本节中,我们将以 OpenAI 的 CLIP 模型为例。CLIP 是 DALL-E 2 的关键组件。我们将讨论 CLIP 是如何被训练来理解文本描述和图像之间的联系的。然后,我们使用一个简短的 Python 程序,通过使用 OpenAI 的 DALL-E 2,从文本提示中生成图像。
15.5.1 CLIP:一个多模态 Transformer
近年来,计算机视觉和自然语言处理(NLP)的交叉领域取得了显著进展,其中之一是 OpenAI 创建的 CLIP 模型。这个创新模型旨在理解并解释自然语言环境中的图像,这一能力在图像生成和图像分类等众多应用中具有巨大的潜力。
CLIP 模型是一个多模态 Transformer,它弥合了视觉数据和文本数据之间的差距。它通过将图像与相应的文本描述关联起来来训练理解图像。与需要显式标记图像的传统模型不同,CLIP 使用大量图像及其自然语言描述的数据集来学习视觉概念的更一般化表示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F08_Liu.png
图 15.8 OpenAI 的 CLIP 模型是如何训练的。收集了一个大规模的文本-图像对训练数据集。模型的文本编码器将文本描述压缩成 D 值文本嵌入。图像编码器将相应的图像转换成具有 D 值的图像嵌入。在训练过程中,一批 N 个文本-图像对被转换成 N 个文本嵌入和 N 个图像嵌入。CLIP 使用对比学习方法来最大化配对嵌入之间的相似性(图中对角线值的总和)同时最小化来自不匹配文本-图像对的嵌入之间的相似性(图中非对角线值的总和)。
CLIP 模型的训练,如图 15.8 所示,始于收集包含图像及其相关文本描述的大规模数据集。OpenAI 利用了多样化的来源,包括公开可用的数据集和网页爬取的数据,以确保有广泛的视觉和文本内容。然后对数据集进行预处理,以标准化图像,使它们都具有相同的形状,并对文本进行分词,为输入模型做准备。
CLIP 采用双编码器架构,包括图像编码器和文本编码器。图像编码器处理输入图像,而文本编码器处理相应的文本描述。这些编码器将图像和文本投影到一个共享的嵌入空间中,在那里它们可以被比较和对齐。
CLIP 训练的核心在于其对比学习方法。对于数据集中每个包含 N 个图像-文本对的批次,模型旨在最大化配对嵌入之间的相似性(如图 15.8 中对角线值的总和),同时最小化来自不匹配的文本-图像对嵌入之间的相似性(非对角线值的总和)。图 15.9 展示了如何基于文本提示生成逼真图像的文本到图像 Transformer,如 DALL-E 2。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F09_Liu.png
图 15.9 展示了如何基于文本提示创建图像的文本到图像 Transformer,如 DALL-E 2。在训练好的文本到图像 Transformer 中,文本编码器首先将提示中的文本描述转换为文本嵌入。文本嵌入被输入到 CLIP 模型中,以获得一个表示潜在空间中图像的先验向量。文本嵌入和先验被连接成一个条件向量。为了生成图像,U-Net 降噪器首先以一个随机噪声向量为输入,使用条件向量生成一个带噪声的图像。然后它以带噪声的图像和条件向量为输入,生成另一个图像,该图像噪声更少。这个过程重复多次,直到最终输出一个干净的图像。
文本到图像 Transformer 的图像生成过程与我们本章 earlier 讨论的逆向扩散过程类似。以 DALL-E 2 为例,它是由 OpenAI 研究人员在 2022 年提出的.^(4) 模型中的文本编码器首先将提示中的文本描述转换为文本嵌入。文本嵌入被输入到 CLIP 模型中,以获得一个表示潜在空间中图像的先验向量。文本嵌入和先验被连接成一个条件向量。在第一次迭代中,我们将一个随机噪声向量输入到模型中的 U-Net 降噪器,并要求它根据条件向量生成一个噪声图像。在第二次迭代中,我们将前一次迭代中的噪声图像输入到 U-Net 降噪器,并要求它根据条件向量生成另一个噪声图像。我们重复这个过程多次,最终输出是一个干净的图像。
15.5.2 使用 DALL-E 2 进行文本到图像生成
现在你已经了解了文本到图像 Transformer 的工作原理,让我们编写一个 Python 程序来与 DALL-E 2 交互,根据文本提示创建图像。
首先,你需要申请一个 OpenAI API 密钥。OpenAI 提供各种定价层,这些定价层根据处理的令牌数量和使用的模型类型而有所不同。访问chat.openai.com/auth/login并点击“注册”按钮创建账户。之后,登录你的账户,并访问platform.openai.com/api-keys以查看你的 API 密钥。将其保存在安全的地方以备后用。我们可以使用 OpenAI 的 DALL-E 2 生成图像。
列表 15.10 使用 DALL-E 2 生成图像
from openai import OpenAI
openai_api_key=your actual OpenAI API key here, in quotes ①
client=OpenAI(api_key=openai_api_key) ②
response = client.images.generate(
model="dall-e-2",
prompt="an astronaut in a space suit riding a unicorn",
size="512x512",
quality="standard",
n=1,
) ③
image_url = response.data[0].url
print(image_url) ④
① 确保你在这里提供实际的 OpenAI API 密钥,并用引号括起来
② 实例化 OpenAI()类以创建代理
③ 使用 images.generate()方法根据文本提示生成图像
④ 打印出图像 URL
你应该将之前获得的 OpenAI API 密钥放置在列表 15.10 中。我们通过实例化OpenAI()类来创建一个代理。要生成图像,我们需要指定模型、文本提示和图像的大小。我们使用了“一个穿着太空服的宇航员骑独角兽”作为提示,代码列表提供了我们可视化和下载图像的 URL。该 URL 在 1 小时内过期,生成的图像如图 15.10 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH15_F10_Liu.png
图 15.10 DALL-E 2 根据文本提示“一个穿着太空服的宇航员骑独角兽”生成的图像
运行列表 15.10 并查看 DALLE-2 为你生成的图像。请注意,你的结果将不同,因为 DALLE-2(以及所有 LLM)的输出是随机的,而不是确定的。
练习 15.2
申请 OpenAI API 密钥。然后修改代码列表 15.10 以使用文本提示“穿着西装的猫在电脑上工作”生成图像。
在本章中,你学习了基于扩散的模型的工作原理及其在文本到图像 Transformer 中的重要性,例如 OpenAI 的 CLIP 模型。你还发现了如何获取你的 OpenAI API 密钥,并使用一个简短的 Python 脚本来生成由 DALL-E 2 创建的图像,该模型集成了 CLIP。
在下一章中,你将继续使用之前获得的 OpenAI API 密钥,使用预训练的 LLM 生成各种内容,包括文本、音频和图像。此外,你将集成 LangChain Python 库与其他 API,使你能够创建一个无所不知的个人助理。
摘要
-
在正向扩散中,我们逐渐向干净图像添加少量随机噪声,直到它们转变为纯噪声。相反,在反向扩散中,我们从随机噪声开始,并使用降噪模型逐步从图像中消除噪声,将噪声转换回干净图像。
-
U-Net 架构最初是为生物医学图像分割设计的,它具有对称的形状,包括一个收缩的编码器路径和一个扩张的解码器路径,两者通过一个瓶颈层相连。在降噪过程中,U-Nets 被调整为去除噪声同时保留细节。跳跃连接将相同空间维度的编码器和解码器特征图连接起来,有助于保留在编码过程中可能丢失的边缘和纹理等空间信息。
-
将注意力机制引入降噪 U-Net 模型,使其能够专注于重要特征并忽略不相关的特征。通过将图像像素视为一个序列,注意力机制学习像素依赖关系,类似于它在自然语言处理中学习标记依赖关系的方式。这增强了模型有效识别相关特征的能力。
-
类似于 OpenAI 的 DALL-E 2、Google 的 Imagen 和 Stability AI 的 Stable Diffusion 这样的文本到图像 Transformer 使用扩散模型从文本描述中创建图像。它们将文本编码为条件扩散模型的潜在表示,然后迭代地去除由编码文本引导的随机噪声向量,以生成符合文本描述的逼真图像。
^(1) Jascha Sohl-Dickstein, Eric A. Weiss, Niru Maheswaranathan, and Surya Ganguli, 2015, “Deep Unsupervised Learning Using Nonequilibrium Thermodynamics.” International Conference on Machine Learning, arxiv.org/abs/1503.03585.
^(2) Sohl-Dickstein et al., 2015, “Deep Unsupervised Learning Using Nonequilibrium Thermodynamics,” https://arxiv.org/abs/1503.03585. 杨松和斯蒂法诺·埃尔蒙,2019, “通过估计数据分布的梯度进行生成建模。” arxiv.org/abs/1907.05600. 乔纳森·霍,阿贾伊·贾因和皮埃特·阿贝尔,2020, “去噪扩散概率模型,” arxiv.org/abs/2006.11239.
^(3) 伊利亚·洛希奇洛夫和弗兰克·胡特,2017, “解耦权重衰减正则化。” arxiv.org/abs/1711.05101.
^(4) 阿迪亚·拉梅斯,普拉夫拉·达里瓦尔,亚历克斯·尼科尔,凯西·丘和马克·陈,2022, “使用 CLIP 潜力的分层文本条件图像生成。” arxiv.org/abs/2204.06125.
第十六章:预训练大型
语言模型和 LangChain 库
本章涵盖
-
使用预训练大型语言模型进行文本、图像、语音和代码生成
-
少样本、单样本和零样本提示技术
-
使用 LangChain 创建零样本个人助理
-
生成式 AI 的限制和伦理问题
预训练大型语言模型(LLMs)的兴起已经改变了自然语言处理(NLP)和生成任务的领域。OpenAI 的 GPT 系列是一个显著的例子,展示了这些模型在生成逼真文本、图像、语音甚至代码方面的广泛能力。有效利用这些预训练 LLMs 对于几个原因至关重要。它使我们能够在不开发这些模型的大量资源的情况下部署高级 AI 功能。此外,理解这些 LLMs 为利用 NLP 和生成 AI 的创新应用铺平了道路,促进了各个行业的进步。
在一个日益受到 AI 影响的世界上,掌握预训练大型语言模型(LLMs)的集成和定制提供了关键竞争优势。随着 AI 的发展,利用这些复杂的模型对于在数字领域的创新和成功变得至关重要。
通常,这些模型通过基于浏览器的界面操作,这些界面在不同的 LLMs 之间有所不同,这些 LLMs 独立于彼此运行。每个模型都有其独特的优势和专长。通过浏览器进行接口限制了我们对每个特定 LLM 潜力的充分利用。利用 Python 等编程语言,尤其是通过 LangChain 库等工具,提供了以下原因的实质性好处。
Python 在交互 LLMs 中的作用增强了工作流程和过程的自动化。能够自主运行的 Python 脚本,无需手动输入即可实现不间断的运行。这对那些经常处理大量数据的业务特别有益。例如,一个 Python 脚本可以通过查询 LLM、综合数据洞察并将这些发现通过电子邮件或数据库传播来自主生成月度报告。Python 在管理 LLMs 交互方面提供了比基于浏览器的界面更高的定制和控制水平,使我们能够编写定制代码以满足特定的操作需求,如实现条件逻辑、在循环中处理多个请求或管理异常。这种适应性对于定制输出以满足特定的商业目标或研究查询至关重要。
Python 丰富的库集合使其非常适合将 LLMs 与现有软件和系统集成。LangChain 库就是这样一个典型的例子,它通过 LLMs 扩展了 Python 的功能。LangChain 允许组合多个 LLMs 或将 LLM 功能与其他服务(如 Wikipedia API 或 Wolfram Alpha API)集成,这些内容将在本章后面进行介绍。这种“链式”不同服务的能力允许构建复杂的、多步骤的 AI 系统,其中任务被分割并由最适合的模型或服务处理,从而提高性能和准确性。
因此,在本章中,你将首先学习如何使用 OpenAI API 通过 Python 编程创建各种内容:文本、图像、语音和 Python 代码。你还将了解少量示例、单次示例和零示例内容生成的区别。少量示例提示意味着你给出多个示例以帮助模型理解任务,而单次或零示例提示则意味着提供一个示例或没有示例。
现代 LLMs,如 ChatGPT,是在几个月前基于现有知识进行训练的,因此它们无法提供如天气状况、航班状态或股价等最新或实时信息。你将学习如何使用 LangChain 库将 LLMs 与 Wolfram Alpha 和 Wikipedia API 结合使用,创建一个零示例的全知个人助理。
尽管 LLMs 具有令人印象深刻的性能,但它们并不具备对内容内容的内在理解。这可能导致逻辑错误、事实不准确,以及无法掌握复杂概念或细微差别。这些模型的快速发展和广泛应用也引发了各种伦理问题,如偏见、错误信息、隐私和版权。这些问题需要仔细考虑和采取主动措施,以确保 LLMs 的开发和部署符合伦理标准和社会主义核心价值观。
16.1 使用 OpenAI API 进行内容生成
虽然还有其他 LLMs,如 Meta 的 LLAMA 和 Google 的 Gemini,但 OpenAI 的 GPT 系列是最突出的。因此,在本章中,我们将使用 OpenAI GPTs 作为示例。
OpenAI 允许你使用 LLMs 生成各种内容,如文本、图像、音频和代码。你可以通过网页浏览器或 API 访问他们的服务。由于之前提到使用 Python 与 LLMs 交互的优势,本章将重点介绍通过 API 使用 Python 程序进行内容生成。
你确实需要本章程序中的 OpenAI API 密钥才能运行。我假设你已经在第十五章中获得了你的 API 密钥。如果没有,请回到第十五章,获取获取 API 密钥的详细说明。
在本节中,我将主要关注文本生成,但将为代码、图像和语音生成的每个案例提供一个示例。
本章涉及使用几个新的 Python 库。要在您的计算机上的 Jupyter Notebook 应用的新单元格中安装它们,请运行以下代码行:
!pip install --upgrade openai langchain_openai langchain
!pip install wolframalpha langchainhub
!pip install --upgrade --quiet wikipedia
按照屏幕上的说明完成安装。
16.1.1 使用 OpenAI API 进行文本生成任务
您可以生成用于多种目的的文本,例如问答、文本摘要和创意写作。
当您向 OpenAI GPT 提问时,请记住,所有 LLM,包括 OpenAI GPT,都是通过自动化网络爬取收集的历史数据训练的。截至本文撰写时,GPT-4 使用截至 2023 年 12 月的数据进行训练,存在三个月的滞后。GPT-3.5 使用截至 2021 年 9 月的数据进行训练。
让我们先向 GPT 提问有关历史事实的问题。在新的单元格中输入以下代码行。
列表 16.1 使用 OpenAI API 检查历史事实
from openai import OpenAI
openai_api_key=put your actual OpenAI API key here, in quotes ①
client=OpenAI(api_key=openai_api_key) ②
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": ③
'''You are a helpful assistant, knowledgeable about recent facts.'''},
{"role": "user", "content":
'''Who won the Nobel Prize in Economics in 2000?'''} ④
]
)
print(completion.choices[0].message.content)
① 提供您的 OpenAI API 密钥
② 创建一个名为 client 的 OpenAI()类实例
③ 定义系统的角色
④ 提出问题
确保在列表 16.1 中提供您的 OpenAI API 密钥。我们首先实例化OpenAI()类,并将其命名为client。在chat.completions.create()方法中,我们指定模型为gpt-3.5-turbo。网站platform.openai.com/docs/models提供了各种可用的模型。您可以使用 gpt-4 或 gpt-3.5-turbo 进行文本生成。前者提供更好的结果,但费用也更高。由于我们的示例足够简单,我们将使用后者,因为它提供同样好的结果。
前一个代码块中的messages参数由几个消息对象组成,每个对象包含一个角色(可以是“system”、“user”或“assistant”)和内容。系统消息决定了助手的行为了;如果没有系统消息,默认设置将助手描述为“一个有用的助手”。用户消息包括助手需要处理的询问或评论。例如,在先前的例子中,用户消息是“2000 年谁获得了诺贝尔经济学奖?”输出如下
The Nobel Prize in Economics in 2000 was awarded to James J. Heckman and
Daniel L. McFadden for their work on microeconometrics and microeconomic theory.
OpenAI 提供了正确答案。
您还可以要求 LLM 就某个特定主题撰写文章。接下来,我们要求它撰写一篇关于自我激励重要性的短文:
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
n=1,
messages=[
{"role": "system", "content":
'''You are a helpful assistant, capable of writing essays.'''},
{"role": "user", "content":
'''Write a short essay on the importance of self-motivation.'''}
]
)
print(completion.choices[0].message.content)
这里的n=1参数告诉助手生成一个响应。如果您想要多个响应,可以将 n 设置为不同的数字。n 的默认值是 1。输出如下
Self-motivation is a key factor in achieving success and personal growth in
various aspects of life. It serves as the driving force behind our
actions, decisions, and goals, pushing us to overcome obstacles and
challenges along the way.
One of the primary benefits of self-motivation is that it helps individuals
take initiative and control of their lives…
输出共有六段,我只包括了前几句话。您可以访问书籍的 GitHub 仓库(github.com/markhliu/DGAI)查看全文。如您所见,文章结构清晰,观点明确,且没有语法错误。
您甚至可以要求 OpenAI 的 GPT 为您写一个笑话:
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content":
'''You are a helpful assistant, capable of telling jokes.'''},
{"role": "user", "content":
'''Tell me a math joke.'''}
]
)
print(completion.choices[0].message.content)
我们要求它讲一个数学笑话,结果是
Why was the equal sign so humble? Because he knew he wasn't less than or greater than anyone else!
您可以与助手进行双向对话。messages 参数自动包括对话历史。例如,在运行前面的代码块之后,如果您运行以下代码:
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content":
'''Haha, that's funny! Tell me another one.'''}
]
)
print(completion.choices[0].message.content)
您将得到一个类似的响应
Sure! Why was the math book sad? Because it had too many problems.
用户的查询“哈哈,这很有趣!再给我讲一个。”只有在您要求助手讲一个数学笑话的前言消息的上下文中才有意义。其他文本生成能力包括文本摘要和分类,您将在本章后面看到这样的例子。
16.1.2 使用 OpenAI API 生成代码
Codex 专门设计用于理解和生成代码。它可以与多种编程语言一起工作,并将自然语言描述转换为代码。Codex 现在是 OpenAI GPTs 的集成部分。
接下来,我们要求 OpenAI GPT 生成一个 Python 程序来绘制正弦曲线:
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content":
'''You are a helpful assistant,
capable of generating Python programs.'''},
{"role": "user", "content":
'''Write a Python program to plot a sine graph.'''}
]
)
print(completion.choices[0].message.content)
注意,ChatGPT 使用 GPT-3.5-Turbo 有效地管理对话方面和代码生成任务。它不只为代码生成提供单独的 Codex 模型。输出如下
Sure! Here's a simple Python program using the matplotlib library to plot
a sine graph:
```python
import matplotlib.pyplot as plt
import numpy as np
# 从 0 到 2*pi 生成 x 值
x = np.linspace(0, 2*np.pi, 100)
# 使用正弦函数计算 y 值
y = np.sin(x)
# 绘制正弦图
plt.figure()
plt.plot(x, y)
plt.title('正弦图')
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.grid(True)
plt.show()
```py
You can run this program in a Python environment with the matplotlib
library installed to visualize the sine graph.
如果您将生成的 Python 程序复制并粘贴到 Jupyter Notebook 中的一个单元中并运行它,您将看到一个类似于图 16.1 的图像。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F01_Liu.png
图 16.1 使用 OpenAI GPT-3.5 生成 Python 代码绘制正弦曲线。我们使用文本描述“编写一个 Python 程序绘制正弦图”来请求它生成一个 Python 程序。然后我们运行程序来创建图表。
LLM 不仅提供了 Python 代码,还让您知道您需要在安装了 matplotlib 库的 Python 环境中运行代码。
16.1.3 使用 OpenAI DALL-E 2 生成图像
DALL-E 2 是由 OpenAI 开发的一个 AI 模型,旨在根据文本描述生成图像。它是原始 DALL-E 模型的继承者,代表了在视觉内容生成 AI 领域的进步。
DALL-E 2 使用了一种类似于我们在第十五章讨论的扩散模型,它从随机的像素模式开始,并逐渐将其细化成一个与输入文本相匹配的连贯图像。它通过产生更高质量、更准确和更详细的文本描述图像来改进了原始的 DALL-E。
将 DALL-E 2 集成到 OpenAI 的 GPT 系列中,使我们不仅能够生成文本,还能根据文本提示创建图像。接下来,我们要求 DALL-E 2 创建一个河岸上钓鱼的人的图像:
response = client.images.generate(
model="dall-e-2",
prompt="someone fishing at the river bank",
size="512x512",
quality="standard",
n=1,
)
image_url = response.data[0].url
print(image_url)
代码块生成一个 URL。如果您点击该 URL,您将看到一个类似于图 16.2 的图像。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F02_Liu.png
图 16.2 使用文本提示“有人在河岸钓鱼”生成的 DALL-E 2 图像
URL 在一小时后过期,所以请确保你及时访问它。此外,即使使用相同的文本提示,DALL-E 2 生成的图像也可能略有不同,因为输出是随机生成的。
16.1.4 使用 OpenAI API 进行语音生成
文本到语音(TTS)是一种将书面文本转换为语音的技术。TTS 通过多模态 Transformer 进行训练,其中输入是文本,输出是音频格式。在 ChatGPT 的背景下,集成 TTS 功能意味着 LLM 不仅能够生成文本响应,还可以大声说出这些响应。接下来,我们请求 OpenAI API 将一段简短文本转换为语音:
response = client.audio.speech.create(
model="tts-1-hd",
voice="shimmer",
input='''This is an audio file generated by
OpenAI's text to speech AI model.'''
)
response.stream_to_file("files/speech.mp3")
运行前面的代码单元后,会在你的电脑上保存一个名为 speech.mp3 的文件,你可以听一下。文档网站(platform.openai.com/docs/guides/text-to-speech)提供了语音选项。这里我们选择了 shimmer 选项。其他选项包括 alloy、echo 等等。
16.2 LangChain 简介
LangChain 是一个 Python 库,旨在简化 LLM 在各种应用中的使用。它提供了一套工具和抽象,使得构建、部署和管理由 LLM(如 GPT-3、GPT-4 以及其他类似模型)驱动的应用程序变得更加容易。
LangChain 抽象掉了与不同 LLM 和应用交互的复杂性,使得开发者可以专注于构建他们的应用程序逻辑,而无需担心底层模型的具体细节。它特别适合通过将 LLM 与如 Wolfram Alpha 和 Wikipedia 这样的应用链接起来,构建一个“全知全能”的代理,这些应用可以提供实时信息或最近的事实。LangChain 的模块化架构允许轻松集成不同的组件,使代理能够利用各种 LLM 和应用的优势。
16.2.1 LangChain 库的需求
想象一下,你的目标是构建一个零样本的全知全能代理,使其能够生成各种内容,检索实时信息,并为我们回答事实性问题。你希望代理能够根据当前任务自动前往正确的来源检索相关信息,而无需明确告诉它要做什么。LangChain 库正是这个任务的正确工具。
在这个项目中,你将学习如何使用 LangChain 库将 LLM 与 Wolfram Alpha 和 Wikipedia API 结合起来,创建一个零样本的全知全能代理。我们使用 Wolfram Alpha API 检索实时信息,使用 Wikipedia API 回答关于最近事实的问题。LangChain 允许我们创建一个代理,利用多个工具来回答问题。代理首先理解查询,然后决定在工具箱中使用哪个工具来回答问题。
为了展示即使是功能最先进的 LLMs 也缺乏这些能力,让我们来问一下 2024 年奥斯卡最佳男主角奖的获得者是谁:
completion = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content":
'''You are a helpful assistant, knowledgeable about recent facts.'''},
{"role": "user", "content":
'''Who won the Best Actor Award in 2024 Academy Awards?'''}
]
)
print(completion.choices[0].message.content)
输出是
I'm sorry, but I cannot provide real-time information or make predictions
about future events such as the 2024 Academy Awards. For the most accurate
and up-to-date information, I recommend checking reliable sources or news
outlets closer to the date of the awards show.
我在 2024 年 3 月 17 日提出了这个查询,GPT-4 无法回答这个问题。有可能当你提出相同的查询时,你会得到正确的答案,因为模型已经使用更近期的数据进行更新。如果那样的话,将问题改为几天前的事件,你应该会得到类似的回答。
因此,我们将使用 LangChain 将 LLM 与 Wolfram Alpha 和 Wikipedia API 链在一起。Wolfram Alpha 擅长科学计算和检索实时信息,而 Wikipedia 则以提供历史和近期事件及事实的信息而闻名。
16.2.2 在 LangChain 中使用 OpenAI API
本章早期安装的 langchain-openai 库允许你以最少的提示工程使用 OpenAI GPTs。你只需要用普通的英语解释你希望 LLM 做什么。
这里是一个例子,展示我们如何要求它纠正文本中的语法错误:
from langchain_openai import OpenAI
llm = OpenAI(openai_api_key=openai_api_key)
prompt = """
Correct the grammar errors in the text:
i had went to stor buy phone. No good. returned get new phone.
"""
res=llm.invoke(prompt)
print(res)
输出是
I went to the store to buy a phone, but it was no good. I returned it and
got a new phone.
注意,我们没有使用任何提示工程。我们也没有指定使用哪个模型。LangChain 根据任务要求和其他因素(如成本、延迟和性能)找到了最适合的模型。它还自动格式化和结构化查询,使其适合使用的模型。前面的提示只是简单地要求代理用普通的英语纠正文本中的语法错误。它返回了语法正确的文本,如前一个输出所示。
这里是另一个例子。我们要求代理说出肯塔基州的首府:
prompt = """
What is the capital city of the state of Kentucky?
"""
res=llm.invoke(prompt)
print(res)
输出是
The capital city of Kentucky is Frankfort.
它告诉我们正确的答案,即肯塔基州的法兰克福。
16.2.3 零样本、单样本和少样本提示
少样本、单样本和零样本提示指的是向 LLMs 提供示例或指令的不同方式,以指导它们的响应。这些技术用于帮助模型理解当前的任务,并生成更准确或相关的输出。
在零样本提示中,模型被赋予一个任务或问题,但没有提供任何示例。提示通常包括对预期的明确描述,但模型必须仅基于其先验知识和理解来生成响应。在单样本提示中,模型提供了一个示例来展示任务。在少样本提示中,模型被提供了多个示例以帮助它理解任务。少样本提示基于这样的理念:提供更多示例可以帮助模型更好地掌握任务的模式或规则,从而产生更准确的响应。
到目前为止,你与 OpenAI GPTs 的所有交互都是零样本提示,因为你没有向它们提供任何示例。
让我们尝试一个少样本提示的例子。假设你想让 LLM 进行情感分析:你希望它将句子归类为正面或负面。你可以在提示中提供几个示例:
prompt = """
The movie is awesome! // Positive
It is so bad! // Negative
Wow, the movie was incredible! // Positive
How horrible the movie is! //
"""
res=llm.invoke(prompt)
print(res)
输出是
Negative
在提示中,我们提供了三个示例。其中两个评论被归类为正面,而另一个被归类为负面。然后我们提供了句子,“这部电影多么糟糕!”LLM 正确将其归类为负面。
在上一个例子中,我们使用 // 来分隔句子和相应的情感。你可以使用其他分隔符,如 ->,只要保持一致即可。
下面是一个单样本提示的例子:
prompt = """
Car -> Driver
Plane ->
"""
res=llm.invoke(prompt)
print(res)
输出是
Pilot
通过提供一个单一示例,我们实际上是在询问 LLM,“飞机对驾驶员来说就像汽车对驾驶员一样?”LLM 正确回答了“飞行员”。
练习 16.1
假设你想问 LLM,“花园对厨师来说就像厨房对厨师一样?”使用单样本提示来获取答案。
最后,这里是一个零样本提示的例子:
prompt = """
Is the tone in the sentence "Today is a great day for me" positive,
negative, or neutral?
"""
res=llm.invoke(prompt)
print(res)
输出是
Positive
我们在提示中没有提供任何示例。然而,我们用简单的英语指令要求 LLM 将句子的语气归类为正面、负面或中性。
16.3 LangChain 中的零样本全知全能代理
在本节中,你将学习如何在 LangChain 中创建一个零样本的全知全能代理。你将使用 OpenAI GPTs 生成各种内容,如文本、图像和代码。为了弥补 LLM 无法提供实时信息的能力,你将学习如何将 Wolfram Alpha 和 Wikipedia API 添加到工具箱中。
Wolfram Alpha 是一个计算知识引擎,旨在在线处理事实查询,特别擅长处理数值和计算任务,尤其是在科学和技术领域。通过集成 Wolfram Alpha API,代理获得了回答各种主题的几乎所有问题的能力。如果 Wolfram Alpha 无法提供响应,我们将使用 Wikipedia 作为特定主题基于事实问题的次要来源。
图 16.3 是我们将在本节中创建零样本全知全能代理所采取步骤的示意图。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F03_Liu.png
图 16.3 使用 LangChain 库创建零样本全知全能代理的步骤
具体来说,我们首先将在 LangChain 中创建一个仅使用一个工具——Wolfram Alpha API 的代理来回答与实时信息和最新事实相关的问题。然后,我们将 Wikipedia API 添加到工具箱中,作为关于最新事实问题的备用。我们将添加各种工具,利用 OpenAI API,如文本摘要器、笑话讲述者和情感分类器。最后,我们将添加图像和代码生成功能。
16.3.1 申请 Wolfram Alpha API 密钥
Wolfram Alpha 每月免费提供高达 2,000 次非商业 API 调用。要获取 API 密钥,请首先访问 account.wolfram.com/login/create/ 并完成创建账户的步骤。
Wolfram 账户本身只提供浏览器访问;您需要在 products.wolframalpha.com/api/ 申请 API 密钥。一旦到达那里,点击左下角的“获取 API 访问”。应该会弹出一个小的对话框,填写名称和描述字段,从下拉菜单中选择“简单 API”,然后点击提交,如图 16.4 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F04_Liu.png
图 16.4 申请 Wolfram Alpha AppID
之后,你的 AppID 应该会出现在一个新窗口中。复制 API 密钥并将其保存到文件中以便以后使用。
这是您如何使用 Wolfram Alpha API 进行数学运算的方法:
import os
os.environ['WOLFRAM_ALPHA_APPID'] = "your Wolfram Alpha AppID"
from langchain_community.utilities.wolfram_alpha import \
WolframAlphaAPIWrapper
wolfram = WolframAlphaAPIWrapper()
res=wolfram.run("how much is 23*55+123?")
print(res)
输出是
Assumption: 23×55 + 123
Answer: 1388
Wolfram Alpha API 提供正确答案。
我们还将包括维基百科 API,以提供对各种主题的答案。如果你已经在你的计算机上安装了维基百科库,你不需要申请 API 密钥。以下是 LangChain 库中使用维基百科 API 的示例:
from langchain.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
res=wikipedia.run("University of Kentucky")
print(res)
输出是
Page: University of Kentucky
Summary: The University of Kentucky (UK, UKY, or U of K) is a public
land-grant research university in Lexington, Kentucky. Founded in 1865 by
John Bryan Bowman as the Agricultural and Mechanical College of Kentucky,
the university is one of the state's two land-grant universities (the
other being Kentucky State University)…
为了简洁,我们省略了大部分输出内容。
16.3.2 在 LangChain 中创建代理
接下来,我们将在 LangChain 中创建一个代理,工具箱中只有 Wolfram Alpha API。在这个上下文中,代理是指通过自然语言交互处理特定任务或过程的单个实体。然后我们将逐步添加更多工具,使代理能够处理更多任务。
列表 16.2 在 LangChain 中创建代理
os.environ['OPENAI_API_KEY'] = openai_api_key
from langchain.agents import load_tools
from langchain_openai import ChatOpenAI
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import OpenAI
prompt = hub.pull("hwchase17/react")
llm = ChatOpenAI(model_name='gpt-3.5-turbo') ①
tool_names = ["wolfram-alpha"]
tools = load_tools(tool_names,llm=llm) ②
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True) ③
res=agent_executor.invoke({"input": """
What is the temperature in Lexington, Kentucky now?
"""}) ④
print(res["output"])
① 定义要使用的 LLM
② 将 Wolfram Alpha 添加到工具箱
③ 定义代理
④ 向代理提问
LangChain 中的 hwchase17/react 指的是一种特定的 ReAct 代理配置。ReAct 代表反应性动作,它是 LangChain 内的一个框架,旨在优化语言模型能力与其他工具结合以有效解决复杂任务。有关更多详细信息,请参阅 python.langchain.com/docs/how_to/migrate_agent/。在 LangChain 中创建代理时,您需要指定代理要使用的工具。在先前的示例中,我们只使用了一个工具,即 Wolfram Alpha API。
例如,我们询问肯塔基州列克星敦的当前温度,以下是输出结果:
> Entering new AgentExecutor chain...
I should use Wolfram Alpha to find the current temperature in Lexington,
Kentucky.
Action: wolfram_alpha
Action Input: temperature in Lexington, KentuckyAssumption: temperature |
Lexington, Kentucky
Answer: 44 °F (wind chill: 41 °F)
(27 minutes ago)I now know the current temperature in Lexington, Kentucky.
Final Answer: The temperature in Lexington, Kentucky is 44 °F with a wind
chill of 41 °F.
> Finished chain.
The temperature in Lexington, Kentucky is 44 °F with a wind chill of 41 °F.
输出不仅显示了最终答案,即肯塔基州列克星敦的当前温度为 44 华氏度,而且还显示了思维链。它使用 Wolfram Alpha 作为获取答案的来源。
我们还可以将维基百科添加到工具箱中:
tool_names += ["wikipedia"]
tools = load_tools(tool_names,llm=llm)
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True)
res=agent_executor.invoke({"input": """
Who won the Best Actor Award in 2024 Academy Awards?
"""})
print(res["output"])
我询问谁赢得了 2024 年奥斯卡最佳男主角奖,代理使用维基百科获取正确答案:
I need to find information about the winner of the Best Actor Award at the
2024 Academy Awards.
Action: wikipedia
Action Input: 2024 Academy Awards Best Actor
…
Cillian Murphy won the Best Actor Award at the 2024 Academy Awards for his
performance in Oppenheimer.
在前面的输出中,代理首先决定使用维基百科作为解决问题的工具。在搜索了各种维基百科来源后,代理提供了正确答案。
接下来,你将学习如何将各种 OpenAI GPT 工具添加到代理的工具箱中。
16.3.3 使用 OpenAI GPTs 添加工具
我们首先添加一个文本摘要器,以便代理可以总结文本。
列表 16.3 向代理的工具箱添加文本摘要器
from langchain.agents import Tool
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
temp = PromptTemplate(input_variables=["text"], ①
template="Write a one sentence summary of the following text: {text}")
summarizer = LLMChain(llm=llm, prompt=temp) ②
sum_tool = Tool.from_function(
func=summarizer.run,
name="Text Summarizer",
description="A tool for summarizing texts") ③
tools+=[sum_tool]
agent = create_react_agent(llm, tools, prompt) ④
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True)
res=agent_executor.invoke({"input":
'''Write a one sentence summary of the following text:
The University of Kentucky's Master of Science
in Finance (MSF) degree prepares students for
a professional career in the finance and banking
industries. The program is designed to provide
rigorous and focused training in finance,
broaden opportunities in your career, and
sharpened skills for the fast-changing
and competitive world of modern finance.'''})
print(res["output"])
① 定义一个模板
② 定义一个摘要器函数
③ 将摘要器作为工具添加
④ 使用更新后的工具箱重新定义代理
我们首先提供一个模板来总结文本。然后定义一个摘要器函数并将其添加到工具箱中。最后,我们通过使用更新后的工具箱重新定义代理,并要求它用一句话总结示例文本。确保你的提示与模板中描述的格式相同,这样代理就知道使用哪个工具。
列表 16.3 的输出是
> Entering new AgentExecutor chain...
I need to summarize the text provided.
Action: Summarizer
…
> Finished chain.
The University of Kentucky's MSF program offers specialized training in
finance to prepare students for successful careers in the finance and
banking industries.
由于输入与摘要器函数中描述的模板相匹配,代理选择摘要器作为任务的工具。我们使用两个长句作为文本输入,前面的输出是一个句子摘要。
你可以添加你喜欢的任何工具。例如,你可以添加一个工具来讲述某个主题的笑话:
temp = PromptTemplate(input_variables=["text"],
template="Tell a joke on the following subject: {subject}")
joke_teller = LLMChain(llm=llm, prompt=temp)
tools+=[Tool.from_function(name='Joke Teller',
func=joke_teller.run,
description='A tool for telling jokes')]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True)
res=agent_executor.invoke({"input":
'''Tell a joke on the following subject: coding'''})
print(res["output"])
输出是
> Entering new AgentExecutor chain...
I should use the Joke Teller tool to find a coding-related joke.
Action: Joke Teller
Action Input: coding
Observation: Why was the JavaScript developer sad?
Because he didn't know how to "null" his feelings.
Thought:That joke was funny!
Final Answer: Why was the JavaScript developer sad? Because he didn't know
how to "null" his feelings.
> Finished chain.
Why was the JavaScript developer sad? Because he didn't know how to "null"
his feelings.
我们要求代理就编程主题讲一个笑话。代理识别出笑话讲述者是工具。这个笑话确实与编程相关。
练习 16.2
向代理的工具箱添加一个进行情感分析的工具。命名为情感分类器。然后要求代理将文本“这部电影一般般”分类为正面、负面或中性。
16.3.4 添加生成代码和图像的工具
你可以将各种工具添加到 LangChain 的工具箱中。感兴趣的读者可以在python.langchain.com/docs/how_to/#tools找到更多详细信息。接下来,我们添加生成其他内容形式(如代码和图像)的工具。
要添加一个生成代码的工具,你可以这样做:
temp = PromptTemplate(input_variables=["text"],
template='''Write a Python program based on the
description in the following text: {text}''')
code_generator = LLMChain(llm=llm, prompt=temp)
tools+=[Tool.from_function(name='Code Generator',
func=code_generator.run,
description='A tool to generate code')]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True)
res=agent_executor.invoke({"input":
'''Write a Python program based on the
description in the following text:
write a python program to plot a sine curve and a cosine curve
in the same graph. The sine curve is in solid line and the cosine
curve is in dashed line. Add a legend to the graph. Set the x-axis
range to -5 to 5\. The title should be "Comparing sine and cosine curves."
'''})
print(res["output"])
输出是
> Entering new AgentExecutor chain...
I should use the Code Generator tool to generate the Python program based on the given description.
Action: Code Generator
Action Input: Write a Python program to plot a sine curve and a cosine
curve in the same graph. The sine curve is in solid line and the cosine
curve is in dashed line. Add a legend to the graph. Set the x-axis range
to -5 to 5\. The title should be "Comparing sine and cosine curves."
Observation: import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-5, 5, 100)
y1 = np.sin(x)
y2 = np.cos(x)
plt.plot(x, y1, label='Sine Curve', linestyle='solid')
plt.plot(x, y2, label='Cosine Curve', linestyle='dashed')
plt.legend()
plt.title('Comparing Sine and Cosine Curves')
plt.xlim(-5, 5)
plt.show()
Thought:The Python program has been successfully generated to plot the sine
and cosine curves. I now know the final answer.
Final Answer: The Python program to plot a sine curve and a cosine curve in
the same graph with the specified requirements has been generated.
> Finished chain.
The Python program to plot a sine curve and a cosine curve in the same
graph with the specified requirements has been generated.
如果你在一个单元格中运行生成的代码,你将看到如图 16.5 所示的图像。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F05_Liu.png
图 16.5 在 LangChain 中添加一个生成 Python 代码的工具。该工具随后生成代码,在同一图表中绘制正弦和余弦曲线,并带有图例和线条样式。
要添加一个图像生成器,你可以这样做:
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper
temp = PromptTemplate(input_variables=["text"],
template="Create an image base on the following text: {text}")
grapher = LLMChain(llm=llm, prompt=temp)
tools+=[Tool.from_function(name='Text to image',
func=grapher.run,
description='A tool for text to image')]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools,
handle_parsing_errors=True,verbose=True)
image_url = DallEAPIWrapper().run(agent_executor.invoke({"input":
'''Create an image base on the following text:
a horse grazes on the grassland.'''})["output"])
print(image_url)
输出是一个 URL,你可以通过它来可视化并下载图像。我们要求代理创建一幅马在草原上吃草的图像。该图像如图 16.6 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F06_Liu.png
图 16.6 LangChain 中一个全知全能代理生成的图像
通过这样,你已经学会了如何在 LangChain 中创建一个零样本的全知全能代理。你可以根据你想让代理完成的事情添加更多工具。
16.4 LLMs 的限制和伦理问题
LLMs(大型语言模型)如 OpenAI 的 GPT 系列在自然语言处理和生成式 AI 领域取得了显著的进步。尽管这些模型拥有令人印象深刻的性能,但它们并非没有局限性。理解这些限制对于发挥它们的优点和减轻它们的弱点至关重要。
同时,这些模型的快速进步和广泛应用也引发了一系列伦理问题,如偏见、不准确、侵犯隐私和版权侵权。这些问题需要仔细考虑并采取主动措施,以确保 LLMs 的开发和部署符合伦理标准和社会主义核心价值观。
在本节中,我们将探讨 LLMs 的限制,分析为什么这些问题持续存在,并举例说明一些显著的失败案例,以强调解决这些挑战的重要性。我们还将检查与 LLMs 相关的关键伦理问题,并提出缓解这些问题的途径。
16.4.1 LLMs 的限制
LLMs 的一个基本局限性是它们缺乏真正的理解和推理能力。虽然它们可以生成连贯且与上下文相关的回答,但它们并不具备对内容的内在理解。这可能导致逻辑错误、事实不准确,以及无法掌握复杂概念或细微差别。
这在 LLMs 犯下的许多史诗级错误中有所体现。《Smart Until It’s Dumb》这本书提供了许多 GPT-3 和 ChatGPT 犯下的此类错误的有趣实例。1 例如,考虑以下问题:March 夫人给了母亲茶和粥,同时她温柔地给小宝宝穿衣,就像它是她自己的孩子一样。谁是这个婴儿的母亲?GPT-3 给出的答案是 March 夫人。
公平地说,随着 LLMs 的快速进步,许多这些错误随着时间的推移得到了纠正。然而,LLMs 仍然会犯低级错误。2023 年 6 月 David Johnston 在 LinkedIn 上的一篇文章(www.linkedin.com/pulse/intelligence-tests-llms-fail-why-david-johnston/)测试了 LLMs 在人类容易解决的十二个问题上的智力。包括 GPT-4 在内的 LLMs 在这些问题上都遇到了困难。其中一个问题是:说出一个动物的名字,其单词长度等于它们腿的数量减去它们尾巴的数量。
-david-johnston/](https://www.linkedin.com/pulse/intelligence-tests-llms-fail-why-david-johnston/))测试了 LLMs 在人类容易解决的十二个问题上的智力。包括 GPT-4 在内的 LLMs 在这些问题上都遇到了困难。其中一个问题是:说出一个动物的名字,其单词长度等于它们腿的数量减去它们尾巴的数量。
到目前为止,这个错误还没有被纠正。图 16.7 是我使用浏览器界面时 GPT-4 给出的答案的截图。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH16_F07_Liu.png
图 16.7 GPT-4 仍然犯的低级错误
图 16.7 的输出显示,根据 GPT-4,数字五等于单词“bee”中的字母数量。
16.4.2 LLMs 的伦理问题
最紧迫的伦理问题之一是 LLMs 可能持续和放大其训练数据中的偏见。由于这些模型从通常来源于人类生成内容的大量数据集中学习,它们可以继承与性别、种族、民族和其他社会因素相关的偏见。这可能导致带有偏见的输出,从而强化刻板印象和歧视。
为了减轻偏见,采用多元包容的训练数据集、实施偏见检测和纠正算法,以及在模型开发和评估中确保透明度是至关重要的。尤其重要的是建立行业范围内的合作,以制定偏见减轻实践的标准,并促进负责任的 AI 发展。
然而,我们必须记住不要过度纠正。一个反例是,谷歌的 Gemini 通过在纳粹时代德国士兵等群体中包含有色人种,过度纠正了图像生成中的刻板印象.^(2)
对于 LLM 的另一个担忧是它们潜在的虚假信息和操纵能力。LLM 能够生成逼真且具有说服力的文本,这可能被用于制造和传播虚假信息、宣传或操纵性内容,这对公共讨论、民主和信息信任构成重大风险。
解决这一担忧的方案在于开发强大的内容审查系统。制定负责任使用的指南,以及促进 AI 开发者、政策制定者和媒体组织之间的合作,是打击虚假信息的关键步骤。
第三个担忧与隐私相关。用于训练 LLM 的大量数据引发了隐私担忧,因为敏感信息可能会在模型输出中无意中被泄露。此外,LLM 被用于网络攻击或绕过安全措施的可能性也带来了重大的安全风险。
此外,用于训练大型语言模型(LLM)的数据大多是在未经授权的情况下收集的。支持者认为,用于训练 LLM 的数据使用方式具有变革性:模型不仅仅重复数据,而是用它来生成新的、原创的内容。这种变革可能符合“合理使用”原则,该原则允许在不获得许可的情况下有限地使用受版权保护的材料,如果使用增加了新的表达或意义。批评者认为,LLM 是在未经许可的情况下使用大量受版权保护的文本进行训练的,这超出了可能被认为是合理使用的范围。所使用的数据规模和训练过程中未经转换直接摄取受版权保护的材料可能被视为侵权。这场辩论仍在继续。现行的版权法并非针对生成式 AI 设计的,导致了对 LLM 等技术如何适用这些法律的模糊性。这可能是需要由立法和司法机构来解决,以提供明确的指导并确保所有方的利益得到公平代表。
围绕 LLMs 的伦理问题多方面且需要全面的方法。AI 研究人员、开发者和政策制定者之间的协作对于制定伦理准则和框架至关重要,这些准则和框架指导着这些强大模型的负责任开发和部署。随着我们继续利用 LLMs 的潜力,伦理考量必须始终是我们努力的重点,以确保人工智能的进步与社会的价值观和人类的福祉和谐一致。
摘要
-
少样本提示意味着你给出多个示例以帮助 LLMs 理解任务,而单样本或零样本提示则意味着提供一个或没有示例。
-
LangChain 是一个旨在简化在各种应用中使用大型语言模型(LLMs)的 Python 库。它抽象化了与不同 LLMs 和应用交互的复杂性。它允许代理根据当前任务自动选择工具箱中的正确工具,而无需明确告知它要做什么。
-
现代预训练的 LLMs,如 OpenAI 的 GPT 系列,可以创建各种格式的内蓉,如文本、图像、音频和代码。
-
尽管它们取得了令人印象深刻的成就,但 LLMs 缺乏对内容内容的真正理解或推理能力。这些局限性可能导致逻辑错误、事实不准确,以及无法掌握复杂概念或细微差别。此外,这些模型的快速进步和广泛应用引发了一系列伦理问题,如偏见、错误信息、隐私侵犯和版权侵权。这些问题需要仔细考虑和主动措施,以确保 LLMs 的开发和部署与伦理标准和社会价值观相一致。
^(1) Maggiori, Emmanuel,2023 年,Smart Until It’s Dumb: Why Artificial Intelligence Keeps Making Epic Mistakes (and Why the AI Bubble Will Burst),Applied Maths Ltd. 电子书版。
^(2) Adi Robertson,2024 年 2 月 21 日,“Google 因 Gemini 生成的种族多样性纳粹而道歉。”The Verge,mng.bz/2ga9。
1669



被折叠的 条评论
为什么被折叠?



