基于Tire树和最大概率法的中文分词功能的Java实现
对于分词系统的实现来说,主要应集中在两方面的考虑上:一是对语料库的组织,二是分词策略的制订。
1. Tire树
Tire树,即字典树,是通过字串的公共前缀来对字串进行统计、排序及存储的一种树形结构。其具有如下三个性质:
1) 根节点不包含字符(或汉字),除根节点以外的每个节点只能包含一个字符(汉字)
2) 从根节点到任一节点的路径上的所有节点中的字符(汉字)按顺序排列的字符串(词组)就是该节点所对应的字符串(词组)
3) 每个节点的所有直接子节点包含的字符(汉字)各不相同
上述性质保证了从Tire树中查找任意字符串(词组)所需要比较的次数尽可能最少,以达到快速搜索语料库的目的。
如下图所示的是一个由词组集<一,一万,一万多,一万元,一上午,一下午,一下子>生成的Tire树的子树:
可见,从子树的根节点“一”开始,任意一条路径都能组成一个以“一”开头的词组。而在实际应用中,需要给每个节点附上一些数据属性,如词频,因而可以用这些属性来区别某条路径上的字串是否是一个词组。如,节点“上”的词频为-1,那么“一上”就不是一个词组。
如下的代码是Tire树的Java实现:
package chn.seg; import java.util.HashMap;
import java.util.Map; public class TireNode { private String character;
private int frequency = -1;
private double antilog = -1;
private Map<String, TireNode> children; public String getCharacter() {
return character;
} public void setCharacter(String character) {
this.character = character;
} public int getFrequency() {
return frequency;
} public void setFrequency(int frequency) {
this.frequency = frequency;
} public double getAntilog() {
return antilog;
} public void setAntilog(double antilog) {
this.antilog = antilog;
} public void addChild(TireNode node) {
if (children == null) {
children = new HashMap<String, TireNode>();
} if (!children.containsKey(node.getCharacter())) {
children.put(node.getCharacter(), node);
}
} public TireNode getChild(String ch) {
if (children == null || !children.containsKey(ch)) {
return null;
} return children.get(ch);
} public void removeChild(String ch) {
if (children == null || !children.containsKey(ch)) {
return;
} children.remove(ch);
}
}
2. 最大概率法(动态规划)
最大概率法是中文分词策略中的一种方法。相较于最大匹配法等策略而言,最大概率法更加准确,同时其实现也更为复杂。
基于动态规划的最大概率法的核心思想是:对于任意一个语句,首先按语句中词组的出现顺序列出所有在语料库中出现过的词组;将上述词组集中的每一个词作为一个顶点,加上开始与结束顶点,按构成语句的顺序组织成有向图;再为有向图中每两个直接相连的顶点间的路径赋上权值,如A→B,则AB间的路径权值为B的费用(若B为结束顶点,则权值为0);此时原问题就转化成了单源最短路径问题,通过动态规划解出最优解即可。
如句子“今天下雨”,按顺序在语料库中存在的词组及其费用如下:
今,a
今天,b
天,c
天下,d
下,e
下雨,f
雨,g
则可以生成如下的加权有向图:
显而易见,从“Start”到“End”的单源路径最优解就是“今天下雨”这个句子的分词结果。
那么,作为权值的费用如何计算呢?对于最大概率法来说,要求的是词组集在语料库中出现的概率之乘积最大。对应单源最短路径问题的费用来说,
费用 = log( 总词频 / 某一词组词频 )
通过上述公式就可以把“最大”问题化为“最小”问题,“乘积”问题化为“求和”问题进行求解了。
如下的代码是基于动态规划的最大概率法的Java实现:
package chn.seg; import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List; public class ChnSeq {
private TireNode tire = null; public void init() throws IOException, ClassNotFoundException {
File file = new File("data" + File.separator + "dict.txt");
if (!file.isFile()) {
System.err.println("语料库不存在!终止程序!");
System.exit(0);
} BufferedReader in = new BufferedReader(
new InputStreamReader(new FileInputStream(file), "utf-8"));
String line = in.readLine();
int totalFreq = Integer.parseInt(line); tire = new TireNode(); while ((line = in.readLine()) != null) {
String[] segs = line.split(" ");
String word = segs[0];
int freq = Integer.parseInt(segs[1]); TireNode root = tire;
for (int i = 0; i < word.length(); i++) {
String c = "" + word.charAt(i);
TireNode node = root.getChild(c);
if (node == null) {
node = new TireNode();
node.setCharacter(c);
root.addChild(node);
}
root = node;
} root.setFrequency(freq);
root.setAntilog(Math.log((double)totalFreq / freq));
}
in.close();
} public TireNode getTire() {
return tire;
} public TireNode getNodeByWord(String word) {
if (tire == null) {
System.err.println("需要先初始化ChnSeq对象!");
return null;
} TireNode node = tire;
for (int i = 0; i < word.length(); i++) {
String ch = word.charAt(i) + "";
if (node == null) {
break;
} else {
node = node.getChild(ch);
}
} return node;
} private class Segment {
public String word;
public String endChar;
public String lastChar;
public double cost; public final static String START_SIGN = "<< STARTING >>";
public final static String END_SIGN = "<< ENDING >>";
} private List<Segment> preSegment(String sentence) {
List<Segment> segs = new ArrayList<Segment>(); Segment terminal = new Segment();
terminal.word = Segment.START_SIGN;
terminal.endChar = Segment.START_SIGN;
terminal.lastChar = null;
segs.add(terminal);
for (int i = 0; i < sentence.length(); i++) {
for (int j = i + 1; j <= sentence.length(); j++) {
String word = sentence.substring(i, j);
TireNode tnode = this.getNodeByWord(word);
if (tnode == null) {
break;
}
if (tnode.getFrequency() <= 0) {
continue;
} Segment seg = new Segment();
seg.word = word;
seg.endChar = word.substring(word.length() - 1, word.length());
if (i == 0) {
seg.lastChar = Segment.START_SIGN;
} else {
seg.lastChar = sentence.substring(i - 1, i);
}
seg.cost = tnode.getAntilog();
segs.add(seg);
}
}
terminal = new Segment();
terminal.word = Segment.END_SIGN;
terminal.endChar = Segment.END_SIGN;
terminal.lastChar = sentence.substring(sentence.length() - 1, sentence.length());
segs.add(terminal); return segs;
} private String[] dynamicSegment(List<Segment> segs) {
final double INFINITE = 9999999; if (segs == null || segs.size() == 0) {
return null;
} int n = segs.size(); double[][] costs = new double[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
costs[i][j] = INFINITE;
}
} for (int i = 0; i < n; i++) {
String endChar = segs.get(i).endChar;
for (int j = 0; j < n; j++) {
String lastChar = segs.get(j).lastChar; if (lastChar != null && lastChar.equals(endChar)) {
costs[i][j] = segs.get(j).cost;
}
}
} int sp = 0; // starting point
int fp = n - 1; // finishing point double[] dist = new double[n];
List<List<Integer>> sPaths = new ArrayList<List<Integer>>();
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
dist[i] = costs[sp][i];
if (sp != i) {
list.add(i);
}
if (dist[i] < INFINITE) {
List<Integer> spa = new ArrayList<Integer>();
sPaths.add(spa);
} else {
sPaths.add(null);
}
} while (!list.isEmpty()) {
Integer minIdx = list.get(0);
for (int i: list) {
if (dist[i] < dist[minIdx]) {
minIdx = i;
}
} list.remove(minIdx); for (int i = 0; i < n; i++) {
if (dist[i] > dist[minIdx] + costs[minIdx][i]) {
dist[i] = dist[minIdx] + costs[minIdx][i];
List<Integer> tmp = new ArrayList<Integer>(sPaths.get(minIdx));
tmp.add(minIdx);
sPaths.set(i, tmp);
}
}
} String[] result = new String[sPaths.get(fp).size()];
for (int i = 0; i < sPaths.get(fp).size(); i++) {
result[i] = segs.get(sPaths.get(fp).get(i)).word;
}
return result;
} public String[] segment(String sentence) {
return dynamicSegment(preSegment(sentence));
}
}
3. 测试代码
package chn.seg; import java.io.IOException; public class Main { public static void main(String[] args) throws ClassNotFoundException, IOException {
ChnSeq cs = new ChnSeq();
cs.init(); String sentence = "生活的决定权也一直都在自己手上"; String[] segs = cs.segment(sentence);
for (String s: segs) {
System.out.print(s + "\t");
}
} }
基于Tire树和最大概率法的中文分词功能的Java实现的更多相关文章
- 使用Python,字标注及最大熵法进行中文分词
使用Python,字标注及最大熵法进行中文分词 在前面的博文中使用python实现了基于词典及匹配的中文分词,这里介绍另外一种方法, 这种方法基于字标注法,并且基于最大熵法,使用机器学习方法进行训练, ...
- PyTorch 高级实战教程:基于 BI-LSTM CRF 实现命名实体识别和中文分词
前言:译者实测 PyTorch 代码非常简洁易懂,只需要将中文分词的数据集预处理成作者提到的格式,即可很快的就迁移了这个代码到中文分词中,相关的代码后续将会分享. 具体的数据格式,这种方式并不适合处理 ...
- 转:从头开始编写基于隐含马尔可夫模型HMM的中文分词器
http://blog.csdn.net/guixunlong/article/details/8925990 从头开始编写基于隐含马尔可夫模型HMM的中文分词器之一 - 资源篇 首先感谢52nlp的 ...
- 基于Deep Learning的中文分词尝试
http://h2ex.com/1282 现有分词介绍 自然语言处理(NLP,Natural Language Processing)是一个信息时代最重要的技术之一,简单来讲,就是让计算机能够理解人类 ...
- 基于MMSeg算法的中文分词类库
原文:基于MMSeg算法的中文分词类库 最近在实现基于lucene.net的搜索方案,涉及中文分词,找了很多,最终选择了MMSeg4j,但MMSeg4j只有Java版,在博客园上找到了*王员外*(ht ...
- 中文分词系列(二) 基于双数组Tire树的AC自动机
秉着能偷懒就偷懒的精神,关于AC自动机本来不想看的,但是HanLp的源码中用户自定义词典的识别是用的AC自动机实现的.唉-没办法,还是看看吧 AC自动机理论 Aho Corasick自动机,简称AC自 ...
- 中文分词系列(一) 双数组Tire树(DART)详解
1 双数组Tire树简介 双数组Tire树是Tire树的升级版,Tire取自英文Retrieval中的一部分,即检索树,又称作字典树或者键树.下面简单介绍一下Tire树. 1.1 Tire树 Trie ...
- UVa 11732 (Tire树) "strcmp()" Anyone?
这道题也是卡了挺久的. 给出一个字符串比较的算法,有n个字符串两两比较一次,问一共会有多少次比较. 因为节点会很多,所以Tire树采用了左儿子右兄弟的表示法来节省空间. 假设两个不相等的字符串的最长公 ...
- Tire树的学习
Tire树是一种基于空间换时间思想的,应用于字符串处理的数据结构. 题目地址 分析:设DP数组Can[MaxL],Can[i]=1表示第i位可以理解. 当Can[i]==1,对第i+1位进行匹配,若能 ...
随机推荐
- WebRTC–getUserMedia & Canvas
下面是一个使用getUserMedia接口和Canvas的drawImage方法实现的截图功能(截取视频中的一帧). 基本思路是这样子的: getUserMedia获取一个MediaStream, s ...
- 手把手教你图片转ASCII码图
效果图 基本思路 把图片每个像素点的信息拿出来,最重要的是拿到rgb的值 把每个像素点由rgb转成灰度图像,即0-255 给0-255分级,把每个等级的像素点转换成ascii码,完成 实现 第一步:获 ...
- Windows Phone 8初学者开发—第11部分:设置SounBoard应用程序
原文 Windows Phone 8初学者开发—第11部分:设置SounBoard应用程序 原文地址: http://channel9.msdn.com/Series/Windows-Phone-8- ...
- CMake学习小结
假定有vegagis工程,工程的目录结构如下: #--vegagis# |--src 源文件目录# |--gui 界面工程,输出类型:dll,依赖于QT的QtCore.QtGui.QtXml ...
- cmake学习笔记(五)
在cmake 学习笔记(三) 中简单学习了 find_package 的 model 模式,在cmake 学习笔记(四)中了解一个CMakeCache相关的东西.但靠这些知识还是不能看懂PySide使 ...
- eclipse导出附带源码的jar包
最近在搞Andengine游戏开发,发现andengine的jar包可以直接点击查看源码,而其他项目的jar包却看不了,因此自己研究了下如何生成可以直接查看源码的jar包. 1.eclipse中点击项 ...
- 数学之路(3)-机器学习(3)-机器学习算法-SVM[9]
我们应用SVM的非线性分类功能对手写数字进行识别,我们在这应用poly做为非线性核 svm = mlpy.LibSvm(svm_type='c_svc', kernel_type='poly',gam ...
- ThinkPHP - 自定义扩展类库
首先在想要使用类库的地方建立文件夹,文件名称随意,不能使用class 然后在配置文件中: 'AUTOLOAD_NAMESPACE' => array( 'Lib' => './Lib', ...
- 【Hibernate】Illegal attempt to associate a collection with two open sessions
今天在用Hibernate对对象进行修改操作的时候报了这个错. 之前一直没什么错误,但是今天修改了一下表结构,增加了一个OneToMany的映射. 所以在我获取对象,重新set一个变量之后就报了这个错 ...
- javascript面向对象创建高级 Web 应用程序
目录 JavaScript 对象是词典 JavaScript 函数是最棒的 构造函数而不是类 原型 静态属性和方法 闭包 模拟私有属性 从类继承 模拟命名空间 应当这样编写 JavaScript ...