본문 바로가기

딥러닝

13. RNN 코드실습

이 포스트는 '텐초의 파이토치 딥러닝 특'을 참고하여 만들어졌음!

 

 

 

RNN 개념도 물론 중요하지만 요것들을 실제로 적용하는 과정에서 무수한 궁금증이 생기기 때문에 한 번 코드실습을 해보는 시간을 가져보겠다. 결과가 가시적인 CNN과는 달리 RNN은 개념도 그렇고 코드 칠 때도 뭐가 뭔지 모르는 부분이 많았다.

 

텐초의 파이토치 딥러닝 특강의 '6장 넷플릭스 주가 예측하기 RNN으로 첫 시계열 학습'을 참고하여 RNN 코드 실습을 진행하였다. 

 

 

ㄱㄱ씽~

 

 

 

 

1. 데이터 가져오기

import pandas as pd
data = pd.read_csv("/content/train.csv") 
data.head()​

실습에 사용할 데이터는 https://www.kaggle.com/c/netflix-stock-prediction/data 에서 다운받으면 된다. csv 데이터를 불러와서 head를 확인해보면 다음과 같다.

 

이 데이터는 날짜(Date), 개장가(Open), 최고가(High), 최저가(Low), 거래량(Volume)으로 구성된 넷플릭스 주가 데이터셋이다. 주가 데이터이므로 시계열이고, Date열에 어느 일자의 데이터인지 순서대로 잘 나와있다. 시계열 모델에 데이터를 입력할 때 행을 섞지 않고 순서대로 넣어주기만 하면 데이터들의 순서정보가 보존된다. 따라서 Date 열은 삭제해도 괜춘

 

 

 

import matplotlib.pyplot as plt
data_used = data.iloc[:, 1:4] # 개장가, 최고가, 최저가 추가
data_used["Close"] = data["Close"] # 종가 추가
hist = data_used.hist()
plt.show()

다음으로 데이터들의 형태를 살펴보도록 한다.

 

 

위 그래프는 히스토그램으로, 어떤 값을 갖는 데이터 개수가 몇 개인지를 나타낸다. 예를 들어 'Open' 열에서 400 근처의 값을 갖는 것들이 대략 20개 정도임을 알 수 있다. 즉 위 4개 열에 대하여 데이터들이 대략 100~400 range의 값을 갖고 있는데, 이렇게 출력값의 범위가 크면 오차의 범위도 커지게 되고, 더불어 역전파에 사용하는 기울기 또한 커져 가중치 수렴이 어려워질 수 있다. 

 

따라서 데이터의 range를 [0, 1]으로 제한하기 위해 Min max Normalization을 사용해야한다.

 

 

 

 

 

 

2. 파이토치 데이터셋 구성하기

import numpy as np
from torch.utils.data.dataset import Dataset


class Netflix(Dataset):
  def __init__(self):
    self.csv = pd.read_csv("/content/train.csv")
    self.data = self.csv.iloc[:, 1:4].values # 첫 번째 열 Data는 사용하지 않음. 또한 Close는 target이므로 역시 제외
    self.data = self.data / np.max(self.data) # 0과 1 사이로 정규화

    self.label = data["Close"].values   # target도 지정해준 후 0과 1 사이로 정규화 수행  
    self.label = self.label / np.max(self.label)


  def __len__(self):
    return len(self.data) - 30 # 배치 개수 설정


  def __getitem__(self, i):
    data = self.data[i:i+30] # 입력 데이터 30일치 읽기
    label = self.label[i+30] # 종가 데이터 30일치 읽기
    return data, label

 

어떠한 파이토치 딥러닝 모델도 그렇듯, 항상 Dataset → Data loader → training → Predict 순으로 흘러간다. Dataset을 생성하는 과정은 위와 같다. 

 

  • __init__ : input, target data를 설정해준다.
  • __len__ : 총 input data의 길이를 설정해준다. 여기서 window size를 30으로 설정할 것이기 때문에 총 data size는 전체-30이 된다.
  • __getitem__ : 한 번의 iteration 마다 불러올 input, target data를 의미한다. 

 

 

 

window size가 의미하는 바를 파악해보자.

 

텐초의 파이토치 딥러닝 특강

 

window size는 한 배치 (한 iteration)에서 사용할 input data의 시간 크기를 의미한다. 이 예제에서는 30으로 설정했기 때문에 한 iteration에서 30개 시간에 대한 input data를 입력으로 사용한다. t=k~k+29를 input data로 사용하고, t=k+30을 target data로 사용한다. 이때 k는 N(전체 데이터 개수)-window size개 까지 반복된다.

 

window size를 정하는 것은 전적으로 사용자에게 달려있다. window size가 크다는 것은 한 iteration을 수행할 때 더 많은 영역을 관찰하겠다는 것이고, window size가 작으면 그 반대이다. 

 

window size가 크면 시계열 모델이 더 장기적인 패턴을 파악하는 데 도움을 줄 수 있지만 memory consumption 문제가 발생한다.

반대로 window size가 작은 경우 학습이 빠르고 memory를 크게 사용하지 않지만, 만약 sequence의 의존성이 긴 경우에는 장기적인 패턴을 파악하기 어렵다.

 

이러한 사항들을 잘 점검하여 효율높은 모델을 설계하는 것이 딥러닝 사용자의 역할이 되겠다. 

 

 

 

 

 

3. RNN 모델 정의

import torch
import torch.nn as nn

class RNN(nn.Module):
  def __init__(self):
    super(RNN, self).__init__()

    self.rnn = nn.RNN(input_size=3, hidden_size=8, num_layers=5,
    batch_first=True)

    self.fc1 = nn.Linear(in_features=240, out_features=64)
    self.fc2 = nn.Linear(in_features=64, out_features=1)
    self.relu = nn.ReLU() # 활성화 함수 정의


  def forward(self, x, h0):
    x, hn = self.rnn(x, h0) # RNN층의 출력
    # MLP층의 입력으로 사용되게 모양 변경
    x = torch.reshape(x, (x.shape[0], -1))
    # MLP층을 이용해 종가 예측
    x = self.fc1(x)
    x = self.relu(x)
    x = self.fc2(x)

    # 예측한 종가를 1차원 벡터로 표현
    x = torch.flatten(x)
    return x

 

Pytorch nn.Module에서 제공하는 RNN을 가져와서 사용했다. 

 

nn.RNN(input_size=3, hidden_size=8, num_layers=5, batch_first=True) 에서 입력 파라미터의 의미는 다음과 같다.

  • input_size : input data의 feature 개수를 의미한다. 여기서는 Open, High, Low 세 개의 열을 사용했으므로 반드시 3으로 입력해야 한다. (data 형태에 의존하는 값이다.)
  • hidden_size : input_size (3)차원의 데이터 $\vec{x}_t$가 입력되면 $\vec{h}_t=\text{tanh}(W_{hh}\vec{h}_{t-1}+W_{xh}\vec{x}_t)$를 이용하여 다음 계층 $\vec{h}_{t+1}$으로 들어간다. 즉 $W_{xh}\vec{x}_t$를 통해 input vector $\vec{x}_t$가 hidden dimension의 크기만큼 확장된다.
  • hidden_size = 8으로 설정했다면 $W_{xh}\vec{x}_t$은 8차원의 vector가 될 것이다. 쉽게 생각해서 hidden size는 학습에 사용할 파라미터 개수를 조정한다. 크게 설정하면 파라미터 차원이 확장되어 더 많은 패턴을 파악할 것이고, 파라미터를 너무 많이 사용하면 과적합이 발생한다.
  • num_layers : 얼마나 많은 RNN 층을 쌓아서 사용할 것인지를 의미한다. 실전에서는 RNN층을 하나만 사용하는 것이 아니라 여러 층을 쌓고 쌓아서 사용한다.

 

 

Understanding RNN Step by Step with PyTorch - Analytics Vidhya

 

위 그림에서 볼 수 있듯이 한 계층에서 가로 cell size는 window size가 결정한다.

 

 

 

RNN 모델을 거친 이후, 그 출력을 Linear 층을 이용하여 최종 출력이 한 개 열이 되도록 해야한다.

self.fc1 = nn.Linear(in_features=240, out_features=64)
self.fc2 = nn.Linear(in_features=64, out_features=1)
self.relu = nn.ReLU() # 활성화 함수 정의

중요한 것은 첫 번째 Linear 층에 들어가는 input size인데, 이는 RNN 모델의 출력 크기와 같다. 이때 RNN 모델의 출력크기는 어떻게 결정될까? 

 

 

RNN 모델을 사용하며 입출력 크기가 굉장히 헷갈렸는데, [PyTorch] RNN Layer 입출력 파라미터와 차원(shape) 이해 - 테디노트 (teddylee777.github.io) 이 포스트가 큰 도움이 됐다.

[PyTorch] RNN Layer 입출력 파라미터와 차원(shape) 이해 - 테디노트 (teddylee777.github.io)

 

먼저 Pytorch RNN 모듈의 두 출력은 다음과 같다.

  1. 첫 번째 출력 : 마지막 RNN 층의 hidden state = output
  2. 두 번째 출력 : 모든 RNN 층의 hidden state

일반적으로 우리는 첫 번째 출력을 사용하여 y값을 도출한다. 위 그림에서 output size = (batch_size, sequence_lenth, hidden_dim)으로 구성되어있음을 알 수 있는데, 여기서 sequence_length = window size = 30, hidden_dim = 8 이므로 한 batch 당 output size는 30x8 = 240임을 예측할 수 있다.

 

 

 

 

 

 

3. 모델 호출, 학습

import tqdm
from torch.optim.adam import Adam
from torch.utils.data.dataloader import DataLoader
device = "cuda" if torch.cuda.is_available() else "cpu"
model = RNN().to(device) # 모델 정의
dataset = Netflix() # 데이터셋 정의​
loader = DataLoader(dataset, batch_size=32)​
optim = Adam(params=model.parameters(), lr=0.0001)

 

이쯤되면 batch size와 window size가 굉장히 헷갈린다. 그래서 다시 한 번 짚고 간다.

 

  • window size : 순차열을 갖는 데이터를 사용할 때 한 번에 몇 개 time sequence를 이용할지를 나타낸다.
  • batch size : 한 batch는 window size 만큼의 input data를 갖는다. 한 batch에 담긴 data 크기는 [batch_size, window_size, input_size]이다.

 

for epoch in range(200):
  iterator = tqdm.tqdm(loader)

  for data, label in iterator:
    optim.zero_grad()
    # 1 초기 은닉 상태
    h0 = torch.zeros(5, data.shape[0], 8).to(device)
    # 2 모델의 예측값
    pred = model(data.type(torch.FloatTensor).to(device), h0)
    # 3 손실의 계산
    loss = nn.MSELoss()(pred,
    label.type(torch.FloatTensor).to(device))
    loss.backward() # 오차 역전파
    optim.step() # 최적화 진행
    iterator.set_description(f"epoch{epoch} loss:{loss.item()}")

 

iterator는 data loader이다. <for data, label in iterator> 에서 iterator는 한 batch의 data, label을 차례대로 넘겨준다. 다시 말하면 한 batch (32개) data들이 전부 RNN 모델에 입력된 후에 backward가 발생한다는 것이다.

 

각 batch에 담겨있는 data들이 전부 입력된 후에 학습이 진행된다고 생각하면 된다.

  • batch size가 크면 모델에 한 번에 입력해야 하는 data 개수가 큰 것을 의미한다. 따라서 메모리에 많은 데이터를 올려야 하는데, 메모리 자원이 한정되어있는 경우 이것이 불가능하므로 batch size를 줄여야 하는 상황이 올 수 있다.
  • 예를 들어 image data의 경우 데이터 하나하나가 비교적 헤비하기 때문에 한 batch에 많은 데이터를 담기 어렵다.
  • batch size를 줄이면 그 만큼 backward가 더 자주 일어나기 때문에 오히려 학습속도가 느려질 수 있다. (근데 메모리 자원이 부족하면 어차피 batch size 못 늘린다 ㅋ)

 

 

 

import matplotlib.pyplot as plt
loader = DataLoader(dataset, batch_size=1) # predict에서는 batch=1로 설정한다. 
preds = [] # 예측값들을 저장하는 리스트
total_loss = 0

for data, label in loader:
  h0 = torch.zeros(5, data.shape[0], 8).to(device) # 초기 은닉 상태 = 영벡터

  pred = model(data.type(torch.FloatTensor).to(device), h0)
  preds.append(pred.item()) # 예측값을 리스트에 추가

  # 손실 계산
  loss = nn.MSELoss()(pred,
  label.type(torch.FloatTensor).to(device))
  
  # 손실의 평균치 계산
  total_loss += loss/len(loader)

total_loss.item()​

 

 

 

 

여기까지 기초 RNN 실습 코드를 살펴보았다. 아주 기본적인 예제이지만 처음 접하면 어려울 수도 있을 것 같다. 오랜만에 코드 필사해서 재밌다~