65自然语言处理底层技术实现及应用--基于字典的中文分词方法

基于字典的中文分词方法

中文分词介绍

中文分词就是将一个汉语句子中的词切分出来。为机器翻译、文本挖掘、情感分析等任务打好基础。你可能会好奇,为什么一定要先进行分词呢?
其实答案很简单,这就像 26 个字母一样,单个字母并不能表达某个意思,将其组合起来成为一个英文单词才有意义。
中文虽然有时候单个字也能表达具体的意思,但是往往要组成一个词才能表达一个完整的意思。例如这个字:学,如果只给你这个字,你的大脑可能啥也浮现不出现。但是当给你一个词,例如:学生、同学。不出意外的话,此时的你,大脑应该会产生对这两个词的联想。
目前,自然语言处理的计算工具主要是计算机。而计算机在识别文本信息时,也跟人一样。无法去理解字这种级别的特征,因此才需要进行分词。
分词工具
目前,常用的中文分词工具主要有:Stanford NLP、HanLP、结巴分词器 jieba 等。而在Python中常用的为 jieba 。让我们使用 jieba 来先做一个简单的例子。

在 jieba 中,分词主要使用 jieba.cut 方法。其接受三个输入参数,分别是:
字符串参数: 需要分词的字符串
cut_all 参数: 用来控制是否采用全模式
HMM 参数: 用来控制是否使用 HMM 模型

import jieba
sent = '自然语言处理是计算机科学领域与人工智能领域中的一个重要方向'
result = jieba.cut(sent)
'|'.join(result)

jieba.cut 方法中,参数 cut_all 默认的是 False,如果想要进行全模式切分,将其设置为 True。如下面实验:

result = jieba.cut(sent, cut_all=True)
'|'.join(result)

从上面的实验可以看到,通过使用 jieba.cut 就可以对汉语句子进行分词。你是否跟我一样对 jieba 的工作原理充满着好奇,为什么它可以把句子的每个词准确的分开。不用着急,理解 jieba 的分词原理,一两句话是说不清楚的。接下来我们来着重讲解中文分词的原理。
当然你也可能对其工作原理不感兴趣,认为只需要会调用工具即可。但是,自然语言处理往往是一个很复杂的工作。如果你只会调用这些工具包来处理任务,那么可能你对自然语言处理的认识还停留在初级阶段。

中文分词方法

中文分词主要有三种方法,分别是基于字典的切分方法、基于规则的切分方法和基于统计的切分方法。具体方法如下图所示:

image.png

基于字典的方法通常也被称为机械切分方法,其工作原理是取句子中相邻的字组成一个词,然后去对比字典中存不存在这个词,如果存在则切分完成。如果不存在则重新组词,再去字典中对比。
基于规则的方法则是通过语法规则等来模拟人对句子的理解,从而对句子进行切分。这种切分方法目前使用得较少,主要是其针对不同领域的文本需要制定不同的规则。通用性极差,所以我们不讲解这种方法。
基于统计的方法是目前许多分词器中使用得最多的方法。一般情况下,相邻的字同时出现的次数越多,就越有可能构成一个词。而基于统计的方法就是利用这一思想先来对大量的语料进行统计,然后再来计算几个字构成一个词的概率。
由于篇幅的原因这里无法对每一种具体的方法进行讲解。而且在基于统计的切分方法中,一些重要的算法,像 隐马尔可夫模型、条件随机场、循环神经网络。我们会在后续的实验中提到。
因此,接下来详细介绍最简单同时也是很有效的方法:基于字典的切分方法。

基于字典的切分方法

基于字典的切分方法虽然古老,而且简单。但却是目前最常用的切分方法,也是许多分词器中采用的方法之一。
正向最大匹配方法
正向最大匹配算法的原理是:从一个句子的正向开始切分,将句子切成两份,取前一份去和字典中的词进行匹配。具体的算法运行过程如下图描述:

image.png

上面的描述可能会让你感觉到有点懵。举一个例子来说明,假设我们有一个这样一个句子:【我想吃苹果】。

