ML Atlas

Machine learning do zero · Aula 4.6 · 25 de 33 aulas prontas · 14 min

Pipeline: da tabela à previsão

Em resumo

O scaler é parte do modelo e aprende só no treino. Um scaler ajustado no próprio teste dá 44,1% em vez de 99,0% quando os pinguins chegam um de cada vez.

Aquecimento Duas perguntas de aulas anteriores. Um minuto, e a aula nunca fica bloqueada.
Da aula 4.5: Vazamento de dados Você preenche os faltantes com a média do arquivo inteiro, divide em treino e teste, treina no treino e avalia no teste. Qual passo vaza?

Ver a resposta

Resposta certa: Preencher as lacunas com a média calculada no arquivo inteiro

A média inclui os dados de teste, então o teste influencia o treino. Ajuste a imputação só no treino, de preferência dentro de um pipeline.

Da aula 4.4: Engenharia de atributos Verdadeiro ou falso: “mais atributos fazem um modelo melhor”.

Ver a resposta

Resposta certa: Falso, porque atributos sem informação trazem ruído e podem baixar a acurácia

200 colunas de ruído derrubam o kNN de 98,1% para 77,4%. O que ajuda é um atributo com informação num bom formato, como x² para a variedade 2.

Na lição 4.3 colocamos medidas em escala e na lição 4.5 procuramos vazamentos. Hoje juntamos tudo numa só máquina. Um pipeline é uma lista de passos que transforma uma tabela bruta numa previsão: escala, codificação, modelo. No scikit-learn ele é um só objeto: você o ajusta como um modelo e pergunta a ele como a um modelo. A pergunta-chave é: em que ordem?

Preveja, depois confira Você tem uma tabela de pinguins e quer avaliar um kNN num teste separado. Antes é preciso padronizar as medidas: subtrair de cada coluna a média e dividi-la pelo desvio padrão. Um scaler é a ferramenta que calcula essa média e esse desvio. Primeiro colocar em escala e depois dividir os dados em treino e teste, ou o contrário?
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: Primeiro dividir, depois ajustar o scaler só no treino e converter o teste com ele

Dividir, e ajustar o scaler só no treino; na validação cruzada, em cada fold separadamente. Nos pinguins, um scaler ajustado em todos os dados dá por acaso o mesmo: 99,0%, as mesmas previsões para 102 de 102 pinguins do teste. Mas a média do teste entra no scaler mesmo assim. E colocar o teste em escala com a própria média estraga o modelo: quando os pinguins chegam um de cada vez, a acurácia cai para 44,1%.

Experimente

  • Comece com o scaler do treino. Troque para o scaler de todos os dados. Quantas previsões mudam, e por quê?
  • Escolha o scaler ajustado no teste e mude a forma como os pinguins chegam. Quando o modelo para de funcionar?
  • Monte o pipeline com os blocos. Qual das três armadilhas tenta mais?

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

A ideia em palavras simples

Um scaler não é preparação de dados “antes do modelo”. Ele é a primeira parte do modelo. Para cada coluna, guarda dois números: uma média e um desvio padrão. Primeiro aprende esses números, depois converte com eles. Como toda parte de um modelo, aprende só no treino. O teste é convertido com os mesmos números, assim como depois cada pinguim novo.

Um scaler ajustado no próprio teste quebra essa regra: o teste ganha a própria média e a própria dispersão. Quando o teste inteiro chega de uma vez e se parece com o treino, o resultado parece bom: 98,0%. Quando chegam lotes de uma só ilha, já são só 82,4%. Nos dados, Torgersen só tem pinguins Adelie. Tirada a própria média, o modelo reconhece 6 de 13. Quando os pinguins chegam um de cada vez, a acurácia cai para 44,1%. Um pinguim sozinho não tem dispersão, então depois da conversão ele vira zero em todas as colunas. O modelo chama todos de “Adelie”, exatamente o que faz “sempre Adelie”.

Um pipeline junta os passos num só objeto, aqui o scaler e o kNN. Ajustado no treino, ele aprende de uma vez as médias, os desvios e o próprio modelo, e na previsão só converte e pergunta. A validação cruzada com um pipeline ajusta o scaler de novo em cada fold, então nenhum fold de teste toca o aprendizado. Como isso fica no scikit-learn, a camada “No código” no fim da lição mostra.

