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

日萌社

人工智能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就是一个好的选择。

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

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


6.1 命名实体识别介绍

  • 学习目标:
    • 了解什么是命名实体识别
    • 了解命名实体识别的作用
    • 了解命名实体识别常用方法
    • 了解医学文本特征

  • 什么是命名实体识别:

    • 命名实体识别(Named Entity Recognition,NER)就是从一段自然语言文本中找出相关实体,并标注出其位置以及类型。是信息提取, 问答系统, 句法分析, 机器翻译等应用领域的重要基础工具, 在自然语言处理技术走向实用化的过程中占有重要地位. 包含行业, 领域专有名词, 如人名, 地名, 公司名, 机构名, 日期, 时间, 疾病名, 症状名, 手术名称, 软件名称等。具体可参看如下示例图:

  • 命名实体识别的作用:
    • 识别专有名词, 为文本结构化提供支持.
    • 主体识别, 辅助句法分析.
    • 实体关系抽取, 有利于知识推理.

  • 命名实体识别常用方法:
  • 基于规则: 针对有特殊上下文的实体, 或实体本身有很多特征的文本, 使用规则的方法简单且有效. 比如抽取文本中物品价格, 如果文本中所有商品价格都是“数字+元”的形式, 则可以通过正则表达式”\d*.?\d+元”进行抽取. 但如果待抽取文本中价格的表达方式多种多样, 例如“一千八百万”, “伍佰贰拾圆”, “2000万元”, 遇到这些情况就要修改规则来满足所有可能的情况. 随着语料数量的增加, 面对的情况也越来越复杂, 规则之间也可能发生冲突, 整个系统也可能变得不可维护. 因此基于规则的方式比较适合半结构化或比较规范的文本中的进行抽取任务, 结合业务需求能够达到一定的效果.
    • 优点: 简单, 快速.
    • 缺点: 适用性差, 维护成本高后期甚至不能维护.

  • 基于模型: 从模型的角度来看, 命名实体识别问题实际上是序列标注问题. 序列标注问题指的是模型的输入是一个序列, 包括文字, 时间等, 输出也是一个序列. 针对输入序列的每一个单元, 输出一个特定的标签. 以中文分词任务进行举例, 例如输入序列是一串文字: "我是中国人", 输出序列是一串标签: "OOBII", 其中"BIO"组成了一种中文分词的标签体系: B表示这个字是词的开始, I表示词的中间到结尾, O表示其他类型词. 因此我们可以根据输出序列"OOBII"进行解码, 得到分词结果"我\是\中国人".

    • 序列标注问题涵盖了自然语言处理中的很多任务, 包括语音识别, 中文分词, 机器翻译, 命名实体识别等, 而常见的序列标注模型包括HMM, CRF, RNN, LSTM, GRU等模型.

    • 其中在命名实体识别技术上, 目前主流的技术是通过BiLSTM+CRF模型进行序列标注, 也是项目中要用到的模型.


  • 医学文本特征:
    • 简短精炼
    • 形容词相对较少
    • 泛化性相对较小
    • 医学名词错字率比较高
    • 同义词、简称比较多

  • 小节总结:
    • 学习了什么是命名实体识别
    • 学习了命名实体识别的作用
    • 学习了命名实体识别常用方法
    • 学习了医学文本特征

6.2 BiLSTM介绍

  • 学习目标:
    • 了解BiLSTM网络结构.
    • 掌握BiLSTM模型实现.

  • BiLSTM网络结构:

    • 所谓的BiLSTM,就是(Bidirectional LSTM)双向LSTM. 单向的LSTM模型只能捕捉到从前向后传递的信息, 而双向的网络可以同时捕捉正向信息和反向信息, 使得对文本信息的利用更全面, 效果也更好.
    • 在BiLSTM网络最终的输出层后面增加了一个线性层, 用来将BiLSTM产生的隐藏层输出结果投射到具有某种表达标签特征意义的区间, 具体如下图所示:

  • BiLSTM模型实现:
    • 第一步: 实现类的初始化和网络结构的搭建.
    • 第二步: 实现文本向量化的函数.
    • 第三步: 实现网络的前向计算.

  • 第一步: 实现类的初始化和网络结构的搭建.
# 本段代码构建类BiLSTM, 完成初始化和网络结构的搭建
# 总共3层: 词嵌入层, 双向LSTM层, 全连接线性层
import torch
import torch.nn as nn


class BiLSTM(nn.Module):
    """
    description: BiLSTM 模型定义
    """
    def __init__(self, vocab_size, tag_to_id, input_feature_size, hidden_size,
                 batch_size, sentence_length, num_layers=1, batch_first=True):
        """
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_id:           标签与 id 对照
        :param input_feature_size:  字嵌入维度( 即LSTM输入层维度 input_size )
        :param hidden_size:         隐藏层向量维度
        :param batch_size:          批训练大小
        :param sentence_length      句子长度
        :param num_layers:          堆叠 LSTM 层数
        :param batch_first:         是否将batch_size放置到矩阵的第一维度
        """
        # 类继承初始化函数
        super(BiLSTM, self).__init__()
        # 设置标签与id对照
        self.tag_to_id = tag_to_id
        # 设置标签大小, 对应BiLSTM最终输出分数矩阵宽度
        self.tag_size = len(tag_to_id)
        # 设定LSTM输入特征大小, 对应词嵌入的维度大小
        self.embedding_size = input_feature_size
        # 设置隐藏层维度, 若为双向时想要得到同样大小的向量, 需要除以2
        self.hidden_size = hidden_size // 2
        # 设置批次大小, 对应每个批次的样本条数, 可以理解为输入张量的第一个维度
        self.batch_size = batch_size
        # 设定句子长度
        self.sentence_length = sentence_length
        # 设定是否将batch_size放置到矩阵的第一维度, 取值True, 或False
        self.batch_first = batch_first
        # 设置网络的LSTM层数
        self.num_layers = num_layers

        # 构建词嵌入层: 字向量, 维度为总单词数量与词嵌入维度
        # 参数: 总体字库的单词数量, 每个字被嵌入的维度
        self.embedding = nn.Embedding(vocab_size, self.embedding_size)

        # 构建双向LSTM层: BiLSTM (参数: input_size      字向量维度(即输入层大小),
        #                               hidden_size     隐藏层维度,
        #                               num_layers      层数,
        #                               bidirectional   是否为双向,
        #                               batch_first     是否批次大小在第一位)
        self.bilstm = nn.LSTM(input_size=input_feature_size,
                              hidden_size=self.hidden_size,
                              num_layers=num_layers,
                              bidirectional=True,
                              batch_first=batch_first)

        # 构建全连接线性层: 将BiLSTM的输出层进行线性变换
        self.linear = nn.Linear(hidden_size, self.tag_size)
  • 输入参数:
# 参数1:码表与id对照
char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,
              "状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}

# 参数2:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}

# 参数3:字向量维度
EMBEDDING_DIM = 200

# 参数4:隐层维度
HIDDEN_DIM = 100

# 参数5:批次大小
BATCH_SIZE = 8

# 参数6:句子长度
SENTENCE_LENGTH = 20

# 参数7:堆叠 LSTM 层数
NUM_LAYERS = 1

  • 调用:
# 初始化模型
model = BiLSTM(vocab_size=len(char_to_id),
               tag_to_id=tag_to_id,
               input_feature_size=EMBEDDING_DIM,
               hidden_size=HIDDEN_DIM,
               batch_size=BATCH_SIZE,
               sentence_length=SENTENCE_LENGTH,
               num_layers=NUM_LAYERS)
print(model)

  • 输出效果:
BiLSTM(
  (embedding): Embedding(14, 200)
  (bilstm): LSTM(200, 50, batch_first=True, bidirectional=True)
  (linear): Linear(in_features=100, out_features=5, bias=True)
)

  • 第二步:实现文本向量化的函数.
# 本函数实现将中文文本映射为数字化的张量
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)
  • 输入参数:
# 参数1:句子集合
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"]

# 参数2:码表与id对照
char_to_id = {"<PAD>":0}

# 参数3:句子长度
SENTENCE_LENGTH = 20

  • 调用:
if __name__ == '__main__':
    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)

    # 将句子转为 id 并用 tensor 包装
    sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    print("sentences_sequence:\n", sentences_sequence)

  • 输出效果:
sentences_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]])

  • 第三步: 实现网络的前向计算.
# 本函数实现类BiLSTM中的前向计算函数forward()
def forward(self, sentences_sequence):
    """
    description: 将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,
                 获得发射矩阵(emission scores)
    :param sentences_sequence: 句子序列对应的编码,
                               若设定 batch_first 为 True,
                               则批量输入的 sequence 的 shape 为(batch_size, sequence_length)
    :return:    返回当前句子特征,转化为 tag_size 的维度的特征
    """
    # 初始化隐藏状态值
    h0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
    # 初始化单元状态值
    c0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
    # 生成字向量, shape 为(batch, sequence_length, input_feature_size)
    # 注:embedding cuda 优化仅支持 SGD 、 SparseAdam
    input_features = self.embedding(sentences_sequence)

    # 将字向量与初始值(隐藏状态 h0 , 单元状态 c0 )传入 LSTM 结构中
    # 输出包含如下内容:
    # 1, 计算的输出特征,shape 为(batch, sentence_length, hidden_size)
    #    顺序为设定 batch_first 为 True 情况, 若未设定则 batch 在第二位
    # 2, 最后得到的隐藏状态 hn , shape 为(num_layers * num_directions, batch, hidden_size)
    # 3, 最后得到的单元状态 cn , shape 为(num_layers * num_directions, batch, hidden_size)
    output, (hn, cn) = self.bilstm(input_features, (h0, c0))
    # 将输出特征进行线性变换,转为 shape 为 (batch, sequence_length, tag_size) 大小的特征
    sequence_features = self.linear(output)
    # 输出线性变换为 tag 映射长度的特征
    return sequence_features
  • 输入参数:
# 参数1:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}

# 参数2:字向量维度
EMBEDDING_DIM = 200

# 参数3:隐层维度
HIDDEN_DIM = 100

# 参数4:批次大小
BATCH_SIZE = 8

# 参数5:句子长度
SENTENCE_LENGTH = 20

# 参数6:堆叠 LSTM 层数
NUM_LAYERS = 1

char_to_id = {"<PAD>":0}
SENTENCE_LENGTH = 20

  • 调用:
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(vocab_size=len(char_to_id), tag_to_id=tag_to_id, input_feature_size=EMBEDDING_DIM, \
    hidden_size=HIDDEN_DIM, batch_size=BATCH_SIZE, sentence_length=SENTENCE_LENGTH, num_layers=NUM_LAYERS)

    sentence_features = model(sentence_sequence)
    print("sequence_features:\n", sentence_features)

  • 输出效果:
sequence_features:
tensor([[[ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01],
         [ 2.9434e-02, -2.5901e-01, -2.0811e-01,  1.3794e-02, -1.8743e-01],
         [-2.7899e-02, -3.4636e-01,  1.3382e-02,  2.2684e-02, -1.2067e-01],
         [-1.9069e-01, -2.6668e-01, -5.7182e-02,  2.1566e-01,  1.1443e-01],
                                        ...
         [-1.6844e-01, -4.0699e-02,  2.6328e-02,  1.3513e-01, -2.4445e-01],
         [-7.3070e-02,  1.2032e-01,  2.2346e-01,  1.8993e-01,  8.3171e-02],
         [-1.6808e-01,  2.1454e-02,  3.2424e-01,  8.0905e-03, -1.5961e-01],
         [-1.9504e-01, -4.9296e-02,  1.7219e-01,  8.9345e-02, -1.4214e-01]],
        ...
        [[-3.4836e-03,  2.6217e-01,  1.9355e-01,  1.8084e-01, -1.6086e-01],
         [-9.1231e-02, -8.4838e-04,  1.0575e-01,  2.2864e-01,  1.6104e-02],
         [-8.7726e-02, -7.6956e-02, -7.0301e-02,  1.7199e-01, -6.5375e-02],
         [-5.9306e-02, -5.4701e-02, -9.3267e-02,  3.2478e-01, -4.0474e-02],
         [-1.1326e-01,  4.8365e-02, -1.7994e-01,  8.1722e-02,  1.8604e-01],
                                        ...
         [-5.8271e-02, -6.5781e-02,  9.9232e-02,  4.8524e-02, -8.2799e-02],
         [-6.8400e-02, -9.1515e-02,  1.1352e-01,  1.0674e-02, -8.2739e-02],
         [-9.1461e-02, -1.2304e-01,  1.2540e-01, -4.2065e-02, -8.3091e-02],
         [-1.5834e-01, -8.7316e-02,  7.0567e-02, -8.8845e-02, -7.0867e-02]],

        [[-1.4069e-01,  4.9171e-02,  1.4314e-01, -1.5284e-02, -1.4395e-01],
         [ 6.5296e-02,  9.3255e-03, -2.8411e-02,  1.5143e-01,  7.8252e-02],
         [ 4.1765e-03, -1.4635e-01, -4.9798e-02,  2.7597e-01, -1.0256e-01],
         ...
         [-3.9810e-02, -7.6746e-03,  1.2418e-01,  4.9897e-02, -8.4538e-02],
         [-3.4474e-02, -1.0586e-02,  1.3861e-01,  4.0395e-02, -8.3676e-02],
         [-3.4092e-02, -2.3208e-02,  1.6097e-01,  2.3498e-02, -8.3332e-02],
         [-4.6900e-02, -5.0335e-02,  1.8982e-01,  3.6287e-03, -7.8078e-02],
         [-6.4105e-02, -4.2628e-02,  1.8999e-01, -2.9888e-02, -1.1875e-01]]],
       grad_fn=<AddBackward0>)

  • 输出结果说明: 该输出结果为输入批次中句子的特征, 利用线性变换分别对应到每个tag的得分. 例如上述标量第一个值:[ 4.0880e-02, -5.8926e-02, -9.3971e-02, 8.4794e-03, -2.9872e-01]表示的意思为第一个句子第一个字分别被标记为["O", "B-dis", "I-dis", "B-sym", "I-sym"]的分数, 由此可以判断, 在这个例子中, 第一个字被标注为"O"的分数最高.

  • 小节总结:
    • 了解了BiLSTM网络结构
      • 设置隐藏层维度的时候, 需要将hidden_size // 2
      • 总共有3层需要构建, 分别是词嵌入层, 双向LSTM层, 全连接线性层
      • 在代码层面, 双向LSTM就是将nn.LSTM()中的参数bidirectional设置为True
    • 掌握了BiLSTM网络的代码实现
      • 构建类BiLSTM的初始化函数
      • 添加文本向量化的辅助函数, 注意padding填充为相同长度的Tensor
      • 要注意forward函数中不同张量的形状约定

6.3 CRF介绍

  • 学习目标:
    • 了解CRF的概念和作用
    • 了解转移概率矩阵
    • 了解发射概率矩阵

  • CRF的概念和作用:

    • CRF(全称Conditional Random Fields), 条件随机场. 是给定输入序列的条件下, 求解输出序列的条件概率分布模型.

    • 下面举两个应用场景的例子:

      • 场景一: 假设有一堆日常生活的给小朋友排拍的视频片段, 可能的状态有睡觉、吃饭、喝水、洗澡、刷牙、玩耍等, 大部分情况, 我们是能够识别出视频片段的状态. 但如果你只是看到一小段拿杯子的视频, 在没有前后相连的视频作为前后文参照的情况下, 我们很难知道拿杯子是要刷牙还是喝水. 这时, 可以用到CRF模型.

      • 场景二: 假设有分好词的句子, 我们要判断每个词的词性, 那么对于一些词来说, 如果我们不知道相邻词的词性的情况下, 是很难准确判断每个词的词性的. 这时, 我们也可以用到CRF.

    • 基本定义: 我们将随机变量的集合称为随机过程. 由一个空间变量索引的随机过程, 我们将其称为随机场. 上面的例子中, 做词性标注时, 可以将{名词、动词、形容词、副词}这些词性定义为随机变量, 然后从中选择相应的词性, 而这组随机变量在某种程度上遵循某种概率分布, 将这些词性按照对应的概率赋值给相应的词, 就完成了句子的词性标注.


  • 关于条件随机场与马尔科夫假设:

    • 前面课程我们介绍过马尔科夫假设, 也就是当前位置的取值只和与它相邻的位置的值有关, 和它不相邻的位置的值无关.
    • 应用到我们上面的词性标注例子中, 可以理解为当前词的词性是根据前一个词和后一个词的词性来决定的, 等效于从词性前后文的概率来给出当前词的词性判断结果.
    • 现实中可以做如下假设: 假设一个动词或者副词后面不会连接同样的动词或者副词, 这样的概率很高. 那么, 可以假定这种给定隐藏状态(也就是词性序列)的情况下, 来计算观测状态的计算过程. 本质上CRF模型考虑到了观测状态这个先验条件, 这也是条件随机场中的条件一词的含义.

  • 转移概率矩阵:
  • 首先假设我们需要标注的实体类型有一下几类:
{"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}

# 其中dis表示疾病(disease), sym表示症状(symptom), B表示命名实体开头, I表示命名实体中间到结尾, O表示其他类型.

  • 因此我们很容易知道每个字的可能标注类型有以上五种可能性, 那么在一个句子中, 由上一个字到下一个字的概率乘积就有5 × 5种可能性, 具体见下图所示:

  • 最终训练出来结果大致会如上图所示, 其中下标索引为(i, j)的方格代表如果当前字符是第i行表示的标签, 那么下一个字符表示第j列表示的标签所对应的概率值. 以第二行为例, 假设当前第i个字的标签为B-dis, 那么第i+1个字最大可能出现的概率应该是I-dis.

  • 发射概率矩阵:
  • 发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率.

  • 下面是几段医疗文本数据的标注结果:

  • 可以得到以上句子的转移矩阵概率如下:

  • 对应的发射矩阵可以理解为如下图所示结果:

  • 小节总结:
    • 学习了CRF的概念和作用
      • 概念: 条件随机场, 一种条件概率分布模型
      • 作用: 增加了先验条件, 可以更好的完成实体序列的识别
    • 学习了转移概率矩阵
    • 学习了发射概率矩阵

6.4 BiLSTM+CRF模型

  • 学习目标:
    • 掌握BiLSTM+CRF模型结构
    • 掌握损失函数的定义
    • 掌握BiLSTM_CRF模型的实现

  • BiLSTM+CRF模型结构:
    • 1, 模型的标签定义与整体架构
    • 2, 模型内部的分层展开
    • 3, CRF层的作用

  • 1, 模型的标签定义与整体架构: 假设我们的数据集中有两类实体-人名, 地名, 与之对应的在训练集中有5类标签如下所示:
B-Person, I-Person, B-Organization, I-Organization, O

# B-Person: 人名的开始
# I-Person: 人名的中间部分
# B-Organization: 地名的开始
# I-Organization: 地名的中间部分
# O: 其他非人名, 非地名的标签
  • 假设一个句子有5个单词构成, (w0, w1, w2, w3, w4), 每一个单元都代表着由字嵌入构成的向量. 其中字嵌入是随机初始化的, 词嵌入是通过数据训练得到的, 所有的嵌入在训练过程中都会调整到最优解.

  • 这些字嵌入或词嵌入作为BiLSTM+CRF模型的输入, 而输出的是句子中每个单元的标签.

2, 模型内部的分层展开: 整个模型明显有两层, 第一层是BiLSTM层, 第二层是CRF层, 将层的内部展开如下图所示:

  • BiLSTM层的输出为每一个标签的预测分值, 例如对于单词w0, BiLSTM层输出是
1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization), 0.05 (O)
  • 这些分值将作为CRF层的输入.

  • 3, CRF层的作用: 如果没有CRF层, 也可以训练一个BiLSTM命名实体识别模型, 如下图所示:

  • 由于BiLSTM的输出为单元的每一个标签分值, 我们可以挑选分值最高的一个作为该单元的标签.例如, 对于单词w0, "B-Person"的分值-1.5是所有标签得分中最高的, 因此可以挑选"B-Person"作为单词w0的预测标签. 同理, 可以得到w1 - "I-Person", w2 - "O", w3 - "B-Organization", w4 - "O"

  • 虽然按照上述方法, 在没有CRF层的条件下我们也可以得到x中每个单元的预测标签, 但是不能保证标签的预测每次都是正确的. 如果出现下图的BiLSTM层输出结果, 则明显预测是错误的.

  • CRF层能从训练数据中获得约束性的规则.

  • CRF层可以为最后预测的标签添加一些约束来保证预测的标签是合法的. 在训练数据训练的过程中, 这些约束可以通过CRF层自动学习到.

