Kiến trúc mạng học sâu: từ nền tảng đến LLM·Chương 4 — LSTM: bộ nhớ dài hạn có cổng

Chế độ xem

Các biến thể LSTM và cách dùng trong thực tế

Mục tiêu

Sau bài này, bạn cần:

  • phân biệt LSTM một chiều, hai chiều và nhiều tầng;
  • hiểu ý tưởng của peephole connection và LSTM có projection;
  • biết các quyết định quan trọng khi dùng LSTM cho phân loại hoặc gán nhãn chuỗi;
  • đọc đúng kích thước output, h_n, c_n trong PyTorch;
  • biết khi nào LSTM là lựa chọn hợp lý và khi nào nên cân nhắc GRU hoặc Transformer.

Bài này không nhằm liệt kê mọi biến thể LSTM từng được công bố. Mục tiêu là nắm các biến thể có ý nghĩa khái niệm và thường gặp trong tài liệu/hệ thống thực tế.

1. “Standard LSTM” hiện nay là biến thể đã tiến hóa theo thời gian

LSTM hiện đại thường gồm:

  • cell state;
  • input gate;
  • forget gate;
  • output gate;
  • candidate update.

Cấu trúc này là kết quả của nhiều bước phát triển sau paper 1997. Vì vậy khi đọc paper cũ, đừng giả sử mọi công thức LSTM đều giống API hiện đại.

Nghiên cứu quy mô lớn của Greff et al. trên nhiều biến thể cho thấy không có biến thể được khảo sát nào luôn vượt rõ standard LSTM; forget gate và output activation là những thành phần đặc biệt quan trọng trong các thí nghiệm của họ.

2. LSTM một chiều

LSTM một chiều đọc chuỗi theo một hướng:

Tại bước t, nó chỉ dùng quá khứ đến thời điểm đó.

Phù hợp khi:

  • dự đoán online;
  • dự báo chuỗi thời gian;
  • sinh chuỗi theo thứ tự;
  • dữ liệu tương lai không có sẵn lúc suy luận.

3. Bidirectional LSTM

Nếu toàn bộ chuỗi đã có trước khi dự đoán, ta có thể đọc theo cả hai hướng.

Mỗi vị trí nhận hai biểu diễn:

ht→,ht←\overrightarrow{h_t},\qquad \overleftarrow{h_t}

thường được ghép:

ht=[ht→;ht←]h_t=[\overrightarrow{h_t};\overleftarrow{h_t}]

Khi hữu ích

  • gán nhãn chuỗi khi cả câu đã có sẵn;
  • xử lý âm thanh offline;
  • nhận dạng chuỗi mà quyết định tại vị trí hiện tại cần cả ngữ cảnh trước và sau.

Graves và Schmidhuber (2005) cho thấy Bidirectional LSTM hoạt động tốt trong phân loại phoneme theo khung trên TIMIT, nhấn mạnh giá trị của ngữ cảnh hai phía trong nhiệm vụ offline.

Khi không dùng

Nếu hệ thống cần dự đoán ngay tại thời điểm hiện tại, nhánh đọc ngược sẽ sử dụng dữ liệu tương lai chưa tồn tại.

4. Stacked / Deep LSTM

Một LSTM có thể được xếp nhiều tầng.

Ở mỗi thời điểm, đầu ra của tầng dưới trở thành đầu vào của tầng trên.

Mục tiêu:

  • tầng dưới học đặc trưng tuần tự mức thấp;
  • tầng trên kết hợp chúng thành biểu diễn phức tạp hơn.

Deep LSTM từng đạt kết quả mạnh trong nhận dạng tiếng nói; Graves, Mohamed và Hinton (2013) là một ví dụ kinh điển.

Nhiều tầng không luôn tốt hơn

Tăng num_layers làm:

  • tăng tham số;
  • tăng thời gian huấn luyện;
  • tăng nguy cơ overfitting;
  • làm tối ưu khó hơn.

Với tập nhỏ, một LSTM 1-2 tầng thường là baseline hợp lý trước khi tăng độ sâu.

5. Dropout trong LSTM nhiều tầng

Một cách regularization phổ biến là dropout giữa các tầng hồi tiếp.

Trong API như nn.LSTM, tham số dropout thường được áp dụng giữa các recurrent layer, không có nghĩa là mọi kết nối hồi tiếp bên trong cell đều bị dropout theo cùng cách.

Vì vậy phải đọc đúng định nghĩa của thư viện thay vì chỉ nhìn tên tham số.

6. Peephole LSTM

Trong standard LSTM cơ bản, các cổng chủ yếu nhìn xtx_t và ht−1h_{t-1}.

Peephole LSTM cho một số cổng nhìn trực tiếp vào cell state.

Gers, Schraudolph và Schmidhuber (2002) cho thấy peephole connections giúp LSTM học các nhiệm vụ cần phân biệt chính xác khoảng cách thời gian, ví dụ các spike cách nhau 49 so với 50 bước.

Ý nghĩa

Nếu quyết định mở/đóng cổng phụ thuộc trực tiếp vào “bộ nhớ hiện đang chứa bao nhiêu”, việc cho cổng truy cập cell state có thể hữu ích.

Không phải mọi framework mặc định đều dùng peephole LSTM.

7. LSTM với projection

Khi hidden state rất lớn, số tham số và chi phí đầu ra có thể cao.

LSTM with projection thêm một phép chiếu tuyến tính để giảm kích thước trạng thái đưa ra ngoài.

Sak, Senior và Beaufays (2014) dùng kiến trúc LSTM có recurrent projection cho nhận dạng tiếng nói quy mô lớn, nhằm dùng tham số hiệu quả hơn.

Projection là lựa chọn kỹ thuật hữu ích khi mô hình cần năng lực cell lớn nhưng muốn giữ biểu diễn đầu ra gọn hơn.

8. LSTM cho phân loại chuỗi

Một thiết kế cơ bản:

Câu hỏi quan trọng nhất là lấy biểu diễn chuỗi bằng cách nào.

Cách 1 - trạng thái cuối

Với LSTM một chiều:

z=hTz=h_T

sau đó:

y^=Wz+b\hat{y}=Wz+b

Phù hợp cho baseline và chuỗi không quá dài.

Cách 2 - pooling các trạng thái

Ví dụ mean pooling:

z=1T∑t=1Thtz=\frac{1}{T}\sum_{t=1}^{T}h_t

Pooling giảm phụ thuộc tuyệt đối vào trạng thái cuối, nhưng phải mask padding đúng.

Cách 3 - bidirectional final state

Ghép trạng thái cuối của hai hướng.

Cách này chỉ hợp lệ khi toàn bộ chuỗi có sẵn lúc dự đoán.

9. LSTM cho gán nhãn từng bước

Với bài toán mỗi thời điểm có nhãn:

(h1,h2,…,hT)→(y^1,y^2,…,y^T)(h_1,h_2,\ldots,h_T) \rightarrow (\hat{y}_1,\hat{y}_2,\ldots,\hat{y}_T)

Ta áp dụng cùng một tầng phân loại cho từng hth_t.

Trong code, thường không cần tạo ba Linear khác nhau; cùng một tầng được áp dụng lên chiều đặc trưng của tensor output.

10. Kích thước tensor trong PyTorch

Ví dụ:

lstm = nn.LSTM(
    input_size=16,
    hidden_size=32,
    num_layers=2,
    batch_first=True,
    bidirectional=False,
)

x = torch.randn(8, 20, 16)
output, (h_n, c_n) = lstm(x)

Với cấu hình trên:

x      : (8, 20, 16)
output : (8, 20, 32)
h_n    : (2, 8, 32)
c_n    : (2, 8, 32)

Nếu dùng hai chiều

bidirectional=True

thì số hướng D=2D=2.

Với hidden_size=32:

output : (8, 20, 64)
h_n    : (4, 8, 32)
c_n    : (4, 8, 32)

Vì:

D×num layers=2×2=4D \times \text{num layers}=2\times 2=4

và output ghép hai hướng nên chiều cuối là:

2×32=642\times32=64

11. Ví dụ code: LSTM phân loại chuỗi

import torch
import torch.nn as nn

class LSTMClassifier(nn.Module):
    def __init__(self, input_size=3, hidden_size=64, num_classes=2):
        super().__init__()
        self.lstm = nn.LSTM(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=1,
            batch_first=True,
        )
        self.classifier = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        output, (h_n, c_n) = self.lstm(x)
        last_hidden = h_n[-1]
        logits = self.classifier(last_hidden)
        return logits

model = LSTMClassifier()
x = torch.randn(32, 8, 3)
logits = model(x)
print(logits.shape)  # (32, 2)

Nếu chuỗi trong batch có độ dài khác nhau, cần xử lý padding/length trước khi coi h_n hay output[:, -1] là biểu diễn cuối hợp lệ.

12. Chuỗi dài khác nhau: vẫn phải xử lý padding

LSTM không tự hiểu token nào là padding nếu bạn chỉ đưa một tensor số thông thường.

Hai cách phổ biến:

  • dùng pack_padded_sequence/pack_sequence;
  • chạy tensor padded rồi dùng mask/length để chọn đúng vị trí và tính loss.

Sai ở bước này có thể làm một LSTM tốt trông như mô hình kém.

13. Trạng thái đầu và reset trạng thái

Với batch độc lập, thường dùng:

h0=0,c0=0h_0=0,\qquad c_0=0

Nếu xử lý một chuỗi dài bị chia đoạn liên tục, có thể truyền:

(hT,cT)(h_T,c_T)

sang đoạn tiếp theo.

Nhưng nếu các đoạn thuộc các đối tượng khác nhau, phải reset để tránh rò rỉ trạng thái.

14. Gradient clipping vẫn có ích với LSTM

LSTM giúp việc học phụ thuộc dài tốt hơn, nhưng không loại bỏ exploding gradient.

Trong thực tế có thể vẫn dùng:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

Ngưỡng không có một giá trị đúng cho mọi bài toán; cần theo dõi gradient norm và validation performance.

15. LSTM hay Vanilla RNN?

Tiêu chíVanilla RNNLSTM
Cấu trúcđơn giảnnhiều cổng hơn
Tham sốít hơnnhiều hơn
Tốc độ/costthường nhẹ hơnthường nặng hơn
Phụ thuộc ngắncó thể đủthường vẫn tốt
Phụ thuộc dàikhó họcphù hợp hơn
Giá trị sư phạmrất tốt để hiểu recurrence/BPTTrất tốt để hiểu cơ chế gated memory

Nếu bài toán đơn giản và chuỗi ngắn, Vanilla RNN vẫn là baseline hợp lệ. Không cần mặc định dùng mô hình phức tạp nhất.

16. LSTM hay GRU?

GRU sẽ được học ở chương sau.

Ở mức định hướng:

  • cả hai đều là gated RNN;
  • GRU gộp một số cơ chế để cấu trúc gọn hơn;
  • LSTM có cell state và hidden state riêng;
  • không có quy tắc “LSTM luôn tốt hơn GRU” hoặc ngược lại.

Nên so sánh thực nghiệm khi chi phí cho phép.

17. LSTM hay Transformer?

Transformer đã thay thế RNN/LSTM trong nhiều bài toán NLP quy mô lớn, nhưng LSTM vẫn có giá trị:

  • baseline mạnh cho dữ liệu tuần tự nhỏ/vừa;
  • mô hình gọn hơn cho một số hệ thống;
  • suy luận streaming tự nhiên theo thời gian;
  • hữu ích trong time series, tín hiệu, speech và hệ thống nhúng tùy bài toán;
  • nền tảng để hiểu lịch sử phát triển của mô hình chuỗi.

Không nên kết luận “Transformer ra đời nên LSTM không còn dùng”. Lựa chọn kiến trúc phụ thuộc dữ liệu, độ dài chuỗi, tài nguyên và yêu cầu triển khai.

18. Một quy trình baseline thực tế

Giả sử bạn có bài toán phân loại chuỗi số.

Bước 1 - baseline không tuần tự

Dùng thống kê đơn giản hoặc MLP trên đặc trưng tổng hợp để biết bài toán có khó không.

Bước 2 - Vanilla RNN

Kiểm tra xem việc mô hình hóa thứ tự có giúp không.

Bước 3 - LSTM

Nếu chuỗi dài hoặc phụ thuộc thời gian quan trọng, so sánh với LSTM.

Bước 4 - kiểm soát biến

Giữ gần giống nhau:

  • dữ liệu train/validation/test;
  • embedding/đầu vào;
  • số epoch;
  • optimizer;
  • metric.

Bước 5 - phân tích lỗi

Không chỉ nhìn một con số cuối. Hãy xem:

  • mẫu ngắn vs dài;
  • lớp hiếm;
  • sequence length;
  • confusion matrix;
  • gradient norm và learning curve.

19. Checklist debug LSTM

  1. input.shape có đúng (batch, time, feature) khi batch_first=True không?
  2. Có mask/padding đúng không?
  3. h_n và c_n có đúng số tầng × số hướng không?
  4. Có vô tình dùng bidirectional trong bài toán online không?
  5. Có reset (h,c) giữa các mẫu độc lập không?
  6. Loss có giảm trên một tập rất nhỏ không? Nếu không, có thể code/pipeline sai.
  7. Gradient có nổ không? Theo dõi gradient norm và thử clipping.
  8. Validation có xấu đi trong khi train loss giảm không? Có thể overfit.
  9. Có thật sự cần nhiều tầng/hidden size lớn không?
  10. Baseline đơn giản có tốt ngang hoặc hơn không?

20. Bài tập

Bài 1 - đọc shape

Cho:

lstm = nn.LSTM(
    input_size=10,
    hidden_size=40,
    num_layers=3,
    batch_first=True,
    bidirectional=True,
)
x = torch.randn(16, 25, 10)

Hãy xác định output.shape, h_n.shape, c_n.shape.

Bài 2 - chọn một chiều hay hai chiều

Với từng bài toán sau, chọn LSTM một chiều hay có thể dùng hai chiều và giải thích:

  1. dự đoán giá điện 15 phút tiếp theo;
  2. gán nhãn từ loại cho một câu đã có đầy đủ;
  3. nhận cảnh báo bất thường ngay khi cảm biến gửi dữ liệu;
  4. phân loại toàn bộ một đoạn transcript đã thu âm xong.

Bài 3 - sửa thiết kế

Một nhóm phân loại chuỗi padded bằng cách lấy:

last = output[:, -1, :]

nhưng mỗi chuỗi có độ dài khác nhau. Giải thích lỗi và đề xuất ít nhất hai cách sửa.

Bài 4 - paper reading

Đọc phần abstract của Greff et al. (2017). Trả lời:

  • nghiên cứu so sánh bao nhiêu biến thể LSTM;
  • họ đánh giá trên những nhóm nhiệm vụ nào;
  • kết luận nào khiến ta không nên mặc định rằng một biến thể phức tạp hơn sẽ luôn tốt hơn.

21. Những điều cần nhớ

  1. Bidirectional LSTM dùng được cả ngữ cảnh trước và sau, nhưng không phù hợp nếu tương lai chưa có ở lúc suy luận.
  2. Stacked LSTM tăng độ sâu nhưng đồng thời tăng cost và nguy cơ overfit.
  3. Peephole cho cổng nhìn trực tiếp vào cell state.
  4. Projection giúp giảm kích thước biểu diễn/chi phí ở LSTM lớn.
  5. output, h_n, c_n có shape phụ thuộc số tầng và số hướng - phải đọc đúng trước khi nối classifier.
  6. Padding, reset trạng thái và split dữ liệu thường ảnh hưởng kết quả nhiều không kém việc tinh chỉnh kiến trúc.
  7. LSTM là một lựa chọn trong hộp công cụ, không phải mặc định tốt nhất cho mọi dữ liệu tuần tự.

Tài liệu tham khảo

  1. Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735-1780. https://doi.org/10.1162/neco.1997.9.8.1735
  2. Gers, F. A., Schmidhuber, J., & Cummins, F. (2000). Learning to Forget: Continual Prediction with LSTM. Neural Computation, 12(10), 2451-2471. https://doi.org/10.1162/089976600300015015
  3. Gers, F. A., Schraudolph, N. N., & Schmidhuber, J. (2002). Learning Precise Timing with LSTM Recurrent Networks. Journal of Machine Learning Research, 3, 115-143. https://www.jmlr.org/papers/v3/gers02a.html
  4. Graves, A., & Schmidhuber, J. (2005). Framewise phoneme classification with bidirectional LSTM and other neural network architectures. Neural Networks, 18(5-6), 602-610. https://doi.org/10.1016/j.neunet.2005.06.042
  5. Graves, A., Mohamed, A.-r., & Hinton, G. (2013). Speech Recognition with Deep Recurrent Neural Networks. ICASSP. https://arxiv.org/abs/1303.5778
  6. Greff, K., Srivastava, R. K., Koutnik, J., Steunebrink, B. R., & Schmidhuber, J. (2017). LSTM: A Search Space Odyssey. IEEE Transactions on Neural Networks and Learning Systems, 28(10), 2222-2232. https://arxiv.org/abs/1503.04069
  7. Sak, H., Senior, A., & Beaufays, F. (2014). Long Short-Term Memory Recurrent Neural Network Architectures for Large Scale Acoustic Modeling. Interspeech, 338-342. https://doi.org/10.21437/Interspeech.2014-80
  8. PyTorch documentation. torch.nn.LSTM. https://docs.pytorch.org/docs/stable/generated/torch.nn.LSTM.html