☰
深度神经网络生成MIDI:从数据编码到训练与解码的完整指南
2026/10/10 16:38:29 网站建设 项目流程

简介:面向音乐生成与深度学习实践者,这份代码包围绕Midi数据集训练深度神经网络生成音乐,涵盖midi数据预处理、贝叶斯超参数优化、模型训练与生成等成套流程,适合有一定Python和深度学习基础、想复现音乐生成项目的学习者。压缩包整体约1.02MB,主体为Python脚本,包含数据计算、训练、生成和超参数搜索等模块,便于按步骤调用;由于上游未提供详细文件清单,暂不展开具体文件数量。现有187人浏览学习。通过阅读源码和运行入口,可以掌握如何将Midi文件转换为numpy数组用于模型输入,利用贝叶斯优化调整超参数,并加载已训练模型生成新的音乐序列,同时了解TensorFlow、Keras、music21、pygame等工具在音乐任务中的组合用法,为后续开展音乐补全或生成类项目提供可直接改造的参考实现。

1. midiGenerator:把旋律交给深度神经网络,能得到什么

如果你玩过几款自动作曲工具,多半会碰到同一个瓶颈:生成的音乐“听上去像那么回事”,但导出成 MIDI 之后一塌糊涂——音符粘连、时值乱跳、连音线断裂。midiGenerator 这类项目的核心思路,是直接让深度神经网络学会“写”MIDI 文件而不是生成音频波形。它把音乐当作序列数据来处理,输出的是标准 MIDI 事件序列,换句话说,模型在学的是“什么时候按下哪个键、按多久、力度多大”。这带来一个直接的好处:生成结果天然就是可编辑、可重新配器、可导入 DAW 的工程文件,而不是一段没法拆开的音频。适合谁用?想做自动伴奏、旋律生成、风格模仿的开发者,以及需要批量产出 MIDI 素材的游戏音频和编曲从业者。这篇文章不聊玄学,直接拆解从数据编码、模型选型到训练和导出 MIDI 的完整落地路径。

2. 深度神经网络生成 MIDI:三个可选方案与一条推荐路线

2.1 为什么“生成 MIDI”不能照搬文本生成模型

文本生成模型处理的是离散 token,MIDI 文件理论上也可以离散化成 token 序列,两者在形式上是相似的。但 MIDI 有一个文本没有的特性:时间是结构化的。一个音符有三个属性:起始时间、持续时长、力度。如果像文本那样简单地“逐字预测”,模型会很容易学会音高分布,却把握不住节奏结构——它不知道一个小节从哪里开始,也不知道强拍弱拍的循环。这就是很多初版模型“听起来音高都对、节奏乱成麻”的根本原因。

所以真正可落地的方案,必须在编码阶段就把“时间”这件事显式地放进 token 序列里,或者在模型结构上加入对时间步的归纳偏置。前者做起来更简单,也是社区主流做法;后者是 Magenta 的 Performance RNN 那一路,用自定义的 LSTM 单元和事件类型来建模。你没看错,深度神经网络生成 MIDI 这个课题,早在 Transformer 火起来之前就有 LSTM 方案在跑。

2.2 三种常见技术方案对比

方案模型形态输入编码优点主要缺点适合场景
LSTM/RNN 序列生成单层或多层 LSTM音符事件 + 时间偏移实现简单、训练快、显存占用低长序列记忆弱,超过 30 秒就明显走神短旋律、单轨动机生成
Transformer 自回归GPT 式因果模型离散 token,含时值/音高/力度能建模更长上下文,风格模仿能力强解码慢、训练数据需求大、易过拟合小数据集多轨流行乐、风格迁移
VAE / Diffusion 生成隐空间生成压缩后的潜在向量或符号序列可控性强,可做插值结构复杂,工程成本高,调试难研究向、需要精细控制生成的场景

对多数人来说,第一条可用路径是 Transformer 自回归方案。不是因为它最先进,而是它的坑最容易被发现:loss 曲线不降说明编码有问题,生成长度不收敛说明序列长度没设好,属于可排查的范畴。VAE 那条路一旦生成结果怪,你很难说清是重构损失的问题、KL 散度的问题还是解码器的问题,黑匣子属性太强。

2.3 推荐路线:GPT 式自回归 + 事件型 token 编码

我一般会推荐的组合是:用 GPT-2 的模型结构(因果注意力 + 位置编码)直接做自回归生成,词表由三类 token 组成——NOTE_ON(音高+力度)、NOTE_OFF(音高)、TIME_SHIFT(时间偏移)。这套方案的逻辑很简单:模型看到前 512 个 token,预测下一个 token。下一个 token 可能是一个新音符开始,可能是某个音结束,也可能是“时间前进 30 ticks”。这种编码方式天然解决了时值问题,因为音符持续多久由 NOTE_ON 和 NOTE_OFF 之间的距离决定,而距离的长短由 TIME_SHIFT 控制。

