命名实体识别学习-从基础算法开始(02)lstm+crf序列标注

命名实体识别学习-从基础算法开始(02)lstm+crf序列标注

[TOC]

代码地址:https://github.com/SStarLib/NERfromBasic

序列标注

将命名实体识别看作序列标注问题,则可以用词性标注的方法来做。

标注方法:Inside–outside–beginning (tagging)

LSTM的不足

序列标注问题,lstm就可以做,但是lstm生成的标注序列是无约束的。而CRFs可以给模型增加约束,以下面例子为例,单纯的lstm极有可能生成第二个非法序列(没有先出现B,就出现了I),而条件随机场可以从训练数据中学习约束

1
2
3
4
5
6
7
合法序列:
"the wall street journal reported today that apple corporation made money"
" B I I I O O O B I O O"

非法序列:
"the wall street journal reported today that apple corporation made money"
" I I I I O O O B I O O"

条件随机场(CRFs):

image-20200622213107290

概率图模型:

image-20200622213529664

隐马尔可夫模型和条件随机场的区别:在CRFs中,观察序列X并不是由模型生成的。标记序列取值以观察序列为条件,同时来自于其邻接点。

从CRFs模型推到代码实现

模型要解决序列标注问题。所以主要使用的是线性链式的CRFs。

李航老师在《统计学习方法》里的定义:

条件随机场(conditional random field)是给定随机变量X条件下,随机变量Y的马尔可夫随机场。其中线性链条件随机场可以用于标注等问题。这时, 在条件概率模型$\hat{P}(Y|X)$中,Y是输出变量,表示标记序列,X是输入变量,表示需要标注的观测序列。也把标记序列称为状态序列(参见隐马尔可夫模型)。学习时,利用训练数据集通过极大似然估计或正则化的极大似然估计得到条件概率模型 $\hat{P}(y|x)$;预测时,对于给定的输入序列$x$,求出条件概率 $\hat{P}(y|x)$最大的输出序列 。

线性链条件随机场参数化形式如:
$$
P(y | x) = \frac{1}{Z(x)}exp\bigg(
\sum_{i,k}\lambda_k t_k (y_{i-1}, y_i, x, i)
+
\sum_{i,l}\mu_l s_l (y_i, x, i)
\bigg) \
Z(x) = \sum_{y}exp\bigg(
\sum_{i,k}\lambda_k t_k (y_{i-1}, y_i, x, i)
+
\sum_{i,l}\mu_l s_l (y_i, x, i)
\bigg) \
$$
可以使用梯度下降的方法对CRF参数进行梯度学习。(这里跟HMM模型很像),同时需要用前向算法递归的计算概率及期望值。前向算法本质是动态规划算法,基本思想L:定义前向变量 $\alpha_t(i)$,递推式:
$$
\alpha_t(i)=p(O_1 O_2 \cdots O_t, q_t=S_i | \mu)
$$

在时间$t+1$的前向变量可以根据时间$t$的前向变量$\alpha_t(1),\cdots \alpha_t(N)$的值递推计算,前向算法如下:

image-20200623223927451

前向算法,可以求出对应的概率,不但可以计算出预测标签的概率,同时可以计算真实标签的概率。然后应用损失函数,梯度下降的思想,可以逐渐优化LSTM的参数和CRF的参数。但是将CRF真正应用到LSTM的顶层,还需要做一些相应的变换,至少应该以训练网络的思想。已知句子序列$X$, 用前向算法判断标注序列的得分
$$
P_X(y_1,y_2,\cdots,y_T)\
=P_X(y_1) P_X(y_2|y_1) P_X(y_3|y_2) \cdots P_X(y_T|y_{T-1})\
=P_X(y_1) \frac{P_X(y_1, y_2)}{P_X(y_1) P_X(y_2)} P_X(y_2) \frac{P_X(y_2, y_3)}{P_X(y_2) P_X(y_3)}P_X(y_3) \dots \frac{P_X(y_{T-1}, y_T)}{P_X(y_{T-1}) P_X(y_T)} P_X(y_T)
$$

