RNN(Recurrent Neural Network)은 시퀀스 모델로 입력과 출력을 시퀀스 단위로 처리하는 모델.
입력층에서 출력층으로 움직이는 모델들과 다르게 RNN은 은닉층 노드에서 나온 값이 출력층 뿐만 아니라 다른 은닉층 노드의 입력값으로 가기도 한다.
t 시점에서 셀이 가진 값이 t+1 시점의 자신에게 보내지는 것을 은닉 상태 라고 합니다.

RNN은 입력과 출력의 길이를 다르게 설계할 수 있고 다양한 용도로 사용할 수 있다.
일 대 다, 다 대 일, 다 대 다 와 같은 형태가 나올 수 있으며 각 형태는 특정 목표를 더 잘 수행합니다.
e.g) 다 대 일 엥서는 여러 입력에 대해서 이진 분류로 나타날 때 (스팸 분류) 주로 사용됩니다.
1) RNN에 대한 수식

현재 시점 t에서의 은닉 상태값을 라고 정의.
은닉층의 메모리 셀은 를 계산하기 위해 총 두 개의 가중치를 갖게 됩니다. 하나는 입력층에서의 가중치 , 다른 하나는 이전 시점 t-1의 은닉 상태값인 을 위한 가중치 입니다.
은닉층 :
출력층 :
여깃서 는 비선형 활성화 함수 중 하나.
배치 크기가 1이고, 를 4로 가정할 때, 은닉층 연산은 다음과 같다.