1: 句子中的第一个词总是以标签"B-"或者"O"开始, 而不是"I-"开始.
2: 标签"B-label1 I-label2 I-label3 ......", 其中的label1, label2, label3应该属于同一类实体. 
比如, "B-Person I-Person"是合法的序列, 但是"B-Person I-Organization"是非法的序列.
3: 标签序列"O I-label"是非法序列, 任意实体标签的首个标签应该是"B-", 而不是"I-".
比如, "O B-label"才是合法的序列
  • 有了上述这些约束, 标签序列的预测中非法序列出现的概率将会大大降低.

# 本段代码构建类BiLSTM, 完成初始化和网络结构的搭建
# 总共3层: 词嵌入层, 双向LSTM层, 全连接线性层
import torch
import torch.nn as nn

""" char_to_id 相当于 word2id """
# 参数1:码表与id对照
char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,
              "状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}
"""
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}
表示的意思如下:
    其中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 

比如:
    1.例子1:
        输入 char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6, "状": 7}
        那么让模型识别出:"双"为B-sym,"肺见多发斑片状"为I-sym。
    2.例子2:
        输入一段省市区地址+电话号码+名字
        那么让模型识别出:B-名字,I-名字,B-省地址,I-省地址,B-电话,I-电话,B-市地址,I-市地址,......,O-其他。
"""
# 参数2:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}
# 参数3:字向量维度
EMBEDDING_DIM = 200
# 参数4:隐层维度
HIDDEN_DIM = 100
# 参数5:批次大小
BATCH_SIZE = 8
# 参数6:句子长度
SENTENCE_LENGTH = 20
# 参数7:堆叠 LSTM 层数
NUM_LAYERS = 1

# 参数1:句子集合
sentence_list = [
    "确诊弥漫大b细胞淋巴瘤1年",
    "反复咳嗽、咳痰40年,再发伴气促5天。",
    "生长发育迟缓9年。",
    "右侧小细胞肺癌第三次化疗入院",
    "反复气促、心悸10年,加重伴胸痛3天。",
    "反复胸闷、心悸、气促2多月,加重3天",
    "咳嗽、胸闷1月余, 加重1周",
    "右上肢无力3年, 加重伴肌肉萎缩半年"]

# 参数2:码表与id对照
char_to_id = {"<PAD>":0}
# 参数3:句子长度
SENTENCE_LENGTH = 20

