Autoregresivní neuron

Praktická ukázka toho, jak může vypadat neuronová síť. Přesněji řečeno jeho nejjednodušší část – neuron.

neural net

Jedná se o rekurentní neuronovou jednotku, což znamená, že vstupem do onoho neuronu jsou jeho předchozí hodhoty. Výstup daného neuronu je stav budoucí – čili nám predikuje budoucí vývoj. Výpočet můžeme zařadit do rodiny backpropagation algoritmů, konkrétně jde o gradient descent metodu. To, jak gradient descent funguje, se můžete dozvědět z tohoto excelentního seznamu od 3blue1brown. Více než tisíc slov, řekne kód samotný, takže hurá do toho.

První částí je import knihoven, které budeme potřebovat.

from IPython.display import display,Math
import matplotlib.pyplot as plt
import string 
import numpy as np
from math import factorial as fact   
from scipy.integrate import odeint

Dále definujeme funkci, jejíž hodnoty budeme predikovat. Jeden z možných účelů této úlohy může být například řízení – cokoliv co vás napadne nejen z průmyslu – kdy zvednout závoru, kdy zrychlit rychlost dopravníku, kdy zvýšit přísun vody. Většina těchto systémů má nějakou vlastní dynamiku, proto namísto importování hotového datasetu vytvoříme data vlastní. Vezměme systém, jehož dynamika se dá popsat následující rovnicí.
y^{''}(t)+2 \cdot \eta \cdot \Omega_0 \cdot y^{'}(t)+\Omega_0^2\cdot y(t)= b_0 \cdot u(t)+b_1 \cdot u^{'} (t)
Tímto ani následujícími řádky se zabývat příliš nemusíte, pro samotnou síť nejsou důležité, cílem je pouze vytvořit nějaká smysluplná data, kterými potom neuron budeme krmit.

Funkci převedeme na stavový popis (z jedné rovnic druhého řádu uděláme 2 rovnice prvního řádu).
\begin{bmatrix} &x_1^{'}(t)&\\ &x_2^{'}(t)& \end{bmatrix} = \begin{bmatrix} f_1\big(\textbf{x}(t),u(t)\big)\\ f_2\big(\textbf{x}(t),u(t)\big)\\ \end{bmatrix} = \begin{bmatrix} -\Omega_0^2\cdot x_2(t)- b_0 \cdot u(t)\\ -2 \cdot \eta \cdot \Omega_0 \cdot x_2(t)-b_1\cdot u(t)+x_1(t) \end{bmatrix}
Za podmínky
y(t)=-x_2(t)
Nyní dané výsledky přepíšeme do pythonu. Prvně funkci, která vrací derivace x. Dále definujeme vzorkování a vstup u do systému, a počáteční podmínky.

def fdxdt(xx,t,u,Omega,eta,b0,b1):    # x=[x1 x2 ... xn] vektor hodnot n stavovych velicin
          dx1dt = -Omega0**2*xx[1]-b0*u
          dx2dt = -2*eta*Omega0*xx[1]-b1*u+xx[0]
          return(dx1dt,dx2dt)

dt = .1  #[sec]
t = np.arange(0,50,dt) ; N = len(t)  # delka dat
Npul = int(N/2)  # konverze na integer
u = np.sin(2*np.pi/10*t); u[Npul+1:] = np.sign(u[Npul+1:]) # Vstup do systému
Omega0 = 10;  eta = .1;   b0 = Omega0**2;  b1 = 0
z = np.zeros(N)
x10 = 0 ; x20=0  # poc. podm
x0 = [x10,x20]

Dále definujeme ODE (Ordinary differential equation) solver, který pro každý bod krok po kroku vypočítá derivace a následující hodnotu.

for i in range(0,N-1):
    tt = [t[i],t[i+1]]  # [t1 t2]
    xx = odeint(fdxdt,x0,t,(u[i],Omega0,eta,b0,b1)) #returns x=[ [x1(t1) x2(t1)] [x1(t2) x2(t2)]]
#    x = odeint(fdxdt,x0,tt,args=(u[i],)) # <-- pokud je jen jeden extra argument, musi se tak    
    z[i+1] = -xx[1,1]
    x0 = xx[1,:]  # jako nove poc. podm pro dalsi integraci

