LSTM¶
Simple LSTM-based sequence-to-sequence model for neural machine translation.
Overview¶
SimpleSeq2SeqLSTM implements a classic encoder-decoder architecture using LSTM (Long Short-Term Memory) cells. The encoder reads the source sequence and compresses it into a context vector, which initializes the decoder to generate the target sequence.
Architecture¶
┌─────────────────────────────────────────────────┐
│ ENCODER │
│ ┌───────────────────────────────────────────┐ │
│ │ Token Embedding │ │
│ │ src_vocab_size → emb_dim │ │
│ └───────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────┐ │
│ │ LSTM Layers × N │ │
│ │ Process sequence step by step │ │
│ │ Output: (hidden_state, cell_state) │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
↓
(final_hidden, final_cell)
↓
┌─────────────────────────────────────────────────┐
│ DECODER │
│ ┌───────────────────────────────────────────┐ │
│ │ Token Embedding │ │
│ │ tgt_vocab_size → emb_dim │ │
│ └───────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────┐ │
│ │ LSTM Layers × N │ │
│ │ Initialized with encoder states │ │
│ │ Process target sequence │ │
│ └───────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────┐ │
│ │ Linear Output │ │
│ │ hidden_dim → tgt_vocab_size │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Quick Start¶
from torchlingo.models import SimpleSeq2SeqLSTM
model = SimpleSeq2SeqLSTM(
src_vocab_size=10000,
tgt_vocab_size=10000,
emb_dim=256,
hidden_dim=512,
num_layers=2,
)
# Training
logits = model(src_batch, tgt_batch) # [batch, tgt_len, vocab]
API Reference¶
SimpleSeq2SeqLSTM
¶
SimpleSeq2SeqLSTM(src_vocab_size: int, tgt_vocab_size: int, emb_dim: int | None = None, hidden_dim: int | None = None, num_layers: int | None = None, dropout: float | None = None, pad_idx: int | None = None, attention: bool | None = None, attn_type: str | None = None, config: Config | None = None)
Bases: Module
Simple LSTM encoder-decoder model for sequence-to-sequence tasks.
This model encodes source sequences into a context vector using an LSTM encoder, then decodes target sequences using an LSTM decoder initialized with the encoder's final hidden and cell states. Token embeddings use padding index from config.
Without attention, the only channel from encoder to decoder is that final
hidden state -- a fixed-size summary of the whole source sentence. With
attention=True the decoder additionally attends over every encoder
output, removing that bottleneck.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src_vocab_size
|
int
|
Size of the source vocabulary. |
required |
tgt_vocab_size
|
int
|
Size of the target vocabulary. |
required |
emb_dim
|
int
|
Embedding dimension. Falls back to config.lstm_emb_dim. |
None
|
hidden_dim
|
int
|
Hidden dimension for LSTM layers. Falls back to config.lstm_hidden_dim. |
None
|
num_layers
|
int
|
Number of LSTM layers in encoder and decoder. Falls back to config.lstm_num_layers. |
None
|
dropout
|
float
|
Dropout rate applied between LSTM layers. Falls back to config.lstm_dropout. |
None
|
pad_idx
|
int
|
Padding token index for embeddings. Falls back to config.pad_idx. |
None
|
attention
|
bool
|
Whether the decoder attends over encoder outputs.
Falls back to config.lstm_attention (default |
None
|
attn_type
|
str
|
Scoring function, |
None
|
config
|
Config
|
Configuration object. Defaults to default config. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
pad_idx |
int
|
Resolved padding index. |
src_embed |
Embedding
|
Source token embedding layer. |
tgt_embed |
Embedding
|
Target token embedding layer. |
encoder |
LSTM
|
LSTM encoder for source sequences. |
decoder |
LSTM
|
LSTM decoder for target sequences. |
attention |
Module | None
|
Attention module, or |
attn_combine |
Linear | None
|
Merges context with the decoder state,
or |
output |
Linear
|
Linear output layer projecting decoder hidden state to target vocabulary. |
hidden_dim |
int
|
Hidden dimension size. |
Note
Enabling attention adds parameters, so a checkpoint saved with
attention=False will not load into a model built with
attention=True (and vice versa). Build the model the same way you
trained it.
Source code in src/torchlingo/models/lstm_simple.py
forward
¶
forward(src: Tensor, tgt: Tensor, return_attention: bool = False) -> Tensor | tuple[Tensor, Tensor | None]
Encode source and decode target sequences.
Encodes source tokens into context using the LSTM encoder, then passes
the final hidden and cell states to the LSTM decoder to generate target
token predictions. When the model was built with attention=True, the
decoder states are additionally blended with a weighted average of the
encoder outputs before the output projection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src
|
Tensor
|
Source token indices with shape (batch_size, src_len). Values should be in range [0, src_vocab_size). |
required |
tgt
|
Tensor
|
Target token indices with shape (batch_size, tgt_len). Values should be in range [0, tgt_vocab_size). |
required |
return_attention
|
bool
|
Also return the attention weights. Defaults to False, which preserves the plain-tensor return type. |
False
|
Returns:
| Type | Description |
|---|---|
Tensor | tuple[Tensor, Tensor | None]
|
torch.Tensor: Logits of shape (batch_size, tgt_len, tgt_vocab_size) representing probability distributions over the target vocabulary for each position. |
Tensor | tuple[Tensor, Tensor | None]
|
If |
Tensor | tuple[Tensor, Tensor | None]
|
tuple instead, where |
Tensor | tuple[Tensor, Tensor | None]
|
(batch_size, tgt_len, src_len) -- or is |
Tensor | tuple[Tensor, Tensor | None]
|
no attention. |
Examples:
>>> model = SimpleSeq2SeqLSTM(50, 50, emb_dim=8, hidden_dim=8, attention=True)
>>> src, tgt = torch.randint(1, 50, (2, 5)), torch.randint(1, 50, (2, 3))
>>> logits, weights = model(src, tgt, return_attention=True)
>>> weights.shape
torch.Size([2, 3, 5])
Source code in src/torchlingo/models/lstm_simple.py
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
src_vocab_size |
int | required | Source vocabulary size |
tgt_vocab_size |
int | required | Target vocabulary size |
emb_dim |
int | 256 | Embedding dimension |
hidden_dim |
int | 512 | LSTM hidden dimension |
num_layers |
int | 2 | Number of stacked LSTM layers |
dropout |
float | 0.1 | Dropout between LSTM layers |
pad_idx |
int | 0 | Padding token index |
attention |
bool | False | Let the decoder attend over encoder outputs |
attn_type |
str | "dot" |
Scorer: "dot" (Luong) or "additive" (Bahdanau) |
config |
Config | None | Configuration object |
Examples¶
Basic Training¶
import torch
from torchlingo.models import SimpleSeq2SeqLSTM
model = SimpleSeq2SeqLSTM(
src_vocab_size=10000,
tgt_vocab_size=10000,
emb_dim=256,
hidden_dim=512,
)
# Dummy data
src = torch.randint(0, 10000, (32, 20)) # [batch, src_len]
tgt = torch.randint(0, 10000, (32, 25)) # [batch, tgt_len]
# Forward pass
logits = model(src, tgt[:, :-1]) # [32, 24, 10000]
# Compute loss
criterion = torch.nn.CrossEntropyLoss(ignore_index=0)
loss = criterion(
logits.reshape(-1, logits.size(-1)),
tgt[:, 1:].reshape(-1)
)
With Config¶
from torchlingo.config import Config
config = Config(
lstm_emb_dim=256,
lstm_hidden_dim=512,
lstm_num_layers=3,
lstm_dropout=0.2,
)
model = SimpleSeq2SeqLSTM(
src_vocab_size=10000,
tgt_vocab_size=10000,
config=config,
)
Inference (Greedy and Beam Search)¶
Use the library's decoders rather than writing the loop yourself. Both accept an LSTM model:
from torchlingo.inference import beam_search_decode, greedy_decode
decoded = greedy_decode(model, src_batch, max_len=50) # list[list[int]]
tokens = beam_search_decode(model, src, beam_size=5) # one sentence
The search is the same code that decodes a Transformer — see Decoding. Three methods make that possible:
| Method | Purpose |
|---|---|
encode_source(src) |
Returns (enc_out, hidden, src_pad_mask) — everything the decoder may need |
decode_prefix(tgt, hidden, enc_out, mask) |
Scores a whole target prefix; the counterpart to a Transformer's decode(tgt, memory) |
decode_step(token, hidden, enc_out, mask) |
Advances one token, carrying state forward |
If you do want to step manually — to inspect attention at each step, say — use
encode_source and decode_step so the encoder outputs actually reach the
decoder:
enc_out, hidden, src_pad_mask = model.encode_source(src)
ys = [tgt_vocab.sos_idx]
alignments = []
for _ in range(max_len):
last = torch.tensor([ys[-1:]], device=src.device)
logits, hidden, weights = model.decode_step(last, hidden, enc_out, src_pad_mask)
alignments.append(weights) # None when attention is disabled
next_token = logits[:, -1, :].argmax().item()
ys.append(next_token)
if next_token == tgt_vocab.eos_idx:
break
Don't re-implement the decoder loop
Driving model.decoder directly from the encoder's final (h, c) — as
earlier versions of this page showed — throws away the per-token encoder
outputs. On an attention model that silently disables attention, and the
model will appear to work while producing worse translations.
How LSTMs Work¶
The Information Bottleneck¶
The encoder compresses the entire source sequence into a fixed-size vector (the final hidden state). This becomes the "context" for the decoder.
Limitation: Long sequences can be hard to compress into a single vector.
With attention=True, the decoder additionally reads every encoder output,
weighted per step, so the sentence no longer has to survive that squeeze. Run
python examples/attention_alignment.py to see the difference measured on a
task with a known correct alignment.
Hidden and Cell States¶
LSTMs maintain two types of state:
- Hidden state (h): Short-term memory, used for output
- Cell state (c): Long-term memory, carries information across time steps
Stacked Layers¶
Multiple LSTM layers create a deeper network:
Layer 3: Higher-level patterns
↑
Layer 2: Intermediate features
↑
Layer 1: Low-level features
↑
Input embeddings
LSTM vs Transformer¶
| Aspect | LSTM | Transformer |
|---|---|---|
| Processing | Sequential | Parallel |
| Long dependencies | Difficult; easier with attention=True |
Easy (attention) |
| Training speed | Slower | Faster |
| Memory efficiency | O(n) | O(n²) |
| Simplicity | Simpler | More complex |
| Parameters | Fewer | More |
When to Use LSTM¶
- ✅ Small datasets (< 50K examples)
- ✅ Limited GPU memory
- ✅ Learning/educational purposes
- ✅ Real-time inference on CPU
When to Use Transformer¶
- ✅ Large datasets
- ✅ Best translation quality
- ✅ GPU available for training
- ✅ Long sequences
Model Variants¶
Small¶
model = SimpleSeq2SeqLSTM(
src_vocab_size, tgt_vocab_size,
emb_dim=128,
hidden_dim=256,
num_layers=1,
)
# ~3M params
Medium¶
model = SimpleSeq2SeqLSTM(
src_vocab_size, tgt_vocab_size,
emb_dim=256,
hidden_dim=512,
num_layers=2,
)
# ~15M params
Large¶
model = SimpleSeq2SeqLSTM(
src_vocab_size, tgt_vocab_size,
emb_dim=512,
hidden_dim=1024,
num_layers=4,
)
# ~50M params
Limitations¶
- Information bottleneck (default): with
attention=False, all source information must fit in the final hidden state - Sequential processing: can't parallelize across time steps
Adding Attention
Pass attention=True to let the decoder look back at every encoder output
instead of relying on the final hidden state alone. Both classic scorers are
implemented — see Attention. Attention is off by default,
so the bottlenecked model remains the baseline you compare against.