Capítulo 4. Patrones de entrenamiento del modelo

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

Los modelos de aprendizaje automático suelen entrenarse de forma iterativa, y este proceso iterativo se denomina informalmente bucle de entrenamiento. En este capítulo, discutiremos cómo es el típico bucle de entrenamiento, y catalogaremos una serie de situaciones en las que podrías querer hacer algo diferente.

Bucle de entrenamiento típico

Los modelos de aprendizaje automático pueden entrenarse utilizando distintos tipos de optimización. Los árboles de decisión suelen construirse nodo a nodo basándose en una medida de ganancia de información. En los algoritmos genéticos, los parámetros del modelo se representan como genes, y el método de optimización implica técnicas basadas en la teoría evolutiva. Sin embargo, el enfoque más común para determinar los parámetros de los modelos de aprendizaje automático es el descenso de gradiente.

Descenso Gradiente Estocástico

En grandes conjuntos de datos, el descenso gradiente se aplica a minilotes de datos de entrada para entrenar desde modelos lineales y árboles potenciados hasta redes neuronales profundas (DNN) y máquinas de vectores de soporte (SVM). Esto se denomina descenso de gradiente estocástico (SGD), y las extensiones de SGD (como Adam y Adagrad) son los optimizadores de facto utilizados en los marcos de aprendizaje automático actuales.

Como el SGD requiere que el entrenamiento tenga lugar de forma iterativa en pequeños lotes del conjunto de datos de entrenamiento, el entrenamiento de un modelo de aprendizaje automático ocurre en un bucle. El SGD encuentra un mínimo, pero no es una solución de forma cerrada, por lo que tenemos que detectar si se ha producido la convergencia del modelo. Por ello, hay que monitorear el error (llamado pérdida) en el conjunto de datos de entrenamiento. Puede producirse un sobreajuste si la complejidad del modelo es mayor de lo que puede permitirse el tamaño y la cobertura del conjunto de datos. Por desgracia, no puedes saber si la complejidad del modelo es demasiado alta para un conjunto de datos concreto hasta que no entrenes realmente el modelo en ese conjunto de datos. Por lo tanto, la evaluación tiene que hacerse dentro del bucle de entrenamiento, y también hay que monitorear las métricas de error en una parte retenida de los datos de entrenamiento, llamada conjunto de datos de validación. Como los conjuntos de datos de entrenamiento y validación se han utilizado en el bucle de entrenamiento, es necesario retener otra división del conjunto de datos de entrenamiento, llamada conjunto de datos de prueba, para informar de las métricas de error reales que cabría esperar en datos nuevos y no vistos. Esta evaluación se realiza al final.

Bucle de entrenamiento Keras

El bucle de entrenamiento típico en Keras tiene este aspecto:

model = keras.Model(...)
model.compile(optimizer=keras.optimizers.Adam(),
              loss=keras.losses.categorical_crossentropy(),
              metrics=['accuracy'])

history = model.fit(x_train, y_train,
                    batch_size=64,
                    epochs=3,
                    validation_data=(x_val, y_val))
results = model.evaluate(x_test, y_test, batch_size=128))
model.save(...)

En este caso, el modelo utiliza el optimizador Adam para llevar a cabo la SGD sobre la entropía cruzada en el conjunto de datos de entrenamiento, e informa de la precisión final obtenida en el conjunto de datos de prueba. El ajuste del modelo se realiza tres veces sobre el conjunto de datos de entrenamiento (cada recorrido sobre el conjunto de datos de entrenamiento se denomina época), y el modelo ve lotes de 64 ejemplos de entrenamiento cada vez. Al final de cada época, se calcula la métrica del error en el conjunto de datos de validación y se añade al historial. Al final del bucle de ajuste, el modelo se evalúa en el conjunto de datos de prueba, se guarda y, potencialmente, se implementa para servir, como se muestra en la Figura 4-1.

A typical training loop consisting of three epochs. Each epoch is processed in chunks of batch_size examples. At the end of the third epoch, the model is evaluated on the testing dataset and saved for potential deployment as a web service.
Figura 4-1. Un bucle de entrenamiento típico que consta de tres épocas. Cada época se procesa en trozos de ejemplos de tamaño_lote. Al final de la tercera época, el modelo se evalúa en el conjunto de datos de prueba y se guarda para su posible implementación como servicio web.

En lugar de utilizar la función preconstruida fit(), también podríamos escribir un bucle de entrenamiento personalizado que itere sobre los lotes explícitamente, pero no necesitaremos hacerlo para ninguno de los patrones de diseño tratados en este capítulo.

Patrones de diseño de formación

Todos los patrones de diseño tratados en este capítulo tienen que ver con modificar de algún modo el típico bucle de entrenamiento. En Sobreajuste útil, renunciamos al uso de un conjunto de datos de validación o prueba porque queremos sobreajustar intencionadamente el conjunto de datos de entrenamiento. En Puntos de comprobación, almacenamos periódicamente el estado completo del modelo, de modo que tengamos acceso a modelos parcialmente entrenados. Cuando utilizamos puntos de control, solemos utilizar también épocas virtuales, en las que decidimos realizar el bucle interno de la función fit(), no sobre el conjunto completo de datos de entrenamiento, sino sobre un número fijo de ejemplos de entrenamiento. En el Aprendizaje por Transferencia, tomamos parte de un modelo previamente entrenado, congelamos los pesos e incorporamos estas capas no entrenables a un nuevo modelo que resuelve el mismo problema, pero en un conjunto de datos más pequeño. En la Estrategia de Distribución, el bucle de entrenamiento se lleva a cabo a escala sobre múltiples trabajadores, a menudo con almacenamiento en caché, aceleración de hardware y paralelización. Por último, en el Ajuste de hiperparámetros, el bucle de entrenamiento se inserta a su vez en un método de optimización para encontrar el conjunto óptimo de hiperparámetros del modelo.

Patrón de diseño 11: Sobreajuste útil

El Sobreajuste Útil es un patrón de diseño en el que renunciamos al uso de mecanismos de generalización porque queremos sobreajustar intencionadamente el conjunto de datos de entrenamiento. En situaciones en las que el sobreajuste puede ser beneficioso, este patrón de diseño recomienda que llevemos a cabo el aprendizaje automático sin regularización, abandono o un conjunto de datos de validación para la detención temprana.

Problema

El objetivo de un modelo de aprendizaje automático es generalizar y hacer predicciones fiables sobre nuevos datos no vistos. Si tu modelo se ajusta en exceso a los datos de entrenamiento (por ejemplo, sigue disminuyendo el error de entrenamiento más allá del punto en el que empieza a aumentar el error de validación), su capacidad de generalización se resiente y también lo hacen tus predicciones futuras. Los libros de texto de introducción al aprendizaje automático aconsejan evitar el sobreajuste mediante técnicas de parada temprana y regularización.

Considera, sin embargo, una situación de simulación del comportamiento de sistemas físicos o dinámicos como los que se encuentran en la ciencia climática, la biología computacional o las finanzas computacionales. En tales sistemas, la dependencia temporal de las observaciones puede describirse mediante una función matemática o un conjunto de ecuaciones diferenciales parciales (EDP). Aunque las ecuaciones que rigen muchos de estos sistemas pueden expresarse formalmente, no tienen una solución de forma cerrada. En su lugar, se han desarrollado métodos numéricos clásicos para aproximar soluciones a estos sistemas. Por desgracia, para muchas aplicaciones del mundo real, estos métodos pueden ser demasiado lentos para ser utilizados en la práctica.

Considera la situación mostrada en la Figura 4-2. Las observaciones recogidas del entorno físico se utilizan como entradas (o condiciones iniciales de partida) para un modelo basado en la física que realiza cálculos numéricos iterativos para calcular el estado preciso del sistema. Supongamos que todas las observaciones tienen un número finito de posibilidades (por ejemplo, la temperatura estará entre 60°C y 80°C en incrementos de 0,01°C). Entonces es posible crear un conjunto de datos de entrenamiento para el sistema de aprendizaje automático que conste del espacio de entrada completo y calcular las etiquetas utilizando el modelo físico.

One situation when it is acceptable to overfit is when the entire domain space of observations can be tabulated and a physical model capable of computing the precise solution is available.
Figura 4-2. Una situación en la que es aceptable sobreajustar es cuando se puede tabular todo el espacio de dominio de las observaciones y se dispone de un modelo físico capaz de calcular la solución precisa.

El modelo ML necesita aprender esta tabla de búsqueda de entradas a salidas, calculada con precisión y sin solapamientos. Dividir un conjunto de datos de este tipo en un conjunto de datos de entrenamiento y un conjunto de datos de evaluación es contraproducente, porque entonces esperaríamos que el modelo aprendiera partes del espacio de entrada que no habrá visto en el conjunto de datos de entrenamiento.

Solución

En este escenario, no hay datos "no vistos" a los que haya que generalizar, ya que se han tabulado todas las entradas posibles. Cuando se construye un modelo de aprendizaje automático para aprender un modelo físico o un sistema dinámico de este tipo, no existe el sobreajuste. El paradigma básico de entrenamiento del aprendizaje automático es ligeramente distinto. En este caso, lo que intentas aprender es un fenómeno físico regido por una EDP subyacente o un sistema de EDP. El aprendizaje automático sólo proporciona un enfoque basado en datos para aproximarse a la solución precisa, y conceptos como sobreajuste deben ser reevaluados.

Por ejemplo, se utiliza un enfoque de trazado de rayos para simular las imágenes de satélite que resultarían de la salida de los modelos numéricos de predicción meteorológica. Esto implica calcular qué cantidad de un rayo solar es absorbida por los hidrometeoros previstos (lluvia, nieve, granizo, bolitas de hielo, etc.) en cada nivel atmosférico. Hay un número finito de posibles tipos de hidrometeoros y un número finito de alturas que predice el modelo numérico. Así que el modelo de trazado de rayos tiene que aplicar ecuaciones ópticas a un conjunto grande pero finito de entradas.

Las ecuaciones de transferencia radiativa rigen el complejo sistema dinámico de cómo se propaga la radiación electromagnética en la atmósfera, y los modelos de transferencia radiativa directa son un medio eficaz de inferir el estado futuro de las imágenes de satélite. Sin embargo, los métodos numéricos clásicos para calcular las soluciones de estas ecuaciones pueden requerir un enorme esfuerzo computacional y son demasiado lentos para utilizarlos en la práctica.

Entra el aprendizaje automático. Es posible utilizar el aprendizaje automático para construir un modelo que se aproxime a las soluciones del modelo de transferencia radiativa directa (ver Figura 4-3). Esta aproximación ML puede acercarse lo suficiente a la solución del modelo que se consiguió originalmente utilizando métodos más clásicos. La ventaja es que la inferencia mediante la aproximación ML aprendida (que sólo necesita calcular una fórmula cerrada) sólo lleva una fracción del tiempo necesario para realizar el trazado de rayos (que requeriría métodos numéricos). Al mismo tiempo, el conjunto de datos de entrenamiento es demasiado grande (varios terabytes) y poco manejable para utilizarlo como tabla de consulta en producción.

Architecture for using a neural network to model the solution of a partial differential equation to solve for I(r,t,n).
Figura 4-3. Arquitectura para utilizar una red neuronal para modelizar la solución de una ecuación diferencial parcial para resolver I(r,t,n).

Hay una diferencia importante entre entrenar un modelo ML para aproximarse a la solución de un sistema dinámico como éste y entrenar un modelo ML para predecir el peso de un bebé basándose en los datos de natalidad recogidos a lo largo de los años. A saber, el sistema dinámico es un conjunto de ecuaciones gobernadas por las leyes de la radiación electromagnética: no hay variables no observadas, ni ruido, ni variabilidad estadística. Para un conjunto dado de entradas, sólo hay una salida calculable con precisión. No hay solapamiento entre los distintos ejemplos del conjunto de datos de entrenamiento. Por esta razón, podemos desechar las preocupaciones sobre la generalización. Queremos que nuestro modelo ML se ajuste a los datos de entrenamiento lo más perfectamente posible, que "sobreajuste".

Esto es contrario al enfoque típico del entrenamiento de un modelo ML, en el que las consideraciones de sesgo, varianza y error de generalización desempeñan un papel importante. El entrenamiento tradicional dice que es posible que un modelo aprenda los datos de entrenamiento "demasiado bien", y que entrenar tu modelo para que la función de pérdida de entrenamiento sea igual a cero es más una señal de alarma que un motivo de celebración. El sobreajuste del conjunto de datos de entrenamiento de este modo hace que el modelo ofrezca predicciones erróneas sobre nuevos puntos de datos no vistos. La diferencia aquí es que sabemos de antemano que no habrá datos no vistos, por lo que el modelo está aproximando una solución a una EDP sobre todo el espectro de entrada. Si tu red neuronal es capaz de aprender un conjunto de parámetros en el que la función de pérdida es cero, entonces ese conjunto de parámetros determina la solución real de la EDP en cuestión.

Por qué funciona

Si se pueden tabular todas las entradas posibles, entonces, como muestra la curva de puntos de la Figura 4-4, un modelo sobreajustado seguirá haciendo las mismas predicciones que el modelo "verdadero" si se entrena para todos los puntos de entrada posibles. Por tanto, el sobreajuste no es preocupante. Tenemos que tener cuidado de que las inferencias se hagan sobre valores redondeados de las entradas, con el redondeo determinado por la resolución con la que se cuadriculó el espacio de entrada.

Overfitting is not a concern if all possible input points are trained for because predictions are the same with both curves.
Figura 4-4. El sobreajuste no es un problema si se entrenan todos los puntos de entrada posibles, porque las predicciones son las mismas con ambas curvas.

¿Es posible encontrar una función modelo que se aproxime arbitrariamente a las etiquetas verdaderas? Una pequeña intuición de por qué esto funciona proviene del Teorema de Aproximación Uniforme del aprendizaje profundo, que, en términos generales, afirma que cualquier función (y sus derivadas) puede aproximarse mediante una red neuronal con al menos una capa oculta y cualquier función de activación "aplastante", como la sigmoidea. Esto significa que no importa qué función nos den, siempre que se comporte relativamente bien, existe una red neuronal con una sola capa oculta que se aproxima a esa función tanto como queramos.1

Los enfoques de aprendizaje profundo para resolver ecuaciones diferenciales o sistemas dinámicos complejos pretenden representar una función definida implícitamente por una ecuación diferencial, o un sistema de ecuaciones, mediante una red neuronal.

El sobreajuste es útil cuando se cumplen las dos condiciones siguientes:

  • No hay ruido, por lo que las etiquetas son exactas en todos los casos.

  • Dispones del conjunto de datos completo (tienes todos los ejemplos que hay). En este caso, el sobreajuste se convierte en interpolar el conjunto de datos.

Contrapartidas y alternativas

Introdujimos que el sobreajuste es útil cuando el conjunto de entradas se puede tabular exhaustivamente y se puede calcular la etiqueta exacta para cada conjunto de entradas. Si se puede tabular todo el espacio de entradas, el sobreajuste no es un problema porque no hay datos no vistos. Sin embargo, el patrón de diseño Sobreajuste útil es útil más allá de este caso de uso limitado. En muchas situaciones del mundo real, aunque haya que relajar una o varias de estas condiciones, el concepto de que el sobreajuste puede ser útil sigue siendo válido.

Interpolación y teoría del caos

El modelo de aprendizaje automático funciona esencialmente como una aproximación a una tabla de consulta de entradas a salidas. Si la tabla de consulta es pequeña, ¡utilízala como tabla de consulta! No hay necesidad de aproximarla mediante un modelo de aprendizaje automático. Una aproximación ML es útil en situaciones en las que la tabla de consulta será demasiado grande para utilizarla eficazmente. Cuando la tabla de consulta es demasiado difícil de manejar, es mejor tratarla como el conjunto de datos de entrenamiento para un modelo de aprendizaje automático que se aproxime a la tabla de consulta.

Ten en cuenta que supusimos que las observaciones tendrían un número finito de posibilidades. Por ejemplo, supusimos que la temperatura se mediría en incrementos de 0,01°C y se situaría entre 60°C y 80°C. Éste será el caso si las observaciones se realizan con instrumentos digitales. Si no es así, el modelo ML es necesario para interpolar entre las entradas de la tabla de consulta.

Los modelos de aprendizaje automático interpolan ponderando los valores no vistos por la distancia de estos valores no vistos a los ejemplos de entrenamiento. Esta interpolación sólo funciona si el sistema subyacente no es caótico. En los sistemas caóticos, aunque el sistema sea determinista, pequeñas diferencias en las condiciones iniciales pueden dar lugar a resultados radicalmente distintos. Sin embargo, en la práctica, cada fenómeno caótico concreto tiene un umbral de resolución específico a partir del cual es posible que los modelos lo pronostiquen en periodos de tiempo cortos. Por tanto, siempre que la tabla de consulta sea lo suficientemente fina y se comprendan los límites de resolubilidad, pueden resultar aproximaciones útiles.

Métodos Monte Carlo

En realidad, tabular todas las entradas posibles podría no ser posible, y podrías adoptar un enfoque Monte Carlo de muestreo del espacio de entrada para crear el conjunto de entradas, especialmente cuando no todas las combinaciones posibles de entradas son físicamente posibles.

En tales casos, el sobreajuste es técnicamente posible (véase la Figura 4-5, donde los círculos sin relleno se aproximan mediante estimaciones erróneas mostradas por círculos cruzados).

If the input space is sampled, not tabulated, then you need to take care to limit model complexity.
Figura 4-5. Si el espacio de entrada está muestreado, no tabulado, debes tener cuidado de limitar la complejidad del modelo.

Sin embargo, incluso aquí, puedes ver que el modelo ML estará interpolando entre respuestas conocidas. El cálculo es siempre determinista, y sólo los puntos de entrada están sujetos a selección aleatoria. Por tanto, estas respuestas conocidas no contienen ruido, y como no hay variables no observadas, los errores en los puntos no muestreados estarán estrictamente limitados por la complejidad del modelo. En este caso, el peligro de sobreajuste procede de la complejidad del modelo y no del ajuste al ruido. El sobreajuste no es tan preocupante cuando el tamaño del conjunto de datos es mayor que el número de parámetros libres. Por lo tanto, utilizar una combinación de modelos de baja complejidad y una regularización suave proporciona una forma práctica de evitar un sobreajuste inaceptable en el caso de la selección Monte Carlo del espacio de entrada.

Discretizaciones basadas en datos

Aunque es posible derivar una solución de forma cerrada para algunas EDP, es más habitual determinar soluciones mediante métodos numéricos. Los métodos numéricos de las EDP son ya un profundo campo de investigación, y hay muchos libros, cursos y revistas dedicados al tema. Un enfoque habitual consiste en utilizar métodos de diferencias finitas, similares a los métodos Runge-Kutta, para resolver ecuaciones diferenciales ordinarias. Esto se suele hacer discretizando el operador diferencial de la EDP y encontrando una solución al problema discreto en una malla espacio-temporal del dominio original. Sin embargo, cuando la dimensión del problema se hace grande, este enfoque basado en la malla falla drásticamente debido a la maldición de la dimensionalidad, porque el espaciado de malla de la rejilla debe ser lo suficientemente pequeño como para capturar el tamaño de característica más pequeño de la solución. Así, para conseguir una resolución 10× mayor de una imagen se necesita 10.000× más potencia de cálculo, porque la malla debe escalarse en cuatro dimensiones teniendo en cuenta el espacio y el tiempo.

Sin embargo, es posible utilizar el aprendizaje automático (en lugar de los métodos de Montecarlo) para seleccionar los puntos de muestreo y crear discretizaciones basadas en datos de las EDP. En el artículo"Learning data-driven discretizations for PDEs", Bar-Sinai et al. demuestran la eficacia de este enfoque. Los autores utilizan una rejilla de puntos fijos de baja resolución para aproximar una solución mediante una interpolación polinómica a trozos utilizando métodos estándar de diferencias finitas, así como una solución obtenida a partir de una red neuronal. La solución obtenida de la red neuronal supera ampliamente a la simulación numérica en la minimización del error absoluto, alcanzando en algunos lugares una mejora de102 órdenes de magnitud. Aunque el aumento de la resolución requiere mucha más potencia de cálculo con los métodos de diferencias finitas, la red neuronal es capaz de mantener un alto rendimiento con un coste adicional sólo marginal. Técnicas como el Método Galerkin Profundo pueden utilizar el aprendizaje profundo para proporcionar una aproximación sin malla de la solución de la EDP dada. De este modo, la solución de la EDP se reduce a un problema de optimización encadenado (ver "Patrón de diseño 8: Cascada ").

Dominios no delimitados

Tanto el método de Monte Carlo como el de discretización basada en datos suponen que el muestreo de todo el espacio de entrada, aunque sea imperfecto, es posible. Por eso el modelo ML se trató como una interpolación entre puntos conocidos.

La generalización y la preocupación por el sobreajuste se vuelven difíciles de ignorar cuando no podemos muestrear puntos en el dominio completo de la función, por ejemplo, para funciones con dominios no limitados o proyecciones a lo largo de un eje temporal hacia el futuro. En estos casos, es importante tener en cuenta el sobreajuste, el infraajuste y el error de generalización. De hecho, se ha demostrado que aunque técnicas como el Método de Galerkin Profundo funcionan bien en regiones que están bien muestreadas, una función que se aprende de este modo no generaliza bien en regiones fuera del dominio que no se muestrearon en la fase de entrenamiento. Esto puede ser problemático a la hora de utilizar el ML para resolver EDP definidas en dominios no limitados, ya que sería imposible capturar una muestra representativa para el entrenamiento.

Destilar el conocimiento de la red neuronal

Otra situación en la que se justifica el sobreajuste es en la destilación, o transferencia de conocimientos, de un gran modelo de aprendizaje automático a otro más pequeño. La destilación de conocimientos es útil cuando no se utiliza plenamente la capacidad de aprendizaje del modelo grande. Si ése es el caso, la complejidad computacional del modelo grande puede no ser necesaria. Sin embargo, también ocurre que entrenar modelos más pequeños es más difícil. Aunque el modelo más pequeño tenga capacidad suficiente para representar el conocimiento, puede que no tenga capacidad suficiente para aprender el conocimiento de forma eficiente.

La solución es entrenar el modelo más pequeño con una gran cantidad de datos generados y etiquetados por el modelo más grande. El modelo más pequeño aprende la salida suave del modelo más grande, en lugar de las etiquetas reales de los datos reales. Se trata de un problema más sencillo que puede aprender el modelo más pequeño. Al igual que con la aproximación de una función numérica mediante un modelo de aprendizaje automático, el objetivo es que el modelo más pequeño represente fielmente las predicciones del modelo de aprendizaje automático más grande. Este segundo paso de entrenamiento puede emplear el Sobreajuste Útil.

Sobreajuste de un lote

En la práctica, entrenar redes neuronales requiere mucha experimentación, y el profesional debe tomar muchas decisiones, desde el tamaño y la arquitectura de la red hasta la elección de la tasa de aprendizaje, las inicializaciones de los pesos u otros hiperparámetros.

El sobreajuste en un lote pequeño es una buena comprobación de cordura tanto para el código del modelo como para la canalización de entrada de datos. Que el modelo se compile y el código se ejecute sin errores no significa que hayas calculado lo que crees o que el objetivo de entrenamiento esté configurado correctamente. Un modelo lo suficientemente complejo debería ser capaz de sobreajustarse con un lote de datos lo suficientemente pequeño, suponiendo que todo esté configurado correctamente. Así que, si no eres capaz de sobreajustar un lote pequeño con ningún modelo, merece la pena que vuelvas a comprobar el código de tu modelo, el canal de entrada y la función de pérdida en busca de errores o simples fallos. El sobreajuste en un lote es una técnica útil para entrenar y solucionar problemas de las redes neuronales.

Consejo

El sobreajuste va más allá de un simple lote. Desde una perspectiva más holística, el sobreajuste sigue el consejo general que se suele dar en relación con el aprendizaje profundo y la regularización. El modelo que mejor se ajusta es un modelo grande que se ha regularizado adecuadamente. En resumen, si tu red neuronal profunda no es capaz de sobreajustar tu conjunto de datos de entrenamiento, deberías utilizar uno más grande. Entonces, una vez que tengas un modelo grande que sobreajuste el conjunto de entrenamiento, puedes aplicar la regularización para mejorar la precisión de la validación, aunque la precisión del entrenamiento pueda disminuir.

Puedes probar el código de tu modelo Keras de esta forma utilizando el tf.data.Dataset que hayas escrito para tu canalización de entrada. Por ejemplo, si tu canal de entrada de datos de entrenamiento se llama trainds, utilizaremos batch() para extraer un único lote de datos. Puedes encontrar el código completo de este ejemplo en el repositorio que acompaña a este libro:

BATCH_SIZE = 256
single_batch = trainds.batch(BATCH_SIZE).take(1)

Luego, al entrenar el modelo, en lugar de llamar al conjunto de datos completo trainds dentro del método fit(), utiliza el lote único que hemos creado:

model.fit(single_batch.repeat(),
          validation_data=evalds,
          )

Ten en cuenta que aplicamos repeat() para que no nos quedemos sin datos al entrenar con ese único lote. Esto garantiza que tomemos el único lote una y otra vez durante el entrenamiento. Todo lo demás (el conjunto de datos de validación, el código del modelo, las características diseñadas, etc.) permanece igual.

Consejo

En lugar de elegir una muestra arbitraria del conjunto de datos de entrenamiento, te recomendamos que sobreajustes en un pequeño conjunto de datos, cada uno de cuyos ejemplos haya sido cuidadosamente verificado para que tenga etiquetas correctas. Diseña la arquitectura de tu red neuronal de modo que sea capaz de aprender este lote de datos con precisión y llegar a cero pérdidas. A continuación, toma la misma red y entrénala en el conjunto de datos de entrenamiento completo.

Patrón de diseño 12: Puntos de control

En los Puntos de Comprobación, almacenamos periódicamente el estado completo del modelo, de forma que dispongamos de modelos parcialmente entrenados. Estos modelos parcialmente entrenados pueden servir como modelo final (en caso de parada anticipada) o como puntos de partida para continuar el entrenamiento (en los casos de fallo de la máquina y ajuste fino).

Problema

Cuanto más complejo sea un modelo (por ejemplo, cuantas más capas y nodos tenga una red neuronal), mayor será el conjunto de datos necesario para entrenarlo eficazmente. Esto se debe a que los modelos más complejos suelen tener más parámetros sintonizables. A medida que aumenta el tamaño de los modelos, también aumenta el tiempo que se tarda en ajustar un lote de ejemplos. A medida que aumenta el tamaño de los datos (y suponiendo que el tamaño de los lotes sea fijo), también aumenta el número de lotes. Por lo tanto, en términos de complejidad computacional, este doble golpe significa que el entrenamiento llevará mucho tiempo.

En el momento de escribir estas líneas, entrenar un modelo de traducción inglés-alemán en una vaina de unidad de procesamiento tensorial (TPU) de última generación con un conjunto de datos relativamente pequeño lleva unas dos horas. En conjuntos de datos reales del tipo utilizado para entrenar dispositivos inteligentes, el entrenamiento puede llevar varios días.

Cuando tenemos un entrenamiento que dura tanto tiempo, las posibilidades de que la máquina falle son incómodamente altas. Si hay un problema, nos gustaría poder reanudar desde un punto intermedio, en lugar de desde el principio.

Solución

Al final de cada época, podemos guardar el estado del modelo. Entonces, si el bucle de entrenamiento se interrumpe por cualquier motivo, podemos volver al estado del modelo guardado y reiniciar. Sin embargo, al hacer esto, tenemos que asegurarnos de guardar el estado intermedio del modelo, no sólo el modelo. ¿Qué significa esto?

Una vez finalizado el entrenamiento, guardamos o exportamos el modelo para poder implementarlo en la inferencia. Un modelo exportado no contiene todo el estado del modelo, sólo la información necesaria para crear la función de predicción. Para un árbol de decisión, por ejemplo, serían las reglas finales de cada nodo intermedio y el valor predicho de cada uno de los nodos hoja. Para un modelo lineal, serían los valores finales de los pesos y los sesgos. Para una red neuronal totalmente conectada, también tendríamos que añadir las funciones de activación y los pesos de las conexiones ocultas.

¿Qué datos sobre el estado del modelo necesitamos al restaurar desde un punto de control que no contenga un modelo exportado? Un modelo exportado no contiene qué época y número de lote está procesando actualmente el modelo, lo que obviamente es importante para reanudar el entrenamiento. Pero hay más información que puede contener un bucle de entrenamiento de un modelo. Para llevar a cabo eficazmente el descenso gradiente, el optimizador puede estar cambiando la tasa de aprendizaje según un programa. Este estado de la tasa de aprendizaje no está presente en un modelo exportado. Además, puede haber un comportamiento estocástico en el modelo, como el abandono. Esto tampoco se recoge en el estado del modelo exportado. Los modelos como las redes neuronales recurrentes incorporan el historial de valores de entrada anteriores. En general, el estado completo del modelo puede tener varias veces el tamaño del modelo exportado.

Guardar el estado completo del modelo para que el entrenamiento del modelo pueda reanudarse a partir de un punto se llama punto de control, y los archivos del modelo guardados se llaman puntos de control. ¿Con qué frecuencia debemos hacer un punto de control? El estado del modelo cambia después de cada tanda debido al descenso de gradiente. Así que, técnicamente, si no queremos perder nada de trabajo, deberíamos hacer un punto de control después de cada lote. Sin embargo, los puntos de control son enormes y esta E/S añadiría una sobrecarga considerable. En su lugar, los marcos de modelos suelen ofrecer la opción de realizar un punto de control al final de cada época. Se trata de un compromiso razonable entre no hacer nunca un punto de control y hacerlo después de cada lote.

Para comprobar un modelo en Keras, proporciona una llamada de retorno al método fit():

checkpoint_path = '{}/checkpoints/taxi'.format(OUTDIR)
cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path, 
                                                 save_weights_only=False,
                                                 verbose=1)
history = model.fit(x_train, y_train,
                    batch_size=64,
                    epochs=3,
                    validation_data=(x_val, y_val), 
                    verbose=2, 
                    callbacks=[cp_callback])

Con el punto de control añadido, el bucle de entrenamiento se convierte en lo que se muestra en la Figura 4-6.

Checkpointing saves the full model state at the end of every epoch.
Figura 4-6. La comprobación guarda el estado completo del modelo al final de cada época.

Por qué funciona

TensorFlow y Keras reanudan automáticamente el entrenamiento desde un punto de control si se encuentran puntos de control en la ruta de salida. Para empezar a entrenar desde cero, por tanto, tienes que empezar desde un nuevo directorio de salida (o eliminar los puntos de control anteriores del directorio de salida). Esto funciona porque los marcos de aprendizaje automático de nivel empresarial respetan la presencia de archivos de puntos de control.

Aunque los puntos de control están diseñados principalmente para apoyar la resiliencia, la disponibilidad de modelos parcialmente entrenados abre una serie de otros casos de uso. Esto se debe a que los modelos parcialmente entrenados suelen ser más generalizables que los modelos creados en iteraciones posteriores. Una buena intuición de por qué ocurre esto puede obtenerse del campo de juego de TensorFlow, como se muestra en la Figura 4-7.

Starting point of the spiral classification problem. You can get to this setup by opening up this link in a web browser.
Figura 4-7. Punto de partida del problema de clasificación en espiral. Puedes llegar a esta configuración abriendo este enlace en un navegador web.

En el campo de juego, estamos intentando construir un clasificador para distinguir entre puntos azules y puntos naranjas (si estás leyendo esto en el libro impreso, por favor, síguelo navegando hasta el enlace en un navegador web). Las dos características de entrada son x1 y x2, que son las coordenadas de los puntos. A partir de estas características, el modelo debe dar la probabilidad de que el punto sea azul. El modelo comienza con pesos aleatorios y el fondo de los puntos muestra la predicción del modelo para cada punto de coordenadas. Como puedes ver, como los pesos son aleatorios, la probabilidad tiende a rondar cerca del valor central para todos los píxeles.

Al iniciar el entrenamiento haciendo clic en la flecha de la parte superior izquierda de la imagen, vemos que el modelo empieza a aprender lentamente con sucesivas épocas, como se muestra en la Figura 4-8.

What the model learns as training progresses. The graphs at the top are the training loss and validation error, while the images show how the model at that stage would predict the color of a point at each coordinate in the grid.
Figura 4-8. Lo que aprende el modelo a medida que avanza el entrenamiento. Los gráficos de la parte superior son la pérdida de entrenamiento y el error de validación, mientras que las imágenes muestran cómo predeciría el modelo en esa fase el color de un punto en cada coordenada de la cuadrícula.

Vemos el primer indicio de aprendizaje en la Figura 4-8(b), y comprobamos que el modelo ha aprendido la visión de alto nivel de los datos en la Figura 4-8(c). A partir de ahí, el modelo va ajustando los límites para meter cada vez más puntos azules en la región central, manteniendo fuera los puntos naranjas. Esto ayuda, pero sólo hasta cierto punto. Para cuando llegamos a la Figura 4-8(e), el ajuste de los pesos está empezando a reflejar perturbaciones aleatorias en los datos de entrenamiento, y éstas son contraproducentes en el conjunto de datos de validación.

