声学模型深入:CTC 与 RNN-T——语音识别怎么对齐声音和文字

· 约 6 分钟 ·
本文目录(10 节)

语音识别首先要处理一个对齐问题:音频帧和输出文字的数量不同,也没有现成的逐帧对应关系。例如一段音频有数百帧,却只对应几个字。CTC 和 RNN-T 提供了两种在没有逐帧标注时训练模型的办法。理解它们的输出路径和条件假设,再看损失函数,会比直接背公式容易得多。

前置阅读:建议先读 语音识别基础、RNN 与 LSTM。

对齐问题:一个具体例子

音频「你好」切成 10 帧,声学模型每帧输出一个字符的概率分布。真实标注是「你好」两个字。帧和字的对应关系可能是:

帧:   1  2  3  4  5  6  7  8  9  10
方案A: 你 你 你 -  -  好 好 好 好  -
方案B: -  你 你 你 你 -  -  好 好  好
方案C: 你 你 -  -  好 好 好 -  -  -

A、B、C 压缩后都是「你好」——正确的路径有成千上万条,且我们事先不知道哪条是对的。这正是 CTC 要解决的核心问题。

CTC:对所有合法路径求和

CTC(Connectionist Temporal Classification)的三件套设计:

1. 引入空白符 blank(记作 -):允许模型在某些帧输出「什么都没有」,解决「音频比文字长」的问题。

2. 定义路径压缩规则:先合并连续重复字符,再删除 blank。「你 你 - 好 好 好」→「你 好」。注意顺序:「你 - 你」压缩后是「你你」(两个你),因为 blank 隔开了重复——这巧妙地区分了单字和叠字。

3. 目标函数:所有能压缩成标注序列的路径概率之和:

P(Y∣X)=∑π∈B−1(Y)∏t=1TP(πt∣X)P(Y|X) = \sum_{\pi \in \mathcal{B}^{-1}(Y)} \prod_{t=1}^{T} P(\pi_t | X)

B−1(Y)\mathcal{B}^{-1}(Y) 表示所有压缩后等于 Y 的帧级路径。路径数是指数级的(10 帧 2 个字就有几十条),直接枚举不可能——用前向后向算法(和 HMM 同源动态规划)把求和压到 O(T×|Y|) 完成。PyTorch 里这个复杂的 DP 已经封装好,直接用:

import torch
import torch.nn as nn

ctc_loss = nn.CTCLoss(blank=0, zero_infinity=True)
# log_probs: (帧数T, batch, 词表大小) 模型每帧输出的对数概率
# targets:   (batch, 标注长度) 文字序列,无需任何对齐信息
loss = ctc_loss(log_probs, targets, input_lengths, target_lengths)
loss.backward()

解码:从帧概率到文字

训练完,推理时要把每帧的概率分布变成最终文字:

贪心解码:每帧取概率最大的符号,再压缩。快(O(T)),但只走了单条最优路径,丢掉了路径求和的优势。

束搜索(Beam Search):维护 top-k 个候选前缀,逐帧扩展。对 CTC 要特别处理「blank 分隔」和「重复合并」的状态。配合语言模型效果更佳——声学上「公式」和「公事」分不清时,语言模型根据上下文投票:

# 概念示意:束搜索 + 语言模型打分
score = log P_acoustic(path) + α * log P_lm(text) + β * len(text)

α 控制语言模型权重,β 修正 LM 对短句的偏好。调这两个参数曾经是语音工程师的日常。

CTC 的两个结构性短板

  1. 帧间条件独立假设:CTC 假设每帧输出相互独立,没有语言模型能力——「继往开来」的声学即使模糊,CTC 自己不会利用上下文纠正成合理成语,必须外挂语言模型。
  2. 输出不能长于输入帧数:对极端压缩的音频(快语速)有硬伤。

RNN-T:把语言模型内置进去

RNN-T(RNN Transducer)在 CTC 骨架上加了一个预测网络(Prediction Network)——它吃「已输出的历史文字」,相当于内置语言模型;再用**联合网络(Joiner)**融合声学信息和语言信息决定下一帧输出:

声学编码器(音频帧) ──┐
                     ├──> 联合网络 ──> 输出(含blank)
预测网络(历史文字) ──┘

