Lineer Regresyon, Piazzi'nin Ceres'i ✨ Gauss'un en küçük kareleri 🧮

Cover Photo by gferla on Unsplash

Makine öğrenmesi konusuna direkt veya dolaylı olarak yolu düşenlerin ilk karşılaştığı algoritmalarından biridir herhalde Lineer Regresyon. Benim de karşılaştığım bir problemle birlikte, makine öğrenmesi algoritmalarını araştırırken ilk tanıştığım bu algoritma oldu.

Lineer Regresyonu çalışmaya başladığımda, kendimi evrensel bilgi örüntüleri içinde geçen bir serüvenin içinde buldum. Aslında yazımın temel ekseni, ilk kez öğrenmeye çalışılan bu tip konular için, herhangi bir kütüphane kullanmadan (üzerinde çalışılan konuda direkt sonuç veren kütüphaneler) kodlanmaya çalışılması ve bu çabanın muhtemel faydaları üzerine. Fakat, en başta bahsettiğim serüvene değinmekten kendimi alıkoyamıyorum.

1801 yılının ilk gecesi, saat 20:00 suları; İtalyan papaz, matematikçi ve astronom Guiseppe Piazzi Palermo’daki gözlem evinde teleskopunu Boğa takımyıldızına çevirmiş, üzerinde çalıştığı yıldız kataloğu için rutin gözlemlerinden birini yapıyordu. Bu gözlem sırasında, yeni bir kütle fark etti. Başta bunu bir yıldız zannetti, fakat ilerleyen günlerdeki gözlemlerinde bu kütlenin hareket ettiğini farrkkettiğinde, bunun yeni bir gezenen keşfi olabileceği ihtimali iyice güçlenmişti.

Ceres cüce gezegeni

Photo from Derescope

1800’lü yıllarda, henüz herhangi bir fotoğraflama teknolojisi yoktu. Ölçümler gözlemci, teleskop, kağıt ve kalem ile yapılıyordu ve bu şekilde kaydediliyordu. O zamanın şartlarında düşünüldüğünde, bunun ne kadar zor ve önemli bir keşif olduğunu daha iyi anlaşılabilir. Keza, Jupiter ve Mars arasındaki kuşağı senelerdir bir çok astronom; kayıp gezegeni 1 bulma ümidiyle inceliyordu. Buna rağmen henüz bir keşif ortaya çıkmamıştı.

Piazzi, 1 Ocak’taki ilk temastan 11 Şubat’a kadar, bu yeni keşfettiği kütleyi gözlemledi ve ölçümler kaydetti. 4 Ocak’ta yeni bir gezegen bulmuş olabileceğine dair basına duyuru yaptı. 27 Şubat’ta “Journal de Paris” Piazzi’nin yeni keşfini konu alan bir haber çıkmıştı bile. Aynı yılın Haziran ayı sonunda, bilim toplulukları Piazzi’nin gözlemlediğinin bir gezegen olabileceği konusunda ikna olmuştu.

Fakat elde edilen veriler çok kısıtlıydı ve Ceres’i tekrar gözlemlemeyi, daha doğrusu tekrar ortaya çıkacağı noktayı tespit etmeyi o zamanın yöntemleriyle çok zorlaştırıyordu. Bu durumun bir sonucu olarak, bazı çevrelerde gözlemlerin doğruluğunu sorgulayan, dahası gerçekten böyle bir gezenenin olmadığına varan söylemler başlamıştı.

Piazzi'nin Ekim ayında Monatliche Correspondenz da yayınlanan ölçümleri Kaynak

Aynı yıl Ekim ayında, astronomi alanında düzenli yayınların çıktığı Alman “Monatliche Correspondenz” dergisinde Piazzi’nin tüm ölçümleriyle beraber bir yazı yayınlandı. Gözlemlerin sınırlı oluşu, Ceres’in tekrar ortaya çıkacağı konumu hesaplamayı oldukça zorlaştırıyordu. Birçok bilim insanı bunun imkansız olduğunu, hatta gözlemlerin yanlış olabileceğini ve böyle bir gezegenin hiç olmayabileceğini çeşitli çevrelerde dillendirmeye başlamışlardı. Her şeye rağmen bu gözlemler o zaman 24 yaşında olan, daha sonralarda matematiğin prensi olarak anılacak Carl Frederick Gauss için oldukça davetkardı. Bu kadar az gözlem verisi ile Ceres’in tekrar ortaya çıkacağı noktayı hesaplayabileceğine inanıyordu. Tabiki bu bir inaçtan ibaret değildi, klasik yöntemlerin dışında, parlak bir fikri de vardı2. Bu hesaplama yönteminde gözlem verisindeki hataları minimize etmek için “en küçük kareler toplamı” methodunu kullanmıştı. Fakat bunu 1809 yılına dek yayınlamadı. Bu methodu Gauss’tan önce Legendre 1805’te yayınlamıştı fakat, birçok çevre tarafından methodun ilk sahibinin Gauss olduğu kabul edilir. Bu kabulde Gauss’un 1809 da yayımlanan ve Ceres’in yörüngesini nasıl hesapladığını açıklayan Theoria Motus’ta, 1795’ten bu yana bu methodu kullandığını belirtmesi etkilidir.

“… for it is now clearly shown that the orbit of a heavenly body may be determined quite nearly from good observations embracing only a few days; and this without any hypothetical assumption.” (from Theoria Motus published 1809) -Carl Friedrich Gauss

Günümüzde bilgiye ve teknolojiye ulaşmak oldukça kolay. Neredeyse herkesin elinin altında 1800 lü yıllarda hayalinin bile kurulamayacağı bilgisayarlar var. Sadece işlem gücü değil kullanabileceğimiz çeşitli açık kaynaklar var. Özellikle son yıllarda çok popüler olan Python dili için kullanılabilir binlerce açık kaynaklı kütüphane mevcut. Birçok hesaplama için, hesaplama hakkında hiç birşey bilmeseniz dahi çok kısıtlı araştırmayla, sadece fonksiyonların birkaç parametresiyle oynayarak analizler yapabilirsiniz. Peki bu yaklaşım ne kadar doğru ? Günümüzdeki “Veri Bilimci” bolluğu bu durumdan mı kaynaklanıyor? Bu sorular zaman zaman aklıma düşmüyor değil.

Yeni karşılaştığım ve kullanmaya niyetlediğim yöntemler için, kendime öğrenmek ve öğrenme sürecini pekiştirmek için eğlenceli bir yol buldum. Bu yazıda bu yolu paylaşmak istiyorum. Aslında yaklaşım basit; Direkt öğrenmek istediğim algoritmanın sonuçlarını veren hiçbir kütüphane kulanmadan, algoritmanın matematiğini çalışıp, nihayetinde python dili ile koda dökmek.

Bunun için kurulabilecek en basit model olan tek değişkenli basit lineer regresyon kodlamaya çalışacağım. Yine çok yaygın ve güzel sonuçlar veren bir veri seti kullanacağım. Ne de olsa amaç öğrenmek. Öğrenme sürecini verimli kılabilmek ve algoritmaya odaklanabilmek için diğer zorlayıcı olabilecek etmenleri elemek istiyorum.

Kodlamayı anlatırken mümkün olduğunca detaylı açıklamalar yapacağım. Yazıyı programlama konusunda fikri olmayanların da okuyabileceğini varsayıyorum. Nitekim, açıklamalarım zaman zaman programlamanın temelleri ile ilgili de olabilir.

Çalışma sırasında bazı kütüphaneler kullanacağım. Bu kütüphaneler Lineer Regresyon ile direkt ilişkili değil. Bazı tamamlayıcı işlemler için bunlara ihtiyaç duyuyorum.

  • Numpy, gerekli olabilecek matematiksel işlemler için. ( örn. ortalama alma, standart sapma bulma, rastgele sayılar üretmek vs.)
  • Pandas, kaynak dizinden veriyi okumak, yazmak ve basit görselleştirmeler için.
  • Random, Lineer Regresyon modelini kuracağımız gözlemlerden rastgele belli sayıda örnek seçebilmek için (sampling).
  • Matplotlib, anlamamızı kolaylaştıracak grafikler çizdirebilmek için.

Haydi eğlenceli kısma başlayalım,

1. Kullanacağımız kütüphanelerin çağırılması,


import pandas as pd
import numpy as np
import random
import matplotlib.pyplot as plt

Kütüphaneler aslında daha önce bir ihtiyaç doğrultusunda; yine o programlama dilinde yazılmış kodlardır. Bu kodlar içerlerinde sınıflar ve fonksiyonlar barındırır. Kütüphaneleri çağırdıktan sonra içerdikleri fonksiyonları kullanabiliriz. Python dilinin gücünün ve popüler olmasının önemli bir nedeni, bu dille çalışan çok fazla insan olması. Bu insanlar arasında bilim insanları ve araştırmacıların çok olması. Nitekim topluluğunun geniş olması ve neredeyse ihtiyaç duyabileceğiniz her alanda çok fazla kütüphane bulunabilmesidir.

2. Üzerinde çalışacağımız verinin okunması ve sadeleştirilmesi,


data = pd.read_csv("data.csv")
data = data[data["Gender"]=="Male"]
data = data[["Height","Weight"]]
data.columns = ["Weight","Height"]

Kütüphanelerin fonksiyonlarının kullanımına bir örnek kodun ilk satırında görülebilir. Veriyi okurken pandas kütüphanesinde bulunan read_csv() fonksiyonunu kullanıyoruz. Bu fonksiyon .csv (virgül ile ayrılmış değişkenler) biçiminde saklanmış verinin dizinden okunmasını sağlayan, daha önce birilerinin bu amaç için yazdığı, bizim tekrar sadece bu iş için yazmamıza gerek olmadan (yüzlerce satırdan bahsediyorum) işimizi görmemizi sağlayan fonksiyonlardan sadece biri.

Kullanacağımız veri bir topluluktaki erkek ve kadının boy ve kilo ölçümlerini içeriyor. Modelimizi bu veri ile kuracağız. Öğrenme projesi için temiz ve güzel sonuçlar vereceğini bildiğimiz bir veri seti. Gerçek hayat problemleri bunun gibi olmuyor. Çok daha karmaşık, üzerinde çalışılması, düşünülmesi gereken veri içeriyor (Uç örnek Ceres in yeniden ortaya çıkacağı konumu tahmin etmek).

Verideki sadece erkeklere ait ölçümleri kullanacağız. Ayrıca, sadece boy ve kilo ölçümlerini içeren sutünlar yeterli olduğu için filtreleme işlemleri yaptık.

3. Verinin keşfedilmesi ve örnekleme yapılması,


Öncelikle verimizi tablo halinde bir görelim. head() fonksiyonu verimizin istediğimiz kadar satırına göz atmamızı sağlayan, sutün isimlerini kontrol edebileceğimiz bir çıktı veren fonksiyon.

Şimdi de len() fonksiyonunu kullanarak verimizin kaç adet gözlem (satır) içerdiğine bakalım.

Verimiz 5000 adet satır ( ölçüm ) içeriyor. Şimdi de kişilerin boy ve kilo özelliklerinin birbiriyle ilişkisini görsel olarak kontrol edelim. Bunun için scatter plot kullanılabilir. Scatter plot birbiriyle ilişkisine bakılacak özellikler x ve y eksenine yerleştirilerek, her veri noktası için x ve y kesişimi nokta şeklinde gösterilir.

plt.scatter(data.Height,data.Weight)
plt.xlabel("Boy")
plt.ylabel("Kilo")
plt.show()
5000 adet kişi ile yapılmış boy ve kilo ölçümlerinin karşılaştırılması

5000 gözlem, çalışmanın temel amacını düşünürsek fazla. Kavrama sürecini basitleştirmek için daha az veri noktası bizim için daha iyi olacaktır. Bu 5000 adetlik gözlemden rastgele 25 adet veri noktası seçelim ve lineer regresyon modelimizi bu verilerle kurmaya çalışalım.

newIndex = random.sample(list(data.index), 25)
dataSampled = data.reindex(newIndex)

Daha önce çağırdığımız random kütüphanesinin sample() fonksiyonunu kullanarak verimizden rastgele 25 nokta seçiyoruz. Ardından pandas kütüphanesinin reindex() fonksiyonundan yararlanarak rastgele seçtiğimiz 25 noktadan yeni bir veri seti tanımlıyoruz.

plt.scatter(dataSampled.Height,dataSampled.Weight)
plt.xlabel("Boy")
plt.ylabel("Kilo")
plt.show()
5000 adet ölçüm içerisinden rastgele seçtiğimiz 25 veri noktası
4. Parametrelerin ölçeklendirilmesi (normalization),


Bu işlemden önce parametrelerimizi tanımlıyalım.

y ile ağırlık ölçümünü, x ile boy ölçümünü ve N ile de gözlem sayımızı tanımlayalım.

x = list(dataSampled.Height)
y = list(dataSampled.Weight)
N = len(dataSampled)

list() fonksiyonu veri kümesini basit liste olarak döndüren fonksiyondur. len() fonksiyonu ise belirttiğimiz veri setinin satır sayısını verir.

Normalleştirme adımının amacı farklı ölçeklerdeki özellikleri benzer ölçeğe indirgemek. Keza modeli kurarken hesaplayacağımız hatalar Öklid mesafesi ile hesaplanmakta ve ölçek farklılıkları bu hesabı kötü etkilemektedir. Sonuç olarak özelliklerimizi aynı ölçeğe getirmeliyiz.

Boy ve Kilo özellikleri arasındaki ölçek farklılığı

Boy özelliği 62 ile 74 arasında değişmekle birlikte, kilo özelliği 150 ile 219 arasında değişmekte. Bu ölçek farklılığı durumdan duruma daha da dramatik olabilir. Örnek olarak, bireylerin yaşları ile tl cinsinden yıllık gelirleri üzerine analiz yaptığınızı düşünün, bu durumda özellikler çok daha farklı ölçeğe sahip olacaktır (örn. yaş 20 ile 80 arasında. Tl cinsinden yıllık gelir 10.000 ile 200.000 arasında değişiyor olabilirdi).

Aslında, bu çalışmayı ilk yaptığımda bu adımı uygulamadım. Doğru bir model oluşturamadım. Bunun nedenlerini araştırırken bu işlemi yapmadığımı farkettim. Bir konuyu öğrenirken hazır kütüphane kullanmadan kodlamaya çalışmanın amacına ve muhtemel faydalarına güzel bir örnek tam olarak buydu. Hata yapmak, hatanın nedenlerini araştırmak ve bu süreçte bilgi birikimini derinleştirmek. Eğer hazır bir kütüphane kullansaydım bu işlem arka planda kendiliğinden yapılıyor olabilirdi. Bu adımı hiç öğrenmeden geçebilirdim. Ya da kütüphanenin ilgili fonksiyonunda seçilen, ne işlem yaptığını bilmediğim, dökümantasyonunu okuyup standart bir değer seçip ilerleyebilirdim. Böyle yapsaydım neden sonuç ilişkisini kuramayacak, yani eksik öğrenmiş olacaktım. Tabiri caizse ezbere olacaktı.

Farklı ölçekteki özellikler için bir benzetme

Bizim örneğimizde her iki özellik de normal dağılıma uyduğu için, normalleştirme methodu olarak stardard score kullanacağım.

Standart score fonksiyonu,

Basitçe açıklamak gerekirse özelliklerimizi normalleştirmek için, her bir değerden içinde bulunduğu kümenin aritmetik ortalaması çıkarılıp, kümenin standart sapmasına bölüyoruz.

x = [(i-np.mean(x))/np.std(x)  for i in x]
y = [(i-np.mean(y))/np.std(y)  for i in y]

Normalleştirme işleminden sonra özelliklerimizi aynı ölçeğe indirgemiş olduk.

Artık iki özellikte aynı ölçekte
5. En küçük kareler toplamı yöntemi,

Modeli oluşturmadan önce, ne yapacağımızın üstünden geçmekte fayda var. Amacımız elimizde bulunan 25 adet gözlemi en iyi açıklayabilecek modeli kurmak. Aslında x ve y nin ilişkisini en iyi ortaya koyan (tabiki deterministik değil) tek değişkenli lineer fonksiyonu belirlemek.

