이미지는 공간을 보고, 문장은 순서를 본다
앞 장에서 우리는 CNN이 이미지를 다루는 방식을 살펴보았다. CNN은 가까운 픽셀들이 함께 만드는 작은 패턴을 읽는다. 선, 모서리, 질감 같은 지역적 특징을 먼저 찾고, 층을 거치며 점점 더 큰 형태를 이해한다. 이미지에서는 공간적 위치가 중요하다. 눈 옆에 코가 있고, 코 아래에 입이 있는 관계가 얼굴을 이해하는 데 큰 힌트가 된다.
그런데 모든 데이터가 이미지처럼 생긴 것은 아니다. 어떤 데이터는 왼쪽과 오른쪽보다 먼저와 나중이 더 중요하다. 문장, 음성, 주가, 센서 기록, 음악, 클릭 로그가 그렇다. 이런 데이터를 시퀀스 데이터라고 부른다. 시퀀스란 순서가 있는 데이터다.
예를 들어 다음 두 문장을 보자.
개가 사람을 물었다.
사람이 개를 물었다.
두 문장에는 비슷한 단어가 들어 있다. 하지만 순서가 달라지는 순간 의미가 완전히 바뀐다. 단어를 봉투에 넣고 흔들어 섞어 버리면, 우리는 누가 누구를 물었는지 알 수 없다. 문장은 단어의 목록이 아니라 단어가 순서대로 놓인 흐름이다.
시간에 따라 기록되는 데이터도 마찬가지다. 오늘의 주가만 보면 별 의미가 없을 수 있다. 하지만 어제, 그제, 지난주와 비교하면 상승세인지 하락세인지 보인다. 심장 박동 센서도 한 순간의 숫자 하나보다 연속된 변화가 더 중요하다. 음악 역시 음표 하나하나가 아니라, 앞선 음과 다음 음의 관계 속에서 멜로디가 된다.
딥러닝 모델이 이런 데이터를 잘 다루려면 한 가지 능력이 필요하다. 바로 이전 정보를 기억하면서 다음 입력을 처리하는 능력이다.

순서 데이터에서는 문맥이 생긴다
시퀀스 데이터의 핵심은 문맥이다. 문맥은 지금 보고 있는 값이 앞뒤 흐름 속에서 어떤 의미를 갖는지를 말한다.
배라는 단어를 생각해 보자. 이 단어는 과일일 수도 있고, 사람의 신체 부위일 수도 있고, 물 위를 다니는 배일 수도 있다. 단어 하나만 보면 뜻을 확정하기 어렵다.
나는 점심을 많이 먹어서 배가 불렀다.
이 문장에서는 신체 부위의 배다.
항구에 큰 배가 들어왔다.
이 문장에서는 선박이다.
모델이 배를 제대로 이해하려면 주변 단어를 함께 봐야 한다. 더 정확히 말하면, 앞에서 어떤 단어들이 나왔는지를 기억해야 한다. 이것이 시퀀스 모델이 필요한 이유다.
MLP는 입력을 한 번에 받아 출력으로 보낸다. CNN은 주변 패턴을 잘 잡아낸다. 하지만 기본적인 MLP나 CNN만으로는 입력이 시간 순서대로 들어오고, 앞의 정보가 뒤의 판단에 영향을 주는 구조를 직접 표현하기 어렵다. 물론 CNN도 문장에 적용할 수 있고, 실제로 좋은 성능을 내는 경우가 있다. 하지만 순서가 길어지고 문맥이 멀리 떨어질수록, 이전 상태를 계속 전달하는 모델이 자연스럽게 떠오른다.
그 역할을 맡는 대표적인 구조가 RNN, 즉 순환 신경망이다.
RNN은 같은 셀을 여러 번 사용하는 신경망이다
RNN은 Recurrent Neural Network의 약자다. recurrent는 반복된다는 뜻이다. 이름 그대로 RNN은 같은 신경망 셀을 시간 방향으로 반복해서 사용한다.
문장을 단어 단위로 읽는다고 생각해 보자.
오늘 / 날씨가 / 정말 / 좋다
일반적인 신경망이라면 이 네 단어를 한꺼번에 입력으로 넣거나, 정해진 길이의 숫자 벡터로 바꿔 넣어야 한다. 반면 RNN은 단어를 하나씩 읽는다. 먼저 오늘을 읽고, 그 결과를 내부 상태에 저장한다. 다음으로 날씨가를 읽을 때는 방금 저장한 상태도 함께 사용한다. 그다음 정말을 읽을 때는 앞의 두 단어를 거치며 만들어진 상태를 사용한다. 마지막 좋다를 읽을 때는 앞선 흐름 전체가 압축된 상태를 참고한다.
이때 RNN이 들고 다니는 내부 상태를 은닉 상태라고 부른다. 영어로는 hidden state다. 이름은 조금 어려워 보이지만, 역할은 단순하다. 은닉 상태는 모델의 짧은 메모장이다. 지금까지 읽은 내용을 요약해서 다음 시점으로 넘겨주는 숫자 묶음이다.
RNN의 한 시점 계산은 보통 다음과 같은 형태로 표현한다.
h_t = tanh(x_t @ W_xh + h_prev @ W_hh + b_h)
y_t = h_t @ W_hy + b_y여기서 x_t는 현재 시점의 입력이다. 문장이라면 현재 단어에 해당하는 벡터일 수 있다. h_prev는 이전 시점의 은닉 상태다. h_t는 현재 시점에서 새로 만든 은닉 상태다. 그리고 y_t는 현재 시점의 출력이다.
중요한 부분은 h_t가 현재 입력 x_t만으로 만들어지지 않는다는 점이다. RNN은 항상 현재 입력과 이전 은닉 상태를 함께 본다.
현재 입력 + 이전 기억 → 새로운 기억이 한 줄이 RNN의 핵심이다.
은닉 상태는 정보를 다음 시점으로 넘긴다
RNN을 작은 이야기꾼이라고 상상해 보자. 이야기꾼은 문장을 한 단어씩 듣는다. 첫 단어를 들었을 때는 아직 아는 것이 별로 없다. 두 번째 단어를 들으면 첫 단어와 연결해 본다. 세 번째 단어를 들으면 앞의 흐름을 더 잘 이해한다. 마지막 단어를 들었을 때는 전체 문장이 긍정적인지, 부정적인지, 어떤 주제인지 판단할 수 있다.
이때 이야기꾼의 머릿속에 남아 있는 요약이 은닉 상태다. 은닉 상태는 원문을 그대로 저장하는 저장소가 아니다. 모든 단어를 낱낱이 보관하는 녹음기가 아니라, 지금까지의 흐름을 다음 판단에 유용한 방식으로 압축한 벡터다.
예를 들어 감성 분류 모델이 다음 문장을 읽는다고 해 보자.
처음에는 지루했지만 마지막 장면은 정말 감동적이었다.
모델이 처음에는, 지루했지만까지만 읽으면 부정적인 느낌이 강할 수 있다. 하지만 뒤에서 정말 감동적이었다가 나오면 전체 감정은 긍정 쪽으로 바뀔 수 있다. RNN은 단어를 읽을 때마다 은닉 상태를 갱신하면서 이런 흐름의 변화를 반영한다.
은닉 상태의 크기는 우리가 정한다. 예를 들어 은닉 상태 크기를 32로 정하면, RNN은 매 시점마다 길이 32짜리 숫자 벡터를 다음 시점으로 넘긴다. 크기를 128로 정하면 더 많은 정보를 담을 수 있지만, 모델이 커지고 학습도 어려워질 수 있다. 이전 장들에서 본 것처럼 딥러닝에는 항상 표현력과 학습 난이도 사이의 균형이 있다.
RNN을 펼쳐 보면 이해가 쉬워진다
RNN 그림을 보면 셀 하나가 자기 자신으로 화살표를 보내는 모습으로 표현되는 경우가 많다. 처음 보면 조금 이상하다. 신경망이 자기 자신에게 연결되어 있다니, 계산이 끝나지 않고 무한히 도는 것처럼 보일 수 있다.
하지만 실제로는 그렇게 무서운 일이 아니다. RNN은 시간 방향으로 펼쳐서 보면 이해하기 쉽다.
x_1 → [RNN 셀] → h_1
x_2 → [RNN 셀] → h_2
x_3 → [RNN 셀] → h_3
x_4 → [RNN 셀] → h_4각 시점의 RNN 셀은 같은 파라미터를 공유한다. 즉 x_1을 처리할 때 쓰는 가중치와 x_2를 처리할 때 쓰는 가중치가 같다. 문장이 길어져도 같은 셀을 반복해서 적용한다.
이것이 RNN의 강점이다. 입력 길이가 달라져도 같은 규칙으로 처리할 수 있다. 세 단어짜리 문장도, 열 단어짜리 문장도, 백 단어짜리 문장도 시간 순서대로 하나씩 읽으면 된다.
물론 현실에서는 너무 긴 시퀀스를 무작정 끝까지 처리하기 어렵다. 계산량도 늘어나고, 뒤에서 볼 기울기 문제도 생긴다. 그래도 기본 아이디어는 분명하다. RNN은 순서가 있는 데이터를 한 단계씩 읽으며 은닉 상태를 갱신하는 모델이다.
PyTorch에서 RNN의 입력과 출력 모양
PyTorch에는 기본 RNN 모듈이 준비되어 있다. 직접 수식을 구현하지 않아도 nn.RNN을 사용하면 순환 구조를 만들 수 있다.
import torch
import torch.nn as nn
rnn = nn.RNN(
input_size=10,
hidden_size=32,
batch_first=True
)
x = torch.randn(4, 5, 10)
output, h_n = rnn(x)
print(output.shape)
print(h_n.shape)여기서 x의 모양은 [4, 5, 10]이다. batch_first=True로 설정했기 때문에 첫 번째 차원은 배치 크기다. 즉 한 번에 4개의 시퀀스를 처리한다. 두 번째 차원인 5는 시퀀스 길이다. 각 시퀀스는 5개의 시점으로 이루어져 있다. 세 번째 차원인 10은 각 시점의 입력 벡터 크기다.
hidden_size=32는 은닉 상태의 크기다. RNN은 각 시점마다 길이 32짜리 은닉 상태를 만든다.
output은 모든 시점의 은닉 상태를 담고 있다. 따라서 모양은 [배치 크기, 시퀀스 길이, 은닉 상태 크기]가 된다. 위 코드에서는 [4, 5, 32]다.
h_n은 마지막 시점의 은닉 상태다. RNN 층이 하나라면 보통 [1, 배치 크기, 은닉 상태 크기] 모양이 된다. 위 코드에서는 [1, 4, 32]다.
처음에는 이 모양이 복잡해 보일 수 있다. 하지만 의미를 하나씩 붙이면 단순하다.
입력 x: 여러 개의 시퀀스, 각 시퀀스의 여러 시점, 각 시점의 벡터
output: 모든 시점에서 나온 은닉 상태
h_n: 마지막까지 읽고 남은 최종 은닉 상태이제 이 출력들을 어떤 문제에 사용할지 생각해 보자. 문제의 종류에 따라 마지막 은닉 상태만 쓰기도 하고, 모든 시점의 출력을 쓰기도 한다.
many-to-one: 여러 입력을 읽고 하나를 예측한다
첫 번째 구조는 many-to-one이다. 여러 시점의 입력을 읽은 뒤, 마지막에 하나의 출력을 만든다.
가장 대표적인 예는 문장 분류다. 영화 리뷰 문장을 읽고 긍정인지 부정인지 판단하는 문제를 생각해 보자.
이 영화는 초반은 느리지만 결말이 훌륭했다.
모델은 단어를 하나씩 읽는다. 마지막 단어까지 읽은 뒤, 최종 은닉 상태에는 문장 전체의 요약이 들어 있다고 기대한다. 이 최종 은닉 상태를 선형 층에 넣으면 긍정/부정 같은 분류 결과를 만들 수 있다.
class RNNClassifier(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True
)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
output, h_n = self.rnn(x)
last_hidden = h_n[-1]
logits = self.fc(last_hidden)
return logits이 구조에서는 output 전체를 사용하지 않고 h_n에서 마지막 은닉 상태를 꺼내 쓴다. 문장 전체를 하나의 벡터로 요약한 뒤, 그 벡터로 분류하는 셈이다.
many-to-one 구조는 다음과 같은 문제에 잘 어울린다.
문장을 읽고 긍정/부정으로 분류하기
센서 기록을 보고 고장 여부 판단하기
사용자의 행동 로그를 보고 이탈 가능성 예측하기
일정 기간의 주가 흐름을 보고 다음 날 상승/하락 분류하기
핵심은 입력은 여러 개지만 최종 판단은 하나라는 점이다. 여러 장면을 본 뒤 영화의 장르를 하나로 말하는 것과 비슷하다.
many-to-many: 여러 입력을 읽고 여러 출력을 만든다
두 번째 구조는 many-to-many다. 여러 시점의 입력을 받고, 각 시점마다 출력을 만든다.
예를 들어 문장에서 각 단어의 품사를 예측한다고 해 보자.
나는 / 학교에 / 갔다
대명사 / 명사 / 동사이 경우에는 문장 전체에 대해 하나의 답을 내는 것이 아니라, 각 단어마다 하나씩 답을 내야 한다. 입력도 여러 개고 출력도 여러 개다.
시계열 예측에서도 many-to-many 구조가 자주 등장한다. 예를 들어 매 시간의 온도, 습도, 전력 사용량을 입력으로 받아 다음 시간대의 전력 사용량을 계속 예측할 수 있다. 음성 인식, 자막 생성, 번역 같은 문제도 넓게 보면 입력과 출력이 모두 시퀀스인 문제다.
PyTorch에서는 RNN의 output을 사용하면 각 시점의 은닉 상태를 얻을 수 있다. 여기에 선형 층을 적용하면 각 시점의 예측을 만들 수 있다.
class RNNSequenceModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True
)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
output, h_n = self.rnn(x)
logits = self.fc(output)
return logits여기서 output의 모양은 [배치 크기, 시퀀스 길이, 은닉 상태 크기]다. self.fc는 마지막 차원인 은닉 상태 크기를 출력 크기로 바꾼다. 따라서 최종 logits의 모양은 [배치 크기, 시퀀스 길이, 출력 크기]가 된다. 즉 각 시점마다 예측값이 생긴다.
many-to-one과 many-to-many의 차이는 마지막 은닉 상태만 볼 것인지, 모든 시점의 은닉 상태를 볼 것인지에 있다.
| 구조 | 입력 | 출력 | 예시 |
|---|---|---|---|
| many-to-one | 여러 시점 | 하나의 예측 | 문장 감성 분류, 고장 여부 판단 |
| many-to-many | 여러 시점 | 여러 예측 | 품사 태깅, 시계열 예측, 프레임별 분류 |
RNN도 결국 학습한다
RNN의 겉모습은 조금 특별하지만, 학습 원리는 지금까지 배운 신경망과 같다.
모델이 예측한다. 틀린 정도를 손실로 계산한다. 어느 가중치가 오차에 얼마나 영향을 주었는지 본다. 가중치를 조금씩 수정한다. 이 과정을 반복한다.
다만 RNN에서는 같은 셀이 시간 방향으로 반복된다. 그래서 역전파도 시간 방향으로 펼쳐진 구조를 따라 흘러간다. 이를 시간을 통한 역전파라고 부른다. 영어로는 Backpropagation Through Time, 줄여서 BPTT라고 한다.
앞에서 역전파를 배울 때 우리는 손실에서 출발해 뒤쪽 층부터 앞쪽 층으로 기울기를 전달한다고 했다. RNN에서도 같은 일이 일어난다. 마지막 시점의 손실은 마지막 은닉 상태에 영향을 주고, 마지막 은닉 상태는 그 전 은닉 상태와 연결되어 있다. 그래서 기울기는 현재 시점에서 이전 시점으로, 다시 그 이전 시점으로 거슬러 올라간다.
손실 → h_5 → h_4 → h_3 → h_2 → h_1이 흐름이 가능하기 때문에 RNN은 앞쪽 입력이 뒤쪽 예측에 어떤 영향을 주었는지 학습할 수 있다. 예를 들어 문장 끝에서 부정적인 결론이 나왔다면, 모델은 앞에서 등장한 단어나 표현이 그 판단에 얼마나 기여했는지 기울기를 통해 조정한다.
하지만 바로 이 지점에서 RNN의 중요한 약점도 나타난다.
긴 문장에서는 기억이 희미해진다
사람도 긴 이야기를 들으면 앞부분을 잊기 쉽다. RNN도 비슷하다. 짧은 문장에서는 은닉 상태가 앞의 정보를 비교적 잘 전달할 수 있다. 하지만 문장이 길어질수록 처음에 나온 정보가 마지막까지 살아남기 어렵다.
예를 들어 다음 문장을 보자.
내가 어릴 때부터 가장 좋아했고, 비 오는 날마다 꺼내 읽었으며, 친구들에게도 여러 번 추천했던 책은 정말 감동적이었다.
마지막의 감동적이었다를 제대로 이해하려면 앞에서 말한 대상이 책이라는 사실을 기억해야 한다. 그런데 이 정보는 중간에 여러 표현을 거치며 멀리 떨어져 있다. RNN은 정보를 한 시점씩 전달하기 때문에, 먼 과거의 정보가 마지막까지 온전히 전달되기 어렵다.
이 문제를 장기 의존성 문제라고 부른다. 장기 의존성이란 멀리 떨어진 요소들 사이의 관계를 말한다. 문장의 처음에 나온 주어가 문장의 끝 동사와 연결되거나, 앞 문단에서 언급한 인물이 뒤 문단에서 다시 등장하는 경우가 그렇다.
RNN은 구조상 이런 관계를 배우기 힘들 수 있다. 왜냐하면 정보가 매 시점마다 은닉 상태라는 작은 통로를 지나가야 하기 때문이다. 처음 정보는 h_1에 들어가고, 그다음 h_2, h_3, h_4를 거쳐 계속 전달된다. 중간에서 조금씩 희석되면 마지막에는 거의 남지 않을 수 있다.
기울기 소실은 왜 생길까
장기 의존성 문제는 학습 과정의 기울기와도 깊이 연결되어 있다. RNN에서 오래전 입력이 현재 출력에 영향을 주려면, 역전파 때 기울기가 먼 과거 시점까지 잘 전달되어야 한다.
그런데 RNN은 같은 변환을 여러 번 반복한다. 역전파에서는 여러 단계의 미분값이 계속 곱해진다. 여기서 작은 값들이 반복해서 곱해지면 어떻게 될까?
0.5 × 0.5 × 0.5 × 0.5 × 0.5 = 0.03125처음에는 0.5가 그리 작아 보이지 않는다. 하지만 여러 번 곱하면 금방 0에 가까워진다. 기울기도 마찬가지다. 이전 시점으로 거슬러 올라갈수록 기울기가 너무 작아지면, 앞쪽 입력과 관련된 가중치는 거의 수정되지 않는다. 모델 입장에서는 문장 앞부분이 결과에 어떤 영향을 줬는지 배울 신호가 희미해지는 셈이다.
이것이 기울기 소실이다. 기울기가 사라진다는 뜻이다. 정확히 0이 되는 것은 아니더라도, 너무 작아져서 학습에 거의 도움이 되지 않는 상태가 된다.
반대로 큰 값들이 반복해서 곱해지면 기울기가 너무 커질 수도 있다.
2 × 2 × 2 × 2 × 2 = 32이 경우에는 기울기 폭주가 생길 수 있다. 기울기가 지나치게 커지면 가중치가 한 번에 크게 흔들리고, 손실이 불안정해진다. 실제 학습에서는 기울기 폭주를 막기 위해 gradient clipping이라는 기법을 쓰기도 한다. 기울기가 일정 크기 이상 커지면 잘라내어 업데이트가 너무 과격해지지 않게 하는 방법이다.
하지만 기울기 소실은 단순히 잘라내서 해결하기 어렵다. 신호가 너무 작아져 사라지는 문제이기 때문이다. 그래서 RNN 이후에는 LSTM과 GRU처럼 더 오래 기억하기 위한 구조가 등장했다. 이들은 중요한 정보를 더 잘 보존하고, 필요 없는 정보는 잊도록 설계된 RNN의 변형이다.
RNN의 좋은 점과 아쉬운 점
RNN은 순서 데이터를 다루는 데 매우 중요한 발상이다. 입력을 하나씩 읽고, 은닉 상태를 통해 이전 정보를 다음 시점으로 넘기는 구조는 자연스럽고 강력하다. 특히 문장, 음성, 시계열처럼 시간적 흐름이 있는 데이터에 잘 어울린다.
RNN이 우리에게 준 핵심 아이디어는 분명하다.
데이터에는 순서가 있고, 순서는 의미를 바꾼다.
모델은 이전 정보를 은닉 상태에 담아 다음 입력을 해석할 수 있다.
시퀀스 전체를 하나로 분류할 수도 있고, 각 시점마다 예측할 수도 있다.
긴 시퀀스에서는 정보와 기울기가 멀리 전달되기 어렵다.
마지막 항목이 특히 중요하다. RNN은 한 시점에서 다음 시점으로 정보를 건네는 릴레이 경주와 같다. 짧은 거리에서는 잘 작동한다. 하지만 거리가 길어질수록 바통을 떨어뜨리거나, 처음의 메시지가 희미해질 가능성이 커진다.
이 약점은 나중에 Attention과 Transformer를 이해하는 데 결정적인 힌트가 된다. Attention은 모든 정보를 한 줄로만 전달하지 않는다. 필요한 위치를 직접 바라보게 만든다. 문장의 마지막 단어가 첫 단어와 관련이 있다면, 중간 단계를 모두 거쳐 희미한 기억을 받는 대신 첫 단어를 직접 참고할 수 있게 한다.
이것이 RNN에서 Attention으로 넘어가는 큰 이유다. RNN은 순서 데이터를 이해하기 위한 중요한 다리이고, Attention은 그 다리 위에서 발견한 병목을 넓히려는 시도다.
작은 정리
RNN은 순서가 있는 데이터를 처리하기 위한 신경망이다. 현재 입력과 이전 은닉 상태를 함께 사용해 새로운 은닉 상태를 만든다. 은닉 상태는 지금까지 읽은 정보를 압축해 다음 시점으로 넘기는 역할을 한다.
문장 분류처럼 여러 입력을 읽고 하나의 출력을 만드는 문제에는 many-to-one 구조를 사용한다. 품사 태깅이나 시계열 예측처럼 각 시점마다 출력을 만들어야 하는 문제에는 many-to-many 구조를 사용한다.
RNN도 손실 함수와 역전파로 학습한다. 다만 시간 방향으로 펼쳐진 구조를 따라 기울기가 전달되기 때문에, 시퀀스가 길어지면 기울기 소실과 장기 의존성 문제가 생기기 쉽다. 먼 과거의 정보가 마지막 판단까지 잘 전달되지 않는 것이다.
이제 우리는 문장을 단어의 묶음이 아니라 시간에 따라 흐르는 데이터로 볼 수 있게 되었다. 다음 장에서는 이 감각을 바탕으로 실제 텍스트 분류 모델을 만들어 본다. 단어를 숫자로 바꾸고, 문장을 모델에 넣고, RNN이 문장의 흐름을 읽어 하나의 판단을 내리게 할 것이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.