可视化与统计分析脚本
本页覆盖 piano_transcription 仓库中的三支分析脚本:utils/plot_statistics.py(训练曲线统计绘图)、utils/plot_for_paper.py(论文级钢琴谱/踏板可视化)以及 pytorch/calculate_score_for_paper.py(推理概率缓存与评分统计计算)。它们共同构成"模型训练完之后如何看结果"这一环节。
Purpose and Scope
本页回答三个问题:
- 训练收敛情况如何看 ——
plot_statistics.py把训练循环落盘的statistics.pkl渲染成 6 张指标曲线图(frame_ap、reg_onset_mae等),输出到results/{select}.pdf。 - 模型输出长什么样 ——
plot_for_paper.py挑选 MAESTRO 测试集中的一首曲目,跑完整推理,把 log-mel 频谱、帧级/onset/offset 真值与预测并排画成 7 行热力图(音符通路)与 7 行折线图(踏板通路),同时用mir_eval打印 note F1,用于论文插图与定性检查。 - 批量指标如何计算 ——
calculate_score_for_paper.py先用infer_prob把所有测试曲目的网络输出概率 pickle 到磁盘(避免后续调阈值时重复 GPU 推理),再由ScoreCalculator基于这些缓存概率计算 mir_eval 指标;其__call__(params)返回平均 F1,被设计为外部 autoth 库阈值搜索的目标函数。
留给兄弟页面的内容:statistics.pkl 的生成方(pytorch/main.py 训练主循环与检查点保存)属于训练主题页面;端到端评估入口 pytorch/evaluate.py 的完整评估流程属于评估主题页面;PianoTranscription、TargetProcessor、RegressionPostProcessor 的内部实现属于推理与后处理页面。本页只在"它们如何被这些分析脚本消费"的边界上引用它们。
Overview
三支脚本共享同一套 workspace 目录约定,都通过 argparse 子命令驱动,且都假定数据已经由前置流程准备好:
plot_statistics.py消费训练阶段写出的workspace/statistics/{filename}/{model_type}/loss_type=.../augmentation=.../max_note_shift=.../batch_size=.../statistics.pkl;plot_for_paper.py与calculate_score_for_paper.py消费workspace/hdf5s/{dataset}/下的 HDF5 数据集(内含waveform、midi_event、midi_event_time与split属性),并调用pytorch/inference.py的PianoTranscription做推理。
三者定位不同但互补:plot_statistics 看的是标量指标随训练迭代的变化(宏观收敛性);plot_for_paper 看的是单个样本上 88 维钢琴谱的预测 vs 真值(微观定性);calculate_score_for_paper 看的是整个测试集上的量化分数与阈值敏感性(可复现的论文数字)。
Architecture
图中可见两条清晰的数据通路:
- 统计通路(左侧):训练脚本产出
statistics.pkl→plot_statistics.py读取并绘图 →results/*.pdf。这条链路上没有任何推理计算,纯 CPU、纯离线。 - 可视化/评分通路(右侧):HDF5 数据集 →
TargetProcessor构造真值 +PianoTranscription前向推理 →RegressionPostProcessor把概率转成音符/踏板事件 →mir_eval计分。plot_for_paper.py与calculate_score_for_paper.py复用完全相同的组件链,区别只在于:前者画图、只处理 1 首曲子;后者把output_dict连同真值一起 pickle 落盘,供后续反复调阈值使用。
这种"组件共享 + 输出缓存"的设计意味着:改一个阈值不需要重跑网络,这正是 calculate_score_for_paper.py 注释里写明的设计意图 —— "Inference the output probabilites on MAESTRO dataset, and write out to disk. This will reduce duplicate computation for later evaluation."
核心实现详解
1. plot_statistics.py —— 训练曲线绘图
plot_statistics.py 是三支脚本中最简单的一支,整个脚本只有一个 plot(args) 函数加 argparse 入口。它的关键结构如下:
1def plot(args):
2
3 # Arguments & parameters
4 workspace = args.workspace
5 select = args.select
6
7 max_plot_iteration = 300001
8 iterations = np.arange(0, max_plot_iteration, 5000)
9 metric_types = ['frame_ap', 'reg_onset_mae', 'reg_offset_mae',
10 'velocity_mae', 'reg_pedal_onset_mae', 'reg_pedal_offset_mae']
11
12 save_out_path = 'results/{}.pdf'.format(select)
13 create_folder(os.path.dirname(save_out_path))
14
15 # Plot
16 fig, axes = plt.subplots(2, 3, figsize=(8, 5))
17 lines = []要点:
- 指标清单即图的面板清单:
metric_types里的 6 个键恰好对应statistics.pkl里每个检查点条目所记录的字段——1 个帧级平均精度(frame_ap)加 5 个回归 MAE(onset、offset、velocity、pedal onset、pedal offset)。2×3 的子图布局就是按这个数量硬编码的。 - 迭代步长 5000:
np.arange(0, 300001, 5000)表明训练脚本每 5000 次迭代评估一次并追加一条记录,最多画到 300k 迭代。x 轴刻度标签['0', '100k', '200k', '300k']与之配套(每 20 个采样点 = 100k 迭代)。 - 输出路径固定为相对路径
results/{select}.pdf,与--workspace无关,create_folder保证目录存在。
核心的数据加载逻辑封装在闭包 _load_metrics 中:
1 def _load_metrics(filename, model_type, loss_type, augmentation,
2 max_note_shift, batch_size, data_type, metric_type):
3 statistics_path = os.path.join(workspace, 'statistics', filename,
4 model_type, 'loss_type={}'.format(loss_type),
5 'augmentation={}'.format(augmentation), 'max_note_shift={}'.format(max_note_shift),
6 'batch_size={}'.format(batch_size), 'statistics.pkl')
7
8 statistics_dict = pickle.load(open(statistics_path, 'rb'))
9
10 if metric_type in statistics_dict[data_type][0].keys():
11 metrics = np.array([statistics[metric_type] for statistics in statistics_dict[data_type]])
12 return metrics
13 else:
14 return None设计意图值得注意的三点:
- 路径模板与训练超参一一对应:
filename/model_type/loss_type=/augmentation=/max_note_shift=/batch_size=/statistics.pkl是训练脚本写入时的镜像约定。绘制哪个实验的曲线,由调用方把这 5 个超参字面量传进来决定,而不是从命令行解析——脚本内部通过select分支硬编码(见下文select == '1a'分支)。 - 容错返回
None而不是抛异常:如果某个指标(例如换用不含踏板头的模型时缺reg_pedal_onset_mae)在第一条记录里不存在,函数静默返回None,调用方据此跳过该条曲线。这解释了为什么同一张图能在"有踏板/无踏板"两种模型下都工作。 statistics_dict[data_type]是按train/test分桶的列表:列表索引即评估时刻,列表元素是包含全部指标的 dict。np.array([s[k] for s in ...])把某一指标的时序抽成一维数组直接喂给plt.plot。
绘图分支目前只有一个 select == '1a',对应论文中的实验 1a(比较 train/test 曲线):
1 if select == '1a':
2
3 for j, metric_type in enumerate(metric_types):
4 lines = []
5 for data_type in ['train', 'test']:
6
7 metrics = _load_metrics('main',
8 'Regress_onset_offset_frame_velocity_CRNN',
9 'regress_onset_offset_frame_velocity_bce', 'none', 0, 12,
10 data_type, metric_type)
11
12 if metrics is not None:
13 line, = axes[j // 3, j % 3].plot(metrics, label=data_type)
14 lines.append(line)
15
16 axes[j // 3, j % 3].set_title(metric_type)
17 axes[j // 3, j % 3].legend(handles=lines, loc=legend_locs[j])
18 axes[j // 3, j % 3].set_ylim(ylims[j][0], ylims[j][1])
19 axes[j // 3, j % 3].set_xlim(0, len(iterations))
20 axes[j // 3, j % 3].xaxis.set_ticks(np.arange(0, len(iterations), 20))
21 axes[j // 3, j % 3].xaxis.set_ticklabels(['0', '100k', '200k', '300k'])
22 axes[j // 3, j % 3].set_xlabel('Iterations')axes[j // 3, j % 3]把一维面板索引映射到 2×3 网格,6 个指标各占一格。ylims与legend_locs是与metric_types等长的预设数组:frame_ap用[0,1](AP 是 0–1 的分数),5 个 MAE 分别用[0,0.5]或[0,0.3];图例位置frame_ap用loc=4(左下)避免遮挡曲线,其余用1(右上)。- 固定模型为
Regress_onset_offset_frame_velocity_CRNN+regress_onset_offset_frame_velocity_bce损失 + 无增强 +max_note_shift=0+batch_size=12,即实验 1a 的具体配置被硬编码在脚本里——这是一支"论文专用"脚本而非通用工具。
2. plot_for_paper.py —— 论文级定性可视化
plot_for_paper.py 的 plot(args) 是一段"推理 + 评分 + 画图"一体的流水线,前半部分与 calculate_score_for_paper.py::infer_prob 几乎逐行相同(构造 PianoTranscription、遍历 HDF5、TargetProcessor 产真值、transcribe 前向),差别在于它只对选中的那一首继续往下走:
1 n = 0
2 for n, hdf5_path in enumerate(hdf5_paths):
3 with h5py.File(hdf5_path, 'r') as hf:
4 if hf.attrs['split'].decode() == split:
5 print(n, hdf5_path)
6
7 if n == 90:
8 # Load audio
9 audio = int16_to_float32(hf['waveform'][:])
10 midi_events = [e.decode() for e in hf['midi_event'][:]]
11 midi_events_time = hf['midi_event_time'][:]
12
13 # Ground truths processor
14 target_processor = TargetProcessor(
15 segment_seconds=len(audio) / sample_rate,
16 frames_per_second=frames_per_second, begin_note=begin_note,
17 classes_num=classes_num)- 曲目选择是硬编码的
n == 90:即满足split条件的第 91 个文件。没有提供命令行参数覆盖,换个曲子需要改源码。 segment_seconds=len(audio) / sample_rate让TargetProcessor按整段音频长度构造真值,而不是训练时的固定片段——保证真值帧数与推理输出帧数一致。
真值与预测对齐后,脚本先算一次 note F1(阈值固定为 0.3/0.3/0.3/0.2),确认这张图对应的模型质量:
1 if True:
2 post_processor = RegressionPostProcessor(100,
3 classes_num=100, onset_threshold=0.3,
4 offset_threshold=0.3,
5 frame_threshold=0.3,
6 pedal_offset_threshold=0.2)
7
8 (est_on_off_note_vels, est_pedal_on_offs) = \
9 post_processor.output_dict_to_note_pedal_arrays(output_dict)
10
11 ref_on_off_pairs = total_dict['ref_on_off_pairs']
12 ref_midi_notes = total_dict['ref_midi_notes']
13
14 est_on_offs = est_on_off_note_vels[:, 0 : 2]
15 est_midi_notes = est_on_off_note_vels[:, 2]
16 est_vels = est_on_off_note_vels[:, 3] * 128
17
18 note_precision, note_recall, note_f1, _ = \
19 mir_eval.transcription.precision_recall_f1_overlap(
20 ref_intervals=ref_on_off_pairs,
21 ref_pitches=note_to_freq(ref_midi_notes),
22 est_intervals=est_on_offs,
23 est_pitches=note_to_freq(est_midi_notes),
24 onset_tolerance=0.05,
25 offset_ratio=0.2,
26 offset_min_tolerance=0.05)
27
28 print('note f1: {:.3f}'.format(note_f1))此段的设计意图:画图前先确认这张图对应的模型质量。est_on_off_note_vels[:, 0:2] 取音符区间、[:, 2] 取 MIDI 音高、[:, 3] * 128 把网络输出的归一化力度映射回 MIDI 力度域;音高经 note_to_freq 转成 Hz 后交给 mir_eval.transcription.precision_recall_f1_overlap(onset 容差 0.05 秒、offset 比例 0.2、最小容差 0.05 秒),打印 note F1。这样论文插图与报告分数来自同一次推理,二者必然自洽。
评分之后进入绘图主体。音符通路画成 7 行共享 x 轴的子图:
1 fig, axs = plt.subplots(7, 1, figsize=(4, 6), sharex=True)
2 mel = librosa.feature.melspectrogram(audio, sr=16000, n_fft=2048, hop_length=160, n_mels=229, fmin=30, fmax=8000).T
3 axs[0].matshow(np.log(mel[bgn : bgn + L]).T, origin='lower', aspect='auto', cmap='jet')
4 axs[1].matshow(target_dict['frame_roll'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)
5 axs[2].matshow(output_dict['frame_output'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)
6 axs[3].matshow(target_dict['reg_onset_roll'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)
7 axs[4].matshow(output_dict['reg_onset_output'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)
8 axs[5].matshow(target_dict['reg_offset_roll'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)
9 axs[6].matshow(output_dict['reg_offset_output'][bgn : bgn + L].T, origin='lower', aspect='auto', cmap='jet', vmin=vmin, vmax=vmax)布局逻辑是刻意设计的"输入 → 真值 → 预测"三段式重复两遍:
| 行 | 内容 | 形状含义 |
|---|---|---|
| 0 | Log mel spectrogram | 229 mel bins × 时间帧(网络输入) |
| 1 | frame_roll 真值 | 88 音符 × 时间帧 |
| 2 | frame_output 预测 | 88 音符 × 时间帧 |
| 3 | reg_onset_roll 真值 | 88 × 时间帧(回归 onset 目标) |
| 4 | reg_onset_output 预测 | 88 × 时间帧 |
| 5 | reg_offset_roll 真值 | 88 × 时间帧 |
| 6 | reg_offset_output 预测 | 88 × 时间帧 |
matshow(...T)转置 +origin='lower':把"时间×音高"矩阵转置成"音高×时间",让低音在下、高音在上,符合钢琴谱阅读习惯。- 可视化窗口
bgn=15500, L=500:只画第 15500 帧起的 500 帧(约 5 秒,100 fps),保证figsize=(4,6)下细节可辨。x 轴刻度[0..499]映射到['0'..'5']秒。 vmin=0, vmax=1固定色标:所有概率型矩阵共用同一色标,目标回归 roll 与输出概率才可直接目视比较;mel 谱单独用np.log压缩动态范围,不设 vmin/vmax。
踏板通路用同样的 7 行布局,但把 matshow 换成 plot 折线(踏板是 1 维时间序列而非 88 维矩阵):
1 fig, axs = plt.subplots(7, 1, figsize=(4, 6), sharex=True)
2 mel = librosa.feature.melspectrogram(audio, sr=16000, n_fft=2048, hop_length=160, n_mels=229, fmin=30, fmax=8000).T
3 axs[0].matshow(np.log(mel[bgn : bgn + L]).T, origin='lower', aspect='auto', cmap='jet')
4 axs[1].plot(target_dict['pedal_frame_roll'][bgn : bgn + L])
5 axs[2].plot(output_dict['pedal_frame_output'][bgn : bgn + L])
6 axs[3].plot(target_dict['reg_pedal_onset_roll'][bgn : bgn + L])
7 axs[4].plot(output_dict['reg_pedal_onset_output'][bgn : bgn + L])
8 axs[5].plot(target_dict['reg_pedal_offset_roll'][bgn : bgn + L])
9 axs[6].plot(output_dict['reg_pedal_offset_output'][bgn : bgn + L])每行折线统一 set_ylim(0, 1.02),y 轴标签为 Value,其余轴样式处理与音符通路一致。两张图分别落盘为 _zz.pdf 与 _zz2.pdf,音频另存 _zz.wav(librosa.output.write_wav)便于人工听音对照。
3. calculate_score_for_paper.py —— 概率缓存与评分
infer_prob:把推理结果变成可复用缓存
1 # Paths
2 hdf5s_dir = os.path.join(workspace, 'hdf5s', dataset)
3 probs_dir = os.path.join(workspace, 'probs',
4 'model_type={}'.format(model_type),
5 'augmentation={}'.format(augmentation), 'dataset={}'.format(dataset),
6 'split={}'.format(split))
7 create_folder(probs_dir)缓存目录名把 model_type / augmentation / dataset / split 四个维度编码进路径,因此不同配置的缓存互不覆盖、可长期并存。
每首曲目的缓存内容是"网络输出 + 真值"打包成的单一 dict:
1 # Pack probabilites to dump
2 total_dict = {key: output_dict[key] for key in output_dict.keys()}
3 total_dict['frame_roll'] = target_dict['frame_roll']
4 total_dict['ref_on_off_pairs'] = ref_on_off_pairs
5 total_dict['ref_midi_notes'] = ref_midi_notes
6 total_dict['ref_velocity'] = ref_velocity
7
8 if 'pedal_frame_output' in output_dict.keys():
9 total_dict['ref_pedal_on_off_pairs'] = \
10 np.array([[event['onset_time'], event['offset_time']] for event in pedal_events])
11 total_dict['pedal_frame_roll'] = target_dict['pedal_frame_roll']
12
13 prob_path = os.path.join(probs_dir, '{}.pkl'.format(get_filename(hdf5_path)))
14 create_folder(os.path.dirname(prob_path))
15 pickle.dump(total_dict, open(prob_path, 'wb'))设计意图:
- 条件写入踏板真值:只有当模型输出里存在
pedal_frame_output键时才附带踏板真值。这使同一套缓存/评分代码天然兼容"音符模型"与"音符+踏板模型"两种架构(配合combine_note_and_pedal_models.py的组合流程)。 get_filename(hdf5_path)作为缓存文件名,保证与源 HDF5 一一对应。- 缓存里同时存概率(
output_dict全量拷贝)和真值(roll + 事件对),后续ScoreCalculator便完全脱离 GPU/HDF5 工作。
ScoreCalculator:以阈值为参数的评分器
1class ScoreCalculator(object):
2 def __init__(self, hdf5s_dir, probs_dir, split, post_processor_type='regression'):
3 """Evaluate piano transcription metrics of the post processed
4 pre-calculated system outputs.
5 """
6 self.split = split
7 self.probs_dir = probs_dir
8 self.frames_per_second = config.frames_per_second
9 self.classes_num = config.classes_num
10 self.velocity_scale = config.velocity_scale
11 self.velocity = True # True | False
12 self.pedal = True
13
14 self.evaluate_frame = True
15 self.onset_tolerance = 0.05
16 self.offset_ratio = 0.2 # None | 0.2
17 self.offset_min_tolerance = 0.05
18
19 self.pedal_offset_threshold = 0.2
20 self.pedal_offset_ratio = 0.2 # None | 0.2
21 self.pedal_offset_min_tolerance = 0.05
22
23 self.post_processor_type = post_processor_type
24
25 (hdf5_names, self.hdf5_paths) = traverse_folder(hdf5s_dir)
26
27 def __call__(self, params):
28 """Calculate metrics of all songs.
29
30 Args:
31 params: list of float, thresholds
32 """
33 stats_dict = self.metrics(params)
34 return np.mean(stats_dict['f1'])__call__签名是关键设计:接收一个params(浮点阈值列表)并返回标量平均 F1。这个形状正是外部自动阈值搜索库(脚本头部sys.path.insert(1, .../autoth))所要求的黑盒目标函数接口——autoth 只需要"给一组阈值 → 返回一个分数",完全不需要知道内部是 mir_eval 还是别的什么。- mir_eval 协议常量集中固化:
onset_tolerance=0.05秒、offset_ratio=0.2、offset_min_tolerance=0.05秒;踏板侧对应pedal_offset_threshold=0.2、pedal_offset_ratio=0.2、pedal_offset_min_tolerance=0.05。注释# None | 0.2暗示关闭 offset 评估的开关值。 - 功能开关:
self.velocity、self.pedal、self.evaluate_frame三个布尔量决定算哪些指标族,默认全开。 - 构造函数只做一次
traverse_folder枚举 HDF5 路径,真正的逐曲评分在metrics(params)里进行(该函数体超出本页摘录范围,未读取,故不作断言)。
Core Flow
流程要点:
- 训练曲线完全离线:
plot_statistics.py不触碰模型,只读一个 pickle,秒级完成。 - 缓存先行:正确的操作顺序是先跑一次
infer_prob(GPU,慢),之后所有阈值实验都基于probs/*.pkl(CPU,快)。 plot_for_paper.py不走缓存:它每次都现场推理,因为只需要一首曲子,且需要原始target_dict里的回归 roll 用于画图(缓存里只存了frame_roll,未存reg_onset_roll等回归真值)。
命令行接口(CLI)
plot_statistics.py
python utils/plot_statistics.py plot --workspace {workspace} --select {select}
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
mode | 子命令 | 是 | — | 目前仅支持 plot,其他值抛 Error argument! |
--workspace | str | 是 | — | workspace 根目录,statistics.pkl 的查找根 |
--select | str | 是 | — | 实验选择分支,当前仅实现 '1a';输出文件名为 results/{select}.pdf |
CLI 入口见源码:
1if __name__ == '__main__':
2
3 parser = argparse.ArgumentParser(description='')
4 subparsers = parser.add_subparsers(dest='mode')
5
6 parser_plot = subparsers.add_parser('plot')
7 parser_plot.add_argument('--workspace', type=str, required=True)
8 parser_plot.add_argument('--select', type=str, required=True)
9
10 args = parser.parse_args()
11
12 if args.mode == 'plot':
13 plot(args)
14
15 else:
16 raise Exception('Error argument!')plot_for_paper.py
该脚本注册了三个子命令 plot / plot2 / plot3,参数基本一致:
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
--workspace | str | 是 | — | workspace 根目录(定位 hdf5s/{dataset}) |
--model_type | str | 是 | — | 传给 PianoTranscription 的模型类型 |
--checkpoint_path | str | plot/plot2 必填 | — | 模型检查点路径 |
--dataset | str | 是 | — | maestro 或 maps |
--split | str | 是 | — | HDF5 属性 split 的过滤值,通常 test |
--post_processor_type | str | 否 | regression | regression 或 onsets_frames(用于与 Google Onsets and Frames 对比) |
--cuda | flag | 否 | False | 启用 GPU 推理 |
1 parser_plot = subparsers.add_parser('plot')
2 parser_plot.add_argument('--workspace', type=str, required=True)
3 parser_plot.add_argument('--model_type', type=str, required=True)
4 parser_plot.add_argument('--checkpoint_path', type=str, required=True)
5 parser_plot.add_argument('--dataset', type=str, required=True, choices=['maestro', 'maps'])
6 parser_plot.add_argument('--split', type=str, required=True)
7 parser_plot.add_argument('--post_processor_type', type=str, default='regression')
8 parser_plot.add_argument('--cuda', action='store_true', default=False)calculate_score_for_paper.py 同样以 argparse 子命令组织,infer_prob 相关参数包括 --workspace、--model_type、--augmentation、--checkpoint_path、--dataset、--split、--post_processor_type、--cuda(见其 docstring 与文件头部导入,具体子命令注册位于未读取的文件后半部分,此处不作断言)。
配置项与常量
脚本从 utils/config.py / pytorch 侧的 config 读取全局常量(sample_rate、segment_seconds、frames_per_second、classes_num、begin_note、velocity_scale)。脚本内硬编码的关键常量汇总如下:
| 常量 | 取值 | 出处 | 作用 |
|---|---|---|---|
max_plot_iteration | 300001 | plot_statistics.py | x 轴迭代上限 |
| 评估采样步长 | 5000 iter | plot_statistics.py | statistics.pkl 每条记录间隔 |
ylims | [[0,1],[0,.5],[0,.5],[0,.3],[0,.3],[0,.3]] | plot_statistics.py | 6 面板 y 轴范围 |
metric_types | 6 项 | plot_statistics.py | 面板指标清单 |
| 曲目序号 | n == 90 | plot_for_paper.py | 选取第 91 个测试曲目 |
| 可视化窗口 | bgn=15500, L=500 | plot_for_paper.py | 起始帧与帧数(≈5 秒) |
| mel 参数 | n_fft=2048, hop=160, n_mels=229, fmin=30, fmax=8000 | plot_for_paper.py | 频谱可视化(与训练特征一致) |
| 固定阈值 | onset/offset/frame 0.3, pedal_offset 0.2 | plot_for_paper.py | 画图前打分用的后处理阈值 |
onset_tolerance | 0.05 s | calculate_score_for_paper.py | mir_eval onset 匹配容差 |
offset_ratio / min_tolerance | 0.2 / 0.05 s | calculate_score_for_paper.py | mir_eval offset 匹配 |
pedal_offset_threshold/ratio/min_tolerance | 0.2 / 0.2 / 0.05 | calculate_score_for_paper.py | 踏板 offset 评估 |
数据模型 / 持久化
本页脚本消费与产出的持久化对象有三个,且都是 pickle:
要点:
statistics.pkl的顶层结构是dict[data_type] -> list[dict],data_type ∈ {train, test};列表下标对应评估时刻(每 5000 迭代一次)。probs/*.pkl(total_dict)的键集合 =output_dict全部键(如frame_output、reg_onset_output、reg_offset_output、velocity_output,以及踏板模型下的pedal_frame_output等)+ 真值键(frame_roll、ref_on_off_pairs、ref_midi_notes、ref_velocity,可选pedal_frame_roll、ref_pedal_on_off_pairs)。- 所有可视化产物(PDF/WAV)都写到当前工作目录的相对路径(
results/、_zz*),而不是 workspace 内——这与 statistics/probs 的 workspace 内路径约定不同,使用时需注意在期望的目录下执行脚本。
Failure Modes, Edge Cases & Concurrency
缺失指标键的静默跳过:_load_metrics 在 metric_type not in statistics_dict[data_type][0].keys() 时返回 None,调用方跳过该条曲线而不报错。这是对"换模型结构导致指标集合变化"的容错,但也意味着画不出某条曲线时不会有任何警告——排查时需先确认 statistics.pkl 首条记录实际包含哪些键(用首条而非任意条判断,隐含假设指标集合在整个训练过程中不变)。
select 未实现分支无提示:plot_statistics.py 只有 select == '1a' 分支;传入其他值时函数体静默结束,只会执行 plt.tight_layout/savefig——生成一张空图(results/{select}.pdf)而打印 Write out to ...。这是已知粗糙点。
plot_for_paper.py 的调试残留:绘图后紧跟 import crash / asdf(未定义名)故意抛 NameError 以中断循环——脚本作者用它作为"只处理一首曲子"的廉价 early-exit。因此该脚本永远以异常结束属预期行为,产物已先落盘。同样,文件头部还有一个被完整注释掉的旧版 plot(其内部也有相同的 import crash / asdf 残留),以及大段被注释的 plot2、plot3 历史实现。
踏板分支的存在性判断:plot_for_paper.py 与 infer_prob 都用 if 'pedal_frame_output' in output_dict.keys() 判别模型是否有踏板头。纯音符模型(如 Regress_onset_offset_frame_velocity_CRNN)不会输出该键,踏板真值也就不会写入缓存——但 plot_for_paper.py 的踏板绘图段仍会无条件引用 pedal_frame_output,即用纯音符模型跑 plot 子命令会在踏板图处 KeyError。这是使用边界,而非通用工具行为。
librosa.output.write_wav 的版本依赖:新版 librosa(≥0.8)已移除 librosa.output 模块,该调用要求旧版 librosa 环境。同理 melspectrogram(audio, sr=..., ...) 使用旧版位置参数风格。这是复现论文插图时的环境约束。
并发:calculate_score_for_paper.py 顶部导入 from concurrent.futures import ProcessPoolExecutor,表明批量评分阶段设计了进程级并行(配合 ScoreCalculator 可 pickle 的设计)。__call__/metrics 返回 numpy 标量与纯 dict 结构,避免跨进程传输不可序列化对象。本页未读取到实际进程池调用代码,具体并行策略不作断言。
Professional Notes
性能:
infer_prob是唯一的 GPU 热路径(逐曲整段前向)。通过probs/*.pkl缓存把"每换一组阈值重跑整个测试集"降为"一次推理 + N 次纯 CPU 后处理"。对于阈值搜索(autoth 往往要评估上百组参数),这个缓存是数量级的提速。plot_statistics.py只读单个 pickle、画 6 条曲线,成本可忽略;真正贵的是上游训练循环里每 5000 迭代一次的 train/test 全量评估。
可复现性:三支脚本把实验配置(模型、损失、增强、batch size、阈值、mir_eval 容差)全部写死在源码或缓存路径名里,复现论文数字必须使用相同字面量;这也意味着它们是"论文实验记录"而非参数化工具。若要扩展(例如画别的实验曲线),正确做法是仿照 select == '1a' 增加分支并在 _load_metrics 里传入对应的超参组合。
扩展点:
ScoreCalculator.__call__(params) -> float是唯一面向外部优化器的接口;接入其它阈值搜索器只需实现同样的可调用签名。plot_for_paper.py的 7 行布局(输入谱 + 真值/预测成对)是可复用模板:新增可视化头(如 velocity 热图)只需在axs中插入对应matshow行并同步调整figsize与刻度循环的范围(注意for i in range(1, 7)这类硬编码下标)。total_dict的键集合是缓存格式的事实契约:新增真值键(例如把reg_onset_roll也存入以支持离线绘图)需要同时更新ScoreCalculator的消费逻辑,否则旧缓存与新代码不兼容。
Related Links
- 训练主循环与
statistics.pkl的生成(pytorch/main.py)→ 训练主题页面 - 端到端评估入口(
pytorch/evaluate.py)→ 评估主题页面 - 推理封装
PianoTranscription(pytorch/inference.py)与音符/踏板模型组合(pytorch/combine_note_and_pedal_models.py)→ 推理与后处理主题页面 - 真值构造与后处理工具(
utils/utilities.py中的TargetProcessor、RegressionPostProcessor、note_to_freq、traverse_folder、get_filename)→ 工具函数主题页面 - 全局常量(
utils/config.py:sample_rate、frames_per_second、classes_num、begin_note、velocity_scale)→ 配置主题页面