TL;DR

Fine-tuning de apenas 1,09% dos parâmetros de um modelo supera uma baseline bag-of-words em 5 pontos de F1, mas o caminho até lá exige enfrentar bugs que nenhum tutorial avisa — incluindo módulos LoRA silenciosamente ignorados e ferramentas de interpretabilidade quebradas pelo PEFT. Este artigo relata a construção, os resultados e as lições de um classificador de risco de insatisfação para 576 mil avaliações de móveis da Amazon, e propõe correções conceituais e práticas para quem deseja levar LoRA a produção.

  • Tarefa: Classificar avaliações de móveis da Amazon como criticamente insatisfeitas (1–2★) vs. satisfeitas (4–5★), produzindo um score de probabilidade calibrado e explicações por token.
  • Método: DistilBERT + LoRA, treinando apenas 739k de 67M de parâmetros (1,09%).
  • Resultado: AUC-ROC 0,996 · F1 0,943 · ECE 0,0105 em 81.715 avaliações de teste.
  • Lições reais: Os nomes dos módulos LoRA não são padronizados entre famílias de modelos; o PEFT quebra silenciosamente ferramentas de interpretabilidade; e LoRA não é um mero “redirecionamento” — é uma adaptação aditiva de baixo posto com trade-offs reais.

1. Introdução: o que significa, de fato, “adaptar” um transformer?

Existe um tipo particular de aprendizado que só acontece quando a teoria colide com um bug às 23h.

Eu tinha lido sobre LoRA (Low-Rank Adaptation). Entendi o conceito: congelar o backbone, injetar matrizes treináveis pequenas nas camadas de atenção, treinar apenas essas. Mentalmente, categorizei LoRA como um “redirecionamento” do conhecimento existente — uma pequena perturbação.

Então sentei para construir um classificador baseado em LoRA sobre 576.000 avaliações de móveis da Amazon — e passei as três semanas seguintes descobrindo o quanto essa metáfora era incompleta.

LoRA não é um “vetor de direcionamento”. É uma adaptação aditiva de baixo posto. A saída de uma camada com LoRA é:

h = W₀ x + BA x

Os pesos originais W₀ permanecem congelados, mas os novos parâmetros A e B se somam à transformação original. O modelo não é “apontado” para a tarefa; ele ganha um novo caminho de computação de baixo posto que se combina com o conhecimento pré-existente. Essa distinção importa para entender tanto o poder quanto as limitações do método.

Este artigo é sobre o que LoRA realmente faz, os bugs que emergem na implementação e por que atingir 0,943 de F1 com 1% dos parâmetros não é um compromisso — é evidência de que as representações pré-treinadas carregam quase tudo que a tarefa precisa.


2. O projeto: um score de risco para insatisfação de clientes

O problema é comercialmente legível: empresas de e-commerce e B2B SaaS se afogam em dados de avaliação. Times de suporte fazem triagem manual de tickets. O sinal — um cliente que não está apenas insatisfeito, mas criticamente insatisfeito — está enterrado em palavras.

Objetivo: Classificar avaliações de móveis da Amazon como criticamente insatisfeitas (1–2 estrelas) versus satisfeitas (4–5 estrelas), produzindo um score de probabilidade calibrado [0,1] e explicações no nível do token.

Avaliações de 3 estrelas foram excluídas por serem estruturalmente ambíguas — a assinatura linguística de “tá bom, né” é genuinamente diferente tanto do elogio quanto da reclamação.

Pergunta central: Um modelo pequeno com fine-tuning eficiente em parâmetros consegue superar uma baseline bag-of-words o suficiente para justificar a complexidade adicional?

Spoiler: sim — em 5 pontos de F1 e 0,5 pontos de AUC. Mas o porquê é mais interessante do que o número.


3. Arquitetura: DistilBERT + LoRA em 1,09% dos parâmetros

O backbone é o distilbert-base-uncased: 6 camadas transformer, 768 dimensões ocultas, 66,9M de parâmetros. Cabe confortavelmente numa RTX 3060 12GB com batch_size=16, max_length=256.

A configuração LoRA foi conservadora:

lora:
  r: 8
  lora_alpha: 16
  lora_dropout: 0.1
  bias: "none"
  task_type: "SEQ_CLS"
  target_modules: ["q_lin", "v_lin"]

Resultado: 739.586 parâmetros treináveis de 67.694.596 — 1,09%.

O backbone não se move. Os 66 milhões de parâmetros que codificam estrutura linguística ficam congelados. Apenas as matrizes LoRA (A e B) nas projeções de query e value da atenção são atualizadas.

