Um XGBoost com feature engineering em 3 frentes contra um DistilBERT fine-tunado: mesmo dataset, mesmo split, mesmas métricas. O placar surpreende: empate técnico, e quem vence de verdade é a combinação dos dois.
Avaliação em 1.963 frases de teste jamais vistas no treino · dataset Banking77 (PolyAI) · métricas e exemplos desta página extraídos diretamente dos artefatos reais de avaliação.
O Banking77 reúne perguntas reais de clientes de um banco digital. O problema não é o volume, é a granularidade: são 77 intenções, e muitas delas são vizinhas semânticas separadas por nuances ("a transferência está pendente" vs "a transferência demora quanto tempo?").
random_state=42, idêntico para os dois modelosTrês frases reais do conjunto de teste. Escolha a intenção correta e depois veja como cada modelo se saiu nessa mesma frase.
I can't pull my card out of the ATM. Help me.
How long until my transfer goes through?
Why is your exchange rate so bad?
De um lado, o time do feature engineering: transformar o texto em números informativos e deixar o gradient boosting decidir. Do outro, o time do fine-tuning end-to-end: entregar o texto cru a um transformer e deixar que ele aprenda tudo sozinho.
O texto é decomposto por três frentes complementares de features, concatenadas em um vetor de 491 dimensões que alimenta o XGBoost.
all-MiniLM-L6-v2 (22M parâmetros, congelado, nunca é treinado). Captura o significado da frase inteira.max_depth=6, lr=0.1, subsample/colsample 0.8, early stopping de 20 rodadas na validação.O transformer recebe o texto cru e ajusta todos os seus 66M de parâmetros para a tarefa. Nenhuma feature manual: a representação é aprendida.
distilbert-base-uncased: destilação do BERT com ~97% da performance, 40% menor e 60% mais rápido.lr=3e-5, batch 32, warmup 200 passos, weight decay 0.01, fp16 em GPU. Avaliação a cada época na validação.fit_transform acidental no teste.Ambos treinam nas mesmas 9.157 frases e validam nas mesmas 1.963, split estratificado com random_state=42. Nenhuma vantagem de dados para ninguém.
A validação serve para early stopping e escolha de checkpoint. As 1.963 frases de teste só aparecem uma única vez, na avaliação final.
Medida frase a frase (single-sample, como um chatbot real), com warmup e perf_counter: P50 e P95 sobre centenas de execuções.
No teste cego com 1.963 frases, o XGBoost híbrido ficou 0,5 ponto à frente do DistilBERT em F1 macro, um empate técnico que já derruba o mito de que "só transformer resolve". A surpresa real vem depois: combinando os dois, o F1 sobe quase 1,5 ponto.
F1 macro trata as 77 classes igualmente; F1 weighted pondera pelo nº de exemplos. Barras em escala de 0 a 100%.
Acurácia: 91,2% (XGB) · 90,8% (DistilBERT) · 92,7% (Ensemble). Curiosidade: o plano do projeto previa 85-88% para o XGBoost híbrido, e ele entregou 91,4% e superou a própria expectativa.
Tempo de ponta a ponta para classificar uma frase (como em um chatbot), incluindo feature engineering no caso do XGBoost. Medido nesta máquina, com GPU disponível (RTX 4070 Laptop).
⚠️ Leitura honesta: com GPU, o DistilBERT é o mais rápido, e a latência do XGBoost híbrido é dominada pelo encode do SBERT, não pelas árvores. Em servidor só-CPU o jogo inverte: transformers tipicamente sobem para dezenas ou centenas de ms, enquanto o pipeline clássico muda pouco. Latência é uma decisão de infraestrutura tanto quanto de modelo.
F1 macro e loss na validação ao fim de cada época. Uma época não basta: o F1 salta de 0.57 para 0.91 ao longo de 5 épocas.
Enquanto isso, o XGBoost híbrido converge em minutos de CPU, com early stopping na validação, sem GPU, sem checkpoints, sem scheduler. O custo de treino das duas abordagens vive em ordens de grandeza diferentes.
F1 por classe (média dos dois modelos, as piores primeiro). Repare: quase todas são da família "transferências e recargas que ainda não aconteceram".
No extremo oposto, 3 intenções saíram perfeitas (F1 = 1.00) nos dois modelos: change_pin, get_physical_card e passcode_forgotten. Vocabulário inconfundível.
Principais confusões no teste (intenção verdadeira → intenção predita) e quantas vezes cada modelo caiu nelas.
| Confusão (verdade → predição) | XGB | BERT | Por que confunde |
|---|---|---|---|
| card_delivery_estimate → card_arrival | 5 | 7 | "quando chega meu cartão?" vs "meu cartão já chegou?". Praticamente a mesma frase |
| verify_my_identity → why_verify_identity | 3 | 5 | "como verifico?" vs "por que verificar?". Uma palavra muda a intenção |
| transfer_timing ↔ balance_not_updated_after_bank_transfer | 6 | 4 | a mesma ansiedade ("cadê minha transferência?") com causas diferentes |
| transfer_fee_charged → card_payment_fee_charged | 3 | 3 | taxa inesperada, mas foi na transferência ou no cartão? |
| wrong_exchange_rate_for_cash_withdrawal → card_payment_wrong_exchange_rate | 4 | - | câmbio errado no saque vs no pagamento, só o contexto distingue |
| pending_top_up → top_up_failed | - | 4 | recarga pendente vs falhada: o desfecho ainda não existe na frase |
Leitura importante: os erros não são aleatórios: concentram-se em pares semanticamente legítimos, onde até um atendente humano hesitaria. É o "teto natural" do dataset aparecendo.
Se dois modelos têm a mesma acurácia mas erram em frases diferentes, há informação grátis na mesa. Foi exatamente o que aconteceu: XGBoost e DistilBERT discordam em 9% das frases, e é nessa zona de discordância que o ensemble constrói sua vantagem.
Quem acerta cada uma das 1.963 frases de teste:
Pens(classe) = 0.5 · Pxgb + 0.5 · Pbert
Nada de retreino: apenas a média das distribuições de probabilidade dos dois modelos. Quando um está confiante e o outro em dúvida, o confiante domina a decisão. Um desempate automático e gratuito.
Casos do conjunto de teste em que os modelos discordaram, e o que cada disputa ensina sobre os dois paradigmas.
Why should I have to prove my identity?
O "Why" inicial é uma pista lexical que o TF-IDF captura com força. Detalhe honesto: aqui a confiança do BERT era tão alta que arrastou o ensemble para o erro: soft voting não é infalível.
How long until my transfer goes through?
O bigrama "how long" é praticamente uma assinatura da classe de prazo, exatamente o tipo de padrão em que features n-gram brilham.
Tell me how to renew my new card?
O verbo "renew" seduz o transformer para a classe de expiração; o léxico "new card" mantém o XGBoost ancorado na ativação.
Is there a problem with the top up system? My transaction hasn't gone through properly
"hasn't gone through" ainda não é "failed": a nuance de estado pendente favoreceu a combinação léxico + heurísticas.
Can I get support?
Quatro palavras, zero sobreposição com o nome da classe. Sem pista lexical, só a semântica aprendida no fine-tuning ("support" = disponibilidade do serviço) resolve.
I can't pull my card out of the ATM. Help me.
Nenhuma palavra da frase aparece no rótulo: é preciso entender a cena (a máquina reteve o cartão). Compreensão contextual pura.
Why is your exchange rate so bad?
Reclamar da taxa aplicada ≠ perguntar a cotação. O BERT leu o tom; o XGBoost viu as palavras "exchange rate" e, desta vez, arrastou o ensemble junto.
show me how to transfer to my account
Frase informal, sem pontuação, vocabulário genérico: o terreno onde representações contextuais superam contagens de termos.
How long until my transfer goes through?
Divergência resolvida a favor do XGBoost: sua distribuição de probabilidade estava mais concentrada (mais confiante) que a do BERT.
I can't pull my card out of the ATM. Help me.
Agora o desempate pende para o BERT: o XGBoost estava em dúvida entre várias classes de saque, e a média premiou a convicção do transformer.
Is there a problem with the top up system? My transaction hasn't gone through properly
Mais um resgate: dos 142 casos em que exatamente um modelo acertava, 105 terminaram assim, com o ensemble do lado certo.
show me how to transfer to my account
O padrão geral: modelos com "olhares" diferentes raramente erram juntos, e é isso que torna a média das probabilidades tão eficaz.
Não existe vencedor absoluto, existe o modelo certo para cada restrição de produção. O resumo executivo da disputa:
| Critério | 🌳 XGBoost Híbrido | 🧠 DistilBERT | 🤝 Ensemble |
|---|---|---|---|
| F1 macro (teste) | 0.9135 | 0.9085 | 0.9280 |
| Latência com GPU (P50) | 13,6 ms | 9,4 ms | 34,0 ms |
| Deploy só-CPU | pouco muda (~15-40 ms) | degrada muito (50-200 ms típicos) | herda o pior dos dois |
| Custo de treino | minutos, em CPU | ~30-60 min sem GPU; minutos com GPU | zero (reusa os dois) |
| Interpretabilidade | alta (feature importance por frente) | baixa (atenção ≠ explicação) | média (decomponível por modelo) |
| Memória de inferência | ~500 MB | ~1,5 GB | ~2 GB |
| Novas features de negócio | plugáveis (Frente C) | exigem retreino completo | plugáveis no lado XGB |
…o deploy é CPU-only ou edge, o orçamento de latência é apertado, o time precisa explicar decisões (auditoria/compliance) ou o modelo será retreinado com frequência.
…há GPU na inferência, as frases fogem do vocabulário conhecido (semântica manda), ou você quer zero manutenção de features, além de um caminho natural para modelos multilíngues.
…cada ponto de F1 vale dinheiro (roteamento errado = cliente irritado) e 34 ms de P50 cabem no SLA. É o ganho mais barato do projeto: nenhum retreino, +1,45pp.
O "clássico" empatou com o transformer por uma fração do custo de treino. Sem esse baseline, o fine-tuning pareceria indispensável, e ninguém saberia que não era.
Embeddings congelados viraram feature: a Frente B entrega a semântica do transformer sem pagar o fine-tuning. As 7 heurísticas artesanais continuam somando sinal que embeddings diluem.
9% de divergência entre dois modelos de ~91% virou +1,45pp de F1 com uma média de probabilidades. Antes de escalar o modelo, verifique se a diversidade que você já tem está sendo usada.
Com GPU, o DistilBERT é o mais rápido; em CPU, o jogo inverte completamente. O mesmo benchmark, em outra máquina, contaria outra história. Meça no hardware do deploy.
91% de F1 macro parece ótimo até você ver why_verify_identity a 0.69 no BERT. A análise por classe e por par confundido é onde moram as decisões de produto.
Split estratificado idêntico, validação para escolher, teste visto uma única vez, latência com warmup e percentis. Sem isso, qualquer comparação é ruído.
python -m src.train_xgboost → python -m src.train_hf → python -m src.evaluate. O repositório inclui ainda um app Streamlit que roda os três modelos lado a lado em qualquer frase que você digitar. Código, seeds e instruções completas no GitHub.