RNN

park paul·2021년 8월 14일
0
post-thumbnail
post-custom-banner

Intro

RNN: 기억을 갖는 신경망 모델

(Recurrent Neural Network) '몇 번이나 반복해서 일어나는 일' 즉, 순환하는 신경망이라는 뜻이다.

forward NN이나 CNN과 달리 RNN는 데이터의 순차구조로 인식하기 위해 데이터를 시간 순서대로 하나씩 입력 받는다.
시간적 공간적 순서 관계가 있는 데이터를 '순차 데이터'라고 부르는데 시공간의 순서에 관한 context를 가지고 있다.
forward NN이나 CNN과 달리 RNN는 데이터의 순차구조로 인식하기 위해 데이터를 시간 순서대로 하나씩 입력 받는다.

일반적으로 RNN은 은닉 계층으로 이루어지며 은닉 계층은 여러 계층이 될 수 있으나 일반적으로 은닉 계층을 깊게 쌓아도 성능이 크게 향상 하지 않아 한 두 계층만 쌓는다.

  • 기억을 전달하는 순환 신경망
    인공 신경망이 데이터의 순서를 고려하는 컨텍스트를 만들려면 데이터의 순차 구조를 인식할 수 있어야 하고, 컨텍스트 범위가 넓더라도 처리할 수 있어야 한다.
    FeedForward Neural Network는 단순하고 이해하기 쉽지만, 시계열 데이터의 성질을 잘 파악 못한다.
    그래서 RNN이 등장함. 또한 언어적 능력에 탁월함.

이전의 어떤 정보가 추가적으로 왔다라는 의미이다.
이전 메모리+ 현재 입력을 함께 고려하는 구조
즉, 한참 전의 메모리도 같이 고려해 출력하게 됨.

이렇게 RNN의 체인처럼 이어지는 성질은 바로 sequence나 list로 이어지는 것을 알려준다.
이런 데이터를 다루기에 최적화된 구조의 neural network인 것이다.
ref)https://dgkim5360.tistory.com/entry/understanding-long-short-term-memory-lstm-kr


은닉 계층은 '이전 상태, 새로운 입력'을 받아서 현재 상태를 매핑한다.

이 식을 전개해 보면.

RNN의 주요 모델

  • 입력은 시퀀스인데 출력은 시퀀스가 아니다 -> many to one

  • 입,출력이 시퀀스다 -> many to many

  • 입력은 시퀀스가 아니지만 출력은 시퀀스다 -> one to many

    many to one

many to many

one to many

양방향 모델


입력을 양쪽으로 살펴보는 방식.
상대적인 순서를 따지기 때문에 양방향으로 살펴보고 판단.
예를 들어, 기계 번역을 할 떄 문장을 양방향으로 보는 것이 효과가 좋다.
방식)
입력 데이터를 순방향 계층과 역방향 계층에 모두 입력한다.
순방향 계층과 역방향 계층의 출력을 출력 계층에 입력되며,
출력 계층에서는 두 결과를 합쳐서 예측한다.

RNN의 문제점

기존 바닐라 RNN같은 경우 시계열 데이터의 장기 의존 관계를 학습하기 어려운데, BPTT(BackPropagation Through Time)에서 'Gradient Vanishing&Exploding이 문제가 된다.

  • back propagation through time (BPTT)

    전체 손실 함수는 각 단계의 손실함수를 더한 값
  • 마지막 단계 역전파

  • 세번째 단계 역전파

  • 단계별 역전파

    은닉 계층부터는 순차적으로 실행된다.

이를 RNNLM를 예를 든다면,

Tom was watching TV in his room. Mary came into the room. Mary said hi to ( ? )

빈칸에 들어갈 단어는 'TOM'이 된다. 이 답의 정보는 RNN 계층의 은닉 상태에 인코딩해 보관해두기때문에 가능하다.
그럼 정답 레이블이 'TOM'이 주어졌을 때, BPTT로 수행함에 따라 이 시점으로부터 과거 방향으로 기울기를 전달하게 된다.

  • 정답 레이블이 "Tom"임을 학습할 때 중요한 것이 바로 RNN 계층의 존재
  • RNN 계층이 과거 방향으로 '의미 있는 기울기'를 전달함으로써 시간 방향의 의존 관계를 학습할 수 있다.
  • 기울기는 학습해야 할 의미가 있는 정보가 들어 있고, 그것을 과거로 전달함으로 장기 의존 관계를 학습한다. 하지만 기울기가 중간에 사그라들면 가중치 매개변수는 전혀 갱신되지 않게 된다. - 장기 의존 관계를 학습할 수 없게 된다.

LSTM

기본 바닐라 RNN은 최적화하기 어렵고 성능적 한계가 존재. 이것을 극복하기 위해 나온 것이 'LSTM', 'GRU'와 같은 셀 구조를 갖는 순환 신경망이 등장

문제점)
바닐라 RNN은 시간이 지나면서 입력데이터의 영향이 점점 사라지는 '장기 의존성 문제'와 '기울기 소실과 폭발'이 쉽게 일어나는 구조적 문제를 가짐.

  • 장기의존성
    컨텍스트 범위가 넓을 때 멀리 떨어진 입력에 대한 의존성이 있음에도 불구하고 입력의 영향이 점점 사라지는 현상. 오래된 입력 정보가 사라져 정확한 예측 힘듬.

  • 기울기 소실과 폭발
    (은닉계층)

기본 순환 신경망은 가중치 행렬 W가 반복적으로 곱해지는 구조여서 기울기 소실과 폭발이 쉽게 일어난다.

  • 그래디언트 클리핑
    그래디언트 폭발은 비교적 간단히 막을 수 있는 방법이다. 원리로는 기울기가 일정 크기 이상으로 커지지 않게 한다.

g: 기울기, v: 임계치

즉, 가파른 절벽으 만나 기울기가 급격히 커져 엉뚱한 방향으로 흘러가는 것을 막는다.

LSTM

ResNet처럼 기울기가 소실되지 않는 구조로 바꾼 것이 'LSTM'

인터페이스를 보면 c(cell)라는 경로가 있고, 이것이 기억 매커니즘이다.
이 셀은 LSTM 계층 내에서만 주고 받고, 출력은 은닉 상태 벡터 h뿐이다.

  • 바닐라RNN과 LSTM의 구조 비교


    셀 상태를 연결하는 경로에 가중치 W와의 행렬곱 연산이 없다.
  • 핵심 아이디어
    LSTMs의 핵심은 cell state, 컨베이어 벨트와 같다. 이 구조로 인해 정보가 큰 변함없이 계속적으로 흘러가며 다음 단계에 전달하게 된다.
    컨베이어 벨트 쪽으로 영향을 주며 계산해 나가기

LSTM에서 그래디언트가 잘 흐르는 이유?

LSTM 계층 조립하기

LSTM에는 기억 셀 ct가 있다. c_t에서 시각 t에서의 LSTM의 기억이 저장돼 있는데, 과거로부터 시각 t까지의 필요한 모든 정보가 저장돼 있다고 가정한다. 그리고 필요한 정보를 모두 간직한 이 기억을 바탕으로, 외부 계층에 은닉 상태 h_t를 출력한다. 이때 출력하는 h_t는 아래 그림과 같이 기억 셀의 값을 tanh함수로 변환한 값이다.

Gate>
댐에서 물을 흘려보내는 것처럼 다음 단계로 얼마나 흘려보낼지를 조절한다.

  • 게이트 조절 값: 0.0 ~ 1.0 사이의 실수 (시그모이드 출력과 같다.)
  • Output Gate

    열림 상태는 입력 xt와 이전 상태h(t-1)로부터 구한다.
  • Output gate 추가
  • Forget Gate
    불필요한 정보는 잊자. 무엇을 잊을까?
  • 새로운 기억 셀 추가

    tanh 노드가 계산한 결과가 이전 시각의 기억 셀 c_(t-1)에 더해진다. 기억 셀에 새로운 정보가 추가 된 것이다.
  • Input Gate
    마지막으로 g에 게이트 하나를 더 추가함.
  • LSTM의 기울기 흐름

정리 step by step

  • Forget Gate
    포겟 게이트에 들어가는 정보는 이전 아웃풋과 현재입력이다. 출력으로 cell state로 바로 가지 않고, 시그모이드를 써서 0~1로 만들고, 0이면 cell state에 있는 어떤 값을 버리고, 1이면 이전 cell state 값을 이어간다. 결국 날려버릴지 살릴지 결정하는 gate.
  • Input Gate
    이전 출력과 현재 입력의 계산으로 cell state에 얼마나 얼마나 반영할지 결정하는 gate
  • Update Gate(cell state)
    Input_gate x curr_state + forget_gate x prev_state
  • Output Gate(hidden state)
    최종적으로 얻어진 cell state를 얼마나 내보낼지 정해주는 gate

GRU(Gated Recurrent Units)

gru는 lstm의 장점을 유지하면서 게이트 구조로 단순하게 만든 순환 신경망이다.

