딥러닝/자연어처리

[자연어처리] 기초부터 시작하는 NLP: 문자-단위 RNN으로 이름 분류하기

johyeongseob 2024. 12. 11. 16:32
Pytorch 기초부터 시작하는 NLP 시리즈

1. 문자-단위 RNN으로 이름 분류하기
2. 문자-단위 RNN으로 이름 생성하기
3. Sequence to Sequence 네트워크와 Attention을 이용한 번역

Pytorch 기초부터 시작하는 NLP: 문자-단위 RNN으로 이름 분류하기

페이지: https://tutorials.pytorch.kr/intermediate/char_rnn_classification_tutorial.html

 

Author: Sean Robertson
번역: 황성수, 김제필

 

 Pytorch 기초부터 시작하는 NLP 첫 번째 튜토리얼입니다. NLP의 기본적인 작동 원리를 배울 수 있는 좋은 예시입니다. 다만 해당 튜토리얼의 코드가 이해하기 어렵습니다. 제가 재해석한 코드를 공유드립니다. 큰 틀에서 작동원리는 동일합니다.

 

 

Step 1: 파일 경로에서 파일 목록을 반환하는 함수

import glob  # 파일 경로 패턴 매칭을 위한 모듈


# 파일 경로에서 지정된 패턴에 맞는 파일 목록을 반환하는 함수
def findFiles(path):
    return glob.glob(path)  # 주어진 경로 패턴에 일치하는 모든 파일 경로를 리스트로 반환

if __name__ == "__main__":
    # 'data/names/' 디렉토리에서 확장자가 '.txt'인 모든 파일을 찾고 출력
    print(findFiles('data/names/*.txt'))

 

실행 결과

[
'data/names\\Arabic.txt', 'data/names\\Chinese.txt', 'data/names\\Czech.txt', 'data/names\\Dutch.txt', 
'data/names\\English.txt', 'data/names\\French.txt', 'data/names\\German.txt', 'data/names\\Greek.txt', 
'data/names\\Irish.txt', 'data/names\\Italian.txt', 'data/names\\Japanese.txt', 'data/names\\Korean.txt', 
'data/names\\Polish.txt', 'data/names\\Portuguese.txt', 'data/names\\Russian.txt', 'data/names\\Scottish.txt', 
'data/names\\Spanish.txt', 'data/names\\Vietnamese.txt'
]

 

 

Step 2: 유니코드 문자열을 ASCII로 변환

import unicodedata
import string


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

# 유니코드 문자열을 ASCII로 변환, https://stackoverflow.com/a/518232/2809427
def unicodeToAscii(string):
    # 1. 문자열을 유니코드 정규화
    normalized = unicodedata.normalize('NFD', string)

    # 2. 결과를 담을 리스트 초기화
    ascii_characters = []

    # 3. 각 문자에 대해 조건 확인
    for character in normalized:
        # 만약 문자가 허용된 범위(all_letters)에 있고, 발음 구별 기호(Mn)가 아니면 추가
        if character in all_letters and unicodedata.category(character) != 'Mn':
            ascii_characters.append(character)

    # 4. 리스트를 문자열로 변환 후 반환
    return ''.join(ascii_characters)


# 테스트
if __name__ == "__main__":
    print(f"\nall_letters: {all_letters}")
    print(f"n_letters: {n_letters}")
    print(f"unicodeToAscii('Ślusàrski'): {unicodeToAscii('Ślusàrski')}")

 

실행 결과

all_letters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'
n_letters: 57
unicodeToAscii('Ślusàrski'): Slusarski

 

 

Step 3: 카테고리 (나라)별 이름 목록 생성

from Tutorial1_step1 import findFiles
from Tutorial1_step2 import unicodeToAscii
import os


category_lines = {}
all_categories = []

# 파일을 읽고 줄 단위로 분리하는 함수
def readLines(filename):
    # 파일 내용을 읽어서 줄 단위로 나눔
    lines = open(filename, encoding='utf-8').read().strip().split('\n')
    # 각 줄을 유니코드에서 ASCII로 변환한 결과를 반환
    return [unicodeToAscii(line) for line in lines]

# 'data/names/' 디렉토리에서 모든 '.txt' 파일을 찾음
for filename in findFiles('../data/names/*.txt'):
    # 파일명에서 카테고리(언어) 이름 추출 (예: 'English.txt' → 'English')
    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)


if __name__ == "__main__":
    # 각 카테고리의 이름 개수와 첫 번째 이름을 출력
    total = 0
    for category in all_categories:
        print(f"category: {category}, num of names: {len(category_lines[category])}, first name: {category_lines[category][0]}")
        total += len(category_lines[category])
    print(f"\nTotal num of name: {total}")

    print(f"\nFive Italian names: {category_lines['Italian'][:5]}")

 

실행 결과

category: Arabic, num of names: 2000, first name: Khoury
category: Chinese, num of names: 268, first name: Ang
category: Czech, num of names: 519, first name: Abl
......

Total num of name: 20074

Five Italian names: ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']

 

 

Step 4: 이름을 Tensor로 변경

import torch
from Tutorial1_step2 import all_letters, n_letters


# 문자열에서 특정 부분 문자열(sub)의 첫 번째 등장 위치(인덱스)를 반환
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


if __name__ == "__main__":
    print(f"\nletterToIndex('J'): {letterToIndex('J')}\n")
    print(f"letterToTensor('J'): {letterToTensor('J')}\n")
    print(f"lineToTensor('Jones').size(): {lineToTensor('Jones').size()}")

 

실행 결과

letterToIndex('J'): 35

letterToTensor('J'): tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
                              0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.,
                              0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
                              0., 0., 0.]])

lineToTensor('Jones').size(): torch.Size([5, 1, 57])

 

 

Step 5: 순환신경망 (RNN) 구현

import torch
import torch.nn as nn
from Tutorial1_step2 import n_letters
from Tutorial1_step3 import n_categories
from Tutorial1_step4 import letterToTensor, lineToTensor

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

        self.hidden_size = hidden_size
        self.output_size = output_size

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

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

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

    def initoutput(self):
        return torch.zeros(1, self.output_size)

if __name__ == "__main__":
    n_hidden = 128
    rnn = RNN(n_letters, n_hidden, n_categories)

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

    output, next_hidden = rnn(input, hidden)

    print(f"input: {input.size()}")
    print(f"output: {output.size()}, next_hidden: {next_hidden.size()}\n")

    # Example 2
    input = lineToTensor('Albert')
    hidden = rnn.initHidden()
    output = rnn.initoutput()

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

    print(f"input: {input.size()}, output: {output.size()}, hidden: {hidden.size()}")

 

실행 결과

input: torch.Size([1, 57])
output: torch.Size([1, 18]), next_hidden: torch.Size([1, 128])

input: torch.Size([6, 1, 57]), output: torch.Size([1, 18]), hidden: torch.Size([1, 128])

 

 

Step 6: 순환신경망 (RNN) 예시 및 훈련 데이터 구성

from Tutorial1_step2 import n_letters
from Tutorial1_step3 import all_categories, n_categories, category_lines
from Tutorial1_step4 import lineToTensor
from Tutorial1_step5 import RNN
import torch
import random


# RNN 예시 및 훈련 데이터 구성
def categoryFromOutput(output):
    _, top_i = output.topk(1)
    category_i = top_i[0].item()
    return all_categories[category_i]

def randomTrainingExample():
    category = random.choice(all_categories)
    line = random.choice(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

if __name__ == "__main__":

    # Example 1
    input = lineToTensor('Albert')
    n_hidden = 128

    rnn = RNN(n_letters, n_hidden, n_categories)
    hidden = rnn.initHidden()
    output = rnn.initoutput()

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

    print(f"Albert's predict category: {categoryFromOutput(output)}\n")

    # Example 2
    print(f"10 random Training samples\n")
    for i in range(1, 11):
        category, line, category_tensor, line_tensor = randomTrainingExample()
        print(f"Name: {line}, Category: {category}")

 

실행 결과

Albert's predict category: Chinese

10 random Training samples

Name: Kuai, Category: Chinese
Name: Gerard, Category: English
Name: Schroeder, Category: German
......

 

 

Step 7: 순환신경망 (RNN) 훈련

from Tutorial1_step2 import n_letters
from Tutorial1_step3 import n_categories
from Tutorial1_step5 import RNN
from Tutorial1_step6 import categoryFromOutput, randomTrainingExample
import torch
import torch.nn as nn
import torch.optim as optim


iters = 100000

n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
criterion = nn.NLLLoss()
optimizer = optim.SGD(rnn.parameters(), lr=0.005)

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

    optimizer.zero_grad()
    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()
    optimizer.step()

    return output, loss.item()


if __name__ == "__main__":
    rnn.train()
    for itr in range(1, iters + 1):
        category, line, category_tensor, line_tensor = randomTrainingExample()
        output, loss = train(category_tensor, line_tensor)

        # ``iter`` 숫자, 손실, 이름, 추측 화면 출력
        if itr % 5000 == 0:
            guess = categoryFromOutput(output)
            correct = '✓' if guess == category else '✗ (%s)' % category
            print(f"Epoch: {itr}/{iters}, Loss: {loss:.4f}, Name: {line} / Guess: {guess} {correct}")

        if itr == iters:
            torch.save(rnn.state_dict(), '../weight/rnn_model_weight1.pth')
            print("\n모델 가중치가 저장되었습니다.")

 

실행 결과

Epoch: 5000/100000, Loss: 1.2412, Name: Youn / Guess: Korean ✓
Epoch: 10000/100000, Loss: 2.2959, Name: Issa / Guess: Japanese ✗ (Arabic)
Epoch: 15000/100000, Loss: 2.9479, Name: Hisamatsu / Guess: Greek ✗ (Japanese)
......
Epoch: 95000/100000, Loss: 1.8370, Name: Schmeling / Guess: Czech ✗ (German)
Epoch: 100000/100000, Loss: 0.3187, Name: Etxeberria / Guess: Spanish ✓
모델 가중치가 저장되었습니다.

 

Step 8: 순환신경망 (RNN) 평가

from Tutorial1_step2 import n_letters
from Tutorial1_step3 import all_categories, n_categories
from Tutorial1_step4 import lineToTensor
from Tutorial1_step5 import RNN
import torch


n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
rnn.load_state_dict(torch.load('../weight/rnn_model_weights.pth'))

# 주어진 라인의 출력 반환
def evaluate(line_tensor):
    hidden = rnn.initHidden()
    output = rnn.initoutput()

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

    return output

def predict(input_line, n_predictions=3):
    with torch.no_grad():
        output = evaluate(lineToTensor(input_line))

        # Get top N categories
        _, topi = output.topk(n_predictions, 1, True)

        predictions = []
        for i in range(n_predictions):
            category_index = topi[0][i].item()
            print(f"{input_line}'s {i+1}th predict category is {all_categories[category_index]}")

predict('Dovesky')
print("")
predict('Jackson')
print("")
predict('Satoshi')

 

실행 결과

Dovesky's 1th predict category is Russian
Dovesky's 2th predict category is Czech
Dovesky's 3th predict category is English

Jackson's 1th predict category is Scottish
Jackson's 2th predict category is English
Jackson's 3th predict category is Russian

Satoshi's 1th predict category is Japanese
Satoshi's 2th predict category is Arabic
Satoshi's 3th predict category is Italian