
简介基于LSTM的音乐生成器Python工程面向对深度学习在音乐创作领域应用感兴趣的开发者、学生或研究人员适合作为入门级序列生成项目的参考代码。压缩包共56个文件总计约723KB包含46个MIDI样例乐曲作为训练语料、4个Python脚本用于模型定义、训练、生成和预处理、5张网络结构与乐理说明示意图以及1份readme说明文档目录结构清晰便于对照学习。当前已有181人学习/下载过该资源具有一定的实践参考热度。通过该项目可完整走通从MIDI数据读取与音符序列化、LSTM网络搭建与超参数配置到训练音乐风格模型、再由起始音符序列生成新旋律并输出MIDI文件的全流程同时还能借助图示理解循环神经网络在序列数据上的工作机制适合用来快速上手音乐生成类深度学习方法。1. 用 LSTM 生成音乐的思路旋律就是一条可以预测的时间序列提到 ai_music 和 LSTM很多人第一反应是“让 AI 作曲”但动手后会发现难点不在“网络怎么产生灵感”而在“怎么把音乐变成神经网络能读的数据”。音乐在底层就是一连串事件什么时刻按下哪个音、持续多久、力度多大。LSTM 天然适合这类带时序依赖的序列——前一个音符决定后一个音符的概率这正是旋律展开的基本规律本质上和 LSTM 时间序列预测是同一套框架。下面按音乐生成项目最常见的路径完整走一遍MIDI 解析、序列编码、LSTM 神经网络搭建、训练与温度采样生成最后给出判断过拟合和调听感的验证手段适合想自己跑通音乐生成又不想只调库的 Python 开发者。2. 数据准备把 MIDI 解析成 LSTM 能吃的音符序列2.1 为什么选 MIDI 而不是音频波形音乐生成项目的数据准备第一步不是写模型而是决定喂什么数据。常见做法是用 MIDI 而不用 MP3/WAV音频波形是每秒上万维的连续采样点直接送进 LSTM 需要非常深的网络和相当大的算力而且生成结果往往是模糊噪音而不是旋律。MIDI 是符号化的乐谱数据一个音是一个事件信息密度高LSTM 学的其实是“音高 时值”的转移规律这正是 ai_music 类开源项目几乎都从 MIDI 入手的原因。数据源可以是古典音乐 MIDI 库也可以自己在 DAW 里导出格式不限解析时统一处理即可。2.2 music21 解析与音符 token 设计Python 端最常用的解析库是 music21它把 MIDI 转成乐谱对象后可以用面向对象的接口直接遍历音符和和弦比裸用 mido 处理 delta time 和 events 省事得多。核心思路是把每个事件压成一个可哈希的 token。from music21 import converter from collections import Counter score converter.parse(bach_sample.mid) tokens [] for part in score.parts: # 遍历每个声部 for n in part.flatten().notes: if n.isNote: # 单个音符 tokens.append((n.pitch.midi, int(round(n.quarterLength * 4)))) elif n.isChord: # 和弦压成元组 pitches tuple(sorted(p.midi for p in n.pitches)) tokens.append((pitches, int(round(n.quarterLength * 4)))) print(Counter(tokens).most_common(8))代码逻辑外层循环按声部切分避免多声部混在一起把转移概率学乱flatten().notes拿到该声部所有音符对象isNote和isChord区分单音与和弦。token 的第二个元素是时值quarterLength * 4把四分音符为单位的浮点时值转成以八分音符为基准的整数这样时值也能进词表。最后用 Counter 打印分布是为了确认数据构成如果和弦占比过高或时值种类太少后面要调整切片方式。编码阶段把 token 映射成整数 ID再用固定窗口切训练样本vocab {tok: i for i, tok in enumerate(sorted(set(tokens)))} ids [vocab[t] for t in tokens if t in vocab] seq_len 32 inputs, targets [], [] for i in range(len(ids) - seq_len): inputs.append(ids[i:i seq_len]) targets.append(ids[i seq_len])seq_len 32表示让模型看 32 个音符预测第 33 个滑动步长取 1 可以在数据量有限时最大化样本数。sorted(set(tokens))是为了保证多次运行映射结果一致避免词表顺序漂移影响后续加载。数据量大时这里可以改成torch.utils.data.TensorDataset配合 DataLoader 加载训练速度会明显提升。2.3 词表压缩与数据增强的取舍词表大小直接决定 LSTM 输出层的规模。常见做法是当词表超过 300 时把出现次数少于 2 次的 token 全部合并成unk否则模型会把大量参数花在永远学不会的冷门和弦上训练 loss 看起来很低但生成时净出怪音。数据增强方面最有效的是转调增强把整首曲子平移 ±2 个半音各生成一份等于把训练集扩大三倍对旋律型数据尤其管用。提示MIDI 里连续休止符会让 loss 快速下降但听感很差预处理时建议把连续 4 拍以上的休止压缩成一个 token。3. 模型搭建LSTM 音乐生成网络的层结构与超参数3.1 为什么是 Embedding LSTM Softmax音乐 token 是离散符号不能把整数直接送进 LSTM。Embedding 层把每个 token 映射成可学习的稠密向量让相邻音高在向量空间里天然靠近LSTM 在时间维度上累积上文信息最后的 Linear 加 Softmax 输出下一个 token 的概率分布。这个结构和字符级 RNN 一脉相承是 ai_music 项目最稳的起点。对比 TransformerLSTM 在小数据集上更容易收敛参数少单卡 CPU 也能训练这也是很多音乐生成 demo 选择 LSTM 而不是直接上注意力机制的现实原因。3.2 PyTorch 实现两层 LSTM 生成网络import torch import torch.nn as nn class MusicLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim256, n_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, n_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_dim, vocab_size) def forward(self, x, hiddenNone): emb self.embedding(x) # (batch, seq, embed_dim) out, hidden self.lstm(emb, hidden) # out: (batch, seq, hidden_dim) logits self.fc(out) # (batch, seq, vocab_size) return logits, hiddenforward 返回两个值logits是每个时间步对词表的打分训练时整条序列都要算交叉熵所以直接取全序列输出生成时只需要最后一个时间步用logits[0, -1]。hidden是 (h, c) 二元组生成阶段需要手动把它传回下一轮否则每个音符都是独立计算的长程结构全丢。batch_firstTrue让输入形状统一成 (batch, seq, dim)后续 reshape 不容易出错。参数常用值作用与调整方向embed_dim64-128太小音高关系学不开太大在小数据上过拟合hidden_dim128-256记忆容量token 总量小于 10 万时 256 足够n_layers21 层欠拟合3 层以上在小数据上基本必过拟合dropout0.2-0.4层间与输出前生效设 0 时验证 loss 容易发散3.3 训练循环与交叉熵的细节损失函数用 CrossEntropyLoss注意把 (batch, seq, vocab) 和 (batch, seq) 的维度对齐。下面这段是完整训练循环包含两个关键细节梯度裁剪和分段学习率。optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(80): for xb, yb in train_loader: optimizer.zero_grad() logits, _ model(xb) # (batch, seq, vocab) loss criterion(logits.reshape(-1, vocab_size), yb.reshape(-1)) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() if epoch 30: optimizer.param_groups[0][lr] 3e-4clip_grad_norm_把梯度范数限到 5.0防止反向传播跨 32 个时间步时梯度爆炸在第 30 轮把学习率从 1e-3 降到 3e-4这是 LSTM 类模型最容易见效的调参手段。Adam 默认的 1e-3 对音乐序列生成偏大直接用默认值常常出现 loss 剧烈震荡。batch 大小取 128 或 256 比较稳超过 512 时 loss 曲线会明显变抖原因是音乐序列样本之间统计差异大小 batch 反而相当于隐式正则。4. 训练与生成loss 降到多少能听温度参数怎么调4.1 训练验证分离与 loss 的听感参考线训练前先把数据按 9:1 切成训练集和验证集验证集按曲子切而不是按 token 随机切否则同一首歌的前后片段会同时出现在两边验证 loss 失真。loss 本身不是听感的唯一标准但有一条经验参考线词表 200 左右时训练 loss 掉到 1.5 以下能听出明显旋律结构1.2 以下开始有乐句感低于 0.8 往往意味着模型开始背谱生成的片段会高频复现训练集内容。更可靠的判断是同时盯验证 loss验证 loss 不再下降、训练 loss 还在跌就是过拟合信号此时应该提前保存模型而不是继续训练。4.2 温度采样控制模型的冒险程度生成时不能直接用 argmax——argmax 永远选概率最高的 token结果就是旋律卡在几个音上原地打转。常见做法是把 logits 除以温度 T 再做 softmax然后按概率分布采样def generate(model, seed_ids, length64, temperature1.0, top_k0): model.eval() cur list(seed_ids) with torch.no_grad(): for _ in range(length): inp torch.tensor([cur[-seq_len:]]).long() logits, _ model(inp) logits logits[0, -1] / temperature # 温度缩放 if top_k 0: # 只保留概率最高的 k 个 vals, _ torch.topk(logits, top_k) logits[logits vals.min()] -float(inf) probs torch.softmax(logits, dim-1) nxt torch.multinomial(probs, 1).item() cur.append(nxt) return cur温度越低概率分布越尖锐模型越倾向选高概率音符温度越高分布越平随机性越强偶尔会蹦出训练集里没有的音程。top_k把概率太低的 token 直接排除防止输出刺耳。三个典型取值温度听感表现适用场景0.4-0.6平稳、重复多、接近背诵找风格基准0.8-1.0旋律完整、有变化默认生成1.2-1.5跳跃大、偶有怪音寻找新动机4.3 从 token 序列写回 MIDI 文件生成结果是 token ID 序列需要先反向映射成 (pitch, duration) 元组再写回 MIDI。这里最容易出错的是时值换算from music21 import note, stream, midi idx2tok {i: t for t, i in vocab.items()} s stream.Stream() for tok_id in cur: tok idx2tok[tok_id] pitch tok[0] if isinstance(tok[0], int) else tok[0][0] dur tok[1] / 4.0 # 还原为四分音符单位 s.append(note.Note(pitch, quarterLengthdur)) mf midi.translate.streamToMidiFile(s) with open(output.mid, wb) as f: mf.write(f)写回时dur一定要除以之前整数化乘的 4否则导出的曲子速度会慢四倍。和弦取第一个音先保证听感稳定想完整保留和弦可以把note.Chord写回但初版先跑通单音更利于排查问题。导出的 MIDI 可以直接用 DAW 或music21的show()方法转成乐谱快速检查。5. 进阶验证同模型不同温度批量生成与过拟合排查判断一个 LSTM 音乐模型是否真正学到了结构最快的办法不是反复看 loss而是用同一份 seed 分别以 0.5、1.0、1.5 三种温度各生成 10 首转成 MIDI 后放进 DAW 里叠起来听。0.5 组如果出现大量与训练集几乎一致的乐句说明模型在背谱而不是在学规律1.0 组旋律流畅但缺少惊喜属于正常状态1.5 组还能保持可辨认的调性感说明数据量和 hidden_dim 都留有余量。排查过拟合还有一个可量化的手段把训练 loss 和验证 loss 画在同一张图里观察两条曲线开始分叉的 epoch在那个位置保存模型参数用于最终生成。配合早停通常比训满 80 轮得到的结果稳定得多。另一个容易被忽略的坑是训练时用了 teacher forcing每个时间步都喂真实音符而生成时是自回归两边输入分布不一致。缓解办法是训练时以 10% 的概率把输入 token 替换成模型自己的输出这个技巧对 LSTM 音乐生成的听感提升非常明显。如果验证 loss 一直降不下来优先检查词表里是否混入大量罕见和弦把unk合并阈值从 2 提高到 5词表变小后模型参数更集中loss 往往能直接降 0.1 以上。最后用score.analyze(key)检查十首生成结果的调性分布如果大部分落在同一个调说明模型学到的是局部统计惯性而不是和声规则此时把seq_len从 32 提到 48 或 64让上下文窗口覆盖更长的乐句边界是性价比最高的下一步。本文还有配套的精品资源点击获取