Jak to policzono
  • Pinguins: Palmer Penguins (Gorman, Williams e Fraser, 2014; licença CC0), 342 pinguins com quatro medidas. Divisão estratificada: 240 para aprender, 102 para o teste. Os pontos do lab são 20 divisões com outras sementes.
  • Modelo: kNN com k = 5 nas medidas padronizadas (média 0, desvio 1). O scaler é ajustado no treino, nos 342 pinguins ou no próprio teste: todo de uma vez, separadamente em cada ilha ou separadamente em cada pinguim.

Por que funciona

No uso real, um modelo recebe pinguins que não estavam nos dados, muitas vezes um de cada vez. Aí não existe “todos os dados” nem “o teste inteiro”. Existem pinguins novos e os números guardados durante o aprendizado. Uma avaliação é honesta quando o teste segue exatamente o mesmo caminho. Um pipeline obriga a isso: ele aprende só no treino, e todo o resto só é convertido e avaliado.

Um scaler ajustado em todos os dados também quebra essa regra, só que em silêncio. Nos pinguins isso quase não muda nada: o comprimento médio da nadadeira de 342 e de 240 pinguins difere em 0,2 mm, e em cada uma das 20 divisões do lab as previsões são as mesmas. O resultado aparente e o honesto diferem aqui em 0. No código abaixo, o scikit-learn dá em 50 divisões as mesmas previsões em 50 de 50. Mas um scaler não vê os rótulos. Um passo que vê pode vazar muito no mesmo lugar: na lição 4.5, escolher atributos em todos os dados fez um “modelo” a partir de ruído puro. Um pipeline protege contra as duas coisas, então você não precisa verificar toda vez se desta vez o vazamento é pequeno.

Um erro comum

Verdadeiro ou falso: “Também dá para ajustar o scaler no próprio teste; afinal, o teste e o treino ficam com a mesma escala”. Falso. O teste ganha a própria média e a própria dispersão, enquanto o modelo aprendeu com os números do treino. Nos pinguins isso só parece inofensivo quando o teste inteiro chega de uma vez: 98,0%. Em lotes de uma ilha são 82,4%, um de cada vez 44,1%. Nas 20 divisões do lab, em média 76,9% e 42,9%, enquanto o pipeline dá 99,2%. No código abaixo: de uma vez 0,971, por ilha 0,786, um de cada vez 0,437, o mesmo que “sempre Adelie”.

A regra é simples: o scaler aprende no treino, e o teste e os dados novos só são convertidos com os mesmos números. Um pipeline faz isso por você.

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 você ajusta o scaler só na parte de treino?
Ver a resposta

Resposta certa: Porque o scaler é parte do modelo e o teste deve imitar dados que o modelo não viu

O teste imita pinguins novos. A média e a dispersão também são algo que o modelo aprende, então são calculadas só no treino.

2. Os pinguins chegam ao modelo um de cada vez, e alguém ajusta o scaler em cada um separadamente. O que acontece?
Ver a resposta

Resposta certa: Cada pinguim vira zero em todas as colunas, e o modelo diz o mesmo sobre cada um

Um pinguim sozinho não tem dispersão, então depois de tirar a própria média só sobram zeros. O modelo chama todos de “Adelie”: 44,1%, o mesmo que “sempre Adelie”.

3. O que faz a validação cruzada no treino quando o scaler está dentro do pipeline?
Ver a resposta

Resposta certa: Ajusta o scaler de novo em cada fold, só na parte desse fold usada para aprender

Um pipeline é um modelo só, então em cada fold a máquina inteira aprende do zero. O fold que faz o papel de teste não toca nem o scaler nem o kNN.

Pontos-chave

  • Um scaler, um codificador e qualquer outro passo que aprende algo são parte do modelo: aprendem só no treino.
  • Um pipeline junta os passos num só objeto, então a validação cruzada os ajusta de novo em cada fold.
  • Um scaler ajustado no próprio teste estraga o modelo: 44,1% em vez de 99,0% quando os pinguins chegam um de cada vez.
  • Um scaler ajustado em todos os dados vaza pouco (aqui 0 previsões mudaram), mas um passo que vê os rótulos pode vazar muito no mesmo lugar.

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

A matemática

Scaler: z = (x − μ) / σ, em que μ e σ são a média e o desvio padrão da coluna calculados no treino. Pipeline P = (S, M): P.fit(X, y) calcula μ e σ em X e depois treina o modelo M em S(X). A previsão é P.predict(x) = M(S(x)) para cada x novo, sempre com os mesmos μ e σ.

