前阵子帮一个做硬件的朋友收拾他的语音降噪项目模型结构照着某篇论文复现训练 loss 曲线漂亮得像教科书插图结果塞进设备里一跑出来的声音像隔着一层水在说话。翻了半天代码问题既不在网络深度也不在数据量而是他把 16kHz 的音频重采样到 8kHz 之后梅尔滤波器组的上限频率还按 8kHz 设置等于一半滤波器落在空频段里喂进去的全是噪声特征。这件事又一次提醒我Deep Learning for Audio Signal Processing这个方向的门槛从来不在“会不会搭网络”而在“懂不懂音频”。音频和图像不一样图像你随手 resize 一下还能看音频你随手改一个采样率、错一个窗长模型学到的东西可能就完全跑偏了而且这种错误在 loss 曲线上通常毫无征兆。这篇内容我想把它写成一份可以直接照着做的实操记录从波形到特征的那一段路怎么走、参数怎么算、模型怎么选、训练时哪些默认设置会坑人、出了问题从哪儿开始查。适合刚把 CNN 那套搬到音频上却发现效果不对的同学也适合已经在做语音增强、音频分类、关键词唤醒这类落地项目、但总觉得模型“差一口气”的同行。我不打算写成教科书只写我自己反复验证过、踩过坑的那部分。1. 先把这团波形掰开看整体设计思路与选型逻辑1.1 为什么很少有人把原始波形直接喂进网络理论上端到端是优雅的波形进去标签出来中间不用人操心特征。但真做过几个项目就知道这条路对数据量和算力的胃口大得离谱。一段 10 秒、16kHz 的音频是 16 万个采样点如果按 1 秒切片也有 1.6 万点第一层卷积想覆盖到 20ms 的上下文就得要 320 个采样点的感受野堆叠几层之后参数量和显存占用直接起飞。更麻烦的是原始波形里每个采样点相邻之间高度相关信息密度极低网络要花大量容量去学“怎么把波形变成频谱”这件人类早就用傅里叶变换解决得很好的事情。我一般的判断标准是数据量在百小时以下或者算力只有单卡就别碰原始波形输入。老老实实用频谱特征让网络把容量花在真正需要学习的模式上。反过来如果你手里有上千小时的标注数据、团队有几十张卡、目标是刷某个榜单的最好指标那端到端或者可学习的前端比如在 STFT 之上叠一层可训练卷积确实能榨出一点额外收益但那点收益往往在 1 到 2 个点以内工程性价比要先算清楚。1.2 三种特征表征路线各自适合什么样的活我在实际项目里主要用三条路线它们不是互相替代的关系而是对应不同的任务形态。短时傅里叶变换得到的复数谱保留了幅度和相位全部信息适合语音增强、分离、声源定位这类需要重建波形回去的任务。代价是维度高、相位难建模而且相位在数学上存在缠绕问题直接回归相位经常会得到一堆听起来更糟的结果。梅尔频谱或对数梅尔频谱做了频率轴的非线性压缩更贴近人耳的感知特性维度低、冗余小是音频分类、关键词唤醒、环境声音识别的主力特征。缺点是把相位彻底扔了只适合不需要重建波形的任务。梅尔频率倒谱系数在梅尔谱基础上再做一次离散余弦变换去相关维度可以压到 13 到 40 维。它在传统 GMM-HMM 时代是标配但现在深度模型直接吃梅尔谱效果通常更好因为卷积层本身就在学类似的去相关变换。我的经验是做嵌入式端的极轻量模型时 MFCC 还有优势输入维度小、第一层算力省其余场景优先用对数梅尔谱。1.3 任务决定架构一张我自己在用的选型对照表选架构这件事与其追新不如先看任务对时间分辨率、频率分辨率、输出形态的要求。下面这张表是我这几年攒下来的经验映射不绝对但能省掉很多试错。任务类型推荐输入主流架构输出形态关键约束音频事件分类64 维对数梅尔CNN 或 CRNN片段级标签时间定位要求低可全局池化关键词唤醒40 维对数梅尔深度可分离 CNN、TC-ResNet帧级后验 平滑参数量常在 10 万以内延迟小于 200ms语音增强降噪复数谱或幅度谱U-Net、CRN、复数域网络掩码或复数谱要重建波形必须处理相位多人语音分离复数谱Conv-TasNet 类、DPRNN、SepFormer多路掩码说话人排列不确定损失要置换不变音乐/歌声生成梅尔谱或隐空间自回归、扩散模型长序列频谱长程结构一致性难保证语音识别80 维对数梅尔Conformer、注意力编码器字符或子词序列要对齐需要 CTC 或注意力解码这张表里最容易被忽略的是最后一列。很多人选完架构就直接开跑结果发现模型能训但没法用分离任务不做置换不变损失模型会随机交换两路输出训到天荒地老也不收敛唤醒词任务不做帧级平滑误唤醒率高得没法上线。2. 特征参数不是随便填的手算一遍就明白了2.1 采样率怎么定从物理上限倒推采样率的选择只有一个硬约束奈奎斯特准则采样率必须大于信号最高频率的两倍。人耳听觉上限约 20kHz所以音乐场景常用 44.1kHz 或 48kHz。语音场景则完全没必要人说话的基频通常在 80 到 300Hz清辅音的能量能延伸到 8kHz 左右但绝大部分可懂度信息集中在 300 到 3400Hz 这个区间这也是传统电话系统的带宽来源。所以做语音任务16kHz 是性价比最高的默认值。降到 8kHz 会损失齿音和摩擦音的信息识别率会掉升到 48kHz 除了让数据量和计算量翻三倍对识别、增强这些任务几乎没帮助。做音乐、声学事件检测比如玻璃碎裂、鸟鸣时情况不同这些信号的高频成分有区分度44.1kHz 更合适。这里有个常见的坑数据集里混着不同采样率的音频直接统一 resample 到 16kHz 就完事了吗不是。从 44.1kHz 降到 16kHz 需要先做抗混叠低通滤波主流库默认会做但如果原始音频本身是 8kHz 上采到 16kHz 的很多老语料是这样那 8kHz 以上是空的你按 16kHz 设滤波器组就是在喂零。我养成的习惯是拿到数据先跑一遍采样率统计看看分布再决定统一到哪个值而不是无脑设 16k。2.2 窗长、帧移与频率分辨率的手算过程这一段我想把算式写清楚因为参数之间的关系搞明白了后面调参就不会瞎猜。假设采样率 fs 16000Hz窗长取 25ms那么窗内点数 N_win 0.025 × 16000 400 点。帧移取 10ms对应 160 点。相邻两帧重叠 400 - 160 240 点重叠率 60%。这个 25ms/10ms 的组合是语音领域的经典配置原因是 25ms 大约覆盖 2 到 3 个基频周期短时平稳假设在这个尺度上基本成立10ms 的帧移对应 100 帧每秒足够捕捉音素级别的变化又不至于让序列长到模型扛不住。频率分辨率由 FFT 点数决定。如果直接对 400 点做 FFTΔf 16000 / 400 40Hz。为了让频率刻度更细一般会补零到 512 点此时 Δf 16000 / 512 31.25Hz频率轴的 bin 数变成 257512/2 1。补零不会提高真实的频率分辨能力真实分辨率由窗长决定400 点对应 40Hz只是让频谱采样更密、插值更平滑方便后续对齐到梅尔滤波器组。这里要解释一个容易混淆的点为什么不能把窗长拉得很长来提高频率分辨率因为时频不确定性。窗长 100ms 的话频率分辨率能到 10Hz但在这个窗口里语音信号早就变了短时平稳假设失效得到的是 100ms 内的平均频谱时间定位能力丧失。反过来窗长太短比如 5ms时间分辨率高了但频率轴上糊成一片基频和谐波结构都分不开。25ms 这个值是几十年实践收敛出来的平衡点没有特别的玄机就是够用。2.3 梅尔滤波器组数量怎么定上限频率写多少梅尔刻度的设计初衷是模拟人耳对低频更敏感的特性。换算公式是 m 2595 × log10(1 f/700)反过来 f 700 × (10^(m/2595) − 1)。在 1kHz 以下梅尔刻度近似线性1kHz 以上接近对数。这意味着低频段滤波器密集、高频段稀疏符合听觉感知。滤波器数量的经验值16kHz 采样、做语音识别时用 40 个这也是很多经典工具包的默认值做环境声音分类可以用 64 个做音乐相关任务可以到 96 或 128 个。数量不是越多越好因为高频段的滤波器在梅尔刻度上间隔很大带宽过宽会损失频率选择性数量太少则低频细节被过度平滑。我自己的判断方式是把滤波器组的中心频率打印出来看一眼如果最高几个滤波器的中心频率已经接近甚至超过奈奎斯特频率的一半说明数量给多了那些频段本来就没多少有效能量。上限频率这个参数是我见过最多的翻车点。默认情况下应该设成 fs/2但如果你的数据经过窄带处理比如电话语音只到 4kHz就必须把这个上限调到 4000。前面提到的那个朋友的项目就是这个问题音频重采样到 8kHz 后滤波器组上限还写死在 8000结果 4kHz 到 8kHz 这段全是零能量相当于给模型输入了一批恒定的噪声维度训练时这些维度上的权重乱跳泛化自然差。2.4 归一化与动态范围压缩别小看这一步对数压缩这一步选自然对数还是以 10 为底的分贝影响没想象中大因为后续通常会做标准化。但压缩前加的那个小常数 eps 有讲究加太小比如 1e-10在静音段会出现极大的负值把整段特征的动态范围拉爆加太大比如 1e-3则会把低能量的细节抹平语音增强任务里这直接导致弱音部分重建不出来。我一般用 1e-6然后对整段做一次均值方差归一化。归一化的粒度也值得说。逐句归一化utterance-level是语音任务的主流做法因为它能消除录音设备增益差异带来的影响让模型关注频谱形状而不是绝对能量。但做语音增强时要注意逐句归一化会破坏信噪比的绝对信息如果你的增强目标包含输出增益控制就得改用全局统计量做归一化或者在归一化前把能量特征单独保留一路。还有一点训练集算出来的均值和方差要存下来推理时用同一组统计量。我见过有人推理时对每条音频重新算均值和方差测试集指标看着还行一上线就崩因为线上是流式处理的根本拿不到整句的统计量。3. 模型搭建与训练从基线到能上线3.1 先跑通一个 CRNN 分类基线别一上来就上大模型我的习惯是任何音频任务先用一个朴素基线打通全流程数据读取、特征提取、训练、评估、导出一条链路跑通之后再谈优化。音频分类任务的基线我通常用 CRNN卷积负责局部时频模式循环层负责时间上的长程依赖。import torch import torch.nn as nn class CRNNBaseline(nn.Module): def __init__(self, n_mels64, n_classes10, rnn_hidden128): super().__init__() # 输入形状: (B, 1, T, n_mels) self.conv nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size(2, 2)), # T/2, F/2 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size(2, 2)), # T/4, F/4 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) self.pool_freq nn.AdaptiveAvgPool2d((None, 1)) # 频率轴压成 1 self.rnn nn.GRU(128, rnn_hidden, num_layers2, batch_firstTrue, bidirectionalTrue) self.head nn.Linear(rnn_hidden * 2, n_classes) def forward(self, x): h self.conv(x) # (B, 128, T/4, F/4) h self.pool_freq(h) # (B, 128, T/4, 1) h h.squeeze(-1).transpose(1, 2) # (B, T/4, 128) h, _ self.rnn(h) h h.mean(dim1) # 时间维度平均池化 return self.head(h)这段代码里有两个设计决定值得解释。频率轴用自适应平均池化压成 1而不是展平后接全连接原因是展平会让参数量随输入频率维数线性增长一旦你改梅尔滤波器数量全连接层就得重写而且展平会破坏频率轴上的平移等变性。时间维度用平均池化而不是取最后一帧是因为分类任务里事件可能出现在任意位置平均池化对时间平移更鲁棒。这两个选择在音频分类任务上几乎是默认操作但我在别人的代码里经常看到展平加全连接然后在换特征配置时踩坑。参数量可以手算一下前几层第一层卷积 1×32×3×3 288 个权重第二层 32×64×3×3 18432第三层 64×128×3×3 73728。GRU 部分是大头双向两层、隐层 128大约 40 万参数。整体在 60 万量级单卡跑起来毫无压力。如果要往嵌入式端部署把卷积换成深度可分离卷积、GRU 换成时间卷积参数量能压到 5 万以内。3.2 语音增强的 U-Net掩码比直接回归谱更靠谱增强任务我一般用 U-Net 结构但输出不是直接回归干净频谱而是预测一个比值掩码。原因很实在直接回归幅度谱时模型要同时学“哪些是噪声”和“绝对能量是多少”两件事而掩码形式把绝对能量这个变量消掉了模型只需要判断每个时频点上有多少比例是语音学习难度低很多。预测目标是理想比值掩码定义为干净语音幅度除以混合信号幅度取值范围限制在 0 到 1 之间也可以用 0 到 10 的截断版本给重建留一点余量。损失函数我主要用两个组合幅度谱上的均方误差保证整体能量对齐再加上一个压缩后的谱收敛损失后者对低能量区域更敏感能显著减少残留噪声。如果任务对听感要求高可以再叠一个基于短时客观可懂度的辅助损失但要注意这个指标本身不可微得用近似实现。U-Net 的下采样层数别贪多。频谱输入的频率维通常只有 64 或 128下采 4 次之后就剩 4 到 8 个频带再往下采频率信息基本没了。时间维可以多下采一些因为语音的时间结构冗余更大。我的经验配置是频率维下采 3 次、时间维下采 4 次跳跃连接把每一层的特征拼回去。这个不对称的设计在增强任务上比对称 U-Net 效果好因为它保住了频率分辨率。3.3 训练策略几个不写进论文但很关键的设置学习率调度上音频模型的收敛往往比图像模型慢前期 loss 下降很平容易让人误以为模型没学到东西。我一般用带预热的余弦退火预热步数占总步数的 5% 左右峰值学习率 3e-4 到 1e-3。预热这一步不能省尤其是用 BatchNorm 的网络前几十步统计量还没收敛学习率太大会直接把权重推到一个坏区域。批大小的选择要配合特征长度。音频样本长度不一通常要裁切或填充到固定长度比如 4 秒。4 秒 16kHz 音频对应 400 帧64 维梅尔谱单样本输入是 400×64批大小 32 时显存占用大概 2 到 3GB还留有余量。如果把长度拉到 10 秒批大小就得减半否则显存爆掉。这里有个取舍长片段能让模型看到更多上下文但会减少每个 batch 里的样本多样性。分类任务我倾向短片段2 到 4 秒增强任务可以长一些5 到 10 秒因为增强要重建完整波形太短的片段拼接处会有明显的不连续感。梯度裁剪在音频模型上是必需品尤其是用循环网络或者 Transformer 的时候。音频特征里偶尔会出现异常大的值比如一段爆音的录音梯度爆炸比图像任务更常见。裁剪阈值设 1.0 到 5.0 之间我一般从 5.0 开始试。混合精度训练在音频任务上收益明显因为大部分计算集中在卷积和矩阵乘上显存能省 30% 到 40%速度提升 20% 以上。但要注意损失计算部分尤其是涉及对数、开方的谱损失最好保持在单精度否则小数值下溢会导致 loss 变成 nan而且这种 nan 往往在几十步之后才出现排查起来很费劲。3.4 评估指标用错了等于没评估这件事我想单独强调因为见过太多项目在指标上自欺欺人。语音增强看信噪比类的指标时一定要区分 SI-SNR 和 SDR。SI-SNR 做了尺度不变处理允许输出和参考之间存在整体增益差异衡量的是波形形状的相似度更符合“降噪降得干不干净”的直觉。SDR 对增益敏感如果你的模型输出整体偏小SDR 会很难看但听感上可能只是音量小。实际项目里我通常两个都报再看 PESQ。PESQ 的取值范围是 −0.5 到 4.5很多人以为是 1 到 5。它最初是为窄带电话语音设计的用在宽带或音乐信号上参考价值有限。而且 PESQ 对时移极其敏感如果你的增强模型引入了哪怕几毫秒的延迟PESQ 会断崖式下跌这时候要先做时延对齐再算。识别任务看词错误率公式是替换 删除 插入/ 总词数。这里最容易出错的是文本归一化数字、标点、大小写处理不一致会导致替换错误虚高。我一般会固定一套归一化脚本训练和评估共用绝不在两边各写一份。分类任务除了准确率一定要看混淆矩阵和每类的召回率。音频事件分类里长尾问题特别严重某些类别样本只有几十条整体准确率 90% 可能意味着那几类全错。我习惯按类别算 macro F1比准确率更能反映真实水平。4. 出问题从哪儿查排查实录与速查表4.1 训练 loss 一路降验证集纹丝不动这是最高频的问题我按顺序查这几件事。先看数据划分有没有泄漏。音频数据集常见的情况是同一条录音被切成多个片段然后随机划分结果训练集和验证集里出现同一段录音的不同片段模型等于在背答案。正确做法是按说话人、按录音会话、按原始文件做划分保证验证集里的录音在训练时完全没见过。这个坑我在一个环境声音项目里踩过验证准确率虚高到 98%改成按文件划分后掉到 76%那才是真实水平。再看特征提取的随机性。有些增强操作比如随机裁剪、随机增益只在训练时开验证时关掉但如果你把归一化的统计量也算在增强流程里验证集实际拿到的是训练集的统计量分布对不上。检查方式是打印验证集特征的均值和方差看是否和训练集接近。最后看模型容量和正则的匹配。音频分类任务里如果训练集只有几千条用 ResNet 级别的骨干网络配上默认的 dropout几乎必然过拟合。这时候该加的是 SpecAugment 这类频谱域增强随机遮住时间轴上的一段、随机遮住频率轴上的一条带相当于给模型制造“局部信息缺失”的样本逼它学冗余特征。这个方法的实测效果比调 dropout 明显得多。4.2 增强出来的音频有“水声”和“音乐噪声”这是语音增强的经典病灶听起来像背景里有细碎的音调在跳动或者整体带一层混响感。水声感的成因通常是相位处理不当。如果你只增强幅度谱相位直接用混合信号的原始相位重建出来的波形在时频点之间会有相位不一致听感上就是那种空洞的、像隔层水的声音。缓解办法有两个一是用复数域网络同时预测幅度和相位参数量翻倍但效果最好二是用迭代相位重建算法代价是推理慢。工程上用幅度加原始相位的方案很多前提是增强不要太激进掩码取值留一点余量。音乐噪声的成因是时频掩码在相邻帧之间跳变。模型对每个时频点独立判断相邻帧的预测结果轻微抖动重建时就会产生孤立的窄带音调。解决办法是在掩码上做平滑时间轴上做中值滤波或者训练时在损失里加一项时频平滑正则。我习惯在推理阶段对掩码沿时间轴做 3 帧的中值滤波成本几乎为零效果立竿见影。还有一个容易被忽略的点是训练和推理的片段长度不一致。训练时用 4 秒片段推理时输入整段 30 秒音频BatchNorm 的统计量在长序列上会有偏移。要么推理时也切成 4 秒片段并带重叠拼接要么训练时就把归一化换成不依赖 batch 统计的层。4.3 模型精度够了但实时性上不去端侧部署时这个问题很常见。先算一笔账假设采样率 16kHz、帧移 10ms模型每 10ms 要处理一帧也就是每秒 100 帧。如果模型单帧推理耗时 5ms那么单核占用率 50%还有余量如果耗时 15ms那就跟不上了必须换结构或者降低帧率。优化的顺序我一般是这样的。先换特征配置把帧移从 10ms 改成 20ms序列长度直接减半这是最省力的。再压模型把标准卷积换成深度可分离卷积参数量和计算量能降一个数量级精度损失通常在 1 到 2 个点。然后是算子层面的优化把循环层换成因果卷积或者有限窗注意力因为循环层的串行依赖在硬件上很难并行。最后才是量化8 位定点量化在音频模型上效果通常不错但要注意输入特征的动态范围梅尔谱做完对数之后范围是负值到几十量化前要先做归一化到合理区间。有个细节值得提醒流式推理时卷积的 padding 方式要改成因果的也就是只在左侧补零不能用对称补零。对称补零会让模型“看到未来”离线评估时指标很好看上线后表现断崖式下滑。这个问题在转写别人代码时极难发现因为代码本身没错错的是场景假设。4.4 常见问题速查表现象最可能的原因排查动作处理方式验证指标虚高但上线崩数据划分泄漏按原始文件而非片段划分重新划分按说话人隔离静音段输出刺耳噪声对数压缩的 eps 过小打印特征最小值eps 调到 1e-6加能量门限高频细节全丢滤波器组上限频率设错打印滤波器中心频率上限对齐实际有效带宽分离任务不收敛未做置换不变损失看两路输出是否互换加最小损失匹配长音频推理结果抖动训练推理片段长度不一致对比两种长度的输出分块推理加重叠拼接loss 出现 nan混合精度下溢定位第一个 nan 的层损失计算保持单精度唤醒词误触发多无后验平滑统计连续帧得分加滑动窗平滑和门限增强后音量忽大忽小逐句归一化破坏了绝对能量比对输入输出能量改用全局统计量归一化5. 工程落地几件文档里不会写的事5.1 数据管线的质量决定上限我越来越确信音频项目的成败八成在数据。模型结构换来换去那点差异远不如把数据清洗做扎实。最基础的一步是做时长和能量统计。把所有音频的时长画个直方图把低于 0.5 秒和高于 30 秒的剔掉或者单独处理把整段的平均能量算出来把那些录成了近乎静音的样本挑出来。这两步能清掉 5% 到 10% 的垃圾数据而这些垃圾数据在训练时会产生极大的梯度对模型的伤害远超它的数量占比。标注质量也要抽查。音频标注最大的问题是边界模糊一段狗叫声到底从哪一帧开始算不同标注员的理解能差半秒。如果做的是帧级任务这种偏差会直接毒化训练信号。我的做法是抽 50 条出来自己重听一遍算一下和原始标注的时间偏差分布如果偏差中位数超过 200ms那批标注就得返工。还有采样率混用的问题前面提过这里补一个检查手段对每条音频做一次频谱分析看高频段能量是不是突然截断。如果在 4kHz 处有一个陡峭的悬崖说明这条是从 8kHz 上采来的把它标记出来要么降采样处理要么在训练时排除。5.2 我踩过的几个坑写下来供参考第一个坑是把验证集指标当成唯一真理。有个项目里我调了两周把客观指标刷上去 0.3结果找人做主观听测评分反而下降了。原因是优化的方向偏向压制所有背景声把一些本不该压的环境音也抹掉了听起来很“干净”但不自然。从那以后我养成习惯任何增强类项目都必须做主观听测哪怕只是自己戴耳机听 20 条。第二个坑是忽略了推理时的输入分布。训练数据是近场录音上线场景是远场带混响模型表现直接掉一半。这件事的教训是训练集里必须混入目标场景的录音哪怕是少量真实场景数据加大量仿真混响也比纯干净数据强。仿真混响的实现方式是拿干净语音和房间冲激响应做卷积冲激响应可以用镜像法生成参数包括房间尺寸和混响时间混响时间在 0.2 到 1.0 秒之间随机采样覆盖大多数室内场景。第三个坑是在特征层面做数据增强却没检查物理合理性。比如对梅尔谱做随机频率遮挡如果遮挡位置落在低频段可能把基频整个抹掉模型学到的就是“基频丢了也照样分类”这在真实场景里是有害的。合理的做法是遮挡时避开最低的若干个频带或者用时间遮挡为主、频率遮挡为辅。SpecAugment 原论文里频率遮挡的上限是 27 个频带针对 80 维输入这个比例差不多是三分之一超过这个比例就容易伤到有效信息。第四个坑是过早引入复杂模型。我见过不少项目一上来就用大型预训练模型微调效果确实好但推理成本高到没法部署最后又退回小模型中间浪费的时间够把基线优化三轮。更务实的路径是小模型基线跑通明确瓶颈在哪是特征不够、数据不够还是容量不够再决定往哪个方向加投入。第五个坑是忘了检查静音段。很多数据集的音频首尾有长长的静音如果直接整段送进模型有效信息占比可能不到一半。这不只是浪费算力的问题静音段的特征统计量会把整段的归一化参数带偏。简单的处理方式是用能量门限做语音活动检测把首尾静音裁掉但要注意别裁得太狠因为噪声的起始段有时候对增强任务是有用信息。最后分享一个我一直在用的小技巧给每个实验固定一个可复现的评估脚本输出一个包含多项指标的 JSON包括整体指标、分类别指标、按信噪比分桶的指标、按音频时长分桶的指标。单看一个总分很容易掩盖问题分桶之后经常能发现模型在某个特定场景下彻底失效而这种失效在平均值上被其他场景的良好表现稀释掉了。这个脚本花半天写好后面每个实验都能省下大量反复对比的时间性价比极高。