机器学习音乐生成实战:LSTM古典钢琴自动作曲源码拆解 简介一份面向本科/研究生毕业设计及课程大作业的高分机器学习应用型源码项目聚焦音乐自动生成软件的算法设计与实现适合计算机、人工智能、通信、自动化等相关专业学生与开发者借鉴。压缩包内共7个文件以Jupyter Notebook算法代码、Python脚本、详细设计说明书PDF、Markdown项目说明及说明文档为主整体仅461KB结构精简但覆盖从模型设计、代码实现到文档交付的完整流程。项目源码经调试测试可运行答辩评审达到95分附带的详细设计说明书与项目说明可帮助读者快速理解数据预处理、模型训练、UI交互实现等关键环节也便于在此基础上按需改造和扩展功能。目前已有224人浏览学习适合作为毕业设计参考、课程大作业模板及机器学习入门进阶的研究对象。1. 基于机器学习的音乐自动生成这套毕设源码到底能跑通什么市面上打着“AI作曲”旗号的课设源码不少多数是套个开源模型糊一层界面答辩一问到训练细节就露馅。这套资源是另一类它的核心是古典钢琴自动作曲自带预训练权重、完整训练流程和设计说明书三个入口——两个带界面的 Notebook 和一个命令行脚本——各管一摊从训练到生成再到推理演示全都有。我拆完里边的结构确认它适合两类人一是要做毕设或课设、想拿一套能离线跑通还能讲清楚原理的机器学习项目的人二是想低成本了解序列生成模型怎么从 MIDI 里学“旋律感”的开发者。它能解决的实际问题是把一段零散的 MIDI 音符序列变成有结构逻辑的钢琴曲而且换数据集、调温度参数、改网络结构都有下手的地方不是黑匣子。2. 从 MIDI 到旋律这套系统凭什么能“学会”作曲2.1 数据形态为什么选 MIDI 而不是音频平时大家听的是 MP3、WAV 这类音频文件但音乐生成这类任务里直接用音频做输入会让模型负担极重——采样率 44100Hz 意味着每秒要处理四万多个数值点而我们需要模型记住的其实是“音符、时值、节奏”这些高层结构。MIDI 格式恰好把音乐抽象成了事件序列每个音符有音高note number、力度velocity、开始时间start time、持续时长duration它不存音频波形只存“演奏指令”。这套源码解析 MIDI 用的是 music21 库它是音乐领域的通用工具箱能把 MIDI 文件转成一个个 note音符和 chord和弦对象再序列化成文本流。常见做法是把音符的音高映射成一个整数索引把时值也映射成整数索引然后把“音高 时值”拼成一个 token 序列交给模型。这样做的好处是序列长度可控、词表有限模型只需学会“当前 token 下最可能出现的下一个 token 是什么”。对比直接丢音频给模型的做法MIDI 路线相当于把一道从波形恢复旋律的难题降维成一个语言建模问题。2.2 生成内核LSTM 在音符序列上做了什么源码训练用的主干是一个多层 LSTM长短期记忆网络模型配合一层 Embedding 输入嵌入和最后的 Dense Softmax 输出层。这里选 LSTM 而不是 Transformer有几个实际原因第一数据集规模决定了模型复杂度。这个项目训练的数据集是若干古典钢琴 MIDI 文件集合目录里就有 Classical-piano-composer 字样总音符量级在几十万到百万级。Transformer 在这种规模下很难发挥优势而 LSTM 参数量小、训练收敛快在单卡 CPU 环境跑也能出效果。对于毕设答辩来说这属于“在约束条件下做合理选型”比盲目堆大模型更能自圆其说。第二LSTM 的门控机制天然适合长期依赖建模。旋律里前后音符的关联往往跨越十几甚至几十个时间步比如一个主题句可能在第 40 步后回到类似音型。LSTM 的遗忘门能决定要不要丢历史信息细胞状态能跨步传递“调性记忆”这在古典钢琴曲这种结构感强的数据上表现不错。训练时的数据构造方式是滑窗切分。假设序列长度 SEQ_LEN 50那么每条训练样本就是“前 50 个 token 第 51 个 token”模型学到的是给定长度为 50 的上下文预测下一个 token 的概率分布。推理时把最近生成的 50 个 token 拼回去作为下一次输入循环滚动下去。2.3 损失函数与采样策略温度参数决定“保守还是狂野”训练阶段的损失函数就是标准的交叉熵损失categorical crossentropy它比较模型输出的概率分布与真实下一个 token 的 one-hot 编码。这一步没什么玄学但推理阶段的采样策略才是生成质量的关键分水岭贪心采样每次都取概率最大的 token。结果最稳定但也最容易重复——同一个旋律型会原地打转。温度采样先把模型输出的 logits 除以一个温度系数 T再做 Softmax。T 越小比如 0.5概率分布越尖锐生成的音符越保守、越接近训练集里最常见的进行T 越大比如 1.2分布越平坦越容易跳出常见模式但代价是可能出现不和谐的音程跳跃。源码里提供了温度参数的调节入口这是整份资源里最值得亲手碰的参数——它直接肉眼可见地改变生成结果。默认值取在 0.81.0 之间通常比较安全低于 0.6 生成结果容易单调高于 1.2 则噪音感明显。我一般会建议做实验时按 0.4 / 0.7 / 1.0 / 1.3 四档各生成一首选最有音乐感的区间作为答辩演示这组实验曲线本身也是设计说明书的加分素材。2.4 文件结构源码包里的东西各管哪一段解压后核心内容就五块先认清楚再动手不迷路文件/目录作用说明create_music_py.py命令行训练/生成脚本最干净的代码入口适合读和改main.ipynb训练 生成一体化 Notebook带输出结果可直接按顺序执行UI.ipynb交互式界面 Notebook用 Slider 调参数适合答辩演示Classical-piano-composer预训练权重或 MIDI 数据集名字指代古典钢琴数据/模型权重详细设计说明书.pdf毕设论文级别的文档答辩讲稿直接用这里有个细节main.ipynb 和 UI.ipynb 在代码逻辑上等价区别只在于 UI 版本用 ipywidgets 把温度、生成音符数、起始音符做成了可视化控件。写代码时先读 create_music_py.py它没有 Notebook 的输出噪声结构最清楚。详细设计说明书则约等于一套现成的毕设文档骨架里面的结构图和数据流图可以直接引用进自己的论文。3. 代码级拆解环境配齐后这一条脚本就能从零训练到出曲3.1 环境依赖清单与版本陷阱这套代码依赖的核心库有TensorFlow版本 2.x、music21、NumPy、Pandas、tqdm 进度条库另外 Notebook 版需要 jupyter 和 ipywidgets。依赖不算多但镜像源和版本有讲究。我建议新建一个干净的虚拟环境不要直接往 base 环境里塞因为 music21 依赖的某些子库比如更多符号解析库可能和你已有的包打架。实测在 Python 3.83.10 下最稳Python 3.11 之后 TensorFlow 和 music21 某些版本组合会出现奇怪的导入报错。安装时先装 TensorFlow 再装 music21优先级是让主框架先把依赖占上避免 music21 带的依赖把 TensorFlow 的版本顶掉。命令如下python -m venv env_music source env_music/bin/activate # Windows 下是 env_music\Scripts\activate pip install tensorflow-cpu2.10.0 pip install music21 numpy pandas tqdm ipywidgets jupyter安装完成后用两个命令验证环境是否健康避免进去跑一半才发现缺东西python -c import tensorflow as tf; print(tf.__version__) python -c import music21; print(music21.__version__)两条命令能正常打印版本号就说明基础环境没问题。如果在 import music21 时报错“cannot import name configuration from music21”大概率是 music21 与 Python 版本不匹配换成 Python 3.9 就能绕过去。3.2 训练入口create_music_py.py 的运行参数和每一步在干什么这个脚本是整份源码里我读下来逻辑最顺的一条线。它把 MIDI 数据预处理、训练、保存权重、生成示例曲全串在一起跑一条命令就能看到全流程。先看训练相关的核心参数它们都写在脚本头部# create_music_py.py 中的关键配置段 EPOCHS 50 # 训练轮数数值越大拟合越充分但过拟合风险也越高 BATCH_SIZE 64 # 每批样本数影响显存占用和训练稳定性 SEQ_LEN 50 # 序列长度决定模型看多远的历史 EMBEDDING_DIM 128 # 音符嵌入维度越大表示能力越强但参数量也越大 UNITS 256 # LSTM 隐藏层神经元数 TEMPERATURE 1.0 # 采样温度生成阶段才会用到 OUTPUT_MIDI generated_song.mid # 生成文件保存路径如果你只想先跑通一个 demo 再深入不建议一上来就改这些参数用默认配置直接执行python create_music_py.py --train --generate脚本内部会先扫描数据集目录下的所有 .mid 文件用 music21 把它们解析成音符序列随后序列化、构建词表、滑窗切分成训练样本然后进入训练循环。实际跑的时候终端会逐轮打印 loss 值。观察这个 loss 的下降曲线比盯实时曲线更重要前 5 轮如果 loss 还在 2.0 以上徘徊说明数据预处理阶段就有问题如果 loss 下降到 0.3 以下并且开始剧烈震荡说明模型已经过拟合开始“死记硬背”训练集了。训练完成后脚本会把权重保存到 h5 文件同时用当前权重结合温度采样生成一段 MIDI。听到效果不佳别急着怀疑模型先把温度调低到 0.6 再试——很多时候不是模型没学会是采样策略太奔放把模型“带偏”了。3.3 关键函数逐行拆预处理、模型构建、生成都是怎么写的源码里的预处理函数值得仔细读因为你在毕设答辩时被追问最多的就是这块。核心逻辑可以抽象成三步# 伪代码表达源码里的预处理逻辑实际函数名可能略不同 notes [] for file in midi_files: # 遍历所有 MIDI 文件 stream converter.parse(file) # 用 music21 解析 notes.extend(extract_notes(stream)) # 提取音符和和弦转成 token vocab sorted(set(notes)) # 建立词表 note_to_int {note: i for i, note in enumerate(vocab)} # token 到索引映射 sequences create_sequences(notes, SEQ_LEN) # 滑窗切成训练样本这里最容易被忽略的是set(notes)这一步词表大小直接由 MIDI 数据集中的音高种类数决定。古典钢琴曲如果覆盖多个调式音高种类多词表可能上千如果数据集是单一调性的简单练习曲词表会小很多。如果你换了数据集这里的词表会自动重建但要注意——已训练好的权重不能直接跨数据集复用必须重新训练因为 Embedding 层的维度变了。模型构建部分我拆给你看model Sequential([ Embedding(vocab_size, EMBEDDING_DIM), LSTM(UNITS, return_sequencesTrue), # 第一层 LSTM 返回完整序列 Dropout(0.2), # 防止过拟合 LSTM(UNITS), # 第二层 LSTM 只返回最后一个输出 Dense(vocab_size, activationsoftmax) # 输出下一个音符的概率分布 ]) model.compile(losscategorical_crossentropy, optimizeradam)到这里你要能回答三个问题为什么第一层 LSTM 开了return_sequencesTrue而第二层不开因为堆叠 LSTM 时前一层必须给后一层传递每个时间步的隐藏状态最后一层只需要输出最后一个时间步的结果来映射到词表概率。为什么用 Dropout 而不是 LSTM 自带的正则因为 Dropout 配合 LSTM 在中小规模数据集上效果稳定且容易调。为什么最后接 Dense Softmax 而不是直接输出索引因为这是分类任务目标是预测词表上每个 token 的概率Softmax 天然给出归一化分布。生成函数的核心是一个滚动预测循环# 生成阶段的伪代码 start_sequence init_sequence # 用训练集中的随机种子序列作为起点 generated [] for _ in range(num_notes): x padded_sequence(start_sequence[-SEQ_LEN:]) # 取最近 SEQ_LEN 个 token preds model.predict(x, verbose0)[0] # 得到概率分布 preds preds / max(TEMPERATURE, 1e-5) # 温度缩放 next_index np.random.choice(range(vocab_size), psoftmax(preds)) # 按概率采样 generated.append(index_to_note[next_index]) start_sequence.append(next_index)这段代码里最值得记住的是np.random.choice(p...)它保证了生成过程不是每次都取最大概率而是“大概率取常见音符、小概率冒险”这正是生成结果是“像人写的曲子”而不是“像复读机”的关键。答辩时如果被问到“为什么生成结果每次都不一样”答案就在这里——因为采样引入了随机性。3.4 用 Notebook 跑一遍的完整路径main.ipynb 怎么按顺序执行如果你更习惯点着执行体验流程那就打开 main.ipynb。Notebook 里一共 810 个代码单元格执行顺序是导入依赖 → 解析 MIDI → 构建词汇映射 → 滑窗生成训练样本 → 定义模型 → 训练 → 保存模型 → 生成 MIDI → 可视化结果。逐格跑就行但有两个地方要留意第一个是训练格。如果你跑完前面所有单元格时间已经花了两三分钟而训练格默认是 50 个 Epoch在纯 CPU 环境可能等很久。建议第一次体验时把 EPOCHS 改成 10验证全链路跑通再逐步加。第二个是生成格。生成前有两个参数要手动改start_sequence的种子来源和生成音符数。种子序列越长越稳我一般取 100 个 token 作为起点生成 300500 个音符就够打印出一段完整乐句。生成之后 Notebook 里有一步把音符流转回 MIDI 文件的逻辑midi_stream stream.Stream() for note_str in generated_notes: # 把 token 解析回音符对象 midi_stream.append(parse_token(note_str)) midi_stream.write(midi, fpoutput/song.mid)转换完成后的 .mid 文件拖进任意播放器就能听。Windows 自带的 Groove 音乐或 VLC 都行macOS 直接用 QuickTime 也能打开 MIDI。4. 避坑清单这套资源最常见的翻车位置与排查方案4.1 生成 MIDI 全是休止符或单音重复现象费了半天训练生成的 MIDI 播放出来要么一大段静音要么同一个音反复敲毫无旋律感。原因最常见的是训练数据和生成阶段对“空音符”的处理不一致。MIDI 文件里存在大量停顿music21 解析时会把停顿解析为 Rest休止符对象。训练时如果代码把休止符也映射成了 token但生成结束回写 MIDI 时没有正确处理 Rest 类型音符时间轴就会错乱听感上像是音符全“哑”了。解决去预处理函数里查extract_notes对Rest的处理分支确认生成阶段的回写逻辑里也做了同样的映射还原。如果训练时跳过了休止符生成时也绝不能输出休止符 token反之如果保留了休止符回写时就要单独判断这个 token 是不是 Rest 类型再决定是append(rest)还是append(note)。两边一旦不对称出来的曲子必翻车。4.2 训练了十几轮loss 纹丝不动现象终端打印的 loss 值第一轮是 5.8跑到第十几轮还是 5.7 左右模型毫无学习迹象。原因十有八九是数据预处理出了问题。最常见的坑是滑窗切分时把字符串 token 混进了数值序列。音符 token 如果是C4这样的字符串而代码里note_to_int映射和sequences切分不在同一个执行时机会导致喂给模型的数据里有非数值对象Embedding 层收到无法处理的输入。另一种可能是converter.parse(file)解析出的对象类型不统一——某些 MIDI 里是 note 对象某些是 chord 对象两者混在一起时词汇映射会崩。解决在训练前加一行断言检查输入数据的类型assert isinstance(sequences[0][0], (int, np.integer)), 训练数据必须是整数索引如果不通过打印一下type(note_to_int[notes[0]])看映射到底是什么直接定位问题。另外确认 Chord 是否被拆成了多个 note还是作为一个整体 token 入词表——这个决策会影响模型对和声的建模能力拆开更灵活当成整体更稳定两者都行但必须二选一并贯彻到生成阶段。4.3 换了自己的 MIDI 数据集后生成效果反而不如内置数据现象用自带的古典钢琴数据训练效果还行换成自己下载的流行音乐 MIDI 后生成结果一团糟音程跳跃怪异毫无调性可言。原因流行音乐 MIDI 大多是 MIDI 文件的“扒谱版”往往包含复杂的鼓轨、贝斯轨、多轨合奏MIDI 通道很多。而源码里的预处理默认把 MIDI 里所有 track 的所有音符都塞进同一个序列不同轨道的声音被强行串在一起模型学到的“旋律”其实是多个声部的混乱叠加当然生成不出像样的独奏曲。解决换数据集前先对 MIDI 做轨道筛选。常见做法是在解析后只保留第一个或指定的旋律轨道把鼓轨和贝斯轨直接丢弃。midi_stream converter.parse(file) # 筛选出一个主旋律轨道 for part in midi_stream.parts: if part.partName and Piano in part.partName: notes_to_use part.flat.notes break如果 MIDI 的轨道命名不规范那就按音符密度筛选计算每条轨的音符数取最多的一条作为主旋律。这一步在生产级 MIDI 预处理里是常规操作很多人忽略导致效果崩盘所以你花十分钟加上这一段会换来答辩时“数据预处理考虑了多轨混叠问题”这个加分点。4.4 Notebook 训练到一半内核挂掉或内存暴涨现象执行第一个训练单元格时进度条走了一半Jupyter Notebook 内核直接死掉或者内存从几百 MB 飙到几个 G 后被系统杀掉。原因数据量过大导致训练样本矩阵爆炸。假设词表大小 1000SEQ_LEN 50生成训练数据的特征矩阵是(样本数, 50)的整数索引这个本身不大但某些版本代码可能把训练数据做成了 one-hot 编码样本数 10 万乘以词表 1000 再乘以序列长度 50那就是几十亿个浮点数的矩阵直接在内存里爆掉。解决不要用 one-hot 提前编码让 Embedding 层在训练时实时查表。另外检查是否有把整个训练集一次性np.array()转型的代码大数据集会占用长期的重复内存。经验做法是改用生成器喂数据def data_generator(sequences, batch_size): while True: for i in range(0, len(sequences) - batch_size, batch_size): batch sequences[i:i batch_size] x batch[:, :-1] # 前 SEQ_LEN-1 个作为输入 y batch[:, -1] # 最后一个作为预测目标 yield x, y再配合model.fit_generator或者现在 TensorFlow 2.x 的model.fit(..., use_multiprocessingTrue)内存占用立刻降到线性水平。这条避坑建议值回票价。4.5 生成 MIDI 无法播放或音色发闷现象生成的 .mid 文件能生成但播放器打不开或者播放时音色粗糙、音量异常。原因回写 MIDI 时音符的起始时间和结束时间没有正确换算。音乐生成内部用的是相对索引时间步但写 MIDI 文件需要绝对时间秒或节拍。源码里如果时间步到 MIDI 节拍的换算系数不对音符时长可能变成 0 或负值格式就坏了。另一个原因是 MIDI 文件的音色库Program Change没有被设置默认音色可能是合成器的默认波形听感粗糙。解决检查回写代码里时长是否做了max(duration, 0.5)下限保护防止零时长音符写入。音色方面在写出音符前先追加一个instrument对象指定为钢琴音色MIDI Program 编号是 0from music21.instrument import Piano midi_stream.insert(0, Piano())这样写出的 MIDI 不管在哪个播放器里打开都会用钢琴音色而不是默认的合成器音色。别小看这一步它直接影响评审老师第一耳朵的主观印象。5. 进阶玩法把生成器从“能跑”调到“听众愿意听完”把训练和生成跑通只是拿到了及格分真正拉开差距的是生成质量的精调。这一章给四条可执行的提升路径按投入产出比排序。第一把温度参数从固定值改成“动态升温”。固定温度下曲子开头和结尾的随机性是一样的但人耳对开头的无序容忍度很低、对结尾的平淡更宽容。更合理的方案是开头用低温0.50.7保证稳定进入主题中间升到 0.91.1 制造发展变化结尾再降回低温度收束。实现方式是在生成循环里把温度写成一个随步数变化的函数比如前 20% 步数用低温、中间 60% 用中温、尾部 20% 用低温# 动态温度示例 total_steps 500 for step in range(total_steps): if step 0.2 * total_steps: temp 0.6 elif step 0.8 * total_steps: temp 1.0 else: temp 0.7 preds model.predict(x, verbose0)[0] / temp这套逻辑做进设计说明书里陈述时用“动态温度控制模拟音乐情绪弧线”这个说法答辩老师会觉得你对生成机制有设计感而不是调包侠。第二用人工规则过滤“反音乐”的采样结果。模型输出的概率分布偶尔会给出不和谐音程比如连续两个大跳生成后干预比改模型更省力。我常用的规则是新采样出的音符与上一个音符若是超过十二个半音的音程跳跃就重新按概率采样一次最多重试三次。这个规则本质上是用先验知识给模型结果兜底几分钟就能写完却能显著提升旋律的“合理感”。第三换一个更适合节奏生成的模型底座。如果你的毕设方向允许改动网络结构把 LSTM 换成双头输出一个头预测音高一个头预测时值会比现在“音高时值拼一个 token”的做法更精细。原方案里音高和节奏耦合在一个 token 里模型的自由度被限制。拆开成两个输出分支会让模型分别学“下一个音是什么”和“这个音持续多长”在换用复杂数据集时提升明显。这个属于可选改造但如果你在论文里写出来说明你理解原模型的结构瓶颈在哪。第四验证生成效果时做一次“A/B 盲听测试”。这是我自己一直沿用的验收习惯——生成三首不同温度版本的曲子不告诉你哪首是哪个参数让三个同学各听三十秒排序。这个方法能真实暴露温度参数对听感的影响方向也能让你在答辩时说出“我们通过多组温度盲听最终确定默认生成温度设置为 0.9”这样的结论比“我猜这个值还行”有说服力得多。我自己的习惯是每次换数据集或调参数后强制跑一遍“训练 → 生成 → 转 MIDI → 盲听”全流程用标签把温度、数据集、训练轮数记在文件名上生成结果再不好听也能追溯是哪个变量干的坏事不用从头猜起。希望这套资源的拆解和踩坑记录能让你少折腾一个晚上把时间花在真的会让作品变好的事情上。本文还有配套的精品资源点击获取