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.
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.
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: 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.
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.
Baixar o notebook (.ipynb). Abra no Jupyter ou envie para o Google Colab; ele baixa sozinho os dados que faltarem.
Para quem pratica
- Em scikit-learn:
StandardScaler,MinMaxScalereRobustScalerpara números,OneHotEncoder(handle_unknown="ignore")para categorias.ColumnTransformermanda colunas numéricas e categóricas para transformações diferentes, e tudo vai dentro demake_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.
Isto foi útil?
Obrigado! Isso nos ajuda a melhorar.
Vá mais fundo no Atlas
Verbetes da enciclopédia sobre as ideias desta aula: