R Üzerinde Güçlendirilmiş Karar Ağacı: İnşaat Ruhsatları Üzerine Bir Çalışma

Bir önceki yazımda güçlendirilmiş karar ağacı yöntemiyle bir gelir tahminleme modeli kurmuştum ve bir sonraki yazının da R üzerinde olacağını söylemiştim. Bu yazıda da aynı yöntemi R üzerinde uygulayacağız fakat bu kez farklı bir veri seti ve farklı bir güçlendirme(boosting) algoritması kullanacağız. Bunların ne olduğuna yazı ilerledikçe değineceğim. Bir önceki yazıda boosting metodunun ne olduğundan, karar ağaçlarından ve kullandığımız boosting algoritmasından teorik olarak bahsettiğim için, bu yazıda teorik kısmı çok kısa tutup ağırlığı uygulamaya vereceğim. Ayrıca yine bir önceki yazıdan farklı olarak, “hyperparameter tuning” işlemini daha detaylı tutacağım. Böylece kurduğumuz modeli geliştirme ve optimize etmeye daha çok odaklanacağım.

İlk aşamada yine kullanacağımız veri setini tanıyalım ve hedefimizin ne olduğunu açıklayalım. Elimizdeki veri seti San Francisco’da verilen inşaat ruhsatları ile alakalı. San Francisco Açık Veri Portalı’ndan 1 Ocak 2013 ve 25 Şubat 2018 tarihleri arasındaki inşaah ruhsatlarının incelenmesi amacıyla indirilmiş. Veri hakkında detaylı bilgi, indirme linki ve değişkenlerin ne olduğu aşağıdaki link içerisinde bulunabilir:

San Francisco İnşaat Ruhsatları Verisi

Biz burada verilen ruhsatın/iznin tipini tahminlemeye çalışacağız. Ruhsat tipleri numaralandırılmış biçimde veride bulunuyor. “Permit Type Description” kolonu da bu numaraların ne ifade ettiğini bize bildiriyor.

Veriyi R içerisine alarak başlayalım.

getwd()
Data <- read.csv("Building_Permits.csv", stringsAsFactors = FALSE)
nrow(Data)
ncol(Data)
head(Data, n = 20)

"getwd" komutu ile üzerinde çalıştığım dosya yolunun ne olduğunu öğrendim ve veriyi oraya kopyaladım. Ya da siz çalışma yolunu değiştirip veriyi oraya da alabilir ya da direkt olarak verinin bulunduğu klasör üzerinde çalışabilirsiniz. "stringsAsFactors" argümanını da FALSE yaptık ki string değişkenler otomatik olarak factor tipinde gelmesin. Veriyi incelediğimizde:

12

Veri 198900 gözlemden(satırdan) ve 43 değişkenden(sütundan) oluşuyor. Verinin ilk 20 satırı için ufak bir kısmı üstteki görsele ekledim. Kolon adlarında normalde boşluk varsa R bunları “.” işareti ile doldurduğu için bazı kolon adları bu şekilde. İşlemlere başlamadan önce veri üzerinde bazı oynamalar yaparsak iyi olacak. Örneğin üstte bahsettiğim “Permit Type” kolonu bizim için aslında yeterli olacaktır. Veya tam tersi “Permit Type Description” değişkenini tutup “Permit Type” değişkenini de kaldırabilirdik çünkü ikisi aslında aynı şeyi ifade ediyor. Fakat zaten kodlanmış hali bulunduğu için “Permit Type” değişkenini tutalım. Aynı durum “Proposed Construction Type” ve “Existing Construction Type” değişkenlerinde de bulunuyor. Bu yüzden bunların da description değişkenlerini kaldıracağız. Son olarak da “Record ID” değişkeni bizim için bir şey ifade etmiyor bu yüzden bunu da verisetinde bulundurmayı düşünmüyorum. Bu yaptığımız aslında önceden hangi değişkenlerin bizim için bir yarar sağlamayacağını bildiğimizden dolayı bunları elemek. Burada herhangi bir istatistiksel metod kullanmıyoruz. Bunun dışında tabii ki bu metodlarla veya algoritmalarla yapılan değişken elemeleri de var fakat bu ayrı bir konu. Ek olarak, daha anlaşılır olması açısından bağımlı değişkeni de verisetinin son kolonu olacak şekilde taşıyalım.

Üstte söylediklerimizi gerçekleştirerek devam edelim:

Data <- Data[ , -c(3, 35, 37, 43)]
Data[ , 40] <- Data$Permit.Type
Data <- Data[ , -2]
names(Data)[39] <- "Permit.Type"

Kodu açıklayacak olursak -c(3, 35, 37, 43) ifadesiyle bu indekslerdeki kolonların veriden çıkarılmasını söylüyorum. Kolonların bulunduğu sıra direkt olarak indekslerine eşit oluyor çünkü R, Python'ın aksine indekslemeyi 0'dan değil 1'den başlatır. Geriye 39 kolonlu bir veri seti kalıyor ve bunun 40. kolonunun oluşturulmasını ve "Permit Type" değişkeni ile aynı olmasını söylüyorum. Bunu yaptıktan sonra asıl "Permit Type" değişkenini atıyorum ve yeni oluşturduğum en sondaki kolonun ismini de "Permit.Type" koyuyorum. Bu işlemleri hallettiğimize göre "Data Preprocessing" aşamasına geçip veriyi makine öğrenimine hazır hale getirebiliriz.

Daha önceki yazılarımda bu aşamanın temelde üç adımdan oluştuğunu söylemiştim. Kayıp verilerin incelenmesi ve çözüm üretilmesi, kategorik değişkenlerin kodlanması ve özelliklerin(feature) normalize edilip belirli bir aralığa sıkıştırılması. Bunların hepsinin neden yapıldığından da bahsetmiştik. Bu aşamaları teker teker gerçekleştirip veriyi hazır hale getirelim.

