ML Atlas

Machine learning do zero · Aula 4.3 · 22 de 33 aulas prontas · 12 min

Categorias e escalonamento

Em resumo

O kNN em vinhos acerta 0,695 sem escalonamento e 0,963 após padronizar. Ilhas escritas como 0, 1, 2 fingem uma ordem: o one-hot sobe o R² de 0,469 para 0,546.

Aquecimento Duas perguntas de aulas anteriores. Um minuto, e a aula nunca fica bloqueada.
Da aula 4.2: Dados faltantes O IMC falta nos 110 pacientes mais pesados. O que faz preencher com a média dos outros?

Ver a resposta

Resposta certa: Enfraquece a relação IMC–doença, porque os mais doentes caem num IMC médio

A correlação cai de 0,586 para 0,352. 110 pacientes com muita progressão ficam num só ponto: na média de 24,3.

Da aula 4.1: Exploração: olhe antes de modelar Verdadeiro ou falso: "as estatísticas descritivas bastam, desenhar os dados é perda de tempo".

Ver a resposta

Resposta certa: Falso, porque os mesmos números podem esconder um arco, grupos ou um ponto distante

No quarteto os mesmos números escondem quatro imagens diferentes, e em 342 pinguins uma correlação tem o sinal oposto ao de cada espécie. Um desenho mostra isso na hora.

Na aula 1.4 o kNN acertava 76,7% dos pinguins sem escalonamento e 96,1% após padronizar, porque a massa em gramas abafava as outras medidas. Hoje, duas coisas que estragam um modelo antes de ele começar a aprender. A primeira são atributos em escalas muito diferentes; o escalonamento os leva a uma faixa comparável. A segunda são categorias, nomes no lugar de números, como a ilha de onde vem um pinguim. A codificação de categorias as transforma em números, e dá para fazer isso mal.

Preveja, depois confira O kNN (k = 5) reconhece a variedade de um vinho a partir de 13 medidas químicas. A prolina tem desvio-padrão de 314; a medida menos variável, de 0,124. Sem escalonamento, a acurácia na validação cruzada é 0,695. Quanto fica após padronizar, ou seja, após converter cada medida em desvios-padrão em relação à média?
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: Muito mais, porque depois de padronizar todas as 13 medidas contam na distância

Após padronizar, 0,963 em vez de 0,695. Sem escalonamento, a distância entre vinhos é quase só a diferença de prolina. Após padronizar, cada medida tem desvio-padrão 1 e vota em pé de igualdade com as outras. O min-max dá 0,958; o escalonamento robusto, 0,946.

Experimente

  • Comece pelos vinhos sem escalonamento. Que medida ocupa quase toda a altura do gráfico?
  • Mude para min-max, padronização e escalonamento robusto. Quanto o kNN ganha e quanto os três diferem entre si?
  • Vá para os pinguins. Compare os códigos 0, 1, 2 em ordem alfabética e na ordem do arquivo. Um modelo deveria depender da ordem em que você escreve as ilhas?
  • Escolha one-hot. Onde ficam agora as linhas de previsão em relação aos pontos?

Depois de conferir no lab, vamos dar nome ao que você vê.

A ideia em palavras simples

O kNN considera dois vinhos parecidos quando eles estão perto nas 13 medidas ao mesmo tempo. A prolina tem desvio-padrão de 314 e a medida menos variável só 0,124, então na soma das diferenças a prolina cobre todo o resto. O escalonamento iguala essas faixas. O min-max leva cada medida ao intervalo de 0 a 1, a padronização à média 0 e desvio-padrão 1, o escalonamento robusto subtrai a mediana e divide pela amplitude interquartil, então valores extremos isolados o movem menos. Nos vinhos o kNN acerta 0,958, 0,963 e 0,946 em vez de 0,695. A escolha do método importa menos do que escalonar.

As categorias têm outro problema. Uma ilha é Biscoe, Dream ou Torgersen, e uma regressão linear precisa de números. A ideia mais simples são os códigos 0, 1, 2. Mas um número carrega uma ordem e uma distância: um modelo com um só peso vai supor que a ilha de código 1 fica exatamente no meio do caminho entre as outras duas. No lab uma regressão prevê a massa de um pinguim a partir da ilha e do sexo. Com códigos alfabéticos o R² na validação cruzada é 0,469. Os mesmos códigos na ordem do arquivo dão 0,193, embora os dados sejam idênticos. O one-hot dá a cada ilha sua própria coluna 0/1 e seu próprio peso: R² 0,546, e o erro médio cai de 476 para 411 g.

Jak to policzono
  • Vinhos: Wine (Aeberhard e Forina, 1992; UCI, licença CC BY 4.0), 178 vinhos, 13 medidas químicas, 3 variedades. kNN com k = 5, validação cruzada em 5 partes, repetida 10 vezes com outro embaralhamento. O escalonador é ajustado só na parte de treino de cada divisão.
  • Pinguins: Palmer Penguins (Gorman, Williams e Fraser, 2014; pacote palmerpenguins: Horst, Hill e Gorman, 2020), licença CC0. 333 de 344 pinguins: 11 não têm o sexo registrado. Regressão linear: massa a partir da ilha e do sexo (uma coluna 0/1), validação cruzada em 5 partes, 10 repetições. Linhas do gráfico: o modelo ajustado com os 333.
  • R²: a parte da variação da massa explicada nas partes de validação (1 = toda, 0 = o mesmo que a média). Erro médio: a diferença absoluta média em gramas.

Por que funciona

A distância no kNN é a raiz quadrada da soma das diferenças ao quadrado. Uma medida com grande dispersão dá diferenças grandes, e são elas que mandam na soma. Padronizar divide cada medida pelo seu desvio-padrão, então uma diferença típica tem tamanho parecido em todas as medidas. As médias, desvios, mínimos e máximos do escalonador são calculados com dados, igual aos pesos de um modelo. Por isso o escalonador é ajustado só na parte de treino: no código abaixo, um escalonador ajustado com todos os vinhos antes da validação cruzada dá 0,965 em vez de 0,962. Aqui a diferença é pequena, mas esse resultado viu a parte de validação, então não é uma estimativa honesta.

Com códigos de ilha, a regressão linear tem um só peso para a coluna inteira. Então do código 0 para o 1 e do 1 para o 2 a massa prevista muda o mesmo tanto. Com códigos alfabéticos o modelo prevê que os pinguins de Dream pesam 625 g a menos que os de Biscoe, e os de Torgersen outros 625 g a menos. Nos dados os pinguins de Dream e de Torgersen pesam quase o mesmo (3719 e 3709 g), e os de Biscoe 4719 g. Na ordem do arquivo Biscoe recebe o código do meio, e a reta pelas três ilhas quase fica plana: 243 g por passo. O one-hot não supõe nenhuma ordem: o modelo prevê para Dream e para Torgersen 997 g a menos que para Biscoe, como nos dados.

Erro comum

Verdadeiro ou falso: "codifique as categorias como 1, 2, 3, o modelo dá conta". Falso. Um número carrega uma ordem e distâncias iguais que não existem entre ilhas. No lab basta reordenar as ilhas para o R² passar de 0,469 para 0,193, embora os dados sejam os mesmos. O one-hot dá 0,546 com qualquer ordem. No código abaixo o one-hot vence os códigos alfabéticos em 49 de 50 folds de validação.

A exceção é o sexo: com duas categorias basta uma coluna 0/1, porque não há um meio onde algo possa cair. O problema começa com três categorias sem ordem natural.

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. Por que o kNN sem escalonamento acerta só 0,695 nos vinhos?
Ver a resposta

Resposta certa: Porque a prolina tem dispersão enorme e manda na distância inteira

A prolina tem desvio-padrão de 314; a medida menos variável, de 0,124. Sem escalonamento a distância é quase só a diferença de prolina. Após padronizar a acurácia sobe para 0,963.

2. As ilhas são codificadas Biscoe = 0, Dream = 1, Torgersen = 2. O que supõe uma regressão linear com um só peso para essa coluna?
Ver a resposta

Resposta certa: Que Dream fica no meio do caminho entre Biscoe e Torgersen, em passos iguais

Um só peso significa passos iguais: o modelo prevê 625 g a menos de Biscoe para Dream e outros 625 g de Dream para Torgersen. Nos dados Dream e Torgersen pesam quase o mesmo.

3. Verdadeiro ou falso: "codifique as categorias como 1, 2, 3, o modelo dá conta".
Ver a resposta

Resposta certa: Falso, porque os códigos trazem uma ordem e distâncias que os dados não têm

Com códigos um modelo linear vê uma escada de passos iguais. O one-hot dá a cada categoria seu próprio peso: R² 0,546 em vez de 0,469 ou 0,193.

Pontos-chave

  • Modelos que medem distâncias, como o kNN, precisam de atributos em escala parecida: nos vinhos 0,695 sem escalonamento, 0,963 após padronizar.
  • Min-max, padronização e escalonamento robusto dão aqui resultados parecidos; o mais importante é escalonar.
  • Ajuste o escalonador só na parte de treino, separadamente em cada divisão.
  • Codifique com one-hot as categorias sem ordem: os códigos 0, 1, 2 impõem uma ordem e passos iguais que os dados não têm.

Mais a fundo: fórmula, código, prática

A matemática

Min-max: x' = (x − min) / (max − min). Padronização: z = (x − x̄) / s. Escalonamento robusto: x' = (x − mediana) / IQR, onde IQR = Q3 − Q1 é a amplitude interquartil. Distância no kNN: d(a, b) = √(Σⱼ (aⱼ − bⱼ)²); uma medida com s grande dá (aⱼ − bⱼ)² grandes e cobre o resto.

Códigos de ilha numa regressão linear: ŷ = a + w · código + v · macho. Com códigos alfabéticos ŷ(Dream) − ŷ(Biscoe) = ŷ(Torgersen) − ŷ(Dream) = w: passos iguais. One-hot: ŷ = a + w_D · [Dream] + w_T · [Torgersen] + v · macho, onde [Dream] vale 1 para um pinguim de Dream e 0 para os demais. Cada ilha tem seu próprio deslocamento, com Biscoe como referência.

No código (Python)

O mesmo experimento em scikit-learn: kNN nos vinhos com quatro escalonamentos em validação cruzada 10 × 5 (o escalonador fica num pipeline, então é ajustado em cada divisão), um escalonador ajustado com todos os vinhos para comparar e a massa dos pinguins com a ilha codificada de três jeitos.

import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score, RepeatedStratifiedKFold, RepeatedKFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression

X, y = load_wine(return_X_y=True)
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=0)
acc = {}
for name, scaler in [("no scaling", None), ("min-max", MinMaxScaler()), ("standardised", StandardScaler()), ("robust", RobustScaler())]:
    model = make_pipeline(scaler, KNeighborsClassifier(5)) if scaler else KNeighborsClassifier(5)  # o escalonador num pipeline: ajustado em cada divisão
    acc[name] = cross_val_score(model, X, y, cv=cv)
print("kNN on 178 wines, 10 x 5 CV: " + ", ".join(f"{k} {v.mean():.3f}" for k, v in acc.items()))
print(f"standardised beats no scaling in {(acc['standardised'] > acc['no scaling']).sum()} of 50 folds")
leaky = cross_val_score(KNeighborsClassifier(5), StandardScaler().fit_transform(X), y, cv=cv)  # este escalonador também viu as partes de validação
print(f"scaler fitted on all wines before the CV: {leaky.mean():.3f}")
df = pd.read_csv("data/raw/penguins.csv").dropna(subset=["sex", "body_mass_g"])
male = (df.sex == "MALE").to_numpy(float)[:, None]
codings = {
    "codes alphabetical": df.island.map({"Biscoe": 0, "Dream": 1, "Torgersen": 2}).to_numpy(float)[:, None],
    "codes in file order": df.island.map({"Torgersen": 0, "Biscoe": 1, "Dream": 2}).to_numpy(float)[:, None],
    "one-hot": pd.get_dummies(df.island).to_numpy(float),
}
cv2 = RepeatedKFold(n_splits=5, n_repeats=10, random_state=0)
r2 = {}
for name, isl in codings.items():
    Xe = np.hstack([isl, male])
    r2[name] = cross_val_score(LinearRegression(), Xe, df.body_mass_g, cv=cv2, scoring="r2")
    mae = -cross_val_score(LinearRegression(), Xe, df.body_mass_g, cv=cv2, scoring="neg_mean_absolute_error").mean()
    print(f"{len(df)} penguins, body mass from island + sex, {name}: R2 {r2[name].mean():.3f}, mean error {mae:.0f} g")
print(f"one-hot beats the alphabetical codes in {(r2['one-hot'] > r2['codes alphabetical']).sum()} of 50 folds")

Saída

kNN on 178 wines, 10 x 5 CV: no scaling 0.706, min-max 0.963, standardised 0.962, robust 0.952
standardised beats no scaling in 50 of 50 folds
scaler fitted on all wines before the CV: 0.965
333 penguins, body mass from island + sex, codes alphabetical: R2 0.464, mean error 477 g
333 penguins, body mass from island + sex, codes in file order: R2 0.179, mean error 618 g
333 penguins, body mass from island + sex, one-hot: R2 0.542, mean error 412 g
one-hot beats the alphabetical codes in 49 of 50 folds

As divisões não são as do lab, então os números também mudam um pouco. A imagem é a mesma: padronizar vence não escalonar em todos os 50 folds de validação, e o one-hot vence os códigos alfabéticos em 49 de 50. A ordem dos códigos volta a decidir o resultado: 0,464 em ordem alfabética, 0,179 na ordem do arquivo.

Para quem pratica
  • Em scikit-learn: StandardScaler, MinMaxScaler e RobustScaler para números, OneHotEncoder(handle_unknown="ignore") para categorias. ColumnTransformer manda colunas numéricas e categóricas para transformações diferentes, e tudo vai dentro de make_pipeline, para que a validação cruzada as ajuste em cada divisão.
  • Árvores de decisão e florestas aleatórias não precisam de escalonamento: elas cortam uma medida num limiar, e o limiar se move junto com a escala.
  • Uma ordem que existe de verdade (pequeno, médio, grande; nível de escolaridade) pode ser escrita com códigos, mas numa ordem dada explicitamente (OrdinalEncoder(categories=...)).
  • Categorias com centenas de valores (CEP, número de produto) dão centenas de colunas com one-hot. Então se juntam os valores raros num grupo "outros" ou se usa a codificação pela média do alvo calculada fora do fold (um verbete do Atlas), porque calculada com todos os dados ela vaza.

Vá mais fundo no Atlas

Verbetes da enciclopédia sobre as ideias desta aula: