【NLP】viterbi 算法图文全解析——词性标注案例分析

【NLP】viterbi 算法图文全解析——词性标注案例分析

案例:输入英文句子,返回对应的词性。
for example:

 sentence= "I like sport ."
 output=['PRP','VBP','NN','.']

这也是一个 Noise Channel Model 的应用,若句子用sentence表示,词性序列用pos表示则,概率公式如下
p ( p o s ∣ s e n t e n c e ) = p ( s e n t e n e ∣ p o s ) ⋅ p ( p o s ) p(pos|sentence)=p(sentene|pos)\cdot p(pos) p(possentence)=p(sentenepos)p(pos)

我们要求的使得p最大的词性序列。
将句子分解为单个单词,w1,w2…wT
将词性序列分解为单个,z1,z2…zT
则有:
p ( p o s ∣ s e n t e n c e ) = p ( w 1 , w 2 . . . . w T ∣ z 1 , z 2 . . . z T ) ⋅ p ( z 1 , z 2 . . . z T ) p(pos|sentence)=p(w_1,w_2....w_T|z_1,z_2...z_T)\cdot p(z_1,z_2...z_T) p(possentence)=p(w1,w2....wTz1,z2...zT)p(z1,z2...zT)
根据由于w和z条件独立,则有:
p ( w 1 , w 2 . . . . w T ∣ z 1 , z 2 . . . z T ) = ∏ i = 1 n p ( w i ∣ z i ) p(w_1,w_2....w_T|z_1,z_2...z_T)= \prod_{i=1}^n p(w_i|z_i) p(w1,w2....wTz1,z2...zT)=i=1np(wizi)
同时根据马尔科夫假设的bigram模型
p ( z 1 , z 2 . . . z T ) = p ( z 1 ) ⋅ ∏ i = 2 n p ( z i ∣ z i − 1 ) p(z_1,z_2...z_T)=p(z_1)\cdot\prod_{i=2}^n p(z_i|z_{i-1}) p(z1,z2...zT)=p(z1)i=2np(zizi1)
综上所述,原式可化简为
p ( p o s ∣ s e n t e n c e ) = {   ∏ i = 1 n p ( w i ∣ z i ) }   ⋅ p ( z 1 ) ⋅ ∏ i = 2 n p ( z i ∣ z i − 1 ) p(pos|sentence)= \{~\prod_{i=1}^n p(w_i|z_i) \}~ \cdot p(z_1)\cdot\prod_{i=2}^n p(z_i|z_{i-1}) p(possentence)={ i=1np(wizi)} p(z1)i=2np(zizi1)

该式子的影响因素共有三条,

  • p(w|z)表示是给定一个词性z的情况下 ,原单词是w 的概率。
  • p(z)表示该词性z在句首的概率(初始状态)
  • p(z2|z1)表示在给定词性z1的情况下,下一个词性是z2的概率(状态转移)

下面用计算机语言实现

设词库字典大小为M,词性字典大小为N

设矩阵A(N*M),矩阵的每一行代表一个词性,每一列代表一个单词。比如A[0][1]表示z1词性的情况下,为w2的单词的概率。

w1 w2 Wm
z1 0.1 0.01 0.01
z2 0.15 0.002 0.002
Zn 0.15 0.002 0.002

设矩阵B(45*45),矩阵的每一行代表一个词性,每一列代表一个单词。比如B[0][0]表示z1的词性的情况下,下一个单词词性仍为z1的概率。

z1 z2 Zn
z1 0.1 0.01 0.01
z2 0.15 0.002 0.002
Zn 0.15 0.002 0.002

设矩阵pi(1*45),矩阵的每一行代表一个词性在句首的概率。比如pi[0]表示的z1词性在句首的概率。

z1 z2 Zn
p 0.1 0.01 0.01

下面实现viterbi算法,
建立一个矩阵V(T*N)相当于动态规划的dp矩阵,其中T是测试句子中单词的个数
注意V和A矩阵是转置的关系

z1 z2 …j… zn
w1
w2
…i…
wt

i表示行坐标,j 表示列坐标
A[0][1]表示z1词性的情况下,为w2的单词的概率
pi[0]表示的z1词性在句首的概率

