들어가며

이 포스팅에서 다루는 내용은 다음의 블로그 & 강의를 참조하였습니다.

colah’s blog - Understanding LSTM Networks

[NN의 꽃 RNN 이야기](https://www.youtube.com/watch? v=-SHPG_KMUkQ)

RNN Tutorial

RNN에 관한 내용은 모두의연구소 기술블로그 를 많이 참조하였습니다.

RNN

딥러닝은 인간이 생각하는 방법을 모방한 것입니다. 그런데 생각을 해 보면, 우리는 어떤 것을 판단할 떄 매 순간순간마다 생각하지 않습니다. 예를 들면, 이 블로그에 맨 처음 방문한 분은 블로그의 포스팅을 읽으실 건데요. 각각의 포스팅을 읽을 때마다 한 문장 문장을 각기 따로따로 이해하기 보다는, 문장의 맥락을 파악하면서 글을 이해하려고 할 것입니다.

일반적인 뉴럴 네트워크는 문장의 맥락을 판단하기에는 좋지 않은 접근 방법입니다. 입력값은 각각의 가중치를 변경한 이후에는 사라져 버릴 테니까요. 그러나 우리가 하고 싶은 것은 다른 종류의 문제풀이입니다. 예를 들면 ‘죽인다!’ 같은 문장을 입력받았을 때 이 문장이 정말로 살해욕구가 있는 것인지, 아니면 환상적인 무언가를 발견했을 때 내지르는 탄성인지는 앞뒤 맥락을 확인해야만 알 수 있을 것입니다.

위와 같은 문제를 해결할 때는 RNN을 사용합니다. RNN은 다음과 같이 값을 전달합니다. 많이 보셨을 이미지일 수도 있어요.

RNN

위의 그림은 RNN의 한 구조인데요. A는 x라는 input을 받아 h라는 output을 리턴합니다. 그러면 A는 자기 자신을 재귀적으로 바라보고 있는데요. A 안에서 무슨 일이 발생하는지 알아내야 할 거여요. 실제로 위의 그림을 풀어쓰면, 아래와 같이 나타낼 수 있습니다.

RNN_2

실제로 A의 내부는 저렇게 되어 있습니다. 실제로 A는 내부에 x를 받아서 h를 리턴하는 신경망을 이어놓은 모양을 하고 있습니다. A에서 생성된 값은 뒤의 신경망에게 전달됩니다. 뒤의 신경망은 또 뒤의 신경망에게 그 값을 전달하게 됩니다. 식으로 보면 다음과 같습니다.

ht=f(ht−1,xt)h*{ t }=f\left( { h }*{ t-1 },\quad { x }_{ t } \right)ht​=f(ht−1​,xt​)

즉, 이전의 상태값 ht−1{ h }{ t-1 }ht−1​ 과 현재 상태의 인풋값 xt{ x }{ t }xt​ 를 가지고 함수 f(x)에 대입하여 현재 상태를 구하는 것입니다. 그렇다면 여기서 함수 f(x)가 무엇인지 알아야 할 것입니다.

함수 f(x)

위에서 보았던 것 처럼, 새로운 상태를 구할 때는 과거의 상태와 현재의 입력값이 필요합니다. 식으로 풀어 쓰면 다음과 같은데요

ht=tanh(Whhht−1+Whxxt)h*{ t }=tanh\left( { W }*{ hh }{ h }{ t-1 }\quad +\quad { W }{ hx }{ x }_{ t } \right)ht​=tanh(Whh​ht−1​+Whx​xt​)

처음 보는 값인 Whh{ W }{ hh }Whh​ 와 Whx{ W }{ hx }Whx​ 가 식에 나타났습니다. 위의 두 값은 weight를 나타냅니다. tanh은 활성함수를 의미합니다. 혹여나 처음 보신다면 시그모이드와 비슷한 것이라고 생각하시면 됩니다.

RNN_2

이렇게 생겼습니다.

그러면 이제 다음 상태를 가져오는 방법을 알게 되었습니다. 다시금 위의 그림으로 돌아가 보면, 현재 상태에서 리턴값을 구해야 하는 경우가 있는데요. 만약 그래야 한다면 다음과 같은 식을 활용하여 리턴값을 구하면 됩니다.

yt=Whyht{ y }{ t }=W{ hy }{ h }_{ t }yt​=Why​ht​

위와 같은 식에 대입하면 리턴값을 구할 수 있습니다. 여기서 주의하실 점은, RNN은 위의 세 weight를 레이어마다 두는 것이 아니라, 이전 상태에서 사용하였던 세 웨이트값을 계속 업데이트 한다는 것입니다. 즉, Whh { W }{ hh }Whh​ 와 Whx{ W }{ hx }Whx​ 그리고 WhyW_{ hy }Why​ 를 계속 변경하면서 학습을 진행하게 됩니다.

코드

실제 코드는 tensorflow의 high level API를 활용하여 작성하겠습니다.

# -*- coding: utf-8 -*-
import tensorflow as tf
 
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("./mnist/data/", one_hot=True)
 
########
# 옵션
########
# mnist는 28 * 28 형태의 데이터이고 사람은 글씨를 위에서부터 아래로 쓰므로, 28개씩 28라인을 입력받게 한다.
learning_rate = 0.001
total_epoch = 30
batch_size = 128
 
n_input = 28
n_step = 28
n_hidden = 128
n_class = 10
 
X = tf.placeholder(tf.float32, [None, n_step, n_input])
Y = tf.placeholder(tf.float32, [None, n_class])
 
W = tf.Variable(tf.random_normal([n_hidden, n_class]))
b = tf.Variable(tf.random_normal([n_class]))
 
cell = tf.contrib.rnn.BasicRNNCell(n_hidden)
outputs, states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)
 
# outputs : [batch_size, n_step, n_hidden]
# -> [n_step, batch_size, n_hidden]
outputs = tf.transpose(outputs, [1, 0, 2])
outputs = outputs[-1]
 
model = tf.matmul(outputs, W) + b
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=model, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)

코드에서는 구현의 번거로움으로, tensorflow High level API를 사용하였습니다. 코드는 RNN을 보고 만들었습니다.

데이터는 mnist를 사용하였으며, mnist가 28 * 28 형태의 데이터이기 때문에, input_size를 28로 두고, 총 28회의 step을 거쳐서 데이터를 처리하게 만들었습니다. n_hidden은 앞에서 말씀드린 세 weight의 크기입니다.

또 살펴볼 점은 최종적으로 softmax를 적용한 후 옵티마이저를 거치는 부분인데요. tf.nn.dlylnamic_rnn함수는 셀을 통과한 [batch_size, n_step, n_hidden]의 값을 리턴하는데요. 결과값을 가져와서 softmax를 적용하기에는 차원이 다르기 때문에, transpose 이후에 n_step 값을 제거합니다.

RNN의 문제점

RNN은 현재 상태를 구하기 위하여, 활성화함수(weight _ 과거 상태값 + weight _ 입력값)를 적용합니다. 시계열 데이터의 경우에는 과거의 값이 현재 상태에 반영되게 하여야 하기 때문이죠. 그러나, 이와 같은 경우에는 depth가 깊어질수록 과거의 상태값이 더 이상 영향을 미치지 못하게 됩니다.

1보다 큰 값을 몇십 번 곱하면 나중에는 큰 값이 되는데요. 반대로 1보다 작은 값을 몇십 번 곱하면 값이 너무 작아지게 됩니다. 값이 너무 작아지기 때문에, 과거의 상태값이 소실되버리죠. 혹은, 값이 엄청나게 커져서 발산되어버리게 됩니다. 발산은 사실 크게 문제되지 않는데요. 상태의 최대값을 정해주고, 이 값을 넘으면 안된다고 지정할 수 있기 때문입니다. 문제는 소실되는 경우인데요. 시그모이드를 서너번 정도 곱하면 거의 모든 구간에서 값이 0에 가까워져 버립니다. 이렇게 되면 값이 전파되지 않게 되버리죠.

LSTM

이를 해결하기 위해 나온 것이 LSTM입니다. RNN의 변형인 LSTM은 오차의 그리디언트가 신경망의 뒤쪽 부분에서도 소실되지 않고 제대로 반영될 수 있게 해줍니다. LSTM은 여러 개의 게이트가 붙어있는 cell의 형태로 이루어져 있습니다. 즉, 위의 그림으로 다시 돌아가면 위의 그림의 A는 내부에서 간단한 곱연산을 통해 상태값을 도출해 내는데요. LSTM은 그 곱 연산 대신에 내부에 여러 개의 게이트를 두고, 이 게이트에서 다음과 같은 요소를 가중치를 통해 결정합니다.

  • 얼마나 과거의 상태값을 반영할 것인지
  • 얼마나 입력값을 반영할 것인지
  • 얼마나 입력값을 출력할지

이 세 가지 게이트를 input gate, forget gate, output gate라고 부릅니다. A 내부에서 이 세 가지 게이트를 통해 다음 A로 넘길 상태값을 정하는 것이지요.

RNN_2

이 이미지는 LSTM을 설명하는 이미지입니다. 위의 이미지에서 나타나는 표시는 다음과 같은 의미입니다.

  • 노란색 상자는 뉴럴 네트워크 레이어를 나타냅니다.
  • 각 화살표는 하나의 노드에서 다음 노드로 출력값을 전달합니다.

프로세스를 하나씩 확인하면 다음과 같습니다.

forget gate

FORGET_GATE

첫 번째 프로세스는 forget-gate 입니다. 이 게이트에서는 이전 상태에서 넘어온 값을 얼마나 잊어버리는지 결정합니다. ‘잊어버린다’ 라는게 이상하게 느껴질 수 있는데요. 어떤 경우에는 상태를 잊어버리는게 더 도움이 됩니다. 사람의 기억도 관련이 없는 컨텍스트는 잊어버리는게 효과적인 것처럼요.

이 게이트에서는 이전 셀에서 넘어온 값과 입력값, 편향을 보고 시그모이드를 취해서 값을 리턴합니다. 시그모이드이기 때문에 [0, 1] 사이의 값이 나오게 되는데요. 0은 완전히 잊어버림, 1은 모두 기억함이 됩니다.

input gate

INPUT_GATE

다음은 input-gate 입니다. 이 게이트는 두 가지 부분으로 나뉩니다.

  • 입력 게이트에서는 우리가 어떤 값을 갱신할지 결정합니다.
  • 다음 tanh 층에서는 셀에 더해질 수 있는 새로운 백터값을 결정합니다.

이 두 값을 곱한 후에, 처음 forget-gate 의 output과 더할 것입니다. 내부 로직에 곱하기 뿐만 아니라 더하기가 존재하기 때문에, 앞의 RNN에서 나온 이슈인 소실이 발생하지 않게 됩니다.

이후에는 이전 셀에서 넘어온 상태값에 forget-gate의 아웃풋을 곱하고, input-gate의 아웃풋을 더합니다. 이를 통해 우리가 결정한 새로운 정보값이 더해지게 됩니다.

COMPOSITION

output gate

OUTPUT_GATE

이제 output 값을 결정할 차례입니다. 이 층에서는 셀에서 어떤 부분을 출력할지 결정합니다. 그 다음에, 그 값을 [0, 1] 사이의 값으로 만들기 위해 tanh 출력으로 변환하고 그 값을 다시 시그모이드 출력값과 곱합니다.