有一个要提前想清楚的决策:速度(velocity)到底要不要放进 token 里。放进去了,模型能生成有强弱变化的旋律,但词表会膨胀;不放进去,所有音符力度一样,生成结果听起来像电子琴自动演奏。折中做法是只保留 4 档力度,对应 pp / mp / mf / ff。这样词表只增加 3 个 token,生成结果的听感提升非常明显,值得做。

3. 从 MIDI 文件到训练集:数据预处理与 token 编码实操

3.1 用 pretty_midi 把 MIDI 拆成音符事件数组

数据预处理是整个流程里最花时间、也最容易翻车的环节。深度神经网络本身不挑数据格式,但它对数据的分布极其敏感——如果你的数据集里大部分 MIDI 是 16 轨的管弦乐,少部分是单轨钢琴曲,模型大概率偏向前者,生成结果会是一个多轨混在一起、声部打架的怪东西。我一般会先做轨道合并和裁剪,再进入 token 化。

import pretty_midi import numpy as np # 参数区:你可以按自己的数据集调整 TIME_STEP = 0.25 # 最小时值粒度(秒),对应 96 ticks 的量化精度 MIN_DURATION = 2 # 短于 2 个 TIME_STEP 的音符直接丢弃 PITCH_MIN = 48 # C3,低于这个音高的音符丢弃(低频噪声多) PITCH_MAX = 84 # C6,高于这个音高的音符丢弃 VELOCITY_BINS = 4 # 力度分成 4 档:pp/mp/mf/ff pm = pretty_midi.PrettyMIDI("input.mid") notes = [] # 多轨合并:把所有非打击乐轨的音符合到一个列表里 # 注意:不合并会导致同一时刻出现大量重复音,模型学成“和弦堆叠狂魔” for inst in pm.instruments: if inst.is_drum: continue for note in inst.notes: start = int(round(note.start / TIME_STEP)) end = int(round(note.end / TIME_STEP)) if end - start < MIN_DURATION: continue if note.pitch < PITCH_MIN or note.pitch > PITCH_MAX: continue # 力度分箱:把 0-127 压到 0-3 vel = min(int(note.velocity / (128 / VELOCITY_BINS)), VELOCITY_BINS - 1) notes.append((start, end, note.pitch, vel)) # 按起始时间排序,保证事件流有序 notes.sort(key=lambda x: (x[0], x[1], x[2]))

这段代码做了什么?它把任意一个 MIDI 文件变成了一组时间上对齐的事件元组。TIME_STEP决定了模型的时间分辨率——设成 0.25 秒意味着它永远无法表达三十二分音符和三连音,但对流行乐和游戏配乐已经很够用。MIN_DURATION是一个去噪参数,很多 MIDI 文件里存在一两个 tick 的杂音音符,不删会让模型学到那种稀碎的弹法。真正需要调的是VELOCITY_BINS,它是生成作品听感生动程度的直接决定因素。

3.2 把音符数据转成模型能吃的 token 序列

有了音符数组还不够,要把它转换成一维 token 序列。这里的关键设计是 TIME_SHIFT token——它不表示具体时间长度,而是一个“相对上一个事件过去了多少时间”的偏移量。这样做的原因是:MIDI 中的绝对时间戳变化范围太大,直接作为数值特征输入模型会让它难以泛化;而偏移量是离散的、有界的,模型学起来更稳定。

SHIFT_BINS = 8 # TIME_SHIFT 最多分 8 档:1,2,3,4,6,8,12,16 个 TIME_STEP tokens = [] prev_end = notes[0][0] # 从第一个音符的 start 开始 # NOTE_ON: 词表前 1024 个位置留给 (pitch, velocity) 组合 # 这里我们把音高 48-84 映射到 0-36,再乘以力度档数 4 for start, end, pitch, vel in notes: # 先处理时间偏移 shift = start - prev_end if shift < 0: # 音符重叠是正常的,直接跳过负偏移;重叠部分由后续 NOTE_OFF 处理 shift = 0 # 找到最接近的 shift 档位 shift_id = min(int(shift / TIME_STEP), SHIFT_BINS - 1) tokens.append(1000 + shift_id) # TIME_SHIFT 放在词表 1000-1007 # 再处理音符开始 pitch_id = (pitch - PITCH_MIN) * VELOCITY_BINS + vel tokens.append(pitch_id) # NOTE_ON 放在词表 0-147 tokens.append(2000 + pitch) # NOTE_OFF 放在词表 2000-2036,只记音高不记力度 prev_end = max(prev_end, end)

