Tutorial Implementasi Recurrent Neural Networks (RNNs) dengan Tensorflow ™

Alfan Farizki Wicaksono

Information Retrieval Lab.
Fakultas Ilmu Komputer, Universitas Indonesia

Kali ini, kita akan coba implementasikan RNNs yang setiap sample mempunyai panjang sequence yang berbeda-beda. Trik untuk melakukan hal ini adalah dengan online learning, yaitu kita update parameter sampe demi sample. Proses pengembangan graf komputasi dilakukan untuk satu sample, dan bukan untuk satu batch atau mini-batch. Kelemahan dari cara ini adalah proses training yang sangat lama.

Pada kuliah sebelumnya, proses komputasi yang terjadi pada Recurrent Neural Networks (RNNs) adalah sebagai berikut.

S_t = tanh(W^{(xh)} X_t + W^{(hh)} S_{t-1} + b_s)
P_t = W^{(hy)} S_t + b_p

dengan,

Kali ini, untuk kemudahan implementasi, kita akan melakukan sedikit trik terhadap persamaan di atas. Dapat dibuktikan bahwa:

W^{(xh)} X_t + W^{(hh)} S_{t-1} = W [X_t; S_{t-1}]

dengan, [X_t; S_{t-1}] adalah konkatenasi dua buah vektor tersebut. W seperti seolah-olah merupakan gabungan antara W^{(xh)} dan W^{(hh)}. Jadi, untuk implementasi kita kali ini, kita akan menggunakan proses komputasi berikut:

S_t = tanh(W [X_t; S_t-1] + b_s)
P_t = U S_t + b_p

dengan U hanyalah nama lain dari W^{(hy)}.

Selain itu, kita tidak menerapkan sama sekali feature engineering dalam pekerjaan kali ini. Artinya, pada bagian input RNNs, kita akan lakukan representation learning yang secara otomatis akan mempelajari representasi vektor untuk setiap kata pada sequence. Hal ini bisa dilakukan dengan cara membuat sebuah matriks embeddings C \in R^{(|V| \times N)}, dengan V dan N adalah vocabulary dan ukuran input (vektor input ke RNNs). Jadi, pada bagian input,

X_t = C(w_t)

dengan,

Pertama, kita import terlebih dahulu beberapa library yang akan kita gunakan pada pekerjaan kali ini.


from operator import itemgetter
import tensorflow as tf
import numpy as np
import sys

Kedua, kita buat terlebih dahulu sebuah kelas untuk merepresentasikan sebuah urutan kata pada kalimat menjadi urutan indeks (integer). Artinya, kita perlu membuat Vocabulary yang berisi dictionary dimana key adalah kata dan value adalah indeks integer dari kata tersebut.


class Vocab():
    def __init__(self):
        self.vocab_size = 0
        self.word_idx = {}
        self.is_vocab_constructed = False
        self.unknown = ''

    # tambahkan sebuah kata
    def add_word(self, word):
        if (word not in self.word_idx):
            self.word_idx[word] = self.vocab_size
            self.vocab_size += 1

    # dapatkan index sebuah kata
    def get_idx(self, word):
        if (word not in self.word_idx):
            return self.word_idx[self.unknown]
        else:
            return self.word_idx[word]

    # sentences: list of lists of words (list of sentences)
    def create_vocab(self, sentences):
        self.add_word(self.unknown)
        for sentence in sentences:
            for word in sentence:
                self.add_word(word)
        self.is_vocab_constructed = True

    # ubat list of words ke list of idxs
    def to_idx(self, sentence):
        if not self.is_vocab_constructed:
            raise Exception('vocabulary belum dibangun')

        wid = []
        for word in sentence:
            wid.append(self.get_idx(word))
        return wid

    # ubah list of lists of words ke list of lists of idxs
    def to_idx_list(self, sentences):

        res = []
        for sentence in sentences:
            res.append(self.to_idx(sentence))
        return res

Kemudian, kita juga perlu membuat fungsi untuk pengelolaan dataset, seperti memuat data dan mengubah sequence of indexes (representasi dari kalimat) menjadi bentuk kategorikal.


#fungsi untuk load dataset
def load_data(dataset):
    sentences = []
    labels = []
    
    file = open(dataset, "r")
    for line in file:
        sent_label = line.split('###')
        sentences.append(sent_label[0])
        labels.append(int(sent_label[1]))
    return sentences, labels

# fungsi dipersiapkan seandainya kedepannya dibutuhkan
# labels adalah list of integer [1, 0, 3]
# output numpy categorical: [[ 0.,  1.,  0.,  0.], [ 1.,  0.,  0.,  0.], [ 0.,  0.,  0.,  1.]]
def to_categorical(labels, num_labels):
    len_labels = len(labels)
    zeros = np.zeros((len_labels, num_labels))
    zeros[np.arange(len_labels), np.array(labels)] += 1
    return zeros

Sementara ini, untuk kebutuhan tutorial, kita hanya akan menggunakan data kalimat dan label dummy. Skenario-nya adalah kita akan mencoba membangun model untuk melakukan klasifikasi jenis orientasi sentiment sebuah kalimat seperti yang terlihat pada potongan kode berikut.


# list of numpy array (num_steps, input_size)
# panjang kalimat bisa berbeda-beda
sentences = [['saya', 'suka', 'buku', 'bagus', 'dan', 'menarik'],\
             ['laptop', 'suka', 'banget'],\
             ['film', 'menarik', 'banget'],\
             ['buku', 'buruk', 'dan', 'mengecewakan'],\
             ['laptop', 'mengecewakan'],\
             ['film', 'buruk']]

# ubah sentences ke list of lists of word index
v = Vocab()
v.create_vocab(sentences)
sentences = v.to_idx_list(sentences)

# [1, 0] -> positif
# [0, 1] -> negatif
labels = np.array([[1,0],[1,0],[1,0],[0,1],[0,1],[0,1]])

Potongan kode berikut adalah variable konfigurasi yang dapat kita ubah-ubah kedepannya.


# config parameters
input_size = 32
output_size = 32
num_classes = 2
state_size = 32
vocab_size = v.vocab_size

Kemudian, kita perlu mendefinisikan semua trainable variable/parameter untuk implementasi RNNs kita. Mohon perhatikan dengan baik scope variable dan dimensi/shape dari parameter berikut.


def add_variables():
    # definisi semua traninable parameters

    # digunakan di Logistic Regression layer
    with tf.variable_scope('logreg'):
        M = tf.get_variable('M', [output_size, num_classes])
        bm = tf.get_variable('bm', [num_classes], initializer=tf.constant_initializer(0.0))

    # digunakan di Embedding layer
    with tf.variable_scope('embedding'):
        # embedding matrix
        C = tf.get_variable("C", shape=[vocab_size, input_size])

    # digunakan di sebuah CELL RNNs
    with tf.variable_scope('basic_rnn_cell'):
        W = tf.get_variable('W', [input_size + state_size, state_size])
        bs = tf.get_variable('bs', [state_size], initializer=tf.constant_initializer(0.0))
        U = tf.get_variable('U', [state_size, output_size])
        bp = tf.get_variable('bp', [output_size], initializer=tf.constant_initializer(0.0))

Kemudian, kita implementasikan realisasi dari fungsi berikut,

X_t = C(w_t)
yang mengembalikan representasi vektor sebuah kata diberikan indeks-nya.


# sentence: list of word index, contoh: [3, 0, 4, 45, 3, ...]
def embed_lookup(sentence):
    with tf.variable_scope('embedding', reuse=True):
        # embedding matrix
        C = tf.get_variable("C", shape=[vocab_size, input_size])

        unstacked_embeds = [tf.gather(C, idx) for idx in sentence]
        return tf.stack(unstacked_embeds)

Potongan kode berikut adalah sebuah fungsi yang membungkus satu cell di RNNs, yaitu merupakan implementasi dari persamaan di dalam cell vanilla RNNs.

S_t = tanh(W [X_t; S_t-1] + b_s)
P_t = U S_t + b_p


def rnn_cell(rnn_input, prev_state):

    # reuse TRUE berarti kita kembali memanfaatk trainable variable yang sudah didefinisikan
    # sebelumnya.
    with tf.variable_scope('basic_rnn_cell', reuse=True):
        W = tf.get_variable('W', [input_size + state_size, state_size])
        bs = tf.get_variable('bs', [state_size], initializer=tf.constant_initializer(0.0))
        U = tf.get_variable('U', [state_size, output_size])
        bp = tf.get_variable('bp', [output_size], initializer=tf.constant_initializer(0.0))

        curr_state = tf.tanh(tf.matmul(tf.concat([rnn_input, prev_state], 1), W) + bs)
    
        # tensor 2D (1,output_size)
        rnn_out = tf.matmul(curr_state, U) + bp

        return curr_state, rnn_out

Definisi fungsi rnn_cell(.) akan digunakan dalam fungsi rnn(.) yang membungkus cell-cell dalam semua timestep RNN menjadi satu. Proses komputasi dilakukan dengan loop dimulai dari state awal, kemudian terus melakukan komputasi untuk mengalirkan state dari satu timestep ke timestep berikutnya.


def rnn(sentence):
    rnn_inputs = tf.unstack(sentence, axis=0)
    init_state = tf.zeros([1, state_size])

    ### RNN
    state = init_state
    rnn_outputs = []
    # untuk setiap timestep, lakukan komputasi dan pass state dari satu timestep
    # ke timstep berikutnya
    for rnn_input in rnn_inputs:
        #rnn_input awalnya adalah (input_size,), setelah di-expand (1, input_size)
        state, rnn_out = rnn_cell(tf.expand_dims(rnn_input, 0), state)
        rnn_outputs.append(rnn_out)

    #rnn_outputs adalah list of tensor 2D (1,output_size) [[...]]
    #perlu diubah kembali ke tensor 2D (num_steps,output_size) -> [[...],[...],...]

    # rnn_outputs: sekarang tensor 3D, dengan (num_steps,1,output_size)
    rnn_outputs = tf.stack(rnn_outputs, axis=0)

    #hilangkan dim 1, tensor 2D (num_steps,output_size)
    rnn_outputs = tf.squeeze(rnn_outputs, axis=1)
    return rnn_outputs

Pada bagian ujung, kita akan melakukan pooling terhadap semua output yang dihasilkan dari setiap timestep. Kali ini, kita coba implementasikan 2 cara melakukan pooling, yaitu dengan penjumlahan dan juga rataan.

\overrightarrow{P'_{sum}} = \sum_{t = 1}^{T} \vec{P_t}
\overrightarrow{P'_{avg}} = \frac{1}{T} \sum_{t = 1}^{T} \vec{P_t}


# rnn_outputs: tensor 2D (num_steps,output_size)
def avg_pool(rnn_outputs):
    # return: tensor 2D (1,y)
    return tf.reduce_mean(rnn_outputs, axis=0, keep_dims=True)

# rnn_outputs: tensor 2D (num_steps,output_size)
def sum_pool(rnn_outputs):
    # return: tensor 2D (1,y)
    return tf.reduce_sum(rnn_outputs, axis=0, keep_dims=True)

Sebagai classifier terakhir, kita akan menggunakan logistic regression sederhana:

Y = M P' + b_m


# logreg biasa: x adalah tensor 2D (batch, input_size)
# tanpa softmax terlebih dahulu
def logreg(x):
    with tf.variable_scope('logreg', reuse=True):
        M = tf.get_variable('M', [output_size, num_classes])
        bm = tf.get_variable('bm', [num_classes], initializer=tf.constant_initializer(0.0))

        return tf.matmul(x, M) + bm

Selanjutnya, kita perlu membangun wrapper yang menyatukan semua layer yang sudah didefinisikan sebelumnya.


# sentence di parameter adalah list of word index, contoh: [3, 0, 4, 45, 3, ...]
# membangun computational graph untuk satu kalimat, dari input hingga output
def inference(sentence):

    # sekarang, sentence adalah tensor 2D (num_steps, embed_size)
    sentence = embed_lookup(sentence)

    # layer RNN
    rnn_outputs = rnn(sentence)

    # Average Pooling
    #avg_outs = avg_pool(rnn_outputs)

    # Sum Poooling
    sum_outs = sum_pool(rnn_outputs)

    # Terakhir, Logreg Classifier
    logreg_outs = logreg(sum_outs)

    return logreg_outs

Loss function kita adalah cross entropy untuk output kategorikal. Tensorflow menyediakan proses komputasi cross entropy yang stabil yang dibungkus di dalam fungsi softmax_cross_entropy_with_logits. Inputnya adalah logits, yaitu nilai sebelum dikenakan fungsi aktivasi, dalam hal ini softmax.

H(y, y') = - \frac{1}{N} \sum_{i = 1}^{N} y log(y')


# loss function = softmax cross entropy
# logit, label: tensor 2D, (1,x)
def loss_function(logit, label):
    losses = tf.nn.softmax_cross_entropy_with_logits(labels=label, logits=logit)
    return tf.reduce_mean(losses)

Fungsi training berikut melakukan perhitungan gradient dan update step untuk setiap sample (sample demi sample, atau online learning). Inilah yang menyebabkan input dari RNNs dapat berupa kalimat-kalimat yang panjangnya dapat berbeda-beda.


def train(sentences, labels, num_epochs=30):

    just_start = True
    RESET_EVERY_N_TIMES = 20

    # untuk setiap EPOCH
    for _ in range(num_epochs):

        pair_s_l = list(zip(sentences, labels))
        len_training_data = len(pair_s_l)

        # terhadap setiap kalimat di training data
        i = 0
        while i < len_training_data:
            # dengan with, maka di akhir scope with akan dilakukan 2 hal:
            # 1. graph komputasi yang terbentuk akan di-reset (ini perlu 
            #    karena di implementasi kita, setiap kalimat, buat graph baru)
            # 2. resources session akan di-close, seperti panggil sess.close()
            with tf.Graph().as_default(), tf.Session() as sess:

                # set ulang trainable variables (karena graph kan sudah di-reset)
                add_variables()

                if just_start:
                    sess.run(tf.global_variables_initializer())
                    just_start = False
                else:
                    saver = tf.train.Saver()

                    # ingat bahwa saver harus dipanggil, dan isinya di-restore, 
                    # setelah VARIABLE ada/ditambahkan pada graph.
                    # kalau sebelumnya Graph sudah di-rest, maka sebelum panggil 
                    # restore, tambahkan kembali Variable ke dalam graph
                    saver.restore(sess, "rnn.model")

                j = 0
                while (j < RESET_EVERY_N_TIMES and i < len_training_data):

                    # bangun computational graph untuk sebuah kalimat
                    lbl = tf.placeholder(shape=[1, num_classes], dtype=tf.float32)

                    sentence, label = pair_s_l[i]

                    rnn_logits = inference(sentence)
                    rnn_loss = loss_function(rnn_logits, lbl)

                    optimizer = tf.train.GradientDescentOptimizer(0.05)
                    train_onestep = optimizer.minimize(rnn_loss)
				
                    loss, _ = sess.run([rnn_loss, train_onestep], \
                                       feed_dict={lbl:np.expand_dims(label, axis=0)})

                    print ('dataset {}, loss-value: {}'.format(i, loss))
                    j += 1
                    i += 1

                # save model untuk setiap RESET_EVERY_N_TIMES
                # save trainable parameters & session
                saver = tf.train.Saver()
                save_path = saver.save(sess, "rnn.model")


Kemudian, kita juga perlu membuat fungsi yang dapat melakukan komputasi berdasarkan parameter yang sudah dipelajari sebelumnya.


# predict a sentence
def predict(sentence):
    with tf.Graph().as_default(), tf.Session() as sess:

        # Restore trainable parameters
        add_variables()
        saver = tf.train.Saver()
        saver.restore(sess, "rnn.model")

        probs = tf.nn.softmax(inference(sentence))
        
        print (sess.run(probs))

Terakhir, kita jalankan training, lalu lihat hasilnya dalam mengklasifikasi beberapa kalimat, baik yang ada di training data dan juga kalimat yang baru.


# jalankan training!
train(sentences, labels)

# dari training data
predict(v.to_idx(['laptop', 'suka', 'banget']))
predict(v.to_idx(['film', 'buruk']))

# instance baru
predict(v.to_idx(['suka', 'laptop', 'dan', 'bagus']))
predict(v.to_idx(['film', 'mengecewakan']))