Danh sách bài viết

Bài 38: GRU — biến thể đơn giản hơn của LSTM

GRU (Gated Recurrent Unit) do Cho và cộng sự đề xuất 2014 trong paper encoder–decoder cho dịch máy. Ý tưởng: giữ ưu điểm chống vanishing gradient của LSTM nhưng rút thiết kế còn 2 cổng (reset \( r_t \), update \( z_t \)) thay vì 3, và bỏ cell state riêng — chỉ giữ một hidden state \( h_t \). Công thức cốt lõi \( h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \) là convex combination giữa "giữ memory cũ" và "ghi memory mới". Bài này dẫn ra 3 phương trình GRU, đếm parameter để thấy GRU bằng 3/4 LSTM cùng config, dùng nn.GRU, so sánh empirical với LSTM, liệt kê tình huống thực tế nên chọn GRU.

24/05/2026
13 phút đọc
0 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Viết được 4 phương trình của một GRU cell và giải thích vai trò từng cổng.
  • Phân biệt rõ GRU vs LSTM ở mức cấu trúc: 2 gate vs 3 gate, 1 state vs 2 state.
  • Đếm chính xác parameter của nn.GRU theo \( d_x \) và \( d_h \).
  • Dùng nn.GRU đầy đủ tham số (num_layers, bidirectional, batch_first).
  • Implement GRU cell bằng tay với PyTorch, đối chiếu numeric với nn.GRU.
  • Biết kết quả empirical "GRU vs LSTM" và tiêu chí thực tế để chọn.

Bài này nối Bài 37 — LSTM. Bài tiếp Bài 39 — LSTM cho time series sẽ áp dụng vào dự đoán chuỗi thời gian.

2

GRU ra đời để giải vấn đề gì

LSTM (B37) giải được vanishing gradient của Vanilla RNN bằng cell state \( c_t \) + 3 cổng (forget, input, output). Đổi lại: thiết kế phức tạp, 4 weight matrix, training chậm hơn RNN khoảng 2 lần cùng hidden size.

Năm 2014, Cho và cộng sự (trong paper "Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation", arXiv:1406.1078) đặt câu hỏi: có thể đơn giản hoá LSTM mà vẫn giữ khả năng học long-range dependency không? Câu trả lời là GRU — Gated Recurrent Unit:

  • Giữ cơ chế "gate điều khiển flow thông tin" — yếu tố giúp tránh vanishing.
  • Bỏ cell state riêng, gộp memory vào một hidden state \( h_t \).
  • Rút 3 gate xuống 2: reset + update (forget gate và input gate của LSTM bị "couple" thành update gate).
  • Bỏ output gate — hidden state được expose trực tiếp.

Cùng paper Cho 2014 còn giới thiệu kiến trúc encoder–decoder cho dịch máy, là tiền đề cho Bahdanau attention (cũng 2014) và sau này là Transformer. Vì vậy GRU thường được nhắc tới song hành với câu chuyện NMT.

3

Khác biệt cốt lõi với LSTM

Trước khi đi vào công thức, hai điểm cấu trúc cần nắm:

Khía cạnh LSTM GRU
State truyền qua time 2: cell state \( c_t \) + hidden \( h_t \) 1: chỉ \( h_t \)
Số gate 3: forget \( f_t \), input \( i_t \), output \( o_t \) 2: reset \( r_t \), update \( z_t \)
Tách "ghi mới" vs "xoá cũ" Hai gate riêng \( f_t, i_t \) Couple thành một \( z_t \): ghi mới = \( z_t \), giữ cũ = \( 1 - z_t \)
Số weight matrix 4 (mỗi cho 1 gate + candidate cell) 3 (reset, update, candidate)

Hai thiết kế khác nhau ở triết lý:

  • LSTM: bảo vệ memory qua một "đường cao tốc" riêng (cell state), gate quyết định xuất bao nhiêu ra ngoài.
  • GRU: dùng convex combination ngay trên hidden state. Không có "đường riêng" — hidden state vừa là memory vừa là output.