初始化v矩阵的第一列
V [ 0 ] [ z i ] = p i [ z j ] ⋅ A [ z j ] [ w i ] V[0][z_i]=pi[z_j]\cdot A[z_j][w_i] V[0][zi]=pi[zj]A[zj][wi]

计算V矩阵的下一列,假如计算V[1][1]
【NLP】viterbi 算法图文全解析——词性标注案例分析_第1张图片

从上一行到V[1][1]有N种方式,最好的方式是概率最大的(由于不是第一行,所以不用乘pi)。

V [ 1 ] [ 1 ] = A [ 1 ] [ 1 ] ⋅ m a x k ( 0 − N ) { V [ 1 ] [ z k ] ⋅ B [ z k ] [ 1 ] } V[1][1]=A[1][1] \cdot max_{k(0-N)}\{ V[1][z_k]\cdot B[z_k][1] \} V[1][1]=A[1][1]maxk(0N){V[1][zk]B[zk][1]}

则通项公式为

V [ w i ] [ z j ] = A [ z j ] [ w i ] ⋅ m a x k ( 0 − N ) { V [ w j − 1 ] [ z k ] ⋅ B [ z k ] [ z j ] } V[w_i][z_j]=A[z_j][w_i] \cdot max_{k(0-N)}\{ V[w_{j-1}][z_k]\cdot B[z_k][z_j] \} V[wi][zj]=A[zj][wi]maxk(0N){V[wj1][zk]B[zk][zj]}

至此 我们已经求出 了V矩阵,在求解的过程中还要构造一个ptr矩阵,用于记录当前节点的最优前驱节点,方便从后向前所以最优路径。具体方法见代码。
下面求最优词性列表best_pos 长度为T 则有
b e s t _ p o s [ t ] = { a r g m a x i ( 0 − n ) ( V [ i ] [ t ] ) t=T-1 p t r [    b e s t _ p o s [ t + 1 ]    ] [ t + 1 ] t>0 and t!=T-1 best\_pos[t]= \begin{cases} argmax_{i(0-n)}(V[i][t]) & \text{t=T-1} \\ ptr[~~best\_pos[t+1]~ ~][t+1]& \text{t>0 and t!=T-1} \end{cases} best_pos[t]={argmaxi(0n)(V[i][t])ptr[  best_pos[t+1]  ][t+1]t=T-1t>0 and t!=T-1
下面是具体的代码实现

# pos_tagging.py
import numpy as np
#用于将词性tag转化为id 方便在矩阵中使用id 代替tag的值
tag2id,id2tag={},{}

#用于将单词word转化为id 方便在矩阵中使用id 代替word的值
word2id,id2word={},{}
#文件的位置要根据实际情况自己调整。
with open('corpurs/traindata.txt','r') as f:
	for x in f:
		l=x.split('/')
		word=l[0]
		tag=l[1].rstrip()#去除换行符号
		if word not in word2id:#如果字典中不存在,将其添加到字典
			word2id[word]=len(word2id)
			id2word[len(id2word)]=word
		if tag not in tag2id:#同上
			tag2id[tag]=len(tag2id)
			id2tag[len(id2tag)]=tag
M=len(word2id)#词典大小 #of words in dictionary
N=len(tag2id)#词性种类个数 #of tags in tag set
# print(M)
# print(tag2id)
#到此 ,词典和id之间的转化工作完成。
#————————————————————————————————————————————————————————————————
# init matrix of word and tag
#初始化A B pi 矩阵
pi = np.zeros(N)# 每个词性出现在句首的概率
A  = np.zeros((N,M))#给定tag i 出现该单词的概率 p(word|tag)
B  = np.zeros((N,N))#状态转化矩阵 

with open('corpurs/traindata.txt','r') as f:
	prev_tag="."
	for x in f:
		l=x.split('/')
		word=l[0]
		tag=l[1].rstrip()
		#将单词转化为id
		wordid,tagid=word2id[word],tag2id[tag]
		if prev_tag==".":#上一个词性是句号,表示当前单词在句首出现
			pi[tagid]+=1
			A[tagid][wordid]+=1#表示该tag词性下的word_id的单词出现一次。
		else:#表示非句首
			A[tagid][wordid]+=1
			B[tag2id[prev_tag]][tagid]+=1#表示该prev_tag词性下,下一次是tag_id的词性。
		prev_tag=tag

#将词出现的频率 转化为概率 使用add one smoothing进行平滑处理

pi=(pi+1)/(2*sum(pi))
A=A+1
B=B+1
for i in range(N):
	#A和B的每一行都代表一个词性,所以每一行的概率和为1,
	A[i]=(A[i])/(2*sum(A[i]))
	B[i]=(B[i])/(2*sum(B[i]))

#到此 A ,B ,pi 计算完毕
#———————————————————————————————————————————————————————————————————— 
# 用于取对数
def log(v):
    if v == 0:
        return np.log(v+0.000001)
    return np.log(v)

def vitebi(x,A,B,pi):
    """
    x: user input string/sentence: x: "I like playing soccer"
    pi: initial probability of tags
    A: 给定tag, 每个单词出现的概率
    B: tag之间的转移概率
    """
    k=len(word2id)
    ls=[]
    #如果单词词库不存在,则将其id 设置为词库+1,依次累加
    for word in x.split(" "):
        if word2id.__contains__(word):
           ls.append(word2id[word])  # x: [4521, 412, 542 ..]
        else:
            k=k+1
            ls.append(k)
    x=ls
    T = len(x)

    
    dp = np.zeros((T,N))  # dp[i][j]: w1...wi, 假设wi的tag是第j个tag
    ptr = np.array([[0 for x in range(N)] for y in range(T)] ) # T*N
    # TODO: ptr = np.zeros((T,N), dtype=int)

    for j in range(N): # basecase for DP算法
        dp[0][j] = log(pi[j]) + log(A[j][x[0]] if x[0] < len(A[j]) else 0.0000001 )

    for i in range(1,T): # 每个单词
        for j in range(N):  # 每个词性
            dp[i][j] = -9999999
            for k in range(N): # 从每一个k可以到达j 若A中不存在该单词id 则出现概率设置为很小的数
                score = dp[i-1][k] + log(B[k][j]) + log(A[j][x[i]] if x[i] < len(A[j]) else 0.0000001)
                if score > dp[i][j]:
                    dp[i][j] = score
                    ptr[i][j] = k
    
    # decoding: 把最好的tag sequence 打印出来,维特比路径
    best_seq = [0]*T  # best_seq = [1,5,2,23,4,...]  
    # step1: 找出对应于最后一个单词的词性
    best_seq[T-1] = np.argmax(dp[T-1])
    # step2: 通过从后到前的循环来依次求出每个单词的词性
    for i in range(T-2, -1, -1): # 从T-2 到 0 步长为-1
        best_seq[i] = ptr[i+1][best_seq[i+1]]
    # 到目前为止, best_seq存放了对应于x的 词性序列,下面将其转化回词性内容
    tag_ls=[id2tag[i] for i in best_seq]
    print (tag_ls)

s="I love sport ."  

# s="A full , color page in Newsweek will cost s
vitebi(s,A,B,pi)

tip:所使用的数据集在附件中。
源码地址:github-daxiaisme

经过上面的案例恐怕大家对维特比算法还是不太清楚,下面再看下百度百科的定义

维特比算法是一种动态规划算法用于寻找最有可能产生观测事件序列的-维特比路径-隐含状态序列,特别是在马尔可夫信息源上下文和隐马尔可夫模型中。术语“维特比路径”和“维特比算法”也被用于寻找观察结果最有可能解释相关的动态规划算法。例如在统计句法分析中动态规划算法可以被用于发现最可能的上下文无关的派生(解析)的字符串,有时被称为“维特比分析”。
【NLP】viterbi 算法图文全解析——词性标注案例分析_第2张图片
来源自百度百科-维特比算法

上面k个状态,对应N种词性
aij 对应B矩阵
p(y|k)对应矩阵A
此处y1,y2…对应w1,w2…
x1,x2,…对应输出的z1,z2

思考下是否有豁然开朗的感觉,欢迎留言一起讨论。

你可能感兴趣的:(机器学习,NLP)