Aula 3, Encoder e decoder
Esta aula monta o bloco completo do Transformer ao redor da atenção. Vamos acrescentar o codificador de posição, a rede feed-forward, as conexões residuais e a normalização, e entender a divisão entre encoder e decoder que organiza toda a família de modelos.
Nas duas aulas anteriores, construímos o coração do Transformer, a atenção de múltiplas cabeças. Mas a atenção sozinha não é um modelo, ela é uma peça. Para virar um Transformer de verdade, precisamos de algumas peças complementares e de uma forma de organizá-las em camadas. É isso que esta aula faz.
Vamos ver que o Transformer original tem duas pilhas, um encoder, que lê e entende uma sequência, e um decoder, que gera uma nova sequência olhando para a do encoder. Dessa divisão nascem as três grandes famílias de modelos que dominam o NLP, as só de encoder, como o BERT, as só de decoder, como o GPT, e as de encoder mais decoder, como os modelos de tradução. Entender essa estrutura é o mapa para os módulos seguintes.
Objetivos
Ao final desta aula, você deve ser capaz de:
- Explicar o papel do codificador de posição em um Transformer.
- Descrever as peças de um bloco, atenção, feed-forward, residual e normalização.
- Diferenciar encoder, decoder e a atenção mascarada.
- Implementar um bloco de encoder do zero.
Teoria
A atenção, por si só, não sabe a ordem das palavras, pois trata a sequência como um conjunto. Como a ordem importa muito na linguagem, o Transformer soma a cada token um codificador de posição, um vetor que depende da posição e que informa onde a palavra está. O artigo original usa funções seno e cosseno de frequências variadas, de modo que cada posição recebe uma assinatura única.
Cada bloco do Transformer tem duas subcamadas. A primeira é a atenção de múltiplas cabeças. A segunda é uma pequena rede feed-forward, aplicada a cada posição separadamente, que dá capacidade de transformação não linear. Ao redor de cada subcamada há uma conexão residual, que soma a entrada à saída, e uma normalização de camada, proposta por Ba e colegas, que estabiliza o treino. Essas duas ideias são o que permite empilhar muitos blocos sem que o sinal se perca.
flowchart TB
X[Entrada + posição] --> A[Atenção multi-cabeça]
A --> R1[Soma residual e normalização]
R1 --> F[Feed-forward]
F --> R2[Soma residual e normalização]
R2 --> Y[Saída do bloco]
A diferença entre encoder e decoder está em como cada um usa a atenção. O encoder usa atenção plena, em que cada token vê todos os outros, ideal para entender uma sequência. O decoder usa atenção mascarada, em que cada token só pode ver os anteriores, necessária para gerar texto da esquerda para a direita sem trapacear olhando o futuro. O decoder ainda tem uma atenção extra que olha para a saída do encoder, ligando as duas pilhas.
Explicação Intuitiva
Pense no bloco do Transformer como uma estação de refinamento. A informação entra, passa pela atenção, onde cada palavra conversa com as outras, e em seguida pela feed-forward, onde cada palavra é processada individualmente. As conexões residuais funcionam como atalhos que garantem que a informação original não se perca no caminho, e a normalização mantém os números em uma faixa saudável. Empilhando muitas dessas estações, a representação vai ficando cada vez mais rica.
A distinção entre encoder e decoder é como a diferença entre ler e escrever. Para ler e entender, você pode olhar a frase inteira, de uma vez, para frente e para trás, e é isso que o encoder faz. Para escrever, você produz uma palavra de cada vez e não pode olhar o que ainda não escreveu, e é isso que a atenção mascarada do decoder garante.
Explicação Matemática
O codificador de posição usa, para a posição $pos$ e a dimensão $i$,
\[PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d}}\right), \qquad PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d}}\right).\]Frequências diferentes em dimensões diferentes dão a cada posição uma assinatura única, que é somada ao embedding do token.
Um bloco de encoder pode ser escrito, com a entrada $X$, como
\(Z = \text{LayerNorm}\big(X + \text{MultiHead}(X)\big),\) \(\text{saída} = \text{LayerNorm}\big(Z + \text{FFN}(Z)\big),\)
em que $\text{FFN}(z) = \max(0, z W_1 + b_1) W_2 + b_2$ é a rede feed-forward com ativação ReLU. A atenção mascarada do decoder é igual à atenção comum, mas com as pontuações das posições futuras zeradas antes da softmax, o que veremos em detalhe na aula sobre o GPT.
Exemplo Prático
Vamos montar um bloco de encoder completo do zero. Primeiro construímos o codificador de posição e o somamos aos embeddings. Depois aplicamos a atenção, a conexão residual e a normalização, em seguida a feed-forward com nova residual e normalização. O resultado é uma representação refinada da sequência, com a dimensão preservada.
Ver o bloco inteiro funcionando deixa claro que um Transformer é, no fundo, a repetição disciplinada dessas peças simples. O código está no notebook notebooks/modulo-06/03-encoder-decoder.ipynb, então abra-o ao lado para acompanhar.
Código Comentado
import numpy as np
def softmax(z, eixo=-1):
z = z - z.max(axis=eixo, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=eixo, keepdims=True)
def positional_encoding(seq_len, d):
"""Assinatura de posição com senos e cossenos de frequências variadas."""
pos = np.arange(seq_len)[:, None]
i = np.arange(d)[None, :]
ang = pos / np.power(10000, (2 * (i // 2)) / d)
pe = np.zeros((seq_len, d))
pe[:, 0::2] = np.sin(ang[:, 0::2])
pe[:, 1::2] = np.cos(ang[:, 1::2])
return pe
def layernorm(x, eps=1e-5):
mu = x.mean(-1, keepdims=True)
var = x.var(-1, keepdims=True)
return (x - mu) / np.sqrt(var + eps)
def atencao(X, Wq, Wk, Wv):
d = X.shape[1]
Q, K, V = X @ Wq, X @ Wk, X @ Wv
A = softmax(Q @ K.T / np.sqrt(d), eixo=-1)
return A @ V
def encoder_block(X, Wq, Wk, Wv, W1, W2):
att = atencao(X, Wq, Wk, Wv)
x = layernorm(X + att) # residual + normalização
ff = np.maximum(0, x @ W1) @ W2 # feed-forward com ReLU
return layernorm(x + ff) # residual + normalização
E = np.array([
[1.0, 0.0, 0.0, 0.0],
[0.9, 0.1, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
])
rng = np.random.default_rng(0)
pe = positional_encoding(4, 4)
X = E + pe # soma a posição aos embeddings
Wq = rng.normal(0, 1, (4, 4)); Wk = rng.normal(0, 1, (4, 4)); Wv = rng.normal(0, 1, (4, 4))
W1 = rng.normal(0, 0.5, (4, 8)); W2 = rng.normal(0, 0.5, (8, 4))
saida = encoder_block(X, Wq, Wk, Wv, W1, W2)
print("Codificação de posição da primeira posição:", np.round(pe[0], 2))
print("Saída do bloco de encoder, shape:", saida.shape)
Ao rodar, a primeira posição recebe a assinatura [0, 1, 0, 1], que vem de seno de zero
igual a zero e cosseno de zero igual a um, e o bloco produz uma saída com a mesma
dimensão da entrada, pronta para alimentar o próximo bloco. Empilhando vários desses
blocos, temos um encoder, a base do BERT, que veremos na próxima aula. Trocando a atenção
plena pela mascarada, temos um decoder, a base do GPT.
Exercícios
1) Conceitual: Por que a atenção, sozinha, não sabe a ordem das palavras, e como o codificador de posição resolve isso? 2) Conceitual: Qual a função das conexões residuais e da normalização de camada em um bloco do Transformer? 3) Prático: Aumente a sequência para mais posições e visualize como o codificador de posição muda de uma posição para outra. 4) Prático: Empilhe dois blocos de encoder e verifique que a saída continua com a dimensão correta. 5) Extensão: Pesquise a diferença entre normalização antes e depois da subcamada, o pre-norm e o post-norm, e descreva o efeito no treino.
Projeto da Aula
Construa um mini encoder de duas camadas. A entrega é um programa que soma o codificador de posição aos embeddings e aplica dois blocos de encoder em sequência, mostrando a forma da saída e como a representação muda de um bloco para o outro.
Considere o projeto pronto quando você conseguir empilhar os blocos sem erros de dimensão e escrever um parágrafo explicando o papel de cada peça do bloco. Com o bloco do Transformer montado, estamos prontos para conhecer os dois grandes modelos que nascem dele, o BERT, só de encoder, e o GPT, só de decoder.
Leituras Recomendadas
- O artigo Attention Is All You Need, que descreve o bloco completo e o codificador de posição.
- O artigo de Ba, Kiros e Hinton sobre a normalização de camada.
- O texto The Annotated Transformer, de Harvard NLP, que implementa o bloco passo a passo.
Referências Científicas
As referências abaixo são reais e estão registradas em references/referencias.bib. As chaves entre parênteses são as do BibTeX.
- Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS.
(
vaswani2017attention) - Ba, J. L., Kiros, J. R., e Hinton, G. E. (2016). Layer Normalization.
(
ba2016layernorm) - Bahdanau, D., Cho, K., e Bengio, Y. (2015). Neural Machine Translation by Jointly
Learning to Align and Translate. ICLR. (
bahdanau2015attention)