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

日萌社

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


 

智能對話系統:Unit對話API

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

linux 安裝 neo4jlinux 安裝 Redissupervisor 安裝

neo4j圖數據庫:Cypher

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

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

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

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

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

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

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

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

系統聯調測試與部署

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

 


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模型僅是判斷前後兩個句子是否有關聯的二分類。
如果前後兩句話有關聯的話,是一起提取這兩句話中關鍵的症狀信息融合在一起作爲查詢條件然後查詢數據庫中對應的疾病名。  

損失函數的定義:

  • BiLSTM層的輸出維度是tag_size, 也就是每個單詞w_i映射到tag的發射概率值, 假設BiLSTM的輸出矩陣是P, 其中P(i,j)代表單詞w_i映射到tag_j的非歸一化概率. 對於CRF層, 假設存在一個轉移矩陣A, 其中A(i,j)代表tag_j轉移到tag_i的概率.

  • 對於輸入序列X對應的輸出tag序列y, 定義分數如下(本質上就是發射概率和轉移概率的累加和):

  • 利用softmax函數, 爲每一個正確的tag序列y定義一個概率值, 在真實的訓練中, 只需要最大化似然概率p(y|X)即可, 具體使用對數似然如下:

  • BiLSTM+CRF模型的實現:
    • 第一步: 構建神經網絡
    • 第二步: 文本信息張量化
    • 第三步: 計算損失函數第一項的分值
    • 第四步: 計算損失函數第二項的分值
    • 第五步: 維特比算法的實現
    • 第六步: 完善BiLSTM_CRF類的全部功能
  • 第一步: 構建神經網絡
# 導入相關包與模塊
import torch
import torch.nn as nn

class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, 
                       num_layers, batch_size, sequence_length):
        '''
        description: 模型初始化
        :param vocab_size:          所有句子包含字符大小
        :param tag_to_ix:           標籤與id對照字典
        :param embedding_dim:       字嵌入維度(即LSTM輸入層維度input_size)
        :param hidden_dim:          隱藏層向量維度
        :param num_layers:          神經網絡的層數
        :param batch_size:          批次的數量
        :param sequence_length:     語句的限制最大長度
        '''
        # 繼承函數的初始化
        super(BiLSTM_CRF, self).__init__()
        # 設置標籤與id對照
        self.tag_to_ix = tag_to_ix
        # 設置標籤大小,對應 BiLSTM 最終輸出分數矩陣寬度
        self.tagset_size = len(tag_to_ix)
        # 設定 LSTM 輸入特徵大小
        self.embedding_dim = embedding_dim
        # 設置隱藏層維度
        self.hidden_dim = hidden_dim
        # 設置單詞總數的大小
        self.vocab_size = vocab_size
        # 設置隱藏層的數量
        self.num_layers = num_layers
        # 設置語句的最大限制長度
        self.sequence_length = sequence_length
        # 設置批次的大小
        self.batch_size = batch_size

        # 構建詞嵌入層, 兩個參數分別是單詞總數, 詞嵌入維度
        self.word_embeds = nn.Embedding(vocab_size, embedding_dim)

        # 構建雙向LSTM層, 輸入參數包括詞嵌入維度, 隱藏層大小, 堆疊的LSTM層數, 是否雙向標誌位
        self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=self.num_layers, bidirectional=True)

        # 構建全連接線性層, 一端對接LSTM隱藏層, 另一端對接輸出層, 相應的維度就是標籤數量tagset_size
        self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)

        # 初始化轉移矩陣, 轉移矩陣是一個方陣[tagset_size, tagset_size]
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))

        # 按照損失函數小節的定義, 任意的合法句子不會轉移到"START_TAG", 因此設置爲-10000
        # 同理, 任意合法的句子不會從"STOP_TAG"繼續向下轉移, 也設置爲-10000
        self.transitions.data[tag_to_ix[START_TAG], :] = -10000
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000

        # 初始化隱藏層, 利用單獨的類函數init_hidden()來完成
        self.hidden = self.init_hidden()

    # 定義類內部專門用於初始化隱藏層的函數
    def init_hidden(self):
        # 爲了符合LSTM的輸入要求, 我們返回h0, c0, 這兩個張量的shape完全一致
        # 需要注意的是shape: [2 * num_layers, batch_size, hidden_dim / 2]
        return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2), 
                 torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2))
  • 輸入參數:
# 開始字符和結束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1
# 初始化的字符和序號的對應碼錶
char_to_id = {"雙": 0, "肺": 1, "見": 2, "多": 3, "發": 4, "斑": 5, "片": 6,
              "狀": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}
  • 調用:
model = BiLSTM_CRF(vocab_size=len(char_to_id),
                   tag_to_ix=tag_to_ix,
                   embedding_dim=EMBEDDING_DIM,
                   hidden_dim=HIDDEN_DIM,
                   num_layers=NUM_LAYERS,
                   batch_size=BATCH_SIZE,
                   sequence_length=SENTENCE_LENGTH)

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


# 在類中將文本信息經過詞嵌入層, BiLSTM層, 線性層的處理, 最終輸出句子張量
def _get_lstm_features(self, sentence):
    self.hidden = self.init_hidden()
    # a = self.word_embeds(sentence)
    # print(a.shape)  torch.Size([8, 20, 200])
    # LSTM的輸入要求形狀爲 [sequence_length, batch_size, embedding_dim]
    # LSTM的隱藏層h0要求形狀爲 [num_layers * direction, batch_size, hidden_dim]
    embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)

    # LSTM的兩個輸入參數: 詞嵌入後的張量, 隨機初始化的隱藏層張量
    lstm_out, self.hidden = self.lstm(embeds, self.hidden)

    # 要保證輸出張量的shape: [sequence_length, batch_size, hidden_dim]
    lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)

    # 將BiLSTM的輸出經過一個全連接層, 得到輸出張量shape:[sequence_length, batch_size, tagset_size]
    lstm_feats = self.hidden2tag(lstm_out)
    return lstm_feats
  • 輸入參數:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1
# 初始化的示例語句, 共8行, 可以理解爲當前批次batch_size=8
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]
  • 調用:
char_to_id = {"<PAD>":0}

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

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

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

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

        ...,

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

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

        [[ 0.3556,  0.0299, -0.1570,  ...,  0.0512, -0.3286, -0.2882],
         [ 0.2074, -0.1521, -0.1487,  ...,  0.0637, -0.2674, -0.0174],
         [ 0.0976, -0.0754, -0.2779,  ..., -0.1588, -0.2096, -0.3432],
         ...,
         [ 0.4961,  0.0583, -0.2965,  ...,  0.0363, -0.2933, -0.1551],
         [ 0.4594,  0.3354, -0.0093,  ...,  0.1681, -0.2508, -0.1423],
         [ 0.0957, -0.0486, -0.2616,  ...,  0.0578, -0.0737, -0.2259]]],
       grad_fn=<AddBackward0>)
  • 第三步: 計算損失函數第一項的分值
# 若干輔助函數, 在類BiLSTM外部定義, 目的是輔助log_sum_exp()函數的計算
# 將Variable類型變量內部的真實值, 以python float類型返回
def to_scalar(var): # var是Variable, 維度是1
    # 返回一個python float類型的值
    return var.view(-1).data.tolist()[0]


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


# 輔助完成損失函數中的公式計算
def log_sum_exp(vec): # vec是1 * 7, type是Variable
    max_score = vec[0, argmax(vec)]
    #max_score維度是1, max_score.view(1,-1)維度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的維度1 * 7
    # 經過expand()之後的張量, 裏面所有的值都相同, 都是最大值max_score
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) # vec.size()維度是1 * 7
    # 先減去max_score,最後再加上max_score, 是爲了防止數值爆炸, 純粹是代碼上的小技巧
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))



# 計算損失函數第一項的分值函數, 本質上是發射矩陣和轉移矩陣的累加和
def _forward_alg(self, feats):
    # 初始化一個alphas張量, 代表轉移矩陣的起始位置
    init_alphas = torch.full((1, self.tagset_size), -10000.)
    # init_alphas: [1, 7] , [-10000, -10000, -10000, -10000, -10000, -10000, -10000]
    # 僅僅把START_TAG賦值爲0, 代表着接下來的轉移只能從START_TAG開始
    init_alphas[0][self.tag_to_ix[START_TAG]] = 0.

    # 前向計算變量的賦值, 這樣在反向求導的過程中就可以自動更新參數
    forward_var = init_alphas

    # 輸入進來的feats: [20, 8, 7], 爲了接下來按句子進行計算, 要將batch_size放在第一個維度上
    feats = feats.transpose(1, 0)

    # feats: [8, 20, 7]是一個3維矩陣, 最外層代表8個句子, 內層代表每個句子有20個字符, 
    # 每一個字符映射成7個標籤的發射概率
    # 初始化最終的結果張量, 每個句子對應一個分數
    result = torch.zeros((1, self.batch_size))
    idx = 0

    # 按行遍歷, 總共循環batch_size次
    for feat_line in feats:
        # 遍歷一行語句, 每一個feat代表一個time_step
        for feat in feat_line:
            # 當前time_step的一個forward tensors
            alphas_t = []
            # 在當前time_step, 遍歷所有可能的轉移標籤, 進行累加計算
            for next_tag in range(self.tagset_size):
                # 廣播發射矩陣的分數
                emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)

                # 第i個time_step循環時, 轉移到next_tag標籤的轉移概率
                trans_score = self.transitions[next_tag].view(1, -1)

                # 將前向矩陣, 轉移矩陣, 發射矩陣累加
                next_tag_var = forward_var + trans_score + emit_score

                # 計算log_sum_exp()函數值
                # a = log_sum_exp(next_tag_var), 注意: log_sum_exp()函數僅僅返回一個實數值
                # print(a.shape) : tensor(1.0975) , ([])
                # b = a.view(1) : tensor([1.0975]), 注意: a.view(1)的操作就是將一個數字變成一個一階矩陣, ([]) -> ([1])
                # print(b.shape) : ([1])
                alphas_t.append(log_sum_exp(next_tag_var).view(1))
            # print(alphas_t) : [tensor([337.6004], grad_fn=<ViewBackward>), tensor([337.0469], grad_fn=<ViewBackward>), tensor([337.8497], grad_fn=<ViewBackward>), tensor([337.8668], grad_fn=<ViewBackward>), tensor([338.0186], grad_fn=<ViewBackward>), tensor([-9662.2734], grad_fn=<ViewBackward>), tensor([337.8692], grad_fn=<ViewBackward>)]
            # temp = torch.cat(alphas_t)
            # print(temp) : tensor([[  337.6004,   337.0469,   337.8497,   337.8668,   338.0186, -9662.2734, 337.8692]])
            # 將列表張量轉變爲二維張量
            forward_var = torch.cat(alphas_t).view(1, -1)
            # print(forward_var.shape) : [1, 7]
        # print(forward_var) : tensor([[   13.7928,    16.0067,    14.1092, -9984.7852,    15.8380]])
        # 添加最後一步轉移到"STOP_TAG"的分數, 就完成了整條語句的分數計算
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        # print(terminal_var) : tensor([[  339.2167,   340.8612,   340.2773,   339.0194,   340.8908, -9659.5732, -9660.0527]])
        # 計算log_sum_exp()函數值, 作爲一條樣本語句的最終得分
        alpha = log_sum_exp(terminal_var)
        # print(alpha) : tensor(341.9394)
        # 將得分添加進結果列表中, 作爲函數結果返回
        result[0][idx] = alpha
        idx += 1
    return result
  • 輸入參數:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1
# 初始化的示例語句, 共8行, 可以理解爲當前批次batch_size=8
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]

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

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

        forward_score = model._forward_alg(feats)
        print(forward_score)
  • 輸出效果:
tensor([[ 44.0279,  87.6439, 132.7635, 176.7535, 221.1325, 265.4456, 309.8346,
         355.9332]], grad_fn=<CopySlices>)
  • 第四步: 計算損失函數第二項的分值
def _score_sentence(self, feats, tags):
    # feats: [20, 8, 7] , tags: [8, 20]
    # 初始化一個0值的tensor, 爲後續累加做準備
    score = torch.zeros(1)
    # 將START_TAG和真實標籤tags做列維度上的拼接
    temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long)
    tags = torch.cat((temp, tags), dim=1)

    # 將傳入的feats形狀轉變爲[bathc_size, sequence_length, tagset_size]
    feats = feats.transpose(1, 0)
    # feats: [8, 20, 7]
    idx = 0

    # 初始化最終的結果分數張量, 每一個句子得到一個分數
    result = torch.zeros((1, self.batch_size))
    for feat_line in feats:
        # 注意: 此處區別於第三步的循環, 最重要的是這是在真實標籤指導下的轉移矩陣和發射矩陣的累加分數
        for i, feat in enumerate(feat_line):
            score = score + self.transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]
        # 最後加上轉移到STOP_TAG的分數
        score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]
        result[0][idx] = score
        idx += 1
        score = torch.zeros(1)
    return result
  • 輸入參數:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1

# 初始化的示例語句, 共8行, 可以理解爲當前批次batch_size=8
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]

# 真實標籤數據, 對應爲tag_to_ix中的數字標籤
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 將標籤轉爲標量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 調用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

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

        feats = model._get_lstm_features(sentence_sequence)

        gold_score = model._score_sentence(feats, tags)
        print(gold_score)
  • 輸出效果:
tensor([[ 5.3102,  9.0228, 14.7486, 19.5984, 32.4324, 37.9789, 57.8647, 66.8853]],
       grad_fn=<CopySlices>)
  • 第五步: 維特比算法的實現
# 根據傳入的語句特徵feats, 推斷出標籤序列
def _viterbi_decode(self, feats):
    # 初始化最佳路徑結果的存放列表
    result_best_path = []
    # 將輸入張量變形爲[batch_size, sequence_length, tagset_size]
    feats = feats.transpose(1, 0)

    # 對批次中的每一行語句進行遍歷, 每個語句產生一個最優標註序列
    for feat_line in feats:
        backpointers = []

        # 初始化前向傳播的張量, 設置START_TAG等於0, 約束合法序列只能從START_TAG開始
        init_vvars = torch.full((1, self.tagset_size), -10000.)
        init_vvars[0][self.tag_to_ix[START_TAG]] = 0

        # 在第i個time_step, 張量forward_var保存第i-1個time_step的viterbi變量
        forward_var = init_vvars

        # 依次遍歷i=0, 到序列最後的每一個time_step
        for feat in feat_line:
            # 保存當前time_step的回溯指針
            bptrs_t = []
            # 保存當前time_step的viterbi變量
            viterbivars_t = []

            for next_tag in range(self.tagset_size):
                # next_tag_var[i]保存了tag_i 在前一個time_step的viterbi變量
                # 前向傳播張量forward_var加上從tag_i轉移到next_tag的分數, 賦值給next_tag_var
                # 注意此處沒有加發射矩陣分數, 因爲求最大值不需要發射矩陣
                next_tag_var = forward_var + self.transitions[next_tag]

                # 將最大的標籤id加入到當前time_step的回溯列表中
                best_tag_id = argmax(next_tag_var)
                bptrs_t.append(best_tag_id)
                viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))

            # 此處再將發射矩陣分數feat加上, 賦值給forward_var, 作爲下一個time_step的前向傳播張量
            forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)

            # 當前time_step的回溯指針添加進當前這一行樣本的總體回溯指針中
            backpointers.append(bptrs_t)

        # 最後加上轉移到STOP_TAG的分數
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        best_tag_id = argmax(terminal_var)
        # path_score是整個路徑的總得分
        path_score = terminal_var[0][best_tag_id]

        # 根據回溯指針, 解碼最佳路徑
        # 首先把最後一步的id值加入
        best_path = [best_tag_id]
        # 從後向前回溯最佳路徑
        for bptrs_t in reversed(backpointers):
            # 通過第i個time_step得到的最佳id, 找到第i-1個time_step的最佳id
            best_tag_id = bptrs_t[best_tag_id]
            best_path.append(best_tag_id)

        # 將START_TAG刪除
        start = best_path.pop()
        # 確認一下最佳路徑中的第一個標籤是START_TAG
        assert start == self.tag_to_ix[START_TAG]

        # 因爲是從後向前回溯, 所以再次逆序得到總前向後的真實路徑
        best_path.reverse()
        # 當前這一行的樣本結果添加到最終的結果列表裏
        result_best_path.append(best_path)

    return result_best_path
  • 輸入參數:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1

# 初始化的示例語句, 共8行, 可以理解爲當前批次batch_size=8
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]

# 真實標籤數據, 對應爲tag_to_ix中的數字標籤
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 將標籤轉爲標量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 調用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

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

        feats = model._get_lstm_features(sentence_sequence)

        result_tags = model._viterbi_decode(feats)
        print(result_tags)
  • 輸出效果:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]]
  • 第六步: 完善BiLSTM類的全部功能
# 對數似然函數的計算, 輸入的是數字化編碼後的語句, 和真實的標籤
# 注意: 這個函數是未來真實訓練中要用到的"虛擬化的forward()"
def neg_log_likelihood(self, sentence, tags):
    # 第一步先得到BiLSTM層的輸出特徵張量
    feats = self._get_lstm_features(sentence)

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

    # forward_score 代表公式推導中損失函數loss的第一項
    forward_score = self._forward_alg(feats)

    # gold_score 代表公式推導中損失函數loss的第二項
    gold_score = self._score_sentence(feats, tags)

    # 按行求和, 在torch.sum()函數值中, 需要設置dim=1 ; 同理, dim=0代表按列求和
    # 注意: 在這裏, 通過forward_score和gold_score的差值來作爲loss, 用來梯度下降訓練模型
    return torch.sum(forward_score - gold_score, dim=1)

# 此處的forward()真實場景是用在預測部分, 訓練的時候並沒有用到
def forward(self, sentence):
    # 獲取從BiLSTM層得到的發射矩陣
    lstm_feats = self._get_lstm_features(sentence)

    # 通過維特比算法直接解碼最佳路徑
    tag_seq = self._viterbi_decode(lstm_feats)
    return tag_seq
  •  輸入參數:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 標籤和序號的對應碼錶
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 詞嵌入的維度
EMBEDDING_DIM = 200
# 隱藏層神經元的數量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 設置最大語句限制長度
SENTENCE_LENGTH = 20
# 默認神經網絡的層數
NUM_LAYERS = 1

# 初始化的示例語句, 共8行, 可以理解爲當前批次batch_size=8
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]

# 真實標籤數據, 對應爲tag_to_ix中的數字標籤
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 將標籤轉爲標量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 調用:
if __name__ == '__main__':
    for sentence in sentence_list:
        for _char in sentence:
            if _char not in char_to_id:
                char_to_id[_char] = len(char_to_id)
    sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)
    model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \
                       hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \
                       sequence_length=SENTENCE_LENGTH)

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

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

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

        loss.backward()
        optimizer.step()

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

6.5 模型訓練

  • 學習目標:
    • 掌握數據的預處理流程
    • 掌握生成批量訓練數據的方法
    • 掌握模型訓練代碼
  • 模型訓練的流程
    • 第一步: 熟悉字符到數字編碼的碼錶
    • 第二步: 熟悉訓練數據集的樣式和含義解釋
    • 第三步: 生成批量訓練數據
    • 第四步: 完成準確率和召回率的評估代碼
    • 第五步: 完成訓練模型的代碼
    • 第六步: 繪製損失曲線和評估曲線圖
  • 第一步: 熟悉字符到數字編碼的碼錶.
# 代表了數據集中所有字符到數字編碼的字典映射
# 碼錶可以包含中文簡體、繁體、英文大小寫字母、數字、中英文標點符號等等
# <PAD>爲填充標識, 訓練時需要將句子轉化成矩陣, 而句子長短不一, 需要做padding處理

{
    "<PAD>": 0,
    "厑": 1,
    "吖": 2,
    "呵": 3,
    "啊": 4,
    "嗄": 5,
    "嬶": 6,
    ...
}
  •  第二步: 熟悉訓練數據集的樣式和含義解釋.
蕁   B-dis
麻   I-dis
疹   I-dis
這   O
麼   O
癢   O
咋   O
辦   O
。   O

突   O
然   O
頭   B-sym
暈   I-sym
嘔   B-sym
吐   I-sym
。   O
  • 訓練數據集的含義解釋:
    • 每一行包含一個字以及與之對應的標籤, 字與標籤之間通過\t分隔
    • 句子與句子之間通過空行分隔
    • 標籤說明:
      • B-dis: 疾病實體名詞起始標識
      • I-dis: 疾病實體名詞中間到結尾標識
      • B-sym: 症狀實體名詞起始標識
      • I-sym: 症狀實體名詞中間到結尾標識
      • O: 其他非實體部分標識
  • 將訓練數據集轉換爲數字化編碼集:
# 導入包
import json
import numpy as np

# 創建訓練數據集, 從原始訓練文件中將中文字符進行數字編碼, 並將標籤頁進行數字編碼
def create_train_data(train_data_file, result_file, json_file, tag2id, max_length=20):
    # 導入json格式的中文字符到id的映射表
    char2id = json.load(open(json_file, mode='r', encoding='utf-8'))

    char_data, tag_data = [], []

    # 打開原始訓練文件
    with open(train_data_file, mode='r', encoding='utf-8') as f:
        # 初始化一條語句數字化編碼後的列表
        char_ids = [0] * max_length
        tag_ids = [0] * max_length
        idx = 0
        for line in f.readlines():
            line = line.strip('\n').strip()
            # 如果不是空行, 並且當前語句長度沒有超過max_length, 則進行字符到id的映射
            if len(line) > 0 and line and idx < max_length:
                ch, tag = line.split('\t')
                # 如果當前字符存在於映射表中, 則直接映射爲對應的id值
                if char2id.get(ch):
                    char_ids[idx] = char2id[ch]
                # 否則直接用"UNK"的id值來代替這個未知字符
                else:
                    char_ids[idx] = char2id['UNK']
                # 將標籤也進行對應的轉換
                tag_ids[idx] = tag2id[tag]
                idx += 1
            # 如果是空行, 或者當前語句長度超過max_length
            else:
                # 如果當前語句長度超過max_length, 直接將[0: max_langth]的部分作爲結果
                if idx <= max_length:
                    char_data.append(char_ids)
                    tag_data.append(tag_ids)
                # 遇到空行, 說明當前句子已經結束, 初始化清零, 爲下一個句子的映射做準備
                char_ids = [0] * max_length
                tag_ids = [0] * max_length
                idx = 0

    # 將數字化編碼後的數據封裝成numpy的數組類型, 數字編碼採用np.int32
    x_data = np.array(char_data, dtype=np.int32)
    y_data = np.array(tag_data, dtype=np.int32)

    # 直接利用np.savez()將數據存儲爲.npz類型的文件
    np.savez(result_file, x_data=x_data, y_data=y_data)
    print("create_train_data Finished!".center(100, "-"))
  • 輸入參數:
# 參數1:字符碼錶文件路
json_file = './data/char_to_id.json'

# 參數2:標籤碼錶對照字典
tag2id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}

# 參數3:訓練數據文件路徑
train_data_file = './data/train.txt'

# 參數4:創建的npz文件保路徑(訓練數據)
result_file = './data/train.npz'
  • 調用:
if __name__ == '__main__':
    create_train_data(train_data_file, result_file, json_file, tag2id)
  • 第三步: 生成批量訓練數據.
# 導入相關的包
import numpy as np
import torch
import torch.utils.data as Data

# 生成批量訓練數據
def load_dataset(data_file, batch_size):
    # 將第二步生成的train.npz文件導入內存
    data = np.load(data_file)

    # 分別取出特徵值和標籤
    x_data = data['x_data']
    y_data = data['y_data']

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

    # 將數據封裝成Tensor數據集
    dataset = Data.TensorDataset(x, y)

    total_length = len(dataset)

    # 採用80%的數據作爲訓練集, 20%的數據作爲測試集
    train_length = int(total_length * 0.8)
    validation_length = total_length - train_length

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

    # 將訓練集進行DataLoader封裝
    # 參數說明如下:
    # dataset:     訓練數據集
    # batch_size:  代表批次大小, 若數據集總樣本數量無法被batch_size整除, 則最後一批數據爲餘數
    #              若設置drop_last爲True, 則自動抹去最後不能被整除的剩餘批次
    # shuffle:     是否每個批次爲隨機抽取, 若爲True, 則每次迭代時數據爲隨機抽取
    # num_workers: 設定有多少子進程用來做數據加載, 默認爲0, 即數據將被加載到主進程中
    # drop_last:   是否去除不能被整除後的最後批次, 若爲True, 則不生成最後不能被整除剩餘的數據內容
    #              例如: dataset長度爲1028, batch_size爲8,
    #              若drop_last=True, 則最後剩餘的4(1028/8=128餘4)條數據將被拋棄不用
    train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,
                                   shuffle=True, num_workers=4, drop_last=True)

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

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

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

    return data_loaders, data_size
  • 輸入參數:
# 批次大小
BATCH_SIZE = 8

# 編碼後的訓練數據文件路徑
DATA_FILE = './data/train.npz'
  • 調用:
if __name__ == '__main__':
    data_loader, data_size = load_dataset(DATA_FILE, BATCH_SIZE)
    print('data_loader:', data_loader, '\ndata_size:', data_size)
  • 輸出效果:
data_loader: {'train': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb3d0>, 'validation': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb5d0>} 
data_size: {'train': 5368, 'validation': 1343}
  • 第四步: 完成準確率和召回率的評估代碼.
# 評估模型的準確率, 召回率, F1, 等指標
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):
    '''
    sentence_list: 文本向量化後的句子向量列表
    true_tag:      真實的標籤
    predict_tag:   模型預測的標籤
    id2char:       id值到中文字符的映射表
    id2tag:        id值到標籤的映射表
    '''
    # 初始化真實的命名實體, 預測的命名實體, 接下來比較兩者來評估各項指標
    true_entities, true_entity = [], []
    predict_entities, predict_entity = [], []

    # 逐條遍歷批次中所有的語句
    for line_num, sentence in enumerate(sentence_list):
        # 遍歷一條樣本語句中的每一個字符編碼(這裏面是數字化編碼)
        for char_num in range(len(sentence)):
            # 編碼爲0, 表示後面都是填充的0, 可以結束for循環
            if sentence[char_num]==0:
                break

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

            # 對真實標籤進行命名實體的匹配
            # 如果第一個字符是"B", 表示一個實體的開始, 將"字符/標籤"的格式添加進實體列表中
            if true_tag_type[0] == "B":
                true_entity = [char_text + "/" + true_tag_type]
            # 如果第一個字符是"I", 表示處於一個實體的中間
            # 如果真實命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
            # 意思就是比如true_entity = ["中/B-Person", "國/I-Person"], 此時的"人/I-Person"就可以添加進去, 因爲都屬於同一個命名實體
            elif true_tag_type[0] == "I" and len(true_entity) != 0 and true_entity[-1].split("/")[1][1:] == true_tag_type[1:]:
                true_entity.append(char_text + "/" + true_tag_type)
            # 如果第一個字符是"O", 並且true_entity非空, 表示一個命名實體的匹配結束了
            elif true_tag_type[0] == "O" and len(true_entity) != 0 :
                # 最後增加進去一個"行號_列號", 作爲區分實體的標誌
                true_entity.append(str(line_num) + "_" + str(char_num))
                # 將這個匹配出來的實體加入到結果列表中
                true_entities.append(true_entity)
                # 清空true_entity, 爲下一個命名實體的匹配做準備
                true_entity=[]
            # 除了上面三種情況, 說明當前沒有匹配出任何命名實體, 則清空true_entity, 繼續下一次匹配
            else:
                true_entity=[]

            # 對預測標籤進行命名實體的匹配
            # 如果第一個字符是"B", 表示一個實體的開始, 將"字符/預測標籤"的格式添加進實體列表中
            if predict_tag_type[0] == "B":
                predict_entity = [char_text + "/" + predict_tag_type]
            # 如果第一個字符是"I", 表示處於一個實體的中間
            # 如果預測命名實體列表非空, 並且最後一個添加進去的標籤類型和當前的標籤類型一樣, 則繼續添加
            # 意思就是比如predict_entity = ["中/B-Person", "國/I-Person"], 此時的"人/I-Person"就可以添>加進去, 因爲都屬於同一個命名實體
            elif predict_tag_type[0] == "I" and len(predict_entity) != 0 and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:
                predict_entity.append(char_text + "/" + predict_tag_type)
            # 如果第一個字符是"O", 並且predict_entity非空, 表示一個命名實體的匹配結束了
            elif predict_tag_type[0] == "O" and len(predict_entity) != 0:
                # 最後增加進去一個"行號_列號", 作爲區分實體的標誌
                predict_entity.append(str(line_num) + "_" + str(char_num))
                # 將這個匹配出來的實體加入到結果列表中
                predict_entities.append(predict_entity)
                # 清空predict_entity, 爲下一個命名實體的匹配做準備
                predict_entity = []
            # 除了上面三種情況, 說明當前沒有匹配出任何命名實體, 則清空predict_entity, 繼續下一次匹配
            else:
                predict_entity = []

    # 遍歷所有預測實體的列表, 只有那些在真實命名實體中的纔是正確的
    acc_entities = [entity for entity in predict_entities if entity in true_entities]

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

    # 至少正確預測了一個, 才計算3個指標, 準確率
    if acc_entities_length > 0:
        accuracy = float(acc_entities_length / predict_entities_length)
        recall = float(acc_entities_length / true_entities_length)
        f1_score = 2 * accuracy * recall / (accuracy + recall)
        return accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length
    else:
        return 0, 0, 0, acc_entities_length, predict_entities_length, true_entities_length 
  • 輸入參數:
# 真實標籤數據
tag_list = [
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

# 預測標籤數據
predict_tag_list = [
    [0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],
    [0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0],
    [3, 4, 0, 3, 4, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

# 編碼與字符對照字典
id2char = {0: '<PAD>', 1: '確', 2: '診', 3: '彌', 4: '漫', 5: '大', 6: 'b', 7: '細', 8: '胞', 9: '淋', 10: '巴', 11: '瘤', 12: '1', 13: '年', 14: '反', 15: '復', 16: '咳', 17: '嗽', 18: '、', 19: '痰', 20: '4', 21: '0', 22: ',', 23: '再', 24: '發', 25: '伴', 26: '氣', 27: '促', 28: '5', 29: '天', 30: '。', 31: '生', 32: '長', 33: '育', 34: '遲', 35: '緩', 36: '9', 37: '右', 38: '側', 39: '小', 40: '肺', 41: '癌', 42: '第', 43: '三', 44: '次', 45: '化', 46: '療', 47: '入', 48: '院', 49: '心', 50: '悸', 51: '加', 52: '重', 53: '胸', 54: '痛', 55: '3', 56: '悶', 57: '2', 58: '多', 59: '月', 60: '餘', 61: ' ', 62: '周', 63: '上', 64: '肢', 65: '無', 66: '力', 67: '肌', 68: '肉', 69: '萎', 70: '縮', 71: '半'}

# 編碼與標籤對照字典
id2tag = {0: 'O', 1: 'B-dis', 2: 'I-dis', 3: 'B-sym', 4: 'I-sym'}

# 輸入的數字化sentences_sequence, 由下面的sentence_list經過映射函數sentence_map()轉化後得到
sentence_list = [
    "確診瀰漫大b細胞淋巴瘤1年",
    "反覆咳嗽、咳痰40年,再發伴氣促5天。",
    "生長髮育遲緩9年。",
    "右側小細胞肺癌第三次化療入院",
    "反覆氣促、心悸10年,加重伴胸痛3天。",
    "反覆胸悶、心悸、氣促2多月,加重3天",
    "咳嗽、胸悶1月餘, 加重1周",
    "右上肢無力3年, 加重伴肌肉萎縮半年"
]
  • 調用:
def sentence_map(sentence_list, char_to_id, max_length):
    sentence_list.sort(key=lambda c:len(c), reverse=True)
    sentence_map_list = []
    for sentence in sentence_list:
        sentence_id_list = [char_to_id[c] for c in sentence]
        padding_list = [0] * (max_length-len(sentence))
        sentence_id_list.extend(padding_list)
        sentence_map_list.append(sentence_id_list)
    return torch.tensor(sentence_map_list, dtype=torch.long)

char_to_id = {"<PAD>":0}

SENTENCE_LENGTH = 20

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

sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)


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

    print("accuracy:",                  accuracy,
          "\nrecall:",                  recall,
          "\nf1_score:",                f1_score,
          "\nacc_entities_length:",     acc_entities_length,
          "\npredict_entities_length:", predict_entities_length,
          "\ntrue_entities_length:",    true_entities_length)
  • 輸出效果:
step_acc: 0.8823529411764706 
step_recall: 0.9375 
f1_score: 0.9090909090909091 
acc_entities_length: 15 
predict_entities_length: 17 
true_entities_length: 16
  • 第五步: 完成訓練模型的代碼.
# 導入包
import json
import time
from tqdm import tqdm
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
from torch.autograd import Variable
# 導入之前編寫好的包, 包括類, 數據集加載, 評估函數
from bilstm_crf import BiLSTM_CRF
from loader_data import load_dataset
from evaluate_model import evaluate

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

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

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

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

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

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

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

        # 當前epochs訓練後更新學習率, 必須在優化器更新之後
        scheduler.step()

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


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

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

				# 訓練日誌內容
				log_text = "Epoch: %s | Step: %s " \
						   "| loss: %.5f " \
						   "| acc: %.5f " \
						   "| recall: %.5f " \
						   "| f1 score: %.5f" % \
						   (epoch, step, step_loss, step_acc, step_recall,f1_score)
				# 分別累計正確總實體數、識別實體數以及真實實體數
				total_acc_entities_length += acc_entities_length
				total_predict_entities_length += predict_entities_length
				total_gold_entities_length += gold_entities_length

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

			# 獲取當前批次平均損失值(每一批次損失總值除以數據量)
			epoch_loss = total_loss / data_size["validation"]
			# 計算總批次準確率
			total_acc = total_acc_entities_length / total_predict_entities_length
			# 計算總批次召回率
			total_recall = total_acc_entities_length / total_gold_entities_length
			# 計算總批次F1值
			total_f1 = 0
			if total_acc + total_recall != 0:
				total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)
			log_text = "Epoch: %s " \
					   "| mean loss: %.5f " \
					   "| total acc: %.5f " \
					   "| total recall: %.5f " \
					   "| total f1 scroe: %.5f" % (epoch, epoch_loss,
												   total_acc,
												   total_recall,
												   total_f1)

			# 記錄當前批次驗證loss值(用於圖表展示)準確率, 召回率, f1值
			validate_loss_list.append(epoch_loss)
			validate_acc_list.append(total_acc)
			validate_recall_list.append(total_recall)
			validate_f1_list.append(total_f1)
			validate_log_file.write(log_text + "\n")


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

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


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

    • 訓練和驗證損失對照曲線:

  • 分析: 損失對照曲線一直下降, 從第5個epoch開始, 迅速降到比較理想的位置, 說明模型能夠從數據中獲取規律了, 到第40個批次之後, 模型趨於穩定, 說明參數基本能夠已經得到最優化效果, 此時, 根據對scheduler的設置, 通過該方法已經對優化器進行了近8次的迭代, 應該在我們原本設置的初始學習率基礎上縮小了0.2的8次方倍, 此時應該找到了當前最優解, 因此也就趨於穩定了.
  • 訓練和驗證準確率對照曲線:

  • 分析:
  • 首先,準確率是指識別正確的實體識別出的實體中的比例.
  • 根據對照曲線來看,整體學習結果都在趨於準確率上升方向增加,而且隨着批次的增加曲線震動相對平穩,不過可能由於訓練與驗證樣本分佈不均衡或者噪聲等原因,導致最終驗證集的準確度沒有達到與訓練集相同的情況.
  • 最終的訓練集和驗證集的召回率分別在:0.85和0.78左右.

  • 訓練和驗證召回率對照曲線:

  • 分析:
  • 在此召回率是指識別正確的實體佔當前批次所包含的所有實體總數的比例.
  • 關於訓練和驗證召回率對照曲線,可以看出召回率的變化相對比較平滑,基本上也在40步左右趨於穩定.
  • 最終的訓練集和驗證集的召回率分別在:0.83和0.75左右.
  • 訓練和驗證F1值對照曲線:

 

  • 分析:
  • F1值主要是指訓練效果而言,在不多識別實體的情況下同時提高準確度的衡量指標.
  • 其公式爲:2×準確率×召回率 / (準確率+召回率)
  • 從曲線可見整體F1值上升與損失、召回率的曲線比較接近,說明在識別出的實體中,正確率比較問題,不過根據前面的準確度來分析,可能在識別過程中,增加了識別出的實體個數而導致不穩定。從這方面來說,可以驗證樣本不均衡問題以及噪聲對模型的影響還是比較大的。
  • 從整體而言,F1值基本也在第40步之後趨於穩定,最終的訓練集和驗證集的結果在:0.85和0.75左右。
  • 小節總結:
    • 學習了數據預處理的相關方法
      • 原始數據集的字符經過數字化編碼變成向量
      • 標註數據集的字符經過數字化編碼變成向量
    • 學習生成批量訓練數據的方法
    • 學習了模型訓練相關代碼的實現
      • 準確率和召回率評估的代碼
      • 模型構建類的全部內部函數代碼
      • 啓動訓練流程的代碼

6.6 模型使用

  • 學習目標:
    • 掌握模型單條文本預測代碼實現
    • 掌握批量文件夾文件預測代碼實現
  • 模型單條文本預測代碼實現:
import os
import torch
import json
from bilstm_crf import BiLSTM_CRF

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

    char_to_id = json.load(open(char_to_id_json_path, mode="r", encoding="utf-8"))
    # 將字符串轉爲碼錶id列表
    char_ids = content_to_id(content, char_to_id)
    # 處理成 batch_size * sentence_length 的 tensor 數據
    # 定義模型輸入列表
    model_inputs_list, model_input_map_list = build_model_input_list(content,
                                                                     char_ids,
                                                                     batch_size,
                                                                     sentence_length,
                                                                     offset)
    # 加載模型
    model = BiLSTM_CRF(vocab_size=len(char_to_id),
                       tag_to_ix=tag2id,
                       embedding_dim=embedding_dim,
                       hidden_dim=hidden_dim,
                       batch_size=batch_size,
                       num_layers=num_layers,
                       sequence_length=sentence_length)
    # 加載模型字典
    model.load_state_dict(torch.load(model_path))

    tag_id_dict = {v: k for k, v in tag_to_id.items() if k[2:] in target_type_list}
    # 定義返回實體列表
    entities = []
    with torch.no_grad():
        for step, model_inputs in enumerate(model_inputs_list):
            prediction_value = model(model_inputs)
            # 獲取每一行預測結果
            for line_no, line_value in enumerate(prediction_value):
                # 定義將要識別的實體
                entity = None
                # 獲取當前行每個字的預測結果
                for char_idx, tag_id in enumerate(line_value):
                    # 若預測結果 tag_id 屬於目標字典數據 key 中
                    if tag_id in tag_id_dict:
                        # 取符合匹配字典id的第一個字符,即B, I
                        tag_index = tag_id_dict[tag_id][0]
                        # 計算當前字符確切的下標位置
                        current_char = model_input_map_list[step][line_no][char_idx]
                        # 若當前字標籤起始爲 B, 則設置爲實體開始
                        if tag_index == "B":
                            entity = current_char
                        # 若當前字標籤起始爲 I, 則進行字符串追加
                        elif tag_index == "I" and entity:
                            entity += current_char
                    # 當實體不爲空且當前標籤類型爲 O 時,加入實體列表
                    if tag_id == tag_to_id["O"] and entity:
                        # 滿足當前字符爲O,上一個字符爲目標提取實體結尾時,將其加入實體列表
                        entities.append(entity)
                        # 重置實體
                        entity = None
    return entities


def content_to_id(content, char_to_id):
    # 定義字符串對應的碼錶 id 列表
    char_ids = []
    for char in list(content):
        # 判斷若字符不在碼錶對應字典中,則取 NUK 的編碼(即 unknown),否則取對應的字符編碼
        if char_to_id.get(char):
            char_ids.append(char_to_id[char])
        else:
            char_ids.append(char_to_id["UNK"])
    return char_ids


def build_model_input_list(content, char_ids, batch_size, sentence_length, offset):
    # 定義模型輸入數據列表
    model_input_list = []
    # 定義每個批次句子 id 數據
    batch_sentence_list = []
    # 將文本內容轉爲列表
    content_list = list(content)
    # 定義與模型 char_id 對照的文字
    model_input_map_list = []
    #  定義每個批次句子字符數據
    batch_sentence_char_list = []
    # 判斷是否需要 padding
    if len(char_ids) % sentence_length > 0:
        # 將不足 batch_size * sentence_length 的部分填充0
        padding_length = (batch_size * sentence_length
                          - len(char_ids) % batch_size * sentence_length
                          - len(char_ids) % sentence_length)
        char_ids.extend([0] * padding_length)
        content_list.extend(["#"] * padding_length)
    # 迭代字符 id 列表
    # 數據滿足 batch_size * sentence_length 將加入 model_input_list
    for step, idx in enumerate(range(0, len(char_ids) + 1, sentence_length)):
        # 起始下標,從第一句開始增加 offset 個字的偏移
        start_idx = 0 if idx == 0 else idx - step * offset
        # 獲取長度爲 sentence_length 的字符 id 數據集
        sub_list = char_ids[start_idx:start_idx + sentence_length]
        # 獲取長度爲 sentence_length 的字符數據集
        sub_char_list = content_list[start_idx:start_idx + sentence_length]
        # 加入批次數據集中
        batch_sentence_list.append(sub_list)
        # 批量句子包含字符列表
        batch_sentence_char_list.append(sub_char_list)
        # 每當批次長度達到 batch_size 時候,將其加入 model_input_list
        if len(batch_sentence_list) == batch_size:
            # 將數據格式轉爲 tensor 格式,大小爲 batch_size * sentence_length
            model_input_list.append(torch.tensor(batch_sentence_list))
            # 重置 batch_sentence_list
            batch_sentence_list = []
            # 將 char_id 對應的字符加入映射表中
            model_input_map_list.append(batch_sentence_char_list)
            # 重置批字符串內容
            batch_sentence_char_list = []
    # 返回模型輸入列表
    return model_input_list, model_input_map_list

  • 輸入參數:
# 參數1:待識別文本
content = "本病是由DNA病毒的單純皰疹病毒所致。人類單純皰疹病毒分爲兩型," \
"即單純皰疹病毒Ⅰ型(HSV-Ⅰ)和單純皰疹病毒Ⅱ型(HSV-Ⅱ)。" \
"Ⅰ型主要引起生殖器以外的皮膚黏膜(口腔黏膜)和器官(腦)的感染。" \
"Ⅱ型主要引起生殖器部位皮膚黏膜感染。" \
"病毒經呼吸道、口腔、生殖器黏膜以及破損皮膚進入體內," \
"潛居於人體正常黏膜、血液、唾液及感覺神經節細胞內。" \
"當機體抵抗力下降時,如發熱胃腸功能紊亂、月經、疲勞等時," \
"體內潛伏的HSV被激活而發病。"
# 參數2:模型保存文件路徑
model_path = "model/bilstm_crf_state_dict_20200129_210417.pt"
# 參數3:批次大小
BATCH_SIZE = 8
# 參數4:字向量維度
EMBEDDING_DIM = 300
# 參數5:隱層維度
HIDDEN_DIM = 128
# 參數6:句子長度
SENTENCE_LENGTH = 100
# 參數7:偏移量
OFFSET = 10
# 參數8:標籤碼錶對照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數9:字符碼錶文件路徑
char_to_id_json_path = "./data/char_to_id.json"
# 參數10:預測結果存儲路徑
prediction_result_path = "prediction_result"
# 參數11:待匹配標籤類型
target_type_list = ["sym"]
  • 調用:
# 單獨文本預測, 獲得實體結果
entities = singel_predict(model_path,
                          content,
                          char_to_id_json_path,
                          BATCH_SIZE,
                          EMBEDDING_DIM,
                          HIDDEN_DIM,
                          SENTENCE_LENGTH,
                          OFFSET,
                          target_type_list,
                          tag_to_id)
# 打印實體結果
print("entities:\n", entities)
  • 輸出效果:
entities:
['感染', '發熱', '##']
  • 批量文件夾文件預測代碼實現:
def batch_predict(data_path, model_path, char_to_id_json_path, batch_size, embedding_dim,
                  hidden_dim, sentence_length, offset, target_type_list,
                  prediction_result_path, tag_to_id):
    """
    description: 批量預測,查詢文件目錄下數據, 
                 從中提取符合條件的實體並存儲至新的目錄下prediction_result_path
    :param data_path:               數據文件路徑
    :param model_path:              模型文件路徑
    :param char_to_id_json_path:    字符碼錶文件路徑
    :param batch_size:              訓練批次大小
    :param embedding_dim:           字向量維度
    :param hidden_dim:              BiLSTM 隱藏層向量維度
    :param sentence_length:         句子長度(句子做了padding)
    :param offset:                  設定偏移量, 
                                    當字符串超出sentence_length時, 換行時增加偏移量
    :param target_type_list:        待匹配類型,符合條件的實體將會被提取出來
    :param prediction_result_path:  預測結果保存路徑
    :param tag_to_id:               標籤碼錶對照字典, 標籤對應 id
    :return:                        無返回
    """
    # 迭代路徑, 讀取文件名
    for fn in os.listdir(data_path):
        # 拼裝全路徑
        fullpath = os.path.join(data_path, fn)
        # 定義輸出結果文件
        entities_file = open(os.path.join(prediction_result_path, fn),
                             mode="w",
                             encoding="utf-8")
        with open(fullpath, mode="r", encoding="utf-8") as f:
            # 讀取文件內容
            content = f.readline()
            # 調用單個預測模型,輸出爲目標類型實體文本列表
            entities = singel_predict(model_path, content, char_to_id_json_path,
                                      batch_size, embedding_dim, hidden_dim, sentence_length,
                                      offset, target_type_list, tag_to_id)
            # 寫入識別結果文件
            entities_file.write("\n".join(entities))
    print("batch_predict Finished".center(100, "-"))
  • 輸入參數:
# 參數1:模型保存路徑
model_path = "model/bilstm_crf_state_dict_20191219_220256.pt"
# 參數2:批次大小
BATCH_SIZE = 8
# 參數3:字向量維度
EMBEDDING_DIM = 200
# 參數4:隱層維度
HIDDEN_DIM = 100
# 參數5:句子長度
SENTENCE_LENGTH = 20
# 參數6:偏移量
OFFSET = 10
# 參數7:標籤碼錶對照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 參數8:字符碼錶文件路徑
char_to_id_json_path = "./data/char_to_id.json"
# 參數9:預測結果存儲路徑
prediction_result_path = "prediction_result"
# 參數10:待匹配標籤類型
target_type_list = ["sym"]
# 參數11:待預測文本文件所在目錄
data_path = "origin_data"
  • 調用:
# 批量文本預測, 並將結果寫入文件中
batch_predict(data_path,
              model_path,
              char_to_id_json_path,
              BATCH_SIZE,
              EMBEDDING_DIM,
              HIDDEN_DIM,
              SENTENCE_LENGTH,
              OFFSET,
              target_type_list,
              prediction_result_path,
              tag_to_id)
  • 輸出效果: 將識別結果保存至prediction_result_path指定的目錄下, 名稱與源文件一致, 內容爲每行存儲識別實體名稱


 

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

 

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