W poprzedniej lekcji wytrenowałeś swój pierwszy model, @name, i oceniłeś go jedną linijką:
model.score(X_test, y_test). Wyobraź sobie, że wypisała 95%. Brzmi jak wynik wart wpisania do dziennika ekspedycji - dopóki nie policzysz, ile klatek z fotopułapki naprawdę pokazuje kłusownika. Jeśli kłusownik pojawia się na pięciu procentach nagrań, to model, który nie wykrywa nikogo i na każdą klatkę odpowiada "spokój", też ma 95% trafień. I jest zupełnie bezużyteczny, bo nie złapał ani jednego kłusownika.To jest prawdziwy problem uczenia nadzorowanego: nie "jak wytrenować model", tylko "skąd mam wiedzieć, czy on cokolwiek umie". Ta lekcja odpowiada na dwa pytania. Pierwsze: jakiego rodzaju odpowiedzi wolno od modelu żądać. Drugie: jak zmierzyć, czy te odpowiedzi są dobre, i z czym w ogóle je porównać.
Supervised Learning, czyli uczenie nadzorowane, poznałeś już z lotu ptaka. Model dostaje pary: zestaw pomiarów oraz prawidłową odpowiedź do tych pomiarów. Słowo "nadzorowane" nie odnosi się do żadnego nadzoru nad algorytmem - oznacza, że ktoś, jakiś przyrodnik z notesem, wcześniej podpisał każdy okaz. Bez tych podpisów nie ma uczenia nadzorowanego, bo model nie ma się do czego przyrównać.
Przyjęły się dwie nazwy, których będziemy używać do końca modułu.
X to cechy (po angielsku features), czyli tabela pomiarów: jeden wiersz na jedno zwierzę, jedna kolumna na jeden mierzony parametr. y to etykieta (label), czyli kolumna prawidłowych odpowiedzi - dokładnie tyle wartości, ile wierszy w X. Duże X i małe y to konwencja z matematyki, którą scikit-learn przejął, i spotkasz ją w każdym przykładzie w sieci.Cała różnica, wokół której kręci się ta lekcja, siedzi w kolumnie
y. Poniżej te same cztery zwierzęta opisane tymi samymi dwoma pomiarami, ale z dwiema różnymi odpowiedziami: raz nazwą gatunku, raz przewidywaną wagą za rok.1# Te same pomiary, dwie rozne odpowiedzi
2X = [[190, 58], [55, 110], [175, 60], [48, 115]]
3
4y_species = ["lion", "cheetah", "lion", "cheetah"] # odpowiedz: nazwa
5y_weight_next_year = [195, 57, 178, 50] # odpowiedz: liczbaZwróć uwagę, że
X się nie zmieniło - to ten sam notes terenowy w obu przypadkach. Nie zmieniła się też liczba wierszy ani sposób ich zapisu: lista list, gdzie każda wewnętrzna lista to jedno zwierzę. Zmienił się wyłącznie typ zawartości y. W pierwszym przypadku odpowiedzią jest jedna z kilku z góry znanych nazw, w drugim - dowolna liczba. Ten jeden szczegół decyduje o wszystkim, co dalej: o nazwie zadania, o modelu, który wybierzesz, i o metrykach, którymi go ocenisz.Gdy
y zawiera nazwy z zamkniętej listy, zadanie nazywa się klasyfikacją, a możliwe odpowiedzi to klasy. "Lew albo gepard" to klasyfikacja. "Spokój albo kłusownik" to klasyfikacja. "Zagrożony, narażony albo niezagrożony" też, tyle że z trzema klasami. Modele nie rozumieją napisów, więc klasy zapisujemy liczbami: 0, 1, 2. To tylko identyfikatory, a nie wielkości - klasa numer 2 nie jest "dwa razy większa" od klasy numer 1.Pierwszym klasyfikatorem, którego użyjemy, jest regresja logistyczna, w kodzie
LogisticRegression. Nazwa jest myląca i warto o tym wiedzieć od razu: mimo słowa "regresja" jest to klasyfikator, bo zwraca klasę, a nie dowolną liczbę. Model korzysta z dwóch metod. fit(X, y) uczy go na parach pomiar-odpowiedź, a predict(X) zwraca przewidywane klasy dla nowych pomiarów. W kolejnej lekcji zobaczysz, że wszystkie modele scikit-learn mówią tymi samymi czasownikami - dlatego warto poznać je teraz, na najprostszym możliwym zbiorze.Zbierzmy więc osiem zwierząt: cztery lwy i cztery gepardy, każde opisane wagą w kilogramach i prędkością w kilometrach na godzinę.
1from sklearn.linear_model import LogisticRegression
2
3# waga w kg, predkosc w km/h
4X_train = [
5 [190, 58], [175, 60], [205, 55], [160, 62],
6 [55, 110], [48, 115], [62, 105], [50, 112],
7]
8y_train = [0, 0, 0, 0, 1, 1, 1, 1] # 0 = lew, 1 = gepard
9
10model = LogisticRegression()
11model.fit(X_train, y_train)
12
13print(model.predict([[52, 108]])) # [1]Model odpowiedział
[1], czyli "gepard" - i słusznie, bo 52 kilogramy przy 108 km/h to sylwetka geparda. Zwróć uwagę na dwie rzeczy, które się nie zmieniły. Po pierwsze, fit nie zwrócił nowego modelu do podstawienia pod zmienną: zmienił obiekt model w miejscu, dlatego wywołujemy go jako osobną instrukcję. Po drugie, X_train i y_train pozostały nietknięte - model zapamiętał wyliczone współczynniki u siebie, a Twoje dane leżą tam, gdzie leżały. Zauważ też, że predict dostaje listę list, nawet dla jednego zwierzęcia, i zwraca tablicę odpowiedzi, a nie pojedynczą wartość. Zawsze przewiduje dla wielu okazów naraz, choćby ten "wiele" wynosił jeden.Sama nazwa klasy to często za mało. Tropiciel wolałby wiedzieć, czy model jest pewny, czy zgaduje. Do tego służy
predict_proba, które zamiast klasy zwraca prawdopodobieństwa - po jednej liczbie na każdą możliwą klasę.1proba = model.predict_proba([[52, 108]])
2
3print(proba.shape) # (1, 2)
4print(round(float(proba.sum()), 2)) # 1.0Wynik ma kształt
(1, 2): jeden wiersz, bo pytaliśmy o jedno zwierzę, i dwie kolumny, bo klasy są dwie. Kolumny idą w kolejności rosnących numerów klas, więc pierwsza to pewność co do lwa, a druga co do geparda. Obie sumują się do jedynki i właśnie to sprawdziliśmy drugim wydrukiem. Dla naszego geparda model jest niemal całkowicie pewny, ale nie deklaruję tu dokładnej liczby, bo zależy ona od wersji biblioteki - uruchom kod i zobacz sam. Praktyczne zastosowanie jest takie: gdy najwyższe prawdopodobieństwo nie przekracza na przykład 0,7, lepiej oznaczyć klatkę do ręcznego obejrzenia, niż udawać, że model wie. Pamiętaj przy tym, że to pewność modelu, a nie prawda o świecie - model potrafi być pewny i mylić się jednocześnie.Gdy
y zawiera liczby, których nie da się wypisać na zamkniętej liście, zadanie nazywa się regresją. Liczebność stada, waga zwierzęcia, roczne opady w rezerwacie, czas do następnej migracji - to wszystko regresja. Kluczowe słowo to ciągłe: odpowiedź może wypaść w dowolnym miejscu skali, a nie tylko w kilku z góry wypisanych punktach. Waga 187,4 kg jest równie sensowną odpowiedzią jak 187,5 kg, a między nimi mieści się jeszcze nieskończenie wiele innych. W klasyfikacji nic pośredniego nie istnieje - nie ma zwierzęcia w połowie lwa, w połowie geparda.Najprostszym modelem regresji jest
LinearRegression, czyli regresja liniowa. Szuka ona prostej zależności "im więcej tego, tym więcej tamtego" i zapisuje ją jako linię. Obsługujesz ją tymi samymi czasownikami co klasyfikator: fit uczy, predict przewiduje. Sprawdźmy ją na pytaniu, które w Safari pada często: ile zwierząt utrzyma rezerwat o danej powierzchni?1from sklearn.linear_model import LinearRegression
2
3X = [[100], [200], [300], [400]] # powierzchnia rezerwatu w km2
4y = [150, 280, 420, 550] # policzone stado
5
6model = LinearRegression()
7model.fit(X, y)
8
9prediction = model.predict([[250]])
10print(f"Przewidywana liczebnosc: {prediction[0]:.0f}") # 350Model odpowiedział 350, choć takiej wartości nie było w danych - policzył ją z linii dopasowanej do czterech obserwacji. To jest cała istota regresji: odpowiedź nie musi pochodzić ze zbioru treningowego. Zwróć uwagę na dwa szczegóły zapisu. Powierzchnia to pojedynczy pomiar, a mimo to każdy rezerwat jest osobną listą -
[[100], [200]], a nie [100, 200] - bo X zawsze jest tabelą z wierszami, nawet przy jednej kolumnie. I znów predict zwrócił tablicę, więc sięgamy po prediction[0]. Zapis :.0f zaokrągla wynik do pełnych osobników, bo pół zwierzęcia nie chodzi po sawannie.Zanim pójdziemy dalej, obalmy trzy pomysły, które krążą wokół pary klasyfikacja-regresja i brzmią sensownie, a są nieprawdziwe.
Pierwszy: "klasyfikacja jest szybsza od regresji". Nie jest - i odwrotnie też nie. Czas obliczeń zależy od wybranego modelu i rozmiaru danych, a nie od tego, czy odpowiedzią jest nazwa, czy liczba. Regresja liniowa na czterech rezerwatach, którą właśnie uruchomiłeś, policzyła się natychmiast, a klasyfikator na milionie zdjęć potrafi trenować godzinami. Podział na klasyfikację i regresję dotyczy rodzaju odpowiedzi, nie wydajności.
Drugi: "regresja wymaga więcej danych". Też nie. Zapotrzebowanie na dane wynika z liczby cech i złożoności zjawiska, a nie z typu zadania. Nasza regresja poradziła sobie z czterema wierszami, bo zależność była prosta i liniowa, a rozpoznanie gatunku na zdjęciach wymaga tysięcy przykładów, choć to klasyfikacja.
Trzeci, najgroźniejszy: "nie ma żadnej różnicy". Różnica jest fundamentalna, bo przenika wszystko dalej. Inne są modele, inne metryki, a nawet pytanie "czy odpowiedź jest poprawna" znaczy co innego. W klasyfikacji odpowiedź jest trafna albo nie, bez odcieni. W regresji nie trafisz idealnie prawie nigdy - liczy się, o ile się pomyliłeś. Zapamiętaj to zdanie, bo za chwilę zobaczysz je w metrykach: klasyfikacja przewiduje kategorie, regresja przewiduje wartości ciągłe.
Wróćmy do fotopułapki z początku lekcji. Do oceny modelu potrzebujesz dwóch ciągów tej samej długości:
y_true, czyli co naprawdę było na klatkach, i y_pred, czyli co powiedział model. Te nazwy są w scikit-learn niepisanym standardem i będziemy się ich trzymać. Wszystkie metryki działają tak samo: dostają te dwa ciągi i zwracają jedną liczbę.Najprostsza z nich to accuracy, czyli dokładność. Mierzy dokładnie jedno: odsetek poprawnych przewidywań, liczbę trafień podzieloną przez liczbę wszystkich prób. Liczy ją funkcja
accuracy_score, którą sprowadzasz z modułu sklearn.metrics - to tam mieszkają wszystkie metryki tej biblioteki.1from sklearn.metrics import accuracy_score
2
3# 12 klatek z fotopulapki: 0 = spokoj, 1 = klusownik
4y_true = [0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0]
5y_pred = [0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]
6
7print(f"Accuracy: {accuracy_score(y_true, y_pred):.2%}") # 75.00%Dziewięć z dwunastu klatek model opisał tak samo jak przyrodnik, więc accuracy wynosi 75%. Zapis
:.2% mnoży wynik przez sto i dokleja znak procentu, bo sama funkcja zwraca ułamek 0,75 - to formatowanie, nie inna metryka. I od razu ważne zastrzeżenie: accuracy nie mierzy niczego innego. Nie mówi, jak długo trwał trening, bo czas mierzy się stoperem, a nie metryką. Nie mówi, ile pamięci zajął model. Nie jest też błędem średniokwadratowym - ten, jak zobaczysz za chwilę, należy do rodziny metryk regresji i liczy odległość od prawdy, a nie liczbę trafień. Accuracy to jedna, konkretna rzecz: procent poprawnych przewidywań.Te 75% wyglądają przyzwoicie, ale przypomnij sobie hipotetyczny model z początku lekcji, który zawsze mówił "spokój". Na naszych dwunastu klatkach kłusownik był cztery razy, więc taki leniwy model trafiłby osiem razy na dwanaście, czyli 66,67%. Nasz model jest lepszy o niecałe dziewięć punktów - a przecież to on ma pilnować rezerwatu. Accuracy tego nie pokaże, bo miesza dwa zupełnie różne rodzaje pomyłek. Nazwijmy je po imieniu.
Model podniósł alarm, choć było spokojnie: to fałszywy alarm, po angielsku false positive. Ktoś pojedzie sprawdzić pusty kwadrat sawanny. Model milczał, choć kłusownik był: to przeoczenie, false negative, i kosztuje dużo więcej. Trafiony alarm to true positive, a poprawne milczenie to true negative.
Dwie metryki zadają o te pomyłki dwa różne pytania. Precision, czyli precyzja, pyta: ile z alarmów podniesionych przez model było prawdziwych? To ocena wiarygodności alarmu. Recall, czyli czułość, pyta: ile z prawdziwych wtargnięć model w ogóle wykrył? To ocena szczelności systemu. Liczą je
precision_score i recall_score z tego samego modułu sklearn.metrics.1from sklearn.metrics import precision_score, recall_score
2
3print(f"Precision: {precision_score(y_true, y_pred):.2%}") # 66.67%
4print(f"Recall: {recall_score(y_true, y_pred):.2%}") # 50.00%Policzmy to ręcznie, bo tu warto zobaczyć skąd biorą się liczby. Model podniósł alarm trzy razy, z czego dwa razy słusznie: 2 podzielone przez 3 to 66,67% precyzji. Prawdziwych wtargnięć było cztery, a model wykrył dwa: 2 przez 4, czyli 50% czułości. Połowa kłusowników przeszła niezauważona. Najważniejsze jest to, co się nie zmieniło: przewidywania są dokładnie te same co przy accuracy, dane też. Zmieniło się wyłącznie pytanie, które zadajemy - i dlatego z jednego zestawu wyników wyszły trzy różne liczby. Moja rada, @name: przy każdym klasyfikatorze licz precision i recall razem z accuracy. Sama accuracy przy rzadkim zjawisku potrafi wyglądać świetnie i nie znaczyć nic.
Precyzja i czułość ciągną w przeciwne strony. Model, który alarmuje przy każdym poruszeniu trawy, będzie miał wysoką czułość i fatalną precyzję. Model, który alarmuje tylko przy stuprocentowej pewności - odwrotnie. Żeby porównywać modele jedną liczbą, łączy się je w metrykę F1, czyli średnią harmoniczną obu.
1from sklearn.metrics import f1_score
2
3print(f"F1: {f1_score(y_true, y_pred):.2%}") # 57.14%Dlaczego harmoniczna, a nie zwykła? Zwykła średnia z 66,67% i 50% to 58,33%, a F1 wyszło 57,14% - odrobinę niżej, bliżej gorszej ze składowych. To celowe. Średnia harmoniczna karze nierównowagę: model z precyzją 100% i czułością 1% dostanie F1 bliskie zeru, choć zwykła średnia dałaby mu przyzwoite 50%. Dzięki temu wysokie F1 można osiągnąć tylko wtedy, gdy obie składowe są przyzwoite naraz.
Każda z dotychczasowych metryk streszczała cały wynik do jednej liczby, a przy okazji gubiła informację, których pomyłek było więcej. Macierz pomyłek (confusion matrix) nie streszcza niczego: pokazuje wszystkie cztery przypadki naraz. Wierszami idzie prawda, kolumnami przewidywanie modelu, a klasy ustawiane są w kolejności rosnącej, więc przy etykietach 0 i 1 najpierw "spokój", potem "kłusownik". Funkcję
confusion_matrix sprowadzasz - jak poprzednie - z modułu sklearn.metrics.1from sklearn.metrics import confusion_matrix
2
3print(confusion_matrix(y_true, y_pred))
4# [[7 1]
5# [2 2]]Czytaj tę tabelkę wiersz po wierszu. Górny wiersz to klatki, na których naprawdę był spokój: 7 razy model też powiedział "spokój" (poprawne milczenie), 1 raz podniósł fałszywy alarm. Dolny wiersz to klatki z kłusownikiem: 2 razy model go przeoczył, 2 razy wykrył. Przekątna z lewego górnego rogu do prawego dolnego to trafienia, wszystko poza nią to pomyłki. Z tych czterech liczb odtworzysz każdą wcześniejszą metrykę: accuracy to (7 plus 2) przez 12, precision to 2 przez (2 plus 1), recall to 2 przez (2 plus 2). Dlatego macierz pomyłek warto oglądać jako pierwszą - metryki wyliczysz z niej, ale z metryk nie odtworzysz macierzy.
Gdy klas jest więcej niż dwie, precision i recall trzeba policzyć osobno dla każdej z nich, a to robi się żmudne. Scikit-learn ma na to gotowy skrót:
classification_report drukuje komplet metryk dla wszystkich klas w jednej tabeli. Parametr target_names pozwala podmienić numery klas na czytelne nazwy.1from sklearn.metrics import classification_report
2
3print(classification_report(y_true, y_pred, target_names=["spokoj", "klusownik"]))
4# tabela: precision, recall, f1-score i support dla kazdej klasyW wydruku zobaczysz po jednym wierszu na klasę, a w kolumnach znajome precision, recall i f1-score. Dochodzi jedna nowa kolumna: support, czyli liczba prawdziwych wystąpień danej klasy w
y_true. To ona zdradza nierównowagę - u nas osiem klatek spokoju i tylko cztery z kłusownikiem. Nie podaję tu dokładnego wyglądu tabeli, bo szerokości kolumn zmieniają się między wersjami biblioteki. Ważniejsze jest, co z niej wyczytasz: klasa rzadka niemal zawsze ma gorsze wyniki niż częsta, a to właśnie ona zwykle jest tą, na której Ci zależy.W klasyfikacji odpowiedź była trafna albo nie. W regresji tak się nie da: model, który przewidział 349 osobników zamiast 350, nie jest "niepoprawny", tylko bliski. Dlatego wszystkie metryki regresji mierzą odległość przewidywań od prawdy, a nie liczbę trafień. Zostajemy przy nazwach
y_true i y_pred, ale teraz kryją się pod nimi zupełnie inne dane: rzeczywiste i przewidziane liczebności stad w czterech rezerwatach.Pierwsza metryka to MSE, czyli mean squared error, błąd średniokwadratowy. Liczy różnicę dla każdego rezerwatu, podnosi ją do kwadratu i uśrednia. Kwadrat robi dwie rzeczy: usuwa znak, żeby pomyłka w dół nie kasowała pomyłki w górę, i wyolbrzymia duże błędy. Kłopot w tym, że wynik jest w kwadratach osobników, czego nie da się sensownie zinterpretować. Dlatego zwykle liczy się jeszcze RMSE, root mean squared error - pierwiastek z MSE, który wraca do jednostki oryginalnej. Pierwiastek weźmiemy funkcją
np.sqrt z NumPy, którą znasz z modułu ósmego.1from sklearn.metrics import mean_squared_error
2import numpy as np
3
4y_true = [150, 280, 420, 550] # rzeczywista liczebnosc stada
5y_pred = [140, 300, 400, 560] # przewidywania modelu
6
7mse = mean_squared_error(y_true, y_pred)
8print(f"MSE: {mse:.2f}") # 250.00
9print(f"RMSE: {np.sqrt(mse):.2f}") # 15.81MSE wyszło 250, ale ta liczba nic Ci nie mówi, bo to "250 kwadratowych osobników". Dopiero RMSE, czyli 15,81, jest zdaniem po polsku: typowa pomyłka modelu to około szesnastu zwierząt. Zwróć uwagę na kolejność argumentów - najpierw prawda, potem przewidywania. Przy MSE akurat zamiana miejscami nie zmieni wyniku, bo różnica jest podnoszona do kwadratu, ale w innych metrykach zmieni, więc lepiej od razu wyrobić sobie nawyk:
y_true zawsze pierwsze.Skoro kwadrat wyolbrzymia duże błędy, czasem chcesz metryki, która tego nie robi. To MAE, mean absolute error, czyli średni błąd bezwzględny: bierze wartość bezwzględną każdej różnicy i uśrednia, bez podnoszenia do kwadratu.
1from sklearn.metrics import mean_absolute_error
2
3print(f"MAE: {mean_absolute_error(y_true, y_pred):.2f}") # 15.00MAE wynosi 15, czyli mniej niż RMSE równe 15,81. Ta różnica nie jest przypadkiem i warto rozumieć, skąd się bierze. Nasze cztery pomyłki to 10, 20, 20 i 10 zwierząt - MAE po prostu je uśrednia i dostaje 15. RMSE, dzięki kwadratom, przyznaje większą wagę tym dwudziestkom, więc wychodzi wyżej. Reguła jest prosta: RMSE zawsze jest większe lub równe MAE, a im bardziej się rozjeżdżają, tym bardziej nierówne są Twoje błędy. Gdy różnica jest duża, poszukaj kilku rezerwatów, na których model kompletnie się gubi.
Obie te metryki mają jedną wadę: są w osobnikach, więc nie porównasz nimi modelu liczącego stada z modelem przewidującym opady. Potrzebna jest miara bez jednostki, i taką jest R², współczynnik determinacji. Liczy ją funkcja
r2_score, a odpowiada ona na pytanie: o ile lepszy jest mój model od najprostszej możliwej strategii, czyli od wypisywania zawsze średniej z prawdziwych wartości?1from sklearn.metrics import r2_score
2
3print(f"R2: {r2_score(y_true, y_pred):.4f}") # 0.9889Wynik 0,9889 czyta się tak: model wyjaśnia niecałe 99% zmienności liczebności stad, czyli jest bardzo blisko idealnego dopasowania. Jedynka oznaczałaby trafienie co do sztuki w każdym rezerwacie. I tu muszę sprostować rozpowszechnioną nieprawdę, którą znajdziesz w wielu ściągach: R² wcale nie mieści się w przedziale od zera do jedynki. Górną granicą jest 1, ale dolnej nie ma żadnej. Zero oznacza model dokładnie tak dobry jak wypisywanie średniej, a wszystko poniżej zera - model gorszy od tej średniej. Zobaczmy oba te przypadki na naszych czterech rezerwatach, bo dopiero wtedy widać, o co chodzi.
1# model, ktory zawsze mowi "srednia" (350)
2print(r2_score([150, 280, 420, 550], [350, 350, 350, 350])) # 0.0
3
4# model, ktory odwrocil kolejnosc rezerwatow
5print(r2_score([150, 280, 420, 550], [550, 420, 280, 150])) # -3.0Pierwszy model nie patrzy na dane w ogóle i dostaje dokładnie 0,0 - to nie zbieg okoliczności, tylko definicja tej metryki. Drugi pomylił rezerwaty i wypada na minus trzy, bo myli się grubo bardziej niż zwykłe zgadywanie średniej. Jeżeli kiedykolwiek zobaczysz ujemne R², nie szukaj błędu w funkcji: Twój model naprawdę jest gorszy od jednej stałej liczby.
Zauważ, co właśnie zrobiło R²: samo z siebie porównało model z najgłupszą sensowną strategią. Taki punkt odniesienia nazywa się baseline. Regresja ma go wbudowanego w metrykę, ale klasyfikacja nie ma - i dlatego można spędzić tydzień na modelu z accuracy 95%, nie wiedząc, że zgadywanie daje 94%.
Baseline dla klasyfikacji trzeba policzyć samodzielnie, ale scikit-learn ma do tego gotowe narzędzie:
DummyClassifier z modułu sklearn.dummy. To celowo bezmyślny model - ze strategią most_frequent zawsze odpowiada najczęstszą klasą ze zbioru treningowego i w ogóle nie patrzy na cechy. Właśnie dlatego musimy podać mu jakieś X o poprawnej liczbie wierszy, choć ich zawartość nie ma tu znaczenia.1from sklearn.dummy import DummyClassifier
2import numpy as np
3
4y_frames = [0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0]
5X_ignored = np.zeros((len(y_frames), 1))
6
7baseline = DummyClassifier(strategy="most_frequent")
8baseline.fit(X_ignored, y_frames)
9
10print(f"Baseline: {baseline.score(X_ignored, y_frames):.2%}") # 66.67%Baseline trafia 66,67%, bo osiem z dwunastu klatek to spokój, a on zawsze mówi "spokój". Nasz model miał 75%. Cała przewaga tygodnia pracy to nieco ponad osiem punktów procentowych - i to jest informacja, której accuracy sama w sobie nigdy by Ci nie dała. Zwróć uwagę, że
DummyClassifier obsługujesz identycznie jak prawdziwy klasyfikator: fit, potem score. Nie zmienił się żaden z Twoich modeli ani danych - to osobny, jednorazowy pomiar odniesienia. Moja rada: policz baseline zanim wytrenujesz cokolwiek poważnego. Jeśli baseline daje 94%, wiesz od razu, że musisz patrzeć na recall, a nie na accuracy.Wszystko, co dotąd zebraliśmy, układa się w stałą kolejność. Zapamiętaj ją, bo powtórzysz ją przy każdym modelu w tym module i w każdym projekcie później.
y_true z y_pred i liczysz accuracy, precision i recall, a przy regresji MSE, MAE i R².Kolejność nie jest przypadkowa i żadnego kroku nie da się przestawić. Nie policzysz metryk przed wykonaniem predykcji, bo nie miałbyś czego porównywać z prawdą. Baseline jest na końcu, bo to on nadaje sens liczbom z kroku trzeciego - dokładnie tak, jak 75% naszego modelu zaczęło coś znaczyć dopiero obok 66,67% od
DummyClassifier.W tym rytuale została jeszcze jedna słabość, i to poważna. Zbiór testowy powstaje z losowego podziału danych. Jeśli traf chciał, że wylądowały w nim same łatwe przypadki, dostaniesz zawyżoną ocenę. Jeśli same trudne - zaniżoną. Przy stu zwierzętach i podziale 80 na 20 cały werdykt opiera się na dwudziestu okazach, a wystarczy inny los i wynik potrafi skoczyć o kilkanaście punktów procentowych.
Lekarstwem jest cross-validation, czyli walidacja krzyżowa. To technika oceny modelu, która dzieli dane na wiele części treningowych i walidacyjnych. Te części nazywają się foldami. Przy pięciu foldach dane dzielone są na pięć równych kawałków, a potem procedura powtarza się pięć razy: za każdym razem inny fold pełni rolę zbioru walidacyjnego, a pozostałe cztery służą do treningu. Na końcu masz pięć ocen zamiast jednej, a każdy okaz był dokładnie raz sprawdzany i cztery razy używany do nauki.
Wyjaśnijmy od razu, czym cross-validation nie jest, bo nazwa bywa myląca. Nie jest rodzajem sieci neuronowej - sieci poznasz w lekcji o PyTorch i to zupełnie inna rzecz. Nie jest metodą czyszczenia danych - braki i duplikaty usuwałeś Pandasem w poprzednim module, i to dzieje się długo przed jakąkolwiek walidacją. Nie jest też algorytmem sortowania - walidacja krzyżowa niczego nie porządkuje, tylko wielokrotnie dzieli i wielokrotnie ocenia.
Do przećwiczenia jej potrzeba więcej niż ośmiu zwierząt, więc wygenerujemy sztuczny zbiór funkcją
make_classification z modułu sklearn.datasets. Uwaga na jej parametry, bo łatwo tu o błąd: n_features to liczba wszystkich kolumn, n_informative mówi, ile z nich naprawdę niesie sygnał, a n_redundant ile jest kopiami przeliczonymi z innych. Te trzy muszą się zgadzać arytmetycznie, a dodatkowo liczba klas razy liczba skupień na klasę (domyślnie dwa) nie może przekroczyć dwójki podniesionej do potęgi n_informative. Przy trzech gatunkach i domyślnym n_informative równym 2 wywołanie skończy się wyjątkiem ValueError, dlatego podajemy wszystkie te liczby jawnie.1from sklearn.datasets import make_classification
2
3X, y = make_classification(
4 n_samples=300,
5 n_features=4,
6 n_informative=4,
7 n_redundant=0,
8 n_classes=3, # 0 = lew, 1 = slon, 2 = gepard
9 random_state=42,
10)
11
12print(X.shape, y.shape) # (300, 4) (300,)Dostaliśmy 300 zwierząt opisanych czterema pomiarami i 300 etykiet - kształty potwierdza
shape, który znasz z NumPy. Parametr random_state=42 ustala ziarno losowości, dzięki czemu przy każdym uruchomieniu wygenerują się te same dane. To nie kosmetyka, tylko warunek powtarzalności: bez niego każde uruchomienie skryptu dawałoby inne wyniki i nie odróżniłbyś poprawki w modelu od zwykłego szczęścia.Teraz najważniejsza funkcja tej sekcji.
cross_val_score bierze niewytrenowany model, dane i liczbę foldów w parametrze cv, po czym sama wykonuje cały cykl: dzieli, trenuje, ocenia i zwraca tablicę wyników. Sprowadzasz ją z modułu sklearn.model_selection - tego samego, z którego brałeś train_test_split w poprzedniej lekcji.1from sklearn.model_selection import cross_val_score
2from sklearn.linear_model import LogisticRegression
3
4model = LogisticRegression(max_iter=1000)
5scores = cross_val_score(model, X, y, cv=5)
6
7print(len(scores)) # 5
8print(f"Srednia: {scores.mean():.2%} (+/- {scores.std() * 2:.2%})")Wynikiem jest pięć liczb, po jednej na fold - i one naprawdę będą się od siebie różnić, często o kilkanaście punktów procentowych. Nie podaję ich dokładnych wartości, bo zależą od wersji biblioteki, ale sam rozrzut jest tu ważniejszy niż konkretne cyfry: to on pokazuje, jak bardzo pojedynczy podział mógł Cię oszukać. Średnia z pięciu ocen jest znacznie uczciwszym podsumowaniem niż którakolwiek z nich osobno, a odchylenie standardowe pomnożone przez dwa daje wygodny margines niepewności. Zwróć uwagę na jeszcze jedną rzecz: przekazujemy model przed treningiem i nie wywołujemy
fit samodzielnie. cross_val_score trenuje pięć niezależnych kopii, a Twój obiekt model zostaje po wszystkim niewytrenowany - dlatego po walidacji krzyżowej i tak musisz go wytrenować na pełnych danych.Sam sposób dzielenia też można kontrolować, ale najpierw warto wiedzieć, co dzieje się domyślnie. Gdy przy klasyfikatorze podajesz samą liczbę, jak nasze
cv=5, scikit-learn wybiera podział warstwowy: pilnuje, żeby w każdym foldzie proporcje gatunków były takie same jak w całym zbiorze. To dobra decyzja i sama z siebie nic nie wymaga. Nie tasuje jednak wierszy przed podziałem, więc jeśli notes jest uporządkowany chronologicznie albo według kolejnych wypraw, sąsiadujące okazy trafią do tego samego foldu. Żeby to zmienić, w miejsce liczby przekazujesz obiekt dzielący. Najprostszym z nich jest KFold, który przyjmuje liczbę foldów, przełącznik shuffle oraz ziarno losowości.1from sklearn.model_selection import KFold
2
3kfold = KFold(n_splits=5, shuffle=True, random_state=42)
4scores = cross_val_score(model, X, y, cv=kfold)
5
6print(len(scores)) # 5Wyników nadal jest pięć, bo
n_splits=5 znaczy to samo co cv=5 - zmienił się wyłącznie sposób wybierania foldów, teraz z losowym potasowaniem. Jest tu jednak pułapka, o której musisz wiedzieć: KFold dzieli na ślepo i nie pilnuje proporcji klas, więc przy klasyfikacji tracisz warstwowanie, które dostawałeś za darmo. Dlatego moja rekomendacja jest taka, @name: przy regresji sięgaj po KFold, a przy klasyfikacji po StratifiedKFold z tego samego modułu - ma dokładnie te same parametry, a przy okazji zachowuje proporcje gatunków w każdym foldzie. Widziałeś pewnie w sieci przykłady z ręczną pętlą po kfold.split(X), gdzie samodzielnie wycina się indeksy treningowe i walidacyjne. Odradzam ten zapis na start, @name: robi dokładnie to samo, co jedna linijka wyżej, a daje cztery dodatkowe okazje do pomyłki. Przekaż KFold jako cv i pozwól bibliotece pilnować księgowości - ręczną pętlę pisz dopiero wtedy, gdy naprawdę potrzebujesz zrobić coś nietypowego wewnątrz każdego foldu.Zapamiętaj z tej lekcji jedno zdanie, @name: liczba, którą model wypisuje po treningu, nic nie znaczy, dopóki nie wiesz, jakiej odpowiedzi w ogóle szukałeś i ile trafiłoby zwykłe zgadywanie - dobry tropiciel nie ufa tropom, których nie ma z czym porównać.