Nyní definujeme prázdná pole, do kterých se budou zapisovat hodnoty. Takovou velikost, kolik předchozích stavů chceme vzít v potaz – běžně jednu, dvě… kolik, můžeme zjistit pomocí autokorelační funkce, ale o tom jindy. Pro tento případ ale zvolíme velké číslo, protože funkce je periodická a až na zlom v polovině konstantní v čase. Zvolíme třeba 120, aby pro dané vzorkování byla obsažena celá perioda. Dále zvolíme hodnotu učícího kroku μ. Tato hodnota je velmi důležitá a na kvalitu predikcí má zásadní vliv. Existuje spoustu triků o kterých možná bude řeč příště (Normalizovaný učící krok, nebo Stochastický gradient descent – Adam, Adagrad, moment…).

y = np.zeros(len(t))
w = np.zeros(120)
x = np.zeros(120)
x[0] = 1 
e = np.zeros(len(t))
wall = np.zeros((len(t), 120))
mi = 0.01

Nyní nejdůležitější část – výpočet. Jedná se o lineární neuron, což znamená, že nová hodnota je lineární kombinací vstupních hodnot. Pro všechny, kdo mají rádi obrázky jeden ukážu (toho b si nevšímejte, to je bias a my se na něj budem dívat jen jako na další váhu).

Protože se jedná a rekuretní neuron, danými vstupnímy hodnotami jsou minulé stavy. Lineární kombinaci nejlíp osvětlí vztah
\hat{y} (k)=w_0+w_1\cdot x_1(k)+w_2\cdot x_2(k)
Y se stříškou je výstup neuronu, čili hodnota naší predikce. Popřípadě pro výpočty potřebnější vektorová forma, kde x je sloupcový vektor [1, x1, x2…]. Tento vztah je někdy nazýván dopřednou propagací, neboli forward propagation.
\hat{y} (k)=[w_0 \ w_1 \ w_2]\cdot \bf x
Kdybysme měli lineární kombinaci 2 prvků, můžeme si ji představit jako přímku. Když budeme mít lineární kombinaci tří prvků, můžeme ji nakreslit jako rovinu. Přestože lineární kombinaci více prvků nevím jak nakreslit, abstrakce zůstává – je to rovné – žádné obloučky.

Nyní jde o to, jak určit správné hodnoty vah neuronu w. K tomu slouží právě metoda gradient descent. Prním krokem je vytvoření chybové funkce e. Ta má za úkol ohodnotit to, jak správné jsou dané výsledky. Porovnáváme tedy predikované hodnoty se skutečností, tedy
e = (y - \hat{y})
Nyní použijeme jako hodnotící funkci metodu nejmenších čtverců.
chyba =\frac{1}{2} (y - \hat{y})^2
To má pro mne dva důledky. Zaprvé chyby 5 a -5 dohromady nedávají nulu, ale vždy kladnou hodnotu!!, a za druhé čím větší chyba, tím více mi ovlivní výsledek. To možná znáte z lineární regrese – jako výhodu – ale velký pozor na to, abyste neměli nějakou chybu ve vstupních datech. Pokud máte data, která se pohybují od 100 do 120 a najednou máte za sebou tři nuly, protože vypadl senzor, tak právě kvadrát měřené hodnoty od predikované, bude mít na kvalitu modelu katastrofální vliv.

Chybovou funkci se snažím minimalizovat. Čím menší chyba, tím lepší máme model. Jedniné parametry, které v tomto konkrétním případě můžeme měnit, jsou váhy neuronu w. Pokud derivuji danou chybovou funkci e vahou w – zjistím jak se mi daná chyba mění právě v závislosti na w. K derivování chybové funkce nám pomůže řetězové pravidlo – anglicky chain rule. Toto pravidlo si možná pamatujete slovně, jako derivace složené funkce, rovná se derivace vnější, krát derivace vnitřní. Uvedu konkrétní příklad derivace
(sin(2x))^{'} = cos(2x) \cdot 2
Pokud by se nejednalo o jednotlivý neuron, ale o celou síť, postupovali bychom s  derivací od konce až k naší váze, kterou chceme derivovat. Pro náš případ tedy derivujeme chybovou funkci.
\frac{\partial chyba}{\partial w} = (y - \hat{y}) \cdot \frac{\partial (y - \hat{y})}{\partial w}
Nyný zderivujeme druhou část řetězce. Yrel můžu rovnou škrtnout – w na něj nemá vliv. Výstupní hodnota modelu y je výše uvedenou lineární kombinací vektoru x a w, tedy jejich dot produktem (rozuměj součin prvního s prvním, druhého s druhým…atd). Derivací podle w, je tedy x. Mohli bychom postupovat pro každou váhu zvlášť, ale můžete mi věřit, že to platí pro celý vektor.

Nyní provedeme drobné estetické úpravy a hurá, máme derivaci. Y reálné mínus y predikované označíme opět jako e. 2 se při derivaci vyruší. Pomocí zpětné propagace, neboli backpropagation, jsme se dostali k derivaci chybové funkce přes váhy neuronu. Gradient descent znamená, že hodnota nových vah modelu w, je hodnota w v předchozím kroku, plus naše derivace, krát učící krok μ. Může to být mínus, to záleží, jak si definujete chybovou funkci (lze obráceně). Ta dvojka pouze zdvojnásobí učící krok. My mazaní ten vztah hezky zjednodušíme, dvojku smažem, a zvolíme menší učící krok. Z derivace
\frac{\partial chyba}{\partial w} = e \cdot x
tedy vypočteme hodnotu nové váhy pomocí
w_{n+1} = w_{n} + e \cdot x \cdot \mu
neural net
Uvedu konkrétní příklad na jedné váze. Pokud jsem experimentálně zjistil, že výsledná váha má vyjít 12. V prvním kroku výpočtu, se váha rovná 1 (nebo náhodné malé číslo). Derivace chybové funkce vyjde nulová, pokud při změně w se chyba mého modelu nezmění – což znamená, že buď na výsledek nemá vliv, a nebo již mám hodnotu optimální. Protože já mám hodnotu jinou, vyjde mi kladná derivace – dejme tomu 3. V dalším kroku již je moje váha 1 + 3 krát učící krok μ – dejme tomu 0.01, tedy 1.03. Takto se budeme neustále přibližovat optimální hodnotě. Až ji přesáhne, derivace bude záporná a další krok hodnotu váhy zmenší. Z této úvahy je vidět, že čím větší zvolím učící krok, tím rychleji se přiblížím k správnému výsledku, ale tím méně přesně, protože budu okolo 12 skákat sem a tam. Metaforou gradient descent metody bývá horolezec, který se snaží co nejrychleji slézt z kopce dolů co nejkratší cestou. Pohoří rovná se chybové funkci.

No a nyní konečně k výsledku samotnému – čáry máry kód…

for i in range(len(t)):
    y[i] = np.dot(w, x)
    e[i] = z[i] - y[i]
    x[2:] = x[1:-1]
    if i>0:
        x[1] = z[i-1]
    dydw = x
    dw = mi*e[i]*dydw
    w = w + dw
    wall[i, :] = w

To nejzajímavější na konec, protože kdo má dneska čas číst články do konce…

Vyobrazení výsledků

plt.figure(figsize=(12,7))
plt.subplot(4, 1, 1)
plt.plot(t, y); plt.grid(); plt.xlabel('t')
plt.ylabel("u4 predikované")

plt.subplot(4, 1, 2)
plt.plot(t, z); plt.grid(); plt.xlabel('t')
plt.ylabel("u4 skutečné")

plt.subplot(4, 1, 3)
plt.plot(t, e); plt.grid(); plt.xlabel('t')
plt.ylabel("Chyba")

plt.subplot(4, 1, 4)
plt.plot(t, wall); plt.grid(); plt.xlabel('t')
plt.ylabel("Hodnoty vah")

plt.suptitle("Predikovaná vs. skutečná hodnota, chyba a váhy", fontsize=20)
plt.subplots_adjust(top=0.88)
plt.show()

print('Celková chyba', sum(abs(e)))

No a vypadá to asi takhle no

Napsat komentář

Vaše emailová adresa nebude zveřejněna.