轉自:http://blog.csdn.net/chenhoujiangsir/article/details/51613144
說明:本文是kaldi主頁相關內容的翻譯(http://kaldi-asr.org/doc/tree_externals.html)。目前網上已經有一個翻譯的版本,但翻譯的不是很清楚,導致我在剛學這部分內容的時候產生了一些誤解,所以我希望結合我目前所知道的一些東西,盡量把這部分內容翻譯地比較容易理解,但由於也是初學者,一些錯誤也是不可避免,希望大家發現后一起交流,以便我后期修正。好了,還是廢話少說吧。
介紹(Introduction)
本頁將對聲學決策樹在kaldi中如何被創建和使用,以及如何在訓練和解碼圖構建過程進行運用給出一個概述性的解釋。對於構建決策樹代碼的內部描述,請參見Decision tree internals;對於構建解碼圖方法的詳細信息,可以參見Decoding graph construction in Kaldi。
實現的基本算法就是自頂向下的貪婪分裂,通過問一些問題,比如說左邊的音素,右邊的音素,中心音素以及當前的狀態等等,我們會得到很多可以把數據進行分裂的路徑。我們實現的算法與標准算法非常相似,請參見Young,Odell和Woodland的這篇論文”Tree-based State Tying for High Accuracy Acoustic Modeling” 。假設我們對數據建模時采用單高斯將它們分成兩部分,在這個算法中,我們通過選擇局部最優的問題進行數據分裂,也就是使得似然值增加最大的那個問題。與標准算法實現不同的地方包括可以自由配置樹的根節點;對HMM狀態和中心音素相關問題提問的能力;以及實際上在Kaldi腳本中默認情況下,問題集是通過對數據自頂向下的二分聚類自動生成的,這就意味着不需要手動去創建問題集。關於樹的根節點的配置:可能是把一個共享的群組里面所有音素分裂的統計量,或者獨立的音素,或者每個音素的HMM狀態,作為樹的根節點來進行分裂,或者把音素組作為樹的根節點(注:多個音素作為一棵樹的根節點)。對於如何用標准的腳本配置根節點,請參見Data preparation。實際上,我們一般讓每棵樹的根節點都對應一個真實的音素(real phone),意思就是說我們把每個音素的詞位置相關、發音相關或者音調相關的所有變種都放進一個音素組,作為決策樹的根節點。
本頁下面主要給出相關代碼層面的一些詳細信息。
音素上下文窗(Phonetic context windows)
這里我們解釋一下在代碼中我們怎樣描述一個音素的上下文。一棵特殊的決策樹將有兩個整型值,分別描述的是上下文窗的寬度和中心位置。下表簡單說明了這兩個值:
N是上下文窗的寬度,P是設計的中心音素的標記。一般P就是窗的中心(因此叫中心位置);舉例說,當N=3時我們一般設P=1,但是我們也可以從0到N-1自由選擇;比如,P=2和N=3意味着有左上下文有兩個音素,並且沒有右上下文。在代碼中,當我們討論中心音素時,我們總是認為討論的是第P個音素,可能是也可能不是上下文窗中心的那個音素。
一個用來表示典型的triphone上下文窗的整型向量可能是:
//probably not valid C++ vector<int32> ctx_window = { 12, 15, 21 };
假設N=3和P=1,這個表示有一個右上下文21和一個左上下文12的音素15。我們處理端點位置上下文的一個方式是使用0(0不是一個合法的音素,因為在OpenFst中0是為空符號epsilon而保留的),所以比如:
vector<int32> ctx_window = { 12, 15, 0};
表示有一個左上下文12和沒有右上下文的音素15,因為音素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(注:輸出概率密度函數,比如高斯),否則它會使得所有的音素都是獨立的。
從一個扁平的初始(注:除了sil,所有的單音素模型都是一樣的)開始訓練一個單音素系統后,我們拿單音素對齊的結果和使用函數AccumulateTreeStats()(被acc-tree-stats調用)來累積訓練決策樹的統計量。這個程序不限於讀取單音素的對齊結果;它也能讀取上下文相關的對齊結果,因此我們也可以基於triphone對齊結果來構建樹。構建樹的統計量以BuildTreeStatsType類型(參見Statistics for building the tree)被寫到磁盤。函數AccumulateTreeStats()輸入N和P的值,N和P就是上文解釋過的上下文窗的大小和中心音素位置。命令行程序會默認地將N和P設為3和1,但是也可以使用–context-width和–central-position可選參數進行覆蓋。程序acc-tree-stats輸入一個上下文無關的音素列表(比如,silence),但是即使存在上下文無關的音素,這個也不是必需的;它只是減少統計量大小的一個機制。對於上下文無關的音素,程序將會累積一個沒有定義keys的相關的統計量,keys是跟左右音素對應的(注:在代碼中會把一個音素不同的上下文和pdf-class分別作為不同的key,然后累積每個key的統計量)(c.f. Event maps)。
當統計量被積累后,我們使用程序build-tree來構建樹。這個程序輸出一棵樹。程序build-tree需要三樣東西:
- 統計量(BuildTreeStatsType類型)
- 問題集配置(Questions類型)
- roots文件(參見下面)
統計量一般從程序acc-tree-stats得到;問題集配置類可以用程序compile-questions輸出,compile-questions輸入一個聲學問題集的拓撲列表(在我們的腳本中,這些都是自動地從構建樹的統計量通過程序cluster-phones得到)(注:cluster-phones輸入構建樹的統計量可以得到一個聲學問題集)。roots文件指定了將要在決策樹聚類過程中共享根節點的音素集,並且對每個音素集指出下面兩個東西:
- “shared”或者“not-shared”指出是每個pdf-class(也就是一般情況下的HMM狀態)都有不同的根節點,還是所有pdf-class共享一個根節點。如果是“shared”,對於所有的HMM狀態(比如在正常的HMM拓撲下所有的三個狀態)將只會有一個樹根節點;如果是“not-shared”,將會有三個樹根節點,每個pdf-class有一個。
- “split”或者“not-split”指出對於根節點要不要根據問題進行決策樹分裂(對於silence,我們一般不分裂)。如果該行指定“split”(正常情況),那么我們進行決策樹分裂。如果指定“not-split”,那么就不會進行分裂,因此根節點就被無分裂地保留。
下面將對這個怎樣使用方面做一些闡述:
- 如果我們指定“shared split”,即使所有的三個HMM狀態有一個根節點,不同的HMM狀態仍然可以到達不同的葉子節點,因為樹可以像對聲學上下文的問題提問一樣對pdf-class的問題提問。
- 對於roots文件中同一行出現的所有音素,我們總是讓它們共享根節點。如果你不想共享音素的根節點,你只要把它們放在不同的行。
下面是roots文件的一個例子;假設音素1是silence,並且其他的音素都有不同的根節點。
not-shared not-split 1 shared split 2 ... shared split 28
當我們有比如位置和聲調相關的音素時,將多個音素放在同一行會非常有用;這樣每個“真實的“音素將關聯到一個整數的音素ID集合。在這種情況下我們將particular underlying(注:這個不知道怎么翻譯)音素的所有變種版本共享一個根節點。下面是來自egs/wsj/s5腳本中Wall Street Journal的roots文件的一個例子(這個例子中音素是用文本表示的,而不是整數形式;但在被Kaldi讀取之前會被轉換成整數形式(注:就是會把音素映射成整數的ID)):
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
當創建這個roots文件時,你應該確保在每一行至少有一個音素是可見的(注:有對應的訓練樣本)。比如上面的情況,如果音素AY至少在聲調和詞位置的某些連接中可見,那就沒問題。
在這個例子中,對於slience等音素我們有很多的詞位置相關的變種。它們將共享它們的pdf’s,因為它們都在同一行,並且是“not-split”,但是它們可能會有不同的狀態轉移參數。實際上,silence的大多數變種都不可能用到,因為silence不可能出現在詞與詞之間;這只是為了防止以后有人做一些奇怪的事而不會過時。
我們用從之前創建的模型(比如,單音素模型)得到的對齊結果來對混合高斯參數進行初始化;對齊的結果會被程序convert-ali從一棵樹轉換到另一棵(注:應該就是說對齊的transition不變,但狀態綁定的參數可能因為決策樹的不同而變化)。
PDF標號(PDF identifiers)
PDF標號(pdf-id)是一個從0開始的數字,用做概率密度函數(p.d.f.)的序號。系統中每一個p.d.f.都有自己的pdf-id,並且是連續的(在一個LVCSR系統中一般會有幾千個)。在樹首先被構建時,它們就會被賦值。對於每一個pdf-id對應的是哪個音素,可能知道也可能不知道,這取決於樹是怎樣被構建的。
上下文相關對象(Context dependency objects)
ContextDependencyInterface對象是樹的一個虛基類,指定了如何與構建解碼圖代碼進行交互。這個接口只包含四個函數:
- ContextWidth()返回樹需要的N(上下文窗的大小)的值。
- CentralPosition()返回樹需要的P(窗中心位置)的值
- NumPdfs()返回樹定義的pdfs的數量;pdfs的編號從0到NumPdfs()-1。
-
Compute()是對某個特殊的上下文計算它對應的pdf-id的函數
ContextDependencyInterface::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,ContextDependency有少量更豐富的接口;唯一主要的添加就是函數GetPdfInfo,被用於TransitionModel類算出一個特殊的pdf可能對應哪些音素(這個函數的功能可以被 ContextDependencyInterface接口遍歷所有的上下文而實現)。
ContextDependency對象實際上是對EventMap對象的簡單組合封裝;請參見Decision tree internals。我們希望盡可能地隱藏樹的真正實現,使得以后需要重構代碼時變得非常簡單。
決策樹的一個例子(An example of a decision tree)
決策樹文件的格式不是以人們的可讀性為首要目標而創建的,但由於大家需要我們在這里試着解釋如何去解讀這個文件。請看下面的例子,這個是一個來自Wall Street Journal腳本中triphone的決策樹。
它以這個對象的名字ContextDependency開始(注:在代碼中整個樹是一個ContextDependency對象);然后是N(上下文窗的大小),這里是3;接着是P(上下文窗的中心位置),這里是1。
文件剩下的部分包含單個EventMap對象。EventMap是一個可能包含指向其他EventMap指針的多態類型。更多詳細信息,請參見Event maps。
這個文件表示一棵決策樹或多棵決策樹的集合,並將一個鍵值對集合(比如,left-phone=5, central-phone=10, right-phone=11, pdf-class=2(注:注意這里是四個鍵值對,表示一個中心音素是10,上文是音素5,下文是音素11的triphone的第2個狀態))映射到一個pdf-id(比如,158)。簡單來說,一個決策樹包含三種基本類型:一個是SplitEventMap(就像決策樹中的分支判斷),一個是ConstantEventMap(就像決策樹的葉子節點,只包含一個表示pdf-id的數字),和一個是TableEventMap(就像是一個包含其他EventMaps的一個查找表)。SplitEventMap和TableEventMap都有一個需要它們判斷的key,這個值可能是0,1或者2,分別表示左上下文音素,中心音素和右上下文音素,也可能是-1,表示pdf-class的標號(注:如果HMM的每個狀態都有對應的pdf,則pdf-class可理解為HMM的第幾個狀態)。一般情況,pdf-class的值與HMM狀態的序號是相同的,比如0,1或2。請嘗試不要因此而感到困惑:key是-1,value是0,1或2,但它們與上下文窗中音素的keys 0,1或2是沒有任何關系的(注:上下文窗中0,1和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是一個以key 1進行分裂的SplitEventMap,也就是按中心音素分裂。在方括號中是一系列連續范圍的phone-ids。然而,這些並不表示一個問題,它們只是音素分裂的一種方法,因此我們可以得到每個音素真正的決策樹(注:音素真正的決策樹是根據音素上下文和pdf-class進行決策的,對中心音素的決策只是為了找到這個音素對應的真正的決策樹)。問題在於這棵樹是通過“shared roots”方式創建的,所以有很多與同一音素不同詞位置和音調標識相關的phone-ids,它們都共享樹的根節點。在這種情況下在樹的頂層我們不能使用TableEventMap,否則我們就不得不將每棵樹重復好幾遍(因為EventMap是一棵純樹,而不是一個通用的圖,它沒有指針共享的機制)。
文件后面的一些“SE”標簽也是quasi-tree的一部分,它們都是首先按中心音素進行分裂(當我們順着文件往下看時我們進入了樹的更深處;注意這個花括號“{”一直是打開的,還沒有關閉)。然后我們看到字符串“TE -1 5 ( CE 0 CE 1 CE 2 CE 3 CE 4 ) ”,HMM狀態5進行分裂,表示通過TableEventMap對pdf-class -1進行分裂(實際上就是,HMM-position),並且返回從0到4的值。這5個值表示的是靜音和噪聲音素SIL,NSN和SPN的5個pdf-ids。在我們的設定中,這三個非語音音素的pdfs是共享的(只有轉移矩陣是不同的)。注意:對於這些音素我們用5狀態而不是3狀態的HMM,所以這里有5個不同的pdf-ids。接下來是“SE -1 [ 0 ] ”,這可以被認為是這棵樹中第一個真正的問題。是不是最左邊的HMM-state,我們可以從上面的SE問題看出這個問題被應用於中心音素為4到19時候,也就是音素AA的不同版本(注:原文寫的是5到19,不過我認為原文有問題,改成了4到19)。這個問題問的是pdf-class(key -1)是不是0(即是不是最左邊的HMM-state)。下一個問題是“SE 2 [ 220 221 222 223 ]”,問的是音素右上下文是不是音素“M”不同形式中的一個(這是一個非常有效的問題,因為我們是在最左邊的HMM-state);如果問題的答案是yes,我們繼續問“SE 0 [ 104 105 106 107… 286 287 ]”,這是一個關於音素左上下文的問題(注:原文寫的是右上下文,但應該是左上下文);如果答案是yes,則pdf-id就是5(“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 …”)。key 0是音素位置0,表示中心(並且只有這一個)音素,因為上下文窗大小(N)為1。TE的條目數量是49(音素的數量加1)。表中第一個EventMap是NULL,因為沒有序號為0的音素。下一個EventMap是一個有三個元素的TableEventMap,關聯到第一個音素的三個HMM狀態(技術上來說,是pdf-class):“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 ilabel_info object)
CLG圖(請參見Decoding graph construction in Kaldi)在它的輸入符號位置上有表示上下文相關音素的符號(輔助符號和可能的空符號也一樣)。在圖中它們總是用整型的標簽來表示。在代碼和文件名中,我們使用一個叫做ilable_info的對象。ilable_info對象跟ContextFst對象有很密切的聯系,請參見see The ContextFst object。就跟許多其他的Kaldi類型一樣,ilabel_info也是一個通用的(STL)類型,但是為了可以辨別出是ilabel_info,我們使用與之相同的變量名。就是下面定義的類型:
std::vector<std::vector<int32> > ilabel_info;
它是一個以FST輸入標簽為索引的vector,給每一個輸入標簽一個對應的音素上下文窗(參見上文,Phonetic context windows)。比如,假設符號1500是左上下文是12和右上下文是4的音素30,我們有:
// not valid C++ ilabel_info[1500] == { 4, 30, 12 };
在單音素的情況下,我們就會像這樣:
ilabel_info[30] == { 28 }
處理輔助符號會有點特殊(參見Disambiguation symbols或者上面引用的Springer Handbook文獻,該文獻解釋了這些輔助符號是什么)。如果一條ilabel_info記錄對應到一個輔助符號,我們就把輔助符號的符號表序號取負值放進去(注意這跟輔助符號打印形式#0,#1,#2等等里面的數字是不一樣的,它是跟這些輔助符號在符號表文件中的順序相關的數字,這個符號表文件在我們現在的腳本中叫做phones_disambig.txt)。比如,
ilabel_info[5] == { -42 }
意味着在HCLG中符號數5對應到整數id是42的輔助符號。為了編程方便我們對這些id取負號,因此解析ilable_info對象的程序不需要給一個輔助符號的列表就可以在單音素情況下將它們跟真實的音素進行區分。有兩個額外特殊情況:
ilabel_info[0] == { }; // epsilon ilabel_info[1] == { 0 }; // disambig symbol #-1; // we use symbol 1, but don't consider this h
ardwired.
第一個是正常的空符號,我們給它一個空的vector作為它的ilabel_info。這個符號一般不會出現在CLG的左邊(注:應該是說不會作為CLG的輸入符號)。第二個是一個特殊的輔助符號,打印形式叫做“#-1”。在epsilons被用做標准(Springer Handbook)腳本中C轉換器輸入符號的時候,我們使用輔助符號“#-1”。它可以確保有空音素表示的詞的CLG網絡可以被確定化。
程序fstmakecontextsyms可以創建一個與ilabel_info對象打印形式對應的符號表;這個主要用於調試和診斷錯誤。