지난 스터디 때 만든 RNN 발표ppt 정리!
'파이토치 첫걸음'책을 이용해 진행하는 스터디인데 RNN, LSTM, GRU 이 세가지 모델이 한 chapter안에 굉장히 간단하게 요약되어 있다.. !! 일단 정리는 함께 했지만 여기서 LSTM, GRU는 간단히 정리하고 나중에 더 자세히 정리해야할 듯,,하다!
순환신경망의 등장 배경
오늘 살펴보게 되는 내용은 Recurrent neural network 즉 순환신경망이라고 불리는 RNN이라는 모델이다.
본격적으로 순환신경망 즉 RNN이 어떻게 동작하는 모델인지에 대해 살펴보기 이전에 이런 모델이 어떻게 등장하게 되었는지, 어떤 데이터들을 처리할 때 사용될 수 있는지를 먼저 짚고 넘어가보자.
우리가 사용하는 데이터 중에서는 Sequence data 가 굉장히 많은데 이 Sequence data가 무엇이냐하면 말그대로 이제 순서를 가지는 그러한 data라고 할 수 있다. 가장 대표적으로 우리가 사용하는 ‘언어’도 이러한 순서에 따라 맥락을 해석할 수 있기 때문에 Sequence data의 예시가 될 수 있다.
언어 뿐 아니라 다음과 같이 샘플링 된 음성 신호 데이터 혹은 주식 데이터 들도 모두 순서가 중요한 Sequence data들이 되고 이때 특별히 어떤 시간에 따른 의미가 존재하는 데이터가 바로 우리가 자주 접하게 되는 시계열 데이터 즉 time series 데이터가 된다.
그런데 이제 이렇게 자주 사용하게 되는 시퀀스 데이터는 기존의 neural network이나, 지난시간에 살펴본 cnn 과 같은 경우에서는 처리하기가 어려운데, 왜냐면 그러한 인공신경망 구조들은 입력 x가 들어오면 바로 출력 y로 나타나는 간단한 형태였기 때문에 이러한 sequence data들을 처리하기에 적합하지 않기 때문이다. 따라서 이제 여기서 살펴볼 순환신경망 모델이 바로 이러한 시퀀스 데이터들에서 어떤 패턴을 찾고 서로 어떤 관계에 있는지 찾아내기 위해서 고안된 모델이라고 할 수 있다.
순환신경망의 작동 원리
이제 본격적으로 순환신경망이 어떻게 구성되어있고 어떻게 동작하는지 하나하나 살펴보도록 하자.
우선 왼쪽에 보이는 그림이 이제 우리가 계속해서 봐왔던 일반적인 인공신경망의 구조라면, 순환신경망이란 다음과 같이 Memory system이 추가된 즉 순환성이 추가된 인공신경망의 구조를 갖게 된다. 앞에서 sequence 데이터를 처리하기 위한 모델이 바로 순환신경망이라고 언급했는데, 순서를 가진 데이터를 처리하기 위해서는 단순히 현재의 입력만 가지고 출력을 예측하는 것이 아니라 이전의 일어난 사건들을 바탕으로 결과를 예측해야하기 때문에 이러한 Memory system을 갖게 된다.
이 순환신경망을 이제 조금더 간단히 살펴보기 위해서 노드의 수를 모두 1개로 바꿔준 뒤에 시간에 따라서 쪼개어 보면서 이 memory system이 어떤 역할을 하는지 살펴보자.
우선 매 시간마다 여기서는 t가 1씩 증가할 때마다 새로운 입력값이 들어오고 그에따른 결괏값이 계산된다고 가정을 하면 다음과 같이 t 가 0일 때 이미 한번 입력값에 의해서 값들이 계산된 그러한 상태가 된다. 이러한 상태에서 t가 1이 되었을 때 어떤 새로운 입력이 들어오게 되고 출력을 내게 되는데 이때 단순히 t가 1일때의 입력값으로 은닉층이 계산되는 것이 아니라 다음과 같이 첫번째 은닉층의 값이 t=1일 때 현재의 입력값과 이전시간 즉, t가 0이었을 때 이미 계산되어진 은닉층 1의 값의 조합으로 현재 은닉층 1의 값이 계산되게 된다.
두 번째 은닉층의 값도 마찬가지의 방법으로 계산이 되게 되는데, 그림을 보면 다음과 같이 현재 t=1일때의 은닉층 1의 값과 전 시간 t가 0일 때 계산되어졌던 두번째 은닉층의 값의 조합으로 현재 은닉층의 값이 결정되게 된다.
따라서 이와 같이 t가 0일때의 값들과 t가 1일때의 값들의 조합으로 연산된 값이 최종적으로 t가 1일때의 출력값으로 나오게 된다.
이렇게 순환하는 과정들을 시간 단위로 다음과 같이 풀어서 보면 어떤 전체적인 RNN이 어떻게 작동하는지 도식적으로 표현할 수 있다. 즉 은닉층의 노드들은 어떤 초기값을 가지고 계산이 시작되고 첫번째 입력값이 들어온 t=0인 시점에서 입력값과 초기값을 조합해 은닉층들의 값들이 계산되게 된다. 이 시점 즉 t=0인 시점에서 결괏값이 도출되면 t=1인 시점에서는 새로들어온 입력값과 t=0인 시점에서 계산된 은닉층의 값과의 조합으로 t가 1일때의 은닉층의 값과 결괏값이 다시 계산되게 되고 이러한 과정들이 우리가 지정한 시간만큼 반복하게 된다.
순환신경망의 역전파
이렇게 일정시간동안 모든 값이 계산되면, 이제 모델을 학습하기 위해서 결괏값과 목푯값의 차이를 손실함수를 통해서 계산을 하게 된다. 그리고 그 값을 이용해 backpropagation을 하는데 이때 순환신경망은 기존의 역전파와 다르게 계산에 사용된 시점의 수에 영향을 받게 되는데, 예를들어 t가 0부터 t가 2까지 계산에 사용됐다면 그 시간 전체에 대해서 역전파를 해야하게 된다.
그리고 이것을 시간에 따른 역전파라는 뜻에서 BPTT 라고 부른다.
다음 그림에서 만약 PYTORCH 라는 단어를 예로 살펴보면 T가 0일 때 입력값으로 P가 들어가고 결괏값으로 다음 글자인 Y가 나오길 기대하게 된다. 따라서 기댓값 Target_0는 y가 되고 마찬가지로 t가 1일 때 입력값으로 y가 들어가면 이번에 target1은 다음글자인 t가 되게 된다. 이렇게 우리가 원하는 기댓값인 target과 결과가 같지 않으면 loss가 발생하게 되는데, 이때 t=2 시점에서 발생한 손실을 역전파 하기 위해서는 우리가 전에 살펴봤던대로 손실을 입력과 은닉층들 사이의 가중치로 미분해서 손실에 대한 각각의 비중을 구해 업데이트 한다. 하지만 이 과정에서 앞에서 살펴봤던 것과 같이 이전 시점의 값들이 계속해서 연산에 포함되게 되는데, 따라서 결과적으로 t가 2인 시점의 손실을 역전파 하기 위해서는 결과적으로 t가 0일떄의 시점의 노드 값들에도 모두 영향을 줘야 하고 즉 시간을 역으로 거슬러 올라가는 방식으로 각 가중치들을 업데이트하게 된다
이러한 과정을 t가 2일때의 경우에 수식으로 살펴보게 되면, 다음과 같이 현재상태의 output o는 h2의 output을 전달받아 갱신되는 저희가 일반적으로 떠올리는 구조로 표현할 수 있고, 기본적으로 순환 신경망의 hidden state는 hyperbolic tangent 함수를 활성함수로 이용하기 때문에 다음과 같이 각 은닉층들의 output을 계산하게 된다. 하지만 이때 결국 h2와 h1의 아웃풋을 계산하기 위한 input에 t가 1일때의 결과값이 들어가게 되기 때문에 우리는 현재 t가 2인 시점에 발생한 손실이 과거의 시점에 전부 영향을 미치게 된다는 것을 알 수 있게 된다. Chain rule에 마지막 부분을 보면 더 확실히 볼 수 있는데 결국 이 마지막 미분식을 계산해보면 t가 1일 때의 h2값이 나오게 되고 이 값은 이전 시점의 값들로 구성되어있는 것을 알 수 있기때문에 따라서 제대로 이 값을 미분하기 위해서는 t=0인 시점까지 계속 미분을 진행해줘야한다.
실제로 업데이트를 할때는 가중치에 대해 시점별 기울기를 모두 더해 한번에 업데이트를 진행하게 된다.
순환신경망의 구현_pytorch
이제 이러한 방식으로 작동하는 순환신경망을 pytorch로 구현한 부분을 간단히 살펴보자.
라이브러리 임포트 같은 부분은 모두 생략했고 모델을 만드는데 핵심적인 함수들을 주로 살펴본다. 우선 이 예시에서 사용하는 문장은 hello pytorch how long can a rnn cell remember?이라는 문장이고 사용할 문자들을 알파벳 소문자와 특수문자 몇 개로 한정해서 진행하게 된다. 이때 node의 수는 n_hidden에 저장하게 되고, 아래에 string to onehot 함수는 말그대로 문장이 들어왔을 때 문장을 이용해 이용해 연산을 가능하게 해야하기 때문에 one hot vector로 만들어주는 함수가 된다.
위에 보이는 onehot to word함수는 말그대로 토치 텐서를 입력으로 받아 넘파이 배열로 변환한뒤 거기서 1인 지점을 인덱스로 잡아 char list에서 뽑아내는 즉 원핫벡터를 다시 문자로 바꾸는 함수다.
이제 앞에서 살펴본 순환 신경망 클래스를 함수로 구성하는 부분이다. 이 클래스는 앞에서 원핫벡터로 변환시켜준 단어 하나를 입력값으로 받고 hidden state하나를 통과시켜 결괏값을 내는 구조다. 함수의 hidden 부분을 보면 앞에서 살펴봤듯이 이전 시간의 은닉층 값과 입력값의 조합으로 새로운 은닉층 값을 생성하고 output은 은닉층에서 결괏값을 내는 부분의 연산을 한번더 통과해 나오는 것을 알 수 있다.
이제 손실함수와 최적화함수를 정의하게 되는데 여기서는 보는 것과같이 MSE LOSS FUNCTION 즉 L2 손실함수를 이용하게 되고 파이토치에는 다음과 같이 구현되어있다.
최종적으로 학습을 시켜줄때는 우리가 학습하고자 했던 문장을 원핫 벡터로 변환한 넘파이 배열을 다시 토치 텐서 형태로 변환해주는데 이때 type as를 이용해서 자료형을 다음과 같이 torch.FloatTensor로 지정하게 되면 앞서 만든 함수처럼 start token +문장+ end token으로 구성된 매트릭스가 생성되게 된다. 이떄 target값은 j 번째 인덱스에 해당하는 값이 입력으로 들어오면 j+1번째 인덱스에 해당하는 값이되기 때문에 다음과 같이 설정해주게 된다.
따라서 이제 설정해준 에폭만큼 문장 전체를 학습하는 과정을 반복하면 된다.
이렇게 RNN에 대해서 살펴봤는데 이 RNN은 어느 정도 이상부터는 결과가 한계에 부딫히게 되되는 것 볼 수 있다 .이는 타임 시퀀스가 늘어나면서 역전파 시 하이퍼볼릭 탄젠트 함수의 미분값이 계속해서 여러 번 곱해지기 때문이다. 즉 타임시퀀스가 길어질수록 모델이 제대로 학습하지 못하는 VANISHING GRADIENT 현상이 발생하게 되는 것이다. 이러한 순환신경망의 한계를 개선하고자 등장한 모델이 이제 살펴볼 LSTM과 GRU라는 모델이다.
LSTM
우선 LSTM이다. LSTM의 구조는 다음과 같은데 단순히 말하면 기존 RNN 모델에 장기기억을 담당하는 부분을 추가한 것이라고 할 수 있다. 즉 기존에는 은닉상태 즉 hidden state만 있었다면 이제 cell state라는 이름을 가지는 전달부분이 추가된 것이다.
조금 더 구체적으로 살펴보기 위해 은닉층 중 하나를 떼어내 내부를 살펴보면 오른쪽 그림과 같다. 굉장히 복잡해보이는 구조를 가지고 있기 때문에 이 그림에서 각 연결점들이 무엇을 의미하는지 하나하나 살펴보도록 하자.
우선 LSTM의 상단에 있는 셀 상태 부분 이다. 이 부분은 앞서 말했듯이 장기기억을 담당하는 부분이다. 따라서 곱하기 라고 되어있는 부분은 기존의 정보를 얼마나 남길 것인지에 따라 비중을 곱하는 부분이 되고, 더하기 부분은 현재 들어온 데이터와 기존의 은닉상태를 통해 정보를 추가하는 부분이 된다.
다음으로 왼쪽 하단에 있는 Forget gate 즉 망각게이트 이다. 이 망각 게이트는 말그대로 기존의 정보들로 구성되어있는 셀 상태의 값을 얼마나 잊어버릴지를 정하는 부분이 된다. 현재 시점의 입력값과 직전 시점의 은닉상태 값을 입력으로 받는 한 층의 인공신경망이라고 보면 간단히 이해할 수 있다. 가중치를 곱해주고 바이어스를 더한 값을 여기 식에 보이는 것처럼 시그모이드 함수에 넣어주면 0에서 1사이의 값이 나오는데 이 값을 이용해서 기존의 정보를 얼마나 전달할지 비중을 정하게 된다.
마지막으로 입력게이트 다. 이 입력게이트는 어떤 정보를 얼마나 셀 상태에 새롭게 저장할 것인지를 결정하는 부분이다. 입력게이트도 기존 방식과 비슷하게 새로운 입력값과 직전 시점의 은닉상태 값을 받아서 여기 보이는 식처럼 한번은 시그모이드 활성함수를 거치고, 또 한번은 하이퍼볼릭 탄젠트 활성화함수를 통과시키게 됩니다. 하이퍼볼릭 탄젠트 활성함수를 통과한 값은
-1에서 1사이의 값을 가지고 이 값은 새롭게 셀 상태에 추가할 정보가 되고, 시그모이드 함수를 통해 나온 값은 0에서 1사이의 비중으로 새롭게 추가할 정보를 얼마큼의 비중으로 셀 상태에 더해줄지 정하게 된다.
최종적으로 셀상태와 은닉상태의 업데이트를 살펴보게 되면, 우선 셀상태에서 forget gate를 통과한 ft의 값과 Ct-1의 값을 곱해서 기존 셀상태의 정보를 얼만큼 전달할지 결정하고 input gate를 통과한 it와 C물결 t의 값을 곱해 어떤 정보를 얼마만큼의 비중으로 더할지 결정하게 된다. 오른쪽에 보이는 hidden state update에서는 현재의 output값에 CT 즉 업데이트 된 셀 상태 값을 하이퍼볼릭 탄젠트 함수를 통과시킨 -1과 1사이의 비중을 곱한 값으로 생성되게 된다.
GRU
다음으로 살펴볼 모델은 GRU 이다. 이 모델은 이제 LSTM보다 간단한 구조를 가지고 있는데 성능면에서는 밀리지 않는 그런 모델로 지금도 LSTM과 함께 굉장히 많이 이용되는 모델이다. 이 GRU는 LSTM과 달리 셀상태와 은닉상태를 분리하지않고 은닉상태 하나로 합친 형태를 가지고 있다.
여기서 이제 ht를 구하기 위한 4가지 수식이 등장하게 되는데 각각 간단히 살펴보면, 우선 첫번째로 보이는 rt는 앞에서 살펴봤던 update gate로 현재 시점의 새로운 입력값과 직전시점의 은닉 상태 값에 가중치를 곱하고 시그모이드 함수를 통과시켜 업데이트할 비중을 정하는 부분이다.
두번째 zt는 리셋 게이트라고 해서 업데이트 게이트와 같은 입력을 받아 동일하게 시그모이드 함수를 통과해 비중을 정하고 이 비중은 밑에 ht물결을 구할 때 기존 은닉 상태 값을 얼만큼 반영할지를 정하는데 이용하게 된다. 따라서 ht 물결은 기존의 은닉 상태에 가중치가 곱해진 값과 새로운 입력값을 입력으로 받아 가중치를 곱한 후 하이퍼볼릭 탄젠트 함수를 통과해 새로운 정보의 값을 리턴하고 마지막 수식에서 새로운 은닉상태를 구하게 된다.
Word Embedding
이렇게 살펴본 LSTM과 GRU를 구현하기 위해서는 워드 임베딩에 대해서 먼저 살펴보고 넘어가야하는데, 우선 우리가 기존의 자연어 처리 분야에서 단어를 표현하는 방법에 대해 배웠던 것은 onehot encoding이었다. 즉 전체 단어에서 해당 단어의 index에 해당하는 값만 1로 설정하고 나머지는 0인 vector로 표기하는 방법이었다. 이 방법은 굉장히 단순하지만 단어를 단순히 index에 따른 vector로 표현하기 때문에 여러 단어 간의 유사성을 평가할 수 없을 뿐 아니라 전체 단어 개수가 증가하는 경우 one hot vector의 크기가 지나치게 커진다는 단점을 가진다.
이러한 단점을 보완하기 위해 등장한 것이 바로 word embedding 이다. Word embedding은 간단히 말해서 단어를 특징을 가지는 N차원의 vector로 표현하는 것이다. 따라서 기존의 onehot vector가 각 단어를 사전의 개수만큼의 차원으로 표현해야만 했던것과 다르게 각 단어를 특징을 가지는 N 차원의 vector로 표현하게 된다. 아래 예시를 통해 둘을 비교해보면 one hot vector와 달리 word embedding에서는 python과 ruby가 프로그래밍 언어라는 의미적인 공통점을 가지고 있기 때문에 벡터값이 유사하다는 것을 볼 수 있다.
좀 더 구체적으로 embedding의 두가지 기법들을 살펴보자.
우선 CBOW 라는 Continuous bag of words라는 방식이다. 이 방식은 주변 단어들로부터 가운데 들어갈 단어가 나오도록하는 임베딩 방식이다. 즉 어떤 context가 주어졌을 때 우리가 설정한 기준 단어에 대해 앞뒤로 2분의 N개씩 총 N개의 문맥단어를 입력으로 사용해 기준단어를 맞추기 위한 네트워크를 생성하게 된다. 여기 간단한 예시를 보면 The boy is going to school 이라는 문장이 있는데 이때 우리가 기준 단어를 the라고 설정하고 입력으로 boy Is going이라는 세 단어의 onehot vector를 넣어주게 되면 hidden layer를 거쳐 나온 output과 우리가 원하는 기준단어인 the와의 loss를 줄여나가는 방향으로 네트워크가 생성되게 된다.
다음으로 skip gram 이라는 모델은 방금 살펴본 CBOW와는 반대로 중심 단어로부터 주변 단어들이 나오도록 모델을 학습하여 임베딩 벡터를 얻는 방식이다. 따라서 context가 주어졌을 때, 기준 단어를 입력으로 사용하여, 기준단어에 대해 앞 뒤로 N/2개 씩 총 N개의 문맥 단어를 맞추기 위한 네트워크라고 할 수 있다. 앞과 마찬가지로 같은 예시를 통해 살펴보면 이번에는 기준단어인 THE를 입력으로 넣어 결과값과 BOY, IS, GOING을 비교해 LOSS를 계산하는 것을 볼 수 있다.
Embedding을 이용한 LSRM / GRU 구현_pytorch
이제 최종적으로 이러한 embedding을 이용해 LSTM과 GRU를 구현하는 방법에 대해 간단히 살펴보자.
신경망을 구성하는 부분의 코드만 가져왔는데, torch.nn에는 torch.nn.embedding이라는 클래스가 있고 이 클래스를 사용하면 임베딩을 쉽게 생성하고 학습시킬수가 있게 된다. 함수의 parameter에서 num embeddings와 embedding dim은 각각 사용할 문자나 단어의 가지수 및 임베딩할 벡터공간을 의미하기 때문에 여기서는 num embedding에 input size를 , embedding dim은 임의로 설정해주게 되고 후에 초기화시킨 은닉 상태를 hidden에 인스턴스로 전달하게 되면 연산이 진행되게 된다. 여기서 view는 reshape와 같은 기능을 한다. 이때 GRU의 경우에는 RNN처럼 은닉상태만 가지고 있기 때문에 다음과 같이 CLASS를 구성했지만 ,
LSTM에서는 앞에서 살펴봤듯이 셀상태, 은닉상태 두가지를 가지고 있기 떄문에 클래스를 다음과 같이 구현해야한다. 나머지는 GRU와 거의 동일하게 진행된다.
참고자료 : [파이토치 첫걸음] , 저자 최건호
'AI' 카테고리의 다른 글
[CNN] Convolution Neural Network 정리 (0) | 2021.09.20 |
---|---|
[정보이론] Entropy, Cross Entropy, KL-divergence 이해하기 (0) | 2021.04.19 |