본문 바로가기
데이터 사이언스/딥러닝

RNN 구조 쉽게 이해하기

by 끙정 2021. 4. 28.
728x90

끙정입니다.

 

딥러닝을 처음 학습할 때, 상대적으로 DNN이나 CNN은 수월하게 배우는 편일 것입니다.

그러나 RNN은 유독 초반에 어렵게 느껴지고, 어떤 Shape으로 집어 넣어서 어떤 Target을 학습시키는지 개념이 잡히지 않을 때가 많습니다.

 

오늘은 RNN 구조를 쉽게 이해하는 시간을 가져보겠습니다.

내용은 거의 아래 책에 나오는 내용에 대한 복습이라고 보시면 됩니다.

제가 가장 좋아하는 책이며, 정말 설명이 잘 되어 있는 책입니다. 강추.

 

www.yes24.com/Product/Goods/73741253?OzSrank=1

 

파이토치 첫걸음

페이스북이 주도하는 딥러닝 프레임워크 파이토치 기초부터 스타일 트랜스퍼, 오토인코더, GAN 실전 기법까지 A to Z 딥러닝 구현 복잡도가 증가함에 따라 ‘파이써닉’하고 사용이 편리한 파이

www.yes24.com

 

순환신경망.

자, 보통 RNN에 대해 검색하면 아래와 같은 그림이 나옵니다.

 

순환신경망을 표현한 그림인데, 우리가 기존에 보던 그림과는 달라서 직관적으로 이해가 잘 가지 않습니다. 보통 RNN을 배우기 전에 DNN->CNN 을 배우기 때문에 그런 것 같습니다. 우리의 뇌 구조가 FNN 구조에 맞춰져 있기 때문이죠. 바로 아래와 같은 모양입니다.

 

우리가 생각하는 딥러닝 아키텍쳐는 이렇게 인풋 레이어가 있고, 히든 레이어 여러 개에 아웃풋 레이어가 있습니다. 각 레이어의 노드들은 다음 레이어의 노드들과 Fully Connected 되어 순전파합니다.

 

자, 그러면 여기서부터 접근을 시작해 보겠습니다. 이러한 보통의 FNN 아키텍쳐에 순환성을 추가해 봅니다.

히든 레이어에서 각 노드(셀)가 다시 자기 자신으로 인풋 되는 것으로 표현할 수 있습니다. 대충 그렇구나, 할 수는 있지만 아직까지도 잘 이해가 가지 않습니다. 그렇다면 각 레이어에서 1개의 노드(셀)만을 가져와 봅시다.

 

인풋 노드 i 가 h1으로 들어감과 동시에 h1도 같이 들어갑니다. 즉, h1 = i1 + h1 이 됩니다. (연산자는 예시입니다.) 말이 되지 않습니다. h1의 값이 다시 h1으로 들어가다니?! 여기에 우리는 시간축을 다시 추가합니다.

 

시간축.

그렇습니다. h1이 h1으로 다시 들어간다는 말은 h1(t-1)의 정보가 그 다음 time인 h1(t)로 들어간다는 말입니다. 즉, 이전 time의 해당 노드(셀)의 정보가 그 다음 time의 노드(셀)의 정보에 영향을 준다는 말입니다. 이렇게 보니 h1이 h1으로 다시 들어가는 것이 이해가 됩니다.

 

그런데 저 아래 노란 박스 그림을 보니 어디서 많이 본 것 같지 않나요? 맞습니다. 맨 처음 나왔던 그림과 똑같습니다.

맨 처음 나왔던 그 그림이다!

 

즉, 일반적으로 우리가 봐 온 RNN의 모형도는 시간축이 추가 된 하나의 노드(레이어)를 표현하는 것입니다. 그것을 전체 아키텍쳐로 확장하면 우리가 흔히 봐 온 RNN의 아키텍쳐가 되는 것입니다. 단지 순환성이 더해졌을 뿐이지요. 추가로 위의 방식으로 순환 부분을 시퀀셜하게 풀어보면 아래와 같이 표현이 됩니다.

 

우리는 이제 저 네모난 셀이 하나의 노드가 아니라 레이어라고 생각할 수 있습니다. 그리고 time만 다른 가로의 노드(레이어)들이 사실은 모두 같은 것입니다. 따라서 가중치(weight)도 공유합니다. 단지 여기에 시간축(time)을 추가하여 반복적으로 다른 input과 hidden state가 전달 될 뿐입니다. 직관적으로 이해하기 쉽게 아래로 펼쳐 놓은 것입니다.

 

 

코드 구현.

코드로 구현하다보면 더 쉽게 이해가 되실 수 있습니다. 마찬가지로 코드 또한 책을 97% 참고했습니다. 우리는 간단한 문장을 학습하여 어떠한 알파벳이 주어졌을 때, 해당 문장을 모델이 똑같이 생성할 수 있는지 코드로 구현해보겠습니다.

 

# library importing

import random

import numpy as np
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.init as init
n_hidden = 40 # 순환 신경망의 노드 수, 늘릴수록 오버피팅 됩니다.
lr = 0.01
epochs = 1000

string = 'hello pytorch. how long can a rnn cell remember' # 학습 할 문장의 길이는 49 입니다.

chars = 'abcdefghijklmnopqrstuvwxyz ?!.,:;01' # chars의 길이는 35 입니다.
#끝의 0과 1은 start token과 end token을 위한 것입니다.

char_list = [i for i in chars]
n_letters = len(char_list)
# 문장을 그대로 학습할 수 없기 때문에 one-hot 벡터로 encoding 합니다.
# 아래는 string을 받아 one-hot vector로 변환해주는 함수입니다.

def string_to_onehot(string):
    """
    definition: sentence to one-hot vector.
    
    """
    # 맨 앞과 뒤에 start token과 end token을 넣어줍니다.
    # 문장의 시작과 끝을 모델에게 알려주기 위한 장치입니다.
    start = np.zeros(shape=len(char_list), dtype=int)
    end = np.zeros(shape=len(char_list), dtype=int)
    start[-2] = 1
    end[-1] = 1
    
    for i in string:
        idx = char_list.index(i)
        zero = np.zeros(shape=n_letters, dtype=int)
        zero[idx] = 1
        start = np.vstack([start, zero])
    output = np.vstack([start, end])
    
    return output
# 마찬가지로 one-hot vector를 character로 변환해주는 함수입니다.

def onehot_to_word(onehot_1):
    onehot = torch.Tensor.numpy(onehot_1)
    
    return char_list[onehot.argmax()]
# 본격적으로 RNN 모듈을 보겠습니다.

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):   # (35, 40, 35)
        super(RNN, self).__init__()
        
        self.input_size = input_size                            # (35)
        self.hidden_size = hidden_size                          # (40)
        self.output_size = output_size                          # (35)
        
        self.i2h = nn.Linear(input_size, hidden_size)           # (in_features = 35, out_features = 40)
        # pytorch 공홈에서는 h2h 레이어가 없습니다.
        self.h2h = nn.Linear(hidden_size, hidden_size)          # (in_features = 40, out_features = 40)
        self.i2o = nn.Linear(hidden_size, output_size)          # (in_features = 40, out_features = 35)
        self.act_fn = nn.Tanh()
        
    def forward(self, input, hidden):
        # i2h레이어를 통과한 input과 h2h레이어를 통과한 hidden을 더하고
        # 활성화 함수 tanh를 통과시킵니다.
        # t+1에 전달될 hidden state가 완성됩니다.
        hidden = self.act_fn(self.i2h(input) + self.h2h(hidden)) # tanh(input + hidden)
        # pytorch 공홈에서는 element wise add 를 하지 않고 concat을 합니다.
        
        output = self.i2o(hidden)                                # output
        return output, hidden
    
    def init_hidden(self):
        return torch.zeros(1, self.hidden_size)                    # (1, 40)
rnn = RNN(n_letters, n_hidden, n_letters) # (35, 40, 35)
loss_func = nn.MSELoss()
optimizer = torch.optim.Adam(rnn.parameters(), lr=lr)

one_hot = torch.from_numpy(string_to_onehot(string)).type_as(torch.FloatTensor())
loss_list = []
for i in range(epochs):
    rnn.zero_grad()
    total_loss = 0
    hidden = rnn.init_hidden()                                  # initialize first hidden state
    
    for j in range(one_hot.size()[0]-1):
        input_ = one_hot[j:j+1]                                 # x(t), (1, 35)
        target = one_hot[j+1]                                   # x(t+1), (1, 35)
        
        output, hidden = rnn.forward(input_, hidden)            # return output and new_hidden
        loss = loss_func(output.view(-1), target.view(-1))      # computing loss
        total_loss += loss
        
    total_loss.backward()
    optimizer.step()
    loss_list.append(total_loss.item())
    
    if i % 10 == 0:
        print(total_loss)
start = torch.zeros(1, len(char_list))
start[:, -2] = 1

with torch.no_grad():
    hidden = rnn.init_hidden()
    input_ = start
    output_string = ""
    for i in range(len(string)):
        output, hidden = rnn.forward(input_, hidden)
        output_string += onehot_to_word(output.data)
        input_ = output
        
print(output_string)

# hello pytorch. how lo pytorch.ch aooo yonncgrnl

은닉 노드의 개수를 45개 수준으로 바꿀 경우 완전히 오버피팅 되어 똑같이 문장을 생성해 내는 것을 확인했습니다. 일부 구조가 파이토치 공홈의 코드와 다르기도 하지만, 그 또한 딥러닝 아키텍쳐를 짜는 재미죠.

 

 

혹시나, 이해하기 어려우시다면 위에서 추천드렸던 책을 조금 더 정독해보시는 게 도움이 되실 겁니다-!

 

감사합니다.

 

 

 

728x90

'데이터 사이언스 > 딥러닝' 카테고리의 다른 글

GRU 구조 쉽게 이해하기  (0) 2021.05.28
LSTM 구조 쉽게 이해하기  (0) 2021.05.26
RNN에는 들어가는 Data Input Shape  (0) 2021.05.24

댓글