4.1 순환 신경망

유명곤·5일 전

순서가 있는 데이터를 처리하려면 현재 입력과 앞서 받은 정보를 함께 계산해야 한다. 순환 신경망(Recurrent Neural Network, RNN)은 이전 계산의 상태를 다음 계산에 전달하는 신경망이다. 상태가 이어지는 방식을 이해하면, 입력의 길이가 달라져도 같은 모델을 쓰는 이유와 반환되는 텐서의 크기를 함께 설명할 수 있다.

이전 정보를 은닉 상태로 전달한다

시퀀스(Sequence)는 순서가 있는 데이터의 나열이다. RNN에서는 각 입력의 위치를 시점(Time step)이라고 부른다. 문장이라면 단어가 놓인 순서가 시점이 될 수 있다.

RNN은 현재 입력과 이전 은닉 상태(Hidden state)로 새 은닉 상태를 만든다. 은닉 상태는 앞서 받은 입력의 정보를 다음 계산으로 전달하는 벡터다. 원래 입력을 그대로 저장하지 않으며, 어떤 정보를 담을지는 학습으로 정해진다. 새 상태를 다시 다음 시점에 넘기므로 이전 입력의 영향이 이후 계산에 이어진다. 같은 입력도 순서가 바뀌면 다른 상태를 만들 수 있다.

기본 RNN의 상태 갱신식은 다음과 같다. 벡터는 열벡터로 두고, 시점은 t=1,…,Tt=1,\ldots,T로 표시한다.

ht=tanh⁡(Wxxt+Whht−1+b)\mathbf{h}_t=\tanh\left(\mathbf{W}_x\mathbf{x}_t+\mathbf{W}_h\mathbf{h}_{t-1}+\mathbf{b}\right)

xt\mathbf{x}_t는 현재 입력, ht−1\mathbf{h}_{t-1}은 이전 상태다. Wx\mathbf{W}_x는 입력을 은닉 상태의 크기로 바꾸고, Wh\mathbf{W}_h는 이전 상태가 새 상태에 미칠 영향을 계산한다. 두 결과에 편향 b\mathbf{b}를 더한 뒤, 각 성분에 하이퍼볼릭 탄젠트 함수 tanh⁡\tanh를 적용한다. 이 함수는 실수를 −1-1과 11 사이로 바꾸는 비선형 함수다.

입력 특성 수가 dd, 은닉 상태 크기가 HH라면 Wx\mathbf{W}_x는 H×dH\times d, Wh\mathbf{W}_h는 H×HH\times H이다. 두 행렬곱의 결과와 편향이 모두 H×1H\times1이므로 서로 더할 수 있다. 여기서 HH는 상태 벡터의 크기이며, 기억할 단어 수를 뜻하지 않는다.

첫 계산에는 초기 상태 h0\mathbf{h}_0가 필요하다. PyTorch의 nn.RNN은 이를 따로 전달하지 않으면 0으로 둔다. 기본 활성화 함수는 tanh이며 relu로 바꿀 수도 있다. 상태를 계속 넘긴다고 해서 오래전 입력의 정보가 끝까지 충분히 유지된다는 보장은 없다.

입력 길이와 파라미터 수는 다르다

상태를 한 시점 갱신하는 계산 단위를 RNN 셀(Cell)이라고 한다. 한 층의 모든 시점은 같은 가중치와 편향을 사용한다. 이러한 가중치 공유(Weight sharing) 때문에 시퀀스가 길어지면 계산 횟수는 늘지만, 그 길이에 비례해 파라미터가 추가되지는 않는다.

위 식처럼 편향 벡터가 하나이면 RNN 층의 파라미터 수는 다음과 같다.

Hd+H2+HHd+H^2+H

각 항은 입력 가중치, 상태 가중치, 편향의 원소 수다. 입력 특성 4개와 은닉 크기 8을 사용하면 8×4+8×8+8=1048\times4+8\times8+8=104개이다.

PyTorch의 기본 nn.RNN은 입력 쪽 편향과 상태 쪽 편향을 따로 저장한다. 두 편향의 합은 위 식의 편향 하나처럼 작용하지만, 저장되는 학습 변수는 두 벡터다. 따라서 단방향·1층·bias=True인 nn.RNN(4, 8)의 파라미터 수는 다음과 같다.

Hd+H2+2H=8×4+8×8+2×8=112Hd+H^2+2H=8\times4+8\times8+2\times8=112

두 계산 모두 RNN 층만 센 것이다. 은닉 상태를 예측값으로 바꾸는 출력층을 붙이면 그 층의 파라미터도 더해야 한다.

모든 시점의 상태와 최종 상태를 구분한다

배치(Batch)는 한 번에 처리하는 여러 시퀀스의 묶음이다. batch_first=True이면 입력 크기는 (B, T, d)이다. BB는 시퀀스 수, TT는 시퀀스 길이, dd는 한 시점의 특성 수다.

nn.RNN은 outputs와 h_n을 반환한다. 단방향 1층에서 outputs는 모든 시점의 상태를 (B, T, H)로 담고, h_n은 마지막 상태만 (1, B, H)로 담는다. batch_first=True는 입력과 outputs의 축 순서를 정하며, h_n에는 적용되지 않는다.

은닉 상태는 예측에 사용할 중간 표현이다. 시퀀스 하나에 결과 하나가 필요한 다 대 일(Many-to-one) 문제에서는 마지막 상태를 분류층에 연결할 수 있다. 각 입력 위치마다 결과가 필요한 다 대 다(Many-to-many) 문제에서는 각 시점의 상태를 사용한다. nn.RNN 자체는 이러한 분류층을 포함하지 않는다.

다음은 길이 3, 특성 수 4인 시퀀스 2개를 처리하는 예시다. 은닉 크기는 8로 두었다.

실행 미확인 코드다. 아래 크기는 실제 출력 로그가 아니라 공식 API에서 도출한 값이다.

import torch
from torch import nn

inputs = torch.zeros(2, 3, 4)
rnn = nn.RNN(input_size=4, hidden_size=8, batch_first=True)
outputs, h_n = rnn(inputs)

outputs는 (2, 3, 8), h_n은 (1, 2, 8)이다. 이 단방향 1층 설정에서는 outputs[:, -1, :]와 h_n[-1]이 같은 마지막 상태를 가리킨다. 다만 마지막은 모델에 전달한 시퀀스의 끝이다. 뒤에 패딩(Padding), 즉 길이를 맞추기 위한 값을 붙여 그대로 처리하면 실제 입력이 끝난 뒤에도 상태 갱신이 이어진다.

층과 방향은 반환값에 다르게 반영된다

다층 RNN(Stacked RNN)은 RNN 층을 여러 개 쌓는다. 다음 층은 같은 시점에서 이전 층이 만든 상태를 입력으로 받고, 자신의 이전 시점 상태와 함께 계산한다. 한 층 안에서는 시점 간 가중치를 공유하지만 층마다 가중치는 별도로 가진다.

양방향 RNN(Bidirectional RNN)은 정방향과 역방향에 각각 RNN을 두고, 같은 위치에서 얻은 두 상태를 이어 붙인다. 두 방향은 서로 다른 가중치를 사용한다. 각 방향의 은닉 크기가 HH라면 합친 표현은 2H2H이다.

층 수를 LL, 방향 수를 DD라고 하면 batch_first=True일 때 반환값은 다음과 같다. 단방향은 D=1D=1, 양방향은 D=2D=2이다.

반환값크기담는 상태
outputs(B, T, D*H)마지막 층의 모든 시점
h_n(D*L, B, H)각 층·각 방향의 최종 상태

층을 늘려도 outputs에는 마지막 층만 담기므로 층 수 축이 생기지 않는다. 반면 h_n은 모든 층의 최종 상태를 담으므로 첫 축에 층 수가 반영된다. 앞 코드에서 num_layers=2, bidirectional=True로 바꾸면 두 반환값은 각각 (2, 3, 16), (4, 2, 8)이 된다.

양방향 모델의 outputs[:, -1, :]는 두 방향의 최종 상태를 합친 값이 아니다. 원래 시퀀스의 마지막 위치에서 정방향은 처리를 마쳤지만, 역방향은 그 위치부터 읽기 시작하기 때문이다. 두 방향이 각각 전체 입력을 처리한 상태가 필요하면 h_n에서 해당 층의 두 상태를 꺼내야 한다.

양방향 처리를 쓸 수 있는지는 예측할 때 입력의 어느 범위까지 주어지는가에 달려 있다. 완성된 문장이나 이미 관측한 과거 구간은 양방향으로 읽을 수 있다. 아직 관측하지 않은 값을 입력에 포함해서는 안 된다. 따라서 미래 값을 예측하는 문제라도, 이미 주어진 과거 구간 안에서 양방향으로 처리하는 것까지 금지되는 것은 아니다.

참고자료

profile
Werde, der du bist!

0개의 댓글