3. 精度评估体系

数据增强,说白了就是给模型“加难度”。

唤醒词模型在实验室里跑得再好,一放到真实环境里,立马现原形。为什么?因为真实世界太“脏”了——有空调声、马路上的车流声、别人说话的声音,甚至你喊唤醒词时离麦克风忽远忽近。模型没见过这些,它就懵了。

所以我们要在训练阶段,人为地给干净语音“加点料”。让模型在各种各样的噪声里,依然能准确识别出唤醒词。我个人习惯把数据增强分成两类:信号域增强和特征域增强。下面我们一个一个来过。

核心观点:数据增强不是越多越好,关键是模拟真实场景。加噪太猛,模型反而学不到唤醒词本身的特征。我踩过这个坑,后面会细说。

数据增强策略全景图 原始语音 信号域增强 特征域增强 加噪 混响 变速 音量扰动 SpecAugment 建议:信号域增强与特征域增强组合使用,效果优于单独使用任何一种

3.1 加噪:让模型学会“听清”唤醒词

加噪是最基础,也是最有效的数据增强手段。我一般会准备一个噪声库,里面包含各种场景的噪声:白噪声、粉红噪声、咖啡馆噪声、马路噪声、空调声、风扇声……

具体做法:从噪声库里随机选一段,和干净语音按一定信噪比(SNR)混合。信噪比越低,噪声越大。

我的经验:信噪比范围控制在 0dB 到 20dB 之间比较合理。低于 0dB,噪声比语音还大,模型基本学不到东西。高于 20dB,噪声太轻,起不到增强效果。我一般用 5dB、10dB、15dB 三个档位随机选。

import librosa
import numpy as np

def add_noise(clean_audio, noise_audio, snr_db):
    # 计算干净语音的能量
    clean_power = np.mean(clean_audio ** 2)
    # 计算噪声能量
    noise_power = np.mean(noise_audio ** 2)
    # 根据 SNR 计算噪声缩放因子
    snr_linear = 10 ** (snr_db / 10)
    scale = np.sqrt(clean_power / (snr_linear * noise_power + 1e-10))
    # 混合
    noisy_audio = clean_audio + scale * noise_audio
    # 防止溢出
    noisy_audio = np.clip(noisy_audio, -1.0, 1.0)
    return noisy_audio

我曾经踩过的坑:噪声长度比语音短怎么办?直接截断噪声会导致频谱不连续。我的做法是:把噪声循环拼接,直到长度超过语音,再随机截取一段。这样能保证噪声的统计特性不变。

3.2 混响:模拟房间回声

唤醒词模型在空旷的客厅里和在小卧室里,表现可能天差地别。为什么?因为混响时间不一样。混响会让语音变得“模糊”,模型需要学会从这种模糊中提取关键特征。

实现方式:用房间冲激响应(RIR)和干净语音做卷积。RIR 可以实测,也可以用工具生成。我常用 pyroomacoustics 这个库来生成不同房间大小的 RIR。

import pyroomacoustics as pra
import numpy as np

def generate_rir(room_dim, mic_pos, src_pos, fs=16000):
    # 创建房间
    room = pra.ShoeBox(room_dim, fs=fs, max_order=15)
    # 添加麦克风和声源
    room.add_microphone(mic_pos)
    room.add_source(src_pos)
    # 计算 RIR
    room.compute_rir()
    rir = room.rir[0][0]
    return rir

def add_reverb(clean_audio, rir):
    # 卷积
    reverb_audio = np.convolve(clean_audio, rir)
    # 截断到原长度
    reverb_audio = reverb_audio[:len(clean_audio)]
    return reverb_audio
房间类型 尺寸(米) 混响时间 T60(秒) 适用场景
小卧室 3×4×2.8 0.3 - 0.5 近场唤醒
客厅 5×6×3 0.5 - 0.8 中等距离唤醒
会议室 8×10×3.5 0.8 - 1.2 远场唤醒
走廊/大厅 15×5×4 1.2 - 2.0 极端远场

注意:混响增强不要和加噪同时做太猛。我试过先加噪再混响,结果模型完全学废了。建议先做混响,再加噪,这样更接近真实场景——房间回声先发生,噪声后叠加。

3.3 变速:应对语速变化

每个人说话的快慢不一样。有人说话像机关枪,有人慢悠悠的。唤醒词模型必须能适应这种变化。

变速方法:用 librosa.effects.time_stretch 改变语速,同时保持音高不变。速度因子在 0.8 到 1.2 之间随机选。低于 0.8 会变得像慢放,高于 1.2 会像快进,都不太自然。

import librosa

def speed_change(audio, rate):
    # rate > 1 加速,rate < 1 减速
    audio_stretched = librosa.effects.time_stretch(audio, rate=rate)
    return audio_stretched

我的习惯:变速之后,我会检查一下音频长度。如果加速后音频太短(比如小于 0.5 秒),模型可能学不到完整的唤醒词发音。我会丢弃这种样本,或者把速度因子调回 1.0。

3.4 音量扰动:模拟距离变化

用户离麦克风 10 厘米和 2 米,音量能差 20dB 以上。模型如果只见过固定音量的数据,遇到小声说话就废了。

做法:随机缩放音频的幅度。缩放因子在 0.3 到 1.5 之间。注意不要超过 1.5,否则会削波(clipping),产生失真。

def volume_perturb(audio, min_gain=0.3, max_gain=1.5):
    gain = np.random.uniform(min_gain, max_gain)
    audio_perturbed = audio * gain
    audio_perturbed = np.clip(audio_perturbed, -1.0, 1.0)
    return audio_perturbed

我曾经犯过的错:音量扰动和加噪一起做时,先做音量扰动,再加噪。如果先加噪再调音量,会把噪声也放大或缩小,导致信噪比失控。顺序很重要。

3.5 SpecAugment:特征域增强的“核武器”

前面几种都是对原始波形做操作。SpecAugment 不一样,它直接在频谱图上做文章。说白了,就是随机遮挡频谱图的一部分——时间轴上的连续几帧,或者频率轴上的连续几个频带。

为什么这招有效?因为模型如果过度依赖某个频段或某个时间片段,一旦真实场景中那个频段被噪声盖住了,模型就认不出来了。SpecAugment 强迫模型学会“看全局”。

import torch
import torchaudio

class SpecAugment:
    def __init__(self, freq_mask_param=15, time_mask_param=30, num_masks=2):
        self.freq_mask_param = freq_mask_param
        self.time_mask_param = time_mask_param
        self.num_masks = num_masks
    
    def __call__(self, spec):
        # spec shape: [freq_bins, time_frames]
        for _ in range(self.num_masks):
            # 频率掩码
            f_start = np.random.randint(0, spec.shape[0] - self.freq_mask_param)
            spec[f_start:f_start+self.freq_mask_param, :] = 0
            # 时间掩码
            t_start = np.random.randint(0, spec.shape[1] - self.time_mask_param)
            spec[:, t_start:t_start+self.time_mask_param] = 0
        return spec
参数 推荐值 说明
频率掩码宽度 15 - 30 个频带 对应约 500-1000Hz 范围
时间掩码宽度 20 - 40 帧 对应约 200-400ms
掩码数量 2 - 4 个 太多会破坏语音结构

实战建议:SpecAugment 最好在训练的前几个 epoch 不要用。等模型先学会基本特征,再逐步引入。我一般前 5 个 epoch 关掉 SpecAugment,后面再打开。这样模型不会一开始就被“打懵”。

3.6 组合策略:怎么搭配最有效?

上面讲了五种增强方法。怎么组合?我个人的经验是:

  1. 信号域增强(加噪、混响、变速、音量扰动)在数据加载时在线做,每次随机选 2-3 种组合。
  2. 特征域增强(SpecAugment)在提取频谱后做,和信号域增强不冲突。
  3. 不要所有增强都上。我试过五种全开,结果模型训练 loss 死活降不下去。后来发现是增强太猛,数据面目全非了。
def augmentation_pipeline(clean_audio, noise_audio, rir):
    # 1. 混响(50% 概率)
    if np.random.rand() > 0.5:
        clean_audio = add_reverb(clean_audio, rir)
    # 2. 加噪(80% 概率)
    if np.random.rand() > 0.2:
        snr = np.random.choice([5, 10, 15])
        clean_audio = add_noise(clean_audio, noise_audio, snr)
    # 3. 变速(30% 概率)
    if np.random.rand() > 0.7:
        rate = np.random.uniform(0.85, 1.15)
        clean_audio = speed_change(clean_audio, rate)
    # 4. 音量扰动(50% 概率)
    if np.random.rand() > 0.5:
        clean_audio = volume_perturb(clean_audio)
    return clean_audio

最后说一句:数据增强没有标准答案。你最好在自己的验证集上做 A/B 测试。我每次调完增强策略,都会在 1000 条真实场景录音上测一遍。如果误唤醒率上升了,说明增强过头了,得往回缩。