문자 단위 RNN (Char RNN)

2024. 7. 16. 17:50CNN, RNN, LSTM

 

 

📍 문자 단위 RNN(Char RNN)

  • 모든 시점의 입력에 대해서 모든 시점에 대해서 출력을 하는 다대다 RNN
  • 다대다 RNN은 대표적으로 품사 태깅, 개체명 인식 등에서 사용
  • RNN의 입출력의 단위가 단어 레벨(word-level)이 아니라 문자 레벨(character-level)로 하여 RNN을 구현한다면, 이를 문자 단위 RNN이라고 합니다
import torch
import torch.nn as nn
import torch.optim as optim

import numpy as np
 

✅ 1. 훈련 데이터 전처리하기

  • 문자 시퀀스 apple을 입력받으면 delicious를 출력하는 RNN을 구현할 예정입니다!
  • 입력 데이터와 레이블 데이터에 대해서 문자 집합(voabulary)을 만듭니다(문자 집합은 중복을 제거한 문자들의 집합)
input_str = 'apple'
output_str = 'pple!'

char_vocab = sorted(list(set((input_str + output_str))))

vocab_size = len(char_vocab)
print('문자 집합 크기 : {}'.format(vocab_size))

📢 현재 문자 집합에는 총 5개의 문자가 있습니다. !, a, e, l, p .이제 하이퍼파라미터를 정의해줍니다. 이때 입력은 원-핫 벡터를 사용할 것이므로 입력의 크기는 문자 집합의 크기여야만 합니다.

input_size = vocab_size
hidden_size = 5
output_size = 5
lr = 0.1

📢 이제 문자 집합에 고유한 정수를 부여합니다.

char_to_index = dict((c, i) for i, c in enumerate(char_vocab))
print(char_to_index)

📢 나중에 예측 결과를 다시 문자 시퀀스로 보기위해서 반대로 정수로부터 문자를 얻을 수 있는 index_to_char을 만듭니다.

index_to_char = dict()

for k, v in char_to_index.items():
    index_to_char[v] = k

print(index_to_char)

📢 이제 입력 데이터와 레이블 데이터의 각 문자들을 정수로 맵핑합니다.

x_data = [char_to_index[c] for c in input_str]
y_data = [char_to_index[c] for c in output_str]

print(x_data)
print(y_data)

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

# 배치 차원 추가

x_data = [x_data]
y_data = [y_data]

print(x_data)
print(y_data)

# Pytorch에서의 텐서 연산인 unsqueeze(0)도 활용 가능

x_data2 = torch.tensor(x_data).unsqueeze(0)
y_data2 = torch.tensor(y_data).unsqueeze(0)

print(x_data2.shape)
print(y_data2.shape)

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

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_)

# 텐서의 shape 확인

print('학습 데이터의 크기 : {}'.format(X.shape))
print('레이블 데이터의 크기 : {}'.format(Y.shape))

✅ 모델 구현하기

class Net(nn.Module):

    def __init__(self, input_size, hidden_size, output_size):

        super(Net, self).__init__()

        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size, bias=True) # 출력층
        
    def forward(self, x): # 구현한 RNN 셀과 FC 층을 연결하는 역할

        x, _ = self.rnn(x)
        x = self.fc(x)
    
        return x
net = Net(input_size, hidden_size, output_size)
outputs = net(X)

print(outputs.shape)

📢 (1, 5, 5)의 크기를 가지는데 각각 배치 차원, 시점(timesteps), 출력의 크기입니다. 나중에 정확도를 측정할 때는 이를 모두 펼쳐서 계산하게 되는데, 이때는 view를 사용하여 배치 차원과 시점 차원을 하나로 만듭니다.

print(outputs.view(-1, output_size).shape)

📢 레이블 데이터 크기 RECAP

print(Y.shape)
print(Y.view(-1).shape)

📢 레이블 데이터는 (1, 5)의 크기를 가지는데, 마찬가지로 나중에 정확도를 측정할 때는 이걸 펼쳐서 계산할 예정입니다. 이 경우 (5)의 크기를 가지게 됩니다. 이제 옵티마이저와 손실 함수를 정의합니다.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr)
# Training code

epoch = 100

for i in range(epoch):

    optimizer.zero_grad()

    outputs = net(X)

    loss = criterion(outputs.view(-1, output_size), Y.view(-1)) # view() -> Batch dimension 제거를 위해
    loss.backward() # 그래디언트 계산

    optimizer.step()

    # 프로그래스 바
    result = outputs.data.numpy().argmax(axis=2) # output(5개의 값)에 대해서 가장 높은 값의 인덱스 선택!, 숫자 형태
    result_str = ''.join([index_to_char[c] for c in np.squeeze(result)]) # helper function을 호출하여 char를 str 형태로 변환
    print(i, "loss : ", loss.item(), "prediction : ", result, "label : ", y_data, "output_str : ", result_str)

 

📍 문자 단위 RNN - with Sentence data

import torch
import torch.nn as nn
import torch.optim as optim

✅ 1. 훈련 데이터 전처리하기

sentence = ("if you want to build a ship, don't drum up people together to "
            "collect wood and don't assign them tasks and work, but rather "
            "teach them to long for the endless immensity of the sea.")
print(len(sentence))

char_set = sorted(list(set(sentence))) # 문자 집합 생성
char_dic = {c: i for i, c in enumerate(char_set)} # 각 문자에 정수 인코딩

print(char_dic) # ❗❗ 문자에서는 공백도 하나의 원소로 대응 ❗❗

📢 각 문자에 정수가 부여되었으며, 총 25개의 문자가 존재! 문자 집합의 크기 또한 25

print('문자 집합의 크기 : {}'.format(len(char_dic)))

📢 입력을 원-핫 벡터로 사용할 것이므로 이는 매 시점마다 들어갈 입력의 크기이기도 합니다. hidden_size(은닉 상태의 크기)를 입력의 크기와 동일하게 줬는데, 이는 사용자의 선택으로 다른 값을 줘도 무방!

# 문장 데이터이기 때문에 단위별로 끊어서 처리
# sentence_length라는 변수를 활용해보자

hidden_size = len(char_dic)
sentence_length = 10 # 문장을 문자 10개 단위로 끊어서 전처리
lr = 0.1

📢 sequence_length 값인 10의 단위로 샘플들을 잘라서 데이터를 만들기

# 데이터 구성

x_data = list()
y_data = list()

for i in range(0, len(sentence) - sentence_length):
    x_str = sentence[i: i + sentence_length]
    y_str = sentence[i + 1: i + sentence_length + 1]
    print(i, x_str, '->', y_str)

    x_data.append([char_dic[c] for c in x_str]) # 입력 데이터를 single-digit화
    y_data.append([char_dic[c] for c in y_str])

  • 📌 현재까지 총 170개로 구성된 데이터 셋(x, y 페어)를 생성하였음. 각 페어의 입력 데이터는 고유 정수로 인코딩이 된 상태
print(x_data[0])
print(y_data[0])

📢 한 칸씩 쉬프트 된 시퀀스가 정상적으로 출력되는 것을 볼 수 있습니다. 이제 입력 시퀀스에 대해서 원-핫 인코딩을 수행하고, 입력 데이터와 레이블 데이터를 텐서로 변환합니다.

x_one_hot = [np.eye(len(char_dic))[x] for x in x_data] # one-hot 벡터 새애성

X = torch.FloatTensor(x_one_hot)
Y = torch.LongTensor(y_data)
print('학습 데이터의 크기 : {}'.format(X.shape))
print('레이블 데이터의 크기 : {}'.format(Y.shape))

print(Y[0])

✅ 모델 구현하기

class Net(torch.nn.Module):

    def __init__(self, input_dim, hidden_dim, layers):

        super(Net, self).__init__()
        
        self.rnn = torch.nn.RNN(input_dim, hidden_dim, num_layers=layers, batch_first=True)
        self.fc = torch.nn.Linear(hidden_dim, hidden_dim, bias=True)

    def forward(self, x):
        x, _ = self.rnn(x)
        x = self.fc(x)

        return x
dic_size = len(char_dic)

net = Net(dic_size, hidden_size, 2) # hidden layer가 2층 짜리인 DRNN
# 손실 함수

criterion = nn.CrossEntropyLoss()

# 옵티마이저

optimizer = optim.Adam(net.parameters(), lr)

📢 학습을 진행해봅시다!

 

epoch = 100

for i in range(epoch):

    optimizer.zero_grad()

    outputs = net(X)

    loss = criterion(outputs.view(-1, dic_size), Y.view(-1))
    loss.backward()

    optimizer.step()

    # 프로그레스 바
    results = outputs.argmax(dim=2)

    predict_str = str()

    for j, result in enumerate(results) :
        if j == 0:
            predict_str += ''.join([char_set[t] for t in result])
        else:
            predict_str += char_set[result[-1]]

    print(predict_str)

📢 실습 문제 - 어떤 문자열이라도 sample에 들어가면 동작할 수 있도록 설계된 Charseq 코드를 구현해보자

## Charseq 실습

smaple = " if you want you"

# 딕셔너리 만들기

char_set = sorted(list(set(smaple)))
char_dic = {c: i for i, c in enumerate(char_set)}
dic_size = len(char_dic)

print(char_dic)

# 네트워크 하이퍼파라미터 설정

hidden_size = dic_size # 은닉층 사이즈는 문자 집합 크기와 무관!!! 랜덤하게 사용 가능

# 데이터 준비

idx = [char_dic[c] for c in smaple]
x_data = [idx[:-1]]

print('x data : {}'.format(x_data))

x_one_hot = [np.eye(dic_size)[x] for x in x_data]

print('one-hot vector : {}'.format(x_one_hot))

y_data = [idx[1:]]

print('y data : {}'.format(y_data))

# tensor casting

X = torch.FloatTensor(x_one_hot)
Y = torch.LongTensor(y_data)

# RNN 선언

rnn = nn.RNN(dic_size, hidden_size, batch_first=True)

# Loss와 optimizer 정의

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(rnn.parameters(), lr)
# 간단한 학습 코드

epoch = 50

for i in range(epoch):

    optimizer.zero_grad()

    outputs, _ = rnn(X)

    loss = criterion(outputs.view(-1, dic_size), Y.view(-1))
    loss.backward()

    optimizer.step()

    # 결과

    results = outputs.data.numpy().argmax(axis=2)
    predict_str = ''.join([char_set[c] for c in np.squeeze(results)])
    print(i, "loss : ", loss.item(), "prediction : ", result, "label : ", y_data, "prediciton_str : ", result_str)

 

📍 Char RNN으로 이름 분류하기

from google.colab import drive

import sys
import os

drive.mount('/content/drive')

  • data/names/[language].txt이 18개 존재
  • 각 파일에는 한 줄에 하나의 이름이 적혀져 있음 -> 로마자로 되어 있음
  • UNICODE -> ASCII
from __future__ import unicode_literals, print_function, division
from io import open

import glob
import os

def findFiles(path):
    return glob.glob(path)

print(findFiles('/content/drive/My Drive/Colab Notebooks/Deep Learning/data/names/*.txt'))

import unicodedata
import string

all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)

# 유니코드 문자열을 ASCII로 변환

def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
        and c in all_letters
    )

print(unicodeToAscii('Ślusàrski'))

# 이름 데이터 파일을 읽고 줄 단위로 분리 -> 데이터 셋 구축!!

def readLines(filename) :
    lines = open(filename, encoding='utf-8').read().strip().split('\n')
    return [unicodeToAscii(line) for line in lines]
category_lines = dict()
all_categories = list()

for filename in findFiles('/content/drive/My Drive/Colab Notebooks/Deep Learning/data/names/*.txt'):
    category = os.path.splitext(os.path.basename(filename))[0]
    all_categories.append(category)
    lines = readLines(filename)
    category_lines[category] = lines

n_categories = len(all_categories)

print(all_categories)
print(category_lines)

  • 📌 각 category(언어)를 line(이름)에 매핑하는 dict인 category_lines 생성
print(category_lines['Korean'][5:10])

 

✅ 이름을 텐서로 변경

  • 하나의 문자를 표현하기 위해 one-hot 벡터를 사용
  • 단어를 만들기 위해 one-hot 벡터들을 2차원 매트릭스로 만들어 주어야 합니다.
import torch

# all_letters로 문자의 위치(인덱스) 찾기 => 'a' -> 0

def letterToIndex(letter):
    return all_letters.find(letter)

# 문자 하나를 one-hot vector(1 * 문자 집합의 크기) 텐서로 변환하는 함수

def letterToTensor(letter):
    t = torch.zeros(1, n_letters) # n_letters : 문자 집합의 크기
    t[0][letterToIndex(letter)] = 1
    return t
# 이름을 <line_length * 1 n_letters> 로 바꿔주어야 함

def nameToTensor(line) :
    tensor = torch.zeros(len(line), 1, n_letters)
    for l, letter in enumerate(line):
        tensor[l][0][letterToIndex(letter)] = 1
    return tensor
print(letterToTensor('J'))

print(nameToTensor('Jones').size())

 

✅ 네트워크 생성

RNN 모델을 구현해 보겠습니다

import torch.nn as nn
import torch.nn.functional as F

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()

        self.hidden_size = hidden_size

        self.i2h = nn.Linear(input_size, hidden_size) # M * N
        self.h2h = nn.Linear(hidden_size, hidden_size) # N * N => (M * N) * (N * N) = (M * N)
        self.h2o = nn.Linear(hidden_size, output_size) # N * P => (M * N) * (N * P) = (M * P)
        self.softmax = nn.LogSoftmax(dim=1) # M * P

    def forward(self, input, hidden):
        hidden = F.tanh(self.i2h(input) + self.h2h(hidden))
        output = self.h2o(hidden)
        output = self.softmax(self.h2o(hidden))

        return output, hidden

    def initHidden(self):
        return torch.zeros(1, self.hidden_size)
n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)

📢 이 네트워크의 한 단계를 실행하려면 입력(현재 문자 Tensor)과 이전의 은닉 상태(처음에는 0으로 초기화)를 전달해야 합니다. 출력(각 언어의 확률)과 다음 은닉 상태(다음 단계를 위해 유지)를 돌려받습니다.

input = letterToTensor('A')
hidden = torch.zeros(1, n_hidden)

output_l, next_hidden = rnn(input, hidden)

print(output_l)

📢 효율성을 위해서 letterToTensor 대신 lineToTensor를 사용하겠습니다.

input = nameToTensor('Albert')
hidden = torch.zeros(1, n_hidden)

output_n, next_hidden = rnn(input[0], hidden)

print(output_n)

 

✅ 학습 준비

  • Hepler function을 선언하자
  • 네트워크에는 softmax layer가 들어가 있습니다!
  • FC layer의 마지막 출력층에 softmax 함수가 적용된다면 CELoss를 쓸 수 없다! -> NLL(Negative Log Likelihood) loss를 써야 함
# Hepler function

def categoryFromOutput(output):
    top_n, top_i = output.topk(1)
    category_i = top_i[0].item() # 텐서에서 정수 값으로 변경

    return all_categories[category_i], category_i

print(categoryFromOutput(output_l))

import random

def randomChoice(l) :
    return l[random.randint(0, len(l) - 1)]

def randomTrainingExample():
    category = randomChoice(all_categories)
    name = randomChoice(category_lines[category])
    category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long)
    name_tensor = nameToTensor(name)

    return category, name, category_tensor, name_tensor

    
for _ in range(10):
    category, name, category_tensor, name_tensor = randomTrainingExample()
    print('category = ', category, '/ name = ', name)

 

✅ 모델 학습

  • nn.CELoss => nn.NLLoss + Softmax()
critierion = nn.NLLLoss()
lr = 0.005
def train(category_tensor, name_tensor):
    hidden = rnn.initHidden()

    rnn.zero_grad()

    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)

    loss = criterion(output, category_tensor)
    loss.backward()

    for p in rnn.parameters():
        p.data.add_(p.grad.data, alpha=-lr)

    return output, loss.item()
import time
import math

n_iters = 100000
print_every = 5000
plot_every = 1000

# 시각화를 위한 loss 추적
current_loss = 0
all_losses = []

def timeSince(since):
    now = time.time()
    s = now - since
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)

start = time.time()

for iter in range(1, n_iters + 1):
    category, line, category_tensor, line_tensor = randomTrainingExample()
    output, loss = train(category_tensor, line_tensor)
    current_loss += loss

    if iter % print_every == 0:
        guess, guess_i = categoryFromOutput(output)
        correct = '✓' if guess == category else '✗ (%s)' % category
        print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter / n_iters * 100, timeSince(start), loss, line, guess, correct))

    # 현재 평균 loss을 전체 loss 리스트에 추가
    if iter % plot_every == 0:
        all_losses.append(current_loss / plot_every)
        current_loss = 0

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

plt.figure()
plt.plot(all_losses)

 

✅ Confusion Matrix

  • Row가 label, Column이 prediction
  • 실제 언어가 네트워크에서 어떤 언어로 추론이 되는지를 확인하기 위한 혼란, 혼돈 행렬
confusion = torch.zeros(n_categories, n_categories)
n_conf = 10000

def evaluate(name_tensor):
    hidden = rnn.initHidden()

    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)

    return output
for i in range(n_conf):
    category, line, category_tensor, name_tensor = randomTrainingExample()
    output = evaluate(name_tensor)
    guess, guess_i = categoryFromOutput(output)
    category_i = all_categories.index(category)
    confusion[category_i][guess_i] += 1

for i in range(n_categories):
    confusion[i] = confusion[i] / confusion[i].sum()

    
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(confusion.numpy())
fig.colorbar(cax)

ax.set_xticklabels([''] + all_categories, rotation=90)
ax.set_yticklabels([''] + all_categories)

ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

plt.show()

def predict(input_line, n_predictions=3):
    print('\n> %s' % input_line)

    with torch.no_grad():
        output = evaluate(nameToTensor(input_line))

        # TopK prediciton
        topv, topi = output.topk(n_predictions, 1, True)
        predictions = list()

        for i in range(n_predictions):
            value = topv[0][i].item()
            category_index = topi[0][i].item()
            print('(%.2f) %s' % (value, all_categories[category_index]))
            predictions.append([value, all_categories[category_index]])

predict('Mo')

 

 

📍 문자-단위 RNN으로 이름 생성하기

  • 이름을 읽은 후 그 언어를 예측하는 것과의 큰 차이점은 언어를 입력하고 한 번에 한 글자를 생성하여 출력하는 것입니다. 언어 형성(단어 또는 다른 고차원 구조로도 수행될 수 있음)을 위해 문자를 반복적으로 예측하는 것을 "언어 모델" 이라고 합니다.
  • 전의 실습과 데이터 구조가 같습니다. 줄마다 이름이 적힌 텍스트 파일 data/names/[Language].txt 있습니다. 이것을 array로 분리하고, Unicode를 ASCII로 변경하고, 딕셔너리 {language: [names ...]} 을 만들어서 마무리합니다.
from google.colab import drive

import sys
import os

drive.mount('/content/drive')

from __future__ import unicode_literals, print_function, division
from io import open

import glob
import os
import unicodedata
import string

# 문자 집합 만들기

all_letters = string.ascii_letters + " .,;'-"
n_letters = len(all_letters) + 1 # 왜 1을 더해줄까? -> EOS(end of sentence)

# Help function 만드는 과정

def findFiles(path):
    return glob.glob(path)

# UNICODE -> ASCII

def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
        and c in all_letters
    )

# 파일을 읽고 줄 단위로 분리해서 이름을 Ascii로

def readLines(filename) :
    with open(filename, encoding='utf-8') as some_file:
        return [unicodeToAscii(name.strip()) for name in some_file]

# 각 언어권의 이름 목록인 딕셔너리 생성

category_names = dict()
all_categories = list()

for filename in findFiles('/content/drive/My Drive/Colab Notebooks/Deep Learning/data/names/*.txt') :
    category = os.path.splitext(os.path.basename(filename))[0]
    all_categories.append(category)
    names = readLines(filename)
    category_names[category] = names

n_c = len(all_categories)

print(n_c)

print(category_names)

import torch

# all_letters로 문자의 주소 찾기, 예시 "a" = 0

def letterToIndex(letter):
    return all_letters.find(letter)

# 검증을 위해서 한 개의 문자를 <1 x n_letters> Tensor로 변환

def letterToTensor(letter):
    tensor = torch.zeros(1, n_letters)
    tensor[0][letterToIndex(letter)] = 1
    return tensor

# 한 줄(이름)을 <line_length x 1 x n_letters>
# 또는 one-hot 문자 벡터의 array로 변경

def lineToTensor(line):
    tensor = torch.zeros(len(line), 1, n_letters)
    for li, letter in enumerate(line) :
        tensor[li][0][letterToIndex(letter)] = 1
    return tensor

 

