ML Atlas

Machine learning do zero · Aula 4.5 · 24 de 33 aulas prontas · 14 min

Vazamento de dados

Em resumo

Vazamento é informação do teste ou do futuro que chega ao treino do modelo. Com ruído puro, ele finge 82% na validação. Conheça 8 tipos e encontre-os.

Aquecimento Duas perguntas de aulas anteriores. Um minuto, e a aula nunca fica bloqueada.
Da aula 4.4: Engenharia de atributos Por que um único limiar só nos flavonoides não consegue separar a variedade 2?

Ver a resposta

Resposta certa: Porque a variedade 2 fica no meio e um limiar não recorta um intervalo central

Em média 2,08 contra 2,98 e 0,78 das outras. Cada limiar deixa também outra variedade do lado “variedade 2”. O melhor limiar acerta 61,6% no treino contra 60,0% de “sempre outra”, e a SVM linear não prevê a variedade 2 em lugar nenhum: 60,4%.

Da aula 4.3: Categorias e escalonamento 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.

Na aula 3.2 a validação cruzada dava uma estimativa honesta do modelo. Honesta com uma condição: nada da parte de teste pode tocar o treino antes. Quando toca, falamos de vazamento de dados. O modelo parece ótimo na validação e desmorona no uso real. Kapoor e Narayanan reuniram relatos de outros pesquisadores sobre vazamento em 294 artigos científicos de 17 áreas. Vamos começar pelo exemplo mais limpo: dados em que não há nada para aprender.

Preveja, depois confira Você tem 60 amostras e 1000 colunas de ruído puro, e os rótulos 0/1 são aleatórios. Você escolhe as 10 colunas mais correlacionadas com o rótulo usando todas as amostras e depois avalia o modelo com validação cruzada de 5 dobras. Que nota a validação vai mostrar?
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: Mais de 80%, embora esses dados não tenham nenhum sinal

A validação mostra 81,7%. Se você escolhe as colunas em cada dobra só com os dados de treino, a nota cai para 43,3%, cara ou coroa. Em 20 novos conjuntos de ruído a média é 86,6% com vazamento e 49,8% de forma honesta.

Experimente

  • Com os atributos escolhidos antes da divisão, mova k de 5 a 50. Até onde sobe a nota com ruído puro?
  • Com o mesmo k, troque para a escolha dentro da validação. Onde cai a barra, e onde caem os pontos dos 20 novos conjuntos de ruído?
  • Na segunda parte do lab, encontre o passo que vaza em cada um dos 8 processos.

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

A ideia em poucas palavras

Vazamento de dados é qualquer informação que o modelo não teria no momento de uma previsão real e que mesmo assim chegou ao treino ou à avaliação dele. A validação deve fazer o papel do futuro: dados que o modelo nunca viu. O vazamento estraga esse papel, então a nota sobe e o modelo não fica nada melhor.

No lab, entre 1000 colunas de ruído sempre há algumas que combinam com os rótulos por acaso. A melhor tem correlação de 0,48 com o rótulo, quando o típico para ruído é cerca de 0,13. Escolher com as 60 amostras ajusta as colunas também às amostras que depois fazem o papel de teste. A validação cruzada então confere o modelo em dados que já o moldaram, e mostra 81,7%. Escolher dentro de uma dobra vê só 48 amostras de treino. As colunas que combinam com elas por acaso não dizem nada sobre as 12 amostras de teste, então a nota cai para 43,3%, dentro do cara ou coroa.

Kapoor e Narayanan dividiram o vazamento em 8 tipos, em três grupos:

  • sem divisão limpa: sem conjunto de teste separado, pré-processamento ajustado em todos os dados, seleção de atributos com todos os dados, duplicatas dos dois lados da divisão;
  • um atributo que você não conhece na hora de prever, por exemplo os dias no hospital quando o modelo deve prever a internação;
  • um teste de outra distribuição que a do uso: uma divisão ao acaso de dados temporais, as mesmas pessoas no treino e no teste, um teste escolhido de outro jeito que os dados em que o modelo vai trabalhar.

Você vai encontrar cada um deles na segunda parte do lab.

Jak to policzono
  • Ruído: 60 amostras, 1000 colunas de uma distribuição normal, 30 rótulos 0 e 30 rótulos 1 em ordem aleatória. O navegador o gera com a semente 1; os pontos são mais 20 conjuntos com outras sementes.
  • Modelo: as k colunas com maior correlação com o rótulo (em valor absoluto), um classificador do centroide mais próximo, validação cruzada estratificada de 5 dobras (6 amostras de cada classe em cada dobra).
  • Biópsias em “Erro comum”: Breast Cancer Wisconsin (Diagnostic), UCI, licença CC BY 4.0, 569 biópsias. Tipos de vazamento: S. Kapoor, A. Narayanan, “Leakage and the reproducibility crisis in machine-learning-based science”, Patterns, 2023.

Por que funciona

Uma avaliação só é honesta quando os dados de teste não influenciaram nenhuma decisão sobre o modelo. Selecionar atributos também é uma decisão, assim como preencher valores faltantes, escalonar, escolher um limiar ou ficar com o melhor de vários modelos. Se alguma delas viu os rótulos do teste, o teste deixa de ser independente. Quanto maior a escolha (10 colunas de 1000), mais ajuste ao acaso passa para a nota.

Um único conjunto de ruído poderia sair assim por acaso. Mas em nenhum dos 20 novos conjuntos do lab a nota com vazamento em k = 10 fica abaixo de 81,7%. No código abaixo o mesmo experimento roda em 50 conjuntos de ruído no scikit-learn: escolher antes da divisão dá em média 86,4% (no mínimo 73,3%), e escolher em cada dobra 50,0%.

Erro comum

Verdadeiro ou falso: “vazamento é só o rótulo escondido nos atributos”. Falso. No lab nenhuma coluna contém o rótulo, e mesmo assim a validação mostra 81,7% com ruído puro. O que vaza é uma decisão tomada com todos os dados. Nas biópsias, sobreamostrar as malignas antes da validação cruzada dá sensibilidade de 97,1%, e sobreamostrar só nos dados de treino dá 92,1%, em 50 de 50 divisões. As cópias caem dos dois lados da divisão, então o modelo reconhece biópsias que já viu. Duplicatas nos dados funcionam do mesmo jeito, e uma divisão ao acaso de dados temporais põe o futuro no treino.

Nem todo vazamento é igualmente perigoso. Um escalonador ajustado em todas as biópsias muda a acurácia de um modelo de vizinho mais próximo em 0,08 ponto em média: ele não vê os rótulos, então transmite muito pouco. A seleção de atributos e a sobreamostragem usam os rótulos, então transmitem muito. A regra é uma só: ajuste cada passo que aprende algo dos dados só na parte de treino.

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. Escolher 10 colunas com as 60 amostras dá 81,7% na validação cruzada com ruído puro. De onde vem essa nota?
Ver a resposta

Resposta certa: As colunas foram escolhidas também pelos rótulos das futuras amostras de teste

Escolhendo em cada dobra só com os dados de treino, o mesmo modelo tem 43,3%. Em 20 novos conjuntos de ruído a média é 86,6% com vazamento e 49,8% de forma honesta.

2. 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.

3. Escalonar antes da divisão muda a nota nas biópsias em 0,08 ponto em média, enquanto sobreamostrar as malignas antes da divisão sobe a sensibilidade de 92,1% para 97,1%. Por que a diferença?
Ver a resposta

Resposta certa: O escalonador ignora os rótulos, e as cópias de malignas chegam ao teste

Um vazamento transmite o quanto a decisão sabe sobre o teste. O escalonador conhece só médias e desvios, e uma cópia de uma biópsia maligna no teste é uma resposta que o modelo já viu.

Pontos-chave

  • Vazamento é informação que o modelo não teria na hora de prever e que mesmo assim tocou o treino ou a avaliação.
  • Ajuste cada passo que aprende dos dados (seleção, imputação, escalonamento, sobreamostragem) só na parte de treino, de preferência dentro de um pipeline.
  • Divida os dados como o modelo será usado: por tempo, por pessoa, sem duplicatas.
  • Quando uma nota parece boa demais, procure primeiro um vazamento.

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

A matemática

Chutar com n amostras dá uma acurácia de média 0,5 e desvio padrão √(0,5 · 0,5 / n). Com n = 60 isso dá 0,065, então cara ou coroa é 50% ± 13 pontos (dois desvios).

Com ruído puro, a correlação de uma coluna com o rótulo tem média 0 e desvio de cerca de 1 / √n, cerca de 0,13 com n = 60. Entre 1000 colunas a melhor fica a vários desvios do zero: 0,48 no lab.

A validação cruzada honesta calcula  = média sobre k de A(f_k, T_k), em que o modelo f_k (incluindo a seleção de atributos) é construído só com os dados fora da dobra T_k. Se os atributos são escolhidos com todos os dados, a seleção depende dos rótulos de T_k, então f_k não é independente de T_k, e  deixa de estimar a acurácia em dados novos.

No código (Python)

O mesmo experimento no scikit-learn: escolher 10 colunas em 50 conjuntos de ruído e, nas biópsias, escalonar e sobreamostrar as malignas antes da divisão e dentro de cada dobra, em 50 divisões.

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_val_predict
from sklearn.neighbors import NearestCentroid, KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import recall_score

def noise(seed):  # 60 amostras x 1000 colunas de ruído puro, rótulos 30/30 em ordem aleatória
    rng = np.random.default_rng(seed)
    X, y = rng.normal(size=(60, 1000)), rng.permutation(np.repeat([0, 1], 30))
    cv = StratifiedKFold(5, shuffle=True, random_state=seed)
    leak = cross_val_score(NearestCentroid(), SelectKBest(f_classif, k=10).fit_transform(X, y), y, cv=cv).mean()  # 10 colunas escolhidas com as 60
    honest = cross_val_score(make_pipeline(SelectKBest(f_classif, k=10), NearestCentroid()), X, y, cv=cv).mean()  # escolhidas dentro de cada dobra
    return leak, honest
r = np.array([noise(s) for s in range(50)])
print(f"pure noise, 50 datasets: picked before the split {r[:, 0].mean():.1%} (lowest {r[:, 0].min():.1%}), inside CV {r[:, 1].mean():.1%}")

X, y = load_breast_cancer(return_X_y=True)
y = 1 - y  # 1 = maligno
knn = lambda: KNeighborsClassifier(1)
scale = []
for s in range(50):
    cv = StratifiedKFold(5, shuffle=True, random_state=s)
    before = cross_val_score(knn(), StandardScaler().fit_transform(X), y, cv=cv).mean()  # escalonador ajustado em todas as biópsias
    inside = cross_val_score(make_pipeline(StandardScaler(), knn()), X, y, cv=cv).mean()  # ajustado em cada parte de treino
    scale.append(abs(before - inside))
print(f"breast cancer, 1-NN, 50 splits: scaling before the split changes accuracy by {np.mean(scale) * 100:.2f} points on average")
def oversample(rows, rng):  # copia malignas ao acaso até as duas classes ficarem iguais
    mal, ben = rows[y[rows] == 1], rows[y[rows] == 0]
    return np.r_[rows, rng.choice(mal, len(ben) - len(mal))]
rec = []
for s in range(50):
    rng, cv = np.random.default_rng(s), StratifiedKFold(5, shuffle=True, random_state=s)
    rows = oversample(np.arange(len(y)), rng)  # cópias feitas ANTES da divisão também caem nas dobras de teste
    before = recall_score(y[rows], cross_val_predict(make_pipeline(StandardScaler(), knn()), X[rows], y[rows], cv=cv))
    pred = np.empty(len(y))
    for tr, te in cv.split(X, y):  # cópias só dentro de cada parte de treino
        t = oversample(tr, rng)
        pred[te] = make_pipeline(StandardScaler(), knn()).fit(X[t], y[t]).predict(X[te])
    rec.append((before, recall_score(y, pred)))
rec = np.array(rec)
print(f"oversampling malignant before the split: recall {rec[:, 0].mean():.1%}, inside each training part: {rec[:, 1].mean():.1%}; higher before in {(rec[:, 0] > rec[:, 1]).sum()} of 50")

Saída

pure noise, 50 datasets: picked before the split 86.4% (lowest 73.3%), inside CV 50.0%
breast cancer, 1-NN, 50 splits: scaling before the split changes accuracy by 0.08 points on average
oversampling malignant before the split: recall 97.1%, inside each training part: 92.1%; higher before in 50 of 50

O lab mostra um conjunto de ruído e mais 20, o código outros 50 conjuntos, então os números diferem um pouco: no lab o vazamento dá 81,7% no primeiro conjunto e 86,6% em média nos 20 seguintes, no código 86,4% em média. A conclusão é a mesma em cada conjunto.

Para quem pratica
  • No scikit-learn ponha cada passo que aprende dos dados num Pipeline junto com o modelo, e só então avalie o todo com cross_val_score: assim o fit de cada passo vê só a parte de treino.
  • Divida dados temporais com TimeSeriesSplit e dados com grupos (pacientes, clientes, fotos de uma mesma câmera) com GroupKFold.
  • Use a sobreamostragem e o SMOTE dentro de um Pipeline do imbalanced-learn, só nas partes de treino (aula 2.5).
  • No Kaggle o vazamento às vezes está no próprio arquivo: identificadores, ordem das linhas, carimbos de tempo. Uma nota muito melhor que a de todos os outros é um sinal para procurar vazamento antes de comemorar.

Vá mais fundo no Atlas

Verbetes da enciclopédia sobre as ideias desta aula: