番外.2.词性标注by Viterbi

文章目录


重新再复习一下NLP,把一些内容以番外的内容记录一下。本节使用维比特算法来实现了一个英文单词词性标注的模型。
公式输入请参考: 在线Latex公式

数据格式说明

数据是一个txt文件,里面包含很多句子,然后按单词(包括标点符号)进行了分词,然后每个词后面对应该词的词性。一个词在不同的语句中词性可能是不一样的。
贴一部分:
Still/RB
,/,
massive/JJ
internal/JJ
debt/NN
has/VBZ
forced/VBN
the/DT
government/NN
to/TO
borrow/VB
massively/RB
on/IN
the/DT
domestic/JJ
market/NN
and/CC
to/TO
offer/VB
inflation-adjusted/JJ
returns/NNS
of/IN
2/CD
%/NN
to/TO
3/CD
%/NN
a/DT
month/NN
just/RB
to/TO
get/VB
investors/NNS
to/TO
hold/VB
on/RP
to/TO
its/PRP$
paper/NN
./.
关于词性不多深入,大概就是有:
NN名词
VB动词
IN介词

模型公式推导

目标描述

假设有一句话 S S S可以表示为多个单词 w w w的序列
S = w 1 w 2 w 3 ⋯ S=w_1w_2w_3\cdots S=w1​w2​w3​⋯
每个单词在句子中都有对应的词性 z z z:
w 1 → z 1 , w 2 → z 2 , w 3 → z 3 ⋯ w_1\rightarrow z_1,w_2\rightarrow z_2,w_3\rightarrow z_3\cdots w1​→z1​,w2​→z2​,w3​→z3​⋯
得到一个词性标注序列 Z Z Z:
Z = z 1 , z 2 , z 3 ⋯ Z=z_1,z_2,z_3\cdots Z=z1​,z2​,z3​⋯
对于有T个单词的句子,上面序列长度为T
我们的目标是构建一个模型,通过对语料库的训练,可以对新的一个句子 S ′ S' S′:
S ′ = w 1 ′ w 2 ′ w 3 ′ ⋯ S'=w'_1w'_2w'_3\cdots S′=w1′​w2′​w3′​⋯
进行词性标注预测:
Z ′ = z 1 ′ z 2 ′ z 3 ′ ⋯ Z'=z'_1z'_2z'_3\cdots Z′=z1′​z2′​z3′​⋯

Noisy Channel Model

根据前面的
Noisy Channel Model的内容,我们可以写出模型就是要使得在给定序列 S S S的条件下,词性序列 Z Z Z出现概率最大化:
P ( Z ∣ S ) = P ( S ∣ Z ) ⋅ P ( Z ) P(Z|S)= P(S|Z)\cdot P(Z) P(Z∣S)=P(S∣Z)⋅P(Z)
其中 P ( S ∣ Z ) P(S|Z) P(S∣Z)是翻译模型(translation model), P ( Z ) P(Z) P(Z)是语言模型(language model),展开(这里使用约等于是前面部分有独立性条件假设约束进行简化。后面部分按bigram来展开):
P ( w 1 w 2 ⋯ w N ∣ z 1 z 2 ⋯ z N ) ⋅ P ( z 1 z 2 ⋯ z N ) ≈ ∏ i = 1 T P ( w i ∣ z i ) ⋅ P ( z 1 ) P ( z 2 ∣ z 1 ) P ( z 3 ∣ z 2 ) ⋯ P ( z n ∣ z n − 1 ) P(w_1w_2\cdots w_N|z_1z_2\cdots z_N)\cdot P(z_1z_2\cdots z_N)\\ \approx\prod_{i=1}^TP(w_i|z_i)\cdot P(z_1)P(z_2|z_1)P(z_3|z_2)\cdots P(z_n|z_{n-1}) P(w1​w2​⋯wN​∣z1​z2​⋯zN​)⋅P(z1​z2​⋯zN​)≈i=1∏T​P(wi​∣zi​)⋅P(z1​)P(z2​∣z1​)P(z3​∣z2​)⋯P(zn​∣zn−1​)
目标函数就是:
Z ^ = a r g max ⁡ P ( Z ∣ S ) = a r g max ⁡ ∏ i = 1 T P ( w i ∣ z i ) ⋅ P ( z 1 ) ∏ j = 2 T P ( z j ∣ z j − 1 ) = a r g max ⁡ log ⁡ ( ∏ i = 1 T P ( w i ∣ z i ) ⋅ P ( z 1 ) ∏ j = 2 T P ( z j ∣ z j − 1 ) ) = a r g max ⁡ ∑ i = 1 T P ( w i ∣ z i ) + log ⁡ P ( z 1 ) + ∑ j = 2 T P ( z j ∣ z j − 1 ) (1) \begin{aligned} \hat Z &=arg\max P(Z|S) \\ &=arg\max \prod_{i=1}^TP(w_i|z_i)\cdot P(z_1)\prod_{j=2}^TP(z_j|z_{j-1}) \\ &= arg\max\log\left(\prod_{i=1}^TP(w_i|z_i)\cdot P(z_1)\prod_{j=2}^TP(z_j|z_{j-1}) \right)\\ &=arg\max\sum_{i=1}^TP(w_i|z_i)+\log P(z_1)+\sum_{j=2}^TP(z_j|z_{j-1}) \end{aligned}\tag{1} Z^​=argmaxP(Z∣S)=argmaxi=1∏T​P(wi​∣zi​)⋅P(z1​)j=2∏T​P(zj​∣zj−1​)=argmaxlog(i=1∏T​P(wi​∣zi​)⋅P(z1​)j=2∏T​P(zj​∣zj−1​))=argmaxi=1∑T​P(wi​∣zi​)+logP(z1​)+j=2∑T​P(zj​∣zj−1​)​(1)
把上面式子中的三个分项目分别记为 A , π , B A,\pi,B A,π,B
Z ^ = a r g max ⁡ A + π + B \hat Z =arg\max A+\pi+B Z^=argmaxA+π+B

