Voltar ao blog

Fine-Tuning de Modelos de Linguagem: Criando um LLM Personalizado com PyTorch

Fine-Tuning de Modelos de Linguagem: Criando um LLM Personalizado com PyTorch

Introdução

Nos últimos anos, Modelos de Linguagem de Grande Porte (os chamados LLMs, do inglês Large Language Models), baseados na arquitetura Transformer, vêm revolucionando o processamento de linguagem natural. Exemplos famosos incluem o GPT, LLaMA, Falcon e muitos outros. Esses modelos são pré-treinados em grandes quantidades de texto e conseguem tarefas como tradução, sumarização e geração de texto de maneira impressionante. Porém, nem sempre um modelo geral atende perfeitamente às necessidades de um caso específico. É aí que entra o fine-tuning: o ajuste fino de um modelo pré-treinado em um conjunto de dados específico, adaptando-o a um domínio ou tarefa particular.

Neste artigo, você aprenderá passo a passo como criar um LLM personalizado usando PyTorch e a biblioteca Transformers da Hugging Face. Vamos abordar desde a preparação dos dados e do ambiente de desenvolvimento até o treinamento e avaliação do modelo ajustado. Em resumo, cobriremos:

  • Preparar e carregar dados adequados (formato e tokenização).
  • Configurar o ambiente Python com as bibliotecas necessárias.
  • Carregar um modelo pré-treinado e seu tokenizador correspondente.
  • Executar o fine-tuning usando o Trainer da Hugging Face ou loops manuais em PyTorch.
  • Aplicar boas práticas avançadas (como quantização e LoRA) para lidar com grandes modelos.
  • Avaliar o modelo personalizado com métricas e exemplos de geração de texto.

Vamos começar entendendo o que é um modelo de linguagem e como funcionam os Transformers!

Modelos de Linguagem e Transformers

O que é um Modelo de Linguagem?

Um Modelo de Linguagem é um modelo estatístico ou de aprendizado de máquina que consegue prever ou gerar texto com base no contexto. Simplificando, ele tenta responder: "Qual a probabilidade da próxima palavra ser X, dado as palavras anteriores?" Por exemplo, num texto em português, após "O gato está em cima do", é provável que a próxima palavra seja "muro" ou "teto". Modelos de linguagem aprendem essas probabilidades treinando-se em grandes quantidades de texto (como artigos da Wikipédia, livros, sites, etc.).

Quando chamamos um modelo de linguagem de "Grande Porte" ou LLM, significa que ele tem bilhões de parâmetros e foi treinado com muito texto. Esses LLMs, como o GPT-2/3/4, LLaMA ou Falcon, já carregam um vasto conhecimento sobre gramática, fatos gerais, estilos de escrita, etc. Porém, um ponto-chave é que esse aprendizado é genérico.

Arquitetura Transformer em Resumo

A maioria dos LLMs modernos é baseada na arquitetura Transformer (apresentada por Vaswani et al. em 2017). O Transformer foi um grande avanço porque consegue analisar o contexto completo de uma sequência de palavras de forma paralela, graças ao mecanismo de atenção. A atenção permite que o modelo considere todas as palavras do contexto ao mesmo tempo, atribuindo mais ou menos peso a cada uma conforme a relevância para a previsão.

Em termos didáticos, você pode imaginar um Transformer como um grupo de leitores focados: cada camada do modelo decide quais palavras são mais importantes para entender a frase. Camadas iniciais aprendem padrões simples (como gramática básica), enquanto camadas mais profundas capturam conceitos mais abstratos. Ao final, o modelo consegue prever a próxima palavra (ou preencher lacunas) com base em todo o contexto dado.

Analogias simplificadas ajudam aqui: Treinar um modelo profundo do zero é como construir uma biblioteca inteira de livros do nada. Já o fine-tuning é ter uma biblioteca (o modelo pré-treinado) e adicionar livros ou anotações específicas que você precisa: você não escreve uma enciclopédia inteira novamente, apenas complementa o que já existe. Assim, o modelo já "sabe falar Português", e você só o ensina a falar, digamos, jargão jurídico ou dialeto de Yoda, por exemplo.

Preparando os Dados e o Ambiente

Antes de treinar nosso modelo, precisamos de dados e do ambiente configurados. Essa etapa é como organizar os ingredientes e utensílios antes de "cozinhar" o modelo.

Fontes de Dados e Formatos de Conjunto de Dados

Para fine-tuning, usamos um conjunto de dados com exemplos relevantes ao domínio ou tarefa desejada. Por exemplo:

  • Se quisermos treinar um LLM para gerar resumos científicos, poderíamos usar artigos científicos (ou resumos existentes) como dados.
  • Para um modelo que responde perguntas de suporte ao cliente, usaríamos logs de conversas ou FAQ.
  • Para um modelo de estilo específico (como o yoda do Star Wars), talvez existam datasets de frases no estilo “língua de Yoda”.

Podemos usar datasets públicos ou criar o próprio. A Hugging Face fornece a biblioteca datasets que facilita pegar muitos conjuntos de dados prontos em NLP. Por exemplo, podemos carregar o conjunto de dados WikiText-2 (textos da Wikipédia) assim:

from datasets import load_dataset dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train") print(dataset[0])

Isso mostraria algo como uma sentença da Wikipédia. Se quisermos um conjunto de dados de conversação ou pares Pergunta-Resposta, também há muitos disponíveis ou podemos criá-lo manualmente. É importante que os dados estejam no formato certo:

  • Para tarefas de geração livre (modelagem de linguagem causal), cada exemplo é apenas um texto qualquer do domínio.
  • Para tarefas de instrução/resposta (conversas tipo chat), geralmente usamos pares prompt-completion ou listas de mensagens com papéis de usuário e assistente.

Por exemplo, um formato de entrada e saída para instrução pode ser:

{"prompt": "Explique a teoria da relatividade de forma simples.", "completion": "A teoria da relatividade de Einstein diz que tempo e espaço ..."}

Ou no formato de mensagens para modelos de chat:

{"messages": [ {"role": "user", "content": "Qual é a capital da França?"}, {"role": "assistant", "content": "A capital da França é Paris."} ]}

No caso de um dataset público como as “Sentenças do Yoda” (dvgodoy/yoda_sentences), poderíamos reformatá-lo para esse formato de conversação ou prompt-completion. Veja um exemplo simplificado em código:

from datasets import load_dataset dataset = load_dataset("dvgodoy/yoda_sentences", split="train") # Suponha que 'sentence' seja o texto normal e 'translation_extra' seja a fala do Yoda dataset = dataset.rename_column("sentence", "prompt") dataset = dataset.rename_column("translation_extra", "completion") # Agora cada exemplo tem 'prompt' e 'completion' print(dataset[0])

Assim, garantimos que cada dado de entrada e saída estão claramente identificados. Isso é essencial para treinar o modelo no formato certo.

Pré-processamento e Tokenização

Com os dados carregados (ou coletados), devemos pré-processá-los. Isso inclui casos como remover caracteres inválidos, normalizar áudios, etc., mas no caso de texto simples, geralmente só precisamos assegurar formatação consistente.

O passo crucial é tokenizar o texto: converter cada frase em uma sequência de tokens (geralmente inteiros) que o modelo possa processar. Para isso, usamos o tokenizador correspondente ao modelo que vamos usar. Por exemplo, se formos treinar um GPT-2, usamos o tokenizador do GPT-2:

from transformers import AutoTokenizer model_name = "gpt2" tokenizer = AutoTokenizer.from_pretrained(model_name) tokens = tokenizer("Olá, como vai você?", return_tensors="pt", padding="max_length", truncation=True, max_length=50) print(tokens)

O tokenizador cuida de dividir o texto em pedaços menores (tokens) e, se necessário, adiciona padding (preenchimento) ou truncation (corte) para que cada sequência tenha um tamanho máximo fixo. É importante usar sempre o tokenizador exato do modelo pré-treinado para manter o vocabulário e os códigos de tokens consistentes.

Em muitos casos, aplicamos tokenização em lote, mapeando sobre todo o dataset:

def tokenize_function(examples): return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=128) tokenized_datasets = dataset.map(tokenize_function, batched=True)

No exemplo acima, dataset["text"] seria o campo de texto (pode variar de acordo com o formato). O resultado tokenized_datasets terá tokens prontos para treinar.

Configurando o Ambiente (Instalação de Bibliotecas)

Para fazer o fine-tuning, precisamos de algumas bibliotecas Python:

  • transformers da Hugging Face (modelos e tokenização).
  • datasets da Hugging Face (manipulação de datasets).
  • torch (PyTorch, o framework de deep learning).
  • Opcionalmente, accelerate (otimização de treinamento multi-GPU/TPU) e peft (para LoRA e finetuning eficiente).
  • bitsandbytes se quisermos usar quantização de 4 bits em grandes modelos (maximiza uso de GPU).

Um comando pip típico para instalar tudo seria:

pip install transformers datasets torch accelerate peft bitsandbytes

Verifique sempre a documentação das bibliotecas para compatibilidade de versões (por exemplo, algumas técnicas de quantização funcionam melhor com versões recentes de torch e accelerate).

Com isso, temos o ambiente preparado e os dados prontos para alimentar o modelo!

Carregando e Preparando o Modelo

Escolhendo um Modelo Pré-Treinado

Agora que os dados estão prontos, escolhemos um modelo pré-treinado base. A Hugging Face oferece centenas de modelos no Hugging Face Hub, desde versões pequenas até gigantes de LSD. Para LLMs personalizados, costumamos usar modelos de linguagem causal (auto-regressivos), como:

  • GPT-2 (pequeno, 117M de parâmetros) – bom para começar em exemplos e testes.
  • GPT-NEO/GPT-J (médio-grandes, preferível se precisar de mais capacidade).
  • LLaMA (de Meta, grandes; requer acesso especial).
  • Falcon e outros (variam em tamanho).
  • Modelos brasileiros como BLOOMZ-Base ou outros multilíngues podem ser usados para Português.

Neste artigo usaremos o GPT-2 como exemplo, mas a abordagem vale para qualquer um. Carregamos o modelo e o tokenizador:

from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "gpt2" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name)

Isso baixa os pesos do GPT-2 pré-treinado. O AutoModelForCausalLM é a classe de modelo para tarefas de linguagem causal (que geram texto). Se estivéssemos fazendo classificação, usaríamos AutoModelForSequenceClassification, mas como vamos gerar texto, o nosso é CausalLM.

Se você tiver uma GPU CUDA disponível, mova o modelo para a GPU para treinos mais rápidos:

import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device)

Uma observação importante: alguns modelos (como o GPT-2) não têm um token de padding pré-definido, pois foram originalmente treinados sem padding. Nesses casos, podemos definir o token de padding manualmente, geralmente igual ao token de final de texto (<|endoftext|>). Exemplo:

tokenizer.pad_token = tokenizer.eos_token

Assim, o tokenizador saberá como preencher sequências.

Também podemos considerar otimizações extras:

  • Quantização de Peso: Para grandes modelos (bilhões de parâmetros), usar quantização (converter pesos de 32 bits para 4 ou 8 bits) pode reduzir uso de memória. Com a biblioteca bitsandbytes, fazemos algo como:

    from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True) model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config)

    Isso permite carregar o modelo mais leve (em 4-bit) e treinar em GPUs menores.

  • Mixed-Precision (fp16): Ao criar o Trainer, podemos ativar fp16=True (treinamento em 16 bits), acelerando e consumindo menos memória. Em código PyTorch manual, usar autocast e GradScaler.

Para simplificar, seguiremos com a instalação padrão. Como um modelo GPT-2 tem ~117M de parâmetros (não é tão pesado), não precisamos de LoRA ou quantização. Mas falaremos disso depois como estratégias avançadas!

Processo de Fine-Tuning com PyTorch

Com o modelo e tokenizador prontos, chega o momento de treinar - isto é, adaptar os pesos do modelo aos nossos dados. Existem duas abordagens comuns: usar o Trainer da Hugging Face (que encapsula o loop de treino) ou escrever manualmente um loop de treinamento em PyTorch. Vamos explorar ambas:

Usando o Trainer do Hugging Face

O Trainer fornece uma maneira simples de fazer fine-tuning sem precisar escrever todo o loop. Ele requer:

  1. model: o modelo carregado.
  2. TrainingArguments: parâmetros de treino (taxa de aprendizado, épocas, batch size, etc).
  3. train_dataset (e opcionalmente eval_dataset): nossos dados tokenizados.
  4. Eventualmente, funções de métricas para avaliação.

Por exemplo, vamos supor que tokenizamos o dataset de texto contido em campos "text" para auto-regressão (cada frase continua a próxima sem distinção usuário/assistente). Primeiro, definimos a tokenização como no exemplo anterior e dividimos em treino/val:

from transformers import Trainer, TrainingArguments # Suponha que já tokenizamos e temos tokenized_train, tokenized_val training_args = TrainingArguments( output_dir="./fine-tuned-model", num_train_epochs=3, per_device_train_batch_size=4, per_device_eval_batch_size=4, evaluation_strategy="epoch", # avalia ao fim de cada época save_strategy="epoch", learning_rate=5e-5, logging_dir="./logs", fp16=True # se a GPU suportar ) trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_train, eval_dataset=tokenized_val ) trainer.train()

Esse código automaticamente executa as etapas: carrega batches dos dados, passa pelo modelo, calcula perda (loss), faz backpropagation e atualiza os pesos. Ao final, ele salva checkpoints e você vê métricas no console (loss de treino e validação, etc.).

Importante: para um modelo de linguagem causal (como GPT-2), simplesmente forneça o dataset tokenizado onde os input_ids são as sequências de texto. O Trainer entende que, sem rótulos explícitos, ele usará o próprio texto como rótulos (objetivo = prever próximo token).

Exemplo de Loop Manual em PyTorch

Caso você queira entender por trás dos panos ou precise personalizar, é possível fazer manualmente com PyTorch. A ideia geral é:

  1. Definir DataLoader para os dados tokenizados.
  2. Escolher otimizador (ex: AdamW) e possivelmente scheduler.
  3. Loop de épocas: para cada batch, calcular a perda e atualizar os pesos.

Um exemplo simplificado:

from torch.utils.data import DataLoader from transformers import AdamW train_loader = DataLoader(tokenized_train, batch_size=4, shuffle=True) optimizer = AdamW(model.parameters(), lr=5e-5) model.train() for epoch in range(3): total_loss = 0 for batch in train_loader: # Move dados para GPU input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print(f"Época {epoch+1}, loss médio: {avg_loss:.4f}")

Nesse código:

  • Usamos labels=input_ids para que o modelo calcule internamente a perda de linguagem (cada token é rotulado como o próximo token da sequência).
  • Fazemos loss.backward() e optimizer.step() manualmente.
  • No PyTorch puro, precisamos também cuidar de gradient clipping, schedulers, e talvez torch.cuda.amp para mixed precision, mas o exemplo acima mostra o básico.

Monitoramento e Métricas

Durante o treinamento, é útil acompanhar métricas:

  • A perplexidade (exp do loss médio) é comum para linguagem. Quanto menor, melhor.
  • Se você tiver um conjunto de validação, veja loss em validação a cada época.
  • Para tarefas específicas (classificação, etc.), pode calcular acurácia ou F1.

Com o Trainer, você pode passar uma função compute_metrics para cálculo de métricas extras. No loop manual, você pode rodar o modelo em eval() no final da época para medir no conjunto de validação.

Salvando e Carregando o Modelo Ajustado

Após o treinamento, salve o modelo e o tokenizador para usá-los depois sem ter que treinar de novo:

model.save_pretrained("meu-llm-ajustado") tokenizer.save_pretrained("meu-llm-ajustado")

Para carregar depois, basta:

from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("meu-llm-ajustado") tokenizer = AutoTokenizer.from_pretrained("meu-llm-ajustado")

Assim você mantém todo o aprendizado que fez e pode gerar texto com o modelo especializado.

Estratégias Avançadas e Boas Práticas

Ao trabalhar com fine-tuning de grandes modelos, algumas boas práticas ajudam a economizar recursos e melhorar resultados:

  • Congelar camadas iniciais: Em um LLM gigante, talvez não seja necessário treinar todos os parâmetros. Você pode congelar (desabilitar treinamento) das primeiras camadas e treinar apenas as últimas ou adaptadores. Isso acelera e requer menos memória, pois só atualiza parte dos pesos.
    for name, param in model.named_parameters(): if "ln_f" not in name and "wte" not in name: # exemplo de camadas para não congelar param.requires_grad = False
  • PEFT / LoRA: Parameter-Efficient Fine-Tuning técnicas como LoRA introduzem matrizes de baixo-rank nas camadas, permitindo ajustar apenas alguns parâmetros extras em vez de milhões. Na prática, você usa a biblioteca peft para envolver o modelo. Isso é avançado, mas permite treinar LLMs de bilhões de parâmetros em uma máquina razoável.
  • Gradientes mistos (Mixed precision): Como mencionado, use fp16 para reduzir memória. Com PyTorch, gerencie isso usando torch.cuda.amp.autocast() e GradScaler.
  • Gradients acumulados: Se o batch for muito grande para a GPU, faça acumulação de gradientes: atualize os pesos a cada 2 ou 4 batches somados.
  • Learning Rate e Scheduler: Ajuste a taxa de aprendizado. Muitas vezes usamos um Warmup (aquece o LR) e depois decai ra(Ex: 5% warmup esteiras linha).
  • Regularização & Overfitting: Cuidado com overfitting. Use validação para ver quando parar (early stopping), serre dropout era.
  • Dados balanceados: Se tiver múltiplas categorias ou tipos de prompts, assegure-se de que nenhum segmento esteja vastamente sobre-representado (pode viciar o modelo).

Em resumo, a chave é reduzir o custo sem estragar a qualidade. Realizar fine-tuning em LLMs é, no fundo, manter o conhecimento geral e adicionar conhecimento específico. É como customizar um carro de fábrica para um terreno diferente: mantemos o motor (conhecimento geral), mas trocamos os pneus e ajustes (pesos do modelo) para rodar bem no novo ambiente.

Avaliação do Modelo Ajustado

Depois de treinar, precisamos verificar o desempenho do LLM:

  • Avaliação Quantitativa: Calcule métricas no conjunto de validação que não foi visto no treino. Para geração de texto, geralmente olhamos a perda final ou a perplexidade. Se estivermos fazendo classificação de texto ou respostas específicas, avaliamos acurácia, precisão, etc.
  • Testes de Geração: O ideal é gerar exemplos reais de texto. Exemplo:
    prompt = "Explique o conceito de gravidade em termos simples:" inputs = tokenizer(prompt, return_tensors="pt").to(device) outputs = model.generate(**inputs, max_new_tokens=50, do_sample=True) print(tokenizer.decode(outputs[0], skip_special_tokens=True))
    Veja se as respostas fazem sentido no contexto específico. Se nosso LLM foi ajustado para Yoda, por exemplo, o texto deveria parecer com ele: *"Gravidade, minha jovem padawan. A força que te prende no chão foi" (exemplo).
  • Comparação com Modelo Base: Uma boa prática é comparar o LLM ajustado com o modelo original. Por exemplo, se dermos o mesmo prompt ao GPT-2 base e ao Fine-tuned, as respostas devem mostrar especialização (por exemplo, um estilo ou informação melhor para seu domínio).
  • Teste humano: No fim, a melhor avaliação é pedir para pessoas verem se o modelo atende às expectativas (isso é comum em chatbots, revisão de texto por humanos, etc.).

Em termos de métricas, lembre-se:

  • Perplexidade (PPL): se fosse ~50 no pré-treinado e caiu para ~20 no ajustado em texto do domínio, está indo bem (quanto menor, melhor).
  • Loss de validação: ver se está descendo, sem começar a subir (overfitting).
  • Para casos de prompt-response, medimos adequação (às vezes com BLEU, ROUGE ou métricas embutidas) ou simplesmente observação qualitativa.

Por fim, temos agora um LLM customizado: ele carrega o conhecimento geral do pré-treino e o adaptado ao nosso domínio específico. Podemos usá-lo para gerar conteúdo, responder perguntas ou qualquer tarefa de linguagem programada.

Conclusão

O fine-tuning de modelos de linguagem pré-treinados é uma poderosa forma de criar um LLM personalizado sem precisar de treinamento do zero. Neste artigo, vimos como:

  • Entender modelos Transformer (base dos LLMs) e por que precisamos do fine-tuning.
  • Preparar e tokenizar um conjunto de dados específico.
  • Carregar um modelo e tokenizador pré-treinados usando PyTorch e Hugging Face.
  • Configurar o treinamento (com Trainer ou manual) para ajustar o modelo ao nosso domínio.
  • Utilizar boas práticas (como congelar camadas, LoRA, mixed precision) para tornar o processo eficiente.
  • Avaliar o modelo treinado tanto quantitativamente quanto qualitativamente.

O resultado é um modelo de linguagem adaptado às nossas necessidades (pode ser um chatbot empresarial, um gerador de textos técnicos, um assistente de escrita estilizado, etc.). Esse LLM tienemem conhecimentos gerais e específicos, como se tivesse feito uma "pós-graduação" na nossa tarefa de interesse.

Como próximos passos, você pode explorar:

  • Treinamento de RLHF: usar reforço (Reinforcement Learning) para ajustar ainda mais às preferências e feedback humano.
  • Implantação: colocar o modelo em um servidor ou serviço (por exemplo, usando FastAPI ou Flask para criar uma API).
  • Explorar outros modelos: tentar arquiteturas diferentes (como BERT para tarefas de compreensão, T5 para tradução, etc.).

Parabéns! Agora você tem as bases para criar e treinar um LLM customizado em PyTorch. Com prática e experimentação, poderá aprimorar cada vez mais seu modelo para o caso de uso desejado. Boa codificação e bom fine-tuning!

Quer aprender mais sobre Programação?

Você acabou de ganhar 1 hora com a minha equipe para uma call exclusiva! Vamos entender o seu momento e te mostrar o caminho para se tornar um programador de sucesso. Clique no botão abaixo e agende agora mesmo!