资讯详情

深度神经网络生成MIDI:从数据编码到训练与解码的完整指南

📅 2026/10/10 16:39:07 | 华诺云谱 👁 阅读
深度神经网络生成MIDI:从数据编码到训练与解码的完整指南
简介面向音乐生成与深度学习实践者这份代码包围绕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”不能照搬文本生成模型文本生成模型处理的是离散 tokenMIDI 文件理论上也可以离散化成 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(keylambda 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_ON1000-1007 是 TIME_SHIFT2000-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所以模型不需要特别深、特别宽。参数设置上如果数据集只有几百首 MIDI8 层 Transformer 已经足够如果数据量上万首可以上到 12 层。embedding 维度一般取 256 到 512 之间太大容易过拟合小数据太小学不到音高之间的关联。我习惯用一个基准配置起步然后只调两三个关键参数。from transformers import GPT2Config, GPT2LMHeadModel VOCAB_SIZE 2100 # 按 3.2 节的词表设计 SEQ_LEN 512 config GPT2Config( vocab_sizeVOCAB_SIZE, n_positionsSEQ_LEN, n_layer8, n_head8, n_embd512, dropout0.1, activation_functiongelu ) model GPT2LMHeadModel(config) print(f参数量: {model.num_parameters() / 1e6:.1f}M)这组参数下模型大约 25M 参数在单张 12GB 显存的卡上可以跑batch size 调到 16 问题不大。相比动辄上百 M 的文本模型这算很轻量。但轻量不代表无脑小——n_head8必须能整除n_embd512如果你改 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_outputsFalse): 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(reductionnone) 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打开文件看所有音符都堆在同一个时间点。原因数据预处理时丢弃了shift0的分支或者把重叠音符的重叠区域全删了导致训练集里“齐奏”比例极少模型没学会错开音符。解决不要删除重叠音符的 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_OFFDAW 无法判定音符结束。解决转 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 编辑体验都没有问题再去扩展数据集。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