简介这份资源是面向计算机、人工智能、自动化等专业学生与教师的高分毕业设计参考包聚焦基于机器学习的音乐自动生成软件的设计与实现可用于毕业设计、课程大作业或期末项目也适合具备一定编程基础的学习者进阶研究。压缩包共7个文件约461KB包含2个ipynb交互式笔记本、1个py脚本、1个pdf详细设计说明书、1个md项目说明、1个txt介绍及1个license覆盖模型训练、音乐生成逻辑与界面交互等核心环节。该毕设答辩评审分达95分代码均经过调试测试可稳定运行。已有224人学习下载读者可从中获取完整的项目结构、算法实现思路、设计文档与可运行源码既能直接用于学习借鉴也能在此基础上修改调整实现不同的音乐生成功能具有较高的参考与复用价值。1. 从一段旋律到一份毕设音乐自动生成到底在做什么你可能遇到过这种场景导师丢来一句“做个能自动写歌的软件”你打开搜索引擎满屏都是 LSTM、Transformer、MIDI、Music21越看越懵。这个标题——基于机器学习的音乐自动生成软件设计与实现——拆开看其实就三件事用机器学习模型学一段音乐里的规律让它续写出新的音符序列再把这串音符变成能播放、能导出、能演示的软件。它解决的不是“AI 取代作曲家”而是“给定一小段种子旋律自动生成风格相近的后续乐句”适合计算机、软件工程、电子信息类毕业设计也适合想入门序列生成的新手。热搜里“机器学习入门”“毕业设计”“源码”这几个词恰好对应了这类项目的三个真实诉求算法要能讲清楚、系统要能跑起来、代码要能交上去。下面我按自己带过几届毕设的经验把这条路从选型到落地讲透。2. 选型先立住为什么是 LSTM 而不是 Transformer2.1 音乐数据长什么样决定了模型怎么选音乐在计算机里最常见的两种表示是音频波形和符号序列。音频波形是几万赫兹的采样点直接生成计算量大、效果难控符号序列则是 MIDI 事件或音高加时值的离散序列比如“C4 四分音符、E4 八分音符”。毕业设计阶段绝大多数可复现的方案都走符号序列这条路因为数据好找、训练快、结果能用 Music21 直接渲染成五线谱或音频。把 MIDI 拆成序列后问题就变成了“给定前 N 个音符预测第 N1 个音符”这和文本生成在数学形式上是同一类问题。区别在于音乐有两个维度音高pitch和时值duration还有和弦、节拍这些结构约束。所以模型不能只预测一个 token通常要同时预测音高和时值或者把两者拼成一个复合 token。常见做法是把每个音符编码成(pitch, duration)二元组pitch 用 0 到 127 的 MIDI 编号duration 量化成若干档比如十六分音符为一档。这样一条旋律就是一个整数序列喂给序列模型即可。2.2 LSTM 和 Transformer 在毕设里的真实取舍热搜里“机器学习模型”“深度学习模型”“传统机器学习模型”都有人搜说明很多人卡在选型。我的建议很直接本科毕设优先 LSTM理由有三条。第一数据量。Transformer 要出效果通常需要几千到上万首曲子的训练集而 LSTM 在几百首、几千个序列样本上就能收敛出可听的旋律。第二训练成本。LSTM 单卡几十分钟能跑完几十轮Transformer 调参周期长答辩前容易翻车。第三可解释性。答辩老师问“你的模型怎么记住旋律的”LSTM 的隐状态、门控机制好讲Transformer 的多头注意力对本科生来说解释成本高。当然如果你的题目明确要求对比实验可以主模型用 LSTM加一组 Transformer 做对照写进设计说明书里这反而是加分项。选型不是越新越好是越能讲清楚、越能复现越好。2.3 环境与依赖一份能跑通的清单动手前先把环境定死避免“在我电脑上能跑”的玄学。推荐 Python 3.8 到 3.10太新的版本有些音频库会编译失败。# 创建独立环境避免污染系统 Python python -m venv music_env source music_env/bin/activate # Windows 用 music_env\Scripts\activate # 核心依赖音乐解析、深度学习、可视化 pip install music219.1.0 pip install tensorflow2.13.0 # 或 pytorch二选一 pip install numpy pandas matplotlib pip install mido # 轻量 MIDI 读写做数据预处理用这里解释几个关键点。music21负责把 MIDI 解析成音符对象也能把生成结果写回 MIDI 和乐谱mido更底层适合批量读取原始 MIDI 事件速度快深度学习框架选 TensorFlow 还是 PyTorch 看你自己熟悉哪个毕设里两者都能做本文示例用 TensorFlow 的 Keras 接口代码短、易读。版本号不是必须完全一致但music21大版本之间 API 有变动建议锁一个稳定版。提示如果安装music21时卡在matplotlib或numpy编译先单独升级 pip再装预编译 wheel不要硬编译源码。3. 数据管道把 MIDI 变成模型能吃的序列3.1 收集与清洗几百首就够起步数据集来源常见有几类公开的古典钢琴 MIDI 合集、游戏配乐、自己用 MuseScore 手写的练习曲。毕设阶段不需要海量数据300 到 500 首结构规整的曲子就能训出有模有样的结果。关键是清洗脏数据比数据少更致命。清洗要做四件事去掉多轨里非旋律的打击轨、统一量化到十六分音符、过滤掉音符数少于 50 的碎片、把调性差异过大的曲子分桶。最后一点很多人忽略——如果训练集里既有 C 大调又有大量升降号密集的曲子模型会学得很乱。简单做法是统一转调到 C 大调或 A 小调用music21的transpose就能做。3.2 用 music21 解析并构建训练序列下面这段代码是数据预处理的核心把 MIDI 文件夹转成整数序列并保存。import os import numpy as np from music21 import converter, note, chord def midi_to_sequence(file_path): 把单个 MIDI 文件解析成 (pitch, duration) 复合 token 序列 score converter.parse(file_path) tokens [] for element in score.flat.notes: if isinstance(element, note.Note): # 音高用 MIDI 编号时值量化成十六分音符的倍数 dur int(element.duration.quarterLength * 4) tokens.append((element.pitch.midi, max(dur, 1))) elif isinstance(element, chord.Chord): # 和弦取最高音作为旋律代表简化处理 top max(p.midi for p in element.pitches) dur int(element.duration.quarterLength * 4) tokens.append((top, max(dur, 1))) return tokens def build_dataset(midi_dir, seq_len32): all_tokens [] for fname in os.listdir(midi_dir): if not fname.lower().endswith((.mid, .midi)): continue try: all_tokens.extend(midi_to_sequence(os.path.join(midi_dir, fname))) except Exception as e: print(f跳过 {fname}: {e}) # 坏文件直接跳过不要让整个流程崩 # 建立词表把 (pitch, duration) 映射成连续整数 vocab sorted(set(all_tokens)) token2idx {t: i for i, t in enumerate(vocab)} idx_seq [token2idx[t] for t in all_tokens] # 切成长度为 seq_len 的滑动窗口 X, y [], [] for i in range(len(idx_seq) - seq_len): X.append(idx_seq[i:i seq_len]) y.append(idx_seq[i seq_len]) return np.array(X), np.array(y), vocab X, y, vocab build_dataset(./midi_data) print(样本数:, len(X), 词表大小:, len(vocab))逻辑说明midi_to_sequence把每个音符或和弦压成一个二元组和弦只取最高音是为了降低复杂度毕设里够用。build_dataset先遍历所有文件收集 token再统一建词表这样训练和生成时用的是同一套映射。滑动窗口把长序列切成“前 32 个预测第 33 个”的样本seq_len是你要调的第一个参数。参数说明seq_len32表示模型看 32 个音符的历史太小会记不住旋律走向太大训练慢且容易过拟合32 到 64 是常见区间。时值乘 4 是因为按十六分音符量化如果你的数据里有三连音量化会有误差可以改成乘 3 或做更细的量化。3.3 词表大小与序列长度的取舍词表大小直接等于不同(pitch, duration)组合的数量。如果发现词表超过 500说明时值量化太细或数据太杂建议把时值档位压缩到 1、2、4、8 四档。词表越大输出层越宽小数据集上越容易过拟合。我一般会把词表控制在 200 以内方法就是粗量化时值、限制音域在 C2 到 C6。序列长度和批大小要一起看。显存小就把seq_len降到 32、batch 降到 64显存够可以上 64 和 128。这两个参数没有标准答案看你的机器和收敛曲线。4. 模型搭建与训练让 LSTM 学会续写旋律4.1 网络结构嵌入层加双层 LSTM结构不复杂但每一层都有存在的理由。嵌入层把离散 token 变成稠密向量LSTM 层捕捉时间依赖全连接层输出下一个 token 的概率分布。import tensorflow as tf from tensorflow.keras import layers, models def build_model(vocab_size, seq_len, embed_dim128, hidden256): model models.Sequential([ # 嵌入层把整数 token 映射成向量输入长度固定为 seq_len layers.Embedding(input_dimvocab_size, output_dimembed_dim, input_lengthseq_len), # 双层 LSTM第一层返回序列给第二层 layers.LSTM(hidden, return_sequencesTrue), layers.Dropout(0.3), # 防过拟合 layers.LSTM(hidden), layers.Dense(vocab_size, activationsoftmax) # 输出每个 token 的概率 ]) model.compile(losssparse_categorical_crossentropy, optimizertf.keras.optimizers.Adam(learning_rate0.002), metrics[accuracy]) return model model build_model(len(vocab), seq_len32) model.summary()逻辑说明Embedding的input_dim必须等于词表大小output_dim是嵌入维度128 是常用起点。两层 LSTM 比单层更能学到乐句级结构但层数再多在小数据上收益递减。Dropout(0.3)放在两层之间是防止过拟合最省事的手段。输出层用 softmax损失用sparse_categorical_crossentropy因为标签是整数索引而不是 one-hot。参数说明hidden256是隐状态维度显存紧张可以降到 128。学习率 0.002 比默认的 0.001 稍大配合 Adam 收敛更快但如果 loss 震荡就调回 0.001。这些参数在训练脚本里应该集中定义方便你写进设计说明书做对比实验。4.2 训练循环与早停训练不是轮数越多越好过拟合的模型生成出来会原样背训练集。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks [ # 验证损失 5 轮不降就停并恢复最优权重 EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), # 只保存验证集上最好的模型 ModelCheckpoint(best_music_model.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X, y, batch_size64, epochs100, validation_split0.1, # 留 10% 做验证 callbackscallbacks )逻辑说明validation_split0.1从训练数据里切出验证集不用单独准备。EarlyStopping的patience5表示连续 5 轮验证损失不下降就停restore_best_weights保证拿到的是最优那轮而不是最后一轮。ModelCheckpoint把最优模型存盘生成阶段直接加载不用重新训练。参数说明batch_size64和前面的显存建议对应。epochs100是上限实际往往二三十轮就触发早停。如果验证损失一直降但训练损失降得更快说明过拟合加大 Dropout 或减少 LSTM 单元数。4.3 生成阶段温度参数决定旋律的“胆量”训练完只是有了模型生成才是软件的核心功能。生成时每一步都取概率最高的 token 会非常死板引入温度采样能让结果更有变化。import numpy as np def generate_sequence(model, seed_seq, vocab, length200, temperature1.0): 从种子序列出发自回归生成 length 个新 token idx2token {i: t for i, t in enumerate(vocab)} result list(seed_seq) for _ in range(length): x np.array(result[-32:]).reshape(1, -1) # 取最近 32 个作为输入 preds model.predict(x, verbose0)[0] # 温度采样temperature 越小越保守越大越随机 preds np.log(preds 1e-8) / temperature preds np.exp(preds) / np.sum(np.exp(preds)) next_idx np.random.choice(len(preds), ppreds) result.append(next_idx) return [idx2token[i] for i in result] # 用训练集里的一段做种子 seed X[0][:32].tolist() melody generate_sequence(model, seed, vocab, length200, temperature0.8) print(melody[:10])逻辑说明自回归生成就是“预测一个、拼回去、再预测下一个”。result[-32:]保证输入长度始终和训练时一致。温度采样先对概率取对数再除以温度重新归一化temperature1.0是原始分布小于 1 更保守大于 1 更随机。参数说明temperature是生成质量最敏感的参数0.5 到 0.8 之间通常最像人写的旋律超过 1.2 会出现大量不和谐音。length200是生成音符数太长后半段容易跑调可以分段生成再拼接。5. 避坑与排查那些让毕设翻车的细节5.1 生成结果全是同一个音现象不管种子是什么生成出来反复是同一个音高。原因通常是模型没收敛或者词表里某个 token 占比过高导致模型学到“全猜它”就能降低损失。解决检查训练集是否某类音符过多做类别平衡或过滤确认训练轮数够、loss 确实在降把温度调到 0.8 以上再试。5.2 生成的 MIDI 播放没声音现象代码跑通、文件也生成了但播放器打开是静音。原因多半是音高或时值越界比如 pitch 超出 0 到 127或者 duration 为 0。解决在写 MIDI 前加一层校验把 pitch 裁剪到合法范围duration 至少为 1用mido打开生成的文件打印事件确认 note_on 和 note_off 成对出现。5.3 训练 loss 不降反升现象前几轮 loss 下降之后突然飙升甚至变成 nan。原因常见是学习率过大或序列里有异常长的样本。解决把学习率降到 0.001 或加梯度裁剪检查预处理阶段有没有产生超长序列给序列长度设上限确认输入没有 NaN。5.4 换一台电脑就跑不起来现象在自己机器上正常拷到同学或答辩机器上报错。原因通常是依赖版本不一致或路径写死。解决把依赖写进requirements.txt并锁版本所有文件路径用相对路径或os.path.join模型文件用save和load_model成对使用不要跨框架版本加载。5.5 答辩被问“创新点在哪”答不上来现象功能能演示但讲不出和现成方案的差别。原因是没有做对比或没有量化评估。解决至少准备两组实验比如不同seq_len或不同温度下的生成结果对比用一个简单指标比如生成序列与训练集的重复率证明模型不是死记硬背。这些数据写进设计说明书比空谈“使用了深度学习”有说服力得多。6. 从能跑到好用评估、导出与一个可演示的收尾技巧模型能生成旋律只是及格线毕设要拿高分得让结果可评估、可导出、可演示。评估这块别只靠“我觉得好听”准备两个可量化的角度一是重复率统计生成序列里 n-gram 与训练集的重合比例太高说明在背数据太低说明没学到风格二是音程分布把生成旋律的音程直方图和训练集对比形状接近说明风格一致。这两个指标用几十行 numpy 就能算写进设计说明书是实打实的分析。导出环节把 token 序列还原成 MIDI 和乐谱这是软件“能用”的关键一步。from music21 import stream, note, chord, tempo def tokens_to_stream(tokens, bpm100): 把 (pitch, duration) 序列还原成 music21 乐谱流 s stream.Stream() s.append(tempo.MetronomeMark(numberbpm)) for pitch, dur in tokens: # duration 是十六分音符倍数quarterLength 要除以 4 n note.Note(pitch) n.duration.quarterLength dur / 4.0 s.append(n) return s score tokens_to_stream(melody) score.write(midi, fpgenerated.mid) # 导出 MIDI score.write(musicxml, fpgenerated.xml) # 导出可编辑乐谱逻辑说明tokens_to_stream把预处理时的量化逆运算回去dur / 4.0对应前面乘 4 的操作两边必须一致否则时值全错。导出 MIDI 用于播放导出 MusicXML 用于在 MuseScore 里打开修改答辩时展示乐谱比只放音频更直观。参数说明bpm100是播放速度可以做成界面上的滑块让用户调。如果你的软件有 GUI把生成、播放、导出三个按钮接上这三个函数就是一个完整闭环。最后一个技巧也是我踩过坑才明白的种子旋律的选择比模型参数更影响观感。用一段节奏规整、音域适中的种子生成结果明显更顺耳随手截一段密集的快速音阶做种子生成出来往往一团乱。演示前多试几个种子挑一个稳定的存成默认值比现场调参靠谱得多。我现在的习惯是任何生成类项目都先固定一个“演示种子”把变量控制住再去讲模型有多强。希望帮到你。本文还有配套的精品资源点击获取