LSTM-based Sentiment Classification
- 2020 年 2 月 15 日
- 筆記
第一次使用需要在實驗環境中下載相關的python庫
!pip install torch !pip install torchtext !python -m spacy download en
我們初步的設想是,首先將一個句子輸入到LSTM,這個句子有多少個單詞,就有多少個輸出,然後將所有輸出通過一個Linear Layer,這個Linear Layer的out_size是1,起到Binary Classification的作用

然後對於每個輸入,我們需要先要進行Embedding,把每個單詞轉換成固定長度的vector,再送到LSTM裡面去,假設每個單詞我們都用一個長度為100的vector來表示,每句話有seq個單詞(動態的,每句話的seq長度不一定一樣),那麼輸入的shape就是[seq, b, 100]
。最終通過Linear Layer輸出的$y$的shape就是[b]
我們使用的數據集是torchtext庫裡面的IMDB數據集
import torch from torch import nn, optim from torchtext import data, datasets print("GPU:",torch.cuda.is_available()) torch.manual_seed(123) TEXT = data.Field(tokenize='spacy') LABEL = data.LabelField(dtype=torch.float) train_data, test_data = datasets.IMDB.splits(TEXT, LABEL) print('len of train data:', len(train_data)) print('len of test data:', len(test_data)) print(train_data.examples[15].text) print(train_data.examples[15].label) # word2vec, glove TEXT.build_vocab(train_data, max_size=10000, vectors='glove.6B.100d') LABEL.build_vocab(train_data) batch_size = 30 device = torch.device('cuda') train_iterator, test_iterator = data.BucketIterator.splits( (train_data, test_data), batch_size = batch_size, device = device )
上面這些程式碼裡面有些參數不懂不要緊,因為只是載入數據集而已,不是很重要。如果想要了解torchtext,可以看這篇文章
接下來比較重要,定義網路結構
class RNN(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim): super(RNN, self).__init__() # [0-10001] => [100] self.embedding = nn.Embedding(vocab_size, embedding_dim) # [100] => [200] self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=2 ,bidirectional=True, dropout=0.5) # [256*2] => [1] self.fc = nn.Linear(hidden_dim*2, 1) self.dropout = nn.Dropout(0.5) def forward(self, x): # [seq, b, 1] => [seq, b, 100] embedding = self.dropout(self.embedding(x)) # output: [seq, b, hid_dim*2] # hidden/h: [num_layers*2, b, hid_dim] # cell/c: [num_layers*2, b, hid_dim] output, (hidden, cell) = self.rnn(embedding) # [num_layers*2, b, hid_dim] => 2 of [b, hid_dim] => [b, hid_dim*2] hidden = torch.cat([hidden[-2], hidden[-1]], dim=1) # [b, hid_dim*2] => [b, 1] hidden = self.dropout(hidden) out = self.fc(hidden) return out
nn.embedding(m, n)
其中m表示單詞的總數目,n表示詞嵌入的維度(每個單詞編碼為長度為n的vector)
然後就是LSTM本身,這裡就不做過多解釋了,參數介紹可以查看我的這篇文章,其中有一點之前的文章中沒有提到,就是這個bidirectional
參數,設置為True表示這個LSTM是雙向的,很好理解,之前學過的RNN都是單向的,很有局限,例如下面這句話
- 我今天不舒服,我打算___一天
如果是單向RNN,這個空肯定會填"醫院"或者"睡覺"之類的,但是如果是雙向的,它就能知道後面跟著"一天",這時"請假","休息"之類的被選擇的概率就會更大
最後的Fully Connected Layer可以理解為把所有輸出的資訊做個綜合,轉化為一個一維的tensor
rnn = RNN(len(TEXT.vocab), 100, 256) pretrained_embedding = TEXT.vocab.vectors print('pretrained_embedding:', pretrained_embedding.shape) rnn.embedding.weight.data.copy_(pretrained_embedding) print('embedding layer inited.')
Embedding層如果不初始化,生成的權值是隨機的,所以必須要初始化,這個權值是通過下載Glove編碼方式得到的,下載得到的其實就是個weight,直接覆蓋掉embedding裡面的weight,通過rnn.embedding.weight.data.copy_(pretrained_embedding)
的方式
然後我們看一下怎麼Train這個網路
import numpy as np def binary_acc(preds, y): """ get accuracy """ preds = torch.round(torch.sigmoid(preds)) correct = torch.eq(preds, y).float() acc = correct.sum() / len(correct) return acc def train(rnn, iterator, optimizer, criteon): avg_acc = [] rnn.train() for i, batch in enumerate(iterator): # [seq, b] => [b, 1] => [b] pred = rnn(batch.text).squeeze() loss = criteon(pred, batch.label) acc = binary_acc(pred, batch.label).item() avg_acc.append(acc) optimizer.zero_grad() loss.backward() optimizer.step() if i%10 == 0: print(i, acc) avg_acc = np.array(avg_acc).mean() print('avg acc:', avg_acc)
Train其實很簡單了,就是把text丟進去,然後返回一個shape為[b, 1]
的output,利用squeeze()
函數,去掉其中維數為1的維度,shape變成[b]
,方便與label進行比較
同樣的道理,Test也非常簡單
def eval(rnn, iterator, criteon): avg_acc = [] rnn.eval() with torch.no_grad(): for batch in iterator: # [b, 1] => [b] pred = rnn(batch.text).squeeze() loss = criteon(pred, batch.label) acc = binary_acc(pred, batch.label).item() avg_acc.append(acc) avg_acc = np.array(avg_acc).mean() print(">>test:", avg_acc)
最後定義一下loss和optimizer
optimizer = optim.Adam(rnn.parameters(), lr=1e-3) criteon = nn.BCEWithLogitsLoss().to(device) rnn.to(device)
其中BCEWithLogitsLoss()
主要用於二分類問題。nn.BCELoss()
是針對二分類用的交叉熵,這倆都是用於二分類,有什麼區別呢?區別在於BCEWithLogitsLoss
將Sigmoid層和BCELoss合併在了一起。如果還是覺得不理解,可以看下這篇部落格