The Gated Recurrent Unit (GRU) is a recurrent cell that uses learned gates to control how much of the past hidden state is kept and how much new information is written. It is a lighter alternative to the LSTM that fights vanishing gradients without a separate cell state.

Motivation

Vanilla Recurrent Neural Networks update the hidden state with . Repeatedly multiplying by makes gradients shrink or blow up over long sequences, so the network cannot learn long range dependencies. Gating Mechanisms fix this by letting the cell copy its state forward almost unchanged when the gates say so, creating a near identity path for gradients.

The Two Gates

A GRU has an update gate and a reset gate , both computed with sigmoids so their entries live in :

  • The reset gate decides how much of the previous state feeds into the candidate . When the cell forgets the past and behaves like it is reading a fresh sequence.
  • The update gate is a learned interpolation between the old state and the candidate. When the state is copied verbatim, which is exactly the identity path that preserves gradients.

Interpolation view

The final line is a convex combination: . This single blend replaces the LSTM’s separate forget and input gates.

GRU vs LSTM

AspectGRULSTM
Gates2 (update, reset)3 (forget, input, output)
Extra statenoneseparate cell state
Parametersfewermore
Output controlexposes full gated via output gate

GRUs train faster and use less memory. LSTMs sometimes edge them out on tasks that need very long memory because the protected cell state is never squashed by a on the recurrent path.

PyTorch Implementation

We batch the three input projections and the two recurrent projections into single matrices, then split with einops.rearrange.

import torch
import torch.nn as nn
from einops import rearrange
 
class GRUCell(nn.Module):
    def __init__(self, d_in, d_hidden):
        super().__init__()
        # stack z, r, h projections -> 3 * d_hidden
        self.W = nn.Linear(d_in, 3 * d_hidden)
        self.U = nn.Linear(d_hidden, 3 * d_hidden, bias=False)
        self.d_hidden = d_hidden
 
    def forward(self, x, h_prev):
        # x: (b, d_in), h_prev: (b, d_hidden)
        xz, xr, xh = rearrange(self.W(x), "b (g d) -> g b d", g=3)
        uz, ur, uh = rearrange(self.U(h_prev), "b (g d) -> g b d", g=3)
 
        z = torch.sigmoid(xz + uz)
        r = torch.sigmoid(xr + ur)
        h_cand = torch.tanh(xh + r * uh)
        return (1 - z) * h_prev + z * h_cand

Reset gate placement

The reset gate multiplies the recurrent term , not the raw input. A common bug is applying to before both the and , projections, which corrupts the gate computations. Only the candidate’s recurrent contribution is reset.

Practical Notes

  • Initialize the update gate bias slightly negative so the cell starts by remembering (small ), which stabilizes early training.
  • GRUs are strong baselines for small to medium sequence tasks; Transformers dominate once data and compute are large.