词典为:【我,苹果,想,吃,今天,发放,可以】。
第一步:求出字典中词的最大长度 word_length。在这个例子中,最大长度的词只有两个字,因此,word_length = 2。
第二步:对句子进行切分,切分结果为:【我想|吃苹果】。
第三步:拿【我想】去和字典中的词进行匹配。
第四步:字典中没有【我想】这个词。因此将斜杠往左移动一位。此时切分结果为:【我|想吃苹果】。
第五步:拿【我】去和字典中的词进行匹配。字典中存在【我】这个词。因此得到一个切分结果【我】。
第六步:把句子中的【我】字去掉,此时的句子为:【想吃苹果】。
第七步:重复上述的过程,最后会得到的切分结果为:【我,想,吃,苹果】。

逆向最大匹配法
如果你理解了上述的正向最大匹配法,那么逆向最大匹配法对于你来说就相对简单了。正向最大匹配法每次是取的是句子的前几个字来构成一个词,然后去和字典进行匹配。而逆向最大匹配法与正向最大匹配法正好相反,每次都是从句子的末尾开始取。
你可能会有疑问,为什么要从后边开始取,这样做与正向最大匹配法又有什么区别呢?
这主要是考虑到汉语的语言习惯问题,一般情况下,汉语的中心词往往在句子的后边。所以逆向最大匹配法的分词效果通常也比正向最大匹配法好一点点。
双向最大匹配法
可能你也想到了,双向最大匹配法就是将正向最大匹配法和逆向最大匹配法相结合起来,组成一个性能更优的分词器。
具体来说就是,使用正向最大匹配切分一次,然后使用逆向最大匹配法再切一次。然后判断两者切分结果是否一致,若不一致则按最大匹配原则来决定使用哪一种切分结果。具体过程如下图所示:

image.png

最大切分原则是:要保证切分结果的词最少,因为切分出的词越多,表达出的含义就越丰富,准确理解这句话的意思就相对越难。例如这句话:【自然语言处理是人工智能的一个重要分支】。

假设我们有两种切法:
【自然|语言|处理|是|人工|智能|的|一个|重要|分支】
【自然语言处理|是|人工智能|的|一个|重要|分支】

很显然,第二种切法,把【自然语言处理】和【人工智能】看做是一个词要更准确些,也更符合人对句子的理解。

使用正向最大匹配法构建分词器

前面主要讲述了基于字典的分词方法。为了更直观的理解,我们现在使用正向最大匹配法来构建一个分词器。正向最大匹配法的实现代码如下:

def cut_word(sentence, word_dic):
    """
    正向最大匹配分词器
    sentence:待切分的句子
    word_dic:字典
    """
    # 寻找字典中最大词的长度
    word_length_list = [len(word) for word in word_dic]
    max_length = max(word_length_list)
    # 求出句子的长度
    word_length = len(sentence)
    # 创建一个列表用来存放切分结果
    cut_word_list = []
    # 判断句子的长度是否为 0 ,若为 0 ,则句子为空
    while word_length > 0:
        max_cut_length = min(max_length, word_length)
        # 取前 max_cut_length 个字组成一个词
        subsentence = sentence[0:max_cut_length]
        while max_cut_length > 0:
            # 匹配字典
            if subsentence in word_dic:
                cut_word_list.append(subsentence)
                break
            elif max_cut_length == 1:
                cut_word_list.append(subsentence)
                break
            else:
                # 若字典没有词匹配,则剔除一个字,重新组成一个新的词
                max_cut_length = max_cut_length-1
                subsentence = subsentence[0:max_cut_length]
        # 剔除切分完成的词
        sentence = sentence[max_cut_length:]
        # 重新计算句子的长度
        word_length = word_length-max_cut_length
    return cut_word_list

构建好分词器之后,我们现在创建一个字典来测试。

word_dict = ['我们', '不错', '太阳', '西瓜', '大厦', '成都', '天气', '大学',
             '雪花', '周末', '我', '吃', '了', '香蕉', '三星', '大家', '看',
             '好玩', '熊猫', '现在', '代码', '书', '同学', '今天', '理性', '抱怨',
             '想', '室友', '字典', '去', '风格', '生成', '大', '奶茶', ]

构建好分词器和字典之后,使用几个句子来进行测试。

test_sentence = '今天天气不错'
result = cut_word(test_sentence, word_dict)
print('|'.join(result))

test_sentence = '我想去成都看大熊猫'
result = cut_word(test_sentence, word_dict)
print('|'.join(result))

从上面的实验结果可以看到,句子分词成功。你或许会有疑问,要是字典里一个对应的词也没有怎么办?我们现在就试一试。

test_sentence = '自然语言处理是人工智能领域的一个重要分支'
result = cut_word(test_sentence, word_dict)
print('|'.join(result))

也许正如你所想的一样,如果字典里一个对应的词都没有,则切分的最终结果会把每个字都切成一个词。这也是基于字典的切分方法的弊端。一般情况下,这种字典不包含的词被称作 未登录词 。关于未登录词的解决方法我们会在下一个实验中讲到。现在我们来测试一下基于正向最大匹配法的分词器性能如何。
这里我们使用北京大学计算语言学研究所提供的中文分词数据集,该数据将搜集的语料是 1998 年人民日报上的文本文字。该数据集可以在 Bakeoff 中下载。
在本次实验中,我们已经将数据存在了服务器中,通过执行下面代码下载到当前目录。

!wget https://labfile.oss.aliyuncs.com/courses/1329/pku_training.txt

下载完成之后,就可以导入数据集。

# 创建一个空列表来存放数据
data_set = []
with open("pku_training.txt",'rb') as f:
    # 每次读取一行数据
    line = f.readline()
    while line:
        line=line.decode("gb18030","ignore")
        data_set.append(line)
        line = f.readline()

查看出前六个句子,并打印出数据集的长度。

# 打印前六行数据
for i in range(7):
    print(data_set[i])
    print('-'*100)
len(data_set)

从上面结果可以看到,该数据集使用空格来将词与词之间区分开。并且总共包含 19056 份数据。该数据集里的每一份数据为一个段落。而分词器的输入为句子。因此将数据集切分成句子的形式。

sentence_list = []  # 创建空列表用于存放处理后的数据
for para in data_set:
    sentence = para.split('。')  # 按 ‘。’ 对句子进行切分
    sentence_list.extend(sentence)
# 打印切分后句子的前 10 个句子
for i in range(10):
    print(sentence_list[i])
    print('-'*100)
len(sentence_list)

从上面的打印结果,可以看出总共含有 54660 个句子。接下来使用该数据集构建一个词典,并划分一部分数据用于测试。

word_dict = []  # 创建一个空列表作为字典
test_sentence_list = []  # 创建一个空列表用于存放测试句子
word_test_labels = []  # 创建一个空列表用于存放测试句子的正确分词结果
for i in range(len(sentence_list)):
    sentence = sentence_list[i]
    sentence = sentence.strip()
    if not sentence:
        continue
    # 将句子按空格进行切分,得到词
    words = sentence.split("  ")
    # 取后 60 个句子用于测试
    if len(sentence_list)-i > 60:
        word_dict.extend(words)
    else:
        test_sentence = ''.join(words)
        test_sentence_list.append(test_sentence)
        word_test_labels.append(words)

查看一下,我们的测试数据是什么样子的。

for i in range(3):
    print(test_sentence_list[i])
    print('-'*100)

查看一下,测试数据对应数据集中所标注的分词结果。

for i in range(3):
    print('|'.join(word_test_labels[i]))
    print('-'*100)

为了能够看出分词器的分词效果,定义一个评价指标来对其进行评价。指标计算公式如下:


image.png

定义计算准确率的函数。

def accurency(y_pre, y):
    """
    分词准确率计算函数
    y_pre:预测结果
    y:     正确结果
    """
    count = 0
    n = len(y_pre)
    for i in range(len(y_pre)):
        # 统计每个句子切分出来的词数
        n += len(y_pre[i])
        for word in y_pre[i]:
            # 统计每个句子切词正确的词数
            if word in y[i]:
                count += 1
    return count/n

下面使用我们前所构建的分词器来进行分词,并求出分词的准确率。注意:这里运行大概要 5 分钟,请耐心等待。

from tqdm.notebook import tqdm

word_cut_result = []  # 创建一个空列表来存放分词结果
# 每次切一个句子
for sent in tqdm(test_sentence_list):
    # 使用前面所构建的分词器进行切词
    temp = cut_word(sent, word_dict)
    # 存放分词后的数据
    word_cut_result.append(temp)
# 计算准确率
acc = accurency(word_cut_result, word_test_labels)
acc

从上面的实验结果可知,分词的准确率约为 85% 左右。这个结果虽然并不算太理想,但也算还可以。下面我们打印出分词结果与数据集的标注结果,进行对比。

for i in range(3):
    print('数据集的标准切分:', '|'.join(word_test_labels[i]))
    print('-'*100)
    print('分词器切分的结果: ', '|'.join(word_cut_result[i]))
    print('='*100)

从上的结果可以看出,我们所构建的基于正向最大匹配的分词器,虽然简单,但分词效果还是很显著的,这也是许多分词器仍然使用这种古老方法的原因,因为我们只需要维护字典就可以。
下面我们使用 jieba 来对同样的数据进行切分,并计算出切分的准确率。

word_cut_result = []  # 创建一个空列表来存放分词结果
# 每次切分一个句子
for sent in test_sentence_list:
    # 使用 jieba 进行分词
    temp = jieba.cut(sent)
    # jieba 分词返回的是一个 jieba 的数据格式,将其转换为列表
    temp = list(temp)
    # 保存分词结果
    word_cut_result.append(temp)
# 计算分词准确率
acc = accurency(word_cut_result, word_test_labels)
acc

使用同样的方法,打印出 jieba 分词的结果。

for i in range(3):
    print('数据集的标准切分:', '|'.join(word_test_labels[i]))
    print('-'*40)
    print('分词器切分的结果: ', '|'.join(word_cut_result[i]))
    print('='*100)

有点意外,jieba 要比我们所构建的分词器要差一点,可能的原因是我们的字典和测试数据属于同一领域的数据,而 jieba 的字典数据则是常用的词数据,因为不同领域的知识通常有差异,常用的词语也不同。例如用医学、金融、法律等文本来测试我们所构建的分词器,效果应该会大打折扣。如下:

test_sentence = '甲状腺激素是一种激素'
result = cut_word(test_sentence, word_dict)
print('|'.join(result))
print('='*100)
result = jieba.cut(test_sentence)
print('|'.join(result))

还有就是切词标准的差异。例如:在我们所使用的的数据中,姓名会切分成 “姓” 和 “名”,而 jieba 则不切分。如下:

test_sentence = '刘德华是歌手,也是演员'
result = cut_word(test_sentence, word_dict)
print('|'.join(result))
print('='*100)
result = jieba.cut(test_sentence)
print('|'.join(result))

但是 jieba 的速度要快许多,当然,这是其内部做了许多的优化。而我们只是搭建了一个简单的分词器,速度没有 jieba 快也是理所当然的,不要灰心。

实现逆向最大匹配法

挑战介绍

在上一个实验中,我们讲解了中文分词的基本方法,并实现了正向最大匹配法。而本次挑战要求实现逆向最大匹配法。其实逆向最大匹配法与正向最大匹配法很相似。正向最大匹配法是从正向开始匹配,而逆向最大匹配法则是从反向开始匹配。两者的差别仅此而已。如果你对正向最大匹配法还不熟悉则可以再去看上一个实验的内容。下面我们再简单讲解一下逆向最大匹配法的流程。

假设我们有要分的句子为【去成都看大熊猫】,词典为【大熊猫,成都】。则切分过程如下:
切分第一个词:输入句子为【去成都看大熊猫】,从句子中取出后边的三个字(因为字典中词的最大长度为 3),开始匹配:
大熊猫
匹配成功,句子变为【去成都看】 。

切分第二个词:输入句子为【去成都看】,从句子中取出后边的三个字,开始匹配:
成都看
都看

单字为词,句子变为【去成都】 。

切分第三个词:输入句子为【去成都】,从句子中取出后边的三个字,开始匹配:
去成都
成都
匹配成功,句子变为【去】 。

切分第四个词:输入句子为【去】,从句子中取出后边的三个字,开始匹配:

单字成词,切分完毕 。
最后切分的结果为【去,成都,看,大熊猫】

挑战内容

在本次挑战中,你需要在 ~/Code/fmm.py 文件中编写一个函数 cut_word,cut_word 函数接受两个参数,分别是要切分的句子和要匹配的字典。然后返回切分结果,切分结果用 list 的形式存放。

挑战要求

代码必须写入 ~/Code/fmm.py 文件中。
函数名必须是 cut_word 。
测试时请使用 /home/shiyanlou/anaconda3/bin/python 运行 fmm.py ,避免出现无相应模块的情况。

你可能感兴趣的:(65自然语言处理底层技术实现及应用--基于字典的中文分词方法)