Por tanto, podemos dividir el entrenamiento en tres fases. En la primera fase, entre las etapas (a) y (c), el modelo está aprendiendo la organización de alto nivel de los datos. En la segunda fase, entre las etapas (c) y (e), el modelo está aprendiendo los detalles. Al llegar a la tercera fase, la etapa (f), el modelo se está sobreajustando. Un modelo parcialmente entrenado desde el final de la fase 1 o desde la fase 2 tiene algunas ventajas precisamente porque ha aprendido la organización de alto nivel, pero no está atrapado en los detalles.

Contrapartidas y alternativas

Además de proporcionar resiliencia, guardar puntos de control intermedios también nos permite implementar capacidades de parada temprana y ajuste fino.

Parada anticipada

En general, cuanto más tiempo entrenes, menor será la pérdida en el conjunto de datos de entrenamiento. Sin embargo, en algún momento, el error en el conjunto de datos de validación puede dejar de disminuir. Si empiezas a sobreajustar el conjunto de datos de entrenamiento, el error de validación podría incluso empezar a aumentar, como se muestra en la Figura 4-9.

Typically, the training loss continues to drop the longer you train, but once overfitting starts, the validation error on a withheld dataset starts to go up.
Figura 4-9. Normalmente, la pérdida de entrenamiento sigue disminuyendo cuanto más tiempo se entrena, pero una vez que empieza el sobreajuste, el error de validación en un conjunto de datos retenido empieza a subir.

En tales casos, puede ser útil observar el error de validación al final de cada época y detener el proceso de entrenamiento cuando el error de validación sea mayor que el de la época anterior. En la Figura 4-9, esto ocurrirá al final de la cuarta época, mostrada por la línea discontinua gruesa. Esto se denomina parada anticipada.

Consejo

Si hubiéramos realizado el punto de control al final de cada lote, podríamos haber capturado el mínimo real, que podría haber estado un poco antes o después del límite de la época. Consulta la discusión sobre las épocas virtuales en esta sección para conocer una forma más frecuente de realizar el punto de control.

Si hacemos puntos de control con mucha más frecuencia, puede ser útil que la parada anticipada no sea demasiado sensible a pequeñas perturbaciones en el error de validación. En su lugar, podemos aplicar la parada anticipada sólo cuando el error de validación no mejore durante más de N puntos de control.

Selección del punto de control

Aunque se puede realizar una parada temprana deteniendo el entrenamiento en cuanto empiece a aumentar el error de validación, recomendamos entrenar durante más tiempo y elegir la ejecución óptima como paso posterior al procesamiento. La razón por la que sugerimos entrenar hasta bien entrada la fase 3 (véase la sección anterior "Por qué funciona" para una explicación de las tres fases del bucle de entrenamiento) es que no es infrecuente que el error de validación aumente durante un tiempo y luego empiece a disminuir de nuevo. Esto suele deberse a que el entrenamiento se centra inicialmente en los escenarios más comunes (fase 1), y luego empieza a centrarse en las situaciones más raras (fase 2). Como las situaciones raras pueden estar imperfectamente muestreadas entre los conjuntos de datos de entrenamiento y validación, es de esperar que en la fase 2 se produzcan aumentos ocasionales del error de validación durante el entrenamiento. Además, hay situaciones endémicas de los grandes modelos en las que se espera un doble descenso profundo, por lo que es esencial entrenar un poco más por si acaso.

En nuestro ejemplo, en lugar de exportar el modelo al final de la ejecución del entrenamiento, cargaremos el cuarto punto de control y exportaremos nuestro modelo final desde allí. Esto se denomina selección de puntos de control, y en TensorFlow se puede conseguir utilizando BestExporter.

Regularización

En lugar de utilizar la parada anticipada o la selección del punto de control, puede ser útil intentar añadir regularización L2 a tu modelo para que el error de validación no aumente y el modelo nunca llegue a la fase 3. En su lugar, tanto la pérdida de entrenamiento como el error de validación deberían estabilizarse, como se muestra en la Figura 4-10. Denominamos a este bucle de entrenamiento (en el que tanto la métrica de entrenamiento como la de validación alcanzan una meseta) un bucle de entrenamiento con buen comportamiento .

In the ideal situation, validation error does not increase. Instead, both the training loss and validation error plateau.
Figura 4-10. En la situación ideal, el error de validación no aumenta. En cambio, tanto la pérdida de entrenamiento como el error de validación se estabilizan.

Si no se realiza la parada anticipada, y sólo se utiliza la pérdida de entrenamiento para decidir la convergencia, podemos evitar tener que reservar un conjunto de datos de prueba aparte. Aunque no realicemos la parada anticipada, puede ser útil visualizar el progreso del entrenamiento del modelo, sobre todo si el modelo tarda mucho tiempo en entrenarse. Aunque el rendimiento y el progreso del entrenamiento del modelo se suelen monitorizar en el conjunto de datos de validación durante el bucle de entrenamiento, sólo se hace con fines de visualización. Como no tenemos que tomar ninguna medida basándonos en las métricas que se muestran, podemos llevar a cabo la visualización en el conjunto de datos de prueba.

La razón por la que utilizar la regularización puede ser mejor que la parada anticipada es que la regularización te permite utilizar todo el conjunto de datos para cambiar los pesos del modelo, mientras que la parada anticipada te obliga a desperdiciar entre el 10% y el 20% de tu conjunto de datos sólo para decidir cuándo dejar de entrenar. Otros métodos para limitar el sobreajuste (como el abandono y el uso de modelos de menor complejidad) también son buenas alternativas a la parada anticipada. Además, investigaciones recientes indican que el doble descenso se produce en diversos problemas de aprendizaje automático, y por tanto es mejor entrenar durante más tiempo que arriesgarse a obtener una solución subóptima deteniéndose antes de tiempo.

Dos divisiones

¿Los consejos de la sección de regularización no entran en conflicto con los consejos de las secciones anteriores sobre la parada anticipada o la selección de puntos de control? La verdad es que no.

Te recomendamos que dividas tus datos en dos partes: un conjunto de datos de entrenamiento y un conjunto de datos de evaluación. El conjunto de datos de evaluación desempeña el papel del conjunto de datos de prueba durante la experimentación (cuando no hay conjunto de datos de validación) y desempeña el papel del conjunto de datos de validación en la producción (cuando no hay conjunto de datos de prueba).

Cuanto mayor sea tu conjunto de datos de entrenamiento, más complejo será el modelo que puedas utilizar y más preciso será el modelo que puedas obtener. Utilizar la regularización en lugar de la parada temprana o la selección de puntos de control te permite utilizar un conjunto de datos de entrenamiento mayor. En la fase de experimentación (cuando estás explorando diferentes arquitecturas de modelos, técnicas de entrenamiento e hiperparámetros), te recomendamos que desactives la parada anticipada y entrenes con modelos más grandes (consulta también "Patrón de diseño 11: Sobreajuste útil"). Esto se hace para garantizar que el modelo tiene capacidad suficiente para aprender los patrones predictivos. Durante este proceso, monitorea la convergencia del error en la división del entrenamiento. Al final de la experimentación, puedes utilizar el conjunto de datos de evaluación para diagnosticar lo bien que se comporta tu modelo con datos que no ha encontrado durante el entrenamiento.

Cuando entrenes el modelo para implementarlo en producción, tendrás que prepararte para poder hacer una evaluación continua y un reentrenamiento del modelo. Activa la parada anticipada o la selección de puntos de control y monitorea la métrica de error en el conjunto de datos de evaluación. Elige entre la parada anticipada y la selección de puntos de control en función de si necesitas controlar el coste (en cuyo caso, elegirías la parada anticipada) o quieres dar prioridad a la precisión del modelo (en cuyo caso, elegirías la selección de puntos de control).

Ajuste fino

En un bucle de entrenamiento bien gestionado, el descenso gradiente se comporta de forma que llegas rápidamente a la vecindad del error óptimo basándote en la mayoría de tus datos, y luego converges lentamente hacia el error más bajo optimizando en los casos de esquina.

Ahora, imagina que necesitas volver a entrenar periódicamente el modelo con datos frescos. Normalmente querrás hacer hincapié en los datos frescos, no en los casos aislados del mes pasado. A menudo es mejor que reanudes el entrenamiento, no desde el último punto de control, sino desde el punto de control marcado por la línea azul de la Figura 4-11. Esto corresponde al inicio de la fase 2 de nuestra discusión sobre las fases del modelo. Esto corresponde al inicio de la fase 2 en nuestra discusión de las fases del entrenamiento del modelo descritas anteriormente en "Por qué funciona". Así te aseguras de que tienes un método general que puedes afinar durante unas cuantas épocas sólo con los datos nuevos.

Cuando reanudes desde el punto de control marcado por la línea vertical discontinua gruesa, estarás en la cuarta época, por lo que la tasa de aprendizaje será bastante baja. Por tanto, los nuevos datos no cambiarán drásticamente el modelo. Sin embargo, el modelo se comportará de forma óptima (en el contexto del modelo más amplio) en los nuevos datos porque lo habrás afinado en este conjunto de datos más pequeño. Esto se denomina ajuste fino. El ajuste fino también se trata en "Patrón de diseño 13: Aprendizaje por transferencia".

Resume from a checkpoint from before the training loss starts to plateau. Train only on fresh data for subsequent iterations.
Figura 4-11. Reanudar a partir de un punto de control desde antes de que la pérdida de entrenamiento empiece a estabilizarse. Entrena sólo con datos nuevos en las iteraciones siguientes.
Advertencia

El ajuste fino sólo funciona mientras no cambies la arquitectura del modelo.

No es necesario empezar siempre desde un punto de control anterior. En algunos casos, el punto de control final (que se utiliza para servir al modelo) puede utilizarse como inicio en caliente para otra iteración de entrenamiento del modelo. Aun así, empezar desde un punto de control anterior tiende a proporcionar una mejor generalización.

Redefinir una época

Los tutoriales de aprendizaje automático suelen tener código como éste:

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

Este código supone que tienes un conjunto de datos que cabe en la memoria y, en consecuencia, que tu modelo puede iterar a lo largo de 15 épocas sin correr el riesgo de que falle la máquina. Ambas suposiciones son poco razonables: los conjuntos de datos de ML pueden llegar a terabytes, y cuando el entrenamiento puede durar horas, las probabilidades de fallo de la máquina son altas.

Para que el código anterior sea más resistente, proporciona un conjunto de datos TensorFlow (no sólo una matriz NumPy) porque el conjunto de datos TensorFlow es un conjunto de datos fuera de memoria. Proporciona capacidad de iteración y carga perezosa. Ahora el código es el siguiente

cp_callback = tf.keras.callbacks.ModelCheckpoint(...)
history = model.fit(trainds, 
                    validation_data=evalds,
                    epochs=15, 
                    batch_size=128,
                    callbacks=[cp_callback])

Sin embargo, utilizar épocas en grandes conjuntos de datos sigue siendo una mala idea. Puede que las épocas sean fáciles de entender, pero el uso de épocas provoca malos efectos en los modelos de ML del mundo real. Para ver por qué, imagina que tienes un conjunto de datos de entrenamiento con un millón de ejemplos. Puede ser tentador recorrer simplemente este conjunto de datos 15 veces (por ejemplo) estableciendo el número de épocas en 15. Esto plantea varios problemas:

  • El número de épocas es un número entero, pero la diferencia en el tiempo de entrenamiento entre procesar el conjunto de datos 14,3 veces y 15 veces puede ser de horas. Si el modelo ha convergido después de haber visto 14,3 millones de ejemplos, quizá quieras salir y no malgastar los recursos informáticos necesarios para procesar 0,7 millones de ejemplos más.

  • Haces un punto de control una vez por época, y esperar un millón de ejemplos entre puntos de control puede ser demasiado tiempo. Para aumentar la resiliencia, tal vez te convenga comprobar los puntos de control más a menudo.

  • Los conjuntos de datos crecen con el tiempo. Si tienes 100.000 ejemplos más y entrenas el modelo y obtienes un error mayor, ¿se debe a que tienes que hacer una parada temprana, o a que los nuevos datos están corruptos de alguna manera? No puedes saberlo porque el entrenamiento anterior se hizo con 15 millones de ejemplos y el nuevo con 16,5 millones de ejemplos.

  • En el entrenamiento distribuido con servidor de parámetros (ver "Patrón de diseño 14: Estrategia de distribución") con paralelismo de datos y barajado adecuado, el concepto de una época ya no está claro. Debido a los trabajadores potencialmente rezagados, sólo puedes ordenar al sistema que entrene en un número determinado de minilotes.

Pasos por época

En lugar de entrenar durante 15 épocas, podríamos decidir entrenar durante 143.000 pasos, donde batch_size es 100:

NUM_STEPS = 143000
BATCH_SIZE = 100
NUM_CHECKPOINTS = 15
cp_callback = tf.keras.callbacks.ModelCheckpoint(...)
history = model.fit(trainds, 
                    validation_data=evalds,
                    epochs=NUM_CHECKPOINTS,
                    steps_per_epoch=NUM_STEPS // NUM_CHECKPOINTS, 
                    batch_size=BATCH_SIZE,
                    callbacks=[cp_callback])

Cada paso implica actualizaciones de pesos basadas en un único minilote de datos, y esto nos permite detenernos en 14,3 épocas. Esto nos da mucha más granularidad, pero tenemos que definir una "época" como 1/15 del número total de pasos:

steps_per_epoch=NUM_STEPS // NUM_CHECKPOINTS, 

Esto es para que obtengamos el número correcto de puntos de control. Funciona siempre que nos aseguremos de repetir el trainds infinitamente:

trainds = trainds.repeat()

El repeat() es necesario porque ya no establecemos num_epochs, por lo que el número de épocas es por defecto uno. Sin repeat(), el modelo saldrá una vez agotados los patrones de entrenamiento tras leer una vez el conjunto de datos.

Reentrenamiento con más datos

¿Qué pasará cuando tengamos 100.000 ejemplos más? Muy fácil. Los añadimos a nuestro almacén de datos, pero no actualizamos el código. Nuestro código seguirá queriendo procesar 143.000 pasos, y llegará a procesar esa cantidad de datos, con la diferencia de que el 10% de los ejemplos que ve son más nuevos. Si el modelo converge, estupendo. Si no lo hace, sabremos que esos nuevos puntos de datos son el problema, porque no estamos entrenando más tiempo que antes. Al mantener constante el número de pasos, hemos podido separar los efectos de los nuevos datos del entrenamiento con más datos.

Una vez que hayamos entrenado durante 143.000 pasos, reiniciamos el entrenamiento y lo ejecutamos un poco más (digamos, 10.000 pasos), y mientras el modelo siga convergiendo, seguimos entrenándolo más tiempo. Entonces, actualizamos el número 143.000 en el código anterior (en realidad, será un parámetro del código) para reflejar el nuevo número de pasos.

Todo esto funciona bien, hasta que quieras hacer el ajuste de hiperparámetros. Cuando hagas el ajuste de hiperparámetros, querrás cambiar el tamaño del lote. Desgraciadamente, si cambias el tamaño del lote a 50, te encontrarás entrenando la mitad de tiempo, porque estamos entrenando 143.000 pasos, y cada paso sólo dura la mitad que antes. Obviamente, esto no es bueno.

Épocas virtuales

La respuesta es mantener constante el número total de ejemplos de entrenamiento mostrados al modelo (no el número de pasos; ver Figura 4-12):

NUM_TRAINING_EXAMPLES = 1000 * 1000
STOP_POINT = 14.3
TOTAL_TRAINING_EXAMPLES = int(STOP_POINT * NUM_TRAINING_EXAMPLES)
BATCH_SIZE = 100
NUM_CHECKPOINTS = 15
steps_per_epoch = (TOTAL_TRAINING_EXAMPLES // 
                   (BATCH_SIZE*NUM_CHECKPOINTS))
cp_callback = tf.keras.callbacks.ModelCheckpoint(...)
history = model.fit(trainds, 
                    validation_data=evalds,
                    epochs=NUM_CHECKPOINTS,
                    steps_per_epoch=steps_per_epoch, 
                    batch_size=BATCH_SIZE,
                    callbacks=[cp_callback])
Defining a virtual epoch in terms of the desired number of steps between checkpoints.
Figura 4-12. Definir una época virtual en función del número deseado de pasos entre puntos de control.

Cuando tengas más datos, primero entrénalos con los ajustes antiguos, luego aumenta el número de ejemplos para reflejar los nuevos datos y, por último, cambia el STOP_POINT para reflejar el número de veces que tienes que recorrer los datos para alcanzar la convergencia.

Esto es ahora seguro incluso con el ajuste de hiperparámetros (que se trata más adelante en este capítulo) y conserva todas las ventajas de mantener constante el número de pasos.

Patrón de diseño 13: Aprendizaje por transferencia

En el Aprendizaje por Transferencia, tomamos parte de un modelo previamente entrenado, congelamos los pesos e incorporamos estas capas no entrenables a un nuevo modelo que resuelve un problema similar, pero en un conjunto de datos más pequeño.

Problema

Entrenar modelos ML personalizados en datos no estructurados requiere conjuntos de datos extremadamente grandes, que no siempre están fácilmente disponibles. Considera el caso de un modelo que identifique si una radiografía de un brazo contiene un hueso roto. Para lograr una gran precisión, necesitarás cientos de miles de imágenes, si no más. Antes de que tu modelo aprenda qué aspecto tiene un hueso roto, primero tiene que aprender a dar sentido a los píxeles, perímetros y formas que forman parte de las imágenes de tu conjunto de datos. Lo mismo ocurre con los modelos entrenados con datos de texto. Digamos que estamos construyendo un modelo que toma descripciones de síntomas de pacientes y predice las posibles afecciones asociadas a esos síntomas. Además de aprender qué palabras diferencian un resfriado de una neumonía, el modelo también necesita aprender la semántica básica del lenguaje y cómo la secuencia de palabras crea significado. Por ejemplo, el modelo tendría que aprender no sólo a detectar la presencia de la palabra fiebre, sino que la secuencia no fiebre tiene un significado muy distinto de fiebre alta.

Para ver cuántos datos se necesitan para entrenar modelos de alta precisión, podemos fijarnos en ImageNet, una base de datos de más de 14 millones de imágenes etiquetadas. ImageNet se utiliza con frecuencia como punto de referencia para evaluar marcos de aprendizaje automático en distintos equipos. Por ejemplo, el paquete de pruebas MLPerf utiliza ImageNet para comparar el tiempo que tardan varios marcos de ML que se ejecutan en distintos equipos en alcanzar una precisión de clasificación del 75,9%. En los resultados del Entrenamiento MLPerf v0.7, un modelo TensorFlow ejecutado en una TPU v3 de Google tardó unos 30 segundos en alcanzar esta precisión objetivo.2 Con más tiempo de entrenamiento, los modelos pueden alcanzar una precisión aún mayor en ImageNet. Sin embargo, esto se debe en gran medida al tamaño de ImageNet. La mayoría de las organizaciones con problemas de predicción especializados no disponen de tantos datos.

Dado que los casos de uso como los ejemplos de imagen y texto descritos anteriormente implican dominios de datos especialmente especializados, tampoco es posible utilizar un modelo de uso general para identificar con éxito fracturas óseas o diagnosticar enfermedades. Un modelo entrenado en ImageNet podría ser capaz de etiquetar una imagen de rayos X como radiografía o imagen médica, pero es poco probable que pueda etiquetarla como fractura de fémur. Como estos modelos suelen entrenarse en una amplia variedad de categorías de etiquetado de alto nivel, no esperaríamos que comprendieran las condiciones presentes en las imágenes que son específicas de nuestro conjunto de datos. Para hacer frente a esto, necesitamos una solución que nos permita construir un modelo personalizado utilizando sólo los datos de que disponemos y con las etiquetas que nos interesan.

Solución

Con el patrón de diseño de Aprendizaje por Transferencia, podemos tomar un modelo que se ha entrenado con el mismo tipo de datos para una tarea similar y aplicarlo a una tarea especializada utilizando nuestros propios datos personalizados. Por "mismo tipo de datos" nos referimos a la misma modalidad de datos: imágenes, texto, etc. Más allá de una categoría amplia como las imágenes, también es ideal utilizar un modelo que haya sido preentrenado en los mismos tipos de imágenes. Por ejemplo, utiliza un modelo que haya sido preentrenado en fotografías si vas a utilizarlo para la clasificación de fotografías y un modelo que haya sido preentrenado en imágenes de teledetección si vas a utilizarlo para clasificar imágenes de satélite. Por tarea similar, nos referimos al problema que se está resolviendo. Para hacer aprendizaje de transferencia para la clasificación de imágenes, por ejemplo, es mejor empezar con un modelo que haya sido entrenado para la clasificación de imágenes, en lugar de para la detección de objetos.

Siguiendo con el ejemplo, supongamos que estamos construyendo un clasificador binario para determinar si la imagen de una radiografía contiene un hueso roto. Sólo tenemos 200 imágenes de cada clase: roto y no roto. Esto no es suficiente para entrenar un modelo de alta calidad desde cero, pero sí para el aprendizaje por transferencia. Para resolver esto con el aprendizaje por transferencia, tendremos que encontrar un modelo que ya haya sido entrenado en un gran conjunto de datos para realizar la clasificación de imágenes. A continuación, eliminaremos la última capa de ese modelo, congelaremos los pesos del mismo y seguiremos entrenando con nuestras 400 imágenes de rayos X. Lo ideal sería encontrar un modelo entrenado en un conjunto de datos con imágenes similares a nuestras radiografías, como imágenes tomadas en un laboratorio u otra condición controlada. Sin embargo, podemos utilizar el aprendizaje por transferencia si los conjuntos de datos son diferentes, siempre que la tarea de predicción sea la misma. En este caso estamos haciendo clasificación de imágenes.

Puedes utilizar el aprendizaje por transferencia para muchas tareas de predicción además de la clasificación de imágenes, siempre que exista un modelo preentrenado que se ajuste a la tarea que te gustaría realizar en tu conjunto de datos. Por ejemplo, el aprendizaje por transferencia también se aplica con frecuencia en la detección de objetos de imágenes, la transferencia de estilos de imágenes, la generación de imágenes, la clasificación de textos, la traducción automática, etc.

Nota

El aprendizaje por transferencia funciona porque nos permite subirnos a hombros de gigantes, utilizando modelos que ya han sido entrenados en conjuntos de datos extremadamente grandes y etiquetados. Podemos utilizar el aprendizaje por transferencia gracias a los años de investigación y al trabajo que otros han dedicado a crear estos conjuntos de datos para nosotros, lo que ha hecho avanzar el estado de la técnica en el aprendizaje por transferencia. Un ejemplo de estos conjuntos de datos es el proyecto ImageNet, iniciado en 2006 por Fei-Fei Li y publicado en 2009. ImageNet3 ha sido esencial para el desarrollo del aprendizaje por transferencia y ha allanado el camino para otros grandes conjuntos de datos como COCO y Open Images.

La idea que subyace al aprendizaje por transferencia es que puedes utilizar los pesos y las capas de un modelo entrenado en el mismo dominio que tu tarea de predicción. En la mayoría de los modelos de aprendizaje profundo, la capa final contiene la etiqueta de clasificación o salida específica de tu tarea de predicción. Con el aprendizaje por transferencia, eliminamos esta capa, congelamos los pesos entrenados del modelo y sustituimos la capa final por la salida de nuestra tarea de predicción especializada antes de seguir entrenando. Podemos ver cómo funciona esto en la Figura 4-13.

Normalmente, la penúltima capa del modelo (la capa anterior a la capa de salida del modelo) se elige como capa cuello de botella. A continuación, explicaremos la capa cuello de botella, junto con diferentes formas de implementar el aprendizaje por transferencia en TensorFlow.

Transfer learning involves training a model on a large dataset. The “top” of the model (typically, just the output layer) is removed and the remaining layers have their weights frozen. The last layer of the remaining model is called the bottleneck layer.
Figura 4-13. El aprendizaje por transferencia consiste en entrenar un modelo con un gran conjunto de datos. Se elimina la "parte superior" del modelo (normalmente, sólo la capa de salida) y se congelan los pesos de las capas restantes. La última capa del modelo restante se denomina capa cuello de botella.

Capa cuello de botella

En relación con un modelo completo, la capa cuello de botella representa la entrada (normalmente una imagen o un documento de texto) en el espacio de menor dimensionalidad. Más concretamente, cuando introducimos datos en nuestro modelo, las primeras capas ven estos datos casi en su forma original. Para ver cómo funciona esto, sigamos con un ejemplo de imágenes médicas, pero esta vez construiremos un modelo con un conjunto de datos de histología colorrectal para clasificar las imágenes de histología en una de ocho categorías.

Para explorar el modelo que vamos a utilizar para el aprendizaje por transferencia, vamos a cargar la arquitectura del modelo VGG preentrenada en el conjunto de datos ImageNet:

vgg_model_withtop = tf.keras.applications.VGG19(
    include_top=True, 
    weights='imagenet', 
)

Observa que hemos puesto include_top=True, lo que significa que estamos cargando el modelo VGG completo, incluida la capa de salida. Para ImageNet, el modelo clasifica las imágenes en 1.000 clases diferentes, por lo que la capa de salida es una matriz de 1.000 elementos. Veamos la salida de model.summary() para comprender qué capa se utilizará como cuello de botella. Por brevedad, hemos omitido aquí algunas de las capas intermedias:

Model: "vgg19"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_3 (InputLayer)         [(None, 224, 224, 3)]     0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792     
...more layers here...
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv4 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 25088)             0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
Total params: 143,667,240
Trainable params: 143,667,240
Non-trainable params: 0
_________________________________________________________________

Como puedes ver, el modelo VGG acepta imágenes como una matriz de 224×224×3 píxeles. A continuación, esta matriz de 128 elementos pasa por capas sucesivas (cada una de las cuales puede cambiar la dimensionalidad de la matriz) hasta que se aplana en una matriz de 25.088×1 dimensiones en la capa llamada flatten. Por último, se introduce en la capa de salida, que devuelve una matriz de 1.000 elementos (para cada clase de ImageNet). En este ejemplo, elegiremos la capa block5_pool como capa cuello de botella cuando adaptemos este modelo para entrenarlo con nuestras imágenes de histología médica. La capa cuello de botella produce una matriz de 7×7×512 dimensiones, que es una representación de baja dimensión de la imagen de entrada. Ha retenido suficiente información de la imagen de entrada para poder clasificarla. Cuando apliquemos este modelo a nuestra tarea de clasificación de imágenes médicas, esperamos que la destilación de información sea suficiente para llevar a cabo con éxito la clasificación en nuestro conjunto de datos.

El conjunto de datos de histología viene con imágenes como matrices dimensionales (150,150,3). Esta representación de 150×150×3 es la de mayor dimensionalidad. Para utilizar el modelo VGG con nuestros datos de imágenes, podemos cargarlo con lo siguiente:

vgg_model = tf.keras.applications.VGG19(
    include_top=False, 
    weights='imagenet', 
    input_shape=((150,150,3))
)

vgg_model.trainable = False

Al establecer include_top=False, estamos especificando que la última capa de VGG que queremos cargar es la capa cuello de botella. El input_shape que hemos pasado coincide con la forma de entrada de nuestras imágenes histológicas. Un resumen de las últimas capas de este modelo VGG actualizado tiene el siguiente aspecto:

block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv4 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 20,024,384
Trainable params: 0
Non-trainable params: 20,024,384
_________________________________________________________________

La última capa es ahora nuestra capa cuello de botella. Puedes observar que el tamaño de block5_pool es (4,4,512), mientras que antes era (7,7,512). Esto se debe a que hemos instanciado VGG con un parámetro input_shape para tener en cuenta el tamaño de las imágenes de nuestro conjunto de datos. También hay que tener en cuenta que el ajuste include_top=False está codificado para utilizar block5_pool como capa de cuello de botella, pero si quieres personalizarlo, puedes cargar el modelo completo y eliminar las capas adicionales que no quieras utilizar.

Antes de que este modelo esté listo para ser entrenado, tendremos que añadir unas cuantas capas encima, específicas para nuestros datos y tarea de clasificación. También es importante tener en cuenta que, como hemos establecido trainable=False, hay 0 parámetros entrenables en el modelo actual.

Consejo

Como regla general, la capa cuello de botella suele ser la última capa aplanada de menor dimensionalidad antes de una operación de aplanamiento.

Como ambas representan características en dimensionalidad reducida, las capas cuello de botella son conceptualmente similares a las incrustaciones. Por ejemplo, en un modelo autocodificador con una arquitectura codificador-decodificador, la capa cuello de botella es una incrustación. En este caso, el cuello de botella actúa como capa intermedia del modelo, mapeando los datos de entrada originales a una representación de menor dimensionalidad, que el decodificador (la segunda mitad de la red) utiliza para mapear la entrada de nuevo a su representación original de mayor dimensionalidad. Para ver un diagrama de la capa cuello de botella en un autocodificador, consulta la Figura 2-13 del Capítulo 2.

Una capa de incrustación es esencialmente una tabla de búsqueda de pesos, que asigna una característica concreta a alguna dimensión del espacio vectorial. La principal diferencia es que los pesos de una capa de incrustación pueden entrenarse, mientras que todas las capas que van hasta la capa cuello de botella, incluida, tienen sus pesos congelados. En otras palabras, toda la red hasta la capa cuello de botella inclusive no se puede entrenar, y los pesos de las capas posteriores al cuello de botella son las únicas capas entrenables del modelo.

Nota

También vale la pena señalar que las incrustaciones preentrenadas pueden utilizarse en el patrón de diseño del Aprendizaje por Transferencia. Cuando construyas un modelo que incluya una capa de incrustación, puedes utilizar una búsqueda de incrustación existente (preentrenada) o entrenar tu propia capa de incrustación desde cero.

En resumen, el aprendizaje por transferencia es una solución que puedes emplear para resolver un problema similar en un conjunto de datos más pequeño. El aprendizaje por transferencia siempre utiliza una capa cuello de botella con pesos congelados no entrenables. Las incrustaciones son un tipo de representación de datos. En última instancia, todo se reduce a la finalidad. Si el propósito es entrenar un modelo similar, utilizarías el aprendizaje por transferencia. Por consiguiente, si el propósito es representar una imagen de entrada de forma más concisa, utilizarías una incrustación. El código puede ser exactamente el mismo.

Aplicar el aprendizaje por transferencia

Puedes implementar el aprendizaje por transferencia en Keras utilizando uno de estos dos métodos:

  • Cargando un modelo preentrenado por tu cuenta, eliminando las capas posteriores al cuello de botella y añadiendo una nueva capa final con tus propios datos y etiquetas

  • Utilizar un módulo TensorFlow Hub preentrenado como base para tu tarea de aprendizaje de transferencia

Empecemos por ver cómo cargar y utilizar un modelo preentrenado por tu cuenta. Para ello, nos basaremos en el ejemplo del modelo VGG que hemos presentado antes. Ten en cuenta que VGG es una arquitectura de modelo, mientras que ImageNet son los datos con los que se entrenó. Juntos forman el modelo preentrenado que utilizaremos para el aprendizaje por transferencia. Aquí estamos utilizando el aprendizaje por transferencia para clasificar imágenes histológicas colorrectales. Mientras que el conjunto de datos ImageNet original contiene 1.000 etiquetas, nuestro modelo resultante sólo devolverá 8 clases posibles que especificaremos, frente a las miles de etiquetas presentes en ImageNet.

Nota

Cargar un modelo preentrenado y utilizarlo para obtener clasificaciones sobre las etiquetas originales con las que se entrenó ese modelo no es aprendizaje por transferencia. El aprendizaje por transferencia es ir un paso más allá, sustituyendo las capas finales del modelo por tu propia tarea de predicción.

El modelo VGG que hemos cargado será nuestro modelo base. Tendremos que añadir unas cuantas capas para aplanar la salida de nuestra capa cuello de botella e introducir esta salida aplanada en una matriz softmax de 8 elementos:

global_avg_layer = tf.keras.layers.GlobalAveragePooling2D()
feature_batch_avg = global_avg_layer(feature_batch)

prediction_layer = tf.keras.layers.Dense(8, activation='softmax')
prediction_batch = prediction_layer(feature_batch_avg)

Por último, podemos utilizar la API Sequential, para crear nuestro nuevo modelo de aprendizaje por transferencia como una pila de capas:

histology_model = keras.Sequential([
  vgg_model,
  global_avg_layer,
  prediction_layer
])

Tomemos nota del resultado de model.summary() en nuestro modelo de aprendizaje por transferencia:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
vgg19 (Model)                (None, 4, 4, 512)         20024384  
_________________________________________________________________
global_average_pooling2d (Gl (None, 512)               0         
_________________________________________________________________
dense (Dense)                (None, 8)                 4104      
=================================================================
Total params: 20,028,488
Trainable params: 4,104
Non-trainable params: 20,024,384
_________________________________________________________________

Lo importante aquí es que los únicos parámetros entrenables son los que están después de nuestra capa cuello de botella. En este ejemplo, la capa cuello de botella son los vectores de características del modelo VGG. Después de compilar este modelo, podemos entrenarlo utilizando nuestro conjunto de datos de imágenes histológicas.

Incrustaciones preentrenadas

Aunque podemos cargar un modelo preentrenado por nuestra cuenta, también podemos aplicar el aprendizaje por transferencia haciendo uso de los muchos modelos preentrenados disponibles en TF Hub, una biblioteca de modelos preentrenados (llamados módulos). Estos módulos abarcan una gran variedad de dominios de datos y casos de uso, como la clasificación, la detección de objetos, la traducción automática, etc. En TensorFlow, puedes cargar estos módulos como una capa, y luego añadir tu propia capa de clasificación encima.

Para ver cómo funciona TF Hub, vamos a construir un modelo que clasifique las críticas de películas como positivas o negativas. En primer lugar, cargaremos un modelo de incrustación previamente entrenado con un gran corpus de artículos de noticias. Podemos instanciar este modelo como hub.KerasLayer:

hub_layer = hub.KerasLayer(
    "https://tfhub.dev/google/tf2-preview/gnews-swivel-20dim/1",
    input_shape=[], dtype=tf.string, trainable=True)

Podemos apilar capas adicionales sobre ésta para construir nuestro clasificador:

model = keras.Sequential([
  hub_layer,
  keras.layers.Dense(32, activation='relu'),
  keras.layers.Dense(1, activation='sigmoid')                          
])

Ahora podemos entrenar este modelo, pasándole como entrada nuestro propio conjunto de datos de texto. La predicción resultante será una matriz de 1 elemento que indicará si nuestro modelo cree que el texto es positivo o negativo.

Por qué funciona

Para entender por qué funciona el aprendizaje por transferencia, veamos primero una analogía. Cuando los niños están aprendiendo su primera lengua, se les expone a muchos ejemplos y se les corrige si identifican algo erróneamente. Por ejemplo, la primera vez que aprenden a identificar un gato, verán a sus padres señalar al gato y decir la palabra gato, y esta repetición refuerza las vías en su cerebro. Del mismo modo, se les corrige cuando dicen gato refiriéndose a un animal que no es un gato. Cuando el niño aprenda a identificar a un perro, no tendrá que empezar de cero. Puede utilizar un proceso de reconocimiento similar al que utilizó para el gato, pero aplicándolo a una tarea ligeramente distinta. De este modo, el niño ha construido una base para el aprendizaje. Además de aprender cosas nuevas, también ha aprendido a aprender cosas nuevas. Aplicar estos métodos de aprendizaje a dominios diferentes es, a grandes rasgos, cómo funciona también el aprendizaje por transferencia.

¿Cómo funciona esto en las redes neuronales? En una red neuronal convolucional (CNN) típica, el aprendizaje es jerárquico. Las primeras capas aprenden a reconocer los perímetros y las formas presentes en una imagen. En el ejemplo del gato, esto podría significar que el modelo puede identificar zonas en una imagen donde el perímetro del cuerpo del gato se encuentra con el fondo. Las siguientes capas del modelo empiezan a comprender grupos de perímetros, tal vez que hay dos perímetros que se encuentran en la esquina superior izquierda de la imagen. Las capas finales de una CNN pueden entonces unir estos grupos de perímetros, desarrollando una comprensión de las diferentes características de la imagen. En el ejemplo del gato, el modelo podría identificar dos formas triangulares hacia la parte superior de la imagen y dos formas ovaladas debajo de ellas. Como humanos, sabemos que esas formas triangulares son orejas y las ovaladas, ojos.

Podemos visualizar este proceso en la Figura 4-14, de la investigación de Zeiler y Fergus sobre la deconstrucción de las CNN para comprender las diferentes características que se activaron a lo largo de cada capa del modelo. Para cada capa de una CNN de cinco capas, se muestra el mapa de características de una capa determinada junto a la imagen real. Esto nos permite ver cómo progresa la percepción de una imagen por parte del modelo a medida que se desplaza por la red. Las capas 1 y 2 sólo reconocen perímetros, la capa 3 empieza a reconocer objetos, y las capas 4 y 5 pueden comprender puntos focales dentro de toda la imagen.

Pero recuerda que, para nuestro modelo, se trata simplemente de agrupaciones de valores de píxeles. No sabe que las formas triangulares y ovaladas son orejas y ojos, sólo sabe asociar agrupaciones específicas de rasgos con las etiquetas con las que ha sido entrenado. De este modo, el proceso de aprendizaje de las agrupaciones de rasgos que componen un gato no difiere mucho del aprendizaje de los grupos de rasgos que forman parte de otros objetos, como una mesa, una montaña o incluso un famoso. Para un modelo, todo esto no son más que combinaciones diferentes de valores de píxeles, perímetros y formas.

Research from Zeiler and Fergus (2013) in deconstructing CNNs helps us visualize how a CNN sees images at each layer of the network.
Figura 4-14. La investigación de Zeiler y Fergus (2013) sobre la deconstrucción de las CNN nos ayuda a visualizar cómo una CNN ve las imágenes en cada capa de la red.

Contrapartidas y alternativas

Hasta ahora, no hemos hablado de métodos para modificar los pesos de nuestro modelo original al aplicar el aprendizaje por transferencia. Aquí examinaremos dos enfoques para ello: la extracción de características y el ajuste fino. También discutiremos por qué el aprendizaje por transferencia se centra principalmente en modelos de imagen y texto, y estudiaremos la relación entre la incrustación de frases de texto y el aprendizaje por transferencia.

Ajuste fino frente a extracción de rasgos

La extracción de características describe un enfoque del aprendizaje por transferencia en el que congelas los pesos de todas las capas anteriores a la capa cuello de botella y entrenas las capas siguientes con tus propios datos y etiquetas. Otra opción es ajustar los pesos de las capas del modelo preentrenado. Con el ajuste fino, puedes actualizar los pesos de cada capa del modelo preentrenado, o sólo de unas pocas capas justo antes del cuello de botella. Entrenar un modelo de aprendizaje por transferencia utilizando el ajuste fino suele llevar más tiempo que la extracción de características. Verás que en nuestro ejemplo anterior de clasificación de texto, establecimos trainable=True al inicializar nuestra capa TF Hub. Este es un ejemplo de ajuste fino.

Al realizar el ajuste fino, es habitual dejar congelados los pesos de las capas iniciales del modelo, ya que estas capas se han entrenado para reconocer características básicas que suelen ser comunes a muchos tipos de imágenes. Para afinar un modelo MobileNet, por ejemplo, estableceríamos trainable=False sólo para un subconjunto de capas del modelo, en lugar de hacer que todas las capas no sean entrenables. Por ejemplo, para afinar después de la capa 100, podríamos ejecutar:

base_model = tf.keras.applications.MobileNetV2(input_shape=(160,160,3),
                                               include_top=False,
                                               weights='imagenet')

for layer in base_model.layers[:100]:
  layer.trainable =  False

Un enfoque recomendado para determinar cuántas capas congelar se conoce como ajuste fino progresivo, y consiste en descongelar capas iterativamente después de cada ejecución de entrenamiento para encontrar el número ideal de capas que ajustar. Esto funciona mejor y es más eficaz si mantienes tu tasa de aprendizaje baja (0,001 es lo habitual) y el número de iteraciones de entrenamiento relativamente pequeño. Para aplicar el ajuste fino progresivo, empieza descongelando sólo la última capa de tu modelo transferido (la capa más cercana a la salida) y calcula la pérdida de tu modelo tras el entrenamiento. Luego, una a una, descongela más capas hasta que llegues a la capa de entrada o hasta que la pérdida empiece a estabilizarse. Utiliza esto para informarte del número de capas que debes afinar.

¿Cómo debes determinar si afinar o congelar todas las capas de tu modelo preentrenado? Normalmente, cuando tienes un conjunto de datos pequeño, es mejor utilizar el modelo preentrenado como extractor de características en lugar de afinarlo. Si estás reentrenando los pesos de un modelo que probablemente se entrenó con miles o millones de ejemplos, el ajuste fino puede hacer que el modelo actualizado se ajuste en exceso a tu pequeño conjunto de datos y pierda la información más general aprendida de esos millones de ejemplos. Aunque depende de tus datos y tarea de predicción, cuando aquí decimos "conjunto de datos pequeño", nos referimos a conjuntos de datos con cientos o unos pocos miles de ejemplos de entrenamiento.

Otro factor a tener en cuenta a la hora de decidir si afinar o no es la similitud de tu tarea de predicción con la del modelo original preentrenado que estás utilizando. Cuando la tarea de predicción es similar o una continuación del entrenamiento previo, como ocurría en nuestro modelo de análisis de sentimientos de críticas de películas, el ajuste fino puede producir resultados de mayor precisión. Cuando la tarea es distinta o los conjuntos de datos son significativamente diferentes, lo mejor es congelar todas las capas del modelo preentrenado en lugar de afinarlas. La Tabla 4-1 resume los puntos clave.4

Tabla 4-1. Criterios para ayudar a elegir entre la extracción de rasgos y el ajuste fino
Criterio Extracción de rasgos Ajuste fino
¿Qué tamaño tiene el conjunto de datos? Pequeño Grande
¿Es tu tarea de predicción la misma que la del modelo preentrenado? Diferentes tareas Misma tarea, o tarea similar con la misma distribución de clases de etiquetas
Presupuesto para tiempo de formación y coste computacional Baja Alta

En nuestro ejemplo del texto, el modelo preentrenado se entrenó en un corpus de texto de noticias, pero nuestro caso de uso era el análisis de sentimientos. Como estas tareas son diferentes, deberíamos utilizar el modelo original como extractor de características en lugar de ajustarlo. Un ejemplo de tareas de predicción diferentes en un dominio de imagen podría ser utilizar nuestro modelo MobileNet entrenado en ImageNet como base para hacer aprendizaje por transferencia en un conjunto de datos de imágenes médicas. Aunque ambas tareas implican la clasificación de imágenes, la naturaleza de las imágenes de cada conjunto de datos es muy diferente.

Centrarse en los modelos de imagen y texto

Habrás observado que todos los ejemplos de esta sección se han centrado en datos de imágenes y texto. Esto se debe a que el aprendizaje por transferencia es principalmente para casos en los que puedes aplicar una tarea similar al mismo dominio de datos. Los modelos entrenados con datos tabulares, sin embargo, abarcan un número potencialmente infinito de posibles tareas de predicción y tipos de datos. Podrías entrenar un modelo con datos tabulares para predecir cómo deberías poner precio a las entradas de tu evento, si es probable o no que alguien deje de pagar un préstamo, los ingresos de tu empresa el próximo trimestre, la duración de un viaje en taxi, etc. Los datos específicos para estas tareas también son increíblemente variados, ya que el problema de las entradas depende de la información sobre artistas y locales, el problema de los préstamos de los ingresos personales y la duración del taxi de los patrones de tráfico urbano. Por estas razones, existen retos inherentes a la transferencia de los aprendizajes de un modelo tabular a otro.

Aunque el aprendizaje por transferencia todavía no es tan común en los datos tabulares como en los dominios de imagen y texto, una nueva arquitectura de modelos llamada TabNet presenta una investigación novedosa en este campo. La mayoría de los modelos tabulares requieren una ingeniería de características significativa en comparación con los modelos de imagen y texto. TabNet emplea una técnica que, en primer lugar, utiliza el aprendizaje no supervisado para aprender representaciones de las características tabulares y, a continuación, ajusta con precisión estas representaciones aprendidas para producir predicciones. De este modo, TabNet automatiza la ingeniería de características para los modelos tabulares.

Incrustaciones de palabras frente a frases

Hasta ahora, en nuestro debate sobre la incrustación de texto, nos hemos referido sobre todo a la incrustación de palabras . Otro tipo de incrustación de texto es la incrustación de frases. Mientras que las incrustaciones de palabras representan palabras individuales en un espacio vectorial, las incrustaciones de frases representan frases enteras. Por tanto, las incrustaciones de palabras son independientes del contexto. Veamos cómo funciona esto con la siguiente frase:

"Te he dejado galletas recién horneadas a la izquierda de la encimera de la cocina".

Observa que la palabra izquierda aparece dos veces en esa frase, primero como verbo y luego como adjetivo. Si generáramos incrustaciones de palabras para esta frase, obtendríamos una matriz distinta para cada palabra. Con las incrustaciones de palabras, la matriz para ambas instancias de la palabra izquierda sería la misma. Sin embargo, con las incrustaciones a nivel de frase, obtendríamos un único vector que representaría toda la frase. Hay varios métodos para generar incrustaciones de frases: desde promediar las incrustaciones de palabras de una frase hasta entrenar un modelo de aprendizaje supervisado en un gran corpus de texto para generar las incrustaciones.

¿Qué relación tiene esto con el aprendizaje por transferencia? Este último método -entrenar un modelo de aprendizaje supervisado para generar incrustaciones a nivel de frase- es en realidad una forma de aprendizaje por transferencia. Es el método utilizado por el Codificador Universal de Frases de Google (disponible en TF Hub) y BERT. Estos métodos difieren de las incrustaciones de palabras en que van más allá de proporcionar simplemente una búsqueda de pesos para palabras individuales. En su lugar, se han construido entrenando un modelo en un gran conjunto de datos de texto variado para comprender el significado que transmiten las secuencias de palabras. De este modo, están diseñados para ser transferidos a diferentes tareas del lenguaje natural y, por tanto, pueden utilizarse para construir modelos que apliquen el aprendizaje por transferencia.

Patrón de diseño 14: Estrategia de distribución

En la Estrategia de Distribución, el bucle de entrenamiento se realiza a escala sobre múltiples trabajadores, a menudo con almacenamiento en caché, aceleración de hardware y paralelización.

Problema

Hoy en día, es habitual que las grandes redes neuronales tengan millones de parámetros y se entrenen con cantidades ingentes de datos. De hecho, se ha demostrado que aumentar la escala del aprendizaje profundo, con respecto al número de ejemplos de entrenamiento, al número de parámetros del modelo, o a ambos, mejora drásticamente el rendimiento del modelo. Sin embargo, a medida que aumenta el tamaño de los modelos y los datos, aumentan proporcionalmente las demandas de computación y memoria, por lo que el tiempo que se tarda en entrenar estos modelos es uno de los mayores problemas del aprendizaje profundo.

Las GPU proporcionan un impulso computacional sustancial y ponen al alcance el tiempo de entrenamiento de redes neuronales profundas de tamaño modesto. Sin embargo, para modelos muy grandes entrenados con cantidades masivas de datos, las GPU individuales no bastan para hacer que el tiempo de entrenamiento sea manejable. Por ejemplo, en el momento de escribir esto, entrenar ResNet-50 en el conjunto de datos de referencia ImageNet durante 90 épocas en una sola GPU NVIDIA M40 requiere1018 operaciones de precisión única y tarda 14 días. Como la IA se utiliza cada vez más para resolver problemas en dominios complejos, y las bibliotecas de código abierto como Tensorflow y PyTorch hacen más accesible la construcción de modelos de aprendizaje profundo, las grandes redes neuronales comparables a ResNet-50 se han convertido en la norma.

Esto es un problema. Si tardas dos semanas en entrenar tu red neuronal, tendrás que esperar dos semanas antes de poder iterar sobre nuevas ideas o experimentar con el ajuste de la configuración. Además, para algunos problemas complejos como las imágenes médicas, la conducción autónoma o la traducción de idiomas, no siempre es factible dividir el problema en componentes más pequeños o trabajar sólo con un subconjunto de los datos. Sólo con la escala completa de los datos puedes evaluar si las cosas funcionan o no.

El tiempo de entrenamiento se traduce literalmente en dinero. En el mundo del aprendizaje automático sin servidor, en lugar de comprar tu propia y costosa GPU, es posible enviar trabajos de entrenamiento a través de un servicio en la nube en el que se te cobra por el tiempo de entrenamiento. El coste de entrenar un modelo, ya sea para pagar una GPU o para pagar un servicio de entrenamiento sin servidor, se acumula rápidamente.

¿Hay alguna forma de acelerar el entrenamiento de estas grandes redes neuronales?

Solución

Una forma de acelerar el entrenamiento es mediante estrategias de distribución en el bucle de entrenamiento. Existen diferentes técnicas de distribución, pero la idea común es dividir el esfuerzo de entrenamiento del modelo entre varias máquinas. Hay dos formas de hacerlo: paralelismo de datos y paralelismo de modelos. En el paralelismo de datos, el cálculo se divide entre distintas máquinas y los distintos trabajadores se entrenan en subconjuntos diferentes de los datos de entrenamiento. En el paralelismo de modelos, el modelo se divide y diferentes trabajadores realizan el cálculo de diferentes partes del modelo. En esta sección, nos centraremos en el paralelismo de datos y mostraremos implementaciones en TensorFlow utilizando la biblioteca tf.distribute.Strategy. Hablaremos del paralelismo de modelos en "Ventajas y Alternativas".

Para implementar el paralelismo de datos, debe existir un método para que los distintos trabajadores calculen gradientes y compartan esa información para realizar actualizaciones de los parámetros del modelo. Esto garantiza que todos los trabajadores sean coherentes y que cada paso de gradiente sirva para entrenar el modelo. En términos generales, el paralelismo de datos puede llevarse a cabo de forma sincrónica o asincrónica.

Entrenamiento sincrónico

En el entrenamiento síncrono, los trabajadores se entrenan en diferentes trozos de datos de entrada en paralelo y los valores del gradiente se agregan al final de cada paso de entrenamiento. Esto se realiza mediante un algoritmo de reducción total. Esto significa que cada trabajador, normalmente una GPU, tiene una copia del modelo en el dispositivo y, para un único paso de descenso de gradiente estocástico (SGD), se divide un mini lote de datos entre cada uno de los trabajadores por separado. Cada dispositivo realiza una pasada hacia delante con su porción del minilote y calcula los gradientes de cada parámetro del modelo. Estos gradientes calculados localmente se recogen de cada dispositivo y se agregan (por ejemplo, se promedian) para producir una única actualización del gradiente de cada parámetro. Un servidor central conserva la copia más actualizada de los parámetros del modelo y realiza el paso de gradiente según los gradientes recibidos de los múltiples trabajadores. Una vez actualizados los parámetros del modelo según este paso de gradiente agregado, el nuevo modelo se envía de nuevo a los trabajadores junto con otra división del siguiente minilote, y el proceso se repite. La Figura 4-15 muestra una arquitectura típica todo-reduce para la distribución sincrónica de datos.

Como ocurre con cualquier estrategia de paralelismo, esto introduce una sobrecarga adicional para gestionar el tiempo y la comunicación entre los trabajadores. Los modelos grandes podrían causar cuellos de botella de E/S al pasar los datos de la CPU a la GPU durante el entrenamiento, y las redes lentas también podrían causar retrasos.

En TensorFlow, tf.distribute.MirroredStrategy admite el entrenamiento distribuido síncrono en varias GPU de la misma máquina. Cada parámetro del modelo se refleja en todos los trabajadores y se almacena como una única variable conceptual llamada MirroredVariable. Durante el paso de reducción total, todos los tensores de gradiente están disponibles en cada dispositivo. Esto ayuda a reducir significativamente la sobrecarga de la sincronización. También existen otras implementaciones del algoritmo de reducción total, muchas de las cuales utilizan NVIDIA NCCL.

In synchronous training, each worker holds a copy of the model and computes gradients using a slice of the training data mini-batch.
Figura 4-15. En el entrenamiento síncrono, cada trabajador tiene una copia del modelo y calcula los gradientes utilizando una porción del mini lote de datos de entrenamiento.

Para poner en práctica esta estrategia reflejada en Keras, primero debes crear una instancia de la estrategia de distribución reflejada y, a continuación, mover la creación y compilación del modelo dentro del ámbito de esa instancia. El código siguiente muestra cómo utilizar MirroredStrategy al entrenar una red neuronal de tres capas:

mirrored_strategy = tf.distribute.MirroredStrategy()
with mirrored_strategy.scope():
    model = tf.keras.Sequential([tf.keras.layers.Dense(32, input_shape=(5,)),
                                 tf.keras.layers.Dense(16, activation='relu'),
                                 tf.keras.layers.Dense(1)])
    model.compile(loss='mse', optimizer='sgd')

Al crear el modelo dentro de este ámbito, los parámetros del modelo se crean como variables reflejadas en lugar de como variables normales. A la hora de ajustar el modelo al conjunto de datos, todo se realiza exactamente igual que antes. ¡El código del modelo sigue siendo el mismo! Envolver el código del modelo en el ámbito de la estrategia de distribución es todo lo que tienes que hacer para activar el entrenamiento distribuido. El MirroredStrategy se encarga de replicar los parámetros del modelo en las GPUs disponibles, agregar gradientes y mucho más. Para entrenar o evaluar el modelo, basta con llamar a fit() o evaluate() como de costumbre:

model.fit(train_dataset, epochs=2)
model.evaluate(train_dataset)

Durante el entrenamiento, cada lote de datos de entrada se divide a partes iguales entre los múltiples trabajadores. Por ejemplo, si utilizas dos GPUs, un lote de 10 se repartirá entre las 2 GPUs, recibiendo cada una 5 ejemplos de entrenamiento en cada paso. También hay otras estrategias de distribución síncrona dentro de Keras, como CentralStorageStrategy y MultiWorkerMirroredStrategy. MultiWorkerMirroredStrategy permiten repartir la distribución no sólo en las GPU de una sola máquina, sino en varias máquinas. En CentralStorageStrategy, las variables del modelo no se replican, sino que se colocan en la CPU y las operaciones se replican en todas las GPU locales. Así, las actualizaciones de las variables sólo se producen en un lugar.

A la hora de elegir entre distintas estrategias de distribución, la mejor opción depende de la topología de tu ordenador y de la velocidad a la que las CPUs y GPUs puedan comunicarse entre sí. La Tabla 4-2 resume cómo se comparan las distintas estrategias aquí descritas según estos criterios.

Tabla 4-2. La elección de una u otra estrategia de distribución depende de la topología de tu ordenador y de la velocidad a la que las CPUs y GPUs puedan comunicarse entre sí
Conexión CPU-GPU más rápida Conexión GPU-GPU más rápida
Una máquina con varias GPU CentralStorageStrategy MirroredStrategy
Varias máquinas con varias GPU MultiWorkerMirroredStrategy MultiWorkerMirroredStrategy

Formación asíncrona

En el entrenamiento asíncrono, los trabajadores se entrenan en diferentes partes de los datos de entrada de forma independiente, y los pesos y parámetros del modelo se actualizan de forma asíncrona, normalmente mediante una arquitectura de servidor de parámetros. Esto significa que ningún trabajador espera las actualizaciones del modelo de los demás trabajadores. En la arquitectura de servidor de parámetros, hay un único servidor de parámetros que gestiona los valores actuales de los pesos del modelo, como en la Figura 4-16.

Al igual que en el entrenamiento síncrono, se divide un mini lote de datos entre cada uno de los distintos trabajadores para cada paso del SGD. Cada dispositivo realiza una pasada hacia delante con su porción del mini lote y calcula gradientes para cada parámetro del modelo. Esos gradientes se envían al servidor de parámetros, que realiza la actualización de los parámetros y, a continuación, devuelve los nuevos parámetros del modelo al trabajador con otra división del siguiente mini lote.

La diferencia clave entre el entrenamiento síncrono y el asíncrono es que el servidor de parámetros no hace una reducción total. En su lugar, calcula los nuevos parámetros del modelo periódicamente basándose en las actualizaciones de gradiente que haya recibido desde el último cálculo. Normalmente, la distribución asíncrona consigue un mayor rendimiento que el entrenamiento síncrono, porque un trabajador lento no bloquea la progresión de los pasos de entrenamiento. Si falla un solo trabajador, el entrenamiento continúa según lo previsto con los demás trabajadores mientras ese trabajador se reinicia. Como resultado, algunas divisiones del minilote pueden perderse durante el entrenamiento, lo que dificulta el seguimiento preciso de cuántas épocas de datos se han procesado. Esta es otra razón por la que solemos especificar épocas virtuales al entrenar grandes trabajos distribuidos, en lugar de épocas; consulta "Patrón de diseño 12: Puntos de control" para una discusión sobre las épocas virtuales.

In asynchronous training, each worker performs a gradient descent step with a split of the mini-batch. No one worker waits for updates to the model from any of the other workers.
Figura 4-16. En el entrenamiento asíncrono, cada trabajador realiza un paso de descenso de gradiente con una división del minilote. Ningún trabajador espera las actualizaciones del modelo de ninguno de los demás trabajadores.

Además, como no hay sincronización entre las actualizaciones de los pesos, es posible que un trabajador actualice los pesos del modelo basándose en el estado obsoleto del modelo. Sin embargo, en la práctica, esto no parece ser un problema. Normalmente, las grandes redes neuronales se entrenan durante varias épocas, y estas pequeñas discrepancias se vuelven insignificantes al final.

En Keras, ParameterServerStrategy implementa la formación asíncrona de servidores de parámetros en varias máquinas. Cuando se utiliza esta distribución, algunas máquinas se designan como trabajadores y otras se mantienen como servidores de parámetros. Los servidores de parámetros mantienen cada variable del modelo, y el cálculo se realiza en los trabajadores, normalmente GPUs.

La implementación es similar a la de otras estrategias de distribución en Keras. Por ejemplo, en tu código, bastaría con sustituir MirroredStrategy() por ParameterServerStrategy().

Consejo

Otra estrategia de distribución soportada en Keras que merece la pena mencionar es OneDeviceStrategy. Esta estrategia colocará cualquier variable creada en su ámbito en el dispositivo especificado. Esta estrategia es particularmente útil como forma de probar tu código antes de cambiar a otras estrategias que realmente distribuyen a múltiples dispositivos/máquinas.

El entrenamiento síncrono y el asíncrono tienen cada uno sus ventajas y sus inconvenientes, y elegir entre ambos suele reducirse a las limitaciones del hardware y de la red .

El entrenamiento síncrono es especialmente vulnerable a los dispositivos lentos o a una mala conexión de red, porque el entrenamiento se paralizará esperando las actualizaciones de todos los trabajadores. Esto significa que la distribución sincrónica es preferible cuando todos los dispositivos están en un único host y hay dispositivos rápidos (por ejemplo, TPUs o GPUs) con enlaces potentes. En cambio, la distribución asíncrona es preferible si hay muchos trabajadores poco potentes o poco fiables. Si un solo trabajador falla o se atasca al devolver una actualización del gradiente, no se atascará el bucle de entrenamiento. La única limitación son las restricciones de E/S.

Por qué funciona

Las redes neuronales grandes y complejas requieren cantidades masivas de datos de entrenamiento para ser eficaces. Los esquemas de entrenamiento distribuido aumentan drásticamente el rendimiento de los datos procesados por estos modelos y pueden reducir eficazmente los tiempos de entrenamiento de semanas a horas. Compartir recursos entre los trabajadores y parametrizar las tareas del servidor conduce a un aumento espectacular del rendimiento de los datos. La Figura 4-17 compara el rendimiento de los datos de entrenamiento, en este caso imágenes, con diferentes configuraciones de distribución.5 Lo más notable es que el rendimiento aumenta con el número de nodos trabajadores y, aunque los servidores de parámetros realizan tareas no relacionadas con el cálculo realizado en los trabajadores de la GPU, dividir la carga de trabajo entre más máquinas es la estrategia más ventajosa.

Además, la paralelización de datos disminuye el tiempo de convergencia durante el entrenamiento. En un estudio similar, se demostró que aumentar los trabajadores lleva a la pérdida mínima mucho más rápido.6 La Figura 4-18 compara el tiempo hasta el mínimo para diferentes estrategias de distribución. A medida que aumenta el número de trabajadores, el tiempo hasta la pérdida mínima de entrenamiento disminuye drásticamente, mostrando casi una aceleración de 5× con 8 trabajadores frente a sólo 1.

Comparison of throughput between different distribution setups. Here, 2W1PS indicates 2 workers and 1 parameter server.
Figura 4-17. Comparación del rendimiento entre diferentes configuraciones de distribución. Aquí, 2W1PS indica dos trabajadores y un servidor de parámetros.
As the number of GPUs increases, the time to convergence during training decreases.
Figura 4-18. A medida que aumenta el número de GPUs, disminuye el tiempo de convergencia durante el entrenamiento.

Contrapartidas y alternativas

Además del paralelismo de datos, hay que tener en cuenta otros aspectos de la distribución, como el paralelismo de modelos, otros aceleradores de entrenamiento (como las TPU) y otras consideraciones (como las limitaciones de E/S y el tamaño de los lotes).

Paralelismo de modelos

En algunos casos, la red neuronal es tan grande que no cabe en la memoria de un solo dispositivo; por ejemplo, la Traducción Neuronal Automática de Google tiene miles de millones de parámetros. Para entrenar modelos tan grandes, deben dividirse en varios dispositivos,7 como se muestra en la Figura 4-19. Esto se llama paralelismo de modelos. Al dividir partes de una red y sus cálculos asociados en varios núcleos, la carga de trabajo de cálculo y memoria se distribuye en varios dispositivos. Cada dispositivo opera sobre el mismo mini lote de datos durante el entrenamiento, pero realiza cálculos relacionados sólo con sus componentes separados del modelo.

Model parallelism partitions the model over multiple devices.
Figura 4-19. El paralelismo de modelos divide el modelo en varios dispositivos.

ASICs para un mejor rendimiento a menor coste

Otra forma de acelerar el proceso de entrenamiento es acelerar el hardware subyacente, por ejemplo utilizando circuitos integrados de aplicación específica (ASIC). En el aprendizaje automático, esto se refiere a los componentes de hardware diseñados específicamente para optimizar el rendimiento en los tipos de grandes cálculos matriciales en el corazón del bucle de entrenamiento. Las TPU de Google Cloud son ASIC que pueden utilizarse tanto para el entrenamiento de modelos como para hacer predicciones. Del mismo modo, Microsoft Azure ofrece el Azure FPGA (field-programmable gate array), que también es un chip de aprendizaje automático personalizado como el ASIC, salvo que puede reconfigurarse con el tiempo. Estos chips son capaces de minimizar enormemente el tiempo de precisión cuando se entrenan modelos de redes neuronales grandes y complejos. Un modelo que tarda dos semanas en entrenarse en las GPU puede converger en horas en las TPU.

Utilizar chips de aprendizaje automático personalizados tiene otras ventajas. Por ejemplo, a medida que los aceleradores (GPU, FPGA, TPU, etc.) se han hecho más rápidos, la E/S se ha convertido en un importante cuello de botella en el entrenamiento de ML. Muchos procesos de entrenamiento desperdician ciclos esperando a leer y mover datos al acelerador y esperando a que las actualizaciones de gradiente lleven a cabo la reducción total. Los pods de TPU tienen interconexión de alta velocidad, por lo que no solemos preocuparnos por la sobrecarga de comunicación dentro de un pod (un pod consta de miles de TPUs). Además, hay mucha memoria disponible en el disco, lo que significa que es posible obtener datos de forma preventiva y hacer llamadas menos frecuentes a la CPU. Como resultado, debes utilizar tamaños de lote mucho mayores para aprovechar al máximo los chips de alta memoria y alta interconexión como las TPUs.

En cuanto al entrenamiento distribuido, TPUStrategy te permite ejecutar trabajos de entrenamiento distribuido en TPUs. Bajo el capó, TPUStrategy es igual que MirroredStrategy, aunque las TPU tienen su propia implementación del algoritmo de reducción total.

Utilizar TPUStrategy es similar a utilizar las otras estrategias de distribución en TensorFlow. Una diferencia es que primero debes configurar un TPUClusterResolver, que apunta a la ubicación de las TPU. Las TPUs están actualmente disponibles para su uso gratuito en Google Colab, y allí no necesitas especificar ningún argumento para tpu_address:

cluster_resolver = tf.distribute.cluster_resolver.TPUClusterResolver(
    tpu=tpu_address)
tf.config.experimental_connect_to_cluster(cluster_resolver)
tf.tpu.experimental.initialize_tpu_system(cluster_resolver)
tpu_strategy = tf.distribute.experimental.TPUStrategy(cluster_resolver)

Elegir el tamaño del lote

Otro factor importante a tener en cuenta es el tamaño del lote. Particularmente en el paralelismo síncrono de datos, cuando el modelo es particularmente grande, es mejor disminuir el número total de iteraciones de entrenamiento, porque cada paso de entrenamiento requiere que el modelo actualizado se comparta entre distintos trabajadores, lo que provoca una ralentización del tiempo de transferencia. Por lo tanto, es importante aumentar el tamaño del minilote todo lo posible para que se pueda alcanzar el mismo rendimiento con menos pasos.

Sin embargo, se ha demostrado que los tamaños de lote muy grandes afectan negativamente a la velocidad a la que converge el descenso por gradiente estocástico, así como a la calidad de la solución final.8 La Figura 4-20 muestra que aumentar el tamaño del lote por sí solo hace que, en última instancia, aumente el error de validación top-1. De hecho, sostienen que es necesario escalar linealmente la tasa de aprendizaje en función del tamaño de lote grande para mantener un error de validación bajo y, al mismo tiempo, disminuir el tiempo de entrenamiento distribuido.

Large batch sizes have been shown to adversely affect the quality of the final trained model.
Figura 4-20. Se ha demostrado que los tamaños de lote grandes afectan negativamente a la calidad del modelo entrenado final.

Así pues, establecer el tamaño del minilote en el contexto del entrenamiento distribuido es un espacio de optimización complejo en sí mismo, ya que afecta tanto a la precisión estadística (generalización) como a la eficiencia del hardware (utilización) del modelo. Un trabajo relacionado, centrado en esta optimización, introduce una técnica de optimización de grandes lotes adaptativa por capas llamada LAMB, que ha conseguido reducir el tiempo de entrenamiento del BERT de 3 días a sólo 76 minutos.

Minimizar las esperas de E/S

Las GPUs y TPUs pueden procesar los datos mucho más rápido que las CPUs, y cuando se utilizan estrategias distribuidas con múltiples aceleradores, los conductos de E/S pueden tener dificultades para seguir el ritmo, creando un cuello de botella para un entrenamiento más eficiente. En concreto, antes de que termine un paso de entrenamiento, los datos del paso siguiente no están disponibles para su procesamiento. Esto se muestra en la Figura 4-21. La CPU se encarga de la cadena de entrada: leer los datos del almacenamiento, preprocesarlos y enviarlos al acelerador para su cálculo. A medida que las estrategias distribuidas aceleran el entrenamiento, se hace más necesario que nunca disponer de canalizaciones de entrada eficientes para aprovechar al máximo la potencia de cálculo disponible.

Esto puede conseguirse de varias maneras, entre ellas utilizando formatos de archivo optimizados como TFRecords y construyendo canalizaciones de datos utilizando la API TensorFlow tf.data. La API tf.data permite manejar grandes cantidades de datos y tiene incorporadas transformaciones útiles para crear canalizaciones flexibles y eficientes. Por ejemplo, tf.data.Dataset.prefetch solapa el preprocesamiento y la ejecución del modelo de un paso de entrenamiento, de modo que mientras el modelo está ejecutando el paso de entrenamiento N, la canalización de entrada está leyendo y preparando datos para el paso de entrenamiento N + 1, como se muestra en la Figura 4-22.

With distributed training on multiple GPU/TPUs available, it is necessary to have efficient input pipelines.
Figura 4-21. Con el entrenamiento distribuido en múltiples GPU/TPU disponibles, es necesario disponer de pipelines de entrada eficientes.
Prefetching overlaps preprocessing and model execution, so that while the model is executing one training step, the input pipeline is reading and preparing data for the next.
Figura 4-22. La precarga solapa el preprocesamiento y la ejecución del modelo, de modo que mientras el modelo ejecuta un paso de entrenamiento, el conducto de entrada lee y prepara los datos para el siguiente.

Patrón de diseño 15: Ajuste de hiperparámetros

En el ajuste de hiperparámetros, el bucle de entrenamiento se inserta a su vez en un método de optimización para encontrar el conjunto óptimo de hiperparámetros del modelo.

Problema

En el aprendizaje automático, el entrenamiento del modelo consiste en encontrar el conjunto óptimo de puntos de ruptura (en el caso de los árboles de decisión), pesos (en el caso de las redes neuronales) o vectores de soporte (en el caso de las máquinas de vectores de soporte). Los denominamos parámetros del modelo. Sin embargo, para llevar a cabo el entrenamiento del modelo y encontrar los parámetros óptimos del mismo, a menudo tenemos que codificar una serie de cosas. Por ejemplo, podemos decidir que la profundidad máxima de un árbol sea 5 (en el caso de los árboles de decisión), o que la función de activación sea ReLU (para las redes neuronales) o elegir el conjunto de núcleos que emplearemos (en las SVM). Estos parámetros se denominan hiperparámetros.

Los parámetros del modelo se refieren a los pesos y sesgos aprendidos por tu modelo. No tienes control directo sobre los parámetros del modelo, ya que dependen en gran medida de tus datos de entrenamiento, de la arquitectura del modelo y de muchos otros factores. En otras palabras, no puedes establecer manualmente los parámetros del modelo. Los pesos de tu modelo se inicializan con valores aleatorios y luego tu modelo los optimiza a medida que pasa por las iteraciones de entrenamiento. Los hiperparámetros, en cambio, se refieren a los parámetros que tú, como constructor del modelo, puedes controlar. Incluyen valores como la tasa de aprendizaje, el número de épocas, el número de capas de tu modelo, etc.

Ajuste manual

Como puedes seleccionar manualmente los valores de los distintos hiperparámetros, tu primer instinto podría ser un enfoque de prueba y error para encontrar la combinación óptima de valores de hiperparámetros. Esto puede funcionar para modelos que se entrenan en segundos o minutos, pero puede resultar caro rápidamente en modelos más grandes que requieren un tiempo de entrenamiento y una infraestructura considerables. Imagina que estás entrenando un modelo de clasificación de imágenes que tarda horas en entrenarse en GPUs. Decides probar unos cuantos valores de hiperparámetros y esperas a los resultados de la primera ejecución del entrenamiento. En función de estos resultados, ajustas los hiperparámetros, vuelves a entrenar el modelo, comparas los resultados con los de la primera ejecución y, a continuación, determinas los mejores valores de hiperparámetros observando la ejecución de entrenamiento con las mejores métricas.

Hay algunos problemas con este enfoque. En primer lugar, has dedicado casi un día y muchas horas de cálculo a esta tarea. En segundo lugar, no hay forma de saber si has llegado a la combinación óptima de valores de hiperparámetros. Sólo has probado dos combinaciones diferentes, y como has cambiado varios valores a la vez, no sabes qué parámetro ha influido más en el rendimiento. Incluso con ensayos adicionales, utilizar este enfoque agotará rápidamente tu tiempo y recursos informáticos, y puede que no produzca los valores de hiperparámetros más óptimos.

Nota

Aquí utilizamos el término ensayo para referirnos a una única ejecución de entrenamiento con un conjunto de valores de hiperparámetros.

Búsqueda en cuadrícula y explosión combinatoria

Una versión más estructurada del método de ensayo y error descrito anteriormente se conoce como búsqueda en cuadrícula. Al aplicar el ajuste de hiperparámetros con la búsqueda en cuadrícula, elegimos una lista de posibles valores que nos gustaría probar para cada hiperparámetro que queremos optimizar. Por ejemplo, en el modelo RandomForestRegressor() de scikit-learn, digamos que queremos probar la siguiente combinación de valores para los hiperparámetros max_depth y n_estimators del modelo:

grid_values = {
  'max_depth': [5, 10, 100],
  'n_estimators': [100, 150, 200]
}

Utilizando la búsqueda de cuadrícula, probaríamos todas las combinaciones de los valores especificados, y luego utilizaríamos la combinación que produjera la mejor métrica de evaluación en nuestro modelo. Veamos cómo funciona esto en un modelo de bosque aleatorio entrenado con el conjunto de datos de viviendas de Boston, que viene preinstalado con scikit-learn. El modelo predecirá el precio de una vivienda basándose en una serie de factores. Podemos ejecutar la búsqueda reticular creando una instancia de la clase GridSearchCV y entrenando el modelo pasándole los valores que hemos definido antes:

from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_boston

X, y = load_boston(return_X_y=True)
housing_model = RandomForestRegressor()

grid_search_housing = GridSearchCV(
   housing_model, param_grid=grid_vals, scoring='max_error')
grid_search_housing.fit(X, y)

Ten en cuenta que aquí el parámetro de puntuación es la métrica que queremos optimizar. En el caso de este modelo de regresión, queremos utilizar la combinación de hiperparámetros que dé como resultado el error más bajo para nuestro modelo. Para obtener la mejor combinación de valores de la búsqueda en la cuadrícula, podemos ejecutar grid_search_housing.best_params_. Esto nos devuelve lo siguiente:

{'max_depth': 100, 'n_estimators': 150}

Deberíamos compararlo con el error que obtendríamos entrenando un modelo regresor de bosque aleatorio sin ajuste de hiperparámetros, utilizando los valores por defecto de scikit-learn para estos parámetros. Este enfoque de búsqueda en cuadrícula funciona bien en el pequeño ejemplo que hemos definido antes, pero con modelos más complejos, probablemente querríamos optimizar más de dos hiperparámetros, cada uno con una amplia gama de valores posibles. A la larga, la búsqueda en cuadrícula conducirá a una explosión combinatoria:a medidaque añadimos hiperparámetros y valores adicionales a nuestra cuadrícula de opciones, el número de combinaciones posibles que tenemos que probar y el tiempo necesario para probarlas todas aumenta significativamente.

Otro problema de este enfoque es que no se aplica ninguna lógica al elegir las distintas combinaciones. La búsqueda en cuadrícula es esencialmente una solución de fuerza bruta, probando todas las combinaciones posibles de valores. Supongamos que, a partir de un determinado valor de profundidad_máxima, el error de nuestro modelo aumenta. El algoritmo de búsqueda en cuadrícula no aprende de pruebas anteriores, por lo que no sabría dejar de probar los valores de max_depth a partir de un determinado umbral. Simplemente probará todos los valores que le proporciones sin importarle los resultados.

Nota

scikit-learn soporta una alternativa a la búsqueda cuadriculada llamada RandomizedSearchCV que implementa la búsqueda aleatoria. En lugar de probar todas las combinaciones posibles de hiperparámetros de un conjunto, determinas el número de veces que te gustaría muestrear aleatoriamente los valores de cada hiperparámetro. Para implementar la búsqueda aleatoria en scikit-learn, crearíamos una instancia de RandomizedSearchCV y le pasaríamos un dict similar al de grid_values anterior, especificando rangos en lugar de valores concretos. La búsqueda aleatoria se ejecuta más rápido que la búsqueda en cuadrícula, ya que no prueba todas las combinaciones de tu conjunto de valores posibles, pero es muy probable que el conjunto óptimo de hiperparámetros no esté entre los seleccionados aleatoriamente.

Para un ajuste robusto de los hiperparámetros, necesitamos una solución que escale y aprenda de los ensayos anteriores para encontrar una combinación óptima de los valores de los hiperparámetros.

Solución

La biblioteca keras-tuner implementa la optimización bayesiana para realizar la búsqueda de hiperparámetros directamente en Keras. Para utilizar keras-tuner, definimos nuestro modelo dentro de una función que toma un argumento hiperparámetro, aquí llamada hp. A continuación, podemos utilizar hp en toda la función siempre que queramos incluir un hiperparámetro, especificando el nombre del hiperparámetro, el tipo de datos, el intervalo de valores que queremos buscar y cuánto incrementarlo cada vez que probemos uno nuevo.

En lugar de codificar el valor del hiperparámetro cuando definimos una capa en nuestro modelo Keras, lo definimos utilizando una variable de hiperparámetro. Aquí queremos ajustar el número de neuronas de la primera capa oculta de nuestra red neuronal:

keras.layers.Dense(hp.Int('first_hidden', 32, 256, step=32), activation='relu')

first_hidden es el nombre que le hemos dado a este hiperparámetro, 32 es el valor mínimo que le hemos definido, 256 es el máximo y 32 es la cantidad en que debemos incrementar este valor dentro del rango que hemos definido. Si estuviéramos construyendo un modelo de clasificación MNIST, la función completa que pasaríamos a keras-tuner podría tener el siguiente aspecto:

def build_model(hp):
 model = keras.Sequential([
  keras.layers.Flatten(input_shape=(28, 28)),
  keras.layers.Dense(
    hp.Int('first_hidden', 32, 256, step=32), activation='relu'),
  keras.layers.Dense(
    hp.Int('second_hidden', 32, 256, step=32), activation='relu'),
  keras.layers.Dense(10, activation='softmax')
])

 model.compile(
   optimizer=tf.keras.optimizers.Adam(
     hp.Float('learning_rate', .005, .01, sampling='log')),
   loss='sparse_categorical_crossentropy', 
   metrics=['accuracy'])
  
 return model

La biblioteca keras-tuner admite muchos algoritmos de optimización diferentes. Aquí, instanciaremos nuestro sintonizador con la optimización bayesiana y optimizaremos la precisión de la validación:

import kerastuner as kt

tuner = kt.BayesianOptimization(
    build_model,
    objective='val_accuracy',
    max_trials=10
)

El código para ejecutar el trabajo de ajuste es similar al entrenamiento de nuestro modelo con fit(). Mientras se ejecuta, podremos ver los valores de los tres hiperparámetros seleccionados para cada prueba. Cuando se complete el trabajo, podremos ver la combinación de hiperparámetros que dio como resultado la mejor prueba. En la Figura 4-23, podemos ver el resultado de ejemplo de un único ensayo realizado con keras-tuner.

Output for one trial run of hyperparameter tuning with keras-tuner. At the top we can see the hyperparameters selected by the tuner, and in the summary section we see the resulting optimization metric.
Figura 4-23. Salida de una prueba de ajuste de hiperparámetros con keras-tuner. En la parte superior, podemos ver los hiperparámetros seleccionados por el sintonizador, y en la sección resumen, vemos la métrica de optimización resultante.

Además de los ejemplos mostrados aquí, keras-tuner ofrece otras funciones que no hemos tratado. Puedes utilizarlo para experimentar con distintos números de capas para tu modelo definiendo un parámetro hp.Int() dentro de un bucle, y también puedes proporcionar un conjunto fijo de valores para un hiperparámetro en lugar de un rango. Para modelos más complejos, este parámetro hp.Choice() podría utilizarse para experimentar con distintos tipos de capas, como BasicLSTMCell y BasicRNNCell. keras-tuner se ejecuta en cualquier entorno en el que puedas entrenar un modelo Keras.

Por qué funciona

Aunque la cuadrícula y la búsqueda aleatoria son más eficaces que un enfoque de prueba y error para el ajuste de hiperparámetros, se vuelven caras rápidamente para los modelos que requieren un tiempo de entrenamiento significativo o que tienen un espacio de búsqueda de hiperparámetros grande.

Puesto que tanto los propios modelos de aprendizaje automático como el proceso de búsqueda de hiperparámetros son problemas de optimización, se deduciría que podríamos utilizar un enfoque que aprendiera a encontrar la combinación óptima de hiperparámetros dentro de un rango determinado de valores posibles, igual que nuestros modelos aprenden de los datos de entrenamiento.

Podemos pensar en el ajuste de hiperparámetros como un bucle de optimización externo (ver Figura 4-24) en el que el bucle interno consiste en el entrenamiento típico del modelo. Aunque representemos las redes neuronales como el modelo cuyos parámetros se están optimizando, esta solución es aplicable a otros tipos de modelos de aprendizaje automático. Además, aunque el caso de uso más común es elegir un único modelo mejor de entre todos los hiperparámetros potenciales, en algunos casos, el marco de hiperparámetros puede utilizarse para generar una familia de modelos que pueden actuar como un conjunto (véase la discusión del patrón Conjuntos en el Capítulo 3).

Hyperparameter tuning can be thought of as an outer optimization loop.
Figura 4-24. El ajuste de hiperparámetros puede considerarse como un bucle externo de optimización.

Optimización no lineal

Los hiperparámetros que hay que ajustar se dividen en dos grupos: los relacionados con la arquitectura del modelo y los relacionados con el entrenamiento del modelo. Los hiperparámetros relacionados con la arquitectura del modelo, como el número de capas de tu modelo o el número de neuronas por capa, controlan la función matemática que subyace al modelo de aprendizaje automático. Los parámetros relacionados con el entrenamiento del modelo, como el número de épocas, la tasa de aprendizaje y el tamaño del lote, controlan el bucle de entrenamiento y a menudo tienen que ver con la forma en que funciona el optimizador de descenso de gradiente. Teniendo en cuenta estos dos tipos de parámetros, está claro que la función global del modelo con respecto a estos hiperparámetros no es, en general, diferenciable.

El bucle interno de entrenamiento es diferenciable, y la búsqueda de los parámetros óptimos puede realizarse mediante el descenso de gradiente estocástico. Un solo paso de un modelo de aprendizaje automático entrenado mediante gradiente estocástico puede tardar sólo unos milisegundos. En cambio, un solo ensayo en el problema de ajuste de hiperparámetros implica entrenar un modelo completo en el conjunto de datos de entrenamiento y puede llevar varias horas. Además, el problema de optimización de los hiperparámetros tendrá que resolverse mediante métodos de optimización no lineales que se aplican a problemas no diferenciables.

Una vez que decidimos que vamos a utilizar métodos de optimización no lineal, nuestra elección de la métrica se amplía. Esta métrica se evaluará en el conjunto de datos de validación y no tiene por qué ser la misma que la pérdida de entrenamiento. Para un modelo de clasificación, tu métrica de optimización podría ser la precisión, y por tanto querrías encontrar la combinación de hiperparámetros que conduzca a la mayor precisión del modelo, aunque la pérdida sea la entropía cruzada binaria. Para un modelo de regresión, puede que quieras optimizar el error absoluto medio aunque la pérdida sea el error al cuadrado. En ese caso, querrías encontrar los hiperparámetros que produzcan el error cuadrático medio más bajo. Esta métrica puede incluso elegirse en función de objetivos empresariales. Por ejemplo, podríamos elegir maximizar los ingresos esperados o minimizar las pérdidas por fraude.

Optimización bayesiana

La optimización bayesiana es una técnica para optimizar funciones de caja negra, desarrollada originalmente en los años 70 por Jonas Mockus. La técnica se ha aplicado a muchos dominios y se aplicó por primera vez al ajuste de hiperparámetros en 2012. Aquí nos centraremos en la optimización bayesiana en relación con el ajuste de hiperparámetros. En este contexto, un modelo de aprendizaje automático es nuestra función de caja negra, ya que los modelos ML producen un conjunto de salidas a partir de las entradas que les proporcionamos, sin necesidad de que conozcamos los detalles internos del propio modelo. El proceso de entrenamiento de nuestro modelo ML se denomina llamar a la función objetivo.

El objetivo de la optimización bayesiana es entrenar directamente nuestro modelo el menor número de veces posible, ya que hacerlo es costoso. Recuerda que cada vez que probamos una nueva combinación de hiperparámetros en nuestro modelo, tenemos que recorrer todo el ciclo de entrenamiento de nuestro modelo. Esto puede parecer trivial con un modelo pequeño como el de scikit-learn que hemos entrenado antes, pero para muchos modelos de producción, el proceso de entrenamiento requiere una infraestructura y un tiempo considerables.

En lugar de entrenar nuestro modelo cada vez que probamos una nueva combinación de hiperparámetros, la optimización bayesiana define una nueva función que emula nuestro modelo, pero que es mucho más barata de ejecutar. Se denomina función sustituta:las entradas de esta función son los valores de tus hiperparámetros y la salida es tu métrica de optimización. La función sustituta se ejecuta con mucha más frecuencia que la función objetivo, con el objetivo de encontrar una combinación óptima de hiperparámetros antes de completar una ejecución de entrenamiento de tu modelo. Con este enfoque, se invierte más tiempo de cálculo en elegir los hiperparámetros para cada ensayo, en comparación con la búsqueda en cuadrícula. Sin embargo, como esto es mucho más barato que ejecutar nuestra función objetivo cada vez que probamos diferentes hiperparámetros, se prefiere el enfoque bayesiano de utilizar una función sustituta. Los enfoques habituales para generar la función sustituta incluyen un proceso gaussiano o un estimador de Parzen estructurado en árbol.

Hasta ahora, hemos tocado las distintas piezas de la optimización bayesiana, pero ¿cómo funcionan juntas? En primer lugar, debemos elegir los hiperparámetros que queremos optimizar y definir un rango de valores para cada hiperparámetro. Esta parte del proceso es manual y definirá el espacio en el que nuestro algoritmo buscará los valores óptimos. También tendremos que definir nuestra función objetivo, que es el código que llama al proceso de entrenamiento de nuestro modelo. A partir de ahí, la optimización bayesiana desarrolla una función sustituta para simular el proceso de entrenamiento de nuestro modelo y utiliza esa función para determinar la mejor combinación de hiperparámetros para ejecutar en nuestro modelo. Sólo cuando este sustituto llega a lo que cree que es una buena combinación de hiperparámetros, realizamos un entrenamiento completo (prueba) de nuestro modelo. Los resultados se devuelven a la función sustituta y el proceso se repite durante el número de pruebas que hayamos especificado.

Contrapartidas y alternativas

Los algoritmos genéticos son una alternativa a los métodos bayesianos para el ajuste de hiperparámetros, pero suelen requerir muchas más ejecuciones de entrenamiento del modelo que los métodos bayesianos. También te mostraremos cómo utilizar un servicio gestionado para la optimización del ajuste de hiperparámetros en modelos construidos con diversos marcos de ML.

Ajuste de hiperparámetros totalmente gestionado

Es posible que el enfoque keras-tuner no se adapte a grandes problemas de aprendizaje automático, porque nos gustaría que los ensayos se realizaran en paralelo, y la probabilidad de error de la máquina y otros fallos aumenta a medida que el tiempo de entrenamiento del modelo se alarga durante horas. Por tanto, un enfoque totalmente gestionado y resistente que proporcione una optimización de caja negra es útil para el ajuste de hiperparámetros. Un ejemplo de servicio gestionado que implementa la optimización bayesiana es el servicio de ajuste de hiperparámetros proporcionado por Google Cloud AI Platform. Este servicio se basa en Vizier, la herramienta de optimización de caja negra utilizada internamente en Google.

Los conceptos subyacentes del servicio en la Nube funcionan de forma similar a keras-tuner: especificas el nombre, tipo, rango y escala de cada hiperparámetro, y estos valores se referencian en el código de entrenamiento de tu modelo. Te mostraremos cómo ejecutar el ajuste de hiperparámetros en la Plataforma IA utilizando un modelo PyTorch entrenado en el conjunto de datos de natalidad BigQuery para predecir el peso al nacer de un bebé.

El primer paso es crear un archivo config.yaml que especifique los hiperparámetros que quieres que optimice el trabajo, junto con algunos otros metadatos sobre tu trabajo. Una ventaja de utilizar el servicio en la Nube es que puedes escalar tu trabajo de optimización ejecutándolo en GPUs o TPUs y repartiéndolo entre varios servidores de parámetros. En este archivo de configuración, también especificas el número total de pruebas de hiperparámetros que quieres ejecutar y cuántas de estas pruebas quieres ejecutar en paralelo. Cuantas más ejecutes en paralelo, más rápido se ejecutará tu trabajo. Sin embargo, la ventaja de ejecutar menos pruebas en paralelo es que el servicio podrá aprender de los resultados de cada prueba completada para optimizar las siguientes.

Para nuestro modelo, un archivo de configuración de ejemplo que haga uso de las GPU podría tener el siguiente aspecto. En este ejemplo, ajustaremos tres hiperparámetros: la tasa de aprendizaje de nuestro modelo, el valor de impulso del optimizador y el número de neuronas de la capa oculta de nuestro modelo. También especificaremos nuestra métrica de optimización. En este ejemplo, nuestro objetivo será minimizar la pérdida de nuestro modelo en nuestro conjunto de validación:

trainingInput:
  scaleTier: BASIC_GPU
  parameterServerType: large_model
  workerCount: 9
  parameterServerCount: 3
  hyperparameters:
    goal: MINIMIZE
    maxTrials: 10
    maxParallelTrials: 5
    hyperparameterMetricTag: val_error
    enableTrialEarlyStopping: TRUE
    params:
    - parameterName: lr
      type: DOUBLE
      minValue: 0.0001
      maxValue: 0.1
      scaleType: UNIT_LINEAR_SCALE
    - parameterName: momentum
      type: DOUBLE
      minValue: 0.0
      maxValue: 1.0
      scaleType: UNIT_LINEAR_SCALE
    - parameterName: hidden-layer-size
      type: INTEGER
      minValue: 8
      maxValue: 32
      scaleType: UNIT_LINEAR_SCALE
Nota

En lugar de utilizar un archivo de configuración para definir estos valores, también puedes hacerlo utilizando la API Python de la Plataforma AI.

Para ello, tendremos que añadir a nuestro código un analizador sintáctico de argumentos que especifique los argumentos que definimos en el archivo anterior, y luego hacer referencia a estos hiperparámetros cuando aparezcan a lo largo del código de nuestro modelo.

A continuación, construiremos nuestro modelo utilizando la API nn.Sequential de PyTorch con el optimizador SGD. Como nuestro modelo predice el peso del bebé como una variable flotante, será un modelo de regresión. Especificamos cada uno de nuestros hiperparámetros utilizando la variable args, que contiene las variables definidas en nuestro analizador sintáctico de argumentos:

import torch.nn as nn

model = nn.Sequential(nn.Linear(num_features, args.hidden_layer_size),
                      nn.ReLU(),
                      nn.Linear(args.hidden_layer_size, 1))

optimizer = torch.optim.SGD(model.parameters(), lr=args.lr, 
                            momentum=args.momentum)

Al final del código de entrenamiento de nuestro modelo, crearemos una instancia de HyperTune(), y le indicaremos la métrica que estamos intentando optimizar. Esto informará del valor resultante de nuestra métrica de optimización después de cada ejecución de entrenamiento. Es importante que la métrica de optimización que elijamos se calcule sobre nuestros conjuntos de datos de prueba o validación, y no sobre nuestro conjunto de datos de entrenamiento:

import hypertune

hpt = hypertune.HyperTune()

val_mse = 0
num_batches = 0

criterion = nn.MSELoss()

with torch.no_grad():
    for i, (data, label) in enumerate(validation_dataloader):
        num_batches += 1
        y_pred = model(data)
        mse = criterion(y_pred, label.view(-1,1))
        val_mse += mse.item()

    avg_val_mse = (val_mse / num_batches)

hpt.report_hyperparameter_tuning_metric(
    hyperparameter_metric_tag='val_mse',
    metric_value=avg_val_mse,
    global_step=epochs        
)

Una vez que hemos enviado nuestro trabajo de entrenamiento a la Plataforma de IA, podemos monitorizar los registros en la consola de la Nube. Una vez completado cada ensayo, podrás ver los valores elegidos para cada hiperparámetro y el valor resultante de tu métrica de optimización, como se ve en la Figura 4-25.

A sample of the HyperTune summary in the AI Platform console. This is for a PyTorch model optimizing three model parameters, with the goal of minimizing mean squared error on the validation dataset.
Figura 4-25. Una muestra del resumen de HyperTune en la consola de la Plataforma IA. Esto es para un modelo PyTorch que optimiza tres parámetros del modelo, con el objetivo de minimizar el error cuadrático medio en el conjunto de datos de validación.

Por defecto, la Plataforma de Formación AI utilizará la optimización bayesiana para tu trabajo de ajuste, pero también puedes especificar si deseas utilizar algoritmos de búsqueda de cuadrícula o aleatorios en su lugar. El servicio en la Nube también optimiza tu búsqueda de hiperparámetros entre los trabajos de entrenamiento. Si ejecutamos otro trabajo de entrenamiento similar al anterior, pero con algunos ajustes en nuestros hiperparámetros y en el espacio de búsqueda, utilizará los resultados de nuestro último trabajo para elegir eficazmente los valores para el siguiente conjunto de ensayos.

Aquí hemos mostrado un ejemplo de PyTorch, pero puedes utilizar la Plataforma de Formación AI para el ajuste de hiperparámetros en cualquier marco de aprendizaje automático empaquetando tu código de formación y proporcionando un archivo setup.py que instale cualquier dependencia de la biblioteca.

Algoritmos genéticos

Hemos explorado varios algoritmos para la optimización de hiperparámetros: búsqueda manual, búsqueda en cuadrícula, búsqueda aleatoria y optimización bayesiana. Otra alternativa menos común es un algoritmo genético, que se basa aproximadamente en la teoría evolutiva de la selección natural de Charles Darwin. Esta teoría, también conocida como "supervivencia del más apto", postula que los miembros de mayor rendimiento ("más aptos") de una población sobrevivirán y transmitirán sus genes a las generaciones futuras, mientras que los miembros menos aptos no lo harán. Los algoritmos genéticos se han aplicado a distintos tipos de problemas de optimización, incluido el ajuste de hiperparámetros.

En lo que se refiere a la búsqueda de hiperparámetros, un enfoque genético funciona definiendo primero una función de aptitud. Esta función mide la calidad de un ensayo concreto, y normalmente puede definirse mediante la métrica de optimización de tu modelo (precisión, error, etc.). Tras definir tu función de adecuación, seleccionas al azar unas cuantas combinaciones de hiperparámetros de tu espacio de búsqueda y ejecutas un ensayo para cada una de esas combinaciones. A continuación, toma los hiperparámetros de los ensayos que hayan funcionado mejor y utiliza esos valores para definir tu nuevo espacio de búsqueda. Este espacio de búsqueda se convierte en tu nueva "población" y lo utilizas para generar nuevas combinaciones de valores que utilizarás en la siguiente serie de pruebas. Continúa este proceso, reduciendo el número de pruebas hasta que llegues a un resultado que satisfaga tus requisitos.

Como utilizan los resultados de ensayos anteriores para mejorar, los algoritmos genéticos son "más inteligentes" que la búsqueda manual, en cuadrícula y aleatoria. Sin embargo, cuando el espacio de búsqueda de hiperparámetros es grande, aumenta la complejidad de los algoritmos genéticos. En lugar de utilizar una función sustituta como sustituto para el entrenamiento del modelo, como en la optimización bayesiana, los algoritmos genéticos requieren entrenar tu modelo para cada combinación posible de valores de hiperparámetros. Además, en el momento de escribir estas líneas, los algoritmos genéticos son menos comunes y hay menos marcos de ML que los admitan directamente para el ajuste de hiperparámetros.

Resumen

Este capítulo se ha centrado en los patrones de diseño que modifican el típico bucle de entrenamiento SGD del aprendizaje automático. Empezamos examinando el patrón Sobreajuste útil , que abarca situaciones en las que el sobreajuste es beneficioso. Por ejemplo, cuando se utilizan métodos basados en datos como el aprendizaje automático para aproximar soluciones a sistemas dinámicos complejos o EDP en los que se puede cubrir todo el espacio de entrada, el objetivo es el sobreajuste en el conjunto de entrenamiento. El sobreajuste también es útil como técnica al desarrollar y depurar arquitecturas de modelos de ML. A continuación, cubrimos los Puntos de Comprobación del modelo y cómo utilizarlos al entrenar modelos ML. En este patrón de diseño, guardamos el estado completo del modelo periódicamente durante el entrenamiento. Estos puntos de comprobación pueden utilizarse como modelo final, como en el caso de la detención temprana, o utilizarse como puntos de partida en caso de fallos en el entrenamiento o de ajuste fino.

El patrón de diseño del Aprendizaje por Transferencia abarcaba la reutilización de partes de un modelo previamente entrenado. El aprendizaje por transferencia es una forma útil de aprovechar las capas de extracción de características aprendidas del modelo preentrenado cuando tu propio conjunto de datos es limitado. También puede utilizarse para ajustar a tu conjunto de datos más especializado un modelo previamente entrenado en un gran conjunto de datos genérico. A continuación, hablamos del patrón de diseño Estrategia de distribución. Entrenar redes neuronales grandes y complejas puede llevar un tiempo considerable. Las estrategias de distribución ofrecen varias formas de modificar el bucle de entrenamiento para que se realice a escala en varios trabajadores, utilizando la paralelización y los aceleradores de hardware.

Por último, el patrón de diseño Ajuste de hiperparámetros analizó cómo puede optimizarse el propio bucle de entrenamiento SGD con respecto a los hiperparámetros del modelo. Vimos algunas bibliotecas útiles que pueden utilizarse para implementar el ajuste de hiperparámetros para modelos creados con Keras y PyTorch.

En el capítulo siguiente se examinan los patrones de diseño relacionados con la resistencia (a un gran número de peticiones, tráfico irregular o gestión de cambios) al poner los modelos en producción.

1 Por supuesto, puede que no podamos aprender la red mediante el descenso de gradiente sólo porque exista tal red neuronal (por eso ayuda cambiar la arquitectura del modelo añadiendo capas: hace que el paisaje de pérdidas sea más susceptible de SGD).

2 MLPerf v0.7 Entrenamiento ResNet cerrado. Obtenido de www.mlperf.org 23 de septiembre de 2020, entrada 0.7-67. El nombre y el logotipo de MLPerf son marcas registradas. Consulta www.mlperf.org para obtener más información.

3 Jia Deng y otros,"ImageNet: Una base de datos jerárquica de imágenes a gran escala", IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR) (2009): 248-255.

4 Para más información, consulta "CS231n Redes neuronales convolucionales para el reconocimiento visual".

5 Victor Campos et al., "Estrategias de entrenamiento distribuido para un algoritmo de aprendizaje profundo de visión por ordenador en un clúster GPU distribuido", Conferencia Internacional sobre Ciencia Computacional, ICCS 2017, 12-14 de junio de 2017.

6 Ibid.

7 Jeffrey Dean et al. "Redes profundas distribuidas a gran escala", Actas del NIPS (2012).

8 Priya Goyal et al., "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour" (2017), arXiv:1706.02677v2 [cs.CV].

Get Patrones de diseño de aprendizaje automático 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.