上一篇博客介绍了到CreateHMM函数,它完成了创建名为“proto”的hmm,并且为HMMSet创建了mtab以及其他基础设施,例如NameCell指针的hashtable数组。
顺着CreateHMM函数执行结束,HCompV调用LoadHMMSet。下面分析该函数的流程。它的函数签名为下所示:
LoadHMMSet(HMMSet *hset, char *hmmDir, char *hmmExt)
其中hmmDir,和hmmExt是命令行参数指定的hmm原型“***/proto”文件的目录和扩展类型,这里hmmDir为“src”,而hmmExt为空,因为它没有扩展文件名。
for (h=0; h<MACHASHSIZE; h++)
for (p=hset->mtab[h]; p!=NULL; p=p->next)
if (p->type == 'h'){
hmm = (HLink)p->structure;
if (hmm->numStates == 0 ) {
ConcatFN(hmmDir,p->id->name,hmmExt,fname);
if(InitScanner(fname,&src,&tok,hset)<SUCCESS){
HRError(7010,"LoadHMMSet: Can't find file");
ResetHMMSet(hset);
return(FAIL);
}
if (trace&T_MAC)
printf("HModel: getting HMM Def from %s\n",fname);
if(GetToken(&src,&tok)<SUCCESS){
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadHMMSet: GetToken failed");
return(FAIL);
}
while (tok.sym == MACRO)
switch (tok.macroType){
case 'o':
if(GetOptions(hset,&src,&tok, &nState)==FAIL){
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadHMMSet: GetOptions failed");
return(FAIL);
}
break;
case 'h':
if (!ReadString(&src,buf)){
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadHMMSet: Macro name failed");
return(FAIL);
}
if (GetLabId(buf,FALSE) != p->id){
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadHMMSet: Inconsistent HMM macro name");
return(FAIL);
}
if(GetToken(&src,&tok)<SUCCESS){
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadAllMacros: GetToken failed");
return(FAIL);
}
break;
default:
TermScanner(&src);
ResetHMMSet(hset);
HMError(&src,"LoadHMMSet: Unexpected macro in HMM def file");
return(FAIL);
break;
}
if(GetHMMDef(hset,&src,&tok,hmm,nState)<SUCCESS){
TermScanner(&src);
ResetHMMSet(hset);
HRError(7032,"LoadHMMSet: GetHMMDef failed");
return(FAIL);
}
TermScanner(&src);
}
}
重点是看上面的代码做了什么事。最外的循环是遍历所有的mtab的入口,然后浏览它是否为物理hmm类型,然后看它的NumStates是否为0。如果为0,则对它进行初始化。
回忆下上一篇说回到,给hset里添加了一个简单初始化的hmm,名为“proto”,只是创建了hmm空壳,里面并没有数据且NumStates为0.现在就是要找到这个hmm实体,并且读取“src\proto”文件所指定的hmm相关参数,比如状态数、转移矩阵等等并赋给这个hset中的对应的hmm。当循环找到存在这么一个物理hmm,然后顺次调用InitScanner和GetToken。
InitScanner的作用是初始化文件句柄,GetToken是读取proto文件的第一个字符,判断它是什么类型宏标记。我们的proto前两个字符分别是“~”和“o”,因此GetToken得到的tok->macroType 的值是o; tok->sym = MACRO;然后进入while循环,处理该文件中的一些符号,首先是调用 GetOptions(HMMSet *hset, Source *src, Token *tok, int *nState)。
/* GetOptions: read a global options macro, return numStates if set */
static ReturnStatus GetOptions(HMMSet *hset, Source *src, Token *tok, int *nState)
{
int p=0;
*nState=0;
if (trace&T_PAR) printf("HModel: GetOptions\n");
if(GetToken(src,tok)<SUCCESS){
HMError(src,"GetOptions: GetToken failed");
return(FAIL);
}
while (tok->sym == PARMKIND || tok->sym == INVDIAGCOV ||
tok->sym == HMMSETID || tok->sym == INPUTXFORM ||
tok->sym == PARENTXFORM || tok->sym == PROJSIZE ||
(tok->sym >= NUMSTATES && tok->sym <= XFORMCOV)){
if(GetOption(hset,src,tok,&p)<SUCCESS){
HMError(src,"GetOptions: GetOption failed");
return(FAIL);
}
if (p>*nState) *nState = p;
}
FreezeOptions(hset);
return(SUCCESS);
}
它里面也会调用GetToken:
/* GetToken: put next symbol from given source into token */
static ReturnStatus GetToken(Source *src, Token *tok)
{
char buf[MAXSYMLEN],tmp[MAXSTRLEN];
int i,c,imax,sym;
tok->binForm = FALSE;
while (isspace(c=GetCh(src))); /* Look for symbol or Macro */
if (c != '<' && c != ':' && c != '~' && c != '.' && c != '#') {
if (c == EOF) {
if (trace&T_TOK) printf("HModel: tok=<EOF>\n");
tok->sym=EOFSYM; return(SUCCESS);
}
HMError(src,"GetToken: Symbol expected");
return(FAIL);
}
if (c == '~'){ /* If macro sym return immediately */
c = tolower(GetCh(src));
if (c!='s' && c!='m' && c!='u' && c!='x' && c!='d' && c!='c' &&
c!='r' && c!='a' && c!='b' && c!='g' && c!='f' && c!='y' && c!='j' &&
c!='v' && c!='i' && c!='t' && c!='w' && c!='h' && c!='o')
{
HMError(src,"GetToken: Illegal macro type");
return(FAIL);
}
tok->macroType = c; tok->sym = MACRO;
if (trace&T_TOK) printf("HModel: MACRO ~%c\n",c);
return(SUCCESS);
}
i=0; imax = MAXSYMLEN-1;
if (c=='#') { /* if V1 mmf header convert to ~h */
while ((c=GetCh(src)) != '#' && i<imax)
buf[i++] = c;
buf[i] = '\0';
if (strcmp(buf,"!MMF!") != 0){
HMError(src,"GetToken: expecting V1 style MMF header #!MMF!#");
return(FAIL);
}
tok->sym = MACRO; tok->macroType = 'h';
if (trace&T_TOK) printf("HModel: MACRO ~h (#!MMF!#)\n");
return(SUCCESS);
}
if (c=='.'){ /* if . and not EOF convert to ~h */
while (isspace(c=GetCh(src)));
if (c == EOF) {
if (trace&T_TOK) printf("HModel: tok=.<EOF>\n");
tok->sym=EOFSYM;
return(SUCCESS);
}
UnGetCh(c,src);
tok->sym = MACRO; tok->macroType = 'h';
if (trace&T_TOK) printf("HModel: MACRO ~h (.)\n");
return(SUCCESS);
}
if (c=='<') { /* Read verbose symbol string into buf */
while ((c=GetCh(src)) != '>' && i<imax)
buf[i++] = islower(c)?toupper(c):c;
buf[i] = '\0';
if (c != '>'){
HMError(src,"GetToken: > missing in symbol");
return(FAIL);
}
/* This is tacky and has to be fixed*/
for (sym=0; sym<NUMSYM; sym++) /* Look symbol up in symMap */
if (strcmp(symMap[sym].name,buf) == 0) {
tok->sym = symMap[sym].sym;
if (trace&T_TOK) printf("HModel: tok=<%s>\n",buf);
return(SUCCESS); /* and return */
}
} else {
/* Read binary symbol into buf */
tok->binForm = TRUE;
sym = GetCh(src);
if (sym>=BEGINHMM && sym<PARMKIND) {
if (trace&T_TOK) printf("HModel: tok=:%s\n",symNames[sym]);
tok->sym = (Symbol) sym;
return(SUCCESS); /* and return */
}
}
/* if symbol not in symMap then it may be a sampkind */
if ((tok->pkind = Str2ParmKind(buf)) != ANON){
tok->sym = PARMKIND;
if (trace&T_TOK) printf("HModel: tok=SK[%s]\n",buf);
return(SUCCESS);
}
strcpy(tmp,"GetToken: Unknown symbol ");
HMError(src,strcat(tmp,buf));
return(FAIL);
}
GetToken函数的作用语法解析,并将结果保存在Token *tok中。
它主要是处理HMM模型配置文件,这里是proto(该文件内包含一个名为proto的hmm初始模型),支持那些字符语义,包括HMM定义的开始与结束、状态数、参数等等。另外,还分析语音特征的一些参数、例如是否包含一阶差分、二阶差分等等。
接着,最重要的是要理解HCompV工具的最终目的,是初始化一个全局hmm,包括状态的均值和方差。到目前为止还都是准备工作,下面才是真正的主角。但难点并不在此,而恰恰是前期那些设置工作,如果理解了前面的逻辑,接下来会非常容易。
/* Create accumulators for the mean and variance */
for (s=1;s<=hset.swidth[0]; s++){
V = hset.swidth[s];
accs[s].meanSum=CreateVector(&gstack,V);
ZeroVector(accs[s].meanSum);
if (fullcNeeded[s]) {
accs[s].squareSum.inv=CreateSTriMat(&gstack,V);
accs[s].fixed.inv=CreateSTriMat(&gstack,V);
ZeroTriMat(accs[s].squareSum.inv);
}
else {
accs[s].squareSum.var=CreateSVector(&gstack,V);
accs[s].fixed.var=CreateSVector(&gstack,V);
ZeroVector(accs[s].squareSum.var);
}
}
这段代码设置了一系列的累加器,它保存了对训练数据的中间统计信息。accs就是一个数组,它的元素是CovAcc,包括了meansSum和squareSum。如何计算它们,代码逻辑也比较清晰,在LoadFile函数中说明。
/* Storage for mean and covariance accumulators */
typedef struct {
Vector meanSum; /* acc for mean vector value */
Covariance squareSum; /* acc for sum of squares */
Covariance fixed; /* fixed (co)variance values */
} CovAcc;
static CovAcc accs[SMAX]; /* one CovAcc for each stream */
到目前为止,还仅仅停留在函数Initialise()中,分配了hmm的模型参数,分配了累加器为了后面计算全局的均值和方差做准备。
接下来看看我们数次提到的“累加器”到底是什么样的。
accs[SMAX]数组,它的元素的CovAcc,上面的代码展示了CovAcc包含三项。分别是meanSum,squareSum和fixed。这里数组可以暂时不用考虑,因为涉及到HTK预留了多数据源的扩展。我们可以理解为CovAcc就是当前数据集的累加器,接下来就是填充这个CovAcc的内容。下面三行代码就是在给累加器创建空间,并将初始值设置为0.
但是,这三行代码看似平淡无奇,实则有些蹊跷在里面。尤其是CreateSVector,它不仅仅是创建vector向量的。
accs[s].squareSum.var=CreateSVector(&gstack,V);
accs[s].fixed.var=CreateSVector(&gstack,V);
ZeroVector(accs[s].squareSum.var);
详细分析下CreateSVector函数:
/* EXPORT->CreateSVector: Shared version */
Vector CreateSVector(MemHeap *x, int size)
{
SVector v;
Ptr *p;
int *i;
p = (Ptr *)New(x,SVectorElemSize(size));
v = (SVector) (p+2);
i = (int *) v; *i = size;
SetHook(v,NULL);
SetUse(v,0);
return v;
}
x表示全局的内存堆,size是要分配的内存数量。实际上分配的内存比size要大一些。
再看New(x, SVectorElemSize(size)):
size_t SVectorElemSize(int size){ return (size+1)*sizeof(float)+2*sizeof(Ptr); }
看到了吗?它实际分配的空间数量是(size+1)*sizeof(float) + 2*sizeof(Ptr)多了一个float和2个指针的空间。看后面的代码可以推测,2个指针变量是存放在这块内存的最前面,且接着存放int变量size,也就是vector的大小,也是这块内存后的空间。
经过SetHook和SetUse之后:
ZeroVector函数就是将上图中绿色标记为float部分设置为0.0。
接着就是创建observation对象,来放置观察向量。
/* EXPORT->MakeObservation: Create obs using info in swidth and pkind */
Observation MakeObservation(MemHeap *x, short *swidth,
ParmKind pkind, Boolean forceDisc, Boolean eSep)
{
Observation ob;
int i,numS;
ob.pk = pkind;
ob.bk = pkind&(~HASNULLE);
ob.eSep = eSep;
for (i=0; i<SMAX; i++) ob.fv[i]=NULL,ob.vq[i]=-1;
if (forceDisc) {
if ((pkind&BASEMASK) != DISCRETE && !(pkind&HASVQ))
HError(6373,"MakeObservation: No way to force discrete observation");
ob.pk = DISCRETE+(pkind&HASNULLE);
}
numS = swidth[0];
if (numS>=SMAX)
HError(6372,"MakeObservation: num streams(%d) > MAX(%d)",numS,SMAX-1);
for (i=0; i<=numS; i++){
ob.swidth[i] =swidth[i];
if (i>0 && (pkind&BASEMASK) == DISCRETE && swidth[i] != 1)
HError(6372,"MakeObservation: discrete stream widths must be 1");
}
/* Note that the vectors are created even if ob.pk==DISCRETE as */
/* these are used in ReadAs????? but should not be accessed elsewhere */
if ((pkind&BASEMASK) != DISCRETE)
for (i=1; i<=numS; i++)
ob.fv[i] = CreateVector(x,swidth[i]);
return ob;
}
该函数根据之前的处理接触,初始化了全局的Observation对象,它用来接收观察向量。