딥러닝/자연어처리

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

johyeongseob 2024. 12. 12. 17:04
Pytorch 기초부터 시작하는 NLP 시리즈

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

 

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

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

 

Author: Sean Robertson
번역: 황성수

 

 Pytorch 기초부터 시작하는 NLP 두 번째 튜토리얼입니다. 첫 번째 튜토리얼과 유사한 점이 많습니다.  제가 재해석한 코드를 공유드립니다. 큰 틀에서 작동원리는 동일합니다.

 

Step 1: 카테고리 (나라)별 이름 목록 생성 (Tutorial1 step1-3 과 동일)

import glob
import unicodedata
import string
import os
import torch
import torch.nn as nn
import random


# Tutorial1 step1
def findFiles(path):
    return glob.glob(path)

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


# Tutorial1 step2
def unicodeToAscii(string):
    # 1. 문자열을 유니코드 정규화
    normalized = unicodedata.normalize('NFD', string)

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

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


# Tutorial1 step3
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)

 

 

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

from Tutorial2_step1 import n_letters, n_categories
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
        self.output_size = output_size

        self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
        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):
        input_combined = torch.cat((category, input, hidden), 1)
        hidden = self.i2h(input_combined)
        output = self.i2o(input_combined)
        output_combined = torch.cat((hidden, output), 1)
        output = self.o2o(output_combined)
        output = self.dropout(output)
        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)
    print(rnn)

 

실행 결과

RNN(
  (i2h): Linear(in_features=203, out_features=128, bias=True)
  (i2o): Linear(in_features=203, out_features=18, bias=True)
  (o2o): Linear(in_features=146, out_features=18, bias=True)
  (dropout): Dropout(p=0.1, inplace=False)
  (softmax): LogSoftmax(dim=1)
)

 

 

Step 3:  훈련 데이터 구성

from Tutorial2_step1 import n_letters, all_letters, all_categories, category_lines, n_categories
import random
import torch


# 임의의 category 및 그 category에서 무작위 줄(이름) 얻기
def randomTrainingPair():
    category = random.choice(all_categories)
    line = random.choice(category_lines[category])
    return category, line

# Category를 위한 One-hot 벡터
def categoryTensor(category):
    category_index = all_categories.index(category)
    tensor = torch.zeros(1, n_categories)
    tensor[0][category_index] = 1
    return tensor

# 입력을 위한 처음부터 마지막 문자(EOS 제외)까지의  One-hot 행렬
def inputTensor(line):
    tensor = torch.zeros(len(line), 1, n_letters)
    for char_idx in range(len(line)):
        letter = line[char_idx]
        tensor[char_idx][0][all_letters.find(letter)] = 1
    return tensor

# 목표를 위한 두번째 문자 부터 마지막(EOS)까지의 ``LongTensor``
def targetTensor(line):
    letter_indexes = []
    for char_idx in range(1, len(line)):
        current_char = line[char_idx]
        char_position = all_letters.find(current_char)
        letter_indexes.append(char_position)

    # EOS (End of Sequence) 토큰 추가
    letter_indexes.append(n_letters - 1)

    return torch.LongTensor(letter_indexes)


def randomTrainingExample():
    category, line = randomTrainingPair()
    category_tensor = categoryTensor(category)
    input_line_tensor = inputTensor(line)
    target_line_tensor = targetTensor(line)
    return category_tensor, input_line_tensor, target_line_tensor

if __name__ == "__main__":
    category_tensor, input_line_tensor, target_line_tensor = randomTrainingExample()
    print(f"Category_tensor: {category_tensor.size()}, Name_tensor: {input_line_tensor.size()}, "
          f"Target_tensor: {target_line_tensor}")

 

실행 결과

Category_tensor: torch.Size([1, 18]), 
Name_tensor: torch.Size([3, 1, 57]), 
Target_tensor: tensor([20,  8, 56])

 

 

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

from Tutorial2_step1 import n_letters
from Tutorial2_step2 import RNN
from Tutorial2_step3 import 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_letters)

criterion = nn.NLLLoss()
optimizer = optim.SGD(rnn.parameters(), lr=0.005)

def train(category_tensor, input_line_tensor, target_line_tensor):
    target_line_tensor.unsqueeze_(-1)
    hidden = rnn.initHidden()

    optimizer.zero_grad()
    rnn.zero_grad()

    loss = torch.Tensor([0])

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

    loss.backward()
    optimizer.step()

    return loss.item() / input_line_tensor.size(0)

if __name__ == "__main__":
    rnn.train()
    for itr in range(1, iters + 1):
        loss = train(*randomTrainingExample())

        if itr % 5000 == 0:
            print(f"Epoch: {itr}/{iters}, Loss: {loss:.4f}")

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

 

실행 결과

Epoch: 5000/100000, Loss: 2.7068
Epoch: 10000/100000, Loss: 1.8891
Epoch: 15000/100000, Loss: 2.9920
Epoch: 20000/100000, Loss: 2.0914
Epoch: 25000/100000, Loss: 2.1707
Epoch: 30000/100000, Loss: 2.8848
Epoch: 35000/100000, Loss: 2.5131
Epoch: 40000/100000, Loss: 2.4639
Epoch: 45000/100000, Loss: 2.4008
Epoch: 50000/100000, Loss: 3.0961
Epoch: 55000/100000, Loss: 2.1982
Epoch: 60000/100000, Loss: 1.7003
Epoch: 65000/100000, Loss: 0.9898
Epoch: 70000/100000, Loss: 3.1328
Epoch: 75000/100000, Loss: 2.8640
Epoch: 80000/100000, Loss: 1.7197
Epoch: 85000/100000, Loss: 2.9831
Epoch: 90000/100000, Loss: 1.5606
Epoch: 95000/100000, Loss: 1.9198
Epoch: 100000/100000, Loss: 1.3451

모델 가중치가 저장되었습니다.

Process finished with exit code 0

 

 

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

from Tutorial2_step1 import n_letters, all_letters
from Tutorial2_step2 import RNN
from Tutorial2_step3 import categoryTensor, inputTensor
import torch


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

max_length = 20

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

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

        output_name = start_letter

        for i in range(max_length):
            output, hidden = rnn(category_tensor, input[0], hidden)
            _, topi = output.topk(1)
            topi = topi[0][0]
            if topi == n_letters - 1:
                break
            else:
                letter = all_letters[topi]
                output_name += letter
            input = inputTensor(letter)

        return output_name


if __name__ == '__main__':
    # 하나의 카테고리와 여러 시작 문자들로 여러 개의 샘플 얻기
    def samples(category, start_letters='ABC'):
        for start_letter in start_letters:
            print(f"Predict name with {start_letter}: {sample(category, start_letter)}")

    if __name__ == '__main__':
        samples('Korean', 'KLP')
        # 예상 결과: 김(Kim), 이(Lee), 박(Park)

 

실행 결과

Predict name with K: Kon
Predict name with L: Lo
Predict name with P: Pon