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

日萌社

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


 

# 导入包
import json
import time
from tqdm import tqdm
import matplotlib.pyplot as plt
from torch.autograd import Variable
import numpy as np
import torch.utils.data as Data
import torch
import torch.nn as nn
import torch.optim as optim

"""
此处还是使用CPU训练,比GPU运行还快
"""
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device = torch.device("cpu")
print("device",device)

"""
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)).to(device)
        # 按照损失函数小节的定义, 任意的合法句子不会转移到"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).to(device),
                torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2).to(device) )

    # 调用:
    # 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.).to(device)
        # 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)).to(device)
        # 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:
        """
        """ 用于每个句子的最终得分 """
        # 初始化一个0值的tensor, 为后续累加做准备
        score = torch.zeros(1).to(device)
        # print("score",score) #tensor([0.])
        # print("score.shape",score.shape) #torch.Size([1])
        """
        创建[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).to(device)
        temp = torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG], dtype=torch.long).clone().detach().to(device)
        # 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).to(device)
        # 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]

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

        # 用于记录当前批量样本句子数中所遍历的第几个句子
        idx = 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:
            """
            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).to(device)
        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.).to(device)
            """
            前向计算矩阵的初始化:把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).to(device)

    # 此处的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)))

"""
模型训练的流程
    第一步: 熟悉字符到数字编码的码表
    第二步: 熟悉训练数据集的样式和含义解释
    第三步: 生成批量训练数据
    第四步: 完成准确率和召回率的评估代码
    第五步: 完成训练模型的代码
    第六步: 绘制损失曲线和评估曲线图
"""

#=================================== 第三步: 生成批量训练数据 ====================================================#

# 创建生成批量训练数据的函数
def load_dataset(data_file, batch_size):
    '''
    data_file: 代表待处理的文件
    batch_size: 代表每一个批次样本的数量
    '''
    # 将train.npz文件带入到内存中
    data = np.load(data_file)

    # 分别提取data中的特征和标签
    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)

    # 将数据再次封装
    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: 是否把最后一个批次的数据(指那些无法被batch_size整除的余数数据)忽略掉
    train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,
                                   shuffle=True, num_workers=2, drop_last=False)

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

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

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

    return data_loaders, data_size

#=================================== 第四步: 完成准确率和召回率的评估代码 ====================================================#

# 评估模型的准确率, 召回率, F1等指标
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):
    '''
    sentence_list: 文本向量化后的句子张量
    true_tag: 真实的标签
    predict_tag: 预测的标签
    id2tag: id值到中文字符的映射表
    id2tag: id值到tag标签的映射表
    '''
    # 初始化真实的命名实体, 预测的命名实体, 接下来比较两者的异同来评估指标
    true_entities, true_entity = [], []
    predict_entities, predict_entity = [], []

    # 逐条的遍历批次中的所有语句
    for line_num, sentence in enumerate(sentence_list):
        # 遍历一条样本语句中的每一个字符编码(这里面都是数字化之后的编码)
        for char_num in range(len(sentence)):
            # 如果编码等于0, 表示PAD, 说明后续全部都是填充的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:
                """ 
                1.之所以要在true_tag_type[0] == "O"的基础上还要加上判断len(true_entity) != 0,
                  是因为防止循环遍历的第一个字符就是"O"并且此时true_entity仍然为空。
                2.执行到此处表示一个命名实体已经匹配结束,也即是B-dis+I-dis 或者 B-sym+I-sym 的匹配命名实体组合结束了,
                  那么就要在每个匹配的命名实体组合后面加上“行号_列号”(line_num_char_num)的标识。
                """
                true_entity.append(str(line_num) + "_" + str(char_num))
                # 将匹配结束的一个命名实体加入到最终的真实实体列表中
                true_entities.append(true_entity)
                # 清空true_entity,为了下一个命名实体的匹配做准备
                true_entity = []
            # 除了上述3种情况, 说明当前没有匹配出任何的实体, 则清空true_entity, 继续下一轮匹配
            else:
                true_entity = []

            # 对预测的标签进行命名实体的匹配
            # 如果第一个字符是"B", 表示一个实体的开始, 将"字符/标签"的格式添加进实体列表中
            if predict_tag_type[0] == "B":
                predict_entity = [char_text + "/" + predict_tag_type]
            # 如果第一个字符是"I", 表示处于一个实体的中间
            # 如果预测命名实体列表非空, 并且最后一个添加进去的标签类型和当前的标签类型一样, 则继续添加
            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:
                """ 
                1.之所以要在true_tag_type[0] == "O"的基础上还要加上判断len(true_entity) != 0,
                  是因为防止循环遍历的第一个字符就是"O"并且此时true_entity仍然为空。
                2.执行到此处表示一个命名实体已经匹配结束,也即是B-dis+I-dis 或者 B-sym+I-sym 的匹配命名实体组合结束了,
                  那么就要在每个匹配的命名实体组合后面加上“行号_列号”(line_num_char_num)的标识。
                """
                predict_entity.append(str(line_num) + "_" + str(char_num))
                # 将这个匹配结束的预测命名实体添加到最终的预测实体列表中
                predict_entities.append(predict_entity)
                # 清空predict_entity, 为下一个命名实体的匹配做准备
                predict_entity = []
            # 除了上述3种情况, 说明当前没有匹配出任何的实体, 则清空predict_entity, 继续下一轮的匹配
            else:
                predict_entity = []

    """
    因为不论是预测的命名实体组合(B-dis+I-dis 或者 B-sym+I-sym)还是真实标签的命名实体组合的后面都是加上了“行号_列号”(line_num_char_num)的标识,
    为的就是在预测的命名实体组合 和 真实标签的命名实体组合 两者进行匹配比较时,不仅要求两者对应的标签一致,
    而且还要求两者对应的标签行号和列号均一致(即保证在相同句子中的相同字符位置)。
    """
    # 遍历所有预测出来的实体列表, 只有那些在真实命名实体列表中的实体才是正确的预测
    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)

    # 至少争取预测了一个实体的情况下, 才计算准确率, 召回率, F1值
    if acc_entities_length > 0:
        accuracy = float(acc_entities_length / predict_entities_length)
        recall = float(acc_entities_length / true_entities_length)
        f1_score = 2.0 * 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

#=================================== 第五步: 完成训练模型的代码 ====================================================#

# 训练模型的函数
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("./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).to(device)

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

    # 设定优化器学习率更新策略
    # 参数说明如下:
    # 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.8)

    # 初始化存放训练中损失, 准确率, 召回率, 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).to(device), Variable(labels).to(device)
            # 在训练模型期间, 要在每个样本计算梯度前将优化器归零, 不然梯度会被累加
            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准确率
        if total_predict_entities_length > 0:
            total_acc = total_acc_entities_length / total_predict_entities_length
        # 计算当前epochs召回率
        if total_gold_entities_length > 0:
            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)
        print(log_text)
        # 当前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():
            for inputs, labels in tqdm(data_loader["validation"]):
                # 将数据以 Variable 进行封装
                inputs, labels = Variable(inputs), Variable(labels)
                # 此处调用的是 BiLSTM_CRF 类中的 neg_log_likelihood 函数
                # 返回最终的 CRF 的对数似然结果
                try:
                    loss = model.neg_log_likelihood(inputs, labels)
                except:
                    continue
                # 获取当前步的 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

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

            # 获取当前批次平均损失值(每一批次损失总值除以数据量)
            epoch_loss = total_loss / data_size["validation"]
            # 计算总批次准确率
            if total_predict_entities_length > 0:
                total_acc = total_acc_entities_length / total_predict_entities_length
            # 计算总批次召回率
            if total_gold_entities_length > 0:
                total_recall = total_acc_entities_length / total_gold_entities_length
            # 计算总批次F1值
            total_f1 = 0
            if total_acc + total_recall != 0.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)
            print(log_text)
            # 记录当前批次验证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 = "./total.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 = "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 = 300
# 参数11:隐层维度
HIDDEN_DIM = 128
# 参数12:句子长度
SENTENCE_LENGTH = 100
# 参数13:堆叠 LSTM 层数
NUM_LAYERS = 1
# 参数14:训练批次
EPOCHS = 25
# 参数15:初始化学习率
LEARNING_RATE = 0.001
# 输入参数:
# 开始字符和结束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"

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)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章