[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 train và evaluate_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ừ đến .target: Chuỗi ký tự từ đến . (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: 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 độ .
- thấp: Ít ngẫu nhiên, an toàn nhưng nhàm chán.
- 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:
- Dữ liệu tuần tự: Batch phải nối tiếp ngay sau Batch . Không được shuffle dữ liệu.
- 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: (Trong Luong Attention đơn giản, ta thường bỏ qua bước chia căn bậc hai của ).
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ụngmaskingđể báo cho mô hình bỏ qua các token padding này (như trongpack_padded_sequencehoặ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ì 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ể.