W naszych poprzednich zadaniach istotną kwestią okazywało się
przeszukiwanie przestrzeni parametrów określających
dany klasyfikator - np. w przypadku metody najbliższych sąsiadów jedyną
zmienną, którą sterowaliśmy była liczba najbliższych sąsiadów. Tę
analizę możną wykonywac “na piechotę”, ale często łatwiej jest się
odwołać do funkcji GridSearchCV(). Jak sama nazwa wskazuje,
jej celem jest wykonanie CV na pewnej sitace, czyli zestawie
parametrów.
Zaczynamy od nieśmiertelnych danych (trochę zmienionych):
import numpy as np
import pandas as pd
def draw_data(n1, n2, m1, m2, S1, S2):
data1 = np.random.multivariate_normal(m1, S1, n1)
data2 = np.random.multivariate_normal(m2, S2, n2)
data = np.concatenate((data1, data2), axis = 0)
data = pd.DataFrame(data, columns=["x", "y"])
data["klasa"] = np.repeat([0, 1], [n1, n2])
return data
m1, m2 = [-1, -1], [1, 1]
S1 = [[4, 0], [0, 4]]
S2 = [[4, 0], [0, 4]]
n1, n2 = 60, 40
np.random.seed(129)
data = draw_data(n1, n2, m1, m2, S1, S2)
Następnie uruchamiamy pakiety oraz określamy siatkę parametrów - na początku bedzie to po prostu liczba najbliższych sąsiadów:
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
param_grid = {'n_neighbors': range(2, 22)}
Kolejnym krokiem jest wskazanie klasyfikatora oraz uruchomienie docelowej funkcji
clf = KNeighborsClassifier()
clf_grid = GridSearchCV(clf, param_grid)
clf_grid.fit(data[["x", "y"]], data.klasa)
GridSearchCV(estimator=KNeighborsClassifier(),
param_grid={'n_neighbors': range(2, 22)})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. KNeighborsClassifier(n_neighbors=15)
Teraz przyjrzyjmy się wynikom, które kryja się pod
clf.grid.cv_results_:
print(clf_grid.cv_results_)
## {'mean_fit_time': array([0.00103087, 0.00086465, 0.00088787, 0.00104766, 0.00090022,
## 0.00097637, 0.00086799, 0.00093455, 0.00082698, 0.00091963,
## 0.00082917, 0.00086117, 0.00082784, 0.00081258, 0.000811 ,
## 0.00125141, 0.00097685, 0.00085316, 0.00079618, 0.00088997]), 'std_fit_time': array([1.80731247e-04, 7.13216099e-05, 5.15489419e-05, 3.22793589e-04,
## 1.59487695e-04, 1.63889798e-04, 5.10796101e-05, 8.93312642e-05,
## 1.39922256e-05, 1.90570248e-04, 3.73005543e-05, 6.37436940e-05,
## 2.84801546e-05, 7.93099323e-06, 9.75432152e-06, 7.41339366e-04,
## 2.01895713e-04, 1.10031515e-04, 2.46206868e-06, 1.70189147e-04]), 'mean_score_time': array([0.00160389, 0.00129361, 0.0015862 , 0.00127063, 0.00130925,
## 0.00146461, 0.00137863, 0.00145402, 0.00131855, 0.00137696,
## 0.00128679, 0.0013298 , 0.00129609, 0.0012548 , 0.00124326,
## 0.00160637, 0.00145893, 0.0014195 , 0.00132179, 0.00135555]), 'std_score_time': array([2.70354197e-04, 1.01480694e-04, 3.94514357e-04, 6.81018956e-05,
## 4.90082366e-05, 1.06534535e-04, 1.54411667e-04, 2.62227863e-04,
## 1.55602245e-04, 1.55494440e-04, 9.28267850e-05, 1.09807802e-04,
## 3.72319143e-05, 3.14712524e-05, 3.88447014e-05, 5.05244300e-04,
## 2.25398327e-04, 3.63612374e-04, 1.73930234e-04, 1.15898957e-04]), 'param_n_neighbors': masked_array(data=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
## 18, 19, 20, 21],
## mask=[False, False, False, False, False, False, False, False,
## False, False, False, False, False, False, False, False,
## False, False, False, False],
## fill_value=999999), 'params': [{'n_neighbors': 2}, {'n_neighbors': 3}, {'n_neighbors': 4}, {'n_neighbors': 5}, {'n_neighbors': 6}, {'n_neighbors': 7}, {'n_neighbors': 8}, {'n_neighbors': 9}, {'n_neighbors': 10}, {'n_neighbors': 11}, {'n_neighbors': 12}, {'n_neighbors': 13}, {'n_neighbors': 14}, {'n_neighbors': 15}, {'n_neighbors': 16}, {'n_neighbors': 17}, {'n_neighbors': 18}, {'n_neighbors': 19}, {'n_neighbors': 20}, {'n_neighbors': 21}], 'split0_test_score': array([0.8 , 0.8 , 0.75, 0.75, 0.85, 0.8 , 0.9 , 0.85, 0.85, 0.85, 0.9 ,
## 0.9 , 0.9 , 0.9 , 0.85, 0.85, 0.85, 0.85, 0.85, 0.85]), 'split1_test_score': array([0.9 , 0.8 , 0.9 , 0.9 , 0.9 , 0.9 , 0.9 , 0.95, 0.9 , 0.9 , 0.9 ,
## 0.95, 0.9 , 0.9 , 0.9 , 0.9 , 0.85, 0.9 , 0.9 , 0.9 ]), 'split2_test_score': array([0.7 , 0.65, 0.7 , 0.8 , 0.8 , 0.75, 0.8 , 0.8 , 0.8 , 0.8 , 0.8 ,
## 0.8 , 0.85, 0.85, 0.85, 0.85, 0.9 , 0.85, 0.85, 0.85]), 'split3_test_score': array([0.7 , 0.75, 0.75, 0.75, 0.75, 0.75, 0.8 , 0.8 , 0.75, 0.75, 0.75,
## 0.8 , 0.75, 0.85, 0.8 , 0.85, 0.85, 0.85, 0.85, 0.85]), 'split4_test_score': array([0.8 , 0.75, 0.8 , 0.8 , 0.8 , 0.85, 0.85, 0.85, 0.85, 0.9 , 0.85,
## 0.9 , 0.9 , 0.9 , 0.85, 0.85, 0.75, 0.8 , 0.7 , 0.75]), 'mean_test_score': array([0.78, 0.75, 0.78, 0.8 , 0.82, 0.81, 0.85, 0.85, 0.83, 0.84, 0.84,
## 0.87, 0.86, 0.88, 0.85, 0.86, 0.84, 0.85, 0.83, 0.84]), 'std_test_score': array([0.07483315, 0.05477226, 0.0678233 , 0.05477226, 0.0509902 ,
## 0.05830952, 0.04472136, 0.05477226, 0.0509902 , 0.05830952,
## 0.05830952, 0.06 , 0.05830952, 0.0244949 , 0.03162278,
## 0.02 , 0.04898979, 0.03162278, 0.0678233 , 0.04898979]), 'rank_test_score': array([18, 20, 19, 17, 15, 16, 5, 8, 14, 9, 9, 2, 3, 1, 5, 3, 9,
## 5, 13, 9], dtype=int32)}
Jest to dość sporo danych. Dla nas najważniejsza jest kwestia jakości (skuteczności) klasyfikatora mierzonego za pomocą wyniku kroswalidacji. Możemy to przedstawić za pomocą prostego wykresu:
import matplotlib.pyplot as plt
plt.errorbar(param_grid["n_neighbors"], clf_grid.cv_results_["mean_test_score"], clf_grid.cv_results_["std_test_score"], fmt="o", capsize=4)
plt.ylim(0.5, 1)
## (0.5, 1.0)
plt.xlabel("liczba najbliższych sąsiadów")
plt.ylabel("wynik CV")
plt.xticks(range(2, 22));
plt.show()
Aby uzyskać informację dla jakiego parametru przeszukiwanie dostrarczyło najlepszą wartość CV, odwołujemy się do:
print(clf_grid.best_params_, clf_grid.best_score_)
## {'n_neighbors': 15} 0.8800000000000001
Oczywiście jest to dość dyskusyjna sprawa, czy powinniśmy wybrać akurat taką wartość parametry, jeśli weźmiemy pod uwagę słupki niepewności.
Nie jest do końca prawdą, że w przypadku metody knn mamy tylko jeden parametr, którym możemy sterować. Mamy takż wpływ na tzw. odległość Mahalanobisa, czyli innymi słowy metrykę, za pomocą określamy które punkty są najbliższym sąsiadami. Odległość Mahalanobisa ma postać macierzy kowariancji
param_grid = {'n_neighbors': range(2, 32), 'metric_params': [{'V': [[2, 0], [0, 2]]}, {'V': [[2, 1], [1, 2]]}, {'V': [[2, 1.5], [1.5, 2]]}]}
clf = KNeighborsClassifier(metric="mahalanobis")
clf_grid = GridSearchCV(clf, param_grid)
clf_grid.fit(data[["x", "y"]], data.klasa)
GridSearchCV(estimator=KNeighborsClassifier(metric='mahalanobis'),
param_grid={'metric_params': [{'V': [[2, 0], [0, 2]]},
{'V': [[2, 1], [1, 2]]},
{'V': [[2, 1.5], [1.5, 2]]}],
'n_neighbors': range(2, 32)})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. KNeighborsClassifier(metric='mahalanobis',
metric_params={'V': [[2, 0], [0, 2]]}, n_neighbors=15)Dla wygody tworzymy ramkę danych z wyników
out = pd.DataFrame.from_dict(clf_grid.cv_results_)
out["mahalanobis"]=out["param_metric_params"].apply(pd.Series).astype(str)
out
## mean_fit_time std_fit_time ... rank_test_score mahalanobis
## 0 0.000908 0.000101 ... 64 [[2, 0], [0, 2]]
## 1 0.000906 0.000110 ... 74 [[2, 0], [0, 2]]
## 2 0.001008 0.000363 ... 68 [[2, 0], [0, 2]]
## 3 0.000997 0.000227 ... 52 [[2, 0], [0, 2]]
## 4 0.000828 0.000013 ... 39 [[2, 0], [0, 2]]
## .. ... ... ... ... ...
## 85 0.000847 0.000014 ... 85 [[2, 1.5], [1.5, 2]]
## 86 0.001174 0.000572 ... 90 [[2, 1.5], [1.5, 2]]
## 87 0.001069 0.000393 ... 84 [[2, 1.5], [1.5, 2]]
## 88 0.001614 0.000570 ... 89 [[2, 1.5], [1.5, 2]]
## 89 0.001232 0.000253 ... 85 [[2, 1.5], [1.5, 2]]
##
## [90 rows x 16 columns]
i rysujemy efekty, które nie są specjalnie niespodziwane
import seaborn as sns
sns.relplot(data = out, x = "param_n_neighbors", y = "mean_test_score", hue = "mahalanobis", kind="line", style="mahalanobis", markers=True);
Zastanówmy się teraz jak możemy wykorzystać zdobytą wiedzę i narzędzie w przypadku drzew decyzyjnych. Na poprzednich zajęciach mieliśmy do czynienia z raczej prostą strukturą. Dla nowych danych sytuacja wygląda trochę odmiennie:
from sklearn import tree
m1, m2 = [-1, -1], [2, 2]
S1 = [[4, 2], [2, 4]]
S2 = [[4, 2], [2, 4]]
n1, n2 = 60, 40
np.random.seed(129)
data = draw_data(n1, n2, m1, m2, S1, S2)
clf = tree.DecisionTreeClassifier()
clf.fit(data[["x", "y"]], data.klasa)
DecisionTreeClassifier()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
print(clf.tree_.n_leaves)
## 24
plt.figure(figsize = (8,8))
tree.plot_tree(clf, feature_names=["x", "y"], class_names = ["0", "1"], filled = True);
plt.show()
Jak widac, drzewo jest dośc spore - składa się aż z 32 liści. Właśnie tę własność (liczbę liści) potraktujemy jako parametr naszej siatki.
from sklearn.model_selection import GridSearchCV
param_grid = {'max_leaf_nodes': range(2, clf.tree_.n_leaves + 1)}
clf_grid = GridSearchCV(clf, param_grid)
clf_grid.fit(data[["x", "y"]], data.klasa)
GridSearchCV(estimator=DecisionTreeClassifier(),
param_grid={'max_leaf_nodes': range(2, 25)})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. DecisionTreeClassifier(max_leaf_nodes=4)
Jako porówanie wykonamy powtórne podstawienie. Żeby skorzystać z gotowych już funkcji, zapiszemy w dość dziwny sposób parametr korswalidacyjny:
n = len(data)
cv_split = [(list(range(n)), list(range(n)))]
clf_grid_res = GridSearchCV(clf, param_grid, cv=cv_split)
clf_grid_res.fit(data[["x", "y"]], data.klasa)
GridSearchCV(cv=[([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, ...],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, ...])],
estimator=DecisionTreeClassifier(),
param_grid={'max_leaf_nodes': range(2, 25)})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. DecisionTreeClassifier(max_leaf_nodes=24)
Na koniec tworzymy wykres, na którym porównujemy wyniki:
plt.errorbar(param_grid["max_leaf_nodes"], clf_grid.cv_results_["mean_test_score"], clf_grid.cv_results_["std_test_score"], fmt="o", capsize=4, label = "CV")
plt.scatter(param_grid["max_leaf_nodes"], clf_grid_res.cv_results_["mean_test_score"], c = "orange", label = "Pow. pod.")
plt.ylim(0.4, 1.05)
## (0.4, 1.05)
plt.xlabel("liczba liści")
plt.ylabel("dokładnośc klasyfikatora")
plt.legend()
Przedstawione powyżej wyniki stoją za ideą tzw. algortmu kosztu-złożoności, opisanego na wykładu 6: sprowadza się on do wybrania takiego paramteru \(\alpha\), który minimalizuje funkcję \(R_{\alpha}(T)=R(T)+\alpha|T|\), gdzie \(R(T)\) to ułamek blędów klasyfikacji na drzewie \(T\), a \(|T|\) to liczba liści w tym drzewie.
Wartości \(\alpha\) możemy otrzymać
z funkcji cost_complexity_pruning_path():
path = clf.cost_complexity_pruning_path(data[["x","y"]], data.klasa)
path
## {'ccp_alphas': array([0. , 0.00833333, 0.00894737, 0.009 , 0.00964286,
## 0.01190476, 0.01602716, 0.01792986, 0.030375 , 0.0427063 ,
## 0.18533088]), 'impurities': array([0. , 0.01666667, 0.05245614, 0.10645614, 0.12574185,
## 0.13764662, 0.18572811, 0.22158782, 0.25196282, 0.29466912,
## 0.48 ])}
Wstawiamy je następnie jako parametry do wyszukiwania po siatce: oznacza to tyle, że dla konkretnego parametru tworzone jest określone drzewo, które statystki (dokładność) następnie wyznaczamy
param_grid = {'ccp_alpha': path['ccp_alphas']}
clf_grid = GridSearchCV(clf, param_grid)
clf_grid.fit(data[["x", "y"]], data.klasa)
GridSearchCV(estimator=DecisionTreeClassifier(),
param_grid={'ccp_alpha': array([0. , 0.00833333, 0.00894737, 0.009 , 0.00964286,
0.01190476, 0.01602716, 0.01792986, 0.030375 , 0.0427063 ,
0.18533088])})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. DecisionTreeClassifier(ccp_alpha=np.float64(0.016027164685908316))
oraz tworzymy wykres
plt.plot(param_grid["ccp_alpha"], clf_grid.cv_results_["mean_test_score"], marker="o", drawstyle="steps-post", c = "#0000ff55")
plt.errorbar(param_grid["ccp_alpha"], clf_grid.cv_results_["mean_test_score"], clf_grid.cv_results_["std_test_score"], fmt="o", capsize=4, c = "#0000ff")
plt.xlabel(r"$\alpha$ - współczynnik złożoności")
plt.ylabel("dokładnośc klasyfikatora")
Pole clf_grid.best_estimator_ przechowuje najlepszy
klasyfikator, wyglądający w następujący sposób:
tree.plot_tree(clf_grid.best_estimator_, feature_names=["x", "y"], class_names = ["0", "1"], filled = True);
plt.show()
De facto, powinniśmy się posłużyć tzw. regułą 1-SE - wziąc ten klasyfikator, który znjaduje się w granicach jednego odchylenia standardowego od najlepszego, ale cechuje się wyższą wartością \(\alpha\). Jego wynik jest porównywalny z tym najlepszym, natomiast liczba liści jest na pewno mniejsza.