pipeline方法关系抽取--课堂笔记
Pipeline方法课堂笔记
一、Pipeline方法原理
pipeline方法是指在实体识别已经完成的基础上再进行实体之间关系的抽取.
pipeline方法流程:
先对输入的句子进行实体抽取,将识别出的实体分别组合;然后再进行关系分类.
注意:这两个子过程是前后串联的,完全分离.
pipeline方法的优缺点:
优点:
- 易于实现,实体模型和关系模型使用独立的数据集,不需要同时标注实体和关系的数据集.
- 两者相互独立,若关系抽取模型没训练好不会影响到实体抽取.
缺点:- 关系和实体两者是紧密相连的,互相之间的联系没有捕捉到.
- 容易造成误差积累、实体冗余、交互缺失.
二、BiLSTM+Attention的模型原理
BiLSTM+Attention算法思想
BiLSTM+Attention模型最初由Zhou等人在2016年的论文《Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification》中提出.
该模型结合了双向长短时记忆网络 (Bidirectional LSTM) 和注意力机制 (Attention) ,用于处理输入序列并提取关系信息. 该模型并被应用于关系分类任务.
BiLSTM+Attention模型架构:
-
基于上图模型架构所示: BiLSTM+Attention模型整体分为五个部分:
- 输入层 (Input Layer) : 输入的是句子,可以是字符序列也可以是单词序列,或者两者相结合. 此外,对于句子中的两个实体,分别计算各个字符相对于实体的位置. 比如有如下样本:
文本描述: “在《逃学威龙》这部电影中周星驰和吴孟达联合出演” 在这个样本中,实体1为周星驰,实体2为吴孟达. 关系为合作关系. 对于第一个字符“在”字来说,其相对于实体1的距离为: “在”字在字符序列中的索引-实体1在字符序列中的索引. 相对于实体2的距离为: “在”字在字符序列中的索引-实体2在字符序列中的索引. 因此,模型的输入为子序列+字符的相对位置编码
-
词嵌入层 (Embedding Layer) : 将每个单词映射到一个高维向量表示,包括: 字符或词嵌入以及相对位置编码的嵌入,可以使用预训练的词向量或从头开始训练.
-
双向LSTM层 (BiLSTM Layer) : LSTM是一种递归神经网络,它可以对序列数据进行建模,用于从句子中提取特征. BiLSTM是一种双向LSTM,它能够同时捕捉上下文信息,包括前向和后向信息,因此在关系抽取任务中得到了广泛应用.
-
注意力机制层 (Attention Layer) : 注意力机制可以让模型集中注意力于关键词或片段,有助于提高模型的性能. 这里注意力机制被用来确定每个句子中的单词对于关系分类的重要性. 具体来说,对于输入句子中的每个单词,注意力机制会为其分配一个权重,表示该单词对于关系分类的重要程度. 这些权重将被用于加权输入句子中每个单词的表示,以计算关系分类的输出. 本次注意力机制的实现是采用的基于注意力权重的加权平均池化方式.
-
具体实现方式如下:
- 第一步: 将 BiLSTM 网络的输出 HHH 经过一个 tanhtanhtanh 激活函数,得到一个矩阵MMM
M=tanh(H) M = tanh(H) M=tanh(H)
- 第二步: 将 MMM 作为输入,通过权重向量 wTw^TwT 和一个 softmax 函数,计算每个单词对于关系分类的重要程度,得到的结果是一个权重向量 ααα .
α=softmax(wT∗M) α = softmax(w^T*M) α=softmax(wT∗M)
- 第三步: 将BiLSTM网络的输出 HHH 和注意力权重 ααα 相乘得到一个加权和 rrr
r=H∗αT r = H * α^T r=H∗αT
- 第四步: 将第三步得到的结果 rrr ,经过一个 tanhtanhtanh 激活函数,得到最终加权后的输入句子中每个单词的表示,以方便后续计算关系分类的输出
h∗=tanh(r) h^* = tanh(r) h∗=tanh(r)
-
-
输出层 (Output Layer) : 根据任务的不同,输出层可以是分类层或回归层. 在关系抽取任务中,输出层通常是一个分类层,用于预测两个实体之间的关系类型.
三、基于BiLSTM+Attention模型的数据预处理
关系抽取项目数据预处理
- 本项目中对数据部分的预处理步骤如下:
- 第一步: 查看项目数据集
- 第二步: 编写Config类项目文件配置代码
- 第三步: 编写数据处理相关函数
- 第四步: 构建DataSet类与dataloader函数
第一步: 查看项目数据集
-
本次项目数据原始来源为公开的千言数据集https://www.luge.ai/#/,使用开源数据的好处,我们无需标注直接使用即可,本次项目的主要需要大家掌握实现关系抽取的思想。
-
项目的数据集包括3个文件:
-
第一个关系类型文件: relation2id.txt
导演 0
歌手 1
作曲 2
作词 3
主演 4
- relation2id.txt中包含5个类别标签, 文件共分为两列,第一列是类别名称,第二列为类别序号,中间空格符号隔开
- 第二个训练数据集:train.txt
今晚会在哪里醒来 黄家强 歌手 《今晚会在哪里醒来》是黄家强的一首粤语歌曲,由何启弘作词,黄家强作曲编曲并演唱,收录于2007年08月01日发行的专辑《她他》中似水流年 许晓杰 作曲 似水流年,由著名作词家闫肃作词,著名音乐人许晓杰作曲,张烨演唱交涉人 朝日电视台 出品公司 《交涉人》是日本朝日电视台制作并播出的8集悬疑推理电视剧生活启示录 闫妮 主演 05闫妮接到《生活启示录》之后,就向王丽萍推荐了胡歌北京北京 汪峰 歌手 ”汪峰我印象最深刻的是汪峰的《北京北京》蚀骨唱成烛骨千岁情人 王菲 主演 难怪春晚把那英秒成不一样很多人都不知道 王菲演 的这部《千岁情人》,是1993年的一部穿越剧
天使的咒语 魏雪漫 歌手 魏雪漫专辑《天使的咒语》的同名主打歌曲与青春有关的日子 白百何 主演 白百何的处女座是《与青春有关的日子》,合作的演员是佟大为、陈羽凡高高至上 秋言 作词 专辑曲目序号 曲目作词作曲编曲1高高至上秋言秋言彭飞2高高至上(伴奏) 秋言秋言彭飞
train.txt 中包含18267行样本, 每行分为4列元素,元素中间用空格隔开,第一列元素为实体1、第二列元素为实体2、第三列元素为关系类型、第四列元素是原始文本
- 第三个测试数据集:test.txt
三生三世十里桃花 安悦溪 主演 当《三生三世》4位女星换上现代装: 第四,安悦溪在《三生三世十里桃花》中饰演少辛,安悦溪穿上现代装十分亮眼,气质清新脱俗失恋33天 白百何 主演 2011年,担任爱情片《失恋33天》的编剧,该片改编自鲍鲸鲸的同名小说,由文章、白百何共同主演6爱人们的故事 裴勇俊 主演 《爱人们的故事》是全基尚导演,裴勇俊、李英爱、李慧英等主演的18集爱情类型的电视剧为你叫好 吕薇 歌手 基本资料 歌曲名称: 为你叫好1歌手: 吕薇 所属专辑: 《但愿人长久》歌词 歌手: 吕薇 词: 清风 曲: 刘青卡拉是条狗 路学长 导演 个人生活李佳璇和导演路学长因拍摄《卡拉是条狗》而相识,2003年两人结婚上帝创造女人 简-路易斯·特林提格南特 主演 《上帝创造女人》是罗杰·瓦迪姆执导的粉红浪漫爱情影片,由碧姬·芭铎和简-路易斯·特林提格南特参加演出上帝创造女人 碧姬·芭铎 主演 《上帝创造女人》是罗杰·瓦迪姆执导的粉红浪漫爱情影片,由碧姬·芭铎和简-路易斯·特林提格南特参加演出
test.txt中包含5873行样本, 每行分为4列元素,元素中间用空格隔开,第一列元素为实体1、第二列元素为实体2、第三列元素为关系类型、第四列元素是原始文本
第二步:编写Config类项目文件配置代码
- config.py
- config文件目的: 配置项目常用变量,一般这些变量属于不经常改变的,比如: 训练文件路径、模型训练次数、模型超参数等等
# coding:utf-8
import torchclass Config(object):def __init__(self):self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')self.train_data_path = "训练文件绝对路径名称"self.test_data_path = "测试文件绝对路径名称" self.rel_data_path = "关系类型文件绝对路径名称" self.embedding_dim = 128self.pos_dim = 32self.hidden_dim = 200self.epochs = 50self.batch_size = 32self.max_len = 70self.learning_rate = 1e-3
第三步: 编写数据处理相关函数
- 数据处理相关函数process.py
- 首选导入必备的工具包
# coding:utf-8
from config import *
from itertools import chain
from collections import Counterconf = Config()
# 获取关系类型字典
relation2id = {}
with open(conf.rel_data_path, 'r', encoding='utf-8')as fr:for line in fr.readlines():word, id = line.rstrip().split(' ')if word not in relation2id:relation2id[word] = id
- 构建第一个数据处理相关函数sent_padding, 位于process.py中的独立函数.
def sent_padding(words, word2id):"""把句子 words 转为 id 形式,并自动补全为 max_len 长度。"""ids = []for word in words:if word in word2id:ids.append(word2id[word])else:ids.append(word2id['UNKNOW'])if len(ids) >= conf.max_len:return ids[:conf.max_len]ids.extend([word2id['BLANK']]*(conf.max_len-len(ids)))return ids
- 构建第二个数据处理相关函数pos, 位于process.py中的独立函数.
def pos(num):'''将实体位置信息进行转换,因为pos_embedding不能出现负数'''if num < -70:return 0if num >= -70 and num <= 70:return num+70if num > 70:return 142
- 构建第三个数据处理相关函数position_padding, 位于process.py中的独立函数.
def position_padding(pos_ids):'''"""把 pos位置信息 转为 id 形式,并自动补全为 max_len 长度。"""'''pos_ids = [pos(id) for id in pos_ids]if len(pos_ids) >= conf.max_len:return pos_ids[:conf.max_len]pos_ids.extend([142]*(conf.max_len - len(pos_ids)))return pos_ids
- 构建第四个数据处理相关函数get_train_data, 位于process.py中的独立函数.
def get_txt_data(data_path):'''编码训练、测试数据集格式'''datas = []labels = []positionE1 = []positionE2 = []entities = []count_dict = {key: 0 for key, value in relation2id.items()}with open(data_path, 'r', encoding='utf-8')as tfr:for line in tfr.readlines():line = line.rstrip().split(' ', maxsplit=3)if line[2] not in count_dict:continueif count_dict[line[2]] > 2000:continueelse:entities.append([line[0], line[1]])sentence = []index1 = line[3].index(line[0])position1 = []index2 = line[3].index(line[1])position2 = []assert len(line) == 4for i, word in enumerate(line[3]):sentence.append(word)position1.append(i-index1)position2.append(i-index2)datas.append(sentence)labels.append(relation2id[line[2]])positionE1.append(position1)positionE2.append(position2)count_dict[line[2]] += 1return datas, labels, positionE1, positionE2, entities
- 构建第五个数据处理相关函数get_word_id, 位于 process.py 中的独立函数.
def get_word_id(data_path):'''文本数字化表示处理,得到word2id, id2word'''datas, labels, positionE1, positionE2, entities = get_txt_data(data_path)data_list = list(set(chain(*datas)))word2id = {word: id for id, word in enumerate(data_list)}id2word = {id: word for id, word in enumerate(data_list)}word2id["BLANK"] = len(word2id)word2id["UNKNOW"] = len(word2id)id2word[len(id2word) + 1] = "BLANK"id2word[len(id2word) + 1] = "UNKNOW"return word2id, id2word
第四步: 构建DataSet类以及Dataloader函数
- 代码路径为: data_loader.py
- 首先导入相应的工具包
# coding:utf-8
import os
from torch.utils.data import DataLoader, Dataset
from utils.process import *
import torch
- 构建第一个数据处理相关类MyDataset, 位于data_loader.py中的独立类.
class MyDataset(Dataset):def __init__(self, data_path):self.data = get_txt_data(data_path)def __len__(self):return len(self.data[0])def __getitem__(self, index):sequence = self.data[0][index]label = int(self.data[1][index])positionE1 = self.data[2][index]positionE2 = self.data[3][index]entites = self.data[4][index]return sequence, label, positionE1, positionE2, entites
- 构建第二个数据处理相关函数collate_fn, 位于data_loader.py中的独立函数.
def collate_fn(datas):sequences = [data[0] for data in datas]labels = [data[1] for data in datas]positionE1 = [data[2] for data in datas]positionE2 = [data[3] for data in datas]entities = [data[4] for data in datas]word2id, id2word = get_word_id(conf.train_data_path)sequences_ids = []for words in sequences:ids = sent_padding(words, word2id)sequences_ids.append(ids)positionE1_ids = []positionE2_ids = []for pos_ids in positionE1:pos1_ids = position_padding(pos_ids)positionE1_ids.append(pos1_ids)for pos_ids in positionE2:pos2_ids = position_padding(pos_ids)positionE2_ids.append(pos2_ids)datas_tensor = torch.tensor(sequences_ids, dtype=torch.long, device=conf.device)positionE1_tensor = torch.tensor(positionE1_ids, dtype=torch.long, device=conf.device)positionE2_tensor = torch.tensor(positionE2_ids, dtype=torch.long, device=conf.device)labels_tensor = torch.tensor(labels, dtype=torch.long, device=conf.device)return datas_tensor, positionE1_tensor, positionE2_tensor, labels_tensor, sequences, labels, entities
- 构建第三个数据处理相关函数get_loader_data, 位于data_loader.py中的独立函数.
def get_loader_data():train_data = MyDataset(conf.train_data_path)train_dataloader = DataLoader(dataset=train_data,batch_size=conf.batch_size,shuffle=False,collate_fn=collate_fn,drop_last=True)test_data = MyDataset(conf.test_data_path)test_dataloader = DataLoader(dataset=test_data,batch_size=conf.batch_size,shuffle=False,collate_fn=collate_fn,drop_last=True)return train_dataloader, test_dataloader
四、基于BiLSTM+Attention模型实现训练
BiLSTM+Attention模型搭建
- 本项目中BiLSTN+Attention模型搭建的步骤如下:
- 第一步: 编写模型类的代码
- 第二步: 编写训练函数
- 第三步: 编写使用模型预测代码的实现.
第一步: 编写模型类的代码
- 构建BiLSTM_ATT模型类
- 代码路径: bilstm_atten.py
# coding:utf8
import torch
import torch.nn as nn
import torch.nn.functional as Fclass BiLSTM_ATT(nn.Module):def __init__(self, conf, vocab_size, pos_size, tag_size):super(BiLSTM_ATT, self).__init__()self.batch = conf.batch_sizeself.device = conf.deviceself.vocab_size = vocab_sizeself.embedding_dim = conf.embedding_dimself.hidden_dim = conf.hidden_dimself.pos_size = pos_sizeself.pos_dim = conf.pos_dimself.tag_size = tag_sizeself.word_embeds = nn.Embedding(self.vocab_size,self.embedding_dim)self.pos1_embeds = nn.Embedding(self.pos_size,self.pos_dim)self.pos2_embeds = nn.Embedding(self.pos_size,self.pos_dim)self.lstm = nn.LSTM(input_size=self.embedding_dim + self.pos_dim * 2,hidden_size=self.hidden_dim // 2,num_layers=1,bidirectional=True)self.linear = nn.Linear(self.hidden_dim,self.tag_size)self.dropout_emb = nn.Dropout(p=0.2)self.dropout_lstm = nn.Dropout(p=0.2)self.dropout_att = nn.Dropout(p=0.2)self.att_weight = nn.Parameter(torch.randn(self.batch,1,self.hidden_dim).to(self.device))def init_hidden_lstm(self):return (torch.randn(2, self.batch, self.hidden_dim // 2).to(self.device),torch.randn(2, self.batch, self.hidden_dim // 2).to(self.device))def attention(self, H):M = F.tanh(H)a = F.softmax(torch.bmm(self.att_weight, M), dim=-1)a = torch.transpose(a, 1, 2)return torch.bmm(H, a)def forward(self, sentence, pos1, pos2):init_hidden = self.init_hidden_lstm()embeds = torch.cat((self.word_embeds(sentence), self.pos1_embeds(pos1), self.pos2_embeds(pos2)), 2)embeds = self.dropout_emb(embeds)embeds = torch.transpose(embeds, 0, 1)lstm_out, lstm_hidden = self.lstm(embeds, init_hidden)lstm_out = lstm_out.permute(1, 2, 0)lstm_out = self.dropout_lstm(lstm_out)att_out = F.tanh(self.attention(lstm_out))att_out = self.dropout_att(att_out).squeeze()result = self.linear(att_out)return result
第二步: 编写训练函数
-
实现训练函数train.py
-
代码位置: train.py
# coding:utf-8
from model.bilstm_atten import *
from utils.data_loader import *
from utils.process import *
import torch
import torch.nn as nn
import torch.optim as optim
import time
from tqdm import tqdmdef train(conf, vocab_size, pos_size, tag_size):# 加载数据集train_iter, test_iter = get_loader_data()print('训练数据集长度', len(train_iter))# 实例化Bilstm+attention模型ba_model = BiLSTM_ATT(conf, vocab_size, pos_size, tag_size).to(conf.device)print(ba_model)# 实例化优化器optimizer = optim.Adam(ba_model.parameters(), lr=conf.learning_rate)# 实例化损失函数criterion = nn.CrossEntropyLoss()# 实现模型训练ba_model.train()# 定义训练模型参数start_time = time.time()train_loss = 0 # 已经训练样本的损失train_acc = 0 # 已经训练样本的准确率total_iter_num = 0 # 训练迭代次数total_sample = 0 # 已经训练的样本数# 开始模型的训练for epoch in range(conf.epochs):for sentence, pos1, pos2, label, _, _, _ in tqdm(train_iter):# 将数据输入模型output = ba_model(sentence, pos1, pos2)# 计算损失loss = criterion(output, label)# 梯度清零optimizer.zero_grad()# 反向传播loss.backward()# 梯度更新optimizer.step()# 计算总损失total_iter_num += 1train_loss += loss.item()# 计算总准确率train_acc = train_acc + sum(torch.argmax(output, dim=1) == label).item()total_sample = total_sample + label.size()[0]# print(f'total_sample--->{total_sample}')# 每25次训练,打印日志if total_iter_num % 25 == 0:tmploss = train_loss / total_iter_numtmpacc = train_acc / total_sampleend_time = time.time()print('轮次: %d, 损失:%.6f, 时间:%d, 准确率:%.3f' % (epoch+1, tmploss, end_time-start_time, tmpacc))if epoch % 10 == 0:torch.save(ba_model.state_dict(), './save_model/20230228_new_model_%d.bin' % epoch)if __name__ == '__main__':word2id, id2word = get_word_id(conf.train_data_path)vocab_size = len(word2id)print(vocab_size)pos_size = 143tag_size = len(relation2id)train(conf, vocab_size, pos_size, tag_size)
- 模型训练结果展示:
结论: BiLSTM+Attention模型在训练集上的最终表现是ACC:80%