Nastał piątek, a zarazem także czas, aby uraczyć nasze oczy kolejnym zbiorem przykładów. Dzisiaj zbudujemy model klasyfikacji, dokonamy za jego pomocą predykcji oraz ocenimy, jak dobrze nasz klasyfikator radzi sobie z zadanym problemem. Nie traćmy więc czasu i zabierzmy się do pracy!

Przygotowanie zbioru danych

Wczytanie wcześniej wygenerowanego zbioru danych

We wcześniejszych przykładach nauczyliśmy się generować syntetyczny zbiór danych, zapisywać go do pliku CSV oraz takowe pliki wczytywać. Zacznijmy więc dzisiejsze zadanie od wczytania wcześniej przygotowanego zestawu danych, który znajduje się w pliku dataset.csv (wygodnie dostępnym do pobrania tutaj).

Zestaw danych został wygenerowany przy pomocy dobrze nam znanego już kodu:

from sklearn.datasets import make_classification
X, y = datasets.make_classification(
    n_samples=100,
    n_features=2,
    n_informative=1,
    n_repeated=0,
    n_redundant=0,
    flip_y=.05,
    random_state=1410,
    n_clusters_per_class=1
)

Wczytywanie problemu i jego podział na zbiór danych i zbiór etykiet, dla przypomnienia, realizujemy w sposób następujący:

import numpy as np
dataset = np.genfromtxt("dataset.csv", delimiter=",")
X = dataset[:, :-1]
y = dataset[:, -1].astype(int)

Mamy gotowy już nasz X i y, jeżeli jednak chcemy aby nasze badania nosiły chociażby znamiona rzetelności, to w żadnym razie nie możemy na tym poprzestać.

Podział danych na zbiór uczący oraz zbiór testowy

Przed wykonaniem jakichkolwiek badań musimy pamiętać, aby podzielić nasz zestaw danych na zbiór uczący oraz zbiór testowy. W zadaniu klasyfikacji zbiór uczący zawiera znane nam instancje problemu, które zostały wcześniej poetykietowane i posłużą do wytrenowania wybranego przez nas modelu, aby potem mógł on generalizować zdobytą wiedzę w odniesieniu do instancji, których wcześniej nie widział (czyli zbioru testowego). Trenowanie i testowanie modelu na tym samym zbiorze danych jest absolutnie niedopuszczalne i uzyskane w ten sposób wyniki w żaden sposób nie odzwierciedlają faktycznej zdolności predykcyjnej klasyfikatora.

W dzisiejszym przykładzie skorzystamy z funkcji train_test_split(), za której pomocą dokonamy pojedynczego podziału naszego zbioru danych. Jest to podejście zdecydowanie intelektualnie nienachalne i urągające godności naukowca, niemniej jednak przedstawimy je tutaj (i tylko tutaj) jako najprostszy możliwy przykład podziału danych na potrzeby zadania klasyfikacji. W przyszłą środę nauczymy się używać walidacji krzyżowej, która pozwoli nam na rzetelne przeprowadzenie badań i to właśnie z niej będziemy korzystać we wszystkich przyszłych starciach z uczeniem maszynowym.

Wracając jednak do teraźniejszości, przygotujmy nasz zbiór testowy oraz zbiór uczący za pomocą train_test_split():

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=.3,
    random_state=42
)

W tym przypadku, jako argumenty funkcji, przekazać musimy tylko dwa parametry t.j. dobrze już nam znany i lubiany random_state oraz test_size, który mówi nam o tym, jaki procent wszystkich danych zostanie wykorzystany do testowania naszego modelu. Tutaj decydujemy się na wykorzystanie 70% danych do uczenia oraz 30% do testowania klasyfikatora.

Klasyfikacja

Inicjalizacja i budowa modelu klasyfikacji

Teraz, gdy przygotowaliśmy już nasze dane, możemy w końcu przejść do inicjalizacji oraz wytrenowania modelu klasyfikacji. Skorzystamy z prostego klasyfikatora probabilistycznego, którym jest Naiwny klasyfikator bayesowski w wersji opartej na rozkładzie normalnym (rozkładzie Gaussa). Jest on nazywany naiwnym, ponieważ zakłada niezależność od siebie cech problemu, a implementuje go klasa GaussianNB.

W celu dopasowania naszego modelu do danych uczących wywołujemy metodę fit(), która przyjmuje kolejno zbiór danych oraz zbiór etykiet naszego zbioru uczącego:

from sklearn.naive_bayes import GaussianNB
clf = GaussianNB()
clf.fit(X_train, y_train)

Nasz model został już wyuczony na poetykietowanych danych, możemy więc w końcu przystąpić do predykcji.

Wyznaczenie macierzy wsparcia oraz predykcji

Klasyfikatory zaimplementowane w bibliotece scikit-learn implementują metodę predict(), która pozwala nam na bezpośrednie uzyskanie etykiet przyporządkowanych nieznanym instancjom przez klasyfikator. My jednak, żeby nie było tak łatwo, nie skorzystamy z tej metody.

Dzięki temu, że zastosowany przez nas model jest probabilistyczny, możemy poprosić go o zwrócenie macierzy wsparć. Liczba wierszy tej macierzy jest równa liczbie instancji problemu w zbiorze testowym (czyli u nas 30), a liczba kolumn równa jest liczbie klas problemu (w naszym przypadku problem jest binarny, więc mamy dwie kolumny). Każdy z wierszy macierzy odpowiada wektorowi wsparć zwróconemu przez model dla danej klasyfikowanej instancji problemu. Wartości w tym wektorze mówią nam o tym, z jakim prawdopodobieństwem klasyfikowana instancja należy do danej klasy. Macierz wsparć uzyskać możemy korzystając z metody predict_proba() i podając jako jej argument nasz testowy zbiór danych:

class_probabilities = clf.predict_proba(X_test)
print("Class probabilities:\n", class_probabilities)
>> Class probabilities:
>> [[2.57883436e-06 9.99997421e 01]
>> [1.99658559e-06 9.99998003e-01]
>> [9.90973563e-01 9.02643748e-03]
>> [5.47593735e-05 9.99945241e-01]
>> [9.98382405e-01 1.61759531e-03]
...

Posiadając wiedzę o prawdopodobieństwach przynależności instancji ze zbioru testowego do klas, możemy w prosty sposób wyznaczyć predykcję. Zrobimy to zakładając, że każda z instancji testowych należy do tej klasy problemu, dla której prawdopodobieństwo przynależności jest największe.

Możemy dokonać tego bardzo sprawnie za pomocą funkcji argmax(), która zwróci nam dla każdego wiersza indeks kolumny, w której wystąpiła największa wartość. W tym celu podajemy jako argumenty wcześniej uzyskaną macierz wsparć oraz oś, po której chcemy się poruszać:

predict = np.argmax(class_probabilities, axis=1)
print("Predicted labels:\n", predict)
>> Predicted labels:
>> [1 1 0 1 0 0 1 0 1 0 1 0 1 1 0 0 1 0 0 1 0 1 1 1 1 1 0 0 0 0]

W celach całkowicie ilustracyjnych, wypiszemy teraz obok siebie prawdziwe etykiety naszego zbioru testowego (y_test) oraz wyznaczoną przez nas predykcję (predict):

print("True labels:     ", y_test)
print("Predicted labels:", predict)
>> True labels:      [1 1 0 1 0 1 1 0 1 0 1 0 1 1 0 1 1 0 0 1 0 1 1 1 1 1 0 0 0 0]
>> Predicted labels: [1 1 0 1 0 0 1 0 1 0 1 0 1 1 0 0 1 0 0 1 0 1 1 1 1 1 0 0 0 0]

Jako, że mamy tutaj do czynienia zaledwie z 30 instancjami problemu, już na pierwszy rzut oka możemy stwierdzić, że oba wektory nieznacznie różnią się od siebie. Świadczy to o tym, że nasza predykcja nie jest idealna (i w praktyce nigdy idealna nie będzie), ale oczywiście ocena działania naszego modelu w ten sposób byłaby niewygodna. Potrzebujemy konkretnej wartości, która powiem nam jak dobry w tym przypadku jest nasz klasyfikator.

Ocena jakości klasyfikacji naszego modelu

Udało nam się dotrzeć do ostatniego dzisiejszego zagadnienia, a mianowicie do oceny jakości klasyfikacji. Istnieje wiele metryk ewaluacji (o nich nauczymy się później przy okazji rozmowy o danych niezbalansowanych), ale w dzisiejszym przykładzie wystarczy nam najprostsza z nich, a mianowicie jakość klasyfikacji. Informuje nas ona o tym, jaki procent instancji w zbiorze testowym został przez nas zaklasyfikowany poprawnie i sprawdza się w przypadku problemów zbalansowanych, czyli takich, w których liczba instancji należących do obu klas jest stosunkowo podobna.

Klasyfikatory w bibliotece scikit-learn implementują metodę score(), która pozwala nam na wyznaczenie jakości klasyfikacji. My jednak skorzystamy z osobnej funkcji accuracy_score(), dzięki której uzyskamy tą wartość na podstawie wyznaczonej przez nas w poprzednim kroku predykcji. W ten sposób przyzwyczaimy się do samodzielnego przeprowadzania predykcji i wyznaczania metryki ewaluacji, co okaże się konieczne w przypadku budowania własnych modeli klasyfikacji (zwłaszcza zespołowych) oraz radzenia sobie z danymi niezbalansowanymi.

from sklearn.metrics import accuracy_score
score = accuracy_score(y_test, predict)
print("Accuracy score:\n %.2f" % score)
>> Accuracy score:
>> 0.93

Jak widzimy, nasz model osiągnął 93% jakości klasyfikacji. Jest to wynik bardzo dobry, ale niestety wynikający głównie z prostoty problemu, którym się dzisiaj zajmowaliśmy.

Dodatkowo, jeżeli chcemy dowiedzieć się więcej o tym jak nasz model się zachował, możemy wyznaczyć za pomocą funkcji confusion_matrix() dobrze znaną nam z wykładu macierz konfuzji:

from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, predict)
print("Confusion matrix: \n", cm)
>> Confusion matrix:
>> [[13  0]
>> [ 2 15]]

Dzięki temu dowiedzieliśmy się, że nasz klasyfikator popełnił dwa błędy polegające na zaklasyfikowaniu obiektu klasy pozytywnej jako obiekt klasy negatywnej (wartość False Negative = 2).

Uff, to już wszystko na dzisiaj. W przyszłą środę nauczymy się przeprowadzać eksperymenty po ludzku, czyli z wykorzystaniem walidacji krzyżowej. A do tego czasu możemy być dumni ze zdobytej dziś podstawowej wiedzy!