命名实体审核任务:模型训练

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


智能对话系统:Unit对话API

在线聊天的总体架构与工具介绍:Flask web、Redis、Gunicorn服务组件、Supervisor服务监控器、Neo4j图数据库

linux 安装 neo4jlinux 安装 Redissupervisor 安装

neo4j图数据库:Cypher

neo4j图数据库:结构化数据流水线、非结构化数据流水线

命名实体审核任务:BERT中文预训练模型

命名实体审核任务:构建RNN模型

命名实体审核任务:模型训练

命名实体识别任务:BiLSTM+CRF part1

命名实体识别任务:BiLSTM+CRF part2

命名实体识别任务:BiLSTM+CRF part3

在线部分:werobot服务、主要逻辑服务、句子相关模型服务、BERT中文预训练模型+微调模型(目的:比较两句话text1和text2之间是否有关联)、模型在Flask部署

系统联调测试与部署

离线部分+在线部分:命名实体审核任务RNN模型、命名实体识别任务BiLSTM+CRF模型、BERT中文预训练+微调模型、werobot服务+flask


5.5 进行模型训练

  • 学习目标:
    • 了解进行模型训练的步骤.
    • 掌握模型训练中每个步骤的实现过程.

  • 进行模型训练的步骤:
    • 第一步: 构建随机选取数据函数.
    • 第二步: 构建模型训练函数.
    • 第三步: 构建模型验证函数.
    • 第四步: 调用训练和验证函数.
    • 第五步: 绘制训练和验证的损失和准确率对照曲线.
    • 第六步: 模型保存.

  • 第一步: 构建随机选取数据函数
# 导入bert中文编码的预训练模型
from bert_chinese_encode import get_bert_encode_for_single
def randomTrainingExample(train_data):
    """随机选取数据函数, train_data是训练集的列表形式数据"""
    # 从train_data随机选择一条数据
    category, line = random.choice(train_data)
    # 将里面的文字使用bert进行编码, 获取编码后的tensor类型数据
    line_tensor = get_bert_encode_for_single(line)
    # 将分类标签封装成tensor
    category_tensor = torch.tensor([int(category)])
    # 返回四个结果
    return category, line, category_tensor, line_tensor

  • 输入参数:
# 将数据集加载到内存获得的train_data

  • 调用:
# 选择10条数据进行查看
for i in range(10):
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
    print('category =', category, '/ line =', line)


  • 输出效果:
category = 1 / line = 触觉失调
category = 0 / line = 颤震性理生
category = 0 / line = 征压血高娠妊
category = 1 / line = 食欲减退
category = 0 / line = 血淤道肠胃
category = 0 / line = 形畸节关
category = 0 / line = 咳呛水饮
category = 0 / line = 症痣巨
category = 1 / line = 昼盲
category = 1 / line = 眼神异常

  • 第二步: 构建模型训练函数
# 选取损失函数为NLLLoss()
criterion = nn.NLLLoss()
# 学习率为0.005
learning_rate = 0.005


def train(category_tensor, line_tensor):
    """模型训练函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层 
    hidden = rnn.initHidden()
    # 模型梯度归0
    rnn.zero_grad()
    # 遍历line_tensor中的每一个字的张量表示
    for i in range(line_tensor.size()[0]):
        # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
    # 根据损失函数计算损失, 输入分别是rnn的输出结果和真正的类别标签
    loss = criterion(output, category_tensor)
    # 将误差进行反向传播
    loss.backward()

    # 更新模型中所有的参数
    for p in rnn.parameters():
        # 将参数的张量表示与参数的梯度乘以学习率的结果相加以此来更新参数
        p.data.add_(-learning_rate, p.grad.data)

    # 返回结果和损失的值
    return output, loss.item()
  • 第三步: 模型验证函数
def valid(category_tensor, line_tensor):
    """模型验证函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层
    hidden = rnn.initHidden()
    # 验证模型不自动求解梯度
    with torch.no_grad():
        # 遍历line_tensor中的每一个字的张量表示    
        for i in range(line_tensor.size()[0]):
            # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
            output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)      
        # 获得损失
        loss = criterion(output, category_tensor)
     # 返回结果和损失的值
    return output, loss.item()
  • 第四步: 调用训练和验证函数
  • 构建时间计算函数:
import time
import math

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)
  • 输入参数:
# 假定模型训练开始时间是10min之前
since = time.time() - 10*60

  • 调用:
period = timeSince(since)
print(period)

  • 输出效果:
10m 0s

  • 调用训练和验证函数并打印日志
# 设置迭代次数为50000步
n_iters = 50000

# 打印间隔为1000步
plot_every = 1000


# 初始化打印间隔中训练和验证的损失和准确率
train_current_loss = 0
train_current_acc = 0
valid_current_loss = 0
valid_current_acc = 0


# 初始化盛装每次打印间隔的平均损失和准确率
all_train_losses = []
all_train_acc = []
all_valid_losses = []
all_valid_acc = []

# 获取开始时间戳
start = time.time()


# 循环遍历n_iters次 
for iter in range(1, n_iters + 1):
    # 调用两次随机函数分别生成一条训练和验证数据
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
    category_, line_, category_tensor_, line_tensor_ = randomTrainingExample(train_data)
    # 分别调用训练和验证函数, 获得输出和损失
    train_output, train_loss = train(category_tensor, line_tensor)
    valid_output, valid_loss = valid(category_tensor_, line_tensor_)
    # 进行训练损失, 验证损失,训练准确率和验证准确率分别累加
    train_current_loss += train_loss
    train_current_acc += (train_output.argmax(1) == category_tensor).sum().item()
    valid_current_loss += valid_loss
    valid_current_acc += (valid_output.argmax(1) == category_tensor_).sum().item()
    # 当迭代次数是指定打印间隔的整数倍时
    if iter % plot_every == 0:
        # 用刚刚累加的损失和准确率除以间隔步数得到平均值
        train_average_loss = train_current_loss / plot_every
        train_average_acc = train_current_acc/ plot_every
        valid_average_loss = valid_current_loss / plot_every
        valid_average_acc = valid_current_acc/ plot_every
        # 打印迭代步, 耗时, 训练损失和准确率, 验证损失和准确率
        print("Iter:", iter, "|", "TimeSince:", timeSince(start))
        print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
        print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
        # 将结果存入对应的列表中,方便后续制图
        all_train_losses.append(train_average_loss)
        all_train_acc.append(train_average_acc)
        all_valid_losses.append(valid_average_loss)
        all_valid_acc.append(valid_average_acc)
        # 将该间隔的训练和验证损失及其准确率归0
        train_current_loss = 0
        train_current_acc = 0
        valid_current_loss = 0
        valid_current_acc = 0
  • 输出效果:
Iter: 1000 | TimeSince: 0m 56s
Train Loss: 0.6127021567507527 | Train Acc: 0.747
Valid Loss: 0.6702297774022868 | Valid Acc: 0.7
Iter: 2000 | TimeSince: 1m 52s
Train Loss: 0.5190641692602076 | Train Acc: 0.789
Valid Loss: 0.5217500487511397 | Valid Acc: 0.784
Iter: 3000 | TimeSince: 2m 48s
Train Loss: 0.5398398997281778 | Train Acc: 0.8
Valid Loss: 0.5844468013737023 | Valid Acc: 0.777
Iter: 4000 | TimeSince: 3m 43s
Train Loss: 0.4700755337187358 | Train Acc: 0.822
Valid Loss: 0.5140456306522071 | Valid Acc: 0.802
Iter: 5000 | TimeSince: 4m 38s
Train Loss: 0.5260879981063878 | Train Acc: 0.804
Valid Loss: 0.5924804099237979 | Valid Acc: 0.796
Iter: 6000 | TimeSince: 5m 33s
Train Loss: 0.4702717279043861 | Train Acc: 0.825
Valid Loss: 0.6675750375208704 | Valid Acc: 0.78
Iter: 7000 | TimeSince: 6m 27s
Train Loss: 0.4734503294042624 | Train Acc: 0.833
Valid Loss: 0.6329268293256277 | Valid Acc: 0.784
Iter: 8000 | TimeSince: 7m 23s
Train Loss: 0.4258338176879665 | Train Acc: 0.847
Valid Loss: 0.5356959595441066 | Valid Acc: 0.82
Iter: 9000 | TimeSince: 8m 18s
Train Loss: 0.45773495503464817 | Train Acc: 0.843
Valid Loss: 0.5413714128659645 | Valid Acc: 0.798
Iter: 10000 | TimeSince: 9m 14s
Train Loss: 0.4856756244019302 | Train Acc: 0.835
Valid Loss: 0.5450502399195044 | Valid Acc: 0.813

  • 第五步: 绘制训练和验证的损失和准确率对照曲线
import matplotlib.pyplot as plt

plt.figure(0)
plt.plot(all_train_losses, label="Train Loss")
plt.plot(all_valid_losses, color="red", label="Valid Loss")
plt.legend(loc='upper left')
plt.savefig("./loss.png")


plt.figure(1)
plt.plot(all_train_acc, label="Train Acc")
plt.plot(all_valid_acc, color="red", label="Valid Acc")
plt.legend(loc='upper left')
plt.savefig("./acc.png")
  • 训练和验证损失对照曲线:

  • 训练和验证准确率对照曲线:

  • 分析:
    • 损失对照曲线一直下降, 说明模型能够从数据中获取规律,正在收敛, 准确率对照曲线中验证准确率一直上升,最终维持在0.98左右.

  • 第六步: 模型保存
# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)
  • 输出效果:
    • 在/data/doctor_offline/review_model/路径下生成BERT_RNN.pth文件.

  • 小节总结:
    • 学习了进行模型训练的步骤:
      • 第一步: 构建随机选取数据函数.
      • 第二步: 构建模型训练函数.
      • 第三步: 构建模型验证函数.
      • 第四步: 调用训练和验证函数.
      • 第五步: 绘制训练和验证的损失和准确率对照曲线.
      • 第六步: 模型保存.

