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 SIL100000 4500000 s5 -3601.9179694500000 4600000 s2 -76.337799 sil -1485.466675 SIL4600000 6200000 s4 -1074.7011726200000 6600000 s5 -334.2942816600000 7400000 s2 -737.986572 no -2600.320801 NO7400000 8200000 s3 -684.4291388200000 8600000 s4 -340.4921888600000 9700000 s5 -837.4129039700000 11000000 s2 -924.753235 yes -7088.080566 YES11000000 12500000 s3 -1245.92907712500000 13500000 s4 -570.13519313500000 20000000 s5 -4347.26318420000000 21100000 s2 -877.634888 sil -2479.738770 SIL21100000 21900000 s3 -585.97778321900000 22200000 s4 -244.96165522200000 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列表等算法,并且支持跨单词的上下文依赖性,网格评分,以及强制对齐等特性。

HTK解码代码分析(二)相关推荐

  1. Android4.0图库Gallery2代码分析(二) 数据管理和数据加载

    Android4.0图库Gallery2代码分析(二) 数据管理和数据加载 2012-09-07 11:19 8152人阅读 评论(12) 收藏 举报 代码分析android相册优化工作 Androi ...

  2. Linux-kernel网桥代码分析(二)

    第三部分: ioctl管理网桥 3.1 通过ioctl系统调用创建网桥      仍然以前的配置作为例,我们分用户空间程序brctl是如何通过ioctl系统调用在kernel空间内创建上述的数据结构. ...

  3. 水平集详解与代码分析二

    上一节我们分析了CV模型的原理并列出了演化公式,这一节我们将通过该公式编写CV模型的C++代码,代码调用了opencv库的一些函数,可以根据自己使用的版本更改.代码一一对应公式里的各项系数. 头文件. ...

  4. OSChina App代码分析二

    转载请标明出处: http://blog.csdn.net/zq2114522/article/details/50346073: 本文出自:[梁大盛的博客] 在第二篇里面我们首先分析OSChinaA ...

  5. 信息管理代码分析二读取二进制文件数据

    first和end做为全局变量,分别指向链表的头和尾.建立链表的方式也比较简易,从二进制文件数据块中,依次从头到尾读取,每读取一个就建立一个结点. /*基本模型*/ EMP *emp1; while( ...

  6. EOS智能合约开发系列(13): 多签合约代码分析(二)

    知识星球地址:https://t.zsxq.com/2zBaE6u 欢迎访问知识星球,并留言探讨.

  7. CP2K代码分析二:cp2k_run

    cp2k_runs cp2k_runs为CP2K主执行模块 #mermaid-svg-oBt60J8r5EXhCvPE .label{font-family:'trebuchet ms', verda ...

  8. lighttpd1.4.18代码分析

    lighttpd1.4.18代码分析(八)--状态机(2)CON_STATE_READ状态 posted @ 2008-09-24 10:50 那谁 阅读(2225) | 评论 (1)  编辑 lig ...

  9. s3c6410 uboot代码分析《一》

    来源:http://hi.baidu.com/__eabi/blog/item/be67533797bc73f014cecb49.html 以下用以记录uboot代码的分析过程,目标是s3c6410, ...

最新文章

  1. 第九届蓝桥杯大赛软件赛省赛 C/C++ 大学B组
  2. matlab 编译 java_MATLAB编译器运行时(MCR)编译器
  3. win7系统临时文件夹的打开方法
  4. linux下拷贝文件的时候出现权限不够,如何解决
  5. mysql v7.11_编译后MySQL v5.7.11安装出错
  6. JDK5.0新特性系列---11.5.4线程 同步装置之Exchanger
  7. 对超级计算机的认识有关论文,计算机科与技术专业的认识与思考.docx
  8. ubuntu切换python版本
  9. C程序设计--指针(对 “ 多维数组 ” 进行输出操作)
  10. android开源进度条,Android github开源进度条类框架SmoothProgressBar的使用
  11. 扫雷代码下载地址可以打开啦!
  12. 如何按行政区划下载谷歌卫星地图并裁剪
  13. ubuntu 下安装 qq wechat VirtualBox notepadqq
  14. 工作日志,常见的统计解决方案
  15. 读《互联网:一部概念史》有感
  16. 多智能体强化学习【Windows\Ubuntu 安装星际争霸Ⅱ】
  17. 反思|Android 事件拦截机制的设计与实现,android组件化开发
  18. 泛泰binx和ota升级包下载工具Android版[2013.6.7提供源代码]
  19. 【ArnoldC4D】6.ArnoldToC4D_光泽和反射光线深度(glossy_and_reflection)
  20. [RK3288][Android6.0] USB OTG模式及切换

热门文章

  1. “对不起,我们不招半路出家的程序猿”
  2. python multi不断乘_芝麻IP:Python几种矩阵乘法np.dot, np.multiply……
  3. Google Android:羊养肥了再杀?
  4. 学习英文和英语听力的网址
  5. android 汽车转向辅助线,Android开发 - 掌握ConstraintLayout(七)辅助线(Guideline)
  6. BSOJ3806 TYVJ 2032 升降梯上
  7. Nvidia 显卡 Failed to initialize NVML Driver/library version
  8. SIwave学习记录:电热耦合仿真
  9. 北航网络空间安全复习资料
  10. 【气动学】外弹道仿真系统含Matlab源码