隐马尔可夫模型HMM实战命名实体识别NER

一个例子

以实体识别为例,
假设我们的标签集合是
labels={B-PER,I-PER,O,B-LOC,I-LOC,B-ORG,I-ORG}
我们的词汇空间有:
vocabs={所有汉字}

假如其中每一个汉字被标记为某一个tag的概率已经知道,例如 p ( ’乔’|B-PER ) = 0.02 p(\text{'乔'|B-PER})=0.02 p(’|B-PER)=0.02,同时我们也已经知道各个tag出现的概率和相互转移的概率,例如 p ( B − P E R ) = 0.31 , P ( I − P E R ∣ B − P E R ) = 0.85 p(B-PER)=0.31,P(I-PER|B-PER)=0.85 p(BPER)=0.31,P(IPERBPER)=0.85

我们怎么生成一段话呢?

  1. 根据语法规则,我们先生成一段标签序列,假如是:[B-PER,I-PER,O,B-ORG,I-ORG,O]
  2. 然后根据上面的标签序列,从每一个标签对应的词汇空间中找单词。假设有N的单词,一共有 N 6 N^6 N6种可能。

假设 p ( 乔|B-PER ) = 0.02 p(\text{乔|B-PER})=0.02 p(|B-PER)=0.02 p ( 丹|I-PER ) = 0.11 p(\text{丹|I-PER})=0.11 p(|I-PER)=0.11 p ( 是|O ) = 0.07 p(\text{是|O})=0.07 p(|O)=0.07 p ( 美|B-ORG ) = 0.02 p(\text{美|B-ORG})=0.02 p(|B-ORG)=0.02 p ( 国|I-ORG ) = 0.021 p(\text{国|I-ORG})=0.021 p(|I-ORG)=0.021 p ( 人|O ) = 0.007 p(\text{人|O})=0.007 p(|O)=0.007,那么根据上面的标签序列生成乔丹是美国人的概率自然是 0.02 ∗ 0.11 ∗ 0.07 ∗ 0.02 ∗ 0.021 ∗ 0.007 0.02*0.11*0.07*0.02*0.021*0.007 0.020.110.070.020.0210.007。我们假设这个概率值是 N 6 N^6 N6种可能里最大的,那么我们就可以生成这个句子了。

我们上面其实是在求解 p ( X ∣ Y ) p(X|Y) p(XY)。上面的过程其实就是对应HMM的概率计算问题。

至于怎么求解 p ( Y ) p(Y) p(Y)呢( p ( Y ) = p ( y 0 ) p ( y 1 ∣ y 0 ) p ( y 2 ∣ y 1 , y 0 ) ⋯ p(Y)=p(y_0)p(y_1|y_0)p(y_2|y_1,y_0)\cdots p(Y)=p(y0)p(y1y0)p(y2y1,y0)),很简单,统计每一个标签出现的频率以及各个标签之间相互转移的频率即可。

那么我们就可以计算 p ( Y ∣ X ) p(Y|X) p(YX)了,因为 p ( Y ∣ X ) ∝ p ( X ∣ Y ) ∗ p ( Y ) p(Y|X)\propto p(X|Y)*p(Y) p(YX)p(XY)p(Y)。而这正对应HMM的预测问题。

当然想要求解概率计算问题和预测问题的前提是把各个概率值估计出来。即: p ( ‘乔’|B-PER ) p(\text{‘乔’|B-PER}) p(’|B-PER)等于多少呢, p ( I − P E R ∣ B − P E R ) p(I-PER|B-PER) p(IPERBPER)是多少呢,这个要从数据中统计。而这个问题对应的是HMM的学习问题。

HMM

HMM是对含有隐状态的马尔可夫链建模的生成模型。
这句话有三个概念:

  • 隐状态
  • 马尔可夫链
  • 生成模型

基本概念

马尔可夫链

马尔可夫链是满足马尔可夫性质的一条状态转移过程。这条链上每一个节点是一个状态,整个链是一个时序的,每一个状态之间是相互转移的。状态之间的转移满足马尔可夫性质。

马尔可夫性质

马尔可夫性质指的是某一时刻的状态只和相邻的状态有关,即t时刻的状态仅仅与t-1和t+1时刻的状态有关

隐状态

