用Transformer生成音乐:把钢琴曲变成事件序列来预测

1 阅读

从图像到序列:换一种思路做音乐生成

之前用MuseGAN生成音乐,是把整段音乐当成一个多维图像来处理。这种方法能做出听起来像样的多音轨作品,但有个问题:它一次性输出所有内容,音符之间的时间关系全靠网络自己学,长一点的曲子就容易前后对不上。

这次换个思路,把音乐当成一串事件来处理。就像GPT预测下一个词一样,我们让模型看着前面发生的所有音乐事件,猜下一个该是什么。这样生成的音乐天然有连贯性,因为每个音符都是基于前面所有内容算出来的。

具体做法是:把MIDI文件拆成四种基本事件——音符什么时候开始、什么时候结束、离上一个事件过了多久、弹得多重。这四种事件组合起来,就能完整描述一段钢琴演奏。2016万个参数的Transformer模型,足够记住一首曲子里远距离的音符关系,比如开头主题和结尾再现的呼应,训练时间也还在可接受范围内。

训练数据用了Maestro数据集,都是真实的钢琴演奏录音转成的MIDI。后面会讲到怎么把这些文件变成模型能吃的数字,以及怎么让生成的曲子既有训练数据的风格,又不至于和原曲一模一样。

音乐为什么要这样表示

MIDI文件本身不是给神经网络准备的。它存的是「第480个时钟点,开C4键,力度67」这种指令,直接喂给模型会出问题:不同曲子的速度不一样,有的每拍480个时钟点,有的是960个;力度值范围是0到127,但不同演奏者实际用的区间差别很大。

更麻烦的是,MIDI把时间和音符分开记录。同一个和弦的五个音,可能在文件里分散在好几行,时间戳还略有不同。模型如果直接读这个,很难理解这五个音其实是同时响的。

所以需要做一层转换,把MIDI变成统一的事件序列。核心想法来自2018年Google的Music Transformer论文:不管原曲什么速度、什么拍号,最后都表达成四种原子事件:

  • Note On:某个音开始响,带音高和力度
  • Note Off:某个音结束
  • Time Shift:距离上个事件过了多长时间
  • Set Velocity:改变之后的力度设置

Time Shift把时间切成固定的小格子,比如10毫秒一格。原曲是快是慢都不重要了,模型看到的只是「过了3格」「过了5格」这种相对时间。力度也做成分档的,比如分成32档,而不是直接用0-127的原始值。

这样一首曲子就变成几百到几千个事件的序列。肖邦的《夜曲》大概3000个事件,一首流行歌可能5000个往上。序列长度和一篇短文差不多,正好适合Transformer处理。

分词:从MIDI到数字

事件序列还是文字,得变成数字才能进网络。这一步和NLP里的分词很像,但音乐有些特殊之处。

首先是词表大小。Note On有128个音高×32档力度=4096种可能,Note Off有128种,Time Shift假设最多支持1秒那就是100种(10毫秒一格),Set Velocity32种。加起来词表大概4300,比英文的GPT小多了,但比中文BERT还是大些。

实际实现时有个优化:很多Note On组合其实很少出现。比如力度0的开键事件几乎没有,极高音区也极少用到。可以只保留训练数据中出现过的组合,把词表压到2000左右,省参数也省计算。

分词流程是:读MIDI→按时间排序所有消息→合并同时发生的Note On→生成事件序列→查表换成索引。反过来,生成时模型输出索引,查表得到事件,再合成回MIDI文件就能听了。

这里有个细节:和弦怎么处理。如果严格按时间戳,同时按下的五个音会有五个Note On,中间夹着零长度的Time Shift。Music Transformer的做法是强制排序:低音在前高音在后,零长度Time Shift直接省略。这样同样的和弦总是变成同样的序列,模型更容易学。

模型架构:为什么用仅解码器

