Capítulo 3 del libro Deep Learning – Introducción práctica con Keras

3  Cómo se entrena una red neuronal

En este capítulo vamos a presentar una visión intuitiva de los  componentes principales del proceso de aprendizaje de una red neuronal. Además veremos algunos de los parámetros e hiperparámetros más relevantes en Deep Learning. En la segunda parte del capítulo proponemos poner a prueba lo aprendido con una herramienta interactiva, y ver el comportamiento de una red neuronal cuando se le cambian los valores de los parámetros e hiperparámetros.

3.1  Proceso de aprendizaje de una red neuronal
3.2  Funciones de activación
3.3  Elementos del backpropagation
3.4  Parametrización de los modelos 125
3.5  Practicando con una clasificación binaria

3.1        Proceso de aprendizaje de una red neuronal

Recordemos del capítulo anterior que una red neuronal está formada de neuronas conectadas entre ellas; a su vez, cada conexión de nuestra red neuronal está asociada a un peso que dictamina la importancia que tendrá esa relación en la neurona al multiplicarse por el valor de entrada.

Cada neurona tiene una función de activación que define la salida de la neurona (recordemos la función sigmoid del capítulo anterior). La función de activación se usa para introducir la no linealidad en las capacidades de modelado de la red (disponemos de varias opciones de funciones de activación que presentaremos en la siguiente sección).

Entrenar nuestra red neuronal, es decir, aprender los valores de nuestros parámetros (pesos wij y sesgos bj) es la parte más genuina de Deep Learning  y podemos ver este proceso de aprendizaje en una red neuronal como un proceso iterativo de “ir y venir” por las capas de neuronas. El “ir” propagando hacia delante lo llamaremos  forwardpropagation y el “venir” retropropagando información en la red lo llamamos  backpropagation.

La primera fase forwardpropagation se da cuando se expone la red a los datos de entrenamiento y estos cruzan toda la red neuronal para ser calculadas sus predicciones (labels). Es decir, pasar los datos de entrada a través de la red de tal manera que todas las neuronas apliquen su transformación a la información que reciben de las neuronas de la capa anterior y la envíen a las neuronas de la capa siguiente. Cuando los datos hayan cruzado todas las capas, y todas sus neuronas han realizado sus cálculos, se llegará a la capa final con un resultado de predicción de la label para aquellos ejemplos de entrada.

A continuación usaremos una función de loss  para estimar la loss (o error) y para comparar y medir cuán bueno/malo fue nuestro resultado de la predicción en relación con el resultado correcto (recordemos que estamos en un entorno de aprendizaje supervisado y disponemos de la etiqueta que nos indica el valor esperado). Idealmente, queremos que nuestro coste sea cero, es decir, sin divergencia entre valor estimado y el esperado. Por eso a medida que se entrena el modelo se irán ajustando los pesos de las interconexiones de las neuronas de manera automática hasta obtener buenas predicciones.

Una se tiene calculado la loss, se propaga hacia atrás esta información. De ahí  su nombre, retropropagación, en inglés backpropagation. Partiendo de la capa de salida, esa información de loss se propaga hacia todas las neuronas de la capa oculta que contribuyen directamente a la salida. Sin embargo las neuronas de la capa oculta solo reciben una fracción de la señal total de la loss, basándose aproximadamente en la contribución relativa que haya aportado cada neurona a la salida original. Este proceso se repite, capa por capa, hasta que todas las neuronas de la red hayan recibido una señal de loss que describa su contribución relativa a la loss total.

Visualmente, podemos resumir lo que hemos contado con este esquema visual de las etapas:

Ahora que ya hemos propagado hacia atrás esta información, podemos ajustar los pesos de las conexiones entre neuronas. Lo  que estamos haciendo es que la loss se aproxime lo más posible a cero la próxima vez que volvamos usar la red para una predicción. Para ello usaremos una técnica llamada gradient descent.  Esta técnica va cambiando los pesos en pequeños incrementos con la ayuda del cálculo de la derivada (o gradiente) de la función de loss, cosa que nos permite ver en qué dirección “descender” hacia el mínimo global; esto lo va haciendo en general en lotes de datos (batches) en las sucesivas iteraciones (epochs) del conjunto de todos los datos que le pasamos a la red en cada iteración.

Recapitulando, el algoritmo de aprendizaje consiste en:

  • Empezar con unos valores (a menudo aleatorios –random en inglés–) para los parámetros de la red (pesos wij y sesgos bj)
  • Coger un conjunto de ejemplos de datos de entrada y pasarlos por la red para obtener su predicción.
  • Comparar estas predicciones obtenidas con los valores de etiquetas esperadas y con ellas calcular la loss.
  • Realizar el backpropagation para propagar esta loss a todos y cada uno de los parámetros que conforman el modelo de la red neuronal.
  • Usar esta información propagada para actualizar con el gradient descent los parámetros de la red neuronal de manera que reduzca la loss total y obtener un mejor modelo.
  • Continuar iterando en los anteriores pasos hasta que consideremos que tenemos un buen modelo (más adelante veremos cuando debemos parar).

A continuación presentamos en más detalle de cada uno de los elementos que hemos destacado en esta sección.

3.2        Funciones de activación

Recordemos que usamos las funciones de activación para propagar hacia adelante la salida de una neurona. Esta salida la reciben las neuronas de la siguiente capa a las que está conectada esta neurona (hasta la capa de salida incluida). Como hemos comentado, la función de activación sirve para introducir la no linealidad en las capacidades de modelado de la red. A continuación vamos a enumerar las más usadas en la actualidad; todas ellas pueden usarse en una capa de Keras (podemos encontrar más información en  su página web[1]).

Linear

La función de activación linear es básicamente la función identidad en la que, en términos prácticos, significa que la señal no cambia.

Sigmoid

La función sigmoid fue introducida en el capítulo anterior y permite reducir valores extremos o atípicos en datos válidos sin eliminarlos. Una función sigmoidea convierte variables independientes de rango casi infinito en probabilidades simples entre 0 y 1. La mayor parte de su salida estará muy cerca de los extremos de 0 o 1 (resulta importante que los valores estén en este rango en algunos tipos de redes neuronales).

Tanh

Del mismo modo que la tangente representa una relación entre el lado opuesto y el adyacente de un triángulo rectángulo, tanh representa la relación entre el seno hiperbólico y el coseno hiperbólico: tanh(x) = sinh(x)/cosh(x). A diferencia de la función sigmoid, el rango normalizado de tanh está entre  -1 y 1, que es la entrada que le va bien a algunas redes neuronales. La ventaja de tanh es que puede tratar más fácilmente con números negativos.

Softmax

La función de activación softmax fue presentada en el capítulo anterior para generalizar la regresión logística, en la medida de que en lugar de clasificar en binario puede contener múltiples límites de decisión.  Como ya hemos visto, la función de activación de softmax devuelve la distribución de probabilidad sobre clases de salida mutuamente excluyentes. Softmax se encontrará a menudo en la capa de salida de un clasificador, tal como ocurre en el primero que el lector ha visto en el capítulo anterior.

ReLU

La función de activación rectified linear unit (ReLU) es una transformación muy interesante que activa un solo nodo si la entrada está por encima de cierto umbral. El comportamiento por defecto y más habitual es que mientras la entrada tenga un valor por debajo de cero, la salida será cero, pero cuando la entrada se eleva por encima, la salida es una relación lineal con la variable de entrada de la forma f (x) = x. La función de activación ReLU ha demostrado funcionar en muchas situaciones diferentes, y actualmente es muy usada.

3.3        Elementos del backpropagation

En resumen, podemos ver el backpropagation como un método para alterar los parámetros (pesos y sesgos) de la red neuronal en la dirección correcta. Empieza primero por calcular el término de loss, y luego los parámetros de la red neuronal son ajustados en orden reverso con un algoritmo de optimización teniendo en cuenta esta loss calculada.

Recordemos que en Keras  se dispone del método compile() para definir cómo queremos que sean los componentes que intervienen en el proceso de aprendizaje:

model.compile(loss='categorical_crossentropy',
              optimizer='sgd',
              metrics=['accuracy'])

En concreto, en este ejemplo se le pasan tres argumentos: un optimizador, una función de loss y una lista de métricas. En los problemas de clasificación como nuestro ejemplo se usa la accuracy como métrica. Entremos un poco más en detalle de estos argumentos.

Función de loss

Una función de loss es uno de los parámetros requeridos para cuantificar lo cercano que está una determinada red neuronal de su ideal mientras está en el proceso de entrenamiento.

