基于LSTM的MIDI音乐生成:从数据清洗到采样实践
2026/9/9 13:33:54 网站建设 项目流程

简介:midiGenerator是一份面向音乐生成与深度学习的开源实现,源自“具有深度概率模型的音乐完成”硕士论文配套代码,适合想在MIDI音乐自动作曲方向作实验的研究者或开发者。项目使用TensorFlow/Keras搭建深度神经网络,并借助music21与pygame等库完成音符解析、乐谱处理与音乐回放,完整覆盖了从MIDI数据集预处理、贝叶斯优化超参数、训练模型到生成新音乐文件的流程。资源包大小约1.02MB,文件以Python脚本为主,包含bayesian-opt.py、compute_data.py、train.py、generate.py等模块,分别对应超参数搜索、数据转换、模型训练与音乐生成,结构精简、模块边界清晰,可独立运行也可串联执行。目前已有186人学习下载,对希望快速上手音乐生成实验的读者,这套代码提供了可运行的完整骨架,有助于理解“数据准备-训练-采样生成”的技术链路,并可在小规模MIDI数据集上直接复现。

1. 项目概述:让机器学会“作曲”这件事

做AI音乐生成这个midiGenerator项目,其实源于一个很朴素的想法:既然深度神经网络能从海量文本里学会写诗,从千万张图片里学会画画,那它能不能从一堆MIDI文件中学会谱曲?这个念头在我脑子里转了挺久,终于在某天晚上动手开写。先说结论:能,而且效果超出我的预期,生成的旋律虽然谈不上大师级,但已经能骗过不少朋友“这怕不是哪部游戏的原声吧”。

这个项目本质上做的事情很简单:收集一批MIDI格式的音乐文件,把音符转成神经网络能理解的时间序列数据,训练一个模型去学习音符之间的衔接规律,最后从一个随机起点开始让模型一个音一个音地“续写”下去,输出一段全新的MIDI。听起来像是魔法,背后的核心其实就三个环节:数据清洗、序列建模、采样生成。

很多朋友问我,为什么非要用MIDI而不是直接上音频波形?这个问题我会在下面详细展开,但先给个直观答案:MIDI记录的本来就不是声音本身,而是“演奏指令”——这个音开头多高、响多久、音量多大。这种结构化特性跟深度学习天然契合,相当于你不需要让模型先学会分辨音高,它一上手面对的就是人类已经抽象好的音乐符号系统。而深度神经网络要学的,就是这些符号背后隐藏的规律和风格特征。

整个项目下来,我用的是三层LSTM加一层注意力机制,训练数据是各类钢琴曲的MIDI文件,数据量不算大,一万多首曲子,在单张消费级显卡上跑了不到十个小时。这个项目适合谁看?如果你对AI音乐生成感兴趣,或者想入门序列生成类任务——不管是写歌、写代码还是写文本——这个项目的思路和方法都能直接借鉴。即使你是刚接触深度学习的新手,只要会一点Python基础,照着这篇文章的思路也能把这个项目跑通。

2. 方案选型:为什么是深度神经网络,为什么是MIDI

2.1 传统算法与深度学习的差距

在做这个项目之前,我最早尝试的是用马尔可夫链来生成旋律。思路是把每个音符当作一个状态,统计曲库里“音A后面跟着音B”的概率,然后按概率随机走。这个方法跑起来快、代码简单,但生成的旋律很快就陷入循环,翻来覆去就是几个音的组合,毫无新意。原因也很好理解:音乐中的模式远不止“上一个音决定下一个音”这么简单,一句旋律的记忆可能要回溯到十几个音符之前,甚至还要考虑节奏型、和声走向这种全局结构。马尔可夫链的“只看眼前一步”设定了它不可能捕捉到这种长程依赖。

深度神经网络的强项恰恰就在这里。以我最终选用的LSTM(长短期记忆网络)为例,它内部有一套门控机制,可以决定哪些信息该记住、哪些该忘掉,从而在几十甚至几百个时间步的跨度上保留有效信息。换成人话就是:模型在决定下一个音怎么写的时候,不只看前一个音,它会参考前面一大段的旋律走向和节奏律动。这种能力对于音乐生成来说是决定性的。

2.2 MIDI数据的结构性优势

那为什么输入选择MIDI而不是直接把MP3或者WAV喂给网络?我在最初做技术预研的时候还真尝试过用频谱图做输入,结果有几个绕不开的坎。一是音频的采样率太高,一秒几万个采样点,一首三分钟的歌曲数据量动辄上千万,对显存和算力都是巨大考验,个人开发者基本跑不动;二是音频里包含了太多与“旋律”无关的信息,比如音色、混响、环境噪音,模型需要花大量精力去分辨哪些是本质、哪些是干扰。

