AIGC实战——MuseGAN详解与实现
0. 前言 1. MuseGAN 1.1 Bach Chorale 数据集 1.2 MuseGAN 生成器 1.3 MuseGAN 判别器 2. MuseGAN 分析 小结 系列链接0. 前言
在基于 Transfromer 生成音乐一节,我们可以看到可视化的乐谱类似于一幅图像,因此,我们可以利用图像生成方法替代序列生成技术生成音乐。可以将音乐生成视为一个图像生成问题,这意味着可以不使用 Transformer
,而是应用在图像生成问题中表现出色的基于卷积的技术,例如生成对抗网络 (Generative Adversarial Network, GAN)。本节中,我们将解决多声部音乐生成的问题,并探讨如何使用基于 GAN
架构创建多声部音乐。
1. MuseGAN
MuseGAN
通过一种新颖的生成对抗网络 (Generative Adversarial Network
, GAN
) 框架来训练模型生成复调、多轨和多小节的音乐。此外,通过将生成器的输人噪声向量划分职责,实现对音乐的时域以及声部等高级特征进行精细控制。为了训练 MuseGAN
,我们将使用 Bach Chorale
数据集。
1.1 Bach Chorale 数据集
首先需要下载用于训练 MuseGAN
的 MIDI
文件,使用 Bach Chorale
数据集的四个声部(声部也称为音轨,tracks
),下载后将 MIDI
文件保存到本地的 ./data
文件夹中。
数据集每个时间步 (timesteps
) 由四个数字组成,分别是四个声部中每个声部 MIDI
音符的音高。在此数据集中,一个时间步等于一个 16
分音符。例如,在 4
个四分音(四拍)拍子的一个小节中,有 16
个时间步。数据集会自动分为训练集、验证集和测试集,使用训练集来训练 MuseGAN
。
首先,我们需要将数据转换成正确的形状以供 GAN
使用,我们将生成两小节的音乐,所以只提取每个声部的前两小节。每小节由 16
个时间步组成,4
个声部有 84
个音高。因此,转换后的数据形状如下:
[BATCH_SIZE, N_BARS, N_STEPS_PER_BAR, N_PITCHES, N_TRACKS]
其中:
BATCH_SIZE = 64
N_BARS = 2
N_STEPS_PER_BAR = 16
N_PITCHES = 84
N_TRACKS = 4
为了使数据转换成这种形状,我们将音高数字进行独热编码,变成长度为 84
的向量,并将每个音符序列拆分成两组,每组 16
个时间步,对应两个小节。在这里,我们假设数据集中的每个众赞歌每个小节都有四拍。下图展示了如何将两个小节的原始数据转换为训练数据集,并提供给 GAN
作为输入。
1.2 MuseGAN 生成器
和所有 GAN
一样,MuseGAN
由一个生成器和一个判别器组成。生成器试图用它的音乐作品欺骗判别器,而判别器试图阻止这种情况的发生,并确保其能够分辨出生成器伪造的 Bach Chorale
音乐和真实的 Bach Chorale
音乐之间的区别。MuseGAN
的不同之处在于,生成器不仅接受单个噪声向量作为输入,而是有四个独立的输入,对应音乐的四个不同特征:和弦、风格、旋律以及节奏。通过独立操作每个输入,我们可以改变生成音乐的高级属性,生成器的架构如下图所示。
MuseGAN
首先将和弦和旋律输入传入到时间网络 (temporal network
),该网络输出一个张量,其维度等于要生成的小节数。样式和节奏输入不会以这种方式在时间上拉伸,因为它们在整个乐曲中保持恒定。
然后,为了针对特定的音轨生成特定的小节,从和弦、风格、旋律和节奏部分提取出相关的输出,并将它们连接起来形成一个更长的向量。然后将这个向量传递给小节生成器,最终为特定声部输出特定小节。
通过将所有声部生成的小节连接起来,我们可以创建一个能够与真实乐谱进行比较的乐谱。接下来,我们首先介绍如何构建一个时间网络。
1.2.1 时间网络
时间网络 (temporal network
) 是一个由卷积转置层组成的神经网络,它的作用是将长度为 Z_DIM = 32
的单个输入噪声向量转换为每个小节的不同噪声向量(同样长度为 32
):
def conv_t(x, f, k, s, a, p, bn):
x = layers.Conv2DTranspose(
filters=f,
kernel_size=k,
padding=p,
strides=s,
kernel_initializer=initializer,
)(x)
if bn:
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.Activation(a)(x)
return x
def TemporalNetwork():
# 时间网络的输入是长度为 32 的向量 (Z_DIM)
input_layer = layers.Input(shape=(Z_DIM,), name="temporal_input")
# 将输入向量调整为一个具有 32 个通道的 1 × 1 张量,以便对其应用二维卷积转置操作
x = layers.Reshape([1, 1, Z_DIM])(input_layer)
# 应用 Conv2DTranspose 层来扩展张量的大小,使其与 N_BARS 的长度相同
x = conv_t(x, f=1024, k=(2, 1), s=(1, 1), a="relu", p="valid", bn=True)
x = conv_t(x, f=Z_DIM, k=(N_BARS - 1, 1), s=(1, 1), a="relu", p="valid", bn=True)
# 使用 Reshape 层去除不必要的额外维度
output_layer = layers.Reshape([N_BARS, Z_DIM])(x)
return models.Model(input_layer, output_layer)
我们之所以使用卷积操作而不是将两个独立的向量输入网络,是因为我们希望网络能够学习如何一致地将一个小节连接到另一个小节。使用神经网络沿时间轴扩展输入向量意味着模型有机会学习音乐如何在小节之间连接,而不是将每个小节完全视为独立的部分。
1.2.2 和弦、风格、旋律和节奏
接下来,我们介绍生成器的四个不同输入。
和弦
和弦 (Chords
) 输入是长度为 Z_DIM(32)
的单个噪声向量,这个向量的作用是控制音乐随时间的整体进展,跨音轨共享,因此我们使用一个 TemporalNetwork
为每个小节的输出一个不同潜向量。虽然我们将这个输入称为和弦,但它实际上可以控制每小节发生的所有变化,例如总体节奏风格,而不特定于任何特定的声部。
风格
风格 (Style
) 输入也是长度为 Z_DIM
的向量。它在传递过程中没有进行任何转换,因此在所有小节和音轨上都是相同的。可以将其视为控制作品整体风格的向量(即,它会一致地影响所有小节和声部)。换句话说,小节生成器应该利用这个向量在小节和声部之间建立一致性。
旋律
旋律 (Melody
) 输入是一个形状为 [N_TRACKS, Z_DIM]
的数组,即对于每个声部,我们提供长度为 Z_DIM
的随机噪声向量。
每个向量都通过一个特定于音轨的 TemporalNetwork
,其中权重在音轨之间不共享。因此,对于每小节的每个声部,输出都是长度为 Z_DIM
的向量。因此,模型可以使用这个输入向量独立调整每个小节和声部。
节奏
节奏 (Groove
) 输入也是一个形状为 [N_TRACKS, Z_DIM]
的数组,即对于每个声部,我们提供长度为 Z_DIM
的随机噪声向量。与旋律输入不同的是,节奏向量不会通过时间网络,而是直接传递给小节生成器,就像风格向量一样。因此,每个节奏向量将影响一个声部的整体属性,并延续到所有小节。与风格向量不同的是,每个声部都有不同的节奏,这意味着我们可以使用这些向量来单独调整每个声部的整体输出。MuseGAN
生成器的最后一部分是小节生成器,使用它可以将和弦、风格、旋律和节奏组件的输出连接在一起。
1.2.3 小节生成器
小节生成器接收四个潜向量——分别来自和弦、风格、旋律和节奏组件,将这些向量被连接起来,得到一个长度为 4 * Z_DIM
的向量作为输入,输出是一个声部的一个小节,即形状为 [1, n_steps_per_bar, n_pitches, 1]
的张量。
小节生成器只是一个使用卷积转置层来扩展输入向量的时间和音高维度的神经网络。我们为每个声部创建一个小节生成器,权重在声部之间不共享:
def BarGenerator():
# 小节生成器的输入是长度为 4 * Z_DIM 的向量
input_layer = layers.Input(shape=(Z_DIM * 4,), name="bar_generator_input")
# 通过 Dense 层将其传递,然后重新调整张量以准备进行卷积转置操作
x = layers.Dense(1024)(input_layer)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.Activation("relu")(x)
x = layers.Reshape([2, 1, 512])(x)
# 首先,我们沿时间步轴扩展张量
x = conv_t(x, f=512, k=(2, 1), s=(2, 1), a="relu", p="same", bn=True)
x = conv_t(x, f=256, k=(2, 1), s=(2, 1), a="relu", p="same", bn=True)
x = conv_t(x, f=256, k=(2, 1), s=(2, 1), a="relu", p="same", bn=True)
# 然后沿音高轴扩展
x = conv_t(x, f=256, k=(1, 7), s=(1, 7), a="relu", p="same", bn=True)
# 最终层应用 tanh 激活函数,因为我们将使用 WGAN-GP (需要 tanh 输出激活)来训练神经网络
x = conv_t(x, f=1, k=(1, 12), s=(1, 12), a="tanh", p="same", bn=False)
# 将张量重新调整形状后,额外添加两个大小为1的维度,以便与其他小节和声部连接
output_layer = layers.Reshape([1, N_STEPS_PER_BAR, N_PITCHES, 1])(x)
return models.Model(input_layer, output_layer)
1.2.4 构建生成器
最终,MuseGAN
生成器将四个输入噪声张量(和弦、风格、旋律和节奏)转换为多轨、多小节的乐谱:
def Generator():
# 定义生成器的输入
chords_input = layers.Input(shape=(Z_DIM,), name="chords_input")
style_input = layers.Input(shape=(Z_DIM,), name="style_input")
melody_input = layers.Input(shape=(N_TRACKS, Z_DIM), name="melody_input")
groove_input = layers.Input(shape=(N_TRACKS, Z_DIM), name="groove_input")
# 将和弦输入通过时间网络
chords_tempNetwork = TemporalNetwork()
chords_over_time = chords_tempNetwork(chords_input) # [n_bars, z_dim]
# 将旋律输入通过时间网络
melody_over_time = [None] * N_TRACKS # list of n_tracks [n_bars, z_dim] tensors
melody_tempNetwork = [None] * N_TRACKS
for track in range(N_TRACKS):
# 为每个声部创建一个独立的小节生成器网络
melody_tempNetwork[track] = TemporalNetwork()
melody_track = layers.Lambda(lambda x, track=track: x[:, track, :])(melody_input)
melody_over_time[track] = melody_tempNetwork[track](melody_track)
# 循环遍历每个声部和小节,为每组合创建一个生成的小节
barGen = [None] * N_TRACKS
for track in range(N_TRACKS):
barGen[track] = BarGenerator()
# CREATE OUTPUT FOR EVERY TRACK AND BAR
bars_output = [None] * N_BARS
c = [None] * N_BARS
for bar in range(N_BARS):
track_output = [None] * N_TRACKS
c[bar] = layers.Lambda(lambda x, bar=bar: x[:, bar, :])(chords_over_time) # [z_dim]
s = style_input # [z_dim]
for track in range(N_TRACKS):
m = layers.Lambda(lambda x, bar=bar: x[:, bar, :])(melody_over_time[track]) # [z_dim]
g = layers.Lambda(lambda x, track=track: x[:, track, :])(groove_input) # [z_dim]
z_input = layers.Concatenate(
axis=1, name="total_input_bar_{}_track_{}".format(bar, track)
)([c[bar], s, m, g])
track_output[track] = barGen[track](z_input)
bars_output[bar] = layers.Concatenate(axis=-1)(track_output)
# 将所有内容连接在一起形成一个输出张量
generator_output = layers.Concatenate(axis=1, name="concat_bars")(bars_output)
# MuseGAN 模型接受 4 个不同的噪声张量作为输入,并输出生成的多轨、多小节的乐谱
return models.Model([chords_input, style_input, melody_input, groove_input], generator_output,)
generator = Generator()
1.3 MuseGAN 判别器
与生成器相比,判别器架构更加简单。判别器试图区分由生成器创建的完整的声部、多小节乐谱和巴赫众赞歌中的真实片段。判别器是一个卷积神经网络,主要由Conv3D层组成,可以将乐谱折叠成单个输出预测。
1.3.1 Conv3D 层
Conv2D
层适用于三维输入图像(宽度、高度、通道),而在本节,模型需要使用 Conv3D
层,它类似于 Conv2D
层,但接受四维输入张量 (n_bars、n_steps_per_bar、n_pitches、n_tracks)
。同时,由于使用 WGAN-GP 框架训练 GAN
,在判别器中不使用批归一化层。
def conv(x, f, k, s, p):
x = layers.Conv3D(
filters=f,
kernel_size=k,
padding=p,
strides=s,
kernel_initializer=initializer,
)(x)
x = layers.LeakyReLU()(x)
return x
def Critic():
# 判别器的输入是一个由多声部、多小节乐谱组成的数组,每个乐谱的形状为 [N_BARS,N_STEPS_PER_BAR,N_PITCHES,N_TRACKS]
critic_input = layers.Input(shape=(N_BARS, N_STEPS_PER_BAR, N_PITCHES, N_TRACKS), name="critic_input",)
x = critic_input
# 首先,沿着小节轴压缩张量。由于我们需要处理 4D 张量,因此在判别器中需要应用 Conv3D 层
x = conv(x, f=128, k=(2, 1, 1), s=(1, 1, 1), p="valid")
x = conv(x, f=128, k=(N_BARS - 1, 1, 1), s=(1, 1, 1), p="valid")
# 接下来,沿音高轴压缩张量
x = conv(x, f=128, k=(1, 1, 12), s=(1, 1, 12), p="same")
x = conv(x, f=128, k=(1, 1, 7), s=(1, 1, 7), p="same")
# 最后,沿时间步轴压缩张量
x = conv(x, f=128, k=(1, 2, 1), s=(1, 2, 1), p="same")
x = conv(x, f=128, k=(1, 2, 1), s=(1, 2, 1), p="same")
x = conv(x, f=256, k=(1, 4, 1), s=(1, 2, 1), p="same")
x = conv(x, f=512, k=(1, 3, 1), s=(1, 2, 1), p="same")
x = layers.Flatten()(x)
x = layers.Dense(1024, kernel_initializer=initializer)(x)
x = layers.LeakyReLU()(x)
# 根据 WGAN-GP 框架,输出是拥有一个单元且没有激活函数的 Dense 层
critic_output = layers.Dense(
1, activation=None, kernel_initializer=initializer
)(x)
return models.Model(critic_input, critic_output)
critic = Critic()
2. MuseGAN 分析
我们可以通过生成一个乐谱,然后调整一些输入噪声参数来观察输出的效果。
生成器的输出是一个数组,值在范围 [-1, 1]
之间(由于最后一层使用tanh激活函数)。为了将这个数组转换成对应于每个声部的音符,我们选择每个时间步长上 84
个音高中值最大的音符。在原始 MuseGAN
模型中使用阈值为 0
,因为每个声部可以包含多个音符;但在本节中,我们可以简单地取最大值,以确保每个声部在每个时间步长只有一个音符。
下图中展示了一个由模型从随机正态分布的噪声向量生成的乐谱(左上角)。使用欧氏距离找到数据集中最接近的乐谱,并检查生成的乐谱是否是数据集中已经存在的音乐的复制品,图下方展示了与之最接近的乐谱,我们可以看到它与生成的乐谱不相同。
通过修改输入噪声来调整生成的乐谱。首先,我们可以尝试改变和弦噪声向量-上图中左下角的乐谱显示了结果。我们可以看到每个声部都按照预期发生了变化,而且两个小节表现出不同的特性。在第 2
小节中,最下面一行更加动态,并且最上面—行的音高比第 1
小节更高,顶部音线的音高比第一小节更高,这是因为影响两个小节的潜向量是不同的,因为将输入和弦向量通过时间网络传递。
改变风格向量(右上角)时,两个小节以类似的方式发生变化。两小节之间的风格没有太大区别,但已与原来生成的乐谱有了很大不同(即,使用相同的潜向量来调整所有轨道和小节)。
我们还可以通过旋律和节奏输入单独改变声部。在上图的乐谱中,我们可以看到仅改变最上面一行旋律噪声输入的效果。所有其他部分保持不变,但最上面的音符发生了显著变化。此外,在最上面一行的两个小节之间可以看到节奏变化:第 2
个小节比第 1
个小节包含更快的音符,更具动态性。
最后,在图示中右下角的乐谱显示了当我们仅改变最下面一行的节奏输入参数时的预测乐谱。所有其他部分均保持不变,仅最下面一行不同。此外,最下面一行的总体模式在各个小节之间保持相似,与我们所期望的相符。
我们已经分析了每个输入参数如何直接影响生成的音乐序列的高级特征,这与调整变分自编码器 (Variational Autoencoder, VAE) 和 GAN
的潜向量以改变生成图像一样。该模型的一个缺点是必须事先指定要生成的小节数量。为了解决这个问题,扩展 MuseGAN
将先前的小节作为输入传递给模型,并通过不断将最近预测的小节作为额外输入反馈到模型中,从而生成更长的乐谱。
小结
MuseGAN
使用卷积来生成具有多个声部的多声部乐谱,可以将乐谱视为图像,其中声部类似于图像的各个通道。MuseGAN
的创新之处在于四个输入噪声向量(和弦、风格、旋律和节奏)的组织方式,以便可以对音乐的高级特征进行完全控制。
系列链接
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)
AIGC实战——基于Transformer实现音乐生成