这条链上含有隐状态是指这条链上的状态不可见,观测不到。

生成模型

我们的目的对于给定的X直接求出对应的Y,即求出来 p ( Y ∣ X ) p(Y|X) p(YX)。而HMM的步骤是,先根据Y,生成X,也就是先得到 p ( X ∣ Y ) p(X|Y) p(XY),然后统计每一个Y出现的概率。最后利用 p ( Y ∣ X ) ∝ P ( X ∣ Y ) ∗ P ( Y ) p(Y|X)\propto P(X|Y)*P(Y) p(YX)P(XY)P(Y)得到 p ( Y ∣ X ) p(Y|X) p(YX),也就是说整个过程实际上是在求 P ( X ∣ Y ) ∗ P ( Y ) P(X|Y)*P(Y) P(XY)P(Y),也就是 P ( X , Y ) P(X,Y) P(X,Y)。所以说它是生成模型。

三个问题、两个假设

HMM是对含有隐状态的马尔可夫链建模的生成模型。这句话中我们已经知道,

  1. HMM是先根据数据中的Y生成对应的X
  2. HMM一定是含有隐状态的马尔可夫链

针对上述两个点,HMM还分别做了两个基本假设
针对第一个点:

观测独立假设

注意这里面Y是隐藏的不可观测的隐变量,X是可观测的变量。HMM是根据隐变量生成观测变量的模型,同时做了一个观测独立假设,即:当前时刻的观测 x t x_t xt仅仅和当前时刻的隐状态 y t y_t yt相关,即:
P ( X ∣ Y ) = P ( x 1 , x 2 , ⋯   , x n ∣ y 1 , y 2 , ⋯   , y n ) = ∏ t = 1 n P ( x t ∣ y t ) P(X|Y)=P(x_1,x_2,\cdots,x_n|y_1,y_2,\cdots,y_n)=\prod_{t=1}^{n}P(x_t|y_t) P(XY)=P(x1,x2,,xny1,y2,,yn)=t=1nP(xtyt)
举个例子就是当前时刻假如是名词,那么当前时刻可能出现的单词与前一时刻的单词是什么词性一点关系没有。

针对第二个点:

齐次马尔可夫假设

我们知道:马尔可夫链是满足马尔可夫性质的状态转移过程,即t时刻的状态仅仅和相邻的t-1,t+1两个时刻相关。齐次马尔可夫性质是指t时刻的状态仅仅之和前一时刻t-1时刻的状态相关。所以我们可以看出,此时的马尔可夫链是单向的,即从左向右的。这也是为什么HMM是有向图模型。
p ( y t ∣ y 1 , y 2 , ⋯   , y t − 1 ) = p ( y y ∣ y t − 1 ) p(y_t|y_1,y_2,\cdots,y_{t-1})=p(y_y|y_{t-1}) p(yty1,y2,,yt1)=p(yyyt1)

三个问题

根据最开始的例子,相信对这三个问题有了初步的了解。我们首先要了解HMM需要哪些参数:

  1. 初始概率矩阵 π \pi π,这个矩阵的长度和标签集合是一样的。记录的是每一个标签作为句子的第一个单词出现的频率。所以叫初始时刻的概率矩阵。(这个只需要统计数据集每一个句子的第一个单词对应的标签是什么即可)
  2. 状态转移概率矩阵 A A A,在上面的例子中有7个标签,这个矩阵是形如7*7的矩阵。 A i j A_{ij} Aij代表,上一时刻是状态 i i i,转移到下一时刻状态 j j j的频率。(我们需要统计的是训练集中每一个句子的每两个单词对应的标签之间的转换频率)
  3. 发射矩阵(也叫观测矩阵) B B B。假如有1000个单词,那么这个矩阵的形状是7*1000。第一行代表每一个单词被标记为B-PER的概率,例如B[0][444]=0.24就代表第445个单词可以作为B-PER出现的概率。(关于这个矩阵,我们需要统计的自然是训练集中每一个句子的每一个单词被标记为每一个tag的次数)。

问题1:学习问题(参数估计)

在NER中,由于是有监督数据,我们使用极大似然估计进行参数估计,也就是频率代替概率。
我们来看代码:

