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

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

人工智能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


做命名识别选用BiLSTM+CRF的重要原因:
    是因为医疗/法律等均是一个非常垂直的领域,尤其是医疗/法律领域的实体,通过自主构建BiLSTM+CRF模型做NER往往表现会更好。
    对于选择BERT预训练模型的话,它会在更泛化的领域上表现更好,对于严苛医疗领域实体带来的帮助并没有那么大。
    如果做的是一个闲聊问答机,那么使用BERT预训练模型做NER就是一个好的选择。

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

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

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文件中每行的内容为对该疾病的进行症状描述的长文本语句。

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

bert模型仅是判断前后两个句子是否有关联的二分类。
如果前后两句话有关联的话,是一起提取这两句话中关键的症状信息融合在一起作为查询条件然后查询数据库中对应的疾病名。  

损失函数的定义:

  • BiLSTM层的输出维度是tag_size, 也就是每个单词w_i映射到tag的发射概率值, 假设BiLSTM的输出矩阵是P, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率. 对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率.

  • 对于输入序列X对应的输出tag序列y, 定义分数如下(本质上就是发射概率和转移概率的累加和):

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

  • 利用softmax函数, 为每一个正确的tag序列y定义一个概率值, 在真实的训练中, 只需要最大化似然概率p(y|X)即可, 具体使用对数似然如下:

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

  • BiLSTM+CRF模型的实现:
    • 第一步: 构建神经网络
    • 第二步: 文本信息张量化
    • 第三步: 计算损失函数第一项的分值
    • 第四步: 计算损失函数第二项的分值
    • 第五步: 维特比算法的实现
    • 第六步: 完善BiLSTM_CRF类的全部功能
  • 第一步: 构建神经网络
# 导入相关包与模块
import torch
import torch.nn as nn

class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, 
                       num_layers, batch_size, sequence_length):
        '''
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_ix:           标签与id对照字典
        :param embedding_dim:       字嵌入维度(即LSTM输入层维度input_size)
        :param hidden_dim:          隐藏层向量维度
        :param num_layers:          神经网络的层数
        :param batch_size:          批次的数量
        :param sequence_length:     语句的限制最大长度
        '''
        # 继承函数的初始化
        super(BiLSTM_CRF, self).__init__()
        # 设置标签与id对照
        self.tag_to_ix = tag_to_ix
        # 设置标签大小,对应 BiLSTM 最终输出分数矩阵宽度
        self.tagset_size = len(tag_to_ix)
        # 设定 LSTM 输入特征大小
        self.embedding_dim = embedding_dim
        # 设置隐藏层维度
        self.hidden_dim = hidden_dim
        # 设置单词总数的大小
        self.vocab_size = vocab_size
        # 设置隐藏层的数量
        self.num_layers = num_layers
        # 设置语句的最大限制长度
        self.sequence_length = sequence_length
        # 设置批次的大小
        self.batch_size = batch_size

        # 构建词嵌入层, 两个参数分别是单词总数, 词嵌入维度
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)

        # 构建双向LSTM层, 输入参数包括词嵌入维度, 隐藏层大小, 堆叠的LSTM层数, 是否双向标志位
        self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=self.num_layers, bidirectional=True)

        # 构建全连接线性层, 一端对接LSTM隐藏层, 另一端对接输出层, 相应的维度就是标签数量tagset_size
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        # 初始化转移矩阵, 转移矩阵是一个方阵[tagset_size, tagset_size]
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))

        # 按照损失函数小节的定义, 任意的合法句子不会转移到"START_TAG", 因此设置为-10000
        # 同理, 任意合法的句子不会从"STOP_TAG"继续向下转移, 也设置为-10000
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000

        # 初始化隐藏层, 利用单独的类函数init_hidden()来完成
        self.hidden = self.init_hidden()

    # 定义类内部专门用于初始化隐藏层的函数
    def init_hidden(self):
        # 为了符合LSTM的输入要求, 我们返回h0, c0, 这两个张量的shape完全一致
        # 需要注意的是shape: [2 * num_layers, batch_size, hidden_dim / 2]
        return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2), 
                 torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2))
  • 输入参数:
# 开始字符和结束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的字符和序号的对应码表
char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,
              "状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}
  • 调用:
model = BiLSTM_CRF(vocab_size=len(char_to_id),
                   tag_to_ix=tag_to_ix,
                   embedding_dim=EMBEDDING_DIM,
                   hidden_dim=HIDDEN_DIM,
                   num_layers=NUM_LAYERS,
                   batch_size=BATCH_SIZE,
                   sequence_length=SENTENCE_LENGTH)

print(model)
  • 输出效果:
BiLSTM(
  (word_embeds): Embedding(14, 200)
  (lstm): LSTM(200, 50, bidirectional=True)
  (hidden2tag): Linear(in_features=100, out_features=7, bias=True)
)
  • 第二步: 文本信息张量化
# 函数sentence_map完成中文文本信息的数字编码, 变成张量
def sentence_map(sentence_list, char_to_id, max_length):
    # 对一个批次的所有语句按照长短进行排序, 此步骤非必须
    sentence_list.sort(key=lambda c:len(c), reverse=True)
    # 定义一个最终存储结果特征向量的空列表
    sentence_map_list = []
    # 循环遍历一个批次内的所有语句
    for sentence in sentence_list:
        # 采用列表生成式完成字符到id的映射
        sentence_id_list = [char_to_id[c] for c in sentence]
        # 长度不够的部分用0填充
        padding_list = [0] * (max_length-len(sentence))
        # 将每一个语句向量扩充成相同长度的向量
        sentence_id_list.extend(padding_list)
        # 追加进最终存储结果的列表中
        sentence_map_list.append(sentence_id_list)
    # 返回一个标量类型值的张量
    return torch.tensor(sentence_map_list, dtype=torch.long)


# 在类中将文本信息经过词嵌入层, BiLSTM层, 线性层的处理, 最终输出句子张量
def _get_lstm_features(self, sentence):
    self.hidden = self.init_hidden()
    # a = self.word_embeds(sentence)
    # print(a.shape)  torch.Size([8, 20, 200])
    # LSTM的输入要求形状为 [sequence_length, batch_size, embedding_dim]
    # LSTM的隐藏层h0要求形状为 [num_layers * direction, batch_size, hidden_dim]
    embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)

    # LSTM的两个输入参数: 词嵌入后的张量, 随机初始化的隐藏层张量
    lstm_out, self.hidden = self.lstm(embeds, self.hidden)

    # 要保证输出张量的shape: [sequence_length, batch_size, hidden_dim]
    lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)

    # 将BiLSTM的输出经过一个全连接层, 得到输出张量shape:[sequence_length, batch_size, tagset_size]
    lstm_feats = self.hidden2tag(lstm_out)
    return lstm_feats
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]
  • 调用:
char_to_id = {"<PAD>":0}

if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    print("sentence_sequence:\n", sentence_sequence)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    sentence_features = model._get_lstm_features(sentence_sequence)
    print("sequence_features:\n", sentence_features)
  • 输出效果:
sentence_sequence:
 tensor([[14, 15, 16, 17, 18, 16, 19, 20, 21, 13, 22, 23, 24, 25, 26, 27, 28, 29,
         30,  0],
        [14, 15, 26, 27, 18, 49, 50, 12, 21, 13, 22, 51, 52, 25, 53, 54, 55, 29,
         30,  0],
        [14, 15, 53, 56, 18, 49, 50, 18, 26, 27, 57, 58, 59, 22, 51, 52, 55, 29,
          0,  0],
        [37, 63, 64, 65, 66, 55, 13, 22, 61, 51, 52, 25, 67, 68, 69, 70, 71, 13,
          0,  0],
        [37, 38, 39,  7,  8, 40, 41, 42, 43, 44, 45, 46, 47, 48,  0,  0,  0,  0,
          0,  0],
        [16, 17, 18, 53, 56, 12, 59, 60, 22, 61, 51, 52, 12, 62,  0,  0,  0,  0,
          0,  0],
        [ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,  0,  0,  0,  0,  0,
          0,  0],
        [31, 32, 24, 33, 34, 35, 36, 13, 30,  0,  0,  0,  0,  0,  0,  0,  0,  0,
          0,  0]])
sequence_features:
 tensor([[[ 0.5118,  0.0895, -0.2030,  ..., -0.2605, -0.2138, -0.0192],
         [ 0.1473, -0.0844, -0.1976,  ..., -0.0260, -0.1921,  0.0378],
         [-0.2201,  0.0790, -0.0173,  ...,  0.1551, -0.0899,  0.2035],
         ...,
         [-0.2387,  0.4015, -0.1882,  ..., -0.0473, -0.0399, -0.2642],
         [ 0.1203,  0.2065,  0.0764,  ...,  0.1412, -0.0817,  0.1800],
         [ 0.0362,  0.1477, -0.0596,  ...,  0.1640, -0.0790,  0.0359]],

        [[ 0.1481, -0.0057, -0.1339,  ...,  0.0348, -0.1515,  0.0797],
         [ 0.1469,  0.0430, -0.1578,  ..., -0.0599, -0.1647,  0.2721],
         [-0.1601,  0.2572,  0.0821,  ...,  0.0455, -0.0430,  0.2123],
         ...,
         [-0.0230,  0.3032, -0.2572,  ..., -0.1670, -0.0009, -0.1256],
         [-0.0643,  0.1889,  0.0266,  ..., -0.1044, -0.2333,  0.1548],
         [ 0.1969,  0.4262, -0.0194,  ...,  0.1344,  0.0094, -0.0583]],

        [[ 0.2893, -0.0850, -0.1214,  ...,  0.0855,  0.0234,  0.0684],
         [-0.0185,  0.0532, -0.1170,  ...,  0.2265, -0.0688,  0.2116],
         [-0.0882, -0.0393, -0.0658,  ...,  0.0006, -0.1219,  0.1954],
         ...,
         [ 0.0035,  0.0627, -0.1165,  ..., -0.1742, -0.1552, -0.0772],
         [-0.1099,  0.2375, -0.0568,  ..., -0.0636, -0.1998,  0.1747],
         [ 0.1005,  0.3047, -0.0009,  ...,  0.1359, -0.0076, -0.1088]],

        ...,

        [[ 0.3587,  0.0157, -0.1612,  ...,  0.0327, -0.3009, -0.2104],
         [ 0.2939, -0.1935, -0.1481,  ...,  0.0349, -0.1136,  0.0226],
         [ 0.1832, -0.0890, -0.3369,  ...,  0.0113, -0.1601, -0.1295],
         ...,
         [ 0.1462,  0.0905, -0.1082,  ...,  0.1253, -0.0416, -0.0082],
         [ 0.2161,  0.0444,  0.0300,  ...,  0.2624, -0.0970,  0.0016],
         [-0.0896, -0.0905, -0.1790,  ...,  0.0711, -0.0477, -0.1236]],

        [[ 0.2954,  0.0616, -0.0810,  ..., -0.0213, -0.1283, -0.1051],
         [-0.0038, -0.1580, -0.0555,  ..., -0.1327, -0.1139,  0.2161],
         [ 0.1022,  0.1964, -0.1896,  ..., -0.1081, -0.1491, -0.1872],
         ...,
         [ 0.3404, -0.0456, -0.2569,  ...,  0.0701, -0.1644, -0.0731],
         [ 0.4573,  0.1885, -0.0779,  ...,  0.1605, -0.1966, -0.0589],
         [ 0.1448, -0.1581, -0.3021,  ...,  0.0837, -0.0334, -0.2364]],

        [[ 0.3556,  0.0299, -0.1570,  ...,  0.0512, -0.3286, -0.2882],
         [ 0.2074, -0.1521, -0.1487,  ...,  0.0637, -0.2674, -0.0174],
         [ 0.0976, -0.0754, -0.2779,  ..., -0.1588, -0.2096, -0.3432],
         ...,
         [ 0.4961,  0.0583, -0.2965,  ...,  0.0363, -0.2933, -0.1551],
         [ 0.4594,  0.3354, -0.0093,  ...,  0.1681, -0.2508, -0.1423],
         [ 0.0957, -0.0486, -0.2616,  ...,  0.0578, -0.0737, -0.2259]]],
       grad_fn=<AddBackward0>)
  • 第三步: 计算损失函数第一项的分值
# 若干辅助函数, 在类BiLSTM外部定义, 目的是辅助log_sum_exp()函数的计算
# 将Variable类型变量内部的真实值, 以python float类型返回
def to_scalar(var): # var是Variable, 维度是1
    # 返回一个python float类型的值
    return var.view(-1).data.tolist()[0]


# 获取最大值的下标
def argmax(vec):
    # 返回列的维度上的最大值下标, 此下标是一个标量float
    _, idx = torch.max(vec, 1)
    return to_scalar(idx)


# 辅助完成损失函数中的公式计算
def log_sum_exp(vec): # vec是1 * 7, type是Variable
    max_score = vec[0, argmax(vec)]
    #max_score维度是1, max_score.view(1,-1)维度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的维度1 * 7
    # 经过expand()之后的张量, 里面所有的值都相同, 都是最大值max_score
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) # vec.size()维度是1 * 7
    # 先减去max_score,最后再加上max_score, 是为了防止数值爆炸, 纯粹是代码上的小技巧
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))



# 计算损失函数第一项的分值函数, 本质上是发射矩阵和转移矩阵的累加和
def _forward_alg(self, feats):
    # 初始化一个alphas张量, 代表转移矩阵的起始位置
    init_alphas = torch.full((1, self.tagset_size), -10000.)
    # init_alphas: [1, 7] , [-10000, -10000, -10000, -10000, -10000, -10000, -10000]
    # 仅仅把START_TAG赋值为0, 代表着接下来的转移只能从START_TAG开始
    init_alphas[0][self.tag_to_ix[START_TAG]] = 0.

    # 前向计算变量的赋值, 这样在反向求导的过程中就可以自动更新参数
    forward_var = init_alphas

    # 输入进来的feats: [20, 8, 7], 为了接下来按句子进行计算, 要将batch_size放在第一个维度上
    feats = feats.transpose(1, 0)

    # feats: [8, 20, 7]是一个3维矩阵, 最外层代表8个句子, 内层代表每个句子有20个字符, 
    # 每一个字符映射成7个标签的发射概率
    # 初始化最终的结果张量, 每个句子对应一个分数
    result = torch.zeros((1, self.batch_size))
    idx = 0

    # 按行遍历, 总共循环batch_size次
    for feat_line in feats:
        # 遍历一行语句, 每一个feat代表一个time_step
        for feat in feat_line:
            # 当前time_step的一个forward tensors
            alphas_t = []
            # 在当前time_step, 遍历所有可能的转移标签, 进行累加计算
            for next_tag in range(self.tagset_size):
                # 广播发射矩阵的分数
                emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)

                # 第i个time_step循环时, 转移到next_tag标签的转移概率
                trans_score = self.transitions[next_tag].view(1, -1)

                # 将前向矩阵, 转移矩阵, 发射矩阵累加
                next_tag_var = forward_var + trans_score + emit_score

                # 计算log_sum_exp()函数值
                # a = log_sum_exp(next_tag_var), 注意: log_sum_exp()函数仅仅返回一个实数值
                # print(a.shape) : tensor(1.0975) , ([])
                # b = a.view(1) : tensor([1.0975]), 注意: a.view(1)的操作就是将一个数字变成一个一阶矩阵, ([]) -> ([1])
                # print(b.shape) : ([1])
                alphas_t.append(log_sum_exp(next_tag_var).view(1))
            # print(alphas_t) : [tensor([337.6004], grad_fn=<ViewBackward>), tensor([337.0469], grad_fn=<ViewBackward>), tensor([337.8497], grad_fn=<ViewBackward>), tensor([337.8668], grad_fn=<ViewBackward>), tensor([338.0186], grad_fn=<ViewBackward>), tensor([-9662.2734], grad_fn=<ViewBackward>), tensor([337.8692], grad_fn=<ViewBackward>)]
            # temp = torch.cat(alphas_t)
            # print(temp) : tensor([[  337.6004,   337.0469,   337.8497,   337.8668,   338.0186, -9662.2734, 337.8692]])
            # 将列表张量转变为二维张量
            forward_var = torch.cat(alphas_t).view(1, -1)
            # print(forward_var.shape) : [1, 7]
        # print(forward_var) : tensor([[   13.7928,    16.0067,    14.1092, -9984.7852,    15.8380]])
        # 添加最后一步转移到"STOP_TAG"的分数, 就完成了整条语句的分数计算
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        # print(terminal_var) : tensor([[  339.2167,   340.8612,   340.2773,   339.0194,   340.8908, -9659.5732, -9660.0527]])
        # 计算log_sum_exp()函数值, 作为一条样本语句的最终得分
        alpha = log_sum_exp(terminal_var)
        # print(alpha) : tensor(341.9394)
        # 将得分添加进结果列表中, 作为函数结果返回
        result[0][idx] = alpha
        idx += 1
    return result
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]

  • 调用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    for epoch in range(1):
        model.zero_grad()
        feats = model._get_lstm_features(sentence_sequence)

        forward_score = model._forward_alg(feats)
        print(forward_score)
  • 输出效果:
tensor([[ 44.0279,  87.6439, 132.7635, 176.7535, 221.1325, 265.4456, 309.8346,
         355.9332]], grad_fn=<CopySlices>)
  • 第四步: 计算损失函数第二项的分值
def _score_sentence(self, feats, tags):
    # feats: [20, 8, 7] , tags: [8, 20]
    # 初始化一个0值的tensor, 为后续累加做准备
    score = torch.zeros(1)
    # 将START_TAG和真实标签tags做列维度上的拼接
    temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long)
    tags = torch.cat((temp, tags), dim=1)

    # 将传入的feats形状转变为[bathc_size, sequence_length, tagset_size]
    feats = feats.transpose(1, 0)
    # feats: [8, 20, 7]
    idx = 0

    # 初始化最终的结果分数张量, 每一个句子得到一个分数
    result = torch.zeros((1, self.batch_size))
    for feat_line in feats:
        # 注意: 此处区别于第三步的循环, 最重要的是这是在真实标签指导下的转移矩阵和发射矩阵的累加分数
        for i, feat in enumerate(feat_line):
            score = score + self.transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]
        # 最后加上转移到STOP_TAG的分数
        score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]
        result[0][idx] = score
        idx += 1
        score = torch.zeros(1)
    return result
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1

# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]

# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    for epoch in range(1):
        model.zero_grad()

        feats = model._get_lstm_features(sentence_sequence)

        gold_score = model._score_sentence(feats, tags)
        print(gold_score)
  • 输出效果:
tensor([[ 5.3102,  9.0228, 14.7486, 19.5984, 32.4324, 37.9789, 57.8647, 66.8853]],
       grad_fn=<CopySlices>)
  • 第五步: 维特比算法的实现
# 根据传入的语句特征feats, 推断出标签序列
def _viterbi_decode(self, feats):
    # 初始化最佳路径结果的存放列表
    result_best_path = []
    # 将输入张量变形为[batch_size, sequence_length, tagset_size]
    feats = feats.transpose(1, 0)

    # 对批次中的每一行语句进行遍历, 每个语句产生一个最优标注序列
    for feat_line in feats:
        backpointers = []

        # 初始化前向传播的张量, 设置START_TAG等于0, 约束合法序列只能从START_TAG开始
        init_vvars = torch.full((1, self.tagset_size), -10000.)
        init_vvars[0][self.tag_to_ix[START_TAG]] = 0

        # 在第i个time_step, 张量forward_var保存第i-1个time_step的viterbi变量
        forward_var = init_vvars

        # 依次遍历i=0, 到序列最后的每一个time_step
        for feat in feat_line:
            # 保存当前time_step的回溯指针
            bptrs_t = []
            # 保存当前time_step的viterbi变量
            viterbivars_t = []

            for next_tag in range(self.tagset_size):
                # next_tag_var[i]保存了tag_i 在前一个time_step的viterbi变量
                # 前向传播张量forward_var加上从tag_i转移到next_tag的分数, 赋值给next_tag_var
                # 注意此处没有加发射矩阵分数, 因为求最大值不需要发射矩阵
                next_tag_var = forward_var + self.transitions[next_tag]

                # 将最大的标签id加入到当前time_step的回溯列表中
                best_tag_id = argmax(next_tag_var)
                bptrs_t.append(best_tag_id)
                viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

            # 此处再将发射矩阵分数feat加上, 赋值给forward_var, 作为下一个time_step的前向传播张量
            forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)

            # 当前time_step的回溯指针添加进当前这一行样本的总体回溯指针中
            backpointers.append(bptrs_t)

        # 最后加上转移到STOP_TAG的分数
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        best_tag_id = argmax(terminal_var)
        # path_score是整个路径的总得分
        path_score = terminal_var[0][best_tag_id]

        # 根据回溯指针, 解码最佳路径
        # 首先把最后一步的id值加入
        best_path = [best_tag_id]
        # 从后向前回溯最佳路径
        for bptrs_t in reversed(backpointers):
            # 通过第i个time_step得到的最佳id, 找到第i-1个time_step的最佳id
            best_tag_id = bptrs_t[best_tag_id]
            best_path.append(best_tag_id)

        # 将START_TAG删除
        start = best_path.pop()
        # 确认一下最佳路径中的第一个标签是START_TAG
        assert start == self.tag_to_ix[START_TAG]

        # 因为是从后向前回溯, 所以再次逆序得到总前向后的真实路径
        best_path.reverse()
        # 当前这一行的样本结果添加到最终的结果列表里
        result_best_path.append(best_path)

    return result_best_path
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1

# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]

# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    for epoch in range(1):
        model.zero_grad()

        feats = model._get_lstm_features(sentence_sequence)

        result_tags = model._viterbi_decode(feats)
        print(result_tags)
  • 输出效果:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]]
  • 第六步: 完善BiLSTM类的全部功能
# 对数似然函数的计算, 输入的是数字化编码后的语句, 和真实的标签
# 注意: 这个函数是未来真实训练中要用到的"虚拟化的forward()"
def neg_log_likelihood(self, sentence, tags):
    # 第一步先得到BiLSTM层的输出特征张量
    feats = self._get_lstm_features(sentence)

    # feats : [20, 8, 7] 代表一个批次有8个样本, 每个样本长度20
    # 每一个word映射到7个标签的概率, 发射矩阵

    # forward_score 代表公式推导中损失函数loss的第一项
    forward_score = self._forward_alg(feats)

    # gold_score 代表公式推导中损失函数loss的第二项
    gold_score = self._score_sentence(feats, tags)

    # 按行求和, 在torch.sum()函数值中, 需要设置dim=1 ; 同理, dim=0代表按列求和
    # 注意: 在这里, 通过forward_score和gold_score的差值来作为loss, 用来梯度下降训练模型
    return torch.sum(forward_score - gold_score, dim=1)

# 此处的forward()真实场景是用在预测部分, 训练的时候并没有用到
def forward(self, sentence):
    # 获取从BiLSTM层得到的发射矩阵
    lstm_feats = self._get_lstm_features(sentence)

    # 通过维特比算法直接解码最佳路径
    tag_seq = self._viterbi_decode(lstm_feats)
    return tag_seq
  •  输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1

# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]

# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)

    for epoch in range(1):
        model.zero_grad()

        loss = model.neg_log_likelihood(sentence_sequence, tags)
        print(loss)

        loss.backward()
        optimizer.step()

        result = model(sentence_sequence)
        print(result)
  • 输出效果:
tensor([2347.2678], grad_fn=<SumBackward1>)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

6.5 模型训练

  • 学习目标:
    • 掌握数据的预处理流程
    • 掌握生成批量训练数据的方法
    • 掌握模型训练代码
  • 模型训练的流程
    • 第一步: 熟悉字符到数字编码的码表
    • 第二步: 熟悉训练数据集的样式和含义解释
    • 第三步: 生成批量训练数据
    • 第四步: 完成准确率和召回率的评估代码
    • 第五步: 完成训练模型的代码
    • 第六步: 绘制损失曲线和评估曲线图
  • 第一步: 熟悉字符到数字编码的码表.
# 代表了数据集中所有字符到数字编码的字典映射
# 码表可以包含中文简体、繁体、英文大小写字母、数字、中英文标点符号等等
# <PAD>为填充标识, 训练时需要将句子转化成矩阵, 而句子长短不一, 需要做padding处理

{
    "<PAD>": 0,
    "厑": 1,
    "吖": 2,
    "呵": 3,
    "啊": 4,
    "嗄": 5,
    "嬶": 6,
    ...
}
  •  第二步: 熟悉训练数据集的样式和含义解释.
荨   B-dis
麻   I-dis
疹   I-dis
这   O
么   O
痒   O
咋   O
办   O
。   O

突   O
然   O
头   B-sym
晕   I-sym
呕   B-sym
吐   I-sym
。   O
  • 训练数据集的含义解释:
    • 每一行包含一个字以及与之对应的标签, 字与标签之间通过\t分隔
    • 句子与句子之间通过空行分隔
    • 标签说明:
      • B-dis: 疾病实体名词起始标识
      • I-dis: 疾病实体名词中间到结尾标识
      • B-sym: 症状实体名词起始标识
      • I-sym: 症状实体名词中间到结尾标识
      • O: 其他非实体部分标识
  • 将训练数据集转换为数字化编码集:
# 导入包
import json
import numpy as np

# 创建训练数据集, 从原始训练文件中将中文字符进行数字编码, 并将标签页进行数字编码
def create_train_data(train_data_file, result_file, json_file, tag2id, max_length=20):
    # 导入json格式的中文字符到id的映射表
    char2id = json.load(open(json_file, mode='r', encoding='utf-8'))

    char_data, tag_data = [], []

    # 打开原始训练文件
    with open(train_data_file, mode='r', encoding='utf-8') as f:
        # 初始化一条语句数字化编码后的列表
        char_ids = [0] * max_length
        tag_ids = [0] * max_length
        idx = 0
        for line in f.readlines():
            line = line.strip('\n').strip()
            # 如果不是空行, 并且当前语句长度没有超过max_length, 则进行字符到id的映射
            if len(line) > 0 and line and idx < max_length:
                ch, tag = line.split('\t')
                # 如果当前字符存在于映射表中, 则直接映射为对应的id值
                if char2id.get(ch):
                    char_ids[idx] = char2id[ch]
                # 否则直接用"UNK"的id值来代替这个未知字符
                else:
                    char_ids[idx] = char2id['UNK']
                # 将标签也进行对应的转换
                tag_ids[idx] = tag2id[tag]
                idx += 1
            # 如果是空行, 或者当前语句长度超过max_length
            else:
                # 如果当前语句长度超过max_length, 直接将[0: max_langth]的部分作为结果
                if idx <= max_length:
                    char_data.append(char_ids)
                    tag_data.append(tag_ids)
                # 遇到空行, 说明当前句子已经结束, 初始化清零, 为下一个句子的映射做准备
                char_ids = [0] * max_length
                tag_ids = [0] * max_length
                idx = 0

    # 将数字化编码后的数据封装成numpy的数组类型, 数字编码采用np.int32
    x_data = np.array(char_data, dtype=np.int32)
    y_data = np.array(tag_data, dtype=np.int32)

    # 直接利用np.savez()将数据存储为.npz类型的文件
    np.savez(result_file, x_data=x_data, y_data=y_data)
    print("create_train_data Finished!".center(100, "-"))
  • 输入参数:
# 参数1:字符码表文件路
json_file = './data/char_to_id.json'

# 参数2:标签码表对照字典
tag2id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}

# 参数3:训练数据文件路径
train_data_file = './data/train.txt'

# 参数4:创建的npz文件保路径(训练数据)
result_file = './data/train.npz'
  • 调用:
if __name__ == '__main__':
    create_train_data(train_data_file, result_file, json_file, tag2id)
  • 第三步: 生成批量训练数据.
# 导入相关的包
import numpy as np
import torch
import torch.utils.data as Data

# 生成批量训练数据
def load_dataset(data_file, batch_size):
    # 将第二步生成的train.npz文件导入内存
    data = np.load(data_file)

    # 分别取出特征值和标签
    x_data = data['x_data']
    y_data = data['y_data']

    # 将数据封装成tensor张量
    x = torch.tensor(x_data, dtype=torch.long)
    y = torch.tensor(y_data, dtype=torch.long)

    # 将数据封装成Tensor数据集
    dataset = Data.TensorDataset(x, y)

    total_length = len(dataset)

    # 采用80%的数据作为训练集, 20%的数据作为测试集
    train_length = int(total_length * 0.8)
    validation_length = total_length - train_length

    # 利用Data.random_split()直接切分集合, 按照80%, 20%的比例划分
    train_dataset, validation_dataset = Data.random_split(dataset=dataset,
                                        lengths=[train_length, validation_length])

    # 将训练集进行DataLoader封装
    # 参数说明如下:
    # dataset:     训练数据集
    # batch_size:  代表批次大小, 若数据集总样本数量无法被batch_size整除, 则最后一批数据为余数
    #              若设置drop_last为True, 则自动抹去最后不能被整除的剩余批次
    # shuffle:     是否每个批次为随机抽取, 若为True, 则每次迭代时数据为随机抽取
    # num_workers: 设定有多少子进程用来做数据加载, 默认为0, 即数据将被加载到主进程中
    # drop_last:   是否去除不能被整除后的最后批次, 若为True, 则不生成最后不能被整除剩余的数据内容
    #              例如: dataset长度为1028, batch_size为8,
    #              若drop_last=True, 则最后剩余的4(1028/8=128余4)条数据将被抛弃不用
    train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,
                                   shuffle=True, num_workers=4, drop_last=True)

    validation_loader = Data.DataLoader(dataset=validation_dataset, batch_size=batch_size,
                                        shuffle=True, num_workers=4, drop_last=True)

    # 将两个数据生成器封装为一个字典类型
    data_loaders = {'train': train_loader, 'validation': validation_loader}

    # 将两个数据集的长度也封装为一个字典类型
    data_size = {'train': train_length, 'validation': validation_length}

    return data_loaders, data_size
  • 输入参数:
# 批次大小
BATCH_SIZE = 8

# 编码后的训练数据文件路径
DATA_FILE = './data/train.npz'
  • 调用:
if __name__ == '__main__':
    data_loader, data_size = load_dataset(DATA_FILE, BATCH_SIZE)
    print('data_loader:', data_loader, '\ndata_size:', data_size)
  • 输出效果:
data_loader: {'train': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb3d0>, 'validation': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb5d0>} 
data_size: {'train': 5368, 'validation': 1343}
  • 第四步: 完成准确率和召回率的评估代码.
# 评估模型的准确率, 召回率, F1, 等指标
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):
    '''
    sentence_list: 文本向量化后的句子向量列表
    true_tag:      真实的标签
    predict_tag:   模型预测的标签
    id2char:       id值到中文字符的映射表
    id2tag:        id值到标签的映射表
    '''
    # 初始化真实的命名实体, 预测的命名实体, 接下来比较两者来评估各项指标
    true_entities, true_entity = [], []
    predict_entities, predict_entity = [], []

    # 逐条遍历批次中所有的语句
    for line_num, sentence in enumerate(sentence_list):
        # 遍历一条样本语句中的每一个字符编码(这里面是数字化编码)
        for char_num in range(len(sentence)):
            # 编码为0, 表示后面都是填充的0, 可以结束for循环
            if sentence[char_num]==0:
                break

            # 依次取出真实的样本字符, 真实的标签, 预测的标签
            char_text = id2char[sentence[char_num]]
            true_tag_type = id2tag[true_tag[line_num][char_num]]
            predict_tag_type = id2tag[predict_tag[line_num][char_num]]

            # 对真实标签进行命名实体的匹配
            # 如果第一个字符是"B", 表示一个实体的开始, 将"字符/标签"的格式添加进实体列表中
            if true_tag_type[0] == "B":
                true_entity = [char_text + "/" + true_tag_type]
            # 如果第一个字符是"I", 表示处于一个实体的中间
            # 如果真实命名实体列表非空, 并且最后一个添加进去的标签类型和当前的标签类型一样, 则继续添加
            # 意思就是比如true_entity = ["中/B-Person", "国/I-Person"], 此时的"人/I-Person"就可以添加进去, 因为都属于同一个命名实体
            elif true_tag_type[0] == "I" and len(true_entity) != 0 and true_entity[-1].split("/")[1][1:] == true_tag_type[1:]:
                true_entity.append(char_text + "/" + true_tag_type)
            # 如果第一个字符是"O", 并且true_entity非空, 表示一个命名实体的匹配结束了
            elif true_tag_type[0] == "O" and len(true_entity) != 0 :
                # 最后增加进去一个"行号_列号", 作为区分实体的标志
                true_entity.append(str(line_num) + "_" + str(char_num))
                # 将这个匹配出来的实体加入到结果列表中
                true_entities.append(true_entity)
                # 清空true_entity, 为下一个命名实体的匹配做准备
                true_entity=[]
            # 除了上面三种情况, 说明当前没有匹配出任何命名实体, 则清空true_entity, 继续下一次匹配
            else:
                true_entity=[]

            # 对预测标签进行命名实体的匹配
            # 如果第一个字符是"B", 表示一个实体的开始, 将"字符/预测标签"的格式添加进实体列表中
            if predict_tag_type[0] == "B":
                predict_entity = [char_text + "/" + predict_tag_type]
            # 如果第一个字符是"I", 表示处于一个实体的中间
            # 如果预测命名实体列表非空, 并且最后一个添加进去的标签类型和当前的标签类型一样, 则继续添加
            # 意思就是比如predict_entity = ["中/B-Person", "国/I-Person"], 此时的"人/I-Person"就可以添>加进去, 因为都属于同一个命名实体
            elif predict_tag_type[0] == "I" and len(predict_entity) != 0 and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:
                predict_entity.append(char_text + "/" + predict_tag_type)
            # 如果第一个字符是"O", 并且predict_entity非空, 表示一个命名实体的匹配结束了
            elif predict_tag_type[0] == "O" and len(predict_entity) != 0:
                # 最后增加进去一个"行号_列号", 作为区分实体的标志
                predict_entity.append(str(line_num) + "_" + str(char_num))
                # 将这个匹配出来的实体加入到结果列表中
                predict_entities.append(predict_entity)
                # 清空predict_entity, 为下一个命名实体的匹配做准备
                predict_entity = []
            # 除了上面三种情况, 说明当前没有匹配出任何命名实体, 则清空predict_entity, 继续下一次匹配
            else:
                predict_entity = []

    # 遍历所有预测实体的列表, 只有那些在真实命名实体中的才是正确的
    acc_entities = [entity for entity in predict_entities if entity in true_entities]

    # 计算正确实体的个数, 预测实体的总个数, 真实实体的总个数
    acc_entities_length = len(acc_entities)
    predict_entities_length = len(predict_entities)
    true_entities_length = len(true_entities)

    # 至少正确预测了一个, 才计算3个指标, 准确率
    if acc_entities_length > 0:
        accuracy = float(acc_entities_length / predict_entities_length)
        recall = float(acc_entities_length / true_entities_length)
        f1_score = 2 * accuracy * recall / (accuracy + recall)
        return accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length
    else:
        return 0, 0, 0, acc_entities_length, predict_entities_length, true_entities_length 
  • 输入参数:
# 真实标签数据
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

# 预测标签数据
predict_tag_list = [
    [0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

# 编码与字符对照字典
id2char = {0: '<PAD>', 1: '确', 2: '诊', 3: '弥', 4: '漫', 5: '大', 6: 'b', 7: '细', 8: '胞', 9: '淋', 10: '巴', 11: '瘤', 12: '1', 13: '年', 14: '反', 15: '复', 16: '咳', 17: '嗽', 18: '、', 19: '痰', 20: '4', 21: '0', 22: ',', 23: '再', 24: '发', 25: '伴', 26: '气', 27: '促', 28: '5', 29: '天', 30: '。', 31: '生', 32: '长', 33: '育', 34: '迟', 35: '缓', 36: '9', 37: '右', 38: '侧', 39: '小', 40: '肺', 41: '癌', 42: '第', 43: '三', 44: '次', 45: '化', 46: '疗', 47: '入', 48: '院', 49: '心', 50: '悸', 51: '加', 52: '重', 53: '胸', 54: '痛', 55: '3', 56: '闷', 57: '2', 58: '多', 59: '月', 60: '余', 61: ' ', 62: '周', 63: '上', 64: '肢', 65: '无', 66: '力', 67: '肌', 68: '肉', 69: '萎', 70: '缩', 71: '半'}

# 编码与标签对照字典
id2tag = {0: 'O', 1: 'B-dis', 2: 'I-dis', 3: 'B-sym', 4: 'I-sym'}

# 输入的数字化sentences_sequence, 由下面的sentence_list经过映射函数sentence_map()转化后得到
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]
  • 调用:
def sentence_map(sentence_list, char_to_id, max_length):
    sentence_list.sort(key=lambda c:len(c), reverse=True)
    sentence_map_list = []
    for sentence in sentence_list:
        sentence_id_list = [char_to_id[c] for c in sentence]
        padding_list = [0] * (max_length-len(sentence))
        sentence_id_list.extend(padding_list)
        sentence_map_list.append(sentence_id_list)
    return torch.tensor(sentence_map_list, dtype=torch.long)

char_to_id = {"<PAD>":0}

SENTENCE_LENGTH = 20

for sentence in sentence_list:
    for _char in sentence:
        if _char not in char_to_id:
            char_to_id[_char] = len(char_to_id)

sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)


if __name__ == '__main__':
    accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length = evaluate(sentences_sequence.tolist(), tag_list, predict_tag_list, id2char, id2tag)

    print("accuracy:",                  accuracy,
          "\nrecall:",                  recall,
          "\nf1_score:",                f1_score,
          "\nacc_entities_length:",     acc_entities_length,
          "\npredict_entities_length:", predict_entities_length,
          "\ntrue_entities_length:",    true_entities_length)
  • 输出效果:
step_acc: 0.8823529411764706 
step_recall: 0.9375 
f1_score: 0.9090909090909091 
acc_entities_length: 15 
predict_entities_length: 17 
true_entities_length: 16
  • 第五步: 完成训练模型的代码.
# 导入包
import json
import time
from tqdm import tqdm
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
from torch.autograd import Variable
# 导入之前编写好的包, 包括类, 数据集加载, 评估函数
from bilstm_crf import BiLSTM_CRF
from loader_data import load_dataset
from evaluate_model import evaluate

# 训练模型的函数
def train(data_loader, data_size, batch_size, embedding_dim, hidden_dim,
          sentence_length, num_layers, epochs, learning_rate, tag2id,
          model_saved_path, train_log_path,
          validate_log_path, train_history_image_path):
    '''
    data_loader: 数据集的加载器, 之前已经通过load_dataset完成了构造
    data_size:   训练集和测试集的样本数量
    batch_size:  批次的样本个数
    embedding_dim:  词嵌入的维度
    hidden_dim:     隐藏层的维度
    sentence_length:  文本限制的长度
    num_layers:       神经网络堆叠的LSTM层数
    epochs:           训练迭代的轮次
    learning_rate:    学习率
    tag2id:           标签到id的映射字典
    model_saved_path: 模型保存的路径
    train_log_path:   训练日志保存的路径
    validate_log_path:  测试集日志保存的路径
    train_history_image_path:  训练数据的相关图片保存路径
    '''
    # 将中文字符和id的对应码表加载进内存
    char2id = json.load(open("./data/char_to_id.json", mode="r", encoding="utf-8"))
    # 初始化BiLSTM_CRF模型
    model = BiLSTM_CRF(vocab_size=len(char2id), tag_to_ix=tag2id,
                   embedding_dim=embedding_dim, hidden_dim=hidden_dim,
                   batch_size=batch_size, num_layers=num_layers,
                   sequence_length=sentence_length)

    # 定义优化器, 使用SGD作为优化器(pytorch中Embedding支持的GPU加速为SGD, SparseAdam)
    # 参数说明如下:
    # lr:          优化器学习率
    # momentum:    优化下降的动量因子, 加速梯度下降过程
    optimizer = optim.SGD(params=model.parameters(), lr=learning_rate, momentum=0.85)

    # 设定优化器学习率更新策略
    # 参数说明如下:
    # optimizer:    优化器
    # step_size:    更新频率, 每过多少个epoch更新一次优化器学习率
    # gamma:        学习率衰减幅度,
    #               按照什么比例调整(衰减)学习率(相对于上一轮epoch), 默认0.1
    #   例如:
    #   初始学习率 lr = 0.5,    step_size = 20,    gamma = 0.1
    #              lr = 0.5     if epoch < 20
    #              lr = 0.05    if 20 <= epoch < 40
    #              lr = 0.005   if 40 <= epoch < 60
    scheduler = optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=5, gamma=0.2)

    # 初始化存放训练中损失, 准确率, 召回率, F1等数值指标
    train_loss_list = []
    train_acc_list = []
    train_recall_list = []
    train_f1_list = []
    train_log_file = open(train_log_path, mode="w", encoding="utf-8")
    # 初始化存放测试中损失, 准确率, 召回率, F1等数值指标
    validate_loss_list = []
    validate_acc_list = []
    validate_recall_list = []
    validate_f1_list = []
    validate_log_file = open(validate_log_path, mode="w", encoding="utf-8")
    # 利用tag2id生成id到tag的映射字典
    id2tag = {v:k for k, v in tag2id.items()}
    # 利用char2id生成id到字符的映射字典
    id2char = {v:k for k, v in char2id.items()}

    # 按照参数epochs的设定来循环epochs次
    for epoch in range(epochs):
        # 在进度条打印前, 先输出当前所执行批次
        tqdm.write("Epoch {}/{}".format(epoch + 1, epochs))
        # 定义要记录的正确总实体数, 识别实体数以及真实实体数
        total_acc_entities_length, \
        total_predict_entities_length, \
        total_gold_entities_length = 0, 0, 0
        # 定义每batch步数, 批次loss总值, 准确度, f1值
        step, total_loss, correct, f1 = 1, 0.0, 0, 0

        # 开启当前epochs的训练部分
        for inputs, labels in tqdm(data_loader["train"]):
            # 将数据以Variable进行封装
            inputs, labels = Variable(inputs), Variable(labels)
            # 在训练模型期间, 要在每个样本计算梯度前将优化器归零, 不然梯度会被累加
            optimizer.zero_grad()
            # 此处调用的是BiLSTM_CRF类中的neg_log_likelihood()函数
            loss = model.neg_log_likelihood(inputs, labels)
            # 获取当前步的loss, 由tensor转为数字
            step_loss = loss.data
            # 累计每步损失值
            total_loss += step_loss
            # 获取解码最佳路径列表, 此时调用的是BiLSTM_CRF类中的forward()函数
            best_path_list = model(inputs)
            # 模型评估指标值获取包括:当前批次准确率, 召回率, F1值以及对应的实体个数
            step_acc, step_recall, f1_score, acc_entities_length, \
            predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
                                                                     labels.tolist(),
                                                                     best_path_list,
                                                                     id2char,
                                                                     id2tag)
            # 训练日志内容
            log_text = "Epoch: %s | Step: %s " \
                       "| loss: %.5f " \
                       "| acc: %.5f " \
                       "| recall: %.5f " \
                       "| f1 score: %.5f" % \
                       (epoch, step, step_loss, step_acc, step_recall,f1_score)
            # 分别累计正确总实体数、识别实体数以及真实实体数
            total_acc_entities_length += acc_entities_length
            total_predict_entities_length += predict_entities_length
            total_gold_entities_length += gold_entities_length

            # 对损失函数进行反向传播
            loss.backward()
            # 通过optimizer.step()计算损失, 梯度和更新参数
            optimizer.step()
            # 记录训练日志
            train_log_file.write(log_text + "\n")
            step += 1
        # 获取当前epochs平均损失值(每一轮迭代的损失总值除以总数据量)
        epoch_loss = total_loss / data_size["train"]
        # 计算当前epochs准确率
        total_acc = total_acc_entities_length / total_predict_entities_length
        # 计算当前epochs召回率
        total_recall = total_acc_entities_length / total_gold_entities_length
        # 计算当前epochs的F1值
        total_f1 = 0
        if total_acc + total_recall != 0:
            total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
        log_text = "Epoch: %s " \
                   "| mean loss: %.5f " \
                   "| total acc: %.5f " \
                   "| total recall: %.5f " \
                   "| total f1 scroe: %.5f" % (epoch, epoch_loss,
                                               total_acc,
                                               total_recall,
                                               total_f1)

        # 当前epochs训练后更新学习率, 必须在优化器更新之后
        scheduler.step()

        # 记录当前epochs训练loss值(用于图表展示), 准确率, 召回率, f1值
        train_loss_list.append(epoch_loss)
        train_acc_list.append(total_acc)
        train_recall_list.append(total_recall)
        train_f1_list.append(total_f1)
        train_log_file.write(log_text + "\n")


        # 定义要记录的正确总实体数, 识别实体数以及真实实体数
        total_acc_entities_length, \
        total_predict_entities_length, \
        total_gold_entities_length = 0, 0, 0
        # 定义每batch步数, 批次loss总值, 准确度, f1值
        step, total_loss, correct, f1 = 1, 0.0, 0, 0

        # 开启当前epochs的验证部分
        with torch.no_grad():
			# 开启当前epochs的验证部分
			for inputs, labels in tqdm(data_loader["validation"]):
				# 将数据以Variable进行封装
				inputs, labels = Variable(inputs), Variable(labels)
				# 此处调用的是BiLSTM_CRF类中的neg_log_likelihood 函数
				# 返回最终的CRF的对数似然结果
				loss = model.neg_log_likelihood(inputs, labels)
				# 获取当前步的loss值, 由tensor转为数字
				step_loss = loss.data
				# 累计每步损失值
				total_loss += step_loss
				# 获取解码最佳路径列表, 此时调用的是BiLSTM_CRF类中的forward()函数
				best_path_list = model(inputs)
				# 模型评估指标值获取: 当前批次准确率, 召回率, F1值以及对应的实体个数
				step_acc, step_recall, f1_score, acc_entities_length, \
				predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
																		 labels.tolist(),
																		 best_path_list,
																		 id_to_char,
																		 id_to_tag)

				# 训练日志内容
				log_text = "Epoch: %s | Step: %s " \
						   "| loss: %.5f " \
						   "| acc: %.5f " \
						   "| recall: %.5f " \
						   "| f1 score: %.5f" % \
						   (epoch, step, step_loss, step_acc, step_recall,f1_score)
				# 分别累计正确总实体数、识别实体数以及真实实体数
				total_acc_entities_length += acc_entities_length
				total_predict_entities_length += predict_entities_length
				total_gold_entities_length += gold_entities_length

				# 记录验证集损失日志
				validate_log_file.write(log_text + "\n")
				step += 1

			# 获取当前批次平均损失值(每一批次损失总值除以数据量)
			epoch_loss = total_loss / data_size["validation"]
			# 计算总批次准确率
			total_acc = total_acc_entities_length / total_predict_entities_length
			# 计算总批次召回率
			total_recall = total_acc_entities_length / total_gold_entities_length
			# 计算总批次F1值
			total_f1 = 0
			if total_acc + total_recall != 0:
				total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
			log_text = "Epoch: %s " \
					   "| mean loss: %.5f " \
					   "| total acc: %.5f " \
					   "| total recall: %.5f " \
					   "| total f1 scroe: %.5f" % (epoch, epoch_loss,
												   total_acc,
												   total_recall,
												   total_f1)

			# 记录当前批次验证loss值(用于图表展示)准确率, 召回率, f1值
			validate_loss_list.append(epoch_loss)
			validate_acc_list.append(total_acc)
			validate_recall_list.append(total_recall)
			validate_f1_list.append(total_f1)
			validate_log_file.write(log_text + "\n")


    # 保存模型
    torch.save(model.state_dict(), model_saved_path)

    # 将loss下降历史数据转为图片存储
    save_train_history_image(train_loss_list,
                             validate_loss_list,
                             train_history_image_path,
                             "Loss")
    # 将准确率提升历史数据转为图片存储
    save_train_history_image(train_acc_list,
                             validate_acc_list,
                             train_history_image_path,
                             "Acc")
    # 将召回率提升历史数据转为图片存储
    save_train_history_image(train_recall_list,
                             validate_recall_list,
                             train_history_image_path,
                             "Recall")
    # 将F1上升历史数据转为图片存储
    save_train_history_image(train_f1_list,
                             validate_f1_list,
                             train_history_image_path,
                             "F1")
    print("train Finished".center(100, "-"))


# 按照传入的不同路径, 绘制不同的训练曲线
def save_train_history_image(train_history_list,
                             validate_history_list,
                             history_image_path,
                             data_type):
    # 根据训练集的数据列表, 绘制折线图
    plt.plot(train_history_list, label="Train %s History" % (data_type))
    # 根据测试集的数据列表, 绘制折线图
    plt.plot(validate_history_list, label="Validate %s History" % (data_type))
    # 将图片放置在最优位置
    plt.legend(loc="best")
    # 设置x轴的图标为轮次Epochs
    plt.xlabel("Epochs")
    # 设置y轴的图标为参数data_type
    plt.ylabel(data_type)
    # 将绘制好的图片保存在特定的路径下面, 并修改图片名字中的"plot"为对应的data_type
    plt.savefig(history_image_path.replace("plot", data_type))
    plt.close()
  • 输入参数:
# 参数1:批次大小
BATCH_SIZE = 8
# 参数2:训练数据文件路径
train_data_file_path = "data/train.npz"
# 参数3:加载 DataLoader 数据
data_loader, data_size = load_dataset(train_data_file_path, BATCH_SIZE)
# 参数4:记录当前训练时间(拼成字符串用)
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
# 参数5:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数6:训练文件存放路径
model_saved_path = "model/bilstm_crf_state_dict_%s.pt" % (time_str)
# 参数7:训练日志文件存放路径
train_log_path = "log/train_%s.log" % (time_str)
# 参数8:验证打印日志存放路径
validate_log_path = "log/validate_%s.log" % (time_str)
# 参数9:训练历史记录图存放路径
train_history_image_path = "log/bilstm_crf_train_plot_%s.png" % (time_str)
# 参数10:字向量维度
EMBEDDING_DIM = 200
# 参数11:隐层维度
HIDDEN_DIM = 100
# 参数12:句子长度
SENTENCE_LENGTH = 20
# 参数13:堆叠 LSTM 层数
NUM_LAYERS = 1
# 参数14:训练批次
EPOCHS = 100
# 参数15:初始化学习率
LEARNING_RATE = 0.5
  • 调用:
if __name__ == '__main__':
    train(data_loader, data_size, BATCH_SIZE, EMBEDDING_DIM, HIDDEN_DIM, SENTENCE_LENGTH,
          NUM_LAYERS, EPOCHS, LEARNING_RATE, tag_to_id,
          model_saved_path, train_log_path, validate_log_path, train_history_image_path)
  • 输出效果:
    • 模型训练结果文件保存位置:model/bilstm_crf_state_dict_[年月日时分秒时间字符串].pt
    • 模型训练日志文件保存位置:log/train_[年月日时分秒时间字符串].log
    • 模型验证日志文件保存位置:log/validate_[年月日时分秒时间字符串].log
    • 模型训练损失历史记录图片保存位置:log/bilstm_crf_train_Loss_[年月日时分秒时间字符串].png
    • 模型训练准确率历史记录图片保存位置:log/bilstm_crf_train_Acc_[年月日时分秒时间字符串].png
    • 模型训练召回率历史记录图片保存位置:log/bilstm_crf_train_Recall_[年月日时分秒时间字符串].png
    • 模型训练F1值历史记录图片保存位置:log/bilstm_crf_train_F1_[年月日时分秒时间字符串].png
  • 训练日志:
Epoch: 0 | train loss: 366.58832 |acc: 0.632 |recall: 0.503 |f1 score: 0.56 | validate loss: 666.032 |acc: 0.591 |recall: 0.457 |f1 score: 0.515
Epoch: 1 | train loss: 123.87159 |acc: 0.743 |recall: 0.687 |f1 score: 0.714 | validate loss: 185.021 |acc: 0.669 |recall: 0.606 |f1 score: 0.636
Epoch: 2 | train loss: 113.04003 |acc: 0.738 |recall: 0.706 |f1 score: 0.722 | validate loss: 107.393 |acc: 0.711 |recall: 0.663 |f1 score: 0.686
Epoch: 3 | train loss: 119.14317 |acc: 0.751 |recall: 0.692 |f1 score: 0.721 | validate loss: 158.381 |acc: 0.713 |recall: 0.64 |f1 score: 0.674
Epoch: 4 | train loss: 105.81506 |acc: 0.741 |recall: 0.699 |f1 score: 0.72 | validate loss: 118.99 |acc: 0.669 |recall: 0.624 |f1 score: 0.646
Epoch: 5 | train loss: 86.67545 |acc: 0.773 |recall: 0.751 |f1 score: 0.762 | validate loss: 123.636 |acc: 0.64 |recall: 0.718 |f1 score: 0.676
Epoch: 6 | train loss: 79.66924 |acc: 0.808 |recall: 0.772 |f1 score: 0.789 | validate loss: 89.771 |acc: 0.735 |recall: 0.714 |f1 score: 0.724
Epoch: 7 | train loss: 85.35771 |acc: 0.766 |recall: 0.752 |f1 score: 0.759 | validate loss: 141.233 |acc: 0.675 |recall: 0.7 |f1 score: 0.687
Epoch: 8 | train loss: 82.38535 |acc: 0.787 |recall: 0.748 |f1 score: 0.767 | validate loss: 108.429 |acc: 0.717 |recall: 0.673 |f1 score: 0.694
Epoch: 9 | train loss: 82.46296 |acc: 0.783 |recall: 0.751 |f1 score: 0.767 | validate loss: 74.716 |acc: 0.692 |recall: 0.702 |f1 score: 0.697
Epoch: 10 | train loss: 75.12292 |acc: 0.814 |recall: 0.779 |f1 score: 0.796 | validate loss: 90.693 |acc: 0.672 |recall: 0.7 |f1 score: 0.686
Epoch: 11 | train loss: 74.89426 |acc: 0.813 |recall: 0.77 |f1 score: 0.791 | validate loss: 77.161 |acc: 0.729 |recall: 0.718 |f1 score: 0.724
Epoch: 12 | train loss: 76.39055 |acc: 0.814 |recall: 0.785 |f1 score: 0.799 | validate loss: 132.545 |acc: 0.806 |recall: 0.685 |f1 score: 0.74
Epoch: 13 | train loss: 75.01093 |acc: 0.814 |recall: 0.787 |f1 score: 0.8 | validate loss: 101.596 |acc: 0.765 |recall: 0.681 |f1 score: 0.721
Epoch: 14 | train loss: 74.35796 |acc: 0.83 |recall: 0.802 |f1 score: 0.816 | validate loss: 92.535 |acc: 0.745 |recall: 0.777 |f1 score: 0.761
Epoch: 15 | train loss: 73.27102 |acc: 0.818 |recall: 0.791 |f1 score: 0.804 | validate loss: 109.51 |acc: 0.68 |recall: 0.76 |f1 score: 0.717
Epoch: 16 | train loss: 67.66725 |acc: 0.841 |recall: 0.811 |f1 score: 0.826 | validate loss: 93.047 |acc: 0.768 |recall: 0.738 |f1 score: 0.753
Epoch: 17 | train loss: 63.75809 |acc: 0.83 |recall: 0.813 |f1 score: 0.822 | validate loss: 76.231 |acc: 0.784 |recall: 0.776 |f1 score: 0.78
Epoch: 18 | train loss: 60.30417 |acc: 0.845 |recall: 0.829 |f1 score: 0.837 | validate loss: 76.019 |acc: 0.806 |recall: 0.758 |f1 score: 0.781
Epoch: 19 | train loss: 60.30238 |acc: 0.849 |recall: 0.823 |f1 score: 0.836 | validate loss: 90.269 |acc: 0.748 |recall: 0.733 |f1 score: 0.741
Epoch: 20 | train loss: 60.20072 |acc: 0.847 |recall: 0.82 |f1 score: 0.833 | validate loss: 61.756 |acc: 0.81 |recall: 0.77 |f1 score: 0.79
Epoch: 21 | train loss: 58.98606 |acc: 0.844 |recall: 0.82 |f1 score: 0.832 | validate loss: 60.799 |acc: 0.765 |recall: 0.754 |f1 score: 0.759
Epoch: 22 | train loss: 60.23671 |acc: 0.848 |recall: 0.828 |f1 score: 0.838 | validate loss: 65.676 |acc: 0.787 |recall: 0.781 |f1 score: 0.784
Epoch: 23 | train loss: 58.57862 |acc: 0.849 |recall: 0.827 |f1 score: 0.838 | validate loss: 65.975 |acc: 0.794 |recall: 0.754 |f1 score: 0.774
Epoch: 24 | train loss: 58.93968 |acc: 0.848 |recall: 0.827 |f1 score: 0.838 | validate loss: 66.994 |acc: 0.784 |recall: 0.746 |f1 score: 0.764
Epoch: 25 | train loss: 59.91834 |acc: 0.862 |recall: 0.828 |f1 score: 0.845 | validate loss: 68.794 |acc: 0.795 |recall: 0.756 |f1 score: 0.775
Epoch: 26 | train loss: 59.09166 |acc: 0.84 |recall: 0.823 |f1 score: 0.831 | validate loss: 68.508 |acc: 0.746 |recall: 0.758 |f1 score: 0.752
Epoch: 27 | train loss: 58.0584 |acc: 0.856 |recall: 0.84 |f1 score: 0.848 | validate loss: 53.158 |acc: 0.802 |recall: 0.774 |f1 score: 0.788
Epoch: 28 | train loss: 54.2857 |acc: 0.858 |recall: 0.834 |f1 score: 0.845 | validate loss: 60.243 |acc: 0.816 |recall: 0.772 |f1 score: 0.793
Epoch: 29 | train loss: 56.44759 |acc: 0.845 |recall: 0.838 |f1 score: 0.841 | validate loss: 56.497 |acc: 0.768 |recall: 0.77 |f1 score: 0.769
Epoch: 30 | train loss: 57.90492 |acc: 0.868 |recall: 0.832 |f1 score: 0.85 | validate loss: 75.158 |acc: 0.773 |recall: 0.762 |f1 score: 0.768
Epoch: 31 | train loss: 56.81468 |acc: 0.861 |recall: 0.835 |f1 score: 0.847 | validate loss: 56.742 |acc: 0.796 |recall: 0.784 |f1 score: 0.79
Epoch: 32 | train loss: 54.72623 |acc: 0.86 |recall: 0.844 |f1 score: 0.852 | validate loss: 63.175 |acc: 0.757 |recall: 0.78 |f1 score: 0.768
Epoch: 33 | train loss: 60.10299 |acc: 0.846 |recall: 0.813 |f1 score: 0.829 | validate loss: 68.994 |acc: 0.768 |recall: 0.724 |f1 score: 0.745
Epoch: 34 | train loss: 59.67491 |acc: 0.849 |recall: 0.826 |f1 score: 0.837 | validate loss: 58.662 |acc: 0.8 |recall: 0.739 |f1 score: 0.769
Epoch: 35 | train loss: 65.01099 |acc: 0.857 |recall: 0.83 |f1 score: 0.844 | validate loss: 69.299 |acc: 0.772 |recall: 0.752 |f1 score: 0.762
Epoch: 36 | train loss: 61.52783 |acc: 0.856 |recall: 0.828 |f1 score: 0.842 | validate loss: 82.373 |acc: 0.761 |recall: 0.777 |f1 score: 0.769
Epoch: 37 | train loss: 66.19576 |acc: 0.844 |recall: 0.822 |f1 score: 0.833 | validate loss: 79.853 |acc: 0.791 |recall: 0.77 |f1 score: 0.781
Epoch: 38 | train loss: 60.32529 |acc: 0.841 |recall: 0.828 |f1 score: 0.835 | validate loss: 69.346 |acc: 0.773 |recall: 0.755 |f1 score: 0.764
Epoch: 39 | train loss: 63.8836 |acc: 0.837 |recall: 0.819 |f1 score: 0.828 | validate loss: 74.759 |acc: 0.732 |recall: 0.759 |f1 score: 0.745
Epoch: 40 | train loss: 67.28363 |acc: 0.838 |recall: 0.824 |f1 score: 0.831 | validate loss: 63.027 |acc: 0.768 |recall: 0.764 |f1 score: 0.766
Epoch: 41 | train loss: 61.40488 |acc: 0.852 |recall: 0.826 |f1 score: 0.839 | validate loss: 58.976 |acc: 0.802 |recall: 0.755 |f1 score: 0.778
Epoch: 42 | train loss: 61.04982 |acc: 0.856 |recall: 0.817 |f1 score: 0.836 | validate loss: 58.47 |acc: 0.783 |recall: 0.74 |f1 score: 0.761
Epoch: 43 | train loss: 64.40567 |acc: 0.849 |recall: 0.821 |f1 score: 0.835 | validate loss: 63.506 |acc: 0.764 |recall: 0.765 |f1 score: 0.765
Epoch: 44 | train loss: 65.09746 |acc: 0.845 |recall: 0.805 |f1 score: 0.825 | validate loss: 65.535 |acc: 0.773 |recall: 0.743 |f1 score: 0.758
Epoch: 45 | train loss: 63.26585 |acc: 0.848 |recall: 0.808 |f1 score: 0.827 | validate loss: 62.477 |acc: 0.789 |recall: 0.733 |f1 score: 0.76
Epoch: 46 | train loss: 63.91504 |acc: 0.847 |recall: 0.812 |f1 score: 0.829 | validate loss: 59.916 |acc: 0.779 |recall: 0.751 |f1 score: 0.765
Epoch: 47 | train loss: 62.3592 |acc: 0.845 |recall: 0.824 |f1 score: 0.835 | validate loss: 63.363 |acc: 0.775 |recall: 0.761 |f1 score: 0.768
Epoch: 48 | train loss: 63.13221 |acc: 0.843 |recall: 0.823 |f1 score: 0.833 | validate loss: 65.71 |acc: 0.767 |recall: 0.755 |f1 score: 0.761
Epoch: 49 | train loss: 64.9964 |acc: 0.845 |recall: 0.811 |f1 score: 0.828 | validate loss: 65.174 |acc: 0.768 |recall: 0.74 |f1 score: 0.754
Epoch: 50 | train loss: 62.40605 |acc: 0.847 |recall: 0.817 |f1 score: 0.832 | validate loss: 60.761 |acc: 0.776 |recall: 0.746 |f1 score: 0.761
Epoch: 51 | train loss: 63.05476 |acc: 0.845 |recall: 0.812 |f1 score: 0.828 | validate loss: 64.217 |acc: 0.764 |recall: 0.748 |f1 score: 0.756
Epoch: 52 | train loss: 59.77727 |acc: 0.84 |recall: 0.831 |f1 score: 0.836 | validate loss: 60.48 |acc: 0.79 |recall: 0.759 |f1 score: 0.774
Epoch: 53 | train loss: 62.7249 |acc: 0.828 |recall: 0.813 |f1 score: 0.821 | validate loss: 64.584 |acc: 0.757 |recall: 0.757 |f1 score: 0.757
Epoch: 54 | train loss: 61.1763 |acc: 0.842 |recall: 0.832 |f1 score: 0.837 | validate loss: 61.088 |acc: 0.775 |recall: 0.768 |f1 score: 0.771
Epoch: 55 | train loss: 64.04366 |acc: 0.835 |recall: 0.816 |f1 score: 0.826 | validate loss: 68.183 |acc: 0.784 |recall: 0.742 |f1 score: 0.762
Epoch: 56 | train loss: 66.76939 |acc: 0.84 |recall: 0.813 |f1 score: 0.827 | validate loss: 67.284 |acc: 0.77 |recall: 0.748 |f1 score: 0.759
Epoch: 57 | train loss: 67.85329 |acc: 0.826 |recall: 0.789 |f1 score: 0.807 | validate loss: 69.961 |acc: 0.766 |recall: 0.732 |f1 score: 0.749
Epoch: 58 | train loss: 64.79573 |acc: 0.84 |recall: 0.812 |f1 score: 0.826 | validate loss: 73.358 |acc: 0.754 |recall: 0.735 |f1 score: 0.745
Epoch: 59 | train loss: 65.36249 |acc: 0.862 |recall: 0.826 |f1 score: 0.844 | validate loss: 66.552 |acc: 0.783 |recall: 0.766 |f1 score: 0.774
Epoch: 60 | train loss: 63.43061 |acc: 0.835 |recall: 0.811 |f1 score: 0.823 | validate loss: 63.138 |acc: 0.771 |recall: 0.746 |f1 score: 0.759
Epoch: 61 | train loss: 62.34639 |acc: 0.848 |recall: 0.825 |f1 score: 0.836 | validate loss: 59.656 |acc: 0.783 |recall: 0.756 |f1 score: 0.769
Epoch: 62 | train loss: 61.83451 |acc: 0.83 |recall: 0.814 |f1 score: 0.822 | validate loss: 60.443 |acc: 0.765 |recall: 0.751 |f1 score: 0.758
Epoch: 63 | train loss: 64.78461 |acc: 0.854 |recall: 0.818 |f1 score: 0.836 | validate loss: 61.125 |acc: 0.786 |recall: 0.748 |f1 score: 0.767
Epoch: 64 | train loss: 63.43409 |acc: 0.838 |recall: 0.818 |f1 score: 0.828 | validate loss: 62.396 |acc: 0.77 |recall: 0.757 |f1 score: 0.764
Epoch: 65 | train loss: 61.20197 |acc: 0.854 |recall: 0.815 |f1 score: 0.834 | validate loss: 59.019 |acc: 0.79 |recall: 0.75 |f1 score: 0.769
Epoch: 66 | train loss: 59.69791 |acc: 0.851 |recall: 0.82 |f1 score: 0.836 | validate loss: 55.06 |acc: 0.789 |recall: 0.754 |f1 score: 0.771
Epoch: 67 | train loss: 63.16074 |acc: 0.836 |recall: 0.811 |f1 score: 0.823 | validate loss: 61.48 |acc: 0.764 |recall: 0.745 |f1 score: 0.755
Epoch: 68 | train loss: 62.15521 |acc: 0.845 |recall: 0.824 |f1 score: 0.835 | validate loss: 62.407 |acc: 0.778 |recall: 0.761 |f1 score: 0.769
Epoch: 69 | train loss: 61.90574 |acc: 0.847 |recall: 0.828 |f1 score: 0.838 | validate loss: 59.801 |acc: 0.781 |recall: 0.762 |f1 score: 0.771
Epoch: 70 | train loss: 60.51348 |acc: 0.852 |recall: 0.827 |f1 score: 0.839 | validate loss: 56.632 |acc: 0.781 |recall: 0.761 |f1 score: 0.771
Epoch: 71 | train loss: 62.78683 |acc: 0.856 |recall: 0.823 |f1 score: 0.84 | validate loss: 62.867 |acc: 0.796 |recall: 0.757 |f1 score: 0.776
Epoch: 72 | train loss: 62.11708 |acc: 0.845 |recall: 0.82 |f1 score: 0.833 | validate loss: 57.211 |acc: 0.784 |recall: 0.754 |f1 score: 0.769
Epoch: 73 | train loss: 63.2298 |acc: 0.839 |recall: 0.816 |f1 score: 0.828 | validate loss: 60.247 |acc: 0.764 |recall: 0.752 |f1 score: 0.758
Epoch: 74 | train loss: 61.87119 |acc: 0.848 |recall: 0.828 |f1 score: 0.838 | validate loss: 59.692 |acc: 0.782 |recall: 0.765 |f1 score: 0.774
Epoch: 75 | train loss: 59.88628 |acc: 0.851 |recall: 0.821 |f1 score: 0.836 | validate loss: 59.461 |acc: 0.78 |recall: 0.755 |f1 score: 0.767
Epoch: 76 | train loss: 61.97182 |acc: 0.858 |recall: 0.812 |f1 score: 0.835 | validate loss: 59.748 |acc: 0.78 |recall: 0.749 |f1 score: 0.765
Epoch: 77 | train loss: 62.2035 |acc: 0.836 |recall: 0.811 |f1 score: 0.823 | validate loss: 56.778 |acc: 0.768 |recall: 0.748 |f1 score: 0.758
Epoch: 78 | train loss: 59.90309 |acc: 0.846 |recall: 0.823 |f1 score: 0.835 | validate loss: 59.424 |acc: 0.771 |recall: 0.76 |f1 score: 0.765
Epoch: 79 | train loss: 62.48097 |acc: 0.844 |recall: 0.821 |f1 score: 0.833 | validate loss: 57.535 |acc: 0.769 |recall: 0.755 |f1 score: 0.762
Epoch: 80 | train loss: 65.83723 |acc: 0.853 |recall: 0.83 |f1 score: 0.842 | validate loss: 60.798 |acc: 0.782 |recall: 0.762 |f1 score: 0.772
Epoch: 81 | train loss: 67.69897 |acc: 0.848 |recall: 0.812 |f1 score: 0.83 | validate loss: 62.135 |acc: 0.78 |recall: 0.746 |f1 score: 0.763
Epoch: 82 | train loss: 64.45554 |acc: 0.863 |recall: 0.845 |f1 score: 0.854 | validate loss: 62.102 |acc: 0.793 |recall: 0.775 |f1 score: 0.784
Epoch: 83 | train loss: 59.9239 |acc: 0.857 |recall: 0.84 |f1 score: 0.848 | validate loss: 57.003 |acc: 0.788 |recall: 0.771 |f1 score: 0.779
Epoch: 84 | train loss: 65.42567 |acc: 0.859 |recall: 0.831 |f1 score: 0.845 | validate loss: 61.993 |acc: 0.788 |recall: 0.763 |f1 score: 0.775
Epoch: 85 | train loss: 62.69893 |acc: 0.852 |recall: 0.828 |f1 score: 0.84 | validate loss: 59.489 |acc: 0.786 |recall: 0.761 |f1 score: 0.773
Epoch: 86 | train loss: 64.58199 |acc: 0.858 |recall: 0.831 |f1 score: 0.845 | validate loss: 60.414 |acc: 0.789 |recall: 0.764 |f1 score: 0.776
Epoch: 87 | train loss: 58.41865 |acc: 0.875 |recall: 0.838 |f1 score: 0.856 | validate loss: 56.525 |acc: 0.805 |recall: 0.768 |f1 score: 0.786
Epoch: 88 | train loss: 61.39529 |acc: 0.848 |recall: 0.824 |f1 score: 0.836 | validate loss: 56.678 |acc: 0.783 |recall: 0.759 |f1 score: 0.771
Epoch: 89 | train loss: 63.69639 |acc: 0.857 |recall: 0.818 |f1 score: 0.837 | validate loss: 59.014 |acc: 0.787 |recall: 0.751 |f1 score: 0.769
Epoch: 90 | train loss: 61.78225 |acc: 0.841 |recall: 0.84 |f1 score: 0.84 | validate loss: 59.58 |acc: 0.773 |recall: 0.775 |f1 score: 0.774
Epoch: 91 | train loss: 58.19114 |acc: 0.845 |recall: 0.826 |f1 score: 0.836 | validate loss: 55.284 |acc: 0.776 |recall: 0.758 |f1 score: 0.767
Epoch: 92 | train loss: 58.67227 |acc: 0.857 |recall: 0.82 |f1 score: 0.838 | validate loss: 54.982 |acc: 0.787 |recall: 0.753 |f1 score: 0.77
Epoch: 93 | train loss: 60.79532 |acc: 0.858 |recall: 0.83 |f1 score: 0.844 | validate loss: 57.808 |acc: 0.792 |recall: 0.764 |f1 score: 0.778
Epoch: 94 | train loss: 56.71145 |acc: 0.872 |recall: 0.851 |f1 score: 0.861 | validate loss: 53.551 |acc: 0.804 |recall: 0.785 |f1 score: 0.795
Epoch: 95 | train loss: 58.791 |acc: 0.864 |recall: 0.83 |f1 score: 0.847 | validate loss: 54.284 |acc: 0.793 |recall: 0.765 |f1 score: 0.779
Epoch: 96 | train loss: 60.07491 |acc: 0.849 |recall: 0.828 |f1 score: 0.839 | validate loss: 55.524 |acc: 0.78 |recall: 0.764 |f1 score: 0.772
Epoch: 97 | train loss: 61.53479 |acc: 0.86 |recall: 0.825 |f1 score: 0.842 | validate loss: 56.891 |acc: 0.796 |recall: 0.759 |f1 score: 0.777
Epoch: 98 | train loss: 61.94878 |acc: 0.85 |recall: 0.836 |f1 score: 0.843 | validate loss: 57.019 |acc: 0.783 |recall: 0.771 |f1 score: 0.777
Epoch: 99 | train loss: 58.49541 |acc: 0.86 |recall: 0.834 |f1 score: 0.847 | validate loss: 56.162 |acc: 0.795 |recall: 0.767 |f1 score: 0.781
  • 第六步: 绘制损失曲线和评估曲线图

    • 训练和验证损失对照曲线:

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

  • 分析: 损失对照曲线一直下降, 从第5个epoch开始, 迅速降到比较理想的位置, 说明模型能够从数据中获取规律了, 到第40个批次之后, 模型趋于稳定, 说明参数基本能够已经得到最优化效果, 此时, 根据对scheduler的设置, 通过该方法已经对优化器进行了近8次的迭代, 应该在我们原本设置的初始学习率基础上缩小了0.2的8次方倍, 此时应该找到了当前最优解, 因此也就趋于稳定了.
  • 训练和验证准确率对照曲线:

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

  • 分析:
  • 首先,准确率是指识别正确的实体识别出的实体中的比例.
  • 根据对照曲线来看,整体学习结果都在趋于准确率上升方向增加,而且随着批次的增加曲线震动相对平稳,不过可能由于训练与验证样本分布不均衡或者噪声等原因,导致最终验证集的准确度没有达到与训练集相同的情况.
  • 最终的训练集和验证集的召回率分别在:0.85和0.78左右.

  • 训练和验证召回率对照曲线:

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

  • 分析:
  • 在此召回率是指识别正确的实体占当前批次所包含的所有实体总数的比例.
  • 关于训练和验证召回率对照曲线,可以看出召回率的变化相对比较平滑,基本上也在40步左右趋于稳定.
  • 最终的训练集和验证集的召回率分别在:0.83和0.75左右.
  • 训练和验证F1值对照曲线:

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

  • 分析:
  • F1值主要是指训练效果而言,在不多识别实体的情况下同时提高准确度的衡量指标.
  • 其公式为:2×准确率×召回率 / (准确率+召回率)
  • 从曲线可见整体F1值上升与损失、召回率的曲线比较接近,说明在识别出的实体中,正确率比较问题,不过根据前面的准确度来分析,可能在识别过程中,增加了识别出的实体个数而导致不稳定。从这方面来说,可以验证样本不均衡问题以及噪声对模型的影响还是比较大的。
  • 从整体而言,F1值基本也在第40步之后趋于稳定,最终的训练集和验证集的结果在:0.85和0.75左右。
  • 小节总结:
    • 学习了数据预处理的相关方法
      • 原始数据集的字符经过数字化编码变成向量
      • 标注数据集的字符经过数字化编码变成向量
    • 学习生成批量训练数据的方法
    • 学习了模型训练相关代码的实现
      • 准确率和召回率评估的代码
      • 模型构建类的全部内部函数代码
      • 启动训练流程的代码

6.6 模型使用

  • 学习目标:
    • 掌握模型单条文本预测代码实现
    • 掌握批量文件夹文件预测代码实现
  • 模型单条文本预测代码实现:
import os
import torch
import json
from bilstm_crf import BiLSTM_CRF

def singel_predict(model_path, content, char_to_id_json_path, batch_size, embedding_dim,
                   hidden_dim, num_layers, sentence_length, offset, target_type_list, tag2id):

    char_to_id = json.load(open(char_to_id_json_path, mode="r", encoding="utf-8"))
    # 将字符串转为码表id列表
    char_ids = content_to_id(content, char_to_id)
    # 处理成 batch_size * sentence_length 的 tensor 数据
    # 定义模型输入列表
    model_inputs_list, model_input_map_list = build_model_input_list(content,
                                                                     char_ids,
                                                                     batch_size,
                                                                     sentence_length,
                                                                     offset)
    # 加载模型
    model = BiLSTM_CRF(vocab_size=len(char_to_id),
                       tag_to_ix=tag2id,
                       embedding_dim=embedding_dim,
                       hidden_dim=hidden_dim,
                       batch_size=batch_size,
                       num_layers=num_layers,
                       sequence_length=sentence_length)
    # 加载模型字典
    model.load_state_dict(torch.load(model_path))

    tag_id_dict = {v: k for k, v in tag_to_id.items() if k[2:] in target_type_list}
    # 定义返回实体列表
    entities = []
    with torch.no_grad():
        for step, model_inputs in enumerate(model_inputs_list):
            prediction_value = model(model_inputs)
            # 获取每一行预测结果
            for line_no, line_value in enumerate(prediction_value):
                # 定义将要识别的实体
                entity = None
                # 获取当前行每个字的预测结果
                for char_idx, tag_id in enumerate(line_value):
                    # 若预测结果 tag_id 属于目标字典数据 key 中
                    if tag_id in tag_id_dict:
                        # 取符合匹配字典id的第一个字符,即B, I
                        tag_index = tag_id_dict[tag_id][0]
                        # 计算当前字符确切的下标位置
                        current_char = model_input_map_list[step][line_no][char_idx]
                        # 若当前字标签起始为 B, 则设置为实体开始
                        if tag_index == "B":
                            entity = current_char
                        # 若当前字标签起始为 I, 则进行字符串追加
                        elif tag_index == "I" and entity:
                            entity += current_char
                    # 当实体不为空且当前标签类型为 O 时,加入实体列表
                    if tag_id == tag_to_id["O"] and entity:
                        # 满足当前字符为O,上一个字符为目标提取实体结尾时,将其加入实体列表
                        entities.append(entity)
                        # 重置实体
                        entity = None
    return entities


def content_to_id(content, char_to_id):
    # 定义字符串对应的码表 id 列表
    char_ids = []
    for char in list(content):
        # 判断若字符不在码表对应字典中,则取 NUK 的编码(即 unknown),否则取对应的字符编码
        if char_to_id.get(char):
            char_ids.append(char_to_id[char])
        else:
            char_ids.append(char_to_id["UNK"])
    return char_ids


