中文分词算法 | 基于词表的三种分词算法

324 阅读8分钟

Hi,大家好啊!最近在学习自然语言处理(NLP)的相关知识,加上这学期开了自然语言处理这门课,并且在飞桨AI Studio上初步学习并运行相关项目。让我们首先认识一下自然语言处理:它主要应用于机器翻译、舆情监测、自动摘要、观点提取、文本分类、问题回答、文本语义对比、语音识别、中文OCR等方面,其与最近很火的语言大模型以及ChatGPT等之类由很强的关联。本文主要介绍中文分词算法中的基于词表的分词算法

分词是中文自然语言处理的基础,没有中文分词,我们对语言很难量化,进而很能运用数学的知识去解决问题。与拉丁语系不同,中文是需要分词的。

  • 对于拉丁语系是不需要分词的,因为他们的词语之间有空格分割,可以根据空格就可以把单词分开。比如英语、法语等。
  • 对于亚系语言是需要分词的,因为他们中间没有空格,比如中文、韩文及日文等。

中文分词指的是将一个汉字序列切分成一个一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。

一、正向最大匹配法(FMM)

1. 算法介绍

正向最大匹配法,对于输入的一段文本从左至右、以贪心的方式切分出当前位置上长度最大的词。正向最大匹配法是基于词典的分词方法,其分词原理是:单词的颗粒度越大,表示的含义越确切。

2. 算法步骤

(1)一般从一个字符串的开始位置,选择一个最大长度的词长的片段,如果序列不足最大词长,则选择全部序列;
(2)首先看该片段是否在词典中,若是,则算为一个分出来的词,若不是,则从右边开始,减少一个字符,然后看短一点的这个片段是否在词典中,依次循环,逐到只剩下一个字;
(3)序列变为第2步骤截取分词后,剩下的部分序列。

3. 代码演示

#  实现正向匹配算法中的切词方法
def cut_words(raw_sentence, words_dic):
    """
    :param raw_sentence: 需要分词句子
    :param words_dic: 词典列表
    :return:
    """

    max_length = max(len(word) for word in words_dic)  # 统计词典中最长的词
    sentence = raw_sentence.strip()
    words_length = len(sentence)  # 统计序列长度
    # 存储切分好的词语
    cut_word_list = []
    while words_length > 0:
        max_cut_length = min(max_length, words_length)
        subSentence = sentence[0: max_cut_length]
        while max_cut_length > 0:
            if sub_sentence in words_dic:
                cut_word_list.append(sub_sentence)
                break
            elif max_cut_length == 1:
                cut_word_list.append(sub_sentence)
                break
            else:
                max_cut_length = max_cut_length - 1
                sub_sentence = sub_sentence[0:max_cut_length]
        sentence = sentence[max_cut_length:]
        words_length = words_length - max_cut_length
    return cut_word_list

二、逆向最大匹配法(BMM)

1. 算法介绍

逆向最大匹配法和正向方法一样,只不过,对于输入的一段文本从右至左,以贪心的方式切分出当前位置上长度最大的词。FMM或BMM对于一些有歧义的词处理能力一般。

举个例子:

原始句子“为人民办公益”
使用BMM可能会分成“为人/民办/公益”。
使用FMM可能是“为/人民/办/公益”。

2. 算法步骤

(1)一般从一个字符串的开始位置,选择一个最大长度的词长的片段,如果序列不足最大词长,则选择全部序列。
(2)首先看该片段是否在词典中,如果是,则算为一个分出来的词,如果不是,则从边开始,减少一个字符,然后看短一点的这个片段是否在词典中,依次循环,逐到只剩下一个字。
(3)序列变为第2步骤截取分词后,剩下的部分序列。

3. 代码演示

# 实现逆向最大匹配算法中的切词方法
def cut_words(raw_sentence, words_dic):
    max_length = max(len(word) for word in words_dic) # 统计词典中词的最长长度
    sentence = raw_sentence.strip()
    words_length = len(sentence)# 统计序列长度
    cut_word_list = []# 存储切分出来的词语
    # 判断是否需要继续切词
    while words_length > 0:
        max_cut_length = min(max_length, words_length)
        sub_sentence = sentence[-max_cut_length:]
        while max_cut_length > 0:
            if sub_sentence in words_dic:
                cut_word_list.append(sub_sentence)
                break
            elif max_cut_length == 1:
                cut_word_list.append(sub_sentence)
                break
            else:
                max_cut_length = max_cut_length -1
                sub_sentence = sub_sentence[-max_cut_length:]
        sentence = sentence[0:-max_cut_length]
        words_length = words_length - max_cut_length
    cut_word_list.reverse()
    return cut_word_list

三、双向最大匹配法(Bi-MM)

1. 算法介绍

双向最大匹配法是将正向最大匹配法得到的分词结果逆向最大匹配法的到的结果进行比较,从而决定正确的分词方法。据SunM.S. 和 Benjamin K.T.(1995)的研究表明,中文中90.0%左右的句子,正向最大匹配法和逆向最大匹配法完全重合且正确,只有大概9.0%的句子两种切分方法得到的结果不一样,但其中必有一个是正确的(歧义检测成功),只有不到1.0%的句子,或者正向最大匹配法和逆向最大匹配法的切分虽重合却是错的,或者正向最大匹配法和逆向最大匹配法切分不同但两个都不对(歧义检测失败)。这正是双向最大匹配法在实用中文信息处理系统中得以广泛使用的原因所在。

2. 算法步骤

(1)如果正反向分词结果词数不同,则取分词数量较少的那个。
(2)如果分词结果词数相同:分词结果相同,就说明没有歧义,可返回任意一个。分词结果不同,返回其中单字较少的那个。

3. 代码演示

# 实现双向匹配算法中的切词方法
def cut_words(raw_sentence, words_dic):
    bmm_word_list = BMM.cut_words(raw_sentence, words_dic)
    fmm_word_list = FMM.cut_words(raw_sentence, words_dic)
    bmm_word_list_size = len(bmm_word_list)
    fmm_word_list_size = len(fmm_word_list)
    if bmm_word_list_size != fmm_word_list_size:
        if bmm_word_list_size < fmm_word_list_size:
            return bmm_word_list
        else:
            return fmm_word_list
    else:
        FSingle = 0
        BSingle = 0
        isSame = True
        for i in range(len(fmm_word_list)):
            if fmm_word_list[i] not in bmm_word_list:
                isSame = False
            if len(fmm_word_list[i]) == 1:
                FSingle = FSingle + 1
            if len(bmm_word_list[i]) == 1:
                BSingle = BSingle + 1
        if isSame:
            return fmm_word_list
        elif BSingle > FSingle:
            return fmm_word_list
        else:
            return bmm_word_list

四、代码实例

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
@Project : 中文分词算法
@File    : 基于词表的三种分词算法.py
@IDE     : PyCharm
@Author  : 源于花海
@Date    : 2023/10/08 22:09
"""
# 查看当前挂载的数据集目录, 该目录下的变更重启环境后会自动还原
# View dataset directory.
# This directory will be recovered automatically after resetting environment.
!ls /home/aistudio/data

# 查看工作区文件, 该目录下的变更将会持久保存. 请及时清理不必要的文件, 避免加载过慢.
# View personal work directory.
# All changes under this directory will be kept even after reset.
# Please clean unnecessary files in time to speed up environment loading.
!ls /home/aistudio/work

# 如果需要进行持久化安装, 需要使用持久化路径, 如下方代码示例:
# If a persistence installation is required,
# you need to use the persistence path as the following:
!mkdir /home/aistudio/external-libraries
!pip install beautifulsoup4 -t /home/aistudio/external-libraries

import sys
sys.path.append('/home/aistudio/external-libraries')


def FMM(words_dic, raw_sentence):
    fmmresult = []
    # 词典中最长词长度
    max_len = max([len(item) for item in words_dic])
    start = 0
    # FMM为正向,start从初始位置开始,指向结尾即为结束
    while start != len(raw_sentence):
        # index的初始值为start的索引+词典中元素的最大长度或句子末尾
        index = start + max_len
        if index > len(raw_sentence):
            index = len(raw_sentence)
        for i in range(max_len):
            # 当分词在字典中时或分到最后一个字时,将其加入到结果列表中
            if (raw_sentence[start:index] in words_dic) or (len(raw_sentence[start:index]) == 1):
                # print(sentence[start:index], end='/')
                fmmresult.append(raw_sentence[start:index])
                # 分出一个词,start设置到index处
                start = index
                break
            # 正向时index每次向句尾挪一位
            index += -1
    return fmmresult


def RMM(words_dic, raw_sentence):
    rmmresult = []
    # 词典中最长词长度
    max_len = max([len(item) for item in words_dic])
    start = len(raw_sentence)
    # RMM为逆向,start从末尾位置开始,指向开头位置即为结束
    while start != 0:
        # 逆向时index的初始值为start的索引-词典中元素的最大长度或句子开头
        index = start - max_len
        if index < 0:
            index = 0
        for i in range(max_len):
            # 当分词在字典中时或分到最后一个字时,将其加入到结果列表中
            if (raw_sentence[index:start] in words_dic) or (len(raw_sentence[index:start]) == 1):
                # print(sentence[index:start], end='/')
                rmmresult.insert(0, raw_sentence[index:start])
                # 分出一个词,start设置到index处
                start = index
                break
            # 逆向时index每次向句头挪一位
            index += 1
    return rmmresult


def BM(words_dic, raw_sentence):
    # res1 与 res2 为FMM与RMM结果
    res1 = FMM(words_dic, raw_sentence)
    res2 = RMM(words_dic, raw_sentence)
    if len(res1) == len(res2):
        # FMM与RMM的结果相同时,取任意一个
        if res1 == res2:
            return res1
        else:
            # res1_sn 和 res2_sn 为两个分词结果的单字数量,返回单字较少的
            res1_sn = len([i for i in res1 if len(i) == 1])
            res2_sn = len([i for i in res2 if len(i) == 1])
            return res1 if res1_sn < res2_sn else res2
    else:
        # 分词数不同则取分出词较少的
        return res1 if len(res1) < len(res2) else res2


# 代码包括定义词典,定义待分词变量,调用并且输出三种分词函数。
words_dic = ['我', '在', '燕山大学', '读书', '专业', '是', '软件', '工程', '软件工程']
raw_sentence = '我在燕山大学读书,专业是软件工程'

print("the results of FMM :\n", FMM(words_dic, raw_sentence), end="\n")
print("the results of RMM :\n", RMM(words_dic, raw_sentence), end="\n")
print("the results of BM :\n", BM(words_dic, raw_sentence))