for textmining

RNN과 LSTM을 이해해보자!

|

이번 포스팅에서는 Recurrent Neural Networks(RNN)과 RNN의 일종인 Long Short-Term Memory models(LSTM)에 대해 알아보도록 하겠습니다. 우선 두 알고리즘의 개요를 간략히 언급한 뒤 foward, backward compute pass를 천천히 뜯어보도록 할게요.

이번 포스팅은 기본적으로 미국 스탠포드대학의 CS231n 강좌를 참고하되 forward, backward pass 관련 설명과 그림은 제가 직접 만들었음을 밝힙니다. GRU(Gated Recurrent Unit)가 궁금하신 분은 이곳을 참고하시면 좋을 것 같습니다. 자, 그럼 시작하겠습니다!

RNN의 기본 구조

RNN은 히든 노드가 방향을 가진 엣지로 연결돼 순환구조를 이루는(directed cycle) 인공신경망의 한 종류입니다. 음성, 문자 등 순차적으로 등장하는 데이터 처리에 적합한 모델로 알려져 있는데요. Convolutional Neural Networks(CNN)과 더불어 최근 들어 각광 받고 있는 알고리즘입니다.

위의 그림에서도 알 수 있듯 시퀀스 길이에 관계없이 인풋과 아웃풋을 받아들일 수 있는 네트워크 구조이기 때문에 필요에 따라 다양하고 유연하게 구조를 만들 수 있다는 점이 RNN의 가장 큰 장점입니다.

RNN의 기본 구조는 위 그림과 같습니다. 녹색 박스는 히든 state를 의미합니다. 빨간 박스는 인풋 $x$, 파란 박스는 아웃풋 $y$입니다. 현재 상태의 히든 state $h_t$는 직전 시점의 히든 state $h_{t-1}$를 받아 갱신됩니다.

현재 상태의 아웃풋 $y_t$는 $h_t$를 전달받아 갱신되는 구조입니다. 수식에서도 알 수 있듯 히든 state의 활성함수(activation function)비선형 함수하이퍼볼릭탄젠트(tanh)입니다.

그런데 활성함수로 왜 비선형 함수를 쓰는걸까요? 밑바닥부터 시작하는 딥러닝의 글귀를 하나 인용해 보겠습니다.

선형 함수인 $h(x) = cx$를 활성 함수로 사용한 3층 네트워크를 떠올려 보세요. 이를 식으로 나타내면 $y(x) = h(h(h(x)))$가 됩니다. 이 계산은 $y(x) = c * c * c * x$처럼 세번의 곱셈을 수행하지만 실은 $y(x) = ax$와 똑같은 식입니다. $a = c^3$이라고만 하면 끝이죠. 즉 히든레이어가 없는 네트워크로 표현할 수 있습니다. 그래서 층을 쌓는 혜택을 얻고 싶다면 활성함수로는 반드시 비선형함수를 사용해야 합니다.

RNN의 기본 구조

RNN의 기본 동작을 직관적으로 이해해 보기 위해 CS231n 강좌의 Karpathy갓파시가 든 예제를 가져와 봤습니다. 어떤 글자가 주어졌을 때 바로 다음 글자를 예측하는 character-level-model을 만든다고 칩시다. 예컨대 RNN 모델에 ‘hell’을 넣으면 ‘o’를 반환하게 해 결과적으로는 ‘hello’를 출력하게 만들고 싶은 겁니다.

우선 우리가 가진 학습데이터의 글자는 ‘h’, ‘e’, ‘l’, ‘o’ 네 개뿐입니다. 이를 one-hot-vector로 바꾸면 각각 $[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]$이 됩니다.

$x_1$은 $[1,0,0,0]$입니다. 이를 기반으로 $h_1$인 $[0.3, -0.1, 0.9]$를 만들었습니다($h_0$는 존재하지 않기 때문에 랜덤 값을 집어넣습니다). 이를 바탕으로 $y_1$인 $[1.0, 2.2, -3.0, 4.1]$로 생성했습니다. 마찬가지로 두번째, 세번째, 네번째 단계들도 모두 갱신하게 됩니다. 이 과정을 순전파(foward propagation)라고 부릅니다.

다른 인공신경망과 마찬가지로 RNN도 정답을 필요로 합니다. 모델에 정답을 알려줘야 모델이 parameter를 적절히 갱신해 나가겠죠. 이 경우엔 바로 다음 글자가 정답이 되겠네요. 예컨대 ‘h’의 다음 정답은 ‘e’, ‘e’ 다음은 ‘l’, ‘l’ 다음은 ‘l’, ‘l’ 다음은 ‘o’가 정답입니다.

위의 그림을 기준으로 설명을 드리면 첫번째 정답인 ‘e’는 두번째 요소만 1이고 나머지가 0인 one-hot-vector입니다. 그림을 보면 아웃풋에 진한 녹색으로 표시된 숫자들이 있는데 정답에 해당하는 인덱스를 의미합니다. 이 정보를 바탕으로 역전파(backpropagation)를 수행해 parameter값들을 갱신해 나갑니다.

그렇다면 RNN이 학습하는 parameter는 무엇일까요? 인풋 $x$를 히든레이어 $h$로 보내는 $W_{xh}$, 이전 히든레이어 $h$에서 다음 히든레이어 $h$로 보내는 $W_{hh}$, 히든레이어 $h$에서 아웃풋 $y$로 보내는 $W_{hy}$가 바로 parameter입니다. 그리고 모든 시점의 state에서 이 parameter는 동일하게 적용됩니다(shared weights).

RNN의 순전파

앞장에서 말씀드린 RNN의 기본 구조를 토대로 forward compute pass를 아래와 같이 그려봤습니다. 위에서 설명한 수식을 그래프로 옮겨놓은 것일 뿐입니다.

RNN의 역전파

자, 이제 backward pass를 볼까요? 아래 그림과 같습니다. 혹시 역전파가 생소하신 분은 이곳을 참고하시기 바랍니다.

위 움짤과 아래 그림은 같은 내용입니다. 우선 forward pass를 따라 최종 출력되는 결과는 $y_t$입니다. 최종 Loss에 대한 $y_t$의 그래디언트($dL/dy_t$)가 RNN의 역전파 연산에서 가장 먼저 등장합니다. 이를 편의상 $dy_t$라고 표기했고, 순전파 결과 $y_t$와 대비해 붉은색으로 표시했습니다. 앞으로 이 표기를 따를 예정입니다.

$dy_t$는 덧셈 그래프를 타고 양방향에 모두 그대로 분배가 됩니다. $dW_{hy}$는 흘러들어온 그래디언트 $dy_t$에 로컬 그래디언트 $h_t$를 곱해 구합니다. $dh_t$는 흘러들어온 그래디언트 $dy_t$에 $W_{hy}$를 곱한 값입니다. $dh_{raw}$는 흘러들어온 그래디언트인 $dh_t$에 로컬 그래디언트인 $1-tanh^2(h_{raw})$을 곱해 구합니다. 나머지도 동일한 방식으로 구합니다.

다만 아래 그림에 주의할 필요가 있습니다. RNN은 히든 노드가 순환 구조를 띄는 신경망입니다. 즉 $h_t$를 만들 때 $h_{t-1}$가 반영됩니다. 바꿔 말하면 아래 그림의 $dh_{t-1}$은 t-1 시점의 Loss에서 흘러들어온 그래디언트인 $W_{hy}*dy_{t-1}$뿐 아니라 ★에 해당하는 그래디언트 또한 더해져 동시에 반영된다는 뜻입니다.

LSTM의 기본 구조

RNN은 관련 정보와 그 정보를 사용하는 지점 사이 거리가 멀 경우 역전파시 그래디언트가 점차 줄어 학습능력이 크게 저하되는 것으로 알려져 있습니다. 이를 vanishing gradient problem이라고 합니다.

이 문제를 극복하기 위해서 고안된 것이 바로 LSTM입니다. LSTM은 RNN의 히든 state에 cell-state를 추가한 구조입니다. LSTM을 가장 쉽게 시각화한 포스트를 기본으로 해서 설명을 이어나가겠습니다.

cell state는 일종의 컨베이어 벨트 역할을 합니다. 덕분에 state가 꽤 오래 경과하더라도 그래디언트가 비교적 전파가 잘 되게 됩니다. LSTM 셀의 수식은 아래와 같습니다. ⊙는 요소별 곱셈을 뜻하는 Hadamard product 연산자입니다.

\[\begin{align*} { f }_{ t }&=\sigma ({ W }_{ xh\_ f }{ x }_{ t }+{ W }_{ hh\_ f }{ h }_{ t-1 }+{ b }_{ h\_ f })\\ { i }_{ t }&=\sigma ({ W }_{ xh\_ i }{ x }_{ t }+{ W }_{ hh\_ i }{ h }_{ t-1 }+{ b }_{ h\_ i })\\ { o }_{ t }&=\sigma ({ W }_{ xh\_ o }{ x }_{ t }+{ W }_{ hh\_ o }{ h }_{ t-1 }+{ b }_{ h\_ o })\\ { g }_{ t }&=\tanh { ({ W }_{ xh\_ g }{ x }_{ t }+{ W }_{ hh\_ g }{ h }_{ t-1 }+{ b }_{ h\_ g }) } \\ { c }_{ t }&={ f }_{ t }\odot { c }_{ t-1 }+{ i }_{ t }\odot { g }_{ t }\\ { h }_{ t }&={ o }_{ t }\odot \tanh { ({ c }_{ t }) } \end{align*}\]

forget gate $f_t$는 ‘과거 정보를 잊기’를 위한 게이트입니다. $h_{t-1}$과 $x_t$를 받아 시그모이드를 취해준 값이 바로 forget gate가 내보내는 값이 됩니다. 시그모이드 함수의 출력 범위는 0에서 1 사이이기 때문에 그 값이 0이라면 이전 상태의 정보는 잊고, 1이라면 이전 상태의 정보를 온전히 기억하게 됩니다.

input gate $i_t⊙g_t$는 ‘현재 정보를 기억하기’ 위한 게이트입니다. $h_{t-1}$과 $x_t$를 받아 시그모이드를 취하고, 또 같은 입력으로 하이퍼볼릭탄젠트를 취해준 다음 Hadamard product 연산을 한 값이 바로 input gate가 내보내는 값이 됩니다. 개인적으로 $i_t$의 범위는 0~1, $g_t$의 범위는 -1~1이기 때문에 각각 강도와 방향을 나타낸다고 이해했습니다.

LSTM의 순전파

LSTM 순전파는 아래와 같습니다.

여기서 주목해야 할 점은 $H_t$입니다. 이 행렬을 행 기준으로 4등분해 $i, f, o, g$ 각각에 해당하는 활성함수를 적용하는 방식으로 $i, f, o, g$를 계산합니다. (물론 이렇게 계산하지 않고 다른 방식을 써도 관계는 없습니다) 이를 그림으로 나타내면 다음과 같습니다.

LSTM의 역전파

그럼 이제 LSTM의 역전파를 알아볼까요? 아래 움짤과 같습니다.

이제부터 나열한 그림은 위 움짤과 내용이 같습니다. 우선 $df_t, di_t, dg_t, do_t$를 구하기까지 backward pass는 RNN과 유사합니다.

$dH_t$를 구하는 과정이 LSTM backward pass 핵심이라고 할 수 있죠. $H_t$는 $i_t, f_t, o_t, g_t$로 구성된 행렬입니다. 바꿔 말하면 각각에 해당하는 그래디언트를 이를 합치면(merge) $dH_t$를 만들 수 있다는 뜻입니다. $i, f, o$의 활성함수는 시그모이드이고, $g$만 하이퍼볼릭탄젠트입니다. 각각의 활성함수에 대한 로컬 그래디언트를 구해 흘러들어온 그래디언트를 곱해주면 됩니다.

순전파 과정에서 $H_t$를 4등분해 $i_t, f_t, o_t, g_t$를 구했던 것처럼, backward pass에서는 $d_i, d_f, d_o, d_g$를 다시 합쳐 $dH_t$를 만듭니다. 이렇게 구한 $dH_t$는 다시 RNN과 같은 방식으로 역전파가 되는 구조입니다.

LSTM은 cell state와 히든 state가 재귀적으로 구해지는 네트워크입니다. 따라서 cell state의 그래디언트와 히든 state의 그래디언트는 직전 시점의 그래디언트 값에 영향을 받습니다. 이는 RNN과 마찬가지입니다. 이를 역전파시 반영해야 합니다.

파이썬 구현

CS231n 강좌에 파이썬 numpy 패키지만 활용해 구현해놓은 RNN 코드가 있습니다. 여기서 손실함수만 바꾸면 비교적 쉽게 LSTM 구조로 변경할 수가 있는데요. 제가 인터넷에 떠다니는 여러 자료를 보면서 글자나 단어 단위로 학습하기 위한 LSTM 손실 함수를 만들어봤습니다. 위 설명과 notation이 약간 다르긴 한데, 본질적으로는 완전히 같은 코드입니다.

파일럿 실험

이광수 장편소설 ‘무정’에 위 코드를 실험해봤습니다. ‘무정’은 32만 어절로 이뤄진 작품입니다. 1917년 작품이며 한자어와 대화체 문장이 많습니다. 텍스트는 이렇게 생겼습니다.

형식은, 아뿔싸! 내가 어찌하여 이러한 생각을 하는가, 내 마음이 이렇게 약하던가 하면서 두 주먹을 불끈 쥐고 전신에 힘을 주어 이러한 약한 생각을 떼어 버리려 하나, 가슴속에는 이상하게 불길이 확확 일어난다. 이때에, “미스터 리, 어디로가는가” 하는 소리에 깜짝 놀라 고개를 들었다. (중략) 형식은 얼마큼 마음에 수치한 생각이 나서 고개를 돌리며, “아직 그런말에 익숙지를 못해서……” 하고 말끝을 못 맺는다. 
“대관절 어디로 가는 길인가? 급하지 않거든 점심이나 하세그려.”
“점심은 먹었는걸.”
“그러면 맥주나 한잔 먹지.”
“내가 술을 먹는가.”
(중략)
“요― 오메데토오(아― 축하하네). 이이나즈케(약혼한사람)가 있나 보네 그려. 음나루호도(그러려니). 그러구도 내게는 아무 말도 없단 말이야. 에, 여보게”하고 손을 후려친다.

이 텍스트를 글자 단위로 one-hot-vector로 바꾼 뒤 LSTM에 넣어 학습시켜 보기로 했습니다. 하이퍼파라메터는 히든 차원수 100, learning rate 0.1을 줬습니다. 다음은 학습 결과입니다.

Iter 0 : 랫萬게좁뉘쁠름끈玄른작밭裸觀갈나맡文플조바늠헝伍下잊볕홀툽뤘혈調記운피悲렙司狼독벗칼둡걷착날完잣老엇낫業4改‘촉수릎낯깽잊쯤죽道넌友련친씌았융타雲채發造거크휘탁亨律與命텐암먼헝평琵헤落유리벤産이馨텐

Iter 4900: 를왔다내 루방덩이종 은얼에는 집어흔영채는아무 우선을 에서가며 건들하아버전는 애양을자에 운 모양이 랐다. 은 한다선과 ‘마는 .식세식가들어 ,

형식다

“내었다.있이문

Iter750000 : 으로 유안하였다. 더할까하는 세상이 솔이요, 알고 게식도 들어울는 듯하였다. 태에그려 깔깔고 웃는듯이 흔반다. 우선형은사람을 어려보낸다.

“그려가?”

한다. 영채는손을 기쁘

Iter1000000 : 에 돌내면서,

“여러 넣어오습데다. 그 말대 아무도좀 집림과 시오 백매, 저는 열녀더러, 기런 소년이가아니라.”

“어리지요.”

노파도 놀라며,

“저희마다가말없습니까.”

“아니 (대화체)

꽤 오랜 시간 학습시켰음에도 여전히 뜻 모를 글자들을 내뱉고 있는 점이 아쉽습니다. 다만 ‘영채’, ‘선형’ 등 무정 인물명들을 언급하거나, 따옴표를 써서 대화체 문장을 구성하거나, ‘-요’ ‘-까’ ‘-다’ 같은 종결어미를 사용해 문장을 끝맺고 있는 등 잘 하고 있는 점도 눈에 띕니다. 긴 글 읽어주셔서 감사합니다.



Comments