整个流程:
语音 → 特征(Fbank/MFCC) → 编码器(Transformer/Conformer) → CTC/Attention → 解码 → 文本
fbank
一、 概念
Fbank = Mel Filter Bank Energies
本质上:
把语音信号切成许多帧 → 做 FFT → 过一组 Mel 滤波器 → 得到每个滤波器的能量 → log → Fbank 特征
二、过程
2.1 预加重(Pre-emphasis)
y[t] = x[t] -- α * x[t-1]
| 系统 | α |
|---|---|
| Kaldi | 0.97 |
| Librosa | 默认 0.97 |
| WeNet | 默认关闭! |
| MFCC 论文(Rabiner) | 0.95~0.97 |
参数α的解释:
| 问题 | 答案 |
|---|---|
| 预加重公式固定吗? | 形式固定,但参数可调 |
| α = 0.97 是必须的吗? | 不是。只是习惯值 |
| WeNet 用吗? | 默认不使用 |
| 必须使用预加重吗? | 现代 ASR 不一定需要 |
2.2 分帧(Framing)
把连续语音切成一帧一帧处理:
帧长:25ms(WeNet 默认)
帧移:10ms(WeNet 默认)
举例:16kHz 采样率
一帧:25ms = 400 samples 16k * 25 / 1000 = 400
帧移:10ms = 160 samples
原因:语音在 20~30ms 内可以视为稳定信号(短时平稳性)。
帧移 概念:
就是解码是一段一段的。但是不是严格连续的。比如我解第一帧的时间是0ms~25ms,第二帧的时间就是10ms ~35ms
可以看到帧是重叠的?那有没有可能这两帧输出的一个东西呢?
答案是会的。
比如我第一帧和第二帧解码结果都是a
怎么保持正常:
1. Encoder 是累积序列建模的
Transformer / Conformer 会把整段特征序列作为输入
即使帧有 overlap,Decoder / CTC 看到的是 序列顺序
前面的帧信息已经"被记住",不会单独把同一个时间段解码两次(有时间戳)
2. CTC 的特性
CTC 输出是一串字符序列 + blank
它会 自动合并重复的符号
比如输入帧0和帧1都可能对应同一音素 "a",CTC 会输出:
a a → a
所以不会重复输出
3. Attention / Decoder 的上下文感知
对于 Attention-based decoder,输出当前字符时,会参考 整个已经编码的序列
当前帧不会单独"看"前一帧,而是整个上下文
| 时间 | 帧编号 | 特征 | CTC 输出 |
|---|---|---|---|
| 0--25ms | F0 | f0 | blank |
| 10--35ms | F1 | f1 | "你" |
| 20--45ms | F2 | f2 | "好" |
2.3 加窗(Windowing)
给每一帧乘上一个窗,例如 Hamming 窗,让边缘更平滑
frame = frame * hamming_window
目的:避免信号突然截断带来的频域泄漏。
2.4 FFT(快速傅里叶变换) + 功率谱(Power Spectrum)
对每帧做 FFT,通常补零到 512/1024 点。
power = |FFT(frame)|^2
这种以时间为横坐标的表示,就是时域。时域信号告诉我们"什么时候发生了什么事",但它很难直接告诉我们这个信号是由哪些'基础频率'组成的。
傅里叶变换 的终极思想就是:任何复杂的时域信号,都可以分解为一系列不同频率、不同幅度、不同相位的简单正弦波(或余弦波)的叠加。
FFT 就是一种高效计算离散傅里叶变换的算法,是连接时域和频域的桥梁。
全称:Fast Fourier Transform。
本质:是一种算法,用于快速计算 DFT(离散傅里叶变换)。
功能:它将一个离散的时域信号(例如一段数字音频的采样点)作为输入,输出该信号的频域表示。
2.5 Mel 滤波器组(Filter Bank)
这是 Fbank 的核心
Mel 滤波器组是一系列 三角滤波器,数量通常为:
80(WeNet 默认)
40(传统配置)
每个滤波器响应不同频段,频率越高滤波器越稀疏(符合人耳感知)。
计算方法:
对每个三角滤波器 i:
fbank[i] = sum(power[k] * mel_filter[i][k] over k)
也就是:
把频谱能量按照滤波器形状加权求和 → 得到各频带能量
2.6 log 取对数
log_fbank = log(fbank + eps)
示例代码
py
import numpy as np
import scipy.io.wavfile as wav
# import matplotlib.pyplot as plt
"""
1. 读取 wav
"""
wav_path = "./wavs/test.wav"
sr, y = wav.read(wav_path) # y: int16
y = y.astype(np.float32)
y = y / np.max(np.abs(y)) # 归一化到 [-1, 1]
"""
2. 预加重
"""
pre_emphasis = 0.97
y_preemph = np.append(y[0], y[1:] - pre_emphasis * y[:-1])
"""
3. 分帧 + 加窗
"""
frame_length = 0.025 # 25ms 帧长
frame_shift = 0.01 # 10ms 帧移
n_fft = 512 #FFT 点数,决定频率分辨率。通常 ≥ win_length。
n_mels = 80 #Mel 滤波器数量,每帧输出 80 个特征。
win_length = int(frame_length * sr) # 400 samples 窗长 0.025×16000
hop_length = int(frame_shift * sr) # 160 samples 10ms对应的采样点数
num_frames = int(np.ceil((len(y_preemph) - win_length) / hop_length)) + 1 #计算该音频有多少帧数 ceil 向上取整
print(f"num_frams:{num_frames}") # 1103
"""
pad 末尾 加窗
"""
pad_len = (num_frames-1)*hop_length + win_length
y_pad = np.append(y_preemph, np.zeros(pad_len - len(y_preemph)))
frames = np.zeros((num_frames, win_length))
for i in range(num_frames):
start = i * hop_length
frames[i] = y_pad[start:start+win_length]
"""
Hamming 窗
"""
frames *= np.hamming(win_length)
"""
4. FFT + 功率谱
FFT
"""
fft_frames = np.fft.rfft(frames, n=n_fft)
power_frames = np.abs(fft_frames)**2
"""
5. Mel 滤波器
"""
def hz_to_mel(hz):
return 2595 * np.log10(1 + hz/700)
def mel_to_hz(mel):
return 700 * (10**(mel/2595) - 1)
"""
Mel 滤波器
"""
def mel_filterbank(n_mels=80, n_fft=512, sr=16000, fmin=0, fmax=None):
if fmax is None:
fmax = sr/2
# Mel scale
mels = np.linspace(hz_to_mel(fmin), hz_to_mel(fmax), n_mels+2)
hz = mel_to_hz(mels)
bins = np.floor((n_fft+1) * hz / sr).astype(int)
fb = np.zeros((n_mels, n_fft//2+1))
for i in range(1, n_mels+1):
start, center, end = bins[i-1], bins[i], bins[i+1]
for k in range(start, center):
fb[i-1, k] = (k - start) / (center - start)
for k in range(center, end):
fb[i-1, k] = (end - k) / (end - center)
return fb
fb = mel_filterbank(n_mels=n_mels, n_fft=n_fft, sr=sr)
fbank = np.dot(power_frames, fb.T)
"""
6. 对数
"""
fbank = np.log(fbank + 1e-6)
num_print = 10
print("Fbank 前 10 帧特征:")
for i in range(min(num_print, fbank.shape[0])):
print(f"Frame {i}: {fbank[i]}")
print("Fbank shape:", fbank.shape) # (num_frames, n_mels)
"""
7. 可视化
"""
"""
plt.figure(figsize=(10,4))
plt.imshow(fbank.T, origin='lower', aspect='auto', cmap='jet')
plt.xlabel("Frame")
plt.ylabel("Mel Filter")
plt.title("Fbank Feature")
plt.colorbar()
plt.show()
"""