Ajuste Fino LFM2: QLoRA, DPO e TRL no Colab

Desvendando o LFM2: Um Guia Completo para Ajuste Fino com QLoRA e DPO no Google Colab

A rápida evolução dos modelos de linguagem grande (LLMs) tem democratizado o acesso a tecnologias de ponta, permitindo que desenvolvedores e pesquisadores personalizem esses gigantes para tarefas específicas. Recentemente, o modelo LFM2 emergiu como uma opção promissora, e o processo de ajuste fino (fine-tuning) é crucial para desbloquear seu potencial máximo. Este artigo técnico se aprofunda em um guia passo a passo para ajustar o LFM2 utilizando técnicas avançadas como QLoRA, Supervised Fine-Tuning (SFT) e Direct Preference Optimization (DPO), com a ajuda das bibliotecas TRL (Transformer Reinforcement Learning) e PEFT (Parameter-Efficient Fine-Tuning) da Hugging Face, tudo executado no ambiente acessível do Google Colab. Exploraremos desde a configuração inicial até a avaliação final, fornecendo insights valiosos para quem deseja mergulhar no mundo da personalização de LLMs.

A capacidade de adaptar modelos pré-treinados a domínios ou tarefas específicas é uma pedra angular na pesquisa e desenvolvimento de Inteligência Artificial. O LFM2, como outros LLMs de grande escala, beneficia-se enormemente desse processo, permitindo que ele se especialize em nuances de linguagem, estilos de escrita ou conjuntos de dados particulares. No entanto, o ajuste fino tradicional de modelos tão grandes pode ser proibitivo em termos de recursos computacionais e de memória. É aqui que entram as técnicas de ajuste fino eficiente em parâmetros (PEFT), como o QLoRA, e métodos de otimização baseados em feedback, como o DPO.

Este tutorial foi inspirado por um artigo detalhado que oferece um roteiro prático para essa tarefa. As informações originais foram detalhadas no Artigo de Origem.

Entendendo os Componentes Chave: LFM2, QLoRA, SFT e DPO

O Modelo LFM2: Uma Visão Geral

Embora os detalhes específicos do LFM2 possam variar dependendo da versão e do contexto de sua publicação, geralmente se refere a um modelo de linguagem grande desenvolvido com arquiteturas Transformer, treinado em vastos corpus de texto. A capacidade de um LLM como o LFM2 reside em sua habilidade de compreender e gerar texto coerente e contextualmente relevante. Para aplicações práticas, como chatbots, assistentes de escrita, ferramentas de resumo ou geração de código, o ajuste fino é essencial para alinhar o comportamento do modelo com os requisitos da tarefa.

QLoRA: Ajuste Fino Eficiente em Parâmetros

QLoRA é uma técnica revolucionária que permite o ajuste fino de modelos de linguagem grandes em hardware com recursos limitados. Ela combina várias inovações:

  • Quantização de 4 bits: Reduz drasticamente a memória necessária para carregar os pesos do modelo, utilizando quantização de 4 bits com normalização de dados. Isso significa que os pesos do modelo são representados com menos precisão (4 bits em vez dos tradicionais 16 ou 32 bits), economizando memória sem uma perda significativa de desempenho.
  • LoRA (Low-Rank Adaptation): Em vez de ajustar todos os parâmetros do modelo pré-treinado, o LoRA introduz pequenas matrizes adaptadoras de baixo rank em camadas específicas do Transformer. Apenas essas matrizes adaptadoras são treinadas, enquanto os pesos originais do modelo permanecem congelados. Isso reduz o número de parâmetros treináveis em ordens de magnitude.
  • Paged Optimizers: Utiliza paginadores de memória para gerenciar eficientemente o uso de memória durante o treinamento, evitando erros de falta de memória (Out-Of-Memory – OOM) em GPUs com VRAM limitada.

A combinação dessas técnicas torna o ajuste fino de modelos como o LFM2 viável em GPUs de consumidor ou instâncias de nuvem mais acessíveis, como as disponíveis no Google Colab.

Supervised Fine-Tuning (SFT)

O SFT é o método mais direto de ajuste fino. Envolve treinar o modelo em um conjunto de dados de pares entrada-saída (prompt-resposta). O modelo aprende a gerar a resposta desejada para um determinado prompt. Em essência, é um aprendizado supervisionado onde o modelo é ensinado a imitar os exemplos fornecidos. Para o LFM2, o SFT seria o primeiro passo lógico para adaptar o modelo a um estilo ou formato específico de resposta.

Direct Preference Optimization (DPO)

DPO é uma abordagem mais recente e eficaz para alinhar LLMs com preferências humanas, superando algumas das complexidades do Reinforcement Learning from Human Feedback (RLHF). Em vez de treinar um modelo de recompensa separado e depois usar RL para otimizar o LLM, o DPO otimiza diretamente o LLM usando um conjunto de dados de preferências. Este conjunto de dados consiste em triplas: um prompt, uma resposta preferida e uma resposta rejeitada. O DPO formula uma função de perda que incentiva o modelo a aumentar a probabilidade de respostas preferidas e diminuir a de respostas rejeitadas, sem a necessidade de um modelo de recompensa explícito.

O DPO é particularmente poderoso para refinar o comportamento do modelo após o SFT, ensinando-o a ser mais útil, inofensivo ou alinhado com um determinado conjunto de diretrizes éticas ou de estilo.

Configuração do Ambiente no Google Colab

O Google Colab oferece um ambiente de notebook Jupyter gratuito com acesso a GPUs, tornando-o ideal para experimentar com LLMs. Para este tutorial, precisaremos instalar as bibliotecas necessárias e configurar o ambiente.

Instalação de Pacotes

Execute as seguintes células no Google Colab para instalar as dependências:


!pip install -q transformers accelerate bitsandbytes peft trl
!pip install -q datasets

Explicação:

  • transformers: A biblioteca principal da Hugging Face para trabalhar com modelos pré-treinados.
  • accelerate: Auxilia no treinamento distribuído e no uso eficiente de hardware.
  • bitsandbytes: Essencial para a quantização de 8 e 4 bits, como usado no QLoRA.
  • peft: Contém implementações de métodos PEFT, incluindo LoRA.
  • trl: Fornece ferramentas para treinar modelos de linguagem com aprendizado por reforço e otimização de preferências, incluindo o DPO.
  • datasets: Para carregar e processar conjuntos de dados.

Carregando o Modelo e Tokenizador

Precisaremos carregar o modelo LFM2 e seu tokenizador correspondente. Para o QLoRA, configuraremos o carregamento com quantização de 4 bits.


import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_name = "lfm2b/lfm2b-4b-instruct"

# Configuração de Quantização para QLoRA
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

# Carregar o modelo com quantização
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=quantization_config,
    device_map="auto", # Permite que accelerate gerencie o mapeamento para GPUs
)

# Carregar o tokenizador
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token # Definir token de padding

Explicação:

  • model_name: O identificador do modelo LFM2 no Hugging Face Hub.
  • BitsAndBytesConfig: Define os parâmetros para carregar o modelo em 4 bits. `nf4` é um tipo de quantização recomendado.
  • device_map="auto": Deixa a biblioteca accelerate decidir como distribuir o modelo pelas GPUs disponíveis.
  • tokenizer.pad_token = tokenizer.eos_token: É uma prática comum definir o token de fim de sequência como token de padding para modelos causais.

Passo 1: Supervised Fine-Tuning (SFT) com LoRA

Antes de aplicar o DPO, é benéfico realizar um SFT para direcionar o modelo para o formato de saída desejado. Usaremos LoRA para tornar este processo eficiente em termos de parâmetros.

Preparando o Conjunto de Dados

Para SFT, você precisará de um conjunto de dados formatado como prompts e respostas. Assumiremos que você tem um conjunto de dados carregado em um objeto Dataset da biblioteca datasets. Para fins de demonstração, vamos criar um pequeno dataset fictício:


from datasets import Dataset

data = {
    "prompt": [
        "Explique o conceito de Inteligência Artificial em termos simples.",
        "Escreva um poema curto sobre a primavera.",
        "Qual a capital da França?"
    ],
    "completion": [
        "Inteligência Artificial (IA) é a capacidade de máquinas realizarem tarefas que normalmente exigiriam inteligência humana, como aprendizado, resolução de problemas e tomada de decisões.",
        "Flores desabrocham, o sol a brilhar,\nUm novo começo, a vida a pulsar.\nA natureza desperta, em cores vibrantes,\nUm hino à beleza, em todos os instantes.",
        "A capital da França é Paris."
    ]
}

dataset = Dataset.from_dict(data)

Agora, precisamos formatar esses dados em um formato que o modelo possa entender. Para modelos instrucionais, um formato comum é:


def formatting_prompts_func(example):
    output_texts = []
    for i in range(len(example['prompt'])):
        text = f"### Instruction:\n{example['prompt'][i]}\n\n### Response:\n{example['completion'][i]}"
        output_texts.append(text)
    return {"text": output_texts}

dataset = dataset.map(formatting_prompts_func, batched=True)

Configurando o LoRA

Vamos configurar o adaptador LoRA. O PEFT facilita isso com a classe LoraConfig.


from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# Preparar o modelo para treinamento k-bit (necessário para QLoRA)
model = prepare_model_for_kbit_training(model)

# Configuração do LoRA
lora_config = LoraConfig(
    r=16,  # Rank das matrizes de atualização
    lora_alpha=32, # Fator de escalonamento
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # Módulos a serem adaptados
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

# Obter o modelo PEFT
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

Explicação:

  • prepare_model_for_kbit_training: Realiza ajustes necessários no modelo para treinamento com quantização.
  • r: O rank da decomposição das matrizes LoRA. Valores mais altos permitem mais capacidade de adaptação, mas aumentam os parâmetros treináveis.
  • lora_alpha: Um fator de escala. A atualização é escalonada por lora_alpha/r.
  • target_modules: Especifica quais camadas do Transformer devem receber os adaptadores LoRA. Para modelos baseados em Llama, as camadas de atenção e feed-forward são alvos comuns.
  • print_trainable_parameters(): Mostra a porcentagem de parâmetros que serão treinados, destacando a eficiência do LoRA.

Treinando com o Trainer da TRL

A biblioteca TRL fornece um SFTTrainer conveniente para realizar o SFT.


from transformers import TrainingArguments
from trl import SFTTrainer

output_dir = "./lfm2-sft-results"

# Configurações de treinamento
training_args = TrainingArguments(
    output_dir=output_dir,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=1,
    learning_rate=2e-4,
    num_train_epochs=1,
    logging_steps=10,
    save_steps=100,
    fp16=True, # Usar precisão mista para acelerar
    push_to_hub=False, # Não enviar para o Hub por enquanto
)

# Inicializar o SFT Trainer
sft_trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    peft_config=lora_config,
    dataset_text_field="text",
    max_seq_length=512, # Comprimento máximo da sequência
    tokenizer=tokenizer,
    args=training_args,
    packing=False, # Não empacotar múltiplas sequências
)

# Iniciar o treinamento
sft_trainer.train()

# Salvar o adaptador LoRA treinado
sft_trainer.save_model(f"{output_dir}/final_sft_adapter")

Explicação:

  • TrainingArguments: Define hiperparâmetros como tamanho do batch, taxa de aprendizado, número de épocas, etc.
  • SFTTrainer: Um wrapper que simplifica o loop de treinamento SFT, integrando PEFT e Transformers.
  • dataset_text_field: O nome da coluna no dataset que contém o texto formatado.
  • max_seq_length: O comprimento máximo das sequências de entrada.
  • packing=False: Evita empacotar múltiplas sequências em uma única entrada, o que pode ser mais simples para começar.

Passo 2: Direct Preference Optimization (DPO)

Após o SFT, o modelo pode gerar respostas no formato correto, mas pode não ser ideal em termos de preferência. O DPO é usado para refinar isso.

Preparando o Conjunto de Dados de Preferência

Para DPO, necessitamos de um dataset com colunas como `prompt`, `chosen` (resposta preferida) e `rejected` (resposta rejeitada). Novamente, criaremos um dataset fictício.


data_dpo = {
    "prompt": [
        "Qual a melhor forma de aprender Inteligência Artificial?",
        "Escreva uma história curta sobre um robô."
    ],
    "chosen": [
        "A melhor forma é combinar estudo teórico com prática constante, como em projetos e cursos online.",
        "Em uma metrópole futurista, vivia Unit 734, um robô de limpeza com um desejo secreto: ver o nascer do sol."
    ],
    "rejected": [
        "Apenas leia livros sobre o assunto, isso é suficiente.",
        "Um robô chamado Bob consertava carros."
    ]
}

dataset_dpo = Dataset.from_dict(data_dpo)

A TRL espera um formato específico para DPO, onde as respostas escolhidas e rejeitadas são concatenadas com o prompt.


def formatting_dpo_func(example):
    output_texts = []
    for i in range(len(example['prompt'])):
        # Formato: prompt + chosen_response
        chosen_text = f"### Instruction:\n{example['prompt'][i]}\n\n### Response:\n{example['chosen'][i]}"
        # Formato: prompt + rejected_response
        rejected_text = f"### Instruction:\n{example['prompt'][i]}\n\n### Response:\n{example['rejected'][i]}"
        output_texts.append({"chosen": chosen_text, "rejected": rejected_text})
    return output_texts

formatted_dpo_data = formatting_dpo_func(dataset_dpo)

# Criar um novo dataset com as colunas formatadas
dataset_dpo_formatted = Dataset.from_dict({
    "chosen": [item['chosen'] for item in formatted_dpo_data],
    "rejected": [item['rejected'] for item in formatted_dpo_data]
})

Configurando o DPO Trainer

A TRL oferece o DPOTrainer.


from trl import DPOTrainer

# Recarregar o modelo base (ou usar o modelo SFT, mas para DPO puro, um modelo base pode ser preferível ou o SFT)
# Para este exemplo, vamos recarregar o modelo quantizado original para demonstrar o DPO de forma isolada.
# Em um fluxo real, você carregaria o modelo SFT treinado.

model_dpo = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=quantization_config,
    device_map="auto",
)

# Configurar LoRA para o modelo DPO (se estivermos otimizando o modelo SFT)
# Se estivermos começando do zero com DPO, precisaríamos configurar LoRA aqui também.
# Para este exemplo, vamos assumir que estamos refinando o modelo SFT, então o LoRA já está configurado e o modelo carregado seria o SFT.
# No entanto, para simplificar o código e evitar carregar o adaptador SFT explicitamente, vamos reconfigurar LoRA aqui.

model_dpo = prepare_model_for_kbit_training(model_dpo)
lora_config_dpo = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model_dpo = get_peft_model(model_dpo, lora_config_dpo)

# A TRL espera que o modelo base para o cálculo da política de referência seja o modelo *antes* do treinamento DPO.
# Se você treinou o SFT, o modelo base para o DPO seria o modelo *antes* do SFT.
# Para este exemplo, vamos usar o modelo quantizado inicial como referência.
ref_model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=quantization_config,
    device_map="auto",
)

# Configurações de treinamento DPO
training_args_dpo = TrainingArguments(
    output_dir="./lfm2-dpo-results",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=1,
    learning_rate=1e-5, # Taxa de aprendizado mais baixa para DPO
    num_train_epochs=1,
    logging_steps=10,
    save_steps=100,
    fp16=True,
    push_to_hub=False,
)

# Inicializar o DPOTrainer
dpo_trainer = DPOTrainer(
    model=model_dpo,
    ref_model=ref_model, # Modelo de referência para calcular a perda DPO
    train_dataset=dataset_dpo_formatted,
    peft_config=lora_config_dpo,
    tokenizer=tokenizer,
    args=training_args_dpo,
    max_prompt_length=512,
    max_length=1024, # Comprimento máximo da sequência de saída
)

# Iniciar o treinamento DPO
dpo_trainer.train()

# Salvar o adaptador DPO treinado
dpo_trainer.save_model("./lfm2-dpo-results/final_dpo_adapter")

Explicação:

  • ref_model: Crucial para DPO. É uma cópia do modelo *antes* do treinamento DPO, usada para calcular a perda de KL divergence e garantir que o modelo otimizado não se afaste demais do comportamento original.
  • DPOTrainer: A classe TRL para executar o treinamento DPO.
  • max_prompt_length e max_length: Definem os limites de comprimento para prompts e sequências completas.
  • A taxa de aprendizado para DPO é geralmente menor do que para SFT.

Passo 3: Mesclagem de Adaptadores (Opcional) e Inferência

Após treinar os adaptadores LoRA para SFT e DPO, você pode querer combiná-los ou simplesmente usar o adaptador DPO (que geralmente refina o modelo SFT) para inferência.

Mesclagem de Adaptadores

Se você treinou SFT e DPO separadamente em adaptadores LoRA, pode mesclá-los para obter um modelo final. No entanto, o fluxo mais comum é treinar SFT primeiro, carregar o modelo SFT treinado e depois treinar DPO nele. O resultado final é o adaptador DPO, que refina o modelo SFT.

Para usar o modelo treinado para inferência, você precisa carregar o modelo base e aplicar os adaptadores LoRA treinados.


from peft import PeftModel

# Carregar o modelo base quantizado novamente
base_model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=quantization_config,
    device_map="auto",
)

# Carregar o adaptador DPO treinado
# Se você treinou SFT e DPO sequencialmente no mesmo modelo, carregue apenas o último adaptador.
# Aqui, vamos carregar o adaptador DPO que treinamos.
dpo_model_path = "./lfm2-dpo-results/final_dpo_adapter"
model_with_adapters = PeftModel.from_pretrained(base_model, dpo_model_path)

