ML Atlas

Kurs uczenia maszynowego od zera · Lekcja 4.5 · 24 z 33 gotowych lekcji · 14 min

Wyciek danych

W skrócie

Wyciek to informacja z testu albo z przyszłości, która trafia do nauki modelu. Na czystym szumie daje 82% w walidacji. Poznaj 8 typów i znajdź je sam.

Rozgrzewka Dwa pytania z wcześniejszych lekcji. Minuta, a lekcji nie blokuje.
Z lekcji 4.4: Inżynieria cech Dlaczego jeden próg na samych flawonoidach nie oddzieli odmiany 2?

Pokaż odpowiedź

Poprawna odpowiedź: Bo odmiana 2 leży pośrodku osi, a jeden próg nie wytnie środkowego przedziału

Średnio 2,08 przy 2,98 i 0,78 dla pozostałych. Każdy próg zostawia po stronie „odmiana 2” też inną odmianę. Najlepszy próg trafia na treningu 61,6% wobec 60,0% za „zawsze inna”, a liniowy SVM nie wskazuje odmiany 2 nigdzie: 60,4%.

Z lekcji 4.3: Kategorie i skalowanie Wyspy zapisano jako Biscoe = 0, Dream = 1, Torgersen = 2. Co zakłada regresja liniowa z jedną wagą na tę kolumnę?

Pokaż odpowiedź

Poprawna odpowiedź: Że Dream leży w połowie drogi między Biscoe a Torgersen, w równych krokach

Jedna waga to równe kroki: model przewiduje 625 g mniej z Biscoe do Dream i kolejne 625 g z Dream do Torgersen. W danych Dream i Torgersen ważą prawie tyle samo.

W lekcji 3.2 walidacja krzyżowa dawała uczciwą ocenę modelu. Uczciwą pod jednym warunkiem: nic z części testowej nie może wcześniej dotknąć nauki. Gdy dotknie, mówimy o wycieku danych. Model wygląda świetnie na walidacji, a w prawdziwym użyciu się sypie. Kapoor i Narayanan zebrali doniesienia innych badaczy o wycieku w 294 pracach naukowych z 17 dziedzin. Zacznijmy od najczystszego przykładu: danych, w których nie ma niczego do nauczenia.

Przewidź, potem sprawdź Masz 60 próbek i 1000 kolumn czystego szumu, a etykiety 0/1 są losowe. Wybierasz 10 kolumn najmocniej skorelowanych z etykietą na wszystkich próbkach, a potem oceniasz model 5-krotną walidacją krzyżową. Jaki wynik pokaże walidacja?
Jak bardzo jesteś pewny(-a)?

Eksperyment

Ten interaktywny eksperyment działa w przeglądarce z włączonym JavaScriptem. Tekst wokół opisuje, co w nim widać.

Pokaż odpowiedź

Poprawna odpowiedź: Ponad 80%, choć w tych danych nie ma żadnego sygnału

Walidacja pokazuje 81,7%. Gdy kolumny wybierasz w każdym foldzie tylko na danych do nauki, wynik spada do 43,3%, czyli do rzutu monetą. W 20 nowych zbiorach szumu średnio 86,6% z wyciekiem i 49,8% uczciwie.

Spróbuj

  • Przy wyborze cech przed podziałem przesuwaj k od 5 do 50. Jak wysoko dochodzi wynik na czystym szumie?
  • Przy tym samym k przełącz na wybór wewnątrz walidacji. Gdzie ląduje słupek, a gdzie kropki z 20 nowych zbiorów szumu?
  • W drugiej części labu znajdź krok, który przecieka, w każdym z 8 opisów.

Gdy już sprawdzisz to w labie, nazwijmy, co widzisz.

Pomysł w prostych słowach

Wyciek danych to każda informacja, której model nie miałby w chwili prawdziwej predykcji, a która trafiła do jego nauki albo oceny. Walidacja ma udawać przyszłość: dane, których model nigdy nie widział. Wyciek psuje to udawanie, więc wynik rośnie, a model wcale nie jest lepszy.

W labie wśród 1000 kolumn szumu zawsze znajdą się takie, które przypadkiem pasują do etykiet. Najlepsza ma korelację 0,48 z etykietą, choć dla szumu typowe jest około 0,13. Wybór na wszystkich 60 próbkach dopasowuje kolumny także do próbek, które potem udają test. Walidacja krzyżowa sprawdza więc model na danych, które już wpłynęły na jego budowę, i pokazuje 81,7%. Wybór wewnątrz foldu widzi tylko 48 próbek do nauki. Kolumny pasujące do nich przypadkiem nic nie mówią o 12 próbkach testowych, więc wynik spada do 43,3%, w granicach rzutu monetą.

