Recurrent Neural Network — Part Three

Backpropagation Through Time and Vanishing Gradient

Original Works by Denny Britz | Dipahami oleh Ron Ashrovy

Pada postingan saya sebelumnya, kami sudah membahas bagai membangun RNN dari nol, tetapi kami belum membahas secara detail bagaimana kinerja algoritma Backpropagation Through Time (BPTT) didalam menghitung gradients. Pada bagian ini kami akan memberikan overview singkat mengenai BPTT dan menjelaskan perbedaannya dengan backpropagation biasa. Dan disini kami akan mencoba menerangkan masalah pada vanishing gradient, yang menyebabkan perkembangan LSTM dan GRU, keduanya merupakan model yang populer digunakan pada NLP. Vanishing gradient ditemukan oleh Sepphochreiter pada tahun 1991 dan kembali menarik perhatian publik semenjak berkembangnya deep architecture.

Supaya anda bisa lebih mengerti tutorial pada bagian ini penulis menyarankan anda untuk mengerti dasar-dasar kerja backpropagation. Jika anda tidak begitu paham, beberapa tutorial bisa anda ikuti pada 3 link berikut (1,2,3). Harapan kami anda kesulitan anda untuk memahami bagaian ini tidak begitu rumit.


Backpropagation Through Time (BPTT)

Gambaran diatas ialah sama halnya dengan algoritma backpropagation secara standar dan kami menggunakan didalam deep FeedForward Neural Networks. Perbedaannya kami menambahkan gradient untuk W disetiap time step. Pada NN biasa kita tidak membagikan parameter ke seluruh layer, jadi kita tidak perlu menjumlahkan apapun. Tetapi menurut opini penulis, BPTT adalah backpropagation untuk meng-unrolled (membeberkan) RNN. Sama seperti dengan Backpropagation untuk menentukan delta vector dengan cara mundur.

dengan

Pada coding, implementasi BPTT seperti ini:

def bptt(self, x, y):
T = len(y)
# Perform forward propagation
o, s = self.forward_propagation(x)
# We accumulate the gradients in these variables
dLdU = np.zeros(self.U.shape)
dLdV = np.zeros(self.V.shape)
dLdW = np.zeros(self.W.shape)
delta_o = o
delta_o[np.arange(len(y)), y] -= 1.
# For each output backwards...
for t in np.arange(T)[::-1]:
dLdV += np.outer(delta_o[t], s[t].T)
# Initial delta calculation: dL/dz
delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))
# Backpropagation through time (for at most self.bptt_truncate steps)
for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)
# Add to gradients at each previous step
dLdW += np.outer(delta_t, s[bptt_step-1])
dLdU[:,x[bptt_step]] += delta_t
# Update delta for next step dL/dz at t-1
delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)
return [dLdU, dLdV, dLdW]

Ini sedikit memberikan gambaran bahwa RNN biasa saja sulit dilatih: Urutan (sentence) bisa sangat panjang, mungkin 20 atau lebih, dan itu membuat anda butuh untuk mem-back-propagate melalui banyak layer. Pada prakteknya kebanyakan orang melakukan truncate backpropagation menjadi beberapa step saja.

The Vanishing Gradient Problem

Pada part pertama kami menyebutkan bahwa RNN memiliki kesulitan didalam mempelajari ketergantungan jarak jauh (long-range dependencies) - interaksi antar kata yang terpisah beberapa bagian.

Hal ini menjadi problematika karena kalimat didalam bahasa inggris jika diterjemahkan seringkali tidak ditentukan oleh kalimat yang tidak dekat dengannya, contoh: “The man who wore a wig on his head went inside”. Kalimat dimaksudkan mengenai Man yang bergerak menuju ke dalam (went inside) bukan tentang wig-nya. Sayangnya, RNN biasa tidak mungkin menangkap informasi yang seperti ini. Mengapa? Silakan liat proses gradient yang akan kita hitung dibawah ini:

Perhatikan…

adalah chain rule itu sendiri! Yang chain rule didalamnya terdapat value didalam value. Contoh:

Catat juga hal ini karena kita mengambil derivatif (turunan) dari fungsi vektor yang berkenaan dengan sebuah vektor, hasilnya ialah matrix (yang disebut dengan Jacobian matrix) yang element-nya adalah semua turunannya. Maka kita akan bisa me-nuliskan semua gradient-nya:

Untuk lebih detail dapat dilihat pada paper ini, ternyata 2-norm yang bisa dibilang nilai absolute dari matrix jacobian diatas memiliki batas atas 1. Secara intuitif ini masuk akal karena tanh (atau sigmoid) fungsi aktivasi me-mapping semua nilai kedalam nilai dengan jarak -1 dan 1, dan derivatif dibatasi dengan 1 (1/4 pada sigmoid), berikut adalah gambar tanh dan derivatif:

Anda dapat melihat bahwa tanh dan fungsi sigmoid memiliki derivatif 0 pada keduanya. Keduanya mendekati sudut datar. Bila hal ini terjadi maka neuron bisa dikatakan jenuh.
Mereka memiliki gradient nol dan gradient yang lainnya layer sebelumnya juga menuju ke arah 0. Jadi, dengan nilai kecil didalam matrix dan banyak matrix multiplication (t-k pada khususnya) nilai gradient akan secara exponensial menyusut dengan cepat, dan akhirnya lenyap tanpa tersisa dalam beberapa time step.

Kontribusi gradient dari step yang “jauh” menjadi nol, kondisi pada langkah-langkah tersebut menjadi tidak berguna terhadap apa yang sedang mesin anda pelajari: Maka anda berakhir dengan tidak mempelajari long-range dependencies. Vanishing gradient tidak hanya pada RNN. Vanishing Gradient juga terjadi pada deep Feedforward Neural Networks. Hanya pada RNN cenderung cukup membuat masalah ini terasa lebih umum.

Kerugian lainnya yang bisa kita bayangkan ialah kebergantungan fungsi aktivasi dan parameter didalam jaringan kita, explode kemudian terjadi vanishing gradient bisa terjadi apabila matrix jacobian-nya besar. Alasan mengapa vanishing gradient lebih diperhatikan dibandingkan explode gradient ada dua. Pertama, exploding gradient itu jelas.

Yaitu gradient anda akan menjadi NaN (not a number) dan program anda akan rusak.

Kedua, pemotongan gradient pada treshold atau titik ambang telah ditentukan sebelumnya (dibahas pada paper ini) hal ini menjadi solusi simple dan efektif pada exploding gradients. Sedangkan pada vanishing gradient tidak terlihat jelas saat kemunculan mereka dan bagaimana cara untuk mengatasinya.

Untungnya, ada beberapa cara untuk mengatasi vanishing gradient, diantaranya inisialisasi matriks W yang tepat dapat mengurangi efek vanishing gradient. Jadi bisa diatur sejak awal.

Solusi yang lebih disukai adalah menggunakan ReLU daripada menggunakan fungsi aktivasi tanh atau sigmoid. Derivatif ReLU adalah konstanta 0 atau 1, jadi tidak mungkin terjadi vanishing gradient.

Solusi yang lebih populer lagi menggunakan arsitektur Long Short-Term Memory (LSTM) atau Gated Recurrent Unit (GRU). LSTM pertama kali diusulkan pada tahun 1997 dan merupakan model yang paling banyak digunakan pada NLP saat ini. Sedangkan GRU, pertama kali diajukan pada tahun 2014 adalah versi LSTM yang disederhanakan. Kedua arsitektur RNN ini dirancang secara eksplisit untuk menangani vanishing gradient dan secara efesien mempelajari long-range dependencies. Penulis akan lebih banyak membahas pada Part 4