例子:“南京市长江大桥”
词典:["南京","市长","大桥","长江","江","市"]
我们有这样一句话"南京市长江大桥",有一个词典[“南京”,“市长”,“江”,“大桥”,“长江”],怎么通过词典来进行分词呢?
前向最大匹配的前向意思是说,从前往后匹配。最大意思是说,我们匹配的词的长度越大越好,也就是这句话中分出来的词的数量越少越好。
这里,我们假设这个最大长度max_len = 5:
第一轮搜索:
①"南京市长江 大桥" 词典中没有南京市长江
这个词,匹配失败
②"南京市长 江大桥" 词典中没有南京市长
这个词,匹配失败
③"南京市长江大桥" 词典中没有南京市
这个词,匹配失败
④"南京市长江大桥" 词典中有南京
这个词,匹配成功,去除
句子变为:“市长江大桥”
第二轮搜索:
①"市长江大桥" 词典中没有市长江大桥
这个词,匹配失败
②"市长江大桥" 词典中没有市长江大
这个词,匹配失败
③"市长江大桥" 词典中没有市长江
这个词,匹配失败
④"市长江大桥" 词典中有市长
这个词,匹配成功,去除
句子变为:“江大桥”
第三轮搜索(句子长度已不足5,将max_len改为3):
①"江大桥" 词典中没有江大桥
这个词,匹配失败
②"江大桥"" 词典中没有江大
这个词,匹配失败
③"江大桥" 词典中有江
这个词,匹配成功,去除
句子变为:“大桥”
第四轮搜索:
①"大桥" 词典中有大桥
这个词,匹配成功,去除
句子变为:"",说明已经处理完毕
最终结果:“南京 / 市长 / 江 / 大桥”
这个结果虽然勉强可以接受,可以认为它说的意思是,南京的市长名字叫江大桥,但是明显跟我们想要表达的或者想要理解的意思不一样,这就有了分词的歧义问题。
如果我们的词典里有南京市这个词,那么结果就是"南京市/长江/大桥"。
后向最大匹配的后向意思是说,从后往前匹配。最大意思同样是说,我们匹配的词的长度越大越好。
这里,我们同样假设这个最大长度max_len = 5:
第一轮搜索:
①"南京市长江大桥 " 词典中没有市长江大桥
这个词,匹配失败
②"南京市长江大桥 " 词典中没有长江大桥
这个词,匹配失败
③"南京市长江大桥 " 词典中没有江大桥
这个词,匹配失败
④"南京市长江大桥 " 词典中有大桥
这个词,匹配成功,去除
句子变为:“南京市长江”
第二轮搜索:
①"南京市长江 " 词典中没有南京市长江
这个词,匹配失败
②"南京市长江 " 词典中没有京市长江
这个词,匹配失败
③"南京市长江 " 词典中没有市长江
这个词,匹配失败
④"南京市长江 " 词典中有长江
这个词,匹配成功,去除
句子变为:“南京市”
第三轮搜索(句子长度已不足5,将max_len改为3):
①"南京市 " 词典中没有南京市
这个词,匹配失败
②"南京市 " 词典中没有京市
这个词,匹配失败
③"南京市 " 词典中有市
这个词,匹配成功,去除
句子变为:“南京”
第四轮搜索:
①"南京 " 词典中有南京
这个词,匹配成功,去除
句子变为:"",说明已经处理完毕
最终结果:“南京 / 市 / 长江 / 大桥”
相同的话,相同的词典,分出来的效果却不一样,导致了歧义问题。统计结果表明,单纯使用后向最大匹配算法的错误率略低于正向最大匹配算法。
python代码实现:
# -*- coding: utf-8 -*-
# Author : 不凡不弃
# Datetime : 2020/5/27 0027 13:39
# description : 分词
dictionaries = ["南京", "市长", "大桥", "长江", "江", "市"]
# 前向最大匹配
def forward_max_matching(text, max_len=5):
result = []
text_ = text
index = max_len
while len(text_) > 0:
if index == 0:
print("分词失败,词典中没有这个词")
return []
if text_[:index] in dictionaries:
result.append(text_[:index])
text_ = text_[index:]
index = 5
else:
index = index - 1
return "".join(word + "/" for word in result)
# 后向最大匹配
def backward_max_matching(text, max_len=5):
result = []
text_ = text
index = max_len
while len(text_) > 0:
if index == 0:
print("分词失败,词典中没有这个词")
return []
# print(text_[-index:])
if text_[-index:] in dictionaries:
result.insert(0, text_[-index:])
# result.append(text_[-index:])
text_ = text_[:-index]
index = 5
else:
index = index - 1
return "".join(word + "/" for word in result)
if __name__ == '__main__':
content = "南京市长江大桥"
forward_result = forward_max_matching(content)
print("forward_result:", forward_result)
backward_result = backward_max_matching(content)
print("backward_result:", backward_result)
以上分词算法的缺点有哪些?
例子:“经常有意见分歧”
词典:["经常","有","意见","意","见","有意见","分歧","分","歧"]
概率P(x):{"经常":0.08,"有":0.04,"意见":0.08,"意":0.01,"见":0.005,"有意见":0.002,"分歧":0.04,"分":0.02, "歧":0.005}
概率P(x)代表的是该词x在我们日常生活所见的文本中出现的概率。
step1:我们根据词典,找出所有可能的分词情况,如下:
step2:我们如果有一个语言模型,可以计算出每种情况属于中文语法的概率,我们选择最高的,也就是效果最好的。例如第一个情况的概率为:
P ( 经 常 , 有 意 见 , 分 歧 ) = P ( 经 常 ) ∗ P ( 有 意 见 ) ∗ P ( 分 歧 ) = 0.08 ∗ 0.02 ∗ 0.04 = 0.000064 P(经常,有意见,分歧) = P(经常)*P(有意见)*P(分歧)=0.08*0.02*0.04=0.000064 P(经常,有意见,分歧)=P(经常)∗P(有意见)∗P(分歧)=0.08∗0.02∗0.04=0.000064
我们考虑到这样算的话值会很小,很容易造成内存溢出,所以引入 − l n -ln −ln来计算,也就是由算P(x)的最大值变为算 − l n ( P ( x ) ) -ln(P(x)) −ln(P(x))的最小值,
-ln(P(x)):{"经常":2.52,"有":3.21,"意见":2.52,"意":4.6,"见":5.29,"有意见":6.21,"分歧":3.21,"分":3.9, "歧":5.29}
− l n ( P ( 经 常 , 有 意 见 , 分 歧 ) ) = − l n ( P ( 经 常 ) ∗ P ( 有 意 见 ) ∗ P ( 分 歧 ) ) = − l n ( P ( 经 常 ) − l n ( 有 意 见 ) − l n ( 分 歧 ) = 2.52 + 6.21 + 3.21 = 11.94 -ln(P(经常,有意见,分歧)) = -ln(P(经常)*P(有意见)*P(分歧))=-ln(P(经常)-ln(有意见)-ln(分歧)=2.52+6.21+3.21=11.94 −ln(P(经常,有意见,分歧))=−ln(P(经常)∗P(有意见)∗P(分歧))=−ln(P(经常)−ln(有意见)−ln(分歧)=2.52+6.21+3.21=11.94
但是这两步下来时间复杂度太高
,并不可取。怎么能够优化一下呢?就是接下来的维特比算法
例子:“经常有意见分歧”
我们仍然是有以下几个数据:
词典:["经常","有","意见","意","见","有意见","分歧","分","歧"]
概率P(x):{"经常":0.08,"有":0.04,"意见":0.08,"意":0.01,"见":0.005,"有意见":0.002,"分歧":0.04,"分":0.02, "歧":0.005}
-ln(P(x)):{"经常":2.52,"有":3.21,"意见":2.52,"意":4.6,"见":5.29,"有意见":6.21,"分歧":3.21,"分":3.9, "歧":5.29}
我们构建以下的DAG(有向图),每一个边代表一个词,我们将-ln(P(x))的值标到边上,
求 − l n ( P ( x ) ) -ln(P(x)) −ln(P(x))的最小值问题,就转变为求最短路径的问题。
由图可以看出,路径 0—>②—>③—>⑤—>⑦ 所求的值最小,所以其就是最优结果:经常 / 有 / 意见 / 分歧
那么我们应该怎样快速计算出来这个结果呢?
我们设 f ( n ) f(n) f(n)代表从起点0到结点n的最短路径的值,所以我们想求的就是 f ( 7 ) f(7) f(7)
,我们考虑到结点⑦有2条路径:
我们应该从2条路径中选择路径短的。
在上面的第1条路径中, f ( 5 ) f(5) f(5)还是未知的,我们要求 f ( 5 ) f(5) f(5),同理我们发现到结点⑤的路径有3条路径:
我们同样从3条路径中选择路径短的。以此类推,直到结点0,所有的路径值都可以算出来。我们维护一个列表来表示f(n)的各值:
结点 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
f(n) | 20 | 2.52 | 5.73 | 25.73 | 8.25 | 12.5 | 11.46 |
结点的上一个结点 | 0 | 0 | ② | ③ | ③ | ⑤ | ⑤ |
第2行代表从起点0到该结点的最短路径的值,第3行代表在最短路径中的该节点的上一个结点。通过表,我们可以找到结点⑦的上一个结点⑤,结点⑤的上一个结点③,结点③的上一个结点②,结点②的上一个结点0,即路径:0—>②—>③—>⑤—>⑦
python代码实现:
# -*- coding: utf-8 -*-
# Author : 不凡不弃
# Datetime : 2020/5/27 0027 13:39
# description : 维特比算法(viterbi)
import math
import collections
# 维特比算法(viterbi)
def word_segmentation(text):
##################################################################################
word_dictionaries = ["经常", "有", "意见", "意", "见", "有意见", "分歧", "分", "歧"]
probability = {"经常": 0.08, "有": 0.04, "意见": 0.08, "意": 0.01, "见": 0.005, "有意见": 0.002, "分歧": 0.04, "分": 0.02,
"歧": 0.005}
probability_ln = {key: -math.log(probability[key]) for key in probability}
# 构造图的代码并没有实现,以下只是手工建立的图,为了说明 维特比算法
##################################################################################
# 有向五环图,存储的格式:key是结点名,value是一个结点的所有上一个结点(以及边上的权重)
graph = {
0: {0: (0, "")},
1: {0: (20, "经")},
2: {0: (2.52, "经常"), 1: (20, "常")},
3: {2: (3.21, "有")},
4: {3: (20, "意")},
5: {2: (6.21, "有意见"), 3: (2.52, "意见"), 4: (20, "见")},
6: {5: (3.9, "分")},
7: {5: (3.21, "分歧"), 6: (5.29, "歧")}
}
# 保存结点n的f(n)以及实现f(n)的上一个结点
f = collections.OrderedDict()
for key, value in graph.items():
# 如果该节点的上一个结点还没有计算f(n),则直接比较大小;如果计算了f(n),则需要加上f(n)
# 从该节点的所有上一个结点中选择f(n)值最小的
min_temp = min((pre_node_value[0], pre_node_key) if pre_node_key not in f else (
pre_node_value[0] + f[pre_node_key][0], pre_node_key) for pre_node_key, pre_node_value in value.items())
f[key] = min_temp
print(f)
# 结果:OrderedDict([
# (0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)),
# (4, (25.73, 3)), (5, (8.25, 3)), (6, (12.15, 5)), (7, (11.46, 5))
# ])
# 最后一个结点7
last = next(reversed(f))
# 第一个结点0
first = next(iter(f))
# 保存路径,最后一个结点先添入
result = [last, ]
# 最后一个结点的所有前一个结点
pre_last = f[last]
# 没到达第一个结点就一直循环
while pre_last[1] is not first:
# 加入一个路径结点X
result.append(pre_last[1])
# 定位到路径结点X的上一个结点
pre_last = f[pre_last[1]]
# 第一个结点添入
result.append(first)
print(result)
# 结果:[7, 5, 3, 2, 0]
text_result = []
# 找到路径上边的词
for i, num in enumerate(result):
if i + 1 == len(result):
break
# print(i, num, result[i + 1])
word = graph[num][result[i + 1]][1]
# print(word)
text_result.append(word)
# 翻转一下
text_result.reverse()
return "".join(word + "/" for word in text_result)
if __name__ == '__main__':
content = "经常有意见分歧"
word_segmentation_result = word_segmentation(content)
print("word_segmentation_result:", word_segmentation_result)