前阵子帮一个做硬件的朋友收拾他的语音降噪项目,模型结构照着某篇论文复现,训练 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 数变成 257(512/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_mels=64, n_classes=10, rnn_hidden=128): super().__init__() # 输入形状: (B, 1, T, n_mels) self.conv = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=(2, 2)), # T/2, F/2 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=(2, 2)), # T/4, F/4 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), ) self.pool_freq = nn.AdaptiveAvgPool2d((None, 1)) # 频率轴压成 1 self.rnn = nn.GRU(128, rnn_hidden, num_layers=2, batch_first=True, bidirectional=True) 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(dim=1) # 时间维度平均池化 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,包括整体指标、分类别指标、按信噪比分桶的指标、按音频时长分桶的指标。单看一个总分很容易掩盖问题,分桶之后经常能发现模型在某个特定场景下彻底失效,而这种失效在平均值上被其他场景的良好表现稀释掉了。这个脚本花半天写好,后面每个实验都能省下大量反复对比的时间,性价比极高。