这里有个容易犯的错:NOTE_OFF 我用了原始pitch而不是pitch_id。原因很简单——一个音符的结束只与音高有关,不需要力度信息。词表被分成了三段:0-147 是 NOTE_ON,1000-1007 是 TIME_SHIFT,2000-2036 是 NOTE_OFF。中间留白是为了方便调试时一眼看出 token 属于哪一类。训练时这些留白不参与计算,但如果你用的是别人写好的 tokenizer,要确保词表 padding 是正确的,否则模型会学到输出空白 token。

3.3 数据集切分:别忘了验证集要按“曲子”切而不是按“片段”切

很多人在这一步入坑:把每首曲子切成 512 token 的定长片段,然后随机分配训练集和验证集。这会导致同一首曲子的前半段在训练集、后半段在验证集,模型实际上“见过”了验证数据的一部分。更隐蔽的是,坏 MIDI 文件(比如解压损坏或者导出异常的)会污染数据分布。

正确做法是按文件维度切分:先按文件名把全部 MIDI 分成 train / valid 两组,再做片段切分。另外,切片时最好带一点重叠——每段保留前 32 个 token 作为“上文”,这样模型在训练时能看到足够的起始上下文,生成时也能从任意位置接续。

4. 训练深度神经网络生成 MIDI:模型配置与 loss 设计

4.1 GPT 式模型的参数怎么定:不是越大越好,而是够用就好

MIDI token 序列的长度天然比文本短——一首 30 秒的钢琴曲大约 300-500 个 token,所以模型不需要特别深、特别宽。参数设置上,如果数据集只有几百首 MIDI,8 层 Transformer 已经足够;如果数据量上万首,可以上到 12 层。embedding 维度一般取 256 到 512 之间,太大容易过拟合小数据,太小学不到音高之间的关联。我习惯用一个基准配置起步,然后只调两三个关键参数。

from transformers import GPT2Config, GPT2LMHeadModel VOCAB_SIZE = 2100 # 按 3.2 节的词表设计 SEQ_LEN = 512 config = GPT2Config( vocab_size=VOCAB_SIZE, n_positions=SEQ_LEN, n_layer=8, n_head=8, n_embd=512, dropout=0.1, activation_function="gelu" ) model = GPT2LMHeadModel(config) print(f"参数量: {model.num_parameters() / 1e6:.1f}M")

这组参数下模型大约 25M 参数,在单张 12GB 显存的卡上可以跑,batch size 调到 16 问题不大。相比动辄上百 M 的文本模型,这算很轻量。但轻量不代表无脑小——n_head=8必须能整除n_embd=512,如果你改 embedding 维度而忘了调整 head 数,会直接报维度错误。

4.2 Loss 函数与训练策略:交叉熵是标配,但温度缩放有讲究

自回归生成的标准 loss 是交叉熵,即预测下一个 token 的概率分布与真实 token 的 one-hot 编码之间的差距。对 MIDI 生成场景,不同 token 类别的错误代价不一样:NOTE_ON 预测错了只是音高不对,但 TIME_SHIFT 预测错了会影响整个节奏结构。一种常见做法是给三类 token 分配不同权重——把 TIME_SHIFT 的 loss 权重调高到 1.5 或 2.0,让模型更重视节奏正确性。这在transformers库里需要自定义 loss 计算函数,不算复杂但值得做。

class MidiLossTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False): labels = inputs.get("labels") outputs = model(**inputs) logits = outputs.logits # (batch, seq_len, vocab_size) # 给 TIME_SHIFT 段(token 1000-1007)更高的权重 shift_mask = (labels >= 1000) & (labels < 1010) weights = torch.where(shift_mask, 1.8, 1.0).float() loss_fct = torch.nn.CrossEntropyLoss(reduction="none") loss = loss_fct(logits.view(-1, VOCAB_SIZE), labels.view(-1)) loss = (loss.view(-1) * weights.view(-1)).mean() return (loss, outputs) if return_outputs else loss

训练参数上,learning rate 用 1e-4 起步,配合 warmup 和线性衰减。batch size 的调整逻辑是:loss 震荡幅度大、降不下去,就把 batch size 翻倍;如果 loss 降得很快但验证 loss 开始上升,说明过拟合了,先把 dropout 加到 0.2 再减小学习率。

4.3 训练过程中监控什么:只看 loss 是不够的

训练深度神经网络生成 MIDI 时,loss 数值有迷惑性——它可能降到很低,但生成结果全是同一个音反复出现。这是因为 loss 反映的是平均预测质量,而音乐的结构性错误(比如连续 8 个 token 全是同一个 NOTE_ON)只占很小比重。我会额外加一个采样器,每训练 N 步就跑到验证集上随机挑几首曲子,用当前模型生成 20 秒片段,转成 wav 听一遍。听起来像“玄学”,但这是最快的质量反馈环。没有这一步,你可能训练了三天最后发现生成的全是单音重复。

另一个值得关注的是 loss 的下降曲线形态。如果前 500 步 loss 几乎不降,大概率是数据预处理出了问题——最常见的是 token 序列里混进了大量负的 shift 值或者越界的音高值,模型在试图学习一个本身不一致的序列。

5. 深度神经网络生成 MIDI 的常见坑:五个必踩问题与排查方法

5.1 全部音符同时开始,生成结果变成“和弦墙”

现象:生成出来的 MIDI 序列里,前 50 个 token 全是 NOTE_ON,没有一个 TIME_SHIFT,打开文件看,所有音符都堆在同一个时间点。原因:数据预处理时丢弃了shift=0的分支,或者把重叠音符的重叠区域全删了,导致训练集里“齐奏”比例极少,模型没学会错开音符。解决:不要删除重叠音符的 NOTE_OFF 逻辑,让 NOTE_ON 可以在 TIME_SHIFT 之前出现;同时保证数据集里有一定比例的非齐奏片段。

5.2 验证 loss 一直下降,但生成旋律完全不像训练集风格

现象:loss 曲线平滑下降,听起来却不是目标风格。原因:验证集和训练集切分有问题,模型见过验证集;或者数据来源混杂太严重——流行乐、古典乐、游戏 OST 混在一个数据集里,模型学了个均匀分布,生成结果风格平庸。解决:先按 3.3 节说的按文件切分;再把数据集风格分开训练的对比实验跑一遍,确认风格特征是否在 loss 上是可区分的。

5.3 MIDI 转音频后总有刺耳的短音爆音

现象:生成的 token 序列能正常转成 MIDI,但渲染成 wav 后有咔嗒声。原因:NOTE_OFF 与下一个 NOTE_ON 的时间间隔小于 1 个 TIME_STEP,导致音符过渡不干净。这在模型层面几乎无法避免,属于解码后处理的问题。解决:在 MIDI 转音频之前加一步“音符粘连”清理——把间隔小于 5ms 的相邻音符合并成一个长音符。

5.4 模型学会复制训练集,但只会开头不会结尾

现象:生成的前 20 个音符和某训练样本完全一样,后面开始乱拼。原因:序列长度设置过长,模型注意力在前半段过度集中;或者学习率太低导致模型直接记住了训练样本的开头部分。解决:把序列长度从 512 降到 256,同时调高 dropout;对比随机起点生成与从头生成的差异,如果差异大说明模型有记忆倾向,需要增加数据量或缩短序列。

5.5 生成 MIDI 文件无法被主流 DAW 正确解析

现象:模型输出的 token 序列在技术上合法,但导入 Cubase / Logic 后出现音符时值错乱、轨道对不齐。原因:token 序列转 MIDI 事件时,NOTE_ON 和 NOTE_OFF 的匹配逻辑写错——同一个音高出现两次 NOTE_ON 而没有 NOTE_OFF,DAW 无法判定音符结束。解决:转 MIDI 时用栈结构维护当前未关闭的音符,确保每个音高在任何时刻最多只有一个活跃实例。

6. 让生成结果可用的最后一步:温度采样与 MIDI 后处理

模型训练完成后,真正决定生成质量的是解码策略。这里只推荐一个最实用的组合:temperature + top-k 采样,禁止贪心解码——贪心解码生成的旋律每个音符都是概率最高的,结果会极其无聊。温度参数直接控制“冒险程度”:低于 0.8 时模型倾向于重复训练集中常见的音型,高于 1.2 时音符跳跃幅度变大、节奏松散。对流行乐风格,0.9 是一个不错的起点。

生成结束后的后处理步骤同样重要。先把连续时间步里音高相同的 NOTE_ON 做合并——模型经常会为了表现力度变化把一个音拆成两段,合并后音符更干净;再做一次八度检查,确认没有超出 MIDI 规范的音高范围。最后直接把 token 序列转成 pretty_midi 对象,就能导出标准 .mid 文件。

一个值得养成的习惯:每次生成后,把温度、top-k 值连同生成的 MIDI 文件一起命名。网上经常能看到从网盘下载的“AI 作曲”demo 文件,但没人知道参数是怎么设的,这种只能当耳朵上的参考。你自己训练时,把每轮实验的配置写进文件名,三个月后回来看还能知道当初为什么这么调。我踩过最大的坑就是用一套参数跑了所有风格,后来才发现不同速度的曲子需要完全不同的 top-k。可以先从一首你熟悉的曲子(比如网上流传的“起风了 midi 下载”这类热门 MIDI)入手,用它的前 8 小节测试你的生成链路,确认输出文件的听感和 MIDI 编辑体验都没有问题,再去扩展数据集。希望帮到你。

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

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

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

立即咨询