GRU는 게이트 메커니즘이 적용된 RNN 프레임워크의 일종, LSTM에 영감을 받았고, 더 간략한 구조를 가지고 있다.

  1. Reset Gate
    과거의 정보를 적당히 리셋시키는게 목적, sigmoid를 출력으로 이용해(0,1)값을 이전 은닉층에 곱해준다.

    직전 시점의 은닉층의 값과 현시점의 정보에 가중치를 곱하여 얻을 수 있다.

  2. Update Gate
    Update gage는 LSTM의 forget gate와 input gate를 합쳐놓은 느낌으로 과거와 현재의 정보의 최신화 비율을 결정한다.

  3. Candidate
    현 시점의 정보 후보군을 계산하는 단계이다. 핵심은 과거 은닉층의 정보를 그래도 이용하지 않고 리셋 게이트의 결과를 곱하여 이용해준다.

  4. 은닉층 계산
    마지막, update gate결과와 candidate 결과를 결합하여 현 시점의 은닉층을 계산하는 단계, sigmoid 함수의 결과는 현시점 결과의 정보의 양을 결정하고, 1-sigmoid함수의 결과는 과거 시점의 정보 양을 결정한다.

  5. 정리

LSTM과 구조상 큰 차이도 없고, 분석 결과도 큰 차이가 없는 것으로 알려져있다. 주제별로 LSTM, GRU가 좋다는 의미, 하지만 GRU가 학습할 가중치가 적다는 것은 확실한 이점이 있다.
(LSTM의 1/4)
(GRU는 LSTM에 비해 셀 구조가 단순해지고 연산량은 줄었지만 성능은 LSTM과 비슷하다.?)
Ref)https://yjjo.tistory.com/18

추가

기울기 소실과 기울기 폭발의 원인을 보자면

역전파로 전해지는 기울기는 차례로 'tanh', '+', 'MatMul(행렬곱)' 연산을 통과하게 된다.
(+는 그냥 흘려보내므로 나머지 두 연산의 의미를 보자.)

  • tanh
  • 점선은 y = tanh(x)의 미분, 값은 1.0 이하이고, x가 0으로부터 멀어질수록 작아진다.
  • 역전파에서는 기울기가 tanh 노드를 지날 때마다 값은 계속 작아진다는 뜻이다.
  • tanh 함수를 T번 통과하면 기울기도 T번 반복해서 작아지게 된다.

<역전파 시 MatMul에서 기울기 변화를 보자>


-> 기울기 dh는 시간 크기에 비례하여 지수적으로 증가한다. 이것이 '기울기 폭발' 이다.
이것은 오버플로을 일으켜 NaN(Not a Number)같은 값을 발생시켜 학습을 제대로 시킬 수 없게 된다.
'''
Wh = np.random.randn(H, H)x0.5
'''

-> 이번엔 처음과 반대로 지수적으로 감소하는 것을 볼 수 있다. 이것이 '기울기 소실' 이다.
이것은 일정 수준 이하로 작아지면 가중치 매개변수가 더 이상 갱신되지 않아, 장기 의존 관계를 학습할 수 없다.

그래서 '기울기 폭발' 해결책으로.

  • 기울기 클리핑

--

  • LSTM 구현

    (클래스)

    초기화 인수는 가중치 매개변수인 Wx와 Wh, 그리고 편향 b

  • Time LSTM 구현

    class TimeRNN:
      def __init__(self, Wx, Wh, b, stateful=False):
          self.params = [Wx, Wh, b]
          self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
          self.layers = None  # RNN 계층을 리스트로 저장
          
          self.h, self.dh = None, None
          self.stateful = stateful
          
      def set_state(self, h):
          '''hidden state(h)를 설정하는 메서드'''
          self.h = h
      
      def reset_state(self):
          '''hidden state(h)를 초기화하는 메서드'''
          self.h = None
          
      def forward(self, xs):
          Wx, Wh, b = self.params
          N, T, D = xs.shape  # N(batch), T(time steps), D(input size)
          D, H = Wx.shape
          
          self.layers = []
          hs = np.empty((N, T, H), dtype='f')
          
          if not self.stateful or self.h is None:
              self.h = np.zeros((N, H), dtype='f')
              
          for t in range(T):
              layer = RNN(*self.params)
              self.h = layer.forward(xs[:, t, :], self.h)
              hs[:, t, :] = self.h
              self.layers.append(layer)
              
          return hs
      
      def backward(self, dhs):
          Wx, Wh, b = self.params
          N, T, H = dhs.shape
          D, H = Wx.shape
          
          dxs = np.empty((N, T, D), dtype='f')
          dh = 0
          grads = [0, 0, 0]
          for t in reversed(range(T)):
              layer = self.layers[t]
              dx, dh = layer.backward(dhs[:, t, :] + dh)  # 합산된 기울기
              dxs[:, t, :] = dx
              
              for i, grad in enumerate(layer.grads):
                  grads[i] += grad
                  
          for i, grad in enumerate(grads):
              self.grads[i][...] = grad
          self.dh = dh
          
          return dxs
  • RNNNL 추가 개선 작업
    1) 계층 다층화 작업

    -> 기존 LSTM계층을 1단으로 쌓아 사용했는데, 층을 겹겹히 2, 3층으로 쌓아 모델의 정확도를 기대할 수 있다.
    2) 드롭아웃에 의한 과적합 억제
    층을 깊게 쌓아 성능을 높이는 장점이 있지만 자칫 오버피팅을 불러일으킨다.
    이에 대한 대책은 지금도 연구되는 주제다.
    (흔한 방법으로 '데이터양 증가', '모델 복잡도 단순화'가 있다. 그 외에 '정규화'도 있음.)

    (뉴런을 무작위로 선택하여 무시)

    그럼 어디에 드랍아웃을 어디에 적용해야 하는가?
    시계열 방향으로 삽입하는 것은 좋지 않다.?

    그 이유는 RNN에서 시계열 방향으로 드랍아웃을 학습 시에 넣어버리면 흐르는 시간에 비례해 노이즈가 축적되어 정보가 사라질 수 있다.
    대신 드랍아웃 계층을 위아래로 삽입하는 방법을 생각한다.

    이렇게 하면 시간 방향(좌우 방향)으로 진행해도 정보 손실이 없다.(시간축과는 독립적으로 상하 방향에난 영향을 주게 된다.)
    하지만, 이를 해결할 대안으로 나온 것이 '변형 드롭아웃'이다.

    이 '변형 드롭아웃'은 깊이 방향, 시간 방향에도 이용할 수 있어 언어 모델의 정화도를 더욱 더 향상시킬 수 있다.
    (*마스크= 데이터의 '통과/차단'을 결정하는 바이너리 형태의 무작위 패턴을 말한다.)

    양방향(Bidirectional)RNN

    진행 방향에 변화를 준 RNN이다.
    배가 너무 [ ] 빵을 먹었다. 라는 예문이 있다면, 빈칸에 들어갈 말은 쉽게 유추가 가능하다.
    그말은 아마도 빵을 먹었다 는 말 때문이다. 만약 순방향이었다면 '배가 불러 빵을 먹었다.'도 될 수 있다. 이를 해결하기 위한 것이 양방향 RNN이다.
    텐서플로우에서 적용시키려면 사용하고자 하는 레이어를 tf.keras.layers.Bidirectional()로 감싸주면 된다.
    ex)tf.keras.layers.Bidirectional(
    tf.keras.layers.SimpleRNN(units=64, use_bias=False, return_sequences=True)
    )

    정리


    쉽게 말해 RNN은 y값이 확 변하는데, LSTM은 세개의 게이트를 통해 변화를 점진적으로 준다.

예)ResNet (Residual block)

  • 단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.

  • 기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 (LSTM이나 GRU)이 효과적

  • LSTM에는 input, forget, output게이트 등 3개의 게이트가 있다.

  • 게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용하여 0.0~1.0 사이의 실수를 출력한다.

  • 언어 모델 개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.

  • RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.

    <참고-밑씨딥2_chapter6>

LSTM

Long Short-Term Memory
기울기 소실 문제를 해결하기 위해 고안된 RNN 레이어
LSTM은 RNN이 진화한 형태이다. RNN을 길게 이으면 훈련 단계는 네트워크를 통해 역전파되는 경사도를 매우 작거나 크게 만들어서 가중치를 0이나 무한대로 만들 수 있다.
딥러닝 네트워크는 backpropergation을 통해 학습을 하는데, 입력값이 길수록 입력된 단어들의 미분 값이 매우 작아지거나 커진다. 너무 작아지는 것을 Vanishing Gradient, 너무 커지면 Exploding Gradient라 한다. LSTM은 gradient vanishing에 강하다.
이 문제를 완화하기 위해서 LSTM에서는 각 단계에 두 개의 출력을 둔다. 하나는 모델의 실제 출력이고 다른 하나는 해당 단계의 내부 상태로 메모리라고 한다.

-기본적인 바닐라(standard)RNN 구조

-LSTM

좀 더 자세히

체인과 같은 구조를 가지고 있지만, 각 반복 모듈은 다른 구조를 가지고 있다. 단순한 neural network layer 한 층 대신에, 4개의 layer가 특별한 방식으로 서로 정보를 주고 받도록 되어 있다.

  • cell state

    LSTM의 핵심은 cell state
    Cell state는 컨베이어 벨트와 같아서, 작은 linear interaction만을 적용시키면서 전체 체인을 계속 구동시킨다. 정보가 전혀 바뀌지 않고 그대로 흐르게만 하는 것은 매우 쉽게 할 수 있다.

-Step by step

  • Forget Gate
    포겟 게이트에 들어가는 정보는 이전 아웃풋과 현재입력이다. 출력으로 cell state로 바로 가지 않고, 시그모이드를 써서 0~1로 만들고, 0이면 cell state에 있는 어떤 값을 버리고, 1이면 이전 cell state 값을 이어간다. 결국 날려버릴지 살릴지 결정하는 gate.
  • Input Gate
    이전 출력과 현재 입력의 계산으로 cell state에 얼마나 얼마나 반영할지 결정하는 gate
  • Update Gate(cell state)
    Input_gate x curr_state + forget_gate x prev_state
  • Output Gate(hidden state)
    최종적으로 얻어진 cell state를 얼마나 내보낼지 정해주는 gate

결국 목적은 내가 현재 입력과 이전 출력가지고 cell state에 값을 집어 넣고 어떻게 밖으로 출력하지를 결정하는 것이다.
실제로 사용할 땐 뉴럴 네트워크 고려할 필요없이
입력과 출력을 정해주고 초기화만 잘 시켜주면 텐서플로우가 알아서 해준다.

  • 가중치 공유

    가중치 공유는 Embedding 계층의 가중치와 Affine 계층의 가중치를 연결하는 기법이다. 두 계층이 가중치를 공유함으로써 학습하는 매개변수 수가 크게 줄어드는 동시에 정확도도 향상되는 일석이조의 기술이다.

  • 개선된 RNNLM 구현

    정리- 개선점 3가지
    1) LSTM 계층의 다층화
    2) 드롭아웃 사용(깊이 방향으로)
    3) 가중치 공유

    이 세 가지 개선점이 들어간 BettherRnnlm 클래스
    '''
    import sys
    sys.path.append('..')
    from common.time_layers import
    from common.np import
    # import numpy as np
    from common.base_model import BaseModel

    class BetterRnnlm(BaseModel):
    
       LSTM 계층을 2개 사용하고 각 층에 드롭아웃을 적용한 모델이다.
       아래 [1]에서 제안한 모델을 기초로 하였고, [2]와 [3]의 가중치 공유(weight tying)를 적용했다.
       [1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
       [2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
       [3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
      '''
      def __init__(self, vocab_size=10000, wordvec_size=650,
                   hidden_size=650, dropout_ratio=0.5):
          V, D, H = vocab_size, wordvec_size, hidden_size
          rn = np.random.randn
    
          embed_W = (rn(V, D) / 100).astype('f')
          lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
          lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
          lstm_b1 = np.zeros(4 * H).astype('f')
          lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
          lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
          lstm_b2 = np.zeros(4 * H).astype('f')
          affine_b = np.zeros(V).astype('f')
    
    # 이 부분이  세 가지 개선 요소가 적용됨!
          self.layers = [
              TimeEmbedding(embed_W),
              TimeDropout(dropout_ratio),
              TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
              TimeDropout(dropout_ratio),
              TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
              TimeDropout(dropout_ratio),
              TimeAffine(embed_W.T, affine_b)  # weight tying!!
          ]
          self.loss_layer = TimeSoftmaxWithLoss()
          self.lstm_layers = [self.layers[2], self.layers[4]]
          self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]
    
          self.params, self.grads = [], []
          for layer in self.layers:
              self.params += layer.params
              self.grads += layer.grads
    
      def predict(self, xs, train_flg=False):
          for layer in self.drop_layers:
              layer.train_flg = train_flg
    
          for layer in self.layers:
              xs = layer.forward(xs)
          return xs
    
      def forward(self, xs, ts, train_flg=True):
          score = self.predict(xs, train_flg)
          loss = self.loss_layer.forward(score, ts)
          return loss
    
      def backward(self, dout=1):
          dout = self.loss_layer.backward(dout)
          for layer in reversed(self.layers):
              dout = layer.backward(dout)
          return dout
    
      def reset_state(self):
          for layer in self.lstm_layers:
              layer.reset_state()
profile
Innovation is mine
post-custom-banner

0개의 댓글