Forward pass RNN pada chapter sebelumnya berjalan mulus, tetapi model belum bisa belajar — bobot Wh dan Wx hanya akan berarti setelah gradien loss mengalir ke keduanya. Tantangannya khas: bobot yang sama dipakai di lima langkah waktu berbeda, sehingga gradien totalnya adalah gabungan kontribusi dari setiap langkah. Backpropagation through time (BPTT) menyelesaikannya dengan langkah dua kata pertamanya: unroll sekuens menjadi jaringan dalam yang panjang — satu salinan RNN cell per langkah — lalu jalankan backpropagation biasa pada jaringan yang sudah terbentang itu. Chapter ini mengerjakannya dengan tangan pada RNN empat langkah dan memverifikasi hasilnya terhadap finite difference.

Unroll Dulu, Backward Biasa Sesudahnya #

Mari pakai loss paling sederhana yang masih bermakna: L = 0,5 * ||h_4||^2 — nilai hanya tergantung pada hidden state terakhir. Untuk menghitung dL/dWh, aturan rantai harus menelusuri jaringan ter-unroll dari loss mundur ke h_0, karena h_4 bergantung pada h_3, yang bergantung pada h_2, dan seterusnya. Pada setiap langkah mundur, gradien melewati dua hal: turunan tanh (1 - h_t^2) dan matriks Wh:

Kontribusi ke dL/dWh terkumpul dari semua langkah: pada langkah t, hidden state h_{t-1} dikalikan Wh menghasilkan bagian dari h_t, sehingga dL/dWh menerima suku dz_t ⊗ h_{t-1}. Gradien juga terus diteruskan mundur lewat g = dz_t @ Wh — baris inilah yang membuat gradien “melintasi waktu”.

BPTT Manual dengan NumPy #

Program berikut menghitung gradien dL/dWh secara analitik dengan BPTT manual pada RNN satu layer empat langkah, lalu membandingkannya dengan beda hingga (finite difference) pada kesembilan elemen matriks:

python
"""Backpropagation Through Time manual pada RNN 1 layer, 4 langkah.

Forward menyimpan seluruh h_t, backward menelusuri rantai waktu mundur,
lalu gradien dL/dWh dibandingkan dengan finite difference (beda hingga).
Loss sederhana L = 0.5 * ||h_4||^2 agar alur gradien mudah diikuti.
"""

import numpy as np

np.random.seed(0)

D, H, T = 2, 3, 4

Wx = np.random.randn(H, D) * 0.8
Wh = np.random.randn(H, H) * 0.6
b = np.zeros(H)

x = np.array(
    [
        [0.60, -0.20],
        [0.10, 0.40],
        [-0.50, 0.30],
        [0.80, 0.00],
    ]
)


def forward(Wh_uji):
    """Forward penuh; kembalikan semua h_t dan loss L = 0.5*||h_T||^2."""
    h = {0: np.zeros(H)}
    for t in range(1, T + 1):
        z = x[t - 1] @ Wx.T + h[t - 1] @ Wh_uji.T + b
        h[t] = np.tanh(z)
    L = 0.5 * float(h[T] @ h[T])
    return h, L


h, L = forward(Wh)
print(f"Loss awal L = 0.5*||h_{T}||^2 = {L:.6f}")
print()

# ---------- BPTT: mundur dari t = T ke t = 1 ----------
dWx = np.zeros_like(Wx)
dWh = np.zeros_like(Wh)
db = np.zeros_like(b)

g = h[T].copy()  # dL/dh_T = h_T (turunan 0.5*||h||^2)
print("Gradien dL/dh_t saat rantai waktu ditelusuri mundur:")
for t in range(T, 0, -1):
    dz = g * (1.0 - h[t] ** 2)        # turunan tanh
    dWh += np.outer(dz, h[t - 1])     # kontribusi langkah t
    dWx += np.outer(dz, x[t - 1])
    db += dz
    g = dz @ Wh                        # teruskan ke h_{t-1}
    print(f"  ||dL/dh_{t - 1}|| = {np.linalg.norm(g):.6f}")

print()
print("dL/dWh hasil BPTT:")
print(np.round(dWh, 6))

# ---------- Verifikasi dengan finite difference ----------
eps = 1e-6
fd = np.zeros_like(Wh)
for i in range(H):
    for j in range(H):
        E = np.zeros_like(Wh)
        E[i, j] = eps
        _, L_plus = forward(Wh + E)
        _, L_minus = forward(Wh - E)
        fd[i, j] = (L_plus - L_minus) / (2.0 * eps)

selisih = np.abs(dWh - fd).max()
print()
print("dL/dWh finite difference:")
print(np.round(fd, 6))
print(f"Selisih maksimum BPTT vs finite difference: {selisih:.3e}")
print("Verifikasi lolos:", bool(selisih < 1e-8))

Output yang Dihasilkan:

text
Loss awal L = 0.5*||h_4||^2 = 0.521279

Gradien dL/dh_t saat rantai waktu ditelusuri mundur:
  ||dL/dh_3|| = 0.424070
  ||dL/dh_2|| = 0.245640
  ||dL/dh_1|| = 0.107788
  ||dL/dh_0|| = 0.034997

dL/dWh hasil BPTT:
[[ 0.149136  0.525417  0.063971]
 [-0.047129  0.069429 -0.063557]
 [-0.06078   0.314685 -0.128043]]

dL/dWh finite difference:
[[ 0.149136  0.525417  0.063971]
 [-0.047129  0.069429 -0.063557]
 [-0.06078   0.314685 -0.128043]]
Selisih maksimum BPTT vs finite difference: 9.062e-11
Verifikasi lolos: True

Mari kita telaah kode di atas baris per baris:

  1. Baris 14-16: bobot dibuat kecil (dikali 0,8 dan 0,6) agar nilai h_t tidak jenuh di tepi tanh — turunan (1 - h_t^2) tetap sehat dan alur gradien mudah dibaca.
  2. Baris 28-35: fungsi forward menyimpan seluruh h_t dalam kamus h berindeks 0 sampai 4. Penyimpanan ini bukan opsional: backward membutuhkan nilai h_{t-1} dan h_t pada setiap langkah, jadi forward harus mengingat semuanya.
  3. Baris 38-39: loss awal tercatat 0,521279 — angka pembanding bagi finite difference nanti.
  4. Baris 47: benih gradien g = h_T adalah turunan 0,5 * ||h||^2 terhadap h itu sendiri; dari titik ini rantai mundur dimulai.
  5. Baris 49-55: perulangan mundur t = 4, 3, 2, 1. Baris 50 menerapkan turunan tanh; baris 51 mengakumulasi np.outer(dz, h[t-1]) — kontribusi langkah t terhadap dWh; baris 54 meneruskan gradien dz @ Wh ke h_{t-1} sebelum perulangan berikutnya.
  6. Baris 62-70: finite difference mengganggu setiap elemen Wh[i, j] sebesar ±1e-6, menghitung ulang loss dua kali, lalu memakai beda pusat (L+ - L-) / 2ε sebagai pendekatan turunan.
  7. Baris 72-77: kedua matriks gradien tercetak — seluruh angka pada dL/dWh BPTT dan finite difference sama sampai enam desimal, dengan selisih maksimum 9,06e-11, setingkat galat pembulatan beda hingga. Verifikasi lolos.

Norma gradien yang tercetak selama mundur layak dicermati: 0,424 → 0,246 → 0,108 → 0,035. Setiap langkah mundur memperkecil norma — pola yang pada sekuens panjang akan berkembang menjadi masalah vanishing gradient pada chapter sesudahnya.

Biaya Memori dan Truncated BPTT #

BPTT penuh menyimpan semua h_t dan menelusuri rantai sepanjang T langkah. Kedua hal itu mahal: memori tumbuh linear terhadap T, dan gradien makin tidak stabil pada rantai panjang. Praktik umum di industri adalah truncated BPTT — potong sekuens panjang menjadi jendela k1 langkah, jalankan backward hanya sepanjang k2 langkah (biasanya k1 = k2), lalu lanjutkan ke jendela berikutnya dengan hidden state yang terbawa:

StrategiYang disimpanGradien menjangkauKapan dipakai
BPTT penuhsemua h_t (T buah)seluruh sekuenssekuens pendek (T kecil)
Truncated BPTTh_t dalam jendela sajak2 langkah terakhirsekuens panjang, training daring
Hidden state terbawastate antar jendelalintas jendela secara bertahapstreaming, log panjang

Dengan gradien yang kini bisa dihitung, RNN siap dilatih. Namun perhatikan kembali deret norma yang menyusut pada output di atas — pada sekuens yang benar-benar panjang, penyusutan itu bukan lagi fenomena kecil. Chapter berikutnya membedah mengapa perkalian Wh berulang membuat gradien RNN menyusut secara eksponensial, dan apa akibatnya bagi dependensi jangka panjang.