5.6 模型使用

  • 学习目标:
    • 掌握模型预测的实现过程.
    • 掌握模型批量预测的实现过程.

  • 模型预测的实现过程:
import os
import torch
import torch.nn as nn

# 导入RNN模型结构
from RNN_MODEL import RNN
# 导入bert预训练模型编码函数
from bert_chinese_encode import get_bert_encode_for_single


# 预加载的模型参数路径
MODEL_PATH = './BERT_RNN.pth'

# 隐层节点数, 输入层尺寸, 类别数都和训练时相同即可
n_hidden = 128
input_size = 768
n_categories = 2

# 实例化RNN模型, 并加载保存模型参数
rnn = RNN(input_size, n_hidden, n_categories)
rnn.load_state_dict(torch.load(MODEL_PATH))




def _test(line_tensor):
    """模型测试函数, 它将用在模型预测函数中, 用于调用RNN模型并返回结果.它的参数line_tensor代表输入文本的张量表示"""
    # 初始化隐层张量
    hidden = rnn.initHidden()
    # 与训练时相同, 遍历输入文本的每一个字符
    for i in range(line_tensor.size()[0]):
        # 将其逐次输送给rnn模型
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
    # 获得rnn模型最终的输出
    return output


def predict(input_line):
    """模型预测函数, 输入参数input_line代表需要预测的文本"""
    # 不自动求解梯度
    with torch.no_grad():
        # 将input_line使用bert模型进行编码
        output = _test(get_bert_encode_for_single(input_line))
        # 从output中取出最大值对应的索引, 比较的维度是1
        _, topi = output.topk(1, 1)
        # 返回结果数值
        return topi.item()


tensor.topk演示:

>>> tr = torch.randn(1, 2)
>>> tr
tensor([[-0.1808, -1.4170]])
>>> tr.topk(1, 1)
torch.return_types.topk(values=tensor([[-0.1808]]), indices=tensor([[0]]))
  • 输入参数:
input_line = "点瘀样尖针性发多"

  • 调用:
result = predict(input_line)
print("result:", result)

  • 输出效果:
result: 0

  • 模型批量预测的实现过程:
def batch_predict(input_path, output_path):
    """批量预测函数, 以原始文本(待识别的命名实体组成的文件)输入路径
       和预测过滤后(去除掉非命名实体的文件)的输出路径为参数"""
    # 待识别的命名实体组成的文件是以疾病名称为csv文件名, 
    # 文件中的每一行是该疾病对应的症状命名实体
    # 读取路径下的每一个csv文件名, 装入csv列表之中
    csv_list = os.listdir(input_path)
    # 遍历每一个csv文件
    for csv in csv_list:
        # 以读的方式打开每一个csv文件
        with open(os.path.join(input_path, csv), "r") as fr:
            # 再以写的方式打开输出路径的同名csv文件
            with open(os.path.join(output_path, csv), "w") as fw:
                # 读取csv文件的每一行
                input_lines = fr.readlines()
                for input_line in input_lines:
                    # print("input_line",input_line)
                    # 使用模型进行预测
                    res = predict(input_line)
                    # 如果结果为1
                    if res:
                        # 说明审核成功, 写入到输出csv中
                        fw.write(input_line)
                    else:
                        pass
  • 输入参数:
input_path = "/data/doctor_offline/structured/noreview/"
output_path = "/data/doctor_offline/structured/reviewed/"

  • 调用:
batch_predict(input_path, output_path)

  • 输出效果:
    • 在输出路径下生成与输入路径等数量的同名csv文件, 内部的症状实体是被审核的可用实体.

  • 小节总结:
    • 学习并实现了模型预测的函数: predict(input_line).
    • 学习并实现了模型批量预测的函数: batch_predict(input_path, output_path)


 

import torch
import torch.nn as nn
import random
import pandas as pd
from collections import Counter
#构建时间计算函数
import time
import math

"""
1.离线部分中的命名实体的审核模型
    1.命名实体的审核模型:
        训练RNN模型让其学会判断结构化的未审核数据中的疾病名/疾病对应的症状名是否符合正常语序,RNN模型负责处理结构化的未审核数据,
        主要将结构化的未审核数据预测输出为结构化的审核过的数据,最终把结构化的审核过的数据(疾病名/疾病对应的症状名)存储到NEO4J数据库中。
    2.训练命名实体的审核模型:
        1.训练数据train_data.csv内容格式:1/0 疾病名/疾病对应的症状名
            第一列为:1/0。1代表正样本,正常语序。0代表负样本,为正常语序的倒序。
            第二列为:疾病名/疾病对应的症状名。
            1/0含义:
                1代表正样本,正常语序:1	手掌软硬度异常
                0代表负样本,为正常语序的倒序:0	常异度硬软掌手
        2.通过读取训练数据train_data.csv中“标记为1/0的正负样本”的疾病名/疾病对应的症状名的数据集,
          让RNN模型学会判断结构化的未审核数据中的疾病名/疾病对应的症状名是否符合正常语序。
    3.命名实体的审核模型的预测流程:
        1.命名实体的审核模型要读取的数据:structured/noreview文件夹中结构化的未审核数据
            (structured/noreview文件夹中结构化的未审核数据实际为命名实体的识别模型预测输出的数据)
            1.“作为csv文件名的”疾病名
            2.每个疾病名.csv中每行就是一个该疾病对应的症状
        2.命名实体的审核模型要预测输出的数据:structured/reviewed文件夹中已审核过的结构化的数据
            1.“作为csv文件名的”疾病名
            2.每个疾病名.csv中每行就是一个该疾病对应的症状
        3.读取structured/noreview文件夹中结构化的未审核数据(疾病名/疾病对应的症状名)进行模型预测判断是否符合正常语序,
          符合则输出存储到structured/reviewed文件夹中代表为已审核过的数据,反之不符合正常语序则丢弃。
          最终把审核通过的疾病名和疾病对应的症状名关联在一起存储到NEO4J数据库中。
          注意:
                第一种方式为对“作为csv文件名的”疾病名和“文件中的疾病对应的”症状名两者同事都进行模型的预测判断,
                第二种方式仅为对“文件中的疾病对应的”症状名进行模型的预测判断,而不对“作为csv文件名的”疾病名进行模型的预测判断。
                第二种方式的特别之处:
                    不使用命名实体的审核模型对“作为csv文件名的”疾病名进行预测判断,
                    而是改为通过人工方式判断“作为csv文件名的”疾病名是否符合正常语序。
                    因为通过人工方式判断便可以避免掉模型对“作为csv文件名的”疾病名的预测判断出现错误,
                    而导致了CSV文件中的症状名内容也一同被丢弃掉的情况,
                    判断避免掉疾病名的csv文件中的疾病对应的症状内容也一并被错误丢弃掉的情况。
   
2.离线部分中的命名实体的识别模型(NER模型:BiLSTM+CRF模型)
    1.命名实体的识别模型(NER模型):
        使用的模型组合为BiLSTM+CRF模型来作为命名实体的识别模型,NER模型负责处理非结构化数据,
        主要从长文本的样本句子中抽取出疾病名/症状名这样的命名实体输出为结构化的未审核数据。
        然后还需要使用命名实体的审核模型(RNN模型)对结构化的未审核数据进行审核(预测)输出为结构化的审核过的数据,
        最终把结构化的审核过的数据(疾病名/疾病对应的症状名)存储到NEO4J数据库中。
    2.训练命名实体的识别模型:
        1.训练数据total.txt内容格式:
            1.第一列为:每条样本句子中的字符。
              第二列为:每条样本句子中的字符对应的真实标签。
            2.真实标签列表:["O","B-dis","I-dis","B-sym","I-sym"]
                dis表示疾病(disease), sym表示症状(symptom), B表示命名实体开头, I表示命名实体中间到结尾, O表示其他类型。
                B-dis: Begin-disease(疾病名的开始)
                I-dis: Inter -disease(疾病名的从中间到结尾)
                B-sym: Begin-symptom(症状名的开始)
                I-sym: Inter-symptom(症状名的从中间到结尾) 
                O: Other 
        2.通过BiLSTM+CRF模型读取total.txt内容进行训练,让模型学会从普通文本句子中抽取出真实的疾病/疾病对应的症状相关的名称,
          并给抽取出疾病/疾病对应的症状相关的名称赋予预测标签。
    3.命名实体的识别模型(NER模型:BiLSTM+CRF模型)的预测:
        1.第一步:
                1.命名实体的识别模型要读取的数据:unstructured/norecognite文件夹中每个txt文件(即为非结构化数据)
                    1.“作为txt文件名的”疾病名
                    2.每个疾病名.txt中每行就是一条对该疾病进行症状描述的长文本语句
                2.命名实体的识别模型要预测输出的数据:structured/noreview文件夹中结构化的未审核数据
                    1.“作为csv文件名的”疾病名
                    2.每个疾病名.csv中每行就是一个该疾病对应的症状
                3.预测流程:
                    命名实体的识别模型读取出每个疾病.txt文件中的症状描述的长文本语句,
                    从长文本语句中抽取出对应该疾病名的短文本(单词)形式的症状名,
                    作为未审核的结构化的数据存储到structured/noreview文件夹中每个对应的疾病名.csv中。
        2.第二步:
                便是使用命名实体的审核模型(RNN模型) 对未审核数据中的疾病名/疾病对应的症状名进行预测判断是否符合正常语序。
                预测流程便为命名实体的审核模型的预测流程,最终把数据输出为structured/reviewed文件夹中已审核过的结构化的数据
 
3.离线部分中的结构化数据流水线 
    结构化的未审核数据:/data/structured/noreview文件夹中,每个csv文件名为疾病名,每个csv文件中的每行内容为疾病对应的症状名。
    结构化的已审核数据:/data/structured/reviewed文件夹中,每个csv文件名为疾病名,每个csv文件中的每行内容为疾病对应的症状名。
 
4.离线部分中的非结构化数据流水线
    非结构化数据:unstructured/norecognite文件夹中,每个txt文件为疾病名,每个txt文件中每行的内容为对该疾病的进行症状描述的长文本语句。
"""

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)

# 假定模型训练开始时间是10min之前
since = time.time() - 10*60
# 调用:
period = timeSince(since)
print(period)

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        """初始化函数中有三个参数,分别是输入张量最后一维的尺寸大小,
            隐层张量最后一维的尺寸大小, 输出张量最后一维的尺寸大小
        """
        super(RNN, self).__init__()
        # 传入隐含层尺寸大小
        self.hidden_size = hidden_size
        # 构建从输入到隐含层的线性变化, 这个线性层的输入尺寸是input_size + hidden_size
        # 这是因为在循环网络中, 每次输入都有两部分组成,分别是此时刻的输入和上一时刻产生的输出.
        # 这个线性层的输出尺寸是hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        # 构建从输入到输出层的线性变化, 这个线性层的输入尺寸还是input_size + hidden_size
        # 这个线性层的输出尺寸是output_size.
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        # 最后需要对输出做softmax处理, 获得结果:nn.LogSoftmax + nn.NLLLoss() = 交叉熵Cross Entropy
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):
        """在forward函数中, 参数分别是规定尺寸的输入张量, 以及规定尺寸的初始化隐层张量"""
        # 首先使用torch.cat将input与hidden进行张量拼接
        combined = torch.cat((input, hidden), 1)
        # 通过输入层到隐层变换获得hidden张量
        hidden = self.i2h(combined)
        # 通过输入到输出层变换获得output张量
        output = self.i2o(combined)
        # 对输出进行softmax处理
        output = self.softmax(output)
        # 返回输出张量和最后的隐层结果
        return output, hidden

    def initHidden(self):
        """隐层初始化函数"""
        # 将隐层初始化成为一个1xhidden_size的全0张量
        return torch.zeros(1, self.hidden_size)

"""
如果每次运行都重新下载“bert-base-chinese”的话,执行如下操作
    window:cd C:/Users/Administrator/.cache/torch/hub/huggingface_pytorch-transformers_master
    linux:cd /root/.cache/torch/hub/huggingface_pytorch-transformers_master
    activate pytorch
    pip install .
"""
# 通过torch.hub(pytorch中专注于迁移学的工具)获得已经训练好的bert-base-chinese模型
model =  torch.hub.load('huggingface/pytorch-transformers', 'model', 'bert-base-chinese')
# 获得对应的字符映射器, 它将把中文的每个字映射成一个数字
tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 'bert-base-chinese')

def get_bert_encode_for_single(text):
    """
    description: 使用bert-chinese编码中文文本
    :param text: 要进行编码的文本
    :return: 使用bert编码后的文本张量表示
    """
    # 首先使用字符映射器对每个汉字进行映射
    # 这里需要注意, bert的tokenizer映射后会为结果前后添加开始和结束标记即101和102
    # 这对于多段文本的编码是有意义的, 但在我们这里没有意义, 因此使用[1:-1]对头和尾进行切片
    indexed_tokens = tokenizer.encode(text)[1:-1]
    # 之后将列表结构转化为tensor
    tokens_tensor = torch.tensor([indexed_tokens])
    # print(tokens_tensor)
    # 使模型不自动计算梯度
    with torch.no_grad():
        # 调用模型获得隐层输出
        encoded_layers, _ = model(tokens_tensor)
    # 输出的隐层是一个三维张量, 最外层一维是1, 我们使用[0]降去它.
    # print(encoded_layers.shape)
    encoded_layers = encoded_layers[0]
    return encoded_layers

"""
进行模型训练的步骤:
    第一步: 构建随机选取数据函数.
    第二步: 构建模型训练函数.
    第三步: 构建模型验证函数.
    第四步: 调用训练和验证函数.
    第五步: 绘制训练和验证的损失和准确率对照曲线.
    第六步: 模型保存.
"""

#-----------------------------第一步: 构建随机选取数据函数---------------------------#
# 导入bert中文编码的预训练模型
def randomTrainingExample(train_data):
    """随机选取数据函数, train_data是训练集的列表形式数据"""
    # 从train_data随机选择一条数据
    category, line = random.choice(train_data)
    # 将里面的文字使用bert进行编码, 获取编码后的tensor类型数据
    line_tensor = get_bert_encode_for_single(line)
    # 将分类标签封装成tensor
    category_tensor = torch.tensor([int(category)])
    # 返回四个结果
    return category, line, category_tensor, line_tensor


# 读取数据
train_data_path = "./train_data.csv"
train_data= pd.read_csv(train_data_path, header=None, sep="\t")

# 打印正负标签比例
print(dict(Counter(train_data[0].values))) #{1: 5740, 0: 5740}

