命名實體識別任務:BiLSTM+CRF part3

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)


智能對話系統:Unit對話API

在線聊天的總體架構與工具介紹:Flask web、Redis、Gunicorn服務組件、Supervisor服務監控器、Neo4j圖數據庫

linux 安裝 neo4jlinux 安裝 Redissupervisor 安裝

neo4j圖數據庫:Cypher

neo4j圖數據庫:結構化數據流水線、非結構化數據流水線

命名實體審覈任務:BERT中文預訓練模型

命名實體審覈任務:構建RNN模型

命名實體審覈任務:模型訓練

命名實體識別任務:BiLSTM+CRF part1

命名實體識別任務:BiLSTM+CRF part2

命名實體識別任務:BiLSTM+CRF part3

在線部分:werobot服務、主要邏輯服務、句子相關模型服務、BERT中文預訓練模型+微調模型(目的:比較兩句話text1和text2之間是否有關聯)、模型在Flask部署

系統聯調測試與部署

離線部分+在線部分:命名實體審覈任務RNN模型、命名實體識別任務BiLSTM+CRF模型、BERT中文預訓練+微調模型、werobot服務+flask


 

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

"""
此處還是使用CPU訓練,比GPU運行還快
"""
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device = torch.device("cpu")
print("device",device)

"""
BiLSTM+CRF模型的實現:
        第一步: 構建神經網絡
        第二步: 文本信息張量化
        第三步: 計算損失函數第一項的分值
        第四步: 計算損失函數第二項的分值
        第五步: 維特比算法的實現
        第六步: 完善BiLSTM_CRF類的全部功能
"""

# ---------------------------------------第一步: 構建神經網絡------------------------------------------------------#
class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, num_layers, batch_size, sequence_length):
        '''
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_ix:           標籤與id對照字典
        :param embedding_dim:       字嵌入維度(即LSTM輸入層維度input_size)
        :param hidden_dim:          隱藏層向量維度
        :param num_layers:          神經網絡的層數
        :param batch_size:          批次的數量
        :param sequence_length:     語句的限制最大長度
        '''
        # 繼承函數的初始化
        super(BiLSTM_CRF, self).__init__()
        # 設置標籤與id對照(標籤到id的映射字典)
        self.tag_to_ix = tag_to_ix
        # 設置標籤的總數,對應 BiLSTM 最終輸出分數矩陣寬度
        self.tagset_size = len(tag_to_ix)
        # 設定 LSTM 輸入特徵大小(詞嵌入的維度)
        self.embedding_dim = embedding_dim
        # 設置隱藏層維度
        self.hidden_dim = hidden_dim
        # 設置單詞總數的大小/單詞的總數量
        self.vocab_size = vocab_size
        # 設置隱藏層的數量
        self.num_layers = num_layers
        # 設置語句的最大限制長度
        self.sequence_length = sequence_length
        # 設置批次的大小
        self.batch_size = batch_size
        """
        nn.Embedding(vocab_size 詞彙總數, embed_dim 單詞嵌入維度)
        注:embedding cuda 優化僅支持 SGD 、 SparseAdam
        """
        # 構建詞嵌入層, 兩個參數分別是單詞總數, 詞嵌入維度
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
        """
        因爲是BiLSTM雙向循環,前向隱藏層佔一半隱藏層維度,後向隱藏層佔一半隱藏層維度,因此需要設置爲hidden_size // 2。
        BiLSTM的輸出層output的維度爲hidden_size,即前向隱藏層的一半隱藏層維度+後向隱藏層的一半隱藏層維度。
        """
        # 構建雙向LSTM層: BiLSTM (參數: input_size      字向量維度(即輸入層大小/詞嵌入維度),
        #                               hidden_size     隱藏層維度,
        #                               num_layers      層數,
        #                               bidirectional   是否爲雙向,
        #                               batch_first     是否批次大小在第一位)
        # 構建雙向LSTM層, 輸入參數包括詞嵌入維度, 隱藏層大小, 堆疊的LSTM層數, 是否雙向標誌位
        self.lstm = nn.LSTM(embedding_dim,  # 詞嵌入維度
                            hidden_dim // 2,  # 若爲雙向時想要得到同樣大小的向量, 需要除以2
                            num_layers=self.num_layers,
                            bidirectional=True)
        """
        1.BiLSTM經過Embedding->BiLSTM->Linear進行特徵計算後輸出的特徵矩陣,並且根據Linear輸出的特徵矩陣計算得出發射概率矩陣(emission scores)。
        2.Linear 可以把 (當前批量樣本句子數, 當前樣本的序列長度(單詞個數), 隱藏層中神經元數數量) 轉換爲 (當前批量樣本句子數, 當前樣本的序列長度(單詞個數), tag_to_id的標籤數)
          Linear 也可以把 (當前樣本的序列長度(單詞個數), 當前批量樣本句子數, 隱藏層中神經元數數量) 轉換爲 (當前樣本的序列長度(單詞個數), 當前批量樣本句子數, tag_to_id的標籤數)
        """
        # 構建全連接線性層, 一端對接BiLSTM隱藏層, 另一端對接輸出層, 輸出層維度就是標籤數量tagset_size
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        """
        1.transitions轉移矩陣 是一個方陣[tagset_size, tagset_size]。
          tag_to_ix[START_TAG]值爲5,tag_to_ix[STOP_TAG]值爲6,不管是行數還是列數都從0開始統計。
          transitions轉移矩陣中行名爲當前字符的標籤,列名爲下一個字符的標籤,那麼列值便是下一個字符出現該標籤的概率值,
          需要計算出列值中下一個字符出現某標籤的最大概率值。

        2.transitions轉移矩陣的 第一種寫法(項目中使用該寫法)
            假設BiLSTM的輸出矩陣是P,維度爲tag_size, 其中P(i,j)代表單詞w_i映射到tag_j的非歸一化概率,
            也就是每個單詞w_i映射到標籤tag的發射概率值。
            那麼對於CRF層, 假設存在一個轉移矩陣A, 其中A(i,j)代表tag_j轉移到tag_i的概率,tag_j代表當前字符的標籤,
            tag_i代表當前字符的下一個字符的標籤,那麼A(i,j)也即爲當前字符的標籤tag_j轉移到下一個字符的標籤tag_i的概率值。

            1.transitions.data[tag_to_ix[START_TAG], :]:
                第5行的所有列都設置爲-10000,那麼所有字符的下一個字符出現“START_TAG”標籤的概率值均爲-10000,
                即保證語義合法的句子中任何字符的下一個字符的標籤都不會是“START_TAG”。
            2.transitions.data[:, tag_to_ix[STOP_TAG]]
                所有行的第5列都設置爲-10000,那麼“標籤爲STOP_TAG的”當前字符它的下一個字符出現任何標籤的的概率值均爲-10000,
                即保證語義合法的句子中“標籤爲STOP_TAG”的字符後面不會再有任何字符。
            3.transitions[i,j]:
                其中下標索引爲[i,j]的方格代表當前字符的標籤爲第j列的列名, 那麼下一個字符的標籤爲第i行的行名,
                那麼transitions[i,j]即爲當前字符的標籤轉移到下一個字符的標籤的概率值。
        3.transitions轉移矩陣的 第二種寫法
            假設BiLSTM的輸出矩陣是P,維度爲tag_size, 其中P(i,j)代表單詞w_i映射到tag_j的非歸一化概率,
            也就是每個單詞w_i映射到標籤tag的發射概率值。
            那麼對於CRF層, 假設存在一個轉移矩陣A, 其中A(i,j)代表tag_i轉移到tag_j的概率,tag_i代表當前字符的標籤,
            tag_j代表當前字符的下一個字符的標籤,那麼A(i,j)也即爲當前字符的標籤tag_i轉移到下一個字符的標籤tag_j的概率值。

            1.transitions.data[:, tag_to_ix[START_TAG]]=-10000:
                所有行的第5列都設置爲-10000,那麼所有字符的下一個字符出現“START_TAG”標籤的概率值均爲-10000,
                即保證語義合法的句子中任何字符的下一個字符的標籤都不會是“START_TAG”。
            2.transitions.data[tag_to_ix[STOP_TAG], :]=-10000:
                第5行的所有列都設置爲-10000,那麼“標籤爲STOP_TAG的”當前字符它的下一個字符出現任何標籤的的概率值均爲-10000,
                即保證語義合法的句子中“標籤爲STOP_TAG”的字符後面不會再有任何字符。
            3.transitions[i,j]:
                其中下標索引爲[i,j]的方格代表當前字符的標籤爲第i行的行名, 那麼下一個字符的標籤爲第j列的列名,
                那麼transitions[i,j]即爲當前字符的標籤轉移到下一個字符的標籤的概率值。
       """
        # 初始化轉移矩陣, 轉移矩陣是一個方陣[tagset_size, tagset_size]
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size)).to(device)
        # 按照損失函數小節的定義, 任意的合法句子不會轉移到"START_TAG", 因此設置爲-10000
        # 同理, 任意合法的句子不會從"STOP_TAG"繼續向下轉移, 也設置爲-10000
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000
        # 初始化隱藏層, 利用單獨的類函數init_hidden()來完成
        self.hidden = self.init_hidden()

    """
    BiLSTM(雙向):
        如果RNN是雙向的,num_directions爲2,單向的話num_directions爲1。
        不管是哪種組合,只有c0/cn 和 h0/hn的形狀 在兩種組合之間有區別,output.shape在兩種組合之間並沒有區別。
        1.第一種組合:
                1.batch_first=False:
                    nn.LSTM(input_size=input_feature_size, #詞嵌入維度
                            hidden_size=hidden_size,    #隱藏層中神經元數量
                            num_layers=num_layers,      #隱藏層層數
                            bidirectional=True,         #是否爲雙向
                            batch_first=False)
                2.c0/cn 和 h0/hn 均爲
                        torch.randn(num_layers * num_directions, sequence_length, hidden_size // 2)
                        即 (隱藏層層數 * 2, 一個句子單詞個數, 隱藏層中神經元數量 // 2)
                        如果RNN是雙向的,num_directions爲2,單向的話num_directions爲1。
                3.output, (hn, cn) = bilstm(input, (h0, c0))
                    input.shape:(BATCH_SIZE, sequence_length, input_feature_size) 即 (當前批量樣本句子數, 句子長度, 詞嵌入維度)
                    hn.shape:torch.Size([2, 20, 50]) 即 (隱藏層層數 * 2, 一個句子單詞個數, 隱藏層中神經元數量 // 2)
                    cn.shape:torch.Size([2, 20, 50]) 即 (隱藏層層數 * 2, 一個句子單詞個數, 隱藏層中神經元數量 // 2)
                    output.shape:torch.Size([8, 20, 100]) 即 (當前批量樣本句子數, 當前樣本的序列長度(單詞個數), 隱藏層中神經元數量 * 2)
        2.第二種組合:
                1.batch_first=True
                    nn.LSTM(input_size=input_feature_size, #詞嵌入維度
                            hidden_size=hidden_size,    #隱藏層中神經元數量
                            num_layers=num_layers,      #隱藏層層數
                            bidirectional=True,         #是否爲雙向
                            batch_first=True)
                2.c0/cn 和 h0/hn 均爲
                        torch.randn(num_layers * num_directions, batch_size, hidden_size // 2)
                        即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
                        如果RNN是雙向的,num_directions爲2,單向的話num_directions爲1。
                3.output, (hn, cn) = bilstm(input, (h0, c0))
                    input.shape:(BATCH_SIZE, sequence_length, input_feature_size) 即 (當前批量樣本句子數, 句子長度, 詞嵌入維度)
                    hn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
                    cn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
                    output.shape torch.Size([8, 20, 100]) 即 (當前批量樣本句子數, 當前樣本的序列長度(單詞個數), 隱藏層中神經元數量 * 2)
    """

    # 定義類內部專門用於初始化隱藏層的函數
    def init_hidden(self):
        """
         hn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
         cn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
        """
        # 爲了符合LSTM的輸入要求, 我們返回h0, c0, 這兩個張量的shape完全一致
        # 需要注意的是shape: [2 * num_layers, batch_size, hidden_dim // 2]
        return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2).to(device),
                torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2).to(device) )

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

    # ---------------------------------------第二步: 文本信息張量化------------------------------------------------------#
    """
    BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數]):
        每個字符對應一個包含7個數值的一維向量,7個數值對應7標籤(["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]),
        那麼每個數值便代表了該字符被標註爲該標籤的概率值
    """

    # 在類中將文本信息經過詞嵌入層, BiLSTM層, 線性層的處理, 最終輸出句子張量
    def _get_lstm_features(self, sentence):
        """
        :param sentence: “每個元素值均爲索引值的”批量句子數據,形狀爲[8, 20] 即 [批量句子數, 句子最大長度]
        :return:BiLSTM中最後的Linear線性層輸出的(句子最大長度, 批量句子數, tag_to_id的標籤數)
        """
        # 返回的hidden爲(hn,cn),hn和cn均爲 torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
        self.hidden = self.init_hidden()
        """
        1.embedding輸入形狀和輸出形狀:(BATCH_SIZE行 sequence_length列,批量大小句子數爲BATCH_SIZE,sequence_length爲句子長度)
            embedding輸入:(BATCH_SIZE, sequence_length) 即 (當前批量樣本句子數, 句子長度)
            embedding輸出:(BATCH_SIZE, sequence_length, embedding_dim) 即 (當前批量樣本句子數, 句子長度, 詞嵌入維度)
        2.embedding 使用cuda(gpu)進行運行優化時 僅支持 SGD、SparseAdam的優化器
        """
        # a = self.word_embeds(sentence)
        # print(a.shape)  # torch.Size([8, 20, 200]) 即 (當前批量樣本句子數, 句子長度, 詞嵌入維度)

        """
        通過 view(self.sequence_length, self.batch_size, -1) 把 [8, 20, 200] 轉換爲 [20, 8, 200]。
        即 (當前批量樣本句子數, 句子長度, 詞嵌入維度) 轉換爲 (句子長度, 當前批量樣本句子數, 詞嵌入維度)。
        """
        # LSTM的輸入要求形狀爲 [sequence_length, batch_size, embedding_dim]
        # LSTM的隱藏層h0要求形狀爲 [num_layers * direction, batch_size, hidden_dim]
        # 讓sentence經歷詞嵌入層
        embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)
        # print("embeds.shape",embeds.shape) #torch.Size([20, 8, 200]) 即 (句子長度, 當前批量樣本句子數, 詞嵌入維度)

        """
        1.output, (hn, cn) = bilstm(input, (h0, c0))
            input.shape(embeds.shape):(sequence_length, BATCH_SIZE, embedding_dim) 即 (句子長度, 當前批量樣本句子數, 詞嵌入維度)
            hn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
            cn.shape torch.Size([2, 8, 50]) 即 (隱藏層層數 * 2, 當前批量樣本句子數, 隱藏層中神經元數量 // 2)
        2.因爲輸入BiLSTM層的數據爲[20, 8, 200](句子長度, 當前批量樣本句子數, 詞嵌入維度),
          因此BiLSTM層輸出的也爲[20, 8, 200],最後通過線性層輸出[20, 8, 100]。
        """
        # 將詞嵌入層的輸出, 進入BiLSTM層, LSTM的兩個輸入參數: 詞嵌入後的張量, 隨機初始化的隱藏層張量
        lstm_out, self.hidden = self.lstm(embeds, self.hidden)
        # print("lstm_out",lstm_out.shape) #torch.Size([20, 8, 100]) 即 [句子長度, 批量句子數, 隱藏層中神經元數]

        # 要保證輸出張量的shape: [sequence_length, batch_size, hidden_dim]
        lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)
        # print("lstm_out", lstm_out.shape) #torch.Size([20, 8, 100]) 即 [句子長度, 批量句子數, 隱藏層中神經元數]

        """ Linear 也可以把 [20, 8, 100] (當前樣本的序列長度(單詞個數), 當前批量樣本句子數, 隱藏層中神經元數數量)
           轉換爲 [20, 8, 7](當前樣本的序列長度(單詞個數), 當前批量樣本句子數, tag_to_id的標籤數)
        """
        # 將BiLSTM的輸出經過一個全連接層, 得到輸出張量shape:[sequence_length, batch_size, tagset_size]
        lstm_feats = self.hidden2tag(lstm_out)
        # print("lstm_feats.shape",lstm_feats.shape) #[20, 8, 7]
        return lstm_feats

    # ---------------------------------------第三步: 計算損失函數第一項的分值forward_score------------------------------------------------------#
    """
    BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣:
        每個字符對應一個包含7個數值的一維向量,7個數值對應7標籤(["O","B-dis","I-dis","B-sym","I-sym","<START>","<STOP>"]),
        那麼每個數值便代表了該字符被標註爲該標籤的概率值

    轉移概率矩陣:
        轉移概率矩陣的形狀爲[tagset_size, tagset_size],tagset_size爲標籤數。
        矩陣中每個數值代表了當前字符的標籤 轉移到 下個字符的出現某標籤的概率值。
    """

    # 計算損失函數第一項的分值函數, 本質上是發射矩陣和轉移矩陣的累加和
    def _forward_alg(self, feats):
        # print("feats",feats)
        """
        :param feats: BiLSTM中最後的Linear線性層輸出的[20, 8, 7] 即 (句子最大長度, 批量句子數, tag_to_id的標籤數)
        :return:
        """
        """ 創建形狀爲(1, self.tagset_size)的二維矩陣作爲前向計算矩陣,其中每個元素值均爲-10000。
            init_alphas = [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
        """
        # init_alphas: [1, 7] , [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
        # 初始化一個alphas張量, 代表前向計算矩陣的起始位置
        init_alphas = torch.full((1, self.tagset_size), -10000.).to(device)
        # print("init_alphas",init_alphas) #tensor([[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]])
        # print("init_alphas.shape",init_alphas.shape) #torch.Size([1, 7])

        """
        前向計算矩陣的初始化:把1行中的第6列設置爲0,第6列代表START_TAG,意思就是當前字符的標籤轉移到下一個字符的標籤只能從START_TAG開始。
            把(1, self.tagset_size)的前向計算矩陣中的索引爲5的元素值設置爲0,索引爲5對應的爲“START_TAG”標籤
            init_alphas = [[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]]
        """
        # 僅僅把START_TAG賦值爲0, 代表着接下來的轉移只能從START_TAG開始
        init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
        # print("init_alphas", init_alphas) #tensor([[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]])

        """ 此處僅爲淺拷貝,只是爲了更方便所以才使用新變量forward_var """
        # 前向計算變量的賦值, 這樣在反向求導的過程中就可以自動更新參數
        # 將初始化的init_alphas賦值爲前向計算變量, 爲了後續在反向傳播求導的時候可以自動更新參數
        forward_var = init_alphas

        """
        feats: BiLSTM中最後的Linear線性層輸出的[20, 8, 7] 即 (句子最大長度, 批量句子數, tag_to_id的標籤數)
        transpose(1, 0):把 (句子最大長度, 批量句子數, tag_to_id的標籤數) 轉換爲 (批量句子數, 句子最大長度, tag_to_id的標籤數)
        """
        # 輸入進來的feats: [20, 8, 7], 爲了接下來按句子進行計算, 要將batch_size放在第一個維度上
        feats = feats.transpose(1, 0)
        # print("feats.shape", feats.shape)# [8, 20, 7]

        """
        result:形狀爲(1, 8)的二維矩陣 即(1, batch_size),每個句子計算出一個分數,批量句子數爲8。
        每個句子中有20個字符,每個字符對應7個標籤的發射概率。
        """
        # feats: [8, 20, 7]是一個3維矩陣, 最外層代表8個句子, 內層代表每個句子有20個字符,每一個字符映射成7個標籤的發射概率
        # 初始化最終的結果張量, 每個句子對應一個分數
        result = torch.zeros((1, self.batch_size)).to(device)
        # print("result.shape", result.shape) #torch.Size([1, 8])
        idx = 0  # 用於記錄當前批量樣本句子數中所遍歷的第幾個句子

        """
        遍歷發射概率矩陣中的每一個句子樣本:遍歷BiLSTM輸出的“根據批量句子計算出來的特徵數據中的”每個句子對應的特徵值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子數, 句子最大長度, tag_to_id的標籤數),也即 BiLSTM輸出的“根據批量句子計算出來的特徵數據
        feat_line:[20, 7] 即 (句子最大長度, tag_to_id的標籤數)
        """
        # 按行遍歷, 總共循環batch_size次:feats爲[8, 20, 7]
        for feat_line in feats:
            """
            遍歷發射概率矩陣中當前一個句子樣本中的每一個字符:遍歷句子中的每個字符。
            feat:[7] 即 (tag_to_id的標籤數)
            """
            # feat_line: [20, 7]
            # 遍歷每一行語句, 每一個feat代表一個time_step,即一個字符就是一個time_step,一共遍歷20個字符(time_step)
            for feat in feat_line:
                """
                alphas_t
                    把當前該字符對應的7個標籤中每個標籤所計算出來的概率值存儲到alphas_t中。
                    例子:[[第1個標籤的概率計算結果單個數值],[第2個標籤...],[第3個標籤...],[第4個...],[第5個...],[第6個...],[第7個...]]
                """
                # 當前的字符(time_step),初始化一個前向計算張量(forward tensors)
                alphas_t = []
                """
                遍歷發射概率矩陣中當前一個字符對應的7個(tagset_size個)標籤的概率值(BiLSTM輸出的概率值):
                    遍歷字符對應的7個(tagset_size個)標籤中的每個標籤的概率值
                """
                # print("===============")
                # 在當前time_step/每一個時間步,遍歷所有可能的轉移標籤, 進行累加計算
                for next_tag in range(self.tagset_size):
                    """
                   1.對發射概率矩陣中字符對應標籤的單個數值的概率值 進行廣播爲 (1,7)的二維數組來使用:
                        把每個字符對應的第1到第7個(tagset_size個)標籤的“BiLSTM輸出的”單個數值的概率值 逐個轉換爲 (1,7)的二維數組來使用。
                   2.feat[next_tag]:獲取出每個字符對應的第1到第7個(tagset_size個)標籤的“BiLSTM輸出的”概率值,爲單個數值的概率值。
                     view(1, -1):把單個數值的概率值轉換爲(1,1)的二維數組
                     expand(1, self.tagset_size):通過廣播張量的方式把(1,1)的二維數組轉換爲(1,7)
                   """
                    # 廣播發射矩陣的分數/構造發射分數的廣播張量
                    emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)
                    # print("emit_score.shape",emit_score.shape) #torch.Size([1, 7])
                    # print("emit_score",emit_score)

                    """
                    1.transitions[next_tag]:
                        獲取轉移概率矩陣中一行7列的一維行向量。
                        next_tag作爲行索引,行索引上的標籤代表了要轉移到該目標行的目標標籤。
                        next_tag行索引對應在轉移概率矩陣transitions上的目標標籤作爲當前循環所遍歷的當前字符的目標標籤,
                        那麼7列上的起始標籤就相當於上一個字符的標籤,一維行向量中的7個值分別代表了上一個字符的可能的7個標籤各自
                        轉移到當前字符的目標標籤的轉移概率值。
                    2.例子
                        #遍歷當前句子中的每個字符
                        for feat in feat_line:
                            #遍歷當前字符對應的每個標籤。tagset_size爲7,next_tag爲0到6的值,每個字符有7個標籤。
                            for next_tag in range(self.tagset_size):
                                #例如:next_tag爲0時,那麼transitions[next_tag]取出轉移概率矩陣中的第一行7列的行向量。
                                #行索引next_tag所在目標行上的標籤認爲是要轉移到的目標標籤,該目標標籤即可認爲是當前循環所遍歷的當前字符的當前標籤。
                                #而每列上的標籤名則可以認爲是轉移的起始標籤,起始標籤即可認爲是上一個字符的標籤。
                                #那麼行向量中的每個轉移概率值便代表了上一個字符的標籤轉移到當前字符的標籤的轉移概率值。
                                trans_score = transitions[next_tag].view(1, -1)
                    3.transitions[next_tag]:torch.Size([1, 7]) 一行7列的一維向量
                      view(1, -1):torch.Size([1, 7]) 一行7列的一維向量
                   """
                    # 第i個time_step循環時, 轉移到next_tag標籤的轉移概率
                    # 當前時間步, 轉移到next_tag標籤的轉移分數
                    trans_score = self.transitions[next_tag].view(1, -1)
                    # print("trans_score.shape",trans_score.shape) #torch.Size([1, 7])
                    # print("trans_score", trans_score)

                    """ next_tag_var:把形狀均爲[1, 7]的前向計算矩陣、轉移概率矩陣、發射概率矩陣 三者進行相加,結果同樣爲[1, 7] """
                    # 將 前向計算矩陣, 轉移矩陣, 發射矩陣累加
                    next_tag_var = forward_var + trans_score + emit_score
                    # print("next_tag_var.shape",next_tag_var.shape) #torch.Size([1, 7])
                    # print("next_tag_var", next_tag_var)

                    """
                    log_sum_exp(next_tag_var) 即 log(sum(exp(next_tag_var)))
                        即把[1, 7]形狀的二維矩陣轉換爲單個數值輸出。
                        log(sum(exp(next_tag_var)))輸出的單個數值代表當前該字符對應的7個標籤中的第N個標籤的計算得分值。
                   """
                    # 計算log_sum_exp()函數值, 並添加進alphas_t列表中
                    # a = log_sum_exp(next_tag_var), 注意: log_sum_exp()函數僅僅返回一個實數值
                    # print(a.shape) : tensor(1.0975) , shape爲([]) 代表沒有維度 即爲單個數值
                    # b = a.view(1) : tensor([1.0975]), 注意: a.view(1)的操作就是將一個數字變成一個一階矩陣, 從([]) 變成 ([1]) 即一維向量
                    # print(b.shape) : ([1]) 代表 一維向量
                    alphas_t.append(log_sum_exp(next_tag_var).view(1))

                # alphas_t 存儲的是 一個字符 對應的 七個標籤的 概率計算結果值
                # print(len(alphas_t)) #7
                # print("alphas_t",alphas_t)

                # print(alphas_t) :
                #       [tensor([337.6004], grad_fn=<ViewBackward>),
                #        tensor([337.0469], grad_fn=<ViewBackward>), tensor([337.8497], grad_fn=<ViewBackward>),
                #        tensor([337.8668], grad_fn=<ViewBackward>), tensor([338.0186], grad_fn=<ViewBackward>),
                #        tensor([-9662.2734], grad_fn=<ViewBackward>), tensor([337.8692], grad_fn=<ViewBackward>)]
                # temp = torch.cat(alphas_t)
                # print(temp) : tensor([[  337.6004,   337.0469,   337.8497,   337.8668,   338.0186, -9662.2734, 337.8692]])
                """
                此處把 alphas_t(封裝了當前字符對應的7個標籤的概率值) 賦值給 前向計算矩陣forward_var 目的爲傳遞給下一個字符計算每個標籤時使用。
                1.forward_var 和 alphas_t 中形狀相同均爲[1, 7],兩者數值均相同,兩者僅所封裝的容器的類型不同。
                  此處僅爲把 [1, 7]形狀的alphas_t 從列表類型的 轉換爲 [1, 7]形狀的forward_var的 tensor類型。
                2.forward_var 和 alphas_t 均代表了 當前這一個字符 對應的 七個標籤的 概率計算結果值。
                  每次循環遍歷每個字符時,還會把當前字符計算出來的前向計算矩陣forward_var 傳遞給下一個字符來使用。
                """
                # 將列表張量轉變爲二維張量
                forward_var = torch.cat(alphas_t).view(1, -1)
                # print(forward_var.shape) # torch.Size([1, 7])
                # print("forward_var",forward_var)

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

            # print("self.transitions", self.transitions)
            # print("self.transitions.shape",self.transitions.shape) #torch.Size([7, 7])
            # print("self.tag_to_ix[STOP_TAG]",self.tag_to_ix[STOP_TAG]) #6
            # print("self.transitions[self.tag_to_ix[STOP_TAG]]",self.transitions[self.tag_to_ix[STOP_TAG]]) #使用索引值爲6作爲獲取轉移概率矩陣的行值
            # print("self.transitions[self.tag_to_ix[STOP_TAG]].shape",self.transitions[self.tag_to_ix[STOP_TAG]].shape) #torch.Size([7])
            """
            transitions[tag_to_ix[STOP_TAG]]
                tag_to_ix[STOP_TAG]的值爲6作爲轉移概率矩陣的行索引,即獲取出轉移概率矩陣中行標籤爲STOP_TAG的這一行7列的行向量。
                行標籤名STOP_TAG作爲要轉移到的目標標籤名,每個列標籤名代表了轉移的起始標籤名。
                那麼每個值便代表了“列標籤名作爲的上一個字符的”每個起始標籤 轉移到 “行標籤名STOP_TAG作爲的”目標標籤的 轉移概率值。

            1.執行到此處表示遍歷完當前句子中的所有字符,並且準備遍歷下一個句子。
            2.transitions[tag_to_ix[STOP_TAG]]:(形狀爲[7, 7]的transitions轉移概率矩陣)
                transitions[6]:獲取出形狀[7]的一維向量,使用行索引爲6 獲取轉移概率矩陣的第7行(即最後一行7列)的STOP_TAG標籤的概率值。
                比如:tensor([ 2.0923e+00, 1.5542e+00, -9.2415e-01, 6.1887e-01, -8.0374e-01, 4.5433e-02, -1.0000e+04])
                其中的最後一個值-1.0000e+04即爲-10000。
            3.執行到此處的[1, 7]形狀的前向計算矩陣forward_var:
                代表了一個句子中全部20個字符對應的7個標籤計算的概率值都保存到了[1, 7]的前向計算矩陣forward_var中。
            4.[1, 7]形狀的前向計算矩陣forward_var + [7]形狀的STOP_TAG標籤的概率值的向量
                代表給當前句子添加“最後一步轉移到STOP_TAG的”概率值,才能完成整條句子的概率值的前向計算。
            """
            # 添加最後一步轉移到"STOP_TAG"的分數, 就完成了整條語句的分數計算
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
            # print("terminal_var",terminal_var) #tensor([[329.3152, 329.5251, 329.1519, 329.7561, 328.9988, -9670.7090, -9671.0156]])
            # print("terminal_var.shape",terminal_var.shape) #torch.Size([1, 7])

            """
            log_sum_exp(terminal_var) 即 log(sum(exp(terminal_var)))
                terminal_var即爲一條樣本句子的最終得分,因此把把[1, 7]形狀的二維矩陣轉換爲單個數值輸出。
           """
            # 計算log_sum_exp()函數值, 作爲一條樣本語句的最終得分(將terminal_var放進log_sum_exp()中進行計算, 得到一條樣本語句最終的分數)
            alpha = log_sum_exp(terminal_var)
            # print(alpha) : tensor(341.9394)

            """ result:形狀爲(1, batch_size),存儲每個句子計算出來的最終得分。每個句子計算出一個分數。 """
            # 將得分添加進結果列表中, 作爲函數結果返回
            result[0][idx] = alpha
            idx += 1  # 用於記錄當前批量樣本句子數中所遍歷的第幾個句子

            """ result:[1, batch_size]中第二維爲批量句子中每個句子的最終計算得分 """
        return result

    # ---------------------------------------第四步: 計算損失函數第二項的分值gold_score------------------------------------------------------#
    def _score_sentence(self, feats, tags):
        """
        :param feats: BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數])
        :param tags: 即每個句子中的每個字符對應的標籤值,[8, 20] 即 [批量樣本句子數, 最大句子長度]
        :return:
        """
        """ 用於每個句子的最終得分 """
        # 初始化一個0值的tensor, 爲後續累加做準備
        score = torch.zeros(1).to(device)
        # print("score",score) #tensor([0.])
        # print("score.shape",score.shape) #torch.Size([1])
        """
        創建[batch_size, 1]形狀的值全部爲START_TAG的二維矩陣:tensor([[5], [5], [5], [5], [5], [5], [5], [5]])

        1.第一種寫法:
            torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long)
            會出現用戶警告如下:
            UserWarning:
                要從張量複製構造,建議使用 sourceTensor.clone().detach()
                或 sourceTensor.clone().detach().requires_grad_(True),而不是 torch.tensor(sourceTensor)。
        2.第二種寫法:
            使用 sourceTensor.clone().detach() 或 sourceTensor.clone().detach().requires_grad_(True) 該方式不會出現用戶警告。
            detach():分離作用使得這個decoder_input與模型構建的張量圖無關,相當於全新的外界輸入
            改寫爲 torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG], dtype=torch.long).clone().detach()

        3.tag_to_ix[START_TAG]:5
          (batch_size, 1) 此處即爲[8,1]:tensor([[5], [5], [5], [5], [5], [5], [5], [5]])
        """
        # 將START_TAG和真實標籤tags做列維度上的拼接。要在tags矩陣的第一列添加,這一列全部都是START_TAG。
        # temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long).to(device)
        temp = torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG], dtype=torch.long).clone().detach().to(device)
        # print("temp",temp) #torch.Size([8, 1])
        # print("temp.shape",temp.shape) #tensor([[5], [5], [5], [5], [5], [5], [5], [5]])

        """
        在[8, 20]的tags 前面增加1列全爲5的真實標籤值的列向量變成 [8, 21],
        即相當於每條樣本句子對應的真實標籤值的最開頭增加一個START_TAG標籤的真實值5。
        如下:tensor([[5, 0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0], 。。。。。。])
        """
        tags = torch.cat((temp, tags), dim=1).to(device)
        # print("tags.shape",tags.shape) #torch.Size([8, 21])

        """ 發射概率矩陣 從[20,8,7]([句子長度,當前批量樣本句子數,標籤數])變成 [8,20, 7]([當前批量樣本句子數,句子長度,標籤數]) """
        # 將傳入的feats形狀轉變爲[bathc_size, sequence_length, tagset_size]
        feats = feats.transpose(1, 0)  # [8, 20, 7]

        # 初始化最終的結果分數張量, 每一個句子均計算得出爲一個分數
        result = torch.zeros((1, self.batch_size)).to(device)
        # print("result",result) #tensor([[0., 0., 0., 0., 0., 0., 0., 0.]])
        # print("result.shape",result.shape) #torch.Size([1, 8])

        # 用於記錄當前批量樣本句子數中所遍歷的第幾個句子
        idx = 0

        """
        遍歷[8, 20, 7]中的每條樣本句子也即[20, 7]。
        遍歷發射概率矩陣中的每一個句子樣本:遍歷BiLSTM輸出的“根據批量句子計算出來的特徵數據中的”每個句子對應的特徵值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子數, 句子最大長度, tag_to_id的標籤數),也即 BiLSTM輸出的“根據批量句子計算出來的”特徵數據
        feat_line:[20, 7] 即 (句子最大長度, tag_to_id的標籤數)
        """
        # 遍歷所有的語句特徵向量
        for feat_line in feats:
            """
            for i, feat in enumerate(feat_line) 遍歷出一條樣本句子中的每個字符對應的7個標籤的的概率值
            i:遍歷從0到19,一共20次,代表遍歷一個句子中的20個字符
            feat:torch.Size([7]),即每個字符對應的7個標籤的的概率值,值也即爲BiLSTM輸出的概率值
            """
            # 此處feat_line: [20, 7]
            # 遍歷每一個時間步, 注意: 最重要的區別在於這裏是在真實標籤tags的指導下進行的轉移矩陣和發射矩陣的累加分數求和
            # 注意: 此處區別於第三步的循環, 最重要的是這是在真實標籤指導下的轉移矩陣和發射矩陣的累加分數
            for i, feat in enumerate(feat_line):
                # print("i", i) # 遍歷從0到19,一共20次,代表遍歷一個句子中的20個字符
                # print("feat.shape",feat.shape) #torch.Size([7])
                """
                1.score:
                    score = score + transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]
                    當前循環計算的分數值爲一行20個字符的總分數值。
                    循環每遍歷出一個字符時:
                        1.第一項的score:之前遍歷的所有字符所計算的score值的總和
                        2.第二項的transitions[tags[idx][i+1],tags[idx][i]](transitions[目標標籤,起始標籤):
		                	 (當前字符的)上一個字符的真實標籤值(作爲起始標籤) 轉移到 當前字符的真實標籤值(作爲目標標籤) 的轉移概率值。
		                    1.tags[idx][i](起始標籤):
		                            (當前字符的)上一個字符的真實標籤值。i從tags標籤列表中的列索引值爲0的第1列的START_TAG標籤值開始遍歷。
		                    2.tags[idx][i+1](目標標籤):
		                            循環所遍歷出來的當前字符的真實標籤值。
		                            i從tags標籤列表中的列索引值爲1的第2列(即句子中第一個字符對應的)真實標籤值開始遍歷。
		        				      從轉移概率矩陣中所獲取的“從上一個字符的真實標籤轉移到當前字符的真實標籤的”轉移概率值。
                        3.第三項的feat[tags[idx][i+1]]:根據當前字符對應的真實標籤值從發射概率矩陣中獲取出當前字符對應的真實標籤的發射概率值。

                2.轉移概率矩陣transitions[tags[idx][i + 1], tags[idx][i]]:
                    從轉移概率矩陣中獲取的是從上一個字符的真實標籤 轉移到 當前字符的真實標籤 的轉移概率值。
                    1.transitions:形狀爲[7, 7]的transitions轉移概率矩陣。
                    2.tags:形狀爲[8, 21],每行第一列的真實標籤值爲START_TAG標籤的真實值5。
                      tags[idx][i + 1] 和 tags[idx][i]的區別:
                            因爲tags從[8, 20]增加到了[8, 21],即是tags中每行的第一列增加了START_TAG標籤的真實值5,
                            那麼會發現發射概率矩陣仍爲[8, 20, 7](只有20個字符),而tags的[8, 21]就有了21個字符,
                            也就是說tags的每行在沒有增加第一列的時候,tags[idx][i]獲取的真實標籤值代表的正是
                            “當前循環從發射概率矩陣中遍歷出來的當前字符的”真實標籤值,但當tags從[8, 20]增加到了[8, 21]之後,
                            必須使用tags[idx][i+1]所獲取的真實標籤值代表的纔是“當前循環從發射概率矩陣中遍歷出來的當前字符的”真實標籤值。
                    3.transitions[tags[idx][i + 1], tags[idx][i]]
                        1.tags[idx][i + 1] 作爲轉移概率矩陣的行索引:
                            由於tags從[8, 20]變成[8, 21]之後,tags[idx][i + 1]在當前循環中實際是從列索引爲1的列開始,
                            從tags的列索引爲1的列開始所獲取出的真實標籤值對應的正是“當前循環從發射概率矩陣中遍歷出來的當前字符的”真實標籤值。
                        2.tags[idx][i] 作爲轉移概率矩陣的列索引:
                            由於tags從[8, 20]變成[8, 21]之後,tags[idx][i]在當前循環中實際是從列索引爲0的列開始(即從第1列的START_TAG標籤值5開始),
                            那麼只有tags[idx][i]纔會從第1列的START_TAG標籤真實值5開始遍歷。
                        3.transitions[當前字符的真實標籤值作爲要轉移到的目標行, 當前字符的上一個字符的真實標籤值作爲轉移的起始列]
                            1.行索引(tags[idx][i + 1]):當前字符的真實標籤值作爲要轉移到的目標行。
                              列索引(tags[idx][i]):當前字符的上一個字符的真實標籤值作爲轉移的起始列,[i]爲從START_TAG標籤值第一列開始的。
                            2.因爲tags從[8, 20]變成[8, 21]的關係,tags[idx][i+1]獲取的實際纔是當前循環所遍歷字符在tags的真實標籤值,
                              而tags[idx][i]獲取的實際是當前循環所遍歷字符的上一個字符對應的在tags的真實標籤值,
                              tags[idx][i]爲從第一列START_TAG標籤值開始。
                            3.transitions[當前字符的真實標籤值作爲要轉移到的目標行, 當前字符的上一個字符的真實標籤值作爲轉移的起始列]
                              實際爲從轉移概率矩陣中獲取的是從上一個字符的真實標籤 轉移到 當前字符的真實標籤 的轉移概率值。
                        4.第一種用法:
                            transitions[當前字符的真實標籤值作爲要轉移到的目標行, 當前字符的上一個字符的真實標籤值作爲轉移的起始列]
                            從轉移概率矩陣中獲取的是“上一個字符的真實標籤轉移到當前字符的真實標籤的”轉移概率值。
                            需要使用 transitions.data[tag_to_ix[START_TAG], :]=-10000 和 transitions.data[:, tag_to_ix[STOP_TAG]]=-10000
                            來進行轉移概率矩陣的初始化。因此transitions轉移概率矩陣中行索引代表了要轉移到的目標行,
                            其目標行上的標籤對應的值爲要轉移到該標籤的轉移概率值。
                            列索引代表了轉移的起始列,其起始列上的標籤作爲轉移的起始標籤。
                        5.第二種用法:
                            transitions[當前字符的上一個字符的真實標籤值作爲轉移的起始行, 當前字符的真實標籤值作爲要轉移到的目標列]
                            從轉移概率矩陣中獲取的是“上一個字符的真實標籤轉移到當前字符的真實標籤的”轉移概率值。
                            需要使用transitions.data[:, tag_to_ix[START_TAG]]=-10000和transitions.data[tag_to_ix[STOP_TAG], :]=-10000
                            來進行轉移概率矩陣的初始化。
                            因此transitions轉移概率矩陣中行索引代表了轉移的起始行,其起始行上的標籤作爲轉移的起始標籤。
                            列索引代表了要轉移到的目標列,其目標列上的標籤對應的值爲要轉移到該標籤的轉移概率值。

                3.發射概率矩陣feat[tags[idx][i + 1]]:獲取出當前字符對應的真實標籤的發射概率值。
                    1.tags[idx]:根據idx行索引獲取[8, 20]中每個句子中所有字符對應的標籤值。
                    2.tags[idx][i + 1]:
                        因爲tags從[8, 20]增加到了[8, 21],即是tags中每行的第一列增加了START_TAG標籤的真實值5,
                        那麼會發現發射概率矩陣仍爲[8, 20, 7](只有20個字符),而tags的[8, 21]就有了21個字符,
                        也就是說tags的每行在沒有增加第一列的時候,tags[idx][i]獲取的真實標籤值代表的正是
                        “當前循環從發射概率矩陣中遍歷出來的當前字符的”真實標籤值,但當tags從[8, 20]增加到了[8, 21]之後,
                        必須使用tags[idx][i+1]所獲取的真實標籤值代表的纔是“當前循環從發射概率矩陣中遍歷出來的當前字符的”真實標籤值。
                    3.feat[tags[idx][i + 1]]:
                        當tags的每行增加了第一列之後,變成使用tags[idx][i+1]獲取的真實標籤值才爲代表當前循環遍歷出來的字符的真實標籤值,
                        那麼便根據當前字符的真實標籤值從形狀[7]的發射概率矩陣feat中取出對應的發射概率值。
               """
                score = score + self.transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]

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

            2.transitions[tag_to_ix[STOP_TAG], tags[idx][-1]]
                1.transitions[tag_to_ix[STOP_TAG]]
                    tag_to_ix[STOP_TAG]的值爲6作爲轉移概率矩陣的行索引,即獲取出轉移概率矩陣中行標籤爲STOP_TAG的這一行7列的行向量。
                    行標籤名STOP_TAG作爲要轉移到的目標標籤名,每個列標籤名代表了轉移的起始標籤名。
                    那麼每個值便代表了“列標籤名作爲的上一個字符的”每個起始標籤 轉移到 “行標籤名STOP_TAG作爲的”目標標籤的 轉移概率值。
                2.tags[idx][-1]
                    從每條樣本數據中每個字符對應的的真實標籤中,即取每條樣本數據中最後一個字符對應的真實標籤值。
                3.transitions[tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[行目標標籤STOP_TAG, 列起始標籤])
                     1.tag_to_ix[STOP_TAG]:
                        值爲6,最終作爲轉移概率矩陣中的行索引值,即取轉移概率矩陣中行標籤名爲STOP_TAG的一行7列的行向量,
                        同時行標籤名STOP_TAG作爲要轉移到的目標標籤。
                     2.tags[idx][-1]:
                        值爲每個樣本句子中的最後一個字符對應的標籤值,最終作爲轉移概率矩陣中的列索引值,
                        同時該列索引值對應的列標籤名作爲轉移的起始標籤。
                     3.transitions[行目標標籤STOP_TAG, 列起始標籤]
                        先從轉移概率矩陣中取出行標籤爲STOP_TAG的這一行7列的行向量,然後根據起始標籤的列索引值從行向量取出某一列的轉移概率值,
                        即該轉移概率值代表了該樣本句子中最後一個字符的標籤轉移到STOP_TAG標籤的轉移概率值。
            3.總結
                第一項的score:整一條樣本句子遍歷完所有20個字符之後計算出來的score值的總和
                第二項的transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[目標標籤,起始標籤]):
                    句子中的最後一個字符對應的真實標籤值(作爲起始標籤) 轉移到 行標籤名STOP_TAG(作爲目標標籤) 的轉移概率值。
                    1.transitions[tag_to_ix[STOP_TAG]](transitions[目標標籤]):
                        行標籤名STOP_TAG作爲要轉移到的目標標籤名,每個列標籤名代表了轉移的起始標籤名。
                        行向量中每個值便代表了“列標籤名作爲的上一個字符的”每個起始標籤 轉移到 “行標籤名STOP_TAG作爲的”目標標籤的 轉移概率值。
                    2.tags[idx][-1](起始標籤):
                        真實標籤值爲每個樣本句子中的最後一個字符對應的真實標籤值,最終作爲轉移概率矩陣中的列索引值,同時該列索引值對應的列標籤名作爲轉移的起始標籤。
            """
            # 遍歷完當前語句所有的時間步之後, 最後添加上"STOP_TAG"的轉移分數
            # 最後加上轉移到STOP_TAG的分數
            score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]

            """ result:形狀爲(1, batch_size),存儲每個句子計算出來的最終得分。每個句子計算出一個分數。 """
            # 將該條語句的最終得分添加進結果列表中
            result[0][idx] = score
            idx += 1  # 用於記錄當前批量樣本句子數中所遍歷的第幾個句子
            """ 用於記錄每個句子計算出來的最終得分,遍歷計算下一個句子的得分之前,先清空該變量值 """
            # score = torch.zeros(1).to(device)
        return result

    # ---------------------------------------第五步: 維特比算法的實現------------------------------------------------------#

    """
    1.在HMM模型中的解碼問題最常用的算法是維特比算法
        1.維特比算法是一個通用的解碼算法,或者說是一個通用的求序列最短路徑的動態規劃算法,
          是基於動態規劃的求序列最短路徑的方法,維特比算法同樣也可以應用於解決很多其他問題。
        2.維特比算法在用於解碼隱藏狀態序列時,實際即給定模型和觀測序列,求給定觀測序列條件下,
          最可能出現的對應的隱藏狀態序列。維特比算法可以將HMM的狀態序列作爲一個整體來考慮,避免近似算法的問題。

    2.當前使用維特比算法用於解碼問題,負責求解解碼出最優路徑,即推斷出最優標籤序列。
      動態規劃要求的是在遍歷(一共20個字符)每個字符依次前向計算找到最優的7個標籤存儲到[20, 7]形狀的回溯列表,
      然後再進行反向回溯解碼時從回溯列表中找出每個字符最優的一個標籤,
      便是按照從最後一個字符往前的方向 根據第i個字符的最優標籤的索引值找到第i-1個字符(即第i個字符的上一個字符)
      的最優標籤的索引值。

        #1.result_best_path最終返回的形狀爲二維的[8, 20],包含“等於批量句子樣本數8的”列表個數,
        #  每個列表中又存放“等於句子最大長度的”元素個數,最終的元素值爲每個字符解碼預測出來的最優標籤的索引值。
        #2.result_best_path存儲的是批量每個句子中每個字符解碼預測出的最優標籤的索引值
        result_best_path = []

        #遍歷發射概率矩陣(形狀[8, 20, 7])中每個樣本句子(形狀[20, 7])
        for feat_line in feats:
            #1.回溯指針:backpointers回溯列表最終返回的形狀爲二維的[20, 7],
            #  包含“等於句子最大長度20的”列表個數,每個列表中又存放“等於標籤數7的”元素個數,
            #  每個小列表中的7個元素值代表每個字符通過前向計算得到的7個最大概率的標籤索引值。
            #2.回溯指針backpointers存儲的是當前句子中每個字符通過前向計算得到的7個最大概率的標籤索引值。
            backpointers = []

            #[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
            init_vvars = torch.full((1, self.tagset_size), -10000.)
            #僅設置索引爲5“START_TAG”標籤的列值爲0,代表只能從START_TAG標籤開始
            #[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]
            init_vvars[0][self.tag_to_ix[START_TAG]] = 0
            #前向計算矩陣forward_var的初始化賦值
            #	在前向計算過程中遍歷的第i個字符(time_step)時,forward_var保存的是第i-1個字符(time_step)的viterbi維特比張量
            forward_var = init_vvars

            #遍歷發射概率矩陣中一條樣本句子(形狀[20, 7])中每個字符(形狀[7])對應的7個標籤的發射概率值
            for feat in feat_line:

                #當前字符對應的回溯列表:負責存儲每個字符中7個(目標)標籤對應的最大概率值的起始標籤的索引值
                bptrs_t = []

                #當前字符對應的維特比列表:負責存儲每個字符中7個(目標)標籤對應的最大概率值
                viterbivars_t = []

                #遍歷發射概率矩陣中的每個字符(形狀[7])對應的7個標籤的發射概率值
                for next_tag in range(self.tagset_size):

                    #1.forward_var(前向計算矩陣):
                    #	實質爲每個字符對應的7個(目標)標籤的最大轉移概率值和7個標籤的發射概率值的累計和。
                    #	前向計算矩陣所計算的每個當前字符的累計和的值都會傳遞給下一個字符作爲forward_var繼續進行累加和計算。
                    #	在前向計算過程中遍歷的第i個字符(time_step)時,
                    #	forward_var保存的是第i-1個字符(time_step)的viterbi維特比張量。
                    #2.transitions[next_tag]:
                    #	從轉移概率矩陣中取出“行索引爲當前標籤值的”一行7列(形狀[7])的行向量。
                    #	行向量中的7個值代表7個標籤轉移到當前字符所遍歷的當前標籤(即目標標籤)的轉移概率值。
                    next_tag_var = forward_var + transitions[next_tag]

                    #best_tag_id:
                    #	因爲每個字符依次前向計算需要找到最優的7個標籤,
                    #	那麼此處首先需要找到每個字符所遍歷的每個(目標)標籤的最大概率值,
                    #	argmax目的就是從當前字符所遍歷的標籤作爲目標標籤的7個概率值中取出一個最大概率值的索引,
                    #	同時該最大概率值的索引代表了“7個作爲轉移的起始標籤轉移到當前目標標籤中”最大概率值的一個起始標籤。
                    best_tag_id = argmax(next_tag_var)

                    #把當前最大概率值的起始標籤的索引值保存到當前字符對應的回溯列表中
                    bptrs_t.append(best_tag_id)

                    #根據當前最大概率值的起始標籤的索引值取出該最大概率值保存到當前字符對應的維特比列表中
                    viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

                #forward_var = torch.cat(viterbivars_t) + feat
                #	1.forward_var:
                #		實質爲每個字符對應的7個標籤的轉移概率值和7個標籤的發射概率值的累計和。
                #		在前向計算過程中遍歷的第i個字符(time_step)時,
                #		forward_var保存的是第i-1個字符(time_step)的viterbi維特比張量。
                #	2.torch.cat(viterbivars_t):變成torch.Size([7])類型。
                #	3.feat:當前字符對應的7個標籤的發射概率值
                forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)

                #把每個字符對應的(形狀[7]的)回溯列表 存儲到(形狀[20, 7]的)句子對應的回溯列表
                backpointers.append(bptrs_t)

            #1.執行到此處代表了句子中全部20個字符已經前向計算完畢,最終前向計算矩陣要添加“轉移到STOP_TAG的”轉移概率值。
            #2.forward_var:保存了“經過句子中全部20個字符前向計算的”(形狀[1, 7]的)矩陣值
            #3.transitions[tag_to_ix[STOP_TAG]]
            #	    tag_to_ix[STOP_TAG]的值爲6作爲轉移概率矩陣的行索引,即獲取出轉移概率矩陣中行標籤爲STOP_TAG的這一行7列的行向量。
            #	    行標籤名STOP_TAG作爲要轉移到的目標標籤名,每個列標籤名代表了轉移的起始標籤名。
            #	    那麼每個值便代表了“列標籤名作爲的上一個字符的”每個起始標籤 轉移到 “行標籤名STOP_TAG作爲的”目標標籤的 轉移概率值。
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]

            #獲取出當前句子對應的(形狀[1, 7]的)最終概率值矩陣中的最大概率值的標籤的索引值
            #該索引值代表句子中最後一個字符(第20個字符)的最優標籤的索引值。
            best_tag_id = argmax(terminal_var)

            #best_path列表最終會保存有20個字符的最優標籤的索引值加上1個START_TAG標籤的索引值,
            #因還需要把START_TAG標籤的索引值移除掉才能作爲函數返回值。
            #此處先保存下句子中最後一個字符(第20個字符)的最優標籤的索引值
            best_path = [best_tag_id]

            #1.reversed翻轉回溯列表即倒序排序,從最後一個字符往前遍歷,即從第i個字符往第i-1個字符進行遍歷。
            #2.先取得第i個字符的最優標籤的索引值,然後便根據當前該第i個字符的最優標籤的索引值取得第i-1個字符的最優標籤的索引值。
            #3.最終best_path列表保存有20個字符的最優標籤的索引值加上一個START_TAG標籤的索引值
            for bptrs_t in reversed(backpointers):
                #先取得第i個字符的最優標籤的索引值,然後便根據當前該第i個字符的最優標籤的索引值取得第i-1個字符的最優標籤的索引值。
                best_tag_id = bptrs_t[best_tag_id]
                #把每個字符對應的最優標籤的索引值追加到best_path列表末尾
                best_path.append(best_tag_id)

            #best_path列表最終會保存有20個字符的最優標籤的索引值加上1個START_TAG標籤的索引值,
            #因還需要把START_TAG標籤的索引值移除掉才能作爲函數返回值。
            #pop()刪除best_path列表中存儲的最後一個值(START_TAG標籤的索引值)
            start = best_path.pop()

            #assert斷言:刪除該值必定爲START_TAG標籤的索引值
            assert start == self.tag_to_ix[START_TAG]

            #重新把best_path列表翻轉回正常的字符順序排序
            best_path.reverse()
    """

    # 根據傳入的語句特徵feats, 推斷出標籤序列
    def _viterbi_decode(self, feats):
        # 初始化最佳路徑結果的存放列表
        result_best_path = []
        # BiLSTM中最後的Linear線性層輸出的[20, 8, 7] 即 (句子最大長度, 批量句子數, 標籤數)
        # 將輸入張量變形爲[batch_size, sequence_length, tagset_size]
        feats = feats.transpose(1, 0)

        """
        遍歷[8, 20, 7]的發射概率矩陣中的每條樣本句子也即[20, 7]。
        遍歷發射概率矩陣中的每一個句子樣本:遍歷BiLSTM輸出的“根據批量句子計算出來的特徵數據中的”每個句子對應的特徵值[20, 7]。
        feats:[8, 20, 7] 即 (批量句子數, 句子最大長度, tag_to_id的標籤數),也即 BiLSTM輸出的“根據批量句子計算出來的”特徵數據
        feat_line:[20, 7] 即 (句子最大長度, tag_to_id的標籤數)
        """
        # 對批次中的每一行語句進行遍歷, 每個語句產生一個最優標註序列
        for feat_line in feats:
            # 回溯指針
            backpointers = []

            """ 創建形狀爲(1, self.tagset_size)的二維矩陣作爲前向計算矩陣,其中每個元素值均爲-10000。
                init_vvars = [[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
            """
            # 初始化前向傳播的張量, 設置START_TAG等於0, 約束合法序列只能從START_TAG開始
            init_vvars = torch.full((1, self.tagset_size), -10000.).to(device)
            """
            前向計算矩陣的初始化:把1行中的第6列設置爲0,第6列代表START_TAG,意思就是句子一開始必須只能從START_TAG標籤開始。
                把(1, self.tagset_size)的前向計算矩陣中的索引爲5的元素值設置爲0,索引爲5對應的爲“START_TAG”標籤
                init_alphas = [[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]]
            """
            # 僅僅把START_TAG賦值爲0, 代表着接下來的轉移只能從START_TAG開始
            init_vvars[0][self.tag_to_ix[START_TAG]] = 0
            # print("init_vvars", init_vvars) #tensor([[-10000., -10000., -10000., -10000., -10000.,      0., -10000.]])

            # 在第i個time_step, 張量forward_var保存第i-1個time_step的viterbi維特比變量
            # 將初始化的變量賦值給forward_var, 在第i個time_step中, 張量forward_var保存的是第i-1個time_step的viterbi維特比張量
            forward_var = init_vvars

            """
            遍歷[20, 7]的發射概率矩陣中當前一個句子樣本中的每一個字符:遍歷句子中的每個字符。
            feat:[7] 即 (tag_to_id的標籤數)
            """
            # 依次遍歷i=0, 到序列最後的每一個time_step, 每一個時間步
            for feat in feat_line:
                # print("feat",feat)
                """ bptrs_t:回溯列表專門用於存儲每個字符對應的7個轉移概率值最大的標籤 """
                # 初始化保存當前time_step的回溯指針
                bptrs_t = []
                # 初始化保存當前time_step的viterbi維特比變量
                viterbivars_t = []

                """
                遍歷發射概率矩陣中當前一個字符對應的7個(tagset_size個)標籤的概率值(BiLSTM輸出的概率值):
                    遍歷字符對應的7個(tagset_size個)標籤中的每個標籤的發射概率值
                """
                # 遍歷所有可能的轉移標籤
                for next_tag in range(self.tagset_size):
                    """
                    next_tag_var = forward_var + transitions[next_tag]

                    1.第一項forward_var:
                            循環每次遍歷計算完一個字符對應的7個標籤的概率值的總和都會存儲到forward_var,
                            當遍歷下一個字符計算其7個標籤的概率值的總和時,仍會把當前字符計算出來的forward_var傳給下一個字符的計算時使用,
                            也即會把上一個字符字符計算出來的前向計算矩陣forward_var傳遞給下一個字符來使用。

                    2.第二項transitions[next_tag]:
                            獲取轉移概率矩陣中一行7列的一維行向量(torch.Size([1, 7]))。
                            next_tag作爲行索引,行索引上的標籤代表了要轉移到該目標行的目標標籤。
                            next_tag行索引對應在轉移概率矩陣transitions上的目標標籤即爲當前循環所遍歷的當前字符的標籤,
                            那麼7列上的起始標籤就相當於上一個字符的標籤,一維行向量中的7個值分別代表了上一個字符的可能的7個標籤各自
                            轉移到當前字符的目標標籤的轉移概率值。
                    3.注意:
                        此處只有前向計算矩陣forward_var和轉移概率矩陣中的轉移概率值相加,並沒有加上發射矩陣分數feat,
                        因此此處只是進行求最大概率值的下標。
                   """
                    # next_tag_var[i]保存了tag_i 在前一個time_step的viterbi維特比變量
                    # 前向傳播張量forward_var加上從tag_i轉移到next_tag的分數, 賦值給next_tag_var
                    # 注意: 在這裏不去加發射矩陣的分數, 因爲發射矩陣分數一致, 不影響求最大值下標
                    next_tag_var = forward_var + self.transitions[next_tag]
                    # print("next_tag_var.shape",next_tag_var.shape) #torch.Size([1, 7])
                    # print("next_tag_var",next_tag_var) #例如:tensor([[41.4296, 31.9482, 33.2792, 32.7001, 34.8837, -9962.9268, -9960.8936]])

                    """
                    調用自定的argmax函數:
                        獲取出[1, 7]二維數組中第二維(列)中的最大值 和 最大值對應的索引值,但只返回最大值對應的索引值。
                        該最大值的索引值對應標籤列表中的相同索引上的標籤,該最大值即爲該標籤的該概率值。
                    next_tag_var
                        代表標籤列表中的7個標籤轉移到當前字符的目標標籤的轉移概率值,
                        那麼提取最大概率值的標籤的索引值 代表 提取出“轉移到當前字符的目標標籤的概率值最大的”標籤。
                   """
                    best_tag_id = argmax(next_tag_var)
                    # print("best_tag_id",best_tag_id) #例如:0
                    # print("next_tag_var[0][best_tag_id]",next_tag_var[0][best_tag_id]) #例如:tensor(41.4296)

                    """
                    把對應最大概率值的標籤的索引值 存儲到 回溯列表bptrs_t中。
                    bptrs_t:回溯列表專門用於存儲每個字符對應的7個轉移概率值最大的標籤
                   """
                    # 將最大的標籤所對應的id加入到當前time_step的回溯列表中
                    bptrs_t.append(best_tag_id)

                    """
                    維特比變量viterbivars_t:
                        根據最大概率值的索引值把next_tag_var中的最大概率值提取出來並存儲到維特比變量viterbivars_t中。
                        維特比變量專門用於存儲每個字符對應的7個標籤中每個標籤所計算的[1, 7]的next_tag_var中的最大概率值。
                    next_tag_var[0][best_tag_id]:根據最大概率值的索引值把next_tag_var中的最大概率值提取出來
                    view(1):tensor(單個數值) 轉換爲 tensor([單個數值])
                   """
                    viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

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

                """
                1.forward_var:
                    循環每次遍歷計算完一個字符對應的7個標籤的概率值的總和都會存儲到forward_var,
                    當遍歷下一個字符計算其7個標籤的概率值的總和時,仍會把當前字符計算出來的forward_var傳給下一個字符的計算時使用,
                    也即會把上一個字符字符計算出來的前向計算矩陣forward_var傳遞給下一個字符來使用。

                2.torch.cat(viterbivars_t) + feat)
                    torch.cat(viterbivars_t):變成torch.Size([7])類型
                    feat:形狀爲[7],包含當前字符對應的7個標籤的發射概率值,也即是這一條句子中的當前字符在發射概率矩陣中對應7個標籤的發射概率值。
                """
                # 此處再將發射矩陣分數feat加上, 賦值給forward_var, 作爲下一個time_step的前向傳播張量
                forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
                # print("forward_var.shape",forward_var.shape) #torch.Size([1, 7])

                # 當前time_step的回溯指針添加進當前這一行樣本的總體回溯指針中
                backpointers.append(bptrs_t)
                # print("len(bptrs_t)",len(bptrs_t)) #7
                # print("bptrs_t",bptrs_t) #例子:[3, 4, 3, 3, 3, 3, 2]

            """
            執行到此處表示已經計算完一條樣本句子中的所有字符的前向計算矩陣forward_var,並且準備遍歷下一個句子。
            此處還將需要對這條樣本句子對應的前向計算矩陣forward_var加上“轉移概率矩陣中負責轉移到STOP_TAG標籤的[1,7]的”轉移概率行向量。

            transitions[tag_to_ix[STOP_TAG]]
                tag_to_ix[STOP_TAG]的值爲6作爲轉移概率矩陣的行索引,即獲取出轉移概率矩陣中行標籤爲STOP_TAG的這一行7列的行向量。
                行標籤名STOP_TAG作爲要轉移到的目標標籤名,每個列標籤名代表了轉移的起始標籤名。
                那麼每個值便代表了“列標籤名作爲的上一個字符的”每個起始標籤 轉移到 “行標籤名STOP_TAG作爲的”目標標籤的 轉移概率值。

            """
            # 最後加上轉移到STOP_TAG的分數
            terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
            # print("terminal_var.shape",terminal_var.shape) #torch.Size([1, 7])

            """
            調用自定的argmax函數:
                獲取出[1, 7]二維數組中第二維(列)中的最大值 和 最大值對應的索引值,但只返回最大值對應的索引值。
                該最大值的索引值對應標籤列表中的相同索引上的標籤,該最大值即爲該標籤的該概率值。
           """
            best_tag_id = argmax(terminal_var)
            # print("best_tag_id",best_tag_id) # 例如:3

            # 根據回溯指針, 解碼最佳路徑
            # 首先把最後一步的id值加入
            best_path = [best_tag_id]
            # print("best_path",best_path)#例如:[3]

            # print("len(backpointers)",len(backpointers)) #20
            # print("len(backpointers[0])",len(backpointers[0])) #7
            # print("backpointers",backpointers) #列表中包含20個小列表,每個小列表又包含7個數值
            # reversed(backpointers):僅把backpointers中所包含的20個小列表進行倒序排列後重新存儲,但每個小列表中的7個數值的順序並不會變
            # print("reversed(backpointers)",[bptrs_t for bptrs_t in reversed(backpointers)])

            """
            reversed(backpointers):僅把backpointers中所包含的20個小列表進行倒序排列後重新存儲,但每個小列表中的7個數值的順序並不會變。
            bptrs_t:每次所遍歷出來的一個包含7個數值的列表,每個數值均爲“對應某標籤的”索引值。
            best_tag_id = bptrs_t[best_tag_id]:
                根據第i個字符對應所得到的最優標籤的索引值,獲得第i-1個字符對應的最優標籤的索引值。
                因爲backpointers列表中順序排列存儲的20個小列表分別對應樣本句子中的順序的20個字符,
                而此處對backpointers列表中的20個小列表進行了倒序排列,所以變成對應樣本句子中倒序排列的20個字符。
                根據從倒序的第i個字符“對應的包含7個標籤索引值的”小列表bptrs_t中“所獲取出的最優標籤的”索引值best_tag_id
                作爲該倒序的第i個字符的最優標籤的索引,同時根據該第i個字符對應的最優標籤的索引值best_tag_id
                作爲 獲取第i-1個字符(即上一個字符)“對應的包含7個標籤索引值的”小列表bptrs_t中的最優標籤的索引值best_tag_id,
                亦即反覆循環 根據第i個字符的最優標籤的索引best_tag_id 來獲取 第i-1個字符(即上一個字符) 的最優標籤的索引best_tag_id。

            """
            # 從後向前回溯最佳路徑
            for bptrs_t in reversed(backpointers):
                # 通過第i個time_step得到的最佳id, 找到第i-1個time_step的最佳id
                best_tag_id = bptrs_t[best_tag_id]
                best_path.append(best_tag_id)

            # print("len(best_path)", len(best_path))  # 21
            # 將START_TAG刪除
            start = best_path.pop()

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

            # 確認一下最佳路徑的第一個標籤是START_TAG
            # if start != self.tag_to_ix["<START>"]:
            #     print(start)
            assert start == self.tag_to_ix[START_TAG]

            # 因爲是從後向前進行回溯, 所以在此對列表進行逆序操作得到從前向後的真實路徑
            best_path.reverse()
            # print("best_path",best_path)
            # print("len(best_path)",len(best_path)) #20

            # 將當前這一行的樣本結果添加到最終的結果列表中
            result_best_path.append(best_path)

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

    # ---------------------------------------第六步: 完善BiLSTM_CRF類的全部功能------------------------------------------------------#
    """
    對數似然函數
        涉及到似然函數的許多應用中,更方便的是使用似然函數的自然對數形式,即“對數似然函數”。
        求解一個函數的極大化往往需要求解該函數的關於未知參數的偏導數。
        由於對數函數是單調遞增的,而且對數似然函數在極大化求解時較爲方便,所以對數似然函數常用在最大似然估計及相關領域中。
    """

    # 對數似然函數的計算, 輸入兩個參數:數字化編碼後的語句, 和真實的標籤
    # 注意: 這個函數是未來真實訓練中要用到的損失函數, 虛擬化的forward()
    def neg_log_likelihood(self, sentence, tags):
        """ 第二步: 文本信息張量化
                最終獲得feats:BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數])
        """
        # 函數中實現 經過Embedding->BiLSTM->Linear進行特徵計算後輸出的特徵矩陣。
        # BiLSTM中最後的Linear線性層輸出的[20, 8, 7] 即 (句子最大長度, 批量句子數, tag_to_id的標籤數)
        # 第一步先得到BiLSTM層的輸出特徵張量
        feats = self._get_lstm_features(sentence)

        # feats : [20, 8, 7] 代表一個批次有8個樣本, 每個樣本長度20, 每一個字符映射成7個標籤
        # 每一個word映射到7個標籤的概率, 發射矩陣

        """ 第三步: 計算損失函數第一項的分值forward_score
                損失函數第一項的分值forward_score:本質上是發射概率emit_score和轉移概率trans_score的累加和。
                feats:BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數])
                最終獲得forward_score:[1, batch_size],其中第二維爲批量句子中每個句子的最終計算得分。
                比如:tensor([[ 39.4420, 79.3957, 118.6056, 158.7210, 198.3160, 237.7789, 277.1398, 317.2183]])
        """
        # forward_score 代表公式推導中損失函數loss的第一項
        forward_score = self._forward_alg(feats)
        # print("損失函數第一項的分值forward_score", forward_score)

        """ 第四步: 計算損失函數第二項的分值gold_score
                損失函數第二項的分值gold_score:發射概率矩陣中真實標籤的發射概率值 和 轉移概率矩陣中真實標籤之間的轉移概率值 的累加和。
                feats:BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數])
                tags:即每個句子中的每個字符對應的標籤值,[8, 20] 即 [批量樣本句子數, 最大句子長度]
                最終獲得gold_score:[1, batch_size],其中第二維爲批量句子中每個句子的最終計算得分。
                比如:tensor([[-11.9251, -13.1060, -11.4474, -12.4318, -10.8670, -14.7720,  -3.8157, -18.1846]])
        """
        # gold_score 代表公式推導中損失函數loss的第二項
        gold_score = self._score_sentence(feats, tags)
        # print("損失函數第二項的分值gold_score", gold_score)

        """
        對數似然函數:(在真實的訓練中, 只需要最大化似然概率p(y|X)即可)
            1.損失函數第一項的分值forward_score:本質上是發射概率emit_score和轉移概率trans_score的累加和。
              損失函數第二項的分值gold_score:發射概率矩陣中真實標籤的發射概率值 和 轉移概率矩陣中真實標籤之間的轉移概率值 的累加和。
            2.loss值:損失函數第一項的分值forward_score - 損失函數第二項的分值gold_score 的差值作爲loss值。
            3.torch.sum():按行求和則設置dim=1,按列求和則設置dim=0。
        """
        # 按行求和, 在torch.sum()函數值中, 需要設置dim=1 ; 同理, dim=0代表按列求和
        # 注意: 在這裏, 通過forward_score和gold_score的差值來作爲loss, 用來梯度下降訓練模型
        return torch.sum(forward_score - gold_score, dim=1).to(device)

    # 此處的forward()真實場景是用在預測部分, 訓練的時候並沒有用到
    # 編寫正式的forward()函數, 注意應用場景是在預測的時候, 模型訓練的時候並沒有用到forward()函數
    def forward(self, sentence):
        """ 文本信息張量化
                最終獲得feats:BiLSTM中Linear層輸出的[20, 8, 7]的發射概率矩陣([句子長度, 當前批量樣本句子數, 標籤數])
        """
        # 函數中實現 經過Embedding->BiLSTM->Linear進行特徵計算後輸出的特徵矩陣。
        # BiLSTM中最後的Linear線性層輸出的[20, 8, 7] 即 (句子最大長度, 批量句子數, tag_to_id的標籤數)
        # 第一步 先得到BiLSTM層的輸出特徵張量
        # 首先獲取BiLSTM層的輸出特徵, 得到發射矩陣
        lstm_feats = self._get_lstm_features(sentence)

        # 通過維特比算法直接解碼出最優路徑
        tag_seq = self._viterbi_decode(lstm_feats)
        return tag_seq

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

# # 函數sentence_map完成中文文本信息的數字編碼, 變成張量
# def sentence_map(sentence_list, char_to_id, max_length):
#     # 對一個批次的所有語句按照長短進行排序, 此步驟非必須
#     sentence_list.sort(key=lambda c: len(c), reverse=True)
#     # 定義一個最終存儲結果特徵向量的空列表
#     sentence_map_list = []
#     # 循環遍歷一個批次內的所有語句
#     for sentence in sentence_list:
#         # 採用列表生成式完成字符到id的映射
#         sentence_id_list = [char_to_id[c] for c in sentence]
#         # 長度不夠的部分用0填充
#         padding_list = [0] * (max_length - len(sentence))
#         # 將每一個語句向量擴充成相同長度的向量
#         sentence_id_list.extend(padding_list)
#         # 追加進最終存儲結果的列表中
#         sentence_map_list.append(sentence_id_list)
#     # 返回一個標量類型值的張量
#     return torch.tensor(sentence_map_list, dtype=torch.long)


# ---------------------------------------第三步: 計算損失函數第一項的分值forward_score------------------------------------------------------#

# 若干輔助函數, 在類BiLSTM外部定義, 目的是輔助log_sum_exp()函數的計算
# 將Variable類型變量內部的真實值, 以python float類型返回
def to_scalar(var):  # var是Variable, 維度是1
    """ 把 傳入的torch.Size([1])的一維向量(只包含一個最大值對應的索引值) 提取出其中的 最大值對應的索引值 """
    # 返回一個python float類型的值
    return var.view(-1).data.tolist()[0]


# 獲取最大值的下標
def argmax(vec):
    """ 獲取出[1, 7]二維數組中第二維(列)中的最大值 和 最大值對應的索引值 """
    # 返回列的維度上的最大值下標, 此下標是一個標量float
    _, idx = torch.max(vec, 1)
    return to_scalar(idx)


"""  """


# 輔助完成損失函數中的公式計算
def log_sum_exp(vec):  # vec是1 * 7, type是Variable
    """
    :param vec: [1, 7]的二維數組
    :return:
    """
    """ 最終獲取出[1, 7]二維數組中第二維(列)中的最大值 """
    # 求向量中的最大值
    max_score = vec[0, argmax(vec)]
    # print(vec)            # 打印[1, 7]的二維數組
    # print(argmax(vec))    # 自動獲取第二維(列)中的最大值對應的索引值
    # print(vec[0, argmax(vec)])    # vec[0, 最大值對應的索引值] 根據最大值對應的索引值 獲取 最大值
    # print(max_score)    #最終獲取出[1, 7]二維數組中第二維(列)中的最大值
    # print(max_score.shape) #torch.Size([]) 代表0維即單個數值

    """ 
    對單個數值(二維數組中第二維(列)中的最大值) 進行廣播爲 [1, 7]。
    view(1, -1):把單個數值的torch.Size([]) 轉換爲 [1, 1]
    expand(1, vec.size()[1]):把 [1, 1] 轉換爲 [1, 7]
    """
    # max_score維度是1, max_score.view(1,-1)維度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的維度1 * 7
    # 構造一個最大值的廣播變量:經過expand()之後的張量, 裏面所有的值都相同, 都是最大值max_score
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])  # vec.size()維度是1 * 7

    """
    下面兩種計算方式實際效果相同,都可以計算出相同的結果值,結果值均爲單個數值:
        max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast))):爲了防止數值爆炸
        torch.log(torch.sum(torch.exp(vec))):可以計算出正常值,但是有可能會出現數值爆炸,其結果值便變爲inf或-inf
    """
    # a = max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
    # b = torch.log(torch.sum(torch.exp(vec)))
    # print("a",a)
    # print("b",b)
    # print(a == b)

    """ 
    實際上就是求log(sum(exp(vec))) 的結果值爲的單個數值。
    vec([1, 7]二維數組):前向計算矩陣、轉移概率矩陣、發射概率矩陣 三者相加的結果
    爲了防止數值爆炸(防止計算出inf或-inf),纔會首先對vec - vec中的最大值的廣播矩陣
     """
    # 先減去最大值max_score,再求解log_sum_exp, 最終的返回值上再加上max_score,是爲了防止數值爆炸, 純粹是代碼上的小技巧
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))

"""
模型訓練的流程
    第一步: 熟悉字符到數字編碼的碼錶
    第二步: 熟悉訓練數據集的樣式和含義解釋
    第三步: 生成批量訓練數據
    第四步: 完成準確率和召回率的評估代碼
    第五步: 完成訓練模型的代碼
    第六步: 繪製損失曲線和評估曲線圖
"""

#=================================== 第三步: 生成批量訓練數據 ====================================================#

# 創建生成批量訓練數據的函數
def load_dataset(data_file, batch_size):
    '''
    data_file: 代表待處理的文件
    batch_size: 代表每一個批次樣本的數量
    '''
    # 將train.npz文件帶入到內存中
    data = np.load(data_file)

    # 分別提取data中的特徵和標籤
    x_data = data['x_data']
    y_data = data['y_data']

    # 將數據封裝成Tensor張量
    x = torch.tensor(x_data, dtype=torch.long)
    y = torch.tensor(y_data, dtype=torch.long)

    # 將數據再次封裝
    dataset = Data.TensorDataset(x, y)

    # 求解一下數據的總量
    total_length = len(dataset)

    # 確認一下將80%的數據作爲訓練集, 剩下的20%的數據作爲測試集
    train_length = int(total_length * 0.8)
    validation_length = total_length - train_length

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

    # 將訓練數據集進行DataLoader封裝
    # dataset: 代表訓練數據集
    # batch_size: 代表一個批次樣本的數量, 若數據集的總樣本數無法被batch_size整除, 則最後一批數據的大小爲餘數,
    #             若設置另一個參數drop_last=True, 則自動忽略最後不能被整除的數量
    # shuffle: 是否每隔批次爲隨機抽取, 若設置爲True, 代表每個批次的數據樣本都是從數據集中隨機抽取的
    # num_workers: 設置有多少子進程負責數據加載, 默認爲0, 即數據將被加載到主進程中
    # drop_last: 是否把最後一個批次的數據(指那些無法被batch_size整除的餘數數據)忽略掉
    train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,
                                   shuffle=True, num_workers=2, drop_last=False)

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

    # 將兩個數據生成器封裝成一個字典類型
    data_loaders = {'train': train_loader, 'validation': validation_loader}

    # 將兩個數據集的長度也封裝成一個字典類型
    data_size = {'train': train_length, 'validation': validation_length}

    return data_loaders, data_size

#=================================== 第四步: 完成準確率和召回率的評估代碼 ====================================================#

# 評估模型的準確率, 召回率, F1等指標
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):
    '''
    sentence_list: 文本向量化後的句子張量
    true_tag: 真實的標籤
    predict_tag: 預測的標籤
    id2tag: id值到中文字符的映射表
    id2tag: id值到tag標籤的映射表
    '''
    # 初始化真實的命名實體, 預測的命名實體, 接下來比較兩者的異同來評估指標
    true_entities, true_entity = [], []
    predict_entities, predict_entity = [], []

    # 逐條的遍歷批次中的所有語句
    for line_num, sentence in enumerate(sentence_list):
        # 遍歷一條樣本語句中的每一個字符編碼(這裏面都是數字化之後的編碼)
        for char_num in range(len(sentence)):
            # 如果編碼等於0, 表示PAD, 說明後續全部都是填充的0, 可以跳出當前for循環
            if sentence[char_num] == 0:
                break

            # 依次提取真實的語句字符, 真實的樣本標籤, 預測的樣本標籤
            char_text = id2char[sentence[char_num]]
            true_tag_type = id2tag[true_tag[line_num][char_num]]
            predict_tag_type = id2tag[predict_tag[line_num][char_num]]

            # 先對真實的標籤進行命名實體的匹配
            # 如果第一個字符是"B", 表示一個實體的開始, 將"字符/標籤"的格式添加進實體列表中
            if true_tag_type[0] == "B":
                true_entity = [char_text + "/" + true_tag_type]
            # 如果第一個字符是"I", 表示處於一個實體的中間
            # 如果真實的命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
            # 意思就是比如true_entity = ["中/B-Person", "國/I-Person"], 此時"人/I-Person"就可以進行添加
            elif true_tag_type[0] == "I" and len(true_entity) != 0 and true_entity[-1].split("/")[1][1:] == true_tag_type[1:]:
                true_entity.append(char_text + "/" + true_tag_type)
            # 如果第一個字符是"O", 並且true_entity非空, 表示一個命名實體已經匹配結束
            elif true_tag_type[0] == "O" and len(true_entity) != 0:
                """ 
                1.之所以要在true_tag_type[0] == "O"的基礎上還要加上判斷len(true_entity) != 0,
                  是因爲防止循環遍歷的第一個字符就是"O"並且此時true_entity仍然爲空。
                2.執行到此處表示一個命名實體已經匹配結束,也即是B-dis+I-dis 或者 B-sym+I-sym 的匹配命名實體組合結束了,
                  那麼就要在每個匹配的命名實體組合後面加上“行號_列號”(line_num_char_num)的標識。
                """
                true_entity.append(str(line_num) + "_" + str(char_num))
                # 將匹配結束的一個命名實體加入到最終的真實實體列表中
                true_entities.append(true_entity)
                # 清空true_entity,爲了下一個命名實體的匹配做準備
                true_entity = []
            # 除了上述3種情況, 說明當前沒有匹配出任何的實體, 則清空true_entity, 繼續下一輪匹配
            else:
                true_entity = []

            # 對預測的標籤進行命名實體的匹配
            # 如果第一個字符是"B", 表示一個實體的開始, 將"字符/標籤"的格式添加進實體列表中
            if predict_tag_type[0] == "B":
                predict_entity = [char_text + "/" + predict_tag_type]
            # 如果第一個字符是"I", 表示處於一個實體的中間
            # 如果預測命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
            elif predict_tag_type[0] == "I" and len(predict_entity) != 0 and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:
                predict_entity.append(char_text + "/" + predict_tag_type)
            # 如果第一個字符是"O", 並且predict_entity非空, 表示一個完整的命名實體已經匹配結束了
            elif predict_tag_type[0] == "O" and len(predict_entity) != 0:
                """ 
                1.之所以要在true_tag_type[0] == "O"的基礎上還要加上判斷len(true_entity) != 0,
                  是因爲防止循環遍歷的第一個字符就是"O"並且此時true_entity仍然爲空。
                2.執行到此處表示一個命名實體已經匹配結束,也即是B-dis+I-dis 或者 B-sym+I-sym 的匹配命名實體組合結束了,
                  那麼就要在每個匹配的命名實體組合後面加上“行號_列號”(line_num_char_num)的標識。
                """
                predict_entity.append(str(line_num) + "_" + str(char_num))
                # 將這個匹配結束的預測命名實體添加到最終的預測實體列表中
                predict_entities.append(predict_entity)
                # 清空predict_entity, 爲下一個命名實體的匹配做準備
                predict_entity = []
            # 除了上述3種情況, 說明當前沒有匹配出任何的實體, 則清空predict_entity, 繼續下一輪的匹配
            else:
                predict_entity = []

    """
    因爲不論是預測的命名實體組合(B-dis+I-dis 或者 B-sym+I-sym)還是真實標籤的命名實體組合的後面都是加上了“行號_列號”(line_num_char_num)的標識,
    爲的就是在預測的命名實體組合 和 真實標籤的命名實體組合 兩者進行匹配比較時,不僅要求兩者對應的標籤一致,
    而且還要求兩者對應的標籤行號和列號均一致(即保證在相同句子中的相同字符位置)。
    """
    # 遍歷所有預測出來的實體列表, 只有那些在真實命名實體列表中的實體纔是正確的預測
    acc_entities = [entity for entity in predict_entities if entity in true_entities]

    # 計算正確實體的個數, 預測實體的個數, 真實實體的個數
    acc_entities_length = len(acc_entities)
    predict_entities_length = len(predict_entities)
    true_entities_length = len(true_entities)

    # 至少爭取預測了一個實體的情況下, 才計算準確率, 召回率, F1值
    if acc_entities_length > 0:
        accuracy = float(acc_entities_length / predict_entities_length)
        recall = float(acc_entities_length / true_entities_length)
        f1_score = 2.0 * accuracy * recall / (accuracy + recall)
        return accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length
    else:
        return 0, 0, 0, acc_entities_length, predict_entities_length, true_entities_length

#=================================== 第五步: 完成訓練模型的代碼 ====================================================#

# 訓練模型的函數
def train(data_loader, data_size, batch_size, embedding_dim, hidden_dim,
          sentence_length, num_layers, epochs, learning_rate, tag2id,
          model_saved_path, train_log_path,
          validate_log_path, train_history_image_path):
    '''
    data_loader: 數據集的加載器, 之前已經通過load_dataset完成了構造
    data_size:   訓練集和測試集的樣本數量
    batch_size:  批次的樣本個數
    embedding_dim:  詞嵌入的維度
    hidden_dim:     隱藏層的維度
    sentence_length:  文本限制的長度
    num_layers:       神經網絡堆疊的LSTM層數
    epochs:           訓練迭代的輪次
    learning_rate:    學習率
    tag2id:           標籤到id的映射字典
    model_saved_path: 模型保存的路徑
    train_log_path:   訓練日誌保存的路徑
    validate_log_path:  測試集日誌保存的路徑
    train_history_image_path:  訓練數據的相關圖片保存路徑
    '''
    # 將中文字符和id的對應碼錶加載進內存
    char2id = json.load(open("./char_to_id.json", mode="r", encoding="utf-8"))
    # 初始化BiLSTM_CRF模型
    model = BiLSTM_CRF(vocab_size=len(char2id), tag_to_ix=tag2id,
                   embedding_dim=embedding_dim, hidden_dim=hidden_dim,
                   batch_size=batch_size, num_layers=num_layers,
                   sequence_length=sentence_length).to(device)

    # 定義優化器, 使用SGD作爲優化器(pytorch中Embedding支持的GPU加速爲SGD, SparseAdam)
    # 參數說明如下:
    # lr:          優化器學習率
    # momentum:    優化下降的動量因子, 加速梯度下降過程
    optimizer = optim.SGD(params=model.parameters(), lr=learning_rate, momentum=0.85, weight_decay=1e-4)
    # optimizer = optim.Adam(params=model.parameters(), lr=learning_rate, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-4)

    # 設定優化器學習率更新策略
    # 參數說明如下:
    # optimizer:    優化器
    # step_size:    更新頻率, 每過多少個epoch更新一次優化器學習率
    # gamma:        學習率衰減幅度,
    #               按照什麼比例調整(衰減)學習率(相對於上一輪epoch), 默認0.1
    #   例如:
    #   初始學習率 lr = 0.5,    step_size = 20,    gamma = 0.1
    #              lr = 0.5     if epoch < 20
    #              lr = 0.05    if 20 <= epoch < 40
    #              lr = 0.005   if 40 <= epoch < 60
    # scheduler = optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=5, gamma=0.8)

    # 初始化存放訓練中損失, 準確率, 召回率, F1等數值指標
    train_loss_list = []
    train_acc_list = []
    train_recall_list = []
    train_f1_list = []
    train_log_file = open(train_log_path, mode="w", encoding="utf-8")
    # 初始化存放測試中損失, 準確率, 召回率, F1等數值指標
    validate_loss_list = []
    validate_acc_list = []
    validate_recall_list = []
    validate_f1_list = []
    validate_log_file = open(validate_log_path, mode="w", encoding="utf-8")
    # 利用tag2id生成id到tag的映射字典
    id2tag = {v:k for k, v in tag2id.items()}
    # 利用char2id生成id到字符的映射字典
    id2char = {v:k for k, v in char2id.items()}

    # 按照參數epochs的設定來循環epochs次
    for epoch in range(epochs):
        # 在進度條打印前, 先輸出當前所執行批次
        tqdm.write("Epoch {}/{}".format(epoch + 1, epochs))
        # 定義要記錄的正確總實體數, 識別實體數以及真實實體數
        total_acc_entities_length, \
        total_predict_entities_length, \
        total_gold_entities_length = 0, 0, 0
        # 定義每batch步數, 批次loss總值, 準確度, f1值
        step, total_loss, correct, f1 = 1, 0.0, 0, 0

        # 開啓當前epochs的訓練部分
        for inputs, labels in tqdm(data_loader["train"]):
            # 將數據以Variable進行封裝
            inputs, labels = Variable(inputs).to(device), Variable(labels).to(device)
            # 在訓練模型期間, 要在每個樣本計算梯度前將優化器歸零, 不然梯度會被累加
            optimizer.zero_grad()
            # 此處調用的是BiLSTM_CRF類中的neg_log_likelihood()函數
            loss = model.neg_log_likelihood(inputs, labels)
            # 獲取當前步的loss, 由tensor轉爲數字
            step_loss = loss.data
            # 累計每步損失值
            total_loss += step_loss
            # 獲取解碼最佳路徑列表, 此時調用的是BiLSTM_CRF類中的forward()函數
            best_path_list = model(inputs)
            # 模型評估指標值獲取包括:當前批次準確率, 召回率, F1值以及對應的實體個數
            step_acc, step_recall, f1_score, acc_entities_length, \
            predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
                                                                     labels.tolist(),
                                                                     best_path_list,
                                                                     id2char,
                                                                     id2tag)
            # 訓練日誌內容
            '''
            log_text = "Epoch: %s | Step: %s " \
                       "| loss: %.5f " \
                       "| acc: %.5f " \
                       "| recall: %.5f " \
                       "| f1 score: %.5f" % \
                       (epoch, step, step_loss, step_acc, step_recall,f1_score)
            '''

            # 分別累計正確總實體數、識別實體數以及真實實體數
            total_acc_entities_length += acc_entities_length
            total_predict_entities_length += predict_entities_length
            total_gold_entities_length += gold_entities_length

            # 對損失函數進行反向傳播
            loss.backward()
            # 通過optimizer.step()計算損失, 梯度和更新參數
            optimizer.step()
            # 記錄訓練日誌
            # train_log_file.write(log_text + "\n")
            step += 1
        # 獲取當前epochs平均損失值(每一輪迭代的損失總值除以總數據量)
        epoch_loss = total_loss / data_size["train"]
        # 計算當前epochs準確率
        if total_predict_entities_length > 0:
            total_acc = total_acc_entities_length / total_predict_entities_length
        # 計算當前epochs召回率
        if total_gold_entities_length > 0:
            total_recall = total_acc_entities_length / total_gold_entities_length
        # 計算當前epochs的F1值
        total_f1 = 0
        if total_acc + total_recall != 0:
            total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
        log_text = "Epoch: %s " \
                   "| mean loss: %.5f " \
                   "| total acc: %.5f " \
                   "| total recall: %.5f " \
                   "| total f1 scroe: %.5f" % (epoch, epoch_loss,
                                               total_acc,
                                               total_recall,
                                               total_f1)
        print(log_text)
        # 當前epochs訓練後更新學習率, 必須在優化器更新之後
        # scheduler.step()

        # 記錄當前epochs訓練loss值(用於圖表展示), 準確率, 召回率, f1值
        train_loss_list.append(epoch_loss)
        train_acc_list.append(total_acc)
        train_recall_list.append(total_recall)
        train_f1_list.append(total_f1)
        train_log_file.write(log_text + "\n")

        # 定義要記錄的正確總實體數, 識別實體數以及真實實體數
        total_acc_entities_length, \
        total_predict_entities_length, \
        total_gold_entities_length = 0, 0, 0
        # 定義每batch步數, 批次loss總值, 準確度, f1值
        step, total_loss, correct, f1 = 1, 0.0, 0, 0

        # 開啓當前epochs的驗證部分
        with torch.no_grad():
            for inputs, labels in tqdm(data_loader["validation"]):
                # 將數據以 Variable 進行封裝
                inputs, labels = Variable(inputs), Variable(labels)
                # 此處調用的是 BiLSTM_CRF 類中的 neg_log_likelihood 函數
                # 返回最終的 CRF 的對數似然結果
                try:
                    loss = model.neg_log_likelihood(inputs, labels)
                except:
                    continue
                # 獲取當前步的 loss 值,由 tensor 轉爲數字
                step_loss = loss.data
                # 累計每步損失值
                total_loss += step_loss
                # 獲取解碼最佳路徑列表, 此時調用的是BiLSTM_CRF類中的forward()函數
                best_path_list = model(inputs)
                # 模型評估指標值獲取: 當前批次準確率, 召回率, F1值以及對應的實體個數
                step_acc, step_recall, f1_score, acc_entities_length, \
                predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),
                                                                         labels.tolist(),
                                                                         best_path_list,
                                                                         id2char,
                                                                         id2tag)

                # 訓練日誌內容
                '''
                log_text = "Epoch: %s | Step: %s " \
                           "| loss: %.5f " \
                           "| acc: %.5f " \
                           "| recall: %.5f " \
                           "| f1 score: %.5f" % \
                           (epoch, step, step_loss, step_acc, step_recall,f1_score)
                '''

                # 分別累計正確總實體數、識別實體數以及真實實體數
                total_acc_entities_length += acc_entities_length
                total_predict_entities_length += predict_entities_length
                total_gold_entities_length += gold_entities_length

                # 記錄驗證集損失日誌
                # validate_log_file.write(log_text + "\n")
                step += 1

            # 獲取當前批次平均損失值(每一批次損失總值除以數據量)
            epoch_loss = total_loss / data_size["validation"]
            # 計算總批次準確率
            if total_predict_entities_length > 0:
                total_acc = total_acc_entities_length / total_predict_entities_length
            # 計算總批次召回率
            if total_gold_entities_length > 0:
                total_recall = total_acc_entities_length / total_gold_entities_length
            # 計算總批次F1值
            total_f1 = 0
            if total_acc + total_recall != 0.0:
                total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
            log_text = "Epoch: %s " \
                       "| mean loss: %.5f " \
                       "| total acc: %.5f " \
                       "| total recall: %.5f " \
                       "| total f1 scroe: %.5f" % (epoch, epoch_loss,
                                                   total_acc,
                                                   total_recall,
                                                   total_f1)
            print(log_text)
            # 記錄當前批次驗證loss值(用於圖表展示)準確率, 召回率, f1值
            validate_loss_list.append(epoch_loss)
            validate_acc_list.append(total_acc)
            validate_recall_list.append(total_recall)
            validate_f1_list.append(total_f1)
            validate_log_file.write(log_text + "\n")


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

    # 將loss下降歷史數據轉爲圖片存儲
    save_train_history_image(train_loss_list,
                             validate_loss_list,
                             train_history_image_path,
                             "Loss")
    # 將準確率提升歷史數據轉爲圖片存儲
    save_train_history_image(train_acc_list,
                             validate_acc_list,
                             train_history_image_path,
                             "Acc")
    # 將召回率提升歷史數據轉爲圖片存儲
    save_train_history_image(train_recall_list,
                             validate_recall_list,
                             train_history_image_path,
                             "Recall")
    # 將F1上升歷史數據轉爲圖片存儲
    save_train_history_image(train_f1_list,
                             validate_f1_list,
                             train_history_image_path,
                             "F1")
    print("train Finished".center(100, "-"))


#=================================== 第六步: 繪製損失曲線和評估曲線圖 ====================================================#

# 按照傳入的不同路徑, 繪製不同的訓練曲線
def save_train_history_image(train_history_list,
                             validate_history_list,
                             history_image_path,
                             data_type):
    # 根據訓練集的數據列表, 繪製折線圖
    plt.plot(train_history_list, label="Train %s History" % (data_type))
    # 根據測試集的數據列表, 繪製折線圖
    plt.plot(validate_history_list, label="Validate %s History" % (data_type))
    # 將圖片放置在最優位置
    plt.legend(loc="best")
    # 設置x軸的圖標爲輪次Epochs
    plt.xlabel("Epochs")
    # 設置y軸的圖標爲參數data_type
    plt.ylabel(data_type)
    # 將繪製好的圖片保存在特定的路徑下面, 並修改圖片名字中的"plot"爲對應的data_type
    plt.savefig(history_image_path.replace("plot", data_type))
    plt.close()



# 參數1:批次大小
BATCH_SIZE = 8
# 參數2:訓練數據文件路徑
train_data_file_path = "./total.npz"
# 參數3:加載 DataLoader 數據
data_loader, data_size = load_dataset(train_data_file_path, BATCH_SIZE)
# 參數4:記錄當前訓練時間(拼成字符串用)
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
# 參數5:標籤碼錶對照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數6:訓練文件存放路徑
model_saved_path = "bilstm_crf_state_dict_%s.pt" % (time_str)
# 參數7:訓練日誌文件存放路徑
train_log_path = "log/train_%s.log" % (time_str)
# 參數8:驗證打印日誌存放路徑
validate_log_path = "log/validate_%s.log" % (time_str)
# 參數9:訓練歷史記錄圖存放路徑
train_history_image_path = "log/bilstm_crf_train_plot_%s.png" % (time_str)
# 參數10:字向量維度
EMBEDDING_DIM = 300
# 參數11:隱層維度
HIDDEN_DIM = 128
# 參數12:句子長度
SENTENCE_LENGTH = 100
# 參數13:堆疊 LSTM 層數
NUM_LAYERS = 1
# 參數14:訓練批次
EPOCHS = 25
# 參數15:初始化學習率
LEARNING_RATE = 0.001
# 輸入參數:
# 開始字符和結束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"

if __name__ == '__main__':
    train(data_loader, data_size, BATCH_SIZE, EMBEDDING_DIM, HIDDEN_DIM,
          SENTENCE_LENGTH, NUM_LAYERS, EPOCHS, LEARNING_RATE, tag_to_id,
          model_saved_path, train_log_path, validate_log_path,
          train_history_image_path)

 

 

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