2026-09-24 【音色评估】项目拆解(四):调试改参与变式测试

系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:从”看懂代码”进阶到”能改、敢改、知道改了会怎样”。所有实验不需要新数据,全部用项目自带音频即可复跑。
与前面的衔接:第三篇把两条管线的数据契约讲透了(15 维特征、六套模板、1213 位置编码)。本篇就沿着这些契约故意去改,看哪一类参数会把系统改坏、哪一类安全。
配套可运行试验台:tuner_lab/(端口 5003,明亮主题,物理端 + 模型端同一页),本文每个数字都能在页面上点出来。
0. 先把参数分成三类(这决定了”改了会不会报错”)
调试最容易踩的坑是”以为调参就是改数字”。实际上一段成熟代码里的参数有完全不同的性质:
| 类别 | 定义 | 改了会怎样 | 本项目例子 |
|---|---|---|---|
| 策略类 | 只影响”怎么用模型/怎么取舍”,不改模型本身 | ✅ 安全,可随时调 | aggregate(mean/max)、threshold、device、error_threshold |
| 数值类 | 影响输入/中间量的数值分布 | ⚠️ 静默漂移:不报错,但结果变了 | 归一化常量 mean/std、采样率、特征缩放系数 |
| 形状/结构类 | 决定张量形状或权重的对应关系 | 🛑 直接报错(反而是好事) | num_mel_bins、target_length、use_custom_patch、nb_classes |
这张表就是本篇的路线图:策略类放心调;形状类改了立刻炸,容易发现;数值类最危险——它会给你一个”看起来正常”的错误答案。
排查顺序也由此确定:先看数值类契约(采样率/归一化),再看策略类(聚合/阈值),最后才动形状类。
1. 调试前的三件准备(否则只能凭感觉)
| 准备 | 本项目怎么做 |
|---|---|
| ① 让中间量可见 | 复刻版物理链路把 STFT 参数、12 段占比、15 维、模板、逐项贡献、raw、a/b/center、偏差全部输出(physics_lab/physics.py) |
| ② 有个”对拍基准” | 每次改动后与原函数 speech_process() 逐字段比对:3/3 维度 identical=True 才算没改坏 |
| ③ 有个”确定性输入” | 12 秒合成音频 test_input.wav(分段基频 + 谐波 + 噪声),保证”结果变了 = 代码变了” |
三条缺一不可:没有 ① 只能猜;没有 ② 不知道改坏没;没有 ③ 结果不稳定。
2. 实验一:阈值敏感性(error_threshold 从 0 扫到 60)
动机:第三篇说过阈值 20 是”防抖容差”。它到底保不保守?扫一遍就知道。
做法:固定维度=厚薄、模型等级=1(最容易触发跨级的分支),把阈值从 0 扫到 60。
实测结果(音频 test_input.wav,采样率模式=线上默认):
| 项 | 数值 |
|---|---|
| 所用模板 | #0(厚薄 C 级边界) |
映射分 f | 442.81 |
边界中心 center | 18.94 |
| 偏差 |f − center| | 423.86 |
| 阈值 0–60 各点是否触发跨级 | 13 / 13 全部触发 |
结论(这条很关键):
偏差 423.86 远大于任何阈值(0~60),所以阈值调多少都不改变结果——问题不在阈值大小,而在”分数尺度与阈值不匹配”。
三个维度的分数尺度实测差了 100 倍以上(厚薄 ~80,虚实 ~540,明暗 ~9400),却共用同一个 20。于是:
| 维度 | f | center | 偏差 | 实际行为 |
|---|---|---|---|---|
| 厚薄 | 80.29 | 76.02 | 4.27 | 只标记 B+,不改级(阈值 20 有效) |
| 虚实 | 541.92 | 80.26 | 461.66 | 必然升级 |
| 明暗 | 9400.03 | 49.06 | 9350.97 | 必然升级 |
修法(三选一,按推荐度排序):
- 维度内标准化:把每个维度的分数量纲统一(例如用训练集分位数映射到 0–100),阈值 20 才恢复”防抖”语义;
- 相对阈值:把判定条件从
|f − center| > 20改成|f − center| > α·(该维度分数标准差); - 分维度阈值:至少给每个维度各标一个阈值(明暗的阈值不可能是 20,它一个小数点就几十)。
顺带一个发现:同一音频、同一等级,16 kHz 模式下
f=327.93,22.05 kHz 模式下f=442.81。同一段代码只是采样率变了,分数就差了 115。这就是下面实验二的内容。
3. 实验二:采样率模式对照(把”隐含依赖”量化出来)
动机:第三篇指出两侧采样率不一致——模型侧用原始采样率,物理侧被 librosa 默认重采样到 22050 Hz。这到底影响多大?
实测(同一音频,以线上默认 22050 为基准):
| 采样率设置 | 12 段占比最大偏差 | 物理分数最大偏差 | 厚薄 f |
|---|---|---|---|
| 线上默认(22050) | 0 | 0 | 80.294 |
| 强制 16000 | 9.16e-03 | 2.58e+03 | 77.115 |
| 强制 24000 | 9.66e-03 | 8.42e+02 | 79.918 |
| 强制 32000 | 1.51e-02 | 4.31e+03 | 71.346 |
| 强制 44100 | 1.53e-02 | 9.60e+03 | 86.152 |
结论:占比只差 1e-2 量级,但经过训练好的权重与线性映射(明暗模板 a≈26)后被放大成上千分。
含义:bands_weights / bands_change 这类常量必须连同采样率与重采样实现一起固定,否则等于换了一把尺子。
修法:把采样率写进常量契约(统一 16 k 或 22.05 k),并在 CI/自检里加一条”换环境后分数漂移 < 1”的回归。
4. 实验三:抗增益实验(响度到底影响多大、影响在哪)
做法:同一段音频乘不同增益(0.05× ~ 10×),重算 12 段占比与三维分数。
| 指标 | 数值 |
|---|---|
| 未削波时 12 段占比最大偏差 | 5.96e-08(浮点噪声级) |
| 未削波时分数最大漂移 | 9.401e+03 |
| 含削波样本时占比偏差 | 3.07e-02 |
这一对数字的对比就是全篇最有价值的线索:
占比几乎完全不变(说明”除以总能量”的归一化是成功的),但分数却能漂 9400 → 说明漂移不是来自占比,而是来自额外拼进去的特征。
顺着查 15 维特征向量:前 12 维是”频段能量占比”(抗增益),第 13 维是”总能量×1e-7”(绝对量,随响度线性变化)。再看六套模板谁用了它:
| 维度 | 等级 2/3 所用模板 | 是否含”总能量”列 | 对该维度的后果 |
|---|---|---|---|
| 厚薄 | #1(列 0–11,仅占比) | 否 | ✅ 免疫(实测漂移 0.00) |
| 虚实 | #3(列 0–12,该列权重 4.85) | 是 | ⚠️ 部分敏感(实测漂移 424.28) |
| 明暗 | #5(列 0–12,只有该列权重=1) | 是 | 🛑 完全敏感(实测漂移 9400.79) |
5. 实验四:响度 → 分数的机制与修复(本篇重点)
5.1 机制:明暗的分数为什么”就是音量”
模板 #5 的权重数组长这样(12 个 0 + 最后一位 1):
bands_weights[5] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]bands_index[5] = [0,1,2,3,4,5,6,7,8,9,10,11,12] ← 第 12 列 = 总能量×1e-7bands_change[5] = [a=26.0197, b=-24.315, center=49.06]代入公式:
raw = 100 × Σ(w·e) = 100 × (1 × 总能量×1e-7) = 总能量 × 1e-5f = a·raw + b = 26.0197 × (总能量 × 1e-5) − 24.315总能量 是绝对能量(与录音音量、时长正相关),所以 f 与响度成正比。实测:
| 增益 | 总能量(×1e-7) | 明暗 f(原版) | 虚实 f(原版) | 厚薄 f(原版) |
|---|---|---|---|---|
| ×0.05 | 0.0091 | −0.75 | 117.63 | 80.29 |
| ×0.1 | 0.0362 | 69.93 | 120.82 | 80.29 |
| ×0.2 | 0.1449 | 352.66 | 133.58 | 80.29 |
| ×0.5 | 0.9055 | 2331.77 | 222.91 | 80.29 |
| ×1.0 | 3.6220 | 9400.03 | 541.92 | 80.29 |
一眼可见:明暗完全跟着音量走(幅度上千);虚实也受影响(117→542,漂移 424),因为它的模板 #3 里”总能量”列权重是 4.85;厚薄纹丝不动,因为它的模板 #1 不含该列。
补充一个”顺带发现”:这里的负分(×0.05 时 −0.75)不是错误,而是线性映射外推——
f = a·(raw − raw_min),raw掉到训练集标定区间下界以下就是负分。代码里elif f_timbre < 0正好用它做”B→C 降级”的判据,所以内部不能简单 clamp;但展示层必须处理(否则前端进度条宽度会变成负数或 9400%)。
5.2 修复方案:能删列的删列,不能删列的要换特征
用户在试验台里可以一键切换三种模式,实测结果如下(增益扫描,排除削波样本):
| 模式 | 做法 | 明暗 | 虚实 | 厚薄 |
|---|---|---|---|---|
original | 原版 | 漂移 9400.79 | 漂移 424.28 | 0.00 |
energy_free | 把”总能量”列权重置 0 | f 恒为 −24.32(k=0) | 116.57(仍随内容变化) | 80.29 |
local_rescale | 去列 + 量纲对齐(乘系数 k) | 同上(模板失效) | 541.92(与原版同量纲) | 80.29 |
修复后的漂移(local_rescale 模式):
| 维度 | 修复前漂移 | 修复后漂移 | 结论 |
|---|---|---|---|
| 厚薄 | 0.00 | 1.78e-05 | 本来就免疫,无需改 |
| 虚实 | 424.28 | 2.64e-04 | ✅ 有效:去掉能量列后不再随响度动,且仍保留区分度 |
| 明暗 | 9400.79 | 0.00 | ⚠️ 但代价是分数恒为常数——见下 |
关键发现(这一条比”改好了”更重要):
明暗的 B/A 模板全部信息都在”总能量”那一列。把它去掉后,
raw ≡ 0、f ≡ b = −24.32,分数变成常数——这个模板彻底失效。 也就是说:虚实可以”删列修”,明暗不能——明暗必须换掉这一维特征并重新学权重与边界。
5.3 明暗的正确修法(工程步骤)
| 步骤 | 做什么 | 对应本项目工具 |
|---|---|---|
| ① 换特征 | 把”总能量”换成归一化量:谱质心(占比加权)、谱倾斜(dB/kHz)、LF/HF 比 | extensions.py 已实现(实测:谱质心模式天然抗增益) |
| ② 重学权重 | 在专家分级语料上重跑 Fisher 判别(与厚薄权重同一套流程) | frenquence_check/torchmodel.py 的 FisherLoss |
| ③ 重定边界 | 网格搜索新边界 + calbound 线性映射,得到新的 [a, b, center] | scatterplot.py + calbound.py |
| ④ 回写常量 | 把新的 bands_weights[5] / bands_change[5] / bands_index[5] 写回线上 | html/speechcheck/speechprocess.py |
| ⑤ 回归验证 | 增益扫描(漂移 < 1)+ 与原版对拍(确认只有预期字段变了) | tuner_lab(③ 修复对照)+ physics_lab/selftest.py |
替代方案(没有标注数据时的过渡做法):把该模板的输入结构改成”只吃 12 段占比”(即参照模板 #1 的结构),先保证抗增益;但 center 必须重新标定,否则判定规则会错——这一点千万别省。
5.4 修复清单小结
| 问题 | 影响 | 能否”删列”修 | 正式修法 |
|---|---|---|---|
| 明暗 B/A 模板 #5 只用总能量 | 分数 ∝ 音量(漂移 9400) | ❌ 删列即失效 | 换归一化特征 + 重学权重/边界 |
| 虚实 B/A 模板 #3 含总能量(权重 4.85) | 部分敏感(漂移 424) | ✅ 可删列 + 量纲对齐 | 同上(重标定后更稳) |
| 厚薄模板 #1 不含能量列 | 无 | — | 无需改 |
| 三维共用阈值 20 | 虚实/明暗必然跨级 | — | 维度内标准化 / 相对阈值 |
| 采样率隐含 22050 | 换环境分数漂移上千 | — | 写进常量契约 + 回归测试 |
6. 实验五:模型端调参(不能只调物理端)
物理端调的是”规则”,模型端调的是”输入与推理策略”。用试验台「② 模型端调参」页签可直接跑,实测数据如下。
6.1 参数三分类与实测结果
| 用例 | 类别 | 结果 | 三维等级 | 与基线最大概率差 |
|---|---|---|---|---|
| 基线(mean / 阈值 0.5 / 正确归一化) | — | ✅ 成功 | T3 / S3 / B2 | 0 |
aggregate = max | 策略 | ✅ 成功 | T3 / S3 / B2 | 0.0000 |
| 阈值 0.3 | 策略 | ✅ 成功 | 不变 | 0.0000(但超阈值标签数 3→4) |
| 阈值 0.7 | 策略 | ✅ 成功 | 不变 | 0.0000 |
| 归一化常量写成 mean=0, std=1 | 数值 | ⚠️ “成功”但结果变了 | T1 / S3 / B3 | 0.6364 |
| std 少乘 2 | 数值 | ⚠️ “成功”但结果变了 | T3 / S3 / B2 | 0.2006 |
num_mel_bins = 64 | 形状 | 🛑 报错 | — | RuntimeError: The size of tensor a (256) must match ... |
target_length = 512 | 形状 | 🛑 报错 | — | Error(s) in loading state_dict ... |
use_custom_patch = True | 结构 | 🛑 报错 | — | 同上(与权重不符) |
nb_classes = 9 | 结构 | 🛑 报错 | — | 同上(分类头维度不匹配) |
三条结论:
- 数值契约最危险:把归一化常量写错,程序不报错,但厚薄等级直接从 T3 变成 T1、明暗从 B2 变成 B3,11 维概率最大差 0.64。这类错误在没有”对拍基准”的项目里几乎不可能发现。
- 形状/结构契约最”善良”:改了就报错,不可能悄悄错。
mel 带数与target_length之所以爆炸,是因为它们改变了 patch 网格(1212 → 其他),与pos_embed和 checkpoint 对不上。 - 策略类确实安全:聚合方式与阈值都不影响判级结果(原因见下)。
6.2 为什么”聚合方式”在这条音频上没差别
| 音频 | mean 等级 | max 等级 | 11 维最大差 |
|---|---|---|---|
| 12 秒(单切片) | T3/S3/B2 | T3/S3/B2 | 0.0000 |
| 24 秒(双切片) | T3/S3/B3 | T3/S3/B3 | 0.0780 |
解释:12 秒音频在 10 ms 帧移下约 1200 帧 → 只切出 1 个 1024 帧片段,mean 与 max 对单个片段完全等价。24 秒会切 2 个片段,两种聚合才开始有差别(概率差 0.078,明暗等级从 B2 变 B3)。
调参建议:要评估聚合策略,必须用 > 10.24 秒的音频;短音频上做这个对比得不出任何结论(这是个很容易被忽略的实验设计错误)。
6.3 模型端”可安全调 / 不可动”清单
| 可以安全调 | 说明 |
|---|---|
device | CPU/GPU 只影响速度(本项目默认 CPU:单次推理实测 3–6 秒,其中模型加载占大头;进程常驻后更快) |
aggregate | mean(稳)↔ max(激进),仅对多切片长音频有差别 |
threshold | 只改变”超过阈值的标签列表”,不影响判级(判级用 argmax) |
| 绝对不能动(除非重训) | 为什么 |
|---|---|
num_mel_bins、frame_shift、target_length | 与 checkpoint 的 patch 网格/位置编码绑定 |
mean / std(归一化常量) | 训练时的统计量,改了就静默漂移 |
use_custom_patch、nb_classes、模型结构 | 与权重一一对应 |
| 特征提取库与版本(Kaldi fbank vs librosa mel) | 分布不同 → 精度崩塌 |
7. 断点调试:怎么把”没被走到的分支”逼出来
第三篇那个 (12,) (13,) 的 bug,不是靠读代码读出来的,而是靠”强制构造输入”逼出来的:
- 看分支:
cal_features里有一条只在”模型判 1 级 + 物理分显著偏高”时才走的递归分支; - 造输入:直接调用
speech_process(path, {'thickness':1,'solidity':1,'brightness':1}),把三个维度都压到最低级 → 必然进那条分支; - 看报错:
ValueError: operands could not be broadcast together with shapes (12,) (13,)→ 定位到”换模板但没换特征列”; - 修 + 固化成回归用例:修复后把这条”强制三围=1级”的调用写进脚本,以后每次改
speechprocess.py都跑一遍。
方法论三句话:
- 覆盖率不是看”跑过多少样本”,而是看关键分支有没有被走到;
- 逼出 bug 最省力的办法是直接构造极端输入(全 1 级、静音、超短、削波、非音频文件);
- 修完立刻写成自检脚本(本项目留下了
physics_lab/selftest.py、diag_bad_inputs.py、tuner_lab/selftest.py),否则下次改代码还会重踩。
异常输入清单(本项目实测,全部已覆盖):
| 输入 | 期望行为 | 实际 |
|---|---|---|
| 空文件(0 字节) | 400 + “文件是空的” | ✅ |
| 随机二进制 | 400 + “无法解码(NoBackendError)” | ✅ |
| 浏览器录音(WebM/Opus) | 前端转 WAV 后成功 | ✅ |
| 全静音 WAV | 200 + 警告(占比全 0,分数退化为 b) | ✅ |
| 超短(0.03 s) | 200 + 警告(STFT 帧数太少) | ✅ |
| 削波音频 | 200 + 警告(谐波失真使分数偏高) | ✅ |
8. 改参检查清单(改之前 / 改之后)
改之前,先回答四个问题:
- 这个参数属于哪一类(策略 / 数值 / 形状)?
- 它影响的是哪一层(输入特征 / 模型结构 / 物理常量 / 判定规则)?
- 改完之后,谁需要跟着改?(例:换特征 → 必须重标定边界;换采样率 → 必须重标定权重)
- 我有没有一个对拍基准能证明”只有预期的地方变了”?
改之后,跑三件事:
| 检查 | 命令/动作 | 通过标准 |
|---|---|---|
| 与原版对拍 | 物理链路实验台「② 链路实验」底部对拍表 | 3/3 维度 identical(若故意改了算法,则记录差异清单) |
| 增益回归 | 试验台「③ 修复对照」 | 未削波时分数漂移 < 1 |
| 异常输入 | diag_bad_inputs.py | 全部 200/400,无 500,无 NaN |
9. 本篇产出物 + 下一篇预告
产出物:
- ✅ 调参试验台(
tuner_lab/,端口 5003,明亮主题,① 物理端 / ② 模型端 / ③ 修复对照 / ④ 实验速查 四个页签,可直接点出本文每一个数字) - ✅ 四组实验的真实数据:阈值扫描、采样率对照、抗增益、模型端 10 组参数
- ✅ “响度 → 明暗/虚实”的机制解释与修复方案(含”为什么明暗不能只删列”这一关键结论)
- ✅ 模型端参数三分类清单(可安全调 / 静默漂移 / 直接报错)
- ✅ 断点调试方法论 + 异常输入清单 + 改参检查清单
- ✅ 自检脚本:
run_fix_sweep.py、run_model_sweep.py、selftest.py
启动方式:
cd tuner_lab..\.venv_html\Scripts\python.exe app.py # → http://127.0.0.1:5003# 或双击 start_tuner.bat**下一篇(第五篇:总结复盘与二次开发)**会把这三篇的”发现”收敛成一份可执行的改进方案:
- 把本篇的修复方案落成一版可上线的
speechprocess.py(去能量列 + 维度内标准化 + 相对阈值),并给出前后对比; - 输出项目级复盘:哪些设计值得保留(占比归一化、两级融合、对拍机制)、哪些必须改(尺度不统一、采样率未固定、缺”刚柔”维度);
- 一份二次开发清单:加第四维度、序数建模、设备域泛化、不确定度区间,各自的工作量与依赖;
- 把”从零复现一个项目”的方法论沉淀成可复用的模板(调研 → 跑通 → 拆解 → 改参 → 复盘)。
系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:从”看懂代码”进阶到”能改、敢改、知道改了会怎样”。所有实验不需要新数据,全部用项目自带音频即可复跑。 与前面的衔接:第三篇把两条管线的数据契约讲透了(15 维特征、六套模板、1213 位置编码)。本篇就沿着这些契约故意去改,看哪一类参数会把系统改坏、哪一类安全。 配套可运行试验台:
tuner_lab/(端口 5003,明亮主题,物理端 + 模型端同一页),本文每个数字都能在页面上点出来。
0. 先把参数分成三类(这决定了”改了会不会报错”)
调试最容易踩的坑是”以为调参就是改数字”。实际上一段成熟代码里的参数有完全不同的性质:
| 类别 | 定义 | 改了会怎样 | 本项目例子 |
|---|---|---|---|
| 策略类 | 只影响”怎么用模型/怎么取舍”,不改模型本身 | ✅ 安全,可随时调 | aggregate(mean/max)、threshold、device、error_threshold |
| 数值类 | 影响输入/中间量的数值分布 | ⚠️ 静默漂移:不报错,但结果变了 | 归一化常量 mean/std、采样率、特征缩放系数 |
| 形状/结构类 | 决定张量形状或权重的对应关系 | 🛑 直接报错(反而是好事) | num_mel_bins、target_length、use_custom_patch、nb_classes |
这张表就是本篇的路线图:策略类放心调;形状类改了立刻炸,容易发现;数值类最危险——它会给你一个”看起来正常”的错误答案。
排查顺序也由此确定:先看数值类契约(采样率/归一化),再看策略类(聚合/阈值),最后才动形状类。
1. 调试前的三件准备(否则只能凭感觉)
| 准备 | 本项目怎么做 |
|---|---|
| ① 让中间量可见 | 复刻版物理链路把 STFT 参数、12 段占比、15 维、模板、逐项贡献、raw、a/b/center、偏差全部输出(physics_lab/physics.py) |
| ② 有个”对拍基准” | 每次改动后与原函数 speech_process() 逐字段比对:3/3 维度 identical=True 才算没改坏 |
| ③ 有个”确定性输入” | 12 秒合成音频 test_input.wav(分段基频 + 谐波 + 噪声),保证”结果变了 = 代码变了” |
三条缺一不可:没有 ① 只能猜;没有 ② 不知道改坏没;没有 ③ 结果不稳定。
2. 实验一:阈值敏感性(error_threshold 从 0 扫到 60)
动机:第三篇说过阈值 20 是”防抖容差”。它到底保不保守?扫一遍就知道。
做法:固定维度=厚薄、模型等级=1(最容易触发跨级的分支),把阈值从 0 扫到 60。
实测结果(音频 test_input.wav,采样率模式=线上默认):
| 项 | 数值 |
|---|---|
| 所用模板 | #0(厚薄 C 级边界) |
映射分 f | 442.81 |
边界中心 center | 18.94 |
| 偏差 |f − center| | 423.86 |
| 阈值 0–60 各点是否触发跨级 | 13 / 13 全部触发 |
结论(这条很关键):
偏差 423.86 远大于任何阈值(0~60),所以阈值调多少都不改变结果——问题不在阈值大小,而在”分数尺度与阈值不匹配”。
三个维度的分数尺度实测差了 100 倍以上(厚薄 ~80,虚实 ~540,明暗 ~9400),却共用同一个 20。于是:
| 维度 | f | center | 偏差 | 实际行为 |
|---|---|---|---|---|
| 厚薄 | 80.29 | 76.02 | 4.27 | 只标记 B+,不改级(阈值 20 有效) |
| 虚实 | 541.92 | 80.26 | 461.66 | 必然升级 |
| 明暗 | 9400.03 | 49.06 | 9350.97 | 必然升级 |
修法(三选一,按推荐度排序):
- 维度内标准化:把每个维度的分数量纲统一(例如用训练集分位数映射到 0–100),阈值 20 才恢复”防抖”语义;
- 相对阈值:把判定条件从
|f − center| > 20改成|f − center| > α·(该维度分数标准差); - 分维度阈值:至少给每个维度各标一个阈值(明暗的阈值不可能是 20,它一个小数点就几十)。
顺带一个发现:同一音频、同一等级,16 kHz 模式下
f=327.93,22.05 kHz 模式下f=442.81。同一段代码只是采样率变了,分数就差了 115。这就是下面实验二的内容。
3. 实验二:采样率模式对照(把”隐含依赖”量化出来)
动机:第三篇指出两侧采样率不一致——模型侧用原始采样率,物理侧被 librosa 默认重采样到 22050 Hz。这到底影响多大?
实测(同一音频,以线上默认 22050 为基准):
| 采样率设置 | 12 段占比最大偏差 | 物理分数最大偏差 | 厚薄 f |
|---|---|---|---|
| 线上默认(22050) | 0 | 0 | 80.294 |
| 强制 16000 | 9.16e-03 | 2.58e+03 | 77.115 |
| 强制 24000 | 9.66e-03 | 8.42e+02 | 79.918 |
| 强制 32000 | 1.51e-02 | 4.31e+03 | 71.346 |
| 强制 44100 | 1.53e-02 | 9.60e+03 | 86.152 |
结论:占比只差 1e-2 量级,但经过训练好的权重与线性映射(明暗模板 a≈26)后被放大成上千分。
含义:bands_weights / bands_change 这类常量必须连同采样率与重采样实现一起固定,否则等于换了一把尺子。
修法:把采样率写进常量契约(统一 16 k 或 22.05 k),并在 CI/自检里加一条”换环境后分数漂移 < 1”的回归。
4. 实验三:抗增益实验(响度到底影响多大、影响在哪)
做法:同一段音频乘不同增益(0.05× ~ 10×),重算 12 段占比与三维分数。
| 指标 | 数值 |
|---|---|
| 未削波时 12 段占比最大偏差 | 5.96e-08(浮点噪声级) |
| 未削波时分数最大漂移 | 9.401e+03 |
| 含削波样本时占比偏差 | 3.07e-02 |
这一对数字的对比就是全篇最有价值的线索:
占比几乎完全不变(说明”除以总能量”的归一化是成功的),但分数却能漂 9400 → 说明漂移不是来自占比,而是来自额外拼进去的特征。
顺着查 15 维特征向量:前 12 维是”频段能量占比”(抗增益),第 13 维是”总能量×1e-7”(绝对量,随响度线性变化)。再看六套模板谁用了它:
| 维度 | 等级 2/3 所用模板 | 是否含”总能量”列 | 对该维度的后果 |
|---|---|---|---|
| 厚薄 | #1(列 0–11,仅占比) | 否 | ✅ 免疫(实测漂移 0.00) |
| 虚实 | #3(列 0–12,该列权重 4.85) | 是 | ⚠️ 部分敏感(实测漂移 424.28) |
| 明暗 | #5(列 0–12,只有该列权重=1) | 是 | 🛑 完全敏感(实测漂移 9400.79) |
5. 实验四:响度 → 分数的机制与修复(本篇重点)
5.1 机制:明暗的分数为什么”就是音量”
模板 #5 的权重数组长这样(12 个 0 + 最后一位 1):
bands_weights[5] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]bands_index[5] = [0,1,2,3,4,5,6,7,8,9,10,11,12] ← 第 12 列 = 总能量×1e-7bands_change[5] = [a=26.0197, b=-24.315, center=49.06]代入公式:
raw = 100 × Σ(w·e) = 100 × (1 × 总能量×1e-7) = 总能量 × 1e-5f = a·raw + b = 26.0197 × (总能量 × 1e-5) − 24.315总能量 是绝对能量(与录音音量、时长正相关),所以 f 与响度成正比。实测:
| 增益 | 总能量(×1e-7) | 明暗 f(原版) | 虚实 f(原版) | 厚薄 f(原版) |
|---|---|---|---|---|
| ×0.05 | 0.0091 | −0.75 | 117.63 | 80.29 |
| ×0.1 | 0.0362 | 69.93 | 120.82 | 80.29 |
| ×0.2 | 0.1449 | 352.66 | 133.58 | 80.29 |
| ×0.5 | 0.9055 | 2331.77 | 222.91 | 80.29 |
| ×1.0 | 3.6220 | 9400.03 | 541.92 | 80.29 |
一眼可见:明暗完全跟着音量走(幅度上千);虚实也受影响(117→542,漂移 424),因为它的模板 #3 里”总能量”列权重是 4.85;厚薄纹丝不动,因为它的模板 #1 不含该列。
补充一个”顺带发现”:这里的负分(×0.05 时 −0.75)不是错误,而是线性映射外推——
f = a·(raw − raw_min),raw掉到训练集标定区间下界以下就是负分。代码里elif f_timbre < 0正好用它做”B→C 降级”的判据,所以内部不能简单 clamp;但展示层必须处理(否则前端进度条宽度会变成负数或 9400%)。
5.2 修复方案:能删列的删列,不能删列的要换特征
用户在试验台里可以一键切换三种模式,实测结果如下(增益扫描,排除削波样本):
| 模式 | 做法 | 明暗 | 虚实 | 厚薄 |
|---|---|---|---|---|
original | 原版 | 漂移 9400.79 | 漂移 424.28 | 0.00 |
energy_free | 把”总能量”列权重置 0 | f 恒为 −24.32(k=0) | 116.57(仍随内容变化) | 80.29 |
local_rescale | 去列 + 量纲对齐(乘系数 k) | 同上(模板失效) | 541.92(与原版同量纲) | 80.29 |
修复后的漂移(local_rescale 模式):
| 维度 | 修复前漂移 | 修复后漂移 | 结论 |
|---|---|---|---|
| 厚薄 | 0.00 | 1.78e-05 | 本来就免疫,无需改 |
| 虚实 | 424.28 | 2.64e-04 | ✅ 有效:去掉能量列后不再随响度动,且仍保留区分度 |
| 明暗 | 9400.79 | 0.00 | ⚠️ 但代价是分数恒为常数——见下 |
关键发现(这一条比”改好了”更重要):
明暗的 B/A 模板全部信息都在”总能量”那一列。把它去掉后,
raw ≡ 0、f ≡ b = −24.32,分数变成常数——这个模板彻底失效。 也就是说:虚实可以”删列修”,明暗不能——明暗必须换掉这一维特征并重新学权重与边界。
5.3 明暗的正确修法(工程步骤)
| 步骤 | 做什么 | 对应本项目工具 |
|---|---|---|
| ① 换特征 | 把”总能量”换成归一化量:谱质心(占比加权)、谱倾斜(dB/kHz)、LF/HF 比 | extensions.py 已实现(实测:谱质心模式天然抗增益) |
| ② 重学权重 | 在专家分级语料上重跑 Fisher 判别(与厚薄权重同一套流程) | frenquence_check/torchmodel.py 的 FisherLoss |
| ③ 重定边界 | 网格搜索新边界 + calbound 线性映射,得到新的 [a, b, center] | scatterplot.py + calbound.py |
| ④ 回写常量 | 把新的 bands_weights[5] / bands_change[5] / bands_index[5] 写回线上 | html/speechcheck/speechprocess.py |
| ⑤ 回归验证 | 增益扫描(漂移 < 1)+ 与原版对拍(确认只有预期字段变了) | tuner_lab(③ 修复对照)+ physics_lab/selftest.py |
替代方案(没有标注数据时的过渡做法):把该模板的输入结构改成”只吃 12 段占比”(即参照模板 #1 的结构),先保证抗增益;但 center 必须重新标定,否则判定规则会错——这一点千万别省。
5.4 修复清单小结
| 问题 | 影响 | 能否”删列”修 | 正式修法 |
|---|---|---|---|
| 明暗 B/A 模板 #5 只用总能量 | 分数 ∝ 音量(漂移 9400) | ❌ 删列即失效 | 换归一化特征 + 重学权重/边界 |
| 虚实 B/A 模板 #3 含总能量(权重 4.85) | 部分敏感(漂移 424) | ✅ 可删列 + 量纲对齐 | 同上(重标定后更稳) |
| 厚薄模板 #1 不含能量列 | 无 | — | 无需改 |
| 三维共用阈值 20 | 虚实/明暗必然跨级 | — | 维度内标准化 / 相对阈值 |
| 采样率隐含 22050 | 换环境分数漂移上千 | — | 写进常量契约 + 回归测试 |
6. 实验五:模型端调参(不能只调物理端)
物理端调的是”规则”,模型端调的是”输入与推理策略”。用试验台「② 模型端调参」页签可直接跑,实测数据如下。
6.1 参数三分类与实测结果
| 用例 | 类别 | 结果 | 三维等级 | 与基线最大概率差 |
|---|---|---|---|---|
| 基线(mean / 阈值 0.5 / 正确归一化) | — | ✅ 成功 | T3 / S3 / B2 | 0 |
aggregate = max | 策略 | ✅ 成功 | T3 / S3 / B2 | 0.0000 |
| 阈值 0.3 | 策略 | ✅ 成功 | 不变 | 0.0000(但超阈值标签数 3→4) |
| 阈值 0.7 | 策略 | ✅ 成功 | 不变 | 0.0000 |
| 归一化常量写成 mean=0, std=1 | 数值 | ⚠️ “成功”但结果变了 | T1 / S3 / B3 | 0.6364 |
| std 少乘 2 | 数值 | ⚠️ “成功”但结果变了 | T3 / S3 / B2 | 0.2006 |
num_mel_bins = 64 | 形状 | 🛑 报错 | — | RuntimeError: The size of tensor a (256) must match ... |
target_length = 512 | 形状 | 🛑 报错 | — | Error(s) in loading state_dict ... |
use_custom_patch = True | 结构 | 🛑 报错 | — | 同上(与权重不符) |
nb_classes = 9 | 结构 | 🛑 报错 | — | 同上(分类头维度不匹配) |
三条结论:
- 数值契约最危险:把归一化常量写错,程序不报错,但厚薄等级直接从 T3 变成 T1、明暗从 B2 变成 B3,11 维概率最大差 0.64。这类错误在没有”对拍基准”的项目里几乎不可能发现。
- 形状/结构契约最”善良”:改了就报错,不可能悄悄错。
mel 带数与target_length之所以爆炸,是因为它们改变了 patch 网格(1212 → 其他),与pos_embed和 checkpoint 对不上。 - 策略类确实安全:聚合方式与阈值都不影响判级结果(原因见下)。
6.2 为什么”聚合方式”在这条音频上没差别
| 音频 | mean 等级 | max 等级 | 11 维最大差 |
|---|---|---|---|
| 12 秒(单切片) | T3/S3/B2 | T3/S3/B2 | 0.0000 |
| 24 秒(双切片) | T3/S3/B3 | T3/S3/B3 | 0.0780 |
解释:12 秒音频在 10 ms 帧移下约 1200 帧 → 只切出 1 个 1024 帧片段,mean 与 max 对单个片段完全等价。24 秒会切 2 个片段,两种聚合才开始有差别(概率差 0.078,明暗等级从 B2 变 B3)。
调参建议:要评估聚合策略,必须用 > 10.24 秒的音频;短音频上做这个对比得不出任何结论(这是个很容易被忽略的实验设计错误)。
6.3 模型端”可安全调 / 不可动”清单
| 可以安全调 | 说明 |
|---|---|
device | CPU/GPU 只影响速度(本项目默认 CPU:单次推理实测 3–6 秒,其中模型加载占大头;进程常驻后更快) |
aggregate | mean(稳)↔ max(激进),仅对多切片长音频有差别 |
threshold | 只改变”超过阈值的标签列表”,不影响判级(判级用 argmax) |
| 绝对不能动(除非重训) | 为什么 |
|---|---|
num_mel_bins、frame_shift、target_length | 与 checkpoint 的 patch 网格/位置编码绑定 |
mean / std(归一化常量) | 训练时的统计量,改了就静默漂移 |
use_custom_patch、nb_classes、模型结构 | 与权重一一对应 |
| 特征提取库与版本(Kaldi fbank vs librosa mel) | 分布不同 → 精度崩塌 |
7. 断点调试:怎么把”没被走到的分支”逼出来
第三篇那个 (12,) (13,) 的 bug,不是靠读代码读出来的,而是靠”强制构造输入”逼出来的:
- 看分支:
cal_features里有一条只在”模型判 1 级 + 物理分显著偏高”时才走的递归分支; - 造输入:直接调用
speech_process(path, {'thickness':1,'solidity':1,'brightness':1}),把三个维度都压到最低级 → 必然进那条分支; - 看报错:
ValueError: operands could not be broadcast together with shapes (12,) (13,)→ 定位到”换模板但没换特征列”; - 修 + 固化成回归用例:修复后把这条”强制三围=1级”的调用写进脚本,以后每次改
speechprocess.py都跑一遍。
方法论三句话:
- 覆盖率不是看”跑过多少样本”,而是看关键分支有没有被走到;
- 逼出 bug 最省力的办法是直接构造极端输入(全 1 级、静音、超短、削波、非音频文件);
- 修完立刻写成自检脚本(本项目留下了
physics_lab/selftest.py、diag_bad_inputs.py、tuner_lab/selftest.py),否则下次改代码还会重踩。
异常输入清单(本项目实测,全部已覆盖):
| 输入 | 期望行为 | 实际 |
|---|---|---|
| 空文件(0 字节) | 400 + “文件是空的” | ✅ |
| 随机二进制 | 400 + “无法解码(NoBackendError)” | ✅ |
| 浏览器录音(WebM/Opus) | 前端转 WAV 后成功 | ✅ |
| 全静音 WAV | 200 + 警告(占比全 0,分数退化为 b) | ✅ |
| 超短(0.03 s) | 200 + 警告(STFT 帧数太少) | ✅ |
| 削波音频 | 200 + 警告(谐波失真使分数偏高) | ✅ |
8. 改参检查清单(改之前 / 改之后)
改之前,先回答四个问题:
- 这个参数属于哪一类(策略 / 数值 / 形状)?
- 它影响的是哪一层(输入特征 / 模型结构 / 物理常量 / 判定规则)?
- 改完之后,谁需要跟着改?(例:换特征 → 必须重标定边界;换采样率 → 必须重标定权重)
- 我有没有一个对拍基准能证明”只有预期的地方变了”?
改之后,跑三件事:
| 检查 | 命令/动作 | 通过标准 |
|---|---|---|
| 与原版对拍 | 物理链路实验台「② 链路实验」底部对拍表 | 3/3 维度 identical(若故意改了算法,则记录差异清单) |
| 增益回归 | 试验台「③ 修复对照」 | 未削波时分数漂移 < 1 |
| 异常输入 | diag_bad_inputs.py | 全部 200/400,无 500,无 NaN |
9. 本篇产出物 + 下一篇预告
产出物:
- ✅ 调参试验台(
tuner_lab/,端口 5003,明亮主题,① 物理端 / ② 模型端 / ③ 修复对照 / ④ 实验速查 四个页签,可直接点出本文每一个数字) - ✅ 四组实验的真实数据:阈值扫描、采样率对照、抗增益、模型端 10 组参数
- ✅ “响度 → 明暗/虚实”的机制解释与修复方案(含”为什么明暗不能只删列”这一关键结论)
- ✅ 模型端参数三分类清单(可安全调 / 静默漂移 / 直接报错)
- ✅ 断点调试方法论 + 异常输入清单 + 改参检查清单
- ✅ 自检脚本:
run_fix_sweep.py、run_model_sweep.py、selftest.py
启动方式:
cd tuner_lab..\.venv_html\Scripts\python.exe app.py # → http://127.0.0.1:5003# 或双击 start_tuner.bat**下一篇(第五篇:总结复盘与二次开发)**会把这三篇的”发现”收敛成一份可执行的改进方案:
- 把本篇的修复方案落成一版可上线的
speechprocess.py(去能量列 + 维度内标准化 + 相对阈值),并给出前后对比; - 输出项目级复盘:哪些设计值得保留(占比归一化、两级融合、对拍机制)、哪些必须改(尺度不统一、采样率未固定、缺”刚柔”维度);
- 一份二次开发清单:加第四维度、序数建模、设备域泛化、不确定度区间,各自的工作量与依赖;
- 把”从零复现一个项目”的方法论沉淀成可复用的模板(调研 → 跑通 → 拆解 → 改参 → 复盘)。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
本文链接
https://study.aokana.cn/posts/2026-09-24-音色评估-项目拆解-四-调试改参与变式测试-162/评论区
一起讨论,一起进步
交流想法,也欢迎提出建议。
评论加载中…