MIDI文件完美避开了这两个问题。它记录的是离散的“音符事件”,一首钢琴曲的MIDI文件通常只有几十KB,里面就是一条条“第几号音符在什么时间开始、持续多久、力度多大”的信息。这样数据量小了至少三个数量级,而且信息高度纯净——你给模型的就是音乐最核心的骨架。跟我后来跑过的音频方案对比,相同的数据规模下,用MIDI的训练速度能快一个量级不止。

另外还有个容易被忽略的点:MIDI天然适合做符号级别的生成评估。你可以把生成的MIDI转成五线谱,用人眼直观判断旋律是否流畅、节奏是否自然,甚至可以直接在DAW里换各种音色试听。这种可解释性是黑盒般的音频模型给不了的。

2.3 模型架构的取舍

确定了用深度神经网络和MIDI数据之后,下一步是选具体架构。当时主流选项有三个:RNN系(LSTM/GRU)、Transformer、以及卷积系列。我做了一个简单的对比:

维度LSTM(选定方案)TransformerCNN
长程依赖能力强,但随序列变长有衰减极强,全局注意力机制弱,受限于感受野
训练效率中等,必须按时间步串行高,可并行计算高,天然适合并行
序列长度限制无硬性限制,但长了会忘有最大长度限制,超了会崩需要固定窗口
部署难度低,推理时状态只需一个向量中,KV缓存机制较复杂
对小数据适应性好,参数少,不容易过拟合差,动辄上亿参数较好

从表格里能看到,Transformer在长程建模上确实最强,但它是个“数据饕餮”,没有大规模数据喂不饱。我手头只有一万多首曲子,硬上Transformer很容易过拟合,生成出来的东西翻来覆去就那几句。LSTM虽然“记忆力”不如Transformer,但胜在参数少、在小数据集上更容易学到泛化规律。最终我选了“两层LSTM编码+一层注意力”的混合架构,兼顾了序列建模能力和对数据量的友好度。

3. 数据准备:从MIDI文件到训练样本

3.1 数据来源与清洗

训练数据我用的是从几个公开MIDI曲库收集的钢琴独奏文件,类型涵盖古典、新世纪、游戏配乐等。这里要提醒一句:数据质量比数据量更重要。我第一版训练用的数据没怎么清洗,里面有大量MIDI文件其实是电子乐编曲工程,一首曲子十几个音轨混在一起,解析出来的音符乱七八糟,训练出来的模型生成的东西也是东一榔头西一棒子。

清洗的标准最终定成三条:第一,必须是钢琴音轨或者单乐器MIDI,多音轨的只提取钢琴轨;第二,去除速度标记、弯音轮、控制变化等对旋律建模没有直接意义的MIDI事件,只保留“音符开”“音符关”两类核心信息;第三,删掉时长过短(少于30秒)和过长(超过10分钟)的文件,前者信息量不足,后者往往结构松散。

3.2 序列化编码

MIDI文件本身是事件列表,但LSTM需要吃定长的数字序列,所以要设计一个映射方案。我用的是“基于时间的音符序列”编码,简单说就是设定一个时间步长(我用的16分音符),把每个时间步上的演奏状态压缩成一个token。对于钢琴曲,大部分时间其实是单音或双音,所以我把每个时间步的事件分成三类:

  • NOTE_ON:表示这个时间步有新的音符开始,后面跟音符编号(如60代表中央C)
  • NOTE_OFF:表示某个正在响的音符结束
  • REST:这个时间步没有任何音符事件

实际编码时,我把这三种事件映射成独立的整数ID,音符编号的映射区间和事件类型区间错开。用一个例子来说明:一段旋律如果是在第1拍弹了中央C持续两拍,然后空一拍,再弹E和G两个音,那么编码出来的序列就是:[NOTE_ON, 60, NOTE_OFF, REST, NOTE_ON, 64, 76]。这样网络要学习的就变成了“给定前面一串ID,预测下一个ID是什么”,本质上跟语言模型预测下一个词的逻辑完全一致。

3.3 训练样本的切分

编码完成后,每首曲子变成了一长串整数序列,几千到几万个token不等。训练时不能整首塞进网络,因为LSTM在超长序列上会梯度消失,而且显存也扛不住。我的做法是用一个滑动窗口切样本,窗口长度设为512个token,步长设为128。也就是说每首曲子能切出多个长度为512的子序列,子序列之间有重叠,这样既扩充了训练样本数量,又保证了样本之间有连续性。

这里有个小技巧:切样本的时候不要用固定的时间长度去切,因为MIDI里音符密度不均,同样五分钟的曲子,有的地方音符密集,有的地方全是休止符。固定token数量切,能保证每个样本包含足够的信息量。我吃了这个亏才改过来的,最早按小节数切,出来的样本有的塞满了音符、有的基本是空白,训练效果很差。

4. 模型构建与训练实现

4.1 网络结构与核心参数

模型结构其实不复杂,我用PyTorch实现,核心就是嵌入层加两层LSTM加注意力加输出层。嵌入层的作用是把每个token ID映射成一个128维的稠密向量,相当于给每个音符事件学一个“语义表示”出来。这里多说一句:嵌入层不是可有可无的,如果直接用one-hot向量喂给LSTM,模型很难学到“C和C#比C和G更接近”这种音程关系,而嵌入表示天然可以捕捉这种相似性。

LSTM的隐藏层维度设为256,两层堆叠。第二层LSTM上面加了一个简化版的注意力层:对第二层LSTM每个时间步的隐状态做加权平均,权重由当前隐状态和前面所有状态的相关性决定。这个设计是为了缓解长序列下LSTM“记性不足”的问题,让模型在生成时可以更主动地去“回看”关键的旋律片段。输出层就是一个全连接层加softmax,词汇表大小是事件类型数量加音符编号范围,大概是不到200。

4.2 训练过程与调参经验

损失函数用的是交叉熵,优化器选了Adam,学习率初始值0.001,配合余弦退火调度,每两轮学习率衰减一次。Batch size是64。训练了大概120个epoch(大约9小时,RTX 3060显卡),损失从最开始的5.8一直降到1.3左右。

这里说一个我踩过的深坑:不要一口气把序列长度拉到512开始训练。我第一版直接上512,结果训练了十几个epoch损失纹丝不动。后面排查发现是梯度消失太严重——虽然LSTM的门控机制能在一定程度上缓解这个问题,但模型初始化后面对512长度的序列,反向传播信号早就衰减没了。解法是“课程学习”:先让模型在64长度的短序列上训练,拿到能收敛的初始权重之后,再逐步拉长到128、256、512。这个策略极其有效,建议做序列生成的直接照抄。

训练时的监控指标除了loss,我还会记录“top-5准确率”——就是下一个音符的判断是否落在概率最高的前5个候选中。这个指标比loss更直观,loss在数值上一直降,但你可能看不出生成效果有什么本质变化,而top-5准确率从最初的不足20%涨到最后的接近60%时,生成的旋律连贯性就能明显感知到了。

4.3 生成阶段的采样策略

训练完的模型本身只负责打分——给定前文,输出下一个token的概率分布。真正决定生成质量的是“怎么在这个概率分布上取答案”。最简单的做法是每一步都取概率最高的token,也就是贪心搜索,但这样生成出来的旋律非常乏味,而且很容易在几个音符之间无限循环。原因是真实音乐的分布是有峰值的,贪心会把多样性扼杀掉。

我最终用的是温度采样(Temperature Sampling),核心公式是:将logits除以温度系数T后再做softmax,T大于1会让分布更平坦,增加随机性;T小于1则更尖锐,让概率高的token更可能被选中。经过多轮试听,我发现T在0.8到1.0之间效果最好,既保留了足够的变化性,又不会让音符之间失去逻辑关系。

另外还加了一个采样技巧:重复惩罚。在采样时,如果某个token在前面最近10步内已经出现过,就在它的logits上减去一个固定值。这个技巧对防止生成内容中频繁出现同音重复非常有效。在没加这个惩罚之前,生成的旋律经常出现同一个音连续弹五六次的情况——这在真实音乐里极其罕见,但对模型来说却是低风险高收益的选择。

5. 生成效果与测试验证

5.1 客观指标与主观听感

训练结束之后怎么评估模型好坏?最直接的方式当然是听。我写了一个工具脚本,把生成的MIDI自动渲染成MP3音频,然后连续试听了大概两百段生成的旋律。整体来说,大约四成以上的输出是能入耳的,有两成左右甚至能达到“可以在音乐软件里循环播放”的水准。

客观指标上我统计了三组数据:音高分布范围、音符时长分布、相邻音符音程差。跟训练集的真实分布做对比,发现模型生成的旋律在这三个维度上都跟原始分布高度接近。说明模型确实学到了曲库里音乐的一些基础统计规律,而不是单纯死记硬背。

从听感上说,几个惊喜的发现:第一,模型生成的内容呈现出明显的“乐句感”,就是你会听到几个音自然地组成一个小短句,句子之间有短暂的停顿,然后开启新的句子。这其实很难,意味着模型自己学会了“呼吸”的节奏。第二,模型似乎捕捉到了“调性感”,生成的内容基本能稳定在一个调性内,很少出现连续刺耳的不和谐音程。但也有明显不足:模型对“段落感”的把握很弱,它知道一句话怎么写,但不知道一首歌怎么组织,生成超过一分钟以后,旋律就开始涣散,像一个人想到哪说到哪。

5.2 与人类创作对比后的一些发现

我把同一个模型生成的旋律片段,跟几段人类创作但比较冷门的钢琴曲片段放在一起,找了一些朋友盲测。结论很有意思:如果只播放前十秒钟,多数人分辨不出哪个是AI写的;但一旦把时长拉到三十秒以上,AI生成内容的“散”和“平”就藏不住了——缺少人类作品中那种贯穿始终的主题动机和情绪起伏。

这个对比让我明白一个道理:当前深度神经网络在音乐生成上的强项是“局部流畅性”,弱项是“全局结构性”。如果你想让AI生成整首完整的曲子,短期内更务实的做法是“人机协同”:人类设计好编曲框架、和弦走向,让AI去填充旋律细节;或者AI生成素材片段,人类负责筛选、拼接和改编。我后来在这个项目基础上做了一个辅助工具,输入一个开头动机,AI自动延续出几个不同方向的旋律分支,再由人来选,这个流程的实用性比让AI独立写完整曲要高得多。

6. 常见问题与排查技巧实录

6.1 典型问题的症状与解法

这个项目从零到跑通,遇到的坑不少,我把几个最有代表性的问题整理成一个速查表,基本涵盖了我被卡住过最久的地方:

症状可能原因排查方法与解决方案
训练损失不下降学习率过大导致震荡,或梯度消失先调小学习率试试;如果无效,检查是否序列过长,改用课程学习从短序列开始
生成内容全是同一个音采样温度过低,或重复惩罚过强调高T值到1.0以上;降低重复惩罚系数,或者只对最近3步做惩罚
生成的旋律像是从训练集抄的模型过拟合增大dropout比例,减少训练轮次,或增加数据集的多样性
MIDI文件解析后音符数量异常原文件包含大量控制事件或非钢琴音轨检查MIDI轨道信息,清洗时按通道过滤,只保留钢琴通道(通常为通道0)
生成结果在几十个音符后开始乱跳模型未学到长程结构增加注意力层权重,或把输入序列从512拉长到1024同时增加训练时间

6.2 一个印象深刻的调参案例

说个具体的案例。有一版模型训练完,生成的旋律总是结束得太突兀——可能前面听着好好的,突然一个刺耳的高音就收尾了,像话说到一半被人打断。我一开始以为是模型能力不够,后来打印出生成过程中每个时间步的概率分布才发现问题:模型在句子结尾位置,几乎把所有概率都压到了几个高音区的音符上,而在真实曲库里,乐句结尾更常用长音和稳定的主音来收束。

问题出在数据编码上:我处理MIDI时把所有音轨都压缩成了单音序列,但当原曲有和弦的时候,压缩过程会丢掉“和弦根音在句尾持续”的信息。模型根本没见过“乐句结束时应该长什么样”的正例,自然学不会。修复方法也不复杂——编码时增加一个“延音”token,如果一个音符在下一时间步仍然保持按下状态,就产生一个延续事件。这相当于让模型意识到:结尾不一定要开启新音符,也可以只是让当前的音继续响下去。改完之后,突兀结束的问题基本消失。

6.3 工程实践中的必坑建议

最后给准备自己做同类项目的朋友几条工程上的建议。第一,训练前务必做好数据可视化,把随机抽出的MIDI渲染成钢琴卷帘图人工检查一遍,这一步能省掉后面大量排查时间。第二,定期保存模型检查点,每完成5个epoch保存一次,方便回溯。我有个惨痛教训:有一版模型训练到一半机器断电,检查点还是十个小时之前的,等于白跑了。第三,生成测试要系统化,不要只听三五段就下结论。我写了一个批量生成脚本,每次从不同的随机种子出发生成二十段,然后统一转成音频,再分发给多人盲测。这种“批量盲测”的结果远比主观试听可靠。

这整个项目做下来的体会,如果要用一句话总结,那就是:深度神经网络学“音乐感觉”比学“音乐结构”容易得多。模型可以轻松学会某个调式里哪些音听着顺耳,但要学会“起承转合”这种高层的组织形式,靠目前的序列模型还很吃力。后续如果想在结构上做突破,我打算尝试更大规模的Transformer架构,并结合强化学习用人类偏好作为奖励信号去微调模型,让AI自己摸索出“怎样才算一首好曲子”。

这个项目我从头到尾写了大概两千行代码,数据预处理占了一小半,剩下的主要是模型和训练逻辑。如果你也在做类似的事情,欢迎沿着上面的思路做改进——比如把LSTM换成Transformer,或者把单音序列改成多音同时生成。而如果你是个纯粹对AI音乐感兴趣、还没怎么写过代码的人,希望这篇复盘能让你理解,所谓“AI作曲”不是玄学,它背后就是一套清晰的工程流程:把音乐变成数据,让模型学习规律,再按规律采样生成。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询