決策樹是如何在kaldi中建立的

20160804更新:

這裏有一個更好的版本:http://blog.csdn.net/chenhoujiangsir/article/details/51613144?from=singlemessage&isappinstalled=1


How decision trees are used in Kaldi

介紹

這部分將介紹音素決策樹在kaldi是如何建立和使用的,以及是如何將訓練和圖建立相交互的。對於決策樹的構建的代碼,可以看 Decision tree internals; 對於建立圖解碼的更多細節,可以看Decoding graph construction in Kaldi.

最基本的實現方法就是自頂向下貪婪的分裂,這裏我們又很多的方法來分裂我們的數據,比如我們可以用左音素、右音素、中間音素,我們所在的狀態等等。我們實現的方法與標準的方法很相似,具體的你可以去看YoungOdell 和Woodland的論文"Tree-based State Tying for High Accuracy Acoustic Modeling"。在這個方法裏,我們通過局部最優問題來分裂數據,比如假設我們通過一個單高斯來分裂我們需要建模的數據,通常我們會用最大似然函數的增加來作爲分裂的依據。與標準實現方法不同的有:對怎麼計算樹的根節點來添加flexibility;對hmm狀態和中間音素決策的能力;但是在kaldi腳本默認的是,決策是通過對數據自頂向下的自動實現二叉樹聚類,意思就是我們不需要提供手工產生的決策。不考慮樹的根節點的構造:它可能用在一個單共享組中的所有音素的所有統計量來進行分裂(包括中心音素和HMM狀態的問題), 或者用一個單一的音素,或者HMM狀態裏的音素,來作爲分裂的樹的根節點,或者音素組作爲樹的根節點。對於在標準的腳本里如何構建,你可以看 Data preparation。在實際中,我們讓每一個樹的根節點對應一個真正的音素,意思就是我們重組了所有all word-position-dependent, 每一個音素的tone-dependent 或者stress-dependent組成一個組來成爲樹的根節點。 

這頁的剩下部分大多數介紹代碼層的一些細節。

上下文相關音素窗(Phonetic context windows)

這裏我們將解釋在我們的代碼裏如何描述上下文相關音素。一個特定的樹含有兩個值,他們分別描述上下文音素窗的寬度和中心位置。下表是總結這些值:

Name in code

Name in command-line arguments

Value (triphone)

Value (monophone)

N

–context-width=?

3

1

P

–central-position=?

1

0

N代表上下文相關音素窗的寬度,P表示指定中心音素。正常地P是窗的中心(因此名字叫中心位置);舉個例子,當N=3,我們將有P=1,但是你可以自由的選擇0N-1中的任何值;比如,P=2 和N=3 表示有2個音素在左邊的上下文,而右邊沒有上下文。在代碼中,當我們討論中心音素時,我們的意思是第P個音素,也許是或者不是上下文相關音素窗的中心音素。 

一個整型的vector表示一個典型的三音素上下文窗也許就是: 

// probably not valid C++ 

vector<int32> ctx_window = { 12, 15, 21 };

假設N=3和P=1,這個就代表音素15有一個右邊的上下文21和左邊的上下文12。這種方式我們處理尾部時就用0(表示不是一個有效的音素,因爲在OpenFst裏的epsilon表示沒有符號),所以舉個例子:

vector<int32> ctx_window = { 12, 15, 0 };

表示音素15 有一個左上下文和沒有右上下文,因爲是一句話的結尾處。尤其在一個句子的結尾處,這種用0的方式也許有一點出乎意料,因爲最後一個音素事實上是後續符號"$" (看Making the context transducer),但是在決策樹代碼裏爲了方便,我們不把後續符號放在這些上下文窗中,我們直接給其賦0。注意如果我們有N=3和P=2,上述的上下文窗將無效,因爲第P個元素是0,它不是一個真正的音素;當然,如果我們用一個樹的N=1, 所有的窗都將無效,因爲他們是錯誤的大小。在單因素的情況下,我們有一個窗像:

vector<int32> ctx_window = { 15 };

所以單音素系統裏是上下文相關係統的一個特殊情況,窗的大小N=1和一個不做任何事情的樹。

樹建立的過程(The tree building process)

在這部分我們將介紹在kaldi中的樹建立的過程。

