基于统计的中文分词主要思想是:利用字与字相邻出现的频率来反应成词的可靠度,统计语料中相邻共现的各个字的组合的频度,当组合频度高于某一个临界值时,我们便可认为此字组可能会构成一个词语。
基于统计的分词一般有两个步骤:
1)建立统计语言模型;
2)对句子进行单词划分,然后对划分结果进行概率计算,获得概率最大的分词方式。
一、语言模型
语言模型用概率论的术语来描述就是:为长度为m 的字符串确定其概率分布,其中到依次表示文本中的各个词语。一般采用链式法则计算概率值,如下:
=
由于文本过长时,计算难度增大,所以提出了n元模型来降低计算难度。n元模型是值在估算条件概率时,忽略距离大于等于n的上文词的影响,因此上式可简化为:
当n=1时称为一元模型,在一元模型中,整个句子的概率等于各个词语概率的乘积。这样就完全损失了句中的词序信息。
当n=2时称为二元模型,当n=3时称为三元模型。当n>=2时,模型可以保留一定的词序信息,n越大,保留的词序信息越丰富,但计算成本也成指数级增长。
二、隐马尔科夫模型(HMM)
隐马尔科夫模型(HMM)是将分词作为字在字符串中的序列标注任务来实现的。基本思路是:每个字在构造一个特定的词语时都占据着一个特定的构词位置(即词位),规定每个字最多只能有四个构词位置:B(词首)、M(词中)、E(词尾)、S(单独成词)。
例如:中文/分词/是/文本处理/不可或缺/的/一步!
逐字标注:中/B文/E分/B词/E是/S文/B本/M处/M理/E不/B可/M或/M缺/E的/S一/B步/E!/S
用公式表示如下:
其中,表示输入的句子,n是句子长度,表示字,代表输出的标签。
在分词时,o是B、E、M、S这四种标记,为句子中的每个字(包含标点等非中文字符)。
通过贝叶斯公式可得:
其中为给定的输入,计算为常数,可以忽略,因此最大化等价于最大化。
接下来要运用到隐马尔科夫模型的两个基本假设:
1)观测独立性假设,即任意时刻的观测只依赖该时刻的状态:
2)齐次马尔科夫假设,即当前输出只与上一个输出有关:
因此,
我们将称为发射概率,称为状态转移概率,初始概率。
求解max的常用方法是维特比算法。它是一种动态规划方法,核心思想是:如果最终的最优路径经过某个,那么从初始节点到的路径必然也是一个最优路径,因为每一个节点只会影响前后两个和。根据递推的方法,在考虑每个时只需要求出所有经过各点的最优路径,然后再与当前结合比较,找出最优路径。
下面通过python实现HMM,这里的语料格式是:每行一句话,每个词以空格分隔。
# coding=utf-8
#根据HMM来进行分词
class HMM():
def __init__(self):
self.pi_prob={}
self.tran_state={}
self.emit_prob={}
self.state_set=['B','M','E','S'] #状态集合
#根据训练样本来计算初始概率、状态转移概率、发射概率
def calculate_prob(self):
count_state = {}
#初始化初始概率、状态转移概率、发射概率
for state in self.state_set:
self.pi_prob[state]=0.0
self.tran_state[state]={s: 0.0 for s in self.state_set}
self.emit_prob [state]={}
count_state[state]=0.0
#获取训练语料中各个字的词位标记
def get_marks(w):
out_text = []
if len(w) == 1:
out_text.append('S')
else:
out_text += ['B'] + ['M'] * (len(w) - 2) + ['E']
#print(out_text)
return out_text
with open('train.txt',encoding='utf-8') as f:
line_num=0
for line in f:
line_num +=1
if not line:
continue
line=line.strip()
word_list =[word for word in line if word !=' ']
#print(word_list)
state_list=[]
line_list=line.split()
for w in line_list:
state_list.extend(get_marks(w))
#print(state_list)
assert len(word_list) == len(state_list)
#断言,条件为True时正常运行,条件为false时,它先向stderr 打印一条出错信息,然后
通过调用 abort 来终止程序运行
for k,v in enumerate(state_list ): #迭代状态列表,K是索引,v是状态
count_state[v]+=1
if k==0:
self.pi_prob[v]+=1
#每个句子第一个字的状态统计,用于计算初始状态概率
else:
self.tran_state[state_list[k-1]][v]+=1 #计算转移概率
self.emit_prob[state_list[k]][word_list[k]]=self.emit_prob[state_list[k]].get(word_list[k],0)+1.0 #计算发射概率
#print(self.emit_prob)
self.pi_prob={s:self.pi_prob [s]/line_num for s in self.state_set}
print("初始概率:",self.pi_prob)
self.tran_state ={k:{k1:(v1+1)/count_state[k] for k1,v1 in v.items()} for k,v in self.tran_state.items()} #加1平滑
print("转移概率:",self.tran_state)
self.emit_prob ={k:{k1:v1/count_state[k] for k1,v1 in v.items()} for k,v in self.emit_prob.items()}
print("发射概率:", self.emit_prob)
return self
#维特比算法(动态规划求最大路径)根据上述求得的三种概率计算输入文本的分词结果
def viterb(self,text):
path=[]
v=[{}]
si_gama=[{}]
#当t=1时,计算t=1时刻各个状态产生各个观测结果的概率
for state in self.state_set:
si_gama[0][state]=self.pi_prob[state] * self.emit_prob[state].get(text[0],0)
#print(si_gama[0])
#当t>=2时,计算最大概率路径
for t in range(1,len(text)):
# 检验训练的发射概率矩阵中是否有该字
neverSeen = text[t] not in self.emit_prob['S'].keys() and \
text[t] not in self.emit_prob['M'].keys() and \
text[t] not in self.emit_prob['E'].keys() and \
text[t] not in self.emit_prob['B'].keys()
si_gama.append({})
v.append({})
for state in self.state_set:
emit = self.emit_prob[state].get(text[t], 0) if not neverSeen else 1 #设置未知字单独成词
(si_gama[t][state],j)=max([(si_gama[t-1][y0] * self.tran_state[y0].get(state,0) * emit,y0) \
for y0 in self.state_set if (si_gama[t - 1][y0]) > 0])
v[t][state]=j
#根据最优路径概率获取最优路径终点,再逆向寻找其他节点
p=max(si_gama[-1], key=si_gama[-1].get)
path.append(p)
for i in range(len(text) - 1, 0, -1):
p2=v[i].get(p)
p=p2
path.append(p)
path.reverse()
print(path)
return path
#根据序列标注来展示分词结果
def cut_text(self,text,path):
s=''
for i,p in enumerate(path):
if p == 'B' or p=='M':
s+=text[i]
elif p =='E' or p=='S':
s=s+text[i]+'/'+' '
print("原文本:",text)
print("分词结果:",s)
if __name__ =='__main__':
model=HMM()
model.calculate_prob()
text="这是一年里股市的最高纪录"
path=model.viterb( text)
model.cut_text(text, path)