Werkplaats / De werkplaats
In jouw eigen tempo
← Deep learning
MODULE 04 · LES 09

Regel wat het geheugen bewaart, schrijft en toont

Bouw een LSTM-cel met vier poorten en onderscheid de directe geheugenroute van de volledige recurrente gradiënt.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent de input-, forget- en outputpoort plus kandidaat-geheugen.
  • Je houdt celtoestand en verborgen uitvoer apart en volgt de directe geheugenroute.

Een notitie die na ieder woord volledig wordt herschreven, kan oude informatie snel verliezen. Een LSTM (long short-term memory) houdt daarom naast zijn zichtbare uitvoer h een aparte celtoestand c bij. Poorten bepalen wat blijft, wat erbij komt en wat je laat zien.

Zie c als een opslagvak en h als het gedeelte dat je op dit moment uitleest. Bewaren en tonen zijn verschillende keuzes: een uitvoerpoort kan informatie verbergen zonder het opslagvak leeg te maken. Ook dit geheugen heeft eindige capaciteit en kan nuttige informatie vergeten.

1. Bereken vier verschillende rollen

Onze cel maakt z=xWx+hWh+b met vier blokken in de volgorde i,f,g,o. Voor D invoerwaarden en H geheugenwaarden zijn Wx en Wh respectievelijk (D,4H) en (H,4H), met bias (4H,). Dezelfde parameters keren bij iedere tijdstap terug.

De inputpoort i regelt schrijven, forgetpoort f behouden en uitvoerpoort o tonen; sigmoid zet ze tussen nul en één. De kandidaat g is tanh van zijn eigen preactivatie en kan ook negatief zijn. Het is dus onjuist om alle vier blokken door sigmoid te sturen.

Neem één invoerwaarde 1, vorige h=0.2 en c=2. Kies niet-nulle invoer- en recurrente gewichten en een bias die de eerste preactivaties op [0,log(4),atanh(0.5),log(3)] brengt. Dan zijn i=0.5, f=0.8, g=0.5 en o=0.75; deze gekozen waarden zijn een rekenvoorbeeld, geen getraind model.

In het codevoorbeeld zijn x en h scalair; wx en wh bevatten ieder vier losse gewichten. Dat is dezelfde berekening als met de twee (1,4)-matrices voor H=1.

import numpy as np

def sigmoid(z):
    z = np.asarray(z,float)
    return np.exp(-np.logaddexp(0,-z))

wx = np.array([.1,.2,.3,.4])
wh = np.array([.5,-.5,.25,-.25])
target = np.array([0.,np.log(4),
    np.arctanh(.5),np.log(3)])
b = target-wx-.2*wh
z = wx+.2*wh+b
i,f,o = sigmoid(z[[0,1,3]])
g = np.tanh(z[2])
print(np.round([i,f,g,o],6).tolist())
# [.5,.8,.5,.75]

Gebruik een stabiele sigmoid, ook voor grote positieve of negatieve getallen. Zet integerinputs vóór mintekens en matrixproducten om naar floating point, anders kan de berekening al vóór sigmoid overlopen. Splits de vier blokken pas na de volledige affine berekening, zodat zowel x als de vorige h meetellen.

2. Houd celgeheugen en uitvoer apart

De forgetpoort vermenigvuldigt het oude geheugen; de schrijfbijdrage i⊙g komt erbij. Daarna bepaalt o welk deel van tanh van de nieuwe c zichtbaar wordt. Het teken ⊙ betekent elementgewijs vermenigvuldigen, niet een matrixproduct.

cnieuw=f⊙c+i⊙ghnieuw=o⊙tanh(cnieuw)

Ons voorbeeld bewaart 0.8×2=1.6 en schrijft 0.5×0.5=0.25 bij. Daardoor wordt c=1.85 en h≈0.713809. De celtoestand kan buiten [−1,1] komen; de verborgen uitvoer blijft door tanh en de sigmoidpoort begrensd.

c = 2.
next_c = f*c+i*g
next_h = o*np.tanh(next_c)
print(round(next_c,6))  # 1.85
print(round(next_h,6))  # .713809

Maak sigmoid(z) en lstm_step(x,h,c,params), met dictionary h,c,i,f,g,o. Behoud de invoer en geef de nieuwe toestanden terug. We gebruiken geen peepholes of projecties; de formule hoort bij deze vereenvoudigde cel.

01
CHECKPOINTVier berekeningen, twee toestanden

3. Geef beide nieuwe toestanden door

Bij de volgende invoer veranderen de poorten, omdat zowel x als de nieuwe h aan z bijdragen. Met de volgende input 0 leveren dezelfde parameters c≈1.520862 en h≈0.580572. Steeds h0 gebruiken zou deze tweede berekening veranderen, ook wanneer je c wel correct doorgeeft.

def cell(x,h,c):
    z = x*wx+h*wh+b
    i,f,o = sigmoid(z[[0,1,3]])
    g = np.tanh(z[2])
    c = f*c+i*g
    return o*np.tanh(c),c

hs = [.2]; cs = [2.]
for item in [1.,0.]:
    h,c = cell(item,hs[-1],cs[-1])
    hs.append(h); cs.append(c)
print(np.round(cs,6).tolist())
# [2.,1.85,1.520862]
print(np.round(hs,6).tolist())
# [.2,.713809,.580572]

Maak lstm_forward(x,params,h0,c0) voor invoer (T,D). Bewaar h_states en c_states van vorm (T+1,H), inclusief kopieën van beide beginwaarden. Een lege reeks bevat alleen de oorspronkelijke h0 en c0; verander inputs, parameters en begintoestanden niet.

02
CHECKPOINTGeef zowel h als c door aan de volgende stap

4. Onderzoek één directe geheugenroute

Houd nu de poorten en schrijfbijdragen bewust vast. Noem de vooraf berekende bijdrage i_tg_t kort w_t: dan volgt c_t=f_t c_(t−1)+w_t. Alleen langs deze directe route is de gevoeligheid naar c0 het product van alle forgetpoorten.

∂cT∂c0=∏t=1Tft

Begin met c0=2, forgetpoorten [0.5,1,0] en bijdragen [1,0.2,3]. De toestanden worden 2,2,2.2,3; de laatste nulpoort wist de afhankelijkheid van c0, ook al eindigt het geheugen zelf op 3. Toestand en gevoeligheid zijn dus verschillende getallen.

states = [2.]; sensitivity = 1.
for f,w in zip([.5,1.,0.],[1.,.2,3.]):
    states.append(f*states[-1]+w)
    sensitivity *= f
print(states,sensitivity)
# [2.,2.,2.2,3.] 0.0
BEWAREN, EENMALIG SCHRIJVEN EN TONENVolg de directe LSTM-geheugenroute

c₀=1. De forgetpoort f blijft gelijk. Alleen stap 1 schrijft i×0.5 bij; daarna is de schrijfbijdrage nul. De gelezen uitvoer is o×tanh(c).

00.511.50246810
Celgeheugen c0.3487Gelezen uitvoer h0.2682Directe ∂c/∂c₀0.3487

Groen: celtoestand. Oranje: gelezen uitvoer. Poorten staan hier vast; de getoonde afgeleide is alleen de directe geheugenroute, niet de volledige LSTM-gradiënt.

Maak memory_path(c0,forget,writes), met states en sensitivity. Eis even lange reeksen en poorten binnen [0,1]; NaN is geen geldige poort. Een lege reeks bewaart c0 met gevoeligheid 1.

Dit product is niet de volledige LSTM-gradiënt: in een echte cel hangen poorten via h ook van eerdere toestanden af. De visualisatie bevriest die afhankelijkheden om één route zichtbaar te maken; een forgetpoort dichtbij 1 is geen garantie op onbeperkt geheugen.

03
CHECKPOINTHoud poorten vast om één route te onderzoeken

Wat neem je mee?

  • i,f,g,o hebben verschillende rollen en activaties.
  • Geef zowel h als c door; alleen c is het aparte celgeheugen.
  • Het forgetproduct beschrijft een vastgehouden directe route, geen volledige BPTT.

Hierna bouw je de trainbare vectoren waarmee token-ID's een netwerk kunnen voeden.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPMaak van een token-ID een trainbare vector
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Vier berekeningen, twee toestanden

Schrijf sigmoid(z) stabiel en lstm_step(x,h,c,params), met dictionary h,c,i,f,g,o. params bevat wx,wh,b.

Een hint

np.split(z,4) geeft de vier gelijke blokken. Gebruik exp(−logaddexp(0,−z)) voor sigmoid.

02
Geef zowel h als c door aan de volgende stap

Schrijf lstm_forward(x,params,h0,c0) met dictionary h_states,c_states van vorm (T+1,H), ook voor T=0.

Een hint

Houd twee lijsten bij. Roep lstm_step aan met de laatste waarde uit beide lijsten.

03
Houd poorten vast om één route te onderzoeken

Schrijf memory_path(c0,forget,writes) met states (inclusief c0) en sensitivity. Eis even lange reeksen en 0≤f≤1, anders ValueError.

Een hint

Start sensitivity=1.0 en vermenigvuldig die bij iedere stap met f. De schrijfbijdrage wordt opgeteld bij de toestand.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def sigmoid(z):
    z=np.asarray(z,float)
    return np.exp(-np.logaddexp(0,-z))

def lstm_step(x,h,c,params):
    x,h,c=[np.asarray(v,float) for v in (x,h,c)]
    zi,zf,zg,zo=np.split(x@np.asarray(params['wx'],float)+h@np.asarray(params['wh'],float)+np.asarray(params['b'],float),4)
    i,f,g,o=sigmoid(zi),sigmoid(zf),np.tanh(zg),sigmoid(zo)
    next_c=f*c+i*g
    return {'h':o*np.tanh(next_c),'c':next_c,'i':i,'f':f,'g':g,'o':o}

def lstm_forward(x,params,h0,c0):
    hs=[np.asarray(h0,float).copy()];cs=[np.asarray(c0,float).copy()]
    for item in x:
        result=lstm_step(item,hs[-1],cs[-1],params)
        hs.append(result["h"]);cs.append(result["c"])
    return {"h_states":np.stack(hs),"c_states":np.stack(cs)}

def memory_path(c0,forget,writes):
    if len(forget)!=len(writes) or any(not 0<=f<=1 for f in forget):
        raise ValueError("Ongeldige poortreeks")
    states=[float(c0)];sensitivity=1.
    for f,w in zip(forget,writes):
        states.append(f*states[-1]+w)
        sensitivity*=f
    return {"states":states,"sensitivity":sensitivity}

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.