[파이토치 딥러닝 프로젝트] 언어 모델링을 위한 트랜스포머 모델
‣ 해당 블로그는 ⌜실전! 파이토치 딥러닝 프로젝트⌟를 공부하며, 배운것들을 토대로 정리되었음을 알려드립니다.
언어 모델링
특정 단어 시퀀스가 주어졌을 때 그 뒤를 따를 단어 또는 단어 시쿼스의 발생 확률을 알아 내는 작업.
("French is a beautiful ___" -> 'language' 나 'word' 등이 나올 확률은? ) -> 확률적 통계적 기법을 사용하여 언어를 모델링함으로써 계산 됨.
이는 어떤 단어들이 함께 등장하고, 어떤 단어들이 절대 함께 나오지 않는지를 배움으로써 텍스트 말뭉치를 관찰하고 문법을 학습하는 것에서 비롯됐다.
트랜스포머 모델 아키텍처
위 그림에서 왼쪽은 '인코드', 오른쪽은 '디코더'에 해당하고, 아키텍처가 깊어질수록 인코더와 디코더는 여러번 이어 붙일 수 있다. 위 그림에서는 두개의 인코더, 하나의 디코더로 구성되어있다. 이 인코더 - 디코더 설정은 시퀀스를 입력으로 가져와서 입력 시퀀스에 있는 단어 수만큼의 임베딩을 생성(단어 하나 당 하나의 임베딩). 이러한 임베딩은 지금까지 모델에서 만들어진 예측과 함께 디코더에 제공.
⚑ 임베딩 계층 : 시퀀스의 각 입력 단어를 숫자 벡터로 변환하는 전형적인 작업.
⚑ 위치 인코더
: 트랜스포머의 아키텍처에는 순환 계층이 없지만, 시퀀스 작업에서 순환 네트워크보다 성능이 좋다. ' 위치 인코더'을 통해 모델이 학습 순서에 대해 감을 잡을 수 있으며, '특정 패턴을 따르는' 벡터가 입력 단어 임베딩에 추가됨. 이러한 벡터는 모델에서 첫번째 단어 뒤 두번째 단어가 따라 나오는 것을 이해할 수 있게 하는 방식으로 생성
트랜스포머 모델의 다양한 요소
<Multi-head Attention>
'여럿의 말이 쇠도 녹인다' 라는 말처럼 이전에는 단 하나의 'Attention'을 통해 학습 시켰으나, 'Attention'을 병렬로 여러 개 사용해 학습을 한다는 것이다. 이렇게 함으로써 더욱 성능이 좋아짐을 볼 수 있었으며, 아래 그림은 Multi-head Attention 메커니즘을 도식화 한것이다.
그림에서 보는 것과 같이 head의 수만큼 Attention을 각각 병렬로 나누어 계산한다. 이렇게 도출된 Attention Value들은 마지막에 concatenate를 통해 하나로 합쳐주고 이는 Attention을 한번 사용할 때와 같은 크기의 결과가 도출된다.
<Self-Attention>
Self-Attention은 쉽게 말해 '같은 문장 내에서 단어들 간의 관계' 즉, 연관성을 고려하여 Attention을 계산하는 방법이다.
<Two self-Attention : Multi-head-Attention>
아래 그림은 두 개의 Self-Attention 유닛으로 구서어된 Multi-head-Attention 구조이다. Self-Attention head를 여러 개 두면 여러 개의 헤드가 시퀀스 단어의 다양한 관점에 집중하도록 도와준다. 이는 합성곱 신경망에서 여러 개의 특징 맵이 다양한 패턴을 학습하는 방법과 유사하다.
또한, 디코더 유닛의 마스킹된 Multi-head-Attention Layer은 마스킹이 추가됐다는 점을 제외하면 Multi-head-Attention Layer과 똑같은 방식으로 작동한다. 즉, 시퀀스 처리의 시간 단계 t가 주어지면 t+1에서 n(시퀀스 길이)까지의 모든 단어가 마스킹된다(숨겨짐)
훈련하는 동안 디코더에는 두 종류의 입력이 제공된다. 그 중 하나는 최종 인더코에서 쿼리와 키 벡터를 입력으로 받아(마스킹 되지 않은) Multi-head-Attention Layer으로 전달한다. 여기에서 이 쿼리와 키 벡터는 최종 인코더 출력을 행렬로 변환한 것이다. 다른 하나는 이전 시간 단계에서 만들어진 예측을 순차적 입력으로 받아 마스킹된 Multi-head-Attention Layer에 전달한다.
(아래와 같은 구조를 가지고 코드를 구현해볼 예정이다.)
추가적으로 'Attention'의 원리와 연산과정을 자세히 살펴보시기를 원하신다면, 해당 블로그 참고
<Two self-Attention : Multi-head-Attention> 구현 : Pytouch
(위키디피아의 텍스트를 사용)
import math
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import TransformerEncoder, TransformerEncoderLayer
import torchtext
from torchtext.data.utils import get_tokenizer
아래 init 메서드를 보게 되면 TransformerEncoder와 TransformerEncoderLayer 함수를 활용했기 때문에 이 기능을 직접구현하지 않아도 된다. 언어 모델링 작업의 경우 단어 시퀀스를 입력으로 받아 단일 출력만 내면 된다. 이 때문에 디코더는 인코더의 벡터 시퀀스를 단일 출력 벡터로 변환하는 선형 계층일 뿐이다. '위치 인코더' 또한, 앞서 설명한 정의를 통해 초기화된다.
class Transformer(nn.Module):
def __init__(self, num_token, num_inputs, num_heads, num_hidden, num_layers, dropout=0.3):
super(Transformer, self).__init__()
self.model_name = 'transformer'
self.mask_source = None
self.position_enc = PosEnc(num_inputs, dropout)
layers_enc = TransformerEncoderLayer(num_inputs, num_heads, num_hidden, dropout)
self.enc_transformer = TransformerEncoder(layers_enc, num_layers)
self.enc = nn.Embedding(num_token, num_inputs)
self.num_inputs = num_inputs
self.dec = nn.Linear(num_inputs, num_token)
self.init_params()
def _gen_sqr_nxt_mask(self, size):
msk = (torch.triu(torch.ones(size, size)) == 1).transpose(0, 1)
msk = msk.float().masked_fill(msk == 0, float('-inf'))
msk = msk.masked_fill(msk == 1, float(0.0))
return msk
def init_params(self):
initial_rng = 0.12
self.enc.weight.data.uniform_(-initial_rng, initial_rng)
self.dec.bias.data.zero_()
self.dec.weight.data.uniform_(-initial_rng, initial_rng)
# 입력은 위치적으로 인코딩 된 다음 인코더를 통과한 다음 디코더를 통과한다.
def forward(self, source):
if self.mask_source is None or self.mask_source.size(0) != len(source):
dvc = source.device
msk = self._gen_sqr_nxt_mask(len(source)).to(dvc)
self.mask_source = msk
source = self.enc(source) * math.sqrt(self.num_inputs)
source = self.position_enc(source)
op = self.enc_transformer(source, self.mask_source)
op = self.dec(op)
return op
class PosEnc(nn.Module):
def __init__(self, d_m, dropout=0.2, size_limit=5000):
# d_m is same as the dimension of the embeddings
super(PosEnc, self).__init__()
self.dropout = nn.Dropout(dropout)
p_enc = torch.zeros(size_limit, d_m)
pos = torch.arange(0, size_limit, dtype=torch.float).unsqueeze(1)
divider = torch.exp(torch.arange(0, d_m, 2).float() * (-math.log(10000.0) / d_m))
# divider is the list of radians, multiplied by position indices of words, and fed to the sinusoidal and cosinusoidal function
p_enc[:, 0::2] = torch.sin(pos * divider)
p_enc[:, 1::2] = torch.cos(pos * divider)
p_enc = p_enc.unsqueeze(0).transpose(0, 1)
self.register_buffer('p_enc', p_enc)
def forward(self, x):
return self.dropout(x + self.p_enc[:x.size(0), :])
# 사전 토큰화
TEXT = torchtext.data.Field(tokenize=get_tokenizer("basic_english"), lower=True, eos_token='<eos>', init_token='<sos>')
training_text, validation_text, testing_text = torchtext.datasets.WikiText2.splits(TEXT)
TEXT.build_vocab(training_text)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 훈련 및 검증에 사용될 배치 크기 정의
def gen_batches(text_dataset, batch_size):
text_dataset = TEXT.numericalize([text_dataset.examples[0].text])
# 텍스트 데이터셋을 batch_size와 동일한 크기의 부분으로 나눔
num_batches = text_dataset.size(0) // batch_size
# 배치 밖에 위치한 데이터 포인트(나머지에 해당하는 부분)를 제거
text_dataset = text_dataset.narrow(0, 0, num_batches * batch_size)
# 데이터셋을 배치에 균등하게 배포
text_dataset = text_dataset.view(batch_size, -1).t().contiguous()
return text_dataset.to(device)
training_batch_size = 32
evaluation_batch_size = 16
training_data = gen_batches(training_text, training_batch_size)
validation_data = gen_batches(validation_text, evaluation_batch_size)
testing_data = gen_batches(testing_text, evaluation_batch_size)
최대 시퀀스 길이를 정의하고 그에 따라 입력 시퀀스와 각 배치에 대한 출력 타깃을 생성하는 함수를 만듬.
max_seq_len = 64
def return_batch(src, k):
sequence_length = min(max_seq_len, len(src) - 1 - k)
sequence_data = src[k:k+sequence_length]
sequence_label = src[k+1:k+1+sequence_length].view(-1)
return sequence_data, sequence_label
트랜스포모 모델 훈련¶
모델 초매개변수를 정의, 트랜스포머 모델 인스턴스화 진행
num_tokens = len(TEXT.vocab.stoi) # vocabulary size
embedding_size = 256 # dimension of embedding layer
num_hidden_params = 256 # transformer encoder's hidden (feed forward) layer dimension
num_layers = 2 # num of transformer encoder layers within transformer encoder
num_heads = 2 # num of heads in (multi head) attention models
dropout = 0.25 # value (fraction) of dropout
loss_func = nn.CrossEntropyLoss()
lrate = 4.0 # learning rate
transformer_model = Transformer(num_tokens, embedding_size, num_heads, num_hidden_params, num_layers,
dropout).to(device)
optim_module = torch.optim.SGD(transformer_model.parameters(), lr=lrate)
sched_module = torch.optim.lr_scheduler.StepLR(optim_module, 1.0, gamma=0.88)
훈련/평가 루틴 정의
def train_model():
transformer_model.train()
loss_total = 0.
time_start = time.time()
num_tokens = len(TEXT.vocab.stoi)
for b, i in enumerate(range(0, training_data.size(0) - 1, max_seq_len)):
train_data_batch, train_label_batch = return_batch(training_data, i)
optim_module.zero_grad()
op = transformer_model(train_data_batch)
loss_curr = loss_func(op.view(-1, num_tokens), train_label_batch)
loss_curr.backward()
torch.nn.utils.clip_grad_norm_(transformer_model.parameters(), 0.6)
optim_module.step()
loss_total += loss_curr.item()
interval = 100
if b % interval == 0 and b > 0:
loss_interval = loss_total / interval
time_delta = time.time() - time_start
print(f"epoch {ep}, {b}/{len(training_data)//max_seq_len} batches, training loss {loss_interval:.2f}, training perplexity {math.exp(loss_interval):.2f}")
loss_total = 0
time_start = time.time()
def eval_model(eval_model_obj, eval_data_source):
eval_model_obj.eval()
loss_total = 0.
num_tokens = len(TEXT.vocab.stoi)
with torch.no_grad():
for j in range(0, eval_data_source.size(0) - 1, max_seq_len):
eval_data, eval_label = return_batch(eval_data_source, j)
op = eval_model_obj(eval_data)
op_flat = op.view(-1, num_tokens)
loss_total += len(eval_data) * loss_func(op_flat, eval_label).item()
return loss_total / (len(eval_data_source) - 1)
# 모델 훈련 루프 실행
min_validation_loss = float("inf")
eps = 5
best_model_so_far = None
for ep in range(1, eps + 1):
ep_time_start = time.time()
train_model()
validation_loss = eval_model(transformer_model, validation_data)
print()
print(f"epoch {ep:}, validation loss {validation_loss:.2f}, validation perplexity {math.exp(validation_loss):.2f}")
print()
if validation_loss < min_validation_loss:
min_validation_loss = validation_loss
best_model_so_far = transformer_model
sched_module.step()
epoch 1, 100/1018 batches, training loss 8.50, training perplexity 4901.66 epoch 1, 200/1018 batches, training loss 7.16, training perplexity 1286.24 epoch 1, 300/1018 batches, training loss 6.76, training perplexity 865.43 epoch 1, 400/1018 batches, training loss 6.55, training perplexity 702.21 epoch 1, 500/1018 batches, training loss 6.45, training perplexity 631.90 epoch 1, 600/1018 batches, training loss 6.31, training perplexity 548.01 epoch 1, 700/1018 batches, training loss 6.25, training perplexity 516.28 epoch 1, 800/1018 batches, training loss 6.11, training perplexity 450.42 epoch 1, 900/1018 batches, training loss 6.09, training perplexity 441.72 epoch 1, 1000/1018 batches, training loss 6.08, training perplexity 436.78 epoch 1, validation loss 5.82, validation perplexity 336.19 epoch 2, 100/1018 batches, training loss 5.98, training perplexity 394.64 epoch 2, 200/1018 batches, training loss 5.90, training perplexity 364.08 epoch 2, 300/1018 batches, training loss 5.82, training perplexity 337.72 epoch 2, 400/1018 batches, training loss 5.78, training perplexity 324.68 epoch 2, 500/1018 batches, training loss 5.82, training perplexity 335.50 epoch 2, 600/1018 batches, training loss 5.77, training perplexity 319.43 epoch 2, 700/1018 batches, training loss 5.78, training perplexity 322.60 epoch 2, 800/1018 batches, training loss 5.65, training perplexity 283.28 epoch 2, 900/1018 batches, training loss 5.67, training perplexity 291.07 epoch 2, 1000/1018 batches, training loss 5.71, training perplexity 300.54 epoch 2, validation loss 5.53, validation perplexity 251.09 epoch 3, 100/1018 batches, training loss 5.67, training perplexity 288.79 epoch 3, 200/1018 batches, training loss 5.59, training perplexity 268.81 epoch 3, 300/1018 batches, training loss 5.55, training perplexity 257.23 epoch 3, 400/1018 batches, training loss 5.52, training perplexity 249.65 epoch 3, 500/1018 batches, training loss 5.55, training perplexity 257.02 epoch 3, 600/1018 batches, training loss 5.52, training perplexity 249.50 epoch 3, 700/1018 batches, training loss 5.53, training perplexity 252.90 epoch 3, 800/1018 batches, training loss 5.39, training perplexity 219.61 epoch 3, 900/1018 batches, training loss 5.44, training perplexity 230.41 epoch 3, 1000/1018 batches, training loss 5.49, training perplexity 241.15 epoch 3, validation loss 5.37, validation perplexity 215.04 epoch 4, 100/1018 batches, training loss 5.46, training perplexity 235.42 epoch 4, 200/1018 batches, training loss 5.40, training perplexity 220.81 epoch 4, 300/1018 batches, training loss 5.36, training perplexity 213.61 epoch 4, 400/1018 batches, training loss 5.34, training perplexity 208.66 epoch 4, 500/1018 batches, training loss 5.37, training perplexity 213.88 epoch 4, 600/1018 batches, training loss 5.35, training perplexity 210.60 epoch 4, 700/1018 batches, training loss 5.36, training perplexity 213.75 epoch 4, 800/1018 batches, training loss 5.21, training perplexity 183.18 epoch 4, 900/1018 batches, training loss 5.26, training perplexity 193.41 epoch 4, 1000/1018 batches, training loss 5.32, training perplexity 205.22 epoch 4, validation loss 5.31, validation perplexity 202.42 epoch 5, 100/1018 batches, training loss 5.31, training perplexity 202.77 epoch 5, 200/1018 batches, training loss 5.25, training perplexity 189.64 epoch 5, 300/1018 batches, training loss 5.22, training perplexity 184.80 epoch 5, 400/1018 batches, training loss 5.20, training perplexity 181.18 epoch 5, 500/1018 batches, training loss 5.22, training perplexity 185.54 epoch 5, 600/1018 batches, training loss 5.21, training perplexity 182.95 epoch 5, 700/1018 batches, training loss 5.22, training perplexity 185.69 epoch 5, 800/1018 batches, training loss 5.07, training perplexity 158.79 epoch 5, 900/1018 batches, training loss 5.13, training perplexity 169.36 epoch 5, 1000/1018 batches, training loss 5.19, training perplexity 179.63 epoch 5, validation loss 5.23, validation perplexity 186.53
Cross-entropy 손실 외에 혼란도(perplexity)도 보여진다. 혼란도는 자연어 처리에서 확률 분포(언어모델) 가 샘플에 얼마나 잘 맞는지 또는 잘 예측하는지를 나타내기 위해 널리 사용되는 지표이다.
testing_loss = eval_model(best_model_so_far, testing_data)
print(f"testing loss {testing_loss:.2f}, testing perplexity {math.exp(testing_loss):.2f}")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[2], line 1 ----> 1 testing_loss = eval_model(best_model_so_far, testing_data) 2 print(f"testing loss {testing_loss:.2f}, testing perplexity {math.exp(testing_loss):.2f}") NameError: name 'eval_model' is not defined
[Reference]
https://www.blossominkyung.com/deeplearning/transformer-mha
https://codingopera.tistory.com/44