[인공지능] AI - CS231n 10강 Recurrent Neural Networks


cs231n 10강 youtube 강의 링크
cs231n 공개 강의자료
cs231n 과제 링크


CS231n 10강 - Recurrent Neural Networks

Keywords

  • Vanilla Neural Network
  • Recurrent Neural Networks
  • Image captioning
  • Sentiment Classification
  • Machine Translation
  • Video Classification on frame level
  • Reccurent core cell / hidden state
  • Recurrence formula
  • Truncated Backpropagation through time
  • Image captioning
  • Attention
  • Visual Question Answering
  • Gradient clipping
  • Long Short Term Memory(LSTM)
  • Gradient flow

1. Recurrent Neural Network

RNN Process Sequences
앞선 강의에서 배운 NN의 architecture들은 모두 맨 왼쪽의 one-to-one 형식이다. 네트워크는 이미지 또는 벡터를 입력으로 받고 입력 하나가 hidden layer를 거쳐서 하나의 출력을 내보낸다. Classification 문제라면 출력은 카테고리가 될 것이다.

하지만 다른 문제들을 해결하려면 모델이 다양한 입력을 처리할 수 있어야 한다. 그렇기 때문에, 우린 다양한 입력 및 출력을 다룰 수 있는 RNN을 배워야 한다.

One-to-many의 경우 Image captioning에 쓰이는데 입력은 단일 입력이지만, 출력은 caption과 같은 가변 출력이다. Caption에 따라서 단어의 수가 달라지기 때문이다.

Many-to-one 모델은 반대로 입력이 가변 입력이다. Sentiment Classification을 예로 들 수 있는데, 문장이 긍정적인지 부정적인지 구별 한다.

Many-to-many는 Machine Translation이나 Video Classification을 예시로 들 수 있다. Machine translation의 경우 가변 입력인 문장이 주어지고 가변 출력인 번역 결과를 얻는다. 프레임 단위의 video classification 또한 가변 입력의 프레임이 주어지고 해당 비디오의 action 등을 분류한다.

결론적으로, RNN은 가변 길이의 데이터를 효과적으로 다루기 위한 일반적인 패러다임이다. 뿐만 아니라, 고정 길이의 데이터일지라도 sequential한 processing이 필요한 경우 유용하다.
ex) 이미지에서 숫자 인식, 이미지 생성

RNN의 사용처와 유용함에 대해 알아봤으니 이제 어떤 식으로 동작하는지 알아보자.
RNN
일반적으로 RNN은 위 그림의 초록색 네모처럼 Reccurent core cell을 가지고 있다. 내부에 hidden state를 가지고 있어서 새로운 입력 x를 받을 때마다 매번 상태가 업데이트되며 결과값 y를 출력한다. 이를 수식으로 표현해보자.

ht = fw(ht-1, xt)

h는 해당 시점의 상태값을 말하며, x는 매 time step 마다의 입력 벡터를 말한다. 결과적으로 새로운 상태값을 현재의 입력값과 과거의 상태값을 파라미터 W를 가진 함수 f에 넣어 정한다. 파라미터 W를 가진 함수 f는 매 time step마다 같은 것을 사용한다. 파라미터 W도 함수 f도 동일하다.

이제 출력을 얻어야 하는데, y를 얻으려면 ht을 입력으로 하는 FC-layer를 추가해야 한다. 활성화 함수, 출력값을 전부 수식으로 표현해보면 아래 그림과 같다.
RNN formula
Wxh는 x에서 RNN core cell로 들어오는 W이고 Whh는 hidden에서 오는 W이다. Why는 y로 넘어갈때의 W이다.

Q1. 왜 tanh를 사용했는가? sigmoid도 아니고 ReLU도 아니고!!

RNN의 경우 과거의 값을 지속적으로 가져와서 사용하기 때문이다. Sigmoid를 사용했다면, 0~1 사이의 값을 갖기 때문에 gradient vanishing problem이 있고, ReLU는 0보다 큰 값은 다 채택하기 때문에 explode의 위험이 있다. tanh는 -1~1사이의 값을 갖게하여 normalize하는 효과를 얻은 것이다.

이런 것이 아주 기본적인 RNN인 Vanilla RNN인데 도식화해보면 아래와 같다. 앞서 말했듯, 같은 W를 재사용한다. RNN CG
Many-to-many라면 h1부터 각 state를 만들때마다 새로운 input이 있으며 각 state마다 출력값이 있을 것이다. h0는 보통 0으로 초기화 또한 각 출력값마다 loss가 있을 것이다. 개별적으로 loss를 계산하며, 최종 loss는 각 loss들의 합이 될 것이다.

One-to-many라면 하나의 입력으로 state마다 출력값 y를 얻을 것이다. 이런 고정 입력은 모델의 initial hidden state를 초기화 시키는 용도로 사용한다.

Many-to-one과 One-to-many를 연결해서 각각 encoder와 decoder로 machine translation을 생각할 수 있다. Encoder에서 가변 입력을 받아 hidden state로 전체 sentence를 요약하고 이를 decoder로 가변 출력을 제공한다. 매 스텝 적절한 단어를 내뱉는 것이다.

Character-level language model을 예시로 살펴보자.
Character level language model
RNN은 문자열을 input으로 얻고, 현재의 문맥에서 다음 문자를 예측해야 한다. 캐릭터가 h,e,l,o만 있고 hello라는 문자열을 학습시키고 싶어한다. 즉 hello가 RNN의 xt이 되며, 입력은 한 글자씩이고 네트워크 또한 한 글자씩 출력한다. One-hot encoding을 통해 해당 글자 위치만 1로 표시하는 벡터로 표현한다.

yt은 h다음에 나올 문자를 예측한 값으로 e를 예측해야 정답인데 값을 보면 softmax 이용 o라고 예측하고 있다. e를 입력으로 넣으면 새로운 hidden state를 만들어내며 정답으로 l을 예측해야 하지만 -1.0으로 상대적으로 낮은 값을 갖기에 굉장히 loss가 크다. 이런 과정을 반복하면 학습이 되는 것이다.

Test time에는 어떻게 될까? 입력값이 주어지면 softmax를 통해 score(더 그럴듯 한 것, 확률)를 얻고, 이 스코어를 다음 글자 선택인 sampling에 이용한다.
Character level language model
여기서는 e가 13%밖에 안되는데도 e가 샘플링이 된 것을 볼 수 있다. 이런 식으로 확률 분포에 따라 문자열이 생성된다.

Q1. 가장 높은 스코어를 택하지 않고 확률 분포에 의거해서 채택하는 이유는?

가장 높은 스코어만을 택하면 결과값이 다채롭지 않을 것이고 위와 같은 예시에서도 정답인 hello가 출력될리 만무하다. 아무튼 다양성이 핵심!!

여하튼, 이런 모델의 경우 시퀀스 스텝마다 출력값이 존재하는데, 이런 출력값들의 loss를 계산해서 최종 loss를 구한다. 이를 Backpropagation through time이라고 한다.

Backward pass에서 전체 시퀀스를 가지고 loss를 계산해야 하는데, 시퀀스가 매우 길다면 문제가 될 수 있다. 학습이 매우 느릴 것인데, gradient를 계산하려면 전체 시퀀스를 다 거쳐야 하기 때문이다. 메모리 사용량도 시퀀스에 길이에 따라 매우 클 수 있다.

그렇기 때문에, 실제론 Truncated Backpropagation을 통해 Backprop을 근사시킨다. 시퀀스 일정 단위로 잘라서 약 100개 정도 그 자른 부분, 즉 서브 시퀀스에 대해서만 forward pass를 하고 loss를 계산한다. Gradient step을 진행하며 이를 반복한다. 이때, 다음 batch의 forward pass를 계산할 땐 이전 hidden state를 사용하며, gradient step은 현재 batch에서만 진행한다. SGD의 방식과 매우 유사하다. 시퀀스 데이터일뿐

이러한 RNN은 완벽하진 않지만 모양새가 셰익스피어처럼 글도 쓰고, 수학자처럼 증명도 하고 코드도 작성한다. 어떤 일반적인 structure를 학습한 것이다. 구조를 알려주지 않고 그저 시퀀스만 주어져도 저런 숨겨진 구조를 알아서 학습하게 되는 것이다. 이런게 딥러닝의 핵심인 것 같다..

이런 RNN이 어떤 방식으로 동작하는 지 눈으로 확인하고 싶어서 해석 가능한 의미 있는 벡터들을 찾고자 했다. 대부분의 hidden state는 아무 의미 없는 패턴이었지만, ““으로 패턴을 인식하고, 개행 문자 삽입 시점을 단어 개수로 세는 듯한 패턴을 확인할 수 있었다. RNN example
이 외에도, if문의 패턴, 주석문의 패턴, 들여쓰기의 패턴을 파악하는 cell들을 볼 수 있다.


2. Image Captioning

RNN-CNN
Image captioning은 일반적으로 CNN과 RNN을 연결해서 해결한다. CNN을 통해 이미지정보가 들어있는 vector를 출력하고 RNN의 입력이 되어서 Caption을 만들어 낸다. Test time에는 어떻게 동작할까?

Image Captioning example
입력 이미지를 받아서 CNN의 입력으로 넣는데, 마지막에 Softmax score를 사용하지 않고 FC layer의 4096 size의 벡터를 출력하고 이를 RNN의 입력으로 사용한다. 이전에는 두 개의 가중치인 Wxh와 Whh를 사용했는데, 여기에다 Wih로 이미지 정보에 대한 W도 추가해준다. 이후의 과정은 앞서 배운 RNN의 동작과 유사하고 라는 토큰이 단어 생성 종료 시점이다. 이 토큰 역시 train time에 모든 caption에 종료지점에 삽입해준다. ~~ 토큰 삽입 시점 또한 학습시켜서 생성을 멈추는 시점 또한 학습~~

이러한 모델은 supervised learning으로 학습시킨다. 학습 데이터로 자연어 caption이 달려있는 이미지가 있어야 한다. ex) Microsoft COCO dataset

하지만 이 모델은 train time에 보지 못한 data는 잘 처리하지 못할 수 밖에 없다. 뭔지 알려준 적이 있어야 알 수 있지!!

여기서 좀 더 진보된 모델이 Attention을 활용한 모델이다. 이 모델은 Caption을 생성할 때 이미지의 다양한 부분을 집중(Attention)해서 볼 수 있다.
Image Captioning with Attention
CNN으로 LxD의 공간 정보를 갖고 있는 벡터를 만들어내는데 Foward pass 시에 매 스텝 vocabulary에서 샘플링할 때 모델이 이미지에서 보고싶은 위치에 대한 분포를 만들어낸다. 이미지의 각 위치에 대한 분포는 train time에 모델이 어느 위치를 집중해서 봐야하는 지를 말한다.

첫 번째, h0는 이미지의 위치에 대한 분포를 계산한다. 이 분포가 a1이고 이를 다시 LxD와 연산해서 이미지 attention인 z1을 생성한다. 이 요약된 벡터 z1은 다음 입력이 되며, a2와 d1이 출력이 생성된다. d1은 vocabulary의 각 단어들의 분포이며 a2는 이미지 위치에 대한 분포이다. 이를 반복한다.

학습이 끝나면, 아래와 같이 모델이 caption을 생성하기 위해 이미지의 attention을 이동시키는 모습을 볼 수 있다.
Attention

cf) Soft attention: 모든 특징과 모든 이미지 위치 간의 weighted combination
Hard: attention: 모델이 각 타임 스텝마다 단 한 곳만 보도록 강제
Attention의 정확한 과정은 잘 모르겠다.. 자세히 공부해봐야지,,

RNN에 Attention을 더하면 Visual Question Answering(VQA) 문제도 풀 수 있다. 이미지에 대해 질문을 하고 답을 얻는 것이므로 입력은 이미지와 질문이다. Many-to-one으로 자연어 질문을 처리하고 RNN으로 질문을 요약한다. 또한 CNN으로 이미지를 요약하고 위 두 가지 벡터를 조합하여 정답을 추론한다.

지금 연구 주제인데,,,ㅜㅜ 너무 어렵다


4. Vanilla RNN Gradient Flow

지금까지는 hidden state가 하나 뿐인 단일 RNN layer를 살펴봤다. 하지만 실제로는 multi-layer RNN을 자주 보게 된다. 아래 그림은 3 layer RNN이다.
Multilayer RNN
RNN 하나를 돌리면 hidden state의 시퀀스가 만들어지고 이를 다른 RNN의 입력으로 넣어주면 다음 RNN layer의 시퀀스가 만들어진다. 일반적으로 2,3,4 layer의 RNN을 사용한다.

우린 어떻게 상태를 생성하고 출력을 하는 지 배웠지만 어떻게 이를 통해 Backpropagation을 하는 지는 다루지 않았다. Backward pass에서 Gradient를 계산하는 과정은 어떨까?

RNN gradient flow
우선 Backward pass할 때, ht에 대한 loss의 미분값을 얻는다. r그 다음 ht-1에 대한 미분값을 계산한다. 이는 위 그림의 빨간색 화살표의 경로와 동일하다. Gradient가 tanh gate를 지나서 matmul gate를 통과하는데, matmul gate의 backprop은 결국 Whh을 곱하게 된다.

결과적으로, 매번 RNN core cell을 통과할때마다, W의 transpose factor를 곱하게 되고, h0에 대한 gradient를 구하려면 모든 cell을 거쳐야 한다. 어림잡아도 계산이 엄~~~~~청날 것이고 이는 비효율적이다. 곱해지는 값(singular value)이 1보다 크면 점점 커져서 explode할 것이고 1보다 작으면 점점 작아져서 0이 될 것이다.

이를 해결하기 위해 gradient clipping이라는 기법을 사용하곤 한다. 이는 휴리스틱한 기법으로 gradient를 계산하고 L2 norm이 임계값보다 큰 경우 gradient가 최대 임계값을 넘지 못하도록 조정해준다. 좋은 방법은 아니지만 Exploding gradient를 해결하기 위해 이렇게 하곤 한다. 1보다 작아지는 Vanishing gradient는 어떻게 해결할 수 있을까?


5. Long Short Term Memory(LSTM)

앞서 살펴본 RNN에서 발생할 수 있는 vanishing gradient를 해결하려면 조금 더 복잡한 RNN architecture인 LSTM이 필요하다. 이는 exploding gradient와 vanishing gradient 문제를 완화하기 위해 만들어졌다. 앞 문단의 해결하기 충분치 않은 gradient clipping 같은 방식이 아닌 다른 방식이다.

LSTM
기존의 vanilla RNN과 LSTM을 비교해보자. 전자의 경우 한 cell 당 한 개의 hidden state가 있었던 반면, LSTM은 하나의 cell 당 두 개의 hidden state가 있다. 하나는 vanilla RNN에도 있던 ht이고, 하나는 cell state라고 말하는 ct이라는 벡터가 있다.

Cell state는 LSTM 내부에 존재하는 변수로 hidden state처럼 밖에 노출되지 않는다. LSTM의 업데이트 식을 살펴보자.
LSTM update
두 개의 입력 ht-1, xt이 주어진다. 그리고 i, f, o, g 총 4개의 gate를 계산한다. 이 4개의 gate들을 cell state(ct)를 업데이트 하는데 이용하고, cell state로 다음 스텝의 hidden state(ht)를 업데이트한다. 더욱 자세히 파헤쳐보자!!

Vanilla RNN의 경우 두 입력을 concat하고 행렬곱 연산으로 hidden state를 직접 구했지만 LSTM은 다르다. LSTM의 경우 이전 hidden state와 input을 받아서 쌓아놓고, 4개의 gates를 계산하기 위해 W를 곱해준다. 각 gate의 출력은 hidden state의 크기와 동일하다.

i는 input gate로 cell에서의 입력 xt에 대한 가중치이고, f는 forget gate로 이전 스텝의 cell 정보를 얼마나 까먹을 지에 대한 가중치이다. O는 output gate로 cell state(ct)를 얼마나 밖에 드러내 보일지에 대한 가중치이며, g는 gate gate(???)로 input cell을 얼마나 포함시킬지 결정하는 가중치이다.

I,f, o의 경우 sigmoid를 사용하고 g는 tanh를 사용한다. Sigmoid를 사용한다는 것은 gate의 값이 0에서 1 사이의 값을 갖는다는 것을 말하고, tanh를 사용한다는 것은 -1에서 1사이 값을 갖는다는 것을 말한다. 직관적으로 sigmoid는 쉽게 이해할 수 있는데, 0이면 값을 안쓰고 1이면 쓴다고 생각하면 된다. forget gate의 경우에선 0이면 값을 잊는 것이고, 1이면 사용하는 것이다. on/off의 개념, matmul이 아니라 element-wise mul이기 때문!! 결과적으로, cell state를 결정할 때, f와 이전 cell state의 element-wise 곱으로 값을 쓸지 말지를 결정한다. 여기에 i와 g의 element-wise 곱을 더해주는데, i는 sigmoid를 쓰므로 마찬가지로 0에서 1 사이의 값이 될 것이다.

cell state는 결국 f와 i에 의해 결정이 된다. 값의 결과가 0 또는 1로 on/off이기 때문 f에 의해 이전 cell state를 기억할 지 말지 결정하고, i*g에서 각 스텝마다 1까지 cell state의 각 요소를 증가시키거나 감소시킬 수 있다. ???~

IFOG
위는 수식을 도식화한 것이다. x와 h를 쌓으면, hx1의 행렬이 되고, 가중치 행렬 W는 4hx2h이다. 가중치 행렬은 행렬 4개를 합쳐 놓은 것이라고 보면 되는데, 각 4개의 행렬은 gate를 계산한다. 그러나 결론적으론, 두 개의 입력을 쌓고 행렬 곱 연산을 수행하는 것이다. 4개의 행렬이 곧 하나의 W란 말씀

이제 LSTM의 backward pass를 살펴보자!! 앞선 vanilla RNN의 gradient 문제가 왜 해결되었을까!! LSTM에서 cell state는 우선, addition operation의 backprob이 있다. Upstream gradient는 그저 두 갈래로 복사되기 때문에 element wise multiplication으로 직접 전달된다. 따라서, gradient는 upstream gradient와 forget gate f의 element wise 곱이기 때문에 우리가 구하고자 하는 cell state의 backprob은 upstream * forget gate가 된다.

위의 특성은 2개의 장점을 갖는다. 1) Forget gate와 곱해지는 연산이 element wise 곱이기 때문에 연산 면에서 매우 효율적이다. 또한 2) element wise 곱을 통해 매 스텝 다른 값의 forget gate와 곱해질 수 있다. vanilla RNN에선 동일한 가중치 행렬인 ht을 계속 곱했는데, 이는 exploding/vanishing gradient를 문제의 원인이었다. 하지만 forget gate가 계속 변하기 때문에 이 문제를 해결했다고 볼 수 있다. 완전히 해결한 것은 아님!! vanishing gradient는 생길 수 있음 또한, forget gate는 0~1사이의 값이기 때문에 반복적으로 곱했을 때, 좋은 수치적 특성을 보인다.

vanilla RNN backward pass에서 매 스텝 gradient가 tanh를 거쳐야 했는데, LSTM에서도 ht를 출력 yt을 계산하는 데 사용한다. LSTM의 최종 hidden state를 제일 처음 cell state까지 backprob하는 것을 보면, RNN처럼 매 스텝마다 tanh를 거치는 것이 아니라 단 한번만 거치면 된다. 왜?

이는 ResNet의 identity connection과 매우 닮았다. gradient flow를 위한 고속도로 같은 역할을 하기 때문이다.

6. Gated Recurrent Unit(GRU)

결국은 생김새도 결과도 다 비슷