4. 预加重与直流分量去除:为什么需要预加重,一阶高通滤波器实现,实战中的参数选择
好,咱们进入第四章。这一章聊的是声学特征提取里一个看似不起眼、但实际影响巨大的环节——预加重。
说实话,我刚入行那会儿,觉得预加重就是个可有可无的预处理。不就是把高频提一提嘛,能有多大差别?直到有一次我在做语音识别的前端,发现某个高频辅音(比如 /s/、/ʃ/)的识别率总是上不去。折腾了半天,最后发现是预加重参数没调对。嗯,从那以后,我再也不敢小看这一步了。
4.1 为什么需要预加重?
先问一个问题:你录一段语音,拿频谱仪一看,会发现什么?
你会发现,能量主要集中在低频段。高频部分的能量,随着频率升高,会以大约 6 dB/倍频程 的速度衰减。这是声带发声的物理特性决定的——声门脉冲的频谱本身就是低通型的。
但人耳对高频的感知并不弱。尤其是辅音、摩擦音,它们的能量虽然小,却承载了大量的信息。如果直接拿原始信号做特征提取,高频成分会被低频的“大能量”淹没,模型根本学不到这些细节。
预加重的目的,说白了就是给高频“补补课”。用一个高通滤波器,把高频部分的能量抬起来,让整个频谱变得更平坦。这样后续的傅里叶变换、梅尔滤波器组才能公平地对待每一个频段。
核心观点:预加重不是“改变信号”,而是“补偿信号”。它补偿的是声门脉冲和唇辐射带来的频谱倾斜,让分析结果更接近人耳的听觉特性。
另外,预加重还有一个附带的好处——数值稳定性。高频信号的能量被提升后,在后续的 FFT 计算中,数值的动态范围会变小,不容易出现浮点精度问题。这一点在嵌入式平台上尤其明显,我踩过这个坑。
4.2 一阶高通滤波器实现
预加重最常用的实现方式,就是一个一阶高通 FIR 滤波器。公式非常简单:
y[n] = x[n] - α * x[n-1]
其中,α 是预加重系数,取值范围通常在 0.9 到 1.0 之间。这个滤波器在 z 域上的传递函数是:
H(z) = 1 - α * z⁻¹
它的频率响应在直流处(0 Hz)是 1 - α,在奈奎斯特频率处是 1 + α。所以 α 越接近 1,低频衰减越厉害,高频提升越明显。
下面是一个 Python 实现,我习惯用 scipy.signal.lfilter 来做,但为了让你看清本质,我写一个纯 NumPy 版本:
import numpy as np
def pre_emphasis(signal, alpha=0.97):
"""
一阶高通滤波器实现预加重
:param signal: 输入信号 (1D array)
:param alpha: 预加重系数,通常 0.95 ~ 0.99
:return: 预加重后的信号
"""
# y[n] = x[n] - alpha * x[n-1]
emphasized = np.append(signal[0], signal[1:] - alpha * signal[:-1])
return emphasized
注意第一帧的处理:y[0] = x[0],因为没有 x[-1]。这个细节容易被忽略,但影响不大,因为第一帧的能量通常很小。
如果你用 scipy,也可以这样写:
from scipy.signal import lfilter
def pre_emphasis_scipy(signal, alpha=0.97):
b = [1, -alpha] # 分子系数
a = [1] # 分母系数(FIR 滤波器)
return lfilter(b, a, signal)
两种写法结果完全一致。我个人更喜欢用 NumPy 版本,因为少一个依赖,而且代码更直观。
4.3 实战中的参数选择
预加重系数 α 怎么选?这是实战中最常见的问题。我直接给结论:
| 应用场景 | 推荐 α 值 | 说明 |
|---|---|---|
| 语音识别(通用) | 0.97 | 最常用,平衡低频衰减和高频提升 |
| 说话人识别 | 0.95 ~ 0.97 | 稍微保守一点,保留更多低频信息 |
| 音乐分析 | 0.90 ~ 0.95 | 音乐频谱更丰富,过度预加重会失真 |
| 噪声环境下的语音 | 0.98 ~ 0.99 | 更强的预加重可以突出高频信噪比 |
| 儿童语音 | 0.95 ~ 0.96 | 儿童声带短,高频能量本身较高 |
你可能会问:为什么是 0.97 这个数字?
其实没有绝对的标准。0.97 这个值最早来自 HTK(Hidden Markov Model Toolkit)的默认配置,后来被 Kaldi、Librosa 等工具继承。它对应的是 16 kHz 采样率下的一个经验值。如果采样率变了,α 也需要调整。
经验公式:对于采样率 fs,预加重系数可以近似为 α = exp(-2π * fc / fs),其中 fc 是截止频率,通常取 50 ~ 100 Hz。比如 fs = 16000,fc = 80,算出来 α ≈ 0.97。
我曾经在一个项目里,采样率是 8 kHz(电话语音),直接用了 0.97,结果低频被削得太狠,元音听起来像“漏气”一样。后来改成 0.94,效果就好了很多。所以,不要盲目照搬默认值,要根据你的采样率和信号特性来调。
4.4 直流分量去除
说完预加重,顺便提一下直流分量去除。这两个操作经常放在一起做,但目的不同。
直流分量(DC offset)是指信号中有一个恒定的偏移量。比如麦克风有偏置电压,或者 ADC 的参考电压不准,都会导致信号整体上移或下移。这个偏移量在频谱上表现为 0 Hz 处的能量,对后续分析没有任何帮助,反而会干扰 FFT 的结果。
去除直流分量的方法很简单:减去信号的均值。
def remove_dc_offset(signal):
return signal - np.mean(signal)
或者用高通滤波器,截止频率设到 20 Hz 以下,也能达到同样的效果。我个人习惯先做直流去除,再做预加重。顺序很重要:
- 先去除直流:避免直流分量被预加重放大(虽然一阶高通本身会衰减直流,但先去掉更干净)。
- 再做预加重:补偿高频。
注意:如果信号本身已经做过高通滤波(比如某些麦克风自带 HPF),再叠加预加重可能会导致低频过度衰减。这种情况下,可以适当降低 α 值,或者干脆跳过预加重。
4.5 知识体系图
下面这张图总结了本章的核心逻辑:
4.6 实战中的几个坑
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 坑一:预加重和加窗的顺序。 有些教程先加窗再预加重,这是错的。预加重必须在分帧加窗之前做,否则帧边界处的信号会被截断,导致高频失真。
- 坑二:α 值过大导致低频丢失。 如果 α 超过 0.99,低频几乎被完全滤除,元音的共振峰结构会被破坏。我见过有人用 0.999,结果语音听起来像“机器人”。
- 坑三:忘记去除直流。 有些麦克风模块(比如 MEMS 麦克风)输出的信号自带直流偏置。如果不做直流去除,预加重后的第一帧会有一个明显的跳变,影响后续的 VAD(语音活动检测)。
- 坑四:在浮点精度低的平台上使用过大的 α。 比如在 ARM Cortex-M4 上,如果 α = 0.999,
1 - α会非常小,导致低频部分的数值精度丢失。建议在嵌入式平台上使用 Q15 定点数,或者限制 α ≤ 0.98。