長短期記憶(LSTM)網絡已被廣泛用于解決各種順序任務。讓我們了解這些網絡如何工作以及如何實施它們。
就像我們一樣,循環神經網絡(RNN)也可能很健忘。這種與短期記憶的斗爭導致 RNN 在大多數任務中失去有效性。不過,不用擔心,長短期記憶網絡 (LSTM) 具有出色的記憶力,可以記住普通 RNN 無法記住的信息!
LSTM 是 RNN 的一種特殊變體,因此掌握 RNN 相關的概念將極大地幫助您理解本文中的 LSTM。我在上一篇文章中介紹了 RNN 的機制。
RNN 快速回顧
RNN 以順序方式處理輸入,其中在計算當前步驟的輸出時考慮先前輸入的上下文。這允許神經網絡在不同的時間步長上攜帶信息,而不是保持所有輸入彼此獨立。
然而,困擾典型 RNN 的一個重大缺點是梯度消失/爆炸問題。在訓練過程中通過 RNN 反向傳播時會出現此問題,特別是對于具有更深層的網絡。由于鏈式法則,梯度在反向傳播過程中必須經過連續的矩陣乘法,導致梯度要么呈指數收縮(消失),要么呈指數爆炸(爆炸)。梯度太小會阻礙權重的更新和學習,而梯度太大會導致模型不穩定。
由于這些問題,RNN 無法處理較長的序列并保持長期依賴性,從而使它們遭受“短期記憶”的困擾。
什么是 LSTM
雖然 LSTM 是一種 RNN,其功能與傳統 RNN 類似,但它的門控機制使其與眾不同。該功能解決了 RNN 的“短期記憶”問題。
從圖中我們可以看出,差異主要在于 LSTM 保存長期記憶的能力。這在大多數自然語言處理 (NLP) 或時間序列和順序任務中尤其重要。例如,假設我們有一個網絡根據給我們的一些輸入生成文本。文章開頭提到作者有一只“名叫克里夫的狗”。在其他幾個沒有提到寵物或狗的句子之后,作者再次提到了他的寵物,模型必須生成下一個單詞“但是,克里夫,我的寵物____”。由于單詞 pet 出現在空白之前,RNN 可以推斷出下一個單詞可能是可以作為寵物飼養的動物。
然而,由于短期記憶的原因,典型的 RNN 只能使用最后幾句話中出現的文本中的上下文信息——這根本沒有用處。RNN 不知道寵物可能是什么動物,因為文本開頭的相關信息已經丟失。
另一方面,LSTM 可以保留作者有一只寵物狗的早期信息,這將有助于模型在生成文本時根據大量上下文信息選擇“狗” 。較早的時間步長。
LSTM 的內部工作原理
LSTM 的秘密在于每個 LSTM 單元內的門控機制。在普通 RNN 單元中,某個時間步的輸入和前一個時間步的隱藏狀態通過tanh激活函數來獲得新的隱藏狀態和輸出。
另一方面,LSTM 的結構稍微復雜一些。在每個時間步,LSTM 單元都會接收 3 條不同的信息:當前輸入數據、前一個單元的短期記憶(類似于 RNN 中的隱藏狀態)以及最后的長期記憶。
短期記憶通常稱為隱藏狀態,長期記憶通常稱為細胞狀態。
然后,在將長期和短期信息傳遞到下一個單元之前,單元使用門來調節每個時間步要保留或丟棄的信息。
這些門可以看作是水過濾器。理想情況下,這些門的作用應該是選擇性地去除任何不相關的信息,類似于水過濾器如何防止雜質通過。同時,只有水和有益的營養物質才能通過這些過濾器,就像大門只保留有用的信息一樣。當然,這些門需要經過訓練才能準確過濾有用的內容和無用的內容。
這些門稱為輸入門、遺忘門和輸出門。這些門的名稱有多種變體。然而,這些門的計算和工作原理大部分是相同的。
讓我們一一了解這些門的機制。
輸入門
輸入門決定哪些新信息將存儲在長期記憶中。它僅適用于當前輸入的信息和上一時間步的短期記憶。因此,它必須從這些變量中過濾掉無用的信息。
從數學上講,這是使用2 層來實現的。第一層可以看作是過濾器,它選擇哪些信息可以通過它以及哪些信息被丟棄。為了創建這一層,我們將短期記憶和當前輸入傳遞給sigmoid函數。sigmoid函數會將值轉換為0到1之間,0表示部分信息不重要,而1表示該信息將被使用。這有助于決定要保留和使用的值,以及要丟棄的值。當該層通過反向傳播進行訓練時,sigmoid函數中的權重將被更新,以便它學會只讓有用的特征通過,同時丟棄不太重要的特征。
i 1 = σ ( W i 1 ? ( H t ? 1 , x t ) + b i a s i 1 ) i_1 = \sigma(W_{i_1} \cdot (H_{t-1}, x_t) + bias_{i_1}) i1?=σ(Wi1???(Ht?1?,xt?)+biasi1??)
第二層也采用短期記憶和當前輸入,并將其傳遞給激活函數(通常是 t a n h tanh tanh函數)來調節網絡。
i 2 = t a n h ( W i 2 ? ( H t ? 1 , x t ) + b i a s i 2 ) i_2 = tanh(W_{i_2} \cdot (H_{t-1}, x_t) + bias_{i_2}) i2?=tanh(Wi2???(Ht?1?,xt?)+biasi2??)
然后將這兩層的輸出相乘,最終結果表示要保存在長期記憶中并用作輸出的信息。
i i n p u t = i 1 ? i 2 i_{input} = i_1 * i_2 iinput?=i1??i2?
遺忘門
遺忘門決定應保留或丟棄長期記憶中的哪些信息。這是通過將傳入的長期記憶乘以當前輸入和傳入的短期記憶生成的遺忘向量來完成的。
就像輸入門中的第一層一樣,遺忘向量也是一個選擇性過濾層。為了獲得遺忘向量,短期記憶和當前輸入通過sigmoid函數傳遞,類似于上面輸入門中的第一層,但具有不同的權重。該向量將由 0 和 1 組成,并將與長期記憶相乘以選擇要保留長期記憶的哪些部分。
f = σ ( W f o r g e t ? ( H t ? 1 , x t ) + b i a s f o r g e t ) f = \sigma(W_{forget} \cdot (H_{t-1}, x_t) + bias_{forget}) f=σ(Wforget??(Ht?1?,xt?)+biasforget?)
輸入門和遺忘門的輸出將進行逐點加法,以給出新版本的長期記憶,并將其傳遞到下一個單元。這個新的長期記憶也將用于最后一個門,即輸出門。
C t = C t ? 1 ? f + i i n p u t C_t = C_{t-1} * f + i_{input} Ct?=Ct?1??f+iinput?
輸出門
輸出門將采用當前輸入、先前的短期記憶和新計算的長期記憶來產生新的短期記憶/隱藏狀態 ,該狀態將在下一個時間步驟中傳遞到單元。當前時間步的輸出也可以從這個隱藏狀態中得出。
首先,之前的短期記憶和當前輸入將再次以不同的權重傳遞到 sigmoid 函數(是的,這是我們第三次這樣做),以創建第三個也是最后一個過濾器。然后,我們將新的長期記憶通過激活 t a n h tanh tanh函數。這兩個過程的輸出將相乘以產生新的短期記憶。
O 1 = σ ( W o u t p u t 1 ? ( H t ? 1 , x t ) + b i a s o u t p u t 1 ) O 2 = t a n h ( W o u t p u t 2 ? C t + b i a s o u t p u t 2 ) H t , O t = O 1 ? O 2 O_1 = \sigma (W_{output_1} \cdot (H_{t-1}, x_t) + bias_{output_1})\\ O_2 = tanh(W_{output_2} \cdot C_t + bias_{output_2})\\ H_t, O_t = O_1 * O_2 O1?=σ(Woutput1???(Ht?1?,xt?)+biasoutput1??)O2?=tanh(Woutput2???Ct?+biasoutput2??)Ht?,Ot?=O1??O2?
這些門產生的短期和長期記憶將被轉移到下一個單元,以重復該過程。每個時間步的輸出可以從短期記憶中獲得,也稱為隱藏狀態。
這就是典型 LSTM 結構的全部機制。沒那么難吧?
代碼實現
對 LSTM 有了必要的理論了解后,讓我們開始在代碼中實現它。今天我們將使用 PyTorch 庫。
在我們進入具有完整數據集的項目之前,讓我們通過可視化輸出來看看 PyTorch LSTM 層在實踐中的實際工作原理。我們不需要實例化模型來查看該層如何工作。您可以使用下面的按鈕在 FloydHub 上運行此程序LSTM_starter.ipynb。(這部分不需要在 GPU 上運行)
import torch
import torch.nn as nn
就像其他類型的層一樣,我們可以實例化 LSTM 層并為其提供必要的參數。可以在此處找到已接受參數的完整文檔。在此示例中,我們將僅定義輸入維度、隱藏維度和層數。
- 輸入維度- 表示每個時間步輸入的大小,例如維度 5 的輸入將如下所示 [1, 3, 8, 2, 3]
- 隱藏維度- 表示每個時間步隱藏狀態和細胞狀態的大小,例如,如果隱藏維度為 3,則隱藏狀態和細胞狀態都將具有 [3, 5, 4] 的形狀
- 層數 - 彼此堆疊的 LSTM 層數
input_dim = 5
hidden_dim = 10
n_layers = 1lstm_layer = nn.LSTM(input_dim, hidden_dim, n_layers, batch_first=True)
讓我們創建一些虛擬數據來查看該層如何接收輸入。由于我們的輸入維度是5,我們必須創建一個形狀為 ( 1, 1, 5 ) 的張量,它表示(批量大小、序列長度、輸入維度)。
此外,我們必須初始化 LSTM 的隱藏狀態和單元狀態,因為這是第一個單元。隱藏狀態和單元狀態存儲在格式為 ( hidden_??state , cell_state ) 的元組中。
batch_size = 1
seq_len = 1inp = torch.randn(batch_size, seq_len, input_dim)
hidden_state = torch.randn(n_layers, batch_size, hidden_dim)
cell_state = torch.randn(n_layers, batch_size, hidden_dim)
hidden = (hidden_state, cell_state)
[Out]:
Input shape: (1, 1, 5)
Hidden shape: ((1, 1, 10), (1, 1, 10))
接下來,我們將提供輸入和隱藏狀態,看看我們會從中得到什么。
out, hidden = lstm_layer(inp, hidden)
print("Output shape: ", out.shape)
print("Hidden: ", hidden)
[Out]: Output shape: torch.size([1, 1, 10])Hidden: (tensor([[[ 0.1749, 0.0099, -0.3004, 0.2846, -0.2262, -0.5257, 0.2925, -0.1894, 0.1166, -0.1197]]], grad_fn=<StackBackward>), tensor([[[ 0.4167, 0.0385, -0.4982, 0.6955, -0.9213, -1.0072, 0.4426,-0.3691, 0.2020, -0.2242]]], grad_fn=<StackBackward>))
在上面的過程中,我們看到了 LSTM 單元如何在每個時間步處理輸入和隱藏狀態。然而,在大多數情況下,我們將以大序列處理輸入數據。LSTM 還可以接收可變長度的序列并在每個時間步產生輸出。這次我們嘗試改變序列長度。
seq_len = 3
inp = torch.randn(batch_size, seq_len, input_dim)
out, hidden = lstm_layer(inp, hidden)
print(out.shape)
[Out]: torch.Size([1, 3, 10])
這次,輸出的第二維是 3,表明 LSTM 給出了 3 個輸出。這對應于我們輸入序列的長度。對于我們需要在每個時間步(多對多)輸出的用例,例如文本生成,每個時間步的輸出可以直接從第二維提取并輸入到完全連接的層中。對于文本分類任務(多對一),例如情感分析,可以將最后的輸出輸入到分類器中。
# Obtaining the last output
out = out.squeeze()[-1, :]
print(out.shape)
[Out]: torch.Size([10])
項目:亞馬遜評論情緒分析
對于此項目,我們將使用 Amazon 客戶評論數據集,該數據集可以在Kaggle上找到。該數據集總共包含 400 萬條評論,每條評論都標記為正面或負面情緒, 可以在此處找到 GitHub 存儲庫的鏈接。
我們實施此項目時的目標是創建一個 LSTM 模型,能夠準確分類和區分評論的情緒。為此,我們必須從一些數據預處理、定義和訓練模型開始,然后評估模型。
我們的實現流程如下所示。
我們在此實現中僅使用 100 萬條評論來加快速度,但是,如果您有時間和計算能力,請隨意使用整個數據集自行運行它。
對于我們的數據預處理步驟,我們將使用regex、Numpy和NLTK(自然語言工具包)庫來實現一些簡單的 NLP 輔助函數。由于數據以bz2格式壓縮,因此我們將使用 Python bz2模塊來讀取數據。
import bz2
from collections import Counter
import re
import nltk
import numpy as np
nltk.download('punkt')train_file = bz2.BZ2File('../input/amazon_reviews/train.ft.txt.bz2')
test_file = bz2.BZ2File('../input/amazon_reviews/test.ft.txt.bz2')train_file = train_file.readlines()
test_file = test_file.readlines()
Number of training reviews: 3600000
Number of test reviews: 400000
該數據集總共包含 400 萬條評論,其中 360 萬條用于訓練,40 萬條用于測試。我們將僅使用 800k 進行訓練,200k 進行測試——這仍然是大量數據。
num_train = 800000 # We're training on the first 800,000 reviews in the dataset
num_test = 200000 # Using 200,000 reviews from test settrain_file = [x.decode('utf-8') for x in train_file[:num_train]]
test_file = [x.decode('utf-8') for x in test_file[:num_test]]
句子的格式如下:
__label__2 Stunning even for the non-gamer: This soundtrack was beautiful! It paints the scenery in your mind so well I would recommend it even to people who hate vid. game music! I have played the game Chrono Cross but out of all of the games I have ever played it has the best music! It backs away from crude keyboarding and takes a fresher step with great guitars and soulful orchestras. It would impress anyone who cares to listen! _
我們必須從句子中提取標簽。數據是格式__label__1/2 ,因此我們可以輕松地相應地分割它。積極情緒標簽存儲為 1,消極情緒標簽存儲為 0。
我們還將所有URL更改為標準,<url>因為在大多數情況下,確切的URL與情緒無關。
# Extracting labels from sentences
train_labels = [0 if x.split(' ')[0] == '__label__1' else 1 for x in train_file]
train_sentences = [x.split(' ', 1)[1][:-1].lower() for x in train_file]test_labels = [0 if x.split(' ')[0] == '__label__1' else 1 for x in test_file]
test_sentences = [x.split(' ', 1)[1][:-1].lower() for x in test_file]# Some simple cleaning of data
for i in range(len(train_sentences)):train_sentences[i] = re.sub('\d','0',train_sentences[i])for i in range(len(test_sentences)):test_sentences[i] = re.sub('\d','0',test_sentences[i])# Modify URLs to <url>
for i in range(len(train_sentences)):if 'www.' in train_sentences[i] or 'http:' in train_sentences[i] or 'https:' in train_sentences[i] or '.com' in train_sentences[i]:train_sentences[i] = re.sub(r"([^ ]+(?<=\.[a-z]{3}))", "<url>", train_sentences[i])for i in range(len(test_sentences)):if 'www.' in test_sentences[i] or 'http:' in test_sentences[i] or 'https:' in test_sentences[i] or '.com' in test_sentences[i]:test_sentences[i] = re.sub(r"([^ ]+(?<=\.[a-z]{3}))", "<url>", test_sentences[i])
快速清理數據后,我們將對句子進行標記化,這是標準的 NLP 任務。
標記化是將句子分割成單個標記的任務,這些標記可以是單詞或標點符號等。
有許多 NLP 庫可以做到這一點,例如spaCy或Scikit-learn,但我們將在這里使用NLTK,因為它具有更快的分詞器之一。
然后,這些單詞將被存儲在字典中,將單詞映射到其出現次數。這些詞將成為我們的詞匯。
words = Counter() # Dictionary that will map a word to the number of times it appeared in all the training sentences
for i, sentence in enumerate(train_sentences):# The sentences will be stored as a list of words/tokenstrain_sentences[i] = []for word in nltk.word_tokenize(sentence): # Tokenizing the wordswords.update([word.lower()]) # Converting all the words to lowercasetrain_sentences[i].append(word)if i%20000 == 0:print(str((i*100)/num_train) + "% done")
print("100% done")
- 為了刪除可能不存在的拼寫錯誤和單詞,我們將從詞匯表中刪除僅出現一次的所有單詞。
- 為了解決未知單詞和填充問題,我們還必須將它們添加到我們的詞匯表中。然后,詞匯表中的每個單詞將被分配一個整數索引,然后映射到該整數。
# Removing the words that only appear once
words = {k:v for k,v in words.items() if v>1}
# Sorting the words according to the number of appearances, with the most common word being first
words = sorted(words, key=words.get, reverse=True)
# Adding padding and unknown to our vocabulary so that they will be assigned an index
words = ['_PAD','_UNK'] + words
# Dictionaries to store the word to index mappings and vice versa
word2idx = {o:i for i,o in enumerate(words)}
idx2word = {i:o for i,o in enumerate(words)}
通過映射,我們將句子中的單詞轉換為其相應的索引。
for i, sentence in enumerate(train_sentences):# Looking up the mapping dictionary and assigning the index to the respective wordstrain_sentences[i] = [word2idx[word] if word in word2idx else 0 for word in sentence]for i, sentence in enumerate(test_sentences):# For test sentences, we have to tokenize the sentences as welltest_sentences[i] = [word2idx[word.lower()] if word.lower() in word2idx else 0 for word in nltk.word_tokenize(sentence)]
在最后的預處理步驟中,我們將用 0 填充句子并縮短冗長的句子,以便可以批量訓練數據以加快速度。
# Defining a function that either shortens sentences or pads sentences with 0 to a fixed length
def pad_input(sentences, seq_len):features = np.zeros((len(sentences), seq_len),dtype=int)for ii, review in enumerate(sentences):if len(review) != 0:features[ii, -len(review):] = np.array(review)[:seq_len]return featuresseq_len = 200 # The length that the sentences will be padded/shortened totrain_sentences = pad_input(train_sentences, seq_len)
test_sentences = pad_input(test_sentences, seq_len)# Converting our labels into numpy arrays
train_labels = np.array(train_labels)
test_labels = np.array(test_labels)
填充的句子看起來像這樣,其中 0 代表填充:
array([ 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, 44, 125, 13, 28, 1701, 5144, 60,31, 10, 3, 44, 2052, 10, 84, 2131, 2,5, 27, 1336, 8, 11, 125, 17, 153, 6,5, 146, 103, 9, 2, 64, 5, 117, 14,7, 42, 1680, 9, 194, 56, 230, 107, 2,7, 128, 1680, 52, 31073, 41, 3243, 14, 3,3674, 2, 11, 125, 52, 10669, 156, 2, 1103,29, 0, 0, 6, 917, 52, 1366, 2, 31,10, 156, 23, 2071, 3574, 2, 11, 12, 7,2954, 9926, 125, 14, 28, 21, 2, 180, 95,132, 147, 9, 220, 12, 52, 718, 56, 2,2339, 5, 272, 11, 4, 72, 695, 562, 4,722, 4, 425, 4, 163, 4, 1491, 4, 1132,1829, 520, 31, 169, 34, 77, 18, 16, 1107,69, 33])
我們的數據集已經分為訓練數據和測試數據。然而,我們在訓練過程中仍然需要一組數據進行驗證。因此,我們將測試數據分成兩半,分為驗證集和測試集。可以在此處找到數據集拆分的詳細說明。
split_frac = 0.5 # 50% validation, 50% test
split_id = int(split_frac * len(test_sentences))
val_sentences, test_sentences = test_sentences[:split_id], test_sentences[split_id:]
val_labels, test_labels = test_labels[:split_id], test_labels[split_id:]
接下來,我們將開始使用 PyTorch 庫。我們首先從句子和標簽定義數據集,然后將它們加載到數據加載器中。我們將批量大小設置為 256。這可以根據您的需要進行調整。
import torch
from torch.utils.data import TensorDataset, DataLoader
import torch.nn as nntrain_data = TensorDataset(torch.from_numpy(train_sentences), torch.from_numpy(train_labels))
val_data = TensorDataset(torch.from_numpy(val_sentences), torch.from_numpy(val_labels))
test_data = TensorDataset(torch.from_numpy(test_sentences), torch.from_numpy(test_labels))batch_size = 400train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size)
val_loader = DataLoader(val_data, shuffle=True, batch_size=batch_size)
test_loader = DataLoader(test_data, shuffle=True, batch_size=batch_size)
我們還可以檢查是否有 GPU 可以將訓練時間加快很多倍。如果您使用帶有 GPU 的 FloydHub 來運行此代碼,訓練時間將顯著減少。
# torch.cuda.is_available() checks and returns a Boolean True if a GPU is available, else it'll return False
is_cuda = torch.cuda.is_available()# If we have a GPU available, we'll set our device to GPU. We'll use this device variable later in our code.
if is_cuda:device = torch.device("cuda")
else:device = torch.device("cpu")
此時,我們將定義模型的架構。在此階段,我們可以創建具有深層或大量相互堆疊的 LSTM 層的神經網絡。然而,像下面這樣的簡單模型,只有一個 LSTM 和一個全連接層,效果很好,并且需要的訓練時間要少得多。在將句子輸入 LSTM 層之前,我們將在第一層訓練我們自己的詞嵌入。
最后一層是一個全連接層,具有 sigmoid 函數,用于對評論是否具有積極/消極情緒進行分類。
class SentimentNet(nn.Module):def __init__(self, vocab_size, output_size, embedding_dim, hidden_dim, n_layers, drop_prob=0.5):super(SentimentNet, self).__init__()self.output_size = output_sizeself.n_layers = n_layersself.hidden_dim = hidden_dimself.embedding = nn.Embedding(vocab_size, embedding_dim)self.lstm = nn.LSTM(embedding_dim, hidden_dim, n_layers, dropout=drop_prob, batch_first=True)self.dropout = nn.Dropout(drop_prob)self.fc = nn.Linear(hidden_dim, output_size)self.sigmoid = nn.Sigmoid()def forward(self, x, hidden):batch_size = x.size(0)x = x.long()embeds = self.embedding(x)lstm_out, hidden = self.lstm(embeds, hidden)lstm_out = lstm_out.contiguous().view(-1, self.hidden_dim)out = self.dropout(lstm_out)out = self.fc(out)out = self.sigmoid(out)out = out.view(batch_size, -1)out = out[:,-1]return out, hiddendef init_hidden(self, batch_size):weight = next(self.parameters()).datahidden = (weight.new(self.n_layers, batch_size, self.hidden_dim).zero_().to(device),weight.new(self.n_layers, batch_size, self.hidden_dim).zero_().to(device))return hidden
請注意,我們實際上可以加載預先訓練的詞嵌入,例如GloVe或fastText,這可以提高模型的準確性并減少訓練時間。
這樣,我們就可以在定義參數后實例化我們的模型。輸出維度僅為 1,因為它只需要輸出 1 或 0。還定義了學習率、損失函數和優化器。
vocab_size = len(word2idx) + 1
output_size = 1
embedding_dim = 400
hidden_dim = 512
n_layers = 2model = SentimentNet(vocab_size, output_size, embedding_dim, hidden_dim, n_layers)
model.to(device)lr=0.005
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
最后,我們可以開始訓練模型。每 1000 個步驟,我們將根據驗證數據集檢查模型的輸出,如果模型的表現比前一次更好,則保存模型。
state_dict是 PyTorch 中模型的權重,可以在單獨的時間或腳本中加載到具有相同架構的模型中。
epochs = 2
counter = 0
print_every = 1000
clip = 5
valid_loss_min = np.Infmodel.train()
for i in range(epochs):h = model.init_hidden(batch_size)for inputs, labels in train_loader:counter += 1h = tuple([e.data for e in h])inputs, labels = inputs.to(device), labels.to(device)model.zero_grad()output, h = model(inputs, h)loss = criterion(output.squeeze(), labels.float())loss.backward()nn.utils.clip_grad_norm_(model.parameters(), clip)optimizer.step()if counter%print_every == 0:val_h = model.init_hidden(batch_size)val_losses = []model.eval()for inp, lab in val_loader:val_h = tuple([each.data for each in val_h])inp, lab = inp.to(device), lab.to(device)out, val_h = model(inp, val_h)val_loss = criterion(out.squeeze(), lab.float())val_losses.append(val_loss.item())model.train()print("Epoch: {}/{}...".format(i+1, epochs),"Step: {}...".format(counter),"Loss: {:.6f}...".format(loss.item()),"Val Loss: {:.6f}".format(np.mean(val_losses)))if np.mean(val_losses) <= valid_loss_min:torch.save(model.state_dict(), './state_dict.pt')print('Validation loss decreased ({:.6f} --> {:.6f}). Saving model ...'.format(valid_loss_min,np.mean(val_losses)))valid_loss_min = np.mean(val_losses)
完成訓練后,是時候在以前從未見過的數據集(我們的測試數據集)上測試我們的模型了。我們首先從驗證損失最低的點加載模型權重。
我們可以計算模型的準確性,看看我們的模型的預測有多準確。
# Loading the best model
model.load_state_dict(torch.load('./state_dict.pt'))test_losses = []
num_correct = 0
h = model.init_hidden(batch_size)model.eval()
for inputs, labels in test_loader:h = tuple([each.data for each in h])inputs, labels = inputs.to(device), labels.to(device)output, h = model(inputs, h)test_loss = criterion(output.squeeze(), labels.float())test_losses.append(test_loss.item())pred = torch.round(output.squeeze()) # Rounds the output to 0/1correct_tensor = pred.eq(labels.float().view_as(pred))correct = np.squeeze(correct_tensor.cpu().numpy())num_correct += np.sum(correct)print("Test loss: {:.3f}".format(np.mean(test_losses)))
test_acc = num_correct/len(test_loader.dataset)
print("Test accuracy: {:.3f}%".format(test_acc*100))
[Out]: Test loss: 0.161Test accuracy: 93.906%
通過這個簡單的 LSTM 模型,我們成功實現了93.8%的準確率!這顯示了 LSTM 在處理此類順序任務方面的有效性。
這個結果是通過幾個簡單的層實現的,并且沒有任何超參數調整。可以進行許多其他改進來提高模型的有效性,并且您可以自由地嘗試通過實施這些改進來超越此準確性!
一些改進建議如下:
- 運行超參數搜索來優化您的配置。可以在此處找到技術指南
- 增加模型復雜性
- 添加更多層/使用雙向 LSTM 使用預先訓練的詞嵌入,例如GloVe嵌入
超越 LSTM
多年來,LSTM 在 NLP 任務方面一直是最先進的。然而,基于注意力的模型和Transformer 的最新進展產生了更好的結果。隨著 Google 的 BERT 和 OpenAI 的 GPT 等預訓練 Transformer 模型的發布,LSTM 的使用量一直在下降。盡管如此,理解 RNN 和 LSTM 背后的概念肯定還是有用的,誰知道,也許有一天 LSTM 會卷土重來呢?
本博文譯自Gabriel Loye的博客。