很多刚接触语音识别的同学,第一次看到“HMM、CTC、RNN-T”这三个词并列都会愣一下:一个上世纪七十年代起家的老牌模型,一个靠空标签把端到端训练变得可行的损失函数,一个现在流式识别里最常听到的架构,为什么要放在同一个章节里讲?其实它们沿着同一条线索递进——都在处理“语音帧序列”与“文本标签序列”之间的对齐问题。标题里的Chapter1-5是语音识别课程里常见的章节顺序,对齐这一部分正好把前面的声学特征、字典、语言模型和解码器串成一条完整的链路。这篇文章我更想把它写成一份工程笔记:不只讲HMM、CTC、RNN-T的公式长什么样,还想说清楚为什么这几代方法要对“对齐”这件事反复折腾,以及我在实际训练和调模型时踩过的那些跟对齐有关的坑。
1. 对齐问题到底是什么:从“先分段还是先识别”说起
1.1 一段语音和一个句子的长度鸿沟
原始音频通常按25ms一帧、10ms移步取特征,一分钟语音大概6000帧,但对应文本可能只有几百个字。字符(或者词片)数和帧数之间没有天然的一对一关系。更麻烦的是,同一个字在语速快慢不同时,持续时长可能差出一倍;相邻音节之间还常有停顿、送气、协同发音,根本没有一个绝对客观的边界能直接切出“这是第几个字的第几帧”。语音识别内部所有对齐机制,做的都是同一件事:建立从帧序列到标签序列的映射。
实际项目里,对齐不只出现在训练损失函数里。训练数据清洗要对齐,强制标注要对齐,流式识别要判断“这个字应该在什么时候吐出来”,口语评测要对齐每个音素的起始时间。所以我一直觉得,理解语音识别不能只站在“分类”角度看,更要站在“序列配准”角度看。把 HMM、CTC、RNN-T 三条路线放在一起对比,你会看到不同时代的人是怎样一次次尝试回答同一个问题的。
1.2 所谓“对齐”其实是三条不同的契约
HMM、CTC、RNN-T虽然都在做对齐,但三者的思想差别非常大。可以看作三种不同的契约。
HMM的契约是:模型先有一个隐状态序列,每个状态对应音素或音素的子段,状态按顺序转移,同时每个状态产生一个观测帧。最终文本顺序由状态转移顺序保证,帧与状态之间是一种“一对一”的对应,只是哪一帧对齐到哪个状态需要推断。
CTC的契约是:每个时刻预测一个标签或空标签,空标签专门表示“什么都没说”。把连续重复的标签合并成一个、把所有空标签删掉之后,剩下的序列必须等于目标文本。这样一来,每帧一个输出,但很多输出可以是同一个标签,也可以输出空。
RNN-T的契约是:每个时间步先利用当前帧和历史输出,决定输出一个真实标签还是空标签。如果输出真实标签,同一帧还可以继续再输出下一个标签;如果输出空,才消耗当前帧。标签序列内部始终保持顺序,帧和标签之间的联系是单调的,但不要求帧和标签一一对应。
区分之后你自然会发现:HMM里对齐是生成模型的隐变量,CTC里对齐是过程性的中间表示,RNN-T里对齐是带记忆的决策结果。三者看待“帧-标签配准”的方式完全不同,所以训练和解码的复杂度也完全不同。
2. HMM的对齐:隐变量、前向后向和强制对齐的土办法
2.1 生成式模型里的状态与发射
HMM用于语音识别时,通常一个音素被拆成几个状态,比如三状态分别表示音素的起始、稳态、结束。每个状态对应一个发射分布,早期用高斯混合模型建模,后来在混合系统里用神经网络输出拟合状态后验。每个状态可以转移到自身,停留时间由自环概率决定。从声学特征视角看,一帧特征是由某个状态生成的,而状态序列随时间单调向前走;语音的长短变化被自环和转移概率吸收掉了。
这里要特别注意,HMM对“对齐”的定义非常强:它假设状态集合是有意义的子词单位,字典会告诉模型哪个词由哪些音素组成。输入帧数可以变,但状态顺序只会线性推进。如果在音频里一个词被读了两遍,HMM对齐不会允许同一个状态序列重复出现在同一个词单元里,因为状态图里这个词到那个词的连接方式是定死的。这种强约束在早期计算资源受限时是好事,因为它大幅缩小了搜索空间;但也让它应对真实口语时很僵硬,比如读错、重读、插入音等。
2.2 Baum-Welch:看不见的对齐怎么训练
因为对齐是隐变量,训练HMM时不能用简单的交叉熵直接标注“帧属于哪个状态”。标准做法是用EM算法里的Baum-Welch迭代。前向概率算的是“截至当前帧,处在某个状态且已经生成这些观测的累计概率”,后向概率算的是“从当前状态出发,能生成后续观测的概率”。两个概率合起来,再加权统计每个状态转移或发射的出现次数,然后更新模型参数,如此迭代直到收敛。
这套机制听起来复杂,但它的核心思想值得反复体会:我们不知道对齐,但可以算“所有可能对齐的加权平均”。这其实就是后来CTC和RNN-T训练里前向后向算法的雏形。区别在于,HMM里的对齐是状态级的,状态转移概率本身会参与训练;CTC和RNN-T里是标签级的路径,没有显式的状态时长模型,时长信息被空标签和重复规则吸收。理解这个递进关系,比死记公式有用得多。
2.3 工程落地:强制对齐的价值与损耗
传统混合系统里,训练神经网络声学模型之前,往往需要先做强制对齐:用训练好的GMM-HMM对每条音频跑一次Viterbi,得到每一帧对应的音素或状态标签,这些标签再拿去训练神经网络。这个流程在很长一段时间里是工业界标准动作,直到现在很多数据标注工具还在用类似思路。
但强制对齐的坑也很多。最典型的是静音段。如果词典里没有专门的静音音素,对齐时静音帧会被强行塞给相邻音素,导致音素边界偏向静音,尤其是在句首句尾。另一个问题是,如果音频和文本不对应,比如说话人读错词了但文本没改,强制对齐会靠转移概率硬凑,产生一些完全不符合听觉常识的状态跳变。所以我在做数据清洗时,检查对齐结果不看准确率,而是看对齐路径里有没有“状态来回震荡”或“某个状态的时长突然短到离谱”。有这种迹象的基本需要返工,这个经验后面还会展开讲。
3. CTC的对齐:让模型自己决定重复和停顿
3.1 blank和重复坍缩,把唯一路径变成路径集合
CTC的核心贡献是在输出类别集合里加了一个空标签,一般记作 blank。训练时,每个时间步模型输出一个类别概率分布;一个完整的时间对齐路径就是帧数长度的类别序列。把这个路径里连续重复的相同标签合并成一个,再把所有 blank删掉,得到的才是最终文本。比如目标文本是“a”,合法路径可以是“a”“aa”“a_”“_a”“aa”等等,只要经过合并和去blank后得到“a”就行。CTC的损失,就是所有合法路径的条件概率之和。
为什么要设计这样一层绕来绕去的规则?本质上是从HMM那里学来的松弛做法。HMM要求状态序列和帧之间严格对齐,建模复杂;CTC跳出显式的子词状态概念,直接把标签当输出,把对齐当作中间变量,通过“重复+空”来容忍时长的伸缩。这给训练带来很大便利:只要输入序列和输出序列是单调可配对的,就能做端到端训练,不需要外部字典,不需要决策树聚类,也不需要人工定义音素状态。
3.2 前向-后向算法在CTC里的局部和整体
合法路径数量非常庞大,不可能枚举。CTC训练用的是带前向变量 alpha(t, u) 的递归,其中 t 是帧时间,u 是输出序列的位置。每一步可以从两个方向转移:一是从当前输出位置继续走,保持或重复当前标签,二是从上一个输出位置跳过来。前向变量把“到时刻t为止、输出到位置u”的所有路径概率累积起来;反过来,后向变量再从末尾往回收一次;两者合起来就能算出每个时刻每个位置的“总概率”,然后对网络输出概率求梯度。整个递归每一步只涉及当前时间步的局部转移,训练效率很高。
如果你用CTC训练过模型,一定见过训练曲线的典型特征:刚开始损失下降很快,因为模型在学“别乱输出”;到后期blank概率变得很高,模型倾向先输出一批标签,再用一堆blank拖着等待后面信息。这说明模型正在用自己的方式找对齐。实际上一旦训练收敛,CTC的blank位置往往对应语音里的静音或音素之间的间隔。我经常把blank概率曲线可视化,它能直观告诉你模型认为哪段音频是“空”的。
3.3 CTC模型最让人头疼的条件独立与延迟问题
CTC有一个基础假设:给定输入特征,每一帧的输出相互独立。这意味着模型无法直接利用已经识别出的词句来修正下一个字的判断,只能靠声学特征本身。所以在识别“我去公园玩”这类句子时,纯CTC容易在“公园”和“公元”之间犯错,后续语言模型可以纠正,但训练阶段没有这个能力。工程上,大家通常会再接一个语言模型做重打分,或者把CTC和其他序列模型结合来补上下文。
其次是延迟问题。CTC在流式场景里常用,但有一个老毛病:为了让输出路径尽量保证单调,模型倾向于先输出大量blank,等积累足够信息后再吐标签,这就造成标签输出相对真实语音滞后。常见缓解办法是在流式识别里加延迟惩罚,或者设计尖峰约束让标签尽量集中在语音能量强的位置。如果你拿一个纯CTC模型做实时识别,会发现识别结果没错,但出字时间比说话内容慢了几百毫秒,这个在交互场景里体验很差。
4. RNN-T的对齐:逐帧决策里塞进序列记忆
4.1 三张网络各管一段:编码器、预测器、联合器
RNN-T,也就是RNN Transducer,核心是三网络架构。编码器负责读入当前帧以及周围声学上下文,输出声学表征;预测网络接收之前已经识别出来的标签,比如上一个字或词片,输出“接下来该说什么”的表征;联合网络把两者融合,产生在输出标签集合上加空标签的一个概率分布。
关键区别在于,RNN-T每一步预测“要不要输出一个标签”,不是只靠当前帧,而是同时依赖前面已经输出的标签。相当于每个时刻,模型都会结合“我刚才说到哪了”来决定现在该不该往外蹦词。这就不存在CTC那种输出条件独立问题。这个特性让RNN-T自带语言建模能力,看起来像是“CTC加了一个条件语言模型”,但它和外部语言模型不一样,外部语言模型是识别后再插进去的,RNN-T直接在学习目标里就带着对历史输出的依赖。
4.2 空标签与循环:RNN-T的路径是怎么走出来的
直观上,RNN-T到底是一帧对应一个标签,还是多帧对应一个标签?答案是都有可能,靠同一个时刻内的循环实现。标准流程是:模型在当前帧和当前输出位置,联合网络给出一个概率分布。如果选择空标签,就消耗这一帧,推进到下一帧;如果选择非空标签,就输出这个标签,同时把输出位置推进到下一个位置,但当前帧还保留。同一个帧内可以连续预测多个标签,直到选择空标签才进到下一帧。这个逻辑既能处理慢速发音(很多帧才一个音),也能处理快速连续输出(一个帧内要吐好几个字符)。
训练时,损失函数也是对所有合法对齐路径求和。这个和可以用一个二维格子上的前向-后向递归来算:横轴是帧,纵轴是输出标签位置,每个状态有两个转移方向,一个由空标签驱动,一个由真实标签驱动。虽然比CTC多一维,因为多了一个预测网络的状态,但核心思想仍然是“把所有可能对齐路径的概率加起来”。
4.3 和HMM、CTC相比的实质变化
从对齐视角看,RNN-T和HMM、CTC的差别很清晰。和HMM比,RNN-T没有显式的子词状态序列,也不需要字典和决策树共享状态;时长信息通过空标签和循环来吸收,训练目标是判别式的序列概率,而不是生成式模型。和CTC比,RNN-T的输出不是每帧一个,而是通过循环来决定;预测网络让每一步输出的概率都依赖已经输出的序列,这一点是CTC做不到的。
正因为结构天然适合流式处理,RNN-T在实时语音识别领域很流行。它可以随音频帧增量计算,不需要看一眼完整句子再输出;同时语言信息直接在模型内部建模,一般比同等规模的CTC在相同测试集上表现更好。不过训练和推理成本也更高:联合网络要融合两路信息,解码器每步都要跑预测网络,beam search的复杂度比CTC大不少。
5. 从对齐到上线:三种方案的实际取舍和我踩过的坑
5.1 场景选型:离线、流式、资源受限怎么选
不少人一上来就想用RNN-T,但实际落地要看你手头的资源和产品形态。如果只是处理一批离线录音,不要求实时出字,用CTC加语言模型重打分往往又快又稳;如果是手机端实时语音助手,RNN-T确实是主流,但对算子融合、量化、内存占用要求都很高,否则延迟控制不住;如果你面对的是极小规模的嵌入式芯片,传统混合系统或经过剪枝的CTC可能更现实。
| 维度 | HMM/GMM混合系统 | CTC | RNN-T |
|---|---|---|---|
| 对齐形态 | 隐状态序列,子词状态 | 帧级标签序列,含blank | 帧与标签间的单调递进决策 |
| 是否需要外部词典 | 通常需要 | 不需要也可以 | 不需要 |
| 输出依赖 | 由状态转移限制 | 条件独立 | 依赖历史输出 |
| 流式支持 | 可以,但延迟较大 | 可以用,注意标签滞后 | 天然适合流式 |
| 训练成本 | 有复杂对齐管道 | 中 | 高 |
| 资源受限设备 | 成熟但系统复杂 | 轻量高效 | 较重,需裁剪 |
HMM本身也不是没有价值。它的强制对齐能力在很多数据产线里仍然被用来打音素级标签,发音词典校验、口语评测、语音切分都还需要精确的音素边界。所以别急着把HMM扫进历史垃圾堆,它更像是“对齐基础设施”。不同方案之间不是简单的替代关系,而是各自解决不同层次的需求。
5.2 对齐结果本身也是一种数据资产
不管用HMM强制对齐、CTC的blank后验,还是RNN-T解码时的标签触发时间,对齐的产出都可以沉淀下来重复使用。HMM强制对齐得到帧级音素边界,能辅助发音词典校验和发音评测;CTC的blank概率曲线可以判断一段音频里是否有过长静音、多说话人切换或者文本错位;RNN-T解码时每个非空标签的触发时间,本质上就是系统认为“现在该出这个字了”的时刻,可以用来做实时字幕、数字人口型同步。
我在实际项目中做过这样一件事:拿一个已经收敛的CTC模型把所有训练音频跑一遍,输出每帧的标签概率,再统计每段音频的对齐路径和标签文本的一致性。很多错误样本,比如文本和实际发音不一致,往往会在错误位置出现一个“高概率真实标签但和文本完全不同”的峰。把这个峰的置信度当成信号,就能批量筛出脏数据。对齐不只是训练阶段的副产品,更是数据质量检测、音频切分、标注校验的重要工具。
5.3 实操中翻车的三件事
第一件事,用Viterbi直接从CTC模型里取一条对齐路径当强制标注。CTC的目标是最大化所有合法路径的概率之和,不是某一条最优路径。所以两条在概率上相差不大的路径,边界可能完全错位。我最早做音素边界时直接拿CTC路径切分,结果发现同一句话跑两次,或者稍微改一下随机种子,边界能抖好几帧。后来我改成“多候选路径一致性检查”:对每条音频解码出多条高概率对齐路径,如果它们对应同一个文本但边界差异过大,就把样本标记为不确定。如果只是想拿稳定的音素边界,用带语言约束的传统强制对齐更可靠。
第二件事,训练RNN-T时loss卡在很平的位置。最常见原因是空标签的概率被模型估得太高,学到了一招“什么都不输出”。大多数帧本来确实不该输出真实标签,这个先验在数据里太强,模型一开始很容易陷进去。我后来发现,初始化时给预测网络一个较小的输出幅度,或者控制一下空标签logit的初始偏置,训练前期更容易蹦出第一批真实标签。这个幅度要根据实际数据去试,调太大模型会乱输出,太小又会在原地停滞很久。
第三件事,流式部署RNN-T时只盯着识别率而忽略对齐触达时间。识别率相同的情况下,不同延迟惩罚配置下标签出现早晚差别很大。特别是做实时字幕,一个字迟到太久体验会非常差。我现在每次测流式模型,都会在评测集上专门算“标签相对真实短语的触发时间分布”,而不只看字错误率。这个指标对交互体验的反映,比单纯准确率要敏锐得多。
最后再分享一个经验:对齐机制在语音识别里的角色,远不只是训练损失的一部分。把HMM、CTC、RNN-T放在一起看,其实对应着“结构约束”“路径松弛”“自回归决策”三种生成对齐的思路。很多新模型都还在这些方案之间做取舍,比如有人在RNN-T里加入中间音素级预测,让对齐可以被外部监督;也有人用CTC输出的对齐路径去做知识蒸馏,把对齐信息传递给更小的学生模型。核心问题始终是如何利用好帧与标签之间那层看不见的对应关系。如果你也在折腾对齐,建议先把三种对齐路径分别画出来,用空标签概率和触发时间去看模型到底在做什么决策。看得多了,很多训练问题自己就有答案。