http://www.hankcs.com/program/java/tire-tree-participle.html
最近在看Ansj中文分词的源码,以前没有涉足过这个领域,所以需要做一些笔记。
Trie树
首先是Ansj分词最基本的数据结构——Trie树。Trie树也称字典树,能在常数时间O(len)内实现插入和查询操作,是一种以空间换取时间的数据结构,广泛用于词频统计和输入统计领域。
Ansj作者ansjsun为此数据结构专门开了一个项目,clone下来之后可以用作者提供的一个demo进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
package
com.hankcs;
import
love.cq.domain.Forest;
import
love.cq.library.Library;
import
love.cq.splitWord.GetWord;
import
java.io.BufferedReader;
import
java.io.StringReader;
/**
* @author hankcs
*/
public
class
Main
{
public
static
void
main(String[] args)
throws
Exception
{
/**
* 词典的构造.一行一个词后面是参数.可以从文件读取.可以是read流.
*/
String dic =
"中国\t1\tzg\n"
+
"人名\t2\n"
+
"中国人民\t4\n"
+
"人民\t3\n"
+
"孙健\t5\n"
+
"CSDN\t6\n"
+
"java\t7\n"
+
"java学习\t10\n"
;
Forest forest = Library.makeForest(
new
BufferedReader(
new
StringReader(dic)));
/**
* 删除一个单词
*/
Library.removeWord(forest,
"中国"
);
/**
* 增加一个新词
*/
Library.insertWord(forest,
"中国人"
);
String content =
"中国人名识别是中国人民的一个骄傲.孙健人民在CSDN中学到了很多最早iteye是java学习笔记叫javaeye但是java123只是一部分"
;
GetWord udg = forest.getWord(content);
String temp =
null
;
while
((temp = udg.getFrontWords()) !=
null
)
System.out.println(temp +
"\t\t"
+ udg.getParam(
1
) +
"\t\t"
+ udg.getParam(
2
));
}
}
|
输出:
1
2
3
4
5
6
7
|
中国人 null null
中国人民 null null
孙健 null null
人民 null null
CSDN null null
java学习 null null
java null null
|
这段demo的目的是利用一个小词典对后面一句话进行分词,词典被用来构造了一颗Trie树,也就是代码中的forest。
词典每一行第一列是单词,之后的几列都是param(属性)。
在tree_split中,一棵Trie树有四种不同的节点:
-
根节点,上图的绿色节点。被称为Forest,没有实际含义,也不含属性。
-
起始节点,上图的蓝色节点。是一个单词的开头第一个字,不含属性。
-
中继节点,上图的黄色节点。可能是一个单词的结尾,含属性;也可能是另一个更长的单词的中间某个字,不含属性。
-
结束节点,上图的红色节点。是一个单词的结尾,含属性。
根节点使用Forest描述,而其它三种节点统一使用Branch描述,并用status = 1 2 3 来区分,它们有如下的类图关系:
Root在构造的时候开了212个空槽以供放置子节点,每个汉字和其他字符都落在这个范围内。每次查找直接用汉字作为下标即可定位,Branch则使用动态数组分配内存,使用二分查找定位,这是Trie树的高速秘诀。Trie树的查询和插入都是类似的方法:从根节点开始沿着词语的开头字符走到结尾字符。在这里除了完成基本的维护操作,还需维护Branch的status。
删除操作比较讨巧,统一将要删除的单词最后一个字对应的节点设为“起始节点”,那么它就不能构成这个词了。
词典分词
词典分词是一种实现简便、速度快但是错误率高的分词方式。用Trie树词典分词就是按照句子的字符顺序从root往下走,每走到一个结束节点则分出一个词。中途遇到的中继节点统统忽略,这种方式也称“最长匹配”,是一种很武断的方式。比如下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package
com.hankcs;
import
love.cq.domain.Forest;
import
love.cq.library.Library;
import
love.cq.splitWord.GetWord;
import
java.io.BufferedReader;
import
java.io.StringReader;
/**
* @author hankcs
*/
public
class
Main
{
public
static
void
main(String[] args)
throws
Exception
{
/**
* 词典的构造.一行一个词后面是参数.可以从文件读取.可以是read流.
*/
String dic =
"商品\t1\tzg\n"
+
"和服\t2\n"
+
"服务\t4\n"
;
Forest forest = Library.makeForest(
new
BufferedReader(
new
StringReader(dic)));
String content =
"商品和服务"
;
GetWord udg = forest.getWord(content);
String temp =
null
;
while
((temp = udg.getFrontWords()) !=
null
)
System.out.println(temp +
"\t\t"
+ udg.getParam(
1
) +
"\t\t"
+ udg.getParam(
2
));
}
}
|
输出:
1
2
|
商品 zg null
和服 null null
|
很明显,效果不好。
要想提高分词效果,就必须引入条件概率(隐马尔可夫模型),这就是Ansj分词的使命吧。