Makine Öğrenimi Felsefesi-6

Uçtan uca tam örnek — Classification Part I

Volkan Yurtseven
38 min readJun 2, 2022
Photo by Mick Haupt on Unsplash

Serinin bu bölümünde artık Makine Öğrenmesi dünyasını daha yakından tanıyacağız ve tam anlamıyla uçtan uca bir örnekle konuları pekiştireceğiz. O yüzden çoooooook uzun bir gönderi olacak.

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, ilk başta data temini ile başlayıp, arkasından EDA ile devam edeceğiz, sonra ara işlemleri takiben detaylı bir preprocessing aşamasına dalacağız.

Daha sonra da modelleme kısmı geliyor. İlk olarak model selection konusunu detaylıca göreceğiz, ki burada cross validation ve gridsearch gibi yöntemler yer alıyor olacak.

Model değerlendirme ve sonraki aşamalar ise Part II’da olacak. Part II 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.

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

Data temini

https://archive.ics.uci.edu/ml/datasets/heart+disease sayfasında detayları anlatılan veri setini kullanacağız. Devam etmeden önce buraya bakmanızı öneriyorum.

Benim özetlemem gerekirse; Cinsiyet, Göğüs ağrısı tipi gibi belli bilgileri kullanarak bir kişinin kalp hastalığına sahip olup olmadığını tamhinlemeye çalışacağız.

dfheart=pd.read_csv("https://raw.githubusercontent.com/VolkiTheDreamer/dataset/master/Classification/Heart.csv")
dfheart.head()
len(dfheart)
#303

EDA

Öncelikle verisetimizi biraz keşfedelim, ne var ne yok, neler birbiriyle korele v.s bakalım

Genel

dfheart.super_info_() #kendi yazdığım extension metodum.

Bu extension metodların kullanım detayları için buraya bakınız.

Yorumlar

  • İlk kolon olan Unnamed:0 gereksiz, bunu sileceğiz.
  • Son kolon AHD, target classımız, kişinin kalp hastalığı geçirip geçirmediğini söylüyor.
  • Bir kategorik(Thal) bir de numerik(Ca) kolonda missing value var.
  • Birkaç kolon numerik olmasına rağmen unique değer adedi çok küçük olduğu için bunların kategorik olarak ele alınması gerekecek. Ör:Sex 1:Male, 2:Female demekmiş
del dfheart["Unnamed: 0"]

Cardinalitesi düşük olanlara bakalım.

da.getColumnsInLowCardinality(dfheart)
  • binaryler zaten aşikar, bunlar categoric olmalı.
  • Dataset açıklamasında Chestpain için bi sıralamadan bahsetmiş, bunları ordinal veri olarak düşüneceğiz.
  • Thal değeri için sıralama var mı emin değilim, bunu nominal alacağız.
  • Ca: tanımda damar sayısı olarak verdiği için bunu categoric değil numeric olarak ele almak lazım, ama dikkat edelim ki imputation yaparken tam sayı değer olması gereken bu alana mean alma nedeniyle küsuratlı birşey gelmesin, o yüzden medianla imputation yapmak gerekecek. (Ben imputation gördüm, babamın imputationı var; merak etmeyin bunları sırayla göreceğiz)
  • restecg: ordinal gibi duruyor.

Şimdi veri türlerimizi belirleyelim.

target=["AHD"]
nums=["Age","RestBP","Chol","MaxHR","Oldpeak","Ca"]
cats=list(dfheart.columns).removeItemsFromList_(nums+target,False) #extension metodum
ords=["ChestPain","RestECG"]
noms=cats.removeItemsFromList_(ords,False)
nums,cats,ords,noms

Visual

sns.pairplot(dfheart[nums],height=1, aspect=1.2);
  • Oldpeak bayağı bi skewed görünüyor, sağ uçta biraz outlier da var. Chol’da da hafif bi skewness var ama o daha çok outlier gibi duruyor.
  • Featurelar arasında pek bi korelasyon da yok gibi, multicollinearity problemimiz olmayacak.
dfheart[nums].plot(kind="box", subplots = True,figsize=(8,5))
plt.tight_layout();

Pairplottaki gözlemimizi doğrulamış olduk, ancak pairplotta çok dikkat çekmeyen Restbps’de de outlierlar dikkat çekiyor

#mükerrer varmı
len(dfheart)-len(dfheart.duplicated(keep=False))
#0
#nulların konumuna bakalım, yanyana mılar yoksa ayrı instancelardalar mı
da.nullPlot(dfheart)

Targetın dağılımına bakalım

sns.countplot(x=dfheart["AHD"]);

Dengeli bir dağılım var gibi, imbalanced dataset problemimiz yok.

Target bazında numeriklerin ortalama değerlerine bakalım.

da.plotNumericsByTarget(dfheart,"AHD",nums=nums,layout=(1,6),figsize=(12, 4))

Oldpeak ve Ca, target bazında oldukça farkediyor, bunların feature importance’ı önemli olacak gibi duruyor. Korelasyon analizinde bu durumu muhtemelen teyit ederiz gibi.

Kategorik kolonlar üzerinden baktığımızda ilgili kategorilerdeki her bir değer için kalp hastalığına yakalanma olasılıklarına(prior probability) bakalım.

plt.figure(figsize=(14,8))
da.plotTargetByCats(dfheart, cats, "AHD", subplot_tpl=(2,4));

Bu grafikleri şöyle okumak lazım. Öncelikle bunların percentage olduğunu unutmayın:

  • Kadınlarda(0) risk daha düşük, erkeklerde belirgin bir yükseklik/düşüklük yok.
  • Göğüs ağrısı asemptomatik dışındaki olanlarda risk düşük.
  • fbs için belirgin bir risklilik durumu yok
  • RestECG ortadaki değerde risk yüksek ama biz buna ordinal demiştik, çok da emin olamadım açıkçası, biz yine de ordinal gibi davranalım buna.
  • Ex Ang=1 olanlarda risk yüksek, 0 olanlarda düşük
  • Slope’u 1 yani up olanlarda risk düşük, 2-flat olanlarda yüksek
  • Thal’i normal olanlarda kalp hastalığı olma riski olmamasına göre daha düşük. reversible ve fixed olanlarda ise risk yüksek. Ama bu demek değil ki en çok hasatlık sahibi olanlar Thal’i reversible ve fixed olanlardır; buna aşağıdaki grafikte bakacağız.

Şimdi de kalp hastalığı olanların kategoriler bazındaki dağılımına bakalım.

plt.figure(figsize=(14,8))
da.plotPositiveTargetByCats(dfheart, cats, "AHD", subplot_tpl=(2,4));

Yorumlar

Kalp hastalığı olanların çoğunluğu;

  • Thal değeri reversable
  • Erkek
  • slope=2
  • Fbs=0
  • göğüs ağrısı asemptomatik olanlar

Korelasyonlar

Şimdi de korelasyonlara bakalım, bunun için dython kütüphanesinden faydalanacağız. Zira bu kütüphane ile hem numeric-numeric, hem numeric-kategorik, hem de kategorik-kategorik korelasyonlar tek bi fonksiyonla elde edilebilmektedir.

from dython.nominal import cramers_v, correlation_ratio, associations
corrdict=associations(dfheart,nominal_columns=cats,numerical_columns=nums,figsize=(10,10))

Targetle korele olan en yüksek N adet featureu görelim.

corr_results=corrdict["corr"] #dataframe
da.getHighestPairsOfCorrelation(corr_results,"AHD",5) #şimdilik N=5 aldım , modellerken bu rakamı değiştirebiliriz
Out[21]:
Thal 0.515606
ChestPain 0.510832
Ca 0.460033
Oldpeak 0.424510
ExAng 0.421617

Bu korelasyon değerlerinden feature selection aşamasında yararlanabiliriz. Bu arada, üstteki grafiklerde Ca ve OldPeak’in yüksek öneme sahip olacağını söylemiştik, gerçekten de bunların korelasyonu da yüksek çıktı.

Varsayımların kontrolü

Bazı algoritmaları modellerinizde kullanabilmek için bazı varsayımların(ön şartların) sağlandığını kontrol etmeniz gerekir. Bu notebookta ana algoritma olarak Logistic Regression’ı seçeceğiz. Onun varsayımlarına bakalım. Bu algoritmanın detaylarını şurada bulabilirsiniz.

  • Lineer Regresyonda olduğu gibi instanceların birbirinden bağımsız olması beklenir: Tıp dünyasından olmadığımız için bilmiyorum ama bu varsayımın karşılandığını varsayalım.
  • Yine LinReg’de olduığu gibi featureler arasında collinearity olmaması gerekir: EDA’da multicollinearity gözlemlemedik.
  • Outlierlara karşı duyarlıdır, dikkatlice ele alınması gerekir: Bir iki kolonda outlier vardı, bunlara bakacağız.
  • Scaling’e duyarlıdır: Ölçeklerde farklılık var, scale edeceğiz.
  • Instance sayısı feature sayısının en az bi 10–15 katı olmalıdır: 303 / 13 = 23 kat, ok.

Dengeli bir dataseti mi

Buna EDA’da baktık ve dengeli olduğunu gördük. Dengesiz bir datasetinde bu durum nasıl ele alırdık bunu görmek isterseniz şu linke bakınız.

X ve y ayrımı

Modellerimizde iki ayrı input veririz. Birisi bağımsız değişken olan featurelar/prediktörler, diğeri de bağımlı değişken olan labellar, yani target. Genel notasyon, tüm prediktörleri X ile, target’ı y ile göstermek şeklindedir.

X=dfheart.iloc[:,:-1]
y=dfheart.iloc[:,-1]
X.shape, y.shape
#((303, 13), (303,))

X’ler için bi notum var, eğer tek bir feature varsa bunu yine de iki boyutlu vermek lazım, bunun için şu iki yöntemden biri uygulanabilir.

#X=dfheart.iloc[:,[0]] #0'ı [] içine almak
#X=dfheart.iloc[:,0].values.reshape(-1,1)

y’nin çoğu durumda tek boyutlu olması yeterlidir. Olur da iki boyutlu olması gerekirse X’te yaptığımız aynı yöntemle yapabilirsiniz.

Train/test setleri

Ana sürecimizde belirttiğimiz gibi, bundan sonra devam etmeden elimizdeki veri setini, eğitim ve test seti olmak üzere ikiye ayırmamız gerekecek ve model evaluation adımına kadar bu test setine asla dokunmayacağız.

#geleneksel yaklaşımla başlayalım
from sklearn.model_selection import train_test_split

train_test_split fonksiyonu, ki aslında ShuffleSplit sınıfının split metoduna ait bir wrapper fonksiyondur, yani ShuffleSplit.split() ile aynı işi yapar. Bu fonksiyon, kendisine verilen istenilen sayıdaki diziyi train ve test olmak üzere belirlenen oranda ayırır. Sadece X verirseniz X_train ve X_test elde edersiniz, X ve y birlikte verirseniz y'nin de train ve test setini elde edersiniz.

#hızlıca bi test edelim, nasıl çalıştığını görelim
sonuc=train_test_split(X,test_size=0.2, random_state=42)
len(sonuc) # 2 çıkacak, biri train seti biri test seti
#2

Buradaki test_size, datasetin ne kadarını test için ayırıcaz, bunu gösterir. random_state değeri ise şu işe yarar: Bu işlem dataframein rasgele bölgelerinden seçilerek yapılır ve her çalıştırdığımızda aynı sonucu görmek istersek, ki istemeliyiz, çünkü ara ara modelimizi tekrar tekrar çalıştıracağız, hep aynı yerden bölündüğünde emin olalım ki, sonuçlarımız uyumlu olsun ve gerekli değişiklikleri yapabilelim. Bu random_state parametresine herhangi bi değer atanabilir, 1,3,5,42, farketmez. Bir de burda görünmeyen(o yüzden default değerini alır) shuffle parametresi var ki, rasgele bölgelerden seçme işini buna True(default bu) diyerek yapıyoruz, False olsaydı, datanın en alttan %20'sini eğitime ayırıp ilk %80ini train yapacaktı. Biz genelde bunu True bırakacağız, bildiğim kadarıyla bir tek Time Series problemlerinde False yapılır, o problemin doğası gereği.

Evet, şimdi bu fonksiyonun yaygın kullanım şeklinde bakabiliriz.

X_train, X_test=train_test_split(X,test_size=0.2, random_state=42)
X_train.shape, X_test.shape
#((242, 13), (61, 13))

veya daha yaygın haliyle

X_train, X_test, y_train, y_test=train_test_split(X,y,test_size=0.2, random_state=42)
list(map(np.shape, (X_train, X_test, y_train, y_test)))
#[(242, 13), (61, 13), (242,), (61,)]

tiplerine bakalım

list(map(type, (X_train, X_test, y_train, y_test)))

X’ler çok sayıda kolondan oluştuğu için Dataframe iken, y’ler tek kolonlu olduğunda Series’dir. Eğer y’yi oluştururken bir de values deseydik o zaman numpy array olurdu, ki bazen dememiz gerekecek.

Önemli: Bu şekilde random bir şekilde eğitim ve test setlerimizi böldük. Birçok yerde de bu şekilde anlatılır ve geçilir ancak durum her zaman bu kadar basit değil. Mesela, bu dataseti için size danışmanlık veren bir hekim Thal değerinin sonuçlara çok etki ettiğini söylemiş olsun veya EDA ile bunu siz keşfetmiş olun. Böyle bir durumda sizin öyle bir test seti ayırmanız lazım ki, ana kitleyi temsil etsin. Bunun için stratified sampling denen bir yöntem var, bunu yapmalısınız. Bazen de veri setiniz imbalanced olduğunda, yani target kolonda minority class majoritynin çok küçük bir oranına denk geliyorsa, yine test seti bu target classa göre orantısal bölmeniz lazım.

Peki ya hem aynı anda target’a hem de ExAng featureına göre orantısal bölmek isteseydik? Kaynaklara bakın, orda bi link bulacaksınız. Biz burada sadece Thal’e göre stratify bölelim.

#Thal'de null değerler olduğu için fillna yapıyoruz, stratification için gerekli
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)

Aşağıda bir yerde yanlışlıkla dfheart’ı kullanmayalım diye onu siliyorum, pipeline yaparken tekrar oluşturacağız zaten.

del dfheart

Kaynaklar

Preprocessing / Feature engineering

Ön bilgiler

fit ve transform kavramları

Daha önce, model kurmanın matematiksel bir model oluşturmak olduğunu söylemiştik. Başka bir ifadeyle, datamızı modele uydurmuş oluyoruz. Buradaki uymak ifadesi önemli, zira Python’da bu ifade, İngilizce karşılığı olan fit metodu ile karşılık buluyor.

Ama bir modeli fit etmek ile preprocessing adımında yaptığımız fit ve transform işlemleri birbirinden farklıdır.

Preprocesingdeki fit ile bir hesap yaparız, mesela imputation yapacaksak, her bir kolonun ortalama değerlerini hesaplamış oluruz ve bunlara imputer objesinin statistics_ attribute’i ile ulaşabiliriz. Bir de üzerine transform yapınca, veri setindeki tüm null alanlar bu ortalama değerlerle replace edilir. sklearn kütüphanesinde iki işlemi ardışık yapan bir de fit_transform metodu bulunur.

Genel kullanım şöyledir:

X_train=preprocesser_objesi.fit_transform(X_train) -->train setinde fit_transform
X_test=model.transform(X_test) -->test setinde sadece transform. Yani diyoruzki X_train'den öğren ve uygula ama sonra X_test için yeniden öğrenme, trainde öğrendiğin değerleri kullan, veri sızıntısı yapma.

Bir model fit ettiğimizde bunu bir değişkene atamaya gerek yoktur, zaten dönen şey modelin kendisidir. Modelin eğitilmesiyle birlikte eğitim sonrası oluşan attributeler(“_” ile bitenler) kullanılabilir hale gelir.

Supervised modeller sadece fit edilirken, unsupervised modeller(clustering v.s’ye ek olarak scaling gibi tüm feature processing işlemleri dahil) ayrıca transform da edilir. Transform ile elimizdeki verinin artık fit edilmiş modele göre yeni bir temsilini elde ederiz. Fit’ten farklı olarak transform ettiğimizde yeni sonuçları bir değişkene atamak gerekir, ki bu yeni değişken artık transform edilmiş halini tutabilsin.

Input Formatı

EDA ve Preprocessing postumda, Makine öğrenme algoritmaları hemen herşeyi sayı olarak içeri alır, buna göre model oluşturur demiştik. Ancak bu tek şart değildir. Bu sayıların formatı da önemlidir. İşte, algoritmadan algoritmaya değişmekle birlikte çoğu zaman input listesinin 2 boyutlu bir matris olması gerekir. 2'den fazla değişkenin olduğu durumda bu zaten mecburen böyledir, ancak tek boyutlu input olan ve özellikle öğretim amaçlı hazırlanmış veri setlerinde bazen tek bir değişken kullanılır. Bu tek değişkenin de 2 boyutlu hale getirilmesi gerekir. Bunun için de dataframein ilgili kolonunun values özelliği ile değerleri alınırken reshape(-1,1) ile 2 boyutlu hale getirilmesi gerekir.

Bunun dışında yine algoritmadan algoritmaya değişmekle birlikte, inputlar bazen numpy dizisi(dataframede bir kolonun values özelliği ile elde edilir), bazen de dataframe olarak modele sokulabilir.

İşlem sırası

Burdaki işlemlerin net bir sırası olmamakla birlikte, farklı kaynaklardan derlediğim ve kendi tecrübemle harmanladığımda şu sıralama uygun gibi görünmekte. Duruma göre küçük ayarlamalar yapılabilir. Hatta vakit varsa, bazı işlemlerin sırasını değiştirerek denemek ve sonucu görmek de faydalı olabilir. Ne de olsa Machine Learning ampirik bir alan, birçok şey deneme yanılma ile yapılıyor(GridSearch konusunda bunu daha iyi anlayacaksınız)

Biz bu kısımda, bunları önce tek tek yapmayı öğreneceğiz, sonra pipeline kullanarak tek seferde nasıl yapıldığını göreceğiz.

Benim düşündüğüm sıra şu şekilde:

  • data cleaning(Kirli data hep gelecekse, bu da pipeline içine konur, aksi halde tek seferde bitirilir)
  • imputation(missing value handling) veya deletion: outlier treatten önce olmalı, IQR’ları etkiliyor
  • outlier handling (Scalingden önce olmalı)
  • gerekiyorsa discretization(onehot öncesinde yapılmalı, ki bunlar da encode olabilsin)
  • feature creation/extraction
  • feature selection/dimension reduction
  • encoding(categoricler için sırası çok kritik değil, ordinallerse scalingden önce olabilir)
  • distribution transform for skewed data(scaling öncesinde yapmak lazım ki normal dağılıma uygun hale gelsin)
  • scaling
  • ovesampling/undersampling for imbalanced data (bu notebookta yapmayacağız)

ÖNEMLİ NOT: Bu işlemler Pipeline denen yapılarla disiplin altına alınır ve hem train hem test için uygulanabilir olması nedeniyle çok verimlidirler. Ancak bu notebook, eğitim amaçlı olduğu için öncelikle herşeyi ayrı ayrı göreceğiz, Pipelineları daha sonra göreceğiz. Bu arada burdaki süreç kesin bir süreç olmayıp duruma göre küçük yer değiştirmeler yapılabilir. Ör:Feature selection olarak Embedded-Lasso seçilirse, bunu öncesinde scaling yapmak gerekebilir.

Şurada da güzel bi özet var, oraya da bakmanızda fayda var.

Bu aşamadaki işlerin büyük kısmını geleneksel kütüphanelerle(pandas, numpy, sklearn, scipy) ile yapmaya çalışacağız. Bununla birlikte iki ayrı kütüphane daha keşfettim, eminim dahası da vardır. Bunlara da bakılabilir.

Önce train/test sonra feature engineering yapma sebebimizi merak ediyorsanız, şu linklere bakabilirsiniz.

Data Cleaning

Aşağıdakine benzer temizlik işlemleri yapılabilir.

  • Mükerrer kayıtları silme
  • Hatalı kayıtları silme
  • veri kalitesi problemlerini düzeltme(veri yönetişimi/data governance çalışmaları önemli):
  • “Direkt Bnk”, “Direkt Bank”, “DIREKT Bank.”
  • A kolonu B’den büyük olmaması gerektiği halde durum buysa
  • A kolonunda bulunmaması gereken değerler varsa (Ör:il kodlarında 89, sıcaklık değeri olarak 500)
  • Dönüşüm işlemleri. Ör: 100 TL → 100
  • Veri tipi dönüştürme

Bizim datamızda var mı diye bakalım.

#datanın sınırlarını görelim, anormal bişey var mı
X_train.describe()
#categoriklerde de uniqe değerlere bir daha bakalım
da.getColumnsInLowCardinality(X_train)

Numerik sınırlar ve kategorik değerler makul görünüyor. Bi işlem yapmaya gerek yok.

Imputation

Verisetindeki null alanların nasıl ele alınacağı aşamasıdır.

  • En başta yaptığımız korelasyon analizine bakıp bir featureun önem derecesine göre ne yapacağımıza karar verebiliriz. Mesela target’a çok az etkisi olan bi kolonsa ve sayı fazla ise o kolon silinebilir.
  • Numerik ve kategorik kolonlar ayrı ele alınmalıdır.
  • outlierları etkilediği için önce bunları yapmak lazım
  • median yerine mean kullanılacaksa ve ortalamayı etkileyen büyük outlierlar varsa önce onlar halledilmelidir.
  • NaN,<NA>, nan, None ayrımına dikkat. Bunlarla ilgili şu linke bakmanızda fayda var.
  • Bazen boş değerler null anlamına değil özel bir anlama sahip olabilir. Ör: Bankacılıkta kredili müşterinin en son ne zaman takibe girdiği bilgisi null ise bu hiç takibe girmediği anlamına gelebilir. Bunu veri sahibine sorup öğrenmek gerekecektir. Duruma göre bunlar için feature extraction aşamasında bir kolon açılıp bu bilgi yazılabilir. Ör:DahaÖnceTakip Flag gibi bir feature.(Bu işlem, SimpleImputer’ın add_indicator parametresi ile de sağlanabilir)

