title: 命名实体识别学习-用lstm+crf处理conll03数据集
date: 2020-07-18 16:32:31
tags:
命名实体识别学习-用lstm+crf处理conll03数据集
一直想写的一篇文章,虽然好像也不是很忙,但是一直拖着没做。就是讲下面两篇文章介绍的数据集和算法做一个整合
命名实体识别学习-从基础算法开始(02)lstm+crf序列标注
一 整合时要解决的问题
-
要为数据和模型读入设计合理的数据结构:即vocab-vecterizer-dataset这个pipeline,几乎所有的nlp任务都要走这个pipeline的模式(看别人的源码发现,真正实现这些数据结构时代码五花八门,不过数据结构本来就是ADT的物理实现,只要把核心功能实现就好了。)
-
原有的算法是一句一句读入的,我实现的时候要用mini-batch, mini-batch已经被证明了其在深度学习应用的作用和功能。不过应用mini-batch要考虑输入句子长短不一的问题,使用pad和mask的技术,尽量避免模型看到pad的元素。本模型主要有三处用到,第一是lstm读入时,然后是crf的算句子得分,和loss计算的时候。在这三处要用mask的方法避免模型读到pad的元素。
-
原代码,即pytorch官网上放的教程为了使代码便于理解,使用了很多for循环,不利于cuda对代码的加速,尽量将能够变为矩阵运算的for循环变为矩阵的计算。
要解决的三个问题:数据结构,pad和mask,for循环改为矩阵计算
。还有一个使代码可以在GPU上运行。(不过我自己最近没法找到卡,所以代码都是凭感觉debug的,不过这次代码已经在学弟卡上跑过了)
二 mask和pad
变长序列的处理是深度学习框架的一大难题,各个框架都有比较成熟的解决问题,
lstm读入
其中pytorch为RNN的读入专门做了处理。所以对于lstm读入时处理就很简单,只需简单调用torch.nn.utils.rnn.pack_padded_sequence()和torch.nn.utils.rnn.pad_packed_sequence即可:
def _get_lstm_features(self, embedded_vec, seq_len):
"""
:param embedded_vec: [max_seq_len, b_s, e_d]
:param seq_len: [b_s]
:return:
"""
# 初始化 h0 和 c0,可以缺省 shape:
# ([num_layers * num_directions, batch, hidden_size],[num_layers * num_directions, batch, hidden_size])
# self.hidden = self.init_hidden(1, seq_len.size(0))
pack_seq = pack_padded_sequence(embedded_vec, seq_len)
# 不初始化状态,默认初始状态都为0
# lstm_out, self.hidden = self.lstm(pack_seq, self.hidden)
lstm_out, self.hidden = self.lstm(pack_seq)
lstm_out, _ = pad_packed_sequence(lstm_out, batch_first=True) #[b_s, seq_len, h_d]
lstm_feats = self.hidden2tag(lstm_out) #[b_s, seq_len, tag_size]
lstm_feats = self.dropout(lstm_feats)
return lstm_feats
注意:使用这两个自带函数有个问题,并不能恢复百分百恢复原来的输入,他恢复后的句长是输入最长句子的长度,也就是说如果你输入时最长句子也有一定长度的pad元素,那样是没办法恢复的。
涉及转移分矩阵的计算
第二处mask是在转移分的计算,因为self.transitions给pad元素留了位置,代码如下:
self.transition = nn.Parameter(
torch.randn(self.tagset_size, self.tagset_size))
这其实不符合我们尽量避免模型看到pad元素的原则(我尝试不在transition里给pad留位置,但是由于 变长序列总会有pad元素,如果没有pad元素的位置,索引就会报错。)这里我使用折中处理,在涉及到转移分矩阵的运算并直接关联结果的都mask掉,(其实存在于矩阵里无所谓,只要最后计算不影响结果即可。
涉及到转移分的计算,主要是loss的计算,在官网文档里:
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
其中,gold_score的计算和forward_score的计算都需要mask机制。
首先是得到mask:
mask = (token_vec != self.token_vocab.lookup_token(self.pad)).to(self.device) # [b_s, max_seq_len]
这个token_vec就是句子向量,mask是一个布尔值向量,其中不等于pad的位置为true,等于pad的位置为false。
def _score_sentence(self, feats, tags):
# Gives the score of a provided tag sequence
score = torch.zeros(1)
tags = torch.cat([torch.tensor([ self.tag_to_ix[START_TAG]], dtype=torch.long), tags ])
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
这个gold_score的代码相对简单:逻辑就是把真实tag对应的转移分和发射分相加,(其实这里的for循环可以去掉换成矩阵运算)因为feats中每个句子(即每个时间步)都参与一次计算,且可能有pad元素,对mask的处理:
total_score = (score * mask.type(torch.float)).sum(dim=1)
forward_score出的mask的处理,官网关于foward_score的计算比较长,就不放了,简述下逻辑,forward_score的计算本质上就是前向算法,前向算法就是DP。(在前面的博客里介绍的比较详细)在每个时间步里求前向变量,而我们用了mini-batch那么一个时间步计算的就不是一个token了。而是一批token,而由于一个mini-batch里句子是不等长的,可能一个句子还没结束,其他句子就已经运算的pad元素了,所以要用mask机制避免pad元素参与到运算中。
for feat_index in range(1, feats.size(1)):
n_unfinish = mask[:, feat_index].