Multilayer RNNs

In a multilayer RNN, we pass the activations from our recurrent neural network into a second recurrent neural network, like in <>.

2-layer RNN

The unrolled representation is shown in <> (similar to <>).

2-layer unrolled RNN

Let’s see how to implement this in practice.

The Model

We can save some time by using PyTorch’s RNN class, which implements exactly what we created earlier, but also gives us the option to stack multiple RNNs, as we have discussed:

In [ ]:

  1. class LMModel5(Module):
  2. def __init__(self, vocab_sz, n_hidden, n_layers):
  3. self.i_h = nn.Embedding(vocab_sz, n_hidden)
  4. self.rnn = nn.RNN(n_hidden, n_hidden, n_layers, batch_first=True)
  5. self.h_o = nn.Linear(n_hidden, vocab_sz)
  6. self.h = torch.zeros(n_layers, bs, n_hidden)
  7. def forward(self, x):
  8. res,h = self.rnn(self.i_h(x), self.h)
  9. self.h = h.detach()
  10. return self.h_o(res)
  11. def reset(self): self.h.zero_()

In [ ]:

  1. learn = Learner(dls, LMModel5(len(vocab), 64, 2),
  2. loss_func=CrossEntropyLossFlat(),
  3. metrics=accuracy, cbs=ModelResetter)
  4. learn.fit_one_cycle(15, 3e-3)
epochtrain_lossvalid_lossaccuracytime
03.0558532.5916400.43790700:01
12.1623591.7873100.47159800:01
21.7106631.9418070.32177700:01
31.5207831.9997260.31201200:01
41.3308462.0129020.41324900:01
51.1632971.8961920.45068400:01
61.0338132.0052090.43481400:01
70.9190902.0470830.45670600:01
80.8229392.0680310.46883100:01
90.7501802.1360640.47509800:01
100.6951202.1391400.48543300:01
110.6557522.1550810.49365200:01
120.6296502.1625830.49853500:01
130.6135832.1716490.49104800:01
140.6043092.1803550.48787400:01

Now that’s disappointing… our previous single-layer RNN performed better. Why? The reason is that we have a deeper model, leading to exploding or vanishing activations.

Exploding or Disappearing Activations

In practice, creating accurate models from this kind of RNN is difficult. We will get better results if we call detach less often, and have more layers—this gives our RNN a longer time horizon to learn from, and richer features to create. But it also means we have a deeper model to train. The key challenge in the development of deep learning has been figuring out how to train these kinds of models.

The reason this is challenging is because of what happens when you multiply by a matrix many times. Think about what happens when you multiply by a number many times. For example, if you multiply by 2, starting at 1, you get the sequence 1, 2, 4, 8,… after 32 steps you are already at 4,294,967,296. A similar issue happens if you multiply by 0.5: you get 0.5, 0.25, 0.125… and after 32 steps it’s 0.00000000023. As you can see, multiplying by a number even slightly higher or lower than 1 results in an explosion or disappearance of our starting number, after just a few repeated multiplications.

Because matrix multiplication is just multiplying numbers and adding them up, exactly the same thing happens with repeated matrix multiplications. And that’s all a deep neural network is —each extra layer is another matrix multiplication. This means that it is very easy for a deep neural network to end up with extremely large or extremely small numbers.

This is a problem, because the way computers store numbers (known as “floating point”) means that they become less and less accurate the further away the numbers get from zero. The diagram in <>, from the excellent article “What You Never Wanted to Know About Floating Point but Will Be Forced to Find Out”, shows how the precision of floating-point numbers varies over the number line.

Precision of floating point numbers

This inaccuracy means that often the gradients calculated for updating the weights end up as zero or infinity for deep networks. This is commonly referred to as the vanishing gradients or exploding gradients problem. It means that in SGD, the weights are either not updated at all or jump to infinity. Either way, they won’t improve with training.

Researchers have developed a number of ways to tackle this problem, which we will be discussing later in the book. One option is to change the definition of a layer in a way that makes it less likely to have exploding activations. We’ll look at the details of how this is done in <>, when we discuss batch normalization, and <>, when we discuss ResNets, although these details don’t generally matter in practice (unless you are a researcher that is creating new approaches to solving this problem). Another strategy for dealing with this is by being careful about initialization, which is a topic we’ll investigate in <>.

For RNNs, there are two types of layers that are frequently used to avoid exploding activations: gated recurrent units (GRUs) and long short-term memory (LSTM) layers. Both of these are available in PyTorch, and are drop-in replacements for the RNN layer. We will only cover LSTMs in this book; there are plenty of good tutorials online explaining GRUs, which are a minor variant on the LSTM design.