EasyGRU

Update Gate

Learning Phrase Representations using RNN Encoder-Decoder (GRU)

Easy

Problem

Implement the GRU update gate:

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

Here, h_{t-1} is the previous hidden state with H features, x_t is the current input with D features, square brackets mean concatenation in that order, and \sigma is the sigmoid function. The weight rows produce H gate values. Accept one sample or a batch and return a matching float64 NumPy array.

Theory

The update gate is one of two gating mechanisms inside the Gated Recurrent Unit (GRU) introduced by Cho et al. (2014). It controls how much of the previous hidden state h_{t-1} to carry forward versus how much of the newly computed candidate hidden state \tilde{h}_t to accept. The paper describes it as performing "leaky integration," producing a smooth, learned interpolation between remembering and updating at every time step.


What It Is

The update gate is a vector z_t \in \mathbb{R}^d (where d is the hidden dimension) whose elements lie in [0, 1] thanks to the sigmoid activation. At each time step t, the GRU uses z_t to decide, dimension by dimension, whether to keep the old hidden state or replace it with a new candidate. When z_t^{(j)} = 1 for some dimension j, the GRU copies h_{t-1}^{(j)} forward unchanged. When z_t^{(j)} = 0, it replaces that dimension entirely with the candidate \tilde{h}_t^{(j)}. Values between zero and one produce a weighted blend.

This is the mechanism that allows GRUs to capture long-range dependencies. Without the update gate, the hidden state would be overwritten at every step. With it, the network can learn to hold certain dimensions constant for hundreds of steps while freely updating others, adapting its memory retention on a per-dimension, per-time-step basis.


Key Equations

Concatenation

The update gate operates on the concatenation of the previous hidden state h_{t-1} \in \mathbb{R}^d and the current input x_t \in \mathbb{R}^n:

[h_{t-1}, x_t] \in \mathbb{R}^{d+n}

Both the current input and the previous hidden state jointly determine how much to update. The gate can learn patterns like "when the input signals a paragraph boundary, open the gate to accept new information" or "when the input is uninformative, keep the gate closed to preserve existing state."

Linear Transformation and Sigmoid

The concatenated vector is multiplied by a learned weight matrix W_z \in \mathbb{R}^{d \times (d+n)}, a bias b_z \in \mathbb{R}^d is added, and the result is passed through the element-wise sigmoid:

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

The weight matrix W_z can be decomposed into two blocks: W_{zh} \in \mathbb{R}^{d \times d} acting on h_{t-1} and W_{zx} \in \mathbb{R}^{d \times n} acting on x_t. Some implementations write this as W_z x_t + U_z h_{t-1} + b_z, where U_z is the hidden-to-hidden weight block. Both formulations are mathematically identical. The sigmoid \sigma(a) = 1/(1 + e^{-a}) squashes each element into (0, 1), producing valid interpolation coefficients.


The Interpolation Mechanism

Once z_t is computed, the GRU produces the final hidden state by linearly interpolating between the previous state h_{t-1} and the candidate \tilde{h}_t:

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

Here \odot denotes element-wise (Hadamard) multiplication. This is a convex combination: for each dimension j, h_t^{(j)} = z_t^{(j)} \cdot h_{t-1}^{(j)} + (1 - z_t^{(j)}) \cdot \tilde{h}_t^{(j)}. The coefficients z_t^{(j)} and 1 - z_t^{(j)} always sum to one.

When z_t = 1: Perfect Memory

If z_t^{(j)} = 1, then h_t^{(j)} = h_{t-1}^{(j)}. The previous state is copied forward with no modification. The candidate is computed but completely ignored. A dimension with z_t \approx 1 at every step carries its value across potentially hundreds of time steps unchanged.

When z_t = 0: Complete Replacement

If z_t^{(j)} = 0, then h_t^{(j)} = \tilde{h}_t^{(j)}. The old state is discarded entirely. This allows the GRU to rapidly adapt when the input requires a fresh representation, for instance at a sentence boundary or topic shift.

Intermediate Values: Soft Blending

For z_t^{(j)} = 0.7, the GRU keeps 70\% of the old state and mixes in 30\% of the candidate. This gradual blending is differentiable and trainable by backpropagation through time. The network does not need to make hard binary decisions; it can learn smooth transitions that balance stability with plasticity.


Why Leaky Integration

Cho et al. (2014) specifically describe the update gate as performing "leaky integration." A leaky integrator is a system whose state decays exponentially unless refreshed by new input. The canonical form is s_t = \alpha \cdot s_{t-1} + (1 - \alpha) \cdot x_t, where \alpha \in [0, 1] is a fixed decay constant. The GRU's update equation matches this structure exactly, with two crucial generalizations: the leak rate z_t is a learned, input-dependent vector that changes at every time step, and the "input" is the candidate \tilde{h}_t rather than the raw x_t.

Connection to Exponential Moving Averages

An exponential moving average (EMA) with smoothing factor \alpha follows s_t = \alpha \cdot s_{t-1} + (1 - \alpha) \cdot x_t. When z_t is nearly constant (say z_t \approx 0.9), the hidden state behaves like an EMA with \alpha = 0.9: the effective memory window spans roughly 1/(1 - 0.9) = 10 time steps. The difference is that the GRU's z_t varies dynamically, allowing the effective memory window to expand or contract based on input content.

Adaptive Time Constants Per Dimension

Because z_t is a vector, each hidden dimension has its own time constant. Some dimensions might maintain z_t^{(j)} \approx 0.99, giving them a time constant of roughly 100 steps. Others might have $ \approx 0.1$, responding rapidly with a time constant of about 1.1 steps. This per-dimension adaptivity lets the GRU simultaneously track slow-moving context (document topic) and fast-changing features (local syntax) within the same hidden state vector.


How Update Differs from Reset

The GRU has two gates: the update gate z_t and the reset gate r_t. They serve fundamentally different roles, and confusing them is a common source of implementation errors.

Reset Gate: Candidate Preparation

The reset gate r_t operates earlier in the computation. It controls how much of h_{t-1} is visible when computing the candidate:

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

When r_t \approx 0, the previous hidden state is masked out, forcing the candidate to depend primarily on x_t. When r_t \approx 1, the full history is available and the candidate can be a refined version of the existing state.

Update Gate: Final Mixing

The update gate operates after the candidate has been computed. It determines how the candidate is blended into the actual hidden state. Even if the reset gate produces a radically different candidate, the update gate can reject it by setting z_t \approx 1. The two gates form a two-stage decision: the reset gate asks "what should the candidate look like?" and the update gate asks "how much of that candidate should I actually use?"


Paper Context

Cho et al. (2014)

The GRU was introduced in "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation." The paper's primary contribution was the encoder-decoder architecture for sequence-to-sequence modeling, and the GRU was proposed as the recurrent unit within it. The authors motivated the gating mechanism by the need to capture long-range dependencies in machine translation, where word meaning may depend on context many tokens away.

The paper explicitly states: "The update gate z_j selects whether the hidden state is to be updated with a new hidden state. This acts like a leaky integration." This positions the update gate as the central mechanism for temporal memory control, with the leak rate determining the effective time scale at which the network operates.

Simpler Than LSTM

The LSTM uses three gates (forget, input, output) plus a separate cell state. The GRU achieves similar gating with only two gates and no separate cell state. The update gate plays the combined role of the LSTM's forget and input gates: z_t simultaneously controls how much to forget (z_t multiplies h_{t-1}) and how much new information to admit (1 - z_t multiplies \tilde{h}_t). This coupling means three weight matrices instead of four and faster training, at the cost of slightly reduced flexibility.


Numerical Example

Consider a GRU with hidden dimension d = 3 and input dimension n = 2.

Given Values

Previous hidden state and current input:

h_{t-1} = [0.8, -0.3, 0.5], \quad x_t = [1.0, -0.5]

Candidate hidden state (already computed via the reset gate path):

\tilde{h}_t = [0.2, 0.9, -0.4]

Update gate weights W_z \in \mathbb{R}^{3 \times 5} and bias b_z \in \mathbb{R}^3:

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

Step 1: Concatenate

[h_{t-1}, x_t] = [0.8, -0.3, 0.5, 1.0, -0.5]

Step 2: Linear Transformation

Dimension 1: a_z^{(1)} = 0.3(0.8) + (-0.1)(-0.3) + 0.4(0.5) + 0.2(1.0) + (-0.3)(-0.5) + 0.1 = 0.24 + 0.03 + 0.20 + 0.20 + 0.15 + 0.1 = 0.92

Dimension 2: a_z^{(2)} = 0.1(0.8) + 0.5(-0.3) + (-0.2)(0.5) + 0.3(1.0) + 0.1(-0.5) + (-0.1) = 0.08 - 0.15 - 0.10 + 0.30 - 0.05 - 0.1 = -0.02

Dimension 3: a_z^{(3)} = (-0.2)(0.8) + 0.3(-0.3) + 0.6(0.5) + (-0.1)(1.0) + 0.4(-0.5) + 0.0 = -0.16 - 0.09 + 0.30 - 0.10 - 0.20 + 0.0 = -0.25

a_z = [0.92, -0.02, -0.25]

Step 3: Sigmoid

z_t^{(1)} = \sigma(0.92) = 1/(1 + e^{-0.92}) = 1/1.3985 = 0.7153

z_t^{(2)} = \sigma(-0.02) = 1/(1 + e^{0.02}) = 1/2.0202 = 0.4950

z_t^{(3)} = \sigma(-0.25) = 1/(1 + e^{0.25}) = 1/2.2840 = 0.4378

z_t = [0.7153, 0.4950, 0.4378]

Step 4: Interpolation

h_t^{(1)} = 0.7153 \cdot 0.8 + 0.2847 \cdot 0.2 = 0.5722 + 0.0569 = 0.6292

h_t^{(2)} = 0.4950 \cdot (-0.3) + 0.5050 \cdot 0.9 = -0.1485 + 0.4545 = 0.3060

h_t^{(3)} = 0.4378 \cdot 0.5 + 0.5622 \cdot (-0.4) = 0.2189 - 0.2249 = -0.0060

h_t = [0.6292, 0.3060, -0.0060]

Interpreting the Result

Dimension 1 has z_t^{(1)} = 0.72, so it keeps most of h_{t-1}^{(1)} = 0.8, resulting in 0.63 -- close to the original. Dimension 2 has $ = 0.50$, producing an equal blend of -0.3 and 0.9, yielding 0.31. Dimension 3 has the lowest gate value ($$), leaning toward the candidate -0.4 and pulling the result near zero. Each dimension makes its own independent keep-versus-replace decision.


Connection to LSTM

The LSTM's cell state update uses two separate gates:

c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t

Here f_t is the forget gate and i_t is the input gate. Crucially, f_t and i_t are computed independently, so there is no constraint that f_t + i_t = 1. The LSTM can simultaneously forget everything (f_t \approx 0) and accept nothing (i_t \approx 0), or keep everything (f_t \approx 1) and also add new information (i_t \approx 1).

The GRU's update gate imposes z_t + (1 - z_t) = 1, a strict convex combination. If the GRU wants to keep more of the old state, it must proportionally accept less of the candidate. One gate (z_t) replaces both f_t and i_t, reducing the parameter count by one full gate's worth of weights. The LSTM also has an output gate o_t that the GRU lacks; the GRU exposes the full hidden state as output at every step. The trade-off is that the LSTM's independent gates provide strictly more representational flexibility, though in practice this rarely translates to a meaningful performance advantage.


Pitfalls


Examples

Example 1

Input
h_prev = [0,0], x_t = [1,0.5], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], b_z = [0,0]
Output
[0.586618,0.562177]
Explanation
Concatenation, the update-gate affine transform, and sigmoid produce one gate value per hidden feature.

Example 2

Input
h_prev = [0.3,-0.2], x_t = [0.5,1], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], b_z = [0,-0.1]
Output
[0.559714,0.571996]

Example 3

Input
h_prev = [[0,0],[0.5,-0.3]], x_t = [[1,0],[0,1]], W_z = [[0.1,0.2,0.3,0.1],[0.2,0.1,0.1,0.3]], b_z = [0,0]
Output
[[0.574443,0.524979],[0.522485,0.591459]]

Hints

  1. Use np.concatenate([h_prev, x_t], axis=-1).
  2. The affine transform is joined @ W_z.T + b_z.

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 update_gate(h_prev: np.ndarray, x_t: np.ndarray,
          W_z: np.ndarray, b_z: np.ndarray) -> np.ndarray:
    """
    Returns the float64 update-gate values.
    """
    pass

Test Cases

CaseMatches
Zero hidden statepublic
Biased gatepublic
Batched gatepublic