ICTCLAS代碼學習筆記之CDictionary類

關於ICTCLAS詞典的組織
詞典相關的操作都在自定義的類CDictionary裏,相關文件爲CDictionary.h和CDictionary.cpp;
涉及幾個結構體變量,只註明了變量沒有寫構造函數。
struct tagWordResult{
char sWord[WORD_MAXLENGTH]; //!可以用string代替
//The word
int nHandle;
//the POS of the word
double dValue;
//The -log(frequency/MAX)
};
typedef struct tagWordResult WORD_RESULT,*PWORD_RESULT;
/*data structure for word item*/
struct tagWordItem{
int nWordLen; //!可省略
char *sWord; //!可以用string代替,有寫拷貝功能性能損失不大
//The word
int nHandle;
//the process or information handle of the word
int nFrequency;
//The count which it appear
};
typedef struct tagWordItem WORD_ITEM,*PWORD_ITEM;
/*data structure for dictionary index table item*/
struct tagIndexTable{ //!索引表,一次性讀入應該可以用一個vector<WORD_ITEM>代替
int nCount;
//The count number of words which initial letter is sInit
PWORD_ITEM pWordItemHead;
//The head of word items
};
typedef struct tagIndexTable INDEX_TABLE;
/*data structure for word item chain*/
struct tagWordChain{ //!詞鏈,可以用一個list<WORD_ITEM>代替?
WORD_ITEM data;
struct tagWordChain *next;
/*----Added By [email protected] 2006-5-30----*/
struct tagWordChain()
{
next=NULL;
}
/*-----------------------------------------------*/
};
typedef struct tagWordChain WORD_CHAIN,*PWORD_CHAIN;
/*data structure for dictionary index table item*/
struct tagModifyTable{
int nCount;
//The count number of words which initial letter is sInit
int nDelete;
//The number of deleted items in the index table
PWORD_CHAIN pWordItemHead;
//The head of word items
};
typedef struct tagModifyTable MODIFY_TABLE,*PMODIFY_TABLE;

詞典類的聲明如下:
class CDictionary
{//!所有的輸入參數爲char*者都可以使用const string&來代替,至少應該是const char*
//!對返回值所用的char*使用string&來代替
//!對參數爲int*的根據情況改爲int&或者vector<int>&
//!其他int,double或者char等內置類型一律傳值,其他複雜類型如果不涉及修改一律const &
public:
bool Optimum();
bool Merge(CDictionary dict2,int nRatio); //!這裏應該用const CDictionary&
bool OutputChars(char *sFilename);
bool Output(char *sFilename);
int GetFrequency(char *sWord, int nHandle);
bool GetPOSString(int nPOS,char *sPOSRet);
int GetPOSValue(char *sPOS);
bool GetMaxMatch(char *sWord, char *sWordRet, int *npHandleRet);
bool MergePOS(int nHandle);
bool GetHandle(char *sWord,int *pnCount,int *pnHandle,int *pnFrequency);
bool IsExist(char *sWord,int nHandle);
bool AddItem(char *sWord,int nHandle,int nFrequency=0);
bool DelItem(char *sWord,int nHandle);
bool Save(char *sFilename);
bool Load(char *sFilename,bool bReset=false);
int GetWordType(char *sWord);
bool PreProcessing(char *sWord,int *nId,char *sWordRet,bool bAdd=false);
CDictionary();
virtual ~CDictionary();
INDEX_TABLE m_IndexTable[CC_NUM]; //!這個換成vector<WORD_ITEM>在構造函數時大小賦爲CC_NUM
PMODIFY_TABLE m_pModifyTable;
//The data for modify
protected:
bool DelModified();
bool FindInOriginalTable(int nInnerCode,char *sWord,int nHandle,int *nPosRet=0);
bool FindInModifyTable(int nInnerCode,char *sWord,int nHandle,PWORD_CHAIN *pFindRet=0);
/*----Added By [email protected] 2006-5-29----*/
void ClearDictionary(void);
/*-----------------------------------------------*/
};


下面是有必要說明的類成員函數
CDictionary::Load和CDictionary::Save
主要說明內容爲詞典本身的存儲結果,文件爲二進制讀寫格式,存儲CC_NUM個索引及相關值。
共有CC_NUM個索引項,對於每個索引的內容:
第一個int型數據的值爲當前索引項的詞個數m_IndexTable[i].nCount,如果大於0表示該索引項後面有m_IndexTable[i].nCount個詞,對於每個詞:
前3個int型的數據分別表示每個詞的出現頻率nFrequency、詞條字符串長度nWordLen及其nHandle。如果詞條長度大於0則後面連續nWordLen個char型字符爲該詞的字符
表示。
Load函數另外一個參數bReset主要是控制nFrequency,如果該值爲true則nFrequency的值爲0
詞典的存儲過程與讀入過程正好相反,需要注意的一點是,存儲時要考慮是否有修改詞典的部分,即m_pModifyTable的相關內容。主要規則如下:
每個索引的詞條個數應該爲原有個數+修改表個數-修改表刪除個數
寫入的順序爲(只針對有修改表的情況)
將修改表中的數據按詞條字母序寫入,如果詞條字符串相等則優先寫入hHandle值較小的。
2006-7-31的學習筆記
在家裏看代碼,心靜不下來啊,呵呵。
接着說CDictionary類的相關操作,save函數原來沒看太清楚,還得仔細看。
m_pModifyTable的大小要麼爲0要麼與m_IndexTable相同,即CC_NUM大小。如果m_pModifyTable非空則在保存時需要判斷。對於第i個索引,寫入的個數nCount等於原始的大小m_IndexTable[i].nCount加上新表中的個數並減去新表中刪除的個數。後面兩個怎麼算見後。
如果還沒有寫夠原始的個數m_IndexTable[i].nCount且修改表中還沒有寫完,則重複下面的操作
如果修改表中的當前詞條
2006-8-2 的學習筆記
Save函數不太容易看,先看一下AddItem和DelItem等會對m_pModifyTable有修改的部分再理解Save就容易得多了。
AddItem需要三個參數,分別是要新添的詞,其handle和頻度。首先進行預處理,如果第一個字是漢字,則返回的是這個漢字的handle及傳入詞的剩餘部分(即要新添的詞);如果是分割符則返回的是handle固定爲3755且傳入詞的全部被返回(即要新添的詞)(//?3755的含義未知)。如果不是這兩種情況則返回false。預處理函數中有一個參數bAdd在當前版本中沒有使用。只有預處理成功的詞串纔會被添加。首先會在原始表即m_IndexTable中查找是否已經有這個詞了,如果找到,則對m_IndexTable及m_pModifyTable進行適當的修改並返回表示成功添加的true。這裏的修改主要根據在原始表中是否已經刪除來區別操作,如果原始表中已刪除,則重置這項的頻度值,並在修改表中相應位置記錄刪除個數減1,如果原始表中仍然有詞,只要添加相應的頻度即可。原始表中沒有的情況會相對複雜一些。首先會在修改表中查找,如果找到了則添加相應的位置的頻度值並返回true。如果修改表中也沒
有,那就要新建一個元素,並將其賦值到修改表的相應項的詞串後面了,同時記錄該記錄的個數加1。其實簡單的說就是,如果在原始表中有該項,則設置相應的頻率,否則如果在修改表中有該項,則設置相應的頻度。如果兩者都沒有,則在修改表中的新添一個元素代表該項並插入到相應的位置中去。
DelItem的過程可以說是AddItem的逆操作,其參數只需要待刪除的詞及其handle值。經過同樣的預處理之後首先在原始表中查找,如果找到了將原始表中該項的頻率值置爲-1,在修改表中記錄刪除個數加一。需要注意的是,如果傳入的handle的值爲-1即要求忽略handle刪除所有相應詞的項,需要遍歷並修改m_IndexTable和m_pModifyTabale的值。如果原始中沒有找到,那麼在修改表中做類似的操作。在修改表中的操作會刪除所有詞相同且handle值相同或者詞相同且傳入handle值爲-1的項。如果在兩個表中都沒有找到則刪除失敗返回false。注意,在修改表中做刪除操作時並未對修改表中的nCount成員做任何的修改,懷疑爲bug//!。
DelModified函數其實就是清空修改表,主要是要釋放其中每個項的sWord所佔用的buffer。如果使用string及相應的stl類表示則這個函數只需要一條clear語句即可。
IsExist函數用來查找給出的詞條及相應的handle是否在原始表和修改表中的存在。
GetHandle函數是一個比較重要的函數,用來查找給定詞條相關信息,不僅僅是handle值。對於同一個詞,可能有多個handle及相應的頻率值。也是先在原始表中查找,如果沒有找到在修改表中查找。
FindInOriginalTable是在原始表中查找相應詞條信息。傳入的參數分別爲漢字的內碼,詞以及handle,返回值爲找到的詞匹配的位置,就是索引。用的是二分法進行查找,因爲m_IndexTable中的詞條本身是有序的。對於已刪除的詞條會做進一步的判斷,會找到第一個匹配當前詞的項的索引。
FindInModifyTable是類似的一個操作,在修改表中查找相應的詞條信息,不同的是返回值的類型爲找到詞條的前一個位置的指針,而且查找過程中是順序查找的。
GetWordType用於簡單判斷輸入字的類型,如果輸入字符串全爲漢字則返回漢字類型,如果第一個是分割符則返回分割符類型,否則返回其他。只有漢字類型和分割符類型是合法的詞典項類型。
預處理函數PreProcessing的功能前面已提過,這裏需要注意的是,在預處理這個函數中會刪除傳入詞串的首末空格,所以輸入參數sWord不能是const string&型。
MergePOS函數是用於將詞的詞性信息壓縮保存在handle裏面。在目前的版本中未使用,會同時處理原始表和修改表。對於每一個索引項中的若干詞條,如果某個詞條與“前一個詞”一樣且當前詞未被刪除(即頻率值不爲-1)則麼會標記該詞爲刪除同時在刪除表中的相應索引處的delete值加一。如果當前詞條爲第一個或者比“前一個詞”小且未被刪除,則置該詞條的handle值爲傳入的參數。對修改
表的操作類似,對於每個索引項,依次處理索引項中的每個詞條,如果當前詞條比“前一個詞”大則令當前詞條的handle值爲傳入的參數,並重置“前一個詞”,考察下一個詞條;如果與“前一個詞”相同,則刪除該項。(不可能存在比前一個詞小的情況)。該函數始終返回true值。
GetMaxMatch也是一個重要的函數,多次調用。主要是根據傳入的詞條獲取最大匹配的詞條及其相應的handle值。首先通過預處理獲取傳入詞條的ID值即內碼和剩餘的詞串(如果第一個字是分割符則爲全部詞串)以及第一個字。在原始表中查找的時候使用的handle值是-1因此會找到該詞在原始表中出現的第一個位置。在找到結果之後會繼續往後匹配直到確實找到詞條完全匹配的結果之後返回。如果原始表中沒有,則在修改表中進行類似的查找,由於修改表爲鏈表形式,因此只能依次往後比較匹配。此函數中最後判斷是否在修改表中找到相應結果的語句有問題,if(pCur!=NULL&&CC_Find(pCur->data.sWord,sWordGet)!=pCur->data.sWord)這一句中的紅色不等號似乎應該爲等號,//!bug吧!。
GetPOSValue用於將傳入的詞性字符串轉換爲相應的詞性值。其規則爲,如果傳入字符串長度小於3,則詞性值等於第一位乘以256加上第二位的值,如果大於等於3,則應該是由若干個加號連接起來的詞性字符串表示,遞歸調用該函數,詞性數值等於加號前的部分所得詞性值乘以100以後加上後面的部分。,這裏應該加入對傳入參數合法性的判斷。
GetPOSString爲GetPOSValue的逆過程,將傳入的詞性數值轉換爲相應的字符串表示輸出。該函數始終返回true。這個函數返回的詞性字符串可能是上位詞性也可能是下位詞性(即可能爲一個字母表示也可能爲兩個字母表示)
GetFrequency即獲得輸入詞條及相應的handle的頻率值,查表即可,如果沒有找到返回0。
Output和OutputChars兩個函數分別將詞典內容及結構輸出到指定的文件中。需要說明的是,在修改模式下不會輸出結果(即如果m_pModifyTable不爲空的話直接返回)這裏打開文件和判斷是否爲修改模式應該交換一下位置。後者只輸出頻率大於50的詞串,不知道是爲了減小輸出的文本情況還是另有深意。
Merge是一個將新詞典合併到已有詞典中的函數,除了詞典以外還同時提供一個比例因子nRatio用於控制兩個詞典信息的權重。只有在原始詞典中沒有修改信息而且傳入詞典中沒有修改信息時纔會執行合併工作。該函數需要改進的第一點就是傳入的參數應該使用const&類型。函數的基本思想就是,如果詞相同handle值也相同,則將兩個詞典中該項的頻率值相加。如果已有詞典中的項較小(詞小或者詞相同但handle值小)則修改已有詞典中該項的頻率值,即乘以一個係數nRatio/(nRatio+1),否則爲已有詞典中的詞較大,要求新詞典中該項的頻率值大於(nRation+1)/10的情況下將這個詞通過調用AddItem函數加入到已有詞典中,注意傳入的頻率值爲新表中該詞的頻率值除以(nRatio+1)。
Optimum函數用於優化已有詞典m_IndexTable。具體規則有兩條,刪除頻率值爲0的詞條,刪除詞條相同但詞性一個爲另外一個父類的情況(因爲子類詞類提供更多的信息)。另外就是會刪除詞性爲x,g和qg的詞條(不知道爲什麼)
ClearDictionary是我個人在修改版本中加入的清空m_IndexTable和m_pModifyTable的函數,主要是釋放其中動態分配的一些內存。
關於save函數在有修改表下的操作。首先需要說明的是三個計數器的作用。原始表中的m_IndexTable[i].nCount是用來記錄其pWordItemHead後一共跟了多少個詞條的,並不會進行修改,如果在修改狀態下刪除了某些詞條,則只是簡單的將其頻率值置爲-1不會刪除這個詞條,因此需要用修改表的相應位置的nDelete記錄刪除了幾個詞條。因此,在調用AddItem,DelItemt和MergePOS這三個會造成修改的函數中都會相應的對nDelete進行修改。而修改表中的nCount則是記錄修改表中相應索引位置跟了多少個詞條(新詞),因此只有在AddItem和DelItem中需要修改,前者是新添了一個原始表中沒有且現有的修改表中沒有的詞條時加一,後者是刪除了一個修改表中的詞條時減一,注意後面這個減一操作在現有的代碼中沒有,疑爲bug//!。在有修改表中的情況下,每個索引的詞條數爲原始表的nCount加上修改表的nCount再減去nDelete的值,即原始表中置了頻率爲-1的個數。然後同時走原始表和修改表,哪個小就先寫哪個,小即詞條小或者詞條相同但handle值小。唯一要注意的就是如果原始表中的詞項頻率值爲-1表示已刪除不要寫入。
關於詞典類的總結。
詞典類的主要數據存儲結構爲兩個表,原始表m_IndexTable和修改表m_pModifyTable,所有的操作都是圍繞這兩個數據結構進行的。前者爲基本數據,從詞典文件中讀入,爲預定義大小的buffer(因爲索引項的個數是固定的),後者爲修改數據,只在修改模式下才分配內存並做相關的操作。原始表的索引項是固定的,但是每個索引項的詞條數目是不固定的,類似於vector<list<WORD_ITEM>>的結構,其中WORD_ITEM爲每個詞條項,爲詞、詞的handle及頻率frequency。使用list結構不能使用二分查找法,也可以用類似vector<vector<WORD_ITEM>>,因爲寫入格式中有每個索引項的詞條個數這一項,可以預先分配vector的大小,不過vector的vector的情況是否會增加內存分配的開銷?m_pModifyTable是一個明顯的vector<pair<int nDelete, list<WORD_ITEM>>>結構,在現有的程序中也是使用指針遍歷方式進行查找或者比較等。一個折中的策略是vector中都只放指針,即原始表用vector<vector<WORD_ITEM>*>來表示,而修改表用vector<pair<int, list<WORD_ITEM>*>>來表示。這樣,對於修改表而言,也會減少一些內存的需求。現有算法中由於刪除項目的存在,因此不得不使用一個變量來存儲已刪除詞條的個數輔助爲-1的頻率值。詞典的另一個要求是每個索引項中的詞條必須有序,這樣主要是方便二分查找,對於輸入的handle值爲-1的情況也可以依順序找到第一個匹配的詞條。如果換做map一類的結構來存儲可以保證有序,查找也很快,就是構造的時候比較慢一些,而且map的鍵值會比較複雜,因爲一是詞本身,二是其handle值,而map的映射值爲其頻率值。需要自定義比較函數,數據結構可能會複雜一些。修改表也要用類似的結構。
方案1:
原始表:vector<vector<WORD_ITEM>>
修改表: vector<pair<int, list<WORD_ITEM>>>
方案2:
原始表:vector<vector<WORD_ITEM>*>
修改表:vector<pair<int, list<WORD_ITEM>*>>>
方案3:
原始表:vector<map<pair<string sword, int nhandle>, int nfrequency>>
修改表:vector<pair<int ndelete, map<pair<string sword, int nhandle>, int nfrequency>>>>
目前考慮用第二種方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章