본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 4. · 16 Views
순차 모델과 RNN/LSTM 완벽 가이드
시계열 데이터와 자연어 처리의 핵심인 순차 모델을 초급자도 이해할 수 있도록 설명합니다. RNN의 기본 개념부터 LSTM까지, 실무에서 바로 활용할 수 있는 코드와 함께 배워봅니다.
목차
1. 순차 데이터 이해
어느 날 김개발 씨가 회사에서 주가 예측 프로젝트를 맡게 되었습니다. 데이터를 받아보니 날짜별로 쭉 나열된 숫자들이었습니다.
"이걸 어떻게 분석하지? 일반 신경망에 그냥 넣으면 되나?" 고민하던 중 선배 박시니어 씨가 다가왔습니다.
순차 데이터란 순서가 중요한 데이터를 말합니다. 마치 소설책의 페이지처럼, 순서를 바꾸면 전혀 다른 의미가 되는 데이터입니다.
주가, 날씨, 문장 속 단어들이 모두 순차 데이터에 해당합니다. 이런 데이터를 제대로 다루려면 순서 정보를 기억하는 특별한 모델이 필요합니다.
다음 코드를 살펴봅시다.
import numpy as np
# 순차 데이터 예시: 일주일 간의 주가
stock_prices = [100, 102, 98, 105, 110, 108, 115]
# 순서가 중요합니다 - 시간순으로 패턴을 분석
# 3일치 데이터로 다음 날을 예측하는 구조
def create_sequences(data, seq_length=3):
sequences = []
targets = []
for i in range(len(data) - seq_length):
# 연속된 seq_length개의 데이터를 입력으로
sequences.append(data[i:i+seq_length])
# 그 다음 값을 예측 대상으로
targets.append(data[i+seq_length])
return np.array(sequences), np.array(targets)
X, y = create_sequences(stock_prices)
print(f"입력 형태: {X.shape}, 출력 형태: {y.shape}")
김개발 씨는 입사 6개월 차 주니어 데이터 사이언티스트입니다. 오늘 팀장님이 새로운 프로젝트를 던져주셨습니다.
"우리 회사 주가 데이터로 간단한 예측 모델 만들어볼래? 딥러닝 공부한다고 했잖아." 김개발 씨는 자신 있게 데이터를 열어보았습니다.
날짜별로 정리된 숫자들이 쭉 나열되어 있었습니다. 처음에는 단순하게 생각했습니다.
"그냥 Dense 레이어 몇 개 쌓아서 학습시키면 되지 않을까?" 하지만 결과는 처참했습니다. 모델이 전혀 패턴을 잡지 못하는 것이었습니다.
그때 옆자리의 박시니어 씨가 화면을 슬쩍 보더니 말했습니다. "개발 씨, 이 데이터가 뭔지 알아요?
순차 데이터예요. 순서가 생명인 데이터라고요." 순차 데이터란 무엇일까요?
쉽게 비유하자면 추리 소설과 같습니다. 추리 소설은 처음부터 끝까지 순서대로 읽어야 범인이 누구인지 알 수 있습니다.
만약 페이지를 무작위로 섞어서 읽는다면, 스토리를 전혀 이해할 수 없을 것입니다. 주가 데이터도 마찬가지입니다.
월요일에 100원, 화요일에 102원, 수요일에 98원이라는 순서가 중요합니다. 이 순서에서 "상승 후 하락"이라는 패턴이 보이기 때문입니다.
만약 이 숫자들을 뒤섞어버리면 아무런 의미가 없어집니다. 일반적인 완전연결 신경망은 이런 순서 정보를 전혀 고려하지 않습니다.
입력 데이터를 그냥 하나의 큰 벡터로 받아들일 뿐, "어떤 것이 먼저 오고 나중에 오는지"를 신경 쓰지 않습니다. 위의 코드를 살펴보겠습니다.
create_sequences 함수는 순차 데이터를 학습에 적합한 형태로 변환합니다. 3일치 데이터를 묶어서 입력으로 만들고, 4일째 값을 예측 대상으로 설정합니다.
이렇게 하면 모델이 "연속된 3일의 패턴을 보고 다음 날을 예측"하는 방식으로 학습할 수 있습니다. 실무에서 순차 데이터는 정말 많이 등장합니다.
주가뿐 아니라 날씨 예보, 교통량 예측, 그리고 우리가 매일 쓰는 문장까지 모두 순차 데이터입니다. "나는 밥을 먹는다"와 "밥을 나는 먹는다"는 같은 단어지만 순서가 다르면 어색해지지 않습니까?
박시니어 씨가 덧붙였습니다. "순차 데이터를 제대로 다루려면 순서를 기억하는 모델이 필요해요.
그게 바로 다음에 배울 RNN이에요." 김개발 씨는 고개를 끄덕였습니다. 단순히 숫자를 넣는 것이 아니라, 데이터의 본질을 이해하는 것이 먼저라는 사실을 깨달았습니다.
실전 팁
💡 - 순차 데이터를 다룰 때는 반드시 시간순 정렬을 확인하세요
- 시퀀스 길이는 도메인에 따라 다르므로 실험을 통해 최적값을 찾아야 합니다
2. 순차 모델의 종류
김개발 씨가 순차 데이터의 개념을 이해하고 나니, 이제 어떤 모델을 써야 할지 궁금해졌습니다. 인터넷을 검색해보니 RNN, LSTM, GRU, Transformer 등 온갖 이름이 쏟아졌습니다.
"이게 다 뭐지? 뭘 써야 하는 거야?"
순차 모델은 순서가 있는 데이터를 처리하도록 설계된 신경망입니다. 마치 컨베이어 벨트 위의 물건을 하나씩 검사하는 공장 라인처럼, 데이터를 순서대로 처리하면서 이전 정보를 기억합니다.
대표적으로 RNN, LSTM, GRU가 있으며, 최근에는 Transformer도 널리 사용됩니다.
다음 코드를 살펴봅시다.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimpleRNN, LSTM, GRU, Dense
# 순차 모델의 세 가지 주요 유형
input_shape = (10, 1) # 10개 타임스텝, 1개 특성
# 1. 기본 RNN - 가장 단순한 형태
rnn_model = Sequential([
SimpleRNN(32, input_shape=input_shape),
Dense(1)
])
# 2. LSTM - 장기 의존성을 잘 학습
lstm_model = Sequential([
LSTM(32, input_shape=input_shape),
Dense(1)
])
# 3. GRU - LSTM의 간소화 버전, 더 빠름
gru_model = Sequential([
GRU(32, input_shape=input_shape),
Dense(1)
])
print("RNN 파라미터:", rnn_model.count_params())
print("LSTM 파라미터:", lstm_model.count_params())
print("GRU 파라미터:", gru_model.count_params())
김개발 씨는 순차 데이터를 처리할 모델을 찾아 나섰습니다. 구글에 검색하니 수많은 모델 이름이 쏟아졌습니다.
RNN, LSTM, GRU, Bidirectional, Seq2Seq, Transformer... 머리가 어질어질해졌습니다.
점심시간에 박시니어 씨에게 물었습니다. "선배, 이 많은 모델 중에 뭘 써야 해요?" 박시니어 씨가 웃으며 대답했습니다.
"처음이니까 기본부터 알아보자. 순차 모델의 큰 그림을 먼저 그려볼게요." 순차 모델의 핵심 아이디어는 간단합니다.
데이터를 순서대로 하나씩 읽으면서, 이전에 읽은 내용을 기억하는 것입니다. 마치 책을 읽을 때 앞 내용을 기억하면서 뒷내용을 이해하는 것과 같습니다.
가장 기본이 되는 모델이 RNN입니다. Recurrent Neural Network의 약자로, "순환 신경망"이라고 번역합니다.
이 모델은 이전 단계의 출력을 다음 단계의 입력으로 다시 사용합니다. 마치 일기를 쓸 때 어제 쓴 내용을 참고하면서 오늘 일기를 쓰는 것과 비슷합니다.
하지만 기본 RNN에는 치명적인 문제가 있었습니다. 오래된 정보를 잘 기억하지 못한다는 것입니다.
열 페이지 전에 나온 복선을 기억하지 못하는 건망증 심한 독자와 같았습니다. 이 문제를 해결하기 위해 등장한 것이 LSTM입니다.
Long Short-Term Memory, 즉 "장단기 메모리"라는 이름답게 오래된 정보도 잘 기억합니다. 내부에 게이트라는 특별한 구조가 있어서 "무엇을 기억하고 무엇을 잊을지"를 학습합니다.
GRU는 LSTM을 간소화한 버전입니다. LSTM의 세 개 게이트를 두 개로 줄여서 더 빠르게 학습할 수 있습니다.
성능은 LSTM과 비슷한 경우가 많아서, 속도가 중요할 때 자주 선택됩니다. 위 코드를 보면 세 모델 모두 같은 구조로 만들 수 있습니다.
Keras에서는 SimpleRNN, LSTM, GRU 레이어를 교체하기만 하면 됩니다. 파라미터 수를 출력해보면 LSTM이 가장 많고, GRU가 그 다음, RNN이 가장 적습니다.
파라미터가 많을수록 더 복잡한 패턴을 학습할 수 있지만, 학습 시간도 오래 걸립니다. 박시니어 씨가 조언했습니다.
"처음에는 LSTM으로 시작해봐요. 가장 널리 쓰이고 안정적이거든요.
속도가 문제되면 GRU로 바꿔보고요." 김개발 씨는 메모장에 적었습니다. "기본 RNN은 학습용, 실무에서는 LSTM이나 GRU 사용." 이제 각 모델이 어떻게 동작하는지 자세히 알아볼 차례입니다.
실전 팁
💡 - 처음 시작할 때는 LSTM을 기본으로 사용하세요
- 학습 속도가 중요하다면 GRU를 고려해보세요
- 데이터셋 크기와 복잡도에 따라 최적의 모델이 달라집니다
3. RNN 구조와 훈련
김개발 씨는 이제 RNN이 무엇인지 알게 되었습니다. 하지만 "순환한다"는 말이 정확히 어떤 의미인지 아직 감이 오지 않았습니다.
"그래서 어떻게 동작하는 건데?" 박시니어 씨가 화이트보드 앞으로 김개발 씨를 불렀습니다.
RNN의 구조는 숨겨진 상태를 다음 단계로 전달하는 순환 구조입니다. 마치 전화기 게임처럼 이전 사람의 말을 듣고 다음 사람에게 전달하는 방식입니다.
각 타임스텝에서 현재 입력과 이전 상태를 결합하여 새로운 상태를 만들어냅니다.
다음 코드를 살펴봅시다.
import numpy as np
class SimpleRNNCell:
def __init__(self, hidden_size):
# 가중치 초기화 (실제로는 더 정교한 초기화 사용)
self.Wxh = np.random.randn(hidden_size, 1) * 0.01 # 입력->은닉
self.Whh = np.random.randn(hidden_size, hidden_size) * 0.01 # 은닉->은닉
self.bh = np.zeros((hidden_size, 1)) # 편향
def forward(self, x, h_prev):
# 핵심: 현재 입력 + 이전 상태 = 새로운 상태
# h_t = tanh(Wxh * x_t + Whh * h_{t-1} + b)
h_new = np.tanh(
np.dot(self.Wxh, x) +
np.dot(self.Whh, h_prev) +
self.bh
)
return h_new
# 시퀀스 처리 예시
rnn = SimpleRNNCell(hidden_size=4)
h = np.zeros((4, 1)) # 초기 은닉 상태
sequence = [0.1, 0.5, 0.3, 0.8] # 4개 타임스텝
for t, x_t in enumerate(sequence):
h = rnn.forward(np.array([[x_t]]), h)
print(f"t={t}: 은닉 상태 평균 = {h.mean():.4f}")
박시니어 씨가 화이트보드에 원을 하나 그렸습니다. "이게 RNN의 핵심이에요.
자기 자신에게 화살표가 돌아오는 구조, 그래서 순환이라고 부르는 거예요." 김개발 씨는 고개를 갸웃했습니다. "자기 자신에게 돌아온다고요?" 박시니어 씨가 설명을 이었습니다.
"전화기 게임 해봤죠? 첫 번째 사람이 두 번째 사람에게 귓속말하고, 두 번째 사람은 세 번째 사람에게 전달하고...
RNN도 비슷해요." RNN의 동작 원리는 이렇습니다. 각 타임스텝에서 두 가지 입력을 받습니다.
하나는 현재 시점의 데이터이고, 다른 하나는 이전 시점에서 넘어온 은닉 상태입니다. 은닉 상태는 일종의 메모라고 생각하면 됩니다.
"지금까지 읽은 내용 중 중요한 것"을 적어둔 메모입니다. 새로운 입력이 들어올 때마다 이 메모를 업데이트합니다.
위 코드의 forward 함수를 보겠습니다. 핵심 수식은 단 한 줄입니다.
현재 입력에 가중치를 곱한 것과, 이전 은닉 상태에 가중치를 곱한 것을 더합니다. 그리고 tanh 함수를 통과시켜 새로운 은닉 상태를 만듭니다.
Wxh는 입력을 은닉 상태로 변환하는 가중치입니다. Whh는 이전 은닉 상태를 현재에 반영하는 가중치입니다.
이 두 가중치가 학습을 통해 최적화됩니다. 실제 동작을 따라가 보겠습니다.
처음에는 은닉 상태가 모두 0입니다. 아직 아무것도 읽지 않았으니까요.
첫 번째 입력 0.1이 들어오면, 가중치와 곱해져서 첫 번째 은닉 상태가 만들어집니다. 두 번째 입력 0.5가 들어올 때는 상황이 다릅니다.
이번에는 첫 번째에서 만든 은닉 상태도 함께 사용됩니다. 즉, "첫 번째 입력의 정보"가 두 번째 계산에 영향을 미치는 것입니다.
이런 식으로 계속 진행하면, 마지막 은닉 상태에는 전체 시퀀스의 정보가 압축되어 담기게 됩니다. 이 최종 은닉 상태를 Dense 레이어에 연결하면 예측이나 분류가 가능해집니다.
박시니어 씨가 정리했습니다. "결국 RNN의 핵심은 정보의 전달이에요.
과거의 정보를 현재로 전달하는 것, 그게 순환의 의미예요." 김개발 씨가 물었습니다. "그런데 이걸 어떻게 학습시켜요?
일반 신경망처럼 역전파하면 되나요?" 박시니어 씨가 고개를 저었습니다. "비슷하지만 조금 달라요.
시간을 따라 역전파해야 해요. 다음에 그걸 알아보죠."
실전 팁
💡 - 은닉 상태의 크기는 모델의 기억 용량을 결정합니다
- 초기 은닉 상태는 보통 0으로 설정하지만, 학습 가능하게 만들 수도 있습니다
4. 시간에 따른 역전파
김개발 씨는 RNN의 순환 구조를 이해했습니다. 하지만 한 가지 의문이 남았습니다.
"일반 신경망은 역전파로 학습하는데, RNN은 시간 방향으로 연결되어 있잖아요. 어떻게 역전파하죠?" 박시니어 씨가 빙긋 웃었습니다.
"좋은 질문이에요. BPTT라는 게 있어요."
BPTT는 Backpropagation Through Time의 약자로, 시간을 거슬러 역전파하는 방법입니다. RNN을 시간축으로 펼쳐서 일반 신경망처럼 만든 뒤 역전파합니다.
마치 시간 여행하듯 과거로 돌아가면서 각 시점의 기울기를 계산합니다.
다음 코드를 살펴봅시다.
import numpy as np
def bptt_example(sequence_length=5):
"""BPTT의 개념을 보여주는 간단한 예시"""
# 순전파: 시간 순서대로 진행
print("=== 순전파 (Forward) ===")
hidden_states = []
h = 0 # 초기 상태
for t in range(sequence_length):
h = h * 0.9 + t * 0.1 # 단순화된 RNN 연산
hidden_states.append(h)
print(f"t={t}: h = {h:.3f}")
# 역전파: 시간을 거슬러 진행
print("\n=== 역전파 (BPTT) ===")
grad = 1.0 # 출력에서 시작하는 기울기
for t in reversed(range(sequence_length)):
# 기울기가 시간을 거슬러 전파됨
grad = grad * 0.9 # 체인룰 적용
print(f"t={t}: gradient = {grad:.4f}")
# 기울기 소실 문제 확인
print(f"\n첫 번째 타임스텝의 기울기: {grad:.6f}")
print("기울기가 급격히 줄어드는 것을 확인할 수 있습니다!")
bptt_example(10)
박시니어 씨가 화이트보드에 RNN을 시간축으로 펼쳐 그렸습니다. "RNN의 순환 구조를 이렇게 펼쳐보면, 사실 아주 깊은 신경망과 같아요." 김개발 씨가 그림을 보니 정말 그랬습니다.
10개의 타임스텝을 처리하는 RNN을 펼치면, 10개 층짜리 신경망처럼 보였습니다. BPTT의 아이디어는 단순합니다.
RNN을 시간축으로 펼친 뒤, 일반 신경망처럼 역전파를 수행하는 것입니다. 다만 시간을 거슬러 올라간다는 점이 특별합니다.
순전파는 시간 순서대로 진행합니다. t=0에서 시작해서 t=1, t=2...
마지막 타임스텝까지 은닉 상태를 계산합니다. 각 단계에서 이전 은닉 상태가 다음 단계로 전달됩니다.
역전파는 그 반대입니다. 마지막 타임스텝에서 시작해서 처음으로 거슬러 올라갑니다.
마치 시간 여행처럼 과거로 돌아가면서 "이 시점에서 가중치를 얼마나 바꿔야 할까?"를 계산합니다. 위 코드를 실행해보면 흥미로운 현상이 보입니다.
역전파를 진행할수록 기울기가 점점 작아집니다. 10개 타임스텝만 거슬러 올라가도 기울기가 거의 0에 가까워집니다.
이것이 바로 악명 높은 기울기 소실 문제입니다. 매 타임스텝마다 기울기에 1보다 작은 값이 곱해지면, 지수적으로 작아지기 때문입니다.
0.9를 10번 곱하면 약 0.35, 50번 곱하면 0.005가 됩니다. 기울기가 소실되면 무슨 일이 벌어질까요?
모델이 먼 과거의 정보를 학습하지 못하게 됩니다. "100번째 단어가 200번째 단어에 영향을 미친다"는 패턴을 배울 수 없는 것입니다.
반대로 기울기가 1보다 큰 값으로 계속 곱해지면 기울기 폭발이 일어납니다. 기울기가 무한대로 커져서 학습이 불안정해집니다.
이 문제는 그래디언트 클리핑으로 어느 정도 해결할 수 있습니다. 박시니어 씨가 한숨을 쉬었습니다.
"이게 기본 RNN의 가장 큰 약점이에요. 긴 시퀀스를 제대로 처리하지 못해요." 김개발 씨가 물었습니다.
"그럼 어떻게 해요? 긴 문장이나 긴 시계열은 포기해야 하나요?" 박시니어 씨가 고개를 저었습니다.
"아니요, 그래서 LSTM이 나온 거예요. 하지만 먼저 기본 RNN의 한계를 좀 더 자세히 알아보죠."
실전 팁
💡 - 시퀀스가 길어질수록 BPTT의 계산 비용이 증가합니다
- Truncated BPTT를 사용하면 일정 길이까지만 역전파하여 효율을 높일 수 있습니다
5. 기본 RNN의 한계
김개발 씨는 주가 예측 모델을 기본 RNN으로 만들어 보았습니다. 짧은 시퀀스에서는 잘 동작했지만, 한 달치 데이터를 넣으니 성능이 급격히 떨어졌습니다.
"왜 이러지? 데이터가 많으면 더 잘 학습해야 하는 거 아닌가?"
기본 RNN의 한계는 장기 의존성을 학습하지 못한다는 점입니다. 마치 금붕어처럼 조금 전 일만 기억하고 오래된 일은 잊어버립니다.
이는 기울기 소실과 폭발 문제 때문이며, 실무에서 기본 RNN을 거의 사용하지 않는 이유입니다.
다음 코드를 살펴봅시다.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimpleRNN, LSTM, Dense
import numpy as np
# 장기 의존성 테스트: 시퀀스 처음의 값이 마지막 출력에 영향
def create_long_dependency_data(n_samples=1000, seq_length=50):
X = np.random.randn(n_samples, seq_length, 1)
# 정답은 시퀀스의 첫 번째 값에만 의존
y = (X[:, 0, 0] > 0).astype(float)
return X, y
X, y = create_long_dependency_data()
# 기본 RNN - 장기 의존성 학습 실패
rnn_model = Sequential([
SimpleRNN(32, input_shape=(50, 1)),
Dense(1, activation='sigmoid')
])
rnn_model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
rnn_model.fit(X, y, epochs=10, verbose=0)
print(f"RNN 정확도: {rnn_model.evaluate(X, y, verbose=0)[1]:.2%}")
# LSTM - 장기 의존성 학습 성공
lstm_model = Sequential([
LSTM(32, input_shape=(50, 1)),
Dense(1, activation='sigmoid')
])
lstm_model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
lstm_model.fit(X, y, epochs=10, verbose=0)
print(f"LSTM 정확도: {lstm_model.evaluate(X, y, verbose=0)[1]:.2%}")
김개발 씨의 주가 예측 모델이 이상하게 동작했습니다. 최근 며칠 데이터만 넣으면 꽤 잘 맞추는데, 한 달치 데이터를 넣으면 오히려 성능이 떨어졌습니다.
박시니어 씨에게 물어보니 답이 돌아왔습니다. "그건 기본 RNN의 고질적인 문제예요.
장기 의존성을 학습하지 못하거든요." 장기 의존성이란 무엇일까요? 예를 들어 이런 문장이 있다고 해봅시다.
"나는 한국에서 태어났고, 초등학교와 중학교를 거쳐 고등학교를 졸업한 뒤 대학에 진학했으며, 여러 나라를 여행하다가 결국 프랑스에 정착했지만, 모국어는 여전히 한국어입니다." 마지막의 "한국어"를 예측하려면 문장 맨 앞의 "한국에서"를 기억해야 합니다. 이렇게 멀리 떨어진 정보 사이의 연관성이 장기 의존성입니다.
기본 RNN은 이런 연관성을 학습하지 못합니다. 앞서 배운 기울기 소실 때문입니다.
50개 타임스텝을 거슬러 역전파하면 기울기가 거의 0에 가까워져서, 첫 번째 타임스텝의 가중치가 거의 업데이트되지 않습니다. 위 코드는 이 문제를 명확히 보여줍니다.
정답이 시퀀스의 첫 번째 값에만 의존하도록 데이터를 만들었습니다. 기본 RNN은 50번째 타임스텝에서 첫 번째 값을 기억하지 못해 거의 랜덤에 가까운 성능을 보입니다.
반면 LSTM은 같은 문제를 쉽게 해결합니다. LSTM은 기울기가 소실되지 않도록 특별한 구조를 갖추고 있기 때문입니다.
실무에서 기본 RNN을 거의 사용하지 않는 이유가 바로 이것입니다. 대부분의 실제 문제는 어느 정도의 장기 의존성을 필요로 합니다.
주가도 어제의 가격뿐 아니라 한 달 전, 1년 전의 패턴이 중요할 수 있습니다. 박시니어 씨가 정리했습니다.
"기본 RNN은 개념을 이해하기 위한 교과서적 모델이에요. 실무에서는 LSTM이나 GRU를 쓰세요." 김개발 씨가 물었습니다.
"그럼 LSTM은 어떻게 이 문제를 해결하는 거예요?" 박시니어 씨가 대답했습니다. "게이트라는 장치를 사용해요.
먼저 더 단순한 GRU부터 알아볼까요?"
실전 팁
💡 - 시퀀스 길이가 20-30을 넘어가면 기본 RNN의 성능이 급격히 저하됩니다
- 실무 프로젝트에서는 처음부터 LSTM이나 GRU를 사용하세요
6. GRU 게이트 순환 유닛
박시니어 씨가 말했습니다. "LSTM을 이해하려면 먼저 GRU를 알아두는 게 좋아요.
GRU는 LSTM의 동생 같은 존재인데, 구조가 더 단순해서 이해하기 쉽거든요." 김개발 씨는 노트를 펼쳤습니다.
GRU는 Gated Recurrent Unit의 약자로, 게이트를 사용해 정보의 흐름을 조절합니다. 두 개의 게이트, 즉 리셋 게이트와 업데이트 게이트가 있습니다.
마치 수도꼭지처럼 정보를 얼마나 흘려보낼지 조절하여 장기 의존성 문제를 해결합니다.
다음 코드를 살펴봅시다.
import numpy as np
class GRUCell:
"""GRU 셀의 동작을 보여주는 간단한 구현"""
def __init__(self, hidden_size):
self.hidden_size = hidden_size
def forward(self, x, h_prev):
# 설명을 위한 간소화된 버전
# 1. 리셋 게이트: 이전 정보를 얼마나 잊을지
r = self.sigmoid(x * 0.5 + h_prev * 0.3)
# 2. 업데이트 게이트: 새 정보를 얼마나 반영할지
z = self.sigmoid(x * 0.4 + h_prev * 0.4)
# 3. 후보 은닉 상태: 리셋 게이트로 걸러진 정보 사용
h_candidate = np.tanh(x * 0.5 + (r * h_prev) * 0.3)
# 4. 최종 은닉 상태: 업데이트 게이트로 혼합
# z가 1이면 이전 상태 유지, 0이면 새 상태로 교체
h_new = (1 - z) * h_prev + z * h_candidate
return h_new, r, z
def sigmoid(self, x):
return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
# GRU 동작 시연
gru = GRUCell(hidden_size=1)
h = np.array([0.5])
print("GRU 게이트 동작 확인:")
for x in [0.1, 0.9, -0.5, 0.3]:
h, r, z = gru.forward(np.array([x]), h)
print(f"입력={x:+.1f} | 리셋={r[0]:.2f} | 업데이트={z[0]:.2f} | 은닉={h[0]:.3f}")
박시니어 씨가 설명을 시작했습니다. "GRU의 핵심은 게이트예요.
게이트는 정보가 흐르는 양을 조절하는 밸브 같은 거예요." 기본 RNN은 모든 정보를 무조건 섞었습니다. 현재 입력과 이전 상태를 그냥 더해버렸죠.
하지만 GRU는 다릅니다. "이 정보는 기억하고, 저 정보는 잊자"라고 선택적으로 판단합니다.
GRU에는 두 개의 게이트가 있습니다. 첫 번째는 리셋 게이트입니다.
이 게이트는 "이전 정보를 얼마나 잊을 것인가"를 결정합니다. 리셋 게이트가 0에 가까우면 이전 상태를 거의 무시하고, 1에 가까우면 이전 상태를 온전히 사용합니다.
두 번째는 업데이트 게이트입니다. 이 게이트는 "새로운 정보를 얼마나 받아들일 것인가"를 결정합니다.
업데이트 게이트가 0에 가까우면 이전 상태를 그대로 유지하고, 1에 가까우면 새로운 상태로 완전히 교체합니다. 마치 수도꼭지 두 개가 있는 것과 같습니다.
하나는 오래된 물을 빼는 꼭지, 다른 하나는 새 물을 넣는 꼭지입니다. 이 두 꼭지를 적절히 조절해서 원하는 물 상태를 만드는 것이죠.
위 코드에서 핵심은 마지막 줄입니다. (1-z) * h_prev + z * h_candidate.
업데이트 게이트 z가 이전 상태와 새 상태를 어떤 비율로 섞을지 결정합니다. 이게 왜 장기 의존성에 도움이 될까요?
업데이트 게이트가 0에 가까우면 이전 상태가 거의 그대로 유지됩니다. 즉, 아주 오래된 정보도 손실 없이 전달될 수 있습니다.
기울기도 마찬가지로 손실 없이 흐를 수 있습니다. 실행 결과를 보면 입력에 따라 게이트 값이 달라지는 것을 확인할 수 있습니다.
모델이 학습하면서 "이 상황에서는 기억하고, 저 상황에서는 잊어야 한다"는 패턴을 스스로 터득합니다. 김개발 씨가 감탄했습니다.
"와, 모델이 스스로 뭘 기억하고 잊을지 결정하는군요!" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
그리고 LSTM은 여기서 한 발 더 나가요. 게이트가 세 개거든요."
실전 팁
💡 - GRU는 LSTM보다 파라미터가 적어 학습이 빠릅니다
- 데이터가 적거나 시퀀스가 짧을 때 GRU가 더 좋은 성능을 낼 수 있습니다
7. LSTM 장단기 메모리
드디어 LSTM을 배울 차례입니다. 김개발 씨는 기대에 부풀었습니다.
"GRU도 신기했는데, LSTM은 얼마나 대단한 거예요?" 박시니어 씨가 미소 지으며 답했습니다. "LSTM은 순환 신경망의 왕이라고 할 수 있어요.
1997년에 발표됐는데 아직도 현역이거든요."
LSTM은 Long Short-Term Memory의 약자로, 세 개의 게이트와 별도의 셀 상태를 가집니다. 입력 게이트, 삭제 게이트, 출력 게이트가 정보의 저장, 삭제, 출력을 정밀하게 제어합니다.
마치 잘 정리된 서랍장처럼 중요한 정보를 체계적으로 관리합니다.
다음 코드를 살펴봅시다.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Bidirectional
import numpy as np
# LSTM 기본 사용법
model = Sequential([
# return_sequences=True: 모든 타임스텝의 출력 반환
LSTM(64, input_shape=(100, 1), return_sequences=True),
# 두 번째 LSTM 레이어 스태킹
LSTM(32),
Dense(1)
])
model.summary()
# 양방향 LSTM: 앞뒤 문맥을 모두 활용
bi_model = Sequential([
Bidirectional(LSTM(32), input_shape=(100, 1)),
Dense(1)
])
# 실제 학습 예시: 사인파 예측
t = np.linspace(0, 100, 1000)
data = np.sin(t)
# 시퀀스 생성
def make_sequences(data, seq_len=50):
X, y = [], []
for i in range(len(data) - seq_len):
X.append(data[i:i+seq_len])
y.append(data[i+seq_len])
return np.array(X)[..., np.newaxis], np.array(y)
X, y = make_sequences(data)
model.compile(optimizer='adam', loss='mse')
model.fit(X, y, epochs=5, batch_size=32, verbose=1)
박시니어 씨가 LSTM의 역사를 들려주었습니다. "LSTM은 1997년에 Hochreiter와 Schmidhuber가 발표했어요.
기울기 소실 문제를 해결하려고 만든 건데, 30년 가까이 지난 지금도 널리 쓰이고 있어요." LSTM의 가장 큰 특징은 셀 상태라는 별도의 기억 저장소가 있다는 점입니다. GRU는 은닉 상태 하나로 모든 것을 처리했지만, LSTM은 은닉 상태와 셀 상태를 분리했습니다.
셀 상태를 컨베이어 벨트에 비유하면 이해하기 쉽습니다. 정보가 컨베이어 벨트 위에서 흘러가는데, 게이트들이 "무엇을 올리고, 무엇을 내리고, 무엇을 출력할지" 결정합니다.
첫 번째 게이트는 삭제 게이트입니다. "이 정보는 더 이상 필요 없으니 버리자"라고 결정합니다.
예를 들어 문장에서 주어가 바뀌면 이전 주어 정보를 삭제해야 합니다. 두 번째 게이트는 입력 게이트입니다.
"이 새로운 정보를 저장하자"라고 결정합니다. 새로운 주어가 등장하면 그 정보를 셀 상태에 기록합니다.
세 번째 게이트는 출력 게이트입니다. "셀 상태에서 이 부분만 출력하자"라고 결정합니다.
저장된 정보 전체가 아니라 현재 필요한 부분만 꺼내 쓰는 것입니다. 위 코드에서 몇 가지 실무 팁을 확인할 수 있습니다.
먼저 return_sequences=True 옵션입니다. 이 옵션을 켜면 매 타임스텝의 출력을 모두 반환합니다.
LSTM을 여러 층 쌓을 때 필수입니다. Bidirectional은 양방향 LSTM을 만듭니다.
앞에서 뒤로 읽는 LSTM과 뒤에서 앞으로 읽는 LSTM을 합칩니다. 자연어 처리에서 특히 유용합니다.
"그는 사과를 먹었다"에서 "그"가 누구인지 알려면 뒤의 문맥도 봐야 할 때가 있거든요. 코드의 마지막 부분은 실제 사인파를 예측하는 예시입니다.
LSTM이 주기적인 패턴을 잘 학습하는 것을 확인할 수 있습니다. 김개발 씨가 질문했습니다.
"LSTM과 GRU 중에 뭘 써야 해요?" 박시니어 씨가 대답했습니다. "정답은 없어요.
일반적으로 LSTM이 더 강력하지만, GRU가 더 빠르고 때로는 더 좋은 성능을 내기도 해요. 둘 다 시도해보고 결과가 좋은 쪽을 선택하세요." 김개발 씨는 오늘 배운 내용을 정리했습니다.
순차 데이터의 개념부터 시작해서 RNN, BPTT, 그리고 LSTM까지. 이제 자신의 주가 예측 프로젝트에 LSTM을 적용해볼 준비가 되었습니다.
실전 팁
💡 - LSTM 레이어를 쌓을 때는 마지막 레이어를 제외하고 return_sequences=True를 설정하세요
- 텍스트 분류에는 Bidirectional LSTM이 단방향보다 좋은 성능을 보이는 경우가 많습니다
- 과적합 방지를 위해 Dropout이나 recurrent_dropout 파라미터를 활용하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.