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合併在了一起。如果還是覺得不理解,可以看下這篇博客

ipynb版本代碼

py版本代碼