본문 바로가기
컴퓨터

[pytorch] 언어별 이름(성씨) 분류

by skyjwoo 2020. 2. 13.
728x90
반응형

https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html

 

NLP From Scratch: Classifying Names with a Character-Level RNN — PyTorch Tutorials 1.4.0 documentation

Note Click here to download the full example code NLP From Scratch: Classifying Names with a Character-Level RNN Author: Sean Robertson We will be building and training a basic character-level RNN to classify words. This tutorial, along with the following

pytorch.org

밑바닥부터 시작하는 NLP: character-level RNN을 이용한 이름 분류

 

단어 분류를 위해 기본적인 character-level RNN을 사용한 모델을 만들고 훈련할 것이다. 이 튜토리얼에서는 torchtext의 편의성을 빌리지 않고 low level에서부터 NLP의 모델링을 위한 전처리가 어떻게 이뤄지는 지 살펴볼 것이다.

character-level RNN은 단어를 문자들의 연속체로 해석한다-각 step 마다 예측값과 hidden state을 생성하고, 이전 단계의 hidden state를 다음 단계로 넘긴다. 가장 단어가 어떤 분류에 포함되는 지를 말해주는 마지막 예측값을 output으로 출력한다.

 

18개의 언어의 수 천개의 성씨를 훈련시킨 후 글자를 기준으로 해서 어떤 언어의 성씨인지 판별할 것이다.

 

$ python predict.py Hinton
(-0.47) Scottish
(-1.52) English
(-3.57) Irish

$ python predict.py Schmidhuber
(-0.19) German
(-2.48) Czech
(-2.68) Dutch

data 준비하기

현재 디렉토리에 다음 파일을 받아 저장한다. 

https://download.pytorch.org/tutorial/data.zip

data/names 디렉토리에 18개의 "[Language].text"로 이름지어진 텍스트 파일이 있을 것이다. 각 파일은 해당되는 언어의 성씨들을 한 줄마다 적어 놓았다. 대부분의 경우 로마자 표기로 돼 있다.(아직 덜 변환된 것도 있어 Unicode에서 Ascii코드로 변환시켜야 한다.)

결국엔 데이터를 {language: [names...]}형식의 딕셔너리 형태로 만들고자 한다. 범용 변수(generic variable)인 "category"와 "line"의 경우(여기선 언어와 성씨)는 후에 확장성을 위해 쓰인다.

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/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 https://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/data/names/*.txt'):
    category = os.path.splitext(os.path.basename(filename))[0]
    #basename으로 가장 아랫단의 주소를 얻음 즉, Dutch.txt
    #splitext로 확장자와 파일명 분리 -> 'Dutch', '.txt' 
    all_categories.append(category)
    lines = readLines(filename)
    category_lines[category] = lines

n_categories = len(all_categories)
print(n_categories)

for filename in findFiles('data/data/names/*.txt'):
    category = os.path.splitext(os.path.basename(filename))
    print(category)

필자는 기본 폴더에 압축을 풀고, 압축된 파일 이름으로 폴더를 생성해 파일 경로를 data/data/names/로 해 주었다. 원본은 data/names/ 식이다.

이제 category_lines를 얻었으니, 각 category들(languge)을 name list에 매핑시키자. 또한 all_categories(languge list)와 n_categories(category의 수)를 후에 참조하기 위해 계속 갖고있을 것이다.

 

print(category_lines['Italian'][:5])

이름(성씨)를 Tensor로 변환

성씨들을 모두 정리했으니, 이들을 다루기 위해 Tensor로 바꿔주어야 한다.

하나의 글자를 표현하기 위해 우리는 <1 * n_letters> 크기의 one-hot vector를 사용할 것이다. one-hot vector는 현재 letter는 1로 나머지는 0으로 채워진 vector이다. 예를 들어, "b" = <0 1 0 0 0 ...>이다.

한 단어를 만들기 위해서 위와 같은 벡터 여럿을 묶어 이차원 행렬을 만든다. <line_length x 1 x n_letters>

추가된 1차원은 PyTorch가 모든 것들을 batch 형태로 가정하기에 추가된 것이다. 여기선 batch_size를 1로 썼다.

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

network 생성하기

자동 차분(autograd)를 적용하기 전에, Torch에서 recurrent neural network를 만드는 것은 한 layer의 매개변수들을 여러 timestep에 걸쳐 복제하는 것과 관련있다. 그래프 자체에서 전체적으로 다뤄지는 hidden state들과 gradients들을 layer들이 갖고 있다. 이 말은 RNN을 일반적인 feed-forward layer처럼 다룰 수 있다는 것이다.

이 RNN module (대부분 여기서 복사한 것이다. 

 

https://pytorch.org/tutorials/beginner/former_torchies/nn_tutorial.html#example-2-recurrent-net) 은 하나의 input과 hidden state에서 작동하는, output 이후에 LogSoftmax layer를 취하는 2개의 선형 layer이다.

 

하나의 layer는 input과 이전 layer의 hidden값을 받는다. 이를 합쳐서 처리한 후 각 output과 hidden의 vector를 생성한다. output vector는 softmax를 한 번 더 거쳐 출력되고, hidden vector는 바로 다음 layer로 보내진다.

 

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)
  • 왜 softmax layer에서 인자로 dim=1? input으로 들어가는 인자의 차원 https://pytorch.org/docs/stable/nn.html
  • torch.cat? 입력값으로 주어진 tensor 여럿을 concatenate(합친다) 한다. 입력된 모든 tensor는 같은 shape을 갖고 있어야 한다. (합쳐지는 차원을 제외하고) 아니면 비어있어야 한다. ex) shape이 (2,3), (1,3), (3, 3)이고 합쳐지는 dim이 0인 경우, -> (2+1+3,3) = (6,3)이 된다. https://pytorch.org/docs/stable/torch.html

이 신경망의 한 step을 작동시키기 위해선 input(이 경우 현재 글자의 Tensor)과 이전 hidden state(처음에 0으로 초기화했던)를 넣어 줍니다. 출력값(각 언어의 확률값)과 다음 hidden state값 (다음 step에 보낼 값)

 

#예시
input = letterToTensor('A') # 'A'를 tensor로 변환
hidden =torch.zeros(1, n_hidden) # hidden state를 0으로 초기화

output, next_hidden = rnn(input, hidden) # rnn에 넘겨서 값을 반환 받음

효율성을 위해 매 step마다 새로운 Tensor를 생성하지 않는다. 따라서 lineToTensor(letterToTensor 대신)와 slice를 사용할 것이다. 이는 Tensor batch들을 사전 연산하여 최적화 될 수 있다. batch형태로 한번에 계산하기에 연산에서 이득을 볼 수 있다.

 

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

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

보다시피 output이 <1 x n_categories> 형식의 tensor로 벡터의 모든 값들이 해당 카테고리의 확률 값을 나타냄을 알 수 있다.(높을 수록 높은 확률값)

 

Training

Training 사전 준비

학습에 들어가기 전에 몇몇 helper function을 만들어야 한다. 첫번째는 신경망의 output을 해석하는 일인데, 이는 각 카테고리의 확률 값이라고 알고 있는 것이다. Tensor.topk를 써서 가장 큰 값의 index를 얻는다.

 

def categoryFromOutput(output):
    top_n, top_i = output.topk(1) #가장 큰 값의 index얻기
    category_i = top_i[0].item() #index로 카데고리에서 찾기
    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)#category_index만을 원소로 하는 tensor
    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, category_tensor)

신경망 훈련시키기

남은 건 여러 예제들을 가지고 신경망에 보여주어 추측하게하고 틀리면 교정하도록 한다.

loss function으로는 nn.NLLLoss가 적절하다. RNN의 마지막 layer는 nn.LogSoftmax이기 때문이다.

 

criterion = nn.NLLLoss() 

각 training loop는 다음을 진행할 것이다,.

  • input과 target tensor를 생성
  • 0으로 초기화된 hidden state 생성
  • 각 글자를 읽고 다음 글자에 대한 hidden state를 유지함
  • 마지막 output을 target(정답)과 비교함
  • Back-propagate
  • output과 loss를 반환한다.
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() #역전파 실행 전 gradients를 0으로 초기화

    for i in range(line_tensor.size()[0]): #성씨의 글자 수 만큼 rnn cell(layer)를 돌린다. 
        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()

이제 예제들을 돌리면 된다. train 함수가 우리가 output과 loss 반환하기에 예측값과 loss를 추적해 도표를 그릴 수 있다. 1000개 가량의 예제가 있기에 print_every에 의해 나오는 예제들만 출력할 것이고, 그 loss값의 평균을 취할 것이다.

 

import time
import math

n_iters = 100000 #학습 횟수, epoch
print_every = 5000 #5000번 마다 출력
plot_every = 1000 #이 횟수만큼 loss 모아서 평균 낸 후 리스트에 추가



# Keep track of losses for plotting
current_loss = 0
all_losses = [] #도표를 그리기 위해 loss들을 담아놓을 곳

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

 

결과를 도표로 그리기

all_losses에서 가져온 loss의 history가 신경망의 학습을 보여준다.

 

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

plt.figure()
plt.plot(all_losses)

결과 평가하기

여러 카테고리들에 대해 얼마나 신경망이 잘 작동하는지 살펴보기 위해, 혼합 행렬(행에 실제 언어들을, 열에 예측한 언어를 나타내도록 한)을 만들 것이다. 혼합 행렬을 계산하기 위해 신경망을 evaluate()(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, 주어진 성씨에 대한 output만 생성
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
# confustion matrix 생성 위한 작업, evaluate 예측값과 정답 확인
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()) #confusion matrix를 numpy로 만든 후 보여주기
fig.colorbar(cax)

# Set up axes, label 설정
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()

잘못 예측한 것들은 중심 축에서 벗어난 밝게 표시된 부분들임을 알 있을 것이다. 예를 들어, 한국어인데 중국어로 예측한 그리고 이탈리아 어인데 스페인어로 예측한 것들. 그리스어는 잘 예측하고, 영어는 예측력이 좋지 못함을 찾아볼 수 있다.(아마 다른 언어들과 겹치는 것들 때문일 것이다. 비슷한 문자 배열?)

 

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) # top3 예측 값의 값과 index
        predictions = []

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

predict('Dovesky')
predict('Jackson')
predict('Satoshi')

 

 

  • torch.topk

torch.topk(input, k, dim=None, largest=True, sorted=True, out=None) -> (Tensor, LongTensor) = (value vector, indices vector)

https://pytorch.org/docs/stable/torch.html#torch.topk

코드 최종본은 다음 링크에 올려져 있다. https://github.com/spro/practical-pytorch/tree/master/char-rnn-classification

728x90
반응형

댓글