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:
"""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:
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: TrueMari kita telaah kode di atas baris per baris:
- Baris 14-16: bobot dibuat kecil (dikali 0,8 dan 0,6) agar nilai
h_ttidak jenuh di tepi tanh — turunan(1 - h_t^2)tetap sehat dan alur gradien mudah dibaca. - Baris 28-35: fungsi
forwardmenyimpan seluruhh_tdalam kamushberindeks 0 sampai 4. Penyimpanan ini bukan opsional: backward membutuhkan nilaih_{t-1}danh_tpada setiap langkah, jadi forward harus mengingat semuanya. - Baris 38-39: loss awal tercatat 0,521279 — angka pembanding bagi finite difference nanti.
- Baris 47: benih gradien
g = h_Tadalah turunan0,5 * ||h||^2terhadaphitu sendiri; dari titik ini rantai mundur dimulai. - 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 terhadapdWh; baris 54 meneruskan gradiendz @ Whkeh_{t-1}sebelum perulangan berikutnya. - 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. - Baris 72-77: kedua matriks gradien tercetak — seluruh angka pada
dL/dWhBPTT 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:
| Strategi | Yang disimpan | Gradien menjangkau | Kapan dipakai |
|---|---|---|---|
| BPTT penuh | semua h_t (T buah) | seluruh sekuens | sekuens pendek (T kecil) |
| Truncated BPTT | h_t dalam jendela saja | k2 langkah terakhir | sekuens panjang, training daring |
| Hidden state terbawa | state antar jendela | lintas jendela secara bertahap | streaming, 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.