本页说明了我们对动态创建的额外部分的语法和解码图的支持,这些部分能够快速编译(例如要添加到词典中的单词;联系人列表;类似的东西)。
我们已经使用“语法”一词作为该框架的易于搜索的术语,但这并不是在Kaldi中实现语法的唯一方法。 如果您的文法较小,固定,则直接从文法中创建FST(G.fst
)可能更容易(必要时能通过消歧符号来进行确定化G.fst
),并使用常规的解码图创建方法。 该框架专用于您迫切需要为各个预编译的HCLG.fst子部分并将它们动态拼接在一起的情况(通常是避免在运行时重新编译大图)。
该框架仅适用于left-biphone
模型。 这不会造成性能损失,因为我们最好的模型(chain模型)已经使用了left-biphone
上下文。
与OpenFst的Replace()
操作的关系
这些工具的设计灵感来自OpenFst的Replace()
操作,该操作由其命令行工具fstreplace
执行,其基本思想通过用法消息加以说明:
Recursively replaces FST arcs with other FST(s).
Usage: fstreplace root.fst rootlabel [rule1.fst label1 ...] [out.fst]
下面是使用fstreplace
的一个非常简单的示例; 它只是将顶层FST中的olabel 5替换为6。
# (echo 0 1 0 5; echo 1 0) | fstcompile > top.fst
# (echo 0 1 0 6; echo 1 0) | fstcompile > x.fst
# fstreplace top.fst 1000 x.fst 5 | fstprint
0 1 0 0
1 2 0 6
2 3 0 0
3
这些工具的架构类似,因为在G.fst
级别上存在一些符号,最终将被其他FST取代。 大多数复杂性与处理语音上下文有关,这就是为什么我们不能只使用现有的Replace()
操作或其按需等效操作的原因。
我们的工具与fstreplace
的接口略有不同,是在我们的工具中,顶层FST(对应于fstreplace
的第一个参数)没有分配一个特定符号用于替换,因此不能被用来“替换”任何FST。
框架概述
为了解释它是如何工作的,我们将采用“联系人列表”场景方案说明,在该方案中,您要构建带有非终结符的大型语言模型,例如在其中输入#nonterm:contact_list
,然后在识别时迅速构建某种小型LM用来表示联系人列表(可能带有未见过的单词),然后编译该解码图。 “大图”和“小图”均已完全编译为HCLG级别。 GrammarFst代码在解码时将它们“缝合在一起”。完成此操作的方法是,在GrammarFst代码知道如何解释的两个HCLG中放置特殊的ilabel
。也就是说:HCLG中的大多数ilabel
对应于transition-ids,但是有值超过一千万的“特殊ilabel
”,GrammarFst代码知道如何解释它,并使用它们将FST缝合在一起,即与OpenFst的Replace()
操作有关,但由于需要正确处理语音上下文而变得更加复杂。 (它仅支持left-biphone
上下文,以保持可管控的复杂性)。
GrammarFst的接口与OpenFst的"Fst"类型非常相似-足够相似,以至于解码器可以将其用作常规FST的即插即用替代品-但它实际上并不继承自任何OpenFst类型。这是为了简化实现并给我们更多设计自由。使用GrammarFst的解码器以FST类型为模板,并且当我们希望使用GrammarFst进行解码时,将其用作模板参数。
GrammarFst代码中使用的StateId是64位的StateId,我们将其解释为一对32位的整数。高位是“ fst实例”,低位是该“ fst实例”中的状态。在联系人列表示例中,fst-instance
零将成为顶层解码图,并且在大语言模型中每次#nonterm:contact_list
非终止符出现时,都有一个分配新的fst-instance
编号1,2,...。但是,只有在实际访问图的那些部分时才按需生成这些内容。 GrammarFst是轻量级的对象,在启动时几乎不做任何工作。当我们不跨FST边界访问,而只是在单个FST内遍历时,它被设计为在“正常情况”下尽可能快。 GrammarFst代码需要一个快速评估的“信号”,它需要对特定的FST状态做一些特殊的事情。我们以最终概率为信号:也就是说,每次初始化ArcIterator
时,GrammarFst代码都会测试最终概率是否具有特殊值。如果它具有该特殊值(4096.0),则GrammarFst代码会做一些额外的工作,以查看是否需要扩展状态,并查找状态的先前扩展版本(如果找不到,则进行扩展)。通过“扩展”状态,我们的意思是计算离开它的弧的向量。
当我们打算支持语法时,FST的编译过程(即从G.fst到HCLG.fst的过程)有些不同。也就是说,我们需要扩展一些在编译中使用的工具,以便与我们引入的某些特殊符号一起正常使用。差异说明如下。
框架概述
此设置的顶层示例脚本在egs/mini_librispeech/s5
中;请参阅脚本local/grammar/simple_demo.sh
和local/grammar/extend_vocab_demo.sh
。在local/grammar/simple_demo_silprobs.sh
和local/grammar/extend_vocab_demo_silprobs.sh
中,这些脚本还存在使用静音概率的版本。 (实际上,脚本的silprob和no-silprob版本的工作流程完全相同;我们出于测试目的创建了这些不同的版本,因为这些演示脚本也帮助我们测试了代码的正确性)。
符号表和特殊符号
使用此架构时,我们将某些额外的符号添加到word.txt和phone.txt符号表中。这些额外的符号表示架构固有的某些特殊符号,以及用户定义的非终端符号。在以下示例中,用户定义的特殊符号是#nonterm:foo
和#nonterm:bar
。
tail words.txt
ZZZ 8431
#0 8432
#nonterm_begin 8434
#nonterm_end 8435
#nonterm:foo 8437
#nonterm:bar 8438
phone.txt包含更多符号:
tail phones.txt
Z_S 243
#0 244
#1 245
#2 246
#nonterm_bos 247
#nonterm_begin 248
#nonterm_end 249
#nonterm_reenter 250
#nonterm:foo 251
#nonterm:bar 252
用户永远不需要将这些符号显式添加到word.txt和phone.txt文件中;它们由utils/prepare_lang.sh
自动添加。用户要做的就是在“字典目录”(包含字典的目录,由validate_dict_dir.pl
验证)中创建文件"nonterminals.txt"。
C++代码从不直接与word.txt中的非终结符交互;这些操作都是在脚本级别完成的(例如,创建L.fst),而C++代码仅与phone.txt中的非终端符号进行交互。因此,如果您准备修改脚本或直接创建“ LG.fst”类型的解码图,则对word.txt中的符号没有特别强的约束。在phones.txt中,这些符号的顺序受到一些限制:在这种情况下,内置符号(不带冒号的符号)必须按照显示的顺序进行操作,用户定义的非终结符必须紧随其后,并且必须存在没有音素编号高于非终端相关符号(尽管允许使用编号更高的消歧符号)。
一些二进制文件接受一个选项–nonterm-phones-offset
,它告诉他们在哪里可以找到非终结符。该值应始终等于phone.txt中符号#nonterm_bos
的整数ID。在上面的示例中,将为–nonterm-phones-offset=247
。
G.fst中的特殊符号
如果使用此架构,则将创建多个图,因此可能会有G.fst(及其中间版本和完全编译版本)的多个副本。允许所有这些图都允许通过非终结符包含子图,这可以递归完成;如果完全编译的图无限大也是可以的,因为它仅按需扩展。
如果要包括一个特定的非终结符(例如#nonterm:foo
的符号),则必须在G.fst的输入端包括该符号#nonterm:foo
。至于您在输出端包含的内容:这取决于你自己,因为架构不在乎,但请记住,没有发音的符号可能会导致词图对齐问题。请更高级的用户注意:如果HCLG.fst中有任何不发音的输出符号,则程序lattice-align-words
将不起作用,但是替代解决方案lattice-align-words-lexicon
仍然可以工作,只要您在align_lexicon.int
为这些发音为空的单词添加条目;假设200007是发音为空的单词的整数id,则条目的格式为200007 200007
。脚本prepare_lang.sh
会为您添加这些条目。
对于不是顶层解码图的图,G.fst中的所有ilabel
序列均应以特殊符号#nonterm_begin
开头,并以#nonterm_end
结尾。这可以通过命令行中的fstconcat
来完成,也可以在创建图形时直接添加它们来完成。当我们输入编译后的HCLG.fst时,这些符号将在以后选择正确的语音上下文中调用。
对于某些应用程序,例如您要添加新词汇表的联系人列表方案,跳过创建G.fst并仅手动创建LG.fst可能会更容易。一旦您知道其预期的结构,这将不会很难。即使您不打算在生产中实际使用这些脚本,egs/mini_librispeech/s5/
中的示例脚本local/grammar/extend_vocab_demo.sh
也可能是一个很好的参考。
LG.fst中的特殊符号
在描述L.fst用特殊符号做什么之前,我们将陈述我们期望LG.fst在合成后将包含的内容。所有特殊符号都在LG.fst的ilabel
上。
让我们将"left-context phones"集定义为可以结束一个单词的音素集,再加上可选静音,再加上特殊符号#nonterm_bos
。当我们开始一个词时,这是一组音素可以作为left-context
出现,外加#nonterm_bos
作为没有音素出现过的序列开始上下文的替代。在使用left-context phones时,我们将用斜体,以强调其具有特殊含义。
仅适用于非顶层图表:
-
FST中的所有
ilabel
序列必须以#nonterm_begin
开头,然后是每个可能的left-context phones,即,平行弧枚举了可能在此非终结符之前的所有可能的语音left-contexts。在不依赖单词位置的系统中,我们可以让这个集合成为所有音素。在与单词位置相关的系统中,它可以是除字词内部音素和单词开头音素以外的所有音素,即除看起来像
XX_B
和XX_I
的音素以外的所有音素。如果已知可能的左上下文的集合较小,那么将其设为较小的集合可能会更有效。除真实音素外,我们在此集合中还包含#nonterm_bos
,它表示在语句开始时遇到的语音上下文。 所有ilabel序列必须以
#nonterm_end
结尾。
每当引用非终止符时,无论是从顶层还是非顶层图中,LG.fst中的ilabel
都将是例如#nonterm:foo
,其后是所有可能的left-context phones。这些left-context由L.fst添加。
L.fst中的特殊符号
本节说明了需要添加哪些包含L.fst中特殊符号的序列,以便使用G.fst所需的属性来编译LG.fst。我们下面描述的内容由utils/lang/make_lexicon_fst.py
和utils/lang/make_lexicon_fst_silprob.py
实现,并在您提供–left-context-phones
和–nonterminals
选项时被激活。当它在输入词典目录中看到文件nonterminals.txt
时,在prepare_lang.sh
中会自动调用。
令L.fst的循环状态为L.fst中具有很高出度的状态,所有单词都从该状态离开(并返回)。
词典除了正常内容外还需要包括:
一个序列从开始状态开始,在循环状态结束,由
olabel
#nonterm_begin
和ilabels
组成,其中#nonterm_begin
紧随其后是所有可能的left-context音素(和#nonterm_bos
)。从循环状态到最终状态的弧,其中
ilabel
和olabel
等于#nonterm_end
。对于每个用户定义的非终结符(例如
#nonterm:foo
)和#nonterm_begin
,一个循环以用户定义的非限定词开头的循环状态开始和结束,例如#nonterm:foo
,位于ilabel
和olabel
上,然后仅在ilabel上有所有的left-context-phones。
为了使LG.fst尽可能地概率化(即,在概率上尽可能“和为1”),当我们具有从所有离去弧中包含所有left-context phones的状态时,我们加上等于left-context phones数的对数值的损失。这将使我们能够在以后的解码图构建过程中推移权重,而不会引起对解码速度和准确性有害的奇怪影响。当解码图实际拼接在一起时,“所有可能的left-context phones”的所有替代路径中的一条除外,其他都会被禁止。到那时,我们将取消的损失。这发生在函数GrammarFst::CombineArcs()
中。
注意,上面的意思是,与用户定义的非终止符相对应的每个子图都将在非终止符之后允许可选的静音,在终止符之前不允许。这与从高层图调用非终止符的方式一致,并且在每对“真”字之间生成一个可选的静音,在顶层图的开头和结尾处生成一个可选的静音。我们在示例脚本(例如egs/mini_librispeech/s5/local/grammar/simple_demo.sh
)的末尾测试了这种等效性。如果用户打算在LG.fst级别手动构建这些子图而不是使用已提供的脚本,则用户应牢记所有这些。
L.fst中的特殊符号
在具有特定于单词的沉默概率的词典版本中(请参阅this paper的解释),实际上有两种状态的循环状态,一种用于沉默后,另一种用于非沉默后。使用“ silprobs”时,每个单词的开头和结尾都有一个特定于单词的成本,分别与从非静音和静默的过渡有关(其中“沉默”是指词典增加的可选静默,不是从更一般的意义上使手机静音)。
请参阅utils/lang/make_lexicon_fst_silprob.py
了解有关如何结合这些类型的图处理非终结符的详细信息。我们将仅在这里共享顶级概念,即:当我们为非终端输入HCLG.fst时,以及从非终端返回时,我们“知道”紧接在前电话的身份。 (这就是该框架的工作方式;如果您感到惊讶,请进一步阅读)。我们使用这些信息来实现“ silprob”的想法,而不必给FST提供额外的切入点。基本上,如果左上下文电话是可选静音电话,那么我们将进入L.fst中看到可选静音后的状态。在正常情况下,这将做正确的事情。在特定配置中,您没有使用与单词位置相关的电话(请参见prepare_lang.sh
的–position-dependent-phones
选项),并且词典中有一些单词以可选静音电话(例如SIL)结尾,这并不是一件正确的事,但是我们并不希望这种差异在任何实际使用案例中都特别重要。
CLG.fst中的特殊符号
首先,介绍一些背景知识:CLG.fst输入中的符号(即ilabel
)具有由ilabel_info
解释。有关 ilabel_info
对象的更多信息,请参见。消耗CLG.fst的程序也总是消耗ilabel_info
,它是vector
。对于特定的ilabel
,例如1536,ilabel_info[1536] = { 5, 21 }
是表示上下文相关音素的整数vector
。例如,上面信息代表音素21
的左上下文为5
。消歧符号也出现在CLG.fst的输入上,并且它们在ilabel_info
中以1维矢量表示,例如{-104}
,其中包含消歧符号的负数整数ID。
我们添加到CLG.fst输入中以支持语法解码架构的特殊符号始终对应于符号对,特别是对(#nontermXXX
, left-context phone),其中#nontermXXX
是任何符号#nonterm_begin
,#nonterm_end
,#nonterm_reenter
或用户定义的非终止符,例如#nonterm:foo
。这些特殊符号的ilabel-info
将是{-104,21}
之类的对,其中第一个元素是#nontermXXX
符号的负数,第二个元素是left-context phone。使用负数来区分这些ilabel_info
条目与常规的phones-in-context。
CLG.fst中的特殊符号如下。
以下特殊符号可能会出现在任何CLG解码图中,无论是否为顶层:
- 当任何图调用子图时,都会有一个带有
ilabel
的弧(#nonterm:foo
, left-context-phone)代表用户指定的非终结符和实际的left-context,随后是带有ilabels
的弧格式(#nonterm_reenter
, left-context-phone),用于所有left-context phone。
仅适用于非顶层CLG解码图:
这些图将从表示对的
ilabel
(#nonterm_begin
, left-context-phone)开始,表示所有潜在的left-contexts。它们将以
ilabel
(#nonterm_end
, left-context-phone)结尾,代表实际的left-contexts。
C.fst中的特殊符号
首先是背景。由于此架构仅支持left-biphone上下文,因此C.fst的状态对应于左上下文音素,并且状态转移上的ilabel
对应于Biphone(加上用于歧义符号的自循环)。
接下来,我们要完成的工作。 C.fst需要执行以下操作(描述如何将LG.fst中的序列更改为CLG.fst中的序列):
- 它需要将序列
#nonterm_begin
p1(其中p1是left-context-phone)变为代表该对的单一符号(#nonterm_begin
, p1)。 - 它需要将符号
#nonterm_end
更改为代表该符号对的单个符号(#nonterm_end
left-context-phone),其中left-context-phone表示当前的语音左上下文。 - 对于每个用户定义的非终止符,例如
#nonterm:foo
,它需要将序列#nonterm:foo
p1(其中p1是左上下文音素)更改为两个符号对,分别代表对(#nonterm:foo
, p0)和(#nonterm_renter
p1)分别。在此,p0代表符号#nonterm:foo
之前的音素。
为了实现上述内容,我们通过添加三个新状态来扩展C.fst的状态空间:
当
olabel
为#nonterm_begin
时,转移过去的一个状态当我们看到任何用户定义的符号
#nonterm:foo
时,转移过去的一个状态。一个状态是当
olabel
为#nonterm_end
时转移过去的。
为了避免更改主要的context-fst代码,我们在特殊的类fst:: InverseLeftBiphoneContextFst
中实现了此功能,该类实现了这些扩展,并且仅支持左biphone的情况。有关更多详细信息,请参见该代码(在grammar-context-fst.h
中搜索state space
)。
HCLG.fst中的特殊符号
HCLG.fst图中的特殊符号与CLG.fst图中的含义相同;但它们的整数形式是不同的。
首先,有一些背景。在CLG.fst的输入符号是由ilabel_info
表的索引。在HCLG.fst的输入处,这些符号通常表示transition-ids
–,也表示歧义符号,但在确定化之后将其删除。关键是HCLG.fst并未附带像ilabel_info
这样的表,该表为我们提供了符号的解释,因此我们需要使用一种编码,该编码允许我们将这两个整数合并为一个。
我们在HCLG.fst中选择特殊符号的表示形式,该表示形式避免与转移ID冲突,并且使解码符号以找到它们所表示的内容相对容易。一般情况下,将这个对(#nonterm:XXX
,left-context-phone)表示为:
当然nonterm_xxx
和left_context_phone
是phone.txt中的相应符号ID。实际上,代替上面的“ 1000”,我们使用最小的1000的倍数,该倍数大于 –nonterm-phones-offset
选项的值。这使我们能够处理大量音素,同时也易于阅读。
H.fst中的特殊符号
由于H.fst仅需要更改特殊符号的整数表示形式,而无需更改它们自身,因此这个更改非常简单。 H.fst有一个高出度的状态,我们将其称为循环状态。我们只需要在循环状态为ilabel_info
中引用的每个特殊符号添加一个自循环弧。因为整数编码不同,所以ilabel
和olabel
也不同。
解码器
当前使用语法解码的方法是将整个内容包装为FST,以便可以使用与以前相同的解码代码。也就是说,我们只是使用不同的FST来调用解码器。我们使用64位的状态ID,这样我们就可以让高位的32位对“ fst实例”进行编码,而低阶位对在该实例内的状态进行编码。第一个实例是在状态被访问时时动态创建的。实例0始终是“顶层” FST,当遇到带有“特殊符号”的弧时,我们会根据需要动态创建新的FST实例。
实际的解码器代码与常规解码器相同;我们只是将其模板化为其他FST类型:输入fst::GrammarFst
而不是fst::Fst
。fst::GrammarFst
类并不是继承自fst::Fst
类或支持其完整接口(实现起来非常复杂);它仅支持解码器实际需要的接口部分。
GrammarFst的ArcIterator
由于解码器的内部循环是弧上的循环,因此设计中最关键的部分可能是ArcIterator
代码。为了避免必须复制基础FST,对于“正常状态”(那些没有弧使它们进入其他FST实例或从其他FST实例返回的状态),ArcIterator
代码实际上指向基础FST的弧,当然有一个不同类型的“ nextstate”,是32位而不是64位。 ArcIterator
还会存储状态ID的高32位,该ID对应于“ fst实例” ID,并且每次调用其Next()
函数时,它都会创建指向的“当前弧”的新本地拷贝的,与底层弧不同,它具有64位的“下一个状态”。我们希望将弧复制到临时文件的开销大部分可以通过编译器优化来消除。 (实际上似乎是这样:GrammarFst解码的开销在-O0时约为15%,在-O2时约为5%)。
GrammarFst中的某些状态是“特殊”状态,因为它们具有使它们越过FST边界的弧。对于这些“特殊”状态,我们必须分别构造弧,并将此信息存储在类GrammarFst中的哈希中。
为了保持解码器代码的快速性和内存效率,每次访问状态时,我们都需要快速知道它是“特殊”状态还是正常状态。我们不想使用大数组来索引状态,因为每个GrammarFst对象都会占用过多的内存。相反,我们通过为GrammarFst缝合在一起的基础FST中的“特殊状态”赋予特殊的最终概率值来实现。 ArcIterator
代码测试最终成本是否具有此特殊值(4096.0),如果有,则知道它是“特殊状态”,并在哈希中查找它。如果不是,它只是在基础FST中从弧边数组的起点查找此状态。
为了避免在遍历弧时必须在ArcIterator
中进行额外的if语句评估,我们确保即使“扩展状态”也具有使用基础弧类型(fst::StdArc
)的弧向量具有32位状态ID。就像正常状态一样,目标FST的“ fst-instance”索引单独存储在ArcIterator
中。当然,这要求我们一定不能有带弧的状态,使它们过渡到多个FST实例。请参阅下一节以了解我们如何确保这一点。
准备用于语法解码的FST
GrammarFst代码对将其缝合在一起的FST有各种要求,其中一些要求已在上面提到。这些要求旨在帮助保持GrammarFst运行快速。函数fst::PrepareForGrammarFst
(由fst::GrammarFstPreparer
类内部实现)可确保满足这些前提条件。要求用户在实例化GrammarFst对象之前调用此准备代码,因此该准备被视为解码图构造的一部分;这样可以使代码运行时保持快速。如果标准解码图构建脚本utils/mkgraph.sh
检测到您正在使用此架构,则会自动(通过二进制程序make-grammar-fst
)调用此函数。
fst::PrepareForGrammarFst
的任务包括将FST状态的最终成本设置为4096.0(最终成为“特殊”状态),并对HCLG.fst进行各种小的更改,以确保其具有fst::GrammarFst
类所需的属性(例如,确保没有状态会转换到多个FST实例)。这些变化主要是通过插入epsilon弧来完成的。有关详细信息,请参见类fst::GrammarFstPreparer
的文档。
GrammarFsts中的输出标签
在我们提供的示例脚本中,因为我们只希望“实词”出现在HCLG.fst的输出侧,所以我们确保在G.fst的输出侧没有#nontermXXX
形式的特殊符号。但是,解码图编译框架确实允许您包含这些符号(如果需要)。这些在某些应用程序场景中可能很有用,在某些应用程序场景中,您想知道特定的单词范围已被解码为子语法的一部分。您唯一需要注意的是,如果您的单词发音为空,则程序lattice-align-words(及其底层代码)将无法工作。如果由于某种原因需要找到单词的准确时间对齐方式,可能会出现问题。在这种情况下,您应该使用备用程序lattice-align-words-lexicon(该程序读取文件lexicon.int给出词典中单词的发音),即使在这种情况下,该程序也可以正常工作。 prepare_lang.sh脚本已经在lexicon.int中以#nontermXXX
形式的符号放置了空发音条目,因此,如果使用提供的lang和graph目录,则单词对齐的lattice-align-words-lexicon
方法应该“正常工作”脚本。