python双向最大匹配算法_中文分词算法 之 基于词典的逆向最大匹配算法_圈泉的博客-程序员秘密

技术标签: python双向最大匹配算法  

在之前的博文中介绍了基于词典的正向最大匹配算法,用了不到50行代码就实现了,然后分析了词典查找算法的时空复杂性,最后使用前缀树来实现词典查找算法,并做了3次优化。

下面我们看看基于词典的逆向最大匹配算法的实现,实验表明,对于汉语来说,逆向最大匹配算法比(正向)最大匹配算法更有效,如下代码所示:

public static List segReverse(String text){

Stack result = new Stack<>();

while(text.length()>0){

int len=MAX_LENGTH;

if(text.length()

len=text.length();

}

//取指定的最大长度的文本去词典里面匹配

String tryWord = text.substring(text.length() - len);

while(!DIC.contains(tryWord)){

//如果长度为一且在词典中未找到匹配,则按长度为一切分

if(tryWord.length()==1){

break;

}

//如果匹配不到,则长度减一继续匹配

tryWord=tryWord.substring(1);

}

result.push(tryWord);

//从待分词文本中去除已经分词的文本

text=text.substring(0, text.length()-tryWord.length());

}

int len=result.size();

List list = new ArrayList<>(len);

for(int i=0;i

list.add(result.pop());

}

return list;

}

算法跟正向相差不大,重点是使用Stack来存储分词结果,具体差异如下图所示:

5b50ff8b8ba0d4e92eb1bb63389b8295.png

下面看看正向和逆向的分词效果,使用如下代码:

public static void main(String[] args){

List sentences = new ArrayList<>();

sentences.add("杨尚川是APDPlat应用级产品开发平台的作者");

sentences.add("研究生命的起源");

sentences.add("长春市长春节致辞");

sentences.add("他从马上下来");

sentences.add("乒乓球拍卖完了");

sentences.add("咬死猎人的狗");

sentences.add("大学生活象白纸");

sentences.add("他有各种才能");

sentences.add("有意见分歧");

for(String sentence : sentences){

System.out.println("正向最大匹配: "+seg(sentence));

System.out.println("逆向最大匹配: "+segReverse(sentence));

}

}

运行结果如下:

开始初始化词典

完成初始化词典,词数目:427452

最大分词长度:16

正向最大匹配: [杨尚川, 是, APDPlat, 应用, 级, 产品开发, 平台, 的, 作者]

逆向最大匹配: [杨尚川, 是, APDPlat, 应用, 级, 产品开发, 平台, 的, 作者]

正向最大匹配: [研究生, 命, 的, 起源]

逆向最大匹配: [研究, 生命, 的, 起源]

正向最大匹配: [长春市, 长春, 节, 致辞]

逆向最大匹配: [长春, 市长, 春节, 致辞]

正向最大匹配: [他, 从, 马上, 下来]

逆向最大匹配: [他, 从, 马上, 下来]

正向最大匹配: [乒乓球拍, 卖完, 了]

逆向最大匹配: [乒乓球拍, 卖完, 了]

正向最大匹配: [咬, 死, 猎人, 的, 狗]

逆向最大匹配: [咬, 死, 猎人, 的, 狗]

正向最大匹配: [大学生, 活象, 白纸]

逆向最大匹配: [大学生, 活象, 白纸]

正向最大匹配: [他, 有, 各种, 才能]

逆向最大匹配: [他, 有, 各种, 才能]

正向最大匹配: [有意, 见, 分歧]

逆向最大匹配: [有, 意见分歧]

下面看看实际的分词性能如何,对输入文件进行分词,然后将分词结果保存到输出文件,输入文本文件从这里下载,解压后大小为69M,词典文件从这里下载,解压后大小为4.5M,项目源代码托管在GITHUB:

/**

* 将一个文件分词后保存到另一个文件

* @author 杨尚川

*/

public class SegFile {

public static void main(String[] args) throws Exception{

String input = "input.txt";

String output = "output.txt";

if(args.length == 2){

input = args[0];

output = args[1];

}

long start = System.currentTimeMillis();

segFile(input, output);

long cost = System.currentTimeMillis()-start;

System.out.println("cost time:"+cost+" ms");

}

public static void segFile(String input, String output) throws Exception{

float max=(float)Runtime.getRuntime().maxMemory()/1000000;

float total=(float)Runtime.getRuntime().totalMemory()/1000000;

float free=(float)Runtime.getRuntime().freeMemory()/1000000;

String pre="执行之前剩余内存:"+max+"-"+total+"+"+free+"="+(max-total+free);

try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(input),"utf-8"));

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(output),"utf-8"))){

int textLength=0;

long start = System.currentTimeMillis();

String line = reader.readLine();

while(line != null){

textLength += line.length();

writer.write(WordSeg.seg(line).toString()+"\n");

line = reader.readLine();

}

long cost = System.currentTimeMillis() - start;

float rate = textLength/cost;

System.out.println("文本字符:"+textLength);

System.out.println("分词耗时:"+cost+" 毫秒");

System.out.println("分词速度:"+rate+" 字符/毫秒");

}

max=(float)Runtime.getRuntime().maxMemory()/1000000;

total=(float)Runtime.getRuntime().totalMemory()/1000000;

free=(float)Runtime.getRuntime().freeMemory()/1000000;

String post="执行之后剩余内存:"+max+"-"+total+"+"+free+"="+(max-total+free);

System.out.println(pre);

System.out.println(post);

}

}

测试结果如下(对比TrieV3和HashSet的表现):

开始初始化词典

dic.class=org.apdplat.word.dictionary.impl.TrieV3

dic.path=dic.txt

完成初始化词典,耗时695 毫秒,词数目:427452

词典最大词长:16

词长  0 的词数为:1

词长  1 的词数为:11581

词长  2 的词数为:146497

词长  3 的词数为:162776

词长  4 的词数为:90855

词长  5 的词数为:6132

词长  6 的词数为:3744

词长  7 的词数为:2206

词长  8 的词数为:1321

词长  9 的词数为:797

词长 10 的词数为:632

词长 11 的词数为:312

词长 12 的词数为:282

词长 13 的词数为:124

词长 14 的词数为:116

词长 15 的词数为:51

词长 16 的词数为:25

词典平均词长:2.94809

字符数目:24960301

分词耗时:64014 毫秒

分词速度:389.0 字符/毫秒

执行之前剩余内存:2423.3901-61.14509+60.505272=2422.7505

执行之后剩余内存:2423.3901-961.08545+203.32925=1665.6339

cost time:64029 ms

开始初始化词典

dic.class=org.apdplat.word.dictionary.impl.HashSet

dic.path=dic.txt

完成初始化词典,耗时293 毫秒,词数目:427452

词典最大词长:16

词长  0 的词数为:1

词长  1 的词数为:11581

词长  2 的词数为:146497

词长  3 的词数为:162776

词长  4 的词数为:90855

词长  5 的词数为:6132

词长  6 的词数为:3744

词长  7 的词数为:2206

词长  8 的词数为:1321

词长  9 的词数为:797

词长 10 的词数为:632

词长 11 的词数为:312

词长 12 的词数为:282

词长 13 的词数为:124

词长 14 的词数为:116

词长 15 的词数为:51

词长 16 的词数为:25

词典平均词长:2.94809

字符数目:24960301

分词耗时:77254 毫秒

分词速度:323.0 字符/毫秒

执行之前剩余内存:2423.3901-61.14509+60.505295=2422.7505

执行之后剩余内存:2423.3901-900.46466+726.91455=2249.84

cost time:77271 ms

在上篇文章基于词典的正向最大匹配算法中,我们已经优化了词典查找算法(DIC.contains(tryWord))的性能(百万次查询只要一秒左右的时间),即使经过优化后TrieV3仍然比HashSet慢4倍,也不影响它在分词算法中的作用,从上面的数据可以看到,TrieV3的整体分词性能领先HashSet十五个百分点(15%),而且内存占用只有HashSet的80%。

如何来优化分词算法呢?分词算法有什么问题吗?

回顾一下代码:

