丹尼尔·D·约翰逊


用递归神经网络作曲

(更新:2017年EvoMusArt大会接受了基于此工作的论文!参见在这里了解更多详细信息。)

如今,很难不被神经网络惊人的威力所震撼。经过足够的训练,具有许多节点和隐藏层的所谓“深度神经网络”可以在建模和预测各种数据方面取得令人印象深刻的成绩。(如果你不知道我在说什么,我建议你阅读递归字符级语言模型谷歌深度梦想、和神经图灵机非常酷的东西!)现在似乎和以往一样适合实验神经网络的功能。

有一段时间,我一直在思考写一个程序来作曲的模糊想法。我最初的想法是基于时间的分形分解和某种重复机制,但在阅读了更多有关神经网络的内容后,我认为它们更适合。所以几周前,我开始设计我的人际网络。经过一段时间的训练,我很高兴能报告我取得了巨大的成功!

以下是将要发生的事情:

但首先,是关于神经网络,特别是RNN的一些背景知识。(如果您已经了解了所有关于神经网络的知识,请跳过这一部分!)

前馈神经网络:

简单神经网络中的一个节点接收一些输入,然后对这些输入进行加权求和,将每个输入乘以一些权重,然后将所有输入相加。然后,添加一些常数(称为“偏差”),然后使用非线性激活函数(例如sigmoid函数)将总和压缩成一个范围(通常为-1到1或0到1)。

乙状结肠功能。

我们可以通过将其输入和单个输出绘制为箭头,并用圆表示加权和和激活,来可视化此节点:

然后,我们可以获取多个节点并向它们提供相同的输入,但允许它们具有不同的权重和偏差。这称为层。

(注意:因为层中的每个节点都执行加权求和,但它们共享相同的输入,所以我们可以使用矩阵乘法计算输出,然后进行元素激活!这就是为什么神经网络可以如此有效地训练的原因之一。)

然后我们可以将多个层连接在一起:

瞧,我们有一个神经网络。(术语简要说明:输入集称为“输入层”,最后一层节点称为“输出层”,所有中间节点层称为“隐藏层”此外,如果不清楚,每个节点的所有箭头都具有相同的值,因为每个节点都有一个输出值。)

为了简单起见,我们可以将层可视化为单个对象,因为大多数情况下都是这样实现的:

从这一点开始,当您看到一个圆圈时,它表示整个网络层,箭头表示值的矢量。

循环神经网络

请注意,在基本前馈网络中,信息流动的方向是单一的:从输入到输出。但在递归神经网络中,此方向约束不存在。有很多可能的网络可以归类为循环网络,但我们将重点关注其中一个最简单、最实用的网络。

基本上,我们可以做的是获取每个隐藏层的输出,并将其作为附加输入反馈给自身。隐藏层的每个节点在最后一个时间步长中接收来自前一层的输入列表和当前层的输出列表。(因此,如果输入层有5个值,而隐藏层有3个节点,则每个隐藏节点作为输入接收的总值为5+3=8。)

通过沿时间轴展开网络,我们可以更清楚地显示这一点:

在这种表示法中,每一层的水平线都是在单个时间步长上运行的网络。每个隐藏层都接收来自前一层的输入和来自其自身的输入,这是过去的一个时间点。

这一功能的强大之处在于,它使网络能够拥有简单版本的内存,并且开销非常小。这就提供了可变长度输入和输出的可能性:我们可以一次输入一个输入,并让网络使用每个时间点传递的状态来组合它们。

这方面的一个问题是记忆非常短暂。在一个时间步长中输出的任何值都将成为下一个时间步的输入,但除非再次输出相同的值,否则它将在下一个刻度处丢失。为了解决这个问题,我们可以使用长短期内存(LSTM)节点而不是普通节点。这引入了一个“内存单元”值,该值被传递给多个时间步长,并且可以在每个刻度处进行加减。(我不打算深入讨论所有细节,但您可以在原始纸张.)

我们可以想象存储单元数据与激活输出并行发送。上面,我用蓝色箭头显示了它,为了简单起见,我将在下面的图表中省略它。

训练神经网络

所有这些漂亮的图片都很好,但我们如何真正让网络输出我们想要的?好吧,神经网络的行为是由每个节点的权重和偏差集决定的,所以我们需要将它们调整到正确的值。

首先,我们需要定义给定输入的任何给定输出的好坏。此值称为成本例如,如果我们试图使用神经网络对数学函数建模,则成本可能是函数答案与网络输出之间的差值平方。或者,如果我们试图对字母按特定顺序出现的可能性进行建模,则成本可能是每次预测正确字母的概率减去一。

一旦我们有了这个成本价值,我们就可以使用反向传播这归结为计算成本相对于权重的梯度(即成本相对于每个层中每个节点的每个权重的导数),然后使用一些优化方法调整权重以降低成本。坏消息是,这些优化方法往往非常复杂。但好消息是,其中许多已经在库中实现了,所以我们可以将梯度输入到正确的函数中,并让它正确地调整权重。(如果你好奇,不介意一些数学问题,一些优化方法包括随机梯度下降无黑森优化阿达格拉德、和阿达德尔塔.)

一些常用优化方法的可视化(来源).

他们能创作音乐吗?

好吧,背景足够了。从现在开始,我将主要谈论我自己的思维过程和网络架构的设计。

当我开始设计我的网络架构时,我很自然地了解了其他人是如何处理这个问题的。以下收集了一些现有方法:

  • Bob Sturm使用基于特征的模型使用LSTM生成歌曲的文本表示(在abc符号). 网络似乎一次只能播放一个音符,但可以实现有趣的时间模式。
  • Doug Eck,印第安纳州LSTM递归神经网络在音乐创作中的应用,使用LSTM进行布鲁斯改进。选定的序列都有相同的和弦集,网络对每个音符都有一个输出节点,输出每个时间步长播放该音符的概率。结果很有希望,因为它可以学习时间结构,但输出的内容非常有限。此外,播放音符和持有音符没有区别,因此网络无法重新表述持有的音符。
  • Nicolas Boulanger-Lewandowski,印第安纳州高维序列中的时间依赖建模:在复调音乐生成和转录中的应用,使用由两部分组成的网络。有一个RNN用于处理时间相关性,它生成一组输出,然后用作受限玻尔兹曼机,它反过来模拟了哪些音符应该与哪些其他音符一起演奏的条件分布。这种模式实际上产生了相当悦耳的音乐,但似乎没有真正的时间感,只演奏了几个和弦。(请参见在这里用于算法和样本输出。)

对于我的网络设计,我希望它具有以下几个属性:

  • 对时间签名有一些了解:我想给神经网络一个参考时间签名的当前时间,因为大多数音乐都是用固定的时间签名创作的。
  • 具有时间不变性:我希望网络能够无限期地组合,因此每个时间步长都必须相同。
  • 不要(大多)变化:音乐可以自由地上下变换,并且基本上保持不变。因此,我希望每个音符的神经网络结构几乎相同。
  • 允许同时演奏多个音符,并允许选择连贯的和弦。
  • 允许重复同样的音符:两次弹奏C应该不同于两次按住单个C。

我将更多地讨论不变性,因为我认为这些是最重要的。现有的大多数基于RNN的音乐创作方法在时间上是不变的,因为每个时间步长都是网络的单个迭代。但它们在音符上通常不是不变的。通常有一些特定的输出节点表示每个音符。因此,通过一个完整的步骤将所有内容进行转换,将产生完全不同的输出。对于大多数序列,这是您想要的:“hello”与“ifmmp”完全不同,后者只是“转置”了一个字母。但对于音乐,你想强调绝对位置上的相对关系:C大调和弦听起来更像D大调和声,而不是C小调和弦,尽管C小调和绝对音符位置更接近。

目前广泛使用的一种神经网络具有沿多个方向的不变性:用于图像识别的卷积神经网络。这些基本上是通过学习卷积核,然后在输入图像的每个像素上应用相同的卷积核来实现的。

卷积是如何工作的。每个像素被周围像素的加权和所代替。神经网络必须学习权重。图片来自developer.apple.com开发人员.

假设,如果我们用其他东西替换卷积内核会发生什么?比如说,一个递归神经网络?然后每个像素都会有自己的神经网络,它会从像素周围的区域获取输入。反过来,每个神经网络都有自己的记忆细胞和跨时间的循环连接。

现在用音符替换像素,我们有了一个想法。如果我们制作一组相同的递归神经网络,每个输出音符对应一个神经网络,并在音符周围为每个神经网络提供一个局部邻域(例如,上下一个倍频程)作为输入,然后我们有一个在时间和注释上都不变的系统:网络可以在两个方向上使用相对输入。

注意:我在这里旋转了时间轴!请注意,现在时间点从页面中出来,重复连接也是如此。您可以将每个“扁平”切片视为上面基本RNN图片的副本。此外,我还展示了从上面和下面的一个注释中获取输入的每一层。这是一个简化:真正的网络从每个方向的12个音符(八度音阶的半步数)获得输入。

然而,这个网络仍然存在一个问题。循环连接允许时间模式,但我们没有机制来获得好的和弦:每个音符的输出完全独立于每个其他音符的输入。在这里,我们可以从上面的RNN-RBM组合中获得灵感:让我们的网络的第一部分处理时间,让第二部分创造优美的和弦。但RBM给出了一组输出的单一条件分布,这与每个音符使用一个网络是不兼容的。

我决定采用的解决方案是我所称的“双轴RNN”。这个想法是我们有两个轴(和一个伪轴):有时间轴和音符轴(以及方向-频率计算伪轴)。每个递归层将输入转换为输出,并沿其中一个轴发送递归连接。但没有理由让它们都必须沿着同一轴发送连接!

请注意,前两个层跨时间步长有连接,但跨注释是独立的。另一方面,最后两层在音符之间有联系,但在时间步长之间是独立的。总之,这允许我们在不牺牲不变性的情况下在时间和笔记空间中都有模式!

如果我们折叠其中一个维度,就更容易看出来:

现在时间连接显示为循环。重要的是要记住,循环总是延迟一个时间步长:time的输出t吨是输入的一部分t吨+1.

输入和输出详细信息

我的网络是基于这种架构思想的,但当然实际实现要复杂一些。首先,我们在每个时间步都有第一个时间轴层的输入:(括号中的数字是输入向量中对应于每个部分的元素数)

  • 职位[1] :当前音符的MIDI音符值。用于模糊地了解给定音符的高低,以考虑差异(例如,下音符通常是和弦,上音符通常为旋律)。
  • 沥青等级[12] :当前音符位置将为1,从A开始表示0,每半步增加1,其他所有音符为0。用于选择更常见的和弦(即C大调和弦比降e大调和键更常见)
  • 上一次邻近[50]:给出最后一个时间步长中周围音符的上下文,每个方向一个八度音。如果上次在timestep中播放了距当前音符偏移量i处的音符,则索引2(i+12)处的值为1,否则为0。如果注释为铰接的最后一个时间步,如果不是,则为0。(所以,如果你演奏一个音符并按住它,第一个时间步长两个都有1,第二个时间步长只在第一个。如果你重复一个音符,第二个时间步长两个都有1。)
  • 上一个上下文[12] :索引i处的值将是任何音符x的次数,其中(x-i-pitchclass)mod 12在最后一个时间步中播放。因此,如果当前音符是C,并且有2个E的最后一个时间步长,则索引4处的值(因为E是C上的4个半步)将是2。
  • 节拍[4] :基本上是度量值内位置的二进制表示,假设为4/4倍。由于每一行是节拍输入之一,每一列是时间步长,它基本上只重复以下模式:
    0101010101010101001100110011001100001111000011110000000011111111
    但是,它被缩放为[-1,1],而不是[0,1]。

然后是第一个隐藏的LSTM堆栈,它由沿时间轴具有循环连接的LSTM组成。最后一个时间轴层输出一些表示任何时间模式的注释状态。第二个LSTM堆栈沿音符轴循环,然后从低音符向上扫描到高音符。在每个note-step(相当于时间步长),它都会作为输入

  • 上一个LSTM堆栈中对应的note-state向量
  • 一个值(0或1),表示是否选择播放上一个(下半步)音符(基于上一个音符-步骤,从0开始)
  • 一个值(0或1),用于表示是否选择了前一个(下半步)音符进行铰接(基于前一个音符-步骤,从0开始)
