GRU Cell - Sequence Models
Implement GRU Cell in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
GRU Cell
What you'll build
One time step of a Gated Recurrent Unit, matching PyTorch's nn.GRUCell exactly (weight layout, gate order and reset-gate placement).
Background
The GRU (Cho et al., 2014) merges the LSTM's cell and hidden state into one vector and uses two gates instead of three. It has 3 weight blocks instead of 4 (25% fewer parameters) and usually trains about as well. Its update rule is a learned interpolation between the old state and a candidate.
Formula
gi = x @ W_ih.T + b_ih # (N, 3H), split into i_r, i_z, i_n
gh = h_prev @ W_hh.T + b_hh # (N, 3H), split into h_r, h_z, h_n
r = sigmoid(i_r + h_r) # reset gate
z = sigmoid(i_z + h_z) # update gate
n = tanh(i_n + r * h_n) # candidate state
h = (1 - z) * n + z * h_prev
Conventions to get right
- Gate order
r, z, nalong the first axis of the weights (rows0:H,H:2H,2H:3H), as innn.GRUCell.weight_ih/weight_hh. Weights are(out, in), hence.T. - The update gate
zkeeps the old state:z = 1copiesh_prev,z = 0takes the candidaten. Some papers swap the roles ofzand1 - z; PyTorch uses the form above. - Reset gate placement:
rmultipliesh_n = h_prev @ W_hn.T + b_hn, including the biasb_hn, after the matrix product. The original paper computes(r * h_prev) @ W_hn.Tinstead, which gives different numbers. Follow PyTorch. This is why the input and hidden projections must stay separate (you can't addgi + ghbefore splitting).
Inputs
| Tensor | Shape |
|---|---|
x | (N, D) |
h_prev | (N, H) |
W_ih | (3H, D) |
W_hh | (3H, H) |
b_ih, b_hh | (3H,) |
Expected return format
The new hidden state h: a float array of shape (N, H).
Edge cases and conventions
- A
sigmoidhelper is provided. If|h_prev| < 1, the newhstays in(-1, 1)(a convex mix of values in that range).
Constraints
NumPy only; vectorised over the batch (two matmuls total).
Follow-up
- Count the parameters of
nn.GRUCell(D, H)(3(HD + H^2 + 2H)) and compare with an LSTM. - When would you pick a GRU over an LSTM, and vice versa?
Examples
Input: x = [[0.3, -0.1]], h_prev = [[1.0, -2.0]], all weights and biases zero Output: array([[0.5, -1.0]])
r = z = 0.5 and n = tanh(0) = 0, so h = 0.5 * 0 + 0.5 * h_prev.
Input: D = H = 1, x = [[1.0]], h_prev = [[0.5]], W_ih = [[0], [1], [1]], W_hh = [[1], [0], [2]], biases 0 Output: array([[0.614295]])
r = sigmoid(0 + 0.5) = 0.6225, z = sigmoid(1 + 0) = 0.7311, n = tanh(1 + 0.6225 * 1.0) = 0.9250, h = 0.2689 * 0.9250 + 0.7311 * 0.5.
Practice in Python
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def gru_cell(x, h_prev, W_ih, W_hh, b_ih, b_hh):
"""
One GRU step (gate order r, z, n, as in nn.GRUCell).
Args:
x: Input (N, D)
h_prev: Previous hidden state (N, H)
W_ih: Input weights (3H, D), row blocks [r; z; n]
W_hh: Hidden weights (3H, H), row blocks [r; z; n]
b_ih, b_hh: Biases (3H,)
Returns:
h: New hidden state (N, H)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 5 tests, or use the progressive hints.