En la página del manual de Keras[2] podemos encontrar todos los tipos de funciones de loss disponibles en ella.  Algunos tienen sus hiperparámetros concretos que deben ser indicados; en el ejemplo del capítulo anterior, cuando usamos categorical_crossentropy como función de loss, nuestra salida debe ser en formato categórico, es decir que la variable de salida debe tomar un valor entre los 10 posibles. La elección de la mejor función de loss reside en entender qué tipo de error es o no es aceptable para el problema en concreto.

Optimizadores

El optimizador es otro de los argumentos que se requieren en el método de compile(). Keras dispone en estos momentos de diferentes optimizadores que pueden usarse: SGD, RMSprop, Adagrad, Adadelta, Adam, Adamax, Nadam.  Se puede encontrar más detalle de cada uno de ellos en la documentación de Keras[3].

De forma general, podemos ver el proceso de aprendizaje como un problema de optimización global donde los parámetros (los pesos y los sesgos) se deben ajustar de tal manera que la función de loss presentada anteriormente se minimice.  En la mayoría de los casos, estos parámetros no se pueden resolver analíticamente, pero en general se pueden aproximar bien con algoritmos de optimización iterativos u optimizadores, como los mencionados anteriormente.

Gradient descent

Vamos aquí a explicar uno de los optimizadores para que veamos su funcionamiento en general. Concretamente el gradient descent, la base de muchos y uno de los algoritmos de optimización más comunes en Machine Learning y Deep Learning.

Gradient descent usa la primera derivada (gradiente) de la función de loss cuando realiza la actualización en los parámetros.  Recordemos que el gradiente nos da la pendiente de una función en ese punto. Sin poder entrar en detalle, el proceso consiste en encadenar las derivadas de la loss de cada capa oculta a partir de las derivadas de la loss de su capa superior, incorporando su función de activación en el cálculo (por eso las funciones de activación deben ser derivables).  En cada iteración, una vez todas las neuronas disponen del valor del gradiente de la función loss que les corresponde, se actualizan los valores de los parámetros en el sentido contrario a la que indica el gradiente. El gradiente, en realidad, siempre apunta en el sentido en la que se incrementa el valor de la función de loss. Por tanto, si se usa el negativo del gradiente podemos conseguir el sentido en que tendemos a reducir  la función de loss.

Veamos el proceso de manera visual asumiendo solo una feature: supongamos que esta línea representa los valores que toma la función de loss para cada posible parámetro y en el punto inicial indicado el negativo del gradiente se indica por la flecha:

Para determinar el siguiente valor para el parámetro (para simplificar la explicación, consideremos solo el peso/weight), el algoritmo de gradient descent  modifica el valor del peso inicial para ir en sentido contrario al del gradiente (ya que este apunta en el sentido en que crece la loss y queremos reducirla), añadiendo una cantidad proporcional a este. La magnitud de este cambio está determinado por el valor del gradiente y por un hiperparámetro learning rate (que presentaremos en breve) que podemos especificar. Por lo tanto, conceptualmente es como si siguiéramos la dirección de la pendiente cuesta abajo hasta que alcanzamos un mínimo local:

El algoritmo gradient descent repite este proceso acercándose cada vez más al mínimo hasta que el valor del parámetro llega a un punto más allá del cual no puede disminuir la función de loss:

Más adelante presentaremos maneras de parar este proceso iterativo.

Stochastic Gradient Descent (SGD)

De momento no hemos hablado sobre con qué frecuencia se ajustan los valores de los parámetros:

  • ¿Después de cada ejemplo de entrada?
  • ¿Después de cada recorrido sobre todo el conjunto de ejemplos de entrenamiento (epoch)?
  • ¿Después de una muestra de ejemplos del conjunto de entrenamiento?

En el primer caso hablamos de online learning,cuando se estima el gradiente a partir de la loss observada para cada ejemplo del entrenamiento; también es cuando originalmente hablábamos de Stochastic Gradient Descent (SGD). Mientras que el segundo se conoce por batch learning y se llama Batch Gradient Descent. La literatura indica que se suele obtener mejores resultados con el online learning, pero existen motivos que justifican el batch learning porque muchas técnicas de optimización solo funcionan con él.

Pero si los datos están bien distribuidos, un pequeño subconjunto de ellos nos debería dar una idea bastante buena del gradiente. Quizás no se obtenga su mejor estimación, pero es más rápido, y debido a que estamos iterando, esta aproximación nos sirve; por esto se usa a menudo la tercera opción antes mencionada conocida como mini-batch. Esta opción suele ser mejor que la online y se requieren menos cálculos para actualizar los parámetros de la red neuronal. Además, el cálculo simultáneo del gradiente para muchos ejemplos de entrada puede realizarse utilizando operaciones con matrices que se implementan de forma muy eficiente con GPU, como hemos visto en el capítulo 1.

