Makine Öğrenmesi Algoritmalarında Hatayı Azaltma Yolları

Saltuk Bugra Karacan
8 min readJul 29, 2020

Merhaba, bu yazımda belirli adımlar kullanarak; Hitters veri setinde tahmin ettiğimiz salary (maaş) değerlerindeki hatayı azaltmaya çalışacağız.

Bu notebook’a Github ve Kaggle hesabımdan ulaşabilirsiniz. Ayrıca soru, görüş ve önerileriniz için de bana LinkedIn hesabımdan ulaşabilirsiniz.

Yapacağımız tahminler öncesinde, veri setini iyi tanımak ve anlamak çok ama çok önemlidir. Ayrıca veri setinin alakalı olduğu konu hakkında da biraz bilgi sahibi olmak gerekir. Hitters veri seti beyzbol oyuncularının olduğu bir veri setidir ve beyzbol hakkında hiçbir bilgi sahibi olmadan yapacağımız işlemler hatamızı düşürmeyebilir. Dolayısıyla işe veri setimizi tanıyarak başlamalıyız.

Hitters Veri seti

Hitters veri seti, Major ligindeki beyzbol oyuncularının 1986–87 yıllarındaki belirli istatistikleri ve maaşlarını bulunduran bir veri setidir.

Bu veri setinde bulunan featureleri (özellikleri) tanımak için Kaggle’dan veri setinin açıklama kısmını okuyabilirsiniz. https://www.kaggle.com/floser/hitters

Veri setini anlama kısmını bitirdik. Beyzbol konusunda uzman olmasam bile artık kafamda bazı şeyler oturmuş oldu :).

Öncelikle işimize yarayacak kütüphaneleri import ederek projemize başlayalım.

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn import model_selection
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn import neighbors
from sklearn.svm import SVR
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
from sklearn import preprocessing

from warnings import filterwarnings
filterwarnings('ignore')

Sade Tahmin

Şimdi, hiçbir işlem yapmadan algoritmalarda direkt olarak tahmin yapacağız ki, yaptığımız işlemlerden sonra hatamızın ne kadar düştüğünü görebilelim.

Ama öncelikle bütün algoritmalarımızın sağlıklı çalışması için NaN yani boş olan değerler ve kategorik değişkenler (League, Division, NewLeague değişkenleri) için birkaç işlem yapmak zorundayız.

df = pd.read_csv("Hitters.csv")
#veri setini okuttuk.
df.drop(["League","Division","NewLeague"],axis=1,inplace=True)
#Kategorik değişkenlerin bulunduğu sütunları (League,Division,NewLeague) tamamen kaldırdık. Bu doğru bir şey değil çünkü veri kaybetmiş oluyoruz. Ama zaten şimdi yapmamamız gereken şeyleri yapıyoruz. İleride bunun yerine ne yapabileceğimizi göreceksiniz.
df.dropna(inplace=True)
#na yani boş değer bulunduran satırları tamamen siliyoruz. Yine bu da veri kaybına yol açar. Çünkü 1 oyuncunun sadece bir özelliği boş olsa bile oyuncuyu ve bilgilerini tamamen siler.

Yukarıdaki silme işlemlerini yapmamızın sebebi algoritmalarımızın boş(na) ve harf bulunduran kategorik değişkenleri kullanamamasıdır. Bunları modelin içine yolladığımız takdirde hata alırız. İleride bunları silmek yerine başka yöntemlerle kullanabileceğiz.

Şimdi mevcut haliyle algoritmaların hatalarını görelim.

Hatamızı RMSE değeri ile hesaplayacağız. RMSE’nin açılımı Root-mean-squared error. Bütün değerler için tahmin edilen değer ve gerçek değerin farkının karesinin kökü alınır. Daha kolay anlaşılması için formül:

Şimdi RMSE yani hata değerlerimizi bulacağız.

models = []

models.append(('KNN', KNeighborsRegressor()))
models.append(('SVR', SVR()))
models.append(('CART', DecisionTreeRegressor()))
models.append(('RandomForests', RandomForestRegressor()))
models.append(('GradientBoosting', GradientBoostingRegressor()))
models.append(('XGBoost', XGBRegressor()))
models.append(('Light GBM', LGBMRegressor()))
models diye bir liste oluşturduk ve kullanacağımız algoritmaları kısa isimleriyle birlikte tuple olarak listeye ekledik.

Bir liste içine algoritmaları eklediğimiz için for ile listede dönerek bütün algoritmaları tek tek yazmadan çalıştırabiliriz.

X = df.drop("Salary",axis=1)
y = df["Salary"]

X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2,random_state=42)

