引言
词性标注即在给定的句子中判定每个单词最合适的词性标记。是自然语言处理的基础。这里用的词性标注模型是ngram模型。
词性标注原理
假设句子,对应每个词的词性标注序列
。
我们要求给定句子得出词性序列
的概率
。
其中我们假定是相互独立的,
用到了bigram模型。
我们要求出最好的序列。
我们要计算出三个概率:,分别记为
。
我们可以看成有三个参数。当我们求出这个三个参数后,就可以使用维特比算法来求出最大的
。
我们先来看下,它可以表示为一个矩阵。行是词性数量,列是单词数。
假设上面有一行向量代表动词(vb),表示每个单词是动词的概率。上面这个矩阵我们可以通过语料库计算出来。
下面来看下,即
,它表示每个词性出现在句首的概率。显示用一个向量就可以表示,向量的大小就是词性数量。
剩下的可以用状态转移矩阵来表示,所谓状态转移,这里指的是一个词性后面接另一个词性。
的大小是
。
计算这个和计算语言模型的Bigram是一样的。
Newsweek/NNP
,/,
trying/VBG
to/TO
keep/VB
pace/NN
with/IN
rival/JJ
Time/NNP
magazine/NN
上面是我们语料库中一句话,左边是单词,右边是词性。
把上面这句话转换成:
NNP,VBG,TO,VB,NN,IN,JJ,NNP,NN
就可以计算这个词性的Bigram。
下面就通过我们的语料库来统计下。
代码实现
这里我们认为英文句号"."代表一个句子结束。它后面第一个单词/词性代表句首。
首先我们用4个字典来保存词性、单词与索引相关信息
# 词性到索引 以及 索引到词性的字典
tag2id, id2tag = {},{}
# 单词到索引,索引到单词
word2id,id2word = {}, {}
# 读取训练数据
for line ine open(./data/traindata.txt):
items = line.split('/')# 单词/词性
word, tag = items[0],items[1].strip() #去掉换行符
if word not in word2id:
word2id[word] = len(word2id)
id2word[len(id2word)] = word
if tag not in tag2id:
tag2id[tag] = len(tag2id)
tag2id[len(tag2id)] = tag
M = len(word2id) #单词数量
N = len(tag2id) #词性数量
下面我们要计算。
import numpy as np
pi =np.zeros(N)#每个词性出现在句首的概率
A = np.zeros((N,M))#A[i][j]表示 给定词性 i,出现单词j的概率
B = np.zeros((N,N)) #B[i][j],词性i后面接j的概率
# 构建 π,A,B
pre_tag = ''#前一个词性
for line in open('./data/traindata.txt'):
items = line.split('/')
# 单词索引和词性索引
wordId,tagId = word2id[items[0]],tag2id[items[1].rstrip()]
A[tagId][wordId] += 1#tag下面 单词出现的次数 加1
if pre_tag == '': #表示句首
# 需要计算 π
pi[tagId] += 1 #先统计次数,后面再除以总数
else: #不是句首
B[tag2id[pre_tag]][tagId] += 1
pre_tag = items[1].rstrip()
if items[0] == '.': #表示后面就是句首了,要清空pre_tag
pre_tag = ''
# 把次数转换为概率
pi = pi / sum(pi)
for i in range(N):
A[i] /= sum(A[i])
B[i] /= sum(B[i])
参数都计算出来了。下面就可以计算给定一句话的词性了。
“Newsweek(NNP) said(VBD) it(PRP) will(MD) introduce(VB) the(DT)”。
假定我们的句子是这样的,下面我们要求出每个单词的词性。我们列出每个单词和每一种词性:
我们要从中找出一个路径,假定上图标出的绿色路径为,我们要如何计算这条路径的得分呢。
用到下面的公式就可以
下面我们用动态规划(维特比算法)的方式来求出最好的路径,定义dp[i,j]
(i,j
分别代表列和行)表示到从起点到这个矩阵中
dp[i,j]
位置最短的路径。
那如何计算dp[i,j]
呢,假设它是从词性nnp
过来的:
dp[i,j] = dp[i-1,0] + log p(dt|nnp) + log p(w_i|dt)
可以这样表示出从每个词性过来的公式,我们最终选择的就是整个式子得分最大的那个。
好,下面用代码实现上面的过程。
import numpy as np
def log(v):
if v == 0:
return np.log(v + 0.000001)
return np.log(v)
def viterbi(x,pi,A,B):
'''
x : 句子
pi : tag出现在句首的概率
A: A[i][j]表示 给定词性 i,出现单词j的概率
B: B[i][j],词性i后面接j的概率
'''
# 简单的对应英文进行分词,并转换为词典中的索引
x = [word2id[word] for word in x.split(" ")]
T = len(x) #句子长度
dp = np.zeros((T,N)) # dp[i][j] 代表w1 到wi,并假设wi的tag是第j个
# 采用动态规划的方式,由左到右填充dp矩阵,先填充第一列,就是某个词性作为句首的情况,因此用到的是pi
for j in range(N):
# j 作为tag的概率 加上 j是tag的情况下,看到单词x[0]的概率(A)
dp[0][j] = log(pi[j]) + log(A[j][x[0]])
# 记录路径 保存到当前词性的最佳词性
path = np.array([[0 for x in range(N)] for y in range(T)]) # T x N
for i in range(1,T): #从第2个单词开始
for j in range(N): #遍历每个词性
# 目前词性为j
dp[i][j] = -float('inf')
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
path[i][j] = k
# 把最好的路径打印出来
best_tag_seq = [0] * T
# 从后往前求
# 找出最后一个单词的词性
best_tag_seq[T-1] = np.argmax(dp[T-1]) #最后一列,值最大的对应的索引
# 通过从后到前的循环来求出依次每个单词的词性
for i in range(T-2,-1,-1):
# 第i个单词最好的词性 保存到了i+1个单词上
best_tag_seq[i] = path[i+1][best_tag_seq[i+1]]
for i in range(len(best_tag_seq)):
print(id2tag[best_tag_seq[i]])
然后我们用训练数据中的一个句子测试一下:
x = 'Social Security number , passport number and details about the services provided for the payment'
viterbi(x,pi,A,B)