中文分词问题主要来源于:在汉语中,句子是以字为单位的,但是语义理解仍然是需要以词为单位,所以也就存在了中文分词问题。
主要的技术可以分为:规则分词、统计分词以及混合分词(规则+统计)
基于规则的分词是一种机械分词,主要依赖于维护词典,在切分时将与剧中的字符串与词典中的词进行匹配。
主要的切分方法包括三种:正向最大匹配法、逆向最大匹配法以及双向最大匹配法。
#定义正向最大匹配法类
class MM(object):
def __init__(self):
self.window_size=3 #词典中最长字符串包含的字数
def cut(self,text):
result=[]
index=0
text_length=len(text)
dic=['研究','研究生','生命','命','的','起源'] #词典
while text_length>index: #只要还有字就进行匹配
for size in range(self.window_size+index,index,-1): #生成可能长度
piece=text[index:size]
if piece in dic:
index=size-1 #匹配成功将index设置为匹配成功的最后一个字的位置
break
index=index+1 #开始下一个字符串的匹配
result.append(piece+'----')
print(result)
if __name__=='__main__':
text='研究生命的起源'
tokenizer=MM()
tokenizer.cut(text)
结果如下所示:
[‘研究生----’, ‘命----’, ‘的----’, ‘起源----’]
#定义逆向最大匹配法类
class RMM(object):
def __init__(self):
self.window_size=3
def cut(self,text):
result=[]
index=len(text) #从文本末尾开始
dic=['研究','研究生','生命','命','的','起源']
while index>0:
for size in range(index-self.window_size,index):
piece=text[size:index] #找到最后几个字组成的字符串
if piece in dic:
index=size+1 #将位置更新为匹配到的最后一个字的位置
break
index=index-1 #开始新的位置
result.append(piece+'----')
result.reverse() #由于从最后进行匹配,所以顺序是反的,需要颠倒过来
print(result)
if __name__=='__main__':
text='研究生命的起源'
tokenizer=RMM()
tokenizer.cut(text)
结果如下所示:
[‘研究----’, ‘生命----’, ‘的----’, ‘起源----’]
#定义双向最大匹配法的类
class BMM(object):
def __init__(self):
self.window_size=3
self.result_MM=[]
self.result_RMM=[]
self.num_MM=0
self.num_RMM=0
#正向最大
def MM(self,text):
index=0
text_length=len(text)
dic=['研究','研究生','生命','命','的','起源'] #词典
while text_length>index: #只要还有字就进行匹配
for size in range(self.window_size+index,index,-1): #生成可能长度
piece=text[index:size]
if piece in dic:
index=size-1 #匹配成功将index设置为匹配成功的最后一个字的位置
break
index=index+1 #开始下一个字符串的匹配
self.result_MM.append(piece+'----')
if len(piece)==1:
self.num_MM+=1
#逆向最大
def RMM(self,text):
index=len(text) #从文本末尾开始
dic=['研究','研究生','生命','命','的','起源']
while index>0:
for size in range(index-self.window_size,index):
piece=text[size:index] #找到最后几个字组成的字符串
if piece in dic:
index=size+1 #将位置更新为匹配到的最后一个字的位置
break
index=index-1 #开始新的位置
self.result_RMM.append(piece+'----')
if len(piece)==1:
self.num_RMM+=1
self.result_RMM.reverse() #由于从最后进行匹配,所以顺序是反的,需要颠倒过来
def cut(self,text):
if len(self.result_MM)>len(self.result_RMM):
result=self.result_RMM
elif len(self.result_MM)<len(self.result_RMM):
result=self.result_MM
elif len(self.result_MM)==len(self.result_RMM):
if self.result_MM==self.result_RMM:
result=self.result_RMM
else:
if self.num_MM>self.num_RMM:
result=self.result_RMM
else:
result=self.result_MM
print(result)
if __name__=='__main__':
text='研究生命的起源'
tokenizer=BMM()
tokenizer.MM(text)
tokenizer.RMM(text)
tokenizer.cut(text)
结果如下所示:
[‘研究----’, ‘生命----’, ‘的----’, ‘起源----’]
主要思想:将每个词视作由字组成,如果相连的字在不同文本中出现次数越多,就越可能是一个词。
基于统计的分词一般有以下两步:
HMM将分词作为字在字串中的序列标注任务来实现的。
一个简单的例子:
中文分词是文本处理不可或缺的一步!
中/B 文/E 分/B 词/E 是/S 文/E 本/M 处/M 理/E 不/B 可/M 或/M 缺/E 的/S 一/B 步/E !/S
抽象表示
用 λ = λ 1 λ 2 ⋯ λ n \lambda = {\lambda _1}{\lambda _2} \cdots {\lambda _n} λ=λ1λ2⋯λn代表输入的句子, n n n为句子长度, λ i \lambda_i λi表示字, o = o 1 o 2 ⋯ o n o = {o_1}{o_2} \cdots {o_n} o=o1o2⋯on表示输出的标签,那么理想输出为:
max = max P ( o 1 o 2 ⋯ o n ∣ λ 1 λ 2 ⋯ λ n ) \max = \max P\left( {{o_1}{o_2} \cdots {o_n}\left| {{\lambda _1}{\lambda _2} \cdots {\lambda _n}} \right.} \right) max=maxP(o1o2⋯on∣λ1λ2⋯λn)
其中 o o o为B、M、E、S这四种标记。
通过HMM计算
通过Veterbi求解 max P ( λ ∣ o ) P ( o ) \max{P\left( {\lambda \left| o \right.} \right)P\left( o \right)} maxP(λ∣o)P(o)
代码示例
使用的训练集来自:https://github.com/canshang/python-nlk/blob/master/trainCorpus.txt_utf8.txt
class HMM(object):
def __init__(self):
import os
# 主要是用于存取算法中间结果,不用每次都训练模型
self.model_file = 'hmm_model.pkl'
# 状态值集合
self.state_list = ['B', 'M', 'E', 'S']
# 参数加载,用于判断是否需要重新加载model_file
self.load_para = False
# 用于加载已计算的中间结果,当需要重新训练时,需初始化清空结果
def try_load_model(self, trained):
if trained:
import pickle
with open(self.model_file, 'rb') as f:
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):
# 重置几个概率矩阵
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 for s in self.state_list}
self.Pi_dic[state] = 0.0
self.B_dic[state] = {}
Count_dic[state] = 0
def makeLabel(text):
out_text = []
if len(text) == 1:
out_text.append('S')
else:
out_text += ['B'] + ['M'] * (len(text) - 2) + ['E']
return out_text
init_parameters()
line_num = -1
# 观察者集合,主要是字以及标点等
words = set()
with open(path, encoding='utf8') as f:
for line in f:
line_num += 1
line = line.strip()
if not line:
continue
word_list = [i for i in line if i != ' ']
words |= set(word_list) # 更新字的集合
linelist = line.split()
line_state = []
for w in linelist:
line_state.extend(makeLabel(w))
assert len(word_list) == len(line_state)
for k, v in enumerate(line_state):
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]].get(word_list[k], 0) + 1.0 # 计算发射概率
self.Pi_dic = {k: v * 1.0 / line_num for k, v in self.Pi_dic.items()}
self.A_dic = {k: {k1: v1 / Count_dic[k] for k1, v1 in v.items()}
for k, v in self.A_dic.items()}
#加1平滑
self.B_dic = {k: {k1: (v1 + 1) / Count_dic[k] for k1, v1 in v.items()}
for k, v in self.B_dic.items()}
#序列化
import pickle
with open(self.model_file, 'wb') as f:
pickle.dump(self.A_dic, f)
pickle.dump(self.B_dic, f)
pickle.dump(self.Pi_dic, f)
return self
def viterbi(self, text, states, start_p, trans_p, emit_p):
V = [{}]
path = {}
for y in states:
V[0][y] = start_p[y] * emit_p[y].get(text[0], 0)
path[y] = [y]
for t in range(1, len(text)):
V.append({})
newpath = {}
#检验训练的发射概率矩阵中是否有该字
neverSeen = text[t] not in emit_p['S'].keys() and \
text[t] not in emit_p['M'].keys() and \
text[t] not in emit_p['E'].keys() and \
text[t] not in emit_p['B'].keys()
for y in states:
emitP = emit_p[y].get(text[t], 0) if not neverSeen else 1.0 #设置未知字单独成词
(prob, state) = max(
[(V[t - 1][y0] * trans_p[y0].get(y, 0) *
emitP, y0)
for y0 in states if V[t - 1][y0] > 0])
V[t][y] = prob
newpath[y] = path[state] + [y]
path = newpath
if emit_p['M'].get(text[-1], 0)> emit_p['S'].get(text[-1], 0):
(prob, state) = max([(V[len(text) - 1][y], y) for y in ('E','M')])
else:
(prob, state) = max([(V[len(text) - 1][y], y) for y in states])
return (prob, path[state])
def cut(self, text):
import os
if not self.load_para:
self.try_load_model(os.path.exists(self.model_file))
prob, pos_list = self.viterbi(text, self.state_list, self.Pi_dic, self.A_dic, self.B_dic)
begin, next = 0, 0
for i, char in enumerate(text):
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('trainCorpus.txt_utf8.txt')
text='这是一个很棒的方案!'
res=hmm.cut(text)
print(text)
print(str(list(res)))
结果如下所示:
这是一个很棒的方案!
[‘这是’, ‘一个’, ‘很’, ‘棒’, ‘的’, ‘方案’, ‘!’]
最常见的是先基于词典进行分词,再用统计分词进行辅助。
规则分词:https://github.com/canshang/python-nlk/blob/master/规则分词.ipynb
数据集(HMM训练集):https://github.com/canshang/python-nlk/blob/master/trainCorpus.txt_utf8.txt
统计分词(HMM):https://github.com/canshang/python-nlk/blob/master/trainCorpus.txt_utf8.txt
《Python自然语言处理实战》