Potpuno povezane neuronske mreže¶

Preporučujem da pogledate sledeće video-materijale:
Prvi deo
Drugi deo
Treći deo
Četvrti deo

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

Došli smo do oblasti jednog od najznačajnijih pristupa savremenog mašinskog učenja – neuronskih mreža. Neuronske mreže mogu se koristiti i u regresionim i u klasifikacionim problemima. Njihova velika prednost ogleda se u sposobnosti modelovanja složenih nelinearnih zavisnosti između ulaznih i izlaznih podataka. Posebno su uspešne u situacijama kada raspolažemo velikom količinom podataka, naročito kada su ti podaci dati u sirovom obliku.

Ideja neuronskih mreža delimično je inspirisana načinom funkcionisanja bioloških neurona, mada su savremene neuronske mreže pre svega matematički i računski modeli. Zbog svoje fleksibilnosti i velike izražajne moći, našle su primenu u rešavanju brojnih zadataka, kao što su prepoznavanje objekata na slikama, obrada govora, mašinsko prevođenje i igranje video igara. Ipak, njihovo obučavanje može biti računski zahtevno, naročito kod velikih modela i velikih skupova podataka.

Uprkos veoma dobrim performansama, neuronske mreže nisu najbolji izbor u svakoj situaciji. Za uspešnu primenu često je potreban dovoljno veliki skup podataka, kao i odgovarajući računski resursi. Njihova prednost posebno dolazi do izražaja kod podataka sirove reprezentacije, kao što su slike, zvuk ili drugi signali, jer mogu automatski da izdvajaju korisne i složene reprezentacije iz podataka.

U zavisnosti od arhitekture i oblasti primene, razlikuje se više tipova neuronskih mreža. Osnovnu i najjednostavniju varijantu predstavljaju potpuno povezane neuronske mreže (fully connected neural networks).

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

Potpuno povezane neuronske mreže sastoje se od više neurona organizovanih u slojeve, pri čemu svaki neuron prima ulaze od svih neurona iz prethodnog sloja i prosleđuje svoj izlaz neuronima narednog sloja. Ovaj tip mreže sastoji se od ulaznog sloja, skrivenih slojeva i izlaznog sloja. Veličina slojeva, odnosno broj neurona u njima, ne mora biti ista. Mreža koja sadrži više skrivenih slojeva naziva se duboka neuronska mreža (deep neural network).

Neuron poseduje težine pridružene svojim ulazima i jedan slobodni član. Na osnovu ulaznih vrednosti najpre računa ponderisanu sumu uz dodatak slobodnog člana, a zatim se na dobijeni rezultat primenjuje aktivaciona funkcija. Rezultat aktivacione funkcije predstavlja izlaz neurona i koristi se kao ulaz neuronima narednih slojeva.

Drugim rečima, neuronska mreža u svojim skrivenim slojevima konstruiše nove atribute. Svaki naredni sloj nadograđuje reprezentacije dobijene u prethodnom sloju i na taj način gradi sve složenije atribute. Smatra se da je upravo ova sposobnost jedan od glavnih razloga za uspešnost dubokih neuronskih mreža.

Ako sa $x$ označimo vektor ulaznih promenljivih, sa $L$ broj slojeva mreže, sa $W_i$ matricu težina sloja $i$, a sa $b_i$ vektor slobodnih članova u sloju $i$, tada se model potpuno povezane neuronske mreže može zapisati u obliku $$h_0=x,$$ $$h_i=g(W_i h_{i-1}+b_i), \quad i\in\{1,2,\dots,L\},$$ gde je $g$ aktivaciona funkcija, a $h_i$ vektor izlaznih vrednosti neurona iz $i$-tog sloja.
Možemo videti da neuronske mreže često imaju veliki broj parametara. Pored toga, pri njihovoj izgradnji potrebno je odabrati i niz važnih elemenata, kao što su broj slojeva, broj neurona u svakom sloju i aktivacione funkcije. Zbog toga projektovanje kvalitetne mreže zahteva pažljiv izbor arhitekture i odgovarajućih hiperparametara.

Struktura mreže, odnosno raspored neurona po slojevima i izbor aktivacionih funkcija, često se određuje na osnovu iskustva i postojećih rešenja koja su dala dobre rezultate na sličnim problemima. Kada je reč o aktivacionim funkcijama, u praksi se često koriste sledeće funkcije:

slika1.png

GRADIJENTNI SPUST¶

Optimizacija neuronskih mreža svodi se na nalaženje vrednosti parametara koje minimizuju odgovarajuću funkciju gubitka. U regresionim problemima često se koristi srednja kvadratna greška, dok se u klasifikacionim problemima najčešće koristi funkcija gubitka zasnovana na negativnoj log-verodostojnosti, odnosno cross-entropy gubitak.

Jedna od osnovnih optimizacionih metoda prvog reda za diferencijabilne funkcije jeste gradijentni spust (gradient descent). Gradijent funkcije u datoj tački pokazuje pravac njenog najbržeg rasta, pa se pri minimizaciji krećemo u pravcu negativnog gradijenta. Ako sa $\theta_k$ označimo vrednost parametara u $k$-tom koraku, tada se naredna iteracija dobija formulom $$\theta_{k+1}=\theta_k-\alpha_k \nabla f(\theta_k),$$ gde je $\alpha_k$ korak učenja (learning rate), a $f$ funkcija gubitka.

Izbor veličine koraka ima veliki značaj. Ako je korak prevelik, algoritam može preskakati oblasti sa manjim vrednostima funkcije gubitka ili čak divergovati. Ako je korak premali, konvergencija može biti veoma spora. U praksi se zato pažljivo bira vrednost parametra learning rate, a često se koriste i adaptivne metode koje ga efektivno prilagođavaju tokom obučavanja. Kao kriterijumi zaustavljanja mogu se koristiti unapred zadat broj iteracija ili epoha, mala promena vrednosti funkcije gubitka, ili izostanak poboljšanja na validacionom skupu.

Kod obučavanja dubokih neuronskih mreža mogu se javiti problemi nestajućih i eksplodirajućih gradijenata, naročito kod dubljih arhitektura. U takvim situacijama gradijenti mogu postati suviše mali, pa se parametri gotovo više ne menjaju, ili suviše veliki, što dovodi do nestabilnog obučavanja. Zbog toga se u praksi koriste različita unapređenja osnovnog gradijentnog spusta, kao što su Momentum, RMSProp i Adam. Adam je naročito popularan jer kombinuje ideju momenta i adaptivnog skaliranja koraka za različite parametre.

Za razliku od konveksnih optimizacionih problema, funkcija gubitka kod neuronskih mreža najčešće nije konveksna, pa zato ne možemo očekivati nalaženje globalnog minimuma jednostavnim optimizacionim postupkom. Ipak, u praksi se često stiče utisak da broj lokalnih minimuma koji daju zaista loš rezultat nije veliki, dok mnoga dobijena rešenja imaju slične vrednosti funkcije gubitka i slične performanse modela. Zbog toga je u praksi često dovoljno pronaći dovoljno dobru tačku u kojoj model daje dobre performanse na podacima. Na konačan rezultat utiču inicijalizacija parametara, izbor arhitekture, optimizacioni algoritam i podešavanje hiperparametara.

1*bYKQqkRCBSwSnry_4VAAQQ.png

Neuronske mreže mogu se koristiti i u regresiji i u klasifikaciji. U regresionim problemima izlazni sloj najčešće sadrži jedan neuron čiji se izlaz dobija primenom identičke funkcije, pa se linearna kombinacija njegovih ulaza direktno uzima kao rezultat modela.

Kod višeklasnih klasifikacionih problema, izlazni sloj obično sadrži onoliko neurona koliko ima kategorija. Na izlaze tih neurona pre aktivacije, odnosno na dobijene skorove, primenjuje se softmax funkcija: $$\mathrm{softmax}(x_1,\dots,x_k)=\left(\frac{e^{x_1}}{\sum_{i=1}^{k} e^{x_i}},\dots,\frac{e^{x_k}}{\sum_{i=1}^{k} e^{x_i}}\right).$$ Dobijene vrednosti interpretiraju se kao verovatnoće pripadnosti odgovarajućim kategorijama, a rezultat klasifikacije određuje se izborom kategorije 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¶

Algoritam propagacije unapred (forward propagation) koristi trenutne vrednosti parametara modela kako bi se, polazeći od ulaznih podataka, redom izračunali izlazi svih slojeva mreže, zatim predikcije modela i na kraju vrednost funkcije gubitka. Pored same vrednosti funkcije gubitka, u ovom postupku dobijaju se i međurezultati po slojevima, koji su potrebni u algoritmu propagacije unazad.

Shema propagacije unapred može se zapisati u obliku $$ X \rightarrow Z_1=XW_1+b_1 \rightarrow H_1=g(Z_1) \rightarrow Z_2=H_1W_2+b_2 \rightarrow H_2=g(Z_2) \rightarrow \cdots \rightarrow H_L \rightarrow \hat{y} \rightarrow \mathcal{L}, $$ gde je $X$ matrica ulaznih podataka, $W_i$ matrica težina u $i$-tom sloju, $b_i$ vektor slobodnih članova u $i$-tom sloju, $Z_i$ matrica vrednosti pre primene aktivacione funkcije, $H_i$ matrica izlaza iz $i$-tog sloja, $\hat{y}$ vektor ili matrica predikcija modela, a $\mathcal{L}$ vrednost funkcije gubitka na trening skupu.

PROPAGACIJA UNAZAD¶

Cilj propagacije unazad (backpropagation) jeste izračunavanje gradijenta funkcije gubitka u odnosu na parametre mreže, kako bi oni mogli da se ažuriraju optimizacionim algoritmom. Ovaj postupak zasniva se na lančanom pravilu za izračunavanje izvoda složene funkcije.

Polazeći od izlaznog sloja i krećući se unazad ka ulaznom sloju, u svakom sloju se obavljaju sledeći koraci:

  1. izračunava se kako promena izlaza tog sloja utiče na vrednost funkcije gubitka, pri čemu se koristi izvod aktivacione funkcije,
  2. na osnovu toga određuje se gradijent funkcije gubitka u odnosu na parametre tog sloja,
  3. zatim se ova informacija prenosi na prethodni sloj, kako bi se i za njegove parametre mogao izračunati odgovarajući gradijent.

Na taj način se, sloj po sloj, efikasno izračunavaju gradijenti po svim parametrima mreže.

Primenu potpuno povezanih neuronskih mreža ilustrovaćemo na skupu podataka MNIST, koji se sastoji od crno-belih slika ručno pisanih cifara dimenzije 28×28 piksela. Svaka slika odgovara jednoj od deset mogućih klasa, označenih ciframa od 0 do 9.

Sledeći kod je u velikoj meri preuzet i prilagođen na osnovu materijala sa vežbi iz Mašinskog učenja koje su držali Anđelka Zečević i Milan Čugurović.

In [ ]:
import numpy as np
from matplotlib import pyplot as plt
In [ ]:
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 [ ]:
np.random.seed(10)
MNIST je skup podataka koji sadrži slike ručno pisanih cifara. Ukupno sadrži 70000 slika, od čega se uobičajeno 60000 koristi za obučavanje modela, a preostalih 10000 za testiranje. Sve slike su dimenzije 28 × 28 piksela i zapisane su u nijansama sive.

Za učitavanje MNIST skupa podataka iskoristićemo podršku biblioteke Keras, pomoću funkcije mnist.load_data().

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

Prva instanca učitanog skupa izgleda ovako

In [ ]:
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 [ ]:
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 [ ]:
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 [ ]:
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 [ ]:
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¶

Formiraćemo neuronsku mrežu sa dva skrivena sloja. Prvi skriveni sloj ima 128 neurona, dok drugi ima 64 neurona. U oba sloja koristi se ReLU aktivaciona funkcija.

Izlazni sloj¶

Izlazni sloj mreže sadrži 10 neurona, pri čemu svaki neuron odgovara jednoj od cifara od 0 do 9. Nakon primene softmax funkcije, dobijene vrednosti mogu se interpretirati kao verovatnoće pripadnosti odgovarajućim klasama. Kao rezultat klasifikacije bira se cifra kojoj odgovara najveća od tih verovatnoća. Zbog toga se u izlaznom sloju koristi aktivaciona funkcija mekog maksimuma (softmax).

In [ ]:
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')
])

Strukturu modela možemo prikazati pozivom metode summary().

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

Za optimizaciju ćemo koristiti Adamov gradijentni spust.

In [ ]:
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 [ ]:
batch_size = 128
epochs = 30

history = model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, verbose=0)
2026-03-31 23:13:32.093613: W tensorflow/core/platform/profile_utils/cpu_utils.cc:128] Failed to get CPU frequency: 0 Hz

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

In [ ]:
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 [ ]:
score = model.evaluate(X_test, y_test)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
313/313 [==============================] - 0s 411us/step - loss: 0.1142 - accuracy: 0.9807
Test loss: 0.11418993771076202
Test accuracy: 0.9807000160217285