for name,model in models:
mod = model.fit(X_train,y_train)
#trainleri modele fit etmek
y_pred = mod.predict(X_test)
# tahmin
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
#rmse hesabı
print(name,rmse)
#yazdırılacak kısım
print("-------------")
Algoritmaya eklemek için X ve y değerlerini atadık, split işlemine aldık. 0.8 train, 0.2 test olarak ayırdık ve fit-predict ile tahminlerimizi yaptık. rmse hesapladık ve tüm sonuçları gördük.

Tahminler sırayla tüm algoritmalara uygulandı. RMSE, hatayı gösterdiği için RMSE değeri ne kadar büyükse; bizim için o kadar kötü, hatalı bir sonuçtur. Burada görünen en başarılı algoritma olan Gradient Boosting ile Maaşları 287 birim hatalı tahmin ediyoruz.

Bu da demek oluyor ki 500 birim olan bir maaşı 787 birim veya 213 birim olarak tahmin ediyoruz. Peki bu rmse, hata değerini nasıl azaltabiliriz?

RMSE Değerini Azaltmanın Bazı Yolları

df = pd.read_csv("Hitters.csv")
#veri setini tekrar yükleyelim ve çalışmalara başlayalım.

Feature Engineering

En başta veri setini anlama kısmını vurgulamamın sebebi Feature Engineering kısmıdır. Kısaca feature engineering, elimizde bulunan değişkenlerden başka değişkenler üretmektir.

Yukarıda verdiğim linkte belirli değişkenler ve bu değişkenlerin “C” ile başlayan halleri var. Bunların birisi “86–87” sezonundaki diğeri ise “kariyerindeki” anlamına gelir.

Örneğin:

Hits = Oyuncunun 86–87 sezonunda yaptığı vuruş sayısı

CHits = Oyuncunun kariyerinde yaptığı vuruş sayısı

Biz yeni bir değişken oluşturup bu sezon yaptığı vuruşları kariyerinde yaptığı vuruşlara oranlarsak belki de oyuncunun istikrarlı olup olmamasıyla ilgili çok önemli bir bilgi elde etmiş olacağız. Daha birçok şekilde yeni değişkenler oluşturabilirsiniz. Bazı oluşturduğunuz değişkenler hatanızı arttırabilir de. Deneme yanılma yoluyla en doğrusunu bulabilirsiniz.

df["AvgCAtBat"] = df["AtBat"] / df["CAtBat"] 
df["AvgCHits"] = df["Hits"] / df["CHits"]
df["AvgCHmRun"] = df["HmRun"] / df["CHmRun"]
df["AvgCruns"] = df["Runs"] / df["CRuns"]
df["AvgCRBI"] = df["RBI"] / df["CRBI"]
df["AvgCWalks"] = df["Walks"] / df["CWalks"]
#Yeni değişkenlerimizi ürettik.

Bir de Years değişkenine sahibiz. Bu değişkende 1 ile 24 arasında değerler bulunuyor. Bu durumda yeni bir değişken oluşturup oyuncuları tecrübelerine göre gruplayabiliriz.

Örneğin:

1–4 yıl = 1. grup | 5–8 yıl = 2. grup | … | 21–24 yıl = 6.grup

df['Year_lab'] = pd.qcut(df['Years'], 6 ,labels = [1,2,3,4,5,6])
#yılları yukarıdaki gibi 6'ya ayırdık.
df.head() #DataFrame e yeni eklenen değerler şöyle gözüküyor.

Preprocessing

One Hot Encoding

One Hot Encoding, elimizdeki kategorik değişkenleri sayısal verilere dönüştürür.

df = pd.get_dummies(df,drop_first=True)
#drop_first özelliği elimizde 3 kategori varsa bunu 3 sütun yerine 2 sütun olarak yazmaya yarar.
Çünkü yukarıdaki resme baktığımızda yellow = 0 | green = 0 görürseniz red = 1 olacağı anlaşılır. Dolayısıyla 3. sütunu görmenin pek anlamı kalmaz ve modelimizi boş yere yorar.
One Hot Encoding’den sonra yeni eklenen sütunlar bu şekilde olur.

Na (Boş) Değerler

Elimizdeki boş değerleri silmek yerine KnnImputer ile doldurabiliriz. KnnImputer, boş değerin en yakınındaki bizim belirlediğimiz sayıdaki komşularına bakar ve bu değerlerin ortalamasını alıp boş olan değerimize atar.

from sklearn.impute import KNNImputer 
imputer = KNNImputer(n_neighbors = 4)
#en yakın 4 değer
df_filled = imputer.fit_transform(df)
# en yakın 4 değerin ortalamalarını alıp boş değerlere atama yapılır.
df = pd.DataFrame(df_filled,columns = df.columns)#df_filled ile boş değerleri doldurulmuş verimizi tekrar DataFrame formatına dönüştürdük.
boş değerleri sorguladığımızda 0 çıkar çünkü KNNImputer ile doldurduk

