In [1]:
# setup
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import warnings
warnings.filterwarnings('ignore')
In [2]:
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
os.chdir('/home/jarekj/Dropbox/dydaktyka/programowanie4/hausingData')

Metody nadzorowane

Metody nadzorowane to takie, w których dla części prób istnieje pewna referencja określana jako zmienna zależna. Proces uczenia pod nadzorem oznacza odwzorowanie zestawu zmiennych niezależnych (znanych dla każdej próby) aby zbudować model, z którego wynikała by wartość zmiennej zależnej. Zmienna zależna może być:

  • typu binarnego (Prawda / Fałsz) - mówimy wtedy o klasyfikacji binarnej;
  • typu kategoryzacyjnego (zestaw kategorii, najczęściej wyrażonych etykietami tekstowymi lub liczbami całkowitymi) - mówimy wtedy o klasyfikacjach wieloklasowych
  • typu numerycznego - mówimy wtedy o regresji

Istnieją jeszcze inne, bardziej złożone sytuacje, takie jak klasyfikacja wieloetykietowa (więcej niż jedna etykieta) czy klasyfikacje gdzie istnieje tylko jedna klasa (nie ma klasy przeciwnej) - kiedy móœimy o tzw klasyfikacjach jednoklasowych. Zagadnienie to wykracza poza zakres niniejszego kursu.

Do analizy użyjemy zestawu danych o wielkosci 1000 prób o cenach nieruchomości i zainteresowaniu nieruchomościami dla Miasta Poznania. Każda nieruchomość na rynku wtórnym jest opisana kilkoma zmiennymi takimi jak powierzchnia, ilość izb czy odległość od różnego typu udogodnień. Mamy dwie zmienne zależne:

  • zainteresowanie: zmienna binarna określająca czy mieszkanie spotka się z zainteresowaniem wielu klientów i nie trzeba będzie negocjować ceny
  • cena $ m^2 $: zmienna numeryczna

W ramach pracy z tymi zbiorami danych zapoznamy się z procesem klasyfikacji binarnej oraz regresji.

W pierwszym kroku wczytamy zbiór danych z pliku csv oraz wydzielimy z niego zmienne niezależne oraz dwie zmienne zależne. Zestaw zmiennych niezależnych oznaczymy jako X, zmienną zależną jako y.

In [3]:
data = pd.read_csv("hausing.csv")
X = data.drop(["cena","zainter"],axis=1)
y = data["zainter"]

Trudno jest przewidzieć jaki algorytm klasyfikujący najlepiej poradzi sobie z danym zestawem danych. W praktyce testuje się kilka klasyfikatorów i do dalszej pracy wybiera ten, który wykazuje się największą skutecznością. Skuteczność klasyfikacji mierzy się przy pomocy kilku wskaźników. Podstawą wyliczenia tych wskaźników jest relacja zmiennych zakwalifikowanych jako True i oryginalnie True (True Positive) lub błędnie zakwalifikowane oryginalne False (False Positive - błąd pierwszego typu); zakwalifikowane jako False oryginalne False (True negative) i błędnie zakwalifikowane oryginalne True (False negative bład drugiego typu). Zestawienie tych zmiennych przedstawiane są w postaci macierzy zmieszania (Confusion matrix)

Najważniejsze z nich to:

  • przywołanie (recall) - określa jaki procent wartości True zostało prawidłowo zakwalifikowane przez klasyfikator pozytywnie
  • precyzja (precision) - jaki procent wartości zakwalifikowanych jako pozytywnie jest orginalnie True
    • dokładność (accuracy) - procent prawidłowo zakwalifikowanych prób
    • statystyka F1 - średnia harmoniczna precyzji i przywołania
    • powierzchnia pod krzywą ROC (AUC - area under curve)

Wszystkie te miary oraz wiele innych są dostępne w pakiecie scikit-learn i mogą być stosowane bezpośrednio na danych jak i być przekazywane jako optymalizowane parametry kontrolne w trakcie procesu uczenia. Maksymalizacja przywołania minimalizuje błąd II typu a maksymalizacja precyzji błąd I typu.

Podstawowe klasyfikatory

W pierwszym kroku sprawdzimy trzy popularne klasyfikatory: K- najbliższych sąsiadów, Liniową Analizę Dyskryminacyjną i Wektory wsparcia. Sama teoria dotycząca stosowanych klasyfikatorów nie będzie omawiana w tej części kursu.

In [4]:
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.svm import SVC as SVM

Każdy klasyfikator zostanie wytrenowany do modelu poprzez zastosowanie metody fit() dla dwóch zbiorów danych X zawierającego zmienne niezależne i y zawierający zmienną zależną. Do każdego klasyfikatora w procesie tworzenia modelu możemy przekazać pewne parametry. Każdy klasyfikator posiada własny zestaw, dodatkowo w wielu modelach nie wszystkie klasyfikatory mogą ze sobą współwystępować. W celu doboru parametrów należy zapoznać się z dokumentacją poszczególnych klasyfikatorów. Brak parametrów to uruchomienie klasyfikatora z parametrami domyślnymi - jest to rozwiązanie dobre na start.

