소스코드 원본 링크 : https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html
코드의 전반적인 메커니즘을 이해하는데 큰 도움이 되는 자료:
https://medium.com/@jongdae.lim/%EA%B8%B0%EA%B3%84-%ED%95%99%EC%8A%B5-machine-learning-%EC%9D%80-%EC%A6%90%EA%B2%81%EB%8B%A4-part-5-83b7a44b797a
Sequence to Sequence 네트워크는 두가지 RNN이 작동한다. 첫번째로 인코더 네트워크는 입력 문장(시퀀스)을 벡터로 변환하고, 두번째로 디코더 네트워크는 그 벡터를 unfold 해주고 새로운 문장(시퀀스)로 변환한다.
Import packages and modules
from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
Loading Data Files
init : 임의의 언어를 초기화시키고 생성자 파라미터에 어떤 언어가 입력되었는지 체크하고 변수에 언어 이름을 저장한다. 입력 문장이 들어왔을 때 문장의 구성요소인 단어들에게 인덱스를 부여하고 몇번 중복되었는지 체크한다. SOS는 시작부분 EOS는 끝부분을 지정해주기 위해 만들었다.
addSentence : 입력 문장이 들어왔을 때 공백으로 split 하고 각 단어에 대해 addWord 함수를 실행한다.
addWord : 단어가 들어왔을 때 조건문에 따라 어떤식으로 딕셔너리를 처리할지 결정한다.
SOS_token = 0
EOS_token = 1
class Lang:
def __init__(self, name):
self.name = name
self.word2index = {}
self.word2count = {}
self.index2word = {0: "SOS", 1: "EOS"}
self.n_words = 2 # Count SOS and EOS
def addSentence(self, sentence):
for word in sentence.split(' '):
self.addWord(word)
def addWord(self, word):
if word not in self.word2index:
self.word2index[word] = self.n_words
self.word2count[word] = 1
self.index2word[self.n_words] = word
self.n_words += 1
else:
self.word2count[word] += 1
유니코드를 아스키코드로 바꾸고 정규표현식을 사용해서 normalize 하는 코드
# 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'
)
# Lowercase, trim, and remove non-letter characters
def normalizeString(s):
s = unicodeToAscii(s.lower().strip())
s = re.sub(r"([.!?])", r" \1", s)
s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
return s
print(lines[0]) –> Go. Va !
print(lines[1]) –> Run! Cours !
print(pairs[0]) –> [‘go .’, ‘va !’]
print(pairs[1]) –> [‘run !’, ‘cours !’]
def readLangs(lang1, lang2, reverse=False):
print("Reading lines...")
# Read the file and split into lines
lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8').\
read().strip().split('\n')
# Split every line into pairs and normalize
pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
# Reverse pairs, make Lang instances
if reverse:
pairs = [list(reversed(p)) for p in pairs]
input_lang = Lang(lang2)
output_lang = Lang(lang1)
else:
input_lang = Lang(lang1)
output_lang = Lang(lang2)
return input_lang, output_lang, pairs
english 문장이 eng_prefixes에 있는 각 문장들로 시작하고 영어 문장, 프랑스어 문장 각각도 공백으로 split 했을 때 길이가 10보다 작아야 한다.
MAX_LENGTH = 10
eng_prefixes = (
"i am ", "i m ",
"he is", "he s ",
"she is", "she s ",
"you are", "you re ",
"we are", "we re ",
"they are", "they re "
)
def filterPair(p):
return len(p[0].split(' ')) < MAX_LENGTH and \
len(p[1].split(' ')) < MAX_LENGTH and \
p[1].startswith(eng_prefixes)
def filterPairs(pairs):
return [pair for pair in pairs if filterPair(pair)]
def prepareData(lang1, lang2, reverse=False):
input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
print("Read %s sentence pairs" % len(pairs))
pairs = filterPairs(pairs)
print("Trimmed to %s sentence pairs" % len(pairs))
print("Counting words...")
for pair in pairs:
input_lang.addSentence(pair[0])
output_lang.addSentence(pair[1])
print("Counted words:")
print(input_lang.name, input_lang.n_words)
print(output_lang.name, output_lang.n_words)
return input_lang, output_lang, pairs
input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))
The Encoder
인코더는 입력 단어를 출력 벡터와 hidden state를 반환한다. hidden state는 다음 입력 단어에 사용한다.
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):
super(EncoderRNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
def forward(self, input, hidden):
embedded = self.embedding(input).view(1, 1, -1)
output = embedded
output, hidden = self.gru(output, hidden)
return output, hidden
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
The Decoder
디코더는 인코더의 출력 벡터를 받고 번역을 위한 단어를 생성한다. 인코더의 마지막 출력을 context 벡터라고 부른다. 이 벡터는 디코더의 초기 hidden state로 사용된다. 매 단계마다 디코더는 입력 토큰과 hidden state를 받는다. 초기 입력 토큰은 SOS 토큰이고 초기 hidden state는 context 벡터이다.
class DecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size):
super(DecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(output_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
self.out = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, input, hidden):
output = self.embedding(input).view(1, 1, -1)
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = self.softmax(self.out(output[0]))
return output, hidden
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
Attention Decoder
지금까지 context 벡터가 전체 문장을 인코딩하는 부담을 지는 것처럼 보였다. 실제로 단순한 인코딩-디코딩 시스템은 여러 문제가 발생할 수 있다고 한다. 위에 그림에 보이는 어텐션 네트워크는 디코더의 입력과 hidden을 받고 어텐션 weight들을 계산하고 인코더의 출력과 곱하여 결과를 반환한다. 디코더의 결과는 문장이다. 어텐션 weights은 디코더가 더 자연스러운 번역을 하도록 도와주는 상수값이라고 생각하면 되겠다.
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_outputs):
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, attn_weights
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)