slm模块的最后一步是,生成拼音词表(lexicon):

$ make lexicon
./genpyt ../raw/dict.utf8 ../data/pydict_sc.bin ../swap/pydict_sc.log.utf8 ../data/lm_sc.t3g

这一步操作包括,建立一个支持不完全拼音的键树(Key Tree);将节点上的候选词,按语言模型中的unigram进行排序。

我 们这里还要说明一下,在训练语言模型时对多音字词的处理。在处理语料库时,理想的情况下,应该将不同的读音视为不同的词ID。但是要准确地进行拼音标注, 本身就十分困难,特别第一遍用FMM分词训练语言模型时。我们使用了下面的折衷方法:在词典文件dict.utf8中,我们对读音的常见程度进行了标记。 例如“长”,它的两个读音“zhang”和“chang”都比较常见,分别被标为1;而“她”(ta),另一个读音“chi”则十分罕见,因此将其标为- 1。在训练时,我们将所有遇到的“她”,都认为是“ta”,然后为“chi”的读音分配一定的概率,并为其分配一个新的词ID。

下面来看代码:

lexicon/pytrie_gen.cpp:

CPinyinTrieMaker::constructFromLexicon(fileName):
将 词典文件的每一行读取到buf中,然后调用parseLine()进行解析,返回词的buf(word_buf,UTF-8编码)、ID和发音(可能有多 个,每个发音有一个cost值,缺省为0)的集合--pyset。判断这个词的编码类型,0x0为GB2312,0x1为GBK,0x2为 GB18030。将m_Lexicon[id]赋值为word_buf。遍历这个词的所有读音,调用insertFullPinyinPair(t, wid),将拼音串为t的词wid,插入到trie树中。在处理完词典文件中的词之后,调用threadNonCompletePinyin(),扩展不 完全拼音的节点。
CPinyinTrieMaker::insertFullPinyinPair(pinyin, wid):
将pnode 设为trie树的根节点。遍历pinyin串,如果*p=='\''(即音节分割符),则将pnode节点的 m_bFullSyllableTransfer设置为true。调用insertTransfer(pnode, *p),在pnode的子转移节点(m_Trans )中查找是否已经存在*p,如果不存在,则创建一个新节点,累计到m_AllNodes,并赋值给m_Trans[c];最后返回*p对应的节点(依旧赋 值给pnode)。将pnode的m_bExpanded设为true,并插入到其自身的m_PrimitiveNodes中。遍历结束后,如果 pinyin串不是以'\''结尾,则插入一个音节分割符的节点。最后,调用insertWordId(),将wid插入到pnode的 m_WordIdSet中。

在将所有完全拼音节点插入完毕之后,trie树上的所有节点(累计在m_AllNodes),都被标记为已扩展,且其m_PrimitiveNodes中,只有其本身一个节点。
CPinyinTrieMaker::threadNonCompletePinyin():
遍 历m_AllNodes中的节点,如果这个节点还没有扩展过(也就是后来加入的不完全拼音的节点),则调用expandNode(pnode),扩展该节 点。如果这个节点的syllable prefix(音节前缀)长度>0,且这个前缀不是一个完整音节(即没有被注册到m_FullSyllables中),并且pnode的转发子节点 中没有'\'',则调用addNonCompleteSyllableTransfer()为pnode节点添加不完整拼音的转发节点。
CPinyinTrieMaker::addNonCompleteSyllableTransfer(pnode):
调用findSyllableChildren(),在pnode->m_PrimitiveNodes个节点的子树(m_Trans)中,递归查找同级的音节边界节点。如果pnode的音节前缀是"c"、"z"或"s",则查找时忽略'h'的子树。

例如我们有:

N0节点的primitive node就是自身,syChildren为N1和N2两个节点。

在m_StateMap 中查找syChildren(本例中是N1和N2两个节点的集合)对应的节点。如果不存在,则创建一个新的TNode节点pChildNoe,将它加入到m_AllNodes的尾部,其 m_PrimitiveNodes为syChildren,且其m_bExpanded为false。将syChildren中各节点的候选词,合并到新节点中。最后将pChildNode作为pnode节点中的'\''转移。即有:

再 如,/--c--a--'--h--a--n--'--(擦汗..)在这条路径上,当遍历到节点N3时,找到的音节边界节点为N5,它可以在m_StateMap中找到(当 初在insertFullPinyinPair()时,所有节点的primitive node都登记在m_StateMap中了)。因此N3节点上新加入的'\''转移,直接指向了尾节点。
CPinyinTrieMaker::expandNode(pnode):
遍 历pnode节点的 primitive nodes,将每个节点的转移节点(除'\''外),都并入到combTrans中。例如,要扩展上图中的N7节点(即c')节点,其primitive nodes为N1和N2,遍历它们各自的转移节点,并入到combTrans中。combTrans['h']中有两个节点,分别为上图的N3和 N4,这两个节点作为一个集合在以前没有遇到过,因此创建一个新TNode节点pChildNode,将它的primitive nodes设置为[N3, N4],且其m_bExpanded为false,然后加到m_AllNodes的尾部,并作为N7对'h'的转移节点,但它自己的转移节点尚为空,这个新节点以后也将被扩展。 combTrans['d']中只有一个节点--N6,且已经见过,将它直接作为N7对'd'的转移节点。

让我们看看最后扩展的结果:

c' 采 擦 才 嚓
c'd' 菜单
c'da' 菜单
c'dan' 菜单
c'h' 才华 擦汗
c'ha' 擦汗
c'han' 擦汗
c'hu' 才华
c'hua' 才华
ca' 擦 嚓
ca'h' 擦汗
ca'ha' 擦汗
ca'han' 擦汗
cai' 采 才
cai'd' 菜单
cai'da' 菜单
cai'dan' 菜单
cai'h' 才华
cai'hu' 才华
cai'hua' 才华
ch' 查 察
cha' 查 察
CPinyinTrieMaker::write(fp, psrt):
遍 历所有m_AllNodes中 的所有,计算每个节点保存到文件中的偏移量,记录在nodeOffsetMap中。遍历m_Lexicon中的词,记录要保存这些词所需要的空间。再次遍 历m_AllNodes,如果节点上的m_WordIdSet不为空,则用传入的psrt(CWordEvaluator,实际使用的是 CUnigramSorter),得到每个词的cost(即unigram的概率);然后对该节点上的候选词表进行排序。将节点输出到文件中。在遍历完 m_AllNodes之后,把词表m_Lexicon也输出到文件中。
最终生成的词表文件,保存到data/pydict_sc.bin中,我们可以通过CPinyinTrie类来访问它。CPinyinTrie中包括加载词表文件,释放内存空间,以及若干的transfer()方法:即输入一个拼音字符(或字符串),从当前状态(亦可能是root),转移新的状态节点。在后面介绍ime部分的代码时,会涉及到对这个类的使用。

至此,slm部分的代码就基本介绍完了,希望能对大家了解n-gram语言模型、和SunPinyin训练语言模型的代码细节,有稍许帮助。ime部分的代码中,细节和诡秘的地方很多,而我掌握得还很浅,要多花些时间来准备。因此,这个系列后面的更新会迟缓些,请见谅。
评论:

发表一条评论:
该日志评论功能被禁用了。

This blog copyright 2009 by yongsun