state_nums=7#状态数目
vocab_size=1000
pi=np.zeros(state_nums)
A=np.zeros((state_nums,state_nums))
B=np.zeros((state_nums,vocab_size))
#我们假设train_sentences记录了训练数据集中的所有的句子
#train_labels记录了对应的每一个句子的每一个单词的标记
#eg:train_sentences[0]=['海', '钓', '比', '赛', '地', '点', '在', '厦', '门', '与', '金', '门', '之', '间', '的', '海', '域', '。']
#train_labels[0]=['O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O', 'B-LOC', 'I-LOC', 'O', 'O', 'O', 'O', 'O', 'O']
#再假设tag2id记录了每一个标签与对应的id之间的转换
#word2id记录了每一个单词与对应的id之间的转换
for sentence,sentence_label in zip(train_sentences,train_sentences_label):
	sentence_length=len(sentence)
	for step in range(sentence_length-1):
		word=sentence[step]#观测变量
		label=sentence_label[step]#隐状态
		if step==0:
			#统计初始时刻每一个tag出现的次数
			pi[tag2id[label]]+=1
		next_label=sentence_label[step+1]
		A[tag2id[label]][tag2id[next_label]]+=1#当前时刻由状态i转移到下一时刻状态j的次数
		B[tag2id[label]][word2id[word]]+=1#当前时刻在状态i观测到变量word的频数
	#step现在指向最后一个时刻
	B[tag2id[sentence_label[step]]][word2id[sentence[step]]]+=1

pi/=np.sum(pi)#频数变频率
A/=np.sum(A,axis=1,keepdims=True)
B/=np.sum(B,axis=1,keepdims=True)

问题2:概率计算问题(求一条观测序列的概率)

假设我们估计了三个参数,怎么评估一句话出现的概率呢?
即求 p ( X ) p(X) p(X)的概率,显然根据概率计算公式
p ( X ) = ∑ Y p ( X , Y ) = ∑ Y p ( X ∣ Y ) p ( Y ) p(X)=\sum_{Y}p(X,Y)=\sum_{Y}p(X|Y)p(Y) p(X)=Yp(X,Y)=Yp(XY)p(Y)

例如:乔丹是美国人

这句话的概率怎么计算?

直接计算

很显然,这个句子长度是6,那么我们穷举所有的长度是6的可能状态序列,
即:

  • B-PER,I-PER,0,B-ORG,I-ORG,O
  • B-PER,I-PER,0,B-ORG,I-ORG,B-ORG
  • B-PER,I-PER,0,B-ORG,I-ORG,I-ORG
  • O,I-PER,0,B-ORG,I-ORG,O

一共有 7 6 7^6 76中可能。以上我们求的是 P ( Y ) P(Y) P(Y)
然后对每一种可能,都会求出来在这种可能下得到乔丹是美国人的概率,即 p ( X ∣ Y ) p(X|Y) p(XY),例如

  • 第一种可能对应的概率值是: p ( 乔|B-PER ) ∗ p ( 丹|I-PER ) ∗ p ( 是|O ) ∗ p ( 美|B-ORG ) ∗ p ( 国|I-ORG ) ∗ p ( 人|O ) p(\text{乔|B-PER})*p(\text{丹|I-PER})*p(\text{是|O})*p(\text{美|B-ORG})*p(\text{国|I-ORG})*p(\text{人|O}) p(|B-PER)p(|I-PER)p(|O)p(|B-ORG)p(|I-ORG)p(|O)
  • 第二种可能对应的概率值是: p ( 乔|B-PER ) ∗ p ( 丹|I-PER ) ∗ p ( 是|O ) ∗ p ( 美|B-ORG ) ∗ p ( 国|I-ORG ) ∗ p ( 人|B-ORG ) p(\text{乔|B-PER})*p(\text{丹|I-PER})*p(\text{是|O})*p(\text{美|B-ORG})*p(\text{国|I-ORG})*p(\text{人|B-ORG}) p(|B-PER)p(|I-PER)p(|O)p(|B-ORG)p(|I-ORG)p(|B-ORG)
  • 第三种可能对应的概率值是: p ( 乔|B-PER ) ∗ p ( 丹|I-PER ) ∗ p ( 是|O ) ∗ p ( 美|B-ORG ) ∗ p ( 国|I-ORG ) ∗ p ( 人|I-ORG ) p(\text{乔|B-PER})*p(\text{丹|I-PER})*p(\text{是|O})*p(\text{美|B-ORG})*p(\text{国|I-ORG})*p(\text{人|I-ORG}) p(|B-PER)p(|I-PER)p(|O)p(|B-ORG)p(|I-ORG)p(|I-ORG)
  • 第四种可能对应的概率值是: p ( 乔|O ) ∗ p ( 丹|I-PER ) ∗ p ( 是|O ) ∗ p ( 美|B-ORG ) ∗ p ( 国|I-ORG ) ∗ p ( 人|B-ORG ) p(\text{乔|O})*p(\text{丹|I-PER})*p(\text{是|O})*p(\text{美|B-ORG})*p(\text{国|I-ORG})*p(\text{人|B-ORG}) p(|O)p(|I-PER)p(|O)p(|B-ORG)p(|I-ORG)p(|B-ORG)
  • 。。。

将这 7 6 7^6 76种可能的概率值相加作为这句话出现的概率。
每一种又要计算6次,所以时间复杂度是 6 ∗ 7 6 6*7^6 676

前向算法

定义t时刻处于状态i并且已经观测到序列 o 1 , o 2 , ⋯   , o t o_1,o_2,\cdots,o_t o1,o2,,ot的概率为
α t ( i ) = P ( o 1 , o 2 , ⋯   , o t , i t = q i ) \alpha_t(i)=P(o_1,o_2,\cdots,o_t,i_t=q_i) αt(i)=P(o1,o2,,ot,it=qi)
这就是前向概率的定义。
那么:
α T ( i ) = P ( o 1 , o 2 , ⋯   , o T , i T = q i ) \alpha_T(i)=P(o_1,o_2,\cdots,o_T,i_T=q_i) αT(i)=P(o1,o2,,oT,iT=qi)
我们的目的就是:
P ( O ) = ∑ i = 1 N α T ( i ) P(O)=\sum_{i=1}^{N}\alpha_T(i) P(O)=i=1NαT(i)
也就是将最后一个时刻所有的可能的状态的前向概率求和。作为观测到整个句子的概率。

那么怎么计算每一个时刻所有状态的前向概率呢?
我们来推导递推公式:
α ( t + 1 ) ( j ) = ∑ i = 1 N P ( o 1 , o 2 , ⋯   , o t , o t + 1 , i t = q i , i t + 1 = q j ) \alpha_{(t+1)}(j)=\sum_{i=1}^{N}P(o_1,o_2,\cdots,o_t,o_{t+1},i_t=q_i,i_{t+1}=q_j) α(t+1)(j)=i=1NP(o1,o2,,ot,ot+1,it=qi,it+1=qj)
我们目的是推导出t时刻处于状态i和t+1时刻处于状态j之间前向概率的递推公式。
α ( t + 1 ) ( j ) = ∑ i = 1 N P ( o t + 1 ∣ o 1 , o 2 , ⋯   , o t , i t = q i , i t + 1 = q j ) ∗ P ( o 1 , o 2 , ⋯   , o t , i t = q i , i t + 1 = q j ) \alpha_{(t+1)}(j)=\sum_{i=1}^{N}P(o_{t+1}|o_1,o_2,\cdots,o_t,i_t=q_i,i_{t+1}=q_j)*P(o_1,o_2,\cdots,o_t,i_t=q_i,i_{t+1}=q_j) α(t+1)(j)=i=1NP(ot+1o1,o2,,ot,it=qi,it+1=qj)P(o1,o2,,ot,it=qi,it+1=qj)
根据观测独立假设:某一时刻的观测值仅仅取决于这一时刻的隐状态
,那么上式等于
α ( t + 1 ) ( j ) = ∑ i = 1 N P ( o t + 1 ∣ i t + 1 = q j ) ∗ P ( o 1 , o 2 , ⋯   , o t , i t = q i , i t + 1 = q j ) \alpha_{(t+1)}(j)=\sum_{i=1}^{N}P(o_{t+1}|i_{t+1}=q_j)*P(o_1,o_2,\cdots,o_t,i_t=q_i,i_{t+1}=q_j) α(t+1)(j)=i=1NP(ot+1it+1=qj)P(o1,o2,,ot,it=qi,it+1=qj)
进一步
α ( t + 1 ) ( j ) = ∑ i = 1 N P ( o t + 1 ∣ i t + 1 = q j ) ∗ P ( i t + 1 = q j ∣ o 1 , o 2 , ⋯   , o t , i t = q i ) ∗ P ( o 1 , o 2 , ⋯   , o t , i t = q i ) \alpha_{(t+1)}(j)=\sum_{i=1}^{N}P(o_{t+1}|i_{t+1}=q_j)*P(i_{t+1}=q_j|o_1,o_2,\cdots,o_t,i_t=q_i)*P(o_1,o_2,\cdots,o_t,i_t=q_i) α(t+1)(j)=i=1NP(ot+1it+1=qj)P(it+1=qjo1,o2,,ot,it=qi)P(o1,o2,,ot,it=qi)
根据齐次马尔可夫假设:当前时刻的隐状态仅仅取决于前一时刻的隐状态
α ( t + 1 ) ( j ) = ∑ i = 1 N P ( o t + 1 ∣ i t + 1 = q j ) ∗ P ( i t + 1 = q j ∣ i t = q i ) ∗ α t ( i ) \alpha_{(t+1)}(j)=\sum_{i=1}^{N}P(o_{t+1}|i_{t+1}=q_j)*P(i_{t+1}=q_j|i_t=q_i)*\alpha_t(i) α(t+1)(j)=i=1NP(ot+1it+1=qj)P(it+1=qjit=qi)αt(i)
最终的递推公式:
α t + 1 ( j ) = ∑ i = 1 N [ α t ( i ) ∗ a i j ] ∗ b j ( o t + 1 ) \alpha_{t+1}(j)=\sum_{i=1}^{N}[\alpha_t(i)*a_{ij}]*b_j(o_{t+1}) αt+1(j)=i=1N[αt(i)aij]bj(ot+1)
其中初始时刻的前向概率利用初始时刻概率矩阵乘以初始时刻的发射概率矩阵 α 1 ( i ) = π i ∗ b i ( o 1 ) \alpha_1(i)=\pi_i*b_i(o_1) α1(i)=πibi(o1)。然后依次递推,每一步都会求出N种状态各自的前向概率。直到最后一步,每一步会利用上一时刻计算好的前向概率乘以相应的转移概率矩阵。
时间复杂度显然是 N 2 ∗ T N^2*T N2T.

