shap.summary_plot, czyli kilka kolejnych słów o zaglądaniu do modelu

Ostatnio w komentarzach coraz częściej spotykam się z opinią, że nie sztuką jest stworzyć model, ale sztuką jest (powinno?) interpretacja uzyskanych wyników w kontekście problemu, który rozwiązujemy. Z drugiej strony, są też całe kursy od niesamowitych wymiataczy (np. course.fast.ai), których motto brzmi mniej więcej – zacznij jak najszybciej, a później będziemy się wgłębiać w detale.

Nie mam silnej opinii, jeśli chodzi o to, które podejście jest najlepsze. Natomiast to, co jest dla mnie najważniejsze to możliwość zbudowania jak najkrótszego cyklu iteracyjnego w rozwiązywaniu problemu. Aby więc sprawnie zbudować taki cykl, staram się czerpać jak najwięcej ze sprawnych bibliotek modelujących i wizualizujących oraz z tzw. explainerów (wyjaśniaczy?), które spinają (często) abstrakcyjne wyniki z konkretnymi elementami zbiorów danych. Od razu wtedy widzę czy poruszam się zgodnie z intuicjami albo, czy dzieje się coś niebywałego (jeśli w ogóle mam jakieś intuicje na temat).

Wydaje mi się, że takich wyjaśniaczy nigdy za wiele. Postanowiłem więc powrócić jeszcze raz do pythonowego modułu shap, którego miałem już przyjemność opisywać w artykule pt. Co to jest SHapley Additive exPlanations (SHAP)? Tym razem pokażę jak przy jego pomocy, można pójść o krok dalej niż poprzednio i zobrazować sobie ogólnie rozstrzał ważności wartości cechy względem naszego celu. Brzmi może ciut niejasno, ale pod koniec artykułu powinno już się wszystko wyklarować.

Przykład

Najlepiej będzie, jak zaczniemy od przykładu. Użyję znanego już nam zbioru breast cancer i znanych hiperparametrów do wytrenowania drzewa i lasu losowego. Obydwa te modele wytrenuję na tym samym zbiorze treningowym, który powstał poprzez pozostawienie 3/4 oryginalnej paczki danych. Zobaczmy, jak to wygląda:

# Kod 1

import pandas as pd
from sklearn.datasets import load_breast_cancer

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier

from sklearn.model_selection import train_test_split
 
breast_cancer = load_breast_cancer()
X = pd.DataFrame(breast_cancer["data"], 
                 columns = breast_cancer["feature_names"])
y = pd.Series(breast_cancer["target"])
 
hyperparameters = {"criterion": 'gini', 
                   "max_depth": 5, 
                   "min_samples_leaf": 1, 
                   "min_samples_split": 2, 
                   "random_state": 42}
 
estimator_tree = DecisionTreeClassifier(random_state = hyperparameters["random_state"], 
                                        criterion = hyperparameters["criterion"], 
                                        max_depth = hyperparameters["max_depth"], 
                                        min_samples_leaf = hyperparameters["min_samples_leaf"], 
                                        min_samples_split = hyperparameters["min_samples_split"])

estimator_forest = RandomForestClassifier(random_state = hyperparameters["random_state"], 
                                          criterion = hyperparameters["criterion"], 
                                          max_depth = hyperparameters["max_depth"], 
                                          min_samples_leaf = hyperparameters["min_samples_leaf"], 
                                          min_samples_split = hyperparameters["min_samples_split"],
                                          n_estimators = 100)

X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                                    test_size = 0.25, 
                                                    random_state = 42)
estimator_tree.fit(X = X_train, y = y_train)
estimator_forest.fit(X = X_train, y = y_train)

Nie było to nic odkrywczego. Skoro mamy wytrenowane dwa modele, możemy sprawdzić, jak wyglądają interakcje pomiędzy wartościami cech a predykcją:

# Kod 2

import shap
explainer_tree = shap.TreeExplainer(estimator_tree)
shap_values_tree = explainer_tree.shap_values(X_test)
shap.summary_plot(shap_values_tree[1], X_test)

Jak widzimy, nie wydarzyło się tutaj nic kosmicznego. Najpierw tworzymy obiekt, później wyliczamy predykcje, a na końcu tworzymy wykres. W sumie to można by te trzy linijki połączyć w jedną, ale mogłoby to trochę zepsuć czytelność przykładu. W każdym razie, dla przykładu z drzewem decyzyjnym otrzymamy następujący wykres:

Wpływ wartości cechy na wynik modelu - drzewo
Wpływ wartości cechy na wynik modelu – drzewo

Powyższy wykres powinniśmy czytać „od góry”. W ten sposób mamy szeregowane cechy, które mają najsilniejszy wpływ na model. Okej, to potrafimy już robić, korzystając z np. feature importance. Przyjrzyjmy się teraz poziomym liniom, które są przyporządkowane każdej cesze. Linie te to osie, na których umieszczone są wartości (w postaci kropek) wszystkich obserwacji. Im dana kropka bardziej na prawo, tym bardziej przechyla szalę klasyfikacji na 1, im bardziej na lewo, tym bardziej ukierunkowuje klasyfikację na 0. Okej, mamy już rozpracowane dwa wymiary (od góry i prawo-lewo). Trzeci wymiar na tym wykresie to kolor danej obserwacji. Im bardziej niebieski, tym dana wartość była niższa, im bardziej czerwony tym dana wartość była wyższa. Oczywiście, żeby to miało sens, kolory te są wyskalowane dla każdej cechy osobno.

Co więc możemy wyczytać z powyższego? Cecha „mean concave points” jako jedyna potrafi mocno przechylić predykcję w stronę 1. Widzimy to pod postacią niebieskich kropek (które zlały się w linię) w prawym górnym rogu wykresu. Widzimy, że tak się dzieje, jeśli występują tam wartości ekstremalnie małe. Jeśli dana obserwacja ma tam wyższe wartości, to praktycznie od razu ląduje po lewej stronie wykresu, czyli wtedy „przechyla” predykcję na 0. Ciekawie wygląda też sytuacja przy „smoothness error” i „worst smoothness”. Obydwie te cechy działają raczej na „+” w modelu, oprócz kilku obserwacji, które bardzo mocno przechyliły model na „-„.

No dobra, a co uzyskamy, jeśli w podobny sposób przeanalizujemy las losowy? Spójrzmy:

Wpływ wartości cechy na wynik modelu - las
Wpływ wartości cechy na wynik modelu – las

I tutaj właśnie uzyskaliśmy spory kawał informacji na temat zachowania naszego modelu. Każdy, kto zna się na rzeczy (w związku z onkologią piersi), od razu będzie mógł stwierdzić, czy ten model może mieć sens, czy wrzucamy go do kosza. I myślę (mimo iż onkologiem nie jestem), że taki model może działać. Widzimy to np. po tym, że wartości górnych cech całkiem fajnie się odseparowały. W pierwszej cesze: im niższa wartość, tym bardziej nasz model skłania się ku 1.

Co ciekawe, widzimy też, że według naszego drugiego modelu, badanie zostało zaprojektowane w taki sposób, że za każdym razem, gdy zmierzona wartość jest wyższa, to skłania się ku tej samej stronie. Z dokumentacji zbioru danych wiemy, że klasa 1 oznacza „niegroźny”. Patrząc więc na powyższy wykres, widzimy, że zasadniczo, jeśli pojawiają się duże wartości w pomiarach to ryzyko „złośliwego” nowotworu jest większe. I jest to zgodne z tzw. chłopskim rozumem, bo raczej częściej badamy się pod względem oznak choroby (pojawienie się jakichś wartości w pomiarze), niż oznak zdrowia. Ale co ja tam wiem o chorobach ;).

Podsumowanie

Jeśli tylko moduł/pakiet modelujący, którego używasz, tworzy modele, które da się wrzucić w „wyjaśniacze” (eli5, pdp, shap, auditor) to warto je wrzucać już od samego początku modelowania. Dzięki temu czasem od razu można wyłapać przydatne informacje i skrócić sobie czas potrzebny do rozwiązania danego problemu. Można też od razu konsultować się z ekspertami w danej tematyce i sprawdzać, czy nasz model nie tworzy śmieci. A że wspomniane wcześniej narzędzia są bardzo proste w obsłudze, nie wydłużą nam one w praktyce całego procesu.

Pełny kod użyty w artykule znajduje się tutaj.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *