一直都想着自己动手写一写中文分词,但是一直都没有动手。今天终于开始了。从最简单的开始,步步深入。希望自己最后能把分词、词性标注、命名实体识别这几块都完成。
好了,话不多述,进入正题。
分词最简单的思路就是查词典,确实,最开始大家都是这么做的。包括现在都有人这样做。所以分词效果的好坏最重要的是要有一部好词典,及一个好的匹配算法。
第一步:找到好词典。
我分词开始用的是在搜狗实验室弄到一份通词典。一共有15万七千多词;后来又找到了一个词典,有28万词。在分词中用的是28万词的词典,我想简单的应用应该够了。
28万词的词典下载地址:http://www.xunsearch.com/scws/download.php,同时这里也实现了一个php的分词。
第二步:确定好算法。
这里的学习主要参考了52nlp里面的一个博客:
http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation
讲得很详细,还有C++版本的分词实现。
基于博客,我简单地用Java实现了一下:
package com.xh.seg; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.xh.dictionary.DictionaryUtil; public class Segmentation { private StringBuilder sb=new StringBuilder(); private List<String> ls=new ArrayList<String>(); private int maxwl;//设置词的最大长度 /* * @param source 待分词的句子 * @return 分词后的结果,用空格隔开。 * */ public String seg_str(String source){ sb.delete(0, sb.length()); int len; String word; maxwl=5; while((len=source.length())>0){ if(maxwl>len){ maxwl=len; } word=source.substring(len-maxwl); int start=len-maxwl; boolean find=false; //从左边不断减字,直到匹配成功或者字的长度为一 while(word.length()>1){ if(DictionaryUtil.match(word)){ sb.insert(0, "\t"+word);find=true; break; } ++start; word=source.substring(start); } if(!find){ sb.insert(0,"\t"+ word); } source=source.substring(0, start); } return sb.toString(); } /* * @param source 待分词的句子 * @return 分词后的结果,用list存储。 * */ public List<String> seg_list(String source){ ls.clear(); int len; String word; maxwl=5; while((len=source.length())>0){ if(maxwl>len){ maxwl=len; } word=source.substring(len-maxwl); int start=len-maxwl; boolean find=false; while(word.length()>1){ if(DictionaryUtil.match(word)){ ls.add(word);find=true; break; } ++start; word=source.substring(start); } if(!find){ ls.add(word); } source=source.substring(0, start); } Collections.reverse(ls); return ls; } }
程序里面的两个方法是一样的,只是返回结果不一样而已。
词典的加载程序如下:
package com.xh.dictionary; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; public class DictionaryUtil { static Map<String,Integer> dic=new HashMap<String,Integer>(160000); static{ //loadDic("dictionary/SougouLabDic_utf8.txt"); loadDic("dictionary/dic_utf8.txt"); } public static void loadDic(String name){ BufferedReader br=null; try { br=new BufferedReader(new InputStreamReader(new FileInputStream(DictionaryUtil.class .getClassLoader() .getResource("dictionary/SogouLabDic_utf8.dic") .getFile()),"utf-8")); String line; int end; while((line=br.readLine())!=null){ end=line.indexOf('\t'); if(end!=-1) dic.put(line.substring(0, end), 1); } } catch (Exception e) { e.printStackTrace(); }finally{try {br.close();} catch (IOException e) {e.printStackTrace();}} System.out.println("dic load success,have been load "+dic.size()+" words!"); } public static boolean match(String word){ if(dic.containsKey(word)) return true; return false; } }
当时考虑了下,感觉用set会更直接,但是set是建立在map的基础上的,所以直接用map的性能应该会高一点,当然只是猜想。
第三步:测试
测试代码如下:
package com.xh.seg; import org.junit.Test; public class SegmentationTest { @Test public void testSeg_str(){ Segmentation seg=new Segmentation(); String str[]={ "按自己喜欢选择的,质量不错", "结婚的和尚未结婚的都需要登记", "他说的确实在理", "阿拉斯加遭强暴风雪袭击致xx人死亡", "邓颖超生前使用过的物品 ", "乒乓球拍卖完了", "吴江西陵印刷厂" }; for (String string : str) { System.out.println(seg.seg_list(string)); } } }
分词的结果如下:
dic load success,have been load 284646 words!
[按, 自己, 喜欢, 选择, 的, ,, 质量, 不错]
[结婚, 的, 和, 尚未, 结婚, 的, 都, 需要, 登记]
[他, 说, 的, 确实, 在理]
[阿拉斯加, 遭, 强, 暴风雪, 袭击, 致, x, x, 人, 死亡]
[邓颖超, 生前, 使用, 过, 的, 物品, ]
[乒乓球, 拍卖, 完了]
[吴江, 西陵, 印刷厂]
第四步:总结
除了“乒乓球拍卖完了”这句分错外,其它的都没什么问题。可见,逆向最大匹配算法在词典够完善的情况下,分词的效果还是勉强可以接受的。毕竟是这么简单的代码实现。最后还是感谢提供词典和分词思路的作者。接下来将想学习双数组Trie树结构在分词上的实现。
参考文献:
(分词算法) http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation
(词 典)http://www.xunsearch.com/scws/download.php
(测试数据) http://my.oschina.net/jcseg/blog/135746