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

Laat een netwerk zijn vorige toestand meenemen

Rol een RNN uit in de tijd en bereken backpropagation through time met gedeelde gewichten en foutsignalen uit latere stappen.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent een verborgen toestand met dezelfde parameters op iedere tijdstap.
  • Je verzamelt gradiënten van directe outputs en toekomstige toestanden.

“Hond bijt man” en “Man bijt hond” bevatten dezelfde woorden, maar geven andere rollen aan hond en man. Een model dat losse woordvectoren alleen optelt, verliest hun volgorde. Een recurrent neuraal netwerk (recurrent neural network, RNN) neemt bij iedere nieuwe invoer een verborgen toestand uit het verleden mee.

Denk aan een notitie die je na ieder gelezen woord bijwerkt. De notitie verandert; de schrijfregel blijft dezelfde. Deze les bouwt één tanh-RNN zonder batch of extra uitvoerlaag, zodat je de toestanden en hun terugweg precies kunt volgen.

1. Combineer nieuwe informatie met de vorige toestand

Op tijdstap t heeft x_t D invoerwaarden en h_t H geheugenwaarden. De gedeelde matrices Wx en Wh hebben vormen (D,H) en (H,H); bias b heeft H waarden. Onze rijvectorconventie gebruikt één gecombineerde bias; de PyTorch-bron heeft een andere opslagconventie met twee biases.

zt=xtWx+htWh+bht+1=tanh(zt)

Neem één inputwaarde 1, vorige toestand 0.5, Wx=1, Wh=−1 en b=0. De preactivatie is 1−0.5=0.5 en de nieuwe toestand tanh(0.5)≈0.462117. Het recurrente gewicht bepaalt hier hoe het verleden de huidige input bijstuurt.

import numpy as np

x = np.array([1.])
h = np.array([.5])
wx = np.array([[1.]])
wh = np.array([[-1.]])
b = np.array([0.])
next_h = np.tanh(x@wx+h@wh+b)
print(np.round(next_h,6).tolist())
# [.462117]

Maak rnn_step(x,h,params) met sleutels wx,wh,b. Geef een nieuwe verborgen vector en behoud de inputs. Reken vóór matrixproducten in floating point om integeroverloop te voorkomen; dezelfde numerieke regel geldt voor de hele reeks.

01
CHECKPOINTEén stap combineert invoer en geheugen

2. Rol dezelfde regel door de tijd uit

Gebruik nu invoer [[0.5],[0]] en h0=[0.5], met dezelfde gewichten. De eerste preactivatie is 0.5−0.5=0 en daarna 0−0=0, dus de opgeslagen toestanden zijn [0.5,0,0]. De twee nullen komen door deze gekozen waarden, niet doordat het netwerk altijd vergeet.

inputs = np.array([[.5],[0.]])
h = np.array([.5])
states = [h.copy()]
for item in inputs:
    h = np.tanh(item@wx+h@wh+b)
    states.append(h.copy())
states = np.stack(states)
print(states.ravel().tolist())
# [.5,0.,0.]

Maak rnn_forward(x,params,h0) voor x van vorm (T,D). Bewaar states van vorm (T+1,H), met de oorspronkelijke h0 op positie 0, plus een kopie van x. Een lege reeks bevat alleen h0; kopieer de invoer zodat latere wijzigingen je opgeslagen berekening niet veranderen.

Verander in de visualisatie de leesrichting en daarna het recurrente gewicht. Dezelfde waarden in een andere volgorde geven doorgaans een andere eindtoestand. Begin voor losse, niet-gerelateerde reeksen met een passende eigen begintoestand, anders kan informatie uit het vorige voorbeeld doorlekken.

DEZELFDE PARAMETERS DOOR DE HELE REEKSVolg vijf stappen door één RNN

hₜ=tanh(0.8xₜ+whₜ₋₁), met h₀=0. Verander de volgorde of het gedeelde recurrente gewicht w. Alle toestanden worden opnieuw berekend.

Een verborgen getal per tijdstap
Stap tInvoer xₜVorige hNieuwe h
11.00.0000.664
20.00.6640.434
3-1.00.434-0.459
41.0-0.4590.445
50.00.4450.302
Gekozen toestand0.6640Laatste toestand0.3019

∂h₅/∂x₁ = 0.050129

Dit is de gevoeligheid voor de eerste gelezen input, via alle vijf tanh-stappen. Bij w=0 heeft een eerdere input geen invloed op een latere toestand.

02
CHECKPOINTBewaar de hele tijdketen voor de terugweg

3. Stuur ook latere fouten terug

Bij terugrekenen door de tijd (backpropagation through time, BPTT) beïnvloedt een toestand zowel zijn directe verlies als latere toestanden. Dy[t] hoort bij states[t+1]; carry is het foutsignaal dat uit de toekomst terugkomt. Tel ze vóór de tanh-afgeleide op en loop achteruit.

Noem die som q, de preactivatiegradiënt d en de volgende toestand h. Het teken ⊙ betekent elementgewijs vermenigvuldigen. T betekent transponeren; de recurrente matrix voert de bijdrage naar de vorige toestand terug.

q=dy+carryd=q⊙(1−h2)carry=dWhT

Kies voor onze twee stappen directe signalen dy=[1,2]. Achteraan wordt d=2 en carry=−2; bij de eerste stap wordt d=1−2=−1. Zonder toekomstbijdrage had je daar ten onrechte 1 gekregen.

dy = np.array([1.,2.])
carry = 0.; gwx = 0.; gwh = 0.
gb = 0.; dx = np.zeros(2)
for t in [1,0]:
    q = dy[t]+carry
    d = q*(1-states[t+1,0]**2)
    gwx += inputs[t,0]*d
    gwh += states[t,0]*d
    gb += d
    dx[t] = d
    carry = -d
print(gwx,gwh,gb)  # -.5 -.5 1.
print(dx.tolist(),carry)
# [-1.,2.] 1.0

In vectorvorm tel je outer(x[t],d) bij dWx, outer(states[t],d) bij dWh en d bij db. Dezelfde parameters waren overal gebruikt, dus krijgen ze bijdragen uit alle stappen. Voor dx[t] gebruik je d@Wx.T; de laatste carry is dh0.

Maak rnn_backward(cache,params,dy) met wx,wh,b,x,h0, zonder cache of parameters te veranderen. Middel niet extra: dy bevat een eventuele verliesmiddeling al. Bij T=0 zijn alle gradiënten nul.

Controleer bijvoorbeeld dWx via de scalaire verliesfunctie L=h1×1+h2×2. Een kleine verandering van het gedeelde Wx verandert beide stappen; de centrale differentie moet ongeveer −0.5 opleveren.

def objective(weight):
    h = .5; loss = 0.
    for x,g in zip([.5,0.],[1.,2.]):
        h = np.tanh(x*weight-h)
        loss += h*g
    return loss

eps = 1e-5
delta = objective(1+eps)
delta -= objective(1-eps)
print(round(delta/(2*eps),6))
# -.5
03
CHECKPOINTTel toekomstig en direct foutsignaal bij elkaar

Wat neem je mee?

  • Toestanden veranderen; gewichten worden door de tijd gedeeld.
  • BPTT telt directe en toekomstige foutsignalen vóór de lokale afgeleide op.
  • Veel vermenigvuldigingen kunnen signalen uitdoven of versterken; afgekapt terugrekenen is een andere benadering.

Hierna voeg je poorten toe die bepalen wat een geheugen bewaart en toont.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPRegel wat het geheugen bewaart, schrijft en toont
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
Eén stap combineert invoer en geheugen

Schrijf rnn_step(x,h,params) met params-sleutels wx,wh,b. Retourneer een nieuwe verborgen vector en verander de inputs niet.

Een hint

Dit is één affine berekening gevolgd door np.tanh; het geheugen loopt via h@wh.

02
Bewaar de hele tijdketen voor de terugweg

Schrijf rnn_forward(x,params,h0) met sleutels x en states. Ondersteun T=0 en kopieer h0 zodat latere mutatie geen effect heeft.

Een hint

Begin met states=[h0.copy()], voeg iedere nieuwe toestand toe en gebruik np.stack.

03
Tel toekomstig en direct foutsignaal bij elkaar

Schrijf rnn_backward(cache,params,dy) met wx,wh,b,x,h0. dy heeft vorm (T,H); er wordt niet extra gemiddeld.

Een hint

Begin alle gradiënten op nul. De states-index t hoort bij de toestand vóór invoer x[t], index t+1 erna.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def rnn_step(x,h,params):
    return np.tanh(np.asarray(x,float)@np.asarray(params['wx'],float)+np.asarray(h,float)@np.asarray(params['wh'],float)+np.asarray(params['b'],float))

def rnn_forward(x,params,h0):
    states=[np.asarray(h0,float).copy()]
    for item in x:
        states.append(rnn_step(item,states[-1],params))
    return {'x':np.asarray(x,float).copy(),'states':np.stack(states)}

def rnn_backward(cache,params,dy):
    x,h=cache['x'],cache['states']
    g={name:np.zeros_like(params[name],dtype=float) for name in ['wx','wh','b']}
    g['x']=np.zeros_like(x,dtype=float)
    carry=np.zeros(h.shape[1])
    for t in range(len(x)-1,-1,-1):
        da=(dy[t]+carry)*(1-h[t+1]**2)
        g['wx']+=np.outer(x[t],da)
        g['wh']+=np.outer(h[t],da)
        g['b']+=da
        g['x'][t]=da@params['wx'].T
        carry=da@params['wh'].T
    g['h0']=carry
    return g

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