关键区别:RNN-T 的每帧输出** conditioned on 已生成的文字历史**,打破了 CTC 的帧独立假设。收益是端到端单模型就有语言建模能力,且天然适合流式识别(边说边出字)——这就是它被 Google 用在 Pixel 手机本地语音输入的原因。代价是训练更复杂(联合网络让对齐搜索空间变大),解码也更重。

第三条路线:Whisper 的编码器-解码器

Whisper 干脆抛弃了「逐帧对齐」范式,回到标准 seq2seq:编码器把 30 秒音频压成特征序列,解码器像机器翻译一样自回归地逐 token 生成文字,对齐完全交给交叉注意力隐式学习。

维度CTCRNN-TWhisper (Enc-Dec)
对齐方式显式路径求和显式路径求和注意力隐式学习
语言能力无(需外挂 LM)内置强(完整解码器)
流式识别可以原生支持困难(要整段音频)
训练数据需求中等中等巨大(68 万小时)
当前地位教学与轻量场景流式产品主力离线转写 SOTA

选型直觉:做实时字幕、语音输入选 RNN-T 系;做录音转写、字幕生成直接用 Whisper 系;CTC 作为理解对齐问题的起点和小数据场景的轻量方案。

我做的对比实验

在同一个 100 小时中文内部数据集上(远低于 Whisper 预训练量),从零训练三个方案:

方案测试集 CER流式能力备注
CTC + 贪心解码18.7%✓同音字错误占大头
CTC + 束搜索 + 4 元 LM14.2%✓LM 救回 4.5 个点
RNN-T12.8%✓单模型效果最好
Whisper-small 微调9.6%✗预训练知识碾压

两个教训:语言信息值 4~5 个点的错误率;100 小时数据时预训练模型的优势是压倒性的——除非流式是硬需求,否则别从零训练。

踩坑与排查

症状可能原因排查方法
CTCLoss 返回 inf标注比帧数还长/有非法字符zero_infinity=True;检查 T ≥
输出全是一个字重复blank 索引设错确认 blank=0 且模型输出第 0 类是 blank
束搜索比贪心还差LM 权重 α 过大网格搜索 α、β;先在验证集调
训练 CER 降测试不降过拟合小数据集SpecAugment 音频增强;降模型容量
RNN-T 训练不收敛学习率/联合网络配置warmup + 较小 lr;检查 joiner 维度
长音频推理 OOM未分块处理按 VAD 切 30 秒以内片段

动手练习

  1. 手写 CTC 路径压缩函数:输入帧级序列(含 blank),输出压缩结果,并用「你 - 你」验证重复处理逻辑。
  2. 用 torchaudio 加载一段音频,提取 Mel 特征,过一个随机初始化的 BiLSTM + CTCLoss,跑通前向反向(不要求收敛,只验证张量形状)。
  3. 在预训练 CTC 中文模型上分别用贪心和束搜索解码同一段音频,对比 CER 差异。

面试常问

Q:CTC 怎么解决对齐问题?为什么需要 blank? CTC 不标注帧级对齐,而是把「所有能压缩成标注文本的帧级路径」的概率求和作为似然,用前向后向动态规划高效计算,反向传播时对每条路径按概率加权分配梯度。blank 的作用:允许帧无输出(音频长于文本);分隔连续重复字符(「你-你」→「你你」,区分叠字),没有 blank 就无法表达连续相同字符。

Q:CTC 的帧独立假设有什么问题?RNN-T 怎么改进? CTC 假设各帧输出条件独立,模型本身没有语言建模能力,遇到声学模糊(同音字)无法利用上下文,必须外挂语言模型。RNN-T 增加预测网络,以已输出文字历史为条件,与声学编码在联合网络中融合,输出分布同时依赖音频和文本历史,单模型具备语言能力,且天然支持流式。

Q:Whisper 为什么不用 CTC? Whisper 用编码器-解码器架构,解码器自回归生成文本,对齐由交叉注意力隐式完成。优势:解码器是完整语言模型,上下文利用充分,且天然输出标点、大小写、多语言标记;前提是有海量数据(68 万小时)让注意力学会对齐。CTC 系的显式对齐在小数据和流式场景仍有优势。

对齐问题是语音识别的「题眼」,看懂 CTC 与 RNN-T,就看懂了过去十五年语音技术的主线。

相关阅读:语音识别基础、Whisper 微调与声音克隆、声纹识别、RNN 与 LSTM。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…