public static List seg(String text){

List result = new ArrayList<>();

while(text.length()>0){

int len=MAX_LENGTH;

if(text.length()

len=text.length();

}

//取指定的最大长度的文本去词典里面匹配

String tryWord = text.substring(0, 0+len);

while(!DIC.contains(tryWord)){

//如果长度为一且在词典中未找到匹配,则按长度为一切分

if(tryWord.length()==1){

break;

}

//如果匹配不到,则长度减一继续匹配

tryWord=tryWord.substring(0, tryWord.length()-1);

}

result.add(tryWord);

//从待分词文本中去除已经分词的文本

text=text.substring(tryWord.length());

}

return result;

}

分析一下算法复杂性,最坏情况为切分出来的每个词的长度都为一(即DIC.contains(tryWord)始终为false),因此算法的复杂度约为外层循环数*内层循环数(即 文本长度*最大词长)=25025017*16=400400272,以TrieV3的查找性能来说,4亿次查询花费的时间大约8分钟左右。

进一步查看算法,发现外层循环有2个substring方法调用,内层循环有1个substring方法调用,substring方法内部new了一个String对象,构造String对象的时候又调用了System.arraycopy来拷贝数组。

最坏情况下,25025017*2+25025017*16=50050034+400400272=450450306,需要构造4.5亿个String对象和拷贝4.5亿次数组。

怎么来优化呢?

除了我们不得不把切分出来的词加入result中外,其他的两个substring是可以去掉的。这样,最坏情况下我们需要构造的String对象个数和拷贝数组的次数就从4.5亿次降低为25025017次,只有原来的5.6%。

看看改进后的代码:

public static List seg(String text){

List result = new ArrayList<>();

//文本长度

final int textLen=text.length();

//从未分词的文本中截取的长度

int len=DIC.getMaxLength();

//剩下未分词的文本的索引

int start=0;

//只要有词未切分完就一直继续

while(start

if(len>textLen-start){

//如果未分词的文本的长度小于截取的长度

//则缩短截取的长度

len=textLen-start;

}

//用长为len的字符串查词典

while(!DIC.contains(text, start, len)){

//如果长度为一且在词典中未找到匹配

//则按长度为一切分

if(len==1){

break;

}

//如果查不到,则长度减一后继续

len--;

}

result.add(text.substring(start, start+len));

//从待分词文本中向后移动索引,滑过已经分词的文本

start+=len;

//每一次成功切词后都要重置截取长度

len=DIC.getMaxLength();

}

return result;

}

public static List segReverse(String text){

Stack result = new Stack<>();

//文本长度

final int textLen=text.length();

//从未分词的文本中截取的长度

int len=DIC.getMaxLength();

//剩下未分词的文本的索引

int start=textLen-len;

//处理文本长度小于最大词长的情况

if(start<0){

start=0;

}

if(len>textLen-start){

//如果未分词的文本的长度小于截取的长度

//则缩短截取的长度

len=textLen-start;

}

//只要有词未切分完就一直继续

while(start>=0 && len>0){

//用长为len的字符串查词典

while(!DIC.contains(text, start, len)){

//如果长度为一且在词典中未找到匹配

//则按长度为一切分

if(len==1){

break;

}

//如果查不到,则长度减一

//索引向后移动一个字,然后继续

len--;

start++;

}

result.push(text.substring(start, start+len));

//每一次成功切词后都要重置截取长度

len=DIC.getMaxLength();

if(len>start){

//如果未分词的文本的长度小于截取的长度

//则缩短截取的长度

len=start;

}

//每一次成功切词后都要重置开始索引位置

//从待分词文本中向前移动最大词长个索引

//将未分词的文本纳入下次分词的范围

start-=len;

}

len=result.size();

List list = new ArrayList<>(len);

for(int i=0;i

list.add(result.pop());

}

return list;

}

对于正向最大匹配算法,代码行数从23增加为33,对于逆向最大匹配算法,代码行数从28增加为51,除了代码行数的增加,代码更复杂,可读性和可维护性也更差,这就是性能的代价!所以,不要过早优化,不要做不成熟的优化,因为不是所有的场合都需要高性能,在数据规模未达到一定程度的时候,各种算法和数据结构的差异表现不大,至少那个差异对你无任何影响。你可能会说,要考虑到明天,要考虑将来,你有你自己的道理,不过,我还是坚持不过度设计,不过早设计,通过单元测试和持续重构来应对变化,不为遥不可及的将来浪费今天,下一秒会发生什么谁知道呢?更不用说明天!因为有单元测试这张安全防护网,所以在出现性能问题的时候,我们可以放心、大胆、迅速地重构来优化性能。

下面看看改进之后的性能(对比TrieV3和HashSet的表现):

开始初始化词典

dic.class=org.apdplat.word.dictionary.impl.TrieV3

dic.path=dic.txt

完成初始化词典,耗时689 毫秒,词数目:427452

词典最大词长:16

词长  0 的词数为:1

词长  1 的词数为:11581

词长  2 的词数为:146497

词长  3 的词数为:162776

词长  4 的词数为:90855

词长  5 的词数为:6132

词长  6 的词数为:3744

词长  7 的词数为:2206

词长  8 的词数为:1321

词长  9 的词数为:797

词长 10 的词数为:632

词长 11 的词数为:312

词长 12 的词数为:282

词长 13 的词数为:124

词长 14 的词数为:116

词长 15 的词数为:51

词长 16 的词数为:25

词典平均词长:2.94809

字符数目:24960301

分词耗时:24782 毫秒

分词速度:1007.0 字符/毫秒

执行之前剩余内存:2423.3901-61.14509+60.505272=2422.7505

执行之后剩余内存:2423.3901-732.0371+308.87476=2000.2278

cost time:25007 ms

开始初始化词典

dic.class=org.apdplat.word.dictionary.impl.HashSet

dic.path=dic.txt

完成初始化词典,耗时293 毫秒,词数目:427452

词典最大词长:16

词长  0 的词数为:1

词长  1 的词数为:11581

词长  2 的词数为:146497

词长  3 的词数为:162776

词长  4 的词数为:90855

词长  5 的词数为:6132

词长  6 的词数为:3744

词长  7 的词数为:2206

词长  8 的词数为:1321

词长  9 的词数为:797

词长 10 的词数为:632

词长 11 的词数为:312

词长 12 的词数为:282

词长 13 的词数为:124

词长 14 的词数为:116

词长 15 的词数为:51

词长 16 的词数为:25

词典平均词长:2.94809

字符数目:24960301

分词耗时:40913 毫秒

分词速度:610.0 字符/毫秒

执行之前剩余内存:907.8702-61.14509+60.505295=907.2304

执行之后剩余内存:907.8702-165.4784+123.30369=865.6955

cost time:40928 ms

可以看到分词算法优化的效果很明显,对于TrieV3来说,提升了2.5倍,对于HashSet来说,提升了1.9倍。我们看看HashSet的实现:

public class HashSet implements Dictionary{

private Set set = new java.util.HashSet<>();

private int maxLength;

@Override

public int getMaxLength() {

return maxLength;

}

@Override

public boolean contains(String item, int start, int length) {

return set.contains(item.substring(start, start+length));

}

@Override

public boolean contains(String item) {

return set.contains(item);

}

@Override

public void addAll(List items) {

for(String item : items){

add(item);

}

}

@Override

public void add(String item) {

//去掉首尾空白字符

item=item.trim();

int len = item.length();

if(len 

//长度小于1则忽略

return;

}

if(len>maxLength){

maxLength=len;

}

set.add(item);

}

}

JDK的HashSet没有这里优化所使用的contains(String item, int start, int length)方法,所以用了substring,这是HashSet提速没有TrieV3大的原因之一。

看一下改进的算法和原来的算法的对比:

正向最大匹配算法:ef8123b8c579cbb98a850c631156c040.png

逆向最大匹配算法:

2ead50d18339549429635bfa1b31f4d6.png

参考资料:

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_36344862/article/details/113508795

智能推荐

设计模式 -- 工厂模式_工厂设计模式csdn_赵不酷的博客-程序员秘密

所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。理论介绍工厂方法模式:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。抽象工厂模式:提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。应用场景工厂方法模式:可以将你的客户代码从需要实例化的具...

Fortran入门教程(二)——数据类型_fortran character_Sumbrella_的博客-程序员秘密

数据类型数据类型是指在计算机中能够记录文本、数值等的数据单位。算法处理的对象是数据,而数据是以某种特定的形式(如整数、实数、字符等形式)存在的。不同的数据之间往往还存在某些联系,例如由若干个整数组成一个整数数组。1. 变量声明隐式声明(不再使用)隐式声明是传统 Fortran 语言预先定义且无须通过类型声明语句对变量类型进行定义,习惯称为I-N规则。Fortran 规定,凡以字母I、J、K、L、M、N(无论大写还是 小写)6个字母开头的变量名,如无另外说明则为整型变量。以其他字母开头的变量被默认为

beeline软件_Beeline_weixin_39938269的博客-程序员秘密

Beeline是一个转为优化国服游戏打造的多功能网络加速应用。可以快速解决手游网络卡顿、延迟、掉线、丢包、跳红跳蓝、加载缓慢等问题。并且有着多个线路可以选择,能智能选择线路、降低延迟、断线重连、防止卡顿,提高网络稳定性。同时还支持一些锁国区的手机应用,让你可以看国内的视频和听音乐。Beeline软件特色【国服手游,一键加速】Beeline游戏加速器支持:王者荣耀、和平精英、第五人格、阴阳师、梦幻西...

Flink on yarn_Amos_Mu的博客-程序员秘密

1.Flink on yarn执行方式和提交命令第一种:是先开辟资源然后在进行资源的调度使用,开辟的资源是供所有的flink进程来使用的,如果某一时刻没有flink程序执行开辟的资源会空转等待新的flink进程。第二种:是一边开辟资源一边进行使用,一个资源供一个flink进程使用,flink进程执行完毕之后就释放资源。 flink的提交命令: ...

Android - java native 异常捕获到本地 - xCrash 简单实现 ,亲测可直接使用_android xcrash_Rainbow Snake的博客-程序员秘密

需求:当APP出现Java异常、native异常和ANR时需要重启当前APP。解决方案:使用爱奇艺的xCrash框架进行捕获并重启。步骤一:在module的build.gradle中添加如下代码:android { defaultConfig { ndk { // 根据需要添加必要的ABI abiFilters 'armeabi', 'armeabi-v7a', 'arm...

这几个冷门但实用的 Python 技巧你还不知道?_wade1203的博客-程序员秘密

这篇文章主要和大家分享一些 Python 不一样的技巧,感受 Python 带给你的乐趣吧。1.print 打印带有颜色的信息大家知道 Python 中的信息打印函数 P...

随便推点

高并发详细讲解以实现_如何实现高并发_gua_niu123的博客-程序员秘密

一、什么是高并发高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。吞吐量:单位时间内处理的请求数量。QPS:每秒响应请求数。在互

HDU - 1045 Fire Net 【DFS】_牧心.的博客-程序员秘密

DescriptionSuppose that we have a square city with straight streets. A map of a city is a square board with n rows and n columns, each representing a street or a piece of wall.A blockhouse is a sm...

【企业框架源码】 SpringMVC mybatis 多数据源 代码生成器 SSM java redis shiro ehcache_weixin_34318272的博客-程序员秘密

获取【下载地址】 QQ: 313596790官网 http://www.fhadmin.org/A代码编辑器,在线模版编辑,仿开发工具编辑器,pdf在线预览,文件转换编码B 集成代码生成器 [正反双向](单表、主表、明细表、树形表,快速开发利器)+快速表单构建器freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本,处理类,service等完整模块C...

appium---第一个脚本--启动一个已存在的app_baodiaoxe346599的博客-程序员秘密

1、可以使用android-sdk中的aapt工具①、选择一个版本的build_tools,加入path环境变量中②、验证aapt环境是否正常3、下载你要测试的包到本地,放入某一地址中(随意):aapt dump badging D:\Users\4admin\Desktop\jianshu_xpgod.apk(包的位置)然后就可以获得包的所有信息,...

关于华为机试的一点建议_twc829的博客-程序员秘密

年前听师姐说华为三月份有实习招聘,让我好好准备。听说有机试,如果这次不过,等到下半年的校招都会没有资格再报技术岗。为了与自己的目标更进一步,年后回到学校,我每天都在看机试题目。资源一般都是百度上别人总结的,题目确实很多,但很杂,没有条理,最关键的是没法确保别人的代码一定正确。(PS:这里的“正确”指的是能通过华为OJ标准)我练习了一部分机试题目,最后也只是验证了程序对于样例数据的正