中文分詞工具探析(一):ICTCLAS (NLPIR)



【開源中文分詞工具探析】系列:

  1. 開源中文分詞工具探析(一):ICTCLAS (NLPIR)
  2. 開源中文分詞工具探析(二):Jieba
  3. 開源中文分詞工具探析(三):Ansj
  4. 開源中文分詞工具探析(四):THULAC
  5. 開源中文分詞工具探析(五):FNLP
  6. 開源中文分詞工具探析(六):Stanford CoreNLP
  7. 開源中文分詞工具探析(七):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分詞流程如下:

  1. 按照核心詞典進行第一次切詞;
  2. 在第一次切詞的基礎上,求解最大聯合概率\eqref{eq:bigram},作者稱之為“二元切分詞圖”;
  3. HMM識別未登錄詞,諸如:人名、翻譯人名、地名、機構名等;
  4. 分詞結果整理,詞性標注。

詞典

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平滑:

\[\begin{aligned} \arg \max \prod_i P(w_{i} | w_{i-1}) & = \arg \min - \sum_i \log P(w_{i} | w_{i-1}) \\ & \approx \arg \min - \sum_i \log \left[ aP(w_{i-1}) + (1-a) P(w_{i}|w_{i-1}) \right] \end{aligned} \]

將所有的可能分詞組合構建成一個有向圖,邊的權重設為(上式中)平滑后的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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM