Hình ảnh là dữ liệu có cấu trúc lưới cố định không thay đổi. Thế nhưng, ngôn ngữ tự nhiên hay tín hiệu âm thanh lại có tính chất hoàn toàn khác: chúng là dữ liệu chuỗi (Sequential Data) có độ dài biến thiên liên tục và mang tính thứ tự thời gian chặt chẽ.
Nếu ta đổi vị trí hai từ trong câu, nghĩa của câu có thể đảo ngược hoặc biến mất hoàn toàn. Để giải quyết bài toán xử lý chuỗi này, chúng ta cần tìm hiểu Mạng nơ-ron tuần hoàn (Recurrent Neural Network - RNN). Trong bài học này, ta sẽ đi sâu vào cấu trúc toán học của RNN, tìm hiểu lý do gây tiêu biến gradient (Vanishing Gradient), cách các bộ nhớ cổng như LSTM/GRU khắc phục lỗi này, và bước đệm vĩ đại nhất hướng tới các mô hình AI hiện đại: cơ chế tập trung chú ý Attention Mechanism.
9.1 Cơ chế nhớ chuỗi thời gian của Recurrent Neural Network (RNN)
Mạng MLP hay CNN thông thường chỉ có một hướng truyền dữ liệu duy nhất từ đầu vào qua các lớp ẩn đến đầu ra (Feedforward). Chúng không hề có khái niệm thời gian hay ký ức: mỗi mẫu dữ liệu nạp vào được xử lý độc lập và không liên quan gì đến mẫu trước đó.
Mạng RNN khắc phục điều này bằng cách tạo ra một vòng lặp phản hồi (recurrent loop) bên trong tế bào nơ-ron. Tại mỗi bước thời gian $t$, mạng nơ-ron nhận đầu vào hiện tại $x_t$, đồng thời nhận lại trạng thái ẩn ở bước thời gian trước đó $h_{t-1}$ (được ví như ký ức của mạng từ quá khứ) để tính toán ra trạng thái ẩn mới $h_t$:
- $x_t$: Vector đặc trưng đầu vào tại bước thời gian $t$.
- $h_{t-1}$: Vector trạng thái ẩn tại bước thời gian trước đó $t-1$.
- $W_{hh}$: Ma trận trọng số kết nối giữa các trạng thái ẩn liên tiếp.
- $W_{xh}$: Ma trận trọng số kết nối từ đầu vào đến trạng thái ẩn.
- $b_h$: Vector chệch (bias).
Tuy nhiên, khi huấn luyện RNN trên các chuỗi dài (ví dụ: đoạn văn có hơn 50 từ), chúng ta vấp phải hiện tượng vật lý toán học cực kỳ nghiêm trọng: Tiêu biến Gradient (Vanishing Gradient).
Để tính đạo hàm cho các bước thời gian ở đầu chuỗi nhằm cập nhật trọng số, thuật toán BPTT (Backpropagation Through Time) phải liên tục nhân ma trận trọng số $W_{hh}^T$ qua từng bước thời gian. Nếu các giá trị riêng trị (eigenvalues) của ma trận $W_{hh}$ nhỏ hơn 1, việc nhân liên tục $N$ lần một số nhỏ hơn 1 sẽ khiến đạo hàm tiến dần về 0 theo hàm mũ: $$\lim_{N \to \infty} (0.9)^N = 0$$ Hậu quả là mạng nơ-ron bị mất khả năng học các mối quan hệ ngữ cảnh xa nhau (Long-term dependencies) và chỉ còn nhớ được các từ sát gần kề cuối câu.
- $N = 5$ bước: $(0.9)^5 \approx 0.5905$ — đạo hàm còn hơn một nửa.
- $N = 10$ bước: $(0.9)^{10} \approx 0.3487$.
- $N = 20$ bước: $(0.9)^{20} \approx 0.1216$.
- $N = 50$ bước: $(0.9)^{50} \approx 0.00515$ — chỉ còn nửa phần nghìn.
- $N = 100$ bước: $(0.9)^{100} \approx 0.0000266$ — gần như bằng 0 tuyệt đối.
NaN (Not a Number) lúc huấn luyện. Để sửa lỗi
này, kỹ sư AI luôn áp dụng kỹ thuật xén đạo hàm Gradient Clipping (ép giá trị đạo hàm
không vượt quá một ngưỡng nhất định trước khi cập nhật trọng số).
9.2 Lưới chọn lọc thông tin: Bộ nhớ dài-ngắn hạn LSTM & GRU
Để giải quyết triệt để lỗi tiêu biến gradient, Hochreiter và Schmidhuber đã đề xuất kiến trúc LSTM (Long Short-Term Memory). Thay vì chỉ duy trì một trạng thái ẩn $h_t$ đơn điệu, LSTM giới thiệu một luồng thông tin chạy song song gọi là Cell State ($c_t$) (được ví như đường cao tốc thông tin chạy suốt chuỗi) và sử dụng các cổng kiểm soát thông tin bằng hàm kích hoạt sigmoid $\sigma$:
- Cổng quên (Forget Gate - $f_t$): Quyết định lượng thông tin cũ nào từ quá khứ cần xóa bỏ khỏi Cell State: $$f_t = \sigma(W_f [h_{t-1}, x_t] + b_f)$$
- Cổng nạp (Input Gate - $i_t$): Quyết định lượng thông tin mới nào từ đầu vào sẽ được ghi thêm vào Cell State: $$i_t = \sigma(W_i [h_{t-1}, x_t] + b_i)$$ $$\tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c)$$
- Cập nhật Cell State ($c_t$): Lấy thông tin cũ nhân chập với cổng quên, rồi cộng thêm thông tin mới: $$c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t$$
- Cổng xuất (Output Gate - $o_t$): Quyết định thông tin nào của Cell State sẽ được xuất ra làm trạng thái ẩn $h_t$ tiếp theo: $$o_t = \sigma(W_o [h_{t-1}, x_t] + b_o)$$ $$h_t = o_t \odot \tanh(c_t)$$
Nhờ việc cập nhật Cell State bằng phép cộng tuyến tính ($+$) thay vì phép nhân ma trận dồn dập, đạo hàm có thể truyền thẳng ngược lại quá khứ mà không bị tiêu biến, giúp LSTM học được các mối ngữ cảnh rất dài.
GRU (Gated Recurrent Unit) là phiên bản tinh gọn của LSTM. Nó gộp Cell State và Hidden State làm một, đồng thời chỉ sử dụng 2 cổng: Cổng cập nhật (Update Gate) và Cổng thiết lập lại (Reset Gate). GRU tính toán nhanh hơn và tiết kiệm bộ nhớ hơn nhưng vẫn đạt được hiệu năng tương đương LSTM trên nhiều tác vụ.
9.3 Khái niệm Attention: Cơ chế chú ý tập trung
Trong mô hình Sequence-to-Sequence truyền thống sử dụng để dịch thuật (ví dụ: LSTM Encoder-Decoder), Encoder có nhiệm vụ nén toàn bộ câu văn dài đầu vào thành một vector trạng thái ẩn cuối cùng duy nhất (gọi là Context Vector), rồi đưa cho Decoder dịch ra câu mới.
Cách làm này giống như bạn bắt ép bộ não học thuộc lòng toàn bộ cuốn sách rồi viết lại bản dịch mà không cho phép lật lại trang sách cũ để tra cứu. Khi câu văn đầu vào quá dài, Context Vector sẽ bị quá tải thông tin, gây suy giảm nghiêm trọng chất lượng dịch.
Cơ chế Attention (Sự chú ý) ra đời để giải quyết điểm nghẽn này. Triết lý của Attention cực kỳ đơn giản: Cho phép Decoder nhìn lại toàn bộ các trạng thái ẩn của Encoder ở mọi bước thời gian, tự động tính toán xem từ nào ở câu gốc đóng vai trò quan trọng nhất đối với từ sắp được dịch ra:
- Tính điểm tương đồng (Attention Scores): Đo góc lệch hướng giữa trạng thái ẩn của Decoder và từng từ của Encoder: $$\text{score}(s_t, h_i) = s_t^T h_i$$
- Tính trọng số chú ý (Attention Weights): Chuẩn hóa điểm số bằng hàm Softmax để có một phân phối xác suất tổng bằng 1: $$\alpha_{ti} = \frac{\exp(\text{score}(s_t, h_i))}{\sum_{k=1}^{T} \exp(\text{score}(s_t, h_k))}$$
- Tính toán Context Vector ($c_t$): Nhân chập trọng số chú ý với các vector ẩn của Encoder để tạo ra một vector ngữ cảnh động tích tụ thông tin quan trọng nhất: $$c_t = \sum_{i=1}^{T} \alpha_{ti} h_i$$
- Điểm số (tích vô hướng): $\text{score}(s_t, h_1) = 1.0$, $\text{score}(s_t, h_2) = 0.5$, $\text{score}(s_t, h_3) = 1.1$ — từ $h_3$ có điểm cao nhất vì hướng vector gần với $s_t$ nhất.
- Trọng số Softmax: $\alpha = (0.3688,\ 0.2237,\ 0.4076)$ — tổng đúng bằng $1$, và $h_3$ (điểm cao nhất) nhận trọng số chú ý lớn nhất, đúng như kỳ vọng.
- Context Vector: $c_t = 0.3688 \cdot h_1 + 0.2237 \cdot h_2 + 0.4076 \cdot h_3 = (0.6948,\ 0.4682)$ — một vector pha trộn, nghiêng nhiều nhất về phía $h_3$ nhưng vẫn giữ lại một phần thông tin của $h_1$ và $h_2$, thay vì phải chọn "một hoặc không gì cả" như Context Vector tĩnh truyền thống.
9.4 Dự án thực hành bài 9: Phân loại cảm xúc văn bản với LSTM
Dự án thực hành của bài học này là xây dựng một mạng nơ-ron tuần hoàn LSTM phân loại cảm xúc tích cực (Positive) hoặc tiêu cực (Negative) của các đánh giá dịch vụ tiếng Việt.
Chương trình được tích hợp sẵn tập dữ liệu câu đánh giá mẫu cùng một bộ tokenizer thô để bạn có thể chạy độc lập ngay lập tức mà không cần bất kỳ dependencies phức tạp nào ngoài PyTorch.
import torch
import torch.nn as nn
import torch.optim as optim
# 1. Tạo tập dữ liệu mẫu đánh giá dịch vụ bằng tiếng Việt
dataset = [
("dịch vụ xuất sắc nhân viên thân thiện", 1), # 1: Tích cực
("đồ ăn ngon phục vụ rất nhanh", 1),
("sản phẩm tuyệt vời đóng gói cẩn thận", 1),
("quá tệ đồ ăn nguội lạnh phục vụ kém", 0), # 0: Tiêu cực
("giao hàng chậm trễ chất lượng tồi tệ", 0),
("thái độ nhân viên rất lồi lõm không mua lại", 0)
]
# 2. Xây dựng Tokenizer thô cấp từ
words = set()
for text, _ in dataset:
words.update(text.split())
vocab = {word: idx + 2 for idx, word in enumerate(words)} # ID 0: Padding, ID 1: OOV
vocab["[PAD]"] = 0
vocab["[UNK]"] = 1
inverse_vocab = {v: k for k, v in vocab.items()}
# Hàm chuyển văn bản thành chuỗi số nguyên có độ dài cố định (padding)
def text_to_sequence(text, max_len=8):
tokens = text.split()
seq = []
for token in tokens:
seq.append(vocab.get(token, 1))
# Padding hoặc Truncate
if len(seq) < max_len:
seq += [0] * (max_len - len(seq))
else:
seq = seq[:max_len]
return seq
# Chuẩn bị Tensor dữ liệu đầu vào
x_data = torch.tensor([text_to_sequence(text) for text, _ in dataset], dtype=torch.long)
y_data = torch.tensor([label for _, label in dataset], dtype=torch.float32).unsqueeze(1)
# 3. Định nghĩa kiến trúc mạng SentimentLSTM
class SentimentLSTM(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim):
super(SentimentLSTM, self).__init__()
# Lớp Embedding tra cứu vector nhúng
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
# Lớp LSTM tuần hoàn
# batch_first=True giúp dữ liệu có cấu trúc đầu vào: (Batch, Sequence Length, Features)
self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
# Lớp phân loại đầu ra sigmoid nhị phân
self.classifier = nn.Linear(hidden_dim, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# x shape: (Batch, Sequence Length)
embedded = self.embedding(x) # shape: (Batch, Sequence Length, Embedding Dim)
# Trích xuất thông tin tuần hoàn qua LSTM
# out: chứa các hidden state của mọi bước thời gian
# (hn, cn): chứa hidden state và cell state của bước thời gian cuối cùng
out, (hn, cn) = self.lstm(embedded)
# Lấy hidden state cuối cùng của LSTM đại diện cho toàn bộ ngữ cảnh câu văn
last_hidden = hn[-1] # shape: (Batch, Hidden Dim)
logits = self.classifier(last_hidden)
predictions = self.sigmoid(logits)
return predictions
if __name__ == "__main__":
print("=== Khởi tạo dữ liệu huấn luyện ===")
print(f"Kích thước từ điển (Vocabulary Size): {len(vocab)}")
print(f"Kích thước Tensor đầu vào: {x_data.shape}\n")
# Khởi tạo mô hình
model = SentimentLSTM(vocab_size=len(vocab), embedding_dim=16, hidden_dim=8)
criterion = nn.BCELoss() # Binary Cross Entropy Loss cho phân loại nhị phân
optimizer = optim.Adam(model.parameters(), lr=0.01)
epochs = 40
print("=== Bắt đầu huấn luyện mạng SentimentLSTM ===")
for epoch in range(1, epochs + 1):
model.train()
optimizer.zero_grad()
predictions = model(x_data)
loss = criterion(predictions, y_data)
loss.backward()
optimizer.step()
if epoch % 10 == 0:
# Tính độ chính xác nhị phân đơn giản
binary_predictions = (predictions >= 0.5).float()
accuracy = (binary_predictions == y_data).sum().item() / len(y_data) * 100
print(f"Epoch {epoch:02d}/{epochs} | Loss: {loss.item():.4f} | Accuracy: {accuracy:.1f}%")
print("\n=== Đang tiến hành suy luận thực tế ===")
model.eval()
with torch.no_grad():
test_comment = "đồ ăn ngon phục vụ nhanh tuyệt vời"
seq_test = torch.tensor([text_to_sequence(test_comment)], dtype=torch.long)
pred = model(seq_test).item()
sentiment = "Tích cực (Positive)" if pred >= 0.5 else "Tiêu cực (Negative)"
print(f"Câu đánh giá: '{test_comment}'")
print(f"Xác suất tích cực: {pred*100:.2f}% -> Dự đoán cảm xúc: {sentiment}")
test_comment_bad = "phục vụ quá tệ chất lượng tồi"
seq_test_bad = torch.tensor([text_to_sequence(test_comment_bad)], dtype=torch.long)
pred_bad = model(seq_test_bad).item()
sentiment_bad = "Tích cực (Positive)" if pred_bad >= 0.5 else "Tiêu cực (Negative)"
print(f"Câu đánh giá: '{test_comment_bad}'")
print(f"Xác suất tích cực: {pred_bad*100:.2f}% -> Dự đoán cảm xúc: {sentiment_bad}")
Tóm tắt bài học & Cầu nối kiến thức
- Đạt được: Hiểu cơ chế nhớ chuỗi của mạng RNN/LSTM và cạm bẫy tiêu biến gradient khi xử lý văn bản dài.
- Đạt được: Làm chủ ý tưởng đột phá của cơ chế Chú ý (Attention Mechanism) giúp mô hình tập trung vào các từ khóa quan trọng.
Cầu nối bài tiếp theo: Loại bỏ hoàn toàn tính tuần hoàn chậm chạp của RNN, cơ chế Attention đã khai sinh ra kiến trúc Transformer huyền thoại - động cơ của mọi LLM lớn hiện nay - mà ta sẽ giải phẫu ở Bài số 10.
Tải file code thực hành minh họa bài học
File Python sentiment_lstm.py — mã nguồn xây dựng mạng LSTM phân loại cảm xúc câu đánh giá
tiếng Việt (chạy python sentiment_lstm.py, yêu cầu cài đặt thư viện torch):
📖 Tài liệu tham khảo
- Understanding LSTM Networks — Bài viết trực quan hóa xuất sắc nhất về cơ chế hoạt động của các cổng LSTM (Chris Olah's Blog)
- PyTorch nn.LSTM Documentation — Hướng dẫn kỹ thuật và mô tả đầu vào/đầu ra của lớp LSTM (Official Docs)
- Neural Machine Translation by Jointly Learning to Align and Translate — Bài báo khoa học gốc đề xuất cơ chế Attention (Bahdanau et al., 2014)
Bình luận