ML Atlas

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.
Da aula 2.4: Calibração: 0,8 quer dizer 80%? O naive Bayes tem ROC AUC 0,966, mas declara 98,3% com acurácia de 88,3%. O que se conclui?

Ver a resposta

Resposta certa: Ordena bem as biópsias, mas suas porcentagens são infladas

O ROC AUC mede a ordem, a calibração mede as porcentagens. São coisas diferentes: a temperatura corrige as porcentagens do naive Bayes, e o ROC AUC continua 0,966.

Da aula 2.3: Limiar, ROC e PR: todos os limiares de uma vez Quando a curva PR diz mais que a curva ROC?

Ver a resposta

Resposta certa: Quando a classe positiva é rara e o que importa é quantos alarmes estão certos

Com uma classe rara a curva ROC parece igual enquanto a precisão cai. No lab com 19 malignas o AP cai para 0,137 em média, embora o ROC AUC quase não se mexa.

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.

Preveja, depois confira Uma regressão logística aprende com biópsias. Nos dados de aprendizado ficam todas as benignas, mas só 8 malignas em vez de 159. Na mesma parte de validação (separada para verificar o modelo, não para aprender), o que cai mais: a acurácia ou a sensibilidade?
Qual é o seu grau de certeza?

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.

1. Ficam 8 biópsias malignas no aprendizado em vez de 159. Por que a acurácia quase não mostra isso?
Ver a resposta

Resposta certa: Porque na validação há mais benignas, e o modelo as reconhece com cada vez mais segurança

Na validação 89 das 142 biópsias são benignas. A sensibilidade cai de 83,0% para 61,5%, a acurácia só de 91,5% para 85,6%.

2. O que a acurácia balanceada mede?
Ver a resposta

Resposta certa: A média das sensibilidades das duas classes, então “sempre a comum” tira 0,5

Cada classe conta igual, seja qual for o tamanho. O modelo fraco do widget tem acurácia balanceada de 0,728 com 212 malignas e de 0,726 com 20, embora a acurácia dele suba de 75,9% para 83,8%.

3. Com 8 malignas no aprendizado, os pesos de classe sobem a sensibilidade de 61,5% para 80,0%. O que eles não fazem?
Ver a resposta

Resposta certa: Não acrescentam exemplos novos, então ainda falta algo em relação a todos os dados

Os pesos só mudam quanto custa um erro em cada classe. Todos os dados com pesos dão sensibilidade de 86,8%, então com 8 malignas faltam 6,8 pontos.

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.

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 de balanced_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 Pipeline dela, 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.

Vá mais fundo no Atlas

Verbetes da enciclopédia sobre as ideias desta aula: