前面解析了HCompV源码,它主要功能是初始化hmm模型,定义一些宏。还没接触到模型训练。HMM模型训练算法是语音识别的算法中的难点之一。
它的输入包括所有hmm模型的名称、以及初始的模型参数(在hmmdefs设置好了),特征参数文件(train.scp)和特征文件对应的MLF转写文件(phone0.mlf)。输出就是包含更新后的hmmdefs。
先写最重要,也是比较难的,然后再分析一些程序流程、配置等。这样避免喧宾夺主。
/* Load data and call FBFile: apply forward-backward to given utterance */
void DoForwardBackward(FBInfo *fbInfo, UttInfo *utt, char * datafn, char * datafn2)
{
char datafn_lab[MAXFNAMELEN];
utt->twoDataFiles = twoDataFiles ;
utt->S = fbInfo->al_hset->swidth[0];
/* Load the labels - support for label masks */
if (labFileMask) {
if (!MaskMatch (labFileMask, datafn_lab, datafn))
HError(2319,"HERest: LABFILEMASK %s has no match with segemnt %s", labFileMask, datafn);
}
else
strcpy (datafn_lab, datafn);
LoadLabs(utt, lff, datafn_lab, labDir, labExt);
/* Load the data */
LoadData(fbInfo->al_hset, utt, dff, datafn, datafn2);
if (firstTime) {
InitUttObservations(utt, fbInfo->al_hset, datafn, fbInfo->maxMixInS);
firstTime = FALSE;
}
/* fill the alpha beta and otprobs (held in fbInfo) */
if (FBFile(fbInfo, utt, datafn)) {
/* update totals */
totalT += utt->T ;
totalPr += utt->pr ;
/* Handle the input xform Jacobian if necssary */
if (fbInfo->al_hset->xf != NULL) {
totalPr += utt->T*0.5*fbInfo->al_hset->xf->xform->det;
}
}
}
上面函数顾名思义,就是实现了前向后向算法。LoadLabs函数加载音素级标注文件,构建transaction对象,它包含了这些音素串的所有信息。接着是LoadData函数,将特征向量的数据加载到内存中,并保存相关的信道信息、数据流宽度信息,时间片长度等等。
/* FBFile: apply forward-backward to given utterance */
Boolean FBFile(FBInfo *fbInfo, UttInfo *utt, char * datafn)
已经有了data和Label以及hmm设置作为输入,接下来就是计算前向后向算法。由上面声明的函数FBFile完成。
/* FBFile: apply forward-backward to given utterance */
Boolean FBFile(FBInfo *fbInfo, UttInfo *utt, char * datafn)
{
Boolean success;
if ((success = StepBack(fbInfo,utt,datafn)))
StepForward(fbInfo,utt);
#ifdef PDE_STATS
PrintPDEstats();
#endif
ResetStacks(fbInfo->ab);
return success;
}
看看代码结构,一目了然。
就是StepBacke和StepForward两个函数完成后向和前向算法。下面首先看看StepBack函数的过程。
/* StepBack: Step utterance from T to 1 calculating Beta matrix*/
static Boolean StepBack(FBInfo *fbInfo, UttInfo *utt, char * datafn)
{
LogDouble lbeta;
LogDouble pruneThresh;
AlphaBeta *ab;
PruneInfo *p;
int qt;
ResetObsCache();
ab = fbInfo->ab;
pruneThresh=pruneSetting.pruneInit;
do
{
ResetStacks(ab);
InitPruneStats(ab);
p = fbInfo->ab->pInfo;
p->pruneThresh = pruneThresh;
qt=CreateInsts(fbInfo,ab,utt->Q,utt->tr);
CreateBeta(ab,utt->T);
SetBeamTaper(p,ab->qDms,utt->Q,utt->T);
CreateOtprob(ab,utt->T);
lbeta=SetBeta(ab,fbInfo,utt);
if (lbeta>LSMALL) break;
pruneThresh+=pruneSetting.pruneInc;
}
while(pruneThresh<=pruneSetting.pruneLim);
return TRUE;
}
这个函数的参数,有FBInfo和UttInfo两个指针。它们指向的对象,包含了计算前向、后向算法非常重要的数据结构——AlphaBeta、ParmBuf和Observation。
/* structure for the forward-backward alpha-beta structures */
typedef struct {
MemHeap abMem; /* alpha beta memory heap */
PruneInfo *pInfo; /* pruning information */
HLink *up_qList; /* array[1..Q] of active HMM defs */
HLink *al_qList; /* array[1..Q] of active align HMM defs */
LabId *qIds; /* array[1..Q] of logical HMM names (in qList) */
short *qDms; /* array[1..Q] of minimum model duration */
DVector *alphat; /* array[1..Q][1..Nq] of prob */
DVector *alphat1; /* alpha[t-1] */
DVector **beta; /* array[1..T][1..Q][1..Nq] of prob */
float *****otprob; /* array[1..T][1..Q][2..Nq-1][0..S][0..M] of prob */
LogDouble pr; /* log prob of current utterance */
Vector occt; /* occ probs for current time t */
Vector *occa; /* array[1..Q][1..Nq] of occ probs (trace only) */
} AlphaBeta;
看一下AlphaBeta的结构体什么样子,分析下它包含哪些部分,分别叫什么名字,以及它在内存是什么样子。因为后面的代码跟它关系密切。
主要有HLink* up_qList它指向HMM指针,也就是包含一系列HMM指针,它是即将训练的句子文本对应的hmm模型序列。DVector*alphat矩阵,包含了alpha的概率值,以及DVector**beta。
这是内存中alphat的样子,beta包含一系列指向类似alphat的指针,它是alphat的数组,这样一步一步嵌套的。Q代表了这句话包含多少个hmm,Nq表示每个hmm包含多少个状态。它是由多个hmm串起来构成一个大的hmm。下图是《Speech and Language Processing》第九章的一个图,示意了嵌入式训练中hmm结构。
再看一下StepForward函数,运行StepBack然后再运行一次StepForward就完成了一句话的嵌入式训练(Embeded training)。
/* StepForward: Step from 1 to T calc'ing Alpha columns and
accumulating statistic */
static void StepForward(FBInfo *fbInfo, UttInfo *utt)
{
int q,t,start,end,negs;
DVector aqt,aqt1,bqt,bqt1,bq1t;
HLink al_hmm, up_hmm;
AlphaBeta *ab;
/* reset the memory heap for alpha for a new utterance */
/* ResetHeap(&(fbMemInfo.alphaStack)); */
ab = fbInfo->ab;
CreateAlpha(ab,fbInfo->al_hset,utt->Q); /* al_hset may be idential to up_hset */
InitAlpha(ab,&start,&end,utt->Q,fbInfo->skipstart,fbInfo->skipend);
ab->occa = NULL;
if (trace&T_OCC)
CreateTraceOcc(ab,utt);
for (q=1;q<=utt->Q;q++){ /* inc access counters */
up_hmm = ab->up_qList[q];
negs = (int)up_hmm->hook+1;
up_hmm->hook = (void *)negs;
}
ResetObsCache();
for (t=1;t<=utt->T;t++) {
GetInputObs(utt, t, fbInfo->hsKind);
if (fbInfo->hsKind == TIEDHS)
PrecomputeTMix(fbInfo->al_hset,&(utt->ot),pruneSetting.minFrwdP,0);
if (t>1)
StepAlpha(ab,t,&start,&end,utt->Q,utt->T,utt->pr,
fbInfo->skipstart,fbInfo->skipend);
if (trace&T_ALF && NonSkipRegion(fbInfo->skipstart,fbInfo->skipend,t))
TraceAlphaBeta(ab,t,start,end,utt->pr);
for (q=start;q<=end;q++) {
/* increment accs for each active model */
al_hmm = ab->al_qList[q];
up_hmm = ab->up_qList[q];
aqt = ab->alphat[q];
bqt = ab->beta[t][q];
bqt1 = (t==utt->T) ? NULL:ab->beta[t+1][q];
aqt1 = (t==1) ? NULL:ab->alphat1[q];
bq1t = (q==utt->Q) ? NULL:ab->beta[t][q+1];
SetOcct(al_hmm,q,ab->occt,ab->occa,aqt,bqt,bq1t,utt->pr);
/* accumulate the statistics */
if (fbInfo->uFlags&(UPMEANS|UPVARS|UPMIXES|UPXFORM))
UpMixParms(fbInfo,q,up_hmm,al_hmm,utt->ot,utt->ot2,t,aqt,aqt1,bqt,
utt->S, utt->twoDataFiles, utt->pr);
if (fbInfo->uFlags&UPTRANS)
UpTranParms(fbInfo,up_hmm,t,q,aqt,bqt,bqt1,bq1t,utt->pr);
}
}
}
然后就是再次读入下一个文件、不断循环直至文件结尾。