Wielowarstwowa sieć neuronowa w Tensorflow do klasyfikacji cyfr z MNIST

We wpisie tym zbudujemy 5-warstwową w pełni połączoną (fully-connected) sieć neuronową klasyfikującą cyfry ze zbioru MNIST. W tym celu wykorzystamy Tensorflow oraz wprowadzimy nowe techniki pozwalające na uczenie głębszego modelu takie jak np. funkcje aktywacji: relu, elu, dropout oraz algorytm optymalizacyjny Adam.

Wpis ten jest kontynuację serii tekstów o klasyfikacji cyfr z MNIST z wykorzystaniem Tensorflow,

  • w poprzednim tekście stworzyliśmy jednowarstwową sieć neuronową, która osiągnęła 0.9237 dokładności, służy ona jako baseline dla dalszych naszych modeli
  • w tym artykule zbudujemy pięć wariantów wielowarstwowej sieci neuronowej, które w zależności od architektury osiągają dokładność od 0.9541 do 0.9817
  • w trzeciej części zbudujemy sieć konwolucyjną, która osiągnie dokładność klasyfikacji na poziomie 0.9880

Polecam także artykuł z  „Wielowarstwowa sieć neuronowa w Pytorch” oraz całą serię artykułów związanych z Pytorch

 Warianty sieci neuronowej

Obecnie zajmiemy się budową trzech wariantów pięciowarstwowej sieci neuronowej:

  • wariant 1 – 5 warstwowa sieć z sigmoidalną funkcją aktywacji, uczona przy pomocy „Gradient descent optimizer”
  • wariant 2 – 5 warstwowa sieć z funkcją aktywacji ReLU, która pozwoli nam na szybsze uczenie oraz wykorzystamy do treningu algorytm Adam
  • wariant 3 – tak jak wariant 2, z tym że dodamy dropout

Format danych oraz struktura wejścia i wyjścia sieci

Opis formatu danych oraz wejścia i wyjścia znajduje się w pierwszym wpisie, więc nie chcąc się powtarzać odsyłam do przeczytania bardziej szczegółowego opisu, tutaj przytoczę tylko najważniejsze informacje:

  • zbiór MNIST składa się z 60k obrazów treningowych i 10k testowych, które mają rozmiar 28×28 px
  • na wejściu sieci każdy obrazek traktujemy jako wektor o rozmiarze 784(=28*28)
  • zbiór danych podawany jest dla algorytmu uczącego w paczkach po 100 obrazków (BATCH)
  • wyjście sieci 10-elementowy wektor wykorzystujący one-hot encoding.

Budowa sieci neuronowej

Dla wszystkich 3 wariantów sieci zastosujemy taką samą architekturę z tą samą ilością neuronów w każdej warstwie. Ilość neuronów w kolejnych warstwach maleje od 200 do 10. Oczywiście nie jest to jedyna słuszna architektura, wynika ona z mojej intuicji oraz kilku eksperymentów, które poczyniłem. Jest to idealne miejsce do Waszych eksperymentów, jeżeli komuś z Was uda się osiągnąć lepsze wyniki to napiszcie w komentarzach lub wstawcie linka do gist na github.

input layer             - X[batch, 784]
1 layer                 - W1[784, 200] + b1[200]
                          Y1[batch, 200] 
2 layer                 - W2[200, 100] + b2[100]
                          Y2[batch, 200] 
3 layer                 - W3[100, 60]  + b3[60]
                          Y3[batch, 200] 
4 layer                 - W4[60, 30]   + b4[30]
                          Y4[batch, 30] 
5 layer                 - W5[30, 10]   + b5[10]
One-hot encoded labels    Y5[batch, 10]

model
Y = softmax(X*W+b)
Matrix mul: X*W - [batch,784]x[784,10] -> [batch,10]

