Mục lục
- Mục tiêu bài học
- LSTM là gì
- Ý tưởng cốt lõi — cell state \( c_t \)
- Hidden state vs cell state
- Forget gate \( f_t \)
- Input gate \( i_t \) và candidate \( \tilde{c}_t \)
- Update cell state \( c_t \)
- Output gate \( o_t \) và \( h_t \)
- Tổng hợp 6 công thức của LSTM cell
- Vì sao LSTM giải vanishing gradient
- Đếm parameter
- PyTorch
nn.LSTMAPI - Cặp state \( (h_n, c_n) \)
- Stacked LSTM
- Bidirectional LSTM
- Forget gate bias init = 1
- Variants của LSTM
- LSTM vs Vanilla RNN
- LSTM vs GRU (preview B38)
- Use case và LSTM trong Transformer era
- Train LSTM char-level + đối chiếu gradient
- Bài tập
- Tóm tắt
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Phân biệt được hidden state \( h_t \) và cell state \( c_t \) trong LSTM.
- Viết được 6 công thức của một LSTM cell (3 gate + candidate + cell update + hidden output).
- Giải thích vì sao "shortcut path" của \( c_t \) qua phép cộng giúp gradient không vanishing.
- Đếm chính xác số parameter của một
nn.LSTMbất kỳ và biết nó gấp ~4 lần Vanilla RNN cùng size. - Dùng được
nn.LSTMvới cặp state \( (h_n, c_n) \), stack nhiều layer, bidirectional. - Biết thủ thuật forget bias init = 1 và vì sao nó quan trọng.
- Hiểu vị trí của LSTM giữa Vanilla RNN (B35), GRU (B38) và Transformer.
Bài này nối tiếp Bài 36 — Vanishing Gradient (vấn đề mà LSTM được thiết kế để giải) và mở đường cho Bài 38 — GRU (biến thể đơn giản hơn).
LSTM là gì
LSTM (Long Short-Term Memory) do Sepp Hochreiter và Jürgen Schmidhuber đề xuất trong bài báo cùng tên năm 1997. Đây là kiến trúc đầu tiên train được trên dependency cách xa hàng trăm step — vốn là điểm chết của Vanilla RNN do vanishing gradient (B36).
Bản gốc 1997 chỉ có 2 cổng (input và output) cộng với một memory cell. Forget gate được Felix Gers, Schmidhuber và Cummins thêm vào năm 2000 ("Learning to Forget") và trở thành phiên bản chuẩn dùng tới hôm nay. Khi nói "LSTM" trong code hiện đại (PyTorch, TensorFlow), mặc định là bản có forget gate.
Đặc trưng thiết kế:
- Hai loại state truyền qua time: cell state \( c_t \) (memory dài hạn) và hidden state \( h_t \) (memory ngắn hạn, đồng thời là output).
- Ba cổng (sigmoid gate) điều khiển luồng thông tin: forget, input, output.
- Cell state \( c_t \) cập nhật bằng cộng chứ không phải tích — tạo nên "memory highway" cho gradient.
Ý tưởng cốt lõi — cell state \( c_t \)
Vanilla RNN có duy nhất một state truyền qua time là \( h_t \), được tính bằng tanh(...) của hỗn hợp tuyến tính. Khi unroll qua \( T \) step, gradient phải nhân tích Jacobian dài, dẫn tới vanishing (B36).
LSTM tách memory thành hai dòng song song:
- Cell state \( c_t \in \mathbb{R}^{d_h} \): "băng tải" memory chạy thẳng qua các step, chỉ bị chỉnh bởi gate (cộng / nhân element-wise) chứ không qua matmul đầy đủ.
- Hidden state \( h_t \in \mathbb{R}^{d_h} \): bản "đọc ra" của cell state qua output gate, đóng vai trò output tại step \( t \) và input recurrent cho step \( t+1 \).
Hình dung: \( c_t \) như một sợi dây kéo dài suốt chuỗi, các cổng quyết định "xoá khúc nào", "thêm khúc nào". Vì update là cộng (\( c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \)) chứ không phải nhân ma trận lặp lại, gradient \( \partial c_T / \partial c_t \) không bị nén như RNN.
Hidden state vs cell state
| Cell state \( c_t \) | Hidden state \( h_t \) | |
|---|---|---|
| Vai trò | Memory dài hạn, "băng tải" nội bộ. | Output tại step \( t \) + input recurrent cho step kế. |
| Truyền ra ngoài? | Không (nội bộ cell). | Có — head trên (classifier / decoder) chỉ thấy \( h_t \). |
| Range giá trị | Không bị bound (chỉ cộng / nhân gate). | \( \tanh(c_t) \cdot o_t \in (-1, 1) \). |
| Cập nhật | Cộng có gate (linear path). | Qua nonlinearity tanh và gate. |
Khi resume training (chia chunk, TBPTT — B35), phải lưu cả cặp \( (h_t, c_t) \), không chỉ \( h_t \). Đây là điểm khác biệt API quan trọng so với nn.RNN.
Forget gate \( f_t \)
Forget gate quyết định giữ lại bao nhiêu cell state cũ \( c_{t-1} \):
\[ f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \]
Trong đó:
- \( [h_{t-1}, x_t] \in \mathbb{R}^{d_h + d_x} \): concat hidden cũ với input hiện tại.
- \( W_f \in \mathbb{R}^{d_h \times (d_h + d_x)} \): weight matrix của forget gate.
- \( b_f \in \mathbb{R}^{d_h} \): bias.
- \( \sigma \) là sigmoid, đảm bảo \( f_t \in (0, 1)^{d_h} \).
Diễn giải từng phần tử \( f_{t, i} \):
- \( f_{t, i} \approx 1 \): giữ nguyên memory chiều \( i \).
- \( f_{t, i} \approx 0 \): xoá memory chiều \( i \).
- \( f_{t, i} = 0.5 \): giữ lại một nửa.
Vì forget gate được tính từ \( (h_{t-1}, x_t) \), nó học "data-dependent" — phụ thuộc ngữ cảnh hiện tại quyết định bỏ gì. Vd trong ngôn ngữ, gặp dấu chấm câu có thể là tín hiệu reset memory.
Input gate \( i_t \) và candidate \( \tilde{c}_t \)
Cập nhật cell state mới chia thành 2 nhánh:
Input gate — bao nhiêu thông tin mới được đưa vào (sigmoid gate):
\[ i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \]
Candidate cell state — "đề xuất" memory mới (tanh, range \( (-1, 1) \)):
\[ \tilde{c}_t = \tanh(W_c \cdot [h_{t-1}, x_t] + b_c) \]
Trong đó \( W_i, W_c \in \mathbb{R}^{d_h \times (d_h + d_x)} \), \( b_i, b_c \in \mathbb{R}^{d_h} \).
Ý nghĩa tách 2 nhánh:
- \( \tilde{c}_t \) đề xuất nội dung (có thể âm hoặc dương).
- \( i_t \) chấp nhận bao nhiêu cho từng chiều.
- Tích element-wise \( i_t \odot \tilde{c}_t \) là phần memory mới thực tế được thêm vào.
So sánh với Vanilla RNN: ở RNN, tanh(W_xh x_t + W_hh h_{t-1} + b) vừa là đề xuất vừa là update — không có cơ chế chọn lọc. LSTM tách ra để có thể "muốn thêm bao nhiêu, thêm bấy nhiêu".
Update cell state \( c_t \)
Đây là phương trình cốt lõi của LSTM — phép cộng có gate:
\[ c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \]
Với \( \odot \) là element-wise multiply (Hadamard product). Hai số hạng:
- Kept memory: \( f_t \odot c_{t-1} \) — phần memory cũ được giữ lại.
- New memory: \( i_t \odot \tilde{c}_t \) — phần memory mới được thêm vào.
Quan sát quan trọng: không có matmul ở đây. Cả 4 toán hạng \( f_t, c_{t-1}, i_t, \tilde{c}_t \) đều shape \( d_h \), kết quả \( c_t \) cũng shape \( d_h \). Chính vì update tuyến tính có gate (không qua matrix recurrent), gradient \( \partial c_t / \partial c_{t-1} = \mathrm{diag}(f_t) \) — không phụ thuộc tích Jacobian phức tạp như RNN.
Một số tham chiếu (Greff và cộng sự, 2017) chỉ ra: nếu cố tình cố định \( f_t = 1 \) (không forget), gradient \( \partial c_T / \partial c_0 \) đúng bằng 1 — gradient pass through hoàn toàn không suy giảm. Đó chính là "memory highway" được nhắc tới ở mục 3.
Output gate \( o_t \) và \( h_t \)
Output gate quyết định expose bao nhiêu phần của cell state ra ngoài làm hidden state:
\[ o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \]
Hidden state:
\[ h_t = o_t \odot \tanh(c_t) \]
Hai bước:
tanh(c_t): squash cell state về \( (-1, 1) \) — vì \( c_t \) tự do, không bound. Đây là chỗ duy nhất nonlinearity tác động lên cell state khi xuất ra.- Nhân với \( o_t \): chọn chiều nào "đáng xuất", chiều nào nên ẩn.
Ý nghĩa thiết kế: cell state có thể chứa nhiều thông tin "ngầm" (long-term memory), nhưng tại mỗi step chỉ một phần cần thiết được expose ra cho task hiện tại. Vd memory đang nhớ chủ ngữ số nhiều / số ít, chỉ cần expose ra khi tới động từ.
Tổng hợp 6 công thức của LSTM cell
Gom hết một chỗ để dễ nhớ:
\[ \begin{aligned} f_t &= \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \\ i_t &= \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \\ o_t &= \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \\ \tilde{c}_t &= \tanh(W_c \cdot [h_{t-1}, x_t] + b_c) \\ c_t &= f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \\ h_t &= o_t \odot \tanh(c_t) \end{aligned} \]
Thứ tự tính: \( f_t, i_t, o_t, \tilde{c}_t \) độc lập (đều từ \( h_{t-1}, x_t \)) — có thể tính song song. Sau đó tính \( c_t \) (cần \( c_{t-1} \)), rồi \( h_t \) (cần \( c_t \)).
Tip cài đặt: 4 phép tính \( W_f, W_i, W_o, W_c \) cùng input \( [h_{t-1}, x_t] \). Trong PyTorch, gộp chúng thành một matrix \( W \in \mathbb{R}^{4 d_h \times (d_h + d_x)} \), tính một matmul rồi split thành 4 phần để tận dụng GEMM lớn. cuDNN làm chính xác vậy.
Vì sao LSTM giải vanishing gradient
Quay lại bài toán vanishing (B36): gradient \( \partial L / \partial h_t \) ở Vanilla RNN phải nhân tích Jacobian \( \prod_k \mathrm{diag}(\tanh'(z_k)) \cdot W_{hh} \), mỗi factor magnitude \( < 1 \) → tích \( \to 0 \) khi chuỗi dài.
Ở LSTM, gradient đi theo hai con đường:
- Đường \( h_t \to h_{t-1} \) (qua các gate) — vẫn có thể vanishing.
- Đường \( c_t \to c_{t-1} \): chỉ qua phép cộng có gate, đạo hàm là \( \mathrm{diag}(f_t) \). Khi \( f_t \approx 1 \), gradient pass through gần như không suy giảm.
Cụ thể, đạo hàm dọc theo cell state:
\[ \frac{\partial c_T}{\partial c_t} = \prod_{k=t+1}^{T} \mathrm{diag}(f_k) \]
Nếu \( f_k \) gần 1 (gate "open"), tích này gần identity → gradient sống. Nếu \( f_k \) gần 0 (model học forget), gradient cũng tự tắt — đây là tính năng, không phải bug.
Tóm gọn: LSTM không loại bỏ hoàn toàn vanishing (vẫn có thể xảy ra), nhưng cho model quyền điều khiển — gradient flow trở thành parameter học được qua forget gate, thay vì cố định bởi spectral radius của \( W_{hh} \).
Đếm parameter
Mỗi cổng (\( f, i, o \)) và candidate \( \tilde{c} \) đều có một bộ \( (W, b) \) với \( W \in \mathbb{R}^{d_h \times (d_h + d_x)} \), \( b \in \mathbb{R}^{d_h} \). Tổng 4 bộ:
\[ P_{\text{LSTM cell}} = 4 \cdot \big( (d_h + d_x) \cdot d_h + d_h \big) = 4 d_h (d_h + d_x + 1) \]
Cách viết khác (tách bias): \( 4 d_h (d_h + d_x) + 4 d_h \).
So với Vanilla RNN cùng size (\( d_h (d_h + d_x) + d_h \)), LSTM có ~4 lần param. Đây là chi phí cho 3 gate + candidate.
Ví dụ kiểm chứng: \( d_x = 10, d_h = 20 \).
\( P = 4 \cdot 20 \cdot (20 + 10 + 1) = 4 \cdot 20 \cdot 31 = 2480 \).
import torch.nn as nn
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=1)
print(sum(p.numel() for p in lstm.parameters())) # 2560
Chú ý 2560 ≠ 2480: PyTorch lưu hai bias riêng (\( b_{ih}, b_{hh} \)) cho mỗi gate (cộng dồn về math thì tương đương 1 bias), nên có thêm \( 4 \cdot 20 = 80 \) param. Tổng \( 2480 + 80 = 2560 \). Convention này kế thừa từ cuDNN.
Quy luật chung: \( P_{\text{nn.LSTM (1 layer)}} = 4 d_h (d_h + d_x) + 8 d_h \). Param scale theo \( d_h^2 \) — chính cùng tính chất với Vanilla RNN, chỉ nhân thêm hệ số 4.
PyTorch nn.LSTM API
API gần như giống nn.RNN, khác ở chỗ trả về cặp state cuối:
import torch
import torch.nn as nn
lstm = nn.LSTM(
input_size=10,
hidden_size=20,
num_layers=1,
bias=True,
batch_first=True, # khuyên dùng True
dropout=0.0, # áp giữa các layer khi num_layers > 1
bidirectional=False,
)
x = torch.randn(32, 100, 10) # (batch=32, T=100, input=10)
output, (h_n, c_n) = lstm(x)
print(output.shape) # torch.Size([32, 100, 20]) — hidden mọi step
print(h_n.shape) # torch.Size([1, 32, 20]) — hidden state cuối
print(c_n.shape) # torch.Size([1, 32, 20]) — cell state cuối
Ba thứ trả về:
output: \( h_t \) tại mọi step ở layer trên cùng — shape \( (B, T, d_h) \) khibatch_first=True.h_n: hidden state ở step cuối, cho mọi layer.c_n: cell state ở step cuối, cho mọi layer.
Lưu ý: output chỉ chứa \( h_t \), không chứa \( c_t \). Nếu cần truy cập cell state ở giữa chuỗi, phải tự viết loop hoặc dùng nn.LSTMCell (1 step).
Cặp state \( (h_n, c_n) \)
Khi truyền state qua chunk (TBPTT) hoặc resume inference, phải pass cả hai:
h_0 = torch.zeros(1, B, d_h)
c_0 = torch.zeros(1, B, d_h)
output, (h_n, c_n) = lstm(x, (h_0, c_0))
# Chunk kế tiếp: detach để cắt graph nhưng giữ giá trị
h_n = h_n.detach()
c_n = c_n.detach()
output2, (h_n, c_n) = lstm(x_next, (h_n, c_n))
Quên \( c_n \) là lỗi rất thường gặp khi chuyển từ nn.RNN sang nn.LSTM: model "reset memory" mỗi chunk vì \( c_0 \) lại bằng 0, mất hết lợi thế của cell state.
Encoder–decoder (seq2seq) cũng phải truyền cả \( (h_n, c_n) \) từ encoder sang decoder, không chỉ \( h_n \).
Khi inference autoregressive (generate 1 token / step), giữ tuple state qua mỗi bước:
state = None
for step in range(max_len):
out, state = lstm(token, state) # state = (h, c)
token = sample(out)
Stacked LSTM
Y hệt RNN (B35 mục 15) — chỉ thay nn.RNN bằng nn.LSTM:
lstm = nn.LSTM(input_size=64, hidden_size=128, num_layers=3,
batch_first=True, dropout=0.2)
x = torch.randn(32, 100, 64)
output, (h_n, c_n) = lstm(x)
print(output.shape) # (32, 100, 128) — hidden layer trên cùng
print(h_n.shape) # (3, 32, 128) — h cuối của 3 layer
print(c_n.shape) # (3, 32, 128) — c cuối của 3 layer
Hidden state đi 2 chiều: theo time (qua \( c_t, h_t \) trong cùng layer) và theo depth (output của layer dưới làm input layer trên). dropout chỉ áp giữa các layer (không áp dọc time).
Thực nghiệm: 2–3 layer là điểm thường thấy nhất trong các baseline NLP cổ điển (vd ELMo dùng 2 layer biLSTM). Sâu hơn ít cải thiện, train chậm hơn nhiều.
Bidirectional LSTM
BiLSTM chạy 2 LSTM độc lập, forward \( 1 \to T \) và backward \( T \to 1 \), concat hidden:
\[ h_t = [\overrightarrow{h_t} \, ; \, \overleftarrow{h_t}] \in \mathbb{R}^{2 d_h} \]
bilstm = nn.LSTM(input_size=64, hidden_size=128, num_layers=1,
batch_first=True, bidirectional=True)
x = torch.randn(32, 50, 64)
output, (h_n, c_n) = bilstm(x)
print(output.shape) # (32, 50, 256) — concat 2 chiều
print(h_n.shape) # (2, 32, 128) — (num_directions, B, d_h)
print(c_n.shape) # (2, 32, 128)
BiLSTM là baseline phổ biến cho NLP cổ điển trước Transformer:
- Sentiment analysis (cần ngữ cảnh cả hai chiều).
- NER (Named Entity Recognition).
- POS tagging, dependency parsing.
- Encoder trong seq2seq dịch máy (decoder vẫn unidirectional).
Hạn chế: không dùng được khi streaming — phải có cả chuỗi trước khi forward. Cho autoregressive generation (ngôn ngữ, audio), chỉ unidirectional dùng được.
Forget gate bias init = 1
Một thủ thuật nhỏ nhưng quan trọng (Jozefowicz, Zaremba, Sutskever 2015 — "An Empirical Exploration of Recurrent Network Architectures"):
Mặc định PyTorch init mọi bias \( \approx 0 \). Khi đó forget gate ban đầu \( f_t = \sigma(0) = 0.5 \) — memory bị nhân với 0.5 mỗi step → sau 10 step còn \( 0.5^{10} \approx 10^{-3} \) → cell state biến mất trước khi model học được gì.
Giải pháp: init bias forget gate \( b_f = 1 \) (hoặc 2). Ban đầu \( f_t = \sigma(1) \approx 0.73 \) (hoặc \( \sigma(2) \approx 0.88 \)) → memory được giữ nhiều hơn trong giai đoạn đầu, model có thời gian học "khi nào nên forget".
Trong PyTorch, nn.LSTM không tự làm — phải init manual:
def init_forget_bias(lstm, value=1.0):
for name, param in lstm.named_parameters():
if "bias_ih" in name or "bias_hh" in name:
# PyTorch layout 4 gate: [i, f, g, c] với mỗi gate d_h
n = param.size(0)
start, end = n // 4, n // 2 # vùng forget gate
param.data[start:end].fill_(value)
lstm = nn.LSTM(10, 20, num_layers=1)
init_forget_bias(lstm, value=1.0)
Chú ý layout PyTorch: 4 gate sắp xếp theo thứ tự [input, forget, cell, output], tức slice thứ hai (\( n/4 \to n/2 \)) là forget. Cộng cả \( b_{ih} \) lẫn \( b_{hh} \) để bias hiệu dụng \( = 1 + 1 = 2 \), hoặc đặt một bên = 1, bên còn lại = 0.
Một số tài liệu khuyến nghị value = 2 thay vì 1 cho chuỗi rất dài. Thử nghiệm trên task cụ thể.
Variants của LSTM
LSTM "chuẩn" (vanilla LSTM với forget gate) là phiên bản dùng phổ biến. Một số biến thể đáng biết:
- Peephole LSTM (Gers & Schmidhuber 2000): các gate cũng nhìn cell state, ví dụ \( f_t = \sigma(W_f \cdot [h_{t-1}, x_t, c_{t-1}] + b_f) \). Cải thiện trên một số task đếm thời gian (counting / timing), nhưng overhead tính toán và thực nghiệm Greff và cộng sự (2017) cho thấy peephole không cải thiện đáng kể trên hầu hết task chung.
- Coupled LSTM: ràng buộc \( f_t = 1 - i_t \) — "nếu thêm mới thì phải forget cùng lượng". Giảm số gate xuống 2, tiết kiệm tham số, mở đường cho GRU (B38).
- LSTM với layer norm (Ba, Kiros, Hinton 2016): áp layer normalization lên các pre-activation của gate. Train ổn định hơn, đặc biệt với batch nhỏ.
torch.nn.LSTMkhông có sẵn — phải tự cài hoặc dùngtorch.nn.LSTMCell+ layer norm. - ConvLSTM (Shi và cộng sự, 2015): thay matmul bằng convolution để xử lý chuỗi ảnh (video, weather forecast).
- GRU (Cho và cộng sự, 2014 — B38): rút xuống còn 2 gate, không tách cell và hidden state.
Khảo sát Greff và cộng sự (2017) — "LSTM: A Search Space Odyssey" — train 8 variant trên 3 task, kết luận: vanilla LSTM với forget gate là baseline đáng tin, các variant phức tạp hơn không cải thiện rõ ràng. Forget gate và output activation tanh là 2 thành phần quan trọng nhất.
LSTM vs Vanilla RNN
| Vanilla RNN (B35) | LSTM | |
|---|---|---|
| State qua time | 1 ( \( h_t \) ) | 2 ( \( h_t, c_t \) ) |
| Số gate | 0 | 3 (forget, input, output) |
| Param (cùng \( d_x, d_h \)) | \( d_h (d_h + d_x) + d_h \) | \( \sim 4 \times \) Vanilla RNN |
| Vanishing gradient | Mạnh; \( \approx 10\text{–}20 \) step | Giảm rõ; \( \approx 100\text{–}500 \) step train được |
| Tốc độ train | Nhanh hơn / step | Chậm hơn ~3–4x do nhiều matmul |
| Use case | Toy, chuỗi ngắn, baseline | NLP cổ điển, time series, speech |
Trade-off: trả nhiều param + compute để đổi long-range memory. Trên chuỗi ngắn (\( T < 20 \)), Vanilla RNN nhiều khi đủ; trên chuỗi dài hoặc khi long-range dependency quan trọng, LSTM gần như luôn thắng.
LSTM vs GRU (preview B38)
GRU (Cho và cộng sự, 2014) là một dạng simplified của LSTM:
- 2 gate (reset, update) thay vì 3.
- Không tách \( c_t \) và \( h_t \) — chỉ một state truyền qua time.
- Param ít hơn ~25% so với LSTM cùng \( d_h \).
Empirical comparison (Chung và cộng sự, 2014; Jozefowicz và cộng sự, 2015): trên đa số task, GRU và LSTM cho kết quả gần tương đương. Debate "LSTM hay GRU tốt hơn" thực ra phụ thuộc dataset và hyperparameter — không có người thắng tuyệt đối.
Quy tắc ngón cái:
- Mặc định thử GRU trước nếu ưu tiên tốc độ / ít param.
- LSTM khi cần điều khiển memory chi tiết, hoặc khi paper baseline dùng LSTM (để so sánh fair).
- Chuỗi rất dài (\( T > 500 \)): cả hai đều có thể vẫn vanishing — cân nhắc Transformer.
Chi tiết GRU và công thức ở B38.
Use case và LSTM trong Transformer era
Trước Transformer (Vaswani và cộng sự, 2017), LSTM là kiến trúc chuẩn cho hầu hết task chuỗi: NMT (Sutskever, Vinyals, Le 2014), language modeling, speech recognition (Graves và cộng sự), sentiment, NER, parsing.
Sau Transformer, LSTM không còn dominant trong NLP scale lớn (BERT, GPT đều là Transformer), nhưng vẫn còn nhiều use case hợp lý:
- Streaming / on-device: ASR real-time, keyboard prediction, sensor fusion — cần xử lý từng token đến, latency thấp. Transformer attention quadratic theo \( T \), không phù hợp streaming.
- Time series forecasting: nhiều benchmark (M4, M5) cho thấy LSTM/GRU cạnh tranh hoặc thắng Transformer khi data hạn chế (vài nghìn point).
- Edge / low-resource: LSTM 1–2 layer chạy được trên MCU; Transformer khó fit.
- Audio frame-level processing: WaveNet, RNN-T (RNN-Transducer) cho ASR vẫn cạnh tranh.
- Reinforcement learning agent có memory: DRL với recurrent policy thường dùng LSTM (vd Impala, R2D2).
- Hybrid models: một số kiến trúc gần đây (Mamba, RWKV) lấy lại ý tưởng "state recurrent" để xử lý chuỗi dài hiệu quả hơn Transformer attention.
Train LSTM char-level + đối chiếu gradient
Refactor toy ở B35 mục 18, đổi nn.RNN sang nn.LSTM, lưu ý cặp state:
import torch
import torch.nn as nn
text = "hello world hello world hello world "
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
V = len(chars)
data = torch.tensor([stoi[c] for c in text])
seq_len = 16
def get_batch(B=32):
idx = torch.randint(0, len(data) - seq_len - 1, (B,))
x = torch.stack([data[i:i+seq_len] for i in idx])
y = torch.stack([data[i+1:i+seq_len+1] for i in idx])
return x, y
class CharLSTM(nn.Module):
def __init__(self, V, d_h=64):
super().__init__()
self.embed = nn.Embedding(V, d_h)
self.lstm = nn.LSTM(d_h, d_h, batch_first=True)
self.head = nn.Linear(d_h, V)
def forward(self, x, state=None):
e = self.embed(x)
out, state = self.lstm(e, state)
return self.head(out), state
model = CharLSTM(V)
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
for step in range(2000):
x, y = get_batch(32)
logits, _ = model(x)
loss = loss_fn(logits.reshape(-1, V), y.reshape(-1))
opt.zero_grad(); loss.backward(); opt.step()
if step % 200 == 0:
print(f"step {step:4d} loss = {loss.item():.4f}")
So với phiên bản RNN: param nhiều hơn ~4 lần, loss tụt nhanh hơn trên text dài. Trên dataset thật (vd tiny Shakespeare 1MB), khoảng cách rõ rệt — LSTM học được long-range pattern (đối thoại giữa nhân vật, prefix/suffix) mà Vanilla RNN không bắt được.
So sánh gradient flow nhanh: lấy norm gradient \( W_{hh} \) (RNN) so với norm \( W_c \) (candidate, LSTM) ở step xa nhất:
# Sau loss.backward()
for name, p in model.named_parameters():
if p.grad is not None:
print(f"{name:40s} grad_norm = {p.grad.norm().item():.4e}")
Trên chuỗi \( T = 200 \), gradient của LSTM giữ magnitude lớn hơn nhiều cấp so với Vanilla RNN cùng setup — bằng chứng quan sát được của "memory highway".
Bài tập
- Tính bằng tay tổng param của
nn.LSTM(input_size=10, hidden_size=20, num_layers=1)theo công thức mục 11, rồi verify vớisum(p.numel() for p in lstm.parameters()). Giải thích chênh lệch giữa công thức "1 bias" (2480) và PyTorch (2560). - Tính bằng tay tổng param của
nn.LSTM(input_size=64, hidden_size=128, num_layers=2, bidirectional=True). Layer 2 nhận input \( 2 \cdot 128 = 256 \) vì bi layer 1 concat 2 chiều. - Implement LSTM cell từ đầu bằng
nn.Linear(gộp 4 gate thành 1 matmul \( W \in \mathbb{R}^{4 d_h \times (d_h + d_x)} \) rồi split), so output vớinn.LSTMCelltrên cùng init + input — sai lệch numeric phải \( \le 10^{-5} \). - Train LSTM trên sequence độ dài \( T = 200 \) (vd Adding Problem: cộng 2 phần tử được đánh dấu trong chuỗi noise dài) và so với Vanilla RNN cùng \( d_h \). Vẽ loss curve — LSTM hội tụ, RNN không.
- Apply forget bias init = 1 (mục 16) lên một LSTM, train trên Adding Problem \( T = 500 \). So sánh thời gian hội tụ với forget bias init = 0 (mặc định).
- Build BiLSTM cho sentiment toy: 2 lớp positive / negative trên 1000 câu giả lập (vd "good" / "bad" + filler). Dùng \( h_n \) (concat 2 chiều) đưa qua
nn.Linear(2 d_h, 2). Đạt accuracy > 90%. - Chứng minh: nếu cố định \( f_k = 1, i_k = 0 \) cho mọi \( k \), thì \( \partial c_T / \partial c_0 = I \) (identity). Đây là kịch bản "memory highway" hoàn hảo — gradient pass through không suy giảm.
- Áp
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)vào training. Quan sát loss có ổn định hơn không trên sequence dài \( T = 1000 \)?
Gợi ý đáp án ngắn
- Công thức: \( 4 \cdot 20 \cdot 31 = 2480 \). PyTorch dùng 2 bias (\( b_{ih}, b_{hh} \)) cho mỗi gate → thêm \( 4 \cdot 20 = 80 \) → 2560.
- Layer 1 (bi): \( 2 \cdot 4 \cdot 128 \cdot (128 + 64 + 1) = 2 \cdot 4 \cdot 128 \cdot 193 = 197 632 \). Layer 2 (bi, input = 256): \( 2 \cdot 4 \cdot 128 \cdot (128 + 256 + 1) = 394 240 \). PyTorch thêm 2 bias mỗi gate, tính tương tự.
- Phải cùng init:
lstm_cell.weight_ih.data.copy_(W[:, d_h:]),lstm_cell.weight_hh.data.copy_(W[:, :d_h]), bias copy tương tự. Sai lệch nhỏ ~\( 10^{-6} \) là pass. - Adding Problem: input là chuỗi 2 chiều (số random + marker 0/1), target là tổng 2 số được mark. RNN baseline accuracy gần random; LSTM về gần 1.0.
- Bias init = 1 thường hội tụ nhanh hơn 2–3 lần trên \( T = 500 \) — vì memory được giữ tốt từ early steps.
- Kiến trúc:
Embedding → BiLSTM → mean-pool or h_n → Linear(2 d_h, 2). LossCrossEntropyLoss. - Khi \( f_k = 1, i_k = 0 \): \( c_T = c_0 \), nên \( \partial c_T / \partial c_0 = I \). Đây là chứng minh mục 10 ở kịch bản extremal.
- Clip grad giảm xác suất NaN khi spike gradient. LSTM ít exploding hơn RNN nhưng vẫn có thể spike trên chuỗi rất dài hoặc data ngoại lệ.
Tóm tắt
- LSTM (Hochreiter & Schmidhuber 1997; forget gate Gers 2000) là kiến trúc đầu tiên train được trên dependency cách xa hàng trăm step.
- Hai state truyền qua time: cell state \( c_t \) (memory dài hạn, băng tải nội bộ) và hidden state \( h_t \) (output + recurrent ngắn hạn).
- 3 cổng sigmoid: forget \( f_t \), input \( i_t \), output \( o_t \). Mỗi cổng có một bộ \( (W, b) \) ánh xạ từ \( [h_{t-1}, x_t] \).
- Candidate \( \tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c) \) là đề xuất memory mới.
- Update: \( c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \) — cộng có gate, không matmul.
- Output: \( h_t = o_t \odot \tanh(c_t) \).
- Vì sao giải vanishing: \( \partial c_T / \partial c_t = \prod \mathrm{diag}(f_k) \) — khi \( f_k \approx 1 \), gradient pass through.
- Param: \( 4 d_h (d_h + d_x + 1) \) — gấp ~4 lần Vanilla RNN cùng size.
- PyTorch
nn.LSTMtrả vềoutput, (h_n, c_n)— phải truyền cặp state khi resume. - Forget bias init = 1 (Jozefowicz và cộng sự 2015): thủ thuật giúp memory không bị xoá quá nhanh ở early training.
- Variants: peephole, coupled, layer norm, ConvLSTM, GRU (B38). Khảo sát Greff và cộng sự 2017 cho thấy vanilla LSTM là baseline đáng tin.
- LSTM dominant cho NLP / time series trước Transformer (2017); sau đó vẫn dùng cho streaming, edge, time series ngắn, RL agent có memory.
- Hochreiter & Schmidhuber (1997) - Long Short-Term Memory
- Gers, Schmidhuber, Cummins (2000) - Learning to Forget: Continual Prediction with LSTM
- Gers & Schmidhuber (2002) - Learning Precise Timing with LSTM Recurrent Networks (peephole)
- Cho, van Merrienboer và cộng sự (2014) - Learning Phrase Representations using RNN Encoder–Decoder (GRU)
- Chung, Gulcehre, Cho, Bengio (2014) - Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling
- Jozefowicz, Zaremba, Sutskever (2015) - An Empirical Exploration of Recurrent Network Architectures (ICML)
- Greff, Srivastava, Koutnik, Steunebrink, Schmidhuber (2017) - LSTM: A Search Space Odyssey (TNNLS)
- Ba, Kiros, Hinton (2016) - Layer Normalization
- Shi và cộng sự (2015) - Convolutional LSTM Network (ConvLSTM)
- PyTorch Docs - torch.nn.LSTM
- PyTorch Docs - torch.nn.LSTMCell
- Christopher Olah - Understanding LSTM Networks
- Goodfellow, Bengio, Courville - Deep Learning (Chương 10: Sequence Modeling)
- Dive into Deep Learning - Long Short-Term Memory (LSTM)