以$P_X(y_1, y_2)$为例,即在观察序列X下,在i=1位置标注记为$y_1$,在i=2位置标注记为$y_2$的概率。从序列标注的角度和CRF的定义出发,我们可以分解这个概率,即在i=1位置标注为$y_1$的概率$P_X(y_1)$, 假设L为标签集合,$y_1=l_i,y_2=l_j$,$i,j \in L$,则有转移概率$P(l_i->l_j)$,然后是$P_X (y_2)$ 即在观察序列X下,在i=2位置标注记为$y_2$的概率。然后可以在对数空间去看这个公式,并舍弃概率的意义。
$$
P_X(y_1,y_2,\cdots,y_T) \
= \frac{1}{Z} \exp \Big[emit(y_1;X)+
trans(y_1, y_2;X) + emit(y_2;X)
+\cdots + trans(y_{T-1}, y_T;X) + emit(y_T;X) \Big]
$$
定义函数 $ emit(y_i ; X) $ 为在观察序列X下,i位置标记为$y_i$的分数。定义函数$ trans(y_i, y_j ; X) $为在观察序列X下,$y_i$标记转移到$y_j$的分。对等式整理得如下所示:
$$
\log{P} = \sum_{i=1}^T emit(y_i) + \sum_{i=1}^{T-1} trans(y_i,y_{i+1}) + \log{Z}
$$
用前向算法的思想实现以上等式,整理递推表达式:
$$
foward_{i+1} = forward_{i} + emit(i) + trans(y_i, y_{i+1})
$$
用Python语言实现该前向算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def _forward_alg(self, feats):
init_alphas = torch.full((1, self.tagset_size), -10000.).to(device)
init_alphas[0][self.tag_to_ix[START_TAG]] = 0

forward_var = init_alphas

# Iterate through the sentence
for feat in feats:
alphas_t = [] # The forward tensors at this timestep 前向张量
for next_tag in range(self.tagset_size):
emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)
trans_score = self.transitions[next_tag].view(1, -1)
next_tag_var = forward_var + trans_score + emit_score
alphas_t.append(log_sum_exp(next_tag_var).view(1))
forward_var = torch.cat(alphas_t).view(1, -1)
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
alpha = log_sum_exp(terminal_var)
return alpha

设计损失函数

有了前向算法,就可以设计损失函数, 由于训练的目的是为了学习到更准确的发射矩阵(emit score matrix)和转移矩阵(trans score matrix),可以将真实标注输入前向算法得到一个分数,以及预测标注输入前向算法得到一个分数,以这两个分数的差值作为损失函数。这不只是经验上的选择,已有文献进行了数学上的证明,就不再赘述。损失函数的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
def _score_sentence(self, feats, tags):
score = torch.zeros(1).to(device)
tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long).to(device), tags]).to(device)
for i, feat in enumerate(feats):
score = score + self.transitions[ tags[i + 1], tags[i] ] + feat[ tags[i + 1] ]
score = score + self.transitions[ self.tag_to_ix[STOP_TAG], tags[-1] ]
return score
def neg_log_likelihood(self, sentence, tags):
feats = self._get_lstm_features(sentence)
forward_score = self._forward_alg(feats)
gold_score = self._score_sentence(feats, tags)
return forward_score - gold_score

LSTM提取特征

代码: lstm的参数就是emit score 即,对于观察序列,给每个可能的标注打分。这是个矩阵,可以用反向传播进行学习。

1
2
3
4
5
6
7
def _get_lstm_features(self, sentence):
self.hidden = self.init_hidden()
embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)
lstm_out, self.hidden = self.lstm(embeds, self.hidden)
lstm_out = lstm_out.view(len(sentence), self.hidden_dim)
lstm_feats = self.hidden2tag(lstm_out)
return lstm_feats

模型整体框架

image-20200623225600435

总结

lstm学习emit score,crf优化trans score,前向算法+neg-log-sum 进行计算loss,维特比算法解码出标记序列在新样本上进行推断。