整个算法的思路就是
1.计算参数 θ = { A , π , B } \theta=\{A,\pi,B\} θ={A,π,B}
对于 A A A,是一个矩阵大小为 M × N M\times N M×N, M M M为词库大小, N N N是词性库大小,矩阵每一行代表每个词出现为该词性的概率。可以从训练数据中计算出来。
对于 π \pi π,是一个向量,大小为 N N N,每个元素代表每个词性做为句子开始单词词性的概率。可以从训练数据中计算出来。
对于 B B B,是一个矩阵,是大小为 N × N N\times N N×N,每个元素代表每个词性转化到另外一个词性的概率。可以从训练数据中计算出来。

2.使用维特比算法计算 Z ^ \hat Z Z^

代码实现

#初始化单词字典word2id, id2word、词性字典tag2id, id2tag
tag2id, id2tag = {}, {}  # maps tag to id . tag2id: {"VB": 0, "NNP":1,..} , id2tag: {0: "VB", 1: "NNP"....}
word2id, id2word = {}, {} # maps word to id

for line in open('traindata.txt'):#按行读取数据
    items = line.split('/')#单词和词性是用斜杠分割的,split后得到单词和词性
    word, tag = items[0], items[1].rstrip()  # 抽取每一行里的单词和词性,.rstrip()是去掉词性后面的换行符\n
    
    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)  # M: 词典的大小:number of words in dictionary
N = len(tag2id)   # N: 词性的种类个数:  number  of tags in tag set

番外.2.词性标注by Viterbi
可以看到这里没有过滤标点。

# 初始化 pi, A, B
import numpy as np
pi = np.zeros(N)   # 词性字典中每个词性tag i出现在句子中第一个位置的概率
A = np.zeros((N,M)) # A[i][j]: 给定词性tag i, 出现单词word j的概率
B = np.zeros((N,N))  # B[i][j]: 之前的状态是词性tag i, 之后转换成转态为词性tag j的概率
prev_tag = ""#前一个单词词性
for line in open('traindata.txt'):#按行读取训练数据
    items = line.split('/')
    wordId, tagId = word2id[items[0]], tag2id[items[1].rstrip()]#将词和词性转化为id表示
    if prev_tag == "":  # 判断是否句子的开始
        pi[tagId] += 1#开始词计数+1,最后算概率
        A[tagId][wordId] += 1#词性对应单词计数+1
        # B矩阵不用更新,因为句首单词的词性属于首状态,不是其他词性转化过来的。
    else:  # 如果不是句子的开头
        A[tagId][wordId] += 1
        B[tag2id[prev_tag]][tagId] += 1
    
    if items[0] == ".":#句号出现表示读完一句话
        prev_tag = ""#重置前一个单词词性为空
    else:
        prev_tag = items[1].rstrip()#将当前单词词性作为下一个单词的prev_tag

# 概率计算
pi = pi/sum(pi)
for i in range(N):
    A[i] /= sum(A[i])
    B[i] /= sum(B[i])


看看那个词性出现在句首概率最大
番外.2.词性标注by Viterbi

问题

有了上面的信息,下面来看看如何来计算一个有6个单词的句子的 Z ^ \hat Z Z^,如果六个单词分别是 w 1 , w 2 , w 3 , w 4 , w 5 , w 6 w_1,w_2,w_3,w_4,w_5,w_6 w1​,w2​,w3​,w4​,w5​,w6​,且根据上面的截图可以知道,目前词库中的词性字典共有54种词性,每个单词都有54种词性的可能,因此整句话六个单词的词性序列的组合排列可能性为: 5 4 6 54^6 546(种)。可以从这个例子里面看出来直接使用穷举的方法来把 5 4 6 54^6 546种排列的 Z ^ \hat Z Z^算出来,然后求最大值是非常低效的,其时间复杂度约为 O ( 5 4 N ) O(54^N) O(54N)。因此我们用维特比算法来解决这个动态规划的问题。
假设句子中有 T T T个单词,每个单词有54种可能的词性,那么如下图所示的绿色路径 r 1 r_1 r1​的 Z ^ \hat Z Z^值我们记为 s c o r e ( r 1 ) score(r_1) score(r1​),其计算公式可以根据前面的公式1推导可以得。
番外.2.词性标注by Viterbi
第一个单词:
log ⁡ P ( V B ) π + log ⁡ P ( w 1 ∣ V B ) A \log \underset{\pi}{P(VB)}+\log \underset{A}{P(w_1|VB)} logπP(VB)​+logAP(w1​∣VB)​
第二个单词:
log ⁡ P ( N N P ∣ V B ) B + log ⁡ P ( w 2 ∣ N N P ) A \log \underset{B}{P(NNP|VB)}+\log \underset{A}{P(w_2|NNP)} logBP(NNP∣VB)​+logAP(w2​∣NNP)​
第三个单词:
log ⁡ P ( V B ∣ N N P ) B + log ⁡ P ( w 3 ∣ V B ) A \log \underset{B}{P(VB|NNP)}+\log \underset{A}{P(w_3|VB)} logBP(VB∣NNP)​+logAP(w3​∣VB)​
除了第一个单词之外,从第二个单词开始都只有A和B项,可以依次写出后面的公式。
s c o r e ( r 1 ) = log ⁡ P ( V B ) π + log ⁡ P ( w 1 ∣ V B ) A + log ⁡ P ( N N P ∣ V B ) B + log ⁡ P ( w 2 ∣ N N P ) A + log ⁡ P ( V B ∣ N N P ) B + log ⁡ P ( w 3 ∣ V B ) A ⋯ ⋯ \begin{aligned} score(r_1)&=\log \underset{\pi}{P(VB)}+\log \underset{A}{P(w_1|VB)}\\ &+\log \underset{B}{P(NNP|VB)}+\log \underset{A}{P(w_2|NNP)}\\ &+\log \underset{B}{P(VB|NNP)}+\log \underset{A}{P(w_3|VB)}\\ &\cdots\cdots \end{aligned} score(r1​)​=logπP(VB)​+logAP(w1​∣VB)​+logBP(NNP∣VB)​+logAP(w2​∣NNP)​+logBP(VB∣NNP)​+logAP(w3​∣VB)​⋯⋯​

动态规划通项

