HTK解碼代碼分析(二)

HTK解碼總體流程:

首先在HVite.C的main函數中調用相應庫的函數。

HVite_main()
{
  解析HVite命令行;
  Initialise();
  net = ExpandWordNet(&netHeap,wdNet,&vocab,&hset);
  for(所有需要識別的MFCC文件)
{
  ProcessFile(datFN,net,n++,genBeam,FALSE);
  }
  釋放內存資源;
  
}

Initialise();

功能說明:從字典文件ditionary中解析字典信息並初始化相應的字典結構體Vocab。從網格文件net中解析網絡信息並
初始化相應的網格結構體Lattice。從HMM文件中解析HMM模型信息並初始化相應的模型結構體HMMSet。 

net = ExpandWordNet(&netHeap,wdNet,&vocab,&hset);

功能說明:整合字典,HMM模型和網格信息,將它們擴展爲可用於識別的網絡結構net。

ProcessFile(datFN,net,n++,genBeam,FALSE);

功能說明:解碼MFCC文件,並保存解碼的結果。
輸入說明:
datFN是需要解碼的MFCC文件指針,這個文件包含一幀一幀MFCC參數的觀察序列。
net是解碼網絡,這個網絡包含了所有可能的識別路徑。
n表示要解碼第幾個MFCC文件(這說明HTK可以解很多MFCC文件)。
genBeam是裁剪值。

ProcessFile()的主要代碼分析:

ProcessFile()
{
   
StartRecognitio(vri,net,lmScale,wordPen,prScale);

SetPruningLevels(vri,maxActive,currGenBeam,wordBeam,nBeam,tmBeam);

while(最後一幀觀察序列!=ture)
{   

ReadAsBuffer(pbuf,&obs);
   
ProcessObservation(vri,&obs,-1,xfInfo.inXForm);

}


lat=CompleteRecognition(vri,pbinfo.tgtSampRate/10000000.0,&ansHeap);

trans=TranscriptionFromLattice(&ansHeap,lat,nTrans);

LSave(lfn,trans,ofmt);
 
}

StartRecognitio(vri,net,lmScale,wordPen,prScale);

功能說明:初始化解碼環境。

SetPruningLevels(vri,maxActive,currGenBeam,wordBeam,nBeam,tmBeam);

功能說明:設置裁剪值。

ReadAsBuffer(pbuf,&obs);

功能說明:從MFCC文件中讀取一幀觀察序列。

ProcessObservation(vri,&obs,-1,xfInfo.inXForm);

功能說明:解碼每一幀MFCC參數的觀察序列。這個函數會被循環調用,直至到最後一幀觀察序列。
輸入說明:
vri是識別過程中保存信息的結構體。
&obs是順序讀取的每一幀MFCC參數的觀察序列。

lat=CompleteRecognition(vri,pbinfo.tgtSampRate/10000000.0,&ansHeap);

功能說明:通過追溯令牌中保存的path信息,生成識別結果網格。

trans=TranscriptionFromLattice(&ansHeap,lat,nTrans);

功能說明:把識別結果網格轉換成腳本。

LSave(lfn,trans,ofmt);

功能說明:把識別結果腳本保存到MLF文件中。

ProcessObservation()的主要代碼分析:

這個函數是HTK解碼的核心。

首先解釋兩個重要的概念。HTK的節點可分爲HMM模型節點和單詞節點,每個HMM模型節點都有一個後續的相應單詞節點。比如bit=b ih t,bit是單詞節點,b,ih和t都是HMM模型節點,b,ih和t都有後續的單詞節點bit。HTK定義了兩個特殊狀態,就是entry state和exit state。這兩個state並沒有觀察序列,但起着承上啓下的作用。節點的entry state能夠接收了最佳節點的最佳令牌,這樣可以保證每個HMM模型節點計算概率的初始條件是一樣的。節點的exit state的令牌概率表示整個節點的最佳輸出概率。

HTK採用了token pass令牌傳遞算法來實現語音識別。這個算法的根本思想還是基於viterbi維特比算法。它分成了兩個步驟,第一步是內部令牌傳遞,用viterbi算法計算出每個HMM模型節點在當前時刻t每個狀態的局部最佳概率,並傳遞給exit state。第二步是外部令牌傳遞。首先每個單詞節點會把entry state的token令牌傳遞給exit state,同時創建當前時刻的path路徑信息並保存在exit state的path鏈表中。接着節點會把exit state的令牌傳遞給概率比它小的後續節點entry state。注意,以後通過回溯path鏈表的成員項prev可以得出最佳單詞節點序列,即識別結果。所以,這個path鏈表的成員項prev是不容易被更新的,只有新的單詞出現時纔會被更新。

對於每個時刻的觀察序列,ProcessObservation()都會計算每個HMM模型節點的exit state概率,並把取得最大概率值的HMM模型節點的token傳遞給後續節點。但這個傳遞不能立即反映新單詞的出現。因爲新出現單詞的令牌概率不會立刻超越當前單詞的令牌概率。隨着多幀觀察序列被計算,新出現單詞的令牌概率會大於舊單詞的令牌概率,這時新單詞的信息會被插入到path鏈表的成員項prev中。

ProcessObservation()
{   

/* 內部令牌傳遞 StepInst1(inst->node);*/

for (識別路徑上的所有節點Node[i]實例)
{
if(節點==HMM模型節點)
{
計算當前時刻觀察序列obs對於HMM模型節點Node[i]的最大概率。
計算HMM模型節點Node[i]實例的exit state概率。
}
else if(節點==單詞節點) 
{清除單詞節點的entry state和exit state的概率值。}
 }
 

 /* 外部令牌傳遞 StepInst2(inst->node);*/

for (識別路徑上的所有節點Node[i]實例)
  if (節點概率值小於裁剪值) {
     刪除節點Node[i]實例。
  }
  else {
     if(節點==非空的單詞節點) 
  {
 
 節點Node[i]實例的exit state的token=節點Node[i]實例的entry state的token(即將單詞節點Node[i]實例的entry state的token令牌傳遞給exit state。)
 創建當前時刻的路徑信息path。
 節點Node[i]實例->exit->token.path=path。
 Node[i]實例->exit->token.path->prev=Node[i]實例->state->token.path。(這條指令將新單詞的path信息插入到path鏈表的成員項prev中。)
}
 for(節點Node[i]實例的後繼節點Node[j])
{
  if(節點Node[j]實例還沒生成)
   創建節點Node[j]實例
  if(節點Node[j]實例的entry state概率值<節點Node[i]實例的exit state概率值)
    節點Node[j]實例的entry state的token=節點Node[i]實例的exit state的token。
 }
  }
 }

一個簡單的yes-no識別例子:

任務語法限制如下:

 $WORD = YES | NO;   
( { SIL } < $WORD > { SIL } ) 

一共有三個HMM模型,分別是sil,yes和no。字典信息如下:

YES [yes] yes 
NO [no] no   
SIL [sil] sil

用HParse命令,轉換成網格信息如下(7個節點,12個連接):

N=7    L=12   
I=0    W=SIL                 
I=1    W=NO                  
I=2    W=!NULL               
I=3    W=YES                 
I=4    W=SIL                 
I=5    W=!NULL               
I=6    W=!NULL               
J=0     S=2    E=0    
J=1     S=2    E=1    
J=2     S=4    E=1    
J=3     S=6    E=1    
J=4     S=1    E=2    
J=5     S=3    E=2    
J=6     S=2    E=3    
J=7     S=4    E=3    
J=8     S=6    E=3    
J=9     S=6    E=4    
J=10    S=0    E=5    
J=11    S=2    E=5  

ProcessObservation()生成的節點實例網絡如下:

小寫的sil,yes和no是HMM模型節點,大寫的SIL,YES,NO和NULL是單詞節點。令牌傳遞過程如下:

sil的令牌從entry state一直傳遞給exit state,接着sil的exit state令牌傳遞給SIL的entry state。SIL的令牌會傳遞給yes和no的entry state。接着yes的exit state令牌會傳遞給單詞節點YES的entry state,no的exit state令牌會傳遞給單詞節點NO的entry state。YES和NO會將entry state令牌傳遞給自身的exit state,接着YES和NO會將各自exit state令牌傳遞給NULL1,但NULL1只會接受概率值最大的令牌。注意,單詞節點NULL不會創建路徑信息path,它僅僅起過渡作用,把前繼節點令牌傳遞給後繼節點。最後,NULL1會將exit state令牌分別傳給yes,no和sil。從這裏可以看出,yes會接受SIL和NULL的令牌,no會接受SIL和NULL的令牌,然後看SIL和NULL哪個令牌大就會接受哪個。

假設現在要識別的MFCC文件內容是SIL-NO-YES-SIL,識別結果如下:

"C:/HTK/htk/data/test/mfcc/yes_no_yes_0.rec"
 0 100000 s3 -156.115662 sil -3760.114014 SIL
 100000 4500000 s5 -3601.917969
 4500000 4600000 s2 -76.337799 sil -1485.466675 SIL
 4600000 6200000 s4 -1074.701172
 6200000 6600000 s5 -334.294281
 6600000 7400000 s2 -737.986572 no -2600.320801 NO
 7400000 8200000 s3 -684.429138
 8200000 8600000 s4 -340.492188
 8600000 9700000 s5 -837.412903
 9700000 11000000 s2 -924.753235 yes -7088.080566 YES
 11000000 12500000 s3 -1245.929077
 12500000 13500000 s4 -570.135193
 13500000 20000000 s5 -4347.263184
 20000000 21100000 s2 -877.634888 sil -2479.738770 SIL
 21100000 21900000 s3 -585.977783
 21900000 22200000 s4 -244.961655
 22200000 23300000 s5 -771.164551

現在看看HTK解碼是如何工作的。

一開始,HTK會用viterbi算法分別計算sil,yes和no的HMM模型的令牌輸出概率,這時sil的概率會最大。

隨着時間的轉移,新單詞NO出現了。這時sil的令牌概率會慢慢 變小,no的令牌概率會慢慢變大。當no的令牌概率超越sil時,no的令牌會傳遞給NULL1。新單詞NO(從第66幀開始)的path信息會被插入到path鏈表的成員項prev中。在單詞YES出現之前,根據令牌傳遞的順序,可以得出:

節點no實例->exit->token.path.frame=66
節點NO實例->entry->token.path.frame=66
節點NO實例->exit->token.path.frame=當前幀數
節點NO實例->exit->token.path->prev.frame=66
節點NULL實例->exit->token.path.frame=當前幀數
節點NULL實例->exit->token.path->prev.frame=66
節點no實例->entry->token.path.frame=當前幀數
節點no實例->entry->token.path->prev.frame=66
節點yes實例->entry->token.path.frame=當前幀數
節點yes實例->entry->token.path->prev.frame=66

當新單詞YES(從第97幀開始)出現後,上面的節點yes實例->entry->token會從entry state一直傳遞到exit state。一旦 yes令牌概率大於no令牌概率,則有:

節點yes實例->exit->token.path.frame=97
節點yes實例->exit->token.path->prev.frame=66
節點YES實例->entry->token.path.frame=97
節點YES實例->entry->token.path->prev.frame=66
節點YES實例->exit->token.path.frame=當前幀數
節點YES實例->exit->token.path->prev.frame=97
節點YES實例->exit->token.path->prev->prev.frame=66
節點NULL實例->exit->token.path.frame=當前幀數
節點NULL實例->exit->token.path->prev.frame=97
節點NULL實例->exit->token.path->prev->prev.frame=66

看到了嗎?prev增加了一個表項,保存了新單詞YES的path信息。

現在讓我們看下令牌是如何從HMM模型節點的entry state傳遞到exit state:

上圖的橫軸表示時間幀,縱軸表示HMM模型的每個狀態。這個圖很形象地表明,每個狀態在每個時間幀都有一個最大令牌概率(把這些概率排列,從數學上看就是一個矩陣)。viterbi算法只會取出同一時間幀上得出概率最大的狀態令牌往後傳遞。所以,entry state的令牌是不能輕易地傳到exit state的。只有待識別序列找到了合適的HMM模型,entry state令牌會自然地傳遞到exit state。

現附上HTK BOOK識別部分的中文翻譯,這能讓我們更好地理解代碼。其實HTK BOOK已經把token pass思想大概說出來了,但如果不配合代碼一起閱讀,肯定是雲裏霧裏。

網絡上關於HTK BOOK識別部分的中文翻譯(轉載):

第一章 HTK基礎

1.5 識別和Viterbi解碼

這裏介紹Viterbi算法。 識別,基於最大似然的狀態序列,這種方法可以很好地用於連續語音,如果使用總概率就很難做到。這個概率的計算,本質上和前向概率的計算一樣,只不過,前向概率在每一步都是求和操作,這裏每一步都是一次最大化操作。 對於給定的模型M,假定表示給定模型M,觀察到向量o1到ot,並且在時刻t處於狀態j的概率最大,就是這個最大概率。這個概率可以使用下面的迭代公式計算。

其中

那麼,對於模型M,觀察的數據O的最大似然概率則是

這種形式的迭代就是Viterbi算法的基礎。如下圖所示,這個算法可以看做是尋找最佳路徑的過程。矩陣中的Y軸表示模型的各狀態,X軸表示語音幀(即時間)。圖中的每個原點表示在時間t處觀察到該語音幀的概率,點之間的弧表示一個轉移概率。

那麼,任何路徑的概率都可以通過簡單地將路徑上的各點和弧的概率相乘(log運算則爲相加)得到。路徑從左向右,逐列發展。在時間點t,每個路徑的對所有的狀態i都是已知的,那麼就可以使用上面的公式來向右擴展一個時間幀,路徑增長一步。

“路徑”的概念非常重要,下面會將它通用化,用於連續語音識別。 沒有HTK工具直接實現了上面的Viterbi算法。但有一個HVite工具,以及它的支撐庫HRec和HNet,用於連續語音識別。

1.6 連續詞語音識別

現在回到圖1.1所示意的語音識別模型,可以清楚地知道,連續語音識別僅僅需要將多個HMM連接起來,而這個連接而成的HMM模型序列中的每個模型,都對應了其隱藏的符號,這個符號可能是一個單詞,那麼這稱爲“連接詞語音識別”,這個符號也可能是一個音素,那麼這稱爲“連續語音識別”。另外,在每個模型中包括首尾兩個不可觀察的狀態的原因,現在應該也清楚了,這是多個HMM模型連接在一起的粘合劑。

然而,依然有一些難點。由孤立詞過渡到連續詞,對於模型訓練算法Baum-Welch算法來說,所作的修改很小,只需要把所有模型連接成一個大模型,然後使用HERest的所謂“嵌入式”訓練即可,原理和過程和HRest中類似。

然而,對於Viterbi識別算法來說,需要進行重大的擴展,這也是HVite中所做的。在HTK中,使用了Viterbi算法的一個變種,叫做“令牌傳送模型”,Token Passing Model。簡單地說,令牌傳送模型採用了狀態路徑對齊的概念。

想象一下,一個HMM中的每個狀態j在時間t處,都擁有一個可移動的令牌,這個令牌中的信息,包含最大似然概率ψj(t),那麼這個令牌就可以表示從o1到ot這個部分觀察向量序列,和模型的匹配程度,限制條件是在時刻t必須處於狀態j。

這樣,上面的路徑增長算法就可以使用新的“令牌傳送”算法替代,這個算法也是在每個時間點t處執行,其中的關鍵步驟是:

 1.將狀態i處的令牌,傳送到和狀態i相連的每個狀態j,然後遞增上面的最大似然概率,使用對數運算時,遞增的數值是log[aij ] + log[bj(o(t)]。
 2.在每個狀態處,都檢查令牌的值,只保留具有最大似然概率的那個令牌,丟棄其它的。

使用令牌傳送模型的好處是,它可以非常容易地擴展到連續語音識別的情況中。假設允許出現的HMM序列由一個有限狀態網絡定義,這個網絡由識別任務的語法生成。即HMM的序列是一個有限的集合。例如,下圖1.7中是一個簡單的網絡,其中每個單詞都是一個音素的序列,每個音素有自己的HMM,並且所有單詞處於一個環路中。

圖1.7 連續語音識別的網絡

在這個網路中,橢圓形表示HMM的實例,而長方形表示“單詞末尾(word-end)節點”。這個組合網絡實際上是一個大的HMM,因此可以利用上面的令牌傳送算法。唯一的區別是,除了最佳令牌的最大似然概率之外,還需要更多的信息。當最佳的令牌到達語音的終點時,它所經過的網絡中的路徑信息需要保留下來,以得到所識別出來的模型序列。

令牌通過網絡的歷史路徑,可以使用如下方法進行記錄。每個令牌攜帶一個指針,叫做“尾部單詞鏈接”(word end link)。當令牌從一個單詞的退出狀態(通過“單詞末尾節點”時,即表明已經從退出狀態轉移了)轉移到另外一個單詞的入口狀態時,這種轉移表明剛剛經過一個單詞邊界。這時生成一個名爲“單詞鏈接記錄(word link record)”的記錄,其中保存了剛剛經過(emerge?)的單詞,以及當前令牌中“尾部單詞鏈接”的值。然後,將令牌的“尾部單詞鏈接”指向這個新生成的WLR(單詞鏈接記錄)。圖1.18演示了這個過程。

圖1.18 記錄單詞邊界的轉移決策

說明:圖中的Before和After表示令牌在從單詞one中轉移出來之前,和之後的狀態,令牌包含兩部分信息,一是令牌當前的最大似然概率,以及一個指針,即“尾部單詞鏈接”,指向當前最後一個識別出來的單詞。在Before時間點,這個指針指向下方左邊第一個節點,即單詞two。

令牌剛剛從單詞“one”中轉移出來,經過了圖中的單詞末尾節點,這時就生成一個WLR,即下方最右邊的一個節點,其中有四個字段,令牌的當前最大似然概率,時間點t,剛經過的單詞“one”,以及一個指針。這個指針指向令牌中的尾部單詞鏈接,即最左邊的單詞two的WLR,然後修改令牌的指針,指向單詞one,說明one是最後一個識別出來的單詞。

問題:不知道中間兩個WLR,分別標以時間點t-2和t-1是什麼含義?

一旦處理完了所有的語音數據,那麼最佳令牌所指向的WLR鏈表,就是識別的單詞序列結果。可以從尾部的節點進行回朔,得到單詞的最佳匹配序列。同時,如果需要的話,可以將語音數據中的單詞邊界提取出來。

上面描述的令牌傳送算法,記錄了傳送過程中的單詞序列。如果需要,可以記錄更詳細的音素序列,甚至狀態序列。

另外,除了可以在每個單詞邊界處,記錄最佳令牌的信息,還可以記錄更多的路徑信息,例如次優令牌,次次優令牌等。這樣,不僅可以生成一個最佳的識別單詞序列,還能生成一個可能的單詞網絡。基於這種思想的算法稱爲lattice N-Best。

它們是次優的,因爲每個狀態使用一個令牌,限制了可以保存的不同令牌的歷史路徑的數目。可以通過讓每個狀態持有多個令牌,並且認爲來自不同的前一個單詞的令牌是不同的,來克服這個數目限制。這是另一類算法,稱爲word N-Best。這種算法已經通過實驗驗證,其性能可以和其它任何最優N-Best算法相比。

上面大致描述了令牌傳送算法的輪廓,它是HTK中實現的識別算法。該算法在模塊HRec和HNet中實現,通過工具HVite調用識別功能。其中提供了單令牌和多令牌傳送識別的算法、單個最佳令牌輸出、網格(lattice)輸出、N-Best列表等算法,並且支持跨單詞的上下文依賴性,網格評分,以及強制對齊等特性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章