构图和解码
基于HMM的语音识别模型实际上是在解码图中寻找最优路径。因此,要进行解码,需要先构建解码图:
# Graph compilation
utils/mkgraph.sh data/lang_test_tg exp/mono0a exp/mono0a/graph_tgpr
# Decoding
steps/decode.sh --nj 1 --cmd "$decode_cmd" \
exp/mono0a/graph_tgpr data/test_yesno exp/mono0a/decode_test_yesno
构图过程和语言模型联系紧密,因此在介绍构图前,先介绍N元文法语言模型。
N元文法语言模型
在实践中,经常简单地用单词在前文环境下出现的条件概率来建模语言知识,这种语言模型称为N元文法(N-gram),N元文法对N-1个单词的历史条件概率建模,即:
P
(
w
i
∣
w
i
−
(
N
−
1
)
⋯
w
i
−
2
w
i
−
1
)
P(w_i|w_{i-(N-1)}\cdots w_{i-2}w_{i-1})
P(wi∣wi−(N−1)⋯wi−2wi−1)
在Kaldi中,语言模型用ARPA格式保存,内容如下:
\data\
# x-gram对应的组合次数
ngram 1=200003
ngram 2=2451827
ngram 3=1134656
\1-grams:
# 每个词对的条件概率P和回退值(未出现词对的惩罚因子)
-2.348754 </s>
-2.752519 <UNK> -0.2779175
-99 <s> -1.070027
-2.619969 A -0.7371845
-7.211563 A''S
-6.221141 A'BODY -0.1187751
-6.583487 A'COURT
-6.240468 A'D -0.05992588
-7.108924 A'GHA
-6.260695 A'GOIN -0.2617707
-5.804425 A'LL -0.1488222
-5.638036 A'M -0.1254423
-6.221141 A'MIGHTY
...
\end\
加权有限状态转录机
Kaldi构图使用加权有限状态转录机(Weighted Finite-State Transducer,WFST)算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n2ONycaY-1642231496923)(etc/README/image-20220112171621829.png)]
如图所示,一个WFST由一组状态(State)和状态间的有向跳转(Transition)构成,其中每个跳转上保存三种信息,即input_label:output_label/weight
。如图a中从状态1向状态2跳转,输入输出都是data,权重是0.66。图b中从状态0向状态1跳转,输入是d,初始是data,权重是1。
WFST还具备一个起始状态(粗圈表示)和至少一个终止状态(双圈表示)。每个终止状态可以有一个终止权重,但在图中没有画出。WFST还需要定义两个二元操作+
,x
,这两个操作和权重集合应构成一个半环(Semiring)。根据半环类别不同,两种操作可被定义为各种运算。
Kaldi的WFST基于OpenFst,以C++ API和二进制可执行文件的形式提供了WSFT的表示和各种操作的实现。
如上图a中内容表示成OpenFst的描述语言为:
# example-a.fst.txt
0 1 using using 1
1 2 data data 0.66
1 3 intuition intuition 0.33
2 4 is is 0.5
2 4 are are 0.5
3 4 is is 1
4 5 better better 0.7
4 5 worse worse 0.3
5 1.0
除了最后一行外,每行代表一个跳转,最后一行表示终止状态5,终止权重为1.0。
OpenFst中输入和输出标签都要用数字表示,因此还需要定义一个标签文本到数字的映射表:
# symbols.txt
<eps> 0
using 1
data 2
intuition 3
is 4
are 5
better 6
worse 7
使用OpenFst编译工具可以将该WFST编译成二进制文件:
fstcompile --isymbols=symbols.txt --osymbols=symbols.txt example-a.fst.txt eample-a.fst
编译完成后,就可以使用OpenFst工具其进行各种操作。如使用fstinfo
查看信息,使用fstprint
打印成文本或者使用fstdraw
输出成Graphviz软件定义的图格式以便可视化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cNo9rWJG-1642231496926)(etc/README/example-a.png)]
用WFST表示语言模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p4c8EErF-1642231496934)(etc/README/image-20220113145821292.png)]
以二元文法(2-gram)为例,每个词只讲前一个词作为历史信息,假定词w2前面的词是w1,那么这个条目的语言模型可写为p(w2|w1)回退概率可写为 β \beta β(w1)。我们为w1和w2分别在图中简历一个状态,用跳转从w1指向w2,跳转的输入标签和输出标签均为w2,权重为p(w2|w1)。这样当序列存在序词对w1w2时,就可以匹配这个跳转。在图中还需建立一个回退状态b,用跳转从w1指向b,跳转权重为p(w2|w1)。该跳转用于匹配从w1出发找不到w2的情况,此时序列的语言概率用回退概率作为惩罚因子。如果经过了回退状态,继续跳转到w2时,就直接使用w2的一元概率p(w2)。
这样生成的WFST就是单词级语法(Word-level Grammar),称为“G”。经过这样展开,如果把概率换成概率的负对数,任意序列的语言模型负对数积累概率恰好等于图中某条路径的累积权重。
Kaldi中从语言模型构建G操作由local/format_lms.sh
完成,核心代码如下:
for lm_suffix in tgsmall tgmed; do
# tglarge is prepared by a separate command, called from run.sh; we don't
# want to compile G.fst for tglarge, as it takes a while.
test=${src_dir}_test_${lm_suffix}
mkdir -p $test
cp -r ${src_dir}/* $test
gunzip -c $lm_dir/lm_${lm_suffix}.arpa.gz | \
arpa2fst --disambig-symbol=#0 \
--read-symbol-table=$test/words.txt - $test/G.fst
utils/validate_lang.pl --skip-determinization-check $test || exit 1;
done
该脚本对tgsmall和tgmed两个不同规模的语言模型进行了G构建操作。对APRA格式语言模型文件解压后,直接输入到arpa2fst程序中,就得到了G.fst。
状态图构建
经典语音识别需要发音词典来获取每个单词的发音。词典会出给每个常见单词提供发音序列。有的单词在发音词典中找不到,这些词被称为集外词(OOV),一般需要人工补充或者使用预料训练的词转音素(G2P)算法自动预测单词发音。发音词典条目示例如下:
yes y eh s
am ae m
发音词典准备好后,需要把发音词典用WFST表示:对每个单词的发音条目,从初始状态引入一条路径,其输入标签序列为音素序列,输出标签序列为单词和若干用于填充的
ϵ
\epsilon
ϵ。以上条目对应的WFST为:
yes这个单词,路径输入标签序列为“y eh s”,即发音词典中的音素序列,其输出标签序列为“yes e e”,其中后面两个e只是为了保证输入标签序列和输出标签序列的长度一致,最后该路径返回初试状态。这个WFST由于和发音词典包含相同信息,故也被称为发音词典,简称L。
在Kaldi中,分别使用utils/lang/make_lexicon_fst.py
或utils/lang/make_lexicon_fst_silprob.py
构建不带静音概率和带静音概率的L。这两种脚本输出的都是文本形式的OpenFst格式,之后用fstcompile编译,并使用fstaddselfloops添加自跳转,用fstarcsort对生成的图按照输出标签做排序。
WFST定义了复合运算,把词图展开成因素图。我们用一个示例解释WFST的复合:
上图所示的WFST记为T1,它把小写字母abc转录为了相应的大写字母。
再看另一个T2,它把A转录为YES,把BC转录为NO。
如果把序列“a c b a”先通过T1转录,转录后再经过T2转录,那么得到得到序列是“YES NO NO YES”。一般说,如果任意输入序列被T转录结果和先后被T1和T2转录结果相同,则称T是T1和T2的复合,记为:
T
=
T
1
∘
T
2
T=T_1\circ T_2
T=T1∘T2
T的状态为T1和T2的状态对。如果记状态集合为Q,那么:
∀
(
q
1
,
q
2
)
∈
Q
→
q
1
∈
Q
1
,
q
2
∈
Q
2
\forall(q_1,q_2)\in Q\rightarrow q_1\in Q_1,q_2\in Q_2
∀(q1,q2)∈Q→q1∈Q1,q2∈Q2
复合后的结果如下所示:
图的复合有固定算法,由fstcompose
实现。
了解了WFST的复合运算,把此图按发音展开到音素级别,可表示为:
L
G
=
L
∘
G
LG=L\circ G
LG=L∘G
LG图把单音子序列转录成单词序列,如下图所示,a为G,b为L,c为LG:
除了G和L外,最终图中还要复合上下文转录机C,HMM拓扑结构H构成HCLG。HCLG是可以把HMM状态序列转录为单词序列的WFST,这样结合声学特征和声学模型,就可以进行解码了。
基于令牌传递的维特比搜索
构建了HCLG后,我们希望在图中找到一条最优路径,该路径上输出标签锁代表的HMM状态在待识别语音上的代价要尽可能小,这条路径上取出e后的输出标签序列就是单词级别识别结果,这个过程就是解码。
有时我们也希望找到最优的多条路径,这个识别结果被称为N-best列表。在HMM上解码的经典算法是维特比算法。维特比算法的朴素实现通常是简历一个TxS的矩阵,T为帧数,S为HMM状态综述。对声学特征按帧遍历,对于每一帧的每个状态,把前一帧各个状态的累积代价和当前帧在当前状态下的代价累加,选择使当前帧代价最低的前置状态作为当前路径的前置状态。在实现中,并不需要始终存储整个矩阵信息,而只保留当前帧以及上一帧的信息即可。
在语音识别中,更常见的是使用一种更灵活的算法来实现维特比算法,即令牌传递算法。该算法的基本思路是把令牌进行传递。 算法启动后,首先在所有起始状态上放置一个令牌,然后对于每一帧,所有令牌都沿跳转向前传递,把传递代价进行累积。如果一个状态有多个跳转,则把令牌复制多分,分别传递。这样传递到最后一帧,检查所有令牌的代价,选出一个最优令牌,就是搜索结果了。上述算法令牌个数随令牌复制剁成会指数级增长。考虑到维特比算法的一个主要思想是全局最优必然局部最优,即如果一条路径是全局最优的,那么该路径必然是其经过任意状态的局部最优路径。所以,当多个令牌传递到同一状态时,只保留最优的令牌即可。
以下是最简单的构建状态图和解码的代码:
# 构建状态图
utils/mkgraph.sh data/lang_nosp_test_tgsmall exp/tri1 exp/tri1/graph_nosp_tgsmall || exit 1
# 特征处理
data=data/test_clean
apply-cmvn --utt2spk=ark:$data/utt2spk scp:$data/cmvn.scp \
scp:$data/feats.scp ark:- | add-deltas ark:- ark:feats_cmvn_delta.ark || exit 1
# 解码
am=exp/tri1/final.mdl
hclg=exp/tri1/graph_nosp_tgsmall/HCLG.fst
gmm-decode-simple $am $hclg ark:feats_cmvn_delta.ark ark,t:result.txt || exit 1