
简介基于 LSTM 的 AI 音乐生成器 Python 项目源码包面向深度学习入门者与音乐科技爱好者演示如何利用长短期记忆网络从 MIDI 数据中学习旋律规律并自动生成风格化片段。项目内包含完整数据集与训练脚本基于 TensorFlow/Keras 等主流框架搭建适合动手实践序列建模与音乐生成任务。压缩包共 56 个文件、约 723KB包括 46 个 MIDI 音乐样本、5 张模型结构与训练过程图示、4 个 Python 脚本和 1 份说明文档目录与功能对应清晰可直接对照阅读、复现训练流程。已有 181 人学习下载。借助该项目可走通音乐数据预处理、LSTM 网络搭建、训练拟合与最终生成音乐片段的完整链路数据、模型、生成器与文档拆分合理既适合作为课程设计素材也能作为继续探索 AI 作曲的起始框架。1. 用 LSTM 让 Python 学会“续写”旋律ai_music 到底在做什么把“ai_music”和“LSTM”放在一起熟悉序列模型的人第一反应就是这是一个用长短期记忆网络做音乐生成的 Python 项目。它的核心思路可以一句话讲完——把旋律当成一串有先后关系的“音符文本”用 LSTM 学会这段文本的统计规律然后从任意一段给定音符出发一个音一个音地把旋律续写下去。做出来的东西不是传统规则作曲而是模型从大量 MIDI 里“听”出来的风格模仿。这个方案尤其适合两类人一类是想入门生成式 AI、但不想一上来就碰大模型和 GPU 集群的开发者LSTM 的参数量在单卡甚至 CPU 上都能跑另一类是做过 NLP 序列任务、想把手头的技能迁移到音乐领域的技术人因为数据预处理、词表构建、采样生成这一套流程和文本生成几乎一一对应。你需要准备的只有一批 MIDI 文件、一个 Python 环境和基本的 Keras 使用经验。接下来我按自己实际做过的路径把数据准备、模型搭建、训练生成和踩坑点完整过一遍。2. 把 MIDI 变成 LSTM 能读的序列音符编码与数据集准备2.1 为什么选 MIDI 而不是 MP3/WAV以及 music21 的解析方式音乐生成的数据源选择决定了整个项目的复杂度。直接用音频波形MP3/WAV做输入意味着模型要同时学习音色、音量、混响等声学特征LSTM 在这种高维连续信号上很容易陷入“生成一堆噪声”的境地。常见做法是退一步用 MIDI 格式。MIDI 本身是符号化的它记录的是“哪个音、在哪个时刻、持续多久、力度多大”而不是声音本身。这正好把问题降维成序列建模和字符级文本生成完全同构。我一般用 music21 库来解析 MIDI它是目前 Python 生态里对乐理结构支持最完整的工具包能直接拿到音符、和弦、声部层级不用自己手写 MIDI 事件解析器。下面先做一个最基础的单文件解析目标是把旋律里的音符提取成一维列表。from music21 import converter, note, chord def extract_notes(midi_path): 从 MIDI 文件提取音符/和弦名称列表 score converter.parse(midi_path) notes [] for element in score.flat.notes: if isinstance(element, note.Note): notes.append(str(element.pitch)) # 例如 C#4 elif isinstance(element, chord.Chord): # 和弦转成用点连接的音名字符串如 C4.E4.G4 notes.append(..join(str(p) for p in element.pitches)) return notes notes extract_notes(bach_847.mid) print(notes[:20], 总长度:, len(notes))这里score.flat.notes会把多声部拉平成一个按时间先后排列的音符序列适合做旋律级的序列建模。如果你只想处理主旋律可以先用score.parts[0]取第一个声部再flat我建议一开始不要这么做——多声部一起建模能让模型学到更丰富的和声关系生成结果也不容易显得单薄。解析完成后把多个 MIDI 文件的音符列表直接拼接就得到一份原始语料。需要注意和弦转成C4.E4.G4这种字符串后它在后续词表中会被当成一个整体 token而不是拆成三个单音。这样处理的理由是模型能直接学习和弦的“用法”而不是让三个音各学各的最后生成出对不齐的破碎和弦。2.2 建立音符词表与滑窗训练序列Embedding 输入的关键准备拿到原始音符列表之后第二步是把字符串音符映射成整数 ID。这一步和 NLP 里构建词表完全没有区别统计出现过的所有音符按出现频率排序给每个音符一个唯一编号。唯一要多留个心眼的是过滤低频 token比如一份语料里只出现过一次的生僻和弦保留它只会让词表膨胀、让 Embedding 层学不到有效信息。from collections import Counter def build_vocab(notes, min_freq2): counter Counter(notes) # 只保留出现次数 min_freq 的音符防止词表被生僻和弦撑爆 vocab [n for n, c in counter.items() if c min_freq] note2idx {n: i for i, n in enumerate(vocab)} idx2note {i: n for n, i in note2idx.items()} return note2idx, idx2note note2idx, idx2note build_vocab(notes, min_freq2) print(词表大小:, len(note2idx))min_freq是词表构建里第一个要调的参数。设成 1词表可能膨胀到几千甚至上万低频 token 占了大半训练时这些 ID 对应的梯度更新极少模型容易过拟合到个别文件上。设得过高比如 5又可能把一些风格化的装饰音过滤掉生成旋律会变得过于“规矩”。我一般先在 min_freq2 上跑通全流程再回头调。接下来是生成训练用的输入输出对。LSTM 的建模方式是给定前sequence_length个音符预测下一个音符。所以要把一维音符序列切成固定窗口的样本。这里有一个朴素但有效的细节不要只从每个文件开头切而是在整个拼接后的序列上每 1 个音符就滑动一次窗口这样训练样本量能扩大好几倍。import numpy as np SEQUENCE_LENGTH 64 def create_sequences(notes, note2idx, seq_lenSEQUENCE_LENGTH): sequences, targets [], [] for i in range(len(notes) - seq_len): seq_in notes[i:i seq_len] seq_out notes[i seq_len] # 先用 note2idx 过滤掉低频音符如果词表里没有就直接跳过 if seq_out not in note2idx: continue sequences.append([note2idx[n] for n in seq_in if n in note2idx]) targets.append(note2idx[seq_out]) return np.array(sequences), np.array(targets) X, y create_sequences(notes, note2idx) print(样本数:, X.shape, 标签数:, y.shape)窗口长度SEQUENCE_LENGTH直接决定了模型能“记住”多长的音乐上下文。长度太短比如 8模型只能学到相邻几个音的连接关系生成结果像随机音符太长比如 256训练样本数量骤减而且 LSTM 对这种跨小节的长程依赖记忆能力有限。64 对应常见 4/4 拍乐曲大约 8 个小节的主旋律长度是性价比不错的起点。2.3 数据增强移调与速度归一化让一份 MIDI 当三份用如果你手里的 MIDI 文件只有几十首训练出来的模型会明显“偏科”——C 大调的曲子多了生成啥都往 C 大调上靠。一个低成本的数据增强手段是移调把每首曲子的所有音符整体平移若干个半音。音乐里的旋律在移调后相对关系不变但音高区域变了模型因此能学到“这个音型在不同调上的表现”而不是死记某个绝对音高。import random def transpose_notes(notes, semitones): 把音符列表整体移调保持时值不变 from music21.pitch import Pitch transposed [] for n in notes: if . in n: # 和弦 transposed.append(..join( str(Pitch(p).transpose(semitones)) for p in n.split(.) )) else: transposed.append(str(Pitch(n).transpose(semitones))) return transposed # 每个文件生成 4 个移调版本原调、2、-2、5四度 augmented_notes list(notes) for semitones in [2, -2, 5]: augmented_notes.extend(transpose_notes(notes, semitones)) print(增强后音符数:, len(augmented_notes))移调范围不是越大越好。超过正负 5 个半音后旋律虽然相对关系不变但音域会超出常见乐器的舒服范围模型学到的东西对原数据分布来说已经是“跑偏”的。另一个增强技巧是速度归一化MIDI 里同一个音可能时值长短不一如果你把时值也当成特征输入会让模型纠结于“这个音该拖多长”而忽略旋律本身。常见做法是解析时直接丢弃 duration让模型只学音高序列。我在早期版本里保留过 duration生成结果节奏破碎、不成句后来果断砍掉只做音高序列建模音乐性反而明显提升。3. 搭建一个会写旋律的 LSTMKeras 网络结构与训练参数3.1 为什么在这个场景选 LSTM数据量、记忆长度与软硬件约束面对“音乐生成”这个任务很多人第一反应是上 Transformer。但在 ai_music 这种以本地 MIDI 小数据集为起点的场景LSTM 有它不可替代的位置首先LSTM 的参数量比同规模 Transformer 小一个量级几千首 MIDI 最多也就百万级音符样本Transformer 的自注意力在这种数据量下很难训练充分其次音乐旋律的依赖关系是局部的——前后 16 个音以内的关联占主导LSTM 的隐状态完全能覆盖不需要全局注意力再者LSTM 推理是逐音符生成的显存占用恒定而 Transformer 生成时要缓存整个序列的 K/V序列一长就吃紧。还有一个实际工程问题LSTM 在 CPU 上也能跑出可接受的训练速度而 Transformer 基本是 GPU 专属。对于很多只在本地笔记本上折腾的开发者来说LSTM 是唯一能在晚饭后启动训练、睡前看到结果的选择。我之前在同一份数据上对比过参数量相近的 LSTM 和 Transformer前者收敛到同样 loss 值所需的训练时间大约只有后者的三分之一。当然如果以后数据量到了几万首再迁移到 Transformer 也不迟因为数据预处理和生成流程是通用的只需要换模型主体。3.2 Keras 实现Embedding、双层 LSTM、Dropout 与 Softmax 的完整代码网络结构我按文本生成的标准配方来Embedding 层把音符 ID 映射成稠密向量双层 LSTM 提取时序特征最后一个 Dense 层输出每个音符的预测概率。这里有三处和文本生成不完全一样的地方我会在代码注释里标出来。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam VOCAB_SIZE len(note2idx) EMBED_DIM 128 LSTM_UNITS 256 DROPOUT_RATE 0.3 def build_model(vocab_size, seq_len, embed_dimEMBED_DIM, lstm_unitsLSTM_UNITS, dropoutDROPOUT_RATE): model Sequential([ Embedding(vocab_size, embed_dim, input_lengthseq_len), LSTM(lstm_units, return_sequencesTrue), Dropout(dropout), LSTM(lstm_units, return_sequencesFalse), Dropout(dropout), Dense(vocab_size, activationsoftmax) ]) return model model build_model(VOCAB_SIZE, SEQUENCE_LENGTH) model.compile( losssparse_categorical_crossentropy, optimizerAdam(learning_rate0.001), metrics[accuracy] ) model.summary()几个参数值得单独说。第一Embedding维度和词表大小要匹配词表只有几百个音符时embed_dim128已经很大再往上加只增加参数量不涨效果只有词表上了几千才值得把 Embedding 加到 256。第二双层 LSTM 里第一层设置return_sequencesTrue是为了把每个时间步的隐状态都传给第二层如果直接设 False第二层就只能收到最后一个时间步的信息序列特征会丢一大半。第三sparse_categorical_crossentropy配合整数标签使用不需要对 target 做 one-hot省内存且数值更稳定。Dropout 参数我个人的经验是 0.3 左右表现最好低了抑制不了过拟合高了模型学习的信号太弱。需要注意 Keras 的 Dropout 在 LSTM 层上默认只作用于输出不作用于循环连接内部所以它防过拟合的效果比想象中弱不要为了压 loss 盲目加大。3.3 训练配置早停、学习率衰减与模型保存策略模型结构定好后训练配置里最容易踩的坑是“无脑训满固定 epoch”。音乐数据通常样本间相似度高同一个动机反复出现模型很容易在十几个 epoch 内过拟合——训练 loss 还在降但生成结果是原曲子的机械复读。我一般会配早停和模型检查点回调保存验证集 loss 最低的那版权重而不是最后一版。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience4, min_lr1e-5), ModelCheckpoint( music_lstm_best.keras, monitorval_loss, save_best_onlyTrue ) ] history model.fit( X, y, batch_size128, epochs100, validation_split0.1, callbackscallbacks, verbose1 )EarlyStopping的patience10表示验证集 loss 连续 10 个 epoch 不下降就停。这个数字要配合ReduceLROnPlateau的patience4一起看先每 4 个 epoch 把学习率减半一次如果减了两次还没效果再停掉。这样比单纯早停能多熬过一些 loss 平台期。batch_size128是我调出来的折中值。音乐序列样本之间信息量差异不大batch 太小比如 16会让每个 step 的梯度方向噪声很大loss 曲线抖得厉害batch 太大比如 512则每个 epoch 更新次数太少模型学得慢。如果你的显存有限优先减 LSTM_UNITS 而不是减 batch因为 LSTM 的训练稳定性对 batch 大小更敏感。另外训练集和验证集的切分要用validation_split0.1在全体样本上随机切不要按文件切否则验证集可能整个落在同一个调性的曲子上模型风格迁移能力会被误判。4. 从权重到旋律温度采样、MIDI 还原与生成质量自检4.1 温度采样让模型在“安全重复”和“胡编乱造”之间滑动模型训练完成后生成阶段的核心不是model.predict直接取概率最大的音符而是用温度采样temperature sampling从预测分布里抽样。直接取 argmax 会让生成结果陷入“最高频音符的无限循环”因为音乐语料里某个音总是有最高的条件概率每一步都取它旋律就原地踏步。温度参数的作用是把预测分布压平或拉尖温度趋近 0分布越尖锐生成越保守温度大于 1分布变平低概率音符也有机会被选中生成更随机。def sample_with_temperature(preds, temperature1.0): 从预测分布中按温度采样返回音符索引 preds np.asarray(preds).astype(float64) # 先取 log 再除以温度避免直接除法在温度很低时数值溢出 log_preds np.log(preds 1e-8) / temperature exp_preds np.exp(log_preds) probas exp_preds / np.sum(exp_preds) return np.random.choice(len(probas), pprobas)这里有个小陷阱preds是 softmax 输出值都在 0 到 1 之间直接除以温度再归一化也可以但温度很低比如 0.2时概率之间的差异会被指数放大容易出现 NaN。先取对数再除温度是数值上更稳的做法。1e-8是防止概率为 0 时log报错。温度值怎么选没有绝对标准。我在巴赫风格的数据上0.6 到 1.0 之间效果都不错低于 0.5 会频繁出现同一个音重复四次以上的情况高于 1.2生成旋律开始出现大量不和谐音程。如果你想生成“听起来有点意外但还不至于难听”的旋律建议固定两个温度0.8 和 1.0各生成几首选出相对满意的而不是只跑一次就下结论。4.2 生成长度控制与 MIDI 还原把音符序列变成可播放的文件生成过程就是一个迭代循环用最后SEQUENCE_LENGTH个音符作为输入预测下一个音符采样后 append 到序列末尾再取新的最后 64 个输入继续。这里要注意每次预测的输入窗口必须和训练时一致不能偷懒把整段历史都塞进去——模型的 Embedding 层固定了input_length长度不对会直接报错。def generate_melody(model, idx2note, seed_notes, note2idx, gen_len200, temperature0.8, seq_lenSEQUENCE_LENGTH): # 把 seed 音符转成 ID并确保长度等于 seq_len start_ids [note2idx[n] for n in seed_notes[-seq_len:] if n in note2idx] # seed 不足 seq_len 时用 start token 补齐这里用词表第一个音符 while len(start_ids) seq_len: start_ids.insert(0, 0) generated list(seed_notes[-seq_len:]) for _ in range(gen_len): x np.array([start_ids]) preds model.predict(x, verbose0)[0] next_idx sample_with_temperature(preds, temperature) generated.append(idx2note[next_idx]) start_ids.append(next_idx) start_ids start_ids[-seq_len:] return generated seed notes[-SEQUENCE_LENGTH:] # 用训练语料的最后 64 个音符做种子 melody generate_melody(model, idx2note, seed, note2idx, gen_len200, temperature0.8)seed 的选择是生成质量的隐形变量。用整首曲子开头做种子生成结果会顺着原曲风格延续用一段随机挑选的片段做种子模型的风格约束会弱一些生成更容易跑偏。我一般会在验证集里随机抽几个样本做 seed而不是用训练集里的免得生成的旋律和原曲重合度太高让人分不清是续写还是默写。MIDI 还原是逆向解析过程的镜像操作。把生成列表中形如C4.E4.G4的和弦字符串重新拆成音符对象写入 music21 的 streamfrom music21 import stream, note as n21, chord as c21 def save_midi_from_notes(notes, output_path): s stream.Stream() for n_str in notes: if . in n_str: # 和弦 chord_notes [n21.Note(p) for p in n_str.split(.)] s.append(c21.Chord(chord_notes)) else: s.append(n21.Note(n_str)) s.write(midi, fpoutput_path) save_midi_from_notes(melody, generated_bach_style.mid)这里默认生成的音符时值全是四分音符因为训练时我们丢弃了 duration 信息MIDI 还原时音乐听感会比较机械。改进方式是训练阶段把时长也编码进 token比如把C4:0.5作为一个 token代价是词表体积翻倍。我自己的取舍是先在“单音高序列”上把旋律走向调通再决定要不要加时值避免一上来就两头都学不好。4.3 生成质量快速自检重复率、音域分布与和声合理性生成结果不能只靠耳朵听至少要做三个客观指标的检查不然你无法判断模型改动一个参数到底是变好了还是碰巧好听。第一个指标是相邻音符重复率统计生成的旋律里连续相同音的比例正常应该在 5% 以下超过 15% 说明温度太低或模型欠拟合第二个是音域分布把生成音符的 pitch 值画出来看是不是集中在训练语料的常见音域内如果频繁越过音域边界说明模型还没学会音域约束第三个是和声合理性抽查相邻两个音符的音程如果大量出现增四度、减七度这类在调性音乐里很刺耳的音程温度多半需要调低。def quick_check(generated_notes): from music21.pitch import Pitch pitches [] repeat_count 0 for i, n in enumerate(generated_notes): first_pitch n.split(.)[0] # 和弦取第一个音做粗略统计 pitches.append(Pitch(first_pitch).midi) if i 0 and generated_notes[i] generated_notes[i - 1]: repeat_count 1 repeat_ratio repeat_count / len(generated_notes) midi_array np.array(pitches) return { repeat_ratio: round(repeat_ratio, 3), pitch_range: [int(midi_array.min()), int(midi_array.max())], mean_pitch: round(float(midi_array.mean()), 1) }这个自检脚本不用做成产品功能训练过程中每改一次参数跑一遍能帮你快速判断改动方向对不对。我个人的习惯是每调一轮参数生成 5 首、每首 200 个音符取平均重复率和音域覆盖度做对比比单听一首曲子靠谱得多。毕竟模型生成有随机性单次结果说明不了问题。5. 避坑LSTM 音乐生成最常翻车的 5 个现场5.1 生成的旋律全是休止符或同一个音现象训练 loss 收敛得不错但生成的音符列表里 80% 是同一个音或者大量输出了一个语料里从没见过的“占位符”。原因最常见的是词表构建阶段把rest休止符混进了训练样本而休止符在原语料里占比很高。LSTM 学到的只是“下一个音符大概率是休止符”生成自然全是休止。另一种可能是输入序列里混入了低频音符的 ID 对齐错位模型学到了错误映射。解决解析 MIDI 时显式过滤掉rest元素只保留 note 和 chord。检查build_vocab里有没有把rest也算进去有就删掉。生成时如果发现预测分布里某个音符概率长期超过 0.9打印一下词表里这个 ID 对应的音符是什么多半就是过滤漏了。5.2 训练 loss 不降反升现象前几个 epoch loss 稳步下降到第 10 个 epoch 左右突然开始上升之后一路飙升。原因典型的学习率过大加梯度爆炸。LSTM 的循环连接在长序列上容易累积梯度学习率稍高就会让参数更新越过最优区域进入 loss 上升的恶性循环。如果你的 LSTM_UNITS 设到了 512 以上这个问题会更明显。解决先把learning_rate从 0.001 降到 0.0005 或 0.0003同时在模型里加梯度裁剪。Keras 里可以用Adam(clipnorm1.0)把梯度的 L2 范数限制在 1.0 以内。如果再不行检查输入数据是否归一化——音符 ID 本身是整数但 Embedding 层之后一般不会出问题重点还是先降学习率。5.3 生成旋律“原地踏步”翻来覆去就那几个音现象采样温度已经调到 1.0生成结果还是在一个小节内反复循环整段旋律没有发展。原因这通常是SEQUENCE_LENGTH太短导致的。序列窗口只有 8 或 16 时模型看到的上下文不足以判断“这个乐句应该往下走了”于是最安全的策略就是重复最近出现的音型。另一个原因是训练数据里本身就有大量重复片段比如练习曲里的音阶重复。解决把SEQUENCE_LENGTH提到 64 或 128。同时检查数据增强是不是把移调后的版本重复拼接了太多次让重复段落占了语料主导。数据层面可以按文件去重同一首曲子的多个移调版本在语料里不要连续排布打乱后再送入训练。5.4 MIDI 解析失败或音符顺序错乱现象converter.parse报编码错误或者解析出来的音符列表明显和原曲对不上和弦散成单个音。原因music21 对中文路径和某些软件导出的 MIDI 兼容性不佳文件内部的时间刻度可能不是标准四分音符制。还有一类情况是 MIDI 里同时存在多个轨道且轨道间时间基准不一致flat.notes拉平时顺序就会混乱。解决统一把 MIDI 文件名改成英文路径再解析遇到解析失败的文件用try/except跳过不要中断整个语料构建。时间基准问题可以用score.makeNotation()让 music21 先规范化内部表示再做 flat。如果某类文件的音符顺序持续错乱直接在数据准备阶段就排除少一个文件不影响大局。5.5 训练时间失控模型容量上去但效果没涨现象LSTM_UNITS 从 256 加到 512Embedding 从 128 加到 256训练时间翻了三倍生成质量没有明显提升。原因LSTM 的参数增长速度是隐层维度的平方级512 维单层 LSTM 的参数量是 256 维的四倍。对于几百到几千规模的数据集256 维已经足够容纳旋律的模式复杂度再加容量只是让模型记住更多细节泛化能力反而下降。解决训练时间超过 2 小时且 loss 还在 1.5 以上时先别急着加参数。优先检查数据量够不够、预处理是否正确。一个快速经验法则先跑一个 LSTM_UNITS128 的小模型看生成旋律有没有基本的句读感没有的话问题在数据而非模型容量。6. 从“能跑”到“能听”三招提升生成旋律的质感先别急着追求大模型。我做过最有用的优化是把生成旋律按“乐句”切分再拼接每次生成 16 个音符就停一下从这 16 个音符里随机挑一个作为下一次生成的起点而不是从头到尾连续生成 200 个音。这样每 16 个音符就是独立的“一句”句与句之间的起点变化能带来更多旋律发展而不是让模型一口气“背书”到底。实践下来这种做法比单纯调 temperature 对音乐性的提升更明显。第二招是训练阶段就把时值信息加回来但不用复杂编码——只区分“长音”大于一拍和“短音”小于等于一拍两种 token。比如C4:L和C4:S词表只会膨胀一倍但生成节奏立刻有了长短交替的感觉听感不再是单调的机器节拍器。这一招的代价是数据预处理代码要多写几行但生成的 MIDI 不需要再手动修时值值得做。最后一招是反向验证。把训练集里随机一首曲子切成两半前半段输入模型让它续写后半段然后对比模型输出与原曲后半段的音符重合率。重合率太高说明模型过拟合到了死记硬背太低接近于随机说明欠拟合或者数据量不足。我一般把重合率控制在 15% 到 30% 之间这个区间意味着模型学到了风格规律而不是具体曲子。每次调完模型我习惯用这个方法先验证一遍再决定要不要保留权重。这个过程里我翻车最多的是温度采样和模型结构同步调整——两者都是生成质量的影响因素同时改就分不清是谁的功劳。现在我的习惯是每次只动一个变量这轮调温度下轮调 LSTM 层数每轮把生成结果和训练曲线一起存档。音乐生成本身有玄学成分但把变量隔离开至少能让玄学收敛得快一点。希望这些经验能帮你少走几步弯路跑出自己的第一条像样旋律。本文还有配套的精品资源点击获取