Na kraju ovog poglavlja, pokažimo kako razne tehnike i ideje koje smo proučavali tijekom cijelog kolegija možemo upotrijebiti zajedno i njima motivirati jednu od danas najpopularnijih metoda strojnog učenja, a to su neuronske mreže.
Prisjetimo se ponovno problema prepoznavanja znamenki koji nam je poslužio kao primjer primjene problema najmanjih kvadrata.
Na danoj slici razlučivosti \(P \times P\) pixela je prikazana neka dekadska znamenka, a naš zadatak je implementirati metodu koja će za takvu sliku odrediti o kojoj je znamenci riječ. Ranije smo pokazali da svaku takvu sliku možemo reprezentirati kao vektor \(x \in \R^n\), gdje je \(n = P^2\).
Neuronske mreže: univerzalni aproksimatori
Definicija 6.28
Neka su \(W_0, W_1, \ldots, W_{\ell}\) matrice težina, a \(b_0, b_1, \ldots, b_{\ell}\) vektori pristranosti (eng. bias) odgovarajućih dimenzija, te neka su \(\sigma_0, \sigma_1, \ldots, \sigma_{\ell} : \R \to \R\) nelinearne aktivacijske funkcije.
Neuronska mreža \(NN\) preslikava ulazni vektor \(x\) u izlazni vektor \(y\) po sljedećoj formuli:
\[\begin{split}
\begin{align*}
h_1 &= \sigma_0(W_0x + b_0) \\
h_2 &= \sigma_1(W_1 h_1 + b_1) \\
& \vdots \\
h_{\ell} &= \sigma_{\ell-1}(W_{\ell-1} h_{\ell-1} + b_{\ell-1}) \\
NN(x) := y &= \sigma_{\ell}(W_{\ell} h_{\ell} + b_{\ell}).
\end{align*}
\end{split}\]
Aktivacijske funkcije se primjenjuju po komponentama vektora.
Komponente vektora \(x\) još zovemo ulazni sloj, vektora \(y\) izlazni sloj, a vektora \(h_1, \ldots, h_{\ell}\) skriveni slojevi.
Dimenziju vektora u nekom sloju nazivamo brojem neurona u tom sloju. Na slici gore, ulazni sloj ima 3 neurona, a prvi skriveni sloj 4 neurona.
Tipično, kod neuronskih mreža fiksiramo topologiju (broj slojeva, broj neurona po slojevima) i aktivacijske funkcije. Funkciju \(NN\) onda promatramo kao parametriziranu funkciju u kojoj smo slobodni odabrati matrice težina i vektore pristranosti.
Neuronska mreža je, dakle, nelinearna funkcija
\[
NN(x) = \sigma_{\ell}(W_{\ell} (\sigma_{\ell-1}(W_{\ell-1} (\cdots (\sigma_1(W_1 \sigma_0(W_0x + b_0) + b_1)) ) + b_{\ell-1}) + b_{\ell}).
\]
Iz naše perspektive, cilj učenja neuronske mreže je odabrati parametre (matrice težina i vektore pristranosti) tako da \(NN\) bude aproksimacija neke funkcije \(\Psi : \R^n \to \R^m\).
Kao što smo raspravili ranije:
Funkcija \(\Psi\) tipično nije zadana eksplicitnom formulom.
Funkcija \(\Psi\) je tipično zadana nizom parova \((x_i, y_i)\) takvih da je \(y_i = \Psi(x_i)\) za \(i=1, 2, \ldots, N\).
Dokazano je da su neuronske mreže s dovoljno mnogo neurona univerzalni aproksimatori. Naime, vrijedi sljedeći teorem.
Teorem 6.29 (Univerzalna aproksimacija pomoću NN (Cybenko, Hornik))
Neka je \(\sigma_0 : \R \to \R\) neprekidna funkcija koja nije polinom, \(K \subseteq \R^n\) kompaktan skup, te \(\Psi : K \to \R^m\) neprekidna funkcija.
Tada za svaki \(\eps > 0\) postoje \(k \in \N\), te \(W_0 \in \R^{k \times n}\), \(b_0 \in \R^k\), \(W_1 \in \R^{m \times k}\) takvi da je
\[
\sup_{x \in K} \|\Psi(x) - NN(x)\| < \eps,
\]
pri čemu je \(NN(x) = W_1 \sigma_0 (W_0 x + b_0)\).
Drugim riječima, svaka neprekidna funkcija se po volji točno može aproksimirati neuronskom mrežom s jednim skrivenim slojem.
Postoje i druge varijante teorema koje npr. ograničavaju maksimalni broj neurona u slojevima na \(\max\{n+1, m\}\).
Radi jednostavnosti, u ovoj cjelini promatramo samo neuronske mreže s tzv. potpuno povezanim slojevima (eng. fully connected layers ili linear layers). Postoji i cijeli raspon drugih mogućnosti kojima se definira koji neuroni iz nekog sloja su povezani s kojim neuronima iz idućeg (ili čak nekog posve drugog) sloja.
Primjer: Prepoznavanje znamenki
Vratimo se na problem prepoznavanja znamenki i riješimo ga korištenjem neuronske mreže. Ovog puta ćemo koristiti znamenke iz kolekcije MNIST. Svaka takva znamenka je spremljena u slici, odnosno, matrici dimenzija \(28 \times 28\). U kolekciji postoji \(60000\) označenih znamenki za treniranje i još \(10000\) za testiranje. Mi ćemo, radi kraćeg trajanja optimizacije, izdvojiti samo prvih \(4096\) znamenki za treniranje. Istreniranu mrežu ćemo provjeriti na prvih \(16\) testnih primjera.
2025-03-02 19:35:02.809731: I tensorflow/core/util/port.cc:113] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2025-03-02 19:35:02.809943: I external/local_tsl/tsl/cuda/cudart_stub.cc:32] Could not find cuda drivers on your machine, GPU will not be used.
2025-03-02 19:35:02.812324: I external/local_tsl/tsl/cuda/cudart_stub.cc:32] Could not find cuda drivers on your machine, GPU will not be used.
2025-03-02 19:35:02.842293: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-03-02 19:35:03.363203: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
Svaka slika je matrica dimenzija \(28 \times 28\). Razmotajmo svaku tu matricu u vektor duljine \(28^2 = 784\). Spremimo sve podatke za treniranje u jednu matricu kojoj je svaki stupac jedan podatak, tj. svaki stupac te matrice predstavlja jednu sliku.
Dimenzije matrice x_train: (784, 4096)
Dimenzije matrice x_test: (784, 16)
Sada prelazimo na definiranje neuronske mreže. Umjesto da nastojimo aproksimirati funkciju \(\Sigma : \R^{784} \to \R\) koja slici znamenke pridružuje jedan broj (tu znamenku), logičnije je pokušati aproksimirati funkciju \(\Psi : \R^{784} \to \R^{10}\) koja slici znamenke pridružuje vektor \(p \in \R^{10}\).
Ideja je da \(p_i\) daje vjerojatnost da se na slici nalazi znamenka \(i\).
Često je slučaj da su npr. slike znamenki \(3\) i \(8\) relativno slične. Na primjer, malom promjenom slike na kojoj je znamenka \(3\) možemo dobiti sliku na kojoj je znamenka \(8\).
Vidimo da stoga ne možemo očekivati da je funkcija \(\Sigma\) neprekidna.
S druge strane, posve je prirodno očekivati da je funkcija \(\Psi\) neprekidna: za slike na kojima je očito znamenka \(3\) će biti \(p_3 \approx 1\), za one na kojima je očito znamenka \(8\) će biti \(p_8 \approx 1\), a za one za koje nismo sigurni će biti \(p_3 \approx p_8 \approx 0.5\).
Pretvorimo polja y_train
i y_test
u kojima piše koja je znamenka na slici u vektore iz \(\R^{10}\). Na primjer, za znamenku \(8\) ćemo napraviti vektor \((0, 0, 0, 0, 0, 0, 0, 0, 1, 0)\) (prva komponenta vektora odgovara vjerojatnosti da je na slici znamenka \(0\)).
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
Sada ćemo definirati neuronsku mrežu \(NN : \R^{784} \to \R^{10}\) kojom ćemo htjeti postići \(NN(x) \approx \Psi(x)\). Naša neuronska mreža će imati samo jedan skriveni sloj sa \(128\) neurona.
Aktivacijska funkcija nakon svakog sloja će biti tzv. sigmoid:
\[
\sigma(x) = \frac{1}{1 + e^{-x}}.
\]
Dakle, preslikavanje \(NN\) će biti definirano sa
\[
NN(x) = \sigma ( W_2 \cdot \sigma(W_1 \cdot x + b_1) + b_2 ).
\]
Uočite da je slika funkcije \(\sigma\) skup \(\langle 0, 1 \rangle\), pa će u tom skupu biti i sve komponente izlaznog vektora \(NN(x)\).
Parametri koje trebamo odrediti su elementi matrica težina \(W_1 \in \R^{128 \times 784}\) i \(W_2 \in \R^{10 \times 128}\), te elementi vektora pristranosti \(b_1 \in \R^{128}\) i \(b_2 \in \R^{10}\).
Dakle, funkcija \(NN\) ovisi o ukupno \(128 \cdot 784 + 10 \cdot 128 + 128 + 10 = 101770\) parametara čije vrijednosti trebamo odabrati.
Postavimo na početku te vrijednosti (manje-više) slučajno.
Kasnije ćemo minimizirati neku funkciju po svim mogućim izborima tih parametara, pa u skladu s notacijom iz prethodne cjeline o optimizaciji, sve te parametre zajedno označimo sa \(\theta\). Dakle, možemo zamišljati da je \(\theta \in \R^{101770}\).
Provjerimo kako neuronska mreža predviđa koje se znamenke nalaze na testnim slikama prije treniranja. Možda imamo jako puno sreće i uopće ne treba trenirati mrežu i prilagođavati parametre :)
Vektor vjerojatnosti za prvi testni primjer:
[0.47588787 0.62811438 0.56604272 0.53980161 0.52412345 0.50055091
0.68787484 0.44174841 0.44224856 0.52644696]
NN predviđa da je na slici znamenka: 6
Zapravo je na slici znamenka: 7
Vidimo da je mreža vrlo „nesigurna” oko toga koja bi znamenka mogla biti na slici; u vektoru vjerojatnosti na svim indexima piše dosta velik broj, zapravo posve slučajno određen.
Sada prelazimo na treniranje neuronske mreže. Želimo odrediti parametre \(W_1\), \(b_1\), \(W_2\), \(b_2\) tako da za trening podatke x_train
mreža na izlazu daje vektore y_train
. To možemo formulirati kao problem minimizacije funkcije
\[
f(\theta) = \frac{1}{N} \sum_{i = 1}^N \|y_i - NN(\theta; x_i)\|^2,
\]
gdje su \((x_i, y_i)\), \(i=1, \ldots, N\) podaci iz trening seta (\(N=4096\)), a \(\theta\) parametri neuronske mreže. Ovaj problem se čini nevjerojatno težak: moramo pronaći optimalni vektor \(\theta_{\ast}\) iz prostora dimenzije čak \(101770\)! Pokušajmo primijeniti metode optimizacije koje smo naučili u prethodnoj cjelini.
Implementirajmo u Pythonu prvo funkciju \(f\) koju želimo optimizirati. Funkcija osim parametara po kojima treba raditi minimizaciju prima matricu x
čiji su stupci podaci za treniranje, te matricu y
čiji stupci su željeni izlazi (\(y_i = \Psi(x_i)\)).
Funkciju ćemo minimizirati korištenjem metode stohastičkog gradijentnog spusta s grupama. Za tu metodu nam treba gradijent funkcije \(f\) po svim parametrima optimizacije. Drugim riječima, moramo naći formule za sve parcijalne derivacije
\[
\frac{\partial f}{\partial (W_1)_{ij}}, \quad
\frac{\partial f}{\partial (b_1)_{i}}, \quad
\frac{\partial f}{\partial (W_2)_{ij}}, \quad
\frac{\partial f}{\partial (b_2)_{i}},
\]
funkcije \(f\) po svakom pojedinom elementu matrica \(W_1\), \(W_2\) i vektora \(b_1\), \(b_2\). To se može napraviti relativno jednostavno korištenjem pravila za derivaciju kompozicije, no nećemo ulaziti u objašnjenje. Zadatak za naprednije studente: pokušajte sami izvesti formule za ove derivacije ili dokazati da funkcija grad_f
implementirana ispod to radi ispravno; pogledajte i članak o tzv. propagaciji unatrag.
Sada napokon možemo prijeći na optimizaciju. Koristimo potpuno istu metodu stohastičkog gradijentnog spusta s grupama kako smo ju implementirali u prethodnoj cjelini. Jedina razlika je što su nam sada parametri optimizacije razdvojeni u 4 varijable.
Pokrenemo optimizaciju funkcije \(f\). Koristimo korak \(\eta = 1.0\) i grupe veličine \(64\). Stajemo nakon \(500\) epoha. Ova faza može potrajati i nekoliko minuta, ovisno o brzini računala.
Epoha 0 -> f(theta) = 0.9806168532
Epoha 50 -> f(theta) = 0.0449391656
Epoha 100 -> f(theta) = 0.0224473860
Epoha 150 -> f(theta) = 0.0161023600
Epoha 200 -> f(theta) = 0.0132895403
Epoha 250 -> f(theta) = 0.0113354620
Epoha 300 -> f(theta) = 0.0101739517
Epoha 350 -> f(theta) = 0.0091083555
Epoha 400 -> f(theta) = 0.0087355454
Epoha 450 -> f(theta) = 0.0081885188
Vidimo da je vrijednost funkcije cilja osjetno pala. Nacrtajmo i graf koji prikazuje kako se ona mijenjala kroz epohe.
Pogledajmo sada može li istrenirana neuronska mreža predvidjeti znamenke s testnih slika. Uočite da te slike mreža nije vidjela prilikom treniranja.
Vektor vjerojatnosti za prvi testni primjer:
[1.10328292e-06 2.83223053e-09 4.14203754e-04 6.59650019e-04
6.84359529e-08 2.06451566e-05 5.30295194e-11 9.99772730e-01
1.19651806e-05 4.82445683e-06]
NN predviđa da je na slici znamenka: 7
Zapravo je na slici znamenka: 7
Sada su svi elementi vektora koji vrati \(NN(x)\) jako maleni, osim onog koji odgovara znamenci \(7\), a taj je vrlo blizu \(1\)! Neuronska mreža je sasvim sigurna da je na slici znamenka \(7\). Provjerimo što je i s ostalim primjerima iz testnog skupa.
Vidimo da je istrenirana mreža točno prepoznala sve znamenke iz testnog skupa osim jedne (za koju zapravo nije ni jasno je li na njoj \(5\) ili \(6\)). Metodom gradijentnog spusta uspjeli smo riješiti problem optimizacije u prostoru dimenzije \(101770\)!