当前位置:   article > 正文

NLP-Beginner任务四学习笔记:基于LSTM+CRF的序列标注_利用lstm-crf网络完成序列标注

利用lstm-crf网络完成序列标注

**用LSTM+CRF来训练序列标注模型:以Named Entity Recognition为例**

数据集:CONLL 2003,https://www.clips.uantwerpen.be/conll2003/ner/

任务一博客链接:https://blog.csdn.net/qq_51983316/article/details/129314052

任务二博客链接:https://blog.csdn.net/qq_51983316/article/details/129387225

任务三博客链接:https://blog.csdn.net/qq_51983316/article/details/129470730

参考论文:
1、https://arxiv.org/pdf/1603.01354.pdf
2、https://arxiv.org/pdf/1603.01360.pdf

目录

一、数据集 

二、知识点学习 

(一)命名实体识别任务(NER)

1、基本概念

2、主要方法

(二)条件随机场(CRF)

1、基本概念

2、基于CRF的命名实体识别

(三)LSTM+CRF

1、模型原理

2、模型详解

3、CRF损失函数

(四)模型评估

1、混淆矩阵

2、精确率(Precision)

3、准确度(Accuracy)

4、召回率(Recall)

5、F1-score

三、实验

(一)代码实现

1、main.py

2、utils.py

(二)结果分析

一、数据集 

The first item on each line is a word, the second a part-of-speech (POS) tag, the third a syntactic chunk tag and the fourth the named entity tag.

原始数据集共有三个文件:train.txt, test.txt 和 dev.txt

CoNLL-2003共享任务数据文件包含由一个空格分隔的四列。每个单词都放在单独的一行,每个句子后面都有一个空行。每行上的第一项是一个单词,第二项是词性标签,第三项是句法组块标签,第四项是命名实体标签。块标记和命名实体标记的格式为I-TYPE,这意味着单词位于TYPE类型的短语内。只有当同一类型的两个短语紧跟在一起时,第二个短语的第一个单词才会有标记B-type,以显示它开始了一个新短语。标记为O的单词不是短语的一部分。下面是一个示例: 

原数据
Peter NNP B-NP B-PER
Blackburn NNP I-NP I-PER
输入文本: Peter Blackburn
输出序列: B-PER I-PER


原数据:
EU NNP B-NP B-ORG
rejects VBZ B-VP O
German JJ B-NP B-MISC
call NN I-NP O
to TO B-VP O
boycott VB I-VP O
British JJ B-NP B-MISC
lamb NN I-NP O
输入文本: EU rejects German call to boycott British lamb.
输出序列: B-ORG O B-MISC O O O B-MISC O O

二、知识点学习 

(一)命名实体识别任务(NER)

1、基本概念

命名实体识别(Named Entity Recognition,NER)是信息提取、问答系统、句法分析、机器翻译等 NLP 应用领域的重要基础工具。一般而言,NER的任务就是识别出待处理文本中三大类(实体类、时间类和数字类)七小类(人名、机构名、地名、时间、日期、货币和百分比)命名实体。

NER英文命名实体识别的目标是识别句子中每个词语的实体类型,包括5大类:PER(人名)、LOC(地名)、ORG(组织名)、MISC(其它类型实体)、O(非实体)

由于实体可能是由多个词语构成的,因此使用标注B、l来区分该词语是该实体的起始词(Begin)还是中间词(Inside) 

但随着 NLP 任务的不断扩充,在特定领域中会出现特定的类别,比如医药领域中,药名、疾病名称等类别。同时,实体类型是根据需求人为定义的,这种定义可以是有层次的。例如,产品类是一个大类,下面可能会包含手机类、电脑类、照相机类等等。这种定义就是本体建模。

NER任务常转化为序列标注问题,利用BIO、BIOES和BMES等常用的标注规则对经过分词的文本进行token标注。序列标注的命名实体识别方法中,CNN、BERT、LSTM等深度模型与条件随机场CRF结合已经成为最主流和普遍的方法。

2、主要方法

(1)基于规则和字典的方法:规则的设计一般基于句法、语法、词汇的模式及特定领域的知识。 制定好规则和词典后,通常使用匹配的方式对文本进行处理以实现命名实体识别。
(2)基于传统机器学习的方法,命名实体识别往往被视作序列标注问题,主要有以下方法: 

  • 隐马尔可夫模型(Hidden Markov Model, HMM)
  • 最大熵(Maximum Entropy, ME) 
  • 最大熵马尔可夫模型(Maximum Entropy Markov Model, MEMM) 
  • 支持向量机(Support Vector Machine, SVM)
  • 条件随机场( Conditional Random Fields, CRF) 

(3)基于深度学习的方法,近年往往在基于神经网络的结构上加入注意力机制、图神经网络、迁移学习、远监督学习等热门研究技术进行NER任务的实现,主要有以下方法: 

  • BiLSTM-CRF
  • IDCNN-CRF
  • CAN-NER
  • BERT-Attention

(二)条件随机场(CRF)

1、基本概念

条件随机场(CRF)是一种在已知一组输入随机变量条件的情况下,输出另一组随机变量的条件概率分布模型;其前提是假设输出随机变量构成马尔可夫随机场;条件随机场可以应用于不同类型的标注问题,例如:单个目标的标注、序列结构的标注和图结构的标注等。

CRF模型是在隐马尔可夫模型(HMM)模型的基础上发展起来的。根据HMM模型的齐次马尔科夫性假设:假设隐藏的马尔科夫链在任意时刻 t 的状态只依赖于其前一时刻的状态,与其他时刻的状态及观测无关,也与时刻 t 无关。但CRF模型不仅考虑前一时刻的状态,还考虑其前面与后面的多个状态。因此一般来说,CRF会具有更好的标记性能。

假设训练集为 x,对应的标记序列为 y 以及多个特征函数,CRF 模型参数 \lambda _j 和u_k 的参数形式和条件概率分布 P(y|x) 公式如下:

在这里插入图片描述

可以看出 CRF 有两类特征函数:其中一类是定义在 y 上下文的局部特征函数,这类特征函数和当前节点和上一节点有关,J 是定义在该节点的局部特征函数的总个数,i 是当前节点在序列的位置,公式如下:

另一类特征函数是定义在 y 节点上的节点特征函数,只和当前节点有关,K 是定义在该节点的节点特征函数的总个数,i 是当前节点在序列的位置,公式如下:

2、基于CRF的命名实体识别

采用 CRF 模型对每个字标注对应实体类型的BIO标记,BIO 标记有 B-Person-人名的开始部分、I-Person-人名的中间部分、B-Organization-组织机构的开始部分、I-Organization-组织机构的中间部分和 O-非实体信息。

例如:句子“白居易是中国杰出的诗人”,其对应的观察序列和标注序列如下所示:

根据 CRF 的特征函数可以构建上下文特征、词本身特征和词性特征等,如下:

(三)LSTM+CRF

1、模型原理

BiLSTM-CRF 的命名实体识别模型架构图如下:(来自论文《Neural Architectures for Named Entity Recognition》),每个句子按照词序逐个输入双向LSTM中,结合正反向隐层输出得到每个词属于每个实体类别标签的概率,输入CRF中,优化目标函数,从而得到每个词所属的实体类别。

其中 BiLSTM 层的输入是每个词的向量表示;BiLSTM 层的输出是当前时刻的输入属于每个实体类别标签的概率;CRF 层主要负责计算得分,确定最终的标注序列。

2、模型详解

假设 x 是一个短句子序列,共包含5个单词,记为(w0,w1,w2,w3,w4)。其中,x 中 [w0,w1] 是人名,[w3] 是组织机构名称,其他非实体均为“O”。

第一层为词嵌入层。句中的每个单词是一条包含词嵌入和字嵌入的词向量,词嵌入通常是事先训练好的,字嵌入则是随机初始化的。所有的嵌入都会随着训练的迭代过程被调整。

第二层为 BiLSTM 编码。该层的输入是将单个词w0通过onehot编码,词嵌入之后的k维稠密向量,BiLSTM层的输出表示该单词对应各个类别的分数,见下图:

其中,W0,BiLSTM节点的输出是1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization) and 0.05 (O),输出这些标签的概率得分后将其传入CRF层

第三层为CRF打分层。所有的经 BiLSTM 层输出的分数将作为 CRF 层的输入,类别序列中分数最高的类别就是预测的最终结果。

从上述描述来看,BiLSTM已然可以完成分类和序列标注工作,那CRF的作用又体现在哪里呢,是不是也可以去掉CRF层?

因为BiLSTM模型的结果是单词对应各类别的分数,我们可以选择分数最高的类别作为预测结果。如W0,“B-Person”的分数最高(1.5),那么我们可以选定“B-Person”作为预测结果。同样的,w1是“I-Person”, w2是“O”,w3是 “B-Organization” ,w4是 “O”。尽管我们在该例子中得到了正确的结果,如左图。但实际情况并不总是这样,如右图。

很明显,右图BiLSTM的输出并不正确。因此,CRF是必要的。

CRF能为BiLSTM提供转换的一些约束,拿词性标注举例子,形容词后面大概率会跟名词,名词后面大概率会跟动词,还可以提供整个序列的概率统计,前面出现了某些单词后,这个位置应该是谓词。因为LSTM只是对每一步的向量进行过滤和叠加,这些信息是LSTM不好捕捉的。

CRF层的约束可以在训练数据时被CRF层自动学习得到

可能的约束条件如下,有了这些有用的约束,错误的预测序列将会大大减少。

  • 句子的开头应该是“B-”或“O”,而不是“I-”。
  • “B-label1 I-label2 I-label3…”,在该模式中,类别1,2,3应该是同一种实体类别。比如,“B-Person I-Person” 是正确的,而“B-Person I-Organization”则是错误的。
  • “O I-label”是错误的,命名实体的开头应该是“B-”而不是“I-”。

因此,BiLSTM可以捕捉长距离的上下文信息,对于每一步能获得很好的语意向量,CRF是对整个序列进行似然概率统计,并且捕捉转换信息,将两者结合起来可以互补不足。

3、CRF损失函数

CRF损失函数两部分组成真实路径的分数 和 所有路径的总分数。真实路径的分数应该是所有路径中分数最高的。其中每条路径上的损失函数都包括两种类型的分数(状态分数+转移分数)。

(1)状态分数(Emission score):也称发射分数(状态分数),该分数来自BiLSTM层的输出,如上左图所示,w0被预测为B-Person的分数是1.5,该分数为发射分数。

(2)转移分数:表示从一个类别转移至另一个类别的得分,所有类别间的转移分数矩阵如下:【为了使转移分数矩阵更具鲁棒性,我们加上START 和 END两类标签。START代表一个句子的开始,END代表一个句子的结束】

实际上,转移矩阵是BiLSTM-CRF模型的一个参数。在训练模型之前,你可以随机初始化转移矩阵的分数。这些分数将随着训练的迭代过程被更新,换言之,CRF层可以自己学到这些约束条件

总体而言,CRF 的损失函数计算公式如下所示:

在这里插入图片描述

其中,对于5个词组成的句子,假定类别标签有5个(B-Person, I-Person, B-Organization, I-Organization, O),其可能的类别序列有5^{5}=3125,即 N = 3125。


总之,LSTM-CRF的模型结构如下:(来自论文《End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF》)

LSTM+CRF模型用字符级别的embedding作为输入捕捉字符信息,然后将其跟单词embedding拼接起来作为BiLSTM的输入。然后BiLSTM的输出给到CRF模型来进行序列的联合解码。BiLSTM的输入和输出都加了dropout。实验表明dropout可以有效提升模型表现。

from "End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF" 

代码实现:

  1. import torch
  2. import torch.nn as nn
  3. from torch.nn.utils.rnn import pack_padded_sequence
  4. from torch.nn.utils.rnn import pad_packed_sequence
  5. from torchcrf import CRF # pytorch-crf包提供了一个CRF层的PyTorch版本实现
  6. """定义神经网络模型类 LSTM_CRF"""
  7. class LSTM_CRF(nn.Module):
  8. # vocab_size: 词汇表的大小(即词汇量)。
  9. # tag_to_index: 一个字典,将标签映射到索引。
  10. # embedding_size: 嵌入层的维数。
  11. # hidden_size: 隐藏层的大小。
  12. # max_length: 句子的最大长度。
  13. # vectors: 预训练词向量(默认为None)。
  14. def __init__(self, vocab_size, tag_to_index, embedding_size, hidden_size, max_length, vectors=None):
  15. # 调用父类的初始化函数
  16. super(LSTM_CRF, self).__init__()
  17. self.embedding_size = embedding_size
  18. self.hidden_size = hidden_size
  19. self.vocab_size = vocab_size
  20. # 标签到索引的映射字典
  21. self.tag_to_index = tag_to_index
  22. # 标签数量
  23. self.target_size = len(tag_to_index)
  24. # 初始化嵌入层,如果提供了预训练词向量,则使用预训练词向量进行初始化
  25. if vectors is None:
  26. self.embedding = nn.Embedding(vocab_size, embedding_size)
  27. else:
  28. self.embedding = nn.Embedding.from_pretrained(vectors)
  29. # 初始化BiLSTM模型
  30. # hidden_size // 2 是整除运算符,表示将 hidden_size 的值除以 2 并向下取整,得到的结果作为新的 hidden_size 的值。
  31. # 这个操作的目的是将 LSTM 层的 hidden_size 拆成两个部分,以便实现双向 LSTM。
  32. # 如果 hidden_size 的值为 100,则 hidden_size // 2 的值为 50,即每个方向上的 LSTM 的 hidden_size 值都是 50。
  33. self.lstm = nn.LSTM(embedding_size, hidden_size // 2, bidirectional=True)
  34. # 定义一个全连接层,将隐藏层的输出映射到标签的数量
  35. self.hidden_to_tag = nn.Linear(hidden_size, self.target_size)
  36. # 定义条件随机场层
  37. self.crf = CRF(self.target_size, batch_first=True)
  38. # 存储最大句子长度
  39. self.max_length = max_length
  40. # 定义函数,根据给定的句子长度列表生成一个掩码
  41. def get_mask(self, length_list):
  42. # 函数根据输入的句子长度列表生成一个掩码张量,掩码张量用于屏蔽输入句子中的填充元素
  43. mask = []
  44. # 零填充: 根据长度列表生成掩码张量,其中长度小于最大长度的位置用0填充,否则用1填充。
  45. for length in length_list:
  46. mask.append([1 for i in range(length)] + [0 for j in range(self.max_length - length)])
  47. return torch.tensor(mask, dtype=torch.bool)
  48. # 定义LSTM层的前向传递函数
  49. def LSTM_Layer(self, sentences, length_list):
  50. # 将输入序列嵌入到低维空间中
  51. embeds = self.embedding(sentences)
  52. # 使用pack_padded_sequence函数将嵌入序列打包
  53. packed_sentences = pack_padded_sequence(embeds, lengths=length_list, batch_first=True, enforce_sorted=False)
  54. # 使用LSTM层处理打包后的序列
  55. lstm_out, _ = self.lstm(packed_sentences)
  56. # 将打包后的序列解包
  57. result, _ = pad_packed_sequence(lstm_out, batch_first=True, total_length=self.max_length)
  58. # 将结果传递到全连接层中进行标记预测
  59. feature = self.hidden_to_tag(result)
  60. return feature
  61. # 计算给定排放分数的标签序列的条件对数似然性
  62. def CRF_layer(self, input, targets, length_list):
  63. """input:发射得分张量,大小为(seq_length, batch_size, num_tags)或(batch_size, seq_length, num_tags),取决于batch_first参数是否为 True。
  64. targets:标记序列张量,大小为(seq_length, batch_size)或(batch_size, seq_length),取决于batch_first参数是否为True。
  65. length_list:每个句子的实际长度列表。
  66. 该函数调用了self.crf,它是一个torchcrf库中的CRF层。它接受3个参数:
  67. emissions:发射得分张量,大小为(seq_length, batch_size, num_tags)或(batch_size, seq_length, num_tags),取决于batch_first参数是否为True。
  68. tags:标记序列张量,大小为(seq_length, batch_size)或(batch_size, seq_length),取决于batch_first参数是否为True。
  69. mask:掩码张量,大小为(seq_length, batch_size)或(batch_size, seq_length),取决于batch_first参数是否为True。
  70. """
  71. return self.crf(input, targets, self.get_mask(length_list))
  72. def forward(self, sentences, length_list, targets):
  73. # length_list 包含了每个句子的实际长度;targets 包含了每个句子中每个词对应的标签
  74. # 调用 LSTM_Layer 方法对输入序列进行处理得到 x。
  75. x = self.LSTM_Layer(sentences, length_list)
  76. # 将 x 和 targets 传递给 CRF_layer 方法,用于计算条件对数似然
  77. x = self.CRF_layer(x, targets, length_list)
  78. return x
  79. def predict(self, sentences, length_list):
  80. out = self.LSTM_Layer(sentences, length_list)
  81. mask = self.get_mask(length_list)
  82. # 将 LSTM_Layer 的输出 out 和 mask 传递给 decode 方法来预测每个词对应的标签序列,然后将预测得到的标签序列返回。
  83. return self.crf.decode(out, mask)

(四)模型评估

1、混淆矩阵

在模型评价中,一般会用准确度(Accuracy)评估模型好坏,但准确度并不总是衡量分类性能的重要指标,准确度、召回率和F1-score在评测分类模型性能起到非常重要的作用。为了帮助确定这些指标的重要性,定义了混淆矩阵:

True Positives (TP):正确分类为阳性的阳性实例数

False Positives (FP):错误分类为阳性的阴性实例数

True Negatives (TN):正确分类为否定的否定实例数

False Negatives (FN):错误分类为阴性的阳性实例数

2、精确率(Precision)

精确率(Precision)是真正例(TP)占所有正例(TP+FP)的比例。精确率也叫查准率,以物体检测为例,精确率高表示模型检测出的物体中大部分确实是物体,只有少量不是物体的对象被当成物体。该指标能够说明模型的正例预测有多精确。当我们认为假阳性比假阴性更重要时,精确度很重要(例如反欺诈识别)。

3、准确度(Accuracy)

准确率(Accuracy)是最直观的评价指标,即模型判断正确的数据(TP+TN)占总数据的比例。

使用准确度的一个问题是当数据不平衡时,没法准确评估精度,数据越不平衡,问题就越严重。

4、召回率(Recall)

召回率(Recall)是模型正确判断出的正例(TP)占数据集中所有正例的比例。召回率也叫查全率,该指标衡量了模型能够正确召回的实际正例的数量。当我们认为假阴性比假阳性更重要时,召回很重要(例如,癌症检测)。

这种权衡试图解决的问题以及偏向误报而非误报的任何固有后果(反之亦然)。

以癌症为例:

设计一个具有高召回率的模型可以识别大多数癌症患者(真阳性),挽救他们的生命,但代价是将健康个体误诊为癌症(假阳性),让他们接受昂贵而危险的治疗。另一方面,设计一个精确的模型可以得到可靠的诊断(即,预测患有癌症的人很可能确实患有癌症),但代价是无法识别每个患有该疾病的人(假阴性),从而对那些未被诊断的人造成潜在的致命后果。(因为假阴性会导致死亡,我们的分类阈值可能会被设置为优化查全率而非查准率)。

5、F1-score

考虑到这种相互竞争的权衡,拥有一个同时考虑精确度和召回率的单一性能指标将非常重要。因此,诞生了F1-score,该指标同时考虑了精确度和召回率,通过取两个指标的调和平均值来计算:

在精确度或召回率较差的情况下,F1-score也会较差。只有当准确率和召回率都有很好的表现时,F1-score才会很高。它是比较多分类器性能的一种很好的方法。当在多个模型之间进行选择时,所有模型都具有不同的精度和/或召回值,它在实践中经常被用作根据性能对模型进行排名的度量。

三、实验

参数设置:

训练集:train.txt中前14000条数据

测试集:test.txt中前3200条数据
模型:LSTM-CRF
词嵌入初始化:GloVe预训练模型初始化(glove.6B.50d.txt)

random_seed:2023
学习率:0.001

n_classes = 5  # 分类个数
batch_size:250
embedding_size:50  # 每个词向量有几维
hidden_size:50
epochs:20

运行环境:

python:3.7

pytorch:1.7.0(gpu)

torchtext:0.8.0

cuda版本:10.1

(一)代码实现

1、main.py

  1. import os
  2. import numpy as np
  3. import time
  4. import torch
  5. import matplotlib.pyplot as plt
  6. from torchtext.vocab import Vectors
  7. from utils import read_data
  8. from utils import get_dataloader
  9. from utils import pre_processing
  10. from utils import compute_f1
  11. from model import LSTM_CRF
  12. # 参数设置
  13. n_classes = 5 # 分类个数
  14. batch_size = 250
  15. embedding_size = 50 # 每个词向量有几维(几个特征)
  16. hidden_size = 50
  17. epochs = 20
  18. vectors = Vectors('data/glove.6B.50d.txt')
  19. def train(model, vocab_size, tag2idx, embedding_size, hidden_size, max_length, vectors=None):
  20. # model: 要训练的模型,类型为 LSTMCRF。
  21. # vocab_size: 词汇表大小,即训练集中不同词汇的数量。
  22. # tag2idx: 标签到索引的映射字典。
  23. # embedding_size: 嵌入层的维度大小。
  24. # hidden_size: LSTM 隐藏层的维度大小。
  25. # max_length: 训练集中句子的最大长度。
  26. # vectors: 预训练的词向量矩阵。
  27. model = model(vocab_size, tag2idx, embedding_size, hidden_size, max_length, vectors=vectors)
  28. optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
  29. # loss_history 记录了每个 epoch 的平均损失,f1_history 记录了每个 epoch 的平均 F1 分数
  30. loss_history = []
  31. # 训练集数据加载器的长度,等于训练集中的样本数量除以batch size
  32. print("dataloader length: ", len(train_dataloader))
  33. model.train()
  34. f1_history = []
  35. # 循环次数为epochs,epoch次数 = 迭代次数 / batch size;迭代次数 = 样本数量 / batch size
  36. for epoch in range(epochs):
  37. total_loss = 0.
  38. f1 = 0
  39. for idx, (inputs, targets, length_list) in enumerate(train_dataloader):
  40. # 梯度清零,以免梯度累积
  41. model.zero_grad()
  42. # 计算每个样本的损失,并将损失加到总损失中。损失是模型的负对数似然损失
  43. loss = (-1) * model(inputs, length_list, targets)
  44. total_loss += loss.item()
  45. # 这两行代码计算每个样本的预测结果,并将F1分数加到总F1分数中
  46. pred = model.predict(inputs, length_list)
  47. f1 += compute_f1(pred, targets, length_list)
  48. # 计算模型的梯度、对梯度进行剪裁,然后使用优化器
  49. loss.backward()
  50. torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
  51. optimizer.step()
  52. # 每 10 个批次输出一次当前 epoch 的平均损失和 F1 分数
  53. if (idx + 1) % 10 == 0 and idx:
  54. cur_loss = total_loss
  55. loss_history.append(cur_loss / (idx+1))
  56. f1_history.append(f1 / (idx+1))
  57. total_loss = 0
  58. # batch指一次性处理的一组训练样本,将其分为多个小批次,每个小批次包含的样本数量为batch size
  59. print("epochs : {}, batch : {}, loss : {}, f1 : {}".format(epoch+1, idx*batch_size,
  60. cur_loss / (idx * batch_size), f1 / (idx+1)))
  61. # 绘制损失图
  62. plt.plot(np.arange(len(loss_history)), np.array(loss_history))
  63. plt.xlabel('Iterations')
  64. plt.ylabel('Training Loss')
  65. plt.title('LSTM+CRF model')
  66. plt.show()
  67. # 绘制f1得分图
  68. plt.plot(np.arange(len(f1_history)), np.array(f1_history))
  69. plt.title('train f1 scores')
  70. plt.show()
  71. # 将模型设置为评估模式,这意味着在模型的前向传播过程中,不会更新权重,也不会计算梯度,以加快模型的执行速度
  72. model.eval()
  73. f1 = 0
  74. f1_history = []
  75. s = 0
  76. with torch.no_grad():
  77. # 迭代测试数据集的每个batch,其中inputs是输入序列,targets是对应的标签序列,length_list是每个输入序列的实际长度。
  78. for idx, (inputs, targets, length_list) in enumerate(test_dataloader):
  79. loss = (-1) * model(inputs, length_list, targets)
  80. total_loss += loss.item()
  81. # 使用模型进行预测,并返回预测的标签序列
  82. pred = model.predict(inputs, length_list)
  83. # 计算预测标签序列和真实标签序列之间的F1值,并将结果累加到f1中
  84. f1 += compute_f1(pred, targets, length_list) * 250
  85. print("f1 score : {}, test size = {}".format(f1/3200, 3200))
  86. if __name__ == '__main__':
  87. x_train, y_train = read_data("data/train.txt", 14000)
  88. x_test, y_test = read_data("data/test.txt", 3200)
  89. word2idx, tag2idx, vocab_size = pre_processing()
  90. train_dataloader, train_max_length = get_dataloader(x_train, y_train, batch_size)
  91. test_dataloader, test_max_length = get_dataloader(x_test, y_test, 250)
  92. train(LSTM_CRF, vocab_size, tag2idx, embedding_size, hidden_size, max_length=train_max_length, vectors=None)

2、utils.py

  1. import torch
  2. from torch.utils.data import DataLoader, Dataset
  3. """ 读取数据 """
  4. def read_data(path, length): # length 限制读取的句子数量
  5. sentences_list = [] # 每一个元素是一整个句子
  6. sentences_list_labels = [] # 每个元素是一整个句子的标签
  7. with open(path, 'r', encoding='UTF-8') as f:
  8. sentence_labels = [] # 每个元素是这个句子的每个单词的标签
  9. sentence = [] # 每个元素是这个句子的每个单词
  10. for line in f:
  11. line = line.strip() # 对于文件中的每一行,删除字符串前后的空白字符
  12. if not line: # 如果遇到了空白行
  13. if sentence: # 如果上一个句子不是空句子(防止空白行连续多个,导致出现空白的句子)
  14. sentences_list.append(' '.join(sentence)) # 将单词合并为句子,并将该句子加入到列表sentences_list中
  15. sentences_list_labels.append(' '.join(sentence_labels)) # 将标签合并为标签序列,并将该标签序列加入到列表sentences_list_labels中
  16. sentence = []
  17. sentence_labels = [] # 重置,开始处理下一个句子的单词和标签
  18. else:
  19. res = line.split() # 将一行字符串按空格划分为单词、空格、标签、空格四个部分
  20. assert len(res) == 4 # 断言每一行都必须划分为4个部分
  21. if res[0] == '-DOCSTART-': #如果该行为起始标志,忽略该行,开始处理下一行
  22. continue
  23. sentence.append(res[0]) # 将单词加入到sentence列表中
  24. sentence_labels.append(res[3]) # 将标签加入到sentence_labels列表中
  25. if sentence: # 处理最后一个句子,防止最后一个句子没有空白行
  26. sentences_list.append(' '.join(sentence))
  27. sentences_list_labels.append(' '.join(sentence_labels))
  28. return sentences_list[:length], sentences_list_labels[:length] # 返回处理好的句子及其对应的标签序列,length指定了返回的句子数量
  29. """ 构建词典(分词)"""
  30. def build_vocab(sentences_list): # sentences_list 包含多个句子的列表
  31. vocab = []
  32. # 使用列表解析式将 sentences 拆分成单个单词,并返回一个由这些单词组成的列表。
  33. for sentences in sentences_list:
  34. vocab += [word for word in sentences.split()]
  35. # 首先使用 set 函数将列表中的元素去重,然后将去重后的元素转换为列表
  36. return list(set(vocab))
  37. """ 自定义数据集 """
  38. class ClsDataset(Dataset): # 用于将输入的数据和标签转换为可迭代的数据集对象
  39. def __init__(self, x: torch.Tensor, y: torch.Tensor, length_list):
  40. self.x = x
  41. self.y = y
  42. self.length_list = length_list
  43. def __getitem__(self, index): # 返回给定索引的数据项
  44. data = self.x[index] # 使用给定索引从输入特征张量中获取相应的输入数据
  45. labels = self.y[index] # 使用给定索引从目标变量张量中获取相应的标签数据
  46. length = self.length_list[index] # 使用给定索引从输入序列长度列表中获取相应的序列长度
  47. return data, labels, length
  48. def __len__(self): # 返回数据集的长度
  49. return len(self.x)
  50. """ 返回单词在字典中的索引 """
  51. def get_idx(word, d):
  52. # 判断字典 d 中是否包含单词 word 的索引,如果包含则返回该索引
  53. if d[word] is not None:
  54. return d[word]
  55. # 如果字典 d 中不包含单词 word 的索引,则返回字典中预先定义好的 '<unknown>' 对应的索引
  56. else:
  57. return d['<unknown>']
  58. """ 将句子转换为由词汇表中的单词索引组成的向量 """
  59. def sentence2vector(sentence, d): # d为词汇表,由单词索引组成的字典
  60. # 使用列表推导式将句子分割成单词,对于每个单词调用get_idx函数获得它在字典中的索引,然后将所有的单词索引组成的列表返回
  61. return [get_idx(word, d) for word in sentence.split()]
  62. """用指定值填充序列"""
  63. def padding(x, max_length, d):
  64. length = 0
  65. # 确定填充长度后,将 <pad> 对应的值标记添加到 x 的末尾,进行填充
  66. for i in range(max_length - len(x)):
  67. x.append(d['<pad>'])
  68. return x
  69. """ 将原始文本数据集 x 和 y 转换为 PyTorch 的数据加载器 """
  70. def get_dataloader(x, y, batch_size):
  71. word2idx, tag2idx, vocab_size = pre_processing() # 预处理数据并建立词表和标签表
  72. inputs = [sentence2vector(s, word2idx) for s in x] # 每一个句子都转化成vector
  73. targets = [sentence2vector(s, tag2idx) for s in y]
  74. # 计算每个句子的长度
  75. length_list = [len(sentence) for sentence in inputs]
  76. # 找到最长的句子的长度并将其截断为124
  77. max_length = max(max(length_list), 124)
  78. # 使用padding将每个句子填充为最大长度
  79. inputs = torch.tensor([padding(sentence, max_length, word2idx) for sentence in inputs])
  80. targets = torch.tensor([padding(sentence, max_length, tag2idx) for sentence in targets], dtype=torch.long)
  81. # 创建数据集并使用DataLoader加载数据
  82. dataset = ClsDataset(inputs, targets, length_list)
  83. dataloader = DataLoader(dataset, shuffle=False, batch_size=batch_size)
  84. # 返回数据加载器和最大长度
  85. return dataloader, max_length
  86. """ 数据预处理 """
  87. def pre_processing():
  88. # 调用 read_data 函数读取训练集和测试集数据,返回两个元组,每个元组包含两个列表,分别是输入数据和标签数据
  89. x_train, y_train = read_data("data/train.txt", 14000)
  90. x_test, y_test = read_data("data/test.txt", 3200)
  91. # 调用 build_vocab 函数,分别对输入和标签数据建立词汇表。这里将训练集和测试集合并后一起建立词汇表。
  92. d_x = build_vocab(x_train+x_test)
  93. d_y = build_vocab(y_train+y_test)
  94. # 将每个词汇/标签映射到一个唯一的整数,用字典存储。字典的键是词汇/标签,值是整数。
  95. word2idx = {d_x[i]: i for i in range(len(d_x))}
  96. tag2idx = {d_y[i]: i for i in range(len(d_y))}
  97. # 为起始标签和终止标签分别添加索引值。这些标签通常用于序列标注任务中。
  98. tag2idx["<START>"] = 9
  99. tag2idx["<STOP>"] = 10
  100. # 为填充标记添加索引。将填充标记添加到词汇表和标签字典的末尾。
  101. pad_idx = len(word2idx)
  102. word2idx['<pad>'] = pad_idx
  103. tag2idx['<pad>'] = len(tag2idx)
  104. # 计算词汇表的大小,建立标签到索引的反向映射字典。输出标签到索引的字典。
  105. vocab_size = len(word2idx)
  106. # idx2tag = {value: key for key, value in tag2idx.items()}
  107. print(tag2idx)
  108. # 返回词汇表、标签字典和词汇表大小
  109. return word2idx, tag2idx, vocab_size
  110. """ 计算F1-score """
  111. def compute_f1(pred, targets, length_list):
  112. # 初始化 TP, FN 和 FP
  113. tp, fn, fp = [], [], []
  114. # 共有15个标签
  115. for i in range(15):
  116. tp.append(0)
  117. fn.append(0)
  118. fp.append(0)
  119. # 遍历每个句子的标签预测结果和真实标签,更新计数。
  120. for i, length in enumerate(length_list):
  121. for j in range(length):
  122. # 获取预测的标签和真实的标签。
  123. a, b = pred[i][j], targets[i][j]
  124. # 若预测的标签和真实的标签一致,则增加标签的 TP 计数。
  125. if (a == b):
  126. tp[a] += 1
  127. else:
  128. fp[a] += 1
  129. fn[b] += 1
  130. # 计算所有有效标签的TP/FP/FN
  131. tps = 0
  132. fps = 0
  133. fns = 0
  134. for i in range(9):
  135. tps += tp[i]
  136. fps += fp[i]
  137. fns += fn[i]
  138. # 计算 Precision 和 Recall
  139. p = tps / (tps + fps)
  140. r = tps / (tps + fns)
  141. # 计算 F1 分数并返回
  142. return 2 * p * r / (p + r)

(二)结果分析

 

总结:

由于是初学者,学习过程中参考了很多大佬的资料和代码,均附上参考链接:

1、https://blog.csdn.net/Raki_J/article/details/122435674

2、邱锡鹏——《神经网络与深度学习》

3、https://blog.csdn.net/qq_42365109/article/details/119246515

4、NLP入门(四)命名实体识别(NER) - 简书 (jianshu.com)

5、命名实体识别系列(一)NER任务介绍_一鸣鸣的博客-CSDN博客

6、https://blog.csdn.net/weixin_45884316/article/details/118684681

7、https://blog.csdn.net/qq_41773806/article/details/115598437

8、https://zhuanlan.zhihu.com/p/519982682

9、https://cloud.tencent.com/developer/article/1490456

10、https://blog.csdn.net/qfikh/article/details/103588744

以上就是NLP-Beginner的任务四,欢迎各位前辈批评指正!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/961600
推荐阅读
相关标签
  

闽ICP备14008679号