音乐生成是个自回归问题:已知t时刻之前的事件,预测t时刻的事件。这和GPT做语言建模一模一样,所以架构也直接照搬:堆叠的Transformer解码器层,带因果掩码的自注意力,每个位置只能看到自己和前面的位置。

具体配置:6层解码器,8头注意力,嵌入维度512,前馈层2048维。这个规模比GPT-2小得多,但音乐序列也比自然语言短,而且事件之间的依赖关系相对局部——人耳对10秒前的和声还有印象,但很少能记住1分钟前的具体音符。所以6层足够捕捉音乐结构。

位置编码用正弦余弦的,不是可学习的。因为音乐里时间有绝对意义,第100个事件确实发生在第50个之后,这种相对关系正弦编码能很好表达。

输出层是标准的分类:每个位置输出词表大小的logits,softmax变成概率分布,采样得到下一个事件。训练时和语言模型一样,每个位置都算交叉熵,只是mask掉未来位置。

温度参数:控制随机性

模型训练好后,生成音乐需要采样。最简单的做法是每一步取概率最大的事件,但这样出来的曲子很死板,几乎是训练数据的平均,没有新意。

更好的办法是用温度参数T控制softmax的陡峭程度。logits除以T再softmax:T=1就是正常分布;T<1分布变尖,采样更确定,结果更接近训练数据;T>1分布变平,采样更随机,可能出现训练数据里没见过的组合。

实际生成时通常T=0.8到1.2之间。太低了像机器人在弹音阶,太高了像随机敲键盘。也可以让T随位置变化:开头用低T保证主题清晰,中间用高T增加变化,结尾再用低T收束。

还有个技巧叫top-k或nucleus采样,限制每一步只从概率最高的k个或累积概率p以内的选项里选。这能避免模型选中概率极低、听起来很怪的边缘事件。

训练细节和常见问题

训练数据和NLP不同,没有现成的「句子」边界。一首曲子几千个事件,直接塞进去太长,需要切成固定长度的片段。比如选512个事件一段,步长256,这样相邻片段有一半重叠。这种切法会破坏少数跨越边界的音乐结构,但大规模数据下影响有限。

数据增强方面,音乐比图像难做。可以transpose,把所有音高上下移几度,相当于换调,但和声色彩会变。也可以stretch时间,把整首曲子放慢或加快,但力度曲线也要相应调整。这些增强不是必须的,Maestro本身有200小时数据,对2016万参数的模型已经够用了。

训练时的一个坑是事件分布极不平衡。Time Shift和Set Velocity出现次数远少于Note On/Off,模型容易学会一直预测音符事件,忽略时间信息。解决办法是在损失函数里给不同类别加权重,或者直接在词表层面做平衡采样。

另一个问题是长程一致性。Transformer理论上能看整个序列,但音乐里的重复结构(A段B段再现)距离可能上千个事件,6层注意力不一定抓得住。Music Transformer用了一种相对位置编码的变体,让模型更容易学习「隔了多久」而不是「绝对位置」,对重复主题有帮助。简单实现也可以加一层循环连接,或者让模型多步预测而不仅下一步。

生成结果和局限

训练好的模型能生成听起来像钢琴独奏的MIDI。节奏基本稳定,不会有人类演奏者的rubato(弹性速度),但也不会机械到像节拍器。和声进行合理,常见套路如I-V-vi-IV能自然出现,偶尔有惊喜转调。

明显的问题是缺乏全局结构。模型能写出一个不错的乐句,但整首曲子没有「开头-发展-高潮-结尾」的弧线,更像是无限延续的即兴。这是自回归模型的通病:每一步只看过去,没有全局规划。Music Transformer论文里加了一个额外的「段落」事件来标记结构,但实现起来更复杂。

另一个局限是音色单一。Maestro只有钢琴,模型没听过小提琴或鼓。要生成多乐器音乐,需要更复杂的事件类型,或者像MuseGAN那样分轨处理。

从MIDI到可听的音频

模型输出的是事件序列,要变成声音还需要一步。MIDI本身不是音频,是演奏指令。可以用软件合成器渲染,比如FluidSynth加载一个钢琴音源,把MIDI转成WAV。也可以用神经网络声码器,比如把MIDI先变成钢琴卷(piano roll,类似频谱图的二维表示),再用WaveNet或HiFi-GAN合成波形。

后者的好处是音色可控,换不同的声码器就能变成不同钢琴甚至其他乐器。坏处是多了个训练环节,而且MIDI到钢琴卷的转换会丢失一些演奏细节,比如踏板信息。

最简单的方案是直接在网上找MIDI播放器,把生成的文件拖进去听。虽然音质取决于播放器用的音源,但足够判断生成质量了。

和其他方法的对比

除了MuseGAN和这种序列模型,还有几种音乐生成思路值得了解。

RNN/LSTM:Transformer之前的主流。能处理序列,但长程依赖靠隐状态传递,容易丢信息。现在基本被Transformer取代,除非内存极度受限。

扩散模型:图像生成那边很火,音乐也有人尝试。把音频谱图当成图像去噪,或者直接在波形上做。优势是能全局规划,生成的结构更完整;劣势是计算量大,实时生成困难。

符号音乐+神经渲染:先用符号模型(比如本文的Transformer)生成MIDI,再用神经网络把MIDI变成真实音频。结合了两边的优点:符号层面好控制,音频层面音质好。Google的MT3和后来的MusicLM都走这个路线。

本文的方法属于最基础的符号生成,适合理解核心概念。实际产品里通常要叠很多层:先规划整体结构,再生成和弦进行,再填充旋律,最后渲染音色。每层可能用不同的模型,或者一个大的多任务模型。

代码实现的关键点

如果自己动手实现,有几个地方容易踩坑。

MIDI解析:用mido或pretty_midi库读文件,注意处理tempo变化。有些曲子中间会变速度,不能直接按固定比例算时间。

事件编码:建议用整数编码而不是字符串,省内存也省查表时间。词表提前算好,出现次数少于阈值的事件合并成UNK。

数据加载:音乐序列长度差异大,有的曲子30秒,有的10分钟。用变长batch需要padding和attention mask,或者干脆只采样固定长度的片段。

生成时的效率:自回归生成是串行的,512个事件要调模型512次。可以用KV cache避免重复计算,或者训练一个非自回归的蒸馏模型来加速。

评估:没有完美的自动指标。BLEU、ROUGE这些NLP指标搬过来不太合适,因为音乐没有唯一的「正确答案」。常用的是让人类听,或者算和训练数据的n-gram重叠度(越低越新颖,但太低可能难听)。

延伸方向

这个基础框架可以往很多方向扩展。

多轨生成:把事件类型扩展到多个乐器,或者每个轨道单独一个模型,再加一个指挥模型协调各轨。

条件生成:给定风格标签、情绪标签,或者一段哼唱旋律,生成配套的伴奏。做法是在输入序列前面加上条件token,或者把条件编码成向量加到每个位置。

交互生成:不是一次性生成整首,而是实时响应用户的输入。比如用户弹几个音,模型接着往下发展。需要把模型做得足够小,能在本地跑,延迟控制在100毫秒以内。

和声分析结合:先训练一个和弦识别模型,生成时强制和弦进行符合某种规则,比如爵士的ii-V-I循环。这比纯学习更可控,也更容易解释。

音乐生成是个还没被完全解决的问题。现在的模型能骗过 casual listener,但专业音乐人一听就知道没有灵魂。核心难点不是技术,而是「音乐性」本身难以定义——节奏、和声、旋律都好量化,但为什么有的组合打动人,有的就是噪音,这还没法教给机器。也许需要结合认知科学的研究,或者让模型从人类的生理反应(心跳、皮肤电)里学习,才能真正做出有感染力的音乐。