HTK的Network把所有的NetNode对象chain,并重新排序

涉及的代码如下:

   /* First disassemble wnHashTab and link to end nodes as necessary */
   AddInitialFinal(lat,net,hci->xc); 

   for (i=0; inn; i++) 
      for (pInst=(PronHolder*)lat->lnodes[i].sublat; pInst!=NULL; pInst=pInst->next) {
         if (pInst->nstart>0)
            AddChain(net,pInst->starts);
         AddChain(net,pInst->chain);
         AddChain(net,pInst->ends);
      }

其中AddInitialFinal函数将net的initial和final节点连接到Lattice的SENT_START和SENT_END节点。看一下它的代码,刚开始通过for循环计数有多少个初始节点,赋值给ninitial;方法是遍历所有LNode,看其pred(指向该节点的所有LArc列表)是否为NULL,如果是,则表明该节点是初始节点。这时它还是在Lattice网络上,要把初始信息附着到Network上。识别的phone net有个数据项initial,它是一个NetNode,表明音子网络的初始节点,它的initial.link指向Lattice的初始节点(在我们的例子中就是SENT_START)的发音信息的starts节点。如果对Lattice的LNode节点是如何进行音子扩展的比较熟悉,就容易理解这句话的意思。也就是在LNode音子扩展过程中,sublat指向PronHolder对象,它里面的音子是包装在NetNode里的。

/* AddInitialFinal: Add links to/from initial/final net nodes */
static void AddInitialFinal(Lattice *wnet, Network *net,int xc)
{
   PronHolder *pInst;
   NetNode *node;
   LNode *thisLNode;
   int ninitial = 0;
   int i,type;

   /* Simple case */
   for (i=0; i < wnet->nn; i++)
      if (wnet->lnodes[i].pred == NULL) 
         for (pInst=(PronHolder*)wnet->lnodes[i].sublat; pInst!=NULL;pInst=pInst->next)
            ninitial++;
   if (ninitial==0) 
      HError(8232,"AddInitialFinal: No initial links found");
   net->initial.type = n_word;
   net->initial.tag = NULL;
   net->initial.info.pron = NULL;
   net->initial.nlinks = 0;
   net->initial.links = (NetLink *) New(net->heap,ninitial*sizeof(NetLink));
   for (i=0,thisLNode=wnet->lnodes; inn; i++,thisLNode++) {
      if (thisLNode->pred != NULL) continue;
      for (pInst=(PronHolder*)thisLNode->sublat; pInst!=NULL;pInst=pInst->next) {
         if (xc==0) node=pInst->starts;
         else if (pInst->nphones!=0) node=pInst->lc[0];
         else node=FindWordNode(NULL,pInst->pron,pInst,n_word);

         net->initial.links[net->initial.nlinks].node = node;
         net->initial.links[net->initial.nlinks++].like = 0.0;
      }
   }
   
   net->final.type = n_word;
   net->final.info.pron = NULL;
   net->final.tag = NULL;
   net->final.nlinks = 0;
   net->final.links = NULL;
   for (i=0; i < wnet->nn; i++) {
      thisLNode = wnet->lnodes+i;
      if (thisLNode->foll != NULL) continue;
      for (pInst=(PronHolder*)thisLNode->sublat;
           pInst!=NULL;pInst=pInst->next) {
         if (xc==0 || pInst->nphones==0)
            node=FindWordNode(NULL,pInst->pron,pInst,n_word);
         else {
            type = n_word + pInst->fc*n_lcontext; /* rc==0 */
            node=FindWordNode(NULL,pInst->pron,pInst,type);
         }

         if (node->nlinks>0)
            HError(8232,"AddInitialFinal: End node already connected");
         node->nlinks = 1;
         node->links = (NetLink *)New(net->heap,sizeof(NetLink));
         node->links[0].node = &net->final;
         node->links[0].like = 0;
      }
   }
}

经过这一步后,net的初始、结尾节点都指向了正确的NetNode节点了。

然后,我们之前说过,HTK有个哈希表是用来保存word节点信息的,也就是NetNode的type为n_word(4),上面代码的第一个for循环就是把所有的词节点chain起来。怎么理解这句话?

就是NetWork的chain指针按顺序从wnHashTab中找到的NetNode对象连接起来。(有一点不确定,就是wnHashTab中的词类型(n_word)NetNode的chain指针保存了什么信息?:它保存的了Lattice中LArc信息,也就是该词的下一个连接节点,而它的inst保存了NetInst对象,与token相关。)

前面有Blog解释过,如何从Lattice的LNode扩展为phone 网络的NetNode,就是它的sublat指向PronHolder对象,而PronHolder对象内有starts、ends、chain指针,它们类型都是NetNode*。ExpandWordNet函数就有代码负责将LNode中的phone信息包装为n_hmm类型的NetNode,并且starts指向开始的节点,ends指向结尾,中间的信息保存在chain指针列表中。比较特殊的是,系统自动为Dictionary中的每个入口DictEntry后面添加了sp,ends指向的就是这个sp NetNode。这里的sp就是n_tr0节点,也是n_wd0节点。它们分别是什么意思,在其他blog有解释。

还会根据Lattice的LArc内容,连接LNode,从而将不同LNode里的PronHolder的starts、ends连接起来,这就是跨词连接函数的功能。

接着的for循环,把非词节点(也就是HMM)节点chain起来。没有考虑顺序关系。但是在每个节点的内部,连接信息都已经包含了。

看一下AddChain函数到底在做什么!

static void AddChain(Network*net, NetNode *hd) 
{
   NetNode *tl;

   if (hd == NULL) 
      return;
   tl = hd;
   while (tl->chain != NULL)
      tl = tl->chain;
   tl->chain = net->chain;
   net->chain = hd;
}

 画图可以很好的理解这个函数在做什么。

鼠标画的,有点丑,凑合着看吧。 

上面的代码显示,接着就是根据lat对象中lnodes列表的顺序,把所有节点的chain指针连接到net的chain指针前面。我们之前说过的,LNode的发音信息都保存在PronHolder对象中,包括stars、chain和ends。看,这段代码就涉及到这三个指针了。

接着往下看,已知net的chain把n_hmm和n_word的NetNode都chain起来了,它们之间还是连接的次序关系,需要对它们进行重排序,以便识别时指导token传递路径。

  /* Count the initial/final nodes/links */
   net->numLink=net->initial.nlinks;
   net->numNode=2;
   /* now reorder links and identify wd0 nodes */
   for (chainNode = net->chain, ncn=0; chainNode != NULL; 
        chainNode = chainNode->chain,net->numNode++,ncn++) {
      chainNode->inst=NULL;
      chainNode->type=chainNode->type&n_nocontext;
      net->numLink+=chainNode->nlinks;
      /* Make !NULL words really NULL */
      if (chainNode->type==n_word && chainNode->info.pron!=NULL &&
          net->nullWord!=NULL && chainNode->info.pron->word==net->nullWord)
         chainNode->info.pron=NULL;
      /* First make n_wd0 nodes */
      if (chainNode->type & n_hmm)
         for (i = 0; i < chainNode->nlinks; i++)
            if ( IsWd0Link(&chainNode->links[i]) ) {
               chainNode->type |= n_wd0;
               break;
            }
      /* Then put all n_tr0 nodes first */
      for (i = 0; i < chainNode->nlinks; i++) {
         /* Don't need to move any initial n_tr0 links */
         if (chainNode->links[i].node->type & n_tr0) continue;
         /* Find if there are any n_tr0 ones to swap with */
         for (j = i+1; j < chainNode->nlinks; j++)
            if (chainNode->links[j].node->type & n_tr0) break;
         /* No, finished */
         if (j >= chainNode->nlinks) break;
         /* Yes, swap then carry on */
         netlink = chainNode->links[i];
         chainNode->links[i] = chainNode->links[j];
         chainNode->links[j] = netlink;
      }
   }

这段代码就是对net的所有节点对象进行排序。

这个排序过程,就是将来识别时,token的传递过程。因此节点的类型就很重要,比如它是hmm的“进入”还是“出口”节点,是null的空节点,还是word节点。不同的节点类型,在令牌传递时做的操作是不同的。

比较常规的有两类,一是n_hmm=2,代表的是hmm的节点;二是n_word=4,代表词结尾节点或null节点。还有一类是n_wd0=1,表示Exit token到达词节点。而n_tr0=4,代表Tee hmm模型节点,就是入口直接连到出口状态,就是在令牌传递时可以跳过该节点的。

   n_unused,            /* Node Instance not yet assigned */
   n_hmm=2,             /* Node Instance represents HMM */
   n_word=4,            /* Node Instance represents word end (or null) */
   n_tr0=4,             /* Entry token reaches exit in t=0 */
   n_wd0=1,             /* Exit token reaches word node in t=0 */

我的理解,n_wd0节点,是词边界的节点。如果A节点连到n_word,它当然是n_wd0;如果它连到的是比如sp节点,且sp后面连到n_word节点,那么A节点也是n_wd0类型的。因为sp节点是Tee hmm模型,Tee 模型的定义就是它可以从开始状态可以直接转移到结束状态。但是如果sp节点后面没有直接连到n_word类型的节点,比如sp后接“s”获取“y”等n_hmm类别hmm,那么A节点就不是n_wd0类型。

一句话总结,如果某个NetNode节点类别是n_wd0,那么它是可以输出完整词的。仔细看下面的代码就明白了。

static Boolean IsWd0Link(NetLink *link)
{
   int i;
   NetNode *nextNode;

   nextNode = link->node;
   if ((nextNode->type&n_nocontext) == n_word)
      return TRUE;
   if (nextNode->type & n_tr0) {
      for (i = 0; i < nextNode->nlinks; i++)
         if (IsWd0Link(&nextNode->links[i]))
            return TRUE;
      return FALSE;
   }
   else
      return FALSE;
}

 

你可能感兴趣的:(语音识别,HTK,chain模型,NetWork)