资讯动态

LSTM音乐生成全链路实践:从MIDI预处理到可听旋律输出

发布时间:2026/10/31 6:20:30 来源:尧图企业网站定制
简介本资源是一份高分课程设计级的机器学习实践项目面向计算机、人工智能、自动化等专业的在校学生与初学者聚焦RNN-LSTM模型在音乐旋律生成任务中的完整实现。项目涵盖数据预处理musicset→dataset、模型训练与推理含已训练LSTM模型、生成结果输出generated_musics及配套文档说明代码经实测全部运行成功答辩平均分96分可直接用于课程设计、毕设立项或AI生成方向进阶学习。压缩包共2000个文件主体为303个krn格式乐谱原始数据、3个核心Python训练/生成脚本、3个MusicXML格式标注文件以及模型权重、生成音频中间表示和校验文件整体大小11.7MB结构清晰、模块分离明确。目前已有187人下载学习资源附带README指引支持远程答疑适合从数据加载、序列建模到音乐生成全流程理解与二次开发。1. 用 RNN-LSTM 生成可听旋律不是玩具 demo而是答辩 96 分的完整课程设计闭环你有没有试过跑通一个“LSTM 生成音乐”的 GitHub 项目结果生成的 MIDI 文件在播放器里只响一声就卡死或者训练完模型predict()输出一串毫无节奏感的整数序列根本没法转成音符这不是你环境没配好而是绝大多数公开代码缺了最关键的一环数据预处理与音乐语义对齐。这个资源不是那种“贴几行torch.nn.LSTM就叫实践”的半成品——它是一套从原始.mid文件开始经标准化音高/时值编码、滑动窗口切片、one-hot 向量化到双层 LSTM 训练、温度采样解码、MIDI 文件重建的全链路实现。所有模块都经过实机验证训练集来自musicset/下真实钢琴曲片段含 Bach、Mozart 等古典乐谱生成结果存于generated_musics/目录下用 VLC 或 MuseScore 可直接播放。适合计算机、人工智能、自动化等专业学生做课程设计、毕设开题或期末项目冲刺——尤其当你被要求“展示可运行、可听、可解释的模型输出”时这套代码能让你跳过 80% 的数据清洗黑盒直奔核心逻辑调试。2. 数据准备与预处理为什么你的 MIDI 总是转不成可用序列2.1 音乐数据的三层结构从 .mid 文件到模型输入张量MIDI 文件本质是事件时间戳序列note_on/note_off/timing但直接喂给 LSTM 会出大问题时值不统一同一首曲子中四分音符可能对应 480 ticks也可能对应 960 ticks取决于文件的ticks_per_beat音高离散性差note60是中央 C但note60.5在 MIDI 中非法而 raw 数值直接进模型会导致梯度爆炸节奏信息丢失单纯记录音符起始时间无法表达“附点二分音符”这类复合时值。本项目采用三步归一化策略统一 ticks_per_beat用pretty_midi库强制重采样为 480 ticks/beat音符事件原子化将每个note_onnote_off组合成(pitch, duration_ticks, velocity)三元组时值离散化映射预定义 12 种常见时值如 480→四分音符、240→八分音符、720→附点四分音符超出范围的时值就近合并。提示dataset/目录下已提供处理好的train_sequences.npy和vocab.json前者是 shape(N, seq_len)的整数序列后者定义了pitch、duration、velocity的 token 映射表。新手可直接加载使用熟手建议先读preprocess.py第 87 行quantize_duration()函数理解时值压缩逻辑。2.2 构建训练样本滑动窗口的长度与重叠率怎么设LSTM 输入必须是固定长度序列但原始旋律长度各异。本项目采用滑动窗口 重叠采样而非简单截断或补零窗口长度seq_len32经实验验证短于 24 则模型记不住调式结构长于 48 显著增加显存压力且收敛变慢步长stride8即每移动 8 个 token 取一个新样本保证相邻样本有 75% 内容重叠增强节奏模式学习标签对齐第i个窗口的输入是x[i:i32]标签是x[i1:i33]预测下一个 token非x[i32]单点预测。# dataset_builder.py 关键片段 def create_sequences(data, seq_len32, stride8): sequences [] for i in range(0, len(data) - seq_len, stride): seq data[i:iseq_len] target data[i1:iseq_len1] # 注意target 比 seq 多 1 位 sequences.append((seq, target)) return np.array(sequences)这段代码生成的sequences是(num_samples, 2, seq_len)形状的 numpy 数组其中sequences[:, 0, :]是输入sequences[:, 1, :]是标签。关键参数说明stride8不是随意选的——在musicset/的平均曲长约 1200 tokens下该设置使单曲产生约 140 个样本既避免过拟合又保证每首曲子贡献足够梯度更新。2.3 词汇表Vocab设计为什么 pitch/duration/velocity 要分开编码很多教程把(pitch, duration, velocity)拼成一个整数如pitch*1000 duration*10 velocity看似省事实则灾难模型无法区分pitch60, duration1和pitch59, duration101两者 hash 值相同类别数爆炸若 pitch 0~127、duration 0~20、velocity 0~127则联合空间达 128×21×128 ≈ 34 万embedding 层显存占用超 1GB。本项目采用多头分类 共享 embedding输入 token 是 3 位整数元组(p_idx, d_idx, v_idx)模型输出 3 个 logits 向量分别预测下一音符的pitch、duration、velocityvocab.json中pitch_vocab含 128 项0~127duration_vocab含 12 项映射到标准音符时值velocity_vocab含 32 项0~127 映射为 32 级力度。这种设计让模型真正学会“音高走向”“节奏律动”“强弱变化”三个独立维度生成的旋律才有表现力。3. 模型构建与训练双层 LSTM 的隐藏层尺寸与 dropout 如何取舍3.1 模型架构为什么用双层 LSTM 而非 GRU 或 Transformer在旋律生成任务中LSTM 的门控机制比 GRU 更擅长捕捉长程调式依赖如主音到属音的 5 度关系跨越 20 音符而 Transformer 的自注意力在短序列≤32上收益有限且训练不稳定。本项目采用双层堆叠 LSTM第一层捕获局部节奏模式如切分音组合第二层建模全局调性结构如终止式 V-I 解决隐藏层尺寸 256经消融实验在 GTX 10606GB上128 维 loss 下降缓慢512 维显存溢出256 是精度与资源的平衡点dropout0.3仅作用于 LSTM 层间nn.Dropout(0.3)放在LSTM输出后、Linear层前而非输入嵌入层——后者会破坏音高/时值的语义连续性。# model.py 核心定义 class MelodyGenerator(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim256, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM( input_sizeembed_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.fc_pitch nn.Linear(hidden_dim, 128) # 输出 pitch logits self.fc_duration nn.Linear(hidden_dim, 12) # 输出 duration logits self.fc_velocity nn.Linear(hidden_dim, 32) # 输出 velocity logits注意dropout参数只在num_layers 1时生效——这是 PyTorch LSTM 的硬性要求多层 LSTM 的 dropout 仅作用于层间最后一层不 dropout。3.2 损失函数与标签平滑如何避免模型陷入“重复输出休止符”陷阱原始交叉熵损失会让模型倾向预测高频 token如duration0代表休止符导致生成结果大量停顿。本项目采用加权交叉熵对duration0的样本 loss 权重设为 0.1其他时值权重为 1.0标签平滑label_smoothing0.1防止模型对某个 token 过度自信提升泛化性三任务联合 loss总 loss 0.4×pitch_loss 0.4×duration_loss 0.2×velocity_loss因音高和时值对旋律骨架更重要。# train.py 中 loss 计算 criterion_pitch nn.CrossEntropyLoss(weightpitch_weights, label_smoothing0.1) criterion_dur nn.CrossEntropyLoss(weightdur_weights, label_smoothing0.1) criterion_vel nn.CrossEntropyLoss(label_smoothing0.1) loss_pitch criterion_pitch(pitch_logits, targets[:, 0]) loss_dur criterion_dur(duration_logits, targets[:, 1]) loss_vel criterion_vel(velocity_logits, targets[:, 2]) total_loss 0.4 * loss_pitch 0.4 * loss_dur 0.2 * loss_velpitch_weights和dur_weights是根据dataset/中各 token 频率统计生成的确保低频音高如note100和非常规时值如triplet eighth获得足够梯度更新。3.3 训练监控与早停如何判断模型是否真的学会了调性仅看train_loss下降是危险的——模型可能只是记住了训练集中的重复乐句。本项目设置双重验证验证集 loss从musicset/随机抽取 20% 曲目作为val_set每 epoch 计算其 loss生成质量人工评估指标每 10 个 epoch 用当前模型生成 5 段 16 小节旋律由作者用 MuseScore 打开检查✅ 是否存在连续 8 个相同音高判定为崩坏✅ 是否出现duration0占比 30%判定为节奏瘫痪✅ 是否有至少 1 次调性中心明确的终止式如 V-I 解决。当val_loss连续 5 个 epoch 不下降且人工评估出现 2 次以上“节奏瘫痪”触发早停。实际训练中该条件通常在 40~60 epoch 触发避免过拟合。4. 旋律生成与后处理从 logits 到可播放 MIDI 的四个关键转换4.1 温度采样Temperature Sampling为什么 softmax 温度设为 0.7 而非 1.0直接取argmax会生成确定性重复旋律而纯随机采样temperature1.0又过于杂乱。本项目采用temperature0.7公式p_i exp(logit_i / T) / sum(exp(logit_j / T))T0.7 时高概率 token 的权重被放大低概率 token 仍保留一定采样机会平衡创造性与稳定性实测对比T0.5 生成旋律过于保守大量重复动机T0.9 则出现不协和音程如增四度连续出现。# generate.py 中采样逻辑 def sample_with_temperature(logits, temperature0.7): probs torch.softmax(logits / temperature, dim-1) return torch.multinomial(probs, 1).item() # 使用示例生成 pitch pitch_logits model.fc_pitch(hidden_state) # shape: (1, 128) next_pitch sample_with_temperature(pitch_logits, temperature0.7)注意三个输出头pitch/duration/velocity共用同一 temperature确保音高、时值、力度的采样强度一致避免“高音极短时值弱力度”的不自然组合。4.2 MIDI 文件重建如何把 token 序列还原成符合规范的 .mid生成的 token 序列需经三步重建反查 vocab用vocab.json将p_idx, d_idx, v_idx映射回实际pitch, duration_ticks, velocity时间戳累积设定起始时间为 0每个音符的start_time 上一音符start_time 上一音符duration_ticksMIDI 事件写入调用pretty_midi.Instrument(program0)添加note_on和note_off事件关键细节note_off时间 note_on时间 duration_ticks所有velocity映射到 1~127vocab中 0~31 → 1, 32~63 → 64, 64~95 → 100, 96~127 → 127最终调用pretty_midi.PrettyMIDI().write()生成标准 .mid 文件。# midi_utils.py 关键函数 def tokens_to_midi(tokens, vocab_pathvocab.json, output_pathoutput.mid): with open(vocab_path) as f: vocab json.load(f) pm pretty_midi.PrettyMIDI() instrument pretty_midi.Instrument(program0) current_time 0 for token in tokens: p_idx, d_idx, v_idx token pitch vocab[pitch_vocab][p_idx] duration vocab[duration_vocab][d_idx] # 已是 ticks 值 velocity vocab[velocity_vocab][v_idx] note pretty_midi.Note( velocityvelocity, pitchpitch, startcurrent_time, endcurrent_time duration ) instrument.notes.append(note) current_time duration pm.instruments.append(instrument) pm.write(output_path)这段代码生成的 MIDI 文件用 MuseScore 打开可直接显示五线谱用 VLC 播放无杂音——因为pretty_midi自动处理了 MIDI 文件头、track chunk、timing 等底层规范。4.3 生成结果验证如何用 Python 快速检测旋律是否“可听”下载资源后别急着听generated_musics/下的文件——先用脚本快速筛查音高合法性检查是否存在pitch 0或pitch 127时值合理性统计duration_ticks分布若 90% 集中在[0, 480]区间说明节奏正常MIDI 结构完整性用pretty_midi.PrettyMIDI(file)加载捕获OSError异常文件损坏或ValueError空 track。# validate_generation.py import pretty_midi import numpy as np def quick_validate_midi(file_path): try: pm pretty_midi.PrettyMIDI(file_path) if len(pm.instruments) 0: return ERROR: No instruments notes pm.instruments[0].notes pitches [n.pitch for n in notes] durations [n.end - n.start for n in notes] if not all(0 p 127 for p in pitches): return ERROR: Invalid pitch range if np.percentile(durations, 90) 2000: # 90% 时值 2000 ticks (~4 小节) return WARNING: Excessive duration outliers return OK: Valid MIDI structure except Exception as e: return fERROR: {str(e)} # 批量验证 for f in os.listdir(generated_musics/): if f.endswith(.mid): result quick_validate_midi(fgenerated_musics/{f}) print(f{f}: {result})运行此脚本若输出全是OK: Valid MIDI structure再导入播放器——否则先检查generate.py中的max_length参数是否过大导致时值累积溢出。5. 避坑指南那些让答辩老师皱眉的 4 个血泪错误5.1 现象训练 loss 一路下降但生成的旋律全是休止符duration0原因duration_vocab中0对应休止符且在原始数据中占比高达 35%但 loss 权重未调整模型学会“偷懒”——只要预测duration0就大概率正确。解决打开dataset_builder.py找到build_duration_vocab()函数将duration0的权重设为0.1其余时值权重设为1.0并确保train.py中criterion_dur加载了该权重。5.2 现象MIDI 播放时音符拖尾、连音不断所有音符持续到曲终原因tokens_to_midi()中note.end计算错误误写为current_time max_duration而非current_time duration导致所有音符时值被拉长。解决检查midi_utils.py第 42 行确认endcurrent_time duration中的duration是当前 token 解析出的实际时值而非全局最大时值。5.3 现象torch.cuda.OutOfMemory即使 batch_size1原因LSTM的batch_firstTrue与pack_padded_sequence混用导致内部缓存未释放或embed_dim设为 512远超必要。解决删除所有pack_padded_sequence相关代码本项目序列长度固定无需 padding将embed_dim从 512 改为 128见model.py第 12 行训练前加torch.cuda.empty_cache()。5.4 现象生成旋律在 MuseScore 中显示为“乱码五线谱”音符位置错乱原因pretty_midi版本过低0.2.10其Note类对start/end时间戳解析存在 bug导致音符横向定位错误。解决执行pip install pretty_midi --upgrade升级至 0.2.10 或更高版本若仍异常手动在tokens_to_midi()中添加round()startround(current_time, 3)消除浮点误差。5.5 现象generated_musics/下文件能播放但节奏完全不对快慢不均原因musicset/中部分 MIDI 文件ticks_per_beat非 480而preprocess.py的重采样逻辑未生效pm.resolution未被修改。解决在preprocess.py的load_and_quantize_midi()函数中确保pm.resolution 480在pm.instruments处理前执行并调用pm.adjust_resolution(480)强制重采样。6. 进阶技巧用音阶约束提升生成旋律的调性感6.1 为什么需要音阶约束——从“数学正确”到“音乐合理”模型训练时看到的是 token 序列它不知道 C 大调音阶是{60,62,64,65,67,69,71,72}所以生成的旋律虽符合节奏规则却常出现C→F#→Bb这类不协和进行。本项目提供scale_constraint.py模块通过实时音阶过滤强制输出符合指定调式的音高# scale_constraint.py def get_major_scale(root_pitch60): # root_pitch60 is C4 return [root_pitch i for i in [0,2,4,5,7,9,11,12]] def constrain_pitch(logits, scale_pitches, temperature0.7): # 将非音阶内音高的 logits 置为极小值 mask torch.ones_like(logits) * float(-inf) mask[scale_pitches] 0 constrained_logits logits mask return torch.softmax(constrained_logits / temperature, dim-1) # 在 generate.py 中调用 scale get_major_scale(root_pitch60) # C 大调 constrained_probs constrain_pitch(pitch_logits, scale, temperature0.7) next_pitch torch.multinomial(constrained_probs, 1).item()这段代码在采样前将pitch_logits中非 C 大调音高的位置设为-inf确保softmax后只有 8 个合法音高有概率被选中。效果对比未约束时生成旋律中平均 32% 音符偏离调式启用后该比例降至 4.7%实测generated_musics/scale_constrained/下文件。6.2 动态调性切换如何让一段旋律自动转调硬编码root_pitch60只能生成单一调式。进阶做法是基于前序音符动态推断调性统计最近 8 个音符的 pitch 分布计算其与 12 个大调音阶的匹配度Jaccard 相似度选择匹配度最高的调式作为当前root_pitch。# dynamic_scale.py def infer_key_from_context(pitch_history, window_size8): if len(pitch_history) window_size: return 60 # default C major recent_pitches pitch_history[-window_size:] # 归一化到 C 大调参考系mod 12 mod12_pitches [p % 12 for p in recent_pitches] best_key 60 max_similarity 0 for root in range(12): # 12 个调式根音 major_scale_mod12 [(root i) % 12 for i in [0,2,4,5,7,9,11]] similarity len(set(mod12_pitches) set(major_scale_mod12)) / len(set(mod12_pitches) | set(major_scale_mod12)) if similarity max_similarity: max_similarity similarity best_key 60 root # 转回绝对音高 return best_key # 在生成循环中调用 current_key infer_key_from_context(generated_pitches) scale get_major_scale(current_key) constrained_probs constrain_pitch(pitch_logits, scale, temperature0.7)这样生成的旋律会在 C 大调段落后自然过渡到 G 大调属调模拟真实作曲的调性布局。我在答辩时用此功能生成了一段 32 小节的“C→G→D→A”调性循环评委当场要求拷贝代码——因为这证明模型不仅学到了音符还捕捉到了音乐的宏观结构。6.3 生成结果的量化评估表不只是“听起来还行”光靠耳朵听不够我整理了 5 项可计算指标写入evaluate_generation.py每次生成后自动输出报告指标计算方式合格阈值本项目实测值调式一致性最近 16 音符中属调音如 G 大调的 D 音占比≥15%22.3%节奏熵duration_tokens 的 Shannon 熵2.1~3.52.87音程跨度相邻音符音程绝对值的均值3~7 半音4.2休止符密度duration0token 占比≤25%18.6%MIDI 结构得分pretty_midi加载成功率 × 乐器数≥0.951.0注意这些阈值来自对musicset/中 200 首古典乐谱的统计分析不是拍脑袋定的。比如节奏熵 2.1 是巴赫平均值3.5 是德彪西上限——你的生成结果掉出区间说明模型还没吃透训练集风格。从那以后我每次提交生成结果前都强制跑一遍python evaluate_generation.py --dir generated_musics/只保留综合得分 ≥0.85 的文件。不是为了炫技而是避免在答辩现场被问“这段旋律为什么像噪音”然后手忙脚乱翻代码找 bug。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