- HTK解码总体流程:
- ProcessFile()的主要代码分析:
- ProcessObservation()的主要代码分析:
- 一个简单的yes-no识别例子:
- 网络上关于HTK BOOK识别部分的中文翻译(转载):
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列表等算法,并且支持跨单词的上下文依赖性,网格评分,以及强制对齐等特性。