Recurrent Neural Network(RNN)은 입력과 출력을 시퀀스 단위로 처리하는 시퀀스 모델이다.
RNN(Recurrent Neural Network, 순환 신경망)은 문장, 시계열, 음성처럼 순서가 있는 데이터(시퀀스 데이터) 를 처리하기 위한 신경망 모델이다.
일반적인 피드포워드 신경망(Feed Forward Neural Network)은 입력이 출력 방향으로만 전달된다. 반면 RNN은 현재 입력뿐 아니라 이전 시점의 정보를 함께 활용한다는 점이 핵심이다.
예를 들어 문장을 처리할 때, 현재 단어의 의미를 파악하려면 앞에 나온 단어들의 맥락이 필요할 수 있다.
“나는 어제 영화를 …”
뒤에 어떤 단어가 올지 예측하려면 나는, 어제, 영화를이라는 이전 단어들의 흐름을 참고해야 한다. RNN은 이런 순차적 문맥을 은닉 상태에 저장하며 다음 시점으로 전달한다. wikidocs
RNN에서는 각 시점의 입력 (xt)와 이전 시점의 은닉 상태 (h{t-1})를 사용해 현재 은닉 상태 (h_t)를 계산한다.
[
ht = \tanh(W_xx_t + W_hh{t-1} + b)
]
각 기호의 의미는 다음과 같다.
| 기호 | 의미 |
|---|---|
| (x_t) | 현재 시점의 입력 벡터 |
| (h_{t-1}) | 이전 시점의 은닉 상태 |
| (h_t) | 현재 시점의 은닉 상태 |
| (W_x) | 입력에 대한 가중치 |
| (W_h) | 이전 은닉 상태에 대한 가중치 |
| (b) | 편향 |
| (\tanh) | 활성화 함수 |
RNN의 은닉 상태는 과거의 입력 정보를 어느 정도 기억하는 역할을 한다. 그래서 RNN의 셀(cell)은 흔히 메모리 셀(memory cell) 이라고도 불린다. 또한 모든 시점에서 같은 가중치 (W_x), (W_h), (W_y)를 공유한다는 특징이 있다. wikidocs
RNN은 입력 시퀀스와 출력 시퀀스의 길이를 다르게 설계할 수 있다.
| 형태 | 설명 | 활용 예시 |
|---|---|---|
| One-to-many | 하나의 입력으로 여러 개의 출력 생성 | 이미지 캡셔닝 |
| Many-to-one | 여러 입력을 하나의 결과로 분류 | 감성 분석, 스팸 메일 분류 |
| Many-to-many | 입력 시퀀스와 출력 시퀀스를 모두 처리 | 번역, 챗봇, 개체명 인식, 품사 태깅 |
예를 들어 감성 분석에서는 문장 전체를 입력받아 “긍정” 또는 “부정”이라는 하나의 결과를 출력한다. 반면 번역 모델은 원문 문장이라는 입력 시퀀스를 받아 번역문이라는 새로운 출력 시퀀스를 생성한다. wikidocs
RNN은 매 시점마다 입력값과 직전 은닉 상태를 결합해 새로운 은닉 상태를 만든다.
import numpy as np
timesteps = 10
input_size = 4
hidden_size = 8
inputs = np.random.random((timesteps, input_size))
hidden_state_t = np.zeros((hidden_size,))
Wx = np.random.random((hidden_size, input_size))
Wh = np.random.random((hidden_size, hidden_size))
b = np.random.random((hidden_size,))
total_hidden_states = []
for input_t in inputs:
output_t = np.tanh(
np.dot(Wx, input_t)
+ np.dot(Wh, hidden_state_t)
+ b
)
total_hidden_states.append(output_t)
hidden_state_t = output_t
total_hidden_states = np.stack(total_hidden_states, axis=0)
print(total_hidden_states.shape)
위 코드에서 중요한 부분은 다음 한 줄이다.
hidden_state_t = output_t
현재 시점에서 계산된 은닉 상태를 다음 시점의 입력으로 다시 전달한다. 이 구조 덕분에 RNN은 이전 시점의 정보를 현재 계산에 반영할 수 있다. wikidocs
nn.RNN()PyTorch에서는 nn.RNN()으로 RNN 레이어를 간단히 구현할 수 있다.
import torch
import torch.nn as nn
input_size = 5
hidden_size = 8
inputs = torch.Tensor(1, 10, 5)
cell = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True
)
outputs, hidden_state = cell(inputs)
print(outputs.shape)
print(hidden_state.shape)
입력 텐서의 기본 형태는 다음과 같다.
(batch_size, timesteps, input_size)
위 예시에서는 다음 의미를 가진다.
| 값 | 의미 |
|---|---|
1 | 배치 크기 |
10 | 시점 또는 시퀀스 길이 |
5 | 각 시점 입력 벡터의 차원 |
출력 결과의 형태는 다음과 같다.
outputs.shape
# torch.Size([1, 10, 8])
outputs는 모든 시점의 은닉 상태를 담는다.
hidden_state.shape
# torch.Size([1, 1, 8])
hidden_state는 마지막 시점의 은닉 상태를 의미한다. wikidocs
RNN도 여러 개의 은닉층을 쌓아 깊은 신경망 구조로 만들 수 있다. PyTorch에서는 num_layers 인자를 사용한다.
cell = nn.RNN(
input_size=5,
hidden_size=8,
num_layers=2,
batch_first=True
)
은닉층을 여러 개 쌓으면 더 복잡한 패턴을 학습할 가능성이 생긴다. 다만 층을 늘린다고 항상 성능이 좋아지는 것은 아니다. 모델이 복잡해질수록 더 많은 학습 데이터와 적절한 하이퍼파라미터 조정이 필요하다. wikidocs
일반 RNN은 과거 방향으로만 정보를 전달한다. 즉, 현재 시점의 예측에 이전 단어 또는 이전 시점의 데이터만 사용한다.
하지만 문장 처리에서는 뒤쪽 단어가 현재 단어를 해석하는 데 중요한 경우도 많다.
예를 들어 다음 문장을 보자.
Exercise is very effective at [ ] belly fat.
빈칸의 답을 고르려면 앞부분만 보는 것보다 뒤에 나오는 belly fat까지 함께 보는 것이 더 유리하다.
양방향 RNN(Bidirectional RNN)은 정방향과 역방향의 두 RNN을 함께 사용한다.
PyTorch에서는 bidirectional=True를 설정한다.
cell = nn.RNN(
input_size=5,
hidden_size=8,
num_layers=2,
batch_first=True,
bidirectional=True
)
outputs, hidden_state = cell(inputs)
print(outputs.shape)
print(hidden_state.shape)
양방향 RNN에서는 정방향과 역방향 은닉 상태가 결합되므로 출력 차원이 2배가 된다.
outputs.shape
# torch.Size([1, 10, 16])
은닉 상태 크기가 8이고 양방향이므로 최종 출력 차원은 (8 \times 2 = 16)이 된다. wikidocs
이번 주차에는 시퀀스 데이터를 처리하는 가장 기본적인 모델인 RNN을 학습했다.
nn.RNN()으로 RNN을 구현할 수 있다.num_layers로 깊은 RNN을 만들 수 있다.bidirectional=True를 사용하면 앞뒤 문맥을 함께 고려하는 양방향 RNN을 구현할 수 있다.다만 기본 RNN은 시퀀스가 길어질수록 오래된 정보를 잘 기억하지 못하는 장기 의존성 문제를 가질 수 있다. 이후에는 이를 보완하기 위해 등장한 LSTM과 GRU를 학습해 보면 좋을 것 같다.