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

LSTM: bộ nhớ, cell state và các cổng

Mục tiêu

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

  • giải thích vì sao LSTM được đề xuất để xử lý khó khăn học phụ thuộc dài của RNN;
  • phân biệt trạng thái ẩn hth_t và trạng thái bộ nhớ ctc_t;
  • hiểu chức năng của cổng quên, cổng ghi và cổng đầu ra;
  • đọc được các phương trình của LSTM hiện đại;
  • hiểu vì sao đường cập nhật của ctc_t giúp dòng gradient thuận lợi hơn Vanilla RNN;
  • tránh một số cách diễn giải quá đơn giản như “LSTM giải quyết hoàn toàn vanishing gradient”.

1. Từ Vanilla RNN đến LSTM

Vanilla RNN cập nhật trạng thái bằng:

ht=tanh⁡(Wxhxt+Whhht−1+b)h_t=\tanh(W_{xh}x_t + W_{hh}h_{t-1}+b)

Mỗi bước, toàn bộ trạng thái cũ đi qua một phép biến đổi phi tuyến mới. Khi gradient đi ngược qua nhiều bước, nó phải đi qua chuỗi dài các phép nhân Jacobian. Đây là nguồn gốc của khó khăn khi học phụ thuộc dài.

Hochreiter và Schmidhuber (1997) đề xuất Long Short-Term Memory - LSTM để tạo một cơ chế lưu trữ có đường truyền lỗi ổn định hơn.

Một chi tiết lịch sử quan trọng

Bản LSTM năm 1997 không giống hoàn toàn LSTM chuẩn trong thư viện hiện nay.

  • LSTM 1997 có cơ chế bộ nhớ, input gate và output gate.
  • Forget gate được Gers, Schmidhuber và Cummins bổ sung năm 2000 để mạng có thể tự học khi nào cần xóa bớt trạng thái cũ.

Trong bài này, ta học LSTM hiện đại phổ biến, gồm ba cổng: forget, input và output.

2. LSTM có hai trạng thái

Vanilla RNN chủ yếu truyền một trạng thái:

hth_t

LSTM truyền hai trạng thái:

  1. trạng thái bộ nhớ ctc_t - đường lưu thông tin dài hạn;
  2. trạng thái ẩn hth_t - phần thông tin được đưa ra để tính bước tiếp theo và đầu ra.

Có thể hiểu đơn giản:

  • ctc_t là “dòng bộ nhớ nội bộ”;
  • hth_t là “phần bộ nhớ được lộ ra ngoài ở bước hiện tại”.

Hai vector này có liên hệ chặt chẽ nhưng không phải cùng một thứ.

3. Các phương trình của LSTM hiện đại

Trước tiên ghép thông tin hiện tại và trạng thái trước đó để các cổng cùng xem:

[xt,ht−1][x_t,h_{t-1}]

Cổng quên

ft=σ(Wf[xt,ht−1]+bf)f_t = \sigma(W_f[x_t,h_{t-1}] + b_f)

Cổng đầu vào (cổng ghi)

it=σ(Wi[xt,ht−1]+bi)i_t = \sigma(W_i[x_t,h_{t-1}] + b_i)

Nội dung ứng viên

c~t=tanh⁡(Wc[xt,ht−1]+bc)\tilde{c}_t = \tanh(W_c[x_t,h_{t-1}] + b_c)

Cập nhật bộ nhớ

ct=ft⊙ct−1+it⊙c~tc_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t

Cổng đầu ra

ot=σ(Wo[xt,ht−1]+bo)o_t = \sigma(W_o[x_t,h_{t-1}] + b_o)

Trạng thái ẩn mới

ht=ot⊙tanh⁡(ct)h_t = o_t \odot \tanh(c_t)

Trong đó ⊙\odot là phép nhân từng phần tử.

4. Toàn bộ LSTM cell nhìn như thế nào?

Điểm nổi bật nằm ở dòng:

ct=ft⊙ct−1+it⊙c~tc_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t

Bộ nhớ mới được tạo bằng giữ lại một phần bộ nhớ cũ + thêm một phần nội dung mới.

5. Vì sao dùng sigmoid cho các cổng?

Hàm sigmoid:

σ(z)=11+e−z\sigma(z)=\frac{1}{1+e^{-z}}

cho đầu ra trong khoảng:

0<σ(z)<10 < \sigma(z) < 1

Do đó mỗi phần tử của cổng có thể được xem như hệ số điều tiết:

  • gần 0: gần như chặn thông tin;
  • gần 1: gần như cho đi qua;
  • ở giữa: giữ/ghi một phần.

Quan trọng: cổng là vector, không phải một công tắc duy nhất cho cả cell. Các chiều khác nhau của bộ nhớ có thể được giữ hoặc cập nhật với mức khác nhau.

6. Cổng quên làm gì?

ft=σ(Wf[xt,ht−1]+bf)f_t = \sigma(W_f[x_t,h_{t-1}] + b_f)

và:

ft⊙ct−1f_t \odot c_{t-1}

Ví dụ một chiều của ct−1c_{t-1} lưu thông tin “chủ thể đang nói là sinh viên A”.

Nếu sang một đoạn hội thoại mới, mô hình có thể học đặt phần tử tương ứng trong ftf_t gần 0 để giảm ảnh hưởng của thông tin cũ.

Nếu thông tin vẫn còn cần thiết, ftf_t có thể gần 1.

Vì sao forget gate được thêm sau?

Gers et al. (2000) chỉ ra rằng khi xử lý luồng liên tục không có ranh giới reset rõ ràng, trạng thái có thể tích lũy không phù hợp. Forget gate cho phép LSTM học cách tự “reset mềm” từng thành phần bộ nhớ.

7. Cổng ghi và nội dung ứng viên

LSTM không chỉ quyết định có ghi hay không. Nó tách thành hai phần:

  1. viết bao nhiêu: iti_t;
  2. viết nội dung gì: c~t\tilde{c}_t.
it=σ(Wi[xt,ht−1]+bi)i_t = \sigma(W_i[x_t,h_{t-1}] + b_i) c~t=tanh⁡(Wc[xt,ht−1]+bc)\tilde{c}_t = \tanh(W_c[x_t,h_{t-1}] + b_c)

Sau đó:

it⊙c~ti_t \odot \tilde{c}_t

Ví dụ với văn bản, nếu xuất hiện một thực thể mới quan trọng, một số chiều trong iti_t có thể mở để cập nhật bộ nhớ tương ứng.

8. Cổng đầu ra: bộ nhớ không nhất thiết lộ ra toàn bộ

Sau khi cập nhật ctc_t, LSTM tính:

ot=σ(Wo[xt,ht−1]+bo)o_t = \sigma(W_o[x_t,h_{t-1}] + b_o)

và:

ht=ot⊙tanh⁡(ct)h_t=o_t\odot\tanh(c_t)

Điều này cho phép mô hình giữ một số thông tin trong bộ nhớ mà chưa cần dùng ngay ở đầu ra hiện tại.

Một cách phân biệt hữu ích:

  • ctc_t: mô hình đang giữ gì;
  • hth_t: ở bước này mô hình cho phần nào của trạng thái đó tham gia vào xử lý tiếp.

9. Ví dụ số một chiều

Giả sử tại một chiều bộ nhớ:

ct−1=0.8c_{t-1}=0.8

và các cổng cho:

ft=0.9,it=0.2,c~t=−0.5f_t=0.9,\qquad i_t=0.2,\qquad \tilde{c}_t=-0.5

Bộ nhớ mới:

ct=0.9×0.8+0.2×(−0.5)c_t=0.9\times0.8+0.2\times(-0.5) ct=0.72−0.10=0.62c_t=0.72-0.10=0.62

Nghĩa là:

  • phần lớn bộ nhớ cũ được giữ lại;
  • một lượng nhỏ thông tin mới có giá trị âm được thêm vào.

Nếu:

ot=0.7o_t=0.7

thì:

ht=0.7tanh⁡(0.62)≈0.386h_t=0.7\tanh(0.62)\approx0.386

Qua ví dụ này có thể thấy ctc_t và hth_t có giá trị khác nhau.

10. Vì sao LSTM giúp dòng gradient tốt hơn?

Xét đường cập nhật trực tiếp của bộ nhớ:

ct=ft⊙ct−1+it⊙c~tc_t=f_t\odot c_{t-1}+i_t\odot\tilde{c}_t

Nếu chỉ nhìn nhánh trực tiếp từ ct−1c_{t-1} sang ctc_t:

∂ct∂ct−1≈ft\frac{\partial c_t}{\partial c_{t-1}} \approx f_t

Nếu một số chiều của ftf_t được giữ gần 1, gradient trên đường bộ nhớ đó có thể truyền qua nhiều bước mà không bị nhân liên tiếp với một ma trận hồi tiếp đầy đủ như Vanilla RNN.

Đây là một trong những lý do quan trọng giúp LSTM học phụ thuộc dài tốt hơn RNN đơn giản.

Nhưng không nên nói quá mức

LSTM không bảo đảm gradient luôn ổn định.

  • nếu ftf_t liên tục nhỏ hơn 1, gradient vẫn có thể suy giảm;
  • các nhánh khác của mạng vẫn chứa phép nhân phi tuyến;
  • exploding gradient vẫn có thể xảy ra;
  • chuỗi rất dài vẫn khó tối ưu và tốn tính toán.

Cách diễn đạt chính xác hơn:

LSTM tạo một cơ chế truyền và cập nhật trạng thái giúp việc học phụ thuộc dài thuận lợi hơn đáng kể so với Vanilla RNN.

11. Cổng của LSTM được học, không được viết bằng luật thủ công

Ta không lập trình:

nếu gặp dấu chấm → quên 80%

Thay vào đó, tham số của các cổng:

Wf,Wi,Wo,WcW_f,W_i,W_o,W_c

được học bằng gradient descent từ dữ liệu và loss.

Vì vậy LSTM không có “quy tắc nhớ” cố định. Mô hình học cách điều khiển bộ nhớ sao cho hữu ích cho nhiệm vụ.

12. LSTM có bao nhiêu tham số so với Vanilla RNN?

Vanilla RNN cần một phép biến đổi chính cho trạng thái.

LSTM hiện đại tính bốn nhóm giá trị:

  • forget gate;
  • input gate;
  • candidate memory;
  • output gate.

Do đó với cùng input_size và hidden_size, LSTM thường có nhiều tham số và nhiều phép tính hơn Vanilla RNN.

Đổi lại, nó có cơ chế kiểm soát bộ nhớ tốt hơn.

13. Những hiểu nhầm phổ biến

“Forget gate xóa toàn bộ ký ức”

Không đúng. ftf_t là vector, nên LSTM có thể quên một số chiều và giữ các chiều khác.

”ctc_t là long-term memory, hth_t là short-term memory”

Cách nói này có thể giúp nhớ tên, nhưng quá đơn giản. Cả hai đều là trạng thái của hệ thống và cùng tham gia vào động lực học. Tốt hơn nên gọi:

  • ctc_t: trạng thái bộ nhớ/cell state;
  • hth_t: trạng thái ẩn/hidden state.

“LSTM không bị vanishing gradient”

Không chính xác. LSTM giảm khó khăn và tạo đường gradient thuận lợi hơn, chứ không loại bỏ mọi trường hợp vanishing gradient.

“Cổng quên có ngay trong paper LSTM 1997”

Không. Forget gate là cải tiến được công bố năm 2000.

14. Từ công thức sang cách triển khai thư viện

Trong PyTorch, nn.LSTM thực hiện cùng họ phương trình với các cổng:

  • input iti_t;
  • forget ftf_t;
  • candidate gtg_t;
  • output oto_t.

Nó trả về cả trạng thái ẩn và trạng thái bộ nhớ cuối:

output, (h_n, c_n) = lstm(x)

Điều này khác nn.RNN, chỉ trả trạng thái ẩn.

Ta sẽ dùng chi tiết này ở bài thực hành tiếp theo.

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

  1. LSTM được thiết kế để việc học phụ thuộc dài khả thi hơn Vanilla RNN.
  2. LSTM truyền hai trạng thái: ctc_t và hth_t.
  3. Cổng quên điều khiển phần bộ nhớ cũ được giữ lại.
  4. Cổng ghi điều khiển lượng nội dung mới được thêm vào.
  5. Cổng đầu ra điều khiển phần trạng thái bộ nhớ được lộ ra thành hth_t.
  6. Cập nhật ctc_t có đường cộng trực tiếp, thuận lợi hơn cho dòng gradient.
  7. Forget gate là cải tiến năm 2000, không phải thành phần của bản LSTM 1997 ban đầu.

Câu hỏi tự kiểm tra

  1. Vì sao LSTM cần cả ctc_t và hth_t?
  2. Nếu một phần tử của ftf_t gần 1 và iti_t gần 0, điều gì xảy ra với chiều bộ nhớ tương ứng?
  3. Vì sao sigmoid phù hợp để làm cổng?
  4. Forget gate khác thao tác reset toàn bộ trạng thái như thế nào?
  5. LSTM có hoàn toàn loại bỏ exploding gradient không?

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. Bengio, Y., Simard, P., & Frasconi, P. (1994). Learning long-term dependencies with gradient descent is difficult. IEEE Transactions on Neural Networks, 5(2), 157-166. https://doi.org/10.1109/72.279181
  4. 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
  5. PyTorch documentation. torch.nn.LSTMCell. https://docs.pytorch.org/docs/stable/generated/torch.nn.LSTMCell.html