PaddleSpeech CTC 前缀评分器CTCPrefixScorer源码解析U2 混合 CTC/注意力模型 Beam Search 的评分核心【免费下载链接】PaddleSpeechEasy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleSpeech导读本篇文章围绕 PaddleSpeech 中paddlespeech.s2t.decoders.scorers.ctc模块展开深度剖析其核心类CTCPrefixScorer的实现原理与在解码流程中的角色。该模块是 U2 / U2 等CTC 与注意力混合语音识别模型在 beam search 解码时不可或缺的评分组件它基于 CTC 前缀概率算法Watanabe 等人的 Algorithm 2为每个部分解码前缀计算扩展任意词元token的对数概率增量。读完本文你将理解 Scorer 接口体系的设计、CTCPrefixScore与CTCPrefixScorePD两种实现的差异、预剪枝pre-beam与批量batch评分的配合方式以及流式解码下状态扩展的实现细节并能在自己的解码配置中正确选用与组合这些评分器。说明本文对应仓库中的 API 文档入口 paddlespeech.s2t.decoders.scorers.ctc.rst该 RST 通过 Sphinxautomodule指令自动收录paddlespeech.s2t.decoders.scorers.ctc模块的全部成员、docstring 与继承关系。本文以该模块源码为骨架展开其设计与实战细节。一、模块定位Scorer 接口体系中的部分评分器在 PaddleSpeech 的 s2tspeech-to-text目录中decoders/scorers/下汇集了所有参与 beam search 的评分器它们是解码器在搜索时给下一个词元打分的策略单元。该目录包含文件内容scorer_interface.py定义了四层 Scorer 接口ctc.pyCTCPrefixScorer本篇文章的主角ctc_prefix_score.pyCTC 前缀概率算法本体NumPy 版与 Paddle 批量版length_bonus.py长度奖励LengthBonusngram.py基于 kenlm 的 n-gram 语言模型评分器1.1 四层接口的设计意图scorer_interface.py 定义了由粗到细的接口层次ScorerInterface最基础的评分接口对词表中所有词元打分核心方法是score(y, state, x)返回形状为(n_vocab,)的分数张量和下一步状态init_state初始化状态select_state按索引选取状态final_score可选地给出 eos 的最终分数。BatchScorerInterface在其上增加batch_score对多个假设hypotheses批量打分。其默认实现是退化的 for 循环加paddle.cat拼接并会打印 batch score is implemented through for loop not parallelized 警告提示子类应自行实现向量化版本。PartialScorerInterface部分评分器——它不扫描全词表而是接收已经预剪枝pre-pruned后的候选词元子集next_tokens进行打分因为对所有词元打分太重。这正是CTCPrefixScorer的用武之地。BatchPartialScorerInterface兼有批量化与部分评分能力CTCPrefixScorer正是实现此接口的类。从接口 docstring 可见PaddleSpeech 设计上把 scorer 分成了三类典型用法搜索启发式如LengthBonus、序列到序列模型的解码器网络如 Transformer/RNN decoder、神经语言模型如 TransformerLM、RNNLM而PartialScorerInterface的示例中明确点名了decoders.scorers.ctc.CTCPrefixScorer是CTC 模型前缀搜索的代表实现。这说明 CTC 前缀评分器在整条 scorer 体系中属于轻量级、只评分子集的一类天然与 beam search 的预剪枝机制配套。1.2 在 beam search 中的角色划分在 beam_search.py 的BeamSearch构造逻辑中传入的scorers字典会被按接口类型自动分成两组for k, v in scorers.items(): ... self.scorers[k] v if isinstance(v, PartialScorerInterface): self.part_scorers[k] v else: self.full_scorers[k] vfull_scorers对全部n_vocab个词元打分如注意力解码器、语言模型part_scorers只对预剪枝后的候选ids打分如 CTC 前缀评分器。打分阶段分别对应score_full与score_partial两个方法beam search 会先按full_scorers的加权分数做一次 top-k 预剪枝得到ids再把ids交给part_scorers精评分。CTCPrefixScorer就工作在第二个阶段。二、CTCPrefixScorer面向解码器接口的 CTC 前缀评分包装类ctc.py 中的CTCPrefixScorer本身并不实现前缀概率算法而是对CTCPrefixScore/CTCPrefixScorePD两个算法类做接口适配让它们能嵌入 beam search 的 scorer 调用链。2.1 构造与初始化class CTCPrefixScorer(BatchPartialScorerInterface): def __init__(self, ctc: paddle.nn.Layer, eos: int): self.ctc ctc self.eos eos self.impl None构造参数只有两个ctc一个 Paddle 网络层paddle.nn.Layer即 CTC 实现本身例如paddlespeech.s2t.modules.ctc.CTC。评分器需要调用它的log_softmax方法把编码器输出转成对数后验概率eos结束符end-of-sequence的 token id用于在评分时给输出到此结束的可能性打分。在init_state(x)中编码器输出x经过 CTC 层的log_softmax先unsqueeze(0)补 batch 维再squeeze(0)去掉转成 NumPy 数组然后用blank0、eos与 NumPy 后端构造单条版本的CTCPrefixScorelogp self.ctc.log_softmax(x.unsqueeze(0)).squeeze(0).numpy() self.impl CTCPrefixScore(logp, 0, self.eos, np) return 0, self.impl.initial_state()返回值是(0, initial_state)——即初始分数 0 与算法初始状态。从代码注释# TODO(karita): use CTCPrefixScorePD可以看出单条路径仍然沿用 NumPy 实现而批量路径才使用 Paddle 原生实现这是当前版本刻意保留的兼容行为。2.2 单条路径score_partial 与 select_statescore_partial(y, ids, state, x)是部分评分器接口的核心回调prev_score, state state presub_score, new_st self.impl(y.cpu(), ids.cpu(), state) tscore paddle.to_tensor( presub_score - prev_score, placex.place, dtypex.dtype) return tscore, (presub_score, new_st)这里有一个关键设计返回的是前缀概率的增量而非绝对值。算法返回累计前缀分数presub_score与新的算法状态new_st而接口层计算出presub_score - prev_score作为本次扩展词元带来的边际分数供 beam search 与其他 scorer 的分数做加权求和。状态则更新为(presub_score, new_st)把累计分数一并携带避免重复计算。select_state(state, i, new_idNone)负责在 beam 剪枝后按索引选取对应假设的状态。代码里对状态类型做了分支处理长度为 2 的 tupleCTCPrefixScore的(sc, st)直接按索引取sc[i], st[i]长度大于 2 的 tupleCTCPrefixScorePD的(r, log_psi, f_min, f_max, scoring_idmap)需要额外的new_id新词元 id来确定log_psi[i, new_id]再沿状态张量索引若存在scoring_idmap还要经过映射表查回真正的列位置。这个分支清楚地反映了两种算法类返回状态的结构差异也是理解select_state为何需要new_id参数的关键。2.3 批量路径batch_init_state 与 batch_score_partial批量路径与单条路径相对称。batch_init_state(x)假设 batch size 为 1# assuming batch_size 1直接从 CTC 层取对数后验构造CTCPrefixScorePDlogp self.ctc.log_softmax(x.unsqueeze(0)) xlen paddle.to_tensor([paddle.shape(logp)[1]]) self.impl CTCPrefixScorePD(logp, xlen, 0, self.eos) return None注意此处不再做.numpy()转换全程保留在 Paddle 张量空间内。xlen记录真实帧长供算法用于填充与末尾帧定位。batch_score_partial先把各假设的状态沿第 2 维 stack 成批量状态(r, s, f_min, f_max)再调用self.impl(y, batch_state, ids)一次性算出一批假设对候选词元的分数——这就是CTCPrefixScorePD存在的意义把对每个假设分别跑一遍前缀算法向量化为张量运算。2.4 流式解码扩展extend_prob 与 extend_stateCTCPrefixScorer还实现了两个扩展接口服务于流式解码streaming decodingdef extend_prob(self, x: paddle.Tensor): logp self.ctc.log_softmax(x.unsqueeze(0)) self.impl.extend_prob(logp) def extend_state(self, state): new_state [] for s in state: new_state.append(self.impl.extend_state(s)) return new_statedocstring 明确指出其理论依据是论文https://arxiv.org/abs/2006.14941即 U2 流式模型的论文中的 Eq (14)当编码器输出随时间逐步到达时CTC 前缀计算所需的概率矩阵与状态可以向后追加而非整体重算从而在保持前缀概率正确性的同时降低流式场景的计算开销。extend_prob负责把新到达帧的对数后验并入内部概率张量self.xextend_state负责把已有假设的前缀状态扩展到新的时间长度。三、算法本体CTCPrefixScore 与 CTCPrefixScorePD真正的前缀概率计算位于 ctc_prefix_score.py文件开头注释给出了权威出处CTCPrefixScore基于Watanabe et al., Hybrid CTC/Attention Architecture for End-to-End Speech Recognition的 Algorithm 2并扩展为同时高效计算多个标签的概率CTCPrefixScorePD是其批量版本扩展自Seki et al., Vectorized Beam Search for CTC-Attention-Based Speech Recognition, INTERSPEECH 2019, pp. 3825-3829用于同时对多个假设计算标签概率。两个类共同的核心思想是给定一个部分前缀假设h与整段声学特征的对数后验x动态维护两类前向概率r_t^n(h)在时间t结束于非空标签non-blank且恰好输出h的对数概率r_t^b(h)在时间t结束于空标签blank且恰好输出h的对数概率。状态张量沿第二维区分两者0 表示非空、1 表示空这正是initial_state中注释 r_t^n( ) and r_t^b( ) 的含义。在此基础上log_psi给出对所有候选下一标签的前缀概率即扩展后的完整前缀含 eos在整段特征下的对数概率。3.1 CTCPrefixScoreNumPy 单条实现构造时保存logzero -10000000000.0用于屏蔽不可能路径的极小值、blank、eos、输入长度与整段后验x。initial_state()构造形状(T, 2)的状态第 1 列全部为logzero非空路径初始不存在第 2 列从t0起累积 blank 概率即r_t^b(sos) Σ_{τ≤t} x[τ, blank]——初始假设sos只可能以一路输出 blank的方式到达任意帧。核心__call__(self, y, cs, r_prev)的算法要点output_length len(y) - 1去掉sos后的有效前缀长度为所有候选标签cs一次性分配(T, 2, len(cs))的新状态张量通过xs self.x[:, cs]只取候选列避免遍历全词表初始化技巧当output_length 0时直接把r[output_length - 1]置为logzero。注释引用了 espnet PR #3655说明由于r_t(h) 0 for t |h|帧数不足以输出整段前缀的路径概率为 0可以从t |h|才开始累加省去前若干帧的零计算——这是与论文 Algorithm 2 写法不同但等价的工程优化前向递推从start max(output_length, 1)到最后一帧r[t, 0] logaddexp(r[t - 1, 0], log_phi[t - 1]) xs[t] # 非空路径 r[t, 1] logaddexp(r[t - 1, 0], r[t - 1, 1]) x[t, blank] # 空路径 log_psi logaddexp(log_psi, log_phi[t - 1] xs[t]) # 前缀概率其中log_phi[t-1]是上一帧的前缀合计与当上一标签等于当前候选标签时只能走非空路径的组合——这是 CTC 中处理相邻相同标签合并collapsing的关键如果上一输出标签与当前候选相同则当前帧必须是非空路径对应r_prev[:, 1]否则可以来自非空与空两条路径之和r_sum 5.eos 处理若候选中有 eoslog_psi[eos_pos] r_sum[-1]即整段特征下的log(r_T^n(g) r_T^b(g))——输出到此结束的概率 6.blank 屏蔽log_psi[blank_pos] self.logzero即不允许 beam search 显式选择 blank 作为输出 7. 返回(log_psi, rollaxis(r, 2))把标签轴移到最前便于切片形状为(n_labels, T, 2)。3.2 CTCPrefixScorePDPaddle 批量实现批量版构造参数多了一个xlens每条样本的真实长度与margindef __init__(self, x, xlens, blank, eos, margin0):margin是论文式22, 23中的窗口参数Mmargin0表示不启用窗口。构造时它会对每条样本把超出xlens的帧填充为logzeroblank 位置填 0保证 batch 内长度一致且越界帧不贡献概率把x转置为(T, B, O)并额外构建xbblank 概率广播到全词表stack 成形状(2, T, B, O)的张量self.x第一维分别对应非空/空两条路径的后验记录end_frames xlens - 1供 eos 概率在每条的末尾帧处取值。__call__(self, y, state, scoring_idsNone, att_wNone)是批量前缀计算的入口n_bh batch × hyps表示批量 × 每句假设数的总假设数预剪枝支持当scoring_ids非空时通过scoring_idmap把全局词表 id映射到候选子集内的局部位置并用paddle.index_select只取出候选列做计算x_形状(2, T, n_bh, snum)snum为剪枝后候选数远小于全词表O反之则退化为对全词表打分注意力窗口当att_w与margin 0同时给出时依据注意力权重求出聚焦帧范围f_min/f_max把计算范围限制在[max(f_min - margin, ...), min(f_max margin, T)]内——这是流式/局部注意力解码中对 CTC 前缀计算做时间窗口剪枝的手段否则start max(output_length, 1)、end input_length即只跳过不足前缀长度的帧前向递推与单条版同构但整体为张量运算r[t] logsumexp(rr, 1) x_[:, t]其中rr组合了r[t-1]与log_phi[t-1]的四条路径非空×非空、空×空、非空×空、空×非空对应关系log_psi的 eos 取值按句索引self.end_frames[si // n_hyps]取该句末尾帧的前缀合计同样把 blank 列的log_psi置为logzero并返回(log_psi - s_prev), (r, log_psi, f_min, f_max, scoring_idmap)。类中还提供两个配套方法index_select_state(state, best_ids)beam 剪枝后按best_ids重新选取各假设的r与累计分数s同时通过scoring_idmap把剪枝 id 换算回候选子集内的位置保证状态索引始终正确extend_prob / extend_state与CTCPrefixScorer同名方法一一对应实现流式场景下概率矩阵与状态的追加扩展extend_state中会把新增帧的 blank 累计路径补上。四、与 BeamSearch 的协作预剪枝、加权与状态维护理解了 scorer 本身后再看它在 beam_search.py 中的完整协作流程4.1 预剪枝开关self.pre_beam_size int(pre_beam_ratio * beam_size) self.do_pre_beam (self.pre_beam_score_key is not None and self.pre_beam_size self.n_vocab and len(self.part_scorers) 0)do_pre_beam为真需同时满足三个条件指定了pre_beam_score_key用哪个 full scorer 做预打分、预剪枝规模小于全词表、且存在部分评分器否则预剪枝没有意义。当条件满足时beam search 会先用full_scorers的加权分数选出pre_beam_size个候选 id再让part_scorers即CTCPrefixScorer只对这少量候选计算len(ids)个分数——这正是PartialScorerInterface文档中因为对所有词元打分太重的工程落点。4.2 分数加权与状态传递初始化假设时init_hyp对每个 scorer 调用init_state(x)并把初始分数置 0每步扩展时score_full对 full scorer 打分、score_partial对 partial scorer 打分各 scorer 的分数按外部传入的weights加权求和后用于 top-k 选择剪枝后通过select_state把每个假设的状态对齐到幸存者CTCPrefixScorer的分支逻辑在此保证 NumPy 版与 Paddle 版状态都能被正确切片。4.3 其他可选评分器同一目录下还有两个可与之组合的 scorerlength_bonus.py 的LengthBonus实现BatchScorerInterface对每个词元恒定返回 1.0作为解码时的长度奖励缓解 beam search 对短句的偏好ngram.py 的Ngrambase基于 kenlm 的 n-gram 语言模型评分器把eos映射为/s后利用 kenlm 状态机做前缀评分适合与 CTC 前缀评分器叠加以引入语言模型先验。在scorers()字典中把这些评分器与ctcCTCPrefixScorer(...)并列注册即可实现注意力解码器 CTC 前缀 LM 长度奖励的联合打分。五、在 U2 模型中的实际接入作为实际使用证据u2.py 中U2DecodeModel的scorers()方法这样注册评分器def scorers(self): Scorers. return dict( decoderself.decoder, ctcCTCPrefixScorer(self.ctc, self.eos))即 U2 模型解码时同时注册了注意力解码器self.decoder作为 full scorer扫描全词表与CTCPrefixScorer作为 partial scorer对预剪枝候选做 CTC 前缀精评分两者的分数在 beam search 中按ctc_weight加权融合——这正是混合 CTC/注意力Hybrid CTC/Attention架构在解码侧的核心体现。ctc_weight、decoding_chunk_size、simulate_streaming、reverse_weight等解码参数在u2.py的forward/ 解码分支中配置其中CTCPrefixScorer的extend_prob/extend_state即为流式streaming与模拟流式解码路径服务。六、小结与工程要点围绕paddlespeech.s2t.decoders.scorers.ctc模块可以从源码层面归纳出以下关键结论接口分层清晰ScorerInterface→BatchScorerInterface/PartialScorerInterface→BatchPartialScorerInterfaceCTC 前缀评分器属于批量 部分打分这一类只对预剪枝后的候选词元计分算法有据可依前缀概率计算源于 Watanabe 等人的 Algorithm 2单条版CTCPrefixScore与 Seki 等人的向量化 beam search批量版CTCPrefixScorePD核心是r_t^n(h)/r_t^b(h)两路动态规划与log_psi前缀概率并正确处理了 CTC 的相邻标签合并、blank 屏蔽与 eos 结束概率工程优化到位单条版从t|h|开始累加以减少循环批量版支持scoring_ids预剪枝列选择与注意力驱动的margin时间窗口另有extend_prob/extend_state支持流式解码的增量扩展接入方式明确通过U2DecodeModel.scorers()注册为ctc评分器与注意力 decoder、kenlm n-gram、长度奖励等任意组合配合pre_beam_ratio等参数在BeamSearch中完成先粗筛、后精评分的两阶段打分。对于希望深入定制解码策略的开发者建议从 ctc.py 与 ctc_prefix_score.py 入手前者是 scorer 接口与算法类之间的胶水层后者是算法本体再结合 beam_search.py 的do_pre_beam与score_partial调用点即可完整理解一条音频从编码器输出到最终文本的评分全链路。【免费下载链接】PaddleSpeechEasy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleSpeech创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考