Po zbudowaniu modelu możemy sprawdzić jak skutecznie klasyfikator odtwarza oryginalną zmienną zależną

In [5]:
model_KNN = KNN().fit(X,y)
model_LDA = LDA().fit(X,y)
model_SVM = SVM().fit(X,y)

class_knn = model_KNN.predict(X)
class_lda = model_LDA.predict(X)
class_svn = model_SVM.predict(X)

Macierz zmieszania jest najpełniejszym narzędziem oceny jakości klasyfiatora. Sposób rozmieszczenia parametrów w tej macierzy zmieszania jest następujący:

TN FN
FP TP

Widzimy że dwa pierwsze klasyfikatory mają problemy z odtworzeniem zmiennej na podstawie której się wyuczyły, natomiast klasyfikator trzcie poradziła z tym sobie bez problemu. Z tego powodu w dalszej części będziemy używać klasyfikatora support vector machine (uważanego za jeden z najlepszych narzedzi klasyfikacyjnych.)

In [6]:
from sklearn.metrics import confusion_matrix
confusion_matrix(y,class_knn)
confusion_matrix(y,class_lda)
confusion_matrix(y,class_svn)
Out[6]:
array([[559,  69],
       [139, 233]])
Out[6]:
array([[559,  69],
       [238, 134]])
Out[6]:
array([[625,   3],
       [ 10, 362]])

Skalowanie danych

Wiele klasyfikatorów wymaga wcześniejszej transformacji danych głównie skalowania standaryzującego (z-score) lub skalowania do pewnego przedziału (najczęściej [0-1]).

Skalowanie/standaryzowanie danych jest najczęściej wykonywanych operacji na danych przed wyliczeniem niepodobieństwa. Scikit-learn oferuje kilka narzędzi skalowania danych i normalizacji. W przypadku klasycznej standaryzacji (usunięcie wartości średniej) i podzielenie przez odchylenie standardowe mamy do dyspozycji dwie funkcje: scale(), która po prostu dokonuje transformacji jednej macierzy w drugą oraz klasę StandardScaler(), która wymaga zastosowania metod fit() i transform(), dokonać transformacji odwrotnej itp. oraz może być użyta w przetwarzaniu potokowym (o czym później w ramach kursu). Podobnie w przypadku skalowania do przedziału 0-1 możemy stosować MinMaxScaler() lub funkcję minmax_scale(). Oczywiście tylko MinMaxScaler pozwala na transformację odwrtotną i może być stosowany w przekształceniach potokowych.

In [7]:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaled = scaler.fit_transform(X)

Macierze zmieszania pokazują że transformacja danych nie wpływa na wynik klasyfikatora LDA natomaist klasyfikator knn ulega pewnej poprawie KNN, co wynika z natury tych klasyfikatorów. Klasyfikator KNN zależy od odległości w przestrzeni cech, z tego powodu przeskalowanie danych prowadzi do jego pewnej poprawy.

In [8]:
print("LDA")
class_scaled_lda = model_LDA.fit(scaled,y).predict(scaled)
confusion_matrix(y,class_lda)
confusion_matrix(y,class_scaled_lda)

print("KNN")
class_scaled_knn = model_KNN.fit(scaled,y).predict(scaled)
confusion_matrix(y,class_knn)
confusion_matrix(y,class_scaled_knn)
LDA
Out[8]:
array([[559,  69],
       [238, 134]])
Out[8]:
array([[559,  69],
       [238, 134]])
KNN
Out[8]:
array([[559,  69],
       [139, 233]])
Out[8]:
array([[569,  59],
       [122, 250]])

Ocena wydajności klasyfikatorów

Aby prawidłowo ocenić wydajność klasyfikatorów nie należy oceniać ich na zbiorze na którym klasyfikator był trenowany ale na zbiorze, który dla klasyfikatora nie jest znany. W tym celu zbiór prób dla których istnieje zmienna zależna dzielony jest na dwie części określane jako zbiory treningowy i testowy. Istnieje wiele strategii z której za najbardziej wiarygodną uważa się podział na dwa niezależne zbiory losowe. Inne strategie jak ocena krzyżowa (corss-validation ) zakładają wielokrotny podział zbioru na część uczącą i testową wg różnych kryteriów regularnych, losowych, usuwania pojedynczych elementów itp. Scikit-learn oferuje wiele strategii podziałów z których omówimy zwykły podział, cross-walidację regularną i losową.

Do podziału na niezależne zbiory służy funkcja train_test_split(), do cross-walidacji cross_val_score(), która jako argumenty przyjmuje cały zbiór i mechanizm podziałów.

In [9]:
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(scaled, y, test_size=0.33, random_state=42)

new_model_SVM = SVM().fit(X_train,y_train)
new_class_train = new_model_SVM.predict(X_train)
new_class_test = new_model_SVM.predict(X_test)

confusion_matrix(y_train,new_class_train)
confusion_matrix(y_test,new_class_test)
Out[9]:
array([[394,  24],
       [129, 123]])
Out[9]:
array([[174,  36],
       [ 65,  55]])

Ocena przy pomocy cross-walidacji zwraca jako domyślny parametr oceny dokładność (accuracy), można go zmienić przy pomocy parametru scoring

In [10]:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(SVM(), scaled, y, cv=5)
scores
Out[10]:
array([0.76616915, 0.68656716, 0.74      , 0.70351759, 0.67336683])

Jeżeli nie chcemy stosować regularnej cross-walidacji możemy podzielić zbiór na losowe sub-zbiory. W przykładzie użyjemy innego narzędzia oceny jakości klasyfikacji - statystyki F1

In [11]:
from sklearn.model_selection import ShuffleSplit
cv = ShuffleSplit(n_splits=6, test_size=0.1, random_state=0)
scores = cross_val_score(SVM(), scaled, y, cv=cv,scoring='f1')
scores
Out[11]:
array([0.46666667, 0.6969697 , 0.59016393, 0.51724138, 0.74285714,
       0.64150943])

Ocena jakości klasyfikacji przy pomocy metryk i krzywej ROC

Krzywa ROC jest jednym z najlepszych graficznych narzędzi oceny jakości klasyfikacji i porównania jakości klasyfikatorów

In [12]:
from sklearn.metrics import roc_curve, accuracy_score, f1_score, recall_score, precision_score, auc

X_train, X_test, y_train, y_test = train_test_split(scaled, y, test_size=0.33)

prob_model_SVM = SVM(probability=True).fit(X_train,y_train)

prob_class_train = prob_model_SVM.predict(X_train)
prob_class_test = prob_model_SVM.predict(X_test)

prob_proba_train = prob_model_SVM.predict_proba(X_train)
prob_proba_test = prob_model_SVM.predict_proba(X_test)

fpr_tr, tpr_tr, thresholds_tr = roc_curve(y_train,prob_proba_train[:,1]) # dla true
fpr_ts, tpr_ts, thresholds_ts = roc_curve(y_test,prob_proba_test[:,1]) # dla true

print("Precision: %f" % precision_score(y_test,prob_class_test))
print("Recall: %f" % recall_score(y_test,prob_class_test))
print("Accuracy: %f" % accuracy_score(y_test,prob_class_test))
print("F1: %f" % f1_score(y_test,prob_class_test))
print("AUC: %f" % auc(fpr_ts, tpr_ts))
Precision: 0.753623
Recall: 0.436975
Accuracy: 0.745455
F1: 0.553191
AUC: 0.771516
In [13]:
fig, ax = plt.subplots()
sns.lineplot(fpr_tr,tpr_tr,ax=ax) # niebieska
sns.lineplot(fpr_ts,tpr_ts,ax=ax) # pomarańczowa
Out[13]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63b940c50>
Out[13]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63b940c50>

Do wyznaczenia optymalnej wartości odcięcia, innej niż domyślne 0.5 można posłużyć się diagramem precision-recall i poszukać miejsca gdzie obie linie się przecinają. Miejsce to wskazuje punkt najbardziej wytrenowania zrównoważonego klasyfikatora. Niezrównoważenie (imbalance) jest efektem różnicy w wielkości klas True i False w zbiorze uczącym. Należy dążyć do sytuacji aby te wartości były do siebie jak najbardziej zbliżone.

In [14]:
fig, ax = plt.subplots()
sns.lineplot(thresholds_ts,1-tpr_ts,ax=ax)
sns.lineplot(thresholds_ts,fpr_ts,ax=ax)
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63788ee48>
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63788ee48>
In [15]:
confusion_matrix(y_test,prob_class_test)
predy = prob_proba_test[:,1]>0.38
confusion_matrix(y_test,predy)
Out[15]:
array([[194,  17],
       [ 67,  52]])
Out[15]:
array([[167,  44],
       [ 46,  73]])

Do wyboru wartości odcięcia można również użyć krzywej precision- recall i poszukać na niej spłaszczeń. Pokazuje to obszary, gdzie klasyfikator jest najbardziej "robust", tzn odporny. Jest to zakres wartości odcięcia, gdzie jej wahanie wpływają znacząco na proporcje pomiędzy błędami. Tu jest to przedział pomiędzy 0.3 a 0.4.

In [16]:
from sklearn.metrics import precision_recall_curve
prec_tr, recl_tr, thresholds_tr = precision_recall_curve(y_train,prob_proba_train[:,1]) # dla true
prec_ts, recl_ts, thresholds_ts = precision_recall_curve(y_test,prob_proba_test[:,1]) 

fig, ax = plt.subplots()
sns.lineplot(recl_ts, prec_ts, ax=ax,color="#FF0000")
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff6377de160>

Wielkość zbioru treningowego

Rozmiar zbioru treningowego jest jednym z parametrów wpływających na jakość modelu. Z drugiej strony rozmiar modelu wpływa negatywnie na czas obliczeń zwłaszcza w sytuacjach gdy model musi być trenowany wielokrotnie - na przykład przy poszukiwaniu optymalnych parametrów. W praktyce powyżej pewnej ilości danych jakość modelu już się nie poprawia. W takiej sytuacji zwiększanie ilości danych nie ma sensu. Aby określić optymalną liczbę danych stosuje się krzywą uczenia. Wyznacza się wielkość podzbioru a następnie bada wartość parametru oceny jakości modelu.

