Por qué el MLP falla con secuencias
23 min read
Una capa de entrada tiene el ancho que se le dio al construirla; un texto tiene el largo que tenga. El bloque anterior, dedicado al perceptrón multicapa (multilayer perceptron, MLP), terminó chocando contra esa tensión sin llegar a escribirla: su clasificador de reseñas acertaba el de las que no había leído, y el techo se lo ponía la entrada y no la red, porque el orden de los tokens no llegaba a cruzar la puerta. Corregirlo obliga a cambiar la entrada, y cambiar la entrada obliga a volver sobre , que tiene columnas desde el instante en que se construye.
La reparación que se le ocurre a cualquiera es ponerlos en fila. Cada token trae su vector —el embedding de la lección sobre representaciones densas—, y pegar esos vectores uno detrás de otro deja el orden escrito en el sitio que ocupa cada uno: la posición pasa a ser una coordenada, y ninguna suma la borra. La reparación aguanta hasta el segundo texto. la fotografía es magnífica trae cuatro vectores y guion flojo trae dos, de modo que producen entradas de dos anchuras distintas, y una capa que multiplica por acepta una sola.
Hay una segunda consecuencia, y ésa no se ve hasta dibujarla.
Lo que queda de lección es esa figura escrita con símbolos, y después medida con NumPy.
Una secuencia que no cabe en una entrada de tamaño fijo
El tokenizador convierte un documento en una secuencia de tokens . Esa longitud pertenece a ese documento: el siguiente traerá otra. Cada posición aporta el vector de su entrada del vocabulario, , la fila de que le corresponde. Apilarlos en una sola columna es escribir
donde los puntos y comas apilan hacia abajo: lo que sale es un vector de números, no una matriz de filas. El orden está dentro, intacto, y leerlo es cuestión de mirar el tramo que toque. Las dos escrituras de la letra conviven a propósito y conviene fijarlas aquí: sin subíndice es la entrada del ejemplo entero, como en todo el bloque anterior, y es el trozo que aporta la posición .
Enfrente hay una capa que no admite negociación. tiene columnas, ese número se elige al construir la red y no sabe nada del documento que vaya a entrar. Con midiendo y con cambiando en cada texto, el producto está definido para los documentos de una longitud y no lo está para los demás. No es un problema de precisión ni de ajuste: es que la multiplicación no existe.
La salida —la única disponible sin cambiar de arquitectura— es fijar un número de posiciones y obligar a todos los documentos a medir eso:
quedándose con las primeras posiciones cuando . Rellenar con el vector nulo lo que sobra se llama padding; cortar por es truncar. Con las dos cosas, para todo documento y la red ya se puede construir.
Las dos mitades de ese arreglo cuestan cosas distintas y las dos hay que decirlas en voz alta. Truncar pierde el final entero de cualquier documento largo, y lo pierde por posición: no se parece al problema de las palabras fuera de vocabulario (out-of-vocabulary, OOV) de la lección sobre el vocabulario, donde lo que caía eran los tipos raros y por eso la pérdida era pequeña. Aquí puede caer la entrada más frecuente del corpus, si le toca estar en la posición . Y el padding paga por el otro extremo: con puesto donde llegue el documento más largo, un documento de longitud media entra en la red con más de la mitad de su vector a cero.
Cada posición aprende por su cuenta
Corta por donde ya está cortada la entrada. Sus columnas se reparten en bloques consecutivos de columnas cada uno, con —ese subíndice es una posición de la secuencia, no una coordenada—, y el producto se parte con ella:
Ahí no ha pasado nada todavía: es el mismo producto de siempre, agrupado por tramos en lugar de por columnas sueltas. Lo que pasa se ve al derivar.
La lección sobre backpropagation deja el gradiente de una capa en una línea, , y para la primera capa , así que
Ese producto exterior se parte por columnas exactamente por donde se partió , y el trozo que le toca a cada bloque es
Ver el corte por columnas
El producto exterior se define coordenada a coordenada: la casilla de vale , con recorriendo las neuronas y las columnas.
La columna pertenece al bloque cuando , y dentro de ese bloque ocupa la posición . Como la concatenación define como la coordenada de , la submatriz formada por esas columnas tiene en su casilla el número —que es, casilla por casilla, —.
El corte funciona porque las dos cosas que se están cortando están cortadas por el mismo sitio: las columnas de y las coordenadas de . Y nada más entra en la cuenta: va entero a todos los bloques, así que lo único que distingue el gradiente de un bloque del de otro es qué vector de la secuencia lo multiplica.
Léela despacio, porque es el resultado de la lección. El gradiente del bloque depende de y de ninguna otra parte de la entrada. Si esa posición viene rellenada, , el bloque recibe la matriz nula: ese ejemplo no le enseña nada, ni poco ni mucho. Y si la entrada llega en one-hot, tiene un solo uno, de manera que es nula salvo en una columna: dentro del bloque , la única columna que se mueve es la de la entrada del vocabulario que ocupó esa posición.
Junta las dos frases y sale la afirmación que da título a este apartado. La columna que representa a una entrada dentro del bloque se ajusta sólo con los ejemplos en los que esa entrada apareció en la posición . Una entrada que en el conjunto de entrenamiento no llegó nunca a la posición 5 deja las columnas correspondientes del bloque 5 tal y como salieron de la inicialización, por muchos pasos que dé el descenso; y lo que la red haya averiguado sobre esa misma entrada en la posición 3 está guardado en otros pesos, a los que la posición 5 no tiene acceso.
Lo que cuesta en pesos
tiene forma , así que guarda pesos, y con la entrada concatenada eso es
Pon números de un tamaño corriente: embeddings de , una capa oculta de neuronas y posiciones dan pesos en la primera capa. El clasificador de reseñas de la lección anterior, con la misma capa oculta sobre una bolsa de palabras, tenía parámetros en total. Y leer párrafos en vez de frases, , sube la cifra a .
Ese crecimiento no es el problema; es el síntoma. Lo que hay debajo es que los pesos añadidos al pasar de posiciones a son pesos nuevos, que empiezan en su inicialización y a los que no les llega nada de lo aprendido en los treinta primeros bloques. El corpus, mientras tanto, sigue teniendo los ejemplos que tenía: más posiciones significa los mismos ejemplos repartidos entre más bloques, es decir, menos experiencia por parámetro justo cuando hay más parámetros.
Midiéndolo con NumPy
Un experimento pequeño lo pone en números. Las secuencias me las he inventado para el curso: seis posiciones, ocho entradas de vocabulario, y la etiqueta vale cuando aparece no entre ellas. La diferencia está en dónde aparece —en el entrenamiento, sólo en las cuatro primeras posiciones; en el examen, sólo en las dos últimas—, y esa es la única diferencia entre los dos conjuntos.
Con los mismos datos se entrena dos veces la misma red: una sobre la concatenación de las seis posiciones y otra sobre su bolsa de palabras, que son las mismas ocho coordenadas sumadas. El bucle es el de la lección sobre implementar un MLP, sin tocar una línea. Mira su salida en tres tramos: qué hace la concatenación, qué hace la bolsa de palabras con esos mismos datos, y qué le ha pasado a cada bloque de .
V = ["la", "película", "es", "muy", "buena", "larga", "divertida", "no"]
NO, T = 7, 6 # «no» es la última entrada; seis posiciones
rng = np.random.default_rng(0)
def corpus(n, posiciones):
S = rng.integers(0, NO, size=(n, T)) # relleno, sacado de las siete primeras
y = np.zeros((n, 1))
y[::2] = 1.0 # la mitad exacta lleva «no»...
for i in np.nonzero(y[:, 0])[0]:
S[i, rng.choice(posiciones)] = NO # ...y sólo donde se le diga
U = np.eye(len(V))[S] # (n, 6, 8): un one-hot por posición
return U.reshape(n, T * len(V)), U.sum(1), y # concatenarlos, o sumarlos
C_ent, B_ent, Y_ent = corpus(600, [0, 1, 2, 3]) # «no» en las posiciones 1-4
C_pru, B_pru, Y_pru = corpus(300, [4, 5]) # en el examen, sólo en las 5-6
def entrena(X_ent, X_pru, pasos=1500, eta=0.5, d1=16):
r = np.random.default_rng(1)
W1, b1 = r.normal(size=(d1, X_ent.shape[1])) * 0.1, np.zeros(d1)
W2, b2 = r.normal(size=(1, d1)) * 0.1, np.zeros(1)
W1_ini = W1.copy()
def salida(X):
H = np.tanh(X @ W1.T + b1)
return H, 1.0 / (1.0 + np.exp(-(H @ W2.T + b2)))
def acierto(X, Y):
return float(((salida(X)[1] >= 0.5) == (Y == 1.0)).mean())
for _ in range(pasos): # el bucle del bloque anterior, sin tocar
H, P = salida(X_ent)
D2 = P - Y_ent
D1 = (D2 @ W2) * (1.0 - H ** 2)
W1 -= eta * (D1.T @ X_ent) / len(X_ent)
b1 -= eta * D1.sum(0) / len(X_ent)
W2 -= eta * (D2.T @ H) / len(X_ent)
b2 -= eta * D2.sum(0) / len(X_ent)
return acierto(X_ent, Y_ent), acierto(X_pru, Y_pru), W1 - W1_ini, salida(X_pru)[1]
ac_e, ac_p, mov, P_pru = entrena(C_ent, C_pru)
print("concatenación entrenamiento %.3f prueba %.3f" % (ac_e, ac_p))
print("bolsa de palabras entrenamiento %.3f prueba %.3f" % entrena(B_ent, B_pru)[:2])
print("ŷ medio de las 150 de prueba que llevan «no»: %.4f" % P_pru[Y_pru == 1.0].mean())
print("\ncuánto se ha movido la columna de «no» en cada bloque:")
for t in range(T):
print(" posición %d: %.6f" % (t + 1, np.linalg.norm(mov[:, t * len(V) + NO])))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
La primera línea es el fallo: sobre lo que ha visto y sobre lo que no. Ese no es una moneda al aire. Las ciento cincuenta secuencias del examen que no llevan no las acierta todas, y las ciento cincuenta que sí lo llevan las falla todas, porque a sus ojos son idénticas a las primeras. La tercera línea dice hasta qué punto: el medio de las ciento cincuenta positivas es . No está dudando entre las dos respuestas; está segura y equivocada.
La segunda línea es la incómoda. La bolsa de palabras —la representación que este bloque ha venido a sustituir, la que no distingue un orden de otro— acierta el en el examen desplazado. Su ventaja es exactamente lo que le falta a la otra: tiene una coordenada para no, que recoge las ocurrencias de las seis posiciones a la vez, así que un peso ajustado con lo que pasó en la posición 2 sirve tal cual cuando no aparece en la 5. Concatenar ganó el orden y perdió eso.
La última salida es la fórmula, verificada. La columna de no se ha movido unas unidades en los bloques 1 a 4, y en los bloques 5 y 6 —cero exacto, no cero redondeado—, porque su gradiente fue la matriz nula en los seiscientos ejemplos y en los mil quinientos pasos. Cambia la inicialización y la cifra de los cuatro primeros bloques cambia; los dos últimos siguen dando cero, y el acierto de prueba sigue dando .
Comprueba tu intuición
Cuatro preguntas: cuántos pesos pide la concatenación, por qué la posición nueva es invisible, qué cuestan el padding y el truncamiento, y qué se puede y qué no se puede concluir del hecho de que la bolsa de palabras gane este examen.
Un MLP concatenado con posiciones, embeddings de y una capa oculta de neuronas. ¿Cuántos pesos tiene ?
A margin of ±0 is accepted.
La celda entrena con no en las cuatro primeras posiciones y examina con no en las dos últimas. El acierto de prueba se queda en , y no lo mueven ni más pasos, ni más neuronas ocultas, ni otra tasa de aprendizaje. ¿Cuál es la razón?
Marca todo lo que sea cierto de un MLP que recibe la concatenación de posiciones.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
En el experimento de la celda, la bolsa de palabras acierta el examen desplazado y la concatenación no. ¿Autoriza eso a concluir que la bolsa de palabras es la mejor de las dos representaciones?
Las dos cosas que esta lección ha medido son la misma cosa. La longitud fija obliga a truncar y a rellenar porque la primera capa tiene un bloque por posición y ninguno de más; y lo aprendido en una posición no llega a las demás porque cada bloque se ajusta con lo suyo. Las dos salen de haber puesto la secuencia entera delante de la red de una vez, como si fuera un punto de con muchas coordenadas, y ninguna se arregla desde dentro de esa idea: ni con más neuronas, ni con más pasos, ni con un mayor, que empeora las dos.
Lo que hace falta es una red en la que la posición 5 multiplique por los mismos pesos que la posición 3, de manera que ajustarlos con una ocurrencia los deje ajustados para todas. Eso obliga a dos cosas a la vez: leer la secuencia una posición cada vez en lugar de toda de golpe, y guardar en algún sitio lo ya leído —porque si la matriz es la misma en todas las posiciones, el orden no puede estar guardado en ella—. Eso es la siguiente lección, sobre la red neuronal recurrente (recurrent neural network, RNN) más simple que hay: una sola matriz aplicada veces, un estado que pasa de un paso al siguiente, y una entrada que ya no tiene que medir lo mismo en todos los documentos.
Further reading1 source · 1 book
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Deep Learning, cap. 10: Sequence Modeling: Recurrent and Recursive Nets
Su §10.1 es el argumento de esta lección: con pesos propios por posición, una red no generaliza a longitudes nuevas ni traslada lo aprendido de una posición a otra. Lo cierra con el mismo ejemplo, una palabra que cambia de sitio.