简介:一套面向计算机、人工智能、自动化等相关专业学生的高分毕设源码,主题是基于机器学习的音乐自动生成软件设计与实现。项目来自个人毕业设计,答辩评审达95分,代码经调试测试可正常运行,既适合小白入门学习,也可作为课程设计、课程大作业或期末大作业的参考模板。压缩包内共7个文件,主要包含Jupyter Notebook交互式源码、Python脚本、项目说明文档、详细设计说明书PDF及License等,Notebook用于模型训练与界面演示,Python脚本负责音乐生成逻辑,PDF说明书则完整阐述系统架构与设计思路。整个资源包约461KB,体量紧凑,便于快速下载与本地复现。目前已有224人浏览学习,对于希望快速搭建音乐生成项目、理解机器学习落地流程的读者具有不错的借鉴价值。读者可在此基础上修改参数或调整模型,拓展出个性化功能。
1. 拿到这份“机器学习+音乐自动生成”毕设源码包,先弄清它该解决什么问题
一个标题里同时出现“机器学习”“音乐自动生成”“软件设计与实现”和“设计说明书”,说明它并不是一个玩具 demo,而是一套完整可交付的毕设工程:从数据预处理、模型训练、推理生成,到代码说明和论文支撑材料都齐。你要做的事不是把 zip 解压后跑通一次,而是搞清楚这套软件的设计边界——它能生成什么形式的音乐、模型在哪一层介入、哪些参数决定作品听感,然后才有可能在开题、中期、答辩三个节点都有话可说。
这套方案最常见的落地形态是:用 LSTM/GRU 这类序列模型学习 MIDI 事件的上下文关系,让模型在给定前面若干音符的前提下,逐步预测下一个音符或休止符。选 MIDI 而不是音频波形,是因为 MIDI 是符号域表示,状态空间小,训练成本低,生成结果还能转成五线谱做可视化验证,天然适合毕设的“软件设计与实现”定位。适合谁读:准备做音乐生成方向毕设、想复现一套完整应用流程的学生,以及想快速评估“机器学习生成音乐”到底能做到什么程度的人。读完你应该能自己搭出一套从 MIDI 到新旋律的最小闭环,并知道哪些环节决定成败。
2. 选型先于编码:为什么 MIDI 序列生成比波形生成更适合毕设落地
2.1 音乐生成的两种表示:MIDI 符号域 vs 音频采样域
音乐自动生成在技术路线上有两条大路:一条是直接生成波形采样点,另一条是生成 MIDI 事件序列。前者以 WaveGAN、SampleRNN 为代表,输出是 16kHz 或 44.1kHz 的 PCM 样本,文件大、训练慢,一张消费级显卡跑一个像样的模型要按天计算,而且评估难度极高——你很难说清楚一段噪声和一段实验音乐谁更“对”。后者把音乐抽象成音符、时值、力度、踏板等离散事件,输出是一段可以转成 .mid 文件的事件流,再由合成器渲染成音频。
对毕设来说,音频域生成有三个硬伤。第一,数据量门槛高,公开的标注音频数据集动辄几十 GB,处理链路长;第二,模型黑盒程度高,答辩时很难直观解释网络内部学到了什么;第三,生成质量不好量化,靠耳朵验收容易让评价标准变成“玄学”。MIDI 域生成的输出是符号序列,可以精确对照原曲统计音高分布、节奏密度、旋律重复率,这些指标放进设计说明书里就是硬支撑。所以如果你的题目是“音乐自动生成软件设计与实现”,常见做法是选 MIDI 事件序列生成,而不是直接端到端产音频。
我一般会把系统拆成三层:数据层负责解析 MIDI 并编码成 token 序列;模型层用序列模型学习条件概率分布;应用层负责采样生成、MIDI 合成和播放。这三层对应到设计说明书里正好是“数据预处理”“模型设计”“软件实现”三章,结构清晰,工作量也容易量化。
2.2 为什么 LSTM/GRU 在这个任务里比 Transformer 更划算
你可能想问:现在序列建模不是 Transformer 的天下吗?确实,Transformer 在长程依赖上更强,但音乐生成这个任务有一个特殊性质:局部连贯性比全局精确性更影响听感。一段旋律听起来“对不对”,主要看相邻音符的音程关系、节奏是否在合理范围、乐句是否重复,这些都属于局部上下文,RNN 的门控机制足够捕获。另外 MIDI 事件序列通常在几百到几千 token 之间,Transformer 的全局注意力优势在小序列上不明显,训练却要更大的数据量和更长的调参周期。
从软件工程角度,LSTM 的好处是部署简单、CPU 也能跑推理、训练收敛快,这对毕设的周期非常友好。GRU 比 LSTM 参数少约三分之一,在 MIDI 生成这类中等规模数据上往往效果接近,我会优先选 GRU 或单层 LSTM,把省下来的参数留给嵌入层和注意力机制。当然,如果你想把模型容量作为设计说明书里的亮点,可以在 LSTM 上层加一个小型 attention 层,不用上完整 Transformer 结构。
这里有一个容易被忽略的“数据处理是什么”层面的问题:MIDI 事件序列不是自然语言,没有分词器可用,你必须自己定义 token 表。常见做法是用三类 token:note_on(音高 0-127)、note_off(音高 0-127)、time_shift(相对上一个事件的时间偏移,按 16 分音符粒度分桶)。这样一张 128 + 128 + N 的词汇表就够用,简单直接,且天然支持复调——同一时刻多个 note_on 可以连续排列。
2.3 一个能写进设计说明书的系统架构:事件序列 + 条件生成
整个应用的流程可以写成下面这样:训练阶段从 MIDI 文件集里提取事件序列,按窗口切分生成训练样本,输入一个事件 token 序列,输出预测下一个事件的概率分布;推理阶段给定一段起始种子序列,采用带温度的采样策略逐 token 生成,再把生成的 token 序列写回 MIDI 文件。这个流程具备一个完整“机器学习应用流程”的标准形态:数据预处理、特征编码、模型训练、推理解码、结果存储。
如果你还想加一点创新点,可以在条件生成上下功夫。常见做法是给模型输入一个额外的控制向量,比如代表“欢快/舒缓/悲伤”的情绪 one-hot 编码,或者代表“古典/流行/爵士”的风格编码,让同一个模型在推理时生成不同倾向的旋律。条件 LSTM 的改造很简单:把控制向量拼接到每个时间步的输入嵌入上,或者作为 LSTM 初始状态的一部分。这个改动成本低,但能明显提升项目的差异化程度,答辩时也更容易讲清楚设计动机。
下面这张表是我常用的技术选型参考,可以直接放进设计说明书的方案对比章节:
| 决策点 | 推荐选择 | 理由 |
|---|---|---|
| 生成表示 | MIDI 事件序列 | 数据小、可评估、可视化方便 |
| 基础模型 | 双层 LSTM 或 GRU | 训练快、CPU 可推理、局部连贯性强 |
| 注意力增强 | 轻量 attention | 提升长乐句连贯性,不过度增加参数量 |
| 采样策略 | temperature + top-k | 平衡随机性与稳定性,避免死循环 |
| 输出格式 | Standard MIDI File | 通用性强,可转五线谱和音频 |
3. 把 MIDI 变成训练样本:解析、编码与增广的三个关键处理
3.1 MIDI 解析:别用管道符硬拆,事件流才是正路
很多新手拿到 MIDI 数据后的第一反应是找某种“MIDI 转文本”的工具,然后按行解析。如果你只是做速度分析,那没问题;但要生成新旋律,你需要的是精确到 tick 的事件序列,包括 note_on、note_off、time_shift 的相对位置。这里我推荐用 mido 这个 Python 库,它把 MIDI 文件解析成 message 列表,每个 message 自带 type、note、velocity、time 字段,直接用循环读取即可。
import mido def midi_to_events(midi_path): """ 解析 MIDI 文件,返回 (type, note, velocity, time) 原始事件列表。 time 单位是 tick,需要结合节拍信息换算成相对时长。 """ mid = mido.MidiFile(midi_path) events = [] for track in mid.tracks: tick_cursor = 0 for msg in track: if not msg.is_meta and msg.type in ('note_on', 'note_off'): tick_cursor += msg.time # 过滤力度为0的note_on,它等价于note_off if msg.type == 'note_on' and msg.velocity == 0: events.append(('note_off', msg.note, 0, tick_cursor)) else: events.append((msg.type, msg.note, msg.velocity, tick_cursor)) return events, mid.ticks_per_beat这段代码的逻辑要点有三个。第一,每个 track 内部的 msg.time 是增量值,所以要累加到 tick_cursor 上得到绝对时间位置;第二,MIDI 规范里 velocity=0 的 note_on 事件等同于 note_off,不处理会导致音符一直挂着不结束;第三,不同轨道的绝对时间是相对各自轨道起点的,后续处理时需要按 tick 排序合并。参数上,ticks_per_beat 通常从文件头读取(常见值是 480 或 960),它决定了你后面 time_shift 分桶的粒度。
3.2 事件序列编码:note_on / note_off / time_shift 三种 token 怎么排
拿到原始事件流后,下一步是转成模型能读的 token 序列。最稳妥的编码方式是谷歌 Magenta 的 Performance RNN 方案:先用 note_on 和 note_off 表示音符的起止,再用 time_shift 表示时间推进。排序规则是:同一 tick 的所有 note_on 先排,note_off 次之,然后插入一个 time_shift token 表示推进到下一个有事件的 tick。
def events_to_tokens(events, ticks_per_beat, shift_steps=16): """ 将时间戳对齐后的 MIDI 事件转换为 token 序列。 shift_steps 表示每个 time_shift token 对应的 tick 数。 """ # 先按绝对 tick 排序 events: (type, note, velocity, abs_tick) events.sort(key=lambda x: x[3]) tokens = [] last_tick = 0 tick_per_shift = ticks_per_beat // shift_steps # 每个 shift 代表1/16拍 for ev_type, note, vel, abs_tick in events: # 计算与上一个事件的 tick 差距,转成整数个 time_shift token gap = abs_tick - last_tick if gap > 0: shift_count = int(round(gap / tick_per_shift)) if shift_count > 0: tokens.extend([f"time_shift_{shift_steps}"] * min(shift_count, 8)) if ev_type == 'note_on': tokens.append(f"note_on_{note}") else: tokens.append(f"note_off_{note}") last_tick = max(last_tick, abs_tick) return tokens这里有个很容易踩的细节:gap 除以 tick_per_shift 后要取整,而不是用整除,否则大量非整数偏移会被直接截断,导致生成旋律的节奏和原曲对不上。time_shift token 的数量要设个上限,比如这里限制 shift_count 最大为 8,防止个别文件中出现过长的休止符把序列撑爆。每个 token 的格式统一为“类型_数值”字符串,之后再用一个字典映射到整数 id。
3.3 序列切分与增广:滑动窗口与转调,顺带解决静音段塌缩
编码完成后,每个 MIDI 文件是一长串 token,比如一个 30 秒的钢琴曲大约产生 300-800 个 token。训练时不能把整条序列直接扔进 LSTM,因为长度太长、内存不够而且梯度不稳。常见做法是滑动窗口切分:窗口长度定在 128 或 256 token,步长设为窗口的一半,这样相邻窗口有重叠,不至于把乐句切断到难以辨认。
def build_samples(token_ids, seq_length=128, step=64): """ 用滑动窗口把 token id 序列切成 (输入, 目标) 训练对。 每个样本的输入是前 seq_length 个 token,目标是右移一位的序列。 """ samples = [] for i in range(0, len(token_ids) - seq_length, step): input_seq = token_ids[i:i + seq_length] target_seq = token_ids[i + 1:i + seq_length + 1] samples.append((input_seq, target_seq)) return samples这个代码没有直接用 token_ids 的长度整除,而是保证了最后一个样本不足窗口时直接丢弃,避免 padding 带来的学习偏差。seq_length 取 128 时,LSTM 的时间步长不算太长,梯度传播也能照顾到前面 10 个音符左右;step=64 让相邻样本共享半段上下文,相当于一种序列层面的过采样,数据量不够时很好用。
数据增广方面,MIDI 最有效的方法是转调:把整段旋律的音高整体上移或下移若干半音,常见范围是 -5 到 +5 半音。转调后的旋律和声关系不变,但模型见过了不同音域,泛化能力会好不少。实现上只需要在编码阶段对 note_on 和 note_off 的 note 值做加减,注意过滤掉超出 0-127 范围的音符。
提示:如果训练数据里 MIDI 文件带有大量鼓轨或低音轨,建议直接过滤掉通道 9(打击乐通道)和音高低于 36 的消息,否则生成结果会出现大段低音噪声。
4. 训练一个能“接着写旋律”的 LSTM:模型结构、损失与采样策略
4.1 模型定义:Embedding + 双层 LSTM + Attention 的边界设定
关于模型结构,我给出的方案是:Embedding 层把 token id 映射成 256 维向量,然后过两层 LSTM(每层 hidden size 512,dropout 0.3),再接一个轻量 attention 层,最后是 Dense + Softmax 输出词表大小的概率分布。之所以不堆更多层,是因为 MIDI 数据量通常不大,层数加深容易过拟合到训练集的音型,反而丢失泛化能力。
import tensorflow as tf def build_model(vocab_size, embed_dim=256, lstm_units=512, dropout=0.3): """ 构建用于 MIDI token 预测的 LSTM 模型。 输入形状: (batch, seq_len),每个位置的值为 token id。 输出形状: (batch, seq_len, vocab_size),每个位置为下一个 token 的概率分布。 """ inputs = tf.keras.Input(shape=(None,), dtype=tf.int32) x = tf.keras.layers.Embedding(vocab_size, embed_dim)(inputs) x = tf.keras.layers.LSTM(lstm_units, return_sequences=True, dropout=dropout)(x) x = tf.keras.layers.LSTM(lstm_units, return_sequences=True, dropout=dropout)(x) # 轻量 attention:对每个时间步的输出做加权融合 score = tf.keras.layers.Dense(1, activation='tanh')(x) # (batch, steps, 1) weights = tf.keras.layers.Softmax(axis=1)(score) context = tf.keras.layers.Multiply()([x, weights]) outputs = tf.keras.layers.Dense(vocab_size, activation='softmax')(context) model = tf.keras.Model(inputs, outputs) return model参数选择的逻辑:embed_dim 256 在词表规模 300-500 时已经足够表达 token 语义;LSTM units 512 是时间步 128 下的平衡点,太小记不住长乐句,太大训练时间成倍上涨;dropout 0.3 是针对小数据集的保守值,如果训练 loss 降得慢可以降到 0.2。attention 层这里只做了一个简单的单头加权,核心目的是让每个位置的输出能“看”到前面所有位置的隐藏状态,而不是只依赖最后一个 LSTM 输出。损失函数用标准 categorical crossentropy,优化器用 Adam,初始学习率 1e-3。
4.2 训练循环:要不要用数据生成器,batch 与学习率怎么定
有了样本之后,如果直接把所有样本一次性加载到内存再model.fit,数据量小的时候没问题,但样本数过万后内存压力会变大。更稳妥的方式是用tf.data.Dataset.from_generator写一个生成器,每次产生一个 batch 的随机窗口样本。这样还可以顺带做样本级的随机偏移,让同一个 MIDI 的不同片段都有机会被看到。
def make_dataset(token_ids_list, seq_length=128, batch_size=128): """ 从多个序列的 token 列表构建 tf.data.Dataset。 每个 epoch 内随机采样窗口起点,增加样本多样性。 """ def gen(): while True: seq = random.choice(token_ids_list) if len(seq) <= seq_length + 1: continue start = random.randint(0, len(seq) - seq_length - 1) input_seq = seq[start:start + seq_length] target_seq = seq[start + 1:start + seq_length + 1] yield input_seq, target_seq dataset = tf.data.Dataset.from_generator( gen, output_signature=( tf.TensorSpec(shape=(seq_length,), dtype=tf.int32), tf.TensorSpec(shape=(seq_length,), dtype=tf.int32), ) ).batch(batch_size).prefetch(tf.data.AUTOTUNE) return dataset这里的核心技巧是while True无限生成,配合model.fit(..., steps_per_epoch=...)控制每轮迭代次数。batch_size 128 是在 8GB 显存下的合理值,如果你的显卡只有 4GB,建议降到 64;steps_per_epoch 设为样本总数除以 batch_size 的整数倍,一般 200-500 步即可。学习率方面,我习惯训练前 20 个 epoch 用 1e-3,之后每 10 个 epoch 降到原来的 0.5,这对 RNN 类模型往往能稳定收敛。如果训练中 loss 出现 nan,先检查学习率,如果没问题再检查样本里是否有超出词表的 token id。
注意:不要用
model.fit默认的 shuffle=True,因为生成器本身已经是随机采样,再做 shuffle 会打乱时间顺序特征,没有任何好处。
4.3 生成策略才是作品感的来源:temperature 与 top-k 采样
训练完成后,模型只是给出了“下一个 token 的概率”,真正决定生成旋律是否好听的是采样策略。这里最常用的两种方法是最小化 loss 的贪心搜索和带温度的随机采样。贪心搜索会反复选择概率最高的 token,结果就是旋律循环重复,两三个小节后就卡死;纯随机采样则会让旋律失去结构感。平衡方案是 temperature + top-k 采样:先把概率分布除以温度然后重新 softmax,再从概率最高的 k 个 token 里随机抽。
def generate_tokens(model, start_tokens, length=256, temperature=0.9, top_k=5): """ 从种子序列开始逐步生成 token。 temperature 控制随机性,top_k 控制候选范围。 """ generated = list(start_tokens) input_seq = start_tokens[-128:] # 只取最后128个token做上下文 for _ in range(length): pred_logits = model.predict(np.array([input_seq]), verbose=0)[0, -1, :] pred_logits = pred_logits / temperature # top-k 过滤:只保留概率最高的 k 个候选 k = min(top_k, len(pred_logits)) top_k_indices = np.argsort(pred_logits)[-k:] mask = np.full_like(pred_logits, -1e9) mask[top_k_indices] = pred_logits[top_k_indices] probs = tf.nn.softmax(mask).numpy() next_token = np.random.choice(len(probs), p=probs) generated.append(next_token) input_seq = input_seq[1:] + [next_token] return generated这个函数的关键参数有三个。temperature 在 0.8 附近时生成旋律比较保守、可预测;1.0 开始有变化但偶尔出怪音;超过 1.2 会明显散架。top_k 通常取 5 到 8,太小旋律太“碎”,太大会重复。一个实用的调参技巧是:先用 temperature=1.0、top_k=8 生成十段样本,挑出节奏感最好的那段,再降低 temperature 到 0.85 重新生成,你会发现结果更“像人写的”。另外,input_seq 只保留最近 128 个 token 是关键,否则预测延迟和内存会随生成长度线性增长。
最后,生成完成后要把 token 序列转回 MIDI 文件,这一步常见做法是反向解析 token 字符串:遇到 note_on 记录音符开始时间,note_off 记录结束,time_shift 推进时间指针。写出文件后用合成器软件(如 TimGM6mb 音色库)渲染成 wav,才算走完整个“设计”流程。
5. 训练到试听全程避坑:5 个最容易翻车的位置与排查方法
5.1 损失降得挺顺利,生成出来的旋律却是同一段反复
现象:训练 30 个 epoch 后 loss 已经到了 1.2 左右,但生成结果永远是那两三个小节的变体,换了种子序列也没用。
原因:模型过拟合到了训练集中高频出现的音型。大多数公共 MIDI 数据集里,C 大调范围内的三和弦进行占据绝对主导,模型学到“最安全”的预测就是重复最常见的组合。另一个原因是采样时 temperature 设得太低,概率分布被压得过尖。
解决:第一,增大训练数据里调式的多样性,不要全部用 C 大调;第二,把 embedding 维度从 256 降到 128,减少模型记忆容量;第三,采样 temperature 调到 1.0 以上,top_k 保持 8 左右。如果还不行,检查是不是训练集里同一首 MIDI 被转调增广了太多份,导致真正独立的曲子太少。
5.2 训练 loss 一度正常,中途突然变成 nan
现象:训练到第 15 个 epoch,loss 从 1.3 左右直接变成 nan,之后无法恢复。
原因:大概率是学习率过大导致梯度爆炸,也可能是某一批样本里出现了超出词表范围的 token id,比如转调后音高越界被处理成了负数。LSTM 的梯度在时间步 128 上累积,学习率 1e-3 虽然通常安全,但配合大 batch 时偶尔会冲爆。
解决:把优化器换成 AdamW,设置clipnorm=1.0;同时在预处理阶段检查所有 token id 是否都在 [0, vocab_size-1] 区间内,越界的直接过滤。如果你用了转调增广,注意过滤转调后 note 小于 0 或大于 127 的事件。
5.3 生成结果全是单音,完全没有和弦
现象:输出的 MIDI 听起来像一个音符一个音符地蹦,没有任何纵向同时发音的和弦。
原因:模型只在事件流层面建模,没有显式约束“同一时刻可以有多个音”。如果训练数据里本来钢琴曲的密集和弦经过事件排序后被打散了,模型学到的是水平旋律,而非垂直和声。
解决:常见做法是把同一个 tick 内的 note_on 保持相邻排列,不要按 note 排序后打散。更激进的做法是用多标签分类头替代单标签 softmax,每个时间步预测一组同时发音的音高组合,但那样会复杂很多,毕设不推荐。我一般会先检查 pre-processing 里的排序函数,确保events.sort时先按 tick、再按 note_on 优先,这个顺序不能颠倒。
5.4 生成 MIDI 导出后打不开,或时长完全不对
现象:生成的 .mid 文件在播放器里要么报错,要么听起来节奏错乱。
原因:导出时 tick_per_beat 和 delta time 换算不一致。比如训练时用的是文件自带的 480 tick_per_beat,生成 token 序列还原时却用了 960,导致时间轴压缩一半;或者没有正确写文件头格式 1。
解决:写 MIDI 文件时固定用同一个ticks_per_beat,我习惯统一设 480。所有 time_shift token 在写回时乘以对应的 tick 数,不要依赖每个文件的原始值。写完用 mido 重新打开一遍做校验,确认 message 数量不为 0。
5.5 生成旋律听起来断断续续,像卡碟
现象:旋律本身音高没问题,但节奏忽快忽慢,有明显的停顿感。
原因:time_shift token 的粒度太粗或太细。如果每个 shift 代表 1/16 拍,那么模型在表达三连音或短倚音时只能凑合用整数值,生成结果缺少平滑感;反过来,如果粒度太细,模型需要连续输出大量 time_shift token,很容易在采样时丢失“连续推进”的上下文。
解决:把 time_shift 粒度设为 1/8 拍或 1/16 拍,但控制每种 time_shift token 的最大连续重复次数,比如最多连续 8 个,防止出现超高密度或超长休止。采样时不要贪心,用重复惩罚技巧:如果模型连续多次预测同一个 time_shift,手动降低它的概率来强制推进。
6. 把“能出音符”做成“能出作品”:情绪条件化与验证方法
6.1 给模型加一个情绪条件:条件 LSTM 的简单改法
无条件的 LSTM 生成结果听感上“像那么回事”,但很难控制风格方向。给模型加一个情绪标签是最划算的结构升级。具体做法是准备一个字典,映射“欢快”“舒缓”“紧张”到离散 id,每个训练样本都标注所属情绪;然后把情绪 id 通过一个独立的 embedding 映射到向量,在时间维上拼接进 LSTM 每个时间步的输入。推理时你只要指定情绪 id,就能稳定生成该情绪倾向的旋律。
# 在 build_model 中增加 emotion 输入分支 emotion_id = tf.keras.Input(shape=(1,), dtype=tf.int32) emotion_embed = tf.keras.layers.Embedding(3, 32)(emotion_id) # 3种情绪 # 将情感嵌入广播到每个时间步并拼接到特征上 broadcast_emotion = tf.keras.layers.Lambda( lambda x: tf.tile(x, [1, tf.shape(inputs)[1], 1]) )(emotion_embed) combined = tf.keras.layers.Concatenate()([x, broadcast_emotion]) x = tf.keras.layers.LSTM(lstm_units, return_sequences=True, dropout=dropout)(combined)这组改动的要点是:情绪 embedding 只有 32 维,模型会学到不同情绪对应的音高范围、音程跳跃大小和节奏密度差异。比如“欢快”倾向于高音区、小音程跳跃和密集八分音符,“舒缓”倾向于中低音区、大音程滑音和稀疏四分音符。你也完全可以把情绪标签换成语料库里的歌词情感分类或演出场合标签,做横向对比实验放进设计说明书。
6.2 验证与试听:别只用 loss 判断,盲听对比才是验收线
模型训练好之后,很多学生会陷入只看 loss 数字的误区。实际上 loss 只能说明模型对词表的预测准确度,完全无法反映音乐结构质量。我常用的验证分两步:客观指标算 perplexity、平均音高分布,主观验证做盲听对比。找一个朋友,让他听三段音频——原曲、你模型生成的曲子、另一个随机 baseline 模型生成的曲子——让他按“是否像人写的”“是否悦耳”“是否有重复”打分。
另有一个比较容易量化且能在说明书写出亮点的指标是旋律自相似度:用动态时间规整(DTW)计算生成曲目片段之间的平均距离,距离小表示重复过多,距离大表示结构松散,落在中间 0.3-0.6 区间通常听感最好。这个指标可以放在实验分析里,比单纯贴十个 loss 曲线更有说服力。
我自己的血泪经验是:生成结果好不好听,一半在采样参数,另一半在训练数据的质量。别再迷信某个神奇的模型结构了,先把数据里的噪声轨道、半截文件清理干净,比调十个超参数都管用。这套基于 MIDI 事件流加上双层 LSTM 的方案,足够支撑一套毕设的完整设计与实现,也为你后续扩展成 Transformer 或加对抗训练留好了接口。希望你从这份源码包里走出来的不是一组参数,而是自己能解释、能调试、能改进的完整能力。希望帮到你。
本文还有配套的精品资源,点击获取