ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于LSTM的MIDI音乐生成:从数据准备到旋律采样

基于LSTM的MIDI音乐生成:从数据准备到旋律采样 简介一份以长短期记忆网络LSTM与Python实现的音乐生成器完整工程面向对深度学习、序列建模及AI音乐创作感兴趣的开发者和学生。压缩包共56个文件大小仅723KB包含46个midi格式音乐数据、4个Python脚本、5张模型结构及乐理说明图片和1份README文档覆盖了音乐数据预处理、LSTM模型定义、训练与旋律生成的全流程。项目基于Python生态的Keras/TensorFlow框架实现从音符序列中学习音乐的模式与风格训练后能够生成风格相近的新旋律并输出为midi文件。资源自带可直接使用的MIDI训练集代码模块划分清晰便于快速上手适合用于课堂实验、课程设计或毕业设计参考。目前已有181人学习浏览体积轻量内容紧凑能够帮助初学者直观理解循环神经网络在序列生成任务中的完整应用思路。1. 把 LSTM 用在音乐生成上ai_music 到底在解决什么问题用 Python 搭一个 LSTM 来写旋律是很多人入门深度学习的第一道甜点。市面上 ai_music 这类项目的核心思路很直接把 MIDI 文件拆成音符序列像训练语言模型一样让 LSTM 预测下一个音符训练完再让模型自己往下续写。它解决的问题不是生成一首完整的歌而是让你用最少的代码验证循环神经网络对序列数据的建模能力。适合两类人一是 Python 基础扎实但没碰过深度学习的开发者想找个比 MNIST 有意思的练手项目二是音乐科技方向的初学者想理解算法生成的底层逻辑。下面从数据准备、模型搭建到采样生成完整走一遍这条链路。2. 准备训练数据从 MIDI 到 LSTM 能吃的序列做 ai_music 这类项目数据准备决定了七成效果。LSTM 吃的是序列数据音乐里最适合当序列的是 MIDI——它记录的是音符、音高、时长这类符号信息不是声波。用 MIDI 训练模型学的是音符之间的组合规律规模可以很小训练速度比直接处理音频快一个数量级。开始之前先确认环境Python 3.8 以上装三个包就够。pip install music21 tensorflow numpymusic21 负责 MIDI 的解析和写入tensorflow 提供 LSTM 模型实现numpy 做数值计算。如果你装的是 CPU 版 TensorFlow这一行就够GPU 版安装命令略不同但后续代码完全一致。2.1 MIDI 解析用 music21 把音符和时长拆出来Python 生态里解析 MIDI 有两个常用库mido 和 music21。mido 更底层按事件流给原始消息music21 把 MIDI 包装成乐谱对象处理调号、拍号、和弦这些音乐概念更方便。做旋律生成我一般用 music21少写很多解析代码。# 解析 MIDI 文件提取音符序列 from music21 import converter, note, chord def extract_notes(midi_path): score converter.parse(midi_path) notes [] # score.flat 把所有声部按时间拍平成一条线性序列 for element in score.flat.notes: if isinstance(element, note.Note): notes.append(str(element.pitch)) elif isinstance(element, chord.Chord): # 和弦的多个音高用点号连接当作一个 token notes.append(..join(str(p) for p in element.pitches)) return notes seq extract_notes(bach_sample.mid) print(seq[:20])这段代码的逻辑converter.parse 读入 MIDI 返回 Score 对象score.flat 把多个声部的音符按时间顺序拍平逐个元素判断普通音符把音高转成字符串比如 C4、F#5和弦把多个音高用点号拼成一个字符串。LSTM 看到的输入就是一行字符串序列和文本处理没有本质区别。三个使用要点拍平后的顺序按时间排序对钢琴独奏这类单声部 MIDI 很友好但多轨管弦乐 MIDI 拍平后会混入大量同时发声的音符序列乱成一团。新手最容易在选数据这一步翻车拿了一首交响乐 MIDI解析出来的根本不是旋律线模型自然学不出东西。解决办法是先用 music21 或 DAW 把 MIDI 拆成单声部或者只取主旋律那一轨。为什么把和弦合并成一个 token因为一个时间点可能有多个音合并成 C4.E4.G4 是最省事的多声部表达。代价是 vocab 里多了很多低频和弦组合需要靠截断清理。训练集最好风格统一。有人拿 20 个 MIDI 文件混了巴洛克、爵士、流行三种风格四种拍号模型学出来四不像。选同一风格、相近调性的曲目比如全是巴赫的赋格或者全是流行钢琴伴奏LSTM 学到的模式才稳定。数据量方面练手项目 20 到 50 个风格一致的 MIDI 文件就够每个文件平均几百个音符合并起来几万条训练样本。这个量级 CPU 就能训得动不需要先租 GPU 再开工。2.2 序列化与编码滑窗切分和音符索引音乐生成本质上和 LSTM 时间序列预测是同一件事一串按时间排列的离散值用过去 N 步预测下一步。把音符换成传感器读数、股票价格后续代码几乎不用动。这也是为什么很多讲 LSTM 时间序列预测的 Python 教程喜欢拿音乐当入门案例——数据公开、结果一听就知道对不对。要把字符串音符喂给模型先转成整数索引from collections import Counter import numpy as np def build_encoding(all_notes, max_vocab200): # 按出现频率排序只保留前 max_vocab 个 token counter Counter(all_notes) vocab [n for n, _ in counter.most_common(max_vocab)] note2idx {n: i for i, n in enumerate(vocab)} idx2note {i: n for i, n in enumerate(vocab)} return note2idx, idx2note def create_windows(notes, note2idx, seq_len32): X, y [], [] for i in range(len(notes) - seq_len): seq_in notes[i:i seq_len] # 过去 32 个音符作为输入窗口 seq_out notes[i seq_len] # 下一个音符作为预测目标 X.append([note2idx[n] for n in seq_in]) y.append(note2idx[seq_out]) return np.array(X, dtypenp.int32), np.array(y, dtypenp.int32) note2idx, idx2note build_encoding(all_notes) X, y create_windows(all_notes, note2idx, seq_len32) print(X.shape, y.shape)build_encoding 用 Counter 统计每个 token 出现次数按频率排序后截断把排在 200 名之外的稀有 token 丢掉。这一手对控制训练成本很关键vocab 从 400 缩到 200最后一层 softmax 的计算量直接减半生成结果反而更稳定。如果个别音符不在词典里create_windows 会直接抛 KeyError这是一种信号说明你的数据风格不统一或者截断阈值太低。create_windows 做滑窗切分窗口长度为 seq_len步长为 1每个窗口对应一个预测目标。两个参数值得细说seq_len 是模型每次回头看多少个音符。16 到 64 是常见范围。太短学不到乐句级别的重复结构太长长距离信息被稀释训练也变慢。步长默认 1数据利用率最高。如果内存吃紧或者训练太慢把循环改成 range(0, len(notes) - seq_len, 2)样本量直接减半代价是模型看到的数据少一点。多个 MIDI 文件可以这样合并处理import glob all_notes [] for path in glob.glob(midi_data/*.mid): all_notes.extend(extract_notes(path))注意X 里每个整数对应的音符名用 idx2note 手工核对一遍错位的话后面所有生成的音符都会跟着偏。为什么用整数索引而不是 one-hotone-hot 在 vocab 变大后非常浪费内存vocab 300、seq_len 32、10 万条样本one-hot 化的 X 大约占 30GB同样的数据用 int32 整数存只有十几 MB。这是下一章模型用 Embedding 层而不是把 one-hot 直接喂进 LSTM 的原因不是图省事是内存账差了两个数量级。3. 搭建音乐 LSTM模型结构、训练参数与损失函数LSTM 是 1997 年 Hochreiter 和 Schmidhuber 提出的循环网络变体核心是用遗忘门、输入门、输出门控制信息的保留和丢弃以此建模长距离依赖。音乐里的主歌副歌重复、和弦走向都是跨越几十个音符的结构模式正好是 LSTM 神经网络擅长处理的类型。3.1 网络结构选型Embedding、LSTM 层数与 Dropout试过几种结构之后最稳的组合是Embedding 双层 LSTM Dense。单层 LSTM 学到的乐句太短三层以上在小数据集上基本过拟合两层是平衡点。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout def build_music_lstm(vocab_size, embedding_dim64, lstm_units128, dropout0.3): model Sequential([ Embedding(vocab_size, embedding_dim), # 整数索引 - 稠密向量 LSTM(lstm_units, return_sequencesTrue), Dropout(dropout), # 防止过拟合 LSTM(lstm_units), Dense(vocab_size, activationsoftmax) # 下一个音符的概率分布 ]) model.compile( losssparse_categorical_crossentropy, optimizeradam, metrics[accuracy] ) return model model build_music_lstm(len(note2idx)) model.summary()各层的作用Embedding 层把整数索引映射成稠密向量。训练时会把语义相近的音符合并到向量空间的相近位置比如经常跟在 C4 后面的 E4、G4向量距离会拉近。这是 one-hot 做不到的。第一个 LSTM 层 return_sequencesTrue把每个时间步的隐藏状态都交给第二层第二层只保留最后一个时间步的输出。两层分别负责短句模式和长程结构。Dropout 放在两个 LSTM 层之间是防止过拟合最有效的一处改动。几万条样本的规模下模型很容易把训练集背下来不加 Dropout 的版本生成结果越来越像训练集的复读机。最后的 Dense 层用 softmax 输出 vocab_size 个概率表示下一个音符是每个候选 token 的可能性。参数经验范围参数取值范围说明embedding_dim32~128vocab 200 左右用 64 够用超过 500 再上 128lstm_units128~256128 是最稳起点256 训练时间翻倍但效果不一定更好dropout0.2~0.5数据量小往高调数据量大可以放宽batch_size64~512CPU 上建议从 128 起步如果你更习惯 PyTorch可以对着 PyTorch 的 LSTM 源码把门控计算流程捋一遍模型结构本身不依赖框架这套骨架搬过去完全一样。3.2 训练参数设置batch size、学习率与早停训练环节最省心的做法是把模型保存和早停挂在一起from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint ModelCheckpoint( music_lstm.keras, monitorval_loss, save_best_onlyTrue, verbose1 ) early_stop EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) history model.fit( X, y, batch_size128, epochs50, validation_split0.1, callbacks[checkpoint, early_stop] )几个容易被忽视的细节ModelCheckpoint 的 monitor 用 val_loss 而不是 loss。训练 loss 一直在降不代表模型在学有用的东西可能只是过拟合。save_best_onlyTrue 保证落到磁盘的是验证集上最好的权重而不是最后一个 epoch 的权重。这是给后面留的后悔药训练结束发现某个 epoch 后验证集开始变差还能把好权重捞回来。EarlyStopping 的 patience5连续 5 个 epoch 验证集 loss 没有改善就停。音乐数据规模小30 到 50 个 epoch 一般足够不需要固定跑满。validation_split0.1 从 X 里随机抽 10% 做验证。这个做法有个坑滑窗重叠导致验证集和训练集有大量重叠片段指标会虚高严谨的切法在避坑章节细说。训练过程看两个指标。第一是训练 loss 是否稳定下降前几个 epoch 就卡住不动多半是学习率或编码出问题。第二是验证集准确率音乐生成任务里 40% 到 60% 已经算正常因为下一个音符本来就不是完全确定的模型是在学概率分布而不是抄答案。如果看到准确率冲上 80%先别高兴大概率是数据里某个音符占比极高模型学会了无脑输出最高频音符。4. LSTM 生成旋律温度采样与 MIDI 还原训练完的模型拿到的是下一个音符的概率分布。生成时给一个种子片段让模型逐步预测后续音符每步把预测结果拼回序列再继续循环几百次就得到一段新旋律。这个环节有个常见坑直接取概率最大的音符生成结果会越来越单调最后变成同一个音反复敲。4.1 温度采样让生成结果不那么死板概率分布越尖锐生成越保守越平缓生成越跳跃。温度采样就是在 softmax 之前把 logits 除以一个温度系数控制尖锐程度。import numpy as np def sample_with_temperature(pred_probs, temperature1.0): # 先取对数再除以温度等价于对 logits 做温度缩放 logits np.log(pred_probs 1e-8) / temperature logits logits - np.max(logits) # 减去最大值防止 exp 溢出 exp_logits np.exp(logits) probs exp_logits / np.sum(exp_logits) return np.random.choice(len(probs), pprobs)这个函数有两个细节。一是 np.log 之前加 1e-8避免 log(0) 报错softmax 输出里有很多接近 0 的概率二是 exp 之前减去最大值是防止溢出的标准数值技巧。temperature1.0 时结果和原始分布一致小于 1 分布变尖大于 1 分布变平。生成循环def generate_melody(model, note2idx, idx2note, seed_notes, length200, temperature1.0): generated list(seed_notes) for _ in range(length): # 取最近 32 个音符作为上下文和训练时的 seq_len 保持一致 seq generated[-32:] seq_idx [note2idx[n] for n in seq] pred model.predict( np.array(seq_idx).reshape(1, -1), verbose0 )[0] next_idx sample_with_temperature(pred, temperature) generated.append(idx2note[next_idx]) return generated参数和注意事项temperature 的可听区间大致在 0.8 到 1.5。第一次跑通用默认 1.0效果勉强能听调到 0.3 之后模型反复输出同一个音调到 2.5 之后满篇跨八度乱跳。经验表温度区间听感特征适用场景0.3~0.6大量重复几乎在复述训练数据确认模型结构没问题的调试手段0.8~1.2重复和变化比较平衡默认起点1.3~1.8跳跃大、新意多偶尔有刺耳音找灵感、做实验seed_notes 至少要有 32 个音符才能填满窗口没有现成乐句就从训练数据里随机截一段。固定种子片段很重要不同温度、不同版本之间对比才公平。生成循环逐音符前向推理每步调用一次 model.predict生成 200 个音符在 CPU 上大概一两分钟属正常现象。4.2 从预测序列还原 MIDI 文件生成的序列还是字符串列表还原成能播放的 MIDIfrom music21 import stream, note, chord, tempo def sequence_to_midi(sequence, output_path, bpm100): s stream.Stream() s.append(tempo.MetronomeMark(numberbpm)) for token in sequence: if . in token: # 含点号的 token 是训练时合并的和弦拆回多个音 s.append(chord.Chord(token.split(.))) else: s.append(note.Note(token)) s.write(midi, fpoutput_path) sequence_to_midi(generated_melody, output.mid, bpm100)还原逻辑必须和第 2 章的提取逻辑严格对应提取时把和弦用点号合并还原时遇到点号就拆回和弦。前后约定不一致是这个环节最常见的 bug。比如训练时用的是 E4.G4 格式生成时却走了 else 分支music21 会把 E4.G4 当成一个音符名直接报错或者写出打不开的文件。另外注意节奏设置。示例里每个音符沿用 music21 默认的四分音符时值200 个音符就是 200 拍100bpm 下大概两分钟。想要真实的节奏变化需要把 duration 也编码成 token 一起训练或者记录训练数据的时值分布生成时按分布采样。5. LSTM 音乐生成避坑与排查指南下面这几条是我自己反复踩过的坑也是帮别人排查时出现频率最高的问题。每条按现象、原因、解决三步写对照着排查会快很多。这类项目不难翻车点基本集中在数据、采样和权重管理三块。5.1 现象loss 正常下降生成的旋律却只有两三个音在反复原因有两种。一是采样温度太低概率分布被压得只剩最大概率的 token。二是数据里某个音符占比过高模型发现无脑输出最高频音符这条捷径就能拿到不错的 loss于是把其他音符的概率都压得很低。解决先把 temperature 提到 1.2 左右。如果还是重复统计训练数据用 Counter 看最高频 token 的占比超过 30% 说明数据不均衡按风格筛选或者对高频 token 降采样。快速判断办法把模型预测的 argmax 打出来看它是否总指向同一个索引。如果是问题在数据分布而不在采样。5.2 现象训练时内存爆掉或者显存 out of memory原因X 用 int32 存几十万条窗口只有几十 MB不是瓶颈。真正的内存杀手是 one-hot 化vocab300、50 万条样本one-hot 化之后直接几十 GB。显存爆掉则主要是 lstm_units 拉得太大或者 batch_size 设得过大。解决X 保持整数索引用 Embedding 层做向量化。显存不够就把 batch_size 从 128 降到 32或者把 lstm_units 从 256 降到 128。样本量很大时用 tf.data 构造数据集别把 X 和 y 一次性全载入内存import tensorflow as tf dataset tf.data.Dataset.from_tensor_slices((X, y)) dataset dataset.shuffle(10000).batch(128).prefetch(1) model.fit(dataset, epochs50)from_tensor_slices 只是建立惰性引用不会复制数据prefetch(1) 让 CPU 准备数据和模型计算并行能省不少训练时间。5.3 现象预测时出现 NaN或者 int() 转换报错原因训练过程梯度爆炸loss 变成 NaN模型权重已经废了。触发条件通常是学习率在小数据集上偏大或者输入数据里有异常值比如某条 MIDI 解析出的 pitch 是空字符串或 None混进了整数转换。解决先检查 X 和 y 里有没有缺值把包含 None 的样本过滤掉。再把优化器加上梯度裁剪from tensorflow.keras.optimizers import Adam model.compile( losssparse_categorical_crossentropy, optimizerAdam(learning_rate0.001, clipnorm1.0) )clipnorm1.0 把梯度全局范数限制在 1 以内挡住绝大多数梯度爆炸。如果 NaN 出现在训练后期直接加载 checkpoint 里保存的最佳权重就行不用重训。5.4 现象训练集 loss 很低验证集 loss 居高不下原因有两个往往同时存在。滑窗重叠导致验证集里大量片段和训练集重叠验证结果本身不可信同时模型容量偏大几万条样本确实容易过拟合。解决按文件切分数据而不是按窗口切分。把 MIDI 文件随机分成 90% 训练、10% 验证每个文件内部的音符序列整体划入其中一个集合再分别做滑窗。这样验证集和训练集没有任何重叠片段指标才可信。同时把 Dropout 提到 0.4或者把双层 LSTM 换成一维卷积 LSTM的混合结构卷积先提取局部模式LSTM 再做长程建模参数量更少更难背题。5.5 现象训练特别慢一个 epoch 要好几分钟原因CPU 上跑双层 256 单元 LSTM 本来就是最慢的组合如果单层还慢大概率是 batch_size 太小Adam 更新频率过高。解决优先把 batch_size 提到 256 或 512通常立竿见影再不行就把 lstm_units 减半。单层 LSTM 加更大的 embedding_dim速度能快一倍效果不一定比双层差。实在没有 GPU先把数据量减到 20 个 MIDI 文件把全流程跑通再上全量数据。这是一条血泪教训一上来就要效果结果卡在训练速度上消磨热情项目就烂尾了。6. 从「能响」到「能听」验证与调参技巧6.1 用统计指标做快速体检好不好听是主观的但生成结果有没有模型崩溃可以用统计指标快速判断。最常用的是音级分布pitch class对比from collections import Counter def pitch_class_distribution(notes): pcs [] for token in notes: for p in token.split(.): # 去掉八度数字C4 - CF#5 - F# pcs.append(p[:-1]) return Counter(pcs) train_dist pitch_class_distribution(train_notes) gen_dist pitch_class_distribution(generated_melody) print(train_dist.most_common(5)) print(gen_dist.most_common(5))把训练集和生成结果的音级分布对比一下如果生成结果只集中在两三个音级上说明模型崩溃了如果分布形状完全对不上说明温度太高模型在乱跳。这两种情况都值得在听之前先修正。6.2 我的固定流程一个种子、四个温度每次训练完我都用同一个种子片段分别在 0.6、1.0、1.4、1.8 四个温度下各生成一份存成四个 MIDI 文件再戴耳机全部听一遍。这不是玄学温度采样是生成阶段唯一能无成本调风格的旋钮比重训快得多。四个版本都不满意说明问题出在模型或数据而不是采样才值得回去改训练集或网络结构。这个项目做到这里链路已经闭环数据解析、序列建模、温度采样、MIDI 还原。它的边界我也可以直接说——学的是音符到音符的概率不是编曲也不是配器别指望它写出完整的作品。想再往前就从基线上加时长 token、加多轨或者换更强的序列模型每一步都有明确入口。我自己的习惯是每次改动都用同一套种子生成四份对比听完再决定下一步这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表