Show HN: 我训练了一个1.25亿参数模型,在设备上实现钢琴自动补全

Hacker News Top 模型

摘要

一位开发者训练了一个1.25亿参数的transformer模型,使用MIDI数据和DPO后训练,在设备上实时自动完成钢琴演奏,并发布了适用于iOS的RollTab应用。

我训练了一个1.25亿参数的transformer模型,以实时自动完成钢琴演奏(在iPhone 15上约为每秒108个音符)。<p>这个想法基本上类似于GitHub Copilot或Tabnine,只不过不是通过代码来提示,而是通过在MIDI钢琴上演奏几个音符来提示模型。然后模型会继续你演奏的内容,完全在设备上完成。<p>这个应用是免费的,如果有人想尝试的话。我很乐意回答关于模型、训练、Core ML,或者那些没有成功的事情的问题。
查看原文
查看缓存全文

缓存时间: 2026/08/20 13:14

# 训练一个1.25亿参数的模型实现钢琴自动补全 来源:https://simedw.com/2026/08/20/midi-autocomplete/ **摘要**:我训练了一个1.25亿参数的Transformer模型,用于实时自动补全钢琴演奏(在iPhone 15上可达约108个音符/秒)。性能提升主要来自三个方面:找到合适的MIDI表示方法、对训练数据进行严格清洗,以及添加DPO后训练。 大约一年前,我开始尝试一个想法:将我的MIDI钢琴连接到手机,弹奏一段旋律,然后让AI为我自动补全这首曲子。就像GitHub Copilot,但这是钢琴版。 结果发现这比我预想的要复杂得多。经过十四次实验,现在终于达到了让我满意并值得撰文分享的程度。 你的浏览器不支持视频标签。视频画质较差,因为好手机正忙于运行MIDI模型。这款名为RollTab的应用可免费下载,如果你有MIDI键盘和iPhone/iPad即可使用。[1](https://simedw.com/2026/08/20/midi-autocomplete/#fn:0) ## 音频示例 每段音频都以一段简短的提示音开始,随后是模型生成的延续部分。 **宝可梦 - 真新镇(8个音符提示)** 你的浏览器不支持音频标签。 **最终幻想VI - 蒂娜主题曲(16个音符提示)** 你的浏览器不支持音频标签。 **致爱丽丝(16个音符提示)** 你的浏览器不支持音频标签。 ## MIDI文件包含什么? MIDI文件与MP3或其他音频格式有很大不同。它不存储录制的原始声音,而是将音乐表示为一系列事件序列:在特定时间和力度按下某个音高的琴键、释放琴键、延音踏板状态变化等。其他事件还包括切换乐器或改变音量。 这些事件通常组织为多个音轨。流行音乐或游戏的MIDI文件可能包含旋律、和弦、贝斯、鼓点、弦乐以及多个合成器声部。本项目专注于钢琴延续,因此我主要保留了钢琴类音轨,移除或减弱了其他部分。 ## 如何对音乐进行分词? 为了在这些演奏数据上训练Transformer,我首先需要将MIDI事件转换为模型可以读取和预测的离散序列。最直接的映射方式是为每个MIDI事件创建一个token: ``` NOTE_ON_60_80 # {音高}_{力度} NOTE_OFF_60 # {音高} TIME_SHIFT_12 # {时间步} ``` 如果在`NOTE_ON`token中直接包含音高和力度,词表会迅速膨胀。MIDI有128个音高和128个力度值,因此简单的组合音符开/关词表最多可能有: 仅音符开和音符关就需要128×128=16384个token。实际中可能会对力度进行分箱处理,但基本问题仍然存在:许多组合很少见,模型必须从稀疏的token中学习大量结构。 一种常见的改进是用语法分解表示: ``` [NOTE_ON, PITCH, VELOCITY] | [NOTE_OFF, PITCH] | [TIME_SHIFT, DURATION] ``` 现在输出空间变小了: ``` NOTE_ON / NOTE_OFF / TIME_SHIFT PITCH: 128个值 VELOCITY: 约16个值 DURATION: 约100个值 ``` 生成时可以通过屏蔽无效的下一个token来强制遵循语法。在`NOTE_ON`之后,只有音高token有效;在音高之后,只有力度token有效。这保证了输出在语法上有效。 我尝试过音符开/音符关式的表示,但模型容易产生漂移。它们会忘记发出音符关事件,留下悬挂音符,或失去活动状态跟踪。这对我的目标——在笔记本电脑或手机上近实时运行的小型模型——尤其不利。 另一种我尝试过的表示更接近: ``` [NOTE, PITCH, VELOCITY, DURATION] | [TIME_SHIFT, DURATION] ``` 这避免了音符关漂移,因为音符时长是显式的。时间步token在不演奏音符时推进播放头。 这种表示在音乐上效果更好,但速度很慢。一个音乐音符大约需要四个自回归Transformer步骤。它也会快速消耗上下文窗口。 ## 最终表示方法 我最终确定的表示是: ``` NOTE(pitch, delta_onset, duration, velocity) ``` 最终版本中没有单独的`TIME_SHIFT`事件。静音由下一个音符的`delta_onset`表示:即距离前一个音符起始的时间差。 例如: ``` NOTE(C4, delta=0, duration=12, velocity=80) NOTE(D4, delta=24, duration=12, velocity=80) ``` 表示:演奏C4,等待24个时间步后再开始下一个音符,然后演奏D4。 和弦表示为多个`delta_onset = 0`的音符,按音高排序[2](https://simedw.com/2026/08/20/midi-autocomplete/#fn:1): ``` NOTE(C4, delta=24, duration=24, velocity=80) NOTE(E4, delta=0, duration=24, velocity=78) NOTE(G4, delta=0, duration=24, velocity=82) ``` 它也不是像这样的扁平token流: ``` NOTE, PITCH, DELTA, DURATION, VELOCITY ``` **Transformer不是通过四次传递来生成一个音符的各个属性,而是每次生成一个完整的音符来推进音乐。**实际上,这使大模型在iPhone上能达到约108个音符/秒的速度,远超人类实时演奏所需的速度。 在内部,每个音符有五个分类字段,每个字段都有自己的词表[3](https://simedw.com/2026/08/20/midi-autocomplete/#fn:2),时序被量化为固定步长[4](https://simedw.com/2026/08/20/midi-autocomplete/#fn:3): ``` [event_type, pitch_id, delta_id, duration_id, velocity_id] ``` 每个字段都有自己的嵌入表示。音符token是所有嵌入的总和: ``` note = event_type_embedding[NOTE] + pitch_embedding[C4] + delta_embedding[12] + duration_embedding[24] + velocity_embedding[80] ``` 模型有独立的输出头:音高、时值、时长等。 字段之间有一个小型嵌套解码器,因此后续字段可以依赖于先前预测的字段。但昂贵的Transformer主干网络每个音符只运行一次,而不是每个字段运行一次。 ## 延音踏板 你可能知道,按下钢琴的延音踏板会使音符在你释放琴键后继续发声。我不希望添加延音踏板事件使实现变得复杂。相反,延音效果在预处理阶段被融入音符时长中。 如果琴键在延音踏板按下时释放,音符会延长到踏板抬起的时间。如果同一音高先被再次弹奏,前一个音符会在重新触发时被截断。结果是一个近似实际发声时长的音符时长。 这丢失了显式的踏板动作,但使建模问题简单得多:模型只需预测音高、起始时间、时长和力度。 ## 数据集 我搜索了许多公开可用的数据集和收藏,主要关注公共领域的古典音乐。数据质量参差不齐,因此我最终编写了许多清洗脚本。 最终数据集包含几十万个MIDI文件,代表约3亿个音符事件。 最终处理流程: - 选择钢琴为主的材料 - 移除或减弱病态的多音轨混合 - 根据密度和音高/时间覆盖范围进行过滤 - 通过忽略全局移调和均匀速度变化的指纹进行去重 - 将同一作品的不同版本分组到相同的训练/验证/测试集中 我尝试将数据集扩大到约5倍规模,希望能提升性能,但结果产生的模型表现更差。清洗和选择数据比简单增加数据量更重要。 ## 训练 初始训练只是对五个输出头进行交叉熵损失求和: ``` type_loss + pitch_loss + delta_loss + duration_loss + velocity_loss ``` 这样可以轻松分别跟踪音高、时长和力度的准确率,而不是依赖单一的聚合下一个token损失。 然而,训练目标有一个重要局限:音乐延续没有唯一正确的答案。一首留出歌曲只给模型一个"正确"的下一个音符,尽管通常有许多在音乐上都可行的延续。交叉熵有助于学习音乐机制,但不是评估完整延续效果的好代理。 ## 数据增强 数据增强很重要,因为实时输入不是纯净的MIDI文件。那是我在弹钢琴,而且弹得很糟糕,音符可能稍微提前、延后、力度过大或过小。 最终我确定了以下增强方法: - 全局移调 - 均匀速度缩放 - 时长/力度抖动 - 丢弃提示音符 ## 模型 架构本质上是相当标准的仅解码器Transformer:RMSNorm、旋转位置嵌入、因果自注意力、SwiGLU/MLP块和自回归生成。 我主要训练了三种模型规模: ``` 小型:约3300万参数 中型:约6400万参数 大型:约1.25亿参数 ``` 小型模型适合快速实验,但中型模型几乎总是优于它。大型模型表现更好,尽管优势不是特别明显。 我目前正尝试让中型模型接近大型模型的质量,主要是为了减小iOS应用的占用空间和延迟。 ## 计划采样 我最好的基础模型在每个音符的字段之间使用了计划采样。通常在训练期间,时值和力度预测可以知道正确的音高。但在推理时,它们必须基于模型实际预测的音高工作。 因此在训练期间,我有时会向模型提供它自己预测的音高。前几个周期从0%开始,然后在训练过程中逐渐增加,在最佳模型中达到50%。 有趣的是,这增加了验证损失但改善了延续效果。 Gemini偏好度 ↑ 计划采样50%:64.3% 无计划采样:35.7% 计划采样损害了验证损失,但提高了生成质量。由Gemini进行成对偏好评分。 ## 评估 起初,评估只是我靠耳朵听。 我用4-32个音符的提示从留出歌曲中生成延续,然后手动比较模型输出。这很慢很烦人,一段时间后听什么都像噪音。 4个音符的提示最难:音乐上下文太少。8个音符效果更好,而16-32个音符的提示要可靠得多,因为模型有足够的结构来推断正在发生什么。 无提示的生成很碰运气,但这不是我的目标用例。 我还编写了一些自动评估指标: - 重复的音高n-gram - 音高熵 - 音级熵 - 音域 - 音符密度 - 长停顿 - 和弦密度 这些指标有助于发现明显问题,但不足以选择最佳模型。 最终我使用Gemini 3.5 Flash进行成对评估。要求给出单一绝对评分时结果不一致。而询问"给定A和B,哪个延续更好?"效果好得多,特别是当我对每个比较进行镜像处理以减少位置偏差时[5](https://simedw.com/2026/08/20/midi-autocomplete/#fn:4)。这让我能够构建一个相当大的偏好数据集,然后用于DPO。 最初,Gemini过于关注延续听起来是否悦耳,而不是它与提示的连贯性。输出单独听起来往往更好,但感觉与我刚弹奏的内容脱节。 改进提示有所帮助,但我最终将评估分为两个标准:延续分数,衡量输出与提示的衔接程度;悦耳度分数,衡量其单独的音乐质量。我将延续分数作为DPO的主要信号。 ## DPO:直接偏好优化 DPO在预训练之后带来了最大的差异。它使模型从偶尔产生好的延续转变为更可靠地做到这一点。 对于每个提示,我生成多个延续,并使用成对评估选择一个较好的和一个较差的: ``` prompt -> 被选中的延续 prompt -> 被拒绝的延续 ``` DPO训练模型使被选中的延续比被拒绝的更可能,同时保持与原始模型的合理接近。 DPO后,在我的成对评估中,超过69%的延续比基础模型更受青睐。 β值控制DPO惩罚偏离基础模型的强度。在我的扫描中,β=0.01和β=0.03改善了模型,而β=0.10惩罚过强导致变差。 我还尝试了"共识"数据集:不是相信每个嘈杂的偏好判断,我只保留评估者一致同意的偏好对。这在本次扫描中产生了最佳结果。 预训练基础模型:24.55% β = 0.01:61.08% β = 0.03:57.14% β = 0.10:38.10% 共识数据集(β = 0.03):69.05% 由Gemini进行成对偏好评分。 我的直觉是,基础模型已经学到了合理的音乐心智模型,只是不知道什么构成好的延续。 ## 无效尝试 很多方法没有效果: - 音符开/音符关表示在小型实时模型中漂移太多。 - 语法屏蔽的token流有效但速度慢。 - 更广泛的数据在数据嘈杂时使结果变差。 - 更大的模型有帮助,但没有神奇地解决循环问题。 - Mirostat减少了重复但常使输出不连贯。 - 额外的局部辅助损失使训练变慢且没有明显的听觉收益。 - Gemini的绝对标量评分比成对判断效果差。 - 仅验证损失忽略了生成质量的重要差异。 - Born-again网络(在模型自身软预测上重新训练)在此没有提高质量。 ## 封装应用 我将PyTorch模型导出为Core ML,并将权重量化为INT8。首次启动仍然很慢,因为Apple的运行时需要针对可用硬件优化模型。 模型只在最多512个音符的上下文上训练,但我想支持更长的会话。每当上下文接近限制时,我保留最近的384个音符,从这些音符重建上下文,然后继续。这意味着重建KV缓存,但模型足够快,这没有成为主要问题。 我使用RoPE进行位置编码,因此理论上可以通过移位位置和环形缓冲区实现更优雅的方案。不幸的是Core ML没有直接暴露Q、K和V。 不过,那时候我已经很高兴它能工作了。 你的浏览器不支持视频标签。最终应用完全在设备上运行。 ## 结论 这是一个非常有趣的项目。关于音乐生成有很多有趣的论文,但我刻意在一开始没有深入阅读。我想享受自己解决问题的乐趣,而不是仅仅实现别人的研究成果。事后我才回头将我的方法与现有文献进行了比较[6](https://simedw.com/2026/08/20/midi-autocomplete/#fn:5)。 它仍然远非完美。它偶尔会循环,短提示很难处理,还有很多我想改进的地方。就像GPT-2,但这是钢琴版。 但我终于达到了真正享受坐到钢琴前,弹奏几个音符,然后看看我们共同创作出什么的程度。

相似文章

多任务多帧视觉钢琴转录

Hugging Face Daily Papers

本文介绍了V2N,这是首个完整的视觉钢琴转录系统,可联合从视频中预测起音、释音、按键保持和力度,在PianoVAM和R3基准上取得了最先进的结果。