Capítulo 4. Extensiones

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

En el último capítulo, después de haber dedicado dos capítulos a razonar desde los primeros principios sobre qué son los modelos de aprendizaje profundo y cómo deberían funcionar, finalmente construimos nuestro primer modelo de aprendizaje profundo y lo entrenamos para resolver el problema relativamente sencillo de predecir el precio de la vivienda a partir de características numéricas sobre las casas. En la mayoría de los problemas del mundo real, sin embargo, entrenar con éxito modelos de aprendizaje profundo no es tan fácil: aunque estos modelos pueden concebiblemente encontrar una solución óptima a cualquier problema que pueda enmarcarse como un problema de aprendizaje supervisado, en la práctica suelen fallar, y de hecho hay pocas garantías teóricas de que una arquitectura de modelo dada encuentre de hecho una buena solución a un problema dado. Aun así, hay algunas técnicas bien conocidas que hacen que el entrenamiento de redes neuronales tenga más probabilidades de éxito; en ellas se centrará este capítulo.

Empezaremos en repasando lo que "intentan hacer" matemáticamente las redes neuronales: encontrar el mínimo de una función. Luego mostraré una serie de técnicas que pueden ayudar a las redes a conseguirlo, demostrando su eficacia en el clásico conjunto de datos MNIST de dígitos escritos a mano. Empezaremos con una función de pérdida que se utiliza en todos los problemas de clasificación en el aprendizaje profundo, mostrando que acelera significativamente el aprendizaje (hasta ahora sólo hemos tratado problemas de regresión en este libro porque aún no habíamos introducido esta función de pérdida y, por tanto, no hemos podido hacer justicia a los problemas de clasificación). De forma similar, trataremos las funciones de activación distintas de la sigmoidea y mostraremos por qué también pueden acelerar el aprendizaje, al tiempo que discutiremos las ventajas y desventajas de las funciones de activación en general. A continuación, trataremos el impulso, la extensión más importante (y directa) de la técnica de optimización por descenso de gradiente estocástico que hemos estado utilizando hasta ahora, y discutiremos brevemente lo que pueden hacer los optimizadores más avanzados. Terminaremos cubriendo tres técnicas que no están relacionadas entre sí, pero que son esenciales: el decaimiento de la tasa de aprendizaje, la inicialización del peso y el abandono. Como veremos, cada una de estas técnicas ayudará a nuestra red neuronal a encontrar soluciones sucesivamente más óptimas.

En el primer capítulo, seguimos el modelo "diagrama-matemática-código" para introducir cada concepto. Aquí no hay un diagrama obvio para cada técnica, así que empezaremos con la "intuición" de cada técnica, seguiremos con las matemáticas (que normalmente serán mucho más sencillas que en el primer capítulo), y terminaremos con el código, que en realidad supondrá incorporar la técnica al marco que hemos construido y describir así con precisión cómo interactúa con los bloques de construcción que formalizamos en el último capítulo. Con este espíritu, empezaremos el capítulo con algunas intuiciones "generales" sobre lo que intentan hacer las redes neuronales: encontrar el mínimo de una función.

Algunas intuiciones sobre las redes neuronales

Como hemos visto en , las redes neuronales contienen un montón de pesos; dados estos pesos, junto con algunos datos de entrada X y y, podemos calcular una "pérdida" resultante. La Figura 4-1 muestra esta visión de muy alto nivel (pero correcta) de las redes neuronales.

dlfs 0401
Figura 4-1. Una forma sencilla de pensar en una red neuronal con pesos

En realidad, cada peso individual tiene una relación compleja y no lineal con las características X, el objetivo y, los otros pesos y, en última instancia, la pérdida L. Si trazamos esto, variando el valor del peso mientras mantenemos constantes los valores de los otros pesos, X, y y, y trazamos el valor resultante de la pérdida L, podríamos ver algo como lo que se muestra en la Figura 4-2.

dlfs 0402
Figura 4-2. Pesos de una red neuronal frente a su pérdida

Cuando empezamos a entrenar redes neuronales, inicializamos cada peso para que tenga un valor en algún punto del eje x de la Figura 4-2. Luego, utilizando los gradientes que calculamos durante la retropropagación, actualizamos iterativamente los pesos. Luego, utilizando los gradientes que calculamos durante la retropropagación, actualizamos iterativamente el peso, basando nuestra primera actualización en la pendiente de esta curva en el valor inicial que elegimos por casualidad.1 La figura 4-3 muestra esta interpretación geométrica de lo que significa actualizar los pesos en una red neuronal basándose en los gradientes y en la tasa de aprendizaje . Las flechas azules de la izquierda representan la aplicación repetida de esta regla de actualización con una tasa de aprendizaje menor que las flechas rojas de la derecha; observa que, en ambos casos, las actualizaciones en la dirección horizontal son proporcionales a la pendiente de la curva en el valor del peso (una pendiente más pronunciada significa una actualización mayor).

Neural net diagram
Figura 4-3. Actualización de los pesos de una red neuronal en función de los gradientes y de la tasa de aprendizaje, representada geométricamente

El objetivo del entrenamiento de un modelo de aprendizaje profundo es mover cada peso hasta el valor "global" para el que se minimiza la pérdida. Como podemos ver en la Figura 4-3, si los pasos que damos son demasiado pequeños, corremos el riesgo de acabar en un mínimo "local", que es menos óptimo que el global (la trayectoria de un peso que sigue ese escenario se ilustra con las flechas azules). Si los pasos son demasiado grandes, corremos el riesgo de "saltar repetidamente" sobre el mínimo global, aunque estemos cerca de él (este escenario está representado por las flechas rojas). Éste es el compromiso fundamental de ajustar los ritmos de aprendizaje: si son demasiado pequeños, podemos quedarnos atascados en un mínimo local; si son demasiado grandes, pueden saltar por encima del mínimo global.

En realidad, el panorama es mucho más complicado que esto. Una de las razones es que hay miles, si no millones, de pesos en una red neuronal, por lo que estamos buscando un mínimo global en un espacio que tiene miles o millones de dimensiones. Además, como actualizamos los pesos en cada iteración, además de pasar por un X y un y diferentes, ¡la curva de la que intentamos encontrar el mínimo cambia constantemente! Esta última es una de las principales razones por las que las redes neuronales fueron recibidas con escepticismo durante tantos años; no parecía que actualizando iterativamente los pesos de este modo se pudiera encontrar realmente una solución globalmente deseable. Yann LeCun et al. lo dicen mejor en un artículo de Nature de 2015:

En concreto, se solía pensar que el descenso de gradiente simple quedaría atrapado en configuraciones de pesos mínimos locales pobres para las que ningún pequeño cambio reduciría el error medio. En la práctica, los mínimos locales pobres no suelen ser un problema en las redes grandes. Independientemente de las condiciones iniciales, el sistema casi siempre alcanza soluciones de calidad muy similar. Los resultados teóricos y empíricos recientes sugieren firmemente que los mínimos locales no son un problema grave en general.

Así que , en la práctica, la Figura 4-3 proporciona tanto un buen modelo mental de por qué los ritmos de aprendizaje no deben ser ni demasiado grandes ni demasiado pequeños, como una intuición adecuada de por qué funcionan realmente muchos de los trucos que vamos a aprender en este capítulo. Equipados con esta intuición de lo que intentan hacer las redes neuronales, empecemos a examinar estos trucos. Empezaremos con una función de pérdida, la función de pérdida de entropía cruzada softmax, que funciona en gran parte por su capacidad de proporcionar gradientes más pronunciados a los pesos que la función de pérdida de error cuadrático medio que vimos en el capítulo anterior.

La función de pérdida de entropía cruzada Softmax

En Capítulo 3, utilizamos el error cuadrático medio (ECM) como función de pérdida. Esta función tenía la agradable propiedad de que era convexa, lo que significaba que cuanto más lejos estuviera la predicción del objetivo, más pronunciado sería el gradiente inicial que la Loss enviaba hacia atrás a la red Layers y, por tanto, mayores serían todos los gradientes recibidos por los parámetros. Sin embargo, resulta que en los problemas de clasificación podemos hacer algo mejor que esto, ya que en tales problemas sabemos que los valores que emite nuestra red deben interpretarse como probabilidades; así, no sólo cada valor debe estar entre 0 y 1, sino que el vector de probabilidades debe sumar 1 para cada observación que hayamos hecho pasar por nuestra red. La función de pérdida de entropía cruzada softmax aprovecha esto para producir gradientes más pronunciados que la pérdida de error cuadrático medio para las mismas entradas. Esta función tiene dos componentes: el primero es la función softmax y el segundo es la pérdida de "entropía cruzada".

Componente nº 1: La función Softmax

Para un problema de clasificación con N clases posibles, haremos que nuestra red neuronal emita un vector de N valores para cada observación. Para un problema con tres clases, estos valores podrían ser, por ejemplo:

[5, 3, 2]

Matemáticas

De nuevo, como se trata de un problema de clasificación, sabemos que esto debe interpretarse como un vector de probabilidades (la probabilidad de que esta observación pertenezca a la clase 1, 2 ó 3, respectivamente). Una forma de transformar estos valores en un vector de probabilidades sería simplemente normalizarlos, sumando y dividiendo por la suma:

Normaliza ( x 1 x 2 x 3 ) = x 1 x 1 +x 2 +x 3 x 2 x 1 +x 2 +x 3 x 3 x 1 +x 2 +x 3

Sin embargo, resulta que hay una forma que produce gradientes más pronunciados y tiene algunas propiedades matemáticas elegantes: la función softmax. Esta función, para un vector de longitud 3, se definiría como

Softmax ( x 1 x 2 x 3 ) = e x 1 e x 1 +e x 2 +e x 3 e x 2 e x 1 +e x 2 +e x 3 e x 3 e x 1 +e x 2 +e x 3

Intuición

La intuición que subyace a la función softmax es que amplifica más fuertemente el valor máximo en relación con los demás valores, obligando a la red neuronal a ser "menos neutral" respecto a qué predicción cree que es la correcta en el contexto de un problema de clasificación. Comparemos lo que harían estas dos funciones, normalizar y softmax, con nuestro vector de probabilidades anterior:

normalize(np.array([5,3,2]))
array([0.5, 0.3, 0.2])
softmax(np.array([5,3,2]))
array([0.84, 0.11, 0.04])

Podemos ver que el valor máximo original -5- tiene un valor significativamente más alto que el que tendría al normalizar simplemente los datos, y los otros dos valores son más bajos que los que salían de la función normalize. Así pues, la función softmax está a medio camino entre la normalización de los valores y la aplicación real de la función max (que en este caso daría como resultado array([1.0, 0.0, 0.0])-de ahí el nombre de "softmax".

Componente nº 2: La pérdida de entropía cruzada

Recuerda que cualquier función de pérdida tomará un vector de probabilidades p 1 p n y un vector de valores reales y 1 y n .

Matemáticas

La función de pérdida de entropía cruzada, para cada índice i de estos vectores, es:

CE ( p i , y i ) = - y i × registro ( p i ) - ( 1 - y i ) × registro ( 1 - p i )

Intuición

Para ver por qué esto tiene sentido como función de pérdida, considera que como cada elemento de y es 0 ó 1, la ecuación anterior se reduce a:

CE ( p i , y i ) = - l o g ( 1 - p i ) si y i = 0 - l o g ( p i ) si y i = 1

Ahora podemos descomponer esto más fácilmente. Si y = 0, entonces el gráfico del valor de esta pérdida frente al valor de la pérdida del error cuadrático medio en el intervalo de 0 a 1 es como se muestra en la Figura 4-4.

dlfs 0404
Figura 4-4. Pérdida de entropía cruzada frente a MSE cuando y = 0

Las penalizaciones de la pérdida de entropía cruzada no sólo son mucho mayores en este intervalo2 sino que se vuelven más pronunciadas a un ritmo mayor; de hecho, ¡el valor de la pérdida de entropía cruzada se aproxima al infinito a medida que la diferencia entre nuestra predicción y el objetivo se acerca a 1! El gráfico para cuando y = 1 es similar, sólo que "volteado" (es decir, girado 180 grados alrededor de la línea x = 0,5).

Así, para los problemas en los que sabemos que la salida estará entre 0 y 1, la pérdida de entropía cruzada produce gradientes más pronunciados que el MSE. La verdadera magia se produce cuando combinamos esta pérdida con la función softmax: primero pasamos la salida de la red neuronal por la función softmax para normalizarla de modo que los valores sumen 1, y luego introducimos las probabilidades resultantes en la función de pérdida de entropía cruzada.

Veamos qué aspecto tiene esto con el escenario de tres clases que hemos estado utilizando hasta ahora; la expresión para el componente del vector de pérdidas de i = 1-es decir, el primer componente de la pérdida para una observación dada, que denotaremos como SCE1-es:

SCE 1 = - y 1 × l o g ( e x 1 e x 1 +e x 2 +e x 3 ) - ( 1 - y 1 ) × l o g ( 1 - e x 1 e x 1 +e x 2 +e x 3 )

Basándonos en esta expresión, el gradiente parecería un poco más complicado para esta pérdida. Sin embargo, hay una expresión elegante que es fácil de escribir matemáticamente y fácil de aplicar:

SCE 1 x 1 = e x 1 e x 1 +e x 2 +e x 3 - y 1

Eso significa que el gradiente total a la entropía cruzada softmax es

softmax ( x 1 x 2 x 3 ) - y 1 y 2 y 3

Ya está. Como prometí, la aplicación resultante también es sencilla:

softmax_x = softmax(x, axis = 1)
loss_grad = softmax_x - y

Vamos a codificar esto.

Código

Para recapitular el Capítulo 3, se espera que cualquier clase Loss reciba dos matrices 2D, una con las predicciones de la red y otra con los objetivos. El número de filas de cada matriz es el tamaño del lote, y el número de columnas es el número de clases n en el problema de clasificación; una fila de cada una representa una observación del conjunto de datos, y los valores n de la fila representan la mejor suposición de la red neuronal sobre las probabilidades de que esa observación pertenezca a cada una de las clases n. Así pues, tendremos que aplicar el softmax a cada fila de la matriz prediction. Esto nos lleva a un primer problema potencial: a continuación introduciremos los números resultantes en la función log para calcular la pérdida. Esto debería preocuparte, ya que log(x) llega a infinito negativo cuando x llega a 0, y del mismo modo, 1 - x llega a infinito cuando x llega a 1. Para evitar valores de pérdida extremadamente grandes que podrían provocar inestabilidad numérica, recortaremos la salida de la función softmax para que no sea inferior a 10-7 ni superior a107.

¡Por fin podemos juntarlo todo!

class SoftmaxCrossEntropyLoss(Loss):
    def __init__(self, eps: float=1e-9)
        super().__init__()
        self.eps = eps
        self.single_output = False

    def _output(self) -> float:

        # applying the softmax function to each row (observation)
        softmax_preds = softmax(self.prediction, axis=1)

        # clipping the softmax output to prevent numeric instability
        self.softmax_preds = np.clip(softmax_preds, self.eps, 1 - self.eps)

        # actual loss computation
softmax_cross_entropy_loss = (
    -1.0 * self.target * np.log(self.softmax_preds) - \
        (1.0 - self.target) * np.log(1 - self.softmax_preds)
)

        return np.sum(softmax_cross_entropy_loss)

    def _input_grad(self) -> ndarray:

        return self.softmax_preds - self.target

Pronto mostraré mediante algunos experimentos en el conjunto de datos MNIST cómo esta pérdida es una mejora de la pérdida de error cuadrático medio. Pero primero vamos a hablar de las ventajas y desventajas de elegir una función de activación y ver si hay una opción mejor que la sigmoidea.

Nota sobre las funciones de activación

En el capítulo 2, argumentamos que sigmoidea era una buena función de activación porque

  • Era una función no lineal y monótona

  • Proporcionó un efecto "regularizador" en el modelo, forzando las características intermedias a un rango finito, concretamente entre 0 y 1

Sin embargo, la sigmoide tiene un inconveniente, similar al inconveniente de la pérdida por error cuadrático medio: produce gradientes relativamente planos durante el paso hacia atrás. El gradiente que se pasa a la función sigmoidea (o a cualquier función) en el paso hacia atrás representa en qué medida la salida de la función afecta en última instancia a la pérdida; como la pendiente máxima de la función sigmoidea es 0,25, estos gradientes se dividirán como mucho por 4 cuando se envíen hacia atrás a la operación anterior del modelo. Peor aún, cuando la entrada a la función sigmoidea es menor que -2 o mayor que 2, el gradiente que reciben esas entradas será casi 0, ya que sigmoide(x) es casi plana en x = -2 o x = 2. Lo que esto significa es que cualquier parámetro que influya en esas entradas recibirá gradientes pequeños, y nuestra red podría aprender lentamente como resultado.3 Además, si se utilizan varias funciones de activación sigmoideas en capas sucesivas de una red neuronal, este problema se agravará, disminuyendo aún más los gradientes que podrían recibir los pesos situados antes en la red neuronal.

¿Cómo sería una activación "en el otro extremo", con los puntos fuertes y débiles opuestos?

El otro extremo: la Unidad Lineal Rectificada

La activación Unidad Lineal Rectificada, o ReLU, es una activación de uso común con los puntos fuertes y débiles opuestos a la sigmoidea. ReLU se define simplemente como 0 si x es menor que 0, y x en caso contrario. En la Figura 4-5 se muestra un gráfico.

dlfs 0405
Figura 4-5. Activación de ReLU

Se trata de una función de activación "válida" en el sentido de que es monótona y no lineal. Produce gradientes mucho mayores que sigmoide-1 si la entrada a la función es mayor que 0, y 0 en caso contrario, para un promedio de 0,5, mientras que el gradiente máximo que puede producir sigmoide es 0,25. La activación ReLU es una opción muy popular en las arquitecturas de redes neuronales profundas, porque su inconveniente (que establece una distinción tajante y algo arbitraria entre valores menores o mayores que 0) puede abordarse con otras técnicas, incluidas algunas que se tratarán en este capítulo, y sus ventajas (producir gradientes grandes) son fundamentales para entrenar los pesos en las arquitecturas de redes neuronales profundas.

Sin embargo, existe una función de activación que es un término medio entre estas dos, y que utilizaremos en las demostraciones de este capítulo: Tanh.

Un término medio: Tanh

La función Tanh tiene una forma similar a la función sigmoidea, pero asigna las entradas a valores entre -1 y 1. La Figura 4-6 muestra esta función.

dlfs 0406
Figura 4-6. Activación del Tanh

Esta función produce gradientes significativamente más pronunciados que la sigmoidea; en concreto, el gradiente máximo de Tanh resulta ser 1, en contraste con el 0,25 de la sigmoidea. La Figura 4-7 muestra los gradientes de estas dos funciones.

dlfs 0407
Figura 4-7. Derivada sigmoidea frente a derivada Tanh

Además, al igual que f ( x ) = s i g m o i d ( x ) tiene una derivada fácil de expresar f ' ( x ) = s i g m o i d ( x ) × ( 1 - s i g m o i d ( x ) ) también f ( x ) = t a n h ( x ) tienen la derivada fácil de expresar f ' (x)=(1-tanh(x) 2 .

La cuestión aquí es que hay concesiones a la hora de elegir una función de activación, independientemente de la arquitectura: queremos una función de activación que permita a nuestra red aprender relaciones no lineales entre la entrada y la salida, sin añadir una complejidad innecesaria que dificulte a la red encontrar una buena solución. Por ejemplo, la función de activación "Leaky ReLU" de permite una ligera pendiente negativa cuando la entrada a la función ReLU es inferior a 0, mejorando la capacidad de ReLU para enviar gradientes hacia atrás, y la función de activación "ReLU6" limita el extremo positivo de ReLU a 6, introduciendo aún más no linealidad en la red. Aun así, estas dos funciones de activación son más complejas que ReLU; y si el problema al que nos enfrentamos es relativamente sencillo, esas funciones de activación más sofisticadas podrían dificultar aún más el aprendizaje de la red. Así pues, en los modelos que demostraremos en el resto de este libro, utilizaremos simplemente la función de activación Tanh, que equilibra bien estas consideraciones.

Ahora que hemos elegido una función de activación, utilicémosla para realizar algunos experimentos.

Experimentos

Hemos justificado el uso de Tanh a lo largo de nuestros experimentos, así que volvamos al punto original de esta sección: mostrar por qué la pérdida de entropía cruzada softmax es tan omnipresente en todo el aprendizaje profundo.4 Utilizaremos el conjunto de datos MNIST, que consiste en imágenes en blanco y negro de dígitos manuscritos de 28 × 28 píxeles, con el valor de cada píxel entre 0 (blanco) y 255 (negro). Además, el conjunto de datos está dividido previamente en un conjunto de entrenamiento de 60.000 imágenes y un conjunto de prueba de 10.000 imágenes adicionales. En el repositorio GitHub del libro, mostramos una función de ayuda para leer tanto las imágenes como sus etiquetas correspondientes en conjuntos de entrenamiento y de prueba utilizando la siguiente línea de código:

X_train, y_train, X_test, y_test = mnist.load()

Nuestro objetivo será entrenar una red neuronal para que aprenda cuál de los 10 dígitos del 0 al 9 contiene la imagen.

Preprocesamiento de datos

Para la clasificación, tenemos que realizar la codificación one-hot para transformar nuestros vectores que representan las etiquetas en un ndarray de la misma forma que las predicciones: concretamente, mapearemos la etiqueta "0" a un vector con un 1 en la primera posición (en el índice 0) y 0s en todas las demás posiciones, "1" a un vector con un 1 en la segunda posición (en el índice 1), y así sucesivamente:

[ 0 , 2 , 1 ] 1 0 0 ... 0 0 0 1 ... 0 0 1 0 ... 0

Por último, siempre es útil escalar nuestros datos a media 0 y varianza 1, igual que hicimos con los conjuntos de datos del "mundo real" en los capítulos anteriores. Aquí, sin embargo, como cada punto de datos es una imagen, no escalaremos cada característica para que tenga media 0 y varianza 1, ya que eso haría que los valores de los píxeles adyacentes cambiaran en cantidades diferentes, ¡lo que podría distorsionar la imagen! En lugar de eso, simplemente proporcionaremos un escalado global a nuestro conjunto de datos que reste la media global y la divida por la varianza global (ten en cuenta que utilizamos las estadísticas del conjunto de entrenamiento para escalar el conjunto de pruebas):

X_train, X_test = X_train - np.mean(X_train), X_test - np.mean(X_train)
X_train, X_test = X_train / np.std(X_train), X_test / np.std(X_train)

Modelo

Tendremos que definir nuestro modelo para que tenga 10 salidas para cada entrada: una para cada una de las probabilidades de que nuestro modelo pertenezca a cada una de las 10 clases. Como sabemos que cada una de nuestras salidas será una probabilidad, en daremos a nuestro modelo una activación sigmoid en la última capa. A lo largo de este capítulo, para ilustrar si los "trucos de entrenamiento" que estamos describiendo mejoran realmente la capacidad de aprendizaje de nuestros modelos, utilizaremos una arquitectura de modelo consistente en una red neuronal de dos capas con un número de neuronas en la capa oculta cercano a la media geométrica de nuestro número de entradas (784) y nuestro número de salidas (10): 89 784 × 10 .

Pasemos ahora a nuestro primer experimento, en el que comparamos una red neuronal entrenada con pérdida de error cuadrático medio simple con otra entrenada con pérdida de entropía cruzada softmax. Los valores de pérdida que se muestran son por observación (recuerda que, por término medio, la pérdida de entropía cruzada tendrá valores de pérdida absoluta tres veces mayores que la pérdida de error cuadrático medio). Si ejecutamos

model = NeuralNetwork(
    layers=[Dense(neurons=89,
                  activation=Tanh()),
            Dense(neurons=10,
                  activation=Sigmoid())],
            loss = MeanSquaredError(),
seed=20190119)

optimizer = SGD(0.1)

trainer = Trainer(model, optimizer)
trainer.fit(X_train, train_labels, X_test, test_labels,
            epochs = 50,
            eval_every = 10,
            seed=20190119,
            batch_size=60);

calc_accuracy_model(model, X_test)

nos da:

Validation loss after 10 epochs is 0.611
Validation loss after 20 epochs is 0.428
Validation loss after 30 epochs is 0.389
Validation loss after 40 epochs is 0.374
Validation loss after 50 epochs is 0.366

The model validation accuracy is: 72.58%

Ahora vamos a probar la afirmación que hicimos antes en el capítulo: que la función de pérdida de entropía cruzada softmax ayudaría a nuestro modelo a aprender más rápido.

Experimento: Pérdida de entropía cruzada Softmax

En primer lugar cambiemos el modelo anterior por:

model = NeuralNetwork(
    layers=[Dense(neurons=89,
                  activation=Tanh()),
            Dense(neurons=10,
                  activation=Linear())],
            loss = SoftmaxCrossEntropy(),
seed=20190119)
Nota

Como ahora alimentamos las salidas del modelo a través de la función softmax como parte de la pérdida, ya no necesitamos alimentarlas a través de una función de activación sigmoidea.

Luego lo ejecutamos para el modelo durante 50 épocas, lo que nos da estos resultados:

Validation loss after 10 epochs is 0.630
Validation loss after 20 epochs is 0.574
Validation loss after 30 epochs is 0.549
Validation loss after 40 epochs is 0.546
Loss increased after epoch 50, final loss was 0.546, using the model from
epoch 40

The model validation accuracy is: 91.01%

De hecho, ¡cambiar nuestra función de pérdida por una que proporcione gradientes mucho más pronunciados por sí sola da un enorme impulso a la precisión de nuestro modelo!5

Por supuesto, podemos hacerlo mucho mejor que esto, incluso sin cambiar nuestra arquitectura. En la próxima sección, trataremos el impulso, la extensión más importante y directa de la técnica de optimización por descenso de gradiente estocástico que hemos estado utilizando hasta ahora.

Impulso

Hasta ahora, hemos estado utilizando sólo una "regla de actualización" para nuestros pesos en cada paso temporal. Basta con tomar la derivada de la pérdida con respecto a los pesos y mover los pesos en la dirección correcta resultante. Esto significa que nuestra función _update_rule en el Optimizer tenía este aspecto:

update = self.lr*kwargs['grad']
kwargs['param'] -= update

Veamos primero la intuición de por qué querríamos ampliar esta regla de actualización para incorporar el impulso.

Intuición para el impulso

Recuerda Figura 4-3, en la que se trazaba el valor de un parámetro individual frente al valor de la pérdida de la red. Imagina un escenario en el que el valor del parámetro se actualiza continuamente en la misma dirección porque la pérdida sigue disminuyendo con cada iteración. Esto sería análogo a que el parámetro "rodara colina abajo", y el valor de la actualización en cada paso de tiempo sería análogo a la "velocidad" del parámetro. En el mundo real, sin embargo, los objetos no se detienen ni cambian de dirección instantáneamente; eso se debe a que tienen impulso, que no es más que una forma concisa de decir que su velocidad en un instante dado es una función no sólo de las fuerzas que actúan sobre ellos en ese instante, sino también de sus velocidades pasadas acumuladas, con una mayor ponderación de las velocidades más recientes. Esta interpretación física es la motivación para aplicar el momento a nuestras actualizaciones de peso. En la siguiente sección lo precisaremos.

Implementación del impulso en la clase Optimizador

Basar las actualizaciones de nuestros parámetros en el impulso significa que la actualización de los parámetros en cada paso temporal será una media ponderada de las actualizaciones de los parámetros en los pasos temporales anteriores, con los pesos decaídos exponencialmente. Por lo tanto, tendremos que elegir un segundo parámetro, el parámetro del momento, que determinará el grado de este decaimiento; cuanto mayor sea, más se basará la actualización del peso en cada paso temporal en el momento acumulado del parámetro, en lugar de en su velocidad actual.

Matemáticas

Matemáticamente, si nuestro parámetro de momento es μ, y el gradiente en cada paso temporal es t nuestra actualización de pesos es

actualiza = t + μ × t-1 + μ 2 × t-2 + ...

Si nuestro parámetro de impulso fuera 0,9, por ejemplo, multiplicaríamos el gradiente de hace un paso de tiempo por 0,9, el de hace dos pasos de tiempo por 0,92 = 0,81, el de hace tres pasos de tiempo por 0,93 = 0,729, y así sucesivamente, y finalmente sumaríamos todos ellos al gradiente del paso de tiempo actual para obtener la actualización global del peso del paso de tiempo actual.

Código

¿Cómo lo hacemos? ¿Tenemos que calcular una suma infinita cada vez que queramos actualizar nuestros pesos?

Resulta que hay una forma más inteligente. Nuestro Optimizer llevará la cuenta de una cantidad separada que representa el historial de actualizaciones de los parámetros, además de limitarse a recibir un gradiente en cada paso temporal. Luego, en cada paso temporal, utilizaremos el gradiente actual para actualizar este historial y calcular la actualización real de los parámetros en función de este historial. Como el momento se basa vagamente en una analogía con la física, llamaremos a esta cantidad "velocidad".

¿Cómo debemos actualizar la velocidad? Resulta que podemos seguir los siguientes pasos:

  1. Multiplícalo por el parámetro del momento.

  2. Añade el degradado.

Esto da lugar a que la velocidad tome los siguientes valores en cada paso temporal, empezando en t = 1:

  1. 1

  2. 2 + μ × 1

  3. 3 + μ × ( 2 + μ × 1 ) = μ × 2 + μ 2 × 1 )

Con esto, ¡podemos utilizar la velocidad como parámetro de actualización! A continuación, podemos incorporar esto a una nueva subclase de Optimizer que llamaremos SGDMomentum; esta clase tendrá las funciones step y _update_rule que tienen el siguiente aspecto:

def step(self) -> None:
    '''
    If first iteration: intialize "velocities" for each param.
    Otherwise, simply apply _update_rule.
    '''
    if self.first:
        # now we will set up velocities on the first iteration
        self.velocities = [np.zeros_like(param)
                           for param in self.net.params()]
        self.first = False

    for (param, param_grad, velocity) in zip(self.net.params(),
                                             self.net.param_grads(),
                                             self.velocities):
        # pass in velocity into the "_update_rule" function
        self._update_rule(param=param,
                          grad=param_grad,
                          velocity=velocity)

def _update_rule(self, **kwargs) -> None:
        '''
        Update rule for SGD with momentum.
        '''
        # Update velocity
        kwargs['velocity'] *= self.momentum
        kwargs['velocity'] += self.lr * kwargs['grad']

        # Use this to update parameters
        kwargs['param'] -= kwargs['velocity']

Veamos si este nuevo optimizador puede mejorar el entrenamiento de nuestra red.

Experimento: Descenso Gradiente Estocástico con Momento

Entrenemos la misma red neuronal con una capa oculta en el conjunto de datos MNIST, sin cambiar nada excepto el uso de optimizer = SGDMomentum(lr=0.1, momentum=0.9) como optimizador en lugar de optimizer = SGD(lr=0.1):

Validation loss after 10 epochs is 0.441
Validation loss after 20 epochs is 0.351
Validation loss after 30 epochs is 0.345
Validation loss after 40 epochs is 0.338
Loss increased after epoch 50, final loss was 0.338, using the model from epoch 40

The model validation accuracy is: 95.51%

Puedes ver que la pérdida es significativamente menor y la precisión significativamente mayor, ¡lo cual es simplemente el resultado de añadir impulso a nuestra regla de actualización de parámetros!6

Por supuesto, otra forma de modificar la actualización de nuestros parámetros en cada iteración sería modificar nuestra tasa de aprendizaje; aunque podemos cambiar manualmente nuestra tasa de aprendizaje inicial, también podemos decaer automáticamente la tasa de aprendizaje a medida que avanza el entrenamiento utilizando alguna regla. A continuación se describen las reglas más comunes.

Decaimiento de la Tasa de Aprendizaje

[La tasa de aprendizaje] suele ser el hiperparámetro más importante y siempre hay que asegurarse de que se ha ajustado.

Yoshua Bengio, Recomendaciones prácticas para el entrenamiento basado en gradientes de arquitecturas profundas, 2012

La motivación de para hacer decaer el ritmo de aprendizaje a medida que avanza el entrenamiento proviene, una vez más, de la Figura 4-3 de la sección anterior: aunque queremos "dar grandes pasos" hacia el principio del entrenamiento, es probable que a medida que sigamos actualizando iterativamente los pesos, lleguemos a un punto en el que empecemos a "saltarnos" el mínimo. Ten en cuenta que esto no será necesariamente un problema, ya que si la relación entre nuestros pesos y la pérdida "disminuye suavemente" a medida que nos acercamos al mínimo, como en la Figura 4-3, la magnitud de los gradientes disminuirá automáticamente a medida que disminuya la pendiente. Aun así, puede que esto no ocurra, e incluso si ocurre, el decaimiento de la tasa de aprendizaje puede darnos un control más preciso sobre este proceso.

Tipos de disminución de la tasa de aprendizaje

En hay diferentes formas de hacer decaer la tasa de aprendizaje. La más sencilla es el decaimiento lineal, en el que la tasa de aprendizaje decae linealmente desde su valor inicial hasta algún valor terminal, aplicándose el decaimiento real al final de cada época. Más concretamente, en el paso de tiempo t, si la tasa de aprendizaje con la que queremos empezar es α start y nuestra tasa de aprendizaje final es α end entonces nuestra tasa de aprendizaje en cada paso temporal es

α t = α start - ( α start - α end ) × t N

donde N es el número total de épocas.

Otro método sencillo que funciona igual de bien es el decaimiento exponencial, en el que la tasa de aprendizaje disminuye en una proporción constante cada época. En este caso, la fórmula sería simplemente

α t = α × δ t

donde:

δ = α end α start 1 N-1

Implementarlos es sencillo: inicializaremos nuestro Optimizers para que tenga una "tasa de aprendizaje final" final_lr hacia la que irá decayendo la tasa de aprendizaje inicial a lo largo de las épocas de entrenamiento:

def __init__(self,
             lr: float = 0.01,
             final_lr: float = 0,
             decay_type: str = 'exponential')
    self.lr = lr
    self.final_lr = final_lr
    self.decay_type = decay_type

Luego, al principio del entrenamiento, podemos llamar a una función _setup_decay que calcule cuánto decaerá la tasa de aprendizaje en cada época:

self.optim._setup_decay()

Estos cálculos aplicarán las fórmulas de decaimiento lineal y exponencial de la tasa de aprendizaje que acabamos de ver:

def _setup_decay(self) -> None:

    if not self.decay_type:
        return
    elif self.decay_type == 'exponential':
        self.decay_per_epoch = np.power(self.final_lr / self.lr,
                                   1.0 / (self.max_epochs-1))
    elif self.decay_type == 'linear':
        self.decay_per_epoch = (self.lr - self.final_lr) / (self.max_epochs-1)

Luego, al final de cada época, decaeremos realmente la tasa de aprendizaje:

def _decay_lr(self) -> None:

    if not self.decay_type:
        return

    if self.decay_type == 'exponential':
        self.lr *= self.decay_per_epoch

    elif self.decay_type == 'linear':
        self.lr -= self.decay_per_epoch

Por último, llamaremos a la función _decay_lr desde la función Trainer durante la función fit, al final de cada época:

if self.optim.final_lr:
    self.optim._decay_lr()

Hagamos algunos experimentos para ver si esto mejora el entrenamiento.

Experimentos: Decaimiento de la Tasa de Aprendizaje

En la siguiente dirección intentamos entrenar la misma arquitectura del modelo con una tasa de aprendizaje decreciente. Inicializamos las tasas de aprendizaje para que la "tasa media de aprendizaje" durante la ejecución sea igual a la tasa de aprendizaje anterior de 0,1: para la ejecución con decaimiento lineal de la tasa de aprendizaje, inicializamos la tasa de aprendizaje en 0,15 y la reducimos a 0,05, y para la ejecución con decaimiento exponencial, inicializamos la tasa de aprendizaje en 0,2 y la reducimos a 0,05. Para la ejecución con decaimiento lineal

optimizer = SGDMomentum(0.15, momentum=0.9, final_lr=0.05, decay_type='linear')

que tenemos:

Validation loss after 10 epochs is 0.403
Validation loss after 20 epochs is 0.343
Validation loss after 30 epochs is 0.282
Loss increased after epoch 40, final loss was 0.282, using the model from epoch 30
The model validation accuracy is: 95.91%

Para la ejecución con decaimiento exponencial, con:

optimizer = SGDMomentum(0.2, momentum=0.9, final_lr=0.05, decay_type='exponential')

que tenemos:

Validation loss after 10 epochs is 0.461
Validation loss after 20 epochs is 0.323
Validation loss after 30 epochs is 0.284
Loss increased after epoch 40, final loss was 0.284, using the model from epoch 30
The model validation accuracy is: 96.06%

Las pérdidas en los "mejores modelos" de estas ejecuciones fueron de 0,282 y 0,284, ¡significativamente inferiores a las de 0,338 que teníamos antes!

A continuación: cómo y por qué inicializar de forma más inteligente los pesos de nuestro modelo.

Inicialización del peso

Como mencionamos en la sección sobre funciones de activación, varias funciones de activación, como la sigmoidea y la Tanh, tienen sus gradientes más pronunciados cuando sus entradas son 0, y las funciones se aplanan rápidamente a medida que las entradas se alejan de 0. Esto puede limitar potencialmente la eficacia de estas funciones, porque si muchas de las entradas tienen valores alejados de 0, los pesos unidos a esas entradas recibirán gradientes muy pequeños en el paso hacia atrás.

Esto resulta ser un problema importante en las redes neuronales que estamos tratando ahora. Considera la capa oculta de la red MNIST que hemos estado examinando. Esta capa recibirá 784 entradas y luego las multiplicará por una matriz de pesos, terminando con algún número n de neuronas (y luego, opcionalmente, añadirá un sesgo a cada neurona). La Figura 4-8 muestra la distribución de estos valores n en la capa oculta de nuestra red neuronal (con 784 entradas) antes y después de alimentarlos a través de la función de activación Tanh.

Distribution of inputs to activation
Figura 4-8. Distribución de entradas a la función de activación y activaciones

Tras pasar por la función de activación, ¡la mayoría de las activaciones son -1 ó 1! Esto se debe a que cada característica está definida matemáticamente como:

f n = w 1,n × x 1 + ... + w 784,n × x 784 + b n

Como hemos inicializado cada peso para que tenga varianza 1 ( Var ( w i,j ) = 1 -y Var ( b n ) = 1 ) y Var ( X 1 + X 2 ) = Var ( X 1 ) + Var ( X 2 ) para variables aleatorias independientes X1 y X2, tenemos:

Var ( f n ) = 785

Eso le da una desviación típica ( 785 ) de algo más de 28, lo que refleja la dispersión de los valores que vemos en la mitad superior de la Figura 4-8.

Esto nos dice que tenemos un problema. ¿Pero el problema es simplemente que las características que introducimos en las funciones de activación no pueden estar "demasiado repartidas"? Si ése fuera el problema, podríamos simplemente dividir las características por algún valor para reducir su varianza. Sin embargo, eso invita a una pregunta obvia: ¿cómo sabemos por qué dividir los valores? La respuesta es que los valores deben escalarse en función del número de neuronas que se introducen en la capa. Si tuviéramos una red neuronal multicapa y una capa tuviera 200 neuronas y la siguiente 100, la capa de 200 neuronas transmitiría valores con una distribución más amplia que la capa de 100 neuronas. Esto no es deseable: no queremos que la escala de las características que aprende nuestra red neuronal durante el entrenamiento dependa del número de características transmitidas, por la misma razón que no queremos que las predicciones de nuestra red dependan de la escala de nuestras características de entrada. Las predicciones de nuestro modelo no deberían verse afectadas, por ejemplo, si multiplicamos o dividimos por 2 todos los valores de una característica.

Hay varias formas de corregirlo; aquí trataremos la más frecuente, sugerida en el párrafo anterior: podemos ajustar la varianza inicial de los pesos en función del número de neuronas de las capas que conectan, de modo que los valores transmitidos a la capa siguiente durante el paso hacia delante y a la capa anterior durante el paso hacia atrás tengan aproximadamente la misma escala. Sí, también tenemos que pensar en el paso hacia atrás, ya que en él existe el mismo problema: la varianza de los gradientes que recibe la capa durante la retropropagación dependerá directamente del número de rasgos de la capa siguiente, ya que es ésta la que envía gradientes hacia atrás a la capa en cuestión.

Matemáticas y código

¿Cómo, concretamente, equilibramos estas preocupaciones? Si cada capa tiene n in neuronas alimentándose y n out neuronas que salen, la varianza para cada peso que mantendría constante la varianza de las características resultantes sólo en el paso hacia delante sería:

1 n in

Del mismo modo, la varianza del peso que mantendría constante la varianza de las características en el paso hacia atrás sería:

1 n out

Como solución intermedia, lo que se suele llamar inicialización Glorot7 consiste en inicializar la varianza de los pesos de cada capa para que sea

2 n in +n out

La codificación es sencilla: añadimos un argumento weight_init a cada capa, y añadimos lo siguiente a nuestra función _setup_layer:

if self.weight_init == "glorot":
    scale = 2/(num_in + self.neurons)
else:
    scale = 1.0

Ahora nuestros modelos tendrán el aspecto siguiente

model = NeuralNetwork(
    layers=[Dense(neurons=89,
                  activation=Tanh(),
                  weight_init="glorot"),
            Dense(neurons=10,
                  activation=Linear(),
                  weight_init="glorot")],
            loss = SoftmaxCrossEntropy(),
seed=20190119)

con weight_init="glorot" especificado para cada capa.

Experimentos: Inicialización del peso

Ejecutando los mismos modelos de la sección anterior, pero con los pesos inicializados mediante la inicialización de Glorot, se obtiene:

Validation loss after 10 epochs is 0.352
Validation loss after 20 epochs is 0.280
Validation loss after 30 epochs is 0.244
Loss increased after epoch 40, final loss was 0.244, using the model from epoch 30
The model validation accuracy is: 96.71%

para el modelo con decaimiento lineal de la tasa de aprendizaje, y

Validation loss after 10 epochs is 0.305
Validation loss after 20 epochs is 0.264
Validation loss after 30 epochs is 0.245
Loss increased after epoch 40, final loss was 0.245, using the model from epoch 30
The model validation accuracy is: 96.71%

para el modelo con decaimiento exponencial de la tasa de aprendizaje. Aquí vemos otro descenso significativo de la pérdida, ¡desde los 0,282 y 0,284 que conseguimos antes hasta los 0,244 y 0,245! Observa que con todos estos cambios no hemos aumentado el tamaño ni el tiempo de entrenamiento de nuestro modelo; simplemente hemos ajustado el proceso de entrenamiento basándonos en nuestra intuición sobre lo que intentan hacer las redes neuronales que mostré al principio de este capítulo.

Hay una última técnica que trataremos en este capítulo. Como motivación, te habrás dado cuenta de que ninguno de los modelos que hemos utilizado a lo largo de este capítulo eran modelos de aprendizaje profundo, sino simplemente redes neuronales con una capa oculta. Esto se debe a que, sin dropout, la técnica que aprenderemos ahora, los modelos de aprendizaje profundo son muy difíciles de entrenar eficazmente sin sobreajuste.

Abandono

En este capítulo, he mostrado varias modificaciones del procedimiento de entrenamiento de nuestra red neuronal que la acercaban cada vez más a su mínimo global. Te habrás dado cuenta de que no hemos probado lo aparentemente más obvio: añadir más capas a nuestra red o más neuronas por capa. La razón es que añadir simplemente más "potencia de fuego" a la mayoría de las arquitecturas de redes neuronales puede dificultar que la red encuentre una solución que generalice bien. La intuición aquí es que, aunque añadir más capacidad a una red neuronal le permite modelar relaciones más complejas entre la entrada y la salida, también corre el riesgo de llevar a la red a una solución que se ajuste demasiado a los datos de entrenamiento. El Dropout nos permite añadir capacidad a nuestras redes neuronales, al tiempo que en la mayoría de los casos hace menos probable que la red se sobreajuste.

¿Qué es concretamente el abandono escolar?

Definición

Dropout consiste simplemente en elegir al azar cierta proporción p de las neuronas de una capa y ponerlas a 0 durante cada pasada hacia delante del entrenamiento. Este extraño truco reduce la capacidad de la red, pero empíricamente en muchos casos puede evitar que la red se ajuste en exceso. Esto es especialmente cierto en redes más profundas, en las que las características que se aprenden son, por construcción, múltiples capas de abstracción alejadas de las características originales.

Aunque el abandono puede ayudar a nuestra red a evitar el sobreajuste durante el entrenamiento, seguimos queriendo dar a nuestra red su "mejor oportunidad" de hacer predicciones correctas cuando llegue el momento de predecir. Por tanto, la operación Dropout tendrá dos "modos": un modo "entrenamiento" en el que se aplica el abandono, y un modo "inferencia", en el que no se aplica. Sin embargo, esto causa otro problema: aplicar el abandono a una capa reduce la magnitud global de los valores que se transmiten en un factor de 1 - p de media, lo que significa que si los pesos de las capas siguientes esperarían normalmente valores con magnitud M, en su lugar están obteniendo la magnitud M × ( 1 - p ) . Queremos imitar este cambio de magnitud al ejecutar la red en modo de inferencia, por lo que, además de eliminar el abandono, multiplicaremos todos los valores por 1 - p.

Para que esto quede más claro, vamos a codificarlo.

Aplicación

En podemos implementar el abandono como un Operation que clavaremos al final de cada capa. Tendrá el siguiente aspecto:

class Dropout(Operation):

    def __init__(self,
                 keep_prob: float = 0.8):
        super().__init__()
        self.keep_prob = keep_prob

    def _output(self, inference: bool) -> ndarray:
        if inference:
            return self.inputs * self.keep_prob
        else:
            self.mask = np.random.binomial(1, self.keep_prob,
                                           size=self.inputs.shape)
            return self.inputs * self.mask

    def _input_grad(self, output_grad: ndarray) -> ndarray:
        return output_grad * self.mask

En el paso hacia delante, al aplicar el abandono, guardamos una "máscara" que representa las neuronas individuales que se pusieron a 0. Luego, en el paso hacia atrás, multiplicamos el gradiente que recibe la operación por esta máscara. Esto se debe a que el abandono hace que el gradiente sea 0 para los valores de entrada que se ponen a cero (ya que cambiar sus valores ahora no tendrá ningún efecto sobre la pérdida) y deja los demás gradientes sin cambios.

Ajustar el resto de nuestro marco para tener en cuenta el abandono

Puede que te hayas dado cuenta en de que hemos incluido una bandera inference en el método _output que afecta a si se aplica o no el abandono. Para que esta bandera se llame correctamente, en realidad tenemos que añadirla en varios otros lugares a lo largo del entrenamiento:

  1. Los métodos Layer y NeuralNetwork forward tomarán como argumento inference (False por defecto) y pasarán la bandera a cada Operation, de modo que cada Operation se comportará de forma diferente en modo entrenamiento que en modo inferencia.

  2. Recuerda que en Trainer, evaluamos el modelo entrenado en el conjunto de pruebas cada eval_every épocas. Ahora, cada vez que lo hagamos, evaluaremos con la bandera inference igual a True:

    test_preds = self.net.forward(X_test, inference=True)
  3. Por último, añadimos una palabra clave dropout a la clase Layer; la firma completa de la función __init__ para la clase Layer tiene ahora el siguiente aspecto:

    def __init__(self,
                 neurons: int,
                 activation: Operation = Linear(),
                 dropout: float = 1.0,
                 weight_init: str = "standard")

    y añadimos la operación dropout añadiendo lo siguiente a la función _setup_layer de la clase:

    if self.dropout < 1.0:
        self.operations.append(Dropout(self.dropout))

Ya está. Veamos si el abandono funciona.

Experimentos: Abandono

En primer lugar, vemos que añadir dropout a nuestro modelo existente disminuye efectivamente la pérdida. Añadiendo un dropout de 0,8 (de modo que el 20% de las neuronas se pongan a 0) a la primera capa, de modo que nuestro modelo tenga el aspecto siguiente:

mnist_soft = NeuralNetwork(
    layers=[Dense(neurons=89,
                  activation=Tanh(),
                  weight_init="glorot",
                  dropout=0.8),
            Dense(neurons=10,
                  activation=Linear(),
                  weight_init="glorot")],
            loss = SoftmaxCrossEntropy(),
seed=20190119)

y entrenando el modelo con los mismos hiperparámetros que antes (decaimiento exponencial del peso desde una tasa de aprendizaje inicial de 0,2 hasta una tasa de aprendizaje final de 0,05) se obtiene

Validation loss after 10 epochs is 0.285
Validation loss after 20 epochs is 0.232
Validation loss after 30 epochs is 0.199
Validation loss after 40 epochs is 0.196
Loss increased after epoch 50, final loss was 0.196, using the model from epoch 40
The model validation accuracy is: 96.95%

Se trata de otra disminución significativa de la pérdida con respecto a lo que vimos anteriormente: el modelo alcanza una pérdida mínima de 0,196, frente a la anterior de 0,244.

El verdadero poder del abandono aparece cuando añadimos más capas. Cambiemos el modelo que hemos estado utilizando a lo largo de este capítulo para que sea un modelo de aprendizaje profundo, definiendo la primera capa oculta para que tenga el doble de neuronas que nuestra capa oculta anterior (178) y nuestra segunda capa oculta para que tenga la mitad (46). Nuestro modelo tiene el aspecto siguiente

model = NeuralNetwork(
    layers=[Dense(neurons=178,
                  activation=Tanh(),
                  weight_init="glorot",
                  dropout=0.8),
            Dense(neurons=46,
                  activation=Tanh(),
                  weight_init="glorot",
                  dropout=0.8),
            Dense(neurons=10,
                  activation=Linear(),
                  weight_init="glorot")],
            loss = SoftmaxCrossEntropy(),
seed=20190119)

Observa la inclusión del abandono en las dos primeras capas.

El entrenamiento de este modelo con el mismo optimizador anterior produce otra disminución significativa de la pérdida mínima alcanzada, ¡y un aumento de la precisión!

Validation loss after 10 epochs is 0.321
Validation loss after 20 epochs is 0.268
Validation loss after 30 epochs is 0.248
Validation loss after 40 epochs is 0.222
Validation loss after 50 epochs is 0.217
Validation loss after 60 epochs is 0.194
Validation loss after 70 epochs is 0.191
Validation loss after 80 epochs is 0.190
Validation loss after 90 epochs is 0.182
Loss increased after epoch 100, final loss was 0.182, using the model from epoch 90
The model validation accuracy is: 97.15%

Pero lo más importante es que esta mejora no es posible sin abandono. Aquí tienes los resultados del entrenamiento del mismo modelo sin abandono:

Validation loss after 10 epochs is 0.375
Validation loss after 20 epochs is 0.305
Validation loss after 30 epochs is 0.262
Validation loss after 40 epochs is 0.246
Loss increased after epoch 50, final loss was 0.246, using the model from epoch 40
The model validation accuracy is: 96.52%

Sin dropout, el modelo de aprendizaje profundo funciona peor que el modelo con una sola capa oculta, ¡a pesar de tener más del doble de parámetros y tardar más del doble de tiempo en entrenarse! Esto ilustra lo esencial que es el dropout para entrenar eficazmente los modelos de aprendizaje profundo; de hecho, el dropout fue un componente esencial del modelo ganador de ImageNet de 2012, que dio el pistoletazo de salida a la era moderna del aprendizaje profundo.8 Sin la deserción, ¡puede que no estuvieras leyendo este libro!

Conclusión

En este capítulo, has aprendido algunas de las técnicas más comunes para mejorar el entrenamiento de las redes neuronales, aprendiendo tanto la intuición de por qué funcionan como los detalles de bajo nivel de su funcionamiento. Para resumirlas, te dejamos con una lista de cosas que puedes probar para exprimir un poco más el rendimiento de tu red neuronal, independientemente del dominio:

  • Añade impulso -o una de las muchas técnicas de optimización avanzada de eficacia similar- a tu regla de actualización del peso.

  • Disminuye tu tasa de aprendizaje a lo largo del tiempo utilizando un decaimiento lineal o exponencial, como se muestra en este capítulo, o una técnica más moderna como el decaimiento del coseno. De hecho, los programas de tasa de aprendizaje más eficaces varían la tasa de aprendizaje en función no sólo de cada época, sino también de la pérdida en el conjunto de prueba, disminuyendo la tasa de aprendizaje sólo cuando esta pérdida no disminuye. ¡Deberías intentar poner esto en práctica como ejercicio!

  • Asegúrate de que la escala de tu inicialización de pesos es una función del número de neuronas de tu capa (esto se hace por defecto en la mayoría de las bibliotecas de redes neuronales).

  • Añade abandonos, sobre todo si tu red contiene varias capas totalmente conectadas sucesivamente.

A continuación, pasaremos a hablar de arquitecturas avanzadas especializadas para dominios concretos, empezando por las redes neuronales convolucionales, especializadas en comprender datos de imágenes. ¡Adelante!

1 Además, como vimos en el capítulo 3, multiplicamos estos gradientes por una tasa de aprendizaje para tener un control más preciso de cuánto cambian los pesos.

2 Podemos ser más concretos: el valor medio de - l o g ( 1 - x ) en el intervalo de 0 a 1 resulta ser 1, mientras que el valor medio de x2 en el mismo intervalo es simplemente 1 3 .

3 Para intuir por qué puede ocurrir esto: imagina que un peso w contribuye a una característica f (de modo que f = w × x 1 + ... ), y durante el paso hacia delante de nuestra red neuronal, f = -10 para alguna observación. Como sigmoide(x) es tan plana en x = -10, cambiar el valor de w no tendrá casi ningún efecto en la predicción del modelo y, por tanto, en la pérdida.

4 Por ejemplo, el tutorial de clasificación MNIST de TensorFlow utiliza la función softmax_cross_entropy_with_logits, y nn.CrossEntropyLoss de PyTorch calcula realmente la función softmax en su interior.

5 Puedes argumentar que la pérdida de entropía cruzada softmax obtiene aquí una "ventaja injusta", ya que la función softmax normaliza los valores que recibe para que sumen 1, mientras que la pérdida de error cuadrático medio simplemente obtiene 10 entradas que han pasado por la función sigmoid y no se han normalizado para que sumen 1. Sin embargo, en el sitio web del libro muestro que el MSE sigue funcionando peor que la pérdida de entropía cruzada softmax incluso después de normalizar las entradas a la pérdida de error cuadrático medio para que sumen 1 en cada observación.

6 Además, el impulso es sólo una de las formas en que podemos utilizar información más allá del gradiente del lote actual de datos para actualizar los parámetros; cubrimos brevemente otras reglas de actualización en el Apéndice A, y puedes ver estas reglas de actualización implementadas en la biblioteca Lincoln incluida en el repositorio GitHub del libro.

7 Esto es así porque lo propusieron Glorot y Bengio en un artículo de 2010: "Comprender la dificultad de entrenar redes neuronales profundas feedforward".

8 Para más información, véase G. E. Hinton y otros, "Mejorar las redes neuronales evitando la coadaptación de los detectores de rasgos".

Get Aprendizaje profundo desde cero now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.