Arquitetura do Modelo Transformer: Uma Abordagem Completa de Encoder-Decoder

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:

  1. 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:

  1. 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".

  1. 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)

  1. Conexão Residual

Durante o aprendizado de redes profundas, dois problemas inevitáveis surgem:

1. Desaparecimento/Explosão de Gradientes

  1. 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

Tags: Transformer attention-mechanism positional-encoding multi-head-attention Encoder-Decoder

Publicado em 9-10 03:18