我训练了一个 125M 模型,在设备端自动补全钢琴演奏
文章摘要
Simon Edwardsson(HN 上 simedw)训练了一个 125M 参数的 decoder-only transformer,用来实时自动补全钢琴演奏:在 iPhone 15 上完全端侧运行,约 108 音符/秒。类比很直白——GitHub Copilot 或 Tabnine,只不过输入不是代码而是你在 MIDI 键盘上弹的几个音,模型接着往下续。成品 App 叫 RollTab,免费,需要一台 MIDI 键盘和 iPhone/iPad。这个项目从一年前开始折腾,做了 14 轮实验才到他愿意写出来的程度。他自己的定位是「GPT-2 级别,但是钢琴版」。
表示法是全文的技术核心。 最朴素的做法是给每个 MIDI 事件一个 token,但把音高和力度直接塞进 NOTE_ON 会让词表爆炸:128 个音高 × 128 个力度 + 128 = 16512 个 token 只用于 note-on/note-off,很多组合极其稀疏。常见改进是用文法拆解成 [NOTE_ON, PITCH, VELOCITY] | [NOTE_OFF, PITCH] | [TIME_SHIFT, DURATION],生成时用 mask 强制文法合法性。他试了 note-on/note-off 路线,结果小模型会「漂移」——忘记发 note-off、留下悬挂的音、丢失活跃状态,这对实时小模型是致命的。他也试过 [NOTE, PITCH, VELOCITY, DURATION],音乐性更好但每个音要约四次自回归前向,还迅速吃光上下文窗口。
最终方案是复合音符事件 NOTE(pitch, delta_onset, duration, velocity),彻底取消 TIME_SHIFT:静音由下一个音的 delta_onset(距上一个音起始点的时间)表示;和弦是多个 delta_onset = 0 的音按音高排序。关键在于它不是扁平 token 流——每个音内部是五个各有自己词表的类别字段 [event_type, pitch_id, delta_id, duration_id, velocity_id],各自有 embedding,音符 token 是所有 embedding 的和;输出侧有各自的 head,字段之间夹一个小的嵌套 decoder 让后面的字段能条件于前面已预测的字段,但昂贵的 transformer 主干每个音只跑一次,不是每个字段一次。这一个改动带来了约 5 倍的自回归次数削减,是端上速度的最大来源。延音踏板则在预处理阶段被「烤进」音长:踏板按下时松键则把音延长到踏板抬起时刻,同音高被重新触发则在重触发处截断——牺牲了显式的踏板动作,换来建模问题大幅简化。
数据比数据量更重要。 最终数据集是几十万个 MIDI 文件、约 3 亿个音符事件,主要来自公共领域的老古典乐。清洗管线包括:挑选钢琴为主的素材、移除或削减病态的多轨混合、按密度与音高/时间覆盖度过滤、用忽略整体转调和均匀速度变化的指纹去重、把同一作品的不同版本归入同一 split。他试过把数据集扩到约 5 倍大,结果模型更差——「清洗和挑选数据比单纯加更多数据重要」。数据增强针对的是「真人现场输入不是干净 MIDI」这一现实(他自评弹得够烂,音会偏早偏晚力度过大):整体转调、均匀速度缩放、时长/力度抖动、随机丢弃提示音。
架构与训练。 标准配方:RMSNorm、rotary 位置编码、causal self-attention、SwiGLU/MLP 块。三个尺寸:small 约 33M、medium 约 64M、large 约 125M;medium 几乎总是打败 small,large 更好但优势不大,他现在正试图把 medium 拉到接近 large 的质量以压缩体积和延迟。训练损失是五个 head 的交叉熵求和。一个反直觉的发现是 scheduled sampling:训练中有时喂给模型自己预测的音高而不是正确音高(前几个 epoch 从 0% 开始,最佳模型逐步升到 50%),结果验证损失变差(2.9998 vs 2.9495),但 Gemini 的成对偏好从 35.7% 涨到 64.3%。
评估与后训练。 最初评估就是他自己听,用 4-32 个音的提示从留出曲目生成续写;4 个音最难,8 个音好些,16-32 个音显著更可靠。他写了一堆自动指标(重复音高 n-gram、音高熵、音级熵、音域、音符密度、长停顿、和弦密度),能抓明显失败但不足以选模型。最终用 Gemini 3.5 Flash 做成对评估——要求给单一绝对分数不稳定,改问「A 和 B 哪个续写更好」效果好得多,并且把每次比较镜像一遍以减少位置偏差。他还发现 Gemini 一开始过度看重「续写本身好不好听」而非「与提示衔接得好不好」,于是拆成两个指标:continuation score(与提示的衔接度)和 sounds-good score(孤立听的音乐质量),用前者作为 DPO 的主信号。DPO 是预训练之后收益最大的一步:只用了约 700 条偏好样本、单卡训练约 12 分钟(预训练则是半天,硬件是 4 张 RTX 4090)。β 扫描结果:base 24.55%、β=0.01 达 61.08%、β=0.03 为 57.14%、β=0.10 掉到 38.10%(推太狠反而变差);只保留评估者一致同意的偏好对构成的 “consensus”(β=0.03)拿到最好的 69.05%。
没work的清单他也列了:note-on/note-off 漂移太多;文法 mask 的 token 流合法但慢;数据噪声大时更广的数据反而更差;更大模型有帮助但不能魔法般解决循环问题;Mirostat 减少了重复但常让输出不连贯;额外的局部辅助损失让训练变慢而没有明显听感收益;Gemini 绝对分数不如成对判断;单看验证损失会漏掉 rollout 质量的重要差异;born-again networks(用自己的软预测重训)没有提升。打包环节:PyTorch 导出到 Core ML,权重量化到 INT8;模型只用 512 音的上下文训练,长会话时保留最近 384 音重建上下文并重建 KV cache;用了 RoPE 理论上可以配环形缓冲做得更优雅,但 Core ML 不暴露 Q、K、V。第一版审核花了 11 天,新版本增加了 top-k、top-p、min-p、XTC、top-h、Mirostat v2 六种采样方式的选择。
HN 评论精华
这条帖子 589 分、101 条评论,是本期最受欢迎的 Show HN。讨论气氛非常正面,主线是三条:作者在评论区非常耐心地回答技术细节(他几乎回了所有技术提问);大量人希望它扩展成「我弹旋律、它配伴奏」的形态;以及一条关于「机器生成音符是否夺走即兴演奏乐趣」的哲学插曲。
- simedw(作者) 在多处补充了博客里没有的信息。关于 DPO 规模:只有约 700 条偏好样本,单 GPU 训练约 12 分钟,而 125M 的预训练大约花了半天。关于成本:他手上有 4 张 RTX 4090,所以没直接付云 GPU 的钱,「如果付了,考虑到我做的训练轮次和实验数量,肯定会累积成一笔不小的开销」。关于端侧速度的来源,他强调最大提升来自换成复合音符事件的表示法(每个音的自回归次数减少约 5 倍),Core ML 会在首次运行时优化 kernel,除此之外他其实没花多少时间调性能,「大概还能挤出远超 100 音符/秒」。关于当前水平,他自评「大致到了 GPT-2 级别:好到值得分享,但还有很大改进空间」,认为加入小节/拍号 token 可能改善节奏,另外需要某种更长程的整体作曲规划。
- davidajackson 问了最有技术含量的问题:怎么扩展支持触键力度(按下速度)、装饰音、时值等元素——每个都是这个模型的一部分还是另一个模型?simedw 给了具体设计:如果属性描述当前音,就先试着作为音符事件的另一个字段/head,比如
NOTE(pitch, delta_onset, duration, attack_velocity, release_velocity, ...);更全局的东西更适合建模为控制事件,比如CONTROL_SUSTAIN(delta_onset, value)、CONTROL_TEMPO(delta_onset, bpm)、CONTROL_PROGRAM(delta_onset, instrument)。他补充说更难的部分其实是找到足够多、这些属性都一致标注了的训练数据。 - subhajeet2107 提出了很直接的权衡:能不能牺牲音符/秒来换生成质量,反正没人能弹 108 音符/秒,也许可以让模型做 CoT。simedw 说规划步骤在 TODO 上,另一个想法是并行生成几个续写、挑看起来最好的那个再往下续,挑选过程也许能自动化。
- 「我弹旋律,它配伴奏」是最高频的功能请求。heikkilevanto 想要正经的三四声部伴奏,最好是巴洛克风格,还能导出成音乐编辑软件能用的文件格式。morkalork 想象成一个爵士乐队跟着你进来加贝斯和鼓;devonsolomon 想跑一个 MIDI clock,自己弹和弦、合成器跟着走贝斯。但 altmanaltman 指出这难度极高:只从旋律生成完整伴奏(而不只是基础和弦)需要大量高度主观的选择,随机生成不会有好结果,因为人耳偏好有意图的、艺术性的创造。评论区还翻出了这条思路的历史:vunderba 提到微软研究院 2009 年的 Songsmith(从麦克风录的旋律生成伴奏,但完全不实时),以及现实世界里最接近实时编配的其实是编曲键盘(左手仍负责和弦走向);zapperdulchen 提到 Ludwig——由 Fritz 国际象棋引擎作者做的规则式系统,建立在传统和声、对位、声部进行和配器原则上,从未流行、最终停更,但仍能免费下载;timmb 指出 François Pachet 2003 年的 Continuator 已经用分层马尔可夫模型做过类似的事。speedgoose 提到几周前发布的 Magenta Realtime 2 也很好玩。
- jwr 抓住了博客里的一个细节调侃:「最终我用 Gemini 3.5 Flash 做成对评估」——「但,但……这不就是(惊)蒸馏吗?」Tostino 接:「多可怕!」
- jasonjmcghee 说这是个很棒的、很 HN 的项目,「不太理解为什么评论都盯着交付物——你学到的东西和这段经历本身有趣多了」,并追问数据规模;munksbeer 直接引用博客回答:几十万个 MIDI 文件、约 3 亿个音符事件。
- 功能与移植请求:lvbyte 和 leobg 分别希望能直接拿到模型(没有 iPhone)以及支持口哨或麦克风采集钢琴(作者未回复这两条)。Tepix 实测「效果不错」,请求把 AI 生成的音频以 MIDI 音符发回设备(通常就是输入那台),而不是只从 iPhone 扬声器播放。mandeepj 问「Gemma 4 E2B 对你的需求来说太重了吗」。the_black_hand 补了一条相关观察:他试过用 LLM 解析乐谱,效果非常差。
- karmelapple 描述了一个有趣的听感:听到《致爱丽丝》开头,然后被带向完全不同的方向,「意外地令人不安」。ABNW 持相反看法:他觉得这很清新有趣而非不安,展示了 AI 如何以新颖方式外推,是个有意思的特性。
- ghm2199 提出了唯一的实质性质疑,写得很长:让机器生成音符是不是把即兴演奏的乐趣拿走了?他描述学钢琴早期最大的乐趣是获得两种直觉——空间(手指在键盘上的落点,从中央 C、G、F 这些地标音出发上下移动,类似从 f 和 j 键出发的盲打)和时间(边弹边把节拍读出来,4/4 拍数「1-2-3-4」,十六分音符粒度数「1-e-and-a-2-e-and-a…」),把空间与时间内化后手指的舞蹈本身极其令人满足,而这项技能才通向旅程的终点即即兴。elwell 用作者自己的话回应:他找到了另一种乐趣——「我想要自己把问题想通的快乐,而不是照着别人的研究实现一遍」,ghm2199 回了「说得公道」。yieldcrv 给出通用回答:你以前能为自我实现而做这件事,现在照样能。debugnik 反驳说大多数人(即便是为自我实现)都是通过工作获得必要的经验积累的,一份无关的工作会占掉太多时间。
- hliyan 引出了一段怀旧支线:MIDI 文件是当年唯一能真正从网上下载的音乐类型,还得在天没亮的时候起来下,免得拨号调制解调器把电话费打爆。killerstorm 纠正说 MIDI 至今在专业音乐制作中广泛使用,当年听起来搞笑是因为播放它的合成器不好;otabdeveloper4 断言 MIDI 是协议,永远不会消失,「有些协议(比如 I2C 或 MIDI)大概会跟着人类走到最后」。andai 讲了个有意思的例子:RuneScape 从内置的微软 MIDI 换成自研音频引擎那天起,一切听起来都不对了,连青蛙都不对,因为对他来说那些粗糙的 MIDI 音色就是这个游戏的全部性格。