HTK中Vocab字典的结构

在语音识别系统中,发音字典是必备的元素之一。系统通过发音字典文件在内存中构建一个字典对象,供后续的训练、解码使用。

本期blog就来跟踪下这个Vocab的细节。

首先贴一下这个Vocab的struct代码,大家有个直观的印象。

typedef struct {
   int nwords;          /* total number of words */
   int nprons;          /* total number of prons */
   Word nullWord;       /* dummy null word/node */
   Word subLatWord;     /* special word for HNet subLats */
   Word *wtab;          /* hash table for DictEntry's */
   MemHeap heap;        /* storage for dictionary */
   MemHeap wordHeap;    /* for DictEntry structs  */
   MemHeap pronHeap;    /* for WordPron structs   */
   MemHeap phonesHeap;  /* for arrays of phones   */
} Vocab;

通过它可以了解[【字典】对象的全貌:有多少个词,有多少中发音(是否包含多音字,从这里可以看出来),以及相关的内存分配器(word/pron/phone)。其中有个不起眼的指针容易被忽略的,就是Word* wtab。它是真正存放发音信息的地方。一个【字典】有成千上万个【单词】(Word),每个单词至少有一个【发音】(pronunciation),它必定需要一大块空间来存储,而且是可高效访问的。HTK就是利用hash table来实现存储和访问的。

再来贴一个【字典】初始化的函数,结合这个函数来讲解如何初始化一个空的字典。

/* EXPORT->InitVocab: Initialise voc data structure */
void InitVocab(Vocab *voc)
{
   int i;

   CreateHeap(&voc->wordHeap,"Word Heap",MHEAP,sizeof(DictEntry),
              0.4,200,2000);
   CreateHeap(&voc->pronHeap,"Pron Heap",MHEAP,sizeof(WordPron),
              0.4,200,2000);
   CreateHeap(&voc->phonesHeap,"Phones Heap",MSTAK,1,0.4,400,4000);
   voc->wtab = (Word*) New(&voc->phonesHeap,sizeof(Word)*VHASHSIZE);
   for (i=0; i<VHASHSIZE; i++)
      voc->wtab[i] = NULL;
   voc->nullWord = GetWord(voc, GetLabId("!NULL",TRUE), TRUE);
   voc->subLatWord = GetWord(voc, GetLabId("!SUBLATID",TRUE), TRUE);
   voc->nwords = voc->nprons = 0;
}

这个函数的作用是给wtab分配空间,大小为sizeof(Word)*VHASHSIZE;然后初始化为NULL;后续会调用 ReadDict(char *dictFn, Vocab *voc)函数,它根据给定的字典文件信息来填充对应的hash表槽位值,也就是使得wtab中元素指向各DictEntry对象。

   Word nullWord;       /* dummy null word/node */
   Word subLatWord;     /* special word for HNet subLats */

比较费解是这个两个数据项有什么作用。其实可以暂时放一放,待后续了解了更多知识后回过头来再理解它们的作用。

在这个初始化一个空Vocab对象的过程中,另一个出现最多次数的名称就是Word,比如在分配内存时,就是以sizeof(Word)为元素大小的。这个hash table是分配在phonesHeap上的,Word是一个指针,它指向DictEntry,而DictEntry对象存储在wordHeap中,DictEntry中又有指针Pron,它指向发音信息对象WordPron。这个WordPron结构体保存了发音的个数(多音字时大于1,通过next访问下一个)、音子的个数(当前发音的)、音子序列、对应的Word、以及这个发音的输出符号。

下面这个图能反应Vocab、wtab、Word、DictEntry、Pron、WordPron之间的关系,其中wtab可以理解为指针(word)数组,word是指向DictEntry的指针。在DictEntry结构内包含了指针Pron,它指向该DictEntry的发音信息WordPron对象。

 

InitVocab函数给Vocab对象分配空间,并且wtab所有槽值都为空,也就这个【字典】对象里一个词(Word)信息都没有,下一步就是通过ReadDict(char* dictFn, Vocab* voc)函数从字典文件dictFn中读取发音信息,然后给voc对象赋值。

/* EXPORT->ReadDict: read and store a dictionary definition */
ReturnStatus ReadDict(char *dictFn, Vocab *voc)
{
   LabId labels[MAXPHONES+4];
   Source src;
   Word word;
   float prob;
   int nphones;
   ReturnStatus ret;

   // src即为字典文件句柄
   if((ret=ReadDictWord(&src,labels,&prob, &nphones))<SUCCESS){
      CloseSource(&src);
      HRError(8013,"ReadDict: Dict format error in first entry");
      return(FAIL);
   }
   while(nphones>=0){
      word = GetWord(voc,labels[0],TRUE);
      if (labels[1]==NULL) labels[1]=labels[0];
      if (labels[1]->name[0]==0) labels[1]=NULL;

      NewPron(voc,word,nphones,labels+2,labels[1],prob);
      if((ret=ReadDictWord(&src,labels,&prob, &nphones))<SUCCESS){
         HRError(8013,"ReadDict: Dict format error");
         return(FAIL);
      }

   }

   return(SUCCESS);
}

主要涉及ReadDictWord、GetWord和NewPron函数。

1)ReadDictWord从字典文件中读取一行,“word phone1 phone2 phone3” 或者 “word2 [xxx] phone1 phone2 phone3 phone3 phone4”,并且把读到的信息保存在label[]、prob=1、nphones(有几个phone)中。如读到上述两行,那么label[] = ["word1" "word1" "phone1" "phone2" "phone3"], nphones值为3;或者label[] = ["word2" "xxx" "phone1" "phone2" "phone3" "phone4"],nphone4值为4.而label[1]就是当识别出这个发音序列时输出的字符。这里面隐藏了一个小细节,如果希望这个单词不输出/输出为空时,该怎么处理?也就是[xxx]里面没有元素,此时label[1]为NULL。

2)GetWord,是在上一步得到label[0]指示的单词后,查看当前词典中是否已经保存了相关信息。如果已经存在,就返回指向Word指针,否则添加进去再返回该指针。

到这里,我们有了什么呢?

voc中Word的位置,从发音字典中读取的Word对应的信息:label[] = ["word2" "xxx" "phone1" "phone2" "phone3" "phone4"],nphone4值为4,prob为1(通常情况)。

3)接下来看NewPron做了什么。

/* EXPORT->NewPron: add a pron to a given word - pron stored in phones */
void NewPron(Vocab *voc, Word wid, int nphones, LabId *phones, LabId outSym, float prob)
{
   WordPron *pron, **p;
   int i;

   pron = (WordPron *) New(&voc->pronHeap, sizeof(WordPron));
   if (nphones>0)
      pron->phones = (LabId *) New(&voc->phonesHeap,nphones*sizeof(LabId));
   else
      pron->phones = NULL;
   pron->nphones = nphones;
   for (i=0; i<nphones; i++)
      pron->phones[i] = phones[i];
   pron->outSym = outSym;
   pron->word = wid;
   if (prob>=MINPRONPROB && prob<=1.0)
      pron->prob = log(prob);
   else if (prob>=0.0 && prob<MINPRONPROB)
      pron->prob=LZERO;
   else
      pron->prob = 0.0;
   for (p=&(wid->pron), i=0; *p!=NULL; p=&((*p)->next), i++);
   pron->next = NULL;
   pron->pnum = i+1;
   *p=pron; 
   wid->nprons++;
   voc->nprons++;
}

不难看出,首先在内存块pronHeap上分配一个WordPron对象,然后又在phoneHeap分了nphones个LabId对象,将传递进去的phones序列信息保存在这上面。还将Word和WordPron互相关联。比如PronWord的有一个word的指针,它表示说这个发音对象对应的单词是哪一个。分配的WordPron对象要追加到传递进去的Word(DictEntry)的发音序列后面(由Next指定)。

这三个函数执行完,整体的效果是,在Vocab中添加/追加了对应的Word的发音(PronWord)信息。该信息保存在pronHeap中,它包含的音子对象保存在phonesHeap中。这样phonesHeap、WordHeap、pronHeap三个内存分配器之间的关系就如下图所示。

 它们之间的关系像是三角债,但是逻辑还是比较清晰的。

4)接着再重新读取字典文件,循环处理,直到文件结尾。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值