简介基于机器学习的音乐自动生成软件设计与实现源码包面向计算机、人工智能、自动化及相关专业学生是高分毕设项目适宜作为毕业设计、课程设计或期末大作业的参考。项目源于个人毕业设计答辩评审分达95分代码均经调试测试可正常直接运行。压缩包共7个文件核心部分包括两个交互式笔记本文件分别用于模型训练与界面演示、一个脚本文件实现音乐生成核心算法并配有详细设计说明书、项目说明文档、介绍文本及开源许可证等材料整体体积约461千字节结构清晰易查找。目前已有224人浏览学习。项目围绕音乐自动生成完整流程展开从数据预处理、模型构建训练到音乐输出与界面交互均有覆盖配套的设计文档与源码相互印证便于快速理解系统架构对于有一定基础的开发者还可在现有代码上扩展风格与功能具有较强的学习借鉴和二次开发价值。1. 高分毕设的真相基于机器学习的音乐自动生成软件难点不在“音乐”而在“序列”第一眼看到“基于机器学习的音乐自动生成软件设计与实现”这个题目很多人下意识以为要做的是“用神经网络直接吐出一段音频”这个方向确实存在但对毕设来说基本是深渊要处理采样率、频谱、相位对齐还要面对生成对抗训练的不稳定性一张普通显卡根本扛不住。真正能落地的解法是把问题拆成两步——先把音乐编码成音符序列让模型预测下一个该出现的音符再把模型输出的序列转回MIDI文件。这套路径的好处非常直接训练成本低、可解释性强、答辩现场能当场演示生成一段旋律并导出播放。这个方向也特别适合想“交出一套完整作品”的同学。标题里那串“源码(含项目说明、设计说明书)”已经提示了交付物的形状不只有一个训练好的模型还要有一份能拉开给人讲的设计文档和一个能点按钮操作的软件界面。你不用会乐器不需要懂乐理四级只要会处理序列数据、调一个循环神经网络就能做出听感、逻辑、演示效果都站得住的成品。如果你手里已经有一定深度学习基础又希望作品能在评审批次现场当场跑起来这个方向是性价比很高的选择。2. 技术方案与数据准备把音乐变成机器能读的序列2.1 为什么先选MIDI表示而不是音频波形音频生成最大的坑在于特征空间太大了。一段三秒的音频采样率44.1kHz光是一秒就有四万多个采样点要预测模型稍微收敛不到位出来的就是一团噪声。而MIDI这个世界里音乐被抽象成了“什么时候按下哪个键、按多久、按多重”本质上是一张乐谱。你不需要预测波形只需要预测一串离散的音符编号、时值和休止符这天然就是序列预测模型的输入输出格式。我可以负责任地说把生成目标从音频换成MIDI是这类项目能否在几个月内完工的分水岭。实际做的时候处理MIDI文件不需要自己去解析二进制协议用现成的库就能把轨、通道、音符变成Python对象。常见做法是分两步先读入文件拿到每个轨道的音符列表再按时间轴排序、过滤、切段。下面这个表格供你在开题时按“数据形态”做对比答辩时评委大概率会问为什么不做音频生成这就是你回答的依据。表示方式数据规模训练难度听感还原度软件集成成本音频波形极大每秒数万点高容易训练不稳定最接近真实演奏高要处理播放与存储频谱图大高依赖声码器重建高钢琴卷帘矩阵中等中适合卷积/循环网络能还原旋律和节奏中MIDI符号序列小一个音符一个token低收敛快纯MIDI音色听感偏机械低导出. mid即可播放我一般会直接选第四行。MIDI序列的词汇表通常只有几百个符号而音频的候选值几乎无穷多训练轮数差一个数量级。代价是生成结果听起来像电子琴自动演奏但这完全不影响毕设验收——你交付的是“自动作曲软件”不是“虚拟演奏家”。2.2 数据准备抽取主旋律并切段数据是这次项目里第一个真正的坑。公开的MIDI语料很多但原始文件的轨道乱七八糟有贝斯轨、鼓轨、弦乐铺底轨。如果直接把所有轨道混在一起训练模型学到的是“同时响好几个音”的密集织体生成结果会像按住钢琴不放手。常见做法是只抽取主旋律轨道把伴奏、打击乐全部丢掉。我一般用这个逻辑抽轨import pretty_midi def extract_track_notes(midi_path, program_filter(0, 8)): midi pretty_midi.PrettyMIDI(midi_path) notes [] for inst in midi.instruments: # 跳过打击乐轨只保留钢琴类音色 if inst.is_drum or inst.program not in range(*program_filter): continue for note in inst.notes: notes.append((note.start, note.pitch, note.end - note.start)) break # 常见做法主旋律默认取第一轨 if not notes: return [] notes.sort(keylambda x: x[0]) return notes这段代码做的事是读入一个MIDI文件循环遍历所有乐器轨道把打击乐和音色编号不在0到7范围内的轨道过滤掉然后从剩下的轨里取第一轨作为主旋律。音符信息用三元组保存开始时间、音高、持续时长。取完第一轨就直接break因为公开语料里第一轨通常是主导旋律再往后取反而会把低音伴奏混进来。排序用开始时间这一步很重要。原始MIDI里的音符是按事件顺序混合存储的不排序会导致序列时序错乱。排序之后下一步是切分片段。训练序列不是越长越好太长会让模型学到“某首曲子从头到尾的完整顺序”生成时反而只会复刻训练集中的片段。我一般把数据切成长度可控的短句def make_segments(notes, step0.25, min_seg16): 把音符序列按乐句边界切分产出长度不等的片段 segments [] b_idx 0 for i in range(len(notes) - 1): if notes[i 1][0] - notes[i][0] 4 * step: if i - b_idx min_seg: segments.append(notes[b_idx:i 1]) b_idx i 1 return [s for s in segments if len(s) min_seg]切分逻辑依据的是乐句之间的停顿如果两个音符之间的间隔超过4个时间步就认为这里是一个乐句边界把前面累积的音符存为一个片段。参数step是时间步长0.25秒大约相当于一个16分音符min_seg是最小片段长度低于16个音符的片段信息量太弱直接丢弃。经过这步处理一个包含几百首曲子的语料库能产出上万条短旋律足够训练一个不错的模型。这里有个血泪经验不要拿全曲直接训练曲子太长会让模型把“下一句”学成“下一小节”生成结果在结构上断裂感严重。2.3 三种编码规则的选择从矩阵到词表数据切好之后还要决定怎么把音符“写”给模型。这里有三条路对应三种编码钢琴卷帘矩阵、音符时值词表、相对区间编码。钢琴卷帘是把时间步作为行、音高作为列做成一个二维矩阵模型看到的是一张类似五线谱连续按压的图音符时值词表是把“音高C4持续0.25秒”这种组合编成一个整数token相对区间编码则记录每个音符相对前一个音的音程变化。编码方式信息完整度实现成本生成可控性适合场景钢琴卷帘完整保留节奏与和弦中需要后处理才能变成符号可视化好适合展示训练过程音符时值词表保留旋律线丢失和声低直接输出符号接MIDI方便毕设首选相对区间编码保留音程走向丢失绝对音高低调性容易漂移研究旋律形态用我推荐用第二种“音符时值词表”。它的核心是把一个事件组合映射成整数比如“音高60时值0.25秒”是一个token“音高64时值0.5秒”是另一个token休止符也单独占一个token。这样做的好处是模型输出天然是一串离散ID解码时根据ID反查音符和时值直接就能写MIDI。词表大小大概在几百到一千之间既不会稀疏到学不动也不会大到训练变慢。构建词表时要注意一个容易被忽略的细节时值必须做量化。MIDI里真实演奏的时值可能带有很多小数位比如0.23秒、0.31秒如果不量化成若干固定档位词表会膨胀出几千种组合很多时值只出现一两次模型只能靠死记硬背。常见做法是把时值归一到0.125、0.25、0.5、1.0这些档位上宁可损失一点演奏细节换来的训练稳定性非常值。这一步是我反复踩过坑之后最想让你记住的数据层面的粗糙模型层面的精雕不如数据层面的精确。3. 模型设计与训练让旋律从“随机”变“像样”3.1 模型选型两层长短时记忆网络加注意力的理由选模型的时候评审问得最多的一句话是“你为什么要用这个而不是Transformer”。真实的答案是毕设体量下长短时记忆网络LSTM加一层注意力已经够用而且训练快、可解释性强。音乐生成本质上是一个自回归任务给前面32个音符预测第33个音符。LSTM天然适合变长序列建模它能记住前面几十步的旋律走向而注意力机制负责在历史状态里挑选哪些音更值得参考。两层网络足够拟合旋律这种中等复杂度的规律三层以上在数据量不够时反而容易过拟合。Transformer不是不能用但它的胃口比LSTM大。要学出位置编码和自注意力的稳定模式通常需要更大语料、更长的训练轮数本地训练一张显卡要挂一天对毕设进度很不友好。建议的主体方案是两层LSTM加注意力把Transformer放在“进一步改进”的章节里写。这样答辩时既展示了基础功又体现了探索空间。模型结构上输入是音符token序列经过Embedding层变成向量送入两层LSTM再把每个时间步的隐状态过一个注意力权重最后经过线性层输出下一个token的分布。这里有个关键的调参经验Embedding维度和隐层维度不是越大越好。音乐词表只有几百个token128维Embedding足够表达音色、音高、时值的组合256维隐层是目前性价比最高的区间。参数再往上翻训练时间翻倍生成效果没有可感知的提升。3.2 训练参数与调用细节模型定义写起来并不长核心骨架如下import torch import torch.nn as nn class MusicLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size256, num_layers2): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_size, num_layers, batch_firstTrue, dropout0.3) self.attn nn.Linear(hidden_size, hidden_size) self.out nn.Linear(hidden_size, vocab_size) def forward(self, x): emb self.embed(x) # [batch, seq_len, embed_dim] lstm_out, _ self.lstm(emb) # [batch, seq_len, hidden_size] score torch.softmax(torch.matmul(lstm_out, self.attn.weight.T), dim-1) ctx lstm_out * score.unsqueeze(-1) return self.out(ctx)前向过程分四步先查Embedding表把token变成向量进两层LSTM得到每个位置的状态对状态做一次基于线性层权重的注意力打分再按分数做加权最后接线性层映射回词表大小。dropout0.3是为了缓解小语料下的过拟合训练时不要关掉它否则验证集loss会明显抬升。整个模型参数量在500万上下单张中端显卡可以在半小时内完成一轮训练。训练循环不需要写得多花哨但要盯住两个指标训练loss和验证loss之间的距离。optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss(ignore_indexpad_id) for epoch in range(30): model.train() for batch in train_loader: x, y batch optimizer.zero_grad() logits model(x) loss criterion(logits.reshape(-1, vocab_size), y.reshape(-1)) loss.backward() optimizer.step() # 每个epoch结束记录验证loss连续3轮不降则早停参数上直接用Adam优化器学习率1e-3起步。seq_len我建议用32或64batch_size用64epochs设30到50之间具体看验证集表现。这里最容易翻车的是一上来就用大学习率loss会在高位剧烈震荡看起来像在降但生成结果全是同一个音的重复。正确姿势是先跑一个小数据集比如几十条旋律确认模型能过拟合到接近0再上全量数据调早停。小数据集过拟合是模型正常的证明如果小数据都学不进去问题一定在数据编码而不是模型结构。3.3 生成不是单纯argmax温度采样与重复惩罚训练完成后真正决定听感的是生成阶段的采样策略。很多人第一次写生成代码时直接拿模型输出的最大概率结果当预测值这会让旋律变得极其“保守”句子和句子之间几乎一个模子。正确做法是引入温度采样和核采样让模型在“稳定”和“惊喜”之间找平衡。def sample_next(logits, temperature1.0, top_p0.9, repeat_penalty1.2, seen_tokensNone): logits logits / max(temperature, 1e-6) if seen_tokens: # 对已出现过的token降权抑制无意义重复 for token in seen_tokens: logits[token] / repeat_penalty probs torch.softmax(logits, dim-1) if top_p 1.0: sorted_probs, indices torch.sort(probs, descendingTrue) cumsum torch.cumsum(sorted_probs, dim-1) mask cumsum - sorted_probs top_p sorted_probs[mask] 0 probs torch.zeros_like(probs).scatter_(-1, indices, sorted_probs) return torch.multinomial(probs, 1).item()temperature控制分布的平滑程度小于1会让分布更尖锐生成更保守大于1会让概率摊平生成更跳跃。我实测下来0.8到1.2之间听感最自然很多翻车案例就是把temperature调到了1.5以上生出来的旋律像喝醉的人在乱按琴键。top_p是核采样阈值只保留累计概率到0.9的那部分候选把那些概率极低的尾部噪声直接剔除。repeat_penalty针对的是已经出现在最近上下文里的token把它们的概率除以一个大于1的系数能显著减少“ABABAB”这类死循环但系数不要超过1.3否则模型会被逼着走向低概率的怪异音符反而更难听。生成结束后的解码同样有讲究模型输出的token要按词表反查回音高和时值再按时间轴展开成音符列表。这里我习惯在内存里维护一个绝对时间游标每个音符事件自动累加时值最后用这个列表去构造MIDI文件。写成文件时除了写note_on和note_off消息还必须写program_change消息把音色设置为钢琴否则很多播放器会按默认的合成器音色播放出来的声音可能完全不像预期。4. 从模型到软件把生成过程包成一个能打开的桌面程序4.1 软件形态选择桌面端比命令行更能撑起“设计与实现”到这一步很多人以为作品已经做完了模型训练好MIDI也能导出。但这是“算法Demo”不是标题要求的“软件设计与实现”。评审打开你的交付目录期待看到的是一个能双击打开、有输入控件、有进度反馈、能导出文件的应用。我最推荐的软件形态是桌面端技术栈用Python自带生态就能搭完。为什么不选Web因为你要额外部署一套前端服务答辩现场还得担心浏览器兼容为什么不选Tkinter基础控件确实太丑体验减分。界面功能至少要覆盖四个区域模型选择区、生成参数区、结果预览区、导出操作区。模型选择区可以加载训练好的权重文件生成参数区放温度、生成长度、随机种子这些可调项结果预览区显示生成的音符数量、时长和技术信息导出区负责把结果写成MIDI文件并自动打开系统播放器试听。把这些想清楚之后代码组织就顺了。4.2 目录结构与模块职责软件设计不要全部堆在一个文件里。我最开始做这类东西时也是“单脚本一把梭”写到三百行之后连自己都找不到函数在哪里。一个清爽的目录结构比多写两行说明文档更有说服力music_generator/ ├── app.py # 程序入口负责启动窗口 ├── ui/ │ ├── __init__.py │ ├── main_window.py # 主窗口布局与信号绑定 │ └── param_panel.py # 参数面板组件 ├── core/ │ ├── __init__.py │ ├── dataset.py # 数据解析、切段、词表构建 │ ├── model.py # 模型定义与权重读写 │ ├── generator.py # 生成逻辑、采样、解码、MIDI导出 │ └── player.py # 调用系统播放器预览 ├── models/ # 训练好的权重文件目录 ├── output/ # 生成结果保存目录 ├── docs/ # 设计说明书与项目说明 └── data/ # 原始MIDI语料app.py负责把窗口和后台逻辑串起来核心代码只有几十行。ui目录只放界面组件里面不允许写模型推理代码core目录放所有业务逻辑不导入任何Qt界面类。这个清晰的模块划分是你设计说明书里“系统架构”一章的支撑数据流是从数据集到训练脚本、再到模型参数文件运行期从界面参数传到生成器、再到导出模块。我在模拟项目X中这样组织之后调试时间直接减半。4.3 生成线程与界面交互避免窗口“卡死”桌面软件最容易暴露的问题不是功能缺失而是交互卡顿。如果生成按钮的点击事件里直接跑模型推理主线程会被占住窗口标题栏显示“未响应”这在答辩现场是灾难级的尴尬。解决办法是把生成任务扔到子线程界面线程只负责接收进度信号。用Qt的线程类就够from PyQt5.QtCore import QThread, pyqtSignal class GenerateThread(QThread): progress pyqtSignal(int) finished_ok pyqtSignal(str, int) def __init__(self, generator, seed_notes, length, temperature): super().__init__() self.generator generator self.seed_notes seed_notes self.length length self.temperature temperature def run(self): notes self.generator.generate( self.seed_notes, self.length, self.temperature, callbacklambda p: self.progress.emit(p) ) path self.generator.save_midi(notes) self.finished_ok.emit(path, len(notes))这个线程类把生成逻辑完整封装在run方法里progress信号在生成循环里按百分比向上抛主窗口接到信号后更新进度条。需要注意模型加载不要放在窗口构造函数里因为加载大权重文件同样耗时正确做法是放进线程第一次运行前的准备工作里启动时界面只显示“模型加载中”的提示。随机种子也要顺着线程参数传进去方便复现同一次生成结果——这个参数在写设计说明书时能解释“生成结果的确定性来自哪里”。5. 避坑指南从“能跑”到“能听”这几关必须过5.1 生成的旋律三秒后开始无限循环现象模型生成的旋律前几个音符还算正常之后就开始反复重复同一个短句比如A-B-C-A-B-C不停打转听感非常机械。原因大多是两个叠加在一起生成温度太低导致每次都选最高概率音模型进入了确定性循环或者重复惩罚系数设得过高反而让模型察觉到“换新音会招来惩罚”于是收缩回高概率区间。解决方法是把temperature调到0.9到1.05之间repeat_penalty降到1.1左右同时在后处理里加一条硬规则同一个小节内连续出现3次相同音符时强制从次高概率候选重新采样一次。这个方法我用下来对消掉“复读机”最有效。5.2 音符密度过大听感像乱码现象生成的MIDI播放时音符非常密集一小节里塞了几十个音高低音区乱跳整体像噪音。原因通常是数据切段时只切长度不切音域把伴奏轨的低音音符和旋律轨的高音音符混在了一起另一个原因是训练数据里没有对休止符做统计模型几乎没有学到“留白”的概念。解决分两头数据端在过滤轨道时把音高限制在一个合理范围比如钢琴的常用音区21到108超出这个范围的轨直接丢弃生成端在解码时统计当前音符与前一个音符的音程差超过两个八度的跳跃直接把候选概率压到最低。加了这两个限制之后生成的旋律立刻“能听”了。5.3 导出的MIDI在播放器里没声音现象生成文件顺利保存为.mid用播放器打开后界面在走动但就是没有声音。这个坑很隐蔽原因几乎都出在MIDI文件的事件顺序上只写了音符事件没有写program_change事件播放器的默认音色可能是空的或者note_on的力度参数被写成了0等于所有音符都在无声按键。解决方法是导出前显式设置音色和力度并保证每个note_on都有对应的note_offfrom mido import MidiFile, MidiTrack, Message mid MidiFile() track MidiTrack() track.append(Message(program_change, program0, time0)) # 0号音色即大钢琴 for start, pitch, duration in notes: track.append(Message(note_on, notepitch, velocity80, timeint(start * 480))) track.append(Message(note_off, notepitch, velocity0, timeint(duration * 480))) mid.tracks.append(track) mid.save(output.mid)这段代码把音符列表按时间轴写入MIDI。time参数以480为每拍脉冲数把秒换算成MIDI内部的时间单位。velocity固定80既不显得突兀也不会太小。写完这一步用系统播放器双击就能听到声音这也是答辩演示前一定要自己验证一遍的最后一关。5.4 训练时loss不降或剧烈震荡现象训练开始后loss卡在某个高位不下降或者在两个区间来回震荡看起来永远收敛不了。这个问题的排查顺序应该是先看数据有没有shuffle批次之间如果顺序固定模型会对批次顺序产生依赖再看学习率1e-3是常见上限如果用的是带warmup的调度器前几个epoch的偏高震荡属于正常最后看词表是否严重不平衡如果休止符token占了四成模型很可能只会输出休止符。一个小技巧是先跑几十条数据看能不能过拟合到很低如果连过拟合都不行问题一定出在数据编码或词表构建而不是模型结构。直接改模型往往改不出效果返工查数据才是最快的路。5.5 打包后模型加载极慢现象开发环境里启动软件只需一两秒但打包成应用程序后双击打开要等十几秒才出界面甚至出现“未响应”的假死状态。原因大多数是权重文件被打进了资源压缩包程序启动时要先解压再反序列化耗时自然成倍增加。解决思路是不要把模型文件塞进代码包而是放在程序根目录的models文件夹下随包分发运行时用当前程序所在路径动态定位。这个“路径定位”的设计要用到可执行文件所在目录的解析而不是用当前工作目录因为双击启动时的当前目录可能指向桌面。处理完之后加载耗时能缩短到两秒左右答辩现场体验会好很多。不要小看这几秒现场设备性能本来就是玄学提前把环境适配好才是后悔药。6. 再进一步用风格标签把“生成”变成“可控生成”如果到这里你还想让项目上一个台阶我强烈建议加一个成本极低、答辩收益极高的功能风格条件控制。核心思路是在训练数据的每个片段上打一个风格标签输入给模型时把标签向量和音符Embedding拼接在一起生成时由用户选择风格。你不需要换模型只需要在数据预处理阶段为每条片段增加一个字段比如0代表古典、1代表流行、2代表民谣然后把标签做成一个独热向量在模型输入拼接时扩展维度。这样你的软件就从“一键随机生成”升级成了“选定风格生成”答辩时能直接回应“你的作品与市面免费软件有什么区别”这类问题。实现时只需要在模型前向入口做一次拼接把风格向量复制到序列的每个时间步与Embedding后的音符向量在最后一维上拼接再接LSTM。生成阶段用户选一个风格软件就把对应的标签向量固定传入。训练语料如果没有现成风格标注可以按数据来源目录粗略归类比如从古典合集抽出的标0、从流行合集抽出的标1。不需要精确到子风格一个大概倾向就能让生成的旋律带上明显差异。这个功能展示起来很直观同样的随机种子选古典风格生成的是规整的琶音式旋律选流行风格生成的是朗朗上口的重复副歌结构。评委看到对比基本就能确认你的系统是真的“学”到了音乐特征而不是盲猜。我现在的习惯是所有生成类项目都先把“可控制性”放在功能清单里哪怕只是最粗糙的标签拼接。因为你永远预测不到评审会从哪个角度提问而“可控”这个词能挡住一半以上的范围攻击。把这个思路写进设计说明书的“功能扩展”章节再在答辩现场播放两段不同风格的生成结果这个毕设的完整度就基本锁定了。希望帮到你。本文还有配套的精品资源点击获取