nlp系列(6)文本实体识别(Bi-LSTM+CRF)pytorch

news2024/11/23 3:12:18

模型介绍

LSTM:长短期记忆网络(Long-short-term-memory),能够记住长句子的前后信息,解决了RNN的问题(时间间隔较大时,网络对前面的信息会遗忘,从而出现梯度消失问题,会形成长期依赖问题),避免长期依赖问题。
Bi-LSTM:由前向LSTM与后向LSTM组合而成。

模型结构

Bi-LSTM

同LSTM,区别在于模型的输出和结构上不同,如下图:

图1 Bi-LSTM的数据输入形式

一共有两个LSTM网络,一个网络从一句话的首段进行学习,另一个网络从一句话的末端进行学习。

相关详情请看nlp系列(5)文本实体识别(LSTM)pytorch 中模型详解

CRF

CRF(条件随机场):是一个判别模型,用于解决标注偏差问题,使用P(Y|X)建模,为全局归一化
适用领域:词性标注、分词、命名实体识别等
以命名实体为例:
在这里插入图片描述
损失计算:
lg ⁡ P ( Y ∣ X ) = − l g e s ( X , Y ) ∑ y ‾ ϵ Y x e s ( X , y ‾ ) = − S ( X , y ) + lg ⁡ ∑ y ‾ ϵ Y x e s ( X , y ‾ ) \lg P(Y|X) = -lg \frac{e^s(X,Y)}{\sum_{\overline{y}\epsilon Y_x}{e^s(X,\overline y)}} = - S(X, y) + \lg\sum_{\overline{y}\epsilon Y_x}{e^s(X,\overline y)} lgP(YX)=lgyϵYxes(X,y)es(X,Y)=S(X,y)+lgyϵYxes(X,y)
推荐一个视频讲解,全程手写推导,讲得很细
机器学习-白板推导系列(十七)-条件随机场CRF(Conditional Random Field)

数据介绍

数据集用的是论文【ACL 2018Chinese NER using Lattice LSTM】中从新浪财经收集的简历数据。每一句话用换行进行隔开。

图2 数据样式

模型准备

方法一:使用ptorch库自带的CRF库,其CRF库关键函数介绍链接

    def forward(self, sentence, tags=None, mask=None):
        # sentence=(batch, seq_len)   tags=(batch, seq_len)  masks=(batch, seq_len)
        # 1. 从 sentence 到 Embedding 层
        embeds = self.word_embeds(sentence).permute(1, 0, 2)  # shape [seq_len, batch_size, embedding_size]

        # 2. 从 Embedding 层到 Bi-LSTM 层
        # Bi-lstm 层的隐藏节点设置
        # 隐藏层就是(h_0, c_0)    num_directions = 2 if self.bidirectional else 1
        # h_0 的结构:(num_layers*num_directions, batch_size, hidden_size)
        self.hidden = (torch.randn(2, sentence.shape[0], self.hidden_dim // 2, device=self.device),
                       torch.randn(2, sentence.shape[0], self.hidden_dim // 2, device=self.device))

        # input=(seq_length, batch_size, embedding_num)
        # output(lstm_out)=(seq_length, batch_size, num_directions * hidden_size)
        # h_0 = (num_layers*num_directions, batch_size, hidden_size)
        lstm_out, self.hidden = self.lstm(embeds, self.hidden)

        # 3. 从 Bi-LSTM 层到全连接层
        # 从 Bi-lstm 的输出转为 target_size 长度的向量组(即输出了每个 tag 的可能性)
        # 输出 shape=(seq_length, batch_size, len(tag_to_ix))
        lstm_feats = self.linear(lstm_out)

        # 4. 全连接层到 CRF 层
        if tags is not None:
            # 训练用
            if mask is not None:
                loss = -1. * self.crf(emissions=lstm_feats.permute(1, 0, 2), tags=tags, mask=mask, reduction='mean')
                # outputs=(batch_size,)   输出 log 形式的 likelihood
            else:
                loss = -1. * self.crf(emissions=lstm_feats.permute(1, 0, 2), tags=tags, reduction='mean')
            return loss
        else:
            # 测试
            if mask is not None:
                prediction = self.crf.decode(emissions=lstm_feats.permute(1, 0, 2), mask=mask)
            else:
                prediction = self.crf.decode(emissions=lstm_feats.permute(1, 0, 2))
            return prediction

方法2:编写CRF实现代码

def argmax(vec):
    """
    返回 vec 中每一行最大的那个元素的下标
    """
    # return the argmax as a python int
    _, idx = torch.max(vec, 1)
    # 获取该元素:tensor只有一个元素才能调用item方法
    return idx.item()


def log_sum_exp(vec, device):
    """
    vec 维度为 1*5
    Compute log sum exp in a numerically stable way for the forward algorithm
    前向算法是不断累积之前的结果,这样就会有个缺点
    指数和累积到一定程度后,会超过计算机浮点值的最大值,变成inf,这样取log后也是inf
    为了避免这种情况,用一个合适的值clip去提指数和的公因子,这样就不会使某项变得过大而无法计算
    计算一维向量 vec 与其最大值的 log_sum_exp
    """
    max_score = vec[0, argmax(vec)]  # max_score的维度为1
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])  # 维度为 1*5
    return max_score.to(device) + torch.log(torch.sum(torch.exp(vec - max_score_broadcast))).to(device)


class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_index, embedding_dim, hidden_dim):
        # 调用父类的init
        super(BiLSTM_CRF, self).__init__()
        self.embedding_dim = embedding_dim  # word embedding dim  嵌入维度: 词向量维度
        self.hidden_dim = hidden_dim  # Bi-LSTM hidden dim  隐藏层维度
        self.vocab_size = vocab_size  # 词汇量大小
        self.tag_to_index = tag_to_index  # 标签转下标的词典
        self.target_size = len(tag_to_index)  # 输出维度:目标取值范围大小,标签预测类别数
        self.device = "cuda:0" if torch.cuda.is_available() else "cpu"

        ''' Embedding 的用法
        A simple lookup table that stores embeddings of a fixed dictionary and size.
        This module is often used to store word embeddings and retrieve them using indices. 
        The input to the module is a list of indices, and the output is the corresponding word embeddings.
        一个简单的查找表,用于存储固定字典和大小的嵌入。该模块通常用于存储词嵌入并使用索引检索它们。模块的输入是索引列表,输出是相应的词嵌入。
        requires_grad: 用于说明当前量是否需要在计算中保留对应的梯度信息
        '''
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)

        '''
        embedding_dim:特征维度
        hidden_dim:隐藏层层数
        num_layers:循环层数
        bidirectional:是否采用 Bi-LSTM(前向LSTM+反向LSTM)
        '''
        self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=1, bidirectional=True)

        # 将 Bi-LSTM 提取的特征向量映射到特征空间,即经过全连接得到发射分数
        self.hidden2tag = nn.Linear(hidden_dim, self.target_size)

        # 转移矩阵的参数初始化,transitions[i,j]代表的是从第j个tag转移到第i个tag的转移分数
        # 转移矩阵是随机的,在网络中会随着训练不断更新
        self.transitions = nn.Parameter(torch.randn(self.target_size, self.target_size))

        # 初始化所有其他 tag 转移到 START_TAG 的分数非常小,即不可能由其他 tag 转移到 START_TAG
        # 初始化 STOP_TAG 转移到所有其他 tag 的分数非常小,即不可能由 STOP_TAG 转移到其他 tag
        # 转移矩阵: 列标 转 行标
        # 规定:其他 tag 不能转向 start,stop 也不能转向其他 tag
        self.transitions.data[self.tag_to_index[START_TAG], :] = -10000  # 从任何标签转移到 START_TAG 不可能
        self.transitions.data[:, self.tag_to_index[STOP_TAG]] = -10000  # 从 STOP_TAG 转移到任何标签不可能

        # 初始化 hidden layer
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 初始化 Bi-LSTM 的参数 h_0, c_0
        return (torch.randn(2, 1, self.hidden_dim // 2).to(self.device),
                torch.randn(2, 1, self.hidden_dim // 2).to(self.device))

    def _get_lstm_features(self, sentence):
        # 通过 Bi-LSTM 提取特征
        self.hidden = self.init_hidden()
        embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)

        '''
        默认参数意义:input_size,hidden_size,num_layers
        hidden_size : LSTM在运行时里面的维度。隐藏层状态的维数,即隐藏层节点的个数
        torch里的LSTM单元接受的输入都必须是3维的张量(Tensors):
           第一维体现的每个句子的长度,即提供给LSTM神经元的每个句子的长度,如果是其他的带有带有序列形式的数据,则表示一个明确分割单位长度,
           第二维度体现的是batch_size,即每一次给网络句子条数
           第三维体现的是输入的元素,即每个具体的单词用多少维向量来表示
        '''
        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

    def _score_sentence(self, feats, tags):
        """
        CRF 的输出,即 emit + transition scores
        """
        # 计算给定 tag 序列的分数,即一条路径的分数
        score = torch.zeros(1).to(self.device)
        tags = torch.cat([torch.tensor([self.tag_to_index[START_TAG]], dtype=torch.long).to(self.device), 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_index[STOP_TAG], tags[-1]]
        return score

    def _forward_alg(self, feats):  # 预测序列的得分,就是 Loss 的右边第一项
        """
        前向算法:feats 表示发射矩阵(emit score),是 Bi-LSTM 所有时间步的输出 意思是经过 Bi-LSTM 的 sentence 的每个 word 对应于每个 label 的得分
        """
        # 通过前向算法递推计算 alpha 初始为 -10000
        init_alphas = torch.full((1, self.target_size), -10000.).to(self.device)  # 用-10000.来填充一个形状为[1,target_size]的tensor

        # 初始化 step 0 即 START 位置的发射分数,START_TAG 取 0 其他位置取 -10000  start 位置的 alpha 为 0
        # 因为 start tag 是4,所以tensor([[-10000., -10000., -10000., 0., -10000.]]),
        # 将 start 的值为零,表示开始进行网络的传播,
        init_alphas[0][self.tag_to_index[START_TAG]] = 0.
        # 将初始化 START 位置为 0 的发射分数赋值给 previous  包装进变量,实现自动反向传播
        previous = init_alphas

        # 迭代整个句子
        for obs in feats:
            # The forward tensors at this timestep
            # 当前时间步的前向 tensor
            alphas_t = []
            for next_tag in range(self.target_size):
                # 取出当前tag的发射分数,与之前时间步的tag无关
                '''
                Bi-LSTM 生成的矩阵是 emit score[观测/发射概率], 即公式中的H()函数的输出
                CRF 是判别式模型
                emit score: Bi-LSTM 对序列中每个位置的对应标签打分的和
                transition score: 是该序列状态转移矩阵中对应的和
                Score = EmissionScore + TransitionScore
                '''
                # Bi-LSTM的生成矩阵是 emit_score,维度为 1*5
                emit_score = obs[next_tag].view(1, -1).expand(1, self.target_size).to(self.device)

                # 取出当前 tag 由之前 tag 转移过来的转移分数
                trans_score = self.transitions[next_tag].view(1, -1)

                # 当前路径的分数:之前时间步分数 + 转移分数 + 发射分数
                next_tag_var = previous.to(self.device) + trans_score.to(self.device) + emit_score.to(self.device)

                # 对当前分数取 log-sum-exp
                alphas_t.append(log_sum_exp(next_tag_var, self.device).view(1))

            # 更新 previous 递推计算下一个时间步
            previous = torch.cat(alphas_t).view(1, -1)
        # 考虑最终转移到 STOP_TAG
        terminal_var = previous + self.transitions[self.tag_to_index[STOP_TAG]]
        # 计算最终的分数
        scores = log_sum_exp(terminal_var, self.device)
        return scores.to(self.device)

    def _viterbi_decode(self, feats):
        """
        Decoding的意义:给定一个已知的观测序列,求其最有可能对应的状态序列
        """
        # 预测序列的得分,维特比解码,输出得分与路径值
        backpointers = []

        # 初始化 viterbi 的 previous 变量
        init_vvars = torch.full((1, self.target_size), -10000.).cpu()  # 这就保证了一定是从START到其他标签
        init_vvars[0][self.tag_to_index[START_TAG]] = 0

        # 第 i 步的 forward_var 保存第 i-1 步的维特比变量
        previous = init_vvars

        for obs in feats:
            # 保存当前时间步的回溯指针
            bptrs_t = []
            # 保存当前时间步的 viterbi 变量
            viterbivars_t = []

            for next_tag in range(self.target_size):
                # 其他标签(B,I,E,Start,End)到标签next_tag的概率
                # 维特比算法记录最优路径时只考虑上一步的分数以及上一步 tag 转移到当前 tag 的转移分数
                # 并不取决与当前 tag 的发射分数
                next_tag_var = previous.cpu() + self.transitions[next_tag].cpu()  # previous 保存的是之前的最优路径的值
                # 找到此刻最好的状态转入点
                best_tag_id = argmax(next_tag_var)  # 返回最大值对应的那个tag
                # 记录点
                bptrs_t.append(best_tag_id)
                viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

            # 更新 previous,加上当前 tag 的发射分数 obs
            # 从 step0 到 step(i-1) 时 5 个序列中每个序列的最大 score
            previous = (torch.cat(viterbivars_t).cpu() + obs.cpu()).view(1, -1)
            # 回溯指针记录当前时间步各个 tag 来源前一步的 tag
            backpointers.append(bptrs_t)

        # 考虑转移到 STOP_TAG 的转移分数
        # 其他标签到STOP_TAG的转移概率
        terminal_var = previous.cpu() + self.transitions[self.tag_to_index[STOP_TAG]].cpu()
        best_tag_id = argmax(terminal_var)
        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_TAG
        start = best_path.pop()
        assert start == self.tag_to_index[START_TAG]  # Sanity check
        best_path.reverse()  # 把从后向前的路径正过来
        return path_score, best_path

    def neg_log_likelihood(self, sentence, tags):
        # CRF 损失函数由两部分组成,真实路径的分数和所有路径的总分数。
        # 真实路径的分数应该是所有路径中分数最高的。
        # log 真实路径的分数/log所有可能路径的分数,越大越好,构造 crf loss 函数取反,loss 越小越好
        feats = self._get_lstm_features(sentence)  # 经过LSTM+Linear后的输出作为CRF的输入
        # 前向算法分数
        forward_score = self._forward_alg(feats)  # loss的log部分的结果
        # 真实分数
        gold_score = self._score_sentence(feats, tags)  # loss的后半部分S(X,y)的结果
        # log P(y|x) = forward_score - gold_score
        return forward_score - gold_score

    # 这里 Bi-LSTM 和 CRF 共同前向输出
    def forward(self, sentence):
        """
        重写原 module 里的 forward
        """
        sentence = sentence.reshape(-1)
        # 通过 Bi-LSTM 提取发射分数
        lstm_feats = self._get_lstm_features(sentence)
        # 根据发射分数以及转移分数,通过 viterbi 解码找到一条最优路径
        score, tag_seq = self._viterbi_decode(lstm_feats)
        return score, tag_seq

模型预测

注:模型只训练了一轮,预测结果与实际会有差异。
方法一:
在这里插入图片描述

图3 方法1预测结果
方法二:

在这里插入图片描述

图4 方法2预测结果

源码获取

Bi-LSTM-CRF 实体识别

硬性的标准其实限制不了无限可能的我们,所以啊!少年们加油吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/785991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux文件管理

WINDOWS/LINUX目录对比 Windows: 以多根的方式组织文件 C:\ D:\ E: Linux: 以单根的方式组织文件 / (根目录) Linux目录简介 /目录结构: FSH (Filesystem Hierarchy Standard) [rootlocalhost ~]# ls / bin dev lib media net root srv usr boot etc lib64 misc …

Qt5.14.2下载及安装

1. 下载 https://download.qt.io/archive/qt/5.14/5.14.2/ 由于Qt 自从5.15版本开始,对非商业版本(也就是开源版本),不提供已经制作好的离线exe安装包。所以,对于5.15(含)之后的版本&#xff…

混合背包--暗黑游戏(pgrune)

混合背包&#xff1a;包含着01背包&#xff0c;完全背包,多重背包 而这个题通过k[i]进行判断是哪个背包&#xff0c;少了个完全背包。 #include<bits/stdc.h> using namespace std; const int N1000; int vp[N]; int vr[N]; int k[N]; int w[N]; int f[151][151]; int m…

C++模拟实现list

1.首先要了解到vs底层的list链表是带头双向循环的链表。 所以首先就要看成员变量 那么就说明我们还需要构造一个Node的结构体&#xff0c;&#xff08;typedef一下就好了&#xff0c;名字不影响&#xff09; 现在就可以完成间的push_back函数了。 1.list的iterator 我们之前模…

随手笔记——3D−2D:PnP

随手笔记——3D−2D&#xff1a;PnP 说明理论源代码雅可比矩阵求解 说明 PnP&#xff08;Perspective-n-Point&#xff09;是求解3D到2D点对运动的方法。它描述了当知道n个3D空间点及其投影位置时&#xff0c;如何估计相机的位姿。 理论 特征点的3D位置可以由三角化或者RGB-…

鸿鹄协助管理华为云与炎凰Ichiban

炎凰对华为云的需求 在炎凰日常的开发中&#xff0c;对于服务器上的需求&#xff0c;我们基本都是采用云服务。目前我们主要选择的是华为云&#xff0c;华为云的云主机比较稳定&#xff0c;提供的云主机配置也比较多样&#xff0c;非常适合对于不同场景硬件配置的需求&#xff…

【前端笔记】本地运行cli项目报错ERR_OSSL_EVP_UNSUPPORTED

报错原因 Node版本>17.x&#xff0c;本地npm run 起项目后会发现终端报错&#xff0c;具体有以下2块关键信息&#xff1a; Error: error:0308010C:digital envelope routines::unsupported和 opensslErrorStack: [ error:03000086:digital envelope routines::initializa…

Jmeter配置起来太繁琐?试试RunnerGo

在用jmeter做性能测试时想看完整一点的测试报告&#xff0c;想配置阶梯模式来压测&#xff0c;想配置不同的接口并发这些都需要安装插件并且影响机器性能&#xff0c;想做自动化测试还得放到jenkins&#xff0c;这些配置起来太繁琐。今天给大家推荐一款测试平台RunnerGo&#x…

可解释的 AI:在transformer中可视化注意力

Visualizing Attention in Transformers | Generative AI (medium.com) 一、说明 在本文中&#xff0c;我们将探讨可视化变压器架构核心区别特征的最流行的工具之一&#xff1a;注意力机制。继续阅读以了解有关BertViz的更多信息&#xff0c;以及如何将此注意力可视化工具整合到…

B074-详情富文本 服务上下架 高级查询 分页 查看详情

目录 服务详情修改优化ProductServiceImplProduct.vue 详情数据-富文本-vue-quill-editor使用步骤测试图片的访问方式富文本集成fastDfs 后台服务上下架&#xff08;批量&#xff09;前端开始后端完成ProductControllerProductServiceImplProductMapper 前台展示上架前端开始后…

【雕爷学编程】Arduino动手做(171)---micro:bit 开发板3

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#xff0c;这…

Jmeter 如何并发执行 Python 脚本

目录 1. 前言 2. Python 实现文件上传 3. Jmeter 并发执行 4. 最后 1. 前言 JMeter 是一个开源性能测试工具&#xff0c;它可以帮助我们更轻松地执行性能测试&#xff0c;并使测试结果更加可靠。Python 是一种广泛使用的编程语言&#xff0c;它可以用于开发各种软件和应用…

ResultMap结果集映射

为了解决属性名和字段名不相同的问题 example&#xff1a;MyBatis-CRUD: Mybatis做增删改查 使用resultmap前查询password时为空&#xff0c;因为属性名与字段名不相同 做结果集映射&#xff1a; <?xml version"1.0" encoding"UTF-8" ?> <!…

自己动手写一个编译器

一、概述 本文将参考《自己动手写编译器这本书》&#xff0c;自己写一个编译器&#xff0c;但是因为本人水平有限。文章中比较晦涩的内容&#xff0c;自己也没弄明白。因此&#xff0c;本文仅在实践层跑一遍流程。具体的原理还需要大家自行探索。 TinyC 编译器可将 TinyC 源程序…

JavaScript 判断先后两个数组增加和减少的元素

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

错误解决:Failed to create Spark client for Spark session

错误解决&#xff1a;Failed to create Spark client for Spark session "Failed to create Spark client for Spark session"的错误通常表示无法为Spark会话创建Spark客户端。这可能是由于以下一些常见问题导致的&#xff1a; Spark配置错误&#xff1a;请检查Spar…

SAMStable-Diffusion集成进化!分割、生成一切!AI绘画新玩法

自SAM「分割一切」模型推出之后&#xff0c;二创潮就开始了&#xff0c;有想法有行动&#xff01;飞桨AI Studio开发者会唱歌的炼丹师就创作出SAM进化版&#xff0c;将SAM、Stable Diffusion集成&#xff0c;实现「分割」、「生成」能力二合一&#xff0c;并部署为应用&#xf…

vue项目入口和个文件之间的关系

vue项目入口和个文件之间的关系 1、代码的执行顺序和引入关系 1、代码的执行顺序和引入关系

新星计划打卡学习:VUE3引入element-plus

目录 1、安装element-plus 2、安装按需导入插件 3、修改配置文件 4、添加页面内容 5、保存并重启项目 1、安装element-plus 官网说要想使用element-plus需要先进行安装&#xff0c;并给出了三种安装方式&#xff0c;我选择了第三种。 报错了&#xff1a; 解决的办法&…

PostgreSQL 设置时区,时间/日期函数汇总

文章目录 前言查看时区修改时区时间/日期操作符和函数时间/日期操作符日期/时间函数&#xff1a;extract&#xff0c;date_part函数支持的field 数据类型格式化函数用于日期/时间格式化的模式&#xff1a; 扩展 前言 本文基于 PostgreSQL 12.6 版本&#xff0c;不同版本的函数…