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
Trainerda 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-completionou 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:
transformersda Hugging Face (modelos e tokenização).datasetsda Hugging Face (manipulação de datasets).torch(PyTorch, o framework de deep learning).- Opcionalmente,
accelerate(otimização de treinamento multi-GPU/TPU) epeft(para LoRA e finetuning eficiente). bitsandbytesse 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 ativarfp16=True(treinamento em 16 bits), acelerando e consumindo menos memória. Em código PyTorch manual, usarautocasteGradScaler.
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:
model: o modelo carregado.TrainingArguments: parâmetros de treino (taxa de aprendizado, épocas, batch size, etc).train_dataset(e opcionalmenteeval_dataset): nossos dados tokenizados.- 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 é:
- Definir
DataLoaderpara os dados tokenizados. - Escolher otimizador (ex: AdamW) e possivelmente scheduler.
- 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_idspara que o modelo calcule internamente a perda de linguagem (cada token é rotulado como o próximo token da sequência). - Fazemos
loss.backward()eoptimizer.step()manualmente. - No PyTorch puro, precisamos também cuidar de gradient clipping, schedulers, e talvez
torch.cuda.amppara 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
lossem 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
fp16para reduzir memória. Com PyTorch, gerencie isso usandotorch.cuda.amp.autocast()eGradScaler. - 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:
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).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)) - 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
Trainerou 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!