简介:本文系统拆解语音识别的技术原理:16kHz 波形如何经分帧、加窗、FFT 与 mel 滤波器组变成 80 维帧级特征;“上百帧对应十几个字”的长度不对等为何是核心难题;CTC、注意力机制与 Transducer 三类端到端范式分别如何应对对齐问题。文中附特征提取示例代码与工程实践要点,帮助读者建立从声学前端到解码输出的完整认知。
语音识别的目标,是把连续的音频信号转换成离散的文字序列。以最常见的 16kHz 采样率为例,一秒钟的语音就是 16000 个采样点;经过特征提取后变成约每秒 100 帧的帧级特征;而这句话对应的文字,往往只有几个到十几个字。于是整个任务可以浓缩为一句话:每秒切出百帧特征,让神经网络把数百帧压缩成十几个字——而真正难啃的,是帧与字之间那段看不见的对齐关系。
这种“输入长度远大于输出长度、且两者之间没有任何天然逐点对应”的问题,是语音识别区别于普通图像分类、序列标注的根本难点,也是理解这门技术所有设计取舍的钥匙。
从统计视角看,识别可形式化为:给定音频 X,寻找最可能的文字序列 Y,即求 argmax P(Y|X)。早期系统把它拆成声学模型与语言模型分别建模再组合;现代端到端模型则用一个网络直接建模整个条件分布。
原始波形当然可以直接作为网络输入,研究中确实存在让模型从波形自行学习特征的路线。但工业系统普遍仍先提取人工设计的谱特征,原因有三:
语音信号有个基本观察:在 20~30ms 的时间尺度内,声道形状近似不变,信号可视为平稳;但拉长到几百毫秒,声道已在连续运动。因此特征提取第一步是分帧:
按这组参数,1 秒音频恰好切出约 100 帧。这个数字值得记住——后文所有关于对齐的麻烦,都由它引发。
对每帧做 FFT 得到频谱(512 点 FFT 对应 257 个频点),取功率后,进入整个前端最有“声学味”的一步:用一组三角形滤波器对频谱加权求和。
这些滤波器在 mel 刻度上均匀铺开,映射回真实频率后却低频密、高频疏——低频处一个滤波器可能只覆盖两三个频点,高频处能覆盖十几个。这样设计源于人耳特性:我们对几百赫兹的细微差异非常敏感,对几千赫兹的差异则相对迟钝。80 个三角滤波器既模拟了人耳的频率分辨力,又顺带把 257 维频谱压缩成 80 维。当前主流端到端模型几乎都沿用“80 维 log-mel、25ms 窗、10ms 帧移”这组经典配置。
对滤波器输出取 log,作用有二:
在 fbank 之后再做一次离散余弦变换(DCT),得到的就是经典的 MFCC。DCT 的目的是去除维度间相关性——但这其实是在伺候早年 GMM 声学模型“对角协方差”的假设。神经网络并不在意特征相关,因此进入深度学习时代后,保留更多原始信息的 log-mel fbank 成了默认输入,MFCC 更多是历史遗产。
import numpy as npsr = 16000 # 采样率t = np.arange(sr) / sr # 1 秒时间轴x = (0.5 * np.sin(2 * np.pi * 220 * t) # 220Hz 基频+ 0.3 * np.sin(2 * np.pi * 660 * t) # 660Hz 泛音+ 0.01 * np.random.randn(sr)) # 轻微噪声# 1) 分帧加窗:窗长 400 点,帧移 160 点win, hop = int(0.025 * sr), int(0.010 * sr)n_frames = 1 + (len(x) - win) // hopframes = np.stack([x[i*hop : i*hop+win] for i in range(n_frames)]) * np.hamming(win)# 2) 功率谱nfft = 512power = np.abs(np.fft.rfft(frames, nfft))**2 / nfft # (n_frames, 257)# 3) mel 三角滤波器组def hz2mel(f): return 2595 * np.log10(1 + f/700)def mel2hz(m): return 700 * (10**(m/2595) - 1)n_mels = 80pts = np.linspace(hz2mel(0), hz2mel(sr/2), n_mels + 2)bins = np.floor((nfft + 1) * mel2hz(pts) / sr).astype(int)fb = np.zeros((n_mels, nfft//2 + 1))for m in range(1, n_mels + 1):l, c, r = bins[m-1], bins[m], bins[m+1]fb[m-1, l:c] = (np.arange(l, c) - l) / max(c - l, 1) # 上升沿fb[m-1, c:r] = (r - np.arange(c, r)) / max(r - c, 1) # 下降沿feat = np.log(power @ fb.T + 1e-10) # (n_frames, 80)print("帧数:", n_frames, "| 每帧维度:", feat.shape[1])print("低频滤波器覆盖频点数:", bins[11] - bins[9], "| 高频:", bins[71] - bins[69])
运行结果能直观看到两点:1 秒语音切出约 98 帧;低频滤波器只覆盖两三个频点,高频则覆盖十几个——mel 刻度的非线性一目了然。
特征解决的是“输入如何表示”,真正的灵魂拷问在监督信号上:训练数据只有“这段音频 → 这句话”的整句配对,“第 37 帧到底属于某个字的发音、字间过渡还是静音”,数据集里没有任何说明。
以“今天天气很好”为例:6 个字对应约 100 帧特征,平均一个字摊到十几帧,且实际比例随语速、停顿剧烈波动。模型必须同时学会“认字”与“对齐”两件事,而对齐标签本身并不存在——这正是语音识别建模的全部戏剧性所在。
经典方案是 GMM-HMM 及其后的 DNN-HMM 混合系统:为每个音素建立隐马尔可夫状态链,把对齐当作隐变量,在“给定对齐估参数、给定参数估对齐”之间交替迭代。DNN 负责估计每帧属于各声学状态的概率,再配合发音词典与独立语言模型,拼出完整流水线。这套体系长期有效,但模块众多、各模块优化目标彼此割裂,错误会沿流水线传播,维护成本也高。
端到端模型把声学、发音、语言知识压进同一个网络,对齐问题则交给三种范式之一:
CTC:用空白符“回避”对齐。 在输出符号表中加入空白符,让网络逐帧输出“某个字”或“空白”;推理时折叠连续重复与空白,剩下的就是文本。它彻底绕开了显式对齐,代价是条件独立假设——给定音频后各时刻输出相互独立,天然不擅长建模字与字之间的语言依赖,实践中常在解码阶段融合语言模型分数来补足。
注意力编码器-解码器:软对齐。 解码器在生成每个字时,对编码器输出的全部帧做注意力加权,学出一种柔性、隐式的对齐。它天然建模输出端依赖,准确率上限高;但对齐完全由数据驱动,小数据下易失稳,且整句依赖使其在低延迟流式场景比较吃力。
Transducer(如 RNN-T):逐帧决策“发不发字”。 网络每处理完一帧,结合内部维护的语言状态,决定“此刻输出一个字”还是“继续吞下一帧”。对齐被拆解为逐帧局部决策,天然支持流式识别,是当前工业实时场景的常见选择;代价是结构与训练流程更复杂。
实际生产中还常见两者的联合训练:用 CTC 的单调对齐为注意力提供约束,用注意力补足语言建模能力,在准确率与时延之间取得平衡。此外,编码器内部通常还会做帧下采样(如 4 倍降采样),把序列长度进一步压缩,降低计算开销。
原理之外,把系统做到线上可用还需关注几个关键变量:
对多数应用团队而言,自建训练与推理链路的成本远高于直接调用云上能力。目前主流云语音服务一般提供录音文件转写、实时流式识别等通用接口类目,以百度智能云的智能语音方向为例,其提供这类标准化能力,开发者按调用量接入即可,把精力留给业务逻辑本身。
整个任务可以浓缩为一句话:每秒切出百帧特征,让网络把数百帧压缩成十几个字——难点全在帧与字之间那段看不见的对齐上。log-mel 特征凝聚了几十年对人耳感知的声学先验,在端到端时代不仅没有过时,反而愈发稳固;而对齐问题先后得到三种回答——CTC 用空白符回避它,注意力用软加权消解它,Transducer 用逐帧决策驯服它。理解“特征表示 + 序列对齐”这两条主线,就抓住了语音识别原理的主干,也具备了评估各类识别方案与工程取舍的基础框架。