4. 基于谱熵的语音活动检测优化
任务目标
在噪声环境下,能量VAD(语音活动检测)容易因背景噪声能量过高而失效。本案例实现基于谱熵的VAD算法,利用语音信号在频域上谱熵较低(频谱集中)而噪声谱熵较高(频谱平坦)的特性进行判别,并与能量VAD在含噪语音上做对比,同时演示平滑参数与熵阈值的调整方法。
环境依赖
需要 numpy 与 scipy。若需绘制对比图,额外安装 matplotlib(本案例不强制绘图,以输出数值指标验证)。
核心实现
import numpy as np
from scipy.io import wavfile
def frame_signal(x, fs, frame_len=0.025, frame_shift=0.010):
"""分帧加汉明窗,返回形状 (num_frames, frame_size)"""
n = int(fs * frame_len)
shift = int(fs * frame_shift)
if len(x) < n:
raise ValueError("信号长度小于单帧长度")
num_frames = (len(x) - n) // shift + 1
idx = np.arange(n)[None, :] + np.arange(num_frames)[:, None] * shift
frames = x[idx]
return frames * np.hamming(n)
def spectral_entropy_vad(frames, alpha=0.9, thr=0.85, eps=1e-8):
"""
谱熵VAD:对每帧计算归一化谱熵,用一阶平滑后与阈值比较。
返回 (vad_flags, entropy_smoothed)
"""
n_frames = frames.shape[0]
N = frames.shape[1]
ent = np.zeros(n_frames)
for i in range(n_frames):
mag = np.abs(np.fft.rfft(frames[i])) + eps
prob = mag / mag.sum()
ent[i] = -np.sum(prob * np.log(prob)) / np.log(N // 2 + 1) # 归一化到[0,1]
smoothed = np.zeros_like(ent)
smoothed[0] = ent[0]
for i in range(1, n_frames):
smoothed[i] = alpha * smoothed[i-1] + (1 - alpha) * ent[i]
vad = (smoothed < thr).astype(int)
return vad, smoothed
def energy_vad(frames, energy_thr_ratio=1.5):
"""能量VAD:帧能量 > 全局平均能量 * ratio 判为语音"""
energy = np.sum(frames ** 2, axis=1)
thr = energy.mean() * energy_thr_ratio
return (energy > thr).astype(int), energy
def compute_metrics(vad, ground_truth):
"""返回 (准确率, 语音帧召回率, 非语音帧精确率)"""
correct = np.mean(vad == ground_truth)
speech_idx = ground_truth == 1
recall = np.sum(vad[speech_idx] == 1) / max(1, np.sum(speech_idx))
noise_idx = ground_truth == 0
noise_prec = np.sum(vad[noise_idx] == 0) / max(1, np.sum(noise_idx))
return correct, recall, noise_prec
if __name__ == "__main__":
# 生成测试信号:1s干净语音(8kHz正弦串)+ 白噪声,SNR=5dB
fs = 8000
t = np.arange(fs) / fs
clean = np.sin(2 * np.pi * 300 * t) * (t < 0.5) + 0.5 * np.sin(2 * np.pi * 600 * t) * (t >= 0.5)
noise = np.random.default_rng(0).normal(0, 1, fs)
noise_pwr = np.mean(noise ** 2)
clean_pwr = np.mean(clean ** 2)
noise_scaled = noise * np.sqrt(clean_pwr / (10 ** (5 / 10) * noise_pwr))
noisy = clean + noise_scaled
# 真实标签(每帧)
frames = frame_signal(noisy, fs)
gt_frames = frame_signal(clean, fs)
ground_truth = (np.mean(gt_frames ** 2, axis=1) > 1e-4).astype(int)
# 谱熵VAD
vad_spec, ent_s = spectral_entropy_vad(frames, alpha=0.9, thr=0.85)
# 能量VAD
vad_eng, eng = energy_vad(frames, energy_thr_ratio=1.5)
# 对比指标
m_spec = compute_metrics(vad_spec, ground_truth)
m_eng = compute_metrics(vad_eng, ground_truth)
print(f"谱熵VAD 准确率={m_spec[0]:.3f} 语音召回={m_spec[1]:.3f} 噪声精确={m_spec[2]:.3f}")
print(f"能量VAD 准确率={m_eng[0]:.3f} 语音召回={m_eng[1]:.3f} 噪声精确={m_eng[2]:.3f}")
print(f"谱熵平滑后范围: [{ent_s.min():.3f}, {ent_s.max():.3f}]")
运行与验证
- 将代码保存为
spectral_entropy_vad.py,在命令行执行python spectral_entropy_vad.py。 - 程序自动生成5dB信噪比的含噪语音(前0.5s为300Hz,后0.5s为600Hz),并构造真实帧标签。
- 观察输出:谱熵VAD的准确率与噪声精确率应明显高于能量VAD(能量VAD在噪声段易误判为语音)。
- 判定通过条件:谱熵VAD的准确率 > 0.85 且噪声精确率 > 0.8,同时能量VAD的噪声精确率低于谱熵VAD至少0.15。
参数调整说明
- 平滑系数
alpha:增大(如0.95)使谱熵轨迹更平滑,减少短促噪声误触发,但会略微延迟语音起止点;减小(如0.7)响应更快但易产生毛刺。 - 熵阈值
thr:语音帧谱熵通常低于0.7,纯噪声接近0.9~1.0。若噪声强可适当降低阈值(如0.8),若语音本身频谱平坦(如摩擦音)则需提高阈值(如0.9)。 - 调整后重新运行,观察准确率与召回率的变化趋势是否符合上述规律。
上图示意谱熵VAD的核心判别逻辑:噪声段谱熵值高且平坦,语音段因频谱集中而出现明显下凹,通过阈值即可区分。能量VAD在噪声能量较高时无法形成类似区分。