class BiLSTM(nn.Module):
    """
    description: BiLSTM 模型定义
    """
    def __init__(self, vocab_size, tag_to_id, input_feature_size, hidden_size,
                 batch_size, sentence_length, num_layers=1, batch_first=False):
        """
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_id:           标签与 id 对照
        :param input_feature_size:  字嵌入维度( 即LSTM输入层维度 input_size )
        :param hidden_size:         隐藏层向量维度
        :param batch_size:          批训练大小
        :param sentence_length      句子长度
        :param num_layers:          堆叠 LSTM 层数
        :param batch_first:         是否将batch_size放置到矩阵的第一维度
        """
        # 类继承初始化函数
        super(BiLSTM, self).__init__()
        # 设置标签与id对照
        self.tag_to_id = tag_to_id
        """ BiLSTM最终输出数据中的最后一个维度为len(tag_to_id),即BiLSTM中最后一层线性层Linear的输出维度为len(tag_to_id) """
        # 设置标签大小, 对应BiLSTM最终输出分数矩阵宽度
        self.tag_size = len(tag_to_id)
        # 设定LSTM输入特征大小, 对应词嵌入的维度大小
        self.embedding_size = input_feature_size
        """ 
        因为是BiLSTM双向循环,前向隐藏层占一半隐藏层维度,后向隐藏层占一半隐藏层维度,因此需要设置为hidden_size // 2。
        BiLSTM的输出层output的维度为hidden_size,即前向隐藏层的一半隐藏层维度+后向隐藏层的一半隐藏层维度。
        """
        # 设置隐藏层维度, 若为双向时想要得到同样大小的向量, 需要除以2
        self.hidden_size = hidden_size // 2
        # 设置批次大小, 对应每个批次的样本条数, 可以理解为输入张量的第一个维度
        self.batch_size = batch_size
        # 设定句子长度
        self.sentence_length = sentence_length
        # 设定是否将batch_size放置到矩阵的第一维度, 取值True, 或False
        self.batch_first = batch_first
        # 设置网络的LSTM层数
        self.num_layers = num_layers
        """ nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度) """
        # 构建词嵌入层: 字向量, 维度为总单词数量与词嵌入维度
        # 参数: 总体字库的单词数量, 每个字被嵌入的维度
        self.embedding = nn.Embedding(vocab_size, self.embedding_size)
        # 构建双向LSTM层: BiLSTM (参数: input_size      字向量维度(即输入层大小),
        #                               hidden_size     隐藏层维度,
        #                               num_layers      层数,
        #                               bidirectional   是否为双向,
        #                               batch_first     是否批次大小在第一位)
        self.bilstm = nn.LSTM(input_size=input_feature_size,
                              hidden_size=self.hidden_size,
                              num_layers=num_layers,
                              bidirectional=True,
                              batch_first=batch_first)
        """ 
        因为是BiLSTM双向循环,前向隐藏层占一半隐藏层维度,后向隐藏层占一半隐藏层维度,因此需要设置为hidden_size // 2。
        BiLSTM的输出层output的维度为hidden_size,即前向隐藏层的一半隐藏层维度+后向隐藏层的一半隐藏层维度。
        """
        # 构建全连接线性层: 将BiLSTM的输出层进行线性变换
        self.linear = nn.Linear(hidden_size, self.tag_size)

    # 本函数实现类BiLSTM中的前向计算函数forward()
    def forward(self, sentences_sequence):
        # print("sentences_sequence.shape",sentences_sequence.shape) #torch.Size([8, 20])
        """
        description: 将句子利用BiLSTM进行特征计算,分别经过 Embedding -> BiLSTM -> Linear,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
        发射概率矩阵:发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率。
        :param sentences_sequence: 句子序列对应的编码,若设定 batch_first 为 True,则批量输入的 sequence 的 shape 为(batch_size, sequence_length)
        :return: 返回当前句子特征,转化为 tag_size 的维度的特征
        """
        """
        BiLSTM(双向):
            如果RNN是双向的,num_directions为2,单向的话num_directions为1。
            不管是哪种组合,只有c0/cn 和 h0/hn的形状 在两种组合之间有区别,output.shape在两种组合之间并没有区别。
            1.第一种组合:
                    1.batch_first=False:
                        nn.LSTM(input_size=embedding_dim, #词嵌入维度
                                hidden_size=hidden_size,    #隐藏层中神经元数量
                                num_layers=num_layers,      #隐藏层层数
                                bidirectional=True,         #是否为双向
                                batch_first=False)  #Pytorch默认batch_size在第二个维度上,设置batch_first=True则batch_size在第一个维度上
                    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, embedding_dim) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)
                        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=embedding_dim,   #词嵌入维度
                                hidden_size=hidden_size,    #隐藏层中神经元数量
                                num_layers=num_layers,      #隐藏层层数
                                bidirectional=True,         #是否为双向
                                batch_first=True)  #Pytorch默认batch_size在第二个维度上,设置batch_first=True则batch_size在第一个维度上
                    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, embedding_dim) 即 (当前批量样本句子数, 句子长度, 词嵌入维度)
                        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)
 
        =================================================================================
        output, hn = rnn(input, h0)
        output, (hn, cn) = rnn(input, (h0, c0))
        
        # output of shape (seq_len, batch, num_directions * hidden_size):
        #	RNN是单向:(当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量 * 1)	
        #	RNN是双向:(当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量 * 2)	
        #             如果RNN是双向的,num_directions为2,单向的话num_directions为1
        output.shape #torch.Size([4, 3, 6])
        
        # h_n of shape (num_layers * num_directions, batch, hidden_size)
        #	RNN是单向:(隐藏层层数 * 1, 一个句子单词个数, 隐藏层中神经元数量)	
        #	RNN是双向:(隐藏层层数 * 2, 一个句子单词个数, 隐藏层中神经元数量)	
        #             如果RNN是双向的,num_directions为2,单向的话num_directions为1
        hn.shape #torch.Size([2, 3, 6])
        
        # c_n of shape (num_layers * num_directions, batch, hidden_size)
        #	RNN是单向:(隐藏层层数 * 1, 一个句子单词个数, 隐藏层中神经元数量)	
        #	RNN是双向:(隐藏层层数 * 2, 一个句子单词个数, 隐藏层中神经元数量)
        #             如果RNN是双向的,num_directions为2,单向的话num_directions为1
        """

        # # 初始化隐藏状态值
        # h0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
        # # 初始化单元状态值
        # c0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)
        # 初始化隐藏状态值
        h0 = torch.randn(self.num_layers * 2, SENTENCE_LENGTH, self.hidden_size)
        # 初始化单元状态值
        c0 = torch.randn(self.num_layers * 2, SENTENCE_LENGTH, self.hidden_size)
        """
        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的优化器
        """
        # 生成字向量, shape 为(batch, sequence_length, input_feature_size)
        # 注:embedding cuda 优化仅支持 SGD 、 SparseAdam
        input_features = self.embedding(sentences_sequence)
        # print("input_features.shape",input_features.shape) #torch.Size([8, 20, 200])

        # 将字向量与初始值(隐藏状态 h0 , 单元状态 c0 )传入 LSTM 结构中
        # 输出包含如下内容:
        # 1, 计算的输出特征,shape 为(batch, sentence_length, hidden_size)
        #    顺序为设定 batch_first 为 True 情况, 若未设定则 batch 在第二位
        # 2, 最后得到的隐藏状态 hn , shape 为(num_layers * num_directions, batch, hidden_size)
        # 3, 最后得到的单元状态 cn , shape 为(num_layers * num_directions, batch, hidden_size)
        output, (hn, cn) = self.bilstm(input_features, (h0, c0))
        # print("hn.shape",hn.shape) #torch.Size([2, 20, 50])
        # print("cn.shape",cn.shape) #torch.Size([2, 20, 50])
        # print("output.shape",output.shape) #torch.Size([8, 20, 100])

        """
        1.发射概率矩阵:
            发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率。
        2.BiLSTM经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
        3.Linear输出的特征矩阵的形状为torch.Size([8, 20, 5]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), tag_to_id的标签数)。
          比如从[8, 20, 5]中取第一个样本句子中的第一个字符对应的维度值为5的一维向量:[ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01]。
          该维度值为5的一维向量中的5个数值 代表了 第一个句子中第一个字分别被标记为["O", "B-dis", "I-dis", "B-sym", "I-sym"]的5个分数, 
          由此可以判断第一个句子中第一个字被标注为"O"的分数最高(4.0880e-02 > 8.4794e-03)。
        """
        # 将输出特征进行线性变换,转为 shape 为 (batch, sequence_length, tag_size) 大小的特征
        sequence_features = self.linear(output)
        # 输出线性变换为 tag 映射长度的特征
        return sequence_features

# 调用:
# 初始化模型
# model = BiLSTM(vocab_size=len(char_to_id),
#                tag_to_id=tag_to_id,
#                input_feature_size=EMBEDDING_DIM,
#                hidden_size=HIDDEN_DIM,
#                batch_size=BATCH_SIZE,
#                sentence_length=SENTENCE_LENGTH,
#                num_layers=NUM_LAYERS)
# print(model)


# 本函数实现将中文文本映射为数字化的张量
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__':
    #遍历每个句子
    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) #torch.Size([8, 20]) 即 [批量句子数, 句子最大长度]

    #创建BiLSTM双向模型
    model = BiLSTM(vocab_size=len(char_to_id), tag_to_id=tag_to_id, input_feature_size=EMBEDDING_DIM, hidden_size=HIDDEN_DIM, batch_size=BATCH_SIZE, sentence_length=SENTENCE_LENGTH, num_layers=NUM_LAYERS)
    print(model)

    """
    1.发射概率矩阵:
        发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率。
    2.BiLSTM经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
    3.Linear输出的特征矩阵的形状为torch.Size([8, 20, 5]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), tag_to_id的标签数)。
      比如从[8, 20, 5]中取第一个样本句子中的第一个字符对应的维度值为5的一维向量:[ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01]。
      该维度值为5的一维向量中的5个数值 代表了 第一个句子中第一个字分别被标记为["O", "B-dis", "I-dis", "B-sym", "I-sym"]的5个分数, 
      由此可以判断第一个句子中第一个字被标注为"O"的分数最高(4.0880e-02 > 8.4794e-03)。
    """
    #调用 BiLSTM中的前向计算函数forward(),实现输出特征计算矩阵作为发射概率矩阵
    sentence_features = model(sentence_sequence)
    # print("sequence_features:\n", sentence_features)
    # print("sequence_features.shape:", sentence_features.shape) #torch.Size([8, 20, 5]) 即 [批量句子数, 句子最大长度, 标签数]

