ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

语音识别中的CTC与RNN-T:端到端对齐与工程选型详解

语音识别中的CTC与RNN-T:端到端对齐与工程选型详解

如果你正好在啃语音识别的经典教材或者课程,大概率会在前几章就碰到 CTC 和 RNN-T 这两个名字。我当年第一次看到这两个缩写,脑子里全是问号:CTC 不是通讯技术么,RNN-T 和 RNN 又差在哪?后来在几个语音项目里真正把这两种模型都训练过、部署过、也踩过坑之后,才慢慢把它们的来龙去脉理顺了。这篇东西就是想把这块硬骨头啃碎,从“为什么需要它们”讲到“训练的时候会发生什么”,最后说说实际工程里怎么选型、怎么调,尽量让看完的人能直接拿去跟同事讨论或者照着搭基线。

先说清楚这东西适配谁看。如果你是要做语音识别系统、做家居助手、做实时字幕或者会议转写,需要自己训模型,这篇可以帮你把最常用的端到端建模方案摸熟。如果你只是调 API,可能不需要自己训网络,但理解 CTC 和 RNN-T 的区别,也能帮你解释为什么有些 API 在流式场景下表现好、有些断句总是慢半拍。下面这些内容全是围绕语音识别里最核心的“序列到序列”建模展开的。

1. 语音识别的核心矛盾:两段不等长的序列怎么对上

语音识别说白了就是给一段声音,输出一段文字。但你随便拿一个句子去录下来,用帧率提取特征之后,语音帧的数量和文本里的字符数量完全不是一个量级。假设一个三秒钟的句子,按 10ms 一帧提特征,大概有 300 帧;而这句话可能只有十几个汉字。让模型把 300 帧映射到 15 个字,这中间谁跟谁对齐,就是语音识别最绕不开的问题。

在早年间,业界主流的做法是用 HMM 来承担对齐这个任务。HMM 把每个音素建模成几个状态,状态之间允许跳转,然后用维特比算法在训练数据上自动算出每一帧对应哪个状态。这套框架和深度神经网络结合之后,变成 DNN-HMM 混合系统,在很长一段时间里都是工业界的主力。但它的麻烦在于整个流程太复杂,你需要音素词典、需要上下文相关状态聚类、需要决策树绑定,还要单独训练语言模型,任何一环出问题,整个识别链条都会受影响。

端到端模型想做的事情,就是把这套复杂的对齐机制从显式设计变成隐式学习。模型自己决定某一帧对最终输出的哪个单元贡献更大,不需要你手工设计“每个音素由三个状态构成”这种先验。而实现隐式对齐最经典的方案,一个是 CTC,一个是基于注意力的编解码结构,另一个就是 RNN-T。这三个方案各占一条路,也各有各的脾性。

如果打一个比方,对齐问题就像两个人要合作翻译一句很长的话,一个人在朗读,一个人在记录。CTC 的做法是让朗读的人自己加很多“停顿和空拍”,记录的人只需要按顺序把听到的字写下来;RNN-T 的做法是记录的人每写完一个字,会轻声“嗯”一下,告诉朗读的人继续念下一段。后面你会看到,这两个比方其实非常贴近两者的数学设计。

2. CTC:用“允许跳过的对齐路径”绕开繁琐的逐帧标注

CTC 全称 Connectionist Temporal Classification,翻译过来是联结主义时间分类。它最早是 2006 年提出的,专门用来解决时序分类问题,后来在语音识别领域被大规模验证。它的核心想法并不复杂:既然我们不知道每一帧对应哪个字符,那就把所有可能的对齐方式都考虑进来,然后让模型去最大化所有合法对齐路径的总概率。

2.1 引入 blank 符号为什么是 CTC 最关键的一步

CTC 有一个在刚接触时比较容易懵的设计——引入了一个名为 blank 的额外符号。它不是一个真的字符,而是表示“当前这一帧不输出任何内容”。为什么需要它?因为语音帧数远多于字符数,中间必然有大量帧是同一个字符的持续发音。如果一个字符持续了十帧,你怎么知道这十帧对应的是同一个字符而不是十次重复?CTC 的方案是允许同一个字符连续输出,并且中间用 blank 隔开。

比如“hello”这个词,假设语音里“e”这个音拖了 8 帧,一种合理的对齐路径可能是:

h h blank e e e e e e l l l o o

最后把重复的相邻字符折叠,再把 blank 删掉,就得到h e l l o。注意这里有个细节,识别目标里连续出现两个“l”,在折叠时要保证它们之间一定有一个 blank。如果模型把两个 l 紧挨着输出,折叠后只会算成一个 l。这就是 CTC 处理重复字符的方式:相邻相同输出如果没有 blank 隔开,就会被合并。

训练的时候,模型对每一帧都输出一个在所有字符加 blank 上的概率分布。整条路径的概率就是每一帧概率的连乘,而最终损失函数是“所有折叠后能得到目标文本的路径概率之和”的负对数。听着好像要枚举无数条路径,但实际可用动态规划高效计算,前向变量 α 和后向变量 β 就是用来做这件事的。这也是 CTC 经典的前向-后向算法,跟 HMM 里的前向-后向思路一脉相承。

2.2 CTC 训练时你会在日志里看到什么

我印象最深的是,第一次训练一个 CTC 语音模型,观察训练 loss 的下降曲线和最终输出的对齐结果,那种感觉非常奇妙。训练初期,网络几乎把所有帧都输出成 blank,因为这样最安全——只要不犯错,空白路径也能得到一定概率。随着训练推进,输出的字符概率开始集中在一些特定帧附近,形成了很明显的“尖峰”。

这里有个典型现象:**学习率调太高时,对齐尖峰可能过早定型,后期想纠正就很难。**比如一个词的重音位置偏向中间,如果早期模型把它对齐到了开头,后面无论怎么训,它都很难把概率挪回去。CTC 的这个特性,让它在训练时需要相对保守的学习率策略,尤其是大规模数据训练时。

另外,CTC 在做推理时有几种解码方式。最简单的是贪心路径合并:每帧取概率最大的输出,合并重复、去掉 blank。这个方式快但容易出错,尤其在没有语言模型纠正时,同音的错字会比较多。更好一点的是束搜索,它不是只保留一条路径,而是同时维护多条候选序列,考虑前缀合并,再加上语言模型分数,这是 CTC 在工程上比较实用的解码方式。我在项目里常用的是在 beam search 里加一个词级语言模型做浅融合,能明显压低词错误率。

2.3 CTC 的“条件独立假设”为什么被吐槽最多

CTC 最大的理论缺陷,是它假设各帧输出之间相互独立。也就是说,模型在判断当前帧输出什么时,不能依赖它上一帧刚输出了什么。这在语音识别里很不自然,因为语言是有依赖性的:前面已经说了“今天天”,后面更可能是“气”,语音上也会有对应唇形和发音的协同发音现象。CTC 没有建模这种输出端依赖,它更多是靠一个外部语言模型在解码阶段弥补。

这就导致了 CTC 的另一个实际问题:**它对词汇和语法的建模能力偏弱。**如果你在纯中文普通话数据上训练,不带任何语言模型,你会看到识别结果经常出现声学上很像但语义上狗屁不通的句子。它不是没听清,而是没有机会利用“哪个词跟哪个词更常搭配”的知识。为了缓解这个毛病,工程上几乎都会在解码时外挂一个语言模型,把语言模型得分和 CTC 声学得分按权重相加。

3. RNN-T:把“声学模型”和“语言模型”喂到一个联合网络里

RNN-T 全称 Recurrent Neural Network Transducer,这个名字里带着“换能器”的叫法。设计它的人很明显是为了解决 CTC 没有输出依赖的问题。核心思路是:在 CTC 的框架上再加一路“已经预测出的文本信息”,让每一帧的决策不仅看声学特征,还看到目前已经生成了什么内容。这样系统的行为就更像一个会边听边想的人。

3.1 三个子网络各司其职,Joint 层负责融合

RNN-T 的总体结构由三个部分组成。首先是编码器,也就是声学编码网络,作用跟 CTC 里的声学网络一样,把语音特征变成高层表示。然后是预测网络,它接收已经输出的 token 序列,负责建模“基于前面已说了什么,下一个词大概该是什么”。最后是联合网络,把编码器输出和预测网络输出组合起来,得到最终在输出词表上的概率分布。

联合网络内部常见的做法是把两个输入向量相加,经过一个非线性变换,再经过一个线性层输出 logits。有的实现是拼接,有的是相加,实验上相加的效果通常不错而且参数更少。但这个融合不是简单地两个特征叠加,它需要学习“声学证据”和“语言先验”之间如何妥协。比如预测网络觉得下一个字大概率是“的”,但是声学信息明显更像“地”,此时联合网络就要根据发音差异来裁决。这个裁决过程是 RNN-T 最核心的学习内容。

预测网络本身也很讲究,它通常是一个单层或者两层的循环神经网络,输入是已经输出的 token 的 embedding,第一个输入是特殊的句子开始符号。这个网络的目的不是做完整的语言模型,而是做一个轻量级的、逐字的语言先验生成器。工程上为了控制延迟,预测网络的层数和隐层大小都不会太大,否则解码时每步要多算一次前向,会拖慢实时率。

3.2 RNN-T 的损失函数怎么算:一张 T×U 的动态规划表

RNN-T 的训练是另一个层面的动态规划。CTC 算的是“帧序列上所有对齐路径的概率总和”,RNN-T 算的是“帧序列和对齐路径同时推进的概率总和”。具体到实现上,你会看到一张 (T+1)×(U+1) 的网格,T 是声学帧数,U 是目标 token 数。从左上角走到右下角,每一步要么“消耗一帧”(输出 blank,表示继续侦听声学),要么“消耗一个 token”(输出一个预测的字符)。每条从左上到右下的路径就是一种对齐方式,RNN-T 的 loss 就是所有合法路径概率和的负对数。

这个动态规划在训练时比较折腾,因为每个 mini-batch 里样本的 T 和 U 都不一样。用得比较多的优化是提前算好所有 t、u 位置的 logits,拼成一个 [batch, T, U, V] 的大张量,然后再用 GPU 上的并行扫描算法做前向-后向。这么做能大幅减少计算次数,但显存开销不小。我试过对中文词表,V 大概几千字,一个 batch 16 条音频,每条 500 帧、30 个字,这个张量也要好几个 GB,所以实际训练时 batch size 常常比 CTC 小得多。用更小的 batch size 意味着梯度估计更不稳定,这对学习率的设置和梯度裁剪的要求就更高,步子太大会直接训飞,步子太小又半天不收敛,需要多试几次才能找到平衡点。

RNN-T 推理时也很有意思。它逐帧推进,每一帧上模型会不断输出字符,直到输出一个 blank,才认为“当前帧已经榨干了信息”,转到下一帧。这种机制天然是流式的,只要系统一边拿音频一边逐帧解码。这也是 RNN-T 在语音交互场景里大受欢迎的原因之一。

3.3 预测网络的梯度问题怎么解

RNN-T 训练中一个隐藏难点是预测网络的梯度。因为预测网络是自回归的,每一步输入依赖前面生成的内容,误差信号在时间步上传播,梯度很容易消失或者爆炸。更麻烦的是,在动态规划的计算里,预测网络的输出不仅影响当前位置,还通过多条路径传播,这让它的梯度信号看起来特别不直观。

业界常见的处理方式是给预测网络单独设置学习率,通常比编码器的学习率低一截,或者配合较大的梯度裁剪阈值。我在实测中发现,如果不加约束,预测网络很容易在训练早期就过拟合到几个高频词上,比如中文里的“的”、“了”,导致模型只要声学稍微模糊一点,就偏着头往高频词上猜。所以处理这个问题的建议是,踩坑之后果断加上 label smoothing,让预测网络的输出不要那么尖,效果会稳很多。

另外,RNN-T 在工程上还有一个版本,叫 RNNT-T 的“无递归预测网络”(比如用 embedding 累加替代 RNN),目的就是绕开梯度传播和推理延迟问题。我在做低延迟项目时用过这种变体,效果跟标准 RNN-T 差别不大,但推理时少算了一个循环网络,实时性提高很明显。

4. 注意力机制、CTC、RNN-T 三条路线到底该站哪边

除了 CTC 和 RNN-T,还有一条非常流行的路线是基于注意力的编码解码结构,也就是常说的 LAS 或者 attention-based encoder-decoder。它跟前面两个方案的区别在于,解码每一帧对应什么字符时,采用注意力机制去“回看”编码器输出,而不是像 CTC 那样机械地对每一帧对齐,也不是像 RNN-T 那样逐帧盲目推进。注意力模型天然能学到一种较灵活的软对齐,从而在整句建模上往往有优势。

但注意力模型有个致命问题——它通常是面向离线场景设计的。标准的注意力计算需要看到整段音频的 encoder 输出,解码第一个单词时就要把所有帧的表示都准备好。即便使用限制单调注意力的变体,流式化也会牺牲部分精度,并且实现复杂度明显高于 RNN-T。实际项目里见过不少团队在“要不要用 attention”这件事上摇摆。如果你的产品是会议转写、录音文件识别这种不要求实时出字的场景,注意力模型的效果通常很好。但如果是对话式语音助手、实时字幕这类的产品,RNN-T 才是更顺手的工具。

