基于词表的中文分词

一、实验目的​

了解并掌握基于匹配的分词方法,以及分词效果的评价方法。


二、实验要求​

实现正向最大匹配、逆向最大匹配以及双向最大匹配等三种分词方法,记录并分析三种方法的准确率以及分词速度。思考并分析哪些因素可能会影响分词的准确性。


三、实验准备​

1. 词典准备

​在GitHub(​​https://github.com/fxsjy/jieba​​​)开源的一个中文词表数据,下载地址:​​https://raw.githubusercontent.com/fxsjy/jieba/master/jieba/dict.txt​

文件含349046个词,内容部分截图如图1,做本实验时加载该文件时需要进行“数据清理”(即把后面的3m, 3i之类的信息删除即可)

Python自然语言处理基础实验1_基于词表的中文分词_词表

图1

2. 待分词的中文句子​

保存于文本文档msr_test.txt,一行一个句子。

扬帆远东做与中国合作的先行
希腊的经济结构较特殊。
海运业雄踞全球之首,按吨位计占世界总数的17%。
另外旅游、侨汇也是经济收入的重要组成部分,制造业规模相对较小。
多年来,中希贸易始终处于较低的水平,希腊几乎没有在中国投资。
十几年来,改革开放的中国经济高速发展,远东在崛起。
瓦西里斯的船只中有40%驶向远东,每个月几乎都有两三条船停靠中国港口。
他感受到了中国经济发展的大潮。
他要与中国人合作。
他来到中国,成为第一个访华的大船主。
访问归来,他对中国发展充满信心,他向希腊海运部长介绍了情况,提出了两国在海运、造船业方面合作的建议。
1995年10月,希腊海运部长访华时,他根据“船长”的建议与中方探讨了在海运、造船方面合作的可能与途径。
“船长”本人还与几个船主联合起来准备与我远洋公司建立合资企业。
“船长”常说,要么不干,干就要争第一。
他拥有世界最大的私人集装箱船队,也要做与中国合作的先行。
找准人生价值的坐标
王思斌,男,1949年10月生。
北京大学社会学系教授、博士生导师、系主任,中国社会学会副会长、中国社会工作教育协会副会长兼秘书长。
多年来,一直从事农村社会学、组织社会学方面的研究,主要著述有《社会学概论》(合编)、《经济体制改革对农村社会关系的影响》等。
译著有《社会管理》、《人的前景》。


3. 已正确分词结果(验证用)​

保存于文本文档msr_test_gold.txt,一行对应每个句子的分词结果,每个词用两个空格隔开。

扬帆  远东  做  与  中国  合作  的  先行  
希腊 的 经济 结构 较 特殊 。
海运 业 雄踞 全球 之 首 , 按 吨位 计 占 世界 总数 的 17% 。
另外 旅游 、 侨汇 也是 经济 收入 的 重要 组成部分 , 制造业 规模 相对 较小 。
多年来 , 中 希 贸易 始终 处于 较低 的 水平 , 希腊 几乎 没有 在 中国 投资 。
十几年 来 , 改革开放 的 中国 经济 高速 发展 , 远东 在 崛起 。
瓦西里斯 的 船只 中 有 40% 驶 向 远东 , 每个 月 几乎 都 有 两三条 船 停靠 中国 港口 。
他 感受 到 了 中国 经济 发展 的 大潮 。
他 要 与 中国人 合作 。
他 来到 中国 , 成为 第一个 访 华 的 大 船主 。
访问 归来 , 他 对 中国 发展 充满 信心 , 他 向 希腊海运部 长 介绍 了 情况 , 提出 了 两国 在 海运 、 造船 业 方面 合作 的 建议 。
1995年10月 , 希腊海运部 长 访 华 时 , 他 根据 “ 船长 ” 的 建议 与 中方 探讨 了 在 海运 、 造船 方面 合作 的 可能 与 途径 。
“ 船长 ” 本人 还 与 几个 船主 联合 起来 准备 与 我 远洋公司 建立 合资 企业 。
“ 船长 ” 常 说 , 要么 不 干 , 干 就要 争 第一 。
他 拥有 世界 最大 的 私人 集装箱 船队 , 也 要 做 与 中国 合作 的 先行 。
找 准 人生 价值 的 坐标
王思斌 , 男 , 1949年10月 生 。
北京大学社会学系 教授 、 博士生 导师 、 系 主任 , 中国社会学会 副 会长 、 中国社会工作教育协会 副 会长 兼 秘书长 。
多年来 , 一直 从事 农村 社会学 、 组织 社会学 方面 的 研究 , 主要 著述 有 《 社会学 概论 》 ( 合编 ) 、 《 经济体制 改革 对 农村 社会关系 的 影响 》 等 。
译著 有 《 社会 管理 》 、 《 人 的 前景 》 。


四、实验设计​

1. 词典、待分词的句子获取

① 定义方法get_word_dict:

输入:词典数据的文本文档的路径

输出:所有“词汇”的元组、最大词汇长度

过程:

  • 打开文本文档
  • 逐行读取文档内容,除去中文词语后面的空格等不需要的信息
  • 保存词语并计算最大词语长度
  • 返回结果

② 定义方法get_sentences

输入:待分词的句子的文本文档路径

输出:包含所有句子的列表

③ 定义方法get_result_splited

输入:正确分词结果的文本文档路径、词语分割符(默认两个空格)

输出:包含所有句子分词结果的列表(列表的元素是一个列表,对应一个句子的正确分词结果)


注意:读取txt文件时要注意文件的编码格式,一般在Windows中txt会被保存为gbk格式,在Linux保存为utf-8格式,编程读取txt文件时需要关注这一点,否则可能报错。在本实验中txt文件统一保存为utf-8格式。


2. 正向最大匹配(FMM)

参数:词典dict_,最长词的长度length_max,待分词的句子文本sentence

  • 从sentence的最左端(句首)开始,按照从左到右的顺序进行扫描,扫描的字符长度为length_max
  • 在扫描得到的字符串与词典中的词进行匹配
  • 匹配成功,则切分出该词
  • 匹配失败,则去掉字符串最右边的一个字
  • 利用剩下的字符串重新和词典中的词法行匹配,直到剩余字符串与词典中的词完全匹配
  • 当不断去掉最右边的一个字后字符串只剩一个字时即未在词典中匹配到“词语”,切分出剩下的最后一个字
  • (一个改进,RMM同)匹配失败时,若当前字符串不包含中文,则截取当前字符串作为一个“词语”(具体实现看“源码”部分)
  • 当前字符串匹配完成后,接着按照最大长度length_max 从左向右左进行文本扫指、匹配,直到扫指到句尾,全都四配成功。


关于改进的说明:

匹配失败时,截取字符串前几个都不是中文的字符作为词组,目的是把形如把“17%”的这样应该分出来但不存在于词典的“词语”。另外,为避免中文标点符号对词语的影响,即可能出现扫描到“,17%”这样的字符串时没有把标点符号分出来的情况,在定义一个判断字符串是否包含中文的方法时,将常见的中文标点符号作为中文,方法定义如下:

Python自然语言处理基础实验1_基于词表的中文分词_中文分词_02

图2


3. 逆向最大匹配(RMM)

基本原理与FMM类似,区别在于扫描及切分的方向与FMM相反。这里以一个图4的例子比较FMM和RMM的原理。

词典dict_= {‘扬帆远航’,’乘风扬帆’,’扬帆’,’任远东’,’远东国家’,’远东’,’中国人’,‘中国’,‘合作社’,‘合作’,‘先知’,‘先’,‘先行’,‘行’}。

最长词的长度length_max = 4

待分词的句子文本sentence = “扬帆远东做与中国合作的先行”


Python自然语言处理基础实验1_基于词表的中文分词_最大匹配算法_03

图3

4. 双向最大匹配(BM)

比较FMM和RMM的结果,选择最优结果,比较规则如下:

  • 如果分词数量结果不同
  • 选择数量较少的那个
  • 如果分词数量结果相同
  • 分词结果完全相同,取任意一个
  • 分词结果不同,取结果中单个字的数量较少的一个
  • 若单个字的数量也相同,取任意一个


5. 三种分词算法结果对比

对比指标:

分词速度:平均每秒分词数量(在当前词典下,这里的分词速度是相对的);

分词准确率:分词正确的句子数量/所有句子数量;

其实,一般来说,不能只是用整个句子是否完全分词正确来评估分词结果(即准确率),还需要中文分词评价标准,也即机器学习分类常用的分词标准:精确率P、召回率R与综合了P、R指标的F-score。这里以句子“扬帆远东做与中国合作的先行”的一个分词结果为例来说明以上三个指标在NLP中的应用。

正确的分词结果:['扬帆', '远东', '做', '与', '中国', '合作', '的', '先行'];

某程序的分词结果:['扬帆', ‘远’, ‘东', '做', '与', '中国', '合作', '的', '先', '行']。

将正确的分词结果中的所有词语构成集合A,某程序的分词结果中的所有词语构成集合B,在计算三个指标前,需要计算TP/FN/FP/TN的值,它们的值含义如下:

  • TP: Ture Positive 把正的判断为正的数目;
  • FN: False Negative 把正的错判为负的数目;
  • FP: False Positive 把负的错判为正的数目;
  • TN: True Negative 把负的判为负的数目;

则有:

  • TP + FN = |A|
  • |A|表示对集合A取模,即集合A中元素的数目,下同
  • TP + FP = |B|
  • TP = |A ∩ B|

三个评价指标计算公式:

  • P = |A ∩ B| / |B|
  • R = |A ∩ B| / |A|
  • F-score=2PR/(R+P)

这里的A ∩ B = {'扬帆', '做', '与', '中国', '合作', '的'},|A ∩ B| = 6,|A| = 8,|B| = 10。最终求得P = 0.6, R = 0.75, F-score = 0.667。一般来说,这三个值的大小越接近1,表示分词结果越接近正确的分词结果。

关于编程如何实现,一般使用区间表示句子中的每个单词。对于长为n的字符串,设分词结果的每个单词按照其在文中的起止位置记作区间[i,j],其中0≤i≤j≤n,进而求得元素都为一个个区间的集合A和集合B。


五、实验源码与运行结果

(运行于Jupyter Notebook

# 获取文件path_file中的的词典(词表)及词表中词语的最大长度
def get_word_dict(path_file='a.txt'):
word_dict = []
length_max = 0
with open(path_file, 'r', encoding='utf-8') as file:
for word in file:
word = word.split(' ')[0]
word_dict.append(word)
length_max = max(length_max, len(word))
# 元组使用的存储空间比列表少(其实应该使用集合,这样分词速度可能会更快)
return tuple(word_dict), length_max


# 获取文件path_file的所有句子,即获取所有待分词的句子
def get_sentences(path_file='msr_test.txt'):
sentences_l = []
with open(path_file, 'r', encoding='utf-8') as file:
for item in file:
sentences_l.append(item.strip())
return sentences_l

# 获取正确的分词结果
def get_result_splited(path_file='msr_test_gold.txt', sep=' '):
sentences_splited_l = []
with open(path_file, 'r', encoding='utf-8') as file:
for item in file:
sentences_splited_l.append(item.strip().split(sep))
return sentences_splited_l
# 词表、词表中词语最大长度
word_dict, length_max = get_word_dict(r"C:\Users\adminXL\Desktop\NLP_experiment\dict\dict.txt")
# 待分词的句子
sentences_splited_pending = get_sentences("msr_test.txt")
# 正确分词结果
sentences_splited_right = get_result_splited('msr_test_gold.txt')

print('词表词语数量:',len(word_dict))
print('词语最大长度:',length_max)
print('待分词句子的数量:',len(sentences_splited_pending),end='\n\n')

print('前5句待分词的句子: ')
for s in sentences_splited_pending[:5]:
print(s)
print()
print('前5句的正确分词结果: ')
for s in sentences_splited_right[:5]:
print(s)

Python自然语言处理基础实验1_基于词表的中文分词_分词结果评价指标_04

图4 运行结果1


# 判断字符串是否含有中文
# 注:“'\u4e00' 和'\u9fa5'分别是utf-8编码的第一个和最后一个中文字符的编码
def is_contain_chinese(strs):
for _char in strs:
if _char in ",。;:、" or '\u4e00' <= _char <= '\u9fa5':
return True
return False

def FMM(dict_, length_max, sentence):
segment_list = [] # 存放分词后的分词词组
start = 0
#
while start != len(sentence):
index = start + length_max
if index > len(sentence):
index = len(sentence)

for i in range(length_max):
w = sentence[start:index]
if w in dict_ or index-start == 1 or not is_contain_chinese(w):
segment_list.append(w)
start = index
break
index -= 1

return segment_list


def RMM(dict_, length_max, sentence):
segment_list = [] # 存放分词后的分词词组
start = len(sentence)
while start != 0:
index = start - length_max
if index < 0:
index = 0

for i in range(length_max):
w = sentence[index:start]
if w in dict_ or start-index == 1 or not is_contain_chinese(w):
segment_list.append(w)
start = index
break
index += 1

return segment_list[::-1]


def BM(dict_, length_max, sentence):
res_fmm = FMM(dict_, length_max, sentence)
res_rmm = RMM(dict_, length_max, sentence)
num_fmm, num_rmm = len(res_fmm), len(res_rmm)
if num_fmm == num_rmm:
if res_fmm == res_fmm:
return res_fmm
else:
num_one_fmm = sum(len(w) == 1 for w in res_fmm)
num_one_rmm = sum(len(w) == 1 for w in res_rmm)
return res_fmm if num_one_fmm < num_one_rmm else res_rmm
else:
return res_fmm if res_fmm < res_fmm else res_rmm
import time

# 基于词表的中文分词,计算分词速度
def get_speed_splited(dict_, length_max, sentences_splited_pending, option='fmm'):
'''输入:
词典dict_
最大词语长度length_max
待分词句子列表sentences_l
分词算法选项option
'''
num_splited = 0
results_l = [] # 存放每个句子的分词结果
t_start = time.time()

if option == 'fmm':
for sentence in sentences_splited_pending:
res = FMM(dict_, length_max, sentence)
num_splited += len(res)
results_l.append(res)
elif option == 'rmm':
for sentence in sentences_splited_pending:
res = RMM(dict_, length_max, sentence)
num_splited += len(res)
results_l.append(res)
else:
for sentence in sentences_splited_pending:
res = BM(dict_, length_max, sentence)
num_splited += len(res)
results_l.append(res)

t_end = time.time()
t_used = round(t_end - t_start) # 取整
if t_used == 0:
t_used = 1
print('本次分词使用的算法为:',option)
print(f'\t所用时间约{t_used}s')
print(f"\t分词速度:平均每秒{int(num_splited/t_used)}个词")
return results_l

# 准确度计算
def get_accuracy_splited(sentences_splited_right: list, sentences_splited_pending: list):
num_res = len(sentences_splited_pending)
num_right = sum(sentences_splited_right[i] == sentences_splited_pending[i] for i in range(num_res))
acc = num_right/num_res
print('\t完全正确分词的句子数量:',num_right)
print(f"\t分词准确率:{acc}")
return acc

# P,R,F评价指标计算
def get_P_R_F_score(splited_result, right_result):
'''程序分词结果splited_result
分词结果标准答案right_result
'''
# 求两个集合的交集
a_b_set = set(splited_result) & set(right_result)
a_b = len(a_b_set)
a = len(right_result)
b = len(splited_result)

P = a_b/b
R = a_b/a
F_score = 2*P*R/(P+R)
return P,R,F_score

# 计算所有分词结果的P,R,F的平均值
def get_averaged_P_R_F(splited_results_l, right_results_l):
p_sum,r_sum,f_sum = 0,0,0
n = len(splited_results_l)
for i in range(n):
p,r,f = get_P_R_F_score(splited_results_l[i],right_results_l[i])
p_sum += p
r_sum += r
f_sum += f
return p_sum/n, r_sum/n, f_sum/n
fmm_results_l = get_speed_splited(word_dict, length_max, sentences_splited_pending, option='fmm')
fmm_acc = get_accuracy_splited(sentences_splited_right, fmm_results_l)
print('P, R, F指标的平均值分别为:')
print(get_averaged_P_R_F(fmm_results_l, sentences_splited_right))

rmm_results_l = get_speed_splited(word_dict, length_max, sentences_splited_pending, option='rmm')
rmm_acc = get_accuracy_splited(sentences_splited_right, rmm_results_l)
print('P, R, F指标的平均值分别为:')
print(get_averaged_P_R_F(rmm_results_l, sentences_splited_right))

bm_results_l = get_speed_splited(word_dict, length_max, sentences_splited_pending, option='BM')
bm_acc = get_accuracy_splited(sentences_splited_right, bm_results_l)
print('P, R, F指标的平均值分别为:')
print(get_averaged_P_R_F(bm_results_l, sentences_splited_right))

Python自然语言处理基础实验1_基于词表的中文分词_中文分词_05

图5 三种分词算法对比运行结果


六、实验小结​

本实验的三种基于词表的分词算法(FMM, RMM, BM)在简单场景可以发挥出较好的分词效果,但其算法的时间复杂度较高,且理解中文的歧义问题不够准确,故存在一定的局限性。

基于词表的分词算法,对词表中的数据十分依赖,所用的词典的内容是影响分词的准确性的最大因素。在一般的分词应用中,词表的“词语”的主题要和待分词的句子的场景的主题尽可能地接近。

另外,词典中各个词语之间的“干扰”也会影响分词的正确与否,这主要表现在“长词”对于“短词”的影响,比如:词典中有词语:{海运,海运业,业,海,运,渔业,渔牧业,牧业},这几个词语在实际中都是可以表示确切的含义,在使用正向/逆向最大匹配算法进行分词时,算法会根据截取的最大的“字串”匹配词典中的“词语”,若有一个句子为“海运业和渔牧业”,对其进行分词时,“海运业”会在当前词典的基础下被分为一个词,实际上,“海运业”还可以继续分为“海运”和“业”两个词语,分词正确与否还要看整个句子的具体场景;“渔牧业”这个词更是如此。

不难看出,如果只是看分词句子的准确率,分词效果是非常差的(准确率不到30%),但我们看到P, R, F指标的是比较接近1的,说明每个句子仅有很少的词语没有被分出来。如图6,以第三个句子为例,它只有“海运业”这个词没被分成两个词语,其它词语均被分出来了。

print('第三句话的分词结果:')
print(rmm_results_l[2])
print(fmm_results_l[2])
print(bm_results_l[2])
print('正确分词结果:')
print(sentences_splited_right[2])
print('rmm分词结果三种评价指标的值:')
print(get_P_R_F_score(rmm_results_l[2],sentences_splited_right[2]))

Python自然语言处理基础实验1_基于词表的中文分词_最大匹配算法_06

图6 

最后,本实验只是作为NLP实验入门学习,可以以此理解分词算法的思想。因为分词是自然语言处理其它应用不可或缺的一部分,好的分词结果将直接影响后续更多处理的效率。


七、参考博文

​中文分词原理及常用Python中文分词库介绍 - 知乎​

​NLP中P,R,F1,acc含义以及怎么求 - CSDN​

​NLP中文分词的评估指标 - 知乎​