☰
深度神经网络作曲:用midiGenerator生成Midi的完整实践
2026/9/29 23:59:00 网站建设 项目流程

简介:midiGenerator是一套基于深度神经网络生成MIDI音乐的完整代码,面向对AI音乐创作、序列生成模型感兴趣的Python开发者与研究人员。项目源于作者硕士论文“具有深度概率模型的音乐完成”,基于TensorFlow/Keras、music21与pygame生态,覆盖MIDI解析、特征编码、模型训练与生成等环节,提供从数据预处理到音乐输出的全流程工具。压缩包仅1.02MB,内含bayesian-opt.py(贝叶斯超参数寻优)、compute_data.py(MIDI转numpy数组)、train.py(模型训练)与generate.py(音乐生成)等核心脚本,并附有数据集放置与调用顺序说明,便于快速搭建环境、复现实验。资源已有187人浏览学习,适合希望深入了解深度音乐生成、贝叶斯优化或自行扩展MIDI数据集的读者。通过研读这些脚本,可掌握循环训练框架的搭建、音乐序列的向量化表示方式,以及如何利用生成模型创作旋律片段。

1. 用 midiGenerator 生成 Midi:先弄明白这套东西解决的是哪一环

做算法作曲的人通常会碰到一个尴尬:模型训练得再好,产物不是人能直接用的音乐,而是一堆概率矩阵。midiGenerator 这类项目解决的就是中间这一段——用深度神经网络把音符事件当作 token 序列来建模,预测下一个事件,再把预测结果写回标准 Midi 文件。它不负责音色、混响和后期渲染,只负责把“音符该在哪个时刻出现、持续多久、力度多大”说清楚。它适合两类人:想跑通“语料→模型→midi”完整链路的入门者,以及手里有模型但卡在数据预处理和 Midi 写盘环节的熟手。一个反直觉的经验是:网络结构反而是这条链路里最不需要纠结的部分,真正吃掉时间的是 midi 解析、token 设计和解码器可靠性。

2. 先想清楚再写代码:Midi 序列化与深度神经网络选型的三个前提

midiGenerator 不是一个模型,而是一条流水线:解析、token 化、训练、解码、写盘。模型只负责一件事——给定前面 N 个事件,预测下一个事件是什么。动手之前,先把三件事定下来:生成结果用什么符号表示、用哪类深度神经网络骨架、训练数据从哪来。这三件事没定,后续写出的代码大概率要推翻重来。

2.1 为什么生成 Midi 而不是直接生成音频

直接生成音频的第一道坎是序列长度。44.1kHz 采样率下,一首 3 分钟的钢琴曲相当于 800 万个采样点,任何序列模型在这种尺度上都难以建模长期结构;就算退一步用 mel spectrogram 做频谱预测,还要处理相位恢复和音质损失,工作量比 Midi 路线大一个量级。

Midi 是符号化表示。一首钢琴曲通常只有几千个 note 事件,序列模型用 256 到 512 的上下文就能覆盖一个完整乐句。另一个实际理由是“后悔药”——Midi 文件生成后,音符位置错了可以拖,力度可以改,音色可以换;波形一旦生成,想逐音符编辑就难了。所以 midiGenerator 的核心价值,是把深度神经网络输出的概率分布翻译成可编辑的音乐符号。它适合旋律生成、伴奏织体生成这类任务,但不负责出成品音频,成品还需要采样音源渲染。明确这个边界,后面做模型评估时就不会拿“音质不好”来冤枉它。

2.2 深度神经网络骨架怎么选:LSTM 与 Transformer 的取舍

LSTM 是这类项目最稳妥的起点。Midi 事件流本质上是时序数据,LSTM 的归纳偏置就是局部连贯性,对 note_on 后面跟 note_off、一小节内音型重复这些模式非常敏感。参数量也小,两层 LSTM、hidden size 512 大约两千万到四千万参数,消费级显卡能训。

Transformer 的优势在长程结构,比如 32 小节后主题再现。代价是要把序列长度拉到 1024 以上才能体现优势,训练时间翻倍,而且 Midi 事件局部强相关,自注意力会算出大量冗余的 attention 分数。选型对比可以参考下面这张表:

项目LSTMTransformer
建模长度256-512 足够建议 1024+
训练开销单卡可训显存和时间约 2-4 倍
适合数据量万首以内尽量大
典型缺陷长距离重复结构弱小数据下容易过拟合

我的建议是:数据量小于一万首、以单轨钢琴为主,先用 LSTM 把整条链路跑通;等需要生成多声部长结构,再切 Transformer。无论哪一种,输入输出都是离散 token,做 next-token prediction,这一点和语言模型没有本质区别。如果你是从 MNIST 分类教程或入门课转过来的,最该改掉的思维惯性是:别把分类头直接搬到生成任务上——midi 生成每一步输出的是整个词表上的概率分布,不是类别标签。

2.3 数据从哪来:公开数据集与自制语料的取舍

MAESTRO 是钢琴演奏 Midi,和音频对齐过,音符质量高,适合训练旋律模型,缺点是风格集中在古典钢琴。Lakh 规模更大,覆盖流行和摇滚,但很多文件是自动扒谱或多轮编辑的结果,音轨混乱、时值误差大。自己收集 Midi 要注意:不少文件把节拍器轨、歌词轨和主旋律混在一起。

清洗原则只有一个方向:只保留音符信息,去掉一切装饰。control change、pitch bend、program change 对旋律模型是纯噪声。先把所有文件统一到 4/4 拍、120 BPM,减少 time_shift 的分布漂移。一首 Midi 的 note 数少于 50 或大于 5000 的都丢掉——太少学不到结构,太多通常是多轨混在一起。按轨道过滤可以写成一个简单函数:

def clean_tracks(mid): keep = [] for track in mid.tracks: name = "" note_count = 0 for msg in track: if msg.type == "track_name": name = msg.name if msg.type in ("note_on", "note_off"): note_count += 1 if note_count < 20: continue if any(k in name.lower() for k in ("drum", "perc", "bass", "lyric")): continue keep.append(track) return keep

逻辑说明:先按轨道名和音符数两层过滤,把碎轨和明显不属于旋律声部的轨道剔除。参数说明:note_count < 20 通常是不完整的装饰轨;bass 轨是不是要排除取决于任务——如果你生成的是独奏钢琴,排掉 bass 轨能少很多低音噪声,如果目标是乐队作品,保留反而是好事。

3. 落地一条“语料→token→模型→midi”的最小流水线

下面这条流水线是我在类似 midiGenerator 项目里用得最顺的形态,改动少、问题容易定位。核心是四步:解析 Midi 成事件、事件转整数 token、训练模型预测下一个 token、推理时采样 token 再翻译回 Midi 文件。

3.1 用 mido 解析 midi:事件流与元信息的取舍

第一步把 .mid 变成程序能直接操作的事件列表。mido 是首选,因为它保留每个消息的 tick 级相对时间,这对 token 化很关键。pretty_midi 更适合做统计分析,它会直接填充出 note 对象、把时间换算成秒,方便但离 token 序列远了一步。

import mido def midi_to_events(path, merge_tracks=True): mid = mido.MidiFile(path) # 合并轨道后,事件按时间顺序排列,方便转 token merged = mido.merge_tracks(mid.tracks) if merge_tracks else mid.tracks[0] events = [] for msg in merged: if msg.is_meta: continue if msg.type in ("note_on", "note_off"): # msg.time 是相对前一个事件的 tick 差 events.append({ "type": msg.type, "note": msg.note, "velocity": msg.velocity, "time": msg.time, }) return mid.ticks_per_beat, events

逻辑说明:merge_tracks 会把多个音轨合并成一条带相对 tick 的事件流,后续转 token 时不用再管轨道概念。过滤 is_meta 是把拍号、速度标记这类对音符生成无效的消息去掉。msg.time 是相对时间,单位是 tick 而非秒,解码写盘时要用同一个时间基准,避免混用。参数说明:merge_tracks=True 只适合单乐器或独奏钢琴;多轨 Midi 想保留声部结构就不要合并,逐轨转独立序列,复调生成的链路会复杂一个量级,不建议第一版就这么做。

3.2 构造训练样本:note_on / note_off / time_shift 三段式 token

Midi 事件不能直接喂给神经网络,要编码成整数 token。最常见的编码就是三类事件:note_on 表示按下某个键、note_off 表示松开某个键、time_shift 表示向前推进一段时间。这种设计的优点是词表小且完整,解码时不用额外推理。

TIME_SHIFT_BUCKETS = [0, 10, 20, 30, 40, 60, 80, 120, 180, 240, 360, 480] def events_to_tokens(events, ticks_per_beat, bpm=120): ms_per_tick = 60000 / (bpm * ticks_per_beat) tokens = [] for ev in events: dt_ms = ev["time"] * ms_per_tick # time_shift 拆成多个桶,保证桶值之和等于原始间隔 while dt_ms > 0: bucket = max(b for b in TIME_SHIFT_BUCKETS if b <= dt_ms) tokens.append(("time_shift", bucket)) dt_ms -= bucket if ev["type"] == "note_on" and ev["velocity"] > 0: tokens.append(("note_on", ev["note"], ev["velocity"])) else: tokens.append(("note_off", ev["note"])) return tokens

逻辑说明:time_shift 用桶离散化,而不是存原始 tick,原因是 tick 几乎是连续量,模型无法对几千种取值做分类。这里用了循环拆分:每次选一个不大于剩余时间的最大桶,不断累加,保证所有 time_shift 桶值之和等于原始间隔,误差只发生在取整那一步。参数说明:TIME_SHIFT_BUCKETS 是这套方案最重要的超参数。最小值 0 表示事件紧跟前一个事件,最大值 480ms 是四分音符;如果数据里有长音或休止,一定要把桶延伸到 960、1920,否则模型永远学不会长音符,生成的旋律全都短促。velocity 还可以进一步量化,把 0-127 压成 8 档,词表能缩小不少。

3.3 训练参数:序列长度、批次、学习率怎么给初值

参数给初值是有章法的,先跑通再调优。下表供直接抄:

参数初值说明
sequence_length256约 2-4 小节,先跑通链路
batch_size32显存 12GB 以下用 16
learning_rate1e-3(LSTM)/ 3e-4(Transformer)配 warmup 更平稳
grad_clip1.0Midi 序列 loss 波动大,必开
训练轮数30-50 epoch看验证 loss 早停

Midi token 词表比 NLP 小得多,一般只有一两千,所以不需要语言模型那套复杂调度。序列长度不建议一上来就 1024:Midi 里真正有用的上下文是“前一小节的和声走向”,256 足够让模型学到乐句内部的收束。batch 大小和显存挂钩,LSTM 按序列反传,Batch 32 加序列 256 的显存占用约 4GB,12GB 卡可以跑得很从容。学习率给初值后先跑 1000 步看曲线——如果 loss 在 3.0 附近震荡不降,问题多半不在学习率,而在 time_shift 桶分布太散,回去调桶更有效。

还有一个容易忽略的细节:next-token prediction 不需要像 BERT 那样做 mask token 任务,这里唯一的 mask 是 padding。如果你的训练代码实现的是“输入序列和标签序列错开一位”,那 padding 位置会被当成真实标签,哪怕 loss 下降了,生成结果也会莫名其妙地长。统一做法是:标签序列是输入序列右移一位,padding 位置在 loss 里置 0。

3.4 推理时的采样策略:temperature 与 top-k 对生成结果的影响

训练结束后,最影响听感的是采样函数。GPT 风格的解码函数可以直接拿来用:

import torch import torch.nn.functional as F def sample_token(logits, temperature=0.9, top_k=16): # temperature 先作用在 logits 上,再取 softmax logits = logits / temperature probs = F.softmax(logits, dim=-1) if top_k > 0: topk_val, _ = torch.topk(probs, top_k) probs[probs < topk_val[-1]] = 0 probs = probs / probs.sum() return torch.multinomial(probs, 1).item()

逻辑说明:temperature 先除到 logits 上,再做 softmax;temperature 大于 1 会让分布趋于平坦,模型容易跳出重复音型,但也更容易选中异常音符;小于 1 则让高概率事件更突出,旋律更顺,但可能陷入“同一个音反复出现”。top_k 把候选缩小到前 16 个,避免长尾里那些很少出现的音符乱入。参数说明:Midi 生成和文本生成一个明显区别是,音符分布天然有尖锐峰,主音出现概率远高于其他音,所以 top_k 不适合设得太小,我一般固定 16 只调 temperature:起步 0.9,旋律连续但缺乏变化时降到 0.85;如果出现大量半音跳跃,把 top_k 缩到 8。

这一节还有一个实用技巧:temperature 可以分类设置。time_shift 对温度非常敏感,温度高了节奏会散;note 对温度相对钝感。分开采样——note_on 用 0.9,time_shift 用 0.7——是生成结果里“旋律自然但节奏规整”的最快途径。

4. 把网络输出还原成 .mid:解码器与文件写入的细节

模型输出的是一串 token id,要变成能听的 Midi,解码器要做三件事:把概率变成事件序列、把事件序列翻译成 Midi 消息、按正确的时间基准写盘。三个环节里最容易翻车的是第三个。

4.1 从概率分布到事件序列:贪心、beam 与采样

贪心解码是每步取概率最大的 token,实现最简单,但用在 Midi 上会很快陷入两三个音符的循环,因为每个局部最优都指向同一个短环。beam search 在文本生成里常用,在 Midi 上更糟:它会保留时值规整、音高重复的路径,生成结果又平又呆,而且 beam 的数量越多,越倾向把音符做成等时值。原因在于 Midi 序列的高概率区域就是“规整节奏、重复音型”,这正好是听觉上最无聊的部分。

所以这类任务实际可用的还是采样解码,配上温度控制。实现时补一个约束:如果连续采样出 time_shift token,要设置最大连续休止数。比如允许 token 层面最多出现 4 个连续的 time_shift 大桶,否则模型可能输出十几秒的空白。这个约束不是模型能力问题,而是自回归生成的常见病:一旦进入“一直输出休止”的低能量区域,概率彼此接近,很难自己走出来。

4.2 用 mido 写 Midi 文件:轨道、节拍与音长换算

写盘的第一步是先把 token 翻译成绝对 tick 的事件表。常见错误是直接按 token 顺序写消息,把相对 time 当成绝对 time,结果所有音符挤在一个时间点上。这里给出我验证过的写法:

def tokens_to_midi(token_seq, ticks_per_beat=480, bpm=120): ms_per_tick = 60000 / (bpm * ticks_per_beat) events = [] abs_tick = 0 for tok in token_seq: if tok[0] == "time_shift": abs_tick += int(tok[1] / ms_per_tick) elif tok[0] == "note_on": events.append((abs_tick, "note_on", tok[1], tok[2])) elif tok[0] == "note_off": events.append((abs_tick, "note_off", tok[1], 0)) mid = mido.MidiFile(ticks_per_beat=ticks_per_beat) track = mido.MidiTrack() prev = 0 for abs_tick, typ, note, vel in sorted(events, key=lambda x: x[0]): delta = abs_tick - prev prev = abs_tick if typ == "note_on": track.append(mido.Message("note_on", note=note, velocity=vel, time=delta)) else: track.append(mido.Message("note_off", note=note, velocity=0, time=delta)) mid.tracks.append(track) return mid

逻辑说明:先用 time_shift 把绝对 tick 算出来,加上 note 事件后再统一按 tick 排序,最后才写 Message。这样做的核心是每条消息的 time 只跟前一条消息相关,排序后 delta 计算是可靠的,不会出现时值全挤在最后。note_off 的 velocity 写 0 是标准做法,多数 DAW 会把 note_on velocity=0 等价处理成 note_off,但显式 note_off 兼容性最好。参数说明:ticks_per_beat=480 是通用值,一拍 480 tick,八分音符 240 tick;如果从 MAESTRO 这类数据集里拿到的是 1024 或 96 ticks/beat,建议统一转成 480,否则 time_shift 桶和 tick 换算会出现小数,多次取整误差累积后,时值越来越离谱。bpm 只影响播放默认速度,不影响音符结构。

写盘前还有一个配对检查:每遇到一个 note_on,把它塞进 pending 字典;遇到 note_off 就从字典里弹出。生成结束时如果 pending 里还有音符,说明序列不完整,要强制补 note_off。这一步能拦住大量“DAW 打开一片空白”的问题。

4.3 一个可直接跑的生成脚本骨架

上面各段拼起来就是一个完整的最小生成器:

def generate_midi(model, tokenizer, start_seq, steps=512, temperature=0.9, top_k=16, out="out.mid"): model.eval() seq = list(start_seq) with torch.no_grad(): for _ in range(steps): input_ids = torch.tensor([seq[-256:]], dtype=torch.long) logits = model(input_ids)[0, -1, :] next_id = sample_token(logits, temperature, top_k) if next_id == tokenizer.eos_id: break seq.append(next_id) mid = tokens_to_midi(tokenizer.decode(seq)) mid.save(out)

逻辑说明:输入每次取序列末尾 256 个 token,让生成步数增加时显存和推理时间都保持恒定。next_id 如果是 EOS token 就提前结束,防止无限生成;如果 tokenizer 没定义 EOS,就用“最大 token 数加最大连续休止数”双保险收尾。参数说明:steps 是 token 数,不是音符数——time_shift 也占 token,所以一首 64 小节的钢琴曲通常需要 2000 到 3000 个 token,steps 给 512 只适合先听短动机。

