LSTM: memoria con compuertas
30 min de lectura
La lección anterior, sobre el gradiente que se desvanece, dejó el diagnóstico cerrado: lo que hunde o dispara la señal de vuelta no es el valor de los pesos sino la forma del camino, y mientras cada paso hacia atrás cruce la misma matriz, el resultado será una potencia de la distancia. También dejó apuntada la salida —una vía por la que el estado avance sumando— y el nombre de la red que la lleva, la LSTM (long short-term memory). Nombrarla no es construirla: una memoria que sólo suma tiene un defecto tan grave como el que arregla, y es que nunca borra nada. Falta lo difícil, que es quién decide, paso a paso, qué se conserva, qué se escribe encima y qué de lo guardado sale a la luz.
Que son tres decisiones y no una se ve en una frase corriente. Toma Las cámaras que el ayuntamiento instaló el año pasado en la plaza no funcionan. Para conjugar funcionan hay que arrastrar el plural de cámaras doce posiciones, sin que ayuntamiento ni plaza —los dos en singular, los dos más cerca— lo pisen por el camino. Arrastrarlo intacto es una cosa; meterlo en la memoria cuando aparece cámaras y no en cada palabra que pasa es otra; y tenerlo guardado durante esas doce posiciones sin que gobierne lo que la red responde en ellas es una tercera. La RNN (recurrent neural network) vanilla hace las tres con el mismo gesto, porque sólo tiene un gesto: reescribir entero en cada paso. Por eso no puede hacer ninguna bien.
Separar los tres trabajos empieza por separar los dos estados. Junto al que ya conoces aparece un segundo vector, el estado de celda , que es la memoria propiamente dicha, y su forma de avanzar es lo único verdaderamente nuevo de esta lección. Todo lo demás de la LSTM existe para gobernar ese vector.
Tres compuertas, cada una por un fallo de la anterior
Empecemos por la vía aditiva sola y vayamos añadiendo lo que le falte. Llamemos candidato al vector que el paso propone guardar, una capa corriente a partir del token y del estado anterior:
y que la memoria lo vaya acumulando: . Para el gradiente esto es perfecto —la derivada de respecto de es la identidad, sin matriz que aplicar veces—, y para la memoria es inservible por dos motivos. Nunca olvida: lo que entró en la coordenada 3 leyendo cámaras sigue ahí trescientos tokens después, en un párrafo que habla de otra cosa. Y crece sin freno: cada término vive en y se suman , así que una coordenada puede acabar valiendo , donde el que la lea está plano —la saturación que el explorador de activaciones de la lección sobre funciones de activación ya te dejó ver—.
Los dos fallos son el mismo: la suma no tiene forma de decir no. Dásela con un factor por coordenada, entre y , que la red calcule en cada paso a partir de lo que está leyendo. Ésa es la compuerta de olvido:
La activación es la sigmoide logística y no la del candidato, por lo mismo que la capa de salida de la lección sobre la RNN vanilla: hace falta una fracción de lo que había, y las fracciones van de a . El pide leerse igual de despacio. Multiplicar coordenada a coordenada deja que la 3 se conserve entera mientras la 7 se borra en el mismo paso; una matriz mezclaría las dos, y mezclar es lo que menos le conviene a una memoria.
Queda el otro lado. Esta versión escribe en cada paso, valga lo que valga el token: que y del escriben tanto como cámaras, y lo escrito sólo se deshace cerrando , que borra de paso lo que esa coordenada guardara de antes. Poner una condición a la escritura pide otro factor igual, la compuerta de entrada:
Y falta decir qué ve el resto de la red. Si el estado oculto fuera —toda la memoria, siempre—, el plural de cámaras gobernaría la respuesta en las doce posiciones del inciso, cuando lo que hace falta es que espere. Guardar algo para luego y no usarlo ahora son dos cosas distintas, así que hay una tercera compuerta, la de salida, y con ella el estado oculto es una vista parcial de la memoria:
Ahí está lo que la RNN vanilla no podía hacer: su tenía dos trabajos a la vez, recordar y enseñar; la LSTM los reparte, y recuerda mientras enseña.
Con las cuatro piezas juntas —tres compuertas y un candidato— el paso completo queda así, con por la misma razón que en la RNN:
Cuatro líneas idénticas y dos que no. Las de arriba son la recurrencia de la lección sobre la RNN vanilla copiada cuatro veces, con , y en todas ellas, y ahí está el precio: la LSTM cuesta pesos, cuatro veces la RNN vanilla. Lo que no cuesta es longitud —tampoco aquí aparece en ninguna forma—.
Ya está toda la maquinaria; muévela antes de derivarla. Recorre la secuencia y mira las tres compuertas abrirse y cerrarse en cada posición; luego sube el sesgo de la de olvido y observa la última fila, lo que sobrevive de .
Por qué el gradiente sobrevive a la vía aditiva
Que la celda «suma» ya se ha dicho tres veces; ahora hay que cobrarlo. Mira la penúltima línea coordenada a coordenada, que es donde se ve lo que el significa:
La coordenada de la celda nueva depende de la coordenada de la vieja y de ninguna otra: por esta vía las coordenadas no se mezclan, porque no hay matriz que las mezcle. Derivando, y dejando quietas las compuertas —volveré sobre esto—, el camino directo de un paso al anterior vale
un número, no una matriz. Encadénalo desde el final hasta un paso lejano , igual que la lección anterior encadenaba la recurrencia hacia atrás, y lo que llega a la celda de por esa vía es
Ponlo al lado del de la lección anterior y cuenta las diferencias, que son tres. La base ya no es una propiedad de una matriz compartida por todo, sino un número por coordenada y por paso que la red calcula a partir de lo que está leyendo. Por esta vía tampoco hay máscara del que muerda en cada paso —ningún que sólo pueda empeorar las cosas—, porque la celda cruza el paso con un producto y una suma y nada más. Y la tercera remata: es alcanzable y además estable, porque una sigmoide saturada se queda en con pendiente casi nula, así que una coordenada que la red aprenda a mantener abierta multiplica por tantas veces como haga falta. El de la RNN era un filo compartido por todas las coordenadas; esto es una decisión de cada una.
Ver qué se ha dejado fuera al «dejar quietas las compuertas»
, y se calculan a partir de , y depende de . Así que la derivada completa suma, al de arriba, los caminos que pasan por las tres compuertas y por el candidato, y todos ellos cruzan una y una máscara de activación: se comportan como los de la lección anterior, y se desvanecen igual.
Que se desvanezcan no rompe nada, y ésa es la parte que merece pensarse. Esos caminos ajustan cómo se decide en cada paso, y una decisión de este paso se toma con lo que hay cerca; el que tiene que llegar de lejos es el de la vía aditiva, y ése no cruza ninguna matriz. La LSTM no elimina el producto de la lección anterior: lo aparta del camino largo y lo deja en los cortos.
Dos concesiones cierran la cuenta, porque sin ellas esto se lee como una garantía. Si la red aprende alrededor de , el producto se hunde igual que antes; la diferencia es que se hunde porque la red lo ha decidido y no porque la arquitectura lo imponga. Y la explosión sigue siendo posible por los caminos del recuadro de arriba, así que el recorte del gradiente de la lección anterior se sigue usando tal cual.
La LSTM, paso a paso, en NumPy
Dos celdas. La primera transcribe el recuadro de seis líneas y lo ejecuta sobre una secuencia corta; la segunda mide lo que sobrevive de un empujón dado al principio de una larga.
Los pesos me los da un generador con semilla fija y no están entrenados, así que las compuertas no significan nada todavía. Lo que sí he puesto a mano es el sesgo de la de olvido, y la celda lo corre con dos valores distintos: es la forma más corta de ver que decide cuánto recuerda la red antes de aprender nada. Mira la última línea de cada bloque.
d_model, d_h, T = 4, 3, 6
X = np.random.default_rng(1).normal(size=(T, d_model)) * 0.5
def sigmoide(z):
return 1.0 / (1.0 + np.exp(-z))
def pesos(sesgo_olvido):
rng = np.random.default_rng(0) # los mismos pesos en los dos casos
return {k: (rng.normal(size=(d_h, d_model)) * 0.5,
rng.normal(size=(d_h, d_h)) * 0.5,
np.full(d_h, b))
for k, b in [("f", sesgo_olvido), ("i", 0.0), ("o", 0.0), ("c", 0.0)]}
def paso(P, h, c, x):
def entra(k):
Wx, Wh, b = P[k]
return Wx @ x + Wh @ h + b
f, i, o = sigmoide(entra("f")), sigmoide(entra("i")), sigmoide(entra("o"))
c = f * c + i * np.tanh(entra("c")) # la vía aditiva, en una línea
return o * np.tanh(c), c, f, i, o
for sesgo in [1.0, 3.0]:
P = pesos(sesgo)
h, c, olvidos = np.zeros(d_h), np.zeros(d_h), []
print("b_f = %.0f" % sesgo)
print(" t f_t i_t o_t c_t")
for t, x in enumerate(X, 1):
h, c, f, i, o = paso(P, h, c, x)
olvidos.append(f)
print(" %d %s %s %s %s" % (t, np.round(f, 2), np.round(i, 2),
np.round(o, 2), np.round(c, 2)))
print(" de c_1 sobrevive en c_6:", np.round(np.prod(olvidos[1:], axis=0), 3))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Las tres compuertas viven en y ninguna vale lo mismo en dos coordenadas, que es todo lo que la fórmula prometía. Compara ahora los dos bloques. Con la de olvido se mueve alrededor de y de llega a un escaso; con ronda y llega un , con los mismos pesos y la misma frase. Cinco pasos bastan para que un sesgo separe una memoria de un colador, y de ahí la costumbre de inicializar positivo: sale más barato que la red aprenda a olvidar que aprenda a recordar. La primera fila de , en cambio, es idéntica en los dos: en la compuerta multiplica .
La segunda celda mide la vía de vuelta sin derivar nada: mueve el estado del primer paso una cantidad diminuta, deja correr treinta y nueve pasos más y compara. Es el sondeo numérico de la lección sobre BPTT (backpropagation through time), aplicado a la distancia en vez de a un peso. Las compuertas de este juguete leen sólo el token —he desconectado el estado a mano— para medir la vía aditiva y nada más.
T, eps = 40, 1e-6
X = np.random.default_rng(2).normal(size=T) * 0.5 # una coordenada, T pasos
def sigmoide(z):
return 1.0 / (1.0 + np.exp(-z))
def rnn(empujon): # h_t = tanh(w h_{t-1} + u x_t)
h = 0.0
for t, x in enumerate(X):
h = np.tanh(0.9 * h + 0.5 * x)
if t == 0:
h += empujon # movemos el estado del paso 1
return h
def lstm(empujon, b_f): # c_t = f_t c_{t-1} + i_t c~_t
c = 0.0
for t, x in enumerate(X):
c = sigmoide(0.5 * x + b_f) * c + sigmoide(0.5 * x) * np.tanh(0.5 * x)
if t == 0:
c += empujon # movemos la celda del paso 1
return c
print("de un empujon dado en el paso 1, cuanto queda 39 pasos despues:")
print(" RNN, w = 0.9 %.2e" % abs((rnn(eps) - rnn(0.0)) / eps))
for b_f in [0.0, 3.0, 5.0]:
q = abs((lstm(eps, b_f) - lstm(0.0, b_f)) / eps)
print(" LSTM, b_f = %.0f %.2e (f_t alrededor de %.3f)" % (b_f, q, sigmoide(b_f)))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Cuatro números y tres lecturas. De la RNN llega : lo pone la matriz y el resto —un orden de magnitud— la máscara del , ese que la lección anterior dijo que sólo podía empeorar las cosas. La segunda línea es la LSTM saliendo peor que la RNN: con la compuerta ronda , el producto cae a y la memoria es peor que la que venía a arreglar. La vía aditiva no regala nada, sólo hace alcanzable lo que en la RNN no lo era. Las dos últimas líneas son ese «alcanzable»: con la compuerta en sobrevive el y con ella en , el , a la distancia donde la RNN llevaba tres ceros tras el punto.
Comprueba tu intuición
Cinco preguntas: por qué la vía aditiva cambia el producto, qué hace cada compuerta en un caso concreto, cuánto cuesta la arquitectura, cómo se ve la diferencia en un número y qué es lo que la LSTM sigue sin garantizar.
¿Por qué el estado de celda de una LSTM no condena al gradiente a la potencia exponencial de la lección anterior?
Una coordenada del estado de celda guarda que el sujeto de la frase era plural. Hay que conservarlo durante toda una oración de relativo, pero sin que gobierne la respuesta de la red en esas posiciones intermedias. ¿Qué hacen las compuertas de esa coordenada mientras dura el relativo?
Una LSTM con un estado de coordenadas sobre embeddings de . ¿Cuántos pesos suman sus ocho matrices y sus cuatro sesgos?
Se acepta un margen de ±0.
Cuarenta pasos de distancia, y en cada uno el gradiente se multiplica por el mismo número: en la RNN por un factor que fija , en la celda de la LSTM por la compuerta de olvido. ¿Qué imprime?
for factor in [0.6, 0.99]:
print(round(factor ** 40, 3))
Marca todo lo que la LSTM no garantiza.
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Y el paso completo escrito por ti, que es la pieza sobre la que se monta todo lo que viene después.
Escribe un paso de LSTM: paso_lstm(h, c, x, P) recibe el estado oculto y el estado de
celda anteriores, el vector del token de turno, y un diccionario P con los pesos de las
cuatro piezas —P["f"], P["i"], P["o"] y P["c"], cada una la tripleta
(Wx, Wh, b)—. Devuelve la pareja (h_nuevo, c_nuevo), en ese orden.
Las cuatro se calculan igual, ; lo único que cambia es la activación: para las tres compuertas, para el candidato. No escribas en los arrays que recibes.
La primera comprobación descarga el intérprete de Python (~15 MB); después queda en la caché del navegador. Este desafío se resuelve mejor con un teclado físico: en el móvil puedes leerlo y volver luego.
La LSTM resuelve el problema de la lección anterior y cobra un precio que la cuenta de pesos sólo dice a medias. Ocho matrices, dos estados que arrastrar y tres compuertas que ajustar: cada pieza llegó aquí arreglando un fallo concreto, sí, pero ninguna con una prueba de que hiciera falta. Y de al menos una se puede sospechar. La compuerta de entrada decide cuánto se escribe justo cuando la de olvido acaba de decidir cuánto se conserva, y las dos preguntas son la misma vista desde los dos lados: si vas a guardar lo nuevo, es porque lo viejo te importa menos.
Alguien tiró de ese hilo. Fundir esas dos compuertas en una, borrar la de salida junto con el estado de celda entero y quedarse con tres cuartas partes de los pesos —conservando casi todo lo que esta lección acaba de comprar— es la siguiente lección, sobre la GRU (gated recurrent unit): la versión simplificada.
Para profundizar3 fuentes · 2 papers, 1 artículo
De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.
- Long Short-Term Memory
El artículo que la introdujo. Trae el estado de celda y el flujo constante de error que lo recorre —tu vía aditiva—, pero sólo con compuertas de entrada y salida: la de olvido la añaden Gers, Schmidhuber y Cummins en 2000.
- Understanding LSTM Networks
Recorre con diagramas las tres compuertas y el estado de celda como «cinta transportadora» —la figura de esta lección—, y de paso presenta la GRU. Dice por qué la vía aditiva ayuda al gradiente; no lo deriva, eso lo has hecho tú.
- LSTM: A Search Space Odyssey
Ocho variantes de la LSTM en 5 400 entrenamientos: lo que de verdad importa es la compuerta de olvido y la activación de salida; acoplar entrada y olvido —lo que hará la GRU— casi no cambia nada. Ninguna variante supera a la estándar.