
📓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의 핵심 공식은 딱 한 줄입니다.
여기서:
- • = 현재 시점의 입력 (예: 단어 하나의 벡터)
- • = 이전 시점의 은닉 상태 (이전 메모장)
- • = 입력을 은닉 상태로 변환하는 가중치
- • = 이전 은닉 상태를 현재에 연결하는 가중치
- • = 편향(bias)
- • = 활성화 함수 (출력 범위: -1 ~ +1)
- • = 현재 시점의 은닉 상태 (새 메모장)
각 기호를 표로 정리하면:
| 기호 | 이름 | 역할 | 차원 예시 |
|---|---|---|---|
| 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개짜리 시퀀스"를 처리하면서 은닉 상태가 어떻게 변하는지 확인해 보세요.
pythonimport 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의 순환 구조를 이해하기 쉽도록 시간축으로 "펼치는" 방법입니다.
[펼치기 전 - 순환 표현]
[펼친 후 - 시간축 표현]
💡 모든 [RNN] 블록은 같은 가중치를 공유합니다!
펼친 모습을 보면 RNN이 사실상 깊은 신경망처럼 보입니다. 이것이 나중에 배울 "기울기 소실" 문제와 연결됩니다.
실행해보기: 은닉 상태 변화 시각화
pythonimport 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)**을 만드는 것도 간단합니다.
용도에 따라:
- •분류: 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으로 간단한 순전파 시뮬레이션
pythonimport 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 등을 실행할 수 있습니다.