PyTorch LSTM 예제

이 예제는 파이토치를 활용해서 LSTM을 구현하는 예제입니다.
LSTM은 RNN(Recurrent Neural Network)의 하나로 좋은 성능을 발휘하는 모델입니다. 파이토치는 LSTM를 직관적으로 구현할 수 있도록 하는 좋은 인공지능 프레임워크입니다.

본 예제는 현재 문장을 주고 다음 문장을 예측하는 알고리즘입니다. 이러한 예제들을 활용하면 다양한 시계열 데이터를 다룰 수 있습니다. 시계열 데이터라 함은 데이터가 어떤 시간의 순서를 가진다는 것을 의미합니다.

예를 들어 한 문장의 다양한 단어들은 비록 같은 단어일지라도 앞에 오느냐 뒤에 오느냐에 따라서 그 의미가 달라지는 경우가 있습니다. 그렇기 때문에 현재 문장을 유추하기 위해서는 앞에 어떤 단어가 있는지를 알아내는 것이 중요합니다.
RNN은 이러한 예측을 가능하게 해줍니다.

RNN은 두개의 Linear 모델이 합쳐진 하나의 Activation Function입니다. 아래 그림이 이를 잘 설명하고 있습니다.

https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

입력 문장은 “In the beginning God created the heavens and the earth”라는 문장입니다. x 데이터는 입력 문장이고 x 데이터를 통해 예측한 결과를 비교하기 위해서 정답 데이터셋 y를 만듭니다. y 데이터는 x 데이터의 첫번째 입력 ‘I’의 경우 ‘n’를 예측하고 ‘n’가 입력된 경우 ‘ ‘(공백) 를 시스템이 예측할 수 있도록 하기 위함입니다.

sentence = 'In the beginning God created the heavens and the earth'
x = sentence[:-1]
y = sentence[1:]

char_set = list(set(sentence))
input_size = len(char_set)
hidden_size = len(char_set)

index2char = {i:c for i, c in enumerate(char_set)}
char2index = {c:i for i, c in enumerate(char_set)}

index2char과 char2index는 각각 문자를 문자 자체로 입력하지 않고 one-hot의 형태로 입력하기 위해서 만들어준 python dict입니다.
char2index를 출력하면 아래와 같은 형태가 됩니다.

{‘s’: 0, ‘ ‘: 1, ‘t’: 2, ‘I’: 3, ‘o’: 4, ‘h’: 5, ‘e’: 6, ‘g’: 7, ‘d’: 8, ‘c’: 9, ‘b’: 10, ‘i’: 11, ‘n’: 12, ‘G’: 13, ‘v’: 14, ‘a’: 15, ‘r’: 16}

one_hot = []
for i, tkn in enumerate(x):
    one_hot.append(np.eye(len(char_set), dtype='int')[char2index[tkn]])

x_train = torch.Tensor(one_hot)
x_train = x_train.view(1,len(x),-1)

입력된 sentence는 그대로 입력값으로 사용하지 않고 one-hot 형태로 변경해서 최종 x_train 형태의 데이터를 만듭니다. 문장을 one-hot 형태로 만들기 위해서 numpy의 eye함수를 사용합니다.

print(x_train)

tensor([[[0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
         [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.] ...

x_train 데이터를 출력하면 위와 같은 형태가 만들어집니다. 위의 데이터셋은 [1, 10, 8] 형태의 3차원 데이터셋입니다. NLP 데이터를 3차원 형태의 입력 데이터셋을 가집니다. 첫번째 차원은 문장의 갯수, 두번째는 단어의 갯수, 세번째는 단어의 입력 차원입니다.

참고로 CNN의 경우에는 4차원 형태의 데이터를 가집니다.
다음으로 아래와 같이 y_data를 만들어줍니다.

# y label
y_data = [char2index[c] for c in y]
y_data = torch.Tensor(y_data)

이제 모델을 만들 차례입니다.
파이토치는 nn.Module을 사용해서 Module을 만들 수 있습니다.

class RNN(nn.Module):
    
    # (batch_size, n, ) torch already know, you don't need to let torch know
    def __init__(self,input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        
        self.rnn = nn.LSTM(
            input_size = input_size, 
            hidden_size = hidden_size, 
            num_layers = 4, 
            batch_first = True,
            bidirectional = True
        )
        
        self.layers = nn.Sequential(
            nn.ReLU(),
            nn.Linear(input_size*2, hidden_size),
        )
        
    def forward(self, x):
        y,_ = self.rnn(x)
        y = self.layers(y)
        return y
    
model = RNN(input_size, hidden_size)
model

RNN 클래스는 init, forward 함수로 구성됩니다. init함수는 LSTM 모델을 선언하는 부분과 softamx 함수를 선언하는 두부분이 있습니다. LSTM 함수는 두개의 인자값을 기본으로 받습니다. input_size, hidden_size입니다. input_size는 입력 벡터의 크기이며 hidden_size는 출력 벡터의 크기입니다. 본 예제에서는 입력 벡터와 출력 벡터가 크기가 같습니다. 배치 사이즈나 시퀀스 사이즈는 파이토치에서 자동으로 계산하기 때문에 입력할 필요가 없습니다.
num_layers는 RNN의 층을 의미합니다. 본 예제는 4개의 층으로 구성했기 때문에 num_layers를 4로 설정했습니다. 그리고 bidirectional을 True로 했기 때문에 마지막 output의 형태는 input_size*2의 형태가 됩니다.
Linear 레이어는 input_size의 차원을 줄이기 위해서 선언합니다.

또 모델을 만들면서 중요한 것은 batch_first를 True로 해줘야 한다는 것입니다. 그렇지 않으면 time-step(=sequence_length), batch_size, input_vector 의 형태가 됩니다.

선언한 모델의 정보를 출력해보면 다음과 같습니다.
RNN(
(rnn): LSTM(8, 8, num_layers=4, bidirectional=True)
(layers): Sequential(
(0): ReLU()
(1): Linear(in_features=16, out_features=8, bias=True)
)
)

# loss & optimizer setting
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

# start training
for i in range(5000):
    model.train()
    outputs = model(x_train)
    loss = criterion(outputs.view(-1, input_size), y_data.view(-1).long())
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if i%500 == 0:
        result = outputs.data.numpy().argmax(axis=2)
        result_str = ''.join([char_set[c] for c in np.squeeze(result)])
        print(i, "loss: ", loss.item(), "\nprediction: ", result, "\ntrue Y: ", y_data, "\nprediction str: ", result_str,"\n")

위의 코드와 같이 학습을 수행합니다.
x_train 데이터를 입력 받아 나온 결과를 y_data와 비교해서 loss를 계산하고 이 loss 값을 Back-propagation을 수행하고 Gradient를 초기화하는 과정을 반복합니다.

5000 회 학습할 경우 다음과 같이 loss가 내려가는 것을 확인 할 수 있습니다.

실제로 데이터를 돌려보면 약 1500번 정도 학습을 완료하면 입력 단어를 통해서 정확히 다음 단어를 예측하는 것을 확인할 수 있습니다.

1500 loss: 0.31987816095352173
prediction: [[12 1 2 5 6 1 10 6 7 11 12 12 11 12 7 1 13 4 8 1 9 16 6 15
2 6 8 1 2 5 6 1 5 6 15 14 6 12 0 1 15 12 8 1 2 5 6 1
6 15 16 2 5]]
true Y: tensor([12., 1., 2., 5., 6., 1., 10., 6., 7., 11., 12., 12., 11., 12.,
7., 1., 13., 4., 8., 1., 9., 16., 6., 15., 2., 6., 8., 1.,
2., 5., 6., 1., 5., 6., 15., 14., 6., 12., 0., 1., 15., 12.,
8., 1., 2., 5., 6., 1., 6., 15., 16., 2., 5.])
prediction str: n the beginning God created the heavens and the earth

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다