def build_model_input_list(content, char_ids, batch_size, sentence_length, offset):
    # 定义模型输入数据列表
    model_input_list = []
    # 定义每个批次句子 id 数据
    batch_sentence_list = []
    # 将文本内容转为列表
    content_list = list(content)
    # 定义与模型 char_id 对照的文字
    model_input_map_list = []
    #  定义每个批次句子字符数据
    batch_sentence_char_list = []
    # 判断是否需要 padding
    if len(char_ids) % sentence_length > 0:
        # 将不足 batch_size * sentence_length 的部分填充0
        padding_length = (batch_size * sentence_length
                          - len(char_ids) % batch_size * sentence_length
                          - len(char_ids) % sentence_length)
        char_ids.extend([0] * padding_length)
        content_list.extend(["#"] * padding_length)
    # 迭代字符 id 列表
    # 数据满足 batch_size * sentence_length 将加入 model_input_list
    for step, idx in enumerate(range(0, len(char_ids) + 1, sentence_length)):
        # 起始下标,从第一句开始增加 offset 个字的偏移
        start_idx = 0 if idx == 0 else idx - step * offset
        # 获取长度为 sentence_length 的字符 id 数据集
        sub_list = char_ids[start_idx:start_idx + sentence_length]
        # 获取长度为 sentence_length 的字符数据集
        sub_char_list = content_list[start_idx:start_idx + sentence_length]
        # 加入批次数据集中
        batch_sentence_list.append(sub_list)
        # 批量句子包含字符列表
        batch_sentence_char_list.append(sub_char_list)
        # 每当批次长度达到 batch_size 时候,将其加入 model_input_list
        if len(batch_sentence_list) == batch_size:
            # 将数据格式转为 tensor 格式,大小为 batch_size * sentence_length
            model_input_list.append(torch.tensor(batch_sentence_list))
            # 重置 batch_sentence_list
            batch_sentence_list = []
            # 将 char_id 对应的字符加入映射表中
            model_input_map_list.append(batch_sentence_char_list)
            # 重置批字符串内容
            batch_sentence_char_list = []
    # 返回模型输入列表
    return model_input_list, model_input_map_list

  • 输入参数:
# 参数1:待识别文本
content = "本病是由DNA病毒的单纯疱疹病毒所致。人类单纯疱疹病毒分为两型," \
"即单纯疱疹病毒Ⅰ型(HSV-Ⅰ)和单纯疱疹病毒Ⅱ型(HSV-Ⅱ)。" \
"Ⅰ型主要引起生殖器以外的皮肤黏膜(口腔黏膜)和器官(脑)的感染。" \
"Ⅱ型主要引起生殖器部位皮肤黏膜感染。" \
"病毒经呼吸道、口腔、生殖器黏膜以及破损皮肤进入体内," \
"潜居于人体正常黏膜、血液、唾液及感觉神经节细胞内。" \
"当机体抵抗力下降时,如发热胃肠功能紊乱、月经、疲劳等时," \
"体内潜伏的HSV被激活而发病。"
# 参数2:模型保存文件路径
model_path = "model/bilstm_crf_state_dict_20200129_210417.pt"
# 参数3:批次大小
BATCH_SIZE = 8
# 参数4:字向量维度
EMBEDDING_DIM = 300
# 参数5:隐层维度
HIDDEN_DIM = 128
# 参数6:句子长度
SENTENCE_LENGTH = 100
# 参数7:偏移量
OFFSET = 10
# 参数8:标签码表对照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数9:字符码表文件路径
char_to_id_json_path = "./data/char_to_id.json"
# 参数10:预测结果存储路径
prediction_result_path = "prediction_result"
# 参数11:待匹配标签类型
target_type_list = ["sym"]
  • 调用:
# 单独文本预测, 获得实体结果
entities = singel_predict(model_path,
                          content,
                          char_to_id_json_path,
                          BATCH_SIZE,
                          EMBEDDING_DIM,
                          HIDDEN_DIM,
                          SENTENCE_LENGTH,
                          OFFSET,
                          target_type_list,
                          tag_to_id)
# 打印实体结果
print("entities:\n", entities)
  • 输出效果:
entities:
['感染', '发热', '##']
  • 批量文件夹文件预测代码实现:
def batch_predict(data_path, model_path, char_to_id_json_path, batch_size, embedding_dim,
                  hidden_dim, sentence_length, offset, target_type_list,
                  prediction_result_path, tag_to_id):
    """
    description: 批量预测,查询文件目录下数据, 
                 从中提取符合条件的实体并存储至新的目录下prediction_result_path
    :param data_path:               数据文件路径
    :param model_path:              模型文件路径
    :param char_to_id_json_path:    字符码表文件路径
    :param batch_size:              训练批次大小
    :param embedding_dim:           字向量维度
    :param hidden_dim:              BiLSTM 隐藏层向量维度
    :param sentence_length:         句子长度(句子做了padding)
    :param offset:                  设定偏移量, 
                                    当字符串超出sentence_length时, 换行时增加偏移量
    :param target_type_list:        待匹配类型,符合条件的实体将会被提取出来
    :param prediction_result_path:  预测结果保存路径
    :param tag_to_id:               标签码表对照字典, 标签对应 id
    :return:                        无返回
    """
    # 迭代路径, 读取文件名
    for fn in os.listdir(data_path):
        # 拼装全路径
        fullpath = os.path.join(data_path, fn)
        # 定义输出结果文件
        entities_file = open(os.path.join(prediction_result_path, fn),
                             mode="w",
                             encoding="utf-8")
        with open(fullpath, mode="r", encoding="utf-8") as f:
            # 读取文件内容
            content = f.readline()
            # 调用单个预测模型,输出为目标类型实体文本列表
            entities = singel_predict(model_path, content, char_to_id_json_path,
                                      batch_size, embedding_dim, hidden_dim, sentence_length,
                                      offset, target_type_list, tag_to_id)
            # 写入识别结果文件
            entities_file.write("\n".join(entities))
    print("batch_predict Finished".center(100, "-"))
  • 输入参数:
# 参数1:模型保存路径
model_path = "model/bilstm_crf_state_dict_20191219_220256.pt"
# 参数2:批次大小
BATCH_SIZE = 8
# 参数3:字向量维度
EMBEDDING_DIM = 200
# 参数4:隐层维度
HIDDEN_DIM = 100
# 参数5:句子长度
SENTENCE_LENGTH = 20
# 参数6:偏移量
OFFSET = 10
# 参数7:标签码表对照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数8:字符码表文件路径
char_to_id_json_path = "./data/char_to_id.json"
# 参数9:预测结果存储路径
prediction_result_path = "prediction_result"
# 参数10:待匹配标签类型
target_type_list = ["sym"]
# 参数11:待预测文本文件所在目录
data_path = "origin_data"
  • 调用:
# 批量文本预测, 并将结果写入文件中
batch_predict(data_path,
              model_path,
              char_to_id_json_path,
              BATCH_SIZE,
              EMBEDDING_DIM,
              HIDDEN_DIM,
              SENTENCE_LENGTH,
              OFFSET,
              target_type_list,
              prediction_result_path,
              tag_to_id)
  • 输出效果: 将识别结果保存至prediction_result_path指定的目录下, 名称与源文件一致, 内容为每行存储识别实体名称

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

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

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

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


1.transitions转移矩阵 是一个方阵[tagset_size, tagset_size]。
  tag_to_ix[START_TAG]值为5,tag_to_ix[STOP_TAG]值为6,不管是行数还是列数都从0开始统计。
  transitions转移矩阵中行名为当前字符的标签,列名为下一个字符的标签,那么列值便是下一个字符出现该标签的概率值,
  需要计算出列值中下一个字符出现某标签的最大概率值。

2.transitions转移矩阵的 第一种写法
	假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,
	也就是每个单词w_i映射到标签tag的发射概率值。
	那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_i转移到tag_j的概率,tag_i代表当前字符的标签,
	tag_j代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_i转移到下一个字符的标签tag_j的概率值。
          
	1.transitions.data[:, tag_to_ix[START_TAG]]=-10000:
		所有行的第5列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,
                	即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。
	2.transitions.data[tag_to_ix[STOP_TAG], :]=-10000:
                	第5行的所有列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,
                	即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。
	3.transitions[i,j]:
                	其中下标索引为[i,j]的方格代表当前字符的标签为第i行的行名, 那么下一个字符的标签为第j列的列名,
                	那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。

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

3.transitions转移矩阵的 第二种写法(项目中使用该写法)
	假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,
         也就是每个单词w_i映射到标签tag的发射概率值。
         那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率,tag_j代表当前字符的标签,
         tag_i代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_j转移到下一个字符的标签tag_i的概率值。
	
	1.transitions.data[tag_to_ix[START_TAG], :]=-10000:
                	第5行的所有列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,
                	即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。
	2.transitions.data[:, tag_to_ix[STOP_TAG]]=-10000:
                	所有行的第5列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,
                	即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。
	3.transitions[i,j]:
		其中下标索引为[i,j]的方格代表当前字符的标签为第j列的列名, 那么下一个字符的标签为第i行的行名,
                	那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。

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


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

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


计算损失函数第一项的分值

损失函数第一项的分值:本质上是发射概率emit_score和转移概率trans_score的累加和。

前向计算矩阵forward_var、转移概率矩阵trans_score、发射概率矩阵emit_score 计算流程:
	#仅仅把START_TAG列赋值为0, 代表着接下来的转移只能从START_TAG开始。
 	init_alphas = torch.full((1, self.tagset_size), -10000.)
	init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
	forward_var = init_alphas #tensor([[-10000., -10000., -10000., -10000., -10000.,  0., -10000.]])

	#feats([8, 20, 7]):遍历发射概率矩阵中的每一个句子样本feat_line([20, 7])
	for feat_line in feats:
 		#遍历当前句子中的每个字符。feat([7]):
		for feat in feat_line:
			#遍历当前字符对应的每个标签。tagset_size为7,next_tag为0到6的值,每个字符有7个标签。
			for next_tag in range(self.tagset_size):
				#发射概率矩阵中每个字符对应的每个标签的概率值(单数值) 广播为 (1,7)形状的全部元素值均为该标签的概率值的二维矩阵
				emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)
				#每个字符对应的每个标签名/标签索引值 作为 转移概率矩阵中 行标签名/行索引值,该行标签名代表要转移到的目标标签,
				#同样的也可以把当前字符对应的标签认为是代表要转移到的目标标签。
				#根据行索引值所获取出的这一行的转移概率向量中的每个值代表了上一个字符的标签转移到当前字符的标签的转移概率值。
				trans_score = transitions[next_tag].view(1, -1)
				#next_tag_var/forward_var:本质上是发射概率emit_score和转移概率trans_score的累加和
				next_tag_var = forward_var + trans_score + emit_score
				#log(sum(exp(next_tag_var))):把[1, 7]形状的二维矩阵转换为单个数值输出
				alphas_t.append(log_sum_exp(next_tag_var).view(1))
			#把当前这个字符对应的7个标签的概率计算结果值传递给下一个字符继续作为forward_var使用
			forward_var = torch.cat(alphas_t).view(1, -1)
		#每个句子中全部的20个字符对应的7个标签的概率值都完成计算之后,最终还需要添加“最后一步转移到STOP_TAG的”概率值,才算完成整条句子的概率值的前向计算。
		terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
		#log(sum(exp(terminal_var))):把[1, 7]形状的二维矩阵转换为单个数值输出
		alpha = log_sum_exp(terminal_var)
前向计算矩阵forward_var:
	(形状为[1, 7]代表当前这一个字符对应的7个标签的前向计算概率值)
	1.forward_var初始化:
		tensor([[-10000., -10000., -10000., -10000., -10000.,  0., -10000.]])
		仅仅把START_TAG列赋值为0, 代表着接下来的转移只能从START_TAG开始。
 		代码:init_alphas = torch.full((1, self.tagset_size), -10000.)
		      init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
		      forward_var = init_alphas

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

	2.每次循环遍历每个字符时,还会把当前字符计算出来的前向计算矩阵forward_var 传递给下一个字符来使用。
	3.一个句子中全部20个字符对应的7个标签的概率值完成计算之后,添加“最后一步转移到STOP_TAG的”概率值,才能完成整条句子的概率值的前向计算。
	  代码:terminal_var = forward_var + transitions[self.tag_to_ix[STOP_TAG]]
		transitions[tag_to_ix[STOP_TAG]]
                		tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
                		行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
                		那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。

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

转移概率矩阵trans_score:
	(形状为[1, 7]代表当前这一个字符对应的7个标签的转移概率值)
	1.transitions[next_tag](转移概率矩阵[行索引]):每个字符对应的第1到第7个标签的索引作为行索引,获取转移概率矩阵某一行。
	2.例子:比如遍历出转移概率矩阵中的START_TAG一行,比如下面的START_TAG一行:tensor([[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]])
	  代码:trans_score = transitions[next_tag].view(1, -1) 遍历每一行7列的一维行向量
                    获取转移概率矩阵中一行7列的一维行向量:一维行向量中的7个值中的每个值分别对应7个标签,那么每个值代表当前该标签转移到下一个某标签的概率分数值。
                    transitions[next_tag]:
                        next_tag作为行索引,行索引上的标签代表了要转移到该目标行的目标标签。
                        next_tag行索引对应在转移概率矩阵transitions上的目标标签作为当前循环所遍历的当前字符的目标标签,
                        那么7列上的起始标签就相当于上一个字符的标签,
		      那么可以认为一维行向量中的7个值分别代表了上一个字符的可能的7个标签各自转移到当前字符的目标标签的转移概率值。

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

		#feats([8, 20, 7]):遍历发射概率矩阵中的每一个句子样本feat_line([20, 7])
		for feat_line in feats:
 			#遍历当前句子中的每个字符。feat([7]):
			for feat in feat_line:
				#遍历当前字符对应的每个标签。tagset_size为7,next_tag为0到6的值,每个字符有7个标签。
				for next_tag in range(self.tagset_size):
					#例如:next_tag为0时,那么transitions[next_tag]取出转移概率矩阵中的第一行7列的行向量。
					#行索引next_tag所在目标行上的标签认为是要转移到的目标标签,该目标标签即可认为是当前循环所遍历的当前字符的当前标签。
					#而每列上的标签名则可以认为是转移的起始标签,起始标签即可认为是上一个字符的标签。
					#那么行向量中的每个转移概率值便代表了上一个字符的标签转移到当前字符的标签的转移概率值。
					trans_score = transitions[next_tag].view(1, -1)

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

发射概率矩阵emit_score:
	(形状为[1, 7]代表当前这一个字符对应的7个标签的发射概率值)
	1.BiLSTM中最后的Linear线性层输出的[8, 20, 7]形状的发射概率矩阵,即[批量句子数, 句子最大长度, 标签数]。
	  每个字符对应有7个标签的概率值,每个标签的概率值(单数值)广播为(1,7)形状的全部元素值均为相同的二维矩阵。
	2.例子:
		1.[8, 20, 7]形状的发射概率矩阵
			tensor(
			[[[ 0.1331,  0.0748,  0.1188,  ...,  0.0182, -0.1034,  0.1898],
         		...,
         		[-0.1063,  0.0288, -0.2222,  ..., -0.1219,  0.1156,  0.0384]]],
       			)

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

		2.每个标签的概率值(单数值)广播为(1,7)形状的全部元素值均为相同的二维矩阵
		 (发射概率矩阵中第一个字符对应的前3个标签的概率值)
			tensor([[0.1331, 0.1331, 0.1331, 0.1331, 0.1331, 0.1331, 0.1331]])
			tensor([[0.0748, 0.0748, 0.0748, 0.0748, 0.0748, 0.0748, 0.0748]])
			tensor([[0.1188, 0.1188, 0.1188, 0.1188, 0.1188, 0.1188, 0.1188]])
			代码:emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)

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


计算损失函数第二项的分值

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

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

损失函数第二项的分值:发射概率矩阵中真实标签的发射概率值 和 转移概率矩阵中真实标签之间的转移概率值 的累加和。

#遍历当前句子中的每个字符,也即遍历[8, 20, 7]的发射概率矩阵中的每条[20, 7]的样本句子
for feat_line in feats:
	#遍历出一条样本句子中的每个字符对应的7个标签的的概率值,也即遍历[20, 7]的样本句子中每个字符对应的[7]的向量
	for i, feat in enumerate(feat_line):
		#第一项的score:之前遍历的所有字符所计算的score值的总和
                  #第二项的transitions[tags[idx][i+1],tags[idx][i]](transitions[目标标签,起始标签]):
		#	(当前字符的)上一个字符的真实标签值(作为起始标签) 转移到 当前字符的真实标签值(作为目标标签) 的转移概率值。
		#	1.tags[idx][i](起始标签):(当前字符的)上一个字符的真实标签值。i从tags标签列表中的列索引值为0的第1列的START_TAG标签值开始遍历。
		#	2.tags[idx][i+1](目标标签):循环所遍历出来的当前字符的真实标签值。i从tags标签列表中的列索引值为1的第2列(即句子中第一个字符对应的)真实标签值开始遍历。
		#				    从转移概率矩阵中所获取的“从上一个字符的真实标签转移到当前字符的真实标签的”转移概率值。
                  #第三项的feat[tags[idx][i+1]]:根据当前字符对应的真实标签值从发射概率矩阵中获取出当前字符对应的真实标签的发射概率值。
		score = score + transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]

	#第一项的score:整一条样本句子遍历完所有20个字符之后计算出来的score值的总和
         #第二项的transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[目标标签,起始标签]):
	#	句子中的最后一个字符对应的真实标签值(作为起始标签) 转移到 行标签名STOP_TAG(作为目标标签) 的转移概率值。
	#	1.transitions[tag_to_ix[STOP_TAG]](transitions[目标标签]):
	#		行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
	#		行向量中每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
	#	2.tags[idx][-1](起始标签):
	#		真实标签值为每个样本句子中的最后一个字符对应的真实标签值,最终作为转移概率矩阵中的列索引值,同时该列索引值对应的列标签名作为转移的起始标签。
	score = score + transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]

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

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

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

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


维特比算法

1.在HMM模型中的解码问题最常用的算法是维特比算法
	1.维特比算法是一个通用的解码算法,或者说是一个通用的求序列最短路径的动态规划算法,
	  是基于动态规划的求序列最短路径的方法,维特比算法同样也可以应用于解决很多其他问题。
	2.维特比算法在用于解码隐藏状态序列时,实际即给定模型和观测序列,求给定观测序列条件下,
	  最可能出现的对应的隐藏状态序列。维特比算法可以将HMM的状态序列作为一个整体来考虑,避免近似算法的问题。
	
2.当前使用维特比算法用于解码问题,负责求解解码出最优路径,即推断出最优标签序列。
  动态规划要求的是在遍历(一共20个字符)每个字符依次前向计算找到最优的7个标签存储到[20, 7]形状的回溯列表,
  然后再进行反向回溯解码时从回溯列表中找出每个字符最优的一个标签,
  便是按照从最后一个字符往前的方向 根据第i个字符的最优标签的索引值找到第i-1个字符(即第i个字符的上一个字符)
  的最优标签的索引值。

	#1.result_best_path最终返回的形状为二维的[8, 20],包含“等于批量句子样本数8的”列表个数,
	#  每个列表中又存放“等于句子最大长度的”元素个数,最终的元素值为每个字符解码预测出来的最优标签的索引值。
	#2.result_best_path存储的是批量每个句子中每个字符解码预测出的最优标签的索引值
	result_best_path = []
	
	#遍历发射概率矩阵(形状[8, 20, 7])中每个样本句子(形状[20, 7])
	for feat_line in feats:
		#1.回溯指针:backpointers回溯列表最终返回的形状为二维的[20, 7],
		#  包含“等于句子最大长度20的”列表个数,每个列表中又存放“等于标签数7的”元素个数,
		#  每个小列表中的7个元素值代表每个字符通过前向计算得到的7个最大概率的标签索引值。
		#2.回溯指针backpointers存储的是当前句子中每个字符通过前向计算得到的7个最大概率的标签索引值。
		backpointers = []
		
		#[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
		init_vvars = torch.full((1, self.tagset_size), -10000.)
		#仅设置索引为5“START_TAG”标签的列值为0,代表只能从START_TAG标签开始
		#[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]
		init_vvars[0][self.tag_to_ix[START_TAG]] = 0
		#前向计算矩阵forward_var的初始化赋值
		#	在前向计算过程中遍历的第i个字符(time_step)时,forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量
		forward_var = init_vvars
		
		#遍历发射概率矩阵中一条样本句子(形状[20, 7])中每个字符(形状[7])对应的7个标签的发射概率值
		for feat in feat_line:
			
			#当前字符对应的回溯列表:负责存储每个字符中7个(目标)标签对应的最大概率值的起始标签的索引值
			bptrs_t = []
			
			#当前字符对应的维特比列表:负责存储每个字符中7个(目标)标签对应的最大概率值
			viterbivars_t = []
			
			#遍历发射概率矩阵中的每个字符(形状[7])对应的7个标签的发射概率值
			for next_tag in range(self.tagset_size):
				
				#1.forward_var(前向计算矩阵):
				#	实质为每个字符对应的7个(目标)标签的最大转移概率值和7个标签的发射概率值的累计和。
				#	前向计算矩阵所计算的每个当前字符的累计和的值都会传递给下一个字符作为forward_var继续进行累加和计算。
				#	在前向计算过程中遍历的第i个字符(time_step)时,
				#	forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。
				#2.transitions[next_tag]:
				#	从转移概率矩阵中取出“行索引为当前标签值的”一行7列(形状[7])的行向量。
				#	行向量中的7个值代表7个标签转移到当前字符所遍历的当前标签(即目标标签)的转移概率值。
				next_tag_var = forward_var + transitions[next_tag]
				
				#best_tag_id:
				#	因为每个字符依次前向计算需要找到最优的7个标签,
				#	那么此处首先需要找到每个字符所遍历的每个(目标)标签的最大概率值,
				#	argmax目的就是从当前字符所遍历的标签作为目标标签的7个概率值中取出一个最大概率值的索引,
				#	同时该最大概率值的索引代表了“7个作为转移的起始标签转移到当前目标标签中”最大概率值的一个起始标签。
				best_tag_id = argmax(next_tag_var)
				
				#把当前最大概率值的起始标签的索引值保存到当前字符对应的回溯列表中
				bptrs_t.append(best_tag_id)
				
				#根据当前最大概率值的起始标签的索引值取出该最大概率值保存到当前字符对应的维特比列表中
				viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
			
			#forward_var = torch.cat(viterbivars_t) + feat
			#	1.forward_var:
			#		实质为每个字符对应的7个标签的转移概率值和7个标签的发射概率值的累计和。
			#		在前向计算过程中遍历的第i个字符(time_step)时,
			#		forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。
			#	2.torch.cat(viterbivars_t):变成torch.Size([7])类型。
			#	3.feat:当前字符对应的7个标签的发射概率值
			forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)	
			
			#把每个字符对应的(形状[7]的)回溯列表 存储到(形状[20, 7]的)句子对应的回溯列表
			backpointers.append(bptrs_t)
				
		#1.执行到此处代表了句子中全部20个字符已经前向计算完毕,最终前向计算矩阵要添加“转移到STOP_TAG的”转移概率值。
		#2.forward_var:保存了“经过句子中全部20个字符前向计算的”(形状[1, 7]的)矩阵值
		#3.transitions[tag_to_ix[STOP_TAG]]
        		#	tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
        		#	行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
        		#	那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
		terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]	
		
		#获取出当前句子对应的(形状[1, 7]的)最终概率值矩阵中的最大概率值的标签的索引值
		#该索引值代表句子中最后一个字符(第20个字符)的最优标签的索引值。
		best_tag_id = argmax(terminal_var)

		#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,
		#因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。
		#此处先保存下句子中最后一个字符(第20个字符)的最优标签的索引值
		best_path = [best_tag_id]		
		
		#1.reversed翻转回溯列表即倒序排序,从最后一个字符往前遍历,即从第i个字符往第i-1个字符进行遍历。
		#2.先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
		#3.最终best_path列表保存有20个字符的最优标签的索引值加上一个START_TAG标签的索引值
		for bptrs_t in reversed(backpointers):
			#先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
			best_tag_id = bptrs_t[best_tag_id]
			#把每个字符对应的最优标签的索引值追加到best_path列表末尾
			best_path.append(best_tag_id)

		#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,
		#因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。
		#pop()删除best_path列表中存储的最后一个值(START_TAG标签的索引值)
		start = best_path.pop()
		
		#assert断言:删除该值必定为START_TAG标签的索引值
		assert start == self.tag_to_ix[START_TAG]
		
		#重新把best_path列表翻转回正常的字符顺序排序
		best_path.reverse()
#[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
init_vvars = torch.full((1, self.tagset_size), -10000.)
#仅设置索引为5“START_TAG”标签的列值为0,代表只能从START_TAG标签开始
#[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]
init_vvars[0][self.tag_to_ix[START_TAG]] = 0
#前向计算矩阵forward_var的初始化赋值
#	在前向计算过程中遍历的第i个字符(time_step)时,forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量
forward_var = init_vvars

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

#当前第一个字符对应的(形状[7]的)回溯列表:保存当前第一个字符中7个(目标)标签对应的最大概率值的起始标签的索引值
bptrs_t = []
#当前字符对应的(形状[7]的)维特比列表:保存当前第一个字符中7个(目标)标签对应的最大概率值
viterbivars_t = []
#一条句子中20个字符对应的(形状[20, 7]的)回溯列表:保存当前样本句子中所有20个字符对应的(形状[7]的)回溯列表
backpointers.append(bptrs_t)

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

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

#最终计算完20个字符的前向计算矩阵forward_var再添加上“转移到STOP_TAG的”转移概率值。
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]	

#获取出当前句子对应的(形状[1, 7]的)最终概率值矩阵中的最大概率值的标签的索引值
#该索引值代表句子中最后一个字符(第20个字符)的最优标签的索引值。
best_tag_id = argmax(terminal_var)

#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,因还需要把START_TAG标签的索引值移除掉才能作为函数返回值
#此处先保存句子中最后一个字符(第20个字符)的最优标签的索引值
best_path = [best_tag_id]		

#1.reversed翻转回溯列表即倒序排序,从最后一个字符往前遍历,即从第i个字符往第i-1个字符进行遍历。
#2.先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
#3.最终best_path列表保存有20个字符的最优标签的索引值加上一个START_TAG标签的索引值
for bptrs_t in reversed(backpointers):
	#先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
	best_tag_id = bptrs_t[best_tag_id]
	#把每个字符对应的最优标签的索引值追加到best_path列表末尾
	best_path.append(best_tag_id)

#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,因还需要把START_TAG标签的索引值移除掉才能作为函数返回值
#pop()删除best_path列表中存储的最后一个值(START_TAG标签的索引值)
start = best_path.pop()

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

