VOCALOID歌曲的小样本偏好筛选

好无聊!我要死了!

2026/03/18 10:12:25
字数: 2.2k , 阅读时长: 7 分钟


写在前面

简单说就是想给自己的 V 家收藏做点什么嘛。

大概半年前?有过一次脱离音乐平台自建曲库的尝试,当时想的是把所有歌都存本地,然后用某种神奇的方式管理起来。结果……失败了。具体怎么失败的我已经忘了,大概率是因为懒(划掉),也可能是发现维护成本太高,总之那 1000 多首从网易云批量下载下来的 VOCALOID 就一直躺在硬盘里吃灰。

直到前几天闲来无事翻文件夹,看到那个 VOCALOID/ 目录,突然又来了兴致。

大体思路

嘛,既然要重建曲库,总得有个核心功能对吧。我其实有个具体的目标:从这 1000 多首歌里,用算法帮我筛选出”真正喜欢”的那些

思路是这样的——先问 ChatGPT:”音乐领域的 BERT 是什么?”它告诉我叫 MERT。诶,名字都差不多嘛,BERT 是 Bidirectional Encoder Representations from Transformers,MERT 是 Musical Encoder Representations from Transformers,挺对仗的。

技术方案很简单:

用 MERT 把每首歌编码成一个向量(song-level embedding)。具体做法是均匀抽取 8 个 12 秒的片段,分别过模型拿到 hidden states,对时间维度做 mean pooling,再把 8 个片段的向量平均,最后 L2 归一化就拿到代表一首歌的一个向量拉。

喜欢/不喜欢的偏好数据有些低效,想用 pairwise comparison——随机抽两首歌 A 和 B,我听一下告诉模型”我更喜欢 A”。

等偏好模型训练好了,直接给所有 1000 多首歌打分,就能听自己最喜欢的歌曲了。

技术上选的是 m-a-p/MERT-v1-330M,3.3 亿参数,HuggingFace 上直接 from_pretrained 就可以。在本地跑,不用 API,毕竟 1000 多首歌呢,调用成本太高。

于是开搞。

预处理

第一次踩坑

一开始直接用 GPT 给我的脚本,大概逻辑是:

  • 扫描目录
  • 对每首歌:
    • 抽取 8 个片段
    • 逐个片段丢进模型推理
    • 保存 embedding

在我的 5070 笔记本上跑了一下……坏哦,这也太慢了吧?

总感觉是串行处理的锅——8 个片段要循环 8 次……

于是开始优化。

和 Kimi 讨论了一下,确定了几个优化点:

优化项原理预期效果
片段批处理把一首歌的 8 个片段打包成 batch,一次性送进模型GPU 利用率拉满
自动混合精度 (AMP)torch.cuda.amp.autocast,用 FP16 计算提速 20-30%,省显存
多进程数据加载DataLoader + num_workersCPU 预处理不阻塞 GPU
模型编译torch.compile (PyTorch 2.0+)再快 10-20%

改完代码跑了一下,诶!真的快了很多!从原来那种”看着进度条发呆”变成”唰唰唰”的感觉。

第二次踩坑

正当我美滋滋地看着进度条往前冲时,powershell 窗口突然没了。鼠标也卡死,过了一会才好。

……哈?

看了眼任务管理器,发现是刚才内存占满了,所以卡死。但奇怪啊,5070 有 8GB 显存,我的电脑有 32GB 内存。现在正在排查是哪里的内存泄漏,或者是不是 DataLoader 的 worker 进程没释放。

困,所以先记到这里,等找到原因再继续写。

第三次踩坑与解决

小睡一会。

醒来继续排查,让 GPT 帮我看了下代码,发现是 DataLoader 的 num_workers 设置的问题

简单说就是,我之前设置了 num_workers=4,但 DataLoader 的 worker 进程在处理完数据后没有正确释放,导致内存越积越多,最后把 32GB 内存撑爆了。Poweshell 窗口消失就是因为系统杀掉了占用内存过多的进程。

解决思路也很简单——干脆不用 DataLoader 了!反正音频文件列表已经在内存里了,直接用 Python 的 tqdm 迭代器配合手动 batching 就行。改完后的脚本就是 extract_mert_embeddings_final.py

另外还做了几个小优化:

  • 分段读取音频:不再把整个音频文件读进内存,而是用 soundfile 的 seek 功能只读需要的片段
  • 及时清理显存:每处理一定数量就调用 torch.cuda.empty_cache()
  • 更保守的批处理:batch_size 设为 8,避免一下子塞太多给 GPU

