ML,DL

RNN, LSTM 이해하기 (PyTorch로 구현한 코드 포함)

yeeunnnn 2026. 3. 11. 17:42

🚨해당 게시글에 포함된 이미지 중 출처가 쓰여있지 않은 이미지는 모두 직접 그렸습니다.

 

CNN과 같은 다른 신경망들은 은닉층에서 활성화 함수를 지나 출력층 방향으로 향하는 feed forward network이다.

따라서 시계열 데이터같은 sequential 한 데이터를 처리하기 위해서는 전체 시퀀스를 하나의 벡터로 넣어줘야한다는 문제가 발생한다.

이를 해결하기 위해서 RNN이라는 시퀀스 모델이 등장했다.

1. RNN(Recurrent Neural Network) : 데이터의 순서(Sequence)를 기억하고 처리하는 딥러닝 모델

RNN의 핵심 아이디어를 수식으로 나타내면 다음과 같다.

그림으로 나타내면 아래의 그림과 같다.


위와 같이 RNN은 은닉층의 노드에서 활성화 함수를 통해 나온 결과값을 출력층 방향으로도 보내면서, 다시 은닉층 노드의 다음 계산의 입력으로 보낸다.

 

RNN은 다양한 용도로 사용할 수 있으며 one-to-many, many-to-one, mant-to-many와 같이 나눌 수 있게 된다.

이때 출력층의 결과값을 계산하기 위한 활성화 함수는 푸는 문제에 따라 다르다.

만약 이진 분류를 해야하는 경우라면 로지스틱 회귀를 사용하여 시그모이드 함수를, 다중 클래스 분류라면 소프트맥스 회귀를 사용하여 소프트맥스 함수를 사용할 수 있다.

https://wikidocs.net/22886

1.1. RNN 코드 (Pytorch)

RNN을 직접 구현하면 아래와 같다. (이때는 b가 없다고 가정)

가장 먼저 RNN cell을 구현하자.

import torch
import torch.nn as nn

class RNNCell(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.W_ih = nn.Linear(input_size, hidden_size)
        self.W_hh = nn.Linear(hidden_size, hidden_size)

    def forward(self, x, h_prev):
        h = torch.tanh(self.W_ih(x) + self.W_hh(h_prev))
        return h

 

RNN 전체를 구현하면 다음과 같다.

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.hidden_size = hidden_size
        self.rnn_cell = RNNCell(input_size, hidden_size)

    def forward(self, x):
        seq_len, input_size = x.shape
        h = torch.zeros(self.hidden_size)
        outputs = []
        for t in range(seq_len):
            x_t = x[t]
            h = self.rnn_cell(x_t, h)
            outputs.append(h)
        outputs = torch.stack(outputs)
        return outputs, h

테스트 코드는 다음과 같다.

model = RNN(input_size=10, hidden_size=20)

x = torch.randn(5,10)

output, hidden = model(x)

print(output.shape) # torch.Size([5,20])
print(hidden.shape) # torch.Size([20])

PyTorch의 nn.RNN을 통한 구현은 아래와 같다. 이때는 문장이 32개라고 가정했다.

import torch
import torch.nn as nn

rnn = nn.RNN(
    input_size=10, # 입력 feature 차원
    hidden_size=20, # hidden state 크기
    num_layers=1, # RNN layer 개수
    batch_first=True # 입력 차원 순서
)

x = torch.randn(32, 5, 10)  # (batch, seq, feature)
# 32개의 문장, 각 문장 길이는 5, 각 단어의 feature는 10

output, hidden = rnn(x)

print(output.shape)  # (32,5,20)
print(hidden.shape)  # (1,32,20)

1.2. RNN의 학습 방법

RNN은 일반적인 역전파를 시간축으로 확장한 Backpropagation Through Time(BPTT)으로 학습한다.

1. forward pass → 2. loss 계산 → 3. 시간 방향으로 역전파(h3→h2→h1)

1.3. RNN의 문제점

문장이 길어지거나 시계열이 길어지면 앞 부분의 정보를 잊어버린다.(long-term dependency)

1. Vanishing Gradient : 시간이 길어질수록 Gradient가 점점 0으로 수렴하여 앞쪽의 데이터의 영향력이 사라진다.

2. Exploding Gradient : gradient가 너무 커져 학습이 불안정해진다.

→ 이 문제를 해결하기 위해 LSTM이 등장하였다.

2. LSTM(Long Short-Term Memory) : 

LSTM은 가장 유명한 RNN 개선 모델이며 forget gate, input gate, output gate를 가지고 long-term dependency를 해결한다.

memory cell을 추가하여 기억을 저장하는 통로를 따로 만들고, 3개의 gate를 통해서 무엇을 잊고, 저장하고, 출력할지 결정한다.

https://wikidocs.net/22888

위 그림은 LSTM의 전체적인 내부의 모습이다. (σ는 시그모이드 함수를, tanh는 하이퍼볼릭탄젠트 함수를 의미)

  • 입력 : x_t(현재 시점에서의 입력 데이터), h_t-1(이전 시점 cell의 출력 데이터), C_t-1(이전 Cell의 정보)
  • 출력 : h_t(현재 시점에서의 출력 데이터), C_t(현재 시점에서의 Cell 정보)

2.1. Cell state

https://wikidocs.net/22888

C_t는 과거의 정보를 얼만큼 기억하고 현재의 데이터를 얼만큼 더할 것인가를 조절한다.

얼마큼의 정도는 연결되어있는 2개의 Gate인 Foreget Gate와 Input Gate가 결정한다.

2.2. Input gate

https://wikidocs.net/22888

Input gate는 현재 정보를 기억하기 위한 게이트이다.

아래의 수식으로 계산되며 i_t는 얼만큼의 정보를 줄 것인지의 정도를 결정하고, g_t는 Cell state에 더해질 후보 값을 만든다.

엄밀히 말하면 시그모이드 부분이 input gate이지만, 통상적으로는 tanh 부분까지 합쳐 부른다.

2.3. Forget gate

https://wikidocs.net/22888

Forget gate는 기억을 삭제하기 위한 게이트이다.

f_t는 시그모이드 함수를 통과한 값이므로 0에서 1사이의 값이 나오게 된다.

이때 0이라면 이전 시점의 cell state는 0과 곱해져 모두 잊게 되며, 1이라면 모두 기억하게 된다.

2.4. Output gate

https://wikidocs.net/22888

Output gate는 현재 시점의 은닉 상태인 h_t를 결정한다. 

셀 상태의 값이 하이퍼볼릭탄젠트 함수를 지나 -1에서 1사이의 값이 되고, 해당 값은 출력 게이트의 값과 연산되며 값이 걸러진다.

얼마큼의 정도는 연결되어있는 2개의 Gate인 Foreget Gate와 Input Gate가 결정한다.

2.5. LSTM 코드 (PyTorch)

LSTM을 직접 구현하면 아래와 같다. (이때는 b가 없다고 가정)

가장 먼저 LSTM cell을 구현하자.

import torch
import torch.nn as nn

class LSTMCell(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # x와 h를 합쳐서 gate 계산
        self.W_f = nn.Linear(input_size + hidden_size, hidden_size)
        self.W_i = nn.Linear(input_size + hidden_size, hidden_size)
        self.W_c = nn.Linear(input_size + hidden_size, hidden_size)
        self.W_o = nn.Linear(input_size + hidden_size, hidden_size)

    def forward(self, x, h_prev, c_prev):
        combined = torch.cat((x, h_prev), dim=0)
        f_t = torch.sigmoid(self.W_f(combined))
        i_t = torch.sigmoid(self.W_i(combined))
        
        c_tilde = torch.tanh(self.W_c(combined))
        
        c_t = f_t * c_prev + i_t * c_tilde
        o_t = torch.sigmoid(self.W_o(combined))
        h_t = o_t * torch.tanh(c_t)

        return h_t, c_t

LSTM 전체를 구현하면 다음과 같다.

class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.hidden_size = hidden_size
        self.cell = LSTMCell(input_size, hidden_size)

    def forward(self, x):
        seq_len, input_size = x.shape

        h = torch.zeros(self.hidden_size)
        c = torch.zeros(self.hidden_size)

        outputs = []

        for t in range(seq_len):
            x_t = x[t]
            h, c = self.cell(x_t, h, c)
            outputs.append(h)

        outputs = torch.stack(outputs)

        return outputs, (h, c)

테스트 코드는 다음과 같다.

model = LSTM(input_size=10, hidden_size=20)

x = torch.randn(5,10)

output, (h, c) = model(x)

print(output.shape) # torch.Size([5,20])
print(h.shape) # torch.Size([20])
print(c.shape) # torch.Size([20])

PyTorch의 nn.RNN을 통한 구현은 아래와 같다. 이때는 문장이 32개라고 가정했다.

import torch
import torch.nn as nn

lstm = nn.LSTM(
    input_size=10,
    hidden_size=20,
    num_layers=1,
    batch_first=True
)

x = torch.randn(32,5,10)

output, (h_n, c_n) = lstm(x)

2.6. LSTM의 문제점

LSTM cell이 복잡하기 때문에 연산이 오래 걸린다는 단점이 존재하게 된다.

이를 해결하기 위해 LSTM을 단순하게 만든 GRU가 제안되었다.