Aykırı Değerleri Baskılama

Aykırı değerler bir formülle belirlenir.

Bu formüle göre bir değişkenimizin üst ve alt sınırları şöyle belirlenir:

İlk önce elimizdeki değişkenin birinci ve üçüncü çeyrekliklerini buluruz.

Aşağıdaki görsele göre;

Birinci çeyreklik (Q1) = 25% = 190.25

Üçüncü çeyreklik (Q3) = 75% = 739.375

IQR = Q3 — Q1

Üst sınır değeri = Q3 + 1.5*IQR

Alt sınır değeri = Q1 – 1.5*IQR

Üst ve Alt sınır değerleri böyle bulunur.

Şimdi kutu grafiği (boxplot) ile Salary değişkeninde aykırı değerlerimizi gözlemleyeceğiz

import seaborn as snssns.boxplot(x = df["Salary"]);

Bu grafiğe göre sağ tarafta kalan küçük küçük görünen noktalar aykırı değerlerimiz.

Şimdi bunları yukarıdaki formül ile baskılayacağız.

Q1 = df.Salary.quantile(0.25)
Q3 = df.Salary.quantile(0.75)
IQR = Q3-Q1
lower = Q1 - 1.5*IQR
upper = Q3 + 1.5*IQR
df.loc[df["Salary"] > upper,"Salary"] = upper
#Yukarıdaki formülün aynısı
upper değerinin üstündeki maaş değerlerini direkt olarak upper değerine eşitledik.

Şimdi kutu grafiğine bakalım ve artık aykırı gözlem olmadığını görelim.

sns.boxplot(x = df["Salary"]);

Göründüğü gibi artık hiç aykırı gözlemimiz yok.

Local Outlier Factor

Local Outlier Factor, bir noktanın anormallik derecesini veren bir algoritmadır. (Daha fazla bilgi için okuyabilirsiniz: https://www.veribilimiokulu.com/local-outlier-factor-ile-anormallik-tespiti/)

df_scores = lof.negative_outlier_factor_
np.sort(df_scores)[0:30]
#negatif outlier faktörlerin ilk 30 tanesini yazdırıyoruz(en anormalden itibaren) karşımıza bazı sayılar çıkıyor. Bu sayılar -1'den uzaklaştıkça daha anormal değerler(daha outlier) anlamına geliyor.
threshold = np.sort(df_scores)[7]outlier = df_scores > threshold
df = df[outlier]
#-2.3670826306785706 değerinden önce gelen değerleri tamamen siliyoruz. Çünkü bunlar fazla anormal değerler. İstesek başka değerleri de silebilirdik veya hiç silmeyebilirdik de. Ama amacımız hatayı azaltmak.

Evet işlemlerimizi artık bitirdik. Şimdi sırada tekrar tahmin kısmı var. Bakalım hatamız ne kadar düştü görelim.

X = df.drop("Salary",axis=1)
y = df["Salary"]

X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2,random_state=42)

for name,model in models:
mod = model.fit(X_train,y_train)
y_pred = mod.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(name,rmse)
print("-------------")

Son değerlerimiz bu şekilde. Bu yaptığımız işlemler sonucu RMSE değerlerimizde büyük düşüşler gözlemledik.

En iyi çalışan algoritmamız olan Random Forests’a göre 500 birim olan bir maaşı 692 birim veya 308 birim olarak hesaplarız.

Şimdi eski ve yeni RMSE değerlerini karşılaştıralım:

KNN eski: 404.7724696402799 | KNN yeni: 208.71618694985816

SVR eski: 423.78981162543784 | SVR yeni: 352.843521491989

CART eski: 523.5755791709253 | CART yeni: 278.26612830733063

RF eski: 325.90335498191894 | RF yeni: 192.39295800691607

GBM eski: 287.5041840549363 | GBM yeni: 206.77155746936847

XGBoost eski: 297.42613210863027 | XGBoost yeni: 227.1291165202612

LGBM eski: 351.67191671024233 | LGBM yeni: 202.06220079973812

Gördüğünüz gibi eski ve yeni değerler arasında çok büyük farklar var. Yaptığımız işlemler ışığında hatayı elimizden geldiğince azalttık.

Farklı yöntemler kullanarak hatayı daha da düşürmek tabii ki mümkün eğer siz de farklı yöntemler düşündüyseniz paylaşabilirsiniz.

Ayrıca bana LinkedIn üzerinden ulaşabilirsiniz: https://www.linkedin.com/in/sbkaracan/

--

--

Saltuk Bugra Karacan

M. Sc. Informatics @ Technical University of Munich | AI Engineer WS @ Retorio