We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Flax step 54 of 100
LSTM Cell Step
Carry two state vectors through one step
An LSTM cell updates a hidden state h and a cell state c. Both have
shape (H,), but they play different roles: the cell state accumulates
information, while the hidden state is a gated, squashed view of it.
The next step receives both returned vectors.
Implement one step for input x of shape (D,). This exercise uses
explicit Linen parameters, with no batch axis, sequence loop, peephole
connections or hidden-state projection.
Compute three gates and a candidate
Concatenate x before h_prev and multiply from the right:
xh = concatenate([x, h_prev]) # (D + H,)
f = sigmoid(xh @ W_f + b_f) # forget gate (H,)
i = sigmoid(xh @ W_i + b_i) # input gate (H,)
o = sigmoid(xh @ W_o + b_o) # output gate (H,)
g = tanh(xh @ W_g + b_g) # candidate (H,)
c_new = f * c_prev + i * g
h_new = o * tanh(c_new)
The sigmoid gates choose how much to retain, write and expose. The candidate uses tanh, so it can add positive or negative values. Every multiplication between state or gate vectors is elementwise.
Use the supplied c_prev; it need not be zero. Compute c_new before
h_new, and apply tanh to the new cell state. Unlike the hidden state,
the cell state is not restricted to the interval from -1 to 1.
What the memory path does and does not guarantee
The term f * c_prev gives the cell state a direct path to the next
step. Holding x and h_prev fixed, the derivative of c_new with
respect to c_prev is a diagonal matrix with f on the diagonal.
A forget gate near one can preserve information along this path.
That is not a guarantee against vanishing gradients. For example, a
fixed forget gate of 0.5 multiplies the direct path by 0.5 at each
step, giving 0.5 ** N after N steps. In a complete recurrence,
dependencies through the hidden state and gates also contribute to
gradients. The cell update can retain, decay or accumulate values;
it does not necessarily leave memory nearly unchanged.
Keep the parameter shapes explicit
Each of W_f, W_i, W_o and W_g has shape (D + H, H).
Each corresponding bias has shape (H,). These are eight parameter
arrays, containing 4 * ((D + H) * H + H) scalar parameters.
For D=3 and H=4, the count is 128.
Concatenation is a storage choice. Splitting a weight into input and hidden rows gives the same affine map:
xh @ W + b = x @ W[:D] + h_prev @ W[D:] + b
Likewise, packing the four matrices side by side can compute the four projections together if you copy the same weights and split in the same order. Freshly initializing a packed matrix does not reproduce this exercise's four separately named random matrices.
A step with known parameters
Set all matrices to zero. Zero gate biases make f, i and o
equal to 0.5; choose a candidate bias that makes g=0.5:
import jax
import jax.numpy as jnp
x = jnp.array([1.])
h_prev = jnp.array([.2, -.1])
c_prev = jnp.array([2., -2.])
xh = jnp.concatenate([x, h_prev])
W = jnp.zeros((3, 2))
gate_bias = jnp.zeros(2)
candidate_bias = jnp.full(2, jnp.arctanh(.5))
f = jax.nn.sigmoid(xh @ W + gate_bias)
i = jax.nn.sigmoid(xh @ W + gate_bias)
o = jax.nn.sigmoid(xh @ W + gate_bias)
g = jnp.tanh(xh @ W + candidate_bias)
c_new = f * c_prev + i * g
h_new = o * jnp.tanh(c_new)
result = jnp.concatenate([h_new, c_new])
assert jnp.allclose(c_new, jnp.array([1.25, -.75]))
assert jnp.allclose(h_new, .5 * jnp.tanh(jnp.array([1.25, -.75])))
assert result.shape == (4,)
assert jnp.array_equal(result[2:], c_new)
The equal matrices in this example make the arithmetic simple. The exercise still creates four independently named weight/bias pairs.
Problem
Implement lstm_cell_step(seed, x, h_prev, c_prev, hidden):
- Convert
hiddento Python integerH. DefineMyLSTMCell(nn.Module)with ahiddenfield and compact__call__(self, x, h_prev, c_prev). - Declare root parameters in this order:
W_f,b_f,W_i,b_i,W_o,b_o,W_g,b_g. Usenn.initializers.lecun_normal()for every matrix andnn.initializers.zerosfor every bias, including the forget bias. Preserve the names and shapes described above. - Compute the equations with
jax.nn.sigmoidandjnp.tanh. Returnjnp.concatenate([h_new, c_new], axis=-1)from the cell. - Initialize the cell directly with
jax.random.PRNGKey(int(seed))and all three input vectors. Apply those variables to the same inputs, then return.reshape(-1).
Inputs are one-dimensional float arrays: x has length D, while
h_prev and c_prev both have length H. Both dimensions are positive,
and hidden equals H. Keep seed and hidden static under JIT.
Return a one-dimensional array of length 2 * H, with the entire new
hidden state first and the entire new cell state second. These states
are explicit inputs and outputs, not learned parameters or mutable
collections. For another step, split the result into those two pieces
and reuse the initialized parameters.
The widget below demonstrates carrying a scalar between steps using
carry = 0.5 * carry + x. It illustrates recurrence, not the LSTM's
two vector states, learned gates or expected outputs.
Loading visualization…
Understand the ideas behind this exercise. The Problem tab contains the exact implementation requirements.
Carry two state vectors through one step
An LSTM cell updates a hidden state h and a cell state c. Both have
shape (H,), but they play different roles: the cell state accumulates
information, while the hidden state is a gated, squashed view of it.
The next step receives both returned vectors.
Implement one step for input x of shape (D,). This exercise uses
explicit Linen parameters, with no batch axis, sequence loop, peephole
connections or hidden-state projection.
Compute three gates and a candidate
Concatenate x before h_prev and multiply from the right:
xh = concatenate([x, h_prev]) # (D + H,)
f = sigmoid(xh @ W_f + b_f) # forget gate (H,)
i = sigmoid(xh @ W_i + b_i) # input gate (H,)
o = sigmoid(xh @ W_o + b_o) # output gate (H,)
g = tanh(xh @ W_g + b_g) # candidate (H,)
c_new = f * c_prev + i * g
h_new = o * tanh(c_new)
The sigmoid gates choose how much to retain, write and expose. The candidate uses tanh, so it can add positive or negative values. Every multiplication between state or gate vectors is elementwise.
Use the supplied c_prev; it need not be zero. Compute c_new before
h_new, and apply tanh to the new cell state. Unlike the hidden state,
the cell state is not restricted to the interval from -1 to 1.
What the memory path does and does not guarantee
The term f * c_prev gives the cell state a direct path to the next
step. Holding x and h_prev fixed, the derivative of c_new with
respect to c_prev is a diagonal matrix with f on the diagonal.
A forget gate near one can preserve information along this path.
That is not a guarantee against vanishing gradients. For example, a
fixed forget gate of 0.5 multiplies the direct path by 0.5 at each
step, giving 0.5 ** N after N steps. In a complete recurrence,
dependencies through the hidden state and gates also contribute to
gradients. The cell update can retain, decay or accumulate values;
it does not necessarily leave memory nearly unchanged.
Keep the parameter shapes explicit
Each of W_f, W_i, W_o and W_g has shape (D + H, H).
Each corresponding bias has shape (H,). These are eight parameter
arrays, containing 4 * ((D + H) * H + H) scalar parameters.
For D=3 and H=4, the count is 128.
Concatenation is a storage choice. Splitting a weight into input and hidden rows gives the same affine map:
xh @ W + b = x @ W[:D] + h_prev @ W[D:] + b
Likewise, packing the four matrices side by side can compute the four projections together if you copy the same weights and split in the same order. Freshly initializing a packed matrix does not reproduce this exercise's four separately named random matrices.
A step with known parameters
Set all matrices to zero. Zero gate biases make f, i and o
equal to 0.5; choose a candidate bias that makes g=0.5:
import jax
import jax.numpy as jnp
x = jnp.array([1.])
h_prev = jnp.array([.2, -.1])
c_prev = jnp.array([2., -2.])
xh = jnp.concatenate([x, h_prev])
W = jnp.zeros((3, 2))
gate_bias = jnp.zeros(2)
candidate_bias = jnp.full(2, jnp.arctanh(.5))
f = jax.nn.sigmoid(xh @ W + gate_bias)
i = jax.nn.sigmoid(xh @ W + gate_bias)
o = jax.nn.sigmoid(xh @ W + gate_bias)
g = jnp.tanh(xh @ W + candidate_bias)
c_new = f * c_prev + i * g
h_new = o * jnp.tanh(c_new)
result = jnp.concatenate([h_new, c_new])
assert jnp.allclose(c_new, jnp.array([1.25, -.75]))
assert jnp.allclose(h_new, .5 * jnp.tanh(jnp.array([1.25, -.75])))
assert result.shape == (4,)
assert jnp.array_equal(result[2:], c_new)
The equal matrices in this example make the arithmetic simple. The exercise still creates four independently named weight/bias pairs.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
JAX reference solution
Sign in to track your attempts toward unlocking the reference solution.