NLP词典切分算法


词的定义

  • 在语言学上,词语的定义是具备独立意义的最小单位
  • 在基于词典的中文分词中,词典中的字符串就是词

词的性质
在这里插入图片描述

一、词典的加载

加载HanLP附带的迷你核心词典

from pyhanlp import *

def load_dictionary():
    """
    加载HanLP中的mini词库
    :return: 一个set形式的词库
    """
    #JClass 函数是连通Java和Python的桥梁,用来根据Java路径名得到--个Python类
    #利用Jclass取得了HanLP中的IOUtil工具类
    IOUtil = JClass('com.hankcs.hanlp.corpus.io.IOUtil')
    #取得HanLP的配置项config中的词典路径,我们写在配置文件中的条目最终会被读入这个结构中
    #比如配置文件写作CoreDictionaryPath=data/dictionary/CorelNatureDictionary.txt,该配置将被读人HanLP.Config.CoreDictionaryPath。
    #这里我们想要加载mini词典,因为其体积更小,加载起来更快
    #将这个路径替换为mini词典的路径
    path = HanLP.Config.CoreDictionaryPath.replace('.txt', '.mini.txt')
    #像对待普通Python 工具类一样调用了工outil的静态方法loadDictionary
    #该方法支持将多个文件读人同一个词典中,因此需要传入一个list。
    #它返回一个Java Map对象
    dic = IOUtil.loadDictionary([path])
    #只取它的键keyset,并将其转换为一个 Python 原生的set对象
    return set(dic.keySet())

if __name__ == '__main__':
    dic = load_dictionary()
    print(len(dic))#词典大小
    print(list(dic)[0])#取词典第一个词
85584
悲痛

二、切分算法

2.1 完全切分

  • 完全切分指的是找出一段文本中所有的单词,并不是标准意义上的分词
def fully_segment(text, dic):
    word_list = []
    for i in range(len(text)):                  # i 从 0 到text的最后一个字的下标遍历
        for j in range(i + 1, len(text) + 1):   # j 遍历[i + 1, len(text)]区间
            word = text[i:j]                    # 取出连续区间[i, j]对应的字符串
            if word in dic:                     # 如果在词典中,则认为是一个词
                word_list.append(word)
    return word_list

if __name__ == '__main__':
    dic = load_dictionary()
    print(fully_segment('商品和服务', dic))#由于词库中含有单字,所以结果中出现了一些单字
['商', '商品', '品', '和', '和服', '服', '服务', '务']

2.2 正向最长匹配

完全切分的输出并不是中文分词,我们更需要那种有意义的词语序列,而不是所有出现在词典中的单词所构成的链表。为了达到这个目的,需要完善一下我们的规则,考虑到越长的单词表达的意义越丰富,于是我们定义单词越长优先级越高

最长匹配算法:以某个下标为起点递增查词的过程中,优先输出更长的单词

正向最长匹配:在最长匹配算法的基础上从前往后匹配

def forward_segment(text, dic):
    word_list = []
    i = 0
    while i < len(text):
        longest_word = text[i]                      # 当前扫描位置的单字
        for j in range(i + 1, len(text) + 1):       # 所有可能的结尾
            word = text[i:j]                        # 从当前位置到结尾的连续字符串
            if word in dic:                         # 在词典中
                if len(word) > len(longest_word):   # 并且更长
                    longest_word = word             # 则更优先输出
        word_list.append(longest_word)              # 输出最长词
        i += len(longest_word)                      # 正向扫描
    return word_list

if __name__ == '__main__':
    dic = load_dictionary()
    print(forward_segment('就读北京大学', dic))
    print(forward_segment('研究生命起源', dic))
['就读', '北京大学']
['研究生', '命', '起源']

2.3 逆向最长匹配

正向最长匹配:在最长匹配算法的基础上从前往后匹配

def backward_segment(text, dic):
    word_list = []
    i = len(text) - 1
    while i >= 0:                                   # 扫描位置作为终点
        longest_word = text[i]                      # 扫描位置的单字
        for j in range(0, i):                       # 遍历[0, i]区间作为待查询词语的起点
            word = text[j: i + 1]                   # 取出[j, i]区间作为待查询单词
            if word in dic:
                if len(word) > len(longest_word):   # 越长优先级越高
                    longest_word = word
                    break
        word_list.insert(0, longest_word)           # 逆向扫描,所以越先查出的单词在位置上越靠后
        i -= len(longest_word)
    return word_list

if __name__ == '__main__':
    dic = load_dictionary()
    print(forward_segment('就读北京大学', dic))
    print(forward_segment('研究生命起源', dic))
['就读', '北京大学']
['研究生', '命', '起源']

2.4 双向最长匹配

正向/逆向最长匹配歧义对比

在这里插入图片描述

由上图可以看出正向和逆向匹配都存在无法消除歧义的情况

启发式算法:在搜索最优解的过程中利用到原来搜索过程中得到的信息,且这个信息会改进我们的搜索过程。

双向最长匹配
一种融合两种匹配方法的复杂规则集,流程如下。

  1. 同时执行正向和逆向最长匹配,若两者的词数不同,则返回词数更少的那一个。
  2. 否则,返回两者中单字更少的那一个。当单字数也相同时,优先返回逆向最长匹配的结果。
def count_single_char(word_list: list):  # 统计单字成词的个数
    return sum(1 for word in word_list if len(word) == 1)

def bidirectional_segment(text, dic):
    f = forward_segment(text, dic)
    b = backward_segment(text, dic)
    if len(f) < len(b):                                  # 词数更少优先级更高
        return f
    elif len(f) > len(b):
        return b
    else:
        if count_single_char(f) < count_single_char(b):  # 单字更少优先级更高
            return f
        else:
            return b                                     # 都相等时逆向匹配优先级更高

if __name__ == '__main__':
    dic = load_dictionary()
    print(bidirectional_segment('研究生命起源', dic))
['研究', '生命', '起源']

3. 速度测评

  • Python的运行速度比Java慢
  • 正向匹配和逆向匹配的速度差不多,是双向匹配的两倍
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXTAsUhp-1647349614770)(attachment:image.png)]
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡拉比丘流形

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值