Explicación de la retropropagación para principiantes (Parte 3): cómo funciona realmente la retropropagación

¡atrás!

En primer lugar, muchas gracias por la respuesta a las dos primeras partes de esta serie.

Se siente bien que muchos de ustedes los hayan encontrado útiles.

Como siempre, si tienes alguna idea, pregunta o sugerencia mientras lees, me encantaría escuchar tu perspectiva.

Ahora, retomemos donde lo dejamos en la parte 2.

Calculamos el gradiente para w1w_1 usando la regla de la cadena.

[
frac{parcial L}{parcial w_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{z_1 parcial}{w_1 parcial}
]

Obtuvimos la misma ecuación que habíamos derivado previamente usando la diferenciación clásica en la Parte 1.

[
frac{parcial L}{parcial w_1}
=
-2(y-hat{y})
cdot
w_3
cdot
mathrm{ReLU}'(w_1x+b_1)
cdot
incógnita
]

Entonces comprendimos lo importante que es la regla de la cadena.

Ahora, ¿qué pasa con los gradientes de los otros parámetros?

[
b_1,; w_2,; b_2,; w_3,; w_4,; b_3
]

Ya hemos visto el proceso completo para w1w_1​. Por lo tanto, generalmente pensamos en repetir los mismos pasos que utilizan la regla de la cadena para los parámetros restantes.

Si echamos un vistazo a todas las ecuaciones de la regla de la cadena para todos los parámetros, podemos observar que muchas de las derivadas parciales aparecen más de una vez.

[
frac{parcial L}{parcial w_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{z_1 parcial}{w_1 parcial}
]

[
frac{parcial L}{parcial b_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{parcial z_1}{parcial b_1}
] [
frac{parcial L}{parcial w_2}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_2}
cdot
frac{parcial a_2}{parcial z_2}
cdot
frac{z_2 parcial}{w_2 parcial}
] [
frac{parcial L}{parcial b_2}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_2}
cdot
frac{parcial a_2}{parcial z_2}
cdot
frac{z_2 parcial}{b_2 parcial}
] [
frac{parcial L}{parcial w_3}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial w_3}
] [
frac{parcial L}{parcial w_4}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial w_4}
] [
frac{parcial L}{parcial b_3}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial b_3}
]

consideremos

[b_1]

La ecuación de la regla de la cadena para este parámetro es

[
frac{parcial L}{parcial b_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{parcial z_1}{parcial b_1}
]

Ahora, comparemos esto con la ecuación de la regla de la cadena de w1w_1​.

[
frac{parcial L}{parcial w_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{z_1 parcial}{w_1 parcial}
]

Podemos observar que la mayor parte de la cadena es en realidad idéntica.

Ambas ecuaciones contienen

[
frac{partial L}{partial hat{y}}
]

También contienen

[
frac{partial hat{y}}{partial a_1}
]

y

[
frac{parcial a_1}{parcial z_1}
]

La única diferencia es la derivada parcial final.

Para w1w_1​, el término final es

[
frac{z_1 parcial}{w_1 parcial}
]

mientras que para b1, el término final es

[
frac{parcial z_1}{parcial b_1}
]

Ahora, si calculamos el gradiente para b1, las primeras tres derivadas parciales se calculan dos veces, aunque sus valores sean exactamente iguales.

El mismo patrón aparece cuando calculamos los gradientes para

[
w_2,; b_2,; w_3,; w_4,; b_3
]

En cada caso, muchas de las derivadas parciales intermedias aparecen más de una vez.

Llegados a este punto, generalmente tenemos una pregunta.

¿Por qué volvemos a calcular las mismas derivadas parciales intermedias que ya calculamos una vez?

Si seguimos así, acabaremos haciendo muchos cálculos innecesarios.

Para nuestra pequeña red neuronal, esto puede no parecer un gran problema, pero en el mundo real tenemos redes neuronales con millones de parámetros.

Si procedemos de la misma manera para redes neuronales enormes, el resultado es un aumento del tiempo y los recursos computacionales necesarios para entrenar el modelo, lo que encarece mucho el proceso de entrenamiento.

¿Qué podemos hacer aquí?

La única idea que nos viene a la mente es, en lugar de recalcular los mismos valores, ¿por qué no reutilizamos esos valores cuando los necesitemos?

Ésta es exactamente la idea detrás de la retropropagación.

No utiliza una forma diferente para calcular los gradientes, sino que se basa completamente en la regla de la cadena.

Ahora conocemos la idea detrás de la retropropagación.

Entonces podríamos pensar, cuando calculamos el gradiente para w1w_1​,

[
frac{parcial L}{parcial w_1}
=
frac{partial L}{partial hat{y}}
cdot
frac{partial hat{y}}{partial a_1}
cdot
frac{parcial a_1}{parcial z_1}
cdot
frac{parcial z_1}{parcial w_1}.
]

almacenamos las derivadas parciales intermedias

[
frac{partial L}{partial hat{y}},
qquad
frac{partial L}{partial a_1},
qquad
frac{partial L}{partial z_1}
]

A continuación, mientras se calcula el gradiente de

[frac{partial L}{partial b_1}]

simplemente podemos reutilizar los valores previamente calculados de

[
frac{partial L}{partial hat{y}},
qquad
frac{partial L}{partial a_1},
qquad
frac{partial L}{partial z_1}
]

De manera similar, continuamos calculando los gradientes de los parámetros restantes, almacenando derivadas parciales intermedias siempre que sea posible y reutilizándolas cuando sea necesario.

¿Qué opinas sobre este enfoque?

Si este es el enfoque, ¿por qué se llama retropropagación?

Aquí, estamos trabajando con una pequeña red neuronal para comprender la idea detrás de la retropropagación.

Para nuestra pequeña red neuronal, se puede utilizar este enfoque o podemos decir que es manejable.

Pero ¿qué pasa si tenemos una red neuronal con miles o incluso millones de parámetros?

Se vuelve cada vez más difícil decidir qué valores intermedios deben almacenarse, cuándo deben reutilizarse y en qué orden deben calcularse los gradientes.

Pero ¿por qué estamos siquiera pensando en esto?

Después de todo, en la práctica, ¿quién calcula estos gradientes manualmente?

¿No escribimos simplemente código?

Sí, lo hacemos.

Si implementamos el enfoque que acabamos de comentar para esta red neuronal, funciona.

Ahora digamos que construimos una red neuronal diferente con una arquitectura diferente.

¿Deberíamos rediseñar nuevamente nuestro procedimiento de cálculo de gradiente?

A medida que las redes neuronales se vuelven más complejas, cambiar continuamente nuestra implementación se vuelve difícil.

¿Qué podemos observar de esto?

No queremos rediseñar la lógica del procedimiento de cálculo del gradiente cada vez que cambia la arquitectura.

En cambio, necesitamos un algoritmo general que calcule gradientes de manera eficiente para cualquier red neuronal diferenciable, independientemente de su arquitectura.

Esto es exactamente lo que nos proporciona la retropropagación.

Nos proporciona un algoritmo que significa una forma sistemática de calcular gradientes de manera eficiente para redes neuronales de diferentes tamaños y arquitecturas.

Antes de aprender cómo implementar la retropropagación en el código, primero comprendamos las matemáticas completas detrás de esto y veamos cómo funciona paso a paso.

Imagen por autor

Antes de continuar, intentemos recordar lo que pasó durante el pase hacia adelante.

Ya sabemos que en el paso directo, la información viaja desde la capa de entrada a la capa de salida.

En cada capa, la red neuronal realiza un pequeño cálculo.

Veamos esos cálculos uno por uno.

Comenzamos con la función de entrada [x]

La primera neurona oculta calculada

[
z_1=w_1x+b_1
]

La segunda neurona oculta calculada

[
z_2=w_2x+b_2
]

Hasta este punto, hemos calculado las combinaciones lineales para ambas neuronas ocultas.

Luego, las salidas de las neuronas ocultas pasan a través de la función de activación ReLU.

Para la primera neurona oculta, tenemos

[
a_1=mathrm{ReLU}(z_1)
]

De manera similar, para la segunda neurona oculta

[
a_2=mathrm{ReLU}(z_2)
]

Estos valores de activación son ahora las entradas a la capa de salida.

Utilizando las salidas de ambas neuronas ocultas, la neurona de salida calculó la predicción final.

[
hat{y}=w_3a_1+w_4a_2+b_3
]

En este punto, obtuvimos nuestro valor previsto.

Finalmente, comparamos el valor predicho con el valor real utilizando la función de pérdida del error cuadrático medio (MSE).

[
L=frac{1}{n}sum_{i=1}^{n}(y_i-hat{y}_i)^2
]

Esto nos dio la pérdida, de la cual entendimos qué tan lejos está nuestra predicción del valor real.

Ahora, aquí hay algo nuevo en lo que deberíamos centrarnos.

Una vez realizado el pase hacia adelante, tenemos los valores de

[
z_1,cuadrado
a_1,cuadrado
z_2,cuadrado
a_2,cuadrado
hat{y},
quadtext{y}quad
l
]

para cada ejemplo de entrenamiento en el conjunto de datos.

Reutilizaremos todos estos valores durante el pase hacia atrás.

Ahora el pase hacia adelante está completo y tenemos la predicción y la pérdida correspondiente.

El siguiente paso es determinar cómo cada peso y sesgo contribuyó a esta pérdida.

Una vez que sabemos cómo cambia la pérdida con respecto a cada parámetro, podemos actualizarlos en una dirección que reduzca la pérdida.

Aquí es exactamente donde comienza el pase hacia atrás.

Nota

Pase hacia atrás: la fase de ejecución durante el entrenamiento.

Retropropagación: el algoritmo utilizado para calcular gradientes.

Es hora de hacer retropropagación.

Ya hemos visto que en las partes anteriores de esta serie, siempre que queríamos calcular el gradiente de un parámetro, como w1w_1​, comenzamos desde ese parámetro y aplicamos la regla de la cadena hasta que finalmente llegamos a la función de pérdida.

La retropropagación aborda el mismo problema de manera diferente.

En lugar de comenzar desde un parámetro, comienza desde la función de pérdida y retrocede sistemáticamente a través de la red.

Como hemos visto que la pérdida depende directamente de la predicción, la primera cantidad calculada durante el paso hacia atrás es

[
frac{partial L}{partial hat{y}}
]

Antes de calcular los gradientes de cualquier peso o sesgo, primero debemos saber cómo cambia la pérdida con respecto a la predicción.

Imagen por autor

Podrías pensar que ya lo hemos calculado en partes anteriores, mientras encontrábamos el gradiente de w1w_1​.

La diferencia es que ahora lo estamos resolviendo usando el algoritmo de retropropagación, que siempre comienza aquí con pérdida.

Conocemos nuestra función de pérdida.

[
L=frac{1}{n}sum_{i=1}^{n}(y_i-hat{y}_i)^2
]

y en partes anteriores ya calculamos esto

[
frac{partial L}{partialhat{y}_i}
=
-frac{2}{n}(y_i-hat{y}_i)
]

Hemos calculado la primera pendiente durante el pase hacia atrás.

Ahora, en el algoritmo de retropropagación, una vez que se calcula un gradiente, no lo volvemos a calcular.

En cambio, lo conservamos y lo reutilizamos donde sea necesario mientras retrocedemos a través de la red.

Esto se sigue en toda la red.

Ahora que hemos calculado el primer gradiente, sabemos cómo cambia la pérdida con respecto al valor previsto.

Nuestro próximo objetivo es determinar cómo cambia la pérdida con respecto a cada peso y sesgo para que podamos actualizar estos parámetros para reducir la pérdida.

Ahora retrocedamos un paso a través de nuestra red. La siguiente capa a la que llegamos es la capa de salida.

Aquí, necesitamos encontrar cómo cambia la pérdida con respecto a w3w_3​, w4w_4 y b3b_3.

Tenemos la ecuación de salida.

[
hat{y}=w_3a_1+w_4a_2+b_3
]

Comencemos con w3w_3​.

Usando la regla de la cadena,

[
frac{parcial L}{parcial w_3}
=
frac{partial L}{partialhat{y}}
cdot
frac{partialhat{y}}{partial w_3}
]

Como ya calculamos esto en detalle en partes anteriores, consideremos simplemente la solución final, ya que nuestro objetivo principal es comprender el proceso.

[
frac{partialhat{y}}{partial w_3}
=
a_1
]

Del paso anterior, ya almacenamos

[
frac{partial L}{partialhat{y}}
=
-frac{2}{n}(y-hat{y})
]

Sustituyendo ambos resultados obtenemos

[
frac{parcial L}{parcial w_3}
=
-frac{2}{n}(y-hat{y})a_1
]

De manera similar, para w4w_4 y b3b_3

[
frac{parcial L}{parcial w_4}
=
-frac{2}{n}(y-hat{y})a_2
]

y

[
frac{parcial L}{parcial b_3}
=
-frac{2}{n}(y-hat{y})
]

Ahora hemos calculado los gradientes de los parámetros en la capa de salida.

Ahora retrocedemos un paso hasta la capa oculta.

Es hora de calcular los gradientes de los parámetros en la capa oculta.

[
w_1,qquad b_1,qquad w_2,qquad text{y}qquad b_2
]

Tenemos dos neuronas ocultas en nuestras capas ocultas.

Primero centrémonos en la primera neurona oculta.

Durante el paso hacia atrás, el gradiente se propaga a través de la siguiente secuencia:

[
l
flecha derecha
sombrero{y}
flecha derecha
a_1
flecha derecha
z_1
flecha derecha
w_1,; b_1
]

Podemos observar que antes de alcanzar los parámetros [w_1] y [b_1] el gradiente primero debe pasar por la función de activación.

Ya sabemos por el pase adelantado que

[
a_1=mathrm{ReLU}(z_1)
]

Por lo tanto, el siguiente paso es determinar cómo cambia la pérdida con respecto a la activación de la capa oculta.

[
a_1
]

Una vez que hemos calculado este gradiente, el siguiente paso es retroceder a través de la activación de ReLU y ver cómo cambia la pérdida con respecto a
[
z_1
]

Después de eso, podemos calcular los gradientes de [w_1] y [b_1]

De manera similar, para la segunda neurona oculta, el gradiente sigue

[
l
flecha derecha
sombrero{y}
flecha derecha
a_2
flecha derecha
z_2
flecha derecha
w_2,; b_2
]

Como ambas neuronas ocultas siguen exactamente la misma secuencia de pasos, derivaremos los gradientes para la primera neurona oculta.

Luego se puede aplicar el mismo procedimiento a la segunda neurona oculta.

Comencemos calculando el gradiente de activación de la capa oculta.

[
a_1
]

Usando la regla de la cadena,

[
frac{parcial L}{parcial a_1}
=
frac{partial L}{partialhat{y}}
cdot
frac{partialhat{y}}{partial a_1}
]

Por los pasos anteriores ya sabemos

[
frac{partial L}{partialhat{y}}
=
-frac{2}{n}(y-hat{y})
]

y de la ecuación de la capa de salida,

[
frac{partialhat{y}}{partial a_1}
=
w_3
]

Sustituyendo estos en la regla de la cadena, obtenemos

[
frac{parcial L}{parcial a_1}
=
-frac{2}{n}(y-hat{y})w_3
]

Nos dice qué tan sensible es la pérdida a los cambios en la activación de la capa oculta,

[
a_1
]

Ahora que sabemos cómo cambia la pérdida con respecto a [a_1] podemos continuar retrocediendo un paso a través de la activación de ReLU.

Hasta ahora, hemos determinado cómo cambia la pérdida con respecto a la activación de la capa oculta.

[
a_1
]

Pero nuestro objetivo es calcular los gradientes de [w_1] y [b_1]

Para eso, primero debemos retroceder un paso a través de la activación de ReLU.

Por el pase adelantado sabemos que

[
a_1=mathrm{ReLU}(z_1)
]

Entonces, nuestro próximo objetivo es determinar cómo cambia la pérdida con respecto a

[
z_1
]

Usando la regla de la cadena,

[
frac{partial L}{partial z_1}
=
frac{parcial L}{parcial a_1}
cdot
frac{parcial a_1}{parcial z_1}
]

ya tenemos

[
frac{parcial L}{parcial a_1}
]

Ahora sólo nos falta calcular

[
frac{parcial a_1}{parcial z_1}
]

Sabemos lo que hace ReLU.

Si la entrada a ReLU es negativa, su derivada es

[
frac{partial a_1}{partial z_1}=0
]

Si la entrada a ReLU es positiva, su derivada es

[
frac{parcial a_1}{parcial z_1}=1
]

Sustituyendo esto en la regla de la cadena, obtenemos

[
frac{partial L}{partial z_1}
=
-frac{2}{n}(y-hat{y})w_3frac{partial a_1}{partial z_1}
]

Aquí, simplemente escribimos esta derivada como

[
mathrm{ReLU}'(z_1)
]

Sustituyendo esto en la regla de la cadena, obtenemos

[
frac{partial L}{partial z_1}
=
-frac{2}{n}(y-hat{y})w_3mathrm{ReLU}'(z_1)
]

¿Qué nos dice esta ecuación?

Nos dice qué tan sensible es la pérdida a los cambios en

[
z_1
]

Ahora que sabemos cómo cambia la pérdida con respecto a [z_1] finalmente podemos calcular los gradientes de [w_1] y [b_1]

Del pase adelantado tenemos

[
z_1=w_1x+b_1
]

Comencemos con [w_1]

Usando la regla de la cadena,

[
frac{parcial L}{parcial w_1}
=
frac{partial L}{partial z_1}
cdot
frac{z_1 parcial}{w_1 parcial}
]

ya calculamos

[
frac{partial L}{partial z_1}
]

Entonces, sólo necesitamos calcular

[
frac{z_1 parcial}{w_1 parcial}
]

como

[
z_1=w_1x+b_1
]

obtenemos

[
frac{z_1 parcial}{w_1 parcial}=x
]

Sustituyendo estos en la regla de la cadena, obtenemos

[
frac{parcial L}{parcial w_1}
=
-frac{2}{n}(y-hat{y})w_3mathrm{ReLU}'(z_1)x
]

Esto ya lo hemos visto en la parte 2, que nos dice cómo cambia la pérdida con respecto a

[
w_1
]

A continuación, necesitamos calcular el gradiente de [b_1]

Usando la regla de la cadena,

[
frac{parcial L}{parcial b_1}
=
frac{partial L}{partial z_1}
cdot
frac{parcial z_1}{parcial b_1}
]

Una vez más, reutilizamos el gradiente calculado previamente.

[
frac{partial L}{partial z_1}
]

Desde

[
z_1=w_1x+b_1
]

tenemos

[
frac{z_1 parcial}{b_1 parcial}=1
]

Por lo tanto,

[
frac{parcial L}{parcial b_1}
=
-frac{2}{n}(y-hat{y})w_3mathrm{ReLU}'(z_1)
]

Ahora, siguiendo el mismo procedimiento para la segunda neurona oculta, obtenemos

[
frac{parcial L}{parcial w_2}
=
-frac{2}{n}(y-hat{y})w_4mathrm{ReLU}'(z_2)x
]

y

[
frac{parcial L}{parcial b_2}
=
-frac{2}{n}(y-hat{y})w_4mathrm{ReLU}'(z_2)
]

Finalmente, hemos calculado los gradientes de cada parámetro de nuestra red neuronal utilizando el algoritmo de retropropagación.

Si miramos las partes anteriores de la serie, podemos observar que las matemáticas no cambiaron.

La regla de la cadena sigue siendo la base de la propagación hacia atrás.

Pero la forma en que aplicamos la regla de la cadena ha cambiado.

En lugar de derivar el gradiente de cada parámetro desde cero, la retropropagación comienza a partir de la pérdida, retrocede a través de la red y reutiliza los gradientes calculados previamente cuando sea necesario.

Esto evita repetir los mismos cálculos intermedios y hace que el proceso sea mucho más sistemático.

Esta misma estrategia se puede aplicar a redes neuronales mucho más grandes.

Cualquiera que sea el número de capas o parámetros, la retropropagación sigue el mismo proceso para calcular los gradientes de todos los parámetros.

El mismo proceso de paso hacia adelante y hacia atrás se repite para cada observación en el conjunto de datos.

Para cada ejemplo de entrenamiento, obtenemos diferentes valores intermedios y gradientes.

Después de calcular los gradientes, actualizamos los pesos y sesgos según el método de optimización que se utiliza, como el descenso de gradiente por lotes, el descenso de gradiente estocástico (SGD) o el descenso de gradiente por mini lotes.

Espero que esta serie te haya resultado útil, hayas obtenido algo sobre lo que construir y hayas disfrutado aprendiendo a lo largo del camino.

Si es nuevo en esta serie y desea leer los artículos anteriores, puede encontrarlos aquí.

Me encantaría escuchar tu opinión. Si tiene alguna pregunta o comentario, no dude en dejar un comentario en LinkedIn.

Si hay algún tema que le gustaría que cubra en un artículo futuro, me encantaría escucharlo.

Las ideas complejas se vuelven simples cuando las entendemos paso a paso.

¡Gracias por leer!