Z poniższego wykresu wynika, że powyżej 300 prób jakość modelu zasadniczo nie ulega już poprawie

In [17]:
from sklearn.model_selection import learning_curve
trainings = [30, 60, 90, 120, 150, 180, 210, 250, 300, 400, 500, 666]
train_sizes, train_scores, valid_scores = learning_curve(SVM(), X, y, train_sizes=trainings, cv=3)

vmean = np.mean(valid_scores,axis=1)
tmean = np.mean(train_scores,axis=1)

import seaborn as sns
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
sns.lineplot(train_sizes,vmean,ax=ax) 
sns.lineplot(train_sizes,tmean,ax=ax)
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff637814198>
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff637814198>

Poprawa klasyfikatorów poprzez dobór optymalnych hiper-parametrów.

Algorytmy klasyfikacji wymagają ustawienia od kilku do kilkunastu parametrów, które wpływają na jakość klasyfikacji. Domyślne wartości parametrów ustawianych przed rozpoczęciem procesu uczenia (określanych jako hiper-parametry) nie zawsze są (z reguły nie są) najlepszą opcją. Z tego powodu w procesie trenowania klasyfikatora dokonuje się doboru optymalnego zestawu parametrów - tj. takiego, dla którego jakiś parametr kontrolny osiągnie wartość minimalną. Proces szukania odbywa się poprzez dobór trening i test klasyfikatora dla każdego z zestawu wartości. Należy pamiętać, że w czasie procesu wyszukiwania parametrów klasyfikator będzie trenowany tyle razy ile wynosi iloczyn wszystkich grup.

W poniższym przykładzie będziemy szukać optymalnej wartości parametrów gamma i C dla algorytmu SVM. Użyjemy 3 wartości Gamma i 6 dla C, co oznacza że wytrenujemy 36 klasyfikatorów.

In [18]:
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report

tuned_parameters = {'kernel': ['rbf'], 'gamma': [0.01, 0.001, 0.0001],'C': [1,2,5,10,20,50]}
clf = GridSearchCV(SVM(),tuned_parameters,cv=5,scoring='roc_auc',n_jobs=4)
clf.fit(X_train,y_train)
clf.best_params_
Out[18]:
GridSearchCV(cv=5, error_score='raise',
       estimator=SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False),
       fit_params=None, iid=True, n_jobs=4,
       param_grid={'C': [1, 2, 5, 10, 20, 50], 'kernel': ['rbf'], 'gamma': [0.01, 0.001, 0.0001]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='roc_auc', verbose=0)
Out[18]:
{'C': 10, 'gamma': 0.01, 'kernel': 'rbf'}

W następnym kroku będziemy szukać jeszcze bardziej dopasowanych wartości (w otoczeniu tych z pierwszego szukania) w celu jeszcze lepszego ich dopasowania. Proces szukania parametrów określa się terminem tuning (strojenie) kolejne iteracje tuningu określa się jako fine-tuning.

In [19]:
tuned_parameters = {'kernel': ['rbf'], 'gamma': [0.008, 0.01, 0.012],'C': [40,45,48,50,52,55,60]}
clf = GridSearchCV(SVM(),tuned_parameters,cv=5,scoring='roc_auc',n_jobs=4)
clf.fit(X_train,y_train)
clf.best_params_
Out[19]:
GridSearchCV(cv=5, error_score='raise',
       estimator=SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False),
       fit_params=None, iid=True, n_jobs=4,
       param_grid={'C': [40, 45, 48, 50, 52, 55, 60], 'kernel': ['rbf'], 'gamma': [0.008, 0.01, 0.012]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='roc_auc', verbose=0)
Out[19]:
{'C': 52, 'gamma': 0.012, 'kernel': 'rbf'}

Ostatnim krokiem jest przygotowanie raportu, który zamkniemy w postaci funkcji. Zmienna obiektu clf (classifier) cv_results_ zawiera tablice estymatorów (średnia, odchylenie standardowe) dla poszczególnych kroków szukania. Wyświetlimy je w postaci listy:

In [20]:
def report(clf):
    means = clf.cv_results_['mean_test_score']
    stds = clf.cv_results_['std_test_score']
    parnames = clf.cv_results_['params']
    for mean, std, params in zip(means, stds, parnames):
        print("%0.3f (+/-%0.03f) for %r" % (mean, std * 2, params))
In [21]:
report(clf)
0.716 (+/-0.070) for {'C': 40, 'kernel': 'rbf', 'gamma': 0.008}
0.720 (+/-0.074) for {'C': 40, 'kernel': 'rbf', 'gamma': 0.01}
0.720 (+/-0.076) for {'C': 40, 'kernel': 'rbf', 'gamma': 0.012}
0.717 (+/-0.075) for {'C': 45, 'kernel': 'rbf', 'gamma': 0.008}
0.720 (+/-0.073) for {'C': 45, 'kernel': 'rbf', 'gamma': 0.01}
0.723 (+/-0.078) for {'C': 45, 'kernel': 'rbf', 'gamma': 0.012}
0.717 (+/-0.074) for {'C': 48, 'kernel': 'rbf', 'gamma': 0.008}
0.719 (+/-0.073) for {'C': 48, 'kernel': 'rbf', 'gamma': 0.01}
0.724 (+/-0.078) for {'C': 48, 'kernel': 'rbf', 'gamma': 0.012}
0.718 (+/-0.075) for {'C': 50, 'kernel': 'rbf', 'gamma': 0.008}
0.719 (+/-0.073) for {'C': 50, 'kernel': 'rbf', 'gamma': 0.01}
0.724 (+/-0.080) for {'C': 50, 'kernel': 'rbf', 'gamma': 0.012}
0.718 (+/-0.075) for {'C': 52, 'kernel': 'rbf', 'gamma': 0.008}
0.718 (+/-0.074) for {'C': 52, 'kernel': 'rbf', 'gamma': 0.01}
0.725 (+/-0.079) for {'C': 52, 'kernel': 'rbf', 'gamma': 0.012}
0.718 (+/-0.074) for {'C': 55, 'kernel': 'rbf', 'gamma': 0.008}
0.718 (+/-0.075) for {'C': 55, 'kernel': 'rbf', 'gamma': 0.01}
0.725 (+/-0.080) for {'C': 55, 'kernel': 'rbf', 'gamma': 0.012}
0.718 (+/-0.074) for {'C': 60, 'kernel': 'rbf', 'gamma': 0.008}
0.719 (+/-0.076) for {'C': 60, 'kernel': 'rbf', 'gamma': 0.01}
0.724 (+/-0.082) for {'C': 60, 'kernel': 'rbf', 'gamma': 0.012}
In [22]:
y_pred = clf.predict(X_test)
print(classification_report(y_test,y_pred))
confusion_matrix(y_test,y_pred)
             precision    recall  f1-score   support

      False       0.73      0.89      0.80       211
       True       0.68      0.40      0.51       119

avg / total       0.71      0.72      0.69       330

Out[22]:
array([[188,  23],
       [ 71,  48]])

Wybór parametru dla którego wykonujemy optymalizację zależy wyłącznie od analityka. Nie zawsze chcemy minimalizować błąd całkowity, często ważniejszy jest błąd I lub II typu. Jeżeli chcemy minimalizować tylko jeden z tych dwóch błędów musimy wybrać jako parametr scoring odpowiednio recall lub precison. Tu będziemy minimalizować błąd I typu. Należy podkreślić, że nazwy minimalizowanych parametrów nie muszą się zgadzać z tymi stosowanymi do oceny jakości klasyfikatora. Lista dostępnych metryk znajduje się pod tym adresem [http://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter]

In [23]:
tuned_parameters = {'kernel': ['rbf'], 'gamma': [0.008, 0.01, 0.012],'C': [40,45,48,50,52,55,60]}
clf = GridSearchCV(SVM(),tuned_parameters,cv=5,scoring='recall')
clf.fit(X_train,y_train)
clf.best_params_
Out[23]:
GridSearchCV(cv=5, error_score='raise',
       estimator=SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False),
       fit_params=None, iid=True, n_jobs=1,
       param_grid={'C': [40, 45, 48, 50, 52, 55, 60], 'kernel': ['rbf'], 'gamma': [0.008, 0.01, 0.012]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='recall', verbose=0)
Out[23]:
{'C': 50, 'gamma': 0.012, 'kernel': 'rbf'}

Inne metody klasyfikacji: bagging/boostiing

Pojedyncze klasyfikatory rzadko są skuteczne, zwłaszcza przy złożonych problemach, gdzie nie można zbudować prostego modelu zastępuje się je rozwiązaniami, gdzie zamiast jednego silnego modelu stosuje się wiele słabych, które działają dobrze jedynie w wybranych fragmentach danych. Takie rozwiązania określa się jako klasyfikatory wzmacniane (boosting) lub zbiorcze (bagging), gdzie wiele słabych klasyfikatorów składa się na jeden silny. Poniżej przetestujemy kilka takich rozwiązań, a jeden z nich (Gradient Boostng)poddamy dodatkowo tuningowi. Należy podkreślić, że poza doborem zestawu innych parametrów, od strony technicznej stosowanie takich klasyfikatorów nie różni się zasadniczo od wcześniej użytego SVM. Klasyfikatory wywodzące się z drzew decyzyjnych nie wymagają współcześniejszego skalowania danych ani redukcji wymiarowości.

In [24]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, ExtraTreesClassifier

model_trees = DecisionTreeClassifier()
scores_trees = cross_val_score(model_trees, X, y, cv=5)
scores_trees

model_ADA = AdaBoostClassifier()
scores_ADA = cross_val_score(model_ADA, X, y, cv=5)
scores_ADA

model_RF = RandomForestClassifier()
scores_RF = cross_val_score(model_RF, X, y, cv=5)
scores_RF

model_ET = ExtraTreesClassifier()
scores_ET = cross_val_score(model_ET, X, y, cv=5)
scores_ET
/usr/local/lib/python3.5/dist-packages/sklearn/ensemble/weight_boosting.py:29: DeprecationWarning: numpy.core.umath_tests is an internal NumPy module and should not be imported. It will be removed in a future NumPy release.
  from numpy.core.umath_tests import inner1d
Out[24]:
array([0.7761194 , 0.73134328, 0.74      , 0.66834171, 0.72361809])
Out[24]:
array([0.7761194 , 0.69154229, 0.725     , 0.69346734, 0.68341709])
Out[24]:
array([0.76119403, 0.71144279, 0.78      , 0.70854271, 0.72361809])
Out[24]:
array([0.78109453, 0.78109453, 0.795     , 0.75879397, 0.70854271])
In [25]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)

ET_class_train = model_ET.fit(X_train,y_train).predict(X_train)
ET_class_test = model_ET.fit(X_train,y_train).predict(X_test)

confusion_matrix(y_train,ET_class_train)
confusion_matrix(y_test,ET_class_test)
Out[25]:
array([[414,   0],
       [  2, 254]])
Out[25]:
array([[193,  21],
       [ 50,  66]])
In [26]:
from sklearn.ensemble import GradientBoostingClassifier

model_GB = GradientBoostingClassifier(n_estimators=300,learning_rate=0.03,max_depth=10,min_samples_split=4)

GB_class_train = model_GB.fit(X_train,y_train).predict(X_train)
GB_class_test = model_GB.fit(X_train,y_train).predict(X_test)

confusion_matrix(y_train,GB_class_train)
confusion_matrix(y_test,GB_class_test)
Out[26]:
array([[413,   1],
       [  1, 255]])
Out[26]:
array([[193,  21],
       [ 52,  64]])
In [27]:
tuned_parameters = {'n_estimators': [30,40,50,60,70],
                    'learning_rate': [0.08,0.09,0.1,0.15,0.02],
                    'max_depth': [12,14,15,16,17],
                    'min_samples_split': [3,4,5]}

clf = GridSearchCV(GradientBoostingClassifier(),tuned_parameters,cv=5,scoring='recall',n_jobs=12)
clf.fit(X_train,y_train)
clf.best_params_
Out[27]:
GridSearchCV(cv=5, error_score='raise',
       estimator=GradientBoostingClassifier(criterion='friedman_mse', init=None,
              learning_rate=0.1, loss='deviance', max_depth=3,
              max_features=None, max_leaf_nodes=None,
              min_impurity_decrease=0.0, min_impurity_split=None,
              min_samples_leaf=1, min_samples_split=2,
              min_weight_fraction_leaf=0.0, n_estimators=100,
              presort='auto', random_state=None, subsample=1.0, verbose=0,
              warm_start=False),
       fit_params=None, iid=True, n_jobs=12,
       param_grid={'min_samples_split': [3, 4, 5], 'n_estimators': [30, 40, 50, 60, 70], 'learning_rate': [0.08, 0.09, 0.1, 0.15, 0.02], 'max_depth': [12, 14, 15, 16, 17]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='recall', verbose=0)
Out[27]:
{'learning_rate': 0.15,
 'max_depth': 14,
 'min_samples_split': 4,
 'n_estimators': 70}

Po wykonaniu tuningu, słownik .best_params_ możemy przekazać bezpośrednio do klasyfikatora zamiast argumentów, pamięając jednak o zastsowaniu operatora rozwijania słownika (**)

In [28]:
model_GB = GradientBoostingClassifier(**clf.best_params_)

GB_class_train = model_GB.fit(X_train,y_train).predict(X_train)
GB_class_test = model_GB.fit(X_train,y_train).predict(X_test)

confusion_matrix(y_train,GB_class_train)
confusion_matrix(y_test,GB_class_test)
Out[28]:
array([[413,   1],
       [  1, 255]])
Out[28]:
array([[188,  26],
       [ 47,  69]])

Sieci neuronowe i funkcja kosztu

Sieci neuronowe są popularnym i ważnym klasyfikatorem, który jest podstawą systemów głębokiego uczenia (deep learning). Ogólnodostępne algorytmy trenowania sieci neuronowych nie są zbyt wydajne i są stosowane głównie w celach szkoleniowych. W tej części kursu wprowadzimy zagadnienia niezbędne do prawidłowego trenowania sieci i analizy wyników. Klasyfikator sieci neuronowych w pakiecie Scikit-learn ukrywa się pod nazwą MLPClassifier (multi-layer-perceptor). Sieci neuronowe są algorytmem iteracyjnym, w każdym kolejnym kroku wagi poszczególnych neuronów są modyfikowane w taki sposób, aby funkcja kosztu malała. Funkcja kosztu (loss) lub odwrotna celu (objective) to funkcja która rzutuje wartości parametrów na pewną wartość liczbową, którą intuicyjnie rozumiemy jako koszt powiązany z parametrami. Jako funkcję kosztu w sieciach neuronowych dla klasyfikacji stosuje się entropię względną - czyli miarę zgodności względem próby kontrolnej

Sieci neuronowe wymagają przeskalowania danych do przedziału 0-1 (nie z-score), należy więc zmienne niezależne w pierwszym kroku potraktować tym narzędziem preprocessingu.

In [29]:
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler().fit(X) #najpierw transform potem split:
mmX = scaler.transform(X)

X_train, X_test, y_train, y_test = train_test_split(mmX, y, test_size=0.33)

NN = MLPClassifier(max_iter=500,validation_fraction=0.5,beta_1=0.94)
NN.fit(X_train,y_train)

pred_train = NN.predict(X_train)
pred_test = NN.predict(X_test)

# użyc confusion matrix
confusion_matrix(y_train,pred_train)
confusion_matrix(y_test,pred_test)
Out[29]:
MLPClassifier(activation='relu', alpha=0.0001, batch_size='auto', beta_1=0.94,
       beta_2=0.999, early_stopping=False, epsilon=1e-08,
       hidden_layer_sizes=(100,), learning_rate='constant',
       learning_rate_init=0.001, max_iter=500, momentum=0.9,
       nesterovs_momentum=True, power_t=0.5, random_state=None,
       shuffle=True, solver='adam', tol=0.0001, validation_fraction=0.5,
       verbose=False, warm_start=False)
Out[29]:
array([[370,  46],
       [120, 134]])
Out[29]:
array([[185,  27],
       [ 71,  47]])

Charakterystyczną cechą sieci neuronowych jest krzywa funkcji kosztu zależna od kolejnej iteracji. Pokazuje nam ona jak klasyfikator ulega polepszeniu z każdą kolejną iteracją. Krzywą kosztu można przedstawić w formie wykresu

In [30]:
plt.plot(np.arange(NN.n_iter_),NN.loss_curve_)
Out[30]:
[<matplotlib.lines.Line2D at 0x7ff64ff2ab38>]

Regresja

Modele predykcyjne pozwalają nie tylko przewidzieć kategorię analizowanego obiektu ale również wartość jaką przyjmuje obiekt/zjawisko. W sytuacji gdy przewidujemy wartość a nie kategorię zamiast o klasyfikacji mówimy o regresji, a model określa się terminem regresor zamiast klasyfikator. W wielu przypadkach do regresji wykorzystujemy te same narzędzia co do klasyfikacji - poza pewnymi wypadkami. Wyłącznie do regresji przeznaczone są penalizowane modele regresji liniowej. Scikit-learn używa osobnych struktur do regresji na przykład wektory wsparcia mają odpowiednio algorytm SVC przeznaczony do klasyfikacji i SVR do regresji, podobnie dla sieci neuronowych funkcjonują odpowiednio MLPClassifier i MLPRegressor.

In [31]:
from sklearn.preprocessing import StandardScaler

from sklearn.linear_model import ElasticNetCV
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR

from sklearn.metrics import mean_absolute_error, explained_variance_score
y = data['cena']
std_X = StandardScaler().fit(X).transform(X)
raw_X = X

Trenowanie regresora nie różni się niczym od trenowania klasyfikatora, z tą różnicą że zmienna zależna nie jest kategoryzowana a jest wektorem wartości numerycznych. Również metryki określające skuteczność predykcji związane są nie tyle z ilością skutecznie przewidzianych obiektów co błędem (np. całkowitym, względnym, średnim) wartości znanej i estymowanej.

In [32]:
X_train, X_test, y_train, y_test = train_test_split(std_X, y, test_size=0.33)
model = SVR().fit(X_train,y_train)
pred_train = model.predict(X_train)
pred_test = model.predict(X_test)

fig, ax = plt.subplots()
sns.scatterplot(y_train,pred_train,ax=ax)
sns.scatterplot(y_test,pred_test,ax=ax)
mean_absolute_error(y_train,pred_train)
mean_absolute_error(y_test,pred_test)
Out[32]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63772c710>
Out[32]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63772c710>
Out[32]:
1047.932938951919
Out[32]:
1112.7914121060921

Modele penalizowane

Popularnymi i często stosowanymi modelami regresji są modele penalizowane (z karą za zbyt dużą liczbą zmiennych niezależnych w stosunku do liczby próbek). Kara to ograniczenia w parametrach równania regresji , które powodują że wartości poszczególnych wskaźników regresji redukowane są w kierunku zera (przy wskaźniku równym 0 zmienna nie ma wpływu na wynik predykcji). W ten sposób mniej istotne zmienne mają mniejszy wpływ na ostateczny wynik. Powoduje to że modele penalizowane są znacznie bardziej odporne na przeuczenie. W praktyce stosuje się modele Ridge stosujący normę L2 (suma kwadratów wskaźników), Lasso z normą L1 (suma wartości bezwzględnych) i ElasticNet będący połączeniem obu. Zalety i wady obu metod są omówione w części teoretycznej. Jako przykład użyjemy metody ElasticNet.

In [33]:
# ridge
X_train, X_test, y_train, y_test = train_test_split(std_X, y, test_size=0.33)
model = ElasticNetCV().fit(X_train,y_train)
pred_train = model.predict(X_train)
pred_test = model.predict(X_test)

fig, ax = plt.subplots()
sns.scatterplot(y_train,pred_train,ax=ax)
sns.scatterplot(y_test,pred_test,ax=ax)
mean_absolute_error(y_train,pred_train)
mean_absolute_error(y_test,pred_test)
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63770fef0>
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff63770fef0>
Out[33]:
951.0785431064703
Out[33]:
935.2947173495637

Boosting - uśrednianie zamiast głosowania

Algorytmy wzmacniane w przypadku klasyfikacji stosują głosowanie - wygrywa kategoria z większą liczbą głosów. W przypadku regresji stosują uśrednianie - często ważone - otrzymanych wyników dla każdego ze słabych klasyfikatorów.

In [34]:
# random forsest
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
model = RandomForestRegressor().fit(X_train,y_train)
pred_train = model.predict(X_train)
pred_test = model.predict(X_test)

fig, ax = plt.subplots()
sns.scatterplot(y_train,pred_train,ax=ax)
sns.scatterplot(y_test,pred_test,ax=ax)
mean_absolute_error(y_train,pred_train)
rf_bo = mean_absolute_error(y_test,pred_test)
rf_bo
Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff6377a2cf8>
Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff6377a2cf8>
Out[34]:
312.6933223880597
Out[34]:
880.7433873737374

Optymalizacja regresora na przykładzie Random Forest

Proces optymalizacji hiperparametrów regresorów nie różni się zasadniczo od optymalizacji parametrów klasyfikatorów. Jedyna różnica do optymalizowany parametr - w tym wypadku jest to jeden z średnich błędów. Należy pamiętać że nazwy metryk używanych w optymalizacji parametrów różnią się od tych stosowanych do oceny jakości regresora. Na przykład zastosujemy średni błąd całkowity (mean absolute error, mae), który tu nosi nazwę neg_mean_absolute_error. Lista dostępnych metryk znajduje się pod tym adresem [http://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter]

In [35]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)

'''tuned_parameters = {'n_estimators': [8,10,20,50,100],
                    'max_features': [3,5,7,12],
                    'max_depth': [3,5,10,15],
                    'min_samples_split': [2,4,8,12]}
'''

tuned_parameters = {'n_estimators': [90,100,110,150,200],
                    'max_features': [12],
                    'max_depth': [10,15,20],
                    'min_samples_split': [2,3]}

model = RandomForestRegressor()
clf = GridSearchCV(model,tuned_parameters,cv=5,scoring='neg_mean_absolute_error',n_jobs=12)
clf.fit(X_train,y_train)
clf.best_params_
Out[35]:
"tuned_parameters = {'n_estimators': [8,10,20,50,100],\n                    'max_features': [3,5,7,12],\n                    'max_depth': [3,5,10,15],\n                    'min_samples_split': [2,4,8,12]}\n"
Out[35]:
GridSearchCV(cv=5, error_score='raise',
       estimator=RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features='auto', max_leaf_nodes=None,
           min_impurity_decrease=0.0, min_impurity_split=None,
           min_samples_leaf=1, min_samples_split=2,
           min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
           oob_score=False, random_state=None, verbose=0, warm_start=False),
       fit_params=None, iid=True, n_jobs=12,
       param_grid={'n_estimators': [90, 100, 110, 150, 200], 'min_samples_split': [2, 3], 'max_features': [12], 'max_depth': [10, 15, 20]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='neg_mean_absolute_error', verbose=0)
Out[35]:
{'max_depth': 20,
 'max_features': 12,
 'min_samples_split': 2,
 'n_estimators': 90}

Po znalezieniu optymalnego zestawu parametrów widzimy, że uzyskany średni błąd całkowity jest mniejszy niż w jakimkolwiek innym regresorze, pomimo to model wciąż pozostaje nieco przeuczony.

In [36]:
model = RandomForestRegressor(**clf.best_params_).fit(X_train,y_train)
pred_train = model.predict(X_train)
pred_test = model.predict(X_test)

fig, ax = plt.subplots()
sns.scatterplot(y_train,pred_train,ax=ax)
sns.scatterplot(y_test,pred_test,ax=ax)
mean_absolute_error(y_train,pred_train)
rf_ao = mean_absolute_error(y_test,pred_test)
Out[36]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff6376f9828>
Out[36]:
<matplotlib.axes._subplots.AxesSubplot at 0x7ff6376f9828>
Out[36]:
309.70505841644626
In [37]:
rf_bo
rf_ao
Out[37]:
880.7433873737374
Out[37]:
740.5680032017998

Scikit-learn pozwala również zapisać klasyfikator na dysku i wywołać go w momencie, gdy będziemy chcieli zastosować go do nowych danych w przyszłości. Nie należy używać do tego modułu pickle(), tylko zastosować moduł joblib, odpowiednio z funkcjami dump() i load() (podobienie jak w pickle).

In [38]:
from sklearn.externals import joblib
joblib.dump(model,'model_svr.pkl')
new_model = joblib.load('model_svr.pkl')
newdata = pd.read_csv("new_hauses.csv")
new_model.predict(newdata)
Out[38]:
['model_svr.pkl']
Out[38]:
array([3930.12166667, 5350.17085185, 5159.15255556, 3761.59397222,
       4570.59933333, 5703.30933333, 5530.59468835, 4977.28188889,
       5072.07888889, 3549.82866667])