上一篇博客介绍了整个HERest工具最重要的功能,就是实现前向-后向算法(Baum-Welch)算法。并简单介绍了涉及的几个重要的数据结构,以及输入和输出。现在开始从头开始,了解这个实现过程。
UttInfo *utt; /* utterance information storage */
FBInfo *fbInfo; /* forward-backward information storage */
HMMSet hset; /* Set of HMMs to be re-estimated */
在main函数的开头就声明了上面三个重要的变量,并且给出了注释,它们就是三个最重要的提示。我们先看下面的流程,待会再回来详细讲这三个数据结构。因为它们太重要了,所以要在合适的时机出场。
代码流程接下来是一系列初始工作,跟其他工具类似,例如命令参数的保存,各个模块的内存分配和初始化。然后是具体涉及上面三个重要变量的内存分配工作,实际上这三个变量,都占有大量内存。所以单独给它们命名了一段内存空间,名字分别是HmmStore、uttStore以及FBInfoStore,然后在它们上面分别创建hset、utt和fbInfo对象。
CreateHeap(&hmmStack,"HmmStore", MSTAK, 1, 1.0, 50000, 500000);
SetConfParms();
CreateHMMSet(&hset,&hmmStack,TRUE);
CreateHeap(&uttStack, "uttStore", MSTAK, 1, 0.5, 100, 1000);
utt = (UttInfo *) New(&uttStack, sizeof(UttInfo));
CreateHeap(&fbInfoStack, "FBInfoStore", MSTAK, 1, 0.5, 100 , 1000 );
fbInfo = (FBInfo *) New(&fbInfoStack, sizeof(FBInfo));
可以通过sizeof操作符计算对象(结构)占用多大空间,通过New函数实现内存分配(HTK3.4.1是C语言实现的,所以这里的New是自己实现的,它的功能与C++中的关键字new类似,都是给对象在堆上分配空间)。
趁这个机会说一下HTK的内存管理。HTK为了提高内存管理的效率,自己实现了一套内存分配、收回机制,涉及内存分配的程序一般都在HMem模块中。
HTK内存管理分三类,分别是MHEAP, MSTAK和CHEAP。为什么分三类,以及它们各自的特点是什么?
MSTAK是stack类型的标记,在此内存上分配的对象是按照“先分配;后收回”的原则执行。而MHEAP则没有这个限制,可随机访问,但是要求每个对象的大小一样,而且它因此是自己管理,没有其他时空消耗。CHEAP则是调用操作系统关于内存分配、收回的一些操作。理论上应该尽量避免。
现在来分别看看上述三个变量的结构是啥样。
第一个要介绍就是HMMSet,其实它没什么好介绍的,因为之前在HCompV中已经详细分析过了。因为它太重要了,就再巩固下。
/* ---------------------- HMM Sets ----------------------------- */
typedef struct _HMMSet{
MemHeap *hmem; /* memory heap for this HMM Set */
Boolean *firstElem; /* first element added to hmem during MakeHMMSet*/
char *hmmSetId; /* identifier for the hmm set */
MILink mmfNames; /* List of external file names */
int numLogHMM; /* Num of logical HMM's */
int numPhyHMM; /* Num of distinct physical HMM's */
int numFiles; /* total number of ext files */
int numMacros; /* num macros used in this set */
MLink * mtab; /* Array[0..MACHASHSIZE-1]OF MLink */
short vecSize; /* dimension of observation vectors */
short swidth[SMAX]; /* [0]=num streams,[i]=width of stream i */
ParmKind pkind; /* kind of obs vector components */
DurKind dkind; /* kind of duration model (model or state) */
CovKind ckind; /* cov kind - only global in V1.X */
HSetKind hsKind; /* kind of HMM set */
int numStates; /* Number of states in HMMSet */
int numSharedStates; /* Number of shared states in HMMSet */
int numMix; /* Number of mixture components in HMMSet */
int numTransP; /* Number of distinct transition matrices */
int ckUsage[NUMCKIND]; /* Number of components using given ckind */
InputXForm *xf; /* Input transform of HMMSet */
short projSize; /* dimension of vector to update */
} HMMSet;
为了让代码看起来简洁,避免信息太多让人绝望,删除了当前单音子模型不涉及的数据项。mtab这个MLink指针,指向一系列MLink,可以把mtab看做MLink数组,它的大小是MACHASHSIZE。每一项代表了一个hash槽,其值为一个指针,指向一个模型宏MacroDef。MacroDef包含了模型类型、模型本身的信息。关于模型类型,代号为[hluvixdtmps*]中的某一个,有什么作用,具体代表什么意思,到为目前为止,我也不清楚。
在函数CreateHMMSet中调用MakeHashTab函数,该函数再通过New函数为mtab分配了MACHASHSIZE个sizeof(void*)的空间,且分别让它们的值为NULL,就是所有指针都指向空。
OK,HMMSet暂时就回顾到这里,后面遇到时再来回顾下,务必要对HMMSet在内存的结构非常清晰,像是刻画在自己的脑海中似得。
再说一次,HMMSet包含一个数组mtab,它的元素是指向MacroDef的指针,该数组的size是MACHASHSIZE。
#define MACHASHSIZE 250007 /* Size of each HMM Set macro hash table */
那么这个MacroDef是什么?
typedef struct _MacroDef{
MLink next; /* next cell in hash table */
char type; /* type of macro [hluvixdtmps*] */
short fidx; /* idx of MMF file (0 = SMF) */
LabId id; /* name of macro */
Ptr structure; /* -> shared structure or HMM Def */
} MacroDef;
重点看Ptr structure,它就是指向某个具体的hmm,下面会看到如何创建hmm对象,并把它赋给Macrodef这个指针。
接下来,再看看UttInfo和FBInfo。
/* structure for the utterance information */
typedef struct {
MemHeap transStack; /* utterance transcript information heap */
MemHeap dataStack; /* utterance data information heap */
MemHeap dataStack2; /* utterance data2 information heap */
int Q; /* number of models in transcription */
Transcription *tr; /* current transcription */
Boolean twoDataFiles; /* Using two data files */
int S; /* number of data streams */
int T; /* number of frames in utterance */
ParmBuf pbuf; /* parameter buffer */
ParmBuf pbuf2; /* a second parameter buffer (if required) */
Observation ot; /* Observation at time t ... */
Observation ot2; /* Cepstral Mean Normalised obervation, used in
single pass re-training */
LogDouble pr; /* log prob of current utterance */
} UttInfo;
UttInfo这个数据结构保存了输入的观察值和对应文本(转录)的信息,分别对应Observation和Transcription。再往下追,看看这两个结构是什么样子。
typedef struct {
Boolean eSep; /* Energy is in separate stream */
short swidth[SMAX]; /* [0]=num streams,[i]=width of stream i */
ParmKind bk; /* parm kind of the parm buffer */
ParmKind pk; /* parm kind of this obs (bk or DISCRETE) */
short vq[SMAX]; /* array[1..swidth[0]] of VQ index */
Vector fv[SMAX]; /* array[1..swidth[0]] of Vector */
} Observation;
它记录了观察向量的信息,包括有多个流、观察值是离散还是连续的、以及参数本身fv[SMAX]。并且我们看到UttInfo中,是以Transcription指针 *tr 的形式保存的,其实它是一个向量,包含了所有观察值包装后的Transcription元素。在LoadData函数里会看到。
FBInfo数据结构保存HMMset和一个指向AlphaBeta结构的指针,该结构是理解前向后向算法(Baum-Welch算法)的核心。也是嵌入式训练算法的核心。
/* structure storing the model set and a pointer to it's alpha-beta pass structure */
typedef struct {
HMMSet *up_hset; /* set of HMMs to be re-estimated */
HMMSet *al_hset; /* HMMs to use for alignment */
/* these are equal unless 2 model reest */
HSetKind hsKind; /* kind of the alignment HMM system */
UPDSet uFlags; /* parameter update flags */
int skipstart; /* Skipover region - debugging only */
int skipend;
AlphaBeta *ab; /* Alpha-beta structure for this model */
} FBInfo;
所以再贴上AlphaBeta结构的代码。
/* 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;
这个结构包含很多信息,pruning info、qList、alphat、beta等等。总之,与前向后向算法有关的数据都包含在其中。因此理解它是理解整个训练过程的关键。