kaldi002 -- kaldi是如何使用决策树的

来源:互联网 发布:淘宝店铺装修特效 编辑:程序博客网 时间:2024/06/06 18:07

Kaldi 是如何使用决策树的

 

介绍

本页概述了Kaldi中如何构建和使用语音决策树,以及如何与训练构建图相互作用。有关内部构造代码描述,请参见决策树内部结构;有关我们构建解码图的方法的更多细节,请参阅解释Kaldi中的解码图的构建

基本算法是自上而下的贪婪拆分来实现,我们可以通过询问左音素右音素中间音素,我们所在的状态等方式来分解数据等等。我们实现的算法类似于标准算法,参见YoungOdellWoodland基于树的状态绑定高精度声学建模一文。在该算法中,我们通过询问本地最优问题,即给出似然度增加的问题,将数据分开,假设我们通过单个高斯模型分割的每一侧的数据。与标准实现的区别包括如何配置树根的增加的灵活性;提出关于HMM状态和中心音素的问题的能力;并且事实上,默认情况下,Kaldi脚本中的问题是由数据的自顶向下的二聚类自动生成的,这意味着不需要提供手工生成的问题。关于树根的配置:可以将单个共享组中的所有音素的所有统计信息(包括关于中心音素HMM状态的问题)(或者具有单个音素)或HMM-音素状态,或者是单个分裂的树根,或者有一组音素是树根。有关如何使用标准脚本进行配置,请参阅数据准备。实际上,我们通常让每个树根对应一个真正的音素,这意味着我们将所有的单词位置相关,音调依赖或每个音素的压力相关版本组合成一个成为树根的组

 

这个页面的其余部分主要提供了代码级别的细节。

 

语音上下文窗口

这里我们解释我们如何在我们的代码中描述语音语境。 一个特定的树将有两个整数值来描述上下文窗口的宽度和中心位置。 下表总结了以下值:

