Skip to content

Backpropagation Through Time

If you completed the exercises in :numref:sec_rnn-scratch, you would have seen that gradient clipping is vital for preventing the occasional massive gradients from destabilizing training. We hinted that the exploding gradients stem from backpropagating across long sequences. Before introducing a slew of modern RNN architectures, let's take a closer look at how backpropagation works in sequence models in mathematical detail. Hopefully, this discussion will bring some precision to the notion of vanishing and exploding gradients. If you recall our discussion of forward and backward propagation through computational graphs when we introduced MLPs in :numref:sec_backprop, then forward propagation in RNNs should be relatively straightforward. Applying backpropagation in RNNs is called backpropagation through time [160]. This procedure requires us to expand (or unroll) the computational graph of an RNN one time step at a time. The unrolled RNN is essentially a feedforward neural network with the special property that the same parameters are repeated throughout the unrolled network, appearing at each time step. Then, just as in any feedforward neural network, we can apply the chain rule, backpropagating gradients through the unrolled net. The gradient with respect to each parameter must be summed across all places that the parameter occurs in the unrolled net. Handling such weight tying should be familiar from our chapters on convolutional neural networks.

Complications arise because sequences can be rather long. It is not unusual to work with text sequences consisting of over a thousand tokens. Note that this poses problems both from a computational (too much memory) and optimization (numerical instability) standpoint. Input from the first step passes through over 1000 matrix products before arriving at the output, and another 1000 matrix products are required to compute the gradient. We now analyze what can go wrong and how to address it in practice.

Analysis of Gradients in RNNs

We start with a simplified model of how an RNN works. This model ignores details about the specifics of the hidden state and how it is updated. The mathematical notation here does not explicitly distinguish scalars, vectors, and matrices. We are just trying to develop some intuition. In this simplified model, we denote ht as the hidden state, xt as input, and ot as output at time step t. Recall our discussions in :numref:subsec_rnn_w_hidden_states that the input and the hidden state can be concatenated before being multiplied by one weight variable in the hidden layer. Thus, we use wh and wo to indicate the weights of the hidden layer and the output layer, respectively. As a result, the hidden states and outputs at each time step are

ht=f(xt,ht1,wh),ot=g(ht,wo),

:eqlabel:eq_bptt_ht_ot

where f and g are transformations of the hidden layer and the output layer, respectively. Hence, we have a chain of values {,(xt1,ht1,ot1),(xt,ht,ot),} that depend on each other via recurrent computation. The forward propagation is fairly straightforward. All we need is to loop through the (xt,ht,ot) triples one time step at a time. The discrepancy between output ot and the desired target yt is then evaluated by an objective function across all the T time steps as

L(x1,,xT,y1,,yT,wh,wo)=1Tt=1Tl(yt,ot).

For backpropagation, matters are a bit trickier, especially when we compute the gradients with regard to the parameters wh of the objective function L. To be specific, by the chain rule,

Lwh=1Tt=1Tl(yt,ot)wh=1Tt=1Tl(yt,ot)otg(ht,wo)hthtwh.

:eqlabel:eq_bptt_partial_L_wh

The first and the second factors of the product in :eqref:eq_bptt_partial_L_wh are easy to compute. The third factor ht/wh is where things get tricky, since we need to recurrently compute the effect of the parameter wh on ht. According to the recurrent computation in :eqref:eq_bptt_ht_ot, ht depends on both ht1 and wh, where computation of ht1 also depends on wh. Thus, evaluating the total derivate of ht with respect to wh using the chain rule yields

htwh=f(xt,ht1,wh)wh+f(xt,ht1,wh)ht1ht1wh.

:eqlabel:eq_bptt_partial_ht_wh_recur

To derive the above gradient, assume that we have three sequences {at},{bt},{ct} satisfying a0=0 and at=bt+ctat1 for t=1,2,. Then for t1, it is easy to show

at=bt+i=1t1(j=i+1tcj)bi.

:eqlabel:eq_bptt_at

By substituting at, bt, and ct according to

at=htwh,bt=f(xt,ht1,wh)wh,ct=f(xt,ht1,wh)ht1,

the gradient computation in :eqref:eq_bptt_partial_ht_wh_recur satisfies at=bt+ctat1. Thus, per :eqref:eq_bptt_at, we can remove the recurrent computation in :eqref:eq_bptt_partial_ht_wh_recur with

htwh=f(xt,ht1,wh)wh+i=1t1(j=i+1tf(xj,hj1,wh)hj1)f(xi,hi1,wh)wh.

:eqlabel:eq_bptt_partial_ht_wh_gen

While we can use the chain rule to compute ht/wh recursively, this chain can get very long whenever t is large. Let's discuss a number of strategies for dealing with this problem.

Full Computation

One idea might be to compute the full sum in :eqref:eq_bptt_partial_ht_wh_gen. However, this is very slow and gradients can blow up, since subtle changes in the initial conditions can potentially affect the outcome a lot. That is, we could see things similar to the butterfly effect, where minimal changes in the initial conditions lead to disproportionate changes in the outcome. This is generally undesirable. After all, we are looking for robust estimators that generalize well. Hence this strategy is almost never used in practice.

Truncating Time Steps###

Alternatively, we can truncate the sum in :eqref:eq_bptt_partial_ht_wh_gen after τ steps. This is what we have been discussing so far. This leads to an approximation of the true gradient, simply by terminating the sum at htτ/wh. In practice this works quite well. It is what is commonly referred to as truncated backpropgation through time [161]. One of the consequences of this is that the model focuses primarily on short-term influence rather than long-term consequences. This is actually desirable, since it biases the estimate towards simpler and more stable models.

Randomized Truncation

Last, we can replace ht/wh by a random variable which is correct in expectation but truncates the sequence. This is achieved by using a sequence of ξt with predefined 0πt1, where P(ξt=0)=1πt and P(ξt=πt1)=πt, thus E[ξt]=1. We use this to replace the gradient ht/wh in :eqref:eq_bptt_partial_ht_wh_recur with

zt=f(xt,ht1,wh)wh+ξtf(xt,ht1,wh)ht1ht1wh.

It follows from the definition of ξt that E[zt]=ht/wh. Whenever ξt=0 the recurrent computation terminates at that time step t. This leads to a weighted sum of sequences of varying lengths, where long sequences are rare but appropriately overweighted. This idea was proposed by Tallec and Ollivier [162].

Comparing Strategies

Comparing strategies for computing gradients in RNNs. From top to bottom: randomized truncation, regular truncation, and full computation.

Figure illustrates the three strategies when analyzing the first few characters of The Time Machine using backpropagation through time for RNNs:

  • The first row is the randomized truncation that partitions the text into segments of varying lengths.

  • The second row is the regular truncation that breaks the text into subsequences of the same length. This is what we have been doing in RNN experiments.

  • The third row is the full backpropagation through time that leads to a computationally infeasible expression.

Unfortunately, while appealing in theory, randomized truncation does not work much better than regular truncation, most likely due to a number of factors. First, the effect of an observation after a number of backpropagation steps into the past is quite sufficient to capture dependencies in practice. Second, the increased variance counteracts the fact that the gradient is more accurate with more steps. Third, we actually want models that have only a short range of interactions. Hence, regularly truncated backpropagation through time has a slight regularizing effect that can be desirable.

Backpropagation Through Time in Detail

After discussing the general principle, let's discuss backpropagation through time in detail. In contrast to the analysis in :numref:subsec_bptt_analysis, in the following we will show how to compute the gradients of the objective function with respect to all the decomposed model parameters. To keep things simple, we consider an RNN without bias parameters, whose activation function in the hidden layer uses the identity mapping (ϕ(x)=x). For time step t, let the single example input and the target be xtRd and yt, respectively. The hidden state htRh and the output otRq are computed as

ht=Whxxt+Whhht1,ot=Wqhht,

where WhxRh×d, WhhRh×h, and WqhRq×h are the weight parameters. Denote by l(ot,yt) the loss at time step t. Our objective function, the loss over T time steps from the beginning of the sequence is thus

L=1Tt=1Tl(ot,yt).

In order to visualize the dependencies among model variables and parameters during computation of the RNN, we can draw a computational graph for the model, as shown in Figure. For example, the computation of the hidden states of time step 3, h3, depends on the model parameters Whx and Whh, the hidden state of the previous time step h2, and the input of the current time step x3.

Computational graph showing dependencies for an RNN model with three time steps. Boxes represent variables (not shaded) or parameters (shaded) and circles represent operators.

As just mentioned, the model parameters in Figure are Whx, Whh, and Wqh. Generally, training this model requires gradient computation with respect to these parameters L/Whx, L/Whh, and L/Wqh. According to the dependencies in Figure, we can traverse in the opposite direction of the arrows to calculate and store the gradients in turn. To flexibly express the multiplication of matrices, vectors, and scalars of different shapes in the chain rule, we continue to use the prod operator as described in :numref:sec_backprop.

First of all, differentiating the objective function with respect to the model output at any time step t is fairly straightforward:

Lot=l(ot,yt)TotRq.

:eqlabel:eq_bptt_partial_L_ot

Now we can calculate the gradient of the objective with respect to the parameter Wqh in the output layer: L/WqhRq×h. Based on Figure, the objective L depends on Wqh via o1,,oT. Using the chain rule yields

LWqh=t=1Tprod(Lot,otWqh)=t=1TLotht,

where L/ot is given by :eqref:eq_bptt_partial_L_ot.

Next, as shown in Figure, at the final time step T, the objective function L depends on the hidden state hT only via oT. Therefore, we can easily find the gradient L/hTRh using the chain rule:

LhT=prod(LoT,oThT)=WqhLoT.

:eqlabel:eq_bptt_partial_L_hT_final_step

It gets trickier for any time step t<T, where the objective function L depends on ht via ht+1 and ot. According to the chain rule, the gradient of the hidden state L/htRh at any time step t<T can be recurrently computed as:

Lht=prod(Lht+1,ht+1ht)+prod(Lot,otht)=WhhLht+1+WqhLot.

:eqlabel:eq_bptt_partial_L_ht_recur

For analysis, expanding the recurrent computation for any time step 1tT gives

Lht=i=tT(Whh)TiWqhLoT+ti.

:eqlabel:eq_bptt_partial_L_ht

We can see from :eqref:eq_bptt_partial_L_ht that this simple linear example already exhibits some key problems of long sequence models: it involves potentially very large powers of Whh. In it, eigenvalues smaller than 1 vanish and eigenvalues larger than 1 diverge. This is numerically unstable, which manifests itself in the form of vanishing and exploding gradients. One way to address this is to truncate the time steps at a computationally convenient size as discussed in :numref:subsec_bptt_analysis. In practice, this truncation can also be effected by detaching the gradient after a given number of time steps. Later on, we will see how more sophisticated sequence models such as long short-term memory can alleviate this further.

Finally, Figure shows that the objective function L depends on model parameters Whx and Whh in the hidden layer via hidden states h1,,hT. To compute gradients with respect to such parameters L/WhxRh×d and L/WhhRh×h, we apply the chain rule giving

$

\begin{aligned} \frac{\partial L}{\partial \mathbf{W}\textrm{hx}} &= \sum^T \textrm{prod}\left(\frac{\partial L}{\partial \mathbf{h}t}, \frac{\partial \mathbf{h}t}{\partial \mathbf{W}\textrm{hx}}\right) = \sum^T \frac{\partial L}{\partial \mathbf{h}t} \mathbf{x}t^\top,
\frac{\partial L}{\partial \mathbf{W}
\textrm{hh}} &= \sum
^T \textrm{prod}\left(\frac{\partial L}{\partial \mathbf{h}t}, \frac{\partial \mathbf{h}t}{\partial \mathbf{W}\textrm{hh}}\right) = \sum^T \frac{\partial L}{\partial \mathbf{h}t} \mathbf{h}^\top, \end{aligned} $

where L/ht which is recurrently computed by :eqref:eq_bptt_partial_L_hT_final_step and :eqref:eq_bptt_partial_L_ht_recur is the key quantity that affects the numerical stability.

Since backpropagation through time is the application of backpropagation in RNNs, as we have explained in :numref:sec_backprop, training RNNs alternates forward propagation with backpropagation through time. Moreover, backpropagation through time computes and stores the above gradients in turn. Specifically, stored intermediate values are reused to avoid duplicate calculations, such as storing L/ht to be used in computation of both L/Whx and L/Whh.

Summary

Backpropagation through time is merely an application of backpropagation to sequence models with a hidden state. Truncation, such as regular or randomized, is needed for computational convenience and numerical stability. High powers of matrices can lead to divergent or vanishing eigenvalues. This manifests itself in the form of exploding or vanishing gradients. For efficient computation, intermediate values are cached during backpropagation through time.

Exercises

  1. Assume that we have a symmetric matrix MRn×n with eigenvalues λi whose corresponding eigenvectors are vi (i=1,,n). Without loss of generality, assume that they are ordered in the order |λi||λi+1|.

  2. Show that Mk has eigenvalues λik.

  3. Prove that for a random vector xRn, with high probability Mkx will be very much aligned with the eigenvector v1

of M. Formalize this statement.

  1. What does the above result mean for gradients in RNNs?

  2. Besides gradient clipping, can you think of any other methods to cope with gradient explosion in recurrent neural networks?

julia