解码图创建示例(测试阶段)

这里我们将解释怎样一步一步的建立我们正常的图模型,可能会涉及到一些数据准备阶段的知识。

这个方法的许多细节没有被编码到我们的工具里;我们仅仅解释现在我们是怎么做的。如果这部分你感到迷惑,最好的办法就是去读Mohri写的"Speech Recognition with Weighted Finite-State Transducers" 。警告你的是,这篇文章非常长,如果你对FST不熟悉可能需要花费几个小时。还有一个更好的资料就是OpenFst website ,可以提供像符号表的更多的介绍。

Preparing the initial symbol tables

我们现在准备OpenFst符号表words.txt和phones.txt。在我们的系统里,他们用整数id来分配所有的词和音素。注意OpenFst为静音保留符号0。对于WSJ任务的一个符号表的例子:

## head words.txt
<eps> 0
!SIL 1
<s> 2
</s> 3
<SPOKEN_NOISE> 4
<UNK> 5
<NOISE> 6
!EXCLAMATION-POINT 7
"CLOSE-QUOTE 8
## tail -2 words.txt
}RIGHT-BRACE 123683
#0 123684
## head data/phones.txt 
<eps> 0
SIL 1
SPN 2
NSN 3
AA 4
AA_B 5

Words.txt文件包含单个消歧符号"#0" (在G.fst的输入里用于静音)。在我们的脚本里这是最后数字的词。如果你的词典含有一个词"#0",你就要小心点。phones.txt 文件不包含消歧符号,但是当我们创建L.fst 后,我们将创建一个含有消歧符号的phones_disambig.txt 文件(在调试时这是非常有用的)。

Preparing the lexicon L

首先我们创建一个文本格式的词典,初始化时没有消歧符号。我们的C++ 工具将不会跟它交互,它仅仅在创建词典FST是被使用。我们WSJ词典的一小部分是:

## head data/lexicon.txt 
!SIL SIL
<s>
</s>
<SPOKEN_NOISE> SPN
<UNK> SPN
<NOISE> NSN
!EXCLAMATION-POINT EH2_B K S K L AH0 M EY1 SH AH0 N P OY2 N T_E
"CLOSE-QUOTE K_B L OW1 Z K W OW1 T_E

音素的开始,结束和应急标记物(e.g. T_E, or AH0) 在我们的WSJ脚本中是特定的,和至于我们的工具箱,它们被视为不同的音素(然而,对于这种设置,我们在树建立时特定的处理;可以看在The tree building process里的根文件)。

注意我们允许含有空音素表示的词。在训练时词典被用来创建L.fst(不含消歧符号)。我们也创建一个含消歧符号的词典,在图解码创建的时候使用。这个文件的一部分在这里:

