seq2seq 톺아보기 (1)

5 minute read

톺아보기 시리즈 (톺아보다: 샅샅이 틈이 있는 곳마다 모조리 더듬어 뒤지면서 살피다)

이 포스팅은 Sequence to Sequence Model(seq2seq), RNN, LSTM, GRU, teacher forcing 등과 관련한 내용을 다루는 seq2seq 톺아보기 시리즈의 1편입니다.

Introduction: Why seq2seq?

기존의 DNN(deep neural network)은 고정된 길이의 input과 output에 대해서만 적용이 가능하다는 문제를 가지고 있다. 따라서, 미리 output 데이터의 길이를 알지 못하는 경우(기계번역, 질의응답 알고리즘)에는 DNN을 사용할 수 없다.

길이가 제한되지 않는 유연한 sequence 형태의 데이터를 다루기 위한 방법론으로 sequence to sequence mapping 방법론이 제시되었습니다. 이는 흔히 seq2seq 모형으로 불리며 이를 구성하는 방법과 모형의 형태는 매우 다양하고, 문제의 영역에 크게 구애받지 않는다는(domain-independent method) 장점이 있습니다. 현재 seq2seq 모형은 sequence 형식의 데이터를 다루는 가장 대표적인 모형이 되었고, 특히 본 포스팅 시리즈는 seq2seq 모형의 기계번역 분야 활용 초점을 맞춰서 진행을 하려고 합니다.

RNN

0. 배경

RNN이 등장한 배경은 사람이 생각하는 방식을 따라하기 위해 만들었다고 생각하면 이해하기 쉽다. 사람은 모든 것을 처음부터 새로 생각하지 않는다. 우리는 이전에 받아들인 정보를 버리거나 하지 않고 이를 바탕으로 새로운 것을 생각하게 된다. 즉, 사람의 사고는 지속성이 있다. 하지만, 기존의 신경망 모형은 이러한 문제를 해결하기가 어려웠다.

RNN은 이러한 문제를 loop를 가지는 신경망 모형을 사용하여 해결하였고, 이러한 loop는 정보가 신경망 연산을 진행하는 동안에 유지될 수 있도록 해준다. 어떠한 방식으로 작동하는지 자세히 살펴보자.

이 포스팅에서 자주 사용되는 notation은 다음과 같습니다.

\[\mathbf{x}: \text{input sequence data} \\ \mathbf{h}: \text{hidden state} \\ \mathbf{y}: \text{output sequence data} \\ t: \text{timestep}\]

(이 포스팅에서는 bias에 대한 부분은 포함하지 않습니다.)

1. 구조

위 사진은 RNN 모형의 일부분을 나타내는 것이다. 이때 cell은 메모리 셀(memory cell)으로도 불리며 RNN layer를 구성하는 요소로써 DNN hidden layer의 node와 유사하다고 생각할 수 있습니다. input으로 $\mathbf{x}$의 $t$번째 timestep에 해당하는 $\mathbf{x}_t$를 받아 $\mathbf{h}_t$를 return한다. 하지만 잘 보면 화살표가 다시 cell로 들어가는 것을 볼 수 있습니다. 이는 앞에서 언급한 loop에 해당하는 부분으로 cell의 output인 $\mathbf{h}_t$가 다시 input으로써 cell에 입력되고, 이러한 점 때문에 재귀적(recurrent)이라는 이름이 붙게 되었습니다(이러한 구조의 RNN을 Vanilla RNN이라고도 하는데, 이 포스팅에서는 RNN이라는 명칭으로 통일하겠습니다).

주의할 점은 RNN cell은 hidden state $\mathbf{h}$를 output으로 return하는 것이지, output sequence vector $\mathbf{y}$를 return하지 않는다는 것이다. output sequence vector $\mathbf{y}$는 별도의 출력층(output layer)를 이용해서 return해야합니다.

따라서 RNN의 loop을 풀어서 표현한다면 다음과 같이 나타낼 수 있습니다.

즉, 동일한 cell의 연속(복사본)으로 RNN layer는 구성되어 있습니다. 위의 chain과 같은 신경망 모형의 구성은 자연스럽게 sequence나 list를 다루는데 적절하도록 만들어준다.

2. 수식

아래 그림은 $t$ 시점의 하나의 cell의 구성을 나타낸다.

$\mathbf{x}_t$

  • $t$ timestep’s input of sequence
  • shape = ($d_{\mathbf{x}}$, 1)
  • 일반적으로 $\mathbf{x}_t$는 기계번역 분야에서 하나의 단어(word, token, subword)으로 간주
  • 따라서 $d_{\mathbf{x}}$는 input_dim이라고하고 기계번역에서는 embedding 차원의 크기(embedding_size)인 경우가 많다.

$\mathbf{h}_{t}$

  • hidden state corresponding to $t$ timestep input
  • shape = ($d_{\mathbf{h}}$, 1)
  • hidden_size 차원을 가짐
  • hidden_size는 하나의 RNN cell이 만드는 hidden state vector의 차원

$\mathbf{y}_{t}$

  • $\mathbf{h}_{t}$을 이용해 계산된 출력 벡터(output)

$\mathbf{W}_\mathbf{x}$

  • input sequence에 대한 가중치 행렬 parameter
  • shape = ($d_{\mathbf{h}}$, $d_{\mathbf{x}}$)

$\mathbf{W}_\mathbf{h}$

  • hidden state에 대한 가중치 행렬 parameter
  • shape = ($d_{\mathbf{h}}$, $d_{\mathbf{h}}$)

$\mathbf{W}_\mathbf{y}$

  • output에 대한 가중치 행렬 parameter

그리고 하나의 RNN layer는 가중치 행렬 parameter들을 모든 시점(timestep)에 대해서 동일하게 사용합니다.

RNN cell에서 이루어지는 행렬의 연산은 다음과 같이 표현된다.

이때, $f$는 출력층(output layer)에서 사용되는 임의의 비선형 함수이다.

3. 문제점

RNN의 loop(chain)과 같은 형태는 이전의 정보를 현재의 문제와 연결해준다는 아주 매력적인 점을 가지고 있습니다. 따라서 현재의 과제를 처리하기 위해 최근의 정보만이 필요한 경우, RNN은 매우 유용하다. 예를 들면, “점심을 굶어서 ___“라는 문장이 주어졌을 때, 꽤 명백하게 빈칸에는 “배고파”라는 단어가 들어가면 적절할 것으로 예상됩니다.

하지만, 최근의 정보가 아닌 다른 문맥이 필요한 경우가 있다. 예를 들면, “나는 통계를 전공하고 현재 4학년 학부생… 그래서 나는 ___ 과목에 제일 자신있어.”라는 문장이 주어진 경우에는, 예측하려는 단어(“통계”)와 관련된 문맥인 “나는 통계를 전공하고”는 그 거리(gap)이 매우 크다. 이렇게 기계번역에서 예측하려는 단어와 그 단어와 관련한 맥락 단어들의 거리가 먼 경우를 “long term dependencies“라고 말한다. 이론적으로는 RNN 모형이 long term dependencies를 해결할 수 있다고 하지만, 실제로는 RNN 모형을 이용해서는 이러한 문제를 해결하기가 매우 어렵다. 따라서 timestep이 적은, 즉 비교적 짧은 sequence(문장)에 대해서 RNN은 성능을 발휘하는 것으로 알려져 있습니다.

4. with Keras

Keras를 이용해 RNN layer를 구현해보자.

import tensorflow as tf
from tensorflow.keras import layers, models

# batch_size가 1인 경우
model = models.Sequential()
model.add(layers.SimpleRNN(units=32,
                           input_shape=(16, 64),
                           activation='tanh',
                           return_sequences=False))
model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_2 (SimpleRNN)     (None, 32)                3104      
=================================================================
Total params: 3,104
Trainable params: 3,104
Non-trainable params: 0
_________________________________________________________________

Keras에서 RNN layer를 구현하기 위해서는 layers.SimpleRNN을 사용하면 됩니다. 주의할 점은 이 코드로 구현한 모형은 1개의 RNN layer만을 나타내는 것이고 output layer에 대한 부분은 담겨있지 않습니다. 이 class의 arguments들의 의미를 하나씩 살펴보자. 자주 쓰이는 용어는 함께 표시해 놓았다.

우선 units(or hidden_size)=32는 hidden state의 차원을 결정하는 argument로 $d_{\mathbf{h}}$를 설정한다. 따라서 $d_{\mathbf{h}} = 32$이다.

input_shape=(16, 64)은 input sequence의 차원을 결정하는 argument이다. 이는 다르게 표현하면 input_shape=(timesteps, input_dim)이다. timesteps(or input_length)=16는 기계번역의 경우에 input sequence의 길이, 즉 문장의 길이가 16임을 나타냅니다. 따라서 input_dim(or embedding_size)=64=$d_{\mathbf{x}}$라고 할 수 있습니다.

Output Shape(None, 32)인 것은 batch_size에 대해 별도로 지정되어 있지 않기 때문이다.

다음과 같이 batch_size를 지정할 수 있다.

model = models.Sequential()
model.add(layers.SimpleRNN(units=32,
                           batch_input_shape=(8, 16, 64),
                           activation='tanh',
                           return_sequences=False))
model.summary()
Model: "sequential_7"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_7 (SimpleRNN)     (8, 32)                   3104      
=================================================================
Total params: 3,104
Trainable params: 3,104
Non-trainable params: 0
_________________________________________________________________

batch_input_shape=(8, 16, 64)을 이용해 batch_size를 8로 지정하면 Output Shape(8, 32)으로 지정되어 나옴을 알 수 있다.

그렇다면 return_sequences의 역할을 알아보자. return_sequences는 각 RNN cell에서 계산되는 hidden state에 대한 return 여부를 나타낸다. 즉, 앞의 예제들은 return_sequences=False이므로 output으로 계산된 결과는 마지막 timestep에 해당하는 RNN cell이 return하는 hidden state에 해당합니다. return_sequences=True로 하게되면 모든 RNN cell에서 hidden state을 return하게 됩니다.

model = models.Sequential()
model.add(layers.SimpleRNN(units=32,
                           input_shape=(16, 64),
                           activation='tanh',
                           return_sequences=True))
model.summary()
Model: "sequential_8"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_8 (SimpleRNN)     (None, 16, 32)            3104      
=================================================================
Total params: 3,104
Trainable params: 3,104
Non-trainable params: 0
_________________________________________________________________

batch_size에 대한 별도의 지정이 없다면 None이 됩니다. 그리고 input sequence의 길이인 16개의 hidden state이 return되므로 (16, 32)의 shape을 가지는 행렬이 return된다.

model = models.Sequential()
model.add(layers.SimpleRNN(units=32,
                           batch_input_shape=(8, 16, 64),
                           activation='tanh',
                           return_sequences=True))
model.summary()
Model: "sequential_9"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_9 (SimpleRNN)     (8, 16, 32)               3104      
=================================================================
Total params: 3,104
Trainable params: 3,104
Non-trainable params: 0
_________________________________________________________________

batch_input_shape=(8, 16, 64)을 이용해 batch_size를 정확히 지정하게 되면 8개의 batch 원소 각각에 대한 (16, 32)의 shape을 가지는 행렬이 return된다.

위의 내용을 그림으로 표현하면 다음과 같다.

참고자료

Comments