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):

text
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:

python
"""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:

text
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:

  1. Baris 11-14: seed PyTorch dan NumPy dikunci; ukuran ditetapkan D = 2 fitur masukan, H = 4 dimensi hidden, T = 5 langkah.
  2. 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.
  3. Baris 19-24: weight_ih_l0 berbentuk (H, D) adalah Wx; weight_hh_l0 berbentuk (H, H) adalah Wh. PyTorch memakai dua bias terpisah (bias_ih dan bias_hh), sehingga keduanya dijumlahkan menjadi satu bias b seperti rumus di awal chapter.
  4. Baris 27-35: sekuens masukan 5 langkah ditulis eksplisit dengan nilai kecil yang mudah dibaca.
  5. 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. Setiap h baru menimpa yang lama setelah disalin, meniru state yang terus dibawa.
  6. Baris 50-51: tensor NumPy di-unsqueeze(0) menjadi (1, T, D) lalu melewati layer PyTorch yang memakai bobot aslinya.
  7. Baris 54-59: verifikasi — selisih maksimum antara seluruh h_t NumPy dan PyTorch hanya 5,55e-17, setara nol pada presisi float64. Angka pada kedua jalur benar-benar sama karena keduanya menghitung rumus yang sama.
  8. 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:

BesaranUkuranPeran
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.