语音识别之HTK入门(十)——HTK解码工具HVite源码分析
这一节讲的内容又是语音识别系统非常重要的一环——veterbi解码,前面我们经过了配置文件,处理音频数据,处理标注文本数据、通过Baum-Welch(前向-后向)算法评估模型参数等多个环节,目的都是为了在这一步通过已知的模型来把音频解码成对应的文字,实现对语音的识别功能。这篇如何通俗地讲解 viterbi 算法讲的比较入门,一看就懂,viterbi的实质也的确如此。现在就是要看它在HTK中是如何应
这一节讲的内容又是语音识别系统非常重要的一环——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、
更多推荐
所有评论(0)