即使一個單因素系統有一個決策樹,但是也比較簡單。看函數MonophoneContextDependency() 和MonophoneContextDependencyShared() ,他們將返回這個簡單的樹。這個在命令行裏叫 gmm-init-mono; 他們主要的輸入是HmmTopology 類和它們的輸出是樹,通常作爲類 ContextDependency 寫到一個名字叫樹的文件(tree),和它們的模型文件(模型文件含有一個TransitionModel類和一個AmDiagGmm 類)。如果程序gmm-init-mono接受一個叫 –shared-phones的選項,它將在音素特定集中共享pdfs;否則將所有的音素分離。 

在訓練一個以flat start開始的單音素系統,我們訓練樹,採用單音素對齊和使用函數AccumulateTreeStats() (稱爲acc-tree-stats)來累積統計量。這個程序不僅僅在在單音素對齊中使用;也可以對上下文相關音素來對齊,所以我們可以根據比如三音素對齊來建立樹.爲樹建立的統計量將寫到disk,作爲類型 BuildTreeStatsType (看 Statistics for building the tree).函數AccumulateTreeStats() 採用值N和P,,像我們在之前的部分解釋那樣;命令行將默認的各自地設定它們爲31,但是這個可以用–context-width和–central-position選項來覆蓋。程序acc-tree-stats 採用上下文音素 (e.g. silence)的列表,但是如果是上下文音素,他們是不需要的。它僅僅是一個減少統計量的一個機制。對於上下文相關音素,程序將積累相對應的統計量 the corresponding statistics without the keys corresponding to the left and right phones defined (c.f. Event maps).

當統計量已經積累,我們將使用程序build-tree 來建立樹。輸出是一棵樹。程序build-tree需要三個東西:

· 統計量( BuildTreeStatsType類的)

· 問題的構建(Questions 類)

· 根文件(看下面)

統計量是通過程序acc-tree-stats得到的;問題構建類是通過問題構建程序得到的,這需要再音素問題集的一個topology表中 (在我們的腳本里,他們通常從程序cluster-phones的樹建立統計量自動獲得)。根文件指定音素集,這些音素集是在決策樹聚類處理中共享根節點,對於每一個音素需要設定2個東西:

· "shared" or "not-shared" 意思是對於每一個pdf-classes是否有單獨的根節點(例如:典型的情況,HMM的狀態), 或者根節點是否共享。如果我們打算去分裂(下面有個"split" 選項),我們執行時,根節點是共享的。

· "split" 或者 "not-split" 意思決策樹分裂是否應該通過根節點的問題來決定(對於silence, 我們一般不split)。

要小心,因爲符號有些棘手。在根文件裏的行"shared" 是表示我們是否在一個單一的樹的根節點上共享的所有的三個HMM狀態。但是我們經常共享所有音素的根節點,這些音素在一個根文件的單一的一行上。這個不是通過這些字符串來構建的,因爲如果你不想共享它們,你會把它們分佈在根文件的分離的行上。

下面是根文件的一個例子;這裏假設音素1silence和所有的其他有分離的根節點。

not-shared not-split 1

shared split 2

shared split 3

...

shared split 28

當我們有像position and stress-dependent 音素時,在同一行上有許多音素是非常有用的。這種情況下,每一個"real" 音素將對應整數音素ids的一個集合。在這種情況下,我們對一個特定標註的音素的所有情況將共享根節點(譯者注:意思應該就是一個音素可能有不同的標註版本,我們用的時候就相當於一個,具體的可以看下面的根文件)。下面是wsj數據庫的一個根文件,在egs/wsj/s5腳本里(這個在text裏,不是整數形式;它將經過kaldi轉爲整數形式):

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 應該至少在stress和word-position有一些組成,我們就認爲是可以的。

在這裏例子裏,我們有許多word-position-dependent 靜音的變異體等等。在這裏例子裏,他們將共享他們的pdf,因爲他們在同一行和是"not-split",但是他們有不同的轉移參數。事實上,靜音的大多數變異體從來不被用作詞間出現的靜音;這些都爲了未來做準備,以防止在未來有人做了一些奇怪的變化。

我們對初始階段的高斯混合是用之前(比如:單音素)的建立來做對齊;對齊通過程序convert-ali來把一個樹轉換成其他的。

PDF標識符 (identifiers)

