May 27

http://club.blogbeta.com/2010/05/13-sunpinyin

时间:2010年6月4日星期五晚上,19: 00开始
地点:奇遇花园咖啡馆(问路电话010-88320741)
地址:西直门北展北街9号华远企业号(华远地产)D座一层(地图

演讲题目:What’s new of SunPinyin 2.0

内容预览:

演讲人简介:
孙勇,SunPinyin项目的Lead Engineer之一,SunPinyin Mac Porting的主要维护者。
个人blog,http://yongsun.me,twitter: @yongsun

参与说明:无需报名,参加免费,点单AA

Tagged with:
Nov 08

从朋友那里得知,ibus-sunpinyin2已经进入gentoo官方库,感谢整个开发团队(特别是Kov),以及所有支持和关心我们的朋友们!:)

Tagged with:
Nov 04

感谢Kov Chai的超辛苦付出,以及William XueLeo Zheng等同学们的共同努力,ibus-sunpinyin 2.0发布了第一个release candidate!详情请参见:

http://groups.google.com/group/sunpinyin-developers/…

Tagged with:
Sep 12

我们刚刚为SunPinyin项目建立了一个google-group。虽然oso-inputmethod项目也有一个mail-list,不过是和其他opensolaris i18n/l10n项目share的。而且似乎国内的开发者或用户,还是更习惯和倾向使用google-group作为交流的途径。因此我们就创建了这个group。

SunPinyin的开发者会使用这个mail group来讨论开发相关的问题,同时用户也可以用这个mail group来报告发现的问题。(觉得再建一个sunpinyin-users的group还不是这么紧迫和必要。)大家也可以直接发邮件到sunpinyin-developers@googlegroups.com

期待您的加入!

Tagged with:
Sep 07

sunpinyin_event_flow

顺便再补充一句,为了将来能让ime-core编译为一个shared object,SunPinyin2开始使用自己的key mapping。这个文件列出了一些特殊键(function key),它们的keycode和X11 keysym是一致的,因此和scim和ibus的keysym也是一致的。其他的键,例如a-z/A-Z/0-9等和ASCII兼容的,可以直接传入(其实这和X11的keysym还是一致的)。再以外的keycode就不处理直接返回了。因此各位在给SunPinyin2做porting时,还需要编写一个简单的键盘事件的影射。

Tagged with:
Sep 06

SunPinyin2在设计和实现的过程中,希望能同时支持简体、繁体的词典及语言模型,支持不同的拼音方案,支持不同的输入风格(包括微软拼音和类似sogou或google拼音)。不同的输入风格,对应了不同的view的实现。(虽然目前SunPinyin2还没有重新实现微软拼音的输入风格——CIMIModernView。)

这些选项组合起来数目繁多,不过它们都是正交的,正好可以让我们使用policy based的实现方式。

我们定义了一个CSunpinyinProfile的模板类,它继承了ISunpinyinProfile这个公共接口类(否则我无法声明一个保存其指针的container),和它的三个模板参数,LanguagePolicy,PinyinSchemePolicy,InputStylePolicy。它的createProfile方法,调用policy类中的相关方法,来创建具体的一个view实例。为了避免创建policy的实例,并且更重要的是要共享policy的公共数据,policy类都是单件(singleton)。

LanguagePolicy负责初始化词典、用户词典和语言模型,以及根据这些资源创建CIMIContext类的实例。CSimplifiedChinesePolicy是一个具体的实现。另外还有一些方法,可以让你设置CIMIContext类实例的缺省属性。例如,enableFullPunctenableFullSymbolsetPunctMapping方法设置s_getFullPunctOp(被所有由该policy类创建的CIMIContext实例所共享)的标点符号影射。我们之前总结为shared & global的用户配置项。可以设想,用户在设置对话框自行定义标点符号的映射关系后,程序员只需要调用CSimplifiedChinesePolicy::setPunctMapping。这个配置项的更改会应用到所有现有的输入上下文(或会话),对输入上下文来说是透明的。

PinyinSchemePolicy负责创建拼音切分器的实例。CQuanpinSchemePolicyCShuangpinSchemePolicy是两个具体的实现。以CQuanpinSchemePolicy类为例,createPySegmentor方法创建了CQuanpinSegmentor的一个实例并返回。和CSimplifiedChinesePolicy类似,CQuanpinSchemePolicy也有一些shared & global的配置项,例如是否支持易混淆音和自动纠错。

InputStylePolicy负责创建CIMIView的实例。CClassicStylePolicy是一个具体的实现。它只是简单的创建一个CIMIClassicView的实例并返回。CSunPinyinProfile<…>::createProfile方法虽然也是返回一个view的实例,不过这个view是已经设置停当、可以工作的view。

CSunpinyinSessionFactory是一个singleton的工厂类,其createSession方法根据factory中policy的设定,调用某个CSunpinyinProfile实例的createProfile方法,创建一个具体的view。要添加新的语言支持或拼音方案,不仅要完成自己的那部分coding,还要记得编写相应的policy classes,并将支持的所有组合注册到CSunpinyinSessionFactory中(没办法,在这个地方就只能穷举了)。

因为我们最后expose给外部的是一个view的实例,所以如果用户的配置涉及到对policy的切换(我们之前总结为non-shared but global的配置项),以前创建的输入上下文(即view),就面临一个抉择。要么保持不变,继续使用其目前的输入风格;要么要重新创建,discard掉以前上下文的状态信息(例如preedit/candidates)。通常,我们对某个平台的移植,都是创建一个包含view的会话类,这个会话类适配该平台的接口,并将输入事件filter给view。那么我们的这些会话类是如何知道发生了non-shared but global选项的改变呢?

CSunpinyinSessionFactory类提供了一个简单的方法,updateToken/getToken。当平台相关的会话类调用CSunpinyinSessionFactory类创建一个view的实例,同时应该调用getToken得到一个token(口令)。当会话类得到焦点的时候,它可以调用getToken再次得到一个token,比较与自己持有的这个token是否一致。如果不一致,就表明发生了non-shared but global选项的更改。与此相对应地,如果用户对non-shared but global的选项发生了修改,程序员应该要调用updateToken去更新factory当前的token。

这个方式有些丑陋,稍微改观一点的方法可能是,我们再加入一个抽象层次,例如CSunpinyinSession,然后它用signal/slot的方式connect到factory的reset_session信号。另一个问题,配置项还是比较分散,如果要编写一个集中的CSunpinyinOption类的话,就会有很多duplicated的代码。我对上面这些问题,还没有想得很清楚,也非常欢迎大家多提宝贵意见!:)

行文至此,我的“What’s new in SunPinyin2”系列也就暂时告一段落了。希望对William、以及SunPinyin2的移植工作有所帮助。我还会继续竭尽全力,投入到SunPinyin2的开发和移植工作中 :)

Tagged with:
Sep 05

原先的SunPinyin没有用户词典,只是通过User History Cache纪录了用户近期输入的bi-gram信息。如果一个bi-gram,其概率比系统词典的某个uni-gram要低,这个bi-gram的组合就不会出现在用户的候选列表中。例如,虽然用户频繁的输入钥匙,但是它依然很难出现在候选列表中,因为“要是”这个unigram的概率要更高。如果“钥匙”出现在候选中,一定是第一候选,因为它是作为一个最佳候选句子来呈现给用户的。如果用户选择了“要是”,那么“钥匙”又不知道要过多久才能出现了。这个缺陷也是广受大家诟病的地方。其实,SunPinyin原先的架构是支持将用户自定义词放到lattice上进行搜索的

我们在SunPinyin2中,通过sqlite3来实现用户词典,目前我们的用户词典支持记录<=6个的字符。

CUserDict::_createTable/_createIndexes:

这两个私有方法尝试创建一个table和index。这个表的结构是,首先词的ID(从1开始的)和长度,接下来是6个声母,然后是6个韵母,再其次是6个声调。然后我们为长度+6个声母建立了一个索引。sqlite的query对搜索条件和index有一些限制,要求条件的次序和index的次序相同,并且一旦某个条件是非相等性的(例如>),则该条件及其之后的条件都无法用index来加速。另外,每个from子句只能使用一个index。

因为我们经常会有不完整拼音的查询,或者换句话说用户经常会输入不完整拼音,所以table和index就被设计成了这个样子。

CUserDict::addWord (syllables, word):

使用了sqlite中的prepared statement来进行插入。最后,在插入成功的情况下,调用sqlite3_last_insert_rowid得到上一次插入记录的row id,加上一个offset并返回。如果失败,就返回0。

CUserDict::removeWord (wid):

这个相对简单,按照wid将记录从数据库中删除。

CUserDict::getWords (syllables, result):

这个方法将syllables在数据库中对应的记录,放到result中。我们返回的词id类型,和系统词典的保持一致,都是CPinyinTrie::TWordIdInfo。这里再解释一下,用户词典的最大容量为什么是6万多个。这个就是由于CPinyinTrie::TWordIdInfo的m_id的长度决定的,是2^18-1。

我们按照sqlite的限制,小心翼翼地组织好查询的where子句,查询,然后迭代结果,填充results,最后返回。

CUserDict::operator [] (wid):

这个方法也相对简单,就是返回wid对应的字符串。因为sqlite只支持utf8和utf16,而且utf16还要使用一套不同的接口,所以我们还是使用utf8作为数据库内的字符串编码。而sunpinyin-ime要求的是ucs-4编码的字符串,所以还要进行一个编码转换。同时把(wid, wstr)这个pair加入到m_dict中。wstr是动态分配的,由m_dict在析构时释放。这里还利用了这样一个事实,std::basic_string是COW的。我们在m_dict中保存的其实是wstr的拷贝,不过这个拷贝和wstr共享同一个C string的buffer。

接下来,我会介绍SunPinyin2中另一个比较复杂和tricky的部分——用户配置(imi_options.h),这一部分到目前还没有完全定型,非常欢迎大家多提宝贵意见。

Tagged with:
Aug 31

原先SunPinyin是使用CBone/CSkeleton来组织search lattice的,参见sunpinyin代码导读(九)。每一个Bone对应一个syllable(或者严格的说,是一个segment),而CSkeleton是CBone的双向链表(std::list)。这个结构不太适合对模糊切分的支持。因此在SunPinyin2中,lattice不再以syllable为单位了,而是以单个的拼音字符为单位。如下图所示:

sunpinyin2_lattice_search

我们定义了一个CLattice的类,表示整个的search lattice,其对应于原来的CSkeleton。将每个column称为一个CLatticeFrame,对应于原来的CBone/CBoneInnerData。而TLexiconStateTLatticeState和以前的差别不大,只是把位置信息从iterator换成了index。另外为了支持用户词典,在TLexiconState加入了一些相应的字段,例如m_wordsm_syls

CIMIContext类的主要入口是buildLattice (segments, rebuildFrom, doSearch)。rebuildFrom这个参数值其实就是IPySegmentor::updatedFrom() + 1,因为在lattice上,拼音字符串是从index 1开始的,而在切分器中是从0开始的。buildLattice遍历segments(由IPySegmentor::getSegments所返回的),跳过rebuildFrom之前的那些segments,然后调用_forwardXXX方法,build每个目标frame上的lexiconStates。直到segments的结束或者超过一定长度,调用_forwardTail处理结尾。然后,在doSearch为true的情况下,调用searchFrom进行搜索。

