[Pytorch] Char RNN으로 단어 생성하기

juyeon lee·2023년 7월 28일

Deep Learning

목록 보기
5/5

📌 문자 단위 RNN(Char RNN)


PyTorch로 시작하는 딥 러닝 입문 - 11. 다대다 RNN을 이용한 텍스트 생성을 참고함

RNN의 입출력의 단위가 단어 레벨(word-level)이 아니라 문자 레벨(character-level)로 하여 RNN을 구현
➡️ RNN 구조 자체가 달라진 것이 아니라 입출력 단위가 문자로 바뀐 것


apple을 입력받으면 elep!를 출력하는 Char RNN 모델을 구현해보자.

0. import


필요한 라이브러리 import

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

1. 훈련데이터 전처리


☑️ 1-1 Vocabulary 생성

input과 label 데이터에 해당하는 단어의 알파벳을 포함하는 Vocabulary를 생성해야한다.

💻Input

#입력과 레이블 데이터에 대해 Vocab 생성
input_str = 'apple'
label_str = 'elep!'
char_vocab = sorted(list(set(input_str+label_str)))
vocab_size = len(char_vocab)
print(char_vocab)
print ('문자 집합의 크기 : {}'.format(vocab_size))

💻Output

['!', 'a', 'e', 'l', 'p']
문자 집합의 크기 : 5

현재 문자 집합에 총 5개의 문자 포함하여 [!, a, e, l, p]가 vocab에 저장된다.

💻Input

input_size = vocab_size # 입력의 크기는 문자 집합의 크기
hidden_size = 3
output_size = 5
learning_rate = 0.1

이 때, 원-핫 벡터를 이용해야하기 때문에 입력의 크기는 문자 집합의 크기여야한다.
은닉층은 3개, 학습률은 0.1로 설정한다.

☑️ 1-2 문자를 정수로 리턴

컴퓨터가 텍스트를 인식하기 위해서는 정수로 변환해주는 과정을 거쳐야한다.
따라서 vocab에 저장된 문자마다 고유한 정수로 매핑해준다.

💻Input

#문자 집합에 고유한 정수 부여
char_to_index = dict((c,i) for i,c in enumerate(char_vocab))
print(char_to_index)

💻Output

{'!': 0, 'a': 1, 'e': 2, 'l': 3, 'p': 4}

☑️ 1-3 정수를 문자로 리턴

예측 결과를 다시 문자 시퀀스로 리턴 받아야하기 때문에 정수로부터 문자를 얻는 과정도 추가해준다.

💻Input

index_to_char = {}
for key, value in char_to_index.items():
    index_to_char[value] = key
print(index_to_char)

💻Output

{0: '!', 1: 'a', 2: 'e', 3: 'l', 4: 'p'}

입력 데이터와 레이블 데이터의 각 문자들이 정수로 잘 매핑되었는지 확인해보자
💻Input

#입력 데이터와 레이블 데이터의 각 문자들을 정수로 매핑
x_data = [char_to_index[c] for c in input_str]
y_data = [char_to_index[c] for c in label_str]
print(x_data) # a, p, p, l, e
print(y_data) # e, l, e, p, !

💻Output

[1, 4, 4, 3, 2] # a, p, p, l, e
[2, 3, 2, 4, 0] # e, l, e, p, !

☑️ 1-4 배치 차원 추가

파이토치의 nn.RNN()은 기본적으로 3차원 텐서를 입력받기 때문에 배치 차원을 추가한다.

💻Input

# 배치 차원 추가
# 텐서 연산인 unsqueeze(0)를 통해 해결할 수도 있었음.
x_data = [x_data]
y_data = [y_data]
print(x_data)
print(y_data)

💻Output

[[1, 4, 4, 3, 2]]
[[2, 3, 2, 4, 0]]

입력 시퀀스의 각 문자들을 원-핫 벡터로 변경해준 후에
입력 데이터와 레이블 데이터를 텐서로 변경해준다.

💻Input

