소스코드 원본 링크 : https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html

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('data/names/*.txt'))

import unicodedata
import string

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

# Turn a Unicode string to plain ASCII, thanks to http://stackoverflow.com/a/518232/2809427
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'))

# Build the category_lines dictionary, a list of names per language
category_lines = {}
all_categories = []

# Read a file and split into lines
def readLines(filename):
    lines = open(filename, encoding='utf-8').read().strip().split('\n')
    return [unicodeToAscii(line) for line in lines]

for filename in findFiles('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)

텍스트 데이터 선처리 코드이다. category_lines는 딕셔너리 {언어: [성1,성2 …] , 언어: [성1,성2….] … } 이런식으로 저장되고 all_categories는 각 언어의 리스트로 저장된다. os.path.basename 에 대한 내용은 이 링크를 참조한다.
http://devanix.tistory.com/298

Turning Names into Tensors

각 알파벳을 one-hot 벡터로 바꿔주는 코드이다. 그래서 한 단어를 처리할 때 각 알파벳에 대한 one-hot 벡터를 리스트에 담아준다. 아래 코드에 Jones로 예시를 들었는데 Jones는 torch.Size([5,1,57]) 을 갖게 된다. Jones 의 batch 5개 각 알파벳 1개 57개의 대소문자 + ,.;에 대한 one-hot 벡터 원소들로 이루어져 있다.

import torch

# Find letter index from all_letters, e.g. "a" = 0
def letterToIndex(letter):
    return all_letters.find(letter)

# Just for demonstration, turn a letter into a <1 x n_letters> Tensor
def letterToTensor(letter):
    tensor = torch.zeros(1, n_letters)
    tensor[0][letterToIndex(letter)] = 1
    return tensor

# Turn a line into a <line_length x 1 x n_letters>,
# or an array of one-hot letter vectors
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

print(letterToTensor('J'))

print(lineToTensor('Jones').size())

Creating The Network

RNN 알고리즘 구성 코드이다. RNN은 input과 recurrent 하는 hidden 성분의 결합으로 출력시킬 1개의 output과 다음 input을 위한 hidden 성분을 만들어낸다. combined 는 torch.cat axis=1 으로 input과 hidden을 합친 텐서다.

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

        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        combined = torch.cat((input, hidden), 1)
        hidden = self.i2h(combined)
        output = self.i2o(combined)
        output = self.softmax(output)
        return output, hidden

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

n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)

맨 처음상태에서는 input만 존재하므로 이전 은닉상태를 표현하기 위해 hidden을 0으로 초기화하고 더해준다. 아래는 처음 상태의 예제코드이다.

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

output, next_hidden = rnn(input, hidden)

매 단계마다 새로운 텐서를 만들어내는 것은 비효율적이므로 lettrToTensor 가 아니라 lineToTensor 메소드를 사용할 것이다. 출력은 각 카테고리에 대한 가능성 정도를 나타내는 리스트가 나온다. 리스트에서 가장 값이 높은것을 채택한다.

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

output, next_hidden = rnn(input[0], hidden)
print(output)

Preparing for Training

어떤 언어의 몇번째 index인지 출력해주는 코드. .topk를 통해 output 리스트에서 가장 큰 값을 출력할 수 있다.

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

랜덤 카테고리와 단어를 고르고 텐서로 바꿔주는 메소드이다.

import random

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

def randomTrainingExample():
    category = randomChoice(all_categories)
    line = randomChoice(category_lines[category])
    category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long)
    line_tensor = lineToTensor(line)
    return category, line, category_tensor, line_tensor

for i in range(10):
    category, line, category_tensor, line_tensor = randomTrainingExample()
    print('category =', category, '/ line =', line)

마지막 레이어가 LogSoftmax 이므로 손실 함수는 NLLLoss 로 선택해준다.

criterion = nn.NLLLoss()

학습의 각 루프는 다음과 같은 과정을 처리한다.

  • Create input and target tensors
  • Create a zeroed initial hidden state
  • Read each letter in and
    • Keep hidden state for next letter
  • Compare final output to target
  • Back-propagate
  • Return the output and loss

train 메소드는 rnn이 output과 hidden을 거치면서 최종적으로 얻어낸 output의 텐서와 category의 텐서의 손실을 계산해준다. 최종 output과 category의 손실을 계산한 것이 무슨 의미가 있을까? 라고 생각했었는데 다시 생각해보니 rnn은 각 단계를 거치면서 모든 알파벳에 영향을 받았고 그것이 최종 output에 반영되어있으므로 최종 output과 category_tensor를 비교하는 것이 합리적이라는 것을 깨달았다.

learning_rate = 0.005 # If you set this too high, it might explode. If too low, it might not learn

def train(category_tensor, line_tensor):
    hidden = rnn.initHidden()

    rnn.zero_grad()

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

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

    # Add parameters' gradients to their values, multiplied by learning rate
    for p in rnn.parameters():
        p.data.add_(-learning_rate, p.grad.data)

    return output, loss.item()

all_losses를 출력해보면 학습이 진행되면서 손실이 감소하고 있는 것을 확인할 수 있다.

import time
import math

n_iters = 100000
print_every = 5000
plot_every = 1000

# Keep track of losses for plotting
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

    # Print iter number, loss, name and guess
    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))

    # Add current loss avg to list of losses
    if iter % plot_every == 0:
        all_losses.append(current_loss / plot_every)
        current_loss = 0

Plotting the Results

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

plt.figure()
plt.plot(all_losses)

Evaluating the Results

네트워크가 다른 카테고리에서 얼마나 잘 작동하는지 알아보기 위해 우선 카테고리 사이즈의 정방 행렬을 만든다. zero로 초기화 시키고 evaluate()에 여러개의 샘플을 넣고 계산해서 정방 행렬에 값을 넣어준다. eval 모드는 train 모드와 비슷한 과정을 거친다는 것을 이미 수차례 확인해왔다. eval 모드에서는 역전파 처리를 하지 않는다는 점을 빼고 train 모드와 코드가 유사하다.

# Keep track of correct guesses in a confusion matrix
confusion = torch.zeros(n_categories, n_categories)
n_confusion = 10000

# Just return an output given a line
def evaluate(line_tensor):
    hidden = rnn.initHidden()

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

    return output

# Go through a bunch of examples and record which are correctly guessed
for i in range(n_confusion):
    category, line, category_tensor, line_tensor = randomTrainingExample()
    output = evaluate(line_tensor)
    guess, guess_i = categoryFromOutput(output)
    category_i = all_categories.index(category)
    confusion[category_i][guess_i] += 1

# Normalize by dividing every row by its sum
for i in range(n_categories):
    confusion[i] = confusion[i] / confusion[i].sum()

# Set up plot
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(confusion.numpy())
fig.colorbar(cax)

# Set up axes
ax.set_xticklabels([''] + all_categories, rotation=90)
ax.set_yticklabels([''] + all_categories)

# Force label at every tick
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

# sphinx_gallery_thumbnail_number = 2
plt.show()

Running on User Input

def predict(input_line, n_predictions=3):
    print('\n> %s' % input_line)
    with torch.no_grad():
        output = evaluate(lineToTensor(input_line))

        # Get top N categories
        topv, topi = output.topk(n_predictions, 1, True)
        predictions = []

        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('Dovesky')
predict('Jackson')
predict('Satoshi')