Warstwa wejściowa dostaje macierz o wymiarach BATCH_SIZE x 784, czyli wierszowo ułożone obrazki z cyframi (nasze dane wejściowe). W warstwie pierwszej znajduje się 200 neuronów stąd macierz wag pomiędzy wejściem i wyjściem W1  ma wymiary 784×200, czyli każdy piksel z obrazka połączony jest wagą z danym neuronem, b1 zawiera wartości przesunięcia (w literaturze ang. bias) dla neuronów w warstwie pierwszej. Po zsumowaniu iloczynów wag i wartości wejść dodaniu przesunięcia przepuszczamy wynik przez funkcję aktywacji i otrzymujemy wyjście Y1 z warstwy pierwszej. I cała procedura się powtarza, wyjście Y1 jest wejściem dla drugiej warstwy już o mniejszej ilości neuronów (100) itd.

Ostatnia warstwa wyjściowa Y używa już funkcji softmax w celu obliczenia prawdopodobieństw przynależności do jednej z 10 klas (cyfry od 0 do 9).

Kod tworzący sieć

Poszczególne warianty prezentowanej sieci nie naruszają jej ogólnej architektury, tylko dodają lub zamieniają poszczególne elementy, takie jak: zmian funkcji aktywacji z sigmoid na ReLu, dodanie dropout itp.
Powyższą architekturę implementuje poniższy kawałek kodu:

import visualizations as vis
import tensorflow as tf
from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets

NUM_ITERS=5000
DISPLAY_STEP=100
BATCH=100

tf.set_random_seed(0)

# Download images and labels 
mnist = read_data_sets("MNISTdata", one_hot=True, reshape=False, validation_size=0)

# Placeholder for input images, each data sample is 28x28 grayscale images
# All the data will be stored in X - tensor, 4 dimensional matrix
# The first dimension (None) will index the images in the mini-batch
X = tf.placeholder(tf.float32, [None, 28, 28, 1])
# correct answers will go here
Y_ = tf.placeholder(tf.float32, [None, 10])

# layers sizes
L1 = 200
L2 = 100
L3 = 60
L4 = 30
L5 = 10

# weights - initialized with random values from normal distribution mean=0, stddev=0.1
# output of one layer is input for the next
W1 = tf.Variable(tf.truncated_normal([784, L1], stddev=0.1))
b1 = tf.Variable(tf.zeros([L1]))

W2 = tf.Variable(tf.truncated_normal([L1, L2], stddev=0.1))
b2 = tf.Variable(tf.zeros([L2]))

W3 = tf.Variable(tf.truncated_normal([L2, L3], stddev=0.1))
b3 = tf.Variable(tf.zeros([L3]))

W4 = tf.Variable(tf.truncated_normal([L3, L4], stddev=0.1))
b4 = tf.Variable(tf.zeros([L4]))

W5 = tf.Variable(tf.truncated_normal([L4, L5], stddev=0.1))
b5 = tf.Variable(tf.zeros([L5]))

# flatten the images, unroll each image row by row, create vector[784]
# -1 in the shape definition means - compute automatically the size of this dimension
XX = tf.reshape(X, [-1, 784])

W pierwszych liniach importujemy niezbędne biblioteki oraz ściągamy zbiór MNIST, następnie tworzymy dwa placeholder’y na dane wejściowe (X) oraz prawidłowe etykiety. Jak zwykle pierwszy wymiar jest ustawiony na None co daje znać dla TF, że zostanie on automatycznie wyliczony w zależności od podanych danych.

Następnie mamy definicję rozmiarów poszczególnych warstw (L1…L5) oraz tworzymy tensorflow’owe zmienne modelu czyli macierze wag pomiędzy warstwami. W ostatniej linie rozpłaszczamy obrazy z 28×28 do postaci wektora o rozmiarze 784.

Sieć neuronowa – wariant 1

Każdy z wariantów modeli jest do siebie podobny, różnią się głównie funkcją aktywacji. Pierwszy model wykorzystuje funkcję sigmoidalną. Aby otrzymać wyjście danej warstwy mnożymy wejście z warstwy poprzedniej (na początku są to nasze dane obrazkowe) przez macierz wag dla danej warstwy, dodajemy bias i stosujemy funkcję aktywacji.

# Define model
Y1 = tf.nn.sigmoid(tf.matmul(XX, W1) + b1)
Y2 = tf.nn.sigmoid(tf.matmul(Y1, W2) + b2)
Y3 = tf.nn.sigmoid(tf.matmul(Y2, W3) + b3)
Y4 = tf.nn.sigmoid(tf.matmul(Y3, W4) + b4)
Ylogits = tf.matmul(Y4, W5) + b5

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100
                                                          
# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# training, learning rate = 0.005
train_step = tf.train.GradientDescentOptimizer(0.005).minimize(cross_entropy)

Po definicji modelu tworzymy naszą loss function, w tym wypadku jest to średnia z softmax_cross_entropy na podstawie tej wartości będziemy oceniać jakość dotychczasowego modelu, minimalizacja tej funkcji będzie skutkowała jego uczeniem. W tym wariancie jako algorytm uczący wykorzystujemy prosty GradientDescent. Pamiętajcie, że Tensorflow buduje tylko graf obliczeń, który musimy jawnie „uruchomić”, zrobimy to w kolejnym kroku w pętli uczącej.

Sieć neuronowa – wariant 2

Tutaj model jest podobny do wariantu 1, z tym że zamiast funkcji sigmoid używamy funkcji ReLU, która ma kilka pożądanych cech wspomagających i przyspieszających proces uczenia sieci. Warto zwrócić uwagę na zastosowanie nowszego algorytmu uczącego Adam, jest on obecnie zalecany przy większości zadań optymalizacyjnych związanych z uczeniem.

# Define model
Y1 = tf.nn.relu(tf.matmul(XX, W1) + b1)
Y2 = tf.nn.relu(tf.matmul(Y1, W2) + b2)
Y3 = tf.nn.relu(tf.matmul(Y2, W3) + b3)
Y4 = tf.nn.relu(tf.matmul(Y3, W4) + b4)
Ylogits = tf.matmul(Y4, W5) + b5

# we use tensorflow function for cross entropy and softmax
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100

                                                          
# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# training, 
learning_rate = 0.003
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

Sieć neuronowa – wariant 3

W wariancie 3 w stosunku do wariantu 2 dodaliśmy dropout, czyli „usuwanie” z pewnym prawdopodobieństwem poszczególnych neuronów, ma to zapewnić mniejszą wrażliwość modelu na przeuczenie. Dropout można rozumieć jako formę regularyzacji modelu.

# Define model
Y1 = tf.nn.relu(tf.matmul(XX, W1) + b1)
Y1 = tf.nn.dropout(Y1, pkeep)
Y2 = tf.nn.relu(tf.matmul(Y1, W2) + b2)
Y2 = tf.nn.dropout(Y2, pkeep)
Y3 = tf.nn.relu(tf.matmul(Y2, W3) + b3)
Y3 = tf.nn.dropout(Y3, pkeep)
Y4 = tf.nn.relu(tf.matmul(Y3, W4) + b4)
Y4 = tf.nn.dropout(Y4, pkeep)
Ylogits = tf.matmul(Y4, W5) + b5

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100
                                                          
# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# training, 
learning_rate = 0.003
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

Procedura uczenia sieci

Poniżej zaprezentowany został kod z pętla uczącą model. Na początku musimy utworzyć sesje, w której inicjujemy wszystkie zmienne modelu sess.run(init) a następnie wykonujemy zadaną ilość iteracji, w każdej pobieramy próbkę danych (BATCH) i na nich dokonujemy aktualizacji modelu. Jednocześnie obliczamy cząstkowe wartości dla funkcji straty i dokładności klasyfikacji na próbce danych oraz na całym zbiorze testowym (acc_trn, loss_trn, acc_tst, loss_tst) w celu ich późniejszej wizualizacji.

# Initializing the variables
init = tf.global_variables_initializer()

train_losses = list()
train_acc = list()
test_losses = list()
test_acc = list()

saver = tf.train.Saver()

# Launch the graph
with tf.Session() as sess:
    sess.run(init)


    for i in range(NUM_ITERS+1):
        # training on batches of 100 images with 100 labels
        batch_X, batch_Y = mnist.train.next_batch(BATCH)
        
        if i%DISPLAY_STEP ==0:
            # compute training values for visualisation
            acc_trn, loss_trn, w, b = sess.run([accuracy, cross_entropy, allweights, allbiases], feed_dict={X: batch_X, Y_: batch_Y, pkeep: 1.0})
            
            
            acc_tst, loss_tst = sess.run([accuracy, cross_entropy], feed_dict={X: mnist.test.images, Y_: mnist.test.labels, pkeep: 1.0})
            
            print("#{} Trn acc={} , Trn loss={} Tst acc={} , Tst loss={}".format(i,acc_trn,loss_trn,acc_tst,loss_tst))

            train_losses.append(loss_trn)
            train_acc.append(acc_trn)
            test_losses.append(loss_trn)
            test_acc.append(acc_tst)

        # the backpropagationn training step
        sess.run(train_step, feed_dict={X: batch_X, Y_: batch_Y, pkeep: 0.75})


title = "MNIST 2.2 5 layers relu adam  dropout"
vis.losses_accuracies_plots(train_losses,train_acc,test_losses, test_acc,title,DISPLAY_STEP)

W ostatniej linii generujemy wykresy przedstawiające tempo uczenia modelu, wizualizacje przedstawiają cząstkowe wartości accuraccy i loss.

Porównanie wyników

Uruchamiając poszczególne skrypty w projekcie

  • mnist_2.0_5_layer_nn.py
  • mnist_2.1_5_layer_nn_relu_adam.py
  • mnist_2.0_5_layer_nn_relu_adam_dropout.py

Zostaną wygenerowane wykresy z tempem uczenia dla 5000 iteracji.

Zestawienie wyników

Wariant sieciAccuracy
Jednowarstwowa0.9237
5-warstwowa f-cja aktywacji sigmoid0.9541
5-warstwowa f-cja aktywacji relu, alg. optymalizacji Adam0.9817
5-warstwowa f-cja aktywacji relu+dropout, alg. optymalizacji Adam0.9761

Wykresy tempa uczenia sieci

Z powyższego zestawienia widać, że zmiana funkcji aktywacji oraz algorytmu optymalizacji zdecydowanie zwiększyła dokładność rozpoznania. W tym przypadku zaskoczeniem może być zmniejszenie dokładności dla wariantu z dodanym dropout prawdopodobnie dla tak małej ilości iteracji oraz niedużego zbioru dropout nie pomaga. Jego zadaniem jest niedopuszczenie do przeuczenia i to robi 🙂 Zobaczmy jak prezentuje się tempo uczenia na wykresach w powyższych przypadkach:

MNIST 5 layer nn sigmoid
Tempo uczenia zbioru MNIST dla 5-warstwowej sieci f-cja aktywacji sigmoid
MNIST 5 layer nn relu
Tempo uczenia zbioru MNIST dla 5-warstwowej sieci f-cja aktywacji ReLU
MNIST 5 layer nn relu dropout
Tempo uczenia zbioru MNIST dla 5-warstwowej sieci f-cja aktywacji ReLU wraz z dropout

Podsumowanie

W tym artykule przedstawiłem sposób budowania 5-warstwowej sieci neuronowej klasyfikującej cyfry z MNIST. Zbadaliśmy trzy warianty tej sieci z różnymi funkcjami aktywacji, alg. optymalizacji oraz zastosowaniem dropout. Sieć ta ma większą dokładność niż siec jednowarstwowa a użycie funkcji ReLU znacząco podnosi dokładność klasyfikacji.

Kod projektu dostępny na Github – Tensorflow MNIST

Jeżeli uważasz ten wpis za wartościowy to Zasubskrybuj bloga. Dostaniesz informacje o nowych artykułach.

Join 100 other subscribers

3 Comments Wielowarstwowa sieć neuronowa w Tensorflow do klasyfikacji cyfr z MNIST

  1. Marcin

    Cześć.

    Czytałem poprzednia artykuł i mam pytanie początkującego. W wszystkich wariantach powinno być po ostatniej warstwie sieci
    Y = tf.nn.softmax(Ylogits) ?

    W wstawkach kodu tego nie widzę.

    Blog bardzo mi się podoba, wiedza przedstawiona w przestępny sposób, linki do opanowania podstaw. Dla świeżaka w tym obszernym temacie bardzo pomocne oraz motywujące.

    Czekam na więcej wpisów 🙂

    Reply

Ciekawe, wartościowe, podziel się proszę opinią!

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.