# [from data/lexicon_disambig.txt]
!SIL    SIL
<s> #1
</s>    #2
<SPOKEN_NOISE>  SPN #3
<UNK>   SPN #4
<NOISE> NSN
...
{BRACE  B_B R EY1 S_E #4
{LEFT-BRACE L_B EH1 F T B R EY1 S_E #4

这个文件通过一个脚本创建;脚本的输出就是我们需要添加的消歧符号的数目,和这个用来创建符号表phones_disambig.txt。这个phones.txt一样,但是它也包含消歧符号#0, #1, #2等等的整数id (#0是从G.fst得到的一个特殊的消歧符号,但是将在L.fst由自环过滤掉)。 phones_disambig.txt文件中间的一部分是:

ZH_E 338
ZH_S 339
#0  340
#1  341
#2  342
#3  343

这个数字是非常大,因为在WSJ脚本中我们为音素添加stress and position信息。注意用在空词的消歧符号(比如<s></s>)与用在正常词的消歧符号是不一样的,所以在这个例子里正常消歧符号是从#3开始的。

把没有消歧符号的词典转换为FST的命令是:

scripts/make_lexicon_fst.pl data/lexicon.txt 0.5 SIL | \
  fstcompile --isymbols=data/phones.txt --osymbols=data/words.txt \
  --keep_isymbols=false --keep_osymbols=false | \
   fstarcsort --sort_type=olabel > data/L.fst

这里,脚本make_lexicon_fst.pl 创建一个文本格式的FST。这个0.5是在句子的开头的静音概率(比如:在句子的开头和在每个词后,我们输出一个概率为0.5的静音;所以分配给没有静音的概率就是1.0 - 0.5 = 0.5。这个例子里命令的其他部分与把FST转换为编译的形式有关;fstarcsort 是很有必要的,因为我们稍后将组合它们。

词典的结构大约跟我们期待的一样。有一个最终的状态(环状态) 。存在具有二个转移成环状态的起始状态,一个是静音,一个没有静音。从环状态存在对应每个词的一个转移,词是在转移时的输出符号;输入符号是这个词的第一个音素。对于有效的组合和最小化来说,最重要的就是输出符号尽可能的早(i.e.在词的开始而不是末尾)。在每个词的末尾,来处理可选的静音,对于最后音素对应的转移有二种形式:一种对环状态和一种它具有与环状态转移的 "silence state"。在静音词后,我们不必烦扰放置可选的静音,我们将定义这是一个静音音素的词。

创建一个带有消歧符号的词典是有点复杂的。问题就是我们不得不添加自环到词典里,以至于从G.fst 的消歧符号#0 可以被词典过滤。我们将通过程序fstaddselfloops来做(参考Adding and removing disambiguation symbols),尽管我们可以很容易通过脚本make_lexicon_fst.pl自动完成。

phone_disambig_symbol=`grep \#0 data/phones_disambig.txt | awk '{print $2}'`
word_disambig_symbol=`grep \#0 data/words.txt | awk '{print $2}'`

scripts/make_lexicon_fst.pl data/lexicon_disambig.txt 0.5 SIL  | \
   fstcompile --isymbols=data/phones_disambig.txt --osymbols=data/words.txt \
   --keep_isymbols=false --keep_osymbols=false |   \
   fstaddselfloops  "echo $phone_disambig_symbol |" "echo $word_disambig_symbol |" | \
   fstarcsort --sort_type=olabel > data/L_disambig.fst

程序fstaddselfloops不是原始的 OpenFst 的命令行工具,是我们自己的工具(我们有许多这样的程序)。

Preparing the grammar G

语法G 是把词作为它的符号的接收器最重要的部分(i.e.输入和输出符号是在每个弧相同)。特殊的就是消歧符号#0 仅仅出现在输入这边。假设我们的输入是一个Arpa 文件,我们用kaldi程序arpa2fst 来转为为FST。程序arpa2fst的输出是一个有嵌入符号的FST。在kaldi中,我们一般使用没有嵌入符号的FSTs (i.e. 我们用分离的符号表)。除了仅仅运行arpa2fst,我们还需要做的步骤如下:

  • 我们必须从FST中移除嵌入符号(和他们依赖于磁盘里的符号表)。
  • 我们必须确保语言模型里没有词典以外的词
  • 我们必须移除句子开始和结束读好的不合法的序列,比如 followed by ,因为这些导致 L o G 不是确定性。
  • 我们必须在输入时用特殊的符号#0代替静音。

做这个工作的实际脚本的稍微简化版本如下:

 gunzip -c data_prep/lm.arpa.gz | \
     scripts/find_arpa_oovs.pl data/words.txt  > data/oovs.txt

  gunzip -c data_prep/lm.arpa.gz | \
    grep -v '<s> <s>' | \
    grep -v '<s> </s>' | \
    grep -v '</s> </s>' | \
     ../src/lm/arpa2fst - | fstprint | \
    scripts/remove_oovs.pl data/oovs.txt | \
    scripts/eps2disambig.pl | \
     fstcompile --isymbols=data/words.txt --osymbols=data/words.txt \
     --keep_isymbols=false --keep_osymbols=false > data/G.fst
  fstisstochastic data/G.fst

最后一个命令(fstisstochastic)是一个诊断步骤(see Preserving stochasticity and testing it)。在一个典型的例子里,它将打印这些数字:

9.14233e-05 -0.259833

第一个数字是非常小的,所以它肯定在这个弧里没有一个状态的概率加上最后一个状态的概率小于1。第二次数字是有意义的,它意思就是存在有很大概率的状态(在FST里的权重的数值可以解释为log概率)。对于一个有反馈的语言模型的FST的表示,有大概率的一些状态是正常的。在后来的图建立步骤,我们将确认这些非随机性并没有比开始的时候变得更糟。

最后结果的FST G.fst 当然仅仅用在测试阶段。在训练阶段,我们使用从训练词序列得到的线性FST,但是这是在kaldi的内部做的,不是脚本做的。

Preparing LG

当组合L和G,我们坚持一个相对标准的脚本,比如我们计算min(det(L o G))。命令行如下:

    fsttablecompose data/L_disambig.fst data/G.fst | \
        fstdeterminizestar --use-log=true | \
        fstminimizeencoded  > somedir/LG.fst

这个与OpenFst's算法有些不一样。我们使用一个更加高效的组合算法(see Composition)的命令行工具"fsttablecompose"。我们的确定性也移除了静音的算法,有命令行fstdeterminizestar实现。选项–use-log=true是询问程序 是否先把FST投掷到log semiring;它保证随机性(在log semiring);可以看Preserving stochasticity and testing it。 我们通过程序"fstminimizeencoded"做最小化。这个大部分跟运用到有权重的接收器的OpenFst's的最小化算法一样。仅仅的改变就是避免pushing weights,因为保证随机性(更多的看Minimization)。

Preparing CLG

为了得到一个输入是上下文相关的音素的转换器,我们需要准备一个叫CLG的FST, 它等于 C o L o G,这里的L和G 是词典和语法,C表示音素上下文。对于一个三音素系统,C的输入符号是a/b/c形式的(i.e. 音素的三元组), 和输出符号是一个单音素(e.g. a or b or c)。关于音素上下文窗可以看Phonetic context windows,和如何用到不同的上下文大小里。首先,我们描述如何来创建上下文FST C,如果我们去通过自身来做和正常的组合(为了有效和可扩展性,我们的脚本事实上不那么去做)。

Making the context transducer

这部分我们来解释如何让获得作为一个独自的FST C。 C的基本结构是它由所有可能音素窗大小N-1的状态(c.f. Phonetic context windows; N=3在三音素的情况里)。在第一个状态,意味着句子的开始,仅仅对应N-1静音。每一个状态有每个音素的转移(现在让我们忘记自环)。作为一个通用的例子,状态a/b在输出时有一个c的转移和输入是a/b/c,到状态b/c。在句子的输入和输出是特殊的情况。

在句子的开始,假设状态是/ 和输出符号是a。通常,输入符号是//a。但是这个不代表一个音素(假设P = 1),中心元素是 ,它不是一个音素。在这种情况下,我们让弧的输入符号是一个特殊的符号#-1 ,这是我们介绍的主要原因。(作为标准的脚本,我们不使用静音这里,当有空词时会导致nondeterminizability)。

句子的结尾是有点复杂的。上下文FST在右边(它的输出边)会有一个在句子末尾的特殊的符号$。考虑三音素的情况。在句子的末尾,看完所有的符号后,我们需要过滤掉最后一个三音素(e.g. a/b/, where represents undefined context)。很自然的方式就是在输入是 a/b/ 和输出是之间有个传递,从状态a/b 到最后一个状态(e.g. b/或者一个特定的最终状态)。但是对于组合这不是有效的,因为如果它不是句子的末尾,我们不得不在删掉这些之前发现这些转移。我们在句子的末尾用符号$ 代替,和保证它出现在LG的每一条路径的末尾。然后我们在C的输出用$代填。一般而言,$的重复数目是N-P-1。为了避免计算出有多少后续的符号添加到LG的麻烦,我们仅仅允许它在句子的末尾接受任何数量这样的符号。可以通过函数AddSubsequentialLoop()和命令行程序fstaddsubsequentialloop得到。

如果我们想要它自己的C,我们首先需要消歧符号的列表;和我们需要计算一个没有使用的符号id,这些符号我们将用在后续的符号里,如下:

grep '#' data/phones_disambig.txt | awk '{print $2}' > $dir/disambig_phones.list
 subseq_sym=`tail -1 data/phones_disambig.txt | awk '{print $2+1;}'`

We could then create C with the following command:
 fstmakecontextfst --read-disambig-syms=$dir/disambig_phones.list \
 --write-disambig-syms=$dir/disambig_ilabels.list data/phones.txt $subseq_sym \
   $dir/ilabels | fstarcsort --sort_type=olabel > $dir/C.fst

程序fstmakecontextfst需要音素的一个列表,消歧符号的一个列表和后续符号的标识。除了C.fst,它写出了解释C.fst左边的符号的"ilabels"文件(看The ilabel_info object)。LG的组合可以按下面的做:

fstaddsubsequentialloop $subseq_sym $dir/LG.fst | \
 fsttablecompose $dir/C.fst - > $dir/CLG.fst

用于打印C.fst 和使用相同的索引为"ilabels"的符号, 我们可以使用下面的命令来做一个合适的符号表:

 fstmakecontextsyms data/phones.txt $dir/ilabels > $dir/context_syms.txt

这个命令要知道"ilabels"格式(The ilabel_info object)。CLG fst的随机一条路径(for Resource Management),打印它的符号表,接下来是:

## fstrandgen --select=log_prob $dir/CLG.fst | \
   fstprint --isymbols=$dir/context_syms.txt --osymbols=data/words.txt -
0   1   #-1 <eps>
1   2   <eps>/s/ax  SUPPLIES
2   3   s/ax/p  <eps>
3   4   ax/p/l  <eps>
4   5   p/l/ay  <eps>
5   6   l/ay/z  <eps>
6   7   ay/z/sil    <eps>
7   8   z/sil/<eps> <eps>
8

Composing with C dynamically

在正常的图建立脚本中,我们使用程序fstcomposecontext ,可以动态的创建需要的状态和C的弧,而不不需要浪费的建立。命令行是:

fstcomposecontext  --read-disambig-syms=$dir/disambig_phones.list \
                   --write-disambig-syms=$dir/disambig_ilabels.list \
                   $dir/ilabels < $dir/LG.fst >$dir/CLG.fst

如果我们有不同的上下文参数N和P ,相当于默认的(3 and 1)。我们将对这个程序使用其他的选项。这个程序写入文件"ilabels" (看The ilabel_info object) ,可以解释为CLG.fst的输入符号。一个rm数据库的ilabels 文件的开始几行是:

65028 [ ]
[ 0 ]
[ -49 ]
[ -50 ]
[ -51 ]
[ 0 1 0 ]
[ 0 1 1 ]
[ 0 1 2 ]
...

65028是文件里元素的个数。像[ -49 ] 行是为了消歧符号;像[ 0 1 2 ] 行代表声学上下文窗;一开始的2个entries是为了静音(从来不使用)的[ ]和and特定的消歧符号[0] ,它的打印格式为#-1 ,这是在C的开始,为了替代静音,为了确保确定性。

Reducing the number of context-dependent input symbols

当创建CLG.fst后,就有一个减小其大小的图建立选项。,我们使用程序make-ilabel-transducer,它可以形成决策树和得到HMM拓扑信息,上下文相关音素的子集相对于相同的图编译和将合并(我们选择每个自己的任意元素和把所有的上下文窗转为为这个上下文窗)。这个跟HTK's logical-to-physical mapping相似。命令行是:

 make-ilabel-transducer --write-disambig-syms=$dir/disambig_ilabels_remapped.list \
  $dir/ilabels $tree $model $dir/ilabels.remapped > $dir/ilabel_map.fst

这个程序需要tree和model;它的输入是一个新的ilabel_info 对象,称为"ilabels.remapped";这个跟原始的"ilabels"文件有相同的格式,但是有很少的行。FST "ilabel_map.fst" 是由CLG.fst 构成的和重新映射标签的。当我们做了这个后,我们将确定性和最小化,所以我们可以马上实现任意大小的减少:

 fstcompose $dir/ilabel_map.fst $dir/CLG.fst  | \
   fstdeterminizestar --use-log=true | \
   fstminimizeencoded > $dir/CLG2.fst

这个阶段为了典型的建立事实上我们不会减少图大小很多(一般减少5% to 20%),和在任何情况下,它是中间图建立阶段的大小,我们通过这个机制来减少。但是这个减少对宽上下文的系统是有意义的。

Making the H transducer

在传统的FST脚本里,H转换器是有它的上下相关音素的输出和输入是代表声学状态的符号。在我们的情况下,H (or HCLG)的输入的符号不是声学状态(在我们的术语里是pdf-id),但是我们用transition-id来代替(看Integer identifiers used by TransitionModel)。transition-id是对pdf-id加上包含音素的一些其他的信息的编码。每一个transition-id可以映射一个pdf-id。我们创建的H转换器没有把自环编码进来。他们稍后将通过一个单独的程序添加进来。H转换器 具有开始和最终的状态,和从这个状态的每个entry有一个转移除了在ilabel_info 对象(the ilabels file, see above)里的第0个。上下文相关音素的转移到相对应的HMM(缺少自环)的结构中,和然后到开始的状态。对于正常的拓扑结构,HMM的这些结构仅仅是三个弧的线性序列。每一个消歧符号(#-1, #0, #1, #2, #3 等等)的开始状态,H都有自环。 脚本的这个部分是生成H 转换器(我们称为Ha 因为这里缺少自环)是:

make-h-transducer --disambig-syms-out=$dir/disambig_tstate.list \
   --transition-scale=1.0  $dir/ilabels.remapped \
   $tree $model  > $dir/Ha.fst

这是一个设置转移尺度的选项;在我们现在训练的脚本里,这个尺度是1.0。这个尺度仅仅影响那些与自环概率无关的转移部分。和正常的拓扑(Bakis model)没有任何影响;更多的解释可以看Scaling of transition and acoustic probabilities。除了FST,程序也写消歧符号的列表,这些符号稍后将被删除。

Making HCLG

生成最后一个图HCLG的第一部就是生成缺少自环的HCLG。命令行是

  fsttablecompose $dir/Ha.fst $dir/CLG2.fst | \
   fstdeterminizestar --use-log=true | \
   fstrmsymbols $dir/disambig_tstate.list | \
   fstrmepslocal  | fstminimizeencoded > $dir/HCLGa.fst

这里,CLG2.fst是一个减少符号集版本的CLG ("logical" triphones, in HTK terminology)。我们删除了消歧符号和任何容易删除的静音(看Removing epsilons), 在最小化之前;我们最小化算法是避免pushing symbols and weights (因此保证随机性), 和接受非确定行的输入(看Minimization)。

Adding self-loops to HCLG

添加自环到HCLG 是用下面的命令完成的:

  add-self-loops --self-loop-scale=0.1 \
    --reorder=true $model < $dir/HCLGa.fst > $dir/HCLG.fst

怎么把self-loop-scale为0.1使用上的更多细节可以看Scaling of transition and acoustic probabilities(注意这会影响non-self-loop概率)。"reorder"选项的更多解释,可以看Reordering transitions;"reorder"选项增加了解码的速度,但是它与kaldi的解码不兼容。add-self-loops 程序不是仅仅添加自环,它也许会复制状态和添加静音转移来确保自环可以以一致的方式加入。这个事情的更多细节可以看Reordering transitions。这是图建立中唯一一步不需要保证随机性;它不需要保证它,是因为self-loop-scale不为1。所以程序fstisstochastic可以给所有的G.fst, LG.fst, CLG.fst和HCLGa.fst相同的输出,但是不能是HCLG.fst。在add-self-loops之后,我们不需要再做确定性;这个会无效,因为我们已经移除了消歧符号。所以,这个会很慢,和我们相信没有什么可以从确定性和最小化那里得到。