RNN(Recurrent Neural Network)은 시퀀스 데이터를 처리하는데 특화 되어 있어 시계열 데이터, 자연어 처리, 음성 인식 등에 순차적으로 연결된 데이터를 모델링 할 때 주로 사용된다. 기존에 신경망과 달리 피드백 루프로 순환 구조를 가져 과거의 정보를 기억하고 시간 순서가 있는 데이터에 대한 모델링을 가능하게 하였다.
RNN이 자연어 처리시에 어떻게 사용되는지를 알아보기 위해 문장 분류를 하는 과정을 예시로 들어 단계 별로 살펴 본다.
“I love this movie” 라는 문장에 대해 토큰화(Tokenization), 임베딩(Embedding)을 거쳐 다음과 같은 vector를 얻었다고 가정한다.
| 단어 | vector () |
|---|---|
| "I" | [0.1, 0.2, ..., 0.05] |
| "love" | [0.3, 0.7, ..., 0.2] |
| "this" | [0.05, 0.1, ..., 0.4] |
| "movie" | [0.6, 0.3, ..., 0.7] |
시점 에 대해서 RNN 셀의 동작은 다음과 같다.

예를 들어, 시점에서는 “love”에 해당하는 [0.3, 0.7, … , 0.2] 벡터가 로 입력된다. 그리고 시점에서 “I”에 해당하는 [0.1, 0.2, … , 0.05] 벡터 값으로 도출된 에 값과 그 가중치가 시점에서 사용되어 업데이트 된다.
그리고, 출력 마지막 시점 의 은닉 상태 를 사용해 감정을 예측한다.
Fully Connected Layer와 분류 문제에 적합 한 를 사용하여 클래스를 출력한다.
분류 문제 이기에 를 사용한다. Loss 값을 구하고 역전파 함으로써 가중치를 업데이트 하는 방식으로 학습이 이루어진다. 이는, 기존에 여러 신경망 모델과 동일한 방식이다.
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.vocab import GloVe
from torchtext.data.utils import get_tokenizer
# 하이퍼파라미터 설정
input_size = 50 # 단어 임베딩 차원 (GloVe 사용)
hidden_size = 128 # 은닉 상태 크기
output_size = 2 # 긍정/부정 클래스
num_layers = 1 # RNN 레이어 수
batch_size = 1 # 한 번에 처리할 문장 개수
seq_length = 4 # 입력 시퀀스 길이 (단어 수)
learning_rate = 0.001
# 1. RNN 모델 정의
class SentimentRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers):
super(SentimentRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
rnn_out, hidden = self.rnn(x) # RNN 출력 계산
output = self.fc(rnn_out[:, -1, :]) # 마지막 시점의 은닉 상태 사용
return output
# 2. 데이터 준비: "I love this movie" -> 임베딩 벡터 변환
tokenizer = get_tokenizer("basic_english")
glove = GloVe(name="6B", dim=input_size) # GloVe 임베딩 로드 (50차원)
sentence = "I love this movie"
tokens = tokenizer(sentence) # 토큰화
print(f"Tokens: {tokens}")
# 각 단어를 GloVe 벡터로 변환
embedded_sentence = torch.stack([glove[token] for token in tokens])
print(f"Embedded Sentence Shape: {embedded_sentence.shape}") # (4, 50)
# 배치 형태로 변환 (batch_size x seq_length x input_size)
x = embedded_sentence.unsqueeze(0)
print(f"Input Shape (Batch): {x.shape}") # (1, 4, 50)
# 정답 라벨 (긍정: 1)
y = torch.tensor([1]) # 배치 크기 1, 라벨 1
# 3. 모델 초기화
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SentimentRNN(input_size, hidden_size, output_size, num_layers).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 데이터도 GPU로 이동
x, y = x.to(device), y.to(device)
# 4. 학습
model.train()
num_epochs = 10
for epoch in range(num_epochs):
optimizer.zero_grad()
outputs = model(x) # 예측값
loss = criterion(outputs, y) # 손실 계산
loss.backward() # 역전파
optimizer.step() # 가중치 업데이트
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
# 5. 예측
model.eval()
with torch.no_grad():
outputs = model(x)
predicted_class = torch.argmax(outputs, dim=1).item()
print(f"Predicted Class: {predicted_class} (1: Positive, 0: Negative)")
입력 데이터에 길이가 길어져 RNN에 시간이 길어질수록 초기 단계의 정보가 제대로 전달되지 못하는 기울기 소실(Vanishing Gradient) 문제와 은닉 상태에 계속적인 반복 계산으로 학습 시간이 길어지고 기울기 폭발(Exploding Gradient) 문제가 발생할 수 있었다.
이러한 한계점을 극복하고자 LSTM(Long Short-Term Memory), GRU(Gated Recurrent Unit)와 같은 모델이 나타났다.
기울기 소실 (Vanishing Gradient)
Vanishing Gradient는 신경망의 역전파 과정에서 기울기가 점점 작아져서 거의 0에 수렴하는 현상을 말한다. 이러한 현상은 모델의 가중치 업데이트를 효과적이지 못하게 만들며 결과적으로 학습을 정체 시키거나 매우 느려지게 만든다.