Level 6: 시퀀스 모델 (RNN/LSTM)
📝

Level 6

RNN의 구조와 동작 원리

순환 구조, 은닉 상태 계산, 파라미터 공유를 numpy로 직접 구현

50분
RNN의 구조와 동작 원리 강의 영상
강의 영상 보기 (새 탭에서 재생)YouTube

📓Google Colab에서 실습하기

이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.

학습 내용

RNN의 구조와 동작 원리

학습 목표

이 레슨을 완료하면:

  • RNN(Recurrent Neural Network)의 정의와 핵심 아이디어를 설명할 수 있다
  • 은닉 상태(Hidden State)가 어떻게 계산되는지 수식과 코드로 이해한다
  • 파라미터 공유가 왜 중요한지, 시간 펼치기가 무엇인지 안다
  • numpy로 RNN의 순전파를 직접 구현할 수 있다
  • 다양한 RNN 아키텍처(One-to-Many, Many-to-One 등)를 구분할 수 있다

핵심 메시지

"RNN은 같은 함수를 반복 적용하면서, 이전 결과를 다음 입력에 전달하는 구조이다." 이전 레슨에서 "메모장을 들고 책을 읽는 사람"에 비유했습니다. 이번에는 그 메모장이 실제로 어떤 수식으로 작동하는지를 파헤쳐 봅니다.


비유 다시 보기: 책을 읽는 사람

비유: 여러분이 소설을 한 문장씩 읽고 있습니다. 매 문장을 읽을 때마다 메모장에 "지금까지의 요약"을 적습니다.

  • 1문장 읽음 --> 메모장 v1 작성
  • 2문장 읽음 --> 메모장 v1 + 새 내용 --> 메모장 v2로 업데이트
  • 3문장 읽음 --> 메모장 v2 + 새 내용 --> 메모장 v3로 업데이트

이 "메모장"이 바로 Hidden State(은닉 상태, h) 이고, "업데이트 규칙"이 바로 RNN의 가중치(W) 입니다. 핵심은 매번 같은 규칙으로 메모장을 업데이트한다는 것입니다.


RNN의 수식: 은닉 상태 계산

RNN의 핵심 공식은 딱 한 줄입니다.

ht=tanh(Wxhxt+Whhht1+bh)h_t = \tanh(W_{xh} \cdot x_t + W_{hh} \cdot h_{t-1} + b_h)

여기서:

  • xtx_t = 현재 시점의 입력 (예: 단어 하나의 벡터)
  • ht1h_{t-1} = 이전 시점의 은닉 상태 (이전 메모장)
  • WxhW_{xh} = 입력을 은닉 상태로 변환하는 가중치
  • WhhW_{hh} = 이전 은닉 상태를 현재에 연결하는 가중치
  • bhb_h = 편향(bias)
  • tanh\tanh = 활성화 함수 (출력 범위: -1 ~ +1)
  • hth_t = 현재 시점의 은닉 상태 (새 메모장)

각 기호를 표로 정리하면:

기호이름역할차원 예시
x_t현재 입력이번에 읽은 데이터(input_size,)
h_(t-1)이전 은닉 상태지금까지의 요약(hidden_size,)
W_xh입력 가중치입력 --> 은닉 변환(hidden_size, input_size)
W_hh순환 가중치이전 은닉 --> 현재 은닉(hidden_size, hidden_size)
b_h편향기본값 조정(hidden_size,)
h_t현재 은닉 상태업데이트된 요약(hidden_size,)

실행해보기: numpy로 RNN 순전파 구현

아래 코드는 RNN의 순전파(forward pass)를 numpy로 직접 구현합니다. "입력 5개짜리 시퀀스"를 처리하면서 은닉 상태가 어떻게 변하는지 확인해 보세요.

python
import numpy as np np.random.seed(42) # --- 하이퍼파라미터 --- input_size = 3 # 입력 벡터 차원 hidden_size = 4 # 은닉 상태 차원 seq_len = 5 # 시퀀스 길이 # --- 가중치 초기화 (작은 랜덤 값) --- W_xh = np.random.randn(hidden_size, input_size) * 0.5 W_hh = np.random.randn(hidden_size, hidden_size) * 0.5 b_h = np.zeros(hidden_size) # --- 입력 시퀀스 (5개 시점, 각 3차원) --- X = np.random.randn(seq_len, input_size) # --- 초기 은닉 상태 (영벡터) --- h = np.zeros(hidden_size) print("=== RNN Forward Pass (step by step) ===") print(f"W_xh shape: {W_xh.shape} (hidden_size x input_size)") print(f"W_hh shape: {W_hh.shape} (hidden_size x hidden_size)") print() all_h = [h.copy()] for t in range(seq_len): # 핵심 공식: h_t = tanh(W_xh @ x_t + W_hh @ h_(t-1) + b_h) h = np.tanh(W_xh @ X[t] + W_hh @ h + b_h) all_h.append(h.copy()) print(f"t={t}: input={np.round(X[t], 2)} --> h={np.round(h, 3)}") print() print("Notice: h changes at every step, accumulating past information!") print(f"Initial h: {np.round(all_h[0], 3)}") print(f"Final h: {np.round(all_h[-1], 3)}")

이 코드에서 핵심을 다시 짚으면:

  • 같은 W_xh, W_hh 를 매 시점(t=0, 1, 2, ...)에서 반복 사용합니다.
  • 이전 h가 다음 단계의 입력으로 들어갑니다 -- 이것이 "순환(recurrence)"입니다.

파라미터 공유 (Parameter Sharing)

RNN의 가장 중요한 특징은 모든 시점에서 같은 가중치를 쓴다는 것입니다.

시점 0: h_0 = tanh(W_xh * x_0 + W_hh * h_init + b)  --+
시점 1: h_1 = tanh(W_xh * x_1 + W_hh * h_0 + b)      |  같은 W_xh, W_hh!
시점 2: h_2 = tanh(W_xh * x_2 + W_hh * h_1 + b)      |
시점 3: h_3 = tanh(W_xh * x_3 + W_hh * h_2 + b)  --+
장점설명
가변 길이 처리시퀀스가 5개든 500개든 같은 모델로 처리
적은 파라미터시점마다 별도 가중치가 필요 없어 효율적
위치 불변 학습"상승 패턴"이 시퀀스 어디에 있든 동일하게 인식

시간 펼치기 (Time Unfolding)

RNN의 순환 구조를 이해하기 쉽도록 시간축으로 "펼치는" 방법입니다.

[펼치기 전 - 순환 표현]

x_t ──▶ [RNN Cell] ──▶ h_t ▲ │ │ ▼ h_(t-1)

[펼친 후 - 시간축 표현]

x_0 ──▶ [RNN] ──▶ h_0 │ ▼ x_1 ──▶ [RNN] ──▶ h_1 │ ▼ x_2 ──▶ [RNN] ──▶ h_2 │ ▼ x_3 ──▶ [RNN] ──▶ h_3

💡 모든 [RNN] 블록은 같은 가중치를 공유합니다!

펼친 모습을 보면 RNN이 사실상 깊은 신경망처럼 보입니다. 이것이 나중에 배울 "기울기 소실" 문제와 연결됩니다.


실행해보기: 은닉 상태 변화 시각화

python
import numpy as np import matplotlib.pyplot as plt np.random.seed(42) # sin 파형을 입력으로 사용 seq_len = 30 input_size = 1 hidden_size = 5 X = np.sin(np.linspace(0, 3 * np.pi, seq_len)).reshape(-1, 1) W_xh = np.random.randn(hidden_size, input_size) * 0.5 W_hh = np.random.randn(hidden_size, hidden_size) * 0.3 b_h = np.zeros(hidden_size) h = np.zeros(hidden_size) hidden_states = [] for t in range(seq_len): h = np.tanh(W_xh @ X[t] + W_hh @ h + b_h) hidden_states.append(h.copy()) hidden_states = np.array(hidden_states) fig, axes = plt.subplots(2, 1, figsize=(12, 6)) axes[0].plot(X, color="steelblue", linewidth=2) axes[0].set_title("Input signal (sin wave)") axes[0].set_ylabel("x_t") for i in range(hidden_size): axes[1].plot(hidden_states[:, i], label=f"h[{i}]", linewidth=1.5) axes[1].set_title("Hidden state neurons over time") axes[1].set_ylabel("h_t value") axes[1].set_xlabel("Time step") axes[1].legend(loc="upper right", fontsize=8) plt.tight_layout() plt.savefig("rnn_hidden_states.png", dpi=100) plt.show() print("Each hidden neuron captures a different aspect of the input history!")

위 그래프에서 각 은닉 뉴런(h[0], h[1], ...)이 입력 신호에 대해 서로 다른 패턴으로 반응하는 것을 볼 수 있습니다.


출력 계산과 다양한 아키텍처

