6287 words
31 minutes
Recurrent Neural Networks (RNNs)
2025-09-19
2026-09-19

Recurrent Neural Networks (RNNs) lays the foundation for Transformers, to systematically study it involves 3 layers:

  1. Intuition & Visualization
  2. Formal Mathematical Theory, and
  3. Implementation from Scratch

Phase 1 - Intuition & Conceptual Visualization#

There’s something magical about Recurrent Neural Networks (RNNs). Sometimes the ratio of how simple a model is to the quality of the results we get out of it blows past our expectations, and RNN gives us one of those times.

Depending on their background one might be wondering: What makes Recurrent Networks so special? A glaring limitation of Vanilla Neural Networks (and also Convolutional Networks) is that their API is too constrained: they accept a fixed-sized vector as input (e.g. an image) and produce a fixed-sized vector as output (e.g. probabilities of different classes). Not only that: These models perform this mapping using a fixed amount of computational steps (e.g. the number of layers in the model). The core reason that recurrent nets are more exciting is that they allow us to operate over sequences of vectors: Sequences in the input, the output, or in the most general case both. A few examples may make this more concrete:

Each rectangle is a vector and arrows represent functions (e.g. matrix multiply). Input vectors are in red, output vectors are in blue and green vectors hold the RNN’s state (more on this later). One great thing about the RNNs is that they offer a lot of flexibility on how we wire up the neural network architecture. Normally when we are working with neural networks, we are given a fixed sized input vector (red boxes above), then we process it with some hidden layers (green), and we produce a fixed sized output vector (blue). The left-most model in figure below is the Vanilla Neural Networks, which receives a single input and produce one output (The green box in between actually represents layers of neurons). The rest of the models on the right are all Recurrent Neural Networks that allow us to operate over sequences of input, output, or both at the same time:

  • An example of one-to-many model is image captioning where we are given a fixed sized image and produce a sequence of words that describe the content of that image through RNN
  • An example of many-to-one task is sentiment classification in NLP where we are given a sequence of words of a sentence and then classify what sentiment (e.g. positive or negative) that sentence is.
  • An example of many-to-many task is machine translation in NLP, where we can have an RNN that takes a sequence of words of a sentence in English, and then this RNN is asked to produce a sequence of words of a sentence in German.
  • There is also a variation of many-to-many task as shown in the last model in figure below, where the model generates an output at every timestep. An example of this many-to-many task is video classification on a frame level where the model classifies every single frame of video with some number of classes. We should note that we don’t want this prediction to only be a function of the current timestep (current frame of the video), but also all the timesteps (frames) that have come before this video.

The sequence regime of operation is much more powerful compared to fixed networks that are doomed from the get-go by a fixed number of computational steps. Moreover, as we’ll see in a bit, RNNs combine the input vector with their state vector with a fixed (but learned) function to produce a new state vector. This can in programming terms be interpreted as running a fixed program with certain inputs and some internal variables. Viewed this way, RNNs essentially describe programs. In fact, it is known that RNNs are Turing-Complete in the sense that they can simulate arbitrary programs (with proper weights).

TIP

Training vanilla neural nets is optimization over functions, training recurrent nets is optimization over programs.

It’s worth mentioning that in practice most of us use a slightly different formulation called a Long Short-Term Memory (LSTM) network. The LSTM is a particular type of recurrent network that works slightly better in practice, owing to its more powerful update equation and some appealing backpropagation dynamics. The “Forget Gate” mechanism, as we introduce next, is an explicit memory management that is a precursor to how Attention mechanisms later decide what to “attend” to.

Understanding LSTM Networks#

We’ve seen so far that a recurrent neural network can be thought of as multiple copies of the same network, each passing a message to a successor:

In the last few years, there have been incredible success applying RNNs to a variety of problems: speech recognition, language modeling, translation, image captioning, etc. Essential to these successes is the use of LSTMs, a kind of recurrent neural network which works, for many tasks, much better than the standard version. Almost all exciting results based on recurrent neural networks are achieved with them.

The Problem of Long-Term Dependencies#

Sometimes, we only need to look at recent information to perform the present task. For example, consider a language model trying to predict the next word based on the previous ones. If we are trying to predict the last word in “the clouds are in the sky”, we don’t need any further context - it’s pretty obvious the next word is going to be sky. In such cases, where the gap between the relevant information and the place that it’s needed is small, RNNs can learn to use the past information.

There are cases, however, where we need more context. Consider trying to predict the last word in the text “I grew up in China … I speak fluent Chinese.” Recent information suggests that the next word is probably the name of a language, but if we want to narrow down which language, we need the context of France, from further back. It’s entirely possible for the gap between the relevant information and the point where it is needed to become very large. Unfortunately, as that gap grows, RNNs become unable to learn to connect the information.

Hochreiter, 1991 (German)

It is very worth mentioning the 1991 Diploma Thesis (equivalent to a Master’s thesis) of Sepp Hochreiter which explored in depth why RSS doesn’t learn in “long-term” and found some pretty fundamental reasons of why so.

The thesis is titled Untersuchungen zu dynamischen neuronalen Netzen (Investigations on Dynamic Neural Networks). It is widely considered a foundational document in the history of Deep Learning for 2 main reasons:

  1. Discovery of the Vanishing Gradient Problem: The thesis provides the first formal analysis of why training deep or recurrent neural networks is so difficult. Hochreiter proved that as error signals are propagated backward through many layers or time steps, they tend to either explode (become infinitely large) or vanish (decay exponentially to zero). This mathematical proof explained why standard Recurrent Neural Networks (RNNs) of the time could not learn to connect information over long time lags.
  2. Precursor to LSTM: This analysis laid the theoretical groundwork for the solution Hochreiter and Schmidhuber would later introduce: Long Short-Term Memory (LSTM). The thesis proposed specific architectural changes (like “constant error carousels”) to enforce constant error flow, which eventually evolved into the memory cells and gating mechanisms used in modern LSTMs.

LSTM Networks#

LSTMs, introduced by Hochreiter & Schmidhuber (1997), are explicitly designed to avoid the long-term dependency problem.

All recurrent neural networks have the form of a chain of repeating modules of neural network. In standard RNNs, this repeating module will have a very simple structure, such as a single tanh layer.

LSTMs also have this chain like structure, but the repeating module has a different structure. Instead of having a single neural network layer, there are four, interacting in a very special way.

Let’s break this down by first making sure we get the notations right:

The key to LSTMs is the cell state, the horizontal line running through the top of the diagram. LSTM has the ability to remove or add information to the cell state, carefully regulated by structures called gates, which are a way to optionally let information flow through. They are composed out of a sigmoid neural net layer and a pointwise multiplication operation:

TIP

The form of data inside the cell state is a vector of numbers. Here is a concrete example of what a Cell State might look like at a specific time step:

# A hypothetical Cell State vector (C_t) with 4 hidden units
cell_state = [0.85, -0.92, 0.01, 0.99]

In the context of the Language Model of predicting the next word based on grammar, each number in this vector tracks a specific piece of context “remembered” from the text read so far.

If we could interpret each number perfectly (which is a simplification, but helpful for understanding), that vector might translate to:

  • 0.85 (Unit 1): Represents Plurality.

    • High positive value could mean the subject is Plural (e.g., “The dogs”).
    • High negative value could mean the subject is Singular (e.g., “The dog”).
    • Near zero means the network isn’t sure or it’s not relevant right now.
  • 0.92 (Unit 2): Represents Gender.

    • High positive value could mean Male.
    • High negative value could mean Female (e.g., “She”, “Queen”).
  • 0.01 (Unit 3): Represents Tense.

    • Near zero implies the network hasn’t seen a strong signal for Past vs. Future yet, or it has “forgotten” the tense because a new sentence started.
  • 0.99 (Unit 4): Represents Quote Status.

    • High positive value could mean “We are currently inside an open quote.”

The first step of a LSTM is to decide that information we need to throw away from the cell state. This decision is made by a sigmoid layer called the forget gate layer shown below:

Step 1

It looks at ht1h_{t - 1} and xtx_t and outputs a number between 0 and 1 for each number in the cell state Ct1C_{t-1}. Each number describes how much of each component should be let through. A value of zero means “let nothing through” while a value of one means “let everything through”. In the example of our language model, the cell state might include the gender of the present subject, so that the correct pronouns can be used. When we see a new subject, we want to forget the gender of the old subject.

Mathematically, we multiply the old state vector by ftf_t. For instance

  • If the network encounters the word “ate,” it doesn’t need to change the Gender number (-0.92). It just passes it along to the next step so the network remembers “the subject is female” for later.
  • Forget/Update: If the network encounters a period (.) or a new subject like “He”, the Forget Gate might multiply the Gender number by 0, erasing it, and the Input Gate (see below) might add a new value (like +0.9 for Male) to update the state.

The next step is to decide what new information we’re going to store in the cell state. As depicted below, this step has 2 parts:

  1. A sigmoid layer called the input gate layer decides which values to update and a tanh layer creates a vector of new values C~\widetilde{C}
  2. Combine the two to create an update to the state

For example, we would want to add the gender of the new subject to the cell state, to replace the old one we are forgetting.

The update of cell state combing the first 2 steps runs as follows:

Finally, we need to decide what we are going to output from this LSTM. First, we run a sigmoid layer which decides what parts of the cell state we are going to output. Then, we put the cell state through tanh (to squash the values to be between −1 and 1) and multiply it by the output of the sigmoid gate, so that we only output the parts we decided to.

For the language model example, since it just saw a subject, it might want to output information relevant to a verb, in case that’s what is coming next. For example, it might output whether the subject is singular or plural, so that we know what form a verb should be conjugated into if that’s what follows next.

Phase 2 - Formal Theory & Mathematics#

Now we have the intuition, we shall proceed to rigorously study RNN. Here are some good resources:

Let’s start with a simple RNN called “character-level language model” where, for example, we input a prefix of a word such as “hell” and the model outputs a complete word “hello”. We call inputs like “hell” a sequence.

How do we train such model? One approach is to have one function invoked 4 times, with each time taking a single character as input and calculates an output:

Input for the function is actually a one-hot encoded vector representing a single character

In our “hello” example above, the input sequence would be “h”, “e”, “l”, “l”, “o”. For each of these characters, the input to the function is not the character itself, but a vector. This vector has a size equal to the total number of unique characters in our vocabulary, i.e. a vocabulary of four possible letters “helo”. For a specific character, the vector will have a value of 1 at the index corresponding to that character, and 0 everywhere else.

For example, the input for the character “h” would be a vector of length 4. This vector would have a value of 1 at the 1st position (since ‘h’ is the 1st letter of the alphabet) and 0s in all other 3 positions. The next input would be the one-hot encoded vector for “e”, and so on. This process allows the function to handle sequential data by processing one character at a time.

But one might have noticed that if the 3rd invocation produces f(l)=lf('l') = 'l', then why would the 4th one, given the same input, outputs a different character of ‘o’? This suggests that we should take the history into account. Instead of having ff depend on 1 parameter, we now have it take 2 parameters.

  1. a character, and
  2. a variable that summarizes the previous calculations:

Now it makes much more sense with:

f(‘l’,h2)=‘l’f(\text{‘l'}, h_2) = \text{‘l'}f(‘l’,h3)=‘o’f(\text{‘l'}, h_3) = \text{‘o'}

But what if we want to predict a longer or shorter word? For example, how about predicting “cat” by “ca”? That’s simple, we will have 2 black boxes to do the work.

What if the function ff is not smart enough to produce the correct output everytime? We will simply collect a lot of examples such as “cat” and “hello”, and feed them into the boxes to train them until they can output correct vocabulary like “cat” and “hello”.

This is the idea behind RNN. It’s recurrent because the boxed function gets invoked repeatedly for each element of the sequence. In the case of our character-level language model, element is a character such as “e” and sequence is a string like “hell”:

CAUTION

The diagram below is not multiple functions chained together, but a single function being repeatedly invoked

Each function ff is a network unit containing 2 perceptrons with one perceptron computing the “history” like h1h_1, h2h_2, and h3h_3.

At the core, RNNs accept an input vector x and give us an output vector y. This output vector’s contents are influenced not only by the input we just fed in, but also on the entire history of inputs we’ve fed in from the past. The RNN’s API consists of a single step function:

rnn = RNN()
y = rnn.step(x)

This is where RNN starts to model the notion of “memory”: The RNN class has some internal state that is updated every time step() is called. In the simplest case this state consists of a single hidden vector h:

class RNN:
# ...
def step(self, x):
# update the hidden state
self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
# compute the output vector
y = np.dot(self.W_hy, self.h)
return y

The code snippet above specifies the forward pass of a vanilla RNN. This RNN’s parameters are the 3 matrices:

  1. W_hh,
  2. W_xh, and
  3. W_hy

The hidden state self.h is initialized with the zero vector. The np.tanh function implements a non-linearity that squashes the activations to the range [-1, 1]. Notice briefly how this works: There are 2 terms inside the tanh: one is based on the previous hidden state and one is based on the current input. In numpy, np.dot is matrix multiplication. The two intermediates interact with addition, and then get squashed by the tanh into the new state vector.

We initialize the matrices of the RNN with random numbers and the bulk of work during training goes into finding the matrices that give rise to desirable behavior, as measured with some loss function that expresses our preference to what kinds of outputs y we would like to see in response to our input sequences x.

The step function above specifies the forward pass of RNN. There are 3 parameters WhhW_{hh}, WxhW_{xh}, and WhyW_{hy}. The hidden vector, or more generally the hidden state, is defined by

h(t)=g1(Whhh(t1)+Wxhx(t)+bh)h^{(t)} = g_1\left( W_{hh}h^{(t - 1)} + W_{xh}x^{(t)} + b_h \right)

where tt is the index of the “black boxes” shown earlier. In our example of “hell”, t{1,2,3,4}t \in \{ 1, 2, 3, 4 \}. The hidden state hh is usually initialized with zero vector (simulating “no memory at all”). There are 2 terms inside the g1g_1:

  1. one term based on the previous hidden state Whhh(t1)W_{hh}h^{(t - 1)}, and
  2. the other term based on the current input Wxhx(t)W_{xh}x^{(t)}

In the program above we use numpy np.dot which is a matrix multiplication. The 2 terms interact with addition.

We initialize matrices WhhW_{hh}, WxhW_{xh}, and WhyW_{hy} with random numbers and the bulk of work during training goes into finding the matrices that gives rise to the desirable behavior, as measured with some loss function that expresses our preferences to what kind of output y we would like to see in response to our input sequence x

The value yy is given by

o(t)=g2(Wyhh(t)+bo)o^{(t)} = g_2\left( W_{yh}h^{(t)} + b_o \right)
What are g1g_1 and g2g_2?

They are activation functions which are used to change the linear function in a perceptron to a non-linear function. Please refer to Machine Learning by Mitchell, Tom M. (1997), Paperback (page 96) for why we bump it to non-linear.

A typical activation function for g1g_1 is tanhtanh:

tanh(x)=exexex+extanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}

which squashes the activations to the range [0,1][0, 1]

In practice, g2g_2 is constance, i.e. g2=1g_2 = 1

We get RNNs as neural networks if we stack up as follows:

y1 = rnn1.step(x)
y = rnn2.step(y1)

In other words we have two separate RNNs: One RNN is receiving the input vectors and the second RNN is receiving the output of the first RNN as its input. Except neither of these RNNs know or care - it’s all just vectors coming in and going out, and some gradients flowing through each module during backpropagation.

Forward Propagation Equations for RNN#

We now develop the forward propagation equations for the RNN. We assume the hyperbolic tangent activation function, i.e. tanh(x)=exexex+extanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} and that the output is discrete, as if the RNN is used to predict words or characters. A natural way to represent discrete variables is to regard the output o\boldsymbol{o} as giving the unnormalized log probabilities of each possible value of the discrete variable. We can then apply the softmax (discussed shortly) operation as a post-processing step to obtain a vector y^(t)\boldsymbol{\hat{y}^{(t)}} of normalized probabilities over the output.

Forward propagation begins with a specification of the initial state h(0)\boldsymbol{h}^{(0)}. The dimension of the hidden state h\boldsymbol{h} is independent of the dimension of the input or output sequences. In fact, h\boldsymbol{h} is a 3D array, whose 1st-dimensional size is exactly the number of RNN parameters.

Then, for each time step from t=1t = 1 to t=τt = \tau, we apply the following update equations:

h(t)=tanh(Whhh(t1)+Wxhx(t)+bh)o(t)=Wyhh(t)+boy^(t)=softmax(o(t))\color{green} \boxed{ \begin{gather*} \boldsymbol{h}^{(t)} = \tanh\left( \boldsymbol{W_{hh}}h^{(t - 1)} + \boldsymbol{W_{xh}}x^{(t)} + \boldsymbol{b_h} \right) \\ \\ \boldsymbol{o}^{(t)} = \boldsymbol{W_{yh}}\boldsymbol{h}^{(t)} + \boldsymbol{b_o} \\ \\ \boldsymbol{\hat{y}^{(t)}} = softmax(\boldsymbol{o}^{(t)}) \end{gather*} }

where

  • h(t)\boldsymbol{h}^{(t)} is the hidden state vector of size (τ+1)(\tau + 1)
  • o(t)\boldsymbol{o}^{(t)} is the output produced by the model at step tt where t{1,2,,τ}t \in \{1, 2, \cdots, \tau\}
  • y^(t)\boldsymbol{\hat{y}^{(t)}} is the normalized probability of o(t)\boldsymbol{o}^{(t)} at τ=t\tau = t
  • bh\boldsymbol{b_h} is the hidden bias vector of size τ\tau
  • bo\boldsymbol{b_o} is the output bias vector of size τ\tau
  • the size of Wxh\boldsymbol{W_{xh}} is (τ1)×τ(\tau - 1) \times \tau
  • the size of Whh\boldsymbol{W_{hh}} is (τ1)×(τ1)(\tau - 1) \times (\tau - 1)
  • the size of Wxh\boldsymbol{W_{xh}} is τ×(τ1)\tau \times (\tau - 1)

Note that this recurrent network maps an input sequence to an output sequence of the same length.

Loss Function of RNN#

According to the discussion of Machine Learning by Mitchell, Tom M. (1997), the key for training RNN or any neural network is through “specifying a measure for the training error”. We call this measure a loss function.

In RNN, the total loss for a given sequence of input x\boldsymbol{x} paired with a sequence of expected y\boldsymbol{y} is the sum of the losses over all the time steps, i.e.

L({x(1),...,x(τ)},{y(1),...,y(τ)})=tτL(t)\mathcal{L}\left( \{ \boldsymbol{x}^{(1)}, ..., \boldsymbol{x}^{(\tau)} \}, \{ \boldsymbol{y}^{(1)}, ..., \boldsymbol{y}^{(\tau)} \} \right) = \sum_t^{\tau} \mathcal{L}^{(t)}

Knowing the exact form of L(t)\mathcal{L}^{(t)} requires our intuitive understanding of cross-entropy

Cross-Entropy#

In information theory, the cross-entropy between two probability distributions pp and qq over the same underlying set of events measures the average number of bits needed to identify an event drawn from the set if a coding scheme used for the set is optimized for an estimated probability distribution qq, rather than the true distribution pp

Confused? Let’s put it in the context of Machine Learning. Machine Learning sees the world based on probability. The “probability distribution” identifies the various tasks to learn. For example, a daily language such as English or Chinese, can be seen as a probability distribution. The probability of “name” followed by “is” is far greater than “are” as in “My name is Jack”. We call such language distribution pp. The task of RNN (or Machine Learning in general) is to learn an approximated distribution of pp; we call this approximation qq

“The average number of bits needed” is can be seen as the distance between pp and qq given an event. In analogy of language, this can be the quantitative measure of the deviation between a real language phrase “My name is Jack” and “My name are Jack”.

At this point, it is easy to imagine that, in the Machine Learning world, the cross entropy indicates the distance between what the model believes the output distribution should be and what the original distribution really is.

Now we have an intuitive understanding of cross entropy, let’s formally define it. The cross-entropy of the discrete probability distribution qq relative to a distribution pp over a given set is defined as

H(p,q)=xp(x)logq(x)H(p, q) = -\sum_x p(x)\log q(x)

Since we assume the softmax probability distribution earlier, the probability distribution of q(x)q(x) is:

L=tp(t)logσ(o(t))=tlogσ(o(t))=tτlogy^(t)\mathcal{L} = -\sum_t p(t)\log\sigma(\boldsymbol{o}^{(t)}) = -\sum_t\log\sigma(\boldsymbol{o}^{(t)}) = -\sum_t^{\tau}\log\boldsymbol{\hat{y}}^{(t)}

where o\boldsymbol{o} is the predicted sequence by RNN and oio_i is the i-th element of the predicted sequence

Therefore, the total loss for a given sequence of input x\boldsymbol{x} paired with a sequence of expected y\boldsymbol{y} is the sum of the losses over all the time steps, i.e.

L({x(1),...,x(τ)},{y(1),...,y(τ)})=tτL(t)=tτlogy^(t)\color{green} \boxed{ \mathcal{L}\left( \{ \boldsymbol{x}^{(1)}, ..., \boldsymbol{x}^{(\tau)} \}, \{ \boldsymbol{y}^{(1)}, ..., \boldsymbol{y}^{(\tau)} \} \right) = \sum_t^{\tau} \mathcal{L}^{(t)} = -\sum_t^{\tau}\log\boldsymbol{\hat{y}}^{(t)} }
What is the Mathematical form of p(i)p(i) in RNN? Why would it become 1?

By definition, p(i)p(i) is the true distribution whose exact functional form is unknown. In the language of Approximation Theory, p(i)p(i) is the function that RNN is trying to learn or approximate mathematically.

Although the p(i)p(i) makes the exact form of L\mathcal{L} unknown, computationally p(i)p(i) is perfectly defined in each training example. Taking our “hello” example:

The 4 probability distributions of q(x)q(x) is “reflected” in the output layer of this example. They are “reflecting” the probability distribution of q(x)q(x) because they are only oo values and have not been transformed to the σ\sigma distribution yet. But in this case, we are 100% sure that the true probability distribution p(i)p(i) for the 4 outputs are

(0100),(0010),(0010),(0001)\begin{pmatrix}0\\1\\0\\0\end{pmatrix}, \begin{pmatrix}0\\0\\1\\0\end{pmatrix}, \begin{pmatrix}0\\0\\1\\0\end{pmatrix}, \begin{pmatrix}0\\0\\0\\1\end{pmatrix}

respectively. That is all we need for calculating the L\mathcal{L}

The softmax function takes as input a vector zz of KK real numbers, and normalizes it into a probability distribution consisting of KK probabilities proportional to the exponentials of the input numbers. That is, prior to applying softmax, some vector components could be negative, or greater than one; and might not sum to 1; but after applying softmax, each component will be in the interval (0,1)(0, 1) and the components will add up to 1, so that they can be interpreted as probabilities. Furthermore, the larger input components will correspond to larger probabilities.

For a vector zz of KK real numbers, the the standard (unit) softmax function σ:RK(0,1)K\sigma: \mathbb{R}^K \mapsto (0, 1)^K, where K1K \ge 1 is defined by

σ(z)i=ezij=1Kezj\sigma(\boldsymbol{z})_i = \frac{e^{z_i}}{\sum_{j = 1}^Ke^{z_j}}

where i=1,2,...,Ki = 1, 2, ..., K and x=(x1,x2,...,xK)RK\boldsymbol{x} = (x_1, x_2, ..., x_K) \in \mathbb{R}^K

In the context of RNN,

σ(o)i=eoij=1neoj\sigma(\boldsymbol{o})_i = -\frac{e^{o_i}}{\sum_{j = 1}^ne^{o_j}}

where

  • nn is the length of a sequence feed into the RNN
  • oio_i is the output by perceptron unit i
  • i=1,2,...,ni = 1, 2, ..., n,
  • o=(o1,o2,...,on)Rn\boldsymbol{o} = (o_1, o_2, ..., o_n) \in \mathbb{R}^n

The softmax function takes an N-dimensional vector of arbitrary real values and produces another N-dimensional vector with real values in the range (0, 1) that add up to 1.0. It maps RNRN\mathbb{R}^N \rightarrow \mathbb{R}^N

σ(o):(o1o2on)(σ1σ2σn)\sigma(\boldsymbol{o}): \begin{pmatrix}o_1\\o_2\\\dots\\o_n\end{pmatrix} \rightarrow \begin{pmatrix}\sigma_1\\\sigma_2\\\dots\\\sigma_n\end{pmatrix}

This property of softmax function that it outputs a probability distribution makes it suitable for probabilistic interpretation in classification tasks. Neural networks, however, are commonly trained under a log loss (or cross-entropy) regime

We are going to compute the derivative of the softmax function because we will be using it for training our RNN model shortly. But before diving in, it is important to keep in mind that Softmax is fundamentally a vector function. It takes a vector as input and produces a vector as output; in other words, it has multiple inputs and multiple outputs. Therefore, we cannot just ask for “the derivative of softmax”; We should instead specify:

  1. Which component (output element) of softmax we are seeking to find the derivative of.
  2. Since softmax has multiple inputs, with respect to which input element the partial derivative is computed.

What we are looking for is the partial derivatives of

σiok=okeoij=1neoj\frac{\partial \sigma_i}{\partial o_k} = \frac{\partial }{\partial o_k} \frac{e^{o_i}}{\sum_{j = 1}^ne^{o_j}}

where σiok\frac{\partial \sigma_i}{\partial o_k} is the partial derivative of the i-th output with respect with the k-th input.

We’ll be using the quotient rule of derivatives. For h(x)=f(x)g(x)h(x) = \frac{f(x)}{g(x)} where both ff and gg are differentiable and g(x)0g(x) \ne 0, The quotient rule states that the derivative of h(x)h(x) is

h(x)=f(x)g(x)f(x)g(x)g2(x)h'(x) = \frac{f'(x)g(x) - f(x)g'(x)}{g^2(x)}

In our case, we have

f(ok)=okeoi={eok,if i=k0,otherwisef'(o_k) = \frac{\partial}{\partial o_k} e^{o_i} = \begin{cases} e^{o_k}, & \text{if}\ i = k \\ 0, & \text{otherwise} \end{cases}g(ok)=okj=1neoj=(eo1ok+eo2ok++eokok++eonok)=eokok=eokg'(o_k) = \frac{\partial}{\partial o_k} \sum_{j = 1}^ne^{o_j} = \left( \frac{\partial e^{o_1}}{\partial o_k} + \frac{\partial e^{o_2}}{\partial o_k} + \dots + \frac{\partial e^{o_k}}{\partial o_k} + \dots + \frac{\partial e^{o_n}}{\partial o_k} \right) = \frac{\partial e^{o_k}}{\partial o_k} = e^{o_k}

The rest of it becomes trivial then. When i=ki = k,

σiok=eokj=1neojeokeoi(j=1neoj)2=eoij=1neojeoieoi(j=1neoj)2=eoij=1neojj=1neojeoij=1neoj=σi(j=1neojj=1neojeoij=1neoj)=σi(1σi)\frac{\partial \sigma_i}{\partial o_k} = \frac{e^{o_k} \sum_{j = 1}^ne^{o_j} - e^{o_k} e^{o_i}}{\left( \sum_{j = 1}^ne^{o_j} \right)^2} = \frac{e^{o_i} \sum_{j = 1}^ne^{o_j} - e^{o_i} e^{o_i}}{\left( \sum_{j = 1}^ne^{o_j} \right)^2} = \frac{e^{o_i}}{\sum_{j = 1}^ne^{o_j}} \frac{\sum_{j = 1}^ne^{o_j} - e^{o_i}}{\sum_{j = 1}^ne^{o_j}} \\ = \sigma_i\left( \frac{\sum_{j = 1}^ne^{o_j}}{\sum_{j = 1}^ne^{o_j}} - \frac{e^{o_i}}{\sum_{j = 1}^ne^{o_j}} \right) = \sigma_i \left( 1 - \sigma_i \right)

When iki \ne k:

σiok=eokeoi(j=1neoj)2=σiσk\frac{\partial \sigma_i}{\partial o_k} = \frac{-e^{o_k} e^{o_i}}{\left( \sum_{j = 1}^ne^{o_j} \right)^2} = -\sigma_i\sigma_k

This concludes the derivative of the softmax function:

σiok={σi(1σi),if i=kσiσk,otherwise\frac{\partial \sigma_i}{\partial o_k} = \begin{cases} \sigma_i \left( 1 - \sigma_i \right), & \text{if}\ i = k \\ -\sigma_i\sigma_k, & \text{otherwise} \end{cases}

Deriving Gradient Descent Weight Update Rule#

Training a RNN model of is the same thing as searching for the optimal values for the following parameters of the Forward Progagation Equations:

  1. WxhW_{xh}
  2. WhhW_{hh}
  3. WyhW_{yh}
  4. bhb_h
  5. bob_o

By the Gradient Descent discussed in Machine Learning by Mitchell, Tom M. (1997), Paperback, we should derive the weight update rule by taking partial derivatives with respect to all of the variables above. Let’s start with WyhW_{yh}

Machine Learning by Mitchell, Tom M. (1997), Paperback has also mentioned gradients and partial derivatives as being important for an optimization algorithm to update, say, the model weights of a neural network to reach an optimal set of weights. The use of partial derivatives permits each weight to be updated independently of the others, by calculating the gradient of the error curve with respect to each weight in turn.

Many of the functions that we usually work with in machine learning are multivariate, vector-valued functions, which means that they map multiple real inputs nn to multiple real outputs mm:

f:RnRmf: \mathbb{R}^n \rightarrow \mathbb{R}^m

In training a neural network, the backpropagation algorithm is responsible for sharing back the error calculated at the output layer among the neurons comprising the different hidden layers of the neural network, until it reaches the input.

If our RNN contains only 1 perceptron unit, the error is propagated back by, using the Chain Rule of dzdx=dzdydydx\frac{dz}{dx} = \frac{dz}{dy}\frac{dy}{dx}:

LW=LooW\frac{\partial \mathcal{L}}{\partial W} = \frac{\partial \mathcal{L}}{\partial o}\frac{\partial o}{\partial W}

Note that in the RNN mode, L\mathcal{L} is not a direct function of WW. Thus its first order derivative cannot be computed unless we connect the L\mathcal{L} to oo first and then to WW, because both the first order derivatives of Lo\frac{\partial \mathcal{L}}{\partial o} and oW\frac{\partial o}{\partial W} are defined by the model presented earlier above

It is more often the case that we’d have many connected perceptrons populating the network, each attributed a different weight. Since this is the case for RNN, we can generalise multiple inputs and multiple outputs using the Generalized Chain Rule:

Generalized Chain Rule

Consider the case where xRmx \in \mathbb{R}^m and uRnu \in \mathbb{R}^n; an inner function, ff, maps mm inputs to nn outputs, while an outer function, gg, receives nn inputs to produce an output, hRkh \in \mathbb{R}^k. For i=1,,mi = 1, \dots, m the generalized chain rule states:

hxi=hu1u1xi+hu2u2xi++hununxi=j=1nhujujxi\frac{\partial h}{\partial x_i} = \frac{\partial h}{\partial u_1} \frac{\partial u_1}{\partial x_i} + \frac{\partial h}{\partial u_2} \frac{\partial u_2}{\partial x_i} + \dots + \frac{\partial h}{\partial u_n} \frac{\partial u_n}{\partial x_i} = \sum_{j = 1}^n \frac{\partial h}{\partial u_j} \frac{\partial u_j}{\partial x_i}

Therefore, the error propagation of Gradient Descent in RNN is

LWyh=t=1τi=1nLoi(t)oi(t)WyhLWhh=t=1τi=1nLhi(t)hi(t)WhhLWxh=t=1τi=1nLhi(t)hi(t)Wxh\color{green} \boxed{ \begin{align} \frac{\partial \mathcal{L}}{\partial W_{yh}} = \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial o_i^{(t)}} \frac{\partial o_i^{(t)}}{\partial W_{yh}} \\ \frac{\partial \mathcal{L}}{\partial W_{hh}} = \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial h_i^{(t)}} \frac{\partial h_i^{(t)}}{\partial W_{hh}} \\ \frac{\partial \mathcal{L}}{\partial W_{xh}} = \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial h_i^{(t)}} \frac{\partial h_i^{(t)}}{\partial W_{xh}} \end{align} }

where nn is the length of a RNN sequence and tt the index of timestep

On t=1τ\sum_{t = 1}^\tau

We assume the error is the sum of all errors of each timestep, which is why we include the t=1τ\sum_{t = 1}^\tau term

Let’s look at LWyh\frac{\partial \mathcal{L}}{W_{yh}} first

LWyh=t=1τi=1nLoi(t)oi(t)Wyh\frac{\partial \mathcal{L}}{W_{yh}} = \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial o_i^{(t)}} \frac{\partial o_i^{(t)}}{\partial W_{yh}}

Since oi=(Wyhhi+bo)o_i = \left( W_{yh}h_i + b_o \right)

oiWyh=Wyh(Wyhhi+bo)=hi\frac{\partial o_i}{W_{yh}} = \frac{\partial }{W_{yh}}\left( W_{yh}h_i + b_o \right) = h_i

For the Loi\frac{\partial \mathcal{L}}{\partial o_i} we shall recall from the earlier discussion on softmax derivative that we CANNOT simply have

Loi=oiinp(i)logσi\frac{\partial \mathcal{L}}{\partial o_i} = -\frac{\partial}{\partial o_i}\sum_i^np(i)\log\sigma_i

because we need to

  1. specify which component (output element) we are seeking to find the derivative of
  2. with respect to which input element the partial derivative is computed

Therefore:

Loi=oijnp(j)logσj=jnoip(j)logσj=jnp(j)logσjoi\frac{\partial \mathcal{L}}{\partial o_i} = -\frac{\partial}{\partial o_i}\sum_j^np(j)\log\sigma_j = -\sum_j^n\frac{\partial}{\partial o_i}p(j)\log\sigma_j = -\sum_j^np(j)\frac{\partial \log\sigma_j}{\partial o_i}

where nn is the number of timesteps (or the length of a sequence such as “hell”)

Applying the chain rule again:

jnp(j)logσjoi=jnp(j)1σjσjoi-\sum_j^np(j)\frac{\partial \log\sigma_j}{\partial o_i} = -\sum_j^np(j)\frac{1}{\sigma_j}\frac{\partial\sigma_j}{\partial o_i}

Recall we have already derived that

σioj={σi(1σi),if i=jσiσj,otherwise\frac{\partial \sigma_i}{\partial o_j} = \begin{cases} \sigma_i \left( 1 - \sigma_i \right), & \text{if}\ i = j \\ -\sigma_i\sigma_j, & \text{otherwise} \end{cases}jnp(j)1σjσjoi=i=jnp(j)1σjσjoiijnp(j)1σjσjoi=p(i)(1σi)+ijnp(j)σi-\sum_j^np(j)\frac{1}{\sigma_j}\frac{\partial\sigma_j}{\partial o_i} = -\sum_{i = j}^np(j)\frac{1}{\sigma_j}\frac{\partial\sigma_j}{\partial o_i} -\sum_{i \ne j}^np(j)\frac{1}{\sigma_j}\frac{\partial\sigma_j}{\partial o_i} = -p(i)(1 - \sigma_i) + \sum_{i \ne j}^np(j)\sigma_i

Observing that

jnp(j)=1\sum_{j}^np(j) = 1p(i)(1σi)+ijnp(j)σi=p(i)+p(i)σi+ijnp(j)σi=σip(i)-p(i)(1 - \sigma_i) + \sum_{i \ne j}^np(j)\sigma_i = -p(i) + p(i)\sigma_i + \sum_{i \ne j}^np(j)\sigma_i = \sigma_i - p(i)Loi=σip(i)\color{green} \boxed{\frac{\partial \mathcal{L}}{\partial o_i} = \sigma_i - p(i)}LWyh=t=1τin[σip(i)]hi=t=1τ(σp)h(t)\color{green} \boxed{ \frac{\partial \mathcal{L}}{\partial W_{yh}} = \sum_{t = 1}^\tau \sum_i^n\left[ \sigma_i - p(i) \right] h_i = \sum_{t = 1}^\tau \left( \boldsymbol{\sigma} - \boldsymbol{p} \right) \boldsymbol{h}^{(t)} }Lbo=t=1τinLoi(t)oi(t)bo(t)=t=1τin[σip(i)]×1\frac{\partial \mathcal{L}}{\partial b_o} = \sum_{t = 1}^\tau \sum_i^n\frac{\partial \mathcal{L}}{\partial o_i^{(t)}}\frac{\partial o_i^{(t)}}{\partial b_o^{(t)}} = \sum_{t = 1}^\tau \sum_i^n\left[ \sigma_i - p(i) \right] \times 1Lbo=t=1τin[σip(i)]=t=1τσp\color{green} \boxed{ \frac{\partial \mathcal{L}}{\partial b_o} = \sum_{t = 1}^\tau \sum_i^n\left[ \sigma_i - p(i) \right] = \sum_{t = 1}^\tau \boldsymbol{\sigma} - \boldsymbol{p} }

We have at this point derived backpropagating rule for WyhW_{yh} and bob_o:

  1. WxhW_{xh}
  2. WhhW_{hh}
  3. WyhW_{yh}
  4. bhb_h
  5. bob_o

Now let’s look at LWhh\frac{\partial \mathcal{L}}{\partial W_{hh}}:

Recall from Deep Learning, section 6.5.2, p. 207 that the vector notation of zxi=jzyjyjxi\frac{\partial z}{\partial x_i} = \sum_j \frac{\partial z}{\partial y_j}\frac{\partial y_j}{\partial x_i} is

xz=(yx)yz\nabla_{\boldsymbol{x}}z = \left( \frac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}} \right)^\intercal \nabla_{\boldsymbol{y}}z

This gives us a start with:

LWhh=t=1τi=1nLhi(t)hi(t)Whh=t=1τ(Lh(t))Whhh(t)=t=1τ(Lh(t))(h(t)Whh)h(t)h(t)=t=1τ(Lh(t))(h(t)Whh)=t=1τ(h(t)Whh)Lh(t)=t=1τ(h(t)h(t1))(h(t1)Whh)Lh(t)=t=1τ(h(t)h(t1))(h(t1)h(t)h(t)h(t)h(t)Whh)Lh(t)=t=1τ(h(t)h(t1))(h(t1)h(t)h(t)Whhh(t)h(t))Lh(t)=t=1τ(h(t)h(t1))(h(t1)h(t))(h(t)Whh)(h(t)h(t))Lh(t)=t=1τ(h(t)Whh)(h(t)h(t))Lh(t)=t=1τdiag[1(h(t))2]h(t1)h(t)L=t=1τdiag[1(h(t))2](h(t)L)h(t1)\begin{align} \frac{\partial \mathcal{L}}{\partial W_{hh}} &= \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial h_i^{(t)}} \frac{\partial h_i^{(t)}}{\partial W_{hh}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \nabla_{\boldsymbol{W_{hh}}}\boldsymbol{h}^{(t)} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\boldsymbol{h}^{(t)} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t - 1)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t - 1)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t - 1)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t - 1)}}{\partial \boldsymbol{h}^{(t)}}\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t)}}\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t - 1)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t - 1)}}{\partial \boldsymbol{h}^{(t)}}\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}}\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t - 1)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t - 1)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{hh}}} \right)^\intercal \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \frac{\partial \mathcal{L}}{\partial \boldsymbol{h}^{(t)}} \\ & = \sum_{t = 1}^\tau diag\left[ 1 - \left(\boldsymbol{h}^{(t)}\right)^2 \right] \boldsymbol{h}^{(t - 1)} \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ & = \sum_{t = 1}^\tau diag\left[ 1 - \left(\boldsymbol{h}^{(t)}\right)^2 \right] \left( \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \right) {\boldsymbol{h}^{(t - 1)}}^\intercal \end{align}LWhh=t=1τdiag[1(h(t))2](h(t)L)h(t1)\color{green} \boxed{ \frac{\partial \mathcal{L}}{\partial W_{hh}} = \sum_{t = 1}^\tau diag\left[ 1 - \left(\boldsymbol{h}^{(t)}\right)^2 \right] \left( \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \right) {\boldsymbol{h}^{(t - 1)}}^\intercal }

The equation above leaves us with a term h(t)L\nabla_{\boldsymbol{h}^{(t)}}\mathcal{L}, which we calculate next. Note that the back propagation on h(t)\boldsymbol{h}^{(t)} has source from both o(t)\boldsymbol{o}^{(t)} and h(t+1)\boldsymbol{h}^{(t + 1)}. It’s gradient, therefore, is given by

h(t)L=(o(t)h(t))o(t)L+(h(t+1)h(t))h(t+1)L=(Wyh)o(t)L+(diag[1(h(t+1))2]Whh)h(t+1)L=(Wyh)o(t)L+Whhh(t+1)L(diag[1(h(t+1))2])\begin{align} \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} &= \left( \frac{\partial \boldsymbol{o}^{(t)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \nabla_{\boldsymbol{o}^{(t)}}\mathcal{L} + \left( \frac{\partial \boldsymbol{h}^{(t + 1)}}{\partial \boldsymbol{h}^{(t)}} \right)^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \\ &= \left( \boldsymbol{W_{yh}} \right)^\intercal \nabla_{\boldsymbol{o}^{(t)}}\mathcal{L} + \left( diag\left[ 1 - (\boldsymbol{h}^{(t + 1)})^2 \right] \boldsymbol{W_{hh}} \right)^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \\ &= \left( \boldsymbol{W_{yh}} \right)^\intercal \nabla_{\boldsymbol{o}^{(t)}}\mathcal{L}+ \boldsymbol{W_{hh}}^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \left( diag\left[ 1 - (\boldsymbol{h}^{(t + 1)})^2 \right] \right) \end{align}h(t)L=(Wyh)o(t)L+Whhh(t+1)L(diag[1(h(t+1))2])\color{green} \boxed{ \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} = \left( \boldsymbol{W_{yh}} \right)^\intercal \nabla_{\boldsymbol{o}^{(t)}}\mathcal{L} + \boldsymbol{W_{hh}}^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \left( diag\left[ 1 - (\boldsymbol{h}^{(t + 1)})^2 \right] \right) }

Note that the 2nd term Wxhh(t+1)L(diag[1(h(t+1))2])\boldsymbol{W_{xh}}^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \left( diag\left[ 1 - (\boldsymbol{h}^{(t + 1)})^2 \right] \right) is zero at first iteration propagating back because for the last-layer (unrolled) of RNN, there’s no gradient update flow from the next hidden state.

So far we have derived backpropagating rule for WhhW_{hh}

  1. WxhW_{xh}
  2. WhhW_{hh}
  3. WyhW_{yh}
  4. bhb_h
  5. bob_o

Let’s tackle the remaining LWxh\frac{\partial \mathcal{L}}{\partial W_{xh}} and bhb_h:

LWxh=t=1τi=1nLhi(t)hi(t)Wxh=t=1τ(h(t)Wxh)h(t)L=t=1τ(diag[1(h(t))2]x(t))h(t)L=t=1τ(diag[1(h(t))2])h(t)L(x(t))\begin{align} \frac{\partial \mathcal{L}}{\partial W_{xh}} &= \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial h_i^{(t)}} \frac{\partial h_i^{(t)}}{\partial W_{xh}} \\ &= \sum_{t = 1}^\tau \left( \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{W_{xh}}} \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ &= \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \boldsymbol{x}^{(t)} \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ &= \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \left( \boldsymbol{x}^{(t)} \right) \end{align}LWxh=t=1τ(diag[1(h(t))2])h(t)L(x(t))\color{green} \boxed{ \frac{\partial \mathcal{L}}{\partial W_{xh}} = \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \left( \boldsymbol{x}^{(t)} \right) }Lbh=t=1τi=1nLhi(t)hi(t)bh(t)=t=1τ(hi(t)bh(t))h(t)L=t=1τ(diag[1(h(t))2])h(t)L\begin{align} \frac{\partial \mathcal{L}}{\partial b_h} &= \sum_{t = 1}^\tau \sum_{i = 1}^n \frac{\partial \mathcal{L}}{\partial h_i^{(t)}} \frac{\partial h_i^{(t)}}{\partial b_h^{(t)}} \\ &= \sum_{t = 1}^\tau \left( \frac{\partial h_i^{(t)}}{\partial b_h^{(t)}} \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ &= \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \end{align}Lbh=t=1τ(diag[1(h(t))2])h(t)L\color{green} \boxed{ \frac{\partial \mathcal{L}}{\partial b_h} = \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} }

This concludes our propagation rules for training RNN:

LWxh=t=1τ(diag[1(h(t))2])h(t)L(x(t))LWhh=t=1τdiag[1(h(t))2](h(t)L)h(t1)LWyh=t=1τ(σp)h(t)Lbh=t=1τ(diag[1(h(t))2])h(t)LLbo=t=1τσp\color{green} \boxed{ \begin{align*} & \frac{\partial \mathcal{L}}{\partial W_{xh}} = \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \left( \boldsymbol{x}^{(t)} \right) \\ \\ & \frac{\partial \mathcal{L}}{\partial W_{hh}} = \sum_{t = 1}^\tau diag\left[ 1 - \left(\boldsymbol{h}^{(t)}\right)^2 \right] \left( \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \right) {\boldsymbol{h}^{(t - 1)}}^\intercal \\ \\ & \frac{\partial \mathcal{L}}{\partial W_{yh}} = \sum_{t = 1}^\tau \left( \boldsymbol{\sigma} - \boldsymbol{p} \right) \boldsymbol{h}^{(t)} \\ \\ & \frac{\partial \mathcal{L}}{\partial b_h} = \sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ \\ & \frac{\partial \mathcal{L}}{\partial b_o} = \sum_{t = 1}^\tau \boldsymbol{\sigma} - \boldsymbol{p} \end{align*} }

According to page 91 of Machine Learning by Mitchell, Tom M. (1997), Paperback, the amount of updates in direction ii is given by

Δwi=ηEwi \Delta{w_i} = -\eta\frac{\partial E}{\partial w_i}

The update rules for training RNN with a learning rate of η\eta, therefore, are:

ΔWxh=ηt=1τ(diag[1(h(t))2])h(t)L(x(t))ΔWhh=ηt=1τdiag[1(h(t))2](h(t)L)h(t1)ΔWyh=ηt=1τ(σp)h(t)Δbh=ηt=1τ(diag[1(h(t))2])h(t)LΔbo=ηt=1τσp\color{green} \boxed{ \begin{align*} & \Delta W_{xh} = -\eta\sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \left( \boldsymbol{x}^{(t)} \right) \\ \\ & \Delta W_{hh} = -\eta\sum_{t = 1}^\tau diag\left[ 1 - \left(\boldsymbol{h}^{(t)}\right)^2 \right] \left( \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \right) {\boldsymbol{h}^{(t - 1)}}^\intercal \\ \\ & \Delta W_{yh} = -\eta\sum_{t = 1}^\tau \left( \boldsymbol{\sigma} - \boldsymbol{p} \right) \boldsymbol{h}^{(t)} \\ \\ & \Delta b_h = -\eta\sum_{t = 1}^\tau \left( diag\left[ 1 - (\boldsymbol{h}^{(t)})^2 \right] \right)^\intercal \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} \\ \\ & \Delta b_o = -\eta\sum_{t = 1}^\tau \boldsymbol{\sigma} - \boldsymbol{p} \end{align*} }

where

h(t)L=(Wyh)o(t)L+Whhh(t+1)L(diag[1(h(t+1))2])\color{green} \boxed{ \nabla_{\boldsymbol{h}^{(t)}}\mathcal{L} = \left( \boldsymbol{W_{yh}} \right)^\intercal \nabla_{\boldsymbol{o}^{(t)}}\mathcal{L} + \boldsymbol{W_{hh}}^\intercal \nabla_{\boldsymbol{h}^{(t + 1)}}\mathcal{L} \left( diag\left[ 1 - (\boldsymbol{h}^{(t + 1)})^2 \right] \right) }

Phase 3: Implementation “From Scratch”#

Just because we understand the math behind it does not mean we can implement it in computer codes. To truly understand the RNN, one should build one without a framework (or with minimal abstractions).

This is a series of Jupyter notebooks that help us build the muscle for making that happen:

Recurrent Neural Networks (RNNs)
https://blogs.openml.io/posts/rnn/
Author
OpenML Blogs
Published at
2025-09-19
License
CC BY-NC-SA 4.0