当前位置:   article > 正文

BERT+BiLSTM命名实体识别

BERT+BiLSTM命名实体识别

这是第一次在CSDN上记录自己的学习过程,加油。

本文是记录b站博主“手写AI”的命名实体识别系列课程的学习笔记,构建五个py文件,直接运行即可。

目录

一、前言

二、数据处理

数据案例

读取数据

 构建label2index

三、数据迭代器

接下来看一下如何构建数据类

对三个函数进行一一解释:

四、模型

训练和验证

当label不为None的时候,就是训练: 

当label为None的时候,就是验证: 

五、训练

六、预测 

六、完整代码

config.py

  utils.py

 model.py

train.py 

predict.py 


一、前言

文章比较简单,就是利用BERT+BiLSTM,然后后面直接连上Linear层进行分类,比较简单。

二、数据处理

数据案例

高 B-NAME 勇 E-NAME : O 男 O , O 中 B-CONT 国 I-CONT 国 I-CONT 籍 E-CONT , O 无 O 境 O 外 O 居 O 留 O 权 O , O

读取数据

  1. def read_data(filename):
  2. with open(filename, 'r', encoding='utf8') as f:
  3. all_data = f.read().split('\n')
  4. all_text = [] # 用来保存所有的文本
  5. all_label = [] # 用来保存所有的标签
  6. text = [] # 用来保存一段文本
  7. labels = [] # 用来保存一段文本的标签
  8. for data in all_data:
  9. if data == '':
  10. all_text.append(text)
  11. all_label.append(labels)
  12. text = []
  13. labels = []
  14. else:
  15. t, l = data.split(' ')
  16. text.append(t)
  17. labels.append(l)
  18. return all_text, all_label

 构建label2index

  1. def build_label_2_index(all_label):
  2. label_2_index = {'PAD': 0, 'UNK': 1}
  3. for labels in all_label:
  4. for label in labels:
  5. if label not in label_2_index:
  6. label_2_index[label] = len(label_2_index)
  7. return label_2_index, list(label_2_index)

因为会设置模型输出的最大长度,所有,当句子不够长的时候,我们需要对标签进行填充[PAD],当遇到不认识的标签时[UNK].

返回值:

label_2_index:是字典,类似于{'PAD': 0, 'UNK': 1}

list(label_2_index):是列表,['PAD', 'UNK']

 

三、数据迭代器

在pytorch里面,Dataset和DataLoader这两个类很重要,可以将数据处理好,然后就可以直接读取了。具体的操作流程都是固定的,主要是以下三个函数:

  1. def __init__(self): # 初始化函数
  2. pass
  3. def __getitem__(self, item): # 读取一个数据
  4. pass
  5. def __len__(self) # 返回整个数据的长度
  6. pass

接下来看一下如何构建数据类

  1. class Data(Dataset):
  2. def __init__(self, all_text, all_label, tokenizer, label2index, max_len):
  3. self.all_text = all_text
  4. self.all_label = all_label
  5. self.tokenizer = tokenizer
  6. self.label2index = label2index
  7. self.max_len = max_len
  8. def __getitem__(self, item):
  9. text = self.all_text[item]
  10. labels = self.all_label[item][:self.max_len]
  11. # 需要对text编码,让bert可以接受
  12. text_index = self.tokenizer.encode(text,
  13. add_special_tokens=True,
  14. max_length=self.max_len + 2,
  15. padding='max_length',
  16. truncation=True,
  17. return_tensors='pt',
  18. )
  19. # 也需要将label进行编码
  20. # 那么我们需要构建一个函数来传入label2index
  21. # labels_index = [self.label2index.get(label, 1) for label in labels]
  22. # 上面那个就仅仅是转化,我们需要将label和text对齐
  23. labels_index = [0] + [self.label2index.get(label, 1) for label in labels] + [0] + [0] * (
  24. self.max_len - len(text))
  25. # 这里需要注意text_index.squeeze(),squeeze()是默认去掉维度为1的那个维度
  26. # text_index的原始维度是:batch_size,1,seq_len
  27. # 在后续操作的过程中,将输入数据喂入模型时,如果不做处理,就会报错
  28. # 这里多输出一个len(text)!目的是在验证的时候,用的上,后面会介绍用处
  29. return text_index.squeeze(), torch.tensor(labels_index), len(text)
  30. def __len__(self):
  31. return len(self.all_text)

对三个函数进行一一解释:

1、 def __init__(self, all_text, all_label, tokenizer, label2index, max_len)

需要在初始化函数中传入需要的参数,比如:

all_text和all_label:你读取的所有文本和标签(数据处理部分);

tokenizer:因为要将文本传入BERT模型中,直接传入肯定是不行的,需要将文本转成数字(这是transformers封装好的,直接调用就行);

label2index:与上面的tokenizer相似,也需要将标签转成数字,这里直接编写代码即可(数据处理部分)

max_len:设置你想要的最大长度

四、模型

  1. class MyModel(nn.Module):
  2. def __init__(self, class_num):
  3. super(MyModel, self).__init__()
  4. self.class_num = class_num
  5. self.bert = BertModel.from_pretrained(BERT_PATH)
  6. self.lstm = nn.LSTM(768,
  7. 768 // 2,
  8. bidirectional=True,
  9. batch_first=True)
  10. self.linear = nn.Linear(768, class_num)
  11. self.loss_fn = nn.CrossEntropyLoss()
  12. def forward(self, batch_text, batch_label=None):
  13. output = self.bert(batch_text)
  14. bert_out0, bert_out1 = output[0], output[1]
  15. output1, _ = self.lstm(bert_out0)
  16. pre = self.linear(output1)
  17. if batch_label is not None:
  18. loss = self.loss_fn(pre.reshape(-1, pre.shape[-1]), batch_label.reshape(-1))
  19. return loss
  20. else:
  21. return torch.argmax(pre, dim=-1)

 将输入数据喂入模型,然后得到输出。

当模型有标签数据的时候,那么就会返回损失值,然后反向传播,更新,梯度清零;

当模型没有标签数据的时候,那么就是预测了,模型的输出应该就是标签类别,所以要在初始化函数中传入整个类别数(len(label2index))

注意:此时,这个标签值,应该是数字,后续还需要将其转换为真是标签进行计算。

所以在初始化函数中:

def __init__(self, class_num),设置了一class_num==整个类别数(len(label2index))

训练和验证

当label不为None的时候,就是训练: 

 if batch_label is not None:
            loss = self.loss_fn(pre.reshape(-1, pre.shape[-1]),batch_label.reshape(-1))
            return loss
  为什么要将pre和batch_label的维度进行改变?

首先,需要看pre的原始维度:

 pre.shape == torch.Size([batch_size, max_len, class_num])

batch_label.shape == torch.Size([batch_size, max_len])

其次,loss_fn = nn.CrossEntropyLoss(),需要输入的向量维度是二维的,所以我们需要对维度进行改变!

最后,

pre.reshape(-1, pre.shape[-1]) == (batch_size*max_len, class_num)

batch_label.reshape(-1) == (batch_size*max_len)

当label为None的时候,就是验证: 

直接返回:return torch.argmax(pre, dim=-1)。

五、训练

  1. def train():
  2. # 读取训练文件夹
  3. train_filename = os.path.join('data', 'train.txt')
  4. # 返回训练数据的文本和标签
  5. train_text, train_label = read_data(train_filename)
  6. # 验证集
  7. dev_filename = os.path.join('data', 'dev.txt')
  8. dev_text, dev_label = read_data(dev_filename)
  9. # print(train_filename)
  10. # 得到label2index, index2label
  11. label2index, index2label = build_label_2_index(train_label)
  12. # 数据迭代器
  13. train_data = Data(train_text, train_label, tokenizer, label2index, MAX_LEN)
  14. train_loader = DataLoader(train_data, batch_size=32, shuffle=False)
  15. dev_data = Data(dev_text, dev_label, tokenizer, label2index, MAX_LEN)
  16. dev_loader = DataLoader(dev_data, batch_size=32, shuffle=False)
  17. # 模型
  18. model = MyModel(len(label2index)).to(DEVICE)
  19. optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE)
  20. # 训练
  21. for epoch in range(EPOCHS):
  22. model.train()
  23. for batch_idx, data in enumerate(train_loader):
  24. batch_text, batch_label, batch_len = data
  25. # 将数据放到GPU上
  26. loss = model(batch_text.to(DEVICE), batch_label.to(DEVICE))
  27. loss.backward()
  28. optimizer.step()
  29. optimizer.zero_grad()
  30. if batch_idx % 10 == 0:
  31. print(f'Epoch: {epoch}, BATCH: {batch_idx}, Training Loss: {loss.item()}')
  32. # torch.save(model, MODEL_DIR + f'model_{epoch}.pth')
  33. model.eval()
  34. # 用来存放预测标签和真实标签
  35. all_pre = []
  36. all_tag = []
  37. for batch_text, batch_label, batch_len in dev_loader:
  38. # 因为是预测,所以在模型输入的地方,没有加入batch_label
  39. pre = model(batch_text.to(DEVICE))
  40. # 将pre从GPU上读下来,转成list
  41. pre = pre.cpu().numpy().tolist()
  42. batch_label = batch_label.cpu().numpy().tolist()
  43. # 还有一点要注意, from seqeval.metrics import f1_score
  44. # 在使用 f1_score的时候,所需要的标签应该是完整的,而不是经过填充过的
  45. # 所以我们需要将填充过的标签信息进行拆分怎么做呢?
  46. # 就需要将最开始没有填充过的文本长度记录下来,在__getitem__的返回量中增加一个长度量,那样我们就能知道文本真实长度
  47. # 然后就此进行切分,因为左边增加了一个开始符,需要去掉一个即可;右边按照长度来切分
  48. for p, t, l in zip(pre, batch_label, batch_len):
  49. p = p[1: l + 1]
  50. t = t[1: l + 1]
  51. pre = [index2label[j] for j in p]
  52. tag = [index2label[j] for j in t]
  53. all_pre.append(pre)
  54. all_tag.append(tag)
  55. f1_score_ = f1_score(all_pre, all_tag)
  56. p_score = precision_score(all_pre, all_tag)
  57. r_score = recall_score(all_pre, all_tag)
  58. # f1_score(batch_label_index, pre)
  59. print(f'p值={p_score}, r值={r_score}, f1={f1_score_}')

六、预测 

就没有跑那么多了,直接保存模型,读取一条数据进行预测。

  1. def predict():
  2. train_filename = os.path.join('data', 'train.txt')
  3. train_text, train_label = read_data(train_filename)
  4. test_filename = os.path.join('data', 'test.txt')
  5. test_text, _ = read_data(test_filename)
  6. text = test_text[1]
  7. print(text)
  8. inputs = tokenizer.encode(text,
  9. return_tensors='pt')
  10. inputs = inputs.to(DEVICE)
  11. model = torch.load(MODEL_DIR + 'model_1.pth')
  12. y_pre = model(inputs).reshape(-1) # 或者是y_pre[0]也行,因为y_pre是一个batch,需要进行reshape
  13. _, id2label = build_label_2_index(train_label)
  14. label = [id2label[l] for l in y_pre[1:-1]]
  15. print(text)
  16. print(label)
  17. if __name__ == '__main__':
  18. predict()

 

六、完整代码

 完整代码分为5部分:config.py, utils.py, model.py, train.py, predict.py

config.py

  1. import torch
  2. from transformers import BertModel, BertTokenizer
  3. from torch.utils.data import DataLoader, Dataset
  4. EPOCHS = 2
  5. BATCH_SIZE = 64
  6. LEARNING_RATE = 2e-5
  7. MAX_LEN = 50
  8. DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' # 调用GPU
  9. BERT_PATH = r'BERT_MODEL\roberta' # 你自己的bert模型地址
  10. tokenizer = BertTokenizer.from_pretrained(BERT_PATH)
  11. MODEL_DIR = 'model/' # 这是保存模型的地址,建在你代码的同一级即可

  utils.py

 

  1. import torch
  2. from torch.utils.data import DataLoader, Dataset
  3. def read_data(filename):
  4. with open(filename, 'r', encoding='utf8') as f:
  5. all_data = f.read().split('\n')
  6. all_text = []
  7. all_label = []
  8. text = []
  9. labels = []
  10. for data in all_data:
  11. if data == '':
  12. all_text.append(text)
  13. all_label.append(labels)
  14. text = []
  15. labels = []
  16. else:
  17. t, l = data.split(' ')
  18. text.append(t)
  19. labels.append(l)
  20. return all_text, all_label
  21. def build_label_2_index(all_label):
  22. label_2_index = {'PAD': 0, 'UNK': 1}
  23. for labels in all_label:
  24. for label in labels:
  25. if label not in label_2_index:
  26. label_2_index[label] = len(label_2_index)
  27. return label_2_index, list(label_2_index)
  28. class Data(Dataset):
  29. def __init__(self, all_text, all_label, tokenizer, label2index, max_len):
  30. self.all_text = all_text
  31. self.all_label = all_label
  32. self.tokenizer = tokenizer
  33. self.label2index = label2index
  34. self.max_len = max_len
  35. def __getitem__(self, item):
  36. text = self.all_text[item]
  37. labels = self.all_label[item][:self.max_len]
  38. # 需要对text编码,让bert可以接受
  39. text_index = self.tokenizer.encode(text,
  40. add_special_tokens=True,
  41. max_length=self.max_len + 2,
  42. padding='max_length',
  43. truncation=True,
  44. return_tensors='pt',
  45. )
  46. # 也需要将label进行编码
  47. # 那么我们需要构建一个函数来传入label2index
  48. # labels_index = [self.label2index.get(label, 1) for label in labels]
  49. # 上面那个就仅仅是转化,我们需要将label和text对齐
  50. labels_index = [0] + [self.label2index.get(label, 1) for label in labels] + [0] + [0] * (
  51. self.max_len - len(text))
  52. return text_index.squeeze(), torch.tensor(labels_index), len(text)
  53. def __len__(self):
  54. return len(self.all_text)

 model.py

  1. import torch.nn as nn
  2. from config import *
  3. class MyModel(nn.Module):
  4. def __init__(self, class_num):
  5. super(MyModel, self).__init__()
  6. self.class_num = class_num
  7. self.bert = BertModel.from_pretrained(BERT_PATH)
  8. self.lstm = nn.LSTM(768,
  9. 768 // 2,
  10. bidirectional=True,
  11. batch_first=True)
  12. self.linear = nn.Linear(768, class_num)
  13. self.loss_fn = nn.CrossEntropyLoss()
  14. def forward(self, batch_text, batch_label=None):
  15. output = self.bert(batch_text)
  16. bert_out0, bert_out1 = output[0], output[1]
  17. output1, _ = self.lstm(bert_out0)
  18. pre = self.linear(output1)
  19. if batch_label is not None:
  20. loss = self.loss_fn(pre.reshape(-1, pre.shape[-1]), batch_label.reshape(-1))
  21. return loss
  22. else:
  23. return torch.argmax(pre, dim=-1)

train.py 

  1. from utils import *
  2. from model import *
  3. from config import *
  4. from seqeval.metrics import f1_score, precision_score, recall_score
  5. import os
  6. def train():
  7. # 读取训练文件夹
  8. train_filename = os.path.join('data', 'train.txt')
  9. # 返回训练数据的文本和标签
  10. train_text, train_label = read_data(train_filename)
  11. # 验证集
  12. dev_filename = os.path.join('data', 'dev.txt')
  13. dev_text, dev_label = read_data(dev_filename)
  14. # print(train_filename)
  15. # 得到label2index, index2label
  16. label2index, index2label = build_label_2_index(train_label)
  17. # 数据迭代器
  18. train_data = Data(train_text, train_label, tokenizer, label2index, MAX_LEN)
  19. train_loader = DataLoader(train_data, batch_size=32, shuffle=False)
  20. dev_data = Data(dev_text, dev_label, tokenizer, label2index, MAX_LEN)
  21. dev_loader = DataLoader(dev_data, batch_size=32, shuffle=False)
  22. # 模型
  23. model = MyModel(len(label2index)).to(DEVICE)
  24. optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE)
  25. # 训练
  26. for epoch in range(EPOCHS):
  27. model.train()
  28. for batch_idx, data in enumerate(train_loader):
  29. batch_text, batch_label, batch_len = data
  30. # 将数据放到GPU上
  31. loss = model(batch_text.to(DEVICE), batch_label.to(DEVICE))
  32. loss.backward()
  33. optimizer.step()
  34. optimizer.zero_grad()
  35. if batch_idx % 10 == 0:
  36. print(f'Epoch: {epoch}, BATCH: {batch_idx}, Training Loss: {loss.item()}')
  37. # torch.save(model, MODEL_DIR + f'model_{epoch}.pth')
  38. model.eval()
  39. # 用来存放预测标签和真实标签
  40. all_pre = []
  41. all_tag = []
  42. for batch_text, batch_label, batch_len in dev_loader:
  43. # 因为是预测,所以在模型输入的地方,没有加入batch_label
  44. pre = model(batch_text.to(DEVICE))
  45. # 将pre从GPU上读下来,转成list
  46. pre = pre.cpu().numpy().tolist()
  47. batch_label = batch_label.cpu().numpy().tolist()
  48. # 还有一点要注意, from seqeval.metrics import f1_score
  49. # 在使用 f1_score的时候,所需要的标签应该是完整的,而不是经过填充过的
  50. # 所以我们需要将填充过的标签信息进行拆分怎么做呢?
  51. # 就需要将最开始没有填充过的文本长度记录下来,在__getitem__的返回量中增加一个长度量,那样我们就能知道文本真实长度
  52. # 然后就此进行切分,因为左边增加了一个开始符,需要去掉一个即可;右边按照长度来切分
  53. for p, t, l in zip(pre, batch_label, batch_len):
  54. p = p[1: l + 1]
  55. t = t[1: l + 1]
  56. pre = [index2label[j] for j in p]
  57. tag = [index2label[j] for j in t]
  58. all_pre.append(pre)
  59. all_tag.append(tag)
  60. f1_score_ = f1_score(all_pre, all_tag)
  61. p_score = precision_score(all_pre, all_tag)
  62. r_score = recall_score(all_pre, all_tag)
  63. # f1_score(batch_label_index, pre)
  64. print(f'p值={p_score}, r值={r_score}, f1={f1_score_}')
  65. # print(2*p_score*r_score/(p_score+r_score))
  66. if __name__ == '__main__':
  67. train()

predict.py 

  1. from utils import *
  2. from model import *
  3. from config import *
  4. import os
  5. def predict():
  6. train_filename = os.path.join('data', 'train.txt')
  7. train_text, train_label = read_data(train_filename)
  8. test_filename = os.path.join('data', 'test.txt')
  9. test_text, _ = read_data(test_filename)
  10. text = test_text[1]
  11. print(text)
  12. inputs = tokenizer.encode(text,
  13. return_tensors='pt')
  14. inputs = inputs.to(DEVICE)
  15. model = torch.load(MODEL_DIR + 'model_1.pth')
  16. y_pre = model(inputs).reshape(-1) # 或者是y_pre[0]也行,因为y_pre是一个batch,需要进行reshape
  17. _, id2label = build_label_2_index(train_label)
  18. label = [id2label[l] for l in y_pre[1:-1]]
  19. print(text)
  20. print(label)
  21. if __name__ == '__main__':
  22. predict()

 

 

 

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

闽ICP备14008679号