본문 바로가기
Deep Learning/Hands On Machine Learning

15.4 긴 시퀀스 다루기

by 대소기 2021. 11. 21.

*RNN의 문제점은 긴 sequence를 처리할 때 잘 작동하지 않는다는 점이다. 이유는 아래 두 가지가 있다.

1) gradient vanishing problem, gradient exploding problem

2) 입력의 첫 부분에 대한 정보를 기억하지 못하는 문제

15.4.1 불안정한 그레디언트 문제와 싸우기

* RNN에서도 역시 가중치 초기화, optimizer, dropout 등을 사용하여 불안정한 gradient를 조절할 수 있다.

* 하지만, ReLU와 같이 수렴하지 않는 활성화 함수의 경우 큰 도움이 되지 않는다. RNN의 경우 DNN과 달리 한 층에서 뉴런은 여러 time step에 걸쳐 동일한 가중치를 사용하게 된다. 만약, gradient descent를 통해 첫 번째 time step에서 출력을 조금 증가시키는 방향으로 가중치를 update한다고 가정하면, 두 번째 time step에서는 가중치가 더욱 증가될 것이고, 결국가중치가 점점 커져서 폭주하게 된다. 그렇기 때문에 hyperbolic tangent와 같이 수렴하는 활성화 함수를 기본적으로 사용하게 된다. 그레디언트의 폭주는 모니터링을 하다가 그레디언트 클리핑을 사용하면 조정 가능하다.

* 배치 정규화 : timestep 사이에 사용 불가능. 순환 층 사이에서만 사용 가능.

층 정규화(layer normalization)

* RNN의 경우 batch normalization보다 layer normalization이 더 잘 작동한다.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.layers import LayerNormalization
import numpy as np

class LNSimpleRNNCell(keras.layers.Layer):
  def __init__(self, units, activation='tanh', **kwargs):
    super.__init__(**kwargs) 
    self.state_size=units
    self.output_size=units
    self.simple_rnn_cell=keras.layers.SimpleRNNCell(units, acitvation=None)

    self.layer_norm=Layernomalization()
    self.activation=keras.activations.get(activation) #activation function

  def get_initial_state(self, inputs=None, batch_size=None, dtype=None):
    if inputs is not None:
      batch_size=tf.shape(inputs)[0]
      dtype=inputs.dtype

    return [tf.zeros([batch_size, self.state_size], dtype=dtype)]

  def call(self, inputs, states): #states는 이전 time step의 상태
    outputs, new_states = self.simple_rnn_cell(inputs, states)
    norm_outputs=self.activation(self.layer_norm(outputs))
    return norm_outputs, [norm_outputs]

* initializer는 rnn 층을 activation function을 지정하지 않은 채 생성하고, 이후 layer normalization층을 생성한다. 이는 선형 연산 후, 활성화 함수에 값이 들어가기 전 layer normaization을 시행하기 위해서이다. 그래서 layer normalization 층 이후에 acitvation function을 설정하게 된다.

* call method는 이전 time step의 hidden state와 이번 time step의 input을 받아서 rnn cell을 적용해 선형 조합을 계산하게 된다. 이후 layer normalization을 수행하고, activation function을 거쳐서 output을 도출해낸다. retval이 2개인 것은 하나는 출력이 되고, 하나는 hidden state로서 다음 time step에 전달되어야 하기 때문이다.

* 이렇게 정의한 사용자 정의 cell을 아래와 같이 사용할 수 있다.

model = keras.models.Sequential([
    keras.layers.RNN(LNSimpleRNNCell(20), return_sequences=True,
                     input_shape=[None, 1]),
    keras.layers.RNN(LNSimpleRNNCell(20), return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

RNN에서의 Dropout

* Dropout을 사용하는 RNN 모델을 만들기 위해 사용자 정의 cell을 생성할 수도 있지만, 더 간단한 방법은 keras를 사용하는 것이다.

* keras.layers.RNN을 제외한 모든 순환 층과 케라스에서 제공하는 모든 cell은 dropout 매개변수와 recurrent_dropout 매개변수를 설정할 수 있다. dropout은 time step마다 입력에 대한 dropout비율을 설정하는 매개변수이고, recurrent_dropout은 timestep마다 은닉 상태에 대한 dropout비율을 정의하게 된다.

14.4.2 단기 기억 문제 해결하기

* 간단한 RNN모델은 장기기억 기능이 없기 때문에 입력 sequence가 길어질수록 앞 쪽의 내용을 기억 못하는 문제가 발생하게 된다. 이를 해결하고자 만들어진 장기 메모리를 가진 셀이 LSTM 셀이다.

LSTM cell(Long Short Term memory cell)

* $h_t$ 는 short-term state 이고 $c_t$는 long-term state이다.

* $c_t$는 삭제게이트를 지나가면서 일부 기억을 잃게 된다. 그리고 입력 게이트를 통한 정보를 새로 더하게 된다. 그리고 추가적인 처리 없이 출력(long-term state 출력)으로 보내지게 된다. 한 편 똑같은 결과 값이 복사되어 tanh함수로 전달된다. 이는 출력 게이트에 의해 걸러져서 shor-term state를 만들게 된다(이 short term state는 해당 time step의 출력 $y_t$와 동일하다).

* $x_t$와 $h_{t-1}$은 네 개의 완결연결층에 연결된다.

* $g_t$ : 주 층이 된다. $x_t$와 $h_{t-1}$을 분석하는 일반적인 역할을 담당한다. 분석한 결과 장기 상태에서 가장 중요한 부분이 저장되고 나머지는 버려진다.

* 게이트 제어기($f_t, i_t, o_t$) : 게이트를 개방할지 개방하지 않을지를 결정한다. logistic 함수를 사용하기 때문에 출력이 0~1사이의 값이다.

* 삭제 게이트 : $f_t$가 제어하게 된다. 장기 상태의 어느 부분이 삭제될지를 제어한다.

* 입력 게이트 : $i_t$가 제어하게 된다. $g_t$의 어느 부분이 장기 상태에 더해져야 할지를 제어한다.

* 출력 게이트 : $o_t$가 제어하게 된다. 장기 상태의 어느 부분을 읽어서 이 타임 스텝의 $h_t$와 $y_t$로 출력해야 하는지 제어한다.

* 각 게이트에서의 계산은 아래와 같은 식에 의해 이뤄진다.

 

 

 

 

LSTM 구현

*lstm을 keras로 구현하기 위해서는 단순히 SimpleRNN 층 대신 LSTM 층을 사용하면 된다.

model = keras.models.Sequential([
    keras.layers.LSTM(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.LSTM(20, return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

model.compile(loss="mse", optimizer="adam", metrics=[last_time_step_mse])

* 또는 범용 목적의 SimpleRNN층의 매개변수로서 LSTM cell을 지정할 수 있다.

model = keras.models.Sequential([
    keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True, input_shape=[None, 1]),
    keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

model.compile(loss="mse", optimizer="adam", metrics=[last_time_step_mse])

* 하지만 LSTM층이 GPU 구현시 최적화된 구현을 사용하기 때문에 일반적으로 RNN층에서 매개변수로 LSTM을 설정하는 것 보다 선호된다.

 

핍홀 연결(Peephole connection)

* 일반 lstm의 게이트 제어기는 이전 time step의 hidden state인 $h_{t-1}$, 이번 time step의 입력인 $x_t$를 통한 연산만 수행하게 된다. 

* 이를 개선시켜 장기 기억 상태 $c_t$가 삭제 게이트와 입력 게이트의 제어기 $f_t$와 $i_t$에 입력으로 추가된다. 현재의 장기 기억 상태 $c_{t-1}$는 출력 게이트의 제어기 $o_t$에 입력으로 추가된다.

* 이를 구현하기 위해서는 tf.keras.experimental.PeepholeLSTMCell 층을 사용할 수 있다. keras.layers.RNN층의 생성자에 PeepholeLSTMCell을 전달하여 생성할 수 있게 된다. 

 

GRU cell(Gate Recurrent Unit)

* GRU 셀은 LSTM의 간소화된 버전이다.

* 두 상태벡터 $h_{t-1}, c_t$는 $h_{t-1}$으로 간소화 되었다.

* 하나의 게이트 제어기 $z_t$가 삭제 게이트와 입력 게이트를 모두 제어한다. 게이트 제어기가 1을 출력하면 삭제 게이트가 열리고 입력 게이트가 닫힌다.  0의 경우 반대로 작동하게 된다.

* 이전 상태의 어느 부분이 주 층($g_t$)에 노출될지 제어하는 새로운 게이트 제어기 $r_t$가 있다.

* 계산은 아래 수식을 통해 이뤄지게 된다.

GRU 구현

model = keras.models.Sequential([
    keras.layers.GRU(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.GRU(20, return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

model.compile(loss="mse", optimizer="adam", metrics=[last_time_step_mse])

 

* LSTM과 GRU 덕분에 장기기억과 관련된 문제는 해결 됐지만, 단기 기억은 여전히 제한적이다. 이를 해결하기 위해서는 1D 합성곱 층(Convolution layer)를 사용하는 방법이 있다.

 

1D 합성곱 층을 사용해 시퀀스 처리하기

* 1D 합성곱 층을 사용하면 입력 sequence의 길이를 짧게 변환하여 단기 기억에 대한 문제를 해결할 수 있다.

* 자연어 처리의 예를 들어 보면 문장이 입력될 때 문장은 embedding vector로 변환된다. 문장이 토큰화, 패딩, 임베딩 층을 거친다면, 다음과 같은 문장 형태의 행렬로 변환된다.

https://wikidocs.net/80437

* n은 문장의 길이이고, k는 embedding vector의 차원이다.

https://wikidocs.net/80437

* 1D합성곱 층의 kernel의 너비는 embedding vector의 차원과 같다. 때문에 높이만 지정해준다. 만약 높이가 2라면 위와 같이 kernel을 통해 두 단어를 하나의 특성 맵 요소로 변환할 수 있게 된다.

* 이러한 과정을 통해 입력 sequence를 짧은 길이로 down sampling 하는 것이 가능해진다.

 

 

model = keras.models.Sequential([
    keras.layers.Conv1D(filters=20, kernel_size=4, strides=2, padding="valid",
                        input_shape=[None, 1]),
    keras.layers.GRU(20, return_sequences=True),
    keras.layers.GRU(20, return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

model.compile(loss="mse", optimizer="adam", metrics=[last_time_step_mse])

 

WAVENT

 

* 이 네트워크에서는 층마다 팽창 비율(각 뉴런의 입력이 떨어져 있는 간격)을 두 배로 늘리는 1D 합성곱 층을 쌓는다.