Atenção: DistilBERT usa q_lin e v_lin. BERT usa query e value. DeBERTa usa query_proj e value_proj. Os nomes não são padronizados. Configurar target_modules: ["query", "value"] em DistilBERT aplica LoRA a zero módulos — silenciosamente.


4. O dataset: nunca confie no intervalo de datas que você assumiu

O dataset é o corpus Amazon US Customer Reviews, categoria Furniture — 576.000 linhas, 350MB em TSV.

O plano: dividir por tempo: treinar com dados até 2016, validar em 2017, testar em 2018.

O intervalo real do dataset: 2000-03-17 a 2015-08-31.

Todas as linhas foram parar em train. Validação e teste ficaram vazios. Esse é o tipo de bug que não levanta exceção — o ETL completa com sucesso. Foi só quando df['review_date'].max() mostrou 2015 que o problema ficou claro.

Split recalibrado:

SplitFronteiraLinhas% Insatisfeitos
Train≤ 2014-12-31406.11916,3%
Val2015-01-01 → 2015-04-3088.16015,8%
Test2015-05-01 → 2015-08-3181.71516,3%

Divisão temporal (não aleatória) é a única forma honesta de simular deployment: treinar no passado, avaliar no futuro.


5. Por que o transformer ganhou do TF‑IDF?

A baseline foi TF‑IDF + Regressão Logística.

ModeloAUC-ROCF1
Classe majoritária0,5000,000
TF‑IDF + Regressão Logística0,9910,893
DistilBERT + LoRA0,9960,943

A baseline TF‑IDF é surpreendentemente forte (AUC 0,991). Isso é típico de tarefas de sentimento — palavras como “quebrou”, “devolvi”, “decepcionante” são altamente preditivas. O ganho do transformer não está nesses tokens óbvios.

Onde o transformer realmente vence (análise qualitativa de erros):

Tipo de casoExemploTF‑IDFTransformer
Negação escopada“Não foi ruim”Erra (pesa “ruim” positivamente)Acerta (lê o escopo)
Conjunção adversativa“Parece bonito mas desmonta”Pesos iguais para ambosEntende a concessão
Título como prior“One Star. Very low quality…”Título é só mais um tokenUsa título como prior para o corpo

Exemplo real (avaliação 1★ do conjunto de teste, atribuição por Integrated Gradients):

“One Star. Very low quality. Ordered two and both had dings and dents. Pore packaging and thin metal. Returned both.”

Atribuições principais:

  • one (0,674)
  • star (0,485)
  • very (0,286)
  • low (0,256)
  • returned (0,139)
  • dent (0,101)

O título (“One Star”) domina porque o modelo aprendeu que, no texto concatenado (headline + ". " + body), o título codifica a avaliação em estrelas — e o usa como prior para o resto.


6. Calibração: por que ECE 0,0105 importa mais do que F1

Em produção, um score de 0,87 precisa significar “87% destas avaliações são insatisfeitas”. Não apenas “está ranqueado acima de 0,60”.

ECE (Expected Calibration Error) mede exatamente isso. Valores abaixo de 0,05 são considerados bem calibrados.

O modelo atingiu ECE de 0,0105 no teste. Regressão isotônica foi preparada como fallback e não foi necessária.

O limiar ótimo de classificação foi 0,574 (vs. padrão 0,5), calculado maximizando F1 na validação. A pequena distância do limiar padrão é, por si só, evidência de boa calibração.


7. Resultados do treinamento: convergência na época 4

ÉpocaF1AUC-ROCECEBrier
10,92210,99470,01590,0200
20,93750,99500,01090,0160
30,94060,99600,01170,0158
40,94270,99570,01050,0151
50,94260,99570,01170,0154

A época 5 não traz ganho. O modelo extraiu o que 1,09% dos parâmetros conseguem aprender. O desbalanceamento (16,3% insatisfeitos) foi tratado com WeightedLossTrainer e pesos 3,07 (positivo) / 0,60 (negativo).


8. O problema do prefixo PEFT: quando LoRA quebra a interpretabilidade

Integrated Gradients (IG) atribui a predição a cada token. Mas quando um modelo é encapsulado pelo PEFT, os caminhos dos módulos mudam:

  • Antes: distilbert.embeddings.word_embeddings
  • Depois: base_model.model.distilbert.embeddings.word_embeddings

Ferramentas que resolvem caminhos por strings hardcoded (incluindo captum por padrão) falham silenciosamente.

Fix: buscar todos os módulos nomeados dinamicamente por qualquer caminho que termine com "word_embeddings".

Um segundo bug: captum passa n_steps cópias do input simultaneamente, mas a máscara de atenção tem shape [1, seq_len] e não [n_steps, seq_len]. Fix com .expand().

Esses bugs não afetam métricas de treinamento. Eles só aparecem quando você tenta explicar a predição.


