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:
| Split | Fronteira | Linhas | % Insatisfeitos |
|---|---|---|---|
| Train | ≤ 2014-12-31 | 406.119 | 16,3% |
| Val | 2015-01-01 → 2015-04-30 | 88.160 | 15,8% |
| Test | 2015-05-01 → 2015-08-31 | 81.715 | 16,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.
| Modelo | AUC-ROC | F1 |
|---|---|---|
| Classe majoritária | 0,500 | 0,000 |
| TF‑IDF + Regressão Logística | 0,991 | 0,893 |
| DistilBERT + LoRA | 0,996 | 0,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 caso | Exemplo | TF‑IDF | Transformer |
|---|---|---|---|
| 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 ambos | Entende a concessão |
| Título como prior | “One Star. Very low quality…” | Título é só mais um token | Usa 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
| Época | F1 | AUC-ROC | ECE | Brier |
|---|---|---|---|---|
| 1 | 0,9221 | 0,9947 | 0,0159 | 0,0200 |
| 2 | 0,9375 | 0,9950 | 0,0109 | 0,0160 |
| 3 | 0,9406 | 0,9960 | 0,0117 | 0,0158 |
| 4 | 0,9427 | 0,9957 | 0,0105 | 0,0151 |
| 5 | 0,9426 | 0,9957 | 0,0117 | 0,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 quebrou | O que ensinou |
|---|---|
576k linhas todas em train | Sempre inspecionar df.date.min() e .max() |
| LoRA apontando para 0 módulos | Nomes de módulos diferem entre famílias — use model.named_modules() |
uv sync sem solução para Python 3.12 | PyTorch 2.2.x não tem wheels para 3.12+ — limite superior no requires-python |
compute_class_weight com lista | Sklearn exige np.ndarray, não lista |
| Falha na resolução da camada de embedding no IG | PEFT adiciona prefixo base_model.model.* — lookup por caminho quebra |
Shape mismatch no captum | Máscaras de atenção precisam ser expandidas para [n_steps, seq_len] |
n_steps=50 → convergência delta > 0,05 | Usar 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_stepsdo 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=16er=32(mais capacidade, mas monitorar overfitting) - Substituir backbone por DeBERTa-v3-base (melhor em tarefas desbalanceadas)
- Aumentar
max_lengthpara 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)
