Napjaink igen felkapott elnevezéseinek egyike minden bizonnyal a Mesterséges Neurális Hálózat (MNH), avagy Artificial Neural Networks (ANN). Ami jórészt annak köszönhető, hogy e technológiának a mindennapi életben való alkalmazásai az emberek egy jelentős részét lenyűgözi. Az MNH az alapja sok arcfelismerő, képfeldolgozó megoldásnak, a Google-fordítónak vagy a Facebook-hirdetés optimalizációjának, hogy csak néhányat említsek. Mai posztomban a legegyszerűbb MNH-t, az un. Feedforward-típust törekszem egy kicsit részletesében bemutatni.
Kezdjük a neurális hálózat részeivel! Ehhez rajzoltam egy diagramot:

Elsőre talán bonyolultnak látszik, de valójában nagyon egyszerű. Ha jól megnézzük a fenti ábrát, láthatjuk, hogy csupán két fajta objektumból épül fel:
- a körök — az úgynevezett neuronok. Ezek végzik a számítást.
- a nyilak — a neuronok közötti kapcsolatokat jelölik. A fenti ábrán minden neuron kapcsolódik minden neuronhoz a következő rétegben. Ezt az elrendezést „teljesen csatolt” rendszernek nevezzük. (Van olyan eset is, amikor ez nem így van. Ilyenkor „részlegesen csatolt” a rendszer, de a mi példánk nem ilyen.)
Az alapelemek nem véletlenszerűen helyezkednek el a térben, hanem rétegekbe rendezve.
Három rétegtípusról beszélhetünk:
- Bemeneti réteg — gondolom, senkit nem lepek meg, ha azt mondom, hogy a megfigyelt adatok itt érkeznek a hálózatba. Ebből a rétegből jobbára csak egy van (a kivételekkel most nem foglalkozunk). A rétegben elhelyezkedő neuronok mind egy-egy tulajdonságot, megfigyelési dimenziót jelölnek.
- Rejtett réteg — a számítások helye. Ennek a rétegnek a célja modellezni a bemeneti rétegben feltüntetett tulajdonságok kapcsolatát. (A fenti példában egy rejtett réteg van, de voltaképpen akármennyi követheti egymást, akár elhagyhatjuk is ezt a réteget.)
- Kimeneti réteg — beszédes neve szerint ez az utolsó réteg, az MNH eredményének helye.
Most nézzük a többi kifejezést az ábrán:
— az n tulajdonság megfigyelt értéke
— a rejtett réteg neuronjára jellemző eltolás
— a bemeneti és a rejtett réteg közötti kapcsolatokhoz rendelt súlyok. Az indexek a rétegeknek megfelelő neuronokat jelölik. Pl:
a bementi réteg 1. neuronját köti össze a rejtett réteg 2. neuronjával.
— a rejtett réteg aktivációs függvénye. Erről később lesz szó. Most csak azt vegyük észre, hogy a réteg összes neuronján megegyezik.
— az aktivációs függvény – az előző réteg kimenetéből és a súlyokból számolt – bemenete.
— a kimeneti réteg neuronjára jellemző eltolás.
— a rejtett és a kimeneti réteg közötti kapcsolatok súlyai. Az indexelés ugyanazon az elven működik, mint a
-nél.
— a kimeneti réteg aktivációs függvénye, hasonlóan a rejtett réteghez, a rétegen belül minden neuronon ugyanaz.
— a kimeneti réteg aktivációs függvényének bemenete.
— a hálózat végső eredménye.
Most, hogy ismerjük a MNH részeit, nézzük, hogyan mozog az adat a rendszeren belül. Ehhez idézzük fel, hogy minden felügyelettel végrehajtott tanulás lényegében három részből áll:
- Alkalmazzuk a már meglévő ismereteinket egy megfigyelésen, azaz az eddigi ismereteink alapján kiszámoljuk, hogy mi lehet a megfigyelés eredménye.
- Összevetjük az előző lépésben kiszámított eredményt a tényleges eredménnyel. Ha a kettő nem egyezik meg, akkor kalkuláljuk az eltérés mennyiségét és irányát. Lényegében Loss kalkulációt végzünk.
- Korrigáljuk az ismereteinket, annak megfelelően, hogy mekkora és milyen az előző pontban kiszámított hiba.
A Feedforward-típusú MNH első lépését nevezzük Lejátszásnak (Forward propagation), a másodikat a Hibaszámításnak, míg az utolsót a Visszajátszásnak (Back propagation). Gondolom, senkit se lepek meg, ha elárulom, hogy a lejátszás során az adatok a Bemeneti réteg felől a Kimeneti réteg felé haladnak. Az első ábrám ezt a lépést szemlélteti. Ezzel szemben a Visszajátszás során az ellenkező irányba haladunk:

Most, hogy ismerjük a hálózat alapvető felépítését, nézzük meg, mi a konkrét kivitelezése az egyes lépéseknek.
Lejátszás — Előrejelzés
Mint fentebb említettem, az összes számítás a neuronokban történik. De mit is csinál egy neuron? Lényegében hipersíkokat állít elő. Magát a konkrét számítást, amit végez, így lehetne megjeleníteni:

A fenti ábra a j rejtett rétegbeli neuron működését szemlélteti, de minden, nem a Bemeneti rétegben elhelyezkedő neuron ugyanígy működik. Első lépésben összeadja a bemeneti adatok súlyozott értekét. Ez lesz a . Vegyük észre, hogy van egy bemeneti érték, ami mindig 1, ez az eltolás. Vagyis:
(1)
Majd ezen a -én alkalmazza a
aktivációs függvényt. És ezzel kész is van. A függvény kimeneti értéke már megy is a neuron kimenetére. Miért kell az aktivációs függvény? Azért használjuk, hogy a lineárisan nem elválasztható feladatokat is meg tudjunk oldani.
Hogy ne csak elméletben legyünk képesek megoldani ezt a feladatot, számszerűsítsük a fenti modellt, valahogy így:

Mivel elég kusza lett volna, ha minden számot feltűntetek a fenti ábrán, csak néhány értéket ábrázoltam. A teljes kezdeti állapot legyen a következő:
(2)
(3)
(4)
Nézzük az első neuront a Rejtett rétegben. Ennek a értéke az (1) alapján:
(5)
Aki egy kicsit járatos a lineáris algebrában, az már biztosan rájött, hogy nem kell minden neuront külön-külön kiszámítanunk, hanem a számítást rétegenként egyszerre elvégezhetjük:
import numpy as np # megfigyelés x = np.array([.05, .1]) # rejtet réteg súlyai w = np.array([[.25,.35],[.55, .65],[.85, .95]]) # rejtett réteg eltolás w0 = np.array([[.15],[.45],[.75]]) # rejtett rétek z érték z = (np.concatenate((w0, w), axis=1))@np.insert(x,0,1,axis=0)
Aminek az eredménye:
(6)
A következő lépésben át kell engednünk ezt az eredményt a rejtett réteg aktivációs függvényén. Ez a 3. ábra szerint, egy függvény. Vagyis:
(7)
Ugye, ez az első neuron esetén:
(8)
A teljes rejtett rétegre:
class Tanh(): def run(self, x): return 1-(2/(np.exp(2*x)+1)) def derivate(self, x): return 4*np.exp(2*x)/(np.exp(2*x)+1)**2 # rejtett réteg aktivációs függvény f_r = Tanh() # rejtett réteg kimenet o_r = f_r.run(z)
Az eredmény pedig:
(9)
A Rejtett réteget be is fejeztük. Most ismételjük meg ugyanezeket a lépéseket a Kimeneti rétegben. A lépések ugyanezek, az egyetlen különbség, hogy az aktivációs függvény ReLU lesz:
(8)
Ne is húzzuk az időt, nézzük mi a Kimeneti réteg eredménye:
class ReLU(): def run(self, x): return np.maximum(0,x) def derivate(self, x): x[x0] = 1 return x # kimeneti réteg súlyai v = np.array([[0.3,0.4,0.5],[0.7,0.8,0.9]]) # kimeneti rétek eltolása v0 = np.array([[.2],[.6]]) # kimeneti réteg u = (np.concatenate((v0, v), axis=1))@np.insert(o_r,0,1,axis=0) f_k = ReLU() o = f_k.run(u)
Aminek az eredményei:
(10)
Mind a két szám pozitív, így nem meglepő módon a ReLU aktivációs függvény nem változtat rajtuk:
(11)
Végére is értünk a Lejátszási szakasznak.
A következő lépésben meg kell néznünk, hogy mekkora hibát követtünk el, és a beállításokat ennek megfelelően kell frissítenünk. Ezt fogjuk a következő blogbejegyzésben megtárgyalni.
Irodalom
- Assaad Moawad: Neural networks and back-propagation explained in a simple way
- Matt Mazur: A Step by Step Backpropagation Example
- John McGonagle, George Shaikouski, Christopher Williams, Andrew Hsu, Jimin Khim: Backpropagation
“A Mesterséges Neurális Hálózat alapjai – 1. rész” bejegyzéshez 4 hozzászólás