{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Deep Recurrent Neural Networks\n",
"\n",
"Up to now, we only discussed recurrent neural networks with a single unidirectional hidden layer. In it the specific functional form of how latent variables and observations interact was rather arbitrary. This isn't a big problem as long as we have enough flexibility to model different types of interactions. With a single layer, however, this can be quite challenging. In the case of the perceptron we fixed this problem by adding more layers. Within RNNs this is a bit more tricky, since we first need to decide how and where to add extra nonlinearity. Our discussion below focuses primarily on LSTMs but it applies to other sequence models, too.\n",
"\n",
"* We could add extra nonlinearity to the gating mechansims. That is, instead of using a single perceptron we could use multiple layers. This leaves the *mechanism* of the LSTM unchanged. Instead it makes it more sophisticated. This would make sense if we were led to believe that the LSTM mechanism describes some form of universal truth of how latent variable autoregressive models work. \n",
"* We could stack multiple layers of LSTMs on top of each other. This results in a mechanism that is more flexible, due to the combination of several simple layers. In particular, data might be relevant at different levels of the stack. For instance, we might want to keep high-level data about financial market conditions (bear or bull market) available at a high level, whereas at a lower level we only record shorter-term temporal dynamics. \n",
"\n",
"Beyond all this abstract discussion it is probably easiest to understand the family of models we are interested in by reviewing the diagram below. It describes a deep recurrent neural network with $L$ hidden layers. Each hidden state is continuously passed to the next time step of the current layer and the next layer of the current time step.\n",
"\n",
"![ Architecture of a deep recurrent neural network. ](../img/deep-rnn.svg)\n",
"\n",
"## Functional Dependencies\n",
"\n",
"At time step $t$ we assume that we have a minibatch $\\mathbf{X}_t \\in \\mathbb{R}^{n \\times d}$ (number of examples: $n$, number of inputs: $d$). The hidden state of hidden layer $\\ell$ ($\\ell=1,\\ldots,T$) is $\\mathbf{H}_t^{(\\ell)} \\in \\mathbb{R}^{n \\times h}$ (number of hidden units: $h$), the output layer variable is $\\mathbf{O}_t \\in \\mathbb{R}^{n \\times q}$ (number of outputs: $q$) and a hidden layer activation function $f_l$ for layer $l$. We compute the hidden state of layer $1$ as before, using $\\mathbf{X}_t$ as input. For all subsequent layers the hidden state of the previous layer is used in its place.\n",
"\n",
"$$\\begin{aligned}\n",
"\\mathbf{H}_t^{(1)} & = f_1\\left(\\mathbf{X}_t, \\mathbf{H}_{t-1}^{(1)}\\right) \\\\\n",
"\\mathbf{H}_t^{(l)} & = f_l\\left(\\mathbf{H}_t^{(l-1)}, \\mathbf{H}_{t-1}^{(l)}\\right)\n",
"\\end{aligned}$$\n",
"\n",
"Finally, the output of the output layer is only based on the hidden state of hidden layer $L$. We use the output function $g$ to address this:\n",
"\n",
"$$\\mathbf{O}_t = g \\left(\\mathbf{H}_t^{(L)}\\right)$$\n",
"\n",
"Just as with multilayer perceptrons, the number of hidden layers $L$ and number of hidden units $h$ are hyper parameters. In particular, we can pick a regular RNN, a GRU or an LSTM to implement the model.\n",
"\n",
"## Concise Implementation\n",
"\n",
"Fortunately many of the logistical details required to implement multiple layers of an RNN are readily available in Gluon. To keep things simple we only illustrate the implementation using such built-in functionality. The code is very similar to the one we used previously for LSTMs. In fact, the only difference is that we specify the number of layers explicitly rather than picking the default of a single layer. Let's begin by importing the appropriate modules and data."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "17"
}
},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import d2l\n",
"from mxnet import nd\n",
"from mxnet.gluon import rnn\n",
"\n",
"corpus_indices, vocab = d2l.load_data_time_machine()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The architectural decisions (parameters, etc.) are very similar to those of previous sections. We pick the same number of inputs and outputs as we have distinct tokens, i.e. `vocab_size`. The number of hidden units is still 256 and we retain a learning rate of 100. The only difference is that we now select a nontrivial number of layers `num_layers = 2`. Since the model is somewhat slower to train we use 3000 iterations."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "22"
}
},
"outputs": [],
"source": [
"num_inputs, num_hiddens, num_layers, num_outputs = len(vocab), 256, 2, len(vocab)\n",
"ctx = d2l.try_gpu()\n",
"num_epochs, num_steps, batch_size, lr, clipping_theta = 500, 35, 32, 5, 1\n",
"prefixes = ['traveller', 'time traveller']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Training\n",
"\n",
"The actual invocation logic is identical to before and we re-use `train_and_predict_rnn_gluon`. The only difference is that we now instantiate two layers with LSTMs. This rather more complex architecture and the large number of epochs slow down training considerably."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"attributes": {
"classes": [],
"id": "",
"n": "8"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 125, perplexity 8.354891, time 6.47 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 250, perplexity 1.065868, time 6.44 sec\n",
" - traveller smiled. 'are you sure we can move freely in space\n",
" - time traveller smiled. 'are you sure we can move freely in space\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 375, perplexity 1.024141, time 6.57 sec\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"epoch 500, perplexity 1.024588, time 6.48 sec\n",
" - traveller smiled. 'are you sure we can move freely in space\n",
" - time traveller smiled. 'are you sure we can move freely in space\n"
]
}
],
"source": [
"lstm_layer = rnn.LSTM(hidden_size = num_hiddens, num_layers=num_layers)\n",
"model = d2l.RNNModel(lstm_layer, len(vocab))\n",
"d2l.train_and_predict_rnn_gluon(model, num_hiddens, corpus_indices, vocab, \n",
" ctx, num_epochs, num_steps, lr, \n",
" clipping_theta, batch_size, prefixes)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"* In deep recurrent neural networks, hidden state information is passed to the next time step of the current layer and the next layer of the current time step.\n",
"* There exist many different flavors of deep RNNs, such as LSTMs, GRUs or regular RNNs. Conveniently these models are all available as parts of the `rnn` module in Gluon. \n",
"* Initialization of the models requires care. Overall, deep RNNs require considerable amount of work (learning rate, clipping, etc) to ensure proper convergence. \n",
"\n",
"## Exercises\n",
"\n",
"1. Try to implement a two-layer RNN from scratch using the [\"single layer implementation\"](rnn-scratch.md) we discussed in an earlier section. \n",
"2. Replace the LSTM by a GRU and compare the accuracy.\n",
"3. Increase the training data to include multiple books. How low can you go on the perplexity scale?\n",
"4. Would you want to combine sources of different authors when modeling text? Why is this a good idea? What could go wrong?\n",
"\n",
"## Scan the QR Code to [Discuss](https://discuss.mxnet.io/t/2369)\n",
"\n",
"![](../img/qr_deep-rnn.svg)"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}