# 转换数据到列表形式
train_data = train_data.values.tolist()

# 将数据集加载到内存获得的train_data
# 选择10条数据进行查看
# for i in range(10):
#     category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
#     print('category =', category, '/ line =', line)

# 输出效果:
# category = 1 / line = 触觉失调
# category = 0 / line = 颤震性理生
# category = 0 / line = 征压血高娠妊
# category = 1 / line = 食欲减退
# category = 0 / line = 血淤道肠胃
# category = 0 / line = 形畸节关
# category = 0 / line = 咳呛水饮
# category = 0 / line = 症痣巨
# category = 1 / line = 昼盲
# category = 1 / line = 眼神异常


#-----------------------------第二步: 构建模型训练函数---------------------------#
# 选取损失函数为NLLLoss():nn.LogSoftmax + nn.NLLLoss() = 交叉熵Cross Entropy
criterion = nn.NLLLoss()
# 学习率为0.005
learning_rate = 0.005

input_size = 768
hidden_size = 128
n_categories = 2
rnn = RNN(input_size, hidden_size, n_categories)

def train(category_tensor, line_tensor):
    """模型训练函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层
    hidden = rnn.initHidden()
    # 模型梯度归0
    rnn.zero_grad()
    # 遍历line_tensor中的每一个字的张量表示
    for i in range(line_tensor.size()[0]):
        # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
    # print("output",output)
    # 根据损失函数计算损失, 输入分别是rnn的输出结果和真正的类别标签
    loss = criterion(output, category_tensor)
    # 将误差进行反向传播
    loss.backward()

    # 更新模型中所有的参数
    for p in rnn.parameters():
        # 将参数的张量表示与参数的梯度乘以学习率的结果相加以此来更新参数
        p.data.add_(-learning_rate, p.grad.data)

    # 返回结果和损失的值
    return output, loss.item()

#-----------------------------第三步: 模型验证函数---------------------------#

def valid(category_tensor, line_tensor):
    """模型验证函数, category_tensor代表类别张量, line_tensor代表编码后的文本张量"""
    # 初始化隐层
    hidden = rnn.initHidden()
    # 验证模型不自动求解梯度
    with torch.no_grad():
        # 遍历line_tensor中的每一个字的张量表示
        for i in range(line_tensor.size()[0]):
            # 然后将其输入到rnn模型中, 因为模型要求是输入必须是二维张量, 因此需要拓展一个维度, 循环调用rnn直到最后一个字
            output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
        # 获得损失
        loss = criterion(output, category_tensor)
     # 返回结果和损失的值
    return output, loss.item()

#-----------------------------第四步: 调用训练和验证函数---------------------------#

# 调用训练和验证函数并打印日志
# 设置迭代次数为50000步
n_iters = 1000
# 打印间隔为1000步
plot_every = 1000

# 初始化打印间隔中训练和验证的损失和准确率
train_current_loss = 0
train_current_acc = 0
valid_current_loss = 0
valid_current_acc = 0

# 初始化盛装每次打印间隔的平均损失和准确率
all_train_losses = []
all_train_acc = []
all_valid_losses = []
all_valid_acc = []

# 获取开始时间戳
start = time.time()

# 循环遍历n_iters次
for iter in range(1, n_iters + 1):
    # 调用两次随机函数分别生成一条训练和验证数据
    category, line, category_tensor, line_tensor = randomTrainingExample(train_data)
    category_, line_, category_tensor_, line_tensor_ = randomTrainingExample(train_data)
    # 分别调用训练和验证函数, 获得输出和损失
    train_output, train_loss = train(category_tensor, line_tensor)
    valid_output, valid_loss = valid(category_tensor_, line_tensor_)
    # 进行训练损失, 验证损失,训练准确率和验证准确率分别累加
    train_current_loss += train_loss
    train_current_acc += (train_output.argmax(1) == category_tensor).sum().item()
    valid_current_loss += valid_loss
    valid_current_acc += (valid_output.argmax(1) == category_tensor_).sum().item()
    # 当迭代次数是指定打印间隔的整数倍时
    if iter % plot_every == 0:
        # 用刚刚累加的损失和准确率除以间隔步数得到平均值
        train_average_loss = train_current_loss / plot_every
        train_average_acc = train_current_acc/ plot_every
        valid_average_loss = valid_current_loss / plot_every
        valid_average_acc = valid_current_acc/ plot_every
        # 打印迭代步, 耗时, 训练损失和准确率, 验证损失和准确率
        print("Iter:", iter, "|", "TimeSince:", timeSince(start))
        print("Train Loss:", train_average_loss, "|", "Train Acc:", train_average_acc)
        print("Valid Loss:", valid_average_loss, "|", "Valid Acc:", valid_average_acc)
        # 将结果存入对应的列表中,方便后续制图
        all_train_losses.append(train_average_loss)
        all_train_acc.append(train_average_acc)
        all_valid_losses.append(valid_average_loss)
        all_valid_acc.append(valid_average_acc)
        # 将该间隔的训练和验证损失及其准确率归0
        train_current_loss = 0
        train_current_acc = 0
        valid_current_loss = 0
        valid_current_acc = 0

#-----------------------------第五步: 绘制训练和验证的损失和准确率对照曲线---------------------------#

import matplotlib.pyplot as plt

plt.figure(0)
plt.plot(all_train_losses, label="Train Loss")
plt.plot(all_valid_losses, color="red", label="Valid Loss")
plt.legend(loc='upper left')
plt.savefig("./loss.png")
plt.show()

plt.figure(1)
plt.plot(all_train_acc, label="Train Acc")
plt.plot(all_valid_acc, color="red", label="Valid Acc")
plt.legend(loc='upper left')
plt.savefig("./acc.png")
plt.show()

#-----------------------------第六步: 模型保存---------------------------#
# 保存路径
MODEL_PATH = './BERT_RNN.pth'
# 保存模型参数
torch.save(rnn.state_dict(), MODEL_PATH)

import os
import torch
import torch.nn as nn

# 设备选择, 我们可以选择在cuda或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("device",device)

# 使用nn.RNN构建完成传统RNN使用类
class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        """
        input_size:输入数据的词嵌入维度为 57 维度的one-hot向量(26个小写字母+26个大写字母+5个常用标点符号(" .,;'")=57维度的one-hot向量)
        hidden_size:隐藏层中神经元数量为 128
        output_size:Linear输出层的输出尺寸为语言类别总数为 18
        num_layers:隐藏层层数 默认为 1
        """
        """初始化函数中传入4个参数
            input_size:词嵌入维度57,也即为input输入的最后一维大小 57
            hidden_size:隐藏层中神经元数量 128,也即为hn隐藏状态输入的最后一维大小 128
            output_size:Linear输出层 输出维度(语言类别总数) 18 
            num_layers:隐藏层数默认为 1
        """
        super(RNN, self).__init__()
        # 将hidden_size与num_layers传入其中
        self.hidden_size = hidden_size #隐藏层中神经元数量 128
        self.num_layers = num_layers #隐藏层层数 默认为 1

        #nn.RNN(输入数据的词嵌入维度 57, 隐藏层中神经元数量 128, 隐藏层层数 1)
        # 实例化预定义的nn.RNN, 它的三个参数分别是input_size, hidden_size, num_layers
        self.rnn = nn.RNN(input_size, hidden_size, num_layers)
        #Linear(输入维度为 隐藏层中神经元数量 128, 输出维度为 类别总数 18)
        # 实例化nn.Linear, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.linear = nn.Linear(hidden_size, output_size)
        # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果
        self.softmax = nn.LogSoftmax(dim=-1)

    """
    rnn_output, next_hidden = rnn(input, hidden)
        由于每次传入一个rnn节点的输入数据是一个字符(一个人名中的一个字母),
        shape为(当前样本的序列长度(字符个数) 1, 词嵌入维度 57)的 one-hot。
            input输入:torch.randn(当前批次的样本个数 1, 当前样本的序列长度(字符个数) 1, 词嵌入维度 57)
            hn隐藏状态输入:torch.randn(隐藏层层数 1, 当前样本的序列长度(字符个数) 1, 隐藏层中神经元数量 128)
            output输出:(当前批次的样本个数 1, 当前样本的序列长度(字符个数) 1, 隐藏层中神经元数量 128)	
            hn隐藏状态输出:(隐藏层层数 1, 当前样本的序列长度(字符个数) 1, 隐藏层中神经元数量 128)
    """
    def forward(self, input, hidden):
        """完成传统RNN中的主要逻辑, 输入参数input代表输入张量, 它的形状是1 x n_letters
           hidden代表RNN的隐层张量, 它的形状是self.num_layers x 1 x self.hidden_size
        """
        # 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        input = input.unsqueeze(0)
        # 将input和hidden输入到传统RNN的实例化对象中,如果num_layers=1, rr恒等于hn
        rr, hn = self.rnn(input, hidden)
        """
        Linear层:
            把(当前批次的样本个数 1, 当前样本的序列长度(字符个数) 1, 隐藏层中神经元数量 128)的rnn输出output
            通过 Linear层 转换为 (当前批次的样本个数 1, 当前样本的序列长度(字符个数)1, 语言类别总数 18),
            即隐藏层中神经元数量的128 转换为 语言类别总数18。
        LogSoftmax(dim=-1):用于把语言类别总数18维度的向量值转换为类别概率值。
        """
        # 将从RNN中获得的结果通过线性变换和softmax返回,同时返回hn作为后续RNN的输入
        return self.softmax(self.linear(rr)), hn

    def initHidden(self):
        """初始化隐层张量:
            不论是初始化输入隐藏层状态还是输出隐藏层状态 均是 (隐藏层层数, 一个句子单词个数, 隐藏层中神经元数量)
        """
        """ hn隐藏状态输入:torch.zeros(隐藏层层数 1, 当前样本的序列长度(字符个数) 1, 隐藏层中神经元数量 128) 初始化值为0的三维张量"""
        # 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
        return torch.zeros(self.num_layers, 1, self.hidden_size).to(device)


"""
如果每次运行都重新下载“bert-base-chinese”的话,执行如下操作
    window:cd C:/Users/Administrator/.cache/torch/hub/huggingface_pytorch-transformers_master
    linux:cd /root/.cache/torch/hub/huggingface_pytorch-transformers_master
    activate pytorch
    pip install .
"""
# 通过torch.hub(pytorch中专注于迁移学的工具)获得已经训练好的bert-base-chinese模型
model =  torch.hub.load('huggingface/pytorch-transformers', 'model', 'bert-base-chinese').to(device)
# 获得对应的字符映射器, 它将把中文的每个字映射成一个数字
tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 'bert-base-chinese')

def get_bert_encode_for_single(text):
    """
    description: 使用bert-chinese编码中文文本
    :param text: 要进行编码的文本
    :return: 使用bert编码后的文本张量表示
    """
    # print("text",text)
    # 首先使用字符映射器对每个汉字进行映射
    # 这里需要注意, bert的tokenizer映射后会为结果前后添加开始和结束标记即101和102
    # 这对于多段文本的编码是有意义的, 但在我们这里没有意义, 因此使用[1:-1]对头和尾进行切片
    indexed_tokens = tokenizer.encode(text)[1:-1]
    # print("indexed_tokens",indexed_tokens)

    # 之后将列表结构转化为tensor
    tokens_tensor = torch.tensor([indexed_tokens])
    # print(tokens_tensor)
    # 使模型不自动计算梯度
    with torch.no_grad():
        # 调用模型获得隐层输出
        encoded_layers, _ = model(tokens_tensor.to(device))
    # 输出的隐层是一个三维张量, 最外层一维是1, 我们使用[0]降去它.
    # print(encoded_layers.shape)
    encoded_layers = encoded_layers[0]
    return encoded_layers

# -----------------------------模型预测的实现过程---------------------------#

# 预加载的模型参数路径
MODEL_PATH = './BERT_RNN.pth'

# 隐层节点数, 输入层尺寸, 类别数都和训练时相同即可
n_hidden = 128
input_size = 768
n_categories = 2

# 实例化RNN模型, 并加载保存模型参数
rnn = RNN(input_size, n_hidden, n_categories).to(device)
rnn.load_state_dict(torch.load(MODEL_PATH))


def _test(line_tensor):
    # print("line_tensor",line_tensor.shape)
    """模型测试函数, 它将用在模型预测函数中, 用于调用RNN模型并返回结果.它的参数line_tensor代表输入文本的张量表示"""
    # 初始化隐层张量
    hidden = rnn.initHidden()
    # 与训练时相同, 遍历输入文本的每一个字符
    for i in range(line_tensor.size()[0]):
        # 将其逐次输送给rnn模型
        output, hidden = rnn(line_tensor[i].unsqueeze(0), hidden)
    output = output.squeeze(0)
    # 获得rnn模型最终的输出
    return output

def predict(input_line):
    """模型预测函数, 输入参数input_line代表需要预测的文本"""
    # 不自动求解梯度
    with torch.no_grad():
        # 将input_line使用bert模型进行编码
        output = _test(get_bert_encode_for_single(input_line))
        # print("output",output.shape)
        # 从output中取出最大值对应的索引, 比较的维度是1
        _, topi = output.topk(1, 1)
        # 返回结果数值
        return topi.item()

# #输入参数:
# input_line = "点瘀样尖针性发多"
# #调用:
# result = predict(input_line)
# print("result:", result)

# 模型批量预测的实现过程
def batch_predict(input_path, output_path):
    """批量预测函数, 以原始文本(待识别的命名实体组成的文件)输入路径
       和预测过滤后(去除掉非命名实体的文件)的输出路径为参数
    """
    # 待识别的命名实体组成的文件是以疾病名称为csv文件名,
    # 文件中的每一行是该疾病对应的症状命名实体
    # 读取路径下的每一个csv文件名, 装入csv列表之中
    csv_list = os.listdir(input_path)
    # 遍历每一个csv文件
    for csv in csv_list:
        # 以读的方式打开每一个csv文件
        with open(os.path.join(input_path, csv), "r", encoding="utf-8") as fr:
            # 再以写的方式打开输出路径的同名csv文件
            with open(os.path.join(output_path, csv), "w", encoding="utf-8") as fw:
                # 读取csv文件的每一行
                input_lines = fr.readlines()
                for input_line in input_lines:
                    # print("input_line",input_line)
                    # 使用模型进行预测
                    res = predict(input_line)
                    # 如果结果为1
                    if res:
                        # 说明审核成功, 写入到输出csv中
                        fw.write(input_line)
                    else:
                        pass

# 输入参数:
input_path = "./data/structured/noreview"
output_path = "./data/structured/reviewed"
# 调用:
batch_predict(input_path, output_path)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值