Kapoor i Narayanan podzielili wyciek na 8 typów w trzech grupach:

  • brak czystego podziału: brak osobnego testu, przetwarzanie dopasowane do całych danych, selekcja cech na całych danych, duplikaty po obu stronach podziału;
  • cecha, której nie znasz w chwili predykcji, np. liczba dni w szpitalu, gdy model ma przewidzieć przyjęcie;
  • test z innego rozkładu niż zastosowanie: losowy podział danych czasowych, te same osoby w treningu i teście, test wybrany inaczej niż dane, na których model będzie pracował.

Każdy z nich znajdziesz w drugiej części labu.

Jak to policzono
  • Szum: 60 próbek, 1000 kolumn z rozkładu normalnego, po 30 etykiet 0 i 1 w losowej kolejności. Generuje go przeglądarka z ziarnem 1; kropki to 20 kolejnych zbiorów z innymi ziarnami.
  • Model: k kolumn o największej korelacji z etykietą (w wartości bezwzględnej), klasyfikator najbliższego centroidu, 5-krotna walidacja krzyżowa ze stratyfikacją (w każdym foldzie 6 próbek z każdej klasy).
  • Biopsje w „Częstym błędzie”: Breast Cancer Wisconsin (Diagnostic), UCI, licencja CC BY 4.0, 569 biopsji. Typy wycieku: S. Kapoor, A. Narayanan, „Leakage and the reproducibility crisis in machine-learning-based science”, Patterns, 2023.

Dlaczego to działa

Ocena jest uczciwa tylko wtedy, gdy dane testowe nie wpłynęły na żadną decyzję o modelu. Selekcja cech to też decyzja, tak samo jak uzupełnianie braków, skalowanie, dobór progu czy wybór najlepszego z kilku modeli. Jeśli którakolwiek z nich widziała etykiety testu, test przestaje być niezależny. Im większy wybór (10 kolumn z 1000), tym więcej przypadkowego dopasowania przechodzi do wyniku.

Jeden zbiór szumu mógłby wypaść tak przypadkiem. Ale w żadnym z 20 nowych zbiorów w labie wynik z wyciekiem przy k = 10 nie spada poniżej 81,7%. W kodzie niżej ten sam eksperyment idzie na 50 zbiorach szumu w scikit-learn: wybór przed podziałem daje średnio 86,4% (najmniej 73,3%), a wybór w każdym foldzie 50,0%.

Częsty błąd

Prawda czy fałsz: „wyciek to tylko etykieta ukryta w cechach”. Fałsz. W labie żadna kolumna nie zawiera etykiety, a walidacja i tak pokazuje 81,7% na czystym szumie. Przecieka decyzja podjęta na całych danych. Na biopsjach powielanie złośliwych przed walidacją krzyżową daje czułość 97,1%, a powielanie tylko w danych do nauki 92,1%, w 50 podziałach na 50. Kopie trafiają po obu stronach podziału, więc model rozpoznaje biopsje, które już widział. Tak samo działają duplikaty w danych, a losowy podział danych czasowych wkłada do treningu przyszłość.

Nie każdy wyciek jest tak samo groźny. Skaler dopasowany na wszystkich biopsjach zmienia trafność modelu najbliższego sąsiada średnio o 0,08 pkt proc.: nie widzi etykiet, więc przenosi bardzo mało. Selekcja cech i powielanie korzystają z etykiet, więc przenoszą dużo. Zasada jest jedna: każdy krok, który uczy się czegoś z danych, dopasuj tylko na części do nauki.

Quiz

Trzy pytania na sprawdzenie się. Wybierz odpowiedź albo rozwiń odpowiedź pod pytaniem. Z klawiatury: strzałki wybierają, Enter albo spacja zatwierdza.

1. Wybór 10 kolumn na wszystkich 60 próbkach daje w walidacji krzyżowej 81,7% na czystym szumie. Skąd ten wynik?
Pokaż odpowiedź

Poprawna odpowiedź: Kolumny wybrano także pod etykiety próbek, które potem udają test

Przy wyborze w każdym foldzie tylko na danych do nauki ten sam model ma 43,3%. W 20 nowych zbiorach szumu średnio 86,6% z wyciekiem i 49,8% uczciwie.

2. Uzupełniasz braki średnią z całego pliku, dzielisz dane na trening i test, uczysz model na treningu i liczysz wynik na teście. Który krok przecieka?
Pokaż odpowiedź

Poprawna odpowiedź: Uzupełnianie braków średnią policzoną na całym pliku

Średnia zawiera dane testowe, więc test wpływa na naukę. Uzupełnianie braków dopasuj tylko na treningu, najlepiej w pipeline.

3. Skalowanie przed podziałem zmienia wynik na biopsjach średnio o 0,08 pkt proc., a powielanie złośliwych przed podziałem podnosi czułość z 92,1% do 97,1%. Skąd ta różnica?
Pokaż odpowiedź

Poprawna odpowiedź: Skaler nie patrzy na etykiety, a kopie złośliwych trafiają też do testu

Wyciek przenosi tyle, ile decyzja wie o teście. Skaler zna tylko średnie i odchylenia, a kopia złośliwej biopsji w teście to odpowiedź, którą model już widział.

Najważniejsze wnioski

  • Wyciek to informacja, której model nie miałby w chwili predykcji, a która dotknęła jego nauki albo oceny.
  • Każdy krok, który uczy się z danych (selekcja, uzupełnianie, skalowanie, powielanie), dopasuj tylko na części do nauki, najlepiej w pipeline.
  • Dziel dane tak, jak model będzie użyty: po czasie, po osobach, bez duplikatów.
  • Wynik, który wygląda za dobrze, najpierw sprawdź pod kątem wycieku.

Głębiej: wzór, kod, praktyka

Matematyka

Zgadywanie na n próbkach ma trafność o średniej 0,5 i odchyleniu √(0,5 · 0,5 / n). Przy n = 60 to 0,065, więc rzut monetą to 50% ± 13 pkt proc. (dwa odchylenia).

Dla czystego szumu korelacja kolumny z etykietą ma średnią 0 i odchylenie około 1 / √n, przy n = 60 około 0,13. Wśród 1000 kolumn najlepsza leży kilka odchyleń od zera: w labie 0,48.

Uczciwa walidacja krzyżowa liczy  = średnia po k z A(f_k, T_k), gdzie model f_k (razem z selekcją cech) powstaje tylko z danych spoza foldu T_k. Przy wyborze cech na całych danych selekcja zależy od etykiet z T_k, więc f_k nie jest od T_k niezależny, a  przestaje szacować trafność na nowych danych.

W kodzie (Python)

Ten sam eksperyment w scikit-learn: wybór 10 kolumn na 50 zbiorach szumu, a na biopsjach skalowanie i powielanie złośliwych przed podziałem i w każdym foldzie, na 50 podziałach.

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 próbek x 1000 kolumn czystego szumu, etykiety 30/30 w losowej kolejności
    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 kolumn wybranych na wszystkich 60
    honest = cross_val_score(make_pipeline(SelectKBest(f_classif, k=10), NearestCentroid()), X, y, cv=cv).mean()  # wybór w każdym foldzie
    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 = złośliwy
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()  # skaler dopasowany na wszystkich biopsjach
    inside = cross_val_score(make_pipeline(StandardScaler(), knn()), X, y, cv=cv).mean()  # dopasowany na każdej części do nauki
    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):  # kopiuje losowe złośliwe, aż obu klas jest tyle samo
    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)  # kopie zrobione PRZED podziałem trafiają też do foldów testowych
    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):  # kopie tylko w każdej części do nauki
        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")

Wynik

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

Lab pokazuje jeden zbiór szumu i 20 kolejnych, kod 50 innych zbiorów, więc liczby są trochę inne: w labie wyciek daje 81,7% na pierwszym zbiorze i średnio 86,6% na 20 kolejnych, w kodzie średnio 86,4%. Wniosek jest ten sam w każdym zbiorze.

Dla praktyka
  • W scikit-learn każdy krok, który uczy się z danych, wkładaj do Pipeline razem z modelem i dopiero całość oceniaj przez cross_val_score: wtedy fit każdego kroku widzi tylko część do nauki.
  • Dane czasowe dziel przez TimeSeriesSplit, a dane z grupami (pacjenci, klienci, zdjęcia z jednego aparatu) przez GroupKFold.
  • Powielanie i SMOTE stosuj w Pipeline z imbalanced-learn, tylko na częściach do nauki (lekcja 2.5).
  • Na Kaggle wyciek bywa w samym pliku: identyfikatory, kolejność wierszy, znaczniki czasu. Wynik dużo lepszy niż u innych to sygnał, żeby szukać wycieku, zanim się ucieszysz.

Więcej w Atlasie

Hasła encyklopedii o pojęciach z tej lekcji: