Kiến trúc mạng học sâu: từ nền tảng đến LLM·Chương 3 — Vanilla RNN: trạng thái cho dữ liệu tuần tự

Chế độ xem

Vanilla RNN: trạng thái cho dữ liệu tuần tự

Mục tiêu

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

  • giải thích được vì sao dữ liệu tuần tự không nên luôn bị ép thành một vector cố định;
  • hiểu vai trò của trạng thái ẩn trong RNN;
  • đọc được công thức cập nhật của một Vanilla RNN;
  • hiểu ý nghĩa của việc dùng chung tham số theo thời gian;
  • phân biệt các dạng bài toán chuỗi phổ biến: nhiều-đến-một, nhiều-đến-nhiều và dự đoán bước tiếp theo;
  • nhận ra giới hạn cơ bản của Vanilla RNN trước khi sang BPTT và LSTM.

Trong chương này, Vanilla RNN chỉ mạng hồi tiếp đơn giản kiểu Elman, không có các cổng như LSTM hay GRU.

1. Vì sao dữ liệu tuần tự khác dữ liệu độc lập?

Ở MLP, ta thường giả sử mỗi mẫu đầu vào đã được biểu diễn thành một vector có kích thước cố định. Ví dụ:

  • một sinh viên: [điểm chuyên cần, điểm giữa kỳ, điểm bài tập];
  • một căn hộ: [diện tích, số phòng, quận];
  • một ảnh sau khi làm phẳng: một vector rất dài.

Nhưng nhiều loại dữ liệu có thứ tự và thứ tự đó mang thông tin:

  • câu: "không tốt" khác "tốt không";
  • chuỗi thao tác trên hệ thống học tập: đăng nhập → xem bài → nộp bài khác nộp bài → đăng nhập → xem bài;
  • tín hiệu cảm biến: nhiệt độ hôm nay thường liên quan đến các thời điểm ngay trước đó;
  • âm thanh: một khung tín hiệu chỉ có ý nghĩa khi đặt trong ngữ cảnh của các khung xung quanh.

Nếu xử lý từng phần tử hoàn toàn độc lập, mô hình mất thông tin về những gì đã xảy ra trước đó.

Đây là lý do RNN đưa thêm một biến trạng thái để mang thông tin từ bước trước sang bước hiện tại.

2. Ý tưởng cốt lõi: mỗi bước nhận cả dữ liệu mới và trạng thái cũ

Tại thời điểm t, RNN nhận:

  • đầu vào hiện tại xtx_t;
  • trạng thái ẩn trước đó ht−1h_{t-1}.

Sau đó tạo ra trạng thái mới hth_t.

Điểm quan trọng là ba khối trong hình không phải ba mạng khác nhau. Chúng là cùng một phép biến đổi được dùng lặp lại cho nhiều bước thời gian.

Một cách đọc dễ hiểu

Có thể xem hth_t như một bản tóm tắt có kích thước cố định về phần chuỗi mà mô hình đã đọc đến thời điểm t.

Ví dụ với chuỗi:

"bài này không ..."

sau khi đọc từ "không", trạng thái ẩn có thể thay đổi để phản ánh rằng từ phía sau có khả năng đảo nghĩa của phần trước. Mô hình không lưu nguyên văn toàn bộ câu; nó học cách nén thông tin quá khứ thành các con số trong hth_t.

Đây là khác biệt chính so với MLP thông thường.

3. Công thức của Vanilla RNN

Một RNN đơn giản thường được viết:

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

Nếu cần tạo đầu ra ở mỗi bước:

yt=Whyht+byy_t = W_{hy}h_t + b_y

Trong đó:

Ký hiệuÝ nghĩa
xtx_tđầu vào tại thời điểm t
ht−1h_{t-1}trạng thái ẩn từ bước trước
hth_ttrạng thái ẩn mới
WxhW_{xh}trọng số biến đổi đầu vào thành trạng thái
WhhW_{hh}trọng số hồi tiếp từ trạng thái cũ sang trạng thái mới
bhb_hbias của trạng thái ẩn
WhyW_{hy}trọng số từ trạng thái ẩn sang đầu ra

Hàm tanh⁡\tanh đưa giá trị về khoảng (−1,1)(-1, 1), giúp trạng thái có miền giá trị bị chặn. Một số thư viện cũng cho phép dùng ReLU, nhưng tanh là lựa chọn kinh điển của Simple/Elman RNN.

Kích thước tensor

Giả sử:

  • mỗi phần tử đầu vào có d đặc trưng;
  • trạng thái ẩn có m phần tử.

Khi đó:

xt∈Rd,ht∈Rmx_t \in \mathbb{R}^{d}, \qquad h_t \in \mathbb{R}^{m}

và:

Wxh∈Rm×d,Whh∈Rm×mW_{xh} \in \mathbb{R}^{m \times d}, \qquad W_{hh} \in \mathbb{R}^{m \times m}

Điều này giải thích vì sao RNN có thể nhận chuỗi dài 10, 100 hay 1000 bước mà không phải tạo thêm một bộ trọng số mới cho từng vị trí.

4. “Mở cuộn” RNN để nhìn rõ quá trình tính toán

Trong sơ đồ mạng, RNN thường được vẽ như một khối có mũi tên quay lại chính nó. Để hiểu quá trình tính toán, ta mở khối đó theo trục thời gian.

Cách biểu diễn này được gọi là unrolling - mở đồ thị tính toán qua thời gian.

Sau khi mở cuộn, RNN trông gần giống một mạng rất sâu:

  • mỗi bước thời gian tương ứng một “tầng” tính toán;
  • cùng bộ trọng số được dùng lại ở mọi bước;
  • trạng thái tại bước sau phụ thuộc vào trạng thái bước trước.

Ý tưởng mở cuộn đặc biệt quan trọng khi ta học lan truyền ngược qua thời gian ở bài tiếp theo.

5. Dùng chung tham số theo thời gian

Giả sử chuỗi dài T=100T=100. Một cách ngây thơ là tạo 100 mạng khác nhau cho 100 vị trí. Cách đó có hai vấn đề:

  1. số tham số tăng theo độ dài chuỗi;
  2. mô hình học riêng cho “vị trí 1”, “vị trí 2”, … nên khó khái quát sang chuỗi có độ dài khác.

RNN dùng cùng WxhW_{xh}, WhhW_{hh} và bhb_h cho tất cả thời điểm.

Hệ quả: cùng một kiểu biến đổi có thể được áp dụng cho phần tử ở bất kỳ vị trí nào trong chuỗi.

Đây là một dạng chia sẻ tham số, tương tự việc CNN dùng cùng một kernel ở nhiều vị trí không gian.

6. Một ví dụ tính tay rất nhỏ

Giả sử RNN có một trạng thái duy nhất để dễ quan sát:

ht=tanh⁡(0.8xt+0.5ht−1)h_t = \tanh(0.8x_t + 0.5h_{t-1})

Cho:

h0=0h_0 = 0

và chuỗi đầu vào:

x1=1,x2=0,x3=1x_1=1,\quad x_2=0,\quad x_3=1

Ta có:

h1=tanh⁡(0.8)≈0.664h_1 = \tanh(0.8) \approx 0.664 h2=tanh⁡(0.5×0.664)≈0.320h_2 = \tanh(0.5 \times 0.664) \approx 0.320 h3=tanh⁡(0.8+0.5×0.320)≈0.744h_3 = \tanh(0.8 + 0.5 \times 0.320) \approx 0.744

Điểm cần quan sát: ở bước 2, mặc dù x2=0x_2=0, trạng thái vẫn không bằng 0 vì nó còn mang ảnh hưởng của h1h_1.

Nói cách khác, quá khứ tiếp tục ảnh hưởng đến hiện tại thông qua trạng thái ẩn.

7. RNN có thể tạo đầu ra theo nhiều kiểu

Không phải bài toán chuỗi nào cũng cần một đầu ra ở mỗi bước.

7.1. Nhiều-đến-một: phân loại cả chuỗi

Ví dụ:

  • phân loại một câu phản hồi thành tích cực/tiêu cực;
  • dự đoán sinh viên có nguy cơ bỏ học dựa trên chuỗi hoạt động nhiều tuần;
  • phân loại một đoạn tín hiệu ECG.

Thông thường ta lấy trạng thái cuối hTh_T rồi đưa qua một tầng phân loại.

7.2. Nhiều-đến-nhiều cùng chiều dài

Ví dụ:

  • gán nhãn từ loại cho từng từ;
  • gán nhãn trạng thái cho từng khung âm thanh;
  • phát hiện bất thường tại từng thời điểm cảm biến.

7.3. Dự đoán bước tiếp theo

Ví dụ:

  • dự đoán ký tự tiếp theo;
  • dự đoán giá trị cảm biến ở thời điểm kế tiếp;
  • mô hình hóa chuỗi sự kiện.

Ở đây đầu ra tại thời điểm t thường được dùng để dự đoán xt+1x_{t+1}.

8. Đầu vào của RNN không nhất thiết là “một số”

Chuỗi số

Với dữ liệu thời gian, mỗi xtx_t có thể là:

xt=[x(1),x(2),x(3)]x_t = [x^{(1)}, x^{(2)}, x^{(3)}]

với x(1)x^{(1)}, x(2)x^{(2)}, x(3)x^{(3)} lần lượt là nhiệt độ, độ ẩm và lượng mưa.

Chuỗi từ

Máy không nhận trực tiếp từ "học" hay "thi". Mỗi token thường được đổi thành vector embedding:

Embedding và RNN có thể được học đồng thời.

Chuỗi đặc trưng đã trích xuất

Trong âm thanh hoặc video, xtx_t cũng có thể là vector đặc trưng do một mạng khác tạo ra.

RNN chỉ yêu cầu đầu vào được sắp thành một dãy vector có thứ tự.

9. Trạng thái đầu h0h_0 lấy từ đâu?

Lựa chọn đơn giản nhất:

h0=0h_0 = \mathbf{0}

Trong một số hệ thống, h0h_0 có thể:

  • là tham số học được;
  • được sinh từ thông tin ngữ cảnh khác;
  • được giữ lại từ đoạn chuỗi trước nếu bài toán thực sự liên tục theo thời gian.

Không nên giữ trạng thái giữa hai mẫu không liên quan. Nếu mẫu A và B là hai sinh viên khác nhau, truyền trạng thái từ A sang B sẽ làm lẫn thông tin.

10. RNN khác MLP ở đâu?

MLPVanilla RNN
xử lý đầu vào cố địnhxử lý chuỗi dài biến đổi
không có trạng thái thời giancó trạng thái hth_t
mỗi mẫu thường xử lý độc lậpcác bước trong cùng chuỗi phụ thuộc nhau
không chia sẻ trọng số theo vị trí thời giancùng trọng số được dùng ở mọi bước
không tự mô hình hóa thứ tựthứ tự ảnh hưởng trực tiếp đến trạng thái

RNN vẫn là mạng neural thông thường: vẫn có trọng số, bias, hàm kích hoạt, loss và tối ưu bằng gradient. Điểm khác nằm ở cấu trúc vòng lặp của trạng thái.

11. Vanilla RNN mạnh ở đâu, yếu ở đâu?

Điểm mạnh

  • mô hình hóa được chuỗi có độ dài khác nhau;
  • số tham số không tăng theo chiều dài chuỗi;
  • trạng thái ẩn tạo cơ chế mang thông tin từ quá khứ;
  • phù hợp để giải thích nền tảng của hầu hết họ mạng hồi tiếp sau này.

Điểm yếu

Vanilla RNN thường khó học quan hệ cách xa nhiều bước. Ví dụ:

"Mình mua chiếc laptop từ đầu học kỳ ... sau rất nhiều sự kiện ... chiếc máy **này** vẫn chạy tốt."

Để hiểu "này" đang nói tới gì, thông tin cũ phải tồn tại qua nhiều bước cập nhật.

Trong quá trình huấn luyện, gradient phải đi ngược qua một chuỗi dài các phép nhân. Nó có thể:

  • nhỏ dần đến gần 0 - vanishing gradient;
  • lớn nhanh và mất ổn định - exploding gradient.

Đó là nội dung của bài tiếp theo.

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

  1. RNN xử lý chuỗi bằng cách duy trì một trạng thái ẩn.
  2. hth_t phụ thuộc cả xtx_t và ht−1h_{t-1}.
  3. Cùng bộ tham số được dùng lặp lại ở mọi thời điểm.
  4. Mở cuộn RNN giúp nhìn nó như một đồ thị tính toán sâu theo thời gian.
  5. RNN có thể dùng cho phân loại chuỗi, gán nhãn từng bước và dự đoán bước tiếp theo.
  6. Vanilla RNN gặp khó với phụ thuộc dài - nguyên nhân nằm ở dòng gradient khi huấn luyện.

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

  1. Nếu đổi thứ tự các phần tử của một chuỗi, RNN có nhất thiết cho cùng kết quả không? Vì sao?
  2. Vì sao số tham số của một RNN không tăng khi chuỗi dài hơn?
  3. hth_t khác xtx_t như thế nào?
  4. Với bài toán phân loại một câu thành 3 chủ đề, bạn cần đầu ra ở mọi bước hay chỉ đầu ra cuối?
  5. Nếu hai chuỗi thuộc hai người dùng khác nhau, có nên truyền hTh_T của chuỗi thứ nhất thành h0h_0 của chuỗi thứ hai không?

Tài liệu tham khảo

  1. Elman, J. L. (1990). Finding Structure in Time. Cognitive Science, 14(2), 179-211. https://doi.org/10.1207/s15516709cog1402_1
  2. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Chapter 10: Sequence Modeling: Recurrent and Recursive Nets. MIT Press. https://www.deeplearningbook.org/contents/rnn.html
  3. Schuster, M., & Paliwal, K. K. (1997). Bidirectional recurrent neural networks. IEEE Transactions on Signal Processing, 45(11), 2673-2681. https://doi.org/10.1109/78.650093
  4. PyTorch documentation. torch.nn.RNN. https://docs.pytorch.org/docs/stable/generated/torch.nn.RNN.html