GRU trong thực tế: so sánh, triển khai và debug
Mục tiêu
Sau bài này, bạn cần:
- so sánh GRU, LSTM và Vanilla RNN mà không rơi vào kết luận “mô hình nào luôn tốt nhất”;
- tính và kiểm tra số tham số recurrent của GRU/LSTM;
- đọc đúng
outputvàh_ntrongtorch.nn.GRU; - xây dựng GRU cho phân loại chuỗi và gán nhãn từng bước;
- xử lý chuỗi có độ dài khác nhau bằng padding/packing;
- hiểu tác động của bidirectional, stacked GRU, dropout và hidden-state reset;
- biết cách thiết kế một thí nghiệm công bằng giữa RNN, GRU và LSTM;
- có checklist debug trước khi tăng độ phức tạp của mô hình.
1. Câu hỏi đúng không phải là “GRU hay LSTM tốt hơn?”
Một câu hỏi thường gặp là:
Nên dùng GRU hay LSTM?
Nếu chỉ có tên hai kiến trúc, không thể trả lời chính xác.
Hiệu quả phụ thuộc vào:
- loại dữ liệu;
- độ dài chuỗi;
- lượng dữ liệu;
- hidden size;
- số tầng;
- cách regularization;
- optimizer và learning rate;
- metric;
- giới hạn latency/bộ nhớ;
- cách xử lý padding và batch;
- cách chia train/validation/test.
Các kết quả học thuật cũng không ủng hộ một luật kiểu “GRU luôn thắng LSTM”.
Chung et al. (2014) so sánh gated recurrent units trên bài toán mô hình hóa nhạc đa âm và tín hiệu tiếng nói, và báo cáo GRU có chất lượng comparable với LSTM trong các thí nghiệm của họ.
Jozefowicz et al. (2015) khảo sát hơn mười nghìn kiến trúc recurrent và tìm được các biến thể vượt cả LSTM lẫn GRU trên một số nhiệm vụ, nhưng không phải mọi nhiệm vụ.
Kết luận thực hành hợp lý là:
Chọn một baseline có lý do, kiểm soát thí nghiệm và đo trên validation/test phù hợp với bài toán.
2. So sánh cơ chế: Vanilla RNN, GRU và LSTM
| Tiêu chí | Vanilla RNN | GRU | LSTM |
|---|---|---|---|
| Trạng thái chính | |||
| Gating | không | reset + update | forget + input + output |
| Candidate | trạng thái mới trực tiếp | ||
| Đường giữ trạng thái | không có gate riêng | nội suy trực tiếp trong | cell state |
| Số nhóm affine cơ bản | 1 | 3 | 4 |
| Độ đơn giản | cao nhất | trung gian | phức tạp hơn |
Bảng này mô tả cấu trúc, không phải bảng xếp hạng chất lượng.
3. Tính số tham số để hiểu chữ “gọn”
Giả sử:
- input size là ;
- hidden size là ;
- một tầng;
- một chiều.
Trong PyTorch, mỗi gate thường có:
- input-hidden weight;
- hidden-hidden weight;
- input-hidden bias;
- hidden-hidden bias.
GRU
GRU có 3 nhóm: reset, update, candidate.
Số tham số:
hay:
LSTM
LSTM có 4 nhóm: input, forget, candidate, output.
hay:
Do đó, với cùng và :
Tức là cấu hình GRU cơ bản có 25% ít tham số recurrent hơn LSTM cùng input/hidden size.
Ví dụ cụ thể
Cho:
GRU:
LSTM:
Chênh lệch:
Đây là giảm tham số, không phải cam kết giảm đúng 25% thời gian chạy.
4. Kiểm tra số tham số bằng code
import torch
import torch.nn as nn
D = 32
H = 64
gru = nn.GRU(input_size=D, hidden_size=H, batch_first=True)
lstm = nn.LSTM(input_size=D, hidden_size=H, batch_first=True)
rnn = nn.RNN(input_size=D, hidden_size=H, batch_first=True)
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print("RNN :", count_parameters(rnn))
print("GRU :", count_parameters(gru))
print("LSTM:", count_parameters(lstm))
Khi đọc kết quả, đừng chỉ nói “GRU nhẹ”. Hãy xác định:
- nhẹ hơn bao nhiêu tham số;
- hidden size có giống nhau không;
- số tầng có giống nhau không;
- có bidirectional không.
5. Cùng số hidden unit chưa chắc là so sánh hoàn toàn công bằng
Nếu dùng cùng hidden_size, GRU có ít tham số hơn LSTM.
Điều này tạo hai cách so sánh khác nhau.
Cách A - cùng hidden size
Ví dụ:
GRU hidden=128
LSTM hidden=128
Mục tiêu: so sánh khi kích thước representation giống nhau.
Nhưng tổng số tham số khác nhau.
Cách B - cùng parameter budget
Ta có thể tăng hidden size của GRU hoặc giảm hidden size của LSTM để tổng số tham số gần nhau.
Mục tiêu: so sánh dưới cùng budget.
Hai thí nghiệm trả lời hai câu hỏi khác nhau. Paper hoặc báo cáo phải nói rõ cách nào được dùng.
6. Dạng tensor của torch.nn.GRU
Ví dụ:
import torch
import torch.nn as nn
gru = nn.GRU(
input_size=16,
hidden_size=32,
num_layers=2,
batch_first=True,
bidirectional=False,
)
x = torch.randn(8, 20, 16)
output, h_n = gru(x)
print(output.shape)
print(h_n.shape)
Kết quả:
output : (8, 20, 32)
h_n : (2, 8, 32)
Ý nghĩa:
8- batch size;20- sequence length;16- input feature;32- hidden size;2trongh_n- số tầng vì đang chạy một chiều.
Khác LSTM
LSTM trả về:
output, (h_n, c_n) = lstm(x)
GRU trả về:
output, h_n = gru(x)
Không có c_n riêng.
7. output và h_n chứa gì?
Với GRU một chiều:
output[:, t, :]
là hidden state của tầng cuối tại thời điểm .
Trong khi:
h_n[layer, batch, hidden]
chứa hidden state cuối của từng layer.
Nếu chỉ có một chiều và không padding phức tạp, output[:, -1, :] thường tương ứng trạng thái cuối của tầng cuối.
Nhưng khi:
- dùng bidirectional;
- dùng padding;
- dùng packing;
cần đọc shape và semantics cẩn thận hơn.
8. Bidirectional GRU
Nếu toàn bộ chuỗi đã có sẵn trước khi suy luận, có thể chạy GRU theo cả hai hướng.
Trong PyTorch:
gru = nn.GRU(
input_size=16,
hidden_size=32,
num_layers=2,
batch_first=True,
bidirectional=True,
)
x = torch.randn(8, 20, 16)
output, h_n = gru(x)
print(output.shape) # (8, 20, 64)
print(h_n.shape) # (4, 8, 32)
Vì:
(hai hướng), nên:
Cảnh báo future leakage
Bidirectional GRU dùng ngữ cảnh phía sau vị trí hiện tại.
Do đó không phù hợp nếu hệ thống phải ra quyết định online mà dữ liệu tương lai chưa tồn tại, ví dụ:
- cảnh báo cảm biến thời gian thực;
- dự đoán giá ở bước kế tiếp;
- nhận diện streaming cần trả kết quả ngay.
9. Stacked GRU
Ta có thể xếp nhiều recurrent layer.
Trong PyTorch:
gru = nn.GRU(
input_size=20,
hidden_size=64,
num_layers=3,
dropout=0.2,
batch_first=True,
)
Với num_layers > 1, dropout của nn.GRU được áp dụng giữa các recurrent layer theo semantics của PyTorch, không có nghĩa là dropout được chèn tùy ý vào mọi phép tính bên trong một GRU cell.
Tăng số tầng làm tăng năng lực biểu diễn nhưng cũng:
- tăng tham số;
- tăng latency;
- tăng nguy cơ overfitting;
- làm tối ưu khó hơn.
Không có lý do để bắt đầu ngay từ 4-5 tầng nếu một tầng nhỏ đã đủ làm baseline.
10. GRU cho phân loại toàn chuỗi
Ví dụ: từ chuỗi hoạt động của sinh viên, dự đoán một nhãn cuối như “có nguy cơ bỏ bài tuần này hay không”.
Kiến trúc cơ bản:
Một implementation đơn giản:
import torch
import torch.nn as nn
class GRUClassifier(nn.Module):
def __init__(self, input_size=6, hidden_size=64, num_classes=2):
super().__init__()
self.gru = nn.GRU(
input_size=input_size,
hidden_size=hidden_size,
num_layers=1,
batch_first=True,
)
self.classifier = nn.Linear(hidden_size, num_classes)
def forward(self, x):
output, h_n = self.gru(x)
last_hidden = h_n[-1]
logits = self.classifier(last_hidden)
return logits
model = GRUClassifier()
x = torch.randn(32, 15, 6)
logits = model(x)
print(logits.shape) # (32, 2)
Nếu tất cả chuỗi có cùng độ dài thật, cách này khá trực tiếp.
Nếu có padding, phải xử lý đúng độ dài thật.
11. Vì sao output[:, -1, :] có thể sai khi có padding?
Giả sử batch:
A: [a1 a2 a3 a4 a5]
B: [b1 b2 PAD PAD PAD]
C: [c1 c2 c3 PAD PAD]
Nếu lấy:
last = output[:, -1, :]
thì với B và C, vị trí -1 nằm sau padding chứ không phải phần tử dữ liệu thật cuối.
Đây là lỗi pipeline, không phải lỗi kiến trúc GRU.
12. Cách dùng pack_padded_sequence
Một pattern PyTorch phổ biến:
import torch
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence
class PackedGRUClassifier(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.gru = nn.GRU(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True,
)
self.classifier = nn.Linear(hidden_size, num_classes)
def forward(self, x, lengths):
packed = pack_padded_sequence(
x,
lengths.cpu(),
batch_first=True,
enforce_sorted=False,
)
_, h_n = self.gru(packed)
last_hidden = h_n[-1]
return self.classifier(last_hidden)
Điểm chính:
xvẫn có thể là padded batch;lengthscho biết độ dài thật;- recurrent layer không cần coi phần padding sau độ dài thật như dữ liệu bình thường;
h_nnhận trạng thái cuối hợp lệ của mỗi chuỗi trong packed sequence.
13. GRU cho gán nhãn từng timestep
Một số bài toán cần nhãn cho từng vị trí:
Ví dụ:
- gán nhãn trạng thái thiết bị ở mỗi giây;
- gán nhãn frame âm thanh;
- phân loại hoạt động ở từng đoạn tín hiệu.
Ta dùng output thay vì chỉ h_n.
class GRUSequenceTagger(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.gru = nn.GRU(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True,
)
self.classifier = nn.Linear(hidden_size, num_classes)
def forward(self, x):
output, _ = self.gru(x)
logits = self.classifier(output)
return logits
Nếu:
x : (B, T, D)
output : (B, T, H)
thì:
logits : (B, T, C)
với là số lớp.
Loss cần mask các timestep padding.
14. Hidden state có cần reset không?
Nếu mỗi sample trong batch là một chuỗi độc lập, mặc định thường dùng:
và không truyền hidden state từ sample A sang sample B.
Khi có thể truyền state tiếp
Nếu một chuỗi dài được chia thành các đoạn liên tục:
đoạn 1 -> đoạn 2 -> đoạn 3
có thể truyền hidden state cuối đoạn trước sang đoạn sau.
Khi phải reset
Nếu:
đoạn 1 của sinh viên A
đoạn 2 của sinh viên B
mà vẫn truyền state, thông tin của A có thể rò sang B.
Đây là bug logic dữ liệu rất dễ bị bỏ qua.
15. Truncated BPTT vẫn áp dụng cho GRU
Với chuỗi cực dài, có thể chia chuỗi thành các đoạn và backpropagate qua cửa sổ hữu hạn.
Ví dụ:
1 ........ 100 | 101 ........ 200 | 201 ........ 300
TBPTT TBPTT TBPTT
Hidden state có thể được truyền tiếp về phía trước, nhưng computational graph được detach giữa các đoạn:
h = h.detach()
Mục đích:
- giới hạn memory;
- giảm độ sâu của graph cho mỗi update.
Đánh đổi:
- gradient không đi xuyên toàn bộ chuỗi;
- phụ thuộc vượt xa cửa sổ TBPTT khó được credit assignment trực tiếp.
16. Gradient clipping vẫn có thể cần
GRU được thiết kế để dòng thông tin thuận lợi hơn Vanilla RNN, nhưng exploding gradient vẫn có thể xảy ra.
Một pattern phổ biến:
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
Không nên chọn 1.0 như một hằng số “đúng chuẩn” cho mọi bài toán.
Nên theo dõi:
- gradient norm;
- loss curve;
- validation metric;
- tỷ lệ update.
17. Một thí nghiệm nhỏ: RNN vs GRU vs LSTM
Mục tiêu không phải chứng minh kiến trúc nào luôn thắng. Mục tiêu là học cách so sánh có kiểm soát.
Ta tạo chuỗi số ngẫu nhiên dài 30. Nhãn phụ thuộc vào tổng tín hiệu của hai feature trên toàn chuỗi.
Bài toán buộc recurrent model phải tổng hợp thông tin theo thời gian, nhưng không được thiết kế để “ép” một kiến trúc cụ thể phải thắng.
Tạo dữ liệu
import random
import numpy as np
import torch
from torch.utils.data import DataLoader, TensorDataset
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
set_seed(42)
def make_dataset(n=2400, seq_len=30, features=4):
x = torch.randn(n, seq_len, features)
# Nhãn phụ thuộc vào thông tin trải trên toàn chuỗi.
score = x[:, :, 0].sum(dim=1) + 0.4 * x[:, :, 1].sum(dim=1)
y = (score > 0).long()
return x.float(), y.long()
x, y = make_dataset()
x_train, y_train = x[:1600], y[:1600]
x_val, y_val = x[1600:2000], y[1600:2000]
x_test, y_test = x[2000:], y[2000:]
train_loader = DataLoader(
TensorDataset(x_train, y_train),
batch_size=64,
shuffle=True,
)
Một wrapper chung
import torch.nn as nn
class RecurrentClassifier(nn.Module):
def __init__(self, kind="GRU", input_size=4, hidden_size=32):
super().__init__()
if kind == "RNN":
self.recurrent = nn.RNN(
input_size,
hidden_size,
batch_first=True,
nonlinearity="tanh",
)
elif kind == "GRU":
self.recurrent = nn.GRU(
input_size,
hidden_size,
batch_first=True,
)
elif kind == "LSTM":
self.recurrent = nn.LSTM(
input_size,
hidden_size,
batch_first=True,
)
else:
raise ValueError(kind)
self.classifier = nn.Linear(hidden_size, 2)
def forward(self, x):
result = self.recurrent(x)
if isinstance(self.recurrent, nn.LSTM):
output, (h_n, c_n) = result
else:
output, h_n = result
return self.classifier(h_n[-1])
Huấn luyện
from sklearn.metrics import f1_score
def evaluate(model, x, y):
model.eval()
with torch.no_grad():
logits = model(x)
pred = logits.argmax(dim=-1)
acc = (pred == y).float().mean().item()
f1 = f1_score(y.numpy(), pred.numpy(), average="macro")
return acc, f1
def train_one(kind, epochs=8):
set_seed(42)
model = RecurrentClassifier(kind=kind)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
model.train()
for xb, yb in train_loader:
optimizer.zero_grad()
logits = model(xb)
loss = criterion(logits, yb)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
val_acc, val_f1 = evaluate(model, x_val, y_val)
test_acc, test_f1 = evaluate(model, x_test, y_test)
return {
"model": kind,
"params": count_parameters(model),
"val_acc": val_acc,
"val_f1": val_f1,
"test_acc": test_acc,
"test_f1": test_f1,
}
results = [train_one(kind) for kind in ["RNN", "GRU", "LSTM"]]
for row in results:
print(row)
Cách đọc thí nghiệm
Không nên viết trước kết luận “GRU chắc chắn thắng”.
Hãy kiểm tra:
- mô hình nào hội tụ ổn định hơn qua nhiều seed;
- chênh lệch F1 có nhất quán không;
- số tham số khác nhau bao nhiêu;
- nếu tăng/giảm sequence length thì kết quả thay đổi ra sao;
- nếu so cùng parameter budget thay vì cùng hidden size thì sao.
Một lần chạy duy nhất chỉ là minh họa, chưa phải bằng chứng khoa học mạnh.
18. Đừng so mô hình bằng một seed duy nhất
Neural network training có ngẫu nhiên từ:
- initialization;
- shuffle;
- dropout;
- CUDA kernels trong một số cấu hình.
Nếu mục tiêu là báo cáo nghiên cứu, nên chạy nhiều seed.
Ví dụ:
seeds = [1, 7, 21, 42, 99]
Sau đó báo cáo:
cho metric quan trọng.
Ví dụ:
Macro-F1: 0.842 ± 0.011
Thay vì chỉ:
Macro-F1: 0.853
mà không biết kết quả đó có phải một lần chạy may mắn hay không.
19. Một protocol so sánh công bằng
Nếu muốn trả lời câu hỏi “GRU và LSTM khác nhau thế nào trên dữ liệu này?”, có thể dùng pipeline:
Giữ cố định càng nhiều yếu tố càng tốt:
- train/validation/test split;
- input representation;
- batch size;
- optimizer;
- training budget;
- early stopping rule;
- preprocessing;
- metric.
Những thứ cần chủ động quyết định:
- cùng hidden size hay cùng parameter budget;
- learning rate riêng cho mỗi architecture có được tuning không;
- latency được đo trên phần cứng nào.
20. Metric nào cần đo?
Bài toán phân loại
Tối thiểu có thể xem:
- accuracy;
- precision/recall;
- macro-F1 nếu các lớp cần được coi trọng tương đối đồng đều;
- confusion matrix.
Nếu dữ liệu lệch lớp mạnh, chỉ accuracy có thể gây hiểu nhầm.
Hiệu quả tính toán
Có thể ghi thêm:
- số tham số;
- model size;
- thời gian một epoch;
- inference latency;
- peak memory.
Không nên suy latency chỉ từ parameter count nếu có thể đo trực tiếp.
21. GRU cho dữ liệu nhỏ có phải mặc định tốt hơn không?
Không có luật như vậy.
Lập luận “GRU ít tham số hơn nên chắc chắn tốt hơn với dữ liệu nhỏ” bỏ qua nhiều yếu tố:
- ít tham số có thể giảm nguy cơ overfit, nhưng cũng giảm capacity;
- hidden size có thể được điều chỉnh;
- regularization và preprocessing ảnh hưởng mạnh;
- cấu trúc phụ thuộc trong dữ liệu mới là yếu tố quan trọng.
GRU là một baseline hợp lý khi muốn gated recurrence tương đối gọn, nhưng vẫn cần validation.
22. GRU có luôn nhanh hơn LSTM không?
Thường GRU có ít phép tính và tham số hơn khi hidden size giống nhau, nhưng “nhanh hơn bao nhiêu” là câu hỏi thực nghiệm.
Tốc độ phụ thuộc:
- thư viện;
- cuDNN/kernel fusion;
- CPU hay GPU;
- sequence length;
- batch size;
- bidirectional;
- datatype;
- overhead của data pipeline.
Vì vậy báo cáo tốt nên viết:
GRU: 18.8k params, 2.1 ms/batch trên thiết bị X
LSTM: 25.1k params, 2.5 ms/batch trên thiết bị X
thay vì:
GRU nhẹ hơn nên chắc chắn nhanh hơn 25%.
23. Khi nào nên thử GRU trước?
GRU đáng thử sớm khi:
- cần một gated RNN nhưng muốn architecture gọn;
- tài nguyên giới hạn;
- cần streaming recurrence;
- muốn baseline giữa Vanilla RNN và LSTM;
- dữ liệu tuần tự nhỏ/vừa và Transformer chưa chắc cần thiết;
- latency/memory là tiêu chí cần đo.
Đây là lý do để thử, không phải bằng chứng rằng GRU sẽ thắng.
24. Khi nào LSTM vẫn là baseline hợp lý?
LSTM hợp lý khi:
- muốn architecture recurrent cổ điển có cell state tách riêng;
- cần so với literature sử dụng LSTM;
- bài toán trước đó đã có baseline LSTM mạnh;
- muốn phân tích forget/input/output gate riêng;
- tài nguyên đủ để kiểm chứng cả hai.
Nếu chi phí cho phép, chạy cả GRU và LSTM thường có giá trị hơn tranh luận lý thuyết xem cái nào “tốt hơn”.
25. GRU và Transformer
Transformer không dùng recurrence theo cách GRU/LSTM.
Với self-attention, token có thể tương tác trực tiếp với các vị trí khác trong context thay vì bắt buộc truyền thông tin tuần tự qua .
Nhưng điều này không khiến GRU vô nghĩa.
GRU vẫn hữu ích để hiểu:
- lịch sử mô hình chuỗi;
- stateful processing;
- streaming;
- hệ thống nhỏ;
- time series/tín hiệu;
- baseline có chi phí thấp trong một số bài toán.
Ở Chương Transformer sau này, điều quan trọng là so sánh cách truyền thông tin chứ không chỉ so “model cũ” và “model mới”.
Trong GRU, trạng thái là đường truyền tuần tự cốt lõi.
26. Debug checklist
Trước khi tăng hidden size hay đổi kiến trúc, kiểm tra:
input.shapecó đúng(batch, time, feature)khibatch_first=Truekhông?- dtype đầu vào có đúng kiểu mà model mong đợi không?
- padding có được xử lý bằng length/mask/packing đúng không?
- classifier đang dùng đúng
h_nhay đúng timestep trongoutputkhông? - nếu bidirectional, đã ghép đúng hai hướng chưa?
- có vô tình dùng dữ liệu tương lai trong bài toán online không?
- hidden state có bị truyền giữa các sequence độc lập không?
- model có overfit được một batch nhỏ không? Nếu không, pipeline có thể sai.
- loss và logits có đúng với
CrossEntropyLosskhông? Đừng softmax trước nếu loss đã nhận logits. - gradient norm có bất thường không?
- train loss giảm nhưng validation xấu đi có phải overfitting không?
- baseline đơn giản có tốt ngang model phức tạp không?
- metric có phù hợp với class imbalance không?
- data split có leakage theo người dùng/thời gian không?
27. Lỗi đặc biệt quan trọng với time series: split ngẫu nhiên có thể gây leakage
Nếu mục tiêu dự đoán tương lai, chia dữ liệu ngẫu nhiên từng timestep có thể đưa mẫu tương lai vào train và mẫu quá khứ vào test.
Một split hợp lý hơn thường tôn trọng thời gian:
Tương tự, nếu có nhiều cửa sổ từ cùng một người/thiết bị, cần cân nhắc split theo entity để các cửa sổ gần như trùng nhau không nằm ở cả train và test.
Một GRU đạt F1 rất cao trên split leakage không phải mô hình tốt.
28. Bài tập
Bài 1 - tính số tham số
Cho một tầng GRU:
nn.GRU(input_size=50, hidden_size=100, batch_first=True)
Tính số tham số bằng công thức rồi kiểm tra với PyTorch.
Làm tương tự cho LSTM cùng kích thước.
Bài 2 - đọc shape
Cho:
gru = nn.GRU(
input_size=24,
hidden_size=40,
num_layers=3,
batch_first=True,
bidirectional=True,
)
x = torch.randn(16, 30, 24)
output, h_n = gru(x)
Xác định:
output.shape;h_n.shape;- kích thước vector nếu ghép hai hướng tại mỗi timestep.
Bài 3 - phát hiện lỗi padding
Một nhóm có batch:
lengths = [20, 13, 7, 5]
nhưng dùng:
last = output[:, -1, :]
để phân loại.
Giải thích vấn đề và sửa bằng:
- packing; hoặc
- indexing theo
lengths.
Bài 4 - thiết kế benchmark
Bạn có dữ liệu cảm biến từ 300 sinh viên, mỗi sinh viên có nhiều chuỗi theo ngày.
Thiết kế benchmark so sánh RNN/GRU/LSTM sao cho:
- không leakage giữa train và test;
- có ít nhất 3 metric;
- có nhiều seed;
- có cả quality và compute cost.
Bài 5 - phản biện kết luận
Một báo cáo viết:
“GRU đạt 91.2% còn LSTM đạt 90.8%, vì vậy GRU tốt hơn LSTM.”
Hãy liệt kê ít nhất 5 thông tin còn thiếu trước khi chấp nhận kết luận này.
29. Thực hành mở rộng: visualization gate theo thời gian
Để hiểu GRU thay vì coi nó là hộp đen, có thể dùng cell tự viết ở bài trước và lưu theo thời gian.
Pseudo-code:
h = torch.zeros(batch_size, hidden_size)
reset_history = []
update_history = []
for t in range(seq_len):
h, r, z, candidate = cell(x[:, t], h)
reset_history.append(r.detach())
update_history.append(z.detach())
Sau đó stack:
reset_history = torch.stack(reset_history, dim=1)
update_history = torch.stack(update_history, dim=1)
Ta có tensor dạng:
(batch, time, hidden)
Có thể visualize một sample bằng heatmap.
Điều cần quan sát không phải “gate này luôn đúng”, mà là:
- gate có thay đổi theo timestep không;
- một số hidden dimension có pattern khác nhau không;
- khi đầu vào có sự kiện quan trọng, update gate có thay đổi hay không;
- pattern có ổn định trên nhiều sample không.
30. Đọc paper như một nhà nghiên cứu
Cho et al. (2014)
Tập trung vào Section 2.3:
- reset gate được định nghĩa thế nào;
- update gate tác động vào thế nào;
- tác giả giải thích khả năng học các time scale khác nhau ra sao.
Chung et al. (2014)
Tập trung vào câu hỏi:
- gated unit có cải thiện so với tanh recurrent unit không;
- GRU và LSTM có chênh lệch nhất quán trên mọi task không.
Jozefowicz et al. (2015)
Tập trung vào phương pháp khoa học:
- thay vì mặc định kiến trúc kinh điển là tối ưu, tác giả khảo sát không gian kiến trúc lớn;
- một kiến trúc thắng trên một task không có nghĩa thắng trên mọi task.
Đây là bài học rộng hơn GRU: architecture choice là giả thuyết cần kiểm chứng, không phải chân lý mặc định.
31. Những điều cần nhớ
- GRU gọn hơn LSTM về số nhóm gate/affine và có 25% ít tham số recurrent hơn khi input size và hidden size giống nhau trong cấu hình chuẩn.
- Ít tham số không tự động suy ra latency giảm đúng cùng tỷ lệ.
- GRU trả về
output, h_n, không cóc_nriêng như LSTM. - Padding, sequence length và hidden-state reset là phần của mô hình hóa chuỗi, không phải chi tiết phụ.
- Bidirectional GRU chỉ hợp lệ khi ngữ cảnh tương lai có sẵn tại thời điểm suy luận.
- So sánh GRU/LSTM phải nói rõ cùng hidden size hay cùng parameter budget.
- Một seed hoặc một metric không đủ để kết luận kiến trúc nào tốt hơn.
- GRU, LSTM và Vanilla RNN đều là baseline có giá trị khi được dùng đúng bài toán và đánh giá đúng protocol.
- Với dữ liệu thời gian, data leakage thường nguy hiểm hơn chênh lệch nhỏ giữa hai recurrent unit.
- Chọn kiến trúc bằng bằng chứng thực nghiệm trên dữ liệu và ràng buộc triển khai thực tế.
Tài liệu tham khảo
- Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. EMNLP 2014, 1724-1734. https://aclanthology.org/D14-1179/
- Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling. NIPS 2014 Deep Learning and Representation Learning Workshop. https://arxiv.org/abs/1412.3555
- Jozefowicz, R., Zaremba, W., & Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. ICML 2015, PMLR 37, 2342-2350. https://proceedings.mlr.press/v37/jozefowicz15.html
- Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735-1780. https://doi.org/10.1162/neco.1997.9.8.1735
- Pascanu, R., Mikolov, T., & Bengio, Y. (2013). On the difficulty of training recurrent neural networks. ICML 2013, PMLR 28(3), 1310-1318. https://proceedings.mlr.press/v28/pascanu13.html
- PyTorch documentation. torch.nn.GRU. https://docs.pytorch.org/docs/stable/generated/torch.nn.GRU.html
- PyTorch documentation. pack_padded_sequence. https://docs.pytorch.org/docs/stable/generated/torch.nn.utils.rnn.pack_padded_sequence.html