Machine learning do zero · Aula 2.5 · 12 de 33 aulas prontas · 12 min
Classes desbalanceadas
Em resumo
Quando uma classe é rara, a acurácia esconde as perdas e o modelo aprende a dizer “a classe comum”. Sensibilidade, acurácia balanceada e pesos corrigem isso.
Aquecimento Duas perguntas de aulas anteriores. Um minuto, e a aula nunca fica bloqueada.
Na aula 1.3, “sempre benigno” tinha acurácia alta e não encontrava ninguém. Lá a classe rara era rara só no teste. Hoje ela é rara nos dados de aprendizado, como na triagem real, em fraudes ou em falhas. Você vai ver o que isso faz com o modelo, como medir e o que dá para consertar sem dados novos.
Escolha uma resposta e o seu grau de certeza.
Responda para liberar o experimento.
Experimento interativo
Este experimento interativo funciona em um navegador com JavaScript ativado. O texto ao redor descreve o que ele mostra.
Ver a resposta
Resposta certa: A sensibilidade, mais de 20 pontos, e a acurácia só cerca de 6 pontos
A sensibilidade cai de 83,0% para 61,5% (média de 5 sorteios de quais malignas ficam), e a acurácia só de 91,5% para 85,6%. Na validação 89 das 142 biópsias são benignas, e o modelo as reconhece com cada vez mais segurança, então a acurácia quase não vê a perda.
Experimente
- Sem pesos, mova o controle de 159 para 8 malignas. Qual linha cai mais rápido: a sensibilidade ou a acurácia?
- Com 8 malignas, ligue os pesos de classe. Quanta sensibilidade volta, e quanto ainda falta em relação a todos os dados?
- Olhe a P(maligno) média prevista. Como ela muda quando as malignas somem?
Depois de conferir no lab, vamos dar nome ao que você vê.
A ideia em poucas palavras
As classes estão desbalanceadas quando uma é muito mais rara que a outra. O modelo então aprende duas coisas ao mesmo tempo: como são as biópsias malignas e quão raras elas são. Essa segunda lição funciona como um preconceito: quando as medidas não são claras, o modelo escolhe “benigno” cada vez mais. A P(maligno) média prevista cai de 0,36 para 0,24 quando ficam 8 malignas no aprendizado em vez de 159.
A acurácia quase não vê isso. A sensibilidade cai de 83,0% para 61,5%, a acurácia só de 91,5% para 85,6%. Por isso, com uma classe rara, informa-se a sensibilidade da classe rara ou a acurácia balanceada, a média das sensibilidades das duas classes. “Sempre benigno” tem acurácia balanceada de 0,5, sejam quais forem as proporções.
A primeira correção é simples: os pesos de classe. Um erro na classe rara custa no aprendizado tantas vezes mais quantas vezes a classe é mais rara (class_weight='balanced'). Com 8 malignas, os pesos sobem a sensibilidade de 61,5% para 80,0%, e a acurácia até sobe, para 91,4%. Em relação a todos os dados com pesos (86,8%) ainda falta algo: os pesos mudam as proporções, não acrescentam exemplos.
Jak to policzono
- Biópsias: Breast Cancer Wisconsin (Diagnostic), UCI, licença CC BY 4.0. 569 biópsias, 10 medidas “mean”.
- Parte de validação: 25% das biópsias, estratificada (142: 53 malignas, 89 benignas), a mesma em todos os ajustes. No aprendizado, 268 benignas e de 159 a 8 malignas; abaixo de 159 é a média de 5 sorteios de quais malignas ficam.
- Modelo: padronização com os dados de aprendizado e regressão logística (C = 1), com ou sem pesos de classe; limiar 0,5.
Por que funciona
A regressão logística procura o modelo que erra menos em média nos dados de aprendizado. Com 268 benignas e 8 malignas, as malignas são só 2,9% da penalidade total. O modelo empurra a fronteira para o lado maligno: abre mão de algumas malignas para errar menos benignas. Os pesos de classe igualam a penalidade: as 8 malignas juntas pesam o mesmo que as 268 benignas.
Uma divisão poderia sair assim por acaso. No código abaixo, o mesmo experimento roda em 50 divisões diferentes: a sensibilidade cai em média de 89,3% para 67,3%, a acurácia de 93,5% para 87,7%, e os pesos com 8 malignas dão 84,0%. A sensibilidade cai mais que a acurácia em 50 divisões de 50, e os pesos a sobem em todas elas.
A acurácia pode até subir sem que o modelo mude nada. No widget do Atlas sobre acurácia balanceada, um modelo fraco com duas medidas pontua as mesmas biópsias enquanto as malignas vão de 212 para 20. A acurácia dele sobe de 75,9% para 83,8%, e a acurácia balanceada fica parada: 0,728 e 0,726. “Sempre benigno” com 20 malignas chega a 94,7% de acurácia.
Erro comum
Verdadeiro ou falso: “SMOTE sempre ajuda”. Falso. O SMOTE cria exemplos sintéticos da classe rara nos segmentos entre exemplos reais e seus vizinhos mais próximos, até as duas classes terem o mesmo tamanho. Isso move a fronteira de decisão do mesmo jeito que os pesos de classe: o modelo diz “maligno” mais vezes. No widget do Atlas sobre SMOTE (357 benignas, 30 malignas, duas medidas, validação cruzada 5 × 5), o modelo sem correção encontra 60,0% das malignas com precisão de 90,0%. O SMOTE sobe a detecção para 86,7%, mas a precisão cai para 42,9%. Os pesos de classe dão a mesma sensibilidade com precisão de 40,2%, e a simples duplicação com 39,6%. É um sorteio de 30 malignas, o de maior queda: em média em 20 sorteios a detecção sobe de 68,7% para 91,9% e a precisão cai de 91,0% para 54,7%. Ela cai em todos eles. O SMOTE não acrescenta conhecimento, só muda as proporções. Se isso ajuda depende do custo dos erros: mais detecções ao preço de muitos alarmes falsos.
Um aviso: a correção vale só para os dados de aprendizado. Exemplos sintéticos ou duplicados na validação ou no teste dariam um resultado que não significa nada.
Quiz
Três perguntas para você se testar. Escolha uma resposta ou abra a resposta embaixo de cada pergunta. No teclado: as setas escolhem, Enter ou Espaço confirma.
Pontos-chave
- Quando uma classe é rara, a acurácia esconde as perdas: informe a sensibilidade da classe rara ou a acurácia balanceada.
- O modelo também aprende as proporções das classes; com uma classe rara no aprendizado ele escolhe a classe comum cada vez mais.
- Pesos de classe e SMOTE movem a fronteira: mais detecções ao preço de alarmes falsos, sem conhecimento novo.
- Aplique as correções só aos dados de aprendizado e confira o resultado numa validação intacta, de preferência em muitas divisões.
Mais a fundo: fórmula, código, prática
A matemática
Acurácia = (TP + TN) / n, sensibilidade = TP / (TP + FN), especificidade = TN / (TN + FP), acurácia balanceada = (sensibilidade + especificidade) / 2. “Sempre benigno” tem sensibilidade 0 e especificidade 1, então acurácia balanceada de (0 + 1) / 2 = 0,5.
A regressão logística minimiza a perda logística média L = Σᵢ wᵢ · ℓ(yᵢ, pᵢ) / Σᵢ wᵢ. Sem pesos wᵢ = 1, então as 8 malignas são 8 / 276 ≈ 2,9% da perda total. Os pesos “balanced” são w = n / (2 · n_classe): 276 / 16 = 17,25 para uma maligna e 276 / 536 ≈ 0,51 para uma benigna, então cada classe pesa no total n / 2 = 138.
No código (Python)
O mesmo experimento no scikit-learn em 50 divisões diferentes: todas as benignas e 159 ou 8 malignas no aprendizado, com e sem pesos de classe, avaliado na parte de validação intacta.
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import recall_score, accuracy_score
X, y = load_breast_cancer(return_X_y=True)
X, y = X[:, :10], 1 - y # as 10 medidas "mean", como no lab; 1 = maligno
def run(seed, m, weights):
tr, va = train_test_split(np.arange(len(y)), test_size=0.25, stratify=y, random_state=seed)
mal = np.random.default_rng(seed).choice(tr[y[tr] == 1], m, replace=False)
keep = np.r_[tr[y[tr] == 0], mal] # todas as benignas + m malignas para aprender
clf = make_pipeline(StandardScaler(), LogisticRegression(class_weight="balanced" if weights else None))
pred = clf.fit(X[keep], y[keep]).predict(X[va]) # a parte de validação nunca muda
return recall_score(y[va], pred), accuracy_score(y[va], pred)
for m, w in ((159, False), (8, False), (8, True)):
rec, acc = np.mean([run(s, m, w) for s in range(50)], axis=0)
print(f"{m} malignant{' + class weights' if w else ''}: recall {rec:.1%}, accuracy {acc:.1%} (mean of 50 splits)")
drop = np.array([np.subtract(run(s, 159, False), run(s, 8, False)) for s in range(50)])
gain = np.array([run(s, 8, True)[0] - run(s, 8, False)[0] for s in range(50)])
print(f"recall fell more than accuracy in {(drop[:, 0] > drop[:, 1]).sum()} of 50 splits; weights raised recall in {(gain > 0).sum()}")
Saída
159 malignant: recall 89.3%, accuracy 93.5% (mean of 50 splits)
8 malignant: recall 67.3%, accuracy 87.7% (mean of 50 splits)
8 malignant + class weights: recall 84.0%, accuracy 91.0% (mean of 50 splits)
recall fell more than accuracy in 50 of 50 splits; weights raised recall in 50
O lab mostra uma divisão e 5 sorteios, o código 50 divisões, então os números são outros: na divisão do lab a sensibilidade com todos os dados é de 83,0%, em média 89,3%. A conclusão é a mesma em cada divisão.
Baixar o notebook (.ipynb). Abra no Jupyter ou envie para o Google Colab; ele baixa sozinho os dados que faltarem.
Para quem pratica
- No scikit-learn os pesos vêm de
class_weight="balanced"(regressão logística, SVM, árvores, florestas) e a acurácia balanceada debalanced_accuracy_score. Informe também a sensibilidade e a precisão da classe rara. - Divida com estratificação (
stratify=y,StratifiedKFold), para que a classe rara chegue a cada parte. - SMOTE e duplicação estão na biblioteca imbalanced-learn. Use-os dentro do
Pipelinedela, para que atuem só nas partes de aprendizado da validação cruzada. - Pesos de classe e um limiar mais baixo fazem algo parecido: movem a decisão para o lado da classe rara. O limiar pode ser escolhido na validação pela curva PR (aula 2.3), sem treinar o modelo de novo.
Isto foi útil?
Obrigado! Isso nos ajuda a melhorar.
Vá mais fundo no Atlas
Verbetes da enciclopédia sobre as ideias desta aula: