涉及的代码如下:
/* 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;
}