天天看點

語音識别之HTK入門(十)——HTK解碼工具HVite源碼分析

這一節講的内容又是語音識别系統非常重要的一環——veterbi解碼,前面我們經過了配置檔案,處理音頻資料,處理标注文本資料、通過Baum-Welch(前向-後向)算法評估模型參數等多個環節,目的都是為了在這一步通過已知的模型來把音頻解碼成對應的文字,實作對語音的識别功能。

這篇如何通俗地講解 viterbi 算法講的比較入門,一看就懂,viterbi的實質也的确如此。現在就是要看它在HTK中是如何應用的,因為實際系統中涉及到很多細節,比如文法、剪枝優化、詞的邊界處理等等,會增加了解的複雜度,但是細心加耐心肯定都能搞明白的。

其實,維特比解碼算法涉及的關鍵步驟在前向算法中已經有所展現了。不同點在于,前向算法計算

語音識别之HTK入門(十)——HTK解碼工具HVite源碼分析

的意義是,說在t時刻,狀态為i情況下,觀察向量為

語音識别之HTK入門(十)——HTK解碼工具HVite源碼分析

的機率,它的實質是求和;那麼,在veterbi解碼算法中,類似的步驟

語音識别之HTK入門(十)——HTK解碼工具HVite源碼分析

是求最大似然值。記

語音識别之HTK入門(十)——HTK解碼工具HVite源碼分析

,,表示從t-1時刻各個狀态到t時刻j狀态下,機率最大的值,記錄下那個對應的上一個狀态i。當考慮連續語音識别時,要考慮的情況就比較多了,例如文法問題、多音字、以及模型的區分度等。

執行解碼指令如下:

HVite -H  .\hmms\hmm7\macros  -H  .\hmms\hmm7\hmmdefs  -S  test.scp  -l  * -i recout_step7.mlf -w wdnet -p 0.0 -s 5.0 dict2 monophones1
           

 現在調試代碼看看HVite是如何解碼操作的。

根據-H指定HMM模型定義,這裡包括兩個一個是宏定義macros,裡面floorvar1的模型參數,作為訓練時模型的協方差的最小值;還有hmmdefs,裡面定義了各子詞(sub-word或phone)的模型參數,是解碼操作的最重要的依據。-S test.scp包含了多個測試檔案提取位置,test.scp裡的每個檔案都是以mfc作為擴充名。-i recout_step7.mlf,指定輸出檔案名recout_step7.mlf,且為mlf檔案格式來存儲識别結果。-w wdnet指明識别過程所依賴的詞網絡。-p -s設定語言模型的參數。dict2位發音字典,monophones1是音子模型清單。

現在進入HVite源碼的main函數裡,看看大體的思路:

int main(int argc, char *argv[])
{

    //函數聲明、一系列初始化等等
   ...


   if (!InfoPrinted() && NumArgs() == 0)
      ReportUsage();
   if (NumArgs() == 0) Exit(0);

   SetConfParms();
   CreateHeap(&modelHeap, "Model heap",  MSTAK, 1, 0.0, 100000, 800000 );
   CreateHMMSet(&hset,&modelHeap,TRUE); 


   // 參數處理
  while (NextArg() == SWITCHARG) {
      s = GetSwtArg();
      if (strlen(s)!=1) 
         HError(3219,"HVite: Bad switch %s; must be single letter",s);
      switch(s[0]){
      case 'a':
         loadLabels=TRUE; break;

   ....


// 資料處理

   if (wdNetFn==NULL)
      DoAlignment();     // 強制對齊
   else
      DoRecognition();   // 識别的處理函數

   /* Free up and we are done */

   if (trace & T_MEM) {
      printf("Memory State on Completion\n");
      PrintAllHeapStats();
   }


// 程式占有的資源釋放

   DeleteVRecInfo(vri);
   ResetHeap(&netHeap);
   FreePSetInfo(psi);
   ...
   Exit(0);
   return (0);           
           

main函數的開頭和前面介紹的幾個工具一樣,聲明變量和将用到的函數,然後就是初始化,例如記憶體、網絡、模型等等準備工作,然後就是指令行參數的處理。核心方法就是 DoRecognition()。

// 通過識别網絡來識别一個個檔案
void DoRecognition(void)
{

   ...

   if ( (nf = FOpen(wdNetFn,NetFilter,&isPipe)) == NULL)
      HError(3210,"DoRecognition: Cannot open Word Net file %s",wdNetFn);
   
   // wdNet為詞級網絡對象
   if((wdNet = ReadLattice(nf,&ansHeap,&vocab,TRUE,FALSE))==NULL)
      HError(3210,"DoAlignment: ReadLattice failed");

   // net為由詞級網絡擴充後的hmm模型網絡
   net = ExpandWordNet(&netHeap,wdNet,&vocab,&hset);


   /* 如果沒有輸入待識别問句,就直接從音頻輸入裝置讀取語音資料 */
   if (NumArgs()==0) {      
      while(TRUE){
         printf("\nREADY[%d]>\n",++n); fflush(stdout);
         ProcessFile(NULL,net,n,genBeam, FALSE);
         if (update > 0 && n%update == 0) {

         ...
         }
      }
   }
   else {              /* 識别指定的音頻檔案 */
      while (NumArgs()>0) {
         if (NextArg()!=STRINGARG)
            HError(3219,"DoRecognition: Data file name expected");
         datFN = GetStrArg();
         if (trace&T_TOP) {
            printf("File: %s\n",datFN); fflush(stdout);
         }
         ....
         // 識别過程
         ProcessFile(datFN,net,n++,genBeam,FALSE);
         .....
      }
   }
}
           

而這裡的ProcessFile是核心函數,參數有檔案名,識别網絡和束寬大小。看看這個函數裡的代碼結構。

Boolean ProcessFile(char *fn, Network *net, int utterNum, LogDouble currGenBeam, Boolean restartable)
{
   ...
   // 初始化識别過程中一些資料結構,比如net
   StartRecognition(vri,net,lmScale,wordPen,prScale);
   // 設定剪枝參數
   SetPruningLevels(vri,maxActive,currGenBeam,wordBeam,nBeam,tmBeam);
 
   tact=0;nFrames=0;
   StartBuffer(pbuf);
   // 讀取檔案中的幀資料到obj中

   // 處理某一幀的語音特征向量,結果儲存在vri相關的資料項中
   ProcessObservation(vri,&obs,-1,xfInfo.inXForm);      

   nFrames++;
   tact+=vri->nact;

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

   lat->utterance=thisFN;
   lat->net=wdNetFn;
   lat->vocab=dictFn;
   
   /* accumulate stats for online unsupervised adaptation only if a token survived */
   if ((lat != NULL) &&  (!vri->noTokenSurvived) && ((update > 0) || (xfInfo.useOutXForm)))
      DoOnlineAdaptation(lat, pbuf, nFrames);
   ...

   Dispose(&ansHeap,lat);
   CloseBuffer(pbuf);
}
           

函數ProcessObservation()才是核心中的核心。它真正實作了維特比算法,也就是token passing model algorithm。這個函數總共大概100行,下面就來詳細了解它是如何一步步實作解碼算法的,并以此為樞紐,連接配接多個資料結構。

涉及到的比較重要的結構體有,與識别網絡有關的:Network、NetNode、NetNodeType、NetLink、NetInst;還有與網格有關的Lattice、LNode、LArc;與識别過程有關的:PRecInfo、VRecInfo、Token、Path、

繼續閱讀