這一節講的内容又是語音識别系統非常重要的一環——veterbi解碼,前面我們經過了配置檔案,處理音頻資料,處理标注文本資料、通過Baum-Welch(前向-後向)算法評估模型參數等多個環節,目的都是為了在這一步通過已知的模型來把音頻解碼成對應的文字,實作對語音的識别功能。
這篇如何通俗地講解 viterbi 算法講的比較入門,一看就懂,viterbi的實質也的确如此。現在就是要看它在HTK中是如何應用的,因為實際系統中涉及到很多細節,比如文法、剪枝優化、詞的邊界處理等等,會增加了解的複雜度,但是細心加耐心肯定都能搞明白的。
其實,維特比解碼算法涉及的關鍵步驟在前向算法中已經有所展現了。不同點在于,前向算法計算
的意義是,說在t時刻,狀态為i情況下,觀察向量為
的機率,它的實質是求和;那麼,在veterbi解碼算法中,類似的步驟
是求最大似然值。記
,,表示從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、