Az előző részben átnéztük, a Neurális Hálózat részeit és a lejátszási szakaszt. Most folytassuk a Hibaszámítással és a Visszajátszással!
Hibaszámítás
Az előző részben, a lejátszás végén kaptunk egy előrejelzést a bemeneti adatok alapján. A konkrét példánk esetében ez így alakult:
Mivel felügyelettel végrehajtott tanulást végzünk ismerjük a bemeneti adatok tényleges eredményét. Semmi akadálya tehát, hogy összevessük ezt az eredményt az MNH előrejelzésével. Nem meglepő, hogy a kettő különbsége lesz az előrejelzés hibája. Több módon számíthatjuk ezt az értéket, az egyetlen megkötés, hogy részekben deriválható legyen. A legegyszerűbb, és amit itt mi is alkalmazunk, a négyzetes hiba:
Ahol:
- E — a Hiba
- y — a valós eredmény
Számítsuk is ki a korábbi példán, legyen mondjuk a tényleges eredmény:
Ennek megfelelően a Hiba:
y = np.array([0,1])
e = 0.5*(y-o)**2
print("Teljes hiba: ", sum(e))
Aminek az eredménye:
Ezzel a lépéssel készek is vagyunk. Most már csak tanulnunk kell a tévedésből, ez lesz a Visszajátszás.
Visszajátszás
Ebben a lépésben frissítjük a Hálózat beállításait annak megfelelően, hogy mekkora részben járultak hozzá az előző lépésben számított Hibához. Ezt a frissítést most visszafelé haladva hajtjuk végre a rendszeren. Vagyis az adatáramlás így alakul:
Hogy miért visszafelé tesszük ezt? Ennek számítástechnikai oka van, amit majd lentebb bemutatok.
De mit is frissítünk konkrétan? Ha valaki odafigyelt, akkor nyilvánvaló a számára, hogy az MNH-nak csupán egyetlen része nem fixen meghatározott: a súlyok. A visszajátszás során ezeket állítjuk. Ugye, így szabályozzuk, hogy az előző rétegben az egyes tulajdonságoknak mekkora szerepük legyen az adott neuron kimeneti értékében.
Most nézzük magát a konkrét számítást! Ez egy minimalizációs probléma: azt szeretnénk, hogy a Hiba minél kisebb legyen. És a Hiba ott a legkisebb, ahol a deriváltja nulla. Legalábbis elméletileg. A gyakorlatban lehetségesek olyan lokális minimumok, amelyek kielégítik ezt a feltételt, és mégsem a legjobb megoldások. Amint azt a nevük is mutatja, csak lokálisan a legjobbak. Ezt mindig tartsuk az észben! A MNH csupán azt garantálja, hogy a lokális minimumot megtaláljuk, de azt nem, hogy ez valóban a legjobb megoldás a lehetséges megoldások végtelenjében.
A kimeneti réteg súlyainak hibája
Térjünk vissza a számításra! Az első lépésben azt nézzük meg, hogy a Kimeneti réteg egyes súlyainak mekkora szerepük volt a Hibában. Ezt az értéket fogjuk gradiensnek nevezni. Hogy ezt kiszámoljuk, deriválnunk kell a Hibát az egyes súlyokra. Nézzünk egy konkrét példát, a súlyt:
Vegyük észre, hogy a csak egyetlen egy hibához, a hibájához járul hozzá. Az teljesen független tőle. Ugye, a négyzetes hibában nem szerepel a , tehát ez a parciális deriválás nem lesz egy lépésben elvégezhető művelet. A láncszabályt használva vissza kell fejtenünk az egyenletet a keresett súlyig. Először is írjuk fel a Hibát, úgy, hogy lássuk a keresett elemet:
Most már látjuk a v súlyokat. Úgyhogy felírhatjuk a parciális deriváltak sorát. Először deriváljuk a -t az -hez tartozó hibából:
Lépjünk egyel visszább, most az -ból a -t. Ez egy kicsit trükkösebb, mivel a ReLU nem deriválható egy az egyben, csak két részben: külön eset ha kisebb, mint 0 és külön eset, ha nagyobb.
És a következőben már meg is érkeztünk a -hoz. Vegyük észre, hogy itt a deriváltnál szintén két lehetőség van: 1 ha az eltolásra deriválunk, és 1 ha bármelyik másra:
A fentieknek megfelelően a Hiba -ra számolt részderiváltja:
Ami behelyettesítve:
Ez azt fejezi ki, hogy mekkora részben felelős a a Hibáért. Mivel csökkenteni akarjuk a hibát, ezért az így kapott eredményt ki kell vonnunk a jelenleg beállított súlyból, hogy a következő alkalommal jobb eredményt kapjunk. Ennek megfelelően a frissített súly így alakul:
Ahol:
- — a tanulási ráta. Erre most nem térek ki, maradjunk annyiban, hogy gradient descent tanulást alkalmazunk.
Gondolom, már mindenki rájött, hogy a lejátszáshoz hasonlóan, ezt se kell egyesével számolni a neuronokra. Rétegenként is elvégezhetjük egyszerű lineáris algebrával:
v_gradient = np.insert(o_r,0,1,axis=0) * ((o-y)*f_k.derivate(u) ).reshape(1,-1).T
Ami ebben a példában:
Most már csak frissíteni kell ezzel az értékkel a kimeneti réteg súlyait az elözöeknek megfelelően. De mielőtt ez megtennénk számoljuk ki a Rejtett réteg súlyainak gradienseit. Ajánlott előszőr minden gradienst kiszámolni és csak utána frissíteni az összes súlyt. Miért? Mindjárt kiderül.
A Rejtett réteg súlyainak hibája
Az alapvető eljárás ugyanaz lesz itt is mint a Kimeneti réteg esetében, azzal az eltéréssel, hogy itt még visszább kell fejteni a láncszabály segítségével. Ami még megbonyolítja az életünket, az az, hogy a Rejtett rétegben nem csak 1 irányból érkezik az adat, hanem a Kimeneti rétegben lévő minden neurontól. Nézzük például a súlyt:
A fenti ábrán pirossal jelöltem, honnan származnak az információk, amelyeket ennek a súlynak a frissítésére használunk. Egyértelmű a különbség a Kimeneti réteghez képest. Ott a neuronokra csak egy irányból érkezett információ, ezzel szemben a Rejtett rétegben az előző rétegben lévő minden neuron befolyásolja, hogy milyen módon frissítjük a súlyt. Ennek megfelelően a -nak megfelelő parciális derivált a következő lesz:
Ahol:
- — a Rejtett réteg 1. neuronjának kimenete
Vegyük észre, hogy menyire hasonlít ez a Kimeneti réteg egyenletére. Számítása is hasonló lesz, csak az első részderivált okozhat némi problémát. Ez a rész arra válaszol, hogy összességében mekkora hibát generált ez a súly a következő rétegen. Vagyis:
Hogyan valósítsuk meg ezt a gyakorlati életben? Úgy, hogy a Visszajátszás során minden előző rétegben kiszámoljuk, ezt az értéket, és csak ezt passzoljuk vissza az előző rétegnek. Ez a magyarázata annak, amiért nem frissítjük a súlyokat egyből, amint kiszámoljuk a hibájukat. Ha így tennénk az összeadás elemei megváltoznának.
Az összeadásban szereplő elemeket meg így általánosíthatjuk:
Pythonban:
elozo_retek_hiba = np.sum(v*((o-y)*f_k.derivate(u)).reshape(1,-1).T, axis=0)
A Kimeneti rétegben megismert módon számíthatjuk ennek a rétegnek a hibáit is:
w_gradient = np.insert(x,0,1,axis=0) * ( elozo_retek_hiba * f_r.derivate(z) ).reshape(1,-1).T
A konkrét számtani példánál maradva az eredmény pedig a következő:
A Súlyok frissítése
Most már, hogy ismerjük az össze súly hibáját, nyugodtan frissíthetjük őket:
eta = 0.002
v = v-eta*v_gradient[:,1:]
v0 = (v0.T - eta*v_gradient[:,0]).T
w = w-eta*w_gradient[:,1:]
w0 = (w0.T - eta*w_gradient[:,0]).T
Aminek megfelelően az új súlyok a következők lesznek:
Befejezés
Ezzel készen is vagyunk. Átnéztük a MNH alapvető lépéseit és elemeit. Napjainkban már persze egy rakás szoftver könyvtár elérhető MNH megvalósítására, és általában azokat használjuk a gyakorlati életben. Viszont, ha anélkül használunk valamit, hogy valóban értenénk a működését, fennáll a veszélye annak, hogy az „mágiává” válik. Sokszor mint valami Delphoi orákulumot kezelik a Neurális Hálózatokat. Ami sajnos magával hozza a rossz felhasználásokat is. Ez a lehetőség maga is megérne egy újabb bejegyzést, de nem részletezem, hanem végszó gyanánt álljon itt két fontos megjegyzés:
- Minden MNH annyit ér, amennyit az adat, amivel tanítjuk.
- Csak a lokális legjobb válasz megtalálása garantált.
A bejegyzés trackback címe:
Kommentek:
A hozzászólások a vonatkozó jogszabályok értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a Felhasználási feltételekben és az adatvédelmi tájékoztatóban.