{
 "cells": [
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "---\n",
    "execute:\n",
    "  enabled: false\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!-- d2l:prose id=rnn-md-1 fw=all -->\n",
    "\n",
    "# Recurrent Neural Networks\n",
    "\n",
    "\n",
    "In that section we introduced $n$-gram models, where the conditional probability of a token $x_t$ at time step $t$ depends only on the $n-1$ preceding tokens.\n",
    "To let tokens earlier than time step $t-(n-1)$ influence $x_t$ we would have to increase $n$, but the number of parameters grows exponentially with it: we must store $|\\mathcal{V}|^n$ numbers for a vocabulary $\\mathcal{V}$.\n",
    "Rather than model $P(x_t \\mid x_{t-1}, \\ldots, x_1)$ directly, then, it is preferable to use a latent variable model,\n",
    "\n",
    "$$P(x_t \\mid x_{t-1}, \\ldots, x_1) \\approx P(x_t \\mid h_{t-1}),$$\n",
    "\n",
    "where $h_{t-1}$ is a *hidden state* that stores the sequence information up to time step $t-1$.\n",
    "In general, the hidden state at any time step $t$ can be computed from both the current input $x_{t}$ and the previous hidden state $h_{t-1}$:\n",
    "\n",
    "$$h_t = f(x_{t}, h_{t-1}).$$\n",
    "\n",
    "For a sufficiently powerful function $f$ in the equation, the latent variable model is not an approximation at all: $h_t$ might simply store everything it has observed so far.\n",
    "Whether that is useful depends on how compactly $f$ can summarize the past, an issue we return to throughout this chapter and the next.\n",
    "\n",
    "Recall the hidden layers and hidden units of that section.\n",
    "It is worth stressing that hidden layers and hidden states name two very different things.\n",
    "Hidden layers are layers that are hidden from view on the path from input to output.\n",
    "Hidden states are, technically speaking, *inputs* to whatever we compute at a given step, and they can only be computed from data seen at earlier time steps.\n",
    "\n",
    "*Recurrent neural networks* (RNNs) are neural networks with hidden states. Before defining the RNN, we first revisit the MLP of that section."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "rnn-recurrent-neural-networks"
   },
   "source": [
    "from d2l import torch as d2l\n",
    "import torch"
   ],
   "execution_count": 1,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!-- d2l:prose id=rnn-md-2 fw=all -->\n",
    "\n",
    "## Neural Networks without Hidden States\n",
    "\n",
    "Consider an MLP with a single hidden layer and activation function $\\phi$.\n",
    "Given a minibatch of examples $\\mathbf{X} \\in \\mathbb{R}^{n \\times d}$ with batch size $n$ and $d$ inputs, the hidden layer output $\\mathbf{H} \\in \\mathbb{R}^{n \\times h}$ is\n",
    "\n",
    "$$\\mathbf{H} = \\phi(\\mathbf{X} \\mathbf{W}_{\\textrm{xh}} + \\mathbf{b}_\\textrm{h}).$$\n",
    "\n",
    "In the equation, $\\mathbf{W}_{\\textrm{xh}} \\in \\mathbb{R}^{d \\times h}$ is a weight, $\\mathbf{b}_\\textrm{h} \\in \\mathbb{R}^{1 \\times h}$ a bias (broadcast over the batch, see that section), and $h$ the number of hidden units.\n",
    "Feeding $\\mathbf{H}$ into the output layer gives\n",
    "\n",
    "$$\\mathbf{O} = \\mathbf{H} \\mathbf{W}_{\\textrm{hq}} + \\mathbf{b}_\\textrm{q},$$\n",
    "\n",
    "where $\\mathbf{O} \\in \\mathbb{R}^{n \\times q}$ is the output, $\\mathbf{W}_{\\textrm{hq}} \\in \\mathbb{R}^{h \\times q}$ the weight, and $\\mathbf{b}_\\textrm{q} \\in \\mathbb{R}^{1 \\times q}$ the bias of the output layer.\n",
    "For a classification problem $\\mathrm{softmax}(\\mathbf{O})$ gives a distribution over the $q$ output categories.\n",
    "\n",
    "To turn this into a language model, feed the MLP a fixed window of the previous tokens and ask it to predict the next one.\n",
    "This *neural $n$-gram*, first proposed by @Bengio.Ducharme.Vincent.ea.2003, replaces the lookup table of counts with a learned function, so unseen contexts are no longer forced to probability zero.\n",
    "Training is exactly the supervised setup of that section: pick window-target pairs at random and learn the parameters by automatic differentiation and stochastic gradient descent.\n",
    "What it cannot escape is the fixed window itself. Just like a plain $n$-gram, a neural $n$-gram sees only $n-1$ tokens of context, and looking further back means enlarging the window and paying for more parameters. This is the limitation the hidden state removes.\n",
    "\n",
    "## Recurrent Neural Networks with Hidden States\n",
    "\n",
    "Matters are entirely different once we keep a hidden state. Let us look at the structure in more detail.\n",
    "\n",
    "Assume that at time step $t$ we have a minibatch of inputs $\\mathbf{X}_t \\in \\mathbb{R}^{n \\times d}$: each of the $n$ rows is one example of the sequence at step $t$.\n",
    "Denote by $\\mathbf{H}_t  \\in \\mathbb{R}^{n \\times h}$ the hidden state at time step $t$.\n",
    "Unlike the MLP, we retain the hidden state $\\mathbf{H}_{t-1}$ from the previous step and introduce a new weight $\\mathbf{W}_{\\textrm{hh}} \\in \\mathbb{R}^{h \\times h}$ that describes how to use it in the current step.\n",
    "The current hidden state combines the current input with the previous hidden state:\n",
    "\n",
    "$$\\mathbf{H}_t = \\phi(\\mathbf{X}_t \\mathbf{W}_{\\textrm{xh}} + \\mathbf{H}_{t-1} \\mathbf{W}_{\\textrm{hh}}  + \\mathbf{b}_\\textrm{h}).$$\n",
    "\n",
    "Compared with the equation, the equation adds the term $\\mathbf{H}_{t-1} \\mathbf{W}_{\\textrm{hh}}$ and thus instantiates the equation.\n",
    "From the relationship between hidden layer outputs $\\mathbf{H}_t$ and $\\mathbf{H}_{t-1}$ at adjacent time steps, we see that these variables capture and retain the sequence's historical information up to the current step, just like the state, or memory, of the network at the current step. Such a hidden layer output is therefore called a *hidden state*.\n",
    "Because the computation of the equation reuses the same definition from the previous step, it is *recurrent*. Hence networks with hidden states based on recurrent computation are named *recurrent neural networks*, and layers performing the computation of the equation are called *recurrent layers*.\n",
    "\n",
    "For time step $t$, the output layer is analogous to the computation in the MLP:\n",
    "\n",
    "$$\\mathbf{O}_t = \\mathbf{H}_t \\mathbf{W}_{\\textrm{hq}} + \\mathbf{b}_\\textrm{q}.$$\n",
    "\n",
    "The parameters of the RNN are the weights $\\mathbf{W}_{\\textrm{xh}} \\in \\mathbb{R}^{d \\times h}$ and $\\mathbf{W}_{\\textrm{hh}} \\in \\mathbb{R}^{h \\times h}$ and the bias $\\mathbf{b}_\\textrm{h} \\in \\mathbb{R}^{1 \\times h}$ of the hidden layer, together with the weights $\\mathbf{W}_{\\textrm{hq}} \\in \\mathbb{R}^{h \\times q}$ and the bias $\\mathbf{b}_\\textrm{q} \\in \\mathbb{R}^{1 \\times q}$ of the output layer.\n",
    "It is worth stressing that these same parameters are reused at every time step, so the parametrization cost of an RNN does not grow as the number of time steps increases.\n",
    "\n",
    "the figure unrolls the RNN across three adjacent time steps.\n",
    "At each step the network reads the input $\\mathbf{X}_t$, updates the hidden state to $\\mathbf{H}_t$ from $\\mathbf{X}_t$, $\\mathbf{H}_{t-1}$, and the shared weights, and emits the output $\\mathbf{O}_t$.\n",
    "The horizontal arrows carry the state forward: $\\mathbf{H}_t$ both feeds the output $\\mathbf{O}_t$ and becomes an input to the next step's hidden state $\\mathbf{H}_{t+1}$.\n",
    "Reading the equation another way, the hidden-state update is exactly a fully connected layer (with activation $\\phi$) applied to the concatenation of $\\mathbf{X}_t$ and $\\mathbf{H}_{t-1}$, whose weight matrix is $\\mathbf{W}_{\\textrm{xh}}$ stacked on top of $\\mathbf{W}_{\\textrm{hh}}$.\n",
    "\n",
    "![An RNN with a hidden state, unrolled across three adjacent time steps. The same weights are reused at every step.](../img/mdl-rnn-unfolded.svg)\n",
    "\n",
    "We just claimed that computing $\\mathbf{X}_t \\mathbf{W}_{\\textrm{xh}} + \\mathbf{H}_{t-1} \\mathbf{W}_{\\textrm{hh}}$ is equivalent to a single matrix multiplication of the concatenation of $\\mathbf{X}_t$ and $\\mathbf{H}_{t-1}$ with the concatenation of $\\mathbf{W}_{\\textrm{xh}}$ and $\\mathbf{W}_{\\textrm{hh}}$.\n",
    "This can be proven directly, but the following snippet is more convincing.\n",
    "We define matrices `X`, `W_xh`, `H`, and `W_hh` whose shapes are (3, 1), (1, 4), (3, 4), and (4, 4), respectively.\n",
    "Multiplying `X` by `W_xh`, and `H` by `W_hh`, and adding the two products yields a matrix of shape (3, 4)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "rnn-recurrent-neural-networks-with-hidden-states-1"
   },
   "source": [
    "X, W_xh = d2l.randn(3, 1), d2l.randn(1, 4)\n",
    "H, W_hh = d2l.randn(3, 4), d2l.randn(4, 4)\n",
    "d2l.matmul(X, W_xh) + d2l.matmul(H, W_hh)"
   ],
   "execution_count": 2,
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "tensor([[-0.5367, -0.1777,  2.4283, -0.5488],\n        [ 5.6435, -1.8725,  2.6781,  0.3557],\n        [ 3.8535, -1.2030,  3.6370, -1.0466]])"
     },
     "metadata": {},
     "execution_count": 2
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!-- d2l:prose id=rnn-md-3 fw=all -->\n",
    "\n",
    "Now we concatenate `X` and `H` along columns (axis 1) and `W_xh` and `W_hh` along rows (axis 0), giving matrices of shape (3, 5) and (5, 4).\n",
    "Multiplying these two concatenated matrices yields the same matrix of shape (3, 4) as above."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "rnn-recurrent-neural-networks-with-hidden-states-2"
   },
   "source": [
    "d2l.matmul(d2l.concat((X, H), 1), d2l.concat((W_xh, W_hh), 0))"
   ],
   "execution_count": 3,
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "tensor([[-0.5367, -0.1777,  2.4283, -0.5488],\n        [ 5.6435, -1.8725,  2.6781,  0.3557],\n        [ 3.8535, -1.2030,  3.6370, -1.0466]])"
     },
     "metadata": {},
     "execution_count": 3
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!-- d2l:prose id=rnn-md-4 fw=all -->\n",
    "\n",
    "This concatenate-then-multiply form is what most framework RNN implementations use in practice.\n",
    "\n",
    "### Constant Memory per Step\n",
    "\n",
    "The recurrence the equation has a property that will organize the rest of this part of the book: it runs in *constant memory per step*.\n",
    "The hidden state $\\mathbf{H}_t$ has a fixed size $n \\times h$ no matter how long the sequence is, and to compute it we need only the previous state $\\mathbf{H}_{t-1}$ and the current input $\\mathbf{X}_t$.\n",
    "Everything older can be discarded the moment $\\mathbf{H}_t$ is formed.\n",
    "An RNN can therefore consume a sequence of any length while its memory and its per-step computation stay flat, in sharp contrast to the neural $n$-gram, whose window (and cost) must grow to reach further back.\n",
    "This is recurrence's defining trade. The state is a *lossy*, fixed-size summary of the whole past, so to record something new it must overwrite something old, and the network has to learn what is worth keeping.\n",
    "The opposite design keeps every past representation around and lets the model look back over all of them on demand: this is *attention* (that section), which buys exact recall at a memory and compute cost that instead grows with the sequence length.\n",
    "Bounded, forgetful state versus unbounded, exact memory is the tension the rest of this part explores: first by making the recurrent state far better at remembering (that section), then by turning to attention.\n",
    "\n",
    "## RNN Language Models\n",
    "\n",
    "Recall from that section that a language model predicts the next token from the current and past tokens, so we obtain training targets by shifting the input sequence forward by one token.\n",
    "@Bengio.Ducharme.Vincent.ea.2003 first used a neural network for this task; an RNN does it with a hidden state in place of a fixed window.\n",
    "\n",
    "We tokenize text with the byte-pair encoding (BPE) tokenizer of that section, which splits text into subword tokens drawn from a fixed vocabulary $\\mathcal{V}$.\n",
    "(Earlier presentations of RNN language models tokenized into single characters; subword tokens give shorter sequences for the same text and are what modern language models actually use.)\n",
    "A token is not fed to the network as a raw id. Instead each id indexes a learned *embedding* table and returns a $d$-dimensional vector; stacking the vectors of a minibatch at step $t$ gives the input $\\mathbf{X}_t \\in \\mathbb{R}^{n \\times d}$.\n",
    "This embedding lookup replaces the one-hot encoding used in older treatments. It is the same operation, selecting one row of a matrix per token, but the rows are trained rather than fixed, and the table is looked up directly instead of materializing a one-hot vector of length $|\\mathcal{V}|$.\n",
    "\n",
    "At each step the hidden state $\\mathbf{H}_t$ is mapped by the output layer to a vector of $|\\mathcal{V}|$ logits, one per vocabulary token, and a softmax turns these into $P(x_{t+1} \\mid x_t, \\ldots, x_1)$, the model's distribution over the next token.\n",
    "Because $\\mathbf{H}_t$ summarizes the entire prefix, the RNN expresses the full autoregressive factorization $P(x_1, \\ldots, x_T) = \\prod_{t} P(x_t \\mid x_{t-1}, \\ldots, x_1)$ with no Markov truncation.\n",
    "\n",
    "the figure shows the training setup.\n",
    "Suppose the corpus contains \"the time machine by\", which the BPE tokenizer splits into the tokens `the`, `time`, `mach`, `ine`, `by` (the word \"machine\" becomes the two subword tokens `mach` and `ine`).\n",
    "The input sequence is `the`, `time`, `mach`, `ine`, and the target sequence is the same stream shifted forward by one token, `time`, `mach`, `ine`, `by`.\n",
    "At every step we run a softmax over the output and compute the cross-entropy against the target token, then average the losses over the sequence.\n",
    "Because of the recurrence, the output $\\mathbf{O}_3$ is determined by the inputs `the`, `time`, `mach`, so its loss measures how well the model predicts the next token `ine` from that prefix.\n",
    "Minimizing perplexity (that section), the exponentiated average cross-entropy, is exactly this objective.\n",
    "\n",
    "![An RNN language model trained by teacher forcing over subword (BPE) tokens. The target sequence is the input shifted forward by one token, so each step is trained to predict the next token.](../img/mdl-rnn-lm-shift.svg)\n",
    "\n",
    "### Teacher Forcing\n",
    "\n",
    "Notice what the model consumes during training: at each step its input is the *ground-truth* previous token, taken from the corpus, regardless of what the model itself predicted the step before.\n",
    "Training a sequence model on the true prefixes in this way is called *teacher forcing*.\n",
    "It makes training efficient and stable, since every step is conditioned on a correct history and the whole sequence can be scored in a single pass.\n",
    "\n",
    "At generation time there is no ground truth to fall back on.\n",
    "We feed the model a short prompt, sample (or take the argmax of) its next-token distribution, and feed that predicted token back in as the next input, repeating to extend the sequence.\n",
    "The model is now conditioned on its *own* outputs rather than on gold prefixes.\n",
    "This is precisely the gap between one-step and multi-step prediction from that section: a token the model gets slightly wrong perturbs the state it feeds itself, and small errors can compound over a long generation.\n",
    "Teacher forcing trains for the easy case and deploys in the hard one, a mismatch that later chapters address both in how we train and in how we decode.\n",
    "\n",
    "In practice each token is a $d$-dimensional embedding and we use a batch size $n > 1$, so the input $\\mathbf{X}_t$ at time step $t$ is the $n \\times d$ matrix of that section.\n",
    "In the following sections we implement RNN language models end to end.\n",
    "\n",
    "## Summary\n",
    "\n",
    "A recurrent neural network computes its hidden state recurrently, reusing one set of weights at every time step, so its parameter count is independent of sequence length and its state is a fixed-size summary of the entire past.\n",
    "This makes the RNN a natural language model: embed each token, update the hidden state, project it to logits over the vocabulary, and train by teacher forcing to predict the next token, minimizing perplexity.\n",
    "\n",
    "What could go wrong?\n",
    "The whole past has to fit through one fixed-size state, and training the model means backpropagating through the recurrence over many time steps.\n",
    "The gradient then becomes a long product of Jacobians, which tends to vanish or explode (that section), making it hard for a plain RNN to actually learn long-range dependencies.\n",
    "Gating the state, the subject of that section, is the standard fix.\n",
    "\n",
    "## Exercises\n",
    "\n",
    "1. If we use an RNN to predict the next token in a text sequence, what is the required dimension for any output?\n",
    "1. Why can an RNN express the conditional probability of a token at some time step based on all the previous tokens in the sequence?\n",
    "1. What happens to the gradient if you backpropagate through a long sequence?\n",
    "1. Consider a context of $k$ previous tokens over a vocabulary of size $|\\mathcal{V}|$. A $(k+1)$-gram stores a count for every possible context, whereas an RNN with $h$ hidden units and $d$-dimensional token embeddings stores a fixed set of weights. Write down the parameter (or table-entry) count for each, and evaluate both for $|\\mathcal{V}| = 10{,}000$, $k = 20$, and $h = d = 256$. Which one grows with $k$, and by how much?\n",
    "1. What are some of the problems associated with the language model described in this section, and how might the next chapters address them?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!-- d2l:prose id=rnn-md-5 fw=pytorch -->\n",
    "\n",
    "[Discussions](https://d2l.discourse.group/t/1050)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "name": "d2l-pytorch",
   "display_name": "d2l (PyTorch)",
   "language": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
