Sıfırdan Sinir Ağı Oluşturma

Neural Networks


Bu yazımızda sıfırdan bir sinir ağı kuracağız. Ünlü MNIST veri setini kullanarak elle yazılmış rakamları tanıması için eğiteceğiz.

Ağımızı oluşturmak için NumPy ve temel Python kullanacağız (Keras veya TensorFlow gibi üst düzey kütüphaneler yok).

scikit-learn kullanacağız, ancak sadece MNIST verilerini almak ve modelimizi oluşturulduktan sonra değerlendirmek için.


Mümkün olan en basit “ağ” ile başlayacağız: sadece 0 rakamını tanıyan tek bir düğüm. Bu aslında sadece lojistik regresyonun bir uygulamasıdır. Ancak, işler daha karmaşık hale gelmeden önce bazı temel bileşenleri çalıştırmamıza yardımcı olacaktır.

Sonra bunu tek gizli katmana sahip bir ağa genişleteceğiz, ardından 0 - 9 arasındaki tüm rakamları tanımak için bir softmax ekleyeceğiz. Bu bize %92 doğruluk oranına sahip bir rakam tanıyıcısı verir ve bizi 1985 teknolojisinin en ileri noktasına getirir.



1. MNIST Veri Seti


MNIST, her biri 28 x 28 boyutlarında, gri tonlarında 0 ila 255 arasında piksel değerlerine sahip 70.000 adet elle yazılmış rakam içerir. Verileri kendimiz indirip işleyebiliriz. Ama scikit-learn üreticileri bunu zaten bizim için yapmışlar. Çabalarını ihmal etmek kaba olacağından, sadece import edelim:


from sklearn.datasets import fetch_mldata

mnist = fetch_mldata('MNIST original')

X, y = mnist["data"], mnist["target"]


Değerleri yönetilebilir tutmak için verileri normalleştireceğiz:


X = X / 255


Varsayılan MNIST etiketleri yedinin görüntüsü için 7, dördün görüntüsü için 4 şeklinde kayıt tutar. Ancak şimdilik sadece sıfır sınıflandırıcı oluşturuyoruz. Bu nedenle, sıfır olduğunda etiketlerimizin 1, aksi halde 0 olmasını istiyoruz. Bunu yapmak için etiketlerin üzerine yazacağız:


import numpy as np

y_new = np.zeros(y.shape)

y_new[np.where(y == 0.0)[0]] = 1

y = y_new


Şimdi train ve test verilerimizi bölebiliriz. MNIST görüntüleri, ilk 60.000'i eğitim (train) için ve son 10.000'i test için kullanılabilecek şekilde önceden düzenlenmiştir. Ayrıca, verileri istediğimiz şekile, her örnek için bir satırı sütuna dönüştürebileceğiz:


m = 60000

m_test = X.shape[0] - m


X_train, X_test = X[:m].T, X[m:].T

y_train, y_test = y[:m].reshape(1,m), y[m:].reshape(1,m_test)


Son olarak, eğitim setini karıştıralım:


np.random.seed(138)

shuffle_index = np.random.permutation(m)

X_train, y_train = X_train[:,shuffle_index], y_train[:,shuffle_index]


Hiçbir şeyi gözden kaçırmadığımızdan emin olmak için rastgele bir resme ve etiketine bakalım:


%matplotlib inline

import matplotlib

import matplotlib.pyplot as plt

i = 3

plt.imshow(X_train[:,i].reshape(28,28), cmap = matplotlib.cm.binary)

plt.axis("off")

plt.show()

print(y_train[:,i])

[1.]


Bu bir sıfır olduğundan etiketin 1 olmasını istiyoruz ki öyle. İyi görünüyor, o halde ilk ağımızı oluşturalım.



2. Tekli Nöron (Lojistik Regresyon)


784 giriş (=28 x 28) ve bir çıkışı üreten tek bir sigmoid birim ile basit, ileri beslemeli bir ağ oluşturmak istiyoruz.



2.1 İleri Yayılma


Tek bir x örneğindeki ileri geçişte, aşağıdaki hesaplama kullanılır:


ŷ = σ((w^T) * x + b)


Burada σ sigmoid fonksiyonudur:


σ(z) = 1 / (1 + e^−z)


Sigmodi tanımlayalım:


def sigmoid(z):

s = 1 / (1 + np.exp(-z))

return s


Örnekleri yan yana istifleyerek vektörleştireceğiz, böylece giriş matrisimiz X’in her sütununda bir örnek olacak. İleri geçişin vectörize şekli:


ŷ = σ((w^T) * X + b)


ŷ 'nin artık bir önceki denklemdeki gibi bir skaler değil, bir vektör olduğunu unutmayın.


Kodumuzda bunu iki aşamada hesaplayacağız: Z = np.matmul(W.T, X) + b ve sonra A = sigmoid(Z). (A, etkinleştirme için.) İşleri böyle aşamalara ayırmak sadece düzenli olmak içindir - ileriye doğru yayılım hesaplarımızın geriye doğru yayılım hesaplarımızdaki adımları yansıtmasını sağlayacaktır.



2.2 Maliyet Fonksiyonu


Maliyet fonksiyonumuz için çapraz entropi kullanacağız. Tek bir eğitim örneği için formül:


L(y,ŷ) = −ylog(ŷ) − (1−y)log(1−ŷ)


Sahip olduğumuz m örneğin bir eğitim seti üzerinde ortalaması:


L(Y,Ŷ) = −1/m m∑i=1 ( y^(i) * log(ŷ^(i)) + (1 − y^(i)) * log(1−ŷ^(i)) )


Bunu tanımlayalım:


def compute_loss(Y, Y_hat):


m = Y.shape[1]

L = -(1./m) * ( np.sum( np.multiply(np.log(Y_hat),Y) ) + np.sum( np.multiply(np.log(1-Y_hat),(1-Y)) ) )


return L



2.3 Geri Yayılma


Geri yayılım için, wj'nin her bir bileşenine göre L'nin nasıl değiştiğini bilmemiz gerekecek. Yani, her bir ∂L/∂wj'yi hesaplamalıyız.

Tek bir örneğe odaklanmak, ihtiyacımız olan formülleri türetmemizi kolaylaştıracaktır. wj dışındaki tüm değerleri sabit tutarak, L'nin üç adımda hesaplandığını düşünebiliriz: wj→z→ŷ→L. Bu adımlar için formüller şunlardır:


z = w ^ x+b

ŷ = σ(z)

L(y,ŷ) = −ylog(1−ŷ)−(1−y)log(1−ŷ)


Ve zincir kuralı bize şunu söyler:


∂L/∂wj = (∂L/∂ŷ) (∂ŷ/∂z) (∂z/∂wj)


Önce ∂L/∂ŷ 'ye bakalım:


∂L/∂ŷ = ∂/∂ŷ (−ylog(ŷ)−(1−y)log(1−ŷ))

   = −y (∂/∂ŷ) log(ŷ) − (1−y) (∂/∂ŷ) log(1−ŷ)

   = −y/ŷ + (1−y)/(1−ŷ)

   = (ŷ − y) / ŷ(1 − ŷ)


Sırada ∂ŷ/∂z:


(∂/∂z) σ(z) = ∂/∂z (1 / 1+e^−z)

           = −1 / (1+e^−z)^2 * (∂/∂z)(1+e^−z)

   = e^−z / (1+e^−z)^2

   = 1 / (1+e)^−z * (e ^ −z) / (1 + e^−z)

           = σ(z) (e^−z) / 1+e^−z

   = σ(z) (1 − 1 / (1+e^−z))

   = σ(z) (1 − σ(z))

   = ŷ (1−ŷ)


Ardından ∂z/∂wj:


∂/∂wj ((w^T)x + b) = (∂/∂wj) (w0 * x0 + … + wn * xn + b)

= wj


Son olarak aşağıdakileri bulmak için zincir kuralının yerine geçebiliriz:


∂L/∂wj = ∂L/∂ŷ * ∂ŷ/∂z * ∂z/∂wj

             = ((ŷ−y) / ŷ(1−ŷ)) ŷ (1−ŷ) wj

     = (ŷ − y)wj


m eğitim örnekleri ile vektörleştirilmiş formda bize şunu verir:


∂L/∂w = (1/m) X(ŷ − y)^T


Peki ya ∂L/∂b? Tek bir örnek için çok benzer bir türetme şu sonuçları verir:


∂L/∂b = (ŷ − y)


Vektörize edilmiş formda:


∂L/∂b = 1/m m∑i=1 (ŷ^(i) − y^(i))


Kodumuzda bu eğimleri paydalarına göre dW ve db olarak etiketleyeceğiz.

Geriye yayılım için hesaplama; dW = (1/m) * np.matmul(X, (A-Y).T) ve db = (1/m) * np.sum(A-Y, axis=1, keepdims=True)



2.4 Build & Train


Ağımızı kurmaya ve eğitmeye hazırız!


learning_rate = 1


X = X_train

Y = y_train


n_x = X.shape[0]

m = X.shape[1]


W = np.random.randn(n_x, 1) * 0.01

b = np.zeros((1, 1))


for i in range(2000):

Z = np.matmul(W.T, X) + b

A = sigmoid(Z)


cost = compute_loss(Y, A)


dW = (1/m) * np.matmul(X, (A-Y).T)

db = (1/m) * np.sum(A-Y, axis=1, keepdims=True)


W = W - learning_rate * dW

b = b - learning_rate * db


if (i % 100 == 0):

print("Epoch", i, "cost: ", cost)


print("Final cost:", cost)


Epoch 0 cost:  0.6840801595436431
Epoch 100 cost:  0.041305162058342754
... *snip* ...
Final cost: 0.02514156608481825


Muhtemelen biraz daha eğitimle daha isabetli bir sonuç verebiliriz. Ancak kazanımlar önemli ölçüde yavaşladı. Şimdi, karışıklık matrisine bakarak nasıl yaptığımızı görelim:


from sklearn.metrics import classification_report, confusion_matrix


Z = np.matmul(W.T, X_test) + b

A = sigmoid(Z)


predictions = (A>.5)[0,:]

labels = (y_test == 1)[0,:]


print(confusion_matrix(predictions, labels))


[[8980   33]
 [  40  947]]


Bu gerçekten oldukça iyi! Sıfırların 947'sini aldık ve sadece 33'ünü kaçırdık, neredeyse tüm negatif vakaları doğru anladık. 0.99 olan f1 puanı açısından:


print(classification_report(predictions, labels))


              precision   recall  f1-score  support

      False       1.00      1.00       1.00      9013
       True       0.97      0.96      0.96       987

avg / total   0.99      0.99      0.99     10000


Şimdi, çalışan bir model ve optimizasyon algoritmamız olduğuna göre, onu zenginleştirelim.



3. Gizli Katman


Şimdi 64 birimli bir gizli katman ekleyelim. Bu sefer ileri ve geri geçişler için tüm formüllerin türevlerini incelemeyeceğim; daha önce yaptığımız çalışmanın doğrudan bir uzantısı. Bunun yerine hemen konuya girip modeli oluşturalım:


X = X_train

Y = y_train


n_x = X.shape[0]

n_h = 64

learning_rate = 1


W1 = np.random.randn(n_h, n_x)

b1 = np.zeros((n_h, 1))

W2 = np.random.randn(1, n_h)

b2 = np.zeros((1, 1))


for i in range(2000):


Z1 = np.matmul(W1, X) + b1

A1 = sigmoid(Z1)

Z2 = np.matmul(W2, A1) + b2

A2 = sigmoid(Z2)


cost = compute_loss(Y, A2)


dZ2 = A2-Y

dW2 = (1./m) * np.matmul(dZ2, A1.T)

db2 = (1./m) * np.sum(dZ2, axis=1, keepdims=True)


dA1 = np.matmul(W2.T, dZ2)

dZ1 = dA1 * sigmoid(Z1) * (1 - sigmoid(Z1))

dW1 = (1./m) * np.matmul(dZ1, X.T)

db1 = (1./m) * np.sum(dZ1, axis=1, keepdims=True)


W2 = W2 - learning_rate * dW2

b2 = b2 - learning_rate * db2

W1 = W1 - learning_rate * dW1

b1 = b1 - learning_rate * db1


if i % 100 == 0:

print("Epoch", i, "cost: ", cost)


print("Final cost:", cost)


Epoch 0 cost:  0.9144384083567224
Epoch 100 cost:  0.08856953026938433
... *snip* ...
Final cost: 0.024249298861903648


Nasıl yaptık?


Z1 = np.matmul(W1, X_test) + b1

A1 = sigmoid(Z1)

Z2 = np.matmul(W2, A1) + b2

A2 = sigmoid(Z2)


predictions = (A2>.5)[0,:]

labels = (y_test == 1)[0,:]


print(confusion_matrix(predictions, labels))

print(classification_report(predictions, labels))


[[8984   36]
 [  36  944]]

              precision    recall  f1-score  support

      False       1.00       1.00      1.00     9020
       True       0.96      0.96      0.96       980

avg / total    0.99      0.99     0.99     10000


Fena değil ama tek nöronlu modelimizin yaptığıyla hemen hemen aynı. Daha fazla eğitim yapabilir ve daha fazla düğüm/katman ekleyebiliriz.

Şimdilik on rakamın tümünü tanımaya dönelim.



4. Çoklu Sınıfa Yükseltme


4.1 Etiketler


Öncelikle etiketlerimizi düzeltmemiz gerekiyor. Her şeyi yeniden içe aktaracağız, böylece geri dönüp daha önceki karıştırma işlemimizle koordinasyon yapmak zorunda kalmayacağız:


mnist = fetch_mldata('MNIST original')

X, y = mnist["data"], mnist["target"]


X = X / 255


Ardından, 10 x 70.000'lik bir dizi elde etmek için MNIST'in etiketlerini kodlayacağız.


digits = 10

examples = y.shape[0]


y = y.reshape(1, examples)


Y_new = np.eye(digits)[y.astype('int32')]

Y_new = Y_new.T.reshape(digits, examples)


Sonra, eğitim setimizi yeniden böler, yeniden şekillendirir ve yeniden karıştırırız:


m = 60000

m_test = X.shape[0] - m


X_train, X_test = X[:m].T, X[m:].T

Y_train, Y_test = Y_new[:,:m], Y_new[:,m:]


shuffle_index = np.random.permutation(m)

X_train, Y_train = X_train[:, shuffle_index], Y_train[:, shuffle_index]


Her şeyin olması gerektiği gibi olup olmadığını hızlı bir şekilde kontrol edelim:


i = 12

plt.imshow(X_train[:,i].reshape(28,28), cmap = matplotlib.cm.binary)

plt.axis("off")

plt.show()

Y_train[:,i]

array([0., 0., 0., 0., 0., 0., 1., 0., 0., 0.])


Şimdi modelde ne tür değişiklikler yapmamız gerektiğini düşünelim.



4.2 İleri Yayılma


Ağımızın sadece son katmanı değişiyor. Softmax'ı eklemek için, yalnız, son düğümümüzü 10 birimlik bir katmanla değiştirmeliyiz. Nihai aktivasyonları, z değerlerinin üstelleridir ve bu tür on üstelin tamamında normalize edilmiştir. Yani sadece σ(z) hesaplamak yerine, her birim I için aktivasyonu hesaplıyoruz:


e^zi / ∑9j=0 e^zj


Dolayısıyla, vektörleştirilmiş kodumuzda, ileri yayılımın son satırı A2 = np.exp(Z2) / np.sum(np.exp(Z2), axis=0) olacaktır.



4.3 Maliyet Fonksiyonu


Maliyet fonksiyonumuz artık ikiden fazla sınıfa genelleştirilmelidir. n sınıf için genel formül şöyledir:


L(y,ŷ) = −n∑i=0 yi log(ŷi)


m üzerinde eğitim örneğinin ortalaması şu olur:


def compute_multiclass_loss(Y, Y_hat):


L_sum = np.sum(np.multiply(Y, np.log(Y_hat)))

m = Y.shape[1]

L = -(1/m) * L_sum


return L



4.4 Destek (Backdrop)


Şans eseri, backprop'un bir softmax'a geçişten gerçekten etkilenmediği ortaya çıktı. Bir softmax, kullandığımız sigmoid aktivasyonunu genelleştirir ve daha önce yazdığımız kod hala çalışır. Bunu türeterek doğrulayabiliriz:


∂L/∂zi = ŷi − yi



4.5 Build & Train


n_x = X_train.shape[0]

n_h = 64

learning_rate = 1


W1 = np.random.randn(n_h, n_x)

b1 = np.zeros((n_h, 1))

W2 = np.random.randn(digits, n_h)

b2 = np.zeros((digits, 1))


X = X_train

Y = Y_train


for i in range(2000):


Z1 = np.matmul(W1,X) + b1

A1 = sigmoid(Z1)

Z2 = np.matmul(W2,A1) + b2

A2 = np.exp(Z2) / np.sum(np.exp(Z2), axis=0)


cost = compute_multiclass_loss(Y, A2)


dZ2 = A2-Y

dW2 = (1./m) * np.matmul(dZ2, A1.T)

db2 = (1./m) * np.sum(dZ2, axis=1, keepdims=True)


dA1 = np.matmul(W2.T, dZ2)

dZ1 = dA1 * sigmoid(Z1) * (1 - sigmoid(Z1))

dW1 = (1./m) * np.matmul(dZ1, X.T)

db1 = (1./m) * np.sum(dZ1, axis=1, keepdims=True)


W2 = W2 - learning_rate * dW2

b2 = b2 - learning_rate * db2

W1 = W1 - learning_rate * dW1

b1 = b1 - learning_rate * db1


if (i % 100 == 0):

print("Epoch", i, "cost: ", cost)


print("Final cost:", cost)


Epoch 0 cost:  9.243960401572568
... *snip* ...
Epoch 1900 cost:  0.24585173887243117
Final cost: 0.24072776877870128


Nasıl yaptığımıza bakalım:


Z1 = np.matmul(W1, X_test) + b1

A1 = sigmoid(Z1)

Z2 = np.matmul(W2, A1) + b2

A2 = np.exp(Z2) / np.sum(np.exp(Z2), axis=0)


predictions = np.argmax(A2, axis=0)

labels = np.argmax(Y_test, axis=0)


print(confusion_matrix(predictions, labels))

print(classification_report(predictions, labels))


[[ 946  0     14     3      3      10     12     2       9      4	]
 [   0   1112    3     2      1        1       2     8       3      4	]
 [   3    4   937    24    10       7       8    18      8      3	]
 [   4    2     17   924     1      39      4     13    26      9	]
 [   0    1     10      0  905       9     11      9     10    40	]
 [  12    5     2     26      3   786     15      3    24     14	]
 [   8    1     19      2      9      10  902      1      9       1	]
 [   2    1     13     14      3       5       1   946     9    25	]
 [   5    9    16      11      5     18       3      5   868     9	]
 [   0    0     1       4    42       7       0    23     8   900]]

             precision  recall  f1-score  support

          0       0.97      0.94      0.95      1003
          1       0.98      0.98      0.98       1136
          2       0.91      0.92      0.91       1022
          3       0.91      0.89      0.90      1039
          4       0.92      0.91      0.92       995
          5       0.88      0.88      0.88       890
          6       0.94      0.94      0.94       962
          7       0.92      0.93      0.92      1019
          8       0.89      0.91      0.90       949
          9       0.89      0.91      0.90       985

avg/total   0.92      0.92      0.92     10000


Tüm rakamlarda %92 doğruluktayız, fena değil! Ve daha fazla eğitimle daha da gelişebiliriz gibi görünüyor.


Kaynaklar

https://en.wikipedia.org/wiki/Neural_network

https://jonathanweisberg.org/post/A%20Neural%20Network%20from%20Scratch%20-%20Part%201

https://ibrahimakin.github.io/blog/#/view/zgypzVO2m1ZRW0uhCVEP

Yorumlar

Bu blogdaki popüler yayınlar

Perceptron