中文分词的原理——正、逆向最大长度匹配法、处理未登录字符串(JAVA)
中文分词就是对中文断句,这样能消除文字的部分歧义。除了基本的分词功能,为了消除歧义还可以进行更多的加工。中文分词可以分成如下几个子任务:
- 分词:把输入的标题或者文本内容等分成词。
- 词性标注(POS):给分出来的词标注上名词或动词等词性。词性标注可以部分消除词的歧义,例如“行”作为量词和作为形容词表示的意思不一样。
- 语义标注:把每个词标注上语义编码。
很多分词方法都借助词库。词库的来源是语料库或者词典,例如“人民日报语料库”或者《现代汉语大词典》。
中文分词的两类方法:
- 机械匹配的方法:例如正向最大长度匹配(Forward Maximum Match)的方法和逆向最大长度匹配(Reverse Maximum Matching)的方法。
- 统计的方法:例如最大概率分词方法和最大熵的分词方法等。
可以把输入文本预切分成句子。可以使用BreakIterator把文本分成句子。BreakIterator.getSentenceInstance返回按标点符号的边界切分句子的实例。简单的分成句子的方法是(jqrw002.java):
import java.text.BreakIterator;
import java.util.Locale;
public class jqrw002 {
public static void main(String[] args) {
// TODO Auto-generated method stub
String stringToExamine = "可那是什么啊?1946年,卡拉什尼科夫开始设计突击步枪。在这种半自动卡宾枪的基础上设计出一种全自动步枪,并送去参加国家靶场选型试验。样枪称之为AK-46,即1946年式自动步枪。";
//根据中文标点符号切分
BreakIterator boundary = BreakIterator.getSentenceInstance(Locale.CHINESE);
//设置要处理的文本
boundary.setText(stringToExamine);
int start = boundary.first(); //开始位置
for (int end = boundary.next(); end != BreakIterator.DONE;
start = end, end = boundary.next()) {
//输出子串,也就是一个句子
System.out.println(stringToExamine.substring(start, end));
}
}
}
所得结果图为:
可以模仿BreakIterator,把分词接口设计成从前往后迭代访问的风格。通过调用next方法返回下一个切分位置。
public class Segmenter {
public int next () {
//得到下一个词,如果没有则返回-1
// 返回最长匹配词,如果没有匹配上,则按单字切分
}
}
或者直接返回下一个词:
Segmenter seg = new Segmenter("大学生活动中心"); //切分文本
String word;
do {
word = seg.nextWord(); //返回一个词
System.out.println(word);
} while (word != null);
1.正向最大长度匹配法
假如要切分“印度尼西亚地震”这句话,希望切分出“印度尼西亚”,而不希望切分出“印度”这个词。正向找最长词是正向最大长度匹配的思想。倾向于写更短的词,除非必要,才用长词表述,所以倾向切分出长词。
正向最大长度匹配的分词方法实现起来很简单。每次从词典找和待匹配串前缀最长匹配的词,如果找到匹配词,则把这个词作为切分词,待匹配串减去该词,如果词典中没有词匹配上,则按单字切分。例如,Trie树结构的词典中包括如下的8个词语:
大 大学 大学生 活动 生活 中 中心 心
输入:“大学生活动中心”,首先匹配出开头的最长词“大学生”,然后匹配出“活动”,最后匹配出“中心”。切分过程如下图:
最后分词结果为:“大学生/活动/中心”。
在分词类Segmenter的构造方法中输入要处理的文本。然后通过nextWord方法遍历单词。有个text变量记录切分文本。offset变量记录已经切分到哪里。分词类基本实现如下:
public class Segmenter {
String text = null; //切分文本
int offset; //已经处理到的位置
public Segmenter(String text) {
this.text = text; //更新待切分的文本
offset = 0; //重置已经处理到的位置
}
public String nextWord() {
//得到下一个词,如果没有则返回null
// 返回最长匹配词,如果没有匹配上,则按单字切分
}
}
为了避免重复加载词典,在这个类的静态方法中加载词典:
private static TSTNode root; //根节点是静态的
static {
//加载词典
String fileName = "SDIC.txt"; //词典文件名
try {
FileReader fileRead = new FileReader(fileName);
BufferedReader read = new BufferedReader(fileRead);
String line; //读入的一行
try {
while ((line = read.readLine()) != null) {
//按行读
StringTokenizer st = new StringTokenizer(line, "\t");
String key = st.nextToken(); //得到词
TSTNode endNode = createNode(key); //创建词对应的结束节点并返回
//设置这个节点对应的值,也就是把它标记成可以结束的节点
endNode.nodeValue = key;
}
} catch (IOException e) {
e.printStackTrace();
}finally {
read.close(); //关闭读入流
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
为了形成平衡的Trie树,把词典中的词先排序,排序后为:
中 中心 大 大学 大学生 心 活动 生活
按平衡方式生成的词典Trie树如下图所示,其中双圈表示的节点可以做为匹配终止节点:
在最大长度匹配的分词方法中,需要用到从待切分字符串返回从指定位置(offset)开始的最长匹配词的方法。例如,当输入串是“大学生活动中心”,则返回“大学生”这个词,而不是返回“大”或者“大学”。匹配的过程就好像一条蛇爬上一棵树。例如当offset=0时,找最长匹配词的过程如下图所示:
从Trie树搜索最长匹配单词的方法如下:
public String nextWord() {
//得到下一个词
String word = null;
if (text == null || root == null) {
return word;
}
if (offset >= text.length()) //已经处理完毕
return word;
TSTNode currentNode = root; //从根节点开始
int charIndex = offset; //待切分字符串的处理开始位置
while (true) {
if (currentNode == null) {
//已经匹配完毕
if(word==null){
//没有匹配上,则按单字切分
word = text.substring(offset,offset+1);
offset++;
}
return word; //返回找到的词
}
int charComp = text.charAt(charIndex) - currentNode.splitChar; //比较两个字符
if (charComp == 0) {
charIndex++; //找字符串中的下一个字符
if (currentNode.nodeValue != null) {
word = currentNode.nodeValue; // 候选最长匹配词
offset = charIndex;
}
if (charIndex == text.length()) {
return word; // 已经匹配完
}
currentNode = currentNode.mid;
} else if (charComp < 0) {
currentNode = currentNode.left;
} else {
currentNode = currentNode.right;
}
}
}
测试分词:
Segmenter seg = new Segmenter("大学生活动中心"); //切分文本
String word; //保存词
do {
word = seg.nextWord(); //返回一个词
System.out.println(word); //输出单词
} while (word != null); //直到没有词
可以给定一个字符串,枚举出所有的匹配点。以“大学生活动中心”为例,第一次调用时,offset是0,第二次调用时,offset是3。因为采用了Trie树结构查找单词,所以和用HashMap查找单词的方式比较起来,这种实现方法代码更简单,而且切分速度更快。
正向最大长度切分方法虽然容易实现,但是精度不高。以“有意见分歧”这句话为例,正向最大长度切分的结果是:“有意/见/分歧”,逆向最大长度切分的结果是:“有/意见/分歧”。因为汉语的主干成分后置,所以逆向最大长度切分的精确度稍高。另外一种最少切分的方法是使每一句中切出的词数最小。
正向最大长度匹配法代码示例(jqrw005.java):
import java.util.ArrayList;
import java.util.List;
public class jqrw005 extends TernarySearchTrie {
private int matchEnglish(