Python自然语言处理实战之规则分词方法全解析
参考书目:python自然语言处理实战——核心技术与算法

规则分词
顾名思义,分词直接依靠规则。这种方法是机械的分词方法。其主要手段是把语句的每个字符串与词表进行匹配。找到匹配的就分词,找不到就不分词。
词表:
天气
真好
今天
伤心
冬瓜汤
句子:
今天天气真好
结果:
今天/天气/真好
规则分词按照匹配的方式,主要有正向最大匹配法;规则分词按照匹配的方式,主要有逆向最大匹配法;规则分词按照匹配的方式,主要有双向最大匹配法
正向最大匹配法
基本思想如下:首先将词典中的最长词长设定为 x,以此作为初始匹配长度。接着,取句子的前 x 个字符与词典进行匹配。倘若未找到匹配的词,就将 x 减 1。如此反复进行操作,一直到找到词或者 x 等于 1 为止。若找到了词,就从该词后面重新开始,以 x 为长度再次进行匹配,如此持续重复,直到句子划分完毕。
算法步骤:
从待划分句子的左边开始向右取 x 个字符,这里的 x 是词典中最大的词长。用这 x 个字符组成一个字符串,然后将这个字符串与词典进行匹配。如果匹配成功,就把这个字段划分出来;如果匹配不成功,就把 x 减 1,再次进行匹配。重复这个步骤,直到划分完成。
class MM(object):
"""
正向最大匹配法 分词
"""
def __init__(self, dic_path):
self.dictionary = set()
self.maximum = 0
打开(dic_path, 'r', encoding='utf8'),并将其赋值给 f 。
for line in f:
line = line.strip()
if not line:
continue
self.dictionary.add(line)
if len(line) > self.maximum:
self.maximum = len(line)
def cut(self, text):
result = []
index = 0
while index < len(text):
word = None
从 self.maximum 开始,以 0 为结束,步长为-1,逐个获取 size 的值。
if len(text)-index < size:
continue
piece = text[index:(index+size)]
if piece in self.dictionary:
word = piece
result.append(word)
index += size
break
if word is None:
result.append(text[index])
index += 1
return result
def main():
text = "南京市长江大桥"
tokenizer = MM('./data/imm_dic.utf8')
print(tokenizer.cut(text))
main()
逆向最大匹配法:
思想思路大致相同,只是方向发生了反转。具体来说,如果要匹配的内容是“今天天气真好”,且 x = 3,那么第一个字段就是“气真好”;如果匹配不到,就去掉“气”,变为“真好”。
class RMM(object):
"""
逆向最大匹配法 分词
"""
def __init__(self, dic_path):
"""
方法:初始化词典以及最大词长度
输入:dic_path 词典地址
"""
self.dictionary = set()#词典
self.maximum = 0#最大词长
# 读取词典
使用 open 函数以只读模式('r')和 utf-8 编码打开指定路径(dic_path)的文件,将其赋值给变量 f 。
for line in f:
line = line.strip()
if not line:
continue
self.dictionary.add(line)
if len(line)>self.maximum:
self.maximum = len(line)
def cut(self, text):
"""
方法:分词
输入:text 待分词文本
输出:result 词列表
"""
result = []
index = len(text)
while index > 0:
word = None
#从最长的词开始找
for size in range(self.maximum, 0, -1):
if index - size < 0:
continue
piece = text[(index - size):index]
if piece in self.dictionary:
word = piece
result.append(word)
index -= size
break
if word is None:
result.append(text[index])
index -= 1
return result[::-1]
def main():
text = "南京市长江大桥"
tokenizer = RMM('./data/imm_dic.utf8')
print(tokenizer.cut(text))
main()
这里要提一句,因为汉语中偏正结构比较多,要是从后往前进行匹配,就能够适当地提高精确度。
双向最大匹配法
基本思想:就是用上述两个方法划完之后,谁词少就选谁
统计分词
主要思想为:将每个词视为由词的最小单位的各个字所构成。倘若相连的字在不同文本中出现的频次越高,那就表明这些相连的字很有可能是一个词。
以下是基于HMM的实现:
class HMM(object):
def __init__(self):
"""
方法:初始化参数
"""
import os
# 主要是用于存取算法中间结果,不用每次都训练模型
self 的 model_file 为 './data/hmm_model.pkl'
# 状态值集合
self 的 state_list 包含了 'B'、'M'、'E'、'S' 这几个元素。
# 参数加载,用于判断是否需要重新加载model_file
self.load_para = False
定义一个名为 try_load_model 的函数,该函数接受一个参数 trained ,用于尝试加载模型。
"""
方法是用来加载已经计算好的中间结果的,在需要重新进行训练的时候,就需要将结果进行初始化并清空。
输入:trained :是否已经训练好
"""
if trained:
import pickle
打开 self.model_file 文件,以二进制读取模式打开,将其赋值给变量 f ,就像这样:f = open(self.model_file, 'rb')
self.A_dic = pickle.load(f)
self.B_dic = pickle.load(f)
self.Pi_dic = pickle.load(f)
self.load_para = True
else:
# 状态转移概率(状态->状态的条件概率)
self.A_dic = {}
# 发射概率(状态->词语的条件概率)
self.B_dic = {}
# 状态的初始概率
self.Pi_dic = {}
self.load_para = False
def train(self, path):
"""
方法:计算转移概率、发射概率以及初始概率
输入:path:训练材料路径
"""
# 重置几个概率矩阵
self.try_load_model(False)
# 统计状态出现次数,求p(o)
Count_dic = {}
# 初始化参数
def init_parameters():
for state in self.state_list:
self.A_dic 中的 state 对应一个字典,该字典中每个键 s 对应的值都为 0.0,这里的 s 取自 self.state_list 这个列表
self.Pi_dic[state] = 0.0
self.B_dic[state] = {}
Count_dic[state] = 0
def makeLabel(text):
"""
方法:为训练材料每个词划BMES
输入:text:一个词
输出:out_text:划好的一个BMES列表
"""
out_text = []
if len(text) == 1:
out_text.append('S')
else:
out_text 加上字符 'B' ,然后加上长度为 (len(text) - 2) 的 'M' 字符组成的列表,最后加上字符 'E'
return out_text
init_parameters()
line_num = -1
# 观察者集合,主要是字以及标点等
words = set()
打开(path,编码为'utf8'),然后将其作为 f 进行操作。
for line in f:
line_num += 1
line = line.strip()
if not line:
continue
对于给定的行 line ,从其中筛选出所有不等于空格字符 ' ' 的元素 i ,将这些元素组成一个列表 word_list 。
words 会将 word_list 中的元素添加进去,从而更新字的集合。
linelist = line.split()
line_state = []
for w in linelist:
line_state 会进行扩展操作,扩展的内容是通过 makeLabel(w) 生成的。
确认单词列表的长度等于行状态的长度。
获取 line_state 中的元素,通过 enumerate 函数进行遍历,每次遍历会返回一个索引值 k 和对应的元素 v
Count_dic[v] += 1
if k == 0:
self.Pi_dic 中的 v 所对应的数值加 1 ,这是用于计算每个句子的第一个字的状态的,也就是用于计算初始状态概率的
else:
self.A_dic 中 line_state 里索引为 k - 1 的元素对应的 v 位置的值加 1 ,以此来计算转移概率
self.B_dic 中 line_state[k] 对应的位置与 word_list[k] 对应的位置的值为
self.B_dic 中 line_state[k] 对应的元素获取到的内容。
计算发射概率,即 word_list[k] 与 0 相加后再加 1.0
self.Pi_dic 等于一个字典推导式,对于 self.Pi_dic 中的每个键值对 k 和 v,将 v 乘以 1.0 后再除以 line_num,得到新的键值对,最终构成一个新的字典。
self.A_dic 等于一个字典,这个字典的键 k 对应的值是另一个字典,该字典的键 k1 对应的值是 v 中键 k1 的值除以 Count_dic 中键 k 的值。
对于 self.A_dic 中的每一个键值对 k 和 v 进行操作。
# 加1平滑
self.B_dic 等于一个字典,这个字典的键 k 对应的值是另一个字典,该字典的键 k1 对应的值是 (v1 + 1) 除以 Count_dic[k],其中 k1 和 v1 是 v 中的键值对
遍历 self.B_dic 中的每一个键值对,将键赋值给 k,将值赋值给 v 。
# 序列化
import pickle
使用 open 函数以二进制写入模式打开 self.model_file 文件,并将文件对象赋值给 f 。
pickle.dump(self.A_dic, f)
pickle.dump(self.B_dic, f)
pickle.dump(self.Pi_dic, f)
return self
定义一个名为 viterbi 的函数,该函数接收 text、states、start_p、trans_p、emit_p 作为参数。
"""
方法:维特比算法,寻找最优路径,即最大可能的分词方案
输入:text:文本
states:状态集
start_p:第一个字的各状态的可能
trans_p:转移概率
emit_p:发射概率
输出:prob:概率
path:划分方案
"""
V = [{}] #路径图
path = {}
对于 y 在 states 中:#对第一个字的各状态的可能性进行初始化
V[0][y]等于 start_p[y]乘以 emit_p[y]中获取到的 text[0]对应的数值,如果不存在则为 0
path[y] = [y]
对于 t 而言,其范围是从 1 到 text 的长度。也就是说,对于每一个字的情况。
V.append({})
newpath = {}
# 检验训练的发射概率矩阵中是否有该字
neverSeen 表示 text[t] 不在 emit_p['S'].keys() 中,并且满足以下条件:
具体来说,就是把原来的长句拆分成几个独立的小句子,每个小句子表达一个相对简单的意思,这样可以使句子更加清晰明了,也更符合语言表达的习惯。例如,对于“text[t] not in emit_p['M'].keys() and”这样的长句,可以拆分成“text[t]不在 emit_p['M']的键中”和“以及”这样两个小分句,这样就更便于理解和阅读。
text[t] 不在 emit_p['E'] 的键中,并且
语言模型暂无法直接处理你提供的这种特定格式的内容呢。你可以用更常见的文字描述来让我进行改写等操作呀。
for y in states:#每个字的每个状态的可能
emitP = emit_p[y].get(
如果 not neverSeen,则设置 text[t], 0;否则设置为 1.0 # 设置未知字单独成词
上一个字可能的状态,接着算出当前字最可能的状态,其中 prob 是最大可能,state 为上一个字的状态。
(prob, state) = max(
在时间 t - 1 时,语言模型输出的结果 V[t - 1][y0],乘以从状态 y0 转移到状态 y 的概率 trans_p[y0].get(y, 0)
emitP, y0)
对于 y0 处于 states 之中,如果 V[t - 1][y0]大于 0 ,则进行相应操作。
V[t][y] = prob
newpath 的 y 等于 path 的 state 加上 y ,以此来更新路径。
path = newpath
如果 emit_p['M'].get(text[-1], 0)比 emit_p['S'].get(text[-1], 0)大,那就意味着最后一个字在词中的出现概率可能大于它单独成词的概率。
先计算[(V[len(text) - 1][y], y) for y in ('E', 'M')]这个列表中的每个元素,然后找出其中概率最大的元素,将其赋值给(prob, state)
else:#否则就直接选最大可能的那条路
首先找到所有状态下的概率值,即计算每个状态对应的概率,然后找出其中的最大值,这个最大值对应的概率和状态分别赋值给 (prob, state) 。具体来说,就是对每个状态 y ,计算 V[len(text) - 1][y] 的值,将其与状态 y 组成一个元组 (V[len(text) - 1][y], y) ,再将所有这些元组放入一个列表中,最后从列表中找出概率值最大的那个元组,其第一个元素即为 prob ,第二个元素即为 state 。
return (prob, path[state])
#用维特比算法分词,并输出
def cut(self, text):
import os
if not self.load_para:
如果存在 self.model_file ,则调用 self.try_load_model 方法
prob 和 pos_list 由 self.viterbi 函数计算得出。
使用 text、self.state_list、self.Pi_dic、self.A_dic 和 self.B_dic 。
begin, next = 0, 0
i 表示索引 ,char 表示字符 。
pos = pos_list[i]
if pos == 'B':
begin = i
elif pos == 'E':
yield text[begin: i+1]
next = i+1
elif pos == 'S':
yield char
next = i+1
if next < len(text):
yield text[next:]
hmm = HMM()
hmm.train('./data/trainCorpus.txt_utf8')
text = '南京市长江大桥'
res = hmm.cut(text)
print(text)
print(str(list(res)))
中文分词工具:jieba
"""
分词
"""
import jieba
sent = '南京市长江大桥'
#全模式
jieba 对 sent 进行全模式切割,得到的切词结果为 seg_list 。
打印由斜杠(/)连接的 seg_list 中的元素。
#精确模式,默认
使用 jieba 库对 sent 进行分词,切分模式为精确模式(cut_all=False),得到的分词结果存储在 seg_list 中。
print('/'.join(seg_list))
#搜索引擎模式
seg_list 是通过 jieba 的 cut_for_search 方法对 sent 进行处理后得到的结果。
print('/'.join(seg_list))