워드 임베딩은 유사한 단어끼리 유사하게 인코딩되도록 표현하는 방법이다. 또한, 높은 차원의 임베딩일수록 단어 간의 세부적인 관계를 잘 파악할 수 있다. 따라서 단일 숫자로 변환된 넘파이 배열을 N차원으로 변경하여 사용한다. 배열은 N차원으로 변환하기 위해 먼저 모든 범주형 칼럼에 대한 임베딩 크기를 정의한다. 보통 컬럼의 고유 값 수를 2로 나누는 것을 많이 사용한다.
다음은 (모든 범주형 칼럼의 고유값 수, 차원의 크기) 형태의 배열을 출력한 결과이다.
categorical_column_sizes = [len(dataset[column].cat.categories) for column in categorical_col]
categorical_embedding_sizes = [(col_size, min(50, (col_size+1)//2)) for col_size in categorical_column_sizes]
print(categorical_embedding_sizes)
데이터셋을 훈련과 테스트 용도로 분리한다.
total_records = 1728
test_records = int(total_records * .2) # 전체 데이터 중 20%를 테스트로 사용
categorical_train = categorical_data[:total_records - test_records]
categorical_test = categorical_data[total_records - test_records:total_records]
train_outputs = outputs[:total_records - test_records]
test_outputs = outputs[total_records - test_records:total_records]
Modeling
class Model(nn.Module):
def __init__(self, embedding_size, output_size, layers, p=0.4):
super().__init__()
self.all_embeddings = nn.ModuleList([nn.Embedding(ni, nf) for ni, nf in embedding_size])
self.embedding_dropout = nn.Dropout(p)
all_layers = []
num_categorical_cols = sum((nf for ni, nf in embedding_size))
input_size = num_categorical_cols # 입력층의 크기를 찾기 위해 범주형 칼럼 개수를 input_size에 저장
for i in layers:
all_layers.append(nn.Linear(input_size, i))
all_layers.append(nn.ReLU(inplace=True))
all_layers.append(nn.BatchNorm1d(i))
all_layers.append(nn.Dropout(p))
input_size = i
all_layers.append(nn.Linear(layers[-1], output_size))
self.layers = nn.Sequential(*all_layers) # 신경망의 모든 계층이 순차적으로 실행되도록 모든 계층에 대한 목록을 nn.Sequential 클래스로 전달
def forward(self, x_categorical):
embeddings = []
for i, e in enumerate(self.all_embeddings):
embeddings.append(e(x_categorical[:, i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)
x = self.layers(x)
return x
model = Model(categorical_embedding_sizes, 4, [200, 100, 50], p=0.4)
print(model)
지난 포스팅에서 Transformer 모델에 대한 클래스를 정의했다. 이제 이를 호출해오자. 각각의 변수들은 주석을 통해 확인할 수 있다.
transformer = Transformer(
opt.src_vocab_size, # src_vocab_size, vocab 내 token의 갯수
opt.trg_vocab_size, # trg_vocab_size, vocab내 token의 갯수
src_pad_idx=opt.src_pad_idx, # src vocab의 padding token의 index
trg_pad_idx=opt.trg_pad_idx, # trg vocab의 padding token의 index
trg_emb_prj_weight_sharing=opt.proj_share_weight,
emb_src_trg_weight_sharing=opt.embs_share_weight,
d_k=opt.d_k, # multi head attention에서 사용할 key 차원 / (d_mode / n_head를 따름)
d_v=opt.d_v, # multi head attention에서 사용할 value 차원
d_model=opt.d_model, # encoder decoder에서의 정해진 입력과 출력의 크기, embedding vector의 차원과 동일
d_word_vec=opt.d_word_vec, #word ebedding 차원
d_inner=opt.d_inner_hid, # position wise layer 은닉층 크기
n_layers=opt.n_layers, # encoder,decoder stack 층 갯수
n_head=opt.n_head, # multi head 값
dropout=opt.dropout, # drop out 값
scale_emb_or_prj=opt.scale_emb_or_prj).to(device)
Attention is All you Need 논문에서는 lr scheduler를 새로 정의하고 이 함수를 통해 학습률을 통제하는 전략을 사용한다.
def cal_performance(pred, gold, trg_pad_idx, smoothing=False):
''' Apply label smoothing if needed '''
# loss 값을 구한다
loss = cal_loss(pred, gold, trg_pad_idx, smoothing=smoothing)
# 전체 단어 중에 가장 값이 높은 index 검색
pred = pred.max(1)[1]
gold = gold.contiguous().view(-1)
# 정답지에서 padding index가 아닌거 조회
non_pad_mask = gold.ne(trg_pad_idx)
# 예측단어 중 정답을 맞춘거의 갯수
n_correct = pred.eq(gold).masked_select(non_pad_mask).sum().item()
# 정답 label 의 갯수
n_word = non_pad_mask.sum().item()
return loss, n_correct, n_word
# loss, 맞춘갯수, 전체갯수
def cal_loss(pred, gold, trg_pad_idx, smoothing=False):
''' Calculate cross entropy loss, apply label smoothing if needed. '''
gold = gold.contiguous().view(-1)
if smoothing:
eps = 0.1
n_class = pred.size(1)
# gold값을 pred의 shape으로 바꿔서, one-hot encoding 적용한다.
one_hot = torch.zeros_like(pred).scatter(1, gold.view(-1, 1), 1)
# epsilon 값에 따라 smoothing 처리한다, [0,1,0,0] → [0.03, 0.9, 0.03, 0.03]
one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1)
# pred 값을 softmax를 이용하여 확률값으로 변환한다.
log_prb = F.log_softmax(pred, dim=1)
# gold에서 padding이 아닌 값의 index를 뽑는다
non_pad_mask = gold.ne(trg_pad_idx)
# 예측값과 smoothing된 정답값을 곱하여 loss를 산출한다.
loss = -(one_hot * log_prb).sum(dim=1)
# 해당 loss값에서 mask되지 않은 값을 제거하고 loss를 구한다
loss = loss.masked_select(non_pad_mask).sum() # average later
else:
# smoothing을 사용하지 않은 경우에는 cross entropy를 사용하여 loss를 구한다.
loss = F.cross_entropy(pred, gold, ignore_index=trg_pad_idx, reduction='sum')
return loss
def train_epoch(model, training_data, optimizer, opt, device, smoothing):
''' Epoch operation in training phase'''
# model.train() 학습할때 필요한 drop out, batch_normalization 등의 기능을 활성화
# model.eval()과 model.train()을 병행하므로, 모델 학습시에는 model.train() 호출해야함
model.train()
total_loss, n_word_total, n_word_correct = 0, 0, 0
desc = ' - (Training) '
for batch in tqdm(training_data, mininterval=2, desc=desc, leave=False):
# prepare data
# ① src_seq.trg_seq, trg에 대한 정답 label "gold" 생성
src_seq = patch_src(batch.src, opt.src_pad_idx).to(device)
trg_seq, gold = map(lambda x: x.to(device), patch_trg(batch.trg, opt.trg_pad_idx))
# forward
# backward 전 optimizer의 기울기를 초기화해야만 새로운 가중치 편향에 대해서 새로운 기울기를 구할 수 있습니다.
optimizer.zero_grad()
# ② model 예측값 생성
pred = model(src_seq, trg_seq) # 256 * (trg 문장길이-1), 10077(vocab)
# ③ loss값 계산
loss, n_correct, n_word = cal_performance(
pred, gold, opt.trg_pad_idx, smoothing=smoothing)
# ④ parameter update 진행
loss.backward()
optimizer.step_and_update_lr()
# note keeping
n_word_total += n_word
n_word_correct += n_correct
total_loss += loss.item()
# 평균 loss
loss_per_word = total_loss/n_word_total
# 평균 acc
accuracy = n_word_correct/n_word_total
return loss_per_word, accuracy
def train(model, training_data, validation_data, optimizer, device, opt):
''' Start training '''
# Use tensorboard to plot curves, e.g. perplexity, accuracy, learning rate
if opt.use_tb:
print("[Info] Use Tensorboard")
from torch.utils.tensorboard import SummaryWriter
tb_writer = SummaryWriter(log_dir=os.path.join(opt.output_dir, 'tensorboard'))
log_train_file = os.path.join(opt.output_dir, 'train.log')
log_valid_file = os.path.join(opt.output_dir, 'valid.log')
print('[Info] Training performance will be written to file: {} and {}'.format(
log_train_file, log_valid_file))
with open(log_train_file, 'w') as log_tf, open(log_valid_file, 'w') as log_vf:
log_tf.write('epoch,loss,ppl,accuracy\n')
log_vf.write('epoch,loss,ppl,accuracy\n')
def print_performances(header, ppl, accu, start_time, lr):
print(' - {header:12} ppl: {ppl: 8.5f}, accuracy: {accu:3.3f} %, lr: {lr:8.5f}, '\
'elapse: {elapse:3.3f} min'.format(
header=f"({header})", ppl=ppl,
accu=100*accu, elapse=(time.time()-start_time)/60, lr=lr))
#valid_accus = []
valid_losses = []
for epoch_i in range(opt.epoch):
print('[ Epoch', epoch_i, ']')
start = time.time()
train_loss, train_accu = train_epoch(
model, training_data, optimizer, opt, device, smoothing=opt.label_smoothing)
train_ppl = math.exp(min(train_loss, 100)) # train_loss : loss_per_word, PPL = exp(cross_entropy)
# Current learning rate
lr = optimizer._optimizer.param_groups[0]['lr']
print_performances('Training', train_ppl, train_accu, start, lr)
start = time.time()
valid_loss, valid_accu = eval_epoch(model, validation_data, device, opt)
valid_ppl = math.exp(min(valid_loss, 100))
print_performances('Validation', valid_ppl, valid_accu, start, lr)
valid_losses += [valid_loss]
checkpoint = {'epoch': epoch_i, 'settings': opt, 'model': model.state_dict()}
if opt.save_mode == 'all':
model_name = 'model_accu_{accu:3.3f}.chkpt'.format(accu=100*valid_accu)
torch.save(checkpoint, model_name)
elif opt.save_mode == 'best':
model_name = 'model.chkpt'
if valid_loss <= min(valid_losses):
torch.save(checkpoint, os.path.join(opt.output_dir, model_name))
print(' - [Info] The checkpoint file has been updated.')
with open(log_train_file, 'a') as log_tf, open(log_valid_file, 'a') as log_vf:
log_tf.write('{epoch},{loss: 8.5f},{ppl: 8.5f},{accu:3.3f}\n'.format(
epoch=epoch_i, loss=train_loss,
ppl=train_ppl, accu=100*train_accu))
log_vf.write('{epoch},{loss: 8.5f},{ppl: 8.5f},{accu:3.3f}\n'.format(
epoch=epoch_i, loss=valid_loss,
ppl=valid_ppl, accu=100*valid_accu))
if opt.use_tb:
tb_writer.add_scalars('ppl', {'train': train_ppl, 'val': valid_ppl}, epoch_i)
tb_writer.add_scalars('accuracy', {'train': train_accu*100, 'val': valid_accu*100}, epoch_i)
tb_writer.add_scalar('learning_rate', lr, epoch_i)
여기서 PPL(Perplexity)은 언어 모델을 평가하기 위한 지표이다. PPL은 곧 언어 모델의 분기계수인데, 분기계수란 tree자료구조에서 branch의 개수를 의미하고, 한 가지 경우를 골라야 하는 task에서 선택지의 개수를 뜻한다. 언어모델에서 분기계수는 이전 단어로 다음 단어를 예측할 때 몇개의 단어 후보를 고려하는지를 의미한다.
즉, PPL 값이 낮을수록 언어 모델이 쉽게 정답을 찾아내는 것이므로 성능이 우수하다고 평가할 수 있다.
3. 번역
번역은 크게 세가지 구조로 구성되어 있다.
Load_data
Load_model
translator
3-1. load_data
먼저 test 데이터를 불러와야 한다.
data = pickle.load(open(opt.data_pkl, 'rb'))
SRC, TRG = data['vocab']['src'], data['vocab']['trg']
# padding index와 시작, 끝 index를 가져온다.
opt.src_pad_idx = SRC.vocab.stoi[Constants.PAD_WORD]
opt.trg_pad_idx = TRG.vocab.stoi[Constants.PAD_WORD]
opt.trg_bos_idx = TRG.vocab.stoi[Constants.BOS_WORD]
opt.trg_eos_idx = TRG.vocab.stoi[Constants.EOS_WORD]
test_loader = Dataset(examples=data['test'], fields={'src': SRC, 'trg': TRG})
3-2. load_model
그 다음 학습한 모델을 불러온다.
''' Translate input text with trained model. '''
import torch
import dill as pickle
from tqdm import tqdm
def load_model(opt, device):
# load model
checkpoint = torch.load(opt.model, map_location=device)
# model의 option load
model_opt = checkpoint['settings']
# transformer model을 model option에 따라 재생성
model = Transformer(
model_opt.src_vocab_size,
model_opt.trg_vocab_size,
model_opt.src_pad_idx,
model_opt.trg_pad_idx,
trg_emb_prj_weight_sharing=model_opt.proj_share_weight,
emb_src_trg_weight_sharing=model_opt.embs_share_weight,
d_k=model_opt.d_k,
d_v=model_opt.d_v,
d_model=model_opt.d_model,
d_word_vec=model_opt.d_word_vec,
d_inner=model_opt.d_inner_hid,
n_layers=model_opt.n_layers,
n_head=model_opt.n_head,
dropout=model_opt.dropout).to(device)
# 학습된 weight를 model에 반영
model.load_state_dict(checkpoint['model'])
print('[Info] Trained model state loaded.')
return model
지난 전처리에서 vocab size는 10077이였다. 이 vocab 내 모든 단어들을 고유 벡터로 만들어서 512차원으로 embedding 시켜주기 위해 nn.Embedding을 사용한다.
import torch.nn as nn
vacab_len = 10077
import torch.nn as nn
embedding_layer = nn.Embedding(num_embeddings=vacab_len,
embedding_dim=512,
padding_idx=1)
print('vocab 내 모든 단어들을 고유 벡터로 만들어서 512차원으로 embedding 시켜줌')
print(f'LOOK UP TABLE SIZE: {embedding_layer.weight.shape}')
print(embedding_layer.weight)
transformer에서 문장 내 각 token의 위치정보를 넣어주기 위해 positional encoding을 해준다. Positional Encoding은 embedding과 차원이 동일하며, embedding vector와 더하여 위치정보를 추가하게 된다.
class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid)) # ①
def _get_sinusoid_encoding_table(self, n_position, d_hid):
'''Sinusoid position encoding table'''
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
def forward(self, x):
return x + self.pos_table[:, :x.size(1)].clone().detach() # x : torch.size([328, 36, 512])
torch.nn.Module.register_buffer는 parameter가 아니라 말 그대로 buffer를 수행하기 위한 목적으로 활용한다. 자세한 설명은 다음의 포스팅을 참고하면 된다.
3. Encoder
3-1. Multi head Attention
class ScaledDotProductAttention(nn.Module):
''' Scaled Dot-Product Attention '''
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None):
# ① attention score 계산
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
# ② attention을 계산하지 않는 위치를 매우작은값으로 치환
# - 매우작은 값은 softmax를 거칠때 0과 가까운 값이 되므로 무시한다.
if mask is not None:
attn = attn.masked_fill(mask == 0, -1e9)
# ③ soft max를 이용하여 attention weight 계산
attn = self.dropout(F.softmax(attn, dim=-1))
# ④ 해당 분포값에 v를 곱하여 attention value를 구한다
output = torch.matmul(attn, v)
return output, attn
# attention values, attention weight
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# Pass through the pre-attention projection: b x lq x (n*dv)
# Separate different heads: b x lq x n x dv
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k) # [256, len, 8, 64]
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k) # [256, len, 8, 64]
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v) # [256, len, 8, 64]
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) # [256, 8, len, 64]
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
q, attn = self.attention(q, k, v, mask=mask) # [256, 8, len, 64], [256, 8, len, 36]
# Transpose to move the head dimension back: b x lq x n x dv
# Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1) # [256, 36, 512]
q = self.dropout(self.fc(q))
q += residual
q = self.layer_norm(q)
return q, attn
Multi Head Attention은 세가지 요소를 입력으로 받는다.
query
key
value
3-2. Position wise Feed Forward
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise 512 -> 2048
self.w_2 = nn.Linear(d_hid, d_in) # position-wise 2048 -> 512
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return x
3-3. Encoder Layer
다음은 하나의 인코더 레이어를 정의한다. 인코더 레이어는 입력과 출력의 차원이 같다. 이러한 특징을 이용해 인코더 레이어를 여러 번 중첩해 사용할 수 있다.
Transformer 코드 리뷰는 세 개의 포스팅으로 나누어 진행을 할 것이고 본 포스팅은 그 첫 번째로 전처리 부분이다.
1. Prepocess
먼저 필요한 라이브러리를 임포트해주고 파라미터를 EasyDict를 통해 선언해주자.
import spacy
import torchtext.data # torchtext의 버전은 0.3.1을 사용
import torchtext.datasets
import dill as pickle
import easydict
opt = easydict.EasyDict({
"lang_src" : 'de_core_news_sm',# source 언어 / spacy 사용시
"lang_trg" : 'en_core_web_sm', # target 언어 / spacy 사용시
"save_data" : True,
"data_src" : None,
"data_trg" : None,
"max_len" :100, # 한 문장에 들어가는 최대 token 수 / 해당 갯수 이상이면 버림
"min_word_count" : 3, # vocab을 만들어 줄때 최소 갯수 : 해당 갯수 미만이면 <unk>로 vocab이 형성됨
"keep_case": True, # 대소문자 구분
"share_vocab" : "store_true", # target vocab과 source vocab을 하나로 합치는지 여부
})
1.1 Tokenizers
tokenizers는 문장을 개별 토큰으로 변환해주는 데 사용된다. 본 코드에서는 nlp를 쉽게 처리할 수 있도록 도와주는 패키지인 spacy를 이용하여 토큰화를 한다. 먼저 영어와 독일어 전처리 모듈을 설치하자.
!python -m spacy download en
!python -m spacy download de
모듈을 설치한 후 독일어와 영어를 토큰화 해주자.
src_lang_model = spacy.load('de_core_news_sm') # 독일어 토큰화
trg_lang_model = spacy.load('en_core_web_sm') # 영어 토큰화
토큰화의 예시는 다음과 같다.
# 토큰화 예시
tokenized = trg_lang_model.tokenizer('I am a student.')
for i, token in enumerate(tokenized):
print(f'index {i}: {token.text}')
index 0: I
index 1: am
index 2: a
index 3: student
index 4: .
독일어와 영어의 토큰화 함수를 정의한다.
# 독일어 문장을 토큰화하는 함수
def tokenize_src(text):
return [tok.text for tok in src_lang_model.tokenizer(text)]
# 영어 문장을 토큰화하는 함수
def tokenize_trg(text):
return [tok.text for tok in trg_lang_model.tokenizer(text)]
default 토큰(padding unknown, start, end token)을 정의하고 torchtext의 Field 라이브러리를 사용하여 데이터 처리를 준비한다.
소스(SRC) : 독일어
목표(TRG) : 영어
Field는 데이터타입과 이를 텐서로 변환할 지시사항과 함께 정의하는 것이다. Field는 텐서로 표현될 수 있는 덱스트 데이터 타입을 처리하고, 각 토큰을 숫자 인덱스로 맵핑시켜주는 단어장(vocab) 객체가 있다. 또한 토큰화 하는 함수, 전처리 등을 지정할 수 있다.
'Eine Person in einem blauen Mantel steht auf einem belebten Gehweg und betrachtet ein Gemälde einer Straßenszene .' => 'A person in a blue coat stands on a busy sidewalk and looks at a painting of a street scene' (파란 코트를 입은 사람이 붐비는 인도에 서서 거리의 풍경을 보고 있다.)
1.2 Build Vocab
field 객체의 build_vocab 메서드를 이용해 영어와 독일어 단어 사전을 생성한다. 여기서는 최소 3번 이상 등장한 단어만을 선택하는데 이때, 2번 이하로 나오는 단어는 '<unk>'token으로 변환된다.
print(TRG.vocab.stoi['abcabc']) # 없는 단어 : 0
print(TRG.vocab.stoi[TRG.pad_token]) # 패딩 : 1
print(TRG.vocab.stoi['<s>']) # <s> : 2
print(TRG.vocab.stoi['</s>']) # </s> : 3
print(TRG.vocab.stoi['python']) # 2번 이하 단어
print(TRG.vocab.stoi["world"])
0
1
2
3
0
1870
본 코드에서는 share_vocab함수를 사용한다. 이 함수는 여러 개의 tokenizer를 사용할 때만 사용하는데 여러 개일 경우 같은 단어가 복수의 토큰 값을 가지게 되기 때문에 하나로 합치는 과정이 필요하기 때문에 사용한다. 본 논문은 독어와 영어에 중복되는 단어가 있기 때문에 사용한 것 같다.
for w, _ in SRC.vocab.stoi.items():
# TODO: Also update the `freq`, although it is not likely to be used.
if w not in TRG.vocab.stoi:
TRG.vocab.stoi[w] = len(TRG.vocab.stoi)
TRG.vocab.itos = [None] * len(TRG.vocab.stoi)
for w, i in TRG.vocab.stoi.items():
TRG.vocab.itos[i] = w
SRC.vocab.stoi = TRG.vocab.stoi
SRC.vocab.itos = TRG.vocab.itos
print('[Info] Get merged vocabulary size:', len(TRG.vocab))
[Info] Get merged vocabulary size: 10079
1.3 Data save
위의 과정을 통해 생성한 option과 vocab(src, trg), train, valid, test data를 저장해야 한다.
data = {
'settings': opt,
'vocab': {'src': SRC, 'trg': TRG},
'train': train.examples,
'valid': val.examples,
'test': test.examples}
print('[Info] Dumping the processed data to pickle file', opt.save_data)
with open("m30k_deen_shr.pkl","wb") as f:
pickle.dump(data,f)
[Info] Dumping the processed data to pickle file True
다음 포스팅에서는 Attention is all you need의 메인이 되는 Transformer 아키택쳐에 대해 리뷰해보겠다.
Transformer는 Attention mechanism의 장점을 극대화한 모델이다. RNN처럼 순차적으로 데이터를 처리하는 것이 아니라 한꺼번에 시퀀스를 처리하는 것이 가장 큰 특징이다.
Transformer 구조
Transformer는 Encoding component와 Decoding component의 연결로 구성되어 있다.
여기서 encoding component와 decoding component는 encoder와 decoder의 stack들이다.
여기서 encoder는 구조가 모두 동일하지만 가중치를 공유하지는 않는다. 또한 각각의 encoder는 self-attention과 feed forward nn 두 개의 하위 계층으로 나뉜다. 반면, decoder는 encoder-decoder attention 계층 하나가 더 추가되어 있는 구조이다.
이제 자세히 transformer 구조를 알아보자.
1.2 Input Embeddings
먼저 Input Embeddings 과정이다.
input이 들어오게 되면 먼저 embedding algorithm을 이용해 각각의 단어들을 벡터화시킨다. 맨 처음 encoder만 embedding vector를 받고 그 다음 encoder부터는 바로 아래의 encoder의 ouput을 받게 된다.
Input Enbedding
1.3 Positional Encoding
두번째로 Positional Encoding 과정이다.
Transformer는 단어들이 한꺼번에 input으로 들어가기 때문에 어떤 단어가 몇번째로 들어갔는지에 대한 정보의 손실이 존재한다. 이러한 손실을 막기 위해 만들어진 것이 positional encoding 과정이다. 이번 포스팅에서는 간단하게만 다루고 추후에 독립적인 포스팅을 할 기회가 있을 것 같다.
positional encoding은 두 벡터의 거리가 멀수록 값이 커저야 한다는 개념이다.
positional encoding 시각화
positional encoding vector는 input embedding과 더해지며 encoder의 input으로 들어가게 된다.
1.3 Self-Attention
그 다음 과정으로 Multi-head attention & Residual connection & Normalization 과정이다. 그 전에 먼저 self-attention의 역할부터 알아보자.
self-attention은 연관 있는 단어들을 살펴보기 위한 역할을 하고 있다. self-attention은 다음과 같은 일련의 과정을 거치게 된다.
1. 각각의 input vector에 대해서 세 종류의 벡터를 생성한다.
Query : 다른 모든 단어에 대해 점수를 매기는 데 사용되는 현재 단어의 표현.
Key : 정보를 제공하는 단어의 집합. 즉, 단어의 label과 같은 의미.
Value : Key에 대한 실제 표현.
2. Score를 계산한다. score는 해당 단어에 대해 다른 단어들을 얼마나 집중하는지를 결정한다.
Query vector와 각각의 key vector를 곱하여 score를 산출
3. 산출된 score를 차원의 루트로 나눠준다. 이는 기울기의 안정성에 도움이 되기 때문이라고 한다.
4. softmax 함수를 통해 확률값으로 표현한다.
5. value vector들과 softmax score를 곱해준 후 모두 합하여 최종적인 output을 도출한다.
1.4 Multi-Head Attention
multi-head attention은 주어진 단어가 다른 단어를 참고할 때 하나의 경우의 수만 참고하는 것이 아닌 여러가지의 경우의 수를 참고하겠다는 아이디어이다. 즉, attention을 head의 수 많큼 쓰겠다라는 뜻이다.
1.5 Residual
residual connection은 resnet과 마찬가지로 입력의 output에 자기자신을 더해주는 방법이다.
그 이유는 위 식과 같이 미분을 하게 되면 도함수가 굉장히 작더라도 기울기가 최소한 1만큼은 흘려주게 되어 학습에 상당히 유리하다.
1.6 Masked Multi-head Attention
다음으로 decoder부분의 Masked Multi-head attention 과정이다.
decoder에서의 self attention layer는 output sequence 내에서 현재 위치의 이전 위치들에 대해서만 고려해야 한다. 이는 self-attention 계산 과정에서 softmax를 취하기 전에 현재 스텝 이후의 위치들에 대해서 masking(-inf로 치환)을 해줌으로써 가능하다.
1.7 Multi-Head attention with Encoder outputs
그 다음 과정으로 encoder의 output과 decoder사이의 attention을 하는 과정이다.
1.8 Final Linear and Softmax Layer
마지막 과정이다. Linear layer와 softmax layer를 통해 최종적으로 확률값을 구하고 argmax를 통해 해당하는 단어를 return하게 된다.
Seq2Seq model이란 일련의 항목 (단어, 문자 등)을 취하여 다른 항목의 시퀀스를 출력하는 모델을 뜻한다.
1.1 핵심 아이디어
Seq2Seq는 encoder와 decoder라는 두가지 구조로 구성되어 있다.
Seq2Seq encoder-decoder 구조
위 영상과 같이 입력된 각각의 item을 encoder에서 처리한 후 정보들을 컴파일하여 하나의 벡터로 처리한다. 이 벡터를 contextvector라고 한다. 이후, 모든 input item을 받은 후, encoder는 context를 decoder로 보내게 된다. Decoder는 cotext vector를 받아 output item들을 반환하게 된다.
1.2 RNN 기반 Encoder-Decoder
encoder-decoder 구조를 표현하기 위한 가장 고전적인 방법으로 RNN이 사용되었었다.
다음과 같이 각각의 입력이 들어올 때 마다 hidden state가 업데이트 된다. 모든 입력이 들어온 후 가장 마지막의 hidden state가 context vector가 되어 decoder로 전달되게 된다. decoder는 context vector를 이용해 output을 배출하게 된다.
2. Attention
Context vector는 가장 마지막 item에 영향을 많이 받기 때문에 긴 길이의 시퀀스에 대해 bottleneck이 있다. 때문에 이후 Attention 방법을 도입하여 input 시퀀스에 대해 item이 주목해야 하는 부분에 대해 조금 더 활용할 수 있다.
2.1 Classic Seq2Seq vs. Attention Model
위 영상과 같이 Encoder가 더이상 마지막 hidden state를 넘겨주는 것이 아니라 모든 hidden state를 decoder로 넘겨주게 된다. 그리고 hidden state들 중에 가장 영향을 주는 hidden state에 가중치를 주어 decoder의 output을 배출하게 된다.
2.2 Decoder
Attention decoder는 output을 배출하기 전 다음과 같은 extra step을 거친다.
encoder에서 배출된 hidden state를 모두 확인. (각각의 hidden state는 해당 입력 item의 정보를 가장 많이 가지고 있음)
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
model = Net()
model.to(device)
하지만 다수의 GPU를 사용한다면 다음 코드와 같이 nn.DataParallel을 이용한다.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Net()
if torch.cuda.device_count() > 1:
model = nn.DataParallel(net)
model.to(device)
nn.DataParallel을 사용할 경우 배치 크기가 알아서 각 GPU로 분배되는 방식으로 작동하기 때문에 GPU 수 만큼 배치 크기도 늘려 주어야 한다.