Skip to content

Componentes Fundamentais do Transformer

Neste módulo, vamos explorar os componentes fundamentais da arquitetura Transformer, que revolucionou o processamento de linguagem natural e serve como base para os LLMs modernos. Vamos implementar cada componente do zero, entendendo seu funcionamento interno e como eles se combinam para formar um modelo completo.

Embeddings: A Base da Representação

Antes que um modelo possa processar texto, precisamos converter tokens em representações vetoriais densas chamadas embeddings. Vamos explorar dois tipos essenciais de embeddings usados em Transformers.

Embeddings de Tokens

Os embeddings de tokens mapeiam cada token do vocabulário para um vetor de dimensão fixa. Estes vetores capturam características semânticas e sintáticas dos tokens.

python
import torch
import torch.nn as nn
import math

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size, embedding_dim):
        """
        Inicializa a camada de embedding de tokens.
        
        Args:
            vocab_size: Tamanho do vocabulário
            embedding_dim: Dimensão do embedding
        """
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.embedding_dim = embedding_dim
        
        # Inicialização dos pesos (importante para convergência)
        nn.init.normal_(self.embedding.weight, mean=0, std=embedding_dim ** -0.5)
    
    def forward(self, x):
        """
        Args:
            x: Tensor de índices de tokens de shape (batch_size, seq_len)
            
        Returns:
            Tensor de embeddings de shape (batch_size, seq_len, embedding_dim)
        """
        return self.embedding(x) * math.sqrt(self.embedding_dim)

A multiplicação por sqrt(embedding_dim) é uma técnica de escala que ajuda a estabilizar o treinamento, conforme proposto no artigo original do Transformer.

Embeddings Posicionais

Como a arquitetura Transformer processa todos os tokens em paralelo (sem recorrência), precisamos injetar informação sobre a posição de cada token na sequência. Os embeddings posicionais fazem exatamente isso.

python
class PositionalEncoding(nn.Module):
    def __init__(self, embedding_dim, max_seq_length=5000, dropout=0.1):
        """
        Inicializa a codificação posicional.
        
        Args:
            embedding_dim: Dimensão do embedding
            max_seq_length: Comprimento máximo de sequência suportado
            dropout: Taxa de dropout
        """
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        
        # Criar matriz de codificação posicional
        pe = torch.zeros(max_seq_length, embedding_dim)
        position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, embedding_dim, 2).float() * (-math.log(10000.0) / embedding_dim))
        
        # Aplicar funções seno e cosseno
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        # Adicionar dimensão de batch e registrar como buffer (não é um parâmetro)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
    
    def forward(self, x):
        """
        Args:
            x: Tensor de embeddings de tokens de shape (batch_size, seq_len, embedding_dim)
            
        Returns:
            Tensor com embeddings posicionais adicionados
        """
        x = x + self.pe[:, :x.size(1), :]
        return self.dropout(x)

Os embeddings posicionais usam funções seno e cosseno de diferentes frequências para cada dimensão. Isso permite que o modelo aprenda a atender a posições relativas, mesmo para sequências mais longas do que as vistas durante o treinamento.

Vamos visualizar como os embeddings posicionais se parecem:

python
import matplotlib.pyplot as plt
import numpy as np

def plot_positional_encoding(pe, dim=64, seq_len=100):
    """
    Visualiza os embeddings posicionais.
    
    Args:
        pe: Tensor de embeddings posicionais
        dim: Número de dimensões a mostrar
        seq_len: Comprimento da sequência a mostrar
    """
    plt.figure(figsize=(10, 6))
    pe_slice = pe[0, :seq_len, :dim].numpy()
    plt.imshow(pe_slice, cmap='viridis', aspect='auto')
    plt.xlabel('Dimensão do Embedding')
    plt.ylabel('Posição na Sequência')
    plt.colorbar(label='Valor')
    plt.title('Visualização dos Embeddings Posicionais')
    plt.savefig('positional_encodings.png')
    plt.close()

# Exemplo de uso
# model_dim = 64
# pos_encoder = PositionalEncoding(model_dim, max_seq_length=200)
# pos_encodings = pos_encoder.pe.detach()
# plot_positional_encoding(pos_encodings)

Embeddings Combinados

Na prática, combinamos embeddings de tokens e posicionais para obter a representação completa de entrada:

python
class EmbeddingLayer(nn.Module):
    def __init__(self, vocab_size, embedding_dim, max_seq_length=5000, dropout=0.1):
        """
        Combina embeddings de tokens e posicionais.
        
        Args:
            vocab_size: Tamanho do vocabulário
            embedding_dim: Dimensão do embedding
            max_seq_length: Comprimento máximo de sequência
            dropout: Taxa de dropout
        """
        super(EmbeddingLayer, self).__init__()
        self.token_embedding = TokenEmbedding(vocab_size, embedding_dim)
        self.positional_encoding = PositionalEncoding(embedding_dim, max_seq_length, dropout)
    
    def forward(self, x):
        """
        Args:
            x: Tensor de índices de tokens de shape (batch_size, seq_len)
            
        Returns:
            Tensor de embeddings combinados de shape (batch_size, seq_len, embedding_dim)
        """
        x = self.token_embedding(x)
        x = self.positional_encoding(x)
        return x

Mecanismo de Atenção Multi-Cabeça

O coração do Transformer é o mecanismo de atenção, que permite que o modelo foque em diferentes partes da entrada ao gerar cada parte da saída.

Atenção Scaled Dot-Product

Primeiro, implementamos a atenção scaled dot-product básica:

python
def scaled_dot_product_attention(query, key, value, mask=None):
    """
    Calcula a atenção scaled dot-product.
    
    Args:
        query: Tensor de shape (..., seq_len_q, d_k)
        key: Tensor de shape (..., seq_len_k, d_k)
        value: Tensor de shape (..., seq_len_k, d_v)
        mask: Tensor opcional para mascarar posições inválidas
        
    Returns:
        output: Tensor de shape (..., seq_len_q, d_v)
        attention_weights: Tensor de shape (..., seq_len_q, seq_len_k)
    """
    d_k = query.size(-1)
    
    # Produto escalar de query e key
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    
    # Aplicar máscara (se fornecida)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    
    # Aplicar softmax para obter pesos de atenção
    attention_weights = torch.nn.functional.softmax(scores, dim=-1)
    
    # Aplicar pesos aos values
    output = torch.matmul(attention_weights, value)
    
    return output, attention_weights

Atenção Multi-Cabeça

A atenção multi-cabeça permite que o modelo atenda a diferentes representações subspace simultaneamente:

python
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads, dropout=0.1):
        """
        Inicializa o módulo de atenção multi-cabeça.
        
        Args:
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            dropout: Taxa de dropout
        """
        super(MultiHeadAttention, self).__init__()
        assert d_model % num_heads == 0, "d_model deve ser divisível por num_heads"
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # Dimensão por cabeça
        
        # Projeções lineares para Q, K, V
        self.wq = nn.Linear(d_model, d_model)
        self.wk = nn.Linear(d_model, d_model)
        self.wv = nn.Linear(d_model, d_model)
        
        # Projeção de saída
        self.wo = nn.Linear(d_model, d_model)
        
        self.dropout = nn.Dropout(dropout)
    
    def split_heads(self, x):
        """
        Divide o último dimensão em (num_heads, d_k)
        e transpõe para (batch_size, num_heads, seq_len, d_k)
        """
        batch_size, seq_length = x.size(0), x.size(1)
        x = x.view(batch_size, seq_length, self.num_heads, self.d_k)
        return x.permute(0, 2, 1, 3)
    
    def combine_heads(self, x):
        """
        Combina as cabeças de atenção.
        Transpõe de (batch_size, num_heads, seq_len, d_k)
        para (batch_size, seq_len, d_model)
        """
        batch_size, _, seq_length, _ = x.size()
        x = x.permute(0, 2, 1, 3)
        return x.contiguous().view(batch_size, seq_length, self.d_model)
    
    def forward(self, query, key, value, mask=None):
        """
        Args:
            query: Tensor de shape (batch_size, seq_len_q, d_model)
            key: Tensor de shape (batch_size, seq_len_k, d_model)
            value: Tensor de shape (batch_size, seq_len_v, d_model)
            mask: Tensor opcional para mascarar posições inválidas
            
        Returns:
            output: Tensor de shape (batch_size, seq_len_q, d_model)
            attention_weights: Tensor de shape (batch_size, num_heads, seq_len_q, seq_len_k)
        """
        batch_size = query.size(0)
        
        # Projeções lineares e divisão em cabeças
        q = self.split_heads(self.wq(query))  # (batch_size, num_heads, seq_len_q, d_k)
        k = self.split_heads(self.wk(key))    # (batch_size, num_heads, seq_len_k, d_k)
        v = self.split_heads(self.wv(value))  # (batch_size, num_heads, seq_len_v, d_k)
        
        # Atenção scaled dot-product para cada cabeça
        output, attention_weights = scaled_dot_product_attention(q, k, v, mask)
        
        # Combinar cabeças
        output = self.combine_heads(output)  # (batch_size, seq_len_q, d_model)
        
        # Projeção final
        output = self.wo(output)
        output = self.dropout(output)
        
        return output, attention_weights

Feed-Forward Networks

Após a atenção, cada posição na sequência passa por uma rede feed-forward idêntica:

python
class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        """
        Inicializa a rede feed-forward.
        
        Args:
            d_model: Dimensão do modelo
            d_ff: Dimensão interna da feed-forward
            dropout: Taxa de dropout
        """
        super(PositionwiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)
        self.activation = nn.GELU()  # GELU é comum em modelos modernos como GPT
    
    def forward(self, x):
        """
        Args:
            x: Tensor de shape (batch_size, seq_len, d_model)
            
        Returns:
            Tensor de shape (batch_size, seq_len, d_model)
        """
        x = self.linear1(x)
        x = self.activation(x)
        x = self.dropout(x)
        x = self.linear2(x)
        return self.dropout(x)

Normalização de Camada

A normalização de camada é crucial para estabilizar o treinamento de Transformers:

python
class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        """
        Inicializa a normalização de camada.
        
        Args:
            features: Número de features
            eps: Valor epsilon para estabilidade numérica
        """
        super(LayerNorm, self).__init__()
        self.gamma = nn.Parameter(torch.ones(features))
        self.beta = nn.Parameter(torch.zeros(features))
        self.eps = eps
    
    def forward(self, x):
        """
        Args:
            x: Tensor de shape (batch_size, seq_len, features)
            
        Returns:
            Tensor normalizado de mesma shape
        """
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.gamma * (x - mean) / (std + self.eps) + self.beta

Montagem do Encoder

Agora, vamos combinar esses componentes para formar um bloco de encoder completo:

python
class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        """
        Inicializa uma camada do encoder.
        
        Args:
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            dropout: Taxa de dropout
        """
        super(EncoderLayer, self).__init__()
        
        self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
        self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
        
        self.norm1 = LayerNorm(d_model)
        self.norm2 = LayerNorm(d_model)
        
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, mask=None):
        """
        Args:
            x: Tensor de shape (batch_size, seq_len, d_model)
            mask: Tensor opcional para mascarar posições inválidas
            
        Returns:
            Tensor de shape (batch_size, seq_len, d_model)
        """
        # Auto-atenção com conexão residual e normalização
        attn_output, _ = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        
        # Feed-forward com conexão residual e normalização
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        
        return x

class Encoder(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_seq_length=5000, dropout=0.1):
        """
        Inicializa o encoder completo.
        
        Args:
            vocab_size: Tamanho do vocabulário
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            num_layers: Número de camadas do encoder
            max_seq_length: Comprimento máximo de sequência
            dropout: Taxa de dropout
        """
        super(Encoder, self).__init__()
        
        self.embedding_layer = EmbeddingLayer(vocab_size, d_model, max_seq_length, dropout)
        
        self.layers = nn.ModuleList([
            EncoderLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        
        self.norm = LayerNorm(d_model)
    
    def forward(self, x, mask=None):
        """
        Args:
            x: Tensor de índices de tokens de shape (batch_size, seq_len)
            mask: Tensor opcional para mascarar posições inválidas
            
        Returns:
            Tensor de shape (batch_size, seq_len, d_model)
        """
        # Aplicar embeddings
        x = self.embedding_layer(x)
        
        # Passar por cada camada do encoder
        for layer in self.layers:
            x = layer(x, mask)
        
        # Normalização final
        return self.norm(x)

Implementação do Decoder

O decoder é semelhante ao encoder, mas com uma camada adicional de atenção cruzada:

python
class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        """
        Inicializa uma camada do decoder.
        
        Args:
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            dropout: Taxa de dropout
        """
        super(DecoderLayer, self).__init__()
        
        self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
        self.cross_attn = MultiHeadAttention(d_model, num_heads, dropout)
        self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
        
        self.norm1 = LayerNorm(d_model)
        self.norm2 = LayerNorm(d_model)
        self.norm3 = LayerNorm(d_model)
        
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, enc_output, look_ahead_mask=None, padding_mask=None):
        """
        Args:
            x: Tensor de shape (batch_size, seq_len, d_model)
            enc_output: Saída do encoder de shape (batch_size, enc_seq_len, d_model)
            look_ahead_mask: Máscara para ocultar tokens futuros
            padding_mask: Máscara para ocultar tokens de padding
            
        Returns:
            Tensor de shape (batch_size, seq_len, d_model)
        """
        # Auto-atenção mascarada
        attn1_output, _ = self.self_attn(x, x, x, look_ahead_mask)
        x = self.norm1(x + self.dropout(attn1_output))
        
        # Atenção cruzada com saída do encoder
        attn2_output, _ = self.cross_attn(x, enc_output, enc_output, padding_mask)
        x = self.norm2(x + self.dropout(attn2_output))
        
        # Feed-forward
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))
        
        return x

class Decoder(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_seq_length=5000, dropout=0.1):
        """
        Inicializa o decoder completo.
        
        Args:
            vocab_size: Tamanho do vocabulário
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            num_layers: Número de camadas do decoder
            max_seq_length: Comprimento máximo de sequência
            dropout: Taxa de dropout
        """
        super(Decoder, self).__init__()
        
        self.embedding_layer = EmbeddingLayer(vocab_size, d_model, max_seq_length, dropout)
        
        self.layers = nn.ModuleList([
            DecoderLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        
        self.norm = LayerNorm(d_model)
    
    def forward(self, x, enc_output, look_ahead_mask=None, padding_mask=None):
        """
        Args:
            x: Tensor de índices de tokens de shape (batch_size, seq_len)
            enc_output: Saída do encoder de shape (batch_size, enc_seq_len, d_model)
            look_ahead_mask: Máscara para ocultar tokens futuros
            padding_mask: Máscara para ocultar tokens de padding
            
        Returns:
            Tensor de shape (batch_size, seq_len, d_model)
        """
        # Aplicar embeddings
        x = self.embedding_layer(x)
        
        # Passar por cada camada do decoder
        for layer in self.layers:
            x = layer(x, enc_output, look_ahead_mask, padding_mask)
        
        # Normalização final
        return self.norm(x)

Atenção Mascarada

No decoder, precisamos garantir que cada posição só possa atender a posições anteriores (para preservar a natureza autoregressiva do modelo). Para isso, usamos uma máscara "look-ahead":

python
def create_masks(src, tgt=None):
    """
    Cria máscaras para encoder e decoder.
    
    Args:
        src: Tensor de índices de tokens de entrada de shape (batch_size, src_seq_len)
        tgt: Tensor opcional de índices de tokens alvo de shape (batch_size, tgt_seq_len)
        
    Returns:
        Dicionário de máscaras
    """
    # Máscara de padding para src (1 para tokens reais, 0 para padding)
    src_mask = (src != 0).unsqueeze(1).unsqueeze(2)  # (batch_size, 1, 1, src_seq_len)
    
    masks = {'src_mask': src_mask}
    
    if tgt is not None:
        # Máscara de padding para tgt
        tgt_padding_mask = (tgt != 0).unsqueeze(1).unsqueeze(2)  # (batch_size, 1, 1, tgt_seq_len)
        
        # Máscara look-ahead para tgt
        tgt_seq_len = tgt.size(1)
        look_ahead_mask = torch.triu(torch.ones((tgt_seq_len, tgt_seq_len)), diagonal=1).bool()
        look_ahead_mask = look_ahead_mask.unsqueeze(0).unsqueeze(0)  # (1, 1, tgt_seq_len, tgt_seq_len)
        
        # Combinar máscaras de padding e look-ahead
        combined_mask = tgt_padding_mask & ~look_ahead_mask
        
        masks.update({
            'tgt_mask': combined_mask,
            'memory_mask': tgt_padding_mask
        })
    
    return masks

Montagem do Transformer Completo

Finalmente, vamos juntar todos os componentes para formar um modelo Transformer completo:

python
class Transformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model=512, num_heads=8, d_ff=2048, 
                 num_encoder_layers=6, num_decoder_layers=6, max_seq_length=5000, dropout=0.1):
        """
        Inicializa o modelo Transformer completo.
        
        Args:
            src_vocab_size: Tamanho do vocabulário de entrada
            tgt_vocab_size: Tamanho do vocabulário de saída
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            num_encoder_layers: Número de camadas do encoder
            num_decoder_layers: Número de camadas do decoder
            max_seq_length: Comprimento máximo de sequência
            dropout: Taxa de dropout
        """
        super(Transformer, self).__init__()
        
        self.encoder = Encoder(
            src_vocab_size, d_model, num_heads, d_ff, 
            num_encoder_layers, max_seq_length, dropout
        )
        
        self.decoder = Decoder(
            tgt_vocab_size, d_model, num_heads, d_ff, 
            num_decoder_layers, max_seq_length, dropout
        )
        
        self.final_layer = nn.Linear(d_model, tgt_vocab_size)
    
    def forward(self, src, tgt):
        """
        Args:
            src: Tensor de índices de tokens de entrada de shape (batch_size, src_seq_len)
            tgt: Tensor de índices de tokens alvo de shape (batch_size, tgt_seq_len)
            
        Returns:
            Tensor de logits de shape (batch_size, tgt_seq_len, tgt_vocab_size)
        """
        # Criar máscaras
        masks = create_masks(src, tgt)
        
        # Codificar a entrada
        enc_output = self.encoder(src, masks['src_mask'])
        
        # Decodificar
        dec_output = self.decoder(
            tgt, enc_output, 
            masks.get('tgt_mask'), masks.get('memory_mask')
        )
        
        # Projeção final para obter logits
        logits = self.final_layer(dec_output)
        
        return logits

Modelo de Linguagem Causal (Apenas Decoder)

Para LLMs como GPT, geralmente usamos apenas a parte decoder do Transformer, em uma configuração autoregressiva:

python
class CausalLanguageModel(nn.Module):
    def __init__(self, vocab_size, d_model=768, num_heads=12, d_ff=3072, 
                 num_layers=12, max_seq_length=1024, dropout=0.1):
        """
        Inicializa um modelo de linguagem causal (apenas decoder).
        
        Args:
            vocab_size: Tamanho do vocabulário
            d_model: Dimensão do modelo
            num_heads: Número de cabeças de atenção
            d_ff: Dimensão interna da feed-forward
            num_layers: Número de camadas
            max_seq_length: Comprimento máximo de sequência
            dropout: Taxa de dropout
        """
        super(CausalLanguageModel, self).__init__()
        
        self.embedding_layer = EmbeddingLayer(vocab_size, d_model, max_seq_length, dropout)
        
        # Camadas do decoder (sem atenção cruzada)
        self.layers = nn.ModuleList([
            DecoderOnlyLayer(d_model, num_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        
        self.norm = LayerNorm(d_model)
        self.lm_head = nn.Linear(d_model, vocab_size)
        
        # Compartilhar pesos entre embedding e camada de saída (técnica comum)
        self.lm_head.weight = self.embedding_layer.token_embedding.embedding.weight
    
    def forward(self, x):
        """
        Args:
            x: Tensor de índices de tokens de shape (batch_size, seq_len)
            
        Returns:
            Tensor de logits de shape (batch_size, seq_len, vocab_size)
        """
        # Criar máscara causal
        seq_len = x.size(1)
        mask = torch.triu(torch.ones((seq_len, seq_len)), diagonal=1).bool()
        mask = mask.unsqueeze(0).unsqueeze(0)  # (1, 1, seq_len, seq_len)
        
        # Aplicar embeddings
        x = self.embedding_layer(x)
        
        # Passar por cada camada
        for layer in self.layers:
            x = layer(x, mask)
        
        # Normalização final
        x = self.norm(x)
        
        # Projeção para logits
        logits = self.lm_head(x)
        
        return logits

class DecoderOnlyLayer(nn.Module):
    """
    Camada de decoder sem atenção cruzada, usada em modelos como GPT.
    """
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super(DecoderOnlyLayer, self).__init__()
        
        self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
        self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
        
        self.norm1 = LayerNorm(d_model)
        self.norm2 = LayerNorm(d_model)
        
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, mask=None):
        # Auto-atenção mascarada
        attn_output, _ = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        
        # Feed-forward
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        
        return x

Exemplo de Uso

Vamos ver como usar nosso modelo em um exemplo simples:

python
def test_causal_lm():
    """
    Testa o modelo de linguagem causal.
    """
    # Parâmetros do modelo
    vocab_size = 10000
    d_model = 256
    num_heads = 4
    d_ff = 1024
    num_layers = 4
    max_seq_length = 128
    
    # Criar modelo
    model = CausalLanguageModel(
        vocab_size, d_model, num_heads, d_ff, num_layers, max_seq_length
    )
    
    # Dados de exemplo
    batch_size = 2
    seq_len = 10
    x = torch.randint(1, vocab_size, (batch_size, seq_len))
    
    # Forward pass
    logits = model(x)
    
    print(f"Input shape: {x.shape}")
    print(f"Output logits shape: {logits.shape}")
    
    # Verificar se as dimensões estão corretas
    assert logits.shape == (batch_size, seq_len, vocab_size), "Dimensões incorretas!"
    print("Teste bem-sucedido!")

# Executar teste
# test_causal_lm()

Conclusão

Nesta aula, implementamos os componentes fundamentais da arquitetura Transformer, incluindo:

  1. Embeddings de tokens e posicionais
  2. Mecanismo de atenção multi-cabeça
  3. Redes feed-forward
  4. Normalização de camada
  5. Blocos de encoder e decoder
  6. Modelo Transformer completo
  7. Modelo de linguagem causal (apenas decoder)

Estes componentes formam a base dos LLMs modernos como GPT, BERT e T5. Na próxima aula, vamos explorar como otimizar esses modelos para funcionar em hardware com limitações de memória.

Exercícios Práticos

  1. Implemente uma versão simplificada do modelo CausalLanguageModel com apenas 2 camadas e teste-o com uma sequência curta.
  2. Modifique o código para adicionar residual connections antes da normalização (pré-normalização), uma técnica usada em modelos como GPT-2.
  3. Implemente uma função para gerar texto autoregressivamente usando o modelo CausalLanguageModel.
  4. Experimente com diferentes funções de ativação (ReLU, Swish) na camada feed-forward e compare com GELU.
  5. Visualize os pesos de atenção gerados pelo modelo para uma sequência de exemplo.

Todos os direitos reservados.