【開源中文分詞工具探析】系列:
- 開源中文分詞工具探析(一):ICTCLAS (NLPIR)
- 開源中文分詞工具探析(二):Jieba
- 開源中文分詞工具探析(三):Ansj
- 開源中文分詞工具探析(四):THULAC
- 開源中文分詞工具探析(五):FNLP
- 開源中文分詞工具探析(六):Stanford CoreNLP
- 開源中文分詞工具探析(七):LTP
1. 前言
ICTCLAS是張華平老師推出的中文分詞系統,於2009年更名為NLPIR。ICTCLAS是中文分詞界元老級工具了,作者開放出了free版本的源代碼(1.0整理版本在此). 作者在論文[1] 中宣稱ICTCLAS是基於HHMM(Hierarchical Hidden Markov Model)實現,后在論文[2]中改成了基於層疊隱馬爾可夫模型CHMM(Cascaded Hidden Markov Model)。我把HHMM的原論文[3]讀了一遍,對照ICTCLAS源碼,發現ICTCLAS本質上就是一個Bigram的Word-Based Generative Model,用HMM來做未登錄詞識別(修正分詞)與詞性標注,與HHMM沒有半毛錢關系。Biagram語法模型對應於1階Markov假設,則ICTCLAS分詞模型的聯合概率為
\begin{equation}
P(w_1^m) = \prod_i P(w_{i} | w_{i-1})
\label{eq:bigram}
\end{equation}
2. ICTCLAS分解
ICTCLAS Free版的C++源代碼實在是佶屈聱牙,因本人水平有限,故參照的源碼為sinboy的Java實現 ictclas4j ("com.googlecode.ictclas4j" % "ictclas4j" % "1.0.1"
)。ICTCLAS分詞流程如下:
- 按照核心詞典進行第一次切詞;
- 在第一次切詞的基礎上,求解最大聯合概率\eqref{eq:bigram},作者稱之為“二元切分詞圖”;
- HMM識別未登錄詞,諸如:人名、翻譯人名、地名、機構名等;
- 分詞結果整理,詞性標注。
詞典
ICTCLAS的詞典包括六種:
import org.ictclas4j.segment._
// 核心詞典
val coreDict = new Dictionary("data/coreDict.dct")
// 二元詞典
val bigramDict = new Dictionary("data/bigramDict.dct")
// 人名詞典
val personTagger = new PosTagger(Utility.TAG_TYPE.TT_PERSON, "data/nr", coreDict)
// 翻譯人名詞典
val transPersonTagger = new PosTagger(Utility.TAG_TYPE.TT_TRANS_PERSON, "data/tr", coreDict)
// 地名詞典
val placeTagger = new PosTagger(Utility.TAG_TYPE.TT_TRANS_PERSON, "data/ns", coreDict)
// 詞性標注
val lexTagger = new PosTagger(Utility.TAG_TYPE.TT_NORMAL, "data/lexical", coreDict)
其中,核心詞典(coreDict)用於初始切詞,二元詞典(bigramDict)記錄了兩個詞聯合出現的頻數。coreDict與bigramDict的數據格式如下
# coreDict
塊0
count:4
wordLen:0 frequency:3 handle:25856 word:(啊)
wordLen:0 frequency:69 handle:30976 word:(啊)
wordLen:2 frequency:0 handle:25856 word:(啊)呀
wordLen:2 frequency:0 handle:25856 word:(啊)喲
塊1
count:133
wordLen:0 frequency:0 handle:26624 word:(阿)
wordLen:0 frequency:94 handle:27136 word:(阿)
...
# bigramDict
塊0
count:9
wordLen:3 frequency:3 handle:3 word:(啊)@、
wordLen:3 frequency:4 handle:3 word:(啊)@。
wordLen:3 frequency:1 handle:3 word:(啊)@”
wordLen:3 frequency:39 handle:3 word:(啊)@!
wordLen:3 frequency:20 handle:3 word:(啊)@,
其中,frequency
表示詞頻,handle
表示詞性:
讓我們先看一個具體的例子:
wordLen:4 frequency:12 handle:28275 word:(愛)爾蘭
handle = 28275, 轉成HEX表示,為0x6E73。
把0x6E73解釋為字符數組,0x6E -> 'n', 0x73 -> 's', 所以0x6E73 -> "ns"。
查北大的《漢語文本詞性標注標記集》, ns ~ 地名 (名詞代碼n和處所詞代碼s並在一起)。
計算
為了計算聯合概率\eqref{eq:bigram}最大值,ICTCLAS做了一個巧妙的轉換——求對數並做Linear interpolation平滑:
將所有的可能分詞組合構建成一個有向圖,邊的權重設為(上式中)平滑后的log值。由此,將求解最大聯合概率轉化成一個圖論中最短路徑問題。有時最大聯合概率對應的分詞結果不一定是最優的。為了解決這個問題,ICTCLAS采取了N-best策略,即求解N-最短路徑而不是直接求解最短路徑。然后在N-最短路徑的分詞結果上,做HMM修正——識別未登錄詞與詞性標注。為了更清晰地了解分詞流程,我用scala把分詞函數SegTag::split()
重新寫了一遍:
import java.util
import org.ictclas4j.bean.{Dictionary, SegNode}
import org.ictclas4j.segment._
import org.ictclas4j.utility.{POSTag, Utility}
import scala.collection.JavaConversions._
val sentence = "深夜的穆赫蘭道發生一樁車禍,女子麗塔在車禍中失憶了"
val pathCount = 1
// 存儲分詞結果
val stringBuilder = StringBuilder.newBuilder
// 按[空格|回車]分隔句子
val sentenceSeg = new SentenceSeg(sentence)
sentenceSeg.getSens.filter(_.isSeg).foreach { sen =>
// 原子切分
val as: AtomSeg = new AtomSeg(sen.getContent)
// 全切分:根據核心詞典,生成所有的可能詞
val segGraph: SegGraph = GraphGenerate.generate(as.getAtoms, coreDict)
// 二元切分詞圖
val biSegGraph: SegGraph = GraphGenerate.biGenerate(segGraph, coreDict, bigramDict)
val nsp: NShortPath = new NShortPath(biSegGraph, pathCount)
nsp.getPaths.foreach { onePath =>
// 得到初次分詞路徑
val segPath = getSegPath(segGraph, onePath)
val firstPath = AdjustSeg.firstAdjust(segPath)
// 處理未登陸詞,進對初次分詞結果進行優化
val optSegGraph: SegGraph = new SegGraph(firstPath)
personTagger.recognition(optSegGraph, firstPath)
transPersonTagger.recognition(optSegGraph, firstPath)
placeTagger.recognition(optSegGraph, firstPath)
// 根據優化后的結果,重新進行生成二叉分詞圖表
val optBiSegGraph: SegGraph = GraphGenerate.biGenerate(optSegGraph, coreDict, bigramDict)
// 重新求取N-最短路徑
val optNsp: NShortPath = new NShortPath(optBiSegGraph, pathCount)
val optBipath: util.ArrayList[util.ArrayList[Integer]] = optNsp.getPaths
// 詞性標記
optBipath.foreach { optOnePath =>
val optSegPath: util.ArrayList[SegNode] = getSegPath(optSegGraph, optOnePath)
lexTagger.recognition(optSegPath)
// 對分詞結果做最終的調整,主要是人名的拆分或重疊詞的合並
val adjResult = AdjustSeg.finaAdjust(optSegPath, personTagger, placeTagger)
val adjrs: String = outputResult(adjResult)
stringBuilder ++= adjrs
}
}
}
println(stringBuilder.toString())
// 深夜/t 的/u 穆赫蘭/nr 道/q 發生/v 一/m 樁/q 車禍/n ,/w 女子/n 麗塔/nr 在/p 車禍/n 中/f 失/v 憶/vg 了/y
其中,調用了函數:
// 根據二叉分詞路徑生成分詞路徑
private def getSegPath(sg: SegGraph, biPath: util.ArrayList[Integer]): util.ArrayList[SegNode] = {
sg != null && biPath != null match {
case true =>
val path = biPath.map { t => sg.getSnList.get(t) }
new util.ArrayList[SegNode](path)
case _ => null
}
}
// 根據分詞路徑生成分詞結果
private def outputResult(wrList: util.ArrayList[SegNode]): String = {
...
}
后記:據聞ICTCLAS的第一版是作者在中科院讀碩期間完成的,雖說代碼質量惹人吐槽,但是不得不驚嘆於作者的代碼功底以及在訓練模型上所下的功夫。
3. 參考資料
[1] Zhang, Hua-Ping, et al. "HHMM-based Chinese lexical analyzer ICTCLAS." Proceedings of the second SIGHAN workshop on Chinese language processing-Volume 17. Association for Computational Linguistics, 2003.
[2] 劉群, et al. "基於層疊隱馬模型的漢語詞法分析." 計算機研究與發展 41.8 (2004): 1421-1429.
[3] Fine, Shai, Yoram Singer, and Naftali Tishby. "The hierarchical hidden Markov model: Analysis and applications." Machine learning 32.1 (1998): 41-62.