Chris Olah's LSTM post Edwin Chen's LSTM post Andrej Karpathy's lecture on RNNs and LSTMs from CS231n
LSTM結構包括幾個門:遺忘門,學習門,記憶門,使用門 這些門的基本工作原理如下:
- 長期記憶進入遺忘門,忘記它認爲沒有用處的一切;
- 短期記憶和事件在學習門裏合併到一起,,並移除掉一切不必要的信息,作爲學到的新信息。
- 還沒遺忘的長期記憶和剛學到的新信息會在記憶門裏合併到一起,這個門把這兩者放到一起,由於它叫記憶門,所以它會輸出更新後的長期記憶
- 最後 使用門會決定要從之前知道的信息以及剛學到的信息中挑出什麼來使用,從而作出預測 ,所以它也接受長期記憶和新信息的輸入,把它們合併到一起並決定要輸出什麼。所以輸出就包括了預測和新的短期記憶
短期記憶輸入進來 我們把它們分別叫做 LTM 和 STM,如下圖所示:
LSTM 結構
LSTMs in Pytorch
In PyTorch an LSTM can be defined as: lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim, num_layers=n_layers)
.
在PyTorch中,LSTM期望其所有輸入都是3D張量,其尺寸定義如下:
-
input_dim
=輸入數量(20的維度可代表20個輸入) -
hidden_dim
=隱藏狀態的大小; 每個LSTM單元在每個時間步產生的輸出數。 -
n_layers
=隱藏LSTM圖層的數量; 通常是1到3之間的值; 值爲1表示每個LSTM單元具有一個隱藏狀態。 其默認值爲1。
隱藏狀態
一旦LSTM輸入和隱藏層維度,就可以調用它並在每個時間步驟檢索輸出和隱藏狀態:
out, hidden = lstm(input.view(1, 1, -1), (h0, c0))
對於LSTM輸入爲:(input, (h0, c0))
.
-
input
= 輸入序列中Tensor; (seq_len,batch,input_size) -
h0
= Tensor,包含批處理中每個元素的初始隱藏狀態 -
c0
= 批次中每個元素的初始單元格內存的Tensor h0和c0默認爲0,如果未指定。 它們的尺寸爲:(n_layers,batch,hidden_dim)。 PyTorch LSTM tutorial.
Example
該LSTM旨在查看4個值的序列,並生成3個值作爲輸出。
import torch import torch.nn as nn import matplotlib.pyplot as plt %matplotlib inline torch.manual_seed(2) # so that random variables will be consistent and repeatable for testing
定義一個簡單的LSTM
除非定義自己的LSTM並通過在網絡的末尾添加線性層(例如,fc=nn.Linear(hidden_dim,output_dim)來改變輸出的數量,否則輸出的“hidden_dim”和輸出大小將是相同的。
from torch.autograd import Variable # define an LSTM with an input dim of 4 and hidden dim of 3 # this expects to see 4 values as input and generates 3 values as output input_dim = 4 hidden_dim = 3 lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim) # make 5 input sequences of 4 random values each inputs_list = [torch.randn(1, input_dim) for _ in range(5)] print('inputs: \n', inputs_list) print('\n') # initialize the hidden state # (1 layer, 1 batch_size, 3 outputs) # first tensor is the hidden state, h0 # second tensor initializes the cell memory, c0 h0 = torch.randn(1, 1, hidden_dim) c0 = torch.randn(1, 1, hidden_dim) h0 = Variable(h0) c0 = Variable(c0) # step through the sequence one element at a time. for i in inputs_list: # wrap in Variable i = Variable(i) # after each step, hidden contains the hidden state out, hidden = lstm(i.view(1, 1, -1), (h0, c0)) print('out: \n', out) print('hidden: \n', hidden)
輸出張量和隱藏張量長度總爲3,這是我們在定義LSTM hidden_dim的時指定的 對於以上的輸出,對於大型數據序列,for循環不是非常有效,所以我們也可以同時處理這些輸入。 1.使用定義的batch_size將所有輸入序列連接成一個大張量 2.定義隱藏狀態的形狀 3.獲取輸出和最近的隱藏狀態(在序列中的最後一個單詞之後創建)
# turn inputs into a tensor with 5 rows of data # add the extra 2nd dimension (1) for batch_size inputs = torch.cat(inputs_list).view(len(inputs_list), 1, -1) # print out our inputs and their shape # you should see (number of sequences, batch size, input_dim) print('inputs size: \n', inputs.size()) print('\n') print('inputs: \n', inputs) print('\n') # initialize the hidden state h0 = torch.randn(1, 1, hidden_dim) c0 = torch.randn(1, 1, hidden_dim) # wrap everything in Variable inputs = Variable(inputs) h0 = Variable(h0) c0 = Variable(c0) # get the outputs and hidden state out, hidden = lstm(inputs, (h0, c0)) print('out: \n', out) print('hidden: \n', hidden)
學習門(The learn gate)
學習門要做的是,取短期記憶和事件將兩者合併 。實際上 ,它做的不止這些:它會接受短期記憶和事件,將兩者合併起來然後忽略其中的一部分,只保留重要的部分。
所以這裏它忘了有棵樹,只記得我們最近看到了一隻松鼠和一條狗/一隻狼。
數學原理
我們有短期記憶 STMt-1,和事件 Et ,學習門會把它們合併起來:也就是把兩個向量放到一起,然後乘以一個矩陣 ,再加一個偏差 ,最後將所得結果代入 tanh 激活函數裏。然後新信息 Nt 就產生了。 那我們要怎麼忽略其中一部分呢? 可以將其乘以一個遺忘因子 it,遺忘因子 it實際上是一個向量 但它可以像元素一樣進行乘法運算。 怎麼計算it呢? 這就要用到短期記憶和事件,再次創建一個小型神經網絡,其輸入爲短期記憶和事件。把它們代入一個小型線性函數裏,在函數裏乘以一個新矩陣,再加一個新偏差 ,把所得結果代入 sigmoid 函數 使其值保持在 0 和 1 之間
遺忘門(The forget Gate)
其工作原理是 遺忘門會接受一個長期記憶並決定要保留和遺忘記憶的哪個部分
數學原理
把 t-1 時的長期記憶輸入進來乘以一個遺忘因子ft. 怎麼計算遺忘因子: 短期記憶 STM 以及事件信息來計算 ft。用一個一層小型神經網絡 將線性函數與sigmoid 函數結合起來 從而算出這個遺忘因子。
記憶門(remember gate)
接受從遺忘門輸出的長期記憶,以及從學習門輸出的短期記憶 然後直接把兩者合併起來
數學原理
把遺忘門的輸出結果和學習門的輸出結果加起來
使用門/輸出門(The Use Gate)
使用來自遺忘門的長期記憶,和來自學習門的短期記憶,找出新的短期記憶和輸出。
1.遺忘門的輸出上 使用雙曲正切 (tanh) 激活函數,應用較小的神經網絡; 2.短期記憶和使用 sigmoid 激活函數的事件上應用另一個較小的神經網絡 3.把兩個相乘 得到新的輸出也是新的短期記憶
總結
- 遺忘門 會接受長期記憶並遺忘掉一部分
- 學習門 把短期記憶和事件放到一起 作爲我們最近學到的信息
- 記憶門 記憶門則把還沒有遺忘的長期記憶和剛學到的新信息放到一起,以便更新長期記憶並將其輸出
- 使用門 把我們剛學到的信息和還沒遺忘的長期記憶放到一起從而作出預測並更新短期記憶
詞性標註的LSTM
神經網絡在輸入單詞方面做得不好,所以我門的第一步是準備我們的訓練數據,並將每個單詞映射到一個數值。
首先創建一小組訓練數據,這是幾個簡單的句子,它們被分解成一個單詞列表和相應的單詞標註。注意,使用low()
將句子轉換爲小寫單詞,然後使用split()
將句子分割爲單獨的單詞,通過空格字符分隔句子。
然後根據這些訓練數據,我們創建一個字典,將詞彙表中的每個獨特單詞映射到一個數值:唯一索引idx。對每個單詞標記都做同樣的處理,例如:一個名詞將由數字1表示。
準備數據
# import resources import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import matplotlib.pyplot as plt %matplotlib inline
# training sentences and their corresponding word-tags training_data = [ ("The cat ate the cheese".lower().split(), ["DET", "NN", "V", "DET", "NN"]), ("She read that book".lower().split(), ["NN", "V", "DET", "NN"]), ("The dog loves art".lower().split(), ["DET", "NN", "V", "NN"]), ("The elephant answers the phone".lower().split(), ["DET", "NN", "V", "DET", "NN"]) ] # create a dictionary that maps words to indices word2idx = {} for sent, tags in training_data: for word in sent: if word not in word2idx: word2idx[word] = len(word2idx) # create a dictionary that maps tags to indices tag2idx = {"DET": 0, "NN": 1, "V": 2}
# print out the created dictionary print(word2idx)
import numpy as np # a helper function for converting a sequence of words to a Tensor of numerical values # will be used later in training def prepare_sequence(seq, to_idx): '''This function takes in a sequence of words and returns a corresponding Tensor of numerical values (indices for each word).''' idxs = [to_idx[w] for w in seq] idxs = np.array(idxs) return torch.from_numpy(idxs) # check out what prepare_sequence does for one of our training sentences: example_input = prepare_sequence("The dog answers the phone".lower().split(), word2idx) print(example_input)
創建模型
模型假設以下幾點:
- 輸入被分解成一些列的單詞[W1,W2,...]
- 這些單詞來自我們已經知道的更多單詞列表(詞彙表)
- 我們有一組有限的標籤,[NN,V,DET],分別表示:名詞,動詞和決定因素(像“the”或“that”這樣的詞)
- 我們想要爲每個輸入詞預測*一個標籤 爲了進行預測,我們將在測試語句上傳遞LSTM,並將softmax函數應用於LSTM的隱藏狀態;結果是標記分數的向量,根據該向量,我們可以基於標記分數分佈的最大值來獲得單詞的預測標記。
詞嵌入 Word embeddings
我們知道LSTM接受預期的輸入大小和hidden_dim,但是句子很少具有一致的大小,那麼我們如何定義LSTM的輸入呢?
在這個網絡的最開始,我們將創建一個“Embedding”層,它接受我們詞彙表的大小,併爲輸入的單詞序列中的每個單詞返回指定大小的矢量embedding_dim
。
重要的是,這是該網絡的第一層。 您可以在PyTorch文檔.中閱讀有關此嵌入層的更多信息
class LSTMTagger(nn.Module): def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size): ''' Initialize the layers of this model.''' super(LSTMTagger, self).__init__() self.hidden_dim = hidden_dim # embedding layer that turns words into a vector of a specified size self.word_embeddings = nn.Embedding(vocab_size, embedding_dim) # the LSTM takes embedded word vectors (of a specified size) as inputs # and outputs hidden states of size hidden_dim self.lstm = nn.LSTM(embedding_dim, hidden_dim) # the linear layer that maps the hidden state output dimension # to the number of tags we want as output, tagset_size (in this case this is 3 tags) self.hidden2tag = nn.Linear(hidden_dim, tagset_size) # initialize the hidden state (see code below) self.hidden = self.init_hidden() def init_hidden(self): ''' At the start of training, we need to initialize a hidden state; there will be none because the hidden state is formed based on perviously seen data. So, this function defines a hidden state with all zeroes and of a specified size.''' # The axes dimensions are (n_layers, batch_size, hidden_dim) return (torch.zeros(1, 1, self.hidden_dim), torch.zeros(1, 1, self.hidden_dim)) def forward(self, sentence): ''' Define the feedforward behavior of the model.''' # create embedded word vectors for each word in a sentence embeds = self.word_embeddings(sentence) # get the output and hidden state by passing the lstm over our word embeddings # the lstm takes in our embeddings and hiddent state lstm_out, self.hidden = self.lstm( embeds.view(len(sentence), 1, -1), self.hidden) # get the scores for the most likely tag for a word tag_outputs = self.hidden2tag(lstm_out.view(len(sentence), -1)) tag_scores = F.log_softmax(tag_outputs, dim=1) return tag_scores
訓練
首先,我們定義單詞嵌入的大小。 EMBEDDING_DIM爲我們簡單的詞彙和訓練集定義了單詞向量的大小; 我們將它們保持在較小的位置,以便我們可以看到當我們訓練時重量如何變化 注意:複雜數據集的嵌入維度通常會大得多,大約爲64,128或256維。 由於我們的LSTM輸出了一系列帶有softmax圖層的標記分數,我們將使用NLLLoss。 與softmax層一起,NLL Loss創建了我們通常用於分析類分數分佈的交叉熵損失。 我們將使用標準梯度下降優化,但我們鼓勵您與其他優化器一起使用!
# the embedding dimension defines the size of our word vectors # for our simple vocabulary and training set, we will keep these small EMBEDDING_DIM = 6 HIDDEN_DIM = 6 # instantiate our model model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word2idx), len(tag2idx)) # define our loss and optimizer loss_function = nn.NLLLoss() optimizer = optim.SGD(model.parameters(), lr=0.1)
爲了檢查我們的模型是否已經學到了什麼,讓我們先看一下樣本測試句的分數,然後再訓練我們的模型。 請注意,測試句必須由我們詞彙表中的單詞組成,否則其單詞不能轉換爲索引。 分數應爲長度爲3的張量(對於我們的每個標籤),並且輸入句子中的每個單詞應該有分數。 對於測試句,“The cheese loves the elephant”,我們知道它有標籤(DET,NN,V,DET,NN)或[0,1,2,0,1],但我們的網絡還沒有 知道這個。 實際上,在這種情況下,我們的模型以隱藏的全零狀態開始,因此所有分數和預測標記應該是低的,隨機的,以及您對尚未訓練的網絡的期望!
test_sentence = "The cheese loves the elephant".lower().split() # see what the scores are before training # element [i,j] of the output is the *score* for tag j for word i. # to check the initial accuracy of our model, we don't need to train, so we use model.eval() inputs = prepare_sequence(test_sentence, word2idx) inputs = inputs tag_scores = model(inputs) print(tag_scores) # tag_scores outputs a vector of tag scores for each word in an inpit sentence # to get the most likely tag index, we grab the index with the maximum score! # recall that these numbers correspond to tag2idx = {"DET": 0, "NN": 1, "V": 2} _, predicted_tags = torch.max(tag_scores, 1) print('\n') print('Predicted tags: \n',predicted_tags)
循環遍歷多個時期的所有訓練數據: 1.通過調整漸變來準備我們的訓練模型 2.初始化LSTM的隱藏狀態 3.準備我們的數據進行培訓 4.在輸入上運行前向傳遞以獲取tag_scores 5.計算tag_scores和真實標記之間的損失 6.使用反向傳播更新模型的權重
在這個例子中,我們打印出每20個epoch的平均epoch損失; 你應該看到它隨着時間的推移而減少
# normally these epochs take a lot longer # but with our toy data (only 3 sentences), we can do many epochs in a short time n_epochs = 300 for epoch in range(n_epochs): epoch_loss = 0.0 # get all sentences and corresponding tags in the training data for sentence, tags in training_data: # zero the gradients model.zero_grad() # zero the hidden state of the LSTM, this detaches it from its history model.hidden = model.init_hidden() # prepare the inputs for processing by out network, # turn all sentences and targets into Tensors of numerical indices sentence_in = prepare_sequence(sentence, word2idx) targets = prepare_sequence(tags, tag2idx) # forward pass to get tag scores tag_scores = model(sentence_in) # compute the loss, and gradients loss = loss_function(tag_scores, targets) epoch_loss += loss.item() loss.backward() # update the model parameters with optimizer.step() optimizer.step() # print out avg loss per 20 epochs if(epoch%20 == 19): print("Epoch: %d, loss: %1.5f" % (epoch+1, epoch_loss/len(training_data)))
同樣,對於測試句“the cheese loves the elephant”,我們知道這裏有標記(DET, NN, V, DET, NN)或“[0,1,2,0,1]”。讓我們看看我們的模型是否學會了找到這些標籤!
test_sentence = "The cheese loves the elephant".lower().split() # see what the scores are after training inputs = prepare_sequence(test_sentence, word2idx) inputs = inputs tag_scores = model(inputs) print(tag_scores) # print the most likely tag index, by grabbing the index with the maximum score! # recall that these numbers correspond to tag2idx = {"DET": 0, "NN": 1, "V": 2} _, predicted_tags = torch.max(tag_scores, 1) print('\n') print('Predicted tags: \n',predicted_tags)