This repository implements a single LSTM cell from scratch in PyTorch and explains the math behind it.
The aim is to make the mapping
explicit and consistent with standard LSTM definitions.
- Custom
LSTMCellCustommodule in PyTorch - Gate-by-gate mathematical formulation
- Minimal example that unrolls the cell across a sequence
- Optional tests (shape checks / comparison to
nn.LSTMCell)
We model an input sequence
The LSTM maintains at each time step:
- hidden state:
$h_t \in \mathbb{R}^{d_h}$ - cell state:
$c_t \in \mathbb{R}^{d_h}$
Given
- input dimension:
$d_x$ - hidden / cell dimension:
$d_h$
Gates:
- input gate:
$i_t \in \mathbb{R}^{d_h}$ - forget gate:
$f_t \in \mathbb{R}^{d_h}$ - output gate:
$o_t \in \mathbb{R}^{d_h}$ - candidate cell (proposal):
$g_t \in \mathbb{R}^{d_h}$
Nonlinearities:
- sigmoid:
$\sigma(z) = \dfrac{1}{1 + e^{-z}}$ - hyperbolic tangent:
$\tanh(z)$
Parameters (single layer, single direction):
- input-to-gates:
$W_x \in \mathbb{R}^{4 d_h \times d_x}$ - hidden-to-gates:
$W_h \in \mathbb{R}^{4 d_h \times d_h}$ - bias:
$b \in \mathbb{R}^{4 d_h}$
All four gates share these matrices and are obtained by splitting the
- Combined affine transform:
- Split into blocks (each
$d_h$ long):
- Apply nonlinearities:
- Cell state update:
where
- Hidden state update:
Stack the gates:
Then
where
The key recurrence is
For component
-
$f_t^{(k)} \approx 1$ → information and gradients are kept over time. -
$f_t^{(k)} \approx 0$ → that component is reset and gradients vanish on this path.
A common trick is to initialize the forget-gate bias
The core cell is implemented as a single nn.Module.
The module is called LSTMCellCustom(input_size, hidden_size) and implements the equations described above:
- input:
x_tof shape(batch_size, input_size) - hidden:
h_{t-1}of shape(batch_size, hidden_size) - cell:
c_{t-1}of shape(batch_size, hidden_size) - output:
(h_t, c_t)with the same shapes as the hidden and cell states.
A small script shows how to unroll the custom LSTM cell across a full sequence and collect all hidden states.
examples/simple_sequence_demo.py:
import torch
from src.lstm_cell import LSTMCellCustom
def run_demo():
torch.manual_seed(0)
batch_size = 2
seq_len = 5
input_size = 3
hidden_size = 4
# Input sequence: (batch, time, features)
x = torch.randn(batch_size, seq_len, input_size)
cell = LSTMCellCustom(input_size, hidden_size)
# Initial states
h_t = torch.zeros(batch_size, hidden_size)
c_t = torch.zeros(batch_size, hidden_size)
outputs = []
for t in range(seq_len):
x_t = x[:, t, :]
h_t, c_t = cell(x_t, h_t, c_t)
outputs.append(h_t.unsqueeze(1))
# Stack over time: (batch, time, hidden)
H = torch.cat(outputs, dim=1)
print("Hidden sequence shape:", H.shape)
if __name__ == "__main__":
run_demo()python examples/simple_sequence_demo.pyYou should see:
Hidden sequence shape: torch.Size([2, 5, 4])This repository includes a small test to check that the LSTM cell returns tensors with the expected shapes.
tests/test_lstm_cell_shapes.py:
import torch
from src.lstm_cell import LSTMCellCustom
def test_lstm_cell_shapes():
batch_size = 3
input_size = 5
hidden_size = 7
x_t = torch.randn(batch_size, input_size)
h_prev = torch.randn(batch_size, hidden_size)
c_prev = torch.randn(batch_size, hidden_size)
cell = LSTMCellCustom(input_size, hidden_size)
h_t, c_t = cell(x_t, h_prev, c_prev)
assert h_t.shape == (batch_size, hidden_size)
assert c_t.shape == (batch_size, hidden_size)- S. Hochreiter and J. Schmidhuber, “Long Short-Term Memory,” Neural Computation, 1997.
- PyTorch documentation for torch.nn.LSTM and torch.nn.LSTMCell.