딥러닝 파이토치 강좌, 양방향 RNN 구조

딥러닝 기술의 발전으로 인해 시퀀스 데이터 처리에 대한 요구가 늘어나고 있습니다. RNN(순환 신경망)은 이러한 시퀀스 데이터를 처리하는 대표적인 구조 중 하나입니다. 이번 글에서는 양방향 RNN(Bi-directional RNN)의 개념과 이를 파이토치(PyTorch)를 이용해 구현하는 방법에 대해 자세히 알아보겠습니다.

1. RNN(순환 신경망) 이해하기

RNN은 순환 구조를 가진 신경망으로, 시퀀스 데이터(예: 텍스트, 시간 시계열)를 처리할 수 있는 능력을 가지고 있습니다. 일반적인 신경망은 입력을 한 번만 받고 출력을 내보내는 반면, RNN은 이전의 상태를 기억하고 이를 이용해 현재의 상태를 업데이트합니다. 이로 인해 RNN은 시퀀스의 시간적 의존성을 학습할 수 있습니다.

1.1. RNN의 기본 구조

RNN의 기본 구조는 기본적인 뉴런의 구조와 비슷하지만, 시간에 따라 반복적으로 연결된 구조를 가집니다. 아래는 단일 RNN 셀의 정보 흐름을 나타낸 것입니다:

     h(t-1)
      |
      v
     (W_hh)
      |
     +---------+
     |         |
    input --> (tanh) --> h(t)
     |         |
     +---------+

이 구조에서, h(t-1)는 이전 시점의 은닉 상태(hidden state)이며, 이 값을 이용해 현재 시점의 은닉 상태 h(t)를 계산합니다. 여기서 가중치 W_hh는 이전 은닉 상태를 현재 은닉 상태로 변환하는 역할을 수행합니다.

1.2. RNN의 한계

RNN은 긴 시퀀스를 처리할 때 “기억의 한계”라는 문제에 직면합니다. 특히 긴 시퀀스에서 초기 입력 정보가 소실될 수 있습니다. 이를 해결하기 위해 LSTM(Long Short-Term Memory)이나 GRU(Gated Recurrent Unit)와 같은 구조가 개발되었습니다.

2. 양방향 RNN(Bi-directional RNN)

양방향 RNN은 시퀀스를 두 방향으로 처리할 수 있는 구조입니다. 즉, 과거 방향(앞에서 뒤로)과 미래 방향(뒤에서 앞으로) 모두에서 정보를 얻을 수 있습니다. 이러한 구조는 다음과 같이 동작합니다.

2.1. 양방향 RNN의 기본 아이디어

양방향 RNN은 두 개의 RNN 계층을 사용합니다. 한 계층은 입력 시퀀스를 정방향으로 처리하고, 다른 계층은 입력 시퀀스를 역방향으로 처리합니다. 아래는 양방향 RNN의 구조를 간단히 나타낸 그림입니다:

  Forward     Backward
   RNN         RNN
     |           |
    h(t-1)   h(t+1)
       \    +--> (merge) --> h(t)
        \   |
         h(t)

정방향 RNN과 역방향 RNN은 동시에 입력을 처리하고, 이 두 은닉 상태를 결합하여 최종 출력값을 만듭니다. 이렇게 함으로써 RNN은 시퀀스의 모든 정보를 보다 효과적으로 활용할 수 있습니다.

3. 파이토치로 양방향 RNN 구현하기

이제 양방향 RNN을 파이토치(Pytorch)를 이용해 구현해 보겠습니다. 이번 예제에서는 데이터로서 임의의 시퀀스를 사용하고, 양방향 RNN을 통해 다음 문자를 예측하는 모델을 만들어 봅니다.

3.1. 필수 라이브러리 임포트

python
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

3.2. 데이터 준비하기

입력 데이터는 간단한 문자열을 사용하여 이 문자열의 다음 문자를 예측하도록 하겠습니다. 문자열 데이터는 연속적으로 나타나는 문자의 시퀀스로 변환됩니다. 다음은 간단한 데이터 준비 코드입니다:

python
# 데이터와 문자 집합 설정
data = "hello deep learning with pytorch"
chars = sorted(list(set(data)))
char_to_index = {ch: ix for ix, ch in enumerate(chars)}
index_to_char = {ix: ch for ix, ch in enumerate(chars)}

# 하이퍼파라미터
seq_length = 5
input_size = len(chars)
hidden_size = 128
num_layers = 2
output_size = len(chars)

# 데이터셋 생성
inputs = []
targets = []
for i in range(len(data) - seq_length):
    inputs.append([char_to_index[ch] for ch in data[i:i + seq_length]])
    targets.append(char_to_index[data[i + seq_length]])

inputs = np.array(inputs)
targets = np.array(targets)

3.3. 양방향 RNN 모델 정의하기

이제 양방향 RNN 모델을 정의해 보겠습니다. 파이토치에서는 nn.RNN() 또는 nn.LSTM() 클래스를 사용하여 RNN 계층을 만들 수 있습니다. 여기서는 nn.RNN()을 사용합니다:

python
class BiRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers):
        super(BiRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # 양방향 RNN 계층
        self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
        self.fc = nn.Linear(hidden_size * 2, output_size) # 두 방향을 고려하여 hidden_size * 2
        
    def forward(self, x):
        # RNN에 데이터를 통과시킵니다.
        out, _ = self.rnn(x)
        # 마지막 시점의 출력을 가져옵니다.
        out = out[:, -1, :]   
        
        # 최종 출력을 생성합니다.
        out = self.fc(out)
        return out

3.4. 모델 학습하기

모델을 정의했으니, 이제 학습 과정을 구현해 보겠습니다. 파이토치의 DataLoader를 사용하여 배치 처리를 지원하고, 손실 함수로 CrossEntropyLoss를 사용합니다:

python
# 하이퍼파라미터 설정
num_epochs = 200
batch_size = 10
learning_rate = 0.01

# 모델, 손실 함수 및 옵티마이저 초기화
model = BiRNN(input_size, hidden_size, output_size, num_layers)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 학습 루프
for epoch in range(num_epochs):
    # 데이터를 텐서로 변환
    x_batch = torch.tensor(inputs, dtype=torch.float32).view(-1, seq_length, input_size)
    y_batch = torch.tensor(targets, dtype=torch.long)

    # 경량화 방지
    model.zero_grad()

    # 모델 예측
    outputs = model(x_batch)
    
    # 손실 계산
    loss = criterion(outputs, y_batch)
    
    # 역전파 및 가중치 업데이트
    loss.backward()
    optimizer.step()

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

3.5. 모델 평가하기

모델을 학습한 후에는 테스트 데이터를 통해 모델을 평가하고, 입력 시퀀스에 대해 다음 문자를 예측하는 방법을 알아보겠습니다:

python
def predict_next_char(model, input_seq):
    model.eval()  # 평가 모드로 전환
    with torch.no_grad():
        input_tensor = torch.tensor([[char_to_index[ch] for ch in input_seq]], dtype=torch.float32)
        input_tensor = input_tensor.view(-1, seq_length, input_size)
        output = model(input_tensor)
        _, predicted_index = torch.max(output, 1)
    return index_to_char[predicted_index.item()]

# 예측 테스트
test_seq = "hello"
predicted_char = predict_next_char(model, test_seq)
print(f'입력 시퀀스: {test_seq} 다음 문자 예측: {predicted_char}')

4. 결론

이번 글에서는 양방향 RNN의 개념과 이를 파이토치를 사용하여 구현하는 방법에 대해 자세히 알아보았습니다. 양방향 RNN은 과거와 미래의 정보를 동시에 활용할 수 있는 강력한 구조로, 자연어 처리(NLP)와 같은 다양한 시퀀스 데이터 처리에 있어 유용하게 활용될 수 있습니다. 이러한 RNN 구조를 통하여 시퀀스 데이터의 패턴과 의존성을 더 효과적으로 학습할 수 있습니다.

앞으로도 다양한 딥러닝 기법과 구조에 대해 지속적으로 탐구할 것이며, 이 글이 여러분의 딥러닝 학습에 많은 도움이 되길 바랍니다!