Sıfırdan Sinir Ağı Oluşturma
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
Yorum Gönder