[CS300 #271] 순환 신경망 — 기억을 가진 신경망과 그 한계
컴퓨터공학 300 주제 시리즈의 271번째 글이다. 전체 지도는 여기.
한 줄 요약
순환 신경망(RNN)은 시퀀스를 한 원소씩 읽으며 은닉 상태 h 를 같은 가중치로 갱신해 “지금까지 본 것”을 요약하고, 긴 시퀀스에서 기울기가 사라지거나 폭주하는 문제를 LSTM·GRU 같은 게이트 구조로 완화한다.
왜 필요한가
문장, 음성, 주가, 서버 메트릭은 모두 순서가 있는 데이터다. “나는 밥을 먹었다”와 “밥은 나를 먹었다”는 같은 단어로 이루어졌지만 뜻이 다르다. 그리고 길이가 제각각이다.
완전연결층은 입력 길이가 고정되어 있고 순서 개념이 없다. CNN 은 국소 패턴은 잘 보지만 먼 거리의 관계는 층을 많이 쌓아야 본다. RNN 은 “앞에서 읽은 것을 기억하며 다음을 읽는다”는 사람의 읽기 방식을 그대로 구조로 만들었다.
트랜스포머가 등장한 뒤 자연어 처리의 주력에서는 밀려났다. 그래도 RNN 을 알아야 하는 이유가 있다. 트랜스포머가 무엇을 해결하려고 나왔는지 이해하려면 RNN 의 한계를 먼저 알아야 하고, 상태를 고정 크기로 압축하는 RNN 의 아이디어는 최근의 상태 공간 모델 같은 구조로 다시 돌아오고 있다.
핵심 개념
기본 RNN
h_t = tanh( x_t · Wx + h_{t−1} · Wh + b )
y_t = h_t · Wy
시간 방향으로 펼치면 같은 층을 시간 길이만큼 복사해 이어 붙인 깊은 신경망이다.
x1 x2 x3 x4
│ │ │ │
▼ ▼ ▼ ▼
[RNN]──h1─▶[RNN]──h2─▶[RNN]──h3─▶[RNN]──h4─▶ 출력
(모든 칸이 같은 Wx, Wh, b 를 공유)
가중치 공유 덕분에 어떤 길이의 시퀀스도 같은 파라미터로 처리한다. CNN 이 공간에서 가중치를 공유했다면 RNN 은 시간에서 공유한다.
쓰임새의 형태
| 형태 | 예 |
|---|---|
| 다 대 일 | 문장 감성 분류 (마지막 h 만 사용) |
| 다 대 다 (같은 길이) | 품사 태깅 |
| 일 대 다 | 이미지 캡션 생성 |
| 인코더-디코더 | 번역: 인코더가 문장을 h 로 요약, 디코더가 풀어냄 |
시간 역전파와 기울기 소실
학습은 펼친 망에 역전파를 적용한다(BPTT, backpropagation through time). 시점 T 의 손실이 시점 0 의 은닉 상태에 미치는 영향은
∂h_T/∂h_0 = Π_{t=1..T} ∂h_t/∂h_{t−1} = Π diag(1 − h_t²) · Wh (행렬 곱의 연쇄)
같은 행렬 Wh 가 T 번 곱해진다. 활성화를 무시하면 Wh^T 이다. Wh 의 고유값 크기 중 가장 큰 것(스펙트럼 반경)이
- 1 보다 작으면 → 지수적으로 0 에 수렴 (기울기 소실). 먼 과거를 배울 수 없다.
- 1 보다 크면 → 지수적으로 커짐 (기울기 폭주). 학습이 발산한다.
게다가 tanh 의 미분은 최대 1 이고 대부분 1 보다 작으므로, 실제 RNN 에서는 소실 쪽으로 더 기운다. 폭주는 기울기의 노름을 상한으로 자르는 기울기 클리핑으로 비교적 쉽게 막지만, 소실은 구조를 바꿔야 한다.
LSTM 과 GRU
Hochreiter 와 Schmidhuber(1997)의 LSTM 은 은닉 상태와 별도로 셀 상태 c 를 두고, 세 게이트로 정보 흐름을 조절한다.
f_t = σ(...) 망각 게이트: 이전 기억을 얼마나 남길까
i_t = σ(...) 입력 게이트: 새 정보를 얼마나 쓸까
o_t = σ(...) 출력 게이트: 기억 중 얼마를 내보낼까
c_t = f_t ⊙ c_{t−1} + i_t ⊙ tanh(...) ← 곱셈이 아니라 "덧셈"으로 갱신
h_t = o_t ⊙ tanh(c_t)
핵심은 셀 상태가 덧셈으로 이어진다는 점이다. 망각 게이트가 1 에 가까우면 c 는 거의 그대로 다음 시점으로 넘어가고, 기울기도 행렬 곱 연쇄 없이 흐른다. ResNet 의 잔차 연결과 같은 원리다. GRU 는 게이트를 둘로 줄인 간소화 버전이다.
남은 한계
- 순차 계산: h_t 를 구하려면 h_{t−1} 이 있어야 한다. 시간 방향으로 병렬화가 안 되어 GPU 를 충분히 쓰지 못한다.
- 병목: 인코더-디코더에서 긴 문장 전체를 고정 크기 벡터 하나에 담아야 한다.
두 번째 문제를 풀려고 나온 것이 어텐션이고, 첫 번째 문제까지 풀려고 RNN 을 아예 버린 것이 트랜스포머다. 다음 글의 주제다.
직접 해 보기
넘파이로 기본 RNN 셀을 만들고, 스펙트럼 반경에 따라 Wh 를 거듭 곱했을 때의 크기가 어떻게 변하는지 본다.
import numpy as np
rng = np.random.default_rng(0)
H = 8
Wx, Wh, b = rng.normal(0, .5, (1, H)), rng.normal(0, .5, (H, H)), np.zeros(H)
def run(seq):
h = np.zeros(H)
for x in seq:
h = np.tanh(np.array([x]) @ Wx + h @ Wh + b) # h_t = tanh(x_t Wx + h_{t-1} Wh + b)
return h
print("같은 가중치로 길이 3, 10 시퀀스 처리:", run([1,0,1]).shape, run([1]*10).shape)
# 기울기 소실/폭주: 활성화를 빼고 보면 dh_T/dh_0 = Wh^T (T 제곱)
rho = np.abs(np.linalg.eigvals(Wh)).max()
for scale in (0.5, 1.0, 1.5):
W = Wh / rho * scale # 스펙트럼 반경을 scale 로 맞춤
norms = [np.linalg.norm(np.linalg.matrix_power(W, t)) for t in (5, 10, 20, 30)]
print(f"스펙트럼 반경 {scale}: 5/10/20/30 스텝 =", " ".join(f"{n:.1e}" for n in norms))
# tanh 의 미분은 최대 1 이라 실제 RNN 에서는 더 줄어든다
z = np.array([0.0, 1.0, 2.0, 3.0]); print("tanh'(0,1,2,3) =", (1 - np.tanh(z)**2).round(3))
실행 결과:
같은 가중치로 길이 3, 10 시퀀스 처리: (8,) (8,)
스펙트럼 반경 0.5: 5/10/20/30 스텝 = 7.8e-02 3.0e-03 3.1e-06 3.0e-09
스펙트럼 반경 1.0: 5/10/20/30 스텝 = 2.5e+00 3.0e+00 3.2e+00 3.2e+00
스펙트럼 반경 1.5: 5/10/20/30 스텝 = 1.9e+01 1.8e+02 1.1e+04 6.2e+05
tanh'(0,1,2,3) = [1. 0.42 0.071 0.01 ]
첫 줄은 길이가 3 이든 10 이든 같은 파라미터로 같은 크기의 상태를 낸다는 것을 보여 준다.
나머지가 핵심이다. 스펙트럼 반경 0.5 에서는 30 스텝 뒤 크기가 10⁻⁹ 수준이다. 30 단어 앞의 정보에 대한 학습 신호가 사실상 0 이다. 1.5 에서는 10⁵ 을 넘는다. 1.0 근처에서만 크기가 유지되지만, 학습 중에 가중치가 계속 변하므로 정확히 그 경계에 머물게 할 방법이 없다. 마지막 줄은 tanh 의 미분이 입력이 커질수록 빠르게 작아져 소실을 더 부추긴다는 것을 보여 준다.
현업에서는
- 시계열·스트리밍: 센서 데이터나 짧은 시계열 예측에서는 LSTM·GRU 가 지금도 가볍고 충분한 선택이다. 한 스텝씩 들어오는 데이터를 상태 하나로 이어 처리할 수 있어 메모리가 일정하다.
- 음성·온디바이스: 지연이 중요하고 연산이 제한된 기기에서 순환 구조가 쓰인다.
- 기울기 클리핑은 RNN 에서 시작됐지만 지금은 트랜스포머 학습에서도 기본 설정이다. 학습 로그에 기울기 노름을 같이 찍어 두면 폭주 징후를 일찍 본다.
- 역사적 맥락: 기계 번역은 RNN 인코더-디코더 → 어텐션 추가 → 트랜스포머로 이어졌다. 이 계보를 알면 논문 읽기가 쉬워진다.
확인 문제
- RNN 이 서로 다른 길이의 시퀀스를 같은 모델로 처리할 수 있는 이유는?
- 기본 RNN 에서 기울기 소실이 생기는 수학적 이유를 한 문장으로 설명하라.
- 기울기 폭주와 소실 중 클리핑으로 해결되는 것은?
- LSTM 이 소실을 완화하는 핵심 구조는?
- RNN 을 GPU 에서 시간 방향으로 병렬화하기 어려운 이유는?
풀이
- 모든 시점에서 같은 가중치를 공유하며 은닉 상태를 한 스텝씩 갱신하기 때문이다.
- 시간 역전파에서 같은 행렬 Wh(와 1 이하인 tanh 미분)가 시퀀스 길이만큼 곱해져, 스펙트럼 반경이 1 보다 작으면 지수적으로 줄어든다.
- 폭주. 기울기 노름이 상한을 넘으면 비율을 유지한 채 잘라 낸다.
- 셀 상태를 게이트로 조절된 덧셈으로 갱신하는 경로. 기울기가 행렬 곱 연쇄를 거치지 않고 흐른다.
- h_t 계산에 h_{t−1} 이 필요한 순차 의존성 때문이다.
더 읽을거리 (References)
- S. Hochreiter, J. Schmidhuber, “Long Short-Term Memory”, Neural Computation 9(8), 1997. PDF
- PyTorch 공식 문서, torch.nn.LSTM (게이트 수식 포함)
- I. Goodfellow, Y. Bengio, A. Courville, Deep Learning, MIT Press, 10장 “Sequence Modeling”. 온라인판