PyTorch專欄(十九):序列模型和長短句記憶(LSTM)模型 | 文末開獎

  • 2019 年 11 月 5 日
  • 筆記

作者 | News 編輯 | 奇予紀

出品 | 磐創AI團隊出品

【磐創AI 導讀】:查看關於本專欄歷史文章,請點擊文末[閱讀全文]。查看本章歷史文章,請點擊下方藍色字體進入相應鏈接閱讀。

專欄目錄:

第五章:PyTorch之文本篇

序列模型和長短句記憶(LSTM)模型

  • 前饋網絡

之前我們已經學過了許多的前饋網絡。所謂前饋網絡, 就是網絡中不會保存狀態。然而有時這並不是我們想要的效果。在自然語言處理(NLP, Natural Language Processing) 中, 序列模型是一個核心的概念。

  • 序列模型

所謂序列模型, 即輸入依賴於時間信息的模型。一個典型的序列模型是隱馬爾科夫模型 (HMM, Hidden Markov Model)。另一個序列模型的例子是條件隨機場 (CRF, Conditional Random Field)。

  • 循環神經網絡

循環神經網絡是指可以保存某種狀態的神經網絡。比如說, 神經網絡中上個時刻的輸出可以作為下個 時刻的輸入的一部分, 以此信息就可以通過序列在網絡中一直往後傳遞。對於LSTM (Long-Short Term Memory) 來說, 序列中的每個元素都有一個相應的隱狀態

,該隱狀態原則上可以包含序列當前結點之前的任一節點的信息。我們可以使用隱藏狀態來預測語言模型中的單詞, 詞性標籤以及其他。

1.Pytorch中的LSTM

在正式學習之前,有幾個點要說明一下,Pytorch中 LSTM 的輸入形式是一個 3D 的Tensor,每一個維度都有重要的意義,第一個維度就是序列本身,第二個維度是mini-batch中實例的索引,第三個維度是輸入元素的索引,我們之前沒有接觸過mini-batch,所以我們就先忽略它並假設第二維的維度是1。如果要用」The cow jumped」這個句子來運行一個序列模型,那麼就應該把它整理成如下的形式:

除了有一個額外的大小為1的第二維度。

此外, 你還可以向網絡逐個輸入序列, 在這種情況下, 第一個軸的大小也是1。

來看一個簡單的例子。

# Author: Robert Guthrie    import torch  import torch.nn as nn  import torch.nn.functional as F  import torch.optim as optim    torch.manual_seed(1)
lstm = nn.LSTM(3, 3)  # 輸入維度為3維,輸出維度為3維  inputs = [torch.randn(1, 3) for _ in range(5)]  # 生成一個長度為5的序列    # 初始化隱藏狀態.  hidden = (torch.randn(1, 1, 3),            torch.randn(1, 1, 3))  for i in inputs:      # 將序列中的元素逐個輸入到LSTM.      # 經過每步操作,hidden 的值包含了隱藏狀態的信息.      out, hidden = lstm(i.view(1, 1, -1), hidden)    # 另外我們可以對一整個序列進行訓練.  # LSTM第一個返回的第一個值是所有時刻的隱藏狀態  # 第二個返回值是最後一個時刻的隱藏狀態  #(所以"out"的最後一個和"hidden"是一樣的)  # 之所以這樣設計:  # 通過"out"你能取得任何一個時刻的隱藏狀態,而"hidden"的值是用來進行序列的反向傳播運算, 具體方式就是將它作為參數傳入後面的 LSTM 網絡.    # 增加額外的第二個維度.  inputs = torch.cat(inputs).view(len(inputs), 1, -1)  hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3))  # 清空隱藏狀態.  out, hidden = lstm(inputs, hidden)  print(out)  print(hidden)
  • 輸出結果:
tensor([[[-0.0187,  0.1713, -0.2944]],            [[-0.3521,  0.1026, -0.2971]],            [[-0.3191,  0.0781, -0.1957]],            [[-0.1634,  0.0941, -0.1637]],            [[-0.3368,  0.0959, -0.0538]]], grad_fn=<StackBackward>)  (tensor([[[-0.3368,  0.0959, -0.0538]]], grad_fn=<StackBackward>), tensor([[[-0.9825,  0.4715, -0.0633]]], grad_fn=<StackBackward>))

2.例子:用LSTM來進行詞性標註

在這部分, 我們將會使用一個 LSTM 網絡來進行詞性標註。在這裡我們不會用到維特比算法, 前向-後向算法或者任何類似的算法,而是將這部分內容作為一個 (有挑戰) 的練習留給讀者, 希望讀者在了解了這部分的內容後能夠實現如何將維特比算法應用到 LSTM 網絡中來。

該模型如下:輸入的句子是

,其中

,標籤的集合定義為 T,

為單詞

的標籤,用

表示對單詞

詞性的預測。

這是一個結構預測模型, 我們的輸出是一個序列

, 其中

在進行預測時, 需將句子每個詞輸入到一個 LSTM 網絡中。將時刻 i 的隱藏狀態標記為

,同樣地, 對每個標籤賦一個獨一無二的索引 (類似 word embeddings 部分 word_to_ix 的設置). 然後就得到了

的預測規則:

即先對隱狀態進行一個仿射變換, 然後計算一個對數 softmax, 最後得到的預測標籤即為對數 softmax 中最大的值對應的標籤. 注意, 這也意味着 A 空間的維度是|T|。

2.1 準備數據

def prepare_sequence(seq, to_ix):      idxs = [to_ix[w] for w in seq]      return torch.tensor(idxs, dtype=torch.long)    training_data = [      ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),      ("Everybody read that book".split(), ["NN", "V", "DET", "NN"])  ]  word_to_ix = {}  for sent, tags in training_data:      for word in sent:          if word not in word_to_ix:              word_to_ix[word] = len(word_to_ix)  print(word_to_ix)  tag_to_ix = {"DET": 0, "NN": 1, "V": 2}    # 實際中通常使用更大的維度如32維, 64維.  # 這裡我們使用小的維度, 為了方便查看訓練過程中權重的變化.  EMBEDDING_DIM = 6  HIDDEN_DIM = 6
  • 輸出結果:
{'The': 0, 'dog': 1, 'ate': 2, 'the': 3, 'apple': 4, 'Everybody': 5, 'read': 6, 'that': 7, 'book': 8}

2.2 創建模型

class LSTMTagger(nn.Module):        def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):          super(LSTMTagger, self).__init__()          self.hidden_dim = hidden_dim            self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)            # LSTM以word_embeddings作為輸入, 輸出維度為 hidden_dim 的隱藏狀態值          self.lstm = nn.LSTM(embedding_dim, hidden_dim)            # 線性層將隱藏狀態空間映射到標註空間          self.hidden2tag = nn.Linear(hidden_dim, tagset_size)          self.hidden = self.init_hidden()        def init_hidden(self):          # 一開始並沒有隱藏狀態所以我們要先初始化一個          # 關於維度為什麼這麼設計請參考Pytoch相關文檔          # 各個維度的含義是 (num_layers, minibatch_size, hidden_dim)          return (torch.zeros(1, 1, self.hidden_dim),                  torch.zeros(1, 1, self.hidden_dim))        def forward(self, sentence):          embeds = self.word_embeddings(sentence)          lstm_out, self.hidden = self.lstm(              embeds.view(len(sentence), 1, -1), self.hidden)          tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))          tag_scores = F.log_softmax(tag_space, dim=1)          return tag_scores

2.3 訓練模型

model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix), len(tag_to_ix))  loss_function = nn.NLLLoss()  optimizer = optim.SGD(model.parameters(), lr=0.1)    # 查看訓練前的分數  # 注意: 輸出的 i,j 元素的值表示單詞 i 的 j 標籤的得分  # 這裡我們不需要訓練不需要求導,所以使用torch.no_grad()  with torch.no_grad():      inputs = prepare_sequence(training_data[0][0], word_to_ix)      tag_scores = model(inputs)      print(tag_scores)    for epoch in range(300):  # 實際情況下你不會訓練300個周期, 此例中我們只是隨便設了一個值      for sentence, tags in training_data:          # 第一步: 請記住Pytorch會累加梯度.          # 我們需要在訓練每個實例前清空梯度          model.zero_grad()            # 此外還需要清空 LSTM 的隱狀態,          # 將其從上個實例的歷史中分離出來.          model.hidden = model.init_hidden()            # 準備網絡輸入, 將其變為詞索引的 Tensor 類型數據          sentence_in = prepare_sequence(sentence, word_to_ix)          targets = prepare_sequence(tags, tag_to_ix)            # 第三步: 前向傳播.          tag_scores = model(sentence_in)            # 第四步: 計算損失和梯度值, 通過調用 optimizer.step() 來更新梯度          loss = loss_function(tag_scores, targets)          loss.backward()          optimizer.step()    # 查看訓練後的得分  with torch.no_grad():      inputs = prepare_sequence(training_data[0][0], word_to_ix)      tag_scores = model(inputs)        # 句子是 "the dog ate the apple", i,j 表示對於單詞 i, 標籤 j 的得分.      # 我們採用得分最高的標籤作為預測的標籤. 從下面的輸出我們可以看到, 預測得      # 到的結果是0 1 2 0 1. 因為 索引是從0開始的, 因此第一個值0表示第一行的      # 最大值, 第二個值1表示第二行的最大值, 以此類推. 所以最後的結果是 DET      # NOUN VERB DET NOUN, 整個序列都是正確的!      print(tag_scores)
  • 輸出結果:
tensor([[-1.1389, -1.2024, -0.9693],          [-1.1065, -1.2200, -0.9834],          [-1.1286, -1.2093, -0.9726],          [-1.1190, -1.1960, -0.9916],          [-1.0137, -1.2642, -1.0366]])  tensor([[-0.0858, -2.9355, -3.5374],          [-5.2313, -0.0234, -4.0314],          [-3.9098, -4.1279, -0.0368],          [-0.0187, -4.7809, -4.5960],          [-5.8170, -0.0183, -4.1879]])

3.練習:使用字符級特徵來增強 LSTM 詞性標註器

在上面的例子中, 每個詞都有一個詞嵌入, 作為序列模型的輸入. 接下來讓我們使用每個的單詞的 字符級別的表達來增強詞嵌入。

我們期望這個操作對結果能有顯著提升, 因為像詞綴這樣的字符級信息對於詞性有很大的影響。比如說, 像包含詞綴 -ly 的單詞基本上都是被標註為副詞。

具體操作如下:用

的字符級表達, 同之前一樣,我們使用

來表示詞嵌入。序列模型的輸入就變成了

的拼接。因此, 如果

的維度是5,

的維度是3,那麼我們的 LSTM 網絡的輸入維度大小就是8。

為了得到字符級別的表達, 將單詞的每個字符輸入一個 LSTM 網絡, 而

則為這個 LSTM 網絡最後的隱狀態。一些提示:

  • 新模型中需要兩個 LSTM, 一個跟之前一樣, 用來輸出詞性標註的得分, 另外一個新增加的用來獲取每個單詞的字符級別表達。
  • 為了在字符級別上運行序列模型,你需要用嵌入的字符來作為字符 LSTM 的輸入。