PDF標識符(pdf-id)是一個數字,從0開始,被用作概率分佈函數(p.d.f.)的一個索引。在系統中的每一個p.d.f. 有自己的pdf-id,和他們是鄰近的(典型的就是在一個LVCSR 有上千上萬個)。當樹一開始建立的時候,他們就被初始化了。這有可能取決於樹是如何建立的,對於每一個pdf-id,都有一個音素與之對應。

上下文相關類(Context dependency objects)

ContextDependencyInterface 對於一個與圖建立相相互的特定書來說,是一個虛擬的基類。這種交互僅僅需要4個函數:

· ContextWidth() 返回樹所需要的N(context-width)的值。 

· CentralPosition() 返回樹所需要的P (central-position)值。

· NumPdfs() 返回由樹定義的pdfs的數量,他們的值是從0 NumPdfs()-1。

· Compute() 是計算一個特定的上下文 (和 pdf-class)的pdf-id的函數。

函數ContextDependencyInterface::Compute() Compute() 如下聲明:

class ContextDependencyInterface {

...

virtualboolCompute(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++

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

int32pdf_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,將輕微的豐富這種交互;唯一一個重要的添加就是函數GetPdfInfo ,它將在類TransitionModel 中計算一個特定的pdf對應某一個音素(通過枚舉所有的上下文,這個函數可以模擬特定的 ContextDependencyInterface的交互)。

對象ContextDependency 事實上是對象EventMap 的一點點的改動;可以看Decision tree internals. 我們想要盡最大可能地隱藏樹的真正的實現來使以後我們需要重組代碼的時候更加的簡單。

決策樹的一個例子(An example of a decision tree)

決策樹文件的格式不是以可讀性爲第一的標準創建的,而是由於大家的需要,我們將嘗試解釋如何讓翻譯這個文件。可以看下面的例子,是一個在wsj數據庫咯的一個三音素的決策樹。它由一個名字叫ContextDependency 對象開始的,然後N (the context-width)等於3P (the "central position" of the context window)等於1,比如這個音素上下文位置的中心,我們是從0開始編號的。文件的剩下部分包含一個單一的對象EventMap 。EventMap是一個多態類型,它包含指向其他EventMap 對象的指針。更多的細節可以看Event maps ;它是一個決策樹或者決策樹集合的表示,它把key-value 對的集合(比如:left-phone=5, central-phone=10, right-phone=11, pdf-class=2)映射成一個pdf-id (比如158)。簡單的說,它有三個類型:SplitEventMap (像決策樹裏的一個分裂),ConstantEventMap (l像決策樹裏的葉子,只包含一個數字,代表一個pdf-id),和TableEventMap(像含有其他的EventMaps表中查找)。SplitEventMap 和TableEventMap 都有一個他們查詢的"key",在這種情況下可以爲0, 1 or 2,與之對應的是left, central or right context,或者-1代表"pdf-class"的標識。通常pdf-class的值與HMM 狀態的索引值一樣,比如0, 1 or 2。嘗試不要因爲這樣而迷惑:key的值是-1,但是value是0, 1 or 2,和他們在上下文窗裏的音素的keys 0, 1 or 2 沒有任何聯繫。SplitEventMap 有一組值,這組值將觸發樹的"yes" 分支。下面是解釋樹文件格式的一種quasi-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 ")"

在下面的這個例子裏,樹的頂層EventMap 是一個分裂的key1,表示中心音素SplitEventMap (SE)。在方括號裏的是phone-ids的鄰近值。就像發生的那樣,他們不是代表一個問題,而僅僅是在音素上的分裂的方式,以至於我們可以得到真正的每個音素的決策樹。關鍵就是這個樹是通過"shared roots"來建立的,所以這裏有許多的phone-ids,對應相同音素的不同版本的word-position-and-stress-marked,他們共享樹節點。我們不可能在樹的頂層使用 TableEventMap (TE),或者我們不得不重複決策樹很多次 (因爲EventMap 是一個純樹,不是一個普通的圖,它沒有任何的機制被指向"shared")。"SE" label 的接下來的幾個例子也是這個"quasi-tree"的一部分,它一開始就用中心音素來分類 (當我們繼續往下看這個文件,我們將更加對這個樹深入;注意到括號"{" 是打開的而不是閉合的)。然後我們的字符串"TE -1 5 ( CE 0 CE 1 CE 2 CE 3 CE 4 )",表示在pdf-class "-1"(effectively, the HMM-position)用TableEventMap來分裂,和通過4返回值0。這個值代表對靜音和噪聲音素SIL, NSN and SPN的5pdf-ids;在我們的建立中,這個pdfs是在這些三個非語音音素(對於每一個非語音音素只有轉移矩陣是特定的)是共享的。注意:對於這些音素,我們有5個狀態而不是三個狀態的HMM,因此有5個不同的pdf-ids。接下來是"SE -1 [ 0 ]"; 和這些可以被認爲在樹中是第一個"real" 問題。當中心音素的值通過19得到5,我們可以從上面提供的看出SE問題,這個是音素AA的不同版本;問題就是pdf-class (key -1)的值是否爲0 (i.e. the leftmost HMM-state)。假如這個答案是"yes",接下來的問題就是"SE 2 [ 220 221 222 223 ]",這就問音素的右邊是不是音素"M"的各種形式(一個相對不直觀的問題被問,因爲他們是leftmost HMM-state);如果是yes,我們將問"SE 0 [ 104 105 106 107... 286 287 ]" 音素的右邊的一個問題,如果是yes,然後pdf-id就是5 ("CE 5") 和如果是no,就是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 93 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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170171 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 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 93 94 95 96 97 98 99 100 101 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 258 259 260 261 262 263 268 269 270 271 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 ]

下面是一個簡單的例子:從rm數據庫的一個單音素tree。頂層EventMap 是一個TableEventMap ("TE 0 49 ...")。key "0" 表示的就是中心音素,因爲context width (N) 爲1。在這個表中項的數量是 49 (這種情況下,音素的數量加1)。在表(索引爲0)中的第一個EventMap  是NULL,因爲這裏沒有索引爲0的音素。接下來是一個有三個元素的TableEventMap ,與第一個音素的三個HMM狀態 (technically, pdf-classes) 想對應:"TE -1 3 ( CE 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 ) 

The i label_info object

CLG圖(看Decoding graph construction in Kaldi) has symbols on its input side that represent context-dependent phones (as well as disambiguation symbols and possibly epsilon symbols)。在途中,這些被表示成整數型的標籤。我們在代碼中用一個對象,在文件名我們通常稱爲ilabel_info。ilabel_info 對象跟ContextFst 對象有很強的聯繫,可以看The ContextFst object。像kaldi中許多其他的類型一樣,ilabel_info是一個通用(STL)的類型,但是我們可以使用一致的變量名,以至於可以被識別出來。就是下面的類型: 

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

它是一個vector,是通過FST 輸入標籤來索引的,它可以給每一個輸入標籤相對應的音素上下文窗(看上面的Phonetic context windows)。舉例來說,假設符號1500是音素30,有一個12右邊的上下文和4的左邊的上下文,我們將有:

// not valid C++

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

In the monophone case, we would have things like:

ilabel_info[30] == { 28 };

這是對處理歧義符號的一個特殊處理(看Disambiguation symbols 或者Springer Handbook paper referenced above for an explanation of what these are)。如果一個ilabel_info entry 對應一個歧義符號,我們把它放在歧義符號的符號表的負部分(note that this is not the same as the number of the printed form of the disambiguation symbol as in #0, #1, #2 etc., 這些數字對應符號表文件的歧義符號,這個在我們現在的腳本中叫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符號,我們將給它一個空的vector作爲它的ilabel_info entry。這個符號不會出現CLG的左邊。第二個是一個特殊的歧義符號,它的印刷形式爲"#-1"。在正常的腳本里,我們使用它,這裏的epsilons被用作C transducer 的輸入;它是確保在空音素表示的詞裏對CLG確定性。

程序fstmakecontextsyms是可以創建一個與 ilabel_info對象的打印形式相對應的符號表;這主要用來調試和診斷。

就像你看到的那樣,ilabel_info對象不是很愉快,因爲它涉及到歧義符號,但是與它密切交互的代碼不是很多:僅僅fst::ContextFst 類(和一些相關的東西;可以看"Classes and functions related to context expansion"),程序 fstmakecontextsyms.cc,和一些在Classes and functions for creating FSTs from HMMs列出來的函數)。ContextDependency對象,特別地,僅僅可以看到代表音素上下文窗的長度N的有效的序列。

發佈了76 篇原創文章 · 獲贊 57 · 訪問量 76萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章