接触过语音识别落地项目的人都会遇到一种尴尬:离线测试集上准确率95%,一到真实环境就掉到80%以下。会议室空调嗡嗡声、键盘敲击声、远处的人声,这些干扰让识别结果里多出一堆乱码和莫名其妙的词。很多人急着去换更大的Conformer模型或者加一个n-gram语言模型,但真正的原因往往在更靠前的位置——音频在进入特征提取之前就已经被污染了。VAD(Voice Activity Detection,端点检测)和降噪预处理这两个环节,决定了ASR系统看到的是干净的人声片段还是一段夹杂着各种背景音的混合信号。

降噪和VAD并不是独立的两个步骤。在实际系统中,它们的顺序、参数和算法选择会互相影响。比如降噪做得太激进,会把人的尾音也削掉,导致VAD把词语切碎;降噪做得太保守,静音段的底噪会让VAD产生大量误触发,识别器就会输出一些不存在的短词。下面从VAD开始,逐步拆解这两个前端环节的工作原理和优化策略。
VAD端点检测:别让识别器去猜静音里有什么
VAD的任务很简单:给每一帧音频打上“有人声”或“无人声”的标签。识别器只需要处理有人声的片段,静音段直接丢弃。这个想法听起来容易,做起来却有很多坑。最简单的实现是基于短时能量和过零率:设定一个能量阈值,高于阈值就认为有语音,低于阈值就认为是静音。这种纯能量方案在安静环境下还行,但一旦遇到稳定的背景噪声(比如风扇声、空调声),阈值就很难固定。调高了会切掉轻声说话的开头,调低了静音段又全被判成语音。
改进方向是用谱熵或者谱平坦度来区分语音和噪声。语音信号的频谱能量集中在少数谐波上,谱熵较低;而白噪声或平稳噪声的频谱比较平坦,谱熵较高。通过计算每一帧的频谱熵值,再结合一个自适应阈值的机制(比如根据前几秒估计噪声水平),可以比纯能量阈值稳健得多。不过谱熵法对非平稳噪声(比如突然的撞击声、关门声)依然敏感,因为这些声音的熵值可能和语音接近。
更可靠的做法是引入统计模型或者深度学习。WebRTC里用的GMM-based VAD就是一个经典方案:用两个高斯混合模型分别建模语音帧和噪声帧的特征分布,然后用似然比做判决。近几年主流的端侧方案则直接用轻量级神经网络(比如2-3层CNN或者小参数量RNN)来对每一帧做二分类,训练数据包含各种噪声场景。这类VAD的鲁棒性明显更高,但代价是计算量和内存占用上升。如果ASR本身已经用了很大的模型,前端再加一个神经网络VAD,端到端延迟可能吃不消。
还有一个容易忽略的点:VAD输出的不是硬标签,而是每一帧的语音概率。很多工程师拿到概率后直接卡一个0.5的阈值,这太粗了。正确的做法是设置一个迟滞区间:概率高于0.7判定为语音开始,低于0.3判定为语音结束,中间保持状态不变。这么做的原因是语音的起止边界往往比较模糊,硬阈值会导致同一个词被切成好几段,识别器就输出重复的字。另外,VAD检测出语音段后,两端应该各保留100到200毫秒的上下文,因为辅音(比如爆破音、摩擦音)的能量可能很低,前置的静音切得太干净会把辅音丢失。
降噪预处理:谱减法和深度学习的真实差距
降噪的目标是保留人声、抑制背景噪声。最古老也最直观的方法是谱减法:估计噪声段的平均功率谱,然后从带噪语音的功率谱中减去这个估计值。公式写出来很简单,但实践中有两个大坑。第一是“音乐噪声”问题:谱减法在低信噪比区域容易产生随机残留的窄带噪声,听起来像远处传来的电子音,这种伪影会严重干扰识别器。第二是相位问题:谱减法只处理幅度谱,相位直接沿用带噪信号的相位,在信噪比较低时重建的语音波形会有明显失真。
# 一个简单的谱减法示例(基于numpy),仅用于说明原理
import numpy as np
import librosa
def spectral_subtraction(noisy_signal, sr, noise_frames):
# 假设noise_frames是前几帧的噪声功率谱估计(取均值)
noisy_stft = librosa.stft(noisy_signal, n_fft=512, hop_length=160)
noisy_mag = np.abs(noisy_stft)
noisy_phase = np.angle(noisy_stft)
# 过减因子alpha=1.5,增益下限0.01防止负值
alpha = 1.5
denoised_mag = np.maximum(noisy_mag - alpha * noise_frames, 0.01)
denoised_stft = denoised_mag * np.exp(1j * noisy_phase)
denoised_signal = librosa.istft(denoised_stft, hop_length=160)
return denoised_signal
维纳滤波是另一条路线。它不直接做减法,而是根据带噪语音和噪声的统计特性计算一个最优线性滤波器,让输出信号和干净语音之间的均方误差最小。维纳滤波在平稳噪声下表现比谱减法好,音乐噪声少很多,但需要准确估计每一帧的先验信噪比。常用的决策导向法(decision-directed)用前一帧的估计结果来平滑当前帧,这种平滑虽然减少了噪声估计的波动,但在噪声突然变化时响应会比较慢。对于非平稳噪声(比如背景有人说话、汽车鸣笛),维纳滤波和谱减法都力不从心。
深度学习降噪(通常是基于掩码的语音增强)直接把带噪语音的幅度谱或者复数谱输入网络,输出一个0到1之间的掩码,乘以原始幅度谱得到干净语音谱。常用的模型有DCCRN、FullSubNet等,参数量从几百万到几千万不等。这类方法在非平稳噪声下的提升是非常明显的,尤其是对突发噪声和混响环境。问题在于实时性:一个手机端ASR系统,如果前端再跑一个几百万参数的降噪模型,延迟和功耗都会翻倍。所以实际部署中经常做折中:用传统的自适应滤波(比如LMS或者NLMS)处理稳态噪声,再用一个小型深度学习模型处理剩余的非平稳成分。
降噪和识别并不是完全解耦的。有些研究把降噪模块和ASR联合训练,让降噪的目标函数从“最小化波形误差”变成“最小化识别错误率”,这样降噪网络会主动保留对识别最有用的频谱特征,而不是追求听感上的干净。不过这种联合训练的工程复杂度比较高,大多数团队还是选择先独立训练降噪模型,再接入ASR做微调。
实战流程:从麦克风阵列到识别器的完整链路
一个典型的实时语音识别前端包含以下步骤:采样率转换和重采样、多通道波束形成(如果有多麦克风)、单通道降噪、自动增益控制(AGC)、VAD、特征提取。这里特别强调一下AGC的作用。很多工程师会忽略AGC,认为它就是简单的音量归一化。但在ASR场景里,AGC的重要性不亚于降噪。说话人距离麦克风忽远忽近、环境音量突变,都会导致送入识别器的信号幅度波动很大,而MFCC和梅尔谱特征对幅度是敏感的。一个设计良好的AGC应该用滑动窗口估计信号包络,动态调整增益,同时设定增益变化的上限,避免放大底噪。
VAD和降噪的顺序也需要根据场景调整。如果降噪放在VAD前面,VAD看到的信号更干净,误触发率更低,但降噪引入的延迟(深度学习模型通常有几十毫秒的帧移延迟)会让VAD的端点判断滞后。如果VAD放在降噪前面,VAD可以在原始信号上以较低的帧移运行,端点更准确,但噪声会干扰VAD的判断。一个实用的折中方案是:先用一个轻量级能量VAD做粗分割,把音频切成候选语音段,然后对候选段做降噪和精细VAD,最后送入识别器。这样既避免了全时段运行降噪的开销,又让最后阶段的信号质量有保障。
下面给出一个简单的伪代码流程,展示一个离线的批处理场景(比如用户上传一段录音做转写):
# 离线批处理流程伪代码
def preprocess_audio(raw_audio, sr):
# 1. 重采样到16kHz单声道
audio = resample(raw_audio, sr, 16000)
audio = to_mono(audio)
# 2. 多通道波束形成(如果有)
if mic_array_channels > 1:
audio = beamform(audio, method='mvdr')
# 3. 单通道降噪(离线可用大模型)
audio = deep_denoise(audio, model='dccrn_16k')
# 4. 自动增益控制
audio = agc(audio, target_level=-23, max_gain=12)
# 5. VAD(输出语音段列表)
vad_result = vad_detect(audio, method='energy+cnn', min_speech_ms=200, min_silence_ms=300)
segments = []
for start_ms, end_ms in vad_result:
# 两端各扩展150ms保留辅音
start = max(0, start_ms - 150)
end = min(len(audio), end_ms + 150)
segments.append(audio[start:end])
return segments
# 识别器对每个segment单独解码
for seg in preprocess_audio(raw, sr):
text = asr_model.recognize(seg)
print(text)
最后提醒一点:降噪和VAD的参数千万不要在安静环境下调好就直接上线。真实环境的信噪比分布、噪声类型、混响条件都不同,最好采集一批目标场景下的真实录音,用这批数据来评估VAD的误触发率和漏检率,以及降噪后的语音可懂度(可以用PESQ或者STOI指标)。如果发现识别错误集中在特定噪声场景,再针对性调整降噪强度或者VAD阈值,比盲目换识别模型要有效得多。