Python’da Sıfırdan Bir Sinir Ağının Uygulanması
Bu yazıda, sıfırdan bir 3 katmanlı sinir ağı inşa etmeye adım adım yol göstereceğiz. Tüm matematiksel türevlerle boğuşmak yerine, yaptığımız işlemlerin sezgisel bir anlayışını geliştirmeye odaklanacağız. Bu konuda daha ayrıntılı bilgi almak isterseniz, Andrew Ng’nin Coursera eğitim platformundaki “Deep Learning Specialization” kursuna göz atmanızı öneririm.
Bu yazıya başlamadan önce, temel Calculus ve Makine Öğrenimi kavramlarına aşina olduğunuzu varsayıyorum, örneğin sınıflandırma veya regularization kavramlarının ne olduğunu bildiğinizi tahmin ediyorum. İdeal olarak gradient descent (gradyan inişi) gibi optimizasyon tekniklerinin nasıl çalıştığı hakkında da biraz bilginiz varsa bu makaleyi anlamanız çok daha kolay olur. Ancak yukarıdakilerin hiçbirine aşina olmasanız bile bu yazı yine de ilginç olabilir!
Peki neden sıfırdan bir sinir ağı kurmalıyız? Gelecekte Keras, Tensorflow, Pytorch gibi sinir ağı kütüphanelerini kullanmayı planlasanız bile, en azından bir kez sıfırdan bir ağ kurmak, sinir ağlarının nasıl çalıştığını anlamanıza ve etkin modeller tasarlamaya yardımcı olur. Ancak belirtmekte fayda var; burada sunulan kod örnekleri tam verimlilik için optimize edilmiş değildir. Keras gibi kütüphanelerle aynı işlemi daha hızlı ve verimli bir şekilde yapabilirsiniz. Ancak bu yazının amacı, derin öğrenmenin temel prensiplerini anlaşılır bir şekilde aktarmaktır.
Bir veri seti oluşturma:
İlk adım olarak, bir veri kümesi oluşturacağız. Neyse ki, scikit-learn’in pratik veri kümesi oluşturma araçları mevcut, bu yüzden kendimiz sıfırdan bir tane yazmak zorunda değiliz. Bu amaçla make_moons fonksiyonunu kullanacağız.
# Package imports
import matplotlib.pyplot as plt
import numpy as np
import sklearn
import sklearn.datasets
import sklearn.linear_model
import matplotlib
# Generate a dataset and plot it
np.random.seed(0)
X, y = sklearn.datasets.make_moons(n_samples=300, noise=0.2)
plt.scatter(X[:,0], X[:,1], s=40, c=y, cmap='bwr')
plt.show()
Oluşturduğumuz veri kümesinde kırmızı ve mavi noktalar olarak gösterilen iki sınıf vardır. Mavi noktaları erkek hastalar, kırmızı noktaları ise kadın hastalar olarak düşünebilirsiniz; x ve y eksenleri ise tıbbi ölçümlerdir.
Amacımız, x- ve y- koordinatları verildiğinde doğru sınıfı (kadın veya erkek) tahmin eden bir Makine Öğrenimi sınıflandırıcısını eğitmektir. Verilerin doğrusal olarak ayrılabilir olmadığını, iki sınıfı ayıran düz bir çizgi çizemeyeceğimizi unutmayın. Bu, verilen veri kümesi için iyi çalışan doğrusal olmayan özellikleri (polinomlar gibi) elle tasarlamadığınız sürece Lojistik Regresyon gibi doğrusal sınıflandırıcıların verilere uyamayacağı anlamına gelir.
Aslında, Sinir Ağlarının en büyük avantajlarından biri de budur. Özellik mühendisliği (feature engineering) konusunda endişelenmenize gerek yoktur. Bir sinir ağının gizli katmanı sizin için özellikleri öğrenecektir.
Lojistik Regresyon:
Konuyu göstermek için bir Lojistik Regresyon sınıflandırıcısını eğitelim. Girdisi x ve y değerleri, çıktısı ise tahmin edilen sınıf (0 veya 1) olacaktır. Hayatımızı kolaylaştırmak için scikit-learn’den Logistic Regression sınıfını kullanıyoruz.
# Helper function to plot a decision boundary.
# If you don't fully understand this function don't worry, it just generates the contour plot below.
def plot_decision_boundary(pred_func):
# Set min and max values and give it some padding
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole gid
Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='CMRmap')
# Plot the decision boundary
plot_decision_boundary(lambda x: clf.predict(x))
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title("Lojistik Regresyon")
plt.show()p
Grafik, Lojistik Regresyon sınıflandırıcımız tarafından öğrenilen karar sınırını göstermektedir. Verileri düz bir çizgi kullanarak olabildiğince iyi bir şekilde ayırıyor, ancak verilerimizin “ay şeklini” yakalayamıyor.
Sinir Ağının Eğitilmesi:
Şimdi bir girdi katmanı, bir gizli katman ve bir çıktı katmanı olan 3 katmanlı bir sinir ağı oluşturalım. Girdi katmanındaki düğüm sayısı verilerimizin boyutluluğuna göre belirlenir.
Benzer şekilde, çıktı katmanındaki düğüm sayısı da sahip olduğumuz sınıf sayısına göre belirlenir. (Sadece 2 sınıfımız olduğu için aslında 0 veya 1'i tahmin eden tek bir çıktı düğümü ile kurtulabiliriz, ancak 2 sınıfı çıktı olması ağı daha sonra daha fazla sınıfa genişletmeyi kolaylaştırır). Ağın girdisi x- ve y- koordinatları, çıktısı ise biri 0. sınıf (“kadın”) diğeri 1. sınıf (“erkek”) için olmak üzere iki olasılık olacaktır. Şöyle bir şeye benziyor:
Gizli katmanın boyutunu (düğüm sayısını) seçebiliriz. Gizli katmana ne kadar çok düğüm koyarsak o kadar karmaşık fonksiyonlar sığdırabiliriz. Ancak daha yüksek boyutluluğun bir bedeli vardır. İlk olarak, tahminler yapmak ve ağ parametrelerini öğrenmek için daha fazla hesaplama gerekir. Daha fazla sayıda parametre, verilerimize aşırı uyum sağlamaya daha yatkın hale geldiğimiz anlamına da gelir.
Gizli katmanın boyutu nasıl seçilir? Bazı genel yönergeler ve tavsiyeler olsa da, bu her zaman özel probleminize bağlıdır ve bir bilimden çok bir sanattır. Gizli katmandaki düğüm sayısı ile daha sonra oynayacak ve bunun çıktımızı nasıl etkilediğini göreceğiz.
Ayrıca gizli katmanımız için bir aktivasyon fonksiyonu seçmemiz gerekir. Aktivasyon fonksiyonu katmanın girdilerini çıktılarına dönüştürür. Doğrusal olmayan bir aktivasyon fonksiyonu, doğrusal olmayan hipotezlere uymamızı sağlayan şeydir. Aktivasyon fonksiyonları için yaygın seçenekler tanh, sigmoid fonksiyonu veya ReLU’lardır. Biz birçok senaryoda oldukça iyi performans gösteren tanh fonksiyonunu kullanacağız. Bu fonksiyonların güzel bir özelliği, türevlerinin orijinal fonksiyon değeri kullanılarak hesaplanabilmesidir.
Örneğin, tanhx’ın türevi 1 - tanh²x’dır. Bu kullanışlıdır çünkü tanhx’ı bir kez hesaplamamıza ve aynı değeri daha sonra türevini almak için yeniden kullanmamıza olanak tanır. Ağımızın olasılık çıktısı vermesini istediğimizden, çıktı katmanı için aktivasyon fonksiyonu, ham puanları olasılıklara dönüştürmenin bir yolu olan softmax olacaktır. Lojistik fonksiyonuna aşinaysanız, softmax’i birden fazla sınıfa genelleştirilmesi olarak düşünebilirsiniz.
Ağımız nasıl tahminlerde bulunuyor?:
Ağımız, sadece bir grup matris çarpımı ve yukarıda tanımladığımız aktivasyon fonksiyonlarının uygulanması olan ileri yayılımı kullanarak tahminler yapar. Eğer x ağımızın 2 boyutlu girdisi ise o zaman y (yine iki boyutlu) tahminimizi aşağıdaki gibi hesaplarız:
‘z’ giriş katmanıyken ‘a’ çıkış katmanıdır. W ve b eğitim verilerimizden öğrenmemiz gereken ağımızın parametreleridir. Bunları ağın katmanları arasında veri dönüştüren matrisler olarak düşünebilirsiniz. Yukarıdaki matris çarpımlarına bakarak bu matrislerin boyutsallığını anlayabiliriz.
Parametreleri Öğrenmek:
Ağımız için parametreleri öğrenmek, eğitim verilerimizdeki hatayı en aza indiren parametreleri (W, b) bulmak anlamına gelir. Peki hatayı nasıl tanımlayacağız? Hatamızı ölçen fonksiyona kayıp fonksiyonu diyoruz. Softmax çıktısı için yaygın bir seçim categorical cross-entropy loss (negative log likelihood olarak da bilinir). N eğitim örneğimiz ve C sınıfımız varsa, gerçek etiketlere göre tahminimiz için kayıp şu şekilde verilir:
Formül karmaşık görünse de aslında yaptığı tek şey eğitim örneklerimizi toplamak ve yanlış sınıfı tahmin etmemiz durumunda kaybımızı eklemektir. Doğru etiketler ve tahminlerimiz iki olasılık dağılımından ne kadar uzaksa kaybımız da o kadar büyük olacaktır. Kaybı en aza indiren parametreleri bularak eğitim verilerimizin olasılığını en üst düzeye çıkarırız.
Bu minimum değeri bulmak için gradyan inişini kullanabiliriz. Sabit bir öğrenme oranına sahip toplu gradyan inişi olarak da adlandırılan gradyan inişinin bir versiyonunu uygulayacağız. SGD (stochastic gradient descent) veya minibatch gradient descent gibi varyasyonlar pratikte genellikle daha iyi performans gösterir. Dolayısıyla, eğer ciddi bir uygulama yapacaksanız bunlardan birini kullanmak isteyeceksiniz ve ideal olarak öğrenme oranını da zaman içinde azaltacaksınız.
Girdi olarak gradyan inişi, parametrelerimize göre kayıp fonksiyonunun gradyanlarına (türevler vektörü) ihtiyaç duyar:
Bu gradyanları hesaplamak için, çıktıdan başlayarak gradyanları verimli bir şekilde hesaplamanın bir yolu olan ünlü geri yayılım algoritmasını kullanırız. Geriye yayılımın nasıl çalıştığının ayrıntılarına girmeyeceğim, ancak internette birçok mükemmel açıklama var.
Geriye yayılım formülünü uyguladığımızda aşağıdakileri buluruz:
Uygulama:
Şimdi uygulamamız için hazırız. Gradyan inişi için bazı yararlı değişkenleri ve parametreleri tanımlayarak başlıyoruz:
num_examples = len(X) # training set size
nn_input_dim = 2 # input layer dimensionality
nn_output_dim = 2 # output layer dimensionality
# Gradient descent parameters (I picked these by hand)
epsilon = 0.01 # learning rate for gradient descent
reg_lambda = 0.01 # regularization strength
İlk olarak yukarıda tanımladığımız kayıp fonksiyonunu uygulayalım. Bunu modelimizin ne kadar iyi çalıştığını değerlendirmek için kullanacağız:
# Helper function to evaluate the total loss on the dataset
def calculate_loss(model):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation to calculate our predictions
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# Calculating the loss
corect_logprobs = -np.log(probs[range(num_examples), y])
data_loss = np.sum(corect_logprobs)
# Add regulatization term to loss (optional)
data_loss += reg_lambda/2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
return 1./num_examples * data_loss
Ayrıca ağın çıktısını hesaplamak için bir yardımcı fonksiyon uyguluyoruz. Yukarıda tanımlandığı gibi ileri yayılım yapar ve en yüksek olasılığa sahip sınıfı döndürür.
# Helper function to predict an output (0 or 1)
def predict(model, x):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation
z1 = x.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
return np.argmax(probs, axis=1)
Son olarak, Sinir Ağımızı eğitecek fonksiyon geliyor. Yukarıda bulduğumuz backpropagation türevlerini kullanarak toplu gradyan inişini uygular.
# This function learns parameters for the neural network and returns the model.
# - nn_hdim: Number of nodes in the hidden layer
# - num_passes: Number of passes through the training data for gradient descent
# - print_loss: If True, print the loss every 1000 iterations
def build_model(nn_hdim, num_passes=20000, print_loss=False):
# Initialize the parameters to random values. We need to learn these.
np.random.seed(0)
W1 = np.random.randn(nn_input_dim, nn_hdim) / np.sqrt(nn_input_dim)
b1 = np.zeros((1, nn_hdim))
W2 = np.random.randn(nn_hdim, nn_output_dim) / np.sqrt(nn_hdim)
b2 = np.zeros((1, nn_output_dim))
# This is what we return at the end
model = {}
# Gradient descent. For each batch...
for i in range(0, num_passes):
# Forward propagation
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# Backpropagation
delta3 = probs
delta3[range(num_examples), y] -= 1
dW2 = (a1.T).dot(delta3)
db2 = np.sum(delta3, axis=0, keepdims=True)
delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
dW1 = np.dot(X.T, delta2)
db1 = np.sum(delta2, axis=0)
# Add regularization terms (b1 and b2 don't have regularization terms)
dW2 += reg_lambda * W2
dW1 += reg_lambda * W1
# Gradient descent parameter update
W1 += -epsilon * dW1
b1 += -epsilon * db1
W2 += -epsilon * dW2
b2 += -epsilon * db2
# Assign new parameters to the model
model = { 'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
# Optionally print the loss.
# This is expensive because it uses the whole dataset, so we don't want to do it too often.
if print_loss and i % 1000 == 0:
print("Loss after iteration %i: %f" %(i, calculate_loss(model)))
return model
Gizli katmanı 3 boyutunda olan bir ağ:
Gizli katman boyutu 3 olan bir ağı eğitirsek ne olacağını görelim.
# Build a model with a 3-dimensional hidden layer
model = build_model(3, print_loss=True)
# Plot the decision boundary
plot_decision_boundary(lambda x: predict(model, x))
plt.title("Gizli katman boyutu 3 olan Sinir Ağı karar sınırı")
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.show()
Harika! Bu oldukça iyi görünüyor. Sinir ağlarımız, sınıfları başarılı bir şekilde ayıran bir karar sınırı bulmayı başardı.
Gizli katman boyutunu değiştirmek:
Yukarıdaki örnekte gizli katman boyutunu 3 olarak seçtik. Şimdi gizli katman boyutunu değiştirmenin sonucu nasıl etkilediğini anlayalım.
plt.figure(figsize=(16, 32))
hidden_layer_dimensions = [1, 2, 3, 4, 5, 20, 50]
for i, nn_hdim in enumerate(hidden_layer_dimensions):
plt.subplot(5, 2, i+1)
plt.title('Gizli Katman Boyutu %d' % nn_hdim)
model = build_model(nn_hdim)
plot_decision_boundary(lambda x: predict(model, x))
plt.show()
Düşük boyutlu bir gizli katmanın verilerimizin genel eğilimini güzel bir şekilde yakaladığını görebiliriz. Daha yüksek boyutluluklar aşırı uyuma eğilimlidir. Genel şekle uymak yerine verileri “ezberlerler”. Modelimizi ayrı bir test setinde değerlendirecek olsaydık (ki kendiniz bunu uygulamalısınız!) daha küçük gizli katman boyutuna sahip model, daha iyi genelleme nedeniyle muhtemelen daha iyi performans gösterecekti. Aşırı uyumu daha güçlü bir düzenlileştirme ile önleyebiliriz, ancak gizli katman için doğru boyutu seçmek çok daha ekonomik bir çözümdür.
Bu yazıda derin öğrenmenin temeli olan sinir ağlarını sıfırdan yazdık. Beğendiyseniz aşağıdaki alkışa istediğiniz kadar tıklayarak yazılarıma destek olabilirsiniz :)
Vakit ayırıp okuduğunuz için teşekkür ederim. Herhangi bir sorunuz olursa veya benimle iletişim kurmak isterseniz tüm sosyal medya hesaplarım aşağıdaki linkte yer alıyor.
KAYNAK: