Makine Öğrenimi Felsefesi-6(Part II)

Volkan Yurtseven
31 min readJun 4, 2022

--

Uçtan uca tam örnek — Classification, Part II

Photo by Helio Dilolwa on Unsplash

Part I için buraya tıklayınız.

NOT: Benim python ayarlarım startup olarak kendi paketimdeki magic ve extension fonksiyonlarımı otomatik yüklüyor. Ayrıca kod kalabalıklığı olmaması ve reusability adına, yine kendi paketimdeki modülleri de kullanıyorum. Nelerin yüklendiğini görmek ve sizde de benzer çıktıların oluşmasını sağlamak için buraya bakınız. Burada extension methods ve magic functions ile Your own utility package maddelerine bakınız. Bunlar çok faydalı özellikler olup, aynı zamanda bu notebookun hatasız çalışması için de gereklidir. Benim anlattığım şekilde kurmayı başaramazsanız bile, ilgili kodları alıp bu notebookun başına yapıştırsanız da olur.

Tüm kodların ve açıklamaların bulunduğu notebooka github veya nbviewer üzerinden ulaşabilirsiniz.

İçeriğe şöyle bi göz atacak olursak;

Modelleme öncesi

Öncelikle gerekli kütüphaneleri import edelim. Modellemeyle ilgili kütüphaneleri ise ileride yavaş yavaş dahil edeceğiz.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as ss
#kendi paketimi de yüklüyorum
from mypyext import dataanalysis as da
from mypyext import ml

Öncelikle yeni bir notebook açtığımız için datamızı baştan okuyalım ve train/test ayrımını baştan yapalım.

from sklearn.model_selection import train_test_splitdfheart=pd.read_csv("https://raw.githubusercontent.com/VolkiTheDreamer/dataset/master/Classification/Heart.csv")
del dfheart["Unnamed: 0"]
X=dfheart.iloc[:,:-1]
y=dfheart.iloc[:,-1]
X_train, X_test, y_train, y_test=train_test_split(X,y,test_size=0.2, stratify=dfheart.Thal.fillna("NA"), random_state=42)
target=["AHD"]
nums=["Age","RestBP","Chol","MaxHR","Oldpeak","Ca"]
cats=list(dfheart.columns).removeItemsFromList_(nums+target,False)
ords=["ChestPain","RestECG"]
noms=cats.removeItemsFromList_(ords,False)

Pipeline ve ColumnTransformers

Genel Bilgiler ve Standart Pipeline

EDA aşamasında veya model kurma aşamasında yaptığımız çeşitli preprocessing ve feature engineering işlemleri vardı. Bunları train/test ayrımından sonra train set üzerinde yapıyorsak aynı işlemleri bir de test verisi için de yapmak gerekir, ki bu aynı kodların tekrar yazılması demektir. Üstelik eğitim setindeki çeşitli bilgilerin bir değişkende depolanmasını da gerektirir, ki bu değişkenleri test seti üzerinde de kullanalım. Özetle uzun iş. Bunun için Pipeline diye bir sınıf/obje geliştirilmiş. Uygulanacak işlemler belli sıralar halinde yazılır, bir nevi bir fonksiyon yazılır, ve bu fonksiyon hem train data setinde hem de test seti üzerinde uygulanır. Tabiki train setinde önce fit sonra transform yapılırken, test üzerinde sadece transform uygulanır.

Bir veri seti için birden fazla pipeline kurgulanabileceği gibi, farklı pipelinelar birleşip tek bir pipeline da oluşturabilir. Böyle durumlarda genelde arada bir yerde ColumnTransformer nesnesi de kullanılır, ki bunu birazdan göreceğiz.

Şimdi bunlar yakından bakalım:

Pipeline: Ardışık işlemleri pratik bir şekilde yapmak için kullanılır.

  • İçine aldığı tüm işlemleri arka arkaya çalıştırır.
  • İlk transformasyon sonrasında kolon isimleri kaybolur, çünkü artık elimizde bir numpy array vardır, o yüzden sonraki aşamalarda ilgili kolonların indeks numarasıyla gitmek gerekecektir. Özellikle alt pipelinelerda bu durum sorun yaratabilmektedir. Burada dikkat edilecek nokta şu: ColumnTransformerlara parça parça gönderildikleri için indeks numaralarını bu parçalar içindeki konumuna göre belirlemek olacaktır, ana dataframedeki pozisyonuna göre değil.
  • Son aşama fit(estimator, yani classifier veya regressor), önceki aşamalar fit_transform(preprocessor, feature selector v.s) mantığıyla çalışan işlemler olmalıdır.
  • Bazı durumlarda birden fazla alt pipeline ana pipeline içine konulabilir.
  • Pipelinlar sadece X’ler üzerinde çalışmalı, target için değil. Target için de bir işlem gerekliyse bunu pipeline dışında yapmak gerekir. Örneğin LabelEncoder. Tabiki train setinde fit_transform, test setinde sadece transform olacak şekilde.
  • Bir işlem ve bu işleme ait ayırdedici bir isim olmak üzere iki elemanlı tuplelar şeklinde tanımlanır
  • Kullanım şekli aşağıdaki gibidir.
mypipe = Pipeline([
('isim', Transformer1),
('isim', Transformer2),
('isim', Estimator()),
])

#örnek
mypipe = Pipeline([
('imp', SimpleImputer(strategy="median")),
('scl', StandartScaler()),
('clf', LogisticRegression(C=1, max_iter=1000)),
])

ColumTransformer: Farklı kolonlar için farklı işlemler yapılmak istendiğinde kullanılır.

  • İçine aldığı işlemleri paralel olarak çalıştırır. Ancak sonuçlar, işlem sırasına göre yanyana konur. Dolayısıyla bir columntransformerı takip eden başka bir işlem varsa, ondan sonraki kolon numaralarını verirken yeni sırasına göre vermek gerekir.
  • İçine aldığı sınıfların fit_transform metoduna sahip olan sınıflar olması gerekir. Custom bir fonksiyonu çağırmak isterseniz FunctionTransformer sınıfından destek alınabilmektedir. Daha ileri durumlarda ise class tanımı yapılabilir. Mesela outlier handling veya log transformasyon yapacaksanız bunlardan birine ihtiyacınız olacaktır. Peki hangisi ne zaman? Az aşağıda bunun cevabını bulacaksınız. Bu ikisi için de şuradan detay bilgi alabilirsiniz.
  • Birden fazla ColumnTransform kullanılabilir ve her birinin sonucu ayrı ayrı görülebilir(Pipeline’ı fit etmeden, sadece columtransformerı fit_transform ederek. Bu da debugging imkanı sunar)
  • ColumnTransfomer içine bir alt pipeline verilebilir veya tek adımlık birşeyse direkt transformerın kendisi(Ör:OneHotEncoder) direkt verilebilir, veyahut da custom bir işlem yapılacaksa, FunctionTransformerla wrap edilmiş bir fonksiyon verilebilir.
  • İşlemin adı, transformerın/alt pipelineın/custom_fonksiyonun kendisi ve hangi kolonları göndereceğiniz şeklinde 3 elemanlı bir tuple listesi alır(*).
  • Kullanım şekli aşağıdaki gibidir.
coltrans = ColumnTransformer([
('isim', Transformer1/Pipeline/Fonksiyon, kolonad/kolonindeks listesi),
('isim', Transformer1/Pipeline/Fonksiyon, kolonad/kolonindeks listesi),
('isim', Transformer1/Pipeline/Fonksiyon, kolonad/kolonindeks listesi)
])

#örnek
coltrans = ColumnTransformer([
('nominals', FunctionTransformer(mycustomFunc), noms), #custom function ve kolon listesi
('ordinals', OrdinalEncoder(categories=[[Kötü', 'Orta', 'İyi']]), [3]), #standart bir transformer ve kolon indeks listesi
('numerics', num_pipe, nums) #pipeline ve kolon listesi
])

Tüm yapı yine nihai olarak bir Gridsearch içine konabilir. Daha önce Gridsearch içine bir estimator konarken, şimdi Pipeline konacak, tek farkı bu.

* kolon bilgisi olarak bunlara ek olarak make_columns_selector ile belli tiplerdeki kolonları da gönderebiliyoruz.

Şimdi kendi pipeline’ımızı kuralım ama öncesinde yardımcı sınıf ve fonksiyonlarımızı yazalım.

Burda outlierHandler’ı neden şöyle yaratmadığımızı merak etmiş olabilirsiniz.

def outlierHandler(X):
Q1 = X["RestBP"].quantile(0.25)
Q3 = X["RestBP"].quantile(0.75)
IQR = Q3-Q1
topRestBP=(Q3 + 1.5 * IQR)
bottomRestBP=(Q1 - 1.5 * IQR)
X["RestBP"]=np.where(X["RestBP"]>topRestBP,topRestBP,X["RestBP"])
X["RestBP"]=np.where(X["RestBP"]<bottomRestBP,bottomRestBP,X["RestBP"])
return X

Tıpkı numericImputer’da yaptığımız gibi yani, ki aslında numericImputer’ın commentinde bunun hatalı olduğunu belirtmiştim, aynı hatayı outlierHandleda yapmıyoruz. Bunun sebebi meşhur data leakage konusu. Bu şekilde yaparsak hem train hem test datası için IQR hesabı ayrı ayrı yapılır, yani fit edilir, halbuki biz hep ne diyoruz; train datası için fit_transform, test datası için ise sadece transform yapılır. O yüzden ayrı bir sınıf yaptık, train datası burda fit_transform edilirken, test ise sadece transform edilmiş olacak. Ne demek bu, yani test datasının transform olması ne demek? Kendisine iletilen bir bilgiyi kullanması demek, peki nedir bu bilgi, yani neyle transform edecek? Train seti üzerinden hesaplanan ve classın(aslında bu classtan ürettiğimiz objenin) üzerinde taşıdığı top ve bottom bilgisiyle. Bunu class kullanmadan yapamazdık, classın(objenin) bu bilgiyi kendi üzerinde taşıyarak getirmesi lazım.

numericImputer fonksiyonunda bu hatayı bilerek yaptık ki hem FunctionTransformer kullanımını gösterelim hem de class inheritance ile arasındaki farkı. Çok yıkıcı bi etkisi olmayacağı ve burda amacımız en ideal modeli kurmak olmadığı için bu hataya katlanabiliriz.

Özetle, eğer işleme soktuğumuz verinin üstünde daha sonra kullanılacak üzere bir hesaplama yapacaksak burda class kullanırız, aksi halde function yazmamız yeterlidir.

Bu arada yazdığımız classın doğru sonuç üretip üretmediğini test edelim.

#RestBP ve Chol'ün indekisine bakalım, 1 ve 2
nums
Out[9]:['Age', 'RestBP', 'Chol', 'MaxHR', 'Oldpeak', 'Ca']

OutlierHandler’ımız kontrol edleim

ouh=OutlierHandler(featureindices=[1,2]) #RestBP ve Cholün indeksleri
kontroldata=ouh.fit_transform(X_train[nums].values)
ouh.top,ouh.bottom
Out[10]:(array([170. , 377.875]), array([ 90. , 110.875]))

RestBP’nin top değeri(maxla karışmasın, IQR yöntemine göre hesaplanan top değeri) 170, Chol’ün 377.875miş. Diğer iki rakam da bottom değerleri.

#şimdi bi max değerleri kontrol edelim
X_train["RestBP"].max(), kontroldata[:,1].max()
Out[11]:(192, 170.0)

Evet orjinal sette 192 olan bir değer top değer olan 170 ile replace edilmiş.

#hangi elemanlar değişmiş görelim
np.argwhere((X_train.RestBP.values==kontroldata[:,1])==False)
np.argwhere((X_train.Chol.values==kontroldata[:,2])==False)
Out[12]:
array([[ 27],
[ 44],
[ 45],
[110],
[111],
[220],
[240]], dtype=int64)
array([[ 30],
[ 48],
[ 78],
[198],
[219]], dtype=int64)

Şimdi pipelineımızı kurmaya devam edelim. Buradan itibaren çok sayıda senaryoya bakacağız ve karşılaştırmalar yapacağımız ve tutarlı sonuçlar almak istediğimiz için gerekli her yerde random_state parametresini kullanmak gerekecek.

Pipeline’dakilerin ardışık/seri, ColumTransformdakilerin paralel resmedildiğine dikkat ettiniz mi? Bu akış gerçekten önemli. Eğer bu ayrımı iyi anlayamazsanız, hatalarla debelenir durursunuz.

çıktı olarak oluşan array ve remainder parametresi

remainder ile, Columtransformerda işlem yapılmasını istediğimiz kolonlar dışındaki kolonlara ne yapılması gerektiğini söyleriz. Default değeri “drop” olup, bunların nihai array’de yer almaması gerektiğin söylemiş olurken, “passthrough” dersek de en sağa eklenmesini söylemiş oluruz.

Şöyle ki yeni oluşan array’imizdeki kolonların sıralaması şöyledir:

  • en solda noms
  • onun hemen sağında ords
  • onun da sağında nums
  • varsa diğer kolonlar da en sağa gider, bizde bi tane ordinal feature(RestECG) vardı işleme tabi tutmadığımız, o en sağa gidecek.

Şimdi, bakalım nasıl bir çıktımız var. Önce coltransı fit_transform edip görelim.

ct=coltrans.fit_transform(X_train)
ct[0]
Out[14]:array([ 0. , 0. , 0. , 0. , 0. ,
1. , 0. , 2. , 1.33663279, 1.19595024,
0.51055787, 0.95924191, -0.87266506, 0.31294034, 0. ])

Bunların kolon isimlerini burada belirtilen yöntemlerle kolaylıkla elde edebilirdik ancak custom fonksiyon kullandığımız için bu şekilde yapamıyoruz, onun yerine benim ml modülümdeki aşağıdaki fonksiyonu kullanabiliriz.

ml.get_feature_names_from_columntransformer(coltrans)Out[15]:
['OHE_Sex_1',
'OHE_Fbs_1',
'OHE_ExAng_1',
'OHE_Slope_2',
'OHE_Slope_3',
'OHE_Thal_normal',
'OHE_Thal_reversable',
'ChestPain',
'Age',
'RestBP',
'Chol',
'MaxHR',
'Oldpeak',
'Ca',
'RestECG']

Coltransın her bir aşamasında ne elde ettiğimizi de görebiliriz. İlgili satırın altındakileri commentleyip, sadece coltransı fit_transform yapmak yeterlidir.

Ana pipeline içindeki son satırdaki estimator kodunu commentleyip pipeline için de fit_transform yapılabilir, böylece feature selection sonucunu da görebiliriz. Aşağıda buna ait sonuçlar var.

pipe = Pipeline(steps=[('ct', coltrans), #fit_trasnformlu bir işlem
('fs', SelectKBest(score_func=mutual_info_classif,k=10)), #fit_transformlu bir işlem
# ('clf', LogisticRegression(C=1,max_iter=1000,random_state=42)) #sadece fitli bir işlem en sonda yer alıyor
])

pipe.fit_transform(X_train,y_train)[0]
Out[16]:
array([0. , 0. , 0. , 1. , 0. ,
2. , 1.19595024, 0.51055787, 0.95924191, 0.31294034])

LogisticRegression satırını tekrar uncomment yapıp, o hücreyi tekrar çalıştırdıktan sonra artık gönül rahatlığı ile modelimizi eğitebiliriz.

pipe = Pipeline(steps=[('ct', coltrans), #fit_trasnformlu bir işlem
('fs', SelectKBest(score_func=mutual_info_classif,k=10)), #fit_transformlu bir işlem
('clf', LogisticRegression(C=1,max_iter=1000,random_state=42)) #sadece fitli bir işlem en sonda yer alıyor
])
pipe.fit(X_train,y_train) #Bu sefer sadece fit yapıyoruz

Hangi featurelar seçilmiş, bir de onlara bakalım.

selecteds=pipe["fs"].get_support()
[x for e,x in enumerate(ml.get_feature_names_from_columntransformer(coltrans)) if e in np.argwhere(selecteds==True).ravel()]
Out[18]:['OHE_Sex_1',
'OHE_ExAng_1',
'OHE_Slope_3',
'OHE_Thal_normal',
'OHE_Thal_reversable',
'ChestPain',
'Chol',
'MaxHR',
'Oldpeak',
'Ca']
#seçilmeyenler
[x for e,x in enumerate(ml.get_feature_names_from_columntransformer(coltrans)) if e not in np.argwhere(selecteds==True).ravel()]
Out[19]:['OHE_Fbs_1', 'OHE_Slope_2', 'Age', 'RestBP', 'RestECG']

Her ne kadar fit etmiş olsak da önceki aşamalar için arka planda fit_transform çalışır ama daha önce belirttiğimiz gibi son aşama mutlaka sadece fit’li bir estimator olmalıdır.

Eğer isim parametreleri(csi, ohe fs, clf vs) kullanılmayacaksa utility function olan make_pipeline ve make_colum_taransfomer kullanılabilir. Peki isimlere ne zaman ihtiyaç duyarız? Mesela grid search yapacaksak, parametreleri gönderirken pipeline isimlerini kullanırız, ki bu örneği de birazdan göreceğiz. Şimdi bu utility functionlara bakalım. Bu arada, bunlarda transformerlar artık bir list içinde yazılmıyor, doğrudan yazılıyor.

GridSearchlü kullanım

Basit versiyon

Şimdi bir de gridsearch yapalım, burda önemli bi notasyon var, ondan bahsedeyim. pipelinenın hangi stepine parametre göndereceksek o stepin adını __ takip eder, sonra da parametre adı. Columntransformer da kullanacaksak zincirleme __ ifadesi kullanılır.

Eğitelim

%%time
gs1.fit(X_train, y_train)
Fitting 15 folds for each of 96 candidates, totalling 1440 fits
Wall time: 1min 53s

Sonuçları dataframe üzerinde görelim.

ml.gridsearch_to_df(gs1)

Bir işlemin yapıldığı ve yapılmadığı senaryo hazırlama

Şimdi bir de gridsearchümüz outlier handling yapsın ve yapmasın seçeneğini nasıl kurgulayacağımızı görelim, ki bu diğer işlemlerin de yap/yapma seçenekleri için bir örnek teşkil edecektir.

Bunun için DummyTransformer adında bir class tanımlayıp bunu placeholder olarak kullanacağız. Bu placeholder'a bi yapmak istediğimiz işlemi(bizim örnekte Outlierhandler) bir de None değerini göndeririz.

Uyarı: DummyTransfomer kullanmaya başladığınızda yukarıda yaptığımız gibi sadece coltrans’ın ara sonucuna bakma şansımız kalmıyor, hata alırsınız.

Eğitelim

%%time
gs2.fit(X_train, y_train)
Fitting 15 folds for each of 48 candidates, totalling 720 fits
Wall time: 56.7 s

Sonuçları görelim

ml.gridsearch_to_df(gs2)

Farklı transformerler

Şimdi de scaling olarak bi StandartScaler bir de MinMaxScaler yap diyelim. Yine benzer şekilde farklı işlemler için de benzer mantık uygulanabilir. Ör: Imputation için bi SimpleImputer bir de IterativeImputer kullanmak, veya feature selection için SelectKBest ve RFE kullanmak veya custom fonk içinde korelasyon seçici kullanmak gibi.

Biz burdaki örnekte 3 farklı scaler deneyelim. Yukardaki dummytransformer’ı bunun için de kullanacağız.

Eğitelim

%%time
gs3.fit(X_train, y_train)
Fitting 9 folds for each of 144 candidates, totalling 1296 fits
Wall time: 1min 32s

Sonuçları görelim

ml.gridsearch_to_df(gs3)

GridSearch içindeki çoklu dictionary kullanma yöntemini iyi anlamak önemli, aksi halde gereksiz sayıda çok deneme yaptırmış olursunuz. Mesela Outlierhandling yapacaksınız diyelim, bunun için bir de RobustScaler yaptırmaya gerek yok(çünkü bu zaten outlierlara çözüm olsun diye kullanılıyor), biz yukarıda yaptık ama bu doğru bi kullanım şekli olmadı, onun yerine şu şekilde yapmak daha mantıklı.

Farklı Classifierlar

Diyelim ki birden fazla algoritma kullanmak istiyoruz, ki gerçek hayatta böyle olacak, olmalı. Bunlar için ayrı ayrı griddsearchler yapmak yerine tek bir gridsearch içinde bu işi gerçekleştirebiliriz.

Bu sefer bir de DummyEstimator adına bir placeholder sınıf yaratıyoruz. Burda ayrıca classifierın duyarlı olduğu şeye göre bazı işlemleri yapar veya yapmayız. Mesela DecisionTreeler konusunda göreceğiz ki, bu algoritma scalinge veya outlierlara duyarlı değil, o yüzden bu işlem(ler)i yapmaya gerek yok ama yine de bu işleme ait placeholder olan dummytransformer için None değerini verdiğimiz bir satır yazmalıyız, yoksa hata alırız.

Eğitelim

%%time
gs4.fit(X_train, y_train)
Fitting 6 folds for each of 72 candidates, totalling 432 fits
Wall time: 32.7 s

Sonuçları görelim

ml.gridsearch_to_df(gs4)

Bu yukarıda kullandığımız DummyEstimator yerine estimatorleri bir list içine alıp döngüyle dolaşıp, her döngü içinde gridsearch yapmak gibe seçenekler veya aşağıdaki linkteki öneriler de var ama bana en sade ve pratiği bizim kullandığımız gibi geliyor. Tercih size kalmış.

HalvingRandomizedSearch ile hızlandırma

Fitting 6 folds for each of 2 candidates, totalling 12 fits
Wall time: 4.31 s

Sonuçları görelim

ml.gridsearch_to_df(hrs)

Estimatorler arasında seçim yapma ve oylama

Farklı classfierlar denediysek, ki gerçek hayatta deniyor olacaksınız, böyle bir sürecimiz olacak ancak biz bu kısmı şimdilik yapmayacağız. Diğer algoritmaları ve özellikle ensemble modelleri gördüğümüz notebooklarda yapacağız. Ve günün sonunda ideal yapı şöyle olacak.

Ana pipeline içinde grid/randomize search ve paramgrid içinde de birden çok estimator olur. Buradaki işlem, best_modeli almak yerine en iyi 3 modeli alıp, bunları bir voting mekanizmasına tabi tutmak olacak.

En iyi estimatorleri aşağıdaki fonksiyonla bulabiliriz.

ml.compareEstimatorsInGridSearch(gs4,tableorplot='table')

Bu custom fonksiyon ile hem eğitim performans sonuçlarını hem de eğitim sürelerini görmüş olursunuz. Nihai modelinizde ikisini de göz önünde bulundurmakta fayda var.

Bu arada sonuçlara grafik olarak da bakabiliriz.

ml.compareEstimatorsInGridSearch(gs4,tableorplot='plot',figsize=(4,4))

Transformerlar için farklı sıralamalar

Farklı alternatifler için birkaç gridsearch çalıştırmak gerekebilir. Örneğin birinde önce feature selection sonra encoding, diğerinde tam tersi gibi. Bu alıştırmayı da size bırakıyorum.

Kaynaklar

Model Evaluation

Şimdi, pipelinımız da oluştuğuna ve en iyi modelimizi seçebilecek durumda olduğumuza göre artık test datası ile tahminleme yapıp modelimizi değerlendirmeye geçebiliriz.

Şimdi burdaki ilk soru şu olabilir, biz zaten train seti üzerinden bir score ölçümü yapmıştık, o değerlendirme değil miydi, daha ne değerlendirmesi yapacağız? Aslında adı üzerinde; bu skor, train setinin skoruydu. Modelimizin generalization(genelleştirilmiş) performansını test setinin skoru verir, traininki değil.

Test performansı genellikle train setinin performansından biraz daha kötü olacaktır. Bu normal olup, hyperparametrelerle biraz daha oynamaya çalışmanıza gerek yoktur. Bu arada bazen ender olarak test setin performansı traininkinden de iyi çıkabilir.

Ancak train ve testin skorları arasında normal sayılamayacak büyüklükte fark varsa, modelimizin overfit ediyor diyeceğiz ve bu problemi çözmeye çalışacağız. Ama aslında daha öncesinde şuna bakacağız: Train setimizin performansı beklentimizin(veya varsa bir baseline modelin) yeterince üstünde mi? Değilse, bu sefer de modelimiz underfit ediyor diyeceğiz ve test setinin ölçümüne hiç geçmeyeceğiz bile. Hadi şimdi bu iki kavrama ve bunların olması durumunda ne tür aksiyonlar alacağımıza bakalım.

Bias-Variance Tradeoff & Overfitting/Underfitting

Train setimizin eğitimi sonucunda çıkan skor beklediğimizin çok altındaysa(classification için böyle, regressionda ise tam tersi) modelimiz underfit ediyor ve high bias bir modelimiz var demektir. Neden high bias, çünkü modelimiz onu gerektirdiği varsayımlara karşı önyargılıdır(biased), varsayımları çok basite indirgiyordur.

Aynı şekilde modelimiz test datasında train datasına göre çok kötü performe ediyorsa overfit ediyordur ve high variance bir modelimiz vardır deriz, buna high variance denme sebebi de, test seti verdiğimizde çıkan hataların çok dalgalanması, yani yüksek variancelı olmasındandır. Bu isimlendirmeyle alakalı şurda güzel bi açıklama mevcut, oraya da bakmanızı tavsiye ederim.

Bias ve variance iki zıt olgudur, biri artarken biri düşer. Amacımız ikisini de düşürmek ama birini düşürürken diğeri arttığı için optimum noktayı bulmaya çalışırız.

Bir başka yaygın gösterim şöyledir.

Özetleyecek olursak, bu iki öğrenme sorununun sebepleri ve çözüm önerileri şöyledir:

Overfitting;

Sebepleri:

  • Model, gürültülü veriyi(hata ve outlierları) ezberlemiştir
  • Elimizde az miktarda data vardır, zaten bir kısmını da teste ayırmışızdır, öğrenilecek data iyice azalmıştır, data en küçük ayrıntısına kadar ezberlenmiştir.
  • Gereksiz karmaşıklıkta bir model kullanmışızdır, halbuki ihtiyaç basit modeldir
  • Çok feature vardır

Nasıl anlarız:

  • Train accuracy >> test accuracy

Çözüm:

  • Outlier handling -> noise düşürme
  • Hatalı kayıtların temizlenmesi
  • Veri azsa artırılabilir
  • Model basitleştirilebilir
  • Kolon çoksa feature selection ve/veya PCA yapılabilir
  • Linear modelse regularization eklenebilir, tree based bir modelse arama derinlik düşürülebilir
  • Ensemble model kullanılabilir

Underfitting

Sebepleri:

  • Elimizde az miktarda data vardır, zaten bir kısmını da teste ayırmışızdır, öğrenilecek data iyice azalmıştır, data yeterli gelmemiştir.
  • Karmaşık bir model gerekliyken basit bir model kurmuşuzdur

Nasıl anlarız:

  • Eğitim setinin performansı çok düşüktür. Testinkine bakmaya gerek bile olmaz.

Çözümler:

  • Daha karmaşık model kullanılabilir
  • Yeni feature eklenebilir
  • Linear modelse regularization miktarını düşürebilir, tree based bir modelse arama derinliğini artırabiliriz.

Test datası tahmini

Buraya kadar bulduğumuz skorlar, hala model selection aşamasında olduğumuz için train setinin skoruydu. Tekrar hatırlatmak gerekirse gridsearch sonucunda çıkan best_score_ değeri, cross validation yaparken ayırdığımız validation setlerinin ortalama skorudur.

Şimdi kısa bir ara verip test datamızı tahminleyelim, sonra farklı ölçüm metrikleriyle ve yöntemleriyle devam edeceğiz.

#öncelikle nolur nolmaz yedeğimizi alalım
X_test_orj=X_test.copy()

pipelinsız tahmin

Bu yöntem çok zahmetli olacaktır. X_test üzerinde, train setinde yaptığımız tüm manuel processingleri yapmayı gerektiriyor. Normalde test tahminlemeyi bu şekilde yapmayız, belki bunu sadece eğitim amaçlı kaynaklarda görebilirsiniz. Ama burda yapsaydık şu şekilde yapardık(kodsuz), böylece bunun ne kadar zahmetli olduğunu siz de görün.

  • X_test[nums] = numimp.transform(X_test[nums]) diyerek numeriklerin imputationuını yapardık
  • aynısını categorikler için yapardık
  • sonra outlierhandlingi yapardık
  • sonra feature selection yapardık
  • sonra nominallerin onehot ecnodingini, chestpain’in ordinal encodingini yapar ve bunlarla diğer ordinal kolonları birleştirirdik, ama burası bizim için biraz daha zahmetli olurdu, zira artık kolon bilgilerini kaybettiğimiz için bunların kolon indekslerini da bulup öyle işleme sokmak zorunda kalırdık
  • sonra scaling yapardık
  • son olarak da tahminlememizi yapardık

pipelinelı tahmin

Pipelinelı bir model kurduğumuzda işlerin ne kadar kolay olduğunu görelim. Yukardaki gridsearchlerden birinin best estimatorünü alıp onunla doğrudan tahminleme yapacağız. Hatta bestestimator almaya da gerek yok, çünkü zaten refit=True idi, yani şuan gs4=gs4.best_estimator_. O kendi içinde tüm transformerları kendi içinde yapıyor olacak.

#generalization performance skoru
gs4.score(X_test,y_test)
Out[40]:0.8032786885245902

Bu kadar basit. Harika değil mi :)

Train scorumuz 0.83 civarıydı, sanki hafiften bi overfitting var gibi ama şimdi bi learning curve’e bakalım, daha derin yorumlamalarda bulunalım.

Learning Curve

Learning curve train seti ile çizdirilir. Amaç, daha fazla veri eklendiğinde cross-validation sonucu iyileşme trendine giriyor mu, onu görmektir. Zira hem overfitting hem underfitting çözümünde ne demiştik; veri miktarı artırılırsa iyileşme bekleriz.

ml.plot_learning_curve(gs4.best_estimator_,"Learnig curve",X_train,y_train,cv=mycv)

Soldaki grafikten analışıyor ki, cv score 100'e kadar artma eğiliminde, ama ondan sonraki sample sayısını artırmanın bi esprisi yok, zira düşüş başlıyor.

Ölçüm metrikleri

Accuracy score

Bu skor, toplam yaptığımız tahmin adedinin ne kadarı doğru(binary classfication için hem 1'ler hem 0'lar) bunu gösterir. Yukarıda score metodu ile yaptığımız işlem bize accuracy’yi döndürdü, çünkü gridsearchü yaparken scoring olarak accuracy seçmiştik. Bununla birlikte sklern.metrics içinde de bu skoru veren bir fonksiyon vardır, ama parametre olarak y_true, yani y_test ve tahminlediğimiz labelları yani y_predleri alır. Bu bazen y¯ (y_hat) olarak da adlandırılır. Bunun için predict metoduyla tahminleme yaparız.

from sklearn.metrics import accuracy_score
y_pred= gs4.predict(X_test)
accuracy_score(y_test, y_pred)
y_pred[0],y_test.values[0] #ilk instance için kontrol
Out[42]:
0.8032786885245902
('Yes', 'Yes')

Logistic regresyon notebookuna baktıysanız ve işleyişi öğrendiyseniz görmüşsünüzdür ki, aslında bu algoritma bi olasılık hesabı yapıyor, bu hesaplama sonucu %50 üzerindeyse 1(Yes/True), altındaysa 0(No/False) değeri üretiliyor. Peki bu olasılık değerlerini nasıl bulacağız? predict_proba metodu ile.

gs4.predict_proba(X_test)[0]Out[43]:array([0.01967801, 0.98032199])

Mesela burda ilk kayda %98 oranında Yes olur dediği için Yes atanmış olduğunu görüyoruz.

Daha anlaşılır bir şekilde gösterelim.

probs = gs4.predict_proba(X_test)
data = {'Actual' : y_test,
'Predicted': gs4.predict(X_test),
'Prob(No)' : probs[:,0],
'Prob(Yes)' : probs[:,1]
}
dfprobs = pd.DataFrame(data)
dfprobs.head(5)

Bu arada istersek tahminlerimizi predict ile değil predict_proba’nın sonucuna göre de yapabiliriz. Mesela %50 değil de %80in üzerindekilere Yes diyelim.

np.where(probs[:,1]>0.8,"Yes","No")

Biraz aşağıdaki farklı thresholdlar için sonuçların ne olacağına da bakacağız, ve bunun daha derin bir anlamı olacak.

confusion matrix

Targettaki classlar arasında dengeli bir dağılım varsa, accuracy genelde yeterlidir. Doğru bildiğimiz tahminlerin toplam kayıt sayısına olan oranı verir demiştik. Ancak diyelim ki verisetinizdeki 100bin satırın, 99bini kanserli değil, bini kanserli diye etiketlenmiş olsun. Yani mevcut durumda kansersiz kişi oranı %99. Siz böyle bir durumda hiç model çalıştırmadan tüm kayıtlar için “kanserli değil” tahminini yaptığınızda sözde modelinizin(!) accuracy oranı %99 olacak. Ama bu tahminler gerçekten o kadar başarılı mıdır? Aslında çok başarısız bir tahminleme yönteminiz var, ama bunu nasıl ifade etmek gerekir?

Böyle durumlar için başka metriklere ihtiyaç duyulur. Bunlar için de öncelikle bir confusion matrix çıkarılır. Gerçek(Actual) Pozitiflerin(True positive:TP de denir), ki bunlara genellikle 1 olarak class etiketi verilir, ve Gerçek Negatiflerin(True Negative:TN, etiketi 0) ne kadarını doğru, ne kadarını yanlış tahmin etmişiz, bunu göstermek için idealdir. Genelde aşağıdaki gibi bir tablo ile gösterilirler.

Not: Bu gösterim şekli binary classficiation içindir ancak benzer mantıkla 3x3lük matris de bize 3 classlı bir veri seti için confusion matrixi oluşturacak ve ona uygun metrikler de hesaplanabilecektir.

Uyarı: Bazen bu tablo transpoze veya köşeler ters olacak şekilde gösterilebilir. Yani aşağıdaki gibi bir tablo da görebilirsiniz. Bu matrix, birçok yerde üstteki gibi gösterilmesine rağmen sklearn’ün confusion matrix’i aşağıdaki şekilde oluşur. Bu biraz kafa karıştırıcı ama maalesef durum böyle, bu konuda dikkatli olmanızı tavsiye ederim.

Şuradan aldığım şu görsel daha da açıklayıcıdır. Bahsekonu Type I ve II errorları istatistik dünyasından gelmektedir. İlgilenen bunları da araştırabilir.

Şimdi sklearn ile bu matrix nasıl çıkarılır, ona bakalım.

from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test,y_pred)
cm
Out[46]:
array([[27, 5],
[ 7, 22]], dtype=int64)

Bunu görsel olarak da ifade edelim.

#classes sırasını doğru yazdığınızdan emin olun
ml.plot_confusion_matrix(confusion_matrix(y_test,y_pred),classes=["No","Yes"])
#manuel accuracy hesabı
# (TP+TN)/Toplam
(cm[1,1]+cm[0,0])/np.sum(cm)
Out[48]:0.8032786885245902#normalized
ml.plot_confusion_matrix(confusion_matrix(y_test,y_pred),classes=["No","Yes"],normalize=True)

Precision(Speficifty)

TP/(TP+FP) şeklinde hesaplanır, ki bu aslında pozitif tahminlerin accuracy’si olarak da yorumlanabilir. Yani bunda odak noktası pozitif tahminlerdir. Positive Prediction Value olarak da bilinir. 1'den çıkarılmış haline False Positive Rate denir, ki bunu da ROC/AUC bölümünde göreceğiz.

Bu metrik özellikle imbalanced veri setlerinde anlamlıdır, ama dengeli bir verisetinde de kullanılabilir. Pozitif tahmini yaptığımız zaman hata yapmış olmak çok sıkıntıya neden oluyorsa bu metriği takip etmek önemlidir. Birkaç örneği şöyle sayabiliriz:

  • Spam olmayan bir maile(actual=0) spam dersek(prediction=1), önemli bir mailin kaçmasına neden olabiliriz.
  • Churn etmeyecek bir müşteriye churn eder dersek, ona gereksiz promosyonlar verilebilir.(Gerçi bunun tersi daha maliyetlidir)
  • Kanser olmayan birine kanser teşhisi konması(Tersi durum daha tehlikeli olmakla birlikte, burda da kişinin hayat standartlarını gereksiz düşürebiliriz)
from sklearn.metrics import precision_score
#y'leri labelencode etmediğimiz için pos_label değişkeninde pozitiflerin ne olduğunu belirtiriz
precision_score(y_test,y_pred,pos_label="Yes")
Out[50]:0.8148148148148148

Pozitif tahminlerimizin %81i doğruymuş.

Recall(Sensitivity)

Bu da yine imbalanced datasetlerde çok anlamlıdır. TP/(TP+FN) oranıyla hesaplanır ve gerçekte pozitif olanları tespit edebilme başarısı olarak düşünülebilir. Yani bundaki odak noktası pozitif classlardır. True Positive Rate(TPR) olarak da bilinir. Burda da önemli olan gerçekte pozitif (actual=1) olanları doğru tahmin edebilmektir, yani yanlış(false) negatif tanıları minimize etmek isteriz.

Yukarıda verdiğimiz örneklerden churn ve kanserli örneklerinde bu metrik çok daha önemlidir.

from sklearn.metrics import recall_score
recall_score(y_test,y_pred,pos_label="Yes")
Out[51]:0.7586206896551724

Bu örnek de sağlıkla ilgili bir örnek olduğu için bence şuan en önemli metrik budur. Yani biz her ne kadar accuracy %80miş desek de asıl başarımız %76 civarındaymış.

F1 ve Fβ score

Precision ve Recall’dan hangi durumda hangisinin kullanıldığını biliyoruz. Peki emin değilsek? İşte o zaman F1 oranını kullanabiliyoruz, bu oran bu iki skor arasında bi denge kurmaya çalışır. Şöyle bi örnek veriyorlar: Polisseniz ve suçluları yakalamak istiyorsanız, yakaladığınız kişinin suçlu olduğundan emin olmak ve mümkün olduğunca çok suçluyu yakalamak istersiniz.

from sklearn.metrics import f1_score
f1_score(y_test,y_pred,pos_label="Yes")
Out[52]:0.7857142857142857

f1 skoru, precision ve recall'u eşit öneme sahip gibi düşünür, ama siz FN'ler daha maliyetli diye düşünüyorsanız, yani recall daha önemli ise F-2 skorunu, FP'ler daha maliyetli diye düşünüyorsanız yani precision daha önemli ise F-0.5 skorunu kullanabilirsiniz.

from sklearn.metrics import fbeta_score
fbeta_score(y_test,y_pred,pos_label="Yes",beta=2)
Out[53]:0.7692307692307692

classification report

Yukarıdaki metrikleri class/label bazında bir arada gösteren faydalı ve özet bir rapordur.

from sklearn.metrics import classification_report
print(classification_report(y_test,y_pred))

Burda support, y_test içinde ilgili class'taki instance sayısını verir. macro avg, aritmetik ortalama verirken weighted avg ise adı üzerinde instance sayısıyla ağırlıklandırarak ortalama alır. accuracy'nin f1 altında göründüğüne bakmayın, tabloda bi yere oturtmak zorunda oldukları için ayrı bir kolon açmak yerine oraya koymuşlar.

Precision-Recall Dengesi

Precision ve Recall aynı anda 1 olamıyor. Biri artarken diğeri azalıyor demiştik. Problemin doğasına göre biri sizin için daha önemli olacaktır. Bunun için de positive demek için nasıl bir threshold belirlemek lazım, buna karar vermek gerekiyor.

Thresholdlar özellikle %50 etrafındaki tahminler için önemli. %51'in 1, %49'ın 0 sayıldığı durumlar gibi. Burda ölçmek istediğiniz şeyde pozitif vakaları(spam mail, tümörlü hücre v.s) ne kadar doğru yapmaya heveslisiniz, bunun kararı kritik olmakta. Spam olmayan maile spam demekle, kanserli olmayan kişiye kanserli demek aynı düzeyde hata değildirler. İlkinde çok az hata yapmak istersiniz, aksi halde önemli bir maili kaçırmış olabilirsiniz, ikincisinde ise sağlıklı kişiye kanser dediğiniz için gereksiz yere kanser tedavi masrafları oluşur, belki bir de yan etkiler görülür. Tersten baktığımızda ise spam bir maile spam değil dersek, inboxımızda bu maili görmüş oluruz, bunun da çok yıkıcı etkisi olmaz, bu hata kabul edilebilir. Ama kanserli kişiye “kanserli değil” dersek kişiyi tedaviden mahrum bıraktığımız için ölümüne neden olabiliriz, ki bu çok büyük bir hatadır.

Bu yüzden spam konusunda amacımız tüm pozitif(P) tahminlerimizi doğru(T) bilmiş olmak, yani %100 precision/specifity sağlamak isteriz. TP/(TP+FP). Burda, thresholdumuzun yüksek olmasına isteyeceğiz. Yani %51i spam saymayacağız, belki diyeceğiz ki %90 üzeri olasılık varsa bunu spam say. Varsın, %80 ihtimalle spam olan bir maile spam demeyelim ve inboxımza düşsün, çok da kritik değil.

Kanser örneğinde ise gerçekte pozitif olan tüm caseleri doğru tahminlemek isteriz, yani kanserliye kanser değilsin(False Negative) demek istemeyiz. TP/(TP+FN), yani %100 recall isteriz. Bunun için de %20 bile bir ihtimal olsa buna kanserli diyelim diyeceğiz, varsın bazı sağlıklılara(%30 ihtimalle kanser) “kansersin” diyelim.

Aynı anda ikisinin %100 olmasını bekleyemeyiz dedik, bi denge sağlamaya çalışacağız.

Konuyu pekiştirmek için güncel bir örnek daha verelim(Yıl 2021). Bir kişiye yanlışlıkla “corona pozitif” demek(False positive), çok büyük bi sorun yaratmayacaktır, o kişi gereksiz yere karantinaya alınacak, hayat standardı düşecek, ancak yanlışlıkla “negatifsin” demek(False Negative), salgının artmasına neden olacaktır. O yüzden burdaki pozitiflik thresholdunun çok düşük tutulması gerektiği aşikardır.

ROC Curve ve AUC score

Çeşitli thresholdlar için TPR(y) ve FPR(x) grafiği çizdiğimizde ROC(Receiver Operating Characteristic) eğrisini elde etmiş oluruz. ROC altında kalan alan ise AUC(Area Under Curve) metriği olarak karşımıza çıkar ve bu metrik farklı modellerin performanslarını karşılaştırmak için kullanılır. AUC değeri ne kadar büyükse o kadar iyidir.

Optimal threshold bilgisi, farklı thresholdlar için oluşan confusion matrixlerden görülebilir ancak bu kadar çok confusion matrixi yorumlamak zor olmakta, o yüzden ROC gibi görsel bir araç yardımımıza koşmaktadır.

Önemli: AUC değeri, positive ve negativelerin sayılarının birbirine yakın olduğu yani imbalance olmadığı durumlarda anlamlıdır ve imbalanced bir verisetinde kullanılmamalıdır. Ayrıca true negative ve true positiveler eşit öneme sahipse kullanılmalıdır. Bakınız Özet ve Karşılaştırma

Bunun için hazırladığım fonksiyonu aşağıdaki gibi kullanabileceğimiz gibi, aşama aşama neler olduğunu görmek için bu fonksiyonu satır satır da çalıştırabilirsiniz.

ml.plotROC(y_test, X_test, gs4, pos_label="Yes")

Burdaki decision point için şuan standart thresholddaki(0.5) confusion matrix sonucuna göre TPR-FPR kesişimi gösterilmiştir. Arzu edilen threshold için ilgili nokta bulunabilir.

Burdan çıkan AUC değerini başka bir modelinkiyle de karşılaştırmak faydalı olacaktır. Şimdi mesela bir gridsearchümüzdeki DecisionTree’nin en iyi parametre bilgilerini alıp bakalım.

cvres = gs4.cv_results_
cv_results = pd.DataFrame(cvres)
cv_results["param_clf"]=cv_results["param_clf"].apply(lambda x:str(x).split('(')[0])
dtc=cv_results[cv_results["param_clf"]=="DecisionTreeClassifier"]
dtc.getRowOnAggregation("mean_test_score","max")["params"].values
Out[56]:
array([{'clf': DecisionTreeClassifier(random_state=42), 'clf__criterion': 'gini', 'clf__max_depth': 3, 'clf__min_samples_split': 4, 'ct__numerics__ouh': None, 'ct__numerics__scl': None}], dtype=object)

Bu parametreleri aşağıda DecisionTreeClassifier constructorı içine verelim.

pipe_dt.score(X_test,y_test)Out[58]:0.7868852459016393ml.plotROC(y_test, X_test, pipe_dt, pos_label="Yes")

Bunun AUC değeri LogReg’den daha düşük çıktı.

Precision-Recall Curve

Imbalanced data sözkonusu olduğunda Precision-Recall curve, ROC-AUC’dan çok daha yararlıdır. Zira, burda farklı modeller arasındaki sonuçlar çok daha çarpıcı olmakta ve model seçimi de daha kolay olmaktadır.

ROC’da Recall ve 1-Precision bakılırken burda Recall ve Precision bakılır.

Burda yine hazır yazılmış bir fonksiyonum var, onu kullanacağız. Fakat bu sefer labellara Yes/No şeklinde değil de 1/0 şeklinde ulaşmak istedim, o yüzden şimdi bunları bi encode edelim.

from sklearn.preprocessing import LabelEncoder
le=LabelEncoder()
y_train_le=le.fit_transform(y_train)
y_test_le=le.transform(y_test)
y_test_le[:5]
Out[60]:array([1, 1, 1, 0, 1])

Kontrol edelim

y_test.values[:5]
Out[61]:array(['Yes', 'Yes', 'Yes', 'No', 'Yes'], dtype=object)

Plotumuzu çizelim

ml.plot_precision_recall_curve(y_test_le,X_test,gs4)

Soldaki grafikte, Precision ve Recall’ün farklı thresholdlar için nasıl bir tradeoff içinde olduğunu görüyoruz. Sağdaki ise Precision-Recall eğrisi olup sağ üst köşeye doğru gittikçe mükemmel bir denge sözkonusu olmaya başlar. Mavi çizgi ideal halken, alttaki yeşil çizgi ise her instance’ın pozitif tahminlendiği, bi beceri gerektirmeyen durumu gösterir.

Son durumda kendimize ideal bir threshold belirledikten sonra nihai tahminlememiz şöyle olacaktır:

from sklearn.preprocessing import binarize
pred_prob = gs4.predict_proba(X_test)
binarize(pred_prob, threshold=0.3)[:,1]
#veya
np.where(pred_prob[:,1]>0.3,1,0)

Cohen’s Kappa

Bunun da bazı durumlarda faydalı bir metrik olduğu söyleniyor. Ben çok inceleme fırsatı yakalayamadım açıkçası. Detaylarını şu sayfalarda bulabilirsiniz:

Ama şurada ise kaçınılması gerektiği yazıyor. O yüzden çok detaylarına girmiyorum, belki ilerleyen dönemlerde daha derin araştırmalar yaparsam burayı güncellerim.

Cumulative Gains ve Lift Charts

Lift ve Gain analizi ile, model olmadan yapacağımız random tahminlere göre modelimizle ne kadar daha iyi pozitif tahminlemesi yaptığımızı ölçeriz. Görsel açıdan model performansını ölçmeye yardımcı olan bu analizlerin ikisinde bir baseline vardır, lift eğrisi ile bu baseline arasındaki alan ne kadar büyükse modelimiz o kadar iyi tahmin yapıyor demektir.

Şu aşamalardan oluşur:

  • İlk olarak pozitif classtaki tahminlerin olasılık değerlerini alır ve datayı buna göre sıralarız.
  • Sonra bunları 10luk dilimlere(Decile) böleriz ve her bir Decile’da gerçek pozitiflerin sayısını buluruz->Number of Responses
  • Gain, bu Number of Responses’ların kümülatif değerinin datadaki toplam pozitiflere oranıdır. Yani ilgili decile seviyesinde gerçek pozitiflerin ne kadarını kapsama almışız, onu gösterir. Mesela aşağıdaki tablodan göreceğiniz üzere 5.decile’da gerçek pozitiflerin ~%80ini kapsama almışız. Yani datanın sadece %50siyle bile pozitiflerin %80lik kısmını doğru tahminleyebiliyoruz.
  • Lift ise, modelimiz sayesinde random guesse(modelsiz duruma) göre ne kadar iyi pozitif tahminlemesi yaptığımızı gösteriyor. Yine aşağıdaki tabloya baktığımızda 2.seviyede, modelimizin lifti 1.90 olup, datanın %20lik kısmını aldığımızda pozitifleri random seçeceğimiz bir duruma göre 1.90 kat daha doğru tespit edebildiğimizi gösterir, ki bu değer 37.93'ün 20ye oranı oluyor.
ml.plot_gain_and_lift(gs4,X_test,y_test,pos_label="Yes")

Log loss(logistic loss or cross-entropy loss)

Bu metriğin detaylı açıklamasını Logistic Regression notebookunda bulabilirsiniz. Özetle, tahminlenen değerle gerçek değer arasındaki farkın negative logaritmasıdır. Algoritma çalışırken zaten bunu minimize etmeye çalışır.

Bir modelde yapılan gerçek hatayı en iyi gösteren metric olup, modelin genel güvenilirliğini sorgulamak istediğinizde bu metrik kullanılabilir. Aşağıda gördüğünüz üzere gridsearch içinde scoring’e bunun negatif versiyonu(maximize edilsin diye) verilerek bu metric minimize edilmeye çalışılabilir.

gs5 = GridSearchCV(estimator = pipe, param_grid = params, cv = mycv, n_jobs=-1, verbose = 1, scoring = 'neg_log_loss',                  error_score='raise')

Eğitelim

%%time
gs5.fit(X_train, y_train)
Fitting 6 folds for each of 72 candidates, totalling 432 fits
Wall time: 32 s

En iyi skora bakalım

gs5.best_score_ 
Out[67]:-0.415460229377167

Sonuçları dataframede görelim

ml.gridsearch_to_df(gs5)

Bu yeni modeli test seti üzerinden tahminleyip classification reportumuzu çıkaralım.

y_pred= gs5.predict(X_test)
print(classification_report(y_test,y_pred))

Sonuçlar gs4'e göre bi miktar iyileşmiş oldu.

Peki bu test setine göre generalization performance’ı ne oluyor, bir de ona bakalım

gs5.score(X_test,y_test)
Out[70]:-0.44937618027248427

Buna şöyle de bakabiliriz, tabi bu sefer pozitif sonuç elde edeceğiz.

from sklearn.metrics import log_loss
log_loss(y_test_le,gs5.predict_proba(X_test))
Out[71]:0.44937618027248427

0.415ten 0.449a yükselmiş oldu, ki generalization performansında beklediğimiz bir durumdur bu.

Maliyet bazlı modelleme

Şimdi de farklı bir şekilde olaya yaklaşıp, yaptığımız hataların ölçülebilir maliyetleri olsaydı nasıl davranırdık, ona bakalım. Tabi ki hatayı minimize etmeye çalışırdık, şimdi bunları nasıl yapabileceğimizi görelim.

Custom Cost function yazma

Daha önce söylediğimiz gibi, iki tür hata vardır ve bunların maliyeti de farklı olabilmektedir. Ör:Spam örneğinde, spam maile “spam değil” demek büyük bir sorun yaratmaz, bu yüzden maliyet 1 olabilir. Ancak spam olmayana spam demek çok büyük sorun yaratabilir, bunun da maliyeti 10 seçilebilir. Buna göre en uygun trehsholdu bularak toplam maliyeti minimize etmek gerekebilir.

Bu bilgiler ışığında kendi modelimiz için çeşitli maliyetler belirlemeye çalışalım. Unutmayın, amacımız, bir hastada kalp hastalığı var mı yok mu onu tahminlemeye çalışıyoruz.

  • Cost of True Negative: Bunun maliyeti 0'dır.
  • Cost of True Positive: Bu hastalara ilave tetkikler yapacağız diyelim, maliyeti 5 olsun. (Ama bi hata yapmadığımız için 0 da diyebilirsiniz isterseniz)
  • Cost of False Negative: Yanlışlıkla “iyisiniz” diyoruz, halbuki kişi aslında hasta. Ölüm durumu sonunda ailesi tarafından tazminat davası açılabilir. Maliyeti 100 olsun.
  • Cost of False Positive: Yanlışlıkla hastasın diyoruz. Gerekisiz ilaç veriyoruz, yan etkiler nedeniyle dava açılabilir. Maliyeti 60 olsun.

Nihai maliyet fonksiyonumuz şöyle:

Şimdi yine hazır fonksiyonumuzu kullanarak çeşitli thresholdlarda(cutoff) maliyet ve diğer metrikler ne oluyormuş onlara bakalım ve bunların bi grafiğini çizdirelim.

ml.find_best_cutoff_for_classification(gs4, y_test_le, X_test,[0,5,100,60])

Görüldüğü üzere thresholdun %21 olduğu yerde maliyetimiz en düşük(15.90) oluyor, ki bakarsanız bu aynı zamanda F1 skorunun da en yüksek olduğu yer. Demek ki maliyeti minimize etmek istiyorsak olasılığı default değer olan %50 değil de %21 üstündeki her şeyi pozitif saymalıyız.

Şimdi diyelim ki, hiç model kullanmadık ve ihtiyatlı davranıp herkese hasta dedik, maliyet nolur, bi bakalım. Yani FP hatası yapıyoruz, threshold 0, yani 33.85 birim maliyetimiz olacak. Şimdi de, kimse hasta değil diyelim, yani FN hatası yapalım, böyle bi durumda da maliyetimiz yaklaşık 44 TL üzerinde olacak.

Her iki durum da modelli yaklaşımımıza göre kötü olacaktır. Sonuç: Modelimiz oldukça başarılıdır.

Class-weight

Bazen elimizde böyle maliyet değerleri olmaz, ama dersiniz ki, FN’deki hata FP’deki hatadan şu kadar kat daha önemli, o yüzden FN hatalarını şu kadar kat daha cezalandır.

sklearn bunun için bize class_weight adına bir parametre verir. Bu şekilde classlar arasında bi denge yakalamaya çalışırız. Bunun default değeri None olup, her iki class(0-1, veya multiclass ise tüm sınıflar) için de eşit ağırlık kullanır, yani 1. Buna ayrıca bir dictionary veya balanced değeri de verilebilir. "balanced" verdiğinizde, classların toplam içindeki oranına göre yani prior probabilitysine göre ters mantıkla ağırlıklandırılır, ör: 1000 kaydın 50si "Yes" ise, yani 1'e 20lik bir oran varsa Yes'lerin ağırlığı 20 "No"'ların ağırlığı ise 1 olur.

Burada ayrıca bir dictionary verebiliriz dedik, {0:1, 1:10} şeklinde. Farkettiyseniz bu aynı zamanda bir hyperparametre, dolayısıyla şöyle de tune edilebilir.

weights = np.arange(2,21,2)
{'class_weight': [{0:1, 1:x} for x in weights]}
Out[74]:
{'class_weight': [{0: 1, 1: 2},
{0: 1, 1: 4},
{0: 1, 1: 6},
{0: 1, 1: 8},
{0: 1, 1: 10},
{0: 1, 1: 12},
{0: 1, 1: 14},
{0: 1, 1: 16},
{0: 1, 1: 18},
{0: 1, 1: 20}]}

class weight’leri atayarak modelimizin cost function’ını da değiştirmiş oluruz. Mesela Logistic Regression için normal log-loss denklemi aşağıdaki gibi olup,

The log-loss cost function:

(Burda, 𝑦𝑖 gerçek değerler, 𝑦𝑖y∗ ise targetın predict_proba değerleridir, N de isntance sayısı)

class weight kullandığımızda şu şekle dönüşür:

Weighted Log-loss:

Sonra bunları da f-beta skoruyla gridsearch içine sokabilirsiniz.

Eğitelim

%%time
grid_cw.fit(X_train, y_train)
Fitting 3 folds for each of 294 candidates, totalling 882 fits
Wall time: 1min 16s

Sonuçları görelim

ml.gridsearch_to_df(grid_cw)

Özet ve Karşılaştırma

Birçok metriğimiz oldu. Peki hangisini ne zaman kullanmak gerek?

Ben mümkün olduğunda hangisinin ne zaman kullanmak gerektiğini anlatamaya çalıştım. Bununla birlikte burada ve şurada güzel özetler yapılmış, bunlara bakmanızı tavsiye ederim.

Kaynaklar

Feature importance

Modelimize en çok hangi featurelar katkıda bulunuyor, yani bunların modeli etkileme gücü ne, bunu ölçebiliyoruz. Bu bilgi DecisionTree/RandomForest/XGBoost gibi modellerin içinde embedded bir şekilde geliyor.(featureimportance propertysi ile). Lineear algoritmalarda da(lineer/logistic regresyon, ridge, lasso, elasticnet gibi) coefficientlar(katsayılar) benzer bir görevi görür.(Ama Lineer regresyon notebooknunda görüleceği üzere multicollinearity problemi olmaması lazım, aksi halde bu katsayılar yanıltıcı çıkabilir, her ne kadar modelin tahminleme gücünü etkilemese de)

Diğer algoritmalarda ise permutation feature importance diye bir yöntem var, onu kullanabiliyoruz. Bunlara tree-based algoritmalarda geleceğiz.

Biz şimdi elimizdeki model için bakalım.

gs4.best_estimator_["clf"].coef_[0]
Out[79]:
array([ 0.83022666, 1.0165264 , 0.51356942, -0.76621522, 0.79085121, -0.38583834, 0.24018411, -0.54950579, 0.93215773, 0.11337794])

Hazır fonksiyonumuz yardımıyla feature isimlerini de içerecek şekilde grafiğe dökelim.

ml.linear_model_feature_importance(gs4,coltrans,"fs","clf")

Buradaki yorumlama lineer regresyondan biraz farklı olacak, zira elimizde bir class label'ı ve bunun olasılık hesabı var, bunda da logit/sigmoid fonksiyonu kullanılıyor.

Mesela Ca'daki 1 birim artış logit(p) değerinde 0.93 birimlik artışa denk deriz ama bu tam olarak ne anlama geliyor, buna bakalım. Burda log'un tersini alarak ilerlemek lazım. Yani np.exp(0.93)=2.53, bundan 1 çıkaralım, 1.53, yani Ca'daki 1 birimlik artış kalp hastalığı riskinde %153'lük bir artışa denk geliyormuş. Binary bir variable olarak Sex'e bakalım, bunda 1 birim artış lafı anlamsız, bunu 0'dan 1'e çıkmak olarak dile getirelim, 0 sanırım Kadındı, 0'dan 1e çıkmak, yani kadın yerine erkek olmak, kalp hastalık riskini np.exp(0.83)=2.29, yani %129 artırıyormuş.

Şurada ve şurada Logistic Regression katsayılarıyla ilgili daha açıklayıcı bir bilgi bulabilirsiniz.

Interpretability ve Explainabilty

Feature importance ile yakından ilişkili bir konu da modelin yorumlanabilirliği ve açıklanabilirliğidir. Aslında bunlarla karıştırma ihtimali olan bir de fature selection konusu var. Şimdi üçünü birden kısaca bi karşılaştıralım.

  • Feature selection, “modele en çok hangi featurelar katkıda bulunur?” sorusunu model oluşturmadan önce(filter ve wrapper yöntemler) veya model oluşturma sırasında(RFE veya embedded yöntemlerde olduğu gibi) belirlemeye çalışır.
  • Feature importance ise model oluştuktan sonra bir featureun model için ne kadar önemli olduğunu söyler.
  • Interpretability/Explainability konuları ise, belirli bir instance için neden o karar çıktı, onu gösterir. Ör: Bir müşteriye neden kredi çıkmadı, hangi özellikleri nedeniyle. Mesela finansal bir modelde en yüksek öneme sahip olan feature “mevcut borç miktarı” olsun. Bu ne kadar yüksekse kredi alma ihtimali o kadar düşüyor. Ama bi müşteriye, hiç borcu olmadığı halde kredi çıkmamış olabilir. Bunun nedenini bu iki kavram bize sağlamaya çalışır.

Interpretability ve Explainability arasında da bazı farklar olmakla birlikte, özetle ilki, Desicison treelerde if’li söylemlerle ifade etmek veya lineer modellerde coefficintların önem derecesiyle “dile getirilebilirken”, ikincisi o kadar kolay değildir ve ekstra yöntemlere ihtiyaç duyar, DeepLearning modellerinde olduğu gibi. İlk kavram white-box modeller için geçerliyken ikincisi black-box modeller için geçerlidir. Bu ikisi arasındaki farklar için kaynaklardaki ilk 3 linke bakmanızı öneririm.

Şunu da söylemek gerekir ki, modelin performansı ile interpretability’si arasında bir trade-off vardır. Modelin karmaşıklığı ve yorumlanabilirliği azaldıkça başarısı da artar, ve tersi de doğrudur. Şimdi şunu düşünebilirsiniz, “O zaman tüm kurumlar en kompleks, en black-box modelleri kullanıyordur”. Cevap hayır, çünkü mesela BDDK gibi regulatif kurumlar bankaların açıklanabilir modeller kullanmasını ister, buna zorlar. Ör:Bir müşteriye neden kredi vermediğini açıklayabilmesini ister.

Bununla birlikte, bu bölümde göreceğimiz gibi SHAP ve LIME gibi toollar aracılığı ile modellerin açıklanabilirliği çok iyi şekilde yapılabilmektedir. Bunlar, satır bazında açıklanabilirlik veren yaklaşımlar olduğu için çok kıymetlidirler. Ama buna rağmen regulatif kurumlar neden hala white-box modellerde ısrar ederler, bu konuda bir fikrim yok açıkçası.

Biz burada sadece Shap’den bahsedeceğiz. Lime ve başka tool’ları araştırmayı size bırakıyorum. Kaynaklarda bunlarla ilgili linkleri bulabilirsiniz.

EDIT: Bu konuyu şimdilik buraya değil de RandomForest notebook’una almaya karar verdim. Siz yine de önden bakmak isterseniz diye kaynakları bırakıyorum. İleriki bir zamanda elimizdeki model için de buraya SHAP yaklaşımını koymaya çalışacağım.

Kaynaklar

Interpretability vs Explainabilty

General concept

SHAP

LIME ve diğerleri

Deployment

Tüm verisetiyle Final Model eğitimi

Hyperparametrelerimizi tune ettikten sonra nihai modelimize karar vermiş olduk. Şimdi bu modeli tüm verisetiyle, yani daha fazla miktarda data ile eğiterek öğrenim gücünü artırmaya çalışalım ve bu haliyle, ilave teste tabi tutmadan deploy edelim.

#en iyi parametre kombinasyonuna tekrar bakalım
gs4.best_params_
Out[83]:
{'clf': LogisticRegression(C=0.5, max_iter=1000, random_state=42),
'clf__C': 0.5,
'clf__penalty': 'l2',
'clf__solver': 'lbfgs',
'ct__numerics__ouh': OutlierHandler(featureindices=[1]),
'ct__numerics__scl': StandardScaler()}

En iyi parametre setiyle yeni bir model objesi yaratalım.

finalmodel=LogisticRegression(C=0.2, max_iter=1000, random_state=42, solver='newton-cg' ,penalty="l2")

Şimdi modelimizi tüm data ile eğitelim.

finalpipe.fit(X,y)

Local’e Kaydetme ve Yükleme(Çağırma)

Pickle yöntemi

Mevcut pickle modülü yerine bunun sklearn replacementı olan joblib’teki dump ve load fonksiyonlarını kullanacağız.

#sadece modeli kaydetme
from joblib import dump, load
dump(finalmodel, 'finalmodel.pkl')
Out[87]:['finalmodel.pkl']

Ama bizim modeli değil pipeline’ı kaydetmemiz lazım, zira production ortamında bu pipeline’ı çağıracağız, tüm preprocessingler yapılsın diye.

dump(finalpipe, 'finalpipe.pkl')Out[88]:['finalpipe.pkl']
#geri yüklemek için
loaded_model = load('finalpipe.pkl')

Şimdi de yeni bir input verip bunu tahminleyelim. Ama önce inputun shape’i nasıl olmalı, ona bakalım.

X_test.iloc[[0],:].shape #olması gereken input shape böyle
X_test.iloc[0,:].shape #bu değil
Out[90]:(1, 13)
Out[90]:(13,)

Sahadan yeni gelen bir veriyi temsilen X_test’teki ilk satırın aynısını alalım.

loaded_model.predict(X_test.iloc[[0],:])Out[91]:array(['Yes'], dtype=object)

Pickling alternatifleri

Şurada belirtildiği gibi pickle yönteminin bazı dezavantajları var. Ona alternatif olarak şu yöntemlerle locale kaydetme yapılabilir.

  • json tercih edilebilir
  • keras(veya tensorflow) kullanıyorsak, bunun kendi save metodu kullanılabilir
  • pmml kullanmak tercih edilebilir

Web Deployment

Modelleri locale kaydedip sahadan gelen gerçek verileri localde yaptığımız gibi sonra tekrar load ederek tahminleme yapmak çok amatörce olacaktır. Bunun için kurumunuzun yazılım ekipleriyle görüşüp modelinizin production ortamına alınması gerekecektir. Biz burada bu kadar profesyonel bir iş yapmayacağız ama onun yerine daha orta seviyede bir iş yapıp, projemizi web ortamında nasıl çalıştırırız, ona bakacağız.

Böylece, yaptığınız bir projeyi başkalarıyla(belki de iş görüşmesi yaptığınız kişilerle) paylaşma imkanınız da olacaktır.

NOT:
Bu kısım biraz daha programcı ve veri mühendisliği yeteneklerini gerektiriyor. O yüzden isterseniz şimdilik es geçin, ML konusunda kendinizi geliştirdikten sonra tekrar ziyaret edin. Ben de şuan bu kısma çok eğilmemeye karar verdim, sadece birkaç kaynak koyacağım, ilerleyen dönemlerde burayı açıklama ve kod örnekleriyle zenginleştirmeye çalışacağım.

Flask ile deployment

Streamlit ile deployment

Docker Container ile deployment

Exe dosyası yaratma

Bir diğer alternatif de projenizi webde değil de masaüstü bir program olarak çalıştırmak olabilir.

Kaynaklar

Genel Concepts

3rd party webs/apps

Deploying on other environments(ör:asp.net)

Faydalı Kaynaklar

Kitaplar

  • Hands-On-Machine-Learning-with-Scikit-Learn-Keras-and-Tensorflow (Aurelien Geron)
  • The Elements of Statistical Learning (Trevor Hastie, Robert Tibshirani, Jerome Friedman)
  • An Introduction to Statistical Learning (Gareth James, Daniela Witten, Trevor Hastie, Robert Tibshirani)

Online

Okuduğunuz için teşekkürler

--

--

Volkan Yurtseven

Once self-taught-Data Science Enthusiast,now graduate one