Mesterséges Neurális Hálózat alapjai – 2. rész

2020/07/07. - írta: sajozsattila

 

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:

 o = \begin{bmatrix} 0.81152104 \\1.77152357 \end{bmatrix}

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:

 E = \frac{(y-o)^2}{2}

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:

  y = \begin{bmatrix} 0 \\1 \end{bmatrix}

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:

 E = \begin{bmatrix} 0.3292832 \\0.29762431 \end{bmatrix}

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:

Visszajátszás

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 v_{11} súlyt:

 \frac{\partial E_{o_1}}{\partial v_{11}}

Vegyük észre, hogy a v_{11} csak egyetlen egy hibához, a o_1 hibájához járul hozzá. Az o_2 teljesen független tőle. Ugye, a négyzetes hibában nem szerepel a v, 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:

 E = 0.5 \cdot (y-f_k(v_p o_r))^2

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 o_1-t az o_1-hez tartozó hibából:

 \frac{ \partial E_{o_p} }{ \partial o_p} = o_p-y_p

Lépjünk egyel visszább, most az f_k-ból a u_p-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 u kisebb, mint 0 és külön eset, ha nagyobb.

 \frac{ \partial f_k(u_p)}{ \partial u_p} = \begin{cases} 1 & \text{ha } u_p > 1 \\ 0 & \text{ha } u_p <=0 \end{cases}

És a következőben már meg is érkeztünk a v-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:

 \frac{ \partial u_p}{ \partial v_{mp}} = \begin{cases} o_r & \text{ha } p >0 \\ 1 & \text{ha } p = 0 \end{cases}

A fentieknek megfelelően a Hiba v_{11}-ra számolt részderiváltja:

 \frac{\partial E_ {o_1} }{\partial v_{11}} = \frac{\partial E_{o_1}}{\partial o_{1}} \cdot \frac{\partial f_k(u_1)}{\partial u_{1}} \cdot \frac{\partial u_1}{\partial v_{11}}

Ami behelyettesítve:

 \frac{\partial E_{o_1}}{\partial v_{11}} = (o_1-y_1 ) \cdot 1 \cdot o_r = 0,1582235

Ez azt fejezi ki, hogy mekkora részben felelős a v_{11} 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:

 v_{11} = v_{11} - \eta \frac{\partial E_{o_1}}{\partial v_{11}}

Ahol:

  • \eta — 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:

 \frac{\partial E}{\partial v_{mp}} = \begin{bmatrix} 0,81152104 & 0,1582235 & 0,40160394 & 0,57630714 \\0,77152357 & 0,15042513 & 0,38181007 & 0,54790266 \end{bmatrix}

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 w_{11} súlyt:

A Rejtett réteg súlyainak hibája

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 w_{11} -nak megfelelő parciális derivált a következő lesz:

 \frac{\partial E}{\partial w_{11}} = \frac{\partial E}{\partial o_{r1}} \cdot \frac{\partial f_r(z_1) }{\partial z_1} \cdot \frac{\partial z_1}{\partial w_{11}}

Ahol:

  •  o_{r1} — 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:

\frac{\partial E}{\partial o_r} = \frac{\partial E_{o_1}}{\partial o_{r1}} + \frac{\partial E_{o_2}}{\partial o_{r1}}

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:

 \frac{\partial E_{o_p}}{\partial o_{rm}} = \frac{\partial E_{o_p}}{\partial o_p} \cdot \frac{ \partial f_r(u_p)}{ \partial u_p} \cdot \frac{ \partial u_p}{ \partial v_{mp}}

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ő:

 \frac{\partial E}{\partial w_{mp}} = \begin{bmatrix} 0,75373805 & 0,0376869 & 0,07537381 \\0,71116975 &  0,03555849 &0,07111697 \\0,54531045 & 0,02726552 & 0,05453104 \end{bmatrix}

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:

 w = \begin{bmatrix} 0,14849252 & 0,24992463 & 0,34984925 \\0,44857766 & 0,54992888 & 0,64985777 \\ 0,74890938 &0,84994547 & 0,94989094 \end{bmatrix}

v = \begin{bmatrix} 0,19837696 & 0,29968355 & 0,39919679 & 0,49884739 \\ 0,59845695 &0,69969915 & 0,79923638 & 0,89890419 \end

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.
Szólj hozzá!

A bejegyzés trackback címe:

https://statisztika.blog.hu/api/trackback/id/tr3715987428

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.

Nincsenek hozzászólások.
süti beállítások módosítása