AIGC实战——基于Transformer实现音乐生成
- 0. 前言
- 1. 音乐生成的挑战
- 2. MuseNet
- 3. 音乐数据
- 3.1 巴赫大提琴组曲数据集
- 3.2 解析 MIDI 文件
- 3.3 分词
- 3.4 创建训练数据集
- 4. MuseNet 模型
- 4.1 正弦位置编码
- 4.2 多输入/输出
- 5. 音乐生成 Transformer 的分析
- 6. 多声部音乐分词
- 6.1 网格分词
- 6.2 基于事件的分词
- 小结
- 系列链接
0. 前言
Transformer 是最流行的音乐生成技术之一,因为音乐可以视为一个序列预测问题,Transformer
模型将音符视为一个个符号的序列(类似于句子中的单词),从而用于生成音乐。Transformer
模型基于先前音符预测下一个音符,生成音乐作品。在本节中,将学习如何处理音乐数据,并应用 Transformer
生成与给定训练集风格相似的音乐。
1. 音乐生成的挑战
要让机器创作出悦耳的音乐,则必须克服与文本生成中所介绍的相似的技术挑战。模型必须能够学习并再现音乐的序列结构,且能够根据一组离散概率选择下一个音符。
然而,除了文本生成所需的技术外,音乐生成还存在其他挑战,即音高和节奏。音乐通常是多声部的,即不同乐器同时演奏多个音符流,它们结合在一起形成的是和声有可能不协和(刺耳)也有可能不协和(和谐)。文本生成仅需要处理一个单一的文本流,而音乐生成则需要处理多个并行的和弦流。
此外,文本生成可以逐个单词处理。但与文本数据不同,音乐是一个多部分、相交织在一起的声音轴,这些声音不一定同时出现,不同乐器之间不同节奏的相互作用是音乐的独特魅力。例如,吉他手可能弹奏一连串快速的音符,而钢琴师则可能弹奏一个较长的持续音。因此,逐音符生成音乐是复杂的,因为我们通常不希望所有乐器同时改变音符。
2. MuseNet
本节,我们将构建一个解码器 Transformer
,模型经过训练后,可以根据先前的音符序列预测下一个音符。在音乐生成任务中,随着音乐的生成,序列的长度 N
将变得很大,这意味着每个头部的 N × N
注意力矩阵的存储和计算成本很高。我们并不希望将输入序列剪切为较少的符号数,因为我们希望模型能够生成长期结构作品。
为了解决此问题,MuseNet
采用了稀疏 Transformer
的 Transformer
形式。注意力矩阵中的每个输出位置只计算输入位置子集的权重,从而降低了训练模型所需的计算复杂度和内存。因此,MuseNet
可以对 4096
个符号执行全局注意力,并且可以学习各种风格的长期结构和旋律结构。
音乐的延续通常受到前几小节音符的影响,例如,Prelude to Bach’s Cello Suite No. 1
的开头小节如下图所示。
小节
小节 (bar
) 是包含固定数量拍子的小段音乐,用穿过五线谱的小节线 (|
) 划分。如果按照1
、2
、1
、2
的节奏数来对一首音乐进行计数,那么每小节有两拍。如果按照1
、2
、3
、1
、2
、3
的节奏数进行计数,那么每小节有三拍。
以上巴赫大提琴音乐的开头小节后的下一音符是什么,即使我们没有进行过专业的音乐训练,我们还是可能可以猜到乐曲的第一个音符为 G
(与乐曲的第一个音符相同)。这是因为每小节和每半个小节都以同一个音符开头,我们可以利用这一信息来做出决策。我们希望模型也能做到同样的事情,我们希望模型能够关注到前半个小节的特定音符。基于注意力的模型 Transformer
将能够在不必维护隐藏状态的情况下,将这种长期结构纳入考虑,而在循环神经网络中必须要维护隐藏状态。
在继续学习音乐生成模型之前,首先必须基本了解音乐理论。在下一节中,我们将了解音乐相关的基本知识以及如何将其数值化,以便将音乐转化为训练 Transformer
所需的输入数据。
3. 音乐数据
3.1 巴赫大提琴组曲数据集
为了训练音乐生成模型,我们所使用的数据集是巴赫的大提琴组曲 (Bach Cello Suite
) 的一组 MIDI
文件,下载地址:https://pan.baidu.com/s/1TBpwnn50uRuKv0FsenPRlw,提取码:jgjg
。下载数据集后,将 MIDI
文件保存到本地的 ./data
文件夹中。
3.2 解析 MIDI 文件
使用 music21
库将 MIDI
文件加载到 Python
中进行处理。加载 MIDI
文件并将其可视化,作为乐谱和结构化数据。
import music21
file = "./data/cs1-2all.mid"
example_score = music21.converter.parse(file).chordify()
八度
音符名称后面的数字表示音符所在的八度 (Octave
)。由于音符名称 (A
到G
) 是重复的,因此需要这个数字来唯一标识音符的音高,例如,G2
是低于G3
一个八度的音符。
接下来,我们将乐谱转换成类似于文本的数据,我们首先循环遍历每个乐谱,并将每个乐曲元素的音符和持续时间提取到两个单独的文本字符串中,元素之间用空格分隔。我们将乐曲的调号和拍号编码为特殊符号,并设置持续时间为零。
单声部音乐与多声部音乐
在本节中,我们将把音乐视为单声部 (Monophonic
) 音乐,只提取和弦中的顶音。有时我们希望保持不同部分的分离数据,以生成多声部 (Polyphonic
) 音乐。
该过程的输出如下图所示,可以看到原始音乐被转换为两个字符串:
这类似于处理后文本数据,单词是音符-持续时间的组合。构建模型,给定先前音符和持续时间的序列,预测下一个音符和持续时间。音乐生成和文本生成之间的一个关键区别是,我们需要构建一个能够同时处理音符和持续时间预测的模型,即我们需要处理两个信息流,而不是单一文本流。
3.3 分词
为了创建用于训练模型的数据集,我们首先需要对每个音符和持续时间进行分词,类似于文本语料库中对每个单词所做的处理。我们可以通过使用一个 TextVectorization
层,分别应用于音符和持续时间来实现。\
def create_dataset(elements):
ds = (
tf.data.Dataset.from_tensor_slices(elements)
.batch(BATCH_SIZE, drop_remainder=True)
.shuffle(1000)
)
vectorize_layer = layers.TextVectorization(
standardize=None, output_mode="int"
)
vectorize_layer.adapt(ds)
vocab = vectorize_layer.get_vocabulary()
return ds, vectorize_layer, vocab
notes_seq_ds, notes_vectorize_layer, notes_vocab = create_dataset(notes)
durations_seq_ds, durations_vectorize_layer, durations_vocab = create_dataset(
durations
)
seq_ds = tf.data.Dataset.zip((notes_seq_ds, durations_seq_ds))
完整的解析和分词过程如下图所示。
3.4 创建训练数据集
预处理的最后一步是创建 Transformer
训练数据集。我们通过使用滑动窗口技术将音符和持续时间字符串分成 50
个元素的块来实现这一点。输出仅仅是将输入窗口向后移动一个音符,给定窗口中的先前元素,训练 Transformer
预测未来一个时间步的音符和持续时间,如下图所示。
4. MuseNet 模型
在 Transformer
音乐生成中,我们将使用与文本生成类似的架构。
4.1 正弦位置编码
首先,我们引入一种称为正弦位置编码 (Sine Position Encoding
) 的编码方式来表示符号位置。使用嵌入层来对每个符号的位置进行编码时,实际上是将每个整数位置映射到模型学到的不同向量。因此,我们需要定义一个最大长度(N)的序列,并在这个长度的序列上进行训练。这种方法的缺点是无法推广到超过最大长度的序列,必须将输入剪切为 N
个符号,这并不适用于生成较长内容。
为了解决这个问题,我们使用正弦位置嵌入,这类似于用于编码扩散模型的噪声方差。具体而言,使用以下函数将输入序列中单词的位置 (pos
) 转换为长度为 d
的唯一向量:
P
E
p
o
s
,
2
i
=
s
i
n
(
p
o
s
1000
0
2
i
d
)
P
E
p
o
s
,
2
i
+
1
=
c
o
s
(
p
o
s
1000
0
2
i
+
1
d
)
PE_{pos, 2i} = sin(\frac {pos} {10000^{\frac {2i} d}})\\ PE_{pos, 2i+1} = cos(\frac {pos} {10000^{\frac {2i + 1} {d}}})
PEpos,2i=sin(10000d2ipos)PEpos,2i+1=cos(10000d2i+1pos)
对于较小的
i
i
i,该函数的波长较短,因此函数值沿着位置轴快速变化。
i
i
i 值越大,波长越长。因此,每个位置都可以使用不同波长的特定组合,得到独特的编码。
需要注意的是,此嵌入是为所有位置值定义的。它是一个确定性函数(即它不是由模型不学习的),使用三角函数为每个位置定义唯一的编码。
Keras NLP
模块内置了一个实现该嵌入的层,可以使用以下代码定义 TokenAndPositionEmbedding
层:
class TokenAndPositionEmbedding(layers.Layer):
def __init__(self, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.vocab_size = vocab_size
self.embed_dim = embed_dim
self.token_emb = layers.Embedding(
input_dim=vocab_size,
output_dim=embed_dim,
embeddings_initializer="he_uniform",
)
self.pos_emb = SinePositionEncoding()
def call(self, x):
embedding = self.token_emb(x)
positions = self.pos_emb(embedding)
return embedding + positions
def get_config(self):
config = super().get_config()
config.update(
{
"vocab_size": self.vocab_size,
"embed_dim": self.embed_dim,
}
)
return config
下图显示了如何将两个嵌入(符号和位置)相加,以获取序列的总嵌入。
4.2 多输入/输出
模型包括两个输入流(音符和持续时间)和两个输出流(预测的音符和持续时间),为此,我们需要调整 Transformer
架构。
为了处理两个输入流,我们可以创建表示每个音符-持续时间对的符号,然后将序列视为一个符号流。然而,这种方法的缺点是无法表示在训练集中未见过的音符-持续时间对(例如,训练集可能有一个独立的 G#2
音符和 1/3
的持续时间,但从未同时出现过,所以没有 G#2:1/3
的符号)。
而分别嵌入音符和持续时间令牌,然后使用连接层创建输入的表示,该表示可以被下游的 Transformer
块使用。类似地,Transformer
块的输出传递到两个单独的全连接层,表示预测的音符和持续时间的概率。整体架构如下图所示,图中 b
表示批大小,I
表示序列长度。
另一种方法是将音符和持续时间符号交错传递到单个输入流中,模型学习输出为音符和持续时间符号交替的单一流。这会增加模型的复杂性,需要确保当模型尚未学习如何正确交错令牌时,输出仍然可以被解析。
5. 音乐生成 Transformer 的分析
为了从头开始生成音乐,使用 START
音符符号和 0.0
持续时间符号初始化网络(即,令模型从乐曲的开头开始生成音乐)。然后,我们可以使用与生成文本序列相同的迭代技术来生成一个音乐片段,具体步骤如下:
- 给定当前序列(音符和持续时间),模型预测下一个音符和下一个持续时间的概率分布
- 使用一个温度参数从这两个分布中采样,以控制采样过程中的随机程度
- 采样的音符和持续时间添加到各自的输入序列中
- 重复此过程,为我们想要生成的元素数生成新的输入序列
下图展示了模型在训练过程的不同 epoch从
头开始生成的音乐样本,将音符和持续时间的温度参数设为 0.5
。
本节中我们主要对生成的音符(而非持续时间)进行分析,因为对于巴赫大提琴组曲来说,和声上的复杂性更难捕捉,我们也可以将同样的分析应用到模型的节奏预测上。
观察模型生成的音乐片段,可以看到,随着训练的进行,音乐变得越来越复杂。起初,模型基本上生成相同的音符和节奏。到第 10
个 epoch
,模型开始生成小的音符串,到第 20
个 epoch
,模型开始生成有趣的节奏。
其次,我们可以通过绘制每个时间步上预测分布的热力图来分析音符随时间的分布,下图展示了第 20
个 epoch
中样本的热力图。
值得注意的是,模型已经学会了哪些音符属于特定的调,因为在不属于该调的音符处,分布中存在空缺。模型在生成过程的早期就确定了调,并且随着音乐的生成,模型通过关注表示该调的符号来选择更有可能出现在该调中的音符。同时,模型已经学会了巴赫的作曲风格,即在大提琴上降到低音结束一个乐句,然后再开始下一个乐句。
最后,检查注意力机制是否按预期工作。下图的水平轴显示了生成的音符序列;垂直轴显示了网络在预测水平轴上的每个音符时所关注的位置。每个方格的颜色显示了整个序列中所有注意力头的最大注意力权重。颜色越深,表示在序列中这个位置上的注意力越多。为了简单起见,图中只显示了音符,但是网络也会关注每个音符的持续时间。
可以看到,在初始的调号、拍号和休止符中,网络选择把几乎所有的注意力都放在了 START
符号上,因为这些符号总是出现在音乐的开头,一旦音符开始流动,START
符号基本上就不再被关注。
随着音符的生成,可以看到网络主要关注最后两到四个音符,很少在四个音符前的音符上施加明显的注意力,在前四个音符中就可能包含足够的信息来理解乐句的延续方式。此外,一些音符更关注 D
小调的调号,因为这些音符恰好依赖于 D
小调的调号以消除歧义音符。在某些样本中,网络选择忽略附近的某个音符或休止符,因为它对于理解乐句并没有额外的信息。
6. 多声部音乐分词
我们已经看到,Transformer
对于单声部(单音轨)的音乐效果很好,接下来,我们分析它能否适用于生成多声部(多音轨)音乐。
生成多声部关键在于如何将不同的音乐声部表示为一系列符号的序列。在上一节中,我们决将音符和音符的持续时间分为网络的两个独立输入和输出,但我们也了解了可以将这些符号交错成一个单一的流。我们可以使用相同的思想来处理多声部音乐,包括网格分词和基于事件的分词。
6.1 网格分词
在下图中显示巴赫组曲两小节音乐中,有四个不同的声部(高音 [S]
、次高音 [A]
、中音 [T]
、低音 [B]
)。
将这段音乐绘制在网格上,其中纵轴表示音符的音高,横轴表示从乐曲开始以来经过的16分音符数(八分音符)。如果网格方块被填充,则表示在该时间点上有一个音符,四个声部都画在同一个网格上。这个网格称为自动钢琴打孔纸卷 (piano roll
),它类似于一个在物理纸卷上打孔的记录机制。我们可以通过首先沿着四个声部移动,然后按顺序沿着时间步移动来将网格序列化为一系列符号。
然后,训练 Transformer
模型,让其根据先前的符号预测下一个符号。我们可以按组(每个声部一个音符)将生成的序列解码回网格结构,将序列在时间上展开。但同一个音符经常被拆分成多个符号,并夹杂着其他声部的符号。除此之外,这种方法还存在以下缺点:
- 模型无法区分相同音高的长音和两个相邻的较短音符。这是因为分词只是明确地编码了音符是否出现在每个时间步上,而没有编码音符的持续时间
- 这种方法需要音乐具有可被合理大小的块划分的规则节拍。例如,使用当前系统,我们不能编码三连音(在一个拍子上演奏的三个音符组)。我们可以将音乐每个四分音符划分为
12
个步长而不是4
个步长,这会导致表示相同的音乐段所需的符号数量增加三倍,增加了训练过程的开销 - 无法将其他组件添加到分词中,例如动态(每个声部音乐的音量大小)或速度的变化。其局限于二维网格结构,虽然能够方便的表示音高和节奏,但难以将其他组件融入使音乐中
6.2 基于事件的分词
基于事件的分词是一种更灵活的方式,可以将其视为一种词汇表,按照事件序列文字描述音乐的构建方式,使用丰富的符号集合。例如,在下图中,使用三种类型的符号:
NOTE_ON
<音高>(开始播放给定音高的音符)NOTE_OFF
<音高>(停止播放给定音高的音符)TIME_SHIFT
<步长>(按给定的步长向前移动时间)
此词汇表可以用来创建一个描述音乐构造的序列,作为一组指令。我们可以很容易地将其他类型的符号纳入这个词汇表中,以表示后续音符的动态和速度变化。通过使用 TIME_SHIFT<0.33>
标记将三连音的音符与四分音符分开,我们还可以在四分音符下生成三连音。总体而言,这是一种更具表现力的分词框架,虽然在训练集音乐中学习内在模式时可能比网格方法更复杂,因为它本质上结构不如网格方法。
小结
基于 Transformer
的音乐生成模型类似于文本生成模型,音乐和文本生成具有许多共同的特征,通常可以使用类似的技术来处理二者。本节中,通过将音符和持续时间作为两个输入流和输出流纳入 Transformer
架构中来扩展 Transforme
r架构,该模型能够通过准确生成巴赫音乐来学习调和音阶等概念。
系列链接
AIGC实战——生成模型简介
AIGC实战——深度学习 (Deep Learning, DL)
AIGC实战——卷积神经网络(Convolutional Neural Network, CNN)
AIGC实战——自编码器(Autoencoder)
AIGC实战——变分自编码器(Variational Autoencoder, VAE)
AIGC实战——使用变分自编码器生成面部图像
AIGC实战——生成对抗网络(Generative Adversarial Network, GAN)
AIGC实战——WGAN(Wasserstein GAN)
AIGC实战——条件生成对抗网络(Conditional Generative Adversarial Net, CGAN)
AIGC实战——自回归模型(Autoregressive Model)
AIGC实战——改进循环神经网络
AIGC实战——像素卷积神经网络(PixelCNN)
AIGC实战——归一化流模型(Normalizing Flow Model)
AIGC实战——能量模型(Energy-Based Model)
AIGC实战——扩散模型(Diffusion Model)
AIGC实战——GPT(Generative Pre-trained Transformer)
AIGC实战——Transformer模型
AIGC实战——ProGAN(Progressive Growing Generative Adversarial Network)
AIGC实战——StyleGAN(Style-Based Generative Adversarial Network)
AIGC实战——VQ-GAN(Vector Quantized Generative Adversarial Network)