text="乔丹是美国人"
T=len(text)
alpha_matrix=np.zeros((state_nums,T))
o_1=text[0]
for i in range(state_nums):
    alpha_matrix[i][0]=pi[i]*B[i][word2id[o_1]]

for t in range(1,T):
    o_t=text[t]
    for i in range(state_nums):
        temp=0.0
        for j in range(state_nums):
            temp+=alpha_matrix[i][t-1]*A[i][j]#上一时刻所有可能的状态转移到下一时刻所有可能的状态
        alpha_matrix[i][t]=temp*B[j][word2id[o_t]]
    #显然时间复杂度是N的平方乘以句子长度
prob=sum([alpha_matrix[i][-1] for i in range(state_nums)])
#最后时刻所有可能的状态的前向概率的和就是生成这个句子的概率

问题3:预测问题(求一条状态序列的概率)

最直接的方法当然是对于给定的X,穷举所有可能对应的Y。例如,
乔丹是美国人
这句话,我们对于”乔“这个字有7种可能的结果,对于”丹“这个字,有7种可能。那么”乔丹“这个单词的标注可能就有 7 2 7^2 72个,那么最终会有 7 6 7^6 76种可能的标注结果,而每一种标注序列都要计算对应的概率,然后求这么多种可能最大的概率值。时间复杂度是 6 ∗ 7 6 6*7^6 676

viterbi算法

其实从这里可以看出,在概率计算问题上就遇到了时间复杂度太大的问题,所以才有了前向算法。将时间复杂度降到了 T ∗ N 2 T*N^2 TN2
Viterbi算法也是同样的思想。对于t时刻所有可能的状态,记录到达这个状态所有路径的最大概率的那条路径。递推下去,每一时刻都记录当前时刻所有状态对应的最大概率的路径,每一步的时间复杂度是 N 2 N^2 N2,最后时刻所有可能的状态中具有最大概率值的那个状态作为最终的预测状态,那个状态对应的路径就是最优路径。时间复杂度是 N 2 ∗ T N^2*T N2T

