5 minute read

RNN의 불합리한 유효성

2015년 5월, 안드레이 카파시는 “The Unreasonable Effectiveness of Recurrent Neural Networks” 라는 제목의 블로그 글을 작성했다. 이 글의 내용은 “Character 단위로 다음에 올 글자 하나만 예측하도록 단순하게 학습시켰을 뿐인데, RNN이 문법, 문맥, 문서 구조(HTML, LaTeX)까지 놀라울 정도로 지켜서 글을 써내려가더라”이다.

2015년 당시의 분위기

당시 RNN에 대한 설명들은 복잡한 수식이 많아 진입 장벽이 높았다. 이때 안드레 카파시가 수학 공식 없이 RNN을 이해하기 쉽게 설명과 코드, 실험 결과들을 블로그에 공개하였다.

카파시가 했던 실험은 Character level 언어 모델이다. 즉, 단어나 문장의 의미를 알려주지 않고 오직 문장 속 글자 하나하나를 순서대로 읽게 한 뒤에 다음 Character를 예측하도록 모델을 학습시킨 것이다.

카파시는 이 모델에 폴 그레이엄이라는 인물의 에세이, 셰익스피어의 작품, 위키백과 문서, LaTeX 수학 수식 코드, 리눅스 소스코드 등의 텍스트를 학습시켰다.

“불합리할 정도로 유효하다”

이때 카파시는 블로그 글에 RNN은 불합리할 정도로 유효하다 라고 하였다. 왜냐하면 단순히 글자 순서만 학습했을 뿐인데, RNN은 철자를 맞추는 것을 넘어서 문맥과 형식을 정말 잘 시늉했기 때문이다.

실험 결과를 예로 들면

  1. 세익스피어 대본을 학습 시키니 인물 이름과 콜론, 줄바꿈 등의 대본 형식을 그대로 맞춰가며 대사를 생성했다.
  2. 위키백과와 LaTeX를 학습 시키니 위키백과의 마크다운 문법과 주석 틀을 잘 따랐고, 수학적 기호와 구문을 LaTeX 문법에 잘 맞게 만들었다.
  3. 리눅스 코드를 학습 시키니 C언어 코드의 들여쓰기, 변수 선언, 괄호 짝 맞추기 등 구조를 학습해서 코드를 잘 생성하였다.

물론 이때 당시에는 생성물을 자세히 보면 내용적으로는 앞뒤가 맞지 않고, 할루시네이션이 많았다. 하지만 Character 단위 수준에서 시작해서 문법 구조와 문맥을 스스로 파악하고 흉내 낸다는 것 자체가 당시에는 매우 놀랄만했다.

RNN 구조 특징

위 실험 결과를 낼 수 있었던 이유는 RNN의 구조에 있다.

일반적인 신경망(e.g. CNN)은 고정된 크기의 데이터(e.g. 이미지)를 받아서 일회성으로 결과를 내놓지만, RNN은 시간의 흐름이나 순서가 있는 시퀀스 데이터(e.g. 텍스트, 음성)을 다룬다.

그리고 RNN은 Hidden State라는 기억장치가 있다. RNN은 새로운 Character(입력값)을 처리할 때, 이전 단계까지 처리했던 정보를 압축한 Hidden State를 함께 전달받아 업데이트한다.

예를들어 “hello”라는 단어를 만들 때, 이전까지 들어온 글자가 “hell”이라는 기억(hidden state)을 가지고 있기 때문에 다음에 올 글자가 “o”라는 것을 예측할 수 있게 된다. 즉 문맥을 유지할 수 있다는 것이다.

이런 RNN의 앞단의 정보를 연속적으로 전달하는 구조 때문에 과거의 통계적 모델(e.g. n-gram)의 고정된 몇 개 단어만 보는 한계를 넘어서서 앞단의 맥락을 길게 이어가며 문잘을 완성할 수 있었다.

카파시 블로그 글에 대한 논쟁

안드레 카파시의 본 블로그 글은 개발자 커뮤니티에서 반응이 폭발적이었고, 당연히 비판도 있었다.

대표적으로 요아브 골드버그라는 연구원은 단순한 통계 모델(n-gram)에 대량의 데이터를 넣어도 겉보기에는 RNN과 비슷하게 그럴듯한 텍스트를 만들어 낸다라고 주장하며, RNN이 대단한 지능을 가진 것처럼 오해해서는 안 된다고 지적했다.

하지만 이후 토마스 미콜로프 등의 연구 결과에서는 데이터가 많은 상황에서 RNN은 기존 통계 모델이 넘지 못하는 정밀도와 성능한계를 명확하게 넘어서는 우수한 언어 표현 능력을 증명하기도 했다.

RNN 이해하기

RNN은 Recurrent Neural Network 의 약자로, 시퀀스 데이터(문장, 음성, 시계열 데이터)를 다룰 때 현재 입력뿐만 아니라 이전 시점의 정보까지 함께 반영하도록 설계된 신경망 구조이다. 일반적인 신경망과 달리 내부에 일종의 기억장치 역할을 하는 hidden state를 가지고 있어서 과거의 관측과 문맥을 시간에 따라 지속적으로 전달할 수 있다.

RNN 동작 방식

다시 설명하면 RNN은 시퀀스 데이터를 한 번에 다루지 않고, 순서대로 하나씩 처리하는 신경망이다. 일반적인 신경망은 입력 데이터를 넣으면 출력을 내보내고 끝낸다. 하지만 RNN은 매 순간 계산을 수행할 때 자기 자신에게 돌아오는 내부 저장소 즉, hidden state를 활용한다.

  • 이전 단계에서 만든 정보($h_{t-1}$)를 버리지 않고 남겨둔다.
  • 새로운 단계의 입력($x_t$)이 들어오면, 남겨둔 이전 정보($h_{t-1}$)와 결합한다.
  • 결합한 결과를 바탕으로 새로운 현재 정보($h_t$)를 만들어낸다.

RNN 계산 순서

1단계: hidden state update

즉, 과거 정보와 현재 입력을 섞어서 새 기억 만들기 단계이다.

\[h_t = \tanh(W_x x_t + W_h h_{t-1} + b)\]
  • $x_t$: 현재 들어온 입력값
  • $h_{t-1}$: 직전 단계까지 쌓인 기억값
  • $W_x$: 현재 입력값에 적용하는 가중치
  • $W_h$: 이전 기억값에 적용하는 가중치
  • $b$: 편향(기본 보정값)
  • $\tanh$: 덧셈 결과를 -1에서 1 사이의 범위로 일정하게 줄여주는 함수

이 연산을 거치면 현재 입력 $x_t$와 이전 기억 $h_{t-1}$이 합쳐져서 새로운 은닉 상태 $h_t$가 완성된다.

2단계: 출력 계산

말 그대로 완성된 새 기억으로 결과 출력하는 단계이다.

\[y_t = W_y h_t + b_y\]
  • $h_t$: 1단계에서 완성한 새로운 기억값입니다.
  • $W_y$: 출력 형태에 맞추기 위해 곱해주는 가중치입니다.
  • $b_y$: 출력용 편향입니다.
  • $y_t$: 최종적으로 나오는 예측 결과값입니다.

설명:

쉽게 예를 들어 입력이 3개가 있다고 가정한다.

x1​, x2​, x3​

RNN은 이것을 한꺼번에 처리하지 않고 다음 순서로 처리한다.

x1​ -> x2 -> x3​

각 시점에서 hidden state가 다음 시점으로 전달된다.

h0​ -> h1​ -> h2​ -> h3​

크게 이 단계를 따른다.

다시 처음으로 돌아가서

처음에는 이전 hidden state가 없으므로 h0은 0으로 설정한다. 이후 첫 번째 입력 x1이 들어오면 \(h_1 = \tanh(W_x x_1 + W_h h_{0} + b)\) 를 계산한다. 즉 첫 번째 시점에서 현재 입력 x1을 이용해서 첫 번째 hidden state h1을 만든다. 이게 1단계 hidden state update이다.

이후 만든 h1을 이용해서 출력을 계산한다. \(y_1 = W_y h_1 + b_y\)

이후 두 번째 입력 x2가 들어온다. 그러면 첫 번째 단계에서 만든 h1을 버리지 않고 사용하여 \(h_2 = \tanh(W_x x_2 + W_h h_1 + b)\) 을 계산한다. 이후 만들어진 h2를 가지고 \(y_2 = W_y h_2 + b_y\) 를 만든다.

이렇게 계속 반복된다.

Hidden State

RNN에서 말하는 기억은 실제로 무언가를 별도로 저장해두는 저장장치가 있다는 의미가 아니다. hidden state 즉, h_t 자체가 이전 입력들을 반영해서 계산된 벡터를 의미하는 것이다.

h1​이 f(x1​)이라면, h2​는 f(x2​,h1​)이고, h3​는 f(x3​,h2​)이 되므로 h3에는 x3, x2, x1에 대한 정보가 반영된 상태라고 이해하면 된다.

이때 중요한 것

W_x 와 W_h는 현재 입력과 과거 정보를 각각 얼마나, 어떤 방식으로 반영할지를 학습하는 가중치라는 사실이다.

\[h_t = \tanh(W_x x_t + W_h h_{t-1} + b)\]

다시 RNN 식을 보면 두 종류의 정보가 들어온다. 현재 정보와 과거 정보이다. 이때 단순히 두 정보를 더하는게 아닌 각각 서로 다른 가중치를 적용한다는 점이다.

\[\underbrace{W_xx_t}_{\text{현재 정보}} + \underbrace{W_hh_{t-1}}_{\text{과거 정보}}\]

이때 이 가중치(W_x​,W_h)가 모든 시점에서 공유되는데 \(h_1 = \tanh(W_xx_1 + W_hh_0+b)\)

\[h_2 = \tanh(W_xx_2 + W_hh_1+b)\]

\(h_3 = \tanh(W_xx_3 + W_hh_2+b)\) 그 이유는 시퀀스의 길이가 데이터마다 다를 수 있기 때문이다.

예를 들어 문장이 “나는 밥을 먹었다.” 처럼 4개의 단어로 구성될 수도 있고, “나는 오늘 친구와 함께 맛있는 밥을 먹었다.” 처럼 더 길 수 있다. 만약 시점마다 서로 다른 가중치($W_{x1}, W_{x2}, W_{x3} \dots$)를 사용하도록 모델을 만들면, 4개의 단어로 구성된 문장을 처리할 때 사용한 가중치로는 그보다 길거나 짧은 문장을 처리할 수 없게 된다. 시점별로 전용 가중치가 따로 존재해야 하므로, 모델을 설계할 때 최대 시퀀스 길이를 지정해 두어야 하고, 그 길이를 벗어나는 새로운 입력 데이터는 아예 계산조차 불가능해지는 문제가 발생한다.

하지만 가중치($W_x, W_h$)를 하나로 고정해서 모든 시점에서 공유하면 입력이 4개 단어든 7개 단어든 상관없이 매 시점마다 현재 입력데이터와 이전 hidden state를 섞는 공식을 반복해서 재사용하기만 하면 된다.

RNN의 문제점

RNN에서 모든 시점마다 동일한 가중치($W_h, W_x$)를 반복해서 곱하며 hidden state를 업데이트하는 구조는 Long-Term Dependency 문제를 만들어낸다.

Long-Term Dependency 문제

Long-Term Dependency 문제는 시퀀스의 길이가 길어질수록, 처음에 입력되었던 정보가 뒤로 갈수록 희미해지거나 왜곡되어 나중 시점의 예측에 영향을 미치지 못하는 현상을 의미한다.

\[h_t = \tanh(W_x x_t + W_h h_{t-1} + b)\]

위 RNN의 학습 과정에서 모델은 가중치를 최적화하기 위해 미래 시점의 오차로부터 과거 시점으로 기울기(Gradient)를 되돌려 전파하는 시간 역전파(Backpropagation Through Time, BPTT)를 수행한다. 이때 시간 축을 거슬러 올라갈수록 동일한 hidden state 가중치 W_h가 시점의 길이(T)만큼 계속해서 반복적으로 곱해지게 된다.

여기서 가중치 W_h의 크기에 따라 두가지 문제가 발생한다. 바로 Vanishing Gradient와 Exploding Gradient 이다.

가중치 $W_h$의 값이 1보다 작은 수치이거나, $\tanh$ 활성화 함수의 미분값(0~1 사이)이 지속적으로 곱해지면, 시점 $t$가 멀어질수록 기울기가 0에 가깝게 계속 작아지면서 결국 초반 시점($h_1, h_2$)의 가중치에는 기울기가 전달되지 않아 학습이 이루어지지 않는것을 Vanishing Gradient,

반대로 가중치 $W_h$의 값이 1보다 크면, 시점 축을 거슬러 올라가면서 값이 거듭제곱되어 무한히 커져서 기울기가 폭발하여 모델의 가중치 수치가 정상적으로 업데이트 되지 못하고 학습이 불안해지거나 완전히 발산하게 되는 Exploding Gradient 이다.

결국 RNN의 연산 구조는 시퀀스가 조금만 길어져도 초기 단계의 정보 X_1 을 끝까지 유지하지 못하고 잊어버리게 된다.

Leave a comment