PyTorch nn.RNNCell과 nn.RNN¶
PyTorch는 Simple RNN을 구현하기 위한 두 모듈을 제공한다.
-
nn.RNNCell- 한 시점의 순환 연산을 수행한다.
- 전체 시퀀스를 처리하려면 반복문과 hidden state 전달을 직접 작성해야 한다.
-
nn.RNN- 전체 시퀀스의 순환 연산을 수행한다.
- 시점 반복과 hidden state 전달을 모듈 내부에서 처리한다.
주의할 점은 PyTorch의 두 module이 반환하는 값은 hidden state 임.
- 별도의 prediction output layer는 포함하지 않음.
- 때문에 분류나 회귀를 위한 예측값이 필요하면 다음과 같은 출력 층을 추가할 것:
nn.RNNCell¶
nn.RNNCell은
- 한 시점의 입력과 이전 hidden state를 받아
- 새로운 hidden state를 계산.
생성자¶
nn.RNNCell(
input_size, # 각 시점의 입력 feature 수
hidden_size, # hidden state의 feature 수
bias=True, # bias 사용 여부
nonlinearity="tanh", # 활성화 함수: "tanh" 또는 "relu"
device=None, # parameter를 생성할 device
dtype=None, # parameter의 data type
)
nonlinearity에 custom function 등의 할당은 안 됨:tanh또는relu에서만 골라야 한다.
한 시점 처리¶
import torch
from torch import nn
batch_size = 4
input_size = 3
hidden_size = 5
rnn_cell = nn.RNNCell(
input_size=input_size,
hidden_size=hidden_size,
)
X_t = torch.randn(batch_size, input_size)
H_prev = torch.zeros(batch_size, hidden_size)
H_t = rnn_cell(X_t, H_prev)
Tensor shape은 다음과 같다.
X_t: (batch_size, input_size) = (4, 3)
H_prev: (batch_size, hidden_size) = (4, 5)
H_t: (batch_size, hidden_size) = (4, 5)
반환값: H_t
- 이 값은 새로운 hidden state 임.
- 분류 점수나 회귀값과 같은 prediction output이 아님에 유의할 것.
전체 시퀀스 처리¶
nn.RNNCell은 한 번 호출할 때 한 시점만 처리함.
전체 시퀀스는 반복문으로 처리할 것.
batch_size = 4
sequence_length = 10
input_size = 3
hidden_size = 5
X = torch.randn(batch_size, sequence_length, input_size)
H = torch.zeros(batch_size, hidden_size)
hidden_states = []
for X_t in X.unbind(dim=1):
H = rnn_cell(X_t, H)
hidden_states.append(H)
H_all = torch.stack(hidden_states, dim=1)
H_all: 모든 timestep(시점)의 hidden state가 저장됨.H: 마지막 timestep(시점)의 hidden state가 저장됨.
각각의 dimension은 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_all: (batch_size, sequence_length, hidden_size)
H: (batch_size, hidden_size)
Prediction output 추가¶
nn.RNNCell
- hidden state를 prediction으로 바꾸는 출력 층이 없음.
- 때문에 예측값이 필요하면
nn.Linear층을 별도로 추가해야 함.
다음의 코드에서 RNNCellMode은
- RNNCell과
- prediction output weight를
- 하나의 모듈로 구현함:
import torch
from torch import nn
class RNNCellModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
# input과 이전 hidden state로 새 hidden state를 계산
self.rnn_cell = nn.RNNCell(
input_size=input_size,
hidden_size=hidden_size,
)
# hidden state를 prediction으로 변환하는 output weights
self.output_layer = nn.Linear(
in_features=hidden_size,
out_features=output_size,
)
def forward(self, X, H_0=None):
batch_size = X.size(0)
if H_0 is None:
H = X.new_zeros(batch_size, self.hidden_size)
else:
H = H_0
hidden_states = []
predictions = []
for X_t in X.unbind(dim=1):
H = self.rnn_cell(X_t, H)
Y_t = self.output_layer(H)
hidden_states.append(H)
predictions.append(Y_t)
H_all = torch.stack(hidden_states, dim=1)
Y_all = torch.stack(predictions, dim=1)
return Y_all, H_all, H
사용 방법:
model = RNNCellModel(
input_size=3,
hidden_size=5,
output_size=2,
)
X = torch.randn(4, 10, 3)
Y_all, H_all, H_last = model(X)
각각의 Tensor shape 는 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_all: (batch_size, sequence_length, hidden_size)
H_last: (batch_size, hidden_size)
Y_all: (batch_size, sequence_length, output_size)
Prediction output의 학습 가능한 parameter 는 다음과 같음:
각 시점의 prediction 계산:
- 점선으로 표시한 부분은 모듈 외부에 별도로 추가한 부분임.
nn.RNN¶
nn.RNN
- 전체 시퀀스를 한 번에 처리.
- 시점 반복과 hidden state 전달은 모듈 내부에서 수행함.
생성자¶
nn.RNN(
input_size, # 각 시점의 입력 feature 수
hidden_size, # hidden state의 feature 수
num_layers=1, # 쌓을 recurrent layer 수
nonlinearity="tanh", # 활성화 함수: "tanh" 또는 "relu"
bias=True, # bias 사용 여부
batch_first=False, # batch 차원을 첫 번째로 둘지 여부
dropout=0.0, # 마지막 층을 제외한 층 사이의 dropout 확률
bidirectional=False, # 양방향 RNN 사용 여부
device=None, # parameter를 생성할 device
dtype=None, # parameter의 data type
)
전체 시퀀스 처리¶
다음 예제에선 batch 차원을 첫 번째로 배치하는 것을 전제로 함:
- PyTorch의 경우
batch_first가False가 기본값이나 - 많은 경우
True를 사용하는 경우도 많기 때문에 이를 전제로 예제 작성함:
import torch
from torch import nn
batch_size = 4
sequence_length = 10
input_size = 3
hidden_size = 5
rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True,
)
X = torch.randn(batch_size, sequence_length, input_size)
H_0 = torch.zeros(1, batch_size, hidden_size)
output, H_n = rnn(X, H_0)
각 tensor의 dimension은 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_0: (num_layers, batch_size, hidden_size)
output: (batch_size, sequence_length, hidden_size)
H_n: (num_layers, batch_size, hidden_size)
위 shape은 다음 설정을 기준으로 함:
반환값은 다음과 같음:
output:- 마지막 recurrent layer가 각 시점에서 만든 hidden state를 모은 tensor이다.
- 여기서 변수 이름의 output은 prediction output을 뜻하지 않는다.
H_n: 각 recurrent layer와 각 방향의 마지막 hidden state를 모은 tensor이다.
nn.RNN 도 별도의 prediction output layer가 없다.
Multi-layer RNN¶
num_layers는 쌓을 recurrent layer의 수를 지정함.
다음은 2개의 recurrent layer를 쌓은 RNN 의 구현 코드임:
- 첫 번째 recurrent layer는 입력 시퀀스를 처리한다.
- 두 번째 recurrent layer는 첫 번째 층이 모든 시점에서 만든 hidden state를 입력으로 받는다.
반환값의 의미는 다음과 같다.
output- 마지막 recurrent layer의 모든 시점별 hidden state
H_n[0]- 첫 번째 recurrent layer의 마지막 hidden state
H_n[1]- 두 번째 recurrent layer의 마지막 hidden state
2층 단방향 RNN의 tensor shape은 다음과 같다.
X: (batch_size, sequence_length, input_size)
H_0: (2, batch_size, hidden_size)
output: (batch_size, sequence_length, hidden_size)
H_n: (2, batch_size, hidden_size)
각 recurrent layer는 독립적인 parameter를 가진다.
첫 번째 recurrent layer:
weight_ih_l0: (hidden_size, input_size)
weight_hh_l0: (hidden_size, hidden_size)
bias_ih_l0: (hidden_size)
bias_hh_l0: (hidden_size)
두 번째 recurrent layer:
weight_ih_l1: (hidden_size, hidden_size)
weight_hh_l1: (hidden_size, hidden_size)
bias_ih_l1: (hidden_size)
bias_hh_l1: (hidden_size)
참고로,
dropout이 0보다 크면- 마지막 recurrent layer를 제외한 층 사이에 적용됨.
단방향과 양방향 설정의 shape¶
다음은 단방향 설정임:
단방향 RNN의 shape은 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_0: (num_layers, batch_size, hidden_size)
output: (batch_size, sequence_length, hidden_size)
H_n: (num_layers, batch_size, hidden_size)
다음은 양방향 설정임:
양방향 RNN의 shape은 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_0: (2 * num_layers, batch_size, hidden_size)
output: (batch_size, sequence_length, 2 * hidden_size)
H_n: (2 * num_layers, batch_size, hidden_size)
- 정방향과 역방향의 hidden state가 함께 저장되므로 관련 차원의 크기가 두 배가 된다.
참고로,
batch_first는X와output의 차원 순서만 바꾼다.H_0과H_n의 차원 순서에는 영향을 주지 않는다.
Prediction output 추가¶
다음 모델은 RNN과 prediction output weight를 하나의 모듈로 구현한 예제임:
import torch
from torch import nn
class RNNModel(nn.Module):
def __init__(
self,
input_size,
hidden_size,
output_size,
num_layers=1,
bidirectional=False,
):
super().__init__()
# 전체 시퀀스의 hidden states를 계산
self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
bidirectional=bidirectional,
)
if bidirectional:
recurrent_output_size = 2 * hidden_size
else:
recurrent_output_size = hidden_size
# hidden states를 predictions로 변환하는 output weights
self.output_layer = nn.Linear(
in_features=recurrent_output_size,
out_features=output_size,
)
def forward(self, X, H_0=None):
hidden_states, H_n = self.rnn(X, H_0)
predictions = self.output_layer(hidden_states)
return predictions, hidden_states, H_n
사용 방법:
model = RNNModel(
input_size=3,
hidden_size=5,
output_size=2,
num_layers=2,
bidirectional=False,
)
X = torch.randn(4, 10, 3)
Y_all, H_all, H_n = model(X)
Tensor shape 는 다음과 같음:
X: (batch_size, sequence_length, input_size)
H_all: (batch_size, sequence_length, hidden_size)
H_n: (2, batch_size, hidden_size)
Y_all: (batch_size, sequence_length, output_size)
Prediction output의 학습 가능한 parameter:
- 위 parameter shape은 단방향 설정을 기준으로 한다.
양방향 설정에서는 정방향과 역방향 hidden state가 결합되므로 다음 shape을 사용한다.
각 시점의 prediction 계산:
단방향 RNN에서 마지막 recurrent layer의 최종 hidden state만 사용하려면 다음과 같이 구현한다.
양방향 RNN에서는 마지막 recurrent layer의 정방향 최종 state와 역방향 최종 state를 결합한다.
bidirectional_model = RNNModel(
input_size=3,
hidden_size=5,
output_size=2,
num_layers=2,
bidirectional=True,
)
Y_all, H_all, H_n = bidirectional_model(X)
# H_n: (2 * num_layers, batch_size, hidden_size)
H_n_by_layer = H_n.reshape(
bidirectional_model.rnn.num_layers,
2,
H_n.size(1),
bidirectional_model.rnn.hidden_size,
)
H_forward = H_n_by_layer[-1, 0]
H_backward = H_n_by_layer[-1, 1]
H_last = torch.cat((H_forward, H_backward), dim=-1)
Y_last = bidirectional_model.output_layer(H_last)
H_forward: (batch_size, hidden_size)
H_backward: (batch_size, hidden_size)
H_last: (batch_size, 2 * hidden_size)
Y_last: (batch_size, output_size)
- 점선으로 표시한 부분은 모듈 외부에 별도로 추가한 부분이다.
예제: 동일한 가중치로 nn.RNNCell과 nn.RNN 비교¶
단일 층, 단방향, 동일한 활성화 함수를 사용하면 두 모듈은 같은 순환 연산을 수행할 수 있음.
정확한 비교를 위해 두 모듈의 가중치와 bias를 동일하게 만든다.
import torch
from torch import nn
# 재현 가능한 결과를 위해 random seed 고정
torch.manual_seed(0)
# 입력 sequence의 기본 크기 설정
batch_size = 4
sequence_length = 10
input_size = 3
hidden_size = 5
# 입력 tensor
#
# shape:
# (batch_size, sequence_length, input_size)
#
# 여기서는
# (4, 10, 3)
#
# 각 batch에는 길이 10의 sequence가 있고,
# 각 time step은 3개의 feature를 가짐.
X = torch.randn(
batch_size,
sequence_length,
input_size,
)
# initial hidden state
#
# RNNCell은 한 time step씩 처리하므로
# hidden state의 shape은 다음과 같음.
#
# (batch_size, hidden_size)
#
# 여기서는
# (4, 5)
H_0 = torch.zeros(
batch_size,
hidden_size,
)
# 전체 sequence를 한 번에 처리할 RNN
#
# batch_first=True 이므로 input shape은
#
# (batch_size, sequence_length, input_size)
#
# 형태를 사용함.
rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
num_layers=1,
nonlinearity="tanh",
bias=True,
batch_first=True,
bidirectional=False,
)
# 한 time step씩 처리할 RNNCell
#
# RNN과 동일하게
# input_size=3,
# hidden_size=5,
# tanh activation,
# bias 사용
#
# 으로 구성함.
rnn_cell = nn.RNNCell(
input_size=input_size,
hidden_size=hidden_size,
bias=True,
nonlinearity="tanh",
)
# RNN과 RNNCell이 같은 계산 결과를 내는지 비교하려면
# 두 module이 동일한 parameter를 사용해야 함.
#
# 따라서 RNN의 첫 번째 recurrent layer에 있는
# weight와 bias를 RNNCell로 복사함.
#
# parameter 값을 직접 변경하는 작업이므로
# gradient tracking은 필요하지 않음.
with torch.no_grad():
# input → hidden weight 복사
#
# rnn.weight_ih_l0
# shape:
# (hidden_size, input_size)
#
# 여기서는
# (5, 3)
rnn_cell.weight_ih.copy_(
rnn.weight_ih_l0
)
# hidden → hidden weight 복사
#
# rnn.weight_hh_l0
# shape:
# (hidden_size, hidden_size)
#
# 여기서는
# (5, 5)
rnn_cell.weight_hh.copy_(
rnn.weight_hh_l0
)
# input → hidden bias 복사
#
# shape:
# (hidden_size,)
#
# 여기서는
# (5,)
rnn_cell.bias_ih.copy_(
rnn.bias_ih_l0
)
# hidden → hidden bias 복사
#
# shape:
# (hidden_size,)
#
# 여기서는
# (5,)
rnn_cell.bias_hh.copy_(
rnn.bias_hh_l0
)
# ------------------------------------------------------------
# RNNCell로 sequence를 직접 순회
# ------------------------------------------------------------
# initial hidden state를 복사하여 시작
#
# clone()을 사용한 이유는
# H_0 자체를 그대로 참조하지 않고
# 별도의 tensor에서 hidden state를 계속 갱신하기 위함.
H_cell = H_0.clone()
# 각 time step에서 계산된 hidden state를 저장할 list
cell_hidden_states = []
# X의 shape:
#
# (batch_size, sequence_length, input_size)
#
# unbind(dim=1)을 수행하면
# sequence dimension을 따라 tensor를 분리함.
#
# 따라서 각 X_t의 shape은
#
# (batch_size, input_size)
#
# 여기서는
# (4, 3)
for X_t in X.unbind(dim=1):
# 현재 time step의 input과
# 이전 hidden state를 RNNCell에 전달
#
# X_t:
# (batch_size, input_size)
#
# H_cell:
# (batch_size, hidden_size)
#
# 반환되는 H_cell:
# (batch_size, hidden_size)
H_cell = rnn_cell(
X_t,
H_cell,
)
# 현재 time step의 hidden state 저장
cell_hidden_states.append(H_cell)
# cell_hidden_states는
# length가 sequence_length인 Python list임.
#
# 각 원소의 shape은
#
# (batch_size, hidden_size)
#
# torch.stack(..., dim=1)을 사용하면
# sequence dimension을 새로 만들어
#
# (batch_size, sequence_length, hidden_size)
#
# 형태로 결합됨.
cell_output = torch.stack(
cell_hidden_states,
dim=1,
)
# ------------------------------------------------------------
# RNN으로 전체 sequence를 한 번에 처리
# ------------------------------------------------------------
# nn.RNN에 전달하는 initial hidden state는
# layer dimension까지 포함해야 함.
#
# RNNCell용 H_0의 shape:
#
# (batch_size, hidden_size)
#
# unsqueeze(0) 후:
#
# (num_layers, batch_size, hidden_size)
#
# 여기서는
# (1, 4, 5)
rnn_output, H_rnn = rnn(
X,
H_0.unsqueeze(0),
)
# rnn_output에는
# 모든 time step의 hidden state가 저장됨.
#
# shape:
# (batch_size, sequence_length, hidden_size)
#
# 여기서는
# (4, 10, 5)
# H_rnn에는
# 각 recurrent layer의 final hidden state가 저장됨.
#
# shape:
# (num_layers, batch_size, hidden_size)
#
# 여기서는
# (1, 4, 5)
# ------------------------------------------------------------
# 결과 비교
# ------------------------------------------------------------
# 모든 time step의 hidden state 비교
#
# cell_output:
# RNNCell을 직접 반복하여 얻은 hidden-state sequence
#
# rnn_output:
# nn.RNN이 내부적으로 sequence를 처리하여 얻은 hidden-state sequence
print(
torch.allclose(
cell_output,
rnn_output,
)
)
# final hidden state 비교
#
# H_cell의 shape은
# (batch_size, hidden_size)
#
# H_rnn의 shape은
# (num_layers, batch_size, hidden_size)
#
# 따라서 H_cell에 layer dimension을 추가한 뒤 비교함.
print(
torch.allclose(
H_cell.unsqueeze(0),
H_rnn,
)
)
실행 결과:
- 첫 번째 결과는 모든 시점의 hidden state가 같음을 의미함.
- 두 번째 결과는 마지막 hidden state가 같음을 의미함.
두 모듈의 핵심 차이는 순환 계산식이 아니라 시퀀스 반복을 누가 담당하는지에 있다.
| 항목 | nn.RNNCell | nn.RNN |
|---|---|---|
| 한 번에 처리하는 범위 | 한 시점 | 전체 시퀀스 |
| 시점 반복 | 직접 작성 | 모듈 내부에서 처리 |
| 주 반환값 | 새 hidden state | 시점별 hidden states와 마지막 hidden state |
| 여러 recurrent layer | 직접 구성 | num_layers로 구성 |
| 양방향 처리 | 직접 구성 | bidirectional=True로 구성 |
| prediction output layer | 포함하지 않음 | 포함하지 않음 |
각각에 적절한 사용 시점은
- 세밀한 시점별 제어가 필요하면
nn.RNNCell이 적합. - 일반적인 시퀀스 처리가 목적이면
nn.RNN이 적합.