解码图构建方法(实验部分)
这里我们一步一步地解释图的构建方法,以及与之相关的某些数据准备阶段。
这种方法的大部分细节不会硬编码到我们的工具中; 我们正在解释它目前是如何做的。 如果这个部分很混乱,那么最好的补救办法可能就是阅读Mohri等人的“Speech Recognition with Weighted Finite-State Transducers”。 要注意:这张纸是相当长的,阅读它至少需要几个小时,对于那些尚不熟悉FST的人。 另一个好的资源是OpenFst网站,它将提供更多相关信息比如符号表。
准备初始符号表
我们需要准备OpenFst符号表word.txt和phones.txt。 这些将整数id分配给系统中的所有单词和phone。 请注意,OpenFst对epsilon的保留符号为零。 示例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
word.txt文件包含单个消歧符号“#0”(用于G.fst输入上的epsilon)。 这是最后编号的单词。 如果您的词典包含单词“#0”,请小心。 phones.txt文件不包含消歧符号,但在创建L.fst之后,我们将创建一个文件phones_disambig.txt,该文件具有消歧符号(这对调试很有用)。
准备词典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
phone上的开始,结束和重音标记(例如T_E或AH0)是我们的WSJ方法特有的,就我们的工具包而言,它们被视为不同的phone(然而,对于这个设置我们做特殊处理;在树的构建过程中读取roots文件)。
请注意,我们允许有空语音表示的单词。 该词典将用于在训练中创建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的特殊消歧符号,但将被“passed through”L.fst通过自循环)。 文件phone_disambig.txt中间的一部分是:
ZH_E 338
ZH_S 339
#0 340
#1 341
#2 342
#3 343
数字是如此之大,因为在这个(WSJ)语聊中,我们向phone添加了重音和位置信息。 注意,用于空字(即<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是必要的,因为我们稍后将作compose。
词典的结构大致如人们期望的那样。 有一个状态(“循环状态”)是终止状态。 有一个起始状态有两个过渡到循环状态:一个有静默和一个没有。 从循环状态,对应于每个单词都有一个转换,该字是转换时的输出符号; 输入符号是该单词的第一个phone。 对于组合的效率和最小化的有效性,重要的是输出符号应尽可能早(即开始时不是单词的结尾)。 在每个单词的末尾,为了处理可选的静音,对应于最后一个phone的转换有两种形式:一种到循环状态,一种转换为“静默状态”,它们转换到循环状态。 我们不要在静音中放置可选的静音,我们将它定义为只有一个phone是静音phone的话。
用消歧符号创建词典只是稍微复杂一点。 问题是我们必须将自循环添加到词典中,以便可以通过词典传递来自G.fst的消歧符号#0。 我们使用程序fstaddselfloops(c.f.添加和删除消歧义符号)来执行此操作,尽管我们可以在脚本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命令行工具之一,它是我们自己的(我们有一些这样的程序)之一。
准备语言模型G
语言模型G大部分是一个接受者(即每个圆弧上的输入和输出符号相同),其中以字为其符号。 除了是消歧符号#0,它只出现在输入端。 假设输入是Arpa文件,我们使用Kaldi程序arpa2fst将其转换为FST。 程序arpa2fst输出带嵌入符号的FST。 在Kaldi,我们通常使用没有嵌入符号的FST(即我们使用单独的符号表)。 只需运行arpa2fst,我们必须采取的步骤如下:
我们必须从FST中删除嵌入的符号(并依赖于磁盘上的符号表)。
我们必须确保在语言模型中没有超出词汇量的单词
我们必须删除起始和结尾符号的“非法”序列,例如 <s>后跟</ s>,因为这些导致L o G不可确定。
我们必须用特殊的消歧符号#0替换输入端的epsilons。
实际脚本的略简化版本如下所示:
gunzip -c data_prep/lm.arpa.gz | \
arpa2fst --disambig-symbol=#0 \
--read-symbol-table=data/words.txt - data/G.fst
最后一个命令(fstisstochastic)是一个诊断步骤(请参阅Preserving stochasticity and testing it)。 在一个典型的例子中,它打印出数字:
9.14233e-05 -0.259833
第一个数字很小,所以它证实没有状态的弧的概率加上最终状态显着小于1。 第二个数字是重要的,这意味着有些状态具有“太多”的概率(FST中权重的数值通常可以解释为负对数概率)。 对于具有回退的语言模型的FST表示,有一些具有“太多”概率的状态是正常的。 在后续的构图步骤中,我们将验证这一非随机性并没有比开始时更糟糕。
所得到的FST G.fst当然只用于测试时间。 在训练时间内,我们使用从训练单词序列生成的线性FST,但这是在Kaldi程序中完成的,而不是在脚本级别。
准备LG
当用G组合L时,我们概括地遵循一个相当标准的方法,即我们计算min(det(L o G))。 命令行如下:
fsttablecompose data/L_disambig.fst data/G.fst | \
fstdeterminizestar --use-log=true | \
fstminimizeencoded | fstpushspecial | \
fstarcsort --sort-type=ilabel > somedir/LG.fst
这里OpenFst的算法有一些小的区别。 我们使用我们的命令行工具“fsttablecompose”实现的更有效的组合算法(参见Composition)。 我们的确定是一种也可以通过命令行程序fstdeterminizestar实现的算法,同时去空边。 选项-use-log = true要求程序首先将FST映射到log半环; 这保留随机性(在log半环); Preserving stochasticity and testing it。
我们通过程序“fstminimizeencoded”进行最小化。 这与OpenFst最小化算法的版本大体相同,适用于加权接收器; 唯一相关的变化是避免pushing weights,从而保持随机性(详见Minimization)。
程序“fstpushspecial”类似于OpenFst的“fstpush”程序,但如果权重不等于1,则可以确保所有状态“总计”为相同的值(可能与一不同),而不是尝试推送 “额外”权重到图表的开头或结尾。 这具有永远不会失败的优点(“Fstpush”可以失败或循环很长时间,如果FST“总和为”无穷大“); 它也快得多。 有关更详细的文档,请参阅push-special.cc。
“fstarcsort”阶段对弧进行排序以有助于稍后组合操作更快速。
准备CLG
为了获得输入为上下文相关phone的转换器,我们需要准备一个叫做CLG的FST,它与C o L o G相同,其中L和G是词典和语言模型,C代表语音上下文。 对于三音系统,C的输入符号将为a / b / c(即三元phone),输出符号将是单个phone(例如a或b或c)。 有关语音上下文窗口的更多信息,请参阅Phonetic context windows,以及我们如何产生为不同的上下文大小。 首先,我们将描述如何创建上下文FST C,如果我们自己做它并且正常组合(为了效率和可扩展性的原因,我们的脚本实际上不是这样工作)。
制作上下文转换器
在本节中,我们将解释如何将C作为独立的FST获得。
C的基本结构是它对所有可能的大小为N-1的phone窗口(c.f.语音上下文窗口,三音素中N = 3)的状态。 第一个状态,意思是开始的话语,只会对应于N-1个子句。 每个状态都有每个phone的转换(现在让我们忘记自循环)。 作为通用示例,状态a / b在输出上具有c的转换,并且输入上的a / b / c转换到状态b / c。 话语开始和结束时都有特殊情况。
在话语开始时,假设状态为<eps> / <eps>,输出符号为a。 通常,输入符号将是<eps> / <eps> / a。 但这并不代表phone,因为(假设P = 1),中心元素是<eps>,不是phone。 在这种情况下,我们让弧的输入符号为#-1,这是我们为此目的引入的特殊符号(我们在这里不使用epsilon作为标准方法,因为当有空字时可能会导致不可确定性)。
话语结束有点复杂。上下文FST在右侧(其输出端)具有在语句结尾处出现的特殊符号$。考虑三元phone的情况。在句子结束后,看到所有符号后,我们需要刷出最后一个三元phone(例如a / b / <eps>,其中<eps>表示未定义的上下文)。这样做的自然方法是在其输入上具有/ b / <eps>的转换,其输出上的<eps>,从状态a / b到最终状态(例如b / <eps>或a特殊最终状态)。但是,这对于组合来说是低效的,因为如果不是话语的结束,我们必须先探索这些转变,才能将它们修剪掉。相反,我们使用$作为终止符号,并确保它在LG的每个路径的末尾出现一次。然后我们用C的输出$替换<eps>。一般来说,$的重复次数等于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;}'
然后,我们可以使用以下命令创建C(但是,关于fstcomposecontext,请参见下文),因为实践中不执行此操作,因为效率低下)。
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需要phone列表,消歧符号列表和后续符号的标识。 除了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
This command knows about the "ilabels" format (The ilabel_info object). An example random path through the CLG fst (for Resource Management), printed out with this symbol table, is as follows:
该命令知道“ilabels”格式(The ilabel_info object)。 通过CLG fst(用于资源管理),示例随机路径与该符号表打印出来,如下所示:
## 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
动态组合C
在正常构图中,我们使用一个程序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
If we had different context parameters N and P than the defaults (3 and 1), we would supply extra options to this program. This program writes the file "ilabels" (see ) which interprets the input symbols of CLG.fst. The first few lines of an ilabels file from the recipe are as follows:
相比于默认值(3和1),如果我们有不同的上下文参数N和P,我们将为此程序提供额外的选项。 该程序写入解释CLG.fst的输入符号的文件“ilabels”(请参阅“The ilabel_info object”)。 Resource Management中的ilabels文件的前几行如下:
65028 [ ]
[ 0 ]
[ -49 ]
[ -50 ]
[ -51 ]
[ 0 1 0 ]
[ 0 1 1 ]
[ 0 1 2 ]
...
数字65028是文件中的元素数。 像[-49]这样的就是消歧符号; 像[0 1 2]这样的行表示声音上下文窗口; 前两个条目是用于epsilon(从未使用)的[],[0]是用于在C开头代替epsilon使用打印形式#-1的特殊消歧符号,以确保可确定性。
减少上下文相关输入符号的数量
在创建CLG.fst之后,有一个可选的构图阶段可以减小其大小。 我们使用从决策树和HMM拓扑信息中得出的程序make-ilabel-transducer,他是上下文相关phone的子集将对应于相同的编译图,因此可以合并(我们选择任何一个子集的元素,并且转换所有上下文窗口到那个窗口)。 这是一个类似于HTK逻辑到物理映射的概念。 命令是:
make-ilabel-transducer --write-disambig-syms=$dir/disambig_ilabels_remapped.list \
$dir/ilabels $tree $model $dir/ilabels.remapped > $dir/ilabel_map.fst
这个程序需要树和模型; 它输出一个名为“ilabels.remapped”的新的ilabel_info对象; 这与原始“ilabels”文件的格式相同,但行数较少。 FST“ilabel_map.fst”和CLG.fst做组合,并重新映射标签。 做到这一点后,我们确定化和最小化,所以我们可以马上实现任何尺寸的缩减:
fstcompose $dir/ilabel_map.fst $dir/CLG.fst | \
fstdeterminizestar --use-log=true | \
fstminimizeencoded > $dir/CLG2.fst
对于一般的设置,这个阶段实际上并没有减少图的大小(通常为5%到20%的减少),在任何情况下,它只是通过这种机制减少的中间构图阶段的大小。 但是,对于具有更广泛背景的系统,节省可能会变得重要。
制作H转换器
在传统的FST制作中中,H转换器是在其输出上具有上下文相关phone,并且在其输入上具有表示声学状态的符号。在我们的例子中,H(或HCLG)的输入上的符号不是声学状态(在我们的术语中是pdf-id),而是我们称之为transition-id的东西(参见Integer identifiers used by TransitionModel)。transition-id是pdf-id加上一些包括phone在内的其他信息进行编码。每个transition-id可以映射到pdf-id。我们创建的H转换器不对自环进行编码。这些稍后由单独的程序添加。 H转换器具有初始和最终状态,并且从该状态到每个条目都有一个转换,但ilabel_info对象中的第零个(the ilabels file,见上文)。上下文相关phone的转换转到相应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模型)中它完全没有影响; 请参阅Scaling of transition and acoustic probabilities以获得更多的解释。 除了FST之外,该程序还写入一个消歧符号列表,稍后必须删除。
制作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的版本,缩略符号集(“逻辑”三音素,HTK术语)。 我们删除消歧符号和任何easy-to-remove epsilons(请参阅Removing epsilons),在最小化之前; 我们的最小化算法是避免推送符号和权重(从而保留随机性),并接受非确定性输入(见Minimization)。
向HCLG添加自环
通过以下命令完成向HCLG添加自环:
add-self-loops --self-loop-scale=0.1 \
--reorder=true $model < $dir/HCLGa.fst > $dir/HCLG.fst
有关如何应用0.1 self-loop-scale的说明,请参见Scaling of transition and acoustic probabilities(注意它也影响非自环概率)。有关“重新排序”选项的说明,请参阅Reordering transitions; “重新排序”选项提高了解码速度,但与kaldi解码器不兼容。 add-self-loops程序不仅添加自环;它可能还必须复制状态并添加epsilon转换,以确保以一致的方式添加自环。在重新排序转换中,在Reordering transitions详述这个问题。这是唯一不保存随机性的构图阶段;它不保存它,因为自环的因子不是1,所以程序fstisstochastic应该给所有的G.fst,LG.fst,CLG.fst和HCLGa.fst相同的输出,但不是HCLG.fst 。在自环阶段之后我们不再确定了;这样会失败,因为我们已经删除了消歧符号。无论如何,这很慢,我们认为在这一点上确定和最小化没有什么进一步的。