categorikler için alternatifler

  • en sık yapılan ama en basit ve yetersiz olan yöntem mod ile impute etmek olabilir
  • bir alternatif en önemli kolona göre veya targeta göre mod aldırabiliriz, özellikle mod açık ara öndeyse
  • bir alternatif de classification uygulamak olabilir, yani bi classification algoritması ile bunları tahmin etmeye çalışabiliriz(Model içinde model)
  • bir alternatif de onehot encoding yapıp sonra iterative imputation yapmak olabilir ama bunun da kendi içinde olumsuzlukları var
  • Eldeki datada null olmayan satırları alabilir, bunlardan ilgili kolonlarda random null yaratıp bunları test etmeye çalışabiliriz, sonra gerçek değerlerle karşılaştırıp en az hatalı imputation yöntemini seçebiliriz.
  • Missing diye bi kategori yaratabiliriz.

numerikler için alternatifler

  • En yaygın ama bence yine yetersiz olan alternatif mean veya daha iyisi medianla replace etmek, zira median outlilerlara duyarlıdır.(Biz bu notebookta işlem basitliği olması adına bu yöntemi tercih edeceğiz, diğer yöntemleri kullandığım notebooklara da link vereceğim)
  • ilgili kolonun full mean/medianını almak yerine belli kategoriler bazında mean/median alınabilir
  • Bir başka alternatif regression uygulamak olabilir(manuel veya IterativeImputer kullanarak)
  • KNNImputer uygulanabilir
  • Hatta korelasyon bile kullanılabilmektedir(kaynaklara bakın)

Bu yukarıdakilerin hepsi replace yöntemidir. Alternatif olarak “en az %xi boş olan kolon veya satırları sil” yaklaşımı da benimsenebilir.

veya bunların hepsi gridsearch içinde denenebilir.

Bunların birçoğunu şu notebookta denedim.

Biz burada çoğunlukla impute modülündeki SimpleImputer sınıfı üzerinde ilerleyeceğiz. Imputation’ı sadece null olan kolonlarda uygulamak yeterlidir. Ancak eğer şuan datada null yoksa fakat ilerde gelme ihtimali olduğunu düşündüğünüz kolonlar varsa onlara da, hatta tüm X’lere uygulanabilir.

numerikler ve categorikler için iki ayrı işlem yapılacağı için iki ayrı imputer objesi yaratıyoruz.

from sklearn.impute import SimpleImputer
numimp = SimpleImputer(strategy="median")
catimp = SimpleImputer(strategy="most_frequent")
#datasetimiz küçük olduğu için tüm kolonlarda imputation yapabiliriz
numimp.fit(X_train[nums]) #daha önce belirttiğimiz üzere burada fit ederek median hesaplanmış oluyor
#şimdi bu medianları görelim
numimp.statistics_
Out[33]:array([ 56. , 130. , 243.5, 152.5, 0.6, 0. ])
#şimdi de bu değerleri verisetindeki missinglere yerleştirelim
X_train_num_imp = numimp.transform(X_train[nums]) #bu noktada artık Dataframe değil numpy array olur.
#fit ve transformu bir arada yapan fit_transform metodu da mevcuttur ve genelde bu kullanılır
X_train_num_imp[0]
Out[34]:array([ 67., 152., 277., 172., 0., 1.])
#sonuçları dataframe üzerine tekrar yazabiliriz
X_train[nums] = X_train_num_imp
X_train[nums].isnull().any().sum() #kontrol edelim
Out[35]:0
#cats için olanı fit_transform ile tek seferde yapalım
X_train_cat_imp = catimp.fit_transform(X_train[cats])
catimp.statistics_
X_train_cat_imp[0]
Out[36]:
array([1, 'asymptomatic', 0, 2, 0, 1, 'normal'], dtype=object)#indeklsere de ihtiyaç olursa
array([0, 'nonanginal', 0, 0, 0, 1, 'normal'], dtype=object)
X_train[cats] = X_train_cat_imp
X_train[cats].isnull().any().sum() #kontrol edelim
Out[37]:0

İlave Kaynaklar

Outlier handling

Tespit kısmını EDA’da görmüştük. Şimdi bunlarla ne yapacağımıza bakalım.

  • Mean/Median Imputation: Eğer outlierların hatalı olduğunu düşünüyorsak bunlara missing/null valuelar gibi davranabiliriz.
  • Trimming/Deletion: Bunların sayısı toplam instance sayısına oranla çok düşükse bunları silebiliriz.
  • Top, Bottom Coding(Cap uygulama): Bir çeşit imputation, fakat Q1–1,5IQR ve Q3+1,5*IQR gibi değerlere(veya tespit edeceğiniz başka bir değere) sabitlenir.
  • Discretization:Veri setini baremlere(bin’lere) ayırma
  • Robust Scaling:MinMax veya StandartSacaler yerine RobustScaler kullanılabilir.
  • Data transformation: Log transformation veya Box-Cox transformation. Ancak bu daha çok skewness çözümüdür, outlier çözümü değil. Çoğu yerde bu da seçenekler içinde diye ben de koydum, ama bunu kullanmayın derim.

NOT: Sadece mevcut datada değil, yeni datada da outlier var mı kontrolü de önemlidir, ki pipeline içindeki outlier handler’ımız bunu yapıyor olacak.

Önce bi histogram ile bakalım

plt.figure(figsize=(5,2))
plt.hist(X_train.RestBP);

Çok büyük bir skewness yok, ama yine de log alıp ne olacağını görelim.

plt.figure(figsize=(5,2))
plt.hist(np.log10(X_train.RestBP));

log alınca outlierlar gitmiş gibi görünüyor, ama emin olmak içn bir de boxplot ile bakalım

#logsuz
plt.figure(figsize=(5,1))
plt.boxplot(X_train.RestBP,vert=False);
#loglu
plt.figure(figsize=(5,1))
plt.boxplot(np.log10(X_train.RestBP),vert=False);

hala bi miktar outlier görünüyor. Veri sayımız çok olmadığı için silme alternatifini düşünmeyeceğim. O zaman cap koymayı deneyelim.

Q1 = X_train["RestBP"].quantile(0.25)
Q3 = X_train["RestBP"].quantile(0.75)
IQR = Q3-Q1
topRestBP=(Q3 + 1.5 * IQR)
bottomRestBP=(Q1 - 1.5 * IQR)
X_train["RestBP"]=np.where(X_train["RestBP"]>topRestBP,topRestBP,X_train["RestBP"])
X_train["RestBP"]=np.where(X_train["RestBP"]<bottomRestBP,bottomRestBP,X_train["RestBP"])
plt.figure(figsize=(5,1))
plt.boxplot(X_train.RestBP,vert=False);

Aynı işlemler Chol feature için de yapılır.(Bunlar notebookta var, buraya koymadım)

Bu cap koyma işlemini nihai pipeline içinde custom bir function veya class olarak yerleştireceğiz.

İlave kaynaklar

Discretization

Numerik kolonların kategorik kolonlara çevrilme işlemidir. Bu işlem çok küçük farkları olan numerik değerlerin target üzerinde önemli bi fark yaratmadığı durumda yapılır. Mesela 25 yaşında olmakla 27 yaşında olmak çok bi fark yaratmıyorsa bunları 25–30 gibi bir yaş grubunda biraraya getirebiliriz.

Genel olarak yapılma sebebini şöyle sıralayabiliriz:

  • Çalıştığımız problemde ağırlıkla kategorik değerler vardır, kalan 1–2 değer de target üzerinden çok fark yaratmıyorsa, ve algoritmamız sadece kategorik veri isteyen bir algoritmaysa
  • kategorik targetle ilişkisini daha iyi yorumlayabilmek için
  • Signal-to-Noise Ratio oranını düşürmek, yani rakamlardaki küçük dalgalanmaların model sonucunu etkilemesine izin vermemek için
  • Outlier handling amacıyla

Türleri

Unsupervised

  • Equal-Width:Her birinin genişliği sabit olan N adet grubua ayırma. width=(max-min)/N
  • Equal-Frequency:Yine N grup, ama bu sefer hepsindeki genişlik farklı olabilir, ama gruplar içindeki elema sayısı aynı.
  • K-Means

Supervised

  • Decision Trees

Şimdi mesela çok fazla değeri olan Chol değerine bakalım. Normalde bunda outlier handling yaptık ama alternatif olarak discretizastion da yapabilirdik. Nasıl yapıldığına bakalım ama bunu kullanmayacağız.(Gridsearchte hem kullan hem kullanma şeklinde bir seçenek de yapabiliriz)

# Equal-Width
# pd.cut ile de yapılabilirdi ama Pipeline için sklearn kullanmak daha iyi, tıpkı imputationda pd.fillna yerine SimpleImputer kullanmak gibi
from sklearn.preprocessing import KBinsDiscretizer
discretizer = KBinsDiscretizer(n_bins=10, encode='ordinal', strategy='uniform')
discretizer.fit_transform(X_train.Chol.values.reshape(-1,1))[:5]
Out[50]:array([[5.],
[2.],
[6.],
[4.],
[8.]])
# Equal frequency
# pd.qcut ile yapılabilirdi
discretizer = KBinsDiscretizer(n_bins=10, encode='ordinal', strategy='quantile')
discretizer.fit_transform(X_train.Chol.values.reshape(-1,1))[:5]
Out[51]:array([[7.],
[1.],
[7.],
[5.],
[9.]])
#Kmeans
discretizer = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='kmeans')
# discretizer.fit_transform(X_train.Chol.values.reshape(-1,1))[:5] #---->hata aldım, numpy versiyonuyla alakalı sanırım,bilahare düzelteceğim

Kmeans ile ilgili örnek için aşağıdaki ilk kaynağa bakınız.

ilave kaynaklar

Feature extraction/Feature creation

Mevcuttaki featurelardan yeni featurelar üretilebiliyor mu, bunlara bakılabilir. Bu, mevcut featureların birbiriyle çarpımı veya toplamı olabileceği gibi bir veriden daha anlamlı/faydalı olabilecek başka bir bilginin çekilip çıkarılması da olabilir. Yeni bir feature üretme işlemidir. Dimension reductionın tam tersi gibi görünse de aslında aynı amaca da hizmet edebilir, zira yeni ürettiğimiz alandan sonra onu üretirken kullandığımız alanları artık silebiliriz. Birden fazla kolonu kullanıp 1 alan ürettiysek, ve bu alanları sildiysek, örneğin oranlama yaptıysak, dimension reduction yapmış olduk. Örneğin hedef ve gerçekleşenleri oranlayıp hedef gerçekleşme oranı bulmak gibi. Birden fazla kolonu kullanıp 1 alan ürettiysek, ve hiç bir alanı silmediysek sayıyı artırmış oluruz, ama bunda da geçerli bir nedenimiz vardır, zira kolonlar kendi başına da anlamlıdır, oranlandığında da.

İlkine örnekler:

  • Brüt + İptal = Net Satış
  • Kar / Personel Adedi = Personel başına kar

İnteraction yaptık diyelim: X=A/B dedik. böyle bi durumda, A ve B’nin ikisini de tutmaya gerek yok, birini tutalım diğerini atabiliriz, dummy variable tuzağı olmasın. Ama hangisini? Ör: risk/limit. Duruma göre değişir. Amacımıza göre. Domain bilgisi önemli.

Peki A/B mi A/C mi yapacağımızı nerden bileceğiz? Biraz akıllı tahminler yapıp, denemeler yapabiliriz. Hepsini denemek çok maliyetli olabilir. Bu arada, Deep Learning’te böyle birşey yapmaya gerek olmuyor, bu aşama sadece pure Machine Learning modelleri için geçerlidir.

İkincisine örnekler:

  • Tarihten ay/yıl/gün bilgilerinin çıkarılması
  • Unvan ve ismin birlikte yazıldığı bir kolondan unvanın çıkarılması
  • Titanic verisetinde yapıldığı iddia edilen, kamara numarasından odanın geminin neresinde bulunduğu bilgisi(sağ üst kıç tarafındakiler en geç battığı için kurtulma şansları daha fazla olmuş)
  • Numerik değerlerin discretize edilmesi: Yaş bilgisinin 5 ayrı grupta gösterilmesi

Bizim veri seti özelinde konuya çok hakim olmadığım için bi işlem yapmayacağız.

ilave kaynaklar

Feature Selection/Feature elimination

Gerçek hayat problemleriyle uğraşırken elimizde birçok veri olacaktır ve hangisinin model sonucuna katkısı olacağından emin olmak gerekiyor. Dimension redcution’a benzemekle birlikte, feature selectionda(FS), varolan featurelar az sayıdaki isimsiz featurelara sıkıştırılmak yerine direkt olarak elenirler/tutulurlar. Bunun için de targeta en yüksek etkisi olanlar tespit edilip, kalanlar çıkartılmaya çalışılır. Gerekirse arkasından yine de dimension reduction yapılabilir.

Burda önemli bir konu, bu işlemi encoding işleminden önce mi sonra mı yapılacağı. Bu konuda net bir görüş görmemekle birlikte bu konuyu ele alan bi makale de bulamadım, sadece bu soruyu soranlar var, verilen cevaplarda ise iki seçenek de savunuluyor. Bu durumda en iyisi, deneyip görmek olacak gibi.

Faydası nedir, ne zamana yapılmalıdır?:

  • Elimizde çok sayıda feature olduğunda, overfittingle karşı karşıyayızdır, bu riski azaltmak için(overfitting kavramını daha sonra göreceğiz)
  • Gereksiz yanıltıcı datadan arınmak daha yüksek isabetli tahmin yapmamıza da sağlar, accuracy yükselir.
  • Eğitim süresi kısalır.

FS’nın model başarı performansına fayda sağlayacağının bi garantisi yoktur, denenip görülmelidir. Bazen arkasından yine de PCA yapmak gerekebilir. Bunun da etkisinin ne olacağını denemeden bilemeyiz.

FS ve Feature Extraction arasındaki farklar için şurada güzel bir açıklama var.

Elimizdeki probleme(classification/regression), input ve outputun tipine göre kullanılacak yöntemler değişebilecektir. Üç ana yaklaşım vardır:

1)Filter metodları: Targetla yüksek seviyede korele olan featurelar seçilir, eğitim yapılmadığı için hızlıdır ama isabet oranları diğer yöntemlere göre düşüktür. Gözle(scatter/heatmap) veya aşağıdaki testlerle yapılabilir.

  • Pearson’s/Spearman’s correlation: Multicollinearity varsa(r>0.9), yani iki değer arasında korelasyon çok yüksekse, birini kullanmasak da olur. ör:metrekare ve odasayısı. Ama bunlardan hangisi daha çok etkiliyor gibi bir çıkarım da yapacaksak ikisini de almamız lazım. Feature selection yapmayıp, PCA yaparsak da korele kolonları yüksek oranda tek kolonda birleştirir. Ama PCA yorumlamayı zorlaştırıyor, mümkünse bu aşamada bitirmek lazım. Bu arada mulitcollinear olmayıp yine de hatırı sayılır bir korelasyon varsa bu aşamada çıkartamayız ama arkadan bi PCA yapmak işe yarayabilir. PCA yapacaksanız çıkarmaya gerek yok. PCA yapmıyorsan 0.9 üstünü çıkarmak performansı iyileştirir ama sonucu ille de çok iyileştirir diyemeyiz.
  • Chi-square test
  • Cramer’s V coefficients
  • wald’s test
  • regressionda statsmodeldeki p-value’su yüksek olanlar
  • Düşük variancelılar.(Uç örnek: Var=0 ise hepsi aynıdır. VarianceThreshold bunu yapacak)
  • Full cardinalitesi olanlar: Müşteri numarası gibi, tüm kolonun benzersiz değerler içerdiği kolonlar

2)Wrapper metodlar: filter metodlara göre daha iyi sonuçlar vaat eder ama eğitim gerektirdiği için daha uzun sürer. Arka planda greedy heuristic(sezgisel) algoritmalar kullanır. Featurelar arasındaki interactionı tespit edebilirler.

  • Forward Selcetion
  • Backward Elimination
  • Recursive Feature Elimination (RFE)
  • Genetic algoritmalar

3)Intrinsic/Embedded metdolar: Kendi içinde feature selection metodları olan modellerdir. Hızlı ve verimlidir. Sonuçları filter metodlara göre daha iyidir. coef_ veya featureimportance gibi attriubteleri olan modeller kullanılır.

  • Lasso regularization: Regressyon notebook’unda görmüştük, multicollinear olanlara 0 katsayı veriyordu. Ama multicollinearity yoksa çok da işe yarayacağını düşünmüyorum.
  • Ağaç bazlı algoritmalar(DT, RF, XGBoos v.s):DT ve RF gibi algoritmalarda embedded bir şekilde feature importance elde ediliyor, diğerlerinde yukarıdaki metodlarla aslında dolaylı olarak önemli featureları elde edebiliyoruz.

Bunlardan önce yapılacak bazı manuel kontroller de var aslında.

  • İş bilgisi kullanarak gereksiz kolonlar silinebilir
  • Çok null olanlar silinebilir,(kolonun en az yarısı nullsa)
  • A+B=C diye türetim yaptıysak A ve B silinebilir

Filter metod: VarianceThreshold(numerikler için)

Düşük varyanslı kolonları elimine eder. Bi threshold veririz, onun altında varyansı olanları eler. Sağlıklı çalışabilmesi için tüm kolonlar aynı ölçekte olması(normalize edilmesi) lazım, yoksa hepsinde aynı varyansı beklemek anlamsız. O yüzden bu kısmın scalingden sonra gelmesi gerektiği aşikar. Scaling olarak da kesinlikle StandartScaler kullanılmamalı, çünkü bu zaten tüm featureların varyansını 1 yapar.

Univariate bir yöntem olduğu yani targetla ilişkiye bakmadığı için önemsiz bir kolonu seçme ihtimali de var. Ben bunu kullanmayı çok önermem.

from sklearn.feature_selection import VarianceThreshold
from sklearn.preprocessing import MinMaxScaler
X_scl=MinMaxScaler().fit_transform(X_train[nums])
np.var(X_scl,axis=0)
#array([0.03551495, 0.04867842, 0.03780438, 0.0310207 , 0.03221265,
0.10255826])
sel = VarianceThreshold(threshold=0.035)
X_train_VT = sel.fit_transform(X_scl)
X_train[X_train[nums].columns[sel.get_support(indices=True)]].head()

Sadece bu 4 kolon önemliymiş!!! Burda tabi threshold olarak neyi vereceğimizi deneme yanılma ile bulmak gerekebilir, ama dediğim gibi ben bu yöntemi sırf göstermiş olmak için gösterdim, supervised modellerde kullanmaya değmez. Bununla beraber unsuperrvised learning modellerinde kullanılabilir.

Filter metod: korelasyon bakma

Buna zaten EDA’da bakmıştık. Belli threshold üstündekileri seçebiliriz.

da.getHighestPairsOfCorrelation(corr_results,"AHD",5)

Burda bence %30 üzeri olanlar seçilebilir.

da.getHighestPairsOfCorrelation(corr_results,"AHD",0.3)

Yalnız şuna dikkat etmek lazım; kullandığımız yöntem dython kütüphanesindeki bir metoddu, bu metodun numerik-numerik ve numerik-kategorik korelasyonları benzer ölçekte olup sonuçları birbiriyle kıyaslayabiliriz ancak kategorik-kategorik korelasyonda güçlü ilişki için sonucun 1e yakın olması gerekmiyor, bunun için cramer’s v metriği kullanılıyor olup yorumlaması şu tabloya göre yapılır:

df  Small  Medium  Large
1 0.10 0.30 0.50
2 0.07 0.21 0.35
3 0.06 0.17 0.29
4 0.05 0.15 0.25
5 0.04 0.13 0.22
df=min(# of rows-1 , # of columns-1)

Şimdi bu kategoriklere bir daha bakalım

corr_results[cats].loc["AHD"].sort_values(ascending=False)

Bizim örneğimizde df=1 olup medium etki var diyebilmek için en az %30luk değerlere bakabiliriz. Aslında yukarda seçtiğimiz %30 da zaten bunları kapsıyordu.

Filter metod:F-statistic

Sonra eklenecek…

Filter metod: SelectKBest

Aşağıda geçen metodlara göre en yüksek skora sahip olan featurelar seçilir.

from sklearn.feature_selection import SelectKBest, chi2, f_classif, f_regression, mutual_info_classif, mutual_info_regression

Öncelikle categorik verileri numerikleştirelim, bunun için OrdinalEncoder kullanabiliriz.(Encoding kısmında detaylarını göreceğiz). Ancak orjinal X_traini bozmak istemediğim için onun şuana kadarki işlemlerin yapıldığı halini içeren bir kopyasını yaratacağım ve onu orjinal olarak saklayacağım. İlerde buna tekrar ihtiyaç duyacağız.

X_train_orj=X_train.copy()from sklearn.preprocessing import OrdinalEncoder
oe1=OrdinalEncoder(categories=[['typical', 'asymptomatic', 'nonanginal', 'nontypical']]) #ChestPain için, bu sırayla yapılsın
#RestECG zaten numerik
oe2=OrdinalEncoder() # nominaller için
X_oe1 = oe1.fit_transform(X_train["ChestPain"].values.reshape(-1,1))
X_oe2 = oe2.fit_transform(X_train[noms].values)
X_train["ChestPain"]=X_oe1
X_train[noms]=X_oe2
X_train.head()
#outputumuz categoric ve inputlarımız mixed, o yüzden mutual information kullanalım
fs = SelectKBest(score_func=mutual_info_classif,k=5)
fs.fit(X_train, y_train)
X_train_fs = fs.transform(X_train) #en yüksek etkisi olan 5 kolon
X_train_fs[0]
Out[63]:array([2.0, 0.0, 0.0, 1.0, 1.0], dtype=object)
X_train.columns[fs.get_support()]
#indeklsere de ihtiyaç olursa
[e for e,x in enumerate(fs.get_support()) if x==True]
Out[64]:
Index(['ChestPain', 'ExAng', 'Slope', 'Ca', 'Thal'], dtype='object')
[2, 8, 10, 11, 12]
plt.bar([i for i in range(len(fs.scores_))], fs.scores_)
plt.xlabel("feature index")
plt.ylabel("scores")
plt.show();
  • Çalışan işin rassal doğasından dolayı her çalıştırdığınıda farklı sonuç çıkacaktır, bikaç kez çalıştırıp ortalama almak gerekebilir, bu da train time’ı uzatır tabiki, zaman sorunu yoksa yapılabilir.
  • Bir diğer alternatif de kategorikler için chi2, numerikler için f_flassif(ANOVA) yapıp sonuçları birleştirmek olabilir.
  • Ve tabiki k’nın optimal değeri için grid search yapılmalıdır.

Wrapper metod:Backward elimintation

Sonra eklenecek…

Wrapper metod:Forward elimination

Sonra eklenecek…

Wrapper-Exhasustive(Brute-Force)

Sonra eklenecek…

Wrapper-Greedy Elimination

Sonra eklenecek…

Wrapper metod:RFE

Bu yöntem, aslında embededed modelle wrapperların bir hibritidir. Burda önce tüm featurelarla model çalıştırılır, accuracy bi kenarda tutulur, sonra feature importancea göre en az önemli olan silinir, tekrar model çalıştırılır ve önceki accuracy ile karşılaştırılır. Çıkan sonuca göre aksiyon alınır, ta ki istenen sayıdaki feature sayısına inene kadar.

Önemli: Bu aşamada, model çalışacağı için modele girecek verilerin düzgün şekilde encode edilmesi gerekir. Yukarıda geçici olarak ordinalencoding yapmıştık, zira chi2/anova testleri için verilerin numerik olması gerekiyordu. Akabinde nihai bir model çalışmadığı için o şekilde kalmasında sorun yoktu, ancak şimdi model çalışacağı için ordinaller dışındaki categorik verilerin yani “noms” değişkenindeki nominal featureların onehot encode edilmesi gerekiyor. Mevcut haliyle modele sokmak doğru değil.

#onehot encoding, burda pandas ile yapıyorum, daha sonra sklearn ile yapıcaz
dummies=pd.get_dummies(X_train.loc[:,noms],drop_first=True)
nomsuz=X_train.loc[:,[x for x in X_train.columns if x not in noms]]
X_train_ohe_dummy=pd.concat([nomsuz,dummies],axis=1)
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(solver='lbfgs',max_iter=1000)
rfe = RFE(model, n_features_to_select=5)
X_rfe = rfe.fit_transform(X_train_ohe_dummy, y_train) #5 en önemli kolon
print("Selected Features: %s" % rfe.support_)
print("Feature Ranking: %s" % rfe.ranking_) # 1 olanlar bizimdir
Selected Features: [False False False False False False False True True False True True
True]
Feature Ranking: [7 3 8 9 5 6 4 1 1 2 1 1 1]

Burda da yine optimum feature sayısını bilmiyoruz, bununu için bi döngü kurabiliriz, veya gridsearch içinde halledebiliriz.

high_score=0 
score_list =[]
for n in range(1,len(X_train.columns)+1):
X_trn, X_tst, y_trn, y_tst = train_test_split(X_train_ohe_dummy, y_train, test_size = 0.3, random_state = 0)
model = LogisticRegression(solver='lbfgs',max_iter=1000)
rfe = RFE(model, n_features_to_select=n)
X_trn_rfe = rfe.fit_transform(X_trn,y_trn)
X_tst_rfe = rfe.transform(X_tst)
model.fit(X_trn_rfe,y_trn)
score = model.score(X_tst_rfe,y_tst)
score_list.append(score)
print(n,score)
if(score>high_score):
high_score = score
best_n = n
print(f"Optimum number of features: {best_n}")
1 0.6986301369863014
2 0.684931506849315
3 0.684931506849315
4 0.7534246575342466
5 0.7808219178082192
6 0.7671232876712328
7 0.7945205479452054
8 0.7808219178082192
9 0.7808219178082192
10 0.7808219178082192
11 0.8082191780821918
12 0.7945205479452054
13 0.821917808219178
Optimum number of features: 13

ilginç bir şekilde bu örnekte tüm featurelar kullanılmalı deniyor.

Bunun bir de cross-validation yapan hali var. Cross validation konusunu öğrendikten sonra daha anlamlı gelecektir. Bilmiyorsanız, şimdilik geçebilirsiniz.

from sklearn.feature_selection import RFECV
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=150)
rfe = RFECV(model, min_features_to_select = 3, step = 1 , cv=5, scoring='accuracy',n_jobs=-1,verbose=-1)
selection = rfe.fit(X_train_ohe_dummy, y_train)
# print the selected features.
print(X_train_ohe_dummy.columns[selection.support_])
Index(['Age', 'ChestPain', 'RestBP', 'Chol', 'MaxHR', 'Oldpeak', 'Ca', 'Sex', 'ExAng', 'Slope', 'Thal'], dtype='object')

Embedded metod:LassoCV

Logistic regresion modelimizi penalty=l1 parametresiyle çalıştırdığımızda bunu yapmış olacağız.(Regression problemi olsaydı direkt Lasso sınıfını kullanabilirdik). Dolayısıyla bu parametreyi sağlayan modellerde kullanılır. Bu arada bu konu regularizasyon ile ilgili olup, bunun detaylarını şurada bulabilirsiniz.

from sklearn.feature_selection import SelectFromModel #Tüm embedded modellerin bununla birlikte kullanılması gerekiyor
from sklearn.linear_model import LogisticRegression
embeded_lr_selector = SelectFromModel(LogisticRegression(penalty="l1",max_iter=1000,solver="saga"), max_features=5)
#**********burdan sonrasında sanırım sklearn versiyonuyla ilgili olarak hata alıyorum, bilahare düzelteceğim***********
# X_train_lasso = embeded_lr_selector.fit_transform(X_train, y_train)
# embeded_lr_support = embeded_lr_selector.get_support()
# embeded_lr_feature = X_train.loc[:,embeded_lr_support].columns.tolist()
# X_train.columns[embeded_lr_support]
# print(str(len(embeded_lr_feature)), 'selected features')

Embedded metod:Tree based bir algoritma ile feature importance bakma

Random forest deneyebiliriz, bunu random forest konusunda göreceğimiz için burayı şimdilik geçebilirsiniz.

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
embeded_rf_selector = SelectFromModel(RandomForestClassifier(n_estimators=100), max_features=5)
# burda da yine aynı şekilde hata alıyorum, bilahare düzelteceğim
# X_train_rfe = embeded_rf_selector.fit_transform(X_train, y_train)
# embeded_rf_support = embeded_rf_selector.get_support()
# embeded_rf_feature = X.loc[:,embeded_rf_support].columns.tolist()
# print(str(len(embeded_rf_feature)), 'selected features')
# X_train.columns[embeded_rf_support]

Karşılaştırma ve Final Feature Selector

Peki ben hangisini kullanayım diye soruyor olabilirsiniz. Burdaki her bir yöntemin kendine göre artısı eksisi var. Yine de size bi guideline vermeye çalışayım.

  • data tipleri hep aynıysa(hepsi numerik gibi), recursive olmayan bir yöntem denenebilir, SelectKBest+f_regreesor
  • data tipleri karışıksa(hem numerik hem kategorik varsa), RFE veya SelectKBest+mutual_information veya Embedded veya numerikler için ayrı kategorikler için ayrı yapıp sonuçları birleştirmek(bu sonuncusu biraz daha zahmetli)
  • ayrıca bu işlemin encoding öncesinde/sonrasında ayrı ayrı denenmesinde de fayda olabilir.
  • dataset çok büyük değilse, gridsearch içinde hepsi birden denenebilir.

Zaman sınırı varsa, benim önerim ilk etapta bi filter metodla ilerlemek olacaktır. Zira wrapper metodlar ve embedded modeller computationally expensive yöntemlerdir.

Kaynaklar

Genel

Başka kütüphaneler

Encoding features

Daha önce belirttiğimiz gibi, ML algoritmaları çoğu durumda X’ler yani featurelar için sayısal değer isterler, bazen y’lerin de sayı olması gerekebilir. Değerleri uygun sayısal formata çevirme işlemleri Encoding olarak geçiyor.

  • Sıralı değerler: Ordinal Encoding. Ör:İyi, orta, kötü gibi. (OrdinalEncoder veya manuel mapping ile)
  • Kategorik değerler: OneHotEncoder: Meslekler, kıtalar gibi. OneHotEncoder.
  • y değerleri: LabelEncoding

LabelEncoding, ilgili kolonda 0 ve 1 değerlerini üretirken, OneHotEncoding bu değerleri alıp kolonlara aktarır. Tek kolonda yapmaz çünkü oluşacak 1–2–3 değerleri sanki önem sırasına sahipmiş gibi olur. Örneğin kıtaları düşünelim, 1-avrupa, 6-afrika olsun. Sanki Afrika Avrupadan 6 kat daha önemliymiş gibi görünür. Bunun yerine kıtalara kolonlara yerleşip 1 tanesine 1 diğerlerine 0 atanır.

Dikkat Edilecek Hususlar

  • Yüksek kardinalitesi olan kolonlarda onehot yapınca feature sayısı çok artar →curse of dimensionality. Bunlarda clustering yaparak veya frekanslarına bakarak seçenekleri düşürmeye çalışmak iyi fikir olabilir. Top 5 almaya karar verdiğimizde ilk 3ten sonra “Diğer” grubu 4. gelebilir, arkasından da son grup gelebilir.
  • bir diğer alternatif de bu kategorik kolonların feature üzerinde etkisi başka nasıl ölçülebilir, ona bi çözüm bulunmaya çalışılabilir. Ör: İl kolonu önemli çıktı, acaba illerin nüfusu veya ortalama kişi başı geliri gibi nümerik değerleri ile replace edilebilir mi, veya sözkonusu target, belli bi yere uzaklıkla alakalıysa longitude/latitude değerleri de kullanılabilir.
  • onehot yaparken dropfirst yapmak lazım, yoksa multicollinearity’ye neden oluruz. böylece N kategori varsa N-1 kolon oluşur.(Tree-based algoritmalar hariç)
  • trainde olmayıp testte olan ve tersi durumlar için handle_unknown parametresini kullanmamız lazım(tabi biz her halükarda stratified sampling yaparsak bu sorunu biraz elimine etmiş oluruz ama sahadan gelen yeni datada hala bu sorun oluşabilir)
  • kullandığınız sklearn versiyonuna göre string değerleri, öncesinde numerikleştirmeniz gerekebilir, akabinde ordinal ve onehot yapabilirsiniz.
  • birden çok binary variable’ı encode etmek için ordinalencoder kullanılabilir. Döngü halinde LabelEncoder kullanmaya göre daha efektiftir, üstelik LabelEncoder’ın amacı zaten targetı yani label’ı encode etmektir.
  • OrdinalEncodere’da kategoriler categories parametresine çift köşeli parantez içinde, artan sırada verilmelidir.[[“kötü”,”orta”,”iyi”]] gibi