Por eso, en realidad muchas aplicaciones usan el stochastic gradient descent (SGD) con un mini-bach de varios ejemplos. Para usar todos los datos lo que se hace es particionar los datos en varios lotes (que llamaremos batches). Entonces cogemos el primer batch, se pasa por la red, se calcula el gradiente de su loss y se actualizan los parámetros de la red neuronal; esto seguiría sucesivamente hasta el último batch.  Ahora, en una sola pasada por todos los datos de entrada, se han realizado solo un número de pasos igual que el número de batches.

SGD es muy fácil de implementar en Keras.  En el método compile() se indica que el optimizador es SGD (valor sgd en el argumento), y entonces todo lo que se debe hacer es especificar el tamaño de batch en el proceso de entrenamiento con el método fit() de la siguiente manera:

model.fit(X_train, y_train, epochs=5, batch_size=100)

Aquí, estamos dividiendo nuestros datos en batches de 100 con el argumento de batch_size.  Con el número de epochs estamos indicando cuantas veces realizamos este proceso sobre todos los datos. Más adelante en este capítulo, cuando ya hayamos presentado los parámetros habituales de los optimizadores, volveremos a hablar de estos dos argumentos.

3.4        Parametrización de los modelos

Si el lector al acabar el anterior capítulo ha creado un modelo con los hiperparámetros que allí usamos, supongo que la accuracy del modelo (es decir, el número de veces que acertamos respecto el total) le habrá salido sobre el 90%. ¿Son buenos estos resultados? Yo creo que son fantásticos, porque significa que el lector ya ha programado y ejecutado su primera red neuronal con Keras. Congratulations!

Otra cosa es que haya modelos que permiten mejorar la accuracy. Y esto depende de tener mucho conocimiento y práctica para manejarse bien con los muchos hiperparámetros que podemos cambiar: por ejemplo, con un simple cambio de la función de activación de la primera capa, pasando de una sigmoid a una relu:

model.add(Dense(10, activation='relu', input_shape=(784,)))

Podemos obtener un 2% más de accuracy con un tiempo de cálculo aproximadamente el mismo.

También es posible aumentar el número de epochs, agregar más neuronas en una capa o agregar  más capas. Sin embargo, en estos casos las ganancias en accuracy tienen el efecto lateral de que aumenta el tiempo de ejecución del proceso de aprendizaje. Por ejemplo, si a la capa intermedia en vez de 10  nodos le ponemos 512 nodos:

model.add(Dense(512, activation='relu', input_shape=(784,)))

Podemos comprobar con el método summary() que aumenta el número de parámetros (recordemos que es una fully connected) y el tiempo de ejecución es notablemente superior, incluso reduciendo el número de epochs.  Con este modelo la accuracy,  llega a 94%. Y si aumentamos a 20 epochs ya se consigue una accuracy de 96%.

En definitiva, todo un mundo de posibilidades que veremos con más detalle en los siguientes capítulos, pero que el lector ya puede irse dando cuenta de que encontrar la mejor arquitectura con los mejores parámetros e hiperparámetros de las funciones de activación requiere cierta pericia y experiencia dadas las múltiples posibilidades que veremos que tenemos.

Parámetros e hiperparámetros

Hasta ahora, por simplicidad, no hemos puesto atención explícita a diferenciar entre parámetros e hiperparámetros, pero creo que ha llegado el momento.  En general, consideramos un parámetro del modelo como una variable de configuración que es interna al modelo y cuyo valor puede ser estimado a partir de los datos. En cambio, por hiperparámetro nos referimos a variables de configuración que son externas al modelo en sí mismo y cuyo valor en general no puede ser estimado a partir de los datos, y son especificados por el programador para ajustar los algoritmos de aprendizaje.

Cuando digo que  Deep Learning es más un arte que una ciencia me refiero a que se requiere mucha experiencia e intuición para encontrar los valores óptimos de estos hiperparámetros, que se deben especificar antes de iniciar el proceso de entrenamiento para que los modelos entrenen mejor y más rápidamente.

Dado el carácter introductorio del libro no entraremos en detalle sobre todos ellos, pero tenemos hiperparámetros que vale la pena comentar brevemente, tanto a nivel de estructura y topología de la red  neuronal (número de capas, número de neuronas, sus funciones de activación, etc) como hiperparámetros a nivel de algoritmo de aprendizaje  (learning rate, momentum, epochs, batch size, etc).

A continuación, vamos a introducir alguno de ellos y el resto irán apareciendo en los siguientes capítulos a medida que vayamos entrando en redes neuronales convolucionales y redes neuronales recurrentes.

Número de epochs

Como ya hemos avanzado, epochs nos indica el número de veces en las que todos los datos de entrenamiento han pasado por la red neuronal en el proceso de entrenamiento. Como presentaremos más adelante, una buena pista es incrementar el número de epochs hasta que la métrica accuracy con los datos de validación empieza a decrecer, incluso cuando la accuracy de los datos de entrenamiento continua incrementándose (es cuando detectamos un potencial sobreajuste u overfitting).

Batch size

Tal como hemos comentado antes, podemos particionar los datos de entrenamiento en mini lotes  para pasarlos por la red.  En Keras, como hemos visto, el  batch_size es el argumento que indica el tamaño que se usará de estos lotes en el método fit() en una iteración del entrenamiento para actualizar el gradiente.  El tamaño óptimo dependerá de muchos factores, entre ellos de la capacidad de memoria del computador que usemos para hacer los cálculos.

Learning rate

El vector de gradiente tiene una dirección y una magnitud. Los algoritmos de gradient descent multiplican la magnitud del gradiente por un escalar conocido como learning rate (también denominado a veces step size) para determinar el siguiente punto; por ejemplo, si la magnitud del gradiente es 1,5 y el learning rate es 0,01, entonces el algoritmo de gradient descent seleccionará el siguiente punto a 0,015 del punto anterior.

El valor adecuado de este hiperparámetro es muy dependiente del problema en cuestión, pero en general, si este es demasiado grande, se está dando pasos enormes que podría ser bueno para ir rápido en el proceso de aprendizaje, pero es posible que se salte el mínimo y dificultar así que el proceso de aprendizaje se detenga  porque al buscar el siguiente punto perpetuamente rebota al azar en el fondo del “pozo”. Visualmente se aprecia en esta figura el efecto que puede producirse, en que nunca se llega al valor mínimo (indicado con una pequeña flecha en el dibujo):

Contrariamente, si el learning rate es pequeño, se harán avances constantes y pequeños, teniéndose  una mejor oportunidad de llegar a un mínimo local, pero esto puede provocar que el proceso de aprendizaje sea muy lento. En general, una buena regla es si nuestro modelo de aprendizaje no funciona, disminuir la learning rate. Si sabemos que el gradiente de la función de loss es pequeño, entonces es seguro probar con learning rate que compensen el gradiente.

Learning rate decay

Ahora bien, el mejor learning rate  en general es aquel que disminuye a medida que el modelo se acerca a una solución. Para conseguir este efecto, disponemos de otro hipermarámetro, el learning rate decay, que se usa para disminuir el learning rate a medida que van pasando epochs para permitir que el aprendizaje avance más rápido al principio con learning rates más grandes. A medida que se avanza, se van haciendo ajustes cada vez más pequeños para facilitar que converja el proceso de entrenamiento al mínimo de la función loss.

Momentum

En el ejemplo visual con el que hemos explicado el algoritmo de gradient descent, para minimizar la función de loss se tiene la garantía de encontrar el mínimo global porque no hay un mínimo local en el que su optimización se pueda atascar.  Sin embargo, en realidad, los casos reales son más complejos y visualmente es como si nos pudiéramos encontrar con varios mínimos locales y la función loss tuviera una forma como la de la siguiente figura:

En este caso, el optimizador puede quedarse atascado fácilmente en un mínimo local y el algoritmo puede pensar que se ha alcanzado el mínimo global, lo que lleva a resultados subóptimos. El motivo es que en el momento en que nos atascamos, el gradiente es cero, y ya no podemos salir del mínimo local siguiendo estrictamente lo que hemos contado de guiarnos por el gradiente.

Una manera de solventar esta situación podría ser reiniciar el proceso desde diferentes posiciones aleatorias y, de esta manera, incrementar la probabilidad de llegar al mínimo global.

Para evitar esta situación, otra solución que generalmente se utiliza involucra el hiperparámetro momentum. De manera intuitiva, podemos verlo como si un avance tomara el promedio ponderado de los pasos anteriores para obtener así un poco de ímpetu y superar los “baches” como una forma de no atascarse en los mínimos locales. Si consideramos que el promedio de los anteriores eran mejores, quizás nos permita hacer el salto.

Pero usar la media ha demostrado ser una solución muy drástica, porque quizás en gradientes anteriores es mucho menos relevante que justo en el gradiente anterior. Por eso se ha optado por ponderar los anteriores gradientes, y el momentum es una constante entre 0 y 1 que se usa para esta ponderación. Se ha demostrado que los algoritmos que usan momentum funcionan mejor en la práctica.

Una variante es el Nesterov momentum,  que es una versión ligeramente diferente de la actualización del momentum que muy recientemente ha ganado popularidad y que básicamente ralentiza el gradiente cuando está ya cerca de la solución.

Pero dado el carácter introductorio del libro, descarto entrar entrar en más detalle en explicar este hiperparámetro.

Inicialización de los pesos de los parámetros

La inicialización del peso de los parámetros no es exactamente un hiperparámetro, pero es tan importante como cualquiera de ellos y por eso hacemos un breve inciso en esta sección. Es recomendable inicializar los pesos con unos valores aleatorios y pequeños para romper la simetría entre diferentes neuronas, puesto que si dos neuronas tienen exactamente los mismos pesos siempre tendrán el mismo gradiente; eso supone que ambas  tengan los mismos valores en las subsecuentes iteraciones, por lo que no serán capaces de aprender características diferentes.

El inicializar los parámetros de forma aleatoria siguiendo una distribución normal estándar es correcto, pero nos puede provocar posibles problemas de vanishing gradients o exploding gradients, que trataremos en el capítulo 7. En general se pueden usar heurísticas teniendo en cuenta el tipo de funciones de activación que tiene nuestra red. Queda fuera del nivel introductorio de este libro entrar en estos detalles, pero si el lector quiere profundizar un poco más le propongo que visite la página web del curso CS231n[4] de Andrej Karpathy en Stanford, donde encontrará conocimientos muy valiosos en esta área expuestos de manera muy didáctica (en inglés).

Hiperparámetros y optimizadores en Keras

¿Cómo podemos especificar estos hiperparámetros? Recordemos que el optimizador es uno de los argumentos que se requieren en el método compile() del modelo; hasta ahora los hemos llamado por su nombre (con un simple strings que los identifica). Pero Keras permite también pasar como argumento una instancia de la clase optimizer con la especificación de alguno hiperparámetros.

Por ejemplo el optimizador stochastic gradient descent permite usar los hiperparámetros momentum, learning rate decay y Nesterov momentum.  En concreto:

keras.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=False)

Los valores indicados en los argumentos del anterior método son los que toma por defecto y cuyo rango pueden ser:

  • lr: float >= 0. (learning rate)
  • momentum: float >= 0
  • decay: float >= 0 (learning rate decay).
  • nesterov: boolean (que indica si usar o no Nesterov momentum).

Como hemos dicho,  hay varios optimizadores en Keras que el lector puede explorar en su página de documentación[5].

3.5        Practicando con una clasificación binaria

Dado el carácter introductorio de este libro, una herramienta muy interesante que puede ser de ayuda al lector para ver cómo se comporta una red neuronal y poner en práctica alguno de los conceptos aquí presentados sin tener que entrar en las matemáticas que hay detrás es TensorFlow Playground[6].

TensorFlow Playground

TensorFlow Playground es una aplicación web de visualización interactiva escrita en JavaScript que nos permite simular redes neuronales simples que se ejecutan en nuestro navegador, y ver los resultados en tiempo real:

Con esta herramienta podremos experimentar con diferentes hiperparámetros y ver su comportamiento.  Precisamente, la flexibilidad de las redes neuronales es una de sus virtudes y a la vez uno de sus inconvenientes para los que se inician en el tema: ¡hay muchos  hiperparámetros para ajustar!

Para empezar, elegimos el conjunto de datos indicado en el apartado “DATA” que se muestra en la figura anterior, y a continuación pulsamos el botón de “Play”. Ahora podemos ver como TensorFlow Playgrond resuelve este problema particular. La línea entre la zona de color azul y naranja empieza a moverse lentamente.  Puede pulsar el botón de “Reset” y volver a probarlo varias veces para ver como la línea se mueve con diferentes valores iniciales a medida que vemos como el contador epoch va aumentando.

Para empezar a entender cómo funciona la herramienta podemos usar el primer ejemplo de perceptron presentado en este libro en el capítulo anterior, un simple problema de clasificación.

