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.
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.
Wybierz odpowiedź i pewność.
Odpowiedz, żeby odblokować lab.
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.
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.
Pobierz notebook (.ipynb). Otwórz go w Jupyterze albo wgraj do Google Colab; brakujące dane pobierze sam.
Dla praktyka
- W scikit-learn każdy krok, który uczy się z danych, wkładaj do
Pipelinerazem z modelem i dopiero całość oceniaj przezcross_val_score: wtedyfitkażdego kroku widzi tylko część do nauki. - Dane czasowe dziel przez
TimeSeriesSplit, a dane z grupami (pacjenci, klienci, zdjęcia z jednego aparatu) przezGroupKFold. - Powielanie i SMOTE stosuj w
Pipelinez 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.
Czy to było pomocne?
Dzięki! To pomaga nam poprawiać Atlas.
Więcej w Atlasie
Hasła encyklopedii o pojęciach z tej lekcji: