reniew's blog
cs and deep learning

CS20(TensorFlow) Lecture Note (11): RNNs in the TensorFlow

|

스탠포드의 TensorFlow 강의인 cs20 강의의 lecture note를 정리한 글입니다. 강의는 오픈되지 않아서 Lecture note, slide 위주로 정리된 글임을 참고 해주시길 바랍니다. 강의의 자세한 Syllabus 및 자료들을 아래 링크를 참고해 주세요.

CS20: TensorFlow for Deep Learning Research


Post list

11. RNNs in the TensorFlow

이번 lecture에서 배울 내용은 다음과 같다.

  • From feed-forward to recurrent
  • Tricks & treats
  • Presidential tweets

From feed-forward to Recurrent Neural Networks(RNNs)

지난 몇년간 feed-forward network 부터 convolutional neural network는 좋은 결과를 보여줬고 많은 문제에 적용시켜서 엄청난 성과를 보여줬다.

그럼에도 불구하고 아직도 이러한 feed-forward network와 convolutional neural network 모델로는 적용시키기 어려운 문제들이 아직 많이 있었다. 이러한 한계의 가장 큰 이유는 모델에 적용시킬 수 있는 data가 singular한 형태만 가능하기 때문이다. 따라서 언어나 음악과 같은 sequence데이터를 적용시키기에는 많은 어려움이 있었다. 따라서 위의 문제에 이어서 이런 sequencial한 데이터를 다루는 모델에 대해 연구가 많이 이뤄졌다.

이러한 연구의 결과로 나온것이 RNN이다. RNN은 sequential한 정보를 잡아내기 위해 만들어 졌고 가장 기본적인 형태의 RNN인 Simple Recurrent Network(SRN)은 Jeff Elman에 의해 만들어졌다.

RNN은 feed-forward의 unit과 똑같은 연산을 하는 unit이 적용되었다. 하지만 이러한 unit들이 계속해서 연결되어 있다는 점이다. Feed-forward의 경우 input에 의한 신호는 계속해서 한방향으로 이어지고, loop은 만들어 지지 않는다. 그에 반해 RNN은 loop이 생기고 neuron들이 각자 스스로 연결된다. 즉 이전의 neuron이 또 옆의 neuron에 영향을 준다.

9999

가장 기본적인 형태의 RNN인 simple recurrent networks(SRN)은 Elman network와 Jordan network를 뜻한다.

  • Elman Network, Jordan Network
  • $x_t$ : input vector
  • $h_t$ : hidden layer vector
  • $y_t$ : output vector
  • $W,~U$ : and $b$ : parameter matrices and vector
  • $\sigma_h$ and $\sigma_y$ : Activation functions

992

대부분의 사람들은 RNN을 NLP의 한 분야라 생각한다. 그도 당연할 것이 언어가 매우 대표적인 sequential한 데이터이기 때문이다. 그러나 NLP분야 외에도 audio, image, video등 많은 분야에서도 RNN은 사용된다. 가령 MNIST의 경우에도 적용할 수 있다. 이 때는 각 image를 pixel들의 sequence로 적용시킨다.

Back-propagation through Time(BPTT)

Feed-forward와 Convolutional Network에서는 error를 back-propagation을 통해 loss 값이 모든 layer에 전달 되었다. 이런 방법을 통해 parameter들을 update시켰다.

RNN에서는 erros는 loss값이 모든 timestep에 전달된다. 앞선 내용과 두가지 큰 차이점이 있다.

  • feed-forward의 각 layer는 각자 자신의 parameter를 가지는 반면 RNN에서는 모든 timestep들이 parameter들을 공유한다. 따라서 모든 timestep의 gradient 값들을 모두 합쳐서 parameter를 update하는데 적용시켰다.
  • feed-forward의 경우 고정된 숫자의 layer를 가지는 반면 RNN은 임의의 timestep 수를 가진다.

아래의 차이점을 보자. 만약에 sequence가 매우 길어진다면, back-propagation은 모든 time-step에서 계산되는데 이 계산량이 매우 많아질 것이다. 또다른 본질적인 문제는 gradient 자체가 매우 커지거나 매우 작아져서 학습이 불가능한 경우가 생긴다.(vanishing/exploding gradients)

9282

모든 timestep에서 모든 parameter를 update해서 계산량이 매우 많아지는 상황을 피하기 위해 보통 update시키는 timestep의 수를 제한시키는 방법을 사용한다.(truncated BPTT)

TensorFlow에서 RNN은 unrolled된 버전의 network를 사용한다. 즉 정확히 몇 개의 timestep을 사용할지를 정해줘야 한다는 뜻이다. RNN의 특성을 생각해보면 이러한 구현 방법은 큰 제약이 된다. input의 경우 길이가 일정할 수도 있지만 정해지지 않을 수도 있기 떄문이다. 예를 들면 여러 text를 다루는데 하나의 text는 20개의 단어로 구성되지만 또 어떤 text는 200개의 단어로 구성될 수 있기 때문이다. 이러한 문제를 해결하기 위한 하나의 방법은 data를 나눠서 각각 다른 bucket으로 넣는 것이다. 이 bucket에는 비슷한 크기의 sequence가 들어간다. 만약 becket보다 길이가 짧다면 padding을 이용하면 된다.

Gated Recurrent unit(LSTM and GRU)

실제로 RNN을 사용해보니 기대와는 달리 Long-term에 대한 정보를 잘 못잡아내는 것이 밝혀졌다. 이런 결함을 해결하기 위해 나온 것이 LSTM이다. 이러한 LSTM의 개발은 사실 오래전에 vanishing gradient 문제를 해결하기 위해 만들어 졌던 것이다.

LSTM의 unit은 gating mechanism이라 불리는 것을 위해 사용된다. 총 4개의 gate가 사용되고 일반적으로 $i,o,f,\tilde{c}$로 작성하고 각각 input, output, forget, candidate/new memory gate라 부른다.

직관적인 각 gate에 대한 이해는 다음과 같다.

  • input gate: 현재 input이 얼마나 사용할지 결정한다.
  • forget gate: 이전 state의 정보를 얼마나 사용할지 결정한다.
  • output gate: hidden state 값이 다음 timestep에 얼마나 전달할지 결정한다.
  • candidate gate: 일반적인 RNN와 유사한 부분이다. 이전 hidden state 값과 현재 input값을 기반으로 candidate를 계산한다.
  • final memory cell: candidate hidden state들을 합쳐서 내부의 memory 값을 만든다.

Long Term에 대한 정보를 잡아내기 위한 모델에 LSTM 뿐만 아니라 GRU도 많이 사용된다. 조금 다른 구조이지만 거의 유사한 방식으로 동작한다.

119

Application

RNN모델을 활용한 application은 다음과 같다.

  • Language modeling
  • Machine Translation
  • Text Summarization
  • Image Captioning

RNN in TensorFlow

RNN은 기본적으로 하나하나의 cell들이 결합된 구조이다. TensorFlow에서는 여러가지 RNN 모델을 만들기 위해 다음과 같은 cell들을 지원한다.

  • BasicRNNCell: 가장 기본적인 RNN cell
  • RNNCell: RNN Cell을 위한 Abstract Object
  • BasicLSTMCell: 기본적인 LSTM recurrent network cell
  • LSTMCell: LSTM recurrent network cell
  • GRUCell: GRU cell

위의 cell들은 다음과 같이 구현한다.

cell = tf.nn.rnn_cell.GRUCell(hidden_size)

그리고 RNN의 모델을 생각해보자. cell들이 stacked된 구조이다. 따라서 여러개의 cell들을 쌓아야하는데 다음과 같이 구현하면 된다.

layers = [tf.nn.rnn_cell.GRUCell(size) for size in hidden_sizes]
cells = tf.nn.rnn_cell.MultiRNNCell(layers)

그리고 동적으로 graph를 만들기 위해 tf.nn.dynamic_rnn, tf.nn.bidirectional_dynamic_rnn를 사용한다. 설명은 다음과 같다.

  • tf.nn.dynamic_rnn: tf.While loop을 사용해서 동적으로 graph를 만든다. graph의 생성이 빠르고 batch를 가변 크기로 사용할 수 있다.(batch의 가변길이가 sequence의 가변길이를 뜻하진 않음)
  • tf.nn.bidirectional_dynamic_rnn: 위와 같은 방식이지만 양방향의 RNN을 만들 수 있다.

dynamic_rnn을 사용해서 다음과 같이 RNN들을 stack 할 수 있다.

layers = [tf.nn.rnn_cell.GRUCell(size) for size in hidden_sizes]
cells = tf.nn.rnn_cell.MultiRNNCell(layers)
output, out_state = tf.nn.dynamic_rnn(cell, seq, length, initial_state)

하지만 앞서 소개한 RNN의 제약을 생각해보자. sequence는 어느정도 비슷한 크기를 가져야 했었다. 따라서 일정 크기(max_length)를 정하고 그 크기보다 큰 경우 자르고 크기보다 작은 경우에는 zero-padding을 사용한다.

하지만 padding을 사용하는 것에도 새로운 문제가 생긴다. input 뿐만 아니라 label에도 padding을 해야하는데 이렇게 label에 padding을 하게되면 loss에 영향을 줘서 학습에 문제가 생길 수 있다. 이를 해결하기 위한 두 가지 접근법이 있다.

  • Approach 1
    • mask를 사용한다. 실제 label에는 True값을 주고 padding된 label에는 False를 준다.
    • model을 real/padded token 모두를 가지고 돌린다.
    • real 값들만을 가지고 loss를 계산한다.

이 방법을 사용하기 위한 구현은 다음과 같다.

full_loss = tf.nn.softmax_cross_entropy_with_logits(preds, labels)
loss = tf.reduce_mean(tf.boolean_mask(full_loss, mask))
  • Approach 2
    • model에게 실제 sequence 길이를 알려줘서 예측도 실제 길이만큼만 하도록 해서 label과 비교한다.

이 방법을 사용한 구현은 다음과 같다. (line 3)

cell = tf.nn.rnn_cell.GRUCell(hidden_size)
rnn_cells = tf.nn.rnn_cell.MultiRNNCell([cell] * num_layers)
tf.reduce_sum(tf.reduce_max(tf.sign(seq), 2), 1)
output, out_state = tf.nn.dynamic_rnn(cell, seq, length, initial_state)

Tips and Tricks for implementation

Vanishing Gradient

RNN의 중요한 문제점 중 하나인 Vanishing gradient를 막기위해서는 다음과 같은 방법을 사용할 수 있다.

  • 다른 종류의 Activation 함수 사용하기(ReLU계열)
    • tf.nn.relu
    • tf.nn.relu6
    • tf.nn.crelu
    • tf.nn.elu
  • 다른 종류의 Activation 함수 사용하기(기타)
    • tf.nn.softplus
    • tf.nn.softsign
    • tf.nn.bias_add
    • tf.sigmoid
    • tf.tanh

Exploding Gradient

그리고 또 하나의 문제점인 Exploding gradient를 방지하기 위해서 gradient 값을 일정 크기 이상 못올라가도록 제한시킨다.

# 모든 학습가능한 변수들에 대한 cost의 gradient를 구한다.
gradients = tf.gradients(cost, tf.trainable_variables())

# gradient를 일정 크기 이상 못올라가도록 할 clip을 정의한다.
clipped_gradients, _ = tf.clip_by_global_norm(gradients, max_grad_norm)


optimizer = tf.train.AdamOptimizer(learning_rate)
train_op = optimizer.apply_gradients(zip(gradients, trainables))

Anneal learning rate

학습률(learning rate)를 학습 과정에서 점차 감소키는 방법은 다음과 같다.

learning_rate = tf.train.exponential_decay(init_lr,
										   global_step,
										   decay_steps,
										   decay_rate,
										   staircase=True)
optimizer = tf.train.AdamOptimizer(learning_rate)

Overfitting

Dropout을 사용해서 overfitting을 방지하는데 dropout을 사용하는 방법은 tf.nn.dropout을 사용하는 방법과, DropoutWrapper를 사용하는 두 가지 방법이 있다.

  • tf.nn.dropout
hidden_layer = tf.nn.dropout(hidden_layer, keep_prob)
  • DropoutWrapper
cell = tf.nn.rnn_cell.GRUCell(hidden_size)
cell = tf.nn.rnn_cell.DropoutWrapper(cell,     
                                    output_keep_prob=keep_prob)

Language Modeling in TensorFLow

이번에는 TensorFlow를 통해 Language modeling을 구현해보도록 한다. 우선 어떤 language modeling을 할지 부터 정해야 하는데, 보통 흔히 사용되는 Neural Language Modeling은 다음과 같다.

  • Word-level: n-gram
    • 매우 전통적인 모델이다.
    • 특정 단어 이전의 n개의 단어를 통해 특정 단어를 예측하는 모델.
    • 단어를 미리 저장하는 vocabulary가 필요한데 이 크기가 매우 크다.
    • Out-of-vocabulary에 대한 대처가 필요하다.
    • 많은 메모리를 요구한다.
  • Character-level
    • input과 output모두 문자 하나하나로 구성된다.
    • vocabulary 크기가 매우 작다(영어의 경우 소문자 26개)
    • 단어 embedding 과정이 필요없다.
    • 학습이 빠르다.
    • 유연하지 않은 단점이 있다.
  • Subword-level:
    • input과 output이 subword이다.
    • W개의 가장 자주나오는 단어와, S개의 자주나오는 음절을 정한 후 기존 text를 변형 시킨다. (e.g new company dreamworks interactive -> new company dre+ am+ wo+ rks: in+ te+ ra+ cti+ ve:)
    • word-level과 char-level보다 좋은 성능을 보인다.

이번 구현에서는 Character-level의 모델을 만들어 보도록 한다. 데이터는 ‘Donald Trump’s tweets’데이터로 2018년 2월 15일까지의 donald trump의 트윗으로 구성되어 있다. 총 19,469개의 트윗이 있으며 각각 최대 140자이다. 그리고 트윗의 모든 링크들 즉, URL은 __HTTP__로 작성되어 있다. 데이터를 학습시킨 후 나온 결과는 다음과 같다.

wwwwwww

이제 구현을 해보자. 먼저 우리가 사용할 라이브러리들을 import먼저 한다.

import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import random
import sys
sys.path.append('..')
import time

import tensorflow as tf

import utils

우선은 char를 각각의 input으로 넣는다고 했었다. 그렇다고 input으로 바로 ‘c’를 넣는 것이 아니라 전체 character들을 vocabulary에 넣고 각 character를 index화 시켜서 input으로 넣는다. 즉 각 input의 character에 해당하는것을 vocabulary에서 찾고 index를 반환하는 encode함수와 출력시 다시 숫자를 character로 바꿔주는 decode함수부터 구현한다.

# encode 후 index를 0이 아닌 1부터 갖도록 만든다.
def vocab_encode(text, vocab):
    return [vocab.index(x) + 1 for x in text if x in vocab]

def vocab_decode(array, vocab):
    return ''.join([vocab[x - 1] for x in array])

이제 데이터를 불러오는 함수를 만들어야 한다. 데이터는 txt 파일로 되어있다. 각 데이터를 한 줄씩 읽어와서 위에 정의한 encode함수를 사용해서 vector화 시켜준다.

def read_data(filename, vocab, window, overlap):
    lines = [line.strip() for line in open(filename, 'r').readlines()]
    while True:
        random.shuffle(lines)

        for text in lines:
            text = vocab_encode(text, vocab)
            for start in range(0, len(text) - window, overlap):
                chunk = text[start: start + window]
                chunk += [0] * (window - len(chunk))
                yield chunk

그리고 불러온 데이터를 배치화 시켜주는 함수를 만든다.

def read_batch(stream, batch_size):
    batch = []
    for element in stream:
        batch.append(element)
        if len(batch) == batch_size:
            yield batch
            batch = []
    yield batch

이제 전체 모델을 Class 형태로 만들어 준다.

class CharRNN(object):
    def __init__(self, model):
        self.model = model
        self.path = 'data/' + model + '.txt'
        self.vocab = ("$%'()+,-./0123456789:;=?ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                    " '\"_abcdefghijklmnopqrstuvwxyz{|}@#➡📈")
        self.seq = tf.placeholder(tf.int32, [None, None])
        self.temp = tf.constant(1.5)
        self.hidden_sizes = [128, 256]
        self.batch_size = 64
        self.lr = 0.0003
        self.skip_step = 1
        self.num_steps = 50 # for RNN unrolled
        self.len_generated = 200
        self.gstep = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')

    def create_rnn(self, seq):
        layers = [tf.nn.rnn_cell.GRUCell(size) for size in self.hidden_sizes]
        cells = tf.nn.rnn_cell.MultiRNNCell(layers)
        batch = tf.shape(seq)[0]
        zero_states = cells.zero_state(batch, dtype=tf.float32)
        self.in_state = tuple([tf.placeholder_with_default(state, [None, state.shape[1]])
                                for state in zero_states])
        # this line to calculate the real length of seq
        # all seq are padded to be of the same length, which is num_steps
        length = tf.reduce_sum(tf.reduce_max(tf.sign(seq), 2), 1)
        self.output, self.out_state = tf.nn.dynamic_rnn(cells, seq, length, self.in_state)

    def create_model(self):
        seq = tf.one_hot(self.seq, len(self.vocab))
        self.create_rnn(seq)
        self.logits = tf.layers.dense(self.output, len(self.vocab), None)
        loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits[:, :-1],
                                                        labels=seq[:, 1:])
        self.loss = tf.reduce_sum(loss)
        # sample the next character from Maxwell-Boltzmann Distribution
        # with temperature temp. It works equally well without tf.exp
        self.sample = tf.multinomial(tf.exp(self.logits[:, -1] / self.temp), 1)[:, 0]
        self.opt = tf.train.AdamOptimizer(self.lr).minimize(self.loss, global_step=self.gstep)

    def train(self):
        saver = tf.train.Saver()
        start = time.time()
        min_loss = None
        with tf.Session() as sess:
            writer = tf.summary.FileWriter('graphs/gist', sess.graph)
            sess.run(tf.global_variables_initializer())

            ckpt = tf.train.get_checkpoint_state(os.path.dirname('checkpoints/' + self.model + '/checkpoint'))
            if ckpt and ckpt.model_checkpoint_path:
                saver.restore(sess, ckpt.model_checkpoint_path)

            iteration = self.gstep.eval()
            stream = read_data(self.path, self.vocab, self.num_steps, overlap=self.num_steps//2)
            data = read_batch(stream, self.batch_size)
            while True:
                batch = next(data)

            # for batch in read_batch(read_data(DATA_PATH, vocab)):
                batch_loss, _ = sess.run([self.loss, self.opt], {self.seq: batch})
                if (iteration + 1) % self.skip_step == 0:
                    print('Iter {}. \n    Loss {}. Time {}'.format(iteration, batch_loss, time.time() - start))
                    self.online_infer(sess)
                    start = time.time()
                    checkpoint_name = 'checkpoints/' + self.model + '/char-rnn'
                    if min_loss is None:
                        saver.save(sess, checkpoint_name, iteration)
                    elif batch_loss < min_loss:
                        saver.save(sess, checkpoint_name, iteration)
                        min_loss = batch_loss
                iteration += 1

    def online_infer(self, sess):
        """ Generate sequence one character at a time, based on the previous character
        """
        for seed in ['Hillary', 'I', 'R', 'T', '@', 'N', 'M', '.', 'G', 'A', 'W']:
            sentence = seed
            state = None
            for _ in range(self.len_generated):
                batch = [vocab_encode(sentence[-1], self.vocab)]
                feed = {self.seq: batch}
                if state is not None: # for the first decoder step, the state is None
                    for i in range(len(state)):
                        feed.update({self.in_state[i]: state[i]})
                index, state = sess.run([self.sample, self.out_state], feed)
                sentence += vocab_decode(index, self.vocab)
            print('\t' + sentence)

마지막으로 구현한 모델들을 실행시키는 main함수를 넣으면 끝난다.

def main():
    model = 'trump_tweets'
    utils.safe_mkdir('checkpoints')
    utils.safe_mkdir('checkpoints/' + model)

    lm = CharRNN(model)
    lm.create_model()
    lm.train()

if __name__ == '__main__':
    main()

RNN을 통해 character단위 뿐만 아니라 word level등 다양한 모델을 만들 수 있으므로 이번 기회에 자세히 알아보도록 하자.

Comments