Desde seu lançamento no paper “Consideration Is All You Want” em 2017, o modelo de Transformers vem causando uma reviravolta na área de Aprendizado de Máquina. Mas o que é, e como funciona o algoritmo por trás de modelos de linguagem como o ChatGPT, o Bard e tantos outros? Neste artigo, introduziremos essa arquitetura de rede neural por meio de uma implementação em Python da versão proposta no paper authentic. Depois, treinaremos nossa rede nas obras completas de Shakespeare!
Busquei deixar o texto o mais didático possível. Ainda assim, será mais fácil seguir o tutorial caso já saiba programar em Python e tenha um conhecimento básico de machine studying e redes neurais.
O Transformer é uma arquitetura de rede neural synthetic criada em 2017 por cientistas da Google Mind com a finalidade de superar limitações existentes nos modelos de processamento de sequências mais potentes da época, as Redes Neurais Recorrentes (RNNs). Apesar de algumas RNNs obterem desempenhos excelentes em tarefas como tradução e geração de texto, elas eram limitadas pelo fato de que processavam sequências de modo (pasmem) sequencial. Se a sequência de dados fosse muito grande, o treino das redes se tornava custoso, levando a um forte estímulo a desenvolver técnicas que fossem paralelizáveis.
O “pulo do gato” veio com o chamado mecanismo de atenção, o grande diferencial das redes transformers. Com ele, foi possível não apenas tornar o processamento de sequências uma tarefa paralelizável, mas também obter ganhos de efficiency sobre as RNNs. Vamos abordar como funciona esse mecanismo, mas antes, que tal criar nosso arquivo Python e importar as bibliotecas necessárias? Em um diretório para nosso projeto, criarei um arquivo modelo.py que conterá as lessons que formarão nosso modelo Transformer e um arquivo config.py que conterá uma classe usada para guardar os hiperparâmetros do nosso modelo.
Em config.py:
class Configs:def __init__(self,
N_HEADS: int = 8,
D_MODEL: int = 512,
BATCH_SIZE: int = 64,
SEQ_LEN: int = 100,
D_HIDDEN: int = 2048,
DROPOUT: int = 0.1,
N_LAYERS: int = 3):
self.n_heads = N_HEADS
self.d_model = D_MODEL
self.d_k = int(D_MODEL/N_HEADS)
self.batch = BATCH_SIZE
self.seq_len = SEQ_LEN
self.d_hid = D_HIDDEN
self.dropout = DROPOUT
self.n_layers = N_LAYERS
Em modelo.py:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.useful as F
from config import Configs
Agora passaremos a estudar as entranhas dos Transformers! No decorrer da exposição, think about que estamos tentando criar um modelo que consiga gerar texto no estilo de Shakespeare com base em um enter, como faz o ChatGPT. Algo como:
Humano: Ser ou não ser, eis a questão…
Shakespeare, o Transformer: será mais nobre, em nosso espírito sofrer pedras e setas com que a Fortuna, enfurecida, nos alveja, ou insurgir-nos contra um mar de provocações e em luta pôr-lhes fim?
Primeiro, precisamos ensiná-lo como funciona o Português (ou qualquer outra língua) e qual o estilo de escrita do Bardo de Avon. Para isso, precisamos de MUITOS dados textuais, de modo que o transformer consiga entender qual a relação entre as palavras no texto, criando o que chamamos de um modelo de linguagem. Mas, tem um problema: o computador não entende texto, apenas números. Então primeiro precisamos encontrar uma forma de converter as palavras presentes nos nossos dados em números (mais corretamente, vetores de números).
1) Camada de Embedding
É aí que entra a primeira camada do transformer: a camada de embedding. Dada uma frase, ela pega cada palavra e a converte para um vetor de números, permitindo que o computador possa lidar com esses dados. Mais do que isso, a camada de embedding faz a conversão de modo que o vetor de números contenha informações sobre o significado da palavra. Lembrem das disciplinas de física que podemos imaginar vetores como setas apontando para algum lugar no espaço. A camada de embedding aprende a criar vetores tais que palavras semelhantes tenham vetores que apontam mais ou menos para a mesma direção no espaço, e palavras de sentido oposto estejam apontando em direções contrárias.
Como podemos implementar isso no Python? Usando PyTorch é muito simples, basta usar o método abaixo.
# 1) camada de embedding
embedding = nn.Embedding(vocab_size, d_model)
Aqui, vocab_size é a quantidade de palavras únicas nos nossos dados que precisarão de representação em forma de vetor e d_model é o tamanho desses vetores (por default materemos D_MODEL = 512, como mostra a classe Config).
Terminamos a primeira camada? Ainda não! Há um detalhe elementary. Lembrem que dissemos que, diferente das RNNs, os Transformers não processam sequências de modo sequencial. Eles olham e operam sobre todas as palavras de uma frase ao mesmo tempo. Mas, como sabemos, para entender uma frase a ordem das palavras é muito importante! Por isso, nos Transformers, é necessário incluir mais uma informação nos vetores de embedding: qual a posição da palavra na frase. Há diversos modos de fazermos isso, mas seguiremos o método do paper authentic. Nele, para cada palavra da frase, é gerado outro vetor do mesmo tamanho que o vetor de embedding contendo informações sobre a posição da palavra na frase, que depois será somado ao vetor de embeddings.
Ou seja, o vetor de posição é gerado tal que seus índices pares tenham valores que sejam o seno de uma fração dependente da posição da palavra e do índice do valor, e os índices ímpares sejam o cosseno da mesma fração. Em Python, podemos criar a seguinte classe para criar esses vetores de posição.
class PositionalEncoding(nn.Module):def __init__(self, config:Configs):
tremendous(PositionalEncoding, self).__init__()
# onde d_model é o tamanho do vetor de embedding
# e seq_len é a quantidade máxima de palavras em
# uma frase
self.d_model = config.d_model
self.seq_len = config.seq_len
def __op_pos_enc(self, dim, pos):
# esta é a fração no inside de cada seno e cosseno
return pos/(1e4**((dim)/self.d_model))
def _pos_encoding_vec(self, pos):
# Cria o vetor de posição para uma dada palavra
pos_vec = np.zeros(self.d_model)
for i in vary(int(self.d_model)):
if i % 2 == 0:
pos_vec[i] = np.sin(self.__op_pos_enc(i, pos))
else:
pos_vec[i] = np.cos(self.__op_pos_enc(i-1, pos))
return pos_vec
def getPositionalEncoding(self):
# cria uma matriz com o vetor de posição de cada
# palavra em uma frase
pos_encodings = np.zeros((self.seq_len, self.d_model))
for pos in vary(self.seq_len):
for i in vary(self.d_model):
if i % 2 == 0:
worth = np.sin(pos/(1e4**(i/self.d_model)))
else:
worth = np.cos(pos/(1e4**((i - 1)/self.d_model)))
pos_encodings[pos, i] = worth
return torch.from_numpy(pos_encodings).sort(torch.float32).to(machine)
def ahead(self, embeddings):
# cria a matriz de vetores de posição e
# a soma com os embeddings das palavras
# da frase
pos_enc = self.getPositionalEncoding()
return embeddings + pos_enc
É interessante visualizar como fica a matriz de vetores de posição para uma frase arbitrária.
Essa matriz de vetores de posição será adicionada à matriz de embedding das palavras da frase.
2) Atenção! Atenção! Atenção!
Agora que temos a representação vetorial das palavras da nossa frase, além de informações sobre sua posição, passamos para a tarefa mais importante: ensinar para nosso modelo o significado contextual das palavras. Sabemos que o contexto influencia fortemente o significado de uma palavra. Por exemplo, nas frases “Fui ao banco sacar dinheiro” e “Sentei no banco para descansar” a mesma palavra “banco” aparece com dois significados completamente diferentes. Queremos que nosso modelo seja capaz de captar essas sutilezas.
Há ainda outras informações contextuais importantes, como mostra a seguinte frase de Romeu e Julieta.
“Minha generosidade é tão ilimitada quanto o mar, Meu amor é tão profundo; quanto mais te dou, mais tenho, pois ambos são infinitos.”
Queremos que o modelo entenda que no contexto dessa frase, a palavra ilimitada tem relação com generosidade e mar.
Isso é feito pelo famoso mecanismo de atenção. Por meio dele, modificamos as representações vetoriais das palavras, de modo que elas incluam seus significados contextuais. Essa parte é a mais difícil de entender, então não se desesperem caso não fique claro da primeira vez que lerem!
Think about que estamos em uma sala com todas as palavras da nossa frase. Cada palavra pergunta em voz alta — “Ei, quais de vocês tem a ver comigo?”. Em seguida, as palavras relacionadas a ela respondem — “Eu!”. A palavra que fez a pergunta olha para as que responderam e atualiza seu significado com base nelas. Na prática, essas “perguntas”, “respostas” e “atualizações de significado” são feitas por meio de operações entre vetores. No mecanismo de atenção, cada palavra da frase ganha mais três representações em vetores: o question, o key e o worth. O question é o vetor que permite a cada palavra fazer a pergunta de quais outras estão relacionadas a ela. O key é o vetor que permite às palavras responderem se elas são relacionadas a alguma outra. O worth é o vetor que será usado para atualizar os embeddings com os novos significados contextuais aprendidos. Quanto mais relacionadas duas palavras são, maior será o produto interno entre os vetores question e key dessas palavras. Depois que fizermos o produto entre os vetores question e key de todas as palavras da frase, teremos uma matriz onde cada elemento indica o quão relacionadas duas palavras da frase estão, o chamado padrão de atenção.
Com base nos valores do padrão de atenção, alteramos o vetor de embedding da palavra usando o vetor worth. Para isso, antes aplicamos a função softmax sobre as linhas do nosso padrão de atenção, convertendo cada uma em uma distribuição de probabilidade. A operação inteira pode ser resumida como se segue.
Implementando em Python temos:
def consideration(Q, Ok, V, d_k, masks=True):den = np.sqrt(d_k)
# calcula o padrão de atenção
dot = torch.matmul(Q, Ok.transpose(-1, -2)) / den
# aplica a mascara causal
if masks:
dot = masking(dot)
# aplica softmax e multiplica o padrão de atencao
# pelos vetores worth
att_pat = F.softmax(dot, dim = -1)
att = torch.matmul(att_pat, V)
return att, att_pat
Como veem pelo código acima, há mais um detalhe que ainda não expliquei: a máscara causal! Apesar de parecer o nome de um ítem de RPG, esse componente é bem menos mágico. Como dissemos, queremos um modelo que produza texto. O modelo gerará texto de modo muito semelhante ao que faz o algoritmo de sugestão de palavras do seu celular quando está escrevendo alguma mensagem — ele olhará para o que você já escreveu e sugerirá a palavra mais provável de seguir a última que você escreveu. O seu celular não consegue ler a sua mente (ainda), então o melhor que ele pode fazer é olhar para o que você já escreveu. De modo semelhante, nosso modelo, ao gerar texto, deverá prever a próxima palavra mais provável de aparecer. Se ele conseguisse ver qual é essa próxima palavra, ele estaria trapasseando, e não faria sentido ele tentar prever a próxima palavra, já que ele já sabe qual ela é. Portanto, quando estamos ensinando nosso modelo a prever palavras, devemos ocultar as palavras que seguem a que estamos analisando no momento, escondendo as “respostas certas”. Isso é feito pela máscara causal.
def masking(att_patt):rows, cols = att_patt.form[-2], att_patt.form[-1]
temp = torch.ones((1, rows, cols))
temp = torch.tril(temp)
masks = (temp == 0).to(machine)
mask_mat = torch.masked_fill(att_patt, masks, -1e9)
return mask_mat
O padrão de atenção com a máscara causal fica da seguinte maneira.
3) Multi-Head Consideration: Um Bicho de Muitas Cabeças
Dada uma mesma frase, há diversas relações entre as palavras diferentes. Podemos dizer que há diversas “interpretações” possíveis para a mesma frase. Assim, para captar esses múltiplos significados, em modelos de transformers, não usamos apenas um único mecanismo de atenção para processar nossa frase. Há diversos mecanismos ou “cabeças” de atenção sendo executados em paralelo, cada uma com o objetivo de extrair informações diferentes de uma mesma sentença.
Para cada cabeça de atenção devemos gerar vetores de question, key e worth. Para isso, a solução é criar matrizes WQ, WK e WV, todas de dimensão D_MODEL por D_MODEL. Essas matrizes, quando multiplicadas pelos embeddings das palavras da nossa frase, geram matrizes de vetores question, key e worth para cada cabeça de atenção no nosso modelo. No nosso caso, havendo 8 cabeças de atenção, e tendo em vista que nossos vetores de embedding tem dimensão 512, teremos como resultado da multiplicação dos embeddings por cada uma das matrizes WQ, WK e WV, oito matrizes de vetores com dimensão 512 / 8 = 64, que será a dimensão dos nossos vetores Q, Ok e V. De modo geral, é importante garantir que a dimensão dos nossos vetores question, key e worth seja sempre igual a D_MODEL dividido pelo número de heads, já que isso garante consistência dimensional e facilita a paralelização.
Depois que cada head executar o mecanismo de atenção, os vetores worth do output de cada head serão concatenados, criando novamente um vetor de tamanho D_MODEL para palavra, que serão subsequentemente passados por uma camada densa. Isso é como unir cada “interpretação” da palavra de volta em um único embedding que contenha os significados possíveis desta. Podemos implementar isso em Python como se segue.
def __init__(self, config:Configs, masks=True):
tremendous(MultiHeadAttention, self).__init__()# hiperparametros
self.d_k = config.d_k
self.d_model = config.d_model
self.n_heads = config.n_heads
self.batch = config.batch
self.masks = masks
# matrizes WQ, WK e WV que criaram as matrizes de
# vetores Q, Ok e V para nossas cabeças de atenção
self.Wq = nn.Linear(self.d_model, self.d_model)
self.Wk = nn.Linear(self.d_model, self.d_model)
self.Wv = nn.Linear(self.d_model, self.d_model)
# Última camada de transformações
self.W0 = nn.Linear(self.d_model, self.d_model)
def ahead(self, pos_encode_toks):
batch, seq_len, d_model = pos_encode_toks.form
# gera os N_HEADS conjuntos de matrizes de vetores Q, Ok e V
Q = self.Wq(pos_encode_toks).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2)
Ok = self.Wk(pos_encode_toks).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2)
V = self.Wq(pos_encode_toks).view(batch, seq_len, self.n_heads, self.d_k).transpose(1, 2)
# aplica o mecanismo de atenção para cada cabeça de atenção
att, att_pat = consideration(Q, Ok, V, d_k=self.d_k, masks=self.masks)
# roda a última transformação
res = self.W0(att.transpose(1, 2).contiguous().view(batch, seq_len, -1))
return res
4) Camadas MLP
Agora já calculamos o mecanismo de atenção para cada uma das nossas cabeças de atenção. Na prática, podemos repetir o passo anterior algumas vezes, com o objetivo de extrair nuances cada vez maiores dos dados textuais. Um dos últimos passos antes da geração dos outputs é organizarmos as informações extraídas pelos nossos mecanismos de atenção. Isso é feito pela aplicação de uma rede neural densa sobre cada embedding transformado pelas camadas multi-head consideration. Podemos testar diversas arquiteturas possíveis, mas para nossa implementação usaremos a mesma que o artigo authentic. Em Python, implementaremos a rede densa como se segue.
class MLP(nn.Module):def __init__(self, config:Configs):
tremendous(MLP, self).__init__()
# rede densa como apenas uma hidden layer
self.layer1 = nn.Linear(config.d_model, config.d_hid)
self.layer2 = nn.Linear(config.d_hid, config.d_model)
def ahead(self, X):
# feedforward
return self.layer2(F.relu(self.layer1(X)))
5) Layer Normalization e Residual Connection
Entre cada camada da nossa rede, tanto de atenção quanto densas, aplicamos algumas técnicas que facilitam seu treino (evitando, por exemplo, problemas de vanishing gradients). Usamos operações de normalização nos outputs (subtrair a média e dividir pelo desvio padrão) e de conexão residual (soma dos inputs com os outputs de uma camada). Além da utilidade prática em termos do treino, a conexão residual pode ser interpretada como sendo a atualização dos embeddings antigos utilizando os outputs das camadas anteriores, incorporando a eles os novos significados contextuais aprendidos
class LayerNorm(nn.Module):def __init__(self, config:Configs):
tremendous(LayerNorm, self).__init__()
# layer normalization
self.layernorm = nn.LayerNorm(config.d_model)
def ahead(self, X, sublayer_X):
# residual connection
X = X + sublayer_X
return self.layernorm(X)
6) Decoder
Vamos juntar nosso progresso até agora em uma única classe que pega nossos embeddings e os processa pelas camadas de atenção e densas.
class DecoderLayer(nn.Module):def __init__(self, config:Configs, masks=True):
tremendous(DecoderLayer, self).__init__()
# camada de atenção
self.mha1 = MultiHeadAttention(config, masks=masks)
# dropout, layer normalization e residual connection
self.dropout1 = nn.Dropout(config.dropout)
self.layernorm1 = LayerNorm(config)
# camada densa
self.ff = MLP(config)
# mais um dropout, layer normalization e residual connection
self.dropout2 = nn.Dropout(config.dropout)
self.layernorm2 = LayerNorm(config)
def ahead(self, X):
# passando embeddings pela camada de atenção
X = self.layernorm1(X, self.dropout1(self.mha1(X)))
# passando autputs pela camada densa
X = self.layernorm2(X, self.dropout2(self.ff(X)))
return X
O output da nossa rede será de vetores de números (o computador só entende número lembra?). Temos que converter os outputs em texto. Por isso empregamos a última camada da nossa rede que converte nosso output para uma distribuição de probabilidade sobre palavras. Essa distribuição nos mostra quais as palavras mais prováveis de se seguirem ao texto que demos como enter. Assim, a dimensão dessa última camada deve ser do tamanho do nosso vocabulário. Essa é a chamada camada de unembedding. A totalidade da rede fica como se segue.
class Transformer(nn.Module):def __init__(self, vocab_size, config:Configs, masks=True):
tremendous(Transformer, self).__init__()
# hiperparametros
self.vocab_size = vocab_size
self.batch = config.batch
self.d_model = config.d_model
self.n_layers = config.n_layers
self.d_k = config.d_k
# camada de embedding
self.embedding = nn.Embedding(vocab_size, self.d_model)
# geração dos vetores de posição e soma desses aos embeddings
self.pos_enc = PositionalEncoding(config)
# dropout
self.dropout = nn.Dropout(config.dropout)
# blocos de atenção + densas
self.blocks = nn.ModuleList([DecoderLayer(config, mask=mask) for _ in range(self.n_layers)])
# unembedding
self.unembed = nn.Linear(self.d_model, self.vocab_size)
def ahead(self, X):
# embedding
embeddings = self.embedding(X)
out = self.dropout(self.pos_enc(embeddings))
# feedforward
for i, l in enumerate(self.blocks):
out = l(out)
# geração da distribuição de probabilidade sobre palaras
# do vocabulário da rede.
logits = self.unembed(out)
return logits
Agora que temos nossos arquivos Python contendo a classe da rede transformer e as configurações de hiperparametros da nossa rede, vamos criar um Jupyter pocket book para treinar nossa rede nas obras completas de Shakespeare. Já que o foco desse artigo é em como funciona uma rede Transformer e não como preprocessar dados nem treinar uma rede neural, passarei um pouco mais rápido por essa parte.
Primeiro vamos importar as bibliotecas relevantes e declarar em qual dispositivo treinaremos a rede (CPU ou GPU).
from tqdm import tqdm
import numpy as np# bibliotecas PyTorch para criar nossa rede
import torch
import torch.nn as nn
import torch.nn.useful as F
# para plotar
import matplotlib.pyplot as plt
# para importar dados da net
from bs4 import BeautifulSoup
import re
import urllib3
# para gerar base de dados
from torch.utils.information import Dataset, DataLoader
from torch.nn.utils import clip_grad_norm_
# métricas úteis
from sklearn.metrics import accuracy_score
# nossos scripts
from fashions import *
from config import Configs
machine = torch.machine('cuda') if torch.cuda.is_available() else torch.machine('cpu')
Vamos importar as obras completas de Shakespeare de um web site.
# dados
url = 'https://www.gutenberg.org/information/100/100-0.txt'# importando e formatando
http = urllib3.PoolManager()
information = http.request('GET', url)
soup = BeautifulSoup(information.information)
soup_str = str(soup)
Agora que temos nossos dados devemos tokenizá-los, ou seja, separar nosso texto em partes menores (para nosso caso podemos assumir que estamos separando o texto em palavras).
def tokenizer(clean_text):toks = re.cut up(r'b', clean_text)
toks = [i.strip() for i in toks if i != ' ']
return np.array(toks)
toks = tokenizer(soup_str)
Tendo em mãos nossos tokens, devemos criar o vocabulário dos nossos dados, ou seja, o conjunto das palavras únicas existentes nos nossos dados (lembrem que devemos fazer o embedding para cada palavra única nos nossos dados).
# criando vocabulário e pegando seu tamanho
vocab = listing(set(toks))
vocab.append('[UNKNOWN]')
VOCAB_SIZE = len(vocab)# dicionário de ID para cada token
id2tok = dict(enumerate(vocab))
tok2id = {v:ok for ok, v in id2tok.gadgets()}
Agora vamos criar nossa base de dados num formato compatível com o PyTorch. Para isso usaremos as lessons Dataset e DataLoader.
class ShakespeareDataset(Dataset):""" Gerando inputs e outputs para treinar nossa rede """
def __init__(self, textual content, seq_len, tok2id):
self.seq_len = seq_len
self.tok2id = tok2id
self.sequence_ids = listing(vary(0, len(toks), self.seq_len+1))
self.sequences = [toks[self.sequence_ids[i]:self.sequence_ids[i+1]] for i in vary(len(self.sequence_ids) - 1)]
def __len__(self):
return len(self.sequences)
def __getitem__(self, idx):
seqids = [self.tok2id[tok] for tok in self.sequences[idx]]
X, y = seqids[:-1], seqids[1:]
return torch.Tensor(X).sort(torch.LongTensor), torch.Tensor(y).sort(torch.LongTensor)
# gerando base de dados
SEQ_LEN = 128
train_d = ShakespeareDataset(toks, SEQ_LEN, tok2id)
# criando dataloader para alimentar nossa rede em batches
BATCH_SIZE = 64
train_dlr = DataLoader(train_d, batch_size=BATCH_SIZE, shuffle=True)
Agora vamos começar os códigos para treinar nossa rede. Vamos escrever um schedular de studying fee como consta no paper authentic e uma função de treino. O scheduler muda a nossa studying fee durante o treino, ajudando a manter a estabilidade na descida do gradiente.
from torch.optim.lr_scheduler import LambdaLRdef lr_sched_func(current_step, d_model=512, warmup=400):
if current_step == 0:
return 1e-7
# consultar paper authentic para justificativa
lr = (d_model ** (-.5)) * min(current_step ** (-.5), current_step * (warmup ** (-1.5)))
return lr
Esse scheduler atualiza valores da studying fee para cada batch ao invés de cada época. O código abaixo tem a função de treino com o scheduler. Para cada batch geramos outputs, calculamos a loss e a retropropagamos na rede, alterando seus parâmetros de modo a melhorar sua efficiency.
def train_with_sched(m, dlrt, loss, optm, sched=None):machine = torch.machine('cuda') if torch.cuda.is_available() else torch.machine('cpu')
# leva modelo para dispositivo (CPU ou GPU)
m.to(machine)
m.prepare()
train_running_loss = 0
# barra de progresso dos dados
train_bar = tqdm(enumerate(dlrt), whole=len(dlrt))
# para cada batch
for i, (seq, rev) in train_bar:
seq = seq.to(machine)
rev = rev.to(machine)
# zera o gradiente
optm.zero_grad()
# feedforward
output = m(seq)
# loss
l = loss(output.view(-1, output.form[-1]), rev.view(-1))
# backpropagation
l.backward()
# gradient clipping
clip_grad_norm_(m.parameters(), 1)
# step
optm.step()
if sched will not be None:
sched.step()
# replace working loss
train_running_loss += l.cpu().merchandise()
print('Completed Batch: ')
epoch_loss = train_running_loss / len(dlrt)
print('Coaching Loss: ', epoch_loss)
return epoch_loss
Para treinar a rede segue o código abaixo.
cfg = Configs(BATCH_SIZE=BATCH_SIZE, SEQ_LEN=SEQ_LEN, N_LAYERS=6)
BardofAvon = Transformer(vocab_size=VOCAB_SIZE, masks=True, config=cfg).to(machine)
optm = torch.optim.Adam(BardofAvon.parameters(), lr=1, betas=(0.9, 0.98), eps=1e-9)
scheduler = LambdaLR(optm, lr_lambda=lr_sched_func)
loss = nn.CrossEntropyLoss()
max_epochs = 50
train_losses = []# treinando
print('########### START ###########')
for e in vary(max_epochs):
print(e)
train_loss = train_with_sched(BardofAvon, train_dlr, loss, optm, sched=scheduler)
train_losses.append(train_loss)
print('############ END ############')
Parabéns! Terminamos de implementar e treinar nosso primeiro Transformer! O treino pode ser bastante demorado, então não se preocupem (mesmo usando GPUs, para mim demorou cerca de duas horas). Esta é apenas a primeira arquitetura de Transformer desenvolvida. Hoje em dia, existem diversas versões, cada uma com suas próprias aplicações.
Código completo pode ser encontrado em: https://github.com/iantextbar/deep_learning_curriculum_jacob_hilton/tree/master/transformers_ch1