CIMIContext类中的_forwardXXX方法,和原来的forwardXXX方法大同小异。_forwardSingleSyllable中对用户词典做了一些额外的处理。因为我们现在的用户词典是基于sqlite实现的,不是一棵trie树,所以和基于trie树的系统词典处理起来有所不同。例如,用户词典中有“测试拼音”这个词,当用户输入到ceshipin的时候,从数据库中得到的词的列表是空的,且对系统词典(CPinyinTrie)的transfer也会返回一个空节点。但这种情况下,我们不能drop掉ceshipin这个音节序列,还是要把它加入到目标latticeFrame的lexiconStates中

CIMIContext::searchFrom也和前作基本相同,都是通过frame上的lexiconStates,调用_transferBetween方法来构建latticeStates,同时调用语言模型进行句子的评分。最后调用_backTraceBestPath,把最佳路径标识出来。稍有不同的是,searchFrom对fuzzyForwarding进行了一些处理,收窄了易混淆音节的搜索范围并降低了其初始的评分

其他的方法,例如_transferBetween, getBestSentence, getCandidates都和之前的实现大体相同。cancelSelectionmakeSelection得到了简化。memorize方法加入了将<=6个字符的短句(其中含有用户选择的),作为用户词条保存的功能。

另外CIMIContext有两个助手类,CGetFullPunctOpCGetFullSymbolOp,用来处理标点符号和字符的全角转换。这两个类也是可配置的,并且CIMIContext的所有实例都share同一份拷贝。因此,也属于shared & global的配置选项。

新版本的CIMIContext,在将拼音切分剥离之后,其职责更加明确和清晰;采用新的数据结构之后,逻辑也更简单和直观。代码行数也由原来的1.5k下降到0.6k;同时可配置性也得到了提升。

下一篇我会介绍一下用户词典的实现。

Tagged with:
Aug 30

如在上篇介绍的那样,SunPinyin原先是借助CPinyinTrie来进行拼音切分的,因为CPinyinTrie是一棵前缀树,因此切分是按最大前向匹配的方式进行的。还有一些额外的处理,例如fangan会被切分为fan’gan,但是dier是被切分为die’r的(我们也因此收到了很多用户的反馈)。在SunPinyin2中,我们依然借助一个trie树来进行音节切分。不过这个tire树是由所有有效音节组成的一棵后缀树。因此,我们的切分原则是以最大后向匹配为基础的。

简单的后向匹配还是有一些问题,例如zhonge(中俄),会被切分成zh’o'n’ge,如果你留意FIT或者ibus-pinyin,就会发现他们都有上述的问题。那这个问题在SunPinyin2中的全拼切分器是如何解决的呢,用户在输入g的当下,切分结果是zh’o'n,加入g之后,segments发生了向前的合并,那么我们就把’g'换成’G'(实际的处理是高位置1)放到内部匹配的buffer中。那么下次用户在输入e的时候,最大后向匹配zhonGe的结果,就会产生正确的结果了。还有一种可能性要处理,例如feng,fe不是一个合法的音节,所以在输入n的当下,切分的结果是f’e,在输入n之后,音节发生了向前的合并,因此N被加入到内部匹配的buffer中,变为feN,但是再输入g时,最大后向匹配却无法返回正确的切分结果了。所以,所以我们还要尝试在某些情况下,将N临时改为n再match一次,如果得到的segment长度是原先的长度+1,就接受这个新的结果,否则再把n改回到N。

我们前面提到了,SunPinyin2使用一棵后缀trie树进行拼音切分的。trie树可以通过逐级二分来实现(如果是内存中的,可以是hash或map)。为了尽可能地保障切分器的效率和性能,我们使用了Double Array Trie(DAT)的方式。DAT的优势在于,可以直接load/mmap到内存中,并且查询子节点的复杂度是O(1)的,缺点是后续的插入和删除比较复杂。因此十分适合作为构造静态词典的方法。在SunPinyin2中,DAT的构造是用python实现的。我们定义了一个C++的模板类CDATrie来访问DAT,其中match_longest (first, last, length)是最核心的方法。读者有兴趣可以去查看相关的代码和资料,这里就不赘述了。