Belirleceğeğimiz fonksiyon aşağıdaki gibi olacak,

İlk kez Gauss’un kullandığı, ilk gezegen keşfinin doğrulanmasına kadar uzayan etkileri olan en küçük kareler toplamını anlamaya çalışalım. Bu yöntemi açıklamak için kullanacağım görseli yalınlaştırmak için 25 adet olan veri noktamızdan sadece 3 adetini ele alacağım.

Rastgele betimlenmiş bir doğru

Yukarıki görselde mavi noktalar gözlemlerimizi, kırmızı doğru bu gözlemleri açıklamak için rastgele betimlediğimiz doğruyu ifade etmekte. Sarı üçgenler ise betimlediğimiz doğrunun verilen boy değişkenine göre kilo çıktısını ifade etmekte.

Görüleceği üzere her gözlem (gerçekte ölçülen) ile bizim betimlediğimiz doğrunun çıktısı (tahminler) arasında fark var. Bu farkları e1,e2 ve e3 olarak ifade ediyoruz.

Gözlemlerimiz için betimleyebileceğimiz en iyi doğru, gözlemler ile tahminlerarasında oluşan hataların karelerinin toplamı en küçük olandır.

Betimlenen ideal doğru aşağıdaki toplam için minumum değeri sağlamalı,

Neden mi kareleri ? Çünkü çıkarma işlemi yapıyoruz ve sonuçlarımız sıfırın altında çıkarsa bundan kurtulmanın en kolay yolu karesini olmak.

Ne kadar basit, fakat bir o kadar etkili bir fikir ? Muazzam! Huzur içinde yat Gauss 🙏🏻

6. Gradient descent algoritması

Peki ama gözlemlerimize sonsuz tane doğru betimleyebiliriz. En küçük hata kareleri toplamı olan doğruya nasıl ulaşacağız?

En iyi doğruyu bulmak için basit gradient descent algoritması kullanacağız. Bu oldukça basit, fakat basit olduğu kadar muaazam ve yetenekli bir algoritma.

Gradient descent algoritması koda dökmeden önce, algoritma ile ne yapacağımızı anlayalım.

Amacımız m ve b değerlerini bulmak.

Bu değerleri bulmaya çalışırken hata kareler toplamının en küçük olduğu noktayı arayacağız. Bunu yapabilmek için,

  • En iyi doğruyu bulana dek denemeler yapacağız (iterasyon) tabi her iterasyonda elde ettiğimiz sonuçları diğer iterasyonda girdi olarak kullanacağız.
  • Başlangıçta rastgele m ve b değeri seçeceğiz.
  • Her iterasyonda hatalarımızın kareler toplamını hesaplayacağız.
  • Hatalarımızın kareler toplamı görece büyükken, bir sonraki adımda daha az hataya ulaşmak için büyük adımlar atacağız.
  • Hatalarımızın kareler toplamı küçüldükçe en iyi değerleri hassas yaklaşabilmek için adımlarımızı küçülteceğiz.

Algoritmanın can alıcı; aynı zamanda matematiğin büyüleyiciliğini tekrar hatırladığımız kısma geldik. Yukarıda belirttiğimiz kayıp fonksiyonumuzun türevini alacağız ve her bir iterasyonda bu türev ile eğim hesaplayacağız. Nitekim eğer eğim büyükse henüz hedefe uzağız ve büyük adım atmamız gerektiğini anlayacağız, eğim küçükse hedefi ıskalamamak için adımlarımızı küçülteceğiz.

Gradient descent parabol Kaynak

Yukarıdaki grafikte y ekseni hata kareleri ortalamasını ifade ediyor. En küçük hataya yaklaştıkça o noktadaki eğim azalacak. Nitekim, başlarda en iyiye yaklaşmak için büyük adımlar atarken, hata azaldıkça en küçük noktada durabilmek için adımlarımızı küçülteceğiz.

Betimleyeceğimiz doğruyu ifade eden fonksiyonda b ve m değerlerini bulmak istiyoruz. Nitekim kayıp fonksiyonunun, hem b ye göre hem de m e göre türevi almamız gerekiyor. Amaç iki nokta için de eğimi bulabilmek.

7. Haydi artık kodlayalım ve modelimizi kuralım 💻!


c=0
LearningRate = 0.008 # bir sabir olarak belirlememiz gerekiyor. aşağıda kullanımı görülebilir.
b = 4 # rastgele seçilmiş ilk değer
m = 3 # rastgele seçilmiş ilk değer
iter = 200 # iterasyon sayısı
liste = []
sst =[]
mselist = [] 

while c <iter: # en iyi b ve m değerleri bulabilmek için belirlediğimiz iterasyon sayısındaki döngü.
        slopem = 0 # m için hesaplayacağımız eğime slopem dedik ve başlangıçta 0 a eşitledik.
        slopeb = 0 # b için hesaplayacağımız eğime slopeb dedik ve başlangıçta 0 a eşitledik.
        for i in range(len(dataSampled)): # tüm veri noktalarını taramak için gereken döngü.

            # her nokta için m e göre eğimin bulunması ve kümülatif toplanması.
                slopem  +=  -2*x[i]*(y[i]-(m*x[i]+b)) 
                
            # her nokta için b e göre eğimin bulunması ve kümülatif toplanması.   
                slopeb  +=  -2*(y[i]-(m*x[i]+b))
                
        """
        b ve m için eğim aritmetik ortalaması bulup, öğrenme oranımızla 
        çarptıktan sonra  mevcut b değerimizden çıkartarak,               
        bir sonraki iterasyon için yeni b ve m değerlerimizin tespiti.           
        """

        #  bir sonraki iterasyon için kullanacağımız b değerinin tespiti
        #  eğim büyükken b den daha büyük değer çıkaracağız nitekim adım büyük olacak
        #  eğim küçüldükçe çıkarılan değer nitekim adımda küçülecek.
        b -= ((slopeb/N)  * LearningRate)
        
        # bir sonraki iterasyon için kullanacağımız m değerinin tespiti
        m -= ((slopem/N) * LearningRate)  
        
        mse = 0
        
        # hata karelerinin aritmetik ortalasının bulunması ve her iterasyon için listeye yazılması.
        # ilerki bölümde görselleştirme yapabilmek için bu veriyi listeye alıyoruz
        for i in range(N):
            mse += (y[i] - (m*x[i]+b))**2 
            
        mselist.append(mse/N)  
        liste.append((b,m))
        
        c+=1

#belirlediğimiz iterasyon sayısının sonunda ulaştığımız sonuçların yazdırılması
print("b = {} ".format(b))
print("m = {} ".format(m,))
print("Hata Kareleri Ortalaması = {}".format(mselist[-1]))

Çıktılarımız,

  • b = 0.1588838217182271
  • m = 0.9044297199218837
  • Hata Kareleri Ortalaması = 0.36404489498478526

Betimlediğimiz doğru için fonksiyonumuz,

8. Görselleştirme vakti 📈


regressionLine = [ (b + m*i)    for i in range(-3,4)]
plt.scatter(x,y, color="k")
plt.plot(range(-3,4),regressionLine, color="r",linewidth=4)
plt.xlabel("X")
plt.ylabel("Y")
plt.show()

Çıktı,

Gözlemlerimiz ve bu gözlemler için betimlediğimiz doğru

Gradient descent sürecini görselleştirelim,

plt.plot(mselist,color="r")
plt.scatter(range(iter),mselist)
plt.xlabel("İterasyon Sayısı")
plt.ylabel("Hata Kareleri Aritmetik Ortalaması")
plt.title("Gradient Descent")
plt.show()

Çıktı,

Gradient descent

Grafikte de görüldü gibi algoritmamız hata büyükken büyük adımlar atmış, hata azaldıkça adımlarını küçültmüş.

Son olarak tüm süreci özetleyen animasyonumuz. Matplotlib ve ffmpeg kütüphaneleriyle bu tarz animasyonlar hazırlamak başka bir yazının konusu olma adayı :)

Sevgiyle kalın, sorgulamayı ve araştırmayı unutmayın.




updated_at 23-05-2020