Een klein neuraal network

Machine learning, data science, artificial intelligence en Big Data zijn al enkele jaren veel gebruikte woorden in de media en bij bedrijven. Tenminste, zo komt dat op mij over. Hoe veel vaker men er nu over bericht ten opzichte van enkele jaren terug, bijvoorbeeld in het jaar 2000, is mij niet bekend.

Wat de laatste jaren is opgevallen door sites zoals Stack Overflow is dat Python populairder is geworden. Deze programmeertaal is tevens een veel gebruikte taal onder data scientists. Data scientists gebruiken soms neurale netwerken in hun modellen en ik zal in deze post proberen uit te leggen hoe zoiets ongeveer werkt aan de hand van een heel simpel model. In Python.

Ten eerste raad ik aan om, als je nog geen Python installatie hebt, anaconda te gebruiken. Het maakt het gebruik van Python iets gemakkelijker omdat in anaconda een stel handige extra's komt zoals Jupyter notebook en Numpy. Het gebruiken van Jupyter notebook heeft als voordeel dat je heel makkelijk stukjes code kan uitvoeren en het ziet er presentabel uit. Zoek vooral op het internet hoe je dat moet installeren en hoe je Jupyter moet starten en gebruiken, zelf uitzoeken helpt heel erg bij het onthouden hoe je zoiets doet.

We gaan in Python een model bouwen dat leert om een correlatie te vinden tussen de input en de output. De input is wat we het model aan data geven en de output is wat het model moet gaan leren te voorspellen. De input geven we in 1 keer aan het model in de vorm van een matrix. De output is een array met cijfers. We zouden ook steeds een enkele input kunnen geven zodat we het model kunnen trainen (trainen kan ook gelezen worden als leren) met de zogenaamde stochastic gradient descent maar, in dit geval gebruiken we slechts gradient descent.

In grote lijnen ga ik in deze post laten zien hoe je 1, een input matrix en een output array kunt maken. 2, hoe je gewichten aan elke input kunt hangen. 3, hoe je een sigmoid functie moet maken. 4, hoe je een derivaat kunt herleiden en 5, hoe je de gewichten kunt updaten met een beter gewicht waardoor je model beter wordt in iedere iteratie.

Voordat dit allemaal begint, moet ik melden dat een neuraal netwerk (of neural network) vrijwel niets te maken heeft met hoe je hersenen in het echt werken. Wetenschappers weten nog steeds niet hoe je hersenen werken maar, de achtergrond van hoe de wiskundige interpretatie is ontstaan komt wel uit hun onderzoek. Men verondersteld dat een hersencel zelf geen idee heeft waar het zich bevindt in de hersenen en wat het voor doel heeft. Het enige wat de hersencel doet is zelf een signaal uitsturen als het een signaal van een andere hersencel krijgt. Wellicht gebeurt dit pas bij een bepaald voltage (hoog signaal) en het is maar de vraag of zijn signaal ergens wordt gehoord. Wat de wiskundige interpretatie als extra bewijs gebruikt is het voorbeeld van een kind dat door schade en schande wijs wordt. Het stoot z'n hoofd tegen een steen, dat doet pijn - of geeft een error - en leert daardoor beter op te letten. Nogmaals, dit heeft niets te maken met hoe je hersenen in het echt werken, het is slechts een benadering van hoe wetenschappers denken dat het werkt. Op zich zelf lijkt het ook wel enigszins logisch te zijn, zoals we gaan zien.

We gaan een netwerk maken dat de volgende tabel gaat proberen te benaderen met als input.
[1, 0, 0
0, 1, 1
1, 1, 1
1, 0, 1]

De rechte haakjes laten zien dat het om een matrix gaat, de komma's laten het verschil zien tussen de individuele cijfers (elke rij heeft 2 cijfers), en de enters laten een nieuwe rij zien. Het gaat dus om binaire data: of een 1 of een 0. In python ziet dat er zo uit:
import numpy as np
inputdata = np.array([[1,0,0], [0,1,1], [1,1,1],[1,0,1]])
print(inputdata)

[[1 0 0]
[0 1 1]
[1 1 1]
[1 0 1]]

Het is een regel om numpy te importeren als np, vrijwel alle Python gebruikers doen het op deze manier en het is raadzaam om je aan deze ongeschreven regel te houden.

Een volgende stap is om de outputdata te definiëren in de vorm van een array, die we als een vector kunnen gebruiken:
[0, 1, 1, 0]. Het is heel erg raadzaam, vooral in data science, om lineaire algebra termen en de het gebruik ervan te begrijpen door of het te (be)studeren of door er over te lezen op het internet/ in de boekenkast van een econometrist of een wiskundige. Lineaire algebra is naast calculus en statistiek namelijk het fundament onder machine learning. De outputdata array creëren we op de volgende manier, dan is namelijk de array te gebruiken als een vector:
outputdata = np.array([[0,1,1,0]]).T
print(outputdata)

[[0]
[1]
[1]
[0]]

 De T staat voor 'transpose', een term om aan te geven dat je je matrix (of vector) omdraait. Normaal zou er staan [0,1,1,0] maar, met de T staat er:
[0,
1,
1,
0]
Willen we vermenigvuldigen met matrixen en vectoren, en dat willen we in dit geval, dan is de juiste notatie ervan van levensbelang.

Een matrix m,n (4,3) kan vermenigvuldigd worden met een vector (3,1) zodat het een nieuwe matrix (of  vector in dat begrip) van 4,1 kan worden gemaakt. Precies zoals onze output dus! Deze (3,1) vector noemen we de weights in een neural network. Daar komen we later in deze post op terug.

We willen dat onze benadering rekening houdt met non-lineariteit. Niet alles (lees: vrijwel niets) is lineair in de echte wereld. In data science doen we dat traditioneel door een sigmoid functie te gebruiken. De sigmoid functie maakt gebruik van een exponentiële functie voor natuurlijke groei en numpy kan dit evenaren door exp() te gebruiken. Een andere toepassing van de sigmoid functie is om getallen altijd tussen een 0 en een 1 te houden.

Het doel van dit neurale netwerk is om het een functie te leren die de correlatie tussen de tweede kolom van de input en de output gaat begrijpen. We zeggen dat als de tweede kolom van de input een waarde 1 heeft, moet de output die waarde overnemen en dus ook 1 zijn. Als de input op de de tweede kolom 0 is, is de output ook 0.  We gaan ons model van z'n fouten laten leren door het z'n hoofd te laten stoten, net zolang tot het heeft begrepen wat we willen zien.

Een van de eerste formules die noodzakelijk is hiervoor, is een activatie formule in de vorm van de sigmoid functie:
Onze formule voor de sigmoid functie is 1/1+(exp^-x) . Deze functie is tamelijk tot zeer belangrijk in het vakgebied van data science.

De sigmoid functie is dus een activatiefunctie maar, wat gaat het activeren?
We gaan de input vermenigvuldigen met een gewicht die het aan de input geeft. Dit kan elk getal zijn en zowel negatief als positief zijn. Bijvoorbeeld, er is een gewicht van -1.3, eentje van 0.9 en een gewicht van 1.4 (ik gebruik een punt (.) om de decimalen te onderscheiden). Die gewichten stop ik in een vector en de input matrix wordt vervolgens vermenigvuldigd met de gewichten-vector: [inputdata]*[gewichten_vector]. Vervolgens ga ik elk element in de nieuwe vector activeren met de sigmoid functie. De totale functie ziet er dan dus zo uit: sigmoid([inputdata]*[gewichten_vector]). In een neuraal netwerk heet dit de hidden layer, de verborgen laag. Eigenlijk is dat omdat het bij hele grote netwerken een beetje onduidelijk wordt wat er nou precies gebeurd is in een netwerk tijdens en na een training. De laag is namelijk optisch gezien helemaal niet 'hidden', je hebt hem er zelf neergezet 

Eerst moeten we natuurlijk een waarde genereren voor de gewichten_vector. Eerder in deze post hebben we laten zien dat een (4,3) matrix vermenigvuldigd kan worden met een (3,1) vector waardoor het product van deze vermenigvuldiging een (4,1) vector wordt. Deze vorm komt overeen met onze outputdata, die is namelijk ook (4,1). De gewichten_vector wordt random geïnitieerd en dat doen we met een random nummer generator. Het gebruiken van een random seed wordt aangeraden maar dat laten we in dit voorbeeld even zitten.
gewichten_vector = 2*np.random.random((3,1)) - 1
print(gewichten_vector)

[[-0.39533485]
[-0.70648822]
[-0.81532281]]

Nu de code voor de sigmoid functie:
def sigmoid(x):
    return(1/(1+np.exp(-x)))
En vervolgens kunnen we alles bij elkaar stoppen voor de eerste stap van het neurale netwerk, de vermenigvuldiging van input, gewichten_vector en de sigmoid activatie.
verborgen_laag = sigmoid(np.dot(inputdata,gewichten_vector))
print(verborgen_laag)

[[ 0.40243371]
[ 0.17919499]
[ 0.12818018]
[ 0.22958471]]

Ervaren mensen zullen opmerken dat je ook een bias moet toevoegen voor de volledigheid. Dat doen we echter niet in dit voorbeeld.

Wat we hierboven hebben gedefinieerd is wat men noemt 'Forward propagation' een term die zegt dat we van links naar rechts een berekening hebben uitgevoerd. Feitelijk hebben we hiermee de eerste voorspelling van het model gemaakt. Als je de vergelijking wil maken met een hersencel: het heeft zelf maar wat output geleverd op basis van z'n input. Z'n eerste 'educated guess' laten we maar zeggen. Dit is natuurlijk bij lange na niet correct; er ziet een behoorlijke error in die we kunnen berekenen. Die berekening is heel simpel namelijk, haal wat je hebt berekend van de eigenlijke waarde af:

error = outputdata - verborgen_laag
error = outputdata - verborgen_laag
print(error)

[[-0.40243371]
[ 0.82080501]
[ 0.87181982]
[-0.22958471]]

We moeten ons model hieruit, van deze error, laten leren. Dat doen we door een derivaat te nemen van de verborgen_laag, van hetgeen we eigenlijk zonet hebben voorspeld, en dat te vermenigvuldigen met de error die die voorspelling had ten opzichte van de eigenlijk voorspelling. Op deze manier leg je een verband met wat er is voorspeld in de verborgen_laag en met wat de voorspelling eigenlijk had moeten zijn om een zo min mogelijke error te krijgen. De uitkomst van deze vermenigvuldiging zal dan gebruikt worden om nieuwe gewichten te genereren.  

De delta is de error maal het derivaat van onze voorspelling, de verborgen_laag. Hoe achterhaal je het derivaat? Zonder al te veel uitleg te geven gebruiken we in dit voorbeeld de ' Error weighted derivative' en dit doen we zo:

derivaat = x*(1-x)

en dat resulteert dus in:

delta = error * derivaat(verborgen_laag)
#definitie van derivaat:
def derivative(x):
    return(x*(1-x))

#bereken de delta:
delta = error * derivative(verborgen_laag)
print(delta)

[[-0.09677759]
[ 0.1207274 ]
[ 0.09742588]
[-0.04060793]]

Nu we de delta's hebben, kunnen we de nieuwe gewichten gaan bepalen. Dat doen we door de input data te vermenigvuldigen met de delta's.

gewichten_vector = np.dot(inputdata.T,delta)
print(gewichten_vector)

[[-0.03995963]
[ 0.21815329]
[ 0.17754536]]

Nu hebben we een nieuwe benadering van de werkelijkheid in onze gewichten opgeslagen en dan is het tijd om opnieuw forward propagation toe te passen. Dus weer input maal de gewichten en vervolgens te activeren met de sigmoid functie. Vervolgens kijken we opnieuw naar hoever we naast de werkelijkheid zitten en gebruiken we deze error om opnieuw onze gewichten te updaten. Enzovoorts, enzovoorts, enzovoorts. 

Deze enzovoorts kunnen we vangen in een iteratie. Afhankelijk van hoeveel we willen itereren duurt de training lang of kort en wordt de voorspelling beter of minder goed.

Het iteratieve model ziet er dan zo uit:
for iteration in range(1000):
    
    verborgen_laag = sigmoid(np.dot(inputdata,gewichten_vector))
    error = outputdata - verborgen_laag
    delta = error * derivative(verborgen_laag)
    gewichten_vector += np.dot(inputdata.T,delta)
print(verborgen_laag)

[[ 0.02284794]
[ 0.99918555]
[ 0.96631402]
[ 0.03399994]]

 We zien na 1000 iteraties dat ons model de werkelijkheid begint te benaderen. We zochten een output van 0, 1, 1, 0 en ons model print 0.02, 0.99, 0.97, 0.03. Dat is aardig in de buurt!

Aangezien ons model altijd de werkelijkheid benadert, moet je niet verwachten dat het ooit 100% goed voorspelt. Echter, met dit kleine neurale netwerk kan je met je menselijke kijk op zaken wel zeggen dat het model na een tijdje goed genoeg presteert.

Hieronder volgt de code gebruikt in deze post in z'n totaliteit. Slechts 14 lijnen aan code :)
import numpy as np
inputdata = np.array([[1,0,0], [0,1,1], [1,1,1],[1,0,1]])
outputdata = np.array([[0,1,1,0]]).T
gewichten_vector = 2*np.random.random((3,1)) - 1
def sigmoid(x):
    return(1/(1+np.exp(-x)))
def derivative(x):
    return(x*(1-x))
for iteration in range(1000):    
    verborgen_laag = sigmoid(np.dot(inputdata,gewichten_vector))
    error = outputdata - verborgen_laag
    delta = error * derivative(verborgen_laag)
    gewichten_vector += np.dot(inputdata.T,delta)
print(verborgen_laag)

[[ 0.02245213]
[ 0.99919797]
[ 0.96623219]
[ 0.03424204]]
 

 

Delen:
Share

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Verplichte velden zijn gemarkeerd met *