在最后一个LSTM之后,有一个简单的非重复输出层,它输出2个值:
  • 游戏概率,这是选择播放此音符的概率
  • 清晰表达概率,这是音符在演奏时被清晰表达的概率。(这仅用于确定持有纸币的重新发音。)
该模型在中实现西雅娜这是一个Python库,通过将网络编译为GPU优化代码并自动计算梯度,可以轻松生成快速神经网络。错误消息可能有点令人困惑(因为在运行生成的代码时会抛出异常,而不是您的代码),但这是值得的。

使用模型

在训练期间,我们可以随机输入一批短音乐片段。然后,我们取所有输出概率,并计算交叉熵,这是一种奇特的说法,即在给定输出概率的情况下,我们可以找到生成正确输出的可能性。在使用对数进行一些操作后,使概率不至于小得离谱,然后对其进行否定,使其成为最小化问题,我们将其作为成本插入AdaDelta优化器,让它优化我们的权重。

我们可以利用这样一个事实来加快训练,即我们已经确切地知道在每个时间步长将选择哪种输出。基本上,我们可以首先将所有笔记一起批处理并训练时间轴层,然后我们可以重新排序输出,将所有时间一起批处理,并训练所有笔记轴层。这使我们能够更有效地利用GPU,它擅长于将巨大的矩阵相乘。

为了防止我们的模型过度(这意味着学习特定部分的特定部分,而不是整体模式和功能),我们可以使用一种称为辍学应用丢弃本质上意味着在每个训练步骤期间从每个层随机移除一半的隐藏节点。这可以防止节点相互之间被吸引到脆弱的依赖关系中,从而促进专门化。(我们可以通过将掩码与每个层的输出相乘来实现这一点。节点通过在给定的时间步长中将其输出归零来“删除”。)

不幸的是,在构图过程中,我们无法有效地对所有内容进行批处理。在每个时间步,我们必须首先按一个刻度运行时间轴层,然后运行注释轴层的整个循环序列,以确定在下一个刻度处为时间轴层提供什么输入。这会使合成速度变慢。此外,我们必须添加一个修正系数,以解释训练期间的辍学。实际上,这意味着将每个节点的输出乘以0.5。这样可以防止网络因活动节点数较多而变得过励磁。

我用g2.2x码训练模型亚马逊网络服务实例。我能够通过使用“现货实例”来节省资金,这是一种廉价、短暂的实例,可以被亚马逊关闭,并且是根据供需情况定价的。我的价格在每小时0.10美元到0.15美元之间波动,而专用按需实例的价格在0.70美元之间波动。我的模型使用了两个隐藏的时间轴层,每个层有300个节点,以及两个注释轴层,分别有100和50个节点。我使用上所有midi文件的转储来训练它古典钢琴Midi页面,一次分为10个随机选择的8度量块。

关于使用spot实例的警告:准备好系统在没有警告的情况下关闭!(在几天的实验和训练中,我两次遇到这种情况。)确保在训练期间经常保存,这样就不会丢失太多数据。此外,请确保取消选中“终止时删除”,以便Amazon在关闭时不会擦除您的实例内存!

结果

不言而喻,下面是一些网络输出的选择:
有时,它似乎会在很长一段时间内弹奏同一和弦。它似乎还没有学会把这些东西拿多久。但总的来说,输出非常有趣。(我很想删除一些重复的部分,但我认为这对项目来说是不忠的,所以我把它们放在了里面。)
所有内容的源代码都可以在github。它不是超级抛光的,但应该可以弄清楚发生了什么。
您可能还对以下方面的讨论感兴趣黑客新闻重新编辑.
如果您想自己复制结果,您可能会对最后训练重量我的网络。

Creative Commons许可证上述生成的音频文件是根据Creative Commons Attribution 4.0国际许可.