4

Reset gate \( r_t \)

Reset gate quyết định bao nhiêu phần của hidden state cũ \( h_{t-1} \) được dùng để tính candidate hidden state mới:

\[ r_t = \sigma(W_r\, [h_{t-1}, x_t] + b_r) \]

với \( [h_{t-1}, x_t] \) là concat hai vector, \( \sigma \) là sigmoid, \( r_t \in (0,1)^{d_h} \) elementwise.

  • \( r_t \to 1 \): giữ nguyên \( h_{t-1} \) khi tính candidate (chế độ "memory bình thường").
  • \( r_t \to 0 \): "reset" \( h_{t-1} \) về 0 — candidate chỉ phụ thuộc input hiện tại. Tương đương "quên sạch quá khứ ở step này".

Reset gate tương đối local: nó được phép tắt mỗi step để cho phép cell tập trung vào input mới khi cần (ví dụ: token bắt đầu một câu / phrase mới). Đây là cơ chế giúp GRU "đoạn câu" thông tin trong NMT.

5

Update gate \( z_t \)

Update gate quyết định bao nhiêu memory được "ghi đè" bởi candidate mới:

\[ z_t = \sigma(W_z\, [h_{t-1}, x_t] + b_z) \]

\( z_t \in (0, 1)^{d_h} \) elementwise. Trong công thức cập nhật cuối ở mục 7, \( z_t \) đóng vai trò interpolation:

  • \( z_t \to 1 \): full update — hidden state mới gần \( \tilde{h}_t \), quá khứ bị thay thế.
  • \( z_t \to 0 \): không update — hidden state mới gần \( h_{t-1} \), giữ nguyên memory cũ.

Update gate là chỗ "couple" forget + input của LSTM. Trong LSTM ta có thể đồng thời \( f_t = 1, i_t = 1 \) (giữ cell cũ AND thêm thông tin mới). GRU không cho phép tách bạch — chọn nhiều cái mới đồng nghĩa với ít cái cũ. Đây là trade-off thiết kế: ít flexibility hơn nhưng ít param hơn.

6

Candidate hidden state \( \tilde{h}_t \)

Candidate hidden state là "đề xuất" hidden state mới, dùng input hiện tại và phần hidden cũ đã được lọc bởi reset gate:

\[ \tilde{h}_t = \tanh(W\, [r_t \odot h_{t-1},\ x_t] + b) \]

Ở đây \( \odot \) là Hadamard (elementwise) product. Khác biệt với LSTM candidate cell \( \tilde{c}_t \): reset gate \( r_t \) được áp ngay vào \( h_{t-1} \) trước khi nhân với weight, không phải áp sau. Lựa chọn này quan trọng: \( r_t \) có thể tắt riêng từng chiều của hidden, không phải cả vector.

Khi \( r_t = \mathbf{1} \), candidate quay về dạng Vanilla RNN \( \tanh(W [h_{t-1}, x_t] + b) \). Khi \( r_t = \mathbf{0} \), candidate chỉ là \( \tanh(W_x x_t + b) \) — một MLP 1-layer trên input.

7

Final hidden state \( h_t \)

Hidden state mới là convex combination giữa hidden cũ và candidate, điều khiển bởi update gate:

\[ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \]

Ba quan sát:

  • Convex combination elementwise: mỗi chiều của \( h_t \) là blend riêng giữa chiều tương ứng của \( h_{t-1} \) và \( \tilde{h}_t \). Cho phép một số chiều "giữ memory dài hạn", một số chiều "thay đổi nhanh".
  • Cơ chế chống vanishing: khi \( z_t \approx 0 \), \( h_t \approx h_{t-1} \) — gradient truyền ngược qua identity, không bị nén bởi sigmoid/tanh. Đây là tương đương "linear self-loop" của LSTM cell state, chỉ là viết khác.
  • Khi \( z_t \to 1 \): cell tương đương Vanilla RNN có reset gate.

Ghi chú quan trọng: trong paper gốc Cho 2014 (và một số implementation cũ), update gate được viết "đảo": \( h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t \). PyTorch dùng convention ở trên (mới = \( z_t \odot \tilde{h}_t \)). Khi đọc paper / source code cần kiểm tra convention để khỏi giải thích sai dấu.

8

3 phương trình gộp

Tóm tắt toàn bộ một GRU cell:

\[ \begin{aligned} r_t &= \sigma(W_r\, [h_{t-1}, x_t] + b_r) \\ z_t &= \sigma(W_z\, [h_{t-1}, x_t] + b_z) \\ \tilde{h}_t &= \tanh(W\, [r_t \odot h_{t-1},\ x_t] + b) \\ h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \end{aligned} \]

Tổng cộng 3 linear layer cần học: \( W_r, W_z, W \) (cùng 3 bias). Khác với LSTM có 4 linear (\( W_f, W_i, W_o, W_c \)).

Khác Vanilla RNN ở chỗ: hidden update không phải chỉ \( \tanh \) hai số cộng lại, mà là tổ hợp tuyến tính giữa hidden cũ và candidate, có gate. Đây mới là điểm cốt lõi giúp GRU/LSTM học được long-range dependency.

9

So sánh side-by-side với LSTM

Đặt cạnh nhau cho dễ đối chiếu (B37 đã chi tiết LSTM):

LSTM — 4 phương trình, 4 weight matrix:

\[ \begin{aligned} f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f) \\ i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i) \\ o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o) \\ \tilde{c}_t &= \tanh(W_c [h_{t-1}, x_t] + b_c) \\ c_t &= f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \\ h_t &= o_t \odot \tanh(c_t) \end{aligned} \]

GRU — 4 phương trình, 3 weight matrix:

\[ \begin{aligned} r_t &= \sigma(W_r [h_{t-1}, x_t] + b_r) \\ z_t &= \sigma(W_z [h_{t-1}, x_t] + b_z) \\ \tilde{h}_t &= \tanh(W [r_t \odot h_{t-1}, x_t] + b) \\ h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \end{aligned} \]

Mapping ý nghĩa gần đúng:

  • \( z_t \) của GRU ↔ cặp \( (f_t, i_t) \) của LSTM bị bind: \( f_t = 1 - z_t \), \( i_t = z_t \).
  • \( r_t \) của GRU ↔ ảnh hưởng giống output gate \( o_t \) ở chỗ "lọc memory trước khi dùng", nhưng áp vào input của candidate chứ không phải output.
  • GRU không có analog của \( o_t \) — hidden state \( h_t \) expose trực tiếp.
10

Đếm parameter GRU

Mỗi trong 3 weight matrix \( W_r, W_z, W \) có shape \( d_h \times (d_x + d_h) \). Mỗi bias có \( d_h \). PyTorch lưu bias kép \( b_{ih} + b_{hh} \) (giống nn.RNN) — không đổi tổng số nhân tử cần học hiệu dụng nhưng đổi count.

Công thức (theo kiểu paper, gộp bias):

\[ P_{\text{GRU}} = 3 \cdot \big( d_h (d_x + d_h) + d_h \big) = 3 d_h (d_x + d_h) + 3 d_h \]

Theo convention PyTorch (2 bias mỗi gate):

\[ P_{\text{nn.GRU}} = 3 \cdot \big( d_h (d_x + d_h) + 2 d_h \big) = 3 d_h (d_x + d_h) + 6 d_h \]

Ví dụ \( d_x = 10, d_h = 20 \):

  • Paper-style: \( 3 \cdot 20 \cdot 30 + 3 \cdot 20 = 1800 + 60 = 1860 \).
  • PyTorch-style: \( 3 \cdot 20 \cdot 30 + 6 \cdot 20 = 1800 + 120 = 1920 \).

So với LSTM cùng \( d_x = 10, d_h = 20 \):

  • Paper-style LSTM: \( 4 \cdot 20 \cdot 30 + 4 \cdot 20 = 2400 + 80 = 2480 \).
  • Tỉ lệ GRU / LSTM = \( 1860 / 2480 = 0.75 \) — GRU bằng đúng 3/4 LSTM.
import torch.nn as nn
gru  = nn.GRU(input_size=10, hidden_size=20, num_layers=1)
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=1)
print(sum(p.numel() for p in gru.parameters()))   # 1920
print(sum(p.numel() for p in lstm.parameters()))  # 2560
print(1920 / 2560)                                # 0.75

Tỉ lệ 3/4 giữ nguyên ở mọi config (cùng \( d_x, d_h \)) vì là tỉ số 3 weight / 4 weight.

11

PyTorch nn.GRU

API gần giống nn.RNN, chỉ thiếu argument nonlinearity (GRU cố định dùng tanh / sigmoid):

import torch
import torch.nn as nn

gru = nn.GRU(
    input_size=10,
    hidden_size=20,
    num_layers=1,
    bias=True,
    batch_first=True,
    dropout=0.0,
    bidirectional=False,
)

x = torch.randn(32, 100, 10)         # (batch=32, T=100, input=10)
output, h_n = gru(x)
print(output.shape)   # torch.Size([32, 100, 20])  hidden state mọi step
print(h_n.shape)      # torch.Size([1, 32, 20])    hidden state cuối

Khác biệt quan trọng với nn.LSTM: chỉ trả về h_n, khôngc_n — vì GRU không có cell state riêng. Khi truyền hidden vào chunk kế tiếp (TBPTT) chỉ pass \( h_0 \):

output, h_n = gru(x, h_0)            # h_0 shape: (num_layers, B, d_h)

Tương tự nn.LSTM, nn.GRU không bao gồm output projection — phải thêm nn.Linear(d_h, num_classes) bên ngoài cho classification / regression head.

12

Bidirectional và stacked GRU

Hai mở rộng quen thuộc từ Vanilla RNN (B35) hoạt động hệt với GRU:

# Stacked 2 layer + bidirectional
gru = nn.GRU(
    input_size=10,
    hidden_size=20,
    num_layers=2,
    bidirectional=True,
    batch_first=True,
    dropout=0.2,         # áp giữa các layer khi num_layers > 1
)

x = torch.randn(32, 50, 10)
output, h_n = gru(x)
print(output.shape)   # (32, 50, 40)   concat 2 chiều
print(h_n.shape)      # (4, 32, 20)    (num_layers * num_directions, B, d_h)

Quy ước shape:

  • output: \( (B, T, d_h \cdot \text{num\_directions}) \) khi batch_first=True.
  • h_n: \( (\text{num\_layers} \cdot \text{num\_directions}, B, d_h) \). Layer 0 forward ở index 0, layer 0 backward ở index 1, layer 1 forward ở index 2, ...

Bidirectional GRU vẫn yêu cầu cả chuỗi có sẵn tại inference — không streaming. Phù hợp encoder trong seq2seq, POS tagging, NER tương tự BiLSTM.

13

Implement GRU cell bằng tay

Viết GRU cell từ đầu để khắc sâu công thức và làm cơ sở cho variants tự thiết kế:

import torch
import torch.nn as nn

class GRUCell(nn.Module):
    def __init__(self, d_x, d_h):
        super().__init__()
        self.d_h = d_h
        # 3 linear gộp input + hidden, mỗi cái shape (d_h, d_x + d_h)
        self.W_r = nn.Linear(d_x + d_h, d_h)
        self.W_z = nn.Linear(d_x + d_h, d_h)
        self.W   = nn.Linear(d_x + d_h, d_h)

    def forward(self, x_t, h_prev):
        # x_t: (B, d_x), h_prev: (B, d_h)
        xh = torch.cat([h_prev, x_t], dim=-1)               # (B, d_x + d_h)
        r = torch.sigmoid(self.W_r(xh))
        z = torch.sigmoid(self.W_z(xh))
        xh_reset = torch.cat([r * h_prev, x_t], dim=-1)
        h_tilde = torch.tanh(self.W(xh_reset))
        h_new = (1 - z) * h_prev + z * h_tilde
        return h_new

class GRUNet(nn.Module):
    def __init__(self, d_x, d_h):
        super().__init__()
        self.cell = GRUCell(d_x, d_h)
        self.d_h = d_h

    def forward(self, x, h0=None):
        # x: (B, T, d_x)
        B, T, _ = x.shape
        h = torch.zeros(B, self.d_h, device=x.device) if h0 is None else h0
        outputs = []
        for t in range(T):
            h = self.cell(x[:, t, :], h)
            outputs.append(h)
        return torch.stack(outputs, dim=1), h               # (B, T, d_h), (B, d_h)

model = GRUNet(d_x=4, d_h=8)
x = torch.randn(2, 5, 4)
out, h_T = model(x)
print(out.shape, h_T.shape)   # torch.Size([2, 5, 8]) torch.Size([2, 8])

Để verify đối chiếu với nn.GRU: copy weight tương ứng (cẩn thận convention PyTorch tách \( W_{ih}, W_{hh}, b_{ih}, b_{hh} \) cho từng gate), feed cùng input, sai số numeric phải \( \le 10^{-5} \).

14

Train so sánh LSTM vs GRU

Đặt 2 model cùng hidden size trên cùng dataset next-char prediction (skeleton lấy từ B35) để so loss curve và thời gian train:

import time
import torch
import torch.nn as nn

text = "the quick brown fox jumps over the lazy dog " * 200
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
V = len(chars)
data = torch.tensor([stoi[c] for c in text])
seq_len = 32

def get_batch(B=64):
    idx = torch.randint(0, len(data) - seq_len - 1, (B,))
    x = torch.stack([data[i:i+seq_len]     for i in idx])
    y = torch.stack([data[i+1:i+seq_len+1] for i in idx])
    return x, y

class CharSeq(nn.Module):
    def __init__(self, cell_cls, V, d_h=128):
        super().__init__()
        self.embed = nn.Embedding(V, d_h)
        self.rnn = cell_cls(d_h, d_h, batch_first=True)
        self.head = nn.Linear(d_h, V)

    def forward(self, x):
        e = self.embed(x)
        h, _ = self.rnn(e)
        return self.head(h)

def train(cell_cls, name, steps=500):
    model = CharSeq(cell_cls, V)
    opt = torch.optim.Adam(model.parameters(), lr=2e-3)
    loss_fn = nn.CrossEntropyLoss()
    t0 = time.time()
    for step in range(steps):
        x, y = get_batch()
        logits = model(x)
        loss = loss_fn(logits.reshape(-1, V), y.reshape(-1))
        opt.zero_grad(); loss.backward(); opt.step()
    dt = time.time() - t0
    n_params = sum(p.numel() for p in model.rnn.parameters())
    print(f"{name:5s}  loss = {loss.item():.4f}  time = {dt:5.2f}s  rnn_params = {n_params}")

train(nn.LSTM, "LSTM")
train(nn.GRU,  "GRU")

Kết quả thường thấy trên dataset toy này (CPU, 500 step, batch 64): GRU và LSTM đạt loss gần ngang nhau cuối train, GRU nhanh hơn khoảng 15–25%. Số param GRU bằng 3/4 LSTM cùng \( d_h \). Trên dataset thật và dài hơn (vd tiny Shakespeare 1M ký tự), khoảng cách có thể nghiêng về LSTM nhờ cell state riêng cho long-range.

15

GRU vs LSTM — empirical

Chung kết empirical từ các paper khảo sát có hệ thống (Chung et al. 2014 "Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling", Jozefowicz et al. 2015 "An Empirical Exploration of Recurrent Network Architectures"):

  • Trên đa số task chuỗi ngắn–trung bình (< 200 step): GRU và LSTM tương đương performance, sai khác nhỏ hơn variance giữa các seed.
  • GRU train nhanh hơn khoảng 20–25% cùng \( d_h \) do ít weight matrix hơn.
  • LSTM thường nhỉnh hơn trên chuỗi rất dài hoặc task cần memory rất dài hạn (vd language modeling Penn Treebank, long-range copy task) — cell state riêng giúp gradient ổn định hơn.
  • GRU thường nhỉnh hơn khi dataset nhỏ — ít param nên ít overfit hơn cùng \( d_h \).
  • Hyperparameter tuning (LR, dropout, init) ảnh hưởng nhiều hơn lựa chọn LSTM hay GRU. Đổi hyper thường được hơn đổi cell.

Không có "winner" rõ rệt giữa LSTM và GRU. Khuyến nghị thực tế: chọn 1 cái mặc định cho project (GRU thường gọn hơn), nếu nghi ngờ thì swap và so cross-validation.

16

Khi nào chọn GRU, khi nào chọn LSTM

Tiêu chí thực tế:

Tình huống Ưu tiên
Dataset nhỏ–trung bình (vài chục nghìn sample trở xuống) GRU — ít param, ít overfit
Sequence ngắn–trung bình (≤ 200 step) GRU — đủ flexibility, train nhanh
Sequence rất dài (1000+ step), cần memory dài hạn LSTM — cell state riêng ổn định gradient hơn
Cần fine-grained memory control (vd copy task, addition task) LSTM — 3 gate độc lập
Edge / mobile / MCU GRU — ít param, ít memory inference
Real-time streaming (latency thấp) GRU — cell đơn giản, ít phép tính/step
Codebase legacy đã có LSTM LSTM — tránh cost migration
Không chắc Thử cả hai trên validation set

Khi đổi giữa hai loại cell trong PyTorch chỉ cần đổi class name và xử lý state tuple (LSTM trả về (h, c), GRU chỉ h). Code training loop hầu như giữ nguyên.

17

Variants của GRU

  • Minimal GRU (MGU) (Zhou et al. 2016): bỏ luôn reset gate, chỉ giữ forget gate (gần update). 2 weight matrix thay vì 3. Trên một số task NLP performance gần GRU đầy đủ, ít param hơn ~33%.
  • GRU với layer normalization (Ba, Kiros, Hinton 2016): áp LayerNorm lên các pre-activation \( W_r [\cdot], W_z [\cdot], W [\cdot] \) trước khi qua sigmoid/tanh. Ổn định hơn cho chuỗi dài và batch nhỏ, đắt hơn về compute.
  • JANET (van der Westhuizen & Lasenby 2018): biến thể GRU/LSTM rút gọn còn 1 gate (forget). Đôi khi đủ cho task đơn giản.
  • Quasi-RNN, SRU: gating-based recurrent có nhánh "song song hoá" được qua time để chạy nhanh trên GPU, lấy cảm hứng từ GRU.

Trong production hiện nay 90% trường hợp dùng GRU/LSTM "vanilla" PyTorch — variants chỉ dùng khi có lý do cụ thể (latency, memory).

18

Lịch sử ngắn

  • 1997: Hochreiter & Schmidhuber giới thiệu LSTM.
  • 2014, tháng 6: Cho và cộng sự đề xuất GRU trong paper RNN encoder–decoder cho SMT (arXiv:1406.1078). Cùng paper introduce kiến trúc encoder–decoder làm tiền đề NMT.
  • 2014, tháng 9: Bahdanau, Cho, Bengio đề xuất attention mechanism (arXiv:1409.0473) trên kiến trúc encoder–decoder, dùng GRU làm cell mặc định.
  • 2014–2017: GRU và LSTM (đặc biệt BiLSTM) thống trị các baseline NMT, speech recognition, sequence labeling.
  • 2017: Vaswani và cộng sự công bố Transformer ("Attention Is All You Need"), dần thay thế RNN-based seq2seq trong large-scale.
  • Sau 2017: GRU/LSTM lùi về các use case streaming, edge, time series, audio frame-level.
19

GRU trong kỷ nguyên Transformer

GRU không biến mất sau Transformer — chỉ thay đổi vị trí trong stack hiện đại:

  • Real-time / streaming: ASR on-device (wake-word, keyboard suggestion) thường vẫn dùng GRU vì stateful inference rẻ và latency thấp; full attention cần re-attend toàn lịch sử mỗi step.
  • Sequential decoder trong audio: WaveRNN (DeepMind 2018), WaveGRU dùng GRU làm autoregressive decoder cho text-to-speech vì cần stateful inference từng sample.
  • Time series forecasting: GRU vẫn là baseline cạnh tranh trên M4, M5 forecasting competitions cùng các kiến trúc mới hơn (N-BEATS, Temporal Fusion Transformer).
  • Reinforcement learning: GRU thường dùng làm "memory" của agent (vd R2D2, IMPALA biến thể) khi không cần context vô hạn.
  • Multimodal / video: GRU đôi khi dùng làm temporal aggregator phía sau frame encoder.
20

Use case industry

  • Speech recognition: encoder GRU/BiGRU trong các pipeline ASR (DeepSpeech 2 dùng GRU); Conformer thay thế ở scale lớn, nhưng GRU vẫn dùng cho on-device.
  • Machine translation pre-Transformer: GRU encoder–decoder + Bahdanau attention từng là kiến trúc baseline cho hầu hết hệ thống NMT 2015–2017.
  • Time series forecasting: dự báo điện, lưu lượng truy cập, doanh số bán hàng. GRU/LSTM mạnh khi feature engineering tốt và dataset hạn chế.
  • Music generation: char/token-level GRU sinh sheet music (vd Magenta projects).
  • Anomaly detection trong stream: GRU autoencoder trên logs / metrics IoT.
  • Recommender system sequential (GRU4Rec, Hidasi et al. 2016): mô hình session-based dùng GRU encode hành vi user theo time.
21

Bài tập

  1. Tính bằng tay tổng param của nn.GRU(input_size=10, hidden_size=20) dùng cả công thức paper-style và PyTorch-style. Verify bằng sum(p.numel() for p in gru.parameters()). Giải thích chênh lệch \( 6 d_h - 3 d_h = 3 d_h \) ở đâu ra.
  2. Train cả nn.GRUnn.LSTM cùng d_h = 128 trên next-char prediction (skeleton mục 14) với cùng seed, lr, batch. Vẽ loss curve. Đo wall-time. So sánh tham số.
  3. Implement GRU cell từ scratch (mục 13). Copy weight từ nn.GRU(input_size=4, hidden_size=8) sang implementation tay của bạn. Feed cùng input batch (B, T, 4), verify hidden state cuối khớp nhau \( \le 10^{-5} \). Lưu ý nn.GRU chia bias thành b_ih + b_hh — phải cộng cả hai.
  4. Đảo convention update gate: thay \( h_t = (1 - z_t) h_{t-1} + z_t \tilde{h}_t \) bằng \( h_t = z_t h_{t-1} + (1 - z_t) \tilde{h}_t \) (convention Cho 2014). Train lại next-char. Loss có khác đáng kể không? Vì sao kỳ vọng không khác?
  5. Thử Minimal GRU: bỏ reset gate, đặt \( r_t = \mathbf{1} \) cố định. Train lại trên cùng dataset. So sánh loss và số param so với GRU đầy đủ.
  6. Train BiGRU và GRU một chiều trên POS tagging toy (chuỗi từ giả gắn nhãn V/N/A). Compare F1. Giải thích vì sao bidirectional có lợi cho tagging.
  7. Thay nn.GRU trong character LM bằng nn.LSTM giữ nguyên d_h. Đo: (a) số param, (b) wall-time / step, (c) loss cuối epoch. Báo cáo tỉ lệ thực tế GRU/LSTM trên hardware của bạn.
Gợi ý đáp án ngắn
  1. Paper: \( 3 \cdot 20 \cdot 30 + 3 \cdot 20 = 1860 \). PyTorch: \( 3 \cdot 20 \cdot 30 + 6 \cdot 20 = 1920 \). Chênh lệch 60 do PyTorch lưu bias kép b_ihb_hh cho mỗi gate, tổng cộng \( 3 \cdot 20 \) extra.
  2. Trên text lặp đơn giản, GRU và LSTM gần như trùng curve loss; GRU thường nhanh hơn 15–25% wall-time. Param GRU 3/4 LSTM.
  3. Sai số chính do convention bias: phải b = b_ih + b_hh mỗi gate. Sau đó sai số chỉ còn floating-point noise.
  4. Đảo convention chỉ đổi parameterization, lý thuyết model identical (chỉ flip sign trong sigmoid). Loss curve gần như trùng — chỉ khác noise của init.
  5. Minimal GRU thường mất 1–2% accuracy trên next-char nhưng tiết kiệm ~33% param. Trade-off đáng thử khi memory hạn chế.
  6. BiGRU có thông tin cả future, F1 cao hơn 5–10 điểm so với GRU một chiều trên POS task. Token tagging quan tâm cả context trước và sau.
  7. Param: LSTM/GRU = 4/3 ≈ 1.33. Wall-time tỉ lệ tương tự, hơi thiên về 1.2 trên CPU và 1.1–1.15 trên GPU do cuDNN tối ưu cả hai.
22

Tóm tắt

  • GRU (Cho et al. 2014) là cell recurrent rút gọn của LSTM: bỏ cell state riêng, gộp memory vào hidden state, dùng 2 gate (reset \( r_t \), update \( z_t \)) thay vì 3.
  • 3 phương trình: \( r_t = \sigma(W_r [h_{t-1}, x_t] + b_r) \), \( z_t = \sigma(W_z [h_{t-1}, x_t] + b_z) \), \( \tilde{h}_t = \tanh(W [r_t \odot h_{t-1}, x_t] + b) \), và update \( h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \).
  • Reset gate filter hidden cũ trước khi tính candidate; update gate là interpolation factor giữa "giữ cũ" và "ghi mới".
  • Cơ chế chống vanishing: khi \( z_t \approx 0 \), \( h_t \approx h_{t-1} \) — gradient truyền qua identity path.
  • Tham số GRU = 3/4 LSTM cùng \( d_h \). Ví dụ \( d_x = 10, d_h = 20 \): GRU 1860 (paper-style) hay 1920 (PyTorch-style); LSTM tương ứng 2480 và 2560.
  • nn.GRU có API gần giống nn.RNN, chỉ trả về \( h_n \) (không có \( c_n \)). Hỗ trợ num_layers, bidirectional, batch_first, dropout.
  • Empirical: GRU và LSTM tương đương ở đa số task chuỗi ngắn–trung bình; GRU train nhanh hơn 20–25%; LSTM thường nhỉnh hơn ở chuỗi rất dài.
  • Chọn GRU khi: dataset nhỏ, sequence ngắn–trung bình, edge device, cần train nhanh. Chọn LSTM khi: sequence rất dài, cần fine-grained memory control, codebase legacy.
  • Variants: Minimal GRU (1 gate), GRU + LayerNorm, JANET, SRU.
  • Sau Transformer (2017), GRU vẫn dùng trong streaming ASR, audio decoder (WaveRNN/WaveGRU), time series, RL agent memory, session-based recsys.