2) 파이썬으로 구현
이라고 가정.
# 아래의 코드는 의사 코드(pseudocode)로 실제 동작하는 코드가 아님.
hidden_state_t = 0 # 초기 은닉 상태를 0(벡터)로 초기화
for input_t in input_length: # 각 시점마다 입력을 받는다.
output_t = tanh(input_t, hidden_state_t) # 각 시점에 대해서 입력과 은닉 상태를 가지고 연산
hidden_state_t = output_t # 계산 결과는 현재 시점의 은닉 상태가 된다.
실제 작동 코드
아래에서는 이해를 위해 2D 텐서를 입력 받지만 실제로 파이톹치에서는 3D(batch_size, timestpes, input_size)를 입력
import numpy as np
timesteps = 10 # 시점의 수. NLP에서는 보통 문장의 길이가 된다.
input_size = 4 # 입력의 차원. NLP에서는 보통 단어 벡터의 차원이 된다.
hidden_size = 8 # 은닉 상태의 크기. 메모리 셀의 용량이다.
inputs = np.random.random((timesteps, input_size)) # 입력에 해당되는 2D 텐서
hidden_state_t = np.zeros((hidden_size,)) # 초기 은닉 상태는 0(벡터)로 초기화
# 은닉 상태의 크기 hidden_size로 은닉 상태를 만듬.
print(hidden_state_t) # 8의 크기를 가지는 은닉 상태. 현재는 초기 은닉 상태로 모든 차원이 0의 값을 가짐.
[0. 0. 0. 0. 0. 0. 0. 0.]
가중치와 편향
Wx = np.random.random((hidden_size, input_size)) # (8, 4)크기의 2D 텐서 생성. 입력에 대한 가중치.
Wh = np.random.random((hidden_size, hidden_size)) # (8, 8)크기의 2D 텐서 생성. 은닉 상태에 대한 가중치.
b = np.random.random((hidden_size,)) # (8,)크기의 1D 텐서 생성. 이 값은 편향(bias).
Wx는 (은닉 상태의 크기 × 입력의 차원)
Wh는 (은닉 상태의 크기 × 은닉 상태의 크기)
b는 (은닉 상태의 크기)의 크기를 가진다.
이제 모든 시점의 은닉 상태를 출력한다고 가정하고, RNN 층을 동작
total_hidden_states = []
# 메모리 셀 동작
for input_t in inputs: # 각 시점에 따라서 입력값이 입력됨.
output_t = np.tanh(np.dot(Wx,input_t) + np.dot(Wh,hidden_state_t) + b) # Wx * Xt + Wh * Ht-1 + b(bias)
total_hidden_states.append(list(output_t)) # 각 시점의 은닉 상태의 값을 계속해서 축적
print(np.shape(total_hidden_states)) # 각 시점 t별 메모리 셀의 출력의 크기는 (timestep, output_dim)
hidden_state_t = output_t
total_hidden_states = np.stack(total_hidden_states, axis = 0)
# 출력 시 값을 깔끔하게 해준다.
print(total_hidden_states) # (timesteps, output_dim)의 크기. 이 경우 (10, 8)의 크기를 가지는 메모리 셀의 2D 텐서를 출력.
(1, 8)
(2, 8)
(3, 8)
(4, 8)
(5, 8)
(6, 8)
(7, 8)
(8, 8)
(9, 8)
(10, 8)
[[0.85575076 0.71627213 0.87703694 0.83938496 0.81045543 0.86482715 0.76387233 0.60007514]
[0.99982366 0.99985897 0.99928638 0.99989791 0.99998252 0.99977656 0.99997677 0.9998397 ]
[0.99997583 0.99996057 0.99972541 0.99997993 0.99998684 0.99954936 0.99997638 0.99993143]
[0.99997782 0.99996494 0.99966651 0.99997989 0.99999115 0.99980087 0.99999107 0.9999622 ]
[0.99997231 0.99996091 0.99976218 0.99998483 0.9999955 0.99989239 0.99999339 0.99997324]
[0.99997082 0.99998754 0.99962158 0.99996278 0.99999331 0.99978731 0.99998831 0.99993414]
[0.99997427 0.99998367 0.99978331 0.99998173 0.99999579 0.99983689 0.99999058 0.99995531]
[0.99992591 0.99996115 0.99941212 0.99991593 0.999986 0.99966571 0.99995842 0.99987795]
[0.99997139 0.99997192 0.99960794 0.99996751 0.99998795 0.9996674 0.99998177 0.99993016]
[0.99997659 0.99998915 0.99985392 0.99998726 0.99999773 0.99988295 0.99999316 0.99996326]]
3) 파이토치의 nn.RNN()
파이토치에서는 nn.RNN()으로 RNN 셀을 구현
import torch
import torch.nn as nn
입력, 은닉 상태의 크기를 정의 (RNN의 대표적 하이퍼파라미터)
input_size = 5 # 입력의 크기
hidden_size = 8 # 은닉 상태의 크기
입력 텐서는 (배치 크기 × 시점의 수 × 매 시점마다 들어가는 입력)의 크기
여기서는 배치 크기는 1, 10번의 시점동안 5차원의 입력 벡터가 들어가도록 텐서를 정의
# (batch_size, time_steps, input_size)
inputs = torch.Tensor(1, 10, 5)
nn.RNN()으로 RNN 셀 생성
인자로 입력의 크기, 은닉 상태의 크기를 정의해주고, batch_first=True를 통해서 입력 텐서의 첫번째 차원이 배치 크기임을 알려준다.
cell = nn.RNN(input_size, hidden_size, batch_first=True)
입력 텐서를 RNN 셀에 입력해 출력 확인
outputs, _status = cell(inputs)
RNN 셀은 두 개의 입력을 리턴하는데,
첫번째 리턴값은 모든 시점(timesteps)의 은닉 상태들이며, 두번째 리턴값은 마지막 시점(timestep)의 은닉 상태입니다.
우선 첫번째 리턴값에 대해서 크기를 확인
print(outputs.shape) # 모든 time-step의 hidden_state
torch.Size([1, 10, 8])
첫번째 리턴값의 은닉 상태들은 (1, 10, 8)의 크기를 가집니다. 이는 10번의 시점동안 8차원의 은닉상태가 출력되었다는 의미입니다. 두번째 리턴값. 다시 말해 마지막 시점의 은닉 상태의 크기를 확인
print(_status.shape) # 최종 time-step의 hidden_state
torch.Size([1, 1, 8])
마지막 시점의 은닉 상태는 (1, 1, 8)의 크기
4) 깊은 순환 신경망(Deep Recurrent Neural Network)

두 개의 은닉층을 가진 깊은 순환 신경망의 모습
이를 파이토치로 구현할 때는 nn.RNN()의 인자인 num_layers에 값을 전달해 층을 쌓는다
# (batch_size, time_steps, input_size)
inputs = torch.Tensor(1, 10, 5)
cell = nn.RNN(input_size = 5, hidden_size = 8, num_layers = 2, batch_first=True)
print(outputs.shape) # 모든 time-step의 hidden_state
torch.Size([1, 10, 8])
첫 리턴값의 크기는 층이 1개일 때와 동일.
print(_status.shape) # (층의 개수, 배치 크기, 은닉 상태의 크기)
torch.Size([2, 1, 8])
두번째 리턴값의 크기는 첫 숫자가 달라진다.
(층의 개수, 배치 크기, 은닉 상태의 크기)
5) 양방향 순환 신경망(Bidirectional Recurrent Neural Network)
t 시점에서의 출력값 예측에서 이전 시점의 데이터 뿐만 아니라 이후 데이터로도 예측할 수 있다는 생각에서 시작된 신경망

양방향 RNN은 하나의 출력값을 예측하기 위해 기본적으로 두 개의 메모리 셀을 사용
그리고 이 두 개의 값 모두가 출력층에서 출력값을 예측하기 위해 사용됩니다.
다수의 은닉층 역시 가능
파이토치 에서는 nn.RNN()의 인자인 bidirectional에 True를 전달하면 된다.
# (batch_size, time_steps, input_size)
inputs = torch.Tensor(1, 10, 5)
cell = nn.RNN(input_size = 5, hidden_size = 8, num_layers = 2, batch_first=True, bidirectional = True)
outputs, _status = cell(inputs)
print(outputs.shape) # (배치 크기, 시퀀스 길이, 은닉 상태의 크기 x 2)
torch.Size([1, 10, 16])
첫번째 리턴값의 크기는 단뱡 RNN 셀 때보다 은닉 상태의 크기의 값이 두 배
(배치 크기, 시퀀스 길이, 은닉 상태의 크기 x 2)의 크기
양방향의 은닉 상태 값들이 연결(concatenate)되었기 때문
print(_status.shape) # (층의 개수 x 2, 배치 크기, 은닉 상태의 크기)
torch.Size([4, 1, 8])
RNN을 바닐라RNN이라고도 하는데, 이는 아이스크림 기본맛인 바닐랑에서 따온 표현(ㅋㅋ)
이후 RNN의 한계를 극복하기 위해 나온 여러 변형 중 하나인 LSTM
1) 바닐라 RNN의 한계
출력 결과가 이전의 계산 결과에 의존하는 RNN.
짧은 경우엔 효과적이지만 시점이 길어지면서 앞의 정보가 뒤로 충분히 전달되지 못하는 현상이 발생.
중요한 정보가 앞에 위치할 경우 모델 자체에 문제가 생긴다.
이를 장기 의존성 문제(The problem of Long-Term dependencies)
2) 바닐라 RNN 내부

편향을 생략한 그림.
, 두 개의 입력이 각각의 가중치와 곱해져 메모리 셀의 입력이 된다.
이를 하이퍼볼릭탄젠트 함수의 입력으로 사용ㅇ하고 이 값은 은닉층의 출력인 은닉 상태가 된다.
3) LSTM

이는 LSTM의 내부
LSTM은 은닉층의 메모리 셀에 입력 게이트, 망각 게이트, 출력 게이트를 추가해 불필요한 기억을 지우고, 기억해야할 것을 정한다
은닉 상태를 계산하는 식이 조금 더 복잡해지며, 셀 상태라는 값을 추가.
t 시점의 셀 상태를 로 표현

셀 상태는 위 그림에서 왼쪽에서 오른쪽으로 가는 굵은 선.
은닉 상태처럼 이전 시점의 셀 상태가 다음 시점의 셀 상태를 구하기 위한 입력으로 사용.
은닉 상태값과 셀 상태값을 구하기 위해 새로 추가된 3개의 게이트를 사용(삭제 게이트, 입력 게이트, 출력 게이트 - 공통적으로 시그모이드 함수)
(1) 입력 게이트

현재 정보를 기억하기 위한 게이트.
현재 시점의 의 값과 입력 게이트로 이어지는 가중치 를 곱한 값과 이전 시점 t-1의 은닉 상태가 입력 게이트로 이어지는 가중치 를 곱한 값을 더해 시그모이드 함수를 지나며 이를 라 한다.
그리고 의 값과 입력게이트로 이어지는 가중치 를 곱한 값을 더해 하이퍼볼릭탄젠트 함수를 지나며 이를 라고 한다.
시그모이드를 지나 0~1의 값, 하이퍼볼릭탄젠트 함수를 지나 -1~1의 값 두 개가 나오며 두 값을 가지고 기억할 정보의 양을 정한다.
(2) 삭제 게이트

현재 시점 t의 값과 이전 시점 t-1의 은닉 상태가 시그모이드 함수를 지남. 0~1의 값이 삭제 과정을 거친 정보의 양, 0에 가까울수록 정보가 많이 삭제된 것이고 1에 가까울수록 정보를 온전히 기억.
(3) 셀 상태(장기 상태)

를 장기 상태라고 부르기도 한다.
셀 상태를 구하는 법
(4) 출력 게이트와 은닉 상태(단기 상태)

출력 게이트는 현 시점 t의 값과 이전 시점 t-1의 은닉 상태가 시그모이드 함수를 지난 값. 해당 값은 현 시점 t의 은닉 상태를 결정하는 일에 쓰이게 됩니다.
은닉 상태는 단기 상태로 불리기도 하며, 장기 상태의 값이 하이퍼볼릭탄젠트 함수를 지나 -1~1의 값을 가진다.
4) 파이토치에서 LSTM 사용
기존 RNN 셀 사용하려 할 때
nn.RNN(input_dim, hidden_size, batch_fisrt=True)
LSTM은
nn.LSTM(input_dim, hidden_size, batch_fisrt=True)