Werkplaats / Notebook
In jouw eigen tempo
← Alle bouwprojecten
Neurale netwerken · PYTHON NOTEBOOK

Train XOR met je eigen autograd-engine

Combineer eigen scalar-operaties, een gedeelde rekengraaf en reverse-mode tot een trainbaar netwerk met zeventien parameters.

160 min4 cellen
De opdracht, leerdoelen en voorkennis
  • Je bouwt een complete kleine autograd-engine zonder differentiatiebibliotheek.
  • Je gebruikt de eigen engine om een netwerk met vier verborgen tanh-neuronen te trainen.
  • Je controleert gedeelde parameters en tekent het werkelijk gemeten trainingsverlies.

Lees eerst: Train je eerste model · Een activatie maakt een nieuwe grens mogelijk · Tel de bijdragen van alle rekenpaden op.

Je hebt losse operaties en hun terugweg gebouwd. Nu gebruiken we die eigen engine om een netwerk te trainen: de computer verzamelt de gradiënten via de graaf, terwijl jij het model, verlies en leerproces samenstelt.

Het speelgoedprobleem is XOR: twee verschillende bits krijgen label +1, twee gelijke bits −1. We trainen op alle vier mogelijkheden, met vier verborgen tanh-neuronen en een lineaire uitgang. Dit toont dat onze engine kan leren; de vier punten vormen geen onafhankelijke test op generalisatie.

1. Neem je gecontroleerde operaties over

Vul de eerste cel met knoop, optel, keer en tanh uit de vorige lessen. Elke uitkomst krijgt een nieuwe knoop, maar haar ouderverwijzingen wijzen naar de oorspronkelijke objecten. Bij hetzelfde blad op twee ingangen blijven beide verbindingen bestaan.

x = knoop(3)
kwadraat = keer(x, x)
print(kwadraat['value'])  # 9.0
print(len(kwadraat['parents']))  # 2

Voer iedere cel uit voordat je de volgende gebruikt. Variabelen blijven binnen dit notebook beschikbaar; na een herstart voer je de cellen opnieuw van boven naar beneden uit. Een correcte losse Python-functie wordt pas onderdeel van de training nadat haar cel is uitgevoerd.

01
CHECKPOINTBouw de voorwaartse scalar-operaties

2. Houd gedeelde parameters werkelijk gedeeld

Neem volgorde, wis_gradients en terug over in cel twee. Hetzelfde gewicht beïnvloedt straks vier voorspellingen en ontvangt dus bijdragen langs meerdere routes. Alle bijdragen moeten bij hetzelfde parameterobject terechtkomen.

uitkomst = optel(kwadraat, x)
terug(uitkomst)
print(x['grad'])  # 7.0
terug(uitkomst, seed=2)
print(x['grad'])  # 14.0

De seed schaalt de terugweg; de reset voorkomt ophoping tussen aanroepen. Tijdens één batch mogen de parameterwaarden nog niet veranderen. Na de gezamenlijke update hergebruik je de parameterbladeren, maar bouw je voor de nieuwe waarden verse operatieknooppunten.

02
CHECKPOINTBouw de gedeelde terugweg

3. Van een parameterlijst naar vier verborgen neuronen

Twee invoergewichten en één bias per verborgen neuron kosten viermaal drie getallen. De uitgang krijgt vier gewichten en één bias: samen zeventien trainbare bladeren. De lijstvolgorde hieronder is het contract van maak_netwerk en voorspel.

RolPosities in paramsAantal
Neuron j3j, 3j+1, 3j+23 per neuron
Gewichten uitgang12 tot en met 154
Bias uitgang161

Hier loopt j van 0 tot en met 3. De drie posities zijn achtereenvolgens gewicht voor invoer 0, gewicht voor invoer 1 en bias. Een vaste seed maakt de start herhaalbaar; een andere seed hoort andere beginwaarden te geven.

import random
def maak_netwerk(seed=7):
    rng = random.Random(seed)
    return [knoop(rng.uniform(-1,1))
            for _ in range(17)]

params = maak_netwerk(7)
print(len(params))  # 17
print(round(params[16]['value'], 6))
# 0.254866

Voor invoer [0,0] vallen beide invoerproducten weg. De vier verborgen waarden worden tanh van hun biases: ongeveer [0.293022,−0.262342,−0.728258,−0.674293]. Na vermenigvuldigen met de uitgangsgewichten krijg je bijdragen −0.044235,−0.171494,0.547939 en 0.373236; met uitgangsbias 0.254866 is de score 0.960312.

Dezelfde opbouw werkt voor andere invoer. In de formule zijn w de verborgen gewichten, b de biases, a de gewogen sommen, h de verborgen activaties en v de uitgangsgewichten. De lineaire uitgang hoeft geen kans of waarde tussen −1 en 1 te zijn.

aj=wj0x0+wj1x1+bjhj=tanh(aj)y^=buit+∑j=03vjhj
def voorspel(inputs, params):
    x = [knoop(v) for v in inputs]
    hidden = []
    for j in range(4):
        w = params[3*j:3*j+3]
        a = optel(keer(x[0], w[0]),
                  keer(x[1], w[1]))
        h = tanh(optel(a,w[2]))
        hidden.append(h)
    uit = params[16]
    for j in range(4):
        v = params[12+j]
        bijdrage = keer(hidden[j],v)
        uit = optel(uit,bijdrage)
    return uit

score = voorspel([0,0], params)
print(round(score['value'], 6))
# 0.960312

Maak deze functies en bewaar de eerste parameterlijst in params; laat de invoerlijst en bestaande parameterwaarden intact. De slice maakt een nieuwe lijst met dezelfde knoopobjecten, geen nieuwe gewichten. Kopieer je binnen voorspel de parameterbladeren, dan komen hun gradiënten niet bij de te trainen objecten uit.

03
CHECKPOINTVerbind twee inputs, vier tanh-neuronen en één uitgang

4. Eén gezamenlijke fout, daarna één update

Voor [00,01,10,11] zijn de startvoorspellingen ongeveer [0.960312,1.243617,0.856594,1.075713]. Trek de doelen [−1,+1,+1,−1] af en kwadrateer: ongeveer [3.842822,0.059349,0.020565,4.308582]. Deel hun som door vier en je krijgt startverlies 2.057830.

L=14∑i=03(y^i−yi)2

Maak batchverlies(params) met je eigen graafoperaties: tel de vier foutkwadraten op en vermenigvuldig met een constante knoop 0.25. De hele batch moet één graaf vormen; roep daarna eenmaal terug aan. Autograd telt dan voor elk gewicht de bijdragen van alle voorbeelden op.

loss = batchverlies(params)
terug(loss)
print(round(loss['value'], 6))
# 2.05783
print(round(params[16]['grad'], 6))
# 2.068118
for p in params:
    p['value'] -= .1*p['grad']
print(round(params[16]['value'], 6))
# 0.048055
nieuw = batchverlies(params)['value']
print(round(nieuw,6))
# 1.166897

De uitgangsbias ontvangt de gemiddelde som van tweemaal de vier fouten, 2.068118. Zijn update is dus 0.254866−0.1×2.068118, ongeveer 0.048055. De andere zestien parameters veranderen tegelijk; pas daarna meet je de nieuwe fout met een verse graaf.

Maak train_xor(epochs=600,eta=.1) met steeds een nieuw netwerk uit seed 7. Bewaar voor iedere update de echte loss en voeg na de laatste update nog één meting toe: standaard 601 waarden. Geef params, history en de vier werkelijk berekende predictions terug.

Voer de training in resultaat uit en teken haar history met asnamen en titel. De x-as toont het aantal uitgevoerde updates: 0 tot en met epochs, standaard 0…600. Een dalende lijn alleen is onvoldoende: de lijn moet de gemeten verliezen tonen.

Print ook de eindvoorspellingen en gebruik hun teken voor de labels. De precieze scores kunnen tussen uitvoeromgevingen licht verschillen; seed7 blijft binnen dezelfde omgeving herhaalbaar.

04
CHECKPOINTTrain, meet en teken de echte leercurve

Wat neem je mee?

  • Dezelfde zeventien parameterobjecten verbinden alle voorbeelden met één batchverlies.
  • Nieuwe graaf, terugweg, gezamenlijke update en nieuwe meting vormen iedere trainingsstap.
  • Een echte leercurve en berekende voorspellingen laten zien wat jouw eigen engine heeft geleerd.

Hierna onderzoek je hoe initialisatie en verschillende optimizers de training beïnvloeden.

Bronnen: PyTorch — introductie tot automatische differentiatie ↗ Karpathy — micrograd, educatieve scalar-autograd-engine ↗ · Gecontroleerd 2026-10-28.

Python start zodra jij code uitvoert.
[1]

Bouw de voorwaartse scalar-operaties

Gebruik je gecontroleerde knoop, optel, keer en tanh. Bewaar oorspronkelijke ouderobjecten en alle verbindingen, ook bij x maal x. Controleer eerst waarde 9 en twee ingangen bij x=3; voer de cel daarna uit.

Bekijk voorbeeld
import math

def knoop(value,parents=()):
    return {"value":float(value),"grad":0.0,"parents":tuple(parents)}

def optel(a,b):
    return knoop(a["value"]+b["value"],((a,1.0),(b,1.0)))

def keer(a,b):
    return knoop(a["value"]*b["value"],((a,b["value"]),(b,a["value"])))

def tanh(a):
    value=math.tanh(a["value"])
    return knoop(value,((a,1-value*value),))
[2]

Bouw de gedeelde terugweg

Neem volgorde, wis_gradients en terug over. Iedere aanroep begint schoon en telt alle ouderbijdragen op. Controleer x maal x plus x bij x=3: gradiënt 7, met seed 2 wordt dat 14. Parameters veranderen pas na de volledige terugweg.

Bekijk voorbeeld
def volgorde(root):
    ordered=[]
    seen=set()
    def visit(node):
        if id(node) in seen:
            return
        seen.add(id(node))
        for parent,_ in node["parents"]:
            visit(parent)
        ordered.append(node)
    visit(root)
    return ordered

def wis_gradients(root):
    for node in volgorde(root):
        node["grad"]=0.0

def terug(root,seed=1.0):
    wis_gradients(root)
    root["grad"]=float(seed)
    for node in reversed(volgorde(root)):
        for parent,local in node["parents"]:
            parent["grad"]+=node["grad"]*local
[3]

Verbind twee inputs, vier tanh-neuronen en één uitgang

Bouw maak_netwerk(seed=7) met zeventien losse bladeren uit random.Random(seed).uniform(-1,1). Vier drietallen bevatten verborgen invoergewichten en bias; posities 12–15 zijn uitgangsgewichten en 16 is de uitgangsbias. Voorspel met dezelfde parameterobjecten en nieuwe operatieknooppunten. Bewaar de invoer en parameterwaarden. De score voor [0,0] begint bij seed 7 op ongeveer 0.960312.

Bekijk voorbeeld
import random

def maak_netwerk(seed=7):
    rng=random.Random(seed)
    return [knoop(rng.uniform(-1,1)) for _ in range(17)]

def voorspel(inputs,params):
    x=[knoop(v) for v in inputs]
    hidden=[]
    for j in range(4):
        a=optel(optel(keer(x[0],params[3*j]),keer(x[1],params[3*j+1])),params[3*j+2])
        hidden.append(tanh(a))
    output=params[16]
    for j in range(4):
        output=optel(output,keer(hidden[j],params[12+j]))
    return output

params=maak_netwerk()
print("Trainbare getallen:",len(params))
[4]

Train, meet en teken de echte leercurve

Maak batchverlies als één graaf met vier foutkwadraten maal 0.25. De start-MSE is 2.057830; na één gezamenlijke stap met eta 0.1 is zij 1.166897. Train_xor begint steeds opnieuw met seed 7 en gebruikt je eigen terugfunctie. Bewaar vóór elke update de echte loss en eenmaal erna; standaard 601 metingen. Geef params/history/predictions terug, teken history tegen het aantal updates 0 t/m epochs met asnamen/titel en print de vier berekende eindvoorspellingen.

Bekijk voorbeeld
import matplotlib.pyplot as plt

def batchverlies(params):
    total=knoop(0)
    for inputs,target in [([0,0],-1),([0,1],1),([1,0],1),([1,1],-1)]:
        error=optel(voorspel(inputs,params),knoop(-target))
        total=optel(total,keer(error,error))
    return keer(total,knoop(.25))

def train_xor(epochs=600,eta=.1):
    parameters=maak_netwerk(7)
    history=[]
    for _ in range(epochs):
        loss=batchverlies(parameters)
        history.append(loss["value"])
        terug(loss)
        for p in parameters:
            p["value"]-=eta*p["grad"]
    history.append(batchverlies(parameters)["value"])
    predictions=[voorspel(x,parameters)["value"] for x in [[0,0],[0,1],[1,0],[1,1]]]
    return {"params":parameters,"history":history,"predictions":predictions}

resultaat=train_xor()
plt.figure(figsize=(6,3.5))
plt.plot(resultaat["history"])
plt.xlabel("Update")
plt.ylabel("Gemiddelde kwadratische fout")
plt.title("Eigen autograd leert XOR")
plt.tight_layout()
print("Eindverlies:",round(resultaat["history"][-1],6))
print("Voorspellingen:",[round(v,3) for v in resultaat["predictions"]])