一. BILSTM + CRF介绍

https://www.jianshu.com/p/97cb3b6db573

1.介绍

基于神经网络的方法,在命名实体识别任务中非常流行和普遍。 如果你不知道Bi-LSTM和CRF是什么,你只需要记住他们分别是命名实体识别模型中的两个层。

1.1开始之前

我们假设我们的数据集中有两类实体——人名和地名,与之相对应在我们的训练数据集中,有五类标签:

B-Person, I- Person,B-Organization,I-Organization

假设句子x由五个字符w1,w2,w3,w4,w5组成,其中【w1,w2】为人名类实体,【w3】为地名类实体,其他字符标签为“O”。

1.2BiLSTM-CRF模型

以下将给出模型的结构:
第一,句子x中的每一个单元都代表着由字嵌入或词嵌入构成的向量。其中,字嵌入是随机初始化的,词嵌入是通过数据训练得到的。所有的嵌入在训练过程中都会调整到最优。
第二,这些字或词嵌入为BiLSTM-CRF模型的输入,输出的是句子x中每个单元的标签。

 
Bi-LSTM结构图

尽管一般不需要详细了解BiLSTM层的原理,但是为了更容易知道CRF层的运行原理,我们需要知道BiLSTM的输出层。

 
图2.Bi-LSTM标签预测原理图

如上图所示,BiLSTM层的输出为每一个标签的预测分值,例如,对于单元w0,BiLSTM层输出的是

1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization) 0.05 (O).

这些分值将作为CRF的输入。

1.3 如果没有CRF层会怎样

你也许已经发现了,即使没有CRF层,我们也可以训练一个BiLSTM命名实体识别模型,如图所示:

 
图3.去除CRF的BiLSTM命名实体识别模型

由于BiLSTM的输出为单元的每一个标签分值,我们可以挑选分值最高的一个作为该单元的标签。例如,对于单元w0,“B-Person”有最高分值—— 1.5,因此我们可以挑选“B-Person”作为w0的预测标签。同理,我们可以得到w1——“I-Person”,w2—— “O” ,w3——“B-Organization”,w4——“O”。
虽然我们可以得到句子x中每个单元的正确标签,但是我们不能保证标签每次都是预测正确的。例如,图4.中的例子,标签序列是“I-Organization I-Person” and “B-Organization I-Person”,很显然这是错误的。

 
在这里插入图片描述

1.4 CRF层能从训练数据中获得约束性的规则

CRF层可以为最后预测的标签添加一些约束来保证预测的标签是合法的。在训练数据训练过程中,这些约束可以通过CRF层自动学习到。
这些约束可以是:
I:句子中第一个词总是以标签“B-“ 或 “O”开始,而不是“I-”
II:标签“B-label1 I-label2 I-label3 I-…”,label1, label2, label3应该属于同一类实体。例如,“B-Person I-Person” 是合法的序列, 但是“B-Person I-Organization” 是非法标签序列.
III:标签序列“O I-label” is 非法的.实体标签的首个标签应该是 “B-“ ,而非 “I-“, 换句话说,有效的标签序列应该是“O B-label”。
有了这些约束,标签序列预测中非法序列出现的概率将会大大降低。

二. 标签的score和损失函数的定义

https://zhuanlan.zhihu.com/p/27338210

Bi-LSTM layer的输出维度是tag size,这就相当于是每个词 wi 映射到tag的发射概率值,设Bi-LSTM的输出矩阵为P,其中Pi,j代表词wi映射到tagj的非归一化概率。对于CRF来说,我们假定存在一个转移矩阵A,则Ai,j代表tagi转移到tagj的转移概率。
对于输入序列 X 对应的输出tag序列 y,定义分数为

 
在这里插入图片描述

利用Softmax函数,我们为每一个正确的tag序列y定义一个概率值(YX代表所有的tag序列,包括不可能出现的)

 
在这里插入图片描述