拿我训练过的几个模型放在一张表里看,差异会变得更清楚:

维度CTCattention encoder-decoderRNN-T
对齐方式帧独立隐式对齐,靠 blank 分隔注意力软对齐,全局可回看逐帧推进 + 语言预测联合对齐
是否建模输出依赖否,条件独立假设是,解码器自带自回归是,预测网络建模语言先验
流式识别支持支持,但精度受限差,需要特殊改造好,天然逐帧
解码复杂度较低,beam search 简单较高,attention 一步步生成中等,需处理非单调前缀路径
对语言模型的依赖特别依赖外部 LM较弱,解码器内置语言模型中等,浅融合外部 LM 仍有效
训练资源开销低高中高,T×U 网格计算较贵

这张表里最值得琢磨的是“对语言模型的依赖”这一行。CTC 那行我特别标了“特别依赖”,因为它真是一点不建模输出端依赖,纯声学模型。而 attention 模型虽然在训练时见过大量文本,但遇到专业领域词汇和生僻表达时,仍然可能因为解码器自身的语言先验不够强而吃亏。RNN-T 的预测网络给了它一定语言建模能力,但这只是一个浅层模型,离一个上 GB 参数的语言模型还差很远,所以工程上一般还是要做浅融合。

还要提一句,很多人把端到端语音识别和“不需要文本数据”挂钩,这是个误解。CTC 和 RNN-T 训练依旧需要大量配对好的语音文本数据,而且需要做大量数据清洗、文本规范化,比如把数字转成汉字、英文词转成音素序列,这些前置处理往往比模型结构选择更影响最终效果。我接手的项目里,数据清洗阶段经常占掉一半的时间,这部分工作看起来不起眼,但做好之后模型效果能涨几个点,做不好后面全白干。

5. 工程落地中比算法更磨人的几个问题

很多人在读 CTC 和 RNN-T 的理论时觉得头头是道,但真正上手训练和部署,就会碰到一堆论文里不写的小麻烦。我在这部分把近期踩过的坑、调理好的方案集中说一下,都是会直接影响上线质量的东西。

5.1 训练稳定性:怎么防止 loss 突然变 NaN

RNN-T 的 loss 计算涉及大量指数和对数运算,数值稳定性是个大坑。早期的实现里经常在序列很长、batch 较大时,出现 loss 变 NaN 的问题。正常人第一反应是降低学习率,但有时候降低学习率也不管用,因为问题出在对数域和指数域的转换精度上。

标准解法是在计算动态规划时全都在 log 空间进行,用 log-sum-exp 而不是先取指数再求和。很多深度学习框架的 loss 实现已经内置了这个处理,但你如果自己写过 RNN-T loss,十有八九会遇到这个问题。另一个常见技巧是梯度裁剪设为全局范数裁剪,阈值大概在 1.0 左右,对 RNN-T 来说算一个比较安全的默认值。如果还想更稳,可以对预测网络单独设置更小的学习率,比如编码器的 0.5 倍或者 0.3 倍,这个方案在很多开源代码里也能看到。

5.2 推理延迟:预测网络是流式解码的隐形瓶颈

RNN-T 看起来是流式的,但真正做实时解码时你会发现,除了声学编码器,预测网络每次生成一个非 blank token 都需要做一次前向计算。如果你的预测网络是一个双层 LSTM,这一步就点亮了:每出一个字,就要跑一遍 LSTM,而 LSTM 的时间步依赖导致它没法并行。偏偏在交互式场景里,用户每说一句话可能产生十几个字,每字都跑一次 LSTM,延迟就被拉上去了。

解决思路有几个。先看你是追求极限延迟还是追求精度。延迟敏感的,可以把预测网络改成纯 embedding 累加结构,或者用 qRNN 这类简化循环层。不太敏感的,可以在解码时做局部束搜索,用一定的计算量换取准确率。实际工程里还会用到一个技巧,就是限制每一帧最多输出的 token 数量,比如每帧最多出一个字,否则容易出现“一帧连出五个字”这种罕见但会让用户明显感到卡顿的路径。

5.3 中文场景的特殊性:分字还是分词