9. Trade-offs e limitações do LoRA

O texto inicial tratava LoRA como uma melhoria sem custos. Uma análise mais honesta exige reconhecer suas limitações.

9.1 Eficiência em parâmetros ≠ eficiência computacional em inferência

  • Fine-tuning total: h = Wx
  • LoRA: h = W₀x + BAx

A adição de BAx tem custo. Para eliminar essa latência, é necessário fundir (merge) os pesos após o treino: W_merged = W₀ + BA. Isso resolve, mas impossibilita trocar adaptadores dinamicamente para diferentes tarefas.

9.2 Limitação fundamental: LoRA não pode “corrigir” conhecimento pré-existente

LoRA adiciona uma adaptação de baixo posto. Se o modelo base tiver um viés grave (ex.: associar “barato” a “ruim” em todo contexto), LoRA pode não ser suficiente para corrigi-lo. Fine-tuning total ainda é superior para domínios muito distintos do pré-treino.

9.3 O problema do rank: mais capacidade não é sempre melhor

Aumentar r de 8 para 16 ou 32 adiciona expressividade, mas também risco de overfitting — especialmente em datasets pequenos. O r=8 conservador usado aqui foi uma escolha deliberada, não apenas por VRAM.


10. O que “aprender fazendo” realmente parece

O que quebrouO que ensinou
576k linhas todas em trainSempre inspecionar df.date.min() e .max()
LoRA apontando para 0 módulosNomes de módulos diferem entre famílias — use model.named_modules()
uv sync sem solução para Python 3.12PyTorch 2.2.x não tem wheels para 3.12+ — limite superior no requires-python
compute_class_weight com listaSklearn exige np.ndarray, não lista
Falha na resolução da camada de embedding no IGPEFT adiciona prefixo base_model.model.* — lookup por caminho quebra
Shape mismatch no captumMáscaras de atenção precisam ser expandidas para [n_steps, seq_len]
n_steps=50 → convergência delta > 0,05Usar n_steps=200 para um transformer de 6 camadas

Nenhum desses bugs aparece em tutorial. Eles aparecem quando você leva uma técnica para um dataset real.


11. A visão mais ampla: eficiência em parâmetros não é um truque

Construir este projeto mudou meu enquadramento inicial.

Fazer fine-tuning em 1,09% dos parâmetros e atingir F1 0,943 não é um compromisso. É evidência de que as representações pré-treinadas carregam quase tudo que a tarefa precisa. O sinal específico da tarefa — a distinção inteira entre criticamente insatisfeito e satisfeito — cabe em 739.586 números.

Mas isso não significa que LoRA seja apenas um “redirecionador”. Ele adiciona capacidade de baixo posto. E como toda adição, tem custos: inferência lenta se não fundido, incapacidade de corrigir vieses profundos do backbone, e sensibilidade ao rank.

A lição final não é “LoRA é sempre melhor”. É: entenda o que você está adicionando, onde e a que custo. O fine-tuning total ainda tem seu lugar. Mas para tarefas de classificação em domínios próximos ao pré-treino, LoRA é, muitas vezes, suficiente — e a suficiência, em produção, vale mais do que a perfeição.


12. Próximos passos e melhorias propostas

Imediatas (curto prazo)

  • Aumentar n_steps do IG para 200 (reduz convergência delta de ~0,97 para ~0,05)
  • Agregar atribuições de subpalavras ao nível da palavra para comparar IG e SHAP corretamente
  • Adicionar early stopping (patience=2) — o modelo convergiu na época 4

Modelo e arquitetura

  • Testar r=16 e r=32 (mais capacidade, mas monitorar overfitting)
  • Substituir backbone por DeBERTa-v3-base (melhor em tarefas desbalanceadas)
  • Aumentar max_length para 384 (capturar avaliações mais longas)

Produção

  • Exportar para ONNX (inferência CPU 3–5x mais rápida)
  • Upload do adapter para HuggingFace Hub (hub.push_adapter: true)
  • Threshold baseado em custo de negócio (FN ≠ FP) em vez de F1 puro
  • Monitorar ECE em produção — recalibrar com isotônico se ultrapassar 0,05

13. Referências

  • Dataset: Amazon US Customer Reviews — Furniture (Kaggle, 2000–2015)
  • DistilBERT: Sanh et al. (2019). DistilBERT, a distilled version of BERT. arXiv:1910.01108
  • LoRA: Hu et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models. arXiv:2106.09685
  • Revisão crítica de PEFT: Xu et al. (2023). Parameter-Efficient Fine-Tuning Methods for Pretrained Language Models: A Critical Review and Assessment. arXiv:2312.12148

Código e experimentos reproduzíveis disponíveis em: (repositório público — a ser adicionado)