[CS231n 10강] Recurrent Neural Networks

[CS231n 10강] Recurrent Neural Networks

Tags
AI
CS231n
Published
November 24, 2024
Author
JH
태그
종류
학문 분야

Recurrent Neural Networks

지금까지 배운 아키텍처들(Vanilla Neural Network)는 전부 one to one와 같은 모양을 하고 있었다.
하지만 Machine Learning의 관점에서 생각해 보면, 모델이 다양한 입력을 처리할 수 있도록 유연해질 필요가 있다.
그런 관점에서 RNN은 네트워크가 다양한 입/출력을 다룰 수 있는 여지를 제공해준다.
notion image
위 그림에서 각 사각형은 vector이며 화살표는 function을 의미한다.
빨간색 box는 input vector이며, 파란색 box는 output vector, 녹색 box는 RNN state이다.
notion image
일반적으로 RNN은 Recurrent Core Cell을 가지고 있다.
입력 가 RNN으로 들어가면, RNN은 새로운 state vector를 만들어내기 위해 fixed function을 사용하여 input vector와 output vector를 결합한다.
RNN 내부의 hidden state에서 새로운 입력을 받아들여 update 되는 방식이다.
이런 방식으로 RNN은 네트워크가 다양한 입출력을 다룰 수 있는 여지를 제공해 준다.
notion image
  1. Sequence vector 를 받는다.
  1. hidden state를 업데이트한다.
  1. 출력 값을 내보낸다.
위의 수식을 통해 RNN이 어떻게 동작하는지 알 수 있다.
함수 와 parameter 는 매 step에서 동일하다.

(Vanilla) RNN

notion image
Vanilla RNN을 수식적으로 표현하면 위와 같이 표현할 수 있다.
이전 hidden state와 현재 입력을 받아서 다음 hidden state를 출력하는데, 이를 수식적으로 가중치 행렬 와 입력 의 곱으로 나타낼 수 있다.
또한 가중치 행렬 도 있는데 이는 이전 hidden state와 곱해지는 값이다.
그리고 non-linearity를 구현하기 위해 를 적용한다.

RNN: Computational Graph

notion image
RNN을 보다 쉽게 이해하기 위해, multiple time steps를 unrolling 해서 보면 좋다. (위 사진처럼…)
첫 step에서는 initial hidden state인 가 있다. 대부분의 경우 는 0으로 초기화한다.
그리고 입력값 이 있는데, 은 함수 의 입력으로 들어간다.
이 과정을 반복하면서 가변 입력 를 받는다. 여기서 알아야 할 점은 동일한 가중치 행렬 가 매번 사용된다는 점이다.
앞선 강의에서 computational graph에서 동일한 node를 여러 번 사용할 때 backprop gradient의 흐름이 어떠한지를 배웠다.
backward pass 시 를 계산하려면 의 gradient를 전부 더해줬다.
따라서 이 RNN 모델의 backprop을 위한 행렬 의 gradient를 구하려면 각 step에서의 에 대한 gradient를 전부 계산한 뒤에 이 값들을 모두 더해주면 된다.

Many to Many

notion image
Computational graph에 도 넣어볼 수 있다. RNN의 출력값 가 또 다른 네트워크의 입력으로 들어가서 를 만들어낸다.
그래서 는 매 스텝의 class score라 할 수 있다. 각 스텝마다 개별적으로 에 대한 loss를 계산하고, RNN의 최종 loss는 각 개별 loss들의 합이 된다.
Loss flowing은 각 스텝에서 이루어진다. 이 경우에는 각 time step마다 가중치 에 대한 local gradient를 계산할 수 있다.
이렇게 개별로 계산된 local gradient를 최종 gradient에 더한다.

Many to One

notion image
감정 분석(sentiment analysis)에 쓰이는 many to one의 경우를 살펴보자.
이 경우에는 네트워크의 최종 hidden state에서만 결과 값이 나올 것이다. 최종 hidden state가 전체 시퀀스 내용에 대한 일종의 요약이기 때문이다.

One to Many

notion image
Fix sized input을 받지만, variable sized output인 network이다.
이 경우에는 대부분 고정 입력은 모델의 initial hidden state를 초기화 시키는 용도로 사용하고, RNN은 모든 step에서 output을 가진다.

Sequence to Sequence: Many-to-one + one-to-many

notion image
Machine translation에 쓰이는 Sequence to Sequence 모델에 대해 알아보자.
이 모델은 두 개의 스테이지로 구성되는데, encoder와 decoder 구조이다.
Encoder는 가변 입력을 받는다. 가령 영어로 된 문장이 될 수 있겠다.
그리고 encoder의 final hidden state를 통해 전체 sentence를 요약한다.
즉, encoder에서는 many to one을 수행하는데, 가변 입력을 하나의 벡터로 요약한다.
반면 decoder는 one to many를 수행하는데, 입력은 앞서 요약한 하나의 벡터이다.
Decoder는 가변 출력을 내뱉는데, 가령 다른 언어로 번역된 문장이 될 수 있다.
전체 computational graph를 풀어서 전체 학습 과정을 해석해보면, output sentence의 각 loss들을 합해서 backprob을 진행한다.

Natural Language Model

RNN은 Language modeling에서 자주 사용한다.
Language modeling 문제에서 하고 싶은 것은 바로 “어떻게 자연어(natural lang)를 만들어낼지”이다.
우선은 간단한 예제를 위해서 character level language model을 살펴보자.
notion image
네트워크는 문자열 시퀀스를 읽어들이고, 현재 문맥에서 다음 문자를 예측해야 한다.
이번 예제에서는 간단하게 단어가 [h, e, l, o]만 있다. 그리고 학습시킬 문장은 “hello”이다.
Train time에서는 training sequence(”hello”)의 각 단어들을 입력으로 넣어줘야 한다.
각 글자는 one-hot encoding을 사용해 하나의 vector로 표현할 수 있다. 예를 들면,11
  • [1, 0, 0, 0]
  • [0, 1, 0, 0]
  • [0, 0, 1, 0]
  • [0, 0, 0, 1]
notion image
Forward pass에서 네트워크의 동작은 첫 번째 step에서는 h가 들어오고, 첫 번째 RNN cell로는 ‘h’가 들어간다.
그러면 네트워크는 를 출력하는데, 어떤 문자가 ‘h’ 다음에 나올 것 같은지를 예측한 값이다.
다음 스텝에서는 두 번째 단어 ‘e’가 입력으로 들어간다. 이런 과정이 반복된다.
notion image
그렇다면 test time은 어떻게 될까?
이렇게 training하면 모델을 활용할 수 있는 방법들 중 하나는 model로부터 sampling하는 것이다.
다시 말해, train time에 모델이 봤을 법한 문장을 모델 스스로 생성해 내는 것이다.
Test time에서는 output layer의 score를 확률분포로 표현하기 위해서 softmax 함수를 사용할 수 있다.
그리고 문장의 두 번째 글자를 선택하기 위해서 이 확률분포를 이용한다.
notion image
확률 분포에서 ‘e’가 나왔고, 이를 다음 스텝의 네트워크 입력으로 넣어준다.
이렇게 학습된 모델만 가지고 새로운 문장을 만들어 내기 위해 이 과정을 반복한다.

Backpropagation through time

RNN 모델의 경우 sequence step 마다 출력값이 존재한다.
이 출력값들의 loss를 계산해 final loss를 얻는데 이를 backpropagation through time이라 한다.
notion image
Forward pass의 경우, 전체 sequence가 끝날 때까지 출력값이 생긴다.
반대로 backward pass에서도 전체 sequence를 가지고 loss를 계산해야 한다.
하지만 이 경우 시퀀스가 아주 긴 경우에는 문제가 될 소지가 있다.
가령 Wikipedia 전체 문서로 모델을 학습시킨다고 가정하면, 전체 문서에 대한 gradient를 계산하고 나면 gradient update가 1회 수행된 꼴이다.
이 과정은 아주 느릴 것이고, 메모리 사용량도 어마어마할 것이다.

Truncated Backpropagation

notion image
실제로는 truncated backpropagation이라는 backprob을 근사시키는 방법을 사용한다.
이 방법의 아이디어는 비록 입력 시퀀스가 엄청나게 길어서 무한대라고 할지라도, test time에 한 스텝을 일정 단위로 자른다. (ex ) 100)
100 step만 forward pass를 하고 이 sub-sequence의 loss를 계산한다. 그리고 gradient step을 진행한다.
다음 batch의 forward pass를 계산할 때에는 이전 hidden state를 이용한다. 그리고 gradient step은 현재 batch에서만 진행한다.

Multilayer RNNs

지금까지는 RNN 레이어를 단일로 사용했다. 하지만 더 자주 보게 될 모델들은 Multilayer RNN이다.
notion image
위 이미지에서는 3-Layer RNN이 있다. 입력이 첫 번째 RNN으로 들어가서 첫 번째 hidden state를 만들어 낸다.
이렇게 만들어진 hidden state 시퀀스를 다른 RNN의 입력으로 넣어줄 수 있다.
그러면 두 번째 RNN layer가 만들어내는 또 다른 hidden state 시퀀스가 생긴다.
이런 식으로 RNN layer를 쌓아올릴 수 있다. 이렇게 하는 이유는 모델이 깊어질수록 다양한 문제들에서 성능이 더 좋아지기 때문이다.
하지만 보통 엄청나게 깊은 RNN 모델을 사용하지는 않는다. 일반적으로 2~4 layer RNN이 적절하다.

Vanilla RNN Gradient Flow

notion image
RNN을 사용할 때의 문제점은 다음과 같다. 일단 Backward pass 시 에 대한 loss의 미분값을 얻는다.
그 다음 loss에 대한 의 미분값을 계산하게 된다. Backward pass의 전체 과정은 빨간색 통로를 따라가면 된다.
우선 그래디언트가 tanh gate를 타고 흘러가고, mat mul gate를 통과한다. Mat mul gate의 backprop은 결국 가중치 행렬의 transpose를 곱하게 된다.
이는 매번 vanilla RNN cells를 하나 통과할 때마다 가중치 행렬의 일부를 곱하게 된다는 것을 의미한다.
notion image
RNN의 특성 상 RNN이 여러 시퀀스의 cell들을 쌓아 올린다는 사실을 고려하면, 그래디언트가 RNN 모델의 layers sequence를 통해 어떤 방식으로 전달되는지 생각해볼 수 있다.
가령 우리가 에 대한 gradient를 구하고자 한다면, 결국에는 모든 RNN cells를 거쳐야 한다.
이는 cell 하나를 통과할 때마다 각 cell의 행렬 W transpose factors가 관여하고, 의 그래디언트를 계산하는 식을 써보면 아주 많은 가중치 행렬들이 개입하며 이는 좋지 않다.
우선 가중치를 행렬이라고 생각하지 말고 스칼라로 생각해보자. 수백 개의 스텝이 있는 경우라면 값들을 수백 번 곱해줘야 한다.
만약 곱해지는 값이 1보다 큰 경우라면 점점 값이 커질 것이고 1보다 작은 경우라면 점점 작아져서 0이 될 것이다.
notion image
그래서 사람들은 gradient clipping이라는 기법을 사용한다.
Gradient clipping은 그래디언트를 계산하고 그래디언트의 L2 norm이 임계값보다 큰 경우 그래디언트가 최대 임계값을 넘지 못하도록 조정한다.
이 방법은 그닥 좋은 방법은 아니지만 많이 사용한다.
하지만 반대로 vanishing gradients를 다루려면 더 복잡한 RNN 아키텍처가 필요하다. 이는 LSTM에 관한 것이다.

Long Short Term Memory (LSTM)

notion image
LSTM은 vanishing & exploding gradients 문제를 완화시키기 위해 디자인 되었다.
LSTM은 아주 재밌게 생겼다(?). Vanilla RNN은 hidden state가 있었고, 매 스텝마다 재귀적인 방법으로 hidden state를 업데이트 했다.
LSTM에는 한 cell 당 두 개의 hidden state가 있다.
  • 하나는 로 vanilla RNN에 있었던 hidden state와 유사한 개념이다.
  • 또 다른 하나는 (cell state)라고 하는 두 번째 벡터가 있다. 는 LSTM 내부에만 존재하며 밖에 노출되지 않는 변수이다.
LSTM도 두 개의 입력()을 받고, 4개의 gate(i, f, o, g)를 계산한다.
이 gate들을 를 업데이트 하는 데 이용한다. 그리고 로 다음 스텝의 hidden state를 업데이트 한다.
notion image
LSTM의 경우, 이전 hidden state를 입력 받아 쌓아두고, 네 개의 gate 값을 계산하기 위해 커다란 가중치 행렬을 곱해준다.
  • Input gate
  • Forget gate
  • Output gate
  • Gate gate
I는 input gate로 입력 에 대한 가중치이다. F는 forget gate로 이전 스텝의 cell의 정보를 얼마나 망각forget)할지에 대한 가중치이다.
O는 output gate로 cell state를 얼마나 밖에 드러내 보일지에 대한 가중치이다. G는 input cell을 얼마나 포함시킬지 결정하는 가중치이다.
중요한 것은 gate에서 사용하는 non-linearity가 각양각색이라는 점이다.
i, f, o는 sigmoid를 사용하므로 [0, 1] 사이의 값을 갖지만, g는 tanh를 사용하므로 [-1, 1] 사이의 값을 갖는다.

LSTM Gradient Flow

notion image
왼쪽을 보면 이전의 의 입력값이 있다. 현재 입력 도 있다. 우선 이 현재 입력 를 쌓는다. 그리고 여기서 가중치 행렬 4개를 곱해 gate를 만든다.
그리고 f는 이전 와 곱한다. 그리고 i, g가 element wise로 곱한 후, cell state와 더해서 다음 cell을 만든다.
c는 를 거친 후 o와 곱해져서 다음 hidden state를 만들어낸다.
이제 LSTM의 backward pass를 살펴보도록 하자. 앞서 vanilla RNN의 경우 가중치 행렬 W가 계속해서 곱해지는 문제가 있었다.
notion image
Cell state에서 내려오는 upstream gradient를 살펴보자. 우선 addition operation의 backprob이 있다.
그래디언트는 upstream gradient와 forget gate의 element wise 곱이다. 그래서 upstream gradient * forget gate가 된다
이 특성이 vanilla RNN에 비해 좋은 점이 두 가지가 있다.
  1. Forget gate와 곱해지는 연산이 matirx multiplication이 아닌 element-wise라는 점이다.
  1. Element wise mutliplication을 통해 매 스텝 다른 값의 forget gate와 곱해질 수 있다.
앞서 vanilla RNN의 경우에는 동일한 가중치 행렬 W만을 계속 곱했다. 이는 exploding/vanishing gradient 문제를 일으켰었는데, LSTM에서는 f가 스텝마다 계속 변한다.
그리고 f는 sigmoid에서 나온 값이므로 element wise multiply가 0~1 사이의 값이다.
따라서 f를 반복적으로 곱한다고 했을 때, 더 좋은 수치적 특성을 보일 수 있다.
또 한가지 명심해야 할 점은 vanilla RNN의 backward pass에서는 매 스텝에서 그래디언트가 tanh를 거쳤다는 점이다.
만약 LSTM의 최종 hidden state 를 가장 첫 cell state()까지 backprop하는 걸 생각해보면, 단 한 번만 tanh를 거치면 된다.

참고