中文语音识别有个绕不开的分叉口:建模单元用字还是用词。CTC 和 RNN-T 在建模单元上都有点挑食。用词做建模单元,词表会特别大,而且 OOV 问题严重,遇到人名地名就抓瞎。用字做建模单元,词表很小,但模型输出的文本没有一个自然分词边界,后续的标点恢复、语义理解都得再想办法。

我最后跑通的做法是字建模加句级语言模型浅融合。声学模型输出汉字序列,然后解码时用一个基于词的语言模型去打分数,这样既控制了词表,又能在解码阶段利用词级别约束。也看到一些团队直接基于子词建模,但中文里子词切分方法不像英文 BPE 那么普及,相对小众。如果你做的是中文项目,一开始就按字建模,后面接标点和逆文本正则化,是比较稳妥的路径。

5.4 WER 降低和主观体验不一定同步

这是很多团队容易忽视的点。CTC 和 RNN-T 的优化目标是词错误率或字错误率,但这个指标和你用户的主观体验并不总是一致。典型情况是,模型把一个四字成语说错了一个字,WER 是 25%,但用户完全看不懂;另一句长语音有一个助词没识别对,WER 是 5%,用户却毫不在意。前者才是真正让用户觉得“这识别好差”的场景。

因此在调模型时,除了盯着 WER,还必须盯着关键语义错误率。实践中可以在测试集里专门标注命名实体和核心槽位,看看这些位置的准确率。如果 CTC 模型在这块表现明显低于 RNN-T,往往不是因为声学差异,而是因为它缺少语言先验,同音字更容易选错。这时候与其改模型结构,不如先加强解码阶段的语言模型权重,拿特定领域的热词表做 biased beam search,见效会更快。

5.5 语言模型浅融合里的两个参数怎么调

浅融合的一般公式是最终的得分 = 声学模型 log 概率 + 语言模型 log 概率 × 权重 + 惩罚项。这个权重直接决定识别结果更偏向声学还是语言。经验范围通常在 0.1 到 1.0 之间,但具体值跟语料领域关系很大。如果你用的是通用领域文本训练的语言模型,权重太高容易把音频里少见的词强行纠正成常见词,导致凭空多出一些识别错误;太低又等于没加。可以从 0.3 起试,每次加减 0.1,用一小块验证集跑一遍,看整句准确率和关键实体准确率的变化趋势。

另一个参数是“插入惩罚”或者叫“词数惩罚”,它控制模型倾向于多输出还是少输出几个字符。在中文里这个尤其敏感,因为同一个句子加不加“了”、“的”、“吗”都可能影响语义。如果发现识别结果总是丢尾字,比如“今天天气很好”识别成“今天天气好”,可以稍微调低惩罚,鼓励模型多输出字符;反过来如果经常出现多余的字,就调高惩罚。

6. 选型决策和个人经验总结

到了真正要落地的时候,选哪条路线其实没有一个万能答案。我把自己的场景梳理成几个决策条件:如果产品是纯离线转写,而且数据多、算力足,attention 编解码模型常常能拿到最低的 WER;如果产品是实时交互场景,首选 RNN-T;如果团队刚起步、希望在相对短时间内跑通一个可用的基线,CTC 是最稳妥的入门选择。CTC 的代码实现、调参经验、并发解码方案都更成熟,虽然在最高精度上有天花板,但作为第一版产品完全够用,后续再迭代到 RNN-T 也顺理成章。

从端到端语音识别的发展脉络来看,CTC 解决的是“机器怎么知道一帧对哪个字”这个问题,RNN-T 在这个基础上补上了“机器怎么结合已说的内容预测下一个字”这半块拼图。理解它们的核心差异后,再去看一些更时髦的变体,无论是对比损失、基于 transducer 的流式模型,还是大规模预训练语音模型自带的对齐机制,都会觉得没那么玄乎了。

最后想分享一个实操中比较铭心刻骨的教训。之前训练 RNN-T 时,日志显示的 loss 一直在降,但测试 WER 就是不见好,后来排查发现是预测网络崩了,它输出的 embedding 全部变成同一个向量,联合网络基本退化成一个纯声学模型。当时在调试上花了两三周才发现是学习率过大导致的。从那之后,我在训练日志里不仅看 loss,还会定期把预测网络输出的 embedding 拉出来看分布,检查离散度,一旦发现分布坍缩就立刻降学习率。这个小习惯建议正在训练这类模型的朋友也保留一下。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进