#입력 시퀀스의 각 문자들을 원-핫 벡터로 변경
x_one_hot = [np.eye(vocab_size)[x] for x in x_data]
print(x_one_hot)

#입력 데이터와 레이블 데이터 텐서로 변경
X = torch.FloatTensor(x_one_hot)
Y = torch.LongTensor(y_data)

#텐서 크기 확인
print('훈련 데이터 크기 : {}'.format(X.shape))
print('레이블 크기 : {}'.format(Y.shape))

💻Output

훈련 데이터 크기 : torch.Size([1, 5, 5])
레이블 크기 : torch.Size([1, 5])

2. 모델 구현


☑️ 2-1 모델 생성

💻Input

class Net(torch.nn.Module) :
    def __init__(self, input_size, hidden_size, output_size) :
        super(Net, self).__init__()
        #RNN셀 구현
        self.rnn = torch.nn.RNN(input_size, hidden_size, batch_first=True)
        #출력층 구현
        self.fc = torch.nn.Linear(hidden_size, output_size, bias=True)

    # 구현한 RNN 셀과 출력층을 연결
    def forward(self, x) :
        x, _status = self.rnn(x)
        x = self.fc(x)
        return x

#클래스로 정의한 모델을 net에 저장
net = Net(input_size, hidden_size, output_size)

#모델에 입력을 넣고 출력 크기 확인
outputs = net(X)
print('출력의 크기 : {}'.format(outputs.shape))

💻Output

출력의 크기 : torch.Size([1, 5, 5])

순서대로 배치 차원, 시점, 출력의 크기를 나타낸다.
이를 2차원 텐서로 변환하여 사이즈를 확인해보자.

💻Input

#2차원 텐서로 변환
print(outputs.view(-1, input_size).shape)

#레이블 데이터 크기
print(Y.shape)
print(Y.view(-1).shape)

💻Output

#2차원 텐서로 변환
torch.Size([5, 5])

#레이블 데이터 크기
torch.Size([1, 5])
torch.Size([5])

정확도를 측정할 때는 이걸 펼쳐서 계산
= 배치가 병렬적으로 계산되던 텐서를 하나로 이어서 정확도 측정한다는 의미

이 경우 (5)의 크기를 가지게 된다.

☑️ 2-2 모델 학습

💻Input

#옵티마이저와 손실 함수 정의
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), learning_rate)

#100번의 에폭 학습
for i in range(100) :
    optimizer.zero_grad()
    outputs = net(X)
    # view를 하는 이유는 Batch 차원 제거를 위해
    loss = criterion(outputs.view(-1,input_size), Y.view(-1))
    loss.backward() #기울기 계산
    optimizer.step() # 아까 optimizer 선언 시 넣어둔 파라미터 업데이트

    # 아래 세 줄은 모델이 실제 어떻게 예측했는지를 확인하기 위한 코드.
    result = outputs.data.numpy().argmax(axis=2) # 최종 예측값인 각 time-step 별 5차원 벡터에 대해서 가장 높은 값의 인덱스를 선택
    result_str = ''.join([index_to_char[c] for c in np.squeeze(result)])
print(i, "loss: ", loss.item(), "prediction: ", result, "true Y: ", y_data, "prediction str: ", result_str)

💻Output

99 loss:  0.002678581979125738 prediction:  [[2 3 2 4 0]] true Y:  [[2, 3, 2, 4, 0]] prediction str:  elep!

총 100번 반복하여 모델을 학습하고 텍스트를 생성한 결과, 정수와 문자 모두 label data값과 일치하는 걸 확인할 수 있다.



원문에는 label data를 'pple!'로 지정하여 실습하고, hidden_size 또한 5로 지정하였다.

하지만 label data를 다른 단어로 변경하여 실습하여보니 과적합되어 정확한 단어로 예측해주지 못하였다.

이 과정에서 hidden_size, learning_rate, 에폭 수를 조절하며 모델을 학습하였고, hidden_size를 3으로 감소시키니 그제서야 모델이 'elep!'를 생성해주었다.

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

좋은 글 감사합니다.

답글 달기