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

8786 字
44 分钟
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 级边界)
映射分 f442.81
边界中心 center18.94
偏差 |f − center|423.86
阈值 0–60 各点是否触发跨级13 / 13 全部触发

结论(这条很关键):

偏差 423.86 远大于任何阈值(0~60),所以阈值调多少都不改变结果——问题不在阈值大小,而在”分数尺度与阈值不匹配”。

三个维度的分数尺度实测差了 100 倍以上(厚薄 ~80,虚实 ~540,明暗 ~9400),却共用同一个 20。于是:

维度fcenter偏差实际行为
厚薄80.2976.024.27只标记 B+,不改级(阈值 20 有效)
虚实541.9280.26461.66必然升级
明暗9400.0349.069350.97必然升级

修法(三选一,按推荐度排序):

  1. 维度内标准化:把每个维度的分数量纲统一(例如用训练集分位数映射到 0–100),阈值 20 才恢复”防抖”语义;
  2. 相对阈值:把判定条件从 |f − center| > 20 改成 |f − center| > α·(该维度分数标准差);
  3. 分维度阈值:至少给每个维度各标一个阈值(明暗的阈值不可能是 20,它一个小数点就几十)。

顺带一个发现:同一音频、同一等级,16 kHz 模式下 f=327.93,22.05 kHz 模式下 f=442.81。同一段代码只是采样率变了,分数就差了 115。这就是下面实验二的内容。


3. 实验二:采样率模式对照(把”隐含依赖”量化出来)#

动机:第三篇指出两侧采样率不一致——模型侧用原始采样率,物理侧被 librosa 默认重采样到 22050 Hz。这到底影响多大?

实测(同一音频,以线上默认 22050 为基准):

采样率设置12 段占比最大偏差物理分数最大偏差厚薄 f
线上默认(22050)0080.294
强制 160009.16e-032.58e+0377.115
强制 240009.66e-038.42e+0279.918
强制 320001.51e-024.31e+0371.346
强制 441001.53e-029.60e+0386.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-7
bands_change[5] = [a=26.0197, b=-24.315, center=49.06]

代入公式:

raw = 100 × Σ(w·e) = 100 × (1 × 总能量×1e-7) = 总能量 × 1e-5
f = a·raw + b = 26.0197 × (总能量 × 1e-5) − 24.315

总能量 是绝对能量(与录音音量、时长正相关),所以 f 与响度成正比。实测:

增益总能量(×1e-7)明暗 f(原版)虚实 f(原版)厚薄 f(原版)
×0.050.0091−0.75117.6380.29
×0.10.036269.93120.8280.29
×0.20.1449352.66133.5880.29
×0.50.90552331.77222.9180.29
×1.03.62209400.03541.9280.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.280.00
energy_free把”总能量”列权重置 0f 恒为 −24.32(k=0)116.57(仍随内容变化)80.29
local_rescale去列 + 量纲对齐(乘系数 k)同上(模板失效)541.92(与原版同量纲)80.29

修复后的漂移(local_rescale 模式):

维度修复前漂移修复后漂移结论
厚薄0.001.78e-05本来就免疫,无需改
虚实424.282.64e-04✅ 有效:去掉能量列后不再随响度动,且仍保留区分度
明暗9400.790.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 / B20
aggregate = max策略✅ 成功T3 / S3 / B20.0000
阈值 0.3策略✅ 成功不变0.0000(但超阈值标签数 3→4)
阈值 0.7策略✅ 成功不变0.0000
归一化常量写成 mean=0, std=1数值⚠️ “成功”但结果变了T1 / S3 / B30.6364
std 少乘 2数值⚠️ “成功”但结果变了T3 / S3 / B20.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结构🛑 报错—同上(分类头维度不匹配)

三条结论:

  1. 数值契约最危险:把归一化常量写错,程序不报错,但厚薄等级直接从 T3 变成 T1、明暗从 B2 变成 B3,11 维概率最大差 0.64。这类错误在没有”对拍基准”的项目里几乎不可能发现。
  2. 形状/结构契约最”善良”:改了就报错,不可能悄悄错。mel 带数 与 target_length 之所以爆炸,是因为它们改变了 patch 网格(1212 → 其他),与 pos_embed 和 checkpoint 对不上。
  3. 策略类确实安全:聚合方式与阈值都不影响判级结果(原因见下)。

6.2 为什么”聚合方式”在这条音频上没差别#

音频mean 等级max 等级11 维最大差
12 秒(单切片)T3/S3/B2T3/S3/B20.0000
24 秒(双切片)T3/S3/B3T3/S3/B30.0780

解释:12 秒音频在 10 ms 帧移下约 1200 帧 → 只切出 1 个 1024 帧片段,mean 与 max 对单个片段完全等价。24 秒会切 2 个片段,两种聚合才开始有差别(概率差 0.078,明暗等级从 B2 变 B3)。

调参建议:要评估聚合策略,必须用 > 10.24 秒的音频;短音频上做这个对比得不出任何结论(这是个很容易被忽略的实验设计错误)。

6.3 模型端”可安全调 / 不可动”清单#

可以安全调说明
deviceCPU/GPU 只影响速度(本项目默认 CPU:单次推理实测 3–6 秒,其中模型加载占大头;进程常驻后更快)
aggregatemean(稳)↔ 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,不是靠读代码读出来的,而是靠”强制构造输入”逼出来的:

  1. 看分支:cal_features 里有一条只在”模型判 1 级 + 物理分显著偏高”时才走的递归分支;
  2. 造输入:直接调用 speech_process(path, {'thickness':1,'solidity':1,'brightness':1}),把三个维度都压到最低级 → 必然进那条分支;
  3. 看报错:ValueError: operands could not be broadcast together with shapes (12,) (13,) → 定位到”换模板但没换特征列”;
  4. 修 + 固化成回归用例:修复后把这条”强制三围=1级”的调用写进脚本,以后每次改 speechprocess.py 都跑一遍。

方法论三句话:

  • 覆盖率不是看”跑过多少样本”,而是看关键分支有没有被走到;
  • 逼出 bug 最省力的办法是直接构造极端输入(全 1 级、静音、超短、削波、非音频文件);
  • 修完立刻写成自检脚本(本项目留下了 physics_lab/selftest.py、diag_bad_inputs.py、tuner_lab/selftest.py),否则下次改代码还会重踩。

异常输入清单(本项目实测,全部已覆盖):

输入期望行为实际
空文件(0 字节)400 + “文件是空的”✅
随机二进制400 + “无法解码(NoBackendError)”✅
浏览器录音(WebM/Opus)前端转 WAV 后成功✅
全静音 WAV200 + 警告(占比全 0,分数退化为 b)✅
超短(0.03 s)200 + 警告(STFT 帧数太少)✅
削波音频200 + 警告(谐波失真使分数偏高)✅

8. 改参检查清单(改之前 / 改之后)#

改之前,先回答四个问题:

  1. 这个参数属于哪一类(策略 / 数值 / 形状)?
  2. 它影响的是哪一层(输入特征 / 模型结构 / 物理常量 / 判定规则)?
  3. 改完之后,谁需要跟着改?(例:换特征 → 必须重标定边界;换采样率 → 必须重标定权重)
  4. 我有没有一个对拍基准能证明”只有预期的地方变了”?

改之后,跑三件事:

检查命令/动作通过标准
与原版对拍物理链路实验台「② 链路实验」底部对拍表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

启动方式:

Terminal window
cd tuner_lab
..\.venv_html\Scripts\python.exe app.py # → http://127.0.0.1:5003
# 或双击 start_tuner.bat

**下一篇(第五篇:总结复盘与二次开发)**会把这三篇的”发现”收敛成一份可执行的改进方案:

  1. 把本篇的修复方案落成一版可上线的 speechprocess.py(去能量列 + 维度内标准化 + 相对阈值),并给出前后对比;
  2. 输出项目级复盘:哪些设计值得保留(占比归一化、两级融合、对拍机制)、哪些必须改(尺度不统一、采样率未固定、缺”刚柔”维度);
  3. 一份二次开发清单:加第四维度、序数建模、设备域泛化、不确定度区间,各自的工作量与依赖;
  4. 把”从零复现一个项目”的方法论沉淀成可复用的模板(调研 → 跑通 → 拆解 → 改参 → 复盘)。

系列定位: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 级边界)
映射分 f442.81
边界中心 center18.94
偏差 |f − center|423.86
阈值 0–60 各点是否触发跨级13 / 13 全部触发

结论(这条很关键):

偏差 423.86 远大于任何阈值(0~60),所以阈值调多少都不改变结果——问题不在阈值大小,而在”分数尺度与阈值不匹配”。

三个维度的分数尺度实测差了 100 倍以上(厚薄 ~80,虚实 ~540,明暗 ~9400),却共用同一个 20。于是:

维度fcenter偏差实际行为
厚薄80.2976.024.27只标记 B+,不改级(阈值 20 有效)
虚实541.9280.26461.66必然升级
明暗9400.0349.069350.97必然升级

修法(三选一,按推荐度排序):

  1. 维度内标准化:把每个维度的分数量纲统一(例如用训练集分位数映射到 0–100),阈值 20 才恢复”防抖”语义;
  2. 相对阈值:把判定条件从 |f − center| > 20 改成 |f − center| > α·(该维度分数标准差);
  3. 分维度阈值:至少给每个维度各标一个阈值(明暗的阈值不可能是 20,它一个小数点就几十)。

顺带一个发现:同一音频、同一等级,16 kHz 模式下 f=327.93,22.05 kHz 模式下 f=442.81。同一段代码只是采样率变了,分数就差了 115。这就是下面实验二的内容。


3. 实验二:采样率模式对照(把”隐含依赖”量化出来)#

动机:第三篇指出两侧采样率不一致——模型侧用原始采样率,物理侧被 librosa 默认重采样到 22050 Hz。这到底影响多大?

实测(同一音频,以线上默认 22050 为基准):

采样率设置12 段占比最大偏差物理分数最大偏差厚薄 f
线上默认(22050)0080.294
强制 160009.16e-032.58e+0377.115
强制 240009.66e-038.42e+0279.918
强制 320001.51e-024.31e+0371.346
强制 441001.53e-029.60e+0386.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-7
bands_change[5] = [a=26.0197, b=-24.315, center=49.06]

代入公式:

raw = 100 × Σ(w·e) = 100 × (1 × 总能量×1e-7) = 总能量 × 1e-5
f = a·raw + b = 26.0197 × (总能量 × 1e-5) − 24.315

总能量 是绝对能量(与录音音量、时长正相关),所以 f 与响度成正比。实测:

增益总能量(×1e-7)明暗 f(原版)虚实 f(原版)厚薄 f(原版)
×0.050.0091−0.75117.6380.29
×0.10.036269.93120.8280.29
×0.20.1449352.66133.5880.29
×0.50.90552331.77222.9180.29
×1.03.62209400.03541.9280.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.280.00
energy_free把”总能量”列权重置 0f 恒为 −24.32(k=0)116.57(仍随内容变化)80.29
local_rescale去列 + 量纲对齐(乘系数 k)同上(模板失效)541.92(与原版同量纲)80.29

修复后的漂移(local_rescale 模式):

维度修复前漂移修复后漂移结论
厚薄0.001.78e-05本来就免疫,无需改
虚实424.282.64e-04✅ 有效:去掉能量列后不再随响度动,且仍保留区分度
明暗9400.790.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 / B20
aggregate = max策略✅ 成功T3 / S3 / B20.0000
阈值 0.3策略✅ 成功不变0.0000(但超阈值标签数 3→4)
阈值 0.7策略✅ 成功不变0.0000
归一化常量写成 mean=0, std=1数值⚠️ “成功”但结果变了T1 / S3 / B30.6364
std 少乘 2数值⚠️ “成功”但结果变了T3 / S3 / B20.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结构🛑 报错—同上(分类头维度不匹配)

三条结论:

  1. 数值契约最危险:把归一化常量写错,程序不报错,但厚薄等级直接从 T3 变成 T1、明暗从 B2 变成 B3,11 维概率最大差 0.64。这类错误在没有”对拍基准”的项目里几乎不可能发现。
  2. 形状/结构契约最”善良”:改了就报错,不可能悄悄错。mel 带数 与 target_length 之所以爆炸,是因为它们改变了 patch 网格(1212 → 其他),与 pos_embed 和 checkpoint 对不上。
  3. 策略类确实安全:聚合方式与阈值都不影响判级结果(原因见下)。

6.2 为什么”聚合方式”在这条音频上没差别#

音频mean 等级max 等级11 维最大差
12 秒(单切片)T3/S3/B2T3/S3/B20.0000
24 秒(双切片)T3/S3/B3T3/S3/B30.0780

解释:12 秒音频在 10 ms 帧移下约 1200 帧 → 只切出 1 个 1024 帧片段,mean 与 max 对单个片段完全等价。24 秒会切 2 个片段,两种聚合才开始有差别(概率差 0.078,明暗等级从 B2 变 B3)。

调参建议:要评估聚合策略,必须用 > 10.24 秒的音频;短音频上做这个对比得不出任何结论(这是个很容易被忽略的实验设计错误)。

6.3 模型端”可安全调 / 不可动”清单#

可以安全调说明
deviceCPU/GPU 只影响速度(本项目默认 CPU:单次推理实测 3–6 秒,其中模型加载占大头;进程常驻后更快)
aggregatemean(稳)↔ 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,不是靠读代码读出来的,而是靠”强制构造输入”逼出来的:

  1. 看分支:cal_features 里有一条只在”模型判 1 级 + 物理分显著偏高”时才走的递归分支;
  2. 造输入:直接调用 speech_process(path, {'thickness':1,'solidity':1,'brightness':1}),把三个维度都压到最低级 → 必然进那条分支;
  3. 看报错:ValueError: operands could not be broadcast together with shapes (12,) (13,) → 定位到”换模板但没换特征列”;
  4. 修 + 固化成回归用例:修复后把这条”强制三围=1级”的调用写进脚本,以后每次改 speechprocess.py 都跑一遍。

方法论三句话:

  • 覆盖率不是看”跑过多少样本”,而是看关键分支有没有被走到;
  • 逼出 bug 最省力的办法是直接构造极端输入(全 1 级、静音、超短、削波、非音频文件);
  • 修完立刻写成自检脚本(本项目留下了 physics_lab/selftest.py、diag_bad_inputs.py、tuner_lab/selftest.py),否则下次改代码还会重踩。

异常输入清单(本项目实测,全部已覆盖):

输入期望行为实际
空文件(0 字节)400 + “文件是空的”✅
随机二进制400 + “无法解码(NoBackendError)”✅
浏览器录音(WebM/Opus)前端转 WAV 后成功✅
全静音 WAV200 + 警告(占比全 0,分数退化为 b)✅
超短(0.03 s)200 + 警告(STFT 帧数太少)✅
削波音频200 + 警告(谐波失真使分数偏高)✅

8. 改参检查清单(改之前 / 改之后)#

改之前,先回答四个问题:

  1. 这个参数属于哪一类(策略 / 数值 / 形状)?
  2. 它影响的是哪一层(输入特征 / 模型结构 / 物理常量 / 判定规则)?
  3. 改完之后,谁需要跟着改?(例:换特征 → 必须重标定边界;换采样率 → 必须重标定权重)
  4. 我有没有一个对拍基准能证明”只有预期的地方变了”?

改之后,跑三件事:

检查命令/动作通过标准
与原版对拍物理链路实验台「② 链路实验」底部对拍表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

启动方式:

Terminal window
cd tuner_lab
..\.venv_html\Scripts\python.exe app.py # → http://127.0.0.1:5003
# 或双击 start_tuner.bat

**下一篇(第五篇:总结复盘与二次开发)**会把这三篇的”发现”收敛成一份可执行的改进方案:

  1. 把本篇的修复方案落成一版可上线的 speechprocess.py(去能量列 + 维度内标准化 + 相对阈值),并给出前后对比;
  2. 输出项目级复盘:哪些设计值得保留(占比归一化、两级融合、对拍机制)、哪些必须改(尺度不统一、采样率未固定、缺”刚柔”维度);
  3. 一份二次开发清单:加第四维度、序数建模、设备域泛化、不确定度区间,各自的工作量与依赖;
  4. 把”从零复现一个项目”的方法论沉淀成可复用的模板(调研 → 跑通 → 拆解 → 改参 → 复盘)。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

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

本文链接

https://study.aokana.cn/posts/2026-09-24-音色评估-项目拆解-四-调试改参与变式测试-162/
作者
乌冬面
发布于
2026-09-24
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
2026-09-23 【音色评估】项目拆解(三):逐行拆解代码
项目拆解系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:把"能跑通的代码"变成"能讲清楚的代码"——不看源码也能说出每一步在做什么、数据形状怎么变、每个常量从哪来。 与前面的衔接:第一篇给了全景(双路线融合、四模块);第 1.5 篇给了底层知识(STFT/梅尔/fbank/Fisher/ViT);第二篇把环境跑通了,并留下两个"跑通即暴露"的 bug。
2
2026-09-22 【音色评估】项目拆解(二):环境搭建与跑通
项目拆解系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:把项目从"一堆压缩包"变成「一条命令能跑、一个接口能验、一份环境手册能复刻」的可用状态。 与前面的衔接:第一篇定了"项目是什么"(双路线融合、四模块分工);第 1.5 篇补了底层知识(STFT / 梅尔刻度 / Fisher 判别 / fbank / ViT)。本篇不再重复概念,只讲实操与排错。
3
2026-09-11 【音色评估】项目拆解(一):前置调研与全局认知
项目拆解系列定位:本系列共5篇文章,从零完整复现并深度吃透「声音色客观评估系统」,覆盖「调研摸底→环境跑通→代码拆解→调试改参→复盘沉淀」全流程。 目录 1. 项目定位:业务背景与核心价值 2. 前置基础:音色三维度的声学本质 2.1 数字音频基础概念 2.2 语音声学核心特征 2.3 「厚薄/明暗/虚实」的声学对应 3. 系统核心架构:双路线融合设计全解析 3.1 整体流程总览 3.2 路线一:物理声学
4
2026-09-14 【音色评估】项目拆解(一点五)前置基础知识全解
项目拆解系列定位:本系列共 5 篇文章(第 1 篇前置调研 → 第 2 篇环境跑通 → 第 3 篇代码拆解 → 第 4 篇调试改参 → 第 5 篇复盘沉淀)。 本篇是第 1 篇与第 2 篇之间的"知识补丁":第 1 篇告诉你"项目是什么",但里面出现的采样定理、STFT、梅尔刻度、Fisher 判别、ViT、MAE、BCE、mAP、ffbank、patch、AMP、DDP 等概念,如果基础不牢,第 2 篇
5
2026-10-25 【泛知碎藏】让真话更容易说出口:从六种聊天方法,理解一个人为什么愿意多说一点
泛知碎藏本文由 B 站创作者「站在外面的Q」的视频《话术博弈》如何优雅的套出对方的真心话?引出。下文保留视频的六条方法线索,研究证据、应用边界和综合讨论为补充延伸。除明确标注的原视频示例外,文中新增的日常情境与对话均为帮助理解而构拟,并非真实个案;真实研究会交代其研究对象与结论范围。 为什么越想问清楚 越容易听到场面话 “你到底怎么想的?”有时是一句关心,有时却像一张必须马上交卷的考卷。朋友说新工作“还
随机文章随机推荐

一起讨论,一起进步

交流想法,也欢迎提出建议。

评论
游客发言

讨论区

评论加载中…

Profile Image of the Author
乌冬面
形而上学,不行退学。
bilibiliSteamMyGameBlog
公告
很难说这是个有用的网站。 或许会存放大量不明所以的东西。
分类
标签
最新动态
站点统计
文章
41
分类
9
标签
45
总字数
218,155
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.14.5
文章许可
CC BY-NC-SA 4.0
1
0. 先把参数分成三类(这决定了”改了会不会报错”)
2
1. 调试前的三件准备(否则只能凭感觉)
3
2. 实验一:阈值敏感性(error_threshold 从 0 扫到 60)
4
3. 实验二:采样率模式对照(把”隐含依赖”量化出来)
5
4. 实验三:抗增益实验(响度到底影响多大、影响在哪)
6
5. 实验四:响度 → 分数的机制与修复(本篇重点)
5.1 机制:明暗的分数为什么”就是音量”
5.2 修复方案:能删列的删列,不能删列的要换特征
5.3 明暗的正确修法(工程步骤)
5.4 修复清单小结
7
6. 实验五:模型端调参(不能只调物理端)
6.1 参数三分类与实测结果
6.2 为什么”聚合方式”在这条音频上没差别
6.3 模型端”可安全调 / 不可动”清单
8
7. 断点调试:怎么把”没被走到的分支”逼出来
9
8. 改参检查清单(改之前 / 改之后)
10
9. 本篇产出物 + 下一篇预告
11
0. 先把参数分成三类(这决定了”改了会不会报错”)
12
1. 调试前的三件准备(否则只能凭感觉)
13
2. 实验一:阈值敏感性(error_threshold 从 0 扫到 60)
14
3. 实验二:采样率模式对照(把”隐含依赖”量化出来)
15
4. 实验三:抗增益实验(响度到底影响多大、影响在哪)
16
5. 实验四:响度 → 分数的机制与修复(本篇重点)
5.1 机制:明暗的分数为什么”就是音量”
5.2 修复方案:能删列的删列,不能删列的要换特征
5.3 明暗的正确修法(工程步骤)
5.4 修复清单小结
17
6. 实验五:模型端调参(不能只调物理端)
6.1 参数三分类与实测结果
6.2 为什么”聚合方式”在这条音频上没差别
6.3 模型端”可安全调 / 不可动”清单
18
7. 断点调试:怎么把”没被走到的分支”逼出来
19
8. 改参检查清单(改之前 / 改之后)
20
9. 本篇产出物 + 下一篇预告