콘텐츠로 이동

PyTorch nn.RNNCellnn.RNN

PyTorch는 Simple RNN을 구현하기 위한 두 모듈을 제공한다.

  • nn.RNNCell

    • 한 시점의 순환 연산을 수행한다.
    • 전체 시퀀스를 처리하려면 반복문과 hidden state 전달을 직접 작성해야 한다.
  • nn.RNN

    • 전체 시퀀스의 순환 연산을 수행한다.
    • 시점 반복과 hidden state 전달을 모듈 내부에서 처리한다.

주의할 점은 PyTorch의 두 module이 반환하는 값은 hidden state 임.

  • 별도의 prediction output layer는 포함하지 않음.
  • 때문에 분류나 회귀를 위한 예측값이 필요하면 다음과 같은 출력 층을 추가할 것:
output_layer = nn.Linear(hidden_size, output_size)
prediction = output_layer(hidden_state)


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)

RNNCell의 한 시점 데이터 흐름 현재 입력 X_t와 이전 hidden state H_(t-1)가 RNNCell에 들어가 새로운 hidden state H_t가 나온다. 현재 입력 Xt 이전 hidden state Ht−1 RNNCell 새 hidden state Ht

반환값: 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 는 다음과 같음:

model.output_layer.weight  # (output_size, hidden_size)
model.output_layer.bias    # (output_size,)

각 시점의 prediction 계산:

Y_t = H_t @ output_layer.weight.T + output_layer.bias

RNNCell과 별도의 출력 층 RNNCell은 hidden state를 반환하고 별도의 Linear 층이 hidden state를 prediction으로 변환한다. RNNCell hidden state Ht 별도 추가 nn.Linear prediction Yt

  • 점선으로 표시한 부분은 모듈 외부에 별도로 추가한 부분임.


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_firstFalse가 기본값이나
  • 많은 경우 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은 다음 설정을 기준으로 함:

num_layers = 1
bidirectional = False
batch_first = True

RNN의 시퀀스 데이터 흐름 입력 시퀀스와 초기 hidden state가 RNN으로 들어가 모든 시점의 hidden state와 마지막 hidden state가 반환된다. 입력 시퀀스 X 초기 hidden state H0 RNN 시점 반복 내장 시점별 hidden states output 마지막 hidden state Hn

반환값은 다음과 같음:

output, H_n = rnn(X, H_0)
  • 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 의 구현 코드임:

rnn = nn.RNN(
    input_size=3,
    hidden_size=5,
    num_layers=2,
    batch_first=True,
)
  • 첫 번째 recurrent layer는 입력 시퀀스를 처리한다.
  • 두 번째 recurrent layer는 첫 번째 층이 모든 시점에서 만든 hidden state를 입력으로 받는다.

2층 RNN의 데이터 흐름 입력 시퀀스가 첫 번째 recurrent layer를 통과하고 첫 번째 층의 시점별 hidden state가 두 번째 recurrent layer의 입력이 된다. 두 번째 층의 시점별 hidden state가 output으로 반환된다. 입력 시퀀스 X recurrent layer 1 시점별 hidden states recurrent layer 2 시점별 hidden states output

반환값의 의미는 다음과 같다.

  • 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

다음은 단방향 설정임:

bidirectional=False
batch_first=True

단방향 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)

다음은 양방향 설정임:

bidirectional=True
batch_first=True

양방향 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_firstXoutput의 차원 순서만 바꾼다.
  • H_0H_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:

model.output_layer.weight  # (output_size, hidden_size)
model.output_layer.bias    # (output_size,)
  • 위 parameter shape은 단방향 설정을 기준으로 한다.

양방향 설정에서는 정방향과 역방향 hidden state가 결합되므로 다음 shape을 사용한다.

model.output_layer.weight:  (output_size, 2 * hidden_size)
model.output_layer.bias:    (output_size)

각 시점의 prediction 계산:

Y_t = H_t @ output_layer.weight.T + output_layer.bias

단방향 RNN에서 마지막 recurrent layer의 최종 hidden state만 사용하려면 다음과 같이 구현한다.

H_last = H_n[-1]
Y_last = model.output_layer(H_last)
H_last:  (batch_size, hidden_size)
Y_last:  (batch_size, output_size)

양방향 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)

RNN과 별도의 출력 층 RNN은 hidden states를 반환하고 별도의 Linear 층이 이를 prediction으로 변환한다. RNN 시점별 hidden states output 별도 추가 nn.Linear prediction Y

  • 점선으로 표시한 부분은 모듈 외부에 별도로 추가한 부분이다.


예제: 동일한 가중치로 nn.RNNCellnn.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,
    )
)

실행 결과:

True
True
  • 첫 번째 결과는 모든 시점의 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이 적합.