RNN의 입출력의 단위가 단어 레벨(word-level)이 아니라 문자 레벨(character-level)로 하여 RNN을 구현
➡️ RNN 구조 자체가 달라진 것이 아니라 입출력 단위가 문자로 바뀐 것
apple을 입력받으면 elep!를 출력하는 Char RNN 모델을 구현해보자.
필요한 라이브러리 import
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
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로 설정한다.
컴퓨터가 텍스트를 인식하기 위해서는 정수로 변환해주는 과정을 거쳐야한다.
따라서 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}
예측 결과를 다시 문자 시퀀스로 리턴 받아야하기 때문에 정수로부터 문자를 얻는 과정도 추가해준다.
💻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, !
파이토치의 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])
💻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)의 크기를 가지게 된다.
💻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!'를 생성해주었다.
좋은 글 감사합니다.