In het hoofdstuk technieken zijn kunstmatige neurale netwerken al geïntroduceerd. Daar kun je teruglezen hoe deze techniek is afgekeken van het menselijke brein. Hier gaan we aan de slag met het coderen van eigen netwerken. In dit hoofdstuk beginnen we met het meest eenvoudige netwerk om een gevoel te krijgen bij een leerproces. Dit hoofdstuk is met enige aanpassingen een vertaling van Nature of code, hoofdstuk 10. In het volgende hoofdstuk werken we een aantal voorbeelden uit met behulp van de Javascript library brains.js.
Een neuraal netwerk heeft de taak om een bepaalde invoer om te zetten naar een uitvoer. Zie figuur 1. Bijvoorbeeld, is de foto die aan het systeem wordt aangeboden (input) een poes ja of nee (output)? Het kunstmatige neurale netwerk (bestaande uit de verborgen lagen) moet dit antwoord met een grote mate van zekerheid kunnen geven. Een klein kind kan al vrij snel een poes herkennen en beseffen dat deze anders is dan een hond, bovendien kan dit kind ook nog lopen zonder te vallen, blokken stapelen, met een lepel eten, zingen, .... Kortom het biologische neurale netwerk kan héél veel en héél veel leren. Wat dat betreft staan de kunstmatige neurale netwerken nog niet eens in de kinderschoenen. De kunstmatige neurale netwerken kunnen meestal nog maar één taak leren, bijvoorbeeld het lezen van handgeschreven letters, schaken of beeldanalyse. Een groot voordeel van een kunstmatig neuraal netwerk is dat het trucje gekopieerd kan worden van de ene naar de ander machine. Waar ieder kind alles zelf moet leren kan een een nieuwe computer het geleerde patroon van een andere computer gelijk gebruiken. Een ander groot voordeel is dat een computer veel meer gegevens kan verwerken dan een mens, daardoor is het mogelijk om patronen te vinden die het menselijk brein nooit zou kunnen vinden.
In het vervolg van dit hoofdstuk en in het volgende hoofdstuk zullen we je laten ruiken aan de manier van werken van kunstmatig neuraal netwerk. We kunnen slechts een zeer kleine aanzet geven. Het punt is dat neurale netwerken ingewikkeld en moeilijk zijn. Ze omvatten allerlei mooie wiskunde. Hoewel deze moeilijkheden allemaal fascinerend zijn (en ongelooflijk belangrijk voor wetenschappelijk onderzoek), zouden we om al dit materiaal te behandelen, een ander boek nodig hebben - of waarschijnlijker: een serie boeken. We beperken ons daarom tot een aantal voorbeelden waarvan we bij sommigen eigenlijk ook zonder een neuraal netwerk het antwoord al weten. Juist daardoor krijg je inzicht in de kracht, maar ook in de beperkingen van een neuraal netwerk. We bieden de stof zo aan dat je het meest simpele neurale netwerk zelf kan programmeren. In de andere voorbeelden gebruiken we de library brains.js.
Het belangrijkste element van een kunstmatig neuraal netwerk is het vermogen om te leren. Een neuraal netwerk is niet alleen een complex systeem, maar een complex adaptief systeem, wat betekent dat het zijn interne structuur kan veranderen op basis van de informatie die er doorheen stroomt. Meestal wordt dit bereikt door het aanpassen van gewichten. In het diagram in figuur 2 vertegenwoordigt elke pijl een verbinding tussen twee neuronen (=knopen=perceptron) en geeft het pad aan voor de informatiestroom. Elke verbinding heeft een gewicht, een getal dat het signaal tussen de twee neuronen regelt. Als het netwerk een "juiste" output genereert (die we later zullen definiëren), is het niet nodig om de gewichten aan te passen. Als het netwerk echter een "onjuiste" output genereert - een fout, om zo te zeggen - dan past het systeem zich aan door de gewichten aan te passen om dichter bij het juiste antwoord te komen.
Er zijn verschillende leerstrategieën voor neurale netwerken.
Dit vermogen van een neuraal netwerk om te leren, om zijn structuur in de loop van de tijd aan te passen, maakt het zo nuttig op het gebied van kunstmatige intelligentie. Hieronder zijn enkele standaardtoepassingen van neurale netwerken weergegeven die aanwezig zijn in verschillende computer programma's . Dit is geenszins een uitgebreide lijst van toepassingen van neurale netwerken, maar hopelijk geeft het je een algemeen beeld van de functies en mogelijkheden.
De perceptron is in 1957 bedacht door Frank Rosenblatt van het Cornell Aeronautical Laboratory. Een perceptron is het meest eenvoudige neurale netwerk dat mogelijk is: een computermodel van een enkel neuron (figuur 3). Een perceptron bestaat uit een of meer ingangen (inputs), één processor en één enkele uitgang (output). Een perceptron volgt het "feed-forward" -model. Dit betekent dat inputs naar het neuron worden gestuurd, worden verwerkt en resulteren in een output, de stroom van informatie is dus in één richting. In het bovenstaande diagram betekent dit dat het netwerk (één neuron) van links naar rechts leest: inputs komen binnen, output gaat uit.
Laten we eens de verschillende stappen bekijken.
Stel dat we een perceptron hebben met twee ingangen - laten we ze $x_{1}$ en $x_{2}$ noemen en als voorbeeld de volgende waarden geven
Elke invoer die naar het neuron wordt gestuurd, moet eerst worden gewogen, d.w.z. worden vermenigvuldigd met een bepaalde waarde. Bij het maken van een perceptron beginnen we meestal met het toewijzen van willekeurige gewichten. Laten we hier de invoer de volgende gewichten ($w_{1}$ en $w_{2}$) geven:
We nemen elke invoer en vermenigvuldigen deze met het bijbehorende gewicht.
De gewogen invoer worden vervolgens opgeteld.
De uitvoer (output) van een perceptron wordt gegenereerd door de som uit de vorige stap door een activeringsfunctie te laten gaan. In het geval van een binaire uitvoer, is het de activeringsfunctie die bepaalt op basis van de waarde van de som of deze moet “vuren” of niet. Je kunt je een LED voorstellen die is aangesloten op het uitgangssignaal van de perceptron: als hij vuurt, gaat het licht aan; zo niet, dan blijft het uit.
Activeringsfuncties zijn er in verschillende vormen, simpele maar ook voor de leek moeilijker te begrijpen wiskundige functies. Voor ons perceptron model houden we het zo simpel mogelijk. De activeringsfunctie die we hier kiezen is het teken van de waarde van de som. Met andere woorden, als de som een positief getal is, is de uitvoer 1, als het negatief is, is de uitvoer -1. In netwerken met meerdere lagen kan deze teken activeringsfunctie niet worden gebruikt om het netwerk te trainen (zie stap 4 in het trainingsproces dat verder op in dit hoofdstuk wordt behandeld). In de figuur hiernaast zie je behalve de teken functie nog de sigmoïde functie en de tangens hyperbolicus, deze activeringsfuncties gebruiken we in het volgende hoofdstuk.
Samengevat hebben we het volgende algoritme dat we hierna gaan programmeren.
We gaan ons perceptron algoritme in Javascript implementeren. In een andere taal kan dat natuurlijk ook. Neem het volgende html document als startpunt. Druk in de browser F12 om de uitvoer van console.log( ... ) te zien
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="author" content="Jouw naam">
<title>Neuraal netwerk: perceptron</title>
<script type="text/javascript">
console.log("Druk in de browser op f12."+
"Je ziet dan in de tab console deze tekst");
var invoer = [12 , 4]; // x1,x2
var gewichten = [0.5 , -1]; // w1,w2
</script>
<head>
<body>
</body>
</html>
Voeg nu code toe om de som van de gewogen invoer te berekenen
en toon de som in de console.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="author" content="Jouw naam">
<title>Neuraal netwerk: perceptron</title>
<script type="text/javascript">
var invoer = [12 , 4]; // x1,x2
var gewichten = [0.5 , -1]; // w1,w2
var som = 0;
var i;
for(i=0;i<invoer.length;i++)
{
som += invoer[i]*gewicht[i];
}
console.log("De gewogen som is "+som);
</script>
<head>
<body>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="author" content="Jouw naam">
<title>Neuraal netwerk: perceptron</title>
<script type="text/javascript">
var invoer = [12 , 4]; // x1,x2
var gewichten = [0.5 , -1]; // w1,w2
var som = 0;
var i;
for(i=0;i<invoer.length;i++)
{
som += invoer[i]*gewicht[i];
}
console.log("De gewogen som is "+som);
uitvoer = Math.sign(som);
console.log("De uitvoer ofwel het teken van de som is "+uitvoer);
</script>
<head>
<body>
</body>
</html>
Nu we het rekenproces van een perceptron begrijpen, gaan we deze met een herkenbaar voorbeeld in actie zetten. We hadden al gemeld dat neurale netwerken vaak worden gebruikt voor patroonherkenningstoepassingen, zoals gezichtsherkenning. Zelfs eenvoudige perceptrons kunnen de basisprincipes van classificatie demonstreren. In het nu volgende voorbeeld (figuur 4), met slechts één perceptron, gaan we een scheidingslijn proberen te vinden tussen de regio -1 en +1. We trainen straks het netwerk met een verzameling $x$- en $y$-coördinaten voorzien van een gewenste output.
Beschouw een lijn in een tweedimensionale ruimte (Zie figuur 4). Punten in die ruimte kunnen worden ingedeeld in een groep die aan de ene kant van een lijn liggen en een groep aan de ander kant. Hoewel dit een ietwat dom voorbeeld is (we kunnen namelijk heel makkelijk bepalen of een punt onder of boven een lijn ligt), laat het voorbeeld zien hoe een perceptron getraind kan worden om punten aan de ene kant versus de andere te herkennen. Geven we het getrainde netwerk een x- en y-coördinaat dan kan het ons vertellen aan welke kant van de lijn dit punt ligt, zonder dat de vergelijking direct aan de perceptron wordt gegeven. Bovendien zul je zien dat in het trainingsproces de gewichten wel een heel speciale waarde krijgen.
We beginnen met de situatie in figuur 3. Ofwel een neuraal netwerk met twee keer een invoer, één perceptron en een uitvoer gelijk aan de uitvoer van dit perceptron. De twee ingangen zijn de $x$- en $y$-coördinaten van een punt. Met behulp van een teken activeringsfunctie is de uitvoer -1, 0 of +1 - dat wil zeggen, de invoergegevens worden ingedeeld volgens het teken van de uitvoer. In figuur 4 kunnen we zien hoe elk punt zich onder de lijn (-1) of boven de lijn (+1) of op de lijn (0) bevindt.
We hebben met slechts twee invoeren wel een groot probleem. Als we het punt (0,0) aan dit netwerk voeren, dan is de gewogen som van de invoer altijd 0, want $\mathbf{som} = x \cdot w_{1}+y \cdot w_{2} = 0 \cdot w_{1} + 0 \cdot w_{2}=0$. Omdat de meeste lijnen niet door de oorsprong kan dit recept in het algemeen niet werken.
Om dit probleem te vermijden, heeft onze perceptron een derde input nodig, die wordt de bias (=afwijking) input genoemd. Een bias-input heeft altijd de waarde 1 en wordt ook gewogen. Hiernaast is in figuur 5 onze perceptron met de toegevoegde bias input weergegeven. De som wordt dan: $\mathbf{som} = w_{0} + x \cdot w_{1}+y \cdot w_{2}$. De som voor het punt (0,0) is dan $\mathbf{som} = w_{0} + 0 \cdot w_{1}+0 \cdot w_{2} = w_{0} $ ofwel het gewicht van de bias. $w_{0}$ bepaalt na training dus of (0,0) boven of onder de lijn ligt.
Nadat de som is berekend wordt in dit voorbeeld de uitvoer berekent met de teken activeringsfunctie.
$\mathbf{teken}(\mathbf{som})= 1$ als $som \gt 0$,
$\mathbf{teken}(\mathbf{som})= -1$ als $som \lt 0$ en
$\mathbf{teken}(\mathbf{som})= 0$ als $som = 0$
In figuur 6 kun je de uitvoer van een realisatie van onze perceptron laten uitrekenen, voor een punt $(x,y)$ vooraf gegaan door de input 1 voor de bias. Als je de knop "Maak nieuw perceptron" indrukt dan worden de gewichten $w_{0}, w_{1}, w_{2}$ opnieuw gekozen. Je kunt ook het punt verslepen om ervaren dat als je de lijn passeert het teken wisselt.
De code voor de perceptron in figuur 6 vindt je onder de knop hieronder. Echter zonder de graphics. Wil je die ook bewaar dan dit document. In de code is het deel voor de perceptron gebundeld in een Javascript class Perceptron. Heb je nog nooit met Javascript classes gewerkt dan is onderstaand voorbeeld waarschijnlijk wel te volgen, anders kun je op w3schools meer informatie vinden of bekijk deze video.
<html> <head> <script> const volgproces=false; // variabele wordt straks gebruikt om naar het console te schrijven var myPerceptron=null; // myPerceptron object wordt straks gemaakt // Het gebruik van objecten in javascript // wordt uitgelegd op https://www.w3schools.com/js/js_classes.asp// Definitie class Perceptron class Perceptron { /* * de methode constructor wordt aangeroepen als we straks * een Perceptron maken. * @param: n is het aantal inputs (inclusief de bias) die de perceptron krijgt * @param: f is de activeringsfunctie die de uitvoer bepaalt op basis van * de som van invoer keer gewichten f(som) */ constructor(n,f) { // Overal waar "this." voorstaat is een eigenschap // van een Perceptron object. // Sla het aantal knopen in een variabele op this.aantalInvoerKnopen = n; // Sla de activeringsfunctie in een variabele op ; this.activeringsfunctie = f; // We maken nu een eerste gok voor de gewichten voor de Perceptron this.gewichten=[]; // maak random gewichten voor het perceptron for(var i=0;i<n;i++) { // Trek een toevalsgetal tussen 0 en 1. Door deze // met 2 te vermenigvuldigen en er 1 van af // te trekken wordt het dus een toevalsgetal tussen -1 en 1 this.gewichten[i]=2*Math.random()-1; } } /* * geefDoor is de feed-forward functie. De invoer [1,x1,...,xn] * wordt verwerkt tot de uitvoer */ geefDoor(invoer) { var som =0; var i,uitvoer; // bepaal de som van de gewogen invoer for(i=0;i<this.aantalInvoerKnopen;i++) { som += invoer[i] * this.gewichten[i]; } if(volgproces) console.log("De som van de gewogen invoer is "+ som); // verwerk de som met de activeringsfunctie return uitvoer = this.activeringsfunctie(som); } } //einde definitie Perceptron// De vind lijn app code /* * functie maakPerceptron maakt een globaal Perceptron object myPerceptron * als die nog niet bestaat met n invoer knopen */ function maakPerceptron(n,f) { if(myPerceptron==null) myPerceptron = new Perceptron(n,f); } /* * functie waarLigtPunt vraagt aan de perceptron in welk gebied het punt ligt * @param punt: [1, x-coördinaat , y - coördinaat ] */ function waarLigtPunt(punt) { // Maak zo nodig de perceptron met de lengte van het punt en de teken functie if(myPerceptron==null) { maakPerceptron(invoer.length, Math.sign ); } document.getElementById("uitvoer").innerHTML= "De output voor het punt " + punt + " is " + myPerceptron.geefDoor(punt); } // einde vind lijn app code</script> </head> <body> <button onclick="waarLigtPunt([1, 12 , 4])">Bereken de perceptron voor [1, 12 , 4] ofwel voor ht punt (x,y)=(12,4)</button> <p id="uitvoer"></p> </body> </html>
Waarom zie je figuur 6 de rechte lijn? Deze rechte lijn is de weergave van de status van
het perceptron. Deze status wordt gevormd door de waarden van de gewichten. Bekijk nogmaals de
som
$\mathbf{som} = w_{0} + x \cdot w_{1}+y \cdot w_{2}$
en laten we eens kijken naar het geval dat de som nul is (dus ook $\mathbf{teken}(\mathbf{som}))=0$).
$0 = w_{0} + x \cdot w_{1}+y \cdot w_{2}$
Nu we de perceptron hebben opgezet en er, met behulp van de geefDoor functie, een voorspelling mee kunnen doen over de ligging van een punt $(x,y)$ ten opzichte van een lijn, hebben we eigenlijk nog niets aan dit netwerk. De gewichten zijn per toeval aangewezen en met kans bijna nul zal de lijn, gevormd door deze gewichten, samenvallen met de "werkelijke" situatie. We moeten het netwerk dus gaan trainen om het een goede voorspeller te laten zijn. In het trainingsproces is het de bedoeling dat het netwerk zelf tot een acceptabel resultaat komt. Afhankelijk van de complexiteit van een netwerk zal dit trainen meer of minder moeite kosten. Daar over later meer. Zoals eerder beloofd gebruiken we hier begeleid leren. We bieden punten aan waarvan we weten wat de uitvoer is. Laten we nu eens kijken naar de essentiële stappen in het trainingsproces.
Stappen 1 t/m 4 kunnen we samenvoegen tot één functie. Voordat we daartoe in staat zijn, moeten we stap 3 en 4 in meer detail bekijken. Wat is de fout van het netwerk? En hoe moeten we de gewichten aanpassen aan deze fout? We beschouwen weer ons simpele netwerk bestaande uit slechts één perceptron.
De fout van een perceptron (netwerk) kan worden gedefinieerd als het verschil tussen het gewenste antwoord en zijn voorspelling.
FOUT = GEWENSTE OUTPUT - VOORSPELLING PERCEPTRON
In het geval van onze perceptron, met de teken functie als activeringsfunctie, heeft de uitvoer slechts drie mogelijke waarden: +1, 0, of -1. De mogelijke combinaties en de bijbehorende fouten staan in de tabel onder de knop. Zo is bijvoorbeeld bij een gewenste uitkomst 1 en voorspelling -1 de fout gelijk aan $1 - (-1) = 2$ en andersom bij een gewenste uitkomst -1 en voorspelling 1 is de fout gelijk aan $ (-1) - 1 = -2$
Nu we de fout hebben berekend moeten we de gewichten aanpassen. De fout is de bepalende factor in hoe de gewichten van de perceptron moeten worden aangepast. Wat we voor een bepaald gewicht willen berekenen, is de verandering in dat gewicht, vaak $\Delta \mathbf{gewicht}$ genoemd (of "delta"-gewicht, waarbij delta de Griekse letter $\Delta$ is). Op basis van deze verandering bereken we een nieuw gewicht.
$\mathbf{nieuw}\,\mathbf{gewicht} = \mathbf{gewicht} + \Delta \mathbf{gewicht}$
$\Delta \mathbf{gewicht}$ wordt berekend door de fout te vermenigvuldigen met de invoer.
$\Delta \mathbf{gewicht} = \mathbf{fout}\,\cdot\,\mathbf{invoer}$.
Dus ieder gewicht, voor bias, $x$ en $y$, wordt gestuurd met de waarde van de fout en de waarde, 1,$x$ en $y$, van de invoer bij dat gewicht. Waarom werkt onze methode? We komen daar snel op terug.
Helaas zijn we er met bovenstaande regel nog niet helemaal. Omdat onze fout de waarden $-2,-1,0,1,2$ kan aannemen kan het zijn dat $\Delta \mathbf{gewicht}$ te groot is en we over ons doel heen springen. We moeten het daarom mogelijk maken de snelheid van veranderen te kunnen sturen. We introduceren daartoe een factor $\mathbf{leersnelheid}$, zodat
$\Delta \mathbf{gewicht} = \mathbf{leersnelheid}\,\cdot\,\mathbf{fout}\,\cdot\,\mathbf{invoer}$.
We hebben nu de regel om de gewichten van de perceptron te veranderen en leggen straks, met behulp van een wiskundige redevoering, uit waarom de regel werkt. Voor we dit doen proberen een beeld bij het trainen te schetsen.
In het begeleid leren wordt het netwerk gevoed met trainingsvoorbeelden waarbij we vooronderstellen dat bij bekende invoer een bekende uitvoer aanwezig is. De manier van trainen in begeleid leren kan verschillen afhankelijk van de zekerheid van deze vooronderstellingen. Als zowel de invoer als de uitvoer volledig vast ligt kan men b.v. per trainingsvoorbeeld net zolang trainen tot het voorbeeld juist wordt voorspeld, dit is meestal efficiënter. Als er echter foute waarnemingen in de trainingsvoorbeelden zitten dan is dit geen goede strategie. Er moet een mogelijkheid voor het netwerk zijn om aan het trainingsvoorbeeld te kunnen "ontsnappen". Doe je dat niet dan kan zo'n foute invoer/uitvoer bij wijze van spreken niet in de grijze massa verdwijnen, maar is dan een dwingeland die met fake news zijn zin wil doordrijven. Voorbeelden van fouten die kunnen optreden zijn b.v. meetfouten in de invoer en of meetfouten in de uitvoer. Als er fouten in de trainingsdata zijn (meetfouten ofwel experimentele bias) dan zou het wiskundig het beste zijn om alle fouten die in de voorspellingen van het netwerk samen nemen en dan pas een aanpassing aan de gewichten van de perceptron te doen. Bij een grote trainingset is dat echter weer lastig uit te voeren. Daarom wordt meestal gekozen voor het één keer aanpassen per datapunt, dit vervolgens voor alle datapunten te doen. Vervolgens kan deze hele reeks weer worden herhaald tot een aanvaardbaar niveau van de voorspellingen is bereikt.
Nu is het toch echt tijd om duidelijk te maken waarom de regel
$\mathbf{nieuw}\,\mathbf{gewicht} = \mathbf{gewicht} + \mathbf{leersnelheid}\,\cdot\,\mathbf{fout}\,\cdot\,\mathbf{invoer}$
werkt.
Er zijn meerdere manieren om dit te doen. Wij bekijken een manier die weinig wiskundige
kennis vergt, namelijk vanuit de fouten tabel. Omdat de kans heel klein is dat we een punt
precies op de lijn vinden, bekijken we alleen de situatie dat de fout -2, 0 of 2 is.
Als de fout 0 is dan is $\mathbf{leersnelheid}\,\cdot\,\mathbf{fout}\,\cdot\,\mathbf{invoer}=0$ en veranderen
de gewichten niet.
Bekijk nu de situatie fout= -2. Dan is de gewenste voorspelling voor een punt $[1,x,y]$
gelijk aan -1 en de voorspelling van de perceptron voor dit punt gelijk aan 1.
De voorspelling kregen we van de perceptron uit de som $\mathbf{som}=w_{0}+w_{1}x+w_{2}y$ en
deze is dan positief $(\gt 0)$. Om de som negatief $(\lt 0)$ te laten worden, ofwel de gewenste voorspelling te kunnen
bereiken, moet de som in de volgende stap, dus met de nieuwe gewichten op zijn minst kleiner
worden voordat som=0 gepasseerd kan worden. De nieuwe gewichten bij fout= -2 zijn:
$[ w_{0}-2 \cdot \mathrm{leersnelheid}\,,\,w_{1}-2\cdot\mathrm{leersnelheid} \cdot x\,,\,w_{2}-2 \cdot\mathrm{leersnelheid} \cdot y]$.
De nieuwe som is dan
$\mathrm{som}=w_{0}-2 \cdot \mathrm{leersnelheid}+(w_{1}-2\cdot\mathrm{leersnelheid} \cdot x)x+(w_{2}-2*\mathrm{leersnelheid} \cdot y)y $
Die gaan we herleiden tot iets mooiers:
$\mathrm{som}=w_{0}+w_{1}x+w_{2}y-2\cdot\mathrm{leersnelheid}-2\cdot\mathrm{leersnelheid} \cdot x^{2}-2\cdot\mathrm{leersnelheid} \cdot y^{2}$
En nog mooier:
$\mathrm{som}=w_{0}+w_{1}x+w_{2}y-2\cdot\mathrm{leersnelheid}\cdot(1+ x^{2}+ y^{2})$
De oude som was $\mathrm{som}=w_{0}+w_{1}x+w_{2}y$, ofwel het eerste deel van de nieuwe som en die
was groter dan nul.
Omdat de leersnelheid positief is en ook $1+ x^{2}+ y^{2}$ positief is, wordt dus
een positief getal $2 \cdot \mathrm{leersnelheid} \cdot (1+ x^{2}+ y^{2})$ van iets positiefs afgetrokken.
De nieuwe som is dus echt kleiner. De regel voor de verandering van de gewichten werkt dus in
ieder geval voor fout= -2
<html> <head> <script> const volgproces=false; // variabele wordt straks gebruikt om naar het console te schrijven var trainer=null; // de trainer van de perceptron /* De functie init maakt de trainer */ function init() { trainer = new Trainer(3, Math.sign, 0.01); }// Definitie class Perceptron class Perceptron { /* * de methode constructor wordt aangeroepen als we straks * een Perceptron maken. * @param: n is het aantal inputs (inclusief de bias) die de perceptron krijgt * @param: f is de activeringsfunctie * @param: c is de snelheid van het leerproces */ constructor(n,f,c) { // Overal waar "this." voorstaat is een eigenschap // van een Perceptron object. // Wijs het aantal knopen aan this.aantalInvoerKnopen = n; // Zet de leersnelheid this.leersnelheid = c; // Zet de activeringsfunctie this.activeringsfunctie = f; // We maken nu een eerste gok voor de gewichten voor de Perceptron this.gewichten=[]; // maak random gewichten voor het perceptron for(var i=0;i<n;i++) { // Trek een toevalsgetal tussen 0 en 1. Door deze // met 2 te vermenigvuldigen en er 1 van af // te trekken wordt het dus een toevals getal tussen -1 en 1 this.gewichten[i]=2*Math.random()-1; } } /* * geefDoor is de feed-forward functie. De invoer [1,x1,...,xn] * wordt verwerkt tot de uitvoer * @param invoer: [1,,x1,...,xn]. De 1 is er voor de bias */ geefDoor(invoer) { var som =0; var i,uitvoer; // bepaal de som van de gewogen invoer for(i=0;i<this.aantalInvoerKnopen;i++) { som += invoer[i] * this.gewichten[i]; } if(volgproces) console.log("De som van de gewogen invoer is "+ som); // verwerk de som met de activeringsfunctie return uitvoer = this.activeringsfunctie(som); } /* train is de functie die de gewichten aanpast bij * één invoer. * @param invoer: de waarden van de invoer knopen * vooraf gegaan door de bias=x0=1 * dus invoer = [1, x1,x2,...] * @param gewensteUitvoer: wat zou de output van de percptron moeten zijn. */ train(invoer,gewensteUitvoer) { var voorspelling=this.geefDoor(invoer); // Dit geeft de perceptron als voorspelling var fout = gewensteUitvoer - voorspelling; // Pas alle gewichten aan for (var i = 0; i < this.aantalInvoerKnopen; i++) { this.gewichten[i] += this.leersnelheid * fout * invoer[i]; } if(volgproces) console.log("De nieuwe gewichten zijn "+ this.gewichten); } /* * schaalPerceptron schaalt w0 terug naar 1 * Dit is handig voor de weergave van de lijn. */ schaalPerceptron() { var lijngewichten=[1]; var w0 = this.gewichten[0]; for(var i=1;i<this.aantalInvoerKnopen;i++) { lijngewichten[i] = this.gewichten[i]/w0; } return lijngewichten; } }//einde definitie Perceptron // begin definitie van de trainer class/* De finitie van de class trainer die wordt ingezet om een perceptron te trainen */ class Trainer { /* @param: n is het aantal inputs (inclusief de bias) die de perceptron krijgt * @param: c is de snelheid van het leerproces * @param: f is de activeringsfunctie */ constructor(n,f,c) { this.settings={ myPreceptron:null }; this.setPerceptron(n,f,c) } setPerceptron(n,f,c) { this.settings.myPerceptron = new Perceptron(n,f,c); } /* * functie eenTraining(a,b) voert één training stap uit. In de functie wordt * eerst een nieuw punt gemaakt in het vierkant met zijden 8 en middelpunt * (0,0). Met dat punt doet de perceptron een training. * Resultaten worden in het uitvoer object getoond. * $param a: richtingscoëfficient in y=ax+b * $param b: y-coördinaat snijpunt y-as in y=ax+b */ eenTraining(a,b) { // wat kortere benamingen var myPerceptron=this.settings.myPerceptron; // Maak een punt met toevalsgetallen var x = 8*Math.random()-4; var y = 8*Math.random()-4; // bepaal afwijking voor deze x en y t.o.v. lijn y=ax+b var som = -b - a*x + y; // maak de invoer met bias // bepaal gewenste voorspelling var gewenst=myPerceptron.activeringsfunctie(som); // bepaal de som met de juiste punten var invoer=[1,x,y]; // bepaal de voorspelling van het perceptron var tekstGewichtenVoor = arrayToString(myPerceptron.gewichten,3); var voorspelling = myPerceptron.geefDoor(invoer); // train de perceptron myPerceptron.train(invoer,gewenst); // status na de training var tekstGewichtenNa = arrayToString(myPerceptron.gewichten,3); var voorspellingNaTraining = myPerceptron.geefDoor(invoer); // produceer tekst voor de gebruiker over de huidige toestand var tekst="Gewenste voorspelling voor<br/>"+ "punt [" + arrayToString(invoer,2) + "] is " + gewenst + "<br/> bij de lijn y="+a+"x + "+b+"."+ "<br/>Voorspelling voor training is " + voorspelling + ".<br/>Gewichten:<br/>"+ "voor training: [" + tekstGewichtenVoor+"]."+ ".<br/>na training: [" + tekstGewichtenNa +"]." + ".<br/>De voorspelling na training: " + voorspellingNaTraining; if(voorspelling==gewenst) { tekst+=".<br/>Gewenst en voorspelling zijn gelijk dus er is niets gebeurd"; } else { tekst+=".<br/>Gewenst en voorspelling zijn niet gelijk dus de gewichten zijn veranderd"; } // plaats de tekst in het uitvoerveld document.getElementById("uitvoer").innerHTML=tekst; } }//einde trainer // andere functies/* de functie yLine berekent de y waarde bij gegeven x voor de lijn y=ax+b */ function yLine(a,b,x) { return a*x+b; } /* * functie vraagt aan de perceptron in welk gebied het punt ligt * @param punt: [1, x-coördinaat , y - coördinaat ] */ function waarLigtPunt(punt) { var invoer = eval(document.getElementById(punt).value) if(trainer==null) trainer = new Trainer( invoer.length, Math.sign, 0.01 ); document.getElementById("uitvoer").innerHTML= "De output voor het punt " + invoer + " is " + trainer.settings.myPerceptron.geefDoor(invoer); } /* * functie arrayToString maakt van een getallen rij een * komma gescheiden reeks in hetgewenste aantal decimalen * @param array: de javascript array * @param decimals: het gewenste aantal decimalen */ function arrayToString(array,decimals) { var tekst=""; var first=true; for(var i=0;i<array.length;i++) { if(first) first=false; else tekst+=","; tekst+=array[i].toFixed(decimals); } return tekst; }</script> </head> <body onload="init()" style="margin:0px; "> <!--<p><b>Trainen met de lijn<br/>y=2x-1 ofwel 1-2x+1y=0</b></p> Train de perceptron met een willekeurig gekozen punt<br/>--> Één training:<br/> <button class="knop" onclick="location.reload();">Herlaad</button> <button class="knop" onclick="trainer.eenTraining(2,-1)">Één keer</button> <p id="uitvoer" style="font-size: smaller; overflow-y: auto; width: 250px; height: 170px;"></p> </body> </html>
In de applet in figuur 6 trainen we ons simpele netwerk met de bedoeling juiste voorspellingen voor de lijn $y=2x-1 \Leftrightarrow 1 - 2x + y =0$ te genereren. De invoer (een punt $(x,y)$) wordt bij toeval gekozen en de gewenste uitvoer wordt precies bepaald door de ligging ten opzichte van de lijn. Ofwel de in en uitvoer voldoen aan de bovengenoemde vooronderstelling, bij een bekende invoer hoort een bekende uitvoer.
Vullen we het bij toeval gekozen punt in deze vergelijking dan komt er alleen 0 uit als het punt op de lijn ligt en is het $\mathbf{teken}$ van $1 - 2x + y$ gelijk aan 0. Anders is de gewenste uitvoer $\mathbf{teken}(1 - 2x + y)$ gelijk aan 1 of -1. Eigenlijk zie je hier de uitkomst van een voorspelling van een perceptron met gewichten $w_{0}=1, w_{1}=-2, w_{2}=1$. Het trainingsproces zal er dan ook voor moeten zorgen dat de gewichten van de perceptron door training naar deze waarden toe gaat (of in gelijke verhouding).
In de applet kun je per punt één training doen of één
training per punt tot dat het punt goed voorspeld wordt. Ook kun je beide
trainingen 100 keer herhalen (of vaker als je vaker de knoppen indrukt).
Daarnaast is er de mogelijkheid om een verzameling punten meermaals te
trainen tot alle punten juist worden voorspeld. Iedere keer dat de knop
"voeg 25 punten toe" wordt de verzameling 25 punten groter.
Druk op de
herlaad button om een nieuwe ongetrainde perceptron te krijgen.
Neurale netwerken zijn te trainen om o.a. handschriften, geluiden en te gezichten te herkennen. Dit zijn zeker geen lineaire structuren. Gelukkig kun je dit soort structuren weer opknippen in lijnstukjes op platte figuurtjes die dan wel weer door één preceptron te leren zijn. Er moeten dan natuurlijk wel meerdere perceptrons in het netwerk aanwezig zijn. Hoe complex zo'n netwerk moet zijn is niet altijd makkelijk te bepalen. In het volgende hoofdstuk gaan we hier dieper op in. Op de afbeelding hieronder zie je dat er heel veel typen neurale netwerken zijn. Ieder type heeft een eigen toepassingsgebied. Klik op de afbeelding als je hier meer over wilt weten.
In het trainen van ons simpele netwerk heb je al gezien dat er heel veel trainingspunten nodig zijn. Dit is in het algemeen zo. Het leren van een netwerk kost veel tijd en energie. In ons simpele netwerk waren er slechts drie gewichten die moeten worden aangepast. Zetten we er nog een zelfde perceptron naast dan worden dat er al zes. In een netwerk van zes knopen met twee inputs en een bias zijn dat er al $6 \cdot 3 = 18$. Om al die gewichten te schatten zijn dus heel veel datapunten nodig.