title: 命名实体识别学习-用lstm+crf处理conll03数据集
date: 2020-07-18 16:32:31
tags:


命名实体识别学习-用lstm+crf处理conll03数据集

文章目录

  • 命名实体识别学习-用lstm+crf处理conll03数据集
    • 一 整合时要解决的问题
    • 二 mask和pad
      • lstm读入
      • 涉及转移分矩阵的计算
    • 三 将for循环改为矩阵运算
      • gold_score的计算
      • forward_score的计算
    • 结果:
    • 总结

一直想写的一篇文章,虽然好像也不是很忙,但是一直拖着没做。就是讲下面两篇文章介绍的数据集和算法做一个整合

命名实体识别学习-数据集介绍-conll03

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

一 整合时要解决的问题

  1. 要为数据和模型读入设计合理的数据结构:即vocab-vecterizer-dataset这个pipeline,几乎所有的nlp任务都要走这个pipeline的模式(看别人的源码发现,真正实现这些数据结构时代码五花八门,不过数据结构本来就是ADT的物理实现,只要把核心功能实现就好了。)

  2. 原有的算法是一句一句读入的,我实现的时候要用mini-batch, mini-batch已经被证明了其在深度学习应用的作用和功能。不过应用mini-batch要考虑输入句子长短不一的问题,使用pad和mask的技术,尽量避免模型看到pad的元素。本模型主要有三处用到,第一是lstm读入时,然后是crf的算句子得分,和loss计算的时候。在这三处要用mask的方法避免模型读到pad的元素。

  3. 原代码,即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 sequencescore = 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].sum()d_uf = d[:n_unfinish] #[uf, 1, tag_size]

这里是直接算出非pad元素的个数,因为我们的输入是按句长排列的,所以可以直接取前n_finish个进行计算。

三 将for循环改为矩阵运算

原官网代码为了代码可读性,使用的了几处for循环,是可以用矩阵运算代替的,同时代码里的前向算法和维特比解码算法是动态规划算法,可能是不适合改为矩阵运算(不过也不一定,可能有大神实现了)

哪些可以改为for循环?这个问题我也没有查到,我的理解就是看能不能并行,能并行的话大部分都可以用矩阵运算(这是句废话),当然如何分析能不能并行,这个还有待后续查资料继续学习。

gold_score的计算

在计算neg_log_likelihood时需要计算gold_score,其代码如下:

def _score_sentence(self, feats, tags):# Gives the score of a provided tag sequencescore = 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

但看这段代码,因为score计算依赖上一个时间步的score,乍一看似乎不能无法更改(不能因为依赖上一步的结果就认为是个dp算法,关键看有没有最优子结构),不过稍加分析,其实这个是最容易改为矩阵计算的,证明如下:

score0=transition[tags[1], tags[0]]+feat[tags[1]]
score1=score0+transition[tags[2], tags[1]]+feat[tags[2]]
...
scorei+1=scorei+transition[tags[i+1], tags[i]]+feats[tags[i+1]]score=score0+score1+...+scorei+1=transition[tags[1], tags[0]]+feat[tags[1]]+...+transition[tags[i+1], tag[i]]+feat[tags[i+1]]

从上面的分析,很容易将这段代码改为矩阵计算,同时加上mask机制。最终代码如下:

    def _score_sentence(self, feats, tags, mask):score = torch.gather(feats, dim=2, index=tags.unsqueeze(dim=2)).squeeze(dim=2)score[:, 1:] += self.transition[tags[:, :-1], tags[:, 1:]]total_score = (score * mask.type(torch.float)).sum(dim=1)return total_score

forward_score的计算

原官网的代码:

 def _forward_alg(self, feats):# Do the forward algorithm to compute the partition functioninit_alphas = torch.full((1, self.tagset_size), -10000.)# START_TAG has all of the score.init_alphas[0][self.tag_to_ix[START_TAG]] = 0.# Wrap in a variable so that we will get automatic backpropforward_var = init_alphas# Iterate through the sentencefor feat in feats:alphas_t = []  # The forward tensors at this timestepfor next_tag in range(self.tagset_size):# broadcast the emission score: it is the same regardless of# the previous tagemit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)# the ith entry of trans_score is the score of transitioning to# next_tag from itrans_score = self.transitions[next_tag].view(1, -1)# The ith entry of next_tag_var is the value for the# edge (i -> next_tag) before we do log-sum-expnext_tag_var = forward_var + trans_score + emit_score# The forward variable for this tag is log-sum-exp of all the# scores.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

下面简单分析下,假设tag_size=2,值集合为:{0,1}

emit_score=feat[0].expand(1, self.tagset_size) #经过扩展emit_score变为shape为[1,2]的矩阵,因为pytorch的广播机制,其实就是[feat[0], feat[0]]
trans_score = self.transitions[0].view(1, -1)# 这个本身就是shape为[1,2]的矩阵,表示tag分别从0,1转为0的转移分。
next_tag_var0= forward_var + trans_score + emit_score=forward_var +[feat[0], feat[0]]+self.transitions[0]# 下一个迭代
emit_score=feat[1].expand(1, self.tagset_size) #经过扩展emit_score变为shape为[1,2]的矩阵,因为pytorch的广播机制,其实就是[feat[0], feat[0]]
trans_score = self.transitions[1].view(1, -1)# 这个本身就是shape为[1,2]的矩阵,表示tag分别从0,1转为0的转移分。
next_tag_var1= forward_var + trans_score + emit_score=forward_var +[feat[1], feat[1]]+self.transitions[1]从这个分析很明显可以看出可以转为矩阵计算,大致思路为:
next_tag_var = forward_var+feats.unsqueeze(dim=1)+self.transitions其中:
feat.unsqueeze(dim=1)+self.transitions # shape为[tag_size, tag_size]
forward_var的shape为[1,tag_size], 这里的矩阵计算,会通过广播机制,自动复制其自身的值。

加上batch所处的维度,刚好是三维张量的计算,是一个比较习惯处理的维度,再加去掉for循环,加高维度没有必要。最终结果:

    def _forward_alg(self, feats, mask):"""前向算法:param feats: [b_s, seq_len, tag_size]:param mask: [b_s, seq_len]:return:"""# Do the forward algorithm to compute the partition functioninit_alphas = torch.full((feats.size(0), self.tagset_size), -10000., device=self.device)    #[b_s, tag_size]# START_TAG has all of the score.along dim=1,init_alphas[:, self.begin_tag_idx]=0.# Wrap in a variable so that we will get automatic backpropforward_var_list=[]forward_var_list.append(init_alphas)d = torch.unsqueeze(feats[:,0], dim=1)  #[b_s, 1, tag_size]for feat_index in range(1, feats.size(1)):n_unfinish = mask[:, feat_index].sum()d_uf = d[:n_unfinish] #[uf, 1, tag_size]emit_and_transition = feats[: n_unfinish, feat_index].unsqueeze(dim=1)+self.transition #[uf,tag_size,tag_size]log_sum = d_uf.transpose(1, 2)+emit_and_transition  #[uf, tag_size, tag_size]max_v = log_sum.max(dim=1)[0].unsqueeze(dim=1)  #[uf, 1, tag_size]log_sum = log_sum - max_v   #[uf, tag_size, tag_size]d_uf = max_v + torch.logsumexp(log_sum, dim=1).unsqueeze(dim=1) # [uf, 1, tag_size]d = torch.cat((d_uf, d[n_unfinish:]), dim=0)d = d.squeeze(dim=1)    #[b_s, tag_size]max_d = d.max(dim=-1)[0]  # [b_s]d = max_d + torch.logsumexp(d - max_d.unsqueeze(dim=1), dim=1)  # [b_s]return d