我们可以看出viterbi算法和前向计算是类似地,每一步都会算出来 N 2 N^2 N2种可能的路径。只不过在前向计算中,要保留这 N 2 N^2 N2种可能的路径为下一时刻计算。而在viterbi算法中,我们保留 N N N条路径,这N条路径分别是到达每一个状态对应的概率最大的路径。在最后一个时刻,前向计算是将N种状态的概率值相加,而viterbi算法是将具有最大概率值的状态作为最终的状态。

举个例子,假如我们有一个句子:乔丹打球。
简化起见,有三个可能的隐藏状态
当我们从乔转移到丹时,我们会计算9种可能的路径,因为第二个时刻的三个状态来源于第一个时刻任意的状态。不过我们只会记录到达第二个时刻每一个状态对应的最大概率的路径。如下图,第一个红线代表第二个时刻丹这个字如果被标记为B-PER,那么最有可能的路径是(O,B-PER)
隐马尔可夫模型HMM实战命名实体识别NER_第1张图片
以此类推,我们只会保留最大概率的路径
隐马尔可夫模型HMM实战命名实体识别NER_第2张图片

这里要注意,viterbi只会考虑最大概率的路径,这也是为什么第一个时刻转移到第二时刻现在只有3条路径。同理:
隐马尔可夫模型HMM实战命名实体识别NER_第3张图片
每一步算9条路径的概率,保留三条路径

现在我们已经算到最终时刻了,假设P(O)>P(B-PER)>P(I-PER)
隐马尔可夫模型HMM实战命名实体识别NER_第4张图片

那么最终时刻的预测标注就是O,对应的最优路径回溯即可,即:(B-PER,I-PER,O,O)。

于是我们可以知道viterbi的递推公式应该是:
α t + 1 ( j ) = max ⁡ 1 ≤ i ≤ N [ α t ( i ) ∗ a i j ] ∗ b j ( o t + 1 ) \alpha_{t+1}(j)=\underset{1\leq i\leq N}{\max}[\alpha_t(i)*a_{ij}]*b_{j}(o_{t+1}) αt+1(j)=1iNmax[αt(i)aij]bj(ot+1)
初始时刻仍然是 α 1 ( i ) = π i ∗ b i ( o 1 ) \alpha_1(i)=\pi_i*b_i(o_1) α1(i)=πibi(o1)
和前向公式的不同就是一个是sum一个是max
此外我们还需要一个额外的变量用来保存每一时间步所有状态对应的路径,也就是这个状态是从前一时刻哪个状态转移过来的。以便最后回溯。

我们来看代码:

text="乔丹是美国人"
T=len(text)
delta_matrix=np.zeros((state_nums,T))#用来计算t时刻转移到t+1时刻每一个路径的概率
gamma_matrix=np.zeros((state_nums,T))#用来保存这所有的路劲中最大概率的那个路径对应的是哪个状态
for i in range(state_nums):
	delta_matrix[i][0]=pi[i]*B[i][word2id[text[0]]]

for t in range(1,T):
	#对于t时刻的每一个状态
	for i in range(state_nums):
		temp=[]#用来临时记录这N个路径的概率
		#j用来遍历上一时刻所有的状态
		for j in range(state_nums):
			temp.append(delta_matrix[j][t-1]*A[j][i])
		max_prob=max(temp)
		alpha_matrix[i][t]=max_prob
		gamma_matrix[i][t]=temp.index(max_prob)
		#temp记录是t-1时刻所有的状态中哪一个状态转移到t时刻的i状态的概率最大

#delta_matrix的最后一列的最大值就是最优路径的概率,对应的状态就是最后一个时刻预测的状态
temp=delta_matrix[:,-1]
temp=temp.tolist()
optimal_path_prob=max(temp)

optimal_path=[]
optimal_path.append(temp.index(optimal_path_prob))#最后一个时刻的状态
for t in range(T-1,0,-1):
	predict_state=optimal_path[-1]
	optimal_path.append(gamma_matrix[predict_state][t])

optimal_path.reverse()#这就是最优路径

完整代码

中文NER数据

加载包