番外.2.词性标注by Viterbi
我们记在单词序列中某个单词 w i w_i wi​对应词性 j j j的时候可以使得在该词的 Z ^ \hat Z Z^最大,此时值为 d p [ i , j ] dp[i,j] dp[i,j],那么这个值有可能是从前一个单词 w i − 1 w_{i-1} wi−1​的某个词性 j j j过来的。
当前一个词的词性为NNP,那么 j = 0 j=0 j=0(第一行红点):
d p [ i , j ] = d p [ i − 1 , 0 ] + log ⁡ P ( V B D ∣ N N P ) + log ⁡ P ( w i ∣ V B D ) dp[i,j]=dp[i-1,0]+\log P(VBD|NNP)+\log P(w_i|VBD) dp[i,j]=dp[i−1,0]+logP(VBD∣NNP)+logP(wi​∣VBD)
当前一个词的词性为VB,那么 j = 1 j=1 j=1(第二行红点):
d p [ i , j ] = d p [ i − 1 , 1 ] + log ⁡ P ( V B D ∣ V B ) + log ⁡ P ( w i ∣ V B D ) dp[i,j]=dp[i-1,1]+\log P(VBD|VB)+\log P(w_i|VBD) dp[i,j]=dp[i−1,1]+logP(VBD∣VB)+logP(wi​∣VBD)
当前一个词的词性为VBD,那么 j = 2 j=2 j=2(第三行左边红点):
d p [ i , j ] = d p [ i − 1 , 2 ] + log ⁡ P ( V B D ∣ V B D ) + log ⁡ P ( w i ∣ V B D ) dp[i,j]=dp[i-1,2]+\log P(VBD|VBD)+\log P(w_i|VBD) dp[i,j]=dp[i−1,2]+logP(VBD∣VBD)+logP(wi​∣VBD)
以此类推,共有54种可能,那么 d p [ i , j ] dp[i,j] dp[i,j]需要计算54次,然后从这54中情况中得到最大值作为当前的 d p [ i , j ] dp[i,j] dp[i,j]
整个算法分两步
第一步就是填充下面的动态规划数组(大小为: T × N T\times N T×N),先算第一个单词对应的所有词性的概率,然后根据第一个单词的结果算第二个单词所有词性的概率,这样按列从左到右填充完成(时间复杂度为 O ( T N ) O(TN) O(TN))。
第二步从后往前每列选出最大值 d p dp dp作为当前的最佳路径(此时时间复杂度为 O ( T N 2 ) O(TN^2) O(TN2))。

词性列表 w 1 w_1 w1​ w 2 w_2 w2​ ⋯ \cdots ⋯ w i − 1 w_{i-1} wi−1​ w i w_i wi​ ⋯ \cdots ⋯ w T − 1 w_{T-1} wT−1​ w T w_T wT​
NNP
VB
VBD
⋮ \vdots ⋮
DT
MD

代码实现

# 重写log,防止无穷大发生
def log(v):
    if v == 0:
        return np.log(v+0.000001)
    return np.log(v)
def viterbi(x, pi, A, B):
    """
    x: 要预测词性的句子,例如: "I like playing toys"
    pi: 首个单词出现某个词性的概率
    A: 给定tag, 每个单词出现的概率
    B: tag之间的转移概率
    """
    x = [word2id[word] for word in x.split(" ")]  # 将x中句子按空格进行分词,然后将单词变成对应的id,这里是有点问题的,最后的句号没隔开,因此输入的句子不能带句号
    T = len(x)#对应上面的公式,这里是句子的长度
    
    dp = np.zeros((T,N))  # 初始化dp矩阵,大小是T*N,dp[i][j]: w1...wi, 假设wi的tag是第j个tag
    #初始化用来保存当前单词对应词性的最佳值是从前一个单词的哪个词性转移过来的,大小是T*N
    ptr = np.zeros((T,N), dtype=int)
    
    #初始化dp矩阵第一列
    for j in range(N): # basecase for DP算法
        dp[0][j] = log(pi[j]) + log(A[j][x[0]])
    
    #核心算法,三层循环,时间复杂度是T*N*N
    for i in range(1,T): # 每个单词
        for j in range(N):  # 每个词性
            # TODO: 以下几行代码可以写成一行(vectorize的操作, 会使得效率变高)
            dp[i][j] = -9999999#初始化一个很小的值
            for k in range(N): # 从每一个k可以到达j,从前一列的第一行到最后一行逐个计算
                score = dp[i-1][k] + log(B[k][j]) + log(A[j][x[i]])
                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, T-1,... 1, 0
        best_seq[i] = ptr[i+1][best_seq[i+1]]
        
    # best_seq存放了对应于输入句子x的 词性序列,打印
    for i in range(len(best_seq)):
        print (id2tag[best_seq[i]])

x = "Social Security number , passport number and details about the services provided for the payment"#注意不能有句号
viterbi(x, pi, A, B)    

小结

模型的实质是最简单的HMM模型的求解,还可以对模型进行三个方面的扩展:

  1. 如果只有句子,没有词性标注,要进行无标注的学习,可以用EM算法进行求解;
  2. 模型只考虑了当前单词和当前词性的关系(简化),可以使用CRF模型,考虑前后上下文,前后词性等约束。
  3. 还可以加入LSTM+CRF模型。
上一篇:VB.NET中的类和模块


下一篇:基础概念(5):怎么声明变量