Şimdi bu noktada iki adım önceki orjinal X_train datasına dönelim ki encoderlar hiç yapılmamış olsun ve onları burada yapalım.

X_train = X_train_orj.copy()from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(drop="first")
X_ohe = ohe.fit_transform(X_train.loc[:,noms])
X_ohe
<242x7 sparse matrix of type '<class 'numpy.float64'>'
with 626 stored elements in Compressed Sparse Row format>

oluşan veri yapısı sparse matrix olup numpy array isteyen bir başka transformera gidi olarak girecekse ya toarray metodu ile çevrilmeli veya fit_transform sırasında sparse=False parametresi girilmelidir.

X_ohe.toarray()[0]
Out[75]: rray([0., 0., 0., 0., 0., 1., 0.])
#ohe objesinin de çeşitli üyelerine ulaşabiliriz
ohe.categories_
ohe.feature_names_in_
ohe.get_feature_names_out()

Şimdi bu yeni değerleri ana dataframemize ekleyelim, ama önce bu sparse matrixten bi dataframe oluşturalım. Bunları, ara aşamaları göstermek için yapıyorum, nihai pipelineımızda bunlara gerek olmayacak.

ohedf=pd.DataFrame(X_ohe.toarray(),columns=ohe.get_feature_names_out(),index=X_train.index)
ohedf.head()
X_train=pd.concat([X_train.drop(noms,axis=1),ohedf],axis=1)
X_train.head()

Şimdi de ordinal encoderlarımız yapalım.

from sklearn.preprocessing import OrdinalEncoder
oe=OrdinalEncoder(categories=[['typical', 'asymptomatic', 'nonanginal', 'nontypical']]) #ChestPain için. çift [[]] kullanımına dikkat
#RestECG zaten sıralı
X_oe = oe.fit_transform(X_train.loc[:,["ChestPain"]])X_train["ChestPain"]=X_oeoe.categories_Out[83]:[array(['typical', 'asymptomatic', 'nonanginal', 'nontypical'], dtype=object)]

Son olarak da target’ımız için LabelEncoder yapalım, logistic regressionda bu gerekmiyor normalde, istersek yapmayabiliriz de. Ancak sonradan kullanacağımız bazı fonksiyonlar bunların numerik olmasını isteyeceği için biz yine de çevirelim.

from sklearn.preprocessing import LabelEncoder
le=LabelEncoder()
y_train_le=le.fit_transform(y_train)

LableEncoder, ileride göreceğimiz gibi confusion_matrix yorumlamasını zorlaştıran bir yapıya sahip. Çünkü bunda alfabetik sıraya göre bakıp sözlük sırasına göre ilk kelimeyi 0, sonrakini 1 yapıyor. Bizim bu örnekte No, Yes’ten önce geliyor ve No 0 olacak, bundan sorun olmaz, ama sorun olacak bir casede ya manuel mapping ya da categorical_encoders kütüphanesini kullanmayı tercih edebilirsiniz, orada kendi sıranızı verebiliyorsunuz.

y_train.values[0],y_train_le[0]Out[85]:('No', 0)

Dummy bi verisetinde yukarıdaki alfabetik sıradan kaynaklı problemi görelim.

dummyset=pd.DataFrame(["Cancer","Cancer","Not-Cancer","Cancer"],columns=["Label"])
le2=LabelEncoder()
le2.fit_transform(dummyset.Label)
Out[86]:array([0, 0, 1, 0])

Mesela biz burda rakamların 1,1,0,1 olmasını beklerdik.

from category_encoders import OrdinalEncoder as COE
coe=COE(mapping=[{'col': "Label", 'mapping': {"Cancer": 1, 'Not-Cancer': 0}}],return_df=False)
coe.fit_transform(dummyset).ravel()
Out[87]:array([1, 1, 0, 1])

Kaynaklar

Distribution transformation

Özellikle skewed olan featureları normal dağılıma getirmek için kullanılır. Normal dağılıma getirmek neden önemli? Bazı algoritmalar bunu gerektiriyor olabileceği gibi, standart scaling yapmadan önce de gereklidir.

#en baştaki pairplotta Oldpeak alanında bi skewness var görünüyordu
X_train.Oldpeak.hist();

Ancak burdaki veriler kesikli(discrete) olduğu ve sayısı çok az olduğu için log transformasyona çok uygun değil. O yüzden bu aşamayı es geçebiliriz.

Scaling

Maaş(4–5 basamaklı) ve yaş(2 basamaklı) gibi iki farklı ölçekteki verinin aynı ölçeğe getirilme işlemidir. Özellikle distance hesabı yapan algoritmalar için kritik öneme sahiptir. Bazı algoritmalar distance hesabı yapmadığı için(tree-based olanlar gibi) onlarda yapılmasa da olur. Nihai pipeline içinde, yapıldığı ve yapılmadığı iki senaryo denenebilir. Bununla birlikte özellikle optimizer olarak GradientDescent veya solver olarak saga kullanan algoritmaların daha hızlı converge etmesi için her halükarda yapılmasında fayda olabilir, zaten çok da yorucu bir işlem değildir.

3 türü vardır.

  • StandartScaling: Ortalaması 0, standart sapması 1 olan bir dağılıma döndürür. Datanın normal dağılıma uygun olması lazımdır.. Aksi halde ya MinMax kullanılmalı ya da öncesinde Log transformasyon uygulanmalıdır.
  • MinMaxScaling: Verileri 0–1 arasında sıkıştır. Ama bunun bi bedeli vardır. Data çok sıkıştığı için variance düşer. Halbuki iyi bir model için featurelarda görece yüksek variance bekleriz. StandarScalerin iyi çalışmayacağı caseler için iyi sonuçlar üretebilir, mesela normal dağılıma uygun değilse. Outlierlara duyarlıdır, yani bu uygulansa bile outlierların relative etkisi devam eder.(Buna, hatta bazen üsttekine bazen yanlışllıkla normalization da denir, ama bu hatalı bi kullanımdır. Nedeni için buraya bakabilirsiniz).
  • Robust Scaler: MinMaxScaler’a benzer ama alt-üst sınır olarak min-max yerine IQR değerlerini kullanır, bu yüzden outlierlara dayanıklıdır.

Yine mümkünse girdsearchte hepsini ayrı ayrı deneyip en uygun olanı bulmaya çalışmak gerekebilir.

Önemli hususlar

  • Ordinal ve Onehot encoding yapılmış kolonlarda scaling yapılıp yapılmaması yönünde bi görüş ayrılığı sözkonusu, o yüzden ikisini(yap-yapma) de deneyip görmek lazım.
  • Regression durumunda, sadece X’ler scale edilir, y’ler edilmez.
X_train[nums].describe()

Scaling ihtiyacı var görünüyor. Featurelarımız normal dağılıma uygun gibi göründüğü için standartscaler ile ilerleyebiliriz. Tabi yine zaman problemi yoksa diğerlerini de gridsearch içinde deneyebilirsiniz.

Bir diğer konu da Ordinal ve onehot encode edilmiş featureların da yine scalinge tabi tutulması olabilir. Biz burdaki manuel işlemde bunları da işleme tabi tutacağız.

from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
ss=StandardScaler()
X_train=ss.fit_transform(X_train)

Kaynaklar

Imbalanced data durumunda ovesampling/undersampling

Bizim datasetimizde bir imbalanace durumu olmadığı için bu aşamayı es geçiyoruz. Ama böyle bir örneği şurada bulabilirsiniz.

Dimension reduction

Çok fazla kolon olması sorununu(Curse of dimensionality) bazen feature selectiona göre daha farklı yöntemlerle çözeriz. Mesela varolan featurelar az sayıdaki isimsiz featurelara sıkıştırılır. Dolayısıyla yorumlamayı zorlaştırır ama model performansına katkıda bulunabilir(bulunmayabilir de)

PCA gibi dimension reduction yöntemleri aslında yeni featurelar yaratmaları bakımından feature extraction olarak da görülmektedirler.

Bizim veri setimizde çok fazla kolon yok, o yüzden uygulamasak da olur, ama yine de tecrübe etmiş olmak için uygulayacağız, fakat nihai pipeline içinde kullanmayacağız. Konunun detayları için ilgili notebookumu şurada bulabilirsiniz.

Dikkat edilecek hususlar

  • Scaling yapılmış olması gerekiyor, sıralama önemli.
  • Sadece numerik kolonlara yapılacaktır.
from sklearn.decomposition import PCA
pca=PCA(n_components=3)
X_train_pca = pca.fit_transform(X_train)
X_train_pca[0]
Out[93]:array([-1.6152172 , 1.73987438, 0.63628478])
pca.explained_variance_ratio_.sum()Out[94]:0.43930454703104677

Eldeki verinin varyansını düşürdüğümüz yani bilgi kaybına uğradığımız için accuracy düşecek gibi gelebilir size ama durum her zaman öyle değil, zira PCA’in amacı sadece training süresini düşürmek değil aynı zamanda overfittingi de engellemek. Böylece aslında daha iyi sonuç bile alabiliriz. Gridsearch sırasında bunun uygulandığı ve uygulanmadığı iki durum için de bakılabilir.

Bu arada, n_components parametresine direkt istediğimiz varyansı da verebiliriz, böylece n_component adedi de ona göre değişecektir.

pca=PCA(n_components=0.90)
X_train_pca2 = pca.fit_transform(X_train)
pca.n_components_
Out[95]:11

Toplam feature sayısının sadece 2 adet azalmasını sağladı. Bunun böyle olacağını EDA bölümündeki pairplottaki scatter diagramlardan da anlayabilirdik aslında. Zira orda featureların hiçbiri birbiriyle korele görünmüyordu, dolayısıyla high dimensiondaki bu featureları low dimensiona indirirken bi sıkıştırma yapmasını bekleyemezdik.

Biz modellerimizde en son scale edilmiş halini kullanacağız, yani X_train’i.

Modelleme

Model Selection ve Validation

Buraya kadar geldiğimize göre, eğitim setinin tüm datasetle yapılmayacağını, sadece eğitim seti ile yapılacağını biliyor olmanız lazım. Zaten preprocessing öncesinde test setimizi ayırmıştık. Duruma göre biraz çeşitlenmekle birlikte süreç özetle şöyle işleyecek:

  • Model Selection: En iyi parametre seçiminin yapılacağı geçici eğitim aşamasıdır. Burada test datası ile ilgili bi işlem yapılmaz. Train datası uygun bir yöntemle farklı seçeneklerle eğitilir ve uygun model seçilir. Farklı seçenekten kastım tek bir algoritmanın farklı parametreleri olabileceği gibi farklı algoritmalar da olabilir.
  • Model Validation: Gerçek hata payımız nedir sorusunun cevabını almaya çalışırız. Model seçiminden sonra test datasının performansına bakılır.
  • Model Evaluation: Çeşitli metriklerle modelin nihai performansını ölçeriz. Başarılı değilsek başa dönülür, başarılıysak final model tüm data üzerinden son birkez daha eğitilir.
  • Final model training: Nihai modele karar verildiğinde uygun parametrelerle tüm veri seti tekrar eğitilir.
  • Deployment: Modelimizi production ortamına deploy ederiz.

İşin özünde elimizdeki datayı farklı yöntemlerde 3 kısma ayırıyoruz. train, validation, test. Bunların amacı şöyle:

  1. Train seti eğitim için kullanılır.
  2. Validation seti farklı modeller ve parametreler arasından uygun model seçimi için kullanılır. A modeli mi B modeli mi, A ise, bunun x parametresi kaç olsun y parametresi kaç olsun gibi.
  3. Test seti de seçtiğimiz model genel olarak ne kadar iyi onu test etmek için kullanılır.

Başka bir bakış açısıyla aslında 4 tür yaklaşım var diyebiliriz.

  1. Evaluation with hold-out data (çok büyük veri setlerinde)
  2. Hold-out validation with a test set (aslında bu da 1-fold cross validation)
  3. k-fold Cross validation
  4. Nested Cross Validation

Şimdi yukarda bahsettiğimiz 4 yaklaşıma bakalım. Öncesinde aşağıdaki videoyu da izleyebilirsiniz.

1.yöntem:Hold-out datası ile validation

Burda eğitim işlemi, tüm train datası ile 1 kere yapılır. Genelde çok büyük veri setlerinde bu kullanılabilir. Diğer durumlarda diğer yöntemlerin kullanılması tercih edilmelidir.

Aşamaları şöyledir:

  1. Verisetini eğitim ve test(validation da denebilir) olmak üzere iki parçaya bölün
  2. Her bir parametre kombinasyonu için eğitim setini eğitin ve test seti üzerinde başarı metriğini hesaplayın
  3. En iyi sonucu veren hyperparametre setini belirleyin
  4. Bu parametrelerle tüm veri seti üzerinden yeniden eğitin

Bu noktada durup, eğer hala bakmadıysanız, öncelikle buradan Logistic regresyon notebookuma bakmanızda fayda var, sonra tekrar gelin, devam edelim.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
lr = LogisticRegression(C = 1, solver='lbfgs')
lr.fit(X_train, y_train) #sadece fit ederiz, trasnform yok, bi değişkene atama yok
y_pred = lr.predict(X_train) #eğitilmiş modeller tahminlelrimizi yaparız
print(lr.score(X_train,y_train)) #veya accuracy_score(y_train,y_pred)
#0.8512396694214877

Bu skor, training acccuracy olarak geçer. Sonrasında bir de test seti üzerinde aynı tahminlemeyi yapabiliriz. Küçük veri setleri için bu casede şöyle bir problem var tabi: ya ayırdığımız validation set çok şanslı bir setse, tesadüfen başarı skoru yüksek çıktıysa? Çözüm olarak, validation sete ek olarak bir de nihai test seti ayırmak önerilir.

Bu ilk yöntem hemen hemen hiç uygulanmaz, eldeki veriseti çok büyükse uygulanabilir denir ama pratikte ben kimsenin bu şekilde ilerlediğini görmedim, ille de bi cross validation yapılır.

2.yöntem: cross validation( validation ve test set ayrım)

Bunda test seti bi kenara koyduktan sonra, train setimizi de kendi içinde sub-train ve validation olmak üzere ikiye ayırıyoruz. Yani elimizde toplamda 3 set oluyor.

Şimdi elimizde daha iyi bir model seçici yöntemimiz var, oluşacak performans skoru da daha genelleştirilmiş bir skor olacak. Ama ilk yöntemdeki problem kısmen yine devam ediyor gibi. Ya burda da validation setinde yine şanslı bir bölme yaptıysak? Büyük veri setinde bu olasılık yine düşük, çünkü veriseti o kadar büyüktür ki, bölmeyi yaptığımızda her alt parçada ana kümeyi temsil kabiliyeti yüksektir. Ama küçük verisetlerinde problem olasılığı var. O zaman çözüm k-fold cross validation yapmakta.

Bunun kodlamasını yapmaya gerek yok, ana datamızı nasıl train ve test diye ikiye bölmüştük, şimdi de X_train’i bir daha kendi içinde train ve test diye iki kısma ayırdıktan sonra 1.yöntemin aynısı yapılır.

3.yöntem: k-fold cross validation

Bunda ise eğitim setini birden çok(k) kez farklı random splitlerle ayırıp o kadar çalıştırıp ortalamasına bakıyoruz.

Bu yukarıdaki gösterimde bir de outer loop düşünebilirsiniz. Diyelim ki 3 ayrı modeliniz var, her biri için tek bir parametre setiyle 10 fold cv yapıyorsunuz, her birinin ortalamasını alıp, bu 3 modelden en büyük ortalamayı seçeriz. Böyle bir durumda toplamda 30 kez model çalışmış olur. Tabi en yüksek ortalamalı modelde cv sonuçlarının standart sapmasına bakmak gerekir; ortalamaya göre yüksek bir standart sapma varsa, yani her bir foldun model sonuçlarında dalgalanma varsa, bu modele güvenmemek gerekir.

Tek bir modelin farklı parametrelerine de bakmış olabiliriz. Gridsearch ile toplam 120 farklı kombinasyona bakılabilir, bunu 10 kez yapınca 1200 kez model çalışmış olur. Yine en iyi seçeneğin cv sonuçlarının standart sapmasına bakarız.

Tek bir modelin tek bir parametre setine de bakılabilir. O zaman toplam 10 kez çalışmış olur. Keza burda da yine en iyi seçeneğin cv sonuçlarının standart sapmasına bakarız.

Standart sapma konusunu açacak olursak, 10 fold’un skorlarını birbirinden çok da farklı olmamasında fayda var. Biri %60larda kalanı %90 civarında olmamalı. Aslında olursa da çok büyük bi sorun yok, zaten bunların ortalamasını alırız ve 60lık skor ortalamayı düşürecek ve biz de diyeceğiz ki, sahadan gelen veriyi tahminleme beceresi %87((9*90+60)/10) civarı olacak. Ama yine de dalgalanma varsa şüpheyle yaklaşmak gerekir.

Özetle, cross validation’ı daha önceki bir gönderimde belirttiğim gibi, overfit edip etmediğini anlamak için yapıyoruz.(Bazı kaynaklarda belirtildiği gibi overfittingi engellemek için değil.)

Burda her bir fold için başarı oranı 90,92,89 gibi 90 civarında dalgalanabilir ve ortalaması 90 çıkabilir. Biz de biliriz ki, modelimizin ortalama training accuracyci 90 civarı olacak. Halbuki tek bir fold ile baksaydık belki tesadüfen 93 çıkacaktı. Sonra bir de test setiyle bakıp, orda da 88 gibi birşey görürsek kısmen overfit ediyor derdik, çünkü train ve test arasında fark var ama aslında 93ten 88'ee değil, 90dan 88e düşmüş, yani o kadar da yüksek bi overfitting yok.

Bu arada iterasyonlardan biri 44 gibi bir accuracy’e sahipse veya birbiriyle alakasız oranlar varsa modelimizde sorun var demektir. Model tam öğrenemiyordur veya verimiz çok karmaşıktır, bizim de modelimizdeki kompleksliği artırmamız gerekmektedir. Bu noktada gerçek test seti(yeşil) üzerinde denemeye geçmeyiz bile. Yapacağımız şey, modelimizi düzeltmek olmalıdır. Nasıl düzelteceğimize biraz ileride bakacağız.

k, genelde 3,5,10 gibi bi değer seçilir. Verimiz çok büyükse 3 yeterlidir, yoksa 10a kadar çıkılabilir. Şimdi kodlamasına bi göz atalım.

from sklearn.model_selection import cross_val_score
scores = cross_val_score(lr, X_train, y_train, cv=3, scoring='accuracy')
print(scores)
scores.mean(), scores.std()
[0.79012346 0.87654321 0.8375 ]
Out[98]:(0.8347222222222221, 0.03533535026868965)

Farkettiyseniz modelin fit metodunu kullanmadık, cross_val_score bunu internal olarak yapıyor. Bundan sonraki aşamalarda da modelin kendisini doğrudan fit ettirmeyeceğiz.

Sonuçlara gelecek olursak, dalgalanma var gibi ama ortalama perf %82 olcak diyebiliriz. Peki logistic regresyon modelinin C parametresinin farklı değerleri için denesek sonucumuz ne olurdu, ona bakalım.

In [99]:

cv_scores = [] ; training_scores = []
c_range = [0.001, 0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 1]
for c in c_range:
print("c=",c,"için değerler")
Clf = LogisticRegression(C = c, solver='lbfgs') # default penalty = l2 used
scores = np.round(cross_val_score(Clf, X_train, y_train, cv=3, scoring='accuracy'),2)
cv_scores.append(scores.mean())
print(f"Skorlar:{scores}, ortalama skor: {scores.mean()*100:.2f}%, skor std:{scores.std():.2f} ",end="\n\n")

Tekrar etmekte fayda var, burdaki skorlar modelin genel performansı değildir, şuan hala training set üzerinde çalışıyoruz ve sadece en uygun parametre setini seçmeye çalışıyoruz. Bu parametrelerle test set üzerinde de çalışıp o zaman model performansını elde edeceğiz ve bu skorları karşılaştıracağız.

Peki sadece C değil başka parametreler için de en iyi değeri bulmaya çalışsaydık? Bunun cevabı GridSearch’te. Birazdan oraya da geliyoruz.

Repeated cv

Devam edelim, cv yerine bi rakam yazılabileceği gibi, oraya bi cv iterator de konulabilir. Mesela RepeatedKFold. Bu, KFold’u n kere yapmaya yarar ve bu da generalization performansımızı daha da gerçekçi hale getirir. Eğer verisetimiz imbalanced olsaydı o zaman da StartifiedRepeteadKfold yapardık. Veriseti büyüklüğüne göre değişmekle birlikte küçük-orta bi verisetinde 100 repeat, 10 fold yapılabilir. Biz basit olsun diye 5–10 yapalım.

from sklearn.model_selection import RepeatedKFold
mycv = RepeatedKFold(n_splits=5, n_repeats=10, random_state=1)
model = LogisticRegression(C=1, solver='newton-cg')
scores = np.round(cross_val_score(model, X_train, y_train, scoring='accuracy', cv=mycv, n_jobs=-1,verbose=1),2)
print(f"Skorlar:{scores}, ortalaması: {np.round(scores.mean(),2)}")

Peki, cv prosedürünü en iyi modeli seçmek için, test setini de genelleştirilmiş performansı ölçmek için kullanıyoruz dedik. k-fold ile validation setteki varyans sorununu çözdük, hatta bunu repeated yaparak model stabilitesini de daha iyi ölçtük, güzel. Ama elimizde sadece bir tane test set var. Peki ya bu test setindeki varyans sorunu ne olacak? Validationda yaptıklarımızı onda da yapmak lazım değil mi? Lazım. Zira farklı bir test seti ile farklı bir generalization performansı elde edebiliriz. Bunun çözümü de Nested Cross Validationda.

4.yöntem: Nested k-fold cross validation

Tahmin edeceğiniz üzere, bu işlem toplam model çalıştırma süresini daha da uzatır. O yüzden büyük veri setlerinde yapmak mantıklı olmaz. Bunun da implementasyonuna girmeyeceğiz, ancak şurada bir kod örneği bulabilirsiniz.

GridSearchCV ve RandomizedSearchCV

En iyi hyperparametre kombinasyonunu bulmak için uygulanan yöntemlerdir.

GridSearch

Grid searchte, tüm parametreler bir gride yazılır. Modelin parametreleri bir dictionary şeklinde belirtilir. Birden fazla değer girilecekse, ki grid kullanmanın esas sebebi budur, bunları list şeklinde gireriz.

Grid tamamlandıktan sonra model değil grid nesnesi fit edilir. Bu da aslında arka planda her bir kombinasyon için modeli fit eder. Gridsearch kendi içinde cross validation da yapar, bu nedenle bi cv parametresi vardır, cross_val_score’da olduğu gibi bir sayı girebilir veya bi cv iterator yaratıp onu atayabiliriz.

Fitting 15 folds for each of 40 candidates, totalling 600 fits

LogisticRegression’ın dokümantasyonuna göre penalty parametresinin l1 olması durumunda sadece liblinear solver’ı kullanabiliyormuşuz. O yüzden aşağıdaki gibi bir list of dictionary vererek işimizi görürüz.

Fitting 15 folds for each of 48 candidates, totalling 720 fits
Wall time: 3.32 s

GridSearch içinde kaç model çalışacağını şöyle hesaplayabilirsiniz:

Burada, d=params içindeki dictionary sayısı, t=bir dictionary içindeki item sayısı, V de dictionarydeki Value’lar, yani ilgili parametrenin alacağı değerlerin listesi oluyor. Bu hesaba göre;

(8x1x1 + 8x1x5)=48 → 48x3x5=720

grid.best_estimator_#refit=True olduğu için şuan elimizde bir 
estimator var
Out[103]:LogisticRegression(C=0.05, max_iter=1000, solver='newton-cg')
y_pred = grid.predict(X_train) #refit=True olduğu için şuan elimizde bir estimator var, ve predict edebiliyoruz
print(grid.score(X_train,y_train)) #refit edilmiş modelin tüm train datası üzerindeki nihai skoru
0.8429752066115702
grid.best_params_Out[105]:{'C': 0.05, 'penalty': 'l2', 'solver': 'newton-cg'}
#validation setlerinin ortalama skoru
grid.best_score_
Out[106]: 0.8239609053497943

best_score_ attribute’ü ve score metodu sonuçları farklı, yanlarına açıklama yazdım ama isterseniz şuraya da bakabilirsiniz.

#skorlara bakalım, en yüksek olanı da kırmızı renkli gösterelim
from colorama import Fore #renkli text için
cvres = grid.cv_results_
for params,mean_score in zip(cvres["params"],cvres["mean_test_score"]):
if mean_score==grid.best_score_:
print(Fore.RED + f"{params} parametreleriyle sonuç: {mean_score:.3f}")
else:
print(Fore.RESET + f"{params} parametreleriyle sonuç: {mean_score:.3f}")

veya şöyle bir dataframede göstermek daha şık olacaktır.

ml.gridsearch_to_df(grid)

NOT:Bu gridsearch olayının bir tık ilerisi, olası tüm algoritma türleri için model çalıştırmaktır. Bunlara Auto ML yaklaşımı deniyor. Vaktim olursa bir ara böyle bi çalışma da yapacağım.

RandomizedSearch

İlk başlarda çok deneyiminiz yokken, gridsearchte gride ne yazacağınızı bilmeyebilirsiniz. Hele hele neural network’lerde göreceğimiz gibi hyperparametre sayısı çok ise ilk başta RandomizedSearch kullanmak daha akıllıca olacaktır.

Burada tüm kombinasyonlar denenmez, sadece bir kombinasyon denenir. Parametre uzayını da geniş tutarak rastgeleliği iyice sağlama alırız. Bunun için olası değerleri tek tek girmek yerine aralık fonksiyonlarından yararlanırız. GridSearchten farklı olarak burada, verilen değerlerden sadece biri rasgele seçilir, ama bu işlem n_iter parametresindeki rakam kadar tekrarlanır.

Fitting 15 folds for each of 10 candidates, totalling 150 fits
Wall time: 1.09 s
ml.gridsearch_to_df(rs)

Önemli hususlar

  • Kodların hızlı çalışması için paralel işlem gücünden yararlanılabilir. Bunun için n_jobs parametresine -1 denir, böylece çalışılan bilgisayardaki tüm core'lar kullanılır. İsterseniz buraya core sayısından daha küçük bir değer de yazabilirsiniz.
  • refit=True denirse, en iyi parametrelere göre model çalıştırılıp ilgili değişkene atanır, böylece en uygun parametrelere göre tekrardan çalıştrma derdi olmaz, özellikle uzun çalışan bir model ise.
  • Tahmin edileceği üzere Gridsearch çok maliyetli bi işlemdir. Kullanılan değerlere göre sadece tek bir algoritma için birkaç bin farklı model çalışabilir. Bir de farklı algoritmalar denediğinizi düşünün, sayı iyice artar. Burda şu öneriliyor, mesela 3 farklı algoritma için özellikle sabit rakam girilen parametrelere az sayıda değer vererek([1,2,3,4,5,6,7,8,9,10] değil de [1,5,10] girmek gibi) gridsearch yapmak, sonra en iyi model hangisiyse onu seçip bu sabit değerlerden de hangisi en iyi çıktıysa onun etrafındaki rakamlardan birkez daha sadece şampiyon algoritmayı çalıştırmak(az önceki rakamlarda 5 çıktı diyelim, [3,4,5,6,7] vermek gibi). Hatta bazı parametrelerde ardışık vermek yerine, exponansiyel verin, sonra en iyisinin etrafında ardışık verebilirsiniz. Ör: [0.1, 0.2,0.3….0.9, 1] yapmak yerine 0.001, 0.01, 0.1 ve 1 diyip atıyorum bunlardan en iyisi 0.1 çıkarsa, sonra 0.1, 0.2, 0.3 verebiliriz.
  • Tabi burda en ideali şu, 3 algoritma yerine belki 7–8 algoritma çalıştırıp 3 tanesini şampiyon seçmek ve bunlara bu ince ayarı yapmak, sonra bu 3ünü de ileride göreceğimiz gibi Ensemble bir modele(ör:VotingClassifer) sokmak olacak.
  • Bunların halving versiyonu da var, ki çok daha hızlı search yapılmasını sağlar, aşağıda bunları göreceğiz.
  • scoring parametresi için f1 türevleri verileceği zaman make_scorer fonksiyonundan yararlanılır. Bunun bir örneğini Part II’da yapacağız. Ör: make_scorer(f1_score , average=’macro’)
  • https://github.com/hyperopt/hyperopt adresinde ise bu işte özelleşmiş başka bir kütüphane bulunuyor.

Adım adım GridSearch

Yukarıdaki verdiğimiz ipuçlarından faydalanıp, C parametresinin en uygun değerlerine adım adım yaklaşmayı deneyelim.

#ilk olarak şu değerleri verelim
np.set_printoptions(suppress=True)
newCrange=np.logspace(4,-5, num=10)
newCrange
Out[112]: array([10000. , 1000. , 100. , 10. , 1. , 0.1 , 0.01 , 0.001 , 0.0001 , 0.00001])
Fitting 15 folds for each of 60 candidates, totalling 900 fits
Wall time: 4.49 s
grid.best_params_Out[114]:{'C': 0.1, 'penalty': 'l2', 'solver': 'liblinear'}
grid.best_score_Out[115]:0.8231172839506172

C için en iyi değer 0.1 çıktığına göre, artık 0.1 civarında yeni ince ayar yapabiliriz.

Fitting 15 folds for each of 30 candidates, totalling 450 fits
Wall time: 1.64 s
grid.best_params_Out[117]: {'C': 0.05, 'penalty': 'l2', 'solver': 'newton-cg'}
grid.best_score_Out[118]:0.8239609053497943

C'yi 0.1'den 0.05'e çekmek performansta küçük de olsa bir artış sağladı.

Halving GridSearch

Normal GridSearch tüm dataya bakarken bu yaklaşımda datanın belli bi subsample'ına bakılıyor. Çıkan sonuçlara göre en yüksek skoru veren adaylar(sayısı, mevcudun yarısı kadar) ilk subsample’ın belli bir katı kadar(factor) data üzerinde tekrar eğitiliyor, ta ki en iyi sonuç bulunana kadar.

Parametreler içindeki "resource" geçen şeyler aslında elimizdeki subsample'ı gösteriyor. Tüm proses, factor ve min_samples seçimleriyle kontrol ediliyor. Bunların nasıl seçilmesiyle ilgili, üstelik ilgili algoritmaları da açıklayan çok güzel bir yazıyı şurada bulabilirsiniz.

n_iterations: 2
n_required_iterations: 4
n_possible_iterations: 2
min_resources_: 60
max_resources_: 242
aggressive_elimination: False
factor: 3
----------
iter: 0
n_candidates: 48
n_resources: 60
Fitting 15 folds for each of 48 candidates, totalling 720 fits
----------
iter: 1
n_candidates: 16
n_resources: 180
Fitting 15 folds for each of 16 candidates, totalling 240 fits
Wall time: 3.36 s

Süre anlamında çok büyük bi fark olmamış gibi görünüyor ancak, daha büyük bir parametergrid içinde bu fark hissedilebilir düzeye gelecektir. Part II’de bunu göreceğiz. Ama performans sonucu olarak hemen hemen aynı sonuçları elde edebiliyoruz.

ml.gridsearch_to_df(grid_h)

Halving RandomizedSearch

n_iterations: 2
n_required_iterations: 2
n_possible_iterations: 2
min_resources_: 60
max_resources_: 242
aggressive_elimination: False
factor: 3
----------
iter: 0
n_candidates: 4
n_resources: 60
Fitting 15 folds for each of 4 candidates, totalling 60 fits
----------
iter: 1
n_candidates: 2
n_resources: 180
Fitting 15 folds for each of 2 candidates, totalling 30 fits
Wall time: 741 ms
Kaynaklar

Part II için buraya tıklayınız

Okuduğunuz için teşekkür ederim.

--

--

Volkan Yurtseven

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