cppjieba分詞包主要提供中文分詞、關鍵詞提取、詞性標注三種功能
一、分詞
cppjieba分詞用的方法是最大概率分詞(MP)和隱馬爾科夫模型(HMM),以及將MP和HMM結合成的MixSegment分詞器。除此之外,cppjieba支持三種模式的分詞:
- 精確模式,試圖將句子最精確地切開,適合文本分析;
- 全模式,把句子中所有的可以成詞的詞語都掃描出來, 速度非常快,但是不能解決歧義;
我/ 來到/ 北京/ 清華/ 清華大學/ 華大/ 大學
- 搜索引擎模式,在精確模式的基礎上,對長詞再次切分,提高召回率,適合用於搜索引擎分詞
小明, 碩士, 畢業, 於, 中國, 科學, 學院, 科學院, 中國科學院, 計算, 計算所, 后, 在, 日本, 京都, 大學, 日本京都大學, 深造
1.最大概率分詞(MP)
- 默認基於jieba.dict.utf8生成前綴詞典,用戶可以添加自己的字典,並且按照一定權重比重一起生成前綴詞典。構建字典時將utf-8格式的輸入轉變為unicode格式
- 分詞器中有一個類Prefilter,pre_fileter會將輸入的字符串轉變為unicode格式,根據某些特殊的字符symbols_,將輸入的字符串切分成一段一段,對每一段分別分詞
- 構建DAG圖,從后往前的動態規划算法,回溯概率最大的切詞方法
void Cut(RuneStrArray::const_iterator begin, RuneStrArray::const_iterator end, vector<WordRange>& words, size_t max_word_len = MAX_WORD_LENGTH) const { vector<Dag> dags; dictTrie_->Find(begin, end, dags, max_word_len); //構建DAG圖 CalcDP(dags); //從后往前的動態規划算法 CutByDag(begin, end, dags, words);//回溯 }
2.隱藏馬爾科夫分詞(HMM)
參考:中文分詞之HMM模型詳解
cppjieba分詞中提供了HMM模型的參數文件,保存在hmm_model.utf8中。cppjieba的HMM分詞器,實際上就是加載HMM模型,然后根據輸入的句子(觀察序列),計算可能性最大的狀態序列。狀態空間由B(開始)、M(中間)、E(結束)、S(單個字)構成。下面是Viterbi算法的過程。
輸入樣例:
小明碩士畢業於中國科學院計算所
定義變量
二維數組 weight[4][15],4是狀態數(0:B,1:E,2:M,3:S),15是輸入句子的字數。比如 weight[0][2] 代表 狀態B的條件下,出現'碩'這個字的可能性。
二維數組 path[4][15],4是狀態數(0:B,1:E,2:M,3:S),15是輸入句子的字數。比如 path[0][2] 代表 weight[0][2]取到最大時,前一個字的狀態,比如 path[0][2] = 1, 則代表 weight[0][2]取到最大時,前一個字(也就是明
)的狀態是E。記錄前一個字的狀態是為了使用viterbi算法計算完整個 weight[4][15] 之后,能對輸入句子從右向左地回溯回來,找出對應的狀態序列。
使用InitStatus對weight二維數組進行初始化
已知InitStatus如下:
#B -0.26268660809250016 #E -3.14e+100 #M -3.14e+100 #S -1.4652633398537678
且由EmitProbMatrix可以得出
Status(B) -> Observed(小) : -5.79545 Status(E) -> Observed(小) : -7.36797 Status(M) -> Observed(小) : -5.09518 Status(S) -> Observed(小) : -6.2475
所以可以初始化 weight[i][0] 的值如下:
weight[0][0] = -0.26268660809250016 + -5.79545 = -6.05814 weight[1][0] = -3.14e+100 + -7.36797 = -3.14e+100 weight[2][0] = -3.14e+100 + -5.09518 = -3.14e+100 weight[3][0] = -1.4652633398537678 + -6.2475 = -7.71276
注意上式計算的時候是相加而不是相乘,因為之前取過對數的原因。
遍歷句子計算整個weight二維數組
//遍歷句子,下標i從1開始是因為剛才初始化的時候已經對0初始化結束了 for(size_t i = 1; i < 15; i++) { // 遍歷可能的狀態 for(size_t j = 0; j < 4; j++) { weight[j][i] = MIN_DOUBLE; path[j][i] = -1; //遍歷前一個字可能的狀態 for(size_t k = 0; k < 4; k++) { double tmp = weight[k][i-1] + _transProb[k][j] + _emitProb[j][sentence[i]]; if(tmp > weight[j][i]) // 找出最大的weight[j][i]值 { weight[j][i] = tmp; path[j][i] = k; } } } }
如此遍歷下來,weight[4][15]
和 path[4][15]
就都計算完畢。
確定邊界條件和路徑回溯
邊界條件如下:
對於每個句子,最后一個字的狀態只可能是 E 或者 S,不可能是 M 或者 B。
所以在本文的例子中我們只需要比較 weight[1(E)][14]
和 weight[3(S)][14]
的大小即可。
在本例中:
weight[1][14] = -102.492; weight[3][14] = -101.632;
所以 S > E,也就是對於路徑回溯的起點是 path[3][14]
。
回溯的路徑是:
SEBEMBEBEMBEBEB
倒序一下就是:
BE/BE/BME/BE/BME/BE/S
所以切詞結果就是:
小明/碩士/畢業於/中國/科學院/計算/所
到此,一個HMM模型中文分詞算法過程就闡述完畢了。
也就是給定我們一個模型,我們對模型進行載入完畢之后,只要運行一遍Viterbi
算法,就可以找出每個字對應的狀態,根據狀態也就可以對句子進行分詞。
3、MixSegment是MP和HMM的結合,首先使用MP分詞,然后對MP分詞的結果使用HMM分詞。其實,第二次使用HMM再分對原有分詞結果調整得並不多,只是將MP結果中單字順序收集再分詞。
void Cut(RuneStrArray::const_iterator begin, RuneStrArray::const_iterator end, vector<WordRange>& res, bool hmm) const { if (!hmm) { mpSeg_.Cut(begin, end, res); return; } vector<WordRange> words; assert(end >= begin); words.reserve(end - begin); mpSeg_.Cut(begin, end, words); vector<WordRange> hmmRes; hmmRes.reserve(end - begin); for (size_t i = 0; i < words.size(); i++) { //if mp Get a word, it's ok, put it into result if (words[i].left != words[i].right || (words[i].left == words[i].right && mpSeg_.IsUserDictSingleChineseWord( words[i].left->rune))) { res.push_back(words[i]); continue; } // if mp Get a single one and it is not in userdict, collect it in sequence size_t j = i; while (j < words.size() && words[j].left == words[j].right && !mpSeg_.IsUserDictSingleChineseWord(words[j].left->rune)) { j++; } // Cut the sequence with hmm assert(j - 1 >= i); // TODO hmmSeg_.Cut(words[i].left, words[j - 1].left + 1, hmmRes); //put hmm result to result for (size_t k = 0; k < hmmRes.size(); k++) { res.push_back(hmmRes[k]); } //clear tmp vars hmmRes.clear(); //let i jump over this piece i = j - 1; } }
二、關鍵詞提取
cpp結巴的提供了兩種關鍵詞提取方法,分別基於TF-IDF和TextRank算法,下面分別介紹
1.基於TF-IDF算法的關鍵詞抽取
TF(term frequency):一個詞語在單個文檔出現的次數
IDF(Inverse document frequency): 逆文檔頻率,是一個詞語普遍度的度量。某一特定詞語的IDF,有總文件數目除以包含該詞語的文件數目,再將取得商取對數的得到。
總的來說某一特定文件內的高頻率詞語,以及詞語在整個文件集合中的低文件頻率,可以產生出高權重的TF-IDF。
cppjieba中提供接近26萬詞語的IDF(idf.utf8),對於在idf.utf8中沒出現的詞語,idf權重取平局值。除此之外,有些詞語在文檔的出現的頻率TF很高,但是對於文檔來說沒什么實在的意義比如“的”,“是”等,因此cppjieba還提供了了一個1534個詞語的stop_words.utf8。
cppjieba中根據權重(tf*idf)排序,取出topN個關鍵詞。
partial_sort(keywords.begin(), keywords.begin() + topN, keywords.end(),Compare);
參考:http://blog.csdn.net/awj3584/article/details/18604901
2.基於TextRank的關鍵詞提取
PageRank根據網頁之間的鏈接關系,建立網頁之間的有向圖,圖中的節點表示每個網頁,然后根據這個有向圖迭代計算出每個網頁的PER值,作為網頁重要性排名的依據。TextRank方法與PageRank的方法非常類似,當使用TextRank方法做文本的關鍵字提取時,首先在給定的共現窗口內(cppjieba中默認為5)根據詞語之間的共現關系建立鏈接關系圖,圖中的每個節點代表每個節點,每條邊的權重表示詞語共現的次數。然后根據詞語之間鏈接關系圖,迭代計算出每個詞語的權重值,根據權重值選出文本中權重值大的幾個關鍵詞。
TextRank 一般模型可以表示為一個有向有權圖 G =(V, E), 由點集合 V和邊集合 E 組成, E 是V ×V的子集。圖中任兩點 Vi , Vj 之間邊的權重為 wji , 對於一個給定的點 Vi, In(Vi) 為 指 向 該 點 的 點 集 合 , Out(Vi) 為點 Vi 指向的點集合。點 Vi 的得分定義如下:
其中, d 為阻尼系數, 取值范圍為 0 到 1, 代表從圖中某一特定點指向其他任意點的概率, 一般取值為 0.85。使用TextRank 算法計算圖中各點的得分時, 需要給圖中的點指定任意的初值, 並遞歸計算直到收斂, 即圖中任意一點的誤差率小於給定的極限值時就可以達到收斂。cppjieba在實現默認迭代10次。
參考:http://www.cnblogs.com/clover-siyecao/p/5726480.html
參考:http://blog.sohu.com/s/MTAzMjM1NDY0/239636012.html(矩陣形式)
三、詞性標注
cppjieba中的詞性標注實現過程總的來說是基於詞典的查詢,詞典中存有大約35萬詞匯的詞性。單個詞語的詞性標注,直接查詢詞典,詞典中不存在一個詞語多個詞性的問題。詞典沒有的詞語,根據詞語的特點的簡單的標注為數字(m),英文(eng),以及x。對於整個句子的詞性標注是用分詞算法分詞,然后用上述單個詞語詞性標注的方法逐個標注詞性。由此可見cppjieba的詞性標注非常依賴詞典。對於句子的詞性標注,沒有考慮詞性之間的關系。
參考: CppJieba代碼詳解