因而在训练中,我们只需要最大化似然概率p(y|X)即可,这里我们利用对数似然

 
在这里插入图片描述

所以我们将损失函数定义为-log(p(y|X)),就可以利用梯度下降法来进行网络的学习了。
loss function:

 
在这里插入图片描述

在对损失函数进行计算的时候,S(X,y)的计算很简单,而

 
在这里插入图片描述

(下面记作logsumexp)的计算稍微复杂一些,因为需要计算每一条可能路径的分数。这里用一种简便的方法,对于到词wi+1的路径,可以先把到词wi的logsumexp计算出来,因为

 
在这里插入图片描述

因此先计算每一步的路径分数和直接计算全局分数相同,但这样可以大大减少计算的时间。

三. 对于损失函数的详细解释

这篇文章对于理解十分有用

https://blog.csdn.net/cuihuijun1hao/article/details/79405740

举例说 【我 爱 中国人民】对应标签【N V N】那这个标签就是一个完整的路径,也就对应一个Score值。
接下来我想讲的是这个公式:

 
在这里插入图片描述

这个公式成立是很显然的,动笔算一算就知道了,代码里其实就是用了这个公式的原理。

def _forward_alg(self, feats):
# Do the forward algorithm to compute the partition function
init_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 backprop
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):
# broadcast the emission score: it is the same regardless of
# the previous tag
emit_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 i
trans_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-exp
next_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

我们看到有这么一段代码 :

next_tag_var = forward_var + trans_score + emit_score

我们主要就是来讲讲他。
首先这个算法的思想是:假设我们要做一个词性标注的任务,对句子【我 爱 中华人民】,我们要对这个句子做

 
在这里插入图片描述

意思就是 对这个句子所有可能的标注,都算出来他们的Score,然后按照指数次幂加起来,再取对数。一般来说取所有可能的标注情况比较复杂,我们这里举例是长度为三,但是实际过程中,可能比这个要大得多,所以我们需要有一个简单高效得算法。也就是我们程序中得用得算法, 他是这么算得:
先算出【我, 爱】可能标注得所有情况,取 log_sum_exp 然后加上 转换到【中国人民】得特征值 再加上【中国人民】对应得某个标签得特征值。其等价于【我,爱,中国人民】所有可能特征值指数次幂相加,然后取对数.
接下来我们来验证一下是不是这样

首先我们假设词性一共只有两种 名词N 和 动词 V
那么【我,爱】得词性组合一共有四种 N + N,N + V, V + N, V + V
那么【爱】标注为N时得log_sum_exp 为

 
在这里插入图片描述

【爱】 标注为 V时的 log_sum_exp为

 
在这里插入图片描述

我们的forward列表里就是存在着这两个值,即:

 
在这里插入图片描述

假设【中华人民】得词性为N,我们按照代码来写一下公式,在forward列表对应位置相加就是这样

 
在这里插入图片描述
 
在这里插入图片描述

四. 代码块详细说明:

https://blog.csdn.net/Jason__Liang/article/details/81772632

先说明两个重要的矩阵:

feats: 发射矩阵(emit score)是sentence 在embedding后,再经过LSTM后得到的矩阵(也就是LSTM的输出), 维度为11 * 5 (11为sentence 的length,5是标签数)。这个矩阵表示经过LSTM后sentence的每个word对应的每个labels的得分)。 表示发射概率。

self.transitions:转移矩阵,维度为55,transitions[i][j]表示label j转移到label i的概率。transtion[i]维度为15,表示每个label转移到label i的概率。 表示概率矩阵

1. def log_sum_exp(vec)

# compute log sum exp in numerically stable way for the forward algorithm
def log_sum_exp(vec): #vec是1*5, type是Variable
max_score = vec[0, argmax(vec)]
# max_score维度是1, max_score.view(1,-1)维度是1*1,
# max_score.view(1, -1).expand(1, vec.size()[1])的维度是1*5
max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) # 里面先做减法,减去最大值可以避免e的指数次,计算机上溢
return max_score + \
torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))

