Transformers: Atenção é Tudo que Você Precisa

Sobre este Tutorial

Este tutorial apresenta uma exploração completa e didática da arquitetura Transformer, introduzida no artigo seminal "Attention Is All You Need" (Vaswani et al., 2017). Vamos examinar cada componente desta arquitetura revolucionária que mudou fundamentalmente o Processamento de Linguagem Natural (NLP) e além.

Referência: Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., & Polosukhin, I. (2017). Attention is all you need. In Advances in neural information processing systems (pp. 5998-6008).
arXiv:1706.03762

1. Introdução aos Transformers

1.1 Por que Transformers?

Durante anos, as Redes Neurais Recorrentes (RNNs) e suas variantes (LSTM, GRU) dominaram tarefas de modelagem de sequências e transdução. No entanto, essas arquiteturas tinham limitações fundamentais:

  • Processamento Sequencial: Não podem processar tokens em paralelo, limitando o uso de GPUs
  • Dependências de Longo Alcance: Dificuldade em capturar relações entre posições distantes
  • Gradientes Desvanecentes: Desafios de treinamento com sequências longas
  • Tempo de Treinamento: Lento devido à natureza sequencial inerente
  • Restrições de Memória: Estado oculto se torna gargalo para sequências longas

Conceito-Chave

O Transformer elimina completamente a recorrência, confiando apenas em mecanismos de atenção para capturar dependências globais entre entrada e saída. Isso possibilita:

  • Paralelização completa durante o treinamento
  • Modelagem direta de dependências independente da distância
  • Comprimento de caminho constante entre quaisquer duas posições
  • Tempo de treinamento significativamente reduzido

Resultados Principais do Artigo (WMT 2014)

O artigo original demonstrou resultados estado-da-arte:

  • Inglês para Alemão: 28.4 BLEU (melhorando o melhor anterior em mais de 2 BLEU)
  • Inglês para Francês: 41.8 BLEU (novo estado-da-arte para modelo único)
  • Tempo de Treinamento: 3.5 dias em 8 GPUs (fração do custo de modelos anteriores)
  • Generalização: Aplicado com sucesso à análise sintática do inglês

1.2 A Revolução em NLP

O paper "Attention Is All You Need" (2017) introduziu os Transformers e mudou completamente o panorama da IA:

2017: Publicação do paper original sobre Transformers
2018: BERT alcança resultados estado-da-arte em 11 tarefas de NLP
2019: GPT-2 demonstra geração de texto impressionante
2020: GPT-3 com 175 bilhões de parâmetros
2021: Vision Transformers (ViT) superam CNNs em visão computacional
2022-2026: ChatGPT, GPT-4, e modelos multimodais dominam a IA

2. Limitações das RNNs

2.1 Processamento Sequencial

Em uma RNN, cada token deve ser processado em ordem sequencial:

$$h_t = f(h_{t-1}, x_t)$$

Onde $h_t$ depende de $h_{t-1}$, impedindo paralelização

Problema

Esta dependência sequencial significa que não podemos usar GPUs eficientemente, pois precisamos esperar cada passo ser computado antes de processar o próximo.

2.2 Dependências de Longo Prazo

Mesmo com LSTMs e GRUs, capturar relações entre tokens distantes é desafiador:

Exemplo

Considere a frase: "O gato que estava dormindo no sofá da sala acordou."

A RNN precisa propagar informação através de múltiplos passos para conectar "O gato" com "acordou". A cada passo, alguma informação é perdida.

Aspecto RNNs/LSTMs Transformers
Processamento Sequencial Paralelo
Dependências Longas Difícil Fácil (atenção direta)
Complexidade O(n) por timestep O(1) operações (paralelas)
Memória Estado oculto fixo Atenção sobre toda sequência

3. Mecanismo de Atenção

3.1 Intuição da Atenção

A Ideia Central

Atenção permite que o modelo "olhe" para diferentes partes da entrada ao processar cada elemento, decidindo dinamicamente quais partes são mais relevantes para o contexto atual.

Exemplo Intuitivo

Ao traduzir "The cat sat on the mat" para português, ao gerar a palavra "sentou":

  • O modelo "presta atenção" principalmente em "sat"
  • Mas também considera "cat" (para concordância de gênero)
  • E pode olhar para "mat" (contexto completo)

A atenção calcula quanto "peso" dar para cada palavra da entrada.

3.2 Self-Attention (Autoatenção)

No self-attention, a sequência "olha para si mesma" - cada token pode prestar atenção em todos os outros tokens da mesma sequência.

3.2.1 Query, Key e Value

O self-attention utiliza três conceitos fundamentais:

Definições

  • Query (Q): "O que estou procurando?" - representa o token atual fazendo a consulta
  • Key (K): "O que eu ofereço?" - representa o conteúdo de cada token
  • Value (V): "O que eu transmito?" - representa a informação que será extraída

Cada embedding de entrada $x_i$ é transformado em três vetores através de matrizes de peso:

$$Q = XW^Q$$ $$K = XW^K$$ $$V = XW^V$$

Analogia: Biblioteca

Imagine uma biblioteca:

  • Query: Sua pergunta/busca ("Preciso de livros sobre IA")
  • Key: Índice/etiqueta de cada livro ("Este livro é sobre IA", "Este é sobre culinária")
  • Value: Conteúdo real dos livros que você leva

Você compara sua Query com as Keys de todos os livros, encontra os mais relevantes, e pega seus Values (conteúdo).

3.2.2 Fórmula da Atenção com Produto Escalar Escalonado

O mecanismo central de atenção é a Atenção com Produto Escalar Escalonado:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

Vamos entender cada componente e por que funciona:

Computação Passo a Passo

1. Calcular Scores de Compatibilidade (QKT):

  • Multiplicação matricial entre consultas (queries) e chaves (keys)
  • Mede similaridade entre cada par query-key
  • Resulta em logits de atenção de forma (seq_len, seq_len)
  • Valores altos indicam forte relevância
$$\text{Score}_{ij} = q_i \cdot k_j = \sum_{k=1}^{d_k} q_{ik} k_{jk}$$

2. Escalar por √dk:

  • Crítico para manter o fluxo de gradiente
  • Para dk grande, produtos escalares crescem em magnitude
  • Empurra softmax para regiões com gradientes pequenos
  • O fator de escala √dk contrabalança este efeito

Por que o Escalonamento Importa

Para ilustrar: assuma que q e k são variáveis aleatórias independentes com média 0 e variância 1. Seu produto escalar tem média 0 e variância dk. Sem escalonamento, para dk grande, a variância dos produtos escalares se torna grande, empurrando as saídas do softmax para 0 ou 1 (regiões saturadas com gradientes desvanecentes).

3. Aplicar Softmax:

  • Converte scores em distribuição de probabilidade
  • Garante que os pesos de atenção somem 1 para cada query
  • Cria padrões de atenção suaves e diferenciáveis
$$\alpha_{ij} = \frac{\exp\left(\frac{q_i \cdot k_j}{\sqrt{d_k}}\right)}{\sum_{k=1}^{n} \exp\left(\frac{q_i \cdot k_k}{\sqrt{d_k}}\right)}$$

4. Soma Ponderada dos Valores:

  • Combina valores de acordo com os pesos de atenção
  • Produz representação contextualizada para cada posição
  • Saída tem mesma dimensionalidade que os valores
$$\text{output}_i = \sum_{j=1}^{n} \alpha_{ij} v_j$$

Análise de Complexidade

Conforme o artigo original, a complexidade computacional é:

  • Complexidade de Tempo: O(n² · d) onde n é comprimento da sequência, d é dimensão
  • Operações Sequenciais: O(1) - completamente paralelizável
  • Comprimento Máximo do Caminho: O(1) - constante para qualquer par de posições

Compare com RNNs: O(n) operações sequenciais, O(n) comprimento máximo do caminho, tornando Transformers superiores para sequências longas e computação paralela.

Exemplo Numérico Simples

Considere a frase: "O gato dorme"

Ao processar "gato", a atenção pode calcular:

  • Atenção para "O": 0.1 (baixa relevância)
  • Atenção para "gato": 0.5 (alta - o próprio token)
  • Atenção para "dorme": 0.4 (alta - ação relacionada)

