Descenso de gradiente
30 min de lectura
Ya se puede escribir entera la pregunta del entrenamiento, y escribirla no la resuelve. La lección anterior, sobre funciones de pérdida, dejó puesta la última pieza: una regla que compara lo que la red contesta con lo que debía contestar y lo resume en un número. Buscar los parámetros que lo hacen mínimo ya es una pregunta bien planteada, y sigue sin haber forma de contestarla probando. La red de la lección sobre el forward pass guarda números; con un vocabulario de veinte mil entradas y una capa oculta de neuronas son , y cada uno de ellos es un número real.
Hay, en cambio, una pregunta mucho más pequeña que sí se contesta. Coge uno solo de esos parámetros, súbelo una centésima, deja los otros quietos y vuelve a evaluar la pérdida: si ha subido, ese parámetro iba en la dirección equivocada, y si ha bajado, iba bien. Es una respuesta local —no dice dónde está el mínimo, dice hacia dónde queda el suelo desde aquí— y, llevada al límite, tiene nombre desde el siglo XVII: es la derivada parcial de la pérdida respecto de ese parámetro. Sondear los cuesta evaluaciones. Derivar los da sin sondear ninguno, y para una neurona la cuenta cabe en una línea; conseguirla con capas ocultas de por medio ocupa el resto del bloque.
Antes de derivar nada, mira el terreno. Fija todos los parámetros de una red menos dos: la pérdida pasa a ser una función de esos dos números, y una función de dos variables se dibuja como una superficie con valles y laderas. La del explorable no sale de ninguna red: me la he inventado para que quepa en un plano y tenga dos fondos en vez de uno.
El descenso no compara los dos valles, porque no los ve. Desde donde está tiene acceso a una sola cosa —cuánto cambia la pérdida al moverse un poco en cada dirección— y con eso decide el paso siguiente.
El gradiente apunta cuesta arriba, y por eso se resta
Llamemos a todos los parámetros de la red a la vez —los pesos y los sesgos de todas las capas, apilados en un solo vector de coordenadas— y a la pérdida sobre el batch. El gradiente es el vector que reúne una derivada parcial por parámetro, . Cada una de sus coordenadas dice a qué ritmo cambia la pérdida cuando se mueve un parámetro y los demás se quedan quietos: lo que medía el sondeo de una centésima, ahora exacto y sin evaluar nada dos veces. Para la neurona de la lección sobre la neurona artificial, con sus pesos y su sesgo , son números:
Lo primero que hay que ver es que tiene la misma forma que . No es casualidad: es lo que permite restar el uno del otro.
Falta justificar por qué se resta ése y no otra cosa. Toma una dirección cualquiera, un vector de longitud , y pregunta cuánto cambia la pérdida al avanzar un poco en ella. Eso es la derivada direccional, que para una función con derivadas parciales es un producto escalar,
que está acotado. La desigualdad de Cauchy-Schwarz da , con igualdad únicamente cuando es paralela al gradiente. De todas las direcciones, entonces, la del gradiente es la que más sube la pérdida y la contraria la que más la baja. Ninguna otra baja más por unidad de paso.
De ahí sale la regla entera. Partiendo de unos parámetros iniciales , cada paso resta el gradiente escalado:
donde el subíndice cuenta pasos — son los parámetros después de cinco pasos, no la quinta coordenada de nada— y es la tasa de aprendizaje (learning rate, que es como la vas a encontrar escrita en cualquier librería), el único número de la fórmula que eliges tú. Eso es el descenso de gradiente, y no tiene más piezas.
Que repetirlo baje la pérdida se ve en la aproximación de primer orden. Cerca de la pérdida se parece a su plano tangente, , y el paso del descenso es precisamente , así que
Lo que se resta es una norma al cuadrado, luego no es negativo: mientras el gradiente no sea nulo, el paso baja. Y toda la deuda está en el signo de esa ecuación, que es un : la igualdad es del plano tangente, no de la pérdida, y sólo vale «cerca». Cuánto es cerca lo decide .
Una concesión, dicha en voz alta: el batch de esta lección son las diez reseñas enteras, así que cada paso mira todos los ejemplos antes de mover nada. Partirlo en trozos y dar un paso por trozo hace falta cuando los conjuntos son grandes, y el del curso tiene diez ejemplos.
Un paso demasiado largo sube la pérdida
Para ver qué hace conviene una pérdida cuya cuenta salga entera, así que tomemos la más pequeña que existe: un solo parámetro y , con el mínimo en y . La regla queda
una multiplicación por la misma constante en cada paso. Iterándola desde ,
y una potencia tiende a cero si y sólo si la base mide menos que . La condición completa es , es decir , y el propio factor cuenta lo que pasa en cada tramo:
| qué hace | ||
|---|---|---|
| entre y | se encoge sin cambiar de signo | |
| cae en el mínimo en un solo paso | ||
| entre y | alterna de signo y se encoge | |
| alterna entre y para siempre | ||
| menor que | alterna y crece: diverge |
Ese del límite es de esta parábola y no de todas. Con la derivada pasa a ser , el factor a y la condición a : cuanto más se curva la pérdida, más corto tiene que ser el paso. Lo incómodo es que una red se curva de manera distinta en cada dirección, y es un solo número para todas.
El explorable arranca justo en ese problema, : casi plano a lo largo de y muy empinado a lo largo de . La cuenta de arriba, coordenada a coordenada, pide para la primera y para la segunda, y manda la segunda.
Con —el valor con el que arranca— el factor de la coordenada empinada vale exacto, y la trayectoria hace en dos dimensiones lo que la cuarta fila de la tabla hace en una: alternar sin avanzar. El cuenco del primer botón es convexo —un solo fondo, así que baje quien baje llega al mismo sitio— y aguanta el deslizador entero: su dirección más empinada sólo pide . Que un mismo paso sirva para una dirección y no para la otra tiene arreglos con nombre propio —momentum, Adam—, y quedan fuera de este curso.
Con una sola capa, el gradiente sale de una línea
Falta bajar de a un parámetro concreto, y con la neurona de las reseñas la cuenta está medio hecha. Es una neurona con sigmoide en la salida y entropía cruzada como pérdida, que es justo el par que la lección anterior derivó: para un ejemplo, . Lo único que falta es cómo depende de cada peso, y eso lo da la definición misma de la neurona, , donde cada aparece una sola vez, multiplicado por :
Encadenando las dos derivadas,
que en forma vectorial es . Esa fórmula dice dos cosas y conviene separarlas. El error es un único número que multiplica a todas las coordenadas por igual: fija cuánto se corrige. Y decide el reparto: una entrada del vocabulario que no aparece en la reseña tiene y su peso no se mueve ni una milésima, y una que aparece dos veces tira el doble. La neurona sólo aprende sobre lo que ha visto.
La pérdida del batch es la media de las pérdidas por ejemplo, así que su gradiente es la media de los gradientes:
donde la segunda forma es la primera escrita como producto: con las predicciones y las etiquetas son columnas de números, tiene forma , y el resultado es la columna de coordenadas que hace falta. El sesgo sale de la misma suma sin el : es .
Todo esto ha costado dos derivadas porque depende de directamente: entre un peso y la pérdida hay una suma y una sigmoide, y nada más.
Descenso a mano, y una neurona que aprende sola
La primera celda es la parábola de arriba, sin red y sin NumPy: cinco pasos desde con las tres tasas de la figura, y al lado la forma cerrada. Ejecútala y compara los dos bloques.
def perdida(w):
return w * w
def desciende(w0, eta, pasos):
w = w0
trayectoria = [w]
for _ in range(pasos):
w = w - eta * 2.0 * w # w <- w - eta * dL/dw
trayectoria.append(w)
return trayectoria
w0 = 1.0
print("%6s %7s %s" % ("eta", "factor", " ".join(("w%d" % t).rjust(9) for t in range(6))))
for eta in [0.10, 0.40, 1.05]:
print("%6.2f %+7.2f %s"
% (eta, 1 - 2 * eta, " ".join("%9.5f" % w for w in desciende(w0, eta, 5))))
print()
# La forma cerrada da los mismos números sin dar un solo paso.
for eta in [0.10, 0.40, 1.05]:
bucle = desciende(w0, eta, 5)
cerrada = [(1 - 2 * eta) ** t * w0 for t in range(6)]
iguales = all(abs(a - b) < 1e-12 for a, b in zip(bucle, cerrada))
print("eta = %.2f bucle y formula coinciden: %-5s perdida final %10.7f"
% (eta, iguales, perdida(bucle[-1])))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Las tres filas son las tres de la figura. Con el factor vale y cinco pasos dejan en : va bien y va despacio. Con el factor es y en dos pasos ya vale . Con el factor es , los signos alternan, las magnitudes crecen y la pérdida final vale : dos veces y media la del punto de partida, después de cinco pasos dedicados a bajarla.
La segunda celda entrena de verdad. La neurona es la de la lección sobre la neurona artificial, con las mismas diez reseñas y el mismo vocabulario, y esta vez sus pesos empiezan en cero: con y la sigmoide contesta a las diez, que es la peor de las tres redes que ordenó la lección anterior. No hay un solo número al azar, así que verás estos valores y no otros. Ejecútala y mira caer la pérdida.
# --- Las diez reseñas de siempre, su vocabulario y sus etiquetas.
resenas = [
"la película es divertida y la recomiendo",
"la película es lenta y aburrida",
"buena película la recomiendo",
"mala película muy lenta",
"divertida y buena",
"la película es mala",
"aburrida y lenta",
"la recomiendo",
"buena película pero lenta",
"divertida y la recomiendo",
]
y = np.array([1., 0., 1., 0., 1., 0., 0., 1., 0., 1.])
V = ["aburrida", "buena", "divertida", "la", "lenta", "mala", "película", "recomiendo"]
X = np.array([[t.split().count(e) for e in V] for t in resenas], dtype=float)
def entropia_cruzada(y_hat):
p = np.clip(y_hat, 1e-12, 1.0 - 1e-12)
return -np.mean(y * np.log(p) + (1.0 - y) * np.log(1.0 - p))
w = np.zeros(len(V)) # todos los pesos a cero
b, eta, B = 0.0, 1.0, len(y) # sesgo, tasa de aprendizaje, tamaño del batch
for paso in range(201):
y_hat = 1.0 / (1.0 + np.exp(-(X @ w + b))) # forward pass
if paso % 25 == 0:
print("paso %3d pérdida %.4f aciertos %2d de 10"
% (paso, entropia_cruzada(y_hat), int(((y_hat >= 0.5) == (y == 1.)).sum())))
if paso == 200:
break
error = y_hat - y # (B,)
w -= eta * (X.T @ error) / B
b -= eta * float(error.sum()) / B
a_mano = np.array([-1., 1., 1., 0., -1., -1., 0., 1.])
print("\ncon los pesos que puse a mano:",
"%.4f" % entropia_cruzada(1.0 / (1.0 + np.exp(-(X @ a_mano)))))
for entrada, peso in sorted(zip(V, w), key=lambda par: -par[1]):
print(" %-11s %+6.3f" % (entrada, peso))
print(" %-11s %+6.3f" % ("(sesgo)", b))
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 punto de partida: pérdida , que es , y cinco aciertos de diez, que es lo que saca cualquier red que conteste lo mismo a las diez reseñas. Veinticinco pasos después la pérdida vale y los aciertos son diez, y en el paso queda en . Compárala con el de los ocho pesos que escribí yo mirando las reseñas: el descenso pasa por delante de mi versión antes del paso y sigue bajando. Nadie le ha dicho a la neurona qué palabra significa qué.
Los pesos finales lo confirman. El mayor, , se lo lleva recomiendo, y el menor, , es el de lenta. Pero mira los dos que enseñan el límite de un conjunto de diez ejemplos: película acaba en y la en , y ninguna de las dos opina de nada. La primera sale en cuatro reseñas negativas y en dos positivas, la segunda al contrario, y el descenso hace lo que se le pidió, que es bajar la pérdida sobre estas diez reseñas y no sobre el idioma. Con diez ejemplos, una neurona no distingue una opinión de una coincidencia.
Comprueba tu intuición
Cuatro preguntas: un descenso a mano, por qué el paso va en esa dirección, qué garantiza el método y qué no, y una actualización en NumPy.
El descenso corre sobre partiendo de , con . ¿Cuánto vale ?
Se acepta un margen de ±0.01.
De todas las direcciones en las que se pueden mover los parámetros, ¿por qué el paso va en la de ?
Marca todo lo que sea cierto sobre lo que el descenso de gradiente garantiza y lo que no.
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Un paso de descenso sobre una neurona de dos entradas, con los pesos empezando en cero y sin sesgo. ¿Qué imprime?
import numpy as np
X = np.array([[1., 0.], [0., 2.]])
y = np.array([1., 0.])
w = np.zeros(2)
y_hat = 1.0 / (1.0 + np.exp(-(X @ w)))
w = w - 1.0 * (X.T @ (y_hat - y)) / len(y)
print(np.round(w, 3).tolist())
El método está completo y la neurona que acaba de aprender tiene una sola capa. Eso no es un detalle del ejemplo, es la condición que ha hecho posible la última sección: entre un peso y la pérdida hay dos pasos, y dos derivadas encadenadas cierran la cuenta. Pon una capa oculta en medio y el camino se ramifica. Un peso de la primera capa mueve la salida de su neurona oculta; esa salida entra en todas las neuronas de la capa siguiente, y cada una contribuye a la pérdida por su cuenta. La regla del descenso no cambia ni una letra. Lo que deja de estar disponible es el gradiente, y sin él no hay nada que restar.
La regla de la cadena que hemos usado dos veces, la de una variable, no cubre ese caso: está escrita para una cantidad que llega a la siguiente por un único camino. Cómo se deriva una composición cuando las variables intermedias son vectores, y qué objeto sustituye a la derivada cuando hay muchas entradas y muchas salidas a la vez, es la siguiente lección, sobre la regla de la cadena. Convertirla después en un procedimiento que recorra una red entera sin repetir ninguna cuenta ocupa el resto del bloque.
Para profundizar2 fuentes · 1 paper, 1 libro
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.
- Deep Learning, cap. 4: Numerical Computation
Su §4.3 explica por qué el paso va en contra del gradiente (la derivada direccional) y cómo la curvatura —el número de condición del hessiano— fija el paso más largo que no diverge: el x² + 20y² de la lección dicho en general.
- An overview of gradient descent optimization algorithms
Sigue donde la lección lo deja: momentum, Adam y los demás métodos que hacen que una sola tasa de aprendizaje sirva para todas las direcciones, y el reparto del batch en trozos. Es un repaso; no desarrolla los métodos desde cero.