✅ 네트워크 생성

  • 이 네트워크는 지난 실습에서 활용된 네트워크에 다른 입력들로 연결되는 category tensor를 추가 인자로 가질 수 있어야합니다.
  • category tensor는 문자 입력과 마찬가지로 one-hot 벡터입니다.
  • 기존 입력과 category tensor를 결합하여 입력으로 사용하기 때문에 입력의 사이즈가 n_categories 만큼 커집니다.
  • 여기서는 출력을 다음 문자의 확률로 해석합니다. 샘플링 할 때, 가장 확률이 높은 문자가 다음 입력 문자로 사용됩니다.
  • 성능 향상을 위해 두 번째 선형 레이어 o2o (은닉과 출력을 결합한 후) 를 추가하겠습니다.
  • Drop-out 계층이 있습니다. 이 계층은 주어진 확률(여기서는 0.1)으로 만듭니다. -> 과적합 방지

여기서 우리는 고의로 일부 혼돈을 추가하고 샘플링 다양성을 높이기 위해 네트워크의 마지막에 이것을 사용합니다.

import torch
import torch.nn as nn

class RNN(nn.Module) :

    def __init__(self, input_size, hidden_size, output_size) :
        super(RNN, self).__init__()

        self.hidden_size = hidden_size

        # input to hidden linear layer
        self.i2h = nn.Linear(n_c+ input_size + hidden_size, hidden_size)

        # input to output linear layer
        self.i2o = nn.Linear(n_c + input_size + hidden_size, output_size)

        # output to output linear layer
        self.o2o = nn.Linear(hidden_size + output_size, output_size)
        self.dropout = nn.Dropout(0.1)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, category, input, hidden) :
        combined_1 = torch.cat((category, input, hidden), 1)
        hidden = self.i2h(combined_1)
        output = self.i2o(combined_1)

        combined_2 = torch.cat((hidden, output), 1)
        x = self.o2o(combined_2)
        x = self.dropout(x)
        x = self.softmax(x)

        return x, hidden

    def initHidden(self) :
        return torch.zeros(1, self.hidden_size)

 

✅ 학습 준비

  • 마찬가지로, helper function을 하나 생각해봅시다
  • 제일 먼저 (category, line)의 무작위 쌍을 얻는 함수

 

import random

def randomChoice(l) :
    return l[random.randint(0, len(l) - 1)]

# 임의의 카테고리와, 그 카테고리에서의 이름 pair 얻기
def randomTrainingPair() :
    category = randomChoice(all_categories)
    name = randomChoice(category_names[category])
    
    return category, name

각 시점 마다 (즉, 학습 단어의 각 문자 마다) 네트워크의 입력은 (언어, 현재 문자, 은닉 상태) 가 되고, 출력은 (다음 문자, 다음 은닉 상태) 가 됩니다. 따라서 각 학습 세트 마다 언어, 입력 문자의 세트, 출력/목표 문자의 세트가 필요하겠죠.

각 시간 단계마다 현재 문자에서 다음 문자를 예측하기 때문에, 문자 쌍은 한 줄(하나의 이름)에서 연속된 문자 그룹입니다. - 예를 들어 "ABCD<EOS>" 는 ("A", "B"), ("B", "C"), ("C", "D"), ("D", "EOS") 로 생성합니다.

  • Category(언어) Tensor는 <1 x n_categories> 크기의 원-핫 벡터 입니다.
'DOYOUNG'

('D', 'O')
('O', 'Y')
('Y', 'O')
('O', 'U')
('U', 'N')
('N', 'G')
('G', 'EOS')

Input = 'D', 'O', 'Y', 'O', 'U', 'N', 'G'
Output = 'O', 'U', 'N', 'G', 'EOS'
# Category를 원-핫 인코딩 시키기 위한 함수

def categoryToTensor(category) :
    l = all_categories.index(category)
    t = torch.zeros(1, n_c)
    t[0][l] = 1

    return t

# Input을 원-핫 인코딩 시키기 위한 함수

def inputToTensor(line) :
    t = torch.zeros(len(line), 1, n_letters)

    for l in range(len(line)) :
        letter = line[l]
        t[l][0][all_letters.find(letter)] = 1

    return t

# 타겟 값을 위한 두번째 문자부터 마지막(EOS)까지의 ``LongTensor``
def targetTensor(line) :
    letter_indexes = [all_letters.find(line[l]) for l in range(1, len(line))]
    letter_indexes.append(n_letters - 1) # EOS

    return torch.LongTensor(letter_indexes)

📢 학습 동안 편의를 위해 무작위로 (category[언어], line[이름])을 가져오고, 필요한 형태 (category[언어], input[현재 문자], target[다음 문자]) Tensor로 바꾸는 randomTrainingExample 함수를 만들겠습니다!

# 임의 언어에서 category, input, target tensor를 리턴해주는 함수

def randomTrainingExample() :
    category, name = randomTrainingPair()
    category_tensor = categoryToTensor(category)
    input_tensor = inputToTensor(name)
    target_tensor = targetTensor(name)

    return category_tensor, input_tensor, target_tensor

 

✅ 네트워크 학습

criterion = nn.NLLLoss()
lr = 0.0005

def train(category_tensor, input_tensor, target_tensor) :
    target_tensor.unsqueeze_(-1)
    # torch.squeeze()
    # x = torch.rand(1, 1, 20, 200)
    # x = x.squeeze() [1, 1, 20, 200] -> [20, 200]

    # torch.unsqueeze()
    # x = torch.rand(3, 20, 200)
    # x = x.unsqueeze(dim=1) [3, 20, 200] -> [3, 1, 20, 200]
    hidden = rnn.initHidden()

    rnn.zero_grad()
    loss = torch.Tensor([0])

    for i in range(input_tensor.size(0)) :
        output, hidden = rnn(category_tensor, input_tensor[i], hidden)
        l = criterion(output, target_tensor[i])
        loss += l

    loss.backward()

    for p in rnn.parameters() :
        p.data.add_(p.grad.data, alpha =- lr)

    return output, loss.item() / input_tensor.size(0)

📢 학습에 걸리는 시간을 추적하기 위해 사람이 읽을 수 있는 문자열을 반환하는timeSince (timestamp) 함수를 추가합니다:

import time
import math

def timeSince(since) :
    now = time.time()
    s = now - since
    m = math.floor(s / 60)
    s -= m * 60

    return '%dm %ds' % (m, s)
rnn = RNN(n_letters, 128, n_letters)
print(rnn)

n_iters = 100000
print_every = 5000
plot_every = 1000

all_losses = list()
total_loss = 0

start_time = time.time()

for iter in range(1, n_iters + 1) :
    output, loss = train(*randomTrainingExample())
    total_loss += loss

    if iter % print_every == 0 :
        print('%s (%d %d%%) %.4f' % (timeSince(start_time), iter, iter / n_iters * 100, loss))

    if iter % plot_every == 0 :
        all_losses.append(total_loss / plot_every)
        total_loss = 0

plt.plot(all_losses)

 

max_length = 20

# 카테고리와 시작 문자로부터 샘플링하기

def sample(category, start_letter='A') :
    with torch.no_grad() : # 샘플링에서 히스토리를 추적할 필요 없음
        category_tensor = categoryToTensor(category)
        input = inputToTensor(start_letter)
        hidden = rnn.initHidden()

        output_name = start_letter

        for i in range(max_length) :
            output, hidden = rnn(category_tensor, input[0], hidden)
            topv, topi = output.topk(1)
            topi = topi[0][0]
            if topi == n_letters - 1 : # EOS면 그만 동작해라!
                break
            else :
                letter = all_letters[topi]
                output_name += letter
            input = inputToTensor(letter)

        return output_name

# 하나의 카테고리와 여러 시작 문자들로 여러 개의 샘플 얻기

def samples(category, start_letters='ABC') :
    for start_letter in start_letters :
        print(sample(category, start_letter))

samples('Russian', 'RUS')
samples('German', 'GER')
samples('Spanish', 'SPA')
samples('Chinese', 'CHI')

 

 

 

 

 

 

'CNN, RNN, LSTM' 카테고리의 다른 글

PC  (0) 2024.07.23
자연어 처리 - 토큰화(Tokenization)  (0) 2024.07.17
순환 신경망  (0) 2024.07.16
Pytorch  (0) 2024.07.15