A representação final de "gato" será: 0.1 × VO + 0.5 × Vgato + 0.4 × Vdorme

3.3 Atenção Multi-Cabeça

Em vez de realizar uma única função de atenção, o Transformer emprega atenção multi-cabeça com h camadas de atenção paralelas (cabeças).

Por que Múltiplas Cabeças?

A atenção multi-cabeça permite que o modelo atenda conjuntamente à informação de diferentes subespaços de representação em diferentes posições. Com uma única cabeça de atenção, a média inibe essa capacidade.

  • Cabeça 1: Pode focar em relações sintáticas (concordância sujeito-verbo)
  • Cabeça 2: Pode capturar similaridades semânticas
  • Cabeça 3: Pode identificar dependências de longo alcance
  • Cabeça 4+: Aprendem padrões diversos relevantes para a tarefa
$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O$$

Onde cada cabeça é computada como:

$$\text{head}_i = \text{Attention}(QW^Q_i, KW^K_i, VW^V_i)$$

Detalhes Matemáticos

Cada cabeça tem matrizes de projeção aprendidas:

  • $W^Q_i \in \mathbb{R}^{d_{model} \times d_k}$ - Projeção de Query para cabeça i
  • $W^K_i \in \mathbb{R}^{d_{model} \times d_k}$ - Projeção de Key para cabeça i
  • $W^V_i \in \mathbb{R}^{d_{model} \times d_v}$ - Projeção de Value para cabeça i
  • $W^O \in \mathbb{R}^{h \cdot d_v \times d_{model}}$ - Projeção de saída

Hiperparâmetros do Artigo:

  • Número de cabeças: h = 8
  • Dimensão do modelo: dmodel = 512
  • Dimensão Key/Query: dk = dv = dmodel/h = 64

Devido à dimensão reduzida de cada cabeça, o custo computacional total é similar à atenção de cabeça única com dimensionalidade completa, enquanto fornece os benefícios de múltiplos subespaços de representação.

💻 Multi-Head Attention em PyTorch

import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        # Matrizes de projeção para Q, K, V
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        
        # Matriz de saída
        self.W_o = nn.Linear(d_model, d_model)
        
    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        # Q, K, V: (batch, num_heads, seq_len, d_k)
        
        # Calcula scores de atenção
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        # Aplica máscara (se fornecida)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        # Aplica softmax
        attention_weights = torch.softmax(scores, dim=-1)
        
        # Multiplica por V
        output = torch.matmul(attention_weights, V)
        
        return output, attention_weights
    
    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)
        
        # Projeções lineares
        Q = self.W_q(Q)  # (batch, seq_len, d_model)
        K = self.W_k(K)
        V = self.W_v(V)
        
        # Divide em múltiplas cabeças
        # (batch, seq_len, d_model) -> (batch, seq_len, num_heads, d_k)
        Q = Q.view(batch_size, -1, self.num_heads, self.d_k)
        K = K.view(batch_size, -1, self.num_heads, self.d_k)
        V = V.view(batch_size, -1, self.num_heads, self.d_k)
        
        # Transpõe para (batch, num_heads, seq_len, d_k)
        Q = Q.transpose(1, 2)
        K = K.transpose(1, 2)
        V = V.transpose(1, 2)
        
        # Calcula atenção
        output, attention_weights = self.scaled_dot_product_attention(Q, K, V, mask)
        
        # Concatena cabeças
        # (batch, num_heads, seq_len, d_k) -> (batch, seq_len, d_model)
        output = output.transpose(1, 2).contiguous()
        output = output.view(batch_size, -1, self.d_model)
        
        # Projeção final
        output = self.W_o(output)
        
        return output, attention_weights

4. Arquitetura do Transformer

4.1 Visão Geral

O Transformer completo é uma arquitetura encoder-decoder composta por:

Componentes Principais

  • Encoder: Processa a sequência de entrada
  • Decoder: Gera a sequência de saída
  • Positional Encoding: Adiciona informação de posição
  • Multi-Head Attention: Captura relações entre tokens
  • Feed-Forward Networks: Transforma representações
  • Layer Normalization: Estabiliza treinamento
  • Residual Connections: Facilita propagação do gradiente

4.2 Encoder

O encoder é composto por uma pilha de N camadas idênticas (geralmente N=6). Cada camada tem dois sub-componentes:

Estrutura de uma Camada do Encoder

1. Multi-Head Self-Attention:

  • Permite que cada posição atenda todas as posições
  • Captura relações e dependências

2. Feed-Forward Network:

  • Duas camadas lineares com ReLU no meio
  • Aplicada independentemente para cada posição

Ambos têm:

  • Residual Connection: adiciona entrada à saída
  • Layer Normalization: normaliza após a adição
$$\text{Output}_1 = \text{LayerNorm}(x + \text{MultiHeadAttention}(x))$$ $$\text{Output}_2 = \text{LayerNorm}(\text{Output}_1 + \text{FFN}(\text{Output}_1))$$

💻 Encoder Layer em PyTorch

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        
        # Multi-Head Attention
        self.self_attention = MultiHeadAttention(d_model, num_heads)
        
        # Feed-Forward Network
        self.feed_forward = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )
        
        # Layer Normalization
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        
        # Dropout
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
    
    def forward(self, x, mask=None):
        # Self-Attention com residual connection
        attn_output, _ = self.self_attention(x, x, x, mask)
        x = self.norm1(x + self.dropout1(attn_output))
        
        # Feed-Forward com residual connection
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout2(ff_output))
        
        return x


class Encoder(nn.Module):
    def __init__(self, num_layers, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        
        # Pilha de camadas encoder
        self.layers = nn.ModuleList([
            EncoderLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        
    def forward(self, x, mask=None):
        # Passa por todas as camadas
        for layer in self.layers:
            x = layer(x, mask)
        
        return x

4.3 Decoder

O decoder também é uma pilha de N camadas, mas com três sub-componentes:

Estrutura de uma Camada do Decoder

1. Masked Multi-Head Self-Attention:

  • Similar ao encoder, mas com máscara
  • Impede que posições atendam a posições futuras
  • Necessário para treinamento autoregressivo

2. Multi-Head Cross-Attention:

  • Queries vêm do decoder
  • Keys e Values vêm do encoder
  • Permite que o decoder "olhe" para a entrada

3. Feed-Forward Network:

  • Mesma estrutura do encoder

Masking no Decoder

Durante o treinamento, o decoder vê toda a sequência alvo de uma vez, mas precisamos impedir que ele "trapaceie" olhando tokens futuros. A máscara garante que a predição na posição i só pode depender das posições anteriores (< i).

💻 Decoder Layer em PyTorch

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        
        # Masked Self-Attention
        self.self_attention = MultiHeadAttention(d_model, num_heads)
        
        # Cross-Attention (com encoder)
        self.cross_attention = MultiHeadAttention(d_model, num_heads)
        
        # Feed-Forward
        self.feed_forward = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )
        
        # Layer Norms
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        
        # Dropouts
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
        self.dropout3 = nn.Dropout(dropout)
    
    def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
        # Masked Self-Attention
        attn_output, _ = self.self_attention(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout1(attn_output))
        
        # Cross-Attention com encoder
        attn_output, _ = self.cross_attention(
            x, encoder_output, encoder_output, src_mask
        )
        x = self.norm2(x + self.dropout2(attn_output))
        
        # Feed-Forward
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout3(ff_output))
        
        return x

4.4 Codificação Posicional

Como o Transformer não contém recorrência ou convolução, ele não tem noção inerente de ordem ou posição dos tokens. Codificações posicionais injetam informação sobre a posição relativa ou absoluta dos tokens na sequência.

Por que Informação Posicional é Crítica

Sem codificação posicional, "O gato persegue o rato" e "O rato persegue o gato" teriam representações idênticas! A ordem das palavras é crucial na linguagem natural.

O artigo usa funções senoidais de diferentes frequências para codificação posicional:

$$PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$ $$PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

Onde:

  • pos = posição na sequência (0 até seq_len - 1)
  • i = índice da dimensão (0 até dmodel/2 - 1)
  • Dimensões pares usam seno, dimensões ímpares usam cosseno

Propriedades da Codificação Senoidal

Esta formulação foi escolhida por várias razões:

1. Transformações Lineares:

  • Para qualquer deslocamento fixo k, PEpos+k pode ser representado como função linear de PEpos
  • O modelo pode facilmente aprender a atender por posições relativas
Usando a identidade trigonométrica: $$\sin(\alpha + \beta) = \sin(\alpha)\cos(\beta) + \cos(\alpha)\sin(\beta)$$ $$\cos(\alpha + \beta) = \cos(\alpha)\cos(\beta) - \sin(\alpha)\sin(\beta)$$

2. Codificação Única:

  • Cada posição recebe um vetor de codificação único
  • Diferentes dimensões têm comprimentos de onda formando progressão geométrica de 2π a 10000·2π

3. Extrapolação:

  • Pode generalizar para comprimentos de sequência maiores que os exemplos de treinamento
  • Embeddings posicionais aprendidos mostraram desempenho similar mas sem garantia de extrapolação

4. Valores Limitados:

  • Todos os valores no intervalo [-1, 1]
  • Estável e bem comportado durante o treinamento

Intuição: Progressão de Comprimento de Onda

Pense na codificação posicional como um relógio com múltiplos ponteiros:

  • Dimensões rápidas (alta frequência): Mudam rapidamente, codificam posição detalhada
  • Dimensões lentas (baixa frequência): Mudam lentamente, codificam posição grosseira
  • Juntas, fornecem um "timestamp" único para cada posição

As primeiras dimensões completam muitos ciclos sobre a sequência (como segundos em um relógio), enquanto dimensões posteriores completam menos ciclos (como horas).

💻 Positional Encoding em PyTorch

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super().__init__()
        
        # Cria matriz de positional encoding
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        
        # Calcula divisor
        div_term = torch.exp(
            torch.arange(0, d_model, 2).float() * 
            (-math.log(10000.0) / d_model)
        )
        
        # Aplica seno para posições pares
        pe[:, 0::2] = torch.sin(position * div_term)
        # Aplica cosseno para posições ímpares
        pe[:, 1::2] = torch.cos(position * div_term)
        
        # Adiciona dimensão batch
        pe = pe.unsqueeze(0)
        
        # Registra como buffer (não é parâmetro treinável)
        self.register_buffer('pe', pe)
    
    def forward(self, x):
        # Adiciona positional encoding ao input
        # x: (batch, seq_len, d_model)
        x = x + self.pe[:, :x.size(1), :]
        return x

4.5 Feed-Forward Networks

Cada camada contém uma rede feed-forward totalmente conectada que é aplicada independentemente a cada posição:

$$\text{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2$$

Características:

  • Duas transformações lineares com ReLU no meio
  • Dimensão interna tipicamente 4× maior (d_ff = 4 × d_model)
  • Mesmos pesos para todas as posições, mas diferentes entre camadas
  • Adiciona capacidade de transformação não-linear

5. Implementação Prática

5.1 Implementação Completa em PyTorch

Vamos juntar todos os componentes em um Transformer completo:

💻 Transformer Completo

class Transformer(nn.Module):
    def __init__(
        self,
        src_vocab_size,
        tgt_vocab_size,
        d_model=512,
        num_heads=8,
        num_encoder_layers=6,
        num_decoder_layers=6,
        d_ff=2048,
        dropout=0.1,
        max_len=5000
    ):
        super().__init__()
        
        # Embeddings
        self.src_embedding = nn.Embedding(src_vocab_size, d_model)
        self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)
        
        # Positional Encoding
        self.positional_encoding = PositionalEncoding(d_model, max_len)
        
        # Encoder e Decoder
        self.encoder = Encoder(
            num_encoder_layers, d_model, num_heads, d_ff, dropout
        )
        self.decoder = Decoder(
            num_decoder_layers, d_model, num_heads, d_ff, dropout
        )
        
        # Camada final de projeção
        self.output_projection = nn.Linear(d_model, tgt_vocab_size)
        
        # Dropout
        self.dropout = nn.Dropout(dropout)
        
        # Fator de escala para embeddings
        self.d_model = d_model
        
    def make_src_mask(self, src):
        # Cria máscara para padding no source
        # src: (batch, src_len)
        src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
        # (batch, 1, 1, src_len)
        return src_mask
    
    def make_tgt_mask(self, tgt):
        # Cria máscara para padding e look-ahead no target
        # tgt: (batch, tgt_len)
        batch_size, tgt_len = tgt.shape
        
        # Máscara de padding
        tgt_pad_mask = (tgt != 0).unsqueeze(1).unsqueeze(2)
        # (batch, 1, 1, tgt_len)
        
        # Máscara look-ahead (triangular inferior)
        tgt_sub_mask = torch.tril(
            torch.ones((tgt_len, tgt_len), device=tgt.device)
        ).bool()
        # (tgt_len, tgt_len)
        
        # Combina máscaras
        tgt_mask = tgt_pad_mask & tgt_sub_mask
        # (batch, 1, tgt_len, tgt_len)
        
        return tgt_mask
    
    def forward(self, src, tgt):
        # src: (batch, src_len)
        # tgt: (batch, tgt_len)
        
        # Cria máscaras
        src_mask = self.make_src_mask(src)
        tgt_mask = self.make_tgt_mask(tgt)
        
        # Embeddings com scaling
        src_embedded = self.dropout(
            self.positional_encoding(
                self.src_embedding(src) * math.sqrt(self.d_model)
            )
        )
        tgt_embedded = self.dropout(
            self.positional_encoding(
                self.tgt_embedding(tgt) * math.sqrt(self.d_model)
            )
        )
        
        # Encoder
        encoder_output = self.encoder(src_embedded, src_mask)
        
        # Decoder
        decoder_output = self.decoder(
            tgt_embedded, encoder_output, src_mask, tgt_mask
        )
        
        # Projeção final
        output = self.output_projection(decoder_output)
        
        return output


class Decoder(nn.Module):
    def __init__(self, num_layers, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        
        self.layers = nn.ModuleList([
            DecoderLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        
    def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
        for layer in self.layers:
            x = layer(x, encoder_output, src_mask, tgt_mask)
        return x

5.2 Exemplo de Uso

💻 Treinando um Transformer

# Hiperparâmetros
src_vocab_size = 10000
tgt_vocab_size = 10000
d_model = 512
num_heads = 8
num_layers = 6
d_ff = 2048
dropout = 0.1
batch_size = 32
max_len = 100

# Cria modelo
model = Transformer(
    src_vocab_size=src_vocab_size,
    tgt_vocab_size=tgt_vocab_size,
    d_model=d_model,
    num_heads=num_heads,
    num_encoder_layers=num_layers,
    num_decoder_layers=num_layers,
    d_ff=d_ff,
    dropout=dropout,
    max_len=max_len
)

# Move para GPU se disponível
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)

# Otimizador e loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
criterion = nn.CrossEntropyLoss(ignore_index=0)  # Ignora padding

# Exemplo de dados (toy example)
src = torch.randint(1, src_vocab_size, (batch_size, 50)).to(device)
tgt = torch.randint(1, tgt_vocab_size, (batch_size, 50)).to(device)

# Forward pass
model.train()
output = model(src, tgt[:, :-1])  # Teacher forcing

# Calcula loss
loss = criterion(
    output.reshape(-1, tgt_vocab_size),
    tgt[:, 1:].reshape(-1)
)

# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()

print(f'Loss: {loss.item():.4f}')

# Inferência (geração)
model.eval()
with torch.no_grad():
    # Começa com token de início
    generated = torch.tensor([[1]]).to(device)  # [BOS]
    
    for _ in range(max_len):
        output = model(src[:1], generated)
        
        # Pega próximo token
        next_token = output[:, -1, :].argmax(dim=-1, keepdim=True)
        
        generated = torch.cat([generated, next_token], dim=1)
        
        # Para se gerar token de fim
        if next_token.item() == 2:  # [EOS]
            break
    
    print(f'Generated sequence: {generated}')

Dicas de Treinamento

  • Learning Rate Scheduling: Use warmup seguido de decay
  • Label Smoothing: Melhora generalização
  • Dropout: Importante para evitar overfitting
  • Gradient Clipping: Estabiliza treinamento
  • Batch Size: Quanto maior, melhor (se couber na memória)

6. Aplicações e Variantes

6.1 BERT (Bidirectional Encoder Representations from Transformers)

Características do BERT

  • Arquitetura: Apenas Encoder (sem Decoder)
  • Treinamento: Masked Language Modeling (MLM) + Next Sentence Prediction (NSP)
  • Bidirectional: Vê contexto completo (esquerda e direita)
  • Uso: Principalmente para tarefas de compreensão (classificação, NER, Q&A)

Masked Language Modeling

Entrada: "O [MASK] está no [MASK]"

Objetivo: Prever "gato" e "telhado"

O modelo aprende representações contextuais ricas ao tentar prever palavras mascaradas usando contexto bidirecional.

💻 Usando BERT com Hugging Face

from transformers import BertTokenizer, BertModel
import torch

# Carrega tokenizer e modelo pré-treinado
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

# Texto de entrada
text = "O Transformer revolucionou o NLP"
inputs = tokenizer(text, return_tensors='pt')

# Forward pass
with torch.no_grad():
    outputs = model(**inputs)

# Obtém representações
last_hidden_states = outputs.last_hidden_state
print(f'Shape: {last_hidden_states.shape}')
# Shape: [1, sequence_length, hidden_size]

6.2 GPT (Generative Pre-trained Transformer)

Características do GPT

  • Arquitetura: Apenas Decoder (sem Encoder)
  • Treinamento: Causal Language Modeling (prever próxima palavra)
  • Unidirecional: Vê apenas contexto à esquerda
  • Uso: Principalmente para geração de texto

Evolução do GPT

  • GPT-1 (2018): 117M parâmetros
  • GPT-2 (2019): 1.5B parâmetros
  • GPT-3 (2020): 175B parâmetros
  • GPT-4 (2023): Multimodal, parâmetros não divulgados

Cada versão demonstrou capacidades emergentes impressionantes com escala crescente.

💻 Geração de Texto com GPT-2

from transformers import GPT2LMHeadModel, GPT2Tokenizer

# Carrega modelo e tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')

# Prompt inicial
prompt = "Transformers são redes neurais que"
inputs = tokenizer(prompt, return_tensors='pt')

# Gera texto
outputs = model.generate(
    inputs['input_ids'],
    max_length=100,
    num_return_sequences=1,
    temperature=0.7,
    top_k=50,
    top_p=0.95,
    do_sample=True
)

# Decodifica resultado
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(generated_text)

6.3 Vision Transformers (ViT)

Transformers não são limitados a texto! Vision Transformers aplicam a mesma arquitetura a imagens.

Como Funciona o ViT

  1. Divide a imagem em patches: Ex: 224×224 → 196 patches de 16×16
  2. Lineariza cada patch: Flatten e projeção linear
  3. Adiciona positional embeddings: Para manter informação espacial
  4. Adiciona token [CLS]: Para representação global
  5. Aplica Transformer Encoder: Como em BERT
  6. Classifica usando [CLS]: MLP na saída do [CLS] token

Outras Aplicações de Transformers

  • Áudio: Whisper (transcrição de áudio)
  • Multimodal: CLIP (visão + linguagem)
  • Proteínas: AlphaFold (estrutura de proteínas)
  • Código: Codex/Copilot (geração de código)
  • Jogos: Decision Transformers (RL)

🎯 Resumo dos Conceitos Principais

✨ O que Aprendemos

  • Self-Attention: Permite que tokens atendam uns aos outros em paralelo
  • Multi-Head Attention: Múltiplas perspectivas de atenção simultaneamente
  • Positional Encoding: Adiciona informação de posição sem recorrência
  • Encoder-Decoder: Arquitetura poderosa para sequence-to-sequence
  • Paralelização: Processamento eficiente aproveitando GPUs
  • Escalabilidade: Funciona incrivelmente bem com mais dados e parâmetros

🚀 Por que Transformers são Revolucionários

Os Transformers mudaram fundamentalmente a IA por permitirem:

  • Modelos maiores e mais poderosos (GPT-3, GPT-4)
  • Transfer learning efetivo (fine-tuning de modelos pré-treinados)
  • Aplicação além de NLP (visão, áudio, multimodal)
  • Emergência de capacidades com escala

"Attention is All You Need" - realmente era tudo que precisávamos! 🎉