赞
踩
更多内容点击查看Python 实战 | 进阶中文分词之 HanLP 词典分词(上)
Python教学专栏,旨在为初学者提供系统、全面的Python编程学习体验。通过逐步讲解Python基础语言和编程逻辑,结合实操案例,让小白也能轻松搞懂Python!
本文目录
一、引言
二、加载 HanLP 词典
三、切分规则
四、实现 HanLP 词典分词
五、结束语
本文共9395个字,阅读大约需要24分钟,欢迎指正!
自然语言处理任务的层次可以分为词法分析、句法分析和语义分析,同时这也是从易到难的递进过程。对中文来说,词法分析(中文分词、词性标注、命名实体识别)是后续任务的基础,而中文分词又是其中最基本的任务。目前中文分词算法大致可以分为基于词典规则与基于机器学习两大派别,无论是哪个派别的算法总有各自的优缺点,我们在工作学习中应该选择最适合当前任务的算法。
本期将为大家介绍如何基于 HanLP 进行词典分词,词典分词是一种基于词典库的分词方法,它的原理是将待处理的文本与词典中的词语进行匹配,找出最长的匹配词并切分。另外,为什么我们先介绍 HanLP 词典分词呢?除了其原理相对容易理解,介绍该方法还有以下两点原因:
HanLP 支持自定义词典(同时也支持设置默认的词典词性),用户可以针对各自领域的内容自定义词典,以此提高分词的精度。
HanLP 开发者何晗(hancks)使用双数组字典树结构来存储字典,具有更小的空间复杂度和更高的匹配效率,在保证精度的同时,提高了分词的效率。
根据词典分词的定义,使用词典来分词其实仅需要一部词典和一套查找词典的规则即可,于是下面我们会先介绍在 HanLP 中如何加载词典,然后介绍三种常用的切分规则以及它们的效果,最后实现 HanLP 的词典分词。
文本基于 HanLP 1.8.4 版本书写。
本文中所有 Python 代码均在集成开发环境 Visual Studio Code (VScode) 中使用交互式开发环境 Jupyter Notebook 中编写。
实现词典分词的第一步,就是准备一部词典。网上已经有许多公开的中文词库了,比如 THUOCL(清华大学开放中文词库)、中文维基百科抽取的词库和何晗发布的千万级巨型汉语词库等等,有需要的读者可以自行下载,供个人研究学习使用。
上期文章我们已经介绍了安装 HanLP 的方法,如果未安装的读者请参考Python 实战 | 文本分析工具之HanLP入门。在我们第一次运行时,HanLP 自带的数据包和字典就会自动下载到pyhanlp
的系统路径中,笔者的词典路径如下:
C:\Users\QIYAN_USER\miniconda3\Lib\site-packages\pyhanlp\static\data\dictionary
这里以 HanLP 自带的核心词典(上图 CoreNatureDictionary.txt)为例,这是一个“utf-8”
编码的纯文本文件,使用记事本打开格式如下:
可以看到,HanLP 词典是以空格作为分隔的表格形式,这三列分别为词语、词性和相应的词频(在某个语料库的统计结果),比如 “叙述” 这个词以动词的形式出现了 72 次、以动名词的形式出现了 18 次。
现在我们来看如何在 HanLP 中加载这份词典。代码如下:
需要注意一点,为了便于展示,本文将代码存放在
.ipynb
文件中。如果读者后续有调用该函数的需求,建议将代码存于扩展名为.py
的脚本文件(也称“模块”)中,然后在需要使用的模块中导入该模块,即可随时调用该模块中的函数。
- from pyhanlp import *
-
- # 参数 path:需要加载的词典路径
- def load_dictionary(path):
- IOUtil = JClass('com.hankcs.hanlp.corpus.io.IOUtil') # 1
- dic = IOUtil.loadDictionary([path]) # 2
- return set(dic.keySet()) # 返回 set 形式的词典
-
- my_dict = load_dictionary(HanLP.Config.CoreDictionaryPath) # 传入核心词典路径
- print(len(my_dict)) # 词典的词条数
-
- # 运行结果
- '''
- 153091
- '''
上述代码中,注释为 1 的这行代码中 JClass 函数是用来根据 Java 路径名得到一个 Python 类的桥梁,该行代码的作用是获取 HanLP 中的工具类 IOUtil(其主要功能是进行输入输出操作,如读写文件等);注释为 2 的这行代码是调用了 IOUtil 的方法loadDictionary
,该方法支持将多个文件读入同一个词典中,所以需要传入一个 list,返回值 dic 是一个 TreeMap,它的键是词语本身,值是一个包含词性和词频的结构(暂时不用管)。在中文分词中,我们更关心词语本身,所以函数只需返回 TreeMap 的键(通过dic.keySet()
)即可。
然后我们将 HanLP 的配置项 Config 中的词典路径作为参数传入,得到了词典my_dic
,并且输出了词典的词条数量。至此,我们已经成功加载 HanLP 的核心词典,后续就可以基于 Python 代码使用该词典了。
如果现在需要使用 HanLP 的核心迷你词典呢?只要将调用函数的代码改为:
my_dict = load_dictionary(HanLP.Config.CoreDictionaryPath.replace('.txt', '.mini.txt'))
准备好词典之后,下一步就需要确定查找词典的规则。常用的三种规则为正向最长匹配、逆向最长匹配和双向最长匹配,在具体了解这三种切分规则之前,首先需要对完全切分有一个认识,因为这是三种切分规则的基础。
完全切分指的是找出一段文本中所有的单词,请注意,这不是标准意义上的分词,完全切分做的就是遍历文本中的连续序列,并查询这个序列是否在词典中。根据这个思想,现在我们切分“完全切分过程是切分规则的基础”
这句话,代码如下:
- def full_seg(text, dic):
- seg_list = []
- for i in range(len(text)): # i 从 0 遍历至 text 最后一个字的下标
- for j in range(i+1, len(text)+1): # j 从 i 的后一个位置开始遍历
- word = text[i:j] # 取出区间 [i, j) 对应的字符串
- if word in dic: # 如果该字符串在词典中,则认为是一个词语
- seg_list.append(word)
- return seg_list
- # 此处使用的词典,为上一步加载的 HanLP 核心词典
- full_seg('完全切分过程是切分规则的基础', my_dict)
结果如下:
可以看到,完全切分将所有可能的词按遍历的顺序全部输出了,包括一些词语和单字。这一定不是我们想要的分词结果,考虑到越长得到单词表达的含义越丰富,于是定义单词越长优先级越高。这时三种切分规则就出现了,在以某个下标为起点递增查词的过程中,优先输出更长的词语,这种规则就是最长匹配算法,其中包括正向、逆向和双向最长匹配。
顾名思义,正向匹配算法就是下标的扫描顺序在递增查词的过程中是从左到右的。简单来说,它会从左到右开始扫描待分词的文本,每次都尝试匹配尽可能长的词语,如果找到匹配的词语,就将其作为一个词切分出来,然后从下一个字符开始继续匹配。来看一下效果,现在我们切分“这项研究在中国人民大学进行”
这句话,代码如下:
- def fore_seg(text, dic):
- seg_list = []
- i = 0
- while i < len(text):
- longest_word = text[i] # 当前扫描位置对应的字符
- for j in range(i+1, len(text)+1): # j 为结束为止,从 i 的下一位开始,遍历得到所有可能的词 [i,j)
- word = text[i:j] # 得到区间 [i,j) 对应的字符串
- if word in dic:
- if len(word) > len(longest_word): # 该词在词典中 & 比longest_word更长,优先输出
- longest_word = word
- seg_list.append(longest_word) # 输出每一个起始位置 i 匹配的最长词
- i += len(longest_word) # 从下一个字符开始匹配
- return seg_list
-
- fore_seg('这项研究在中国人民大学进行', my_dict)
-
- # 输出结果
- '''
- ['这项', '研究', '在', '中国人', '民', '大学', '进行']
- '''
可以看到分词结果将“中国人”作为一个词语切分出来了,实际上,对于这句话,我们理想的切分结果是有“中国”和“人民”两个词语。这是因为正向最长匹配认为“中国人”的优先级比“中国”更高。那么如果这句话用逆向最长匹配的规则来切分,会得到什么结果呢?
逆向最长匹配与正向最长匹配的区别在于,它会从右到左开始扫描待分词的文本。此时只需对代码做出相应的更改即可,如下:
- def back_seg(text, dic):
- seg_list = []
- j = len(text) - 1
- while j >= 0: # 逆向扫描, 当前扫描位置为终点
- longest_word = text[j] # 当前扫描位置对应的字符
- for i in range(0, j): # i 为起始位置,从 0 开始遍历至 j 的前一个位置
- word = text[i: j+1] # 得到区间 [i, j] 对应的字符串
- if word in dic:
- if len(word) > len(longest_word): # 该词在词典中 & 比longest_word更长,优先级更高
- longest_word = word
- seg_list.insert(0, longest_word) # 由于逆向扫描,查出的单词位置靠后
- j -= len(longest_word)
- return seg_list
-
- back_seg('这项研究在中国人民大学进行', my_dict)
-
- # 输出结果
- '''
- ['这项', '研究', '在', '中国', '人民', '大学', '进行']
- '''
可以看到,这句话的分词结果就是我们想要的,这可以说明逆向最长匹配的效果比正向最长匹配的效果更好吗?那么再看另一句话的分词结果,代码如下:
- print(fore_seg('项目的研究目的值得人们的关注', my_dict))
- print(back_seg('项目的研究目的值得人们的关注', my_dict))
-
- # 输出结果
- '''
- ['项目', '的', '研究', '目的', '值得', '人们', '的', '关注']
- ['项', '目的', '研究', '目的', '值得', '人们', '的', '关注']
- '''
针对上例中的这句话,正向最长匹配的结果是优于逆向最长匹配的,可见这两个切分规则是各有千秋。
于是有人就提出了综合这两个规则的一个切分规则——双向最长匹配,它的规则更加复杂一些:
同时进行正向、逆向最长匹配,如果两个分词结果词数不同,则返回词数更少的结果
如果两个分词结果词数相同,则返回两者中单字更少的结果
如果单字的数量也相同,优先返回逆向最长匹配的结果
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。