Advertisement

WeNet 更新:支持热词增强

阅读量:

在语音识别的实际应用中,对于常用的词汇识别效果比较好,但是对于一些特有的人名、歌名、地名或者某个领域的专有词汇,例如人名宋星辰 、歌名国际歌 、地名丽泽商务区 以及语音识别专业词汇解码器 ,可能存在识别准确率不高的情况。对于这些专有词汇,通过在 WeNet 中使用热词增强方案,添加热词可以显著提升识别的准确率。

近期,WeNet 的更新支持了两种解码器上热词增强,包括 CTC Prefix Beam Search 和 WFST Beam Search。

热词增强

热词增强在论文中也叫 Context Biasing 或者 Contextual Biasing,相当于是把一些先验的知识加入到了语音识别系统中。WeNet 在解码过程中维护一个 Context Graph 中的状态。通过子图中的状态计算热词的得分,然后通过浅融合 (Shallow Fusion) 的形式在束搜索的过程中进行加分。

声学得分和热词得分的加权公式如下:

Context Graph

假如我们需要对下述热词进行增强:

王思

欧阳唯一

唯品会

指定热词中每个字的得分为 3,则 Context Graph 的构图如下图所示:
图片

在解码过程中,当匹配到对应前缀时,就会得到相应的分数奖励。为了防止前缀相同,但不是完全匹配的现象。我们加了一条回退弧,通过这条弧移除之前奖励的分数。例如“欧阳修”不能完全匹配任意热词,回退弧则移除“欧阳”两个字得到的 6 分奖励。

为了易于确定匹配到的热词的起始点,对于每个前缀,WeNet 只记录一个状态。即在同一时刻,只能进行一个热词的匹配,只有该热词匹配成功或者失败后才能开始匹配其他热词。

使用该策略,“欧阳唯品会”则无法成功匹配“唯品会”。因为“欧阳唯品”的“品”字导致“欧阳唯一”匹配失败,回到 0 状态,而“会”字无法匹配任意热词(此处还可以做一些优化的工作,例如回到 0 状态后,“品”字继续匹配等等)。WeNet 不考虑 这种情况。

复制代码
 int ContextGraph::GetNextState(int cur_state, int word_id, float* score,

    
                                bool* is_start_boundary, bool* is_end_boundary) {
    
   int next_state = 0;
    
   // 遍历当前状态上所有的弧
    
   for (fst::ArcIterator<fst::StdFst> aiter(*graph_, cur_state); !aiter.Done();
    
        aiter.Next()) {
    
     const fst::StdArc& arc = aiter.Value();
    
     if (arc.ilabel == 0) {
    
       // 记录回退弧的分数,如果匹配成功,该分数会被覆盖
    
       *score = arc.weight.Value();
    
     } else if (arc.ilabel == word_id) {
    
       // 匹配成功,记录下一个状态和奖励的分数
    
       next_state = arc.nextstate;
    
       *score = arc.weight.Value();
    
       // 判断是否为热词的开始或结束
    
       if (cur_state == 0) {
    
         *is_start_boundary = true;
    
       }
    
       if (graph_->Final(arc.nextstate) == fst::StdArc::Weight::One()) {
    
         *is_end_boundary = true;
    
       }
    
       break;
    
     }
    
   }
    
   return next_state;
    
 }
    
    
    
    
    代码解读

在前缀束搜索的过程中,每个前缀需要记录热词匹配的信息。加上当前时刻的输出后,若前缀发生变化则直接调用上述 GetNextState 函数更新热词匹配的状态和分数。如果是热词的开始或者结束,还需要记录开始的位置和结束的位置,用于在识别结果中插入开始标签和结束标签,如:“前往吴迪幸福里的家”。

由于 WFST 束搜索采用的是 Kaldi 中的 Lattice Faster Online Decoder,所以为了支持热词增强,我们需要对 lattice-faster-decoder.cc 做一些修改。

WFST 束搜索根据 CTC 的输出在 TLG 上进行解码,我们一开始的想法是在 TLG 的输入标签 (ilabel) 上做热词增强,也就是经过语言模型之前做热词增强。这样的话 Context Graph 的输入标签和输出标签都是字级别,而且我们只需要修改 ProcessEmitting 函数。但是由于 TLG 的输入是 CTC 的输出,如果我们要维护热词的匹配状态,那么 Context Graph 还需要和 T (Token Graph) 进行 compose 操作。

我们最终决定在 TLG 的输出标签 (olabel) 上做热词增强,也就是在经过语言模型之后做热词增强。同时修改 ProcessEmittingProcessNonemitting 函数,加入相同的热词增强逻辑。Context Graph 构图时,只需要根据语言模型的字典对所有热词进行分词即可:
图片

复制代码
 Elem *e_next =

    
     FindOrAddToken(arc.nextstate, frame + 1, tot_cost, tok, NULL);
    
 // NULL: no change indicator needed
    
  
    
 // ========== Context code BEGIN ===========
    
 bool is_start_boundary = false;
    
 bool is_end_boundary = false;
    
 float context_score = 0;
    
 if (context_graph_) {
    
   if (arc.olabel == 0) {
    
     e_next->val->context_state = tok->context_state;
    
   } else {
    
     e_next->val->context_state = context_graph_->GetNextState(
    
       tok->context_state, arc.olabel, &context_score,
    
       &is_start_boundary, &is_end_boundary);
    
     graph_cost -= context_score;
    
   }
    
 }
    
 // ========== Context code END ==========
    
  
    
 // Add ForwardLink from tok to next_tok (put on head of list
    
 // tok->links)
    
 tok->links = new ForwardLinkT(e_next->val, arc.ilabel, arc.olabel,
    
                               graph_cost, ac_cost, is_start_boundary,
    
                               is_end_boundary, tok->links);
    
 tok->links->context_score = context_score;
    
    
    
    
    代码解读
剪枝

Context Graph 的回退弧会一次性将前面累计的奖励分数回退到一条弧 (ForwardLink) 上,导致该弧的 cost 特别大而被剪枝,它不该一条弧承受这么多。因此在剪枝之前,我们需要移除回退弧产生的 cost,此处感谢爱奇艺@陈海涛 同学提出的解决方案。

复制代码
 void LatticeFasterDecoderTpl<FST, Token>::PruneForwardLinks(

    
     int32 frame_plus_one, bool *extra_costs_changed, bool *links_pruned,
    
     BaseFloat delta) {
    
   ...
    
   BaseFloat link_extra_cost =
    
             next_tok->extra_cost +
    
             ((tok->tot_cost + link->acoustic_cost + link->graph_cost) -
    
              next_tok->tot_cost);  // difference in brackets is >= 0
    
   // ========== Context code BEGIN ===========
    
   // graph_cost 中包含了热词的分数
    
   // link->context_score < 0 表示该 link 上热词的分数来自于回退弧
    
   // 剪枝的时候不应该考虑回退弧的分数,所以应该从 link_extra_cost 中移除
    
   if (link->context_score < 0) {
    
     link_extra_cost += link->context_score;
    
   }
    
   // ========== Context code END ==========
    
   // link_exta_cost is the difference in score between the best paths
    
   // through link source state and through link destination state
    
    
    
    
    代码解读
实验结果

With-context (183 条):

拨打崔文平的办公电话

打给王思

打欧阳唯一159开头电话

...

Without-context (200 条): 从 aishell 测试集中随机抽 200 条,表示通用数据集

模型使用的是 aishell2 的 20210602_unified_transformer_server.zip。每一列对应不同的 context_score。由于 with-context 中包含较多的数字,而解码结果没有做 text-normalization,故 WER 较高。

Prefix Beam Search 0 1 3 5 10
with-context 27.71% 27.65% 25.64% 22.26% 19.05%
without-context 1.59% 1.59% 1.66% 1.86% 2.62%
WFST Beam Search 0 1 3 5 10
with-context 29.83% 29.40% 28.36% 26.73% 25.10%
without-context 0.99% 0.99% 0.99% 0.99% 1.09%

从实验结果可以看出,随着 context_score 的增加,with-context 数据集上的 WER 越低。当 context_score 超过 5 时,会导致通用数据集上的 WER 升高。WFST Beam Search 有了语言模型的加持, context_score 可以设置大一些。

限制

WeNet 提供了一种端到端热词定制化的方案,但是该方案在工程化的时候没有对下述问题做详细的探索。

context_score 设置为多少比较合适?对于不同模型和不同数据集,如何能快速确定该值?

某些情况下,热词奖励(但不是正确识别结果)得分过高,导致正确路径被裁剪掉,又该如何避免?

等等。对于上述限制,希望感兴趣的同学可以多多提供方案,多多到 github 上提交 pull request。




WeNet 更新:支持热词增强

全部评论 (0)

还没有任何评论哟~