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.
dengan,
- W^{(xh)}, W^{(hh)}, W^{(hy)} adalah parameter pada setiap sisi RNNs
- S_t adalah state pada timestep t
- X_t adalah input pada timestep t
- P_t adalah output pada timestep t
Kali ini, untuk kemudahan implementasi, kita akan melakukan sedikit trik terhadap persamaan di atas. Dapat dibuktikan bahwa:
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:
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,
dengan,
- C: R \rightarrow R^N adalah fungsi yang menerima indeks kata (integer) dalam vocabulary, dan mengembalikan representasi vektor dari kata tersebut
- w_t adalah indeks kata tertentu (integer) dalam vocabulary
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,
# 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.
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.
# 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:
# 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.
# 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']))