Back to Blog

[DL101] Chương 6: Xử lý Ngôn ngữ Tự nhiên (NLP) với RNN và Attention

Ứng dụng RNN trong NLP, mô hình Encoder-Decoder và cơ chế Attention

Bài viết có tham khảo, sử dụng và sửa đổi tài nguyên từ kho lưu trữ handson-mlp, tuân thủ giấy phép Apache‑2.0. Chúng tôi chân thành cảm ơn tác giả Aurélien Géron (@aureliengeron) vì sự chia sẻ kiến thức tuyệt vời và những đóng góp quý giá cho cộng đồng.

Ở chương này, chúng ta sẽ khám phá Xử lý Ngôn ngữ Tự nhiên (Natural Language Processing - NLP) với Mạng Nơ-ron (Neural Networks).

Ngôn ngữ không chỉ là chuỗi ký tự hay từ ngữ; nó chứa đựng cấu trúc ngữ pháp phức tạp và ngữ nghĩa sâu sắc. Để máy tính hiểu được ngôn ngữ, chúng ta cần chuyển đổi văn bản thành các đại diện số học (vector representation) và xây dựng các mô hình có khả năng nắm bắt sự phụ thuộc dài hạn (long-term dependency).

Trong chương này, chúng ta sẽ bắt đầu với các mô hình RNN cơ bản để sinh văn bản, đi qua các kỹ thuật xử lý văn bản hiện đại như Tokenization và Embedding, và cuối cùng là chinh phục kiến trúc Encoder-Decoder với cơ chế Attention - nền tảng của các mô hình ngôn ngữ lớn (LLMs) ngày nay.

Bạn có thể chạy trực tiếp các đoạn mã code tại: Google Colab.

Thiết lập môi trường (Setup)

Kiểm tra phiên bản Python:

import sys

# Kiểm tra phiên bản Python
assert sys.version_info >= (3, 10)

Xác định môi trường thực thi:

IS_COLAB = "google.colab" in sys.modules
IS_KAGGLE = "kaggle_secrets" in sys.modules

Cài đặt thư viện TorchMetrics:

if IS_COLAB:
    %pip install -q torchmetrics
output:
     [?25l    [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [0m  [32m0.0/983.2 kB [0m  [31m? [0m eta  [36m-:--:-- [0m
 [2K    [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [0m  [32m983.2/983.2 kB [0m  [31m26.8 MB/s [0m eta  [36m0:00:00 [0m
     [?25h

Kiểm tra phiên bản PyTorch:

from packaging.version import Version
import torch

assert Version(torch.__version__) >= Version("2.6.0")

Cấu hình thiết bị phần cứng (Hardware Accelerator): Mô hình ngôn ngữ, đặc biệt là Transformer, yêu cầu tính toán song song lượng lớn. GPU là thiết bị bắt buộc để huấn luyện hiệu quả.

if torch.cuda.is_available():
    device = "cuda"
elif torch.backends.mps.is_available():
    device = "mps"
else:
    device = "cpu"

device
output:
    'cuda'

Cảnh báo nếu không có GPU:

if device == "cpu":
    print("Mạng nơ-ron có thể rất chậm nếu không có bộ tăng tốc phần cứng.")
    if IS_COLAB:
        print("Vào Runtime > Change runtime và chọn GPU hardware accelerator.")
    if IS_KAGGLE:
        print("Vào Settings > Accelerator và chọn GPU.")

Cấu hình hiển thị biểu đồ:

import matplotlib.pyplot as plt

plt.rc('font', size=14)
plt.rc('axes', labelsize=14, titlesize=14)
plt.rc('legend', fontsize=14)
plt.rc('xtick', labelsize=10)
plt.rc('ytick', labelsize=10)

Chúng ta xây dựng lại các hàm tiện ích trainevaluate_tm để tái sử dụng, giúp code gọn gàng hơn. Hàm train được thiết kế tổng quát, hỗ trợ scheduler và callback.

import torchmetrics

def evaluate_tm(model, data_loader, metric):
    """Đánh giá mô hình trên tập dữ liệu sử dụng TorchMetrics."""
    model.eval()  # Chuyển sang chế độ đánh giá (tắt dropout, batchnorm...)
    metric.reset()
    with torch.no_grad():  # Không tính gradient để tiết kiệm bộ nhớ
        for X_batch, y_batch in data_loader:
            X_batch, y_batch = X_batch.to(device), y_batch.to(device)
            y_pred = model(X_batch)
            metric.update(y_pred, y_batch)
    return metric.compute()

def train(model, optimizer, loss_fn, metric, train_loader, valid_loader,
          n_epochs, patience=2, factor=0.5, epoch_callback=None):
    """Hàm huấn luyện mô hình tổng quát."""
    # Scheduler giảm learning rate khi metric validation không cải thiện
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
        optimizer, mode="max", patience=patience, factor=factor)
    history = {"train_losses": [], "train_metrics": [], "valid_metrics": []}

    for epoch in range(n_epochs):
        total_loss = 0.0
        metric.reset()
        model.train()  # Chuyển sang chế độ huấn luyện
        if epoch_callback is not None:
            epoch_callback(model, epoch)

        for index, (X_batch, y_batch) in enumerate(train_loader):
            X_batch, y_batch = X_batch.to(device), y_batch.to(device)
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)

            # Lan truyền ngược và cập nhật trọng số
            total_loss += loss.item()
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()

            metric.update(y_pred, y_batch)
            train_metric = metric.compute().item()

            # Hiển thị tiến trình
            print(f"\rBatch {index + 1}/{len(train_loader)}", end="")
            print(f", loss={total_loss/(index+1):.4f}", end="")
            print(f", {train_metric=:.2%}", end="")

        history["train_losses"].append(total_loss / len(train_loader))
        history["train_metrics"].append(train_metric)

        val_metric = evaluate_tm(model, valid_loader, metric).item()
        history["valid_metrics"].append(val_metric)
        scheduler.step(val_metric)

        print(f"\rEpoch {epoch + 1}/{n_epochs},                      "
              f"train loss: {history['train_losses'][-1]:.4f}, "
              f"train metric: {history['train_metrics'][-1]:.2%}, "
              f"valid metric: {history['valid_metrics'][-1]:.2%}")
    return history

Hàm quản lý bộ nhớ:

import gc

def del_vars(variable_names=[]):
    for name in variable_names:
        try:
            del globals()[name]
        except KeyError:
            pass  # bỏ qua nếu biến đã bị xóa
    gc.collect()  # Gọi bộ thu gom rác của Python
    if device == "cuda":
        torch.cuda.empty_cache()  # Xóa cache của CUDA

Sinh văn bản phong cách Shakespeare dùng Character RNN

Mô hình ngôn ngữ cấp ký tự (Character-Level Language Model) học cách dự đoán ký tự tiếp theo dựa trên chuỗi ký tự trước đó. Mặc dù đơn giản, nó có thể học được cấu trúc từ vựng và ngữ pháp cơ bản.

Tạo Tập dữ liệu Huấn luyện (Training Dataset)

Tải dữ liệu văn bản Shakespeare:

from pathlib import Path
import urllib.request

def download_shakespeare_text():
    path = Path("datasets/shakespeare/shakespeare.txt")
    if not path.is_file():
        path.parent.mkdir(parents=True, exist_ok=True)
        url = "https://homl.info/shakespeare"
        urllib.request.urlretrieve(url, path)
    return path.read_text()

shakespeare_text = download_shakespeare_text()
# mã bổ sung – hiển thị một đoạn văn bản mẫu
print(shakespeare_text[:80])
output:
    First Citizen:
    Before we proceed any further, hear me speak.
    
    All:
    Speak, speak.

Tokenization cấp ký tự (Character-level Tokenization)

Xây dựng bộ từ điển (vocabulary) gồm các ký tự duy nhất và tạo ánh xạ hai chiều char <-> id.

vocab = sorted(set(shakespeare_text.lower()))
"".join(vocab)
output:
    "\n !$&',-.3:;?abcdefghijklmnopqrstuvwxyz"
char_to_id = {char: index for index, char in enumerate(vocab)}
id_to_char = {index: char for index, char in enumerate(vocab)}
char_to_id["a"]
output:
    13
id_to_char[13]
output:
    'a'

Hàm mã hóa và giải mã văn bản:

import torch

def encode_text(text):
    # Chuyển văn bản thành tensor các ID (chữ thường)
    return torch.tensor([char_to_id[char] for char in text.lower()])

def decode_text(char_ids):
    # Chuyển tensor các ID thành chuỗi văn bản
    return "".join([id_to_char[char_id.item()] for char_id in char_ids])
encoded = encode_text("Hello, world!")
encoded
output:
    tensor([20, 17, 24, 24, 27,  6,  1, 35, 27, 30, 24, 16,  2])
decode_text(encoded)
output:
    'hello, world!'

Chuẩn bị dữ liệu cho RNN (Sliding Window)

Sử dụng phương pháp cửa sổ trượt để tạo các cặp (input, target). Dataset trả về:

  • window: Chuỗi ký tự từ tt đến t+L1t + L - 1.
  • target: Chuỗi ký tự từ t+1t+1 đến t+Lt + L. (Dự đoán ký tự tiếp theo tại mọi bước thời gian).
from torch.utils.data import Dataset, DataLoader

class CharDataset(Dataset):
    def __init__(self, text, window_length):
        self.encoded_text = encode_text(text)
        self.window_length = window_length

    def __len__(self):
        # Số lượng mẫu dữ liệu có thể tạo ra
        return len(self.encoded_text) - self.window_length

    def __getitem__(self, idx):
        if idx >= len(self):
            raise IndexError("dataset index out of range")
        end = idx + self.window_length
        # Cắt cửa sổ cho đầu vào
        window = self.encoded_text[idx : end]
        # Cắt cửa sổ cho mục tiêu (dịch đi 1 ký tự)
        target = self.encoded_text[idx + 1 : end + 1]
        return window, target
# mã bổ sung – ví dụ đơn giản sử dụng CharDataset
to_be_dataset = CharDataset("To be or not to be", window_length=10)
for x, y in to_be_dataset:
    print(f"x={x}, y={y}")
    print(f"    decoded: x={decode_text(x)!r}, y={decode_text(y)!r}")
output:
    x=tensor([32, 27,  1, 14, 17,  1, 27, 30,  1, 26]), y=tensor([27,  1, 14, 17,  1, 27, 30,  1, 26, 27])
        decoded: x='to be or n', y='o be or no'
    ...

Chia dữ liệu và tạo DataLoader:

window_length = 50
batch_size = 512  # Giảm nếu GPU của bạn không đủ bộ nhớ

# Chia dữ liệu: 1M ký tự đầu cho train, tiếp theo cho valid, còn lại cho test
train_set = CharDataset(shakespeare_text[:1_000_000], window_length)
valid_set = CharDataset(shakespeare_text[1_000_000:1_060_000], window_length)
test_set = CharDataset(shakespeare_text[1_060_000:], window_length)

train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
valid_loader = DataLoader(valid_set, batch_size=batch_size)
test_loader = DataLoader(test_set, batch_size=batch_size)

Lớp Embedding (Embedding Layer)

nn.Embedding là một ma trận trọng số V×DV \times D (V: vocab size, D: embedding dimension). Nó giúp mô hình học biểu diễn vector dày đặc của ký tự thay vì dùng one-hot vector thưa thớt.

import torch.nn as nn

torch.manual_seed(42)
embed = nn.Embedding(5, 3)  # 5 danh mục (từ vựng) x 3 chiều embedding
embed(torch.tensor([[3, 2], [0, 2]])) # Lấy vector embedding cho các ID
output:
    tensor([[[ 0.2674,  0.5349,  0.8094],
             [ 2.2082, -0.6380,  0.4617]],
            [[ 0.3367,  0.1288,  0.2345],
             [ 2.2082, -0.6380,  0.4617]]], grad_fn=<EmbeddingBackward0>)

Xây dựng và Huấn luyện Mô hình Char-RNN

Mô hình sử dụng GRU, một biến thể của RNN giúp giải quyết vấn đề Vanishing Gradient tốt hơn RNN thuần nhưng đơn giản hơn LSTM.

class ShakespeareModel(nn.Module):
    def __init__(self, vocab_size, n_layers=2, embed_dim=10, hidden_dim=128,
                 dropout=0.1):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim)
        # GRU: Gated Recurrent Unit
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout)
        self.output = nn.Linear(hidden_dim, vocab_size)

    def forward(self, X):
        embeddings = self.embed(X)
        # GRU trả về (outputs, hidden_states). Ta chỉ cần outputs.
        outputs, _states = self.gru(embeddings)
        # Biến đổi output để phù hợp với hàm loss (batch, vocab, time)
        return self.output(outputs).permute(0, 2, 1)

torch.manual_seed(42)
model = ShakespeareModel(len(vocab)).to(device)

Huấn luyện mô hình:

n_epochs = 20
xentropy = nn.CrossEntropyLoss()
optimizer = torch.optim.NAdam(model.parameters())
accuracy = torchmetrics.Accuracy(task="multiclass",
                                 num_classes=len(vocab)).to(device)

history = train(model, optimizer, xentropy, accuracy, train_loader, valid_loader,
                n_epochs)
output:
    Epoch 1/20,                      train loss: 1.6036, train metric: 51.31%, valid metric: 51.05%
    ...
    Epoch 20/20,                      train loss: 1.2965, train metric: 58.96%, valid metric: 54.81%
# Lưu trạng thái mô hình
torch.save(model.state_dict(), "my_shakespeare_model.pt")

Thử nghiệm dự đoán:

model.eval()  # Đừng quên chuyển sang chế độ đánh giá!
text = "To be or not to b"
encoded_text = encode_text(text).unsqueeze(dim=0).to(device)
with torch.no_grad():
    Y_logits = model(encoded_text)
    # Lấy dự đoán cho bước thời gian cuối cùng (-1)
    predicted_char_id = Y_logits[0, :, -1].argmax().item()
    predicted_char = id_to_char[predicted_char_id]  # Dự đoán chính xác là "e"
predicted_char
output:
    'e'

Sinh văn bản theo phong cách Shakespeare

Chúng ta sử dụng kỹ thuật Temperature Sampling để sinh văn bản. Thay vì chọn ký tự có xác suất cao nhất (argmax), ta lấy mẫu ngẫu nhiên dựa trên phân phối xác suất đã được điều chỉnh bởi nhiệt độ TT.

  • TT thấp: Ít ngẫu nhiên, an toàn nhưng nhàm chán.
  • TT cao: Ngẫu nhiên cao, sáng tạo nhưng dễ sai chính tả/ngữ pháp.
torch.manual_seed(42)
probs = torch.tensor([[0.5, 0.4, 0.1]])  # xác suất = 50%, 40%, và 10%
samples = torch.multinomial(probs, replacement=True, num_samples=8)
samples
output:
    tensor([[0, 0, 0, 0, 1, 0, 2, 2]])
import torch.nn.functional as F

def next_char(model, text, temperature=1):
    encoded_text = encode_text(text).unsqueeze(dim=0).to(device)
    with torch.no_grad():
        Y_logits = model(encoded_text)
        # Áp dụng temperature và softmax
        Y_probas = F.softmax(Y_logits[0, :, -1] / temperature, dim=-1)
        # Lấy mẫu từ phân phối xác suất
        predicted_char_id = torch.multinomial(Y_probas, num_samples=1).item()
    return id_to_char[predicted_char_id]
def extend_text(model, text, n_chars=80, temperature=1):
    for _ in range(n_chars):
        text += next_char(model, text, temperature)
    return text

Thử nghiệm với các nhiệt độ khác nhau:

# Nhiệt độ thấp: văn bản an toàn nhưng có thể lặp lại
print(extend_text(model, "To be or not to b", temperature=0.01))
output:
    To be or not to be the court?
    
    coriolanus:
    the state to the court, the state to the court?
    
    corio
# Nhiệt độ trung bình: cân bằng giữa sáng tạo và đúng ngữ pháp
print(extend_text(model, "To be or not to b", temperature=0.4))
output:
    To be or not to be so deputy,
    which i see the truth of her father will i do the shepherd's mother
# Nhiệt độ quá cao: văn bản trở nên vô nghĩa
print(extend_text(model, "To be or not to b", temperature=100))
output:
    To be or not to bmhf:my:rtk;s-h cqvvnfnfsut&-oq'ryoeen?x-hp:d,y&wv f3,dzrdzj-pilv?xpzh,fborp;'?$u

Huấn luyện RNN có trạng thái (Stateful RNN)

Trong Stateless RNN (mặc định), trạng thái ẩn (hidden state) được reset về 0 ở đầu mỗi batch. Stateful RNN bảo lưu trạng thái ẩn giữa các batch, cho phép mô hình học các phụ thuộc dài hạn vượt qua kích thước cửa sổ.

Yêu cầu kỹ thuật:

  1. Dữ liệu tuần tự: Batch i+1i+1 phải nối tiếp ngay sau Batch ii. Không được shuffle dữ liệu.
  2. Quản lý Hidden State: Phải lưu trữ và truyền hidden state thủ công, đồng thời .detach() nó để tránh lan truyền ngược gradient quá xa (tràn bộ nhớ).
class StatefulShakespeareModel(nn.Module):
    def __init__(self, vocab_size, n_layers=2, embed_dim=10, hidden_dim=128,
                 dropout=0.1):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim)
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout)
        self.output = nn.Linear(hidden_dim, vocab_size)
        self.hidden_states = None

    def forward(self, X):
        embeddings = self.embed(X)
        # Truyền hidden_states cũ vào GRU
        outputs, hidden_states = self.gru(embeddings, self.hidden_states)
        # Lưu trạng thái mới nhưng ngắt gradient (.detach())
        self.hidden_states = hidden_states.detach()
        return self.output(outputs).permute(0, 2, 1)

Chuẩn bị Dataset đặc biệt cho Stateful RNN:

from torch.utils.data import Dataset, DataLoader

class StatefulCharDataset(Dataset):
    def __init__(self, text, window_length, batch_size):
        self.encoded_text = encode_text(text)
        self.window_length = window_length
        self.batch_size = batch_size
        # Tính toán số lượng cửa sổ có thể tạo ra
        n_consecutive_windows = (len(self.encoded_text) - 1) // window_length
        n_windows_per_slot = n_consecutive_windows // batch_size
        self.length = n_windows_per_slot * batch_size
        self.spacing = n_windows_per_slot * window_length

    def __len__(self):
        return self.length

    def __getitem__(self, idx):
        if idx >= len(self):
            raise IndexError("dataset index out of range")
        # Công thức tính chỉ mục bắt đầu để đảm bảo tính liên tục giữa các batch
        start = ((idx % self.batch_size) * self.spacing
                 +(idx // self.batch_size) * self.window_length)
        end = start + self.window_length
        window = self.encoded_text[start : end]
        target = self.encoded_text[start + 1 : end + 1]
        return window, target
batch_size = 128
stateful_train_set = StatefulCharDataset(shakespeare_text[:1_000_000],
                                         window_length, batch_size)
stateful_train_loader = DataLoader(stateful_train_set, batch_size=batch_size,
                                   drop_last=True)
stateful_valid_set = StatefulCharDataset(shakespeare_text[1_000_000:1_060_000],
                                         window_length, batch_size)
stateful_valid_loader = DataLoader(stateful_valid_set, batch_size=batch_size,
                                   drop_last=True)
stateful_test_set = StatefulCharDataset(shakespeare_text[1_060_000:],
                                        window_length, batch_size)
stateful_test_loader = DataLoader(stateful_test_set, batch_size=batch_size,
                                  drop_last=True)
torch.manual_seed(42)

stateful_model = StatefulShakespeareModel(len(vocab)).to(device)

n_epochs = 10
xentropy = nn.CrossEntropyLoss()
optimizer = torch.optim.NAdam(stateful_model.parameters())
accuracy = torchmetrics.Accuracy(task="multiclass",
                                 num_classes=len(vocab)).to(device)

def reset_hidden_states(model, epoch):
    model.hidden_states = None

history = train(stateful_model, optimizer, xentropy, accuracy, stateful_train_loader,
                stateful_valid_loader, n_epochs, epoch_callback=reset_hidden_states)
output:
    Epoch 1/10,                      train loss: 2.4737, train metric: 29.86%, valid metric: 39.11%
    ...
    Epoch 10/10,                      train loss: 1.4236, train metric: 56.11%, valid metric: 53.60%
torch.save(stateful_model.state_dict(), "my_stateful_shakespeare_model.pt")

Sinh văn bản với Stateful RNN:

def extend_text_with_stateful_rnn(model, text, n_chars=80, temperature=1):
    model.hidden_states = None
    rnn_input = text
    for _ in range(n_chars):
        char = next_char(model, rnn_input, temperature)
        text += char
        rnn_input = char # Đầu vào tiếp theo chỉ cần là ký tự vừa sinh ra
    return text + ""
torch.manual_seed(42)
stateful_model.eval()
print(extend_text_with_stateful_rnn(stateful_model, "To be or not to b",
                                    temperature=0.1))
output:
    To be or not to be so the consent.
    
    clarence:
    and the content the strange and the consent.
    
    clare…
print(extend_text_with_stateful_rnn(stateful_model, "To be or not to b", temperature=0.4))
output:
    To be or not to be so.
    
    second citizen:
    and the good person the finds and proppee.
    
    henry bolingb…
print(extend_text_with_stateful_rnn(stateful_model, "To be or not to b", temperature=1))
output:
    To be or not to be fouth,
    we please no flote--rest enough, greeting:
    hath do self? that born;'? g…
Out.clear()  # Xóa biến `Out` của Jupyter lưu trữ output của cell
del_vars(["accuracy", "embed", "encoded", "encoded_text", "optimizer", "probs",
          "samples", "x", "y", "shakespeare_text", "stateful_test_loader",
          "stateful_train_loader", "Y_logits", "stateful_valid_loader",
          "test_loader", "train_loader", "valid_loader", "xentropy"])

Phân tích Cảm xúc (Sentiment Analysis)

Tải tập dữ liệu IMDB

from datasets import load_dataset

imdb_dataset = load_dataset("imdb")
split = imdb_dataset["train"].train_test_split(train_size=0.8, seed=42)
imdb_train_set, imdb_valid_set = split["train"], split["test"]
imdb_test_set = imdb_dataset["test"]
output:
    README.md: 0.00B [00:00, ?B/s]
    ...
    Generating unsupervised split:   0%|          | 0/50000 [00:00<?, ? examples/s]
imdb_train_set[1]["text"]
output:
    "'The Rookie' was a wonderful movie about the second chances life holds for us and also puts an emotional thought over the audience, making them realize that your dreams can come true. If you loved 'Remember the Titans', 'The Rookie' is the movie for you!! It's the feel good movie of the year and it is the perfect movie for all ages. 'The Rookie' hits a major home run!"
imdb_train_set[1]["label"] # 0: Negative, 1: Positive
output:
    1

Tokenization sử dụng thư viện tokenizers

Chúng ta sẽ sử dụng Subword Tokenization (BPE) để cân bằng giữa kích thước từ vựng và khả năng biểu diễn ngữ nghĩa.

Huấn luyện BPE Tokenizer trên tập IMDB

import tokenizers

bpe_model = tokenizers.models.BPE(unk_token="<unk>")
bpe_tokenizer = tokenizers.Tokenizer(bpe_model)
# Pre-tokenizer: Tách từ cơ bản bằng khoảng trắng
bpe_tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.Whitespace()
special_tokens = ["<pad>", "<unk>"]
bpe_trainer = tokenizers.trainers.BpeTrainer(vocab_size=1000,
                                             special_tokens=special_tokens)
train_reviews = [review["text"].lower() for review in imdb_train_set]
bpe_tokenizer.train_from_iterator(train_reviews, bpe_trainer)
tokenizers.pre_tokenizers.Whitespace().pre_tokenize_str("Hello, world!!!")
output:
    [('Hello', (0, 5)), (',', (5, 6)), ('world', (7, 12)), ('!!!', (12, 15))]

Mã hóa (Encoding) và Giải mã (Decoding) văn bản

some_review = "what an awesome movie! 😊"
bpe_encoding = bpe_tokenizer.encode(some_review)
bpe_encoding
output:
    Encoding(num_tokens=8, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])
bpe_encoding.tokens
output:
    ['what', 'an', 'aw', 'es', 'ome', 'movie', '!', '<unk>']
bpe_token_ids = bpe_encoding.ids
bpe_token_ids
output:
    [303, 139, 373, 149, 240, 211, 4, 1]
bpe_tokenizer.decode(bpe_token_ids)
output:
    'what an aw es ome movie !'

Xử lý Batch

bpe_tokenizer.enable_padding(pad_id=0, pad_token="<pad>")
bpe_tokenizer.enable_truncation(max_length=500)
bpe_encodings = bpe_tokenizer.encode_batch(train_reviews[:3])
bpe_batch_ids = torch.tensor([encoding.ids for encoding in bpe_encodings])
bpe_batch_ids
output:
    tensor([[159, 402, 176, 246,  61, 782, 156, 737, 252,  42, 239,  51, 154, 460,
             ...
             841,   4,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
             ...
              22,  17,  24,  18,  24]])
# Attention mask cho biết token nào là thật (1), token nào là padding (0)
attention_mask = torch.tensor([encoding.attention_mask
                               for encoding in bpe_encodings])
attention_mask
output:
    tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
             ...
             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

Dưới đây là các phương pháp tokenize khác:

# Byte-Level BPE
bbpe_model = tokenizers.models.BPE(unk_token="<unk>")
bbpe_tokenizer = tokenizers.Tokenizer(bbpe_model)
bbpe_tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.ByteLevel()
bbpe_trainer = tokenizers.trainers.BpeTrainer(vocab_size=1000,
                                              special_tokens=special_tokens)
bbpe_tokenizer.train_from_iterator(train_reviews, bbpe_trainer)
# WordPiece
wp_model = tokenizers.models.WordPiece(unk_token="<unk>")
wp_tokenizer = tokenizers.Tokenizer(wp_model)
wp_tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.Whitespace()
wp_trainer = tokenizers.trainers.WordPieceTrainer(vocab_size=1000,
                                                  special_tokens=special_tokens)
wp_tokenizer.train_from_iterator(train_reviews, wp_trainer)
# Unigram
unigram_model = tokenizers.models.Unigram()
unigram_tokenizer = tokenizers.Tokenizer(unigram_model)
unigram_tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.Whitespace()
unigram_trainer = tokenizers.trainers.UnigramTrainer(
    vocab_size=1000, special_tokens=special_tokens, unk_token="<unk>")
unigram_tokenizer.train_from_iterator(train_reviews, unigram_trainer)

Sử dụng Tokenizer đã huấn luyện sẵn (Pretrained Tokenizers)

import transformers

# Tải tokenizer của BERT
bert_tokenizer = transformers.AutoTokenizer.from_pretrained("bert-base-uncased")
bert_encoding = bert_tokenizer(train_reviews[:3], padding=True,
                               truncation=True, max_length=500,
                               return_tensors="pt")
output:
    tokenizer_config.json:   0%|          | 0.00/48.0 [00:00<?, ?B/s]
    config.json:   0%|          | 0.00/570 [00:00<?, ?B/s]
    vocab.txt:   0%|          | 0.00/232k [00:00<?, ?B/s]
    tokenizer.json:   0%|          | 0.00/466k [00:00<?, ?B/s]

Xây dựng và Huấn luyện Mô hình Phân tích Cảm xúc

Hàm collate_fn để xử lý batch động:

def collate_fn(batch, tokenizer=bert_tokenizer):
    # Hàm này được DataLoader dùng để gom các mẫu thành batch
    reviews = [review["text"] for review in batch]
    labels = [[review["label"]] for review in batch]
    # Tokenize và padding động
    encodings = tokenizer(reviews, padding=True, truncation=True,
                          max_length=200, return_tensors="pt")
    labels = torch.tensor(labels, dtype=torch.float32)
    return encodings, labels

batch_size = 256
imdb_train_loader = DataLoader(imdb_train_set, batch_size=batch_size,
                               collate_fn=collate_fn, shuffle=True)
imdb_valid_loader = DataLoader(imdb_valid_set, batch_size=batch_size,
                               collate_fn=collate_fn)
imdb_test_loader = DataLoader(imdb_test_set, batch_size=batch_size,
                              collate_fn=collate_fn)

Mô hình 1: Cơ bản (Basic GRU)

class SentimentAnalysisModel(nn.Module):
    def __init__(self, vocab_size, n_layers=2, embed_dim=128, hidden_dim=64,
                 pad_id=0, dropout=0.2):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim,
                                  padding_idx=pad_id)
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout)
        self.output = nn.Linear(hidden_dim, 1)

    def forward(self, encodings):
        embeddings = self.embed(encodings["input_ids"])
        # _outputs chứa output tại mọi bước thời gian
        # hidden_states chứa trạng thái ẩn cuối cùng của các layer
        _outputs, hidden_states = self.gru(embeddings)
        # Lấy hidden state của layer cuối cùng
        return self.output(hidden_states[-1])

Mô hình 2: Sử dụng Packed Sequences

pack_padded_sequence giúp RNN bỏ qua các token padding, tăng tốc độ tính toán.

from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

class SentimentAnalysisModelPackedSeq(nn.Module):
    def __init__(self, vocab_size, n_layers=2, embed_dim=128,
                 hidden_dim=64, pad_id=0, dropout=0.2):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim,
                                  padding_idx=pad_id)
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout)
        self.output = nn.Linear(hidden_dim, 1)

    def forward(self, encodings):
        embeddings = self.embed(encodings["input_ids"])
        # Tính độ dài thực của từng câu dựa trên attention mask
        lengths = encodings["attention_mask"].sum(dim=1)
        # Đóng gói chuỗi
        packed = pack_padded_sequence(embeddings, lengths=lengths.cpu(),
                                      batch_first=True, enforce_sorted=False)
        _outputs, hidden_states = self.gru(packed)
        return self.output(hidden_states[-1])
torch.manual_seed(42)

vocab_size = bert_tokenizer.vocab_size
imdb_model_ps = SentimentAnalysisModelPackedSeq(vocab_size).to(device)

n_epochs = 10
xentropy = nn.BCEWithLogitsLoss() # Hàm loss cho phân loại nhị phân
optimizer = torch.optim.NAdam(imdb_model_ps.parameters())
accuracy = torchmetrics.Accuracy(task="binary").to(device)

history = train(imdb_model_ps, optimizer, xentropy, accuracy,
                imdb_train_loader, imdb_valid_loader, n_epochs)
output:
    Epoch 1/10,                      train loss: 0.6706, train metric: 58.64%, valid metric: 53.20%
    ...
    Epoch 10/10,                      train loss: 0.0257, train metric: 99.49%, valid metric: 81.18%

Mô hình 3: Bidirectional RNN (BiRNN)

BiRNN đọc chuỗi theo cả hai chiều, giúp nắm bắt ngữ cảnh tốt hơn (ví dụ: từ “bank” trong “river bank” vs “bank account”).

class SentimentAnalysisModelBidi(nn.Module):
    def __init__(self, vocab_size, n_layers=2, embed_dim=128,
                 hidden_dim=64, pad_id=0, dropout=0.2):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim,
                                  padding_idx=pad_id)
        # Thêm tham số bidirectional=True
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout, bidirectional=True)
        # Input của lớp linear tăng gấp đôi (2 * hidden_dim)
        self.output = nn.Linear(2 * hidden_dim, 1)

    def forward(self, encodings):
        embeddings = self.embed(encodings["input_ids"])
        lengths = encodings["attention_mask"].sum(dim=1)
        packed = pack_padded_sequence(embeddings, lengths=lengths.cpu(),
                                      batch_first=True, enforce_sorted=False)
        _outputs, hidden_states = self.gru(packed)
        # Lấy 2 hidden states cuối cùng (của lớp RNN cuối cùng, cả chiều xuôi và ngược)
        n_dims = self.output.in_features
        top_states = hidden_states[-2:].permute(1, 0, 2).reshape(-1, n_dims)
        return self.output(top_states)
torch.manual_seed(42)
imdb_model_bidi = SentimentAnalysisModelBidi(vocab_size).to(device)
optimizer = torch.optim.NAdam(imdb_model_bidi.parameters())
history = train(imdb_model_bidi, optimizer, xentropy, accuracy, imdb_train_loader,
                imdb_valid_loader, n_epochs)
output:
    Epoch 1/10,                      train loss: 0.6565, train metric: 59.79%, valid metric: 65.00%
    ...
    Epoch 10/10,                      train loss: 0.0087, train metric: 99.85%, valid metric: 84.04%
Out.clear()
del_vars(["imdb_model_ps", "imdb_model_bidi"])

Tái sử dụng Pretrained Embeddings và Language Models

Sử dụng BERT như một bộ trích xuất đặc trưng (feature extractor) mạnh mẽ.

class SentimentAnalysisModelBert(nn.Module):
    def __init__(self, n_layers=2, hidden_dim=64, dropout=0.2):
        super().__init__()
        # Tải mô hình BERT đã huấn luyện sẵn
        self.bert = transformers.AutoModel.from_pretrained("bert-base-uncased")
        embed_dim = self.bert.config.hidden_size
        self.gru = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                          batch_first=True, dropout=dropout)
        self.output = nn.Linear(hidden_dim, 1)

    def forward(self, encodings):
        # Lấy output từ BERT (contextualized embeddings)
        contextualized_embeddings = self.bert(**encodings).last_hidden_state
        lengths = encodings["attention_mask"].sum(dim=1)
        packed = pack_padded_sequence(contextualized_embeddings, lengths=lengths.cpu(),
                                      batch_first=True, enforce_sorted=False)
        _outputs, hidden_states = self.gru(packed)
        return self.output(hidden_states[-1])
torch.manual_seed(42)
imdb_model_bert = SentimentAnalysisModelBert().to(device)
# Đóng băng (freeze) trọng số của BERT để chỉ huấn luyện phần RNN và Linear
imdb_model_bert.bert.requires_grad_(False)

n_epochs = 4
optimizer = torch.optim.NAdam(imdb_model_bert.parameters())
history = train(imdb_model_bert, optimizer, xentropy, accuracy,
                imdb_train_loader, imdb_valid_loader, n_epochs)
output:
    Epoch 1/4,                      train loss: 0.4794, train metric: 75.81%, valid metric: 87.48%
    ...
    Epoch 4/4,                      train loss: 0.2504, train metric: 89.50%, valid metric: 84.98%

Nhiệm vụ Dịch máy (Neural Machine Translation) với Encoder-Decoder

Kiến trúc Encoder-Decoder

Chúng ta sử dụng kiến trúc Seq2Seq cơ bản, nơi Encoder nén câu nguồn thành một context vector, và Decoder giải mã nó thành câu đích.

# Tải dữ liệu Anh-Tây Ban Nha
nmt_original_valid_set, nmt_test_set = load_dataset(
    path="ageron/tatoeba_mt_train", name="eng-spa",
    split=["validation", "test"])
split = nmt_original_valid_set.train_test_split(train_size=0.8, seed=42)
nmt_train_set, nmt_valid_set = split["train"], split["test"]
output:
    README.md: 0.00B [00:00, ?B/s]
    ...
    Generating test split:   0%|          | 0/24514 [00:00<?, ? examples/s]

Xây dựng Tokenizer chung cho cả 2 ngôn ngữ (để đơn giản):

def train_eng_spa():  # Generator function cho dữ liệu huấn luyện
    for pair in nmt_train_set:
        yield pair["source_text"]
        yield pair["target_text"]

max_length = 256
vocab_size = 10_000

nmt_tokenizer_model = tokenizers.models.BPE(unk_token="<unk>")
nmt_tokenizer = tokenizers.Tokenizer(nmt_tokenizer_model)
nmt_tokenizer.enable_padding(pad_id=0, pad_token="<pad>")
nmt_tokenizer.enable_truncation(max_length=max_length)
nmt_tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.Whitespace()
# Thêm các token đặc biệt: <s> (bắt đầu câu), </s> (kết thúc câu)
nmt_tokenizer_trainer = tokenizers.trainers.BpeTrainer(
    vocab_size=vocab_size, special_tokens=["<pad>", "<unk>", "<s>", "</s>"])
nmt_tokenizer.train_from_iterator(train_eng_spa(), nmt_tokenizer_trainer)
from collections import namedtuple

fields = ["src_token_ids", "src_mask", "tgt_token_ids", "tgt_mask"]
class NmtPair(namedtuple("NmtPairBase", fields)):
    def to(self, device):
        return NmtPair(self.src_token_ids.to(device), self.src_mask.to(device),
                       self.tgt_token_ids.to(device), self.tgt_mask.to(device))

Hàm collate_fn chuẩn bị dữ liệu: Input decoder là chuỗi đích dịch đi 1 bước thời gian (Teacher Forcing).

def nmt_collate_fn(batch):
    src_texts = [pair['source_text'] for pair in batch]
    # Thêm token bắt đầu và kết thúc cho chuỗi đích
    tgt_texts = [f"<s> {pair['target_text']} </s>" for pair in batch]
    src_encodings = nmt_tokenizer.encode_batch(src_texts)
    tgt_encodings = nmt_tokenizer.encode_batch(tgt_texts)
    src_token_ids = torch.tensor([enc.ids for enc in src_encodings])
    tgt_token_ids = torch.tensor([enc.ids for enc in tgt_encodings])
    src_mask = torch.tensor([enc.attention_mask for enc in src_encodings])
    tgt_mask = torch.tensor([enc.attention_mask for enc in tgt_encodings])
    # Inputs cho decoder bỏ token cuối (</s>)
    inputs = NmtPair(src_token_ids, src_mask,
                     tgt_token_ids[:, :-1], tgt_mask[:, :-1])
    # Labels cho decoder bỏ token đầu (<s>)
    labels = tgt_token_ids[:, 1:]
    return inputs, labels

batch_size = 32
nmt_train_loader = DataLoader(nmt_train_set, batch_size=batch_size,
                              collate_fn=nmt_collate_fn, shuffle=True)
nmt_valid_loader = DataLoader(nmt_valid_set, batch_size=batch_size,
                              collate_fn=nmt_collate_fn)
nmt_test_loader = DataLoader(nmt_test_set, batch_size=batch_size,
                             collate_fn=nmt_collate_fn)
class NmtModel(nn.Module):
    def __init__(self, vocab_size, embed_dim=512, pad_id=0, hidden_dim=512,
                 n_layers=2):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_id)
        # Encoder
        self.encoder = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                              batch_first=True)
        # Decoder
        self.decoder = nn.GRU(embed_dim, hidden_dim, num_layers=n_layers,
                              batch_first=True)
        self.output = nn.Linear(hidden_dim, vocab_size)

    def forward(self, pair):
        src_embeddings = self.embed(pair.src_token_ids)
        tgt_embeddings = self.embed(pair.tgt_token_ids)
        src_lengths = pair.src_mask.sum(dim=1)

        # Đóng gói input encoder
        src_packed = pack_padded_sequence(
            src_embeddings, lengths=src_lengths.cpu(),
            batch_first=True, enforce_sorted=False)

        # Encoder xử lý, trả về hidden_states cuối cùng
        _, hidden_states = self.encoder(src_packed)

        # Decoder nhận hidden_states của Encoder làm trạng thái khởi tạo
        outputs, _ = self.decoder(tgt_embeddings, hidden_states)
        return self.output(outputs).permute(0, 2, 1)

torch.manual_seed(42)
vocab_size = nmt_tokenizer.get_vocab_size()
nmt_model = NmtModel(vocab_size).to(device)
n_epochs = 10
xentropy = nn.CrossEntropyLoss(ignore_index=0)  # Bỏ qua token <pad>
optimizer = torch.optim.NAdam(nmt_model.parameters(), lr=0.001)
accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=vocab_size)
accuracy = accuracy.to(device)

history = train(nmt_model, optimizer, xentropy, accuracy,
                nmt_train_loader, nmt_valid_loader, n_epochs)
output:
    Epoch 1/10,                      train loss: 3.1343, train metric: 17.34%, valid metric: 20.33%
    ...
    Epoch 10/10,                      train loss: 0.6600, train metric: 30.36%, valid metric: 22.14%

Beam Search (Tìm kiếm chùm)

Beam Search giúp tìm chuỗi dịch có xác suất tổng thể cao nhất, thay vì tham lam chọn từ tốt nhất ở mỗi bước.

import torch.nn.functional as F

def beam_search(model, src_text, beam_width=3, max_length=20,
                verbose=False, length_penalty=0.6):
    # Khởi tạo: danh sách chứa (log_proba, text)
    top_translations = [(torch.tensor(0.), "")]

    for index in range(max_length):
        candidates = []
        for log_proba, tgt_text in top_translations:
            if tgt_text.endswith(" </s>"):
                candidates.append((log_proba, tgt_text))
                continue

            # Dự đoán từ tiếp theo cho mỗi ứng viên hiện tại
            batch, _ = nmt_collate_fn([{"source_text": src_text,
                                        "target_text": tgt_text}])
            with torch.no_grad():
                Y_logits = model(batch.to(device))
                Y_log_proba = F.log_softmax(Y_logits, dim=1)
                # Lấy top k từ có xác suất cao nhất
                Y_top_log_probas = torch.topk(Y_log_proba, k=beam_width, dim=1)

            for beam_index in range(beam_width):
                next_token_log_proba = Y_top_log_probas.values[0, beam_index, index]
                next_token_id = Y_top_log_probas.indices[0, beam_index, index]
                next_token = nmt_tokenizer.id_to_token(next_token_id)
                next_tgt_text = tgt_text + " " + next_token
                # Cộng dồn log xác suất
                candidates.append((log_proba + next_token_log_proba, next_tgt_text))

        # Hàm phạt độ dài để tránh ưu tiên các câu quá ngắn
        def length_penalized_score(candidate, alpha=length_penalty):
            log_proba, text = candidate
            length = len(text.split())
            penalty = ((5 + length) ** alpha) / (6 ** alpha)
            return log_proba / penalty

        # Sắp xếp và chỉ giữ lại top k ứng viên tốt nhất
        top_translations = sorted(candidates,
                                  key=length_penalized_score,
                                  reverse=True)[:beam_width]

    return top_translations[-1][1]

Cơ chế Attention (Sự chú ý)

Cài đặt Luong Attention (Dot-Product Attention)

Công thức tổng quát: Attention(Q,K,V)=softmax(QKTdk)VAttention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V (Trong Luong Attention đơn giản, ta thường bỏ qua bước chia căn bậc hai của dkd_k).

def attention(query, key, value):
    # query: trạng thái của decoder [Batch, Lq, dq]
    # key/value: trạng thái của encoder [Batch, Lk, dk]

    # 1. Tính điểm số (Scores) bằng tích vô hướng (Dot product)
    scores = query @ key.transpose(1, 2)  # [B, Lq, Lk]

    # 2. Tính trọng số (Weights) bằng Softmax
    weights = torch.softmax(scores, dim=-1)  # [B, Lq, Lk]

    # 3. Tính vector ngữ cảnh (Context Vector) - Tổng trọng số của Value
    return weights @ value  # [B, Lq, dv]
class NmtModelWithAttention(nn.Module):
    def __init__(self, vocab_size, embed_dim=512, pad_id=0, hidden_dim=512,
                 n_layers=2):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_id)
        self.encoder = nn.GRU(
            embed_dim, hidden_dim, num_layers=n_layers, batch_first=True)
        self.decoder = nn.GRU(
            embed_dim, hidden_dim, num_layers=n_layers, batch_first=True)
        # Output layer nhận vào cả context vector và decoder output nên kích thước nhân đôi
        self.output = nn.Linear(2 * hidden_dim, vocab_size)

    def forward(self, pair):
        src_embeddings = self.embed(pair.src_token_ids)
        tgt_embeddings = self.embed(pair.tgt_token_ids)
        src_lengths = pair.src_mask.sum(dim=1)

        src_packed = pack_padded_sequence(
            src_embeddings, lengths=src_lengths.cpu(),
            batch_first=True, enforce_sorted=False)

        # Encoder trả về output tại TẤT CẢ các bước thời gian (để làm Key/Value cho Attention)
        encoder_outputs_packed, hidden_states = self.encoder(src_packed)

        # Decoder xử lý
        decoder_outputs, _ = self.decoder(tgt_embeddings, hidden_states)

        # Giải nén output của encoder
        encoder_outputs, _ = pad_packed_sequence(encoder_outputs_packed,
                                                 batch_first=True)

        # Áp dụng Attention
        # Query: Decoder outputs
        # Key/Value: Encoder outputs
        attn_output = attention(query=decoder_outputs,
                                key=encoder_outputs, value=encoder_outputs)

        # Nối output của attention và output của decoder
        combined_output = torch.cat((attn_output, decoder_outputs), dim=-1)
        return self.output(combined_output).permute(0, 2, 1)
torch.manual_seed(42)
nmt_attn_model = NmtModelWithAttention(vocab_size).to(device)

n_epochs = 10
xentropy = nn.CrossEntropyLoss(ignore_index=0)
optimizer = torch.optim.NAdam(nmt_attn_model.parameters(), lr=0.001)
accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=vocab_size)
accuracy = accuracy.to(device)

history = train(nmt_attn_model, optimizer, xentropy, accuracy,
                nmt_train_loader, nmt_valid_loader, n_epochs)
output:
    Epoch 1/10,                      train loss: 3.0097, train metric: 18.02%, valid metric: 20.35%
    ...
    Epoch 10/10,                      train loss: 1.1039, train metric: 27.00%, valid metric: 22.61%
torch.save(nmt_attn_model.state_dict(), "my_nmt_attn_model.pt")
del_vars(["nmt_attn_model"])

Khám phá Word Embeddings (Phần mở rộng)

Sử dụng BERT embeddings để tìm các từ tương đồng và giải các bài toán loại suy (analogy).

from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# Lấy ma trận embedding từ mô hình BERT
embedding_matrix = model.get_input_embeddings().weight.detach()
import torch.nn.functional as F

def get_embedding(token):
    token_id = tokenizer.vocab[token]
    return embedding_matrix[token_id]

def find_closest_tokens(token1, token2, token3, top_n=5):
    # Tính vector kết quả: E(token2) - E(token1) + E(token3)
    E = get_embedding
    result = E(token2) - E(token1) + E(token3)
    # Tính độ tương đồng cosine với toàn bộ từ vựng
    similarities = F.cosine_similarity(result, embedding_matrix)
    # Lấy top n token có độ tương đồng cao nhất
    top_k = torch.topk(similarities, k=top_n)
    return [(sim.item(), tokenizer.decode(idx.item()))
            for sim, idx in zip(top_k.values, top_k.indices)]
examples = [
    ("king", "queen", "man"),
    ("man", "woman", "nephew"),
    ("father", "mother", "son"),
    ("man", "woman", "doctor"),
    ("germany", "hitler", "italy"),
    ("england", "london", "germany"),
]
for (token1, token2, token3) in examples:
    print(f"{token1} is to {token2} as {token3} is to: ", end="")
    for similarity, token in find_closest_tokens(token1, token2, token3):
        print(f"{token} ({similarity:.1f})", end=" ")
    print()
output:
    king is to queen as man is to: man (0.7) woman (0.6) queen (0.5) girl (0.5) lady (0.5) 
    man is to woman as nephew is to: nephew (0.8) niece (0.8) granddaughter (0.7) grandson (0.7) daughters (0.6) 
    ...

Ôn tập

1. Ưu và nhược điểm của Stateful RNN so với Stateless RNN?

  • Stateless RNNs: Chỉ có thể nắm bắt các mẫu (patterns) có độ dài nhỏ hơn hoặc bằng kích thước cửa sổ (window size) dùng để huấn luyện. Chúng dễ cài đặt và huấn luyện hơn vì các batch độc lập với nhau.
  • Stateful RNNs: Có khả năng nắm bắt các phụ thuộc dài hạn vượt quá kích thước cửa sổ vì trạng thái ẩn được bảo lưu qua các batch. Tuy nhiên, việc cài đặt phức tạp hơn (dữ liệu phải được sắp xếp tuần tự, không được shuffle, kích thước batch cố định). Hơn nữa, dữ liệu không còn tính chất IID (Independent and Identically Distributed), điều này có thể gây khó khăn cho Gradient Descent trong việc hội tụ.

2. Tại sao ta thường dùng Encoder-Decoder cho dịch máy thay vì Seq2Seq đơn giản dịch từng từ?

Dịch từng từ (word-by-word) thường cho kết quả tồi tệ vì ngữ pháp và cấu trúc câu giữa các ngôn ngữ rất khác nhau (ví dụ: tiếng Anh đặt tính từ trước danh từ, tiếng Pháp thường ngược lại). Kiến trúc Encoder-Decoder đọc toàn bộ câu nguồn trước (Encoder) để hiểu ngữ cảnh tổng thể, sau đó mới bắt đầu sinh câu đích (Decoder). Điều này cho phép mô hình sắp xếp lại trật tự từ và chọn từ vựng phù hợp với ngữ cảnh.

3. Làm thế nào để xử lý chuỗi đầu vào có độ dài thay đổi? Và chuỗi đầu ra?

  • Đầu vào: Sử dụng padding (thêm token đặc biệt <pad>) để đưa tất cả chuỗi về cùng độ dài trong một batch. Sử dụng masking để báo cho mô hình bỏ qua các token padding này (như trong pack_padded_sequence hoặc tham số attention_mask).
  • Đầu ra: Nếu biết trước độ dài, ta có thể bỏ qua phần thừa. Nhưng thường ta không biết, nên giải pháp là huấn luyện mô hình sinh ra một token kết thúc đặc biệt (EOS - End Of Sequence). Khi infer, ta dừng lại khi gặp token này.

4. Beam Search là gì và tại sao nên dùng nó?

Beam Search là kỹ thuật tìm kiếm dùng trong quá trình sinh văn bản (decoding). Thay vì tham lam chọn từ có xác suất cao nhất tại mỗi bước (có thể dẫn đến tối ưu cục bộ nhưng sai lầm về tổng thể), Beam Search duy trì kk chuỗi tiềm năng nhất tại mọi thời điểm. Nó cân bằng giữa hiệu năng tính toán và chất lượng bản dịch. Nó giúp tìm ra câu có xác suất tổng thể cao hơn.

5. Cơ chế Attention là gì? Lợi ích chính của nó?

Attention là cơ chế cho phép Decoder truy cập trực tiếp vào toàn bộ chuỗi trạng thái ẩn của Encoder tại mỗi bước sinh từ, thay vì chỉ dựa vào một context vector duy nhất. Nó tính toán trọng số (độ quan tâm) cho từng từ trong câu nguồn.

  • Lợi ích: Giải quyết vấn đề “nút thắt cổ chai” thông tin với các câu dài. Cải thiện đáng kể hiệu suất dịch thuật. Giúp mô hình “diễn giải” được (interpretable) thông qua việc quan sát ma trận trọng số attention.

6. Sampled Softmax là gì?

Khi số lượng lớp (từ vựng) quá lớn (ví dụ: hàng trăm nghìn từ), việc tính Softmax trên toàn bộ từ vựng rất tốn kém. Sampled Softmax xấp xỉ hàm loss bằng cách chỉ tính toán trên từ đúng (target) và một mẫu ngẫu nhiên các từ sai (negative samples). Điều này giúp tăng tốc quá trình huấn luyện đáng kể.