Show HN: 我训练了一个1.25亿参数模型,在设备上实现钢琴自动补全
摘要
一位开发者训练了一个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,但这是钢琴版。
但我终于达到了真正享受坐到钢琴前,弹奏几个音符,然后看看我们共同创作出什么的程度。
相似文章
多任务多帧视觉钢琴转录
本文介绍了V2N,这是首个完整的视觉钢琴转录系统,可联合从视频中预测起音、释音、按键保持和力度,在PianoVAM和R3基准上取得了最先进的结果。
展示HN:公共Muscriptor实例(最新、最强大的Audio-to-MIDI模型)
Muscriptor的公共实例,作为最新、最强大的Audio-to-MIDI模型,现已可用,能通过GPU加速将任何歌曲转换为钢琴MIDI。
@danshipper: Codex 教我弹钢琴
一条推文分享了OpenAI的Codex被用于教钢琴,并附有演示链接。
构建并发布了BetterGPT-150M——一个紧凑的1.5亿参数补全模型(含在线HF Space演示)
构建并发布了BetterGPT-150M,一个紧凑的1.5亿参数因果语言模型,资源占用低,性能优于GPT-2 Small。包含用于文本补全的实时Hugging Face Space演示。
Show HN:Maple-Preview——在 iPhone 上以 120 tok/s 运行的三值 20B MoE
Maple-Preview 是一个三值 20B MoE 模型,在 iPhone 上每秒可运行 120 个 token,展现了高效的端侧推理能力。