Magenta-音乐生成实用指南-全-

Magenta 音乐生成实用指南(全)

原文:annas-archive.org/md5/5d28aa20168e891a3113f761b50d6ecc

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

由于近年来该领域的进展,机器学习在艺术中的地位日益牢固。Magenta 处于这一创新的前沿。本书提供了关于音乐生成的机器学习模型的实用方法,并演示如何将其集成到现有的音乐制作工作流程中。通过实践例子和理论背景的解释,本书是探索音乐生成的完美起点。

在《使用 Magenta 进行音乐生成实战》一书中,你将学习如何使用 Magenta 中的模型生成打击乐序列、单音和多音旋律的 MIDI 文件以及原始音频中的乐器音效。我们将看到大量的实践例子,并深入解释机器学习模型,如递归神经网络RNNs)、变分自编码器VAEs)和生成对抗网络GANs)。借助这些知识,我们将创建并训练自己的模型,处理高级音乐生成的用例,并准备新的数据集。最后,我们还将探讨如何将 Magenta 与其他技术集成,如数字音频工作站DAWs),以及使用 Magenta.js 在浏览器中分发音乐生成应用程序。

到本书结束时,你将掌握 Magenta 的所有功能,并具备足够的知识来用自己的风格进行音乐生成。

本书适合谁阅读

本书将吸引既有技术倾向的艺术家,也有音乐倾向的计算机科学家。它面向任何想要获取关于构建使用深度学习的生成音乐应用程序的实际知识的读者。它不要求你具备任何音乐或技术上的专业能力,除了对 Python 编程语言的基本知识。

本书涵盖内容

第一章,Magenta 与生成艺术简介,将向你展示生成音乐的基础知识以及现有的生成艺术。你将了解生成艺术的新技术,如机器学习,以及这些技术如何应用于创作音乐和艺术。还将介绍 Google 的 Magenta 开源研究平台,以及 Google 的开源机器学习平台 TensorFlow,概述其不同部分,并指导本书所需软件的安装。最后,我们将在命令行生成一个简单的 MIDI 文件来完成安装。

第二章,使用 Drums RNN 生成鼓序列,将向你展示许多人认为是音乐基础的内容——打击乐。我们将展示 RNN 在音乐生成中的重要性。然后,你将学习如何使用预训练的鼓组模型,通过在命令行窗口和 Python 中直接调用它,来生成鼓序列。我们将介绍不同的模型参数,包括模型的 MIDI 编码,并展示如何解读模型的输出。

第三章,生成复调旋律,将展示长短期记忆LSTM)网络在生成较长序列中的重要性。我们将学习如何使用一个单旋律的 Magenta 模型——旋律 RNN,它是一个带有回馈和注意力机制的 LSTM 网络。你还将学习使用两个复调模型——复调 RNN 和表演 RNN,它们都是使用特定编码的 LSTM 网络,其中后者支持音符的力度和表达性时间。

第四章,使用 MusicVAE 进行潜在空间插值,将展示 VAE 的连续潜在空间在音乐生成中的重要性,并与标准自编码器AEs)进行比较。我们将使用 Magenta 中的 MusicVAE 模型,一个分层的递归 VAE,从中采样序列,然后在它们之间进行插值,平滑地从一个序列过渡到另一个序列。接着,我们将看到如何使用 GrooVAE 模型为现有的序列添加节奏感或人性化效果。最后,我们将查看用于构建 VAE 模型的 TensorFlow 代码。

第五章,使用 NSynth 和 GANSynth 进行音频生成,将展示音频生成。我们首先提供 WaveNet 的概述,这是一个现有的音频生成模型,尤其在文本转语音应用中高效。在 Magenta 中,我们将使用 NSynth,一个基于 WaveNet 的自编码器模型,来生成小的音频片段,这些片段可以作为伴奏 MIDI 曲谱的乐器。NSynth 还支持音频转换,如缩放、时间拉伸和插值。我们还将使用 GANSynth,一种基于 GAN 的更快速方法。

第六章,训练数据准备,将展示为什么训练我们自己的模型至关重要,因为它可以让我们生成特定风格的音乐、生成特定的结构或乐器。构建和准备数据集是训练我们自己模型的第一步。为此,我们首先查看现有的数据集和 API,帮助我们找到有意义的数据。然后,我们为特定风格(舞曲和爵士)构建两个 MIDI 数据集。最后,我们使用数据转换和管道准备 MIDI 文件以进行训练。

第七章,训练 Magenta 模型,将展示如何调整超参数,如批量大小、学习率和网络大小,以优化网络性能和训练时间。我们还将展示常见的训练问题,如过拟合和模型无法收敛。一旦模型的训练完成,我们将展示如何使用训练好的模型生成新的序列。最后,我们将展示如何使用 Google Cloud Platform 在云端更快地训练模型。

第八章,在浏览器中使用 Magenta.js 展示 Magenta,将展示 Magenta 的 JavaScript 实现,Magenta 因其易用性而广受欢迎,因为它运行在浏览器中,并且可以作为网页共享。我们将介绍 Magenta.js 所依赖的技术 TensorFlow.js,并展示 Magenta.js 中可用的模型,包括如何转换我们之前训练过的模型。接着,我们将使用 GANSynth 和 MusicVAE 创建小型 Web 应用,分别用于音频和序列的采样。最后,我们将看到 Magenta.js 如何与其他应用互动,使用 Web MIDI API 和 Node.js。

第九章,使 Magenta 与音乐应用互动,将展示 Magenta 如何在更广阔的背景下运作,展示如何使其与其他音乐应用(如数字音频工作站(DAW)和合成器)互动。我们将解释如何通过 MIDI 接口将 MIDI 序列从 Magenta 发送到 FluidSynth 和 DAW。通过这样做,我们将学习如何在所有平台上处理 MIDI 端口,以及如何在 Magenta 中循环 MIDI 序列。我们将展示如何使用 MIDI 时钟和传输信息来同步多个应用。最后,我们将介绍 Magenta Studio,这是一种基于 Magenta.js 的独立打包版本,也可以作为插件集成到 Ableton Live 中。

为了充分利用本书

本书不要求具备任何关于音乐或机器学习的特定知识,因为我们将在整本书中覆盖这两个主题的所有技术细节。然而,我们假设你具备一定的 Python 编程知识。我们提供的代码都有详细的注释和解释,方便新手使用和理解。

提供的代码和内容适用于所有平台,包括 Linux、macOS 和 Windows。我们将在过程中设置开发环境,因此在开始之前不需要任何特定的设置。如果你已经在使用集成开发环境IDE)和 DAW,你可以在本书的学习过程中继续使用它们。

下载示例代码文件

你可以从www.packt.com上的账户下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

您可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:

  • Windows 版本的 WinRAR/7-Zip

  • Mac 版本的 Zipeg/iZip/UnRarX

  • Linux 版本的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Hands-On-Music-Generation-with-Magenta。如果代码有任何更新,GitHub 上的现有仓库将进行更新。

我们还提供了来自我们丰富书籍和视频目录中的其他代码包,您可以访问github.com/PacktPublishing/进行查看!

下载彩色图片

我们还提供了一个 PDF 文件,包含本书中使用的屏幕截图/图表的彩色图像。您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781838824419_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。举个例子:“每次调用 step 操作时,RNN 需要更新其状态,即隐藏向量h。”

代码块格式如下所示:

import os
import magenta.music as mm

mm.notebook_utils.download_bundle("drum_kit_rnn.mag", "bundles")
bundle = mm.sequence_generator_bundle.read_bundle_file(
    os.path.join("bundles", "drum_kit_rnn.mag"))

当我们希望您注意代码块中特定部分时,相关的行或项目会以粗体显示:

> drums_rnn_generate --helpfull

    USAGE: drums_rnn_generate [flags]
    ...

magenta.models.drums_rnn.drums_rnn_config_flags:
    ...

magenta.models.drums_rnn.drums_rnn_generate:
    ...

任何命令行输入或输出格式如下所示:

> drums_rnn_generate --bundle_file=bundles/drum_kit_rnn.mag --output_dir output

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词汇会以这种方式出现在文本中。举个例子:“坚持使用bar的主要原因是为了遵循 Magenta 的代码约定,其中 bar 比measure更为一致地使用。”

警告或重要提示将以这种方式显示。

提示和技巧如下所示。

动作中的代码

访问以下链接查看代码运行的视频:

bit.ly/2uHplI4

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何内容有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com联系我们。

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误还是难免会发生。如果您在本书中发现了错误,我们将不胜感激您能向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误表提交表格链接,并填写详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请您提供其位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 向我们发送链接。

如果您有意成为作者:如果您在某个专题有专业知识,并且有意撰写或为书籍贡献,请访问 authors.packtpub.com

评论

请留下您的评论。一旦您阅读并使用了本书,为什么不在购买网站上留下您的评论呢?潜在读者可以看到并使用您的客观意见来做出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!

欲了解更多 Packt 的信息,请访问 packt.com

第一章:艺术作品生成简介

本节介绍了艺术作品生成以及机器学习在这一领域的应用,并全面概述了 Magenta 和 TensorFlow。我们将探讨用于音乐生成的不同模型,并解释这些模型为何如此重要。

本节包含以下章节:

  • 第一章,Magenta 与生成艺术简介

第二章:Magenta 和生成艺术简介

在本章中,您将学习生成音乐的基础知识以及现有的相关技术。您将了解生成艺术的新技术,例如机器学习,以及如何将这些技术应用于音乐和艺术创作。我们将介绍 Google 的 Magenta 开源研究平台,Google 的开源机器学习平台 TensorFlow,并概述其不同的部分,以及为本书安装所需的软件。最后,我们将通过命令行生成一个简单的 MIDI 文件来完成安装。

本章将涵盖以下主题:

  • 生成艺术概述

  • 机器学习的新技术

  • Magenta 和 TensorFlow 在音乐生成中的应用

  • 安装 Magenta

  • 安装音乐软件和合成器

  • 安装代码编辑软件

  • 生成基本的 MIDI 文件

技术要求

在本章中,我们将使用以下工具:

  • PythonCondapip,用于安装和执行 Magenta 环境

  • Magenta,通过执行音乐生成来测试我们的设置

  • Magenta GPU可选)、CUDA 驱动程序和 cuDNN 驱动程序,使 Magenta 在 GPU 上运行

  • FluidSynth,通过软件合成器收听生成的音乐样本

  • 本书中可能会使用的其他可选软件,例如用于音频编辑的 Audacity,用于乐谱编辑的 MuseScore,以及用于代码编辑的 Jupyter Notebook

强烈建议在阅读本书章节时,遵循书中的源代码。源代码还提供了有用的脚本和技巧。按照以下步骤,在您的用户目录中查看代码(如果您愿意,也可以选择其他位置):

  1. 首先,您需要安装 Git,可以通过下载并执行 git-scm.com/downloads 上的安装程序,在任何平台上安装。然后,按照提示操作,确保将该程序添加到您的 PATH 中,以便在命令行上使用。

  2. 然后,打开新的终端并执行以下命令,克隆源代码库:

> git clone https://github.com/PacktPublishing/hands-on-music-generation-with-magenta
> cd hands-on-music-generation-with-magenta

每个章节都有自己的文件夹;Chapter01Chapter02 等。例如,本章的代码位于 github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter01。示例和代码片段将位于本章的文件夹中。在开始之前,您应该打开 cd Chapter01

我们不会使用很多 Git 命令,除了 git clone,该命令将代码库复制到您的机器上,但如果您不熟悉 Git 并想学习更多内容,一个很好的起点是优秀的 Git 书籍 (git-scm.com/book/en/v2),它支持多种语言。

查看以下视频,观看代码实践:

bit.ly/2O847tW

生成艺术概览

生成艺术一词是在计算机出现后被创造的,早期的计算机科学中,艺术家和科学家们使用技术作为工具来创作艺术。有趣的是,生成艺术早于计算机的出现,因为生成系统本来可以手工制作。

在本节中,我们将通过展示一些来自艺术史的有趣例子,概述生成音乐,例子可以追溯到 18 世纪。这将帮助你通过具体的示例了解不同类型的生成音乐,并为后续章节的学习奠定基础。

钢笔和纸上的生成音乐

人类历史上有很多生成艺术的例子。一个流行的例子可以追溯到 18 世纪,当时一款名为 Musikalisches Würfelspiel(德语为音乐骰子游戏)的游戏在欧洲广受欢迎。该游戏的概念由尼古劳斯·辛梅罗克于 1792 年归功于莫扎特,尽管没有证实这确实是他的创作。

游戏的玩家掷骰子,并从结果中选择一个预定义的 272 个音乐乐句中的一个。通过反复掷骰,玩家可以创作出一整分钟(该游戏生成的音乐类型),并且遵循该音乐类型的规则,因为它是通过这样的方式创作的,生成的音乐编排听起来非常和谐。

在下面的表格和随后的图片中,可以看到一个音乐骰子游戏的小部分。在表格中,y 轴代表骰子投掷结果,而 x 轴代表你当前生成的乐句。玩家将掷两颗骰子 16 次:

  1. 在两颗骰子的第一次掷骰中,我们读取第一列。两个骰子的总和为二时,输出的是第 96 号乐句(第一行),两个骰子的总和为二时,输出的是第 32 号乐句(第二行),以此类推。

  2. 在两颗骰子的第二次掷骰中,我们读取第二列。两个骰子的总和为二时,输出的是第 22 号乐句(第一行),总和为三时,输出的是第 6 号乐句(第二行),以此类推。

经过 16 次投掷后,游戏将为该指标输出 16 个乐句:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
2 96 22 141 41 105 122 11 30 70 121 26 9 112 49 109 14
3 32 6 128 63 146 46 134 81 117 39 126 56 174 18 116 83
4 69 95 158 13 153 55 110 24 66 139 15 132 73 58 145 79
5 40 17 113 85 161 2 159 100 90 176 7 34 67 160 52 170
6 148 74 163 45 80 97 36 107 25 143 64 125 76 136 1 93
7 104 157 27 167 154 68 118 91 138 71 150 29 101 162 23 151
8 152 60 171 53 99 133 21 127 16 155 57 175 43 168 89 172
9 119 84 114 50 140 86 169 94 120 88 48 166 51 115 72 111
10 98 142 42 156 75 129 62 123 65 77 19 82 137 38 149 8
11 3 87 165 61 135 47 147 33 102 4 31 164 144 59 173 78
12 54 130 10 103 28 37 106 5 35 20 108 92 12 124 44 131

上表显示了整个乐谱的一小部分,每一小节都有索引标注。对于每一个生成的 16 个索引,我们按顺序选取相应的小节,这就构成了我们的华尔兹舞曲(华尔兹是这个游戏生成的风格——基本上,它是一首遵循特定规则的音乐乐谱)。

生成属性有不同的类型:

  • 偶然性或随机性,例如骰子游戏就是一个很好的例子,其中生成的艺术结果部分或完全由偶然性定义。有趣的是,向艺术过程中添加随机性通常被认为是人性化这一过程,因为一个底层的刚性算法可能会生成一些听起来人造的东西。

  • 算法生成(或基于规则的生成),在这种情况下,生成的规则将定义其结果。这种生成的典型例子包括像著名的康威生命游戏这样的细胞自动机,这是一种每次迭代时,网格中的细胞根据预定规则变化的游戏:每个细胞可能是开或关,邻近的细胞根据网格的状态和规则进行更新。这种生成的结果是完全确定的;其中不涉及随机性或概率。

  • 基于随机过程的生成,其中序列由元素的概率派生。一个例子是马尔科夫链,这是一种随机模型,其中序列的每个元素的结果概率仅由当前系统状态定义。另一个很好的基于随机过程的生成例子是机器学习生成,这将在本书中多次涉及。

在本书中,我们将采用生成艺术的简单定义:

“生成艺术是一种部分或完全由自主系统创作的艺术作品”

到现在为止,你应该明白我们实际上并不需要计算机来生成艺术,因为一个系统的规则可以手工推导出来。但使用计算机使得定义复杂的规则并处理大量数据成为可能,正如我们在接下来的章节中将看到的。

计算机生成的音乐

计算机生成艺术的首次实例可以追溯到 1957 年,当时使用马尔科夫链在电子计算机 ILLIAC I 上生成了一个乐谱,由作曲家 Lejaren Hiller 和 Leonard Issacson 创作。他们的论文《使用高速数字计算机的音乐创作》描述了用于创作音乐的技术。这部名为《Illac Suite》的作品由四个乐章组成,每个乐章都探索了一种特定的音乐生成技巧,从基于规则的cantus firmus生成到使用马尔科夫链的随机生成。

随后出现了许多著名的生成作曲实例,例如 1962 年泽纳基斯的Atrées,它探索了随机作曲的理念;埃布齐奥格罗(Ebcioglo)的作曲软件 CHORAL,内含手工编写的规则;以及大卫·科普(David Cope)的 EMI 软件,它扩展了这一概念,能够从大量乐谱中学习。

截至今天,生成音乐无处不在。许多工具允许音乐家基于我们之前描述的生成技术创作原创音乐。一个名为算法派对(algorave)的全新音乐流派和音乐社区就是由这些技术衍生出来的。源于地下电子音乐场景,音乐家们使用生成算法和软件在舞台上实时创作舞曲,因此这个流派被命名为“算法派对”。如TidalCyclesOrca等软件允许音乐家即时定义规则,让系统自主生成音乐。

回顾这些技术,随机模型(如马尔可夫链)已被广泛应用于生成音乐。这是因为它们在概念上简单,容易表示,因为该模型是一个转移概率表,并且可以从少量实例中学习。马尔可夫模型的问题在于表示长期时间结构非常困难,因为大多数模型只会考虑前 n个状态,其中n是一个较小的数字,用以定义结果的概率。让我们看看有哪些其他类型的模型可以用来生成音乐。

在 2012 年的一篇题为关于生成计算机艺术的十个问题的论文中,作者讨论了机器创作的可能性、人类美学的形式化以及随机性。更重要的是,它定义了此类系统的局限性。生成系统能够产生什么?机器只能做它们被指示去做的事吗?

机器学习的新技术

机器学习对于计算机科学至关重要,因为它可以在无需明确编写的情况下对复杂功能进行建模。这些模型是通过从实例中自动学习得到的,而不是手动定义的。这对艺术领域有着巨大的意义,因为明确编写绘画或音乐乐谱的规则本身就非常困难。

近年来,深度学习的出现将机器学习推向了新的高度,尤其在效率方面。深度学习对于我们音乐生成的应用尤为重要,因为使用深度学习技术不需要像传统机器学习那样进行特征提取的预处理步骤,而特征提取在处理图像、文本以及——你猜到了——音频等原始数据时非常困难。换句话说,传统的机器学习算法在音乐生成中并不适用。因此,本书中所有的网络都会是深度神经网络。

在本节中,我们将学习深度学习的进展如何推动音乐生成,并介绍本书中将要使用的概念。我们还将探讨这些算法的不同类型的音乐表示方法,这一点很重要,因为它将为本书中的数据处理提供基础。

深度学习的进展

我们都知道,深度学习最近已经成为计算机科学中一个快速发展的领域。就在不久前,没有任何深度学习算法能够超越传统技术。那是在 2012 年之前,当时第一次有一个深度学习算法——AlexNet,通过使用在 GPU 上训练的深度神经网络,在图像分类比赛中表现得更好(有关 AlexNet 论文的详细信息,请参见进一步阅读部分,它是计算机视觉领域最具影响力的论文之一)。神经网络技术已经有超过 30 年的历史,但其近期的复兴可以通过大数据的可用性、高效的计算能力以及技术进步来解释。

最重要的是,深度学习技术是通用的,即与我们之前指定的音乐生成技术不同,机器学习系统是与音乐类型无关的,可以从任意的音乐语料库中学习。我们将在本书中看到同一系统可以用于多种音乐风格,例如在第六章中,我们会训练一个现有模型来处理爵士乐,训练数据准备

深度学习中的许多技术早在很久以前就被发现了,但直到今天才找到了有意义的应用。在与音乐生成相关的技术进展方面,这些技术在 Magenta 中得到了应用,并将在本书后面进行解释:

  • 递归神经网络RNNs)在音乐生成中非常有趣,因为它们允许我们对输入和输出的向量序列进行操作。当使用经典神经网络或卷积网络(这些网络用于图像分类)时,你只能使用固定大小的输入向量来产生固定大小的输出向量,这对于音乐处理来说非常有限,但在某些类型的图像处理上效果良好。RNN 的另一个优势是每次迭代时可以通过将一个函数与前一个状态向量结合来生成新的状态向量,这是一种描述复杂行为和长期状态的强大手段。在第二章中,我们将讨论 RNNs,用 Drums RNN 生成鼓点序列

  • 长短期记忆LSTM)是一个具有稍微不同特性的 RNN。它解决了 RNN 中存在的梯度消失问题,这使得网络即使在理论上可以学习长期依赖关系,也无法做到。Douglas Eck 和 Jurgen Schmidhuber 于 2002 年在一篇名为《在音乐中寻找时间结构:基于 LSTM 递归网络的蓝调即兴演奏》的论文中提出了 LSTM 在音乐生成中的应用。我们将在第三章《生成复调旋律》中讨论 LSTM。

  • 变分自编码器VAE)类似于经典的自编码器,其架构相似,包含编码器(将输入转换为隐藏层)、解码器(将隐藏层转换为输出)以及损失函数,模型通过特定的约束学习重建原始输入。VAE 在生成模型中的应用较为新颖,但已显示出有趣的结果。我们将在第四章《基于音乐 VAE 的潜在空间插值》中讨论 VAE。

  • 生成对抗网络GANs)是一类机器学习系统,其中两个神经网络相互竞争:一个生成网络生成候选项,而一个判别网络对其进行评估。我们将在第五章《使用 NSynth 和 GANSynth 进行音频生成》中讨论 GAN。

最近的深度学习进展不仅深刻改变了音乐生成,还影响了音乐风格分类、音频转录、音符检测、作曲等领域。我们在这里不会讨论这些主题,但它们都有一个共同点:音乐表示。

音乐处理过程中的表示方式

这些系统可以使用不同的表示方式:

  • 符号表示,例如MIDI音乐仪器数字接口MIDI)),通过包含音乐音符和节奏的符号描述音乐,但不涉及实际声音的音质或音色。一般来说,乐谱是这一表示方式的典型例子。音乐的符号表示本身没有声音;它必须通过乐器演奏出来。

  • 子符号表示,例如原始音频波形或频谱图,描述了音乐的实际声音。

不同的处理过程需要不同的表示方式。例如,大多数语音识别和合成模型使用频谱图,而本书中大部分示例使用 MIDI 来生成音乐乐谱。集成这两种表示方式的过程比较少见,但一个例子可能是乐谱转录,它将音频文件转换为 MIDI 或其他符号表示。

使用 MIDI 表示音乐

还有其他符号化表示法,如 MusicXML 和 AbcNotation,但 MIDI 无疑是最常见的表示方式。MIDI 规范本身也充当了一种协议,因为它被用来传输音符消息,这些消息可以用于实时演奏,也可以用于控制消息。

让我们考虑一下在本书中会用到的一些 MIDI 消息的部分:

  • 通道 [0-15]:这表示消息发送的轨道

  • 音符编号 [0-127]:这表示音符的音高

  • 速度 [0-127]:这表示音符的音量

要在 MIDI 中表示一个音乐音符,你必须发送两种不同的消息类型,并且确保适当的时间顺序:一个Note On事件,紧接着一个Note Off事件。这隐含了音符的时长,但 MIDI 消息中并未包含此信息。之所以如此重要,是因为 MIDI 最初是为了实时演奏定义的,所以使用两条消息——一条用于按键,另一条用于释放按键——是合理的。

从数据角度来看,我们要么需要将 MIDI 音符转换为一种编码了音符时长的格式,要么保持按键和释放按键的方式,具体取决于我们想做什么。对于 Magenta 中的每个模型,我们将看到 MIDI 音符是如何被编码的。

下图显示了生成的鼓文件的 MIDI 表示,它以时间和音高的图形形式展示。每个 MIDI 音符由一个矩形表示。由于打击乐数据的特点,所有音符的时长相同("note on"后跟"note off"消息),但通常情况下,这个时长可能会有所不同。由于鼓文件本质上是复音的,这意味着多个音符可以同时演奏。我们将在接下来的章节中讨论单音和复音。

请注意,横坐标以秒为单位表示,但通常也可以用小节(bars)来表示。该图中未显示 MIDI 通道:

绘制生成的 MIDI 文件的脚本可以在本章节的 GitHub 代码中的Chapter01/provided文件夹里找到。这个脚本的名字叫做midi2plot.py

在音乐生成的过程中,目前大多数深度学习系统使用符号化表示法,Magenta 也采用了这种方式。这背后有几个原因:

  • 用符号数据表示音乐的本质,特别是在作曲和和声方面,是更为简便的。

  • 处理这两种表示方式并使用深度学习网络进行训练是类似的,因此选择其中一种表示方式,归根结底取决于哪一种更快、更方便。一个很好的例子是,WaveNet 音频生成网络也有一个 MIDI 实现,称为 MidiNet 符号生成网络。

我们会看到,MIDI 格式并未直接被 Magenta 使用,而是被转换为NoteSequence,这是一种协议缓冲区Protobuf)实现的音乐结构,之后由 TensorFlow 使用。这一过程对最终用户是透明的,因为输入和输出数据始终是 MIDI 格式。NoteSequence的实现之所以有用,是因为它实现了一种数据格式,可以供模型进行训练。例如,在NoteSequence中的Note有一个长度属性,而不是使用两个消息来定义音符的长度。我们将在接下来的讲解中详细说明NoteSequence的实现。

将音乐表示为波形

音频波形是一个显示振幅随时间变化的图表。从远距离看,波形看起来相当简单和平滑,但放大后,我们可以看到微小的变化——这些变化代表了声音。

为了说明波形是如何工作的,假设扬声器锥体在振幅为 0 时处于静止状态。如果振幅变化到负值 1,例如,扬声器就会向后移动一点,或者在振幅为正值时,扬声器则会向前移动。每次振幅变化时,扬声器都会移动,进而推动空气流动,从而使耳膜发生震动。

波形中的振幅越大,扬声器锥体的位移距离就越大,声音也就越响。这用分贝dB)表示,是声音压力的度量。

运动越快,音高越高。这用赫兹Hz)表示。

在以下的图像中,我们可以看到上一部分的 MIDI 文件由乐器演奏,并制作成 WAV 录音。使用的乐器是 1982 年款的 Roland TR-808 鼓样本包。你可以通过图像大致匹配一些乐器,例如,在大约 4.5 秒时,看到双倍 Conga Mid(MIDI 音符 48)。在右上角,你可以看到一个缩放的波形图,精确到百分之一秒,用来显示实际的振幅变化:

绘制 WAV 文件的脚本可以在本章的 GitHub 代码中找到,位于Chapter01/provided文件夹中。该脚本名为wav2plot.py

在机器学习中,使用原始音频波形作为数据源曾经并不常见,因为与其他转换过的表示方式相比,原始波形的计算负担更大,无论是在内存使用还是处理速度上。但随着该领域的最新进展,例如 WaveNet 模型,原始波形与其他音频表示方法(如频谱图)已不相上下,后者在机器学习算法中,尤其是在语音识别和合成中,历史上更为流行。

请记住,对音频进行训练是非常昂贵的,因为原始音频是一种密集的媒介。基本上,波形是动态电压随时间变化的数字重建。简单来说,一个叫做脉冲编码调制PCM)的过程为你所运行的采样率下的每个样本分配一个比特值。录音用的采样率相当标准:44,100 Hz,这就是所谓的奈奎斯特频率。但是你并不总是需要 44,100 Hz 的采样率;例如,16,000 Hz 就足够覆盖人类语音频率了。在这个频率下,音频的第一秒由 16,000 个样本表示。

如果你想了解更多关于 PCM、音频采样理论以及奈奎斯特频率的内容,请查看本章末尾的进一步阅读部分。

选择这个频率是有特定目的的。得益于奈奎斯特定理,它可以让我们在不丢失人耳能够听到的声音的情况下重建原始音频。

人耳能够听到的声音频率最高为 20,000 Hz,因此你需要 40,000 Hz 来表示它,因为你需要一个负值和一个正值来产生声音(参见本小节开头的解释)。然后,你可以为非常低和非常高频率的舍入误差加上 4,100 Hz,得到 44,100 Hz。

这是一个很好的采样(离散)表示的例子,它可以被反转回原始的连续表示,因为人耳能听到的音高范围是有限的。

我们将在第五章中更详细地探讨音频表示问题,使用 NSynth 和 GANSynth 进行音频生成,因为我们将使用 NSynth,这是一个 Wavenet 模型,用于生成音频样本。

用频谱图表示音乐

从历史上看,频谱图一直是机器学习处理中常用的音频表示方式,原因有两个——它既紧凑又容易从中提取特征。为了说明这一点,假设我们有上一节中的原始音频流,并将其切割成每段 1/50 秒(20 毫秒)的块进行处理。现在,你将得到 882 个样本的音频块,这些样本很难表示;它们是一个混合的振幅集合,实际上并没有代表任何东西。

频谱图是对音频流进行傅里叶变换的结果。傅里叶变换将一个信号(时间的函数)分解为其组成频率。对于音频信号,这为我们提供了每个频带的强度,其中频带是整个频谱的小块,例如 50 Hz。在对我们之前的例子进行傅里叶变换并取 882 个样本中的第 1 个样本后,我们将得到每个频带的强度:

  • [0 Hz - 50 Hz]:a[1]

  • [50 Hz - 100 Hz]:a[2]

  • ...

  • [22000 Hz - 22050 Hz]:a[n]

你将为每个 50 Hz 到 22,050 Hz 的频带得到强度 [a[1], a[2], ..., a[n]],作为 y 轴,并为不同强度分配颜色谱。通过对每 20 毫秒的时间段重复这个过程,直到覆盖整个音频,便得到一个频谱图。频谱图的有趣之处在于,你可以实际看到音乐的内容。如果演奏了 C 大调和弦,你将在频谱图中看到 C、E 和 G 出现,并在对应的频率上显示。

以下频谱图是从上一节的波形生成的。从中,你可以清楚地看到 TR 808 在给定 MIDI 文件中播放的频率。你应该能够在视觉上将上一节的波形与频谱图对应起来:

绘制 WAV 文件频谱图的脚本可以在本章 GitHub 代码的 Chapter01/provided 文件夹中找到,脚本名为 wav2spectrogram.py

频谱图主要用于语音识别,也应用于语音合成:首先,训练一个与文本对齐的频谱图模型,之后该模型便能生成与给定文本相对应的频谱图。Griffin-Lim 算法用于从频谱图中恢复音频信号。

本书中不会使用频谱图,但了解其工作原理和应用领域非常重要,因为它们被广泛应用于许多场景中。

趣味小知识:音乐家们有时会在音乐中隐藏图片,这些图片在查看音频的频谱图时会显现出来。一个著名的例子是 Aphex Twin 的Windowlicker专辑,他在第二首曲目中嵌入了自己笑脸的图像。

到目前为止,我们已经了解了在音乐生成中哪些深度学习技术进展是重要的,并学习了这些算法中的音乐表示。这两个主题很重要,因为我们将在本书中持续关注它们。

在下一节中,我们将介绍 Magenta,在这里你会看到本节的许多内容得以应用。

谷歌的 Magenta 和 TensorFlow 在音乐生成中的应用

自推出以来,TensorFlow 因其面向所有人的开源机器学习框架而成为数据科学家社区的重要工具。基于 TensorFlow 的 Magenta 也可以这样看待:即使它采用了最先进的机器学习技术,仍然可以被任何人使用。无论是音乐家还是计算机科学家,都可以安装它,并在短时间内创作新音乐。

在这一节中,我们将通过介绍 Magenta 的功能和限制,来深入探讨它的内容,并参考章节内容以获得更详细的解释。

创建一个音乐生成系统

Magenta 是一个艺术生成框架,但它也涉及注意力机制、讲故事和生成音乐的评估。随着本书的推进,我们将看到并理解这些元素在令人愉悦的音乐生成中的重要性。

评估和解释生成模型本质上是困难的,尤其是对于音频。机器学习中一个常见的标准是平均对数似然(average log-likelihood),它计算生成的样本与训练数据的偏差,这可能给你提供两个元素的接近度,但无法体现生成音乐的音乐性。

即使 GAN 在这种评估中有了很大的进展,我们往往只能依靠我们的耳朵来进行评估。我们也可以想象一个音乐作品的图灵测试:将一首作品演奏给观众,观众需要判断这首作品是否是由计算机生成的。

我们将使用 Magenta 来实现两个不同的目的:辅助和自主音乐创作:

  • 辅助音乐系统有助于作曲过程。这类系统的例子包括 Magenta 界面 magenta_midi.py,在这里,音乐家可以输入一个 MIDI 序列,Magenta 会生成一个基于提供序列的音乐灵感。此类系统可以与传统系统一起使用,帮助作曲并获得新的灵感。我们将在第九章《让 Magenta 与音乐应用互动》中讨论这一点,Magenta Studio 可以集成到传统的音乐制作工具中。

  • 自主音乐系统能在没有操作员输入的情况下持续生成音乐。在本书的最后,你将掌握构建一个自主音乐生成系统所需的所有工具,这个系统由 Magenta 的各种构建模块组成。

查看 Magenta 的内容

回顾上一节的内容,音乐可以通过多种方式表示:符号数据、谱图数据和原始音频数据。Magenta 主要处理符号数据,这意味着我们将主要关注音乐中的基础乐谱,而不是直接处理音频。让我们逐个模型地了解 Magenta 的内容。

区分模型、配置和预训练模型

在 Magenta 和本书中,模型一词指的是一个特定任务的深度神经网络。例如,Drums RNN 模型是一个带有注意力配置的 LSTM 网络,而 MusicVAE 模型是一个变分自编码器网络。Melody RNN 模型也是一个 LSTM 网络,但它专门用于生成旋律,而不是打击乐模式。

每个模型都有不同的 配置,这些配置会改变数据如何被编码进网络以及网络的配置方式。例如,Drums RNN 模型有一个 one_drum 配置,它将序列编码为单一类别;还有一个 drum_kit 配置,它将序列映射到九种打击乐器,并将注意力长度配置为 32。

最后,每种配置都配有一个或多个 预训练模型。例如,Magenta 提供了一个预训练的 Drums RNN drum_kit 模型,以及多个预训练的 MusicVAE cat-drums_2bar_small 模型。

本书中我们将使用这些术语。在前几章,我们将使用 Magenta 预训练模型,因为它们已经非常强大。之后,我们将创建自己的配置并训练自己的模型。

生成和风格化图像

图像生成和风格化可以通过 Magenta 中的Sketch RNNStyle Transfer模型分别实现。Sketch-RNN 是一个序列到序列Seq2Seq)变分自编码器。

Seq2Seq 模型用于将一个领域的序列转换为另一个领域的序列(例如,将一句英语句子翻译为法语句子),这些序列的长度不一定相同,这是传统模型结构无法实现的。网络将把输入序列编码成一个向量,称为潜在向量,解码器将尽可能准确地重现输入序列。

图像处理不属于本书的内容,但我们将在第四章《使用 MusicVAE 进行潜在空间插值》中看到潜在空间的应用,当时我们会使用 MusicVAE 模型。如果你对 SketchRNN 模型感兴趣,请参阅进一步阅读部分了解更多信息。

生成音频

在 Magenta 中,音频生成是通过NSynth(一个基于 WaveNet 的自编码器)和GANSynth模型完成的。WaveNet 的有趣之处在于,它是一种卷积架构,广泛应用于图像领域,但在音乐应用中很少使用,反而更多使用递归网络。卷积神经网络CNNs)主要通过卷积阶段定义,在这个阶段中,滤波器在图像中滑动,计算图像的特征图。可以使用不同的滤波器矩阵来检测不同的特征,如边缘或曲线,这对图像分类非常有用。

我们将在第五章《使用 NSynth 和 GANSynth 生成音频》中看到这些模型的应用。

生成、插值和转换乐谱

乐谱生成是 Magenta 的主要部分,可以分为代表乐谱不同部分的不同类别:

  • 节奏生成:这可以通过"Drums RNN"模型完成,该模型是一个应用 LSTM 进行语言建模的 RNN 网络。鼓轨道从定义上来说是多声部的,因为多个鼓可以同时击打。这个模型将在第二章《使用 Drums RNN 生成鼓序列》中介绍。

  • 旋律生成:也称为单声部生成,这可以通过"Melody RNN"和"Improv RNN"模型完成,这些模型同样实现了注意力机制,允许模型学习更长的依赖关系。这些模型将在第三章《生成多声部旋律》中介绍。

  • 多声部生成:可以使用Polyphony RNNPerformance RNN模型完成,其中后者还实现了表现力的时序(有时称为 groove,即音符并非完全按照网格开始和停止,给人一种人类演奏的感觉)和动态(或速度)。这些模型将在第三章,生成多声部旋律中介绍。

  • 插值:可以使用 MusicVAE 模型完成,它是一个变分自编码器,可以学习音乐序列的潜在空间,并在现有序列之间进行插值。这个模型将在第四章,使用 Music VAE 进行潜在空间插值中介绍。

  • 转换:可以使用GrooVAE模型完成,这是 MusicVAE 模型的一个变种,可以为现有的鼓表演添加节奏感。这个模型将在第四章,使用 Music VAE 进行潜在空间插值中介绍。

安装 Magenta 和适用于 GPU 的 Magenta

安装机器学习框架并非易事,且通常是一个较大的入门障碍,主要是因为 Python 在依赖管理方面有着恶名。我们将通过提供清晰的指引和版本信息来简化这一过程。我们将覆盖 Linux、Windows 和 macOS 的安装指引,因为这些系统的命令和版本大多相同。

在本节中,我们将安装 Magenta 以及适用于 GPU 的 Magenta 版本(如果你有合适的硬件)。为 GPU 安装 Magenta 需要多一些步骤,但如果你希望训练模型(我们将在第七章,训练 Magenta 模型中进行),这是必要的。如果你不确定是否进行此操作,可以跳过本节并稍后再回来。我们还将提供一种解决方案,帮助你在没有 GPU 的情况下,通过云端解决方案完成这一章节。

TensorFlow 将通过 Magenta 的依赖项进行安装。我们还将介绍一些可选但有用的程序,帮助你可视化和播放音频内容。

选择正确的版本

截至本文撰写时,已有更新版的 Python 和 CUDA 可供使用,但由于与 TensorFlow 和 TensorFlow GPU 的不兼容,我们将使用以下版本。我们将使用 Magenta 1.1.7 版本,因为这是本文撰写时 Magenta 的稳定版本。你可以尝试使用更新版本进行示例操作,如果不工作可以回退到当前版本:

  • Magenta: 1.1.7

  • TensorFlow: 1.15.0(此版本在安装 Magenta 时会自动安装)

这意味着我们需要使用以下版本,才能让 TensorFlow 正常工作:

  • Python: 3.6.x

  • CUDA 库:10.0

  • CudRNN: 7.6.x(最新版本可以)

让我们来看一下如何安装这些版本。

使用 Conda 创建 Python 环境

在本书的整个过程中,我们将使用一个 Python 环境,这是一个独立的、与系统安装分开的 Python 环境,你可以在处理特定软件时切换到它,比如在编写本书或处理其他软件时。这样也确保了系统范围的安装保持安全。

有许多 Python 环境管理工具,但我们在这里使用 Conda,它会随一个名为 Miniconda 的独立 Python 安装包一起安装。你可以将 Miniconda 理解为一个包含打包好的 Python、一部分依赖和 Conda 的程序。

要安装 Miniconda,请访问 docs.conda.io/en/latest/miniconda.html,下载适合你平台的安装程序,选择 Python 3.7 作为 Python 版本(这不是 Magenta 将运行的 Python 版本),并选择 32 位或 64 位(你可能是后者)。

对于 Windows,请按照以下步骤操作:

  1. 双击安装程序。然后,按照提示操作,保持默认设置不变。

  2. 通过进入 控制面板 > 系统 > 高级系统设置 > 环境变量... > Path > 编辑... > 新建 来将 conda 添加到 PATH 中,并将 condabin 文件夹添加到 Miniconda 安装文件夹(通常是 C:\Users\Packt\Miniconda3\condabin)。

如果你在 Windows 上安装 Miniconda 时遇到问题,你也可以使用 Anaconda,它是相同的软件,但捆绑了更多的工具。

首先,从 www.anaconda.com/distribution 下载 Anaconda,双击安装程序,按照提示操作,保持默认设置不变。

然后,从开始菜单启动 Anaconda Prompt 而不是 命令提示符,这将启动一个新的命令行窗口,并初始化 Conda。

对于 macOS 和 Linux,请打开终端,进入下载文件的目录,按以下步骤操作:

  1. 通过将 <platform> 替换为你下载的平台,使脚本对你的用户可执行:
chmod u+x Miniconda3-latest-<platform>
  • 现在,执行脚本,安装软件:
./Miniconda3-latest-<platform>

现在 Conda 已经安装好了,让我们检查它是否正常工作:

  1. 打开一个新的终端并输入以下命令:
> conda info

 active environment : None
 shell level : 0
 user config file : C:\Users\Packt\.condarc
 populated config files :
 conda version : 4.7.5
 conda-build version : not installed
 python version : 3.7.3.final.0
 virtual packages : __cuda=10.1
 base environment : C:\Users\Packt\Miniconda3  (writable)
[...]
 envs directories : C:\Users\Packt\Miniconda3\envs
 C:\Users\Packt\.conda\envs
 C:\Users\Packt\AppData\Local\conda\conda\envs
 platform : win-64
 user-agent : conda/4.7.5 requests/2.21.0 CPython/3.7.3 Windows/10 Windows/10.0.18362
 administrator : False
 netrc file : None
 offline mode : False

你的输出可能有所不同,但原理是一样的。

  1. 现在,我们需要为本书创建一个新的环境。我们将其命名为“magenta”:
> conda create --name magenta python=3.6

注意,Python 版本为 3.6,正如我们在本节开头提到的那样。

你已经创建了一个包含正确版本 Python 和一些依赖项的新环境。现在你有三个不同的 Python 环境,每个环境都有自己版本的 Python 和依赖:

  • None:这是系统范围的 Python 安装(在 Windows 上可能不存在),你可以通过 conda deactivate 切换到它。

  • base:这是我们下载的 Python 3.7 的 Miniconda 安装版本,你可以通过 conda activate base 切换到它。

  • magenta:这是我们为本项目安装的新的 Python 3.6 环境,你可以通过 conda activate magenta 切换到它。

由于我们仍然处于基础环境中,我们需要激活“magenta”环境。

  1. 使用activate标志来切换环境:
> conda activate magenta

从此开始,你的终端行前缀会显示“(magenta)”,这意味着你执行的命令是在此特定环境中运行的。

  1. 让我们检查一下 Python 版本:
> python --version
Python 3.6.9 :: Anaconda, Inc.

如果你这里有其他内容(你应该安装 Python 版本 3.6.x 和 Anaconda 打包工具),请停止操作并确保你已正确按照安装说明进行安装。

这里提醒一下,你需要 Python 3.6.x。较旧的 Python 版本无法运行本书中的代码,因为我们使用了 3.6 版本的语言特性,而较新的版本则无法运行 TensorFlow,因为它尚不支持 3.7。

安装先决软件

现在我们已经启动并运行了 Python 环境,你将需要一些本书中会用到的先决软件。让我们开始吧:

  1. 首先,我们需要安装 curl,在 Windows 和 macOS 上默认预装,但 Linux 上通常没有(至少并非所有发行版都预装)。在 Debian 发行版上,使用以下命令。在其他发行版上,使用你的包管理工具:
> sudo apt install curl
  1. 现在,我们需要安装 Visual MIDI,这是一种 MIDI 可视化库,我们将用它来生成我们创建的乐谱的图表。在 Magenta 环境中,运行以下命令:
> pip install visual_midi 
  1. 最后,我们将安装表格模块,这对于稍后读取存储在 H5 数据库中的外部数据集非常有用:
> pip install tables

安装 Magenta

在我们安装了环境和所需的软件后,现在可以安装 Magenta 版本 1.1.7。你也可以使用 Magenta 的更新版本,但请自行承担风险:本书的代码是基于 1.1.7 版本编写的,并且 Magenta 源代码有变化的趋势。让我们开始吧:

  1. 在 Magenta 环境中,运行以下命令:
> pip install magenta==1.1.7

如果你想尝试更新版本的 Magenta,只需在pip命令中去掉版本信息。这样,它将安装最新版本。如果使用更新版本时遇到问题,你可以使用pip install 'magenta==1.1.7' --force-reinstall命令重新安装 1.1.7 版本。

  1. 然后,通过在 Python shell 中导入 Magenta 并打印版本来测试安装:
> python -c "import magenta; print(magenta.__version__)"

安装适用于 GPU 的 Magenta(可选)

现在我们已经安装了 Magenta,接下来我们将安装适用于 GPU 的 Magenta,这是让 Magenta 在 GPU 上执行所必需的。如果你没有 GPU、没有计划训练模型,或希望使用基于云的训练解决方案,则可以选择不安装。继续之前,我们需要确保我们的 GPU 支持 CUDA,并且计算能力大于 3.0,检查方法是访问 NVIDIA 网站:developer.nvidia.com/cuda-gpus

在 Windows 上,你需要从 visualstudio.microsoft.com 下载并安装 Visual Studio Community IDE,它应该会为我们安装所有所需的依赖项。

然后,对于所有平台,按照以下步骤进行操作:

  1. developer.nvidia.com/cuda-10.0-download-archive 下载 CUDA 工具包,并使用提供的任何安装方法启动安装向导。

在安装 CUDA 驱动程序时,可能会出现一条消息,提示“您的显示驱动程序比此安装包提供的驱动程序更新”。这是正常的,因为此 CUDA 版本不是最新的。您可以选择仅安装 CUDA 驱动程序来保留当前的显示驱动程序。

  1. 现在 CUDA 已经安装完成,您可能需要重新启动计算机以加载 NVIDIA 驱动程序。您可以使用以下命令来测试您的安装是否成功:
> nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2018 NVIDIA Corporation
Built on Sat_Aug_25_21:08:04_Central_Daylight_Time_2018
Cuda compilation tools, release 10.0, V10.0.130
  1. 现在,我们需要安装 cuDNN 库,它是一个用于在 GPU 上使用 CUDA 驱动程序执行深度学习命令的工具包。您应该能够从 developer.nvidia.com/rdp/cudnn-download 获取适用于 CUDA 10.0 的最新 cuDNN 版本。选择 Download cuDNN v7.6.x (...),for CUDA 10.0

    确保使用 cuDNN Library for Platform 这个链接,以便我们可以获得完整的库存档进行操作(例如,不要下载 .deb 文件)。

  2. 下载后,我们需要从正确的位置复制文件;请查看每个平台的以下命令:

  3. 现在,我们准备好为 GPU 安装 Magenta 了:

> pip install magenta-gpu===1.1.7

请查看 生成基本 MIDI 文件 部分的提示,以验证 TensorFlow 是否与您的 GPU 正常工作。

安装音乐软件和合成器

在本书的过程中,我们将处理 MIDI 和音频文件。处理 MIDI 文件需要特定的软件,您现在应该安装它,因为在整个本书中您都会用到它。

安装 FluidSynth 软件合成器

软件合成器是一个能够用虚拟乐器(来自音色库,即 SoundFont)或通过合成音频使用波形来播放输入的 MIDI 音符或 MIDI 文件的软件。我们将需要一个软件合成器来播放我们模型生成的音符。

对于本书,我们将使用 FluidSynth,这是一款强大且跨平台的命令行软件合成器。本节将介绍每个平台的安装过程。

安装 SoundFont

SoundFont 安装在所有平台上都是相同的。我们将下载并将 SoundFont 文件保存在一个便于访问的位置,因为我们将在本书中需要它。请按照以下步骤操作:

  1. 下载 SoundFont 文件:ftp.debian.org/debian/pool/main/f/fluid-soundfont/fluid-soundfont_3.1.orig.tar.gz

  2. 解压 .tar.gz 文件。

  3. FluidR3_GM.sf2 文件复制到一个便于访问的位置。

安装 FluidSynth

不幸的是,对于Windows,FluidSynth 核心团队没有维护二进制文件。我们需要从 GitHub 项目中获取二进制文件(尽管这些版本可能略微滞后于发布计划)。请按照以下步骤操作:

  1. 下载 github.com/JoshuaPrzyborowski/FluidSynth-Windows-Builds/archive/master.zip 的压缩包。

  2. 解压文件并进入 bin64 文件夹。

  3. fluidsynth-2.0.x 文件夹(包含最新版本的 FluidSynth)复制到一个便于访问的位置。

  4. fluidsynth-required-dlls 文件的内容复制到 C:\Windows\System32

  5. 通过进入 控制面板 > 系统 > 高级系统设置 > 环境变量... > Path > 编辑... > 新建 来将 FluidSynth 添加到 PATH,然后添加步骤 3 中复制的文件夹的 bin 文件夹。

对于Linux,大多数发行版都维护着 FluidSynth 包。在这里,我们提供针对基于 Debian 的发行版的安装说明。对于其他发行版,请参考你的包管理器。在终端中,使用 sudo apt install fluidsynth 命令来下载 FluidSynth。

对于 MacOS X,我们将使用 Homebrew 安装 FluidSynth。在开始之前,请确保你已安装了最新版本的 Homebrew。在终端中,使用 brew install fluidsynth 命令来下载 FluidSynth。

测试你的安装

现在,你可以测试你的 FluidSynth 安装(通过将 PATH_SF2 替换为我们之前安装的 SoundFont 路径):

  • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2

  • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2

  • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2

你应该看到类似以下的输出,且没有任何错误:

FluidSynth runtime version 2.0.3
Copyright (C) 2000-2019 Peter Hanappe and others.
Distributed under the LGPL license.
SoundFont(R) is a registered trademark of E-mu Systems, Inc

使用硬件合成器(可选)

除了使用软件合成器,你还可以使用硬件合成器来聆听生成的 MIDI 文件。我们将在第九章《让 Magenta 与音乐应用程序互动》中更详细地讲解这一点,但你可以通过 USB 将合成器连接到电脑;该设备应该会作为一个新的输入 MIDI 端口注册。Magenta 可以使用这个端口来发送传入的 MIDI 音符。

安装 Audacity 作为数字音频编辑器(可选)

我们不会处理音频,直到 第五章,使用 NSynth 和 GANSynth 生成音频,所以你可以等到那一章再安装 Audacity。Audacity 是一款出色的开源跨平台(Windows、Linux、macOS)音频剪辑编辑软件。它没有数字音频工作站的全部功能(有关更多信息,请参见安装数字音频工作站部分),但它易于使用且功能强大。

Audacity 可用于轻松录制音频、剪切和拆分音频片段、添加简单效果、进行简单的均衡调整,并导出各种格式:

我们将在 第五章,使用 NSynth 和 GANSynth 生成音频 中详细解释如何使用 Audacity。

安装 MuseScore 用于乐谱(可选)

在本书中,我们将大量使用乐谱,尤其是 MIDI。我们将使用命令行工具生成表示乐谱的静态图像,但在 GUI 中查看和编辑乐谱并使用数字乐器播放它们也是非常有用的。请注意,MuseScore 无法播放实时 MIDI,因此它与软件合成器不同。它也不太适用于表现性节奏(即音符不在开始和结束的精确时刻)。我们将在下一章中说明何时不使用 MuseScore。

MuseScore 是一款免费的优秀乐谱软件,网址为 musescore.org,适用于所有平台。如果你愿意,现在就可以安装,或者等到需要时再安装。

MuseScore 还充当了一个协作的乐谱数据库,网址为 musescore.com,我们将在本书中贯穿使用:

安装数字音频工作站(可选)

本书不需要安装 DAW,除非是 第九章,让 Magenta 与音乐应用程序互动。这类软件有不同的形式和复杂性,并且在音乐制作中非常重要,因为它能处理音乐制作的所有必要任务,如音频和 MIDI 处理、作曲、效果、母带处理、VST 插件等。

Ardour (ardour.org) 是唯一一款开源且跨平台的 DAW,它要求你为预构建版本支付一小笔费用。根据你的平台,你可能想尝试不同的 DAW。在 Linux 上,你可以使用 Ardour;在 macOS 和 Windows 上,你可以使用 Ableton Live,这是一款成熟的 DAW。我们不会为这一部分推荐任何特定软件,因此你可以使用任何你习惯的软件。在 第九章,让 Magenta 与音乐应用程序互动 中,我们会通过具体例子详细介绍各个 DAW,因此你可以等到那时再安装新的 DAW。

安装代码编辑软件

在本节中,我们将推荐一些关于代码编辑的可选软件。虽然不是必需的,但对于初学者来说,使用这些软件会有很大帮助,因为纯文本代码编辑软件可能让他们感到困惑。

安装 Jupyter Notebook(可选)

Notebooks 是一种很好的方式来共享包含文本、解释、图表和其他丰富内容的代码。它在数据科学社区中被广泛使用,因为它可以存储和显示长时间运行操作的结果,同时还提供一个动态的运行时环境,以便编辑和执行内容。

本书的代码可以在 GitHub 上以普通 Python 代码的形式获得,也可以以 Jupyter Notebook 的形式提供。每个章节都有一个自己的笔记本,作为示例。

要安装 Jupyter 并启动你的第一个笔记本,请按照以下步骤操作:

  1. 在 Magenta 环境中,执行以下命令:
> pip install jupyter
  1. 现在,我们可以通过执行以下命令来启动 Jupyter 服务器(也是在 Magenta 环境中执行):
> jupyter notebook

Jupyter 界面将在 Web 浏览器中显示。之前的命令应该已经启动了默认浏览器。如果没有,请使用命令输出中的 URL 打开它。

  1. 进入笔记本 UI 后,你应该能看到磁盘内容。导航到本书的代码并加载 Chapter01/notebook.ipynb 笔记本。

  2. 确保选择的内核是 Python 3。这个内核对应于已经安装在你 Magenta 环境中的 Python 解释器。

  3. 使用 运行 按钮运行每个单元的代码块。这将确保 Jupyter 在正确的环境中执行,并通过打印 TensorFlow 和 Magenta 的版本来验证。

这是笔记本应该显示的样子:

安装和配置 IDE(可选)

本书并不要求必须使用 集成开发环境IDE),因为所有示例都可以从命令行运行。然而,IDE 是一个很好的工具,因为它提供了自动补全、集成开发工具、重构选项等功能。它在调试时也非常有用,因为你可以直接进入代码进行调试。

本书推荐的一个好 IDE 是 JetBrains 的 PyCharm(www.jetbrains.com/pycharm),这是一个 Python IDE,提供社区版(开源版),它提供了你所需要的一切。

无论你使用 PyCharm 还是其他 IDE,都需要将 Python 解释器更改为我们之前安装的那个。这相当于使用 Conda 激活我们的 Magenta 环境。在 IDE 的项目设置中,找到 Python 解释器设置,并将其更改为我们环境的安装路径。

如果你忘记了它的位置,可以使用以下命令:

> conda activate magenta
> conda info
...
 active env location : C:\Users\Packt\Miniconda3\envs\magenta
...

在 Windows 上,Python 解释器位于根文件夹,而在 Linux 或 macOS 上,它位于根目录下的 bin 目录中。

生成基本的 MIDI 文件

Magenta 提供了多个命令行脚本(安装在 Magenta 环境的 bin 文件夹中)。基本上,每个模型都有自己的控制台脚本,用于数据集准备、模型训练和生成。让我们来看看:

  1. 在 Magenta 环境中,下载 Drums RNN 预训练模型 drum_kit_rnn
> curl --output "drum_kit_rnn.mag" "http://download.magenta.tensorflow.org/models/drum_kit_rnn.mag"
  1. 然后,使用以下命令生成你的第一个几个 MIDI 文件:
> drums_rnn_generate --bundle_file="drum_kit_rnn.mag"

默认情况下,前面的命令会在 /tmp/drums_rnn/generated 目录生成文件(在 Windows 上是 C:\tmp\drums_rnn\generated)。你应该会看到 10 个新的 MIDI 文件,以及时间戳和生成索引。

如果你正在使用 GPU,可以通过在脚本输出中搜索“Created TensorFlow device ... -> physical GPU (name: ..., compute capability: ...)”来验证 TensorFlow 是否正确使用了它。如果没有找到,说明它正在你的 CPU 上执行。

你还可以在 Magenta 执行时检查 GPU 的使用情况,如果 Magenta 正确使用 GPU,这个值应该会增加。

  1. 最后,为了听到生成的 MIDI,你可以使用软件合成器或 MuseScore。对于软件合成器,请根据你的平台参考以下命令,并将 PATH_TO_SF2PATH_TO_MIDI 替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

恭喜!你已经使用机器学习模型生成了你的第一个音乐乐谱!你将在本书中学到如何生成更多的内容。

总结

本章很重要,因为它介绍了使用机器学习生成音乐的基本概念,所有这些内容将在本书中进一步展开。

在本章中,我们了解了什么是生成音乐,并且它的起源甚至早于计算机的出现。通过查看具体示例,我们看到了不同类型的生成音乐:随机的、算法的和随机过程的。

我们还学习了机器学习如何快速改变我们生成音乐的方式。通过引入音乐表示和各种处理方法,我们了解了 MIDI、波形和频谱图,以及本书中将要接触的各种神经网络架构。

最后,我们回顾了在生成和处理图像、音频以及乐谱方面,Magenta 可以做些什么。通过这些内容,我们介绍了本书中将要使用的主要模型,即 Drums RNN、Melody RNN、MusicVAE、NSynth 等。

你还为本书安装了开发环境,并生成了你的第一个音乐乐谱。现在,我们准备开始了!

下一章将深入探讨我们在本章中介绍的一些概念。我们将解释什么是 RNN,以及它在音乐生成中的重要性。然后,我们将在线命令行和 Python 中使用 Drums RNN 模型,并解释其输入和输出。最后,我们将创建我们自主音乐生成系统的第一个构建模块。

问题

  1. 音乐骰子游戏依赖于什么生成原理?

  2. 在第一部计算机生成的音乐作品《Illiac Suite》中,使用了什么基于随机的生成技术?

  3. 现场程序员在现场实现生成音乐的音乐类型叫什么名字?

  4. 什么样的模型结构对于追踪音乐乐谱中时间上相距较远的事件很重要?

  5. 自主音乐系统和辅助音乐系统有什么区别?

  6. 什么是符号表示和子符号表示的例子?

  7. 音符在 MIDI 中是如何表示的?

  8. 在 96 kHz 的采样率下,能表示哪些频率范围而不会失真?这对听音频有帮助吗?

  9. 在频谱图中,440 Hz 频率处显示一个 1 秒钟的强烈颜色块。这代表着什么音符?

  10. 使用 Magenta 可以生成音乐乐谱中的哪些不同部分?

进一步阅读

第二部分:使用机器学习生成音乐

本节介绍如何使用 Magenta 生成鼓序列和旋律,插值乐谱以及生成音频。我们将为你提供所有所需的工具,帮助你创作一首完全生成的歌曲。

本节包含以下章节:

  • 第二章,使用 Drums RNN 生成鼓序列

  • 第三章,生成复音旋律

  • 第四章,使用 MusicVAE 进行潜空间插值

  • 第五章,使用 NSynth 和 GANSynth 进行音频生成

第四章:使用 Drums RNN 生成鼓序列

在这一章中,你将学习到许多人认为音乐的基础——打击乐。我们将展示递归神经网络RNNs)在音乐生成中的重要性。然后,你将学习如何使用预训练的鼓套件模型,调用它在命令行窗口中以及直接在 Python 中使用 Drums RNN 模型,来生成鼓序列。我们将介绍不同的模型参数,包括模型的 MIDI 编码,并展示如何解读模型的输出。

本章将涵盖以下主题:

  • RNN 在音乐生成中的重要性

  • 在命令行中使用 Drums RNN

  • 在 Python 中使用 Drums RNN

技术要求

在本章中,我们将使用以下工具:

  • 使用命令行bash从终端启动 Magenta

  • 使用Python及其库来编写使用 Magenta 生成音乐的代码

  • 使用Magenta生成 MIDI 格式的音乐

  • 使用MuseScoreFluidSynth来听生成的 MIDI

在 Magenta 中,我们将使用Drums RNN模型。我们将深入解释这个模型,但如果你觉得需要更多信息,Magenta 源代码中的模型 README(github.com/tensorflow/magenta/tree/master/magenta/models/drums_rnn)是一个很好的起点。你也可以查看 Magenta 的 GitHub 代码,它有很好的文档。我们还在最后一节提供了额外的内容,进一步阅读

本章的代码位于本书的 GitHub 仓库中的Chapter02文件夹,位置在github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter02。对于本章,你应该在命令行窗口中运行cd Chapter02,然后再开始。

查看以下视频,查看代码如何运作:

bit.ly/37G0mmW

RNN 在音乐生成中的重要性

特定的神经网络架构是为特定问题设计的。这并不意味着某一种架构比另一种更好——它只是更适合某一特定任务。

在这一节中,我们将关注我们特定的问题——生成音乐,并了解为什么 RNN 非常适合这项任务。在本书中,我们将通过每一章介绍特定的概念,逐步构建关于音乐的神经网络架构的知识。

对于音乐生成,我们关注的是 RNN 解决的两个具体问题——在输入和输出方面处理序列,以及保持过去事件的内部状态。让我们来看看这些特性。

音乐谱预测类似于生成音乐。通过预测输入序列中的下一个音符,你可以通过在每次迭代中选择预测来逐步生成一个新的序列。这个过程在本章的理解生成算法部分进行了描述。

操作一个向量序列

在许多神经网络架构中,输入大小输出大小是固定的。以卷积神经网络CNN)为例。该神经网络可用于图像分类,输入是表示图像的像素数组,输出则是每个类别集的预测(例如,“猫”,“狗”等)。请注意,输入和输出的大小是固定的。

RNN 的优点在于输入和输出的大小可以是任意长度。对于音乐谱预测网络,输入可以是一个任意长度的音符序列,输出则是基于该输入的预测音符序列。

这在 RNN 中是可能的,因为它是基于序列向量进行操作的。表示 RNN 类型的方式有很多:

  • --:在这里,有固定输入和输出;一个例子是图像分类。

  • --:在这里,有固定输入到序列输出;一个例子是图像标注,其中网络会基于图像内容生成文本。

  • --:在这里,有序列输入到固定输出;一个例子是情感分析,其中网络会输出一个描述输入句子的单一词汇(情感)。

  • --:在这里,有序列输入到序列输出;一个例子是语言翻译,其中网络会根据另一种语言的完整句子输出一种语言的完整句子。

表示 RNN 的经典方式如下图所示。在图表的左侧,你可以看到网络的简洁表示——隐藏层输出馈送到自身。在右侧,你可以看到相同网络的详细表示——在每一步,隐藏层接收输入和前一个状态,并产生输出:

图表的底部行展示了输入向量,中间行展示了隐藏层,顶部行展示了输出层。这个表示展示了 RNN 如何表示多对多输入输出,如下所示:

  • 输入的向量序列:{ ..., x(t - 1), x(t), x(t + 1), ... }

  • 输出的向量序列:{ ..., y(t - 1), y(t), y(t + 1), ... }

记住过去,以便更好地预测未来

正如我们在前一部分所看到的,在 RNN 中,输入向量与其状态向量结合,产生输出,并用于更新下一个步骤的状态向量。这与卷积神经网络(CNN)等前馈神经网络不同,后者是将信息从输入传递到输出,并且只在那个方向上进行传播,意味着输出仅仅是输入的函数,而不是前一个事件的结果。

让我们来看一下如何定义一个简单的 RNN。我们实现一个单一操作,step 操作,该操作接受一个输入向量 x,并返回一个输出向量 y。每次调用 step 操作时,RNN 需要更新其状态,即隐向量 h

需要注意的是,我们可以堆叠任意数量的 RNN,通过将一个 RNN 的输出作为下一个 RNN 的输入,就像前面的图示所示。例如,我们可以这样操作:y1 = rnn1.step(x1)y2 = rnn2.step(y1),依此类推。

在训练 RNN 时,在前向传播过程中,我们需要更新状态,计算输出向量,并更新损失。那么我们如何更新状态呢?让我们看看需要遵循的步骤:

  1. 首先,我们进行隐状态矩阵(Whh)与前一个隐状态(hs[t-1])的矩阵乘法,即np.dot(Whh, hs[t-1])

  2. 然后,我们将其与当前输入矩阵(Wxh)和输入向量(xs[t])的矩阵乘法相加,即np.dot(Wxh, xs[t])

  3. 最后,我们对结果矩阵使用 tanh 激活函数,将激活值限制在 -1 到 1 之间。

我们在每一步都进行更新,这意味着,在训练的每一步,网络都能保持最新的上下文信息,以应对它正在处理的序列。

为了理解 RNN 如何处理序列数据,例如音符序列,我们以 RNN 在断开和弦上的训练为例,断开和弦是将和弦分解成一系列音符。我们有输入数据 "A"、"C"、"E" 和 "G",它们被编码为向量,例如,第一个音符的编码是 [1, 0, 0, 0](对应于前图中的x(t - 1)),第二个音符的编码是 [0, 1, 0, 0](对应于前图中的x(t)),依此类推。

在第一步中,使用第一个输入向量时,RNN 输出的结果是,例如,"A"的下一个音符的置信度为 0.5,"C"为 1.8,"E"为-2.5,"G"为 3.1。由于我们的训练数据告诉我们,正确的下一个音符是"C",所以我们希望增加 1.8 的置信度,并减少其他音符的置信度。类似地,对于每一步(对于四个输入音符),我们都有一个正确的音符需要预测。记住,在每一步中,RNN 都会使用隐向量和输入向量来进行预测。在反向传播过程中,参数会通过一个小的调整被推向正确的方向,经过多次重复训练后,我们得到的预测将与训练数据匹配。

在推理过程中,如果网络第一次接收到输入“C”,它不一定会预测“E”,因为它还没有看到“A”,这与用于训练模型的示例和弦不符。RNN 的预测基于其递归连接,该连接跟踪上下文,而不仅仅依赖于输入。

要从训练好的 RNN 中进行采样,我们将一个音符输入到网络中,网络会输出下一个音符的分布。通过从分布中采样,我们得到一个可能的下一个音符,然后可以将其反馈回网络。我们可以重复这个过程,直到生成足够长的序列。这个生成过程将在下一节中详细描述,理解生成算法

在反向传播中,我们看到我们在网络中向后更新参数。假设网络正在学习一个很长的音符序列:梯度能在网络中反向传播多远,以便序列中很远的音符与序列开头的音符之间的联系仍然成立?事实证明,这是传统 RNN 的一个难题。为此,有一个答案是长短期记忆LSTM)单元,它使用一种不同的机制来保持当前状态。

使用正确的 RNN 术语

现在我们已经理解了 RNN,我们可以说大多数 RNN 都使用 LSTM 单元。这些 RNN 有时被称为LSTM 网络,但更常见的是直接称其为 RNN。不幸的是,这两个术语经常互换使用。在 Magenta 中,所有的 RNN 都是 LSTM,但并没有专门以此命名。这就是我们在本章中所看到的鼓乐 RNN 模型以及接下来章节中我们将使用的所有模型的情况。

我们将在第三章中解释 LSTM,即生成多声部旋律。现在,只需要记住我们在前一部分所看到的内容仍然成立,但隐藏状态的更新比我们之前描述的要复杂。

在命令行中使用鼓乐 RNN

现在我们已经了解了 RNN 如何成为强大的音乐生成工具,我们将使用鼓乐 RNN 模型来实现这一点。Magenta 中的预训练模型是直接开始音乐生成的好方法。对于鼓乐 RNN 模型,我们将使用drum_kit预训练包,它是基于成千上万的打击乐 MIDI 文件训练的。

本节将深入讲解如何在命令行中使用 Magenta。我们主要使用 Python 代码来调用 Magenta,但使用命令行有一些优势:

  • 它使用简单,适合快速使用的场景。

  • 它不需要编写任何代码或具备编程知识。

  • 它将参数封装在有用的命令和标志中。

在本节中,我们将使用命令行中的鼓乐 RNN 模型,并学习通过标志来配置生成过程。我们将解释生成算法如何工作,并查看其参数和输出。

Magenta 的命令行工具

Magenta 配备了多个命令行工具。这些命令行工具是可以直接从命令行调用的 Python 脚本,作为控制台入口点,并在安装 Magenta 时被安装到你的 Conda 环境中(如果使用 Windows,请查看 Magenta 环境中的bin文件夹或scripts文件夹)。命令行工具的完整列表位于 Magenta 源代码中的setup.py文件内的CONSOLE_SCRIPTS部分。

你可以随时查看 Magenta 的源代码并进行浏览。刚开始可能会觉得有些令人生畏,但源代码有很好的文档,并提供了关于软件内部工作原理的宝贵洞察。使用 Git,执行git clone https://github.com/tensorflow/magenta命令,并在你喜欢的 IDE 中打开该仓库。拥有源代码的另一个优势是,可以查看某些未打包在应用程序中的文件。

对于我们将使用的鼓 RNN 模型,我们有三个命令行工具(就像大多数模型一样):

  • drums_rnn_create_dataset 将帮助创建训练命令所需的数据集。我们将在第六章,训练数据准备中进一步探讨这个命令。

  • drums_rnn_generate 将在本章中用于生成乐谱。

  • drums_rnn_train 将在输入数据集上训练模型。我们将在第七章,训练 Magenta 模型中进一步探讨这个命令。

生成一个简单的鼓序列

在上一章,我们生成了一个简单的 MIDI 文件来测试我们的安装。我们将使用这个示例并稍作修改。

在开始之前,返回终端到主书籍文件夹,然后切换到Chapter02目录。确保你在 Magenta 环境中。如果不是,请使用conda activate magenta来激活环境:

  1. 首先,我们下载鼓 RNN 捆绑文件drum_kit_rnn.mag,并将其放入bundles文件夹。你只需执行一次此操作:
> curl --output bundles/drum_kit_rnn.mag http://download.magenta.tensorflow.org/models/drum_kit_rnn.mag

捆绑文件是包含模型检查点和元数据的文件。这是一个预训练模型,包含来自训练阶段的权重,将用于初始化 RNN 网络。我们将在第七章,训练 Magenta 模型中详细介绍这种格式。

  1. 然后,我们可以使用捆绑文件通过--output-dir生成 MIDI 文件到输出目录:
> drums_rnn_generate --bundle_file=bundles/drum_kit_rnn.mag --output_dir output
  1. 打开output文件夹中生成的一个文件,在 MuseScore 或 Visual MIDI 中查看。对于后者,你需要将 MIDI 文件转换为图表,并渲染为 HTML 文件,然后可以在浏览器中打开。要将 MIDI 文件转换为图表,使用以下命令:
# Replace GENERATED by the name of the file
> visual_midi "output/GENERATED.mid"
  1. 然后,打开output/GENERATED.html HTML 文件,其中包含图表:

  1. 要聆听生成的 MIDI,可以使用你的软件合成器或 MuseScore。对于软件合成器,根据你的平台使用以下命令,并将 PATH_TO_SF2PATH_TO_MIDI 替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

理解模型的参数

从上一节的截图中,你已经可以看到模型使用了一些默认配置来生成乐谱:生成的步数、节奏等等。现在,让我们看看还有哪些其他标志是可用的。要查看模型支持的标志,请使用 --helpfull 标志:

> drums_rnn_generate --helpfull

    USAGE: drums_rnn_generate [flags]
    ...

magenta.models.drums_rnn.drums_rnn_config_flags:
    ...

magenta.models.drums_rnn.drums_rnn_generate:
    ...

你将看到许多可能的标志。我们感兴趣的部分是 drums_rnn_config_flagsdrums_rnn_generate,这些标志是针对 Drums RNN 模型的特定标志。

以下小节将解释最重要的部分。因为大多数内容也适用于其他模型,所以你可以将学到的内容应用到接下来的章节中。我们会在后续章节中根据需要解释其他模型特定的标志。

更改输出大小

更改生成样本数量的一个简单标志是 --num_outputs

--num_outputs: The number of drum tracks to generate. One MIDI file will be created for each. (default: '10')

你还可以使用 --num_steps 标志来更改生成样本的大小:

--num_steps: The total number of steps the generated drum tracks should be, priming drum track length + generated steps. Each step is a 16th of a bar. (default: '128')

我们生成的最后一个例子有 128 步长,因为我们使用默认值生成它。从之前的截图中,你可以数出竖条线的数量,共计 8 bars。这是因为,128 步,每 bar 16 步,得到 128/16 = 8 bars。如果你想生成 1 bar,你需要请求 16 步,例如。你可以将单个步骤视为一个音符插槽,因为生成器每个步骤最多生成一个音符。这是一种方便的时间划分方式。

本书将使用术语 bar,这是英国英语中更为常用的词,但读者可能更习惯使用 measure,这是美国英语中更常见的说法。两者在使用上有一些差异,具体取决于上下文,可能会有一个词使用得更多。不过,两者主要具有相同的含义。

本书始终使用 bar 的主要原因是遵循 Magenta 的代码约定,在该约定中,bar 的使用比 measure 更为一致。

我们可以通过放大之前例子的最后两 bars 来展示步骤:

你可以在这个图示中看到,有 2 个小节,每个小节为 2 秒(参见下一节获取节奏信息),每个小节都有不同的背景。你还可以看到每个小节有 16 步长;我们已用不同的背景标记了其中的一步。如果模型是多音的(如鼓 RNN 模型),则每个步长可以包含多个音符。根据模型的不同,一个音符可能会产生多个步长,但这里并非如此。对于这个模型,音符总是会在步长的开始和结束时准确起止,因为模型输出的是量化的序列。

更改节奏

节奏是乐谱播放的速度。请注意,它不会改变音符的数量或生成时长——它只会将信息写入生成的 MIDI 中,以便 MIDI 播放器能够以正确的速度播放。

Magenta 中的节奏以 每分钟四分音符数QPM)表示。四分音符是一个小节划分为四个——如果你有 16 步长的小节,那么四分音符就包含 4 步。因此,如果你的节奏是 120 QPM,那么你每秒播放 120 四分音符/60 秒 = 2 四分音符。这意味着每 2 秒播放 1 小节(参考之前的图示)。

QPM 是一种与BPM每分钟拍数)相似但不应混淆的节奏度量,因为在后者中,拍子的意义可能会随着某些节拍类型而改变。此外,拍子的概念可能会根据听众的不同而有所变化。QPM 在 MIDI 和 MusicXML 格式中得到了明确定义和使用。

要更改节奏,请使用 --qpm 标志:

--qpm: The quarters per minute to play generated output at. If a primer MIDI is given, the qpm from that will override this flag. (default: '120')

在以下图示中,我们使用 --qpm 150 生成了一个 150 QPM 的鼓文件:

你可以看到小节不再与 2 秒、4 秒等对齐。这是因为在 120 QPM 下,一个小节正好是 2 秒长,但现在稍微短了一点。我们生成的样本仍然有 --num_steps 128,但现在时长为 12.8 秒(仍然是 8 小节),因为我们仍然有相同数量的步长——它们只是以更快的速度播放。

要找到特定 QPM(如 150)序列的时长(秒),我们首先计算每步长的时长(秒)。方法是将一分钟的秒数(60)除以 QPM(150),再除以每四分音符的步长数(4)。这样我们得到每步长 0.1 秒。对于 128 步长,序列的时长为 12.8 秒。

更改模型类型

--config 标志改变模型的配置。每个 Magenta 配置都附带一个预训练的模型。在本章节中,我们使用 drum_kit_rnn.mag 预训练模型(或模型包)作为 drum_kit 配置。选择的预训练模型必须与其训练时的配置匹配:

--config: Which config to use. Must be one of 'one_drum' or 'drum_kit'. (default: 'drum_kit')

现在这对我们没有用处,但在第三章,生成复调旋律时它将派上用场。这还会改变鼓点的映射,其中生成的编码向量在两种情况下是不同的。当我们查看 Python 代码时,我们将在下一节讨论向量编码。

用 Led Zeppelin 来引导模型

可以给模型提供一个引导序列,以在生成之前准备它。这在 Magenta 中被广泛使用,如果你希望模型生成一些受到你提供的引导启发的内容,这非常有用。你可以用硬编码的序列或者直接从 MIDI 文件中引导模型。引导序列在生成开始前被输入到模型中。

--primer_drums标志的字符串表示如下:你输入一系列元组,每个元组对应一个步骤,每个元组包含在同一时刻播放的 MIDI 音符。在这个例子中,在第一步中,MIDI 音符 36 和 42 同时播放,接着是 3 步的静默,然后 MIDI 音符 42 单独在其自己的步骤中播放:

--primer_drums: A string representation of a Python list of tuples containing drum pitch values. For example: "[(36,42),(),(),(),(42,),(),(),()]". If specified, this drum track will be used as the priming drum track. If a priming drum track is not specified, drum tracks will be generated from scratch. (default: ''

正如你可能还记得从上一章,MIDI 音符也有音量信息,但这里没有给出。因为 Drums RNN 不支持音量,所以这不是必需的。每个生成的音符会默认音量值为 100(最大值为 127)。

Magenta 中的一些模型支持音量(velocity),正如我们在接下来的章节中将看到的那样。由于音量必须被编码到在训练过程中输入到网络的向量中,因此是否包含它们是一个设计选择。我们也将在接下来的章节中讨论编码问题。

要提供一个与小节对应的引导序列,你需要提供 16 个元组,因为每小节有 16 步。之前的引导序列只有半小节长。

你也可以使用--primer_midi标志提供一个 MIDI 文件的路径:

--primer_midi: The path to a MIDI file containing a drum track that will  be used as a priming drum track. If a primer drum track is not specified, drum tracks will be generated from scratch. (default: '')

引导的 MIDI 文件会提供节奏,如果你同时提供--qpm标志,它将覆盖该标志。

当使用引导初始化模型时,你还会在生成的输出序列中看到引导序列。这意味着--num_steps需要大于引导的长度,否则 Magenta 就没有足够的空间来生成。例如,下面的命令会报错,因为步数不足:

> drums_rnn_generate --bundle_file=bundles/drum_kit_rnn.mag --output_dir=output --primer_drums="[(36,),(36,),(36,),(36,)]" --num_steps=4

这会生成以下输出:

CRITICAL - Priming sequence is longer than the total number of steps requested: Priming sequence length: 0.625, Generation length requested: 0.62

让我们生成一些基于 Jon Bonham(Led Zeppelin)When The Levee Breaks曲目的小鼓部分的内容。这里是一个两小节的引导序列:

然后,我们通过设置引导序列、温度和适当的步数来生成一些 MIDI 文件。记住,步数是总步数,包括引导部分:

drums_rnn_generate --bundle_file bundles/drum_kit_rnn.mag --output_dir output --num_steps 46 --primer_midi primers/When_The_Levee_Breaks_Led_Zeppelin.mid --temperature 1.1

我们得到了一个有趣的序列,如下图所示:

你仍然可以在大约前 3 秒钟内找到前导音符,然后我们注意到模型保留了轨道的音乐结构,但在此基础上进行即兴创作,加入了几次低音鼓、镲片和小军鼓的音效。我们将在下一节中讨论打击乐序列的 MIDI 映射,包括每个音高与对应乐器的映射,将 MIDI 音符映射到现实世界

现在,我们验证 Magenta 是否知道如何计数:你有 16 步前导音符,1 步静音,然后是 29 步生成,总共 46 步,这是我们要求的。静音步长来自 Magenta 生成起始的方式。我们将在 Python 代码中看到如何更好地处理这一点。

我们还注意到,前导音符在生成的乐谱中的音符时值不同。你可以看到相同的前导音符存在,但时长不同。这是因为 Magenta 会在将前导音符输入到模型之前进行量化,并生成量化后的序列。这取决于模型。量化是指将音符的开始和结束时间调整到某些小节的子分割点上。在这种情况下,Magenta 将音符的结束位置调整到最接近的步骤上。

配置生成算法

--temperature标志非常重要,因为它决定了生成序列的随机性:

--temperature: The randomness of the generated drum tracks. 1.0 uses the unaltered softmax probabilities, greater than 1.0 makes tracks more random, less than 1.0 makes tracks less random. (default: '1.0')

让我们尝试使用--temperature 1.5生成一个更具随机性的鼓点轨道:

这真是太疯狂了!记住,温度为 1.5 时比较高,因此你可以选择一个更保守的值,比如 1.1,来生成一个更加一致的样本。

现在,要生成一个随机性较小的轨道,使用--temperature 0.9

你可以明显看到,这里的生成结果更加保守。选择温度值取决于个人口味,并且与你想要实现的效果有关。尝试不同的温度值,看看哪些最适合你想要生成的音乐。此外,一些模型可能在较为极端的温度值下效果更好。

其他 Magenta 和 TensorFlow 的标志

还有一些我们没有提到的标志,比如用--hparams配置模型的超参数,但我们会在第七章《训练 Magenta 模型》中讨论这些内容,训练 Magenta 模型

理解生成算法

在前面的章节中,我们介绍了生成算法的工作原理——通过在每个生成步骤中预测序列中的下一个音符,我们可以迭代地生成完整的乐谱。生成的预测结果取决于模型在训练阶段学到的内容。本节将通过逐步执行的示例深入探讨生成算法的运作。

我们还将解释一些修改生成执行过程的参数:

--beam_size: The beam size to use for beam search when generating drum tracks. (default: '1')
--branch_factor: The branch factor to use for beam search when generating drum tracks. (default: '1')
--steps_per_iteration: The number of steps to take per beam search iteration. (default: '1')

生成序列分支和步骤

让我们使用这个命令来启动生成:

drums_rnn_generate --bundle_file=bundles/drum_kit_rnn.mag --output_dir=output --temperature 1.1 --beam_size 1 --branch_factor 2 --steps_per_iteration 1 --num_steps 64

Magenta 将执行以下操作:

  1. 它将启动器序列转换为模型能够理解的格式(这称为 编码——请参阅 将打击乐事件编码为类别 部分)。

  2. 它使用该编码的启动器来初始化模型状态。

  3. 它会循环直到生成所有步骤(--num_steps 64):

    1. 它会循环生成 N 个分支(--branch_factor 2):

      1. 它通过使用 温度--temperature 1.1)运行模型及其当前状态,生成 X 步(--steps_per_iterations 1)。这会返回预测的序列以及结果的 softmax 概率。softmax 概率是每个类别(即编码音符)在网络最后一层的实际概率分数。

      2. 它计算结果序列的 负对数似然值,这是从 softmax 概率中得出的整个序列的评分评估。

      3. 它更新模型状态以便进行下一次迭代。

    2. 它通过使用计算得分来修剪生成的分支,保留最佳的 K 个分支(--beam_size 1)。

该图展示了生成过程,最终序列为 363842

在图中,S 值表示整个序列的计算得分(见 步骤 3.1.23.2)。这里展示的束搜索算法在输出序列长度(即树的深度)上的复杂度是线性的,因此它相当快速。--beam_size 1 的默认值是有用的,因为该算法变成了一个最佳优先搜索算法,你实际上并没有进行广度优先搜索,因为你只保留最好的候选。

理解随机性

当我们启动一个使用束搜索(beam search)的生成时,Magenta 会显示整个序列的对数似然值:

Beam search yields sequence with log-likelihood: -16.006279

如果使用 --temperature 1.25 而不是 1.1 会发生什么?对数似然值会更小(远离零),因为生成过程更具随机性:

Beam search yields sequence with log-likelihood: -57.161125

如果我们只生成 1 个分支,使用 --branch_factor 1 但保持温度为 1.25,会发生什么?对数似然值会更小:

Beam search yields sequence with log-likelihood: -140.033295

为什么对数似然值更小?因为我们减少了分支因子,算法每次迭代生成的分支会更少,这意味着每次迭代中可供选择的分支更少,从而导致全局上序列更随机。

现在,让我们利用我们学到的 Drums RNN 模型的知识,创建一个使用这些概念的小型 Python 应用程序。

在 Python 中使用 Drums RNN

在前一节中,我们已经看到我们可以通过命令行使用 Drums RNN 模型做多少事情。在本节中,你将创建一个小应用程序,使用该模型在 Python 中生成音乐。

在 Python 中使用 Magenta 有些困难,原因如下:

  • 它要求你编写代码并理解 Magenta 的架构。

  • 它需要更多的样板代码,且不那么直观。

但它也有我们认为很重要的优点:

  • 你在使用模型时有更多的自由度。

  • 你可以创建新的模型并修改现有的模型。

  • 你可以超越生成单一的序列。

最后一项对我们很重要,因为我们将构建一个小型的音乐应用程序,该应用能够自主生成音乐。在命令行上调用 Magenta 的脚本非常方便,但仅凭这一点无法构建应用程序。你将在本章最后一节创建音乐生成应用中开始这个工作,并在后续章节中继续扩展它。

让我们通过重现我们在命令行中做的操作来深入了解代码,然后再从这里开始构建。

使用 Python 生成鼓序列

我们将使用 Python 从引导生成一个 MIDI 文件,就像我们在上一节做的那样。

你可以在本章的源代码中的 chapter_02_example_01.py 文件中查看这个示例。源代码中有更多的注释和内容,你应该去看看。

  1. 让我们首先下载捆绑包。magenta.music 包中有很多有用的工具,我们将在许多示例中使用它:
import os
import magenta.music as mm

mm.notebook_utils.download_bundle("drum_kit_rnn.mag", "bundles")
bundle = mm.sequence_generator_bundle.read_bundle_file(
 os.path.join("bundles", "drum_kit_rnn.mag"))
  1. 然后,我们使用鼓组生成器来初始化生成器类,并使用drum_kit配置。我们从自己的包中导入了鼓 RNN 模型,接下来我们会对每个模型做同样的操作:
from magenta.models.drums_rnn import drums_rnn_sequence_generator

generator_map = drums_rnn_sequence_generator.get_generator_map()
generator = generator_map"drum_kit"
generator.initialize()
  1. 通过声明节奏,我们还可以计算小节的长度(单位为秒)。我们需要这个值,因为生成的开始和结束时间是以秒为单位传递给 Magenta 的。

我们首先计算每步的秒数,这等于一分钟的秒数除以每分钟的四分音符数(即节奏),再除以每四分音符的步数。这个值依赖于生成器,但通常等于 4:

from magenta.music import constants

qpm = 120
seconds_per_step = 60.0 / qpm / generator.steps_per_quarter
  1. 接着,我们计算每小节的秒数,这等于每小节的步数乘以我们之前计算的每步的秒数。每小节的步数根据拍号有所不同,但目前我们将使用默认值 16,用于以每四分音符四步采样的 4/4 拍音乐:
num_steps_per_bar = constants.DEFAULT_STEPS_PER_BAR
seconds_per_bar = num_steps_per_bar * seconds_per_step

print("Seconds per step: " + str(seconds_per_step))
print("Seconds per bar: " + str(seconds_per_bar))
  1. 我们现在准备初始化我们的引导序列。我们将使用一个简单的 1 小节爵士鼓序列作为引导(你可以在本书的源代码中,Chapter02 文件夹中的 primers/Jazz_Drum_Basic_1_bar.mid 文件中查看),因此我们需要一个包含 16 个步骤的列表。引导定义将在下一节解释。

我们将已经定义的 QPM 用来将该引导鼓轨道转换为引导序列:

primer_drums = mm.DrumTrack(
 [frozenset(pitches) for pitches in
   [(38, 51),     (), (36,),    (),
    (38, 44, 51), (), (36,),    (),
    (),           (), (38,),    (),
    (38, 44),     (), (36, 51), (),]])
primer_sequence = primer_drums.to_sequence(qpm=qpm)
  1. 我们可以计算引导的时长(单位为秒),因为引导只有 1 小节,所以它的时长仅等于每小节的秒数:
primer_start_time = 0
primer_end_time = primer_start_time + seconds_per_bar
  1. 我们现在计算生成器部分的开始和结束时间。首先,我们定义生成的节拍数为 3,然后从引导的结束开始生成,并在秒数上扩展三小节的时长:
num_bars = 3
generation_start_time = primer_end_time
generation_end_time = generation_start_time + (seconds_per_bar * num_bars)

print("Primer start and end: [" + str(primer_start_time) + ", " 
 + str(primer_end_time) + "]")
print("Generation start and end: [" + str(generation_start_time) + ", " 
 + str(generation_end_time) + "]")
  1. 现在我们可以使用开始和结束时间来配置我们的生成器选项。生成选项还包含温度,我们将其设置为 1.1,以增加一些随机性。生成器接口对于所有模型都是通用的:
from magenta.protobuf import generator_pb2

generator_options = generator_pb2.GeneratorOptions()
generator_options.args['temperature'].float_value = 1.1
generator_options.generate_sections.add(
 start_time=generation_start_time,
 end_time=generation_end_time)
  1. 现在是生成的时候了!你现在可以使用初始序列作为输入来调用生成器的生成方法。该方法的返回值是一个NoteSequence实例:
sequence = generator.generate(primer_sequence, generator_options)
  1. 有许多工具可以将生成的NoteSequence实例转换为其他格式,如 PrettyMidi。我们现在将转换结果,并将文件和图表写入磁盘:
from visual_midi import Plotter

# Write the resulting midi file to the output directory
midi_file = os.path.join("output", "out.mid")
mm.midi_io.note_sequence_to_midi_file(sequence, midi_file)
print("Generated midi file: " + str(os.path.abspath(midi_file)))

# Write the resulting plot file to the output directory
from visual_midi import Plotter
plot_file = os.path.join("output", "out.html")
print("Generated plot file: " + str(os.path.abspath(plot_file)))
pretty_midi = mm.midi_io.note_sequence_to_pretty_midi(sequence)
plotter = Plotter()
plotter.show(pretty_midi, plot_file)
  1. 让我们打开output/out.html文件:

请注意,你的初始序列应该保持不变(因为它是硬编码的),但你生成的 3 个小节应该与这些不同。

  1. 要听生成的 MIDI,请使用你的软件合成器或 MuseScore。对于软件合成器,请根据你的平台使用以下命令,并将PATH_TO_SF2PATH_TO_MIDI替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

将检查点打包为捆绑文件

在上一个示例中,我们看到了捆绑文件的使用。在 Magenta 中,捆绑文件是一种方便的方式,用于将TensorFlow 检查点和元数据打包成一个单独的文件。检查点用于 TensorFlow 中保存训练过程中模型的状态,使得稍后能够轻松地重新加载模型状态。

捆绑文件的另一个好处是,它为多个生成器定义了一个通用接口。你可以查看 Magenta 源代码中的generator.proto文件,该文件位于magenta/protobuf文件夹中,定义了该接口,包括生成器的iddescription,以及生成器选项,如我们将在许多示例中使用的generate_sections,它用来提供生成长度。

这个通用接口涵盖了许多模型,包括第二章和第三章中的所有模型。遗憾的是,第四章的 MusicVAE 模型没有使用捆绑文件,但我们将在第七章 训练 Magenta 模型中看到更多的捆绑文件。

使用 Protobuf 对 MIDI 进行编码在 NoteSequence 中

在上一个示例中,我们看到了一个名为NoteSequence的类的使用,这是 Magenta 中的一个重要部分,因为每个处理乐谱的模型都会使用它来表示一系列 MIDI 音符。NoteSequenceGeneratorOptions是 Protobuf(协议缓冲区)的一部分,Protobuf 是一种语言中立、平台中立的可扩展机制,用于序列化结构化数据。在 Magenta 的源代码中,你可以在magenta/protobuf/music.proto文件中看到NoteSequence的消息定义。

NoteSequence的定义基于 MIDI 消息内容,因此你会看到以下内容:

  • 一个TimeSignature变化的列表:根据 MIDI 标准,默认假设为 4/4。

  • 一个KeySignature变化的列表:根据 MIDI 标准,默认假设为 C 大调。

  • 一个Tempo变化的列表:根据 MIDI 标准,默认假设为 120 QPM。

  • 一个Note变化的列表。

还有更多内容,包括注释、量化信息、音高弯曲和控制变化,但在本书中我们不会深入探讨这些内容。

Note列表是我们主要使用的,它包含了pitch(即 MIDI 音符,基于 MIDI 音准标准)、start_timeend_time属性,表示一个音符。

NoteSequence转换为其他格式以及从其他格式转换为NoteSequence非常重要。在前面的示例中,我们使用了以下函数:

  • magenta.music.midi_io.note_sequence_to_midi_file:用于将音符序列转换为 MIDI 文件。你也可以转换为PrettyMIDI,这是一种在内存中编辑 MIDI 的有用格式。

  • magenta.music.midi_io.midi_file_to_note_sequence:用于将 MIDI 文件转换为音符序列;在我们之前的示例中,这会非常有用。我们本可以使用midi_file_to_note_sequence("primers/Jazz_Drum_Basic_1_bar.mid"),而不是在 Python 代码中硬编码引导。

关于NoteSequence的另一个重要点是,它并没有明确地定义开始和结束;它只是假定从第一个音符的开始开始,到最后一个音符的结束结束。换句话说,不能定义以静音开始或结束的序列。

在下图中,预计序列为 2 小节,共 32 步,但在第 31 步时停止,这意味着最后一个音符的结束时间是 3.875 秒,而不是 4 秒:

将这个序列与另一个序列连接可能会导致意外的结果序列长度。幸运的是,处理音符序列的方法有选项可以确保这正常工作。

将 MIDI 音符映射到现实世界。

在前面的示例中,我们展示了以下的 1 小节引导,但我们并没有解释这些音高对应的内容:

[(38, 51),     (), (36),     (),
 (38, 44, 51), (), (36),     (),
 (),           (), (38),     (),
 (38, 44),     (), (36, 51), (),]

我们不会详细讲解 MIDI 规范,因为它非常庞大(你可以在www.midi.org/specifications查看),但我们会关注与本书相关的部分。有两个规范对我们来说非常有趣:

  • MIDI 规范定义了不同乐器之间的低级通信和编码协议。

    • 通用 MIDI 规范GM)定义了一个更高级别的协议,规定了符合要求的乐器标准,并指定了乐器的声音。

2019 年 1 月标志着 MIDI 规范自 1983 年标准化以来的第一次重大更新,发布了 MIDI 2.0 规范。这是在超过 25 年的使用后,数百万设备和用户使用的成果。

MIDI 2.0 引入了更高的分辨率值,精度为 16 位而非 7 位,并增加了 MIDI 能力查询功能,使得工具之间的集成更加顺畅。MIDI 的新版本与旧版本完全向后兼容。

乐器声音定义对我们来说很有趣,我们将研究GM 1 音色集规范,它定义了每个 MIDI 音符应该播放的声音。在 GM 1 音色集中,每个 MIDI 程序变化(PC#)对应合成器中的一个特定乐器。例如,PC# 1 是原声大钢琴,PC# 42 是中提琴。记住,这些声音是由实现 GM 1 规范的合成器定义的,可能会因合成器不同而有所变化。

打击乐键盘映射有些不同。在 MIDI 通道 10 上,每个 MIDI 音符编号(音高)对应一个特定的鼓声。我们之前的示例可以如下解读:

  • 36:低音鼓 1

  • 38:木质军鼓

  • 44:踏板高帽

  • 51:踩镲 1

你可以随时参考www.midi.org/specifications-old/item/gm-level-1-sound-set上的完整表格。

将打击乐事件编码为类别

上一部分解释了按 MIDI 映射的打击乐映射。那么,我们如何为鼓 RNN 模型编码打击乐呢?为了将 MIDI 映射编码为一个向量,我们将使用所谓的独热编码,这基本上是将每个可能的输入事件映射到类别,然后映射到二进制向量。

为了实现这一点,我们需要首先减少鼓类的数量,将 MIDI 中所有可能的鼓(46 种不同的鼓实在太多)减少到更易管理的 9 个类别。你可以在DEFAULT_DRUM_TYPE_PITCHES属性中看到该映射,它位于magenta.music.drums_encoder_decoder模块中。然后,我们在一个向量中进行按位翻转,索引由将类索引的 2 的幂相加所定义。

例如,我们的音高集合{51, 38},对于第一步映射到类别{8, 1}。这个值会在向量中的索引 258 处按位翻转,因为2⁸ + 2¹ = 258。该向量对于每一步的大小是 2⁹,还包含一些二进制计数器和标志,我们在这里不讨论这些。

这张图展示了初始示例第一步的编码部分,如所述:

在这个特定的编码中,有些信息会丢失,因为类别比 MIDI 音符少。这意味着,例如,如果 MIDI 音符 35 和 36 都映射到相同的类别索引 0,那么 35 和 36 之间的区别就丢失了。在这种特定情况下,36 被任意选择(你实际上可以从上一篇文章《为模型做初步训练(Led Zeppelin)》中的示例看到,MIDI 音符 35 丢失了)。

这种编码用于训练时将数据集转换为序列,在生成过程中,如果使用了引导序列来初始化模型时也会用到。使用引导序列时,引导序列会被编码为模型的输入。然后,模型状态会使用该输入进行初始化。

该操作的逆过程对于生成同样重要:当模型进行新生成时,需要解码以找到它所代表的序列。

有多种方法用于编码事件,而 Magenta 中使用了不同的编码方式。这是该模型的 "drum_kit" 配置的编码方式,使用 LookbackEventSequenceEncoderDecoder,通过二进制计数器实现重复事件的编码。one_drum 配置的编码方式则不同且更简单,你可以在 OneHotEventSequenceEncoderDecoder 中查看。

鼓类的 one-hot 编码实现于 MultiDrumOneHotEncoding 类中,这个类也被其他模型使用,例如我们将在第四章中看到的 MusicVAE 模型。当没有鼓音高被传入时,它将使用我们在本节中看到的 9 类简化鼓编码,这种编码足够表达多种乐器,同时保持模型的可管理性。

我们将在接下来的章节中进一步讨论编码的相关内容。

将 MIDI 文件发送到其他应用程序

虽然生成 MIDI 文件并将其写入磁盘是不错的,但是将 MIDI 音符动态地发送到另一个软件会更有用,这样我们的 Python Magenta 应用程序就能直接与其他音乐软件互动。我们将为此话题专门设立一个章节,因为有很多内容需要讨论。

如果你现在想了解更多关于这个话题的信息,可以去查看第九章,让 Magenta 与音乐应用程序互动,然后再回来这里。

总结

在本章中,我们介绍了 RNN 以及它在音乐生成中的作用,展示了在处理序列和记忆过去信息方面,RNN 是音乐生成的必要属性。

我们还使用命令行中的 Drums RNN 模型生成了一个 MIDI 文件。我们已经涵盖了大部分参数,并学习了如何配置模型的输出。通过查看生成算法,我们解释了它是如何工作的,以及不同的标志如何改变其执行过程。

通过在 Python 中使用 Drums RNN 模型,我们展示了如何构建一个多功能的应用程序。通过这个过程,我们了解了 MIDI 规范、Magenta 如何使用 Protobuf 编码 NoteSequence,以及如何将序列编码为 one-hot 向量。我们还介绍了将生成的 MIDI 发送到其他应用程序的概念,这个话题将在第九章,让 Magenta 与音乐应用程序互动中进一步探讨。

在下一章,我们将使用其他模型来生成旋律。我们还将继续编写 Python 代码,完成对 RNN 的学习。

问题

  1. 如果你想生成一段乐谱,你需要训练你的模型去做什么?

  2. 在音乐预测中,RNN 的哪些特性是有趣的?

  3. 给定一个 RNN 的隐藏层表示h(t + 2),该隐藏层接收到的两个输入是什么?

  4. 给定生成的以下参数,--num_steps 32--qpm 80,生成的 MIDI 文件会有多长时间?它会有多少小节?

  5. 如果在生成阶段增加--branch_factor并提高--temperature,会发生什么?

  6. 在生成 3 步的过程中,使用--branch_factor 4--beam_size 2参数,束搜索算法在最后一次迭代中会经过多少个节点?

  7. Magenta 中用于表示 MIDI 音符序列的 Protobuf 消息类是什么?(NoteSequence)

  8. 使用编码部分描述的独热编码,对于播放 MIDI 音符{36, 40, 42}的一个步骤,其编码向量是什么?

  9. 使用相同的编码,从一个索引为 131 的编码向量中,解码得到的 MIDI 音符是什么?

进一步阅读

第五章:生成和声旋律

在上一章中,我们创建了鼓序列,现在我们可以继续创作音乐的核心——旋律。在本章中,你将学习 长短期记忆LSTM)网络在生成较长序列中的重要性。我们将学习如何使用一个单声部的 Magenta 模型——Melody RNN,它是一个带有回环和注意力配置的 LSTM 网络。你还将学习如何使用两个和声模型:Polyphony RNN 和 Performance RNN,它们都是使用特定编码的 LSTM 网络,后者支持音符的力度和表现力的时值。

本章将覆盖以下主题:

  • 用于长期依赖的 LSTM

  • 使用 Melody RNN 生成旋律

  • 使用 Polyphony RNN 和 Performance RNN 生成和声

技术要求

在本章中,我们将使用以下工具:

  • 启动 Magenta 的 命令行bash 来自终端

  • 使用 Python 及其库来编写使用 Magenta 的音乐生成代码

  • Magenta 用于生成 MIDI 音乐

  • MuseScoreFluidSynth 用于听生成的 MIDI

在 Magenta 中,我们将使用 Melody RNNPolyphony RNNPerformance RNN 模型。我们将深入解释这些模型,但如果你需要更多信息,可以查看 Magenta 源代码中的模型 README(github.com/tensorflow/magenta/tree/master/magenta/models)。你也可以查看 Magenta 的代码,它有很好的文档说明。我们还在最后一节提供了额外的内容,进一步阅读

本章的代码在本书的 GitHub 仓库中的 Chapter03 文件夹,位置在 github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter03。示例和代码片段假设你位于该章节文件夹。在本章中,你在开始之前应该执行 cd Chapter03

查看以下视频,查看代码实际应用:

bit.ly/314KEzq

用于长期依赖的 LSTM

在上一章中,我们学习了 递归神经网络RNNs)在音乐生成中的重要性,因为它们使得可以操作一系列向量并记住过去的事件。这个“记住过去事件”的部分在音乐生成中非常重要,因为过去的事件在定义全局音乐结构中起着重要作用。让我们考虑一个破碎的小九和弦例子,包括“A”、“C”、“E”、“G”和“B”五个音符。为了预测最后一个音符“B”,网络必须记住四个音符之前的事件,才能知道这很可能是一个小九和弦。

不幸的是,随着相关信息和需求点之间的间隔增大,RNN 变得无法学习这些依赖关系。理论上,网络应该能够做到这一点,但实际上,确实很困难。传统 RNN 的两个常见问题是梯度消失问题和梯度爆炸问题,我们将在本节中看到这两个问题。

幸运的是,1997 年引入的 LSTM 网络解决了这个问题。它们是一种特殊类型的 RNN,每个神经元都有一个带有特殊门控的记忆单元。正如前一章中介绍的,Drums RNN 模型就是 LSTM 网络,本章中的所有模型也是如此。现在,让我们看看 LSTM 是如何工作的。

查看 LSTM 记忆单元

LSTM 网络自从发明以来一直很受欢迎,而且有充分的理由:它们是专门为解决我们一直讨论的长期依赖问题而设计的。在 Magenta 中,RNN 模型就是 LSTM 网络。

让我们回顾一下前一章的 RNN 图示。我们将使用相同的图示,但放大其中一个单元并添加一些细节:

我们可以看到,这里重复的模块非常简单:它从前一层获取输出,将其与当前输入进行连接,并使用激活函数(如 tanh、sigmoid 或 ReLU)层来生成该层的输出和下一层的输入。我们还记得,长期信息必须通过所有单元和层按顺序传递,这意味着信息必须在每一步都进行乘法运算。这就是梯度消失问题出现的地方:被多个小数字乘积的值往往会消失。

现在让我们来看看 LSTM 记忆单元是如何设计的:

这里首先要注意的是添加的水平线,注释为{ ..., c(t-1), c(t) , c(t+1), ... },它将单元状态信息传递到前面。单元状态可以通过三个门控来修改——遗忘输入输出。我们不会详细讨论这些门控的工作原理,因为它超出了本书的范围,但我们会看一个实例,展示它在我们用例中的工作方式。

查看最后一节,进一步阅读,其中包含关于 LSTM 的更多参考资料。

让我们以一个破碎的小九和弦为例,来说明门层是如何工作的。网络正在训练,至今已经接收到 "A"、"C"、"E"、"G"、"B",这就是它的当前状态。现在 LSTM 看到了一个新的音符 "C",会发生什么呢?首先,让我们看看 遗忘门层。LSTM 将查看 h(t-1),即上一层的输出,以及 x(t),即当前输入 "C",并为上一层的每个元素 c(t-1) 输出一个值。然后,状态会与该输出相乘,输出值范围在 0 到 1 之间,意味着接近 0 的值会导致该状态丢失这个值,而接近 1 的值则会导致该状态保留这个值。由于输入是 "C",而且在我们的状态中,已经看到了一个完整的和弦,因此网络可能会学习忘记之前的信息,因为我们开始了一个新的和弦。

接下来,输入门层将查看 h(t-1)x(t),并决定对状态做出哪些添加。利用这些信息,遗忘门的输出被更新,产生 c(t)。此时,细胞状态已经有了新的内容,意味着我们的输入 "C" 已经加入到细胞状态中,这对后续层来说是有用的,比如,检测到一个潜在的 C 大调和弦的开始。此时,网络也可能会学习到其他和弦,具体取决于训练数据。经过正确训练的网络将根据其训练学习不同的音乐和弦,并在推理过程中输出相应的预测。

最后,输出门层将通过查看新的状态 c(t)h(t-1)x(t) 来产生输出 h(t)。此时,状态已经更新,不需要进一步的更新。由于我们的模型刚看到一个 "C",它可能会输出一个 "E" 来组成 C 大调和弦。

这是 LSTM 的简化解释,但它有助于理解其在我们使用场景中的工作原理。

查看代码,你可以看到 LSTM 内存单元的使用。在 events_rnn_graph.py 模块中,make_rnn_cell 函数使用了 tf.contrib.rnn.BasicLSTMCell。你可以看到 Magenta 使用了 TensorFlow 作为后台引擎,因为 LSTM 并没有在 Magenta 中定义。

探索替代网络

总结上一节内容,我们有 RNN,它能够处理序列并查看过去的事件,而 LSTM 是 RNN 的一种特定内存单元实现。通常,一个网络可能被称为仅仅是 RNN,但实际上使用了 LSTM 内存单元。

虽然 LSTM 在保持长期信息方面迈出了重要一步,但仍然有可能的改进。另一种类似的记忆单元 门控循环单元GRU)近年来因其更简洁的设计而获得了广泛关注。由于这一点,GRU 的表达能力较弱,这是一个需要注意的权衡。

LSTM 的一个问题是它们运行时需要更多的资源,因为记忆单元需要更多的内存和计算来操作。一种受欢迎的改进方案是引入 注意力机制,使得 RNN 可以关注过去输出的子集,从而在不使用过多记忆单元的情况下查看过去的事件。我们将在 专注于特定步骤 一节中讨论注意力机制。

使用 Melody RNN 生成旋律

在这一节中,我们将通过使用 Python 代码生成音乐,基于上一章的知识,使用新的模型——Melody RNN。本节将展示如何生成单旋律,下一节将展示如何处理多旋律。

单旋律 是最简单的音乐纹理形式,其中音符——旋律——由单一乐器逐个演奏。有时候,旋律可以由多个乐器或多个歌手在不同的八度音高上演奏(例如合唱团中),但仍然被视为单旋律,因为伴奏部分是单旋律的。

多旋律 则由两个或更多旋律线一起演奏。例如,用两只手演奏的钢琴谱就是多旋律的,因为需要同时演奏两条独立的旋律。

乐器可以是单旋律或多旋律。例如,单旋律的合成器一次只能演奏一个音符(如果你按下两个音符,只会有一个音符发出),而多旋律的合成器或经典钢琴则可以同时演奏多个音符。

这是贝多芬 Für Elise 钢琴谱中的一个小单旋律示例,出现在第 37 小节:

你会注意到这里只有一条旋律,音符是逐个演奏的。以下是同一谱子中的一个小多旋律示例,出现在第 25 小节:

在这个示例中,你会看到两条旋律同时演奏,通常是用钢琴的双手演奏。

多旋律听起来是不是有点熟悉?它可能让你联想到打击乐谱,因为打击乐谱本质上是多旋律的,因为多个旋律(如低音鼓、踩镲、军鼓等)一起演奏,形成完整的节奏。然而,接下来我们将在多旋律部分看到的内容有些不同,因为我们需要一种方法来表示跨越多个时值的音符,这与上一章不同。

让我们先写一些代码来生成旋律。

Für Elise 生成一首曲子

在这个示例中,我们将使用 Für Elise 钢琴谱中的一个小片段来生成基于该片段的旋律。这个片段如下所示:

请注意时间签名是 3/8。我们将在后面的 时间的流逝 部分探讨这个问题。

既然你已经知道如何生成一个序列,我们只会提供与之前代码不同的部分;你可以重用上一章写的代码。我们将代码封装在一个generate函数中,便于使用不同的模型和配置调用。

你可以在本章源代码的chapter_03_example_01.py文件中跟随这个示例。源代码中有更多的注释和内容,所以你应该去查看一下。

你可以在该文件中找到generate函数。随着进展,我们将会做更多版本的方法。本示例的引导文件位于primers/Fur_Elisa_Beethoveen_Monophonic.mid

我们将逐步解释generate函数中的重要变化。新的函数签名如下:

from magenta.music import DEFAULT_QUARTERS_PER_MINUTE
from magenta.protobuf.music_pb2 import NoteSequence

def generate(bundle_name: str,
             sequence_generator,
             generator_id: str,
             primer_filename: str = None,
             qpm: float = DEFAULT_QUARTERS_PER_MINUTE,
             total_length_steps: int = 64,
             temperature: float = 1.0,
             beam_size: int = 1,
             branch_factor: int = 1,
             steps_per_iteration: int = 1) -> NoteSequence:

在函数的开始部分,我们可以保留之前的代码,只需要将对 Drums RNN 包、生成器和配置的引用修改为相应的参数——bundle_namesequence_generatorgenerator_id

  1. 首先,我们将处理primer_filename参数,通过之前看到的 MIDI 文件记谱序列函数来处理,如果没有提供引导,则使用空序列:
import magenta.music as mm

if primer_filename:
  primer_sequence = mm.midi_io.midi_file_to_note_sequence(
    os.path.join("primers", primer_filename))
else:
  primer_sequence = NoteSequence()
  1. 接着,我们将处理qpm参数。如果引导序列有节奏,我们将使用它。如果没有,我们将使用提供的qpm参数:
if primer_sequence.tempos:
  if len(primer_sequence.tempos) > 1:
    raise Exception("No support for multiple tempos")
  qpm = primer_sequence.tempos[0].qpm

这引入了NoteSequence消息中的tempos属性,该属性包含一个节奏变化列表。与 MIDI 一样,一个乐谱可以有多个节奏,每个节奏都有一个特定的开始和停止时间。为了简化处理,并且因为 Magenta 不处理多个节奏,我们不会处理多个节奏。

  1. 然后我们改变了计算引导长度的方法。以前这是一个固定值,但现在我们使用total_time(一个序列属性)给出的最后一个音符的结束时间,并将其向上取整到最接近的步长起点。然后,我们从该值开始计算序列长度(以秒为单位):
primer_sequence_length_steps = math.ceil(primer_sequence.total_time
                                         / seconds_per_step)
primer_sequence_length_time = (primer_sequence_length_steps 
                               * seconds_per_step)

结果生成的引导结束时间将是primer_sequence_length_time。请记住,Magenta 处理序列时是以秒为单位的,因此我们始终需要按秒计算时间。

  1. 我们还通过从提供的total_length_steps值中减去引导长度来改变生成长度的计算方法:
generation_length_steps = total_length_steps - primer_sequence_length_steps
generation_length_time = generation_length_steps * seconds_per_step

我们在上一章中使用小节来计算引导和生成长度,而现在,我们使用步长来做同样的事情。这两种方法在不同情况下都很有用,我们想展示这两者的不同。

通常情况下,使用步长更容易计算,因为你不需要担心时间签名,这会导致每小节的步长数量发生变化。

另一方面,使用小节使得制作具有正确时序的开始和结束循环变得更加容易,就像我们在上一章的练习中做的那样。

  1. 我们还可以将beam_sizebranch_factorsteps_per_iteration添加到生成器选项中,如下所示:
generator_options.args['beam_size'].int_value = beam_size
generator_options.args['branch_factor'].int_value = branch_factor
generator_options.args['steps_per_iteration'].int_value = steps_per_iteration
  1. 最后,我们将保存 MIDI 和绘图到磁盘,以便我们可以听到这个序列并展示它。它是与以 <generator_name>_<generator_id>_<date_time>.<format> 模式命名的文件中稍微多一些信息的相同代码,你之前看到的。
# Writes the resulting midi file to the output directory
date_and_time = time.strftime('%Y-%m-%d_%H%M%S')
generator_name = str(generator.__class__).split(".")[2]
midi_filename = "%s_%s_%s.mid" % (generator_name,
                                  generator_id,
                                  date_and_time)
midi_path = os.path.join("output", midi_filename)
mm.midi_io.note_sequence_to_midi_file(sequence, midi_path)
print("Generated midi file: " + str(os.path.abspath(midi_path)))

# Writes the resulting plot file to the output directory
date_and_time = time.strftime('%Y-%m-%d_%H%M%S')
generator_name = str(generator.__class__).split(".")[2]
plot_filename = "%s_%s_%s.html" % (generator_name,
                                   generator_id,
                                   date_and_time)
plot_path = os.path.join("output", plot_filename)
pretty_midi = mm.midi_io.note_sequence_to_pretty_midi(sequence)
plotter = Plotter()
plotter.save(pretty_midi, plot_path)
print("Generated plot file: " + str(os.path.abspath(plot_path)))
  1. 现在我们可以调用我们全新的 generate 方法!让我们用 Melody RNN 模型做一个简单的例子:
from magenta.models.melody_rnn import melody_rnn_sequence_generator

generate(
  "basic_rnn.mag",
  melody_rnn_sequence_generator,
  "basic_rnn",
  primer_filename="Fur_Elisa_Beethoveen_Polyphonic.mid",
  total_length_steps=64)

所以,我们使用了 basic_rnn.mag 预训练的捆绑包与 basic_rnn 配置和 melody_rnn_sequence_generator。我们要求 64 个步骤,这是 4/4 拍的 4 小节。但我们不是说 primer 有 3/8 拍的节拍吗?是的,但生成的序列将是 4/4 拍,因此我们必须基于这个来进行计算。我们将在后面的章节中讨论这个问题,时间的流逝

调用该方法将在 output 目录中生成两个文件,一个是 MIDI .mid 文件,另一个是绘图 .html 文件。

  1. 要听生成的 MIDI,使用你的软件合成器或 MuseScore。对于软件合成器,请参考以下命令,根据你的平台更改 PATH_TO_SF2PATH_TO_MIDI 的正确值:

    • Linux:fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS:fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows:`fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI`

  2. 打开绘图文件,我们会得到类似这样的结果:

如果你听一下,你会发现生成的样本与 primer 中相同调性和类似音符,但 primer 的全局结构丢失了。这是因为 basic_rnn 配置不像回顾配置那样能够学习音乐结构,因为编码的向量不包含步骤位置和重复的音乐步骤。

让我们看看如何通过查看 attention_rnn 和 lookback_rnn 配置来修复它,它们都是带有特定编码的 LSTM。

理解回顾配置

要查看回顾配置的效果,我们将首先使用以下参数生成一个新序列:

generator.generate(
  "lookback_rnn.mag",
  melody_rnn_sequence_generator,
  "lookback_rnn",
  primer_filename="Fur_Elisa_Beethoveen_Monophonic.mid",
  total_length_steps=64,
  temperature=1.1)

你可以看到我们正在使用相同的 melody_rnn_sequence_generator 函数,但是在配置和捆绑文件上有所更改。让我们看一下 lookback 配置的生成样本:

你可以看到这里的第 1 小节和第 3 小节在图表中用 s1s2 注释有一个重复的音乐结构,第 0 小节和第 2 小节也有类似的结构。

如果重复的音乐结构让你感到熟悉,这是因为我们已经见过这个概念——Drums RNN 使用了回顾编码器,即 LookbackEventSequenceEncoderDecoder,与我们在这里使用的相同。在上一章节的编码部分中,我们看到鼓音符被编码为用于 RNN 输入的 one-hot 向量。这里也是同样的情况,但不同的是,这里是旋律被编码为一个 one-hot 向量。

让我们以第二章中提到的图示,使用鼓 RNN 生成鼓序列,并添加更多细节:

我们提供了一个小的示例向量作为例子。该一热编码的索引范围为 16,这意味着我们只能编码 16 个类别。记住,鼓类的编码长度是 512。Melody RNN 模型的basic_rnn配置通过仅映射部分音高将旋律编码为 36 个类别。如果我们想要完整的 127 音高范围,则应使用mono_rnn配置。向量的总长度是 55,因为我们有 3 次 16 类的一热编码,加上一个 5 位的二进制计数器,再加上 2 个回溯标志。

让我们将其拆解成五个部分,并解释向量的组成:

  1. 首先,我们编码当前步进的事件,这是我们在上一章中已经解释过的部分。在示例向量中,编码了事件类别 1,意味着当前步进时播放的是最低音高。

  2. 然后,我们编码下一个步进事件的第一次回溯。那么,什么是回溯呢?当编码器-解码器初始化时,它默认使用【默认每小节的步进数,默认每小节的步进数 * 2】的回溯距离,即[16, 32],对应于 4/4 节拍中最后两个小节的位置。现在,我们正在查看第一次回溯,它是距离当前步进 16 步,或者说 1 个小节之前的步进。编码的事件是第一次回溯的下一个步进。在示例向量中,编码了事件类别 6,意味着 15 步之前播放了相应的音高。

  3. 接下来,我们编码下一个步进事件的第二次回溯,这是距离当前步进 31 步,或者说 2 个小节减去 1 步的位置。在示例向量中,编码了事件类别 8,意味着 31 步之前播放了相应的音高。

  4. 接下来,我们编码小节内的步进位置二进制计数器。这个 5 位向量可以编码从 0 到 15 的值,这就是我们在 4/4 音乐中所拥有的步进范围。这有助于模型通过追踪其在小节中的位置来学习音乐结构。在示例向量中,小节中的位置是第三步。

  5. 最后,我们编码重复回溯标志,它编码当前步进是否重复了第一次或第二次回溯。这有助于判断事件是新的内容还是先前内容的重复。在示例向量中,没有重复。

Magenta 的源代码有很好的文档记录,您可以在magenta.music模块的encoder_decoder.py文件中查看此代码。我们正在查看的类是LookbackEventSequenceEncoderDecoder,而方法是events_to_input

如果你想知道模型是如何配置的,可以去查找配置模块。对于 Melody RNN,搜索 melody_rnn_model.py 文件;你会在这个模块中找到我们在本节中讨论的配置。

这是给模型输入的重要信息,因为它使模型能够保持序列的音乐结构。模型还使用自定义标签来减少模型必须学习表示的信息的复杂性。由于音乐中经常有一小节和两小节的重复结构,模型会根据需要使用自定义标签,例如 repeat-1-bar-agorepeat-2-bar-ago。这使得模型能够更容易地重复这些短语,而不必将它们存储在其记忆单元中。

理解注意力掩码

现在我们了解了回顾配置,让我们来看看注意力配置。我们将从生成一个序列开始,使用以下配置:

generator.generate(
  "attention_rnn.mag",
  melody_rnn_sequence_generator,
  "attention_rnn",
  primer_filename="Fur_Elisa_Beethoveen_Monophonic.mid",
  total_length_steps=128,
  temperature=1.1)

我们正在生成一个更长的 128 步的序列,以便尝试查看音乐结构中的长期依赖关系。让我们看看 注意力 配置生成的示例:

如你所见,在八小节的生成过程中,模型能够在偏离之前跟踪六小节的音乐结构。正如本章第一节中所述,查看 LSTM 记忆单元,注意力模型是相对较新的强大工具,能够记住长期结构。

在 Magenta 中,注意力是通过查看之前的 n 步骤来实现的,使用一种注意力机制。注意力机制的具体工作原理超出了本书的范围,但我们会展示一个示例,以便大致了解它是如何工作的。

首先,计算一个 n 长度的向量,使用之前的 n 步和当前的单元状态。这将告诉我们每一步应该接收多少注意力。通过对其进行归一化处理,我们得到 注意力掩码。例如,当 n 等于 3 时,它可能是 [0.2, 0.8, 0.5],其中第一个元素(0.2)对应于前一步得到的注意力,第二个元素(0.8)是前一步的注意力,依此类推。

然后,我们将前面三步的输出应用注意力掩码。一步的输出,例如 [0.0, 1.0, 1.0, 0.0],表示对一步的编码。看看这个示例:

  • 步骤 1:* [0.0, 1.0, 1.0, 0.0] * 通过应用 0.2(注意力掩码的第一个元素)到每个值,变为 [0.0, 0.2, 0.2, 0.0]

  • 步骤 2:* [0.0, 0.0, 1.0, 0.0] * 通过应用 0.8(注意力掩码的第二个元素)到每个值,变为 [0.0, 0.0, 0.8, 0.0]

  • 步骤 3:* [0.5, 0.0, 0.0, 0.0] * 通过应用 0.5(注意力掩码的第三个元素)到每个值,变为 [0.25, 0.0, 0.0, 0.0]

最后,我们将结果向量相加,得到 [0.25, 0.2, 1.0, 0.0],它对应的是 n 个前期输出,每个输出在不同的比例下作出贡献。然后,将该结果向量与当前步骤的 RNN 输出结合,并应用于下一个步骤的输入。

通过使用注意力机制,我们可以直接将前面输出的信息注入当前步骤的计算中,而不必存储关于单元状态的所有信息。这是一个强大的机制,广泛应用于多种网络类型中。

在 Magenta 中,你可以通过在模型配置中搜索 attn_length 参数来看到注意力机制的使用。如果提供了这个参数,当 RNN 单元被实例化时,会使用注意力包装器。你可以在 events_rnn_graph.py 中的 make_rnn_cell 查看代码:

# Add attention wrapper to first layer.
cell = tf.contrib.rnn.AttentionCellWrapper(cell, attn_length, 
                                           state_is_tuple=True)

注意力的长度将定义训练过程中注意力会考虑的前期输出的步数 (n)。你可以看到,Drums RNN、Melody RNN 和 Improv RNN 都有注意力配置。

例如,要在训练期间将注意力配置更改为 64 步,可以使用 attn_length=64 超参数。

忘记时间

到现在为止,你应该注意到我们失去了初始的 3/8 拍拍号。为了理解 3/8 拍的含义,我们可以回顾一下我们学过的内容。首先,记住我们每个四分音符有 4 步,因为这主要是 Magenta 中的采样率。然后,我们得到以下内容:

  • 4/4 拍中,每小节有 4 步每四分音符,再乘以每小节的 4 四分音符(分子),等于每小节 16 步。

  • 3/4 拍中,每小节有 4 步每四分音符,再乘以每小节的 3 四分音符(分子),等于每小节 12 步。

  • 3/8 拍中,每小节有 2 步每八分音符,再乘以每小节的 3 八分音符(分子),等于每小节 6 步。这是因为八分音符是四分音符的一半时间,所以每八分音符有 2 步。

为什么要研究这个?我们这样做是因为拍号不会改变乐谱中的步骤数或音符数,但它确实会改变其结构。由于 Melody RNN 模型假定了一定的结构,它无法适应新的结构。在我们的例子中,模型假定 4/4 拍有两个原因:

  • 用于表示小节位置的二进制计数器是为 4/4 拍定义的,因为它计算的是从 0 到 15 的一个小节(而不是在 3/8 拍中从 0 到 5)。

  • 模型中的默认回顾长度配置为 [16, 32] 步,这是在 4/4 拍中,1 小节和 2 小节回顾的步数(而不是在 3/8 拍中回顾的 6 步和 12 步)。

这些就是为什么该模型无法理解我们初始音符的拍号,并且会在 4/4 拍中找到结构和重复模式,而不是 3/8 拍的原因。你可能还注意到生成的序列没有拍号,我们默认假设它是 4/4 拍。

节拍对于音乐作品的全球结构和量化非常重要。不同的节拍会改变音符四舍五入到最近音符的方式,因为它会改变步伐的数量。

你可以通过在NoteSequence实例上使用sequence.time_signatures随时获取节拍信息。它返回一个 Protobuf 列表,你可以在该列表上使用add方法,添加并返回一个新的TimeSignature元素。

Magenta 支持任何节拍,但 Magenta 中的所有模型都是在 4/4 拍的节拍下训练的。要在其他节拍下生成序列,我们必须构建一个适当的数据集,创建一个新的配置,并训练模型。有关如何执行此操作的更多信息,请参阅第六章,训练数据准备,以及第七章,Magenta 模型训练

使用 Polyphony RNN 和 Performance RNN 生成和声

现在,我们已经深入讨论了旋律、它们的表示、编码和配置,接下来可以讨论和声。我们将使用两个模型,Polyphony RNN 和 Performance RNN,来生成和声音乐。我们还将研究这种音乐结构的编码,因为它与单声部编码不同。

首先,我们提醒自己,在上一个示例中,我们使用了贝多芬的《致爱丽丝》作为引子。现在我们将使用它的和声音版本,内容如下:

你可以看到引子确实是和声的,因为多个音符同时演奏。你应该知道,在单声部模型中使用和声引子会导致错误。你可以通过使用以下参数调用我们上一节中的generate方法来验证这一点:

generate(
  "basic_rnn.mag",
  melody_rnn_sequence_generator,
  "basic_rnn",
  primer_filename="Fur_Elisa_Beethoveen_Polyphonic.mid",
  total_length_steps=32,
  temperature=0.9)

你会遇到以下错误,因为提取的旋律太多:

Traceback (most recent call last):
 File "/home/Packt/hands-on-music-generation-with-magenta/Chapter03/01.py", line 263, in <module>
 tf.app.run(app)
 ...
 File "/home/Packt/miniconda3/envs/magenta/lib/python3.5/site-packages/magenta/models/melody_rnn/melody_rnn_sequence_generator.py", line 91, in _generate
 assert len(extracted_melodies) <= 1
AssertionError

区分条件和注入

现在,让我们使用我们已经编写的代码——generate函数,并添加一些内容,以便可以使用 Polyphony RNN 模型进行调用:

你可以在本章的源代码中的chapter_03_example_02.py文件中查看这个示例。源代码中有更多的注释和内容,所以你应该去查看它。

你可以在那个文件中找到generate方法。随着我们的深入,我们将不断创建这个方法的更多版本。这个示例的引子位于primers/Fur_Elisa_Beethoveen_Polyphonic.mid

  1. 首先,我们将添加两个特定于此模型的新参数,condition_on_primerinject_primer_during_generation。你可以按如下方式修改generate方法的签名:
from magenta.music import DEFAULT_QUARTERS_PER_MINUTE
from magenta.protobuf.music_pb2 import NoteSequence

def generate(bundle_name: str,
             sequence_generator,
             generator_id: str,
             qpm: float = DEFAULT_QUARTERS_PER_MINUTE,
             primer_filename: str = None,
             condition_on_primer: bool = False,
             inject_primer_during_generation: bool = False,
             total_length_steps: int = 64,
             temperature: float = 1.0,
             beam_size: int = 1,
             branch_factor: int = 1,
            steps_per_iteration: int = 1) -> NoteSequence:
  1. 然后,向生成器选项中添加参数:
generator_options.args['condition_on_primer'].bool_value = (
    condition_on_primer)
generator_options.args['no_inject_primer_during_generation'].bool_value = (
    not inject_primer_during_generation)

小心使用inject_primer_during_generation,它在参数映射中是反向的。

  1. 现在我们可以启动一些生成:
generate(
  "polyphony_rnn.mag",
  polyphony_sequence_generator,
  "polyphony",
  condition_on_primer=True,
  inject_primer_during_generation=False,
  temperature=0.9,
  primer_filename="Fur_Elisa_Beethoveen_Polyphonic.mid")

generate(
  "polyphony_rnn.mag",
  polyphony_sequence_generator,
  "polyphony",
  condition_on_primer=False,
  inject_primer_during_generation=True,
  temperature=0.9,
  primer_filename="Fur_Elisa_Beethoveen_Polyphonic.mid")

我们在这里所做的是一次只激活一个新参数,以观察它对生成序列的影响。

condition_on_primer 参数用于在 RNN 开始生成之前提供引导序列。必须激活该参数才能使引导序列生效。它对于在某个特定的键上开始一个序列很有用。你可以在这个生成过程中看到它的作用:

注意生成的序列是按键生成的。

inject_primer_during_generation 参数会将引导序列注入生成器的输出,这意味着我们基本上会在输出中看到完整的引导序列。你可以在这个生成过程中看到它的作用:

注意生成的序列包含完整的引导序列。你应该尝试不同的值,看看它们对生成序列的影响。

解释多声部编码

现在我们看到一个生成的多声部序列,接下来让我们看看这种类型的序列是如何生成的。首先,我们看一下模块 polyphony_model.py 中的 PolyphonyRnnModel 模型。我们首先注意到该模型没有定义任何新的内容,这意味着生成代码与上一章中的代码是一样的,在 理解生成算法 部分中定义的。

不同之处在于模型使用 PolyphonyOneHotEncoding 对其独热向量的编码方式。现在,多个音符可以同时播放,并且一个音符可以生成多个步骤。

在鼓 RNN 编码中,可以同时敲击多个音符,因为它将多个音符的组合编码为一个特定的事件,但它无法编码生成多个步骤的音符,因为音符没有特定的开始和结束标记。旋律 RNN 编码在这方面也类似。

让我们以之前生成的例子中的前四个步骤来查看这个多声部编码是如何工作的:

在这里,我们看到 5 个音高为 {69, 45, 52, 57, 60} 的音符,分布在 4 个步骤中,其中第一个音符 69 跨越了两个步骤。Polyphony RNN 使用五个不同的事件类来编码这个。对于没有音高的类,用于表示序列的结构,你有 STARTENDSTEP_END。对于带有音高的类,用于表示音符,你有 NEW_NOTECONTINUED_NOTE

让我们尝试编码我们的序列:

START
NEW_NOTE 69
NEW_NOTE 45
STEP_END
CONTINUED_NOTE 69
NEW_NOTE 52
STEP_END
NEW_NOTE 57
STEP_END
NEW_NOTE 60
STEP_END
END

有趣的是,在第二个步骤中要注意音符的延续。另外,音符的结束并没有明确指定;如果在 CONTINUED_NOTE 步骤中,后续步骤中没有出现该事件,音符会结束。这与下一节中介绍的编码方式不同。

这个序列是通过 RNN 生成的多次传递生成的。这与我们之前看到的单声部生成有所不同,因为以前 RNN 生成一个序列步骤只需一步。现在,我们需要大约 5 步 RNN 才能生成一个序列步骤。你可以在控制台输出中看到这一点。对于这个例子,我们有如下内容:

[polyphony_sequence_generator.py:171] Need to generate 40 more steps 
for this sequence, will try asking for 200 RNN steps

使用 Performance RNN 进行表现音乐生成。

既然我们已经掌握了 Polyphony RNN,接下来我们将探讨 Performance RNN,它是一个比 Polyphony RNN 更强大的模型,提供更多选项和预训练模型。首先,让我们来看看不同的预训练包。请记住,预训练包与特定的配置相关。这次,你可以在 performance_model.py 模块中查看不同的配置。

在 Performance RNN 中,使用了一种不同于 Polyphony RNN 的编码方式,新的事件类包括 NOTE_ONNOTE_OFF。这可能听起来很熟悉,因为 MIDI 也是这样编码信息的。

让我们首先看看几个配置:

  • performance 配置支持富有表现力的时序,其中音符不会精确地落在开始和结束的步骤上,从而赋予它更具“人性化”的感觉或“律动感”(我们将在下一章中探讨律动感,使用音乐 VAE 进行潜空间插值)。一个事件类,TIME_SHIFT,用于表示这一点,它定义了时间上的提前。

  • performance_with_dynamics 配置支持音符力度,其中音符不会都以相同的力度演奏。一个事件类,VELOCITY,用于表示这一点。

这两个附加项在生成更接近人类演奏的富有表现力的序列时非常重要。

现在,让我们来看两个更多的配置:

  • density_conditioned_performance_with_dynamics 配置支持密度调节,其中可以修改生成音符的数量。

  • pitch_conditioned_performance_with_dynamics 配置支持音高调节,其中可以控制生成序列的音高分布。

这些配置不会改变编码,但控制生成的执行方式。

对于第一个配置,我们需要记住之前关于 Polyphony RNN 的示例,其中生成一个序列步骤需要多个 RNN 步骤。改变生成选项 notes_per_second 将改变每个序列步骤的 RNN 步骤数,从而减少或增加生成的密度。

对于第二个配置,可以通过生成选项 pitch_class_histogram 提供一个直方图,显示每个音高在一个八度音程中的相对密度。这个直方图是一个包含 12 个值的列表(每个八度有 12 个音符),每个音符对应一个频率值,分别对应 [C, C#, D, D#, E, F, F#, G, G#, A, A#, B]。对于 F 大调音阶,由于 F 出现的频率是其他音符的两倍,直方图将是: [1, 0, 1, 0, 1, 2, 0, 1, 0, 1, 0, 1]

你可以在本章源代码中的 chapter_03_example_03.py 文件中看到这个示例的实际应用。我们在这里不讨论代码,因为它与前两个示例类似。

为了学习表现力的时序和动态,这些模型已在雅马哈电子钢琴比赛的真实钢琴表演数据上进行了训练(你可以在www.piano-e-competition.com/midiinstructions.asp找到它们)。

生成像人类一样的表现力时序

这是使用 Performance RNN 的生成示例,使用了预训练模型density_conditioned_performance_with_dynamics,并且参数设置为notes_per_second=8。我们仅展示生成的部分,这是引导部分后的四小节:

你会注意到几点:

  • 首先,音符的高度并不完全相同。这是因为我们可以让 Visual MIDI 根据音符的力度来缩放音符的高度。音符越大,声音越响。请记住,MIDI 中的力度范围是从 0 到 127。例如,第一个音符的音高为 71,力度为 77。

  • 其次,音符并不直接落在小节的划分上——它们开始和结束时略微偏离,稍早或稍晚于步伐的边界。这是因为模型使用了TIME_SHIFT事件,并且它在由人类演奏者演奏的数据集上进行了训练,该数据集包含了这种节奏感。这非常有趣,并且与我们之前的工作有所不同:我们不再生成乐谱;我们在生成的是一种演奏。

生成量化的乐谱或生成有节奏感的演奏,各有其特定的用途,因此你需要根据目标来决定哪种方法最适合。由于生成序列的表演性质,将文件打开在音乐符号软件(如 MuseScore)中可能会显得有些杂乱。

总结

在这一章中,我们探讨了使用单声部和多声部模型生成旋律。

我们首先从查看 LSTM 单元和它们在 RNN 中的应用开始,LSTM 可以使用遗忘、输入和输出门长时间保持信息。

然后,我们使用 Melody RNN 生成了旋律,使用了多个预训练模型,如基础模型、回溯模型和注意力模型。我们发现基础模型无法学习重复结构,因为它的输入向量编码中不包含此类信息。接着我们查看了回溯编码,其中小节中的步态位置和重复结构被编码进输入向量,从而使模型能够学习这些信息。最后,我们看到了注意力模型,在该模型中,注意力机制使得模型能够查看多个前置步骤,使用注意力掩码为每一步赋予权重。

最后,我们使用 Polyphony RNN 和 Performance RNN 生成了多声部音乐。在前者模型中,我们学习了如何使用开始和持续事件将多声部音乐编码为向量。在后者模型中,我们学习了另一种多声部编码方法,使用音符开启和音符关闭事件,类似于 MIDI 使用的方式。在 Performance RNN 中,我们还学习了表现力的生成,包括时序和力度变化。

正如我们现在所知,表现性的时序感使得音乐具有人的感觉,其中音符不会落在预定的时间点上。这就是我们有时所说的 groove,我们将在下一章《使用音乐 VAE 进行潜在空间插值》中进一步探讨这个主题。我们还将研究乐谱插值,它使得从一个乐谱平滑过渡到另一个乐谱成为可能。

问题

  1. RNN 在学习过程中遇到的主要问题是什么?LSTM 提供了哪些解决方案?

  2. LSTM 记忆单元的一个更简单的替代方案是什么?它们的优缺点是什么?

  3. 你想配置 Melody RNN 的回溯编码器-解码器,以学习 3/4 拍子的结构。二进制步进计数器的大小是多少?对于 3 个回溯距离,回溯距离是如何配置的?

  4. 你得到了以下结果向量,* [0.10, 0.50, 0.00, 0.25]*,这是应用了注意力掩码 [0.1, 0.5] 后得到的,且 n = 2,之前的第 1 步为 [1, 0, 0, 0],第 2 步为 [0, 1, 0, x]。那么 x 的值是多少?

  5. 你有以下的 Polyphony RNN 编码:{ (START), (NEW_NOTE, 67), (NEW_NOTE, 64), (NEW_NOTE, 60), (STEP_END), (CONTINUED_NOTE, 67), (CONTINUED_NOTE, 64), (CONTINUED_NOTE, 60), (STEP_END), (CONTINUED_NOTE, 67), (CONTINUED_NOTE, 64), (CONTINUED_NOTE, 60), (STEP_END), (CONTINUED_NOTE, 67), (CONTINUED_NOTE, 64), (CONTINUED_NOTE, 60), (STEP_END), (END) }。这里播放的是什么?

  6. 在 Polyphony RNN 编码中,结束音高为 56 的音符会使用什么事件?在 Performance RNN 中又是怎样的?

  7. 生成的音符中,有哪两个组成部分能使其更具人类感觉?你会使用什么模型和参数来实现这一点?

  8. 在使用 notes_per_seconds 参数时,density_conditioned_performance_with_dynamics 模型会对生成算法产生什么影响?

深入阅读

第六章:使用 MusicVAE 进行潜在空间插值

在本章中,我们将了解变分自编码器VAEs)的连续潜在空间的重要性,以及它在音乐生成中的重要性,相较于标准的自编码器AEs)。我们将使用 Magenta 中的 MusicVAE 模型,这是一个层次递归的 VAE,来生成序列并在它们之间进行插值,从而实现平滑地从一个序列过渡到另一个序列。接着,我们将看到如何使用 GrooVAE 模型为现有序列添加律动或人性化处理。最后,我们将看看用于构建 VAE 模型的 TensorFlow 代码。

本章将涵盖以下主题:

  • VAEs 中的连续潜在空间

  • 使用 MusicVAE 和 GrooVAE 进行乐谱转换

  • 理解 TensorFlow 代码

技术要求

在本章中,我们将使用以下工具:

  • 命令行bash用来从终端启动 Magenta

  • 使用Python及其库编写音乐生成代码,配合 Magenta 使用

  • 使用Magenta生成 MIDI 音乐

  • MuseScoreFluidSynth用来播放生成的 MIDI

在 Magenta 中,我们将使用MusicVAEGrooVAE模型。我们将深入讲解这些模型,但如果你觉得需要更多信息,可以参考 Magenta 源代码中的模型 README(github.com/tensorflow/magenta/tree/master/magenta/models),这是一个很好的起点。你还可以查看 Magenta 的代码,它有很好的文档记录。我们还在最后一节进一步阅读中提供了额外的内容。

本章的代码可以在本书的 GitHub 代码库中的Chapter04文件夹里找到,地址是github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter04。示例和代码片段假设你已进入本章的文件夹。在开始之前,你应进入cd Chapter04

查看以下视频,观看代码实战:bit.ly/3176ylN

VAEs 中的连续潜在空间

在第二章,使用 Drums RNN 生成鼓点序列中,我们展示了如何使用 RNN(LSTM)和束搜索来迭代地生成序列,通过输入并逐个音符地预测下一个最可能的音符。这使我们能够使用引导音作为生成的基础,设置一个起始旋律或某个特定的调性。

使用这种技术是有益的,但也有其局限性。如果我们希望从一个起点开始,探索围绕它的变化,而不仅仅是随机地变化,而是沿着特定方向变化,那该怎么办呢?例如,我们可能有一个用于低音线的两小节旋律,并希望听到它作为琶音演奏时的效果。另一个例子是平滑地在两段旋律之间过渡。这正是我们之前看到的 RNN 模型的不足之处,也是 VAE 的优势所在。

在深入了解 VAE 及其在 MusicVAE 中的实现之前,让我们先介绍一下标准的 AE。

标准 AE 中的潜在空间

AE 网络是一对连接的网络,包含一个编码器和一个解码器,其中编码器从输入中生成一个嵌入,而解码器会尝试重现该嵌入。嵌入是输入的密集表示,其中无用的特征已被去除,但仍然足够具有代表性,以便解码器能够尝试重建输入。

如果解码器仅仅尝试重现输入,那么编码器和解码器的组合有什么用呢?它的主要用途是降维,在降维的过程中,输入可以以较低的空间分辨率(即更少的维度)表示,同时仍然保持其含义。这迫使网络发现重要的特征并将其编码在隐藏层节点中。

在下图中,我们展示了一个 VAE 网络,它分为三个主要部分——中间的隐藏层节点(潜在空间或潜在变量)、左侧的编码器和右侧的解码器:

关于网络训练,损失函数被称为重构损失,定义为网络在创建与输入不同的输出时受到惩罚。

生成是通过实例化潜在变量来实现的,潜在变量生成嵌入,然后解码该嵌入以产生新的输出。不幸的是,AE 学习到的潜在空间可能不是连续的,这是该架构的一个重大缺点,限制了其在实际应用中的使用。非连续的潜在空间意味着随机采样一个点可能会导致解码器无法理解的向量。这是因为编码器尚未学会如何处理该特定点,无法从其他学习中进行泛化。

在下图中,黑色的点由?标记,位于这样的空间中,意味着编码器无法从中重构输入。这是潜在空间样本(针对三个类别)的可视化示例,轴表示潜在空间的前两个维度,颜色表示三个类别,显示了不同簇的形成:

如果你只是复制输入,那这样做没问题,但如果你想从潜在空间中采样或者在两个输入之间进行插值呢?在图示中,你可以看到黑色数据点(用问号表示)落在一个解码器无法理解的区域。这就是自编码器(AEs)中不连续的潜在空间对于我们的应用场景是一个问题的原因。

现在,让我们来看一下变分自编码器(VAE)是如何解决这个问题的。

使用 VAE 生成音乐

VAE 有一个特性使其在生成音乐(或任何生成任务)时非常有用,那就是它们的潜在空间是连续的。为了实现这一点,编码器并不是输出一个向量,而是输出两个向量:一个表示均值的向量,称为µ(mu),以及一个表示标准差的向量,称为σ(sigma)。因此,潜在变量,通常按惯例称为z,遵循一个概率分布P(z),通常是高斯分布。

换句话说,向量的均值控制输入编码应该位于哪里,标准差控制其周围区域的大小,使得潜在空间变得连续。以之前的例子为例,潜在空间的一个示意图,其中x轴和y轴表示前两个维度,三个由不同颜色表示的类别,你可以看到这些簇现在覆盖了一个区域,而不是离散的:

这里是 VAE 网络,你可以看到隐藏层中µ和σ的变化:

这种网络架构在生成音乐方面非常强大,通常被认为属于一种被称为生成模型的模型类别。此类模型的一个特性是生成过程是随机的,这意味着对于一个给定的输入(以及相同的均值和标准差),每次采样都会使编码略有变化。

这个模型有多个非常适合音乐生成的特性,以下是其中一些:

  • 表达性:一个音乐序列可以映射到潜在空间,并从中重建。

  • 现实性:潜在空间中的每一个点都代表一个现实的例子。

  • 平滑性:来自附近点的样本是相似的。

在本章中,我们将进一步讲解 VAE,但这段简短的介绍对理解我们即将编写的代码非常重要。

使用 MusicVAE 和 GrooVAE 的乐谱变换

在前面的章节中,我们学习了如何生成乐谱的各个部分。我们已经生成了打击乐和单声部、复声部旋律,并了解了表现性时值。本节内容在此基础上进行扩展,展示了如何操作和转换生成的乐谱。在我们的示例中,我们将从潜在空间中采样两个小乐谱,接着我们将在这两个样本之间进行插值(逐步从第一个样本过渡到第二个样本),最后我们将为生成的乐谱添加一些律动(或称人性化,更多信息请参考下方信息框)。

对于我们的示例,我们将重点研究打击乐,因为在 MusicVAE 中,只有打击乐能添加律动。我们将在 MusicVAE 中使用不同的配置和预训练模型来执行以下步骤。请记住,Magenta 中有比我们在此展示的更多预训练模型(参见第一部分,技术要求,了解包含所有模型的 README 链接):

  • 样本:通过使用cat-drums_2bar_small配置和预训练模型,我们从潜在空间中采样了两个不同的 2 小节乐谱。对于旋律,我们可以通过使用cat-mel_2bar_big配置来做同样的操作。

  • 插值:通过使用相同的配置,我们可以在两个生成的乐谱之间进行插值。插值的意思是它会逐步改变乐谱,从第一个样本过渡到第二个样本。通过请求不同数量的输出,我们可以决定在两个样本之间过渡的渐变程度。

  • 律动:通过使用groovae_2bar_humanize配置,我们可以通过加入律动来使之前的 16 小节序列更具人性化。

下面是一个解释我们示例中不同步骤的图示:

首先,我们将采样sample1sample2(每个 2 小节)。然后,我们会请求插值生成 4 个输出序列(“i1”,“i2”,“i3”和“i4”),每个序列为 2 小节。最终生成的 6 个输出序列总共 12 小节,包含了两端的 2 个输入序列,以及中间 6 个序列的乐谱进展。最后,我们将为整个序列添加律动。

如果你记得上一章中的Performance music with the Performance RNN部分,我们介绍了什么是律动人性化,以及如何生成感觉不那么机械的序列。这归结为两点:表现性时值和动态。前者改变了音符的时值,使其不完全落在节拍边界上,后者则改变了每个音符的力度(即速度),以模拟人类演奏乐器的感觉。

在接下来的内容中,我们将进一步解释这些配置。如果你想尝试旋律的示例而不是打击乐,只需将cat-drums_2bar_small替换为cat-mel_2bar_big。我们稍后也会在本章中介绍其他模型,包括旋律模型。

初始化模型

在进行采样、插值和旋律时,我们需要初始化将要使用的模型。你首先会注意到,MusicVAE 没有像前几章那样的接口;它有自己独特的接口和模型定义。这意味着到目前为止我们写的代码不能复用,除了一些像 MIDI 和图表文件处理的内容。

你可以在本章源代码中的chapter_04_example_01.py文件中查看这个示例。源代码中有更多的注释和内容,所以你应该去查看一下。

预训练的 MusicVAE 模型不像前几章那样被打包成捆绑包(.mag文件)。现在,模型和配置对应的是一个检查点,它的表现形式略逊于捆绑包。我们已经简要地解释了检查点是什么,接下来会在第七章《训练 Magenta 模型》中详细探讨这一点。现在记住,检查点是用于 TensorFlow 中保存训练过程中模型状态的,使得我们能够在之后轻松地重新加载模型的状态:

  1. 首先我们来实现一个download_checkpoint方法,下载与模型对应的检查点:
import os
import tensorflow as tf
from six.moves import urllib

def download_checkpoint(model_name: str,
                        checkpoint_name: str,
                        target_dir: str):
  tf.gfile.MakeDirs(target_dir)
  checkpoint_target = os.path.join(target_dir, checkpoint_name)
  if not os.path.exists(checkpoint_target):
    response = urllib.request.urlopen(
      f"https://storage.googleapis.com/magentadata/models/"
      f"{model_name}/checkpoints/{checkpoint_name}")
    data = response.read()
    local_file = open(checkpoint_target, 'wb')
    local_file.write(data)
    local_file.close()

你不必过于担心这个方法的细节;基本上,它是从在线存储中下载检查点。它类似于我们在前几章中使用的magenta.music.notebook_utils中的download_bundle方法。

  1. 我们现在可以编写一个get_model方法,使用检查点实例化 MusicVAE 模型:
from magenta.models.music_vae import TrainedModel, configs

def get_model(name: str):
  checkpoint = name + ".tar"
  download_checkpoint("music_vae", checkpoint, "bundles")
  return TrainedModel(
    # Removes the .lohl in some training checkpoints
    # which shares the same config
    configs.CONFIG_MAP[name.split(".")[0] if "." in name else name]
    # The batch size changes the number of sequences 
    # to be run together
    batch_size=8,
    checkpoint_dir_or_path=os.path.join("bundles", checkpoint))

在这个方法中,我们首先使用download_checkpoint方法下载给定模型名称的检查点。然后,我们用检查点batch_size=8实例化magenta.models.music_vae中的TrainedModel类。这个值定义了模型同时处理多少个序列。

批处理大小过大会导致浪费开销;批处理大小过小则会导致多次传递,可能会使整个代码运行得更慢。与训练过程中不同,批处理大小不需要太大。在这个示例中,采样使用两个序列,插值使用两个序列,而人性化代码使用六个序列,因此如果我们想挑剔一下,完全可以修改batch_size来匹配每个调用。

对于TrainedModel的第一个参数,我们传入一个Config的实例。每个模型在models/music_vae/configs.py文件中都有对应的配置。如果你查看该文件的内容,你可能会认出一些我们已经看到的内容。例如,拿CONFIG_MAP中的配置cat-drums_2bar_small来举例,这是我们将在采样时使用的配置。

现在,按照 data_converter 属性的参考,你将进入一个名为 DrumsConverter 的类,位于 models.music_vae.data 中。在 __init__ 方法中,你可以看到我们之前在第二章中讨论过的类和方法,使用 Drums RNN 生成鼓序列,这些类和方法也用于 DrumsRNN 模型,例如我们在 将打击乐事件编码为类 部分解释的 MultiDrumOneHotEncoding 类。

MusicVAE 代码在我们之前看到的内容的基础上构建,添加了一个新的层,使得能够将音符序列转换为 TensorFlow 张量。我们将在理解 TensorFlow 2.0 代码部分更详细地探讨 TensorFlow 代码。

采样潜空间

现在我们可以下载并初始化我们的 MusicVAE 模型,我们可以进行采样(类似于生成)序列。回想我们在上一节关于 VAE 的内容,我们知道我们可以通过实例化与概率分布相对应的潜变量,并解码嵌入,来采样潜空间中的任何一点。

到目前为止,我们一直在使用生成这个术语来表示创建一个新的序列。这个术语指的是我们在第二章中描述的生成算法,使用 Drums RNN 生成鼓序列,该算法也在第三章中使用,生成复音旋律

现在我们使用采样这个术语来表示创建一个新的序列。这指的是采样的行为(因为我们实际上是在采样一个概率分布)并且与我们之前描述的生成算法不同。

编写采样代码

现在,让我们为示例编写第一个方法,sample 方法:

  1. 首先,让我们定义这个方法,它接受一个模型名称作为输入,并返回一个包含两个生成的 NoteSequence 对象的列表:
from typing import List
from magenta.protobuf.music_pb2 import NoteSequence

from utils import save_midi, save_plot

def sample(model_name: str,
           num_steps_per_sample: int) -> List[NoteSequence]:
  model = get_model(model_name)

  # Uses the model to sample 2 sequences
  sample_sequences = model.sample(n=2, length=num_steps_per_sample)

  # Saves the midi and the plot in the sample folder
  save_midi(sample_sequences, "sample", model_name)
  save_plot(sample_sequences, "sample", model_name)

  return sample_sequences

在这个方法中,我们首先使用之前的 get_model 方法实例化模型。然后我们调用 sample 方法,请求返回 n=2 个序列。我们保持默认的温度(所有模型的默认值为 1.0),但也可以通过 temperature 参数进行更改。最后,我们使用来自前一章的 save_midisave_plot 方法分别保存 MIDI 文件和绘图文件,这些方法位于 utils.py 文件中。

  1. 让我们调用我们创建的采样方法:
num_bar_per_sample = 2
num_steps_per_sample = num_bar_per_sample * DEFAULT_STEPS_PER_BAR
generated_sample_sequences = sample("cat-drums_2bar_small.lokl",
                                    num_steps_per_sample)

你可能已经注意到,预训练模型 cat-drums_2bar_small.lokl 具有 .lokl 后缀。还有一个 .hikl 模型,表示训练过程中使用的 KL 散度。我们将在下一节通过 KL 散度细化损失函数中解释这个内容。

在之前的代码片段中,num_bar_per_samplenum_steps_per_sample分别定义了每个样本的条数和步骤数。我们使用的配置cat-drums_2bar_small是一个小型的 9 类鼓组配置,类似于我们在第二章中看到的配置。对于我们的示例,我们将使用 32 个步骤(2 小节)。

  1. 打开output/sample/music_vae_00_TIMESTAMP.html文件,将TIMESTAMP替换为控制台中打印的值。这里是我们要处理的第一个生成样本:

注意,我们在 Visual MIDI 中启用了力度输出,这意味着音符没有填满整个垂直空间,因为 Magenta 中的默认力度是 100(记住 MIDI 值从 0 到 127)。由于我们稍后会添加节奏,因此需要查看音符的力度。

  1. 打开output/sample/music_vae_01_TIMESTAMP.html文件,将TIMESTAMP替换为控制台中打印的值。这里是第二个生成的样本:

  1. 要听生成的 MIDI,请使用您的软件合成器或 MuseScore。对于软件合成器,根据您的平台使用以下命令,并将PATH_TO_SF2PATH_TO_MIDI替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

我们现在有两个 2 小节的样本可以使用;接下来我们将在这两个样本之间进行插值。

使用 KL 散度优化损失函数

在前面的代码片段中,您可能已经注意到我们使用的cat-drums_2bar_small.lokl检查点后缀为lokl。这是因为该配置有两个不同的训练检查点:loklhikl。第一个检查点已针对更真实的采样进行了训练,而第二个检查点则是为了更好的重构和插值训练的。我们在之前的代码中使用了第一个检查点进行采样,接下来我们将在下一节使用第二个检查点进行插值。

那么loklhikl到底是什么意思呢?它们分别表示Kulback-LeiblerKL)散度。KL 散度用于衡量两个概率分布的差异。通过我们之前的示例,我们可以展示我们希望最小化 KL 散度,以便在插值过程中实现平滑效果:

这是一个潜在空间样本的可视化示例(针对 3 个类别),其中轴表示潜在空间的前两个维度,颜色表示 3 个类别。在左侧,我们有相互接近的编码,能够实现平滑的插值。在右侧,我们有更加分散的聚类,这意味着插值会更加困难,但可能会生成更好的样本,因为这些聚类更加明确。

KL 损失函数会将所有 KL 散度与标准正态分布进行求和。单独的 KL 损失会导致围绕先验(接近 0 的圆形斑点)生成一个随机集群,这本身并没有太大用处。通过结合重构损失函数和 KL 损失函数,我们能够实现相似编码的聚类,这些聚类密集地围绕潜在空间的原点。

你可以查看 Magenta 代码中MusicVAE类的模型损失函数实现,位置在magenta.models.music_vae包中的_compute_model_loss函数。

在训练过程中,KL 散度会通过超参数free_bitsmax_beta进行调节。通过增加 KL 损失的效果(即减小free_bits或增大max_beta),你将得到一个能够生成更好随机样本但在重构方面表现较差的模型。

从潜在空间的相同区域进行采样

采样的一个有趣之处是,我们可以在同一个批次中重用相同的z变量来生成每个序列。这对于从潜在空间的相同区域生成序列非常有用。例如,要使用相同的z变量生成 2 个 64 步(4 小节)的序列,我们将使用以下代码:

sample_sequences = model.sample(n=2, length=64, same_z=True)

从命令行进行采样

你还可以通过命令行调用模型采样。本节中的示例可以通过以下命令行来调用:

> curl --output "checkpoints/cat-drums_2bar_small.lokl.tar" "https://storage.googleapis.com/magentadata/models/music_vae/checkpoints/cat-drums_2bar_small.lokl.tar"
> music_vae_generate --config="cat-drums_2bar_small" --checkpoint_file="checkpoints/cat-drums_2bar_small.lokl.tar" --mode="sample" --num_outputs="2" --output_dir="output/sample"

在两个样本之间插值

我们现在有了 2 个生成的样本,并且想要在它们之间插值,插入 4 个中间序列,最终生成一个连续的 6 个 2 小节的序列,总共是 12 小节的序列。

获取正确的序列长度

在我们的示例中,我们在调用模型的sample方法时使用了length=32,因此该方法的返回值是每个 2 小节的序列。你应该知道,序列长度在 MusicVAE 中很重要,因为每个模型处理的序列长度不同——cat-drums_2bar_small处理的是 2 小节的序列,而hierdec-mel_16bar处理的是 16 小节的序列。

在采样时,Magenta 不会报错,因为它可以生成一个更长的序列,然后将其截断。但是在插值过程中,你会遇到像这样的异常,意味着你没有请求正确数量的步数:

Traceback (most recent call last):
...
  File "/home/Packt/miniconda3/envs/magenta/lib/python3.5/site-packages/magenta/models/music_vae/trained_model.py", line 224, in encode
    (len(extracted_tensors.inputs), note_sequence))
magenta.models.music_vae.trained_model.MultipleExtractedExamplesError: Multiple (2) examples extracted from NoteSequence: ticks_per_quarter: 220

在 MusicVAE 中,异常信息特别难懂,而且编码器非常挑剔,所以我们会尽量列出常见的错误及其相关的异常。

编写插值代码

现在我们来为示例编写第二种方法,即interpolate方法:

  1. 首先,定义该方法,它接收两个NoteSequence对象的列表作为输入,并返回一个 16 小节的插值序列:
import magenta.music as mm

def interpolate(model_name: str,
                sample_sequences: List[NoteSequence],
                num_steps_per_sample: int,
                num_output: int,
                total_bars: int) -> NoteSequence:
  model = get_model(model_name)

  # Use the model to interpolate between the 2 input sequences
  interpolate_sequences = model.interpolate(
      start_sequence=sample_sequences[0],
      end_sequence=sample_sequences[1],
      num_steps=num_output,
      length=num_steps_per_sample)

  save_midi(interpolate_sequences, "interpolate", model_name)
  save_plot(interpolate_sequences, "interpolate", model_name)

  # Concatenates the resulting sequences into one single sequence
  interpolate_sequence = mm.sequences_lib.concatenate_sequences(
      interpolate_sequences, [4] * num_output)

  save_midi(interpolate_sequence, "merge", model_name)
  save_plot(interpolate_sequence, "merge", model_name,               
            plot_max_length_bar=total_bars,
            bar_fill_alphas=[0.50, 0.50, 0.05, 0.05])

  return interpolate_sequence

我们首先实例化模型,然后使用start_sequenceend_sequence参数分别调用interpolate方法,第一个和最后一个样本的输出序列数量为 6,使用num_steps参数(小心,它与步骤中的序列长度无关),并且length参数设置为 2 小节(以步骤为单位)。插值结果是一个包含六个NoteSequence对象的列表,每个对象包含 2 小节。

然后,我们使用magenta.music.sequence_lib中的concatenate_sequences将列表中的元素连接成一个 12 小节的单一NoteSequence对象。第二个参数([4] * num_output)是一个包含每个元素时间(以秒为单位)的列表。我们应该记住,这是必要的,因为NoteSequence没有定义开始和结束,所以一个以静音结束的 2 小节序列与另一个 2 小节序列连接后,不会得到一个 4 小节的序列。

调用interpolate方法时,如果输入序列没有量化,或者输入序列为空,例如,可能会出现NoExtractedExamplesError异常。记住,你还必须请求正确的长度,否则会收到MultipleExtractedExamplesError

  1. 然后我们可以调用interpolate方法:
num_output = 6
total_bars = num_output * num_bar_per_sample
generated_interpolate_sequence = \
interpolate("cat-drums_2bar_small.hikl",
             generated_sample_sequences,
             num_steps_per_sample,
             num_output,
             total_bars)
  1. 让我们打开output/merge/music_vae_00_TIMESTAMP.html文件,将TIMESTAMP替换为控制台中打印的值。对应我们的样本,我们得到了这个插值序列:

我们为每两个小节标记了不同的背景透明度。你可以在前一节中找到我们生成的第一个样本,它位于 0 到 4 秒之间,背景较暗。然后,4 个新的插值块可以位于 4 到 20 秒之间。最后,你可以看到第二个输入样本,它位于 20 到 24 秒之间。

  1. 要听生成的 MIDI,请使用你的软件合成器或 MuseScore。对于软件合成器,根据你的平台,使用以下命令并将PATH_TO_SF2PATH_TO_MIDI替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

在两个序列之间进行插值是一个困难的问题,但 MusicVAE 做得很好,我们的示例结果相当令人印象深刻。你应该尝试其他长度的生成,并听一听它们。

从命令行进行插值

你也可以从命令行调用插值操作。本节中的示例可以使用以下命令行调用(你需要自己下载检查点):

> curl --output "checkpoints/cat-drums_2bar_small.hikl.tar" "https://storage.googleapis.com/magentadata/models/music_vae/checkpoints/cat-drums_2bar_small.hikl.tar" > music_vae_generate --config="cat-drums_2bar_small" --checkpoint_file="checkpoints/cat-drums_2bar_small.hikl.tar" --mode="interpolate" --num_outputs="6" --output_dir="output/interpolate" --input_midi_1="output/sample/SAMPLE_1.mid" --input_midi_2="output/sample/SAMPLE_2.mid"

通过更改 SAMPLE_1.midSAMPLE_2.mid 文件名为之前采样部分中的文件,你将能够在两个序列之间进行插值。

人性化序列

最后,我们将为生成的序列添加人性化(或groove)。groove 模型是 GrooVAE(发音为 groovay)的一部分,并且已经包含在 MusicVAE 的代码中。

编写人性化代码

现在让我们编写我们示例中的最后一个方法,groove 方法:

  1. 首先,让我们定义这个方法,它接受 NoteSequence 作为输入并返回一个人性化的序列:
def groove(model_name: str,
           interpolate_sequence: NoteSequence,
           num_steps_per_sample: int,
           num_output: int,
           total_bars: int) -> NoteSequence:
  model = get_model(model_name)

  # Split the sequences in chunks of 4 seconds
  split_interpolate_sequences = mm.sequences_lib.split_note_sequence(
      interpolate_sequence, 4)

  # Uses the model to encode the list of sequences
  encoding, mu, sigma = model.encode(
      note_sequences=split_interpolate_sequences)

  # Uses the model to decode the encoding
  groove_sequences = model.decode(
      z=encoding, length=num_steps_per_sample)

  groove_sequence = mm.sequences_lib.concatenate_sequences(
      groove_sequences, [4] * num_output)

  save_midi(groove_sequence, "groove", model_name)
  save_plot(groove_sequence, "groove", model_name,
            plot_max_length_bar=total_bars, show_velocity=True,
            bar_fill_alphas=[0.50, 0.50, 0.05, 0.05])

  return groove_sequence

首先,我们下载模型。然后,我们将序列分割成 4 秒的块,因为模型需要 2 小节的块才能处理。接着,我们调用 encode 函数,再调用 decode 函数。不幸的是,模型中目前还没有 groove 方法。

encode 方法接受一个序列列表,它将对这些序列进行编码,返回 encoding 向量(也叫做 z 向量或潜在向量)、musigma。我们在这里不会使用 musigma,但为了清晰起见,我们保留了它们。编码数组的最终形状是 (6, 256),其中 6 是分割序列的数量,256 是在模型中定义的编码大小,在稍后的部分“构建隐藏层”中会详细解释。

对于 interpolate 方法,如果序列没有正确构建,调用 encode 方法可能会抛出异常。

然后,decode 方法接受前一个 encoding 值和每个样本的步数,并尝试重现输入,结果是一个包含 6 个 2 小节的人性化序列的列表。

最后,我们像插值代码片段一样连接这些序列。

  1. 让我们试着调用 groove 方法:
generated_groove_sequence = groove("groovae_2bar_humanize",
                                   generated_interpolate_sequence,
                                   num_steps_per_sample,
                                   num_output,
                                   total_bars)

返回的序列,generated_groove_sequence,是我们这个示例的最终序列。

  1. 让我们打开 output/groove/music_vae_00_TIMESTAMP.html 文件,将 TIMESTAMP 替换为控制台中打印的值。对应我们的插值序列,我们有这个人性化的序列:

让我们看看生成的图表文件。首先,音符的音量现在是动态的,例如,音符在标记节拍的开始或结束时会更响亮,就像一个真实的鼓手一样。你可以在 20 到 24 秒之间的低音鼓上看到一个例子。然后,注意到音符是以富有表现力的节奏播放的,这意味着音符并不完全落在节奏的开始和结束上。最后,一些音符不再播放,而其他一些音符被添加到生成的乐谱中。

  1. 要聆听生成的 MIDI,请使用你的软件合成器,但不要使用 MuseScore,因为它在处理富有表现力的节奏时会遇到困难,可能会听到与实际乐谱不同的音符。对于软件合成器,请根据你的平台使用以下命令,并替换 PATH_TO_SF2PATH_TO_MIDI 为正确的路径:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

若要了解更多关于 groove 和人性化的信息,可以参考最后一节,Further reading,它在 GrooVAE 博客文章和 GrooVAE 论文中有详细解释。

从命令行进行人性化处理

不幸的是,目前无法从命令行调用人性化方法。我们将在第九章《让 Magenta 与音乐应用互动》中看到其他的序列人性化方法,Making Magenta Interact with Music Applications

对旋律进行更多插值

在之前的章节中,我们对鼓乐序列进行了采样和插值。通过稍微修改代码,我们也可以对旋律进行同样的操作。不幸的是,由于 GrooVAE 模型是基于打击乐数据训练的,你无法对旋律进行人性化处理:

你可以在本章源代码中的chapter_04_example_02.py文件中找到这个例子。源代码中有更多注释和内容,建议你去查看。

  1. 为了实现这个,我们修改了调用代码,保持sampleinterpolate方法不变。我们将生成一个稍长的序列,进行 10 次插值,而不是 6 次。以下是代码(警告:检查点的大小为 1.6GB):
num_output = 10
num_steps_per_sample = num_bar_per_sample * DEFAULT_STEPS_PER_BAR
total_bars = num_output * num_bar_per_sample

generated_sample_sequences = sample("cat-mel_2bar_big",
                                    num_steps_per_sample)
interpolate("cat-mel_2bar_big",
            generated_sample_sequences,
            num_steps_per_sample,
            num_output,
            total_bars)

你会注意到,我们在采样和插值时都使用了cat-mel_2bar_big配置。

  1. 我们通过替换TIMESTAMP为适当的值,打开生成的output/merge/cat-mel_2bar_big_00_TIMESTAMP.html文件。生成的输出如下所示:

  1. 要收听生成的 MIDI,可以使用你的软件合成器或 MuseScore。对于软件合成器,请根据你的平台使用以下命令,并将PATH_TO_SF2PATH_TO_MIDI替换为正确的路径:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

采样整个乐队

在前面的章节中,我们已经对鼓乐和旋律进行过采样和插值。现在,我们将同时采样一组三重奏(打击乐、旋律和低音),并使用一个较大的模型。这也许是最令人印象深刻的模型之一,因为它可以一次生成较长的 16 小节序列,并使用多种互相协调的乐器:

你可以在本章源代码中的chapter_04_example_03.py文件中找到这个例子。源代码中有更多注释和内容,建议你去查看。

  1. 在这个例子中,我们使用sample方法,并将hierdec-trio_16bar预训练模型名称作为参数传入(警告:检查点的大小为 2.6GB):
sample("hierdec-trio_16bar", num_steps_per_sample)
  1. 打开生成的 output/sample/hierdec-trio_16bar_00_TIMESTAMP.html 文件,将 TIMESTAMP 替换为正确的值。生成的输出如下所示:

在 Visual MIDI 中使用 coloring=Coloring.INSTRUMENT 参数,我们可以为每个乐器设置不同的颜色。由于低音线与鼓线在相同的音高上,因此难以阅读,但你可以在图表中看到这三种乐器。

  1. 要收听生成的 MIDI,请使用你的软件合成器或 MuseScore。对于软件合成器,根据你的平台使用以下命令,并将 PATH_TO_SF2PATH_TO_MIDI 替换为正确的值:

    • Linux: fluidsynth -a pulseaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • macOS: fluidsynth -a coreaudio -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

    • Windows: fluidsynth -g 1 -n -i PATH_TO_SF2 PATH_TO_MIDI

你可以听到生成的 MIDI 包含三种乐器,并且你的合成器应该为每个轨道分配不同的乐器音色(通常是钢琴、低音和鼓)。这是 Magenta 中唯一能够同时生成多个乐器的预训练模型,查看第一部分,技术要求,获取 README 的链接,里面列出了所有可用的预训练模型。

该模型的有趣之处在于,16 小节序列的长期结构通过一种特殊类型的解码器 HierarchicalLstmDecoder 得以保持。该架构在潜在编码和解码器之间增加了一个额外层,称为 指挥器,它是一个 RNN,每个小节的输出会产生一个新的嵌入。解码器层然后继续解码每个小节。

要了解更多关于层次化编码器和解码器架构的信息,可以参考最后一部分,进一步阅读,获取关于该主题的更多信息,这在 MusicVAE 博客文章和 MusicVAE 论文中有详细说明。

其他预训练模型的概述

我们已经看到许多在 MusicVAE 中预训练的模型,还有一些有趣的模型,但在这里无法深入探讨。请记住,你可以在 README 中找到它们的完整列表,查看第一部分,技术要求,获取链接。

这是我们发现一些有趣的模型概述:

  • nade-drums_2bar_full 模型是一个类似于我们示例的鼓类预训练模型,但使用了来自 General MIDI 的 61 类,而不是 9 类。不过该模型较大。你可以在 magenta.models.music_vae 模块中的 data.py 文件中查看编码的类及其对应的内容。

  • groovae_2bar_tap_fixed_velocity预训练模型可以将“tap”模式转换为完整的鼓节奏,同时保持相同的 groove。 “tap”序列是从另一个节奏中获取的序列,甚至是通过用手指敲击桌子获取的序列。换句话说,它是一个带有 groove 的单音序列,可以转换为鼓模式。使用它的方法是从真实乐器中录制低音部,然后“tap”节奏(或从音频中转换),然后将其馈送到网络中,以采样与低音部相符的鼓模式。

  • groovae_2bar_add_closed_hh预训练模型在现有的 groove 上添加或替换闭合的 hi-hat。

理解 TensorFlow 代码。

在本节中,我们将快速查看 TensorFlow 代码,以更深入地了解采样、插值和人性化代码的工作原理。这也将参考本章节的第一部分,“VAE 中的连续潜在空间”,以便我们能够理解理论和我们已经进行的实际操作。

但首先,让我们先看一下模型初始化代码的概述。在本节中,我们将以cat-drums_2bar_small配置为例,并使用本章节已经介绍过的相同模型初始化代码,即batch_size为 8。

构建 VAE 图。

我们将从models.music_vae.trained_model模块中的TrainedModel构造函数开始。通过从我们已经在前一节“初始化模型”中介绍过的配置映射中获取z_sizeenc_rnn_sizedec_rnn_size的配置值,我们可以找到关于编码器的 RNN、隐藏层和解码器的 RNN 的相关信息。

注意编码器是BidirectionalLstmEncoder,解码器是CategoricalLstmDecoder,都来自magenta.models.music_vae.lstm_models模块。

使用 BidirectionalLstmEncoder 构建编码器。

让我们首先看看编码器的 RNN,在magenta.models.music_vae.lstm_models模块的BidirectionalLstmEncoder类中初始化,在build方法中,编码层的初始化如下:

lstm_utils.rnn_cell(
    [layer_size],
    hparams.dropout_keep_prob,
    hparams.residual_encoder,
    is_training)

magenta.models.music_vae.lstm_utils模块中的rnn_cell方法中,可以看到层是LSTMBlockCell(来自tensorflow.contrib.rnn模块),具有 512 个单元和一个 dropout 包装器:

cell = rnn.LSTMBlockCell(rnn_cell_size[i])
cell = rnn.DropoutWrapper(cell, input_keep_prob=dropout_keep_prob)

magenta.models.music_vae.data模块中的DrumsConverter类(在configs.py文件中实例化)中,可以看到我们使用了相同的MutltiDrumOneHotEncoding类,这个类我们在第二章中已经解释过:

self._oh_encoder_decoder = mm.MultiDrumOneHotEncoding(
    drum_type_pitches=[(i,) for i in range(num_classes)])

旋律配置将使用OneHotMelodyConverter类。

构建一个使用 CategoricalLstmDecoder 的解码器。

然后,让我们看看解码器的 RNN 初始化,在magenta.models.music_vae.lstm_models模块的BaseLstmDecoder类中,在build方法中,解码层的初始化如下:

self._output_layer = layers_core.Dense(
    output_depth, name='output_projection')
self._dec_cell = lstm_utils.rnn_cell(
    hparams.dec_rnn_size, hparams.dropout_keep_prob,
    hparams.residual_decoder, is_training)

在这里,output_depth将是 512。输出层被初始化为一个密集层,后面接着两层 256 单元的LSTMBlockCell

你还可以在执行过程中通过控制台查看当前配置的编码器和解码器信息:

INFO:tensorflow:Building MusicVAE model with BidirectionalLstmEncoder, CategoricalLstmDecoder:
INFO:tensorflow:Encoder Cells (bidirectional): units: [512]
INFO:tensorflow:Decoder Cells: units: [256, 256]

构建隐藏层

最后,隐藏层的初始化位于magenta.models.music_vae.base_model模块中的MusicVAE类的encode方法中:

mu = tf.layers.dense(
    encoder_output,
    z_size,
    name='encoder/mu',
    kernel_initializer=tf.random_normal_initializer(stddev=0.001))
sigma = tf.layers.dense(
    encoder_output,
    z_size,
    activation=tf.nn.softplus,
    name='encoder/sigma',
    kernel_initializer=tf.random_normal_initializer(stddev=0.001))

return ds.MultivariateNormalDiag(loc=mu, scale_diag=sigma)

musigma层与之前的encoder_output值密集连接,形状为(8, 256),其中 8 对应于batch_size,256 对应于z_size。该方法返回MultivariateNormalDiag,这是一个以musigma为参数的正态分布:

查看sample方法

现在让我们看一下位于models.music_vae.trained_model模块的TrainedModel类中的sample方法。该方法的核心如下:

for _ in range(int(np.ceil(n / batch_size))):
  if self._z_input is not None and not same_z:
    feed_dict[self._z_input] = (
        np.random.randn(batch_size, z_size).astype(np.float32))
  outputs.append(self._sess.run(self._outputs, feed_dict))
samples = np.vstack(outputs)[:n]

该方法会将所需样本的数量n拆分为最大batch_size的小批量,然后使用randn从标准正态分布中采样大小为(8, 256)z_input,最后使用这些值运行模型。记住,z是嵌入,因此我们在这里做的基本上是实例化潜在变量,然后对其进行解码。

回想我们在上一节看到的内容——从潜在空间的相同区域进行采样,我们知道如果我们复用相同的z变量,z可能只会被采样一次。

然后,通过调用样本的单热解码,将样本转换回序列:

self._config.data_converter.to_items(samples)

查看interpolate方法

TrainedModel类中的interpolate方法非常简短:

_, mu, _ = self.encode([start_sequence, end_sequence], assert_same_length)
z = np.array([_slerp(mu[0], mu[1], t)
              for t in np.linspace(0, 1, num_steps)])
return self.decode(
    length=length,
    z=z,
    temperature=temperature)

我们在这里做的是对开始和结束序列进行编码,并从编码中仅返回mu值,利用它实例化z,然后对z进行解码,得到插值序列的结果列表。

那么,那个实例化z_slerp方法是什么呢?“slerp”代表“球形线性插值”,它计算第一个序列和第二个序列之间的方向,从而使插值能够在潜在空间中沿正确的方向移动。

我们不需要过于担心 slerp方法的实现细节;我们只需记住“标准自编码器中的潜在空间”这一部分的图示,该图示展示了在潜在空间中朝某个特定方向移动将导致从一个序列到另一个序列的过渡。通过沿着该方向定期解码,我们最终得到一个逐步从一个序列过渡到另一个序列的结果。

查看groove方法

最后,让我们来看看我们的groove方法。提醒一下,groove方法在 Magenta 中没有,因此我们不得不自己编写它:

encoding, _, _ = model.encode(split_interpolate_sequences)
groove_sequences = model.decode(encoding, num_steps_per_sample)

除了变量命名之外,这段代码与interpolate方法几乎相同,但它不是使用µ值来实例化潜在变量以朝某个方向移动,而是直接对序列进行编码,然后通过模型进行解码。

总结

在这一章中,我们研究了如何使用变分自编码器以及 MusicVAE 和 GrooVAE 模型来采样、插值和人性化音乐乐谱。

我们首先解释了在 AE 中什么是潜在空间,以及如何在编码器和解码器对中使用降维方法来强制网络在训练阶段学习重要特征。我们还了解了 VAE 及其连续的潜在空间,使得我们可以从空间中的任何一点进行采样,并且能够在两个点之间平滑地插值,这两者在音乐生成中都非常有用。

然后,我们编写了代码来采样并转换一个序列。我们学习了如何从一个预训练的检查点初始化模型,采样潜在空间,在两个序列之间进行插值,并且使序列更具人性化。在这个过程中,我们了解了 VAE 的一些重要信息,比如损失函数的定义和 KL 散度。

最后,我们查看了 TensorFlow 代码,以理解 VAE 图的构建过程。我们展示了编码器、解码器和隐藏层的构建代码,并解释了各层的配置和形状。我们还详细讲解了采样、插值和 Groove 方法的实现。

本章标志着关于生成符号数据的模型内容的结束。在前几章中,我们深入探讨了生成和处理 MIDI 的最重要的模型。下一章,使用 NSynth 和 GANSynth 生成音频,将讨论生成亚符号内容,如音频。

问题

  1. AE 中编码器和解码器对的主要用途是什么?这种设计的主要缺点是什么?

  2. AE 中损失函数是如何定义的?

  3. VAE 相比 AE 的主要改进是什么?这一改进是如何实现的?

  4. KL 散度是什么,它对损失函数有什么影响?

  5. 如何使用批量大小为 4 且z大小为 512 的代码来采样z

  6. slerp方法在插值过程中有什么作用?

进一步阅读

第七章:使用 NSynth 和 GANSynth 进行音频生成

在本章中,我们将探讨音频生成。我们将首先概述 WaveNet,这是一种现有的音频生成模型,尤其在语音合成应用中效率较高。在 Magenta 中,我们将使用 NSynth,这是一个 WaveNet 自编码器模型,用于生成可以作为伴奏 MIDI 乐谱的音频片段。NSynth 还支持音频变换,如缩放、时间拉伸和插值。我们还将使用 GANSynth,这是基于生成对抗网络GAN)的更快速方法。

本章将涵盖以下主题:

  • 了解 WaveNet 和音乐的时间结构

  • 使用 NSynth 进行神经音频合成

  • 使用 GANSynth 作为生成乐器

技术要求

在本章中,我们将使用以下工具:

  • 使用命令行Bash从终端启动 Magenta

  • 使用Python及其库编写音乐生成代码,利用 Magenta

  • 使用Magenta生成音频片段

  • 使用Audacity编辑音频片段

  • 使用任何媒体播放器播放生成的 WAV 文件

在 Magenta 中,我们将使用NSynthGANSynth模型。我们会深入解释这些模型,但如果你觉得需要更多信息,可以查看 Magenta 源代码中的模型 README 文件(github.com/tensorflow/magenta/tree/master/magenta/models),这是一个很好的起点。你还可以查看 Magenta 的代码,该代码文档完善。此外,我们还在进一步阅读部分提供了额外的内容。

本章的代码位于本书的 GitHub 仓库中的Chapter05文件夹,地址为github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter05。示例和代码片段假设你已经位于本章的文件夹中。为了开始本章的内容,请先执行cd Chapter05

请观看以下视频,查看代码示范:

bit.ly/37QgQsI

了解 WaveNet 和音乐的时间结构

在前几章中,我们一直在生成符号内容,如 MIDI。在本章中,我们将探讨生成非符号内容,如原始音频。我们将使用波形音频文件格式(WAVE 或 WAV,存储在.wav文件中),这是一种包含未压缩音频内容的格式,可以在几乎所有平台和设备上使用。有关波形的更多信息,请参见第一章,Magenta 与生成艺术简介

使用神经网络生成原始音频是近年来的一个成就,源于 2016 年发布的 WaveNet 论文,A Generative Model For Raw Audio。其他网络架构在音频生成中也表现良好,比如 SampleRNN,2016 年同样发布并被用于制作音乐曲目和专辑(见 databots 的示例)。

正如第二章中所述,使用 DrumsRNN 生成鼓声序列,卷积架构在音乐生成中相对罕见,因为它们在处理序列数据时存在不足。WaveNet 使用了一堆因果卷积层来解决这些问题,这在某种程度上类似于递归层。

建模原始音频是困难的——你需要处理每秒 16,000 个样本(至少),并在更大的时间尺度上跟踪整体结构。WaveNet 的实现经过优化,能够处理这些数据,通过使用膨胀卷积,卷积滤波器通过跳过输入值一定的步长应用于一个较大的区域,从而使网络能够在整个网络中通过仅使用少数几层来保持输入分辨率。在训练过程中,预测可以并行进行,而在生成过程中,预测必须按顺序进行,或者逐个样本地生成。

WaveNet 架构在语音合成应用中表现优异,并且最近在音乐生成中也取得了很好的效果,但其计算开销非常大。Magenta 的 NSynth 模型是一个WaveNet 自回归模型,这种方法用于保持一致的长期结构。让我们来看看 NSynth 及其在生成音乐中的重要性。

查看 NSynth 和 WaveNet 自编码器

NSynth 模型可以看作是一个神经合成器——与其拥有一个可以定义包络线并指定振荡器波形、音高和力度的合成器,不如拥有一个生成新的、真实的乐器声音的模型。NSynth 是以乐器为导向的,或者说是以音符为导向的,这意味着它可以用来生成某个生成乐器的单个音符。

NSynth 是一个 WaveNet 风格的自编码器,它学习输入数据的时间嵌入。要理解 WaveNet 自编码器(AE)网络,可以参考第四章中解释的概念,使用 MusicVAE 进行潜在空间插值,因为这两个网络都是自编码器。在这里,你会看到我们之前展示的许多概念,比如编码、潜在空间和插值。

这里是 WaveNet AE 网络的简化视图:

首先,编码器看到整个输入,即整个单声道波形(.wav格式),经过 30 层计算后,通过平均池化计算出一个时域嵌入(图中的z),该嵌入具有 16 个维度,每 512 个样本计算一次,这是 32 倍的维度压缩。例如,一个包含 16,000 个样本(1 秒的音频,采样率为 16,000)的音频输入,经过编码后,其潜在向量的维度为 16,时间维度为 16,000/512(参见下一节编码 WAV 文件,其中有示例)。然后,WaveNet 解码器将使用 1x1 卷积将嵌入上采样至其原始时间分辨率,尽可能精确地重现输入的声音。

你可以在magenta.models.nsynth.wavenet.h512_bo16模块的Config类中看到 WaveNet 的实现。用于synthesize方法的 fastgen 实现位于FastGenerationConfig类中。

z表示,或潜在向量,具有与我们在第四章中看到的类似的特性,Latent Space Interpolation with MusicVAE—相似的声音具有相似的z表示,且可以在两个潜在向量之间进行混合或插值。这为声音探索创造了无尽的可能性。传统的音频混音围绕着改变两个音频片段的音量,使它们同时播放,而将两个编码混合在一起则是创造一个由两种原始声音混合而成的声音。

在本章节中,你将听到很多生成的声音,我们建议你多听这些声音,而不仅仅是查看声谱图。你可能会注意到这些声音有一种颗粒感或低保真的质感。这是因为模型使用的是经过μ-law 编码的 8 位 16 kHz 声音,这些声音的质量低于你通常听到的声音,这是出于计算原因的需要。

由于训练的原因,模型在重建音频时有时可能会出现不足,导致额外的谐波、近似或奇怪的声音。虽然这些结果令人惊讶,但它们为生成的音频增添了一种有趣的转折。

在本章节中,我们将使用 NSynth 生成音频片段,然后可以用之前生成的 MIDI 序列进行排列。例如,我们将听到猫声和低音声之间的插值声音,通过将两段音频的编码相加并合成结果。我们会生成一些音频组合,以便感受音频插值的可能性。

使用常数-Q 变换谱图可视化音频

在我们开始之前,我们将介绍一种音频可视化图谱,称为Constant-Q TransformCQT)频谱图。我们将在最后一节进一步阅读中提供更多关于绘制音频信号和 CQT 的资料。在前几章中,我们一直用钢琴卷轴图来表示 MIDI,且这些表示方式简单易懂。另一方面,音频的表示较为复杂:两幅几乎相同的频谱图可能听起来却不同。

在第一章,Magenta 与生成艺术简介用频谱图表示音乐部分中,我们展示了频谱图是时间与频率的图示。在本章中,我们将查看 CQT 频谱图,这是一种通过强度表示幅度、通过颜色表示瞬时频率的频谱图。颜色代表嵌入的 16 个不同维度。线条的强度与功率谱的对数幅度成比例,颜色则由相位的导数决定,使相位以彩虹色的形式呈现,因此 Magenta 团队将其称为“彩虹图”。

对于这一部分,我们提供了四个音频样本,用于我们的示例,并以彩虹图形式展示。像往常一样,这些图形无法替代聆听音频内容。这些样本显示在下图中:

在截图中,你可以注意到几点。首先,长笛和低音的频谱图有着相当清晰的谐波系列。其次,金属的频谱图则显得更加混乱,因为它是金属板被敲击的声音。你可以清晰地看到声音的攻击部分,以及随后的噪声覆盖整个频率范围。

在我们的示例中,我们将组合这些声音的每一对,例如金属和猫咪,猫咪和长笛。

NSynth 数据集

在我们开始之前,我们先简要了解一下用于训练 NSynth 模型的 NSynth 数据集。该数据集可以在magenta.tensorflow.org/datasets/nsynth找到,是一个高质量且大规模的数据集,比其他类似数据集大一个数量级。即使它在使用 NSynth 进行训练时可能有些困难,但从其内容来看非常有趣:超过 30 万个按来源、家族和质量分类的音符。它也可以作为生成音频片段的内容。

音频片段的长度均为 4 秒(音符持续了 3 秒,释放音符用了 1 秒),并代表了不同乐器的单一音符。每个音符在标准 MIDI 钢琴范围的 21 到 108 之间的每个音高上都已录制,且以五种不同的力度进行了录制。

由于乐器是按声音来源分类的,即声音的产生方式(例如声学、电子或合成),因此可以将数据集拆分,以便针对特定的乐器来源进行训练。例如,我们将要使用的预训练 GANSynth 模型,acoustic_only,对于生成更经典类型的声音非常有用,因为训练集中的乐器种类繁多。乐器还按家族分类,如钢琴和低音,以及按音质分类,如明亮、阴暗和打击音。

有趣的是,像 NSynth 数据集这样专注于单个音符的数据集在生成音符的神经音频合成中非常有用,这些音符又可以与 Magenta 中的其他模型一起进行排序。从这个角度看,NSynth 模型非常适合 Magenta 生态系统。

使用 NSynth 进行神经音频合成

在这一部分,我们将把不同的音频片段结合在一起。我们将学习如何对音频进行编码,并可选择将结果编码保存到磁盘,再对其进行混合(添加),然后解码添加后的编码以检索音频片段。

我们将只处理 1 秒钟的音频片段。这样做有两个原因:首先,处理音频的成本很高,其次,我们想要生成乐器音符,以短音频片段的形式呈现。后者对我们很有趣,因为我们可以使用我们在前几章中使用的模型生成的 MIDI 来对音频片段进行排序。从这个角度看,你可以将 NSynth 视为一个生成性乐器,而将之前的模型,如 MusicVAE 或 Melody RNN,视为生成性乐谱(曲谱)作曲器。结合这两个元素,我们可以生成完整的音轨,包含音频和结构。

要生成音频片段,我们将使用fastgen模块,这是 Magenta 的一个外部贡献,目前已集成到 NSynth 的代码中,优化了通过易于使用的 API 快速生成音频的功能。

选择 WaveNet 模型

Magenta 提供了两个包含权重的预训练 NSynth 模型。我们将使用 WaveNet 预训练模型。这个模型的训练非常昂贵,使用 32 个 K40 GPU 需要大约 10 天时间,因此我们在这里不讨论训练。

你可以在本章源代码中的chapter_05_example_01.py文件中参考这个示例。源代码中有更多的注释和内容,所以你应该去查看它。

本章还包含sounds文件夹中的音频片段,你可以在这一部分使用它们。

要下载预训练模型,使用以下方法,它会下载并提取模型:

import os
import tarfile
import tensorflow as tf
from six.moves import urllib

def download_checkpoint(checkpoint_name: str,
                        target_dir: str = "checkpoints"):
  tf.gfile.MakeDirs(target_dir)
  checkpoint_target = os.path.join(target_dir, f"{checkpoint_name}.tar")
  if not os.path.exists(checkpoint_target):
    response = urllib.request.urlopen(
      f"http://download.magenta.tensorflow.org/"
      f"models/nsynth/{checkpoint_name}.tar")
    data = response.read()
    local_file = open(checkpoint_target, 'wb')
    local_file.write(data)
    local_file.close()
    tar = tarfile.open(checkpoint_target)
    tar.extractall(target_dir)
    tar.close()

这段代码下载合适的检查点并将其提取到目标目录。这类似于我们在上一章中编写的download_checkpoint。使用wavenet-ckpt检查点名称,得到的检查点可以通过checkpoints/wavenet-ckpt/model.ckpt-200000路径使用。

请注意,这种方法可能会下载较大的文件(本章的预训练模型较大),所以程序看起来可能会卡住一段时间。这只是意味着文件正在本地下载(仅下载一次)。

编码 WAV 文件

首先,我们将使用 fastgen 库来编码 WAV 文件。我们定义 encode 方法并加载音频:

from typing import List
import numpy as np
from magenta.models.nsynth import utils
from magenta.models.nsynth.wavenet import fastgen

def encode(wav_filenames: List[str],
           checkpoint: str = "checkpoints/wavenet-ckpt/model.ckpt-200000",
           sample_length: int = 16000,
           sample_rate: int = 16000) -> List[np.ndarray]:
  # Loads the audio for each filenames
  audios = []
  for wav_filename in wav_filenames:
    audio = utils.load_audio(os.path.join("sounds", wav_filename),
                             sample_length=sample_length,
                             sr=sample_rate)
    audios.append(audio)

  # Encodes the audio for each new wav
  audios = np.array(audios)
  encodings = fastgen.encode(audios, checkpoint, sample_length)

  return encodings

在前面的代码中,我们首先通过 magenta.models.nsynth.utils 模块中的 load_audio 方法加载 wav_filenames 参数中的每个音频文件。要加载音频,两个参数非常重要,sample_lengthsample_rate

  • 采样率设置为 16000,这是底层模型使用的采样率。记住,采样率是指每秒钟音频的离散采样数。

  • 采样长度可以通过将所需的秒数与采样率相乘来计算。以我们的示例为例,我们将使用 1 秒钟的音频片段,采样长度为 16,000。

我们首先将 audios 列表转换为 ndarray,然后传递给 encode 方法,形状为 (4, 16000),因为我们有 4 个样本,每个样本 16,000 个采样点。来自 magenta.models.nsynth.wavenetencode 方法返回提供的音频片段的编码。返回的编码形状为 (4, 31, 16),其中 4 表示元素数量,31 表示时间,16 表示潜在向量的大小,z

你可能会想知道为什么 encodings 中的时间长度为 31。记住,我们的模型将每 512 个采样点减少为 16 个(参见 Looking at NSynth and WaveNet autoencoders 部分),但我们的采样数 16,000 并不能被 512 整除,所以最后得到的是 31.25。这也会影响解码,导致生成的 WAV 文件长度为 0.992 秒。

另一个需要注意的重要点是,所有编码都是一次性计算的,采用相同的批量(批量大小通过在 encode 方法中取 audios.shape[0] 来定义),这比逐个计算要更快。

可视化编码

编码可以通过绘制图表进行可视化,横坐标为时间,纵坐标为编码值。图中的每一条曲线代表一个 z 维度,具有 16 种不同的颜色。以下是我们示例中每个编码声音的示意图:

你可以使用本章代码中的 audio_utils.py 文件中的 save_encoding_plot 绘图方法来生成编码图。

保存编码以便后续使用

一旦计算出编码,保存和加载它们是一个好习惯,因为这将加快程序的运行速度,即使程序中较长的部分仍然是合成部分。

你可以在本章的源代码中的 audio_utils.py 文件中找到这段代码。源代码中有更多的注释和内容,你可以去查看。

为了保存编码,我们使用 NumPy .npy 文件,如下所示:

import os
import numpy as np

def save_encoding(encodings: List[np.ndarray],
                  filenames: List[str],
                  output_dir: str = "encodings") -> None:
  os.makedirs(output_dir, exist_ok=True)
  for encoding, filename in zip(encodings, filenames):
    filename = filename if filename.endswith(".npy") else filename + ".npy"
    np.save(os.path.join(output_dir, filename), encoding)

你可以看到,我们在这里使用了numpy模块中的save方法。我们通过以下方式使用load方法从文件中获取编码:

def load_encodings(filenames: List[str],
                   input_dir: str = "encodings") -> List[np.ndarray]:
  encodings = []
  for filename in filenames:
    encoding = np.load(os.path.join(input_dir, filename))
    encodings.append(encoding)
  return encodings

然后我们可以使用返回的编码,而不是调用fastgen.encode(...)。现在我们已经准备好了编码,接下来我们将看到如何将它们混合在一起。

通过在潜在空间中移动混合编码

现在我们已经拥有了音频文件的编码,可以将它们混合在一起。混合这个术语在音频制作中很常见,通常指的是将两种声音叠加并调整音量,使两者都能清晰地听到。但在这里,我们做的不是这种操作;我们实际上是在相加这些声音,产生一个新的声音,而不仅仅是它们的简单叠加。

为此,我们定义一个mix_encoding_pairs方法:

def mix_encoding_pairs(encodings: List[np.ndarray],
                       encodings_name: List[str]) \
    -> Tuple[np.ndarray, List[str]]:
  encodings_mix = []
  encodings_mix_name = []
  # Takes the pair of encodings two by two
  for encoding1, encoding1_name in zip(encodings, encodings_name):
    for encoding2, encoding2_name in zip(encodings, encodings_name):
      if encoding1_name == encoding2_name:
        continue
      # Adds the encodings together
      encoding_mix = encoding1 + encoding2 / 2.0
      encodings_mix.append(encoding_mix)
      # Merges the beginning of the track names
      if "_" in encoding1_name and "_" in encoding2_name:
        encoding_name = (f"{encoding1_name.split('_', 1)[0]}_"
                         f"{encoding2_name.split('_', 1)[0]}")
      else:
        encoding_name = f"{encoding1_name}_{encoding2_name}"
      encodings_mix_name.append(encoding_name)
  return np.array(encodings_mix), encodings_mix_name

关键部分是encoding1 + encoding2 / 2.0,在这里我们将两个编码相加,产生一个新的编码,稍后我们会合成这个编码。在方法的其余部分,我们对编码进行两两迭代,为每对编码产生一个新的混合编码,且不计算样本与自身的混合,最终方法返回 12 个元素。

我们还保持名称前缀为<encoding-prefix-1>_<encoding-prefix-2>格式,以便在保存 WAV 文件时更好地识别它们(我们使用_字符进行分割,因为 Freesound 中的样本有这个唯一 ID 分隔符)。

最后,我们返回包含混合编码的ndarray以及与编码对应的名称列表。

将混合编码合成 WAV 格式

最后,我们定义synth方法,它接收编码并将其转换为声音:

def synthesize(encodings_mix: np.ndarray,
               encodings_mix_name: List[str],
               checkpoint: str = "checkpoints/wavenet-ckpt/model.ckpt-200000") \
    -> None:
  os.makedirs(os.path.join("output", "nsynth"), exist_ok=True)
  encodings_mix_name = [os.path.join("output", "nsynth",
                                     encoding_mix_name + ".wav")
                        for encoding_mix_name in encodings_mix_name]
  fastgen.synthesize(encodings_mix,
                     checkpoint_path=checkpoint,
                     save_paths=encodings_mix_name)

基本上,这个方法所做的就是在magenta.models.nsynth.wavenet.fastgen模块中调用synthesize方法。encodings_mix的形状为(12, 31, 16),其中 12 是我们的batch_size(最终输出音频片段的数量),31是时间,16是潜在空间的维度。

为了理解synthesize方法的作用,看看这个摘录:

for sample_i in range(total_length):
  encoding_i = sample_i // hop_length
  audio = generate_audio_sample(sess, net,
                                audio, encodings[:, encoding_i, :])
  audio_batch[:, sample_i] = audio[:, 0]
  if sample_i % 100 == 0:
    tf.logging.info("Sample: %d" % sample_i)
  if sample_i % samples_per_save == 0 and save_paths:
    save_batch(audio_batch, save_paths)

这里,total_length是 15,872,略低于我们设定的 16,000 样本长度(对应 1 秒的时间),因为长度是通过将时间(31)乘以步幅长度(512)来计算的。有关更多信息,请参见前一部分“编码 WAV 文件”的信息框。这将导致音频文件的时长不会完全为 1 秒。

另一个需要注意的地方是这个过程一次生成一个样本。这可能看起来效率低下,实际上确实如此:模型在重建音频方面非常出色,但速度却极慢。你可以看到,这个过程的大部分操作是在 Python 中以串行方式执行的,而不是在 GPU 上并行执行的。

在接下来的部分“将 GANSynth 用作生成乐器”中,我们将介绍一个类似但速度更快的模型。

把所有东西组合起来

现在我们有了三个方法:encodemixsynth,我们可以调用它们来创造新的声音和质感。

准备音频剪辑

对于这个示例,我们在sounds文件夹中提供了一些音频剪辑供您使用。虽然我们建议您尝试使用自己的声音,但您可以先用这些进行测试,稍后再尝试您自己的方法。

您可以从许多地方找到音频剪辑:

  • 制作您自己的音频!只需打开麦克风,用棍子敲击盘子即可(请参阅以下列表,了解如何使用 Audacity 录制)。

  • Freesound 网站,freesound.org,是一个热衷于分享音频剪辑的令人惊叹的社区。Freesound 是一个分享无版权音频剪辑的网站(大多数属于 CC0 1.0 通用(CC0 1.0)公共领域贡献)。

  • 还有 NSynth 数据集,magenta.tensorflow.org/datasets/nsynth

您可以使用任何您想要的样本,但我们建议保持较短(1 或 2 秒),因为这是一个耗时的过程。

无论您选择哪个来源,拥有简单的数字音频编辑器和录音应用程序软件都将帮助您大大简化切割、归一化和处理声音。正如介绍中所述,Audacity 是一个出色的开源跨平台(Windows、Linux 和 macOS)软件。

例如,如果您从 Freesound 下载了音频剪辑,它们的长度和音量可能不一致,或者它们可能对齐不良。Audacity 非常适合处理这类任务:

在这个截图中,我们看到每一行对应一个音频剪辑。它们都被裁剪为 1 秒钟,准备用于我们的示例。以下是熟练使用 Audacity 的一些提示:

  • 录制你自己的声音,请首先点击点击开始监控选项。如果你看到红条,就表示一切正常。然后,点击左侧的红色录制按钮。

  • 剪切您的录音,请在顶部使用选择工具F1),选择一个部分,然后按下删除键删除该部分。您可以使用底部的音频位置来精确选择 1 秒钟的部分。

  • 调整音频内容(例如将尖锐的噪音移到剪辑的开头),请在顶部使用时间移动工具F5),选择一个部分,然后拖放您想要移动的部分。

  • 对于本章,您将希望将音轨设置为单声道(而不是两个声道)。如果您看到单个文件有两条波形线,则说明您的音频是立体声的。在左侧,单击文件名。在下拉菜单中,使用分离立体声轨道,然后删除左侧或右侧轨道。您还需要将轨道声道设置在LR之间的中心位置。

  • 归一化是在不修改音频内容的情况下使音量更大或更小的操作,这在您的样本音量不合适时可能很有用。要执行此操作,请选择整个轨道,然后使用效果 > 归一化,然后将最大振幅更改为您想要的值。

  • 要以 WAV 格式导出,请使用文件 > 导出 > 导出为 WAV菜单。如果你有多个轨道,可以使用左侧的独奏按钮,因为如果不使用,它们会混合在一起。

现在我们知道如何生成音频片段了,接下来让我们编写代码来使用它们。

生成新的乐器

我们现在准备通过混合音频片段的配对来渲染音频片段。这个过程相对耗时,取决于你的电脑速度以及是否有 GPU;这可能差异很大。编写本文时,一台中等性能的 i7 笔记本电脑需要 20 分钟来计算所有 12 个样本,而一台配有入门级 GPU(例如 NVIDIA RTX 2060)的 PC 只需 4 分钟。

如果你发现生成过程太长,可以从WAV_FILENAMES中仅选择两个样本开始。稍后我们将在使用 GANSynth 作为生成乐器一节中看到有更快的替代方法。

最后,让我们调用我们的encodemixsynth方法:

WAV_FILENAMES = ["83249__zgump__bass-0205__crop.wav",
                 "160045__jorickhoofd__metal-hit-with-metal-bar-resonance"
                 "__crop.wav",
                 "412017__skymary__cat-meow-short__crop.wav",
                 "427567__maria-mannone__flute__crop.wav"]

# Downloads and extracts the checkpoint to "checkpoints/wavenet-ckpt"
download_checkpoint("wavenet-ckpt")

# Encodes the wav files into 4 encodings (and saves them for later use)
encodings = encode(WAV_FILENAMES)

# Mix the 4 encodings pairs into 12 encodings
encodings_mix, encodings_mix_name = mix_encoding_pairs(encodings,
                                                       WAV_FILENAMES)

# Synthesize the 12 encodings into wavs
synthesize(encodings_mix, encodings_mix_name)

如果你决定使用自己的声音,确保它们位于sounds文件夹中。此外,你可以在声音文件名和下划线字符之间加上一个标识符;生成的音频片段将保留这些标识符对。

生成的输出将作为 WAV 文件保存在output/nsynth文件夹中;每一对唯一的输入对应该有一个文件,如果你使用了 4 个输入片段,应该会有 12 个 WAV 片段。去听一听它们吧。

可视化和聆听我们的结果

现在我们已经生成了片段,我们也可以查看彩虹图了。

你可以在本章的源代码中的audio_utils.py文件中找到频谱图和彩虹图的代码。源代码中有更多的注释和内容,你应该去查看一下。

要为我们示例中的所有生成音频文件生成彩虹图,让我们调用save_rainbowgram_plot方法:

import os
import librosa
import glob
from audio_utils import save_rainbowgram_plot

for path in glob.glob("output/nsynth/*.wav"):
  audio, _ = librosa.load(path, 16000)
  filename = os.path.basename(path)
  output_dir = os.path.join("output", "nsynth", "plots")
  print(f"Writing rainbowgram for {path} in {output_dir}")
  save_rainbowgram_plot(audio,
                        filename=filename.replace(".wav", "_rainbowgram.png"),
                        output_dir=output_dir)

上述代码将在output/nsynth/plots中输出以下图形:

关于生成的音频文件及其对应的频谱图,有几点需要注意:

  • 首先,左边的三种金属生成声音很有趣,因为它们展示了生成的声音保留了音符的包络,因为原始的金属声音有很强的攻击性。生成的音频听起来像是有什么东西被击打了,类似于原始的声音,但现在的谐波更好。

  • 然后,三种猫咪生成的声音也很有趣。在这些声音中,萌萌的猫咪叫声变成了外星般的咆哮。由于 NSynth 模型是在乐器音符上训练的,而猫咪的声音差异如此之大,模型不得不进行猜测,这就产生了一个有趣的声音。尝试使用训练数据集外的声音,比如打击乐;看看模型会生成什么很有趣。

  • 在一些音频片段中,比如长笛+低音片段,我们可以听到生成的音频中有一些点击声。这是因为模型采样了一个极端值后进行自我修正时发生的。

你应该尝试并实验不同的声音组合和持续时间。我们使用的是较短的样本,以加快处理速度,但你可以使用任意长度的样本。只需要记住,NSynth 数据集中仅包含 4 秒长的单个音符,意味着生成多个连续音符的长样本将导致模型猜测它们之间的过渡。

使用 NSynth 生成的样本作为乐器音符

现在我们已经从 NSynth 生成了一些样本,我们可以使用 MIDI 对它们进行排序。最简单的方式是使用数字音频工作站DAW)。由于这需要编写特定代码让 Magenta 将 MIDI 发送到 DAW,因此我们将在第九章中专门讨论这个话题,让 Magenta 与音乐应用互动。如果你现在想尝试,可以跳到后面,再回来查看这里的内容。

使用命令行

命令行对于 NSynth 的使用有一定的限制,但你仍然可以生成音频片段。如果你还没有这么做,你需要下载并解压checkpoints/wavenet-ckpt文件夹中的检查点。在本章的代码中,使用以下命令从sounds文件夹中的音频片段生成音频(警告:这个过程会耗时较长):

nsynth_generate --checkpoint_path="checkpoints/wavenet-ckpt/model.ckpt-200000" --source_path="sounds" --save_path="output/nsynth" --batch_size=4 --sample_length=16000

通过使用batch_size=4sample_length=16000,你确保代码运行尽可能快。生成的文件将保存在output/nsynth文件夹中,文件名格式为gen_FILENAME.wav,其中FILENAME是源文件名。你将看到每个源声音生成一个音频片段,共四个音频片段。

生成的音频片段是通过对音频进行编码后再合成的。将它们与原始音频进行比较:这将帮助你感受 NSynth 的声音特性。

更多关于 NSynth 的内容

NSynth 的功能远不止我们展示的这些,还包括更高级的插值和混合使用、时间拉伸等。NSynth 已经产生了一些有趣的项目,比如移动应用(mSynth)和物理硬件(NSynth Super)。更多关于 NSynth 的信息,请参考进一步阅读部分。

使用 GANSynth 作为生成乐器

在前一节中,我们使用 NSynth 通过组合现有的声音生成了新的声音样本。你可能已经注意到,音频合成过程非常耗时。这是因为自回归模型(如 WaveNet)专注于单个音频样本,这使得波形的重建过程非常缓慢,因为它必须迭代地处理这些样本。

另一方面,GANSynth 使用上采样卷积,使得整个音频样本的训练和生成过程可以并行进行。这是它相对于自回归模型(如 NSynth)的一个重大优势,因为这些算法在 GPU 硬件上往往会受到 I/O 限制。

GANSynth 的结果令人印象深刻:

  • 在单个 V100 GPU 上,NSynth 数据集的训练需要大约 3-4 天时间才能收敛。相比之下,NSynth WaveNet 模型在 32 个 K40 GPU 上需要 10 天才能收敛。

  • 在 TitanX GPU 上,通过 GANSynth 合成 4 秒音频样本只需要 20 毫秒。相比之下,WaveNet 基线需要 1,077 秒,慢了 50,000 倍。

GAN 的另一个重要含义是,该模型具有球形高斯先验,解码后产生整个声音,使得两个样本之间的插值更加平滑,没有额外的工件,不像 WaveNet 插值那样。这是因为像 NSynth 这样的 WaveNet 自编码器在学习控制毫秒级生成的局部潜在编码时,有限的范围。

在本节中,我们将使用 GANSynth 生成一个 30 秒的音频片段,通过从模型的潜在空间中随机选择乐器样本,并在音频轨道中的一段有限时间内播放每个乐器,例如每个乐器播放 5 秒,并在乐器变换时进行混合。

选择声学模型

Magenta 提供了两个预训练的 GANSynth 模型:acoustic_only,该模型仅在声学乐器上进行训练,以及all_instruments,该模型在整个 NSynth 数据集上进行训练(有关数据集的更多信息,请参见上一节,NSynth 数据集)。

我们将在示例中使用acoustic_only数据集,因为对于巴赫乐谱的生成,生成的音频轨道在乐器选择方面会更自然。如果您想生成更广泛的音频生成,请使用all_instruments模型。

您可以在本章的源代码chapter_05_example_02.py文件中查看此示例。源代码中有更多的注释和内容,所以您应该去查看一下。

本章还在midi文件夹中包含一个 MIDI 片段,我们将在本节中使用它。

要下载模型,请使用以下方法,该方法将下载并提取模型:

def download_checkpoint(checkpoint_name: str,
                        target_dir: str = "checkpoints"):
  tf.gfile.MakeDirs(target_dir)
  checkpoint_target = os.path.join(target_dir, f"{checkpoint_name}.zip")
  if not os.path.exists(checkpoint_target):
    response = urllib.request.urlopen(
      f"https://storage.googleapis.com/magentadata/"
      f"models/gansynth/{checkpoint_name}.zip")
    data = response.read()
    local_file = open(checkpoint_target, 'wb')
    local_file.write(data)
    local_file.close()
    with zipfile.ZipFile(checkpoint_target, 'r') as zip:
      zip.extractall(target_dir)

使用acoustic_only检查点名称,生成的检查点可通过checkpoints/acoustic_only路径使用。

获取音符信息

为了开始这个示例,我们将加载一个 MIDI 文件,作为音频生成的背景乐谱。

首先,使用magenta.models.gansynth.lib.generate_util模块中的load_midi方法加载 MIDI 文件:

import os
from magenta.models.gansynth.lib.generate_util import load_midi
from note_sequence_utils import save_plot

def get_midi(midi_filename: str = "cs1-1pre-short.mid") -> dict:
  midi_path = os.path.join("midi", midi_filename)
  _, notes = load_midi(midi_path)
  return notes

我们在midi文件夹中提供了一个 MIDI 文件,但您也可以提供自己喜欢的 MIDI 文件,例如从前几章生成的文件。然后,load_midi方法返回关于 MIDI 文件中音符的信息字典,例如音调、速度以及开始和结束时间的列表。

提供的cs1-1pre-short.mid文件如下所示:

您可以看到 MIDI 文件的长度为 28 秒(每分钟 120 拍,共 14 小节),包含两种乐器。

从潜在空间逐步采样

现在我们已经拥有 MIDI 文件的信息(在notes变量中),我们可以从中生成音频。

让我们定义generate_audio方法:

from magenta.models.gansynth.lib import flags as lib_flags
from magenta.models.gansynth.lib import model as lib_model
from magenta.models.gansynth.lib.generate_util import combine_notes
from magenta.models.gansynth.lib.generate_util import get_random_instruments
from magenta.models.gansynth.lib.generate_util import get_z_notes

def generate_audio(notes: dict,
                   seconds_per_instrument: int = 5,
                   batch_size: int = 16,
                   checkpoint_dir: str = "checkpoints/acoustic_only") \
    -> np.ndarray:
  flags = lib_flags.Flags({"batch_size_schedule": [batch_size]})
  model = lib_model.Model.load_from_path(checkpoint_dir, flags)

  # Distribute latent vectors linearly in time
  z_instruments, t_instruments = get_random_instruments(
    model,
    notes["end_times"][-1],
    secs_per_instrument=seconds_per_instrument)

  # Get latent vectors for each note
  z_notes = get_z_notes(notes["start_times"], z_instruments, t_instruments)

  # Generate audio for each note
  audio_notes = model.generate_samples_from_z(z_notes, notes["pitches"])

  # Make a single audio clip
  audio_clip = combine_notes(audio_notes,
                             notes["start_times"],
                             notes["end_times"],
                             notes["velocities"])

  return audio_clip

该方法有四个重要部分,我们将在接下来的三个子节中解释——获取随机乐器、获取潜在向量、从潜在向量生成样本,然后将音符组合成完整的音频片段。

生成随机乐器

magenta.models.gansynth.lib.generate_util中的get_random_instruments方法如下所示:

def get_random_instruments(model, total_time, secs_per_instrument=2.0):
  """Get random latent vectors evenly spaced in time."""
  n_instruments = int(total_time / secs_per_instrument)
  z_instruments = model.generate_z(n_instruments)
  t_instruments = np.linspace(-.0001, total_time, n_instruments)
  return z_instruments, t_instruments

使用 28 秒的示例,每个乐器 5 秒钟,得到的n_instruments5,然后模型通过generate_z方法初始化潜在向量,这是从正态分布中采样的:

np.random.normal(size=[n, self.config['latent_vector_size']])

这会得到一个形状为(5, 256)的z_instruments,5 表示乐器的数量,256 表示潜在向量的大小。最后,我们在t_instruments的开始和结束时间之间采取五个相等的步长。

获取潜在向量

magenta.models.gansynth.lib.generate_util中的get_z_notes方法如下所示:

def get_z_notes(start_times, z_instruments, t_instruments):
  """Get interpolated latent vectors for each note."""
  z_notes = []
  for t in start_times:
    idx = np.searchsorted(t_instruments, t, side='left') - 1
    t_left = t_instruments[idx]
    t_right = t_instruments[idx + 1]
    interp = (t - t_left) / (t_right - t_left)
    z_notes.append(slerp(z_instruments[idx], z_instruments[idx + 1], interp))
  z_notes = np.vstack(z_notes)
  return z_notes

该方法获取每个起始音符的时间,并查找应该使用哪个乐器(前一个乐器,t_left,以及下一个乐器,t_right)。然后,它查找音符在这两个乐器之间的位置,在interp中调用slerp方法,该方法会找到对应于这两个最接近向量之间的乐器的潜在向量。这使得从一个乐器平滑过渡到另一个乐器成为可能。

从编码中生成样本

我们不会深入探讨magenta.models.gansynth.lib.model中的generate_samples_from_z方法的细节。我们只使用这段代码来说明我们在使用 GANSynth 作为生成乐器部分中介绍的内容,即模型整体生成音频片段:

# Generate waves
start_time = time.time()
waves_list = []
for i in range(num_batches):
  start = i * self.batch_size
  end = (i + 1) * self.batch_size

  waves = self.sess.run(self.fake_waves_ph,
                        feed_dict={self.labels_ph: labels[start:end],
                                   self.noises_ph: z[start:end]})
  # Trim waves
 for wave in waves:
    waves_list.append(wave[:max_audio_length, 0])

对于我们的示例,该方法将迭代 27 次,以每次处理 8 个labelsz的块(我们的batch_size为 8)。批量大小越大,可以并行生成更多的波形。可以看到,与 NSynth 不同,音频样本并不是逐个生成的。

最后,一旦所有音频片段生成完毕,magenta.models.gansynth.lib.generate_util模块中的combine_notes将利用音频片段和 MIDI 音符生成音频。基本上,该方法计算每个 MIDI 音符的包络线,当音符触发时,能听到音频片段的正确部分。

将所有部分整合起来

现在我们已经定义并解释了代码的不同部分,接下来我们调用相应的方法,从 MIDI 文件生成音频片段,使用逐步插值的乐器:

# Downloads and extracts the checkpoint to "checkpoint/acoustic_only"
download_checkpoint("acoustic_only")

# Loads the midi file and get the notes dictionary
notes = get_midi_notes()

# Generates the audio clip from the notes dictionary
audio_clip = generate_audio(notes)

# Saves the audio plot and the audio file
save_audio(audio_clip)

生成的彩虹图如下所示:

该图表没有给我们提供太多关于声音的信息,除了看到整个音频片段中音符的进展。去听一听生成的片段。多次生成将引入不同的乐器;确保多次测试并使用多个 MIDI 文件,以便体验乐器生成的各种可能性。

使用命令行

GANSynth 命令行工具使得从 MIDI 文件生成音频片段成为可能,就像我们在 Python 代码中所做的那样。如果你还没有完成此操作,你需要下载并解压检查点到 checkpoints/wavenet-ckpt 文件夹中。在本章的代码中,使用以下命令和 midi 文件夹中的 MIDI 文件生成音频片段(警告:此过程需要较长时间):

gansynth_generate --ckpt_dir="checkpoints/acoustic_only" --output_dir="output/gansynth" --midi_file="midi/cs1-1pre-short.mid"

生成的文件将位于 output/gansynth 文件夹中,并命名为 generated_clip.wav。像我们示例中的生成片段一样,生成的片段包含多个乐器,它们逐渐融合在一起。你可以使用 secs_per_instrument 参数来调整每个乐器播放的时间。

概述

在本章中,我们探讨了使用两个模型(NSynth 和 GANSynth)进行音频生成,并通过插值样本和生成新乐器生成了许多音频片段。我们从解释 WaveNet 模型是什么以及为什么它们在音频生成中(尤其是在语音合成应用中)被使用开始。我们还介绍了 WaveNet 自编码器,它是一种能够学习自身时间嵌入的编码器和解码器网络。我们讨论了如何使用降低维度的潜在空间进行音频可视化,并展示了彩虹图的应用。

然后,我们展示了 NSynth 数据集和 NSynth 神经网络乐器。通过展示组合不同声音的示例,我们学会了如何将两种不同的编码混合在一起,从而合成出新的声音。最后,我们看到了 GANSynth 模型,它是一个性能更强的音频生成模型。我们展示了生成随机乐器并在它们之间平滑插值的示例。

本章标志着本书音乐生成内容的结束——你现在可以使用 MIDI 作为伴奏谱、神经网络乐器作为音频生成完整的歌曲。在前几章中,我们一直在使用预训练模型,展示了 Magenta 中的模型已准备就绪且功能强大。

尽管如此,训练你自己的模型仍有很多理由,正如我们将在接下来的章节中看到的那样。在 第六章,训练数据准备 中,我们将探讨如何为特定的音乐类型和乐器准备数据集。在 第七章,训练 Magenta 模型 中,我们将使用这些数据集训练我们自己的模型,然后用它们生成新的音乐类型和乐器。

问题

  1. 为什么生成音频这么难?

  2. 是什么让 WaveNet 自编码器变得有趣?

  3. 彩虹图中有哪些不同的颜色?一共有多少种?

  4. 如何使用 NSynth 将音频片段的播放速度降低 2 秒?

  5. 为什么 GANSynth 比 NSynth 更快?

  6. 需要什么代码来从 GANSynth 潜在空间中采样 10 种乐器?

进一步阅读

第三部分:训练、学习与生成特定风格

本节包括如何准备来自不同输入格式的数据,并在这些数据上训练现有模型的信息。通过本节内容,你将理解如何生成符合特定风格的音乐。

本节包含以下章节:

  • 第六章,训练数据准备

  • 第七章,训练 Magenta 模型

第八章:训练数据准备

到目前为止,我们使用的是现有的 Magenta 预训练模型,因为它们非常强大且易于使用。但训练我们自己的模型至关重要,因为这能让我们生成特定风格的音乐或特定的结构或乐器。构建和准备数据集是训练我们自己模型的第一步。为了做到这一点,我们需要查看现有的数据集和 API,这些工具可以帮助我们找到有意义的数据。接着,我们需要为特定风格(舞曲和爵士)构建两个 MIDI 数据集。最后,我们需要通过数据转换和管道准备 MIDI 文件用于训练。

本章将涵盖以下主题:

  • 查看现有的数据集

  • 构建舞曲音乐数据集

  • 构建一个爵士数据集

  • 使用管道准备数据

技术要求

在本章中,我们将使用以下工具:

  • 命令行Bash从终端启动 Magenta

  • Python及其库

  • Python 的多进程模块用于多线程数据准备

  • Matplotlib用于绘制数据准备结果

  • Magenta启动数据管道转换

  • MIDIABCNotationMusicXML作为数据格式

  • 外部 API,如Last.fm

在 Magenta 中,我们将使用数据管道。我们将在本章后续部分深入讲解这些内容,但如果你觉得需要更多信息,可以参考 Magenta 源代码中的管道 README 文件(github.com/tensorflow/magenta/tree/master/magenta/pipelines),这是一个很好的起点。你还可以查看 Magenta 的代码,文档也非常完善。另有更多内容可参考进一步阅读部分。

本章的代码可以在本书的 GitHub 仓库中找到,位于Chapter06文件夹,链接为github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter06。在本章中,你应该在开始之前使用cd Chapter06命令。

查看以下视频,看看代码如何运作:bit.ly/3aXWLmC

查看现有的数据集

在本章中,我们将为训练准备一些数据。请注意,关于这一点将在第七章中详细介绍,训练 Magenta 模型。数据准备和模型训练是两个不同的活动,它们是并行进行的——首先,我们准备数据,然后训练模型,最后再回过头来准备数据,以提高模型的性能。

首先,我们将看看除了 MIDI 以外的符号表示法,如 MusicXML 和 ABCNotation,因为 Magenta 也支持它们,即使我们在本章中使用的主要数据集是 MIDI 格式。接着,我们将概述现有的数据集,包括 Magenta 团队用来训练我们已经介绍过的一些模型的数据集。这个概述并不详尽,但可以作为寻找训练数据的起点。

我们将重点关注的主要数据集是 Lakh MIDI 数据集LMD),这是一个最新且精心制作的 MIDI 数据集,将作为我们大部分示例的基础。你也可以使用其他数据集;我们提供的代码可以轻松适配其他内容。

查看符号表示法

有三种主要的符号表示法:MIDI、MusicXML 和 ABCNotation。我们已经详细讲解了 MIDI,但还没有讨论 MusicXML 和 ABCNotation。虽然我们在本章中不会使用这两种格式,但知道它们的存在以及 Magenta 能够处理它们和 MIDI 文件也是有帮助的。

MusicXML,顾名思义,是一种基于 XML 的音乐表示格式。它的优点是基于文本,这意味着它不需要像 PrettyMIDI 这样的外部库来处理,并且在许多乐谱编辑器中都得到了支持,例如 MuseScore。你可以在其官方网站找到 MusicXML 规范:www.musicxml.com

这是一个 MusicXML 文件的示例:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE score-partwise PUBLIC
    "-//Recordare//DTD MusicXML 3.1 Partwise//EN"
    "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
 <part-list>
    <score-part id="P1">
      <part-name>Music</part-name>
    </score-part>
  </part-list>
  <part id="P1">
    <measure number="1">
      <attributes>
...
      </attributes>
      <note>
        <pitch>
          <step>C</step>
          <octave>4</octave>
        </pitch>
        <duration>4</duration>
        <type>whole</type>
      </note>
    </measure>
  </part>
</score-partwise>

ABCNotation,顾名思义,是一种基于字母记谱(A-G)的文本音乐表示格式。该格式非常紧凑,包含一些关于整个文件的头部信息,后面跟着使用字母记谱法的歌曲内容。该记谱法在乐谱软件中也得到了广泛支持。下面是一个 ABCNotation 文件的示例:

<score lang="ABC">
X:1
T:The Legacy Jig
M:6/8
L:1/8
R:jig
K:G
GFG BAB | gfg gab | GFG BAB | d2A AFD |
GFG BAB | gfg gab | age edB |1 dBA AFD :|2 dBA ABd |:
efe edB | dBA ABd | efe edB | gdB ABd |
efe edB | d2d def | gfe edB |1 dBA ABd :|2 dBA AFD |]
</score>

我们将在本章的查看其他数据集部分提供一些 ABCNotation 内容。

Magenta 提供的用于准备训练数据集的工具可以通过单个命令处理 MIDI、MusicXML 和 ABCNotation,这非常方便。convert_dir_to_note_sequences命令会根据内容类型进行解析,并返回 NoteSequence

我们将在第七章,训练 Magenta 模型 中详细介绍这些工具。

从零开始构建数据集

使用现有的数据集并对其进行裁剪是构建和准备训练数据集的最简单方法,因为这是一种快速获取足够训练数据的方法。

另一种选择是从头开始构建数据集,要么通过为其创建新数据,要么通过整合来自不同来源的数据。虽然这种方法需要更多的工作,但在训练过程中可能会得到更好的结果,因为最终的数据是经过精心挑选的。如果你是音乐人并且已经有自己的 MIDI 文件,可以遵循这个过程。

使用 LMD 进行 MIDI 和音频文件

LMD(colinraffel.com/projects/lmd)是其中一个最完整和易于使用的 MIDI 数据集。如果您现在手头没有合适的数据集,我们建议使用它。本章的代码将使用此数据集,但您也可以使用其他数据集来跟随示例,因为即使信息不同,我们这里使用的大部分技术仍然适用。

数据集有各种分布,但我们特别关注以下几个:

  • LMD-full:这是完整数据集,包含 176,581 个 MIDI 文件。

  • LMD-matched:该数据集部分匹配到另一个数据集,即million song datasetMSD),该数据集包含 45,129 个 MIDI 文件。这个子集很有用,因为匹配的文件包含艺术家和标题等元数据。

  • LMD-aligned:LMD 匹配的数据集与音频 MP3 预览对齐。

使用 MSD 获取元数据信息

MSD(millionsongdataset.com)是一个大规模数据集,包含超过一百万条条目,包括音频特征和元数据信息。我们不会直接使用 MSD 数据集;相反,我们将使用包含在 LMD 匹配数据集中的匹配内容。

使用 MAESTRO 数据集进行表演音乐

MIDI 和音频编辑同步轨道和组织MAESTRO)数据集(magenta.tensorflow.org/datasets/maestro)由 Magenta 团队策划,基于录制的现场表演,包括音频和 MIDI,超过 200 小时的内容。由于这些录制的演奏是由人类演奏的,音符具有表现力的时序和动态(还包括效果踏板的表示)。

数据集内容来自国际钢琴电竞(piano-e-competition.com/),主要包含古典音乐。该数据集有多种用途,如自动音频转换为符号表示转录,已用于训练 Magenta Onsets 和 Frames 模型。还用于训练我们在 Chapter 3 中介绍的 Performance RNN 模型生成复调旋律。

与 MAESTRO 类似,但内容较少,您还可以使用 MusicNet 和 MAPS 数据集。我们在示例中不会使用 MAESTRO 数据集,但这是一个重要的数据集,您可能需要查看。有关更多信息,请查看本章末尾的进一步阅读部分。

使用 Groove MIDI 数据集进行动感鼓声

Groove MIDI 数据集 (www.tensorflow.org/datasets/catalog/groove) 包含了 13.6 小时的对齐 MIDI 和(合成的)人类演奏的、节奏对齐的富有表现力的电子鼓音频。该数据集还被分割成 2 小节和 4 小节的 MIDI 片段,并已用于训练 GrooVAE 模型,我们在第四章中介绍了该模型,使用 MusicVAE 进行潜在空间插值

GMD 是一个令人印象深刻的数据集,包含了专业鼓手的录音表演,这些鼓手还即兴创作,因而产生了多样化的数据集。这些表演带有一个类型标签,可以用来筛选和提取特定的 MIDI 文件。

虽然未量化,但 GMD 也可以用于训练量化模型,如鼓 RNN。我们将在 使用管道准备数据 一节中展示的管道可以将未量化的 MIDI 转换为量化 MIDI。

使用 Bach Doodle 数据集

Bach Doodle 是 Google 为庆祝德国作曲家和音乐家约翰·塞巴斯蒂安·巴赫周年纪念日而制作的一个 Web 应用程序。Doodle 允许用户创作 2 小节旋律,并通过浏览器中的 Coconet 和 TensorFlow.js 请求应用程序以巴赫风格和声化输入的旋律。

生成的 Bach Doodle 数据集 (magenta.tensorflow.org/datasets/bach-doodle) 包含和声编曲,令人印象深刻:为大约 6 年用户输入的音乐提供了 2160 万种和声。它包含用户输入的序列和网络输出的音符序列格式的输出序列,以及一些元数据,如用户所在国家和播放次数。

请参见 进一步阅读 部分,了解有关数据集的数据可视化的更多信息。

使用 NSynth 数据集进行音频内容处理

我们在上一章中介绍了 NSynth 数据集 (www.tensorflow.org/datasets/catalog/nsynth),使用 NSynth 和 GANSynth 进行音频生成。本书不会涉及音频训练,但这是一个很好的数据集,可以用于训练 GANSynth 模型。

使用 API 来丰富现有数据

使用 API 是查找更多 MIDI 曲目相关信息的好方法。在本章中,我们将展示如何使用歌曲艺术家和标题查询 API,来找到与该歌曲相关的类型和标签。

有多个服务可以用来查找此类信息。我们不会列出所有这些服务,但两个好的起点是 Last.fm (www.last.fm/) 和 Spotify 的 Echo Nest (static.echonest.com/enspex/)。tagtraum 数据集 (www.tagtraum.com/msd_genre_datasets.html) 是另一个基于 MSD 的良好类型数据集。

在本章中,我们将使用Last.fm API 来获取歌曲信息,并将在即将到来的部分构建爵士乐数据集中学习如何使用其 API。

查看其他数据源

除了已整理的数据集,互联网上还有大量其他音乐文件的来源——主要是提供可搜索数据库的网站。使用这些来源的缺点是你必须手动下载和验证每个文件,虽然这可能会很耗时,但如果你想从头开始构建自己的数据集,这是必要的。

MidiWorld (www.midiworld.com)和MuseScore (musescore.com)等网站包含了大量的 MIDI 文件,通常按风格、作曲家和乐器分类。Reddit 上也有一些帖子列出了各种质量的 MIDI 数据集。

对于 MIDI 以外的格式,你可以访问 ABCNotation 网站(abcnotation.com),它收录了超过 60 万个文件,主要是民间和传统音乐,还有指向其他资源的链接。

构建舞曲数据集

现在我们有了可用的数据集,可以开始构建自己的数据集了。我们将探讨不同的方法来使用 MIDI 文件中的信息。本节将作为仅使用 MIDI 文件构建数据集的不同工具的介绍。在这一节中,我们将使用LMD-full发行版。

在下一节构建爵士乐数据集中,我们将更深入地使用外部信息。

使用线程加速大数据集的处理

在构建数据集时,我们希望代码能够快速执行,因为我们需要处理大量的数据。在 Python 中,使用线程和多进程是一个有效的选择。Python 中有许多并行执行代码的方式,但我们将使用multiprocessing模块,因为它设计简单,而且与其他流行的技术,如使用joblib模块,类似。

你可以在本章的源代码中的multiprocessing_utils.py文件中找到这段代码。源代码中有更多的注释和内容,记得查看一下。

在我们的示例中,我们将使用以下代码启动四个并行执行的线程:

from itertools import cycle
from multiprocessing import Manager
from multiprocessing.pool import Pool

with Pool(4) as pool:
  # Add elements to process here
  elements = []
  manager = Manager()
  counter = AtomicCounter(manager, len(elements))
  results = pool.starmap(process, zip(elements, cycle([counter])))
  results = [result for result in results if result]

让我们更详细地解释这段代码:

  • 你可以通过修改Pool(4)中的线程数量来适应你的硬件。通常,物理 CPU 对应一个线程是性能较好的选择。

  • 我们实例化了Manager,它用于在线程之间共享资源,并且实例化了AtomicCounter,这是本书源代码中multiprocessing_utils模块中的一个组件,用来在线程之间共享一个计数器。

  • 最后,我们使用pool.starmap方法在elements列表上启动process方法(我们稍后将定义这个方法)。starmap方法会传递两个参数给process方法,第一个是elements列表中的一个元素,第二个是counter

  • process方法将一次处理一个元素,并在每次调用时递增计数器。

  • process方法应该能够返回None,以便过滤掉不需要的元素。

  • 线程的生命周期和元素列表的拆分由multiprocessing模块处理。

从 MIDI 文件中提取鼓乐器

MIDI 文件可以包含多种乐器——多种打击乐器、钢琴、吉他等等。从 MIDI 文件中提取特定乐器并将结果保存到新的 MIDI 文件中,是构建数据集时必不可少的一步。

在这个示例中,我们将使用PrettyMIDI来获取 MIDI 文件中的乐器信息,提取鼓乐器,将它们合并成一个单一的鼓乐器,并将结果保存到新的 MIDI 文件中。有些 MIDI 文件有多个鼓乐器,拆分鼓乐器为多个部分,例如低音鼓、军鼓等等。在我们的示例中,我们选择将它们合并,但根据你要做的事情,你可能想要将它们分开或只保留某些部分。

你可以在chapter_06_example_00.py文件中找到这段代码,它位于本章的源代码中。源代码中有更多的注释和内容,记得查看一下。

extract_drums方法接受midi_path并返回一个包含合并后鼓乐器的单一PrettyMIDI实例:

import copy
from typing import Optional

from pretty_midi import Instrument
from pretty_midi import PrettyMIDI

def extract_drums(midi_path: str) -> Optional[PrettyMIDI]:
  pm = PrettyMIDI(midi_path)
  pm_drums = copy.deepcopy(pm)
  pm_drums.instruments = [instrument for instrument in pm_drums.instruments
                          if instrument.is_drum]
  if len(pm_drums.instruments) > 1:
    # Some drum tracks are split, we can merge them
    drums = Instrument(program=0, is_drum=True)
    for instrument in pm_drums.instruments:
      for note in instrument.notes:
        drums.notes.append(note)
    pm_drums.instruments = [drums]
  if len(pm_drums.instruments) != 1:
    raise Exception(f"Invalid number of drums {midi_path}: "
                    f"{len(pm_drums.instruments)}")
  return pm_drums

首先,我们使用copy.deepcopy复制整个 MIDI 文件的内容,因为我们仍然希望保留原始文件中的时间签名和节奏变化。这些信息保存在通过copy.deepcopy方法从pm_drums变量返回的PrettyMIDI实例中。接着,我们使用is_drum属性过滤乐器。如果有多个鼓乐器,我们通过复制音符将它们合并成一个新的Instrument

检测特定的音乐结构

现在,我们可以从 MIDI 文件中提取特定乐器,还可以在 MIDI 文件中找到特定结构,从而进一步细化我们的数据集。舞曲和电子舞曲大多数情况下每个拍子都有低音鼓,这给人一种无法抗拒的舞动冲动。让我们看看能否在我们的 MIDI 文件中找到这种情况。

get_bass_drums_on_beat方法返回直接落在拍子上的低音鼓的比例:

import math
from pretty_midi import PrettyMIDI

def get_bass_drums_on_beat(pm_drums: PrettyMIDI) -> float:
  beats = pm_drums.get_beats()
  bass_drums = [note.start for note in pm_drums.instruments[0].notes
                if note.pitch == 35 or note.pitch == 36]
  bass_drums_on_beat = []
  for beat in beats:
    beat_has_bass_drum = False
    for bass_drum in bass_drums:
      if math.isclose(beat, bass_drum):
        beat_has_bass_drum = True
        break
    bass_drums_on_beat.append(True if beat_has_bass_drum else False)
  num_bass_drums_on_beat = len([bd for bd in bass_drums_on_beat if bd])
  return num_bass_drums_on_beat / len(bass_drums_on_beat)

PrettyMIDI中的get_beats方法返回一个数组,表示每个拍子的开始时间。例如,在一个 150 QPM 的文件中,我们有以下数组:

[  0\.    0.4   0.8   1.2   1.6   2\.    2.4   2.8   3.2   3.6   4\.    4.4
   4.8   5.2   5.6   6\.    6.4   6.8   7.2   7.6   8\.    8.4   8.8   9.2
...
 201.6 202\.  202.4 202.8 203.2 203.6 204\.  204.4 204.8 205.2 205.6 206.
 206.4 206.8 207.2 207.6]

然后,我们只取给定 MIDI 中的低音鼓音高(35 或 36),并假设我们有一个乐器,因为我们之前的方法extract_drums应该已经被调用过。接着,我们检查每个拍子是否在该时刻播放了低音鼓,并返回一个比例值作为float

分析我们的 MIDI 文件中的拍子

现在让我们把所有内容放在一起,检查我们的结果。

首先,我们将使用argparse模块为程序添加一些参数:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--path_output_dir", type=str, required=True)
parser.add_argument("--bass_drums_on_beat_threshold", 
                    type=float, required=True, default=0)
args = parser.parse_args()

在这里,我们使用argparse模块声明了两个命令行参数,--path_output_dir--bass_drums_on_beat_threshold。输出目录对于将提取的 MIDI 文件保存在单独的文件夹中很有用,而阈值则有助于过滤更多或更少的提取 MIDI 序列。

编写处理方法

现在我们已经准备好了参数,可以开始编写处理方法。

这里是process方法,将由我们的多线程代码调用:

import os
from typing import Optional
from multiprocessing_utils import AtomicCounter

def process(midi_path: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    os.makedirs(args.path_output_dir, exist_ok=True)
    pm_drums = extract_drums(midi_path)
    bass_drums_on_beat = get_bass_drums_on_beat(pm_drums)
    if bass_drums_on_beat >= args.bass_drums_on_beat_threshold:
      midi_filename = os.path.basename(midi_path)
      pm_drums.write(os.path.join(args.path_output_dir, f"{midi_filename}.mid"))
    else:
      raise Exception(f"Not on beat {midi_path}: {bass_drums_on_beat}")
    return {"midi_path": midi_path,
            "pm_drums": pm_drums,
            "bass_drums_on_beat": bass_drums_on_beat}
  except Exception as e:
    print(f"Exception during processing of {midi_path}: {e}")
  finally:
    counter.increment()

process方法将在输出目录不存在时创建它。然后,它将使用给定的 MIDI 路径调用extract_drums方法,再使用返回的PrettyMIDI鼓调用get_bass_drums_on_beat方法,该方法返回低音鼓的节奏比例。如果该比例超过阈值,我们将把该 MIDI 文件保存到磁盘;否则,我们将退出该方法。

process方法的返回值很重要——通过返回PrettyMIDI文件和节奏上的低音鼓比例,我们将能够对数据集进行统计,以便做出关于其大小和内容的明智决策。process方法还可以返回空值(None)或抛出异常,这将使调用者丢弃该 MIDI 文件。

使用线程调用 process 方法

现在我们有了处理方法,可以用它来启动执行。

我们来创建一个app方法,通过线程调用process方法:

import shutil
from itertools import cycle
from multiprocessing import Manager
from multiprocessing.pool import Pool
from typing import List
from multiprocessing_utils import AtomicCounter

def app(midi_paths: List[str]):
  shutil.rmtree(args.path_output_dir, ignore_errors=True)

  with Pool(4) as pool:
    manager = Manager()
    counter = AtomicCounter(manager, len(midi_paths))
    results = pool.starmap(process, zip(midi_paths, cycle([counter])))
    results = [result for result in results if result]
    results_percentage = len(results) / len(midi_paths) * 100
    print(f"Number of tracks: {len(MIDI_PATHS)}, "
          f"number of tracks in sample: {len(midi_paths)}, "
          f"number of results: {len(results)} "
          f"({results_percentage:.2f}%)")

app方法将在程序启动时使用 MIDI 路径列表被调用。首先,我们清理输出目录,为处理方法做准备,以便可以在其中写入内容。然后,我们使用Pool(4)启动四个线程(更多信息请参见前一节,使用线程处理大数据集)。最后,我们计算返回的结果数量以供参考。

使用 Matplotlib 绘制结果

使用返回的结果,我们可以找到关于数据集的统计信息。作为示例,绘制鼓的长度:

import matplotlib.pyplot as plt

pm_drums = [result["pm_drums"] for result in results]
pm_drums_lengths = [pm.get_end_time() for pm in pm_drums]
plt.hist(pm_drums_lengths, bins=100)
plt.title('Drums lengths')
plt.ylabel('length (sec)')
plt.show()

我们使用 Matplotlib(matplotlib.org/),这是一个流行且易于使用的 Python 绘图库。最终将得到以下输出:

绘制不同统计数据的图表帮助你可视化数据集的内容和大小。

处理数据集的一个样本

现在我们已经准备好了所有内容,可以使用数据集的一个子集来调用app方法。

首先,我们将使用较小的样本大小,以确保我们的代码正常工作:

parser.add_argument("--sample_size", type=int, default=1000)
parser.add_argument("--path_dataset_dir", type=str, required=True)

MIDI_PATHS = glob.glob(os.path.join(args.path_dataset_dir, "**", "*.mid"),
                       recursive=True)

if __name__ == "__main__":
  if args.sample_size:
    # Process a sample of it
    MIDI_PATHS_SAMPLE = random.sample(list(MIDI_PATHS), args.sample_size)
  else:
    # Process all the dataset
    MIDI_PATHS_SAMPLE = list(MIDI_PATHS)
  app(MIDI_PATHS_SAMPLE)

让我们更详细地解释前面的代码:

  • 我们声明了两个新参数,--sample_size--path_dataset_dir。第一个参数声明了样本的大小,第二个参数声明了数据集的位置。

  • 然后,我们在数据集的根文件夹上使用glob.glob,它将返回一个路径列表,每个 MIDI 文件对应一个元素。

  • 因为在大型数据集上执行此操作需要一段时间,你也可以将结果缓存到磁盘(例如使用pickle模块),如果你频繁执行此操作的话。

  • 我们使用random.sample从 MIDI 路径中获取一个子集,并使用得到的 MIDI 路径调用app方法。

你可以使用 LMD 的一个发行版来启动以下代码。你需要下载它(例如,LMD-full发行版)并解压 ZIP 文件。下载链接请参见使用 Lakh MIDI 数据集部分。

要使用小样本启动此代码,请在终端中使用以下命令(将PATH_DATASET替换为解压数据集的根文件夹,PATH_OUTPUT替换为输出路径):

python chapter_06_example_00.py --sample_size=1000 --path_dataset_dir="PATH_DATASET" --path_output_dir="PATH_OUTPUT"

解压后的 MIDI 文件将位于PATH_OUTPUT中,生成以下统计数据:

Number of tracks: 116189, number of tracks in sample: 116189, number of results: 12634 (10.87%)
Time:  7197.6346254

在这里,我们可以看到大约 10%的 MIDI 文件的--bass_drums_on_beat_threshold超过 0.75。整个 LMD 数据集返回了 12,634 个结果,这足以在后续训练中使用。我们将在下一章讨论训练。

构建爵士乐数据集

在前一节中,我们介绍了构建基于完整 LMD 数据集中的 MIDI 文件信息的数据集所需的工具。在本节中,我们将深入探讨如何使用外部 API(如 Last.fm API)构建自定义数据集。

在本节中,我们将使用LMD-matched发行版,因为它与包含元数据的 MSD 部分匹配,这些元数据对我们很有用,如艺术家和标题。然后可以将这些元数据与 Last.fm 结合使用,获取歌曲的类型。我们还将提取鼓和钢琴乐器,就像在前一节中所做的那样。

LMD 提取工具

在开始之前,我们先来看一下如何处理 LMD 数据集。首先,我们需要从colinraffel.com/projects/lmd/下载以下三个元素:

  • LMD-matched:与 MDS 匹配的 LMD 子集

  • LMD-matched 元数据:包含元数据的 H5 数据库

  • 匹配分数:匹配分数字典

解压后,你应该在同一文件夹中找到以下元素:lmd_matched目录、lmd_matched_h5目录和match_scores.json文件。

你可以在lakh_utils.py文件中找到这段代码,位于本章的源代码中。源代码中有更多注释和内容,请查阅。

lakh_utils.py文件中,你可以使用实用工具通过唯一标识符MSD_ID查找元数据和 MIDI 文件路径。我们的起点将是match_scores.json文件,它是一个匹配文件的字典,格式如下:

{
...
  "TRRNARX128F4264AEB": {"cd3b9c8bb118575bcd712cffdba85fce": 0.7040202098544246},
  "TRWMHMP128EF34293F": {
    "c3da6699f64da3db8e523cbbaa80f384": 0.7321245522741104,
    "d8392424ea57a0fe6f65447680924d37": 0.7476196649194942
  },
...
}

作为键,我们有MSD_ID,作为值,我们有一个匹配字典,每个匹配都有一个分数。从MSD_ID中,我们可以使用get_matched_midi_md5方法获取最高分匹配。通过该 MD5,我们将能够使用get_midi_path方法加载相应的 MIDI 文件。

使用 Last.fm API 获取歌曲的类型

我们示例的第一部分使用了 Last.fm API 来获取每首歌的类型。还有其他 API,例如 Spotify 的 Echo Nest,也可以用来获取这些信息。如果你愿意,你可以选择其他服务提供商来完成这一部分。

第一步是在www.last.fm/api/上创建一个账户。由于我们不会通过 API 进行更改,创建账户后,你只需要保留API 密钥即可。

你可以在本章的源代码中的chapter_06_example_01.py文件中找到本节的代码。源代码中有更多注释和内容,快去查看吧。

从 MSD 中读取信息

在我们调用 Last.fm 获取歌曲类型之前,我们需要找到每首歌的艺术家和标题。由于 LMD 与 MSD 是匹配的,找到这些信息非常简单。按照以下步骤操作即可:

  1. 首先,让我们定义一个process方法,就像在前一章中一样,可以通过线程调用,并从 H5 数据库中获取艺术家信息:
import argparse
import tables
from typing import List, Optional
from lakh_utils import msd_id_to_h5
from threading_utils import AtomicCounter

parser = argparse.ArgumentParser()
parser.add_argument("--sample_size", type=int, default=1000)
parser.add_argument("--path_dataset_dir", type=str, required=True)
parser.add_argument("--path_match_scores_file", type=str, required=True)
args = parser.parse_args()

def process(msd_id: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    with tables.open_file(msd_id_to_h5(msd_id, args.path_dataset_dir)) as h5:
      artist = h5.root.metadata.songs.cols.artist_name[0].decode("utf-8")
      title = h5.root.metadata.songs.cols.title[0].decode("utf-8")
      return {"msd_id": msd_id, "artist": artist, "title": title}
  except Exception as e:
    print(f"Exception during processing of {msd_id}: {e}")
  finally:
    counter.increment()

这段代码看起来与我们在前一节中写的代码非常相似。在这里,我们使用tables模块打开 H5 数据库。然后,我们使用来自lakh_utils模块的msd_id_to_h5方法来获取 H5 文件的路径。最后,我们从 H5 数据库中提取艺术家和标题,然后以字典的形式返回结果。

  1. 现在,我们可以像在前一章中一样调用process方法。在此之前,我们需要加载得分匹配字典,其中包含 LMD 和 MSD 之间的所有匹配项:
from lakh_utils import get_msd_score_matches

MSD_SCORE_MATCHES = get_msd_score_matches(args.path_match_scores_file)

if __name__ == "__main__":
  if args.sample_size:
    # Process a sample of it
    MSD_IDS = random.sample(list(MSD_SCORE_MATCHES), args.sample_size)
  else:
    # Process all the dataset
    MSD_IDS = list(MSD_SCORE_MATCHES)
  app(MSD_IDS)

为此,我们需要使用get_msd_score_matches方法,它会将字典加载到内存中。然后,我们使用app方法从完整的数据集中抽取一个样本。

  1. 最后,要使用一个小的样本集来运行此代码,在终端中使用以下命令(替换PATH_DATASETPATH_MATCH_SCORES):
python chapter_06_example_01.py --sample_size=1000 --path_dataset_dir="PATH_DATASET" --path_match_score="PATH_MATCH_SCORES"

你应该会看到以下输出:

Number of tracks: 31034, number of tracks in sample: 31034, number of results: 31034 (100.00%)
Time:  21.0088559

现在,我们可以绘制出 25 个最常见的艺术家,这将生成如下图表:

如果你愿意,你可以基于一个或多个你喜欢的艺术家创建数据集。你可能会发现可用于训练的 MIDI 文件太少,但也许值得尝试。

使用顶级标签来查找类型

现在我们知道如何在匹配的 MSD 数据库中获取信息,我们可以调用 Last.fm 来获取一首歌曲的类型信息。

你可以在本章的源代码中的chapter_06_example_02.py文件中找到本节的代码。源代码中有更多注释和内容,快去查看吧。

让我们开始吧:

  1. 调用 Last.fm API 的最简单方法是执行一个简单的GET请求。我们将在一个名为get_tags的方法中执行该操作,并将 H5 数据库作为参数传入:
import argparse
from typing import List, Optional
import requests
from threading_utils import AtomicCounter

parser = argparse.ArgumentParser()
parser.add_argument("--sample_size", type=int, default=1000)
parser.add_argument("--path_dataset_dir", type=str, required=True)
parser.add_argument("--path_match_scores_file", type=str, required=True)
parser.add_argument("--last_fm_api_key", type=str, required=True)
args = parser.parse_args()

def get_tags(h5) -> Optional[list]:
  title = h5.root.metadata.songs.cols.title[0].decode("utf-8")
  artist = h5.root.metadata.songs.cols.artist_name[0].decode("utf-8")
  request = (f"https://ws.audioscrobbler.com/2.0/"
             f"?method=track.gettoptags"
             f"&artist={artist}"
             f"&track={title}"
             f"&api_key={args.last_fm_api_key}"
             f"&format=json")
  response = requests.get(request, timeout=10)
  json = response.json()
  if "error" in json:
    raise Exception(f"Error in request for '{artist}' - '{title}': "
                    f"'{json['message']}'")
  if "toptags" not in json:
    raise Exception(f"Error in request for '{artist}' - '{title}': "
                    f"no top tags")
  tags = [tag["name"] for tag in json["toptags"]["tag"]]
  tags = [tag.lower().strip() for tag in tags if tag]
  return tags

这段代码向track.gettoptags API 端点发起请求,返回一个按标签数量排序的曲目流派列表,从最多标签到最少标签,其中标签数量是通过用户提交计算得出的。不同艺术家的标签分类差异很大。

你可以通过像 Last.fm 或 Echo Nest 这样的 API 获取关于曲目、艺术家或专辑的很多信息。确保在构建自己的数据集时检查它们提供的内容。

尽管有些部分可能比较简单(我们没有清理曲目名称和艺术家名称,也没有尝试使用其他匹配方式),但大多数曲目(超过 80%)都能在 Last.fm 上找到,这对于我们示例的目的来说已经足够好。

  1. 这是一个简单的过程方法,我们可以用它来调用我们的get_tags方法:
from typing import Optional
import tables
from threading_utils import AtomicCounter
from lakh_utils import msd_id_to_h5

def process(msd_id: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    with tables.open_file(msd_id_to_h5(msd_id, args.path_dataset_dir)) as h5:
      tags = get_tags(h5)
      return {"msd_id": msd_id, "tags": tags}
  except Exception as e:
    print(f"Exception during processing of {msd_id}: {e}")
  finally:
    counter.increment()
  1. 这个示例基于爵士乐,但如果你愿意,也可以选择其他流派。你可以使用以下代码绘制 LMD 中最受欢迎的共通流派:
from collections import Counter

tags = [result["tags"][0] for result in results if result["tags"]]
most_common_tags_20 = Counter(tags).most_common(20)
plt.bar([tag for tag, _ in most_common_tags_20],
        [count for _, count in most_common_tags_20])
plt.title("Most common tags (20)")
plt.xticks(rotation=30, horizontalalignment="right")
plt.ylabel("count")
plt.show()

这应该生成一个与以下图示类似的图表:

我们将使用爵士乐流派,但你可能希望将多个流派结合起来,例如爵士乐蓝调,以便你有更多的内容可以使用,或创造混合风格。在下一章中,我们将探讨你实际需要多少数据来训练模型,以便能够正确训练。

使用 MIDI 查找乐器类别

我们将在示例中使用两种乐器进行训练,但如果你有其他想法,也可以选择其他乐器:

  • 打击乐:我们将从 MIDI 文件中提取鼓轨道,训练一个鼓 RNN 模型。

  • 钢琴:我们将提取钢琴轨道来训练一个旋律 RNN 模型。

在这里,第一步是找到我们数据集中包含的乐器。在PrettyMIDI模块中,Instrument类包含一个program属性,可以用来查找这些信息。提醒一下,你可以在《通用 MIDI 1 声音集规范》(www.midi.org/specifications/item/gm-level-1-sound-set)中找到更多关于各类程序的信息。

每个程序对应一个乐器,每个乐器被分类到一个乐器类别中。我们将使用这种分类来查找我们的数据集统计信息。让我们开始吧:

你可以在本章的源代码中找到这一部分的代码,文件名为chapter_06_example_04.py。源代码中有更多的注释和内容,别忘了查看。

  1. 首先,我们为此目的编写get_instrument_classes方法:
from typing import List, Optional
from pretty_midi import PrettyMIDI, program_to_instrument_class
from lakh_utils import get_midi_path
from lakh_utils import get_matched_midi_md5

def get_instrument_classes(msd_id) -> Optional[list]:
  midi_md5 = get_matched_midi_md5(msd_id, MSD_SCORE_MATCHES)
  midi_path = get_midi_path(msd_id, midi_md5, args.path_dataset_dir)
  pm = PrettyMIDI(midi_path)
  classes = [program_to_instrument_class(instrument.program)
             for instrument in pm.instruments
             if not instrument.is_drum]
  drums = ["Drums" for instrument in pm.instruments if instrument.is_drum]
  classes = classes + drums
  if not classes:
    raise Exception(f"No program classes for {msd_id}: "
                    f"{len(classes)}")
  return classes

首先,我们加载一个PrettyMIDI实例,然后将program转换为其乐器类别。在这里,你可以看到我们单独处理了鼓部分,因为鼓没有program属性。

  1. 现在,我们可以按照如下方式编写我们的process方法:
from typing import Optional
import tables
from threading_utils import AtomicCounter
from lakh_utils import msd_id_to_h5

def process(msd_id: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    with tables.open_file(msd_id_to_h5(msd_id, args.path_dataset_dir)) as h5:
      classes = get_instrument_classes(msd_id)
      return {"msd_id": msd_id, "classes": classes}
  except Exception as e:
    print(f"Exception during processing of {msd_id}: {e}")
  finally:
    counter.increment()
  1. 可以使用以下代码找到最常见的乐器类别:
from collections import Counter

classes_list = [result["classes"] for result in results]
classes = [c for classes in classes_list for c in classes]
most_common_classes = Counter(classes).most_common()
plt.bar([c for c, _ in most_common_classes],
        [count for _, count in most_common_classes])
plt.title('Instrument classes')
plt.xticks(rotation=30, horizontalalignment="right")
plt.ylabel('count')
plt.show()

你应该得到类似于 LMD 中以下图表所展示的结果:

在这里,我们可以看到 LMD 中使用最频繁的乐器类别是吉他合奏钢琴。我们将在接下来的章节中使用钢琴类别,但如果你愿意的话,也可以使用其他类别。

提取爵士、鼓和钢琴轨道

现在,我们能够找到我们想要的乐器轨道,按类型筛选歌曲,并导出生成的 MIDI 文件,我们可以将所有内容整合在一起,创建两个爵士数据集,一个包含打击乐,另一个包含钢琴。

提取和合并爵士鼓

我们已经实现了大部分用于提取爵士鼓的代码,即get_tags方法和extract_drums方法。

你可以在本章的源代码文件chapter_06_example_07.py中找到这一部分的代码。源代码中有更多的注释和内容,记得查看。

process方法应该像这样调用get_tagsextract_drums方法:

import argparse
import ast
from typing import Optional
import tables
from multiprocessing_utils import AtomicCounter

parser = argparse.ArgumentParser()
parser.add_argument("--tags", type=str, required=True)
args = parser.parse_args()

TAGS = ast.literal_eval(args.tags)

def process(msd_id: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    with tables.open_file(msd_id_to_h5(msd_id, args.path_dataset_dir)) as h5:
      tags = get_tags(h5)
      matching_tags = [tag for tag in tags if tag in TAGS]
      if not matching_tags:
        return
      pm_drums = extract_drums(msd_id)
      pm_drums.write(os.path.join(args.path_output_dir, f"{msd_id}.mid"))
      return {"msd_id": msd_id,
              "pm_drums": pm_drums,
              "tags": matching_tags}
  except Exception as e:
    print(f"Exception during processing of {msd_id}: {e}")
  finally:
    counter.increment()

在这里,我们使用ast模块来解析tags参数。这很有用,因为它允许我们使用 Python 列表语法来表示标志的值,例如--tags="['jazz', 'blues']"。然后,我们可以检查来自 Last.fm 的标签是否与所需标签之一匹配,如果匹配,则将生成的 MIDI 鼓文件写入磁盘。

鼓的长度和类型分布可以通过以下图表查看:

在这里,我们可以看到,通过合并“爵士”和“蓝调”两种类型的轨道,我们大约有 2,000 个 MIDI 文件。

提取和拆分爵士钢琴

我们需要编写的最后一个方法是钢琴提取方法。extract_pianos方法类似于之前的extract_drums方法,但它不是将轨道合并在一起,而是将它们拆分成单独的钢琴轨道,可能会为每首歌曲返回多个轨道。让我们开始吧:

你可以在本章的源代码文件chapter_06_example_08.py中找到这一部分的代码。源代码中有更多的注释和内容,记得查看。

  1. 首先,我们将编写extract_pianos方法,如下所示:
from typing import List
from pretty_midi import Instrument
from pretty_midi import PrettyMIDI
from lakh_utils import get_matched_midi_md5
from lakh_utils import get_midi_path

PIANO_PROGRAMS = list(range(0, 8))

def extract_pianos(msd_id: str) -> List[PrettyMIDI]:
  os.makedirs(args.path_output_dir, exist_ok=True)
  midi_md5 = get_matched_midi_md5(msd_id, MSD_SCORE_MATCHES)
  midi_path = get_midi_path(msd_id, midi_md5, args.path_dataset_dir)
  pm = PrettyMIDI(midi_path)
  pm.instruments = [instrument for instrument in pm.instruments
                    if instrument.program in PIANO_PROGRAMS
 and not instrument.is_drum]
  pm_pianos = []
  if len(pm.instruments) > 1:
    for piano_instrument in pm.instruments:
      pm_piano = copy.deepcopy(pm)
      pm_piano_instrument = Instrument(program=piano_instrument.program)
      pm_piano.instruments = [pm_piano_instrument]
      for note in piano_instrument.notes:
        pm_piano_instrument.notes.append(note)
      pm_pianos.append(pm_piano)
  else:
    pm_pianos.append(pm)
  for index, pm_piano in enumerate(pm_pianos):
    if len(pm_piano.instruments) != 1:
      raise Exception(f"Invalid number of piano {msd_id}: "
                      f"{len(pm_piano.instruments)}")
    if pm_piano.get_end_time() > 1000:
      raise Exception(f"Piano track too long {msd_id}: "
                      f"{pm_piano.get_end_time()}")
  return pm_pianos

我们已经涵盖了这段代码的大部分内容。不同之处在于,我们正在筛选任何钢琴程序中的乐器,范围从 0 到 8。我们还将返回多个钢琴 MIDI 文件。

  1. 现在,我们可以使用以下process方法调用我们的方法:
import argparse
import ast
from typing import Optional
import tables
from lakh_utils import msd_id_to_h5
from multiprocessing_utils import AtomicCounter

parser = argparse.ArgumentParser()
parser.add_argument("--tags", type=str, required=True)
args = parser.parse_args()

TAGS = ast.literal_eval(args.tags)

def process(msd_id: str, counter: AtomicCounter) -> Optional[dict]:
  try:
    with tables.open_file(msd_id_to_h5(msd_id, args.path_dataset_dir)) as h5:
      tags = get_tags(h5)
      matching_tags = [tag for tag in tags if tag in TAGS]
      if not matching_tags:
        return
      pm_pianos = extract_pianos(msd_id)
      for index, pm_piano in enumerate(pm_pianos):
        pm_piano.write(os.path.join(args.path_output_dir,
                                    f"{msd_id}_{index}.mid"))
      return {"msd_id": msd_id,
              "pm_pianos": pm_pianos,
              "tags": matching_tags}
  except Exception as e:
    print(f"Exception during processing of {msd_id}: {e}")
  finally:
    counter.increment()

这段代码在上一节关于鼓的部分中已经讲过,只不过在这里,每个钢琴文件是使用索引单独写入的。钢琴的长度和类型可以像这样绘制:

在这里,我们找到了接近 2,000 个爵士蓝调钢琴轨道。

使用管道准备数据

在前面的部分中,我们查看了现有的数据集,并开发了工具以便能够查找并提取特定内容。通过这些操作,我们有效地构建了一个想要用来训练模型的数据集。但构建数据集并非一切——我们还需要准备它。准备的意思是移除那些不适合训练的内容,剪辑和拆分轨道,并且自动添加更多的内容。

在这一部分中,我们将介绍一些内置的工具,它们可以帮助我们将不同的数据格式(MIDI、MusicXML 和 ABCNotation)转换为适合训练的格式。这些工具在 Magenta 中被称为管道(pipelines),它们是一系列在输入数据上执行的操作。

在管道中已经实现的操作之一是丢弃音调过多、过长或过短的旋律。另一个操作是转调旋律,即通过上下移动音符的音高来创造新的旋律。这是一种在机器学习中常见的做法,叫做数据集增强,如果我们希望通过提供原始数据的变体来让模型更好地训练,这是很有用的。

让我们来看一下什么是管道(pipelines),以及它们如何被使用。在这一部分中,我们将以 Melody RNN 管道为例,但每个模型都有其独特的管道和特定内容。例如,Drums RNN 管道不会转调鼓的序列,因为这样做没有意义。

在我们开始讨论管道之前,我们将简要地看看如何手动优化我们之前构建的数据集。

手动优化数据集

这可能听起来显而易见,但我们还是要强调,因为它真的很重要:验证你提取的 MIDI 文件。数据集的内容应该与音乐方面的目标相符。

验证轨道内容最简单的方法是将其在 MuseScore 中打开并进行试听:

让我们来看看我们可以验证每个文件的哪些内容:

  • 首先要验证的是我们提取的乐器是否出现在生成的 MIDI 文件中。以我们的爵士钢琴示例为例,我们应该只有一个乐器轨道,并且它应该是八个钢琴程序中的任意一个。

  • 另一个需要验证的内容是音乐风格(genre)是否符合要求。这些钢琴声音像爵士乐吗?这段音乐听起来对你来说好听吗?

  • 其他需要注意的问题包括不完整的轨道,过短、过长或充满沉默的轨道。这些轨道中的一些将会通过数据准备管道过滤掉,但对数据进行人工检查也同样重要。

  • 如果你喜欢的一些曲目被数据准备管道过滤掉,例如,因为它们太短,你可以手动解决这个问题,方法是复制并粘贴它们的一部分以使其更长。你也可以编写一个管道来自动执行这个操作。

如果一些曲目不符合你的要求,删除它们。

一旦你验证了数据集的内容并移除了所有不需要的曲目,你也可以修剪和清理每个文件的内容。最简单的方法是进入 MuseScore,听一下曲目,移除你不喜欢的部分,然后将文件重新导出为 MIDI。

查看 Melody RNN 管道

一旦我们手动完善了数据集,就可以使用管道准备它,从而生成已准备好用于训练的数据。作为示例,我们将查看 Melody RNN 管道。

启动数据准备阶段并处理我们的数据集。

准备数据的第一步是调用convert_dir_to_note_sequences命令,不论你将使用哪个模型,这个命令都是相同的。该命令将一个包含 MIDI 文件、MusicXML 文件或 ABCNotation 文件的目录作为输入,并将其转换为 TensorFlow 记录格式的NoteSequence

我们建议你为训练数据创建另一个文件夹(与你之前创建的数据集文件夹分开)。现在,让我们开始:

  1. 首先,切换到你创建的文件夹,并使用以下命令调用convert_dir_to_note_sequences命令(将PATH_OUTPUT_DIR替换为你在上一节中使用的目录):
convert_dir_to_note_sequences --input_dir="PATH_OUTPUT_DIR" --output_file="notesequences.tfrecord"

这将输出一堆“转换后的 MIDI”文件,并生成一个notesequences.tfrecord。从现在开始,无论我们在构建数据集时使用的是哪种符号表示,数据格式都是相同的。

  1. 现在,我们可以使用以下代码在我们的数据上启动管道:
melody_rnn_create_dataset --config="attention_rnn" --input="notesequences.tfrecord" --output_dir="sequence_examples" --eval_ratio=0.10

首先,我们需要提供--config作为一个参数。这是必要的,因为编码器和解码器在配置中定义(参见第三章,生成复调旋律,以回顾编码和解码的工作原理)。

我们还传递了--eval_ratio参数,它将给管道提供训练集和评估集中的元素数量。执行时,管道将输出它遇到的文件的统计信息和警告。

统计信息会在每处理 500 个文件时打印到控制台,但只有最后部分(完成。输出之后)才是我们关心的。以下是爵士钢琴数据集 500 个样本的输出:

Processed 500 inputs total. Produced 122 outputs.
DAGPipeline_MelodyExtractor_eval_melodies_discarded_too_few_pitches: 7
DAGPipeline_MelodyExtractor_eval_melodies_discarded_too_long: 0
DAGPipeline_MelodyExtractor_eval_melodies_discarded_too_short: 42
DAGPipeline_MelodyExtractor_eval_melodies_truncated: 2
DAGPipeline_MelodyExtractor_eval_melody_lengths_in_bars:
  [7,8): 4
  [8,10): 2
  [10,20): 2
  [30,40): 2
DAGPipeline_MelodyExtractor_eval_polyphonic_tracks_discarded: 113
DAGPipeline_MelodyExtractor_training_melodies_discarded_too_few_pitches: 45
DAGPipeline_MelodyExtractor_training_melodies_discarded_too_long: 0
DAGPipeline_MelodyExtractor_training_melodies_discarded_too_short: 439
DAGPipeline_MelodyExtractor_training_melodies_truncated: 20
DAGPipeline_MelodyExtractor_training_melody_lengths_in_bars:
 [7,8): 22
 [8,10): 21
 [10,20): 42
 [20,30): 11
 [30,40): 16
DAGPipeline_MelodyExtractor_training_polyphonic_tracks_discarded: 982
DAGPipeline_RandomPartition_eval_melodies_count: 49
DAGPipeline_RandomPartition_training_melodies_count: 451
DAGPipeline_TranspositionPipeline_eval_skipped_due_to_range_exceeded: 0
DAGPipeline_TranspositionPipeline_eval_transpositions_generated: 317
DAGPipeline_TranspositionPipeline_training_skipped_due_to_range_exceeded: 0
DAGPipeline_TranspositionPipeline_training_transpositions_generated: 2387

这里关注的统计数据如下:

  • Processed 500 inputs total. **Produced 122 outputs.**:这告诉我们输入的大小或提供的 MIDI 文件数量,以及生成的SequenceExample的数量,这些将用于训练(122,计算包括评估集和训练集)。这是最重要的统计数据。

  • DAGPipeline_MelodyExtractor_MODE_melody_lengths_in_bars:这将为每个 "MODE" 提供生成的 SequenceExample 元素的长度。

SequenceExample 封装了将在训练过程中输入网络的数据。这些统计数据很有用,因为数据的数量(以及质量)对模型训练至关重要。如果一个模型在 122 个输出上没有正确训练,我们需要确保下次训练时有更多的数据。

从这个角度来看,查看生成的输出非常重要,它能告诉我们网络将接收到的数据量。即使我们给数据准备管道输入了 100,000 个 MIDI 文件,如果生成的 SequenceExample 很少,也没有关系,因为输入数据不好。如果管道对大量输入生成了少量输出,查看统计数据并找出哪个处理步骤删除了元素。

现在,让我们来看一下管道是如何为我们的示例定义和执行的。

理解管道的执行

我们在上一节提供的统计数据有些混乱,因为它们没有按照执行的顺序显示。让我们正视一下这个过程的实际执行方式,以便理解发生了什么:

  • DagInput 启动管道的执行,逐个读取 TensorFlow 记录中的每个 NoteSequence 作为输入(500 个元素)。

  • RandomPartition 根据命令中提供的比例随机将元素划分为训练集和评估集(训练集有 450 个元素,评估集有 50 个元素)。

  • TimeChangeSplitter 在每个时间变化点拆分元素(不输出统计数据)。

  • Quantizer 根据配置中的 steps_per_quarter 属性将音符序列量化到最近的步长,并删除具有多重节奏和时间签名的元素(不输出统计数据)。

  • TranspositionPipeline 将音符序列转换为多个音高,并在此过程中添加新的元素(训练集生成了 2,387 个通过转调产生的元素)。

  • MelodyExtractorNoteSequence 中提取旋律,返回一个 Melody 并根据需要删除元素,比如多声部轨道以及那些过短或过长的轨道(训练集中删除了 1,466 个元素)。这一部分还输出旋律的长度(以小节为单位):

    • 旋律的最小和最大长度分别由 min_barsmax_steps 定义。请参阅下一节,了解如何更改这些值。

    • ignore_polyphonic_notes 被设置为 True,会丢弃多声部轨道。

  • EncoderPipeline 使用为注意力配置定义的 KeyMelodyEncoderDecoderMelody 编码为 SequenceExample(不输出统计数据)。编码器管道接收作为参数传递的配置;例如,对于 lookback_rnn 配置,使用 LookbackEventSequenceEncoderDecoder

  • DagOutput 完成执行。

如果你想查看Pipeline的实现,查看melody_rnn_pipeline模块中的get_pipeline方法。

编写你自己的管道

正如你从get_pipeline方法中的代码中可能注意到的,大部分配置是无法更改的。不过,我们可以编写自己的管道并直接调用它。

你可以在本章的源代码中找到这一节的代码,文件名为melody_rnn_pipeline_example.py。源代码中有更多注释和内容,记得查看一下。

在这个例子中,我们将使用现有的 Melody RNN 管道,复制它,并更改移调和序列长度。让我们开始吧:

  1. 首先,复制get_pipeline方法,并使用以下 Python 代码调用它(将INPUT_DIROUTPUT_DIR替换为适当的值):
from magenta.pipelines import pipeline

pipeline_instance = get_pipeline("attention_rnn", eval_ratio=0.10)
pipeline.run_pipeline_serial(
    pipeline_instance,
    pipeline.tf_record_iterator(INPUT_DIR, pipeline_instance.input_type),
    OUTPUT_DIR)

你应该看到与我们之前使用管道方法时相同的输出。通过对钢琴爵士数据集的一个小样本(500 条数据)进行处理,我们得到了以下输出:

INFO:tensorflow:Processed 500 inputs total. Produced 115 outputs.
...
INFO:tensorflow:DAGPipeline_MelodyExtractor_training_melody_lengths_in_bars:
 [7,8): 31
 [8,10): 9
 [10,20): 34
 [20,30): 11
 [30,40): 17
 [50,100): 2
...
INFO:tensorflow:DAGPipeline_TranspositionPipeline_training_transpositions_generated: 2058
...
  1. 现在,让我们更改一些参数,看看它是如何工作的。在以下代码中,我们添加了一些移调(默认的移调值是(0,),表示没有移调偏移):
...
transposition_pipeline = note_sequence_pipelines.TranspositionPipeline(
  (0,12), name='TranspositionPipeline_' + mode)
...

通过使用移调值(0,12),我们告诉移调管道为每个现有的序列创建一个比原序列高 12 个音高的序列,相当于一个完整的八度向上偏移。其余的代码保持不变。

移调值应该遵循以半音(MIDI 音高值)表示的音乐间隔。最简单的间隔是我们正在使用的完美间隔,它对应于一个八度,或者 12 个半音,或 MIDI 音高。也可以使用其他间隔,例如大三度,这在 Polyphony RNN 管道中使用,移调范围为(-4, 5)

现在,输出应该如下所示:

...
INFO:tensorflow:Processed 500 inputs total. Produced 230 outputs.
...
INFO:tensorflow:DAGPipeline_MelodyExtractor_training_melody_lengths_in_bars:
 [7,8): 66
 [8,10): 14
 [10,20): 64
 [20,30): 22
 [30,40): 34
 [50,100): 4
...
INFO:tensorflow:DAGPipeline_TranspositionPipeline_training_transpositions_generated: 4297
...

请注意,现在我们大约有了两倍的数据可以使用。数据增强对于处理小型数据集非常重要。

  1. 我们还可以更改旋律提取器中序列的最小和最大长度,如下所示:
...
melody_extractor = melody_pipelines.MelodyExtractor(
 min_bars=15, max_steps=1024, min_unique_pitches=5,
 gap_bars=1.0, ignore_polyphonic_notes=False,
 name='MelodyExtractor_' + mode)
...

上面的代码将输出总共 92 个结果(而不是之前的 230 个)。

我们还可以编写我们自己的管道类。例如,我们可以自动剪切过长的序列,或者复制过短的序列,而不是丢弃它们。对于音符序列管道,我们需要扩展NoteSequencePipeline类,并实现transform方法,如以下代码所示:

from magenta.pipelines.note_sequence_pipelines import NoteSequencePipeline

class MyTransformationClass(NoteSequencePipeline):
  def transform(self, note_sequence):
    # My transformation code here
    pass

查看 Magenta 中的sequences_lib模块,该模块包含了许多用于处理音符序列的工具。每个数据集都需要进行准备,准备数据的最简单方法是创建新的管道。

查看 MusicVAE 数据转换

MusicVAE 模型不使用流水线——实际上,它甚至没有数据集创建脚本。与我们之前使用 Melody RNN 的例子相比,它仍然使用类似的变换(如数据增强),并且更具可配置性,因为一些变换可以进行配置,而不需要我们编写新的流水线。

让我们来看看music_vae模块中configs模块中包含的一个简单的 MusicVAE 配置。在这里,你可以找到以下cat-mel_2bar_small配置:

# Melody
CONFIG_MAP['cat-mel_2bar_small'] = Config(
    model=MusicVAE(lstm_models.BidirectionalLstmEncoder(),
                   lstm_models.CategoricalLstmDecoder()),
    hparams=merge_hparams(
        lstm_models.get_default_hparams(),
        HParams(
            batch_size=512,
            max_seq_len=32,  # 2 bars w/ 16 steps per bar
            z_size=256,
            enc_rnn_size=[512],
            dec_rnn_size=[256, 256],
            free_bits=0,
            max_beta=0.2,
            beta_rate=0.99999,
            sampling_schedule='inverse_sigmoid',
            sampling_rate=1000,
        )),
    note_sequence_augmenter=data.NoteSequenceAugmenter(transpose_range=(-5, 5)),
    data_converter=data.OneHotMelodyConverter(
        valid_programs=data.MEL_PROGRAMS,
        skip_polyphony=False,
        max_bars=100,  # Truncate long melodies before slicing.
        slice_bars=2,
        steps_per_quarter=4),
    train_examples_path=None,
    eval_examples_path=None,
)

以下列表进一步解释了代码:

  • 通过查看NoteSequenceAugmenter类,你可以看到它通过使用平移(如我们自定义的流水线中所做)和拉伸这一数据增强技术来进行音符序列的增强。

  • 它还将旋律的最大长度限制为max_bars=100,但请记住,MusicVAE 因其网络类型的原因,只能处理有限大小的样本。在这个例子中,每个样本被切片为slice_bars=2的长度。

  • 音符序列增强器让你决定一个转调范围,它将在这个范围内随机选择一个值。

  • Melody RNN 不使用拉伸,因为大多数拉伸比例对量化的序列不起作用。比如,Performance RNN 就可以使用拉伸。

我们现在不会讨论如何创建新的配置。有关如何做到这一点的更多信息,请参见第七章,训练 Magenta 模型

总结

在本章中,我们研究了如何构建和准备一个用于训练的数据集。首先,我们查看了现有的数据集,并解释了为什么一些数据集比其他数据集更适合特定的使用案例。接着,我们研究了 LMD 和 MSD,它们由于规模和完整性而具有重要价值,还研究了 Magenta 团队的数据集,如 MAESTRO 数据集和 GMD。我们还查看了外部 API,如 Last.fm,它可以用来丰富现有的数据集。

然后,我们构建了一个舞曲数据集,并使用 MIDI 文件中包含的信息来检测特定结构和乐器。我们学习了如何使用多进程计算结果,以及如何绘制关于生成的 MIDI 文件的统计数据。

接着,我们通过从 LMD 中提取信息,并使用 Last.fm API 来查找每首歌曲的流派,构建了一个爵士乐数据集。我们还研究了如何在 MIDI 文件中查找和提取不同的乐器轨道。

最后,我们为训练准备了数据。通过使用流水线,我们能够处理提取的文件,删除不符合适当长度的文件,对其进行量化,并使用数据增强技术创建一个适合训练的正确数据集。通过这样做,我们看到了不同模型根据其网络类型具有不同的流水线。

在下一章,我们将使用本章中产生的内容来训练一些模型,使用我们创建的数据集。你将看到,训练是一个经验过程,涉及数据准备与模型训练之间的大量反复。在这个过程中,你可能会回到这一章寻求更多的信息。

问题

  1. 不同符号表示法的优缺点是什么?

  2. 编写一段代码,从 MIDI 文件中提取大提琴乐器。

  3. LMD 中有多少首摇滚歌曲?有多少符合“爵士”,“蓝调”,“乡村”标签?

  4. 编写一段代码,扩展那些对于 Melody RNN 管道来说过短的 MIDI 文件。

  5. 从 GMD 中提取爵士鼓。我们能用这个训练量化模型吗?

  6. 为什么数据增强如此重要?

进一步阅读

第九章:训练 Magenta 模型

在本章中,我们将使用前一章准备的数据来训练一些 RNN 和 VAE 网络。机器学习训练是一个复杂的过程,涉及大量的调优、实验以及数据与模型之间的反复迭代。我们将学习如何调整超参数,如批次大小、学习率和网络大小,以优化网络性能和训练时间。我们还将展示常见的训练问题,如过拟合和模型不收敛。一旦模型训练完成,我们将展示如何使用训练好的模型生成新的序列。最后,我们将展示如何使用 Google Cloud Platform 在云端加速模型训练。

本章将涉及以下主题:

  • 选择模型和配置

  • 训练和调整模型

  • 使用 Google Cloud Platform

技术要求

在本章中,我们将使用以下工具:

  • 使用命令行Bash从终端启动 Magenta

  • 使用Python及其库编写特定的模型训练配置

  • 使用MagentaMagenta GPU来训练我们的模型

  • TensorBoard 用于验证训练指标

  • 使用Google Cloud Platform在云端进行训练

在 Magenta 中,我们将使用Drums RNNMelody RNNMusicVAE模型进行训练。我们将解释这些模型的训练过程,但如果你觉得需要更多信息,可以查看 Magenta 源代码中的模型 README (github.com/tensorflow/magenta/tree/master/magenta/models),它是一个不错的起点。你还可以查看 Magenta 的代码,它有很好的文档支持。我们还在最后一节提供了进一步阅读的内容。

本章的代码位于本书的 GitHub 仓库中的Chapter07文件夹,地址是github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter07。示例和代码片段假定你位于该章节的文件夹中。在本章开始之前,你应该先运行cd Chapter07

查看以下视频,查看代码的实际应用:

bit.ly/2OcaY5p

选择模型和配置

在第六章中,训练数据准备,我们研究了如何构建数据集。我们制作的数据集是符号型数据,由包含特定乐器(如打击乐器或钢琴)和特定音乐风格(如舞曲和爵士乐)的 MIDI 文件组成。

我们还探讨了如何准备数据集,这对应于将输入格式(MIDI、MusicXML 或 ABCNotation)准备为可以输入到网络中的格式。该格式是特定于 Magenta 模型的,这意味着即使两个模型都能在打击乐数据上进行训练,Drums RNN 和 MusicVAE 模型的准备工作也会有所不同。

在开始训练之前的第一步是为我们的使用案例选择合适的模型和配置。记住,Magenta 中的模型定义了一个深度神经网络架构,每种网络类型都有其优缺点。让我们来看看如何选择一个模型,配置它并从头开始训练它。

比较音乐生成的使用案例

让我们以训练一个打击乐模型为例。如果我们想要训练一个生成节奏性打击乐的模型,我们可以选择 Drums RNN 模型或 MusicVAE 模型:

  • 第一个模型,Drums RNN,将在生成较长序列时更有效,因为该模型可以通过注意力机制学习长期依赖关系,从而保持全球音乐结构(有关详细信息,请参见第二章,使用 Drums RNN 生成鼓序列)。

  • 第二个模型,MusicVAE,无法做到这一点,但能够从潜在空间中进行采样并在序列之间插值(有关详细信息,请参见第四章,使用 MusicVAE 进行潜在空间插值)。

根据你的使用案例,你可能想训练其中之一或两者,但请记住它们的优缺点。

如果我们以训练一个旋律模型为例,如果我们希望生成的结果是单声部的,我们可以使用单声部模型,如 Melody RNN 或 MusicVAE(具有之前提到的相同限制)。如果我们希望生成的结果是多声部的,我们可以使用多声部模型,如 Polyphony RNN。

有时,我们知道要使用哪个模型,但配置不适合我们的使用案例。让我们来看看如何创建一个新的配置。

创建新配置

我们将以我们想要使用 Music VAE 模型训练的贝斯数据集为例。查看magenta.models.music_vae模块中的configs模块,我们找到了cat-mel_2bar_small配置,它接近我们想要实现的目标,但当数据集被转换时,不对应旋律程序的音符(在 Magenta 中定义为 0 到 32)会被丢弃。

你可以在本章的源代码中的chapter_07_example_01.py文件中找到此代码。源代码中有更多的注释和内容,所以你应该去查看它。

为了实现这一点,我们将创建一个名为cat-bass_2bar_small的新配置,并将有效的程序更改为bass程序:

  1. 首先,让我们创建一个新的Config实例,内容如下:
from magenta.common import merge_hparams
from magenta.models.music_vae import Config
from magenta.models.music_vae import MusicVAE
from magenta.models.music_vae import lstm_models
from magenta.models.music_vae.data import BASS_PROGRAMS
from magenta.models.music_vae.data import NoteSequenceAugmenter
from magenta.models.music_vae.data import OneHotMelodyConverter
from tensorflow.contrib.training import HParams

cat_bass_2bar_small = Config(
  model=MusicVAE(lstm_models.BidirectionalLstmEncoder(),
                 lstm_models.CategoricalLstmDecoder()),
  hparams=merge_hparams(
    lstm_models.get_default_hparams(),
    HParams(
      batch_size=512,
      max_seq_len=32,
      z_size=256,
      enc_rnn_size=[512],
      dec_rnn_size=[256, 256],
      free_bits=0,
      max_beta=0.2,
      beta_rate=0.99999,
      sampling_schedule="inverse_sigmoid",
      sampling_rate=1000,
    )),
  note_sequence_augmenter=NoteSequenceAugmenter(transpose_range=(-5, 5)),
  data_converter=OneHotMelodyConverter(
    valid_programs=BASS_PROGRAMS,
    skip_polyphony=False,
    max_bars=100,
    slice_bars=2,
    steps_per_quarter=4),
  train_examples_path=None,
  eval_examples_path=None,
)

我们在这里唯一改变的部分是OneHotMelodyConverter中的valid_programs=BASS_PROGRAMS参数,但我们本可以更改其他元素,例如上一章提到的NoteSequenceAugmenter。超参数可以使用hparams标志进行更改,但如果我们希望为模型定义默认值,也可以在配置中定义它们。

  1. 要使用新的配置,我们可以调用music_vae_train模块的run方法:
import tensorflow as tf
from magenta.models.music_vae.configs import CONFIG_MAP
from magenta.models.music_vae.music_vae_train import run

def main(unused_argv):
  CONFIG_MAP["cat-bass_2bar_small"] = cat_bass_2bar_small
  run(CONFIG_MAP)

if __name__ == "__main__":
  tf.app.run(main)

在这里,我们导入整个配置映射并在调用run方法之前添加新的配置,以便我们仍然可以在--config标志中传递其他配置。

  1. 然后,我们可以像调用music_vae_train命令一样调用此代码:
python chapter_07_example_01.py --config="cat-bass_2bar_small" [FLAGS]

在这里,FLAGS是我们需要传递的训练标志,例如--run_dir--sequence_example_file

其他模型,如鼓乐 RNN 或旋律 RNN 模型,也将以相同的方式进行配置。有关如何操作的示例,请参考下一节。

现在我们知道如何选择模型和配置(或创建新配置),接下来看看如何开始和配置训练。

训练和调整模型

训练机器模型是一种经验性和迭代的方法,我们首先准备数据和配置,然后训练模型,失败后重新开始。在第一次尝试时让模型成功训练是很少见的,但我们会共同克服困难,继续前行。

在启动训练阶段时,我们将查看特定的度量指标,以验证我们的模型是否在正确训练并且收敛。我们还将启动评估阶段,它将在一个单独的小型数据集上执行,以验证模型是否能够在未见过的数据上正确地泛化。

评估数据集通常在机器学习中称为验证数据集,但我们将保持“评估”这一术语,因为在 Magenta 中使用的是这个术语。

验证数据集与测试数据集不同,测试数据集是外部数据集,通常由人工整理,并包含难度较大的样本,用来对网络性能进行最终测试。测试数据集通常用于比较不同模型的性能。我们这里不会涉及测试数据集。

我们将分解并解释每个步骤的过程。首先,让我们看看本章将使用的一些最佳实践和约定。

组织数据集和训练数据

由于训练是一个迭代过程,我们将生成许多数据集和多个训练运行。最好的做法是将这两者分开,例如,分别存放在名为datasetstraining的文件夹中。

datasets文件夹中,我们可以将上一章中生成的内容复制到不同的文件夹中,例如dance_drumsjazz_drumspiano_drums等,每个文件夹中包含 MIDI 文件:

  • 我们将notesequences.tfrecords文件保存在正确的数据集文件夹中,因为它仅在每个数据集上生成一次。

  • 我们将sequence_examples文件夹放在这个文件夹外面,因为它与模型相关,意味着我们将为每个模型重新生成这个文件夹,例如,Drums RNN 和 MusicVAE 各生成一次(即使我们使用相同的数据)。

training文件夹中,我们将为每个模型和数据集创建一个新文件夹,例如,drums_rnn_dance_drums

  • 我们将执行MODEL_create_dataset命令(如果可用),并为模型创建sequence_examples目录(如果有的话)。

  • 然后,我们将启动多个训练运行,并进行适当命名,例如,run1_with_dropout,或者其他我们可能想使用的配置:

拥有一个包含多个运行的单一训练文件夹非常有用,因为我们可以在 TensorBoard 中加载多个训练运行,并比较每个模型的表现。

在 CPU 或 GPU 上训练

训练是一项计算密集型活动。比如在入门级 GPU(例如 RTX 2060)上训练一个简单的模型,如 Drums RNN 模型,约需要 5 个小时。使用 CPU 训练则需要更多时间,因为网络训练中所需的操作(即向量运算)已被优化以在 GPU 上并行执行。要使用 GPU,我们还需要正确安装magenta-gpu包和 CUDA 库(有关更多信息,请参见第一章,Magenta 与生成艺术简介)。

如果你没有 GPU,不用担心,你仍然可以继续学习本章。你可以完成数据准备步骤,并在一个小型网络上启动训练(请参见后面第一个训练示例,了解如何进行)。然后,让网络训练一段时间,如果你看到令人鼓舞的结果,可以按照最后一节使用 Google 云平台中的步骤,在更快的机器上重新启动训练。这将允许你先在本地测试命令和数据集,然后将大部分工作卸载到 GCP。即使你有 GPU,这也可能是一个不错的选择,尤其是当你希望同时训练多个模型,或同时测试不同超参数时。

以下章节和命令同样适用于 CPU 和 GPU 训练,以及 GCP。

训练 RNN 模型

现在我们已经拥有了开始训练模型的所有元素。让我们以一个简单的例子开始,我们将使用上一章的dance_drums数据集,并训练 Drums RNN 模型。

你可以在本章源代码的README.md文件中找到这段代码。由于本章中的大多数代码片段是命令行格式,我们没有为每个示例提供示例文件。

从上一章开始,我们应该已经准备好了包含 MIDI 文件的datasets/dance_drums文件夹。我们已经执行了convert_dir_to_note_sequences命令,生成了notesequences.tfrecord文件。

创建数据集并启动训练

我们现在将创建数据集(这是我们在上一章已经做过的操作,但在这里再次展示以帮助复习),并启动训练。

  1. 首先,让我们创建序列示例。在training文件夹中创建并切换到名为drums_rnn_dance_drums的新文件夹,并执行(将PATH_TO_NOTE_SEQUENCES替换为正确的文件):
drums_rnn_create_dataset --config="drum_kit" --input="PATH_TO_NOTE_SEQUENCES" --output_dir="sequence_examples" --eval_ratio=0.10

这应该会创建一个sequence_examples文件夹,里面包含两个文件,一个是训练集,另一个是鼓序列的评估集。

理想情况下,drums_rnn_create_dataset命令应该只在所有训练运行中调用一次。由于我们将在每次运行之间调整超参数,而超参数对训练数据非常敏感,因此在调优模型时改变训练和评估数据集并不是一个好主意。

  1. 我们现在将开始使用一个小型网络进行训练:
drums_rnn_train --config="drum_kit" --run_dir="logdir/run1_small" --sequence_example_file="sequence_examples/training_drum_tracks.tfrecord" --hparams="batch_size=64,rnn_layer_sizes=[64,64]" --num_training_steps=20000

在 Windows 上,--run_dir标志应该使用反斜杠。对于这个例子以及所有后续的例子,不要写--run_dir="logdir/run1_small",而应使用--run_dir="logdir\run1_small"

我们使用一个名为run1_small的输出目录,这样以后可以记住是哪次运行,以及一个名为training_drum_tracks.tfrecord的输入文件。超参数是批量大小为 64,每层 64 个单元的两层 RNN 网络,列表中定义了层数。对于一个 3 层 RNN 网络,使用[64, 64, 64]。

你应该在终端看到完整的超参数列表及其值,这些值来自配置文件,除非被标志覆盖。

INFO:tensorflow:hparams = {'batch_size': 64, 'rnn_layer_sizes': [64, 64], 'dropout_keep_prob': 0.5, 'attn_length': 32, 'clip_norm': 3, 'learning_rate': 0.001, 'residual_connections': False, 'use_cudnn': False}

我们很快就会看到超参数如何影响训练。在接下来的章节中,如果你使用的是 GPU,请通过检查以下输出确保 TensorFlow 可以使用你的 GPU:

2019-11-20 01:56:12.058398: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1304] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 5089 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1060 6GB, pci bus id: 0000:01:00.0, compute capability: 6.1)
  1. 现在,网络将开始训练,你应该看到类似这样的输出:
INFO:tensorflow:Accuracy = 0.27782458, Global Step = 10, Loss = 5.4186254, Perplexity = 225.56882 (16.487 sec)
INFO:tensorflow:global_step/sec: 0.586516

我们很快会讨论不同的评估指标。当我们启动训练时,使用了--num_training_steps=20000标志,这意味着网络将在达到 20,000 个全局步骤后停止训练。我们在这里不会讨论 epoch,它是指遍历训练数据的完整周期,因为我们在 Magenta 中只处理步骤。模型应该会在此之前收敛,但给出一个上限是有好处的,这样它就不会无故执行太久。

如果你想大致估算训练完成 20,000 步所需的时间,可以使用global_step/sec输出。对于之前的输出,我们的工作大约需要 9 小时完成,但这是一个上限,因此我们有可能在此之前停止它。

现在训练已启动,我们可以启动评估。

启动评估

评估作业在评估数据集上执行,该数据集比训练集小(我们之前使用了--eval_ratio=0.10标志,表示 10%),且与训练集分开。评估作业对模型进行评估,并计算损失函数,但不会更新网络中的任何权重。因此,评估过程非常快速,可以与训练作业在 CPU 上同时执行。

要启动评估,我们使用相同的命令,并使用--eval标志。如果你正在使用 GPU,你需要通过CUDA_VISIBLE_DEVICES=""环境变量禁用 GPU 执行,因为之前的 TensorFlow 进程占用了所有可用内存。

在 Windows 上,不要忘记在--run_dir标志中使用反斜杠。此外,在 Windows 上,使用set命令为当前命令行会话设置环境变量。在 Linux 和 macOS 上,你可以通过在命令前面加上变量值来为单个命令设置环境变量。

在 Windows 上,使用以下命令:

set CUDA_VISIBLE_DEVICES=""
drums_rnn_train --config="drum_kit" --run_dir="logdir\run1_small" --sequence_example_file="sequence_examples/eval_drum_tracks.tfrecord" --hparams="batch_size=64,rnn_layer_sizes=[64,64]" --num_training_steps=20000 --eval

在 Linux 和 macOS 上,使用以下命令:

CUDA_VISIBLE_DEVICES="" drums_rnn_train --config="drum_kit" --run_dir="logdir/run1_small" --sequence_example_file="sequence_examples/eval_drum_tracks.tfrecord" --hparams="batch_size=64,rnn_layer_sizes=[64,64]" --num_training_steps=20000 --eval

对于这个命令,提供的网络大小需要与训练时的网络大小一致。如果你在训练命令中使用了rnn_layer_sizes=[64,64],那么这里也需要使用相同的配置。我们从之前的命令中改变了两个标志,分别是--eval--sequence_example_file标志。

当运行目录中添加了一个新的检查点(大约每 40 步添加一次)时,评估将开始执行。发生这种情况时,你会看到类似以下的输出:

Starting evaluation at 2019-11-25-23:38:24
INFO:tensorflow:Accuracy = 0.0, Global Step = 35, Loss = 0.0, Perplexity = 1.0
INFO:tensorflow:Evaluation [1/3]
INFO:tensorflow:Evaluation [2/3]
INFO:tensorflow:Evaluation [3/3]
Finished evaluation at 2019-11-25-23:38:30

当训练作业一段时间未生成检查点时,评估作业会自动停止。

查看 TensorBoard

在训练期间以及训练后,我们可以启动 TensorBoard,帮助可视化网络指标。我们将使用 TensorBoard 来调整超参数并与数据准备阶段进行迭代。

要启动 TensorBoard,请使用以下命令:

tensorboard --logdir=logdir

请注意,我们传递了父输出目录,这意味着我们可以访问所有包含的运行(目前只有一个)。我们可以在控制台中找到 TensorBoard 的 URL。打开后,页面将如下所示:

这是我们训练模型经过 20,000 步后的结果。左侧是训练和评估作业,可以切换显示。右侧的截图显示了不同的指标,其中横坐标是全局步数,达到了 20,000 步。对我们来说最重要的两个指标是损失准确度。我们希望损失下降,无论是训练集还是评估集,同时希望准确度上升

我们注意到该模型已经收敛,这意味着我们成功地完成了训练,但我们需要通过查看损失指标来验证结果模型的好坏。让我们来看看损失函数,比较训练损失和评估损失:

我们可以看到这里模型略微过拟合了训练数据。您可以通过找到评估损失曲线的最低点来找到模型的最优点,在该点之前曲线下降,之后曲线开始上升。在该点的左侧,模型在欠拟合训练数据;而在右侧,模型在过拟合数据。两条曲线之间的差异被称为泛化误差。

在继续其他例子之前,我们先来解释一下欠拟合和过拟合。

解释欠拟合和过拟合

理解欠拟合和过拟合以及如何防止它们,对于正确的网络训练至关重要。当模型过于简单,无法从训练数据集中学习时,我们称模型为欠拟合。另一方面,当模型能够正确地从训练数据集中学习,但无法将其泛化到数据集之外时,我们称模型为过拟合

我们在下图中展示了欠拟合、最优解和过拟合:

在左侧,我们展示了欠拟合,意味着模型没有在数据集上学习。中间展示了一个最优解,而右侧展示了过拟合,其中得到的模型对于给定数据来说过于复杂,这意味着该模型无法泛化到其他数据集。

请记住,神经网络的目标是既能在训练数据集上表现良好,也能在从未见过的新数据上表现良好,而这些数据将用于做出预测。要实现这一目标非常困难,因为这需要在数据的质量和数量、网络规模和学习参数之间找到适当的平衡。

让我们看看如何解决这些问题。

解决欠拟合问题

欠拟合问题容易解决,可以通过增加模型的容量来解决——基本上就是通过增加更多的层和单元。通过增加模型的容量,网络可以学习更多种类的函数,将输入映射到输出。

对于我们之前的例子,我们可以通过在两个层中每层增加更多的单元(神经元)来增加模型的容量:

drums_rnn_train --config="drum_kit" --run_dir="logdir/run1_small" --sequence_example_file="sequence_examples/training_drum_tracks.tfrecord" --hparams="batch_size=64,rnn_layer_sizes=[128,128]" --num_training_steps=20000

我们还需要训练足够长的时间,这可能与硬件有关,因为如果我们在性能较慢的硬件上训练,可能需要很长时间才能达到最优点。

有关网络规模的更多信息,请参见定义适当的网络规模和超参数一节。

解决过拟合问题

另一方面,过拟合问题更难解决,因为它源于多种因素。最常见的两种原因如下:

  • 当模型出现过拟合时,是因为它具有过拟合训练数据集的能力。通过保持相同的训练数据集,您可以减少网络容量,以便网络没有足够的资源来过拟合训练数据。要减少网络容量,可以像之前的例子那样使用rnn_layer_sizes超参数。

  • 通过保持相同的网络容量,你可以增加训练数据集的大小,这样,随着数据的增加,网络可能就不再有过拟合的能力。添加的数据需要足够多样化,以修复过拟合问题,但并不总是有效。要增加训练数据集的大小,请返回到第六章,数据准备训练,并向数据集添加内容。请注意,如果增加的数据不够多样化,将无助于解决过拟合问题。

训练数据集与网络大小之间存在关系:数据集越大且越多样化,网络就越大。如果训练数据集不够大或者质量不够好,一个更大的网络不一定会产生更好的结果。

在 Magenta 模型中,有其他修复过拟合的方法可以使用:

  • 提前停止在最佳点结束训练阶段是一种方法,因为在那一点之后的所有训练都会使结果网络变得更差。要使用提前停止,请参见下一节,使用特定的检查点实现提前停止

  • 使用诸如dropout这样的正则化技术,它会随机且临时地从网络中丢弃一个单元/神经元。要使用 dropout,请使用dropout_keep_prob超参数。

正则化技术是一类方法,旨在约束神经网络中权重的大小,并广泛用作预防过拟合的方法。

正如你现在可能已经注意到的,我们的数据集和模型之间存在关系,在调整训练阶段时需要考虑这一点。让我们更详细地看一下网络大小和超参数。

定义网络大小和超参数

定义适当的网络大小是一个反复试验的过程,但是一个很好的起点是,如果你的硬件足够好,那么就使用你想要的模型配置中的数值。

让我们举一个例子,使用 Melody RNN 模型的attention_rnn配置,使用batch_size=128rnn_layer_sizes=[128, 128]dropout_keep_prob=0.5learning_rate=0.001clip_norm=3

  1. 如果模型出现过拟合,我们可以尝试以下方法:

    1. 例如,使用更多的 dropout,如dropout_keep_prob=0.4和较低的值。

    2. 添加更多数据

    3. 使用rnn_layer_sizes=[64, 64]来减小网络大小

  2. 如果模型正在收敛且没有过拟合,我们可以尝试使用更大的模型,例如rnn_layer_sizes=[256, 256]。如果我们有足够好的数据,使用更大的模型将产生更好的结果,因此我们希望优化这一点。

在更改某些内容时,我们需要确保每次只进行单一修改,然后测试结果,而不是同时更改多个参数。同时更改多个参数会阻止我们了解每个参数的直接影响。

有时,当增加网络大小时,我们可能会遇到模型无法收敛的情况,这意味着损失函数开始再次增加,从而导致训练错误。通过更改learning_rateclip_norm可以解决这个问题。有关更多信息,请参阅下一节,修复无法收敛的模型

确定批量大小

我们还没有讨论过batch_size。批量大小是网络一次处理的数据量。较大的批量大小可以通过使模型参数更快地收敛来提高训练时间。它还应该通过一次将更多数据传输到 GPU 内存中来减少一些计算开销。

一个经验法则是,当你增加批量大小时,你也需要增加学习率。由于一次处理的数据更多,模型的权重可以使用更大的比例进行更新。

增加批量大小可能会改善训练时间,但也可能降低模型的性能,因此使用过大的批量大小可能不是一个好主意。总的来说,批量大小通常是执行时间和模型质量之间的权衡。

我们在最后一节中提供了更多信息,进一步阅读

修复内存溢出错误

有时,当使用过大的批量大小或网络大小时,你可能会遇到以下错误:

[tensorflow/stream_executor/cuda/cuda_driver.cc:890] failed to alloc 8589934592 bytes on host: CUDA_ERROR_OUT_OF_MEMORY: out of memory

减小批量大小和网络大小,直到内存溢出错误消失。有时,错误并不致命,在这种情况下,它会对训练性能产生负面影响。

修复错误的网络大小

当使用现有的运行目录时,无论是继续先前的训练、启动评估作业,还是启动生成作业,我们都需要提供与首次启动时相同的网络大小。

如果训练运行首次使用rnn_layer_sizes=[256,256,256]启动,然后使用rnn_layer_sizes=[128,128,128]重新启动,我们将遇到以下错误:

Invalid argument: Assign requires shapes of both tensors to match. lhs shape= [128] rhs shape= [256]

在这种情况下,我们需要使用我们在开始训练时使用的网络大小。

修复无法收敛的模型

收敛的模型由训练集和评估集上的损失函数下降定义。如果我们的损失函数在某个时刻上升,模型就不稳定,并且没有正确地收敛。模型无法收敛的原因有很多。

让我们以一个简单的例子为例(这个例子使用了我们在上一章创建的jazz_drums数据集):

drums_rnn_train --config="drum_kit" --run_dir="logdir/run1_diverge" --sequence_example_file="sequence_examples/training_drum_tracks.tfrecord" --hparams="batch_size=128,rnn_layer_sizes=[128,128,128]" --num_training_steps=20000

当模型发散时,我们可能会在某个时候遇到错误:

E1112 20:03:08.279203 10460 basic_session_run_hooks.py:760] Model diverged with loss = NaN.
...
  File "c:\users\magenta\appdata\local\programs\python\python35\lib\site-packages\tensorflow\python\training\basic_session_run_hooks.py", line 761, in after_run
    raise NanLossDuringTrainingError
tensorflow.python.training.basic_session_run_hooks.NanLossDuringTrainingError: NaN loss during training.

结果的 TensorBoard 图将显示损失函数的上升。让我们通过使用较小的学习率来解决这个问题。前一个命令中使用的默认学习率值是learning_rate=0.001,所以我们将其调整为原来值的十分之一:

drums_rnn_train --config="drum_kit" --run_dir="logdir/run2_learning_rate" --sequence_example_file="sequence_examples/training_drum_tracks.tfrecord" --hparams="learning_rate=0.0001,batch_size=128,rnn_layer_sizes=[128,128,128]" --num_training_steps=20000

这是包含两次运行结果的 TensorBoard 图:

你可以看到,run1_diverge的损失函数在不断增大,而run2_learning_rate正在正常训练。

修复发散模型的方法有很多,但由于问题依赖于数据和网络规模,你需要测试各种方法:

  • 尝试降低学习率,就像在我们之前的示例中一样。学习率衰减(在 Music VAE 模型中可用),即学习率逐渐降低,也能有所帮助。

  • 尝试更改网络规模。在这个示例中,使用rnn_layer_sizes=[256,256,256]的网络规模也可以修复该问题。

  • 尝试减少梯度裁剪。在我们之前的示例中,梯度裁剪的默认值是clip_norm=3,因此你可能需要减少clip_norm超参数的值,例如设置为clip_norm=2。记住,默认的超参数值会在每个模型的配置文件中(有关更多信息,请参见前一节,创建新配置)。

有时候,修复一个发散的模型会引发另一个问题。例如,使用更大的网络规模来修复问题可能会导致网络过拟合。确保你测试多个解决方案,以便选择最合适的一个。

在训练过程中,NaN 损失错误大多是由我们在第四章中提到的梯度爆炸问题引起的,生成多声部旋律。这个问题在 RNN 中很常见,即使 LSTM 单元有助于模型的收敛,但由于在数百个输入时间步长上展开的梯度累积,梯度爆炸问题仍然可能发生。

在训练过程中,损失函数会在训练示例上计算,然后其导数会通过网络反向传播,按照传播误差的一个比例更新权重,这个比例就是学习率。当权重被不断更新且更新值过大时,往往会导致爆炸或溢出,这也是为什么使用较小的学习率可能修复该问题。

梯度裁剪有类似的效果,但如果我们不想改变学习率,它仍然是有用的。通过使用梯度裁剪,我们可以重新调整或裁剪梯度向量(即误差导数)的最大值,该梯度会通过网络反向传播。在 Magenta 中,我们提供了clip_norm参数,它在 TensorFlow 中被用作tf.clip_by_norm(t, clip_norm)。通过减少参数的值,我们有效地对误差梯度进行归一化,使得其范数小于或等于提供的值。

修复不足的训练数据

现在我们使用上一章的爵士钢琴数据集来训练 Melody RNN 模型:

  1. 我们首先使用attention_rnn配置创建数据集。在training文件夹中,创建并切换到一个名为melody_rnn_jazz_piano的新文件夹,然后执行(将PATH_TO_NOTE_SEQUENCES替换为适当的文件路径,文件应该在你的datasets文件夹中):
melody_rnn_create_dataset --config="attention_rnn" --input="PATH_TO_NOTE_SEQUENCES" --output_dir="sequence_examples" --eval_ratio=0.10
  1. 然后我们使用以下内容训练模型:
melody_rnn_train --config="attention_rnn" --run_dir="logdir/run1_few_data" --sequence_example_file="sequence_examples/training_melodies.tfrecord" --hparams="batch_size=128,rnn_layer_sizes=[128,128]" --num_training_steps=20000

在 TensorFlow 中查看时,我们可以查看run1_few_data的运行情况:

损失图中,顶部的前两条线是run1_few_data运行的训练和评估指标。评估损失值上升,意味着模型在快速过拟合。这是因为我们没有很多数据(确切地说是 659 个输出)。

解决这个问题需要我们回到数据准备步骤。对于run2_more_data运行,在损失图中,最底部的两条曲线表明问题已经解决。为了获取更多的数据,我们回到上一章的管道melody_rnn_pipeline_example.py,并将MelodyExtractor管道中的ignore_polyphonic_notes=False更改为True。这意味着,管道不再丢弃多声部旋律,而是将其转换为单声部旋律,保留最高的音符。转换方法在melodies_lib模块中,如果我们想改变这个行为,就必须编写我们自己的管道。

因为这个变化修改了我们数据集中的音乐内容,我们需要仔细听生成的结果,以验证训练好的模型是否输出有趣的样本。以下是来自run2_more_data训练模型的生成样本:

这个例子很好地说明了在数据准备步骤和训练步骤之间反复切换的重要性。请参阅下一节,从训练好的模型生成序列,以获取更多关于如何从训练模型生成序列的信息。

配置注意力和其他超参数

Melody RNN 模型在训练时使用注意力机制查看前面的步骤(有关此内容的回顾,请参见第三章,生成多声部旋律)。你可以使用attn_length超参数来配置注意力范围的长度。

每个模型都有自己的配置。确保查看它们各自的配置文件,了解在训练过程中可以调整的参数。

从训练好的模型生成序列

现在我们已经有了一个训练好的模型run1_small,并且知道它稍微出现了过拟合,接下来我们来尝试从它生成一个序列,看看结果如何。要在网络训练完成后或训练过程中生成序列,我们可以使用模型的generate命令。

我们来使用以下参数调用drums_rnn_generate方法(如果你在训练过程中启动此命令并且使用 GPU,请记得使用CUDA_VISIBLE_DEVICES=""):

drums_rnn_generate --config="drum_kit" --run_dir="logdir/run1_small" --hparams="rnn_layer_sizes=[64,64]" --output_dir="generated/generated1_small"

generate命令将会获取运行目录中的最新检查点,并用它生成一个序列。我们需要使用与训练阶段相同的网络大小。

这是我们训练阶段生成的一个序列:

模型应该已经生成了 10 个序列;我们应该听一听它的声音是什么样的。恭喜!你已经听到了自己生成模型的第一音符。

使用dance数据集的好处在于,可以轻松验证训练模型是否生成了我们预期的内容:在生成的 10 个序列中,每个序列基本上都有一个低音鼓在节拍上。现在,我们需要问自己,生成的结果是否具有多样性和趣味性。如果没有,我们应该回去准备一个新的数据集并进行迭代。

使用特定的检查点实现提前停止

我们之前讨论过提前停止,这是在最优点停止训练的操作,而不是让模型进一步退化,以防止过拟合。有多种方式可以做到这一点,比如编写一个停止条件,检查评估损失函数是否开始上升,但最简单的方法是在训练阶段结束后仅保留距离最优点最近的检查点。

回到之前的例子,我们发现评估损失曲线的最小值大约在 7000 步时。在logdir/run1_small目录中,我们发现一个接近最优的检查点可以使用:model.**ckpt-6745**.data-00000

要使用该检查点,我们需要使用--checkpoint_file标志,而不是--run_dir标志:

drums_rnn_generate --config="drum_kit" --checkpoint_file="logdir/run1_small/train/model.ckpt-6745" --hparams="batch_size=64,rnn_layer_sizes=[64,64]" --output_dir="generated/generated1_small"

注意,我们没有传递完整的文件名(只使用model.ckpt-6745,而不是model.ckpt-6745.data-00000-of-00001),因为 TensorFlow 只需要传递文件名的第一部分。该命令应该使用该检查点生成 10 个新的元素。

使用捆绑包打包并分发结果

当我们对训练好的模型感到满意时,可以使用 Magenta 的捆绑包进行打包以供分发。记住,捆绑包仅适用于 RNN 模型,但我们将在后面提供将其他模型(如 Music VAE)打包的方法。按照以下步骤操作:

  1. 要打包一个捆绑包,我们使用--bundle_file--save_generator_bundle标志调用生成命令:
drums_rnn_generate --config="drum_kit" --run_dir="logdir/run1_small" --hparams="batch_size=64,rnn_layer_sizes=[64,64]" --bundle_file="drums_rnn_dance_small.mag" --save_generator_bundle

这将使用最新的检查点将捆绑包保存在drums_rnn_dance_small.mag文件中。如果我们需要另一个不是最新的检查点,可以使用之前命令中的--checkpoint_file标志。

  1. 我们现在可以按如下方式使用捆绑包:
drums_rnn_generate --config="drum_kit" --bundle_file="drums_rnn_dance_small.mag" --output_dir="generated/generated1_small"

注意超参数是被省略的——这是因为它们已在捆绑包文件中配置。这也意味着捆绑包中的超参数将覆盖在drum_kit配置中的设置。

现在我们已经训练、调优并打包了第一个模型,我们将继续看看如何训练其他模型。

训练 MusicVAE

现在让我们训练 MusicVAE 模型,以便可以将采样与 RNN 生成进行比较。MusicVAE 训练的一个不同之处在于,数据准备步骤(create dataset命令)不存在,因为数据转换是在模型开始训练之前完成的。我们将手动使用管道创建数据集,然后开始训练。

将数据集拆分为评估集和训练集

由于没有创建数据集的命令,但我们仍然需要将数据集拆分为训练数据和评估数据,因此我们将编写一个管道来完成此操作。

你可以在本章的源代码中的chapter_07_example_02.py文件中找到这段代码。源代码中有更多的注释和内容,建议你去查看一下。

我们还将把笔记序列转换为张量,这将帮助我们在开始训练之前验证数据:

  1. 首先,我们编写partition方法,将数据集拆分为训练数据和评估数据:
from magenta.music.protobuf.music_pb2 import NoteSequence
from magenta.pipelines.dag_pipeline import DAGPipeline
from magenta.pipelines.dag_pipeline import DagInput
from magenta.pipelines.dag_pipeline import DagOutput
from magenta.pipelines.pipeline import run_pipeline_serial
from magenta.pipelines.pipeline import tf_record_iterator
from magenta.pipelines.pipelines_common import RandomPartition

def partition(config: str, input: str, output_dir: str, eval_ratio: int):
  modes = ["eval", "train"]
  partitioner = RandomPartition(NoteSequence, modes, [eval_ratio])
  dag = {partitioner: DagInput(NoteSequence)}
  for mode in modes:
    validator = TensorValidator(NoteSequence, f"{mode}_TensorValidator", config)
    dag[validator] = partitioner[f"{mode}"]
    dag[DagOutput(f"{mode}")] = validator

  pipeline = DAGPipeline(dag)
  run_pipeline_serial(
    pipeline, tf_record_iterator(input, pipeline.input_type), output_dir)

我们在前一章已经看到过类似的代码;我们实际上是在重复使用我们之前讲解过的RandomPartition类,该类将使用给定的比例将输入拆分为两个集合。

  1. 然后,让我们编写TensorValidator类:
from magenta.models.music_vae.configs import CONFIG_MAP
from magenta.pipelines.pipeline import Pipeline

class TensorValidator(Pipeline):

  def __init__(self, type_, name, config):
    super(TensorValidator, self).__init__(type_, type_, name)
    self._model = CONFIG_MAP[config]
    self._data_converter = self._model.data_converter

  def transform(self, note_sequence):
    tensors = self._data_converter.to_tensors(note_sequence)
    if not tensors.lengths:
      path = str(note_sequence).split('\n')[0:2]
      print(f"Empty tensor for {path}")
      return []
    return [note_sequence]

这里有趣的是,我们使用配置来找到数据转换器(鼓转换、旋律转换等),然后将其转换为张量,这个步骤会在模型开始训练之前完成。这个步骤验证了我们的输入数据,并可以帮助我们统计“有效”张量的数量以及我们有多少数据。不幸的是,由于没有“创建数据集”的命令,我们很难准确知道将被馈送到网络的数据类型,这就是这个类有用的原因。

  1. 最后,我们将调用partition方法并声明一些命令行标志:
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--config", type=str, required=True)
parser.add_argument("--input", type=str, required=True)
parser.add_argument("--output_dir", type=str, required=True)
parser.add_argument("--eval_ratio", type=float, default=0.1)

def main():
  args = parser.parse_args()
  partition(args.config, args.input, args.output_dir, args.eval_ratio)

if __name__ == "__main__":
  main()
  1. 现在,让我们创建一个新的训练目录,然后调用我们的 Python 脚本(将PATH_TO_PYTHON_SCRIPTPATH_TO_DATASET_TFRECORDS替换为正确的值):
python PATH_TO_PYTHON_SCRIPT --config="cat-drums_2bar_small" --input="PATH_TO_DATASET_TFRECORDS" --output_dir="sequence_examples"

这将创建一个名为sequence_examples的目录,其中包含eval.tfrecordstrain.tfrecords数据集。

启动训练和评估

现在,我们已经验证了数据并将其拆分为两个数据集,我们可以开始训练了。

启动训练、评估和 TensorBoard 与前面的章节类似:

# Start the training job
music_vae_train --config="cat-drums_2bar_small" --run_dir="logdir/run1" --mode="train" --examples_path="sequence_examples/train.tfrecord"

# Start the evaluation job
music_vae_train --config="cat-drums_2bar_small" --run_dir="logdir/run1" --mode="eval" --examples_path="sequence_examples/eval.tfrecord"

# Start the TensorBoard
tensorboard --logdir=logdir

与之前的模型一样,你可以使用--hparams=FLAGS标志传递超参数。在这里,我们使用的是“小型”配置,因为 MusicVAE 模型的大小增长得很快。一个小型模型就足够提供良好的性能。例如,Magenta 预训练的鼓模型使用的是cat-drums_2bar_small配置。

在训练 Music VAE 时,我们还需要调整以下两个超参数:free_bitsmax_beta。通过增加free_bits或减少max_beta,我们减少了 KL 损失的影响,从而得到一个在重建上表现更好的模型,但随机样本可能会变差。如果你不记得Kulback-LeiblerKL)散度如何影响模型性能,可以查看前一章,第四章,使用 MusicVAE 进行潜在空间插值

分发训练好的模型

不幸的是,对于 MusicVAE,我们无法创建一个 Magenta 包。分发 TensorFlow 检查点的最简单方法是复制检查点文件并将其压缩以便传输:

  1. 首先,让我们复制相应的文件(将 STEP 替换为你想保留的检查点步骤):
mkdir "trained/cat-drums_2bar_small"
cp logdir/run1/train/model.ckpt-STEP* "trained/cat-drums_2bar_small"

你现在应该在 complete/cat-drums_2bar_small 目录中有三个文件。记住,TensorFlow 检查点应该使用其前缀加载,例如 model.ckpt-157780

  1. 输入以下内容以在生成中使用检查点(将 STEP 替换为你想要使用的检查点):
music_vae_generate --config="cat-drums_2bar_small" --checkpoint_file="trained/cat-drums_2bar_small/model.ckpt-STEP"

请记住,检查点不包含关于你对超参数所做的更改的信息(与 Magenta 包不同),因此每次使用时,你需要传递训练时使用的相同超参数。

一些超参数是可以更改的。例如,使用 512 的批量大小进行采样是没有意义的(除非你一次采样 512 个序列),但这可能是你在训练时使用的值。

你需要保持与 TensorFlow 图相关的所有内容,包括网络大小以及与编码和解码相关的任何内容。

为这个特定训练创建配置可能是跟踪所使用超参数的最简单方法。

训练其他模型

我们不会在这里训练所有模型,但训练其他模型应该与我们展示的相似。针对过拟合的相同模型调优可以应用到其他模型上。请参考你想要训练的模型的 README 文件以获取更多信息。

使用 Google Cloud Platform

使用云计算服务提供商可以将计算任务转移到更快的机器上,这非常有用。如果我们希望同时进行多次运行,也可以使用云计算。例如,我们可以通过启动两个运行来尝试修复梯度爆炸:一个使用较低的学习率,另一个使用较低的梯度裁剪。我们可以启动两个不同的虚拟机,每个训练自己的模型,然后比较哪个表现更好。

我们将使用 Google Cloud Platform(GCP),但其他云服务提供商,如 Amazon AWS 或 Microsoft Azure,也可以使用。我们将介绍训练上一章中的钢琴爵士乐数据集的 Melody RNN 模型所需的不同步骤,包括 GCP 账户配置和虚拟机实例创建。

创建和配置一个账户

首先,访问 console.cloud.google.com,并创建一个新的 Google 账户(或使用现有账户)。进入 GCP 后,按照以下步骤操作:

  1. 如果这是你第一次登录,你需要创建一个新的项目,可以命名为 Magenta。如果不是,找到屏幕顶部当前的项目,如果需要可以创建一个新的项目。

  2. 然后,我们需要设置配额,因为新账户创建时,默认不能创建带 GPU 的虚拟机实例。在左侧,进入IAM 与管理员 > 配额,并通过在指标字段中搜索GPU来找到GPU(所有区域)的配额。勾选框后,点击编辑,并将配额更改为其他值,如 5。配额修改需要一些时间才能验证通过。

  3. 最后,我们需要设置结算。在左侧,进入结算,然后按照提示添加结算账户

账户设置完成后,我们可以创建一个新的虚拟机实例。

准备 SSH 密钥(可选)

使用 SSH 密钥有助于通过本地终端连接到虚拟机实例。这是一个可选步骤,因为在 GCP 中,你也可以使用浏览器中的终端连接虚拟机实例,虽然这种方式很有效,但上传和下载速度非常慢。

如果你已经有了 SSH 密钥,可以跳过此步骤。如果不确定,请检查~/.ssh/id_rsa.pub文件。

在 Linux 和 macOS 上,你可以通过在终端中输入以下命令生成新的 SSH 密钥:

ssh-keygen

这将在~/.ssh/id_rsa.pub中保存一个密钥。在 Windows 上,最简单的方法是安装 Git Bash(git-scm.com/download/win),它包含我们将使用的两个命令——ssh-keygenscp,我们将在接下来的部分中使用这些命令。

生成后,公钥看起来是这样的:

ssh-rsa AAAA... user@host

主机前面的user部分很重要,因为它将在登录时作为提供给 GCP 的用户。

从 TensorFlow 镜像创建虚拟机实例

现在,我们返回到 GCP,在左侧菜单中,进入计算引擎

  1. 进入计算引擎,从左侧菜单中选择镜像

  2. 筛选镜像搜索框中,输入TensorFlow并找到最新的镜像。在撰写本文时,该镜像名为c3-deeplearning-tf-1-15-cu100-20191112

  3. 选择镜像后,点击创建实例;你将看到创建实例的界面:

接下来,我们将根据前面的示意图填写信息:

  1. 名称中,使用Magenta

  2. 区域区域中,选择一个靠近你的地方。

  3. 机器类型中,选择至少n1-standard-4,即配备 4 核 CPU 和 15GB 内存的配置。

  4. CPU 平台和 GPU中,点击添加 GPU,并选择至少一款NVIDIA Tesla K80 GPU。

根据选择的区域和当前的可用性,你将看到不同的 GPU 可供选择。NVIDIA Tesla K80 GPU 的计算能力平均为(Melody RNN 上为 0.45 全球步长/秒),而 P100 GPU 的计算能力几乎是其两倍(Melody RNN 上为 0.75 全球步长/秒)。作为比较,入门级游戏 GPU 如 RTX 2060,在 Melody RNN 上每秒可处理 0.6 全球步长。

接下来,我们来处理磁盘内容:

我们将按照以下步骤初始化实例:

  1. 启动磁盘应该已经填充了 深度学习镜像,大小为 50 GB。

  2. 在展开 管理、安全、磁盘、网络、单独租用 部分后,在 安全 标签中粘贴你的公钥 SSH 密钥(如果有的话)。结果用户(在此例中为 user)将显示在左侧。

  3. 检查右上角显示的价格,大约是每小时 1.269 美元。请注意,我们按机器的运行时间收费。如果不使用该机器,我们将不会被收费,所以完成后我们需要关闭它。

  4. 点击 创建

在左侧的 虚拟机实例 菜单中,你应该能看到新创建的虚拟机。

初始化虚拟机(VM)

现在我们有了一个新的虚拟机来工作,我们需要安装所有必需的软件。我们可以参考 第一章,Magenta 和生成艺术的介绍,以获取详细的安装说明,但我们也会在这里提供主要命令。

安装 NVIDIA CUDA 驱动程序

幸运的是,我们使用的虚拟机镜像为我们做了很多安装工作。首先,让我们连接并安装所需的驱动程序:

  1. 首先,我们需要连接到虚拟机。如果没有 SSH 密钥,我们可以使用虚拟机实例右侧的 SSH 按钮,启动新的浏览器终端。如果有 SSH 密钥,我们可以使用以下命令,适用于 Linux、macOS 和 Windows(使用 Git Bash):
ssh USER@IP

在这里,我们需要将 USER 替换为我们 SSH 密钥中的用户,并将 IP 替换为虚拟机实例页面中显示的外部 IP。

  1. 虚拟机会用以下消息问候我们,我们需要回答y
This VM requires Nvidia drivers to function correctly. Installation takes ~1 minute.
Would you like to install the Nvidia driver? [y/n] y

NVIDIA 驱动程序(CUDA 驱动程序和 cuDNN),以及适用于 TensorFlow 的正确版本应该已经安装。不幸的是,cuDNN 版本存在问题,我们将需要手动重新安装它。

  1. developer.nvidia.com/rdp/cudnn-download 下载适用于 CUDA 10.0 的最新 cuDNN a.b.c(下载完整的 cuDNN 库)到本地机器。

  2. 将 cuDNN 归档文件传输到虚拟机实例。如果没有 SSH 密钥,我们将使用界面来传输文件(在右上角)。如果有 SSH 密钥,我们可以使用终端:

scp PATH_TO_CUDNN_ARCHIVE USER@IP:

我们需要将 PATH_TO_CUDNN_ARCHIVEUSERIP 替换为适当的值。归档文件现在应该位于虚拟机实例的主目录中。

  1. 现在,使用 SSH 登录到机器(如果使用浏览器终端,我们无需执行此操作):
ssh USER@IP

我们需要将 USERIP 替换为适当的值。

  1. 解压归档文件:
# On the VM instance
tar -xzvf CUDNN_ARCHIVE_NAME

我们需要将CUDNN_ARCHIVE_NAME替换为归档文件的名称。

  1. 现在让我们用新版本覆盖当前的 cuDNN 安装:
# On the VM instance
sudo cp cuda/include/cudnn.h /usr/local/cuda/include
sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64
sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*

现在我们已经成功安装了正确版本的 CUDA 驱动程序,接下来让我们安装所需的软件和 Magenta GPU。

安装 Magenta GPU

该镜像默认未安装某些软件包,因此我们需要手动安装它们:

  1. 首先,我们将安装一些音频依赖项:
# On the VM instance
sudo apt install libasound2-dev libsndfile-dev
  1. 然后,我们将安装 Miniconda(docs.conda.io/en/latest/miniconda.html)并创建一个新的环境:
# On the VM instance
conda create -n magenta python=3.6
conda activate magenta
  1. 最后,让我们安装 Magenta GPU:
# On the VM instance
pip install magenta-gpu

我们现在可以开始了,所以让我们在新的虚拟机实例上启动训练作业。

启动训练

要开始训练,我们首先需要将数据集传输到虚拟机实例:

  1. 要将数据集传输到虚拟机实例,首先压缩它,如果你没有使用 SSH 密钥,则可以通过终端浏览器界面上传它。如果你正在使用 SSH 密钥,则可以使用 scp 来完成:
scp PATH_TO_DATASET USER@IP:

我们需要将 PATH_TO_DATASETUSERIP 替换为合适的值。现在,压缩包应该已经在我们的虚拟机实例的主目录中了。

  1. 上传后,我们解压缩该压缩包,然后像在本地机器上一样开始训练过程,例如,训练 Melody RNN 模型:
# On the VM instance
melody_rnn_create_dataset --config="attention_rnn" --input="PATH_TO_NOTE_SEQUENCE_TFRECORDS" --output_dir="sequence_examples" --eval_ratio=0.10

# On the VM instance
melody_rnn_train --config="attention_rnn" --run_dir="logdir/run1" --sequence_example_file="sequence_examples/training_melodies.tfrecord" --hparams="batch_size=128,rnn_layer_sizes=[128,128]" --num_training_steps=20000

我们需要将 PATH_TO_NOTE_SEQUENCE_TFRECORDS 替换为合适的值。

  1. 要开始评估,我们需要启动一个新的终端;再次通过 SSH 连接到虚拟机实例,然后我们可以启动评估:
# On the VM instance
CUDA_VISIBLE_DEVICES="" melody_rnn_train --config="attention_rnn" --run_dir="logdir/run1" --sequence_example_file="sequence_examples/eval_melodies.tfrecord" --hparams="batch_size=62,rnn_layer_sizes=[128,128]" --num_training_steps=20000 --eval
  1. 我们还可以通过 SSH 使用连接转发来查看 TensorBoard。在新的终端中,使用以下命令连接到虚拟机实例:
ssh -L 16006:127.0.0.1:6006 USER@IP

我们需要将 USERIP 替换为合适的值。此命令将本地端口 16006 转发到虚拟机实例中的 6006 端口。

  1. 然后,在之前的终端中,我们可以启动 TensorBoard:
# On the VM instance
tensorboard --logdir=logdir

TensorBoard 可以通过在浏览器中使用 127.0.0.1:16006 地址本地打开。一旦训练完成,我们可以压缩虚拟机实例上的训练文件夹,然后使用 scp 或浏览器终端将其获取回来。

  1. 完成后,别忘了停止运行的实例。记住,我们在 GCP 上是按使用情况收费的,我们不想在没有理由的情况下让虚拟机实例持续运行。

在写本文时,使用 P100 GPU 训练 Melody RNN 模型 20,000 步的费用大约是 $5。

总结

在本章中,我们使用了在上一章中准备的数据集,训练了 Magenta 模型,涵盖了不同的乐器和风格。我们首先比较了不同模型和配置的适用性,然后展示了如何在必要时创建一个新的模型。

然后,我们启动了不同的训练任务,并查看了如何调优模型以获得更好的训练效果。我们展示了如何启动训练和评估作业,并在 TensorBoard 上检查结果指标。接着,我们观察到了过拟合的情况,并解释了如何解决过拟合和欠拟合问题。我们还展示了如何定义合适的网络大小和超参数,解决诸如不正确的批处理大小、内存错误、模型未收敛以及训练数据不足等问题。使用我们新训练的模型,我们生成了序列并展示了如何打包和处理检查点。

最后,我们展示了如何使用 GCP 在强大的机器上云端训练我们的模型。我们介绍了如何创建 Magenta VM 实例,以及如何在上面启动训练、评估和 TensorBoard。

本章标志着本书训练内容的结束。在接下来的章节中,我们将探讨一些 Magenta 核心功能以外的部分,比如 Magenta.js,并且让 Magenta 与数字音频工作站DAW)进行交互。

问题

  1. 编写一个新的 Drums RNN 模型配置,使用 64 的注意力长度,并且只对小军鼓和低音鼓进行编码,但进行反向处理。

  2. 我们有一个欠拟合的模型:这是什么意思,我们该如何解决这个问题?

  3. 我们有一个过拟合的模型:这是什么意思,我们该如何解决这个问题?

  4. 有什么技术可以确保我们在给定的训练过程中停止在最佳状态?

  5. 为什么增加批量大小会使得模型在性能上变得更差?它是否会在效率或训练时间上造成不良影响?

  6. 什么是合适的网络规模?

  7. 在反向传播之前,限制误差导数的值是否有助于或加剧梯度爆炸的问题?这个问题还有什么其他的解决方法?

  8. 为什么使用云服务商来训练我们的模型很有用?

深入阅读

第四部分:让你的模型与其他应用程序交互

本节通过演示如何让 Magenta 与其他应用程序(如浏览器、数字音频工作站(DAW)或 MIDI 控制器)进行通信,解释了 Magenta 在音乐制作环境中的应用。通过本节内容,你将能够使用 MIDI 接口和 Magenta.js。

本节包含以下章节:

  • 第八章,在浏览器中使用 Magenta.js

  • 第九章,让 Magenta 与音乐应用程序交互

第十章:使用 Magenta.js 在浏览器中运行 Magenta。

本章将介绍 Magenta.js,这是 Magenta 的 JavaScript 实现,由于它运行在浏览器中并且可以作为网页共享,因此在易用性上获得了广泛的关注。我们将介绍 TensorFlow.js,它是构建 Magenta.js 的技术,并展示 Magenta.js 中可用的模型,包括如何转换我们之前训练的模型。然后,我们将使用 GANSynth 和 MusicVAE 创建小型 Web 应用程序,分别用于采样音频和序列。最后,我们将看到 Magenta.js 如何通过 Web MIDI API 和 Node.js 与其他应用程序交互。

本章将涵盖以下主题:

  • 介绍 Magenta.js 和 TensorFlow.js。

  • 创建 Magenta.js Web 应用程序。

  • 使 Magenta.js 与其他应用程序交互。

技术要求。

本章将使用以下工具:

  • 使用 命令行Bash 从终端启动 Magenta。

  • PythonMagenta 用于将训练好的模型转换为 Magenta.js 可用格式。

  • 使用 TensorFlow.jsMagenta.js 在浏览器中创建音乐生成应用程序。

  • 使用 JavaScriptHTMLCSS 编写 Magenta.js Web 应用程序。

  • 一个 最新版本的浏览器(Chrome、Firefox、Edge、Safari),以支持最新的 Web API。

  • Node.jsnpm 用于安装 Magenta.js 及其依赖项(服务器端)。

  • 使用 FluidSynth 从浏览器中播放生成的 MIDI。

在 Magenta.js 中,我们将使用 Music RNNMusicVAE 模型来生成 MIDI 序列,使用 GANSynth 进行音频生成。我们将深入探讨它们的使用方法,但如果你觉得需要更多信息,Magenta.js 源代码中的 Magenta.js Music README(github.com/tensorflow/magenta-js/tree/master/music)是一个不错的起点。你还可以查看 Magenta.js 代码,它有详细的文档。最后一部分 进一步阅读 中也提供了额外的内容。

本章的代码位于本书 GitHub 代码库的 Chapter08 文件夹中,位置在 github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter08。所用示例和代码片段假设你在章节文件夹中。对于本章,你应该在开始之前运行 cd Chapter08

查看以下视频,观看代码演示:

占位符链接。

介绍 Magenta.js 和 TensorFlow.js。

在前几章中,我们已经介绍了 Python 中的 Magenta、其使用方法和内部工作原理。现在我们将关注 Google 的 Magenta.js,它是 Magenta 在 JavaScript 中的精简实现。Magenta 和 Magenta.js 各有优缺点;让我们进行比较,看看在不同使用场景下应该选择哪一个。

Magenta.js 应用程序易于使用和部署,因为它在浏览器中执行。开发和部署一个 Web 应用程序非常简单:你只需要一个 HTML 文件和一个 Web 服务器,应用程序就可以让全世界看到和使用。这是基于浏览器的应用程序的一大优势,因为它不仅让我们能够轻松创建自己的音乐生成应用程序,而且使得协作使用变得更加容易。有关流行 Magenta.js Web 应用程序的精彩示例,请参见本章末尾的 进一步阅读 部分。

这就是 Web 浏览器的强大之处:每个人都有一个,而且网页不需要安装即可运行。Magenta.js Web 应用程序的缺点是它也运行在浏览器中:浏览器并不是处理高质量、实时音频的最佳场所,而且使你的应用程序与传统音乐制作工具(如 数字音频工作站 (DAWs))交互变得更加困难。

随着内容的深入,我们将逐步了解在浏览器中工作的具体细节。首先,我们将在 在浏览器中介绍 Tone.js 用于声音合成 部分中,了解 Web Audio API 的使用。接着,我们将在 使用 Web Workers API 将计算从 UI 线程卸载 部分中,讨论如何让实时音频变得更加轻松。最后,我们将在 使 Magenta.js 与其他应用程序交互 部分中,讨论如何让 Magenta.js 与其他音乐应用程序互动。

在浏览器中介绍 TensorFlow.js 机器学习

首先,让我们介绍 TensorFlow.js(www.tensorflow.org/js),Magenta.js 构建的项目。正如其名称所示,TensorFlow.js 是 TensorFlow 的 JavaScript 实现,使得在浏览器中使用和训练 模型成为可能。也可以导入并运行来自 TensorFlow SavedModel 或 Keras 的预训练模型。

使用 TensorFlow.js 很简单。你可以使用 script 标签,如以下代码块所示:

<html>
<head>
  <script src="img/tf.min.js"></script>
  <script>
    const model = tf.sequential();
    model.add(tf.layers.dense({units: 1, inputShape: [1]}));
    model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});
  </script>
</head>
<body>
</body>
</html>

另外,你也可以使用 npmyarn 命令来运行以下代码块:

import * as tf from '@tensorflow/tfjs';
const model = tf.sequential();
model.add(tf.layers.dense({units: 1, inputShape: [1]}));
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});

注意在这两个代码片段中 tf 变量的使用,它是通过脚本导入的(在本章的示例中我们将看到更多 tf 的使用)。我们不会特别研究 TensorFlow.js,但我们将在 Magenta.js 代码中使用它。

TensorFlow.js 另一个优点是它使用 WebGL(www.khronos.org/registry/webgl/specs/latest/)进行计算,这意味着它是 图形处理单元GPU)加速的(如果你有 GPU),且无需安装 CUDA 库。数学运算在 WebGL 着色器中实现,张量被编码为 WebGL 纹理,这是 WebGL 的一种非常巧妙的使用方法。我们无需做任何事来启用 GPU 加速,因为 TensorFlow.js 后端会为我们处理。当使用 Node.js 服务器时,TensorFlow C API 用于硬件加速,这意味着也可以使用 CUDA 库。

使用 WebGL 有一些注意事项,最显著的是计算在某些情况下可能会阻塞 UI 线程,以及张量分配所使用的内存必须在使用后进行回收(释放)。关于计算线程,我们将在讨论 Web Workers 时更深入地探讨。关于内存管理,我们将在代码中展示正确的使用方法。有关这些问题的更多信息,请参见 进一步阅读 部分。

在浏览器中生成音乐的 Magenta.js 介绍

现在我们理解了 TensorFlow.js,接下来讨论 Magenta.js。首先,我们需要了解 Magenta.js 能做什么,不能做什么。目前,Magenta.js 中无法训练模型(除了在 MidiMe 中的部分训练),但我们在前一章中训练的模型可以轻松转换和导入。Magenta.js 的另一个限制是并非所有模型都包含在内,但最重要的模型都包括在内。在编写 Magenta.js 代码时,我们会发现大部分已覆盖的概念都在其中,只是语法有所不同。

以下是 Magenta.js 中一些预训练模型的概述:

  • Onsets and Frames 用于钢琴转录,将原始音频转换为 MIDI

  • Music RNN长短期记忆LSTM)网络)用于单音和多音 MIDI 生成,包括 Melody RNN、Drums RNN、Improv RNN 和 Performance RNN 模型

  • MusicVAE 用于单音或三重音采样与插值,另包括 GrooVAE

  • Piano Genie 将 8 键输入映射到完整的 88 键钢琴

我们已经在前面的章节中讨论了这些模型。我们可以在 Magenta.js 源代码中找到预训练检查点列表,路径为 music/checkpoints/checkpoints.json 文件,或者在托管版本中,通过 goo.gl/magenta/js-checkpoints 访问。我们使用的大多数检查点(或模型包)都包含在 Magenta.js 中,还有一些新增加的模型,例如更长的 4 小节 MusicVAE 和 GrooVAE 模型。

将训练好的模型转换为 Magenta.js 格式

使用预训练模型非常好,但我们也可以导入我们自己训练的模型,比如我们在上一章中训练的模型——第七章,训练 Magenta 模型。我们通过使用 checkpoint_converted.py 脚本实现这一点,该脚本将 Magenta 检查点中的权重转储到 Magenta.js 可以使用的格式。

你可以在本章节的源代码中找到这段代码,文件名为 chapter_08_example_01.html。源代码中有更多的注释和内容,你应该去查看它。

让我们通过以下步骤,将一个简单的 RNN 模型转换为适用于 Magenta.js 的格式:

  1. 首先,我们需要从 Magenta.js 获取 checkpoint_converter.py 脚本。最简单的方法是直接从 GitHub 上的源代码下载该脚本,如下所示:
curl -o "checkpoint_converter.py" "https://raw.githubusercontent.com/tensorflow/magenta-js/master/scripts/checkpoint_converter.py"

这应该会在本地创建 checkpoint_converter.py 文件。

  1. 现在,我们需要 TensorFlow.js Python 打包文件,这是 checkpoint_converter.py 脚本所依赖的。运行以下代码:
# While in your Magenta conda environment
pip install tensorflowjs
  1. 我们现在可以运行转换脚本,例如,使用我们之前训练的 DrumsRNN 模型(将 PATH_TO_TRAINING_DIR 替换为合适的值),如下所示:
python checkpoint_converter.py "PATH_TO_TRAINING_DIR/drums_rnn_dance_drums/logdir/run1_small/train/model.ckpt-20000" "checkpoints/drums_rnn_dance_small"

这将创建 checkpoints/drums_rnn_dance_small 目录,其中包含一个 JSON 元数据文件和将由 TensorFlow.js 加载的检查点二进制文件。

请记住,在引用 TensorFlow 检查点时,您需要提供前缀——例如,model.ckpt-20000,但不应加上 .data.index.meta

  1. 然后,我们需要创建一个 JSON 配置文件,描述模型的配置。打开 checkpoints/drums_rnn_dance_small/config.json 文件,并输入以下内容:
{
  "type": "MusicRNN",
  "dataConverter": {
    "type": "DrumsConverter",
    "args": {}
  }
}

这是 DrumsRNN 模型的一个最小示例,没有进一步的配置。请注意,即使没有提供任何参数,dataConverterargs 键也是必要的。dataConvertertypeDataConverter 的子类之一,位于 Magenta.js 源代码中的 music/src/core 文件夹下的 data.ts 文件中。其他可能的数据转换器包括 MelodyConverterTrioConverterGrooveConverter

其他模型和转换器将需要更多的配置。找到适当配置的最简单方法是找到一个类似的 Magenta 预训练模型,并使用类似的值。为此,按照 下载预训练模型到本地 部分操作,并在下载的 config.json 文件中找到所需的信息。

  1. 我们的自定义模型现在已转换为 TensorFlow.js 可以理解的格式。接下来,让我们创建一个小型网页,导入并初始化该模型进行测试,如下所示:
<html lang="en">
<body>
<script src="img/magentamusic.js"></script>
<script>
  // Initialize a locally trained DrumsRNN model from the local directory
  // at: checkpoints/drums_rnn_dance_small
  async function startLocalModel() {
    const musicRnn = new mm.MusicRNN("http://0.0.0.0:8000/" +
 "checkpoints/drums_rnn_dance_small");
    await musicRnn.initialize();
  }

  // Calls the initialization of the local model
  try {
    Promise.all([startLocalModel()]);
  } catch (error) {
    console.error(error);
  }
</script>
</body>
</html>

不必过于担心 HTML 页面中的内容,因为它将在接下来的章节中得到详细解释。这里重要的是,MusicRNN 构造函数(mm.MusicRNN("URL"))正在加载我们转换后的 DrumsRNN 检查点到 MusicRNN 模型中。

你可能注意到检查点的 URL 是本地的,位于http://0.0.0.0:8000。这是因为大多数浏览器实现了跨源资源共享CORS)限制,其中之一是本地文件只能获取以统一资源标识符URI)方案httphttps开头的资源。

  1. 绕过这一点的最简单方法是本地启动一个 web 服务器,如下所示:
python -m http.server

这将启动一个 web 服务器,在http://0.0.0.0:8000提供当前文件夹的内容,这意味着前面代码片段中的 HTML 文件将通过http://0.0.0.0:8000/example.html提供,且我们的检查点将位于http://0.0.0.0:8000/checkpoints/drums_rnn_dance_small

  1. 打开 HTML 文件并检查控制台。你应该会看到以下内容:
* Tone.js v13.8.25 * 
MusicRNN  Initialized model in 0.695s

这意味着我们的模型已成功初始化。

本地下载预训练模型

本地下载预训练模型很有用,如果我们想自己提供模型或检查config.json的内容:

  1. 首先,我们需要 Magenta.js 中的checkpoint_converter.py脚本。最简单的方法是直接从 GitHub 的源代码下载该脚本,如下所示:
curl -o "checkpoint_downloader.py" "https://raw.githubusercontent.com/tensorflow/magenta-js/master/scripts/checkpoint_downloader.py"

这应该会在本地创建checkpoint_converter.py文件。

  1. 然后,我们可以通过输入以下代码来调用该脚本:
python checkpoint_downloader.py "https://storage.googleapis.com/magentadata/js/checkpoints/music_vae/mel_16bar_small_q2" "checkpoints/music_vae_mel_16bar_small_q2"

这将下载mel_16bar_small_q2 MusicVAE 预训练模型到checkpoints文件夹中。

在浏览器中引入 Tone.js 进行声音合成

在本章中,你将听到在浏览器中生成的音频,这意味着音频合成,类似于我们在前几章中使用 FluidSynth 来播放 MIDI 文件的方式,是在浏览器中发生的,使用的是 Web Audio API。

Web Audio APIwww.w3.org/TR/webaudio/)提供了相当低级的概念,通过音频节点来处理声音源、转换和路由。首先,我们有一个声音源,它提供一组声音强度(有关这一点的复习,请参见第一章,关于 Magenta 和生成艺术的介绍),它可以是一个声音文件(样本)或一个振荡器。然后,声音源节点可以连接到一个转换节点,如增益(用于改变音量)。最后,结果需要连接到一个目标(输出),使声音能够通过扬声器播放出来。

该规范已经相当成熟,列为W3C 候选推荐,2018 年 9 月 18 日,因此一些实现细节可能会发生变化,但可以认为它是稳定的。就支持而言,所有主要浏览器都支持 Web Audio API,这非常好。有关更多信息,请参阅进一步阅读部分。

我们不会直接使用 Web Audio API,而是使用 Tone.js (tonejs.github.io),这是一个建立在 Web Audio API 之上的 JavaScript 库,提供更高层次的功能。使用 Tone.js 的另一个优势是,它能够适应底层 Web Audio API 的变化。

由于 Web Audio API 在不同浏览器中的实现有所不同,音频质量可能会有所不同。例如,在 Firefox 中叠加多个来自 GANSynth 的音频样本时会出现音频削波问题,但在 Chrome 中则可以正常工作。请记住,对于专业级别的音频质量,浏览器中的音频合成可能不是最佳选择。

创建一个 Magenta.js Web 应用

现在我们已经介绍了 Magenta.js 的相关概念,接下来我们将使用 Magenta.js 创建一个 Web 应用。让我们创建一个 Web 应用,使用 MusicVAE 生成三种乐器(鼓组、低音和主音),并且可以将主乐器替换为 GANSynth 生成的乐器。

我们将一步步构建这个应用。首先,我们将做一个生成乐器的应用,使用 GANSynth。然后,我们将创建一个可以采样三重奏序列的应用。最后,我们将把这两个应用合并在一起。

在浏览器中使用 GANSynth 生成乐器

在我们示例的第一部分,我们将使用 GANSynth 来采样单个乐器音符,这些音符是时长为 4 秒的短音频片段。我们将能够将多个音频片段叠加,从而产生有趣的效果。

首先,我们将创建 HTML 页面并导入所需的脚本。接下来,我们将编写 GANSynth 采样代码,并详细解释每一步。最后,我们将通过聆听生成的音频来完成示例。

编写页面结构

我们将保持页面结构和样式的简洁,专注于 Magenta.js 的代码。

你可以在本章源代码中的 chapter_08_example_02.html 文件中找到这段代码。源代码中有更多的注释和内容,你应该去查看一下。

首先,让我们创建页面结构并导入所需的脚本,如下所示:

<html lang="en">
<body>
<div>
  <button disabled id="button-sample-gansynth-note">
    Sample GANSynth note
  </button>
  <div id="container-plots"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
  // GANSynth code
</script>
</body>
</html>

页面结构只包含一个按钮,用于调用 GANSynth 生成音频,以及一个容器,用于绘制生成的频谱图。

在浏览器中使用 Magenta.js 有两种方式,具体如下:

  1. 我们可以在 dist/magentamusic.min.js 中导入整个 Magenta.js 音乐库。在 Magenta 文档中,这被称为 ES5 bundle 方法。这将包括 Magenta.js(绑定为 mm)及其所有依赖项,包括 TensorFlow.js(绑定为 mm.tf)和 Tone.js(绑定为 mm.Player.tone)。

  2. 我们可以仅导入我们需要的 Magenta.js 元素,这些元素位于 es6 文件夹中。在 Magenta 文档中,这称为 ES6 打包 方法。例如,如果我们只需要 GANSynth 模型,我们需要导入 Tone.js(绑定到 Tone),TensorFlow.js(绑定到 tf),Magenta.js 核心(绑定到 core),以及 Magenta.js GANSynth(绑定到 gansynth)。

在这里我们不讨论 ES5 和 ES6 打包文件之间的差异。只需记住,最简单的方法是使用 ES5 打包方式,导入一个包含所有内容的大文件。如果你想对发送到客户端的内容有更多控制(例如,出于性能原因),你将需要使用 ES6 打包方式。请记住,两种方法之间的模块绑定不同,因此如果你更改了导入,你需要调整代码。

以下是仅包含 GANSynth 模型的 ES6 打包导入:

<script src="img/Tone.min.js"></script>
<script src="img/tf.min.js"></script>
<script src="img/core.js"></script>
<script src="img/gansynth.js"></script>

这仅导入 GANSynth 模型,模型可以通过 new gansynth.GANSynth(...) 实例化。在使用 ES6 模块时,我们需要单独导入每个脚本。对于我们的示例,这些脚本是 Tone.js、TensorFlow.js、Magenta.js 核心和 GANSynth。

对于我们的示例,我们将坚持使用 ES5 打包方式,但如果你愿意,可以使用 ES6 打包方式。在我们的示例中,我们将展示每种方法之间代码的不同之处。

你可以在 chapter_08_example_02_es6.html 文件中找到本例的 ES6 代码,该文件位于本章的源代码中。

现在,让我们编写 GANSynth 代码(在 GANSynth code 注释中),并解释每一步。

使用 GANSynth 采样音频

现在我们已经正确导入了 Magenta.js,我们可以按照以下步骤编写 GANSynth 音频生成代码:

  1. 首先,我们将初始化 DOM 元素并初始化 GANSynth,如下所示:
// Get DOM elements
const buttonSampleGanSynthNote = document
    .getElementById("button-sample-gansynth-note");
const containerPlots = document
    .getElementById("container-plots");

// Starts the GANSynth model and initializes it. When finished, enables
// the button to start the sampling
async function startGanSynth() {
  const ganSynth = new mm.GANSynth("https://storage.googleapis.com/" +
 "magentadata/js/checkpoints/gansynth/acoustic_only");
  await ganSynth.initialize();
  window.ganSynth = gansynth;
  buttonSampleGanSynthNote.disabled = false;
}

在这里,我们使用 mm.GANSynth(...) 实例化 GANSynth。记住,当使用 ES5 模块导入时,Magenta.js 上下文位于 mm 变量下。检查点的 URL 与我们在上一章中使用的相同——第五章,使用 NSynth 和 GANSynth 生成音频。如果你想要更多信息,请参考那一章。我们还将 ganSynth 引用设置为全局变量,以便稍后可以轻松调用。

使用 Magenta.js ES6 打包时,我们将拥有以下代码:

const ganSynth = new gansynth.GANSynth("https://storage.googleapis.com/" +
    "magentadata/js/checkpoints/gansynth/acoustic_only");

对于 ES6 打包,模块变量是 gansynth.GANSynth,而不是 mm.GANSynth

  1. 现在,让我们编写一个异步函数,使用 canvas 将生成的频谱图插入网页中,如下所示:
// Plots the spectrogram of the given channel
// see music/demos/gansynth.ts:28 in magenta.js source code
async function plotSpectra(spectra, channel) {
  const spectraPlot = mm.tf.tidy(() => {
    // Slice a single example.
    let spectraPlot = mm.tf.slice(spectra, [0, 0, 0, channel], [1, -1, -1, 1])
        .reshape([128, 1024]);
    // Scale to [0, 1].
    spectraPlot = mm.tf.sub(spectraPlot, mm.tf.min(spectraPlot));
    spectraPlot = mm.tf.div(spectraPlot, mm.tf.max(spectraPlot));
    return spectraPlot;
  });
  // Plot on canvas.
  const canvas = document.createElement("canvas");
  containerPlots.appendChild(canvas);
  await mm.tf.browser.toPixels(spectraPlot, canvas);
  spectraPlot.dispose();
}

此方法创建一个频谱图,并将其插入我们之前声明的 containerPlots 元素中的 canvas 元素。它将继续为每次生成添加频谱图。

你可能已经注意到在示例中使用了 tf.tidydispose。使用这些方法是为了避免 TensorFlow.js 代码中的内存泄漏。这是因为 TensorFlow.js 使用 WebGL 来进行计算,而WebGL 资源在使用后需要显式回收。任何 tf.Tensor 在使用后都需要通过使用 dispose 来进行清理。tf.tidy 方法可以在执行完函数后,自动清理所有未返回的张量。

你可能会想知道在之前的 JavaScript 代码中,asyncawait 关键字是什么意思。这两个关键字标志着异步方法的使用。当调用一个被标记为 async 的方法时,表示它是异步的,调用者需要使用 await 来标记调用,这意味着它会等待(阻塞)直到返回一个值。await 关键字只能在 async 方法中使用。在我们的示例中,mm.tf.browser.toPixels 方法被标记为 async,因此我们需要使用 await 等待它的返回。调用一个 async 方法而不使用 await 可以通过 Promise 语法完成—Promise.all([myAsyncMethod()])

Promise 是在 JavaScript 中引入的,目的是解决编写异步代码时遇到的一个反复出现的问题:回调地狱。回调地狱是一个问题,当多个关联的调用都是异步时,会导致嵌套的回调(地狱般的回调)。

Promise 非常棒,因为它们提供了一个干净的机制来处理复杂的异步调用链,并且有适当的错误处理。然而,它们有点冗长,这就是为什么引入了 asyncawait 关键字作为语法糖,以缓解使用 Promise 时的常见用例。

  1. 然后,我们编写一个异步函数,从 GANSynth 中采样一个音符,播放它,并使用我们之前的方法绘制它,如下所示:
// Samples a single note of 4 seconds from GANSynth and plays it repeatedly
async function sampleGanNote() {
  const lengthInSeconds = 4.0;
  const sampleRate = 16000;
  const length = lengthInSeconds * sampleRate;

  // The sampling returns a spectrogram, convert that to audio in
  // a tone.js buffer
  const specgrams = await ganSynth.randomSample(60);
  const audio = await ganSynth.specgramsToAudio(specgrams);
  const audioBuffer = mm.Player.tone.context.createBuffer(
      1, length, sampleRate);
  audioBuffer.copyToChannel(audio, 0, 0);

  // Play the sample audio using tone.js and loop it
  const playerOptions = {"url": audioBuffer, "loop": true, "volume": -25};
  const player = new mm.Player.tone.Player(playerOptions).toMaster();
  player.start();

  // Plots the resulting spectrograms
  await plotSpectra(specgrams, 0);
  await plotSpectra(specgrams, 1);
}

我们首先使用 randomSample 方法从 GANSynth 中采样,传入基准音高 60(即 C4)作为参数。这告诉模型从与该音高相对应的值进行采样。然后,返回的频谱图使用 specgramsToAudio 转换为音频。最后,我们使用 Tone.js 的音频缓冲区来播放该采样,通过实例化一个新播放器并使用音频缓冲区。由于我们为每个采样实例化一个新播放器,因此每个新的音频采样都会叠加在其他采样之上。

实例化播放器的代码 mm.Player.tone.Player 有些复杂,因为我们首先需要找到已经被 Magenta.js 对象通过 mm.Player.tone 实例化的 Tone.js 引用(这里,Player 引用是 Magenta.js 的一个类)。

使用 ES6 打包文件更为直接,如这里所示:

const player = new Tone.Player(playerOptions).toMaster();

由于 Magenta.js 的 ES6 打包文件未包含 Tone.js,它需要自行初始化,并可以直接通过 Tone 变量进行引用。

  1. 最后,我们通过将按钮绑定到一个操作并初始化 GANSynth 来总结我们的示例,如下所示:
// Add on click handler to call the GANSynth sampling
buttonSampleGanSynthNote.addEventListener("click", () => {
  sampleGanNote();
});

// Calls the initialization of GANSynth
try {
  Promise.all([startGanSynth()]);
} catch (error) {
  console.error(error);
}

首先,我们将按钮绑定到 sampleGanNote 方法,然后我们初始化 GANSynth,使用 startGanSynth 方法。

启动网页应用

现在我们的网页应用已经准备好,可以测试代码了。让我们用浏览器打开我们创建的 HTML 页面。我们应该能看到一个与下图相似的页面:

在前面的图中,我们已经生成了一些 GANSynth 样本。每次生成都会绘制两个频谱图,并将之前的保持在页面上。在前面的截图右侧,您可以在控制台调试器中看到 Tone.js 和 GANSynth 初始化。当完成后,Sample GANSynth note 按钮将启用。

继续生成声音:当你叠加很多声音时,会得到非常有趣的效果。恭喜你——你已经完成了第一个 Magenta.js 网页应用!

使用 MusicVAE 生成三重奏

我们现在将使用 Magenta.js 中的 MusicVAE 模型来生成一些序列,并直接在浏览器中播放,使用 Tone.js。我们将使用的检查点是一个 trio 模型,这意味着我们将同时生成三种序列:打击乐、低音和主旋律。

您可以在本章源代码的 chapter_08_example_03.html 文件中找到这段代码。源代码中有更多的注释和内容,您应该去查看一下。

由于这段代码与上一节类似,我们不会逐一讲解所有内容,但会解释主要的区别:

  1. 首先,我们定义页面结构和脚本导入,如下所示:
<html lang="en">
<body>
<div>
  <button disabled id="button-sample-musicae-trio">
    Sample MusicVAE trio
 </button>
  <canvas id="canvas-musicvae-plot"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
  // MusicVAE code
</script>
</body>
</html>

页面结构与上一节相同。我们将在 MusicVAE 代码 注释中填入代码。

  1. 接下来,让我们初始化 MusicVAE 模型,如下所示:
// Get DOM elements
const buttonSampleMusicVaeTrio = document
    .getElementById("button-sample-musicae-trio");
const canvasMusicVaePlot = document
    .getElementById("canvas-musicvae-plot");

// Starts the MusicVAE model and initializes it. When finished, enables
// the button to start the sampling
async function startMusicVae() {
  const musicvae = new mm.MusicVAE("https://storage.googleapis.com/" +
 "magentadata/js/checkpoints/music_vae/trio_4bar");
  await musicvae.initialize();
  window.musicvae = musicvae;
  buttonSampleMusicVaeTrio.disabled = false;
}

检查点的 URL 与上一章使用的相同——第四章,使用 MusicVAE 进行潜在空间插值。如果您想了解该检查点的更多信息,请参考本章。

  1. 我们现在创建一个新的 Tone.js 播放器来播放生成的三个序列,如下所示:
// Declares a new player that have 3 synths for the drum kit (only the
// bass drum), the bass and the lead.
class Player extends mm.BasePlayer {

  bassDrumSynth = new mm.Player.tone.MembraneSynth().toMaster();

  bassSynth = new mm.Player.tone.Synth({
 volume: 5,
 oscillator: {type: "triangle"}
 }).toMaster();

  leadSynth = new mm.Player.tone.PolySynth(5).toMaster();

  // Plays the note at the proper time using tone.js
  playNote(time, note) {
    let frequency, duration, synth;
    if (note.isDrum) {
      if (note.pitch === 35 || note.pitch === 36) {
        // If this is a bass drum, we use the kick pitch for
        // an eight note and the bass drum synth
        frequency = "C2";
        duration = "8n";
        synth = this.bassDrumSynth;
      }
    } else {
      // If this is a bass note or lead note, we convert the
      // frequency and the duration for tone.js and fetch
      // the proper synth
      frequency = new mm.Player.tone.Frequency(note.pitch, "midi");
      duration = note.endTime - note.startTime;
      if (note.program >= 32 && note.program <= 39) {
        synth = this.bassSynth;
      } else {
        synth = this.leadSynth;
      }
    }
    if (synth) {
      synth.triggerAttackRelease(frequency, duration, time, 1);
    }
  }
}

这段代码扩展了 Magenta.js 中的 mm.BasePlayer 类,这很有用,因为我们只需要实现 playNote 方法来播放序列。首先,我们定义了三个合成器:bassDrumSynthbassSynthleadSynth,如下所示:

  • 低音鼓合成器只播放低音鼓,由note.isDrum属性和 MIDI 音符 35 或 36 表示,并且总是播放 C2 的频率,长度为 8 分音符(8n),使用 Tone.js 的 MembraneSynth。请记住:在 MIDI 规范中,打击乐通道中的乐器(如低音鼓、军鼓等)是通过音符的音高来定义的——例如,音高 35 是原声低音鼓。

  • 低音合成器只播放从 32 到 39 的程序,使用 Tone.js 中的Synth和三角波形。记住:根据 MIDI 规范,程序指定了应该播放的乐器。例如,程序 1 是钢琴,而程序 33 是木吉他。

  • 主音合成器使用 Tone.js 中的PolySynth和五个音轨来演奏其他程序。

需要注意的是,对于低音和主音合成器,我们首先需要将 MIDI 音符转换为 Tone.js 的频率,使用Frequency类。

另一个需要讨论的重要内容是音符包络,它在 Tone.js 中的合成器上通过triggerAttackRelease方法使用。包络充当过滤器,允许音符在一定时间内被听到。你可以把合成器想象成始终在播放,而包络—当关闭时—不会让声音通过。当包络打开时,它允许声音被听到,使用一定的斜率,意味着声音可以慢慢(或快速)出现,并慢慢(或快速)结束。这分别被称为包络的起音释放。每次我们调用触发方法时,合成器会根据给定的持续时间和一定的斜率发出声音。

你可能已经听说过起音衰减延音释放ADSR)这个术语,尤其是在谈论包络时。在 Tone.js 中,我们使用的是这个概念的简化版,仅使用包络的起音释放。如果使用完整的 ADSR 包络,我们可以更好地控制结果的形状。为了简单起见,我们的例子中将使用简化版。

  1. 现在让我们来采样 MusicVAE 模型,如下所示:
// Samples a trio of drum kit, bass and lead from MusicVAE and
// plays it repeatedly at 120 QPM
async function sampleMusicVaeTrio() {
  const samples = await musicvae.sample(1);
  const sample = samples[0];
  new mm.PianoRollCanvasVisualizer(sample, canvasMusicVaePlot,
      {"pixelsPerTimeStep": 50});

  const player = new Player();
  mm.Player.tone.Transport.loop = true;
  mm.Player.tone.Transport.loopStart = 0;
  mm.Player.tone.Transport.loopEnd = 8;
  player.start(sample, 120);
}

首先,我们使用sample方法和参数 1 来采样 MusicVAE 模型,1 表示所需的样本数量。然后,我们使用之前声明的canvas中的mm.PianoRollCanvasVisualizer绘制结果音符序列。最后,我们以 120 QPM 启动播放器,并循环 8 秒的音序,使用 Tone.js 中的Transport类。记住,MusicVAE 模型具有固定的长度,这意味着使用 4 小节三重奏模型,我们生成 8 秒的样本,速度为 120 QPM。

  1. 最后,让我们通过绑定按钮到一个动作并初始化 MusicVAE 模型来完成我们的示例,如下所示:
// Add on click handler to call the MusicVAE sampling
buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
  sampleMusicVaeTrio();
  event.target.disabled = true;
});

// Calls the initialization of MusicVAE
try {
  Promise.all([startMusicVae()]);
} catch (error) {
  console.error(error);
}

首先,我们将按钮绑定到sampleMusicVaeTrio方法,然后我们使用startMusicVae方法初始化 MusicVAE 模型。你可以看到我们这里使用了之前介绍的Promise.all调用来启动我们的异步代码。

  1. 现在我们已经准备好我们的网页应用程序,可以测试我们的代码了。让我们使用浏览器打开我们创建的 HTML 页面。我们应该能看到一个类似于以下截图的页面:

通过点击Sample MusicVAE trio按钮,MusicVAE 应该会采样一个序列,绘制出来并使用我们定义的合成器播放。生成的图形相当基础,因为它没有区分三个乐器,也没有时间或音高标记,但可以通过PianoRollCanvasVisualizer类进行自定义。

要生成一个新序列,重新加载页面以重新开始。

使用 SoundFont 来获得更真实的乐器音色

当听到生成的声音时,你可能会注意到音乐听起来有点基础简单。这是因为我们使用了 Tone.js 的默认合成器,它的优点是易于使用,但缺点是音质不如更复杂的合成器好。记住,Tone.js 的合成器可以进行自定义,以便听起来更好。

我们可以使用 SoundFont 代替合成器。SoundFont 是多种乐器的录制音符,我们从本书一开始就一直在 FluidSynth 中使用它们。在 Magenta.js 中,我们可以使用SoundFontPlayer来代替Player实例,代码如下所示:

const player = new mm.SoundFontPlayer("https://storage.googleapis.com/" +
    "magentadata/js/soundfonts/salamander"));
player.start(sequence, 120)

Magenta 团队托管的 SoundFont 列表可以在 Magenta.js 音乐文档中找到(github.com/tensorflow/magenta-js/tree/master/music)。

演奏生成的三重奏乐器

现在,我们有了 MusicVAE 生成三种乐器的序列,以及 GANSynth 生成音频,接下来让我们让这两者协同工作。

你可以在本章的源代码中的chapter_08_example_04.html文件中找到这段代码。源代码中有更多的注释和内容,你应该去查看一下。

由于代码与上一节类似,我们不会逐一讲解所有内容,但会解释主要的区别:

  1. 首先,让我们定义页面结构和脚本导入,如下所示:
<html lang="en">
<body>
<div>
  <button disabled id="button-sample-musicae-trio">
    Sample MusicVAE trio
  </button>
  <button disabled id="button-sample-gansynth-note">
    Sample GANSynth note for the lead synth
  </button>
  <canvas id="canvas-musicvae-plot"></canvas>
  <div id="container-plots"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
  // MusicVAE + GANSynth code
</script>
</body>
</html>

该页面与上一节的结构相同。我们将填充MusicVAE + GANSynth code注释中的代码。

  1. 然后,让我们初始化 MusicVAE 模型和 GANSynth 模型,如下所示:
// Get DOM elements
const buttonSampleGanSynthNote = document
    .getElementById("button-sample-gansynth-note");
const buttonSampleMusicVaeTrio = document
    .getElementById("button-sample-musicae-trio");
const containerPlots = document
    .getElementById("container-plots");
const canvasMusicVaePlot = document
    .getElementById("canvas-musicvae-plot");

// Starts the MusicVAE model and initializes it. When finished, enables
// the button to start the sampling
async function startMusicVae() {
  const musicvae = new mm.MusicVAE("https://storage.googleapis.com/" +
 "magentadata/js/checkpoints/music_vae/trio_4bar");
  await musicvae.initialize();
  window.musicvae = musicvae;
  buttonSampleMusicVaeTrio.disabled = false;
}

// Starts the GANSynth model and initializes it
async function startGanSynth() {
  const ganSynth = new mm.GANSynth("https://storage.googleapis.com/" +
 "magentadata/js/checkpoints/gansynth/acoustic_only");
  await ganSynth.initialize();
  window.ganSynth = ganSynth
}

在这里,我们仅启用MusicVAE sampling按钮。GANSynth sampling按钮将在 MusicVAE 完成生成后启用。

  1. 我们保持使用相同的plotSpectra方法(来自之前的示例)。

  2. 我们保持使用相同的Player类(来自之前的示例)进行声音合成。我们可以设置leadSynth = null,因为它将被 GANSynth 生成替代,但这不是必需的。

  3. 我们保持使用相同的sampleMusicVaeTrio方法(来自之前的示例),但我们还将实例化的播放器设置为全局变量,使用window.player = player,因为 GANSynth 稍后需要更改主合成器。

  4. 我们重写了sampleGanNote方法(来自之前的示例),以添加一个样本播放器,如下所示:

// Samples a single note of 4 seconds from GANSynth and plays it repeatedly
async function sampleGanNote() {
  const lengthInSeconds = 4.0;
  const sampleRate = 16000;
  const length = lengthInSeconds * sampleRate;

  // The sampling returns a spectrogram, convert that to audio in
  // a tone.js buffer
  const specgrams = await ganSynth.randomSample(60);
  const audio = await ganSynth.specgramsToAudio(specgrams);
  const audioBuffer = mm.Player.tone.context.createBuffer(
      1, length, sampleRate);
  audioBuffer.copyToChannel(audio, 0, 0);

  // Plays the sample using tone.js by using C4 as a base note,
  // since this is what we asked the model for (MIDI pitch 60).
  // If the sequence contains other notes, the pitch will be
  // changed automatically
  const volume = new mm.Player.tone.Volume(-10);
  const instrument = new mm.Player.tone.Sampler({"C4": audioBuffer});
  instrument.chain(volume, mm.Player.tone.Master);
  window.player.leadSynth = instrument;

  // Plots the resulting spectrograms
  await plotSpectra(specgrams, 0);
  await plotSpectra(specgrams, 1);
}

首先,我们使用 randomSample 从 GANSynth 中随机采样一个乐器,像前面的示例那样。然后,我们需要在 Tone.js 合成器中播放该样本,因此我们使用 Sampler 类,它接收一个包含每个键的样本字典。因为我们使用 MIDI 音高 60 对模型进行了采样,所以我们使用 C4 来表示生成的音频缓冲区。最后,我们通过 window.player.leadSynth = instrument 将该合成器添加到我们的播放器中。

  1. 让我们通过将按钮绑定到相应的操作,并初始化 MusicVAE 和 GANSynth 模型来总结我们的示例,如下所示:
// Add on click handler to call the MusicVAE sampling
buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
  sampleMusicVaeTrio();
  event.target.disabled = true;
  buttonSampleGanSynthNote.disabled = false;
});

// Add on click handler to call the GANSynth sampling
buttonSampleGanSynthNote.addEventListener("click", () => {
  sampleGanNote();
});

// Calls the initialization of MusicVAE and GanSynth
try {
  Promise.all([startMusicVae(), startGanSynth()]);
} catch (error) {
  console.error(error);
}

这段代码将启动模型,绑定按钮,并更新按钮状态。

  1. 现在我们已经准备好我们的 Web 应用程序,可以测试我们的代码了。让我们使用浏览器打开我们创建的 HTML 页面。我们应该会看到一个类似于以下屏幕截图的页面:

通过按下 为主合成器采样 MusicVAE 三重奏 按钮,MusicVAE 应该会采样一个序列,绘制它,并使用我们定义的合成器进行播放。然后,可以使用 为主合成器采样 GANSynth 音符 按钮来生成一个新的声音用于主合成器,这可以多次使用。

要生成一个新的序列,重新加载页面以重新开始。

使用 Web Workers API 将计算卸载出 UI 线程

正如你从前面的示例中可能注意到的,当你使用 为主合成器采样 GANSynth 音符 按钮时,音频会冻结(你将听不到来自 MusicVAE 的任何声音),这是因为 GANSynth 正在生成它的第一个样本。

这是因为 JavaScript 的并发是基于事件循环模式构建的,这意味着 JavaScript 不是多线程的,一切都在一个称为UI 线程的单线程中执行。这是可行的,因为 JavaScript 使用非阻塞 I/O,这意味着大多数昂贵的操作可以立即完成,并通过事件和回调返回它们的值。然而,如果一个长时间的计算是同步的,它将在执行时阻塞 UI 线程,这正是 GANSynth 在生成其样本时发生的情况(有关 TensorFlow 如何使用 WebGL 处理计算的更多信息,请参见前面的 在浏览器中使用 TensorFlow.js 进行机器学习 部分)。

解决此问题的一种方法是Web Workers APIhtml.spec.whatwg.org/multipage/workers.html),由Web 超文本应用技术工作组WHATWG)规范,该 API 使得将计算卸载到不会影响 UI 线程的另一个线程成为可能。Web Worker 基本上是一个 JavaScript 文件,它从主线程启动并在自己的线程中执行。它可以与主线程发送和接收消息。Web Workers API 已经成熟,并且在浏览器中得到了很好的支持。你可以在 进一步阅读 部分了解更多关于 Web Worker 的信息。

你可以在本章的源代码中找到 chapter_08_example_05.htmlchapter_09_example_05.js 文件中的代码。源代码中有更多的注释和内容——你应该去查看一下。

不幸的是,在撰写本文时,Magenta 的某些部分与 Web 工作线程的兼容性不佳。我们将展示一个使用 MusicVAE 模型的示例,但我们无法展示同样的示例来使用 GANSynth,因为该模型无法在 Web 工作线程中加载。我们仍然提供此示例,因为它可以作为以后使用的基础:

  1. 让我们编写主页面的代码。我们将只包括完整 HTML 页面中的 JavaScript 代码,因为前面章节已经涵盖了其他部分。请按以下步骤进行:
  // Starts a new worker that will load the MusicVAE model
  const worker = new Worker("chapter_09_example_05.js");
  worker.onmessage = function (event) {
    const message = event.data[0];
    if (message === "initialized") {
      // When the worker sends the "initialized" message,
      // we enable the button to sample the model
      buttonSampleMusicVaeTrio.disabled = false;
    }
    if (message === "sample") {
      // When the worked sends the "sample" message,
      // we take the data (the note sequence sample)
      // from the event, create and start a new player
      // using the sequence
 const data = event.data[1];
 const sample = data[0];
      const player = new mm.Player();
      mm.Player.tone.Transport.loop = true;
      mm.Player.tone.Transport.loopStart = 0;
      mm.Player.tone.Transport.loopEnd = 8;
      player.start(sample, 120);
    }
  };
  // Add click handler to call the MusicVAE sampling,
  // by posting a message to the web worker which
  // sample and return the sequence using a message
  const buttonSampleMusicVaeTrio = document
      .getElementById("button-sample-musicae-trio");
  buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
    worker.postMessage([]);
    event.target.disabled = true;
  });

我们已经在前面的示例中覆盖了大部分代码,现在让我们分解新的内容,重点讲解 Web 工作线程的创建以及 Web 工作线程与主线程之间的消息传递,如下所示:

  • 首先,我们需要启动工作线程,方法是使用 new Worker("chapter_09_example_05.js")。这将执行 JavaScript 文件的内容并返回一个可以分配给变量的句柄。

  • 然后,我们将 onmessage 属性绑定到工作线程,当工作线程使用其 postMessage 函数时,该属性会被调用。在 event 对象的 data 属性中,我们可以传递任何我们想要的内容(请参见下面描述的工作线程代码):

  • 如果工作线程将 initialized 作为 data 数组的第一个元素发送,则意味着工作线程已初始化。

  • 如果工作线程将 sample 作为 data 数组的第一个元素发送,则意味着工作线程已对 MusicVAE 序列进行采样,并将其作为 data 数组的第二个元素返回。

  • 最后,当点击 HTML 按钮时,我们在工作线程实例上调用 postMessage 方法(无参数,但至少需要一个空数组),这将启动采样过程。

请记住,Web 工作线程与主线程没有共享状态,这意味着所有的数据共享必须通过 onmessagepostMessage 方法或函数来实现。

  1. 现在,让我们编写 JavaScript 工作线程的代码(该代码与 HTML 文件位于同一位置),如下所示:
importScripts("https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.4.0/dist/tf.min.js");
importScripts("https://cdn.jsdelivr.net/npm/@magenta/music@¹.12.0/es6/core.js");
importScripts("https://cdn.jsdelivr.net/npm/@magenta/music@¹.12.0/es6/music_vae.js");

async function initialize() {
  musicvae = new music_vae.MusicVAE("https://storage.googleapis.com/" +
      "magentadata/js/checkpoints/music_vae/trio_4bar");
  await musicvae.initialize();
  postMessage(["initialized"]);
}

onmessage = function (event) {
  Promise.all([musicvae.sample(1)])
      .then(samples => postMessage(["sample", samples[0]]));
};

try {
  Promise.all([initialize()]);
} catch (error) {
  console.error(error);
}

你首先会注意到,我们使用了 Magenta 的 ES6 打包版本,因为我们不能在 Web 工作线程中导入所有内容。例如,导入 Tone.js 时会导致类似 该浏览器不支持 Tone.js 的错误。另外,记住 Magenta.js 尚未完全兼容 Web 工作线程,这意味着导入 GANSynth 可能会导致错误。

由于我们已经在前面的代码块中覆盖了大部分代码,因此我们将只讨论 Web 工作线程的附加内容,如下所示:

  • 首先,当模型准备好运行时,我们需要使用 postMessage 向主线程发送一个 initialized 消息。

  • 然后,我们绑定在模块的 onmessage 属性上,当主线程发送消息给工作线程时,这个属性会被调用。接收到消息后,我们对 MusicVAE 模型进行采样,然后使用 postMessage 将结果返回给主线程。

这部分涵盖了创建 Web Worker 并使其与主线程交换数据的基本用法。

使用其他 Magenta.js 模型

和往常一样,我们无法涵盖所有模型,但其他模型的使用方法将与我们提供的示例类似。网上有许多 Magenta.js 示例和演示,一些非常令人印象深刻的音乐生成网页应用程序。

我们在 进一步阅读 部分提供了查找示例和演示的资源。

让 Magenta.js 与其他应用程序互动

因为 Magenta.js 运行在浏览器中,它与其他应用程序(如 DAW)的交互要比与 Magenta 应用程序的交互难一些,但随着 Web 标准的发展,这将变得更容易。

使用 Web MIDI API

Web MIDI API (www.w3.org/TR/webmidi/) 是一个 W3C 标准,其规范还不成熟,状态为 W3C 工作草案 2015 年 3 月 17 日。它在浏览器中并不被广泛支持,Firefox 和 Edge 完全不支持。不过,它在 Chrome 中表现得相当不错,因此如果你要求用户使用该浏览器,你的应用可能会正常工作。更多信息请参阅最后一部分 进一步阅读

你可以在 chapter_08_example_06.html 文件中找到这段代码,它在本章的源代码中。源代码中有更多的注释和内容——你应该去查看一下。

我们将编写一个小示例,使用 Web MIDI API,基于之前的 MusicVAE 示例和三重奏采样。你可以复制之前的示例并添加新内容:

  1. 首先,让我们向页面中添加一个 select 元素,像这样:
<label for="select-midi-output">Select MIDI output:</label>
<select disabled id="select-midi-output">
</select>
  1. 然后,在 startMusicVae 方法中,让我们初始化可用的 MIDI 输出列表,如下所示:
// Starts a MIDI player, and for each available MIDI outputs,
// adds an option to the select drop down.
const player = new mm.MIDIPlayer();
player.requestMIDIAccess()
    .then((outputs) => {
        if (outputs && outputs.length) {
            const option = document.createElement("option");
            selectMidiOutput.appendChild(option);
            outputs.forEach(output => {
                const option = document.createElement("option");
                option.innerHTML = output.name;
                selectMidiOutput.appendChild(option);
            });
            selectMidiOutput.disabled = false;
        } else {
            selectMidiOutput.disabled = true;
        }
    });
window.player = player;

在这里,我们使用了 Magenta.js 的 MIDIPlayer 类,它使得使用 requestMIDIAccess 方法比直接调用 Web MIDI API 更加简便。调用此方法将返回一个 output 列表,我们通过选择列表中的 name 属性来添加。

  1. 最后,在 sampleMusicVaeTrio 方法中,我们使用播放器将 MIDI 直接发送到该输出,如下所示:
// Gets the selected MIDI output (if any) and uses the
// output in the MIDI player
const midiOutputIndex = selectMidiOutput.selectedIndex;
if (midiOutputIndex) {
    player.outputs = [player.availableOutputs[midiOutputIndex - 1]];
    mm.Player.tone.Transport.loop = true;
    mm.Player.tone.Transport.loopStart = 0;
    mm.Player.tone.Transport.loopEnd = 8;
    player.start(sample, 120);
}
selectMidiOutput.disabled = true;

在这里,我们只需要用从下拉菜单中选择的元素(如果有)来设置 outputs 列表。

  1. 为了测试我们的代码,我们可以使用我们可靠的 FluidSynth,使用以下命令:

    • Linux: fluidsynth -a pulseaudio -g 1 PATH_TO_SF2

    • macOS: fluidsynth -a coreaudio -g 1 PATH_TO_SF2

    • Windows: fluidsynth -g 1 PATH_TO_SF2

FluidSynth 应该启动并显示一个终端(注意我们移除了 -n-i 标志,以便接收 MIDI 音符)。

  1. 现在,让我们打开我们的 Web 应用。一旦模型初始化完成,我们应该能够在选择 MIDI 输出的下拉列表中看到 FluidSynth MIDI 输入端口。它应该是这样的:合成器输入端口 (17921:0)。选择这个选项,然后点击Sample MusicVAE trio。你应该能听到来自 FluidSynth 的声音。

你会注意到所有的音符都作为钢琴序列播放,即使我们有三种乐器。这是因为MIDIPlayer非常基础,它不会在鼓道上发送打击乐信号,这是 MIDI 规范中规定的。

在 Node.js 中运行 Magenta.js 的服务器端版本

Magenta.js 也可以在服务器端使用,通过 Node.js 来运行。使用 Node.js 的好处是,你可以在服务器端和客户端运行相同的(或几乎相同的)代码。客户端和服务器之间的通信可以通过 WebSockets 来处理。

WebSocket API(developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)是一个 API,它使得客户端和服务器之间能够进行双向通信。我们在这里不会详细讨论 WebSockets,但它们可以是一个非常好的方式,用于在服务器端 Magenta 进程(使用 Node.js 的 Magenta.js 或 Python 中的 Magenta)和客户端应用程序之间传输数据。使用 WebSockets 最简单的方式是使用像 Socket.IO 这样的框架(socket.io/)。

使用 Node.js 的另一个优点是我们的程序在服务器端运行,这意味着它不依赖于浏览器的实现。一个很好的例子是,我们可以使用 Node.js 的包来处理向其他进程发送 MIDI,例如node-midiwww.npmjs.com/package/midi),这样就不需要使用 Web MIDI API。

让我们展示一个 Magenta.js 与 Node.js 一起运行的简单示例。这里显示的代码与我们之前在 JavaScript 中讨论的内容类似:

你可以在本章的源代码中的chapter_08_example_07.js文件中找到这段代码。源代码中有更多的注释和内容——你应该去查看一下。

  1. 首先,让我们安装 Node.js(nodejs.org/en/download/

  2. 接下来,让我们使用npm命令安装 Magenta.js,npm是 Node.js 的依赖管理工具,命令如下:

npm install --save @magenta/music

这将把 Magenta.js 及其依赖项安装到node_modules目录中。当 Node.js 运行时,它会在这个目录中查找脚本的依赖项,处理每个require调用。

  1. 现在我们可以创建一个 JavaScript 文件来采样一个序列,如下所示:
const music_vae = require("@magenta/music/node/music_vae");

// These hacks below are needed because the library uses performance
// and fetch which exist in browsers but not in node.
const globalAny = global;
globalAny.performance = Date;
globalAny.fetch = require("node-fetch");

const model = new music_vae.MusicVAE(
    "https://storage.googleapis.com/magentadata/js/checkpoints/" +
    "music_vae/drums_2bar_lokl_small");
model
    .initialize()
    .then(() => model.sample(1))
    .then(samples => {
        console.log(samples[0])
   });

这段代码与之前的示例相似,唯一的区别是添加了require方法,该方法在 Node.js 中用于导入依赖模块。

  1. 要执行你的 Node.js 应用,请使用node命令(将PATH_TO_JAVASCRIPT_FILE替换为合适的值),如以下所示:
node PATH_TO_JAVASCRIPT_FILE

由于我们使用了 console.log,样本应该显示在控制台上。你还会注意到控制台上有一些信息,内容如下:

This browser does not support Tone.js
Hi there. Looks like you are running TensorFlow.js in Node.js. To speed things up dramatically, install our node backend, which binds to TensorFlow C++, by running npm i @tensorflow/tfjs-node, or npm i @tensorflow/tfjs-node-gpu if you have CUDA. Then call require('@tensorflow/tfjs-node'); (-gpu suffix for CUDA) at the start of your program. Visit https://github.com/tensorflow/tfjs-node for more details.

这提醒我们,Tone.js 不能在 Node.js 上运行,因为 Web Audio API 是在客户端实现的。它还提醒我们,Node.js 可以使用 CUDA 库来提升性能。

总结

在这一章中,我们介绍了 Tensorflow.js 和 Magenta.js,分别是 TensorFlow 和 Magenta 的 JavaScript 实现。我们了解到 TensorFlow.js 是通过 WebGL 加速的,并且 Magenta.js 仅提供有限的模型集,这些模型只能用于生成,不能用于训练。我们还将上一章中使用 Python 训练的模型转换为 TensorFlow.js 可加载的格式。我们还介绍了 Tone.js 和 Web Audio API,Magenta.js 使用它们在浏览器中合成声音。

然后,我们创建了三个音乐生成 web 应用程序。第一个应用使用 GANSynth 采样短音频音符。通过这样做,我们学会了如何导入所需的脚本,可以使用一个大的 ES5 包,也可以使用一个更小的、拆分的 ES6 包。第二个应用使用 MusicVAE 采样三件乐器,分别是鼓组、贝斯和主旋律,并循环播放该序列。第三个应用同时使用了这两个模型生成序列和音频,并介绍了如何使用 Web Workers API 将计算任务转移到另一个线程。

最后,我们讨论了如何使 Magenta.js 与其他应用程序进行交互。我们使用 Web MIDI API 将生成的序列发送到另一个合成器——例如,FluidSynth。我们还使用了 Node.js 在服务器端运行 Magenta.js 应用程序。

Magenta.js 是一个很棒的项目,因为它使得使用 web 技术创建和分享音乐生成应用程序变得容易。还有其他方法可以将 Magenta 融入更广泛的应用场景,例如使用 Magenta Studio(它可以让 Magenta 在 Ableton Live 中运行)和使用 MIDI,这是控制各种设备(如软件和硬件合成器)的好方法。我们将在下一章中展示这些内容。

问题

  1. 是否可以使用 Tensorflow.js 来训练模型?使用 Magenta.js 可以吗?

  2. Web Audio API 的作用是什么,使用它的最简单方法是什么?

  3. GANSynth 中的生成方法是什么?需要提供什么参数?

  4. MusicVAE 中的生成方法是什么?它生成多少种乐器?

  5. 为什么 Web Workers API 在 JavaScript 中有用?

  6. 列举两种将 MIDI 从 Magenta.js 应用程序发送到另一个应用程序的方法。

进一步阅读

第十一章:使 Magenta 与音乐应用程序互动

在本章中,我们将展示 Magenta 如何融入更广泛的应用场景,展示如何使其与其他音乐应用程序(如数字音频工作站DAWs)和合成器)互动。我们将解释如何通过 MIDI 接口将 MIDI 序列从 Magenta 发送到 FluidSynth 和 DAW。通过这种方式,我们将学习如何在所有平台上处理 MIDI 端口,以及如何在 Magenta 中循环 MIDI 序列。我们还将展示如何使用 MIDI 时钟和传输信息同步多个应用程序。最后,我们将介绍 Magenta Studio,它是基于 Magenta.js 的 Magenta 独立包装,可以作为插件集成到 Ableton Live 中。

本章将涵盖以下主题:

  • 将 MIDI 发送到 DAW 或合成器

  • 循环生成的 MIDI

  • 将 Magenta 作为独立应用程序与 Magenta Studio 一起使用

技术要求

在本章中,我们将使用以下工具:

  • 使用命令行Bash从终端启动 Magenta

  • 使用Python及其库编写音乐生成代码,使用 Magenta

  • 使用Magenta生成 MIDI 音乐并与其他应用程序同步

  • 使用Mido及其他 MIDI 工具发送 MIDI 音符和时钟

  • 使用FluidSynth接收来自 Magenta 的 MIDI

  • 你选择的DAW(例如 Ableton Live、Bitwig 等)来接收来自 Magenta 的 MIDI

  • Magenta Studio作为独立应用程序或 Ableton Live 插件

在 Magenta 中,我们将使用MIDI 接口将 MIDI 序列和 MIDI 时钟发送到其他音乐应用程序。我们将深入讨论其使用方法,但如果你需要更多信息,可以查看 Magenta 源代码中的 Magenta MIDI 接口README.md,链接为(github.com/tensorflow/magenta/tree/master/magenta/interfaces/midi),这是一个很好的起点。你还可以查看 Magenta 的代码,里面有很好的文档。我们还在本章末尾的进一步阅读部分提供了额外的内容。

我们还将使用Magenta Studio项目,你可以在其 GitHub 页面上找到更多信息,链接为github.com/tensorflow/magenta-studio

本章的代码位于本书 GitHub 仓库中的Chapter09文件夹,地址为github.com/PacktPublishing/hands-on-music-generation-with-magenta/tree/master/Chapter09。示例和代码片段假定你位于该章节的文件夹中。开始之前,你应使用cd Chapter09进入该文件夹。查看以下视频,了解代码的实际操作:bit.ly/2RGkEaG

将 MIDI 发送到 DAW 或合成器

从本书开始,我们一直在生成 MIDI 作为物理文件,然后使用 MuseScore 或 FluidSynth 进行播放。这是一种很好的作曲方式,能够生成新的序列,保留我们喜欢的部分,并基于它们生成更多内容。但是,如果我们希望 MIDI 音符在模型生成它们时持续播放呢?这就是建立一个自主的音乐生成系统的好方法,Magenta 作为作曲者,外部程序作为播放器,播放它收到的音符并使用乐器。

在这一部分,我们将介绍如何将 MIDI 从 Magenta 发送到合成器或 DAW。我们还将展示如何循环 Magenta 生成的序列,并如何将我们的 Magenta 程序与发送序列的应用程序同步。

介绍一些 DAW

使用 DAW 制作音乐相较于简单的合成器(如 FluidSynth)有很多优势:

  • 录制和编辑 MIDI序列

  • 录制和编辑音频,无论是母带轨道还是单一(乐器)轨道

  • 使用振荡器、包络、滤波器等创建我们自己的合成器

  • 使用效果(如混响、延迟、饱和度等)

  • 对音轨应用均衡器(EQ)和母带处理(mastering)

  • 剪辑、合并和混合音频片段以制作完整的轨道

市面上有很多 DAW,但不幸的是,其中很少有开源或免费的可供使用。我们将对一些我们认为与 Magenta 搭配使用时很有趣的 DAW 进行简要介绍(这并不是全面的介绍):

  • Ableton Livewww.ableton.com非免费)是音乐行业中广为人知的产品,已有很长历史。Ableton Live 是市场上最完整的 DAW 之一,但其所有功能的价格较高。它仅支持 Windows 和 macOS 系统。

  • Bitwigwww.bitwig.com非免费)也是一款非常完整的产品,类似于 Ableton Live,价格略低于其对手。它是一款功能丰富的 DAW,支持所有平台:Windows、macOS 和 Linux。

  • Reasonwww.reasonstudios.com/非免费)是一款专注于乐器和效果的 DAW,而非作曲。它与其他软件(如 Ableton Live 或 Magenta)结合使用时,MIDI 编排效果特别好。它仅支持 Windows 和 macOS 系统。

  • Cubasenew.steinberg.net/cubase/非免费),由著名的音频软件和硬件公司 Steinberg 开发,是市场上最古老的 DAW 之一。它仅支持 Windows 和 macOS 系统。

  • Cakewalkwww.bandlab.com/products/cakewalk免费)是 Bandlab 推出的一个完整且易于使用的 DAW。这是唯一一款非开源但免费的 DAW。遗憾的是,它仅支持 Windows 系统。

  • SuperCollider (supercollider.github.io/免费且开源) 是一个音频合成和算法作曲的平台,允许通过编程开发合成器和效果器,使用的编程语言叫做sclang。它适用于所有平台并且是开源的。

  • VCV Rack (vcvrack.com/免费且开源) 是一个 DAW,它以软件形式再现了模块化合成的乐趣。它适用于所有平台并且是开源的。

我们将使用 Ableton Live 来做示例,但所有 DAW 在接收 MIDI 方面都有类似的功能,所以这些示例应该适用于所有软件。如果有必要,我们会重点说明一些注意事项,比如在 Linux 上处理 MIDI 路由的问题。

使用 Mido 查看 MIDI 端口

首先,我们需要查找机器上可用的 MIDI 端口(如果有的话),以便在应用程序之间发送 MIDI 信息,比如从 Magenta 到 FluidSynth 或 DAW。这里有一个非常实用的库叫做 Mido,它是 Python 的 MIDI 对象库(mido.readthedocs.io),在查找 MIDI 端口、创建新端口和发送数据方面非常有用。

由于 Magenta 依赖于 Mido,它已经在我们的 Magenta 环境中安装好了。

你可以在本章的源代码中的chapter_09_example_01.py文件中跟随这个示例。源代码中有更多的注释和内容,所以你应该去查看一下。

让我们来看一下我们机器上可用的 MIDI 端口:

import mido
print(f"Input ports: {mido.get_input_names()}")
print(f"Output ports: {mido.get_output_names()}")

这应该会产生类似于以下的输出:

Input ports: ['Midi Through:Midi Through Port-0 14:0']
Output ports: ['Midi Through:Midi Through Port-0 14:0']

在 Linux 和 macOS 上,应该已经有一个输入端口和一个输出端口,如前面的输出所示。在 Windows 上,列表可能是空的,因为操作系统不会自动创建任何虚拟 MIDI 端口,或者列表只包含Microsoft GS Wavetable Synth,这是一个类似 FluidSynth 的 MIDI 合成器。

让我们来看一下如何为我们的应用程序创建新的端口以进行通信。

在 macOS 和 Linux 上创建虚拟 MIDI 端口

FluidSynth 的一个优点是它在启动时会自动打开一个虚拟 MIDI 端口。不幸的是,它在 Windows 上无法使用,因此我们首先会了解如何创建虚拟 MIDI 端口。

虚拟 MIDI 端口是可以创建的 MIDI 端口,用于应用程序之间发送 MIDI 消息。这是所有音乐制作应用程序的必备功能。为了让 Magenta 将 MIDI 数据发送到其他程序(如 DAW),我们需要为它们打开一个虚拟端口以便进行通信。

正如我们在前一个例子中看到的,虚拟 MIDI 端口分为输入端口和输出端口。这意味着我们可以创建一个名为magenta的输入端口和一个名为magenta的输出端口。通常来说,使用两个不同的名称会更清晰,例如,magenta_out用于输出端口,magenta_in用于输入端口。在 DAW 中映射端口时,这样也会更简单。

我们将从 Magenta 的角度选择端口名称,也就是说,magenta_out之所以被命名为magenta_out,是因为 Magenta 正在发送信息。

在 macOS 和 Linux 上,创建新的虚拟端口非常简单,因为 Mido 支持可以创建端口的 RtMidi 后台。在 Magenta 中使用MidiHub,我们可以为每个输入和输出提供一个字符串,表示我们想要创建的虚拟端口名称:

from magenta.interfaces.midi.midi_hub import MidiHub

# Doesn't work on Windows if the ports do not exist
midi_hub = MidiHub(input_midi_ports="magenta_in",
                   output_midi_ports="magenta_out",
                   texture_type=None)

如果端口不存在,这将创建两个虚拟端口,magenta_inmagenta_out,如果已存在,则使用现有的。仅使用 Mido,我们可以使用以下代码:

import mido

# Doesn't work on Windows if the ports do not exist
inport = mido.open_input("magenta_in")
outport = mido.open_output("magenta_out")

请注意,输入端口有一个receive方法,而输出端口有一个send方法。当打印端口时,我们应该看到以下内容:

Input ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiOut Client:magenta_out 128:0']
Output ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiIn Client:magenta_in 128:0']

现在,命名的虚拟端口可以在重启前供应用程序使用。

然而,具体是否有效取决于所使用的 DAW。例如,Linux 下的 Bitwig 与 ALSA 虚拟端口配合不佳,因此仅仅通过 RtMidi 打开一个端口是不够的;你需要查看文档,寻找使用JACK 音频连接工具包JACK)的解决方法。其他 Linux 上的 DAW,例如 VCV Rack,则能正常工作并显示虚拟端口。

使用 loopMIDI 在 Windows 上创建虚拟 MIDI 端口

在 Windows 上,我们无法使用之前提供的代码创建虚拟端口。幸运的是,我们有loopMIDI软件(www.tobias-erichsen.de/software/loopmidi.html),这是一款小而老的程序,在 Windows 上使用 MIDI 时简直是救星。它唯一的功能就是在机器上创建命名的虚拟 MIDI 端口。

安装完成后,启动软件,并使用底部的名称字段和加号按钮创建两个新的端口,命名为magenta_inmagenta_out

命名为magenta_inmagenta_out的虚拟端口现在应该可以同时用于 Ableton Live 和 Magenta 进行通信。当创建新端口时,loopMIDI总是同时创建输入端口和输出端口,这意味着我们可以从magenta_in端口发送和接收 MIDI。为了简便起见,我们将保持两个端口分开。

在 Windows 上,如果启动 Magenta MidiHub时遇到以下错误,那是因为你没有正确创建或命名虚拟端口:

INFO:tensorflow:Opening '['magenta_out 2']' as a virtual MIDI port for output.
I1218 15:05:52.208604  6012 midi_hub.py:932] Opening '['magenta_out 2']' as a virtual MIDI port for output.
Traceback (most recent call last):
  ...
NotImplementedError: Virtual ports are not supported by the Windows MultiMedia API.

请注意端口名称magenta_out 2中也包含了端口索引2。这在 Windows 中引用端口时非常重要,因为它们是使用格式:名称 索引进行命名的。这有点麻烦,因为如果你创建新的端口(或插件新的 MIDI 设备)来改变索引,端口索引可能会发生变化。

为了解决这个问题,我们确保使用字符串包含而不是精确匹配来过滤端口(我们提供的所有示例在这方面都能正常工作)。

在 macOS 上添加虚拟 MIDI 端口

在 macOS 上,我们可以使用前面在查看虚拟 MIDI 端口部分中描述的那种方法,或者使用内置的 macOS 界面创建一个新的虚拟端口。使用内置界面很简单:

  1. 启动音频 MIDI 设置

  2. 打开窗口菜单并点击显示 MIDI 工作室

  3. 选择IAC 驱动程序图标。

  4. 启用设备在线复选框。

然后,我们可以使用+按钮创建命名的虚拟端口。

发送生成的 MIDI 到 FluidSynth

为了将 Magenta 生成的 MIDI 发送到 FluidSynth,我们将从第二章中编写的第一个示例中,使用 DrumsRNN 生成鼓序列,并添加一些代码将 MIDI 消息直接发送到软件合成器。

你可以在本章的源代码中找到chapter_09_example_02.py文件中的示例。源代码中有更多的注释和内容,所以你应该去查看一下。

这与我们在上一章中使用 Web MIDI API 从浏览器将 MIDI 音符发送到 FluidSynth 时所做的类似:

  1. 首先,我们将使用以下之一启动 FluidSynth:

    • Linux: fluidsynth -a pulseaudio -g 1 PATH_TO_SF2

    • macOS: fluidsynth -a coreaudio -g 1 PATH_TO_SF2

    • Windows: fluidsynth -g 1 -o midi.winmidi.device=magenta_out PATH_TO_SF2

请注意 Windows 命令中的-o标志,它告诉 FluidSynth 监听这个 MIDI 端口,因为在 Windows 上,它不会自动打开端口。

另外,请注意我们这次没有使用-n-i标志,因为我们希望保留传入的 MIDI 消息并使用合成器命令行。程序应该会停留在命令行界面,并且应该自动创建一个新的输入 MIDI 端口(或者使用提供的端口)。

在 Windows 上,如果你在启动 FluidSynth 时看到以下错误消息:fluidsynth: error: no MIDI in devices foundFailed to create the MIDI thread,这意味着你可能拼写错误了 MIDI 端口名,或者没有打开loopMIDI

在 macOS 和 Linux 上,你可以再次运行之前的示例代码,应该会看到类似如下的输出:

Input ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiOut Client:magenta_out 128:0']
Output ports: ['FLUID Synth (7171):Synth input port (7171:0) 129:0', 'Midi Through:Midi Through Port-0 14:0', 'RtMidiIn Client:magenta_in 128:0']

在这里,FLUID Synth (7171): Synth input port (7171:0) 129:0端口是 FluidSynth 端口。我们还可以看到来自前一个示例的magenta_outmagenta_in端口。

在 Windows 上,重新运行之前的示例代码应该会给你这个:

Input ports: ['magenta_in 0', 'magenta_out 1']
Output ports: ['Microsoft GS Wavetable Synth 1', 'magenta_in 2', 'magenta_out 3']

我们将使用的 FluidSynth 输入端口是magenta_out 3端口,它应该与提供给 FluidSynth 的-o midi.winmidi.device=magenta_out标志匹配。

  1. 接下来,我们将复制chapter_02_example_01.py示例:
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--midi_port", type=str, default="FLUID Synth")
args = parser.parse_args()

def generate(unused_argv):
 # The previous example is here
 ...

  # Write the resulting plot file to the output directory
  plot_file = os.path.join("output", "out.html")
  pretty_midi = mm.midi_io.note_sequence_to_pretty_midi(sequence)
  plotter = Plotter()
  plotter.show(pretty_midi, plot_file)
  print(f"Generated plot file: {os.path.abspath(plot_file)}")

 # Write the code to send the generated "sequence" to FluidSynth
  pass

  return 0

if __name__ == "__main__":
  tf.app.run(generate)

我们添加了一个--midi_port标志来轻松更改 MIDI 输出端口(记住,输入和输出术语是从 Magenta 的角度看待的)。我们将在generate方法的末尾编写代码,以发送 MIDI 内容(它存储在sequence变量中)。

  1. 我们找到提供的输出端口并使用该端口初始化MidiHub
import mido
from magenta.interfaces.midi.midi_hub import MidiHub

# We find the proper input port for the software synth
# (which is the output port for Magenta)
output_ports = [name for name in mido.get_output_names()
 if args.midi_port in name]

# Start a new MIDI hub on that port (output only)
midi_hub = MidiHub(input_midi_ports=[], 
 output_midi_ports=output_ports, 
 texture_type=None)

然后,我们在该端口上启动一个新的 MIDI 中心;它将作为我们应用程序与合成器之间的通信接口。它很有用,因为它使我们能够直接使用NoteSequence对象,而无需手动转换它们。

midi_hub模块位于 Magenta 的magenta.interfaces.midi模块中,并包含处理 MIDI 的有用工具。

  1. 接下来,我们将从中心获取一个播放器实例,并将播放通道设置为9
import music_pb2

empty_sequence = music_pb2.NoteSequence()
player = midi_hub.start_playback(empty_sequence, allow_updates=True)
player._channel = 9

请记住,兼容 GM 1 的合成器如果 MIDI 通道为10时会播放鼓声音色(但在 Magenta MIDI 中,通道是从零开始计数的,因此我们需要使用9)。我们将在一个空序列上开始播放,允许稍后更新序列。

  1. 现在我们可以播放我们的sequence,但首先需要调整它,以便播放器知道何时开始:
import time
from magenta.interfaces.midi.midi_interaction import adjust_sequence_times

wall_start_time = time.time()
sequence_adjusted = music_pb2.NoteSequence()
sequence_adjusted.CopyFrom(sequence)
sequence_adjusted = adjust_sequence_times(sequence_adjusted, 
 wall_start_time)

MIDI 播放器将根据墙时(wall time)播放sequence,但我们的序列从0开始(墙时是从纪元开始的时间)。例如,如果墙时(由time.time()提供)为1564950205,那么我们需要将序列的起始时间向前调整这个数值。我们通过保持当前序列不变,并制作一个副本交给播放器来做到这一点。我们使用 Magenta 中的adjust_sequence_times函数来完成这个操作。

请注意这里使用了CopyFrom方法,该方法存在于 Protobuf 消息对象中。你可以随时检查google.protobuf.message.Message类中的方法,以便找到对NoteSequence有用的方法。

  1. 现在我们已经将序列调整到正确的时间,让我们播放它吧!我们使用播放器上的update_sequence方法来实现这一点,它相当于play
player.update_sequence(sequence_adjusted, start_time=wall_start_time)
try:
  player.join(generation_end_time)
except KeyboardInterrupt:
  return 0
finally:
  return 0

我们还向播放器的instance提供了start_time参数,这个参数等于我们调整后的(向前偏移的)序列的起始时间。

由于player是一个线程,我们需要等它完成后再退出,否则程序会在序列播放之前退出。我们通过在播放器实例上使用join方法来做到这一点,join方法存在于任何线程类中。这个方法会阻塞,直到线程完成,但因为播放器线程永远不会停止,这个调用将无限期阻塞。通过添加generation_end_time(即生成序列的长度)作为超时,这个调用将在序列播放结束后返回。被阻塞的join调用可以通过按Ctrl + C中断,此操作会被KeyboardInterrupt异常类捕获。

  1. 现在,我们可以在 Linux 和 macOS 上使用以下命令启动程序:
> python chapter_09_example_02.py

通过保持默认的--midi_port标志,它将使用 FluidSynth 启动的端口。

或者我们可以在 Windows 上使用magenta_out MIDI 端口:

> python chapter_09_example_02.py --midi_port=magenta_out

现在,你应该能听到你的音乐从 FluidSynth 播放!在执行代码时,你可能会看到以下警告:

WARNING:tensorflow:No input port specified. Capture disabled.

这是因为 MIDI 中心(MIDI hub)也可以接收 MIDI 消息,但我们尚未提供任何 MIDI 端口来接收。因此,这仅仅是一个警告,不应该成为问题。

将生成的 MIDI 发送到 DAW

将 MIDI 发送到 FluidSynth 很不错,但你可能希望使用其他软件来制作音乐。我们不会讨论所有 DAW,但会展示一些适用于大多数音乐制作软件的示例。

现在我们已经为从 Magenta 应用程序传输 MIDI 打开了虚拟 MIDI 端口,接下来在 Ableton Live 中进行测试。你也可以在任何其他具备 MIDI 功能的 DAW 中尝试此方法。

你可以在本章的源代码中找到 Ableton Live 设置(扩展名为 .als 文件),路径为 chapter_09_example_02.als 文件。

你可以将这个 Ableton 设置与我们在前一个示例中展示的 Python 代码 chapter_09_example_02.py 一起使用。

让我们在 Ableton Live 中配置 magenta_out 端口,该端口也将被 Magenta 应用程序使用:

  1. 首先,在 Ableton 中,进入 文件 > 选项 > 首选项... > 链接 MIDI,然后找到 magenta_out 输入:

我们需要将 轨道远程 都设置为 开启 以接收 MIDI 音符。

  1. 现在 MIDI 输入已被激活,我们可以通过右键点击 在此处拖放文件和设备 区域,选择 插入 MIDI 轨道 来创建一个新的 MIDI 轨道。

  2. 在新轨道中,我们可以看到以下的 MIDI From 区域:

在截图中,我们标出了三个部分:

  • MIDI From 区域,这是一个 MIDI 轨道的设置,我们现在可以选择 magenta_out MIDI 端口。我们还选择了 Ch. 10 作为鼓道 10 和 监视器 设置为 输入

  • 第三八度 位于表示所有 127 种可能 MIDI 值的 8 个八度音阶条上,其中定义了 808 核心套件。这对应于 MIDI 音符 36 到 52,你可以看到音符 38 当前正在播放。

  • 当前播放的音符,808 小军鼓,属于 808 核心套件 乐器。

在右上角,一个黄色指示灯显示是否有输入 MIDI,这对于调试非常有用。

  1. 现在我们已经设置好了 Ableton Live,可以通过以下方式启动我们的应用程序:
> python chapter_09_example_02.py --midi_port="magenta_out"

你应该能在 Ableton Live 中接收到 MIDI 信号,并听到 808 核心套件 播放打击乐音效。

使用 NSynth 生成的样本作为乐器

在前一章节 第五章,使用 NSynth 和 GANSynth 生成音频,我们讨论了如何通过使用 Magenta 生成的 MIDI 来编排我们生成的样本。现在我们可以动态地将生成的 MIDI 发送到 DAW,这正是一个很好的测试时机。

在 Ableton Live 中,在 808 核心套件 区域,我们可以拖放一个生成的样本来替换现有的鼓组样本。例如,我们可以将 Cowbell 808 乐器替换为我们的一个样本,例如 160045_412017

当双击新声音时,采样器界面将打开,你可以修改循环的开始和结束位置,以及音量。我们选择这个样本是因为它有很强的攻击性(声音包络上升得很快),非常适合做打击乐样本。你也可以尝试自己的样本。

在映射通道 10 上的鼓声时,请记住打击乐器是根据 MIDI 音高选择的。在之前的图中,网格中的 16 种乐器被映射到 MIDI 音高,如下所示:

48 49 50 51
44 45 46 47
40 41 42 43
36 37 38 39

在这里,音高 36 对应于Kick 808,音高 37 对应于Rim 808,音高 51 对应于我们的160045_412017样本,依此类推。你可以将这个网格与我们的程序输出的 MIDI 图(在output/out.html中)进行对比。

这对于鼓元素非常有效。但如果你将旋律发送到 DAW,你可能会想使用采样器,它会根据输入音符改变声音的音高。为此,在 Ableton Live 中,按照以下步骤操作:

  1. 右键点击Drop Files and Devices Here区域,选择Insert MIDI track来创建一个新的 MIDI 轨道。

  2. 通过选择Instruments > Sampler来找到Sampler乐器。

  3. Sampler拖放到底部的Drop Audio Effects Here区域(在新的 MIDI 轨道中)。

  4. 将生成的412017_83249样本(或你选择的其他样本)拖放到底部的Drop Sample Here区域(在Sampler中)。

我们选择了412017_83249生成的样本,因为猫的声音在作为旋律播放时发出一个不错的(且有趣的)音符。你应该看到以下界面:

现在,当你从 Magenta 程序发送旋律时,你会听到样本412017_83249被播放并根据旋律音符的音高进行升降调。

循环生成的 MIDI

现在我们可以将生成的 MIDI 发送到 DAW,让我们来看一下如何循环生成的 MIDI。这开启了许多不同的用例,例如构建一个持续生成音乐的系统。我们将首先看看如何循环NoteSequence。我们还将讨论如何使用 MIDI 时钟将 Magenta 与 DAW 同步,这在长时间运行的现场音乐系统中非常重要。

使用 MIDI 播放器循环一个序列

在这个示例中,我们将使用 Magenta 中的player实例来循环生成的NoteSequence,通过复制序列并在稍后的时间播放,直到播放器结束播放。

你可以在本章源代码中的chapter_09_example_03.py文件中跟随这个示例。源代码中有更多注释和内容,所以你应该去查看。

让我们用之前的例子并让序列无限循环:

  1. 首先,我们来找出周期,这相当于循环时间(以秒为单位):
from decimal import Decimal
from magenta.common import concurrency

period = Decimal(240) / qpm
period = period * (num_bars + 1)
sleeper = concurrency.Sleeper()

在这里,我们需要一个 4 小节的周期(以秒为单位),即循环长度。使用 240/QPM,我们可以得到 1 小节的周期(例如,120 QPM 下为 2 秒)。然后我们将其乘以 4 小节(num_bars + 1),这就是我们的循环长度。此外,我们使用Decimal类,它不像内置的float那样有舍入误差,以提高时间精度。

我们利用 Magenta 的Sleeper类,它实现了比time模块中的sleep更精确的版本,因此它应该能以正确的时间更加一致地唤醒。

  1. 现在让我们定义主循环,它将复制当前序列,调整时间并使用播放器播放:
while True:
  try:
    # We get the next tick time by using the period
    # to find the absolute tick number (since epoch)
    now = Decimal(time.time())
 tick_number = int(now // period)
    tick_number_next = tick_number + 1
 tick_time = tick_number * period
    tick_time_next = tick_number_next * period

    # Update the player time to the current tick time
    sequence_adjusted = music_pb2.NoteSequence()
    sequence_adjusted.CopyFrom(sequence)
 sequence_adjusted = adjust_sequence_times(sequence_adjusted,
 float(tick_time))
 player.update_sequence(sequence_adjusted,
 start_time=float(tick_time))

    # Sleep until the next tick time
 sleeper.sleep_until(float(tick_time_next))
  except KeyboardInterrupt:
    print(f"Stopping")
    return 0

让我们稍微解析一下代码:

  • 在每个循环开始时,我们获取当前的自纪元以来的时间(以now表示)。

  • 我们通过将当前时间除以周期来获取当前的节拍数(以tick_number表示)。节拍数对应于从纪元到现在的时间区间按period分割后的当前索引。

  • 我们通过将周期与节拍数相乘来获取当前的节拍时间(以tick_time表示)。

例如,如果起始时间是1577021349,我们有一个滴答时间1577021344和下一个滴答时间1577021352(周期为 8 秒)。在这种情况下,我们处于循环的第一次迭代,这就是为什么起始时间和滴答时间之间有如此大的差异。第二次循环时,起始时间将是1577021352(大约),因为线程将在正确的时间唤醒。

由于第一次循环的起始时间差异,这意味着当播放器启动时,它可能会从生成的序列的中间开始。如果我们希望它从序列的开头开始,我们需要在计算节拍数时减去起始时间。请查看magenta.interfaces.midi.midi_hub模块中的Metronome类,了解更完整的实现。

最后,我们使用tick_time更新序列和播放器,并在tick_time_next之前休眠。

  1. 现在我们可以通过以下方式启动程序:
> python chapter_09_example_03.py --midi_port="magenta_out"

你现在应该能在你使用的 DAW 中听到一个 120 QPM、持续 8 秒的 4 小节循环。

将 Magenta 与 DAW 同步

在演奏乐器时,同步设备非常重要。两个同步的乐器会有相同的 QPM节奏)并且在相同的拍子相位)上开始。解决这些问题表面看起来很简单,但良好的同步非常难以实现,因为精确的时间控制很困难。

将我们的 Magenta 应用与 DAW 同步有很多用途,例如,在 DAW 中以正确的时间(节奏和相位)录制 MIDI 序列,或者同时播放多个序列,其中一些来自 Magenta,另一些来自 DAW。

发送 MIDI 时钟和传输

在此示例中,我们将使用 MIDI 时钟和传输(启动、停止和重置)信息将 Magenta 与数字音频工作站(DAW)同步。MIDI 时钟是最古老且最流行的设备同步方式之一,几乎所有的乐器和音乐软件都支持它。

我们将给出在 Ableton Live 中的示例,但你也可以在任何具有 MIDI 时钟功能的 DAW 中尝试此操作。

你可以在本章的源代码中的chapter_09_example_04.py文件中查看此示例。源代码中有更多的注释和内容,你应该去查看一下。

为了将我们的 Magenta 程序与 Ableton Live 同步,我们将启动一个节拍器线程,该线程将在每个节拍上唤醒并发送一个时钟消息:

  1. 首先,让我们声明Metronome类,它继承自Thread类:
import mido
from decimal import Decimal
from threading import Thread

class Metronome(Thread):

  def __init__(self, outport, qpm):
    super(Metronome, self).__init__()
 self._message_clock = mido.Message(type='clock')
 self._message_start = mido.Message(type='start')
 self._message_stop = mido.Message(type='stop')
 self._message_reset = mido.Message(type='reset')
    self._outport = outport
 self._period = Decimal(2.5) / qpm
    self._stop_signal = False

  def stop(self):
    self._stop_signal = True

  def run(self):
 # Run code
 pass

在实例化时,我们使用 Mido 定义以下消息(有关 Mido 支持的消息及其在 MIDI 规范中的对应项,请参阅最后一节,进一步阅读):

    • clock消息,每个节拍发送一次

    • start消息,在序列开始时发送

    • stop消息,在序列结束时或程序退出时发送

    • reset消息,在start消息之前发送,确保同步的设备从节拍计数的开始重新启动

    • continue消息,我们不会使用它,但它可以用来在不重置节拍计数的情况下重新启动播放

我们还定义了周期,即每次线程唤醒之间的确切时间。线程需要在每个节拍时唤醒,因此在 120 QPM 的 4/4 拍中,它需要每 0.5 秒唤醒一次,这就是周期。

在这里,我们选择使用每个节拍一个消息(或脉冲)来同步两个应用程序,这是我们的周期,因为这样做很简单。在 MIDI 规范中(www.midi.org/specifications/item/table-1-summary-of-midi-message),还描述了另一种同步周期,称为24 每四分之一音符脉冲24 PPQN),它比我们这里实现的更精确。

每个节拍一个脉冲和 24 PPQN 都在许多 DAW 和乐器中使用。然而,还有其他的同步脉冲,例如 Korg 乐器使用的 48 PPQN。还有其他同步乐器的方式,例如MIDI 时间码MTC),我们在这里不讨论。

根据你尝试同步的软件或硬件,确保检查它们配置处理的同步脉冲类型。如果这个方法不起作用,可能是因为你发送了一个意外的脉冲率。

  1. 现在,让我们实现# Run code注释中的run方法:
import time
from magenta.common.concurrency import Sleeper

def run(self):
  sleeper = Sleeper()

  # Sends reset and the start, we could also
  # use the "continue" message
 self._outport.send(self._message_reset)
 self._outport.send(self._message_start)

  # Loops until the stop signal is True
  while not self._stop_signal:
    # Calculates the next tick for current time
    now = Decimal(time.time())
    tick_number = max(0, int(now // self._period) + 1)
 tick_time = tick_number * self._period
 sleeper.sleep_until(float(tick_time))

    # Sends the clock message as soon it wakeup
 self._outport.send(self._message_clock)

  # Sends a stop message when finished
 self._outport.send(self._message_stop)

以下列表进一步解释了代码:

    • 当线程首次启动时,它发送一个reset消息,紧接着发送一个start消息,意味着 Ableton Live 将其节拍计数重置为 0,然后开始播放。

    • 然后,我们计算下一个时钟滴答的时间,并让线程休眠至该时间(请参见前面关于滴答时间的解释)。醒来后,我们发送 clock 消息,这将在每个节拍时发生。

    • 最后,如果调用 stop 方法,self._stop_signal 将被设置为 True,这将退出循环,并发送 stop 消息。

  1. 让我们初始化线程并启动它:
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--midi_port", type=str, default="magenta_out")
args = parser.parse_args()

def send_clock():
  output_ports = [name for name in mido.get_output_names()
                  if args.midi_port in name]
  midi_hub = MidiHub(input_midi_ports=[],
                     output_midi_ports=output_ports,
                     texture_type=None)
 outport = midi_hub._outport

  # Starts the metronome at 120 QPM
 metronome = Metronome(outport, 120)
 metronome.start()

  # Waits for 16 seconds and send the stop command
 metronome.join(timeout=16)
 metronome.stop()

  return 0

if __name__ == "__main__":
  send_clock()

以下列表将进一步解释:

  • 代码与我们之前的示例类似。我们首先改变的内容是,我们保留对 midi_hub._outport 端口的引用,以便将 MIDI 时钟发送到该端口。

  • 然后,我们使用 outport 初始化 Metronome 类,并通过 start 启动它。这将执行线程中的 run 方法。

  • 然后,我们通过 16 秒的超时调用 join 方法,意味着我们将在退出并调用 stop 方法之前播放 8 小节。我们这样做只是为了展示 stop 方法的使用及其对 Ableton Live 的影响。

  1. 在 Ableton Live 中,我们需要确保 Sync 按钮已为 magenta_out 端口 开启

  1. 一旦完成这一步,我们需要确保屏幕左上角的 Ext 按钮已激活:

Ext 按钮,代表 External,意味着 Ableton 不会使用其内部时钟,而是依赖外部时钟源。

大多数 DAW 和硬件合成器都有类似的 External 选项,但默认情况下通常是禁用的。确保查看如何为你正在同步的软件或硬件激活该选项。

Ext 按钮右侧,两个指示器显示进出 MIDI 时钟消息,这对于调试非常有用。我们还突出显示了以下内容:

    • QPM 指标将在播放过程中更新为 120(目前为了测试目的,设置为 110 QPM)

    • Arrangement position 部分,显示 9.1.1,这是当我们的 Python 程序退出并发送 stop 消息时节拍计数的值(因为我们在 8 小节后停止)

    • Transport section 部分,其中包含开始、停止和录音按钮,当我们启动和停止程序时,按钮会更新

现在,我们可以向 Ableton Live 发送 MIDI 时钟。

  1. 最后,启动我们的 Magenta 应用程序:
> python chapter_09_example_04.py --midi_port="magenta_out"

在 Ableton Live 中,你应该看到 BPM 改为 120 QPM。虽然可能需要一些时间才能达到这个值,并且可能会在稳定过程中上下波动,但最终应该稳定在 120 QPM。16 秒后,Ableton Live 应该停止,最终的节拍计数为 8 个完整的节拍(显示为 9.1.1)。

使用 MIDI 控制消息

发送 MIDI 时钟是最常见的设备同步方式,因为所有设备都支持 MIDI 时钟。另一种将 Magenta 与 DAW 同步的方法是使用 MIDI 控制消息

MIDI 控制消息是一个发送controlvalue的消息。例如,我们可以使用以下 Mido 消息来发送 MIDI 控制:mido.Message(type="control_change", control="...", value"...")。让我们定义一些控制消息来执行我们想要的操作:

  • 开始/停止:用于启动和停止传输,用于同步相位(分别使用control="1"control="2")。

  • QPM:这是在传输开始前设置节奏的方式(使用control="3")。

这只是一个控制值的示例;你可以使用任何你想要的值,只要它在 DAW 端正确映射即可。在大多数 DAW 中,将控制消息映射到输入是很容易的。通常,DAW 会提供一个learn功能,激活后,它会将选定的输入映射到接下来到来的任何 MIDI 消息。

让我们在 Ableton Live 中尝试一下:

  1. 使用右上角的MIDI按钮激活 MIDI 映射模式(Ableton 中的所有可映射输入会变成紫色)。

  2. 选择你想映射的输入(例如QPM),然后发送相应的 MIDI 控制消息(参见前面的代码片段),它将把输入映射到控制消息。

  3. 在接收到 MIDI 控制消息后,Ableton 中的输入将会与之映射。

  4. 退出 MIDI 映射模式,然后发送相同的 MIDI 控制消息。映射的输入应该会被激活。

一旦我们所有的输入都被映射,我们就可以从 Magenta 应用程序发送相应的消息,按需开始、停止或更改 QPM。例如,Magenta 应用程序可以在开始之前发送 QPM,然后在发送第一个 MIDI 音符时,同时发送 MIDI 控制消息开始

这种方法的缺点是,如果两个应用程序中的任何一个出现不同步的情况,就无法在不停止并重新启动播放的情况下将它们重新同步。另一方面,MIDI 时钟则会持续地同步设备。

使用 Ableton Link 同步设备

Ableton Link (github.com/Ableton/link)是一个旨在同步软件设备的开源标准。它支持在本地网络上的自动发现,并且易于使用。现在许多 DAW 都支持 Ableton Link,这又是另一种将 Magenta 应用程序与 DAW 同步的方式,但需要实现该规范。

向硬件合成器发送 MIDI

向硬件合成器发送 MIDI 与我们在前面章节中的操作非常相似,唯一不同的是硬件合成器需要自己打开一个新的 MIDI 端口(就像 FluidSynth 一样),所以我们不需要为它创建虚拟端口。

我们将使用 Arturia BeatStep Pro 作为示例,但这应该适用于任何支持 MIDI 的设备:

  1. 首先,我们需要为合成器安装驱动程序,是否需要安装取决于合成器和平台。

  2. 然后,我们通过 USB 将合成器连接到计算机,并运行第一个示例,以找出已声明的 MIDI 端口。对于 Windows 上的 Arturia BeatStep Pro,我们有输出端口 MIDIIN2 (Arturia BeatStep Pro) 1

  3. 现在,我们可以通过将 Magenta 输出端口更改为合成器输入端口来运行之前的示例:

> python chapter_09_example_03.py --midi_port="MIDIIN2 (Arturia BeatStep Pro) 1"

这应该直接将 MIDI 发送到硬件合成器。

此示例使用 USB MIDI 发送 MIDI,然而并不是所有合成器都支持这种方式。有些合成器仅支持通过 MIDI 电缆连接,而非 USB 电缆,这意味着你需要一个声卡或 USB 转 MIDI 转换器。过程依然相同,但你必须通过声卡或转换器。

将 Magenta 作为独立应用程序与 Magenta Studio 一起使用

Magenta Studio 是最接近 Magenta 独立应用程序的工具,因为它不需要任何安装,也不需要了解任何技术来使其工作。这一点尤为重要,因为 Magenta 及其技术是复杂的,但最终,每个人都能使用它 这一点是非常重要的。

我们将了解 Magenta Studio 的工作原理,并找到我们在前几章中已经覆盖过的许多元素。Magenta Studio 有两种打包方式:

我们不会过多讨论独立应用程序,因为我们已经涵盖了关于它们的所有必要知识。实际上,Electron 应用程序是一个带有其运行时和 Chromium 浏览器的 Node.js 应用程序,因此我们已经在前一章 第八章 中讲解了这些内容,Magenta.js 中的 Magenta 浏览器

查看 Magenta Studio 的内容

由于这两种打包方式都基于 Magenta.js,它们包含相同的功能:

  • CONTINUE 使用 MusicRNN(基于 LSTM),根据使用情况选择 DrumsRNN 模型或 MelodyRNN 模型,从一个引导器开始继续一个序列。

  • GENERATE 使用 MusicVAE 模型,使用一个 4 小节的模型来生成鼓点或旋律。

  • INTERPOLATE 也使用 MusicVAE 模型。

  • GROOVE 使用 GrooVAE 模型为量化序列添加 groove。

  • DRUMIFY 使用 GrooVAE tap 模型将 tap 序列 转换为 鼓点序列

下载独立版本时,你将能够安装任何五个应用程序(取决于平台使用 .exe.dmg)。安装并启动后,应用程序将如下所示:

你可以找到我们之前讨论过的许多参数:温度、长度、变化(生成序列的数量)、步数(插值的数量)等。独立应用程序和 Ableton 包装版本的区别在于它们如何与我们的音乐工具集成:独立应用程序可以处理磁盘上的文件(如前述截图所示,使用选择文件...按钮),而 Ableton Live 插件则可以直接读取和写入Session View中的剪辑。

让我们来看看 Ableton Live 插件的集成。

在 Ableton Live 中集成 Magenta Studio

Magenta Studio 在 Ableton Live 中的插件集成非常棒,因为它符合机器学习增强音乐制作环境的理念。一般来说,Magenta 在现有工具中的集成非常重要,Magenta Studio 就是一个很好的例子。

了解 Ableton Live 插件的设计很有趣,因为它非常巧妙。在 Ableton Live 中,你可以将 Max MSP 应用程序作为插件或设备集成。Max MSP(cycling74.com/products/max-features/)是一个强大的音乐视觉编程语言。Ableton Live 插件的工作方式如下:

  1. Ableton Live 启动了magenta.amxd补丁,这是一个 Max MSP 程序。

  2. Max MSP 程序会在 Ableton Live 中显示一个 UI 界面,我们可以选择ContinueGenerate等程序。

  3. 选择后,Max MSP 程序将启动一个 Node.js 进程,包含 Magenta.js 应用程序(与独立应用程序相同)。

  4. 使用 Max MSP API,Magenta.js 应用程序可以查看 Ableton Live 的Session View内容,包括剪辑和轨道,并进行内容写入。

目前,Magenta Studio 仅在 Ableton Live 中集成。未来可能会集成其他 DAW,因为 Magenta Studio 的实现并没有什么特定于 Ableton Live 的内容。

为了使这个示例生效,我们需要 Ableton Live 10.1 Suite 版,因为 Magenta Studio 的运行需要集成 Max For Live(仅在Suite版中可用)。如果你没有该程序,可以在www.ableton.com/en/trial/尝试演示版。

让我们通过一个完整的示例来演示Continue应用程序:

  1. magenta.tensorflow.org/studio/ableton-live下载适用于你平台的 Max MSP 补丁,点击下载按钮,这将下载magenta_studio-VERSION-windows.amxd文件。

  2. 打开 Ableton Live,创建一个新的 MIDI 轨道,将文件拖放到 MIDI 轨道设备中(加载可能需要一些时间):

在前面的截图中,我们看到我们从之前的示例中录制了两个 MIDI 片段,MIDI from Magenta 1MIDI from Magenta 2,我们将使用这些片段通过 Continue 插件生成新内容。我们可以在 Magenta Studio Plugin 轨道的底部看到 Magenta Studio 补丁。

  1. 现在,让我们点击 Magenta Studio 插件中的 CONTINUE。你应该看到 Continue Node.js 应用程序启动:

Input Clip 部分,我们从 MIDI from Magenta 轨道中添加了 MIDI from Magenta 2 的 MIDI 片段,这将由 DrumsRNN 模型作为启动器使用。四种变化将在启动器片段后自动添加到 Ableton Live 中,名称为 x/4 [MIDI from Magenta 2],其中 x 是生成的片段的索引。

总结

在本章中,我们讨论了 Magenta 与已建立的音乐制作软件的互动。

首先,我们展示了如何将 MIDI 从 Magenta 发送到 DAW 或合成器。我们首先使用 Mido,这是一个强大的 Python 库,用于处理 MIDI 操作,查看了 MIDI 端口。我们展示了如何在 Magenta 中循环 MIDI 的示例,这需要正确的时序和线程工具。我们还讨论了 Magenta 和 DAW 之间的同步,使用了各种方法,最著名的是使用 MIDI 时钟消息和传输消息。我们通过展示 Magenta 如何直接将 MIDI 发送到硬件合成器(如键盘)来结束 MIDI 部分。

最后,我们介绍了 Magenta Studio,既作为独立应用程序,又作为 Ableton Live 插件。我们查看了它在 Ableton Live 中的整合,以及将 Magenta 集成到现有音乐工具中的重要性。

观察 Magenta 在音乐制作生态系统中的整合是完美的结尾。它提醒我们,Magenta 不是一个独立的终点,而是一个需要与其他音乐制作工具结合使用才能真正有用的工具。通过开发像 Magenta.js 和 Magenta Studio 这样的项目,Magenta 正在变得更加易于更广泛的非技术用户使用。

在提高 Magenta 对所有人可用性方面,仍然有很多工作要做。然而,这是一个伟大音乐制作工具的开始。

问题

  1. 软件合成器(如 FluidSynth)和数字音频工作站(DAW)(如 Ableton Live)之间有什么区别?

  2. 为什么打开 MIDI 虚拟端口是使音乐软件相互互动所必需的?

  3. 基于 chapter_09_example_03.py 编写代码,而不是循环四小节的序列,每四小节生成一个新序列。

  4. 为什么基于 MIDI 控制消息的同步不够稳定?

  5. 为什么 Magenta Studio 在音乐创作生态系统中如此重要?

  6. Magenta Studio 插件和 Magenta Studio 独立版背后的技术是什么?

进一步阅读

第十二章:评估

第一章:Magenta 与生成艺术简介

  1. 随机性。

  2. 马尔可夫链。

  3. Algorave。

  4. 长短期记忆LSTM)。

  5. 自主系统生成音乐,无需操作员输入;辅助音乐系统将在创作时补充艺术家的工作。

  6. 符号化:乐谱、MIDI、MusicXML、AbcNotation。子符号化:原始音频(波形)、频谱图。

  7. "音符开启"与"音符关闭"的时序、音高范围为 1 到 127 kHz、速度和通道。

  8. 在 96 kHz 的采样率下,奈奎斯特频率为 96 kHz/2 = 48 kHz,频率范围为 0 到 48 kHz。这对于听音频来说效果较差,因为 28 kHz 的音频在耳朵上是听不见的(记住,超过 20 kHz 的声音是无法听到的),而且这种采样率并不被很多音频设备正确支持。不过,在录音和音频编辑中它还是有用的。

  9. 单个音乐音符 A4 被响亮地演奏 1 秒钟。

  10. 鼓、声部(旋律)、和声(复音)、插值和操作。

第二章:使用鼓 RNN 生成鼓序列

  1. 给定一个当前序列,预测下一个音符的乐谱,然后对每一个你希望生成的步骤进行预测。

  2. (1) RNN(递归神经网络)对向量序列进行操作,用于输入和输出,这对于像乐谱这样的序列数据非常适用;(2) 保持一个由前一输出步骤组成的内部状态,这对于基于过去输入做出预测非常有用,而不仅仅是基于当前输入。

  3. (1) 首先,隐藏层将得到 h(t + 1),即前一个隐藏层的输出;(2) 它还将接收 x(t + 2),即当前步骤的输入。

  4. 生成的音符小节数将是 2 小节,或者 32 个步骤,因为每小节有 16 个步骤。在 80 QPM 下,每个步骤的时长为 0.1875 秒,因为你将一分钟的秒数除以 QPM,再除以每小节的步骤数:60 / 80 / 4 = 0.1875。最后,你有 32 个步骤,每个步骤时长 0.1875 秒,所以总时间为 32 * 0.1875 = 6 秒。

  5. 增加分支因子会减少随机性,因为你有更多的分支可以选择最佳分支,但增加温度会增加随机性。两者同时进行会互相抵消,只是我们不知道这种抵消的比例。

  6. 在每个步骤中,算法将生成四个分支并保留两个。在最后一次迭代中,束搜索将通过检查每一层剩余的两个节点与生成图的步骤数(即树的高度)相乘来搜索图中最佳的分支,这个数是三。因此,我们会遍历 2 * 3 个节点 = 6 个节点。

  7. NoteSequence

  8. MIDI 音符映射到以下类别:36 映射到 0(踢鼓),40 映射到 1(军鼓),42 映射到 2(闭合高帽)。计算得到的索引为 2⁰ + 2¹ + 2² = 7,因此得到的向量为 v = [0, 0, 0, 0, 0, 0, 1, 0, ... ]

  9. 索引 131 的位表示是10000011(在 Python 中,你可以使用"{0:b}".format(131)来获取)。这是 2⁰ + 2¹ + 2⁷,得到以下类别:0(鼓组)、1(小军鼓)和 7(撞击钹)。然后,我们任意选择每个类别的第一个元素: {36, 38, 49}

第三章:生成复调旋律

  1. 消失梯度(在每个 RNN 步骤中,值被小值相乘)和梯度爆炸是常见的 RNN 问题,通常发生在反向传播步骤的训练过程中。LSTM 提供了一个专用的细胞状态,通过遗忘门、输入门和输出门进行修改,以缓解这些问题。

  2. 门控 递归 单元GRUs)是更简单但表达力较弱的记忆单元,其中遗忘门和输入门合并为一个单一的更新门

  3. 对于 3/4 拍的节奏标记,你需要每四分之一音符 3 步,每四分之一音符 4 步,总共每小节 12 步。对于一个二进制步数计数器,要计数到 12,你需要 5 个比特位(像 4/4 拍那样),它们只能计数到 12。对于 3 个回顾,你需要查看过去 3 小节,每小节 12 步,所以你得到 [36, 24, 12]

  4. 结果向量是前一步向量的和,每个向量都应用了注意力掩码,因此我们对 [1, 0, 0, 0] 应用了 0.1,对 [0, 1, 0, x] 应用了 0.5,得到了 [0.10, 0.50, 0.00, 0.25]。x 的值是 0.5,因为 0.5 乘以 0.5 等于 0.25。

  5. 一个 C 大调和弦,时值为一个四分音符。

  6. 在 Polyphony RNN 中,没有音符结束事件。如果在一个步骤中没有使用CONTINUED_NOTE来表示某个音高,则该音符会停止。在 Performance RNN 中,则会使用NOTE_END 56事件。

  7. (1) 使用TIME_SHIFT事件表达的时序性,这些事件在所有的 Performance RNN 模型中都有,例如在performance配置中,以及(2) 使用VELOCITY事件的动态播放,这些事件出现在performance_with_dynamics配置中。

  8. 它将在 RNN 步骤调用期间改变迭代次数。每秒钟的音符数量越大,在生成过程中所需的 RNN 步骤就越多。

第四章:使用 MusicVAE 进行潜在空间插值

  1. 主要用途是降维,迫使网络学习重要特征,从而使得重构原始输入成为可能。AE 的缺点是隐藏层表示的潜在空间不是连续的,难以进行采样,因为解码器无法理解某些点。

  2. 重建损失在网络生成与输入不同的输出时进行惩罚。

  3. 在 VAE 中,潜在空间是连续和平滑的,使得可以对空间中的任何点进行采样,并在两点之间进行插值。它是通过让潜在变量遵循 P(z) 的概率分布来实现的,通常是高斯分布。

  4. KL 散度衡量两个概率分布之间的差异。当与重构损失结合时,它会将聚类集中在 0 附近,并使它们更接近或更远离彼此。

  5. 我们使用np.random.randn(4, 512)来采样正态分布。

  6. 计算潜在空间中两点之间的方向。

第五章:使用 NSynth 和 GANSynth 进行音频生成

  1. 你需要处理每秒 16,000 个样本(至少),并在更大的时间尺度上跟踪整体结构。

  2. NSynth 是一个 WaveNet 风格的自编码器,能够学习自己的时间嵌入,使得捕捉长期结构成为可能,并提供访问有用的隐藏空间。

  3. 彩虹图中的颜色代表了时间嵌入的 16 个维度。

  4. 请查看章节代码中audio_utils.py文件中的timestretch方法。

  5. GANSynth 使用上采样卷积,使得整个音频样本的训练和生成处理可以并行进行。

  6. 你需要使用np.random.normal(size=[10, 256])来采样随机正态分布,其中 10 是采样的乐器数量,256 是潜在向量的大小(由latent_vector_size配置给出)。

第六章:训练数据准备

  1. MIDI 不是文本格式,因此更难使用和修改,但它非常常见。MusicXML 相对较少见且笨重,但它的优势在于采用文本格式。ABCNotation 也相对罕见,但其优势在于是文本格式,并且更接近乐谱。

  2. 使用chapter_06_example_08.py中的代码,并在提取过程中更改program=43

  3. LMD 中有 1,116 首摇滚歌曲,3,138 首爵士、蓝调和乡村歌曲。请参考chapter_06_example_02.pychapter_06_example_03.py,了解如何根据音乐风格信息进行统计。

  4. melody_rnn_pipeline_example.py中使用RepeatSequence类。

  5. 使用chapter_06_example_09.py中的代码。是的,我们可以用它训练一个量化模型,因为数据准备管道会量化输入。

  6. 对于小型数据集,数据增强在创造更多数据方面起着至关重要的作用,因为有时你根本没有更多数据。对于较大的数据集,数据增强也起到作用,通过创建更多相关数据和现有数据的变种,帮助网络的训练阶段。

第七章:训练 Magenta 模型

  1. 请参见chapter_07_example_03.py

  2. 欠拟合的网络是指尚未达到最佳状态的网络,这意味着它在评估数据上无法做出良好预测,因为它对训练数据的拟合不佳(目前为止)。可以通过让其训练足够长时间、增加网络容量和更多数据来修正。

  3. 过拟合的网络是指已学会预测输入数据,但无法推广到训练集之外的值的网络。可以通过增加更多数据、减少网络容量,或使用正则化技术(如 dropout)来修正。

  4. 提前停止。

  5. 阅读《关于深度学习大批量训练的问题:泛化差距与锐化极小值》,其中解释了更大的批量大小会导致锐化极小值,从而导致泛化能力较差。因此在效率方面更差,但在训练时间方面可能更好,因为可以同时处理更多数据。

  6. 更大的网络会在预测中更加精确,因此最大化这一点很重要。网络的规模也应随数据的大小(和质量)增长。例如,对于数据而言过大的网络可能会导致过拟合。

  7. 它有助于解决梯度爆炸问题,因为权重将乘以较小的值,限制大梯度的可能性。另一种方法是降低学习率。

  8. 可用于在更强大的机器上启动训练,也可以同时启动多个训练会话。不幸的是,使用云提供商会产生成本,这意味着我们使用的训练时间和功率越多,成本就会越高。

第八章:在浏览器中使用 Magenta.js

  1. 我们可以使用 TensorFlow.js 训练模型,但无法使用 Magenta.js 训练模型。我们需要使用 Python 在 Magenta 中训练模型,然后在 Magenta.js 中导入生成的模型。

  2. Web Audio API 允许在浏览器中使用音频节点进行音频合成、转换和路由。最简单的使用方法是使用像 Tone.js 这样的音频框架。

  3. 方法是 randomSample,参数是生成音符的音高。例如,使用 60 将生成 MIDI 音高为 60 的单音符,或者在字母符号中是 C4。这也是使用 Tone.js 调高或调低音符音高的参考。

  4. 方法是 sample,乐器数量取决于正在使用的模型。在我们的示例中,我们使用了 trio 模型,它生成三种乐器。使用 melody 模型将只生成一个主导乐器。

  5. 因为 JavaScript 是单线程的,如果在 UI 线程中启动长时间的同步计算,将会阻塞其执行。使用 Web Worker 可以在另一个线程中执行代码。

  6. 在浏览器中使用 Web MIDI API,目前支持不够完善,或者在服务器端使用 Magenta.js 进程,这样可以更容易地向其他进程发送 MIDI。

第九章:使 Magenta 与音乐应用程序互动

  1. DAW 将具有更多面向音乐制作的功能,如录制、音频、MIDI 编辑、效果和母带处理以及歌曲作曲。像 FluidSynth 这样的软件合成器功能较少,但优点是轻量且易于使用。

  2. 大多数音乐软件不会自动打开 MIDI 端口,因此要在它们之间发送序列,我们必须手动打开端口。

  3. 参见本章的代码 chapter_09_example_05.py

  4. 因为将两个失去同步的软件重新同步需要重启它们。MIDI 时钟可以在每个节拍时启用同步。

  5. 因为 Magenta Studio 与现有的音乐制作工具(如 DAW)集成,并且不需要任何技术知识,它使得 AI 生成的音乐能够面向更广泛的受众,这也是 Magenta 的最终目标。

  6. Magenta Studio 插件和 Magenta Studio 独立版都基于 Magenta.js,并通过 Electron 打包。Magenta Studio 插件使用 Ableton Live 中的 Max MSP 集成来执行。

posted @ 2025-07-12 11:41  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报