- 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.
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.
013. 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.
024. 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.
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
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).
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.
03Wat 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.