简介:一份面向本科/研究生毕业设计及课程大作业的高分机器学习应用型源码项目,聚焦音乐自动生成软件的算法设计与实现,适合计算机、人工智能、通信、自动化等相关专业学生与开发者借鉴。压缩包内共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.8~1.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.8~3.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-cpu==2.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_sequences=True), # 第一层 LSTM 返回完整序列 Dropout(0.2), # 防止过拟合 LSTM(UNITS), # 第二层 LSTM 只返回最后一个输出 Dense(vocab_size, activation='softmax') # 输出下一个音符的概率分布 ]) model.compile(loss='categorical_crossentropy', optimizer='adam')到这里你要能回答三个问题:为什么第一层 LSTM 开了return_sequences=True而第二层不开?因为堆叠 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, verbose=0)[0] # 得到概率分布 preds = preds / max(TEMPERATURE, 1e-5) # 温度缩放 next_index = np.random.choice(range(vocab_size), p=softmax(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 里一共 8~10 个代码单元格,执行顺序是:导入依赖 → 解析 MIDI → 构建词汇映射 → 滑窗生成训练样本 → 定义模型 → 训练 → 保存模型 → 生成 MIDI → 可视化结果。逐格跑就行,但有两个地方要留意:
第一个是训练格。如果你跑完前面所有单元格,时间已经花了两三分钟,而训练格默认是 50 个 Epoch,在纯 CPU 环境可能等很久。建议第一次体验时把 EPOCHS 改成 10,验证全链路跑通再逐步加。第二个是生成格。生成前有两个参数要手动改:start_sequence的种子来源和生成音符数。种子序列越长越稳,我一般取 100 个 token 作为起点,生成 300~500 个音符就够打印出一段完整乐句。生成之后 Notebook 里有一步把音符流转回 MIDI 文件的逻辑:
midi_stream = stream.Stream() for note_str in generated_notes: # 把 token 解析回音符对象 midi_stream.append(parse_token(note_str)) midi_stream.write('midi', fp='output/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 后被系统杀掉。
原因:数据量过大导致训练样本矩阵爆炸。假设词表大小 1000,SEQ_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_multiprocessing=True),内存占用立刻降到线性水平。这条避坑建议值回票价。
4.5 生成 MIDI 无法播放或音色发闷
现象:生成的 .mid 文件能生成但播放器打不开,或者播放时音色粗糙、音量异常。
原因:回写 MIDI 时音符的起始时间和结束时间没有正确换算。音乐生成内部用的是相对索引(时间步),但写 MIDI 文件需要绝对时间(秒或节拍)。源码里如果时间步到 MIDI 节拍的换算系数不对,音符时长可能变成 0 或负值,格式就坏了。另一个原因是 MIDI 文件的音色库(Program Change)没有被设置,默认音色可能是合成器的默认波形,听感粗糙。
解决:检查回写代码里时长是否做了max(duration, 0.5)下限保护,防止零时长音符写入。音色方面,在写出音符前先追加一个instrument对象,指定为钢琴音色,MIDI Program 编号是 0:
from music21.instrument import Piano midi_stream.insert(0, Piano())这样写出的 MIDI 不管在哪个播放器里打开,都会用钢琴音色而不是默认的合成器音色。别小看这一步,它直接影响评审老师第一耳朵的主观印象。
5. 进阶玩法:把生成器从“能跑”调到“听众愿意听完”
把训练和生成跑通只是拿到了及格分,真正拉开差距的是生成质量的精调。这一章给四条可执行的提升路径,按投入产出比排序。
第一,把温度参数从固定值改成“动态升温”。固定温度下,曲子开头和结尾的随机性是一样的,但人耳对开头的无序容忍度很低、对结尾的平淡更宽容。更合理的方案是开头用低温(0.5~0.7)保证稳定进入主题,中间升到 0.9~1.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, verbose=0)[0] / temp这套逻辑做进设计说明书里,陈述时用“动态温度控制模拟音乐情绪弧线”这个说法,答辩老师会觉得你对生成机制有设计感,而不是调包侠。
第二,用人工规则过滤“反音乐”的采样结果。模型输出的概率分布偶尔会给出不和谐音程(比如连续两个大跳),生成后干预比改模型更省力。我常用的规则是:新采样出的音符与上一个音符若是超过十二个半音的音程跳跃,就重新按概率采样一次,最多重试三次。这个规则本质上是用先验知识给模型结果兜底,几分钟就能写完,却能显著提升旋律的“合理感”。
第三,换一个更适合节奏生成的模型底座。如果你的毕设方向允许改动网络结构,把 LSTM 换成双头输出(一个头预测音高,一个头预测时值)会比现在“音高+时值拼一个 token”的做法更精细。原方案里音高和节奏耦合在一个 token 里,模型的自由度被限制。拆开成两个输出分支,会让模型分别学“下一个音是什么”和“这个音持续多长”,在换用复杂数据集时提升明显。这个属于可选改造,但如果你在论文里写出来,说明你理解原模型的结构瓶颈在哪。
第四,验证生成效果时,做一次“A/B 盲听测试”。这是我自己一直沿用的验收习惯——生成三首不同温度版本的曲子,不告诉你哪首是哪个参数,让三个同学各听三十秒排序。这个方法能真实暴露温度参数对听感的影响方向,也能让你在答辩时说出“我们通过多组温度盲听,最终确定默认生成温度设置为 0.9”这样的结论,比“我猜这个值还行”有说服力得多。
我自己的习惯是:每次换数据集或调参数后,强制跑一遍“训练 → 生成 → 转 MIDI → 盲听”全流程,用标签把温度、数据集、训练轮数记在文件名上,生成结果再不好听也能追溯是哪个变量干的坏事,不用从头猜起。希望这套资源的拆解和踩坑记录能让你少折腾一个晚上,把时间花在真的会让作品变好的事情上。
本文还有配套的精品资源,点击获取