순환 신경망, RNN

SSO·2024년 10월 22일

RNN의 기본 구조

  • 순환 신경망(Recurrent Neural Network, RNN)은 시계열 데이터나 순차적인 데이터를 처리하기 위해 설계된 신경망이다.
  • RNN은 이전 시간 단계의 정보를 현재 시간 단계로 전달해, 시퀀스 데이터의 패턴을 학습할 수 있다. 즉 '순서'의 중요성을 둔다고 보면 이해하기 쉽다.

(출처 : 내일배움캠프 강의)


RNN의 동작 원리

순환 구조

  • RNN의 기본요소는 순환하는, 셀이라한다. 각 시점의 입력과 숨겨진 상태를 결합 및 새로 숨겨진 상태와 출력을 만들어주는 것을 hidden state 순환, 즉 은닉 상태라고 한다.

  • RNN은 입력 데이터와 이전 시간 단계의 은닉 상태(hidden state)를 입력으로 받아, 현재 시간 단계의 은닉 상태를 출력한다.

  • 은닉 상태는 시퀀스의 정보를 저장하고, 다음 시간 단계로 전달된다.

    동작 원리

  • RNN은 시퀀스의 각 시간 단계에서 동일한 가중치를 업데이트하여, 시퀀스의 패턴을 학습한다.

  • 모든 시점에서 가중치가 동시에 업데이트한다. 마지막만 업데이트가 아닌 처음부터 갱신된다고 생각하면 된다.

  • 순전파(Forward Propagation)와 역전파(Backpropagation Through Time, BPTT)를 통해 가중치를 학습합니다.

  • cf) 역전파의 경우에는 출력을 실제 값과 비교 오류확인 거슬러 올라가면 가중치 업데이트한다.

RNN의 장점과 단점

  • 장점 : 순차적으로 진행되며, 이전 데이터을 기억한다.
  • 단점 : 구조가 너무 길고 복잡하며, 오래된 기억일수록 소실된다.
    이를 해결하기 위해 다음과 같이, LSTM과 GRU를 사용한다.

LSTM & GRU

RNN은 장기 의존성 문제(long-term dependency problem)를 겪을 수 있기에 이를 해결하기 위해 LSTM과 GRU가 개발되었다.

1. LSTM(Long Short-Term Memory)

  • LSTM은 셀 상태(cell state)와 게이트(gate) 구조를 도입, 장기 의존성을 효과적으로 학습가능하다.

  • LSTM은 입력 게이트(input gate), 출력 게이트(output gate), 망각 게이트(forget gate)를 사용하여 정보를 조절한다.

  • LSTM의 원리를 쉽게 풀어 쓴다면 다음과 같다.

      1. 셀에다가 게이트(정보를 선택적으로 저장하거나 삭제하는 조절하는 기능을 가진다.)를 더해줘서 셀의 각 시점에서 정보의 흐름을 조절한다. 이를 통해 정보를 장기적으로 유지가 가능하다.
      1. 입력 및 망각 데이터 출력을 바탕으로 셀 상태를 업데이트한다.
      1. 선택적으로 데이터 저장한다.
      1. 새로운 셀 상태와 입력 데이터 결합을 바탕으로 출력 게이트에 내보낸다.
      1. 이(1~4)를 순환한다.
  • 장점 : 긴 시퀀스에서도 정보를 잘 저장한다. (소실 적음)

  • 단점 : 기능이 많아서 복잡하고 학습이 오래걸림 그래서 데이터의 의존성이 높다.

 GRU (Gated Recurrent Unit)

  • GRU는 LSTM의 변형으로, 셀 상태 대신 은닉 상태(hidden state)만을 사용하여 구조를 단순화한다.
  • GRU는 업데이트 게이트(update gate)와 리셋 게이트(reset gate)를 사용하여 정보를 조절한다.
    • reset gate : 새로운 정보와 이전 정보 결합한다.
    • update gate : 상태 업데이트, 정보를 선택적으로 기억한다.
  • LSTM과 다르게 셀 상태와 숨겨진 상태를 즉 hidden state를 통합해서 reset gate와 update gate를 활용해 단일 상태로 유지한다. 이로인해 중요하지 않는 정보는 삭제하고 중요한 정보는 유지하게된다.

LSTM과 GRU도 순전파 역전파 동일 적용한다.
즉 여기 게이트들은 함수들로 이루어져있고, 함수들을 계산할 때 가중치 조절해서 함수의 최소가 되는 결과로 오차함수를 줄이게되면 최적화 상태
이를 역전파(Backpropagation)이라고 한다.

LSTM과 GRU의 차이점

  • LSTM은 셀 상태와 은닉 상태를 모두 사용하며, 더 복잡한 게이트 구조를 가진다.
  • GRU는 은닉 상태만을 사용하며, 더 간단한 게이트 구조를 가진다. 따라서 계산 비용이 적고, 학습이 빠를 수 있다.

RNN을 이용한 시계열 데이터 처리 방법

RNN은 시계열 데이터나 순차적인 데이터를 처리하는 데 적합하다. 예를 들어, 주식 가격 예측, 날씨 예측, 텍스트 생성 등이 있다.

  1. 데이터 전처리:
    • 시계열 데이터를 적절한 형태로 변환하고, 정규화(normalization)한다.
    • 입력 시퀀스와 출력 시퀀스를 정의한다.
  2. 모델 구축:
    • RNN, LSTM, GRU 등의 모델을 정의한다.
    • 입력 크기, 은닉 상태 크기, 출력 크기 등을 설정한다.
  3. 모델 학습:
    • 손실 함수와 최적화 알고리즘을 정의한다.
    • 순전파와 역전파를 통해 모델을 학습시킨다.
  4. 모델 평가:
    • 테스트 데이터를 사용하여 모델의 성능을 평가한다.

예제

PyTorch를 사용하여 간단한 RNN과 LSTM 모델을 구축하고, 시계열 데이터를 예측해보려고한다. 예제로는 Sine 파형 데이터를 사용할 것.

<PyTorch 및 필요한 라이브러리 임포트>

# 불러오기
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
  

<데이터셋 생성 및 전처리>

# Sine 파형 데이터 생성
# ~모양이 나오는 sine 함수에서 값이 중복으로 나오니까 시퀀스가 필요
def create_sine_wave_data(seq_length, num_samples):
    X = []
    y = []
    for _ in range(num_samples):
        start = np.random.rand()
        x = np.linspace(start, start + 2 * np.pi, seq_length)
        X.append(np.sin(x))
        y.append(np.sin(x + 0.1))
    return np.array(X), np.array(y)

seq_length = 50
num_samples = 1000
X, y = create_sine_wave_data(seq_length, num_samples)

# 데이터셋을 PyTorch 텐서(tensor)로 변환
# PyTouch에서 텐서(tensor)는 기울기를 계산할 수 있는 자료 구조라서 텐서로 변환해주는게 맞음 (가중치를 위해)
X = torch.tensor(X, dtype=torch.float32).unsqueeze(-1)
y = torch.tensor(y, dtype=torch.float32).unsqueeze(-1)

<간단한 RNN 모델 정의>

  • nn.RNN: 순환 신경망(RNN) 층을 정의한다.
    • nn.RNN(input_size, hidden_size, batch_first)는 입력 크기, 은닉 상태 크기, 배치 차원을  번째로 설정한다.\scriptsize\textsf{nn.RNN(input\_size, hidden\_size, batch\_first)는 입력 크기, 은닉 상태 크기, 배치 차원을 첫 번째로 설정한다.}
  • nn.Linear: 선형 변환을 적용하는 완전 연결(fully connected) 레이어를 정의한다.
    • nn.Linear(in_features, out_features)는 입력 특징의 수와 출력 특징의 수를 지정한다.\scriptsize\textsf{nn.Linear(in\_features, out\_features)는 입력 특징의 수와 출력 특징의 수를 지정한다.}
  • 초기 은닉 상태를 설정하는 이유 : 첫 값은 hidden state가 없기 때문에 초기 은닉 상태 설정해줘야한다.
class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleRNN, self).__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(1, x.size(0), hidden_size)  # 초기 은닉 상태
        out, _ = self.rnn(x, h0)
        out = self.fc(out[:, -1, :])  # 마지막 시간 단계의 출력
        return out

input_size = 1
hidden_size = 32
output_size = 1
model = SimpleRNN(input_size, hidden_size, output_size)

<간단한 LSTM 모델 정의>

  • nn.LSTM: 장단기 메모리(LSTM) 층을 정의한다.
    • nn.LSTM(input_size, hidden_size, batch_first)는 입력 크기, 은닉 상태 크기, 배치 차원을  번째로 설정한다.\scriptsize\textsf{nn.LSTM(input\_size, hidden\_size, batch\_first)는 입력 크기, 은닉 상태 크기, 배치 차원을 첫 번째로 설정한다.}
 class SimpleLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleLSTM, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(1, x.size(0), hidden_size)  # 초기 은닉 상태
        c0 = torch.zeros(1, x.size(0), hidden_size)  # 초기 셀 상태
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])  # 마지막 시간 단계의 출력
        return out

model = SimpleLSTM(input_size, hidden_size, output_size)

<모델 학습>

# 손실 함수와 최적화 알고리즘 정의
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# 모델 학습
num_epochs = 100
for epoch in range(num_epochs):
    outputs = model(X)
    optimizer.zero_grad()
    loss = criterion(outputs, y)
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')

print('Finished Training')
  • nn.MSELoss: 평균 제곱 오차(MSE) 손실 함수를 정의한다.
  • optim.Adam: 여태까지 사용했던 것은 SGD 확률적 경사 하강법인데, 여기서는 Adam 적응형 모멘트 추정, 경사 하강법 최적화 알고리즘 중 하나이다. lr은 학습률을 지정한다.
  • optimizer.zero_grad(): 이전 단계에서 계산된 기울기를 초기화한다.
  • loss.backward(): 역전파를 통해 기울기를 계산한다.
  • optimizer.step(): 계산된 기울기를 바탕으로 가중치를 업데이트한다.

<모델 평가 및 시각화>

# 모델 평가
model.eval()
with torch.no_grad():
    predicted = model(X).detach().numpy()

# 시각화
plt.figure(figsize=(10, 5))
plt.plot(y.numpy().flatten(), label='True')
plt.plot(predicted.flatten(), label='Predicted')
plt.legend()
plt.show()
  • model.eval(): 모델을 평가 모드로 전환한다.
  • torch.no_grad(): 평가 단계에서는 기울기를 계산할 필요가 없으므로, 이를 비활성화하여 메모리 사용을 줄인다.
  • detach(): 텐서를 계산 그래프에서 분리한다.

상황에 따라 사용하는 모델구조가 다르다. 하지만 순환 작용 및 보다 값을 빠르고 효율적으로 적용하는 모델을 센스있게 찾아내는 것이 중요하다는 것을 다시 알게 되는 공부였다.

profile
개발자로 한걸음씩!

0개의 댓글