"""
损失函数的定义:
    1.BiLSTM层的输出维度是tag_size, 也就是每个单词w_i映射到tag_size维度个数的发射概率值。
    2.假设BiLSTM的输出矩阵是P, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率。
      对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率。
    3.对于输入序列X对应的输出tag序列y, 定义分数如下(本质上就是发射概率和转移概率的累加和):
    4.利用softmax函数, 为每一个正确的tag序列y定义一个概率值, 在真实的训练中, 只需要最大化似然概率p(y|X)即可。
"""
"""
1.发射概率矩阵:
    1.发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率。
    2.BiLSTM经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
    3.Linear输出的特征矩阵的形状为torch.Size([8, 20, 7]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), tag_to_id的标签数)。
      比如从[8, 20, 7]中取第一个样本句子中的第一个字符对应的维度值为7的一维向量:
      [ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01,  7.4794e-03, -3.9872e-01]。
      该维度值为7的一维向量中的7个数值 代表了 第一个句子中第一个字分别被标记为["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]的7个分数, 
      由此可以判断第一个句子中第一个字被标注为"O"的分数最高(4.0880e-02 > 8.4794e-03)。

2.转移概率矩阵
    1.首先假设我们需要标注的实体类型有一下几类:["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]
        其中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 
        "<START>":句子的开始字符
        "<STOP>":句子的结束字符
    2.因此我们很容易知道每个字的可能标注类型有以上五种可能性, 那么在一个句子中, 由上一个字到下一个字的概率乘积就有7x7种可能性。
    3.最终训练出来结果大致会如上图所示, 其中下标索引为(i, j)的方格代表如果当前字符是第i行表示的标签, 
      那么下一个字符表示第j列表示的标签所对应的概率值. 以第二行为例, 假设当前第i个字的标签为B-dis, 
      那么第i+1个字最大可能出现的概率应该是I-dis.
"""

# 导入相关包与模块
import torch
import torch.nn as nn
import torch.optim as optim

# 输入参数:
# 开始字符和结束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
"""
tag_to_id 表示的意思如下:
    其中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 
    START_TAG: 句子开始字符
    STOP_TAG: 句子结束字符
"""
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的字符和序号的对应码表
# char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,
#               "状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}

# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"
]
char_to_id = {"<PAD>":0}

# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
# print("标签",tags.shape) #torch.Size([8, 20]) 即 [批量样本句子数, 最大句子长度]

"""
BiLSTM+CRF模型的实现:
        第一步: 构建神经网络
        第二步: 文本信息张量化
        第三步: 计算损失函数第一项的分值
        第四步: 计算损失函数第二项的分值
        第五步: 维特比算法的实现
        第六步: 完善BiLSTM_CRF类的全部功能
"""

#---------------------------------------第一步: 构建神经网络------------------------------------------------------#
class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, num_layers, batch_size, sequence_length):
        '''
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_ix:           标签与id对照字典
        :param embedding_dim:       字嵌入维度(即LSTM输入层维度input_size)
        :param hidden_dim:          隐藏层向量维度
        :param num_layers:          神经网络的层数
        :param batch_size:          批次的数量
        :param sequence_length:     语句的限制最大长度
        '''
        # 继承函数的初始化
        super(BiLSTM_CRF, self).__init__()
        # 设置标签与id对照(标签到id的映射字典)
        self.tag_to_ix = tag_to_ix
        # 设置标签的总数,对应 BiLSTM 最终输出分数矩阵宽度
        self.tagset_size = len(tag_to_ix)
        # 设定 LSTM 输入特征大小(词嵌入的维度)
        self.embedding_dim = embedding_dim
        # 设置隐藏层维度
        self.hidden_dim = hidden_dim
        # 设置单词总数的大小/单词的总数量
        self.vocab_size = vocab_size
        # 设置隐藏层的数量
        self.num_layers = num_layers
        # 设置语句的最大限制长度
        self.sequence_length = sequence_length
        # 设置批次的大小
        self.batch_size = batch_size
        """ 
        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度) 
        注:embedding cuda 优化仅支持 SGD 、 SparseAdam
        """
        # 构建词嵌入层, 两个参数分别是单词总数, 词嵌入维度
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
        """ 
        因为是BiLSTM双向循环,前向隐藏层占一半隐藏层维度,后向隐藏层占一半隐藏层维度,因此需要设置为hidden_size // 2。
        BiLSTM的输出层output的维度为hidden_size,即前向隐藏层的一半隐藏层维度+后向隐藏层的一半隐藏层维度。
        """
        # 构建双向LSTM层: BiLSTM (参数: input_size      字向量维度(即输入层大小/词嵌入维度),
        #                               hidden_size     隐藏层维度,
        #                               num_layers      层数,
        #                               bidirectional   是否为双向,
        #                               batch_first     是否批次大小在第一位)
        # 构建双向LSTM层, 输入参数包括词嵌入维度, 隐藏层大小, 堆叠的LSTM层数, 是否双向标志位
        self.lstm = nn.LSTM(embedding_dim,      #词嵌入维度
                            hidden_dim // 2,    #若为双向时想要得到同样大小的向量, 需要除以2
                            num_layers=self.num_layers,
                            bidirectional=True)
        """ 
        1.BiLSTM经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
        2.Linear 可以把 (当前批量样本句子数, 当前样本的序列长度(单词个数), 隐藏层中神经元数数量) 转换为 (当前批量样本句子数, 当前样本的序列长度(单词个数), tag_to_id的标签数)
          Linear 也可以把 (当前样本的序列长度(单词个数), 当前批量样本句子数, 隐藏层中神经元数数量) 转换为 (当前样本的序列长度(单词个数), 当前批量样本句子数, tag_to_id的标签数)
        """
        # 构建全连接线性层, 一端对接BiLSTM隐藏层, 另一端对接输出层, 输出层维度就是标签数量tagset_size
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        """
        1.transitions转移矩阵 是一个方阵[tagset_size, tagset_size]。
          tag_to_ix[START_TAG]值为5,tag_to_ix[STOP_TAG]值为6,不管是行数还是列数都从0开始统计。
          transitions转移矩阵中行名为当前字符的标签,列名为下一个字符的标签,那么列值便是下一个字符出现该标签的概率值,
          需要计算出列值中下一个字符出现某标签的最大概率值。
        
        2.transitions转移矩阵的 第一种写法(项目中使用该写法)
            假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,
            也就是每个单词w_i映射到标签tag的发射概率值。
            那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率,tag_j代表当前字符的标签,
            tag_i代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_j转移到下一个字符的标签tag_i的概率值。
          
            1.transitions.data[tag_to_ix[START_TAG], :]:
                第5行的所有列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,
                即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。
            2.transitions.data[:, tag_to_ix[STOP_TAG]]
                所有行的第5列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,
                即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。
            3.transitions[i,j]:
                其中下标索引为[i,j]的方格代表当前字符的标签为第j列的列名, 那么下一个字符的标签为第i行的行名,
                那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。
        3.transitions转移矩阵的 第二种写法
            假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,
            也就是每个单词w_i映射到标签tag的发射概率值。
            那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_i转移到tag_j的概率,tag_i代表当前字符的标签,
            tag_j代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_i转移到下一个字符的标签tag_j的概率值。
          
            1.transitions.data[:, tag_to_ix[START_TAG]]=-10000:
                所有行的第5列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,
                即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。
            2.transitions.data[tag_to_ix[STOP_TAG], :]=-10000:
                第5行的所有列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,
                即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。
            3.transitions[i,j]:
                其中下标索引为[i,j]的方格代表当前字符的标签为第i行的行名, 那么下一个字符的标签为第j列的列名,
                那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。
       """
        # 初始化转移矩阵, 转移矩阵是一个方阵[tagset_size, tagset_size]
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))
        # 按照损失函数小节的定义, 任意的合法句子不会转移到"START_TAG", 因此设置为-10000
        # 同理, 任意合法的句子不会从"STOP_TAG"继续向下转移, 也设置为-10000
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000
        # 初始化隐藏层, 利用单独的类函数init_hidden()来完成
        self.hidden = self.init_hidden()

    """
    BiLSTM(双向):
        如果RNN是双向的,num_directions为2,单向的话num_directions为1。
        不管是哪种组合,只有c0/cn 和 h0/hn的形状 在两种组合之间有区别,output.shape在两种组合之间并没有区别。
        1.第一种组合:
                1.batch_first=False:
                    nn.LSTM(input_size=input_feature_size, #词嵌入维度
                            hidden_size=hidden_size,    #隐藏层中神经元数量
                            num_layers=num_layers,      #隐藏层层数
                            bidirectional=True,         #是否为双向
                            batch_first=False)  
                2.c0/cn 和 h0/hn 均为
                        torch.randn(num_layers * num_directions, sequence_length, hidden_size // 2) 
                        即 (隐藏层层数 * 2, 一个句子单词个数, 隐藏层中神经元数量 // 2)
                        如果RNN是双向的,num_directions为2,单向的话num_directions为1。
                3.output, (hn, cn) = bilstm(input, (h0, c0))
                    input.shape:(BATCH_SIZE, sequence_length, input_feature_size) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)
                    hn.shape:torch.Size([2, 20, 50]) 即 (隐藏层层数 * 2, 一个句子单词个数, 隐藏层中神经元数量 // 2)	
                    cn.shape:torch.Size([2, 20, 50]) 即 (隐藏层层数 * 2, 一个句子单词个数, 隐藏层中神经元数量 // 2)	
                    output.shape:torch.Size([8, 20, 100]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量 * 2)	
        2.第二种组合:
                1.batch_first=True
                    nn.LSTM(input_size=input_feature_size, #词嵌入维度
                            hidden_size=hidden_size,    #隐藏层中神经元数量
                            num_layers=num_layers,      #隐藏层层数
                            bidirectional=True,         #是否为双向
                            batch_first=True)  
                2.c0/cn 和 h0/hn 均为 
                        torch.randn(num_layers * num_directions, batch_size, hidden_size // 2) 
                        即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
                        如果RNN是双向的,num_directions为2,单向的话num_directions为1。
                3.output, (hn, cn) = bilstm(input, (h0, c0))
                    input.shape:(BATCH_SIZE, sequence_length, input_feature_size) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)
                    hn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)	
                    cn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
                    output.shape torch.Size([8, 20, 100]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量 * 2)
    """
    # 定义类内部专门用于初始化隐藏层的函数
    def init_hidden(self):
        """
         hn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
         cn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
        """
        # 为了符合LSTM的输入要求, 我们返回h0, c0, 这两个张量的shape完全一致
        # 需要注意的是shape: [2 * num_layers, batch_size, hidden_dim // 2]
        return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2),
                 torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2))

# 调用:
# model = BiLSTM_CRF(vocab_size=len(char_to_id),
#                    tag_to_ix=tag_to_ix,
#                    embedding_dim=EMBEDDING_DIM,
#                    hidden_dim=HIDDEN_DIM,
#                    num_layers=NUM_LAYERS,
#                    batch_size=BATCH_SIZE,
#                    sequence_length=SENTENCE_LENGTH)
# print(model)

    #---------------------------------------第二步: 文本信息张量化------------------------------------------------------#
    """
    BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数]):
        每个字符对应一个包含7个数值的一维向量,7个数值对应7标签(["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]),
        那么每个数值便代表了该字符被标注为该标签的概率值
    """
    # 在类中将文本信息经过词嵌入层, BiLSTM层, 线性层的处理, 最终输出句子张量
    def _get_lstm_features(self, sentence):
        """
        :param sentence: “每个元素值均为索引值的”批量句子数据,形状为[8, 20] 即 [批量句子数, 句子最大长度]
        :return:BiLSTM中最后的Linear线性层输出的(句子最大长度, 批量句子数, tag_to_id的标签数)
        """
        # 返回的hidden为(hn,cn),hn和cn均为 torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
        self.hidden = self.init_hidden()
        """
        1.embedding输入形状和输出形状:(BATCH_SIZE行 sequence_length列,批量大小句子数为BATCH_SIZE,sequence_length为句子长度)
            embedding输入:(BATCH_SIZE, sequence_length) 即 (当前批量样本句子数, 句子长度)
            embedding输出:(BATCH_SIZE, sequence_length, embedding_dim) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)
        2.embedding 使用cuda(gpu)进行运行优化时 仅支持 SGD、SparseAdam的优化器
        """
        # a = self.word_embeds(sentence)
        # print(a.shape)  # torch.Size([8, 20, 200]) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)

        """
        通过 view(self.sequence_length, self.batch_size, -1) 把 [8, 20, 200] 转换为 [20, 8, 200]。
        即 (当前批量样本句子数, 句子长度, 词嵌入维度) 转换为 (句子长度, 当前批量样本句子数, 词嵌入维度)。
        """
        # LSTM的输入要求形状为 [sequence_length, batch_size, embedding_dim]
        # LSTM的隐藏层h0要求形状为 [num_layers * direction, batch_size, hidden_dim]
        # 让sentence经历词嵌入层
        embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)
        # print("embeds.shape",embeds.shape) #torch.Size([20, 8, 200]) 即 (句子长度, 当前批量样本句子数, 词嵌入维度)

        """
        1.output, (hn, cn) = bilstm(input, (h0, c0))
            input.shape(embeds.shape):(sequence_length, BATCH_SIZE, embedding_dim) 即 (句子长度, 当前批量样本句子数, 词嵌入维度)
            hn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
            cn.shape torch.Size([2, 8, 50]) 即 (隐藏层层数 * 2, 当前批量样本句子数, 隐藏层中神经元数量 // 2)
        2.因为输入BiLSTM层的数据为[20, 8, 200](句子长度, 当前批量样本句子数, 词嵌入维度),
          因此BiLSTM层输出的也为[20, 8, 200],最后通过线性层输出[20, 8, 100]。
        """
        # 将词嵌入层的输出, 进入BiLSTM层, LSTM的两个输入参数: 词嵌入后的张量, 随机初始化的隐藏层张量
        lstm_out, self.hidden = self.lstm(embeds, self.hidden)
        # print("lstm_out",lstm_out.shape) #torch.Size([20, 8, 100]) 即 [句子长度, 批量句子数, 隐藏层中神经元数]

        # 要保证输出张量的shape: [sequence_length, batch_size, hidden_dim]
        lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)
        # print("lstm_out", lstm_out.shape) #torch.Size([20, 8, 100]) 即 [句子长度, 批量句子数, 隐藏层中神经元数]

        """ Linear 也可以把 [20, 8, 100] (当前样本的序列长度(单词个数), 当前批量样本句子数, 隐藏层中神经元数数量) 
           转换为 [20, 8, 7](当前样本的序列长度(单词个数), 当前批量样本句子数, tag_to_id的标签数) 
        """
        # 将BiLSTM的输出经过一个全连接层, 得到输出张量shape:[sequence_length, batch_size, tagset_size]
        lstm_feats = self.hidden2tag(lstm_out)
        # print("lstm_feats.shape",lstm_feats.shape) #[20, 8, 7]
        return lstm_feats

    #---------------------------------------第三步: 计算损失函数第一项的分值forward_score------------------------------------------------------#
    """
    BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵:
        每个字符对应一个包含7个数值的一维向量,7个数值对应7标签(["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]),
        那么每个数值便代表了该字符被标注为该标签的概率值
        
    转移概率矩阵:
        转移概率矩阵的形状为[tagset_size, tagset_size],tagset_size为标签数。
        矩阵中每个数值代表了当前字符的标签 转移到 下个字符的出现某标签的概率值。
    """
    # 计算损失函数第一项的分值函数, 本质上是发射矩阵和转移矩阵的累加和
    def _forward_alg(self, feats):
        # print("feats",feats)
        """
        :param feats: BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
        :return:
        """
        """ 创建形状为(1, self.tagset_size)的二维矩阵作为前向计算矩阵,其中每个元素值均为-10000。
            init_alphas = [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
        """
        # init_alphas: [1, 7] , [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
        # 初始化一个alphas张量, 代表前向计算矩阵的起始位置
        init_alphas = torch.full((1, self.tagset_size), -10000.)
        # print("init_alphas",init_alphas) #tensor([[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]])
        # print("init_alphas.shape",init_alphas.shape) #torch.Size([1, 7])

        """ 
        前向计算矩阵的初始化:把1行中的第6列设置为0,第6列代表START_TAG,意思就是当前字符的标签转移到下一个字符的标签只能从START_TAG开始。
            把(1, self.tagset_size)的前向计算矩阵中的索引为5的元素值设置为0,索引为5对应的为“START_TAG”标签 
            init_alphas = [[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]]
        """
        # 仅仅把START_TAG赋值为0, 代表着接下来的转移只能从START_TAG开始
        init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
        # print("init_alphas", init_alphas) #tensor([[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]])

        """ 此处仅为浅拷贝,只是为了更方便所以才使用新变量forward_var """
        # 前向计算变量的赋值, 这样在反向求导的过程中就可以自动更新参数
        # 将初始化的init_alphas赋值为前向计算变量, 为了后续在反向传播求导的时候可以自动更新参数
        forward_var = init_alphas

        """ 
        feats: BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
        transpose(1, 0):把 (句子最大长度, 批量句子数, tag_to_id的标签数) 转换为 (批量句子数, 句子最大长度, tag_to_id的标签数)
        """
        # 输入进来的feats: [20, 8, 7], 为了接下来按句子进行计算, 要将batch_size放在第一个维度上
        feats = feats.transpose(1, 0)
        # print("feats.shape", feats.shape)# [8, 20, 7]

        """ 
        result:形状为(1, 8)的二维矩阵 即(1, batch_size),每个句子计算出一个分数,批量句子数为8。
        每个句子中有20个字符,每个字符对应7个标签的发射概率。
        """
        # feats: [8, 20, 7]是一个3维矩阵, 最外层代表8个句子, 内层代表每个句子有20个字符,每一个字符映射成7个标签的发射概率
        # 初始化最终的结果张量, 每个句子对应一个分数
        result = torch.zeros((1, self.batch_size))
        # print("result.shape", result.shape) #torch.Size([1, 8])
        idx = 0 #用于记录当前批量样本句子数中所遍历的第几个句子

        """ 
        遍历发射概率矩阵中的每一个句子样本:遍历BiLSTM输出的“根据批量句子计算出来的特征数据中的”每个句子对应的特征值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子数, 句子最大长度, tag_to_id的标签数),也即 BiLSTM输出的“根据批量句子计算出来的特征数据
        feat_line:[20, 7] 即 (句子最大长度, tag_to_id的标签数)
        """
        # 按行遍历, 总共循环batch_size次:feats为[8, 20, 7]
        for feat_line in feats:
            """ 
            遍历发射概率矩阵中当前一个句子样本中的每一个字符:遍历句子中的每个字符。
            feat:[7] 即 (tag_to_id的标签数)
            """
            # feat_line: [20, 7]
            # 遍历每一行语句, 每一个feat代表一个time_step,即一个字符就是一个time_step,一共遍历20个字符(time_step)
            for feat in feat_line:
                """ 
                alphas_t
                    把当前该字符对应的7个标签中每个标签所计算出来的概率值存储到alphas_t中。
                    例子:[[第1个标签的概率计算结果单个数值],[第2个标签...],[第3个标签...],[第4个...],[第5个...],[第6个...],[第7个...]] 
                """
                # 当前的字符(time_step),初始化一个前向计算张量(forward tensors)
                alphas_t = []
                """ 
                遍历发射概率矩阵中当前一个字符对应的7个(tagset_size个)标签的概率值(BiLSTM输出的概率值):
                    遍历字符对应的7个(tagset_size个)标签中的每个标签的概率值
                """
                # print("===============")
                # 在当前time_step/每一个时间步,遍历所有可能的转移标签, 进行累加计算
                for next_tag in range(self.tagset_size):
                    """
                   1.对发射概率矩阵中字符对应标签的单个数值的概率值 进行广播为 (1,7)的二维数组来使用:
                        把每个字符对应的第1到第7个(tagset_size个)标签的“BiLSTM输出的”单个数值的概率值 逐个转换为 (1,7)的二维数组来使用。
                   2.feat[next_tag]:获取出每个字符对应的第1到第7个(tagset_size个)标签的“BiLSTM输出的”概率值,为单个数值的概率值。
                     view(1, -1):把单个数值的概率值转换为(1,1)的二维数组
                     expand(1, self.tagset_size):通过广播张量的方式把(1,1)的二维数组转换为(1,7)
                   """
                    # 广播发射矩阵的分数/构造发射分数的广播张量
                    emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)
                    # print("emit_score.shape",emit_score.shape) #torch.Size([1, 7])
                    # print("emit_score",emit_score)

                    """ 
                    1.transitions[next_tag]:
                        获取转移概率矩阵中一行7列的一维行向量。
                        next_tag作为行索引,行索引上的标签代表了要转移到该目标行的目标标签。
                        next_tag行索引对应在转移概率矩阵transitions上的目标标签作为当前循环所遍历的当前字符的目标标签,
                        那么7列上的起始标签就相当于上一个字符的标签,一维行向量中的7个值分别代表了上一个字符的可能的7个标签各自
                        转移到当前字符的目标标签的转移概率值。
                    2.例子
                        #遍历当前句子中的每个字符
                        for feat in feat_line:
                            #遍历当前字符对应的每个标签。tagset_size为7,next_tag为0到6的值,每个字符有7个标签。
                            for next_tag in range(self.tagset_size):
                                #例如:next_tag为0时,那么transitions[next_tag]取出转移概率矩阵中的第一行7列的行向量。
                                #行索引next_tag所在目标行上的标签认为是要转移到的目标标签,该目标标签即可认为是当前循环所遍历的当前字符的当前标签。
                                #而每列上的标签名则可以认为是转移的起始标签,起始标签即可认为是上一个字符的标签。
                                #那么行向量中的每个转移概率值便代表了上一个字符的标签转移到当前字符的标签的转移概率值。
                                trans_score = transitions[next_tag].view(1, -1)
                    3.transitions[next_tag]:torch.Size([1, 7]) 一行7列的一维向量
                      view(1, -1):torch.Size([1, 7]) 一行7列的一维向量
                   """
                    # 第i个time_step循环时, 转移到next_tag标签的转移概率
                    # 当前时间步, 转移到next_tag标签的转移分数
                    trans_score = self.transitions[next_tag].view(1, -1)
                    # print("trans_score.shape",trans_score.shape) #torch.Size([1, 7])
                    # print("trans_score", trans_score)

                    """ next_tag_var:把形状均为[1, 7]的前向计算矩阵、转移概率矩阵、发射概率矩阵 三者进行相加,结果同样为[1, 7] """
                    # 将 前向计算矩阵, 转移矩阵, 发射矩阵累加
                    next_tag_var = forward_var + trans_score + emit_score
                    # print("next_tag_var.shape",next_tag_var.shape) #torch.Size([1, 7])
                    # print("next_tag_var", next_tag_var)

                    """ 
                    log_sum_exp(next_tag_var) 即 log(sum(exp(next_tag_var))) 
                        即把[1, 7]形状的二维矩阵转换为单个数值输出。
                        log(sum(exp(next_tag_var)))输出的单个数值代表当前该字符对应的7个标签中的第N个标签的计算得分值。
                   """
                    # 计算log_sum_exp()函数值, 并添加进alphas_t列表中
                    # a = log_sum_exp(next_tag_var), 注意: log_sum_exp()函数仅仅返回一个实数值
                    # print(a.shape) : tensor(1.0975) , shape为([]) 代表没有维度 即为单个数值
                    # b = a.view(1) : tensor([1.0975]), 注意: a.view(1)的操作就是将一个数字变成一个一阶矩阵, 从([]) 变成 ([1]) 即一维向量
                    # print(b.shape) : ([1]) 代表 一维向量
                    alphas_t.append(log_sum_exp(next_tag_var).view(1))

                #alphas_t 存储的是 一个字符 对应的 七个标签的 概率计算结果值
                # print(len(alphas_t)) #7
                # print("alphas_t",alphas_t)

                # print(alphas_t) :
                #       [tensor([337.6004], grad_fn=<ViewBackward>),
                #        tensor([337.0469], grad_fn=<ViewBackward>), tensor([337.8497], grad_fn=<ViewBackward>),
                #        tensor([337.8668], grad_fn=<ViewBackward>), tensor([338.0186], grad_fn=<ViewBackward>),
                #        tensor([-9662.2734], grad_fn=<ViewBackward>), tensor([337.8692], grad_fn=<ViewBackward>)]
                # temp = torch.cat(alphas_t)
                # print(temp) : tensor([[  337.6004,   337.0469,   337.8497,   337.8668,   338.0186, -9662.2734, 337.8692]])
                """ 
                此处把 alphas_t(封装了当前字符对应的7个标签的概率值) 赋值给 前向计算矩阵forward_var 目的为传递给下一个字符计算每个标签时使用。
                1.forward_var 和 alphas_t 中形状相同均为[1, 7],两者数值均相同,两者仅所封装的容器的类型不同。
                  此处仅为把 [1, 7]形状的alphas_t 从列表类型的 转换为 [1, 7]形状的forward_var的 tensor类型。
                2.forward_var 和 alphas_t 均代表了 当前这一个字符 对应的 七个标签的 概率计算结果值。
                  每次循环遍历每个字符时,还会把当前字符计算出来的前向计算矩阵forward_var 传递给下一个字符来使用。
                """
                # 将列表张量转变为二维张量
                forward_var = torch.cat(alphas_t).view(1, -1)
                # print(forward_var.shape) # torch.Size([1, 7])
                # print("forward_var",forward_var)

            # print("forward_var",forward_var) #tensor([[43.5019, 42.9249, 42.8782, 42.6559, 43.1508, -9957.1201, 42.7291]])
            # print("forward_var.shape",forward_var.shape) #torch.Size([1, 7])

            # print("self.transitions", self.transitions)
            # print("self.transitions.shape",self.transitions.shape) #torch.Size([7, 7])
            # print("self.tag_to_ix[STOP_TAG]",self.tag_to_ix[STOP_TAG]) #6
            # print("self.transitions[self.tag_to_ix[STOP_TAG]]",self.transitions[self.tag_to_ix[STOP_TAG]]) #使用索引值为6作为获取转移概率矩阵的行值
            # print("self.transitions[self.tag_to_ix[STOP_TAG]].shape",self.transitions[self.tag_to_ix[STOP_TAG]].shape) #torch.Size([7])
            """ 
            transitions[tag_to_ix[STOP_TAG]]
                tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
                行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
                那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
                
            1.执行到此处表示遍历完当前句子中的所有字符,并且准备遍历下一个句子。
            2.transitions[tag_to_ix[STOP_TAG]]:(形状为[7, 7]的transitions转移概率矩阵)
                transitions[6]:获取出形状[7]的一维向量,使用行索引为6 获取转移概率矩阵的第7行(即最后一行7列)的STOP_TAG标签的概率值。
                比如:tensor([ 2.0923e+00, 1.5542e+00, -9.2415e-01, 6.1887e-01, -8.0374e-01, 4.5433e-02, -1.0000e+04])
                其中的最后一个值-1.0000e+04即为-10000。
            3.执行到此处的[1, 7]形状的前向计算矩阵forward_var:
                代表了一个句子中全部20个字符对应的7个标签计算的概率值都保存到了[1, 7]的前向计算矩阵forward_var中。
            4.[1, 7]形状的前向计算矩阵forward_var + [7]形状的STOP_TAG标签的概率值的向量
                代表给当前句子添加“最后一步转移到STOP_TAG的”概率值,才能完成整条句子的概率值的前向计算。
            """
            # 添加最后一步转移到"STOP_TAG"的分数, 就完成了整条语句的分数计算
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
            # print("terminal_var",terminal_var) #tensor([[329.3152, 329.5251, 329.1519, 329.7561, 328.9988, -9670.7090, -9671.0156]])
            # print("terminal_var.shape",terminal_var.shape) #torch.Size([1, 7])

            """ 
            log_sum_exp(terminal_var) 即 log(sum(exp(terminal_var))) 
                terminal_var即为一条样本句子的最终得分,因此把把[1, 7]形状的二维矩阵转换为单个数值输出。
           """
            # 计算log_sum_exp()函数值, 作为一条样本语句的最终得分(将terminal_var放进log_sum_exp()中进行计算, 得到一条样本语句最终的分数)
            alpha = log_sum_exp(terminal_var)
            # print(alpha) : tensor(341.9394)

            """ result:形状为(1, batch_size),存储每个句子计算出来的最终得分。每个句子计算出一个分数。 """
            # 将得分添加进结果列表中, 作为函数结果返回
            result[0][idx] = alpha
            idx += 1 #用于记录当前批量样本句子数中所遍历的第几个句子

            """ result:[1, batch_size]中第二维为批量句子中每个句子的最终计算得分 """
        return result

    #---------------------------------------第四步: 计算损失函数第二项的分值gold_score------------------------------------------------------#
    def _score_sentence(self, feats, tags):
        """
        :param feats: BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        :param tags: 即每个句子中的每个字符对应的标签值,[8, 20] 即 [批量样本句子数, 最大句子长度]
        :return:
        """
        """
        创建[batch_size, 1]形状的值全部为START_TAG的二维矩阵:tensor([[5], [5], [5], [5], [5], [5], [5], [5]])
        
        1.第一种写法:
            torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long) 
            会出现用户警告如下:
            UserWarning:
                要从张量复制构造,建议使用 sourceTensor.clone().detach()  
                或 sourceTensor.clone().detach().requires_grad_(True),而不是 torch.tensor(sourceTensor)。
        2.第二种写法:
            使用 sourceTensor.clone().detach() 或 sourceTensor.clone().detach().requires_grad_(True) 该方式不会出现用户警告。
            detach():分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
            改写为 torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG], dtype=torch.long).clone().detach()
            
        3.tag_to_ix[START_TAG]:5
          (batch_size, 1) 此处即为[8,1]:tensor([[5], [5], [5], [5], [5], [5], [5], [5]])
        """
        # 将START_TAG和真实标签tags做列维度上的拼接。要在tags矩阵的第一列添加,这一列全部都是START_TAG。
        # temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long)
        temp = torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG], dtype=torch.long).clone().detach()
        # print("temp",temp) #torch.Size([8, 1])
        # print("temp.shape",temp.shape) #tensor([[5], [5], [5], [5], [5], [5], [5], [5]])

        """
        在[8, 20]的tags 前面增加1列全为5的真实标签值的列向量变成 [8, 21],
        即相当于每条样本句子对应的真实标签值的最开头增加一个START_TAG标签的真实值5。
        如下:tensor([[5, 0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0], 。。。。。。])
        """
        tags = torch.cat((temp, tags), dim=1)
        # print("tags.shape",tags.shape) #torch.Size([8, 21])

        """ 发射概率矩阵 从[20,8,7]([句子长度,当前批量样本句子数,标签数])变成 [8,20, 7]([当前批量样本句子数,句子长度,标签数]) """
        # 将传入的feats形状转变为[bathc_size, sequence_length, tagset_size]
        feats = feats.transpose(1, 0) #  [8, 20, 7]
        # 用于记录当前批量样本句子数中所遍历的第几个句子
        idx = 0
        """ 用于每个句子的最终得分 """
        # 初始化一个0值的tensor, 为后续累加做准备
        score = torch.zeros(1)
        # print("score",score) #tensor([0.])
        # print("score.shape",score.shape) #torch.Size([1])

        # 初始化最终的结果分数张量, 每一个句子均计算得出为一个分数
        result = torch.zeros((1, self.batch_size))
        # print("result",result) #tensor([[0., 0., 0., 0., 0., 0., 0., 0.]])
        # print("result.shape",result.shape) #torch.Size([1, 8])

        """ 
        遍历[8, 20, 7]中的每条样本句子也即[20, 7]。
        遍历发射概率矩阵中的每一个句子样本:遍历BiLSTM输出的“根据批量句子计算出来的特征数据中的”每个句子对应的特征值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子数, 句子最大长度, tag_to_id的标签数),也即 BiLSTM输出的“根据批量句子计算出来的”特征数据
        feat_line:[20, 7] 即 (句子最大长度, tag_to_id的标签数)
        """
        # 遍历所有的语句特征向量
        for feat_line in feats:
            """
            for i, feat in enumerate(feat_line) 遍历出一条样本句子中的每个字符对应的7个标签的的概率值
            i:遍历从0到19,一共20次,代表遍历一个句子中的20个字符
            feat:torch.Size([7]),即每个字符对应的7个标签的的概率值,值也即为BiLSTM输出的概率值
            """
            # 此处feat_line: [20, 7]
            # 遍历每一个时间步, 注意: 最重要的区别在于这里是在真实标签tags的指导下进行的转移矩阵和发射矩阵的累加分数求和
            # 注意: 此处区别于第三步的循环, 最重要的是这是在真实标签指导下的转移矩阵和发射矩阵的累加分数
            for i, feat in enumerate(feat_line):
                # print("i", i) # 遍历从0到19,一共20次,代表遍历一个句子中的20个字符
                # print("feat.shape",feat.shape) #torch.Size([7])
                """
                1.score:
                    score = score + transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]
                    当前循环计算的分数值为一行20个字符的总分数值。
                    循环每遍历出一个字符时:
                        1.第一项的score:之前遍历的所有字符所计算的score值的总和
                        2.第二项的transitions[tags[idx][i+1],tags[idx][i]](transitions[目标标签,起始标签):
		                	 (当前字符的)上一个字符的真实标签值(作为起始标签) 转移到 当前字符的真实标签值(作为目标标签) 的转移概率值。
		                    1.tags[idx][i](起始标签):
		                            (当前字符的)上一个字符的真实标签值。i从tags标签列表中的列索引值为0的第1列的START_TAG标签值开始遍历。
		                    2.tags[idx][i+1](目标标签):
		                            循环所遍历出来的当前字符的真实标签值。
		                            i从tags标签列表中的列索引值为1的第2列(即句子中第一个字符对应的)真实标签值开始遍历。
		        				      从转移概率矩阵中所获取的“从上一个字符的真实标签转移到当前字符的真实标签的”转移概率值。
                        3.第三项的feat[tags[idx][i+1]]:根据当前字符对应的真实标签值从发射概率矩阵中获取出当前字符对应的真实标签的发射概率值。
                
                2.转移概率矩阵transitions[tags[idx][i + 1], tags[idx][i]]:
                    从转移概率矩阵中获取的是从上一个字符的真实标签 转移到 当前字符的真实标签 的转移概率值。
                    1.transitions:形状为[7, 7]的transitions转移概率矩阵。
                    2.tags:形状为[8, 21],每行第一列的真实标签值为START_TAG标签的真实值5。
                      tags[idx][i + 1] 和 tags[idx][i]的区别:
                            因为tags从[8, 20]增加到了[8, 21],即是tags中每行的第一列增加了START_TAG标签的真实值5,
                            那么会发现发射概率矩阵仍为[8, 20, 7](只有20个字符),而tags的[8, 21]就有了21个字符,
                            也就是说tags的每行在没有增加第一列的时候,tags[idx][i]获取的真实标签值代表的正是
                            “当前循环从发射概率矩阵中遍历出来的当前字符的”真实标签值,但当tags从[8, 20]增加到了[8, 21]之后,
                            必须使用tags[idx][i+1]所获取的真实标签值代表的才是“当前循环从发射概率矩阵中遍历出来的当前字符的”真实标签值。
                    3.transitions[tags[idx][i + 1], tags[idx][i]]        
                        1.tags[idx][i + 1] 作为转移概率矩阵的行索引:
                            由于tags从[8, 20]变成[8, 21]之后,tags[idx][i + 1]在当前循环中实际是从列索引为1的列开始,
                            从tags的列索引为1的列开始所获取出的真实标签值对应的正是“当前循环从发射概率矩阵中遍历出来的当前字符的”真实标签值。
                        2.tags[idx][i] 作为转移概率矩阵的列索引:   
                            由于tags从[8, 20]变成[8, 21]之后,tags[idx][i]在当前循环中实际是从列索引为0的列开始(即从第1列的START_TAG标签值5开始),
                            那么只有tags[idx][i]才会从第1列的START_TAG标签真实值5开始遍历。
                        3.transitions[当前字符的真实标签值作为要转移到的目标行, 当前字符的上一个字符的真实标签值作为转移的起始列]
                            1.行索引(tags[idx][i + 1]):当前字符的真实标签值作为要转移到的目标行。
                              列索引(tags[idx][i]):当前字符的上一个字符的真实标签值作为转移的起始列,[i]为从START_TAG标签值第一列开始的。
                            2.因为tags从[8, 20]变成[8, 21]的关系,tags[idx][i+1]获取的实际才是当前循环所遍历字符在tags的真实标签值,
                              而tags[idx][i]获取的实际是当前循环所遍历字符的上一个字符对应的在tags的真实标签值,
                              tags[idx][i]为从第一列START_TAG标签值开始。
                            3.transitions[当前字符的真实标签值作为要转移到的目标行, 当前字符的上一个字符的真实标签值作为转移的起始列]
                              实际为从转移概率矩阵中获取的是从上一个字符的真实标签 转移到 当前字符的真实标签 的转移概率值。
                        4.第一种用法:
                            transitions[当前字符的真实标签值作为要转移到的目标行, 当前字符的上一个字符的真实标签值作为转移的起始列]
                            从转移概率矩阵中获取的是“上一个字符的真实标签转移到当前字符的真实标签的”转移概率值。
                            需要使用 transitions.data[tag_to_ix[START_TAG], :]=-10000 和 transitions.data[:, tag_to_ix[STOP_TAG]]=-10000
                            来进行转移概率矩阵的初始化。因此transitions转移概率矩阵中行索引代表了要转移到的目标行,
                            其目标行上的标签对应的值为要转移到该标签的转移概率值。
                            列索引代表了转移的起始列,其起始列上的标签作为转移的起始标签。
                        5.第二种用法:
                            transitions[当前字符的上一个字符的真实标签值作为转移的起始行, 当前字符的真实标签值作为要转移到的目标列]
                            从转移概率矩阵中获取的是“上一个字符的真实标签转移到当前字符的真实标签的”转移概率值。
                            需要使用transitions.data[:, tag_to_ix[START_TAG]]=-10000和transitions.data[tag_to_ix[STOP_TAG], :]=-10000
                            来进行转移概率矩阵的初始化。
                            因此transitions转移概率矩阵中行索引代表了转移的起始行,其起始行上的标签作为转移的起始标签。
                            列索引代表了要转移到的目标列,其目标列上的标签对应的值为要转移到该标签的转移概率值。
                            
                3.发射概率矩阵feat[tags[idx][i + 1]]:获取出当前字符对应的真实标签的发射概率值。
                    1.tags[idx]:根据idx行索引获取[8, 20]中每个句子中所有字符对应的标签值。
                    2.tags[idx][i + 1]:
                        因为tags从[8, 20]增加到了[8, 21],即是tags中每行的第一列增加了START_TAG标签的真实值5,
                        那么会发现发射概率矩阵仍为[8, 20, 7](只有20个字符),而tags的[8, 21]就有了21个字符,
                        也就是说tags的每行在没有增加第一列的时候,tags[idx][i]获取的真实标签值代表的正是
                        “当前循环从发射概率矩阵中遍历出来的当前字符的”真实标签值,但当tags从[8, 20]增加到了[8, 21]之后,
                        必须使用tags[idx][i+1]所获取的真实标签值代表的才是“当前循环从发射概率矩阵中遍历出来的当前字符的”真实标签值。
                    3.feat[tags[idx][i + 1]]:
                        当tags的每行增加了第一列之后,变成使用tags[idx][i+1]获取的真实标签值才为代表当前循环遍历出来的字符的真实标签值,
                        那么便根据当前字符的真实标签值从形状[7]的发射概率矩阵feat中取出对应的发射概率值。
               """
                score = score + self.transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]

            # print("score",score) #单个数值:例如 tensor([10.6912])
            # print("self.tag_to_ix[STOP_TAG]",self.tag_to_ix[STOP_TAG]) #6
            # print("self.transitions[self.tag_to_ix[STOP_TAG]]",self.transitions[self.tag_to_ix[STOP_TAG]])
            # print("tags[idx][-1]",tags[idx][-1]) #tensor(0)
            # print("self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]",self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]])
            # print("self.transitions",self.transitions)
            """ 
            1.例子:
                1.transitions[tag_to_ix[STOP_TAG]]:tensor([-2.0109e-01, -1.3705e-02,  1.5107e-01,  5.0857e-01, 8.0426e-01, 
                                                          -4.7377e-01, -1.0000e+04])
                  其中的最后一个值-1.0000e+04即为-10000。                                        
                2.tags[idx][-1]:tensor(0)
                3.transitions[tag_to_ix[STOP_TAG], tags[idx][-1]]:tensor(-0.2011, grad_fn=<SelectBackward>)

            2.transitions[tag_to_ix[STOP_TAG], tags[idx][-1]] 
                1.transitions[tag_to_ix[STOP_TAG]]
                    tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
                    行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
                    那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
                2.tags[idx][-1]
                    从每条样本数据中每个字符对应的的真实标签中,即取每条样本数据中最后一个字符对应的真实标签值。
                3.transitions[tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[行目标标签STOP_TAG, 列起始标签])
                     1.tag_to_ix[STOP_TAG]:
                        值为6,最终作为转移概率矩阵中的行索引值,即取转移概率矩阵中行标签名为STOP_TAG的一行7列的行向量,
                        同时行标签名STOP_TAG作为要转移到的目标标签。
                     2.tags[idx][-1]:
                        值为每个样本句子中的最后一个字符对应的标签值,最终作为转移概率矩阵中的列索引值,
                        同时该列索引值对应的列标签名作为转移的起始标签。
                     3.transitions[行目标标签STOP_TAG, 列起始标签]
                        先从转移概率矩阵中取出行标签为STOP_TAG的这一行7列的行向量,然后根据起始标签的列索引值从行向量取出某一列的转移概率值,
                        即该转移概率值代表了该样本句子中最后一个字符的标签转移到STOP_TAG标签的转移概率值。
            3.总结
                第一项的score:整一条样本句子遍历完所有20个字符之后计算出来的score值的总和
                第二项的transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[目标标签,起始标签]):
                    句子中的最后一个字符对应的真实标签值(作为起始标签) 转移到 行标签名STOP_TAG(作为目标标签) 的转移概率值。
                    1.transitions[tag_to_ix[STOP_TAG]](transitions[目标标签]):
                        行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
                        行向量中每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
                    2.tags[idx][-1](起始标签):
                        真实标签值为每个样本句子中的最后一个字符对应的真实标签值,最终作为转移概率矩阵中的列索引值,同时该列索引值对应的列标签名作为转移的起始标签。
            """
            # 遍历完当前语句所有的时间步之后, 最后添加上"STOP_TAG"的转移分数
            # 最后加上转移到STOP_TAG的分数
            score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]

            """ result:形状为(1, batch_size),存储每个句子计算出来的最终得分。每个句子计算出一个分数。 """
            # 将该条语句的最终得分添加进结果列表中
            result[0][idx] = score
            idx += 1 #用于记录当前批量样本句子数中所遍历的第几个句子
            """ 用于记录每个句子计算出来的最终得分,遍历计算下一个句子的得分之前,先清空该变量值 """
            score = torch.zeros(1)
        return result


    #---------------------------------------第五步: 维特比算法的实现------------------------------------------------------#

    """
    1.在HMM模型中的解码问题最常用的算法是维特比算法
        1.维特比算法是一个通用的解码算法,或者说是一个通用的求序列最短路径的动态规划算法,
          是基于动态规划的求序列最短路径的方法,维特比算法同样也可以应用于解决很多其他问题。
        2.维特比算法在用于解码隐藏状态序列时,实际即给定模型和观测序列,求给定观测序列条件下,
          最可能出现的对应的隐藏状态序列。维特比算法可以将HMM的状态序列作为一个整体来考虑,避免近似算法的问题。
        
    2.当前使用维特比算法用于解码问题,负责求解解码出最优路径,即推断出最优标签序列。
      动态规划要求的是在遍历(一共20个字符)每个字符依次前向计算找到最优的7个标签存储到[20, 7]形状的回溯列表,
      然后再进行反向回溯解码时从回溯列表中找出每个字符最优的一个标签,
      便是按照从最后一个字符往前的方向 根据第i个字符的最优标签的索引值找到第i-1个字符(即第i个字符的上一个字符)
      的最优标签的索引值。
    
        #1.result_best_path最终返回的形状为二维的[8, 20],包含“等于批量句子样本数8的”列表个数,
        #  每个列表中又存放“等于句子最大长度的”元素个数,最终的元素值为每个字符解码预测出来的最优标签的索引值。
        #2.result_best_path存储的是批量每个句子中每个字符解码预测出的最优标签的索引值
        result_best_path = []
        
        #遍历发射概率矩阵(形状[8, 20, 7])中每个样本句子(形状[20, 7])
        for feat_line in feats:
            #1.回溯指针:backpointers回溯列表最终返回的形状为二维的[20, 7],
            #  包含“等于句子最大长度20的”列表个数,每个列表中又存放“等于标签数7的”元素个数,
            #  每个小列表中的7个元素值代表每个字符通过前向计算得到的7个最大概率的标签索引值。
            #2.回溯指针backpointers存储的是当前句子中每个字符通过前向计算得到的7个最大概率的标签索引值。
            backpointers = []
            
            #[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
            init_vvars = torch.full((1, self.tagset_size), -10000.)
            #仅设置索引为5“START_TAG”标签的列值为0,代表只能从START_TAG标签开始
            #[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]
            init_vvars[0][self.tag_to_ix[START_TAG]] = 0
            #前向计算矩阵forward_var的初始化赋值
            #	在前向计算过程中遍历的第i个字符(time_step)时,forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量
            forward_var = init_vvars
            
            #遍历发射概率矩阵中一条样本句子(形状[20, 7])中每个字符(形状[7])对应的7个标签的发射概率值
            for feat in feat_line:
                
                #当前字符对应的回溯列表:负责存储每个字符中7个(目标)标签对应的最大概率值的起始标签的索引值
                bptrs_t = []
                
                #当前字符对应的维特比列表:负责存储每个字符中7个(目标)标签对应的最大概率值
                viterbivars_t = []
                
                #遍历发射概率矩阵中的每个字符(形状[7])对应的7个标签的发射概率值
                for next_tag in range(self.tagset_size):
                    
                    #1.forward_var(前向计算矩阵):
                    #	实质为每个字符对应的7个(目标)标签的最大转移概率值和7个标签的发射概率值的累计和。
                    #	前向计算矩阵所计算的每个当前字符的累计和的值都会传递给下一个字符作为forward_var继续进行累加和计算。
                    #	在前向计算过程中遍历的第i个字符(time_step)时,
                    #	forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。
                    #2.transitions[next_tag]:
                    #	从转移概率矩阵中取出“行索引为当前标签值的”一行7列(形状[7])的行向量。
                    #	行向量中的7个值代表7个标签转移到当前字符所遍历的当前标签(即目标标签)的转移概率值。
                    next_tag_var = forward_var + transitions[next_tag]
                    
                    #best_tag_id:
                    #	因为每个字符依次前向计算需要找到最优的7个标签,
                    #	那么此处首先需要找到每个字符所遍历的每个(目标)标签的最大概率值,
                    #	argmax目的就是从当前字符所遍历的标签作为目标标签的7个概率值中取出一个最大概率值的索引,
                    #	同时该最大概率值的索引代表了“7个作为转移的起始标签转移到当前目标标签中”最大概率值的一个起始标签。
                    best_tag_id = argmax(next_tag_var)
                    
                    #把当前最大概率值的起始标签的索引值保存到当前字符对应的回溯列表中
                    bptrs_t.append(best_tag_id)
                    
                    #根据当前最大概率值的起始标签的索引值取出该最大概率值保存到当前字符对应的维特比列表中
                    viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
                
                #forward_var = torch.cat(viterbivars_t) + feat
                #	1.forward_var:
                #		实质为每个字符对应的7个标签的转移概率值和7个标签的发射概率值的累计和。
                #		在前向计算过程中遍历的第i个字符(time_step)时,
                #		forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。
                #	2.torch.cat(viterbivars_t):变成torch.Size([7])类型。
                #	3.feat:当前字符对应的7个标签的发射概率值
                forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)	
                
                #把每个字符对应的(形状[7]的)回溯列表 存储到(形状[20, 7]的)句子对应的回溯列表
                backpointers.append(bptrs_t)
                    
            #1.执行到此处代表了句子中全部20个字符已经前向计算完毕,最终前向计算矩阵要添加“转移到STOP_TAG的”转移概率值。
            #2.forward_var:保存了“经过句子中全部20个字符前向计算的”(形状[1, 7]的)矩阵值
            #3.transitions[tag_to_ix[STOP_TAG]]
            #	tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
            #	行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
            #	那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]	
            
            #获取出当前句子对应的(形状[1, 7]的)最终概率值矩阵中的最大概率值的标签的索引值
            #该索引值代表句子中最后一个字符(第20个字符)的最优标签的索引值。
            best_tag_id = argmax(terminal_var)
    
            #best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,
            #因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。
            #此处先保存下句子中最后一个字符(第20个字符)的最优标签的索引值
            best_path = [best_tag_id]		
            
            #1.reversed翻转回溯列表即倒序排序,从最后一个字符往前遍历,即从第i个字符往第i-1个字符进行遍历。
            #2.先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
            #3.最终best_path列表保存有20个字符的最优标签的索引值加上一个START_TAG标签的索引值
            for bptrs_t in reversed(backpointers):
                #先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
                best_tag_id = bptrs_t[best_tag_id]
                #把每个字符对应的最优标签的索引值追加到best_path列表末尾
                best_path.append(best_tag_id)
    
            #best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,
            #因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。
            #pop()删除best_path列表中存储的最后一个值(START_TAG标签的索引值)
            start = best_path.pop()
            
            #assert断言:删除该值必定为START_TAG标签的索引值
            assert start == self.tag_to_ix[START_TAG]
            
            #重新把best_path列表翻转回正常的字符顺序排序
            best_path.reverse()
    """
    # 根据传入的语句特征feats, 推断出标签序列
    def _viterbi_decode(self, feats):
        # 初始化最佳路径结果的存放列表
        result_best_path = []
        #BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, 标签数)
        # 将输入张量变形为[batch_size, sequence_length, tagset_size]
        feats = feats.transpose(1, 0)

        """ 
        遍历[8, 20, 7]的发射概率矩阵中的每条样本句子也即[20, 7]。
        遍历发射概率矩阵中的每一个句子样本:遍历BiLSTM输出的“根据批量句子计算出来的特征数据中的”每个句子对应的特征值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子数, 句子最大长度, tag_to_id的标签数),也即 BiLSTM输出的“根据批量句子计算出来的”特征数据
        feat_line:[20, 7] 即 (句子最大长度, tag_to_id的标签数)
        """
        # 对批次中的每一行语句进行遍历, 每个语句产生一个最优标注序列
        for feat_line in feats:
            #回溯指针
            backpointers = []

            """ 创建形状为(1, self.tagset_size)的二维矩阵作为前向计算矩阵,其中每个元素值均为-10000。
                init_vvars = [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
            """
            # 初始化前向传播的张量, 设置START_TAG等于0, 约束合法序列只能从START_TAG开始
            init_vvars = torch.full((1, self.tagset_size), -10000.)
            """ 
            前向计算矩阵的初始化:把1行中的第6列设置为0,第6列代表START_TAG,意思就是句子一开始必须只能从START_TAG标签开始。
                把(1, self.tagset_size)的前向计算矩阵中的索引为5的元素值设置为0,索引为5对应的为“START_TAG”标签 
                init_alphas = [[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]]
            """
            # 仅仅把START_TAG赋值为0, 代表着接下来的转移只能从START_TAG开始
            init_vvars[0][self.tag_to_ix[START_TAG]] = 0
            # print("init_vvars", init_vvars) #tensor([[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]])

            # 在第i个time_step, 张量forward_var保存第i-1个time_step的viterbi维特比变量
            # 将初始化的变量赋值给forward_var, 在第i个time_step中, 张量forward_var保存的是第i-1个time_step的viterbi维特比张量
            forward_var = init_vvars

            """ 
            遍历[20, 7]的发射概率矩阵中当前一个句子样本中的每一个字符:遍历句子中的每个字符。
            feat:[7] 即 (tag_to_id的标签数)
            """
            # 依次遍历i=0, 到序列最后的每一个time_step, 每一个时间步
            for feat in feat_line:
                # print("feat",feat)
                """ bptrs_t:回溯列表专门用于存储每个字符对应的7个转移概率值最大的标签 """
                # 初始化保存当前time_step的回溯指针
                bptrs_t = []
                # 初始化保存当前time_step的viterbi维特比变量
                viterbivars_t = []

                """ 
                遍历发射概率矩阵中当前一个字符对应的7个(tagset_size个)标签的概率值(BiLSTM输出的概率值):
                    遍历字符对应的7个(tagset_size个)标签中的每个标签的发射概率值
                """
                # 遍历所有可能的转移标签
                for next_tag in range(self.tagset_size):
                    """ 
                    next_tag_var = forward_var + transitions[next_tag]

                    1.第一项forward_var:
                            循环每次遍历计算完一个字符对应的7个标签的概率值的总和都会存储到forward_var,
                            当遍历下一个字符计算其7个标签的概率值的总和时,仍会把当前字符计算出来的forward_var传给下一个字符的计算时使用,
                            也即会把上一个字符字符计算出来的前向计算矩阵forward_var传递给下一个字符来使用。

                    2.第二项transitions[next_tag]:
                            获取转移概率矩阵中一行7列的一维行向量(torch.Size([1, 7]))。
                            next_tag作为行索引,行索引上的标签代表了要转移到该目标行的目标标签。
                            next_tag行索引对应在转移概率矩阵transitions上的目标标签即为当前循环所遍历的当前字符的标签,
                            那么7列上的起始标签就相当于上一个字符的标签,一维行向量中的7个值分别代表了上一个字符的可能的7个标签各自
                            转移到当前字符的目标标签的转移概率值。
                    3.注意:
                        此处只有前向计算矩阵forward_var和转移概率矩阵中的转移概率值相加,并没有加上发射矩阵分数feat,
                        因此此处只是进行求最大概率值的下标。
                   """
                    # next_tag_var[i]保存了tag_i 在前一个time_step的viterbi维特比变量
                    # 前向传播张量forward_var加上从tag_i转移到next_tag的分数, 赋值给next_tag_var
                    # 注意: 在这里不去加发射矩阵的分数, 因为发射矩阵分数一致, 不影响求最大值下标
                    next_tag_var = forward_var + self.transitions[next_tag]
                    # print("next_tag_var.shape",next_tag_var.shape) #torch.Size([1, 7])
                    # print("next_tag_var",next_tag_var) #例如:tensor([[41.4296, 31.9482, 33.2792, 32.7001, 34.8837, -9962.9268, -9960.8936]])

                    """ 
                    调用自定的argmax函数:
                        获取出[1, 7]二维数组中第二维(列)中的最大值 和 最大值对应的索引值,但只返回最大值对应的索引值。
                        该最大值的索引值对应标签列表中的相同索引上的标签,该最大值即为该标签的该概率值。
                    next_tag_var
                        代表标签列表中的7个标签转移到当前字符的目标标签的转移概率值,
                        那么提取最大概率值的标签的索引值 代表 提取出“转移到当前字符的目标标签的概率值最大的”标签。
                   """
                    best_tag_id = argmax(next_tag_var)
                    # print("best_tag_id",best_tag_id) #例如:0
                    # print("next_tag_var[0][best_tag_id]",next_tag_var[0][best_tag_id]) #例如:tensor(41.4296)

                    """ 
                    把对应最大概率值的标签的索引值 存储到 回溯列表bptrs_t中。
                    bptrs_t:回溯列表专门用于存储每个字符对应的7个转移概率值最大的标签
                   """
                    # 将最大的标签所对应的id加入到当前time_step的回溯列表中
                    bptrs_t.append(best_tag_id)

                    """ 
                    维特比变量viterbivars_t:
                        根据最大概率值的索引值把next_tag_var中的最大概率值提取出来并存储到维特比变量viterbivars_t中。
                        维特比变量专门用于存储每个字符对应的7个标签中每个标签所计算的[1, 7]的next_tag_var中的最大概率值。
                    next_tag_var[0][best_tag_id]:根据最大概率值的索引值把next_tag_var中的最大概率值提取出来
                    view(1):tensor(单个数值) 转换为 tensor([单个数值])
                   """
                    viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

                #   [tensor([5.5494]), tensor([6.4252]), tensor([4.3440]), tensor([3.7513]), tensor([5.5284]),
                #    tensor([-9994.1152]), tensor([5.4671])]
                # print("viterbivars_t",viterbivars_t)
                #   tensor([64.3906, 62.7719, 61.9870, 62.7612, 62.1738, -9937.4932, 63.3974])
                # print("torch.cat(viterbivars_t)",torch.cat(viterbivars_t))
                # print("torch.cat(viterbivars_t).shape", torch.cat(viterbivars_t).shape) #torch.Size([7])
                # print("feat.shape", feat.shape) #torch.Size([7])

                """
                1.forward_var:
                    循环每次遍历计算完一个字符对应的7个标签的概率值的总和都会存储到forward_var,
                    当遍历下一个字符计算其7个标签的概率值的总和时,仍会把当前字符计算出来的forward_var传给下一个字符的计算时使用,
                    也即会把上一个字符字符计算出来的前向计算矩阵forward_var传递给下一个字符来使用。
                    
                2.torch.cat(viterbivars_t) + feat)
                    torch.cat(viterbivars_t):变成torch.Size([7])类型
                    feat:形状为[7],包含当前字符对应的7个标签的发射概率值,也即是这一条句子中的当前字符在发射概率矩阵中对应7个标签的发射概率值。
                """
                # 此处再将发射矩阵分数feat加上, 赋值给forward_var, 作为下一个time_step的前向传播张量
                forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
                # print("forward_var.shape",forward_var.shape) #torch.Size([1, 7])

                # 当前time_step的回溯指针添加进当前这一行样本的总体回溯指针中
                backpointers.append(bptrs_t)
                # print("len(bptrs_t)",len(bptrs_t)) #7
                # print("bptrs_t",bptrs_t) #例子:[3, 4, 3, 3, 3, 3, 2]

            """
            执行到此处表示已经计算完一条样本句子中的所有字符的前向计算矩阵forward_var,并且准备遍历下一个句子。
            此处还将需要对这条样本句子对应的前向计算矩阵forward_var加上“转移概率矩阵中负责转移到STOP_TAG标签的[1,7]的”转移概率行向量。
            
            transitions[tag_to_ix[STOP_TAG]]
                tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。
                行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。
                那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。
                
            """
            # 最后加上转移到STOP_TAG的分数
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
            # print("terminal_var.shape",terminal_var.shape) #torch.Size([1, 7])

            """ 
            调用自定的argmax函数:
                获取出[1, 7]二维数组中第二维(列)中的最大值 和 最大值对应的索引值,但只返回最大值对应的索引值。
                该最大值的索引值对应标签列表中的相同索引上的标签,该最大值即为该标签的该概率值。
           """
            best_tag_id = argmax(terminal_var)
            # print("best_tag_id",best_tag_id) # 例如:3

            # 根据回溯指针, 解码最佳路径
            # 首先把最后一步的id值加入
            best_path = [best_tag_id]
            # print("best_path",best_path)#例如:[3]

            # print("len(backpointers)",len(backpointers)) #20
            # print("len(backpointers[0])",len(backpointers[0])) #7
            # print("backpointers",backpointers) #列表中包含20个小列表,每个小列表又包含7个数值
            # reversed(backpointers):仅把backpointers中所包含的20个小列表进行倒序排列后重新存储,但每个小列表中的7个数值的顺序并不会变
            # print("reversed(backpointers)",[bptrs_t for bptrs_t in reversed(backpointers)])

            """
            reversed(backpointers):仅把backpointers中所包含的20个小列表进行倒序排列后重新存储,但每个小列表中的7个数值的顺序并不会变。
            bptrs_t:每次所遍历出来的一个包含7个数值的列表,每个数值均为“对应某标签的”索引值。
            best_tag_id = bptrs_t[best_tag_id]:
                根据第i个字符对应所得到的最优标签的索引值,获得第i-1个字符对应的最优标签的索引值。
                因为backpointers列表中顺序排列存储的20个小列表分别对应样本句子中的顺序的20个字符,
                而此处对backpointers列表中的20个小列表进行了倒序排列,所以变成对应样本句子中倒序排列的20个字符。
                根据从倒序的第i个字符“对应的包含7个标签索引值的”小列表bptrs_t中“所获取出的最优标签的”索引值best_tag_id 
                作为该倒序的第i个字符的最优标签的索引,同时根据该第i个字符对应的最优标签的索引值best_tag_id 
                作为 获取第i-1个字符(即上一个字符)“对应的包含7个标签索引值的”小列表bptrs_t中的最优标签的索引值best_tag_id,
                亦即反复循环 根据第i个字符的最优标签的索引best_tag_id 来获取 第i-1个字符(即上一个字符) 的最优标签的索引best_tag_id。
                
            """
            # 从后向前回溯最佳路径
            for bptrs_t in reversed(backpointers):
                # 通过第i个time_step得到的最佳id, 找到第i-1个time_step的最佳id
                best_tag_id = bptrs_t[best_tag_id]
                best_path.append(best_tag_id)

            # print("len(best_path)", len(best_path))  # 21
            # 将START_TAG删除
            start = best_path.pop()

            # print("start",start) #5
            # print("START_TAG",self.tag_to_ix[START_TAG]) #5

            # 确认一下最佳路径的第一个标签是START_TAG
            # if start != self.tag_to_ix["<START>"]:
            #     print(start)
            assert start == self.tag_to_ix[START_TAG]

            # 因为是从后向前进行回溯, 所以在此对列表进行逆序操作得到从前向后的真实路径
            best_path.reverse()
            # print("best_path",best_path)
            # print("len(best_path)",len(best_path)) #20

            # 将当前这一行的样本结果添加到最终的结果列表中
            result_best_path.append(best_path)

        print("result_best_path",result_best_path)
        # print("len(result_best_path)",len(result_best_path)) #8
        # print("len(result_best_path[0])",len(result_best_path[0])) #20
        return result_best_path

    #---------------------------------------第六步: 完善BiLSTM_CRF类的全部功能------------------------------------------------------#
    """
    对数似然函数
        涉及到似然函数的许多应用中,更方便的是使用似然函数的自然对数形式,即“对数似然函数”。
        求解一个函数的极大化往往需要求解该函数的关于未知参数的偏导数。
        由于对数函数是单调递增的,而且对数似然函数在极大化求解时较为方便,所以对数似然函数常用在最大似然估计及相关领域中。
    """
    # 对数似然函数的计算, 输入两个参数:数字化编码后的语句, 和真实的标签
    # 注意: 这个函数是未来真实训练中要用到的损失函数, 虚拟化的forward()
    def neg_log_likelihood(self, sentence, tags):
        """ 第二步: 文本信息张量化
                最终获得feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        """
        #函数中实现 经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵。
        #BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
        # 第一步先得到BiLSTM层的输出特征张量
        feats = self._get_lstm_features(sentence)

        # feats : [20, 8, 7] 代表一个批次有8个样本, 每个样本长度20, 每一个字符映射成7个标签
        # 每一个word映射到7个标签的概率, 发射矩阵

        """ 第三步: 计算损失函数第一项的分值forward_score
                损失函数第一项的分值forward_score:本质上是发射概率emit_score和转移概率trans_score的累加和。
                feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
                最终获得forward_score:[1, batch_size],其中第二维为批量句子中每个句子的最终计算得分。
                比如:tensor([[ 39.4420, 79.3957, 118.6056, 158.7210, 198.3160, 237.7789, 277.1398, 317.2183]])
        """
        # forward_score 代表公式推导中损失函数loss的第一项
        forward_score = self._forward_alg(feats)
        print("损失函数第一项的分值forward_score",forward_score)

        """ 第四步: 计算损失函数第二项的分值gold_score
                损失函数第二项的分值gold_score:发射概率矩阵中真实标签的发射概率值 和 转移概率矩阵中真实标签之间的转移概率值 的累加和。
                feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
                tags:即每个句子中的每个字符对应的标签值,[8, 20] 即 [批量样本句子数, 最大句子长度]
                最终获得gold_score:[1, batch_size],其中第二维为批量句子中每个句子的最终计算得分。
                比如:tensor([[-11.9251, -13.1060, -11.4474, -12.4318, -10.8670, -14.7720,  -3.8157, -18.1846]])
        """
        # gold_score 代表公式推导中损失函数loss的第二项
        gold_score = self._score_sentence(feats, tags)
        print("损失函数第二项的分值gold_score",gold_score)

        """
        对数似然函数:(在真实的训练中, 只需要最大化似然概率p(y|X)即可)
            1.损失函数第一项的分值forward_score:本质上是发射概率emit_score和转移概率trans_score的累加和。
              损失函数第二项的分值gold_score:发射概率矩阵中真实标签的发射概率值 和 转移概率矩阵中真实标签之间的转移概率值 的累加和。
            2.loss值:损失函数第一项的分值forward_score - 损失函数第二项的分值gold_score 的差值作为loss值。
            3.torch.sum():按行求和则设置dim=1,按列求和则设置dim=0。
        """
        # 按行求和, 在torch.sum()函数值中, 需要设置dim=1 ; 同理, dim=0代表按列求和
        # 注意: 在这里, 通过forward_score和gold_score的差值来作为loss, 用来梯度下降训练模型
        return torch.sum(forward_score - gold_score, dim=1)

    # 此处的forward()真实场景是用在预测部分, 训练的时候并没有用到
    # 编写正式的forward()函数, 注意应用场景是在预测的时候, 模型训练的时候并没有用到forward()函数
    def forward(self, sentence):
        """ 文本信息张量化
                最终获得feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        """
        #函数中实现 经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵。
        #BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
        # 第一步 先得到BiLSTM层的输出特征张量
        # 首先获取BiLSTM层的输出特征, 得到发射矩阵
        lstm_feats = self._get_lstm_features(sentence)

        # 通过维特比算法直接解码出最优路径
        tag_seq = self._viterbi_decode(lstm_feats)
        return tag_seq

#---------------------------------------第二步: 文本信息张量化------------------------------------------------------#

# 函数sentence_map完成中文文本信息的数字编码, 变成张量
def sentence_map(sentence_list, char_to_id, max_length):
    # 对一个批次的所有语句按照长短进行排序, 此步骤非必须
    sentence_list.sort(key=lambda c:len(c), reverse=True)
    # 定义一个最终存储结果特征向量的空列表
    sentence_map_list = []
    # 循环遍历一个批次内的所有语句
    for sentence in sentence_list:
        # 采用列表生成式完成字符到id的映射
        sentence_id_list = [char_to_id[c] for c in sentence]
        # 长度不够的部分用0填充
        padding_list = [0] * (max_length-len(sentence))
        # 将每一个语句向量扩充成相同长度的向量
        sentence_id_list.extend(padding_list)
        # 追加进最终存储结果的列表中
        sentence_map_list.append(sentence_id_list)
    # 返回一个标量类型值的张量
    return torch.tensor(sentence_map_list, dtype=torch.long)

#---------------------------------------第三步: 计算损失函数第一项的分值forward_score------------------------------------------------------#

# 若干辅助函数, 在类BiLSTM外部定义, 目的是辅助log_sum_exp()函数的计算
# 将Variable类型变量内部的真实值, 以python float类型返回
def to_scalar(var): # var是Variable, 维度是1
    """ 把 传入的torch.Size([1])的一维向量(只包含一个最大值对应的索引值) 提取出其中的 最大值对应的索引值 """
    # 返回一个python float类型的值
    return var.view(-1).data.tolist()[0]

# 获取最大值的下标
def argmax(vec):
    """ 获取出[1, 7]二维数组中第二维(列)中的最大值 和 最大值对应的索引值 """
    # 返回列的维度上的最大值下标, 此下标是一个标量float
    _, idx = torch.max(vec, 1)
    return to_scalar(idx)

"""  """
# 辅助完成损失函数中的公式计算
def log_sum_exp(vec): # vec是1 * 7, type是Variable
    """
    :param vec: [1, 7]的二维数组
    :return:
    """
    """ 最终获取出[1, 7]二维数组中第二维(列)中的最大值 """
    # 求向量中的最大值
    max_score = vec[0, argmax(vec)]
    # print(vec)            # 打印[1, 7]的二维数组
    # print(argmax(vec))    # 自动获取第二维(列)中的最大值对应的索引值
    # print(vec[0, argmax(vec)])    # vec[0, 最大值对应的索引值] 根据最大值对应的索引值 获取 最大值
    # print(max_score)    #最终获取出[1, 7]二维数组中第二维(列)中的最大值
    # print(max_score.shape) #torch.Size([]) 代表0维即单个数值

    """ 
    对单个数值(二维数组中第二维(列)中的最大值) 进行广播为 [1, 7]。
    view(1, -1):把单个数值的torch.Size([]) 转换为 [1, 1]
    expand(1, vec.size()[1]):把 [1, 1] 转换为 [1, 7]
    """
    # max_score维度是1, max_score.view(1,-1)维度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的维度1 * 7
    # 构造一个最大值的广播变量:经过expand()之后的张量, 里面所有的值都相同, 都是最大值max_score
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) # vec.size()维度是1 * 7

    """
    下面两种计算方式实际效果相同,都可以计算出相同的结果值,结果值均为单个数值:
        max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast))):为了防止数值爆炸
        torch.log(torch.sum(torch.exp(vec))):可以计算出正常值,但是有可能会出现数值爆炸,其结果值便变为inf或-inf
    """
    # a = max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
    # b = torch.log(torch.sum(torch.exp(vec)))
    # print("a",a)
    # print("b",b)
    # print(a == b)

    """ 
    实际上就是求log(sum(exp(vec))) 的结果值为的单个数值。
    vec([1, 7]二维数组):前向计算矩阵、转移概率矩阵、发射概率矩阵 三者相加的结果
    为了防止数值爆炸(防止计算出inf或-inf),才会首先对vec - vec中的最大值的广播矩阵
     """
    # 先减去最大值max_score,再求解log_sum_exp, 最终的返回值上再加上max_score,是为了防止数值爆炸, 纯粹是代码上的小技巧
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))

if __name__ == '__main__':
    #遍历每个句子
    for sentence in sentence_list:
        # 遍历句子中的每个字符
        for _char in sentence:
            # 判断只要这个字符不在字典中
            if _char not in char_to_id:
                # 新增字符到字典中:key为该新增字符,value为对应顺序索引值(当前字典的大小)
                char_to_id[_char] = len(char_to_id)

    # 将批量样本句子中的每个字符替换为在字典中对应的value值(索引值)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    # print("sentence_sequence.shape",sentence_sequence.shape) #torch.Size([8, 20]) 即 [批量句子数, 句子最大长度]
    # print("sentence_sequence:\n", sentence_sequence)

    # 创建BiLSTM双向模型+CRF模型
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

    """
    sentence_sequence:“每个元素值均为索引值的”批量句子数据,形状为[8, 20] 即 [批量句子数, 句子最大长度]。
    函数中实现 经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵。
    BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
    """
    # sentence_features = model._get_lstm_features(sentence_sequence)
    # # print("sequence_features:\n", sentence_features)
    # print("sentence_features.shape",sentence_features.shape) #torch.Size([20, 8, 7])

    # 定义优化器
    optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)

    for epoch in range(1):
        # 设置当前该次循环时的参数梯度置为0,即梯度清零
        model.zero_grad()

        # """ 第二步: 文本信息张量化
        #         最终获得feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        # """
        # #函数中实现 经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵。
        # #BiLSTM中最后的Linear线性层输出的[20, 8, 7] 即 (句子最大长度, 批量句子数, tag_to_id的标签数)
        # feats = model._get_lstm_features(sentence_sequence)
        #
        # """ 第三步: 计算损失函数第一项的分值forward_score
        #         feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        #         最终获得forward_score:[1, batch_size],其中第二维为批量句子中每个句子的最终计算得分。
        #         比如:tensor([[ 39.4420, 79.3957, 118.6056, 158.7210, 198.3160, 237.7789, 277.1398, 317.2183]])
        # """
        # forward_score = model._forward_alg(feats)
        # print("损失函数第一项的分值forward_score",forward_score)
        #
        # """ 第四步: 计算损失函数第二项的分值gold_score
        #         feats:BiLSTM中Linear层输出的[20, 8, 7]的发射概率矩阵([句子长度, 当前批量样本句子数, 标签数])
        #         tags:即每个句子中的每个字符对应的标签值,[8, 20] 即 [批量样本句子数, 最大句子长度]
        #         最终获得gold_score:[1, batch_size],其中第二维为批量句子中每个句子的最终计算得分。
        #         比如:tensor([[-11.9251, -13.1060, -11.4474, -12.4318, -10.8670, -14.7720,  -3.8157, -18.1846]])
        # """
        # gold_score = model._score_sentence(feats, tags)
        # print("损失函数第二项的分值gold_score",gold_score)
        #
        # result_tags = model._viterbi_decode(feats)
        # print("维特比算法的实现",result_tags)

        """
        模型训练 前向传播:
            sentence_sequence:“每个元素值均为索引值的”批量句子数据,形状为[8, 20] 即 [批量句子数, 句子最大长度]
            tags:即每个句子中的每个字符对应的标签值,[8, 20] 即 [批量样本句子数, 最大句子长度]
        """
        loss = model.neg_log_likelihood(sentence_sequence, tags)
        print("loss",loss)
        """ 反向传播求梯度 """
        loss.backward()
        """ 优化器 根据梯度更新权重参数 """
        optimizer.step()
        """ 模型预测(使用维特比算法直接解码出最优路径)
                调用forward函数:输入“每个元素值均为索引值的”批量句子样本数据进行预测
        """
        result = model(sentence_sequence)
        # print(result)

"""
修改版修改的位置
1.(torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2),
   torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2))
   替换成
   (torch.randn(2 * self.num_layers, self.sequence_length, self.hidden_dim // 2),
    torch.randn(2 * self.num_layers, self.sequence_length, self.hidden_dim // 2))

2.embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)
  替换成
  embeds = self.word_embeds(sentence)

3.注释掉 feats = feats.transpose(1, 0)
4.注释掉 lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)
"""

改进版

# -*- coding: utf-8 -*-#
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim

torch.manual_seed(1)
START_TAG = "<START>"
STOP_TAG = "<STOP>"


# 获取最大值的下标
def argmax(vec):
    # 返回列的维度上的最大值下标, 此下标是一个标量float
    _, idx = torch.max(vec, 1)
    return idx.item()


# 辅助完成损失函数中的公式计算
def log_sum_exp(vec):
    max_score = vec[0, argmax(vec)]
    # max_score维度是1, max_score.view(1,-1)维度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的维度1 * 7
    # 经过expand()之后的张量, 里面所有的值都相同, 都是最大值max_score
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
    # 先减去max_score,最后再加上max_score, 是为了防止数值爆炸, 纯粹是代码上的小技巧
    return max_score + \
        torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))


class BiLSTM_CRF(nn.Module):

    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
        '''
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_ix:           标签与id对照字典
        :param embedding_dim:       字嵌入维度(即LSTM输入层维度input_size)
        :param hidden_dim:          隐藏层向量维度
        '''
        super(BiLSTM_CRF, self).__init__()
        self.embedding_dim = embedding_dim
        self.hidden_dim = hidden_dim
        self.vocab_size = vocab_size
        self.tag_to_ix = tag_to_ix
        self.tagset_size = len(tag_to_ix)

        # 构建词嵌入层, 两个参数分别是单词总数, 词嵌入维度
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
        # 构建双向LSTM层, 输入参数包括词嵌入维度, 隐藏层大小, 堆叠的LSTM层数, 是否双向标志位
        self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,
                            num_layers=1, bidirectional=True)

        # 构建全连接线性层, 一端对接LSTM隐藏层, 另一端对接输出层, 相应的维度就是标签数量tagset_size
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        # 初始化转移矩阵
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))

        # 按照损失函数小节的定义, 任意的合法句子不会转移到"START_TAG", 因此设置为-10000
        # 同理, 任意合法的句子不会从"STOP_TAG"继续向下转移, 也设置为-10000
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000

        # 初始化隐藏层, 利用单独的类函数init_hidden()来完成
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 为了符合LSTM的输入要求, 我们返回h0, c0, 这两个张量的shape完全一致
        return (torch.randn(2, 1, self.hidden_dim // 2),
                torch.randn(2, 1, self.hidden_dim // 2))

    # 计算损失函数第一项的分值函数, 本质上是发射矩阵和转移矩阵的累加和
    def _forward_alg(self, feats):
        # 初始化一个alphas张量, 代表转移矩阵的起始位置
        init_alphas = torch.full((1, self.tagset_size), -10000.)
        # 仅仅把START_TAG赋值为0, 代表着接下来的转移只能从START_TAG开始
        init_alphas[0][self.tag_to_ix[START_TAG]] = 0.

        # 前向计算变量的赋值, 这样在反向求导的过程中就可以自动更新参数
        forward_var = init_alphas

        # 遍历一行语句, 每一个feat代表一个time_step
        for feat in feats:
            # 当前time_step的一个forward tensors
            alphas_t = []
            # 在当前time_step, 遍历所有可能的转移标签, 进行累加计算
            for next_tag in range(self.tagset_size):
                # 广播发射矩阵的分数
                emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)

                # 第i个time_step循环时, 转移到next_tag标签的转移概率
                trans_score = self.transitions[next_tag].view(1, -1)

                # 将前向矩阵, 转移矩阵, 发射矩阵累加
                next_tag_var = forward_var + trans_score + emit_score

                # 计算log_sum_exp()函数值
                alphas_t.append(log_sum_exp(next_tag_var).view(1))

            # 将列表张量转变为二维张量
            forward_var = torch.cat(alphas_t).view(1, -1)

        # 添加最后一步转移到"STOP_TAG"的分数, 就完成了整条语句的分数计算
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]

        # 计算log_sum_exp()函数值, 作为一条样本语句的最终得分
        alpha = log_sum_exp(terminal_var)
        return alpha

    # 在类中将文本信息经过词嵌入层, BiLSTM层, 线性层的处理, 最终输出句子张量
    def _get_lstm_features(self, sentence):
        self.hidden = self.init_hidden()

        # LSTM的输入要求形状为 [sequence_length, batch_size, embedding_dim]
        embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)

        # LSTM的两个输入参数: 词嵌入后的张量, 随机初始化的隐藏层张量
        lstm_out, self.hidden = self.lstm(embeds, self.hidden)

        # 要保证输出张量的shape: [sequence_length, hidden_dim]
        lstm_out = lstm_out.view(len(sentence), self.hidden_dim)

        # 将BiLSTM的输出经过一个全连接层
        lstm_feats = self.hidden2tag(lstm_out)
        return lstm_feats

    # 计算损失函数第二项的分值函数
    def _score_sentence(self, feats, tags):
        # 初始化一个0值的tensor, 为后续累加做准备
        score = torch.zeros(1)

        # 将START_TAG和真实标签tags做列维度上的拼接
        tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])

        # 注意: 此处最重要的是这是在真实标签指导下的转移矩阵和发射矩阵的累加分数
        for i, feat in enumerate(feats):
            score = score + self.transitions[tags[i + 1], tags[i]] + feat[tags[i + 1]]
        score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]
        return score

    # 根据传入的语句特征feats, 推断出标签序列
    def _viterbi_decode(self, feats):
        backpointers = []

        # 初始化前向传播的张量, 设置START_TAG等于0, 约束合法序列只能从START_TAG开始
        init_vvars = torch.full((1, self.tagset_size), -10000.)
        init_vvars[0][self.tag_to_ix[START_TAG]] = 0

        # 在第i个time_step, 张量forward_var保存第i-1个time_step的viterbi变量
        forward_var = init_vvars

        # 依次遍历i=0, 到序列最后的每一个time_step
        for feat in feats:
            # 保存当前time_step的回溯指针
            bptrs_t = []
            # 保存当前time_step的viterbi变量
            viterbivars_t = []

            for next_tag in range(self.tagset_size):
                # next_tag_var[i]保存了tag_i 在前一个time_step的viterbi变量
                # 前向传播张量forward_var加上从tag_i转移到next_tag的分数, 赋值给next_tag_var
                # 注意此处没有加发射矩阵分数, 因为求最大值不需要发射矩阵
                next_tag_var = forward_var + self.transitions[next_tag]

                # 将最大的标签id加入到当前time_step的回溯列表中
                best_tag_id = argmax(next_tag_var)
                bptrs_t.append(best_tag_id)
                viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

            # 此处再将发射矩阵分数feat加上, 赋值给forward_var, 作为下一个time_step的前向传播张量
            forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
            backpointers.append(bptrs_t)

        # 最后加上转移到STOP_TAG的分数
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        best_tag_id = argmax(terminal_var)
        path_score = terminal_var[0][best_tag_id]

        # 首先把最后一步的id值加入
        best_path = [best_tag_id]
        # 从后向前回溯最佳路径
        for bptrs_t in reversed(backpointers):
            best_tag_id = bptrs_t[best_tag_id]
            best_path.append(best_tag_id)

        # 将START_TAG删除
        start = best_path.pop()
        # 确认一下最佳路径中的第一个标签是START_TAG
        assert start == self.tag_to_ix[START_TAG]
        # 因为是从后向前回溯, 所以再次逆序得到总前向后的真实路径
        best_path.reverse()
        return path_score, best_path

    # 对数似然函数的计算, 输入的是数字化编码后的语句, 和真实的标签
    # 注意: 这个函数是未来真实训练中要用到的"虚拟化的forward()"
    def neg_log_likelihood(self, sentence, tags):
        # 第一步先得到BiLSTM层的输出特征张量
        feats = self._get_lstm_features(sentence)

        # forward_score 代表公式推导中损失函数loss的第一项
        forward_score = self._forward_alg(feats)

        # gold_score 代表公式推导中损失函数loss的第二项
        gold_score = self._score_sentence(feats, tags)
        return forward_score - gold_score

    # 此处的forward()真实场景是用在预测部分, 训练的时候并没有用到
    def forward(self, sentence):
        # 获取从BiLSTM层得到的发射矩阵
        lstm_feats = self._get_lstm_features(sentence)

        # 通过维特比算法直接解码最佳路径
        score, tag_seq = self._viterbi_decode(lstm_feats)
        return score, tag_seq
# -*- coding: utf-8 -*-#

import os
import torch
import json
from tqdm import tqdm
from pytorch_ner import BiLSTM_CRF

def prepare_sequence(seq, char_to_id):
    char_ids = []
    for idx, char in enumerate(seq):
        # 判断若字符不在码表对应字典中,则取 NUK 的编码(即 unknown),否则取对应的字符编码
        if char_to_id.get(char):
            char_ids.append(char_to_id[char])
        else:
            char_ids.append(char_to_id["<UNK>"])
    return torch.tensor(char_ids, dtype=torch.long)

def singel_predict(model_path,
                   content,
                   char_to_id_json_path,
                   embedding_dim,
                   hidden_dim,
                   target_type_list,
                   tag_to_id):
    """
    description:                    单句命名实体识别预测,返回实体列表
    :param model_path:              模型文件路径
    :param content:                 待预测文本
    :param char_to_id_json_path:    字符码表文件路径
    :param embedding_dim:           字向量维度
    :param hidden_dim:              BiLSTM 隐藏层向量维度
    :param target_type_list:        待匹配类型,符合条件的实体将会被提取出来
    :param tag_to_id:               标签码表对照字典,标签对应 id
    :return:                        实体列表
    """
    # 加载码表文件,转为码表字典
    char_to_id = json.load(open(char_to_id_json_path, mode="r", encoding="utf8"))
    # 加载模型
    model = BiLSTM_CRF(len(char_to_id), tag_to_id, embedding_dim, hidden_dim)
    # 加载模型字典
    model.load_state_dict(torch.load(model_path))
    # 获取需要提取的 tag 对应的 id 列表
    tag_id_dict = {v: k for k, v in tag_to_id.items() if k[2:] in target_type_list}
    # 定义返回实体列表
    entities = []
    # 初始化梯度
    with torch.no_grad():
        # 将组装的模型输入数据分批进行预测
        sentence_in = prepare_sequence(content, char_to_id)
        score, best_path_list = model(sentence_in)
        entity = None
        for char_idx, tag_id in enumerate(best_path_list):
            # 若预测结果 tag_id 属于目标字典数据 key 中
            if tag_id in tag_id_dict:
                # 取符合匹配字典id的第一个字符,即【B、I】
                tag_index = tag_id_dict[tag_id][0]
                # 计算当前字符确切的下标位置
                current_char = content[char_idx]
                # 若当前字标签起始为 B,则设置为实体开始
                if tag_index == "B":
                    entity = current_char
                # 若当前字标签起始为 I,则进行字符串追加
                elif tag_index == "I" and entity:
                    entity += current_char
            # 当实体不为空且当前标签类型为 O 时,加入实体列表
            if tag_id == tag_to_id["O"] and entity:
                # 满足当前字符为O,上一个字符为目标提取实体结尾时,将其加入实体列表
                if "、" not in entity\
                        and "~" not in entity\
                        and "。" not in entity\
                        and "”" not in entity\
                        and ":" not in entity\
                        and ":" not in entity\
                        and "," not in entity\
                        and "," not in entity\
                        and "." not in entity\
                        and ";" not in entity\
                        and ";" not in entity\
                        and "【" not in entity\
                        and "】" not in entity\
                        and "[" not in entity\
                        and "]" not in entity:
                    entities.append(entity)
                # 重置实体
                entity = None
    return set(entities)

def batch_predict(data_path,
                  model_path,
                  char_to_id_json_path,
                  embedding_dim,
                  hidden_dim,
                  target_type_list,
                  prediction_result_path,
                  tag_to_id):
    """
    description: 批量预测,查询文件目录下数据,
                 从中提取符合条件的实体并存储至新的目录【prediction_result_path】
    :param data_path:               数据文件路径
    :param model_path:              模型文件路径
    :param char_to_id_json_path:    字符码表文件路径
    :param batch_size:              训练批次大小
    :param embedding_dim:           字向量维度
    :param hidden_dim:              BiLSTM 隐藏层向量维度
    :param sentence_length:         句子长度(句子做了 padding )
    :param offset:                  设定偏移量,
                                    当字符串超出 sentence_length 时,换行时增加偏移量【经验值】
    :param target_type_list:        待匹配类型,符合条件的实体将会被提取出来
    :param prediction_result_path:  预测结果保存路径
    :param tag_to_id:               标签码表对照字典,标签对应 id
    :return:                        无返回
    """
    # 迭代路径,读取文件名
    for fn in tqdm(os.listdir(data_path)):
        # 拼装全路径
        fullpath = os.path.join(data_path, fn)
        # 定义输出结果文件
        entities_file = open(os.path.join(prediction_result_path, fn.replace("txt", "csv")),
                             mode="w",
                             encoding="utf8")
        with open(fullpath, mode="r", encoding="utf8") as f:
            # 读取文件内容
            content = f.readline()
            # 调用单个预测模型,输出为目标类型实体文本列表
            entities = singel_predict(model_path,
                                      content,
                                      char_to_id_json_path,
                                      embedding_dim,
                                      hidden_dim,
                                      target_type_list,
                                      tag_to_id)
            # 写入识别结果文件
            entities_file.write("\n".join(entities))
    print("batch_predict Finished".center(100, "-"))


if __name__ == '__main__':
    # 待识别文本
    # content = "本病是由DNA病毒的单纯疱疹病毒所致。人类单纯疱疹病毒分为两型," \
    #           "即单纯疱疹病毒Ⅰ型(HSV-Ⅰ)和单纯疱疹病毒Ⅱ型(HSV-Ⅱ)。" \
    #           "Ⅰ型主要引起生殖器以外的皮肤黏膜(口腔黏膜)和器官(脑)的感染。" \
    #           "Ⅱ型主要引起生殖器部位皮肤黏膜感染。" \
    #           "病毒经呼吸道、口腔、生殖器黏膜以及破损皮肤进入体内," \
    #           "潜居于人体正常黏膜、血液、唾液及感觉神经节细胞内。" \
    #           "当机体抵抗力下降时,如发热胃肠功能紊乱、月经、疲劳等时," \
    #           "体内潜伏的HSV被激活而发病。"
    # 模型保存路径
    model_path = "model/bilstm_crf_state_dict_20200603_172556.pt"
    # 字向量维度
    EMBEDDING_DIM = 200
    # 隐层维度
    HIDDEN_DIM = 100
    # 标签码表对照字典
    tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
    # 字符码表文件路径
    char_to_id_json_path = "char_to_id.json"
    # 预测结果存储路径
    prediction_result_path = "prediction_result"
    # 待匹配标签类型
    target_type_list = ["sym"]
    # # 单独文本预测,获得实体结果
    # entities = singel_predict(model_path,
    #                           content,
    #                           char_to_id_json_path,
    #                           EMBEDDING_DIM,
    #                           HIDDEN_DIM,
    #                           target_type_list,
    #                           tag_to_id)
    # # 打印实体结果
    # print("entities:\n", entities)
    # 待预测文本文件所在目录
    data_path = "./data/unstructed_data"
    # 批量文本预测,并将结果写入文件中
    batch_predict(data_path,
                  model_path,
                  char_to_id_json_path,
                  EMBEDDING_DIM,
                  HIDDEN_DIM,
                  target_type_list,
                  prediction_result_path,
                  tag_to_id)
# -*- coding: utf-8 -*-#
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
from pytorch_ner import BiLSTM_CRF
import train_indicator
from tqdm import tqdm
import json
import random
import time
import matplotlib.pyplot as plt


# 通过函数来准备训练序列
def prepare_sequence(seq, char_to_id):
    char_ids = []
    for idx, char in enumerate(seq):
        # 判断若字符不在码表对应字典中,则取 NUK 的编码(即 unknown),否则取对应的字符编码
        if char_to_id.get(char):
            char_ids.append(char_to_id[char])
        else:
            char_ids.append(char_to_id["UNK"])
    return torch.tensor(char_ids, dtype=torch.long)


# 读取文件中的数据
def get_train_data():
    train_data_file_path = "data/train_data.txt"
    validate_file_path = "data/validate_data.txt"
    train_data_list = []
    validate_data_list = []
    for line in open(train_data_file_path, mode="r", encoding="utf8"):
        data = json.loads(line)
        train_data_list.append(data)
    for line in open(validate_file_path, mode="r", encoding="utf8"):
        data = json.loads(line)
        validate_data_list.append(data)
    return train_data_list, validate_data_list


# 保存训练中的图片
def save_train_history_image(train_history_list,
                             validate_history_list,
                             history_image_path,
                             data_type):
    """
    description: 存储训练历史图片
    :param train_history_list:          训练历史结果
    :param validate_history_list:       验证历史结果
    :param history_image_path:          历史数据生成图像保存路径
    :param data_type:                   数据类型[用于替换label,y轴以及保存文件名中数据类型]
    :return:                            无,直接将数据转为图片进行存储
    """
    # 存储训练历史图片
    plt.plot(train_history_list, label="Train %s History" % (data_type))
    plt.plot(validate_history_list, label="Validate %s History" % (data_type))
    plt.legend(loc="best")
    plt.xlabel("Epochs")
    plt.ylabel(data_type)
    plt.savefig(history_image_path.replace("plot", data_type))
    plt.close()


# 训练模型的主函数
if __name__ == '__main__':
    # 初始化若干参数
    EMBEDDING_DIM = 200
    HIDDEN_DIM = 100
    train_data_list, validate_data_list = get_train_data()
    char_to_id_json_path = "char_to_id.json"
    char_to_id = json.load(open('char_to_id.json', mode='r', encoding='utf8'))
    tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}

    # 实例化模型对象, 实例化优化器
    model = BiLSTM_CRF(len(char_to_id), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM)
    optimizer = optim.SGD(model.parameters(), lr=0.5, momentum=0.85, weight_decay=1e-4)
    # 调转字符标签与id值
    id_to_tag = {v: k for k, v in tag_to_ix.items()}
    # 调转字符编码与id值
    id_to_char = {v: k for k, v in char_to_id.items()}
    time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
    model_saved_path = "model/bilstm_crf_state_dict_%s.pt" % (time_str)
    train_history_image_path = "log/bilstm_crf_train_plot_%s.png" % (time_str)
    log_file = open("log/train_%s.log"%(time_str), mode="w", encoding="utf8")

    # 模型训练10个轮次
    epochs = 10
    # 初始化未来的画图数据列表
    train_loss_history, train_acc_history, train_recall_history, train_f1_history = [], [], [], []
    validate_loss_history, validate_acc_history, validate_recall_history, validate_f1_history = [], [], [], []

    for epoch in range(epochs):
        tqdm.write("Epoch {}/{}".format(epoch + 1, epochs))
        mode = "train"
        total_acc_length, total_prediction_length, total_gold_length, total_loss = 0, 0, 0, 0
        # 在每一个轮次epoch中, 首先应用训练集来训练模型
        for train_data in tqdm(train_data_list):
            model.zero_grad()

            # 获取特征数据和标签, 并进行数字化封装
            sentence, tags = train_data.get("text"), train_data.get("label")
            sentence_in = prepare_sequence(sentence, char_to_id)
            targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)

            # 得到损失值并反向传播, 更新参数
            loss = model.neg_log_likelihood(sentence_in, targets)
            loss.backward()
            optimizer.step()

            # 直接调用当前模型得到最佳路径的解码结果
            score, best_path_list = model(sentence_in)
            # 累加损失值
            step_loss = loss.data.numpy()
            total_loss += step_loss
            sentence_in_unq = sentence_in.unsqueeze(0)
            targets_unq = targets.unsqueeze(0)
            best_path_list_up = [best_path_list]
            # 调用评估函数得到当前的准确率, 召回率, F1的累加值
            step_acc, step_recall,  f1_score,  acc_entities_length,  predict_entities_length, gold_entities_length = train_indicator.indicator(sentence_in_unq.tolist(), targets_unq.tolist(), best_path_list_up, id_to_char, id_to_tag)
            total_acc_length += acc_entities_length
            total_prediction_length += predict_entities_length
            total_gold_length += gold_entities_length

        print("train:", total_acc_length, total_prediction_length, total_gold_length)
        # 具体的计算平均损失值, 准确率, 召回率, F1值
        if total_prediction_length > 0:
            train_mean_loss = total_loss / len(train_data_list)
            train_epoch_acc = total_acc_length / total_prediction_length
            train_epoch_recall = total_acc_length / total_gold_length
            train_epoch_f1 = 2 * train_epoch_acc * train_epoch_recall / (train_epoch_acc + train_epoch_recall)
        else:
            log_file.write("train_total_prediction_length is zero" + "\n")

        # 训练之后, 直接在当前轮次epoch下进入验证集的验证过程
        mode = "validate"
        total_acc_length, total_prediction_length, total_gold_length, total_loss = 0, 0, 0, 0
        # 验证保持模型参数不变
        with torch.no_grad():
            for validate_data in tqdm(validate_data_list):
                # 获取验证集的特征数据和标签
                sentence, tags = validate_data.get("text"), validate_data.get("label")
                sentence_in = prepare_sequence(sentence, char_to_id)
                targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)

                # 得到损失值
                loss = model.neg_log_likelihood(sentence_in, targets)

                # 直接调用当前模型得到最佳路径的解码结果
                score, best_path_list = model(sentence_in)
                # 累加损失值
                step_loss = loss.data.numpy()
                total_loss += step_loss
                sentence_in_unq = sentence_in.unsqueeze(0)
                targets_unq = targets.unsqueeze(0)
                best_path_list_up = [best_path_list]

                # 调用评估函数得到当前的准确率, 召回率, F1的累加值
                step_acc, step_recall,  f1_score,  acc_entities_length,  predict_entities_length, gold_entities_length = train_indicator.indicator(sentence_in_unq.tolist(), targets_unq.tolist(), best_path_list_up, id_to_char, id_to_tag)
                total_acc_length += acc_entities_length
                total_prediction_length += predict_entities_length
                total_gold_length += gold_entities_length

        print("validate:", total_acc_length, total_prediction_length, total_gold_length)

        # 具体的计算平均损失值, 准确率, 召回率, F1值
        # 这里面有一个前提, 就是最少要预测正确一个才有意义
        if total_acc_length != 0 and total_prediction_length != 0:
            validate_mean_loss = total_loss / len(validate_data_list)
            validate_epoch_acc = total_acc_length / total_prediction_length
            validate_epoch_recall = total_acc_length / total_gold_length
            validate_epoch_f1 = 2 * validate_epoch_acc * validate_epoch_recall / (validate_epoch_acc + validate_epoch_recall)
            log_text = "Epoch: %s | train loss: %.5f |train acc: %.3f |train recall: %.3f |train f1 score: %.3f" \
                       " | validate loss: %.5f |validate acc: %.3f |validate recall: %.3f |validate f1 score: %.3f" % \
                       (epoch,
                        train_mean_loss, train_epoch_acc, train_epoch_recall, train_epoch_f1,
                        validate_mean_loss, validate_epoch_acc, validate_epoch_recall, validate_epoch_f1)
            log_file.write(log_text+"\n")
            train_loss_history.append(train_mean_loss)
            train_acc_history.append(train_epoch_acc)
            train_recall_history.append(train_epoch_recall)
            train_f1_history.append(train_epoch_f1)
            validate_loss_history.append(validate_mean_loss)
            validate_acc_history.append(validate_epoch_acc)
            validate_recall_history.append(validate_epoch_recall)
            validate_f1_history.append(validate_epoch_f1)
        else:
            log_file.write("validate_total_prediction_length is zero" + "\n")

    # 当10轮epochs全部完成之后, 就可以保存模型, 并开始绘图了
    # 保存模型
    torch.save(model.state_dict(), model_saved_path)

    # 将 loss 下降历史数据转为图片存储
    save_train_history_image(train_loss_history,
                             validate_loss_history,
                             train_history_image_path,
                             "Loss")
    # 将准确率提升历史数据转为图片存储
    save_train_history_image(train_acc_history,
                             validate_acc_history,
                             train_history_image_path,
                             "Acc")
    # 将召回提升历史数据转为图片存储
    save_train_history_image(train_recall_history,
                             validate_recall_history,
                             train_history_image_path,
                             "Recall")
    # 将F1上升历史数据转为图片存储
    save_train_history_image(train_f1_history,
                             validate_f1_history,
                             train_history_image_path,
                             "F1")
    print("train Finished".center(100, "-"))
    for name, parameters in model.named_parameters():
        print(name, ':', parameters.size())
# -*- coding: utf-8 -*-#
import torch


def indicator(sentence_list, gold_tags, predict_tags, id2char, id2tag):
    """
    description: 评价模型指标方法
    :param sentence_list:    句子列表
    :param gold_tags:        标签序列
    :param predict_tags:     预测标签序列
    :param id2char:          文字码表
    :param id2tag:           标签码表
    :return: step_acc,                   当前批次准确率
             step_recall,                当前批次召回率
             f1_score,                   当前批次 f1 值
             acc_entities_length,        当前批次正确识别实体总数
             predict_entities_length,    当前批次识别出的实体总数
             gold_entities_length        当前批次金标准的实体总数
    """
    # 金标准实体集合以及每个实体的字与标签列表
    gold_entities, gold_entity = [], []
    # 预测实体集合以及每个实体的字与标签列表
    predict_entities, predict_entity = [], []
    # 迭代句子列表
    for line_no, sentence in enumerate(sentence_list):
        # 迭代句子每个字符
        for char_no in range(len(sentence)):
            # 判断:若句子的id值对应的是0(即:<PAD>)则跳过循环
            if sentence[char_no]==0:
                break
            # 获取当前句子中的每一个文字
            char_text = id2char[sentence[char_no]]
            # 获取当前字金标准实体标注类型
            gold_tag_type = id2tag[gold_tags[line_no][char_no]]
            # 获取当前预测实体标注类型
            predict_tag_type = id2tag[predict_tags[line_no][char_no]]
            # 判断 id2tag 第一个字符是否为 B ,表示实体开始
            if gold_tag_type[0] == "B":
                # 将实体文字与类别加入实体列表中
                gold_entity = [char_text + "/" + gold_tag_type]
            # 判断 id2tag 第一个字符是否为 I ,表示实体中部到结尾
            # 判断依据: I 开头; entiry 不为空; 实体类别相同.
            elif gold_tag_type[0] == "I" \
                    and len(gold_entity) != 0 \
                    and gold_entity[-1].split("/")[1][1:] == gold_tag_type[1:]:
                # 加入实体列表中
                gold_entity.append(char_text + "/" + gold_tag_type)
            # 判断依据: O 开头; entiry 不为空.
            elif gold_tag_type[0] == "O" and len(gold_entity) != 0 :
                # 增加唯一标识[实体后的O的索引位置]
                gold_entity.append(str(line_no) + "_" + str(char_no))
                # 将实体文字与类别加入实体列表中
                gold_entities.append(gold_entity)
                # 重置实体列表
                gold_entity=[]
            else:
                # 重置实体列表
                gold_entity=[]


            # 判断 id2tag 第一个字符是否为 B ,表示实体开始
            if predict_tag_type[0] == "B":
                # 将实体文字与类别加入实体列表中
                predict_entity = [char_text + "/" + predict_tag_type]
            # 判断 id2tag 第一个字符是否为 I ,表示实体中部到结尾
            # 判断依据: I 开头; entiry 不为空; 实体类别相同.
            elif predict_tag_type[0] == "I" \
                    and len(predict_entity) != 0 \
                    and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:
                # 将实体文字与类别加入实体列表中
                predict_entity.append(char_text + "/" + predict_tag_type)
            # 判断依据: O 开头; entiry 不为空.
            elif predict_tag_type[0] == "O" and len(predict_entity) != 0:
                # 增加唯一标识[实体后的O的索引位置]
                predict_entity.append(str(line_no) + "_" + str(char_no))
                # 将实体加入列表中
                predict_entities.append(predict_entity)
                # 重置实体列表
                predict_entity = []
            else:
                # 重置实体列表
                predict_entity = []
    # 获取预测正确的实体集合
    acc_entities = [entity for entity in predict_entities if entity in gold_entities]
    # 预测正确实体长度[用于计算准确率\召回\F1值]
    acc_entities_length = len(acc_entities)
    # 预测出实体个数
    predict_entities_length = len(predict_entities)
    # 真实实体列表个数
    gold_entities_length = len(gold_entities)
    # 如果准确实体个数大于 0,则计算准确度\召回率\f1值
    if acc_entities_length > 0:
        # 准确率
        step_acc = float(acc_entities_length / predict_entities_length)
        # 召回率
        step_recall = float(acc_entities_length / gold_entities_length)
        # f1 值
        f1_score = 2 * step_acc * step_recall / (step_acc + step_recall)
        # 返回评估值与各个实体长度(用于整体计算)
        return step_acc, step_recall, f1_score, acc_entities_length, predict_entities_length, gold_entities_length
    else:
        # 准确率\召回率\f1值均为0
        return 0, 0, 0, acc_entities_length, predict_entities_length, gold_entities_length


def sentence_map(sentence_list, char_to_id, max_length):
    """
    description: 将句子中的每一个文字映射到码表中
    :param sentence: 待映射句子,类型为字符串或列表
    :param char_to_id: 码表,类型为字典,格式为{“字1”:1, “字2”:2}
    :return: 每一个字对应的顺序码,类型为tensor
    """
    # 字符串按照逆序进行排序
    sentence_list.sort(key=lambda c: len(c), reverse=True)
    # 定义句子映射列表
    sentence_map_list = []
    for sentence in sentence_list:
        # 生成句子中每个字对应的 id 列表
        sentence_id_list = [char_to_id[c] for c in sentence]
        # 计算所要填充 0 的长度
        padding_list = [0] * (max_length - len(sentence))
        # 组合
        sentence_id_list.extend(padding_list)
        # 将填充后的列表加入句子映射总表中
        sentence_map_list.append(sentence_id_list)
    # 返回句子映射集合,转为标量
    return torch.tensor(sentence_map_list, dtype=torch.long)


if __name__ == '__main__':
    sentence_list = [
        "确诊弥漫大b细胞淋巴瘤1年",
        "反复咳嗽、咳痰40年,再发伴气促5天。",
        "生长发育迟缓9年。",
        "右侧小细胞肺癌第三次化疗入院",
        "反复气促、心悸10年,加重伴胸痛3天。",
        "反复胸闷、心悸、气促2多月,加重3天",
        "咳嗽、胸闷1月余, 加重1周",
        "右上肢无力3年, 加重伴肌肉萎缩半年"
    ]
    # 码表与id对照
    char_to_id = {"<PAD>": 0}
    # 迭代句子集合,获取每一个句子
    for sentence in sentence_list:
        # 获取句子中的每一个字
        for _char in sentence:
            # 判断是否在码表 id 对照字典中存在
            if _char not in char_to_id:
                # 加入字符id对照字典
                char_to_id[_char] = len(char_to_id)
    # 标签码表对照
    tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}
    sentences_sequence = sentence_map(sentence_list, char_to_id, 20)

    # 真实标签数据,对应为 tag_to_id 中的 id
    tag_list = [
        [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
        [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
        [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
        [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]

    # 预测标签数据,对应为 tag_to_id 中的 id
    predict_tag_list = [
        [0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
        [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
        [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
        [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0],
        [3, 4, 0, 3, 4, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
    # 调转字符标签与id值
    id_to_tag = {v:k for k, v in tag_to_id.items()}
    # 调转字符编码与id值
    id_to_char = {v:k for k, v in char_to_id.items()}
    # 获取返回结果
    step_acc, \
    step_recall, \
    f1_score, \
    acc_entities_length, \
    predict_entities_length, \
    gold_entities_length = indicator(sentences_sequence.tolist(),
                                     tag_list,
                                     predict_tag_list,
                                     id_to_char,
                                     id_to_tag)
    # 打印输出
    print("step_acc:",                  step_acc,                   #当前批次准确率
          "\nstep_recall:",              step_recall,               #当前批次召回率
          "\nf1_score:",                 f1_score,                  #当前批次 f1 值
          "\nacc_entities_length:",      acc_entities_length,       #当前批次正确识别实体总数
          "\npredict_entities_length:",  predict_entities_length,   #当前批次识别出的实体总数
          "\ngold_entities_length:",     gold_entities_length)      #当前批次金标准的实体总数

上一篇:CRF++:一个 CRF 工具包


下一篇:自然语言处理之动手学NER