重新跑了一下……二十分钟就全部处理完了! 做对了诶!

这次生成了两个关键文件:

  • manifest.jsonl:记录每首歌的元数据和 embedding 路径
  • embeddings/*.npy:1199 个 1024 维的向量文件

偏好训练

从 MLP 到 XGBoost

有了 embedding,下一步就是训练偏好模型。

一开始 GPT 给我写的方案是用 MLP(多层感知机),就是那种很普通的神经网络,输入 embedding 输出一个分数。但我试了一下发现效果不太行——小样本下神经网络太容易过拟合了

然后 GPT 提到了一个算法 XGBoost

简单说,XGBoost 是一种决策树集成算法,特别擅长处理表格数据。它在小样本下的表现比神经网络稳定得多,而且训练速度快,还能输出特征重要性,看看到底 embedding 的哪些维度对偏好判断更重要。

核心思路是 pairwise ranking

  1. 每次选两首歌 A 和 B
  2. 我听一下,告诉模型”我更喜欢 A”
  3. 模型把 (A, B) 作为一个训练样本,学习”在相似的歌曲中,什么样的特征组合代表’更好’”
  4. 用 XGBoost 的 rank:pairwise 目标函数来优化

GPT 写了 preference_pairwise_webui.py,用 Gradio 做了一个简单的 Web 界面:

python preference_pairwise_webui.py --server_port 7860 --inbrowser

界面长这样:

  • 上面是搜索框,可以搜索歌名找种子歌曲
  • 中间是两首待比较的歌曲,可以直接播放
  • 下面是”更喜欢 A”、”更喜欢 B”、”跳过”三个按钮
  • 右侧实时显示当前 Top 30 推荐

标注 30 个就烦了……

计划是标 100 对以上,让模型充分学习我的偏好。但事实是——标了大概 30 个我就烦了(?)

Pairwise 标注真的很累人!要一直做二选一,听着听着就开始怀疑人生,我感觉我就像那个数据标注女工。

于是我干脆 摆烂了——直接下载 XGBoost 生成的 ranking.csv,取前 100 名复制到 favorites/ 文件夹里。

结果……莫名的好听!

我随机播了几首,发现排名靠前的确实都是我喜欢的风格:MIMI 和 Deco*27……甚至还有几首我忘记名字但一听前奏就“哦哦哦这首!”的老歌。

看来虽然只标了 30 对,但种子歌曲选得好(我挑了大概 15 首确定喜欢的作为种子),加上 XGBoost 本身在小样本下的稳定性,效果已经很不错了。

总结与下一步

做到这里,整个流程是:

步骤工具/脚本耗时产出
提取 embeddingextract_mert_embeddings_final.py~20 分钟1199 个向量
偏好训练preference_pairwise_webui.py~30 分钟(标注)ranking.csv
生成歌单用 OpenCode Agent 复制前 100 名 歌曲2 分钟favorites/ 文件夹

总共不到一小时,就从 1199 首歌里筛选出了 100 首”最可能喜欢”的。

进一步做的话可以……

虽然现在的结果已经让我很满意了,但还有很多可以优化的地方:

现在的 pairwise 选择策略是随机挑排名相近的歌。可以改成不确定性采样——选模型最”纠结”的 pair(也就是模型给出的分数差距最小的),这样每次标注都能带来最大信息增益。

MERT 只编码了音频信息,但可以结合歌词文本(用 BERT 编码)和元数据(艺术家、专辑、年代等),做多模态融合。毕竟有时候喜欢一首歌不只是因为旋律,还因为歌词戳中了某个点。

不过嘛……先让我把这 100 首歌听完再说!

写在最后

这个项目让我重新捡起了那堆吃灰的 VOCALOID 歌曲。算法帮我筛选出来的歌单确实质量很高,甚至有几首我以前经常单曲循环但忘记了名字的宝藏曲目。

技术上最意外的收获是——小样本下 XGBoost 真的比神经网络好用太多。以前总觉得深度学习万能,但这次实践证明,有时候简单的决策树集成反而更靠谱。

还有就是 pairwise 标注比我想象的累多了。如果不是特别闲,可能还是直接用种子歌曲的相似度+10-20个偏好排序就够了(效果已经很不错)。

总之,歌单建好了,耳机戴上,开始循环播放!


代码仓库:Coming sooooooooooooon