이번 포스팅에서는 딥러닝의 고전적인 접근법 중 하나의 Recurrent Neural Networks (RNNs)에 대해서 간단히 살펴보도록 하겠습니다. 사실 CNN과 함께 RNN이 고전적인 딥러닝 방법론의 양대축입니다만, 최초에 RNN이 적용되었던 자연어처리 분야에서 이미 Transformer 계열의 딥러닝 구조에 정복당한 감이 있어....;;; 포스팅을 올릴까 말까 고민이 있었는데요. 최근에 등장한 모델 중에서도 RNN구조를 하위항목으로 종종 포함하는 경우도 있고, 시계열 예측 같은 경우에는 아직까지 Transformer모델이 범용적으로 강력하게 작동하는 상황은 아닌 것 같습니다. 때문에 RNN을 체크해 둘 필요성은 있는 것이죠. 다만, 이미 다른 블로그나 자료 등에서 쉽게 자료를 찾아볼 수 있기에 강의자료 요약형태로 간단간단하게 살펴보도록 하겠습니다.
일단 RNN구조가 처음에 적용된 분야인 자연어 처리 문제를 하나 살펴볼까요? 불어인 "le chat est noir"라는 문장을 "the cat is black"으로 번역하려고 합니다. 우리가 만든 모델이 the cat까지는 번역을 했는데 이 뒤에 어떤 단어를 선정해야하는지 결정해야 한다고 해볼까요? 이 때는 너무나 당연하게도 est라는 불어 단어뿐만 아니라 앞선 다른 정보들도 중요하게 작용합니다. le chat 같은 단어도 (성, 수, 격 등) 매우 중요하게 영향요인으로 작용합니다. 그런데 가장 기초적인 딥러닝 구조인 feed-forward networks 같은 경우에는 이런 순차적/시간적 정보 (sequential/temporal information)를 충분히 담아내지 못합니다. 이런 순차적 정보를 처리하기 위한 구조로서 RNN이 등장합니다.
one-to-many: 하나의 input을 받아서 sequence output을 뱉어내야 하는 task입니다. 위 그림에서 보시는 것처럼(show attend and tell 논문입니다) 사진이라는 input을 받아서 sequence output인 caption 문장을 뱉어냅니다.
many-to-one: sequence input을 받아서 한 개의 output을 뱉어내야 하는 task입니다. 영화 평론을 보고 긍정/부정 여부를 판별하는 모델이 예가 될 수 있습니다(감성 분석 sentiment analysis 라고도 하죠?)
many-to-many: 우리가 이번 포스팅에서 봤던 번역 문제가 input sequence를 받아서 output sequence를 뱉어내는 manay-to-many 문제가 됩니다. 이런 번역작업 말고도 음성신호를 받아적는 모델, input sequence를 받아서 각각의 항목의 문법적 역할을 밝히는 형태소 분석 등의 task가 이 항목에 속한다고 볼 수 있습니다.
이런 Task를 처리하기 위해서는 "이전의 정보"를 저장해 두었다가 써먹을 수 있는 일종의 메모리구조가 필요합니다.
Reference:Feng, Weijiang, et al. "Audio visual speech recognition with multimodal recurrent neural networks."  2017 International Joint Conference on Neural Networks (IJCNN) . IEEE, 2017.
hidden state를 통해서 이전의 정보값을 저장해두고 다음 step에서 활용하게 되는 구조가 RNN입니다. Output을 모두 다 가져와서 쓸 것인지, 아니면 sequence의 마지막 output만 가져와서 쓸 것인지는 위에서 말씀드린 Task에 따라서 결정하시면 됩니다(Tensorflow/Keras API로 치면 return_sequence argument에 해당한다고 보시면 되겠죠).
그런데, 이러한 구조를 이용해서 어느정도 sequential/temporal information을 뽑아낼 수는 있으나 길이가 긴 sequence의 정보를 뽑아내는 데는 어려움을 겪게 됩니다. 바로 vanishing gradient 문제입니다. 잠시 vanshing gradient를 짚어보고 다시 RNN계열의 딥러닝 구조로 돌아오겠습니다.
Vanishing Gradient
딥러닝 구조를 학습시키는 데 일반적으로 Gradient Descent/Backpropagation을 사용하는 것은 잘 알고 계실 겁니다. output에 가까운 parameter들은 output에 직접적인 영향을 주고 있기에 미분을 했을 때도 그 값이 크게 나오지만(해당 값들이 조금만 변해도 output에 큰 변화를 이끌어낼 수 있죠), 앞 단에 있는 parameter 일수록 output에 미치는 영향력이 미미해집니다(그 parameter값이 크게 요동친다고 해도 output에는 그렇게까지 극적인 변화가 나타나지 않습니다). 기본적으로 Gradient Descent/Backpropagation라는 방법론이
업데이트하고자 하는 parameter에 변화를 준다 --> output에 변화가 생긴다 --> Loss(목적함수)에 변화가 생기는 것을 보고 목적함수를 최소화하는 방향으로 parameter를 업데이트 한다
컨셉으로 작동하는 것인데 output에 변화가 미미하면 이 작업을 할 수가 없는 것이고, 이를 Vanishing gradient 문제라고 합니다. 특히 딥러닝 구조의 층이 깊어질 수록 이러한 문제가 대두되는데, 이 문제는 신경망의 activation function(활성화함수)와도 관련이 있습니다. 아래 그림에는 activation function으로 자주 활용되는 두 함수의 그래프가 나타나있습니다. 쉽게 눈치채실 수 있겠지만 이 함수들은 정의역이 0근처인 곳에서만 유의미한 미분계수(기울기)가 나오고 0에서 멀어질수록 미분계수의 절대값이 급격하게 감소합니다. 신경망이 깊어지면 깊어질수록 이 문제가 골칫거리가 됩니다.
Sigmoid 함수와 tanh함수.
그래서 초창기에 feed-forward network에서 activation 함수로 사용하던 sigmoid 대신 이 문제를 해결하기 위해 ReLU와 같은 activation function이 도입된 것이죠.
자, 이제 다시 RNN으로 돌아와볼까요? RNN에서 hidden state를 그전 hidden state와 현재 input을 가지고 업데이트 시킬 때 어떤 함수를 활용했죠? tanh입니다.... 그래서 seuqence의 길이가 길어지면 마찬가지로 vanishing gradient 문제가 발생하고 parameter가 제대로 업데이트되지 않는 현상이 발생하게 됩니다.
이러한 문제를 완화하기 위해서 개선된 RNN계열의 구조가 LSTM(Long Term Short Memory), GRU(Gated Recurrent Unit)와 같은 구조들입니다.
LSTM과 GRU와 같은 구조로서 시간축(Sequence를 따라 계산하는)의 Vanishing gradient 문제는 완화할 수 있었지만 만약 RNN의 구조를 깊이쌓는다면 층의 깊어지면서 생기는 Vanishing gradient에 대한 문제는 해결할 수가 없습니다.
RNN 구조를 다룰 때, 몇 가지 기술적인 유의사항을 말씀드리겠습니다.
Autoregressive/Non-Autoregressive : Sequential/Temporal information을 활용할 때 task에 따라서 Autoregressive 접근법을 써야할 수도 있고, Non-Autoregressive 접근법을 써야할 수도 있습니다. text classification이나 형태소 분석, 등이 목적이라면 sequence의 현재 step에 대한 output을 결정하는 과정에서 앞,뒤 맥락을 모두 고려해서 판단하는 것이 합리적일 것입니다. 예를 들어 "배를"이라는 단어가 하는 역할을 결정할 때, 뒤에 나오는 단어를 고려하여 결정해야겠죠. "나는 배를 탄다"인지 "나는 배를 먹는다"인지에 따라 그 역할과 의미가 달라집니다. 이 때는 Non-Autoregressive task가 되고, sequence의 뒷쪽 step의 정보를 앞단에서도 활용할수 있도록 역방향의 RNN을 쌓는 Bidirectional RNN구조를 활용할 수도 있습니다.Reference: https://jwcn-eurasipjournals.springeropen.com/articles/10.1186/s13638-019-1511-4/figures/9
그런데, NLG(Natural Language Generation)이라든가 시계열 예측 같은 경우에는 어떨까요? sequence의 다음 step 의 정보를 가져다가 써도 될까요? 안됩니다. 인과율(Causality) 위반이죠. 내일의 주가를 예측하려고 하는데 모레나 글피의 정보를 가져다가 예측한다고 하면 이게 말이 되겠습니까? 그래서 이런 경우에는 Autoregressive task가 되고 당연히 Bidirectional RNN구조를 사용해서는 안됩니다.
Stateful RNN: Tensorflow/Keras같은 API를 사용하다보면 RNN구조 옵션에 stateful이 붙어있는 것을 확인하실 수가 있습니다. "학습 샘플의 가장 마지막 상태가 다음 샘플 학습 시에 입력으로 전달"되는지를 지정해주는 argument인데요, 이미지적으로는 https://tykimos.github.io/2017/04/09/RNN_Layer_Talk/ 블로그의 그림을 참고해보시면 좋을 것 같고 여기서는 task 예를 가지고 생각해보겠습니다. 영화 한줄 평을 보고서 이 평가가 긍정적인지 부정적인지를 평가하는 모델을 만든다고 하겠습니다. 이 경우에 관측치(observation)는 "고전의 간결함. 연기 거장들의 리즈시절. 리스펙", "시종일관 으악으악 소리만 듣다가 졸다깨다 반복한 영화", "진짜 연기도 그렇고 CG도 미쳤다 그냥 다 좋았음"... 과 같은 한줄평이 될 것입니다. 그런데 이 개별 관측치(한줄평)들은 서로 독립적인 input입니다. 그래서 "고전의 간결함. 연기 거장들의 리즈시절. 리스펙"라는 첫번째 문장을 처리하고 두번째 "시종일관 으악으악 소리만 듣다가 졸다깨다 반복한 영화"라는 문장을 처리할 때, 첫문장에서 산출된 hidden state를 두번째 문장의 입력으로 넣지 않습니다. 그런데, 만약 아래 블로그의 예시처럼 악보의 음표 4개를 입력시키면 다음 음표를 뱉어내는 모델을 만든다거나 지난 6일의 주가를 가지고 다음날의 주가를 예측하는 모델을 만든다면 어떻게될까요? 흔히 rolling(sliding) window라고 하는 방식으로 input을 넣어줄 겁니다. 첫번째 관측치는 0일~5일까지의 주가, 두번째 관측치는 1일~6일까지의 주가, ... 이런식이 되는 거죠. 이런 경우에는 앞선 관측치와 그 다음 관측치가 독립적이지 않고 연속적으로 이어지는 구조입니다. 그러니 앞선 관측에서 산출된 hidden state가 다음 관측 학습시 입력으로 활용될 수 있는 것입니다.
이와 같이 stateful RNN을 학습시키는 경우, 동일 epoch 내에서는 stateful 방식으로 진행하되, 한 epoch가 끝나면 reset_state()를 해줄 필요가 있습니다(이 때는 앞선 epoch 관측치와 다음 epoch의 관측치가 연속으로 이어지는 상황이 아니니). 그리고 이것은 evaluate/predict 과정에서도 이 작업이 필요합니다.
오늘 살펴본 RNN구조는 최근에는 단독적으로 활용된다기 보다는 구조의 하위 항목으로 사용되는 경우가 많은 것 같습니다. 음성인식 분야의 LAS(Listen Attend and Spell),
Reference: Chan, William, et al. "Listen, attend and spell." arXiv preprint arXiv:1508.01211 (2015).
시계열예측 분야의 TFT(Temporal Fusion Transformers)
Reference: Lim, Bryan, et al. "Temporal fusion transformers for interpretable multi-horizon time series forecasting." International Journal of Forecasting (2021).
모두 이러한 예시라고 할 수 있겠습니다.
그럼, RNN의 기초적인 내용에 대해서는 정리가 된 것 같아 여기서 이번 포스팅을 마치도록 하겠습니다.