fit_transform numa só linha: μ = x e σ = 0 (o scikit-learn troca um desvio nulo por 1), então z = (x − x) / 1 = 0 em todas as colunas, seja qual for o pinguim.

No código (Python)

No código, o scaler aprende com fit, converte com transform, e fit_transform faz as duas coisas de uma vez. O mesmo experimento no scikit-learn: um pipeline no treino, fit_transform no teste de uma vez, por ilha e pinguim por pinguim, e no fim um scaler ajustado em todos os dados antes da divisão, em 50 divisões.

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier

df = pd.read_csv("data/raw/penguins.csv")
cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
d = df.dropna(subset=cols)
X, y = d[cols], d["species"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=1)
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier(5))  # o scaler é parte do modelo
print(f"train {len(y_tr)}, test {len(y_te)}; CV on the training part: {cross_val_score(pipe, X_tr, y_tr, cv=5).mean():.3f}")
pipe.fit(X_tr, y_tr)
print(f"test, scaler fitted on the training part: {pipe.score(X_te, y_te):.3f}")
knn = pipe[-1]  # o kNN ajustado, sem o seu scaler
batch = knn.score(StandardScaler().fit_transform(X_te), y_te)  # fit_transform no teste
right = [knn.predict(StandardScaler().fit_transform(part)) == y_te[part.index] for _, part in X_te.groupby(d["island"])]
one = [knn.predict(StandardScaler().fit_transform(X_te.loc[[i]]))[0] == y_te[i] for i in X_te.index]
print(f"fit_transform on the test: all at once {batch:.3f}, by island {np.concatenate(right).mean():.3f}, one at a time {np.mean(one):.3f}")
print(f"always Adelie: {(y_te == 'Adelie').mean():.3f}")
same = equal = 0
for s in range(50):
    a, b, ya, yb = train_test_split(X, y, test_size=0.3, stratify=y, random_state=s)
    honest = make_pipeline(StandardScaler(), KNeighborsClassifier(5)).fit(a, ya).predict(b)
    scaler = StandardScaler().fit(X)  # scaler.fit(X): todos os pinguins, antes da divisão
    leaky = KNeighborsClassifier(5).fit(scaler.transform(a), ya).predict(scaler.transform(b))
    same += (honest == leaky).all()
    equal += (leaky == yb).sum() == (honest == yb).sum()
print(f"scaler.fit(X) before the split, 50 splits: the same predictions in {same} of 50, the same test accuracy in {equal} of 50")

Saída

train 239, test 103; CV on the training part: 0.983
test, scaler fitted on the training part: 0.961
fit_transform on the test: all at once 0.971, by island 0.786, one at a time 0.437
always Adelie: 0.437
scaler.fit(X) before the split, 50 splits: the same predictions in 50 of 50, the same test accuracy in 50 of 50

A divisão é diferente da do lab (103 pinguins no teste em vez de 102), então os números também diferem um pouco. A imagem é a mesma: fit_transform no teste inteiro de uma vez dá por sorte até 0,971, por ilha 0,786 e um de cada vez 0,437, o mesmo que “sempre Adelie”. Um scaler ajustado em todos os dados não muda nenhuma previsão nessas 50 divisões.

Para quem pratica
  • No scikit-learn: make_pipeline ou Pipeline([...]). Quando as colunas são de tipos diferentes, o primeiro passo é um ColumnTransformer: colocar números em escala, codificar categorias em one-hot, preencher lacunas.
  • Ajuste de hiperparâmetros: GridSearchCV(pipe, param_grid) ajusta o pipeline inteiro dentro dos folds. Os parâmetros de um passo levam o nome do passo como prefixo, p. ex. kneighborsclassifier__n_neighbors.
  • Salve o pipeline inteiro (joblib.dump(pipe, "model.joblib")), não só o modelo. Assim, em produção, ninguém consegue esquecer a escala nem converter dados novos com outra média.
  • Preencher lacunas (lição 4.2), codificar categorias (lição 4.3), atributos novos (lição 4.4) e seleção de atributos (lição 4.5): coloque cada um desses passos dentro do pipeline.

Vá mais fundo no Atlas

Verbetes da enciclopédia sobre as ideias desta aula: