SunPinyin-2.0模糊音节切分的实现

模糊切分,即根据上下文,将有歧义的拼音字符串进行自动切分,例如,将fangan切分为fang'an或fan'gan。这是许多现代拼音输入法都具备的功能。SunPinyin的ime-core本身具有搜索多种切分组合的能力,只要在buildLattice时,保证传入的segments是按照起始位置(m_start)排好顺序的即可。

那么首先要解决的问题就是,根据得到的最佳句子,反查到对应的切分序列。例如,最佳句子是“我的方案获得通过”,可以推得“wo'de'fang'an'huo'de'tong'guo”。

我们为TLexiconState加入了m_seg_path的成员,用来记录这个LexiconState对应的切分路径,例如我们有一个lexiconState对应是上例中的fang'an,其切分路径是[4, 8, 10]。然后,为CLatticeState加入了m_pLexiconState成员,用来记录之前transfer时所引用的那个lexiconState。这样,在backTrace最佳句子的时候,就可以得到对应的音节了。由于易混淆音(即z<->zh)的存在,一个seg_path可能对应多个syllable_paths(m_syls);但是最后在存入用户词典时,必须要知道真实的syllables,所以没有采用TLexiconState结构包含一个seg_path、和多个syllable_paths的方案。

接下来,就是该如何产生模糊切分的了。我们为CPinyinData类加入了一些额外的table,这些table都是使用pinyin_data.py脚本生成的。包括fuzzy_finals_map,这是为处理型如xian->xian/xi'an的模糊切分的;以及fuzzy_pre_syllables和fuzzy_pro_syllables,分别代表可能产生切分歧义的前一个音节和后一个音节。经过pinyin_data.py的筛选,发现只有“r/g/n”三个声母,可能作为前一个音节的结尾,或后一个音节的声母。

接下来,我们为CQuanpinSegmentor加入了一个新的helper functor,CGetFuzzySegmentsOp。这个助手类的输入是主切分器(即基于double-array trie的、改良的、最大后向匹配算法)所得到的segments;输出是,其对应的模糊切分segments。这个寻找模糊切分的过程,和切分器的主过程是平行的。但是,我们并不是简单的每次输入都从头到尾扫描一遍。

首先,根据主切分序列的最后一个segment(记为seg),invalidate那些受影响的fuzzy segments。在我们目前的实现中,fuzzy_segs中的模糊切分都是成对出现的,我们从后向前一对一对的进行筛选,只有当某一对的右边界(r),小于或等于seg的起始位置,才能够保留下来。然后,我们仅需要对主切分的最后一个切分(如果是xian的情况),或最近两个切分(如果是fangan的情况),进行处理。然后小心地调整好updatedFrom的值,并返回给CQuanpinSegmentor::_push()。

因此,如果输入xian,会生成xian和xi'an的两种切分,如果继续输入一个a,则会得到xia'na和xian'a,而不会包括xi'an'a这个切分了。主切分器的最后一个segment(即na),会先将xi'an给invalidate掉;然后辅助切分器会将xian'a加入到fuzzy_segs中。从这一点来说,我们和google和sogou输入法的处理仍然有所不同,他们都会保留xi'an'a这个切分。

我个人的感觉,sogou和google输入法每次在追加或删除一个拼音字符时,都是会从头进行一遍扫描处理;其间处理了各种情况,包括易混淆音,自动就错,和模糊音节切分等。而SunPinyin的push/pop操作,尽可能少的对拼音字符串进行扫描和匹配,应该来说效率要高一些。而且,我感觉目前的这种实现方式,也基本满足大家的需要了。:)

返回到_push方法之后,如果设置了易混淆音,就对m_fuzzy_segs的最后两个segments,加入易混淆音。无论之前是否已经加入过易混淆音,CQuanpinSegmentor::_addFuzzySyllables都会先将seg.m_syllables resize为1,即清空了之前的易混淆音。

最后,在getSegments()时,将m_fuzzy_segs和m_segs合并到m_merged_segs中,并按照m_start排好顺序,返回给外层的调用者。我们今后可能会改进这部分,其实m_segs和m_fuzzy_segs都是有序的,只要让CIMIContext::buildLattice可以按照m_start的顺序,同时迭代这两个有序的vector就可以了。

还有其他的一些辅助的修改,例如,CIMIContext::getCandidates的循环退出条件不同了,导致我们现在迭代的次数会明显增多了,需要想一些更好的解决方法。