你可能会疑问return 的结果为什么先减去max_score.其实这是一个小技巧,因为一上来就做指数运算可能会引起计算结果溢出,先减去score,经过log_sum_exp后,再把max_score给加上。
其实就等同于:

return torch.log(torch.sum(torch.exp(vec)))

2. def neg_log_likelihood(self, sentence, tags)

如果你完整地把代码读完,你会发现neg_log_likelihood()这个函数是loss function.

loss = model.neg_log_likelihood(sentence_in, targets)

我们来分析一下neg_log_likelihood()函数代码:

    def neg_log_likelihood(self, sentence, tags):
# feats: 11*5 经过了LSTM+Linear矩阵后的输出,之后作为CRF的输入。
feats = self._get_lstm_features(sentence)
forward_score = self._forward_alg(feats)
gold_score = self._score_sentence(feats, tags)
return forward_score - gold_score

你在这里可能会有疑问:问什么forward_score - gold_score可以作为loss呢。
这里,我们回顾一下我们在上文中说明的loss function函数公式:

 
在这里插入图片描述

你就会发现forward_score和gold_score分别表示上述等式右边的两项。

3. def _forward_alg(self, feats):

我们通过上一个函数的分析得知这个函数就是用来求forward_score的,也就是loss function等式右边的第一项:

 
在这里插入图片描述
 # 预测序列的得分
# 只是根据随机的transitions,前向传播算出的一个score
#用到了动态规划的思想,但因为用的是随机的转移矩阵,算出的值很大score>20
def _forward_alg(self, feats):
# do the forward algorithm to compute the partition function
init_alphas = torch.full((1, self.tagset_size), -10000.) # 1*5 而且全是-10000 # START_TAG has all of the score
# 因为start tag是4,所以tensor([[-10000., -10000., -10000., 0., -10000.]]),
# 将start的值为零,表示开始进行网络的传播,
init_alphas[0][self.tag_to_ix[START_TAG]] = 0 # warp in a variable so that we will get automatic backprop
forward_var = init_alphas # 初始状态的forward_var,随着step t变化 # iterate through the sentence
# 会迭代feats的行数次
for feat in feats: #feat的维度是5 依次把每一行取出来~
alphas_t = [] # the forward tensors at this timestep
for next_tag in range(self.tagset_size): #next tag 就是简单 i,从0到len
# broadcast the emission(发射) score:
# it is the same regardless of the previous tag
# 维度是1*5 LSTM生成的矩阵是emit score
emit_score = feat[next_tag].view(
1, -1).expand(1, self.tagset_size) # the i_th entry of trans_score is the score of transitioning
# to next_tag from i
trans_score = self.transitions[next_tag].view(1, -1) # 维度是1*5 # The ith entry of next_tag_var is the value for the
# edge (i -> next_tag) before we do log-sum-exp
# 第一次迭代时理解:
# trans_score所有其他标签到B标签的概率
# 由lstm运行进入隐层再到输出层得到标签B的概率,emit_score维度是1*5,5个值是相同的
next_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))
# 此时的alphas t 是一个长度为5,例如<class 'list'>:
# [tensor(0.8259), tensor(2.1739), tensor(1.3526), tensor(-9999.7168), tensor(-0.7102)]
forward_var = torch.cat(alphas_t).view(1, -1) #到第(t-1)step时5个标签的各自分数
# 最后只将最后一个单词的forward var与转移 stop tag的概率相加
# tensor([[ 21.1036, 18.8673, 20.7906, -9982.2734, -9980.3135]])
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
alpha = log_sum_exp(terminal_var) # alpha是一个0维的tensor return alpha

4. def _score_sentence(self, feats, tags)

由2的函数分析,我们知道这个函数就是求gold_score,即loss function的第二项

# 根据真实的标签算出的一个score,
# 这与上面的def _forward_alg(self, feats)共同之处在于:
# 两者都是用的随机转移矩阵算的score
# 不同地方在于,上面那个函数算了一个最大可能路径,但实际上可能不是真实的各个标签转移的值
# 例如:真实标签是N V V,但是因为transitions是随机的,所以上面的函数得到其实是N N N这样,
# 两者之间的score就有了差距。而后来的反向传播,就能够更新transitions,使得转移矩阵逼近
#真实的“转移矩阵” # 得到gold_seq tag的score 即根据真实的label 来计算一个score,
# 但是因为转移矩阵是随机生成的,故算出来的score不是最理想的值
def _score_sentence(self, feats, tags): #feats 11*5 tag 11 维
# gives the score of a provied tag sequence
score = torch.zeros(1) # 将START_TAG的标签3拼接到tag序列最前面,这样tag就是12个了
tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags]) for i, feat in enumerate(feats):
# self.transitions[tags[i + 1], tags[i]] 实际得到的是从标签i到标签i+1的转移概率
# feat[tags[i+1]], feat是step i 的输出结果,有5个值,
# 对应B, I, E, START_TAG, END_TAG, 取对应标签的值
# transition【j,i】 就是从i ->j 的转移概率值
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

5. def _viterbi_decode(self, feats):

# 维特比解码, 实际上就是在预测的时候使用了, 输出得分与路径值
# 预测序列的得分
def _viterbi_decode(self, feats):
backpointers = [] # initialize the viterbi variables in long space
init_vvars = torch.full((1, self.tagset_size), -10000.)
init_vvars[0][self.tag_to_ix[START_TAG]] = 0 # forward_var at step i holds the viterbi variables for step i-1
forward_var = init_vvars
for feat in feats:
bptrs_t = [] # holds the backpointers for this step
viterbivars_t = [] # holds the viterbi variables for this step for next_tag in range(self.tagset_size):
# next-tag_var[i] holds the viterbi variable for tag i
# at the previous step, plus the score of transitioning
# from tag i to next_tag.
# we don't include the emission scores here because the max
# does not depend on them(we add them in below)
# 其他标签(B,I,E,Start,End)到标签next_tag的概率
next_tag_var = forward_var + self.transitions[next_tag]
best_tag_id = argmax(next_tag_var)
bptrs_t.append(best_tag_id)
viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
# now add in the emssion scores, and assign forward_var to the set
# of viterbi variables we just computed
# 从step0到step(i-1)时5个序列中每个序列的最大score
forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
backpointers.append(bptrs_t) # bptrs_t有5个元素 # transition to STOP_TAG
# 其他标签到STOP_TAG的转移概率
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
best_tag_id = argmax(terminal_var)
path_score = terminal_var[0][best_tag_id] # follow the back pointers to decode the best path
best_path = [best_tag_id]
for bptrs_t in reversed(backpointers):
best_tag_id = bptrs_t[best_tag_id]
best_path.append(best_tag_id)
# pop off the start tag
# we don't want to return that ti the caller
start = best_path.pop()
assert start == self.tag_to_ix[START_TAG] # Sanity check
best_path.reverse() # 把从后向前的路径正过来
return path_score, best_path

如果对于该函数还没有太理解,可以参考这篇博客:

https://blog.csdn.net/appleml/article/details/78579675

总结

以上就是我结合了几篇比较不错的博客后的总结,欢迎大家提问

作者:17shouuu
链接:https://www.jianshu.com/p/566c6faace64
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

pytorch BiLSTM+CRF代码详解 重点的更多相关文章

  1. Pytorch Bi-LSTM + CRF 代码详解

    久闻LSTM + CRF的效果强大,最近在看Pytorch官网文档的时候,看到了这段代码,前前后后查了很多资料,终于把代码弄懂了.我希望在后来人看这段代码的时候,直接就看我的博客就能完全弄懂这段代码. ...

  2. pytorch lstm crf 代码理解

    好久没有写博客了,这一次就将最近看的pytorch 教程中的lstm+crf的一些心得与困惑记录下来. 原文 PyTorch Tutorials 参考了很多其他大神的博客,https://blog.c ...

  3. 开胃小菜——impress.js代码详解

    README 友情提醒,下面有大量代码,由于网页上代码显示都是同一个颜色,所以推荐大家复制到自己的代码编辑器中看. 今天闲来无事,研究了一番impress.js的源码.由于之前研究过jQuery,看i ...

  4. DeepLearning tutorial(3)MLP多层感知机原理简介+代码详解

    本文介绍多层感知机算法,特别是详细解读其代码实现,基于python theano,代码来自:Multilayer Perceptron,如果你想详细了解多层感知机算法,可以参考:UFLDL教程,或者参 ...

  5. BM算法  Boyer-Moore高质量实现代码详解与算法详解

    Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...

  6. ASP.NET MVC 5 学习教程:生成的代码详解

    原文 ASP.NET MVC 5 学习教程:生成的代码详解 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 ...

  7. Github-karpathy/char-rnn代码详解

    Github-karpathy/char-rnn代码详解 zoerywzhou@gmail.com http://www.cnblogs.com/swje/ 作者:Zhouwan  2016-1-10 ...

  8. 代码详解:TensorFlow Core带你探索深度神经网络“黑匣子”

    来源商业新知网,原标题:代码详解:TensorFlow Core带你探索深度神经网络“黑匣子” 想学TensorFlow?先从低阶API开始吧~某种程度而言,它能够帮助我们更好地理解Tensorflo ...

  9. JAVA类与类之间的全部关系简述+代码详解

    本文转自: https://blog.csdn.net/wq6ylg08/article/details/81092056类和类之间关系包括了 is a,has a, use a三种关系(1)is a ...

随机推荐

  1. Docker.[1].环境准备.

    Docker.[1].环境准备. 环境描述: 在笔记本中安装了虚拟机,虚拟机中又安装了RedHat 7.x操作系统,然后在这个RedHat7.x的操作系统上,进行安装Docker. 虚拟机中的操作系统 ...

  2. maven与sbt修改国内镜像

    一.idea中的maven 1.打开IntelliJ IDEA->Settings ->Build, Execution, Deployment -> Build Tools > ...

  3. Apache Camel继承Spring Boot 实现文件远程复制和转移

    pom.xml <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-f ...

  4. datepicker插件的使用

    教程链接:http://www.runoob.com/jqueryui/example-datepicker.html 参数:http://hare6.blog.163.com/blog/static ...

  5. 【JZOJ1922】【Usaco 2005 NOV Gold】小行星群

    题目描述 Bessie想驾驶她的飞船穿过危险的小行星群,小行星群是一个N×N的网格(1 <= N <= 500),在网格内有K个小行星(1 <= K <= 10,000). 幸 ...

  6. python 不定长参数*args

  7. python之 算法和数据结构

    什么是计算机科学? --首先明确的一点就是计算机科学不仅仅是对计算机的研究,虽然计算机在科学发展的过程中发挥了重大的作用,但是它只是一个工具,一个没有灵魂的工具而已,所谓的计算机科学实际上是对问题,解 ...

  8. oracle函数 ASCII(x1)

    [功能]:返回字符表达式最左端字符的ASCII 码值. [参数]:x1,字符表达式 [返回]:数值型 [示例] SQL> select ascii('A') A,ascii('a') a,asc ...

  9. PyODPS DataFrame 的代码在哪里跑

    在使用 PyODPS DataFrame 编写数据应用时,尽管编写的是同一个脚本文件,但其中的代码会在不同位置执行,这可能导致一些无法预期的问题,本文介绍当出现相关问题时,如何确定代码在何处执行,以及 ...

  10. SVN更换新的svn链接

    输入新的SVN地址即可: