对于分词系统的实现来说,主要应集中在两方面的考虑上:一是对语料库的组织,二是分词策略的制订。

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实现的更多相关文章

  1. 使用Python,字标注及最大熵法进行中文分词

    使用Python,字标注及最大熵法进行中文分词 在前面的博文中使用python实现了基于词典及匹配的中文分词,这里介绍另外一种方法, 这种方法基于字标注法,并且基于最大熵法,使用机器学习方法进行训练, ...

  2. PyTorch 高级实战教程:基于 BI-LSTM CRF 实现命名实体识别和中文分词

    前言:译者实测 PyTorch 代码非常简洁易懂,只需要将中文分词的数据集预处理成作者提到的格式,即可很快的就迁移了这个代码到中文分词中,相关的代码后续将会分享. 具体的数据格式,这种方式并不适合处理 ...

  3. 转:从头开始编写基于隐含马尔可夫模型HMM的中文分词器

    http://blog.csdn.net/guixunlong/article/details/8925990 从头开始编写基于隐含马尔可夫模型HMM的中文分词器之一 - 资源篇 首先感谢52nlp的 ...

  4. 基于Deep Learning的中文分词尝试

    http://h2ex.com/1282 现有分词介绍 自然语言处理(NLP,Natural Language Processing)是一个信息时代最重要的技术之一,简单来讲,就是让计算机能够理解人类 ...

  5. 基于MMSeg算法的中文分词类库

    原文:基于MMSeg算法的中文分词类库 最近在实现基于lucene.net的搜索方案,涉及中文分词,找了很多,最终选择了MMSeg4j,但MMSeg4j只有Java版,在博客园上找到了*王员外*(ht ...

  6. 中文分词系列(二) 基于双数组Tire树的AC自动机

    秉着能偷懒就偷懒的精神,关于AC自动机本来不想看的,但是HanLp的源码中用户自定义词典的识别是用的AC自动机实现的.唉-没办法,还是看看吧 AC自动机理论 Aho Corasick自动机,简称AC自 ...

  7. 中文分词系列(一) 双数组Tire树(DART)详解

    1 双数组Tire树简介 双数组Tire树是Tire树的升级版,Tire取自英文Retrieval中的一部分,即检索树,又称作字典树或者键树.下面简单介绍一下Tire树. 1.1 Tire树 Trie ...

  8. UVa 11732 (Tire树) "strcmp()" Anyone?

    这道题也是卡了挺久的. 给出一个字符串比较的算法,有n个字符串两两比较一次,问一共会有多少次比较. 因为节点会很多,所以Tire树采用了左儿子右兄弟的表示法来节省空间. 假设两个不相等的字符串的最长公 ...

  9. Tire树的学习

    Tire树是一种基于空间换时间思想的,应用于字符串处理的数据结构. 题目地址 分析:设DP数组Can[MaxL],Can[i]=1表示第i位可以理解. 当Can[i]==1,对第i+1位进行匹配,若能 ...

随机推荐

  1. 第一节 UPC 码

    UPC码(Universal Product Code)是最早大规模应用的条码,其特性是一种长度固定.连续性的条码,目前主要在美国和加拿大使用,由於其应用范围广泛,故又被称万用条码. UPC码仅可用来 ...

  2. VC程序快速删除自己(可能做升级程序的时候有用)

    项目一般都会带有卸载程序,如果这个程序是自己来做的话,在调用完卸载程序后需要删除自己的所有文件,在Google了好久终于找到一些相关信息,一般只能删除一个文件,经过自己的处理,可以删除文件夹下面所有内 ...

  3. Qt的信号槽,一个老MFC的经验

    最近在利用闲暇时间研究Qt,大概有3周了,看过了官网的white paper并浏览了一遍<C++ GUI Programming with Qt 4, 2nd Edition>.总的来说, ...

  4. Ubuntu上用premake编译GDAL

    GDAL的编译脚本呈现出不同平台不同解决方案的百花齐放现状.我是从windows平台开始编译GDAL的,用的自然是nmake.那就是一种每个目录下都需要写makefile文件的构建方法,写的人麻烦,我 ...

  5. vs使代码可以折叠的方法

    set [工具]->[选项]->[文本编辑器]->[C/C++]->[查看]->[大纲显示]->[大纲语句块] = True

  6. Objective-C 类,实例成员,静态变量,对象方法,类方法(静态方法),对象,

    Objective-C 类,实例成员,静态变量,对象方法,类方法(静态方法),对象, 一.类 在ios中,类的声明和实现时分离的,也就是说不能写在同一个文件中,声明放在 .h文件中,实现放在 .m 文 ...

  7. 第一种:NStread

    - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typica ...

  8. POJ 1692 Crossed Matchings(DP)

    Description There are two rows of positive integer numbers. We can draw one line segment between any ...

  9. Android经常使用UI组件 - TextView

    TextView是Android里面用的最多的UI组件,一般使用在须要显示一些信息的时候,其不能输入,仅仅能初始设定或者在程序中改动. 实例:TextViewDemo 执行效果: 代码清单: 布局文件 ...

  10. search_word

    一个小程序,用asc码输出自己的名字.要求是,a~z两路输入,输出了一个完整的拼音之后还需要输出一个空格.—— 信息硬件过滤的雏形. module search_word ( clock , rese ...