train_txt="./train.txt"
import numpy as np
np.set_printoptions(precision=3, suppress=True, linewidth=120)
import json,os,tqdm
from collections import Counter
from pprint import pprint

数据预处理

def get_data(txt_file):
    with open(txt_file) as f:
        lines=f.readlines()
    sentences=[]
    sentences_label=[]
    
    sentence=[]
    sentence_label=[]
    for line in lines:
        if line=='\n':
            sentences.append(sentence)
            sentences_label.append(sentence_label)
            sentence=[]
            sentence_label=[]
        line_split=line.strip().split()
        if len(line_split)!=2:
            #print(line)
            continue
        
        assert len(line_split)==2
        word,label=line_split
        sentence.append(word)
        sentence_label.append(label)
    return [sentences,sentences_label]
def print_data(sentences,sentences_label):
    for sentence,sentence_label in zip(sentences,sentences_label):
        for word,label in zip(sentence,sentence_label):
            print(word,label)
        print('-'*50)

train_data=get_data(train_txt)
train_sentences,train_sentences_label=train_data
print_data(train_sentences[5:10],train_sentences_label[5:10])

构造必要的word2id和label2id

def get_states(sentences_label):
    states=set()
    for sentence_label in sentences_label:
        for label in sentence_label:
            states.add(label)
    states=list(states)
    return states

def get_word2id(sentences):
    all_words=[]
    for sentence in sentences:
        for word in sentence:
            all_words.append(word)
    
    counter_words=Counter(all_words)#统计每一个数字的词频
    sorted_word=sorted(counter_words.items(),key=lambda x:x[1],reverse=True)#按照词频降序排列
    word2id={
     }
    for word,freq in sorted_word:
        word2id[word]=len(word2id)
    word2id['']=len(word2id)
    return word2id

states=get_states(train_sentences_label)
label2id={
     }
for id_ in range(len(states)):
    label2id[states[id_]]=id_
print(states)
print(label2id)

state_nums=len(states)
word2id=get_word2id(train_sentences)
observation_nums=len(word2id)

构造HMM的三个参数矩阵

def init_parameter(train_sentences,train_sentences_label,label2id,word2id):
    '''
    由于是监督学习问题,所以可以用极大似然估计通过计数的方法来初始化HMM的三个参数
    pi是初始状态概率矩阵,它的每一个值代表的是对应的标签作为句子的第一个单词的标签出现的频率
    A是状态概率转移矩阵,当前时刻的第i个标签转移到下一时刻各个标签的概率
    B是观测矩阵,在第i个标签下观测到各个单词的频率
    '''
    A=np.zeros((state_nums,state_nums))
    pi=np.zeros(state_nums)
    B=np.zeros((state_nums,observation_nums))#B=[{'B-LOC':{'中':0.0014,'华':0.000007,..}},{'I-ORG':{'中':0.0022,'华':0.0019,...}},......{
     {'I-LOC':{}}}]

    for sentence,sentence_label in zip(train_sentences,train_sentences_label):
        sentence_length=len(sentence)
        assert sentence_length==len(sentence_label)
        for step in range(sentence_length-1):
            #不包括最后一个时间步
            observation=sentence[step]#观测
            state=sentence_label[step]#隐状态
            next_state=sentence_label[step+1]#下一时刻的状态,因为我们要计算A,这也是为什么不包含最后一个时刻,因为最后时刻没有下一时刻
            if step==0:
                #初始时刻
                pi[label2id[state]]+=1#当前的状态作为初始状态,累计频次
            A[label2id[state]][label2id[next_state]]+=1
            B[label2id[state]][word2id[observation]]+=1
            #以上就是在累计我们需要的频次,都有哪些频次呢
            #第一个是每一个状态作为初始时刻频次
            #第二个是两个状态之间转移的频次
            #第三个是在某个状态下观测的各个单词出现的频次
        #step现在停留在这个序列的最后一个时间步,然而最后一个时间步的观测值还没有统计频次
        B[label2id[sentence_label[step]]][word2id[sentence[step]]]+=1
    print(A)
    print(pi)
    print(label2id)
    pi/=np.sum(pi)
    A/=np.sum(A,axis=1,keepdims=True)
    B/=np.sum(B,axis=1,keepdims=True)
    #既然pi,A,B都是概率矩阵,那么就必须要归一化,A和B的每一行相加是1
    print("初始化HMM的三个参数矩阵完毕")
    return pi,A,B

