PyTorch 是一个用于深度学习模型研究和开发基于深度学习应用的绝佳工具。在前面的章节中,我们探讨了跨越各种领域和模型类型的模型架构。我们使用 PyTorch 从头开始构建这些架构,并使用了 PyTorch 模型库中的预训练模型。从本章开始,我们将切换主题,深入探讨生成模型。
在前面的章节中,大多数示例和练习都围绕着开发分类模型,这是一种监督学习任务。然而,深度学习模型在处理无监督学习任务时也表现得非常出色。深度生成模型就是一个例子。这些模型使用大量未标记的数据进行训练。一旦训练完成,模型可以生成类似的有意义的数据。它通过学习输入数据的底层结构和模式来实现这一点。
在本章中,我们将开发文本和音乐生成器。为了开发文本生成器,我们将利用在第 5 章《高级混合模型》中训练的基于 Transformer 的语言模型。我们将使用 PyTorch 扩展 Transformer 模型,使其能够作为文本生成器。此外,我们将演示如何使用 GPT-2 和 GPT-3 等先进的预训练 Transformer 模型,在 PyTorch 中通过几行代码设置文本生成器。最后,我们将从零开始构建一个基于 PyTorch 的音乐生成模型,该模型将在 MIDI 数据集上进行训练。
在本章结束时,您应该能够在 PyTorch 中创建自己的文本和音乐生成模型。您还将能够应用不同的采样或生成策略,从这些模型中生成数据。本章涵盖以下主题:
- 使用 PyTorch 构建基于 Transformer 的文本生成器
- 使用 GPT(生成预训练 Transformer)模型作为文本生成器
- 使用 PyTorch 通过 LSTM 生成 MIDI 音乐
使用 PyTorch 构建基于 Transformer 的文本生成器
在前一章中,我们使用 PyTorch 构建了一个基于 Transformer 的语言模型。由于语言模型能够建模给定词序列之后某个词出现的概率,因此我们已经完成了构建文本生成器的一大步。在本节中,我们将学习如何将该语言模型扩展为一个深度生成模型,该模型可以在给定初始文本提示(即一系列单词)的情况下生成任意但有意义的句子。
训练基于 Transformer 的语言模型
在前一章中,我们训练了一个语言模型,训练了 5 个周期。在本节中,我们将按照相同的步骤进行训练,但训练时间更长——达到 50 个周期。这里的目标是获得一个表现更好的语言模型,从而能够生成更真实的句子。需要注意的是,模型训练可能需要几个小时,因此建议在后台训练,例如在夜间。要了解如何训练语言模型的步骤,请访问 GitHub [1] 以获取完整代码。
经过 50 个周期的训练后,我们得到了以下输出:
epoch 1, 100/1000 batches, training loss 8.81, training perplexity 6724.85
epoch 1, 200/1000 batches, training loss 7.35, training perplexity 1555.26
epoch 1, 300/1000 batches, training loss 6.90, training perplexity 991.85
epoch 1, 400/1000 batches, training loss 6.67, training perplexity 792.05
epoch 1, 500/1000 batches, training loss 6.54, training perplexity 694.65
epoch 1, 600/1000 batches, training loss 6.39, training perplexity 597.00
...
epoch 50, 600/1000 batches, training loss 4.45, training perplexity 86.05
epoch 50, 700/1000 batches, training loss 4.46, training perplexity 86.37
epoch 50, 800/1000 batches, training loss 4.33, training perplexity 76.17
epoch 50, 900/1000 batches, training loss 4.39, training perplexity 80.44
epoch 50, 1000/1000 batches, training loss 4.45, training perplexity 85.74
epoch 50, validation loss 5.07, validation perplexity 159.50
现在我们已经成功地训练了 50 个周期的 Transformer 模型,可以开始实际操作,将该训练好的语言模型扩展为一个文本生成模型。
保存和加载语言模型
在这里,我们将在训练完成后保存表现最佳的模型检查点。然后可以单独加载此预训练模型:
一旦模型训练完成,理想的做法是将其保存在本地,以避免从头开始重新训练。可以按如下方式保存模型:
mdl_pth = './transformer.pth'
torch.save(best_model_so_far.state_dict(), mdl_pth)
现在我们可以加载保存的模型,以便将此语言模型扩展为一个文本生成模型:
# 加载最佳训练模型
transformer_cached = Transformer(
num_tokens, embedding_size, num_heads,
num_hidden_params, num_layers, dropout).to(device)
transformer_cached.load_state_dict(torch.load(mdl_pth))
在本节中,我们重新实例化了一个 Transformer 模型对象,然后将预训练的模型权重加载到这个新模型对象中。接下来,我们将使用此模型生成文本。
使用语言模型生成文本
现在模型已经保存并加载,我们可以扩展已训练的语言模型生成文本:
首先,我们需要定义想要生成的目标单词数,并提供一个初始词序列作为模型的提示:
ln = 5
sntc = 'They are _'
sntc_split = sntc.split()
mask_source = gen_sqr_nxt_mask(max_seq_len).to(device)
最后,我们可以在一个循环中逐个生成单词。在每次迭代中,我们将预测的单词附加到输入序列中。这个扩展后的序列将成为模型在下一次迭代中的输入,依此类推。添加随机种子以确保一致性。通过改变种子,可以生成不同的文本,如以下代码块所示:
torch.manual_seed(34)
with torch.no_grad():
for i in range(ln):
sntc = ' '.join(sntc_split)
txt_ds = Tensor(
vocabulary(sntc_split)).unsqueeze(0).to(torch.long)
num_b = txt_ds.size(0)
txt_ds = txt_ds.narrow(0, 0, num_b)
txt_ds = txt_ds.view(1, -1).t().contiguous().to(device)
ev_X, _ = return_batch(txt_ds, i+1)
sequence_length = ev_X.size(0)
if sequence_length != max_seq_len:
mask_source = mask_source[:sequence_length,
:sequence_length]
op = transformer_cached(ev_X, mask_source)
op_flat = op.view(-1, num_tokens)
res = vocabulary.get_itos()[op_flat.argmax(1)[0]]
sntc_split.insert(-1, res)
print(sntc[:-2])
这应该输出以下内容:
They are often used for the
正如我们所看到的,使用 PyTorch,我们可以训练一个语言模型(在本例中为基于 Transformer 的模型),然后通过几行额外代码将其用于生成文本。生成的文本似乎合乎逻辑。这样的文本生成器的效果取决于底层语言模型所训练的数据量以及语言模型的强大程度。在本节中,我们基本上从零开始构建了一个文本生成器。
在下一节中,我们将加载预训练的语言模型并将其用作文本生成器。我们将使用 Transformer 模型的高级继承者——生成预训练 Transformer(GPT-2 和 GPT-3)。我们将展示如何在不到 10 行代码中使用 PyTorch 构建一个开箱即用的高级文本生成器。我们还将探讨从语言模型生成文本时涉及的一些策略。
使用 GPT 模型作为文本生成器
借助 Hugging Face 的 transformers 库或 openai 库与 PyTorch 结合,我们可以加载大多数最新的高级 Transformer 模型,以执行各种任务,如语言建模、文本分类、机器翻译等。我们在第 5 章《高级混合模型》中演示了如何做到这一点。
在本节中,我们将首先使用 transformers 库加载 GPT-2 语言模型。然后,我们将扩展这个拥有 15 亿参数的模型,使其成为一个文本生成器。接着,我们将探讨从预训练语言模型生成文本时可以采用的各种策略,并使用 PyTorch 来演示这些策略。
最后,我们将使用 openai 库加载拥有 1750 亿参数的 GPT-3 模型,并展示其生成逼真的自然语言的能力。
使用 GPT-2 进行开箱即用的文本生成
作为一个练习,我们将使用 transformers 库加载 GPT-2 语言模型,并将其扩展为一个文本生成模型,以生成任意但有意义的文本。出于演示目的,我们将仅展示代码的重要部分。要访问完整代码,请转到 GitHub [2]。请按照以下步骤操作:
首先,我们需要导入必要的库:
from transformers import GPT2LMHeadModel, GPT2Tokenizer
import torch
我们将导入 GPT-2 多头语言模型 [3] 及其相应的分词器 [4] 以生成词汇表。
接下来,我们将实例化 GPT2Tokenizer 和语言模型。然后,我们将提供一组初始单词作为模型的提示,如下所示:
torch.manual_seed(799)
tkz = GPT2Tokenizer.from_pretrained("gpt2")
mdl = GPT2LMHeadModel.from_pretrained('gpt2')
ln = 10
cue = "They"
gen = tkz(cue, return_tensors="pt")
to_ret = gen["input_ids"][0]
最后,我们将使用语言模型迭代地预测给定输入词序列的下一个单词。在每次迭代中,预测的单词都会附加到下一个迭代的输入词序列中:
prv = None
for i in range(ln):
outputs = mdl(**gen)
next_token_logits = torch.argmax(outputs.logits[-1, :])
to_ret = torch.cat([to_ret, next_token_logits.unsqueeze(0)])
gen = {"input_ids": to_ret}
seq = tkz.decode(to_ret)
print(seq)
输出结果如下所示:
They are not the only ones who are being targeted.
这种生成文本的方法也称为贪婪搜索。在下一节中,我们将更详细地讨论贪婪搜索以及其他一些文本生成策略。
使用 PyTorch 的文本生成策略
当我们使用训练好的文本生成模型来生成文本时,通常会逐字预测。然后我们将预测的单词序列整合为预测文本。当我们在循环中迭代单词预测时,需要指定一种方法来根据之前的 k 个预测找到或预测下一个单词。这些方法也被称为文本生成策略,在本节中我们将讨论一些常见的策略。
贪婪搜索
之所以称为“贪婪”,是因为模型在当前迭代中选择具有最高概率的单词,而不管未来的时间步中可能出现的单词。在这种策略下,模型可能会错过一个隐藏在低概率单词之后的高概率单词,仅仅因为模型没有在当前时间步选择低概率单词。图 7.1 通过一个假设的场景演示了贪婪搜索策略,说明了在前一个练习的步骤 3 中,模型可能正在发生的情况。在每个时间步,文本生成模型输出可能的单词及其概率:
正如我们所看到的,在每一步中,模型在贪婪搜索策略下选择了概率最高的单词。注意倒数第二步,模型预测了 system、people 和 future 这三个单词,它们的概率大致相等。使用贪婪搜索时,system 被选为下一个单词,因为它的概率稍微高于其他单词。然而,你可能会认为 people 或 future 会导致更好或更有意义的生成文本。
这是贪婪搜索方法的核心局限性。此外,由于缺乏随机性,贪婪搜索还可能导致生成结果重复。如果有人想用这种文本生成器来进行艺术创作,贪婪搜索并不是最好的选择,因为它的生成结果往往过于单一。
在上一节中,我们手动编写了文本生成循环。得益于 transformers 库,我们可以用三行代码来编写文本生成步骤:
ip_ids = tkz.encode(cue, return_tensors='pt')
op_greedy = mdl.generate(ip_ids, max_length=ln, pad_token_id=tkz.eos_token_id)
seq = tkz.decode(op_greedy[0], skip_special_tokens=True)
print(seq)
这应该会输出以下内容:
They are not the only ones who are being targeted
注意,生成的句子比手动文本生成循环生成的句子少了一个标点符号(句号)。这种差异是因为在后面的代码中,max_length 参数包括了提示词(cue words)。所以,如果我们有一个提示词,则只会预测九个新单词,这里就是这种情况。
束搜索(Beam Search)
贪婪搜索并不是生成文本的唯一方法。束搜索是贪婪搜索方法的一种改进,在束搜索中,我们根据整体预测序列的概率而不是仅仅根据下一个单词的概率,维护一组潜在候选序列。要追踪的候选序列数目就是沿着单词预测树展开的束数。
图 7.2 演示了如何使用束大小为三的束搜索来生成三个候选序列(按整体序列概率排序),每个序列包含五个单词:
在每次迭代中,这个束搜索示例中会维护三个最有可能的候选序列。随着序列的推进,可能的候选序列数量会指数增加。然而,我们只对排名前三的序列感兴趣。这样,我们不会像贪婪搜索那样错过潜在的更好序列。
在 PyTorch 中,我们可以用一行代码直接使用束搜索。以下代码演示了基于束搜索的文本生成,生成三个最可能的句子,每个句子包含五个单词:
op_beam = mdl.generate(
ip_ids,
max_length=5,
num_beams=3,
num_return_sequences=3,
pad_token_id=tkz.eos_token_id
)
for op_beam_cur in op_beam:
print(tkz.decode(op_beam_cur, skip_special_tokens=True))
这将输出以下内容:
They have a lot of
They have a lot to
They are not the only
束搜索仍然存在重复性或单调性的问题。不同的运行会产生相同的结果,因为它决定性地寻找具有最大整体概率的序列。在下一节中,我们将探讨一些方法,以使生成的文本更加不可预测或富有创意。
Top-k 和 Top-p 采样
与总是选择下一个单词概率最高的单词不同,我们可以根据相对概率从可能的单词集中随机采样下一个单词。例如,在图 7.2 中,单词 be、know 和 show 的概率分别为 0.7、0.2 和 0.1。我们可以随机采样这三个单词中的任何一个,而不是总是选择 be。如果我们重复此过程 10 次以生成 10 个不同的文本,be 将被选择约 7 次,know 和 show 分别被选择 2 次和 1 次。这为我们提供了比束搜索或贪婪搜索更多的不同可能组合。
两种最流行的使用采样技术生成文本的方法是 top-k 采样和 top-p 采样。在 top-k 采样中,我们预定义一个参数 k,这是在采样下一个单词时应考虑的候选单词数量。所有其他单词都被丢弃,概率在前 k 个单词之间进行归一化。在我们之前的例子中,如果 k 是 2,那么单词 show 将被丢弃,单词 be 和 know 的概率(分别为 0.7 和 0.2)将归一化为 0.78 和 0.22。
以下代码演示了 top-k 文本生成方法:
for i in range(3):
torch.manual_seed(i+10)
op = mdl.generate(
ip_ids,
do_sample=True,
max_length=5,
top_k=2,
pad_token_id=tkz.eos_token_id
)
seq = tkz.decode(op[0], skip_special_tokens=True)
print(seq)
这将生成以下输出:
They are the most important
They have a lot to
They are not going to
要从所有可能的单词中采样,而不仅仅是 top-k 单词,我们可以将 top_k 参数设置为 0。在之前的代码输出中,可以看到不同的运行产生了不同的结果,而不是贪婪搜索,每次运行都产生相同的结果,如以下代码所示:
for i in range(3):
torch.manual_seed(i+10)
op_greedy = mdl.generate(ip_ids, max_length=5, pad_token_id=tkz.eos_token_id)
seq = tkz.decode(op_greedy[0], skip_special_tokens=True)
print(seq)
这将输出:
They are not the only
They are not the only
They are not the only
在 top-p 采样策略下,我们不是定义要查看的前 k 个单词,而是定义一个累积概率阈值 (p),然后保留概率总和达到 p 的单词。在我们的例子中,如果 p 在 0.7 到 0.9 之间,则丢弃 know 和 show;如果 p 在 0.9 到 1.0 之间,则丢弃 show;如果 p 是 1.0,则保留所有三个单词,即 be、know 和 show。
在概率分布较平坦的情况下,top-k 策略可能会不公平,因为它会剪掉那些几乎与保留单词一样可能的单词。在这些情况下,top-p 策略会保留更多的单词池进行采样,而在概率分布较陡峭的情况下,则会保留更小的单词池。
以下代码演示了 top-p 采样方法:
for i in range(3):
torch.manual_seed(i+10)
op = mdl.generate(
ip_ids,
do_sample=True,
max_length=5,
top_p=0.75,
top_k=0,
pad_token_id=tkz.eos_token_id
)
seq = tkz.decode(op[0], skip_special_tokens=True)
print(seq)
这将输出以下内容:
They got them here in
They have also challenged foreign
They said it would be
我们可以将 top-k 和 top-p 策略一起设置。在这个例子中,我们将 top_k 设置为 0,以基本上禁用 top-k 策略,top_p 设置为 0.75。这再次导致不同的句子生成,并可能导致比贪婪搜索或束搜索更具创造性的文本生成。还有许多其他文本生成策略,相关的研究也在不断进行。我们鼓励你进一步探索这个领域。
一个很好的起点是尝试 transformers 库中提供的文本生成策略。你可以在他们的博客文章中阅读更多内容。现在让我们看看 GPT-2 的继任者 GPT-3,并通过 openai 库使用这个模型生成有意义的文本。
使用 GPT-3 进行文本生成
如前面的练习所示,我们将加载一个预训练的 GPT-3 模型,并将其用作文本生成器。我们将使用 openai 库提供的 API。OpenAI 是开发了 GPT-2 和 GPT-3 模型的公司。要访问 GPT-3 模型,我们需要一个 OpenAI API 密钥,这需要创建一个 OpenAI 帐户。
一旦拥有 API 密钥,我们可以进入我们的迷你练习,代码可以在 GitHub 上找到。
首先,我们需要导入必要的库并设置环境变量:
import os
from openai import OpenAI
client = OpenAI(api_key="<your-open-ai-api-key-here>")
然后,我们定义我们的提示(与之前相同):
prompt = "They"
最后,通过一行代码,我们可以使用 OpenAI 的 Completion.create API 来提示 GPT-3 模型生成文本:
response = client.chat.completions.create(
model="gpt-3.5-turbo-instruct",
response_format={"type": "json_object"},
messages=[
{"role": "user", "content": prompt}
],
temperature=0.5,
max_tokens=5,
top_p=1.0,
frequency_penalty=0.0,
presence_penalty=0.0
)
参数 temperature 和 top_p 用于调整序列中下一个单词的采样策略,其中较高的值会导致采样的随机性增加。然而,建议仅调整其中一个参数,而不是同时调整两个参数。你可以在 OpenAI 的 API 参考文档中详细阅读它们的说明。
这里使用的具体模型叫做 gpt-3.5-turbo-instruct,这是 OpenAI 认为最强大的 GPT-3 模型之一。
我们现在可以打印响应:
print(response.choices[0].message.content)
这应该会生成类似以下内容的结果:
are an important part of
因为我们选择了 top_p=1,你可能会观察到不同但有意义的结果。然而,生成的单词数量会被限制在 max_tokens 参数中指定的 5 个单词以内。
你可能会好奇,为什么我们需要 1750 亿个参数来生成一个以“they”开头的有意义的英文句子。GPT-3 能做的远不止这些。这里有一个例子:
prompt = "Write a poem starting with they"
response = client.chat.completions.create(
model="gpt-3.5-turbo-instruct",
response_format={"type": "json_object"},
messages=[
{"role": "user", "content": prompt}
],
temperature=0.5,
max_tokens=100,
top_p=1.0,
)
我从上面的代码中得到了以下输出:
They always say
That life is a mystery
And we will never really know
What happens after we die
But I think
That we can be pretty sure
That there is something
After this life
Something better
And we will finally be able
To rest in peace
这个例子展示了 GPT-3 模型的能力范围。
在提示这样强大的模型时,你的创造力是唯一的限制。实际上,随着这些强大语言模型的出现,产生了一门全新的学科,叫做提示工程。OpenAI 的网站上有一个很好的指南,介绍了如何针对不同用例提示 GPT-3 模型。最后,我强烈推荐 Packt 出版的《Exploring-GPT-3》进一步探索 GPT-3 的功能。
这结束了我们使用 PyTorch 生成文本的探索。在下一节中,我们将进行类似的练习,但这次我们将处理音乐而不是文本。我们的想法是训练一个无监督模型在音乐数据集上,并使用训练好的模型生成类似于训练数据集中的旋律。
使用 PyTorch 和 LSTM 生成 MIDI 音乐
在文本生成的基础上,本节将使用 PyTorch 创建一个能够作曲的机器学习模型。我们在前一节使用了 Transformers 来生成文本,而这里我们将使用长短期记忆(LSTM)模型来处理序列音乐数据。我们将训练模型来生成类似莫扎特的古典音乐作品。
每个音乐作品将被分解为一系列钢琴音符。我们将以 Musical Instruments Digital Interface(MIDI)文件的形式读取音乐数据,这是一种方便的格式,广泛用于在不同设备和环境之间读取和写入音乐数据。
在将 MIDI 文件转换为钢琴音符序列(即钢琴卷轴)后,我们将使用这些序列来训练一个下一个钢琴音符检测系统。在这个系统中,我们将构建一个基于 LSTM 的分类器,用于预测给定前序音符序列的下一个音符,总共有 88 个音符(根据标准 88 键钢琴)。
我们将展示构建 AI 音乐作曲器的整个过程。我们的重点将是 PyTorch 代码,用于数据加载、模型训练和生成音乐样本。请注意,模型训练过程可能需要几个小时,因此建议在后台运行训练过程,例如,过夜。这里的代码经过简化,以保持文本简洁。
处理 MIDI 音乐文件的细节超出了本书的范围,但鼓励你探索 GitHub 上提供的完整代码 [12]。
加载 MIDI 音乐数据
首先,我们将演示如何加载以 MIDI 格式提供的音乐数据。我们将简要提及处理 MIDI 数据的代码,然后展示如何将其转换为 PyTorch 数据加载器。让我们开始吧:
首先,我们将导入必要的库。在本练习中,我们将使用以下新库:
import skimage.io as io
from struct import pack, unpack
from io import StringIO, BytesIO
skimage 用于可视化模型生成的音乐样本序列,struct 和 io 用于将 MIDI 音乐数据转换为钢琴卷轴的处理过程。
接下来,我们将编写用于加载 MIDI 文件和将其转换为可以输入到 LSTM 模型的钢琴音符序列(矩阵)的辅助类和函数。首先,我们定义一些 MIDI 常量,以配置各种音乐控制,如音高、通道、序列的开始和结束等:
NOTE_MIDI_OFF = 0x80
NOTE_MIDI_ON = 0x90
CHNL_PRESS = 0xD0
MIDI_PITCH_BND = 0xE0
...
然后,我们将定义一系列处理 MIDI 数据输入和输出流、MIDI 数据解析器等的类,如下所示:
class MOStrm:
# MIDI Output Stream
...
class MIFl:
# MIDI Input File Reader
...
class MOFl(MOStrm):
# MIDI Output File Writer
...
class RIStrFl:
# Raw Input Stream File Reader
...
class ROStrFl:
# Raw Output Stream File Writer
...
class MFlPrsr:
# MIDI File Parser
...
class EvtDspch:
# Event Dispatcher
...
class MidiDataRead(MOStrm):
# MIDI Data Reader
...
处理完所有 MIDI 数据 I/O 相关的代码后,我们准备实例化 PyTorch 数据集类。在此之前,我们必须定义两个关键函数,一个用于将读取的 MIDI 文件转换为钢琴卷轴,另一个用于用空音符填充钢琴卷轴,以便标准化数据集中的音乐作品长度:
def md_fl_to_pio_rl(md_fl):
md_d = MidiDataRead(md_fl, dtm=0.3)
pio_rl = md_d.pio_rl.transpose()
pio_rl[pio_rl > 0] = 1
return pio_rl
def pd_pio_rl(pio_rl, mx_l=132333, pd_v=0):
orig_rol_len = pio_rl.shape[1]
pdd_rol = np.zeros((88, mx_l))
pdd_rol[:] = pd_v
pdd_rol[:, -orig_rol_len:] = pio_rl
return pdd_rol
现在,我们可以定义 PyTorch 数据集类,如下所示:
class NtGenDataset(data.Dataset):
def __init__(self, md_pth, mx_seq_ln=1491):
...
def mx_len_upd(self):
...
def __len__(self):
return len(self.md_fnames_ful)
def __getitem__(self, index):
md_fname_ful = self.md_fnames_ful[index]
pio_rl = md_fl_to_pio_rl(md_fname_ful)
seq_len = pio_rl.shape[1] - 1
ip_seq = pio_rl[:, :-1]
gt_seq = pio_rl[:, 1:]
...
return (torch.FloatTensor(ip_seq_pad),
torch.LongTensor(gt_seq_pad),
torch.LongTensor([seq_len]))
除了数据集类,我们还需要添加另一个辅助函数,将训练数据批次中的音乐序列后处理为三个单独的列表。这些列表包括输入序列、输出序列和序列长度,按序列长度的降序排列:
def pos_proc_seq(btch):
ip_seqs, op_seqs, lens = btch
...
ord_tr_data_tups = sorted(tr_data_tups,
key=lambda c: int(c[2]),
reverse=True)
ip_seq_splt_btch, op_seq_splt_btch, btch_splt_lens = \
zip(*ord_tr_data_tups)
...
return tps_ip_seq_btch, ord_op_seq_btch, list(
ord_btch_lens_l)
在本练习中,我们将使用一组莫扎特的作品。你可以从钢琴 MIDI 网站 [13] 下载数据集。下载的文件夹包含 21 个 MIDI 文件,我们将其分为 18 个训练文件和 3 个验证文件。下载的数据存储在 ./mozart/train 和 ./mozart/valid 目录下。下载完成后,我们可以读取数据并实例化训练和验证数据集加载器:
training_dataset = NtGenDataset(
'./mozart/train', mx_seq_ln=None)
training_datasetloader = data.DataLoader(
training_dataset, batch_size=5, shuffle=True,
drop_last=True)
validation_dataset = NtGenDataset(
'./mozart/valid/', mx_seq_ln=None)
validation_datasetloader = data.DataLoader(
validation_dataset, batch_size=3,
shuffle=False, drop_last=False)
查看验证集的第一个批次:
X_validation = next(iter(validation_datasetloader))
X_validation[0].shape
这应该会给出以下输出:
torch.Size([3, 1587, 88])
从中可以看出,第一个验证批次包含 3 个长度为 1587 的序列,每个序列被编码为一个 88 维的向量,其中 88 是钢琴键的总数。对于那些受过音乐训练的人,这里是验证集中的一个音乐文件的前几个音符的乐谱等效物:
另一种方法是将音符序列可视化为一个矩阵,矩阵有 88 行,每行代表一个钢琴键。以下是前面旋律的可视化矩阵表示(从 1,587 个音符中的前 300 个音符):
数据集引用
Bernd Krueger 的 MIDI、音频(MP3、OGG)和视频文件采用 CC BY-SA Germany 许可证。名称:Bernd Krueger。来源:www.piano-midi.de。仅在相同许可证条件下允许这些文件的分发或公开播放。这些乐谱是开源的。
定义 LSTM 模型和训练过程
到目前为止,我们已经成功加载了 MIDI 数据集,并用它创建了自己的训练和验证数据加载器。在本节中,我们将定义 LSTM 模型架构,以及在模型训练循环中运行的训练和评估例程。让我们开始吧:
首先,我们必须定义模型架构。如前所述,我们将使用一个 LSTM 模型,该模型包括一个编码器层,该层在序列的每个时间步将 88 维的输入数据表示编码为 512 维的隐藏层表示。编码器后面跟着两个 LSTM 层,然后是一个全连接层,最后是一个 Softmax 层,产生 88 个类(钢琴键)的 88 个概率。
根据我们在第 4 章《深度递归模型架构》中讨论的不同类型的递归神经网络(RNN),这是一个多对一的序列分类任务,其中输入是从时间步 0 到时间步 t 的整个序列,输出是时间步 t+1 的 88 个类中的一个,如下所示:
class MusicLSTM(nn.Module):
def __init__(self, ip_sz, hd_sz, n_cls, lyrs=2):
...
self.nts_enc = nn.Linear(in_features=ip_sz, out_features=hd_sz)
self.bn_layer = nn.BatchNorm1d(hd_sz)
self.lstm_layer = nn.LSTM(hd_sz, hd_sz, lyrs)
self.fc_layer = nn.Linear(hd_sz, n_cls)
def forward(self, ip_seqs, ip_seqs_len, hd=None):
...
pkd = torch.nn.utils.rnn.pack_padded_sequence(nts_enc_ful, ip_seqs_len)
op, hd = self.lstm_layer(pkd, hd)
...
lgts = self.fc_layer(op_nrm_drp.permute(2,0,1))
...
zero_one_lgts = torch.stack((lgts, rev_lgts), dim=3).contiguous()
flt_lgts = zero_one_lgts.view(-1, 2)
return flt_lgts, hd
在定义了模型架构后,我们可以指定模型训练过程。我们将使用带有梯度裁剪的 Adam 优化器来避免过拟合。另一个已经在前一步中实现的对抗过拟合的措施是使用 Dropout 层:
def lstm_model_training(
lstm_model, lr, ep=10, val_loss_best=float("inf")):
...
for curr_ep in range(ep):
...
for batch in training_datasetloader:
...
lgts, _ = lstm_model(ip_seq_b_v, seq_l)
loss = loss_func(lgts, op_seq_b_v)
...
if vl_ep_cur < val_loss_best:
torch.save(lstm_model.state_dict(), 'best_model.pth')
val_loss_best = vl_ep_cur
return val_loss_best, lstm_model
类似地,我们将定义模型评估过程,其中在模型上运行前向传递,模型参数保持不变:
def evaluate_model(lstm_model):
...
for batch in validation_datasetloader:
...
lgts, _ = lstm_model(ip_seq_b_v, seq_l)
loss = loss_func(lgts, op_seq_b_v)
vl_loss_full += loss.item()
seq_len += sum(seq_l)
return vl_loss_full/(seq_len*88)
训练和测试音乐生成模型
在最后一节中,我们将实际训练 LSTM 模型。然后,我们将使用训练好的音乐生成模型生成一个音乐样本,以便听取和分析。让我们开始吧:
我们已经准备好实例化模型并开始训练。我们为这个分类任务使用了类别交叉熵作为损失函数。我们使用学习率为 0.01 训练模型,训练 10 个周期:
loss_func = nn.CrossEntropyLoss().cpu()
lstm_model = MusicLSTM(ip_sz=88, hd_sz=512, n_cls=88).cpu()
val_loss_best, lstm_model = lstm_model_training(lstm_model, lr=0.01, ep=10)
这将输出如下内容:
ep 0 , train loss = 2.3905489842096963
ep 0 , val loss = 3.8042128349324635e-06
ep 1 , train loss = 0.9679248531659445
ep 1 , val loss = 2.122019823561201e-06
ep 2 , train loss = 0.2935091306765874
ep 2 , val loss = 1.2749193585637908e-06
...
ep 7 , train loss = 0.16012300054232279
ep 7 , val loss = 1.2555179474370303e-06
ep 8 , train loss = 0.12387428929408391
ep 8 , val loss = 1.4818597425925305e-06
ep 9 , train loss = 0.13243193179368973
ep 9 , val loss = 1.6489400508525355e-06
到了有趣的部分。一旦我们有了下一个音乐音符预测器,我们可以将其用作音乐生成器。我们只需通过提供初始音符作为提示来启动预测过程。然后,模型可以在每个时间步递归地预测下一个音符,其中时间步 t 的预测将附加到时间步 t+1 的输入序列中。
在这里,我们将编写一个音乐生成函数,该函数接受训练好的模型对象、生成音乐的预期长度、起始音符以及温度。温度是对分类层中 Softmax 函数的标准数学操作。它用于操作 Softmax 概率的分布,既可以扩展也可以缩小 Softmax 概率分布。代码如下:
def generate_music(lstm_model, ln=100, tmp=1, seq_st=None):
...
for i in range(ln):
op, hd = lstm_model(seq_ip_cur, [1], hd)
probs = nn.functional.softmax(op.div(tmp), dim=1)
...
gen_seq = torch.cat(op_seq, dim=0).cpu().numpy()
return gen_seq
最后,我们可以使用此函数创建全新的音乐作品:
seq = generate_music(lstm_model, ln=100, tmp=0.8, seq_st=None)
midiwrite('generated_music.mid', seq, dtm=0.2)
这将创建音乐作品并将其保存为当前目录中的 MIDI 文件。我们可以打开文件并播放它,听听模型生成的内容。我们还可以查看生成音乐的可视化矩阵表示:
io.imshow(seq)
这将显示以下输出:
此外,生成的音乐将如下所示的乐谱:
这里,我们可以看到生成的旋律似乎不如莫扎特的原作那样动听。然而,您可以看到模型学习到的一些关键组合的相似性。此外,通过在更多的数据上训练模型以及增加训练的轮数,可以提升生成音乐的质量。
这次练习介绍了如何使用机器学习生成音乐。在这一部分,我们展示了如何利用现有的音乐数据从头开始训练一个音符预测模型,并使用训练好的模型生成音乐。实际上,您可以扩展这一思想,使用生成模型生成任何类型的数据样本。PyTorch 在这种用例中是一个非常有效的工具,特别是由于其直观的 API 用于数据加载、模型构建/训练/测试,以及将训练好的模型用作数据生成器。建议您尝试在不同的用例和数据类型上进行更多类似的任务。
总结
在本章中,我们探索了使用 PyTorch 的生成模型。在下章中,我们将学习如何利用机器学习将一种图像的风格转移到另一种图像上。借助 PyTorch,我们将使用卷积神经网络(CNNs)从各种图像中学习艺术风格,并将这些风格应用于不同的图像——这一任务更为人熟知的名称是神经风格迁移。