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.
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.
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:
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.
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.
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:
1
# A hypothetical Cell State vector (C_t) with 4 hidden units
2
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 ht−1 and xt and outputs a number between 0 and 1 for each number in the cell state Ct−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 ft. 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:
A sigmoid layer called the input gate layer decides which values to update and a tanh layer creates a vector of
new values C
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.
Now we have the intuition, we shall proceed to rigorously study RNN. Here are some good resources:
Deep Learning (Goodfellow, Bengio, Courville) – Chapter 10:
Sequence Modeling. This is the academic standard. Chapter 10 covers the mathematical formalization of unfolding
computational graphs across time. It explains the “Teacher Forcing” concept and the precise difficulty of training
RNNs due to gradient instability.
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′)=′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 f depend on 1 parameter, we now have it take 2 parameters.
a character, and
a variable that summarizes the previous calculations:
Now it makes much more sense with:
f(‘l’,h2)=‘l’f(‘l’,h3)=‘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 f 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 f is a network unit containing 2 perceptrons with one perceptron computing the “history” like h1,
h2, and h3.
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:
1
rnn =RNN()
2
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:
The code snippet above specifies the forward pass of a vanilla RNN. This RNN’s parameters are the 3 matrices:
W_hh,
W_xh, and
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 Whh, Wxh, and Why. The
hidden vector, or more generally the hidden state, is defined by
h(t)=g1(Whhh(t−1)+Wxhx(t)+bh)
where t is the index of the “black boxes” shown earlier. In our example of “hell”, t∈{1,2,3,4}. The
hidden state h is usually initialized with zero vector (simulating “no memory at all”). There are 2 terms inside the
g1:
one term based on the previous hidden state Whhh(t−1), and
the other term based on the current input Wxhx(t)
In the program above we use numpy np.dot which is a matrix multiplication. The 2 terms interact with addition.
We initialize matrices Whh, Wxh, and Why 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 y is given by
o(t)=g2(Wyhh(t)+bo)
What are g1 and g2?
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 g1 is tanh:
tanh(x)=ex+e−xex−e−x
which squashes the activations to the range [0,1]
In practice, g2 is constance, i.e. g2=1
We get RNNs as neural networks if we stack up as follows:
1
y1 = rnn1.step(x)
2
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.
We now develop the forward propagation equations for the RNN. We assume the hyperbolic tangent activation function,
i.e. tanh(x)=ex+e−xex−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 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) of normalized
probabilities over the output.
Forward propagation begins with a specification of the initial state h(0). The dimension of the hidden
state h is independent of the dimension of the input or output sequences. In fact, h is a
3D array, whose 1st-dimensional size is exactly the number of RNN parameters.
Then, for each time step from t=1 to t=τ, we apply the following update equations:
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 paired with a sequence of expected
y is the sum of the losses over all the time steps, i.e.
L({x(1),...,x(τ)},{y(1),...,y(τ)})=t∑τL(t)
Knowing the exact form of L(t) requires our intuitive understanding of cross-entropy
In information theory, the cross-entropy between two probability
distributions p and q 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
q, rather than the true distribution p
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 p. The task of RNN (or Machine Learning in general) is to
learn an approximated distribution of p; we call this approximation q
“The average number of bits needed” is can be seen as the distance between p and q 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 q relative to a distribution p over a given set is defined as
H(p,q)=−x∑p(x)logq(x)
Since we assume the softmax probability distribution earlier, the probability distribution of q(x) is:
What is the Mathematical form of p(i) in RNN? Why would it become 1?
By definition, p(i) is the true distribution whose exact functional form is unknown. In the language of
Approximation Theory, p(i) is the function that RNN is trying to learn or approximate mathematically.
Although the p(i) makes the exact form of L unknown, computationally p(i) is perfectly defined in each
training example. Taking our “hello” example:
The 4 probability distributions of q(x) is “reflected” in the output layer of this example. They are “reflecting” the
probability distribution of q(x) because they are only o values and have not been transformed to the σ
distribution yet. But in this case, we are 100% sure that the true probability distribution p(i) for the 4 outputs are
0100,0010,0010,0001
respectively. That is all we need for calculating the L
The softmax function takes as input a vector z of K real numbers,
and normalizes it into a probability distribution consisting of K 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) 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 z of K real numbers, the the standard (unit) softmax function σ:RK↦(0,1)K,
where K≥1 is defined by
σ(z)i=∑j=1Kezjezi
where i=1,2,...,K and x=(x1,x2,...,xK)∈RK
In the context of RNN,
σ(o)i=−∑j=1neojeoi
where
n is the length of a sequence feed into the RNN
oi is the output by perceptron unit i
i=1,2,...,n,
o=(o1,o2,...,on)∈Rn
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 RN→RN
σ(o):o1o2…on→σ1σ2…σn
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:
Which component (output element) of softmax we are seeking to find the derivative of.
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
∂ok∂σi=∂ok∂∑j=1neojeoi
where ∂ok∂σi 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)=g(x)f(x) where both f and g are
differentiable and g(x)=0, The quotient rule states that the
derivative of h(x) is
Training a RNN model of is the same thing as searching for the optimal values for the following parameters of the
Forward Progagation Equations:
Wxh
Whh
Wyh
bh
bo
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 Wyh
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 n to multiple real outputs m:
f:Rn→Rm
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 dxdz=dydzdxdy:
∂W∂L=∂o∂L∂W∂o
Note that in the RNN mode, L is not a direct function of W. Thus its first order derivative cannot be
computed unless we connect the L to o first and then to W, because both the first order derivatives of
∂o∂L and ∂W∂o 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 x∈Rm and u∈Rn; an inner function, f, maps m inputs to n
outputs, while an outer function, g, receives n inputs to produce an output, h∈Rk. For
i=1,…,m the generalized chain rule states:
The equation above leaves us with a term ∇h(t)L, which we calculate next. Note that
the back propagation on h(t) has source from both o(t) and
h(t+1). It’s gradient, therefore, is given by
Note that the 2nd term
Wxh⊺∇h(t+1)L(diag[1−(h(t+1))2])
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 Whh
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: