假如我们有218monophone,然后现在要考虑上下文音素对发音的影响,这时候我们通常使用triphone。那么会有几个triphone呢?答案是218的3次方。如果不进行聚类,我们需要建立(218的3次方)*3个混合gmm模型(假设每个triphone有3个状态),计算量巨大,另一方面会引起数据稀疏。所以通常我们会根据数据特征对triphone的状态进行绑定。
常见的状态绑定方法有数据驱动聚类(Data-Driven Clustering)和决策树聚类(Tree-Based Clustering)。
数据驱动聚类的方式缺点引用HTK book中的表述:
One limitation of the data-driven clustering procedure described above is that it does not deal with triphones for which there are no examples in the training data. When building word-internal triphone systems, this problem can often be avoided by careful design of the training database but when building large vocabulary cross-word triphone systems unseen triphones are unavoidable.
所以现在基本上是使用决策树聚类的方式。
要建立一颗决策树,我们首先要有问题集。*在HTK中,问题集是我们人工自己定义的。而在kaldi中,问题集是通过训练数据自动生成的。
我们先引入一个概念:EventType,表示三音素的某个状态。比如三音素a/b/c的状态0就是一个EventType。为了方便说明,这里我们认为的使用这种表示方法:{a b c 0}。
一、先得到统计量:EventType的出现次数count_,特征向量的均值和均值的平方(至于为什么需要这些统计量,后面生成问题集的时候回解释)
在得到统计量之前,我们需要对每个句子进行维特比强制对齐(kaldi中的格式如下):
有了强制对齐结果,我们就可以得到每一帧是属于哪个EventType(上面的数字在kaldi中表示transition-id,根据transition-id可以得到三音素及其对应的状态)。然后我们就可以用这个EventType的所有帧对应的特征(比如MFCC)来得到这个EventType的统计量(出现次数count_,特征向量的均值和均值的平方)。
kaldi中统计量的结果保存在treeacc文件中,如下:
简单解释下这个图的格式:
BTS 35930表示训练集中出现过的三音素个数。
EV 4 -1 0 0 0 1 29 2 134就是一个EventType,4表示后面有4对pair。-1 0 0 0 1 29 2 134表示该三音素是0/29/134(这些数字是phone的id)的第1个hmm状态。
GCL 1表示该EventType出现的次数(对应count_变量); 0.01表示方差如果有小于0.01,则等于0.01。
接下来的一行 表示特征向量的均值
在接下来的一行 表示特征向量的均值的平方 图中0.577325就是-0.759819的平方
二、构造问题集
为了清楚地说明问题集的构建过程,我们假设现在有如下三音素及其hmm状态(假设特征是2维的,实际多用39维的mfcc;统计量:出现次数,均值和方差,也是假设的,不过不影响问题的说明):
{d a g 0} count:3 mean:4,3 square:16,9
{d a g 1} count:1 mean:2,3 square:4,9
{d a g 2} count:2 mean:4,4 square:16,16
{a b c 0} count:1 mean:1,2 square:1,4
{a b c 1} count:1 mean:1,3 square:1,9
{a b c 2} count:2 mean:1,4 square:1,16
{d b a 0} count:3 mean:2,1 square:4,1
{d b a 1} count:2 mean:3,1 square:9,1
{d b a 2} count:2 mean:2,1 square:4,1
{f b c 0} count:2 mean:1,3 square:1,9
{f b c 1} count:4 mean:1,4 square:1,16
{f b c 2} count:6 mean:1,5 square:1,25
{m c a 0} count:5 mean:3,1 square:9,1
{m c a 1} count:5 mean:3,2 square:9,4
{m c a 2} count:3 mean:3,3 square:9,9
{z c d 0} count:4 mean:3,4 square:9,16
{z c d 1} count:5 mean:3,5 square:9,25
{z c d 2} count:3 mean:3,6 square:16,36
{p d h 0} count:5 mean:4,6 square:9,36
{p d h 1} count:2 mean:3,5 square:9,25
{p d h 2} count:3 mean:3,6 square:9,36
{k e n 0} count:1 mean:5,2 square:25,4
{k e n 1} count:8 mean:5,3 square:25,9
{k e n 2} count:3 mean:5,5 square:25,25
{m f k 0} count:7 mean:4,8 square:16,64
{m f k 1} count:4 mean:4,4 square:16,16
{m f k 2} count:1 mean:7,4 square:49,16
{d a g 1} count:1 mean:4,3 square:16,9
{a b c 1} count:1 mean:1,3 square:1,9
{d b a 1} count:2 mean:2,1 square:4,1
{f b c 1} count:4 mean:1,4 square:1,16
{m c a 1} count:5 mean:3,2 square:9,4
{z c d 1} count:5 mean:3,5 square:9,25
{p d h 1} count:2 mean:3,6 square:9,36
{k e n 1} count:8 mean:5,3 square:25,9
{m f k 1} count:4 mean:4,4 square:16,16
中间音素a: {d a g 1} count:1 mean:4,3 square:16,9
中间音素b: {a b c 1} count:1 mean:1,3 square:1,9 {d b a 2} count:2 mean:2,1 square:4,1 {f b c 1} count:4 mean:1,4 square:1,16
中间音素c: {m c a 1} count:5 mean:3,2 square:9,4 {z c d 2} count:5 mean:3,5 square:9,25
中间音素d: {p d h 1} count:2 mean:3,6 square:9,36
中间音素e: {k e n 1} count:8 mean:5,3 square:25,9
中间音素f: {m f k 1} count:4 mean:4,4 square:16,16
中间音素a的统计量:count:1 mean:4,3 square:16,9
中间音素b的统计量:count:7 mean:4,8 square:6,26
中间音素c的统计量:count:10 mean:6,7 square:18,29
中间音素d的统计量:count:2 mean:3,6 square:9,36
中间音素e的统计量:count:8 mean:5,3 square:25,9
中间音素f的统计量:count:4 mean:4,4 square:16,16
a
b
c
d
e
f
那么我们最后得到的统计量还是:
中间音素a的统计量:count:1 mean:4,3 square:16,9
中间音素b的统计量:count:7 mean:4,8 square:6,26
中间音素c的统计量:count:10 mean:6,7 square:18,29
中间音素d的统计量:count:2 mean:3,6 square:9,36
中间音素e的统计量:count:8 mean:5,3 square:25,9
中间音素f的统计量:count:4 mean:4,4 square:16,16
kaldi中生成的问题集存储在questions.int文件中,格式如下:
上图中的一行就对应一个问题,也是一系列phone组成的集合。
-1 4 [ 0 ]
[ 0 1 ]
[ 0 1 2 ]
[ 0 1 2 3 ]
0 2
:-1:表示对hmm-state提问 ;0: 表示对左边的音素提问;1: 表示对中间的音素提问;2: 表示对右边的音素提问。
4: 对于hmm-state的提问,总共有4个问题。中括号[]的内容即代表问题。比如[0]就表示的“当前三音素的hmm状态是0吗?”这个问题;[ 0 1 ]表示的“当前三音素的hmm状态是0或1吗?”这个问题。
: 第一个数字代表num-iters-refine(Number of iters of refining questions at each node.) 第二个数字代表top_n(用k-means算法时,对于每一个point,计算离他最近的top_n个clusters)
有了问题集,怎么构建决策树呢?
同理,下面是对三音素的左音素进行提问(比如当前三音素的左边音素是{p,m,n}中的其中一个吗?):
0 438 [ 1 ]
[ 2 21 23 65 66 67 68 69 75 76 77 78 79 80 81 83 84 155 157 159 209 ]
[ 2 65 66 67 68 69 155 157 159 ]
[ 2 155 157 159 ]
[ 2 159 ]
… … …
下面对三音素的中间音素进行提问(比如当前三音素的中间音素是{a,b,c}中的其中一个吗?):
ps:中括号中的数字是音素的编号
1 438 [ 1 ]
[ 2 21 23 65 66 67 68 69 75 76 77 78 79 80 81 83 84 155 157 159 209 ]
[ 2 65 66 67 68 69 155 157 159 ]
[ 2 155 157 159 ]
[ 2 159 ]
… … …
下面对三音素的右音素进行提问(比如当前三音素的右边音素是{g,d,e}中的其中一个吗?):
2 438 [ 1 ]
[ 2 21 23 65 66 67 68 69 75 76 77 78 79 80 81 83 84 155 157 159 209 ]
[ 2 65 66 67 68 69 155 157 159 ]
[ 2 155 157 159 ]
[ 2 159 ]
… … …
kaldi中,对于三音素的位置进行提问时,可选的问题集都是一样的(如上图,都是438个问题),都是来自第5步得到的问题集。
截止到当前,我们已经得到了三音素不同位置以及hmm state对应的可选的问题集。接下来我们需要从这些问题集中选取一些比较”好“的问题,对每个音素的每个hmm state建立一颗属于他们自己的决策树,从而达到状态绑定的目的。
在建立决策数之前我们需要读取roots文件。该文件包含有两组关键词
第一组:shared or not-shared 表示对一个音素的3(或5)个hmm state分别建立3颗不同的决策树,还是所有hmm state共享一颗决策树树根。
第二组:split or not-split 表示是否需要对该音素对应的决策树进行分裂,如果不进行分裂,该决策树就只有一个树根。
一个典型的roots文件形式如下:
shared split 1
shared split 2
shared split 3
shared split 4
shared split 5
shared split 6
shared split 7
shared split 8
shared split 9
shared split 10
shared split 11
shared split 12
shared split 13
shared split 14
… … …
上面的数字是音素的编号。假设我们有218个音素,每个音素有3个hmm state。根据这个roots文件,那么我们就需要建立218颗决策树(而不是218*3,因为shared,即同一个音素的不同hmm state共享用一颗决策树)。
理论上我们需要建立218颗决策树,但我们把它放在一颗大的决策树下,如下图所示:
接下来需要对每一颗小决策树进行分裂,如下图所示:
实际上多个小决策树是同时进行分裂的,那么每次选取哪颗决策树的结点进行分裂呢?可以通过优先队列这样的数据结构进行调度,每次对似然度提升最大的结点进行进一步分裂。
构建完决策树后,我们还会将两两空间距离较近(如欧式距离)的叶子结点绑定在一起。最后生成的决策树如下:
这两张图其实是同一张,只是图太大了,分开截取。第一张主要展示的是决策树的前面几层,第二张主要展示的是决策树的后面几层。双圆圈就是叶子结点,对应一个混合高斯分布。
kaldi最后构建出来的树的结构如下:
ContextDependency 3 1 ToPdf TE 1 219 ( NULL SE -1 [ 0 1 ]
{ SE -1 [ 0 ] -1hmm-state [0]hmm-statepdfclass0
{ CE 0 CE 220 }
SE -1 [ 0 1 2 3 ]
{ SE -1 [ 0 1 2 ]
{ CE 218 CE 233 }
CE 219 }
}
SE -1 [ 0 1 ]
{ SE -1 [ 0 ]
{ SE 0 [ 144 149 ]
{ CE 969 SE 0 [ 29 30 123 145 217 ]
{ CE 969 SE 0 [ 7 59 124 ]
{ CE 969 SE 0 [ 7 28 31 57 58 59 117 124 138 144 149 218 ]
{ CE 1206 CE 1255 }
}
}
}
SE 2 [ 1 ]
{ CE 275 SE 0 [ 144 149 ]
{ CE 276 CE 701 }
}
}
说明:
ContextDependency 3 1表示音素宽度N_(=3)、中间音素位置P_(=1)
TE 1 219:TE表示决策树树根。1表示对中间音素进行划分,219表示划分完后总共有219个结点(实际上我们只有218个音素(1-218),但代码中计算的是0-218,所以总共有219个结点。NULL就是0对应的结点)
SE -1 [0 1]:SE表示决策树的非叶子结点,-1表示该结点是对hmm state进行提问;[0 1]表示提的问题是”该三音素的hmm state是0或1吗“。
CE 0:CE表示决策树的叶子结点 0表示该叶子结点的编号(在kaldi中叫pdf-id)
最后还是要强调下,上面流程实际上我们做了两次的决策树建立,要注意区分:
第一次:是为了得到问题集。通过将所有音素作为决策树根节点,然后计算对音素集进行划分带来的似然度提升,不断对结点进行分裂,进而得到一系列问题集;
第二次:是为了进行状态绑定。对所有中间音素以及hmm state相同的EventType,我们从第一步得到的问题集中选出对似然度提升最大的问题,对建立一颗决策树。然后比较所有叶子结点,把两两空间距离较近(如欧式距离)的叶子结点绑定在一起,有共同的pdf-id(混合高斯函数)。