Vanishing gradient membuat RNN vanilla kehilangan akses ke konteks jauh: gradien menyusut eksponensial karena setiap langkah waktu memaksa gradien melewati perkalian matriks dan turunan tanh. Long Short-Term Memory (LSTM) dari Hochreiter dan Schmidhuber (1997) menyerang masalah ini lewat desain arsitektur: sel baru bernama cell state ditambahkan, dan aliran masuk-keluarnya dikendalikan tiga gate yang bisa membiarkan informasi lewat nyaris tanpa diubah. Hasilnya adalah jalur gradien yang panjang dan hampir konstan — syarat yang gagal dipenuhi RNN vanilla. Chapter ini membedah rumus keempat komponen itu, mengimplementasikannya dari nol dengan NumPy, lalu memverifikasi angkanya terhadap torch.nn.LSTM.

Cell State dan Tiga Gate #

LSTM memisahkan dua jalur: cell state c_t sebagai memori jangka panjang, dan hidden state h_t sebagai keluaran yang diperlihatkan ke lapisan berikutnya. Tiga gate — masing-masing bernilai 0 sampai 1 hasil sigmoid — mengatur lalu lintas pada jalur tersebut:

KomponenRumusPeran
Gate forgetf_t = σ(W_f x_t + U_f h_{t-1} + b_f)berapa bagian c_{t-1} yang dipertahankan
Gate inputi_t = σ(W_i x_t + U_i h_{t-1} + b_i)berapa kandidat baru yang masuk ke c_t
Kandidatg_t = tanh(W_g x_t + U_g h_{t-1} + b_g)isi memori yang diusulkan langkah ini
Gate outputo_t = σ(W_o x_t + U_o h_{t-1} + b_o)berapa c_t yang diperlihatkan sebagai h_t
Cell statec_t = f_t ⊙ c_{t-1} + i_t ⊙ g_tmemori jangka panjang
Hidden stateh_t = o_t ⊙ tanh(c_t)keluaran langkah ini

Kunci jawaban LSTM terhadap vanishing gradient ada pada baris cell state: c_t = f_t ⊙ c_{t-1} + ... adalah penjumlahan, bukan perkalian matriks berulang. Ketika gate forget bernilai dekat 1 dan gate input dekat 0, informasi lama mengalir dari c_0 sampai c_T hanya dikalikan bilangan skalar mendekati 1 — gradien pada jalur itu nyaris tidak menyusut walau T besar. Gate-gate inilah yang dipelajari selama training: model belajar kapan mengingat, kapan menimpa, dan kapan membuka memori.

Diagram di atas menunjukkan alur intinya: cell state lama dan kandidat baru dijumlahkan setelah masing-masing dikalikan gatenya, lalu sebagian cell state yang baru dikeluarkan sebagai hidden state. Keempat gate semuanya dihitung dari pasangan x_t dan h_{t-1} yang sama.

LSTM Cell Murni dengan NumPy #

Program berikut mengambil bobot dari layer torch.nn.LSTM, membaginya menjadi empat porsi gate, lalu menghitung forward pass lima langkah secara manual pada sekuens yang menaruh token penting di langkah kedua — ujian kecil bagi kemampuan menyimpan informasi:

python
"""LSTM cell dari nol dengan NumPy, diverifikasi terhadap torch.nn.LSTM.

Tiga gate (forget, input, output) dan cell state c_t dihitung manual per
langkah pada sekuens 5 langkah. Bobot diambil dari layer torch.nn.LSTM
(seed sama) sehingga jejak c_t dan h_t dapat dibandingkan angka demi angka.
"""

import numpy as np
import torch

torch.manual_seed(0)
np.random.seed(0)

D, H, T = 2, 3, 5


def sigmoid(v):
    return 1.0 / (1.0 + np.exp(-v))


# ---------- Layer torch: sumber bobot ----------
lstm_torch = torch.nn.LSTM(D, H, batch_first=True, dtype=torch.float64)

W_ih = lstm_torch.weight_ih_l0.detach().numpy()  # (4H, D)
W_hh = lstm_torch.weight_hh_l0.detach().numpy()  # (4H, H)
b = (
    lstm_torch.bias_ih_l0.detach().numpy()
    + lstm_torch.bias_hh_l0.detach().numpy()
)  # (4H,) -- dua bias dijumlahkan

# Urutan baris PyTorch: [input, forget, g (kandidat), output]
Wi, Wf, Wg, Wo = np.split(W_ih, 4)
Ui, Uf, Ug, Uo = np.split(W_hh, 4)
bi, bf, bg, bo = np.split(b, 4)

# ---------- Sekuens masukan: token penting di langkah 2 ----------
x = np.array(
    [
        [0.00, 0.00],
        [1.50, -1.00],  # token "penanda" yang perlu diingat
        [0.00, 0.00],
        [0.00, 0.00],
        [0.00, 0.00],
    ]
)

# ---------- Forward NumPy ----------
print("Forward LSTM NumPy, jejak c_t dan h_t:")
h = np.zeros(H)
c = np.zeros(H)
simpan_h = []
for t in range(T):
    f = sigmoid(x[t] @ Wf.T + h @ Uf.T + bf)   # gate forget
    i = sigmoid(x[t] @ Wi.T + h @ Ui.T + bi)   # gate input
    g = np.tanh(x[t] @ Wg.T + h @ Ug.T + bg)   # kandidat nilai baru
    o = sigmoid(x[t] @ Wo.T + h @ Uo.T + bo)   # gate output
    c = f * c + i * g                          # cell state baru
    h = o * np.tanh(c)                         # hidden baru
    simpan_h.append(h.copy())
    print(f"t={t}  c={np.round(c, 6)}  h={np.round(h, 6)}")
    if t == 1:
        print(f"     gate: f={np.round(f, 4)} i={np.round(i, 4)} "
              f"o={np.round(o, 4)}")

# ---------- Forward PyTorch ----------
out, (h_n, c_n) = lstm_torch(torch.from_numpy(x).unsqueeze(0))
h_torch = out[0].detach().numpy()
c_torch = c_n[0, 0].detach().numpy()

# ---------- Verifikasi ----------
selisih_h = np.abs(np.array(simpan_h) - h_torch).max()
selisih_c = np.abs(c - c_torch).max()
print()
print("c_5 NumPy  :", np.round(c, 6))
print("c_5 PyTorch:", np.round(c_torch, 6))
print("h_5 NumPy  :", np.round(h, 6))
print("h_5 PyTorch:", np.round(h_torch[-1], 6))
print(f"Selisih maksimum h: {selisih_h:.3e} | c: {selisih_c:.3e}")
print("Cocok dengan torch.nn.LSTM:",
      bool(selisih_h < 1e-12 and selisih_c < 1e-12))

# ---------- Selisih jumlah parameter vs RNN vanilla ----------
rnn = torch.nn.RNN(D, H, dtype=torch.float64)
n_rnn = sum(p.numel() for p in rnn.parameters())
n_lstm = sum(p.numel() for p in lstm_torch.parameters())
print()
print(f"Parameter RNN vanilla (D={D}, H={H}) : {n_rnn}")
print(f"Parameter LSTM      (D={D}, H={H}) : {n_lstm}")
print(f"Rasio LSTM / RNN vanilla            : {n_lstm / n_rnn:.1f}x")

Output yang Dihasilkan:

text
Forward LSTM NumPy, jejak c_t dan h_t:
t=0  c=[-0.062721  0.008911 -0.178631]  h=[-0.035279  0.004874 -0.087386]
t=1  c=[-0.339295  0.037836 -0.216777]  h=[-0.177287  0.013957 -0.068622]
     gate: f=[0.4705 0.5273 0.3887] i=[0.634  0.3104 0.2652] o=[0.5424 0.3691 0.3215]
t=2  c=[-0.289781  0.046273 -0.261321]  h=[-0.159224  0.024547 -0.122247]
t=3  c=[-0.257687  0.064078 -0.281016]  h=[-0.139965  0.033971 -0.130585]
t=4  c=[-0.236548  0.075187 -0.287242]  h=[-0.128144  0.039916 -0.133268]

c_5 NumPy  : [-0.236548  0.075187 -0.287242]
c_5 PyTorch: [-0.236548  0.075187 -0.287242]
h_5 NumPy  : [-0.128144  0.039916 -0.133268]
h_5 PyTorch: [-0.128144  0.039916 -0.133268]
Selisih maksimum h: 2.776e-17 | c: 5.551e-17
Cocok dengan torch.nn.LSTM: True

Parameter RNN vanilla (D=2, H=3) : 21
Parameter LSTM      (D=2, H=3) : 84
Rasio LSTM / RNN vanilla            : 4.0x

Mari kita telaah kode di atas baris per baris:

  1. Baris 22-29: layer torch.nn.LSTM(2, 3, dtype=torch.float64) menjadi sumber bobot. Matriks weight_ih_l0 berbentuk (4H, D) dan weight_hh_l0 berbentuk (4H, H) — keempat gate digabung dalam satu matriks besar; dua bias PyTorch dijumlahkan menjadi b seperti pada chapter RNN.
  2. Baris 31-34: np.split(W_ih, 4) memotong matriks menjadi empat blok. Urutan blok PyTorch adalah [input, forget, kandidat g, output] — urutan ini wajib benar; tertukarnya blok forget dan input membuat hasil melenceng tanpa galat yang mencurigakan.
  3. Baris 37-45: sekuens masukan berisi nol kecuali langkah t=1 yang bernilai [1,5; -1,0] — token penanda yang harus “ditahan” cell state setelah langkah itu lewat.
  4. Baris 49-63: perulangan forward. Baris 53-56 menghitung keempat gate; baris 57 memperbarui cell state lewat penjumlahan f * c + i * g; baris 58 menghasilkan hidden state. Gate pada t=1 ikut dicetak agar perilakunya terbaca.
  5. Baris 66-68: tensor yang sama dijalankan lewat layer PyTorch; c_n adalah cell state akhir dan out berisi seluruh h_t.
  6. Baris 71-80: verifikasi — selisih maksimum h sebesar 2,78e-17 dan c sebesar 5,55e-17, keduanya setara nol pada presisi float64. Rumus NumPy kita identik dengan implementasi PyTorch.
  7. Baris 83-89: hitungan parameter: RNN vanilla 21, LSTM 84 — tepat 4 kali lipat karena keempat porsi gate memakai struktur bobot yang sama.

Perhatikan jejak pada output setelah token penanda lewat: c berubah paling besar pada t=1 (dari sekitar nol menjadi [-0,339; 0,038; -0,217]), lalu hanya bergeser sedikit pada t=2 sampai t=4 karena masukan kembali nol dan gate yang menimpanya kecil. Inilah perilaku “menahan memori” yang sulit dicapai RNN vanilla pada sekuens panjang.

Dokumentasi resmi torch.nn.LSTM memuat rumus lengkap serta konvensi urutan gate [i, f, g, o] yang kita ikuti; visualisasi gate yang klasik dan mudah diingat tersedia di colah.github.io .

LSTM bukan satu-satunya rancangan gate. Enam tahun kemudian muncul versi yang lebih ramping dengan dua gate saja dan performa yang setara di banyak tugas — Gated Recurrent Unit, pokok bahasan chapter berikutnya.