Semua RNN sejauh ini membaca sekuens satu arah: dari kiri ke kanan, dari masa lalu ke masa depan. Padahal banyak keputusan justru lebih mudah diambil bila masa depan juga terlihat — kata “bank” pada “dia duduk di bank sungai” hanya bisa diterjemahkan atau dilabeli dengan benar setelah kalimat selesai dibaca, dan perekam medis mentranskrip audio jauh lebih akurat ketika frasa sesudahnya ikut dipakai. Bidirectional RNN menjawab kebutuhan itu dengan cara paling sederhana: jalankan dua RNN (atau LSTM/GRU) yang sama persis, satu dari depan ke belakang dan satu dari belakang ke depan, lalu gabungkan hidden state keduanya pada setiap langkah. Chapter ini membongkar mekanismenya, memverifikasinya terhadap torch.nn.LSTM dua arah, dan — sama pentingnya — menjelaskan kapan arsitektur ini justru tidak boleh dipakai.
Dua Arah, Satu Output #
Pada setiap langkah t, RNN maju menghasilkan h_maju(t) yang merangkum x_1 sampai x_t, sedangkan RNN mundur menghasilkan h_mundur(t) yang merangkum x_T sampai x_t — ia membaca sekuens terbalik. Output gabungannya adalah konkatenasi:
h_gabung(t) = [h_maju(t) ; h_mundur(t)] -- panjang 2HSetiap posisi kini membawa informasi dari kedua sisi sekaligus. Implementasi di PyTorch hanya butuh satu argumen: bidirectional=True pada layer, dan dimensi output otomatis menjadi 2*H. RNN mundur bukan RNN baru dengan jenis berbeda — ia LSTM biasa yang dijalankan pada sekuens yang dibalik, dengan set bobotnya sendiri yang terpisah dari arah maju.
Diagram di atas menyoroti satu posisi (langkah ke-2): komponen maju datang dari arah kiri, komponen mundur dari arah kanan, dan keduanya digabung menjadi vektor 2H untuk posisi tersebut.
Verifikasi terhadap torch.nn.LSTM #
Program berikut membangun bi-LSTM pada sekuens kecil, lalu membuktikan bahwa bidirectional=True benar-benar berarti “dua LSTM satu arah yang digabung”: bobot arah maju dan mundur dipisahkan ke dua layer biasa, layer mundur dijalankan pada sekuens terbalik, dan hasil konkatenasinya dibandingkan dengan output bawaan PyTorch:
"""Bidirectional RNN: hidden maju dan mundur digabung, diverifikasi manual.
Layer torch.nn.LSTM dengan bidirectional=True menghasilkan output berdimensi
2*H. Verifikasi: potong bobot arah maju/mundur ke dua LSTM satu arah, jalankan
arah mundur pada sekuens yang dibalik, lalu bandingkan hasil penggabungan.
"""
import numpy as np
import torch
torch.manual_seed(0)
np.random.seed(0)
N, T, D, H = 1, 6, 3, 4
# ---------- Sekuens masukan ----------
x = torch.randn(N, T, D)
print("Shape masukan x :", tuple(x.shape))
print()
# ---------- Layer bidirectional ----------
bi = torch.nn.LSTM(D, H, batch_first=True, bidirectional=True, dtype=torch.float64)
out_bi, (h_n, _) = bi(x.double())
print("Shape output bi-LSTM :", tuple(out_bi.shape), "-> (N, T, 2*H)")
print("Hidden maju (h_T maju) :", np.round(h_n[0, 0].detach().numpy(), 6))
print("Hidden mundur (h_T mund):", np.round(h_n[1, 0].detach().numpy(), 6))
print()
# ---------- Dua LSTM satu arah dengan bobot yang dipisah ----------
maju = torch.nn.LSTM(D, H, batch_first=True, dtype=torch.float64)
mundur = torch.nn.LSTM(D, H, batch_first=True, dtype=torch.float64)
with torch.no_grad():
for satu_arah, arah in ((maju, ""), (mundur, "_reverse")):
satu_arah.weight_ih_l0.copy_(getattr(bi, f"weight_ih_l0{arah}"))
satu_arah.weight_hh_l0.copy_(getattr(bi, f"weight_hh_l0{arah}"))
satu_arah.bias_ih_l0.copy_(getattr(bi, f"bias_ih_l0{arah}"))
satu_arah.bias_hh_l0.copy_(getattr(bi, f"bias_hh_l0{arah}"))
out_maju, _ = maju(x.double())
out_mundur_terbalik, _ = mundur(torch.flip(x.double(), dims=[1]))
out_mundur = torch.flip(out_mundur_terbalik, dims=[1])
manual = torch.cat([out_maju, out_mundur], dim=2)
# ---------- Verifikasi ----------
selisih = (out_bi - manual).abs().max().item()
print("Verifikasi penggabungan manual:")
print(" out_bi[..., :H] == hidden maju :",
bool(torch.allclose(out_bi[..., :H], out_maju, atol=1e-12)))
print(" out_bi[..., H:] == hidden mundur :",
bool(torch.allclose(out_bi[..., H:], out_mundur, atol=1e-12)))
print(f" Selisih maksimum vs torch bidirectional: {selisih:.3e}")
print()
# ---------- Bukti konteks masa depan ----------
print("Nilai h pada langkah t=0:")
print(" maju (lihat x_0..x_0) :", np.round(out_maju[0, 0].detach().numpy(), 6))
print(" mundur (lihat x_0..x_5):", np.round(out_mundur[0, 0].detach().numpy(), 6))
print()
print("Nilai h pada langkah t=5:")
print(" maju (lihat x_0..x_5):", np.round(out_maju[0, 5].detach().numpy(), 6))
print(" mundur (lihat x_5..x_5):", np.round(out_mundur[0, 5].detach().numpy(), 6))Output yang Dihasilkan:
Shape masukan x : (1, 6, 3)
Shape output bi-LSTM : (1, 6, 8) -> (N, T, 2*H)
Hidden maju (h_T maju) : [ 0.285516 0.201805 0.180315 -0.424829]
Hidden mundur (h_T mund): [ 0.105133 -0.081522 -0.073533 -0.221456]
Verifikasi penggabungan manual:
out_bi[..., :H] == hidden maju : True
out_bi[..., H:] == hidden mundur : True
Selisih maksimum vs torch bidirectional: 0.000e+00
Nilai h pada langkah t=0:
maju (lihat x_0..x_0) : [ 0.082419 0.063508 0.072456 -0.095705]
mundur (lihat x_0..x_5): [ 0.105133 -0.081522 -0.073533 -0.221456]
Nilai h pada langkah t=5:
maju (lihat x_0..x_5): [ 0.285516 0.201805 0.180315 -0.424829]
mundur (lihat x_5..x_5): [ 0.085019 0.152062 -0.119252 -0.018916]Mari kita telaah kode di atas baris per baris:
- Baris 14-18: sekuens masukan
(1, 6, 3)— satu sampel, enam langkah, tiga fitur — dibuat dari seed tetap. - Baris 22-26: layer
torch.nn.LSTM(3, 4, bidirectional=True)menghasilkan output(1, 6, 8): dimensi terakhir menjadi2*H = 8. Hidden akhirh_nberukuran(2, N, H)— baris pertama untuk arah maju, baris kedua untuk arah mundur. - Baris 30-38: dua LSTM satu arah dibuat, lalu keempat tensor bobot masing-masing disalin dari layer dua arah. Nama parameter PyTorch memakai akhiran
_reverseuntuk arah mundur, sehinggagetattrdengan akhiran kosong dan"_reverse"mengambil kedua set sekaligus. - Baris 40-42: layer maju dijalankan pada
xasli; layer mundur dijalankan padatorch.flip(x, dims=[1])— sekuens yang dibalik pada sumbu waktu — lalu outputnya dibalik kembali agar berjajar dengan posisi aslinya. - Baris 44-53: konkatenasi pada sumbu terakhir dibandingkan dengan
out_bi: kedua ujiallclosebernilai True dan selisih maksimum 0,000e+00 — penggabungan manual identik bit demi bit dengan implementasi bawaan. - Baris 57-63: pembuktian konteks. Pada t = 0, hidden mundur melihat
x_0sampaix_5(seluruh sekuens) sedangkan hidden maju baru melihatx_0saja; pada t = 5 posisinya bertukar. Inilah informasi masa depan yang diterima tiap posisi.
Kapan Boleh, Kapan Tidak #
Kekuatan bidirectional sekaligus menjadi batas keras pemakaiannya: hidden state setiap langkah memakai data sesudah langkah itu. Sistem yang harus memutuskan sekarang tanpa mengenal masa depan tidak boleh memakainya:
| Skenario | Boleh dua arah? | Alasan |
|---|---|---|
| Pelabelan token (NER, POS tagging) | ya | seluruh kalimat sudah tersedia saat pelabelan |
| Terjemahan dokumen offline | ya | kalimat sumber utuh sebelum decoding |
| Transkripsi audio pasca-rekam | ya | rekaman lengkap diproses setelah selesai |
| Prediksi kata/harga berikutnya | tidak | masa depan justru target yang diprediksi |
| Deteksi anomali streaming real-time | tidak | data berhenti sampai saat ini |
| Chatbot yang membalas token demi token | tidak | balasan dibangkitkan sebelum masa depan ada |
Dokumentasi torch.nn.LSTM
menjelaskan parameter bidirectional, penamaan bobot _reverse, dan bentuk output 2*H yang kita verifikasi di atas.
Dengan tiga arsitektur rekuren di tangan, kita siap menyusunnya menjadi pola yang mengubah satu sekuens menjadi sekuens lain — kerangka di balik terjemahan mesin dan peramalan multi-langkah. Chapter berikutnya: sequence-to-sequence dengan encoder dan decoder.