代码中的名称      命令行参数中的名称    值(triphone  值(单声母)
Ñ                  -context-width=        3             1
P                 –central-position==    1             0

N是上下文窗口的宽度,P是指定的中心音素的标识。 通常P正好在窗口的中间(因此名称为中心位置;例如,N = 3,我们通常会有P = 1,但是你可以自由地选择从0N-1的任何值;例如,P = 2N = 3表示两个左上下文的音素,没有上下文。在代码中,当我们谈论中心音素时,我们总是意味着第P音素可能实际上也可能不是上下文窗口的中心音素

 

表示典型三音素上下文窗口的整数向量可能是:

 

vector <int32> ctx_window = {121521};

 

假设N = 3并且P = 1,这将代表具有21的右上下文和12的左上下文的音素15.我们处理结束的方式是使用零(这不是有效的音素,因为它在OpenFst中保留用于epsilon,意思是无符号),例如:

vector <int32> ctx_window = {12150};

意思是音素15,左上下文为12,没有右上下文,因为它是句子的结束。 特别是在话语结束时,以这种方式使用零可能有点意想不到,因为最后的音素实际上是后续符号“$”(参见制作上下文转换器),但是为了方便决策树代码我们不将后续符号放在这些上下文窗口中,我们置零。 注意,如果我们有N = 3P = 2,上述上下文窗口将是无效的,因为它的第P个元素将为零,这不是真正的音素;当然,如果我们有一个N = 1的树,上面的窗口都不会有效,因为它们是错误的大小。 在单音素的情况下,我们会有一个窗口:

vector <int32> ctx_window = {15};

所以单音素系统只是被视为一个特定情况的上下文相关系统,窗口大小为N,而一棵树不做任何事情。

 

建树过程

 

在本节中,我们将概述Kaldi的建树过程。

 

即使是单音素系统也有一个决策树,但是一个微不足道的一个; 请参阅MonophoneContextDependency()和MonophoneContextDependencyShared()函数,返回这些简单的树。 这些由命令行程序gmm-init-mono调用;它的主要输入是HmmTopology对象,它输出树,通常是作为ContextDependency类型的对象写入名为“tree”的文件,以及模型文件(模型文件包含一个TransitionModel对象和一个AmDiagGmm对象)。 如果程序gmm-init-mono接收到一个名为-shared-phones的选项,它将在指定的音素组之间共享pdf;否则会使所有音素分开。

 

从扁平的开始训练单音素系统后,我们采用单音素对齐,并使用函数AccumulateTreeStats()(从acc-tree-stats调用)来累积训练树的统计信息。该程序不限于阅读单音素对齐;它从上下文相关的对齐也起作用,所以我们可以基于三音素对齐。树构建的统计信息作为类型BuildTreeStatsType写入磁盘(请参阅构建树的统计信息)。函数AccumulateTreeStats()取值NP,如上一节所述;命令行程序将默认将它们设置为31,但可以使用-context-width-central-position选项覆盖它们。程序acc-tree-stats需要一个与上下文无关的音素列表(例如静音),但即使有上下文无关的音素,这也不是必需的。它只是减少统计数据的一种机制。对于与上下文无关的音调,程序将累积对应的统计信息,而不需要对应于定义的左和右音素的键(c.f.事件映射)。

 

统计数据累积后,我们使用程序build-tree构建树。 这将输出树。 程序构建树需要三件事:

 

统计(类型为BuildTreeStatsType

问题配置(类型问题集)

根文件(见下文)

 

统计数据通常来自程序acc-tree-stats;问题集配置类将由compile-questions程序输出,其中包含语音问题集的拓扑列表(在我们的脚本中,这些是由程序集群音素从建树统计过程中自动获得的)根文件指定手机在决策树聚类过程中共享根源,并针对每个音素设置以下两件事情:

 

共享不共享表示每个pdf-class(即经典情况下的HMM状态)是否应该有单独的根,或者是否应该共享根。如果它表示共享,则对于所有HMM状态(例如,在正常拓扑中的所有三种状态)将共享单个树根;如果不共享将有三个树根,每个pdf-class一个。

分裂不分裂表示决定树分裂是否应该实际上针对有问题的根(默认,我们通常不分裂)。如果说“split”(正常情况),那么我们做决策树拆分。如果它说不分裂,那么没有分裂,根不被分开。

 

以下将澄清如何工作:

 

如果我们说共享分裂,即使所有三个HMM状态都有一个根节点,不同的HMM状态仍然可以得到不同的叶子,因为树可以提出有关pdf-class问题以及语音上下文。

我们总是分享出现在根文件单行上的所有音素的根源。 这是不可配置的这些字符串,因为如果你不想共享音素的根,你可以把它们放在根文件的单独行。

 

下面是一个根文件的例子。 假设音素1是静音,所有其他音素都有独立的根。

 

not-shared not-split 1

shared split 2

shared split 3

...

shared split 28

 

当我们有一些像位置和压力相关的音素的东西,同一条线上的多个音素是最有用的;在这种情况下,每个真实音素将对应于一组整数音素索引。在这种情况下,我们为特定底层音素的所有版本共享根。下面是一个来自华尔街日报的根文件的示例,来自egs / wsj / s5脚本(这是文本,不是整数形式;在被Kalid读取之前必须转换为整数形式):

 

not-shared not-split SIL SIL_B SIL_E SIL_I SIL_S SPN SPN_B SPN_E SPN_I SPN_S NSN NSN_B NSN_E NSN_I NSN_S

shared split AA_B AA_E AA_I AA_S AA0_B AA0_E AA0_I AA0_S AA1_B AA1_E AA1_I AA1_S AA2_B AA2_E AA2_I AA2_S

shared split AE_B AE_E AE_I AE_S AE0_B AE0_E AE0_I AE0_S AE1_B AE1_E AE1_I AE1_S AE2_B AE2_E AE2_I AE2_S

shared split AH_B AH_E AH_I AH_S AH0_B AH0_E AH0_I AH0_S AH1_B AH1_E AH1_I AH1_S AH2_B AH2_E AH2_I AH2_S

shared split AO_B AO_E AO_I AO_S AO0_B AO0_E AO0_I AO0_S AO1_B AO1_E AO1_I AO1_S AO2_B AO2_E AO2_I AO2_S

shared split AW_B AW_E AW_I AW_S AW0_B AW0_E AW0_I AW0_S AW1_B AW1_E AW1_I AW1_S AW2_B AW2_E AW2_I AW2_S

shared split AY_B AY_E AY_I AY_S AY0_B AY0_E AY0_I AY0_S AY1_B AY1_E AY1_I AY1_S AY2_B AY2_E AY2_I AY2_S

shared split B_B B_E B_I B_S

shared split CH_B CH_E CH_I CH_S

shared split D_B D_E D_I D_S

 

创建根文件时,应确保每行上至少有一个音素。 例如,在这种情况下,如果音素AY至少在音调和字位置的组合中可见,那就可以了。

 

在这个例子中,我们有各种基于字位置的静音变体等等。 在这个例子中,他们将共享他们的pdf,因为它们在同一行,并且是不分裂” -但是它们可能具有不同的转换参数。 事实上,绝大多数这些静音的变体永远不会被用作静音,不会出现在单词之内;这是为了将来的设置,以防万一有人在未来发生做奇怪的事情。

 

我们从之前(例如单音素)构建的对齐来做高斯混合的初始化; 使用程序convert-ali将对齐从一棵树转换为另一棵树。

 

 

PDF标识符

 

PDF标识符(pdf-id)是从零开始的数字,用作概率分布函数(p.d.f.)的索引。 每个p.d.f.在系统中有自己的pdf-id,这些都是连续的(通常在LVCSR系统中有几千个)。 当树首次建成时,它们最初被分配。 对于每个pdf-id,它可能知道也可能不知道,它对应于哪个音素,取决于树是怎么构建的。

 

上下文相关对象

 

ContextDependencyInterface对象是树的虚基类,用于指定如何与图构建代码进行交互。 该接口只包含四个功能:

 

ContextWidth()返回树需要的N(上下文宽度)的值。

CentralPosition()返回树需要的P(中心位置)的值。

NumPdfs()返回由树定义的pdf数量;这些编号从零到NumPdfs()- 1

Compute()是为特定上下文(和pdf-class)计算pdf-id的函数。

 

ContextDependencyInterface :: Compute()Compute()函数声明如下:

class ContextDependencyInterface {

...

 virtual bool Compute(const std::vector<int32> &phoneseq, int32 pdf_class, int32 *pdf_id) const;

}

 

如果能够为此上下文和pdf-class计算pdf-id,则返回true。 返回值为false将指示某种错误或不匹配。 使用此功能的示例是:

 

ContextDependencyInterface *ctx_dep = ... ;

vector<int32> ctx_window = { 12, 15, 21 }; // not valid C++

int32 pdf_class = 1; // probably central state of 3-state HMM.

int32 pdf_id;

if(!ctx_dep->Compute(ctx_window, pdf_class, &pdf_id))

  KALDI_ERR << "Something went wrong!"

else

  KALDI_LOG << "Got pdf-id, it is " << pdf_id;

 

ContextDependencyInterface当前继承的唯一类是ContextDependency类,它具有更丰富的接口;唯一重要的补充是由TransitionModel类使用的函数GetPdfInfo来计算出特定pdf可能对应的哪个音素(只有通过枚举所有上下文才能模拟ContextDependencyInterface的接口)。

 

ContextDependency对象实际上是EventMap对象的相当薄的包装器;参见决策树内部结构。 我们希望尽可能地隐藏树的实际实现,以便在需要时稍后重构代码。

 

决策树的一个例子

 

决策树文件格式不是以人的可读性作为首要,而是由于大众的需求,我们将尝试解释如何解释该文件。看看下面的例子,这是华尔街日报语料中的一个三音素树。它以ContextDependency开头,它是对象的名称;那么N(上下文宽度),即3;那么P(上下文窗口的中心位置)为1,即音素上下文位置的中心,因为我们处于基于零的索引。该文件的其余部分包含一个EventMap对象。EventMap是一种多态类型,可能包含指向其他EventMap对象的指针。有关详细信息,请参阅事件映射它是从一组键值对(例如左音素= 5,中心音素= 10,右音素= 11pdf-class = 2)映射的决策树或决策树集合的表示,到一个pdf-id(例如158)。简而言之,它有三种类型:SplitEventMap(如决策树中的分割),ConstantEventMap(如决策树的叶,仅包含表示pdf-id的数字)和TableEventMap(如表查找包含其他EventMaps)。SplitEventMapTableEventMap都有一个,它们在这种情况下是012对应于左,中央或右上下文,或-1对应于“pdf-class”的标识。通常,pdf-class的值与HMM状态的索引相同,即012.尽量不要让它感到困惑:键为-1,但值为0,12,并且这没有连接到上下文窗口中的音素的键的0,12SplitEventMap有一组值将触发树的分支。下面是一种准BNF符号,它解释了树形文件格式。

 

 

EventMap := ConstantEventMap | SplitEventMap | TableEventMap | "NULL"

ConstantEventMap := "CE" <numeric pdf-id>

SplitEventMap := "SE" <key-to-split-on> "[" yes-value-list "]" "{" EventMap EventMap "}"

TableEventMap := "TE" <key-to-split-on> <table-size> "(" EventMapList ")

 

在下面的示例中,树的顶级EventMapSplitEventMapSE),它分割在键1上,这是中心音素。方括号是音素索引的连续范围。事实上,这些并不代表一个问题,而只是一种在音素上分裂的方法,所以我们可以得到每个音素的真实决策树。问题是这棵树是用共享根构建的,所以有不同的音素索引,对应于同一个音素的不同的字位置和音调标记的版本,共享根。我们不能在树的顶层使用TableEventMapTE),或者我们不得不重复每个决策树多次(因为EventMap是一个纯树,而不是一般的图,它没有指针的共享机制)。“SE”标签的另外几个例子也是这个准树的一部分,这个准树最初在中心音素上分裂(当我们进入这个文件时,我们将进一步深入树中;注意大括号“{”正在开放,但还没有关闭)。然后我们有字符串“TE -1 5CE 0 CE 1 CE 2 CE 3 CE 4,它表示在pdf-class“-1”(实际上是HMM位置)上用TableEventMap分割,并返回值04.这些值代表静音和噪声音素SILNSNSPN的五个pdf-id;在我们的设置中,这些三种非语音音素之间共享了这些pdf(只有转换矩阵是每个非语音音素都具体的)。注意:对于这些音素,我们有5状态而不是3HMM,因此有5种不同的pdf-ids。接下来是“SE -1 [0]”;这可以被认为是树中第一个真实的问题。我们可以从上面的SE问题中看出,当中心音素的值为419,它是音素AA的各种版本时,它适用。问题是询问pdf-class(键-1)是否具有值0(即最左边的HMM状态)。假设答案是,下一个问题是“SE 2 [220 221 222 223]”,这是询问音素是否是音素“M”的各种形式之一(一个相当不直观的问题,因为我们处于最左边的HMM状态);如果是的话,我们问“SE 0 [104 105 106 107 ... 286 287]”这是一个关于右边音素的问题。如果是,则pdf-id5“CE 5”),否则为696“CE 696”)。

 

