Potpuno povezane neuronske mreže¶

Preporučujem da pogledate sledeće snimke:
https://www.youtube.com/watch?v=aircAruvnKk (prvi deo)
https://www.youtube.com/watch?v=IHZwWFHWa-w (drugi deo)
https://www.youtube.com/watch?v=Ilg3gGewQ5U (treći deo)
https://www.youtube.com/watch?v=tIeHLnjs5U8 (četvrti deo)

Delovi materijala su preuzeti iz skripte "Mašinsko učenje" profesora Mladena Nikolića

Došli smo do trenutka kada ćemo uvesti jedan od najnaprednijih i najšire korišćenih modela mašinskog učenja - neuronskih mreža. Neuronske mreže se mogu koristiti kako u regresionim, tako i u klasifikacionim problemima. Moć neuronskih mreža se, između ostalog, ogleda u njihovoj sposobnosti razdvajanja podataka nelinearno. Neuronske mreže su takođe superiornije u odnosu na druge modele u situacijama kada imamo veliku količinu podataka sirove reprezentacije.
Neuronske mreže su konstruisane po uzoru na model biološkog neurona, sa željom da oponašaju učenje svojstveno živim bićima, a posebno čoveku. Kao takve su našle primenu u rešavanju brojnih zadataka, poput prepoznavanja objekata na slikama, igranja video igara, prevođenja stranih jezika itd. Međutim, iako su neuronske mreže poprilično uspešne u takvim situacijama, njihova mana je u tome što je proces njihovog obučavanja vremenski zahtevan i može potrajati po više sati, dana ili čak meseci. Iako su po svojim performansima neuronske mreže zaista impresivne, ne treba ih koristiti u apsolutno svakoj situaciji. Preduslov za izgradnju uspešnog modela neuronske mreže je veliki skup podataka za njihovo obučavanje. Zatim, kako postojanje više skrivenih slojeva na neki način od polaznih atributa konstruiše nove atribute, neuronske mreže je poželjno upotrebiti baš u radu sa podacima koji dolaze u sirovom obliku poput raznih vrsta signala, kako bi same ekstrahovale najznačajnije prediktore. U zavisnosti od arhitekture i oblasti u kojima su našle primenu, neuronske mreže se mogu podeliti na više tipova. Osnovnu varijantu mreža predstavljaju potpuno povezane neuroske mreže (eng. fully connected neural networks).

Example-of-fully-connected-neural-network.png

Potpuno povezane mreže se sastoje iz više neurona organizovanih u slojeve, koji primaju informacije od svih neurona iz prethodnog sloja i prenose ih nakon određenih transformacija neuronima narednog sloja. Ovaj tip mreže se sastoji iz ulaznog sloja, skrivenih slojeva i izlaznog sloja. Veličina različitih slojeva, odnosno broj neurona u njima, ne mora biti ista. Mreža koja se sastoji od barem dva skrivena sloja naziva se duboka neuronska mreža (eng. deep neural network).
Neuron se sastoji od onoliko parametara koliko prima ulaznih argumenata od prethodnih neurona i nad njima računa najpre linearnu funkciju (koristeći svoje parametre), a potom na nju dodaje i jedan slobodni član. Zatim se na dobijenom rezultatu primenjuje tzv. aktivaciona funkcija. Reultat aktivacione funkcije je ulaz neurona narednih slojeva. Drugim rečima, neuronska mreža konstruiše nove atribute u svojim skrivenim slojevima. Svaki sloj je u stanju da nadograđuje nad prethodnim i tako gradi složenije i složenije atribute. Smatra se da je ova mogućnost konstrukcije novih atributa jedan od glavnih razloga za uspešnost dubokih neuronskih mreža.
Dakle, ako bismo sa $x$ označili vektor ulaznih promenljivih (koje primaju neuroni ulaznog sloja), sa $L$ broj slojeva mreže, sa $W_i$ matricu čija j-ta vrsta predstavlja vektor vrednosti parametara neurona j u sloju i, model neuronske mreže je sledeći: $$h_0 = x$$ $$h_i = g(W_ih_{i-1} + 1); i \in \{1,2,...,L\}$$ gde je $g$ aktivaciona funkcija, a $h_i$ vektor vrednosti koje izlaze iz neurona i-tog sloja.
Možemo videti da neuronske mreže imaju veliki broj parametara, kao i da je za njihovu konstrukciju potrebno zadati i odgovarajuću aktivacionu funkciju, broj slojeva i njihovu veličinu, te je jasno da je za izgradnju kvalitetne mreže potrebno vreme. Struktura mreže, odnosno organizacija neurona po slojevima i aktivaciona funkcija se obično biraju po uzoru na mreže kojima su uspešno rešeni slični problemi onom koji posmatramo. Kada je reč o izboru aktivacione funkcije, u čestoj upotrebi su sledeće neopadajuće funkcije:

slika1.png

GRADIJENTNI SPUST¶

Optimizacija neuronskih mreža, odnosno nalaženje njenih parametara, vrši se slično kao i kod drugih probabilističkih modela - minimizacijom srednje kvadratne greške u slučaju regresije i minimizacijom negativne vrednosti funkcije verodostojnosti u slučaju klasifikacije.
Najjednostavnija i najpoznatija metoda optimizacije prvog reda za diferencijabilne funkcije je gradijentni spust (eng. gradient descent). Gradijentni spust se primenjuje na funkciju gubitaka jer nju želimo da minimizujemo. Dakle cilj je pronaći globalni minimum funkcije gubitaka.
Gradijent ukazuje na pravac najbržeg spusta ka lokalnom ekstremumu. Osnovna ideja je da se, polazeći od neke nasumice odabrane tačke, nizom koraka u pravcu gradijenta dođe vrlo blizu lokalnom ekstremumu. Ako je polazna tačka $x_0$, svaka sledeća se dobija primenom formule: $$x_{k+1} = x_k - \alpha_k \nabla f(x_k) $$ Postavlja se pitanje kako izabrati $\alpha_k$ i koliko iteracija treba izvršiti.
Cilj je da koraci budu dovoljno veliki da bi se što brže došlo do lokalnog minimuma, ali sa druge strane koraci ne smeju biti takvi da preskočimo lokalni minimum. Dakle, poželjno je da se koraci smanjuju sa povećanjem k. Najčešći kriterijumi zaustavljanja su zaustavljanje nakon unapred zadatog broja iteracija, nakon što razlika izmedu susednih koraka postane manja od unapred zadate vrednosti $\epsilon$, ili nakon što razlika izmedu vrednosti funkcije u susednim koracima postane manja od $\epsilon$.
Problem sa gradijentnim spustom je što gradijent u nekom trenutku može da postane premali (tj. da nestane) ili prevelik (tj. da eksplodira), pa se može desiti da ne možemo da stignemo do lokalnog minimuma, odnosno može da se desi da ga preskočimo. Postoje brojna unapređenja ideje gradijentnog spusta, npr. Adamov gradijentni spust. Njegova glavna ideja je da gradijent delimo sa njegovom normom kako bismo izbegli problem nestajućih i eskplodirajućih gradijenata.
Do sada smo sve vreme pričali o nalaženju lokalnog minimuma, iako nam zapravo treba globalni minimum. Ako je funkcija gubitaka konveksna, imaće tačno jedan lokalni minimum i to će ujedno biti i globalni minimum. Šta se dešava ako funkcija gubitaka nije konveksna. Jedan od načina da izbegnemo lokalni minimum koji nije globalni je da optimizacioni problem pokrenemo više puta. Svaki put krećemo od slučajno odabrane tačke, pa ćemo najverovatnije svaki put završiti u različitom lokalnom minimumu. Nadamo se da ćemo jednom završiti u globalnom minimumu, ili bar u dovoljno dobrom lokalnom minimumu. U praksi se pokazalo da broj lokalnih minimuma najčešće nije mnogo velik, pa uz dovoljno pokušaja zaista dobijemo dobar rezultat. Problem sa ovim pristupom je vremenska zahtevnost.

1*bYKQqkRCBSwSnry_4VAAQQ.png

Neuronske mreže se mogu koristiti i u regresiji i u klasifikaciji. U regresiji izlazni sloj sadrži samo jedan neuron koji ne primenjuje aktivacionu funkciju na linearnoj kombinaciji svojih ulaza, već se ta linearna kombinacija uzima kao rezultat modela. U klasifikaciji je postavka problema drugačija i tada se nad linearnim kombinacijama neurona izlaznog sloja (kojih ima onoliko koliko imamo kategorija) primenjuje softmax funkcija, čije vrednosti možemo videti kao verovatnoće pripadnosti svakoj od kategorija: $$softmax(x_1,...,x_k) = \biggl(\frac{e^{x_1}}{\sum_{i=1}^{k}e^{x_i}},...,\frac{e^{x_k}}{\sum_{i=1}^{k}e^{x_i}}\biggr) $$ Rezultat klasifikacije će biti kategorija sa najvećom verovatnoćom.
Obučavanje potpuno povezane neuronske mreže se vrši kroz dve etape koje se naizmenično smenjuju: propagacija unapred (eng. feedforward) i propagacija unazad (eng. backpropagation).

PROPAGACIJA UNAPRED¶

Algoritmom propagacije unapred se računa vrednost funkcije gubitaka na trening skupu, pomoću trenutnih vrednosti parametara modela. Vrednost funkcije gubitaka će biti potrebna za računanje parcijalnih izvoda funkcije gubitaka po vrednostima parameta, a to nam je potrebno za propagaciju unazad. Shema po kojoj se vrši propagacija unapred: $$ X \rightarrow XW_1 \rightarrow H_1 = g(XW_1) \rightarrow H_1W_2 \rightarrow \cdot \cdot \cdot \rightarrow H_{L} \rightarrow H_LW_{L+1} \rightarrow \hat{y} \rightarrow l, $$ gde je $X$ matrica ulaznih atributa, $W_i$ matrica parametara u $i{\text -}$tom sloju, $H_i$ matrica atributa u $i{\text -}$tom sloju, $\hat{y}$ vrednosti predikcije zavisne promenljive na trening skupu, a $l$ funkcija gubitaka na trening skupu.

PROPAGACIJA UNAZAD¶

Cilj propagacije unazad je izračunati gradijent funkcije gubitaka, a zatim ažurirati vrednosti parametara. Algoritam propagacije unazad zasniva se na pravilu izračunavanja parcijalnog izvoda složene funkcije. U svakoj iteraciji se, krećući se po slojevima neuronske mreže od poslednjeg ka prvom, obavljaju sledeći zadaci: 1. proširivanje do tada izračunatog parcijalnog izvoda izvodom aktivacione funkcije u skladu sa pravilom za računanje izvoda složene funkcije, 2. izračunavanje vrednosti gradijenta funkcije gubitaka po vrednostima parametara na tekućem sloju, zarad čega se do tada izračunati parcijalni izvod množi ulazima jedinica koje ti parametri množe, 3. proširivanje do tada izračunatog parcijalnog izvoda izvodom linearne kombinacije po ulazima.
Demonstriraćemo upotrebu potpuno povezanih neuronskih mreža na bazi ručno pisanih brojeva MNIST. MNIST sadrži crno-bele slike dimenzije 28x28 piksela na kojima se nalazi neka od cifara od 0 do 9. Sledeći kod je većinski preuzet od asistenata Anđelke Zečević i Milana Čugurovića sa časova vežbi iz Mašinskog učenja.
In [32]:
import numpy as np
from matplotlib import pyplot as plt
In [33]:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
In [34]:
np.random.seed(10)

MNIST je skup podataka koji sadrži slike rukom pisanih cifara. Slika ima ukupno 70000 od čega se obično 60000 slika koristi za treniranje, a preostalih 10000 za testiranje. Sve slike u skupu podataka su dimenzije 28px x 28px i u crno-belom su modu. Da bismo učitali MNIST skup podataka iskoristićemo podršku Keras biblioteke preko mnist.load_data() funkcije.

In [35]:
(X_train, y_train), (X_test, y_test) = mnist.load_data()

Prva instanca učitanog skupa izgleda ovako

In [36]:
plt.imshow(X_train[0], cmap='gray')
plt.show()

Da bismo slike prosledili kao ulaze mreže, transformisaćemo ih u vektore dužine 28x28=784.

In [37]:
image_size = X_train.shape[1]

X_train = X_train.reshape(X_train.shape[0], image_size*image_size)
X_test = X_test.reshape(X_test.shape[0], image_size*image_size)

Slike ćemo pripremiti tako što ćemo ih normalizovati, tj. svesti vrednosti pojedinačnih piksela na vrednost iz intervala iz [0, 1]. Prethodno ćemo promeniti tip elemenata matrice sa uint8 na float32 kako bi mogli da sačuvamo realne vrednosti.

In [38]:
X_train = X_train.astype('float32')
X_train /= 255
X_test = X_test.astype('float32')
X_test /= 255

Kao što smo videli, vrednosti ciljne promenljive su brojevi od 0 od 9. Uvid u brojnost instanci po klasama možemo dobiti na osnovu sledećeg grafika.

In [39]:
classes = np.unique(y_train)
plt.xlabel('Klasa')
plt.ylabel('Broj instanci')
plt.title ('MNIST skup za treniranje')

plt.xticks(classes)

plt.bar(classes, np.bincount(y_train))
plt.show()

Vrednosti ciljne promenljive ćemo pripremiti tako što ćemo ih transformisati u vektore dužine 10 sa jedinicom na poziciji koja odgovara vrednosti cifre (takozvano one-hot kodiranje). Na primer, broj 3 će biti transformisan u vektor [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] sa jedinicom na poziciji tri.

In [40]:
number_of_classes = classes.shape[0]

y_train = keras.utils.to_categorical(y_train, number_of_classes)
y_test = keras.utils.to_categorical(y_test, number_of_classes)

Ulazni sloj¶

Ulaz u mrežu je slika dimenzije 784 piksela.

Skriveni slojevi¶

Napravićemo mrežu koja ima dva skrivena sloja, jedan dimenzije 128 neurona i drugi dimenzije 64 neurona. Oba sloja kao aktivaciju koristiće relu funkciju.

Izlazni sloj¶

Izlazni sloj mreže ima 10 neurona. Svaki od njih odgovara jednoj od cifara od 0 do 9. Ideja je da vrednosti izlaza predstavljaju verovatnoće pripadanja odgovarajućoj klasi cifara i dodeljujemo onu cifru za koju je verovatnoća najveća. Zato ćemo u poslednjem sloju mreže kao aktivacionu funkciju iskoristiti funkciju mekog maksimuma (engl. softmax).

In [41]:
model = Sequential([
    Input(shape=(image_size*image_size, )), 
    Dense(units=128, activation='relu'), 
    Dense(units=64, activation='relu'),
    Dense(units=number_of_classes, activation='softmax')
])

Model možemo kontrolno prikazati pozivom metode summary.

In [42]:
model.summary()
Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense_6 (Dense)             (None, 128)               100480    
                                                                 
 dense_7 (Dense)             (None, 64)                8256      
                                                                 
 dense_8 (Dense)             (None, 10)                650       
                                                                 
=================================================================
Total params: 109,386
Trainable params: 109,386
Non-trainable params: 0
_________________________________________________________________

Za optimizaciju ćemo koristiti Adamov gradijentni spust.

In [43]:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

Mrežu ćemo trenirati u 30 epoha korišćenjem paketića veličine 128 instanci.

In [44]:
batch_size = 128
epochs = 30

history = model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, verbose=0)

Možemo nacrtati i grafike funkcije gubitka i tačnosti u toku treniranja.

In [45]:
plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.title('Funkcija gubitka')
plt.plot(np.arange(0, epochs), history.history['loss'], label='train')

plt.subplot(1, 2, 2)
plt.title('Tacnost')
plt.plot(np.arange(0, epochs), history.history['accuracy'])

plt.show()

Model ćemo oceniti na skupu za testiranje.

In [46]:
score = model.evaluate(X_test, y_test)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
313/313 [==============================] - 0s 401us/step - loss: 0.1272 - accuracy: 0.9770
Test loss: 0.12718001008033752
Test accuracy: 0.9769999980926514

Dobili smo da je preciznost na test skupu 98%.