维特比解码算法的修改思路和前向算法大致一致,修改后的代码如下:

    def _viterbi_decode(self, feats, mask, seq_len):batch_size = feats.size(0)tags = [[[i] for i in range(len(self.tag_vocab))]] * batch_size  # list, shape: (b, K, 1)d = torch.unsqueeze(feats[:, 0], dim=1)  # shape: (b, 1, K)for i in range(1, seq_len[0]):n_unfinished = mask[:, i].sum()d_uf = d[: n_unfinished]  # shape: (uf, 1, K)emit_and_transition = self.transition + feats[: n_unfinished, i].unsqueeze(dim=1)  # shape: (uf, K, K)new_d_uf = d_uf.transpose(1, 2) + emit_and_transition  # shape: (uf, K, K)d_uf, max_idx = torch.max(new_d_uf, dim=1)max_idx = max_idx.tolist()  # list, shape: (nf, K)tags[: n_unfinished] = [[tags[b][k] + [j] for j, k in enumerate(max_idx[b])] for b in range(n_unfinished)]d = torch.cat((torch.unsqueeze(d_uf, dim=1), d[n_unfinished:]), dim=0)  # shape: (b, 1, K)d = d.squeeze(dim=1)  # [b_s, tag_sizescore, max_idx = torch.max(d, dim=1)  # shape: (b,)max_idx = max_idx.tolist()tags = [tags[b][k] for b, k in enumerate(max_idx)]return score, tags

整个工程代码地址:https://github.com/SStarLib/NERfromBasic/tree/master/Day03minibatch-lstm%2Bcrfs/conll03Ner

结果:


这个是加入了预训练embedding的结果,比不加稍微好一点。不过好的有限。甚至有的地方还要更差。

总结

整个工程代码花费了接近一天的时间,写代码的时候出现了各种奇奇怪怪的bug,有的是变量命名差个s,结果写的时候没有注意出错,好不容易调通了,结果发现loss一直不变化,我还以为是我的代码实现的有问题,结果把代码改的面目全非,甚至很多地方已经背离了我最初的思路了,几乎是把整个工程推翻重写,最后debug时,发现lstm生成的feats几乎不变化,才开始意识到模型根本没起作用,最终发现是因为没有对模型参数初始化的原因,(真的是太久不写pytorch的代码了,连模型参数初始化都先不起来)虽然最后整个工程项目成功了,但是花费了大量时间,真的,写出bug free的工程代码是一个人素质的体现!

关于加入了预训练embedding的结果不显著的问题,可能是glove,只有小写字母,而字母的大小写本身就是实体的一个重要特征,使用了glove反而可能丢失了这个重要特征,解决办法是加入char embedding ,下次可以在模型中加入,或者一步到位,使用Elmo的embedding,

尽量要使用 mini batch

for循环改为矩阵计算,提高并行性,提升运算速度很重要,一般常用的都是三维张量,包含一维batch,所以尽量改成三维张量的运算为好。

pad和mask机制的使用,有空可以专门写一篇文章,介绍这个,并总结一下。

pipeline的数据结构,有空了再介绍吧,不难,主要是要明确要实现的功能,并抽象出来,然后用代码实现即可。

一直想要系统性的学一下pytorch,可是到现在连本相关的书都没买过,(甚至30分钟的官网视频也没看),甚至不知道pytorch能做啥不能做啥。所以我的coding过程非常痛苦,使用google的频率非常高。pytorch的api,几乎要不停的查。磨刀不误砍柴工,真的应该系统性的学习下这个框架,然后再想着复现论文。

比赛的时候一定不会用pytorch,相比keras的搭积木似的coding过程,pytorch的开发周期确实有点长了,甚至要一直照顾shape,所以我在我的代码里要一直备注shape的变化,和广播机制将要怎么发生。

要好好了解下pytorch的广播机制,这个是代码可读性的天敌,不好好了解,有时候无法看懂别人的代码。

