MENU

LSTM-based Sentiment Classification

February 7, 2020 • Read: 7236 • Deep Learning阅读设置

首先推荐一个 Jupyter 环境,是由 Google 提供的 colab,有免费的 GPU 可以使用

第一次使用需要在实验环境中下载相关的 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 合并在了一起。如果还是觉得不理解,可以看下这篇博客

Last Modified: June 15, 2022
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment

3 Comments
  1. zzz zzz

    老哥,Github 没了 @(泪)

    1. mathor mathor

      @zzz 好像是我把它删了,不过其实只要你把我文章中的代码都 copy 一遍就可以了

  2. Blazing Stellar Blazing Stellar

    话说训练完咋用来预测