前言
本文将使用隐马尔可夫模型对中文进行分词操作。因为在训练集中,是给定了观测序列和隐藏序列数据,所以采用的将是监督式学习。对隐马尔可夫模型的推导过程感兴趣的,可以看HMM隐马尔可夫模型的数学推导(一)
目的
首先,我们要知道,中文分词,该如何分?
假如我们现在有一句话——“今天天气不错”
我们自然是希望将其分成——“今天 天气 不错”。
在隐马尔可夫模型对其进行分词的过程中,我们需要对每一个词进行编码。最后我们预测出来的结果不会是“今天 天气 不错”。而是“BEBEBE”。这个东西是什么呢?
实际上,对一句话——“今天天气不错”,我们会得到对应的分词编码——“BEBEBE”。这个编码的B是英文的Begin,意为开始。而E对应为End,代表结束。
那这个东西他有什么用呢?如果你仔细数一下里面的长度,你会发现“今天天气不错”长度是6,而对应的编码“BEBEBE”长度也是6。实际上,句子和编码的每一个字符是一一对应的。比如对一个字“今”,对应的编码就是"B"。再比如一个字“天”,对应的编码就是“E”。
为什么会有这样的编码?我们不妨看看上面我们说到的分词结果是“今天 天气 不错”。我们将"今“,”天"组成一个词、"天“,”气"组成一个词、"不“,”错"组成一个词。所以得到了三个词,那么对应的词编码,就是“BE”,“BE” ,“BE”。
再回到我们之前说的,B是begin的意思,E是end的意思。所以对于词“今天”,“今”对应编码“B”代表一个词的开始位置,而“天”对应编码“E”则代表一个词的结束位置。所以对于“今天”这个词,“今”代表词的,“天”代表词的结束。
所以“BE”就是一个词。因为里面包含了词的开始和词的结束。所以对于句子——“今天天气不错”,其可以分为三个词,所以最终的结果是“BEBEBE”。
可是我们不得不去想,如果我们的词是三个字呢?比如“共产党”这个词,其对应的编码是什么呢?以我们前面所提到规律来看,应当是“B?E”,里面的?,我们一般用M(Middle)表示,代表其属于中间词。所以结果是"BME"。那如果是四个词呢?一样的,比如“玛玛哈哈”,对应“BMME”。五个,六个…中间都是M。
考虑了两个字和两个字以上的编码表达,可是如果有单字成词的我们又该如何表达呢?对于单字成词,我们用S(Single)表示,比如“的”,对应编码“S”。
那么如果我们有下面一句话——“中国共产党的党旗”,对应编码当燃是“BEBMESBE”,分词结果为“中国 共产党 的 党旗”。所以为了看着舒服,我们同样也可以将编码用空格隔开,表达为“BE BME S BE”。
隐马尔可夫模型
预测
所谓预测,实际上就是求 P ( Z ∣ X , θ ) P(Z|X,\theta) P(Z∣X,θ)概率最大的 Z Z Z。
比如
我们给定"今天天气不错",然后预测每一个词所对应的编码,然后我们就可以做分词了。比如对于"今"这个字,可选的编码有4个,我们选择其中概率最大的那一个。比如选到了"B",对于"天"这个字又选到了“E"。以此类推每一次选一个概率最大的编码。然后最终把这些概率最大的编码连在一起,就是最终的编码。
然而,每一个字对应概率最大的编码,但是最终的概率也未必最后的概率最大,因为我们计算的并不是简单的概率相加,具体可以看上文的公式推导便知道了,这种方式只能够局部最大,而不能全局最大,要让全局最大,并且算法复杂度较低的算法。一般使用维特比算法。采用动态规划的思想,那究竟算法是如何?在这里本文就不讲了,主要是在电脑上作图实在是太麻烦了。B站上有一个讲的很不错的视频。里面的例子也是来自李航老师的统计机器学习,很通俗易懂维特比算法(上)。
但是,从图中你就会发现,预测算法离不开里面的初始概率,状态转移矩阵,发射矩阵这些参数。那么如何学习到这些参数呢?
参数学习
假如你对隐马尔可夫模型并不了解可以百度去简单了解一下。不然下文你估计看不懂。
对于隐马尔可夫模型,由于观测变量和隐变量都是给定的( { ( X 1 , Z 1 ) , ( X 2 , Z 2 ) , ⋯ } \left\{(X_1,Z_1),(X_2,Z_2),\cdots\right\} {(X1,Z1),(X2,Z2),⋯}),采用的监督式学习,我们直接用极大似然估计直接求解即可。前言部分提供了原理推导里面的参数学习纵然没有监督学习的部分的推导,但是如果能够看得懂推导,又知道极大似然估计。读者自行求解很容易。本文将不作推导,因为监督式学习下是相对容易的。
对于隐马尔可夫模型,我们的参数就三个 θ = { π , A , B } \theta=\left\{\pi,A,B\right\} θ={π,A,B},其中, π \pi π表示初始概率,A代表转移矩阵,B代表发射矩阵。
那么具体这些参数如何计算。如果你使用极大似然估计求解之后会得到。在这里我们不作推导,而是用例子去说明
比如现在我们有一些句子
句子 | 编码 |
---|---|
今天 天气 不错 | BE BE BE |
我 爱 中国 共产党 | S S BE BME |
我们 去 哪里 | BE S BE |
对于马尔可夫模型,编码作为隐状态,所以,对于隐变量z的取值只能够是 B 、 M 、 E 、 S B、M、E、S B、M、E、S。
那么对于马尔可夫模型来说,初始概率状态就是4个。
也就是对于初始概率,有
π \pi π | B | M | E | S |
---|---|---|---|---|
P P P | p 1 p_1 p1 | p 2 p_2 p2 | p 3 p_3 p3 | p 4 p_4 p4 |
那么我们该如何计算 π \pi π的参数呢?在隐马尔可夫模型中,初始概率即为每一个句子的首个编码的个数除以总数,说明意思呢?以上面的句子为例,每一个句子对应的首个编码分别是B,S,B,一共有三个句子,所以实际上的 π \pi π就是
π \pi π | B | M | E | S |
---|---|---|---|---|
P P P | 2 3 \frac{2}{3} 32 | 0 | 0 | 1 3 \frac{1}{3} 31 |
对于状态转移矩阵,因为隐变量z的状态数是4,所以状态转移矩阵就是一个4x4的矩阵,
那他该怎么算呢?所谓状态转移矩阵就是当前时刻t对应隐状态 q i q_i qi转移到t+1时刻隐状态 q j q_j qj的概率。
比如我们上面提到的第一句话——“今天 天气 不错”,对应编码为"BE BE BE"。假如“今”作为第一个时刻的观测状态,那么对应隐状态的编码为B。那么下一个时刻的观测状态为"天",所以对应的隐状态的编码就是E,所以这就是由状态B转移到E。我们计算B转移到E的概率就是计算训练集中由B转移到E的频次,再按行求和,然后用频次求和就可以得到概率了
比如上面的三个句子当中,有6个B下一个就是E的。那么频次就是6
下面的表第一行表示从B分别转移到B、M、E、S的频次,依此类推。
B | M | E | S | |
---|---|---|---|---|
B | 0 | 1 | 6 | 0 |
M | 0 | 0 | 1 | 0 |
E | 3 | 0 | 0 | 1 |
S | 2 | 0 | 0 | 1 |
按行求和,所以对应的状态转移矩阵A就是
B | M | E | S | |
---|---|---|---|---|
B | 0 | 1 7 \frac{1}{7} 71 | 6 7 \frac{6}{7} 76 | 0 |
M | 0 | 0 | 1 | 0 |
E | 3 4 \frac{3}{4} 43 | 0 | 0 | 1 4 \frac{1}{4} 41 |
S | 2 3 \frac{2}{3} 32 | 0 | 0 | 1 3 \frac{1}{3} 31 |
而发射矩阵。就是每一个字在不同隐状态的频次,然后按行求和,接着用频次除以总数即可。
比如,对于“今天”的“天”这个字,其对应编码为E。而在"天气"中的“天”,其编码为"B",而M和S中没有这个字。所以对于这个字的频次
隐状态/字 | 天 |
---|---|
B | 1 |
M | 0 |
E | 1 |
S | 0 |
那么以此类推其他字。在此之前我们要计算一共有多少个非重复的字
隐状态/字 | 今 | 天 | 气 | 不 | 错 | 我 | 爱 | 中 | 国 | 共 | 产 | 党 | 们 | 去 | 哪 | 里 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
B | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
M | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
E | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
S | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
按行求和,所以对应的发射矩阵为
隐状态/字 | 今 | 天 | 气 | 不 | 错 | 我 | 爱 | 中 | 国 | 共 | 产 | 党 | 们 | 去 | 哪 | 里 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
B | 1 7 \frac{1}{7} 71 | 1 7 \frac{1}{7} 71 | 0 | 1 7 \frac{1}{7} 71 | 0 | 1 7 \frac{1}{7} 71 | 0 | 1 7 \frac{1}{7} 71 | 0 | 1 7 \frac{1}{7} 71 | 0 | 0 | 0 | 0 | 1 7 \frac{1}{7} 71 | 0 |
M | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
E | 0 | 1 7 \frac{1}{7} 71 | 1 7 \frac{1}{7} 71 | 0 | 1 7 \frac{1}{7} 71 | 0 | 0 | 0 | 1 7 \frac{1}{7} 71 | 0 | 0 | 1 7 \frac{1}{7} 71 | 1 7 \frac{1}{7} 71 | 0 | 0 | 1 7 \frac{1}{7} 71 |
S | 0 | 0 | 0 | 0 | 0 | 1 3 \frac{1}{3} 31 | 1 3 \frac{1}{3} 31 | 0 | 0 | 0 | 0 | 0 | 0 | 1 3 \frac{1}{3} 31 | 0 | 0 |
代码实现
训练集分为两个文本文件,第一个文本时句子,第二个是编码。其中每一行一一对应。
在代码中,我预测句子——“今天的天气很好”,得到结果——“今天 的 天气 很 好”
注意了,我并没有在代码中设置没有出现在训练集中的字的处理。一般情况下是独立成词,读者可自行尝试
import numpy as np
import pandas as pd
class HMM():
def __init__(self):
pass
def train(self,X,Z):
pi=np.zeros(shape=(4)) #初始化全为0的初始矩阵
A=np.zeros(shape=(4,4)) #初始化全为0的状态转移矩阵
to_num={"B":0,"M":1,"S":2,"E":3} #将状态转化为数字,用作记录
########################################
#记录发射矩阵有多少种可能,用字典记录
self.word_dict={} #初始化字典,同时也用作判定某个字处于发射矩阵的第几列
num=0 #计数
for line in X: #迭代所有行
words_list = list(line.strip().replace(" ", "")) #去掉一行字的所有空格、换行后。转化为列表,一个字作为一个元素
for word in words_list: #迭代每个字
is_exist=self.word_dict.get(word,-1) #寻找是否字典中存在了某个字,如果没有。则返回-1
# 如果字典中没有存在这个字,则将其加入到字典中,并赋给对应值为num。然后num+1
if is_exist==-1:
self.word_dict[word]=num
num+=1
########################################
word_len=num #一共有多少个单词
B=np.zeros(shape=(4,word_len)) #生成4行,word_len列的发射矩阵
for line_z,line_x in zip(Z,X): #迭代每一行观测数据和隐藏数据
z_list=list(line_z.strip().replace(" ","")) #单字转化为列表
x_list=list(line_x.strip().replace(" ","")) #单字转化为列表
z_len=len(z_list) #获取该行隐藏数据的长度
first_z=to_num[z_list[0]] #将行首属于的状态z取出来并转化为数字,用作初始化矩阵
pi[first_z]+=1 #对应位置+1
for index in range(z_len): #迭代一行字和状态
z=z_list[index] #取出当前索引对应的状态
x=x_list[index] #取出当前索引对应的字
line = to_num[z] #将状态转化为数字
x_row=self.word_dict[x] #用于发射矩阵,将对应的字转化为在发射矩阵的第几行
B[line,x_row]+=1 #发射矩阵对应状态,对应字的数量+1
if (index+1)!=z_len: #用于判断是否属于每一行的最后一个字或状态
#不是,则给状态转移矩阵累加,是的话则布雷加
next_z=z_list[index+1] #取出下一个字的状态
row = to_num[next_z] #将其转化为数字
A[line,row]+=1 #状态转移矩阵对应的行和列+1
pi_sum=np.sum(pi) #求和
self.pi=(pi/pi_sum).reshape(-1,1) #归一化,即转化为概率,加和为一,并且重新做成(4,1)矩阵
A_line_sum=np.sum(A,1).reshape(-1,1) #沿列按行求和,重塑为(4,1)矩阵
self.A=A/A_line_sum #归一化,加和为一
B_line_sum=np.sum(B,1).reshape(-1,1) #沿列按行求和,重塑为(4,1)矩阵
self.B=(B/B_line_sum)*100 #归一化,加和为一。但在此基础上*100。是为了防止后面概率计算导致概率越来越小从而导致下溢出。
def predict(self,text):
x=[ self.word_dict[i] for i in list(text)] #将一行字转化为单字乘元素的别表。并将所有字转化为发射矩阵所在的列
x_len=len(x) #获取一行的长度
self.δ=np.zeros(shape=(4,x_len)) #用于计算δ值。因为要用到维特比算法,维度(4,x_len)
self.δ[:, 0:1] = self.pi*(self.B[:,x[0]].reshape(-1,1)) #初始化δ第一列为初始化矩阵点乘乘第一个字的发射概率
self.ψ=np.zeros(shape=(4,x_len)) #用于记录路径
self.ψ[:,0]=range(4) #第一列数据记为(0,1,2,3)
num=1 #计数,从1开始
for t in range(x_len-1): #迭代所有字
#用上一个δ值与状态转移矩阵乘法(不是叉乘)。(4,1)与(4,4),发射矩阵第一行与δ值的第一行所有值都乘
#第一列即为第t个时刻,前一刻所有状态到当前时刻第一个状态概率。以此类推
δ_mul_z=self.δ[:,num-1].reshape(-1,1)*self.A
#按列求最大值
max_index=np.argmax(δ_mul_z,0)
path=self.ψ[max_index,:num] #求出前一个时刻的对应路径
new_path=np.insert(path,path.shape[1],range(4),axis=1) #在末尾出加上当前路径
self.ψ[:,:(num+1)]=new_path #把得到的路径赋给用于记录的矩阵
δ_mul_and_x=δ_mul_z[max_index,[range(4)]] #找出t时刻对应每个状态最大位置所对应的最大值
next_δ=δ_mul_and_x*self.B[:,x[num]] #将其乘以对应的发射矩阵对应字的概率,得到当前的δ值
self.δ[:,num:num+1]=next_δ.T #将新的δ值赋给用于记录的矩阵
num+=1 #累加
max_option=np.argmax(self.δ[:,num-1]) #取出最后一列δ值对应的并求最大,最大的那一行就是我们要选的路径
all_option=self.ψ[max_option,:] #取出对应路径
# 将数字转化回状态
to_num={0:"B",1:"M",2:"S",3:"E"}
result=[ to_num[i] for i in all_option]
text=list(text) #将一行字转化为列表
######将其按照状态拆分
num=0 #
for index in range(len(result)):
if result[index]=="E" or result[index]=="S":
text.insert(num+index+1," ")
num+=1
return ("".join(text).strip())
if __name__ == '__main__':
with open("text.txt","r",encoding="utf-8") as f: #读取矩句子文件
X=f.readlines()
with open("state.txt",encoding="utf-8") as f: #读取编码文件
Z=f.readlines()
hmm=HMM() #初始化
hmm.train(X,Z) #训练
predict_str="今天的天气很好"
result=hmm.predict(predict_str) #预测
print(result) #打印结果
结束
这就是HMM隐马尔可夫模型在中文分词中的应用了,实际上,对于目前基于深度学习的分词,HMM的效果实在是有待提高,但无论如何,我们也应当对其有一个了解。如有问题,还请指出。阿里嘎多。