点击上方,选择星标,每天给你送干货!


来自:python遇见NLP

导读:本文主要解析Pytorch Tutorial中BiLSTM_CRF代码,几乎注释了每行代码,希望本文能够帮助大家理解这个tutorial,除此之外借助代码和图解也对理解条件随机场(CRF)会有一定帮助,因为这个tutorial代码主要还是在实现CRF部分。

 1   知识准备

在阅读tutorial前,需具备一些理论或知识基础,包括LSTM单元、BiLSTM-CRF模型、CRF原理以及一些代码中的函数使用,参考资料中涵盖了主要的涉及知识,可配合tutorial一同学习。

 2   理解CRF中归一化因子Z(x)的计算

条件随机场中的Z(x)表示归一化因子,它是一个句子所有可能标记tag序列路径的得分总和。一般的,我们会有一个直接的想法,就是列举出所有可能的路径,计算出每条路径的得分之后再加和。如上图中的例子所示,有5个字符和5个tag,如果按照上述的暴力穷举法进行计算,就有











种路径组合,而在我们的实际工作中,可能会有更长的序列和更多的tag标签,此时暴力穷举法未免显得有些效率低下。于是我们考虑采用分数累积的方式进行所有路径得分总和的计算,即先计算出到达







的所有路径的总得分,然后计算







->







的所有路径的得分,然后依次计算







->...->







间的所有路径的得分,最后便得到了我们的得分总和,这个思路源于如下等价等式:

上式相等表明,直接计算整个句子序列的全局分数与计算每一步的路径得分再加和等价,计算每一步的路径得分再加和这种方式可以大大减少计算的时间,故Pytorch Tutorial中的_forward_alg()函数据此实现。这种计算每一步的路径得分再加和的方法还可以以下图方式进行计算。

3   理解CRF中序列解码过程,即viterbi算法

如上图所示,在每个时间步上,比如’word==去‘这一列,每一个tag处(0~6竖框是tag的id),关注两个值:前一个时间步上所有tag到当前tag中总得分最大值以及该最大值对应的前一个时间步上tag的id。这样一来每个tag都记录了它前一个时间步上到自己的最优路径,最后通过tag的id进行回溯,这样就可以得到最终的最优tag标记序列。此部分对应Pytorch Tutorial中的_viterbi_decode()函数实现。

4   理解log_sum_exp()函数

Pytorch Tutorial中的log_sum_exp()函数最后返回的计算方式数学推导如下:

 5   Pytorch Tutorial代码部分注释辅助理解

import torch
import torch.nn as nn
import torch.optim as optim# 人工设定随机种子以保证相同的初始化参数,使模型可复现
torch.manual_seed(1)# 得到每行最大值索引idx
def argmax(vec):# 得到每行最大值索引idx_, idx = torch.max(vec, 1)# 返回每行最大值位置索引return idx.item()# 将序列中的字转化为数字(int)表示
def prepare_sequence(seq, to_ix):# 将序列中的字转化为数字(int)表示idx = [to_ix[c] for c in seq]return torch.tensor(idx, dtype=torch.long)# 前向算法是不断积累之前的结果,这样就会有个缺点
# 指数和积累到一定程度之后,会超过计算机浮点值的最大值
# 变成inf,这样取log后也是inf
# 为了避免这种情况,用一个合适的值clip=max去提指数和的公因子
# 这样不会使某项变得过大而无法计算
def log_sum_exp(vec):# vec:形似[[tag个元素]]# 取vec中最大值max_score = vec[0, argmax(vec)]# vec.size()[1]:tag数max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])# 里面先做减法,减去最大值可以避免e的指数次,计算机上溢# 等同于torch.log(torch.sum(torch.exp(vec))),防止e的指数导致计算机上溢return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))class BiLSTM_CRF(nn.Module):# 初始化参数def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):super(BiLSTM_CRF, self).__init__()# 词嵌入维度self.embedding_dim = embedding_dim# BiLSTM 隐藏层维度self.hidden_dim = hidden_dim# 词典的大小self.vocab_size = vocab_size# tag到数字的映射self.tag_to_ix = tag_to_ix# tag个数self.tagset_size = len(tag_to_ix)# num_embeddings (int):vocab_size 词典的大小# embedding_dim (int):embedding_dim 嵌入向量的维度,即用多少维来表示一个符号self.word_embeds = nn.Embedding(vocab_size, embedding_dim)# input_size: embedding_dim 输入数据的特征维数,通常就是embedding_dim(词向量的维度)# hidden_size: hidden_dim LSTM中隐藏层的维度# num_layers:循环神经网络的层数# 默认使用偏置,默认不用dropout# bidirectional = True 用双向LSTM# 设定为单层双向# 隐藏层设定为指定维度的一半,便于后期拼接# // 表示整数除法,返回不大于结果的一个最大的整数self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,num_layers=1, bidirectional=True)# 将BiLSTM提取的特征向量映射到特征空间,即经过全连接得到发射分数# in_features: hidden_dim 每个输入样本的大小# out_features:tagset_size 每个输出样本的大小self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)# 转移矩阵的参数初始化,transition[i,j]代表的是从第j个tag转移到第i个tag的转移分数self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))# 初始化所有其他tag转移到START_TAG的分数非常小,即不可能由其他tag转移到START_TAG# 初始化STOP_TAG转移到所有其他的分数非常小,即不可能有STOP_TAG转移到其他tag# CRF的转移矩阵,T[i,j]表示从j标签转移到i标签,self.transitions.data[tag_to_ix[START_TAG], :] = -10000self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000# 初始化LSTM的参数self.hidden = self.init_hidden()# 使用随机正态分布初始化LSTM的h0和c0# 否则模型自动初始化为零值,维度为[num_layers*num_directions, batch_size, hidden_dim]def init_hidden(self):return (torch.randn(2, 1, self.hidden_dim // 2),torch.randn(2, 1, self.hidden_dim // 2))# 计算归一化因子Z(x)def _forward_alg(self, feats):'''输入:发射矩阵(emission score),实际上就是LSTM的输出sentence的每个word经BiLSTM后对应于每个label的得分输出:所有可能路径得分之和/归一化因子/配分函数/Z(x)'''# 通过前向算法递推计算# 初始化1行 tagset_size列的嵌套列表init_alphas = torch.full((1, self.tagset_size), -10000.)# 初始化step 0 即START位置的发射分数,START_TAG取0其他位置取-10000init_alphas[0][self.tag_to_ix[START_TAG]] = 0.# 包装到一个变量里面以便自动反向传播forward_var = init_alphas# 迭代整个句子# feats:形似[[....], 每个字映射到tag的发射概率,#        [....],#        [....]]for feat in feats:# 存储当前时间步下各tag得分alphas_t = []for next_tag in range(self.tagset_size):# 取出当前tag的发射分数(与之前时间步的tag无关),扩展成tag维emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)# 取出当前tag由之前tag转移过来的转移分数trans_score = self.transitions[next_tag].view(1, -1)# 当前路径的分数:之前时间步分数+转移分数+发射分数next_tag_var = forward_var + trans_score + emit_score# 对当前分数取log-sum-expalphas_t.append(log_sum_exp(next_tag_var).view(1))# 更新forward_var 递推计算下一个时间步# torch.cat 默认按行添加forward_var = torch.cat(alphas_t).view(1, -1)# 考虑最终转移到STOP_TAGterminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]# 对当前分数取log-sum-expscores = log_sum_exp(terminal_var)return scores# 通过BiLSTM提取特征def _get_lstm_features(self, sentence):# 初始化LSTM的h0和c0self.hidden = self.init_hidden()# 使用之前构造的词嵌入为语句中每个词(word_id)生成向量表示# 并将shape改为[seq_len, 1(batch_size), embedding_dim]embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)# LSTM网络根据输入的词向量和初始状态h0和c0# 计算得到输出结果lstm_out和最后状态hn和cnlstm_out, self.hidden = self.lstm(embeds, self.hidden)lstm_out = lstm_out.view(len(sentence), self.hidden_dim)# 转换为词 - 标签([seq_len, tagset_size])表# 可以看作为每个词被标注为对应标签的得分情况,即维特比算法中的发射矩阵lstm_feats = self.hidden2tag(lstm_out)return lstm_feats# 计算一个tag序列路径的得分def _score_sentence(self, feats, tags):# feats发射分数矩阵# 计算给定tag序列的分数,即一条路径的分数score = torch.zeros(1)# tags前面补上一个句首标签便于计算转移得分tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])# 循环用于计算给定tag序列的分数for i, feat in enumerate(feats):# 递推计算路径分数:转移分数+发射分数# T[i,j]表示j转移到iscore = score + self.transitions[tags[i + 1], tags[i]] + feat[tags[i + 1]]# 加上转移到句尾的得分,便得到了gold_scorescore = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]return score# veterbi解码,得到最优tag序列def _viterbi_decode(self, feats):''':param feats: 发射分数矩阵:return:'''# 便于之后回溯最优路径backpointers = []# 初始化viterbi的forward_var变量init_vvars = torch.full((1, self.tagset_size), -10000.)init_vvars[0][self.tag_to_ix[START_TAG]] = 0# forward_var表示每个标签的前向状态得分,即上一个词被打作每个标签的对应得分值forward_var = init_vvars# 遍历每个时间步时的发射分数for feat in feats:# 记录当前词对应每个标签的最优转移结点# 保存当前时间步的回溯指针bptrs_t = []# 与bptrs_t对应,记录对应的最优值# 保存当前时间步的viterbi变量viterbivars_t = []# 遍历每个标签,求得当前词被打作每个标签的得分# 并将其与当前词的发射矩阵feat相加,得到当前状态,即下一个词的前向状态for next_tag in range(self.tagset_size):# transitions[next_tag]表示每个标签转移到next_tag的转移得分# forward_var表示每个标签的前向状态得分,即上一个词被打作每个标签的对应得分值# 二者相加即得到当前词被打作next_tag的所有可能得分# 维特比算法记录最优路径时只考虑上一步的分数以及上一步的tag转移到当前tag的转移分数# 并不取决于当前的tag发射分数next_tag_var = forward_var + self.transitions[next_tag]# 得到上一个可能的tag到当前tag中得分最大值的tag位置索引idbest_tag_id = argmax(next_tag_var)# 将最优tag的位置索引存入bptrs_tbptrs_t.append(best_tag_id)# 添加最优tag位置索引对应的值viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))# 更新forward_var = 当前词的发射分数feat + 前一个最优tag当前tag的状态下的得分forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)# 回溯指针记录当前时间步各个tag来源前一步的最优tagbackpointers.append(bptrs_t)# forward_var表示每个标签的前向状态得分# 加上转移到句尾标签STOP_TAG的转移得分terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]# 得到标签STOP_TAG前一个时间步的最优tag位置索引best_tag_id = argmax(terminal_var)# 得到标签STOP_TAG当前最优tag对应的分数值path_score = terminal_var[0][best_tag_id]# 根据过程中存储的转移路径结点,反推最优转移路径# 通过回溯指针解码出最优路径best_path = [best_tag_id]# best_tag_id作为线头,反向遍历backpointers找到最优路径for bptrs_t in reversed(backpointers):best_tag_id = bptrs_t[best_tag_id]best_path.append(best_tag_id)# 去除START_TAGstart = best_path.pop()# 最初的转移结点一定是人为构建的START_TAG,删除,并根据这一点确认路径正确性assert start == self.tag_to_ix[START_TAG]# 最后将路径倒序即得到从头开始的最优转移路径best_pathbest_path.reverse()return path_score, best_path# 损失函数lossdef neg_log_likelihood(self, sentence, tags):# 得到句子对应的发射分数矩阵feats = self._get_lstm_features(sentence)# 通过前向算法得到归一化因子Z(x)forward_score = self._forward_alg(feats)# 得到tag序列的路径得分gold_score = self._score_sentence(feats, tags)return forward_score - gold_score# 输入语句序列得到最佳tag路径及其得分def forward(self, sentence):  # dont confuse this with _forward_alg above.# 从BiLSTM获得发射分数矩阵lstm_feats = self._get_lstm_features(sentence)# 使用维特比算法进行解码,计算最佳tag路径及其得分score, tag_seq = self._viterbi_decode(lstm_feats)return score, tag_seqSTART_TAG = "<START>"
STOP_TAG = "<STOP>"
# 词嵌入维度
EMBEDDING_DIM = 5
# LSTM隐藏层维度
HIDDEN_DIM = 4# 训练数据
training_data = [("the wall street journal reported today that apple corporation made money".split(),"B I I I O O O B I O O".split()
), ("georgia tech is a university in georgia".split(),"B I O O O O B".split()
)]word_to_ix = {}
# 构建词索引表,数字化以便计算机处理
for sentence, tags in training_data:for word in sentence:if word not in word_to_ix:word_to_ix[word] = len(word_to_ix)# 构建标签索引表,数字化以便计算机处理
tag_to_ix = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}# 初始化模型参数
model = BiLSTM_CRF(len(word_to_ix), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM)
# 使用随机梯度下降法(SGD)进行参数优化
# model.parameters()为该实例中可优化的参数,
# lr:学习率,weight_decay:正则化系数,防止模型过拟合
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)# 在no_grad模式下进行前向推断的检测,函数作用是暂时不进行导数的计算,目的在于减少计算量和内存消耗
# 训练前检查模型预测结果
with torch.no_grad():# 取训练数据中第一条语句序列转化为数字precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)# 取训练数据中第一条语句序列对应的标签序列进行数字化precheck_tags = torch.tensor([tag_to_ix[t] for t in training_data[0][1]], dtype=torch.long)print(model(precheck_sent))# 300轮迭代训练
for epoch in range(300):for sentence, tags in training_data:# Step 1. 每次开始前将上一轮的参数梯度清零,防止累加影响model.zero_grad()# Step 2. seq、tags分别数字化为sentence_in、targetssentence_in = prepare_sequence(sentence, word_to_ix)targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)# Step 3. 损失函数lossloss = model.neg_log_likelihood(sentence_in, targets)# Step 4. 通过调用optimizer.step()计算损失、梯度、更新参数loss.backward()optimizer.step()# torch.no_grad() 是一个上下文管理器,被该语句 wrap 起来的部分将不会track 梯度
# 训练结束查看模型预测结果,对比观察模型是否学到
with torch.no_grad():precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)print(model(precheck_sent))
# We got it!

欢迎交流指正

参考资料:

[1]torch.max()使用讲解

https://www.jianshu.com/p/3ed11362b54f

[2]torch.manual_seed()用法

https://www.cnblogs.com/dychen/p/13920000.html

[3]BiLSTM-CRF原理介绍+Pytorch_Tutorial代码解析

https://blog.csdn.net/misite_j/article/details/109036725

[4]关于nn.embedding函数的理解

https://blog.csdn.net/a845717607/article/details/104752736

[5]torch.nn.LSTM()详解

https://blog.csdn.net/m0_45478865/article/details/104455978

[6]pytorch函数之nn.Linear

https://www.cnblogs.com/Archer-Fang/p/10645473.html

[7]pytorch之torch.randn()

https://blog.csdn.net/zouxiaolv/article/details/99568414

[8]torch.full()

https://blog.csdn.net/Fluid_ray/article/details/109855155

[9]PyTorch中view的用法

https://blog.csdn.net/york1996/article/details/81949843

https://blog.csdn.net/zkq_1986/article/details/100319146

[10]torch.cat()函数

https://blog.csdn.net/xinjieyuan/article/details/105208352

[11]ADVANCED: MAKING DYNAMIC DECISIONS AND THE BI-LSTM CRF

https://pytorch.org/tutorials/beginner/nlp/advanced_tutorial.html

[12]条件随机场理论理解

https://blog.csdn.net/qq_27009517/article/details/107154441

[13]PyTorch tutorial - BiLSTM CRF 代码解析

https://blog.csdn.net/ono_online/article/details/105089750

说个正事哈

由于微信平台算法改版,公号内容将不再以时间排序展示,如果大家想第一时间看到我们的推送,强烈建议星标我们和给我们多点点【在看】。星标具体步骤为:

(1)点击页面最上方深度学习自然语言处理”,进入公众号主页。

(2)点击右上角的小点点,在弹出页面点击“设为星标”,就可以啦。

感谢支持,比心

投稿或交流学习,备注:昵称-学校(公司)-方向,进入DL&NLP交流群。

方向有很多:机器学习、深度学习,python,情感分析、意见挖掘、句法分析、机器翻译、人机对话、知识图谱、语音识别等。

记得备注呦

整理不易,还望给个在看!

【NER】命名实体识别:详解BiLSTM_CRF_Pytorch_Tutorial代码相关推荐

  1. pytorch实现BiLSTM+CRF用于NER(命名实体识别)

    pytorch实现BiLSTM+CRF用于NER(命名实体识别) 在写这篇博客之前,我看了网上关于pytorch,BiLstm+CRF的实现,都是一个版本(对pytorch教程的翻译), 翻译得一点质 ...

  2. NER命名实体识别,基于数据是字典的形式怎么识别

    什么是命名实体识别: 命名实体识别(Named Entity Recognition,简称NER),又称作"专名识别,就是从文本中提取出具有特定意义的实体,主要包括人名,地名,专有名字等等. ...

  3. CLUENER 细粒度命名实体识别,附完整代码

    CLUENER 细粒度命名实体识别 文章目录 CLUENER 细粒度命名实体识别 一.任务说明: 二.数据集介绍: 2.1 数据集划分和数据内容 2.2 标签类别和定义: 2.3 数据分布 三.处理j ...

  4. 【NLP-NER】命名实体识别中最常用的两种深度学习模型

    命名实体识别(Named Entity Recognition,NER)是NLP中一项非常基础的任务.NER是信息提取.问答系统.句法分析.机器翻译等众多NLP任务的重要基础工具. 上一期我们介绍了N ...

  5. 【NLP-NER】什么是命名实体识别?

    命名实体识别(Named Entity Recognition,NER)是NLP中一项非常基础的任务.NER是信息提取.问答系统.句法分析.机器翻译等众多NLP任务的重要基础工具. 命名实体识别的准确 ...

  6. 用隐马尔可夫模型(HMM)做命名实体识别——NER系列(二)

    上一篇文章里<用规则做命名实体识别--NER系列(一)>,介绍了最简单的做命名实体识别的方法–规则.这一篇,我们循序渐进,继续介绍下一个模型--隐马尔可夫模型. 隐马尔可夫模型,看上去,和 ...

  7. 用CRF做命名实体识别——NER系列(三)

    在上一篇文章<用隐马尔可夫模型(HMM)做命名实体识别--NER系列(二)>中,我们使用HMM模型来做命名实体识别,将问题转化为统计概率问题,进行求解.显然,它的效果是非常有限的. 在深度 ...

  8. python命名实体识别工具_Day14:使用斯坦福 NER 软件包实现你自己的命名实体识别器(Named Entity Recognition,NER)...

    编者注:我们发现了有趣的一系列文章<30天学习30种新技术>,正在翻译中,一天一篇更新,年终礼包.下面是第 14 天的内容. 我并不是一个机器学习(Machine Learning).自然 ...

  9. python中文命名实体识别工具包_中文命名实体识别工具(NER)比较

    既然中文分词.词性标注已经有了,那下一步很自然想到的是命名实体识别(NER,Named-entity recognition)工具了,不过根据我目前了解到的情况,开源的中文命名实体工具并不多,这里主要 ...

  10. 命名实体识别新SOTA:改进Transformer模型

    2019-11-27 05:02:16 作者 | 刘旺旺 编辑 | 唐里 TENER: Adapting Transformer Encoder for Name Entity Recognition ...

最新文章

  1. mqtt+htttp+websocket
  2. OI常用的常数优化小技巧
  3. 机器学习笔记: Discriminative vs Generative Models
  4. xgboost之spark上运行-scala接口
  5. [vue] vue和微信小程序写法上有什么区别?
  6. SharePoint关于publish page, WiKi page, Web part page区别
  7. 字符串、文件操作,英文词频统计预处理
  8. 二阶偏微分方程组 龙格库塔法_数值方法(MATLAB版)(原书第3版)[Numerical Methods Using MATLAB,Third Edition]pdf...
  9. Android 控件 之 菜单(Menu)
  10. 将beyond compare设置为svn的代码比较工具
  11. IDEA2019开发WebService实例
  12. 电脑变慢,电脑越来越慢怎么办 电脑用久了网速变慢如何解决
  13. 前端微信公众号开发,成功拿到了字节跳动、腾讯大厂offer
  14. fiddler证书下载(模拟器)
  15. 我学历低,学软件测试能找到工作吗?
  16. 360浏览器中页面打开如何默认极速模式
  17. Java学习日记1——基础认知
  18. Linux下用C语言实现文件的写入
  19. FastRule: Efficient Flow Entry Updates for TCAM-based OpenFlow Switches(二)
  20. 淘宝运营 钻展基本了解 钻展投放方案 钻展投放注意的点

热门文章

  1. Python删除文件第一行
  2. java byte数组与String互转
  3. Excel的实用函数
  4. 简单的Jquery焦点图切换效果
  5. Windows上SVN服务器搭建【转】
  6. 20190825 On Java8 第十二章 集合
  7. java day29【DBC基本概念 、快速入门 、对JDBC中各个接口和类详解】
  8. MySQL 入门教程
  9. 移动硬盘提示由于IO设备错误,无法运行此项请求要怎么办啊
  10. BZOJ3697 采药人的路径 【点分治】