这篇文章会围绕近期的一篇论文和对应的开源项目——GraphMuse Python库展开。不过,在切入正题之前,先铺垫一下符号音乐处理的一些基础知识。
故事是这样开始的……
所谓符号音乐处理,核心就是从乐谱中提取信息。这里的“符号”二字,泛指各种乐谱或音乐记谱中间出现的标记。乐谱里除了音符,还藏着拍号、调号、表情记号、力度记号等等。这些乐谱的格式也五花八门,比如MIDI、MusicXML、MEI、Kern、ABC等等。
这几年,图神经网络(GNN)在各个领域都火得不行,从生物网络到推荐系统,再到音乐分析,都能看到它的身影。在音乐分析这块,GNN已经被用来解决和声分析、乐句切分、声部分离这些具体任务。核心理念其实很直观:把乐谱里的每个音符看作图中的一个节点,而节点之间的边,则通过音符之间的时间关系来定义,就像下图展示的那样。

这些边一共分为四种类型:
- 同时开始的音符,用“起始”边(蓝色)连接;
- 某个音符结束后紧接着开始的音符,用“连续”边(红色)连接;
- 在某个音符开始和结束之间开始的音符,用“期间”边(绿色)连接;
- 当出现休止时,所有最后结束的音符,与接下来第一个开始的音符,通过“静止”边(黄色)相连。
这种最小化的图建模方式,能确保乐谱从头到尾都是连续连接的,不会出现孤立的子图。
什么是 GraphMuse
GraphMuse是一个Python库,专门用于训练和应用深度图模型,来处理乐谱音乐分析。它提供了加载器、模型和一系列工具,把GNN和符号音乐处理结合在了一起。底层基于PyTorch和PyTorch Geometric构建,保证了较高的灵活性和互操作性。PyTorch是开源机器学习库,能高效构建深度学习模型并支持GPU加速;而PyTorch Geometric则是基于PyTorch的库,让图神经网络的编写和训练变得简单很多。
最关键的是,GraphMuse提供了将乐谱转换成图的功能。这个图的创建过程是用C代码实现的,并通过Python绑定来加速,速度比之前基于numpy的实现快了将近300倍,这一点相当硬核。
科学基础
图在音乐分析和表示中其实是个老面孔了。像Tonnetz、申克分析、树形结构分析这些方法都离不开图。图的优势在于,它可以通过设计不同的边,同时捕捉音乐的层次感和序列感。
基于图来做符号音乐处理,再结合GNN,这个方向最早可以追溯到2021年,当时有人用了一个从乐谱生成演奏的模型。从那以后,陆续出现了很多图模型,有些在音乐分析任务上已经达到了当前最先进的水平。
既然已经聊了图的必要性,接下来就得面对设计和训练符号音乐图模型时真正棘手的部分了。图与音乐结合的主要难点在于:音乐作品的篇幅长短不一,生成的图大小也天差地别。一首巴赫的合唱可能只有两百个音符,而一首贝多芬的奏鸣曲轻轻松松就能超过五千个。在我们的图里,音符的数量直接决定了每个乐谱图中节点的数量。
要在乐谱图上高效快速地训练,可不是件容易的事。这需要一套采样方法,能够在不降低模型性能的前提下,最大化计算资源的利用率,包括内存和时间,有时候甚至还能提升模型性能。训练过程中,采样的意思是把不同乐谱的图组合起来,形成一个新图,在计算机科学里通常叫做一个“批次”。每个批次会被送入GNN模型,计算损失值,这个损失值用来回传并更新模型参数。这个过程会重复很多次,直到训练收敛,模型达到理想状态。听着挺复杂?别担心,GraphMuse能够帮你搞定这些。
GraphMuse 的内部工作原理
GraphMuse里,符号音乐乐谱的通用图处理/训练流程,大致分为这么几步:
- 预处理乐谱数据库,生成输入图——这一步GraphMuse可以快速完成;
- 对输入图进行采样,创建内存高效的批次——GraphMuse也能搞定;
- 把多个采样后的输入图组合成一个新图,里面包含各个采样图的节点和边;对每个图,选定一组节点作为目标节点,目标节点的邻居也可以按需获取,这叫节点采样;
- 通过图卷积更新目标节点的表示,生成节点嵌入——GraphMuse提供了一些可用的模型,你也可以用PyTorch Geometric;
- 把这些嵌入应用到具体任务上——这部分就靠你自己发挥了。
需要注意的是,根据采样策略不同,目标节点可能包含批次节点的全部或一部分。
流程讲清楚了,再详细看看GraphMuse是怎么处理每个乐谱中的音符采样的。

每个乐谱的采样过程
- 先随机选一个音符(黄色)作为采样起点;
- 计算目标音符的边界,示例里预算是15个音符(粉色和黄色音符);
- 接着获取目标音符的k跳邻居(浅蓝色是1跳,深蓝色是2跳)。k跳邻居是根据输入图来计算的(图里由连接音符的彩色边表示);
- 还可以扩展采样,把节拍和小节元素也包含进来。注意,k跳邻居不一定和时间窗口严格挂钩。
为了最大化计算资源利用,上述过程会同时对多个乐谱重复,形成一个批次。GraphMuse通过这个过程,确保每个采样片段的目标音符数量相同。每个采样片段可以组合成一个新图,大小最多是乐谱数量乘以目标音符数量,这个新图就构成了当前训练步骤的批次。
GraphMuse 实操
到了动手环节,我们用GraphMuse和一个模型来处理音高拼写任务。这个任务说的是,当乐谱里缺少音符名称和变音符号时,去推断这些信息。比如,拿到一个量化后的MIDI文件,希望生成下面这样的乐谱:

在安装GraphMuse之前,得先把PyTorch和PyTorch Geometric装好。请根据你自己的系统,去对应的官方页面找到合适的版本。搞定之后,在终端里输入下面这行命令:
pip install graphmuse
装好之后,从URL读一个MIDI文件,然后用GraphMuse创建乐谱图:
import graphmuse as gm
midi_url_raw = "https://github.com/CPJKU/partitura/raw/refs/heads/main/tests/data/midi/bach_midi_score.mid"
graph = gm.load_midi_to_graph(midi_url_raw)
底层会先通过Partitura读取文件,再由GraphMuse进一步处理。
为了训练音高拼写模型,需要一个已经标注好的乐谱数据集。这里使用ASAP数据集(CC BY-NC-SA 4.0许可),作为模型学习的基础。可以从GitHub下载:
git clone https://github.com/cpjku/asap-dataset.git
ASAP数据集包含各种古典钢琴作品的乐谱和演奏数据。对于这个用例,只使用后缀为.musicxml的乐谱文件。加载数据集时,需要两个关键工具:一个用来编码音高拼写,另一个用来处理调号信息,它们都会被转换成数值标签。GraphMuse预构建的音高拼写模型里已经提供了这些工具。接下来导入必要的包,加载第一个乐谱来上手:
import graphmuse as gm
import partitura as pt
import os
import torch
import numpy as np
# 数据集所在目录,请改成你自己的路径
dataset_dir ="/your/path/to/the/asap-dataset"
# 找到数据集中所有名为'xml_score.musicxml'的文件
score_files =[os.path.join(dp, f)for dp, dn, filenames in os.walk(dataset_dir)for f in filenames if f =='xml_score.musicxml']
# 先用前30个乐谱,你可以根据需要调整这个数字
score_files = score_files[:30]
# 加载第一个乐谱文件
score = pt.load_score(score_files[0])
# 提取特征和音符数组
features, f_names = gm.utils.get_score_features(score)
na = score.note_array(include_pitch_spelling=True, include_key_signature=True)
# 用特征创建图
graph = gm.create_score_graph(features, score.note_array())
# 获取输入特征的大小和图的元数据
in_feats = graph["note"].x.shape[1]
metadata = graph.metadata()
# 创建音高拼写预测模型
model = gm.nn.models.PitchSpellingGNN(
in_feats=in_feats, n_hidden=128, out_feats_enc=64, n_layers=2, metadata=metadata, add_seq=True
)
# 创建音高和调号标签的编码器
pe = model.pitch_label_encoder
ke = model.key_label_encoder
接下来,加载数据集中剩余的乐谱文件,继续为模型训练准备数据:
# 初始化一个列表来存放图和编码器
graphs =[]
# 逐个处理乐谱文件
for score_file in score_files:
# 加载乐谱
score = pt.load_score(score_file)
# 提取特征和音符数组
features, f_names = gm.utils.get_score_features(score)
na = score.note_array(include_pitch_spelling=True, include_key_signature=True)
# 编码音高和调号标签
labels_pitch = pe.encode(na)
labels_key = ke.encode(na)
# 用特征创建图
graph = gm.create_score_graph(features, score.note_array())
# 把编码后的标签添加到图中
graph["note"].y_pitch = torch.from_numpy(labels_pitch).long()
graph["note"].y_key = torch.from_numpy(labels_key).long()
# 把图加入列表
graphs.append(graph)
图结构准备好之后,接着创建数据加载器,这部分由GraphMuse提供。同时,还需要定义标准的训练组件,比如损失函数和优化器:
# 创建一个DataLoader,从图中采样子图
loader = gm.loader.MuseNeighborLoader(graphs, subgraph_size=100, batch_size=16, num_neighbors=[3, 3])
# 定义音高和调号预测的损失函数
loss_pitch = torch.nn.CrossEntropyLoss()
loss_key = torch.nn.CrossEntropyLoss()
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
这里稍微解释一下gm.loader.MuseNeighborLoader。这是GraphMuse里的核心数据加载器,包含了前面提到的采样过程。subgraph_size指的是每个输入图的目标节点数量,batch_size是每批次采样的图数量,而num_neighbors则指定了每层中每个采样节点的邻居数量。
万事俱备,终于可以开始训练模型了:
# 训练5个轮次
for epoch in range(5):
loss =0
i =0
for batch in loader:
# 清空梯度
optimizer.zero_grad()
# 获取节点和边的邻居掩码,提升训练效率
neighbor_mask_node ={k: batch[k].neighbor_mask for k in batch.node_types}
neighbor_mask_edge ={k: batch[k].neighbor_mask for k in batch.edge_types}
# 模型前向传播
pred_pitch, pred_key = model(
batch.x_dict, batch.edge_index_dict, neighbor_mask_node, neighbor_mask_edge,
batch["note"].batch[batch["note"].neighbor_mask ==0]
)
# 计算音高和调号预测的损失
loss_pitch_val = loss_pitch(pred_pitch, batch["note"].y_pitch[batch["note"].neighbor_mask ==0])
loss_key_val = loss_key(pred_key, batch["note"].y_key[batch["note"].neighbor_mask ==0])
# 总损失
loss_val = loss_pitch_val + loss_key_val
# 反向传播和优化
loss_val.backward()
optimizer.step()
# 累积损失
loss += loss_val.item()
i +=1
# 打印每个轮次的平均损失
print(f"Epoch {epoch} Loss {loss / i}")
如果一切顺利,很快就能看到损失值在下降,说明模型正在有效学习音高拼写。

为什么选择 GraphMuse?
GraphMuse这个框架,核心目标就是让基于图的符号音乐处理模型的训练和部署变得更简单。对于那些希望重新训练、部署或微调先前最先进的符号音乐分析模型的人来说,GraphMuse提供了一些必要的组件,能更快、更高效地重建和重新训练模型。而对于那些希望做原型设计、创新和设计新模型的人来说,GraphMuse通过保持简洁来保留灵活性。它提供的是简单工具的组合,而不是一套复杂的、可能阻碍创新的管道。至于想学习、可视化或者亲自体验一下的人,GraphMuse是一个不错的起点。通过几行代码,就能轻松上手基本功能。而且,GraphMuse还跟MusGViz关联,可以方便地将图和乐谱一起可视化。
局限性与未来计划
任何项目都有两面性,不能光说好话。GraphMuse毕竟是个新生项目,目前还比较简单,专注在涵盖图学习的基本部分,而不是一个包罗万象的完整框架。因此,在上述流程的很多环节,它仍然高度依赖用户自己的实现。跟每个正在成长的开源项目一样,GraphMuse也需要大家的帮助才能发展壮大。如果发现了bug,或者希望增加更多功能,欢迎在GraphMuse的GitHub项目里报告、提需求或者直接贡献代码。
另外,GraphMuse使用了像torch-sparse和torch-scatter这样的C库,还有自己用来加速图创建的C绑定,所以安装过程并不总是那么顺畅。根据用户反馈,Windows上的安装更有挑战性一些,但也不是不可能。
未来的计划包括:把安装过程弄得简单点;增加对更具体任务的支持,包括模型和数据加载器;以及围绕GraphMuse发展一个开源社区,持续推动音乐的图形编码技术向前走。
