天天看點

SunPinyin代碼導讀-IME部分

0. 綜述 這一部分主要介紹Sunpinyin中ime部分的代碼。

要建構ime部分的代碼,可以執行下面的操作:

$ ./autogen.sh --prefix=/usr --enable-debug --enable-gtkstandalone

$ make

使用--enable-gtkstandalone選項,會生成一個不依賴于輸入法架構的示範程式,位于[url=]wrapper/gtk_standalone[/url]目錄中。你可以通過這個示範程式來了解或調試Sunpinyin輸入法的代碼。我們後面的系列,也會從這個示範程式入手。

同時,在configure.ac中,--enable-cle是預設打開的(你可以通過--disable-cle來關閉這個選項),是以在執行make時,還同時會為iiimf-cle(Chinese Language Engine)建構一個輸入法子產品,執行make install,會把它安裝到/usr/lib/iiim/le/cle的目錄下。如果要在Mac OS上編譯,需要export LDFLAGS=-liconv。

Sunpinyin支援兩種輸入風格,“經典風格”和“及時轉換風格”。“及時轉換風格”和微軟拼音的風格類似,使用者在輸入拼音時,隻在preedit區域中顯示目前的最佳句子,可以通過左右鍵,對其中任意不滿意的部分進行編輯(即使用者選擇,user selection)。

SunPinyin代碼導讀-IME部分
SunPinyin代碼導讀-IME部分

“經典風格”和紫光拼音(以及後來的搜狗拼音、Google拼音)的風格類似。使用者在輸入拼音時,preedit區域中顯示的是輸入的拼音串(或使用者選擇),候選區中顯示目前的最佳句子,以及拼音串開始部分的拼音可能得到的詞和字。但這種風格下,第一次使用者選擇隻能從拼音串的起始部分開始。

SunPinyin代碼導讀-IME部分
SunPinyin代碼導讀-IME部分

Sunpinyin采用了view/window-handler的結構。上述的兩種風格,在代碼中分别對應于兩個視圖(view)類:[url=]CIMIModernView[/url]和[url=]CIMIClassicView[/url]。所謂window-handler,就是view的一個callback類。[url=]ime/wrapper[/url]目錄下有五個window-handler的實作,[url=]cle[/url], [url=]scim[/url], [url=]beos[/url], [url=]macos[/url]和[url=]gtk_standalone[/url]。要将Sunpinyin移植到其它輸入法架構或平台,主要的工作就是為其編寫一個wrapper。

下一節,我們會介紹ime部分的概念模型(concept model)。

1. 概念模型 讓我們來看看ime部分的概念模型(concept model):

SunPinyin代碼導讀-IME部分

插圖1

Static SLM,Lexicon: 我們在前面介紹slm部分的代碼時已經了解過了。通路統計語言模型和拼音詞表的代碼,分别位于[url=]ime/src/slm[/url]和[url=]ime/src/lexicon[/url]目錄中。這裡就不多介紹了。

View :由Window-Handler接收使用者的輸入,通過回調(call-back),将pre-edit string和candidates傳回給Window-Handler來進行顯示。使用者的按鍵輸入,可能會對目前的拼音串兒進行添加、删除、插入等操作,然後将拼音串兒傳遞給Syllable Segmentor進行音節切分。在使用者送出(commit)候選句(或詞)之後,它要更新History Cache來記錄使用者最近的輸入。如果使用者通過數字鍵對候選進行了選擇,它要通知Lattice Builder對使用者選擇進行相應的處理(給使用者選擇的詞一個很高的評分。并不是将使用者選擇之外的詞及由它們轉移得到的狀态從Lattice上删除,因為使用者後面的編輯可能會取消該選擇,保留這些資訊可以節省重建構Lattice的工作量)。

Syllable Segmentor :使用Lexicon,對拼音串進行切分。因為切分可能存在歧義,例如mingan可以切分為ming'an或min'gan,故而生成一個Syllable Graph,将其傳遞給Lattice Builder。(不過,目前的實作尚不支援模糊音節切分,使用者需要通過輸入'鍵來明确指定音節邊界,是以Graph目前隻是一個List。就下面的示例來說,得到的切分結果為xian'shi'min'gan。)

SunPinyin代碼導讀-IME部分

插圖2

Lattice Builder :對每個有效的音節,從Lexcion及History Cache中查到對應的詞(通常有很多個),将它們放到Lattice上。Lattice的寬度會影響搜尋的速度。在這裡會進行了一定的剪枝(pruning):優先将History Cache中的詞放到Lattice上;再将Lexicon中出現頻率較高的詞,也放到Lattice上。上圖中,藍色加粗顯示的豎線,就是Lattice的一列(或稱為frame)。如果我們從左向右對Lattice的列進行編号,L0是<S>(即句子開始),L1上的詞有(西,戲,系,喜),L2上的詞有(安,案,暗,先,現,西安),L3上的詞有(是,市,示,使,西安市,暗示)... 我們也可以将每個拼音字母作為Lattice的一個frame,這樣并不會增加許多記憶體上的開銷,而且對将來的插入删除操作更為友善。

Context Searcher: 對得到的Word Lattice進行搜尋。我們設定beam width(預設為32),來限制每個Lattice frame上狀态節點的數目。某些得分比較低的狀态,可能就被剪枝掉了(例如下圖中帶紅叉的節點)。如我們在第一部分所講的,這是一個動态規劃問題,其搜尋的複雜度為O(B*C*N),其中B為beam width,C為Lattice每一幀上詞的平均個數,N是Lattice的長度。因為兩處都進行了剪枝,得到結果的可能 并不是 最優解。

另外要注意,在下圖中(目前的實作),每個音節下面的狀态節點,是屬于前一個音節的。這種結構是不适用于Syllable Graph的。我們需要一個單獨的、共有的起始幀,這樣就不需要T2這一列了。如顔色較淺的第二行所示。

SunPinyin代碼導讀-IME部分

插圖3

History Cache: 記錄使用者最近送出的句子。這裡使用的是一個類Bigram的模型。在計算P(z|xy)時,用Psmooth(z|xy)和Pcache(z|y)來插值得到。而Pcache(z|y),是P(z|y)的MLE (Maximum Likelihood Estimation),和P(z)的MLE的插值。

SunPinyin代碼導讀-IME部分
注:在實作中,計算Pcache的公式稍有不同。到時我們再具體說明。

2. 資料結構及核心算法 下面,我們來介紹一下主要的資料結構,看看在程式中Lattice是如何表示的。

[url=]class CBone[/url];

CBone類對應于Word Lattice上的一個frame。CBone的public成員包括,m_BoneType(表示Bone的類型,拼音或标點等), m_BoundaryType(表示邊界的類型,是自動邊界或使用者指定的邊界等),以及m_String(如果是拼音節點,就是這個syllable的拼 音字元串)。它還有一個protected成員,[url=]CBoneInnerData[/url] *m_pInnerData,這個資料結構記錄了屬于前一個Bone的Lattice資訊(參見插圖3)。[url=]CIMIContext[/url]是CBone的友類,可以通路這個保護成員。

[url=]class CSkeleton[/url];

Bone的清單,即std::list<CBone>,就是我們上面所說的Syllable List。例如插圖三中的清單,[ce]-[shi]-[pin]-[yin]-[shu]-[ru]-[T1]-[T2],除了T1和T2,所有的Bone都是拼音類型的Bone。

[url=]class CBoneInnerData[/url];

它為搜尋儲存了兩方面的狀态資料。一是拼音詞表([url=]Pinyin Trie[/url]樹)的若幹狀态,m_LexiconStates,當新的拼音Bone加入之後,我們通過嘗試擴充它們來判斷是否存在更長的詞可以加入 Lattice。其二是Lattice的若幹搜尋狀态,m_LatticeNodes,這些狀态可以用後續Bone中的詞來擴充。最後還包括與使用者互動相 關的成員變量:這個Bone是否位于最佳路徑上;如果是,該Bone上的最佳詞(m_BestWord)。就插圖3中的示例來說,B2(由0開始從左到右編号)處于最佳路徑上,且其m_BestWord為“拼音”。

[url=]class CLexiconState[/url];

m_pPYNode是Pinyin Trie上節點的指針,m_BoneStart記錄了這個拼音節點是從哪個Bone開始的,如果該狀态不是一個PINYIN節點(m_bPinyin == False,例如标點等),則直接使用m_WordId。

[url=]class CLexiconStates[/url];

即std::vector<CLexiconState>。我們并不需要像插圖2那樣,将實際的詞放到Lattice上。因為随時可以從Pinyin Trie樹的節點上得到按unigram排序的詞清單。對插圖3中的示例來說,B0上的LexiconStates為空,B1上為[(B0, ce')](ce'表示Pinyin Trie樹上的一條邊,即c-e-'),B2上為[(B0, ce'shi'), (B1, shi')],B3上為[(B1, shi'pin'), (B2, pin')](因為在ce'shi'這個節點的子樹中,不存在pin'這條子邊;而在shi'的子樹中存在)... ...

[url=]struct TLatticeState[/url];

m_Score是該節點上的機率值。m_BoneAfter就是該Lattice節點所在Bone,例如插圖3示例中的B4[3]節點(B4那一列最下面那個節點),其m_BoneAfter就是B4;Bone和TLatticeState是一個1:n的關系,我們可以很容易地周遊得到Bone下所有的Lattice狀态節點,而這個反向引用是為了友善回查。m_State是[url=]CThreadSlm[/url]上的狀态節點(即後續Lattice狀态節點的history),目前節點是由m_pBackTraceNode通過m_BackTraceWordId轉移得到的。這裡順便提一下 Sunpinyin的coding style,以C開頭的類型是class,以T開頭的類型是struct或union。

[url=]class CLatticeStates[/url];

這個類實際上是CLatticeStateVec(即std::vector<TLatticeState>)的一個包裝類(wrapper),設定了最大容量(即beam width,預設為32)。其内部使用了堆(heap)排序。

下面讓我們來看看構造和搜尋Word Lattice的方法。

[url=]CIMIContext[/url]類是整個[url=]ime[/url]部分代碼的核心,對應上一節的[url=]概念模型[/url],它充當了Syllable Segmentor、Lattice Builder和Context Searcher的角色:

[url=]CIMIContext::modifyAndReseg[/url] (boneStart, boneEnd, skel, cursor, cursorIdx, candiStart):

實際使用時,boneStart、boneEnd、candiStart均指向m_Skeleton中的節點。調用segPinyin(),對參數skel中的諸Bone節點(外加boneStart之前的2/3個節點,避免切分的不充分),和[boneEnd, T1-1)進行重新切分,得到的結果儲存在newskel中。重新計算cursor及其index。然後調用modify(its, T1-1, newskel),重新進行搜尋。

[url=]CIMIContext::segPinyin[/url] (head1, tail1, head2, tail2, newskel):

[head1, tail1)和[head2, tail2)在邏輯上組成一個待切分的拼音串兒。實際使用時,[head2, tail2)是CIMIContex::m_Skeleton中的節點。将切分的結果儲存到newskel變量中。

[url=]CIMIContext::modify[/url] (boneStart, boneEnd, skel):

将[boneStart, boneEnd)中的bone從其所在的skeleton清單(即m_Skeleton)中删除,将參數skel中的bone插入其間。調用searchFrom(),從新插入的Bone開始,重新進行搜尋。

[url=]CIMIContext::searchFrom[/url] (boneStart):

周遊boneStart到第二個尾節點(T2),根據前一個Bone,調用forwardXXX()方法擴充自己的Lexicon狀态節點,然後調用buildLatticeStates()構造自己的Lattice狀态節點。周遊結束之後,回溯得到最佳路徑。

從某一個bone開始進行搜尋,意味着從這個Bone到T2,每個Lattice Frame上原有的Lexicon和Lattice狀态節點都被清空并重新計算。

[url=]CIMIContext::forwardOnePinyinBone[/url] (bone):

将該bone的後續(boneNext)中的Lexicon狀态節點集合(lexss2)清空。周遊bone上的Lexicon狀态節點,用本bone上的拼音字元串,嘗試擴充它,并将新的Lexicon節點加入到lexss2中。疊代結束之後,嘗試用bone上的拼音字元串,從Pinyin Trie的根節點進行擴充,并加入到lexss2中。最後傳回boneNext。其它的forwardXXX()方法比較簡單,這裡就不多介紹了。

[url=]CIMIContext::buildLatticeStates[/url] (bone):

得到bone的前繼bonePrev(這意味着傳入的參數不能是第一個Bone節點),清空bone的Lattice狀态節點。周遊目前bone的Lexicon狀态節點,找到該節點的起始bone,對Pinyin Trie節點上對應的詞表,調用transferBetween(boneFrom, boneTo, wid, 1.0),構造bone上的Lattice狀态節點。

對[url=]上一節的插圖3[/url]來說,B3上有兩個Lexicon狀态,(B1, shi'pin')和(B2, pin'),可以用“食品”、“視訊”、“飾品”等詞來擴充B1上的Lattice節點,然後用“品”、“頻”、“拼”等詞再次來擴充B2上的Lattice節點,将新得到的狀态節點加入到B3的Lattice狀态節點集合中。

[url=]CIMIContext::transferBetween[/url] (from, to, wid, ic):

得到from Bone的Lattice狀态節點集合latss1。周遊latss1,對每個LatticeState節點,用其中儲存的SLM狀态節點m_State(即後續Lattice狀态節點的history),調用CThreadSlm::transfer()來計算Pslm(wid|history)的值。

如果轉移之後的狀态節點,其曆史(回退)節點為pseudo root節點(即level為0,表示平均分布),而最近的曆史記錄中卻見到過這個詞,則将這個pseudo unigram節點的index設定為wid;這樣對下一個詞w',才能通過[url=]CThreadSlm::lastWordId[/url](state)得到上一個詞的ID(即wid),以正确計算Pcache(w'|wid)的機率值。

然後計算Pcache(w|h),和Pslm(w|h)進行插值,得到句子的機率評分儲存到node.m_Score中。将這個新的Lattice節點,插入到to Bone的Lattice狀态節點集合latss2中(同時也進行了beam pruning)。

3. 處理候選 下面的資料結構和方法用來組織候選詞清單(即如何利用搜尋結果):

[url=]class CCandidate[/url]:

[m_BoneStart, m_BoneEnd)是這個候選詞對應的bone區間;m_WordID是候選詞對應的id;m_String是寬位元組字元串的指針。通常 m_String就是m_WordID對應的寬位元組字元串表示,但有時候選詞并不在詞表中(例如無效的拼音串兒)。

[url=]CIMIContext::getCandidates[/url] (bone, result):

如 果bone是尾節點,則直接傳回。如果不是一個有效的pinyin Bone節點,将bone上的拼音string作為候選,放到result中,并傳回。如果這個bone上有使用者選擇或最佳詞,設定cp(候選詞和評分 pair),将它儲存在以word_id為索引的map中。

然後周遊[bone+1, T1]。對每個Bone節點,周遊其lexicon狀态節點,找到起始節點為bone的狀态節點([url=]itlex-> m_BoneStart == bone[/url]),将pytrie上的詞(如果不是拼音節點,則是Bone上的拼音字元串)加入到map中;周遊其lattice狀态節點,如果該節點是從參數bone上轉移而來的([url=]its->m_pBackTraceNode->m_BoneAfter == bone[/url]),則将m_BackTraceWordId對應的候選詞,加入到map中。最後,按評分([url=]TCandiRank[/url]),對這個map進行堆排序,将排序後的結果儲存在result中。

評分的原則是:首先列出使用者選擇的詞,或者位于最佳路徑上的詞;優先列出從Lattice狀态中得到的詞;然後是Lexicon中的詞。且詞的長度越長,評分越高。

[url=]CIMIContext::getBestSentence[/url](result, boneStart, boneEnd, original_format):

如果boneStart上沒有最佳詞,則向前(左)查找,直到某個存在最佳詞的Bone,儲存在realStart中。沿着最佳路徑的這一段([realStart, boneEnd)),将最佳詞添加到result中,記錄期間有效拼音節點的個數,并傳回。

下面的方法,和使用者選擇的設定和取消有關:

[url=]CIMIContext::cancelSelection[/url](bone, update):

注 意,m_Skeleton上的最佳詞(無論是使用者選擇還是計算确定)是前後相連的。從bone開始向左查找。如果某個Bone(記為uBone)的類型為 UserSelectedBestWord,則将其改為NoBestWordStartHere,設定found為true,退出循環;否則,如果該 Bone是計算确定的最佳詞,則退出循環;否則,則繼續向左查找,直到第一個Bone。如果found為true,且update參數為true, uBone開始搜尋,并傳回它;否則,因為無需搜尋,直接傳回參數bone。将[firstBone, bone]這個區間離bone最近的使用者選擇取消。

[url=]CIMIContext::cancelSelectionCover[/url](bone, update):

這個方法和上面的類似,隻是當參數bone的類型是最佳詞時,直接傳回。相當于将[firstBone, bone)這個區間離bone最近的使用者選擇取消。

[url=]CIMIContext::makeSelection[/url](candi):

找到候選candi的起始Bone(m_BoneStart),調用cancelSelection()将其所處的使用者選擇取消(傳回值為boneLeft),并将候選起始Bone的類型設定為UserSelectedBestWord,然後從boneLeft重新搜尋。

4. 使用者編輯 這一節我們以以“[url=]經典風格[/url]”為例,介紹對User Editing(包括insertPinyin,erase,move cursor,selection等)的處理。

class  [url=] CIMIClassicVIew [/url]  {

     m_CursorBone: 目前光标所在的Bone

     m_CursorIdx: 光标在m_CursorBone中的位置索引

     m_CandiBone: 候選清單起始的Bone

     m_CandiFirst: 目前視窗中第一條候選在整個候選清單中的索引

     m_TailSetence:從m_CandiFirst開始的尾句

};

考察下面的例子,m_CursorBone指向的是'yin'所在的Bone;m_CursorIdx為2,指向的是'n'所在的位置;而m_CandiBone指向的是'pin'所在的Bone;因為沒有翻頁,是以m_CandiFirst為0;而候選1,即“拼音輸入”,就是m_TailSetence。

SunPinyin代碼導讀-IME部分
SunPinyin代碼導讀-IME部分

在經典風格中,向左移動光标,意味着使用者可能要修正以前的UserSelection,是以要清除以前的使用者選擇并重新搜尋。而向後移動,後續的可能操作是插入、删除和添加等操作,不影響當下的候選結果,是以不會重新進行搜尋。

CIMIClassicView::[url=]moveHome[/url](changeMask, searchAgain) :

将[firstBone, m_CursorBone]内所有的使用者選擇都清除,傳回開始新搜尋的Bone節點。如果存在使用者選擇,則從firstBone重新搜尋。如果不存在使用者選擇,則傳回lastBone。

CIMIClassicView::[url=]moveLeft[/url](changeMask, searchAgain):

如果m_CursorIdx(光标在m_CursorBone中的索引)大于0,則減去1,傳回lastBone(即不影響目前的候選結果)。如果m_CursorIdx為0,并且m_CursorBone不是firstBone,則将m_CursorBone左移一個機關,如果移出了目前的候選詞範圍(也就是進入了前一個候選詞範圍),則調用m_pIC->[url=]cancelSelection[/url]()取消其所在的UserSelection,然後調用[url=]getCandidates[/url]()重新得到候選,将m_CursorIdx設定為m_CursorBone中拼音字元串兒的長度(如果m_CursorBone不是一個拼音類型的Bone,則m_CursorIdx為0)。最後,傳回新搜尋的起始Bone節點。

CIMIClassicView::[url=]moveLeftSyllable[/url](changeMask, searchAgain):

過程和上面的類似,隻不過移動是以Bone為機關的,且移動之後,m_CursorIdx設定為0。

CIMIClassicView::[url=]moveRight[/url](changeMask):

如果m_CursorIdx尚未到達m_CursorBone的尾部,則m_CursorIdx++并傳回。如果已到尾部,若m_CursorBone是一個拼音類型的Bone節點,則m_CursorIdx++(相當于一個音節的邊界);否則将m_CursorIdx設定為0,且m_CursorBone向右移動一個機關。最後一種可能([url=]m_CursorIdx == m_CursorBone->m_String.size()[/url]),是光标位于音節邊界上,将m_CursorIdx設定為0,且m_CursorBone向右移動一個機關,并傳回。

CIMIClassicView::[url=]moveRightSyllable[/url](changeMask):

将m_CursorBone向右移動一個機關,并相應地設定m_CursorIdx。

CIMIClassicView::[url=]moveEnd[/url](changeMask):

将m_CursorBone移動到結尾。

CIMIClassicView::[url=]makeSelection[/url](idx, mask):

idx是在目前候選視窗中的索引,和m_CandiFirst相加,得到在整個候選清單中的索引。如果idx<0,則送出preedit并重置IC,然後傳回。如果idx在候選清單的範圍内,則得到索引下的候選對象cand,調用m_pIC->[url=]makeSelection[/url](cand)設定使用者選擇。将m_CandiBone指向cand的結束位置(還記得麼,[m_BoneStart, m_BoneEnd)是候選詞對應的區間),如果它是不是一個拼音節點且未到達尾節點,則将m_CandiBone向右移動。如果一直移動到了尾節點,則送出preedit并重置IC;否則,如果m_Cursor位于[cand.m_BoneStart, m_CandiBone)之間,則将m_Cursor指向m_CandiBone,且将m_CursorIdx設定為0。然後将m_CandiFirst設定為0,調用getCandidates()來得到新的候選。

CIMIClassicView::[url=]erase[/url](bLeft, changeMask):

bLeft辨別是向左還是向右進行删除。如果向左删除,則調用moveLeft()将光标向左移動一個機關。然後,如果m_CursorIdx指向的是音節邊界的位置(即[url=]m_CursorIdx == m_CursorBone->m_String.size()[/url]),且目前光标是使用者确定的邊界,則調用m_pIC->[url=]modifyAndReseg[/url]()對該部分重新節點進行切分和搜尋,如果這影響到候選清單,則調用getCandidates()重新擷取;如果目前光标不是使用者确定的邊界,且要向右删除,則調用moveRight()将光标向右移動一個機關。如果m_CursorIdx并非指向音階邊界的位置,則修改光标内的拼音字元串,并調用m_pIC->modifyAndReseg()對該部分節點進行重新切分和搜尋,如果這影響到候選清單,則調用getCandidates()重新擷取。

CIMIClassicView::[url=]insertPinyin[/url](keyvalue, changeMask):

如果m_CursorBone是最後一個Bone(也就是在尾部添加拼音字母的情況),則将keyvalue作為一個自動切分的拼音節點,加到一個新的清單skel中,并将m_CursorBone設定為skel.begin(),且m_CursorIdx為1(m_CursorIdx指向的是音階邊界,因為Bone中隻有一個拼音字母)。如果m_CursorBone不是一個拼音節點,當m_CursorIdx>0時(通常不會發生這種情況),将[0..cursorIdx)、keyvalue、 [cursorIdx..end)這三個字元串構造為Bone,加入到清單skel中,cursor2自加,m_CursorBone指向中間的Bone,m_CursorIdx設定為1;當m_CursorIdx為0時,将keyvalue作為一個自動切分的拼音節點,加到一個清單skel中,并将m_CursorBone設定為skel.begin(),且m_CursorIdx為0。最後是插入拼音字母的情況,将m_CursorBone壓入到skel中,且将keyvalue附加到目前Bone的拼音字元串的尾部,m_CursorBone為skel.begin(),m_CursorIdx自加,且cursor2自加。然後調用m_pIC->modifyAndReseg()對該部分Bone節點重新切分并搜尋,如果這影響到候選清單,則調用getCandidates()重新擷取。

還有其它的一些insert方法,例如[url=]insertNormalChar[/url]()、[url=]insertBoundary[/url](),都會涉及到對Bone清單的修改,而重新進行切分搜尋,并擷取更新的候選清單。這裡就不多作介紹了。

5. History Cache 這一節我們來考察History Cache:

class  [url=] CBigramHistory [/url] {

     typedef unsigned                               TWordId;

     typedef std::pair<TWordId, TWordId>            TBigram;

     typedef TWordId                                TUnigram;

     typedef std::map<TBigram, int>                 TBigramPool;

     typedef std::map<TUnigram, int>                TUnigramPool;

     typedef std::deque<TWordId>                    TContextMemory;

     TContextMemory           m_memory;

     TUnigramPool             m_unifreq;

     TBigramPool              m_bifreq;

};

從上面的定義可以看出,m_unifreq和m_bifreq分别儲存了每個Unigram和Bigram出現在History Cache中的次數。而m_memory是一個雙向隊列,context_memory_size是其空間的上限,[url=]預設為8K[/url]。

CBigramHistory::[url=]incUniFreq[/url](ug):

将TUnigram ug的出現次數加1。

CBigramHistory::[url=]decUniFreq[/url](ug):

如果m_unifreq map中存在這個unigram,且出現的次數大于1,則減1;如果出現次數已經為0,則将其從m_unifreq中删除,以避免m_unifreq這個map占用的空間過大。

CBigramHistory::[url=]uniFreq[/url](ug):

如果ug不是stopword(在CBigramHistory::[url=]initClass[/url]()時初始化,且要和[url=]slm詞典[/url]的id相一緻),且存在于m_unifreq中,則傳回這個key對應的value。否則傳回0。

CBigramHistory::[url=]incBiFreq[/url](bg):

将TBigram bg的出現次數加1。

CBigramHistory::[url=]decBiFreq[/url](bg):

如果m_bifreq map中存在這個bigram,且出現的次數大于1,則減1;如果出現次數已經為0,則将其從m_bifreq中删除,以避免m_bifreq這個map占用的空間過大。

CBigramHistory::[url=]biFreq[/url](bg):

如果bg不是<DCWID, DCWID>,且存在于m_unifreq中,則傳回這個key對應的value。否則傳回0。

CBigramHistory::[url=]memorize[/url](its_wid, ite_wid):

将詞的序列[its_wid, ite_wid)記錄下來。首先我們要在目前的曆史記錄中,插入一個分割符(DCWID,相當于</s>),以和前一個流分隔開來;如果此時m_memory已經達到上限,則需将m_memory頭部的一個詞彈出,并且要減少相應unigram和bigram的出現次數。接下來,周遊[its_wid, ite_wid),假定目前的詞為Wi(i從0開始),将Wi加入到m_memory中,将Wi出現的次數加1,并将bigram <Wi-1, Wi>(W-1為DCWID)出現的次數加1;期間,如果m_memory達到了上限,和上面的操作一樣,将頭部的記錄彈出。

當視圖類調用[url=]doCommit()[/url]時,會調用這個方法将使用者送出的候選句子記錄下來。

CBigramHistory::[url=]bufferize[/url](buf_ptr, sz):

将m_memory持久化到buf_ptr指向的緩沖區中,同時注意大端和小端的問題。而m_unifreq和m_bifreq并不儲存。

CBigramHistory::[url=]loadFromBuffer[/url](buf_ptr, sz):

加載已持久化的History Cache,并統計unigram和bigram的出現次數。

CBigramHistory::[url=]seenBefore[/url](wid):

如果wid不是分割符,且非stopword,同時存在于m_unifreq中,則傳回true。

CBigramHistory::[url=]pr[/url](bigram):

求P(bigram.second|bigram.first)的機率值。以Bigram <x, y>舉例來說,uf0是C(x), bf為C(x, y),而uf1是C(y)。求解機率的計算公式為:
SunPinyin代碼導讀-IME部分
這個公式是Pml(y|x)、Pml(y)的插值,0.5是為了平滑Pml(y|x),而( actual_size + unused_size/10)是為了防止剛開始的時候history cache的size過小。最後這個機率值再和Pslm(y|h)進行[url=]插值[/url],參見[url=]ime/src/imi_context.cpp#902[/url]。

CBigramHistory::[url=]pr[/url](its_wid, ite_wid):

y = *(ite_wid-1),x = *(ite_wid-2),求P(y|x)。

CBigramHistory::[url=]pr[/url](its_wid, ite_wid, wid):

y = wid, x = *(ite_wid-1),求P(y|x)。

6. 雜項 TDoubleAnatomy and TLongExpFloat: [url=]TDoubleAnatomy[/url]是一個union,用來把一個雙精度的浮點數,分解為符号位、指數和尾數。這個方法适用于所有符合[url=]IEEE-754[/url]浮點數的平台。下面是雙精度浮點數的二進制布局:

SunPinyin代碼導讀-IME部分

這個浮點數對應的有理數為:

V = sign * 2^{exp} * (1.fract)

注意:其中指數使用的是移碼,對雙精度浮點數來說,指數是11位,是以實際的指數要減去2(11-1)-1,即0x3FF。[url=]TDoubleAnatomy::clearExp[/url]()方法将浮點數的指數部厘清除(即設定為0x3FF),然後再調用[url=]TDoubleAnatomy::getValue[/url]()得到的雙精度浮點數,就是sign * (1.fraction),這也就是[url=]TLongExpFloat::m_base[/url]的值。且在Sunpinyin中,所有的機率值都不會出現負值。

直接使用double進行機率值的連續相乘運算,容易溢出。而[url=]TLongExpFloat[/url]表示的範圍要大了很多,相當于将64位(1+11+52)的浮點數擴充到96位(32+64)。是以能緩解溢出的狀況。TLongExpFloat重載了部分[url=]算術操作符[/url],讓我們可以像對一個primitive資料類型那樣進行比較和乘除的操作(但不支援加減)。

你可能會問另外一個問題,為什麼不用對數将乘法操作轉換為加法呢?這是因為插值的原因,回憶一下[url=]插值的公式[/url]。如果使用對數,必須将對數形式的結果轉換回來,才能執行加法。

處理計算bow時由于浮點數累加所造成的誤差 在計算bow時(參見[url=]CalcNodeBow[/url]()),可能因為浮點數的連續相加造成的誤差,而導緻sumnext或sum出現大于1的情況,此時使用下面的公式計算bow:

SunPinyin代碼導讀-IME部分

原來要考察的是sumnext和sum離1.0距離的比值,現在因為誤差的原因,兩者都有可能稍稍超過1.0,是以将比較的基準點向右移動一些(即γ = Max(sumnext, sum)+δ,且δ是一個較小的值,0.0001)。

繼續閱讀