Aşağıdaki kod bloğu ile istediklerimizi uygulayalım:

#Bölüm 1 - Çok fazla kayıp veri içeren kolonları kaldırma
Data <- Data[ , -c(6, 10, 16, 18, 21, 22, 23, 31, 34, 38)]
Data <- Data[ , -c(3, 4, 5, 6, 7, 8, 9)]
as.data.frame(sapply(Data, class))
names(which(sapply(Data, anyNA)))

Üstteki kod bloğu birinci bölümü oluşturuyor. Öncelikle ilk satırda indekslerini kullanarak veri setinden çıkarttığımız kolonlar çok fazla kayıp veri içeriyordu. Bu kolonların adlarını buraya uzun uzun yazmıyorum. İndekslerinden yola çıkarak veri seti içerisindeki hangi kolonlar olduklarını bulabilirsiniz. Fazla miktarda kayıp veri içerdikleri için bunlara "imputing" metodunu da uygulayamayız. Çünkü nerdeyse bir değişkeni kendimiz uydurmuşuz olur ki bu veri setine yüksek derecede "bias" yani yanlılık katacaktır. Bu yüzden bu kolonları veri setinde tutmadık. İkinci satırda çıkardığımız veriler ise adres ile alakalı olan verilerdi. Aslına bakarsanız veri setindeki "zipcode" değişkeni adrese dair her bilgiyi içerdiğinden, bu kolonların veride bulunmasına da gerek yok. Son olarak da "Location" kolonu coğrafik veri içerdiğinden ve coğrafik veri ekstra ilgi istediğinden makaleyi daha fazla uzatmamak adına bu kolonu da veriden çıkardım. Bunları da veriden çıkardıktan sonra değişkenlerin tiplerine baktık ve aşağıdaki sonucu elde ettik:

3

“sapply” komutu veri setindeki her kolona “class” fonksiyonunu uyguladı ve hepsinin veri tipini aldı. Sonucun güzel gözükmesi açısından da bu sonucu bir “data frame” nesnesi tipinde gördük. Burada dikkat edersek her kolon olması gereken veri tipinde değil. Örneğin tarih kolonları “character” tipinde gözüküyor. Bunun gibi başka kolonlar da olup olmadığını inceledim ve tarihler dışında değişmesi gereken olmadığını gördüm. ayrıca bunların hangileri kayıp veri içeriyor bunu da görmek için son satırı kullandım ve sonuç:

4

Veri seti içerisinde kayıp veri içeren kolonlar bunlar. Kod bloğu yine “sapply” komutu ile tüm kolonlara “anyNA” fonksiyonunu uygulayarak “NA” veriler içerip içermediğini kontrol ediyor. “which” komutu “NA” içerenlerin indekslerini alıyor ve “name” komutu da bu indekslerin kolon adlarını getiriyor. Bunların kayıp veri oranı nedir, nasıl icabına bakacağız bunları yazı ilerledikçe halledeceğiz. Devam edelim ve ikinci bölümde veri tiplerini düzeltelim:

#Bölüm 2 - Tarih kolonların tipini karakterden tarihe çevirme
Data$Permit.Creation.Date <- as.Date(Data$Permit.Creation.Date, format = "%m/%d/%Y")
Data$Current.Status.Date <- as.Date(Data$Current.Status.Date, format = "%m/%d/%Y")
Data$Filed.Date <- as.Date(Data$Filed.Date, format = "%m/%d/%Y")
Data$Issued.Date <- as.Date(Data$Issued.Date, format = "%m/%d/%Y")
Data$First.Construction.Document.Date <- as.Date(Data$First.Construction.Document.Date, format = "%m/%d/%Y")

Burada elimizdeki veri setinde tipi "date" olması gereken fakat olmayan tüm değişkenleri "date" tipine çevirdik ve formatlarını da verdik. Veri tiplerini düzelttik fakat şimdi ikinci problemle karşı karşıyayız. Elimizdeki tarih tipli kolonları nasıl işleme sokacağız? Bunun için kolonları gün, ay ve yıl olmak üzere üçe ayıracağım ve bunlara kategorik değişkenler gibi davranacağım. Böylece bunların modele etkisi de rahatça belli olacak. Aşağıdaki kod bloğu ile bunu gerçekleştiriyorum:

#Bölüm 3 - Hesaplamalarda kullanılabilmesi için tarih kolonlarını yıl, ay ve gün olarak ayırma
install.packages("lubridate")
library(lubridate)
Data$Permit.Creation.Date_Year <- year(Data$Permit.Creation.Date)
Data$Permit.Creation.Date_Month <- months(Data$Permit.Creation.Date)
Data$Permit.Creation.Date_Day <- weekdays(Data$Permit.Creation.Date)
Data <- Data[ , -2]
Data$Current.Status.Date_Year <- year(Data$Current.Status.Date)
Data$Current.Status.Date_Month <- months(Data$Current.Status.Date)
Data$Current.Status.Date_Day <- weekdays(Data$Current.Status.Date)
Data <- Data[ , -3]
Data$Filed.Date_Year <- year(Data$Filed.Date)
Data$Filed.Date_Month <- months(Data$Filed.Date)
Data$Filed.Date_Day <- weekdays(Data$Filed.Date)
Data <- Data[ , -3]
Data$Issued.Date_Year <- year(Data$Issued.Date)
Data$Issued.Date_Month <- months(Data$Issued.Date)
Data$Issued.Date_Day <- weekdays(Data$Issued.Date)
Data <- Data[ , -3]
Data$First.Construction.Document.Date_Year <- year(Data$First.Construction.Document.Date)
Data$First.Construction.Document.Date_Month <- months(Data$First.Construction.Document.Date)
Data$First.Construction.Document.Date_Day <- weekdays(Data$First.Construction.Document.Date)
Data <- Data[ , -3]

Üstteki fonksiyonların kullanımı için öncelikle "lubridate" paketini indirdim ve "library" komutu ile de kullanıma hazır hale getirdim. Daha sonra örneğin "Current.Status.Date" kolonu için Current.Status.Date_Year, Current.Status.Date_Month ve Current.Status.Date_Day adlı 3 tane kolon oluşturdum ve bunlara da sırasıyla yıl, ay ve gün değerlerini atadım. Komutların kullanımı çok basit olduğundan anlatmıyorum. Bu fonksiyonların altındaki son satırlar ise tarihin bulunduğu asıl değişkene artık ihtiyaç kalmadığından bunu kaldırıyor. Bu işlemi tüm "date" tipindeki kolonlar için uyguluyorum. Ayrıştırıldıktan sonra her tarih kolonunun orjinal halini veriden çıkarttığımız için, diğer tarih kolonu onunla aynı indekse geldi bu yüzden koddaki indeksler değişmiyor. Kolonları görecek olursak:

6

Evet gördüğünüz gibi kolonlar istediğimiz tarzda oluşmuş durumda.
Üçüncü bölümü de tamamladığımıza göre artık kayıp verilerle ilgilenmeye başlayabiliriz. Şu ana kadar yaptığımız işlemler bir nevi basit düzeyde “Feature Engineering” kategorisine girmektedir. Tabii ki çok daha büyük veri setlerinde bu işlemler daha kapsamlı ve detaylı olacaktır. Biz ise burada yazının amacından sapmamak adına daha basit düzeyde ele aldık. Çünkü “Feature Engineering” meselesi tek başına bile bir yazı serisinin konusu olabilir. Gelelim kayıp verilere. Öncelikle kayıp veriler ile alakalı bir iki mesele üzerinde konuşalım. İlk aşamada zaten kayıp verilere ilişkin bir hamlede bulunmuştuk ve yüksek düzeyde kayıp veri içeren değişkenler veri setinden çıkarmıştık. Aynı işlem satırlar bazında da yapılabilir. Burada netlik olması açısından bir eşik belirlemek güzel olacaktır. Örneğin %25’ten fazla kayıp veri içeren değişkenler veri setinden çıkarılmalıdır gibisinden bir sınır çizmek genelde yapılan bir uygulamadır. Biz de burada %25 ± 1 olmak üzere bir sınır koyalım ve bu şekilde ilerleyelim. Fakat dikkat edilmesi gerekir ki aslında bu sınır fazla büyük oldu. Normalde ideal sınır %5 – %10 arasıdır diyebiliriz. Biz bu yazıda yazının amacının sapmaması açısından bu sınır ile devam edelim. Dediğim gibi bu konular zaten başlı başına bir yazı konusu. Ayrıca, verilerin rastgele kayıp olup olmadığı da önemli bir konudur. Eğer veriler rastgele kayıpsa(ki biz bunu tercih ederiz) o zaman “imputation” metodu daha uygun olacaktır. Fakat veriler rastgele değil de belli bir nedenden dolayı kayıpsa burada durup inceleme yapmak ve bunun nedenini araştırmak gerekir. Öncelikle kolonların kayıp veri oranlarına bir bakalım:

#Bölüm 4 - Kayıp veri probleminin çözülmesi
as.data.frame(sapply(Data, class))
Data$Current.Status.Date_Year <- as.character(Data$Current.Status.Date_Year)
Data$Filed.Date_Year <- as.character(Data$Filed.Date_Year)
Data$Issued.Date_Year <- as.character(Data$Issued.Date_Year)
Data$First.Construction.Document.Date_Year <- as.character(Data$First.Construction.Document.Date_Year)
names(which(sapply(Data, anyNA)))
MissRatio <- function(x){sum(is.na(x))/length(x)*100}
apply(Data, 2, FUN = MissRatio)
apply(Data, 1, FUN = MissRatio)

Yeni değişkenleri de oluşturduktan sonra veri tipi görme işlemini tekrar uyguluyorum:

7

Evet yeni değişkenlerin yıl kısımları doğal olarak nümerik kalmış. Biz bunlara da kategorik olarak davranacağımız için karaktere çeviriyoruz. Bu işlemi de peşpeşe gelen 4 satırdaki kodlar ile yapıyoruz. Daha sonra “NA” içeren kolonları tekrar görmek adına komut satırını çalıştırıyoruz.

8

“NA” içeren kolonlar yukarıda. Şimdi genel olarak satır ve sütun bazında kayıp verilerin oranlarına bakalım ve belirlediğimiz eşikten yüksek olanları veriden çıkaralım. Bu işlemi de üstte tanımladığımız “MissRatio” fonksiyonu yapıyor. Basitçe açıklamak gerekirse kolondaki “NA” verileri toplam veri sayısına bölüyor. Alttaki iki satırla da “apply” komutu vasıtasıyla sütun ve satır bazında fonksiyonu uyguluyoruz. İkinci argümandaki 2 numarası sütun bazını, 1 ise satır bazını temsil ediyor. Sonuç:

910

Yine resimlerin tamamı büyük olduğundan bir kısmını koydum. Görüleceği üzere kolonlarda %26’nın üzerinde kayıp veri içeren kolon yok. Fakat satırlarda durum farklı. %40’ın üzerinde bile kayıp veri içeren satırlar bulunuyor. Belirlediğimiz eşik zaten yüksekken, bu eşiğin üzerinde kayıp veri içeren satırlar bir işimize yaramayacağından bunları veri setinden çıkarıyoruz. Aşağıdaki kod bloğu ile bu işlemi gerçekleştirelim:

#Bölüm 4 - Kayıp veri probleminin çözülmesi
nrow(as.data.frame(which(apply(Data, 1, FUN = MissRatio) > 26.0)))
MissingRows = which(apply(Data, 1, FUN = MissRatio) > 26.0)
Data <- Data[-MissingRows, ]

Üstteki kod bloğunda ilk satırda kayıp veri oranı 26'dan büyük olan satırların indekslerini "which" komutu ile alıyoruz ve bunları data frame biçimine dönüştürdükten sonra "nrow" ile satır sayısına yani bu satırların kaç tane olduğuna bakıyoruz. Sonuç:

11

Evet görüldüğü üzere 40384 satırda kayıp veri oranı %26’dan büyük. Biz de koyduğumuz eşiğe göre değerlendirerek bu satırları veri setinden çıkartıyoruz. İkinci satır bu bahsi geçen satırların indekslerini bir nesneye atıyor ve son satır da veri setinden çıkartıyor. Veri setinin son hali 158516 satır ve 30 sütun şeklinde. Artık “imputation” metodunu gerçekleştirmeye hazırız. Nümerik değişkenleri ortalama ile, kategorik değişkenleri de mod(en çok görülen gözlem) ile değiştireceğiz. Aşağıdaki kod ile gerçekleştirelim:

#Bölüm 4 - Kayıp veri probleminin çözülmesi
mode <- function(x) {
  uniq_x <- unique(x)
  uniq_x[which.max(tabulate(match(x, uniq_x)))]
}
for (col in 1:ncol(Data)) {
  if (class(Data[ , col]) == "numeric" | class(Data[ , col]) == "integer") {
    Data[ , col][is.na(Data[ , col])] <- mean(Data[ , col], na.rm = TRUE)
  } else if (class(Data[ , col]) == "character") {
    Data[ , col][is.na(Data[ , col])] <- mode(Data[ , col])
  }
}

Üstteki kod bloğu ile R içerisinde doğrudan bir mod fonksiyonu bulunmadığı için bu fonksiyonu biz oluşturduk ve alttaki for döngüsü ile de sayısal kolonların kayıp verilerini ortalamaları ile, karakter kolonların kayıp verilerini de modları ile değiştirdik.

Sıra geldi bir sonraki aşamaya yani kategorik değişkenleri kodlamaya. Aşağıdaki kod bloğu ile de bunu gerçekleştirelim:

install.packages("dplyr")
library(dplyr)
Data %
  select(which(sapply(., is.character))))
Data[ , 30] <- Data$Permit.Type
Data <- Data[ , -17]
names(Data)[29] <- "Permit.Type"
for (col in 1:(ncol(Data) - 1)) {
  if (class(Data[ , col]) == "character") {
    unique_col <- unique(Data[ , col])
    Data[ , col] <- factor(Data[ , col],
                           levels = sort(unique_col),
                           labels = c(1:nrow(as.data.frame(unique(Data[ , col])))),
                           ordered = FALSE)
  }
}
unique_pt <- unique(Data$Permit.Type)
Data$Permit.Type <- factor(Data$Permit.Type,
                           levels = sort(unique_pt),
                           labels = c(0, 1, 2, 3, 4, 5, 6, 7),
                           ordered = FALSE)
as.data.frame(sapply(Data, class))

Evet yukarıdaki kod bloğu ile kategorik değişkenleri kodladık. Şimdi bloğun detaylarına inelim. Kullanacağımız kütüphane olan "dplyr" kütüphanesini indirip yüklüyoruz. Sonrasında veriden "Permit Number" sütununu çıkarıyorum çünkü bizim için bir anlam ifade etmiyor. Bir sonraki adımda veri içerisindeki "character" tipli kolonları buluyor ve isimlerini yazdırıyorum. O kodun sonucu şu şekilde:

12

Daha sonra bağımlı değişken olan “Permit.Type” değişkenini tekrar verinin sonuna ekliyorum çünkü tarih kolonlarımızı ayrıştırdığımızda yeni kolonlar veri setinin sonunda oluşmuştu ve biz bunu düzeltmemiştik. O yüzden bağımlı değişkeni tekrar veri setinin sonuna taşıdım. Sonraki aşamada for döngüsü ile bu “character” tipli kolonları “factor” tipine çevirdim. R üzerinde kategorik verileri kodlamak için onlara böyle bir dönüşüm uygulamak yeterlidir. İlgili kolonun içerisinde kaç farklı seviye varsa bunların isimlerini “levels” argümanına verdim. “labels” argümanına ise kaç farklı seviye varsa o kadar rakam verdim. Rakamların 0’dan başlayıp devam etmesi bir sıralama ifade etmiyor. Burada dikkat edilmesi gereken nokta “ordered = FALSE” argümanı. Bahsettiğimiz rakamların bir sıralama ifade etmemesini sağlayan argüman bu. Nominal kategorik değişkenlerde yani seviyeleri arasında herhangi bir sıralama olmayan değişkenlerde bu argümanı “FALSE” yapıyoruz. Döngüden sonra aşağıda bağımlı değişkenimizi kodlama aşamasını ayrı gösterdim ki onu da kodladığımızı belirtelim. Elimizde ordinal kategorik yani seviyeleri arasında sıralama bulunan bir kategorik değişken bulunmadığından “ordered” argümanını “TRUE” yaptığımız bir değişken bulunmuyor. Son olarak da veri tiplerine tekrar bakalım:

13

Evet istediğimiz gibi gerekli değişkenler kodlandı ve tipleri “factor” oldu.

Data preprocessing aşamasının bir diğer adımının da “Feature Scaling” olduğunu söylemiştik. Fakat bir önceki yazımızda da bahsettiğim gibi yine karar ağacı modeli üzerinden gideceğimiz için bu aşamayı gerçekleştirmemize gerek yok. Nedenini bir önceki yazımda anlatmıştım. Karar ağaçlarında nümerik işlemler, denklemlerden daha çok ağacın dallarını bölme işlemleri var ve bu da “Feature Scaling” işleminin önemini azaltıyor. Bu yüzden uygulamadan devam ediyoruz.

Bir sonraki aşama veriyi train ve test setler olarak ikiye bölmek. Validation set oluşturmayacağım çünkü validation set metodu yerine “k-fold cross-validation” metodu kullanacağım. Bu metoddan da önceki yazılarımın birinde bahsetmiştim. Kısaca birazdan tekrar açıklayacağım. Aşağıdaki kod bloğu ile bölmeyi gerçekleştirelim:

#Train-test Split
install.packages('caTools')
library(caTools)
set.seed(123)
split = sample.split(Data$Permit.Type, SplitRatio = 0.8)
training_set = subset(Data, split == TRUE)
test_set = subset(Data, split == FALSE)
nrow(training_set)
nrow(test_set)

Evet “caTools” paketi işimize yarayacak olan paket olduğundan indirip çağırdık. Daha sonra sabit sonuçlar elde etmek adına “set.seed” komutunu çalıştırdık. Burada herhangi bir sayı da verebilirdik. “split” adında logical bir nesne tanımlayarak bağımlı değişkeni verdik ve oranı %80 training set, %20 test set olarak tayin ettik. “split” nesnesinin “TRUE” olduğu veriler yani %80’lik kısım training set, “FALSE” olduğu veriler yani %20’lik kısmı da test set olarak atadık. En son aşamada da satır sayılarına teyit etmek amaçlı baktık:

14

Son adımda bağımlı değişkenin sınıflarının veri içerisine nasıl dağıldığına bakalım. Eğer dengeli bir dağılım yoksa “imbalanced dataset” problemi ile karşı karşıyayız demektir ki bu da özel çözümlere yönelmemizi gerektirebilir.

Data %>%
  group_by(Permit.Type) %>%
  summarise(number = n())

Sonuç:

35

Evet görüleceği üzere sınıflar veri setine dengeli bir biçimde dağılmamış. Bu problemin “Imbalanced Dataset” problemi olarak bilindiğini önceki yazılarda söylemiştik. Bununla başetmek için birden fazla yöntem bulunuyor. Biz burada 2 farklı metodu birlikte kullanarak bu problemi çözmeye çalışacağız. Bunlardan bir tanesi kalite metriğini değiştirmek, diğeri de veri örnekleme ile alakalı bir metod kullanmak. Bunların detaylarından şimdi bahsedeceğim. Kalite metriği zaten kullandığımız algoritmayla alakalı olduğu için bunu aşağıda uygulayacağız. Fakat veri örneklemesi ile alakalı olan metodumuz “data preprocessing” aşaması içerisinde olmalı o yüzden bunu burada gerçekleştirip öyle devam edeceğiz.

Kullanacağımız metod SMOTE(Synthetic Minority Over-sampling Technique) olacak. Bu metod, azınlık sınıfı alıp bundan bir örneklem çeker. Daha sonra bu örneklemi KNN(K nearest Neighbours ya da K En Yakın Komşu) yöntemi kullanarak çoğaltır ve aslında veri setinde olmayan, azınlık sınıfa benzer yapay gözlemler üretir. Böylece verideki azınlık sınıfı dengelemiş olur. Çok fazla detaya değinmeden devam edeceğim. Unutmayalım ki dengeleyeceğimiz yeni veri seti training setten oluşacak. Modeli yeni dengelenmiş set üzerinde kuracak ve hiperparametre ayarlarını da bunun üzerinde yapacağız. Fakat en son performans kontrol etme aşamasını eskisi gibi test set üzerinde gerçekleştireceğiz. Aşağıdaki kod bloğu ile uygulayalım:

library(DMwR)
Balanced_Data = SmoteClassif(Permit.Type ~ .,
                              training_set,
                              C.perc = list("0" = 283.69,
                                            "1" = 65.53,
                                            "2" = 1.6,
                                            "3" = 11.45,
                                            "4" = 1100.46,
                                            "5" = 135.34,
                                            "6" = 140.34,
                                            "7" = 0.16),
                              dist = "HEOM",
                              k = 5)
Balanced_Data %>%
  group_by(Permit.Type) %>%
  summarise(number = n())

Balanced_Data <- Balanced_Data[sample(nrow(Balanced_Data)), ]
Balanced_Data2 <- Balanced_Data

"SmoteClassif" fonksiyonu ile öncelikle formül argümanına bağımlı değişkeni sol tarafa yazdıktan sonra "." işareti ile de diğer tüm değişkenleri simgeleyerek sağ tarafa yazıyoruz. Veri setinde daha az olan sınıflara daha çok ağırlık vererek miktarlarını yapay olarak artırdık. Buradaki oranlar ile sizde kendinize göre dengeler oluşturabilirsiniz. Buradaki oranlar mevcut veri sayısının kaç ile çarpılacağını veriyor. Kod bloğunun son satırı veriyi sadece karma bir hale getiriyor. Evet kısmen veri sınıflarını dengeledik. Verinin yeni dağılımına bakacak olursak;

45

Data preprocessing aşamasını da bu şekilde tamamlamış olduk. Artık “Hyperparameter Tuning” aşamasına geçerek model için en optimum hiperparametre değerlerini belirleyip, son olarak da bu optimum modelin performansını ölçelim.

Bu bölüme detaylıca değinmeden önce, kullanacağımız boosting algoritmasından biraz bahsedelim. “Gradient Boosting Machines” yani GBM. Geçen yazıda AdaBoost algoritmasından bahsetmiş ve kullanmıştık. Yapısına da detaylıca değinmiştik. Bu kez GBM’in yapısına çok fazla değinmeyi düşünmüyorum. Fakat temel prensiplerinden bahsedeilim. GBM algoritmasında öğrenme prosedürü daha doğru tahminler oluşturabilmek için ard arda modeller eğitir. AdaBoost algoritması da buna benzer çalışıyordu. Fakat GBM’de, yeni oluşan modeller(base-learners) kayıp fonksiyonunun gradyanı ile negatif yönde maksimum korelasyonlu olarak eğitilir. Gradyan bir fonksiyonun maksimuma doğru gidiş yönüne dair bilgi verdiğinden, gradyanın tersi yönde ilerlemek fonksiyonu minimize etmeye doğru gidecektir. Yani GBM içerisindeki iterasyonlarda kurulan modeller bu prensibe göre oluşur ve kayıp fonksiyonunu minimize etmeye çalışır. En sonda ise kurulan tüm karar ağacı modelleri birleştirilir ve hepsinin final modele bir etkisi olur. Bu final modele birleşik güçlendirilmiş model(Boosted Ensemble) diyebiliriz. Genelde regresyon işlemleri için tüm bu ağaçların ortalaması, sınıflandırma işlemleri için de en çok görülen sonuç yani ağaçlar en çok hangi sınıfı sonuç olarak göstermişse(majority vote) o kullanılır.

Biz ise algoritma olarak “XGBOOST (Extreme Gradient Boosting)” algoritmasını kullanacağız. Aslında daha çok bu algoritma, karar ağacı bazlı boosting(güçlendirme) algoritmalarının hesaplama yükünü optimize etmek adına kurulmuş bir algoritma ya da kütüphanedir diyebiliriz. Bu yüzden teorik olarak GBM’den pek bir fark olmasa da pratik olarak bulunuyor. Artık data preprocessingden sonra gelen hiperparametre ayarlama bölümüne geçebiliriz. Biraz aşamalardan bahsedelim.

Hiperparametre ayarlama bölümünde dikkat edilmesi gereken nokta hiperparametre türlerinin fazlalığı ve sınıfları. Bunlar boosting için kullanılanlar ve base-learner ya da metod(karar ağacı) için kullanılanlar olarak ikiye ayrılabilir. Biz her iki grupta bulunan hiperparametreleri de optimize etmeye çalışacağız. Geçen seferki yazıda bu aşamanın üzerinde çok detaylı olarak durmamıştık. Bu sefer bu aşamayı daha detaylı yapacağız ve tüm hiperparametreleri ayarlamaya çalışacağız. Önce boosting bazında ve metod bazında hiperparametreler ne olacak bunları listeleyelim:

Metod(decision tree) bazındaki hiperparametreler:

min_child_weight = Ağacın herhangi bir kökünde bölünmenin devam edebilmesi için gereken minimum gözlem ağırlığı. Overfittingi önlemek adına kullanılır. Yüksek değerler ağacın oluşması için seçilen örnekleme özel olan ilişkilerin yanlışlıkla öğrenilmesini önler. Çok büyük sayılar ise overfittinge neden olabilir bu yüzden ayarlanması gerekir.

max_depth = Ağacın maksimum derinliği. Overfittingi kontrol etmek adına kullanılır.

gamma = Bir bölünmenin yapılabilmesi için kayıp fonksiyonundaki minimum azalmayı temsil eder. Büyük sayılar vermek bölünmeleri zorlaştıracaktır. Bu yüzden ayarlanması gerekir.

subsample = Birleşik güçlendirilmiş modelin (boosted ensemble) içerisindeki her zayıf ağacın (decision stump) oluşması için veriden alınan örneklem büyüklüğü.

colsample_bytree = Üstte bahsedilen örneklem çekilirken kullanılacak olan kolonlar da aynı şekilde rastgele seçilir. Bu parametre o kolonların sayısını belirler.

Üsttekilerin dışında boosting bazında parametreler:

eta = Her ağacın final sonuca olan katkısını belirler. Öğrenme oranı olarak da bilinir. Overfittingi önlemek adına kullanılır.

nrounds = İterasyon sayısı ya da kaç adet ağaç kurulacağı.

Hiperparametre ayarlama adımlarını şu şekilde yapacağız. Öncelikle eta parametresini sabitleyip metod bazlılara da bir başlangıç değeri vererek, bu learning rate(eta) değeri için optimum nrounds yani iterasyon sayısını ya da ağaç sayısını bulacağız. Daha sonra bu ağaç sayısı ve eta parametresini sabitleyip bunları kullanarak metod bazlı parametreleri ayarlayacağız. Önce max_depth ve min_child_weight parametrelerini, daha sonra bunları da sabitleyip ikinci adımda gamma’yı, üçüncü ve son adımda da subsample ve colsample_bytree parametrelerini ayarlayarak metod bazlı hiperparametre ayarlama işini bitireceğiz. Son aşamada ise eta’yı düşürerek iterasyon sayısını artıracak ve en optimum şekli belirlemeye çalışacağız. Vakit kaybetmeden başlayalım.

İlk aşamada ağaç bazında hiperparametreleri sabitleyip “eta” ve “nrounds” parametreleri için optimum değerleri bulalım. Aşağıdaki kod bloğu ile gerçekleştiriyoruz:

set.seed(123)
library(caret)
ControlMethod = trainControl(method = "cv", number = 5)

ParameterGrid1 = expand.grid(eta = 0.1,
                           nrounds = seq(10, 200, 20),
                           max_depth = 5,
                           min_child_weight = 1,
                           gamma = 0,
                           subsample = 0.8,
                           colsample_bytree = 0.8)

OptimumModel1 = caret::train(Permit.Type ~ .,
                     data = Balanced_Data2,
                     method = "xgbTree",
                     trControl = ControlMethod,
                     tuneGrid = ParameterGrid1)

print(OptimumModel)

Kod bloğunu açıklayayım. Kullanacağımız kütüphane olan “caret” kütüphanesini çağırıyor ve ilk satırda da tutarlı sonuçlar almak adına “random seed” değeri belirliyoruz. Sonrasında “train” fonksiyonunun çalışma prensibini belirleyecek olan “trainControl” komutu ile “method” argümanına “cv” diyerek cross-validation yapmak istediğimizi söylüyor ve “number = 5” ile de 5-fold cross-validation yapacağımızı belirtiyoruz. Sonrasında “ParameterGrid” adlı bir nesne oluşturup burada “nrounds” parametresi için bir sayı dizisi belirleyip, diğer ağaç bazında parametrelere daha sonra ayarlanmak üzere bir başlangıç değeri veriyoruz. Son aşamadı “train” fonksiyonu bu nesnedeki her satır için 5-fold cross-validation yapıyor ve en optimumu belirliyor. method = “xgbTree” ile de xgboost yapmak istediğimizi ve bunu decision tree bazında yapacağımızı söylüyoruz. Sonucu görelim:

52

Evet üstte her cv işlemi için sonuçları görüyoruz. accuracy ve kappa istatistiği değerleri gözüküyor. Bunlara bağlı olarak optimum modele bakacak olursak:

53

Evet final modelde seçilen hiperparametre değerlerini çıktı bize veriyor. Eta değeri 0.1 iken, nrounds değeri de 190 olmuş. Artık ikinci aşamaya geçelim. İkinci aşamada ise max_depth ve min_child_weight hiperparametrelerini ayarlayacak, diğer tüm tree ve boosting bazındaki parametreleri sabit tutacağız. Aşağıdaki gibi gerçekleştirelim:

ParameterGrid2 = expand.grid(eta = 0.1,
                            nrounds = 190,
                            max_depth = seq(1, 10, 2),
                            min_child_weight = seq(1, 5, 2),
                            gamma = 0,
                            subsample = 0.8,
                            colsample_bytree = 0.8)

OptimumModel2 = caret::train(Permit.Type ~ .,
                     data = Balanced_Data2,
                     method = "xgbTree",
                     trControl = ControlMethod,
                     tuneGrid = ParameterGrid2)

print(OptimumModel2)

Bu kez eta gibi nrounds parametresini sabitleyip max_depth ve min_child_weight parametrelerini ayarlıyoruz. Sonuca bakalım:

54

Modelin seçtiği hiperparametreler max_depth = 9 ve min_child_weight = 1 olarak gözüküyor(En yüksek accuracy bu değerler için elde edilmiş durumda). Bunları da sabitleyip gamma’yı belirleyelim:

ParameterGrid3 = expand.grid(eta = 0.1,
                            nrounds = 190,
                            max_depth = 9,
                            min_child_weight = 1,
                            gamma = seq(0.1, 0.5, 0.1),
                            subsample = 0.8,
                            colsample_bytree = 0.8)

OptimumModel3 = caret::train(Permit.Type ~ .,
                     data = Balanced_Data2,
                     method = "xgbTree",
                     trControl = ControlMethod,
                     tuneGrid = ParameterGrid3)

print(OptimumModel3)

Sonuca bakacak olursak:

55

Evet model optimum gamma değeri olarak 0.1’i belirlemiş. Bunu da sabitleyerek son 2 hiperparametreyi belirleme aşamasına geçiyoruz:

ParameterGrid4 = expand.grid(eta = 0.1,
                             nrounds = 900,
                             max_depth = 9,
                             min_child_weight = 3,
                             gamma = 0.1,
                             subsample = seq(0.1, 1, 0.2),
                             colsample_bytree = seq(0.1, 1, 0.2))

OptimumModel4 = caret::train(Permit.Type ~ .,
                            data = Balanced_Data2,
                            method = "xgbTree",
                            trControl = ControlMethod,
                            tuneGrid = ParameterGrid4)

print(OptimumModel4)

Sonuç:

56

Son olarak daha düşük eta değerleri için iterasyon miktarını artırarak performansın artıp artmayacağına bakalım.

ParameterGrid5 = expand.grid(eta = c(0.1, 0.05, 0.01),
                             nrounds = c(200, 500, 800, 1100),
                             max_depth = 9,
                             min_child_weight = 1,
                             gamma = 0.1,
                             subsample = 0.9,
                             colsample_bytree = 0.5)

OptimumModel5 = caret::train(Permit.Type ~ .,
                             data = Balanced_Data2,
                             method = "xgbTree",
                             trControl = ControlMethod,
                             tuneGrid = ParameterGrid5)

print(OptimumModel5)

Farklı eta ve nrounds değerleri ile modeli çalıştırdığımızda elde edilen sonuç:

57

Evet hiperparametre belirleme işlemini tamamladık ve artık elde ettiğimiz final modeli test set üzerinde deneyelim:

Predictions <- predict(OptimumModel5,
                       newdata = as.matrix(test_set[ , -31]))

confusionMatrix(Predictions, test_set$Permit.Type)

Evet kodun ilk kısmında elde ettiğimiz son model olan OptimumModel5'i kullanarak test set üzerinde tahminleri gerçekleştirdik. İkinci kısımda ise Confusion Matrix yani tahminler ile gerçek verideki etiketlerin örtüşmelerini gösteren bir tablo oluşturduk. Sonucu görelim:

58

Evet üstteki matrisi incelediğimizde köşegen üzerindeki sayılar aslında doğru tahmin ettiğimiz etiketleri, diğerleri de yanlış tahmin edilenleri gösteriyor. Örneğin sınıf etiketi 1 olan sınıf için 120 değerin 119 tanesini doğru tahmin etmişiz, 1 tanesini ise yanlış tahmin etmiş ve 1 dememiz gerekirken 0 demişiz. Gördüğünüz gibi Accuracy değeri 0.95 civarı fakat bizim için bu değerin bir kriter olmadığını yukarıda belirtmiştik. Bizim için kriter olan olan değer aşağıdaki “Kappa” değeri. “Kappa” istatistiği, gerçek etiketler ve tahmin edilen etiketlerin birbiri ile ne kadar uyumlu olduğunun bir ölçüsüdür. 0.7’nin üzerinde bir Kappa değeri iyi derecede uyum olduğunu gösterir. Bu değer ne kadar yüksek olursa uyum da o kadar yüksek demektir. Biz de burada modelin performansını ölçmek için sınıf dengesizliğinden dolayı Accuracy değerini değil, Kappa değerini seçtik. Kappa değerimiz 0.76 civarı ki bu da iyi bir uyum yakaladığımızı gösteriyor.

Son olarak modeldeki değişkenlerin önemini ve modele etkilerini inceleyelim. Aşağıdaki kod bloğu ile en etkin değişkenleri tespit edeceğiz:

importance <- varImp(OptimumModel5)

"varImp" metodu, en etkin değişkenleri hesaplayıp bir "list" halinde bize sunuyor. Sonuca bakacak olursak:

59

Evet verilen inşaat ruhsatının tipinin belirlenmesinde en etkin değişkenin “Proposed.Construction.Type” yani önerilen inşaat türü olduğunu görebiliyoruz. Aynı şekilde düzeltilmiş maliyet de ikinci sırada gözüküyor. Bunlardan en önemli 6 tanesini alıp daha net görmek adına grafiklerini çizelim:

Top6Variables <- data.frame(VariableName = rownames(importance$importance)[1:6], 
                            Impact = importance$importance[1:6,], 
                            stringsAsFactors = FALSE)


barplot(Top6Variables$Impact, 
        horiz = TRUE,
        names.arg = c("Proposed.Construction.Type",
                      "Revised.Cost",
                      "Number.of.Existing.Stories",
                      "Estimated.Cost",
                      "Number.of.Proposed.Stories",
                      "Existing.Construction.Type"),
        main = "En etkin 6 değişken",
        xlab = "Etki Miktarı",
        ylab = "Değişkenler",
        cex.names = 0.55,
        col = c("red", "blue", "green", "yellow", "brown", "purple"))

Kod bloğunun ilk kısmında ilk 6 değişkenin adını ve etki miktarlarını alıp bir "data frame" nesnesine attık. Sonra da "barplot" komutu ile grafiğini çizdirdik. Sonuç:

60

Evet grafik üstte görüldüğü gibi. XGBoost kullanarak güçlendirdiğimiz karar ağacı modelini artık burada tamamladık ve değişkenlerin etkilerini de gördük. Aynı zamanda “Data Preprocessing(Veri Ön-İşleme)” ve “Hyperparameter Tuning(Hiperparametre Ayarlama)” işlemlerini de gerçekleştirdik.

 

NOTLAR

  1. Hiperparametre ayarlama aşamasındaki kodların çalışması sisteminize bağlı olarak da değişmek kaydıyla uzun sürebilir. Çünkü çok fazla değer için çok fazla cross-validation metodu uygulanmaktadır.
  2. Hiperparametre ayarlama aşamasında çok daha fazla geniş bir skala değerlendirilip daha iyi sonuçlar da alınabilirdi fakat bu bize aynı zamanda hesaplama açısından büyük bir yük getirirdi.

REFERANSLAR

http://topepo.github.io/caret/index.html

https://github.com/dmlc/xgboost/blob/master/doc/parameter.md

https://www.analyticsvidhya.com/blog/2016/03/complete-guide-parameter-tuning-xgboost-with-codes-python/

R Üzerinde Güçlendirilmiş Karar Ağacı: İnşaat Ruhsatları Üzerine Bir Çalışma” için 2 yorum

  1. O kadar derin bir yazi ki, okuyucu olarak hakkini vermek icin basinda birkac saat ve fazlasini gecirmek gerekiyor. Veri temizligi ve tahrifi konusunda rehber niteliginde bir kaynak olusturmak yetmemis, ‘imputation’ konusunu da gayet detayli islemissiniz. Onceki yazilar ustune vakit gecirirken aklima takilan bircok konuyu sonraki yazilarinizda bulmak ayri bir hos. Buradaki ‘boosting’ kismina henuz gecemedim bile, benim makineyi zorladi biraz. Veri setini bese ya da ona bolup deneyecegim artik.

    Baska okuyucu yorumlarinda da paylasilmis, teorik konulardaki birikimizi de yazilariniza yansitmaniz cok guzel. Kendi acimdan hem bildiklerimin ustunden yeniden gecerken hem de yeni seyler buluyorum ve bunlardan bazilari beni yeni meraklara sevk ediyor. Mesela verilerin rastgele kayıp olma (MAR) durumundan bahsetmissiniz, ben bu konuya Turkce bir kaynakta deginildigine ilk defa bu sitede rastladim, gerci butun kaynaklarin altini ustune getirmisligim yok ama demek istedigim boyle konseptlerin detayina girmeseniz bile adlarini anmaniz ve haklarinda kisa bir bilgi vermeniz gayet iyi bir sey.

    Beğen

    1. Selamlar,
      Geç cevap için kusura bakmayın. Bazen gelen yorumlar maillerde kaybolup gidiyor. Ben de sürekli öğrenerek devam ediyorum aslında, teorik bilgiyi geliştirdikçe ve bunu pratiğe döktükçe yazılar da bir o kadar dallanıp budaklanıyor. Yazacak çok konu var, fakat zaman bulmak zor. Elimden geldiğince yazmaya devam edeceğim.

      Beğen

Bir Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Google fotoğrafı

Google hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Connecting to %s