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$:

📐 Công thức toán học của RNN cơ bản (Vanilla RNN)
Trạng thái ẩn $h_t$ tại bước thời gian $t$ được cập nhật thông qua hàm kích hoạt phi tuyến tính $\tanh$ (hoặc $\text{ReLU}$): $$h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h)$$ Trong đó:
  • $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).
Sau đó, đầu ra dự đoán $y_t$ (nếu có) được tính từ trạng thái ẩn hiện tại: $$y_t = \text{Softmax}(W_{hy} h_t + b_y)$$

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.

🔢 Con số thật: tốc độ tiêu biến của $(0.9)^N$
Giới hạn $\lim_{N \to \infty} (0.9)^N = 0$ nghe trừu tượng, nhưng tốc độ hội tụ về 0 nhanh đến mức đáng ngạc nhiên ngay cả với trị riêng "hiền lành" như $0.9$ (rất gần 1):
  • $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.
Một đoạn văn 100 từ là chuyện rất bình thường trong xử lý ngôn ngữ tự nhiên, nên chỉ với trị riêng $0.9$ (chưa hề "nguy hiểm" theo trực giác), gradient của từ đầu câu gần như không còn tác động gì đến việc cập nhật trọng số nữa — đây là lý do Vanilla RNN gần như vô dụng với ngữ cảnh dài trong thực tế.
⚠️ Cạm bẫy: Hiện tượng bùng nổ Gradient (Exploding Gradient)
Ngược lại với tiêu biến, nếu các giá trị riêng trị của ma trận $W_{hh}$ lớn hơn 1, đạo hàm sẽ tăng lên theo cấp số nhân khiến mô hình bị lỗi tràn số 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ụ.

⚠️ Cạm bẫy: Điểm nghẽn tính toán tuần tự của RNN/LSTM
Do bản chất toán học của lớp tuần hoàn: bước tính toán tiếp theo bắt buộc phải đợi kết quả trạng thái ẩn của bước trước đó ($h_{t-1}$), chúng ta không thể song song hóa việc huấn luyện RNN trên phần cứng GPU hiện đại. Đây là điểm nghẽn chí mạng khiến RNN/LSTM bị lép vế hoàn toàn trước kiến trúc CNN và Transformer trong kỷ nguyên dữ liệu lớn.

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:

🧠 Thuật toán Dot-product Attention sơ khởi
Giả sử Decoder đang ở trạng thái ẩn hiện tại là $s_t$, và toàn bộ các trạng thái ẩn đầu ra của Encoder là $h_1, h_2, \dots, h_T$:
  1. 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$$
  2. 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))}$$
  3. 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$$
Decoder sử dụng vector ngữ cảnh động này để đưa ra từ dịch chính xác nhất.
🔢 Ví dụ tính tay Dot-product Attention (đã kiểm chứng bằng số thật)
Giả sử câu gốc chỉ có 3 từ, Encoder tạo ra 3 trạng thái ẩn 2 chiều: $h_1 = (1.0, 0.0)$, $h_2 = (0.0, 1.0)$, $h_3 = (0.8, 0.6)$. Decoder đang ở trạng thái $s_t = (1.0, 0.5)$.
  1. Đ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.
  2. 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.
  3. 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.

sentiment_lstm.py
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

🔑 Bài học đạt đượ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 về sentiment_lstm.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 8: Xử lý Văn bản & Word Embeddings Bài 10: Cơ chế Self-Attention & Kỷ nguyên Transformer Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận