Repository Wiki
bytedance/piano_transcription

可视化与统计分析脚本

本页覆盖 piano_transcription 仓库中的三支分析脚本:utils/plot_statistics.py(训练曲线统计绘图)、utils/plot_for_paper.py(论文级钢琴谱/踏板可视化)以及 pytorch/calculate_score_for_paper.py(推理概率缓存与评分统计计算)。它们共同构成"模型训练完之后如何看结果"这一环节。

Purpose and Scope

本页回答三个问题:

  1. 训练收敛情况如何看 —— plot_statistics.py 把训练循环落盘的 statistics.pkl 渲染成 6 张指标曲线图(frame_ap、reg_onset_mae 等),输出到 results/{select}.pdf。
  2. 模型输出长什么样 —— plot_for_paper.py 挑选 MAESTRO 测试集中的一首曲目,跑完整推理,把 log-mel 频谱、帧级/onset/offset 真值与预测并排画成 7 行热力图(音符通路)与 7 行折线图(踏板通路),同时用 mir_eval 打印 note F1,用于论文插图与定性检查。
  3. 批量指标如何计算 —— 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

Loading diagram...

图中可见两条清晰的数据通路:

  • 统计通路(左侧):训练脚本产出 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 入口。它的关键结构如下:

python
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 = []

plot_statistics.py

要点:

  • 指标清单即图的面板清单: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 中:

python
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

plot_statistics.py

设计意图值得注意的三点:

  1. 路径模板与训练超参一一对应:filename/model_type/loss_type=/augmentation=/max_note_shift=/batch_size=/statistics.pkl 是训练脚本写入时的镜像约定。绘制哪个实验的曲线,由调用方把这 5 个超参字面量传进来决定,而不是从命令行解析——脚本内部通过 select 分支硬编码(见下文 select == '1a' 分支)。
  2. 容错返回 None 而不是抛异常:如果某个指标(例如换用不含踏板头的模型时缺 reg_pedal_onset_mae)在第一条记录里不存在,函数静默返回 None,调用方据此跳过该条曲线。这解释了为什么同一张图能在"有踏板/无踏板"两种模型下都工作。
  3. statistics_dict[data_type] 是按 train/test 分桶的列表:列表索引即评估时刻,列表元素是包含全部指标的 dict。np.array([s[k] for s in ...]) 把某一指标的时序抽成一维数组直接喂给 plt.plot。

绘图分支目前只有一个 select == '1a',对应论文中的实验 1a(比较 train/test 曲线):

python
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')

plot_statistics.py

  • 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 前向),差别在于它只对选中的那一首继续往下走:

python
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)

plot_for_paper.py

  • 曲目选择是硬编码的 n == 90:即满足 split 条件的第 91 个文件。没有提供命令行参数覆盖,换个曲子需要改源码。
  • segment_seconds=len(audio) / sample_rate 让 TargetProcessor 按整段音频长度构造真值,而不是训练时的固定片段——保证真值帧数与推理输出帧数一致。

真值与预测对齐后,脚本先算一次 note F1(阈值固定为 0.3/0.3/0.3/0.2),确认这张图对应的模型质量:

python
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))

plot_for_paper.py

此段的设计意图:画图前先确认这张图对应的模型质量。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 轴的子图:

python
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)

plot_for_paper.py

布局逻辑是刻意设计的"输入 → 真值 → 预测"三段式重复两遍:

行内容形状含义
0Log mel spectrogram229 mel bins × 时间帧(网络输入)
1frame_roll 真值88 音符 × 时间帧
2frame_output 预测88 音符 × 时间帧
3reg_onset_roll 真值88 × 时间帧(回归 onset 目标)
4reg_onset_output 预测88 × 时间帧
5reg_offset_roll 真值88 × 时间帧
6reg_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 维矩阵):

python
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])

plot_for_paper.py

每行折线统一 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:把推理结果变成可复用缓存

python
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)

calculate_score_for_paper.py

缓存目录名把 model_type / augmentation / dataset / split 四个维度编码进路径,因此不同配置的缓存互不覆盖、可长期并存。

每首曲目的缓存内容是"网络输出 + 真值"打包成的单一 dict:

python
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'))

calculate_score_for_paper.py

设计意图:

  • 条件写入踏板真值:只有当模型输出里存在 pedal_frame_output 键时才附带踏板真值。这使同一套缓存/评分代码天然兼容"音符模型"与"音符+踏板模型"两种架构(配合 combine_note_and_pedal_models.py 的组合流程)。
  • get_filename(hdf5_path) 作为缓存文件名,保证与源 HDF5 一一对应。
  • 缓存里同时存概率(output_dict 全量拷贝)和真值(roll + 事件对),后续 ScoreCalculator 便完全脱离 GPU/HDF5 工作。

ScoreCalculator:以阈值为参数的评分器

python
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'])

calculate_score_for_paper.py

  • __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

Loading diagram...

流程要点:

  1. 训练曲线完全离线:plot_statistics.py 不触碰模型,只读一个 pickle,秒级完成。
  2. 缓存先行:正确的操作顺序是先跑一次 infer_prob(GPU,慢),之后所有阈值实验都基于 probs/*.pkl(CPU,快)。
  3. 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!
--workspacestr是—workspace 根目录,statistics.pkl 的查找根
--selectstr是—实验选择分支,当前仅实现 '1a';输出文件名为 results/{select}.pdf

CLI 入口见源码:

python
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_statistics.py

plot_for_paper.py

该脚本注册了三个子命令 plot / plot2 / plot3,参数基本一致:

参数类型必填默认说明
--workspacestr是—workspace 根目录(定位 hdf5s/{dataset})
--model_typestr是—传给 PianoTranscription 的模型类型
--checkpoint_pathstrplot/plot2 必填—模型检查点路径
--datasetstr是—maestro 或 maps
--splitstr是—HDF5 属性 split 的过滤值,通常 test
--post_processor_typestr否regressionregression 或 onsets_frames(用于与 Google Onsets and Frames 对比)
--cudaflag否False启用 GPU 推理
python
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)

plot_for_paper.py

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_iteration300001plot_statistics.pyx 轴迭代上限
评估采样步长5000 iterplot_statistics.pystatistics.pkl 每条记录间隔
ylims[[0,1],[0,.5],[0,.5],[0,.3],[0,.3],[0,.3]]plot_statistics.py6 面板 y 轴范围
metric_types6 项plot_statistics.py面板指标清单
曲目序号n == 90plot_for_paper.py选取第 91 个测试曲目
可视化窗口bgn=15500, L=500plot_for_paper.py起始帧与帧数(≈5 秒)
mel 参数n_fft=2048, hop=160, n_mels=229, fmin=30, fmax=8000plot_for_paper.py频谱可视化(与训练特征一致)
固定阈值onset/offset/frame 0.3, pedal_offset 0.2plot_for_paper.py画图前打分用的后处理阈值
onset_tolerance0.05 scalculate_score_for_paper.pymir_eval onset 匹配容差
offset_ratio / min_tolerance0.2 / 0.05 scalculate_score_for_paper.pymir_eval offset 匹配
pedal_offset_threshold/ratio/min_tolerance0.2 / 0.2 / 0.05calculate_score_for_paper.py踏板 offset 评估

数据模型 / 持久化

本页脚本消费与产出的持久化对象有三个,且都是 pickle:

Loading diagram...

要点:

  • 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 的消费逻辑,否则旧缓存与新代码不兼容。
  • 训练主循环与 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)→ 配置主题页面