HanLP: Han Language Processing
汉语言处理包
HanLP是由一系列模型与算法组成的Java工具包,目标是促进自然语言处理在生产环境中的应用。HanLP具备功能完善、性能高效、架构清晰、语料时新、可自定义的特点。
HanLP提供下列功能:
-
中文分词
-
最短路分词
-
N-最短路分词
-
CRF分词
-
索引分词
-
极速词典分词
-
用户自定义词典
-
词性标注
-
命名实体识别
-
中国人名识别
-
音译人名识别
-
日本人名识别
-
地名识别
-
实体机构名识别
-
关键词提取
-
自动摘要
-
短语提取
-
拼音转换
-
简繁转换
-
文本推荐
-
依存句法分析
-
语料库工具
-
分词语料预处理
-
词频词性词典制作
-
BiGram统计
-
词共现统计
-
CoNLL语料预处理
-
CoNLL UA/LA/DA评测工具
在提供丰富功能的同时,HanLP内部模块坚持低耦合、模型坚持惰性加载、服务坚持静态提供、词典坚持明文发布,使用非常方便,同时自带一些语料处理工具,帮助用户训练自己的语料。
项目地址
HanLP项目主页:https://github.com/hankcs/HanLP
HanLP下载地址:https://github.com/hankcs/HanLP/releases
内存要求
内存120MB以上(-Xms120m -Xmx120m -Xmn64m),标准数据包(35万核心词库+默认用户词典),分词测试正常。
全部词典和模型都是惰性加载的,如果你只用拼音转换的话,则只加载拼音词典,未加载的词典相当于不存在,不占内存。同理,模型也是如此。
下载与配置
方式一、通过Maven的pom.xml
为了方便用户,特提供内置了数据包的Portable版,只需在pom.xml加入:
- <dependency>
- <groupId>com.hankcs</groupId>
- <artifactId>hanlp</artifactId>
- <version>portable-1.2.4</version>
- </dependency>
零配置,即可使用基本功能(除CRF分词、依存句法分析外的全部功能)。连Maven都懒得用的话,可以直接下载portable版的jar。
如果用户有自定义的需求,可以参考方式二,使用hanlp.properties进行配置。
目前Portable体积仅仅5.7MB,作为代价,使用的是1998年的小词典,对现代汉语的支持有限;所以还是建议外挂下面的数据包比较好。
方式二、下载jar、data、hanlp.properties
HanLP将数据与程序分离,给予用户自定义的自由。
1、下载jar
hanlp.jar
2、下载data
数据包 | 功能 | 体积(MB) |
---|
data.zip | 全部词典,全部模型 | 280(注:分词词典大约40MB,主要是句法分析模型占体积,可以自行删除。) |
在GitHub的release页面Ctrl+F搜索data即可,下载后解压到任意目录,接下来通过配置文件告诉HanLP数据包的位置。
HanLP中的数据分为词典和模型,其中词典是词法分析必需的,模型是句法分析必需的。
- data
- │
- ├─dictionary
- └─model
用户可以自行增删替换,如果不需要句法分析功能的话,随时可以删除model文件夹。
3、配置文件
示例配置文件:hanlp.properties
配置文件的作用是告诉HanLP数据包的位置,只需修改第一行
- root=usr/home/HanLP/
为data的父目录即可,比如data目录是/Users/hankcs/Documents/data
,那么root=/Users/hankcs/Documents/
。
最后将HanLP.properties放入classpath即可,对于Eclipse,一般是:
- $Project/bin
Web项目的话可以放在如下位置:
- $Project/WEB-INF/classes
对于任何项目,都可以放到src目录下,编译时IDE会自动将其复制到classpath中。
如果放置不当,HanLP会智能提示当前环境下的合适路径,并且尝试从项目根目录读取数据集。
调用方法
HanLP几乎所有的功能都可以通过工具类HanLP
快捷调用,当你想不起来调用方法时,只需键入HanLP.
,IDE应当会给出提示,并展示HanLP完善的文档。
推荐用户始终通过工具类HanLP
调用,这么做的好处是,将来HanLP升级后,用户无需修改调用代码。
所有Demo都位于com.hankcs.demo下。
1. 第一个Demo
- System.out.println(HanLP.segment("你好,欢迎使用HanLP汉语处理包!"));
2. 标准分词
- List<Term> termList = StandardTokenizer.segment("商品和服务");
- System.out.println(termList);
-
说明
-
HanLP中有一系列“开箱即用”的静态分词器,以Tokenizer
结尾,在接下来的例子中会继续介绍。
-
HanLP.segment
其实是对StandardTokenizer.segment
的包装。
-
分词结果包含词性,每个词性的意思请查阅《HanLP词性标注集》。
-
算法详解
3. NLP分词
- List<Term> termList = NLPTokenizer.segment("中国科学院计算技术研究所的宗成庆教授正在教授自然语言处理课程");
- System.out.println(termList);
4. 索引分词
- List<Term> termList = IndexTokenizer.segment("主副食品");
- for (Term term : termList)
- {
- System.out.println(term + " [" + term.offset + ":" + (term.offset + term.word.length()) + "]");
- }
5. N-最短路径分词
- Segment nShortSegment = new NShortSegment().enableCustomDictionary(false).enablePlaceRecognize(true).enableOrganizationRecognize(true);
- Segment shortestSegment = new DijkstraSegment().enableCustomDictionary(false).enablePlaceRecognize(true).enableOrganizationRecognize(true);
- String[] testCase = new String[]{
- "今天,刘志军案的关键人物,山西女商人丁书苗在市二中院出庭受审。",
- "刘喜杰石国祥会见吴亚琴先进事迹报告团成员",
- };
- for (String sentence : testCase)
- {
- System.out.println("N-最短分词:" + nShortSegment.seg(sentence) + "\n最短路分词:" + shortestSegment.seg(sentence));
- }
6. CRF分词
- /**
- * CRF分词(在最新训练的未压缩100MB模型下,能够取得较好的效果,可以投入生产环境)
- *
- * @author hankcs
- */
- public class DemoCRFSegment
- {
- public static void main(String[] args)
- {
- HanLP.Config.ShowTermNature = false; // 关闭词性显示
- Segment segment = new CRFSegment();
- String[] sentenceArray = new String[]
- {
- "HanLP是由一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中的应用。",
- "鐵桿部隊憤怒情緒集結 馬英九腹背受敵", // 繁体无压力
- "馬英九回應連勝文“丐幫說”:稱黨內同志談話應謹慎",
- "高锰酸钾,强氧化剂,紫红色晶体,可溶于水,遇乙醇即被还原。常用作消毒剂、水净化剂、氧化剂、漂白剂、毒气吸收剂、二氧化碳精制剂等。", // 专业名词有一定辨识能力
- "《夜晚的骰子》通过描述浅草的舞女在暗夜中扔骰子的情景,寄托了作者对庶民生活区的情感", // 非新闻语料
- "这个像是真的[委屈]前面那个打扮太江户了,一点不上品...@hankcs", // 微博
- "鼎泰丰的小笼一点味道也没有...每样都淡淡的...淡淡的,哪有食堂2A的好次",
- "克里斯蒂娜·克罗尔说:不,我不是虎妈。我全家都热爱音乐,我也鼓励他们这么做。",
- "今日APPS:Sago Mini Toolbox培养孩子动手能力",
- "财政部副部长王保安调任国家统计局党组书记",
- "2.34米男子娶1.53米女粉丝 称夫妻生活没问题",
- "你看过穆赫兰道吗",
- "乐视超级手机能否承载贾布斯的生态梦"
- };
- for (String sentence : sentenceArray)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
- }
- }
7. 极速词典分词
- /**
- * 演示极速分词,基于AhoCorasickDoubleArrayTrie实现的词典分词,适用于“高吞吐量”“精度一般”的场合
- * @author hankcs
- */
- public class DemoHighSpeedSegment
- {
- public static void main(String[] args)
- {
- String text = "江西鄱阳湖干枯,中国最大淡水湖变成大草原";
- System.out.println(SpeedTokenizer.segment(text));
- long start = System.currentTimeMillis();
- int pressure = 1000000;
- for (int i = 0; i < pressure; ++i)
- {
- SpeedTokenizer.segment(text);
- }
- double costTime = (System.currentTimeMillis() - start) / (double)1000;
- System.out.printf("分词速度:%.2f字每秒", text.length() * pressure / costTime);
- }
- }
-
说明
-
极速分词是词典最长分词,速度极其快,精度一般。
-
在i7上跑出了2000万字每秒的速度。
-
算法详解
8. 用户自定义词典
- public class DemoCustomDictionary
- {
- public static void main(String[] args)
- {
- // 动态增加
- CustomDictionary.add("攻城狮");
- // 强行插入
- CustomDictionary.insert("白富美", "nz 1024");
- // 删除词语(注释掉试试)
- // CustomDictionary.remove("攻城狮");
- System.out.println(CustomDictionary.add("单身狗", "nz 1024 n 1"));
- System.out.println(CustomDictionary.get("单身狗"));
-
- String text = "攻城狮逆袭单身狗,迎娶白富美,走上人生巅峰"; // 怎么可能噗哈哈!
-
- // DoubleArrayTrie分词
- final char[] charArray = text.toCharArray();
- CustomDictionary.parseText(charArray, new AhoCorasickDoubleArrayTrie.IHit<CoreDictionary.Attribute>()
- {
- @Override
- public void hit(int begin, int end, CoreDictionary.Attribute value)
- {
- System.out.printf("[%d:%d]=%s %s\n", begin, end, new String(charArray, begin, end - begin), value);
- }
- });
- // 首字哈希之后二分的trie树分词
- BaseSearcher searcher = CustomDictionary.getSearcher(text);
- Map.Entry entry;
- while ((entry = searcher.next()) != null)
- {
- System.out.println(entry);
- }
-
- // 标准分词
- System.out.println(HanLP.segment(text));
-
- // Note:动态增删不会影响词典文件
- // 目前CustomDictionary使用DAT储存词典文件中的词语,用BinTrie储存动态加入的词语,前者性能高,后者性能低
- // 之所以保留动态增删功能,一方面是历史遗留特性,另一方面是调试用;未来可能会去掉动态增删特性。
- }
-
说明
-
追加词典
-
CustomDictionary
主词典文本路径是data/dictionary/custom/CustomDictionary.txt
,用户可以在此增加自己的词语(不推荐);也可以单独新建一个文本文件,通过配置文件CustomDictionaryPath=data/dictionary/custom/CustomDictionary.txt; 我的词典.txt;
来追加词典(推荐)。
-
始终建议将相同词性的词语放到同一个词典文件里,便于维护和分享。
-
词典格式
-
算法详解
9. 中国人名识别
- String[] testCase = new String[]{
- "签约仪式前,秦光荣、李纪恒、仇和等一同会见了参加签约的企业家。",
- "王国强、高峰、汪洋、张朝阳光着头、韩寒、小四",
- "张浩和胡健康复员回家了",
- "王总和小丽结婚了",
- "编剧邵钧林和稽道青说",
- "这里有关天培的有关事迹",
- "龚学平等领导,邓颖超生前",
- };
- Segment segment = HanLP.newSegment().enableNameRecognize(true);
- for (String sentence : testCase)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
-
说明
-
目前分词器基本上都默认开启了中国人名识别,比如HanLP.segment()
接口中使用的分词器等等,用户不必手动开启;上面的代码只是为了强调。
-
有一定的误命中率,比如误命中关键年
,则可以通过在data/dictionary/person/nr.txt
加入一条关键年 A 1
来排除关键年
作为人名的可能性,也可以将关键年
作为新词登记到自定义词典中。
-
如果你通过上述办法解决了问题,欢迎向我提交pull request,词典也是宝贵的财富。
-
算法详解
9. 音译人名识别
- String[] testCase = new String[]{
- "一桶冰水当头倒下,微软的比尔盖茨、Facebook的扎克伯格跟桑德博格、亚马逊的贝索斯、苹果的库克全都不惜湿身入镜,这些硅谷的科技人,飞蛾扑火似地牺牲演出,其实全为了慈善。",
- "世界上最长的姓名是简森·乔伊·亚历山大·比基·卡利斯勒·达夫·埃利奥特·福克斯·伊维鲁莫·马尔尼·梅尔斯·帕特森·汤普森·华莱士·普雷斯顿。",
- };
- Segment segment = HanLP.newSegment().enableTranslatedNameRecognize(true);
- for (String sentence : testCase)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
11. 日本人名识别
- String[] testCase = new String[]{
- "北川景子参演了林诣彬导演的《速度与激情3》",
- "林志玲亮相网友:确定不是波多野结衣?",
- };
- Segment segment = HanLP.newSegment().enableJapaneseNameRecognize(true);
- for (String sentence : testCase)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
12. 地名识别
- String[] testCase = new String[]{
- "武胜县新学乡政府大楼门前锣鼓喧天",
- "蓝翔给宁夏固原市彭阳县红河镇黑牛沟村捐赠了挖掘机",
- };
- Segment segment = HanLP.newSegment().enablePlaceRecognize(true);
- for (String sentence : testCase)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
13. 机构名识别
- String[] testCase = new String[]{
- "我在上海林原科技有限公司兼职工作,",
- "我经常在台川喜宴餐厅吃饭,",
- "偶尔去地中海影城看电影。",
- };
- Segment segment = HanLP.newSegment().enableOrganizationRecognize(true);
- for (String sentence : testCase)
- {
- List<Term> termList = segment.seg(sentence);
- System.out.println(termList);
- }
14. 关键词提取
- String content = "程序员(英文Programmer)是从事程序开发、维护的专业人员。一般将程序员分为程序设计人员和程序编码人员,但两者的界限并不非常清楚,特别是在中国。软件从业人员分为初级程序员、高级程序员、系统分析员和项目经理四大类。";
- List<String> keywordList = HanLP.extractKeyword(content, 5);
- System.out.println(keywordList);
15. 自动摘要
- String document = "算法可大致分为基本算法、数据结构的算法、数论算法、计算几何的算法、图的算法、动态规划以及数值分析、加密算法、排序算法、检索算法、随机化算法、并行算法、厄米变形模型、随机森林算法。\n" +
- "算法可以宽泛的分为三类,\n" +
- "一,有限的确定性算法,这类算法在有限的一段时间内终止。他们可能要花很长时间来执行指定的任务,但仍将在一定的时间内终止。这类算法得出的结果常取决于输入值。\n" +
- "二,有限的非确定算法,这类算法在有限的时间内终止。然而,对于一个(或一些)给定的数值,算法的结果并不是唯一的或确定的。\n" +
- "三,无限的算法,是那些由于没有定义终止定义条件,或定义的条件无法由输入的数据满足而不终止运行的算法。通常,无限算法的产生是由于未能确定的定义终止条件。";
- List<String> sentenceList = HanLP.extractSummary(document, 3);
- System.out.println(sentenceList);
16. 短语提取
- String text = "算法工程师\n" +
- "算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。" +
- "如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、" +
- "空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。算法工程师就是利用算法处理事物的人。\n" +
- "\n" +
- "1职位简介\n" +
- "算法工程师是一个非常高端的职位;\n" +
- "专业要求:计算机、电子、通信、数学等相关专业;\n" +
- "学历要求:本科及其以上的学历,大多数是硕士学历及其以上;\n" +
- "语言要求:英语要求是熟练,基本上能阅读国外专业书刊;\n" +
- "必须掌握计算机相关知识,熟练使用仿真工具MATLAB等,必须会一门编程语言。\n" +
- "\n" +
- "2研究方向\n" +
- "视频算法工程师、图像处理算法工程师、音频算法工程师 通信基带算法工程师\n" +
- "\n" +
- "3目前国内外状况\n" +
- "目前国内从事算法研究的工程师不少,但是高级算法工程师却很少,是一个非常紧缺的专业工程师。" +
- "算法工程师根据研究领域来分主要有音频/视频算法处理、图像技术方面的二维信息算法处理和通信物理层、" +
- "雷达信号处理、生物医学信号处理等领域的一维信息算法处理。\n" +
- "在计算机音视频和图形图像技术等二维信息算法处理方面目前比较先进的视频处理算法:机器视觉成为此类算法研究的核心;" +
- "另外还有2D转3D算法(2D-to-3D conversion),去隔行算法(de-interlacing),运动估计运动补偿算法" +
- "(Motion estimation/Motion Compensation),去噪算法(Noise Reduction),缩放算法(scaling)," +
- "锐化处理算法(Sharpness),超分辨率算法(Super Resolution),手势识别(gesture recognition),人脸识别(face recognition)。\n" +
- "在通信物理层等一维信息领域目前常用的算法:无线领域的RRM、RTT,传送领域的调制解调、信道均衡、信号检测、网络优化、信号分解等。\n" +
- "另外数据挖掘、互联网搜索算法也成为当今的热门方向。\n" +
- "算法工程师逐渐往人工智能方向发展。";
- List<String> phraseList = HanLP.extractPhrase(text, 5);
- System.out.println(phraseList);
17. 拼音转换
- /**
- * 汉字转拼音
- * @author hankcs
- */
- public class DemoPinyin
- {
- public static void main(String[] args)
- {
- String text = "重载不是重任";
- List<Pinyin> pinyinList = HanLP.convertToPinyinList(text);
- System.out.print("原文,");
- for (char c : text.toCharArray())
- {
- System.out.printf("%c,", c);
- }
- System.out.println();
- System.out.print("拼音(数字音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin);
- }
- System.out.println();
- System.out.print("拼音(符号音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getPinyinWithToneMark());
- }
- System.out.println();
- System.out.print("拼音(无音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getPinyinWithoutTone());
- }
- System.out.println();
- System.out.print("声调,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getTone());
- }
- System.out.println();
- System.out.print("声母,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getShengmu());
- }
- System.out.println();
- System.out.print("韵母,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getYunmu());
- }
- System.out.println();
- System.out.print("输入法头,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getHead());
- }
- System.out.println();
- }
- }
18. 简繁转换
- /**
- * 简繁转换
- * @author hankcs
- */
- public class DemoTraditionalChinese2SimplifiedChinese
- {
- public static void main(String[] args)
- {
- System.out.println(HanLP.convertToTraditionalChinese("用笔记本电脑写程序"));
- System.out.println(HanLP.convertToSimplifiedChinese("「以後等妳當上皇后,就能買士多啤梨慶祝了」"));
- }
- }
19. 文本推荐
- /**
- * 文本推荐(句子级别,从一系列句子中挑出与输入句子最相似的那一个)
- * @author hankcs
- */
- public class DemoSuggester
- {
- public static void main(String[] args)
- {
- Suggester suggester = new Suggester();
- String[] titleArray =
- (
- "威廉王子发表演说 呼吁保护野生动物\n" +
- "《时代》年度人物最终入围名单出炉 普京马云入选\n" +
- "“黑格比”横扫菲:菲吸取“海燕”经验及早疏散\n" +
- "日本保密法将正式生效 日媒指其损害国民知情权\n" +
- "英报告说空气污染带来“公共健康危机”"
- ).split("\\n");
- for (String title : titleArray)
- {
- suggester.addSentence(title);
- }
- System.out.println(suggester.suggest("发言", 1)); // 语义
- System.out.println(suggester.suggest("危机公共", 1)); // 字符
- System.out.println(suggester.suggest("mayun", 1)); // 拼音
- }
- }
20. 语义距离
- /**
- * 语义距离
- * @author hankcs
- */
- public class DemoWordDistance
- {
- public static void main(String[] args)
- {
- String[] wordArray = new String[]
- {
- "香蕉",
- "苹果",
- "白菜",
- "水果",
- "蔬菜",
- "自行车",
- "公交车",
- "飞机",
- "买",
- "卖",
- "购入",
- "新年",
- "春节",
- "丢失",
- "补办",
- "办理",
- "送给",
- "寻找",
- "孩子",
- "教室",
- "教师",
- "会计",
- };
- for (String a : wordArray)
- {
- for (String b : wordArray)
- {
- System.out.println(a + "\t" + b + "\t之间的距离是\t" + CoreSynonymDictionary.distance(a, b));
- }
- }
- }
- }
21. 依存句法解析
- /**
- * 依存句法解析
- * @author hankcs
- */
- public class DemoDependencyParser
- {
- public static void main(String[] args)
- {
- System.out.println(HanLP.parseDependency("把市场经济奉行的等价交换原则引入党的生活和国家机关政务活动中"));
- }
- }
-
说明
-
内部采用MaxEntDependencyParser
实现,用户可以直接调用MaxEntDependencyParser.compute(sentence)
-
也可以调用基于随机条件场的依存句法分析器CRFDependencyParser.compute(sentence)
-
在封闭测试集上准确率有90%以上,但在开放测试集上则不理想。
-
算法详解
词典说明
本章详细介绍HanLP中的词典格式,满足用户自定义的需要。HanLP中有许多词典,它们的格式都是相似的,形式都是文本文档,随时可以修改。
基本格式
词典分为词频词性词典和词频词典。
少数词典有自己的专用格式,比如同义词词典兼容《同义词词林扩展版》的文本格式,而转移矩阵词典则是一个csv表格。
下文主要介绍通用词典,如不注明,词典特指通用词典。
数据结构
Trie树(字典树)是HanLP中使用最多的数据结构,为此,我实现了通用的Trie树,支持泛型、遍历、储存、载入。
用户自定义词典采用AhoCorasickDoubleArrayTrie和二分Trie树储存,其他词典采用基于双数组Trie树(DoubleArrayTrie)实现的AC自动机AhoCorasickDoubleArrayTrie。
储存形式
词典有两个形态:文本文件(filename.txt)和缓存文件(filename.txt.bin或filename.txt.trie.dat和filename.txt.trie.value)。
修改方法
HanLP的核心词典训练自人民日报2014语料,语料不是完美的,总会存在一些错误。这些错误可能会导致分词出现奇怪的结果,这时请打开调试模式排查问题:
- HanLP.Config.enableDebug();
-
核心词性词频词典
-
核心二元文法词典
-
命名实体识别词典
如果问题解决了,欢迎向我提交一个pull request,这是我在代码库中保留明文词典的原因,众人拾柴火焰高!