O modelo Transformer foi introduzido no artigo seminal "Attention is All You Need" e apresenta uma estrutura completa de encoder-decoder composta principalmente por mecanismos de atenção. Endereço do artigo: https://arxiv.org/abs/1706.03762.
A arquitetura geral é mostrada na figura abaixo:
O modelo consiste em duas partes principais: codificador (Encoder) e decodificador (Decoder), com a estrutura interna total ilustrada na seguinte imagem:
Figura 2
No artigo original, a parte do codificador é composta por 6 codificadores idênticos empilhados juntos, e a parte do decodificador também é formada por 6 decodificadores idênticos empilhados, onde os codificadores não compartilham parâmetros entre si. (Não necessariamente precisam ser 6.)
Antes de alimentar as representações vetoriais das palavras nos codificadores e decodificadores, primeiro aplicamos a codificação posicional. A seguir, abordaremos sequencialmente os pontos técnicos da estrutura: codificação posicional, codificação (camada multi-head attention, conexão residual, normalização de camada) e decodificação:
- Codificação Posicional
Como mostrado na figura, devido ao fato de que o mecanismo de atenção não conter informações de posição, as frases primeiramente passam pelo embedding para obter representações vetoriais, e simultaneamente, para adicionar informações de posição, codificações posicionais são adicionadas aos embeddings com base nas posições das palavras na frase. No artigo, o método de adição da codificação posicional é: construir uma matriz com dimensões iguais ao embedding de entrada, então somar com o embedding de entrada para obter a entrada do multi-head attention.
O autor desejava introduzir uma fórmula de codificação de posição absoluta para permitir que o modelo aprendesse informações de posição relativa. O autor utilizou a geração de representações fixas de posição da seguinte forma:
Implementação em código:
1 class CodificadorPosicional(nn.Module):
2
3 def __init__(self, tamanho_modelo, taxa_dropout, tam_max=5000):
4 super(CodificadorPosicional, self).__init__()
5 self.taxa_dropout = nn.Dropout(p=taxa_dropout)
6 cod_pos = torch.zeros(tam_max, tamanho_modelo)
7 posicao = torch.arange(0, tam_max).unsqueeze(1)
8 term_div = torch.exp(
9 torch.arange(0, tamanho_modelo, 2) * -(math.log(10000.0) / tamanho_modelo)
10 )
11 cod_pos[:, 0::2] = torch.sin(posicao * term_div)
12 cod_pos[:, 1::2] = torch.cos(posicao * term_div)
13 cod_pos = cod_pos.unsqueeze(0)
14 self.register_buffer("cod_pos", cod_pos)
15
16 def forward(self, x):
17 x = x + self.cod_pos[:, : x.size(1)].requires_grad_(False)
18 return self.taxa_dropout(x)
Dado a fórmula trigonométrica:
O autor esperava que através da fórmula de codificação de posição absoluta, o modelo pudesse aprender informações de posição relativa. Embora os embeddings de posição obtidos dessa maneira, cujo produto escalar pode refletir distância relativa, careçam de direcionalidade, e essa característica (distância relativa) possa ser destruída pelo mecanismo de atenção do Transformer original.
Com base na fórmula (1), a incorporação posicional na posição t pode ser expressa como:
- Codificação
Conforme mostrado na estrutura esquerda da Figura 2, o codificador é principalmente composto por camadas de rede neural feedforward e pela camada MultiHeadedAttention. É importante notar que, em cada subcamada (auto-atenção, rede feedforward) de cada codificador, há uma conexão residual acompanhada por uma etapa de "normalização de camada".
- Primeiro, vamos introduzir o mecanismo de atenção com um exemplo:
Suponha que queremos traduzir esta frase:
"The animal didn't cross the street because it was too tired"
Nesta frase, o "it" se refere a "animal" ou "street"? Embora seja fácil para humanos entender essa frase, é muito difícil para máquinas. Quando o modelo processa a palavra "it", o mecanismo de auto-atenção permite que "it" estabeleça uma conexão com "animal". Conforme o modelo processa cada palavra da sequência de entrada, a auto-atenção considera todas as palavras da sequência inteira, ajudando o modelo a codificar melhor a palavra atual, conforme mostrado na figura abaixo.
Quando codificamos a palavra "it" no codificador #5 (o codificador mais alto na pilha), parte do mecanismo de atenção irá focar em "The Animal", incorporando parte de sua representação na codificação de "it".
A seguir, vamos introduzir a ideia por trás da implementação da atenção.
O primeiro passo para calcular a auto-atenção é gerar três vetores a partir de cada vetor de entrada do codificador (cada embedding de palavra). Ou seja, para cada palavra, criamos um vetor de consulta, um vetor-chave e um vetor de valor. Esses três vetores são criados multiplicando o embedding da palavra por três matrizes de pesos. No artigo, as dimensões desses vetores são menores que o embedding da palavra, embora na prática isso não seja obrigatório, sendo apenas uma escolha arquitetural que mantém grande parte do cálculo da atenção multi-cabeça inalterado.
O segundo passo para calcular a auto-atenção é calcular pontuações. Suponha que precisamos calcular o vetor de auto-atenção para a primeira palavra 'Thinking', então precisamos pontuar cada palavra da frase de entrada em relação a "Thinking". Essas pontuações determinam quanta atenção dar às outras partes da frase durante a codificação da palavra "Thinking".
Essas pontuações são calculadas fazendo o produto escalar entre o vetor-chave da palavra pontuada (todas as palavras da frase de entrada) e o vetor de consulta de "Thinking". Portanto, se estamos lidando com a auto-atenção da palavra mais à frente da posição, a primeira pontuação seria o produto escalar de q1 e k1, a segunda pontuação seria o produto escalar de q1 e k2.
O terceiro e quarto passos envolvem dividir as pontuações por 8 (8 é a raiz quadrada da dimensão do vetor-chave usada no artigo, 64, o que torna os gradientes mais estáveis. Outros valores podem ser usados aqui, 8 é apenas o valor padrão), e depois passar o resultado pela função softmax. A função softmax serve para normalizar as pontuações de todas as palavras, resultando em pontuações positivas que somam 1.
Esta pontuação softmax determina a contribuição de cada palavra para a codificação da posição atual ("Thinking"). Obviamente, a palavra já nessa posição receberá a maior pontuação softmax, mas às vezes também pode ser útil prestar atenção em outra palavra relacionada à palavra atual.
O quinto passo é multiplicar cada vetor de valor pelas pontuações softmax (isso serve para preparar a soma subsequente). A intuição aqui é que desejamos focar em palavras semanticamente relevantes e enfraquecer palavras irrelevantes.
O sexto passo é somar os vetores de valor ponderados, resultando na saída da camada de auto-atenção nessa posição.
Assim, o cálculo da auto-atenção é concluído. O vetor resultante pode então ser passado para a rede neural feedforward.
Na prática, o mecanismo de auto-atenção é implementado usando matrizes, seguindo a mesma lógica descrita acima:
O primeiro passo é calcular as matrizes de consulta, chave e valor, como mostrado na figura abaixo:
Os passos de cálculo anteriores podem ser combinados em:
Após apresentar o mecanismo de auto-atenção, vamos introduzir o mecanismo de atenção multi-cabeça "multi-headed" usado no artigo.
Cada cabeça é uma matriz de pesos de consulta/chave/valor independente, produzindo assim diferentes matrizes de consulta/chave/valor. No artigo, são usadas 8 cabeças, então após 8 operações com diferentes matrizes de pesos, obtemos 8 matrizes Z diferentes.
Então comprimimos essas 8 matrizes em uma única matriz. O princípio de implementação é concatenar essas 8 matrizes juntas e depois multiplicar por uma matriz de pesos para obter uma matriz Z que funde informações de todas as cabeças de atenção, que é então somada e normalizada antes de ser passada para a camada feedforward.
Implementação em código:
1 def atencao(consulta, chave, valor, mascara=None, taxa_dropout=None):
2 dim_chave = consulta.size(-1)
3 pontuacoes = torch.matmul(consulta, chave.transpose(-2, -1)) / math.sqrt(dim_chave)
4 if mascara is not None:
5 pontuacoes = pontuacoes.masked_fill(mascara == 0, -1e9)
6 prob_attn = pontuacoes.softmax(dim=-1)
7 if taxa_dropout is not None:
8 prob_attn = taxa_dropout(prob_attn)
9 return torch.matmul(prob_attn, valor), prob_attn
10
11 class AttencaoMultiCabeca(nn.Module):
12 def __init__(self, num_cabecas, dim_modelo, taxa_dropout=0.1):
13 #num_cabecas é o número de cabeças de atenção, dim_modelo é a dimensão do vetor
14 super(AttencaoMultiCabeca, self).__init__()
15 assert dim_modelo % num_cabecas == 0
16 # Assumimos que d_v sempre igual a d_k
17 self.dim_chave = dim_modelo // num_cabecas
18 self.num_cabecas = num_cabecas
19 self.camadas_lineares = clona_camadas(nn.Linear(dim_modelo, dim_modelo), 4)
20 self.attn = None
21 self.taxa_dropout = nn.Dropout(p=taxa_dropout)
22
23 def forward(self, consulta, chave, valor, mascara=None):
24 if mascara is not None:
25 mascara = mascara.unsqueeze(1)
26 tamanho_batch = consulta.size(0)
27
28 # 1) Fazer transformação linear para converter d_modelo em num_cabecas x dim_chave
29 consulta, chave, valor = [
30 lin(camada).view(tamanho_batch, -1, self.num_cabecas, self.dim_chave).transpose(1, 2)
31 for lin, camada in zip(self.camadas_lineares, (consulta, chave, valor))
32 ]
33
34 # 2) Mecanismo de atenção
35 saida, self.attn = atencao(
36 consulta, chave, valor, mascara=mascara, taxa_dropout=self.taxa_dropout
37 )
38
39 # 3) Fundir as matrizes de atenção das num_cabecas cabeças
40 saida = (
31 saida.transpose(1, 2)
42 .contiguous()
43 .view(tamanho_batch, -1, self.num_cabecas * self.dim_chave)
44 )
45 del consulta
46 del chave
47 del valor
48 return self.camadas_lineares[-1](saida)
- Conexão Residual
Durante o aprendizado de redes profundas, dois problemas inevitáveis surgem:
1. Desaparecimento/Explosão de Gradientes
- Degradação da Rede
O Transformer usa conexão residual e normalização de camada para melhorar esses problemas.
Sua estrutura é mostrada na figura acima, onde a conexão residual soma os dados de entrada e saída de cada posição, permitindo que o Transformer treine efetivamente redes mais profundas. Após a conexão residual, aplica-se a normalização de camada.
Implementação em código:
Normalização de Camada:
1 class NormCamada(nn.Module):
2
3 def __init__(self, caracteristicas, eps=1e-6):
4 super(NormCamada, self).__init__()
5 self.param_a = nn.Parameter(torch.ones(caracteristicas))
6 self.param_b = nn.Parameter(torch.zeros(caracteristicas))
7 self.eps = eps
8
9 def forward(self, x):
10 media = x.mean(-1, keepdim=True)
11 std_dev = x.std(-1, keepdim=True)
12 return self.param_a * (x - media) / (std_dev + self.eps) + self.param_b
Conexão Residual:
1 class ConexaoSubcamada(nn.Module):
2
3 def __init__(self, tamanho, taxa_dropout):
4 super(ConexaoSubcamada, self).__init__()
5 self.norm = NormCamada(tamanho)
6 self.taxa_dropout = nn.Dropout(taxa_dropout)
7
8 def forward(self, x, subcamada):
9 "Aplica conexão residual a qualquer subcamada com o mesmo tamanho."
10 return x + self.taxa_dropout(subcamada(self.norm(x)))
Decodificação (Decodificador):
Os componentes internos do decodificador são semelhantes aos do codificador, com a ressalva de que a primeira camada de atenção do decodificador é chamada de MaskedMulti-Head Attention. Com a adição da operação MASK, só é permitido processar posições anteriores na sequência de saída, ou seja, só podemos atender às sentenças já processadas anteriormente. A segunda camada de atenção é chamada de camada de atenção encoder-decoder. Como mostra a Figura 2, sua consulta vem da saída da camada decoder anterior, enquanto chave e valor vêm da saída do encoder. As saídas do encoder ajudam o decodificador a focar nas posições adequadas da sequência de entrada. Em seguida, são enviadas para a camada feedforward, e essas etapas se repetem até alcançar um símbolo especial de término, indicando que o decodificador do Transformer completou sua saída. A saída de cada etapa é fornecida ao decodificador inferior na próxima etapa de tempo, e como feito anteriormente com os codificadores, esses decodificadores emitirão seus resultados decodificados. Além disso, como fizemos com as entradas dos codificadores, incorporamos e adicionamos codificação posicional a esses decodificadores para representar a posição de cada palavra.
Após a decodificação, um vetor de números reais é produzido e mapeado através de uma simples rede neural totalmente conectada (camada de transformação linear) para um vetor chamado logit. Supondo que aprendemos dez mil palavras do conjunto de treinamento, o vetor de logit terá dez mil células - cada célula correspondendo à pontuação de uma palavra específica. Em seguida, a camada Softmax converte essas pontuações em probabilidades (todas positivas, com limite superior de 1,0). A célula com a maior probabilidade é selecionada e a palavra correspondente é usada como saída dessa etapa de tempo.
Referências: Attention is All You Need
BERT é popular mas você não entende o Transformer? Leia este artigo
Codificação Posicional no Transformer
Discussão sobre Representação de Posição em Modelos Baseados em Transformer
The Annotated Transformer