FEATURED · 精选文章

MIDI钢琴曲预处理:构建AI作曲训练数据集的完整流水线

发布时间 / 2026/9/12 16:16:19
来源 / 创域科博编辑部
栏目 / 资讯中心
MIDI钢琴曲预处理:构建AI作曲训练数据集的完整流水线 简介一份面向人工智能作曲与音乐信息检索研究者的MIDI钢琴曲数据集源自MAESTRO v3.0.0收录了专业钢琴家演奏并精细对齐的曲目可用于训练循环神经网络、长短时记忆网络、Transformer等生成模型。压缩包共包含1280个文件其中1276个为.midi钢琴曲辅以1个license授权说明、1个readme说明文件、1个csv元数据清单和1个json配置文件总大小约55.71MB。MIDI文件以结构化方式记录音符、时值、速度、踏板等表现信息轻量易处理适合构建大规模训练集csv与json文件提供了曲目元数据和实验划分依据便于研究者快速筛选指定作曲家或风格的数据。目前已有523人浏览学习数据集既可直接用于训练与微调也可用于数据清洗、格式转换和特征工程等实践为AI作曲入门及复现顶会实验提供了扎实的数据基础设施。1. 当MIDI钢琴曲想喂给AI作曲模型别把第一步押在模型上MIDI不是音频它是一串带时间戳的事件描述。一个钢琴曲MIDI文件里记录的是哪个音符在什么时候按下、什么时候抬起、力度多大这种结构化程度天然适配序列建模——Transformer、Diffusion系列都能量直接消费这类符号数据不用走音频转谱那层误差很大的弯路。对钢琴曲尤其合适单乐器、声部清晰、音高时值明确是搭建人工智能作曲训练数据集时最容易上手的起点。这篇博文从MIDI的文件格式讲起给出抽取音符、量化、token化、清洗筛选和数据集划分的完整流水线让手头有一批钢琴MIDI文件但不知道怎么处理的工程师能直接跑通一套可复现的方案。2. MIDI格式的数据特性为什么钢琴曲是AI作曲训练集的第一选择2.1 MIDI记录的是哪个键在何时按下而不是声音长什么样MIDI文件的核心内容是一系列消息事件note_on、note_off、control_change、program_change、pitch_bend等。音频波形要表达的是振幅随时间连续变化而MIDI把这些全部抽象成离散的指令模型不需要面对采样率、频谱泄漏、相位对齐这类信号处理问题。钢琴曲在这个层面表现得特别纯粹——绝大多数事件就是note_on和note_off外加少量延音踏板控制几乎没有弯音轮和滑音。也就是说训练数据的每个事件都有了明确的语义边界loss可以定义在下一个合法事件是什么上而不是下一帧波形长什么样上。2.2 从一轮数据对比看为什么要选MIDI而不是音频拿同一个钢琴音频文件与它的MIDI版本对比音频按44.1kHz采样一分钟大约是264万个采样点同样一分钟的钢琴曲MIDI通常在数千到数万个事件之间。量级差了百倍以上。如果做音频域的生成模型你得先处理频谱重建、相位恢复这些和作曲无关的负担然后再指望模型学到音乐结构。而MIDI训练集可以直接跳过声学渲染把算力全部花在和弦进行、旋律走向、节奏模式这些高层级特征上。不少从业团队做音乐生成的第一步都是收集MIDI先把作曲本身跑通需要音频时再用采样器或合成器渲染这条路复用性最强。2.3 钢琴MIDI的轨道结构与读取方式钢琴曲MIDI常见格式是Format 0和Format 1两种。Format 0把所有事件塞在一条轨道里Format 1按声部分轨左手、右手或各个声道分布在不同的track上。用mido读取文件元信息时关键看三个值format类型、ticks_per_beatPPQN、轨道数和每个轨道的事件数量后者用来快速识别空轨。import mido mid mido.MidiFile(Chopin_op10_no1.mid) print(format:, mid.type) print(ticks_per_beat:, mid.ticks_per_beat) print(track count:, len(mid.tracks)) for i, track in enumerate(mid.tracks): print(ftrack {i}: name{track.name!r}, events{len(track)})逻辑说明先实例化MidiFile读取文件头再遍历整个轨道列表统计事件量。参数说明ticks_per_beat表示一个四分音符被切分成多少个tick常见值是96、120、480这个值直接影响后续量化网格的选择不统一的话需要在预处理阶段对齐track.name在部分MIDI里为空字符串但不要依赖名字做筛选多轨钢琴曲的左右手判定最好用音高区间或目标通道数来做。3. 用Python把MIDI钢琴曲预处理成可训练的token序列3.1 从note_on和note_off配对出音符对象MIDI事件是流式的note_on按下后对应的note_off不一定紧跟其后中间可能穿插其他事件。要得到可用的音符序列必须做配对并把delta time换算成绝对时间。一个常用写法是用字典暂存未闭合的note事件。def midi_to_notes(mid): notes [] pending {} for track in mid.tracks: t 0 for msg in track: t msg.time if msg.type note_on and msg.velocity 0: pending[(msg.channel, msg.note)] (t, msg.velocity) elif msg.type note_off or (msg.type note_on and msg.velocity 0): key (msg.channel, msg.note) if key in pending: start, vel pending.pop(key) notes.append({ pitch: msg.note, start: start / mid.ticks_per_beat, end: t / mid.ticks_per_beat, velocity: vel, channel: msg.channel, track: len(notes) # 占位后面可替换成真实track索引 }) return sorted(notes, keylambda n: (n[start], n[pitch]))逻辑说明遍历每一条track用t累加delta time维持绝对时间轴note_on且velocity大于0才视为有效按下velocity等于0的note_on按note_off处理这是MIDI协议里常见的释放写法。参数说明除以ticks_per_beat得到以拍为单位的时间方便后续处理channel保留用于区分左右手声部多轨曲目中的同一声道往往在前后段落分别负责旋律和伴奏直接按channel分组比按track分组更稳。3.2 量化到统一网格并过滤过密音符不同MIDI文件的tick精度不一致原始时间长短参差直接训练会让模型学到时间抖动而不是乐感。标准做法是把音符时间对齐到固定网格这里以十六分音符为基准四分音符被分成16份。GRID 16 # 一个四分音符平均分成16份 def quantize_notes(notes, gridGRID): for n in notes: n[start_q] int(round(n[start] * grid)) n[end_q] max(n[start_q] 1, int(round(n[end] * grid))) n[dur_q] n[end_q] - n[start_q] return notes逻辑说明乘以GRID再四舍五入目的就是把时间轴离散化让同一个位置的音符在数学上完全对齐end_q强制比start_q至少大1避免时值为0的非法token。参数说明GRID的取值决定了模型能表达的最小音符时值GRID16对应十六分音符GRID8精度减半但序列长度也缩短如果数据集里大量出现三十二分音符建议GRID32但训练成本会明显上升一般的钢琴曲用16足够也能兼容多数古典钢琴作品。3.3 转为REMI风格的token并检查覆盖度量化完成后把音符序列转成事件token。REMI把每个音符拆成小节、拍内位置、音高、时值和力度5个维度的事件标签显式编码结构信息模型在生成时更容易控制小节边界和强拍位置。事件域取值示例作用BarBar_0, Bar_1标记小节号让模型感知曲式PositionPos_0, Pos_1, ...标记音符在拍内的偏移与Bar组合定位PitchNote_60, Note_64具体音高字典直接映射MIDI note号DurationDur_16, Dur_48量化后的时值长度VelocityVel_90, Vel_100力度归并到10的倍数压缩词表def notes_to_tokens(notes): tokens [] last_bar, last_pos 0, 0 for n in notes: bar n[start_q] // 16 pos n[start_q] % 16 if bar ! last_bar: tokens.append(fBar_{bar}) if pos ! last_pos: tokens.append(fPos_{pos}) tokens.append(fNote_{n[pitch]}) tokens.append(fDur_{n[dur_q]}) tokens.append(fVel_{n[velocity] // 10 * 10}) last_bar, last_pos bar, pos return tokens逻辑说明bar和pos分别取自量化后的start_q整除和取余分别编码小节与拍内位置只有小节号或位置号变化时才插入对应token减少序列长度。参数说明velocity除以10取整再乘10把127级力度归并成0到120的13个档位这个压缩对作曲任务影响极小但能显著缩小词表大小如果数据集是编曲键盘导出的MIDI力度值可能出现大量127这时候relu式截断到110更合理避免力度分布向最大值塌缩。3.4 落盘与元数据记录预处理完成的token序列建议保存成JSON或二进制行格式同时生成一个打包了源文件路径、时长、音符数、词表覆盖度的meta表。常见做法是把JSONL作为存储介质每行一个样本的token数组用meta表记录哪个文件对应哪个样本。python prepare.py --input_dir ./midi_piano --output_dir ./dataset --grid 16 --max_len 2048参数说明--input_dir指向原始MIDI目录--output_dir存放生成的token和meta--grid控制量化精度--max_len是token序列的最大长度超过的样本会被截断成多段而不是直接丢弃。4. 构建AI作曲训练数据集筛选、去重与划分策略4.1 用程序过滤掉不能进训练集的MIDI互联网上收集的古典钢琴MIDI来源复杂从人工扒谱到MIDI键盘录制再到自动转录工具都有混入训练集前需要设置一批硬性指标。按下面的参数表过滤比较稳妥指标建议值判定理由最短时长15秒太短没有旋律上下文最长时长10分钟过长训练时截断浪费token最少音符数32过滤碎片化MIDI最低音符密度0.5音符/秒稀疏到接近无声最高音符密度30音符/秒密集和弦可保留异常重复要处理音高范围21到108超出钢琴88键范围视为转录错误未闭合note数0残留note_on对应不上note_off为损坏文件def pass_filter(notes, seconds, min_notes32, min_len15.0, max_len600.0): if len(notes) min_notes: return False if seconds min_len or seconds max_len: return False density len(notes) / seconds if not (0.5 density 30): return False pitches [n[pitch] for n in notes] return min(pitches) 21 and max(pitches) 108逻辑说明先按音符数量剔除碎片再按时长和密度限制保住正常曲目最后用音高范围确认它确实是钢琴曲而非损坏或乱码文件。参数说明min_len设15秒是因为大量短小片段在collect阶段就是被误截的训练时也没有足够上下文让模型学出乐句density的上限30音符/秒看起来宽松但能拦截住一些自动生成器反复堆叠同一音高造成的伪密集片段。4.2 去重同一首曲子以不同调性和速度反复出现来自不同站点的MIDI文件对同一首古典曲目常常存在多个转录版本移调、变速、加装饰音各不相同。直接训练会让热门曲目主导模型风格肖邦和巴赫的比例严重失衡。实际操作可以先做归一化哈希把绝对音高转成相对首音的间隔把时间间隔量化后拼成字符串再取MD5移调后的序列相对音高不变轻微节奏抖动被量化抹平可以识别出本质相同的曲目。import hashlib def normalized_digest(notes): if not notes: return None first_pitch notes[0][pitch] rel_pitches [n[pitch] - first_pitch for n in notes] intervals [0] for i in range(1, len(notes)): gap round((notes[i][start] - notes[i-1][start]) * 4) intervals.append(gap) raw |.join(map(str, rel_pitches intervals)) return hashlib.md5(raw.encode()).hexdigest()逻辑说明相对音高序列消除移调影响间隔乘以4再取整消除轻微节奏波动两者拼接后做消息摘要。参数说明乘4本质是对拍位置做四分之一拍量化间隔容忍度较低如果发现同曲目的改编版没有被识别出来可以把系数降到2甚至1但此时不同曲目发生碰撞的概率也会上升需要测试后取一个平衡值。更精细的做法是对音符序列做局部敏感哈希但MD5这种确定性哈希在数据量几万级别时速度优势明显够用就行。4.3 数据划分不能随机拆文件列表做了去重之后仍然会有原版和改编版结构相似但哈希不同的残留而且同一作曲家同一时期的作品在整体风格上高度相关。如果直接按文件列表随机划分训练集和验证集之间会混入风格高度重叠的样本模型在验证集上的指标会被严重高估。常见做法是先按归一化哈希聚类再用GroupKFold按组划分from sklearn.model_selection import GroupKFold file_ids list(range(len(samples))) groups [sample[digest] for sample in samples] splitter GroupKFold(n_splits10) for train_idx, val_idx in splitter.split(file_ids, groupsgroups): if len(val_idx) len(samples) // 10: break逻辑说明GroupKFold按groups参数保证同一个分组的样本不会被拆分到train和val两个集合里防止变相数据泄露。参数说明n_splits设10意味着各组被分到验证集的概率是1/10实际生产数据量五万级别时验证集会有一万左右样本如果觉得太大可以把n_splits设为20但fold内分布稳定性会略降。4.4 按作曲家或风格桶做上限截断古典钢琴MIDI的数据分布极不均衡巴赫、莫扎特、肖邦的作品数量远多于近现代作曲家而当代流行钢琴的曲目往往只有少量。简单加权采样可以缓解但更好用的是给每个风格桶设置容量上限cap超出上限的样本按哈希排序后丢弃。风格桶原始曲目数cap值巴洛克巴赫等1800400古典主义莫扎特等1200400浪漫主义肖邦等900400近现代/流行340340逻辑说明cap迫使模型在每个风格里都要学而不是靠语料量优势碾压其他风格。参数说明cap值自己按轮次调第一轮从400开始训练完成后看验证集里风格混淆矩阵哪个类别的loss异常低就把cap调小哪个风格生成质量不行就适当提高配额。5. 训练集发布前的质量自检用token分布找坑5.1 三种穿透筛选的隐藏异常即使通过了过滤和去重数据里仍可能藏着三类只在训练中暴露的异常。一是时间轴空洞某个段落被错误拉长成多个空小节模型会学着拖拍二是事件戳错位note事件集中出现在0时刻附近通常由上游配对逻辑漏洞导致三是力度分布畸形所有音符力度都是127或全部小于30来源集中在游戏MIDI或自动琶音器生成乐感极差。import json from collections import Counter def audit_token_file(path): tokens json.load(open(path)) bar_seq [int(t.split(_)[1]) for t in tokens if t.startswith(Bar_)] vel_seq [int(t.split(_)[1]) for t in tokens if t.startswith(Vel_)] empty_bars sum(1 for i in range(1, len(bar_seq)) if bar_seq[i] - bar_seq[i-1] 4) return { length: len(tokens), empty_bar_gaps: empty_bars, vel_127_ratio: sum(1 for v in vel_seq if v 127) / max(len(vel_seq), 1), vel_20_ratio: sum(1 for v in vel_seq if v 20) / max(len(vel_seq), 1), }逻辑说明分别统计小节号断层和力度极值的占比。小节号连续跳过4个以上意味着该处有巨大空白vel_127_ratio偏高说明数据源有问题vel_20_ratio偏高说明力度信息几乎没有表现力。参数说明阈值4个小节对应四拍左右在古典钢琴曲里可以接受如果超过8个小节必然异常vel_127_ratio高于0.3的样本我会直接拔出数据集vel_20_ratio高于0.5的样本在训练时会导致模型输出的力度几乎恒定为极弱影响真实感。5.2 把审计放进自动化流水线python audit.py --input ./dataset --output ./dataset/audit_report.json --max_empty_gap 4 --max_vel127 0.3这条命令直接对预处理后的整个数据集目录做审计输出一个汇总报告JSON里面包括每条样本的异常计数、出现位置和判定结果。参数说明--max_empty_gap控制允许的最大连续空小节数--max_vel127控制力度为127的比值上限终端里显示通过率失败样本写入report里关联的原始MIDI路径方便回去修预处理问题而不是直接删数据。5.3 用力度直方图和时长分布做最后验收把审计通过的全部样本汇总画一张力度直方图理想状态是30到110之间相对均匀或至少有一个明显的宽峰而不是全部挤在127处。同时打印时长中位数确认预处理后的有效样本长度分布没有因截断而严重改变原始曲目结构。这两张图是我在训练前最后看的两样东西它们不需要模型介入直接把这个数据集可不可用判断到了源码层面。这一步做好后整个AI作曲训练数据集的构建也就真正闭环了。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