Pengenalan RNN/LSTM

Rekurensi pada RNN dan LSTM beserta keterbatasannya.

Daftar isi

Topik ini memperkenalkan jaringan saraf berulang (recurrent neural network, RNN) beserta varian LSTM, dua layer PyTorch yang memproses sekuens langkah demi langkah. Pembahasan mencakup mekanisme rekurensi pada nn.RNN, bentuk keluarannya, perluasan menuju nn.LSTM dengan cell state, serta keterbatasan yang kelak memotivasi mekanisme attention. Setelah topik ini, kedua layer tersebut dapat digunakan dengan bentuk input dan output yang benar.

Rekurensi pada RNN#

RNN membaca sekuens satu langkah demi satu langkah sambil memelihara sebuah vektor bernama hidden state. Vektor itu diperbarui pada setiap langkah dengan rumus yang sama, sehingga bobot yang digunakan di langkah mana pun tidak berubah:

ht=tanh⁡(xtWih⊤+bih+ht−1Whh⊤+bhh) h_t = \tanh(x_t W_{ih}^{\top} + b_{ih} + h_{t-1} W_{hh}^{\top} + b_{hh})

Dalam rumus tersebut, xt x_t adalah fitur pada langkah t t , sedangkan ht−1 h_{t-1} adalah hidden state langkah sebelumnya yang menjadi jalur informasi antara langkah. Hidden state awal h0 h_0 diisi nol bila tidak diberikan. Diagram berikut membentangkan proses itu sepanjang waktu agar aliran state terlihat.

Bobot yang sama digunakan ulang pada setiap langkah waktuh1stateh2stateh3statestate diteruskanstate diteruskanx1inputx2inputx3inputstate langkah terakhir = h_n
RNN yang dibentangkan sepanjang waktu

Layer nn.RNN mengimplementasikan rumus di atas. Argumen konstruktor yang terpenting adalah input_size, yaitu jumlah fitur pada satu langkah, dan hidden_size, yaitu ukuran hidden state. Contoh berikut menggunakan data sensor dengan empat fitur, lima langkah, dan dua contoh dalam satu batch.

python rnn-dasar.py
import torch

torch.manual_seed(0)

rnn = torch.nn.RNN(input_size=4, hidden_size=8, batch_first=True)

# input: (batch, panjang_sekuens, fitur) = (2, 5, 4)
x = torch.randn(2, 5, 4)
output, hn = rnn(x)

# output: (2, 5, 8) = (batch, panjang_sekuens, hidden_size)
# hn    : (1, 2, 8) = (num_layers, batch, hidden_size)
print("Bentuk output:", tuple(output.shape))
print("Bentuk hn    :", tuple(hn.shape))
text Keluaran
Bentuk output: (2, 5, 8)
Bentuk hn    : (1, 2, 8)

Keluaran output berisi hidden state untuk setiap langkah, sedangkan hn berisi hidden state terakhir untuk setiap layer. Perlu diperhatikan bahwa dimensi pertama hn adalah jumlah layer, bukan batch, meskipun batch_first=True sudah dipilih.

Keluaran per langkah dan state akhir#

Dua keluaran nn.RNN berkaitan secara langsung. Nilai output pada langkah terakhir sama persis dengan baris layer terakhir hn, sehingga keduanya memuat angka yang sama dengan penyajian yang berbeda:

python rnn-output-vs-hn.py
import torch

torch.manual_seed(0)

rnn = torch.nn.RNN(input_size=4, hidden_size=8, batch_first=True)

# input: (2, 5, 4) = (batch, panjang_sekuens, fitur)
x = torch.randn(2, 5, 4)
output, hn = rnn(x)

# output: (2, 5, 8), hn: (1, 2, 8); bandingkan langkah terakhir output
# dengan lapisan terakhir hn
print("output[:, -1, :] == hn[-1] :", torch.equal(output[:, -1, :], hn[-1]))
print("Selisih maksimum           :", (output[:, -1, :] - hn[-1]).abs().max().item())
text Keluaran
output[:, -1, :] == hn[-1] : True
Selisih maksimum           : 0.0

Pilihan di antara keduanya bergantung pada tugas. Untuk klasifikasi sekuens, state akhir lazim digunakan sebagai ringkasan seluruh sekuens sebelum dimasukkan ke layer pengklasifikasi. Untuk tugas yang menghasilkan keluaran di setiap langkah, seperti pelabelan per kata, output yang berukuran sama dengan sekuensnya itulah yang digunakan.

Bentuk output yang sama panjang dengan sekuensnya juga memudahkan penyambungan ke layer lain. Keluaran setiap langkah dapat langsung diberikan ke nn.Linear tanpa pengolahan tambahan apabila prediksi diperlukan per langkah. Apabila hanya akhir sekuens yang penting, banyak implementasi mengambil hn semata agar tensor perantara tidak perlu dipelihara. Pilihan antara keduanya ditentukan oleh bentuk keluaran tugas, bukan oleh kemampuan layer, karena angka yang dibandingkan di atas memang sama. Pada penggunaan lanjutan, keluaran per langkah dari layer terbawah juga menjadi masukan bagi layer rekuren di atasnya.

LSTM: gerbang dan cell state#

RNN murni menulis ulang hidden state sepenuhnya pada setiap langkah, sehingga informasi lama mudah terhapus oleh input baru. LSTM (long short-term memory) menambahkan cell state ct c_t , sebuah jalur memori yang mengalir antara langkah dan diatur oleh tiga gerbang bernilai nol sampai satu, yaitu gerbang input it i_t , gerbang forget ft f_t , dan gerbang output ot o_t . Pembaruan cell state-nya mengikuti

ct=ft⊙ct−1+it⊙gt,ht=ot⊙tanh⁡(ct) c_t = f_t \odot c_{t-1} + i_t \odot g_t, \qquad h_t = o_t \odot \tanh(c_t)

dengan ⊙ \odot sebagai perkalian per elemen, sedangkan gt g_t adalah usulan konten baru. Gerbang forget yang bernilai besar membuat informasi lama bertahan di cell state tanpa ditulis ulang penuh. Layer nn.LSTM mengembalikan satu tuple tambahan karena cell state ikut dikeluarkan:

python lstm-dasar.py
import torch

torch.manual_seed(0)

lstm = torch.nn.LSTM(input_size=4, hidden_size=8, batch_first=True)

# input: (batch, panjang_sekuens, fitur) = (2, 5, 4)
x = torch.randn(2, 5, 4)
output, (hn, cn) = lstm(x)

# output: (2, 5, 8) = (batch, panjang_sekuens, hidden_size)
# hn, cn: (1, 2, 8) = (num_layers, batch, hidden_size)
print("Bentuk output:", tuple(output.shape))
print("Bentuk hn    :", tuple(hn.shape))
print("Bentuk cn    :", tuple(cn.shape))

# dua lapisan: dimensi pertama hn dan cn ikut bertambah
lstm_2lapis = torch.nn.LSTM(input_size=4, hidden_size=8, num_layers=2, batch_first=True)
output2, (hn2, cn2) = lstm_2lapis(x)
print("Dua lapisan, hn:", tuple(hn2.shape))
text Keluaran
Bentuk output: (2, 5, 8)
Bentuk hn    : (1, 2, 8)
Bentuk cn    : (1, 2, 8)
Dua lapisan, hn: (2, 2, 8)

Bentuk output, hn, dan cn mengikuti pola nn.RNN, dengan tambahan bahwa cn memuat cell state per layer. Argumen num_layers menumpuk beberapa layer secara vertikal, dan terlihat pada keluaran bahwa dimensi pertama hn ikut bertambah mengikuti jumlah layer, sedangkan dimensi batch tetap di tengah. Varian nn.GRU menyederhanakan gerbang LSTM menjadi dua, dengan pola bentuk keluaran yang sama.

Keterbatasan rekurensi#

Keterbatasan pertama bersifat matematis. Gradien yang mengalir mundur melewati ratusan langkah waktu cenderung mengecil hingga hilang, sehingga hubungan antara dua kata yang jauh terpisah sulit dipelajari oleh RNN murni. Cell state pada LSTM meredam masalah ini, tetapi tidak menghapusnya, dan sekuens yang sangat panjang tetap menantang bagi keluarga layer rekuren.

Keterbatasan kedua bersifat komputasi. Langkah t t harus menunggu hidden state langkah t−1 t-1 selesai, sehingga perhitungan sepanjang waktu tidak dapat diparalelkan dan pelatihan menjadi lambat pada sekuens panjang. LSTM memperpanjang rentang ketergantungan yang dapat dipelajari, tetapi biaya komputasi sekuensialnya tetap ada. Untuk meredam kedua masalah tersebut, praktik umum membatasi panjang sekuens, menumpuk beberapa layer rekuren, atau memotong gradien yang meledak dengan gradient clipping.

Kedua keterbatasan ini pula yang mendorong mekanisme attention, yang memungkinkan setiap posisi mengakses seluruh sekuens secara langsung tanpa menumpuk informasi pada satu rantai state.

Kesalahan umum#

Kesalahan pertama adalah memberikan tensor tanpa mengatur batch_first=True. Layer kemudian membaca (batch, panjang_sekuens, fitur) sebagai (panjang_sekuens, batch, fitur), dan bila dua dimensi depan kebetulan berukuran sama, tidak ada error yang muncul, hanya hasil yang salah tanpa disadari. Opsi batch_first=True sebaiknya dituliskan secara eksplisit sejak konstruksi layer.

Kesalahan kedua adalah memberikan indeks token langsung ke layer rekuren tanpa konversi. Indeks token bertipe long, sedangkan bobot layer bertipe float, sehingga pemanggilan berhenti dengan pesan berikut:

python indeks-ke-rnn.py
import torch

rnn = torch.nn.RNN(input_size=4, hidden_size=8, batch_first=True)

# indeks token bertipe long diberikan langsung ke nn.RNN
x = torch.zeros(2, 5, dtype=torch.long)   # (2, 5) = (batch, panjang_sekuens)

try:
    output, hn = rnn(x)
except (RuntimeError, ValueError) as e:
    print(type(e).__name__ + ":", e)
text Keluaran
ValueError: RNN input dtype (torch.int64) does not match weight dtype (torch.float32). Convert input: input.to(torch.float32), or convert model: model.to(torch.int64)

Pesan errornya menyarankan konversi dtype, tetapi jalan yang benar untuk data teks bukan mengubah indeks menjadi float. Indeks token diteruskan ke layer embedding terlebih dahulu, dan keluaran bertipe float itulah yang dimasukkan ke layer rekuren.

Kesalahan ketiga adalah menganggap output atau hn sebagai prediksi akhir. Keduanya masih berupa hidden state berukuran hidden_size, bukan kelas atau nilai yang diinginkan, sehingga layer lain, lazimnya nn.Linear, ditempatkan di atasnya untuk mengubah ringkasan tersebut menjadi keluaran tugas.

Ringkasan#

  • RNN memproses sekuens langkah demi langkah dengan hidden state yang diteruskan antara langkah, dan satu set bobot digunakan ulang di semua langkah.
  • output berisi hidden state setiap langkah dari layer terakhir, sedangkan hn berisi state akhir per layer dengan bentuk (num_layers, batch, hidden_size) apa pun nilai batch_first.
  • LSTM menambahkan cell state dan tiga gerbang agar informasi jarak jauh bertahan lebih baik daripada RNN murni.
  • Rekurensi menyulitkan paralelisasi dan melemahkan gradien pada sekuens panjang, dua alasan kemunculan attention.

Referensi#