pi,A,B=init_parameter(train_sentences,train_sentences_label,label2id,word2id)

隐马尔可夫模型HMM实战命名实体识别NER_第5张图片

利用viterbi算法解码

def viterbi_decode(observation_sequence):
    T=len(observation_sequence)
    delta_matrix=np.zeros((state_nums,T))
    gamma_matrix=np.zeros((state_nums,T))
    
    for i in range(state_nums):
        observation=word2id.get(observation_sequence[0],word2id[''])#防止测试集中出现未见过的单词
        delta_matrix[i][0]=pi[i]*B[i][observation]
    
    for t in range(1,T):
        observation=word2id.get(observation_sequence[t],word2id[''])#防止测试集中出现未见过的单词
        for i in range(state_nums):
            #for i in range(state_nums)代表的是当前时刻所有可能的状态
            temp=[delta_matrix[j][t-1]*A[j][i] for j in range(state_nums)]#for j in range(state_nums)
            #代表的是上一个时刻的所有可能的状态转移到当前时刻的第i个状态
            delta_matrix[i][t]=max(temp)*B[i][observation]#按照公式,我们要取最大的值
            gamma_matrix[i][t]=temp.index(max(temp))#记录最大的值对应的位置
    
    #delta_matrix的最后一列的最大值就是最优路径的概率值
    final_path_prob=A[:,-1].tolist()
    optimal_path_prob=max(final_path_prob)
    result_path=[final_path_prob.index(optimal_path_prob)]
    #print("最优路径的概率值 : ",optimal_path_prob)
    #根据gamma_matrix回溯
    
    for t in range(T-1,0,-1):
        result_path.append(int(gamma_matrix[result_path[-1]][t]))
        
    result_path.reverse()
    id2label={
     id_:label for label,id_ in label2id.items()}
    result=[id2label[id_] for id_ in result_path]
    return result

验证效果

sequences=[list('长江和黄河是中国的'),list("我在北京人民大会堂"),list("金庸,古龙,梁羽生是武侠小说的作家")]

for sequence in sequences:
    result=viterbi_decode(sequence)
    for word,predict_tag in zip(sequence,result):
        print(word,predict_tag)
    print('-'*50)

隐马尔可夫模型HMM实战命名实体识别NER_第6张图片

在测试集上测试

加载测试数据

test_txt="./test.txt"
test_data=get_data(test_txt)
test_sentences,test_sentences_labels=test_data

record_all_tags_result={
     }
for each_tag in label2id.keys():
    record_all_tags_result[each_tag]={
     'golden_nums':0,'pred_nums':0,'correct_pred_nums':0}
    
print(record_all_tags_result)

隐马尔可夫模型HMM实战命名实体识别NER_第7张图片

以B-PER为例,首先统计测试数据集中有多少个B-PER,记为golden_bper_nums,然后统计预测出多少个B-PER,记为pred_bper_nums
然后计算出预测的B-PER有多少是正确的,记为tp
那么针对B-PER来讲,precision=tp/pred_bper_nums,recall=tp/golden_bper_nums,f1自然出来了

记录每一个标签在测试集中出现的次数、预测出这个标签的次数

for index in range(len(test_sentences)):
    sentence=test_sentences[index]
    sentence_label=test_sentences_labels[index]
    predict=viterbi_decode(sentence)
    assert len(predict)==len(sentence_label)
    
    for predict_token,golden_token in zip(predict,sentence_label):
        record_all_tags_result[golden_token]['golden_nums']+=1
        record_all_tags_result[predict_token]['pred_nums']+=1
        if predict_token==golden_token:
            record_all_tags_result[predict_token]['correct_pred_nums']+=1

隐马尔可夫模型HMM实战命名实体识别NER_第8张图片

那么针对B-LOC标签来说,它的精确率是2505/16611=0.1508,召回率是2505/3658=0.6848,f1=2*0.1508*0.6848/(0.1508+0.6848)=0.2471

这个标签的各个指标都很低,其它标签的还可以。

你可能感兴趣的:(NLP,python,机器学习,人工智能,深度学习,算法)