命名实体识别学习-用lstm+crf处理conll03数据集相关推荐

  1. 命名实体识别学习记录(spaCy/OpenNLP..)

    命名实体识别学习记录(spaCy/OpenNLP..) spaCy 环境 功能实现 NLTK 环境 功能实现 Stanford NLP 环境 功能实现 NER works Spacy Install ...

  2. 命名实体识别学习笔记

    1 命名实体识别概述 1.1 定义 命名实体识别(Name Entity Recognition,NER),也称作"专名识别",是指识别文本中具有特定意义的实体,包括人名.地名.机 ...

  3. 零基础入门--中文命名实体识别(BiLSTM+CRF模型,含代码)

    https://github.com/mali19064/LSTM-CRF-pytorch-faster 中文分词 说到命名实体抽取,先要了解一下基于字标注的中文分词. 比如一句话 "我爱北 ...

  4. 命名实体识别学习笔记——使用Ltp

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/xuewenstudy/article/ ...

  5. 知识图谱 基于CRF的命名实体识别模型

    基于CRF的命名实体识别模型 条件随机场 CRF ​ 条件随机场 CRF 是在已知一组输入随机变量条件的情况下,输出另一组随机变量的条件概率分布模型:其前提是假设输出随机变量构成马尔可夫随机场:条件随 ...

  6. 逐行讲解CRF实现命名实体识别(NER)

    文章标题 本文概述 NER介绍 代码详解 任务 导入库 加载数据集 构造特征字典 数据处理 模型训练 模型验证 模型参数 备注 随机搜索RandomizedSearchCV 本文概述 使用sklear ...

  7. 序列标注 | (5) 命名实体识别技术综述

    本文是对 <命名实体识别技术综述>的摘录和笔记. 论文链接 文章目录 1. 简介 2. 研究难点 3. 主要方法 4. 研究热点 5. 数据集和评价指标 6. 参考文献 1. 简介 命名实 ...

  8. Stanford Named Entity Recognizer (NER) 斯坦福命名实体识别(NER)

    以下翻译内容来自:https://nlp.stanford.edu/software/CRF-NER.html About 关于 Stanford NER is a Java implementati ...

  9. 【NLP】一文了解命名实体识别

    导读:从1991年开始,命名实体识别逐渐开始走进人们的视野,在各评测会议的推动下,命名实体识别技术得到了极大地发展,从最初的基于规则和字典的方法,到现在热门的注意力机制.图神经网络等方法,命名实体识别 ...

最新文章

  1. 数据库增长到一定程度后的瘦身,优化问题
  2. 在servlet中设置的字符编码集为什么还会出现乱码(亲测)
  3. postgresql10.5安装
  4. 使用SAP WebIDE进行一个典型的bug修复流程
  5. java 写文件filewriter_使用FileWriter写文件
  6. 信息学奥赛一本通(1208:2的幂次方表示)
  7. 【AI视野·今日CV 计算机视觉论文速览 第210期】Tue, 1 Jun 2021
  8. python 基本类型,运算,循环
  9. python提高——进程、线程、协程对比及代码实现
  10. epub 深入linux内核架构_深入分析Linux内核源代码6-Linux 内存管理(2)
  11. Chrome firefox ie等浏览器空格nbsp;宽度不一样
  12. TASKCTL-函数表达式分类
  13. debian 连接 蓝牙键盘
  14. js-table2excel 导出带图片的excel
  15. adb wifi远程连接手机
  16. 航天生物计算机作文,科幻遨游太空作文(精选6篇)
  17. 设备功耗计算专题《低功耗实战篇,ESP8266睡眠模式1@深度睡眠Deep-sleep》
  18. QT打印打印excel
  19. DetachedCriteria查询和子查询
  20. SpringBoot redis GEO 实战应用

热门文章

  1. 从 0 开始带你成为JVM实战高手
  2. PTL仓库提货解决方案
  3. 浏览器开发者工具控制台重播教程
  4. Fritzing软件绘制Arduino面包板接线图传感器模块库文件269
  5. Xilinx ISE 14.7与Modelsim10.1a联合仿真
  6. tek示波器软件_泰克增强型多台示波器分析软件TekScope,有效加快分析速度
  7. Linux---如何远程连接其他服务
  8. 代数余子式矩阵求行列式
  9. 男人们必知的人生定律~(其实女人也得知道)
  10. uniapp微信小程序复制电话或者文字