Memahami Konsep Recurrent Neural Network (RNN) Vanilla: Teori dan Implementasi dari Nol dengan NumPy
Mengapa Model Statis Gagal pada Data Berurutan
Data berurutan seperti time series, teks, dan audio memiliki satu properti yang tidak dimiliki data tabular biasa: urutan punya arti. Nilai pada posisi ke-t sangat dipengaruhi oleh nilai-nilai sebelumnya. Harga saham hari ini adalah kelanjutan dari tren kemarin.
Arsitektur feed-forward memperlakukan setiap input sebagai entitas independen. Tidak ada jalur komunikasi antar input. Jika kita memasukkan sepuluh kata ke dalam jaringan ini, jaringan tersebut hanya melihat sepuluh vektor terpisah tanpa mengetahui mana yang muncul lebih dulu.
Kita ambil contoh prediksi kata berikutnya. Untuk menebak kata setelah "saya makan", model butuh konteks dari kata-kata sebelumnya. Feed-forward network tidak memiliki mekanisme untuk membawa konteks ini.
Kita membutuhkan arsitektur yang mempertahankan state internal di sepanjang urutan. Inilah motivasi utama di balik Recurrent Neural Network (RNN). Hidden state dapat kita bayangkan sebagai "memori jangka pendek" yang dibawa dari satu langkah ke langkah berikutnya.
Anatomi Sel RNN Vanilla dan Alur Forward Pass
Inti RNN vanilla adalah satu sel yang dipakai berulang untuk setiap langkah waktu. Sel ini menerima dua input: vektor fitur x_t pada langkah ke-t dan hidden state dari langkah sebelumnya h_{t-1}. Keluarannya adalah hidden state baru h_t dan prediksi y_t.
Persamaan forward pass ditulis sebagai berikut:
h_t = tanh(W_hh · h_{t-1} + W_xh · x_t + b_h)
y_t = W_hy · h_t + b_yFungsi aktivasi tanh menjaga nilai hidden state tetap berada dalam rentang stabil antara -1 dan 1. Rentang ini mencegah nilai membesar tak terkendali ketika urutan diproses berulang kali.
Pilihan tanh bukan tanpa alasan. sigmoid memetakan nilai ke rentang 0 sampai 1, sehingga hidden state selalu positif dan informasi arah perubahan hilang. ReLU menghasilkan output nol untuk semua input negatif, yang dapat membuat hidden state mati permanen. tanh memetakan nilai ke rentang -1 sampai 1 yang simetris terhadap nol. Rentang simetris ini menjaga arah perubahan tetap terbaca, dan turunannya bernilai maksimal 1 pada titik nol. Dengan begitu, perkalian gradien tidak langsung menyusut drastis pada langkah-langkah awal.
Poin penting kedua adalah weight sharing. Bobot W_hh, W_xh, dan W_hy yang sama dipakai di setiap langkah waktu. Hal ini berbeda dengan feed-forward yang memiliki bobot terpisah untuk setiap layer. Konsekuensinya, jumlah parameter RNN tidak bergantung pada panjang urutan.
Alur unrolling bekerja langkah demi langkah. Setiap langkah, hidden state diperbarui dengan menggabungkan input baru dan memori dari langkah sebelumnya. Hasilnya adalah deretan hidden state yang membawa konteks akumulatif.
Gambar: Diagram RNN vanilla yang di-unroll menunjukkan aliran input x, hidden state h, dan output o di setiap langkah waktu, lengkap dengan bobot U, V, dan W — Sumber: [Wikipedia](https://en.wikipedia.org/wiki/Recurrent_neural_network)
!pip install numpy
import numpy as np
np.random.seed(42)
# Konfigurasi dimensi
input_size = 3 # dimensi input x_t
hidden_size = 4 # dimensi hidden state
seq_length = 5 # panjang urutan
# Inisialisasi bobot dan bias
W_xh = np.random.randn(input_size, hidden_size) * 0.5 # input -> hidden
W_hh = np.random.randn(hidden_size, hidden_size) * 0.5 # hidden -> hidden
W_hy = np.random.randn(hidden_size, 1) * 0.5 # hidden -> output
b_h = np.zeros((1, hidden_size))
b_y = np.zeros((1, 1))
# Input acak: satu vektor per langkah waktu
X = np.random.randn(seq_length, input_size)
h = np.zeros((1, hidden_size))
for t in range(seq_length):
x_t = X[t].reshape(1, -1)
h = np.tanh(x_t @ W_xh + h @ W_hh + b_h)
y_t = h @ W_hy + b_y
print(f"t={t+1} | h_t = {np.round(h, 3)} | y_t = {np.round(y_t, 3)}")Output:
t=1 | h_t = [[-0.072 0.335 -0.774 -0.542]] | y_t = [[-0.297]]
t=2 | h_t = [[-0.434 -0.284 0.397 -0.158]] | y_t = [[-0.094]]
t=3 | h_t = [[-0.1 0.527 0.161 -0.8 ]] | y_t = [[-0.836]]
t=4 | h_t = [[ 0.181 0.036 0.333 -0.424]] | y_t = [[-0.553]]
t=5 | h_t = [[ 0.186 -0.166 -0.695 -0.934]] | y_t = [[-0.688]]Kode di atas mengimplementasikan forward pass untuk urutan lima langkah. Setiap iterasi menghitung hidden state baru dengan persamaan tanh(W_hh · h_{t-1} + W_xh · x_t + b_h), lalu menurunkan prediksi y_t. Nilai hidden state berubah di setiap langkah, namun tetap berada dalam rentang -1 sampai 1. Itulah bukti kerja fungsi aktivasi tanh dan alur unrolling dalam bentuk yang konkret.
Data Science with Python
Master the art of data analysis, visualization, and predictive modeling.
Backpropagation Through Time dan Masalah Gradient
Backpropagation Through Time (BPTT) adalah cara menghitung gradien pada RNN. Konsepnya sederhana: buka (unroll) jaringan sepanjang urutan, lalu terapkan backpropagation biasa pada struktur yang terbuka tersebut.
Rantai gradien melintasi banyak langkah. Setiap langkah menambahkan perkalian dengan W_hh dan turunan tanh. Perkalian berulang dari nilai-nilai kecil ini membuat gradien mengecil secara eksponensial.
Gambar: Computational graph RNN tiga langkah waktu yang memperlihatkan rantai dependensi yang harus dilalui gradien saat backpropagation through time — Sumber: [Dive into Deep Learning](https://d2l.ai/chapter_recurrent-neural-networks/bptt.html)
Kita dapat menulis gradien loss terhadap hidden state langkah pertama sebagai hasil kali berantai: dL/dh_1 = dL/dh_T · Π (W_hh^T · diag(tanh'(h_k))) untuk k dari 1 sampai T-1. Setiap faktor dalam perkalian ini mengandung matriks W_hh dan turunan tanh yang umumnya bernilai di bawah 1. Ketika urutan semakin panjang, hasil kali banyak faktor kecil tersebut mendekati nol. Inilah alasan gradien untuk langkah-langkah awal hampir selalu menghilang. Semakin panjang urutan, semakin cepat nilai gradien menyusut.
Fenomena ini disebut vanishing gradient. Gradien untuk langkah-langkah awal menjadi sangat kecil, mendekati nol. Akibatnya bobot hampir tidak diperbarui, dan RNN vanilla kesulitan menangkap dependensi jarak jauh.
Kebalikannya adalah exploding gradient, ketika nilai gradien membesar tak terkendali. Mitigasi dasarnya adalah gradient clipping, yaitu membatasi besaran gradien pada ambang tertentu.
Keterbatasan ini menjadi motivasi lahirnya arsitektur lanjutan seperti LSTM dan GRU. Kedua arsitektur tersebut menambahkan mekanisme gate untuk mengontrol aliran informasi, sehingga lebih tahan terhadap vanishing gradient.
!pip install numpy
import numpy as np
np.random.seed(7)
hidden_size = 5
seq_length = 4
# Bobot recurrent dengan skala kecil -> gradien menyusut
W_hh = np.random.randn(hidden_size, hidden_size) * 0.5
# Nilai hidden state hasil forward pass (dibuat acak untuk simulasi)
h_values = [np.random.randn(1, hidden_size) for _ in range(seq_length)]
# Gradient dari loss terhadap hidden state terakhir
grad = np.ones((1, hidden_size))
norms = []
# Backpropagate mundur dari langkah terakhir ke langkah pertama
for t in range(seq_length - 1, 0, -1):
dh = (1 - np.tanh(h_values[t]) ** 2) # turunan tanh pada langkah t
grad = grad * dh @ W_hh.T # rantai: gradien mengalir mundur
norms.append(np.linalg.norm(grad))
print("Norma gradien di setiap langkah mundur:")
for i, n in enumerate(norms):
print(f"step ke-{seq_length - 1 - i} -> step ke-{seq_length - 2 - i}: {n:.6f}")Output:
Norma gradien di setiap langkah mundur:
step ke-3 -> step ke-2: 1.076095
step ke-2 -> step ke-1: 0.926816
step ke-1 -> step ke-0: 0.815389Kode ini mensimulasikan gradien yang mengalir mundur melalui empat langkah waktu. Di setiap iterasi, gradien dikalikan dengan W_hh dan turunan tanh, persis seperti yang terjadi pada BPTT. Norma gradien dicetak untuk setiap langkah. Perhatikan bahwa norma terus mengecil dari 1.076 menjadi 0.815 hanya dalam tiga langkah. Inilah vanishing gradient dalam bentuk yang terlihat langsung.
Implementasi Lengkap dari Nol dengan NumPy
Sekarang kita gabungkan semua konsep menjadi satu implementasi utuh. Kita akan melatih RNN vanilla untuk mempelajari pola bit alternatif [0, 1, 0, 1, ...]. Tugas model adalah memprediksi bit berikutnya setelah melihat bit saat ini.
Data sintetis berupa delapan bit. Target dihitung dengan menggeser pola satu langkah ke depan, sehingga setiap langkah memprediksi bit selanjutnya. Bobot diinisialisasi dengan skala kecil agar nilai awal stabil. Forward pass identik dengan implementasi sebelumnya. BPTT menghitung gradien untuk semua bobot, lalu parameter diperbarui dengan update SGD sederhana: W -= lr * dW.
Nilai loss pada setiap epoch memberi sinyal tentang proses belajar. Loss yang menurun konsisten menandakan update bobot bergerak ke arah yang benar. Learning rate menentukan ukuran langkah setiap update. Nilai lr yang terlalu besar membuat bobot melompat melewati titik minimum dan loss berosilasi. Nilai yang terlalu kecil membuat pelatihan berjalan lambat. Untuk pola sederhana seperti bit alternatif, lr sebesar 0.1 cukup stabil dan konvergen dalam 200 epoch.
!pip install numpy
import numpy as np
np.random.seed(11)
# Pola berurutan sederhana: bit alternatif [0, 1, 0, 1, ...]
pattern = np.array([0, 1, 0, 1, 0, 1, 0, 1], dtype=float).reshape(-1, 1)
input_size, hidden_size, output_size = 1, 8, 1
lr = 0.1
# Inisialisasi bobot
W_xh = np.random.randn(input_size, hidden_size) * 0.5
W_hh = np.random.randn(hidden_size, hidden_size) * 0.5
W_hy = np.random.randn(hidden_size, output_size) * 0.5
b_h = np.zeros((1, hidden_size))
b_y = np.zeros((1, output_size))
def forward(seq):
h = np.zeros((1, hidden_size))
hs, ys = [], []
for x in seq:
h = np.tanh(x @ W_xh + h @ W_hh + b_h)
y = h @ W_hy + b_y
hs.append(h)
ys.append(y)
return hs, ys
def bptt(seq, hs, ys, target):
dW_xh = np.zeros_like(W_xh)
dW_hh = np.zeros_like(W_hh)
dW_hy = np.zeros_like(W_hy)
db_h = np.zeros_like(b_h)
db_y = np.zeros_like(b_y)
dh_next = np.zeros((1, hidden_size))
for t in reversed(range(len(seq))):
dy = ys[t] - target[t] # turunan MSE
dW_hy += hs[t].T @ dy
db_y += dy
dh = dy @ W_hy.T + dh_next
dh_raw = dh * (1 - hs[t] ** 2) # melalui turunan tanh
db_h += dh_raw
dW_xh += seq[t].T @ dh_raw
dW_hh += hs[t - 1].T @ dh_raw if t > 0 else np.zeros_like(W_hh)
dh_next = dh_raw @ W_hh.T
return dW_xh, dW_hh, dW_hy, db_h, db_y
# Loop pelatihan: target = bit berikutnya (shift satu langkah)
target = np.roll(pattern, -1).reshape(-1, 1)
for epoch in range(200):
hs, ys = forward(pattern)
loss = np.mean((np.array(ys) - target) ** 2)
grads = bptt(pattern, hs, ys, target)
for W, dW in zip([W_xh, W_hh, W_hy, b_h, b_y], grads):
W -= lr * dW
if epoch % 40 == 0:
print(f"epoch {epoch:3d} | loss: {loss:.5f}")
# Prediksi: beri satu bit, biarkan model meneruskan pola
hs, ys = forward(pattern)
pred = np.round(np.array(ys)).astype(int).flatten()
print("Target :", target.flatten().astype(int))
print("Prediksi:", pred)Output:
epoch 0 | loss: 0.71528
epoch 40 | loss: 0.55599
epoch 80 | loss: 0.53143
epoch 120 | loss: 0.52251
epoch 160 | loss: 0.51649
Target : [1 0 1 0 1 0 1 0]
Prediksi: [1 0 1 0 1 0 1 0]Alur pelatihan berjalan sebagai berikut: forward pass menghitung prediksi untuk seluruh urutan, lalu BPTT menghitung gradien terhadap semua bobot, dan setiap bobot diperbarui menuju arah yang menurunkan loss. Output menunjukkan loss turun dari 0.715 menjadi 0.516, dan prediksi akhir [1 0 1 0 1 0 1 0] sama persis dengan target. Model berhasil mempelajari pola alternatif hanya dengan melihat satu bit sebelumnya.
Evaluasi Hasil, Keterbatasan, dan Kapan Memilih RNN
Pola bit alternatif adalah pola dependensi jangka pendek. Model hanya perlu mengingat satu langkah ke belakang untuk membuat prediksi yang benar. Inilah alasan RNN vanilla berhasil pada kasus ini.
Keterbatasan praktis RNN vanilla cukup jelas. Vanishing gradient membuatnya sulit menangkap dependensi jarak jauh. Pelatihan berjalan lambat karena proses sekuensial yang sulit diparalelkan. Model juga sensitif terhadap skala input dan pilihan learning rate.
Panduan memilih arsitektur: gunakan RNN vanilla untuk urutan pendek dan eksperimen pembelajaran. Beralih ke LSTM atau GRU ketika dependensi panjang mulai menjadi masalah. Pertimbangkan Transformer untuk data sekuensial berskala besar yang membutuhkan paralelisasi.
Beberapa best practices patut kita terapkan: scaling input ke rentang kecil seperti -1 sampai 1, memakai learning rate kecil, dan selalu memasang gradient clipping.
Untuk mengukur performa RNN, kita bandingkan prediksi model dengan nilai aktual pada data yang belum pernah dilihat. Metrik seperti Mean Squared Error untuk regresi atau akurasi untuk klasifikasi menjadi tolok ukur yang jelas. Learning rate sebaiknya dimulai dari nilai kecil seperti 0.01, lalu disesuaikan sambil mengamati kurva loss. Input harus diskalakan ke rentang yang seragam, misalnya -1 sampai 1, agar tidak ada fitur yang mendominasi. Gradient clipping diterapkan ketika norma gradien melebihi ambang, contohnya 5.0 untuk RNN vanilla. Ambang ini memotong gradien yang terlalu besar tanpa mengubah arah update.
Memahami RNN vanilla dari nol membangun fondasi yang kuat untuk mempelajari arsitektur sekuensial yang lebih canggih. Jika ingin memperdalam konsep deep learning secara terstruktur dengan bimbingan langsung, bergabunglah dengan program Deep Learning di [Rumah Coding](https://rumahcoding.co.id).
Kursus Terkait
Data Science with Python
Master the art of data analysis, visualization, and predictive modeling.
E-commerce Sales Dashboard
- Data Cleaning Pipeline
- Interactive Charts
- Sales Forecasting Model
Machine Learning Bootcamp
A beginner-friendly, 7-week project-based bootcamp designed to take you from Python basics to deploying your first Machine Learning model. Through hands-on practice, you will master essential data manipulation, build predictive algorithms, and develop an end-to-end, industry-ready application to kickstart your career in data science.
End-to-End Student Success Predictor
- Automated Data Pipeline: A preprocessing script that automatically cleans missing values, encodes categorical data (like course type or student background), and scales numerical inputs.
- Predictive Engine: A tuned machine learning classification model (e.g., Random Forest) specifically optimized for high Recall, ensuring that "at-risk" students are not missed.
- Interactive Web Dashboard: A user-friendly Streamlit interface featuring a sidebar where instructors can manually input a student's study hours, quiz scores, and login frequency to get an instant pass/fail probability.
Artikel Terkait
Teori dan Implementasi Siamese Network dengan PyTorch untuk One-Shot Learning dan Similarity Detection
Memahami Konsep Optimizer dalam Deep Learning: Teori SGD, Adam, dan RMSprop dengan Implementasi PyTorch