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 sieci | Accuracy |
---|---|
Jednowarstwowa | 0.9237 |
5-warstwowa f-cja aktywacji sigmoid | 0.9541 |
5-warstwowa f-cja aktywacji relu, alg. optymalizacji Adam | 0.9817 |
5-warstwowa f-cja aktywacji relu+dropout, alg. optymalizacji Adam | 0.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:
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.
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 🙂