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
'딥러닝 > 자연어처리' 카테고리의 다른 글
[자연어처리] 기초부터 시작하는 NLP: Sequence to Sequence 네트워크와 Attention을 이용한 번역 (0) | 2024.12.15 |
---|---|
[자연어처리] 기초부터 시작하는 NLP: 문자-단위 RNN으로 이름 분류하기 (0) | 2024.12.11 |