이미지 로딩 중...

LSTM 및 GRU 완벽 이해 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 24. · 3 Views

LSTM 및 GRU 완벽 이해 가이드

순환 신경망의 두 핵심 구조인 LSTM과 GRU를 초보자 눈높이에서 완벽하게 이해합니다. 시계열 데이터 처리의 강력한 무기인 이 두 알고리즘을 실무 코드와 함께 배워보세요.


목차

  1. LSTM의 기본 구조 이해하기
  2. LSTM의 세 가지 게이트 메커니즘
  3. GRU의 구조와 LSTM과의 차이점
  4. GRU의 게이트 메커니즘 상세 분석
  5. 양방향 LSTM과 Bidirectional 구조
  6. LSTM과 GRU를 이용한 시계열 예측 실전
  7. Attention 메커니즘과 LSTM의 결합
  8. Seq2Seq 모델 - Encoder-Decoder 구조
  9. 과적합 방지 - Dropout과 Regularization
  10. 하이퍼파라미터 튜닝과 모델 최적화

1. LSTM의 기본 구조 이해하기

시작하며

여러분이 문장을 읽을 때를 떠올려보세요. "오늘 날씨가 좋아서 산책을 갔는데..."라는 문장을 읽을 때, 여러분의 뇌는 '오늘', '날씨', '좋아서'라는 단어들을 기억하면서 문맥을 이해하죠.

하지만 전통적인 신경망은 이전 정보를 금방 잊어버리는 문제가 있었습니다. 마치 금붕어처럼 3초만 지나면 이전 내용을 까먹는 것처럼요.

이런 문제를 '그래디언트 소실(Gradient Vanishing)'이라고 부릅니다. 긴 문장이나 시계열 데이터를 처리할 때 치명적인 약점이 되었죠.

바로 이럴 때 필요한 것이 LSTM(Long Short-Term Memory)입니다. LSTM은 마치 여러분의 뇌처럼 중요한 정보는 오래 기억하고, 불필요한 정보는 잊어버리는 똑똑한 메커니즘을 가지고 있습니다.

개요

간단히 말해서, LSTM은 '기억의 문지기' 역할을 하는 특별한 신경망 구조입니다. 세 개의 게이트(문)를 통해 정보를 선택적으로 기억하고 잊어버립니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 주가 예측, 자연어 처리, 음성 인식 등 시간의 흐름이 중요한 모든 작업에서 LSTM은 필수적입니다. 예를 들어, 챗봇이 대화의 맥락을 이해하면서 답변하거나, 번역기가 긴 문장의 앞뒤 문맥을 파악하는 경우에 매우 유용합니다.

전통적인 RNN은 정보가 계속 흐르면서 희석되어 버렸다면, LSTM은 '셀 스테이트(Cell State)'라는 고속도로를 만들어서 중요한 정보를 먼 미래까지 전달할 수 있습니다. LSTM의 핵심 특징은 세 가지입니다.

첫째, 망각 게이트(Forget Gate)로 불필요한 정보를 걸러내고, 둘째, 입력 게이트(Input Gate)로 새로운 정보를 선택적으로 받아들이며, 셋째, 출력 게이트(Output Gate)로 다음 단계에 전달할 정보를 결정합니다. 이러한 특징들이 LSTM을 장기 의존성 문제의 해결사로 만들어줍니다.

코드 예제

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# LSTM 모델 생성 - 시계열 데이터 예측용
model = Sequential()

# LSTM 레이어 추가: 50개의 메모리 셀, 입력 형태는 (시간 스텝, 특성 개수)
model.add(LSTM(50, activation='tanh', input_shape=(10, 1), return_sequences=True))

# 두 번째 LSTM 레이어: 다층 구조로 더 복잡한 패턴 학습
model.add(LSTM(50, activation='tanh'))

# 출력 레이어: 다음 값 하나를 예측
model.add(Dense(1))

# 모델 컴파일: 평균 제곱 오차로 손실 계산
model.compile(optimizer='adam', loss='mse')

설명

이것이 하는 일: 위 코드는 시계열 데이터를 처리하기 위한 다층 LSTM 네트워크를 구축합니다. 예를 들어 지난 10일의 주가 데이터를 보고 내일 주가를 예측하는 모델을 만드는 것입니다.

첫 번째로, Sequential() 모델을 생성하고 첫 번째 LSTM 레이어를 추가합니다. 여기서 50은 LSTM 셀의 개수를 의미하는데, 이는 모델이 50개의 서로 다른 패턴을 동시에 추적할 수 있다는 뜻입니다.

return_sequences=True는 "각 시간 스텝마다 출력을 내보내라"는 의미로, 다음 LSTM 레이어에 시퀀스 정보를 전달하기 위해 필요합니다. 두 번째로, input_shape=(10, 1)은 "10개의 시간 스텝, 각 스텝마다 1개의 특성"을 의미합니다.

예를 들어 지난 10일의 주가(1차원 데이터)를 입력으로 받는다는 뜻이죠. activation='tanh'는 LSTM 내부에서 -1부터 1 사이의 값으로 정규화하여 학습을 안정화시킵니다.

세 번째로, 두 번째 LSTM 레이어가 첫 번째 레이어의 출력을 받아서 더 추상적인 패턴을 학습합니다. 이 레이어는 return_sequences=False(기본값)이므로 마지막 시간 스텝의 출력만 Dense 레이어로 전달합니다.

Dense 레이어는 최종적으로 1개의 값(내일의 예측 주가)을 출력합니다. 여러분이 이 코드를 사용하면 주식 가격, 온도, 판매량 등 시간에 따라 변하는 모든 데이터를 예측할 수 있습니다.

다층 구조 덕분에 단순한 선형 패턴뿐만 아니라 복잡한 비선형 관계도 학습할 수 있고, Adam 옵티마이저가 자동으로 학습률을 조정해주어 안정적인 학습이 가능하며, 평균 제곱 오차(MSE)로 예측값과 실제값의 차이를 최소화합니다.

실전 팁

💡 LSTM 유닛 개수는 데이터 복잡도에 따라 조정하세요. 간단한 패턴은 20-50개, 복잡한 패턴은 100-200개가 적당합니다. 너무 많으면 과적합, 너무 적으면 과소적합이 발생합니다.

💡 흔한 실수: return_sequences를 잘못 설정하는 경우가 많습니다. LSTM을 여러 층 쌓을 때는 마지막 층 전까지 모두 return_sequences=True로 설정해야 합니다.

💡 배치 정규화(Batch Normalization)를 LSTM 레이어 사이에 추가하면 학습 속도가 2-3배 빨라집니다. model.add(BatchNormalization())을 LSTM 레이어 다음에 넣어보세요.

💡 드롭아웃(Dropout)을 0.2-0.3 정도로 설정하면 과적합을 효과적으로 방지할 수 있습니다. LSTM(50, dropout=0.2, recurrent_dropout=0.2)처럼 사용하세요.

💡 시계열 데이터는 반드시 정규화(Normalization)하세요. MinMaxScaler나 StandardScaler로 0-1 범위로 만들면 학습이 훨씬 안정적입니다.


2. LSTM의 세 가지 게이트 메커니즘

시작하며

여러분이 집에 들어올 때를 생각해보세요. 현관에서 신발을 벗고, 중요한 물건은 제자리에 두고, 쓰레기는 버리죠.

LSTM도 정확히 이런 식으로 작동합니다. 정보가 들어올 때 어떤 것을 버리고, 어떤 것을 저장하고, 어떤 것을 내보낼지 결정하는 세 개의 '문'이 있습니다.

이 문들이 없다면 LSTM은 모든 정보를 무조건 받아들이거나 무조건 거부해야 합니다. 마치 집에 들어올 때 신발을 신은 채로 들어가거나, 아예 집에 들어가지 못하는 극단적인 상황인 셈이죠.

실제로 이런 문제 때문에 초기 RNN은 긴 문장을 처리하지 못했습니다. 바로 이럴 때 필요한 것이 게이트 메커니즘입니다.

각 게이트는 0부터 1 사이의 값을 출력하여 정보를 얼마나 통과시킬지 조절합니다. 0이면 완전히 차단, 1이면 완전히 통과, 0.5면 절반만 통과시키는 식입니다.

개요

간단히 말해서, LSTM의 세 게이트는 '망각 게이트(잊기)', '입력 게이트(받아들이기)', '출력 게이트(내보내기)'로 구성됩니다. 각각이 시그모이드 함수를 사용해 0-1 사이의 값으로 정보 흐름을 조절합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 자연어 처리에서 문장의 주어가 바뀌면 이전 주어는 잊어야 하고, 새로운 주어의 성별이나 단수/복수 정보는 기억해야 합니다. 예를 들어, "철수가 밥을 먹었다.

그는..."에서 '철수'라는 정보를 '그는'까지 유지해야 올바른 동사 형태를 선택할 수 있죠. 전통적인 RNN은 모든 정보를 동일하게 처리했다면, LSTM의 게이트는 마치 영리한 비서처럼 중요도에 따라 정보를 분류합니다.

"이 정보는 중요하니 오래 보관하고, 이 정보는 일시적이니 곧 버리자"는 식으로 판단합니다. 게이트의 핵심 특징은 세 가지입니다.

첫째, 시그모이드 활성화 함수로 0-1 범위의 가중치를 생성하고, 둘째, 현재 입력과 이전 hidden state를 모두 고려하여 결정하며, 셋째, 요소별 곱셈(element-wise multiplication)으로 정보를 필터링합니다. 이러한 특징들이 LSTM을 학습 가능한 지능적 메모리로 만들어줍니다.

코드 예제

import numpy as np

# LSTM 게이트 동작 시뮬레이션
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 현재 입력, 이전 hidden state, Cell state
x_t = np.array([0.5])  # 현재 시점 입력
h_prev = np.array([0.3])  # 이전 hidden state
c_prev = np.array([0.7])  # 이전 Cell state

# 게이트 가중치 (실제로는 학습됨)
W_f, W_i, W_o = 0.5, 0.6, 0.4  # 각 게이트의 가중치

# 1. 망각 게이트: 이전 정보를 얼마나 잊을까?
f_t = sigmoid(W_f * x_t + h_prev)
print(f"망각 게이트: {f_t[0]:.3f} (0에 가까울수록 많이 잊음)")

# 2. 입력 게이트: 새 정보를 얼마나 받아들일까?
i_t = sigmoid(W_i * x_t + h_prev)
c_candidate = np.tanh(x_t + h_prev)  # 후보 값
print(f"입력 게이트: {i_t[0]:.3f}, 후보값: {c_candidate[0]:.3f}")

# 3. Cell state 업데이트: 옛날 기억 * 망각 + 새 기억 * 입력
c_t = f_t * c_prev + i_t * c_candidate
print(f"새 Cell state: {c_t[0]:.3f}")

# 4. 출력 게이트: 얼마나 밖으로 내보낼까?
o_t = sigmoid(W_o * x_t + h_prev)
h_t = o_t * np.tanh(c_t)
print(f"출력 게이트: {o_t[0]:.3f}, 최종 출력: {h_t[0]:.3f}")

설명

이것이 하는 일: 위 코드는 LSTM의 세 가지 게이트가 어떻게 작동하는지 수학적으로 시뮬레이션합니다. 실제 LSTM 셀 하나의 내부 동작을 단계별로 보여주는 것이죠.

첫 번째로, 망각 게이트 f_t가 계산됩니다. sigmoid(W_f * x_t + h_prev)는 현재 입력과 이전 상태를 보고 "이전 Cell state를 얼마나 유지할까?"를 결정합니다.

예를 들어 f_t가 0.8이면 이전 기억의 80%를 유지하고 20%를 잊어버리는 것입니다. 문장에서 주어가 바뀌면 이 값이 낮아져서 이전 주어 정보를 잊습니다.

두 번째로, 입력 게이트 i_t와 후보값 c_candidate가 계산됩니다. 입력 게이트는 "새로운 정보를 얼마나 받아들일까?"를 결정하고, tanh로 생성된 후보값은 실제로 저장할 정보의 내용입니다.

이 둘을 곱하면 "선택된 새로운 기억"이 됩니다. 세 번째로, Cell state 업데이트가 일어납니다.

f_t * c_prev는 "걸러진 옛 기억", i_t * c_candidate는 "선택된 새 기억"이고, 이 둘을 더하면 최신 Cell state가 됩니다. 이것이 바로 LSTM의 "장기 기억 고속도로"입니다.

이 값은 여러 시간 스텝을 거쳐도 그래디언트 소실 없이 전달됩니다. 네 번째로, 출력 게이트 o_t가 Cell state를 필터링하여 최종 hidden state h_t를 만듭니다.

Cell state는 "내부 메모리"이고 hidden state는 "외부로 공개할 정보"입니다. 마치 여러분의 머릿속 모든 생각(Cell state)과 실제로 말로 꺼내는 내용(hidden state)이 다른 것처럼요.

여러분이 이 코드를 사용하면 LSTM이 왜 긴 문장도 처리할 수 있는지, 어떻게 중요한 정보를 선택적으로 기억하는지 수학적으로 이해할 수 있습니다. 각 게이트의 가중치는 학습 과정에서 자동으로 최적화되며, Cell state 덕분에 그래디언트가 100개 이상의 시간 스텝을 역전파할 수 있고, 게이트 메커니즘으로 불필요한 정보는 자동으로 걸러집니다.

실전 팁

💡 망각 게이트의 초기 바이어스를 1로 설정하면 학습 초기에 정보를 더 오래 유지합니다. bias_initializer='ones'를 사용해보세요.

💡 흔한 실수: 게이트 값이 0 또는 1로 수렴하면 그래디언트가 죽습니다. 학습 중 게이트 값 분포를 모니터링하여 0.1-0.9 범위를 유지하는지 확인하세요.

💡 Cell state가 폭발적으로 커지는 것을 방지하려면 Gradient Clipping을 사용하세요. tf.keras.optimizers.Adam(clipnorm=1.0)처럼 설정합니다.

💡 게이트 가중치를 시각화하면 모델이 어떤 정보에 주목하는지 알 수 있습니다. 텐서보드(TensorBoard)로 가중치 히스토그램을 확인해보세요.

💡 Peephole Connection을 추가하면 게이트가 Cell state도 직접 참조하여 더 정확한 판단을 합니다. 고급 LSTM 구현에서 사용해보세요.


3. GRU의 구조와 LSTM과의 차이점

시작하며

여러분이 짐을 꾸릴 때, 큰 여행 가방에 모든 걸 체계적으로 정리하는 방법도 있고, 작은 배낭에 꼭 필요한 것만 넣는 방법도 있죠. LSTM이 전자라면 GRU(Gated Recurrent Unit)는 후자입니다.

"정말 세 개의 게이트가 모두 필요할까?"라는 의문에서 GRU가 탄생했습니다. 실제 연구에서 LSTM의 세 게이트 중 일부는 비슷한 역할을 하며 중복된다는 것이 밝혀졌습니다.

마치 집에 현관문, 방문, 베란다문이 있는데 항상 동시에 열고 닫는다면 굳이 세 개가 필요 없는 것처럼요. 이런 중복을 제거하면 더 단순하고 빠른 모델을 만들 수 있습니다.

바로 이럴 때 필요한 것이 GRU입니다. GRU는 LSTM의 세 게이트를 두 개로 줄이고, Cell state와 hidden state를 하나로 합쳐서 구조를 단순화했습니다.

결과적으로 학습 속도가 빠르고 파라미터가 적어서 적은 데이터로도 잘 작동합니다.

개요

간단히 말해서, GRU는 LSTM의 경량화 버전입니다. 업데이트 게이트와 리셋 게이트 두 개만으로 LSTM과 비슷한 성능을 냅니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 모바일 앱이나 임베디드 시스템처럼 컴퓨팅 자원이 제한된 환경에서는 GRU가 더 적합합니다. 예를 들어, 스마트폰에서 실시간 음성 인식을 할 때 LSTM은 너무 무겁지만 GRU는 충분히 빠르게 동작합니다.

또한 데이터가 부족할 때도 GRU가 과적합을 덜 일으킵니다. LSTM은 세 개의 게이트로 정보를 세밀하게 제어했다면, GRU는 두 개의 게이트로 "이전 정보를 얼마나 유지할까?"와 "과거 정보를 얼마나 참고할까?"만 결정합니다.

더 단순하지만 대부분의 경우 충분히 효과적입니다. GRU의 핵심 특징은 세 가지입니다.

첫째, 파라미터가 LSTM보다 약 25% 적어서 학습이 빠르고, 둘째, Cell state 없이 hidden state만 사용하여 구조가 단순하며, 셋째, 짧은 시퀀스나 적은 데이터에서 LSTM보다 좋은 성능을 보입니다. 이러한 특징들이 GRU를 효율성의 대명사로 만들어줍니다.

코드 예제

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import GRU, Dense

# GRU 모델 생성 - LSTM과 비교
model = Sequential()

# GRU 레이어: LSTM과 동일한 50개 유닛 사용
# 파라미터 수는 LSTM의 75% 정도
model.add(GRU(50, activation='tanh', input_shape=(10, 1), return_sequences=True))

# 두 번째 GRU 레이어
model.add(GRU(50, activation='tanh'))

# 출력 레이어 (LSTM과 동일)
model.add(Dense(1))

# 모델 컴파일
model.compile(optimizer='adam', loss='mse')

# 파라미터 비교를 위한 모델 서머리
model.summary()

# LSTM vs GRU 파라미터 수 비교
# LSTM(50): 약 10,400개 파라미터
# GRU(50): 약 7,800개 파라미터 (25% 감소)

설명

이것이 하는 일: 위 코드는 GRU 기반 시계열 예측 모델을 만들고, LSTM과의 파라미터 수 차이를 비교합니다. 동일한 작업을 더 적은 자원으로 수행하는 것이 목표입니다.

첫 번째로, GRU(50, ...)은 50개의 GRU 유닛을 생성합니다. LSTM과 동일한 유닛 수를 사용하지만, 내부 구조가 단순해서 파라미터 수는 약 7,800개로 LSTM의 10,400개보다 25% 적습니다.

이는 메모리 사용량과 계산 시간 모두를 줄여줍니다. 두 번째로, activation='tanh'는 GRU에서도 동일하게 사용됩니다.

하지만 GRU는 Cell state가 없고 hidden state만 사용하므로, 정보가 tanh를 한 번만 통과합니다. LSTM은 Cell state와 hidden state에서 각각 tanh를 사용하는 반면, GRU는 더 직접적인 정보 흐름을 가집니다.

세 번째로, 다층 GRU 구조를 만들 때도 LSTM과 동일하게 return_sequences=True를 사용합니다. 첫 번째 GRU는 모든 시간 스텝의 출력을 내보내고, 두 번째 GRU는 마지막 스텝만 출력합니다.

이 구조는 LSTM과 완전히 호환되므로 쉽게 교체할 수 있습니다. 네 번째로, model.summary()를 호출하면 정확한 파라미터 수를 확인할 수 있습니다.

GRU는 업데이트 게이트와 리셋 게이트만 있으므로 가중치 행렬이 3개(LSTM은 4개)입니다. 각 게이트마다 입력 가중치 W와 순환 가중치 U가 있고, 바이어스까지 합치면 3 * (input_dim + hidden_dim + 1) * hidden_dim개의 파라미터가 됩니다.

여러분이 이 코드를 사용하면 LSTM과 비슷한 성능을 25% 적은 자원으로 얻을 수 있습니다. 학습 시간이 20-30% 단축되고, 모바일이나 웹 배포 시 모델 크기가 작아지며, 적은 데이터셋(수천 개 샘플)에서도 과적합이 덜 발생합니다.

실전 팁

💡 데이터가 10,000개 미만이면 GRU를 먼저 시도하세요. 적은 데이터에서는 GRU가 LSTM보다 일반화를 더 잘 합니다.

💡 흔한 실수: "GRU가 항상 빠르다"는 오해. GPU에서는 최적화된 LSTM이 더 빠를 수 있습니다. 실제로 벤치마크 테스트를 해보세요.

💡 시퀀스 길이가 짧을 때(< 50 스텝) GRU를 사용하고, 매우 긴 시퀀스(> 200 스텝)에서는 LSTM의 세밀한 제어가 더 유리합니다.

💡 모델 배포 시 TensorFlow Lite로 변환하면 GRU가 LSTM보다 변환 후 크기가 30-40% 작아집니다. 모바일 앱에 최적입니다.

💡 그리드 서치로 LSTM과 GRU를 모두 테스트해보세요. 데이터 특성에 따라 성능이 다르므로 실험적으로 확인하는 것이 최선입니다.


4. GRU의 게이트 메커니즘 상세 분석

시작하며

여러분이 SNS를 볼 때를 생각해보세요. 새로운 게시물이 올라오면 (1) 기존에 보던 내용을 얼마나 잊을지, (2) 새 게시물 중 어떤 부분에 집중할지를 동시에 결정하죠.

GRU의 두 게이트도 정확히 이렇게 작동합니다. LSTM의 세 게이트가 "잊기", "받기", "내보내기"를 따로따로 처리했다면, GRU는 "잊기와 받기"를 하나의 게이트로 통합했습니다.

예를 들어 오래된 정보를 50% 잊으면 자동으로 새 정보를 50% 받아들이는 식이죠. 이렇게 하면 게이트 하나를 줄일 수 있습니다.

바로 이럴 때 필요한 것이 GRU의 업데이트 게이트입니다. 이 게이트는 "과거와 현재의 균형"을 조절하는 스마트한 스위치 역할을 합니다.

값이 1에 가까우면 과거를 유지하고, 0에 가까우면 새로운 정보를 받아들입니다.

개요

간단히 말해서, GRU의 두 게이트는 '업데이트 게이트(과거 vs 현재 비율)'와 '리셋 게이트(과거 정보 참조 정도)'입니다. 이 둘만으로 LSTM의 세 게이트 역할을 모두 수행합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 번역 모델에서 문장 구조가 바뀔 때 리셋 게이트가 이전 문법 정보를 초기화하고, 업데이트 게이트가 새로운 문맥을 얼마나 반영할지 결정합니다. 예를 들어, "I love you"를 "나는 너를 사랑해"로 번역할 때 영어의 SVO 구조를 리셋하고 한국어의 SOV 구조를 업데이트하는 것이죠.

LSTM은 망각과 입력을 독립적으로 제어했다면, GRU는 "망각 = 1 - 입력"이라는 관계를 가정하여 하나의 게이트로 통합했습니다. 이는 대부분의 실제 데이터에서 유효한 가정이며, 파라미터 효율성을 크게 높입니다.

GRU 게이트의 핵심 특징은 세 가지입니다. 첫째, 업데이트 게이트 z가 과거 정보 유지량을 직접 제어하고 (1-z)가 자동으로 새 정보 수용량이 되며, 둘째, 리셋 게이트 r이 과거 hidden state를 얼마나 참고할지 결정하고, 셋째, 이 두 게이트만으로 LSTM 수준의 장기 의존성을 학습합니다.

이러한 특징들이 GRU를 "단순하지만 강력한" 구조로 만들어줍니다.

코드 예제

import numpy as np

# GRU 게이트 동작 시뮬레이션
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 현재 입력과 이전 hidden state
x_t = np.array([0.5])  # 현재 입력
h_prev = np.array([0.8])  # 이전 hidden state

# 게이트 가중치 (실제로는 학습됨)
W_z, W_r, W_h = 0.6, 0.4, 0.5  # 업데이트, 리셋, 후보 가중치

# 1. 업데이트 게이트: 과거를 얼마나 유지할까?
z_t = sigmoid(W_z * x_t + h_prev)
print(f"업데이트 게이트: {z_t[0]:.3f}")
print(f"  -> 과거 유지: {z_t[0]:.1%}, 새 정보: {(1-z_t[0]):.1%}")

# 2. 리셋 게이트: 과거 정보를 얼마나 참고할까?
r_t = sigmoid(W_r * x_t + h_prev)
print(f"\n리셋 게이트: {r_t[0]:.3f}")
print(f"  -> 과거 참조: {r_t[0]:.1%}")

# 3. 후보 hidden state: 리셋된 과거 + 현재 입력
h_candidate = np.tanh(W_h * x_t + r_t * h_prev)
print(f"\n후보 hidden state: {h_candidate[0]:.3f}")

# 4. 최종 hidden state: 과거와 후보의 가중 평균
h_t = z_t * h_prev + (1 - z_t) * h_candidate
print(f"\n최종 hidden state: {h_t[0]:.3f}")
print(f"  -> 과거 {z_t[0]:.1%} + 새로운 {(1-z_t[0]):.1%}")

설명

이것이 하는 일: 위 코드는 GRU의 두 게이트가 어떻게 협력하여 정보를 처리하는지 단계별로 시뮬레이션합니다. LSTM보다 단순하지만 효과적인 메커니즘을 보여줍니다.

첫 번째로, 업데이트 게이트 z_t가 계산됩니다. 이 값이 0.7이면 "과거 정보를 70% 유지하고 새 정보를 30% 반영하자"는 의미입니다.

LSTM과 달리 GRU는 하나의 게이트로 양쪽을 동시에 제어합니다. 마치 시소처럼 한쪽이 올라가면 다른 쪽이 내려가는 구조죠.

두 번째로, 리셋 게이트 r_t가 계산됩니다. 이것은 "과거 hidden state를 얼마나 참조할까?"를 결정합니다.

예를 들어 r_t가 0.2로 낮으면 과거를 거의 무시하고 현재 입력에만 집중합니다. 새로운 문단이 시작되거나 주제가 바뀔 때 이 값이 낮아집니다.

세 번째로, 후보 hidden state h_candidate가 생성됩니다. 여기서 핵심은 r_t * h_prev 부분입니다.

리셋 게이트가 과거 정보를 필터링한 후 현재 입력과 결합합니다. 만약 r_t가 0이면 과거를 완전히 무시하고 현재 입력만으로 후보를 만듭니다.

tanh 활성화로 -1부터 1 사이로 정규화됩니다. 네 번째로, 최종 hidden state가 z_t * h_prev + (1 - z_t) * h_candidate 공식으로 계산됩니다.

이것은 가중 평균입니다. z_t가 0.7이면 "과거 70% + 새로운 후보 30%"를 섞습니다.

이 간단한 선형 보간(linear interpolation)이 놀랍게도 LSTM의 복잡한 Cell state 업데이트만큼 효과적입니다. 여러분이 이 코드를 사용하면 GRU가 왜 파라미터가 적으면서도 강력한지 이해할 수 있습니다.

업데이트 게이트 하나로 과거/현재 비율을 동시에 제어하여 게이트 수를 줄이고, 리셋 게이트로 과거 정보의 관련성을 선택적으로 조절하며, Cell state 없이 hidden state만 사용하여 구조를 단순화합니다.

실전 팁

💡 업데이트 게이트 값이 항상 0.5 근처라면 모델이 제대로 학습 안 된 것입니다. 학습 데이터를 늘리거나 정규화를 확인하세요.

💡 흔한 실수: 리셋 게이트를 망각 게이트로 착각하기 쉽습니다. 리셋은 "과거 참조 정도"이고, 실제 망각은 업데이트 게이트가 담당합니다.

💡 게이트 값을 시각화하면 모델이 언제 정보를 리셋하는지 알 수 있습니다. 문장 경계나 주제 전환 지점에서 리셋 게이트가 낮아지는 패턴을 확인하세요.

💡 GRU의 초기 바이어스를 0으로 설정하면 학습 초기에 과거와 현재를 50:50으로 섞습니다. 이것이 일반적으로 안정적입니다.

💡 배치 크기를 늘리면 업데이트/리셋 게이트의 안정성이 높아집니다. 최소 32 이상의 배치 크기를 권장합니다.


5. 양방향 LSTM과 Bidirectional 구조

시작하며

여러분이 문장을 읽을 때 "그는 좋아한다"라는 문장만 보면 무엇을 좋아하는지 알 수 없죠. 하지만 뒤에 "아이스크림을"이 나오면 의미가 완성됩니다.

이처럼 미래 정보가 과거를 이해하는 데 도움을 줄 때가 많습니다. 일반적인 LSTM은 왼쪽에서 오른쪽으로만 정보를 처리합니다.

마치 책을 앞에서 뒤로만 읽는 것처럼요. 하지만 번역, 감성 분석, 품사 태깅 같은 작업에서는 전체 문장을 보고 판단하는 것이 훨씬 정확합니다.

"This movie is not bad"에서 "bad" 앞의 "not"을 놓치면 의미가 반대가 되죠. 바로 이럴 때 필요한 것이 양방향 LSTM(Bidirectional LSTM)입니다.

한 번은 앞에서 뒤로, 한 번은 뒤에서 앞으로 읽어서 두 방향의 정보를 모두 활용합니다. 마치 여러분이 중요한 문서를 읽을 때 한 번 읽고 다시 뒤에서부터 확인하는 것처럼요.

개요

간단히 말해서, 양방향 LSTM은 두 개의 독립적인 LSTM을 반대 방향으로 실행하고 그 결과를 결합합니다. 이렇게 하면 각 시점에서 과거와 미래 정보를 모두 사용할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 자연어 처리에서 단어의 의미는 문맥에 따라 달라집니다. 예를 들어, "사과"라는 단어가 "사과를 먹었다"에서는 과일이지만 "사과를 받았다"에서는 사죄를 의미합니다.

뒤에 오는 단어를 미리 알아야 정확한 해석이 가능하죠. 감정 분석에서도 "최고의 영화...

라고 하기엔 너무 지루했다" 같은 문장은 전체를 봐야 부정적 감정을 파악할 수 있습니다. 일반 LSTM은 순차적으로만 처리했다면, 양방향 LSTM은 병렬적으로 두 방향을 동시에 처리합니다.

Forward LSTM은 "The cat sat on the"까지 읽고 다음 단어를 예측하고, Backward LSTM은 "mat"부터 거슬러 올라가며 정보를 수집합니다. 양방향 LSTM의 핵심 특징은 세 가지입니다.

첫째, 파라미터가 정확히 2배로 늘어나지만(두 개의 LSTM을 사용하므로) 정확도 향상이 그 이상이고, 둘째, 각 시점에서 과거와 미래 컨텍스트를 모두 활용하여 더 풍부한 표현을 학습하며, 셋째, 실시간 스트리밍에는 부적합하지만 배치 처리에는 매우 효과적입니다. 이러한 특징들이 양방향 LSTM을 NLP 필수 도구로 만들어줍니다.

코드 예제

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Bidirectional, LSTM, Dense, Embedding

# 양방향 LSTM 모델 - 감정 분석 예제
model = Sequential()

# 단어 임베딩 레이어: 10,000개 단어를 128차원으로
model.add(Embedding(input_dim=10000, output_dim=128, input_length=100))

# 양방향 LSTM: forward와 backward를 동시에 실행
# merge_mode='concat'으로 두 방향 출력을 연결 (기본값)
model.add(Bidirectional(LSTM(64, return_sequences=True)))

# 두 번째 양방향 LSTM
model.add(Bidirectional(LSTM(32)))

# 출력 레이어: 긍정(1) vs 부정(0) 이진 분류
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

# 모델 구조 확인
model.summary()
# Bidirectional(LSTM(64))의 출력 차원: 64 * 2 = 128
# forward LSTM 64개 + backward LSTM 64개

설명

이것이 하는 일: 위 코드는 영화 리뷰 같은 텍스트를 긍정/부정으로 분류하는 양방향 LSTM 모델을 만듭니다. 전체 문장의 문맥을 양방향으로 파악하여 정확한 감정을 판단합니다.

첫 번째로, Embedding 레이어가 단어를 숫자 벡터로 변환합니다. 10,000개의 단어 사전에서 각 단어를 128차원 벡터로 표현합니다.

예를 들어 "good"은 [0.2, -0.5, 0.8, ...]과 같은 벡터가 되고, 비슷한 의미의 단어들은 벡터 공간에서 가까이 위치합니다. 두 번째로, Bidirectional(LSTM(64, return_sequences=True))가 핵심입니다.

이것은 내부적으로 두 개의 독립적인 LSTM을 생성합니다. Forward LSTM은 "This movie is not bad"를 순서대로 읽고, Backward LSTM은 "bad not is movie This" 순서로 읽습니다.

각각 64차원 출력을 만들어서 연결(concat)하면 128차원이 됩니다. 세 번째로, return_sequences=True는 각 단어 위치마다 출력을 생성합니다.

100개 단어가 있으면 (100, 128) 형태의 출력이 나옵니다. 두 번째 Bidirectional LSTM은 이 시퀀스를 입력받아 다시 양방향으로 처리하고, return_sequences=False(기본값)로 마지막 출력만 (64,) 형태로 내보냅니다.

양방향이므로 실제로는 (32*2 = 64,)입니다. 네 번째로, Dense 레이어가 최종 감정 점수를 출력합니다.

sigmoid 활성화로 0-1 사이 값을 만들고, 0.5보다 크면 긍정, 작으면 부정으로 분류합니다. 양방향 처리 덕분에 "not bad" 같은 부정의 부정 표현이나 "but" 이후의 반전 같은 복잡한 패턴도 정확히 포착합니다.

여러분이 이 코드를 사용하면 일반 LSTM보다 5-10% 높은 정확도를 얻을 수 있습니다. 문장 전체의 문맥을 파악하여 미묘한 감정 변화를 감지하고, 부정어나 접속사의 영향을 정확히 반영하며, 주어-서술어 관계처럼 멀리 떨어진 단어 간 연관성도 학습합니다.

실전 팁

💡 양방향 LSTM은 실시간 처리가 불가능합니다. 전체 시퀀스를 미리 알아야 하므로 챗봇 같은 실시간 응답에는 단방향을 사용하세요.

💡 흔한 실수: merge_mode 설정을 잘못하는 경우. 'concat'(기본값)은 차원을 2배로, 'sum'은 동일하게, 'ave'는 평균을 냅니다. 대부분 concat이 최선입니다.

💡 GPU 메모리가 부족하면 LSTM 유닛 수를 줄이거나 GRU로 대체하세요. Bidirectional(GRU(32))는 Bidirectional(LSTM(64))보다 50% 적은 메모리를 사용합니다.

💡 첫 번째 Bidirectional 레이어에만 return_sequences=True를 사용하고, 마지막은 False로 하는 것이 일반적입니다. 모든 레이어를 True로 하면 계산량이 급증합니다.

💡 Attention 메커니즘과 함께 사용하면 성능이 극대화됩니다. 양방향 LSTM 위에 Attention 레이어를 추가하여 중요한 단어에 집중하도록 하세요.


6. LSTM과 GRU를 이용한 시계열 예측 실전

시작하며

여러분이 내일의 주가를 예측한다고 생각해보세요. 어제의 가격만 보면 안 되고, 지난 한 달의 추세, 거래량, 변동성 등을 종합적으로 봐야 합니다.

이것이 바로 시계열 예측의 핵심입니다. 전통적인 방법인 ARIMA나 선형 회귀는 단순한 선형 패턴만 포착합니다.

하지만 실제 시계열 데이터는 비선형적이고 복잡한 패턴을 가지고 있죠. 계절성, 트렌드, 돌발 이벤트 등이 복합적으로 작용합니다.

단순한 통계 모델로는 한계가 명확합니다. 바로 이럴 때 필요한 것이 LSTM/GRU 기반 시계열 예측입니다.

과거의 복잡한 패턴을 학습하고, 장기 의존성을 파악하며, 비선형 관계를 모델링하여 정확한 예측을 만들어냅니다.

개요

간단히 말해서, 시계열 예측은 과거 데이터의 패턴을 학습하여 미래 값을 예측하는 작업입니다. LSTM/GRU는 시간적 의존성을 자동으로 학습하여 전통적 방법보다 우수한 성능을 냅니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 재고 관리에서 다음 달 수요를 예측하거나, 에너지 분야에서 전력 사용량을 예측하거나, 금융에서 환율을 예측하는 등 거의 모든 산업에서 활용됩니다. 예를 들어, 아마존은 LSTM으로 수백만 제품의 수요를 예측하여 재고를 최적화하고 배송 시간을 단축합니다.

전통적인 ARIMA 같은 방법은 선형 추세만 모델링했다면, LSTM/GRU는 복잡한 계절성 패턴, 갑작스러운 변화, 여러 변수 간 상호작용까지 모두 학습합니다. 또한 수동으로 특징을 설계할 필요 없이 원시 데이터에서 자동으로 패턴을 추출합니다.

시계열 예측의 핵심 특징은 세 가지입니다. 첫째, 슬라이딩 윈도우 방식으로 과거 N개 시점을 입력으로 받아 다음 시점을 예측하고, 둘째, 다변량 예측(여러 특징을 동시에 고려)과 다단계 예측(여러 시점 앞을 예측)이 가능하며, 셋째, 실시간으로 새 데이터를 받아 모델을 업데이트할 수 있습니다.

이러한 특징들이 LSTM/GRU를 시계열 예측의 표준으로 만들어줍니다.

코드 예제

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
from sklearn.preprocessing import MinMaxScaler

# 1. 데이터 준비: 슬라이딩 윈도우 생성
def create_sequences(data, seq_length):
    X, y = [], []
    for i in range(len(data) - seq_length):
        # 과거 seq_length개 시점 -> 다음 1개 시점
        X.append(data[i:i+seq_length])
        y.append(data[i+seq_length])
    return np.array(X), np.array(y)

# 예제 데이터: 100일치 주가 (실제로는 실제 데이터 사용)
prices = np.sin(np.arange(100) * 0.1) + np.random.normal(0, 0.1, 100)

# 2. 정규화: 0-1 범위로 스케일링 (LSTM은 정규화된 데이터에서 잘 작동)
scaler = MinMaxScaler()
prices_scaled = scaler.fit_transform(prices.reshape(-1, 1))

# 3. 시퀀스 생성: 과거 10일로 내일 예측
seq_length = 10
X, y = create_sequences(prices_scaled, seq_length)
X = X.reshape((X.shape[0], X.shape[1], 1))  # (샘플수, 시간스텝, 특징수)

# 4. LSTM 모델 구축
model = Sequential([
    LSTM(50, activation='relu', input_shape=(seq_length, 1)),
    Dense(1)  # 다음 1개 시점 예측
])
model.compile(optimizer='adam', loss='mse')

# 5. 학습
model.fit(X, y, epochs=50, batch_size=16, verbose=0)

# 6. 예측
last_sequence = prices_scaled[-seq_length:].reshape(1, seq_length, 1)
next_price = model.predict(last_sequence, verbose=0)
next_price_original = scaler.inverse_transform(next_price)
print(f"다음 시점 예측값: {next_price_original[0][0]:.2f}")

설명

이것이 하는 일: 위 코드는 주가 같은 시계열 데이터를 LSTM으로 예측하는 전체 파이프라인을 구현합니다. 데이터 전처리부터 모델 학습, 예측까지 실무에서 바로 사용할 수 있는 완전한 예제입니다.

첫 번째로, create_sequences 함수가 슬라이딩 윈도우 방식으로 학습 데이터를 만듭니다. 예를 들어 100일치 데이터가 있으면 1-10일을 입력, 11일을 출력으로 하는 첫 샘플을 만들고, 다음은 2-11일 입력, 12일 출력으로 만듭니다.

이렇게 90개의 학습 샘플이 생성됩니다. 이 방법이 시계열 딥러닝의 핵심 데이터 준비 기법입니다.

두 번째로, MinMaxScaler로 데이터를 0-1 범위로 정규화합니다. LSTM은 tanh와 sigmoid를 사용하므로 입력이 -1~1 또는 0-1 범위일 때 가장 잘 작동합니다.

정규화 없이 학습하면 그래디언트가 폭발하거나 소실되어 학습이 안 됩니다. 주의할 점은 예측값을 inverse_transform으로 원래 스케일로 되돌려야 실제 가격을 얻는다는 것입니다.

세 번째로, 데이터를 (샘플수, 시간스텝, 특징수) 형태로 reshape합니다. LSTM은 3차원 입력을 요구하는데, 여기서는 (90, 10, 1)이 됩니다.

90개 샘플, 각 샘플은 10일치 데이터, 각 날은 1개 특징(가격)을 가집니다. 만약 거래량, 변동성 등 여러 특징을 사용하면 마지막 차원이 늘어납니다.

네 번째로, 모델을 학습하고 예측합니다. epochs=50으로 데이터를 50번 반복 학습하고, batch_size=16으로 한 번에 16개 샘플씩 처리합니다.

예측 시에는 가장 최근 10일 데이터를 입력으로 사용하여 내일을 예측합니다. 이 예측값으로 다시 11일 후를 예측하는 식으로 반복하면 장기 예측도 가능합니다.

여러분이 이 코드를 사용하면 주가, 판매량, 온도, 트래픽 등 모든 시계열 데이터를 예측할 수 있습니다. 슬라이딩 윈도우로 충분한 학습 샘플을 생성하고, 정규화로 안정적인 학습을 보장하며, LSTM이 자동으로 복잡한 시간 패턴을 추출합니다.

실무에서는 여기에 교차 검증, 하이퍼파라미터 튜닝, 앙상블을 추가하여 정확도를 더 높입니다.

실전 팁

💡 시퀀스 길이는 데이터의 주기성에 맞추세요. 일별 데이터라면 7(주간), 30(월간)을 시도해보고, 시간별 데이터라면 24(일간), 168(주간)을 사용하세요.

💡 흔한 실수: Train/Test 분할을 랜덤으로 하는 것. 시계열은 반드시 시간 순서대로 분할해야 합니다. 처음 80%를 학습, 나머지 20%를 테스트로 사용하세요.

💡 다단계 예측(Multi-step)이 필요하면 출력 레이어를 Dense(n)으로 바꿔서 n개 시점을 동시에 예측하세요. 또는 재귀적으로 1개씩 예측하는 방법도 있습니다.

💡 LSTM이 과적합되면 Dropout을 추가하세요. LSTM(50, dropout=0.2, recurrent_dropout=0.2)로 설정하면 일반화 성능이 크게 향상됩니다.

💡 여러 특징을 사용하는 다변량 예측에서는 각 특징을 개별적으로 정규화하세요. 가격과 거래량은 스케일이 다르므로 함께 정규화하면 안 됩니다.


7. Attention 메커니즘과 LSTM의 결합

시작하며

여러분이 긴 논문을 요약할 때를 생각해보세요. 모든 문장이 똑같이 중요한 게 아니라 핵심 문장에 집중하죠.

LSTM도 마찬가지로 모든 시간 스텝을 동등하게 처리하는데, 실제로는 어떤 시점이 더 중요할 수 있습니다. 전통적인 LSTM은 긴 시퀀스를 처리할 때 초반 정보를 점점 잊어버립니다.

100개 단어 문장에서 첫 번째 단어의 정보가 100번째에 도달할 때는 많이 희석되어 있죠. 또한 어떤 부분이 중요한지 모델이 자동으로 판단하기 어렵습니다.

바로 이럴 때 필요한 것이 Attention(어텐션) 메커니즘입니다. LSTM의 모든 시간 스텝 출력에 가중치를 주어 중요한 부분에 집중하도록 만듭니다.

마치 형광펜으로 중요한 부분을 강조하는 것처럼요.

개요

간단히 말해서, Attention은 LSTM의 각 시간 스텝 출력에 중요도 점수를 매겨서 가중 평균을 만드는 메커니즘입니다. 중요한 정보는 크게, 덜 중요한 정보는 작게 반영합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 기계 번역에서 영어 "I love you"를 한국어로 번역할 때 "love"가 가장 중요한 단어입니다. Attention이 없으면 모든 단어를 똑같이 처리하지만, Attention이 있으면 "love"에 높은 가중치를 주어 "사랑해"로 정확히 번역합니다.

문서 분류에서도 제목이나 첫 문단에 높은 Attention을 주면 정확도가 크게 향상됩니다. 전통적인 LSTM은 마지막 hidden state만 사용했다면, Attention은 모든 hidden state를 사용하되 중요도에 따라 다르게 가중치를 줍니다.

이렇게 하면 긴 시퀀스에서도 중요한 정보를 놓치지 않습니다. Attention의 핵심 특징은 세 가지입니다.

첫째, 소프트맥스로 정규화된 0-1 사이의 Attention 가중치를 생성하고, 둘째, 모든 시간 스텝의 가중 평균(weighted sum)을 계산하여 컨텍스트 벡터를 만들며, 셋째, 어떤 부분에 집중했는지 시각화할 수 있어 모델 해석이 가능합니다. 이러한 특징들이 Attention을 현대 NLP의 핵심 기술로 만들어줍니다.

코드 예제

import tensorflow as tf
from tensorflow.keras.layers import Layer, LSTM, Dense
from tensorflow.keras.models import Model
import numpy as np

# Attention 레이어 구현
class AttentionLayer(Layer):
    def __init__(self):
        super(AttentionLayer, self).__init__()

    def build(self, input_shape):
        # Attention 가중치 학습용 Dense 레이어
        self.W = Dense(input_shape[-1])
        self.V = Dense(1)
        super(AttentionLayer, self).build(input_shape)

    def call(self, inputs):
        # inputs: LSTM의 모든 시간 스텝 출력 (batch, timesteps, features)

        # 1. Attention 점수 계산
        score = self.V(tf.nn.tanh(self.W(inputs)))  # (batch, timesteps, 1)

        # 2. 소프트맥스로 정규화 (합이 1이 되도록)
        attention_weights = tf.nn.softmax(score, axis=1)  # (batch, timesteps, 1)

        # 3. 가중 평균 계산
        context_vector = attention_weights * inputs  # (batch, timesteps, features)
        context_vector = tf.reduce_sum(context_vector, axis=1)  # (batch, features)

        return context_vector, attention_weights

# Attention을 사용하는 모델
inputs = tf.keras.Input(shape=(10, 1))  # 10 timesteps, 1 feature
lstm_out = LSTM(64, return_sequences=True)(inputs)  # 모든 timestep 출력

# Attention 적용
context_vector, attention_weights = AttentionLayer()(lstm_out)

# 최종 출력
output = Dense(1)(context_vector)

model = Model(inputs=inputs, outputs=output)
model.compile(optimizer='adam', loss='mse')

설명

이것이 하는 일: 위 코드는 LSTM 위에 Attention 레이어를 추가하여 중요한 시간 스텝에 자동으로 집중하는 모델을 만듭니다. 긴 시퀀스에서도 핵심 정보를 놓치지 않습니다.

첫 번째로, AttentionLayer가 커스텀 레이어로 구현됩니다. build 메소드에서 학습 가능한 가중치 행렬 W와 V를 생성합니다.

이 가중치들이 학습 과정에서 "어떤 시점이 중요한가"를 자동으로 배웁니다. 예를 들어 감정 분석에서는 "훌륭하다", "최악" 같은 감정 단어에 높은 가중치를 학습합니다.

두 번째로, call 메소드에서 Attention 점수를 계산합니다. self.W(inputs)로 각 시간 스텝을 변환하고, tanh로 활성화한 후, self.V로 스칼라 점수를 만듭니다.

이 점수가 각 시점의 중요도를 나타냅니다. 예를 들어 10개 시점 중 3번째가 가장 중요하다면 3번째의 점수가 높게 나옵니다.

세 번째로, tf.nn.softmax로 점수를 0-1 사이로 정규화하고 합이 1이 되도록 만듭니다. 이것이 Attention 가중치입니다.

예를 들어 [0.1, 0.05, 0.6, 0.1, 0.05, ...]처럼 나오면 3번째 시점에 60%의 주의를 기울인다는 뜻입니다. 소프트맥스 덕분에 "상대적 중요도"를 명확히 표현할 수 있습니다.

네 번째로, Attention 가중치와 LSTM 출력을 곱하여 가중 평균인 컨텍스트 벡터를 만듭니다. tf.reduce_sum(axis=1)로 시간 축을 따라 합치면 (batch, features) 형태가 됩니다.

이 벡터가 "전체 시퀀스의 중요한 정보만 압축한 표현"이 되고, Dense 레이어로 최종 예측을 만듭니다. 여러분이 이 코드를 사용하면 LSTM만 사용할 때보다 10-20% 높은 정확도를 얻을 수 있습니다.

긴 문장(50+ 단어)에서 초반 정보도 정확히 활용하고, attention_weights를 시각화하면 모델이 어디에 집중하는지 알 수 있으며, 번역, 요약, 질의응답 등 거의 모든 시퀀스 태스크에서 성능이 향상됩니다.

실전 팁

💡 Attention 가중치를 시각화하려면 히트맵으로 그려보세요. sns.heatmap(attention_weights)로 어떤 단어에 집중하는지 한눈에 볼 수 있습니다.

💡 흔한 실수: return_sequences=False로 설정하는 것. Attention은 모든 시간 스텝이 필요하므로 반드시 return_sequences=True여야 합니다.

💡 Self-Attention과 헷갈리지 마세요. 위 코드는 LSTM 출력에 대한 Attention이고, Self-Attention(Transformer)은 입력끼리 서로 Attention을 계산합니다.

💡 다중 헤드 Attention(Multi-head Attention)을 사용하면 여러 관점에서 중요도를 학습합니다. AttentionLayer를 여러 개 만들어 결합하세요.

💡 Attention이 한두 개 시점에만 집중한다면 과적합입니다. Attention Dropout이나 Temperature Scaling으로 분포를 넓혀보세요.


8. Seq2Seq 모델 - Encoder-Decoder 구조

시작하며

여러분이 한국어를 영어로 번역할 때를 생각해보세요. 먼저 한국어 문장 전체를 읽고 이해한 다음, 영어로 표현하죠.

입력(한국어)과 출력(영어)의 길이도 다르고 순서도 다릅니다. 이런 시퀀스-투-시퀀스(Sequence-to-Sequence) 문제는 어떻게 해결할까요?

일반적인 LSTM은 입력과 출력 길이가 같아야 합니다. 10개 단어 입력에 10개 값 출력처럼요.

하지만 번역, 요약, 대화 생성 같은 작업은 입력 "오늘 날씨 어때?"가 출력 "It's sunny and warm today"처럼 길이가 다릅니다. 기존 구조로는 불가능합니다.

바로 이럴 때 필요한 것이 Seq2Seq(Sequence-to-Sequence) 모델입니다. Encoder LSTM이 입력을 읽어서 압축하고, Decoder LSTM이 그 압축된 정보를 풀어서 출력을 생성합니다.

마치 우편물을 압축해서 보내고 받는 쪽에서 다시 푸는 것처럼요.

개요

간단히 말해서, Seq2Seq는 Encoder와 Decoder 두 개의 LSTM으로 구성됩니다. Encoder는 입력 시퀀스를 고정 길이 벡터로 압축하고, Decoder는 그 벡터에서 출력 시퀀스를 생성합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 구글 번역, 챗봇, 텍스트 요약, 이미지 캡셔닝 등 입출력 길이가 다른 모든 작업에서 필수입니다. 예를 들어, 고객 문의 "배송 언제 되나요?"를 "귀하의 주문은 내일 도착 예정입니다.

송장번호는..."처럼 훨씬 긴 답변으로 생성하는 챗봇을 만들 수 있습니다. 전통적인 단일 LSTM은 고정 길이 입출력만 가능했다면, Seq2Seq는 가변 길이를 자유롭게 처리합니다.

Encoder는 어떤 길이든 받아서 고정 크기로 압축하고, Decoder는 필요한 만큼 생성합니다. 끝을 알리는 특수 토큰(EOS)이 나올 때까지 계속 생성할 수 있습니다.

Seq2Seq의 핵심 특징은 세 가지입니다. 첫째, Encoder의 마지막 hidden state가 "전체 입력의 의미"를 담은 컨텍스트 벡터가 되고, 둘째, Decoder는 이 벡터를 초기 상태로 받아서 한 단어씩 순차적으로 생성하며, 셋째, Teacher Forcing으로 학습 속도를 높이고 Beam Search로 최적의 출력을 찾습니다.

이러한 특징들이 Seq2Seq를 자연어 생성의 기본 구조로 만들어줍니다.

코드 예제

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, LSTM, Dense
import numpy as np

# Seq2Seq 모델 구성
latent_dim = 256  # 압축된 표현의 차원

# ===== Encoder =====
encoder_inputs = Input(shape=(None, num_encoder_tokens))  # 가변 길이 입력
encoder_lstm = LSTM(latent_dim, return_state=True)
# encoder_outputs는 사용 안 함, state만 사용
encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs)
encoder_states = [state_h, state_c]  # 컨텍스트 벡터

# ===== Decoder =====
decoder_inputs = Input(shape=(None, num_decoder_tokens))
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)

# Encoder의 state를 초기 상태로 사용
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)

# 각 시점마다 단어 확률 분포 출력
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

# ===== 학습용 모델 =====
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

# ===== 추론용 Encoder 모델 =====
encoder_model = Model(encoder_inputs, encoder_states)

# ===== 추론용 Decoder 모델 =====
decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

decoder_outputs, state_h_dec, state_c_dec = decoder_lstm(
    decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h_dec, state_c_dec]
decoder_outputs = decoder_dense(decoder_outputs)

decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs] + decoder_states)

설명

이것이 하는 일: 위 코드는 번역이나 대화 생성에 사용되는 완전한 Seq2Seq 모델을 구축합니다. 학습용과 추론용 모델을 분리하여 효율적으로 구성합니다.

첫 번째로, Encoder 부분이 입력 시퀀스를 처리합니다. return_state=True로 설정하여 마지막 hidden state(state_h)와 cell state(state_c)를 추출합니다.

이 두 state가 합쳐져서 "입력 문장 전체의 의미"를 담은 컨텍스트 벡터가 됩니다. 예를 들어 "안녕하세요"라는 입력이 256차원 벡터로 압축되는 것이죠.

두 번째로, Decoder 부분이 컨텍스트 벡터에서 출력을 생성합니다. initial_state=encoder_states가 핵심인데, Encoder의 마지막 상태를 Decoder의 초기 상태로 사용합니다.

이렇게 하면 Decoder가 입력의 의미를 "기억"한 채로 출력을 만들기 시작합니다. return_sequences=True로 각 시점마다 출력을 생성합니다.

세 번째로, Dense 레이어가 각 시점에서 단어 확률 분포를 만듭니다. num_decoder_tokens는 출력 단어 사전 크기입니다.

예를 들어 10,000개 영어 단어가 있다면 각 시점마다 10,000차원 벡터를 출력하고, 소프트맥스로 확률로 변환합니다. 가장 높은 확률의 단어를 선택하여 번역 결과를 만듭니다.

네 번째로, 추론용 모델을 별도로 구성합니다. 학습 시에는 정답을 미리 알고 있지만(Teacher Forcing), 실제 번역할 때는 한 단어씩 생성하면서 이전 출력을 다음 입력으로 사용해야 합니다.

encoder_model로 입력을 압축하고, decoder_model로 한 단어씩 반복 생성합니다. 이 분리된 구조가 Seq2Seq의 표준 패턴입니다.

여러분이 이 코드를 사용하면 번역, 요약, 대화 등 다양한 텍스트 생성 작업을 수행할 수 있습니다. 입력 길이에 상관없이 고정 크기로 압축하여 효율적으로 처리하고, 출력 길이를 자유롭게 조절할 수 있으며, Teacher Forcing으로 학습 시간을 단축하고 안정적으로 수렴합니다.

실전 팁

💡 Teacher Forcing 비율을 점진적으로 줄이세요. 학습 초기엔 100% 정답 사용, 후반엔 50%로 낮춰서 실전과 비슷한 환경을 만드세요.

💡 흔한 실수: Encoder와 Decoder의 latent_dim을 다르게 설정하는 것. 반드시 같아야 state를 전달할 수 있습니다.

💡 긴 문장에서는 Encoder state 하나로 부족합니다. Attention을 추가하여 모든 Encoder 출력을 참조하도록 하세요.

💡 추론 시 Beam Search를 사용하면 탐욕적 선택(Greedy)보다 5-10% 나은 결과를 얻습니다. beam_width=5 정도가 적당합니다.

💡 시작 토큰(SOS)과 끝 토큰(EOS)을 반드시 추가하세요. Decoder가 언제 생성을 시작하고 끝낼지 알 수 있습니다.


9. 과적합 방지 - Dropout과 Regularization

시작하며

여러분이 시험 문제를 달달 외워서 100점을 받았지만, 조금만 바뀐 문제는 못 푸는 상황을 떠올려보세요. 모델도 마찬가지로 학습 데이터만 완벽히 외우고 새로운 데이터는 처리하지 못하는 '과적합(Overfitting)' 문제가 발생합니다.

LSTM과 GRU는 파라미터가 많아서 과적합에 특히 취약합니다. 예를 들어 1,000개 문장으로 학습한 감정 분석 모델이 학습 데이터에서는 99% 정확도를 보이지만, 실제 고객 리뷰에서는 70%만 맞추는 경우가 흔합니다.

모델이 데이터를 "이해"한 게 아니라 "암기"한 것이죠. 바로 이럴 때 필요한 것이 Dropout과 Regularization입니다.

이 기법들은 모델이 너무 완벽하게 학습하는 것을 의도적으로 방해하여 일반화 능력을 높입니다. 마치 시험 공부할 때 핵심 개념을 이해하도록 유도하는 것처럼요.

개요

간단히 말해서, Dropout은 학습 중 일부 뉴런을 랜덤하게 끄고, Regularization은 가중치에 페널티를 주어 과적합을 방지합니다. 둘 다 모델이 특정 패턴에 과도하게 의존하지 않도록 만듭니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 데이터는 노이즈가 많고 학습 데이터와 다릅니다. 예를 들어 뉴스 기사로 학습한 감정 분석 모델을 SNS에 적용하면 오타, 신조어, 이모티콘 때문에 성능이 급락합니다.

Dropout을 사용하면 이런 변화에 강건한(Robust) 모델을 만들 수 있습니다. 일반 LSTM은 모든 뉴런이 항상 작동했다면, Dropout을 적용하면 매 학습 스텝마다 랜덤하게 20-30%의 뉴런을 비활성화합니다.

이렇게 하면 특정 뉴런에 과도하게 의존하지 않고 여러 뉴런이 협력하여 패턴을 학습합니다. 마치 팀 프로젝트에서 한 명이 빠져도 나머지가 보완할 수 있게 만드는 것이죠.

과적합 방지의 핵심 특징은 세 가지입니다. 첫째, Dropout은 학습 시에만 적용되고 예측 시에는 모든 뉴런을 사용하며, 둘째, Recurrent Dropout은 시간 축을 따라 동일한 뉴런을 계속 끄므로 LSTM에 특화되어 있고, 셋째, L2 Regularization은 큰 가중치에 페널티를 주어 가중치를 작고 고르게 분산시킵니다.

이러한 특징들이 모델의 일반화 성능을 크게 향상시킵니다.

코드 예제

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.regularizers import l2

# 과적합 방지 기법을 적용한 LSTM 모델
model = Sequential()

# 1. Dropout: 입력의 20%를 랜덤하게 끄기
# Recurrent Dropout: 순환 연결의 20%를 랜덤하게 끄기
model.add(LSTM(
    128,
    dropout=0.2,           # 입력 Dropout
    recurrent_dropout=0.2, # 순환 Dropout (LSTM 특화)
    kernel_regularizer=l2(0.01),  # L2 정규화
    return_sequences=True,
    input_shape=(50, 100)  # 50 timesteps, 100 features
))

# 2. 추가 Dropout 레이어 (LSTM 출력에 적용)
model.add(Dropout(0.3))  # 30% 뉴런 끄기

# 3. 두 번째 LSTM도 동일하게 Dropout 적용
model.add(LSTM(
    64,
    dropout=0.2,
    recurrent_dropout=0.2,
    kernel_regularizer=l2(0.01)
))

model.add(Dropout(0.3))

# 4. 출력 레이어
model.add(Dense(10, activation='softmax'))

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

# Early Stopping으로 과적합 시점에 학습 중단
from tensorflow.keras.callbacks import EarlyStopping
early_stop = EarlyStopping(
    monitor='val_loss',  # 검증 손실 감시
    patience=5,          # 5번 개선 안 되면 중단
    restore_best_weights=True  # 최적 가중치 복원
)

# model.fit(..., callbacks=[early_stop])

설명

이것이 하는 일: 위 코드는 과적합을 방지하는 세 가지 기법(Dropout, Regularization, Early Stopping)을 모두 적용한 강건한 LSTM 모델을 만듭니다. 학습 데이터에 과도하게 맞추지 않고 새로운 데이터에도 잘 작동합니다.

첫 번째로, dropout=0.2는 입력에서 LSTM으로 가는 연결의 20%를 랜덤하게 끕니다. 예를 들어 100개 입력 특징이 있으면 매 학습 스텝마다 20개를 무작위로 0으로 만듭니다.

이렇게 하면 모델이 특정 특징에만 의존하지 않고 여러 특징을 골고루 사용하게 됩니다. 한 두 개 특징에 문제가 생겨도 나머지로 보완할 수 있죠.

두 번째로, recurrent_dropout=0.2는 LSTM 내부의 순환 연결(이전 hidden state에서 현재로)을 끕니다. 일반 Dropout과 달리 같은 시퀀스 내에서는 동일한 뉴런을 계속 끄므로 시간적 일관성을 유지합니다.

이것이 시계열 데이터에 특화된 Dropout입니다. 연구에 따르면 LSTM에서는 일반 Dropout보다 Recurrent Dropout이 더 효과적입니다.

세 번째로, kernel_regularizer=l2(0.01)은 가중치 제곱의 합에 0.01을 곱해서 손실에 추가합니다. 가중치가 커지면 손실도 커지므로 모델은 가중치를 작게 유지하려고 합니다.

작고 고른 가중치는 과적합을 방지합니다. 예를 들어 [-10, 0, 0, 12]보다 [-3, 2, -1, 3]처럼 분산된 가중치가 더 안정적입니다.

네 번째로, Dropout(0.3) 레이어를 LSTM 다음에 추가하여 LSTM 출력의 30%를 끕니다. LSTM 내부 Dropout과 출력 Dropout을 함께 사용하면 효과가 배가됩니다.

마지막으로 EarlyStopping은 검증 손실이 5번 연속 개선되지 않으면 학습을 멈추고 최적 가중치를 복원합니다. 불필요한 학습을 막아 시간을 절약하고 과적합을 방지합니다.

여러분이 이 코드를 사용하면 학습 정확도 95%, 테스트 정확도 92%처럼 일반화 갭을 크게 줄일 수 있습니다. 작은 데이터셋(수천 개)에서도 과적합 없이 학습하고, 노이즈나 변형에 강건한 모델을 만들며, Early Stopping으로 최적 지점을 자동으로 찾습니다.

실전 팁

💡 Dropout 비율은 0.2-0.5 범위에서 실험하세요. 0.5 이상은 너무 많은 정보를 버려서 학습이 안 될 수 있습니다.

💡 흔한 실수: Dropout을 예측 시에도 적용하는 것. Keras는 자동으로 처리하지만, 커스텀 구현 시 training=False 파라미터를 확인하세요.

💡 작은 데이터셋(< 10,000 샘플)에서는 Dropout을 높이고(0.4-0.5), 큰 데이터셋에서는 낮추세요(0.1-0.2).

💡 L1 정규화는 희소한 가중치(많은 0), L2는 작고 고른 가중치를 만듭니다. 대부분의 경우 L2가 LSTM에 더 적합합니다.

💡 배치 정규화(Batch Normalization)도 과적합을 방지하지만, LSTM에서는 주의가 필요합니다. Layer Normalization을 대신 사용하는 것이 안전합니다.


10. 하이퍼파라미터 튜닝과 모델 최적화

시작하며

여러분이 요리를 할 때 재료는 같아도 불의 세기, 조리 시간, 양념 비율에 따라 맛이 천차만별이죠. 딥러닝 모델도 마찬가지입니다.

LSTM 유닛 개수, 학습률, 배치 크기 같은 하이퍼파라미터 설정에 따라 성능이 크게 달라집니다. 초보자들은 기본값을 그대로 쓰거나 감으로 설정하는 경우가 많습니다.

하지만 LSTM 유닛을 50개 쓸지 200개 쓸지, 학습률을 0.001로 할지 0.01로 할지에 따라 정확도가 10-20% 차이 날 수 있습니다. 잘못된 하이퍼파라미터는 학습이 아예 안 되거나 과적합을 일으킵니다.

바로 이럴 때 필요한 것이 체계적인 하이퍼파라미터 튜닝입니다. 무작위로 시도하는 대신 그리드 서치, 랜덤 서치, 베이지안 최적화 같은 기법으로 최적 조합을 찾습니다.

마치 레시피를 과학적으로 개선하는 것처럼요.

개요

간단히 말해서, 하이퍼파라미터 튜닝은 모델 구조와 학습 설정을 체계적으로 변경하면서 최고 성능을 내는 조합을 찾는 과정입니다. 수동 시행착오보다 훨씬 효율적입니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 같은 데이터와 알고리즘으로도 하이퍼파라미터만 잘 조정하면 경쟁사보다 높은 성능을 낼 수 있습니다. 예를 들어 Kaggle 경진대회에서 상위권은 대부분 하이퍼파라미터 튜닝에 많은 시간을 투자합니다.

또한 프로덕션 환경에서는 추론 속도와 메모리 사용량도 중요한데, 이것도 하이퍼파라미터로 조절할 수 있습니다. 전통적으로는 수동으로 하나씩 바꿔가며 테스트했다면, 현대적 방법은 자동화 도구를 사용합니다.

그리드 서치는 모든 조합을 시도하고(느리지만 확실), 랜덤 서치는 무작위로 샘플링하며(빠르지만 불확실), 베이지안 최적화는 이전 결과를 학습하여 유망한 영역을 집중 탐색합니다(효율적). 하이퍼파라미터 튜닝의 핵심 특징은 세 가지입니다.

첫째, 중요한 파라미터(LSTM 유닛 수, 학습률)를 먼저 튜닝하고 덜 중요한 것(배치 크기)은 나중에 하며, 둘째, 교차 검증(Cross-Validation)으로 과적합 없이 평가하고, 셋째, Early Stopping과 함께 사용하여 시간을 절약합니다. 이러한 특징들이 효율적인 모델 최적화를 가능하게 합니다.

코드 예제

import keras_tuner as kt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout

# 하이퍼파라미터 탐색 공간 정의
def build_model(hp):
    model = Sequential()

    # LSTM 유닛 수: 32, 64, 128, 256 중 선택
    units = hp.Choice('units', values=[32, 64, 128, 256])

    # Dropout 비율: 0.0 ~ 0.5 범위에서 0.1 간격으로
    dropout_rate = hp.Float('dropout', min_value=0.0, max_value=0.5, step=0.1)

    # LSTM 레이어 개수: 1개 또는 2개
    num_layers = hp.Int('num_layers', min_value=1, max_value=2)

    for i in range(num_layers):
        model.add(LSTM(
            units,
            dropout=dropout_rate,
            recurrent_dropout=dropout_rate,
            return_sequences=(i < num_layers - 1),
            input_shape=(50, 100) if i == 0 else None
        ))
        model.add(Dropout(dropout_rate))

    model.add(Dense(10, activation='softmax'))

    # 학습률: 0.0001 ~ 0.01 범위에서 로그 스케일로
    learning_rate = hp.Float('learning_rate', min_value=1e-4, max_value=1e-2, sampling='log')

    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss='categorical_crossentropy',
        metrics=['accuracy']
    )

    return model

# 랜덤 서치 튜너 생성
tuner = kt.RandomSearch(
    build_model,
    objective='val_accuracy',  # 검증 정확도 최대화
    max_trials=20,             # 20개 조합 시도
    executions_per_trial=2,    # 각 조합을 2번 실행 (안정성)
    directory='tuning_results',
    project_name='lstm_optimization'
)

# 탐색 실행
tuner.search(X_train, y_train, epochs=30, validation_data=(X_val, y_val),
             callbacks=[tf.keras.callbacks.EarlyStopping(patience=3)])

# 최적 하이퍼파라미터 확인
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
print(f"최적 LSTM 유닛: {best_hps.get('units')}")
print(f"최적 Dropout: {best_hps.get('dropout')}")
print(f"최적 학습률: {best_hps.get('learning_rate')}")

# 최적 모델로 재학습
best_model = tuner.hypermodel.build(best_hps)

설명

이것이 하는 일: 위 코드는 Keras Tuner를 사용하여 LSTM 모델의 하이퍼파라미터를 자동으로 탐색하고 최적 조합을 찾습니다. 수동으로 수십 번 시도하는 대신 체계적으로 최선을 찾아냅니다.

첫 번째로, build_model 함수가 하이퍼파라미터 탐색 공간을 정의합니다. hp.Choice('units', [32, 64, 128, 256])는 "LSTM 유닛 개수를 32, 64, 128, 256 중에서 선택하라"는 뜻입니다.

hp.Float는 연속 범위에서, hp.Int는 정수 범위에서 선택합니다. 이렇게 정의하면 Tuner가 자동으로 조합을 생성합니다.

두 번째로, learning_ratesampling='log'를 사용합니다. 학습률은 0.001과 0.002의 차이보다 0.001과 0.01의 차이가 더 중요하므로 로그 스케일로 탐색하는 것이 효율적입니다.

선형 스케일로 하면 대부분 샘플이 큰 값에 몰려서 작은 학습률을 충분히 탐색하지 못합니다. 세 번째로, RandomSearch 튜너가 20개 조합을 무작위로 시도합니다.

그리드 서치로 모든 조합을 하면 4(units) × 6(dropout) × 2(layers) × 10(lr) = 480개나 되지만, 랜덤 서치는 20개만 해도 충분히 좋은 결과를 냅니다. 연구에 따르면 랜덤 서치가 그리드 서치보다 효율적인 경우가 많습니다.

네 번째로, executions_per_trial=2로 각 조합을 2번 실행합니다. 딥러닝은 초기 가중치에 따라 결과가 달라지므로 평균을 내야 신뢰할 수 있습니다.

Early Stopping으로 각 실행을 3번 연속 개선 없으면 중단하여 시간을 절약합니다. tuner.search()가 끝나면 최적 하이퍼파라미터를 get_best_hyperparameters()로 가져와서 최종 모델을 학습합니다.

여러분이 이 코드를 사용하면 수동 튜닝보다 5-15% 높은 정확도를 달성할 수 있습니다. 체계적 탐색으로 놓치는 조합 없이 최적을 찾고, Early Stopping으로 시간을 절약하며, 재현 가능한 결과를 얻어 팀원과 공유할 수 있습니다.

실전 팁

💡 중요도가 높은 파라미터부터 튜닝하세요. 보통 LSTM 유닛 수 > 학습률 > Dropout > 배치 크기 순입니다. 덜 중요한 것은 기본값으로 두세요.

💡 흔한 실수: 너무 넓은 범위를 탐색하는 것. 학습률을 1e-6부터 1까지 하면 대부분 무의미한 값만 시도합니다. 합리적 범위(1e-4 ~ 1e-2)로 좁히세요.

💡 베이지안 최적화(Bayesian Optimization)는 랜덤 서치보다 효율적입니다. kt.BayesianOptimization을 사용하면 10-20개 시도로도 좋은 결과를 얻습니다.

💡 GPU가 여러 개라면 병렬 튜닝을 사용하세요. max_concurrent_trials=4로 4개 조합을 동시에 학습하여 시간을 1/4로 줄입니다.

💡 최종 모델은 찾은 하이퍼파라미터로 전체 데이터(학습+검증)로 재학습하세요. 튜닝은 검증 세트로 하지만, 최종 배포는 모든 데이터를 활용해야 합니다.


#Python#LSTM#GRU#RNN#DeepLearning#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.