# Para inferência, é comum mover o modelo para a GPU e usar torch.no_grad()
model_with_adapters.eval()

# Exemplo de inferência
prompt = "Explique o processo de aprendizado por reforço em Inteligência Artificial."

# Formatar o prompt para o modelo
formatted_prompt = f"### Instruction:\n{prompt}\n\n### Response:"

inputs = tokenizer(formatted_prompt, return_tensors="pt").to(model_with_adapters.device)

with torch.no_grad():
    outputs = model_with_adapters.generate(
        **inputs,
        max_new_tokens=200,
        do_sample=True,
        top_p=0.9,
        temperature=0.7,
    )

response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)

Explicação:

  • PeftModel.from_pretrained(): Carrega o modelo base e aplica os pesos do adaptador LoRA.
  • model_with_adapters.eval(): Coloca o modelo em modo de avaliação, desativando dropout e outras camadas específicas de treinamento.
  • model_with_adapters.generate(): Gera texto a partir do prompt. Parâmetros como max_new_tokens, do_sample, top_p e temperature controlam a geração.

Considerações Avançadas e Melhores Práticas

Conjuntos de Dados de Alta Qualidade

O desempenho do ajuste fino é altamente dependente da qualidade e relevância do conjunto de dados. Para SFT, os pares prompt-resposta devem ser precisos e no formato desejado. Para DPO, as preferências (escolhido vs. rejeitado) devem refletir genuinamente o comportamento desejado.

Avaliação Rigorosa

Após o ajuste fino, é crucial avaliar o modelo em um conjunto de dados de teste separado para medir seu desempenho em tarefas não vistas. Métricas como perplexidade, BLEU, ROUGE, ou avaliações humanas podem ser usadas. Para DPO, a avaliação deve focar se o modelo agora gera respostas que são preferidas de acordo com os critérios definidos.

Gerenciamento de Memória e Hardware

Mesmo com QLoRA, ajustar modelos grandes pode exigir GPUs com VRAM substancial. O Google Colab oferece diferentes níveis de acesso a GPUs (T4, V100, A100). Monitore o uso de VRAM e ajuste o per_device_train_batch_size e gradient_accumulation_steps conforme necessário.

Hiperparâmetros

Os hiperparâmetros de treinamento (taxa de aprendizado, número de épocas, rank do LoRA, etc.) podem ter um impacto significativo. Experimentação e ajuste fino desses parâmetros são frequentemente necessários para obter os melhores resultados.

Fluxo de Trabalho Combinado (SFT + DPO)

O fluxo de trabalho mais eficaz geralmente envolve:

  1. Carregar o modelo base com QLoRA.
  2. Realizar SFT com LoRA para adaptar o modelo a um estilo ou tarefa específica.
  3. Salvar os adaptadores SFT.
  4. Carregar o modelo base novamente (ou o modelo SFT).
  5. Treinar DPO com LoRA, usando o modelo SFT como ponto de partida, para refinar o alinhamento com preferências.
  6. Salvar os adaptadores DPO.

Este processo garante que o modelo primeiro aprenda a tarefa (SFT) e depois seja polido para melhor seguir instruções ou preferências (DPO).

Conclusão

Ajustar o modelo LFM2 usando QLoRA e DPO no Google Colab abre um leque de possibilidades para personalizar LLMs de forma eficiente. Ao combinar as técnicas de quantização de 4 bits, LoRA, SFT e DPO, desenvolvedores podem adaptar modelos poderosos para suas necessidades específicas, mesmo com recursos computacionais limitados. Este guia passo a passo, desde a configuração do ambiente até a inferência, fornece uma base sólida para começar. A chave para o sucesso reside na experimentação, na utilização de conjuntos de dados de alta qualidade e na avaliação contínua do desempenho do modelo. A democratização do acesso a essas técnicas avançadas impulsiona a inovação em Inteligência Artificial, permitindo que mais pessoas construam e implementem soluções de IA personalizadas.

Este artigo é uma adaptação e expansão de um tutorial prático encontrado no MarkTechPost. Para detalhes técnicos completos e código original, consulte o Artigo de Origem.

📚 Fontes E Referências

  1. How to Fine-Tune LFM2 Using QLoRA and DPO: A Complete Step-by-Step Coding Tutorial on Google ColabPortal Internacional

Deixe um comentário