Ansj是由孫健(ansjsun)開源的一個中文分詞器,為ICTLAS的Java版本,也采用了Bigram + HMM分詞模型(可參考我之前寫的文章):在Bigram分詞的基礎上,識別未登錄詞,以提高分詞准確度。雖然基本分詞原理與ICTLAS的一樣,但是Ansj做了一些工程上的優化,比如:用DAT高效地實現檢索詞典、鄰接表實現分詞DAG、支持自定義詞典與自定義消歧義規則等。
【開源中文分詞工具探析】系列:
- 開源中文分詞工具探析(一):ICTCLAS (NLPIR)
- 開源中文分詞工具探析(二):Jieba
- 開源中文分詞工具探析(三):Ansj
- 開源中文分詞工具探析(四):THULAC
- 開源中文分詞工具探析(五):FNLP
- 開源中文分詞工具探析(六):Stanford CoreNLP
- 開源中文分詞工具探析(七):LTP
1. 前言
Ansj支持多種分詞方式,其中ToAnalysis為店長推薦款:
它在易用性,穩定性.准確性.以及分詞效率上.都取得了一個不錯的平衡.如果你初次嘗試Ansj如果你想開箱即用.那么就用這個分詞方式是不會錯的.
因此,本文將主要分析ToAnalysis的分詞實現(基於ansj-5.1.0版本)。
ToAnalysis
繼承自抽象類org.ansj.splitWord.Analysis
,重寫了抽象方法getResult
。其中,分詞方法的依賴關系:ToAnalysis::parse -> Analysis::parseStr -> Analysis::analysisStr。analysisStr
方法就干了兩件事:
- 按照消歧義規則分詞;
- 在此基礎上,按照核心詞典分詞;
analysisStr
方法的最后調用了抽象方法getResult
,用於對分詞DAG的再處理;所有的分詞類均重寫這個方法。為了便於理解ToAnalysis分詞,我用Scala 2.11重寫了:
import java.util
import org.ansj.domain.{Result, Term}
import org.ansj.recognition.arrimpl.{AsianPersonRecognition, ForeignPersonRecognition, NumRecognition, UserDefineRecognition}
import org.ansj.splitWord.Analysis
import org.ansj.util.TermUtil.InsertTermType
import org.ansj.util.{Graph, NameFix}
/**
* @author rain
*/
object ToAnalysis extends Analysis {
def parse(sentence: String): Result = {
parseStr(sentence)
}
override def getResult(graph: Graph): util.List[Term] = {
// bigram分詞
graph.walkPath()
// 數字發現
if (isNumRecognition && graph.hasNum)
new NumRecognition().recognition(graph.terms)
// 人名識別
if (graph.hasPerson && isNameRecognition) {
// 亞洲人名識別
new AsianPersonRecognition().recognition(graph.terms)
// 詞黏結后修正打分, 求取最優路徑
graph.walkPathByScore()
NameFix.nameAmbiguity(graph.terms)
// 外國人名識別
new ForeignPersonRecognition().recognition(graph.terms)
graph.walkPathByScore()
}
// 用戶自定義詞典的識別
new UserDefineRecognition(InsertTermType.SKIP, forests: _*).recognition(graph.terms)
graph.rmLittlePath()
graph.walkPathByScore()
import scala.collection.JavaConversions._
val terms = graph.terms
new util.ArrayList[Term](terms.take(terms.length - 1).filter(t => t != null).toSeq)
}
}
如果沒看懂,沒關系,且看下小節分解。
2. 分解
分詞DAG
分詞DAG是由類org.ansj.util.Graph
實現的,主要的字段terms
為org.ansj.domain.Term
數組。其中,類Term為DAG的節點,字段包括:offe
首字符在句子中的位置、name
為詞,next
具有相同首字符的節點、from
前驅節點、score
打分。仔細看源碼容易發現DAG是用鄰接表(array + linked-list)方式所實現的。
Bigram模型
Bigram模型對應於一階Markov假設,詞只與其前面一個詞相關,其對應的分詞模型:
\begin{equation}
\arg \max \prod_{i=1}^m P(w_{i} | w_{i-1}) = \arg \min - \sum_{i=1}^m \log P(w_{i} | w_{i-1})
\label{eq:bigram}
\end{equation}
對應的詞典為bigramdict.dic
,格式如下:
始##始@和 11
和@尚 1
和@尚未 1
世紀@末##末 3
...
初始狀態\(w_0\)對應於Bigram詞典中的“始##始”,\(w_{m+1}\)對應於“末##末”。Bigram分詞的實現為Graph::walkPath
函數:
/**
* bigram分詞
* @param relationMap 干涉性打分, key為"first_word \tab second_word", value為干涉性score值
*/
public void walkPath(Map<String, Double> relationMap) {
Term term = null;
// 給terms[0] bigram打分, 且前驅節點為root_term "始##始"
merger(root, 0, relationMap);
// 從第一個字符開始往后更新打分
for (int i = 0; i < terms.length; i++) {
term = terms[i];
while (term != null && term.from() != null && term != end) {
int to = term.toValue();
// 給terms[to] bigram打分, 更新最小非零score值對應的term為前驅
merger(term, to, relationMap);
term = term.next();
}
}
// 求解最短路徑
optimalRoot();
}
對條件概率\(P(w_{i} | w_{i-1})\)做如下的平滑處理:
其中,\(a=0.1\)為平滑因子,\(N=2079997\)為訓練語料中的總詞數,\(\lambda = \frac{1}{N}\)。上述平滑處理實現函數為MathUtil.compuScore
。
求解式子\eqref{eq:bigram}的最優解等價於求解分詞DAG的最短路徑。Ansj采用了類似於Dijkstra的動態規划算法(作者稱之為Viterbi算法)來求解最短路徑。記\(G=(V,E)\)為分詞DAG,其中邊\((u,v) \in E\)滿足如下性質:
即DAG頂點的序號的順序與圖流向是一致的。這個重要的性質確保了(按Graph.terms[]
的index依次遞增)用動態規划求解最短路徑的正確性。用\(d_i\)標記源節點到節點\(i\)的最短距離,則有遞推式:
其中,\(b_{(j,i)}\)為兩個相鄰詞的條件概率的負log值-$ \log P(w_{i} | w_{j})$。上述實現請參照源碼Graph::walkPath
與Graph::optimalRoot
。
自定義詞典
Ansj支持自定義詞典分詞,是通過詞黏結的方式——如果相鄰的詞黏結后正好為自定義詞典中的詞,則可以被分詞——實現的。換句話說,如果自定義的詞未能完全覆蓋相鄰詞,則不能被分詞。舉個例子:
import scala.collection.JavaConversions._
val sentence = "倒模,替身算什么?鍾漢良、ab《孤芳不自賞》摳圖來充數"
println(ToAnalysis.parse(sentence).mkString(" "))
// 倒/v 模/ng ,/w 替身/n 算/v 什么/r ?/w 鍾漢良/nr 、/w ab/en 《/w 孤芳/nr 不/d 自賞/v 》/w 摳/v 圖/n 來/v 充數/v
DicLibrary.insert(DicLibrary.DEFAULT, "身算")
DicLibrary.insert(DicLibrary.DEFAULT, "摳圖")
println(ToAnalysis.parse(sentence).mkString(" "))
// 倒/v 模/ng ,/w 替身/n 算/v 什么/r ?/w 鍾漢良/nr 、/w ab/en 《/w 孤芳/nr 不/d 自賞/v 》/w 摳圖/userDefine 來/v 充數/v
3. 參考資料
[1] goofyan, ansj詞典加載及簡要分詞過程.