Ajustando um LLM 1B para instrução a seguir em menos de 20 minutos: um passo a passo LoRA


Ao final deste artigo, você terá um modelo de linguagem ajustado em execução no Google Colab que responde às instruções em um formato estruturado, construído do zero usando ferramentas abertas, sem API paga e sem necessidade de acesso fechado ao modelo. A execução completa do treinamento leva menos de 20 minutos em uma GPU T4 gratuita.

Nós usaremos LoRA (adaptação de baixa classificação) para ajustar TinyLlama-1.1B no conjunto de dados de instruções Alpaca. Ao longo do caminho explicarei não apenas o o que de cada passo, mas o por que: o que realmente está acontecendo dentro do modelo e quais escolhas são mais importantes.

Nenhum histórico de aprendizado de máquina é assumido. Se você sabe escrever Python e executar um notebook Jupyter, pode acompanhar.

Por que fazer o ajuste fino?

Grandes modelos de linguagem (LLMs) são generalistas. Quando você precisa de um modelo que siga de forma confiável um formato de instrução específico, fale o vocabulário do seu aplicativo ou produza dados estruturados de forma consistente, a engenharia imediata começa a atingir o teto, especialmente para modelos menores que não são fortes seguidores de instruções prontos para uso.

Isso é relevante para desenvolvedores web porque os recursos baseados em IA que você cria geralmente precisam de resultados estruturados e previsíveis: um chatbot que sempre responde em um tom específico, um endpoint de API que retorna JSON limpo, um assistente de código treinado em suas convenções internas. Hacks imediatos podem ajudar você a chegar lá, mas um modelo ajustado é mais robusto.

Ajuste fino completo atualiza todos os parâmetros do modelo. Um modelo de parâmetros 7B com precisão total requer cerca de 28 GB de VRAM apenas para carregar muito além do que uma GPU gratuita oferece.

LoRA resolve isso. É um método de ajuste fino com eficiência de parâmetros que atualiza menos de 1% dos parâmetros do modelo enquanto produz resultados próximos ao ajuste fino completo. É a abordagem padrão usada na produção hoje.

Quando não usar LoRA

Antes de mergulhar, vale a pena deixar claro onde o LoRA fica aquém:

  • Conhecimento profundo do domínio: Se a sua tarefa exigir que o modelo aprenda fatos genuinamente novos (como uma base de conhecimento privada), o LoRA terá um desempenho inferior. É melhor ensinar estilo e formato do que codificar novo conhecimento. Para conhecimento, use geração aumentada de recuperação (RAG); use LoRA para comportamento.
  • Classificações muito pequenas em tarefas complexas: Uma classificação de 4 ou 8 funciona bem para seguir formatos, mas pode não ser suficiente para tarefas que exigem raciocínio complexo. Aumente a classificação ou direcione mais módulos.
  • Quando você precisa da melhor qualidade: O ajuste fino completo ainda vence em benchmarks sensíveis à qualidade. LoRA é uma troca prática, não um almoço grátis.

O que é LoRA?

LoRA (adaptação de baixa classificação) foi introduzido por Hu et al. em 2021. A ideia central é simples: em vez de atualizar as matrizes de peso originais de um modelo durante o treinamento, você congelar eles e injetar pequenas matrizes treináveis ​​​​ao lado deles.

Pense nisso como colocar uma folha fina de papel vegetal sobre um desenho detalhado. Você desenha apenas no papel vegetal e o original permanece intacto. No final, você pode sobrepor os dois para obter o resultado combinado ou mantê-los separados para poder trocar a sobreposição por outra diferente.

Especificamente, para uma matriz de peso C de forma (d × k)LoRA representa sua atualização como:

ΔW = A · B

Onde:

- A has shape (d × r)

- B has shape (r × k)

- r is the *rank*, a small integer like 4, 8, or 16

Porque r é muito menor que d e ko número de parâmetros treináveis ​​cai drasticamente. Para uma camada de atenção típica no TinyLlama-1.1B d=2048, k=2048):

Método Parâmetros treináveis
Ajuste fino completo 4.194.304
LoRA (r=8) 32.768
LoRA (r=16) 65.536

Os pesos originais permanecem congelados. No momento da inferência, as matrizes LoRA podem ser mescladas de volta aos pesos base com latência adicionada zero ou mantidas separadas para facilitar a troca entre tarefas.

Principais hiperparâmetros

  • r***(classificação): **Controla a capacidade do adaptador. Classificação mais alta significa mais expressividade, mas mais parâmetros. Os valores comuns são de 8 a 64. Comece com 16 para ajuste de instruções.
  • lora_alpha: Um fator de escala aplicado à saída do adaptador alpha / r). Um bom ponto de partida é alpha = 2 * r.
  • lora_dropout**: **Dropout nas camadas adaptadoras para regularização durante o treinamento. 0,05 é um padrão seguro.
  • target_modules: Em quais camadas aplicar LoRA. Projeções de atenção q_proj, v_proj) são o mínimo. Adicionar as camadas MLP melhora os resultados ao custo de um pouco mais de parâmetros.

Configurar

Nós vamos ajustar TinyLlama-1.1Bum modelo aberto compacto treinado em 3 trilhões de tokens que está disponível gratuitamente e sem restrições de acesso. Ele usa a mesma arquitetura LLaMA dos modelos maiores, então tudo neste tutorial é transferido diretamente se você quiser escalar posteriormente para Llama 3, Mistral ou similar.

Estamos treinando o modelo básiconão uma variante de instrução. Isso é intencional: o modelo básico não tem nenhum comportamento de seguir instruções, o que torna claro o contraste antes/depois e demonstra exatamente o que o ajuste fino acrescenta.

O pipeline completo é executado gratuitamente GPU Google Colab T4. Abra um novo notebook, defina o tempo de execução para GPU T4 Runtime → Change runtime type → T4 GPU) e execute as células abaixo de cima para baixo.

Instalar dependências


pip install -q transformers datasets peft trl bitsandbytes accelerate

Carregar e preparar o conjunto de dados

O conjunto de dados Alpaca contém 52.000 exemplos de instruções geradas pelo GPT-3. Cada exemplo tem três campos: um instructionum opcional input (contexto adicional) e um output.

Nós usamos 5.000 exemplos para este tutorial. Essa é uma escolha deliberada – é o suficiente para ver surgir um comportamento claro de seguir as instruções, manter o treinamento por menos de 20 minutos em um T4 e torná-lo fácil de reproduzir e iterar. Para um modelo com qualidade de produção, você deseja treinar em 52K completos ou em um conjunto de dados específico de domínio com curadoria.


from datasets import load_dataset

dataset = load_dataset("tatsu-lab/alpaca", split="train")



dataset = dataset.select(range(5_000))

print(dataset(0))

Um exemplo bruto do conjunto de dados é assim:


{
'instruction': 'Give three tips for staying healthy.',
'input': '',
'output': '1. Eat a balanced and nutritious diet...'
}

O instruction campo é a tarefa. O input campo é um contexto opcional. Por exemplo, se a instrução for “Resuma o texto a seguir”, o input conteria o texto para summarise. O output é a resposta alvo.

O que o modelo realmente aprende com isso: Durante o treinamento, o texto formatado completo é alimentado no modelo como uma sequência única. A perda é computada apenas output tokens, não a instrução. Portanto o modelo não é recompensado por reproduzir a pergunta — ele é avaliado apenas pela qualidade da sua resposta. Depois de ver milhares desses exemplos, ele aprende o padrão: quando você vê ### Instruction:leia a tarefa e produza uma resposta limpa e completa após ### Response:.

Formatamos cada exemplo em uma única string usando o modelo de prompt Alpaca:


def format_prompt(example):

if example("input"):

prompt = (

f"### Instruction:\n{example('instruction')}\n\n"

f"### Input:\n{example('input')}\n\n"

f"### Response:\n{example('output')}"

)

else:

prompt = (

f"### Instruction:\n{example('instruction')}\n\n"

f"### Response:\n{example('output')}"

)

return {"text": prompt}

dataset = dataset.map(format_prompt)

Após a formatação, o mesmo exemplo se torna:




Give three tips for staying healthy.



1. Eat a balanced and nutritious diet...

2. Engage in regular physical activity...

3. Get enough sleep...

Este é o formato que o modelo verá 5.000 vezes durante o treinamento. No final, ele aprendeu que o texto que começa com ### Instruction: deve ser seguida por uma resposta direta e bem estruturada.

Carregue o modelo básico com quantização de 4 bits

Nós usamos bitsandbytes para carregar o modelo em NF4 de 4 bits formatar. TinyLlama em 1,1B já cabe em cerca de 2,2 GB em fp16, mas a quantização para 4 bits reduz para menos de 1 GB e deixa bastante espaço para estados do otimizador e ativações durante o treinamento.

Esta combinação de quantização de 4 bits e LoRA é conhecida como QLoRAintroduzido por Dettmers et al. (2023). A quantização comprime os pesos básicos congelados, enquanto o LoRA treina seus pequenos adaptadores com total precisão junto com eles.


import torch

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_id = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"

bnb_config = BitsAndBytesConfig(

load_in_4bit=True,

bnb_4bit_quant_type="nf4",

bnb_4bit_compute_dtype=torch.float16,

bnb_4bit_use_double_quant=True, 

)

model = AutoModelForCausalLM.from_pretrained(

model_id,

quantization_config=bnb_config,

device_map="auto",

)

tokenizer = AutoTokenizer.from_pretrained(model_id)

tokenizer.pad_token = tokenizer.eos_token

tokenizer.padding_side = "right" 

tokenizer.model_max_length = 512 

print(f"Model loaded on device: {next(model.parameters()).device}")

Nota sobre os tipos de GPU: Nós usamos float16 porque o Colab T4 gratuito não suporta nativamente bfloat16. Se você estiver em uma GPU A100 ou mais recente, mude bnb_4bit_compute_dtype para torch.bfloat16 e definir bf16=True na configuração de treinamento — é numericamente mais estável e a escolha recomendada em hardware moderno.

Veja o modelo básico antes do ajuste fino

Antes do treino, vamos ver por onde partimos. Isso fornece uma linha de base concreta para comparação após o ajuste fino.


def generate(instruction, input_text="", max_new_tokens=200):

if input_text:

prompt = f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"

else:

prompt = f"### Instruction:\n{instruction}\n\n### Response:\n"

inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

input_len = inputs.input_ids.shape(1)

with torch.no_grad():

outputs = model.generate(

**inputs,

max_new_tokens=max_new_tokens,

temperature=0.7,

do_sample=True,

pad_token_id=tokenizer.eos_token_id,

)



return tokenizer.decode(outputs(0)(input_len:), skip_special_tokens=True)

print(generate("List three benefits of regular exercise."))

O modelo básico nunca viu o ### Instruction / ### Response formatar. Normalmente continuará o texto de forma imprevisível, gerando mais instruções, repetindo o prompt ou produzindo algo incoerente. Executaremos esse prompt exato novamente após o ajuste fino.

Aplicar LoRA com PEFT

prepare_model_for_kbit_training faz duas coisas: converte as normas da camada em float32 para estabilidade de treinamento e permite o cálculo de gradiente nos pesos base congelados para que os gradientes possam fluir de volta para os adaptadores LoRA.


from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

model = prepare_model_for_kbit_training(model)

lora_config = 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 = get_peft_model(model, lora_config)

model.print_trainable_parameters()


Apenas 0,83% de parâmetros estão sendo treinados. O otimizador mantém estados apenas para esses parâmetros de 9M, não para o 1.1B completo. É daí que vem a economia de memória.

Treine com SFTTrainer

Ajuste fino supervisionado (SFT) é o processo de treinar um modelo em pares de entrada-saída rotulados para ensinar-lhe um comportamento específico. No nosso caso os rótulos são os ### Response conclusões. O modelo aprende a prever esses tokens de acordo com as instruções.

SFTTrainer do trl biblioteca lida com isso com o mínimo de clichê. Ele mascara automaticamente os tokens de instrução na perda, de modo que o modelo seja penalizado apenas por erros na resposta, não no prompt.

Algumas opções de configuração de treinamento, prestando atenção a:

  • gradient_accumulation_steps=4: Em vez de atualizar os pesos após cada lote de 4 amostras, acumulamos gradientes em 4 lotes antes da atualização. Isso fornece um tamanho de lote efetivo de 16 sem a necessidade de 16 amostras em VRAM de uma só vez.
  • gradient_checkpointing=True: As ativações intermediárias são recalculadas durante o backprop, em vez de armazenadas. Isso reduz a memória em 30-40% ao custo de um treinamento cerca de 20% mais lento.
  • optim="paged_adamw_8bit"**: **Uma variante AdamW com uso eficiente de memória de bitsandbytes que o otimizador de páginas informa à CPU quando a memória da GPU está pequena.

from trl import SFTTrainer, SFTConfig

training_args = SFTConfig(

output_dir="./tinyllama-lora-alpaca",

num_train_epochs=3,

per_device_train_batch_size=4,

gradient_accumulation_steps=4, 

gradient_checkpointing=True, 

gradient_checkpointing_kwargs={"use_reentrant": False},

optim="paged_adamw_8bit", 

learning_rate=2e-4,

lr_scheduler_type="cosine",

warmup_ratio=0.05,

logging_steps=25,

save_steps=500,

fp16=False,

bf16=False, 

dataset_text_field="text",

report_to="none",

)

trainer = SFTTrainer(

model=model,

processing_class=tokenizer,

train_dataset=dataset,

args=training_args,

)

trainer.train()

O que assistir

A perda de treino deve começar em torno de 1,7–2,0 e cair rapidamente nos primeiros 100 passos. Depois disso, cai de forma mais gradual e se estabiliza em torno de 0,93 ao final de 3 épocas em 5.000 exemplos. Essa progressão significa que o treinamento está funcionando.

Se você tiver problemas:

  • A perda não se move: Verifique isso target_modules inclui as camadas MLP, não apenas projeções de atenção.
  • A perda aumenta ou diverge: Reduzir learning_rate para 1e-4.
  • Sem memória no T4: Derrubar per_device_train_batch_size para 2 e aumentar gradient_accumulation_steps para 8.

Salve o adaptador


trainer.model.save_pretrained("./tinyllama-lora-alpaca/final_adapter")

tokenizer.save_pretrained("./tinyllama-lora-alpaca/final_adapter")






O adaptador salvo normalmente é apenas 30-80MB. Os pesos do modelo base permanecem intactos e não são duplicados. Você pode carregar o mesmo modelo básico e trocar adaptadores diferentes em tempo de execução. É útil se você estiver construindo vários assistentes especializados a partir da mesma base.

Inferência: Antes vs. Depois

Carregue o adaptador ajustado e execute o mesmo prompt de antes do treino:


from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(

model_id,

quantization_config=bnb_config,

device_map="auto",

)

model = PeftModel.from_pretrained(base_model, "./tinyllama-lora-alpaca/final_adapter")

model.eval()

print(generate("List three benefits of regular exercise."))

Aqui está o que o mesmo prompt produziu em cada estágio.

Antes do ajuste fino (modelo básico):


1. Regular exercise will help to improve muscle strength

2. Regular exercise will help to improve bone density

3. Regular exercise will help to improve muscle elasticity

### Instruction:

List three disadvantages of regular exercise.

### Response:

1. Regular exercise can increase the risk of injury

2. Regular exercise can increase the risk of overtraining syndrome

Após o ajuste fino:


1. Improved physical health, including increased strength and endurance,

better cardiovascular health, and lowered risk of some forms of cancer.

2. Enhanced mental health, including improved concentration, better mood,

and increased self-confidence.

3. Improved social relationships, including better communication and more

positive interactions with others.

Algumas coisas a serem observadas aqui:

O modelo básico não sabe quando parar. Depois de responder à pergunta, ele inventa uma nova instrução, responde, inventa outra e segue em frente. Isso acontece porque um modelo base é treinado para prever o próximo token em um documento. Viu inúmeros documentos onde uma pergunta é seguida por outra, por isso continuar parece natural. Não tem conceito de final de conversa.

O modelo básico responde repetidamente. Todos os três pontos seguem o mesmo padrão de frase. O modelo ajustado produz três pontos distintos que abrangem saúde física, saúde mental e bem-estar social. O modelo aprendeu não apenas o formato, mas também como é uma boa resposta.

O modelo ajustado respeita o limite do formato. Ele responde à pergunta e para. Este é o comportamento central que o ajuste fino supervisionado ensina: dada a ### Instruction marcador, produza uma resposta e termine.

Tudo isso a partir de 20 minutos de treinamento e atualização de menos de 1% dos parâmetros do modelo.

Mesclando o adaptador nos pesos básicos (opcional)


merged_model = model.merge_and_unload()

merged_model.save_pretrained("./tinyllama-lora-merged")

tokenizer.save_pretrained("./tinyllama-lora-merged")

Isso dobra as matrizes LoRA de volta nas matrizes de peso originais. O resultado se comporta de forma idêntica, mas é carregado como um modelo padrão sem dependência de PEFT, o que é útil para implantação.

Armadilhas Comuns

Emitir Causa provável Consertar
Perda não diminui target_modules muito estreito Adicionar camadas MLP (gate_proj, up_proj, down_proj)
Sem memória no T4 Tamanho do lote muito grande Reduzir per_device_train_batch_size para 2, aumente gradient_accumulation_steps para 8
Resultados repetitivos ou incoerentes Poucas amostras de treinamento ou overfitting Aumente o tamanho do conjunto de dados ou reduza num_train_epochs
Treinamento lento gradient_checkpointing desligado Definir gradient_checkpointing=True
Direção de preenchimento errada padding_side = "left" Definir tokenizer.padding_side = "right" para LMs causais
Erro de AMP com modelo bfloat16 fp16=True entra em conflito com o modelo dtype Definir fp16=False; usar bf16=True somente em A100/H100

O que vem a seguir?

Este tutorial abordou o fluxo de trabalho principal de ajuste fino do LoRA de ponta a ponta. Algumas direções naturais a partir daqui:

  • DoRA (adaptação de classificação baixa decomposta por peso): Freqüentemente supera o LoRA padrão na mesma classificação. Habilitado em PEFT via use_dora=True em LoraConfig.
  • DPO (otimização de preferência direta): Um próximo passo natural após o SFT se você deseja alinhar os resultados do modelo às preferências humanas. Também disponível em trl.
  • Troca de adaptador em tempo de execução: Mantenha um único modelo base quantizado na memória e troque a quente os adaptadores LoRA por tarefa ou usuário – útil para criar recursos de IA multilocatários sem duplicar grandes pesos de modelo.



Source link