Skip to content

← Flax step 55 of 100

Medium Primitives

Bidirectional RNN (Flax)

Jump to the task

Read a sequence in both directions

A forward recurrent network processes tokens from left to right. Its output at position t can depend on inputs 0 through t. A backward network processes the same sequence from right to left, so its aligned output at t can depend on inputs t through T - 1.

Run these two networks with independent parameters and concatenate their outputs at each original position. The result has shape (T, 2H): H forward features followed by H backward features.

Both halves include the current input. Together they can use context from the whole sequence, but a finite hidden vector need not preserve every detail. The backward half also needs later inputs, so this model cannot produce its complete representation immediately as tokens arrive in a left-to-right stream.

Reverse the input, then realign the output

This exercise uses two separate nn.RNN(nn.GRUCell(features=H)) instances. The wrapper adds a batch dimension of size one:

x                 (T, D)
x_b = x[None, ...] (1, T, D)
forward outputs   (1, T, H)
backward outputs  (1, T, H)
concatenated      (1, T, 2H)
remove batch      (T, 2H)

Call the forward RNN on x_b. Call the backward RNN on x_b[:, ::-1, :], then reverse that result on the time axis as well. The first reversal chooses processing order; the second puts the backward states alongside the inputs they belong to.

A simple cumulative sum makes the alignment visible without GRU weights:

import jax.numpy as jnp

x = jnp.array([1., 2., 4.])
forward = jnp.cumsum(x)                  # [1, 3, 7]
backward_processing = jnp.cumsum(x[::-1]) # [4, 6, 7]
backward = backward_processing[::-1]     # [7, 6, 4]
aligned = jnp.stack([forward, backward], axis=-1)

assert jnp.array_equal(aligned, jnp.array([[1., 7.], [3., 6.], [7., 4.]]))
assert jnp.array_equal(aligned.reshape(-1), jnp.array([1., 7., 3., 6., 7., 4.]))

This example illustrates order and concatenation only. A GRU computes learned gates rather than cumulative sums. Without the second reversal, row zero would pair the forward state for the first input with the backward state for the last input.

Use Linen's built-in GRU

Use nn.GRUCell defaults for the activations, initializers and zero initial carry. Linen's cell weights the previous state by its update gate and applies the reset gate after the recurrent candidate projection. This differs from the custom GRU cell exercise, which weights the candidate by its update gate and resets before that projection. Substituting the custom cell changes this problem's outputs.

Each direction shares its own cell parameters across time. The two directions do not share parameters with each other. Construct both unnamed RNN/cell pairs inside BiRNN in forward-then-backward order, preserving the built-in cells' parameter scopes and defaults.

nn.RNN supports inputs with zero or more batch dimensions; the leading size-one dimension is the layout used by this wrapper, not a general requirement of the API. With the default time_major=False, this wrapper's time axis is axis 1. The default return_carry=False returns all timestep outputs, not just a final state.

Every supplied timestep is valid. Do not supply sequence lengths, carry between calls, or padding masks. Each call starts both directions from zero carry. The model has no training step or output prediction head.

Problem

Implement birnn_forward(seed, x, hidden):

  1. Convert hidden to Python integer H. Define a compact BiRNN module with a hidden field.
  2. Construct rnn_fwd = nn.RNN(nn.GRUCell(features=H)), followed by rnn_bwd = nn.RNN(nn.GRUCell(features=H)). Keep all defaults.
  3. Add the batch axis, run the forward RNN, and run the backward RNN on reversed inputs. Reverse the backward outputs back to original order. Leave the RNNs' own reverse flags at their default false because the array slices already perform the reversals.
  4. Concatenate [fwd, bwd] along axis=-1, then remove the batch axis with out[0].
  5. Initialize the complete model directly with jax.random.PRNGKey(int(seed)) and x, apply those variables to x, and return .reshape(-1).

x is a two-dimensional float array with shape (T, D). T, D and H are positive. Return a one-dimensional array of length T * 2 * H. Within each timestep, forward features come first and backward features second. Keep seed and hidden static under JIT. Module construction order and initializer choices matter for seeded tests.

The widget below shows a single scalar recurrence with carry = 0.5 * carry + x. It demonstrates passing state between steps; it does not display the two GRUs or their learned outputs.

Loading visualization…