start_seq 可以来自用户给的一段种子旋律:用 3.1 和 3.2 的函数把 midi 转成 token 序列,取前 32 个 token 作为引导。空序列时用 BOS token 开头,让模型自由发挥。这个骨架跑通后,再往里面加采样约束和滑窗缓存。

5. midiGenerator 生成 Midi 常见问题排查:五个高频踩坑点

下面五条是我自己踩过、也帮别人排查过的问题。每一条都按现象、原因、解决三件事说清楚,你遇到时可以直接对号入座。

5.1 生成的 midi 在 DAW 里是空白文件

现象:训练和推理都不报错,loss 也正常降,但生成的 midi 拖进 DAW,只有一个空轨道,音符一个都看不到。原因:绝大部分是 note_on 后面没有配对 note_off,或是所有 note_on 的绝对 tick 相同,播放器把零时值音符直接丢弃;也有可能是解码时 note 编号超出 0-127,mido 写文件时抛了异常,被上层代码吞掉。解决:往解码器加断言——每个 note_on 必须入 pending 字典,遇到对应 note_off 才弹出;写盘结束后检查 pending 为空,不为空就强制补 note_off 并记日志。再加一个自检函数,读回刚保存的 midi,统计音符数,等于 0 直接抛错,别让坏文件一路进到试听环节。

def assert_valid_midi(path): mid = mido.MidiFile(path) # velocity > 0 的 note_on 才算有效音符 note_count = sum( 1 for t in mid.tracks for m in t if m.type == "note_on" and m.velocity > 0 ) assert note_count > 0, f"empty midi: {path}"

逻辑说明:写完文件立刻读回,统计 velocity 大于 0 的 note_on 数量,为 0 就是空文件。参数说明:velocity=0 的 note_on 在标准里等价于 note_off,所以统计时排除掉,避免把闭合事件误算成音符。

5.2 旋律像机关枪:所有音符时值都偏短

现象:生成结果听感全是八分或十六分音符,没有长音也没有休止,像节拍器上挂了几个音。原因:time_shift 桶最大只到 240ms,模型无论怎么采样都生成不了超过一拍的音符;另一种可能是训练数据里 time_shift 占比太低,模型学会了直接忽略它,把每个 note 都排在相同 tick 上。解决:把 TIME_SHIFT_BUCKETS 最大值扩到 1920ms,并检查训练语料里长时值 token 的占比,最好不低于 5%。如果数据没问题,就在 loss 上给 time_shift 类 token 加权,比如系数 1.5,逼模型把时值预测重新当回事。检查占比可以先写个小工具:

from collections import Counter def check_time_shift_ratio(token_seq): cnt = Counter(t[0] for t in token_seq) return cnt.get("time_shift", 0) / max(len(token_seq), 1)

逻辑说明:统计三类事件各自占比,time_shift 低于 0.1 说明时值信息几乎没进序列,模型不可能学出节奏。参数说明:不同数据集比值会有波动,但古典钢琴语料里 time_shift 占比通常在 0.15 到 0.3 之间,低于 0.1 就直接停训查预处理。

5.3 loss 很低但生成结果像乱弹

现象:验证 loss 降到了 2.x,听起来却毫无调性,频繁半音跳跃。原因:数据里混入了大量无效音轨——有些 midi 的鼓轨用 note_on 表示打击乐,有些自动扒谱轨时值错误严重,模型把这些噪声当成统计规律学到权重里。解决:回到数据清洗,按 channel 过滤,只保留钢琴轨;统计每首 midi 的音符数,低于阈值直接丢弃。这里想再多说一句:从“头歌神经网络与深度学习”这类入门课程里出来的人,容易把 MNIST 时代的“loss 低等于模型好”直接搬过来。生成任务里 loss 低只说明模型拟合了训练分布,不代表那个分布本身是干净的。遇到这种情况,先怀疑数据,再怀疑模型。想确认是不是数据问题,可以从训练集里随机抽几首转成 token 人工看一眼,如果原始序列已经是一堆无意义跳进,模型学到乱弹不奇怪。

5.4 Windows 下中文路径保存失败或生成 0 字节文件

现象:输出路径含中文、空格或括号,程序不报错,但生成的 midi 是 0 字节,或者直接抛 OSError。原因:mido 在 Windows 下写文件用的是系统默认编码,中文路径会触发编码问题;有些封装把异常吞掉了,留下一个空文件。解决:统一把输出路径先做规范化,只允许 ASCII 字符,文件名用 run_001.mid 这种风格,不带空格和括号。这不是模型的错,却能让一次完整的推理在最后一步翻车。我的做法是在 generate_midi 函数入口直接调一个 sanitize_path 工具,把所有非法字符替换掉,一劳永逸。

import re def sanitize_path(path): # 只保留英文字母、数字、点、下划线、连字符和路径分隔符 return re.sub(r'[^A-Za-z0-9_.\-/\\]', '_', path)

逻辑说明:把所有非白名单字符统一替换成下划线,中文会被替换成多个下划线,旧路径没法还原,但文件能正常保存。参数说明:字符集合里保留反斜杠和正斜杠是为了兼容 Windows 和 Linux 的目录分隔;如果路径里只有文件名,可以连斜杠一起去掉,看使用场景。

5.5 训练时 batch 内序列长度不一致导致崩溃

现象:DataLoader 报 tensor 维度对不上,或 LSTM 训练中途 hidden state 的 shape 变成 0。原因:midi 事件序列天然不等长,直接 pad 到固定长度但没传 attention mask,Transformer 就会把 padding 位置当真实 token 参与计算;LSTM 如果用 pack_padded_sequence,又要求按真实长度降序排列,很多人没注意到。解决:第一版最省事的方案是训练时直接截断到固定长度 256,不要 pad,batch 天然等长,代码简单,排查容易。等后面要上长上下文,再引入 padding mask。截断时不要只取前 256 个 token,而是随机选起点切一段,等于免费的数据增强。

import random def random_crop(seq, length): if len(seq) <= length: return seq start = random.randint(0, len(seq) - length) return seq[start:start + length]

逻辑说明:随机起点切一段,而不是固定从头切,这样不同 epoch 看到同一首曲子的不同片段,模型不会记住固定开头。参数说明:length 要和训练时 sequence_length 保持一致;切完后不需要校验有没有完整的 note_on/note_off 对——模型本来就是在窗口内做条件概率预测,片段从哪里开始都能学习。

6. 进阶验证:用客观指标和听觉测试给 midiGenerator 打分

6.1 生成质量怎么量化

模型好不好,不能只看 loss。用 pretty_midi 把生成文件读回来,算几个和训练集可比的指标:

import pretty_midi def analyze_midi(path): mid = pretty_midi.PrettyMIDI(path) # 把所有乐器的 note 对象展平,统一统计 notes = [n for instr in mid.instruments for n in instr.notes] durations = [n.end - n.start for n in notes] pitches = [n.pitch for n in notes] return { "note_count": len(notes), "pitch_mean": sum(pitches) / len(pitches), "dur_mean": sum(durations) / len(durations), "notes_per_sec": len(notes) / mid.get_end_time(), }

逻辑说明:把生成 100 条结果的平均值和训练集统计值对比,重点看三项:音符数是否在合理范围——太多说明机关枪,太少说明模型学会了无限休止;平均时值是否明显偏短;音高均值是否因为模型偏爱某个音域而偏移。参数说明:notes_per_sec 是密度指标,训练集如果是每 10 秒 30 个音,生成结果是 60,那说明时值分布已经崩了,不用试听就能定位到 time_shift 桶。还有一个可选指标是音符重复率:统计相邻音符间相同 pitch 的比例,过高说明模型在重复音型里打转。

6.2 试听顺序与人工校验

客观指标只能拦住明显异常,最终判断还是靠耳朵。我的试听顺序是固定的:先用钢琴音源把 midi 渲染成音频,听前 30 秒,重点听节奏是否变化、乐句是否有起伏;再打开 midi 编辑器看音符的横向形状,有没有连续大跳、有没有超过三个相同节奏型连着出现;最后把听感最差的样本单独放进一个文件夹,作为下一轮训练的负例参考。这一步比看曲线有用得多。

我现在的习惯是,每次训练完不急着展示结果,先跑一遍 analyze_midi 再听。这个习惯帮我省下大量“听起来怪但说不清哪怪”的返工时间。midiGenerator 这个方向,网络结构只占三成,数据清洗和解码器的可靠性占七成。先把 token 化和写盘做扎实,再用统计指标做闸门,最后用耳朵投票,这条链路就能持续地产出让人愿意听完的旋律。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询