Chyba każdy z nas miał lub nadal ma w swoim życiu osobistego bohatera. Osobę, którą inspirował się od najmłodszych lat. Wzór do naśladowania, którego dokonania wywierały wpływ na nasze życie.
Mój „bohater” dokładnie dziś kończyłby 76 lat. Muhammad Ali, bo o nim mowa, był prawdziwym królem boksu. Jest powszechnie uważany przez ekspertów za najlepszego boksera w historii, a przydomek jaki nosił – „The Greatest” – mówi w zasadzie wszystko o jego karierze.
- Wstęp.
- Cel projektu
- Założenia dotyczące projektu
- Opis zbioru danych
- Wczytanie i przygotowanie zbioru
- Model 1
- Model 2
- Model 3
- Model 4
- Finalna weryfikacja jakości modelu
- Podsumowanie
Wstęp
Kiedy byłem nastolatkiem, plakat z podobizną Alego wisiał nad moim łóżkiem. Czytałem książki, artykuły, a kiedy w moim domu założono pierwsze „stałe łącze”, to obejrzałem chyba wszystkie ważniejsze materiały wideo na jego temat. Do dziś znam życiorys, nazwiska przeciwników, wyniki walk i historie, które za nimi stały.
Muhammad Ali w swojej karierze bokserskiej stoczył 166 walk (105 amatorskich i 61 zawodowych), spośród których aż 156 zakończyło się jego zwycięstwem. Przyszło mu walczyć w czasach świetności wagi ciężkiej, dlatego przeciwników najwyższej próby miał wielu: Joe Frazier, Sonny Liston, Floyd Patterson, czy George Foreman. Znany był również ze swoich walk poza ringiem: wykorzystał fakt bycia bokserskim celebrytą i walczył z przestępczością, analfabetyzmem i rasizmem. Największy przeciwnik stanął na jego drodze 3 lata po zakończeniu zawodowej kariery. Była nim choroba Parkinsona, z którą zmagał się aż do końca swoich dni.
Diagnozę postawiono w 1984 roku. Wtedy rozpoczęła się najdłuższa walka w jego życiu, która trwała ponad 30 lat i zmieniła go nie do poznania. Z człowieka pełnego charyzmy stał się wycofany i małomówny. Niechętnie pokazywał się publicznie. Gdy w 1996 roku zapalał znicz olimpijski w Atlancie, ciężko było uwierzyć, że jest to ten sam człowiek, który kilkanaście lat temu z taką gracją poruszał się po ringu. Przyczyną takiego stanu rzeczy jest specyfika samej choroby.
Choroba Parkinsona ma kilka stadiów, a jej objawy nasilają się z czasem. Początkowe dolegliwości, takie jak, np. drżenie rąk nie są bardzo uciążliwe. Okres pierwszych 3-5 lat nazywany jest tzw. miesiącem miodowym, gdyż chory może wieść względnie normalne życie, a jego organizm reaguje na leczenie farmakologiczne. Z czasem jednak pojawiają się kolejne objawy, takie jak zmiany osobowościowe, depresja i problemy z samodzielnym poruszaniem się.
Na dzisiejszy dzień choroba Parkinsona pozostaje nieuleczalna. Średni czas przeżycia od chwili postawienia diagnozy ok. 16 lat. Jedynie 25% badanych żyje z chorobą 20 lat (Ali przeżył 32). Dużą nadzieją na przedłużenie życia danej osoby daje wczesne rozpoznanie choroby. Im wcześniej dana osoba zostanie poddana rehabilitacji i terapii farmakologicznej, tym większe szanse na wydłużenie życia chorego. Kiedy rozpoczynają drżeć palce, wargi, czy broda jest już nieco za późno, dlatego naukowcy na całym świecie walczą, by móc rozpoznać chorobę Parkinsona jeszcze przed wystąpieniem wspomnianych objawów.
Cel projektu
Dzisiejszy projekt dotyczył będzie diagnozowania choroby Parkinsona. Jego głównym celem będzie klasyfikacja pacjentów na chorych i zdrowych na podstawie danych biomedycznych.
Uwaga: nie będę przewidywać czy dana osoba zachoruje na chorobę Parkinsona. By to zrobić, potrzebowałbym danych zawierających próbki głosu osób przed i po zachorowaniu. Niestety nie natknąłem się na tego typu dane w sieci, więc w tym projekcie problem jest nieco inaczej zdefiniowany.
Założenia dotyczące projektu
Cały zbiór podzielę na 3 podzbiory: uczący (60%), walidacyjny (20%) i testowy (20%). Ich rola będzie następująca:
- Zbiór uczący – posłuży do uczenia modelu.
- Zbiór walidacyjny – użyję go do weryfikacji hipotez dotyczących wpływu poszczególnych zmiennych i zastosowanych technik na jakość modelu.
- Zbiór testowy – finalny test mojego najlepszego modelu. Tych danych użyję tylko i wyłącznie raz – przy końcowej weryfikacji jakości modelu.
Do zbudowania najlepszego modelu użyję trzech algorytmów, których do tej pory na blogu nie było: SVM, PCA i ISOMAP. SVM będzie podstawowym klasyfikatorem, natomiast PCA lub ISOMAP będzie algorytmem, którego użyję do redukcji wymiarów przestrzeni, w jakiej będzie operować SVM.
O ile na co dzień z należytą starannością analizuję dane przed ich użyciem, to dziś pominę ten krok. W tym projekcie liczy się tylko walka o końcowy wynik. Parametrem jakości modelu będzie Accuracy.
Opis zbioru danych
Do uczenia modelu użyję danych pobranych ze strony archive.ics.uci.edu. Zbiór zawiera dane dotyczące pomiarów głosu 31 osób, z których 23 cierpiały na chorobę Parkinsona. Od każdej z osób średnio sześciokrotnie została pobrana próbka głosu w postaci nagrania. Sumarycznie zbiór zawiera 195 obserwacji.
Opis zmiennych:
- name – identyfikator pacjenta,
- MDVP:Fo(Hz) – średnia częstotliwość podstawowa głosu,
- MDVP:Fhi(Hz) – maksymalna częstotliwość podstawowa głosu,
- MDVP:Flo(Hz) – minimalna częstotliwość podstawowa głosu,
- MDVP:Jitter(%), MDVP:Jitter(Abs), MDVP:RAP, MDVP:PPQ, Jitter:DDP – zmienne opisujące zmienność częstotliwości
- MDVP:Shimmer, MDVP:Shimmer(dB), Shimmer:APQ3, Shimmer:APQ5, MDVP:APQ, Shimmer:DDA – zmienne opisujące zmienność w amplitudzie głosu,
- NHR, HNR – zmienne opisujące stosunek szumu w głosie,
- RPDE, D2 – miary złożoności sygnału głosowego,
- DFA – eksponenta skalowania fluktuacji sygnału głosowego,
- spread1, spread2, PPE, – miary zmienności podstawowej częstotliwości głosu,
- status – stan pacjenta, gdzie: 1 – chory, 0 – zdrowy.
Zmienną celu będzie oczywiście zmienna „status”. Niestety nie jestem ekspertem dziedzinowym i nie wiem, co dokładnie oznaczają pozostałe zmienne i jaki może być ich wpływ na wyniki. Dobra wiadomość jest taka, że wszystkie zmienne opisujące (prócz identyfikatora pacjenta, którego i tak nie użyję), to zmienne typu numerycznego. Powinno to nieco ułatwić zadanie i zwiększy liczbę algorytmów, które będę mógł wykorzystać.
Wczytanie i przygotowanie zbioru
Zbiór wczytuję tak jak zwykły plik csv, rozdzielony przecinkami. Na tym etapie muszę zrobić 3 rzeczy:
- Zamienić typ zmiennej „status” na kategoryczny.
- Usunąć zmienną „name” – byłaby ona dodatkowym ułatwieniem i zakładam, że wpłynęłaby pozytywnie na wynik. Zbudowałbym z jej użyciem model, który znakomicie rozpoznaje chorobę, ale tylko dla grupy 31. Byłby on więc mało wartościowy.
- Sprawdzić, czy w zbiorze są jakieś brakujące wartości.
- Podzielić zbiór wejściowy na zbiory: uczący, walidacyjny i testowy.
# 1. Change the 'status' column to categorical df.status = pd.Categorical(df.status) # 2. Drop 'name' column df.drop(axis=1, labels='name', inplace=True) # copy 'y' variable and remove it from datasets y = df.status.copy() df.drop(axis=1, labels='status', inplace=True) # 3. Check if there are any nulls print(df.isnull().any()) # 4. Splitting dataset into training and test datasets x_tr, x_te, y_tr, y_te = train_test_split(df, y, random_state=17012018, test_size=0.4) x_te, x_va, y_te, y_va = train_test_split(x_te, y_te, random_state=17012018, test_size=0.5)
Model 1
Model bazowy, bez żadnych parametrów, na „surowym” zbiorze.
model = SVC() model.fit(x_tr, y_tr) # score validation data print(round(model.score(x_va, y_va),4))
Wynik: 0.7949. Pierwszy rezultat i już jest całkiem nieźle. Jak na „surowy” model, to można powiedzieć, że startuję z „wysokiego C” 🙂
Model 2
Pierwszą rzeczą, który muszę poprawić, jest dobór wag obu klas. Zbiór jest mocno niezbalansowany: zawiera 147 obserwacji pozytywnych (1) i jedynie 48 obserwacji negatywnych (0). Definiuję wartości parametrów obu klas. Będą nimi: 1 (dla klasy „1”) i stosunek liczby obserwacji klasy „1” do liczby obserwacji klasy „0”. Wprowadzam tę drobną korektę.
# Number of "1" in y num_of_PD = sum(y_tr) # Number of "0" in y num_of_no_PD = y_tr.shape[0]-sum(y_tr) # Setting weights weight_PD = 1 weight_no_PD = num_of_PD/num_of_no_PD model = SVC(class_weight={0:weight_no_PD, 1:weight_PD }) model.fit(x_tr, y_tr) print(round(model.score(x_va, y_va),4))
Wynik: 0.8718. Jest znacząca poprawa. Zgodnie z zasadą pareto, to chyba było 20% procent działań przynoszących 80% rezultatów 🙂
Model 3
W trzecim modelu wprowadzę dwie poprawki:
- Znormalizuję zmienne numeryczne. Powód jest oczywisty: wartości średnie niektórych zmiennych są na poziomie 100+, a innych <1. Tak duża różnica nie wpływa dobrze na jakość modelu. Przetestuję wpływ kilku metod normalizacji dostępnych w SKLearn: Normalizer, MaxAbsScaler, KernelCenterer, StandardScaler.
- Zweryfikuję, przy jakich parametrach algorytmu SVM model daje najwyższy wynik.
By nieco zautomatyzować proces testowania różnych parametrów, buduję funkcję, którą będę wywoływać iteracyjnie:
def lets_make_SVC(_gamma, _c, _x_tr, _y_tr, _x_te, _y_te): model = SVC(gamma=_gamma, C=_c, class_weight={0:weight_no_PD, 1:weight_PD }) model.fit(_x_tr, _y_tr) return model.score(_x_te, _y_te)
Dodatkowo definiuję funkcję do poszukiwania najlepszych parametró algorytmu SVM:
def tune_model_hyperparameters(_x_tr, _y_tr, _x_te, _y_te): best_result = 0.0 best_c = 0.05 best_gamma = 0.001 for cc in np.arange(0.05,2.0,0.05): for gg in np.arange(0.001,0.1,0.001): result = lets_make_SVC(gg, cc, _x_tr, _y_tr, _x_te, _y_te) if result > best_result: best_result = result best_c = cc best_gamma = gg return best_result , best_c, best_gamma
Testy kolejnych podejść do normalizacji danych:
### Normalizer() ### nlz = Normalizer() nlz.fit(x_tr) coll = x_tr.columns ## Training dataset nlz_x_tr = pd.DataFrame(nlz.transform(x_tr)) nlz_x_tr.columns = coll ## Validation dataset nlz_x_va = pd.DataFrame(nlz.transform(x_va)) nlz_x_va.columns = coll # Result of model with normalized data best_result = 0 best_c = 0 best_gamma = 0 best_kernel = '' best_result, best_c, best_gamma, best_kernel = tune_model_hyperparameters(nlz_x_tr, y_tr, nlz_x_va, y_va) print(round(best_result,4)) ### MaxAbsScaler() ### mas = MaxAbsScaler() mas.fit(x_tr) ## Training dataset mas_x_tr = pd.DataFrame(mas.transform(x_tr)) mas_x_tr.columns = coll ## Validation dataset mas_x_va = pd.DataFrame(mas.transform(x_va)) mas_x_va.columns = coll # Result of model with normalized data best_result = 0 best_c = 0 best_gamma = 0 best_kernel = '' best_result, best_c, best_gamma, best_kernel = tune_model_hyperparameters(mas_x_tr, y_tr, mas_x_va, y_va) print(round(best_result,4)) ### KernelCenterer() ### kc = KernelCenterer() kc.fit(x_tr) ## training dataset kc_x_tr = pd.DataFrame(kc.transform(x_tr)) kc_x_tr.columns = coll ## test dataset kc_x_va = pd.DataFrame(kc.transform(x_va)) kc_x_va.columns = coll # Result of model with normalized data best_result = 0 best_c = 0 best_gamma = 0 best_kernel = '' best_result, best_c, best_gamma, best_kernel = tune_model_hyperparameters(kc_x_tr, y_tr, kc_x_va, y_va) print(round(best_result,4)) # 0.8974 ### StandardScaler() ### sc = StandardScaler() sc.fit(x_tr) ## training dataset sc_x_tr = pd.DataFrame(sc.transform(x_tr)) sc_x_tr.columns = coll ## validation dataset sc_x_va = pd.DataFrame(sc.transform(x_va)) sc_x_va.columns = coll # Result of model with normalized data best_result = 0 best_c = 0 best_gamma = 0 best_result, best_c, best_gamma = tune_model_hyperparameters(sc_x_tr, y_tr, sc_x_va, y_va) print(round(best_result,4))
Rezultaty dla kolejnych sposobów normalizacji danych wyglądają następująco:
- Normalizer – 0.7692 – brak poprawy najlepszego wyniku.
- MaxAbsScaler – 0.8974 – jest poprawa.
- KernelCenterer – 0.8974 – jest poprawa.
- StandardScaler – 0.9487 – jest duża poprawa.
Model daje najlepsze rezultaty dla następujących parametrów SVM:
- C = 0.05
- gamma = 0.085
- kernel = ‘rbf’
Wynik: 0.9487. StandardScaler i dobrane parametry poprawiły najlepszy wynik przeszło 0.07.
Model 4
Mam jeszcze dwa asy w rękawie, które chciałbym użyć: ISOMAP i PCA. Z doświadczenia wiem, że mogą w znacznym stopniu wpłynąć na wynik. Przetestuję zatem oba, by zweryfikować, który z nich daje lepsze wyniki.
Definiuję zatem dwie funkcje, które użyję podczas iteracyjnego budowania i testowania modeli:
# PCA def lets_make_PCA(_n, _x_tr, _x_te): pca = PCA(n_components=_n) pca.fit(_x_tr) pca_x_tr = pd.DataFrame(pca.transform(_x_tr)) pca_x_te = pd.DataFrame(pca.transform(_x_te)) return pca_x_tr, pca_x_te # ISOMAP def lets_make_ISOMAP(_n_n, _n_c, _x_tr, _x_va): iso = Isomap(n_neighbors=_n_n, n_components=_n_c) iso.fit(_x_tr) iso_x_tr = pd.DataFrame(iso.transform(_x_tr)) iso_x_te = pd.DataFrame(iso.transform(_x_va)) return iso_x_tr, iso_x_te
Dla obu algorytmów muszę znaleźć optymalne parametry. Muszę zatem skorzystać z iteracyjnego podejścia do ich poszukiwania.
# PCA best_result = 0.0 best_x = 4 for x in range(4,15): print(x-3) tr, va = lets_make_PCA(x, sc_x_tr , sc_x_va) res, best_c, best_gamma = tune_model_hyperparameters(tr, y_tr, va, y_va) if res > best_result: best_result = res print(best_result) best_x = x print(best_x) print(best_result) best_result = 0.0 best_n_n = 2 best_n_c = 4 # ISOMAP for n_n in range(2,5): print('n_n = ' + str(n_n)) for n_c in range(4,7): print('n_c = ' + str(n_c)) tr, va = lets_make_ISOMAP(n_n, n_c, sc_x_tr, sc_x_va) res, best_c, best_gamma = tune_model_hyperparameters(tr, y_tr, va, y_va) if res > best_result: best_result = res best_n_n = n_n best_n_c = n_c print('Best n_n parameter: ' + str(best_n_n)) print('Best n_c parameter: ' + str(best_n_c)) print('Best gamma: ' + str(best_gamma)) print('Best c: ' + str(best_c)) print('Best score: ' + str(round(best_result,4)))
PCA: 0.9231.
ISOMAP: 0.9744.
Po zastosowaniu ISOMAP zmieniły się parametry najlepszego modelu SVM. Teraz wyglądają one następująco:
- C = 0.2
- gamma = 0.063
- kernel = ‘rbf’
Wynik: 0.9744. PCA nie poprawił wyniku osiągniętego przez poprzedni model. Poprawa nastąpiła dopiero po użyciu ISOMAP. Wyśróbował on wynik do zadowalającego poziomu. Kończę więc fazę budowania i walidowania modelu. Przechodzę do finalnego testu.
Finalna weryfikacja jakości modelu
By zweryfikować jakość modelu, przetestuję go na próbie, które wcześniej „nie widział”. Użyje do tego wcześniej zbudowanego zbioru testowego.
sc = StandardScaler() sc.fit(x_tr) # Training dataset sc_x_tr = pd.DataFrame(sc.transform(x_tr)) sc_x_tr.columns = coll # Test dataset sc_x_te = pd.DataFrame(sc.transform(x_te)) sc_x_te.columns = coll # Final model tr, te = lets_make_ISOMAP(2,4, sc_x_tr, sc_x_te) print(round(lets_make_SVC(0.063, 0.2, tr, y_tr, te, y_te),4))
Końcowy wynik: 0.8974. Wynik oceniam jako więcej niż zadowalający. Pozwoliłem sobie podejrzeć wyniki, jakie dawał pierwszy model dla zbioru testowego. Było to dokładnie 0.7949. Dzięki trzem prostym krokom udało mi się poprawić początkowy rezultat o przeszło 10%. Ciągle nie jest to różnica, jaką uzyskałem na zbiorze walidacyjnym. Należy jednak pamiętać, że bazując na wynikach uzyskanych na zbiorze walidacyjnym wprowadzałem kolejne zmiany w modelu. Siłą rzeczy, wynik uzyskany na nim musiał być wyższy od tego uzyskanego na zbiorze testowym.
Podsumowanie
Niniejszy projekt nie wyczerpuje tematu. Pomimo iż wynik Accuracy na poziomie 0.8974 uznaję za bardzo dobry, to mam pełną świadomość, że można go poprawić. Z pełną premedytacją zawęziłem arsenał dostępnych algorytmów do trzech, by użyć tych, które do tej pory nie były opisywane na łamach mojego bloga. Gdybym zawsze celował w najwyższy wynik bez względu na koszty to pewnie w jakichś 75% procentach przypadków finalnym wyborem byłby XGBoost. Wtedy zanudziłbym Was na śmierć, a tego bym bardzo nie chciał 😉
–
Dziękuję Ci za dobrnięcie do samego końca. Jeśli masz jakiekolwiek uwagi, to proszę, podziel się nimi w komentarzu na social media, lub skontaktuj się ze mną.
Wszystkie pliki utworzone przy projekcie można znaleźć na moim GitHub’ie.
photo: theundefeated.com
PODOBAŁ CI SIĘ TEN ARTYKUŁ?
Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach.
Dodatkowo w prezencie wyślę Ci bezpłatny poradnik :-)
Dodaj komentarz