안녕하세요 Oneclick AI 입니다!!
오늘은, RNN(순환 신경망, Recurrent Neural Network) 모델에 대해서 알아보는 시간을 가져볼까 합니다.
딥러닝이 문장, 음성, 주가 예측과 같은 순서가 있는 데이터(Sequential Data)를 다룰 수 있게 된 것은 전적으로 RNN 덕분입니다.
단순한 신경망이 '순서'라는 개념을 이해하지 못하는 반면,
RNN은 마치 사람처럼 시간의 흐름에 따라 정보를 기억하고, 다음을 예측할 수 있는 능력을 갖추고 있습니다.
오늘은 이 RNN이라는 신경망이 어떻게 과거의 정보를 기억하며 작동하는지,
그리고 어떻게 문장의 숨겨진 문맥과 의미를 파악할 수 있는지 알아봅시다.
목차
- RNN 핵심 원리 파악하기
- 왜 순차 데이터에 RNN을 사용해야만 할까?
- RNN의 심장 : 순환 구조와 은닉 상태의 역학
- RNN을 시간에 따라 펼쳐보기
- RNN의 주요 구성 요소 상세 분석
- 아키텍처를 통한 내부 코드 들여다 보기
- Keras로 구현한 RNN 모델 아키텍처
- model.summary()로 구조 확인하기
- 직접 RNN 구현해 보기
- 1단계 : 데이터 로드 및 전처리
- 2단계 : 모델 컴파일
- 3단계 : 모델 학습 및 평가
- 4단계 : 학습된 모델 저장 및 재사용
- 5단계 : 나만의 문장으로 모델 테스트하기
- 나만의 RNN 모델 업그레이드하기
- 기초 체력 훈련 : 하이퍼파라미터 튜닝
- RNN의 치명적 약점 : 장기 의존성 문제
- 기억력 강화 : LSTM과 GRU의 등장
- 과거와 미래를 동시에 : 양방향 RNN
- 결론
1. RNN 핵심원리 파악하기
가장 먼저, RNN이 왜 순차적인 데이터를 이해하는 데 필수적인 도구인지 그 근본적인 이유부터 살펴보겠습니다.
왜 순차 데이터에 RNN을 사용할까?? with MLP, CNN의 한계
가장 기본적인 신경망인 MLP(다층 퍼셉트론)에 "나는 학교에 간다"라는 문장을 입력한다고 상상해 봅시다.
MLP는 각 단어를 독립적인 특징으로 보기 때문에, "학교에 나는 간다"라는 문장과 거의 동일하게 받아들입니다.
단어의 '순서'가 가진 중요한 의미, 즉 문맥을 완전히 잃어버리는 것입니다.
이미지 처리에 특화된 CNN 역시 마찬가지입니다.
CNN은 공간적인 특징(픽셀 주변 관계)을 추출하는 데는 뛰어나지만, 시간적인 순서나 흐름을 파악하도록 설계되지 않았습니다.
반면, RNN은 설계 자체가 '순서'를 기억하기 위해 만들어졌습니다.
이전 단계의 처리 결과를 다음 단계의 입력으로 재사용하는 '순환' 구조를 통해, 마치 우리가 문장을 앞에서부터 차례로 읽으며 내용을 머릿속에 축적하는 것과 같은 방식으로 작동합니다.
RNN의 심장 : 순환 구조와 은닉 상태의 역학
RNN의 핵심 아이디어는 바로 순환 구조(Recurrent Structure)와 은닉 상태(Hidden State) 입니다.
순환 구조(Recurrent Structure)
신경망 내부에 '루프'가 존재하여, 정보가 계속해서 순환할 수 있는 구조를 말합니다.
각 타임스텝 $t$에서 모델은 입력 $x_t$와 이전 타임스텝의 정보 요약본인 $h_{t-1}$을 함께 받아 처리합니다.은닉 상태(Hidden State, $h_t$)
은닉 상태는 '메모리' 또는 '문맥 벡터'라고 불리며, RNN의 모든 것을 담고 있습니다.
특정 타임스텝 t에서의 은닉 상태 $h_t$는 다음과 같은 수식으로 계산됩니다.
$h_t=tanh(W_hhh_t−1+W_xhx_t+b_h)$여기서 $W_hh$, $W_{xh}$는 학습을 통해 최적화되는 가중치 행렬이며, $b_h$는 편향입니다.
중요한 점은 모든 타임스텝에서 동일한 가중치($W$)와 편향($b$)이 공유된다는 것입니다.
이는 모델이 시간과 관계없이 일관된 패턴을 학습하게 하며, 파라미터 수를 크게 줄여줍니다.
tanh와 같은 활성화 함수는 계산된 값을 특정 범위(-1에서 1 사이)로 압축하는 역할을 합니다.
RNN을 시간에 따라 펼쳐보기
아래 그림처럼 시간에 따라 네트워크를 길게 펼쳐서 표현하면, 쉽게 이해할 수 있습니다.
시간 흐름 ───▶
입력 시퀀스: x₁ x₂ x₃ ... xₜ
↓ ↓ ↓ ↓
┌────┐ ┌────┐ ┌────┐ ... ┌────┐
h₀ ───▶ │RNN │▶│RNN │▶│RNN │ ▶ ... ▶│RNN │
└────┘ └────┘ └────┘ └────┘
│ │ │ │
▼ ▼ ▼ ▼
h₁ h₂ h₃ hₜ
RNN은 내부적으로 같은 셀 을 매 시점마다 반복해서 사용하는 구조 입니다.
펼쳐서 보면, 위 그림처럼 하나의 RNN셀이 시간이 흐름에 따라 여러 번 복제된 것 처럼 보입니다.
순서대로 보자면,
- 각 시점의 $X_t$가 RNN 셀에 들어가고,
- 이전 시점의 은식 상태의 $h_(t^-1)$도 함께 들어가서,
- 새로운 은닉상태 $h_t$를 출력합니다.
- 이 $h_t$는 다음 시점으로 전달되어 메모리 역할을 합니다.
위 그림 속 여러 셀은 같은 RNN 셀을 시간에 따라 복제한 것이고, 모든 셀은 가중치를 공유하기 때문에 시점이 달라도 같은 규칙으로 계산이 이루어 집니다.
보충설명 하자면,나는 학교에 간다
를 입력하면,
$X_1$이 나는, $X_2$가 학교에, $X_3$ 가 간다가 되면서 시간에 따른 연산이 이루어 진다고 생각하시면 됩니다.
2. 아키텍처를 통한 내부 코드 들여다 보기
이제 이론을 바탕으로, TensorFlow Keras 를 통해 직접 RNN을 구현해 봅시다.
Keras로 구현한 RNN 모델 아키텍처 심층 분석다음은 IMDB 영화 리뷰 감성 분석을 위한 간단한 RNN 모델입니다.
import tensorflow as tf
from tensorflow import keras
# 모델 아키텍처 정의
model = keras.Sequential([
# 1. 단어 임베딩 층
# input_dim: 전체 단어 집합의 크기 (가장 빈번한 1만개 단어)
# output_dim: 각 단어를 표현할 벡터의 차원 (32차원)
keras.layers.Embedding(input_dim=10000, output_dim=32),
# 2. RNN 층
# units: 은닉 상태 벡터의 차원 (32차원)
keras.layers.SimpleRNN(32),
# 3. 최종 분류기(Classifier)
# units: 출력 뉴런의 수 (긍정/부정 1개)
# activation: 출력 값을 0~1 사이 확률로 변환 (이진 분류)
keras.layers.Dense(1, activation="sigmoid"),
])
# 모델 구조 요약 출력
model.summary()
레이어를 자세히 들어다 봅시다.
- 임베딩 층(Embedding)
keras.layers.Embedding(input_dim=10000, output_dim=32)
컴퓨터는 '영화', '재미' 같은 단어를 직접 이해하지 못합니다.
Embedding 층은 각 단어에 부여된 고유한 정수 인덱스를 output_dim 차원의 의미론적 벡터로 변환합니다.
이 과정에서 '슬픔'과 '비극' 같은 단어들은 벡터 공간상에서 가까운 위치에, '행복'과는 먼 위치에 표현되도록 학습됩니다.
- 순환 계층(SimpleRNN)
keras.layers.SimpleRNN(32),
이 층이 순환 신경망의 본체입니다.
입력된 단어 임베딩 벡터 시퀀스를 순서대로 하나씩 처리하며 은닉 상태를 계속해서 업데이트합니다.
기본적으로 마지막 단어까지 처리한 후의 최종 은닉 상태만을 다음 층으로 전달합니다.
이 최종 은닉 상태 벡터는 전체 문장의 문맥을 압축한 결과물입니다.
- 완전 연결 계층(Dense)
keras.layers.Dense(1, activation="sigmoid")
최종 은닉 상태 벡터를 받아, 리뷰가 긍정(1에 가까운 값)인지 부정(0에 가까운 값)인지 최종 판단을 내립니다. model.summary()로 파라미터 수 계산 원리 이해하기위 코드에서 model.summary()를 실행하면 다음과 같은 결과가 나옵니다.
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, None, 32) 320000
simple_rnn (SimpleRNN) (None, 32) 2080
dense (Dense) (None, 1) 33
=================================================================
Total params: 322,113
Trainable params: 322,113
Non-trainable params: 0
_________________________________________________________________
각 층의 파라미터 수는 어떻게 계산되는지 알아보자면,
- Embedding: input_dim * output_dim = 10,000 * 32 = 320,000 개. (1만 개 단어 각각에 대한 32차원 벡터)
- SimpleRNN:
- 입력 가중치(W_xh): input_shape * units = 32 * 32 = 1024
- 은닉 상태 가중치(W_hh): units * units = 32 * 32 = 1024
- 편향(b_h): units = 32
- 총합: 1024 + 1024 + 32 = 2,080 개.
- Dense: input_shape * units + bias = 32 * 1 + 1 = 33 개.
이처럼 summary를 통해 모델의 구조뿐만 아니라, 각 층이 얼마나 많은 파라미터를 학습해야 하는지 정확히 파악할 수 있습니다.
3. 직접 RNN 구현해 보기 (실전 예제)
이제 전체 코드를 단계별로 실행하며 직접 모델을 학습시켜 보겠습니다.
1단계 : 데이터 로드 및 전처리 (패딩의 중요성)
RNN은 고정된 길이의 시퀀스를 입력으로 받습니다.
하지만 영화 리뷰는 길이가 제각각이므로, **패딩(Padding)**을 통해 모든 리뷰의 길이를 동일하게 맞춰줘야 합니다.
import numpy as np
import tensorflow as tf
from tensorflow import keras
from keras import layers
# 가장 빈도가 높은 1만개 단어만 사용하여 데이터셋 로드
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=10000)
print(f"패딩 전 첫 번째 리뷰 길이: {len(x_train[0])}")
# 모든 시퀀스의 길이를 256으로 통일
# maxlen보다 길면 잘라내고, 짧으면 앞부분을 0으로 채움 (pre-padding)
x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=256)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=256)
print(f"패딩 후 첫 번째 리뷰 길이: {len(x_train[0])}")
2단계 : 모델 컴파일
모델을 어떻게 학습시킬지 학습 방법을 설정합니다.
model.compile(
# 손실 함수: 예측이 정답과 얼마나 다른지 측정.
# 이진 분류(0 또는 1) 문제이므로 binary_crossentropy가 가장 적합.
loss="binary_crossentropy",
# 옵티마이저: 손실을 최소화하기 위해 모델의 가중치를 업데이트하는 알고리즘.
# Adam은 현재 가장 널리 쓰이고 성능이 좋은 옵티마이저 중 하나.
optimizer="adam",
# 평가지표: 훈련 과정을 모니터링할 지표. 정확도를 사용.
metrics=["accuracy"]
)
3단계 : 모델 학습 및 평가 (배치, 에포크, 그리고 과적합)
model.fit() 함수로 실제 학습을 시작합니다.
batch_size: 한 번에 처리할 데이터 샘플의 수. 메모리 효율성과 학습 속도에 영향을 줍니다.
epochs: 전체 데이터셋을 몇 번 반복하여 학습할지 결정합니다.
batch_size = 128
epochs = 10
# 모델 학습 실행
# validation_data를 지정하여 매 에포크마다 테스트 데이터로 성능을 검증
history = model.fit(
x_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, y_test)
)
# 학습 완료 후 최종 성능 평가
score = model.evaluate(x_test, y_test, verbose=0)
print(f"\nTest loss: {score[0]:.4f}")
print(f"Test accuracy: {score[1]:.4f}")
학습 과정을 지켜볼 때, 훈련 데이터의 정확도(accuracy)는 계속 오르는데 검증 데이터의 정확도(val_accuracy)가 어느 순간부터 정체되거나 떨어진다면, 모델이 훈련 데이터에만 과하게 적응하는 과적합(Overfitting)이 발생하고 있다는 신호입니다.
이럴 때는, 드롭아웃의 비율을 높이거나 학습율을 조정하는 방법으로 해결할 수 있습니다.
4단계 : 학습된 모델 저장 및 재사용
학습에 오랜 시간이 걸리는 모델은 그 결과를 저장해두고 필요할 때마다 불러서 사용하는 것이 효율적입니다.
# 모델의 구조, 가중치, 학습 설정을 모두 '.keras' 파일 하나에 저장
model.save("my_rnn_model_imdb.keras")
# 저장된 모델 불러오기
loaded_model = keras.models.load_model("my_rnn_model_imdb.keras")
5단계 : 나만의 문장으로 모델 테스트하기 (실전 예측)
실제 문장을 예측하려면, 훈련 데이터와 동일한 전처리 과정을 거쳐야 합니다.
# IMDB 데이터셋의 단어-인덱스 사전 로드
word_index = keras.datasets.imdb.get_word_index()
# 새로운 리뷰 문장
review = "This movie was fantastic and wonderful"
# 1. 소문자 변환 및 단어 분리 -> 2. 단어 인덱스로 변환
tokens = [word_index.get(word, 2) for word in review.lower().split()]
# 3. 패딩 처리
padded_tokens = keras.preprocessing.sequence.pad_sequences([tokens], maxlen=256)
# 4. 예측
prediction = loaded_model.predict(padded_tokens)
print(f"리뷰: '{review}'")
print(f"긍정 확률: {prediction[0][0] * 100:.2f}%")
- 나만의 RNN 모델 업그레이드하기
기본 RNN도 좋지만, 더 복잡한 문제를 해결하기 위해서는 몇 가지 한계를 극복해야 합니다.
RNN의 치명적 약점 : 장기 의존성 문제 (Vanishing Gradients)
"나는 프랑스에서 태어나 자랐다. ... (중략) ... 그래서 나는 ___를 유창하게 구사한다."
빈칸에 들어갈 말은 '프랑스어'입니다.
사람은 문장 맨 앞의 '프랑스'라는 단어를 기억하여 쉽게 답을 찾습니다.
하지만 기본 RNN은 시퀀스가 길어질수록, 역전파 과정에서 그래디언트(기울기)가 계속 곱해지면서 0에 가까워져 사라지는 그래디언트 소실(Vanishing Gradient) 문제가 발생합니다.
이로 인해 문장 앞부분의 중요한 정보를 학습하지 못하게 됩니다.
이를 장기 의존성 문제라고 합니다.
실제로, 지금 허깅페이스에 올라가와 있는 RNN 모델을 사용해 보면,
편향되어 한가지 라벨만 계속 출력되는 것을 보여줍니다.
이 모습이 RNN의 한계를 명확하게 보여주는데요,
바로 앞서 설명한 장기의존성, 기울기 소실 문제 입니다.
이를 해결하기 위해 다음 구조가 등장했습니다.
기억력 강화 : LSTM과 GRU의 등장
이 문제를 해결하기 위해 **LSTM(Long Short-Term Memory)**과 **GRU(Gated Recurrent Unit)**가 등장했습니다.
이들은 RNN 내부에 **게이트(Gate)**라는 정교한 장치를 추가하여 정보의 흐름을 제어합니다.
- LSTM: '셀 상태(Cell State)'라는 별도의 기억 컨베이어 벨트를 두고, 망각 게이트(Forget Gate), 입력 게이트(Input Gate), 출력 게이트(Output Gate) 3개의 게이트를 통해 어떤 정보를 버리고, 어떤 정보를 새로 기억하고, 어떤 정보를 출력할지 학습합니다.
장기 기억에 매우 효과적입니다. - GRU: LSTM을 더 단순화한 모델로, **리셋 게이트(Reset Gate)**와 업데이트 게이트(Update Gate) 2개의 게이트만 사용합니다.
파라미터 수가 적어 계산 효율성이 높고, 많은 경우 LSTM과 비슷한 성능을 보입니다.
# LSTM을 2개 층으로 쌓은 모델
model_lstm = keras.Sequential([
layers.Embedding(input_dim=10000, output_dim=64),
# return_sequences=True: 다음 LSTM 층으로 전체 시퀀스를 전달
layers.LSTM(64, return_sequences=True),
layers.LSTM(32),
layers.Dense(1, activation='sigmoid')
])
과거와 미래를 동시에 : 양방향 RNN (Bidirectional RNN)
"오늘 새로 온 ___ 선생님은 정말 멋지다."
빈칸에 들어갈 '영어'라는 단어는 뒷부분의 '선생님'이라는 단어를 봐야 더 정확히 유추할 수 있습니다.
이처럼 문맥은 순방향뿐만 아니라 역방향의 정보도 중요합니다.
**양방향 RNN(Bidirectional RNN)**은 시퀀스를 정방향으로 한번, 역방향으로 한번 독립적으로 처리한 후, 두 결과를 합쳐서 최종 출력을 만듭니다.
이를 통해 문맥을 훨씬 더 풍부하게 이해할 수 있습니다.
model_bidirectional = keras.Sequential([
layers.Embedding(input_dim=10000, output_dim=64),
# LSTM 레이어를 Bidirectional 래퍼로 감싸기만 하면 됨
layers.Bidirectional(layers.LSTM(64)),
layers.Dropout(0.5), # 과적합 방지를 위한 드롭아웃 추가
layers.Dense(1, activation='sigmoid')
])
5. 결론
오늘은 순차 데이터 처리의 근간이 되는 RNN의 핵심 원리부터 시작하여, 실제 코드로 모델을 구현하고, LSTM, GRU, 양방향 RNN과 같은 고급 기법을 통해 성능을 개선하는 방법까지 상세하게 알아보았습니다.
RNN은 그 자체로도 강력하지만, 자연어 처리 분야의 발전에 엄청난 기여를 한 기념비적인 모델입니다.
특히 RNN의 한계를 극복하려는 시도 속에서 탄생한 어텐션(Attention) 메커니즘은 이후 **트랜스포머(Transformer)**라는 혁신적인 모델의 기반이 되었습니다.
다음에는 오늘 짧게 알아본 LSTM과 GRU로 돌아오겠습니다!!
오늘도 좋은하루 보내세요!!
- Downloads last month
- 8