Czy lubicie statystykę? Jest duże prawdopodobieństwo, 😉 że nie. Nie przejmujcie się jednak, statystyka nie jest również moją mocną stroną. W sumie trudno powiedzieć, dlaczego większość osób stroni od statystyki, ale czuję, że wiele zagadnień statystycznych jest nie do końca intuicyjnych. Wystarczy sięgnąć po paradoks hazardzisty lub paradoks Monty’ego Halla. Ciekawa lektura, 🙂 Serio! Niezależnie od tego, czy lubicie statystykę czy nie, to zdecydowana większość uczenia maszynowego jest oparta lub korzysta ze statystyki. A już na pewno Naiwny Bayes lub bardziej precyzyjnie naiwny klasyfikator bayesowski.
Po przeczytaniu niniejszego posta:
- będziecie wiedzieli dlaczego Naiwny Bayes jest naiwny,
- poznacie podstawy teorii stojącej za naiwnym klasyfikatorem bayesowskim,
- będziecie potrafili samodzielnie wyliczyć wynik działania klasyfikatora,
- będziecie wiedzieli jak skorzystać z klasyfikatora dostępnego w bibliotece scikit-learn.
Dlaczego Naiwny Bayes jest właściwie naiwny?
Chciałbym to wyjaśnić na początku, bo jest to kluczowa kwestia dla zrozumienia zasady działania klasyfikatora. Przypuśćmy, że chcemy sklasyfikować jakąś informację na podstawie kilku cech wejściowych. Niech będzie to klasyfikacja jest słonecznie / jest pochmurno na podstawie: pory roku, temperatury, wilgotności i ciśnienia atmosferycznego. W przypadku klasyfikatora bayesowskiego przyjmujemy założenie, że cechy wejściowe zupełnie nie zależą od siebie nawzajem. W naszym przykładzie jednym z założeń będzie więc: temperatura nie zależy od pory roku. Czy nie jest to założenie naiwne? 😉
Dodatkowo przyjmujemy, że każda z cech wejściowych jest tak samo ważna jak każda inna. To również jest dość naiwne, bo mimo że meteorologiem nie jestem, to mógłbym się założyć, że pewne parametry będą miały większy wpływ na „słoneczność” dnia, a inne nieco mniejszy.
Mimo że założenia bayesowskie są nieco oderwane od rzeczywistości, to jednak naiwne klasyfikatory bayesowskie sprawdzają się wyjątkowo dobrze w zadaniach klasyfikacyjnych (detekcja spamu, klasyfikacja tekstu, analiza sentymentu), systemach rekomendacji, a ze względu na szybkość działania również w przewidywaniu w czasie rzeczywistym. Nie sprawdzają się za to w regresji.
Dane do nauki
Czas przyjrzeć się danym, na których nauczymy naiwniaka. Ponieważ jestem fanem koszykówki i lubię oglądać NBA, to przygotowałem własny mały zbiór, na którym można będzie dokonać relatywnie prostych ręcznych obliczeń, ale również uruchomić algorytm oferowany przez scikit-learn. W tabeli mamy następujące kolumny:
- Zespół – kolumna pomocnicza/opisowa zawierająca nazwę zespołu; nie bierze udziału w obliczeniach.
- Sezon – kolumna pomocnicza/opisowa zawierająca wskazanie sezonu, którego dotyczy wiersz danych; nie bierze udziału w obliczeniach.
- Play-offy w poprzednim roku – pierwsza z cech wejściowych wskazująca, czy zespół był w poprzednim roku w fazie play-off.
- Gracz(e) w meczu All-Star – czy zespół miał reprezentanta w meczu gwiazd w tym sezonie.
- Salary cap ponad 50. centyl – zespoły mają limit wydatków na pensje swoich graczy – tzw. salary cap. Po jego przekroczeniu zespół musi płacić dodatkowy, kosztowny podatek. W założeniu mechanizm ma chronić przed dominacją bardzo bogatych zespołów i wyrównywać szanse. Działa to skutecznie, ale i tak większość zespołów przekracza salary cap, czasami o kilkadziesiąt milionów. Kolumna wskazuje, czy dany zespół przekroczył salary cap więcej niż połowa innych zespołów.
- Play-offy – kolumna wskazująca na faktyczną przynależność do klasy: Tak – drużyna zakwalifikowała się do play-off w danym sezonie / Nie – drużyna zakończyła. rozgrywki na sezonie zasadniczym i nie grała w play-off
Zespół | Sezon | Play-offy w poprzednim roku | Gracz(e) w meczu All-Star | Salary cap ponad 50-ty centyl | Play-offy |
Atlanta Hawks | 2018-19 | Nie | Nie | Nie | Nie |
Boston Celtics | 2018-19 | Tak | Tak | Tak | Tak |
Brooklyn Nets | 2018-19 | Nie | Tak | Nie | Tak |
Charlotte Hornets | 2018-19 | Nie | Tak | Tak | Nie |
… | … | … | … | … | … |
Sacramento Kings | 2017-18 | Nie | Nie | Nie | Nie |
San Antonio Spurs | 2017-18 | Tak | Tak | Tak | Tak |
Toronto Raptors | 2017-18 | Tak | Tak | Tak | Tak |
Utah Jazz | 2017-18 | Tak | Nie | Nie | Tak |
Washington Wizards | 2017-18 | Tak | Tak | Tak | Tak |
Dane są dostępne do pobrania w formacie csv: NBA – Naiwny Bayes. Wszystkie „Tak” reprezentowane są przez 1.0, wszystkie „Nie” przez 0.0. Jeżeli chcecie zaimportować plik do OpenOffice lub innego arkusza kalkulacyjnego, to należy ustawić parametry importu w następujący sposób:
Naiwny Bayes z wykorzystaniem scikit-learn
Zanim przejdziemy do samodzielnego zbudowania klasyfikatora, wykorzystajmy do tego celu bibliotekę scikit-learn. W tym celu proponuję, abyście utworzyli nowe środowisko wirtualne używając condy i uruchomili Jupyter Notebook. Jeżeli nie jesteście pewni, jak przygotować środowisko programistyczne do uczenia maszynowego, to w tym poście przeczytacie jak je zbudować . Do środowiska, poza pakietami wymienionymi we wskazanym poście, należy dodać również bibliotekę scikit-learn.
Na wstępie importujemy biblioteki numpy i pandas oraz wczytujemy plik .csv do obiektu dataframe:
import numpy as np
import pandas as pd
data_frame = pd.read_csv(r'.\NBA - Naiwny Bayes.csv', encoding='utf-8')
Wyświetlamy kilka pierwszych wierszy dataframe:
data_frame.head()
Zespół | Sezon | Play-offy w poprzednim roku | Gracz(e) w meczu All-Star | Salary cap ponad 50-ty centyl | Play-offy | |
---|---|---|---|---|---|---|
0 | Atlanta Hawks | 2018-19 | 0.0 | 0.0 | 0.0 | 0.0 |
1 | Boston Celtics | 2018-19 | 1.0 | 1.0 | 1.0 | 1.0 |
2 | Brooklyn Nets | 2018-19 | 0.0 | 1.0 | 0.0 | 1.0 |
3 | Charlotte Hornets | 2018-19 | 0.0 | 1.0 | 1.0 | 0.0 |
4 | Chicago Bulls | 2018-19 | 0.0 | 0.0 | 0.0 | 0.0 |
Jak widać, dane wczytały się poprawnie. Dodatkowo dataframe dodaje pierwszą kolumnę indeksującą. Kolumny nr 2 „Zespół” i nr 3 „Sezon” są opisowe i nie będziemy używali ich w procesie nauki. Kolumna ostatnia – nr 7 „Play-offy” – jest z kolei informacją o tym, czy zespół zakwalifikował się do play-off czy nie. Jest to nasz target, czyli faktyczna kwalifikacja do play-off lub nie. W procesie nauki udział będą brały kolumny nr 4, 5 i 6 – jako dane wejściowe oraz nr 7 jako target (labelka). Trzeba zatem tak przekształcić dane, abyśmy otrzymali dwie zmienne. Jedną z kolumnami 4,5 oraz 6 i drugą z kolumną nr 7. Najpierw wykonajmy kopię dataframe zawierającą tylko kolumny 4, 5 i 6:
df_X = data_frame.drop(['Zespół', 'Sezon', 'Play-offy'], axis=1)
df_X.head()
Play-offy w poprzednim roku | Gracz(e) w meczu All-Star | Salary cap ponad 50-ty centyl | |
---|---|---|---|
0 | 0.0 | 0.0 | 0.0 |
1 | 1.0 | 1.0 | 1.0 |
2 | 0.0 | 1.0 | 0.0 |
3 | 0.0 | 1.0 | 1.0 |
4 | 0.0 | 0.0 | 0.0 |
W kolejnym kroku konwertujemy dataframe do tablicy numpy, a następnie całą operację powtarzamy dla zmiennej target:
data = df_X.to_numpy()
data[0:5]
>>>array([[0., 0., 0.],
[1., 1., 1.],
[0., 1., 0.],
[0., 1., 1.],
[0., 0., 0.]])
target = data_frame[['Play-offy']].to_numpy()
target[0:5]
>>>array([[0.],
[1.],
[1.],
[0.],
[0.]])
Mając przygotowane dane w tablicach numpy, możemy przekazać je do nauki w klasyfikatorach udostępnianych przez bibliotekę scikit-learn.
Przeglądając dokumentację scikit-learn dla naiwnego Bayesa, można łatwo zauważyć, że dostępnych mamy kilka klasyfikatorów. Pytanie: którego użyć?
- Gaussian – używamy dla danych ciągłych, dla których możemy założyć rozkład normalny.
- Dla danych dyskretnych (np. ilość graczy All-Star w zespole, ocena zakupionego towaru w skali od 1 do 5) użyjemy Multinomial Naive Bayes.
- Jeżeli nasze dane mają charakter binarny (zera i jedynki) – a tak jest w naszym przypadku – najbardziej odpowiednim będzie Bernoulli.
Zanim przejdziemy do kodu, warto zwrócić uwagę, że nasz zbiór jest bardzo mały i zawiera tylko trzy parametry wejściowe. Współczesna koszykówka jest zaawansowaną dyscypliną sportu, w której znaczenie odgrywa bardzo wiele czynników. Zespoły rywalizujące na najwyższym poziomie starają się dopracować każdy aspekt związany z techniką gry, taktyką zespołu, zdrowiem zawodników, czy nawet takimi kwestiami, jak odżywianie, suplementacja czy logistyka przejazdów. Aby dobrze ocenić faktyczne szanse zespołu na wejście do fazy play-off należałoby zapewne wziąć pod uwagę dziesiątki, jeśli nie setki parametrów. Na pewno dużo więcej niż trzy. 😉 Stąd wynik jaki otrzymamy należy traktować z przymrużeniem oka, a celem tego prostego ćwiczenia jest jedynie zapoznanie Was ze sposobem budowania klasyfikatora oraz teorią leżącą u podstaw naiwnego Bayesa. Co więcej, dobrą praktyką w uczeniu maszynowym jest wydzielenie zbioru uczącego i testowego. My tego nie zrobimy ze względu na wielkość zbioru i opisany powyżej, raczej teoretyczny charakter rozważań. Wracając do kodu:
from sklearn.naive_bayes import BernoulliNB
klasyfikator = BernoulliNB()
klasyfikator.fit(data, target.ravel())
predictions = klasyfikator.predict(data)
predictions
>>> array([0., 1., 0., 1., 0., 0., 0., 0., 1., 1., 1., 1., 0., 0., 0., 1., 1.,
1., 1., 0., 1., 0., 1., 0., 1., 0., 1., 1., 0., 1., 0., 1., 0., 1.,
0., 1., 0., 0., 1., 1., 1., 1., 0., 0., 0., 0., 1., 0., 1., 0., 1.,
0., 0., 0., 1., 0., 1., 1., 0., 1.]
Aby sprawdzić jak dobrze klasyfikator poradził sobie z danymi, wyliczamy accuracy:
print("Bernoulli Naive Bayes Accuracy: ",
np.round((predictions == target.ravel()).sum() / target.size * 100, decimals=2), "%"
)
>>>Bernoulli Naive Bayes Accuracy: 73.33 %
Naiwny Bayes – jak to policzyć „na piechotę”?
Dotarliśmy do momentu, w którym nie można się już zasłonić sprawdzoną biblioteką do uczenia maszynowego i trzeba zakasać rękawy do pracy. Ucząc klasyfikatora bayesowskiego, tak naprawdę tworzymy model statystyczny. Dokładniej rzecz ujmując – model prawdopodobieństwa. Musimy zatem sięgnąć po trochę statystyki. Tworząc model prawdopodobieństwa dla klasyfikatora, staramy się odpowiedzieć na pytanie: jakie jest prawdopodobieństwo, że mamy do czynienia z jakąś klasą C, zakładając dane wejściowe: x1, x2, …, xn. W naszym przypadku byłoby to np.: jakie jest prawdopodobieństwo, że klasa C=drużyna zagra w play-off, zakładając. że x1=zespół grał w play-off w zeszłym roku, x2=zespół nie ma reprezentanta w meczu gwiazd oraz x3=zespół wydaje mniej na pensje niż co najmniej połowa innych zespołów. Problem ten można zapisać następującym wzorem:
P(C | x1, x2, x3)
lub bardziej ogólnie:
P (C | Xn)
Formułę tę czytamy: Jakie jest prawdopodobieństwo wystąpienia klasy C, mając dany wektor wejściowy Xn.
W naszym prostym przykładzie, gdy mamy 3 binarne cechy wejściowe, model można byłoby prosto zbudować, obliczając prawdopodobieństwa dla każdego wariantu. Jednak zwykle cech jest dużo więcej (a więc i ich kombinacji) i dodatkowo mogą one przyjmować wartości ciągłe, co powoduje, że tak sformułowanego problemu praktycznie rozwiązać się nie da. Tu z pomocą przychodzi nam Twierdzenie Bayesa, które jak pamiętamy zakładało „naiwnie”, że cechy wejściowe X są od siebie niezależne i każda z nich jest tak samo istotna dla modelu. Jest to wprawdzie bardzo duże uproszczenie świata rzeczywistego, ale po pierwsze działa, 😎 a po drugie pozwala znacząco uprościć obliczenia. Twierdzenie Bayesa wygląda tak:
P(C | X) = P(C) * P(X | C)P(X)
A dla naszego prostego przykładu można je zapisać tak:
P(C | x1,x2,x3) =P(C) * P(x1,x2,x3 | C)P(x1,x2,x3)
Uwierzcie lub nie, ale tworząc klasyfikator możemy całkowicie zignorować mianownik tego wyrażenia. Dlaczego? Sięgnijmy do naszego przykładu i przyjmijmy, że chcemy sobie odpowiedzieć na pytanie, czy zespół zagra w play-off w tym roku (C = Tak), czy jednak nie zagra (C=Nie), jeżeli x1=Tak=zespół grał w play-off w zeszłym roku, x2=Nie=zespół nie ma reprezentanta w meczu gwiazd oraz x3=Nie=zespół wydaje mniej na pensje niż co najmniej połowa innych zespołów. Odpowiedź poznamy, jeżeli uda nam się wyliczyć i porównać prawdopodobieństwa dla obu klas:
P(C=Tak) * P(Tak, Nie, Nie | C=Tak) P(Tak, Nie, Nie)
oraz
P(C=Nie) * P(Tak, Nie, Nie | C=Nie) P(Tak, Nie, Nie)
Czy oceniając które z tych prawdopodobieństw jest wyższe, będziemy w ogóle brali pod uwagę mianownik? Nie, gdyż jest on taki sam w obu wyrażeniach. Decydować będzie zatem tylko wartość licznika. Uprości nam to zagadnienie do postaci:
P(C | x1,x2,x3) = P(C) * P(x1,x2,x3 | C)
Co więcej, skoro działamy przy naiwnych założeniach bayesowskich, a dokładniej skoro cechy x1,x2,x3 są od siebie niezależne, to ostatecznie prawdopodobieństwo dla danej klasy C wyliczymy ze wzoru:
P(C | x1,x2,x3) = P(C) * P(x1 | C) * P(x2 | C) * P(x3 | C)
Pamiętajmy, że powyższy wzór to wariant dla dwóch klas i trzech danych wejściowych. Można go jednak uogólnić do następującej postaci:
P(C | Xn) = P(C) * ∑P(Xi | C)i=1n
Wracając do naszego prostego przykładu, w którym mamy dwie klasy C i trzy parametry wejściowe X: aby móc wyliczyć prawdopodobieństwo wystąpienia klasy dla dowolnej kombinacji parametrów wejściowych, potrzebujemy mieć wartości prawdopodobieństwa wystąpienia każdej z klas – P(C) oraz wszystkie kombinacje P(Xi | C). Proponuję nieśmiało, abyście spróbowali policzyć to samemu i najlepiej skorzystać do tego celu z arkusza kalkulacyjnego i opcji filtrowania. Nie przejmujcie się, że możecie gdzieś popełnić błąd (ja pewnie gdzieś jakiś poniżej zrobiłem) – wszystkie te żmudne obliczenia ostatecznie wykonywać będzie maszyna. A nam chodzi jedynie o to, aby złapać metodologię i rozumieć zasady działania klasyfikatora bayesowskiego.
Moje obliczenia zebrałem w tabeli poniżej.
P(C=NIE) | 28/62 | |
P(C=TAK) | 32/62 | |
Play-offy w poprzednim roku | P(C=NIE) | P(C=TAK) |
Nie | 19/28 | 9/32 |
Tak | 9/28 | 23/32 |
Gracz(e) w meczu All-Star | P(C=NIE) | P(C=TAK) |
Nie | 18/28 | 3/32 |
Tak | 10/28 | 29/32 |
Salary cap ponad 50-ty centyl | P(C=NIE) | P(C=TAK) |
Nie | 18/28 | 12/32 |
Tak | 10/28 | 20/32 |
Mając obliczone poszczególne prawdopodobieństwa, możemy pokusić się o policzenie klasyfikacji dla przykładowego zestawu danych wejściowych. Weźmy na warsztat zespół Altlanta Hawks. Jest to młody zespół, będący w sezonie 2019-20 na fali wznoszącej, ze wschodzącą gwiazdą młodego pokolenia – Trae Youngiem, który obstawiam, że znajdzie się w meczu All-Star 2020, a więc niech x2 = Tak. W sezonie 2018-19 Atlanta była poza fazą play-off (x1 = Nie), a na płace zawodników wydaje obecnie najmniej w całej lidze (x3 = Nie). Dla takich danych wejściowych chcemy policzyć dwa prawdopodobieństwa: P(C=Tak – zagra w play-off) oraz P(C=Nie – nie zagra w play-off), a następnie porównać, które z nich jest wyższe i co za tym idzie, które zostanie wybrane przez nasz klasyfikator.
Ponieważ porównując prawdopodobieństwo dla obu klas, możemy zignorować mianowniki (będą one takie same), to należy tu dla ścisłości dodać, że nie będziemy liczyć prawdopodobieństwa, a jedynie liczniki wzoru na prawdopodobieństwo. Jest to jednak wystarczające do poprawnej klasyfikacji.
Dla klasy Tak – zagra w play-off mamy:
P(Tak|Nie,Tak,Nie)=P(Tak)*P(x1=Nie|Tak)*P(x2=Tak|Tak)*P(x3=Nie|Tak) = 3262*932*2932*1232=313263488≈0.04933
Dla klasy Nie – nie zagra w play-off mamy:
P(Nie|Nie,Tak,Nie)=P(Nie)*P(x1=Nie|Nie)*P(x2=Tak|Nie)*P(x3=Nie|Nie)= 2862*1928*1028*1828=342048608≈0.07036
Jak widać, wyższą wartość, a więc i prawdopodobieństwo, ma wariant, w którym Atlanta Hawks nie zagrają w play-off. Ja jednak liczę, że nasz klasyfikator się myli. 😉
Masz pytanie? Zadaj je w komentarzu.
Spodobał ci się post? Będzie mi miło, gdy go polecisz.
Do zobaczenia wkrótce, przy okazji omawiania innego ciekawego tematu!
A skąd się wzęły te wyniki w tabeli? Jak oblicza się te prawdopodobieństwa? 🙂