s3# copy-tree --binary=false exp/tri1/tree - 2>/dev/null | head -100

ContextDependency 3 1 ToPdf SE 1 [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59\

 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 9\

3 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 1\

20 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 14\

5 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170\

 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 \

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 ]

{ SE 1 [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34\

 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 6\

8 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 10\

1 102 103 104 105 106 107 108 109 110 111 ]

{ SE 1 [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34\

 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 ]

{ SE 1 [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ]

{ SE 1 [ 1 2 3 ]

{ TE -1 5 ( CE 0 CE 1 CE 2 CE 3 CE 4 )

SE -1 [ 0 ]

{ SE 2 [ 220 221 222 223 ]

{ SE 0 [ 104 105 106 107 112 113 114 115 172 173 174 175 208 209 210 211 212 213 214 215 264 265 266 \

267 280 281 282 283 284 285 286 287 ]

{ CE 5 CE 696 }

SE 2 [ 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 132 \

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 248 249 250 251 252 253 254 255 256 257 2\

58 259 260 261 262 263 268 269 270 271 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 30\

3 ]

 

以下是一个更简单的例子:Resource Management 数据库中的单音素树。 顶级EventMap是一个TableEventMap“TE 0 49 ...”)。 由于上下文宽度(N)为1,所以键“0”是表示中心(且仅))音素位置为零。表中的条目数为49(在这种情况下,音素数量加1)。 表中的第一个EventMap(索引为零)为NULL,因为没有索引为零的音素。 下一个是具有三个元素的TableEventMap,对应于第一个音素的三个HMM状态(技术上,pdf-class):“TE -1 3CE 0 CE 1 CE 2)。

 

s3# copy-tree --binary=false exp/mono/tree - 2>/dev/null| head -5

ContextDependency 1 0 ToPdf TE 0 49 ( NULL TE -1 3 ( CE 0 CE 1 CE 2 )

TE -1 3 ( CE 3 CE 4 CE 5 )

TE -1 3 ( CE 6 CE 7 CE 8 )

TE -1 3 ( CE 9 CE 10 CE 11 )

TE -1 3 ( CE 12 CE 13 CE 14 )

 

 

ilabel_info对象

 

CLG图(参见Kaldi中的解码图构造)在其输入端具有表示上下文相关音素(以及消歧符号和可能的epsilon符号)的符号。 在图中,一如以往,这些由整数标签表示。 我们使用一个对象,在代码和文件名中,通常称为ilabel_infoilabel_info对象4ContextFst对象有很强的连接,请参阅ContextFst对象。 与许多其他Kaldi类型一样,ilabel_info是一种通用(STL)类型,但是我们使用一致的变量名称来标识它。 它是以下类型:

 

std::vector<std::vector<int32> > ilabel_info;

 

它是由FST输入标签索引的向量,为每个输入标签赋予相应的语音上下文窗口(见上文,语音上下文窗口)。 例如,假设符号1500是音素30,其右上下文为12,左上下文为4,我们将具有

 

ilabel_info[1500] == { 4, 30, 12 };

 

ilabel_info[30] == { 28 };

 

在单音素的情况下,我们会有以下形式:

 

ilabel_info [30] == {28};

有特殊情况来处理消歧符号(参见消歧符号或上面引用的Springer Handbook paper的解释)。 如果一个ilabel_info条目对应一个消歧符号,我们把它放在消歧符号的符号表项的负数(注意,这与#0, #1,#2等中消歧符号的打印形式的数量不同,它是一个符号表文件中对应的数字,在我们当前的脚本中称为phones_disambig.txt)。 例如,

 

ilabel_info[5] == { -42 };

 

将意味着HCLG上的符号5对应于整数id42的消歧符号。我们否定这些脚本的方便性,因此解释ilabel_info对象的程序不需要给出消歧符号列表,以便被 能够将它们与单音素情况下的真实音素区分开来。 还有两个额外的特殊情况:我们有

 

ilabel_info[0] == { }; // epsilon

ilabel_info[1] == { 0 }; // disambig symbol #-1;

// we use symbol 1, but don't consider this hardwired.

 

其中第一个是正常的epsilon符号,我们给它一个空的向量作为其ilabel_info条目。 这个符号通常不会出现在CLG的左边。 第二个是一个特殊的消歧符号,印刷形式叫做-1”。 我们在正常(Springer手册)配方中使用它在C转换器的输入上使用epsilons的方法;在有空语音表示的单词存在的情况下,需要确保CLG的确定性。

 

程序fstmakecontextsyms能够创建一个符号表,它对应于ilabel_info对象的打印形式;这主要用于调试和诊断。

 

你可以看到,ilabel_info对象并不是非常好用,因为它与消歧符号相关,但代码中没有很多部分与它进行紧密的交互:只有fst :: ContextFst类(和相关的东西;上下文扩展相关的类和函数),程序fstmakecontextsyms.cc,以及在类和函数中列出的一些功能,用于从HMM创建FST)。ContextDependency对象尤其仅仅看到代表音素上下文窗口的长度为N的有效序列。