接下来我们看看CQuanpinSegmentor的实现细节。CQuanpinSegmentor有两个助手类,CGetFuzzySyllablesOpCGetCorrectionPairOp。这两个助手类分别用来得到易混淆音(例如zhan -> zan/zang/zhang),和纠错结果(例如ign -> ing)。这两个类都是可配置的。并且所有的CQuanpinSegmentor实例,应该共享相同的助手类的实例。这种可配置项,我们称为shared & global的,在用户修改用户配置的时候,不会影响已经创建的那些会话(sessions)。我们后面还会看到,有些配置项是non-shared  but global的,对这种配置项的更改,需要重新创建已存的那些会话。这是后话,暂且不表。

这两个助手类比较直观,这里也就不赘述了,读者可以参考相关的代码。

CQuanpinSegmentor的核心是_push方法。目前的实现中,deleteAt/insertAt都是用_push方法来实现的,也就是把修改位置后面的字符串重新push了一遍。这个实在是比较粗笨,应该还有一定的改进空间。不过因为用户输入的拼音字符串长度有限(缺省的设置是512个字符),性能目前还是可以接受的。

_push方法首先将传入的字符ch,push到m_pystr中,然后调用m_pytrie的match_longest (m_pystr.rbegin(), m_pystr.rend(), l)方法进行后向匹配,匹配的结果主要有以下几种情况:

  1. l == 0,表明ch不是一个有效的音节,根据字符的特征选定一个seg_type,创建一个长度为1的segment并push到m_segs中。
  2. l == 1,可能是一个新的segment,但是如果是上面类似feNg这样的情况,就修改上一个segment的属性。否则,创建一个新的长度为1的segment,并push到m_segs中。
  3. l == m_segs.back().m_len + 1,也就是说ch可以扩展上一个segment,那么修改上一个segment的属性。
  4. 其他情况,一种是 l > m_segs.back().m_len + 1,例如zhon+g,l = 5,但是上一个segment(n)的长度是1。另一种情况是 l < m_segs.back().m_len + 1,例如die+r,l=2,但是上一个segment(die)的长度是3。这两种情况都会涉及合并或拆分现有的segments。并且在此过程中,可能会引起进一步的合并和拆分动作。因此我们用一个while循环来追赶,直到达到一个既有的音节边界为止。

最后,所有的分支都会跳到RETURN,进行post-editing的操作,这里我们只有一个post-editing的action,就是处理易混淆音。

SunPinyin2中的双拼切分器是由William来实现的,期待他会写一篇文档来总结一下他的实现细节。我将在下一篇介绍对CIMIContext类的改造。

Tagged with:
Aug 29

在原先SunPinyin的ime部分中,拼音切分是借助CPinyinTrie来完成的,同时紧密地耦合在CIMIContext类中(注:CIMIContext类是输入引擎的核心,主要负责根据拼音字符串建立一个search lattice并进行搜索,并处理用户的user selection,以及获取某个位置开始的candidates,参见sunpinyin代码导读(九))。

这为我们添加新的拼音方案(例如双拼)带来了极大的不便,所以在2.0中,我们将拼音切分从CIMIContext中剥离了出来,定义了一个统一的接口——IPySegmentor。要为SunPinyin2添加一个新的拼音方案,就是要实现一个IPySegmentor的接口(当然,如果该方案无法和全拼一一对应,例如粤拼,还需要自行定义自己的lexicon,参见上篇)。

让我们来看看这个IPySegmentor接口:

struct IPySegmentor
{
    enum ESegmentType
        {SYLLABLE, SYLLABLE_SEP, INVALID, STRING,};

    struct TSegment {
        std::vector<unsigned>   m_syllables;
        unsigned                m_start        : 16;
        unsigned                m_len          : 8;
        ESegmentType            m_type         : 8;
    };

    typedef std::vector<TSegment>  TSegmentVec;

    virtual TSegmentVec& getSegments () = 0;
    virtual wstring& getInputBuffer () = 0;
    virtual const char* getSylSeps () = 0;

    virtual unsigned push (unsigned ch) = 0;
    virtual unsigned pop () = 0;
    virtual unsigned insertAt (unsigned idx, unsigned ch) = 0;
    virtual unsigned deleteAt (unsigned idx, bool backward=true) = 0;
    virtual unsigned clear (unsigned from=0) = 0;

    virtual unsigned updatedFrom () = 0;
    virtual void locateSegment (unsigned idx, unsigned &strIdx, unsigned &segIdx) = 0;
};

拼音切分器的任务是,将用户的输入切分成TSegment(s),例如用户的输入是“xi’anshiAi”,那么得到的segments是,[(xi), s:0, l:2, SYLLABLE], [(), s:2, l:1, SYLLABLE_SEP], [(an), s:3, l:2, SYLLABLE], [(shi), s:5, l:3, SYLLABLE], [('A'), s:8, l:1, STRING], [('i'), s:9, l:1, INVALID]。之所以区分INVALID和STRING,是为了preedit可以考虑用不同的颜色来显示它们。

如果切分器支持易混淆音(例如z-zh, c-ch, s-sh),一个segment可能会有多个syllables。例如,对上例中的shi,对应的segment为[(shi, si), s:3, l:3, SYLLABLE]。注意,CIMIContext会将m_syllables的第二个及之后的音节视为是易混淆音,而在搜索的宽度和句子评分方面有所降低。

细心的读者可能发现了,在上面的例子中,start的信息好像是冗余的。但如果切分器支持模糊切分,例如fangan -> fang’an, fan’gan,那么TSegment的m_start就有用武之地了。并且隐含要求,TSegmentVec中的segments是按照m_start从小到大排序的。不过,现有的CIMIContext还需要一些改进,需要根据搜索的最优路径来返回正确的切分路径。

下面我们来看一看IPySegmentor的主要接口。因为用户输入、删除或编辑,是以单个字符为单位进行的,我们的接口也是按照这种风格设计的。例如push,是指用户在尾部添加一个字符,这个方法的返回值是,指示segments从拼音字符串的什么位置发生了更新。这个更新值非常重要,考虑下面的示例,

Input | Segments | UpdatedFrom
------+----------+------------
z     | z        | 0
zh    | zh       | 0
zho   | zh, o    | 2
zhon  | zh, o, n | 3
zhong | zhong    | 0

可以看到,在用户输入g时,原本3个segments被合并成了一个,且返回值为0,表示segments从拼音字符串0这个位置发生了更新。这个返回值,和updatedFrom方法返回的是一致的,只是为了client调用方便而添加的。之所以需要这个updatedFrom值,是出于性能的要求:CIMIContext希望在用户输入/编辑之后,只重新build产生更新部分拼音字符串对应的lattice,而不需要从新build整个lattice。

其他的几个online editing方法,也是类似的。pop指从尾部删除一个字符,insertAt/deleteAt分别表示从中间位置插入/删除一个字符。之所以区分push/insertAt,或pop/deleteAt,是因为push/pop可能有很多优化的空间。locateSegment返回拼音字符串的某个位置(idx),其所在音节的起始位置(strIdx),和segments vector中的下标(segIdx)。getSegments返回一个TSegmentVec的引用,即切分的结果。getInputBuffer返回用户输入的原始数据,getSylSeps返回这个切分器支持的音节切分符。

目前SunPinyin2中实现了两个切分器,分别为CQuanpinSegmentorCShuangpinSegmentor。其中CQuanpinSegmentor支持易混淆音、自动纠错等功能,且这些功能都是可配置的,但尚不支持模糊切分。我们将在下一篇介绍CQuanpinSegmentor的实现。

Tagged with:
preload preload preload