ICTCLAS代碼學習筆記之CSpan類

CSpan是標註器,詞性標註和角色標註都是使用這個類來完成的,使用的是相同結構的詞典,另標註算法是隱馬模型(HMM)完成的。
類中使用的一些宏變量如下:
#define MAX_WORDS_PER_SENTENCE 120 //!用於記錄每個句子中最多詞的個數
#define MAX_UNKNOWN_PER_SENTENCE 200 //!用於定義每個句子中的最多未登陸詞
#define MAX_POS_PER_WORD 20 //!用於定義每個詞的最多詞性
#define LITTLE_FREQUENCY 6 //!頻率?
enum TAG_TYPE{
TT_NORMAL,
TT_PERSON,
TT_PLACE,
TT_TRANS_PERSON
};
//!標註的類型,如果是詞性標註用的是第一個,否則是未登陸詞,目前只處理這些類型
CSpan類的公有成員變量裏面,用一個未登陸詞的計數m_nUnknownIndex和二維數組
m_nUnknownWords來配合記錄未登陸詞的個數及每個未登陸詞起始位置,數組m_dWordsPossibility則配合記錄每個未登陸詞的概率值(即dValue)。爲什麼感覺m_nUnknownIndex的名字有些名不符實呢?似乎應該是m_nUnknownCount纔對,呵呵。
另外一個公有成員m_context是CContextStat類的,用於處理上下文文法吧,呵呵。待定//!
私有成員變量比較多,依次說明。m_tagType表示標記的類型,如果是TT_NORMAL就是詞性標記,否則則是各個不同命名實體的角色標註;m_nStartPos記錄起始位置;而數組m_nBestTag記錄每個詞的最佳標註結果;一起配合使用的幾個數組m_sWords記錄切出來的詞,m_nTags記錄每個詞的所有可能詞性(或者標註角色類型),m_nWordPosition記錄詞的位置,而m_nBestPrev是用於Viterbi解碼算法中記錄最佳路徑的數組;m_dFrequency記錄每個詞的每個詞性(標註類型)的概率。最後,m_nCurLenght記錄當前長度,其實也可以看作是一個上述幾個數組的有效元素的個數。
下面是重頭戲,即各個成員函數的分析。
構造函數就是將上述數組、變量初始化,先看看這段代碼再說要注意的事項:
if(m_tagType!=TT_NORMAL)
m_nTags[0][0]=100;//Begin tag
else
m_nTags[0][0]=0;//Begin tag
m_nTags[0][1]=-1;
m_dFrequency[0][0]=0;
m_nCurLength=1;
m_nUnknownIndex=0;
m_nStartPos=0;
m_nWordPosition[1]=0;
m_sWords[0][0]=0;
m_tagType=TT_NORMAL;//Default tagging type
第一就是構造函數的第一句對m_tgType的判斷,由於是在構造函數中m_tagType其實未被賦值,那麼m_tagType就是編譯器的默認值(這裏就是枚舉類型的第一個值TT_NORMAL),而且最後還對m_tagType又賦了個初值TT_NORMAL,從最終結果上看似乎不錯,但感覺賦值很無厘頭。
另外就是m_nCurLength的初值不是0而是1,m_nWordPosition初值不是m_nWordPosition[0]而是m_nWordPosition [1]。具體的含義看看代碼再解釋//!
POSTagging函數應該是進行標註的主函數部分,在這之前要做的首先是初始化,即讀入相應的未登陸詞詞典和上下文規則,這部分應該是在系統的初始化過程中完成的,對應的函數爲LoadContext。傳入的參數用做m_context初始化時使用。
下面正式說POSTagging函數。傳入的第一個參數是NSP算法生成的粗分結果,後面的兩個詞典分別是核心詞典和未登陸詞詞典。讀分詞結果,從當前讀到的詞開始,猜測該詞的最好詞性(或角色標註結果),然後根據當前標註器的類型(標詞性或者相應的未登陸詞)進行相應的處理。如果是詞性,則根據猜測的結果對詞性進行相應的更新;如果是命名實體,則調用相應的函數進一步處理,這裏有一點要注意的是TT_PLACE(地名)和TT_TRANS_PERSON(音譯人名)用的都是PlaceRecognize函數,不知道是寫錯了還是怎麼着。每猜完一次都要對變量進行重置Reset()。大概過程就是這樣,這裏面比較核心的部分是猜測詞性以及命名實體識別對應的幾個函數。
GetFrom函數用於從傳入的詞鏈中根據起始位置nIndex讀入一個詞,讀入的信息存在相應的成員變量中。主循環中有兩個索引變量,I從1開始取值,控制的是m_sWords、m_nWordPosition、m_nTags、m_nBestPrev、m_dFrequency等成員變量的索引值,最後的值記錄在m_nCurLength中用於表示標註後詞的個數。而nWordsIndex則從傳入的起始位置nIndex開始取值,控制的是傳入的原始詞串數據結構pWordItems的索引。I的取值不會超過MAX_WORDS_PER_SENTENCE(120)而nWordsIndex的取值不會超過MAX_WORDS(650)。主循環對I的最大取值進行了判斷但是沒有判斷nWordsIndex的最大取值,只是根據對應位置的字符串是否爲空來決定的,如果前面的計算中沒有清空工作現場或者傳入參數非法則有可能非法訪問造成系統崩潰。
在標註的主循環中,如果進行詞性標記或者未登陸詞典中沒有出現當前處理的這個詞pWordItems[nWordsIndex].sWord,那麼簡單的將詞拷入m_sWords[i]中,並記錄下位置信息在m_nWordPosition[I+1]中,這也是爲什麼構造函數初始化時m_nWordPosition是從m_nWordPosition [1]開始賦值的,因爲I的取值從1開始。這裏同樣存在一個隱患,因爲只判斷i<MAX_WORDS_PER_SENTENCE,當I的取值爲MAX_WORDS_PER_SENTENCE-1時,對m_nWordPosition[I+1]的訪問會造成越界。對於角色標註且在詞典中找到詞的情況,會做一個切分,把第一個字和該串的其餘部分分開考慮。
記錄下當前詞的起始位置在m_nStartPos中,然後根據標註的不同類型進行處理。對於角色標註,如果是外國人名標註,會對半角符號“.”和“-”分別替換爲相應的全角符號“.”“-”進行處理。從未登陸詞詞典中讀入該詞的所有詞性及相應概率值,並記錄在相應的m_nTags和m_dFrequency中。如果當前詞爲開始標記或者結束標記且從未登陸詞詞典中只出現了一次要更新m_nTags和m_dFrequency的相應值,注意對於開始標記更新的是[i][j-1]而結束標記更新的是[i][j]。其他的需要標記角色的詞,從核心詞典中讀入該詞的詞性及相應頻率,如果詞典中已經有相應的詞及詞性,則將角色標記結果置爲0,概率結果也重置。注意這裏更新了j的值,以便後面判斷是否已有標註結果。相對而言詞性標註的工作則比較簡單,如果當前詞條的nHandle值大於0,即只有一個唯一確定的詞性那麼記錄下來就OK了;否則,屬於有多個詞性的情況,記錄詞性的時候取個負值即可。然後從核心詞典中讀入該詞的多個詞性,依次記錄入m_nTags和m_dFrequency的後面的位置。
上面兩種類型的標記都做完以後,如果j的取值仍然爲0,即什麼標記也沒有得到,那麼調用GuessPOS猜一個詞性。至此會有一個標記了,不要忘記給m_nTags[i][j]添上結束標記-1。如果此時j的取值爲1即只有一個標註結果且又不是開始標註(結束標記就可以退出了)那麼表示無歧
義了可以跳出循環,否則繼續。最後要做一些清理工作,包括判斷是否已到詞串的結尾,下面這段代碼的含義沒有看太明白,先記錄下後補:
if(m_nTags[i-1][1]!=-1)//||m_sWords[i][0]==0
{//Set end for words like "張/華/平"
if(m_tagType!=TT_NORMAL)
m_nTags[i][0]=101;
else
m_nTags[i][0]=1;
m_dFrequency[i][0]=0;
m_sWords[i][0]=0;//Set virtual ending
m_nTags[i++][1]=-1;
}
GuessPos函數只對未登陸詞進行角色的猜測,如果是詞性標記則直接忽略掉了。對於中國人名、音譯人名和地名,分別有更新m_nTags和m_dFrequency的計算方法,但是全部使用的是數值不清楚確切含義。
Disamb()函數用於消歧,即使用Viterbi算法選取全局最優的標註結果。對每個詞的每個標註結果m_nTags[i][j],考察當前詞的前面一個詞的每個詞性標註結果m_nTags[i-1][k],得到最小代價的路徑之後將路徑和頻率值分別記錄在m_nBestPrev[i][j]和m_dFrequency[i][j]之中。
Reset函數顧名思義用來重置內部變量,參數bContinue是用來控制重置的程度,如果bContinue爲真則只是將上次的結果拿來做爲初始狀態,否則清空重新開始。
GetBestPOS()函數就很簡單的,先進行消歧(調用Disamb函數),根據計算得到的結果從後向前回溯,將最好結果記錄在m_nBestTag中。
PersonRecognize函數是專門用來做中國人名識別的,幾種不同模式的定義在代碼中都有說明,例如BBCD是表示姓+姓+名1+名2,這種模式的參數值是0.003606,模式的長度爲4。先將計算得到的最佳角色標註結果的數值轉換爲字符。然後對於切好的詞串進行於全排列的方式進行組合來匹配所有的模式,如果匹配到則更新相應的m_nUnknownWords和m_dWordsPossibility的值,並增加m_nUnknownIndex的計數。
PlaceRecognize的過程類似,但代碼寫得沒有人名部分那麼清楚。只有在m_nBestTag[i]的值爲1或者2的時候纔會引發識別過程,這兩個數字代表的含義目前未知。程序同時引入了懲罰因子來對長度進行懲罰。最終更新m_nUnknownWords和m_dWordsPossibility的值。這個函數中使用多處while循環,並沒有索引的值進行判斷,可能會出現溢出的情況吧。
ComputePossibility函數就是計算從傳入的起始位置nStartPos長度爲nLength的詞串的相應概論,用的是詞的最佳標註結果來進行計算,沒有更多可說的內容。
CSpan類的總結:
至此,CSpan的內容基本上寫完了,因爲最初寫的時候也沒有完整的概念體系所以比較凌亂,現在再總結一下。這個類是一個“通用的”進行標註類,使用的算法是隱馬模型,解碼用的是Viterbi算法。使用此類可進行詞性的標註,也可以進行角色的標註,在部分代碼中需要一定的判斷。類的對外接口有兩種功能類型,一種是初始化的時候讀入相應的詞典(未登陸詞詞典)及相應的上下文無關信息,另外一種就是對傳入的詞串進行標註,可能是詞性也可能是命名實體,這部分的框架在POSTagging函數即可看到。讀入一個詞串和相應的詞性概率等信息,用Viterbi算法計算得到最好的標註結果後,如果是詞性標記則修改相應的詞性標註結果,否則調用相應的命名實體單元來進行進一步的識別工作。直到處理完所有的結果。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章