Chapter sebelumnya telah menyiapkan data sekuens dalam bentuk batch (N, T, D). Pertanyaan berikutnya: arsitektur apa yang membacanya? MLP tidak cocok karena jumlah inputnya berubah mengikuti panjang sekuens, dan MLP juga tidak punya tempat menyimpan informasi dari langkah sebelumnya. Recurrent neural network (RNN) menjawab keduanya sekaligus dengan satu ide sederhana: sebuah jaringan yang sama dipanggil berulang kali, satu kali untuk setiap langkah waktu, sambil membawa catatan ringkas bernama hidden state dari satu langkah ke langkah berikutnya. Chapter ini membedah mekanisme tersebut sampai ke rumus, mengimplementasikannya dari nol dengan NumPy, lalu memverifikasi angkanya terhadap torch.nn.RNN.
Hidden State: Memori yang Dibawa Antar Langkah #
Pada setiap langkah t, RNN menggabungkan dua masukan: fitur langkah ini x_t dan ringkasan langkah sebelumnya h_{t-1}. Keduanya dijumlah setelah dikalikan matriks bobotnya sendiri, lalu dilewatkan tanh agar nilainya terkurung di rentang (-1, 1):
h_t = tanh(Wx x_t + Wh h_{t-1} + b)Hidden state pertama h_0 biasanya diisi nol. Kunci dari seluruh mekanisme ini adalah pemakaian ulang: matriks Wx, Wh, dan bias b yang sama dipakai di setiap langkah. Jika kita “unroll” perulangan tersebut menjadi rantai, ketiga blok salinan itu adalah jaringan yang identik — bukan tiga jaringan berbeda:
RNN Cell Murni dengan NumPy #
Program berikut membangun layer torch.nn.RNN sebagai sumber bobot, menyalin bobotnya ke NumPy, menghitung forward pass lima langkah secara manual, lalu membandingkan hasil keduanya. Presisi float64 dipakai agar perbandingan angka benar-benar ketat:
"""RNN cell dari nol dengan NumPy, diverifikasi terhadap torch.nn.RNN.
Satu bobot dipakai ulang di setiap langkah waktu: h_t = tanh(Wx x_t +
Wh h_{t-1} + b). Bobot diambil dari layer torch.nn.RNN (seed sama) agar
angka NumPy dan PyTorch dapat dibandingkan langsung.
"""
import numpy as np
import torch
torch.manual_seed(0)
np.random.seed(0)
D, H, T = 2, 4, 5 # dimensi input, hidden, panjang sekuens
# ---------- Layer torch: sumber bobot ----------
rnn_torch = torch.nn.RNN(D, H, batch_first=True, dtype=torch.float64)
Wx = rnn_torch.weight_ih_l0.detach().numpy() # (H, D)
Wh = rnn_torch.weight_hh_l0.detach().numpy() # (H, H)
b = (
rnn_torch.bias_ih_l0.detach().numpy()
+ rnn_torch.bias_hh_l0.detach().numpy()
) # PyTorch memakai dua bias terpisah; kita jumlahkan
# ---------- Sekuens masukan kecil ----------
x = np.array(
[
[0.60, -0.20],
[0.10, 0.40],
[-0.50, 0.30],
[0.80, 0.00],
[0.20, -0.70],
]
)
# ---------- Forward NumPy: h dibawa antar langkah ----------
print("Forward NumPy (h_t = tanh(Wx x_t + Wh h_{t-1} + b))")
print("h_0 =", np.zeros(H))
h = np.zeros(H)
simpan_h = [h.copy()]
for t in range(T):
z = x[t] @ Wx.T + h @ Wh.T + b
h = np.tanh(z)
simpan_h.append(h.copy())
print(f"h_{t + 1} =", np.round(h, 6))
# ---------- Forward PyTorch ----------
out, h_akhir = rnn_torch(torch.from_numpy(x).unsqueeze(0)) # (1, T, H)
h_torch = out[0].detach().numpy()
# ---------- Verifikasi ----------
selisih = np.abs(np.array(simpan_h[1:]) - h_torch)
print()
print("Hidden akhir NumPy :", np.round(simpan_h[-1], 6))
print("Hidden akhir PyTorch:", np.round(h_torch[-1], 6))
print("Selisih maksimum :", float(selisih.max()))
print("Cocok dengan torch.nn.RNN:", bool(selisih.max() < 1e-12))
# ---------- Perkalian matriks yang sama, lima kali ----------
jumlah_param = Wx.size + Wh.size + b.size
print()
print(f"Ukuran matriks Wh : {Wh.shape} -> dipakai ulang di {T} langkah")
print(f"Total parameter : {jumlah_param} (tidak bertambah walau T membesar)")Output yang Dihasilkan:
Forward NumPy (h_t = tanh(Wx x_t + Wh h_{t-1} + b))
h_0 = [0. 0. 0. 0.]
h_1 = [ 0.20875 -0.311875 0.402146 0.148375]
h_2 = [ 0.117105 -0.13338 0.610383 -0.048124]
h_3 = [-0.174911 -0.108578 0.48363 0.130964]
h_4 = [ 0.283131 -0.245903 0.590717 -0.098422]
h_5 = [-0.000994 -0.531762 0.325921 -0.005857]
Hidden akhir NumPy : [-0.000994 -0.531762 0.325921 -0.005857]
Hidden akhir PyTorch: [-0.000994 -0.531762 0.325921 -0.005857]
Selisih maksimum : 5.551115123125783e-17
Cocok dengan torch.nn.RNN: True
Ukuran matriks Wh : (4, 4) -> dipakai ulang di 5 langkah
Total parameter : 28 (tidak bertambah walau T membesar)Mari kita telaah kode di atas baris per baris:
- Baris 11-14: seed PyTorch dan NumPy dikunci; ukuran ditetapkan D = 2 fitur masukan, H = 4 dimensi hidden, T = 5 langkah.
- Baris 17: layer
torch.nn.RNN(2, 4, batch_first=True, dtype=torch.float64)dibuat — seluruh bobotnya berasal dari sumber yang sama dan akan disalin ke NumPy. - Baris 19-24:
weight_ih_l0berbentuk(H, D)adalah Wx;weight_hh_l0berbentuk(H, H)adalah Wh. PyTorch memakai dua bias terpisah (bias_ihdanbias_hh), sehingga keduanya dijumlahkan menjadi satu biasbseperti rumus di awal chapter. - Baris 27-35: sekuens masukan 5 langkah ditulis eksplisit dengan nilai kecil yang mudah dibaca.
- Baris 41-47: inti RNN cell. Baris 44 menghitung
z = x[t] @ Wx.T + h @ Wh.T + b— perkalian matriks dari rumus dalam bentuk baris-vektor — lalu baris 45 menerapkan tanh. Setiaphbaru menimpa yang lama setelah disalin, meniru state yang terus dibawa. - Baris 50-51: tensor NumPy di-
unsqueeze(0)menjadi(1, T, D)lalu melewati layer PyTorch yang memakai bobot aslinya. - Baris 54-59: verifikasi — selisih maksimum antara seluruh
h_tNumPy dan PyTorch hanya 5,55e-17, setara nol pada presisi float64. Angka pada kedua jalur benar-benar sama karena keduanya menghitung rumus yang sama. - Baris 62-65: total parameter dihitung dari ukuran matriks: 8 (Wx) + 16 (Wh) + 4 (b) = 28.
Membaca Perkembangan h_t #
Perhatikan jejak pada output: h_1 = [0,209, -0,312, 0,402, 0,148], lalu h_2 berubah penuh menjadi [0,117, -0,133, 0,610, -0,048] meskipun x_2 kecil. Setiap nilai h_t adalah fungsi dari seluruh masukan sebelumnya — mengubah x_1 akan mengubah h_1, h_2, hingga h_5. Tabel berikut merangkum peran tiap besaran:
| Besaran | Ukuran | Peran |
|---|---|---|
x_t | (D,) | fitur pada langkah t |
h_t | (H,) | ringkasan x_1 sampai x_t |
Wx (weight_ih_l0) | (H, D) | bobot masukan langkah ini |
Wh (weight_hh_l0) | (H, H) | bobot hidden state sebelumnya |
| b | (H,) | bias (jumlah dua bias PyTorch) |
Dokumentasi resmi torch.nn.RNN memuat rumus dan konvensi penamaan bobot yang kita pakai di sini.
Satu pertanyaan masih menggantung: bagaimana bobot Wh dan Wx itu sendiri dilatih? Gradien harus mengalir mundur melewati seluruh rantai langkah waktu yang kita lihat pada diagram unroll di atas. Proses itulah yang dibedah pada chapter berikutnya: backpropagation through time.