中文分词的原理——正、逆向最大长度匹配法、处理未登录字符串(JAVA)

中文分词的原理——正、逆向最大长度匹配法、处理未登录字符串(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(
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值