EasyGRU

Complete GRU Cell

Learning Phrase Representations using RNN Encoder-Decoder (GRU)

Easy

Problem

Implement one complete GRU cell. Compute the reset gate, update gate, candidate state, and new hidden state in this order:

r_t=\sigma\!\left(W_r[h_{t-1},x_t]+b_r\right).

z_t=\sigma\!\left(W_z[h_{t-1},x_t]+b_z\right).

\widetilde{h}_t=\tanh\!\left(W_h[r_t\odot h_{t-1},x_t]+b_h\right).

h_t=z_t\odot h_{t-1}+(1-z_t)\odot\widetilde{h}_t.

Here, H is the hidden width, D is the input width, square brackets mean concatenation in the displayed order, and \sigma is sigmoid. Accept one sample or a batch and return h_t as a matching float64 NumPy array.

Theory

The Gated Recurrent Unit (GRU) is a recurrent neural network introduced by Cho et al. (2014) in "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation." It combines a reset gate, an update gate, and a candidate hidden state into a single cell that produces a new hidden state at each time step. The GRU achieves comparable performance to the LSTM while using fewer parameters and no separate cell state.


What It Is

The complete GRU cell is a recurrent unit that takes two inputs at each time step: the current input vector x_t \in \mathbb{R}^D and the previous hidden state h_{t-1} \in \mathbb{R}^H, and produces a new hidden state h_t \in \mathbb{R}^H. Unlike the LSTM, which maintains both a hidden state and a separate cell state, the GRU uses only a single hidden state vector. All memory and gating is folded into this one vector.

The cell operates in three steps. First, the reset gate decides how much of the previous hidden state to expose when forming a candidate. Second, the candidate hidden state is computed using the reset-gated version of h_{t-1}. Third, the update gate interpolates between the old hidden state and the new candidate to produce h_t. These three steps use three weight matrices and three bias vectors, making the GRU simpler than the LSTM's four-gate architecture.

The paper describes this as an architecture where "gating units modulate the flow of information inside the unit, without having separate memory cells." The GRU was developed as part of an RNN Encoder-Decoder for machine translation, where the encoder reads a source sentence and the decoder generates the target one token at a time.


Key Equations

Let x_t \in \mathbb{R}^D be the input at time step t and h_{t-1} \in \mathbb{R}^H be the previous hidden state. The concatenation [h_{t-1}, x_t] \in \mathbb{R}^{H+D} is formed by stacking these two vectors. All four equations execute in sequence at each time step.

Equation 1 -- Reset gate. Determines how much of the previous hidden state to forget when computing the candidate:

r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r)

Here W_r \in \mathbb{R}^{H \times (H+D)} and b_r \in \mathbb{R}^H. The sigmoid squashes each element to (0, 1). When r_t \approx 0, the previous hidden state is effectively erased before forming the candidate, allowing the unit to behave as if reading the first token of a new subsequence.

Equation 2 -- Update gate. Controls how much of the old hidden state to carry forward versus replace with new content:

z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z)

Here W_z \in \mathbb{R}^{H \times (H+D)} and b_z \in \mathbb{R}^H. The update gate serves a dual role corresponding to both the forget and input gates in an LSTM. When z_t \approx 1, the cell copies the old hidden state. When z_t \approx 0, the cell replaces the old state entirely with the candidate.

Equation 3 -- Candidate hidden state. Proposes new content to write into the hidden state:

\tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h)

Here W_h \in \mathbb{R}^{H \times (H+D)} and b_h \in \mathbb{R}^H. The element-wise product r_t \odot h_{t-1} applies the reset gate, scaling each hidden dimension independently. The tanh activation bounds the candidate to (-1, 1). This is the only equation where the reset gate appears: it modifies the recurrent input before the linear transformation.

Equation 4 -- Final hidden state. Interpolates between old and new:

h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t

This linear interpolation uses the update gate as a mixing coefficient. Each hidden dimension independently decides its own mix. The constraint that coefficients sum to 1 means the GRU cannot simultaneously amplify both old and new information in the same dimension, acting as an implicit regularizer.


The Three-Step Pipeline

Step 1: Reset and Compute Candidate

The reset gate r_t is computed from the full concatenation [h_{t-1}, x_t], then applied element-wise to h_{t-1} to produce a "reset" version of the previous state. This reset state is concatenated with x_t and passed through a tanh-activated affine transform to produce \tilde{h}_t. The reset gate lets the network learn to selectively ignore parts of the history when forming new proposals. For language modeling, this allows the GRU to treat a sentence boundary as a partial reset without discarding everything.

Step 2: Update Gate Controls Memory Persistence

The update gate z_t is computed in parallel with the reset gate (same concatenation, different weights). A high update gate value means "keep the old value," while a low value means "accept the new candidate." This is the GRU's mechanism for long-range memory: dimensions with z_t \approx 1 persist across many time steps, since the interpolation reduces to h_t \approx h_{t-1} for those dimensions.

Step 3: Interpolation and Data Flow

The final state h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t has an important gradient property. During BPTT, the gradient with respect to h_{t-1} includes a direct path through z_t \odot h_{t-1}. When z_t is close to 1, the gradient passes through nearly unchanged, mitigating the vanishing gradient problem. This additive structure is the same principle that makes LSTM cell states effective at preserving gradients across long sequences.

The complete data flow: concatenate h_{t-1} and x_t, compute r_t and z_t (two independent affine + sigmoid), apply r_t to h_{t-1}, concatenate with x_t, compute \tilde{h}_t via affine + tanh, then interpolate. The output h_t serves as both the cell's output and the recurrent state for the next time step.


Why No Separate Cell State

The LSTM maintains two state vectors: the hidden state h_t (exposed to the outside) and the cell state c_t (internal memory). The output gate filters the cell state to produce h_t = o_t \odot \tanh(c_t), so the LSTM can store information not yet reflected in its output.

The GRU merges these roles into a single vector h_t. Whatever the GRU remembers is always directly available as output. This reduces the state carried between time steps by half, simplifies gradient flow to a single path, and eliminates the output gate entirely. The trade-off is expressiveness: the LSTM can store "private" information in c_t that only becomes visible when the output gate opens. The GRU has no such private memory. Empirically, this matters on tasks requiring complex state tracking but is negligible on many standard benchmarks.


Parameter Count

The GRU has three weight matrices and three bias vectors. Each weight matrix has shape (H, H + D) and each bias vector has shape (H,).

Total GRU parameters:

3H(H + D) + 3H

The LSTM has four gates with the same shapes, totaling 4H(H + D) + 4H. The GRU uses exactly \frac{3}{4} of the LSTM's parameters. With H = 256 and D = 128: GRU has 3 \times 256 \times 384 + 768 = 295{,}680 parameters, the equivalent LSTM has 4 \times 256 \times 384 + 1{,}024 = 394{,}240. The GRU saves roughly 99,000 parameters per layer.


Paper Context

Cho et al. (2014) introduced the GRU in "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation." The paper proposed an RNN Encoder-Decoder where the encoder reads a source sentence token by token, compressing it into a fixed-length vector, and the decoder generates the target sentence from that vector. The GRU was the recurrent unit in both components.

The paper was motivated by the failure of vanilla RNNs to capture long-range dependencies. Machine translation requires remembering subject-verb agreement and syntactic structures across many tokens. The vanishing gradient problem makes this effectively impossible for longer sentences. The GRU's gating mechanism was designed as a simpler alternative to the LSTM that could still maintain information over long spans.

The broader contribution was demonstrating that learned phrase representations from the encoder-decoder could improve phrase-based statistical machine translation. The GRU encoder-decoder scored phrase pairs, and these scores combined with traditional features improved BLEU on WMT'14 English-to-French. Chung et al. (2014) later compared GRU and LSTM across music, speech, and language modeling, concluding that neither consistently dominates, but the GRU's fewer parameters provide a practical efficiency advantage.


Numerical Example

Consider a GRU with H = 2 and D = 2. At time step t:

x_t = \begin{pmatrix} 0.5 \\ -0.3 \end{pmatrix}, \quad h_{t-1} = \begin{pmatrix} 0.1 \\ -0.2 \end{pmatrix}

The concatenation is [h_{t-1}, x_t] = [0.1, -0.2, 0.5, -0.3]. The weight matrices and biases:

W_r = \begin{pmatrix} 0.3 & -0.1 & 0.2 & 0.4 \\ 0.1 & 0.5 & -0.3 & 0.2 \end{pmatrix}, \quad b_r = \begin{pmatrix} 0.1 \\ -0.1 \end{pmatrix}

W_z = \begin{pmatrix} 0.2 & 0.3 & -0.1 & 0.5 \\ -0.2 & 0.1 & 0.4 & -0.3 \end{pmatrix}, \quad b_z = \begin{pmatrix} 0.0 \\ 0.1 \end{pmatrix}

W_h = \begin{pmatrix} -0.1 & 0.4 & 0.3 & -0.2 \\ 0.2 & -0.3 & 0.1 & 0.5 \end{pmatrix}, \quad b_h = \begin{pmatrix} 0.0 \\ 0.1 \end{pmatrix}

Step 1: Reset gate. W_r \cdot [h, x] + b_r:

Row 1: 0.3(0.1) + (-0.1)(-0.2) + 0.2(0.5) + 0.4(-0.3) + 0.1 = 0.03 + 0.02 + 0.10 - 0.12 + 0.1 = 0.13

Row 2: 0.1(0.1) + 0.5(-0.2) + (-0.3)(0.5) + 0.2(-0.3) - 0.1 = 0.01 - 0.10 - 0.15 - 0.06 - 0.1 = -0.40

r_t = [\sigma(0.13), \sigma(-0.40)] = [0.5324, 0.4013]

Step 2: Update gate. W_z \cdot [h, x] + b_z:

Row 1: 0.2(0.1) + 0.3(-0.2) + (-0.1)(0.5) + 0.5(-0.3) + 0.0 = 0.02 - 0.06 - 0.05 - 0.15 = -0.24

Row 2: (-0.2)(0.1) + 0.1(-0.2) + 0.4(0.5) + (-0.3)(-0.3) + 0.1 = -0.02 - 0.02 + 0.20 + 0.09 + 0.1 = 0.35

z_t = [\sigma(-0.24), \sigma(0.35)] = [0.4403, 0.5866]

Step 3: Candidate. r_t \odot h_{t-1} = [0.0532, -0.0803]. Concatenate with x_t: [0.0532, -0.0803, 0.5, -0.3]. Compute W_h \cdot [\cdot] + b_h:

Row 1: (-0.1)(0.0532) + 0.4(-0.0803) + 0.3(0.5) + (-0.2)(-0.3) + 0.0 = -0.005 - 0.032 + 0.15 + 0.06 = 0.173

Row 2: 0.2(0.0532) + (-0.3)(-0.0803) + 0.1(0.5) + 0.5(-0.3) + 0.1 = 0.011 + 0.024 + 0.05 - 0.15 + 0.1 = 0.035

\tilde{h}_t = [\tanh(0.173), \tanh(0.035)] = [0.1714, 0.0350]

Step 4: Interpolation.

h_t[0] = 0.4403 \times 0.1 + 0.5597 \times 0.1714 = 0.0440 + 0.0959 = 0.1400

h_t[1] = 0.5866 \times (-0.2) + 0.4134 \times 0.0350 = -0.1173 + 0.0145 = -0.1028

Final: h_t = [0.1400, -0.1028]. Dimension 0 ($ = 0.44) absorbed more from the candidate, while dimension 1 ( = 0.59$) retained more of the old state.


GRU vs LSTM Comparison

Parameters. GRU: 3 weight matrices of shape (H, H+D), 3 biases. LSTM: 4 of each. Parameter ratio is exactly $$. For H = 1024, D = 512, the GRU saves over 1.5 million parameters per layer.

Gates. The LSTM has four gates: forget, input, candidate cell, output. The GRU has three: reset, update, candidate hidden. The update gate performs the combined role of LSTM's forget and input gates: z_t controls both forgetting (z_t \odot h_{t-1}) and acceptance ((1 - z_t) \odot \tilde{h}_t). The LSTM decouples these, allowing f_t and i_t to vary independently.

Cell state. The LSTM maintains a separate c_t shielded by the output gate. The GRU has no cell state. The LSTM can store information internally that is not yet reflected in its output; the GRU's memory is always fully exposed.

Performance. On most benchmarks (language modeling, speech, translation), GRU and LSTM perform comparably. The GRU trains faster per epoch due to fewer parameters. On tasks requiring precise internal state management, the LSTM sometimes edges ahead.

When to prefer which. Use the GRU when computational budget is limited or datasets are small (fewer parameters reduce overfitting). Use the LSTM when the task involves long sequences with intricate dependencies or maximum model capacity is desired.


Pitfalls


Examples

Example 1

Input
x_t = [1,0.5], h_prev = [0,0], W_r = [[0.2,0.1,0.1,0.2],[0.1,0.3,0.2,0.1]], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], W_h = [[0.3,0.1,0.2,0.1],[0.1,0.2,0.1,0.3]], b_r = [0,0], b_z = [0,0], b_h = [0,0]
Output
[0.101245,0.107231]
Explanation
The two gates and reset-aware candidate combine to produce the next hidden state.

Example 2

Input
x_t = [0.5,1], h_prev = [0.3,-0.2], W_r = [[0.2,0.1,0.1,0.2],[0.1,0.3,0.2,0.1]], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], W_h = [[0.3,0.1,0.2,0.1],[0.1,0.2,0.1,0.3]], b_r = [0.1,0], b_z = [0,-0.1], b_h = [0.1,0.1]
Output
[0.313204,0.064833]

Example 3

Input
x_t = [[1,0],[0,1]], h_prev = [[0,0],[0.5,-0.3]], W_r = [[0.2,0.1,0.1,0.2],[0.1,0.3,0.2,0.1]], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], W_h = [[0.3,0.1,0.2,0.1],[0.1,0.2,0.1,0.3]], b_r = [0,0], b_z = [0,0], b_h = [0,0]
Output
[[0.083995,0.047344],[0.341468,-0.059377]]

Hints

  1. Reuse np.concatenate([h_prev, x_t], axis=-1) for both gates.
  2. Build the candidate input from r_t * h_prev followed by x_t.
  3. Finish with z_t * h_prev + (1 - z_t) * h_tilde.

Requirements

Constraints

Starter Code

import numpy as np

def sigmoid(x: np.ndarray) -> np.ndarray:
    return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))

def gru_cell(x_t: np.ndarray, h_prev: np.ndarray,
             W_r: np.ndarray, W_z: np.ndarray, W_h: np.ndarray,
             b_r: np.ndarray, b_z: np.ndarray, b_h: np.ndarray) -> np.ndarray:
    """
    Returns the float64 next hidden state.
    """
    pass

Test Cases

CaseMatches
One GRU steppublic
Biased GRU steppublic
Batched GRU steppublic