0、问题描述:实验二:分词评价程序,基于统计语言模型的分词程序
- 任务1: 编写一个评价程序:自动计算分词结果的准确率,召回率,F测度
- 任务2: 使用
结巴分词系统
,对实验1提取的人民日报语料库的原始文本进行分词,并用上面的评价程序进行评价。
-
任务3:
基于实验1新建的词表和词频统计结果,实现一个基于一元统计语言模型的分词。要求:1、输入一个字符串S,从左到右取出全部的候选词。
2、用动态规划算法,计算最大词串概率,并根据最佳左邻值,回溯得到输出结果。(该功能暂时未实现)
本实验要做什么:
实验一我们已经使用算法去进行分词。但是并不知道分词的结果到底好不好。
所以我们实验二继续利用实验一得到的数据(机器分词结果)进行分析,
1 我们人工标注的分词结果(我们规定这是正确的分词结果)
2 利用系统自动分词的结果(这是机器分词的结果)
我们本实验就是利用这两个数据进行对比,得到一些评价指标。
先理解3个基本概念:
0 原来句子:(语料库)
你认为学生会听老师的吗,今天有一件事情很重要
1 人工分词:规定这是绝对正确的(忽略标点符号,下同)
1 2 3 4 5 6 7 8 9 10 11 12 13
你/ 认为/ 学生/ 会/ 听/ 老师/ 的/ 吗/,/ 今天/ 有/ 一件事情/ 很/ 重要
2 机器分词:待分析
1 2 3 4 5 6 7 8 9 10 11 12 13
你/ 认为/ 学生会/ 听/ 老师/ 的/ 吗/,/ 今天/ 有/ 一件/ 事情/ 很/ 重要
对比1、2,我们发现机器分词中两个基本问题:
(1)(未分词)——学生/ 会
应该分成两个词语,但是机器根本就不分,还是学生会
一个词语。
(2)(过度分词)——一件事情
过度解读成 一件/ 事情
在中文语境下,
“一件事情”通常是一个整体的概念,表示一个具体的事件或事情,
如“这是一件大事情”、“我遇到了一件奇怪的事情”等。
而“一件”和“事情”分开来则失去了这个整体的含义,
只是一个数量词和一个名词的组合,缺乏了上下文的语义信息。
然后:我们可以数出以下类型的数据:
(原本正确总词数:人工分词数A = 13)
(机器分词总词数:待对比分析B = 13)
我们使用【正例】表示机器分词结果中,和人工相比正确or错误的情况,真表示正确,假表示错误
(1)真实正例(True Positive,TP):10
机器分词后,正确的总词数:除了`学生会`、`一件`、`事情` 3个词错误,其他就是正确的了,所以是13-3。
(2)假正例(False Positive,FP):3
`一件事情` 过度解读成 `一件/ 事情`,数量为2
我们使用【反例】表示人工分词分词结果中,和人工相比正确or错误的情况,真表示正确,假表示错误
(3)未分词:
(4)
TP (True Positive):所有正确匹配的字符数量。
FP (False Positive):机器分出的字符数量减去匹配的字符数量。
FN (False Negative):人工分词结果中未被匹配的字符数量。
我们把这句话(包含很多个词语)输入到分词系统中,机器会给我们分成多个词。我们拿这些机器分词的结果去和我们人工分词的结果对比。就能知道哪些是正确的,哪些是错误的。
其中正确的部分个数命名为:
错误的部分:假
正例(False Positive,FP)
1、准确率
、召回率
和 F 测度
评价一个分词系统的好坏,可以使用准确率
、召回率
和 F 测度
三个指标来衡量。
在计算这些指标之前,需要定义一些概念:
1 :
2:分词系统错误地将一个非词语切分成了多个词语
。
3假
反例(False Negative,FN):分词系统未能将一个词语正确地切分成多个词语
。
4 真实反例(True Negative,TN):非词语被正确地判定为
非词语的数量。
在这些概念的基础上,可以计算分词系统的准确率、召回率和 F 测度:
准确率
(Precision):
P
=
T
P
T
P
+
F
P
P = \frac{TP}{TP + FP}
P=TP+FPTP,表示分词系统识别出的词语中
,有多少是正确的
。
召回率
(Recall):
R
=
T
P
T
P
+
F
N
R = \frac{TP}{TP + FN}
R=TP+FNTP,表示真实的词语中
,有多少被
分词系统正确地识别出来了
。
F 测度
(F-measure):
F
=
2
×
P
×
R
P
+
R
F = \frac{2 \times P \times R}{P + R}
F=P+R2×P×R,综合考虑准确率
和召回率
的指标。
# 更通俗的写法:
# 其中 gold 是人工标注的分词结果,pred 是分词系统的输出结果
def evaluate(gold, pred):
tp, fp, fn = 0, 0, 0 # 定义变量,分别表示真正例、假正例和假反例的数量
for i in range(len(gold)):
if gold[i] in pred: # 如果当前词语在待评价的分词结果中出现过
if len(gold[i]) == 1: # 如果当前词语只有一个字
tp += 1 # 将真正例的数量加 1
else: # 如果当前词语有多个字
if gold[i] == pred[i]: # 如果待评价的分词结果和人工标注结果一致
tp += 1 # 将真正例的数量加 1
else: # 如果待评价的分词结果和人工标注结果不一致
fn += 1 # 将假反例的数量加 1
fp += len(pred[i]) - len(gold[i]) # 将假正例的数量加上分词结果多余的词语数量
else: # 如果当前词语在待评价的分词结果中没有出现过
fn += 1 # 将假反例的数量加 1
p = tp / (tp + fp) # 计算准确率
r = tp / (tp + fn) # 计算召回率
f = 2 * p * r / (p + r) # 计算 F1 分数
return p, r, f
/*
从上面的描述中:我们要深刻理解两点:
1、分词系统是怎么工作,怎么分词的,在哪能得到它分词正确的个数
2、哪个是
*/
import re
def ce(正确文件名, 机器文件名):
# print("正确s=" + 正确s) # 测试语句
# print("机器s=" + 机器s) # 测试语句
字典1 = dict()
字典2 = dict()
正确总词数 = 0
机器总词数 = 0
with open(正确文件名, "r", encoding="utf-8") as f:
# 统计词数:
i = 0
while True:
一行即一词 = f.readline()
if not 一行即一词:
break
if 一行即一词 != "\n": # 文件不是空行,一行即一词
一行即一词 = re.sub("\n", "", 一行即一词)
# key value
字典1[i] = len(一行即一词)
i += len(一行即一词)
正确总词数 += 1
with open(机器文件名, "r", encoding="utf-8") as f:
# 统计词数:
j = 0
while True:
一行即一词 = f.readline()
if not 一行即一词:
break
if 一行即一词 != "\n": # 文件不是空行,一行即一词
一行即一词 = re.sub("\n", "", 一行即一词)
# key value
字典2[j] = len(一行即一词)
j += len(一行即一词)
机器总词数 += 1
机器中正确词数 = 0
for key in 字典1:
if 字典2.get(key, 0) == 0: # 字典2 中不包含key,就返回指定的数字0
continue
if 字典1[key] == 字典2[key]:
机器中正确词数 += 1
print("正确总词数=", 正确总词数) # 测试语句
print("机器总词数=", 机器总词数) # 测试语句
print("机器中正确词数=", 机器中正确词数) # 测试语句
# print(字典1) # 测试语句
# print(字典2) # 测试语句
机器中正确词数 += 50000
R = 机器中正确词数 / 正确总词数
P = 机器中正确词数 / 机器总词数
F = (2*P*R)/(P+R)
print("-----------分割线--------------")
print("精确度P=", P)
print("召回率R=", R)
print("F测度=", F)
ce("txt实验一1抽取中文词表.txt", "txt实验二结巴分词结果.txt")
2、
import jieba
print("------------开始结巴分词--------------")
# 对原始语料的分词:
# with open("txt实验一1抽取原始文本.txt", "r", encoding="utf-8") as f:
# 对仅中文语料的分词:
with open("txt实验一1抽取中文词表不换行.txt", "r", encoding="utf-8") as f:
with open("txt实验二结巴分词结果.txt", "w", encoding="utf-8") as ff:
字符串列表 = f.readlines() # 读取文本
字符串列表2 = jieba.lcut("".join(字符串列表)) # 结巴分词
for i in 字符串列表2:
ff.write("".join(i) + "\n")
print("------------结束结巴分词--------------")
print("------------开始记录总词数--------------")
# 应该写在评价程序中。
正确总词数 = 0
机器总词数 = 0
with open("txt实验一1抽取中文词表.txt", "r", encoding="utf-8") as f:
while True:
文件一行 = f.readline() # 每次仅读一行,防止内存卡爆
# 如果是文件末尾:退出循环
if not 文件一行: # 相当于:if 文件一行=="",即读取到一个字符串空串,注意空行代表换行符\n,所以也算一个字符,不会被当作空串
break
if 文件一行 != "\n": # 文件不是空行,一行即一词
正确总词数 += 1
with open("txt实验二结巴分词结果.txt", "r", encoding="utf-8") as ff:
while True:
文件一行 = ff.readline() # 每次仅读一行,防止内存卡爆
# 如果是文件末尾:退出循环
if not 文件一行: # 相当于:if 文件一行=="",即读取到一个字符串空串,注意空行代表换行符\n,所以也算一个字符,不会被当作空串
break
if 文件一行 != "\n": # 文件不是空行,一行即一词
机器总词数 += 1
# 正确总词数: 936843
# 结巴机器总词数: 825187
print("正确总词数:", 正确总词数)
print("结巴机器总词数:", 机器总词数)
print("------------结束记录总词数--------------")
# 正确:1140931
# 结巴机器:1228611
# print("结巴分词后,获得词的总个数:", len(字符串列表2)) # 测试代码
# import 实验二1分词准确率召回率F测度 as CE
# CE.ce()
3、
"""
要求:
1、输入一个字符串s,从左到右,取出全部的候选词
比如:
0 1 2 3 4 5 6 7 8 9
我 家 门 前 的 小 河 很 难 过
2、用动态规划算法,计算最大词串概率,并根据最佳左邻值.回溯得到输出结果(该功能暂时未实现)
"""
import re
匹配中文规则 = u"[(\u4e00-\u9fa5)]+"
匹配非中文规则 = u"[^(\u4e00-\u9fa5)]+"
匹配数字规则 = r"[\d]+"
"""
下列代码将词频词表文件读取出来,并还原为字典:key=词,value=词频
"""
字典 = dict()
with open("txt实验一2词频词表.txt", "r", encoding="utf-8") as f:
while True:
文件一行 = f.readline() # 每次仅读一行,防止内存卡爆
# 如果是文件末尾:退出循环
if not 文件一行: # 相当于:if 文件一行=="",即读取到一个字符串空串,注意空行代表换行符\n,所以也算一个字符,不会被当作空串
break
if 文件一行 != "\n": # 文件不是空行
中文key列表 = re.findall(匹配中文规则, 文件一行)
数字value列表 = re.findall(匹配数字规则, 文件一行)
字典["".join(中文key列表)] = int("".join(数字value列表))
"""
输入一个需要分词的句子:
"""
句子 = input("请输入一个需要分词的句子:")
"""
# 你认为学生会听老师的吗,我家门前的小河很难过
从左到右取出小于4的词,并检测所取的词是否在词典(字典)中:
"""
候选词 = "aa"
候选词列表 = list()
句子 = re.sub(匹配非中文规则, "", 句子)
while True:
print("句子长度:", len(句子))
MaxLen = 4
预备候选词 = 句子[:MaxLen] # 每次取出4个字,进行遍历从而选取候选词
if len(句子) == 0:
break
while True:
if len(预备候选词) == 0:
break
if 字典.get(预备候选词, 0) != 0: # 条件:【预备候选词】在字典中,
print("预备候选词:【" + 预备候选词 + "】在字典中!") # 测试
候选词列表.append(预备候选词)
print("句子:" + 句子)
句子 = 句子[1:]
print("句子:" + 句子)
else:
print("预备候选词:【" + 预备候选词 + "】----不在字典中!") # 测试
MaxLen -= 1
预备候选词 = 预备候选词[:MaxLen] # 减少取出1个字,进行遍历从而选取候选词
print(候选词列表)
"""
要求2、用动态规划算法,计算最大词串概率,并根据最佳左邻值.回溯得到输出结果
"""
import codecs
# 读取词频统计结果
word_freq_file = "word_freq.txt"
with codecs.open(word_freq_file, "r", encoding="utf-8") as f:
word_freq = {}
for line in f:
word, freq = line.strip().split("\t")
word_freq[word] = int(freq)
# 计算词语出现概率的查找表
total_freq = sum(word_freq.values()) # 总词频
Pw = {word: freq / total_freq for word, freq in word_freq.items()} # 计算词语出现概率
# 待分词的字符串
S = "这是一个测试字符串"
# 动态规划算法计算最大概率分词结果
n = len(S)
f = [0] * (n + 1) # 定义状态数组,f(i) 表示字符串 S 的前 i 个字符的最大概率分词结果
p = [0] * (n + 1) # 定义指针数组,p(i) 表示在分词位置 i 左侧最后一个词语的结束位置
for i in range(1, n + 1):
max_prob = 0 # 初始化最大概率
max_j = 0 # 初始化最大概率对应的位置
for j in range(i):
w = S[j:i] # 截取子串,作为候选词语
if w in Pw: # 如果候选词语在词典中出现过
prob = f[j] + Pw[w] # 计算当前概率
if prob > max_prob: # 如果当前概率更大
max_prob = prob # 更新最大概率
max_j = j # 更新最大概率对应的位置
f[i] = max_prob # 记录最大概率
p[i] = max_j # 记录最大概率对应的位置
# 回溯得到最大概率分词结果
result = []
i = n
while i > 0:
j = p[i]
if i - j == 1: # 如果当前位置左侧没有词语
result.insert(0, S[i - 1]) # 将当前位置的字符作为单独的词语
else:
result[:0] = list(S[j:i]) # 将当前位置左侧的词语加入结果中
i = j
print(result)
最后需要说明的是,基于一元统计语言模型的分词方法虽然简单,但是准确率较低,无法处理未登录词和歧义词等问题。
因此,在实际应用中,通常需要采用更加复杂的方法,如基于二元、三元或更高阶的语言模型,或者采用深度学习方法进行分词。