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}
object ToAnalysis extends Analysis {
def parse(sentence: String): Result = {
parseStr(sentence)
}
override def getResult(graph: Graph): util.List[Term] = {
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假设,词只与其前面一个词相关,其对应的分词模型:
对应的词典为bigramdict.dic
,格式如下:
始##始@和 11
和@尚 1
和@尚未 1
世纪@末##末 3
...
初始状态w0对应于Bigram词典中的“始##始”,wm+1对应于“末##末”。Bigram分词的实现为Graph::walkPath
函数:
public void walkPath(Map<String, Double> relationMap) {
Term term = null;
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();
merger(term, to, relationMap);
term = term.next();
}
}
optimalRoot();
}
对条件概率P(wi|wi−1)做如下的平滑处理:
其中,a=0.1为平滑因子,N=2079997为训练语料中的总词数,λ=1N。上述平滑处理实现函数为MathUtil.compuScore
。
求解式子(1)的最优解等价于求解分词DAG的最短路径。Ansj采用了类似于Dijkstra的动态规划算法(作者称之为Viterbi算法)来求解最短路径。记G=(V,E)为分词DAG,其中边(u,v)∈E满足如下性质:
即DAG顶点的序号的顺序与图流向是一致的。这个重要的性质确保了(按Graph.terms[]
的index依次递增)用动态规划求解最短路径的正确性。用di标记源节点到节点i的最短距离,则有递推式:
其中,b(j,i)为两个相邻词的条件概率的负log值-logP(wi|wj)。上述实现请参照源码Graph::walkPath
与Graph::optimalRoot
。
自定义词典
Ansj支持自定义词典分词,是通过词黏结的方式——如果相邻的词黏结后正好为自定义词典中的词,则可以被分词——实现的。换句话说,如果自定义的词未能完全覆盖相邻词,则不能被分词。举个例子:
import scala.collection.JavaConversions._
val sentence = "倒模,替身算什么?钟汉良、ab《孤芳不自赏》抠图来充数"
println(ToAnalysis.parse(sentence).mkString(" "))
DicLibrary.insert(DicLibrary.DEFAULT, "身算")
DicLibrary.insert(DicLibrary.DEFAULT, "抠图")
println(ToAnalysis.parse(sentence).mkString(" "))
3. 参考资料
[1] goofyan, ansj词典加载及简要分词过程.
如需转载,请注明作者及出处.
作者:Treant
出处:http://www.cnblogs.com/en-heng/