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.
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?
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: 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.
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.
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:
make_pipelineouPipeline([...]). Quando as colunas são de tipos diferentes, o primeiro passo é umColumnTransformer: 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.
Isto foi útil?
Obrigado! Isso nos ajuda a melhorar.
Vá mais fundo no Atlas
Verbetes da enciclopédia sobre as ideias desta aula: