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
| Aspect | GRU | LSTM |
|---|---|---|
| Gates | 2 (update, reset) | 3 (forget, input, output) |
| Extra state | none | separate cell state |
| Parameters | fewer | more |
| Output control | exposes 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_candReset 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.