En el
último capítulo, vimos cómo las redes neuronales pueden aprender independientemente los pesos y las compensaciones utilizando el algoritmo de descenso de gradiente. Sin embargo, hubo una brecha en nuestra explicación: no discutimos el cálculo del gradiente de la función de costo. ¡Y esta es una brecha decente! En este capítulo, presentaré un algoritmo rápido para calcular tales gradientes, conocido como retropropagación.
El algoritmo de retropropagación se inventó por primera vez en la década de 1970, pero su importancia no se entendió completamente hasta el famoso trabajo de 1986, escrito por David Rumelhart, Joffrey Hinton y Ronald Williams. El documento describe varias redes neuronales en las que la retropropagación funciona mucho más rápido que en enfoques anteriores de aprendizaje, por lo que desde entonces ha sido posible utilizar una red neuronal para resolver problemas que antes no se podían resolver. Hoy, el algoritmo de retropropagación es el caballo de batalla para aprender una red neuronal.
Este capítulo contiene más matemáticas que todos los demás en el libro. Si no le gustan particularmente las matemáticas, puede sentirse tentado a omitir este capítulo y simplemente tratar la propagación hacia atrás como un recuadro negro cuyos detalles está dispuesto a ignorar. ¿Por qué perder el tiempo estudiándolos?
La razón, por supuesto, es la comprensión. La propagación hacia atrás se basa en la expresión de la derivada parcial ∂C / ∂w de la función de costo C con respecto al peso w (o sesgo b) de la red. La expresión muestra qué tan rápido cambia el valor cuando cambian los pesos y las compensaciones. Y aunque esta expresión es bastante compleja, tiene su propia belleza, porque cada uno de sus elementos tiene una interpretación natural e intuitiva. Por lo tanto, la retropropagación no es solo un algoritmo de aprendizaje rápido. Nos da una comprensión detallada de cómo cambiar los pesos y las compensaciones cambia todo el comportamiento de la red. Y vale la pena estudiar los detalles.
Dado todo esto, si solo quiere pasar este capítulo o saltar al siguiente, está bien. Escribí el resto del libro para que sea comprensible, incluso si consideramos la distribución inversa como una caja negra. Por supuesto, más adelante en el libro habrá momentos en los que haré referencias a los resultados de este capítulo. Pero en ese momento, debe comprender las conclusiones básicas, incluso si no siguió todos los argumentos.
Para el calentamiento: un enfoque matricial rápido para calcular la salida de una red neuronal
Antes de analizar la propagación hacia atrás, obtengamos un algoritmo rápido de matriz para calcular la salida de una red neuronal. En realidad, ya conocimos este algoritmo al final del capítulo anterior, pero lo describí rápidamente, por lo que vale la pena considerarlo nuevamente con más detalle. En particular, será una buena forma de adaptarse al registro utilizado en la distribución posterior, pero en un contexto familiar.
Comencemos con un registro que nos permita indicar claramente los pesos en la red. Usaremos w
l jk para denotar el peso de conexión de la neurona No. k en la capa No. (l-1) con la neurona No. j en la capa No. l. Entonces, por ejemplo, el siguiente diagrama muestra el peso de conexión de la cuarta neurona de la segunda capa con la segunda neurona de la tercera capa:

Al principio, tal grabación parece incómoda y requiere algo de tiempo para acostumbrarse. Sin embargo, pronto te parecerá simple y natural. Una de sus características es el orden de los índices j y k. Podría decidir que sería más razonable usar j para designar la neurona de entrada, yk - para la neurona de salida, y no viceversa, como lo hemos hecho. Explicaré la razón de esta característica a continuación.
Usaremos notaciones similares para compensaciones y activaciones de red. En particular, b
l j denotará el desplazamiento de la neurona No. j en la capa No. l. a
l j denotará la activación de la neurona No. j en la capa No. l. El siguiente diagrama muestra ejemplos de cómo usar esta entrada:

Con tal registro, la activación de un
l j de la neurona No.
j en la capa No. l se asocia con la activación en la capa No. (l-1) mediante la siguiente ecuación (compárese con la ecuación (4) y su discusión en el capítulo anterior):
alj= sigma( sumkwljkal−1k+blj) tag23
donde la suma va sobre todas las neuronas k en la capa (l-1). Para reescribir esta expresión en forma de matriz, definimos una matriz de peso w
l para cada capa l. Los elementos de la matriz de peso son simplemente los pesos conectados a la capa No. l, es decir, el elemento en la fila No. j y la columna No. k serán w
l jk . Del mismo modo, para cada capa l definimos el vector de desplazamiento b
l . Probablemente haya adivinado cómo funciona esto: los componentes del vector de desplazamiento serán simplemente los valores b
l j , un componente para cada neurona en la capa No. l. Y finalmente, definimos el vector de activación a
l , cuyos componentes son la activación a
l j .
El último ingrediente necesario para reescribir (23) es la forma matricial de la vectorización de la función σ. En el último capítulo nos encontramos con la vectorización: la idea es que queremos aplicar la función σ a cada elemento del vector v. Usamos la notación obvia σ (v) para denotar la aplicación de la función basada en elementos. Es decir, los componentes σ (v) son simplemente σ (v)
j = σ (v
j ). Por ejemplo, si tenemos una función f (x) = x
2 , entonces la forma vectorizada f nos da
f( beginbmatrix23 endbmatrix)= beginbmatrixf(2)f(3) endbmatrix= beginbmatrix49 endbmatrix tag24
es decir, una f vectorizada simplemente cuadra cada elemento del vector.
Dadas todas estas formas de escritura, la ecuación (23) se puede reescribir en una forma vectorizada hermosa y compacta:
al= sigma(wlal−1+bl) tag25
Tal expresión nos permite tener una visión más global de la relación entre las activaciones de una capa y las activaciones de la anterior: simplemente aplicamos la matriz de peso a las activaciones, agregamos el vector de desplazamiento y luego aplicamos el sigmoide. Por cierto, es este registro el que requiere el uso del registro w
l jk . Si usáramos el índice j para denotar la neurona de entrada, yk para la neurona de salida, tendríamos que reemplazar la matriz de peso en la ecuación (25) con la transpuesta. Este es un cambio pequeño pero molesto, y perderíamos la simplicidad de la declaración (y la reflexión) sobre "aplicar la matriz de peso a las activaciones". Este enfoque global es más simple y conciso (¡y utiliza menos índices!) Que el poneuron. Esta es solo una forma de evitar el infierno del índice sin perder la precisión de lo que está sucediendo. Esta expresión también es útil en la práctica, ya que la mayoría de las bibliotecas de matrices ofrecen formas rápidas de multiplicar matrices, agregar vectores y vectorizar. El código del último capítulo utilizó directamente esta expresión para calcular el comportamiento de la red.
Usando la ecuación (25) para calcular a
l , calculamos el valor intermedio z
l ≡ w
l a
l - 1 + b
l . Este valor es bastante útil para nombrar: llamamos a z
l la entrada ponderada de las neuronas de la capa No. l. Más tarde utilizaremos activamente esta entrada ponderada. La ecuación (25) a veces se escribe a través de una entrada ponderada, como a
l = σ (z
l ). También vale la pena señalar que z
l tiene componentes
zlj= sumkwljkal−1k+blj es decir, z
l j es solo una entrada ponderada de la función de activación de la neurona j en la capa l.
Dos supuestos esenciales sobre la función de costo
El objetivo de la retropropagación es calcular las derivadas parciales ∂C / ∂w y ∂C / ∂b de la función de costo C para cada peso w y el sesgo b de la red. Para que la retropropagación funcione, debemos hacer dos suposiciones principales sobre la forma de la función de costo. Sin embargo, antes de eso sería útil imaginar un ejemplo de una función de costo. Usamos la función cuadrática del capítulo anterior (ecuación (6)). En la entrada de la sección anterior, se verá como
C= frac12n sumx||y(x)−aL(x)||2 tag26
donde: n es el número total de ejemplos de entrenamiento; la suma va para todos los ejemplos x; y = y (x) es la salida requerida; L denota el número de capas en la red; a
L = a
L (x) es el vector de salida de las activaciones de red cuando x está en la entrada.
Bien, entonces, ¿qué tipo de supuestos necesitamos sobre la función de costo C para aplicar la propagación hacia atrás? Primero, la función de costo se puede escribir como el promedio C = 1 / n ∑
x C
x de la función de costo C
x para ejemplos de entrenamiento individuales x. Esto se hace en el caso de una función de costo cuadrático, donde el costo de un ejemplo de entrenamiento es C
x = 1/2 || y - a
L ||
2) Esta suposición será cierta para todas las demás funciones de costos que encontraremos en el libro.
Necesitamos esta suposición porque, de hecho, la propagación inversa nos permite calcular las derivadas parciales ∂C / ∂w y ∂C / ∂b, promediando los ejemplos de entrenamiento. Aceptando esta suposición, asumiremos que el ejemplo de entrenamiento x es fijo y dejaremos de especificar el índice x, escribiendo el valor de C
x como C. Luego devolveremos x, pero por ahora es mejor simplemente decirlo.
La segunda suposición con respecto a la función de costo: se puede escribir en función de la salida de una red neuronal:

Por ejemplo, la función de costo cuadrático cumple este requisito, ya que el costo cuadrático de un ejemplo de capacitación x puede escribirse como
C=1/2||y−aL||2=1/2 sumj(yj−aLj)2 tag27
que será la función de las activaciones de salida. Por supuesto, esta función de costo también depende de la salida deseada y, y puede preguntarse por qué no consideramos a C como una función también de y. Sin embargo, recuerde que el ejemplo de entrenamiento de entrada x es fijo, por lo que la salida y también es fija. En particular, no podemos cambiarlo cambiando los pesos y los desplazamientos, es decir, esto no es lo que aprende la red neuronal. Por lo tanto, tiene sentido considerar C como una función solo de las activaciones de salida a
L , e y como solo un parámetro que ayuda a determinarlo.
El trabajo de Hadamard s⊙t
El algoritmo de retropropagación se basa en las operaciones habituales de álgebra lineal: adición de vectores, multiplicación de un vector por una matriz, etc. Sin embargo, una de las operaciones se usa con menos frecuencia. Suponga que syt son dos vectores de la misma dimensión. Luego, por s⊙t, denotamos la multiplicación por elementos de dos vectores. Entonces los componentes s⊙t son simplemente (s⊙t)
j = s
j t
j . Por ejemplo:
beginbmatrix12 endbmatrix odot beginbmatrix34 endbmatrix= beginbmatrix1∗32∗4 endbmatrix= beginbmatrix38 endbmatrix tag28
Tal trabajo por partes a veces se llama el
trabajo de Hadamard o el trabajo de Schur. Lo llamaremos el trabajo de Hadamard. Las buenas bibliotecas para trabajar con matrices generalmente tienen una implementación rápida del producto Hadamard, y esto puede ser conveniente al implementar la retropropagación.
Cuatro ecuaciones fundamentales para la propagación hacia atrás.
La retropropagación implica comprender cómo el cambio de pesos y compensaciones de la red cambia la función de costo. En esencia, esto significa calcular las derivadas parciales ∂C / ∂w
l jk y ∂C / ∂b
l j . Pero para su cálculo, primero calculamos el valor intermedio δ
l j , que llamamos el error en la neurona No. j en la capa No. l. La propagación hacia atrás nos dará un procedimiento para calcular el error δ
l j , y luego asociará δ
l j con ∂C / ∂w
l jk y ∂C / ∂b
l j .
Para comprender cómo se determina un error, imagine que se ha iniciado un demonio en nuestra red neuronal:

Se sienta en la neurona No. j en la capa No. l. Al recibir los datos de entrada, el demonio interrumpe el funcionamiento de la neurona. Agrega un pequeño cambio en Δz
l j a la entrada ponderada de la neurona, y en lugar de producir σ (z
l j ), la neurona producirá σ (z
l j + Δz
l j ). Este cambio también se extenderá a través de las siguientes capas de la red, lo que finalmente cambiará el costo total por (∂C / ∂z
l j ) * Δz
l j .
Pero nuestro demonio es bueno, y está tratando de ayudarlo a mejorar el costo, es decir, encontrar Δz
l j que reduzca el costo. Suponga que el valor ∂C / ∂z
l j es grande (positivo o negativo). Entonces el demonio puede reducir seriamente el costo eligiendo Δz
l j con el signo opuesto a ∂C / ∂z
l j . Pero si ∂C / ∂z
l j está cerca de cero, entonces el demonio no puede mejorar mucho el costo cambiando la entrada ponderada z
l j . Entonces, desde el punto de vista del demonio, la neurona ya está cerca del óptimo (esto, por supuesto, es cierto solo para pequeños Δz
l j . Supongamos que estas son las limitaciones de las acciones del demonio). Por lo tanto, en el sentido heurístico, ∂C / ∂z
l j es una medida del error neuronal.
Bajo la motivación de esta historia, definimos el error δ
l j de la neurona j en la capa l, como
deltalj equiv frac partialC partialzlj tag29
Según nuestra convención habitual, usamos δ
l para denotar el vector de error asociado con la capa l. La propagación inversa nos dará una manera de calcular δ
l para cualquier capa y luego correlacionar estos errores con las cantidades que realmente nos interesan, ∂C / ∂w
l jk y ∂C / ∂b
l j .
Tal vez se pregunte por qué el daemon cambia la entrada ponderada z
l j . Después de todo, sería más natural imaginar que el demonio cambia la activación de salida a
l j para que usemos ∂C / ∂a
l j como medida de error. De hecho, si lo hace, entonces todo resulta muy similar a lo que discutiremos más adelante. Sin embargo, en este caso, la representación de la propagación inversa será algebraicamente un poco más complicada. Por lo tanto, nos detenemos en la variante δ
l j = ∂C / ∂z
l j como una medida de error.
En problemas de clasificación, el término "error" a veces significa el número de clasificaciones incorrectas. Por ejemplo, si una red neuronal clasifica correctamente el 96.0% de los dígitos, entonces el error será 4.0%. Obviamente, esto no es lo que queremos decir con los vectores δ. Pero en la práctica, generalmente puede comprender fácilmente qué significa.
Plan de ataque : la propagación hacia atrás se basa en cuatro ecuaciones fundamentales. Juntos, nos dan una manera de calcular tanto el error δ
l como el gradiente de la función de costo. Les doy a continuación. No es necesario esperar su desarrollo instantáneo. Estarás decepcionado. Las ecuaciones de retropropagación son tan profundas que una buena comprensión requiere tiempo y paciencia tangibles, y una profundización gradual de la pregunta. La buena noticia es que esta paciencia dará buenos resultados. Por lo tanto, en esta sección, nuestro razonamiento apenas comienza, ayudándole a seguir el camino de una comprensión profunda de las ecuaciones.
Aquí hay un diagrama de cómo profundizaremos en estas ecuaciones más adelante: daré una breve prueba de ellas para ayudar a explicar por qué son verdaderas; los reescribiremos en forma algorítmica en forma de pseudocódigo, y veremos cómo implementarlo en código python real; En la última parte del capítulo, desarrollaremos una idea intuitiva del significado de las ecuaciones de retropropagación y cómo se pueden encontrar desde cero. Periódicamente volveremos a las cuatro ecuaciones fundamentales, y cuanto más las comprenda, más cómodas y tal vez hermosas y naturales le parecerán.
La ecuación del error de la capa de salida, δ L : los componentes de δ
L se consideran como
deltaLj= frac partialC partialaLj sigma′(zLj) tagBP1
Expresión muy natural. El primer término a la derecha, ∂C / ∂a
L j , mide qué tan rápido cambia el costo en función de la activación de salida No. j. Si, por ejemplo, C no depende particularmente de la neurona de salida particular j, entonces δ
L j será pequeña, como se esperaba. El segundo término a la derecha, σ '(z
L j ), mide qué tan rápido cambia la función de activación σ en z
L j .
Tenga en cuenta que todo en (BP1) es fácil de contar. En particular, calculamos z
L j al calcular el comportamiento de la red, y tomará un poco más de recursos calcular σ '(z
L j ). Por supuesto, la forma exacta ∂C / ∂a
L j depende de la forma de la función de costo. Sin embargo, si se conoce la función de costo, entonces no debería haber problemas para calcular ∂C / ∂a
L j . Por ejemplo, si usamos la función de costo cuadrático, entonces C = 1/2 ∑
j (y
j - a
L j )
2 , por lo tanto ∂C / ∂a
L j = (a
L j - y
j ), que es fácil de calcular.
La ecuación (BP1) es una expresión explotada de δ
L. Es completamente normal, pero no se registra en forma de matriz, que necesitamos para la distribución posterior. Sin embargo, es fácil reescribir en forma de matriz, ya que
deltaL= nablaaC odot sigma′(zL) tagBP1a
Aquí ∇
a C se define como un vector cuyos componentes son las derivadas parciales ∂C / ∂a
L j . Se puede representar como una expresión de la tasa de cambio de C con respecto a las activaciones de salida. Es fácil ver que las ecuaciones (BP1a) y (BP1) son equivalentes, por lo tanto, utilizaremos (BP1) para referirnos a cualquiera de ellas a continuación. Por ejemplo, en el caso de un valor cuadrático, tenemos ∇
a C = (a
L - y), por lo que la forma de matriz completa (BP1) será
deltaL=(aL−y) odot sigma′(zL) tag30
Todo en esta expresión tiene una forma vectorial conveniente, y es fácil de calcular usando una biblioteca como Numpy.
La expresión del error δ l a través del error en la siguiente capa, δ l + 1 : en particular,
deltal=((wl+1)T deltal+1) cdot sigma′(zl) tagBP2
donde (w
l + 1 )
T es la transposición de la matriz de peso w
l + 1 para la capa No. (l + 1). La ecuación parece complicada, pero cada elemento es fácil de interpretar. Supongamos que conocemos el error δ
l + 1 para la capa (l + 1). La transposición de la matriz de peso, (w
l + 1 )
T , se puede imaginar como mover el error hacia atrás a través de la red, lo que nos da alguna medida de error en la salida de la capa No. l. Luego consideramos el producto Hadamard ⊙σ '(z
l ). Esto hace retroceder el error a través de la función de activación en la capa l, dándonos el valor de error δl en la entrada ponderada para la capa l.
Al combinar (BP2) con (BP1), podemos calcular el error δ
l para cualquier capa de red.
Comenzamos usando (BP1) para calcular δ L , luego usamos la ecuación (BP2) para calcular δ L-1 , luego nuevamente para calcular δ L-2 , y así sucesivamente, hasta el final de la red.La ecuación de la tasa de cambio de costo en relación con cualquier compensación en la red : en particular:∂ C∂ b l j =δ l j
Es decir, el error δ l j es exactamente igual a la tasa de cambio ∂C / ∂b l j . Esto es excelente porque (BP1) y (BP2) ya nos han dicho cómo calcular δ l j . Podemos reescribir (BP3) más corto como∂ C∂ b =δ
donde δ se estima para la misma neurona que el sesgo b.La ecuación para la tasa de cambio de valor en relación con cualquier peso en la red : en particular:∂ C∂ w l j k =a l - 1 k δ l j
A partir de aquí, aprendemos cómo calcular la derivada parcial ∂C / ∂w l jk a través de los valores de δ l y a l-1 , cuyo método de cálculo ya conocemos. Esta ecuación se puede reescribir en una forma menos cargada:∂ C∂ w =ainδout
donde a in es la activación de la entrada neural para el peso w, y δ out es el error de la salida neural del peso w. Si observamos con más detalle el peso w y dos neuronas conectadas por él, podemos dibujarlo así:
una buena consecuencia de la ecuación (32) es que cuando la activación a in es pequeña, a in ≈ 0, el término término ∂C / ∂w también tiende a a cero En este caso, decimos que el peso se entrena lentamente, es decir, no cambia mucho durante el descenso del gradiente. En otras palabras, una de las consecuencias (BP4) es que la salida ponderada de las neuronas con baja activación se aprende lentamente.Se pueden extraer otras ideas de (BP1) - (BP4). Comencemos con la capa de salida. Considere el término σ '(z L j) en (BP1). Recuerde del gráfico del sigmoide del capítulo anterior que se vuelve plano cuando σ (z L j ) se acerca a 0 o 1. En estos casos, σ '(z L j ) ≈ 0. Por lo tanto, el peso en la última capa se entrenará lentamente si se activa la neurona de salida es pequeña (≈ 0) o grande (≈ 1). En este caso, generalmente se dice que la neurona de salida está saturada y, como resultado, el peso ha dejado de entrenarse (o se está entrenando lentamente). Los mismos comentarios son válidos para los desplazamientos de la neurona de salida.Se pueden obtener ideas similares con respecto a las capas anteriores. En particular, considere el término σ '(z l ) en (BP2). Esto significa que δ l jLo más probable es que sea pequeña a medida que la neurona se acerca a la saturación. Y esto, a su vez, significa que cualquier peso en la entrada de una neurona saturada se entrenará lentamente (aunque esto no funcionará si w l + 1 T δ l + 1 tendrá elementos suficientemente grandes que compensen el pequeño valor de σ '(z L j )).Para resumir: aprendimos que el peso se entrenará lentamente si la activación de la neurona de entrada es pequeña o si la neurona de salida está saturada, es decir, su activación es pequeña o grande.Esto no es particularmente sorprendente. Y, sin embargo, estas observaciones ayudan a mejorar nuestra comprensión de lo que sucede cuando capacitamos a la red. Además, podemos abordar estos argumentos desde el otro lado. Las cuatro ecuaciones fundamentales son válidas para cualquier función de activación, y no solo para el sigmoide estándar (ya que, como veremos más adelante, las propiedades no usan el sigmoide). Por lo tanto, estas ecuaciones pueden usarse para desarrollar funciones de activación con ciertas propiedades de aprendizaje necesarias. Por ejemplo, supongamos que elegimos una función de activación σ que es diferente de un sigmoide, de modo que σ 'siempre es positivo y no se acerca a cero. Esto evita la desaceleración del aprendizaje que ocurre cuando las neuronas sigmoideas normales están saturadas. Más adelante en el libro veremos ejemplos en los que la función de activación cambia de manera similar.Dadas las ecuaciones (BP1) - (BP4), podemos explicar por qué se necesitan tales modificaciones y cómo pueden afectar la situación.
:δL=Σ′(zL)∇aC
donde Σ '(z L ) es una matriz cuadrada con valores de σ' (z L j ) ubicados a lo largo de la diagonal y otros elementos iguales a 0. Tenga en cuenta que esta matriz interactúa con ∇ a C a través de la multiplicación matricial habitual.Mostrar que (BP2) se puede reescribir comoδ l = Σ ′ ( z l ) ( w l + 1 ) T δ l + 1
Combinando las tareas anteriores, demuestre que:δ l = Σ ′ ( z l ) ( w l + 1 ) T … Σ ′ ( z L - 1 ) ( w L ) T Σ ′ ( z L ) ∇ a C
Para los lectores acostumbrados a la multiplicación de matrices, esta ecuación será más fácil de entender que (BP1) y (BP2). Me concentro en (BP1) y (BP2) porque este enfoque es más rápido de implementar numéricamente. [aquí Σ no es la suma (∑), sino la capital σ (sigma) / aprox. perev. ]
Prueba de las cuatro ecuaciones fundamentales (sección opcional)
Ahora probamos las cuatro ecuaciones fundamentales (BP1) - (BP4). Todos ellos son consecuencias de la regla de la cadena (la regla de diferenciación de una función compleja) del análisis de funciones de muchas variables. Si está familiarizado con la regla de la cadena, le recomiendo que intente contar los derivados usted mismo antes de continuar con la lectura.Vamos a empezar con la ecuación (BP1), lo que nos da una expresión para el delta del error de salida de L . Para demostrarlo, recordamos que, por definición:δ L j = ∂ C∂ z L j
Aplicando la regla de la cadena, reescribimos las derivadas parciales a través de las derivadas parciales de las activaciones de salida:δ L j = ∑ k ∂ C∂ a L k ∂a L k∂ z L j
donde el resumen va sobre todas las neuronas k en la capa de salida. Por supuesto, la activación de salida a L k de la neurona No. k depende solo de la entrada ponderada z L j para la neurona No. j cuando k = j. Por lo tanto, ∂a L k / ∂z L j desaparece cuando k ≠ j. Como resultado, simplificamos la ecuación anterior aδ L j = ∂ C∂ a L j ∂a L j∂ z L j
Recordando que a L j = σ (z L j ), podemos reescribir el segundo término a la derecha como σ '(z L j ), y la ecuación se convierte enδ L j = ∂ C∂ a L j σ′(z L j )
es decir, en (BP1) en una vista despiezada.Luego demostramos (BP2), que da la ecuación para el error δ l a través del error en la siguiente capa δ l + 1 . Para hacer esto, necesitamos reescribir δ l j = ∂C / ∂z l j a través de δ l + 1 k = ∂C / ∂z l + 1 k . Esto se puede hacer usando la regla de la cadena:δ l j = ∂ C∂ z l j
= ∑ k ∂ C∂ z l + 1 k ∂z l + 1 k∂ z l j
= ∑ k ∂ z l + 1 k∂ z l j δ l + 1 k
donde en la última línea intercambiamos los dos términos de la derecha y sustituimos la definición de δ l + 1 k . Para calcular el primer término en la última línea, tenga en cuenta quez l + 1 k = ∑ j w l + 1 k j a l j + b l + 1 k = ∑ j w l + 1 k j σ ( z l j ) + b l + 1 k
Diferenciando, obtenemos∂ z l + 1 k∂ z l j =w l + 1 k j σ′(z l j ).
Sustituyendo esto en (42), obtenemosδ l j = ∑ k w l + 1 k j δ l + 1 k σ ′ ( z l j ) .
Es decir, (BP2) en una entrada en despiece.Queda por probar (BP3) y (BP4). También siguen la regla de la cadena, aproximadamente de la misma manera que las dos anteriores. Te los dejaré como ejercicio.Ejercicio
Esa es toda la prueba de las cuatro ecuaciones fundamentales de propagación hacia atrás. Puede parecer complicado Pero en realidad, esto es simplemente el resultado de una aplicación cuidadosa de la regla de la cadena. Hablando de manera menos sucinta, la propagación hacia atrás se puede imaginar como una forma de calcular el gradiente de la función de costo a través de la aplicación sistemática de la regla de la cadena a partir del análisis de las funciones de muchas variables. Y eso es realmente todo lo que es la distribución posterior: el resto son solo detalles.Algoritmo de retropropagación
Las ecuaciones de retropropagación nos dan un método para calcular el gradiente de la función de costo. Escribamos esto explícitamente como un algoritmo:- Entrada x: asigne la activación apropiada a 1 para la capa de entrada.
- : l = 2,3,…,L z l = w l a l−1 +b l a l = σ(z l ).
- δ L : δ L = ∇ a C ⊙ σ'(z L ).
- : l = L−1,L−2,…,2 δ l = ((w l+1 ) T δ l+1 ) ⊙ σ'(z l ).
- : ∂C∂wljk=al−1kδlj y ∂C∂blj=δlj .
Si observa el algoritmo, comprenderá por qué se llama retropropagación. Calculamos los vectores de error δ l hacia atrás, comenzando desde la última capa. Puede parecer extraño que estemos retrocediendo a través de la red. Pero si piensa en la prueba de propagación hacia atrás, entonces el movimiento inverso es una consecuencia del hecho de que el costo es una función de la salida de la red. Para comprender cómo varía el costo con los primeros pesos y compensaciones, necesitamos aplicar la regla de la cadena una y otra vez, volviendo a través de las capas para obtener expresiones útiles.Ejercicios
- . , , f(∑ j w j x j +b), f – , . ?
- . , σ(z) = z . .
Como expliqué anteriormente, el algoritmo de retropropagación calcula el gradiente de la función de costo para un ejemplo de capacitación, C = C x . En la práctica, la propagación hacia atrás a menudo se combina con un algoritmo de aprendizaje, por ejemplo, con descenso de gradiente estocástico, cuando calculamos el gradiente para muchos ejemplos de entrenamiento. En particular, para un mini paquete dado de ejemplos de entrenamiento m, el siguiente algoritmo aplica el descenso de gradiente basado en este mini paquete:- Entrada: un conjunto de ejemplos de entrenamiento.
- Para cada ejemplo de entrenamiento x, asigne la activación de entrada correspondiente a x, 1 y realice los siguientes pasos:
- l=2,3,…,L z x,l = w l a x,l−1 +b l a x,l = σ(z x,l ).
- δ x,L : δ x,L = ∇ a C x ⋅ σ'(z x,L ).
- : l=L−1,L−2,…,2 δ x,l = ((w l+1 ) T δ x,l+1 ) ⋅ σ'(z x,l ).
- : l=L,L−1,…,2 wl rightarrowwl− frac etam sumx deltax,l(ax,l−1)T y compensaciones de acuerdo con la regla bl rightarrowbl− frac etam sumx deltax,l .
Por supuesto, para implementar el descenso de gradiente estocástico en la práctica, también necesitará un ciclo externo que genere mini paquetes de ejemplos de entrenamiento y un ciclo externo que atraviese varias épocas de entrenamiento. Por simplicidad, los omití.
Código para distribución posterior
Habiendo entendido el lado abstracto de la retropropagación, ahora podemos entender el código utilizado en el capítulo anterior que implementa la retropropagación. Recuerde de ese capítulo que el código estaba contenido en los métodos update_mini_batch y backprop de la clase Network. El código para estos métodos es una traducción directa del algoritmo descrito anteriormente. En particular, el método update_mini_batch actualiza los pesos y las compensaciones de la red calculando el gradiente para los ejemplos de entrenamiento actuales de mini_batch:
class Network(object): ... def update_mini_batch(self, mini_batch, eta): """ , -. mini_batch – (x, y), eta – .""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]
La mayor parte del trabajo se realiza mediante las líneas delta_nabla_b, delta_nabla_w = self.backprop (x, y), utilizando el método de backprop para calcular las derivadas parciales ∂C
x / ∂b
l j y ∂C
x / ∂w
l jk . El método de backprop casi repite el algoritmo de la sección anterior. Hay una pequeña diferencia: utilizamos un enfoque ligeramente diferente para la indexación de capas. Esto se hace para aprovechar la función de Python, índices negativos de matriz que le permiten contar elementos hacia atrás desde el final. l [-3] será el tercer elemento desde el final de la matriz l. El código de backprop se proporciona a continuación, junto con las funciones auxiliares utilizadas para calcular el sigmoide, su derivada y la derivada de la función de costo. Con ellos, el código es completo y comprensible. Si algo no está claro, consulte la primera descripción completa del código de listado.
class Network(object): ... def backprop(self, x, y): """ ``(nabla_b, nabla_w)``, C_x. ``nabla_b`` ``nabla_w`` - numpy, ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights]
Desafío
- Un enfoque de retropropagación totalmente matricial en el minipack. Nuestra implementación del descenso de gradiente estocástico utiliza una serie de ejemplos de entrenamiento del mini paquete. El algoritmo de retropropagación se puede cambiar para que calcule los gradientes para todos los ejemplos de entrenamiento del mini paquete al mismo tiempo. En lugar de comenzar con un solo vector x, podemos comenzar con la matriz X = [x 1 x 2 ... x m ], cuyas columnas son los vectores del minipack. La distribución directa es a través del producto de matrices de peso, la adición de una matriz adecuada para desplazamientos y el uso generalizado de sigmoide. La propagación hacia atrás sigue el mismo patrón. Escriba pseudocódigo para este enfoque para el algoritmo de retropropagación. Modifique network.py para que use este enfoque matricial. La ventaja de este enfoque será el uso de todas las ventajas de las bibliotecas modernas para el álgebra lineal. Como resultado, puede ejecutarse más rápido que el ciclo de mini paquetes (por ejemplo, en mi computadora el programa acelera aproximadamente 2 veces en las tareas de clasificación de MNIST). En la práctica, todas las bibliotecas serias para distribución posterior utilizan un enfoque de matriz completa o alguna versión de este.
¿En qué sentido la retropropagación es un algoritmo rápido?
¿En qué sentido la retropropagación es un algoritmo rápido? Para responder a esta pregunta, considere otro enfoque para calcular el gradiente. Imagine los primeros días de la investigación de redes neuronales. Tal vez esta sea la década de 1950 o 1960, ¡y usted es la primera persona en el mundo a la que se le ocurrió la idea de usar el descenso de gradiente para el entrenamiento! Pero para que esto funcione, debe calcular el gradiente de la función de costo. Recuerda álgebra y decide ver si puede usar la regla de la cadena para calcular el gradiente. Después de haber jugado un poco, ves que el álgebra parece difícil y estás decepcionado. Estás tratando de encontrar un enfoque diferente. Decide considerar el costo en función de solo los pesos C = C (w) (volveremos a los desplazamientos un poco más tarde). Usted numera los pesos w
1 , w
2 , ... y desea calcular ∂C / ∂w
j para el peso w
j . La forma obvia es usar la aproximación
frac partialC partialwj aprox fracC(w+ epsilonej)−C(w) epsilon tag46
Donde ε> 0 es un número positivo pequeño, y e
j es el vector de dirección de la unidad j. En otras palabras, podemos estimar aproximadamente ∂C / ∂w
j calculando el costo C para dos valores ligeramente diferentes de w
j , y luego aplicar la ecuación (46). La misma idea nos permite calcular las derivadas parciales de ∂C / ∂b con respecto a los desplazamientos.
El enfoque parece prometedor. Conceptualmente simple, fácil de implementar, utiliza solo unas pocas líneas de código. ¡Parece mucho más prometedor que la idea de usar una regla de cadena para calcular el gradiente!
Desafortunadamente, aunque este enfoque parece prometedor, cuando se implementa en código, resulta que funciona extremadamente lento. Para entender por qué, imagine que tenemos un millón de pesos en la red. Luego, para cada peso w
j necesitamos calcular C (w + εe
j ) para calcular ∂C / ∂w
j . Y esto significa que para calcular el gradiente, necesitamos calcular la función de costo un millón de veces, lo que requerirá un millón de pases directos a través de la red (para cada ejemplo de capacitación). Y también necesitamos calcular C (w), por lo que obtenemos un millón y un paso a través de la red.
El truco de la propagación hacia atrás es que nos permite calcular simultáneamente todas las derivadas parciales ∂C / ∂w
j utilizando solo un paso directo a través de la red, seguido de un paso inverso. En términos generales, el costo computacional del pase de devolución es casi el mismo que el del directo.
Por lo tanto, el costo total de la propagación inversa es aproximadamente el mismo que el de dos pases directos a través de la red. ¡Compare esto con el millón y un pase directo necesario para implementar el método (46)! Entonces, aunque la retropropagación parece un enfoque más complicado, en realidad es mucho más rápido.
Por primera vez, esta aceleración fue plenamente apreciada en 1986, y esto amplió dramáticamente el rango de tareas resueltas con la ayuda de redes neuronales. A su vez, esto ha llevado a un aumento en el número de personas que usan redes neuronales. Por supuesto, la propagación hacia atrás no es una panacea. Incluso a fines de la década de 1980, las personas ya encontraron sus limitaciones, especialmente cuando intentaban utilizar la propagación hacia atrás para entrenar redes neuronales profundas, es decir, redes con muchas capas ocultas. Más adelante veremos cómo las computadoras modernas y las nuevas ideas engañosas hacen posible utilizar la propagación hacia atrás para entrenar redes neuronales tan profundas.
Distribución inversa: en general
Como ya he explicado, la propagación hacia atrás nos revela dos misterios. ¿Lo primero que hace realmente el algoritmo? Hemos desarrollado un esquema de propagación hacia atrás para el error de la salida. ¿Es posible ir más allá, tener una idea más intuitiva de lo que sucede durante todas estas multiplicaciones de vectores y matrices? El segundo enigma es ¿cómo alguien detectó la propagación hacia atrás? Una cosa es seguir los pasos del algoritmo o la prueba de su funcionamiento. Pero esto no significa que haya entendido el problema tan bien que podría inventar este algoritmo. ¿Existe una línea razonable de razonamiento que nos pueda llevar al descubrimiento del algoritmo de retropropagación? En esta sección, cubriré ambos acertijos.
Para mejorar la comprensión del funcionamiento del algoritmo, imagine que hicimos un pequeño cambio Δw
l jk de cierto peso w
l jk :

Este cambio en el peso conducirá a un cambio en la activación de salida de la neurona correspondiente:

Esto conducirá a un cambio en todas las activaciones de la siguiente capa:

Estos cambios conducirán a cambios en la siguiente capa, y así sucesivamente, hasta la última, y luego a cambios en la función de costo:

El cambio en ΔC está relacionado con el cambio en Δw
l jk por la ecuación
DeltaC approx frac partialC partialwljk Deltawljk tag47
De ello se deduce que un enfoque probable para calcular ∂C / ∂w
l jk es monitorear cuidadosamente la propagación de un pequeño cambio w
l jk , lo que lleva a un pequeño cambio en C. Si podemos hacer esto, expresamos cuidadosamente en el camino todo en cantidades que sean fáciles de calcular , entonces podemos calcular ∂C / ∂w
l jk .
Probémoslo. Un cambio en Δw
l jk provoca un ligero cambio en Δa
l j en la activación de la neurona j en la capa l. Este cambio está establecido.
Deltaalj approx frac partialalj partialwljk Deltawljk tag48
El cambio en la activación Δa
l j conduce a cambios en todas las activaciones de la siguiente capa, (l + 1). Nos centraremos solo en una de estas activaciones alteradas, por ejemplo, a
l + 1 q ,

Esto dará como resultado los siguientes cambios:
Deltaal+1q approx frac partialal+1q partialalj Deltaalj tag49
Sustituyendo la ecuación (48), obtenemos:
Deltaal+1q approx frac partialal+1q partialalj frac partialalj partialwljk Deltawljk tag50
Por supuesto, un cambio en Δa
l + 1 q también cambiará la activación en la siguiente capa. Incluso podemos imaginar una ruta a lo largo de toda la red desde w
l jk hasta C, donde cada cambio en la activación conduce a un cambio en la próxima activación y, finalmente, a un cambio en el costo en la salida. Si la ruta pasa por activaciones a
l j , a
l + 1 q , ..., a
L - 1 n , a
L m , entonces la expresión final será
DeltaC approx frac partialC partialaLm frac partialaLm partialaL−1n frac partialaL−1n partialaL−2p ldots frac partialal+1q partialalj frac partialalj parcialwljk Deltawljk tag51
Es decir, elegimos un miembro de la forma ∂a / ∂a para cada una de las siguientes neuronas que pasamos, así como para el término ∂C / ∂a
L m al final. Esta es una representación de los cambios en C debido a cambios en las activaciones en esta ruta particular a través de la red. Por supuesto, hay muchas formas en que un cambio en w
l jk puede pasar y afectar el costo, y consideramos solo una de ellas. Para calcular el cambio total en C, es razonable suponer que debemos resumir todas las rutas posibles desde el peso hasta el costo final:
DeltaC approx summnp ldotsq frac partialC partialaLm frac partialaLm partialaL−1n frac partialaL−1n partialaL−2p ldots frac partialal+1q partialalj frac parcialalj parcialwljk Deltawljk tag52
donde resumimos todas las opciones posibles para neuronas intermedias en el camino. Comparando esto con (47), vemos que:
frac partialC partialwljk= summnp ldotsq frac partialC partialaLm frac partialaLm partialaL−1n frac partialaL−1n partialaL−2p ldots frac pa r t i a l a l + 1 q p a r t i a l a l j f r a c p a r t i a l a l j p a r t i a l w l j k . t a g 53
La ecuación (53) parece complicada. Sin embargo, tiene una buena interpretación intuitiva. Contamos el cambio en C con respecto a los pesos de la red. Nos dice que cada borde entre dos neuronas de la red está asociado con un factor de relación, que es solo una derivada parcial de la activación de una neurona con respecto a la activación de otra neurona. Para una costilla desde el primer peso hasta la primera neurona, el factor de relación es ∂a
l j / ∂w
l jk . El coeficiente de relación para la ruta es simplemente el producto de los coeficientes a lo largo de la ruta. Y el coeficiente de cambio total ∂C / ∂w
l jk es la suma de los coeficientes en todos los sentidos, desde el peso inicial hasta el costo final. Este procedimiento se muestra a continuación para una ruta:

Hasta ahora, hemos estado dando un argumento heurístico, una forma de representar lo que sucede cuando cambian los pesos de la red. Permítanme esbozar una nueva forma de pensar sobre este tema para el desarrollo de este argumento. Primero, podemos derivar la expresión exacta para todas las derivadas parciales individuales en la ecuación (53). Esto es fácil de hacer usando álgebra simple. Después de eso, puede intentar comprender cómo anotar todas las sumas por índices en forma de productos matriciales. Esto resulta ser una tarea tediosa que requiere paciencia, pero no algo extraordinario. Después de todo esto y la máxima simplificación, verá que se obtiene exactamente el mismo algoritmo de propagación hacia atrás. Por lo tanto, el algoritmo de retropropagación se puede imaginar como una forma de calcular la suma de los coeficientes para todas las rutas. O, para reformular, el algoritmo de retropropagación es una forma complicada de rastrear pequeños cambios en los pesos (y las compensaciones) cuando se propagan a través de una red, alcanzan un resultado y afectan el costo.
Aquí no haré todo esto. Este negocio no es atractivo y requiere un estudio cuidadoso de los detalles. Si está listo para esto, puede hacer esto. Si no, espero que tales pensamientos le den algunas ideas sobre los objetivos de la propagación hacia atrás.
¿Qué pasa con otro enigma? ¿Cómo podría descubrirse la propagación hacia atrás? De hecho, si sigue el camino que he esbozado, recibirá evidencia de propagación hacia atrás. Desafortunadamente, la prueba será más larga y más complicada que lo que describí anteriormente. Entonces, ¿cómo se descubrió esa evidencia corta (pero aún más misteriosa)? Si anota todos los detalles de una prueba larga, notará inmediatamente algunas simplificaciones obvias. Aplica simplificaciones, obtiene pruebas más simples, escríbalas. Y luego te encuentras con algunas simplificaciones obvias. Y repites el proceso. Después de varias repeticiones, la prueba que vimos antes será breve, pero un poco incomprensible, ¡ya que se han eliminado todos los hitos! Por supuesto, le sugiero que tome mi palabra, pero de hecho no hay ningún misterio sobre el origen de la evidencia. Solo mucho trabajo duro para simplificar la prueba que describí en esta sección.
Sin embargo, hay un truco inteligente en este proceso. En la ecuación (53), las variables intermedias son activaciones del tipo a
l + 1 q . El truco consiste en utilizar entradas ponderadas, como z
l + 1 q , como variables intermedias. Si no usa esto y continúa usando activaciones, la evidencia obtenida será un poco más complicada que la que se dio anteriormente en este capítulo.