"""
1.什么是命名实体识别:
    命名实体识别(Named Entity Recognition,NER)就是从一段自然语言文本中找出相关实体,并标注出其位置以及类型。
    是信息提取, 问答系统, 句法分析, 机器翻译等应用领域的重要基础工具, 在自然语言处理技术走向实用化的过程中占有重要地位. 
    包含行业, 领域专有名词, 如人名, 地名, 公司名, 机构名, 日期, 时间, 疾病名, 症状名, 手术名称, 软件名称等。
2.命名实体识别的作用:
    识别专有名词, 为文本结构化提供支持.
    主体识别, 辅助句法分析.
    实体关系抽取, 有利于知识推理.
3.命名实体识别常用方法:
    1.基于规则: 
        针对有特殊上下文的实体, 或实体本身有很多特征的文本, 使用规则的方法简单且有效. 
        比如抽取文本中物品价格, 如果文本中所有商品价格都是“数字+元”的形式, 则可以通过正则表达式”\d*.?\d+元”进行抽取. 
        但如果待抽取文本中价格的表达方式多种多样, 例如“一千八百万”, “伍佰贰拾圆”, “2000万元”, 
        遇到这些情况就要修改规则来满足所有可能的情况. 随着语料数量的增加, 面对的情况也越来越复杂, 规则之间也可能发生冲突, 
        整个系统也可能变得不可维护. 因此基于规则的方式比较适合半结构化或比较规范的文本中的进行抽取任务, 结合业务需求能够达到一定的效果.
        优点: 简单, 快速.
        缺点: 适用性差, 维护成本高后期甚至不能维护.

    2.基于模型: 
        从模型的角度来看, 命名实体识别问题实际上是序列标注问题. 序列标注问题指的是模型的输入是一个序列, 
        包括文字, 时间等, 输出也是一个序列. 针对输入序列的每一个单元, 输出一个特定的标签. 
        以中文分词任务进行举例, 例如输入序列是一串文字: "我是中国人", 输出序列是一串标签: "OOBII", 
        其中"BIO"组成了一种中文分词的标签体系: B表示这个字是词的开始, I表示词的中间到结尾, O表示其他类型词. 
        因此我们可以根据输出序列"OOBII"进行解码, 得到分词结果"我\是\中国人".
        序列标注问题涵盖了自然语言处理中的很多任务, 包括语音识别, 中文分词, 机器翻译, 命名实体识别等, 
        而常见的序列标注模型包括HMM, CRF, RNN, LSTM, GRU等模型.
        其中在命名实体识别技术上, 目前主流的技术是通过BiLSTM+CRF模型进行序列标注, 也是项目中要用到的模型.

4.BiLSTM网络结构:
    1.所谓的BiLSTM,就是(Bidirectional LSTM)双向LSTM. 
        单向的LSTM模型只能捕捉到从前向后传递的信息, 而双向的网络可以同时捕捉正向信息和反向信息, 
        使得对文本信息的利用更全面, 效果也更好.
    2.在BiLSTM网络最终的输出层后面增加了一个线性层, 用来将BiLSTM产生的隐藏层输出结果投射到具有某种表达标签特征意义的区间。
    
5.CRF的概念和作用:
    1.CRF(全称Conditional Random Fields), 条件随机场. 是给定输入序列的条件下, 求解输出序列的条件概率分布模型.
    2.下面举两个应用场景的例子:
        1.场景一: 
            假设有一堆日常生活的给小朋友排拍的视频片段, 可能的状态有睡觉、吃饭、喝水、洗澡、刷牙、玩耍等, 大部分情况, 
            我们是能够识别出视频片段的状态. 但如果你只是看到一小段拿杯子的视频, 在没有前后相连的视频作为前后文参照的情况下, 
            我们很难知道拿杯子是要刷牙还是喝水. 这时, 可以用到CRF模型.
        2.场景二: 
            假设有分好词的句子, 我们要判断每个词的词性, 那么对于一些词来说, 如果我们不知道相邻词的词性的情况下, 
            是很难准确判断每个词的词性的. 这时, 我们也可以用到CRF.
    3.基本定义: 
        我们将随机变量的集合称为随机过程. 由一个空间变量索引的随机过程, 我们将其称为随机场. 
        上面的例子中, 做词性标注时, 可以将{名词、动词、形容词、副词}这些词性定义为随机变量, 然后从中选择相应的词性, 
        而这组随机变量在某种程度上遵循某种概率分布, 将这些词性按照对应的概率赋值给相应的词, 就完成了句子的词性标注.
        
6.关于条件随机场与马尔科夫假设:
    1.马尔科夫假设:也就是当前位置的取值只和与它相邻的位置的值有关, 和它不相邻的位置的值无关.
    2.马尔科夫假设应用到我们上面的词性标注例子中, 可以理解为当前词的词性是根据前一个词和后一个词的词性来决定的, 
      等效于从词性前后文的概率来给出当前词的词性判断结果.
    3.现实中可以做如下假设: 
        假设一个动词或者副词后面不会连接同样的动词或者副词, 这样的概率很高. 
        那么, 可以假定这种给定隐藏状态(也就是词性序列)的情况下, 来计算观测状态的计算过程. 
        本质上CRF模型考虑到了观测状态这个先验条件, 这也是条件随机场中的条件一词的含义.
        
7.发射概率矩阵:
    1.发射概率, 是指已知当前标签的情况下, 对应所出现字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率。
    2.BiLSTM经过Embedding->BiLSTM->Linear进行特征计算后输出的特征矩阵,并且根据Linear输出的特征矩阵计算得出发射概率矩阵(emission scores)。
    3.Linear输出的特征矩阵的形状为torch.Size([8, 20, 5]) 即 (当前批量样本句子数, 当前样本的序列长度(单词个数), tag_to_id的标签数)。
      比如从[8, 20, 5]中取第一个样本句子中的第一个字符对应的维度值为5的一维向量:[ 4.0880e-02, -5.8926e-02, -9.3971e-02,  8.4794e-03, -2.9872e-01]。
      该维度值为5的一维向量中的5个数值 代表了 第一个句子中第一个字分别被标记为["O", "B-dis", "I-dis", "B-sym", "I-sym"]的5个分数, 
      由此可以判断第一个句子中第一个字被标注为"O"的分数最高(4.0880e-02 > 8.4794e-03)。

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


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]即为当前字符的标签转移到下一个字符的标签的概率值。

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]即为当前字符的标签转移到下一个字符的标签的概率值。



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

损失函数第一项的分值:本质上是发射概率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

	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作为的”目标标签的 转移概率值。

转移概率矩阵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个标签各自转移到当前字符的目标标签的转移概率值。

		#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)

发射概率矩阵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]]],
       			)

		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)


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

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

#遍历当前句子中的每个字符,也即遍历[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]]


维特比算法

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

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

#最终计算完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()

"""
损失函数的定义:
    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)
"""
# 导入包
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)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

あずにゃん

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

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

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

打赏作者

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

抵扣说明:

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

余额充值