En este caso la aplicación trata de encontrar los mejores valores de los parámetros que permitan clasificar correctamente estos puntos. Si pone el cursor encima de los arcos, el lector verá que aparece el valor que se ha asignado a cada parámetro (e incluso permite editarlo, y probarlo para ver su efecto):

Recordemos que este peso dictamina la importancia que tendrá esa relación en la neurona al multiplicarse por el valor de entrada.

Después de esta primera toma de contacto vamos a presentar un poco la herramienta que nos permitirá entender cómo se comporta una red neuronal. En la parte superior del menú nos encontramos básicamente hiperparámetros, alguno de los cuales ya comentados en el capítulo anterior: Epoch, Learning rate, Activation, Regularization rate (lo veremos en el capítulo 6),  y Problem type. Todos ellos son menús desplegables en los que podemos elegir el valor de estos hiperparámetros.

En la pestaña Problem type la plataforma permite especificar dos tipos de problema: Regresión (problema continuo) y Clasificación. En total, hay cuatro tipos de datos que podemos elegir para clasificación  y dos tipos para regresión:

Los puntos azules y naranjas forman el conjunto de datos (dataset). Los puntos naranjas tienen el valor -1 y los puntos azules el de +1. En la parte lateral izquierda, debajo del tipo de datos, se encuentran diferentes parámetros que podemos modificar para tunear nuestros datos de entrada.

Usando la pestaña “Ratio of training to test data” podemos controlar el porcentaje de datos que se asigna al conjunto de entrenamiento (si lo modificamos vemos como interactivamente se cambian los puntos que aparecen en el “OUTPUT” de la derecha de la pantalla). El nivel de ruido de los datos también puede definirse y controlarse por el campo “Noise”; el patrón de datos se vuelve más irregular a medida que aumenta el ruido. Como podemos experimentar, cuando el ruido es cero, los datos del problema se distinguen claramente en sus regiones. Sin embargo, al llegar a más de 50, puede verse que los puntos azules y los puntos anaranjados se mezclan, por lo que es muy difícil clasificarlos.

Con “Batch size”, como su nombre indica,  podemos determinar la cantidad de datos que se usará para cada batch de entrenamiento.

A continuación, en la siguiente columna podemos realizar la selección de características. Propongo que usemos  “X1” y “X2” entre los muchos de que disponemos: “X1” es un valor en el eje horizontal, y “X2” es el valor en el eje vertical.

La topología de la red se puede definir en la siguiente columna. Podemos tener hasta seis capas ocultas (agregando capas ocultas, haciendo clic en el signo “+”). Y podemos tener hasta ocho neuronas por capa oculta (haciendo clic en el signo “+” de la capa correspondiente):

Finalmente, recordar que al entrenar la red neuronal queremos minimizar la “Training loss” y luego ver que con los datos de test también se minimiza la “Test loss”.  Los cambios de ambas métricas en cada epoch se muestran interactivamente en la parte superior derecha de la pantalla, en una pequeña gráfica, donde si la loss es reducida la curva va hacia abajo. La test loss se pinta en color negro, y la training loss se pinta en gris.

Clasificación con una sola neurona

Ahora que sabemos un poco más sobre esta herramienta volvamos al primer ejemplo de clasificación que separa en dos grupos (clusters) los datos.

Les propongo que modifiquemos algunos parámetros para coger soltura con la herramienta antes de avanzar. Por ejemplo, podemos modificar algunos parámetros con un learning rate de 0.03 y una función de activación ReLU  (la regularización no vamos a usarla puesto que aún no la hemos presentado hasta el capítulo 6).

Mantenemos el problema como de clasificación, y propongo que pongamos el “ratio of training-to-test” al 50% de los datos y que mantengamos también el parámetro “noise” a cero para facilitar la visualización de la solución (aunque les propongo que más tarde jueguen con él por su cuenta). El  “batch size” lo podemos dejar a 10.

Y, como antes,  usaremos “X1” y “X2” para la entrada. Sugiero comenzar con una sola capa oculta de una sola neurona. Podemos conseguir esto usando los botones de “-“ o “+”:

En la parte superior derecha vemos que los valores iniciales de la “Test loss” y “Training loss“ son altos (al lector le pueden salir valores diferentes dado que los valores iniciales son generados de manera aleatoria).  Pero después de pulsar el botón “play” puede verse que tanto la “Training loss” como la “Test loss” convergen a unos ratios muy bajos  y se mantiene.  Además, en este caso, ambas líneas, negra y gris, se superponen perfectamente.

 Clasificación con más de una neurona

Escojamos otro conjunto de datos de partida como el de la figura adjunta:

Queremos ahora separar los dos conjuntos de datos: los de color naranja se deben clasificar en un grupo y los azules en otro. Pero el problema es que, en este caso, tendrán una forma circular donde los puntos naranja estarán en el círculo exterior y los puntos azules estarán dentro; ahora no se pueden separar estos puntos con una sola línea como antes. Si entrenamos con una capa oculta que tiene una sola neurona, la clasificación anterior aquí fallará.

Les propongo que probemos con múltiples neuronas en la capa oculta. Por ejemplo, prueben con dos neuronas: verán que aún no afina suficiente. Les propongo que después prueben con tres. Verán que al final pueden conseguir una loss tanto de training como de test mucho mejores:

Vayamos a por otro de los ejemplos de la izquierda, aquel donde los datos están divididos en cuatro zonas cuadradas diferentes. Ahora, este problema no se puede solucionar con la anterior red, pero propongo que lo prueben:

Como ven, no somos capaces de conseguir una buena clasificación (aunque en algún caso podría suceder que con solo 3 neuronas funciona dado que la inicialización es aleatoria, pero si hacen varias pruebas verán que no se consigue en general). Pero si tenemos 5 neuronas  como en la figura siguiente, el lector puede ver cómo esta red neuronal consigue una buena clasificación para este caso:

Clasificación con varias capas

Ahora trataremos de clasificar el conjunto de datos con el patrón más complejo que tenemos en esta herramienta. La estructura de remolino de los puntos de datos naranja y azul es un problema desafiante. Si nos basamos en la red anterior, vemos que ni llegando a tener 8 neuronas, el máximo que nos deja la herramienta, conseguimos un buen resultado de clasificación:

Si lo ha probado el lector, en este caso supongo que obtendrá unos valores poco buenos para la loss  de test. Ha llegado el momento de poner más capas; les aseguro que si usan todas las que permite la herramienta lo conseguirán:

Pero verán que como es obvio, el proceso de aprendizaje de los parámetros tarda mucho.

En realidad, con menos capas o neuronas se pueden conseguir unos buenos resultados; les reto a que jueguen un poco por su cuenta, también cambiando por ejemplo las funciones de activación para conseguir un modelo más simple. También pueden considerar probar cualquier otro parámetro.

Esta herramienta solo considera redes neuronales densas; después veremos que las redes neuronales convolucionales y las redes neuronales recurrentes presentan dilemas adicionales, y más complejos. Pero ya solo con estas redes densas podemos ver que uno de los hiperparámetros más difíciles de ajustar es el de decidir cuántas capas tiene el modelo y cuántas neuronas tiene cada una de estas capas.

Como veremos en siguientes capítulos, usar muy pocas neuronas en las capas ocultas dará lugar a lo que es llamado sobreajuste o underfitting, una falta de ajuste del modelo debido a que  hay muy pocas neuronas en las capas ocultas para detectar adecuadamente las señales en un conjunto de datos complicado.

Por otro lado, usar demasiadas neuronas en las capas ocultas puede causar varios problemas. Primero, puede producir overfitting,  que ocurre cuando la red neuronal tiene tanta capacidad de procesamiento de información que la cantidad limitada de información contenida en el conjunto de entrenamiento no es suficiente para entrenar a todas las neuronas en las capas ocultas.  Pero por otro lado, una cantidad grande de neuronas en las capas ocultas puede aumentar el tiempo necesario para entrenar la red hasta el punto de que es imposible entrenar adecuadamente la red neuronal en el tiempo necesario.

Obviamente, se debe llegar a un compromiso entre demasiadas y muy pocas neuronas en las capas ocultas y por eso ya he comentado anteriormente que nos encontramos ante un reto que requiere más arte que ciencia.


Referencias:

[1] Véase https://keras.io/activations/

[2] Véase https://keras.io/losses/

[3] Véase https://keras.io/optimizers/

[4] CS231n Convolutional Neural Networks for Visual Recognition. [online] Disponible en http://cs231n.github.io/neural-networks-2/#init [Accedido 20/04/2018]

[5] Véase http://keras.io/optimizers

[6] Véase http://playground.tensorflow.org

2018-06-12T20:00:15+00:00June 11th, 2018|