一、注意力机制实现过程

1.1 bilstm_atten.py

import torch
import torch.nn as nn

from P04_RE.Bilstm_Attention_RE.config import Config
from P04_RE.Bilstm_Attention_RE.utils.data_loader import word2id, get_data_loader
from P04_RE.Bilstm_Attention_RE.utils.process import relation2id


class BiLSTM_Attention(nn.Module):
    def __init__(self, config, vocab_size, pos_size, tag_size):
        '''
        初始化
        :param config: 配置文件对象
        :param vocab_size: 字符词表大小
        :param pos_size: 相对位置编码的数量
        :param tag_size: 标签数量
        '''
        super(BiLSTM_Attention, self).__init__()
        self.conf = config
        self.vocab_size = vocab_size
        self.pos_size = pos_size
        self.tag_size = tag_size

        # 定义词嵌入层
        self.wordEmbed = nn.Embedding(self.vocab_size, self.conf.embedding_dim)
        self.pos1Embed = nn.Embedding(self.pos_size, self.conf.pos_dim)
        self.pos2Embed = nn.Embedding(self.pos_size, self.conf.pos_dim)
        # 定义bilstm层
        self.bilstm = nn.LSTM(input_size=self.conf.embedding_dim + self.conf.pos_dim * 2,
                              hidden_size=self.conf.hidden_dim // 2,
                              bidirectional=True,
                              batch_first=True)
        # 定义全连接层
        self.fc = nn.Linear(self.conf.hidden_dim, self.tag_size)
        # 定义3个dropout层
        self.dropout_embed = nn.Dropout(p=0.2)
        self.dropout_lstm = nn.Dropout(p=0.2)
        self.dropout_attention = nn.Dropout(p=0.2)

        # 定义注意力参数,即wT,需要注意的是:不能将batch_size写死,因为一旦写死之后,后续在进行使用时,只能使用固定的batch_size
        # 所以,这里将batch_size设置成1,然后在具体使用时,根据具体的batch_size进行动态广播
        self.wT = nn.Parameter(torch.randn(1, 1, self.conf.hidden_dim)).to(self.conf.device)

    def forward(self, sentence, pos1, pos2):
        # 1)将sentence,pos1,pos2进行embedding处理,获取词嵌入向量后再将结果拼接到一起
        embeds = torch.concat([self.wordEmbed(sentence), self.pos1Embed(pos1), self.pos2Embed(pos2)], dim=-1)
        # print(f'embeds-->{embeds.shape}')
        embeds = self.dropout_embed(embeds)

        # 2)将拼接结果送入bilstm层进行训练
        lstm_out, (h_n, c_n) = self.bilstm(embeds)
        lstm_out = self.dropout_lstm(lstm_out)

        # 3)将bilstm层输出结果进行转置,然后送到注意力机制层中
        H = lstm_out.transpose(1, 2)
        attention_out = self.attention(H)
        attention_out = self.dropout_attention(attention_out)

        # 4)将注意力机制的结果进行降维,然后送入全连接层
        result = self.fc(attention_out.squeeze(-1))
        return result

    def attention(self, H):
        '''
        实现注意力机制
        :param H: bilstm层输出结果,维度为[batch_size, hidden_dim, seq_len]
        :return:
        '''
        # 1)经过tanh激活函数,得到M
        M = torch.tanh(H)
        # print(f'M-->{M.shape}')

        # 2)将wT和M进行相乘,然后送入softmax中,得到注意力权重
        alpha = torch.softmax(torch.matmul(self.wT, M), dim=-1)  # 使用matmul进行矩阵乘法,此时wT会自动进行广播
        # print(f'alpha-->{alpha.shape}')
        # print(f'alpha-->{alpha}')

        # 3)将 alpha 进行转置,然后再和H进行相乘
        attention_out = torch.matmul(H, alpha.transpose(1, 2))
        # print(f'attention_out-->{attention_out.shape}')

        # 4)返回经过tanh激活函数后的结果
        return torch.tanh(attention_out)



if __name__ == '__main__':
    config = Config()
    vocab_size = len(word2id)
    pos_size = 142
    tag_size = len(relation2id)
    model = BiLSTM_Attention(config, vocab_size, pos_size, tag_size).to(config.device)
    print(model)

    train_dataloader, test_dataloader = get_data_loader()
    for datas_tensor, labels_tensor, positionE1_tensor, positionE2_tensor, entities in train_dataloader:
        result = model(datas_tensor, positionE1_tensor, positionE2_tensor)
        print(f'result-->{result.shape}')
        break

1.2训练模型 train.py

训练函数基本步骤
1.构建数据迭代器Dataloader(包括数据处理与构建数据源Dataset)
2.实例化模型
3.实例化损失函数对象
4.实例化优化器对象
5.定义打印日志参数
6.开始训练
6.1 实现外层大循环epoch
    6.2 将模型设置为训练模式
    6.3 内部遍历数据迭代器dataloader
        1)将数据送入模型得到输出结果
        2)计算损失
        3)梯度清零: optimizer.zero_grad()
        4)反向传播(计算梯度): loss.backward()
        5)梯度更新(参数更新): optimizer.step()
        6)打印内部训练日志
    6.4 使用验证集进行模型评估【将模型设置为评估模式】
    6.5 保存模型: torch.save(model.state_dict(), "model_path")
6.6 打印外部训练日志

验证函数基本步骤——
1.定义打印日志参数
2.将模型设置为评估模式
3.内部遍历数据迭代器dataloader
  3.1 将数据送入模型得到输出结果
  3.2 计算损失
  3.3 处理结果
  3.4 统计批次内指标
4.统计整体指标1.4

import time

import torch
import torch.nn as nn
from tqdm import tqdm

from P04_RE.Bilstm_Attention_RE.config import Config
from P04_RE.Bilstm_Attention_RE.model.bilstm_atten import BiLSTM_Attention
from P04_RE.Bilstm_Attention_RE.utils.data_loader import get_data_loader, word2id
from P04_RE.Bilstm_Attention_RE.utils.process import relation2id


def model2dev(test_dataloader, model, criterion):
    # 1.定义打印日志参数
    train_loss = 0  # 每个批次的损失之和
    total_iter_num = 0  # 总的批次数
    train_acc = 0  # 预测正确的样本数
    total_sample = 0  # 总的样本数
    # 2.将模型设置为评估模式
    model.eval()
    # 3.内部遍历数据迭代器dataloader
    for index, (datas, labels, positionE1, positionE2, entities) in enumerate(tqdm(test_dataloader, desc='模型评估')):
        # 3.1 将数据送入模型得到输出结果
        output = model(datas, positionE1, positionE2)
        # 3.2 计算损失
        loss = criterion(output, labels)
        # 3.3 处理结果
        predict_labels = output.argmax(dim=-1)
        # predict_labels = torch.argmax(output, dim=-1)  # 另一种写法
        # 3.4 统计批次内指标
        train_loss += loss.item()
        train_acc += sum(predict_labels == labels)
        total_iter_num += 1
        total_sample += labels.shape[0]
    # 4.统计整体指标
    dev_loss = train_loss / total_iter_num
    dev_acc = train_acc / total_sample
    return dev_loss, dev_acc

def model2train(conf, vocab_size, pos_size, tag_size):
    # 1.构建数据迭代器Dataloader(包括数据处理与构建数据源Dataset)
    train_dataloader, test_dataloader = get_data_loader()
    # 2.实例化模型
    model = BiLSTM_Attention(conf, vocab_size, pos_size, tag_size).to(conf.device)
    print(f'model-->{model}')
    # 3.实例化损失函数对象
    criterion = nn.CrossEntropyLoss()
    # 4.实例化优化器对象
    optimizer = torch.optim.Adam(model.parameters(), lr=conf.learning_rate)
    # 5.定义打印日志参数
    start_time = time.time()
    train_loss = 0  # 每个批次的损失之和
    total_iter_num = 0  # 总的批次数
    train_acc = 0  # 预测正确的样本数
    total_sample = 0  # 总的样本数
    best_acc = 0  # 最佳准确率

    # 6.开始训练
    # 6.1 实现外层大循环epoch
    for epoch in range(conf.epochs):
        # 6.2 将模型设置为训练模式
        model.train()
        # 6.3 内部遍历数据迭代器dataloader
        for index, (datas, labels, positionE1, positionE2, entities) in enumerate(tqdm(train_dataloader, desc='模型训练')):
            # 1)将数据送入模型得到输出结果
            output = model(datas, positionE1, positionE2)
            # print(f'output-->{output.shape}')
            # 2)计算损失
            loss = criterion(output, labels)
            # print(f'loss-->{loss.item()}')
            # 3)梯度清零: optimizer.zero_grad()
            optimizer.zero_grad()
            # 4)反向传播(计算梯度): loss.backward()
            loss.backward()
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(parameters=model.parameters(), max_norm=10)
            # 5)梯度更新(参数更新): optimizer.step()
            optimizer.step()
            # 6)打印内部训练日志
            train_loss += loss.item()
            # 计算预测正确的样本数
            predict_labels = output.argmax(dim=-1)
            # print(f'predict_labels-->{predict_labels}')
            # print(f'labels-->{labels}')
            # print(f'predict_labels==labels-->{predict_labels == labels}')
            # print(f'sum(predict_labels==labels)-->{sum(predict_labels == labels)}')
            train_acc += sum(predict_labels == labels)
            total_iter_num += 1
            total_sample += labels.shape[0]
            # 每隔50次,打印日志
            if (index + 1) % 50 == 0:
                loss_avg = train_loss / total_iter_num
                acc_avg = train_acc / total_sample
                end_time = time.time()
                print(f'轮次:{epoch + 1},,训练损失:{loss_avg:.4f},训练准确率:{acc_avg:.4f},用时:{end_time - start_time:.2f}秒')
                # break
            # break
        # 6.4 使用验证集进行模型评估【将模型设置为评估模式】
        dev_loss, dev_acc = model2dev(test_dataloader, model, criterion)
        # print(f'dev_loss-->{dev_loss:.4f}')
        # print(f'dev_acc-->{dev_acc:.4f}')
        # 6.5 保存模型: torch.save(model.state_dict(), "model_path")
        if dev_acc > best_acc:
            best_acc = dev_acc
            torch.save(model.state_dict(), "save_model/bilstm_atten_best.pth")
            print(f'保存模型,准确率:{best_acc:.4f}, 平均损失:{dev_loss:.4f}')
        # break
    # 6.6 打印外部训练日志
    end_time = time.time()
    print(f'训练时间:{end_time - start_time:.2f}')


if __name__ == '__main__':
    conf = Config()
    vocab_size = len(word2id)
    pos_size = 142
    tag_size = len(relation2id)
    model2train(conf, vocab_size, pos_size, tag_size)

1.3模型预测 predict.py

import torch

from P04_RE.Bilstm_Attention_RE.config import Config
from P04_RE.Bilstm_Attention_RE.model.bilstm_atten import BiLSTM_Attention
from P04_RE.Bilstm_Attention_RE.utils.data_loader import word2id
from P04_RE.Bilstm_Attention_RE.utils.process import relation2id, sentence_padding, position_padding

id2relation = {v: k for k, v in relation2id.items()}

# 1.实例化模型
config = Config()
vocab_size = len(word2id)
pos_size = 142
tag_size = len(relation2id)
model = BiLSTM_Attention(config, vocab_size, pos_size, tag_size).to(config.device)
print(f'model-->{model}')

# 2.加载模型参数
model.load_state_dict(torch.load('save_model/bilstm_atten_best.pth', weights_only=True))

def model2predict(sample, entity1, entity2):
    '''
    :param sample: 样本(要预测的句子)
    :param entity1: 主实体
    :param entity2: 客实体
    :return:
    '''
    # 3.处理数据
    # 3.1 通过遍历,获取中间数据
    # 获取主实体的索引
    e1_index = sample.index(entity1)
    # 获取客实体的索引
    e2_index = sample.index(entity2)
    # 定义3个空列表,分别存储每个文本中的字符、主实体的相对位置编码和客实体的相对位置编码。
    sentence, position1, position2 = [], [], []
    for index, word in enumerate(sample):
        # ①遍历原始文本,将每个字符存储到一个子列表中,遍历完成后再存到datas列表中。
        sentence.append(word)
        # ②先获取主实体的索引,在遍历过程中使用原始索引-主实体的索引,获取相对于主实体的位置编码,存储到一个子列表中,遍历完成后再存到positionE1列表中。
        position1.append(index - e1_index)
        # ③使用相同的方式,获取客实体的相对位置编码。
        position2.append(index - e2_index)
    # print(f'sentence-->{sentence}')
    # print(f'position1-->{position1}')
    # print(f'position2-->{position2}')

    # 3.2 将字符转成id,且将负数转成正数,同时对齐长度
    sentence_id = sentence_padding(sentence, word2id)
    position1_ids = position_padding(position1)
    position2_ids = position_padding(position2)
    # print(f'sentence_id-->{sentence_id}')
    # print(f'position1_ids-->{position1_ids}')
    # print(f'position2_ids-->{position2_ids}')

    # 3.3 将数据转成张量,并且将数据移动到GPU上
    datas_tensor = torch.tensor([sentence_id], dtype=torch.long).to(config.device)   # 需要给数据添加一个batch_size维度
    positionE1_tensor = torch.tensor([position1_ids], dtype=torch.long).to(config.device)
    positionE2_tensor = torch.tensor([position2_ids], dtype=torch.long).to(config.device)
    # print(f'datas_tensor-->{datas_tensor.shape}')
    # print(f'positionE1_tensor-->{positionE1_tensor.shape}')
    # print(f'positionE2_tensor-->{positionE2_tensor.shape}')

    # 4.模型预测
    model.eval()
    with torch.no_grad():
        output = model(datas_tensor, positionE1_tensor, positionE2_tensor)
        # print(f'output-->{output}')

        # 5.结果解析
        predict_label = torch.argmax(output, dim=-1)[0].item()
        # print(f'predict_label-->{predict_label}')
        # 将标签类型id转成标签类型
        final_label = id2relation[predict_label]

        print(f'输入的句子为:{sample}')
        print(f'主实体为:{entity1}')
        print(f'客实体为:{entity2}')
        print(f'预测结果为:{final_label}')



if __name__ == '__main__':
    sample = '《爱人们的故事》是全基尚导演,裴勇俊、李英爱、李慧英等主演的18集爱情类型的电视剧'
    entity1 = '爱人们的故事'
    entity2 = '全基尚'
    model2predict(sample, entity1, entity2)

    print('----------------------------------------------------------------------------------------')
    entity2 = '裴勇俊'
    model2predict(sample, entity1, entity2)

二、BiLSTM+Attention模型优化项

1)模型优化

  • 句子嵌入方式:可以使用jieba分词得到词语,然后再使用词语的方式进行嵌入。

  • 替换BiLSTM:将BiLSTM替换成BERT/RoBERTa等这种预训练模型 或 BiGRU去 做语义编码,看是否可以提供模型的语义表达能力。

  • 多头注意力机制:借鉴Transformer中多头注意力机制,将单一注意力拆分到多个子空间,去捕捉不同维度的语义信息。

  • 修改注意力机制的方式:使用transformer中注意力机制的计算方式 或者先进行从concat再经过linear层的方式等,来计算注意力机制,看模型的性能效果。

  • 调整随机失活层:调整随机失活层的位置、有无或随机失活比例,来观察模型的性能变化。

2)训练过程的优化

  • shuffle设置:注意在真正训练时,需要将dataloader中的shuffle设置为True

  • 梯度裁剪:在反向传播时对梯度进行裁剪,防止梯度消失或爆炸。

  • 早停机制:监控验证集上F1值或其他关键指标,如果连续多个epoch未提升或者开始下降,则提前终止训练。

3)训练数据优化

  • 通过过采样或欠采样来解决样本不均衡问题

  • 通过同义词替换、回译、实体替换等方法来扩充数据集。或者直接使用大模型进行训练样本的生成。

三、Pipeline方法的优缺点

Pipline方法的主要特点是实体抽取和关系抽取是分开的

  • 优点:

    • 易于实现,实体模型和关系模型使用独立的数据集,不需要同时标注实体和关系的数据集.

    • 两者相互独立,若关系抽取模型没训练好不会影响到实体抽取.(这也会导致实体抽取会影响到关系抽取)

    • 可以解决sep问题(一句话里面只有一对实体,只有一个关系。可以提取实体关系)

  • 缺点:

    • 关系和实体两者是紧密相连的,互相之间的联系没有捕捉到.

    • 上游 NER 的错误会直接影响下游关系抽取,容易造成误差积累(实体抽取和关系抽取本来是有关联的,分开实现会导致误差累积).

    • BiLSTM+Attention难以处理EPO问题(多对实体共享一个实体,关系交叉。无法提取实体关系)

      鲁迅 出生于 绍兴, 鲁迅 代表作是 《朝花夕拾》

      实体对 1:(鲁迅,绍兴)

      实体对 2:(鲁迅,朝花夕拾)👉 共享头实体「鲁迅」,这就是 EPO 重叠问题

四、Joint方法实现关系抽取

4.1 概念

通过修改标注方法和模型结构直接输出文本中包含的(ei,rk,ej)三元组

4.2 类型

4.2.1 参数共享的联合模型【修改模型结构】

主体、客体和关系的抽取不是严格同步进行的 (通常是依次执行,但是某些情况下也可以其中两个任务一起进行) ,各个过程都可以得到一个loss值,==整个模型的loss是各过程loss值之和.

4.2.2 联合解码的联合模型【修改标注方法】

主体、客体和关系的抽取是同时进行的,通过一个模型直接得到SPO三元组.

BIOS里面的S指的是signle,表示如果实体是一个字,那么标记为s;

标签类型总数:2*3*N+1

2指的是主实体+客实体

3指的是B、I、S
N指的是有N种关系

1指的是非实体

例如   我是中国人

有一种标签类型是:

主实体会被标注为1

客实体会被标注为2

我-1s

中-2B

国-2I

人-2I

两个实体直接如果有a关系那么会被标注为:

a-1-S O a-2-B a-2-I a-2-I

五、Casrel模型架构

  • Casrel是2020 ACL 上的实体关系抽取的一篇论文,该论文的主要解决的问题为关系三元组重叠(EPO)问题;
  • CasRel 本质上是基于参数共享的联合实体关系抽取方法(顺序识别实体和关系)。

casrel实现流程:

BERT编码 → 线性+Sigmoid预测边界(预测每个token是不是实体的开始、结束位置) → 最近匹配生成候选实体 

输入隐藏层输出+subject的特征向量→ 使用预定义的关系匹配是否有合适的客实体→解决epo问题

两个线性层+两个sigmoid判断候选实体的开始结束索引:

线性层只作用在向量化的最后一个维度上面,得到一个数值以后在使用sigmoid函数判断是否是开头和结尾;

主实体的开始索引放在一起,结束索引放在一起,然后使用就近匹配原则进行匹配:

bert编码的结果+实体的平均向量作为模型的输入:

然后使用线性层+sigmoid函数预测关系和客实体:和预测主实体不同,这里输出的结果是每种关系的客实体开始位置和结束位置;

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