ML Atlas

Kurs uczenia maszynowego od zera · Lekcja 4.3 · 22 z 33 gotowych lekcji · 12 min

Kategorie i skalowanie

W skrócie

kNN na winach bez skalowania ma trafność 0,695, po standaryzacji 0,963. Wyspy zapisane jako 0, 1, 2 udają porządek: one-hot podnosi R² z 0,469 do 0,546.

Rozgrzewka Dwa pytania z wcześniejszych lekcji. Minuta, a lekcji nie blokuje.
Z lekcji 4.2: Braki danych BMI brakuje u 110 najcięższych pacjentów. Co robi wpisanie średniej z pozostałych?

Pokaż odpowiedź

Poprawna odpowiedź: Osłabia związek BMI z chorobą, bo chorzy najbardziej lądują przy przeciętnym BMI

Korelacja spada z 0,586 do 0,352. 110 pacjentów z dużym postępem choroby siedzi w jednym punkcie: przy średniej 24,3.

Z lekcji 4.1: Eksploracja: patrz, zanim modelujesz Prawda czy fałsz: „statystyki opisowe wystarczą, rysowanie danych to strata czasu”.

Pokaż odpowiedź

Poprawna odpowiedź: Fałsz, bo te same liczby mogą kryć łuk, grupy albo jeden daleki punkt

W kwartecie te same liczby kryją cztery różne obrazy, a u 342 pingwinów jedna korelacja ma inny znak niż w każdym gatunku. Rysunek pokazuje to od razu.

W lekcji 1.4 kNN na pingwinach trafiał 76,7% bez skalowania i 96,1% po standaryzacji, bo masa w gramach zagłuszała pozostałe pomiary. Dziś dwie rzeczy, które psują model, zanim zacznie się uczyć. Pierwsza to cechy w bardzo różnych skalach; skalowanie sprowadza je do porównywalnego zakresu. Druga to kategorie, czyli nazwy zamiast liczb, na przykład wyspa, z której pochodzi pingwin. Kodowanie kategorii zamienia je na liczby i da się to zrobić źle.

Przewidź, potem sprawdź kNN (k = 5) rozpoznaje odmianę wina po 13 pomiarach chemicznych. Prolina ma odchylenie standardowe 314, a najmniej zmienny pomiar 0,124. Bez skalowania trafność w walidacji krzyżowej to 0,695. Ile po standaryzacji, czyli po przeliczeniu każdego pomiaru na odchylenia standardowe od średniej?
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ź: Znacznie więcej, bo po standaryzacji w odległości liczy się każdy z 13 pomiarów

Po standaryzacji 0,963 zamiast 0,695. Bez skalowania odległość między winami to prawie sama różnica proliny. Po standaryzacji każdy pomiar ma odchylenie 1 i głosuje na równi z innymi. Min-max daje 0,958, skalowanie odporne 0,946.

Spróbuj

  • Zacznij od win bez skalowania. Który pomiar zajmuje na wykresie prawie całą wysokość?
  • Przełącz na min-max, standaryzację i skalowanie odporne. Ile zyskuje kNN i jak bardzo różnią się te trzy sposoby?
  • Przejdź do pingwinów. Porównaj kody 0, 1, 2 alfabetycznie i w kolejności z pliku. Czy model powinien zależeć od tego, w jakiej kolejności wypiszesz wyspy?
  • Wybierz one-hot. Gdzie teraz leżą linie przewidywań względem kropek?

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

Pomysł w prostych słowach

kNN uznaje za podobne te wina, które są blisko we wszystkich 13 pomiarach naraz. Prolina ma odchylenie standardowe 314, a najmniej zmienny pomiar tylko 0,124, więc w sumie różnic prolina przykrywa całą resztę. Skalowanie wyrównuje te zakresy. Min-max przesuwa każdy pomiar do przedziału od 0 do 1, standaryzacja do średniej 0 i odchylenia 1, skalowanie odporne odejmuje medianę i dzieli przez rozstęp międzykwartylowy, więc pojedyncze skrajne wartości mniej je ruszają. Na winach kNN ma trafność 0,958, 0,963 i 0,946 zamiast 0,695. Wybór sposobu jest mniej ważny niż to, żeby skalować w ogóle.

Kategorie mają inny kłopot. Wyspa to Biscoe, Dream albo Torgersen, a regresja liniowa potrzebuje liczb. Najprostszy pomysł to kody 0, 1, 2. Liczba niesie jednak porządek i odległość: model z jedną wagą uzna, że wyspa z kodem 1 leży dokładnie w połowie między dwiema pozostałymi. W labie regresja przewiduje masę pingwina z wyspy i płci. Z kodami alfabetycznymi R² w walidacji krzyżowej wynosi 0,469. Te same kody w kolejności z pliku dają 0,193, choć dane są identyczne. One-hot daje każdej wyspie osobną kolumnę 0/1 i osobną wagę: R² 0,546, a średni błąd spada z 476 do 411 g.

Jak to policzono
  • Wina: Wine (Aeberhard i Forina, 1992; UCI, licencja CC BY 4.0), 178 win, 13 pomiarów chemicznych, 3 odmiany. kNN z k = 5, walidacja krzyżowa na 5 części, powtórzona 10 razy z innym losowaniem. Skaler dopasowany tylko na części treningowej każdego podziału.
  • Pingwiny: Palmer Penguins (Gorman, Williams i Fraser, 2014; pakiet palmerpenguins: Horst, Hill i Gorman, 2020), licencja CC0. 333 z 344 pingwinów: 11 nie ma wpisanej płci. Regresja liniowa: masa z wyspy i płci (jedna kolumna 0/1), walidacja krzyżowa na 5 części, 10 powtórzeń. Linie na wykresie: model dopasowany na wszystkich 333.
  • R²: część zmienności masy wyjaśniona na częściach walidacyjnych (1 = wszystko, 0 = tyle co średnia). Średni błąd: średnia bezwzględna różnica w gramach.

Dlaczego to działa

Odległość w kNN to pierwiastek z sumy kwadratów różnic. Pomiar o dużym rozrzucie daje duże różnice i to one rządzą sumą. Standaryzacja dzieli każdy pomiar przez jego odchylenie, więc typowa różnica w każdym pomiarze ma podobną wielkość. Średnie, odchylenia, minima i maksima skalera są wyliczone z danych, tak jak wagi modelu. Dlatego skaler dopasowuje się tylko na części treningowej: w kodzie niżej skaler dopasowany na wszystkich winach przed walidacją daje 0,965 zamiast 0,962. Tu różnica jest mała, ale taki wynik widział część walidacyjną, więc nie jest uczciwą oceną.

Z kodami wysp regresja liniowa ma jedną wagę na całą kolumnę. Z kodu 0 na 1 i z 1 na 2 przewidywana masa zmienia się więc o tyle samo. Z kodami alfabetycznymi model przewiduje, że pingwiny z Dream ważą o 625 g mniej niż z Biscoe, a z Torgersen o kolejne 625 g mniej. W danych pingwiny z Dream i Torgersen ważą prawie tyle samo (3719 i 3709 g), a z Biscoe 4719 g. W kolejności z pliku Biscoe dostaje środkowy kod, a prosta przez trzy wyspy prawie się spłaszcza: 243 g na krok. One-hot nie zakłada żadnej kolejności: model przewiduje dla Dream i Torgersen 997 g mniej niż dla Biscoe, tak jak jest w danych.

Częsty błąd

Prawda czy fałsz: „kategorie zakoduj jako 1, 2, 3, model sobie poradzi”. Fałsz. Liczba niesie porządek i równe odstępy, których między wyspami nie ma. W labie samo przestawienie kolejności wysp zmienia R² z 0,469 na 0,193, choć dane są te same. One-hot daje 0,546 przy każdej kolejności. W kodzie niżej one-hot wygrywa z kodami alfabetycznymi w 49 z 50 części walidacyjnych.

Wyjątek to płeć: przy dwóch kategoriach jedna kolumna 0/1 wystarczy, bo nie ma środka, w którym coś mogłoby się znaleźć. Kłopot zaczyna się od trzech kategorii bez naturalnego porządku.

Quiz

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

1. Dlaczego kNN bez skalowania ma na winach trafność tylko 0,695?
Pokaż odpowiedź

Poprawna odpowiedź: Bo prolina ma ogromny rozrzut i to ona rządzi całą odległością

Prolina ma odchylenie standardowe 314, najmniej zmienny pomiar 0,124. Bez skalowania odległość to prawie sama różnica proliny. Po standaryzacji trafność rośnie do 0,963.

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

3. Prawda czy fałsz: „kategorie zakoduj jako 1, 2, 3, model sobie poradzi”.
Pokaż odpowiedź

Poprawna odpowiedź: Fałsz, bo kody wprowadzają porządek i odstępy, których w danych nie ma

Z kodami model liniowy widzi drabinkę równych kroków. One-hot daje każdej kategorii osobną wagę: R² 0,546 zamiast 0,469 albo 0,193.

Najważniejsze wnioski

  • Modele liczące odległość, jak kNN, potrzebują cech w podobnej skali: na winach 0,695 bez skalowania, 0,963 po standaryzacji.
  • Min-max, standaryzacja i skalowanie odporne dają tu podobne wyniki; najważniejsze, żeby skalować w ogóle.
  • Skaler dopasowuj tylko na części treningowej, w każdym podziale osobno.
  • Kategorie bez porządku koduj one-hot: kody 0, 1, 2 wymuszają kolejność i równe kroki, których w danych nie ma.

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

Matematyka

Min-max: x' = (x − min) / (max − min). Standaryzacja: z = (x − x̄) / s. Skalowanie odporne: x' = (x − mediana) / IQR, gdzie IQR = Q3 − Q1, czyli rozstęp międzykwartylowy. Odległość w kNN: d(a, b) = √(Σⱼ (aⱼ − bⱼ)²); pomiar o dużym s daje duże (aⱼ − bⱼ)² i przykrywa resztę.

Kody wysp w regresji liniowej: ŷ = a + w · kod + v · samiec. Przy kodach alfabetycznych ŷ(Dream) − ŷ(Biscoe) = ŷ(Torgersen) − ŷ(Dream) = w, czyli równe kroki. One-hot: ŷ = a + w_D · [Dream] + w_T · [Torgersen] + v · samiec, gdzie [Dream] to 1 dla pingwina z Dream i 0 dla innych. Każda wyspa ma własne przesunięcie, a Biscoe jest punktem odniesienia.

W kodzie (Python)

Ten sam eksperyment w scikit-learn: kNN na winach z czterema skalowaniami w walidacji krzyżowej 10 × 5 (skaler w pipeline'ie, więc dopasowuje się w każdym podziale), skaler dopasowany na wszystkich winach dla porównania i masa pingwinów z wyspą zakodowaną na trzy sposoby.

import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score, RepeatedStratifiedKFold, RepeatedKFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression

X, y = load_wine(return_X_y=True)
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=0)
acc = {}
for name, scaler in [("no scaling", None), ("min-max", MinMaxScaler()), ("standardised", StandardScaler()), ("robust", RobustScaler())]:
    model = make_pipeline(scaler, KNeighborsClassifier(5)) if scaler else KNeighborsClassifier(5)  # skaler w pipeline'ie: dopasowany w każdym podziale
    acc[name] = cross_val_score(model, X, y, cv=cv)
print("kNN on 178 wines, 10 x 5 CV: " + ", ".join(f"{k} {v.mean():.3f}" for k, v in acc.items()))
print(f"standardised beats no scaling in {(acc['standardised'] > acc['no scaling']).sum()} of 50 folds")
leaky = cross_val_score(KNeighborsClassifier(5), StandardScaler().fit_transform(X), y, cv=cv)  # skaler widział też części walidacyjne
print(f"scaler fitted on all wines before the CV: {leaky.mean():.3f}")
df = pd.read_csv("data/raw/penguins.csv").dropna(subset=["sex", "body_mass_g"])
male = (df.sex == "MALE").to_numpy(float)[:, None]
codings = {
    "codes alphabetical": df.island.map({"Biscoe": 0, "Dream": 1, "Torgersen": 2}).to_numpy(float)[:, None],
    "codes in file order": df.island.map({"Torgersen": 0, "Biscoe": 1, "Dream": 2}).to_numpy(float)[:, None],
    "one-hot": pd.get_dummies(df.island).to_numpy(float),
}
cv2 = RepeatedKFold(n_splits=5, n_repeats=10, random_state=0)
r2 = {}
for name, isl in codings.items():
    Xe = np.hstack([isl, male])
    r2[name] = cross_val_score(LinearRegression(), Xe, df.body_mass_g, cv=cv2, scoring="r2")
    mae = -cross_val_score(LinearRegression(), Xe, df.body_mass_g, cv=cv2, scoring="neg_mean_absolute_error").mean()
    print(f"{len(df)} penguins, body mass from island + sex, {name}: R2 {r2[name].mean():.3f}, mean error {mae:.0f} g")
print(f"one-hot beats the alphabetical codes in {(r2['one-hot'] > r2['codes alphabetical']).sum()} of 50 folds")

Wynik

kNN on 178 wines, 10 x 5 CV: no scaling 0.706, min-max 0.963, standardised 0.962, robust 0.952
standardised beats no scaling in 50 of 50 folds
scaler fitted on all wines before the CV: 0.965
333 penguins, body mass from island + sex, codes alphabetical: R2 0.464, mean error 477 g
333 penguins, body mass from island + sex, codes in file order: R2 0.179, mean error 618 g
333 penguins, body mass from island + sex, one-hot: R2 0.542, mean error 412 g
one-hot beats the alphabetical codes in 49 of 50 folds

Podziały są inne niż w labie, więc liczby też są trochę inne. Obraz jest ten sam: standaryzacja wygrywa z brakiem skalowania we wszystkich 50 częściach walidacyjnych, a one-hot z kodami alfabetycznymi w 49 z 50. Kolejność kodów znów decyduje o wyniku: 0,464 alfabetycznie, 0,179 w kolejności z pliku.

Dla praktyka
  • W scikit-learn: StandardScaler, MinMaxScaler i RobustScaler dla liczb, OneHotEncoder(handle_unknown="ignore") dla kategorii. ColumnTransformer wysyła kolumny liczbowe i kategoryczne do różnych przekształceń, a całość wkładasz do make_pipeline, żeby walidacja krzyżowa dopasowywała je w każdym podziale.
  • Drzewa decyzyjne i lasy losowe nie potrzebują skalowania: dzielą jeden pomiar progiem, a próg przesuwa się razem ze skalą.
  • Porządek, który naprawdę istnieje (mały, średni, duży; poziom wykształcenia), możesz zapisać kodami, ale w jawnie podanej kolejności (OrdinalEncoder(categories=...)).
  • Kategorie z setkami wartości (kod pocztowy, numer produktu) dają przy one-hot setki kolumn. Wtedy łączy się rzadkie wartości w grupę „inne” albo używa kodowania średnią celu liczonego poza foldem (hasło w Atlasie), bo liczone na całych danych przecieka.

Więcej w Atlasie

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