본문 바로가기
딥러닝

RNN 2

by 볼록티 2021. 5. 6.
728x90
반응형

 

 

 RNN 에 대해서 한 번 간단히 정리한 적이 있는데, 대강 흐름이나 아이디어나 뭐 직관적으로만 본 것이라 몇시간만 보고 끝냈던 적이 있다. 좀 더 자세히 강의와 블로그를 참조하여 RNN이 왜 필요하고 어떻게 작동하고 어떻게 사용되는지 알아보도록 한다.

 

 

RNN 은 recurrent neural network 의 약자이다. recurrent 는 말그대로 '되풀이되는' 이라는 뜻을 가지고 있고, 실제로 그렇게 네트워크 구조가 구성되어 있다.~

 

먼저 Sequantial Data 가 뭔지 알아보자. 주로 우리는 이미지나 다변량 데이터를 다뤘다. 다변량 같은 거는 각 feature들이 독립이라고 가정을 하였고, 이미지 데이터같은거는 하나의 픽셀을 하나하나의 variable 로 가정을 했는데 이 때 픽셀 간에 coherence 가 존재하는 그런 데이터였다.

 시퀀스 데이터도 비슷하다. 스퀀스 데이터는 순서정보를 갖는다. 어떻게 나열되어 있는가가 데이터의 의미를 결정짓는다고 생각하면 된다. 가장 대표적인 예제는 아래의 문장이다. 단어들의 나열로 하나의 문장을 이루고 있다. 각 단어를 x1, x2, x3 라고 보면 된다.

 

 다음 예는 의학 신호 정보이다. 시간 순서에 따라서 의학 정보를 나타내고 있다.

 

 다음 예는 음성 신호이다. 매 시간마다 음성의 크기나 빈도 등이 기록되게 된다.

 다음 예는 비디오 데이터이다. 비디오 데이터 역시 시퀀스인데, 앞에서 CNN을 다룰때 static한 하나의 이미지 같은 w x h x c 같은 텐서로 표현했다. 동영상에서는 이러한 w x h x c 와 같은 컬러 이미지들이 시간순서대로 죽 나열이 되어 있는 4 Dimension 형태로 보면 된다. 여기에 배치형태로 학습이 되니 5 D가 된다.

 

 

 


 

* 시퀀스데이터로 어떻게 모델링을 할까?

 

첫번째로는 시퀀스 자체에 분포를 학습해서 그 분포로부터 새로운 시퀀스를 샘플링 할 수가 있다.  $P(x_{1},x_{2},...,x_{N})$ N 개의 원소로 이뤄진 시퀀스에서 각 원소에 대한 joint probability 분포를 추정하는 문제로 귀속된다. 관측 데이터로 이 분포를 성공적으로 추정할 수있다면 다양한 종류의 시퀀스들을 샘플링 할 수 있게 된다.

 

두번째로는, $P(y_{1},y_{2},...,y_{M} | I)$ 와 같은 Task 이다. 예를 들면 이미지가 주어져있을 때, 이미지를 설명하는 문장을 생성하는 것이다. 이럴 때는 주어진 것이 I 이미지이다. 이 이미지를 설명하는 단어들의 시퀀스를 생성해내는 모델이 된다. 여기서 추정해야하는 분포는 이미지가 주어져있을 때 단어들의 시퀀드에 대한 분포를 추정해야한다.

 

세번째로는, $P(y | x_{1},x_{2},...,x_{N})$ 의 경우이다. 예를 들면, 비디오가 있다. 비디오 클립에서 사람이 수행하고 있는 행동을 인식하는 그런 문제라 생각해보자. 이때 input 은 비디오이고 output은 class 가 될 것이다. 이런 경우에는 N개의 x 프레임 시퀀스가 주어질때 x의 분포를 추정해내는 문제이다.


네번째로는, $P(y_{1},y_{2},...,y_{M} | x_{1},x_{2},...,x_{N})$ 로 나타내는 음성 인식이다. 위에 예시 그림에서 보았던 speech waveform 이 input 으로 들어간다. 그러면 output 형태는 음성에 해당되는 단어들의 나열이 된다.  시리나 구글 어시스턴스 쓸 때 음성으로 명령을 내리면, 먼저 음성을 텍스트 형태로 만들어야하는데 그 때 필요한 기술이라고 할 수 있다. 즉, 시퀀스가 들어오고 시퀀스가 나온다.

 

다섯번째로는, $P(y_{1},y_{2},...,y_{M} | x_{1},x_{2},...,x_{N})$ 동일한 형태인데, 번역이다. 파파고, 카카오i 와 같은 번역기를 말한다. 마찬가지로 시퀀스가 들어오고 시퀀스가 나온다.

 

여섯번째로는, object tracking도 있다. 이것은 비디오에서 어떤 객체를 계속해서 추적해나가는 것이다. 이런것도 역시 프레임이 들어오고 output을 분류와 Box 를 출력해주는 것이라 할 수 있을 것이다.

 

또 하나를 더 보면 비디오 프레임 시퀀스가 input 으로 들어오고 output으로 문장이 나와서 그 비디오를 설명해주는 문장을 출력한다.

 

 


 

* sequence representation

 

단어들은 결국 숫자로 즉, 벡터로 표현해야한다. 그 중 오래된 방식 중 하나가 Bag of words 방식이다. 이는 문장이 있으면 문장에 포함된 단어들을 이용해서 고정된 크기의 벡터로 모든 문장들을 표현한다. 아래의 예시처럼 문장의 구성요소인 단어별로 쪼개서 단어가 해당하는 벡터 인덱스 부분에 1로 할당한다. 

 이를 위해서는 vocabulary dictionary를 구성해야 한다. 즉 문서의 모든 단어에 대한 단어장이 있어야 한다는 것이다. 이말인 즉슨, 단어장에 없는 새로운 단어가 들어오면? mapping 을 할 수 없게 된다는 것을 알 수 있다.

 

 아래의 예제를 보면 문장에 나오는 단어는 총 8개가 되고, I dislike rain 이라는 문장이 들어오면, vocabulary set에서 해당 단어의 인덱스에 해당하는 곳에 1로 할당을 해서 숫자 벡터로 representation을 해준다.

 그리고 이 방법의 치명적인 단점은 단어의 순서 정보를 고려할 수 없게 된다. 단어가 있냐 없냐만을 가지고 벡터화 하는 것이지 순서에 따라서 벡터가 달라지지는 않는다. 아래의 두 문장을 비교해놓은 것을 보면 왜그런지 알 수 있다. 문장의 의미는 다른데, Bag of words에서는 같은 벡터로 같는다. 

 


 

그러면 순서정보를 어떻게 고려할 수 있을까?

 가장 나이브한 방법으로는 위의 방식인 one-hot-vector를 죽 나열한다. 이렇게 하면 단어의 순서에 따라서 벡터 representation을 다르게 가져갈 수 있다. 

 하지만 이 표현 방식은 굉장히 비효율적이다. 두 문장을 비교해놓은것을 보면 둘 다 동일한 의미의 문장인데, 벡터 표현으로 보면 완전히 다른 문장으로 인식이 될 수 있다는 것이다.

 


세번째로는 마르코프 모델을 구성해볼 수 있다. 다음 번에 뭐가 될지를 예측할 때, 바로 직전의 정보만을 가지고 그 다음 형태를 예측할 수 있다를 가정한다. 즉, 다음 단어가 무슨 단어가 나올 것이냐? 하면 바로 직전에 무슨단어들이 모있는지를 보고 결정을 하게 된다는 것이다. 

 여기서의 문제는 long-term dependency를 고려할 수 없다는 것이다. 일단 문장을 보면 앞서 나왔던 단어나 맥락에 의해서 다음에 무슨 얘기가 나올지 예측해야되는 경우가 있을 텐데, 이럴때는 이 방법을 사용하면 비효율적이고 적합하지 않은 한계가 있다는 것을 알 수 있다. 

 


그래서 시퀀스 모델링을 만들기 위해서 모델이 어떤 속성을 가져야하는지 정리해보자.

 

1. 가변적인 길이의 시퀀스 정보를 처리할 수 잇어야한다

    -> 이전까지는 하나의 input 은 고정된 크기를 가졌지만, 시퀀스는 객체마다 다른 크기의 시퀀스를 가질 수 있기 때문이다.

 

2. 시퀀스의 순서 정보를 모델링 할 수 있어야 한다.

 

3. long-term 정보를 계속 tracking 할 수 있어야 한다.

 

4. 1~3을 충족시키기 위해 시퀀스를 관통하는 파라미터를 공유할 수 있어야 한다. 이것은 사실 가변적인 길이의 시퀀스를 다루기 위해서 자연스럽게 도출되는 결과라고 생각할 수 있다. 

  -> 예를 들어 두 문장이 있다고 해보자. x1 = {w0, w1, w2, w3, w5}, x2 = {w0, ... , w7} 이 둘은 서로 다른 개수의 원소를 가진 문장이다. x1 문장을 보고서 아래와 같이 w0, w1, ... 를 서로다른 layer에 각각 입력으로 넣고 하는 구성을 만들었다고 하면, 첫번째 데이터는 5개의 데이터를 가지지만 두번째 x2는 8개의 원소를 가지니까 두번째 x2를 처리하기위한 아키텍처가 달라져야한다. 근데 여기서 layer들이 공유된 parameter를 가진다고하면, 몇개가 들어오던지간에 재활용이 가능해진다. 여기서 가변적인 길이를 처리하지 못하는 이유는 이 layer들이 서로다른 weight를 가지기 때문이다. 하지만 공유가 된다면 가변적인 길이의 데이터를 처리할 수 있기 때문이다.

 


 앞서 설명한 것들을 다 처리할 수 있게 고안된 방법이 RNN 방법이다. 

RNN은 앞서 배웟던 NN 개념에서 크게 벗어나지는 않는다. RNN 의 특징은, layer들이 parameter를 sharing 하고 있다는 것이다. 

 아래는 단순한 Feed forward network와 RNN 을 비교해놓은 그림이다. 기존에는 Feed forward 하게 인풋들어가고 히든레이어 거쳐서 아웃풋나오는 방식이었다.

RNN에서는 input 이 들어가면 h_n 이라는 representation을 만들어낸다. 우리가 다루는 데이터는 시퀀스이기 때문에 하나의 데이터를 처리하기 위해서 x1을 input으로 넣어서 h1을 만들고, x2를 input으로 넣어서 h2를 만들어내는데, h2를 만들어 낼때 이전 input으로 만들었던 h1을 활용한다. 

 이부분에서 recurrent 한 representation이 존재하여 RNN으로 불린다.

 가장 오른쪽 그림을 보면 RNN을 unfolding(unrolling) 하게 Feed forward 구조와 비슷하게 죽 나열된 것을 볼 수 있다. x1은 y1이라는 output을 출력할 수 있을 것이고, x2의 output y2는 x1으로부터 만들어진 h1의 영향을 받아서 출력된것을 알 수 있다. 

 

여기서 중요한 특징 중에 하나가. 위의 Notice 이다. 매 time step에서 입력 데이터를 처리하는 부분이랑 이전 step에 activation을 가지고 처리하는 부분에 대해서 모든 time step에서 parameter들이 공유된다는 것이다. 여기서 만약 공유가 안된다면 아까 말한 가변적인 길이의 데이터를 처리할 수 없는 문제에 맞닥들이게 되는 것이다.

 


식을 통해서 RNN을 살펴보도록 하자.

가장 기본적인 구조를 vanilla RNN이라 부른다. 입력 데이터가 어떻게 연산되는지 보자.

아래의 그림이 도식화한 그림이다. 왼쪽에 보이는 그림은 두개의 입력을 받는 것을 볼 수 있다. x는 입력데이터이고, h는 히든 스테이트에서 가져오는 representation이다. 즉, h_t를 계산하기 위해 입력으로 x, h가 활용되고, W 행렬을 곱하고 activation함수로 출력을 해준다.

 W 라는 라는 것은 U랑 V로 표현, 즉 아래의 파란색 필기처럼 표현하면 이해하기가 쉬울 수 있다. 각각 weight를 곱하고 더해서 activation 취해서 h_t를 만들어내는 것이다.!

 처음 h_0는 initial 하게 랜덤을 주거나 0을 주거나 그렇게 한다. 

 이 예제는 매 해당 x 마다 y가 출력된다. 이런 아키텍처는 모델을 어떻게 구성하냐에 따라 다른건데, 여기서는 매 time step마다 예측값이 출력된다고 할 수 있다. 각 y에서 나온 loss의 합이 최소화가 되게끔 학습한다고 할 수 있다. !

 

 


Applications of RNN

이제 이 RNN을 가지고 어떤 문제를 풀 수 있는지 알아보도록 하자.

 

가장 많이 사용된 어플리케이션이 Sentiment classification 이다.

이것은 시퀀스 문장이 들어오고 binary 하게 classification 하는 문제이다. 예를 들면 영화에 대한 평가나 식당에 대한 후기를 가지고 긍정인지 부정인지를 분류하는 문제라고 할 수 있다.

 

input으로는 문장이 들어오고 output으로는 positive/negative로 나오는 것이다.

 

RNN에서는 input으로 문장의 각 단어를 넣어주게 된다. 예측값은 하나의 binary class이니까, 이런 경우에는 RNN 모델은 x1 이 들어가면 h1 이 생성되고 , x2가 들어가면 h1 반영해서 h2가 생성되고 죽죽 가서 h_n 까지 가는데, 이 과정에서 h1, h2, 들이 예측값을 내지는 않는다 ignore한다는 것이다. 그래서 매 time step에서의 output은 무시하고, 최종적으로 모든 word들과 순서정보가 encoding된 h_n이라는 output을 이용해서 binary classification을 진행한다.

 

Sentiment classification

 


 

두번째 어플리케이션은 Language modeling 이다. 

 자연어 처리에서 궁극적인 목표라고 할 수 있다. 하고자 하는 것은 사람의 언어를 잘 이해하고 있는 모델이 있다면, 사람이 말하는 것 혹은 쓰는 문장을 생성해내는 것이다. 예를 들어 단어를 넣으면 그 다음 단어를 예측하는 식으로 말이다. 

 어떤 주제에 대한 의견을 쓴다고 할 때, 관련 문장을 무한정 생성해 낼 수 있다는 것이다.

이 Task는 이전 단어를 보고 다음 단어를 예측하는 것이 된다.

아래의 그림을 보면 먼저 <s>라는 처음 토큰(문장을 구성하는 가장 작은 단위 : 단어)을 입력해준다. 어떤 문장이 주어질 때, 문장을 어떤 원소들의 시퀀스로 표현한다면 각 원소를 토큰이라 부른다. 여기서 <s>라는 start token은 임의의 원소라 생각하면 되고, network한테 output을 출력해라~ 라고 signal을 준다고 생각하면 된다. 

 이 start token이 첫번째 단어가 되서 모델의 입력으로 들어가면 h1을 만들어 낼테고, 이 h1을 이용해서 vocabulary set에 대한 예측을 진행한다.즉 classification문제랑 똑같은거다. 그러면 softmax해서  $\hat{p}_{1}$ 이 나온다. 여기서 뽑힌게 There인데, 이 단어가 다시 입력으로 들어가고 또다시 반복한다. 

 이 문제같은 경우에는 매 time step 마다 loss가 계산된다. 매 time step 마다 다음 단어를 맞게 예측했는지를 가지고 loss를 계산할 수 있다. 하나의 문장이 들어가면 매 time step에서 나온 loss의 합을 가지고 모델을 학습시킬 수 있게 된다. 그래서 language modeling 할 때는 x,y가 pair로 주어진게 아니라, 그냥 문단 즉, 문장 덩어리로 학습을 시킨다. 첫번째로 문장 첫번째 단어를 넣고, 두번째 단어를 예측하도록 학습한다. 

 아래의 예제는 512개의 hidden node를 가진 512dim vector를 RNN을 3개 쌓은 것이다. 매 time step 마다 예측을 하게 된다. 오른쪽의 output을 보면 학습을 하면 할수록 실제로 존재하는 단어, 그리고 읽었을 때 말이 되는 문장을 생성하는 것을 확인할 수 있다. 

 

 

 


 

세번째 같은 경우는 Image capioning 을 하는 경우이다.

 이미지가 입력으로 들어오면, 이미지를 설명하는 하나의 문장을 생성하는 Task 이다. 예제를 보면 할아버지 이미지가 들어왔다면, 이미지가 담고 있는 의미를 추출하기 위해서 앞서 배웠던 CNN을 활용한다. CNN이 이미지 representation뽑아내는데 잘하기 때문이다. 여기서는 pre-trained된 feature를 가져와서 이미지를 transformation해주는 역할이다.

 아래의 필기를 예를 들면, ResNet 의 pre-trained된 모델을 50layer 를 쓴다고 하면 인풋이 224x224라고 하면 layer를 거쳐서 512 dim의 벡터를 뽑아내게 된다. 워낙 많은 데이터로 학습이 되었기 때문에 왠만한 이미지에 대해서 의미정보를 뽑아낼 수 있다. 512 dim 벡터에 다 녹아있다고 할 수 있다. 이렇게 뽑아낸 feature가 RNN에 초기 히든 스테이트 벡터로 사용이 된다.  앞에서는 0이나 랜덤값을 채워넣는다고 했는데, generation되는 단어들이 input 이미지에 conditioned 되어 있어야 한다. 그래서 CNN으로 뽑은 feature를 초기 h_0값으로 넣어주게 된다. 이후에 다음 단어를 예측하는 거는 동일하다. 

그리고 출력된 단어들의 조합으로의 문장을 보고 실제 이미지에 딸린 ground truth 문장과 비교해서 얼마나 일치하는지 계산을 할 수 있게 된다. 이 loss를 계산해서 caption을 generation하는 부분을 업데이트하며 학습한다.

 

 

 

 


 

 그다음 네번째 경우는 Machine translation 이다. 여기서는 영어를 받아서 불어로 번역하는 문제를 예로 든다.

여기서 좀 다른거는

첫번째, 입력 시퀀스 길이와 출력 시퀀스 길이가 둘 다 가변적이라는 것이다.

두번째, 입력 문장이 주어질 때 특정 위치의 단어가 출력 문장에서 같은 위치에 나타나지 않는다는 것이다. 대표적으로 한국어랑 영어의 어순이 다른것 처럼 말이다.

 아래의 그림처럼 입력 문장이 3개 들어오면 E3, F1 사이에 화살표에서 나오는 벡터는 E를 몽땅 다 인코딩한 벡터라고 할 수 있다. 이를 출발점으로 삼아서 디코딩을 실시한다. 그리고 번역된 문장을 ground truth와 비교해서 loss를 계산해서 BP하고 학습한다. 

 이것이 유명한 인코더 디코더를 이용한 Seq2Seq 모델이다. 정리하면 입력을 죽 인코딩하는 인코더가 있고, 마지막 representation을 받아서 디코딩을 수행하는 디코더로 이뤄진 것이라 할 수 있다.

 

 

 

RNN은 매시점 예측할 수도 있고, 모아서 마지막에 한번만 예측할 수도 있는 것 처럼 굉장히 Flexible 하다는 것을 알 수 있다. 아래의 아키텍처를 통한 어플리케이션을 보면 한눈에 알아볼 수 있다. 

 

 


 

Backpropagation

 

이제 RNN 에서 역전파 하는걸 알아보자. 우선 역전파 리뷰 먼저해보자.! 

RNN은 학습하기가 특별하게 까다롭다. 직관적으로 이해하려면, 어떻게 역전파하는지를 알아야한다. 지금까지는 feed forward 라서 아래의 왼쪽 그림처럼 Cost까지 뽑아서 gradient 구하는 과정은 알고 있다. 오른쪽의 업데이트 식이나, 그 아래의 Cost 을 미분한 gradient 를 구하는 것까지 배웠다. feed forward 에서 gradient는 chain rule만하면 구할 수 있었다......

 

 

 


 

RNN에서 feed forward 하는 걸 살펴보자. 먼저 x가 입력으로 들어가면 W와 곱해지고, 아까는 h였는데 여기서는 s 라는 히든스테이트 representation과 곱해지고 activation거쳐서 다음 s를 만들어주게 된다. 다음 스텝에서도 W weight 행렬은 share라는 것을 기억하자.! 

 

 

 

 

 

다음으로 시간축에 대해서 unfolding 한 것에 대해 알아보자.

unfolding을 하면 아래의 왼쪽 구조와 같다. 시간에 따라 죽죽죽 표현된 것이다. recurrent 한 operation을 빼면 오른쪽으로 죽 activation이 전달되는 구조를 가지고 있다. 만약에 비디오 프레임이 들어와서 매 스텝마다 예측값을 내야하고 loss도 구할 수 있는 Task라고 생각해보자. 

 그 그림이 오른쪽이다. 먼저 s0와 V를 가지고 output을 낸다. 그래서 J0 라는 loss를 계산한다. 그리고 이어서 마찬가지로 J1이라는 loss도 또 계산하다. 이렇게 forward를 시켜볼 수 있다.

 

 

 

 

 

 자, 이제 gradient 를 계산해보도록 하자. 왼쪽 그림처럼 RNN 구조를 가지고 있다면, 하나의 데이터에 대한 전체 loss 는 매 스텝에서 나온 lsos들의 총 합이라고 할 수 있다. $J = \sum J_{t}$ 처럼 말이다. 

 아래의 예제를 통해서 x2가 들어가는 부분의 W 를 계산해보자. 그렇다면 전체 loss를 W에 대해서 미분을 해야하는 문제에 직면한다. 여기서는 $\frac{\partial J_{0}}{\partial W} + \frac{\partial J_{1}}{\partial W} + \frac{\partial J_{2}}{\partial W} + \cdot \cdot \cdot$ 중에서 2번째 Jacobian loss에 대한 gradient구하는 과정을 자세히 다뤄본다. 

 그 전개를 한것이 아래의 수식에 나와있는데, J_2를 y_2로 y_2를 s_2로 s_2를 W로 이렇게 죽 연계된 chain rule을 구성한다.

여기서 중요한게 s_2 라는 것은 s_1 이라는 이전상태의 히든 스테이드정보와 U가 곱해진 것으로 이뤄진 것이다. 그래서 s_2를 s1과 U로 이뤄진것이니까 그렇게 표현하는데 그렇게 하려니까 아래의 수식에 가장 아래의 하늘색 칠해놓은걸 보면 s_1 도 역시나 또 W로 계산된거긴한데, s_0에 영향을 받고 있는거다. 그래서 단순히 s2를 W로 미분하는걸 constant로 볼 수 가 없다는 것이다. 단순하게 W에 대해 간단히 미분할 수 있는 형태가 아니다. 

 

 

 

 그러면 어떻게 표현을 해줘야 하느냐? 

gradient가 의미하는게 애초에 분모의 해당하는 텀의 단위만큼 변했을때 분자의 해당하는 텀이 얼마나 변할 것이냐를 말한다. 여기서 $\frac{\partial s_{2}}{\partial W}$를 알아야하는데, 아래의 그림에서 y2를 계산하는데 있어서 필요한 값이 W가 변함으로써 얼마나 변하는지를 알아야 한다. 그런데 W라고 하는애가 모든 스텝에 다 걸쳐있다. 그러면 먼저, W를 1만큼 변했다고 하면, s2가 그만큼 변할 것이다. 그 양을 아래의 $\frac{\partial s_{2}}{\partial W}$ 라고 할 수 있겠고, W를 1만큼 변화시켰다고 하면 x0, x1과 만나는 곳에서의 W도 변화됐다는걸 의미한다. 그러면 s1에서 s2로 넘어갈때 s1밑에서 받은 W에서 얼마나 받았는지도 살펴봐야한다는 것이다. 즉 s2는 s1에 영향을 s1은 W에 영향을 받는것을 나타내는게 $\frac{\partial s_{2}}{\partial s_{1}}\frac{\partial s_{1}}{\partial W}$ 부분인 것이다. 또다시 x1에 있는 W가 1만큼 변하면 역시 s0도 일정부분 변할 것이다. 그러면 s0가 단위만큼 변할 때, s2가 얼마나 변하는지도 알아야 한다. 그 텀이 $\frac{\partial s_{2}}{\partial s_{0}}\frac{\partial s_{0}}{\partial W}$ 가 된다. 

 W 행렬이 매 스텝마다 공유되어있어서 W를 한단위 변화시킨다는 것은 매 스텝에서의 output에 영향을 주는것이다.  

 W 가 한단위 변한다는거는 x2에서 올라오고 x1에서 올라오고 x0에서 올라오는 W를 다 더해줘야! W가 변했을 때, ㄴ2가 얼마나 변하는지를 알 수 있다는 것이다. 

 위의 설명은 summation 을 활용해 식으로 표현가능하다 이것이 오른쪽 부분의 식이다. 

이 summation 이 feedforward와의 차이라고 생각하면 된다. 

 

아래의 식은 위에서 전개를 다시 정리한 식이다. 여기서 주목해야할 부분은 아래의 초록색 마킹 부분이다. $\frac{\partial s_{n}}{\partial s_{0}}$ 를 나타내는데, 맨처음 히든 스테이트 벡터가 조금 변한게 n번째 스텝의 히든스테이트에 얼마만큼의 변동을 가져오는가를 뜻한다. 오른쪽의 식을 보면은 문제점을 바로 지적할 수 있다. 

 

 

특정 k 시점의 히든스테이트를 이전 히든스테이트로 미분을 해보면 아래의 식과 같다. sk는 f로 묶인 텀이 되고, 얘를 k-1시점의 히든스테이트로 미분한다. 그러면 diag(~~)U로 표현된다. U는 weight 행렬이다. 보통 0을 중심으로한 작은 값으로 초기화를 시켜준다. f' 같은 경우는 activation 함수를 미분한 그 값이고, 역시 1보다 작은 값이다. 전체적으로 특수한 경우를 제외하고 1보다 작은 값을 가질 수 밖에 없다. 맨초기 시점의 히든 스테이트 벡터가 n번째 스텝의 히든스테이트에 영향을 주는 변동성에 위에 오른쪽 마스킹부분처럼 다 곱해져야하는데, 이 값이 결국 엄청 작아져서 0에 가까워진다. 엄청나게 작아진다는 것은 곧 학습이 잘 안되는 결과를 야기한다. 특히 n번째 스텝과 0번째 스텝의 term 이 long 해지면 학습이 잘안된다고 할 수 있는 것이다. 

 

 

RNN 그래디언트는 아래의 chain rule로 표현된다. 이전 시점의 히든스테이트들 간의 gradient가 계속 곱해지는데, 이게 곧 Jacobian 행렬로 계산이 되게 되는데, 이 Jacobian 행렬의 singular value가 1보다 크면 explode해지게되고, singular value가 1보다 작으면 vanish 해버리는 현상이 발생한다.

 1에 가까워야한당.. 뭐 결국 롱텀에는 어렵다는 말!

Gradient vanishing/exploding problem(Bengio et al. 1994)

 

 

 


그러면 RNN 모델을 학습하기 위한 팁은 어떤게 있을까?

RNN 은 병렬계산구조에 도움받기 힘들게 구성되어 있다. 이전에는 행렬곱을 병렬로 한방에 딱 계산이 가능하다만 RNN은 그렇지 않다는 것이다. 첫 입력이 들어가고 끝나야 두번째 입력이 들어가야하는 루프이기 때문에 활용하기 힘들다. for루프를 돌려야하는것이다.

 

 연산상의 단점을 좀 효율적이게 할 수 있는 방법

 

1. 미니배치를 만들 때 약간의 트릭을 넣어주는 것이다. 전체 데이터가 갖는 각각 데이터의 길이가 잇을텐데 길이가 같은 데이터로 미니배치를 구성하는 것이다. 왜냐하면 만약에 들어가는 입력데이터의 길이가 너무 차이나면 엄청 패딩을 해줘야하니 비효율적이라는 소리이다. 그래서 대강 비슷한 길이의 시퀀스끼리 구성을 하게 하자는 아이디어이다. 

 

 2.  Clipping 하는 것이다. 아래의 왼쪽 그림을 보면, optimal을 찾다가 절벽을 만나는 것이다. 그러면 기울기가 엄청심해져서 위로 확튀었다가 또 저 멀리 튀는 것처럼 난리가 난다. 하지만 clipping을 사용하면 이런 것을 방지해준다. 임계점을 정해놓고, 이를 넘어가면 그냥 임계점이 되도록 하는 식으로 방지해주는 것이다. 그냥 안튀고 수렴하게끔 말이다. CNN에서도 사용한다. loss가 nan이 뜬다거나 infinity가 뜬다면 gradient의 magnitude를 살펴볼 필요가 있겠다. 

  

3. Truncated Back Propagation through time

 앞서 살펴본 것 처럼 맨 마지막 loss 텀에 gradient 계산할 때 초기 스텝까지 쭉 끌어와서 다 계산해야해서 계산량이 많았는데, 이러면 너무 시간이 오래걸리고, 엄청 오래전에 정보는 제대로 전달이 안되니까 일정 시간의 안에서만 gradient를 활용해보자는 아이디어이다. y5에 대한 것으로 미분가자고 하면, 정해진 chunk 에서만 gradient 를 구해보자는 것이다. 이러면은 어느정도 exploding/vanishing 문제를 일정 해결할 수 있겠다.

4. Teacher forcing

 이거 약간 트릭같은 방법이다. 학습을 할 때, 실제 데이터가 가진 이전 시점에서의 ground truth 를 준다. 그러면 모델 성능이 좋아지고, 예측도 용이해지게 된다. 

 식으로 표하면 기존의 $P(y_{t}|x_{t}, x_{t-1}, ..., x_{0})$ 에서 $P(y_{t} | x_{t}, ... x_{0}, y_{t-1}, ... ,y_{0})$와 같이 condtion을 늘린다는 것이다. test 할 때는 ground truth가 없으니까 이전 입력의 결과를 활용한다.

 

5. Dropout

6. Initialization

 히든스테이트를 어떻게 초기화하느냐에 따라서 효율성을 증가시켜주는 것.

 


Gated RNNs

 

 RNN의 단점도 보완하면서 좀 더 학습이 용이하고 컨트롤하기 쉬운 메커니즘이 구조적으로 아예 들어가있는 모델을 사용하는 방법이 있다. RNN의 gate 를 포함하고 있다. 이 gate의 역할은 히든 스테이트가 이전거를 계속 이어지면서 이전 정보가 계속 이어질거다라고 생각하는 건 너무 나이브한 접근이라는 것이다. 그래서 어떤 information을 충분히 남기고 어떨때는 output으로 넘겨줘야하고 그러한 결정을 해주는 것이 gate의 역할이다. 

 또한 gate 메커니즘을 활용하면 이전의 과거 정보를 활용하는 새로운 통로가 하나 생긴다. 시퀀스 데이터에서 key가 되는건 순서들이 long term일 때 어떻게 이 정보를 학습할건가인데 이에 대해서 또다른 통로를 생성하여 관리한다. 기본적으로 RNN 보다 성능이 좋다고 알려져있고, LSTM과 GRU가 대표적인 예이다.

 


 

Long short-term memory(LSTM)

 기본적으로 recurrent unit이기 때문에 특정 시점에서 입력을 받고 이전 시점에서의 히든 스테이트 정보가 넘어오는 것은 동일하다. 그 두개의 정보를 어떻게 처리해서 이 셀을 어떻게 업데이트 할 것인가? 에 차이만 있을 뿐이다. LSTM에서는 3개의 새로운 텀이 등장하게 된다. 여기서는  $f_{t}, i_{t}, o_{t}$ 인 애들이다. $f_{t}$는 forget의 약자이다. forget gate라는 말이다. 이전 정보 중에서 지워야할 것을 결정해주는 gate라고 생각하면 된다. $i_{t}$는 input의 약자로 input gate라는 말이다. input으로 들어오는 것 중에서 필요한것만 남기는 역할이다. $o_{t}$는 output gate라는 말이고, 예측을 수행해야할 때, output을 내야할 때, 혹은 셀의 update를 해야할 때 t 시점에서의 $h_{t}$가 output으로도 쓸 수 있고, 그다음 스테이트에 넘겨줄 수도 있다는 것이다. final state에서 꼭 남아있어야 하는 것 또는 지워도 되는 것을 결정해주는 역할이라고 보면 된다. 이 $f, i, o$ 라는 세개의 gate는 동일한 operation을 가진다. 입력으로는 $h_{t-1}$을 입력으로 받고, 지금 시점 t 의 입력을 받는다. 그리고 sigmoid activation $\sigma$을 취한다. 그러니 이 셋은 0~1 사이의 값을 가지게 된다. 이 각 gate는 고유의 weight matrix를 가지고 있다. 각각 $W_{f}$, $W_{i}$, $W_{o}$ 라는 행렬을 가지고 있고, bias 벡터 $b_{f}$, $b_{i}$, $b_{o}$도 가지고 있는걸 볼 수 있다. 

 

 그리고 $\tilde{C}_{t}$ 가 나온다. 이거는 LSTM에서 히든 스테이트 말고 또다른 상태를 나타내는 정보가 있는데, 아까 위에서 말한 새로운 통로를 뭐 하나 더 만든다는 그게 이것이다. 셀 스테이트 라고 부른다. $\tilde{C}$라는 것은 최종적으로 $h_{t}$를 계산하기 위해 후보가 될만 정보들을 남겨놓는 variable이라고 보면된다. 우리의 재료는 $h_{t-1}$이랑 $x_{t}$라고 할 수 있다. 이 두개로 weighted sum을 한다. 그리고 $b_{C}$ bias를 더해준다. 그다음 tanh activation을 취한다.  그렇게 $\tilde{C}$ 를 만들어 낸다. 이 후보 정보들을 가지고, 새롭게 $C_{t}$라는 놈을 업데이트 해준다. $C_{t}$는 이전 상태의 $C_{t-1}$ 이랑 지금 상태에서 계산된 $C_{t}$에 대한 후보정보를 조합해서 새로운 C 를 만들어 낸다. 새로운 C 를 만들때 forget gate와 input gate가 개입을 하게 된다. forget gate가 해주는 것은 t-1시점에서 C벡터가 갖는 정보중에 남길거 혹은 지울것을 결정하는 것이다. 이 결정을 위한 연산은 *(element-wise)로 곱을 수행한다. $f_{t}$가 0에 가까우면 지워지고 1에 가까우면 남아있게 된다. 그리고 새롭게 계산한 후보군인 $\tilde{C}$를 이용해서 똑같이 input gate와 *곱을 해준다. 전달해줄것은 1에 가까울 것이고 지울것은 0에 가까울 것이다. 그러면 새로운 $C_{t}$ 가 만들어 지게 된다. 그러면 최종적으로 $h_{t}$ 를 업데이트한다. 그때는 $C_{t}$에 tanh 를 해주고 output gate를 곱해주면된다. output gate 역시 남길건 1에 가깝고 반대는 0에 가까울 것이다.  

 LSTM은 3개의 gate가 있다는 것이고, 이 gate의 정보를 조합해서 최종적으로 $h_{t}$와 $C_{t}$를 업데이트하는 operation으로 구성되어 있다. RNN에서는 $h_{t}$ 의 역할로 두개로 분리했다고 보면 된다. ! 

 

 

 

 


Gated Recurrent Unit(GRU)

LSTM 보다 훨씬 간단하다. 비슷한 효과를 내면서 간단하게 표현할 수 없을까 하는 접근에서 등장했다. 기존 RNN보다 LSTM은 학습해야하는 parameter가 대략 4배가 증가한다고 한다. 학습할 때 BP를 생각해보면 gradient를 구할 때 기존에는 히든스테이트 정보를 타고 내려갔는데 새로운 통로에 대해서도 gradient를 구해야할 것이고, 곱절로 계산량이 많아지기 때문인 것으로 보인다. 

 여기서는 수정을 했는지 보자. LSTM 보다 gate를 줄였다. 

 

 $z_{t}$라는 update gate, 그리고 $r_{t}$라는 reset gate만 남겼다. 우리의 재료는 역시나 h랑 x이다. update gate, reset gate에서 각각 weighted sum을 하고 bias를 더하고, sigmoid를 씌워서 히든 스테이트를 업데이트를 하러 간다. 히든 스테이를 일단 후보군으로 놓는다 그게 $\tilde{h}_{t}$이다. 이 t-1시점에서의 h, 그리고 t 시점에서의 x 이 둘을 weighted sum하는 건 동일한데 여기에 reset gate $r_{t}$가 개입한다. reset gate의 역할은 t-1시점에서의 정보 중에서 reset할 애들을 결정하는 것이다. 0에 가까운 정보는 사라지고, 보존해야할 정보는 1에 가깝게 만들어준다. 그다음 tanh 을 취해서 값을 만들어 냈다면, 최종적으로 이 후보 정보를 final state 정보에 업데이트 할건지를 update gate에서 결정을 하게 된다. 이전 시점에서의 히든 스테이트 정보와 방금 생성한 후보 정보를 합쳐서 *곱해주고 두개의 상호정보를 합해주게 되어서 $h_{t}$가 생성되게 된다. LSTM에 비하면 구조가 확실히 간단해지긴 했다. 

 

 

- GRU랑 LSTM 중에 뭐가 더 좋을까? 그 때 그 때 다르다. LSTM의 parameter가 훨씬 많아서 오래걸리지만... 그런데 LSTM이 더 많이 활용이 된다. 모델을 좀 light하게 만들고 싶으면 GRU를 사용한다. 뭐 성능이 많이 하락이 되면은 LSTM쓰고 성능하락이 없으면 GRU를 쓰거나 한다.

 

 


 

* Bidirectional RNN

 

좀 더 복잡한 구조의 구조를 만들어보자!. 널리 쓰이는 RNN 구조인데, 이름과 같이 왼쪽부터 오른쪽으로 입력을 넣어가면서 인코딩했는데, 시퀀스라는 것이 꼭 왼쪽에서 오른쪽으로 가란 법이 없다. 즉 오른쪽에서 왼쪽으로 갔을 때 보완되는 정보도 얻을 수 있다는 뜻이다.! 오른쪽에서 왼쪽으로도 인코딩하자.!

그래서 두 path 에서 나오는 representation을 가지고 예측을 하거나 하자!

 

 매 스텝에서 아래의 그림에 실선이 나타내는 것은 오른쪽 방향이다. 점선은 왼쪽으로 가는 방향이다. 

Bidirectional RNN에서는 매 스텝마다 두개의 히든 히든스테이트 정보를 얻을 수 있다. 이 두개를 보통 concat을 한다. 그래서 그 시점에서의 예측을 수행할 수 있게 한다. 

 식으로 보면 concat 한 두 h 를 가지고 y 를 뽑는 것을 확인할 수 있다. 네모칸친 부분에 LSTM 적용하면 Bidirectrional LSTM이 되는 것이다. 갈아 끼우면 된다... 그렇게 되면 히든 스테이트 $h_{t}$를 구하는 부분만 달라지게 된다.

 

 


이어서 히든 레이어를 여러개 쌓고 싶다면, 레고 조립하는 것처럼 조립하면 된다.

입력으로 시퀀스 데이터들이 들어오게 되고, 히든스테이트 h1 이 각각 나오고, 그다음 h1은 또다른 sequential한 입력으로 들어가서 h2를 만드는데 기여하게 된다. 아래의 수식을 보면, 이전 시점의 h를 활용하는 것이 위의 기본 bidirectional RNN식과 차이점이라고 할 수 있겠다.

 

 

 

 

 

ref)

karpathy.github.io/2015/05/21/rnn-effectiveness/

 

The Unreasonable Effectiveness of Recurrent Neural Networks

There’s something magical about Recurrent Neural Networks (RNNs). I still remember when I trained my first recurrent network for Image Captioning. Within a few dozen minutes of training my first baby model (with rather arbitrarily-chosen hyperparameters)

karpathy.github.io

arunmallya.github.io/writeups/nn/backprop.html

 

Backprop

A Quick Introduction to Backpropagation Multiple Layers Consider a network made of multiple layers, connected to some error function \( E(\cdot) \) at the top, as shown in the figure below. Each layer takes in some input, transforms it in some fashion, and

arunmallya.github.io

 

728x90
반응형

댓글