은닉 상태에서 **출력(y_t)**을 만드는 것도 간단합니다.

yt=Whyht+byy_t = W_{hy} \cdot h_t + b_y

용도에 따라:

  • 분류: y에 softmax 적용
  • 회귀: y를 그대로 사용
  • 시퀀스 생성: 매 시점 y 출력

RNN은 입력과 출력의 관계에 따라 여러 구조로 활용됩니다.

구조입력출력활용 예시
Many-to-One시퀀스하나감성 분류, 스팸 탐지
One-to-Many하나시퀀스이미지 캡션 생성
Many-to-Many시퀀스시퀀스기계 번역, 품사 태깅
양방향(Bidirectional)시퀀스시퀀스문맥 이해 강화

PyTorch로 보는 RNN (참고)

실제 프로젝트에서는 PyTorch의 nn.RNN을 사용합니다.

import torch
import torch.nn as nn

rnn = nn.RNN(
    input_size=10,
    hidden_size=20,
    num_layers=1,
    batch_first=True
)

x = torch.randn(3, 5, 10)      # (배치=3, 시퀀스=5, 입력=10)
h0 = torch.zeros(1, 3, 20)     # 초기 은닉

output, hn = rnn(x, h0)
print(f"output shape: {output.shape}")   # (3, 5, 20)
print(f"hn shape:     {hn.shape}")       # (1, 3, 20)

실행해보기: RNN으로 간단한 순전파 시뮬레이션

python
import numpy as np import matplotlib.pyplot as plt np.random.seed(0) t = np.linspace(0, 4 * np.pi, 100) data = np.sin(t) input_size = 1 hidden_size = 10 W_xh = np.random.randn(hidden_size, input_size) * 0.3 W_hh = np.random.randn(hidden_size, hidden_size) * 0.1 W_hy = np.random.randn(1, hidden_size) * 0.3 b_h = np.zeros(hidden_size) b_y = np.zeros(1) h = np.zeros(hidden_size) predictions = [] for i in range(len(data)): x = np.array([data[i]]) h = np.tanh(W_xh @ x + W_hh @ h + b_h) y = W_hy @ h + b_y predictions.append(y[0]) predictions = np.array(predictions) plt.figure(figsize=(12, 4)) plt.plot(data, label="Input (sin)", color="steelblue") plt.plot(predictions, label="RNN output (untrained)", color="coral", linestyle="--") plt.title("RNN forward pass (random weights, no training)") plt.xlabel("Time step") plt.legend() plt.tight_layout() plt.savefig("rnn_forward.png", dpi=100) plt.show() print("With random weights, the output is meaningless.") print("After training (BPTT), it would learn to predict patterns!")

학습되지 않은 랜덤 가중치이므로 출력은 의미 없는 값입니다. 하지만 구조적으로 RNN이 어떻게 시퀀스를 한 시점씩 처리하는지 볼 수 있습니다.


핵심 요약

개념설명비유
RNN시퀀스 처리용 순환 신경망메모장을 업데이트하며 책 읽기
은닉 상태 h_t과거 정보의 요약 벡터메모장의 현재 내용
W_xh입력 --> 은닉 변환새 정보를 메모에 적는 방법
W_hh이전 은닉 --> 현재 은닉이전 메모를 참고하는 방법
파라미터 공유모든 시점에서 동일한 가중치항상 같은 규칙으로 메모 업데이트
시간 펼치기순환을 시간축으로 시각화책의 각 페이지를 옆으로 펼침
tanh활성화 함수 (-1~+1)메모 값이 너무 커지지 않게 조절

학습 체크리스트

  • RNN의 핵심 공식을 설명할 수 있다
  • "파라미터 공유"가 무엇이고 왜 유용한지 말할 수 있다
  • 은닉 상태가 시점마다 어떻게 업데이트되는지 그림으로 그릴 수 있다
  • Many-to-One, One-to-Many, Many-to-Many의 차이를 설명할 수 있다
  • numpy로 RNN 순전파를 직접 코딩할 수 있다

다음 레슨 예고

다음 시간에는 RNN의 치명적 약점인 기울기 소실(Vanishing Gradient) 문제를 알아보고, 이를 해결하기 위해 등장한 LSTM의 게이트 메커니즘을 깊이 있게 배웁니다.

레슨 정보

레벨
Level 6: 시퀀스 모델 (RNN/LSTM)
예상 소요 시간
50분
참고 영상
YouTube 링크

💡실습 환경 안내

이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.

Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.