在看源碼以前,先看幾遍論文《基於角色標註的中國人名自動識別研究》html
關於命名識別的一些問題,可參考下列一些issue:java
HanLP參考博客:算法
詞性標註3d
層疊HMM-Viterbi角色標註模型下的機構名識別code
分詞orm
在HMM與分詞、詞性標註、命名實體識別中說:htm
分詞:給定一個字的序列,找出最可能的標籤序列(斷句符號:[詞尾]或[非詞尾]構成的序列)。結巴分詞目前就是利用BMES標籤來分詞的,B(開頭),M(中間),E(結尾),S(獨立成詞)對象
分詞也是採用了維特比算法的動態規劃性質求解的,具體可參考:文本挖掘的分詞原理blog
角色觀察get
以「唱首張學友的歌情已逝」爲例,
先將起始頂點 始##始,角色標註爲:NR.A 和 NR.K,頻次默認爲1
對於第一個詞「唱首」,它不存在於 nr.txt中,EnumItem<NR> nrEnumItem = PersonDictionary.dictionary.get(vertex.realWord);返回null,因而根據它自己的詞性猜一個角色標註:
因爲"唱首"的Attribute爲 nz 16,不是nr 和 nnt,故默認給它指定一個角色NR.A,頻率爲nr.tr.txt中 NR.A 角色的總頻率。
此時,角色列表以下:
接下來是頂點「張」,因爲「張」在nr.txt中,所以PersonDictionary.dictionary.get(vertex.realWord)返回EnumItem對象,直接將它加入到角色列表中:
加入「張」以後的角色列表以下:
「唱首張學友的歌情已逝」 整句的角色列表以下:
至此,角色觀察 部分 就完成了。
總結一下,對句子進行角色觀察,首先是經過分詞算法將句子分紅若干個詞,而後對每一個詞查詢人名詞典(PersonDictionary)。
維特比算法(動態規劃)求解最優路徑
在上圖中,給每一個詞都打上了角色標記,能夠看出,一個詞能夠有多個標記。而咱們須要將這些詞選擇一條路徑最短的角色路徑。參考隱馬爾可夫模型維特比算法詳解
List<NR> nrList = viterbiComputeSimply(roleTagList);//some code....return Viterbi.computeEnumSimply(roleTagList, PersonDictionary.transformMatrixDictionary);
而這個過程,其實就是:維特比算法解碼隱藏狀態序列。在這裏,五元組是:
Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur)
維特比解碼隱藏狀態的動態規劃求解核心代碼以下:
for (E cur : item.labelMap.keySet())
{
double now = transformMatrixDictionary.transititon_probability[pre.ordinal()][cur.ordinal()] - Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur));
if (perfect_cost > now)
{
perfect_cost = now;
perfect_tag = cur;
}
}
transformMatrixDictionary.transititon_probability[pre.ordinal()][cur.ordinal()] 是前一個隱藏狀態 pre.ordinal()轉換到當前隱藏狀態cur.ordinal()的轉移機率。Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur)是當前隱藏狀態的發射機率。兩者「相減」獲得一個機率 保存在double now變量中,而後經過 for 循環找出 當前觀察狀態 對應的 最可能的(perfect_cost最小) 隱藏狀態 perfect_tag。
至於爲何是上面那個公式來計算轉移機率和發射機率,可參考論文:《基於角色標註的中國人名自動識別研究》
在上面例子中,獲得的最優隱藏狀態序列(最優路徑)K->A->K->Z->L->E->A->A 以下:
nrList = {LinkedList@1065} size = 8
"K" 始##始
"A" 唱首
"K" 張
"Z" 學友
"L" 的
"E" 歌
"A" 情已逝
"A" 末##末
例如:
隱藏狀態---觀察狀態
"K"----------始##始
最大匹配
有了最優隱藏序列:KAKZLEAA,接下來就是:後續的「最大匹配處理」了。
PersonDictionary.parsePattern(nrList, pWordSegResult, wordNetOptimum, wordNetAll);
在最大匹配以前,會進行「模式拆分」。在com.hankcs.hanlp.corpus.tag.NR.java 定義了隱藏狀態的具體含義。好比說,若最優隱藏序列中 存在 'U' 或者 'V',
U Ppf 人名的上文和姓成詞 這裏【有關】天培的壯烈
V Pnw 三字人名的末字和下文成詞 龔學平等領導, 鄧穎【超生】前
則會作「拆分處理」
switch(nr)
{
case U:
//拆分紅K B
case V:
//視狀況拆分
}
拆分完成以後,從新獲得一個新的隱藏序列(模式)
String pattern = sbPattern.toString();
接下來,就用AC自動機進行最大模式匹配了,並將匹配的結果存儲到「最優詞網」中。固然,在這裏就能夠自定義一些針對特定應用的 識別處理規則
trie.parseText(pattern, new AhoCorasickDoubleArrayTrie.IHit<NRPattern>(){
//.....
wordNetOptimum.insert(offset, new Vertex(Predefine.TAG_PEOPLE, name, ATTRIBUTE, WORD_ID), wordNetAll);
}
將識別出來的人名保存到最優詞網後,再基於最優詞網調用一次維特比分詞算法,獲得最終的分詞結果---細分結果。
if (wordNetOptimum.size() != preSize)
{
vertexList = viterbi(wordNetOptimum);
if (HanLP.Config.DEBUG)
{
System.out.printf("細分詞網:\n%s\n", wordNetOptimum);
}
}
總結
源碼上的人名識別基本上是按照論文中的內容來實現的。對於一個給定的句子,先進行下面三大步驟處理:
角色觀察
維特比算法解碼求解隱藏狀態(求解各個分詞 的 角色標記)
對角色標記進行最大匹配(可作一些後處理操做)
最後,再使用維特比算法進行一次分詞,獲得細分結果,即爲最後的識別結果。
這篇文章裏面沒有寫維特比分詞算法的詳細過程,以及轉移矩陣的生成過程,之後有時間再補上。看源碼,對隱馬模型的理解又加深了一點,感覺到了理論的東西如何用代碼一步步來實現。因爲我也是初學,對源碼的理解不夠深刻或者存在一些誤差,歡迎批評指正。
關於動態規劃的一個簡單示例,可參考:動態規劃之Fib數列類問題應用
文章來源hapjin 的博客