Sentiment Analysis: Using Recurrent Neural Networks

:label:sec_sentiment_rnn

Like word similarity and analogy tasks, we can also apply pretrained word vectors to sentiment analysis. Since the IMDb review dataset in :numref:sec_sentiment is not very big, using text representations that were pretrained on large-scale corpora may reduce overfitting of the model. As a specific example illustrated in :numref:fig_nlp-map-sa-rnn, we will represent each token using the pretrained GloVe model, and feed these token representations into a multilayer bidirectional RNN to obtain the text sequence representation, which will be transformed into sentiment analysis outputs :cite:Maas.Daly.Pham.ea.2011. For the same downstream application, we will consider a different architectural choice later.

This section feeds pretrained GloVe to an RNN-based architecture for sentiment analysis. :label:fig_nlp-map-sa-rnn

```{.python .input} from d2l import mxnet as d2l from mxnet import gluon, init, np, npx from mxnet.gluon import nn, rnn npx.set_np()

batch_size = 64 train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)

  1. ```{.python .input}
  2. #@tab pytorch
  3. from d2l import torch as d2l
  4. import torch
  5. from torch import nn
  6. batch_size = 64
  7. train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)

Representing Single Text with RNNs

In text classifications tasks, such as sentiment analysis, a varying-length text sequence will be transformed into fixed-length categories. In the following BiRNN class, while each token of a text sequence gets its individual pretrained GloVe representation via the embedding layer (self.embedding), the entire sequence is encoded by a bidirectional RNN (self.encoder). More concretely, the hidden states (at the last layer) of the bidirectional LSTM at both the initial and final time steps are concatenated as the representation of the text sequence. This single text representation is then transformed into output categories by a fully connected layer (self.decoder) with two outputs (“positive” and “negative”).

```{.python .input} class BiRNN(nn.Block): def init(self, vocabsize, embedsize, num_hiddens, num_layers, **kwargs): super(BiRNN, self).__init(**kwargs) self.embedding = nn.Embedding(vocab_size, embed_size)

  1. # Set `bidirectional` to True to get a bidirectional RNN
  2. self.encoder = rnn.LSTM(num_hiddens, num_layers=num_layers,
  3. bidirectional=True, input_size=embed_size)
  4. self.decoder = nn.Dense(2)
  5. def forward(self, inputs):
  6. # The shape of `inputs` is (batch size, no. of time steps). Because
  7. # LSTM requires its input's first dimension to be the temporal
  8. # dimension, the input is transposed before obtaining token
  9. # representations. The output shape is (no. of time steps, batch size,
  10. # word vector dimension)
  11. embeddings = self.embedding(inputs.T)
  12. # Returns hidden states of the last hidden layer at different time
  13. # steps. The shape of `outputs` is (no. of time steps, batch size,
  14. # 2 * no. of hidden units)
  15. outputs = self.encoder(embeddings)
  16. # Concatenate the hidden states at the initial and final time steps as
  17. # the input of the fully connected layer. Its shape is (batch size,
  18. # 4 * no. of hidden units)
  19. encoding = np.concatenate((outputs[0], outputs[-1]), axis=1)
  20. outs = self.decoder(encoding)
  21. return outs
  1. ```{.python .input}
  2. #@tab pytorch
  3. class BiRNN(nn.Module):
  4. def __init__(self, vocab_size, embed_size, num_hiddens,
  5. num_layers, **kwargs):
  6. super(BiRNN, self).__init__(**kwargs)
  7. self.embedding = nn.Embedding(vocab_size, embed_size)
  8. # Set `bidirectional` to True to get a bidirectional RNN
  9. self.encoder = nn.LSTM(embed_size, num_hiddens, num_layers=num_layers,
  10. bidirectional=True)
  11. self.decoder = nn.Linear(4 * num_hiddens, 2)
  12. def forward(self, inputs):
  13. # The shape of `inputs` is (batch size, no. of time steps). Because
  14. # LSTM requires its input's first dimension to be the temporal
  15. # dimension, the input is transposed before obtaining token
  16. # representations. The output shape is (no. of time steps, batch size,
  17. # word vector dimension)
  18. embeddings = self.embedding(inputs.T)
  19. self.encoder.flatten_parameters()
  20. # Returns hidden states of the last hidden layer at different time
  21. # steps. The shape of `outputs` is (no. of time steps, batch size,
  22. # 2 * no. of hidden units)
  23. outputs, _ = self.encoder(embeddings)
  24. # Concatenate the hidden states of the initial time step and final
  25. # time step to use as the input of the fully connected layer. Its
  26. # shape is (batch size, 4 * no. of hidden units)
  27. encoding = torch.cat((outputs[0], outputs[-1]), dim=1)
  28. # Concatenate the hidden states at the initial and final time steps as
  29. # the input of the fully connected layer. Its shape is (batch size,
  30. # 4 * no. of hidden units)
  31. outs = self.decoder(encoding)
  32. return outs

Let’s construct a bidirectional RNN with two hidden layers to represent single text for sentiment analysis.

```{.python .input}

@tab all

embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus() net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers)

  1. ```{.python .input}
  2. net.initialize(init.Xavier(), ctx=devices)

```{.python .input}

@tab pytorch

def initweights(m): if type(m) == nn.Linear: nn.init.xavier_uniform(m.weight) if type(m) == nn.LSTM: for param in m.flat_weights_names: if “weight” in param: nn.init.xavier_uniform(m._parameters[param]) net.apply(init_weights);

  1. ## Loading Pretrained Word Vectors
  2. Below we load the pretrained 100-dimensional (needs to be consistent with `embed_size`) GloVe embeddings for tokens in the vocabulary.
  3. ```{.python .input}
  4. #@tab all
  5. glove_embedding = d2l.TokenEmbedding('glove.6b.100d')

Print the shape of the vectors for all the tokens in the vocabulary.

```{.python .input}

@tab all

embeds = glove_embedding[vocab.idx_to_token] embeds.shape

  1. We use these pretrained
  2. word vectors
  3. to represent tokens in the reviews
  4. and will not update
  5. these vectors during training.
  6. ```{.python .input}
  7. net.embedding.weight.set_data(embeds)
  8. net.embedding.collect_params().setattr('grad_req', 'null')

```{.python .input}

@tab pytorch

net.embedding.weight.data.copy_(embeds) net.embedding.weight.requires_grad = False

  1. ## Training and Evaluating the Model
  2. Now we can train the bidirectional RNN for sentiment analysis.
  3. ```{.python .input}
  4. lr, num_epochs = 0.01, 5
  5. trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
  6. loss = gluon.loss.SoftmaxCrossEntropyLoss()
  7. d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)

```{.python .input}

@tab pytorch

lr, num_epochs = 0.01, 5 trainer = torch.optim.Adam(net.parameters(), lr=lr) loss = nn.CrossEntropyLoss(reduction=”none”) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)

  1. We define the following function to predict the sentiment of a text sequence using the trained model `net`.
  2. ```{.python .input}
  3. #@save
  4. def predict_sentiment(net, vocab, sequence):
  5. """Predict the sentiment of a text sequence."""
  6. sequence = np.array(vocab[sequence.split()], ctx=d2l.try_gpu())
  7. label = np.argmax(net(sequence.reshape(1, -1)), axis=1)
  8. return 'positive' if label == 1 else 'negative'

```{.python .input}

@tab pytorch

@save

def predict_sentiment(net, vocab, sequence): “””Predict the sentiment of a text sequence.””” sequence = torch.tensor(vocab[sequence.split()], device=d2l.try_gpu()) label = torch.argmax(net(sequence.reshape(1, -1)), dim=1) return ‘positive’ if label == 1 else ‘negative’

  1. Finally, let's use the trained model to predict the sentiment for two simple sentences.
  2. ```{.python .input}
  3. #@tab all
  4. predict_sentiment(net, vocab, 'this movie is so great')

```{.python .input}

@tab all

predict_sentiment(net, vocab, ‘this movie is so bad’) ```

Summary

  • Pretrained word vectors can represent individual tokens in a text sequence.
  • Bidirectional RNNs can represent a text sequence, such as via the concatenation of its hidden states at the initial and final time steps. This single text representation can be transformed into categories using a fully connected layer.

Exercises

  1. Increase the number of epochs. Can you improve the training and testing accuracies? How about tuning other hyperparameters?
  2. Use larger pretrained word vectors, such as 300-dimensional GloVe embeddings. Does it improve classification accuracy?
  3. Can we improve the classification accuracy by using the spaCy tokenization? You need to install spaCy (pip install spacy) and install the English package (python -m spacy download en). In the code, first, import spaCy (import spacy). Then, load the spaCy English package (spacy_en = spacy.load('en')). Finally, define the function def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)] and replace the original tokenizer function. Note the different forms of phrase tokens in GloVe and spaCy. For example, the phrase token “new york” takes the form of “new-york” in GloVe and the form of “new york” after the spaCy tokenization.

:begin_tab:mxnet Discussions :end_tab:

:begin_tab:pytorch Discussions :end_tab: