Las múltiples aplicaciones del descenso de gradiente en TensorFlow

Publicado: 2022-03-11

TensorFlow de Google es una de las herramientas líderes para entrenar e implementar modelos de aprendizaje profundo. Es capaz de optimizar arquitecturas de redes neuronales tremendamente complejas con cientos de millones de parámetros, y viene con una amplia gama de herramientas para la aceleración de hardware, la capacitación distribuida y los flujos de trabajo de producción. Estas potentes funciones pueden hacer que parezca intimidante e innecesario fuera del dominio del aprendizaje profundo.

Pero TensorFlow puede ser accesible y utilizable para problemas más simples que no están directamente relacionados con el entrenamiento de modelos de aprendizaje profundo. En esencia, TensorFlow es solo una biblioteca optimizada para operaciones de tensor (vectores, matrices, etc.) y las operaciones de cálculo utilizadas para realizar el descenso de gradiente en secuencias arbitrarias de cálculos. Los científicos de datos experimentados reconocerán el "descenso de gradiente" como una herramienta fundamental para las matemáticas computacionales, pero generalmente requiere la implementación de códigos y ecuaciones específicos de la aplicación. Como veremos, aquí es donde entra en juego la moderna arquitectura de "diferenciación automática" de TensorFlow.

Casos de uso de TensorFlow

  • Ejemplo 1: regresión lineal con descenso de gradiente en TensorFlow 2.0
    • ¿Qué es el descenso de gradiente?
  • Ejemplo 2: vectores unitarios de dispersión máxima
  • Ejemplo 3: Generación de entradas de IA contradictorias
  • Pensamientos finales: Optimización de descenso de gradiente
  • Descenso de gradiente en TensorFlow: desde encontrar mínimos hasta atacar sistemas de IA

Ejemplo 1: regresión lineal con descenso de gradiente en TensorFlow 2.0

Ejemplo 1 Cuaderno

Antes de llegar al código de TensorFlow, es importante familiarizarse con el descenso de gradiente y la regresión lineal.

¿Qué es el descenso de gradiente?

En los términos más simples, es una técnica numérica para encontrar las entradas de un sistema de ecuaciones que minimizan su salida. En el contexto del aprendizaje automático, ese sistema de ecuaciones es nuestro modelo , las entradas son los parámetros desconocidos del modelo y la salida es una función de pérdida a minimizar, que representa cuánto error hay entre el modelo y nuestros datos. Para algunos problemas (como la regresión lineal), existen ecuaciones para calcular directamente los parámetros que minimizan nuestro error, pero para la mayoría de las aplicaciones prácticas, necesitamos técnicas numéricas como el descenso de gradiente para llegar a una solución satisfactoria.

El punto más importante de este artículo es que el descenso de gradiente generalmente requiere diseñar nuestras ecuaciones y usar cálculo para derivar la relación entre nuestra función de pérdida y nuestros parámetros. Con TensorFlow (y cualquier herramienta moderna de diferenciación automática), nosotros manejamos el cálculo, por lo que podemos centrarnos en diseñar la solución y no tener que dedicar tiempo a su implementación.

Esto es lo que parece en un problema de regresión lineal simple. Tenemos una muestra de las alturas (h) y los pesos (w) de 150 machos adultos y comenzamos con una suposición imperfecta de la pendiente y la desviación estándar de esta línea. Después de aproximadamente 15 iteraciones de descenso de gradiente, llegamos a una solución casi óptima.

Dos animaciones sincronizadas. El lado izquierdo muestra un diagrama de dispersión de altura-peso, con una línea ajustada que comienza lejos de los datos, luego se mueve rápidamente hacia ellos, disminuyendo la velocidad antes de encontrar el ajuste final. El tamaño correcto muestra un gráfico de pérdida versus iteración, con cada cuadro agregando una nueva iteración al gráfico. La pérdida comienza arriba de la parte superior del gráfico en 2000, pero se acerca rápidamente a la línea de pérdida mínima en unas pocas iteraciones en lo que parece ser una curva logarítmica.

Veamos cómo producimos la solución anterior usando TensorFlow 2.0.

Para la regresión lineal, decimos que los pesos se pueden predecir mediante una ecuación lineal de alturas.

w-subíndice-i,pred es igual a alfa punto-producto h-subíndice-i más beta.

Queremos encontrar parámetros α y β (pendiente e intersección) que minimicen el error cuadrático promedio (pérdida) entre las predicciones y los valores verdaderos. Entonces, nuestra función de pérdida (en este caso, el "error cuadrático medio" o MSE) se ve así:

MSE es igual a uno sobre N veces la suma de i es igual a uno a N del cuadrado de la diferencia entre w-subíndice-i,verdadero y w-subíndice-i,pred.

Podemos ver como el error cuadrático medio busca un par de líneas imperfectas, y luego con la solución exacta (α=6.04, β=-230.5).

Tres copias del mismo diagrama de dispersión de altura y peso, cada una con una línea ajustada diferente. El primero tiene w = 4,00 * h + -120,0 y una pérdida de 1057,0; la línea está debajo de los datos y menos empinada que ellos. El segundo tiene w = 2,00 * h + 70,0 y una pérdida de 720,8; la línea está cerca de la parte superior de los puntos de datos e incluso menos empinada. El tercero tiene w = 60,4 * h + -230,5 y una pérdida de 127,1; la línea pasa a través de los puntos de datos de manera que aparecen agrupados uniformemente a su alrededor.

Pongamos esta idea en acción con TensorFlow. Lo primero que hay que hacer es codificar la función de pérdida usando tensores y funciones tf.* .

 def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse

Esto parece bastante sencillo. Todos los operadores algebraicos estándar están sobrecargados para los tensores, por lo que solo tenemos que asegurarnos de que las variables que estamos optimizando sean tensores, y usamos los métodos tf.* para cualquier otra cosa.

Luego, todo lo que tenemos que hacer es poner esto en un ciclo de descenso de gradiente:

 def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]

Tomemos un momento para apreciar lo bueno que es esto. El descenso de gradiente requiere calcular derivadas de la función de pérdida con respecto a todas las variables que estamos tratando de optimizar. Se supone que el cálculo está involucrado, pero en realidad no hicimos nada de eso. La magia está en el hecho de que:

  1. TensorFlow crea un gráfico de cálculo de cada cálculo realizado bajo un tf.GradientTape() .
  2. TensorFlow sabe cómo calcular las derivadas (gradientes) de cada operación, por lo que puede determinar cómo cualquier variable en el gráfico de cálculo afecta a cualquier otra variable.

¿Cómo se ve el proceso desde diferentes puntos de partida?

Los mismos gráficos sincronizados que antes, pero también sincronizados con un par de gráficos similares debajo de ellos para comparar. El gráfico de iteración de pérdida del par inferior es similar pero parece converger más rápido; su línea ajustada correspondiente comienza desde arriba de los puntos de datos en lugar de desde abajo, y más cerca de su lugar de descanso final.

El descenso de gradiente se acerca notablemente al MSE óptimo, pero en realidad converge a una pendiente e intersección sustancialmente diferentes que el óptimo en ambos ejemplos. En algunos casos, esto es simplemente un descenso de gradiente que converge al mínimo local, lo cual es un desafío inherente con los algoritmos de descenso de gradiente. Pero es probable que la regresión lineal solo tenga un mínimo global. Entonces, ¿cómo terminamos en la pendiente e intersección equivocadas?

En este caso, el problema es que simplificamos demasiado el código por el bien de la demostración. No normalizamos nuestros datos y el parámetro de pendiente tiene una característica diferente al parámetro de intersección. Pequeños cambios en la pendiente pueden producir cambios masivos en la pérdida, mientras que pequeños cambios en la intercepción tienen muy poco efecto. Esta enorme diferencia en la escala de los parámetros entrenables lleva a que la pendiente domine los cálculos de gradiente, con el parámetro de intercepción casi ignorado.

Entonces, el descenso de gradiente encuentra efectivamente la mejor pendiente muy cerca de la conjetura de intercepción inicial. Y dado que el error está tan cerca del óptimo, los gradientes a su alrededor son pequeños, por lo que cada iteración sucesiva se mueve solo un poquito. La normalización de nuestros datos primero habría mejorado drásticamente este fenómeno, pero no lo habría eliminado.

Este fue un ejemplo relativamente simple, pero veremos en las siguientes secciones que esta capacidad de "diferenciación automática" puede manejar algunas cosas bastante complejas.

Ejemplo 2: vectores unitarios de dispersión máxima

Ejemplo 2 Cuaderno

El siguiente ejemplo se basa en un divertido ejercicio de aprendizaje profundo en un curso de aprendizaje profundo que tomé el año pasado.

La esencia del problema es que tenemos un "codificador automático variacional" (VAE) que puede producir caras realistas a partir de un conjunto de 32 números normalmente distribuidos. Para la identificación de sospechosos, queremos usar el VAE para producir un conjunto diverso de caras (teóricas) para que un testigo elija, luego restringir la búsqueda produciendo más caras similares a las que se eligieron. Para este ejercicio, se sugirió aleatorizar el conjunto inicial de vectores, pero quería encontrar un estado inicial óptimo.

Podemos expresar el problema de esta manera: dado un espacio de 32 dimensiones, encuentre un conjunto de X vectores unitarios que estén separados al máximo. En dos dimensiones, esto es fácil de calcular exactamente. Pero para tres dimensiones (¡o 32 dimensiones!), no hay una respuesta sencilla. Sin embargo, si podemos definir una función de pérdida adecuada que esté en su mínimo cuando hayamos alcanzado nuestro estado objetivo, tal vez el gradiente descendente pueda ayudarnos a llegar allí.

Dos gráficos. El gráfico de la izquierda, Estado inicial para todos los experimentos, tiene un punto central conectado a otros puntos, casi todos los cuales forman un semicírculo a su alrededor; un punto se encuentra aproximadamente opuesto al semicírculo. El gráfico de la derecha, Estado objetivo, es como una rueda, con los radios distribuidos uniformemente.

Comenzaremos con un conjunto aleatorio de 20 vectores como se muestra arriba y experimentaremos con tres funciones de pérdida diferentes, cada una con una complejidad creciente, para demostrar las capacidades de TensorFlow.

Primero definamos nuestro ciclo de entrenamiento. Pondremos toda la lógica de TensorFlow bajo el método self.calc_loss() , y luego simplemente podemos anular ese método para cada técnica, reciclando este bucle.

 # Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients

La primera técnica a probar es la más simple. Definimos una métrica de dispersión que es el ángulo de los vectores que están más juntos. Queremos maximizar la dispersión, pero es convencional convertirlo en un problema de minimización. Así que simplemente tomamos el negativo de la métrica de propagación:

 class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric

Un poco de magia de Matplotlib producirá una visualización.

Una animación que va del estado inicial al estado objetivo. El punto solitario permanece fijo, y el resto de los radios en el semicírculo se alternan moviéndose de un lado a otro, extendiéndose lentamente y sin lograr la equidistancia incluso después de 1200 iteraciones.

Esto es torpe (¡literalmente!) pero funciona. Solo dos de los 20 vectores se actualizan a la vez, aumentando el espacio entre ellos hasta que ya no son los más cercanos y luego cambiando para aumentar el ángulo entre los nuevos dos vectores más cercanos. Lo importante a tener en cuenta es que funciona . Vemos que TensorFlow pudo pasar gradientes a través del método tf.reduce_min() y el método tf.acos() para hacer lo correcto.

Probemos algo un poco más elaborado. Sabemos que en la solución óptima, todos los vectores deben tener el mismo ángulo con sus vecinos más cercanos. Así que agreguemos "varianza de ángulos mínimos" a la función de pérdida.

 class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric 

Una animación que va del estado inicial al estado objetivo. El radio solitario no permanece fijo, moviéndose rápidamente hacia el resto de los radios en el semicírculo; en lugar de cerrar dos espacios a cada lado del radio solitario, el temblor ahora cierra un gran espacio con el tiempo. La equidistancia aquí tampoco se logra del todo después de 1.200 iteraciones.

Ese vector solitario hacia el norte ahora se une rápidamente a sus pares, porque el ángulo con su vecino más cercano es enorme y aumenta el término de varianza que ahora se minimiza. Pero, en última instancia, sigue siendo impulsado por el ángulo mínimo global, que sigue siendo lento para aumentar. Las ideas que tengo para mejorar esto generalmente funcionan en este caso 2D, pero no en dimensiones superiores.

Pero centrarse demasiado en la calidad de este intento matemático es perder el sentido. Observe cuántas operaciones de tensor están involucradas en los cálculos de media y varianza, y cómo TensorFlow rastrea y diferencia con éxito cada cálculo para cada componente en la matriz de entrada. Y no tuvimos que hacer ningún cálculo manual. Simplemente hicimos algunos cálculos matemáticos simples y TensorFlow hizo el cálculo por nosotros.

Finalmente, probemos una cosa más: una solución basada en la fuerza. Imagina que cada vector es un pequeño planeta atado a un punto central. Cada planeta emite una fuerza que lo repele de los otros planetas. Si tuviéramos que ejecutar una simulación física de este modelo, deberíamos terminar en la solución deseada.

Mi hipótesis es que el descenso de gradiente también debería funcionar. En la solución óptima, la fuerza tangente en cada planeta de todos los demás planetas debería cancelarse a una fuerza neta cero (si no fuera cero, los planetas se estarían moviendo). Así que calculemos la magnitud de la fuerza en cada vector y usemos el gradiente descendente para empujarlo hacia cero.

Primero, necesitamos definir el método que calcula la fuerza usando los métodos tf.* :

 class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component

Luego, definimos nuestra función de pérdida usando la función de fuerza anterior. Acumulamos la fuerza neta en cada vector y calculamos su magnitud. En nuestra solución óptima, todas las fuerzas deberían cancelarse y deberíamos tener fuerza cero.

 def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list)) 

Una animación que va del estado inicial al estado objetivo. Los primeros cuadros ven un movimiento rápido en todos los radios, y después de solo 200 iteraciones, la imagen general ya está bastante cerca del objetivo. Solo se muestran 700 iteraciones en total; después del 300, los ángulos cambian solo minuciosamente con cada cuadro.

La solución no solo funciona a la perfección (además de algo de caos en los primeros cuadros), sino que el mérito real es de TensorFlow. Esta solución involucró múltiples bucles for , una declaración if y una gran red de cálculos, y TensorFlow rastreó con éxito los gradientes a través de todo eso para nosotros.

Ejemplo 3: Generación de entradas de IA contradictorias

Ejemplo 3 Cuaderno

En este punto, los lectores pueden estar pensando: "¡Oye! ¡No se suponía que esta publicación fuera sobre aprendizaje profundo!" Pero técnicamente, la introducción se refiere a ir más allá de " entrenar modelos de aprendizaje profundo". En este caso, no estamos entrenando , sino explotando algunas propiedades matemáticas de una red neuronal profunda preentrenada para engañarla y que nos dé resultados incorrectos. Esto resultó ser mucho más fácil y efectivo de lo imaginado. Y todo lo que se necesitó fue otra pequeña gota de código TensorFlow 2.0.

Comenzamos por encontrar un clasificador de imágenes para atacar. Usaremos una de las mejores soluciones para la competencia Kaggle Dogs vs. Cats; específicamente, la solución presentada por Kaggler “uysimty”. Todo el crédito para ellos por proporcionar un modelo efectivo de gato contra perro y proporcionar una excelente documentación. Este es un modelo poderoso que consta de 13 millones de parámetros en 18 capas de redes neuronales. (Los lectores pueden leer más sobre esto en el cuaderno correspondiente).

Tenga en cuenta que el objetivo aquí no es resaltar ninguna deficiencia en esta red en particular, sino mostrar cómo es vulnerable cualquier red neuronal estándar con una gran cantidad de entradas.

Relacionado: Lógica de sonido y modelos monotónicos de IA

Con algunos retoques, pude averiguar cómo cargar el modelo y preprocesar las imágenes para clasificarlas.

Cinco imágenes de muestra, cada una de un perro o un gato, con su correspondiente clasificación y nivel de confianza. Los niveles de confianza mostrados van del 95 al 100 por ciento.

¡Esto parece un clasificador realmente sólido! Todas las clasificaciones de las muestras son correctas y superan el 95 % de confianza. ¡Vamos a atacarlo!

Queremos producir una imagen que obviamente sea un gato pero que el clasificador decida que es un perro con mucha confianza. ¿Cómo podemos hacer eso?

Comencemos con la imagen de un gato que clasifica correctamente, luego descubramos cómo las pequeñas modificaciones en cada canal de color (valores 0-255) de un píxel de entrada determinado afectan la salida final del clasificador. La modificación de un píxel probablemente no sirva de mucho, pero quizás los ajustes acumulativos de todos los valores de 128x128x3 = 49 152 píxeles logren nuestro objetivo.

¿Cómo sabemos en qué dirección empujar cada píxel? Durante el entrenamiento normal de la red neuronal, tratamos de minimizar la pérdida entre la etiqueta de destino y la etiqueta predicha mediante el descenso de gradiente en TensorFlow para actualizar simultáneamente los 13 millones de parámetros libres. En este caso, dejaremos fijos los 13 millones de parámetros y ajustaremos los valores de píxel de la entrada en sí.

¿Cuál es nuestra función de pérdida? Bueno, ¡es lo mucho que la imagen se parece a un gato! Si calculamos la derivada del valor cat con respecto a cada píxel de entrada, sabemos de qué manera empujar cada uno para minimizar la probabilidad de clasificación cat.

 def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad

La magia de Matplotlib nuevamente ayuda a visualizar los resultados.

Una imagen de gato de muestra original junto con 4 iteraciones, con clasificaciones, "Gato 99,0 %", "Gato 67,3 %", "Perro 71,7 %", "Perro 94,3 %" y "Perro 99,4 %", respectivamente.

¡Guau! Para el ojo humano, cada una de estas imágenes es idéntica. Sin embargo, después de cuatro iteraciones, hemos convencido al clasificador de que es un perro, ¡con un 99,4 por ciento de confianza!

Asegurémonos de que esto no sea una casualidad y que funcione en la otra dirección también.

Una imagen de perro de muestra original junto con 4 iteraciones, con clasificaciones, "Perro 98,4 %", "Perro 83,9 %", "Perro 54,6 %", "Gato 90,4 %" y "Gato 99,8 %", respectivamente. Como antes, las diferencias son invisibles a simple vista.

¡Éxito! El clasificador originalmente predijo esto correctamente como un perro con un 98,4 % de confianza, y ahora cree que es un gato con un 99,8 % de confianza.

Finalmente, veamos un parche de imagen de muestra y veamos cómo cambió.

Tres cuadrículas de filas y columnas de píxeles, que muestran valores numéricos para el canal rojo de cada píxel. El parche de la imagen de la izquierda muestra en su mayoría cuadrados azulados, resaltando valores de 218 o menos, con algunos cuadrados rojos (219 y más) agrupados en la esquina inferior derecha. La página de imagen central, "victimizada", muestra un diseño numerado y coloreado muy similar. El parche de la imagen de la derecha muestra la diferencia numérica entre los otros dos, con diferencias que van solo de -4 a +4, e incluyen varios ceros.

Como era de esperar, el parche final es muy similar al original, con cada píxel cambiando solo de -4 a +4 en el valor de intensidad del canal rojo. Este cambio no es suficiente para que un humano distinga la diferencia, pero cambia por completo la salida del clasificador.

Pensamientos finales: Optimización de descenso de gradiente

A lo largo de este artículo, hemos analizado la aplicación manual de gradientes a nuestros parámetros entrenables en aras de la simplicidad y la transparencia. Sin embargo, en el mundo real, los científicos de datos deberían usar optimizadores de inmediato, ya que tienden a ser mucho más efectivos, sin agregar ningún exceso de código.

Hay muchos optimizadores populares, incluidos RMSprop, Adagrad y Adadelta, pero el más común es probablemente Adam . A veces, se denominan "métodos de tasa de aprendizaje adaptativo" porque mantienen dinámicamente una tasa de aprendizaje diferente para cada parámetro. Muchos de ellos utilizan términos de momento y derivadas de orden superior aproximadas, con el objetivo de escapar de los mínimos locales y lograr una convergencia más rápida.

En una animación tomada de Sebastian Ruder, podemos ver el camino de varios optimizadores descendiendo por una superficie de pérdida. Las técnicas manuales que hemos demostrado son más comparables a "SGD". El optimizador de mejor rendimiento no será el mismo para cada superficie de pérdida; sin embargo, los optimizadores más avanzados normalmente funcionan mejor que los más simples.

Un mapa de contorno animado, que muestra la ruta tomada por seis métodos diferentes para converger en un punto de destino. SGD es, con mucho, el más lento, tomando una curva constante desde su punto de partida. El impulso inicialmente se aleja del objetivo, luego se entrecruza en su propio camino dos veces antes de dirigirse hacia él no del todo directamente, y parece que lo sobrepasa y luego retrocede. NAG es similar, pero no se aleja tanto del objetivo y se entrecruza solo una vez, generalmente alcanzando el objetivo más rápido y rebasándolo menos. Adagrad comienza en una línea recta que es la más desviada, pero muy rápidamente hace un giro cerrado hacia la colina en la que se encuentra el objetivo y se curva hacia ella más rápido que los tres primeros. Adadelta tiene un recorrido similar, pero con una curva más suave; adelanta a Adagrad y se mantiene por delante después del primer segundo más o menos. Finalmente, Rmsprop sigue un camino muy similar a Adadelta, pero se inclina un poco más hacia el objetivo desde el principio; en particular, su curso es mucho más estable, por lo que se queda atrás de Adagrad y Adadelta durante la mayor parte de la animación; a diferencia de los otros cinco, parece tener dos saltos repentinos y rápidos en dos direcciones diferentes cerca del final de la animación antes de dejar de moverse, mientras que los otros, en el último momento, continúan arrastrándose lentamente hacia el objetivo.

Sin embargo, rara vez es útil ser un experto en optimizadores, incluso para aquellos interesados ​​​​en proporcionar servicios de desarrollo de inteligencia artificial. Es un mejor uso del tiempo de los desarrolladores familiarizarse con un par, solo para comprender cómo mejoran el gradiente descendente en TensorFlow. Después de eso, pueden usar Adam de manera predeterminada y probar diferentes solo si sus modelos no convergen.

Para los lectores que estén realmente interesados ​​en cómo y por qué funcionan estos optimizadores, la descripción general de Ruder, en la que aparece la animación, es uno de los mejores y más completos recursos sobre el tema.

Actualicemos nuestra solución de regresión lineal de la primera sección para usar optimizadores. El siguiente es el código de descenso de gradiente original usando gradientes manuales.

 # Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]

Ahora, aquí está el mismo código usando un optimizador en su lugar. Verá que casi no hay código adicional (las líneas modificadas están resaltadas en azul):

 # Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))

¡Eso es todo! Definimos un optimizador RMSprop fuera del ciclo de descenso de gradiente y luego usamos el optimizer.apply_gradients() después de cada cálculo de gradiente para actualizar los parámetros entrenables. El optimizador se define fuera del ciclo porque realizará un seguimiento de los gradientes históricos para calcular términos adicionales como impulso y derivados de orden superior.

Veamos cómo se ve con el optimizador RMSprop .

Similar a los pares de animaciones sincronizadas anteriores; la línea ajustada comienza por encima de su lugar de descanso. El gráfico de pérdida lo muestra casi convergiendo después de solo cinco iteraciones.

¡Se ve muy bien! Ahora probemos con el optimizador de Adam .

Otro gráfico de dispersión sincronizado y la correspondiente animación del gráfico de pérdida. El gráfico de pérdida se destaca de los demás en que estrictamente no continúa acercándose al mínimo; en cambio, se asemeja a la trayectoria de una pelota que rebota. La línea ajustada correspondiente en el diagrama de dispersión comienza por encima de los puntos de muestra, oscila hacia la parte inferior de ellos, luego retrocede pero no tan alto, y así sucesivamente, con cada cambio de dirección más cerca de una posición central.

Vaya, ¿qué pasó aquí? Parece que la mecánica del impulso en Adam hace que sobrepase la solución óptima e invierta el curso varias veces. Normalmente, esta mecánica de impulso ayuda con superficies de pérdida complejas, pero nos perjudica en este caso simple. Esto enfatiza el consejo de elegir el optimizador como uno de los hiperparámetros a ajustar al entrenar su modelo.

Cualquiera que desee explorar el aprendizaje profundo querrá familiarizarse con este patrón, ya que se usa ampliamente en arquitecturas personalizadas de TensorFlow, donde es necesario tener mecanismos de pérdida complejos que no se incluyen fácilmente en el flujo de trabajo estándar. En este ejemplo simple de descenso de gradiente de TensorFlow, solo había dos parámetros entrenables, pero es necesario cuando se trabaja con arquitecturas que contienen cientos de millones de parámetros para optimizar.

Descenso de gradiente en TensorFlow: desde encontrar mínimos hasta atacar sistemas de IA

Todos los fragmentos de código y las imágenes se generaron a partir de los cuadernos en el repositorio de GitHub correspondiente. También contiene un resumen de todas las secciones, con enlaces a los cuadernos individuales, para los lectores que deseen ver el código completo. En aras de simplificar el mensaje, se omitieron muchos detalles que se pueden encontrar en la extensa documentación en línea.

Espero que este artículo haya sido revelador y te haya hecho pensar en formas de usar el gradiente descendente en TensorFlow. Incluso si no lo usa usted mismo, es de esperar que aclare cómo funcionan todas las arquitecturas de redes neuronales modernas: cree un modelo, defina una función de pérdida y use el descenso de gradiente para ajustar el modelo a su conjunto de datos.


Insignia de Google Cloud Partner.

Como Google Cloud Partner, los expertos certificados por Google de Toptal están disponibles para las empresas que lo soliciten para sus proyectos más importantes.