赵走x博客
网站访问量:151464
首页
书籍
软件
工具
古诗词
搜索
登录
16、算法部分源码解析
15、系统总体流程与词典结构
14、中文分词
13、词汇与分词技术
12、三个平面中的语义研究
11、汉语的发展
10、字形的流变
9、六书及其他
8、文字符号的起源
7、整合语义角色标注模块
6、整合句法解析模块
5、整合命名实体识别模块
4、整合词性标注模块
3、整合中文分词模块
2、现代自然语言系统简介
1、中文语言的机器处理:历史回顾
16、算法部分源码解析
资源编号:76152
NLP汉语自然语言处理原理与实践
自然语言处理
热度:106
本节介绍了HanLP的源文件结构及部分核心源代码解析。由于整个项目包含中文分词、未登录词识别、词性标注三大部分,代码量较大,细节也很多,因此,下文将把重点放在中文分词和未登录词识别两部分的源码解析内容。
本节介绍了HanLP的源文件结构及部分核心源代码解析。由于整个项目包含中文分词、未登录词识别、词性标注三大部分,代码量较大,细节也很多,因此,下文将把重点放在中文分词和未登录词识别两部分的源码解析内容。 # 1、系统配置 为了尽快使读者上手,我们简化一些构建项目的方法。有关这些资料,读者阅读HanLP的帮助文档即可。下面介绍系统配置。首先要修改系统的配置文件:hanlp.properties。然后将该文件复制到src目录下,并在执行之前根据自己的环境对该文件进行配置。该文件默认使用ANSI编码写成,如果运行在Linux上,建议转换为UTF-8编码。文件内容如下。 #本配置文件中的路径的根目录,根目录+其他路径=绝对路径 #Windows用户请注意,路径分隔符统一使用/ root=D:/JavaProjects/HanLP/ #核心词典路径 CoreDictionaryPath=data/dictionary/CoreNatureDictionary.txt #二元语法词典路径 BiGramDictionaryPath=data/dictionary/CoreNatureDictionary.ngram.txt #停用词词典路径 CoreStopWordDictionaryPath=data/dictionary/stopwords.txt #同义词词典路径 CoreSynonymDictionaryDictionaryPath=data/dictionary/synonym/CoreSynonym.txt #人名词典路径 PersonDictionaryPath=data/dictionary/person/nr.txt #人名词典转移矩阵路径 PersonDictionaryTrPath=data/dictionary/person/nr.tr.txt #繁简词典路径 TraditionalChineseDictionaryPath=data/dictionary/tc/TraditionalChinese.txt #自定义词典路径,用;隔开多个自定义词典,空格开头表示在同一个目录,使用“文件名 词性”形式则表 示这个词典的词性默认是该词性。优先级递减。 #另外data/dictionary/custom/CustomDictionary.txt是一个高质量的词库,请不要删除 CustomDictionaryPath=data/dictionary/custom/CustomDictionary.txt; 现代汉语补充词 库.txt; 全国地名大全.txt ns; 人名词典.txt; 机构名词典.txt; 上海地名.txt ns; data/ dictionary/person/nrf.txt nrf #CRF分词模型路径 CRFSegmentModelPath=data/model/segment/CRFSegmentModel.txt #HMM分词模型 HMMSegmentModelPath=data/model/segment/HMMSegmentModel.bin #分词结果是否展示词性 ShowTermNature=true 如果系统运行仅需要修改一项,则将“root=”后面的内容修改为项目根目录。例如,改为本书的项目根目录为“root=/home/your_username/workspace /Hanlp-1.28/”。 3.3.2 Main方法与例句 HanLP源码并未提供测试的入口方法,但该框架提供的文档比较全,读者可从http://hanlp.linrunsoft.com/doc.html页面查看。为此我们新建了“com.test.Hanlp; ”包,并创建一个测试类:ICTCLASSeg。暂且以此为例进行简单的测试。主方法的源代码如下。 package com.test.Hanlp; import com.hankcs.hanlp.seg.Segment; import com.hankcs.hanlp.seg.NShort.NShortSegment; import com.hankcs.hanlp.seg.Viterbi.ViterbiSegment; public class ICTCLASSeg { public static void main(String[] args) { // 实例化NShort分词器,并启用地名、组织名词识别模块 Segment nShortSegment = new NShortSegment().enableCustomDictionary (false).enablePlaceRecognize(true).enableOrganizationRecognize(true); // 输入例句 String[] testCase = new String[]{ "张强在铁岭对王小红说的确实在理。", }; // 输出最短路径分词结果 System.out.println("N-最短分词:" + nShortSegment.seg(testCase[0]) ); } } 参考教程的NShortSegment代码,简单实现了一个入口方法,可以看到执行结果如下。 N-最短分词:[乔布斯/nrf, 说/v, iphone/nx, 是/vshi, 最好/d, 用/p, 的/ude1, 手机/n, 。/w] 这里enableCustomDictionary()、enablePlaceRecognize()、enableOrganizationRecognize()为命名实体识别的启用或关闭设置。这部分的详细代码在NShortSegment类中,比较简单,在此不做讲解。 3.3.3 句子切分 句子切分是中文分词的预处理阶段,这个节点的主要功能是对输入字符串按照分隔符(全角分隔符、半角分隔符)来分隔句子。这个阶段使用的类是com.hankcs.hanlp.seg. Segment,功能函数为seg()函数。 这些分隔符位于SentenceUtility类中,如下为参考源代码。 public static List
toSentenceList(char[] chars) { StringBuilder sb = new StringBuilder(); List
sentences = new LinkedList
(); for (int i = 0; i < chars.length; ++i) { if (sb.length() == 0 && (Character.isWhitespace(chars[i]) || chars[i] == ' ')) { continue; } sb.append(chars[i]); switch (chars[i]) { case '.': if (i < chars.length - 1 && chars[i + 1] > 128) { insertIntoList(sb, sentences); sb = new StringBuilder(); } break; case '…': { if (i < chars.length - 1 && chars[i + 1] == '…') { sb.append('…'); ++i; insertIntoList(sb, sentences); sb = new StringBuilder(); } }break; case ' ': case ' ': case '? : case '。': case ', ': case ', ': insertIntoList(sb, sentences); sb = new StringBuilder(); break; case '; ': case '; ': insertIntoList(sb, sentences); sb = new StringBuilder(); break; case '! ': case '! ': insertIntoList(sb, sentences); sb = new StringBuilder(); break; case '? ': case '? ': insertIntoList(sb, sentences); sb = new StringBuilder(); break; case '\n': case '\r': insertIntoList(sb, sentences); sb = new StringBuilder(); break; } } if (sb.length() > 0) { insertIntoList(sb, sentences); } return sentences; } 这里所谓的句子,就是被一些标句符号分隔的字符串,这些标句符号包括:'…'、'。'、', '、', '、'; '、'; '、'! '、'! '、'? '、'? '、'\n'、'\r’等(上述标点符号通过顿号来分隔)。句子切分的执行函数为toSentenceList,该函数的逻辑很简单,就是将输入的一个字符串(可以理解为一个大的字符串)按照上述标句符号切分为字符串数组(这里是一个链表结构)。 如果输入的文本比较大,Seg函数还支持多线程的调用来处理大型文本,源代码如下。 public List
seg(String text) { char[] charArray = text.toCharArray(); //原子切分 if (HanLP.Config.Normalization) { CharTable.normalization(charArray); } if (config.threadNumber > 1 && charArray.length > 10000) { // 小文本多线程 没意义,反而变慢了 List
sentenceList = SentencesUtil.toSentenceList(charArray); //将篇章切分为句子链表 String[] sentenceArray = new String[sentenceList.size()]; sentenceList.toArray(sentenceArray); List
[] termListArray = new List[sentenceArray.length]; final int per = sentenceArray.length / config.threadNumber; WorkThread[] threadArray = new WorkThread[config.threadNumber]; //创 建工作线程组 for (int i = 0; i < config.threadNumber - 1; ++i) { int from = i * per; threadArray[i] = new WorkThread(sentenceArray, termListArray, from, from + per); //创建线程 threadArray[i].start(); //启动线程->run方法 } threadArray[config.threadNumber - 1] = new WorkThread(sentenceArray, termListArray, (config.threadNumber - 1) * per, sentenceArray.length); threadArray[config.threadNumber - 1].start(); //启动线程->run方法 try { for (WorkThread thread : threadArray) { thread.join(); //合并执行完的线程 } } catch (InterruptedException e) { logger.severe("线程同步异常:" + TextUtility.exceptionToString(e)); return Collections.emptyList(); } List
termList = new LinkedList
(); if (config.offset || config.indexMode) { int sentenceOffset = 0; // 由于分割了句子,所以需要重新校正offset for (int i = 0; i < sentenceArray.length; ++i) { for (Term term : termListArray[i]) { term.offset += sentenceOffset; termList.add(term); } sentenceOffset += sentenceArray[i].length(); } } else { for (List
list : termListArray) { termList.addAll(list); } } return termList; } 如下函数(run)就是start调用的多线程分词方法。 @Override public void run() { for (int i = from; i < to; ++i) { termListArray[i] = segSentence(sentenceArray[i].toCharArray()); } } } 分词后的结果都被保存在termListArray中。 3.3.4 分词流程 对输入的文本预处理完成(句子切分阶段结束)之后,即进入中文分词流程。NShortSegment类完成了NShort中文分词算法的全过程。这个类继承自WordBasedGenerativeModelSegment类,而WordBasedGenerativeModelSegment类又继承自Segment类。 HanLP不仅提供了NShort分词算法,还提供了基于HMM、CRF、Dijkstra等的多种切分和标注方法。所有这些分词方法都继承自Segment类,而NShort和HMM在概率图模型中均属于产生式模型。因此,这两个类继承自WordBasedGenerativeModelSegment类。通过如此复杂的继承关系,系统将若干个分词算法形成一个整体。 在CResult::Processing函数中,这里主要分析中文分词的完整流程。其流程如图3.3所示。 图3.3 中文分词的流程 如图3.3所示,分词的过程包含4个大的步骤,分别为:一元词网、二元词图、输出最短路径、应用后处理规则。 (1)一元词网。根据输入的字符串,查询核心词典,将原子分词的结果与词典中的词进行最大匹配。匹配的结果包括词字符串、词性、词频等信息形成一个二维数组,并将此数组存入WordNet二维数组中。 (2)二元词图。用一元分词的结果(二维数组)查询二元词典,与二元词典进行最大匹配。匹配的结果为一个二维数组,并将此结果存入新的Graph中,形成一个词图。 (3)输出最短路径。将查出的每个结果按平滑算法计算一元分词和二元分词的词频数得到词网中每个节点的权值(概率的倒数),应用NShort算法累加词网中每个节点构成的所有路径,权值最小(概率最大)的那条路径对应的词网节点就是初分的结果。 (4)应用后处理规则。对粗分结果应用规则,识别特殊组合的名词性结构。 NShortSegment类的segSentence()是控制分词流程的主函数。如下是经过注释的粗分词阶段的函数代码片段。 @Override public List
segSentence(char[] sentence) { WordNet wordNetOptimum = new WordNet(sentence); //最优词网 WordNet wordNetAll = new WordNet(sentence); //一元词网 // 粗分主函数 List
> coarseResult = BiSegment(sentence, 2, wordNetOptimum, wordNetAll); boolean NERexists = false; for (List
vertexList : coarseResult) { if (HanLP.Config.DEBUG) { System.out.println("粗分结果" + convert(vertexList, false)); } ……