Les nombreuses applications de la descente de gradient dans TensorFlow

Publié: 2022-03-11

TensorFlow de Google est l'un des principaux outils de formation et de déploiement de modèles d'apprentissage en profondeur. Il est capable d'optimiser des architectures de réseaux de neurones extrêmement complexes avec des centaines de millions de paramètres, et il est livré avec un large éventail d'outils pour l'accélération matérielle, la formation distribuée et les workflows de production. Ces fonctionnalités puissantes peuvent le rendre intimidant et inutile en dehors du domaine de l'apprentissage en profondeur.

Mais TensorFlow peut être à la fois accessible et utilisable pour des problèmes plus simples qui ne sont pas directement liés à la formation de modèles d'apprentissage en profondeur. À la base, TensorFlow n'est qu'une bibliothèque optimisée pour les opérations tensorielles (vecteurs, matrices, etc.) et les opérations de calcul utilisées pour effectuer une descente de gradient sur des séquences arbitraires de calculs. Les spécialistes des données expérimentés reconnaîtront la « descente de gradient » comme un outil fondamental pour les mathématiques computationnelles, mais cela nécessite généralement la mise en œuvre d'un code et d'équations spécifiques à l'application. Comme nous le verrons, c'est là qu'intervient l'architecture moderne de "différenciation automatique" de TensorFlow.

Cas d'utilisation de TensorFlow

  • Exemple 1 : Régression linéaire avec descente de gradient dans TensorFlow 2.0
    • Qu'est-ce que la descente de gradient ?
  • Exemple 2 : vecteurs unitaires à propagation maximale
  • Exemple 3 : Génération d'entrées d'IA contradictoires
  • Réflexions finales : optimisation de la descente de gradient
  • Descente de gradient dans TensorFlow : de la recherche des minimums à l'attaque des systèmes d'IA

Exemple 1 : Régression linéaire avec descente de gradient dans TensorFlow 2.0

Exemple 1 Cahier

Avant d'aborder le code TensorFlow, il est important de se familiariser avec la descente de gradient et la régression linéaire.

Qu'est-ce que la descente de gradient ?

Dans les termes les plus simples, il s'agit d'une technique numérique pour trouver les entrées d'un système d'équations qui minimisent sa sortie. Dans le contexte de l'apprentissage automatique, ce système d'équations est notre modèle , les entrées sont les paramètres inconnus du modèle et la sortie est une fonction de perte à minimiser, qui représente l'erreur entre le modèle et nos données. Pour certains problèmes (comme la régression linéaire), il existe des équations pour calculer directement les paramètres qui minimisent notre erreur, mais pour la plupart des applications pratiques, nous avons besoin de techniques numériques comme la descente de gradient pour arriver à une solution satisfaisante.

Le point le plus important de cet article est que la descente de gradient nécessite généralement de disposer nos équations et d'utiliser le calcul pour dériver la relation entre notre fonction de perte et nos paramètres. Avec TensorFlow (et tout outil d'auto-différenciation moderne), le calcul est géré pour nous, nous pouvons donc nous concentrer sur la conception de la solution et ne pas perdre de temps sur sa mise en œuvre.

Voici à quoi cela ressemble sur un problème de régression linéaire simple. Nous avons un échantillon des tailles (h) et des poids (w) de 150 mâles adultes, et commençons par une estimation imparfaite de la pente et de l'écart type de cette ligne. Après environ 15 itérations de descente de gradient, nous arrivons à une solution quasi-optimale.

Deux animations synchronisées. Le côté gauche montre un nuage de points taille-poids, avec une ligne ajustée qui commence loin des données, puis se déplace rapidement vers elles, ralentissant avant de trouver l'ajustement final. La bonne taille montre un graphique de perte par rapport à l'itération, chaque image ajoutant une nouvelle itération au graphique. La perte commence au-dessus du haut du graphique à 2 000, mais se rapproche rapidement de la ligne de perte minimale en quelques itérations dans ce qui semble être une courbe logarithmique.

Voyons comment nous avons produit la solution ci-dessus en utilisant TensorFlow 2.0.

Pour la régression linéaire, nous disons que les poids peuvent être prédits par une équation linéaire des hauteurs.

w-indice-i,pred est égal à alpha produit scalaire h-indice-i plus bêta.

Nous voulons trouver les paramètres α et β (pente et interception) qui minimisent l'erreur quadratique moyenne (perte) entre les prédictions et les valeurs réelles. Ainsi, notre fonction de perte (dans ce cas, « l'erreur quadratique moyenne » ou MSE) ressemble à ceci :

MSE est égal à un sur N fois la somme de i est égal à un à N du carré de la différence entre w-indice-i,vrai et w-indice-i,pred.

Nous pouvons voir à quoi ressemble l'erreur quadratique moyenne pour quelques lignes imparfaites, puis avec la solution exacte (α = 6,04, β = -230,5).

Trois copies du même nuage de points taille-poids, chacune avec une droite d'ajustement différente. Le premier a w = 4,00 * h + -120,0 et une perte de 1057,0 ; la ligne est en dessous des données et moins raide qu'elle. Le second a w = 2,00 * h + 70,0 et une perte de 720,8 ; la ligne est proche de la partie supérieure des points de données, et encore moins raide. Le troisième a w = 60,4 * h + -230,5 et une perte de 127,1 ; la ligne passe par les points de données de sorte qu'ils apparaissent uniformément regroupés autour d'elle.

Mettons cette idée en action avec TensorFlow. La première chose à faire est de coder la fonction de perte en utilisant des tenseurs et des fonctions 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

Cela semble assez simple. Tous les opérateurs algébriques standard sont surchargés pour les tenseurs, nous devons donc seulement nous assurer que les variables que nous optimisons sont des tenseurs, et nous utilisons les méthodes tf.* pour tout le reste.

Ensuite, tout ce que nous avons à faire est de mettre cela dans une boucle de descente de gradient :

 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]

Prenons un moment pour apprécier à quel point c'est soigné. La descente de gradient nécessite le calcul des dérivées de la fonction de perte par rapport à toutes les variables que nous essayons d'optimiser. Le calcul est censé être impliqué, mais nous n'en avons en fait rien fait. La magie réside dans le fait que :

  1. TensorFlow construit un graphique de calcul de chaque calcul effectué sous un tf.GradientTape() .
  2. TensorFlow sait comment calculer les dérivées (gradients) de chaque opération, de sorte qu'il peut déterminer comment n'importe quelle variable du graphique de calcul affecte n'importe quelle autre variable.

À quoi ressemble le processus à partir de différents points de départ ?

Les mêmes graphiques synchronisés qu'auparavant, mais également synchronisés avec une paire de graphiques similaires en dessous d'eux pour comparaison. Le graphique d'itération de perte de la paire inférieure est similaire mais semble converger plus rapidement ; sa ligne ajustée correspondante commence au-dessus des points de données plutôt qu'au-dessous, et plus près de son lieu de repos final.

La descente de gradient se rapproche remarquablement de la MSE optimale, mais converge en fait vers une pente et une interception sensiblement différentes de celles de l'optimum dans les deux exemples. Dans certains cas, il s'agit simplement d'une descente de gradient convergeant vers le minimum local, ce qui est un défi inhérent aux algorithmes de descente de gradient. Mais il est prouvé que la régression linéaire n'a qu'un seul minimum global. Alors, comment s'est-on retrouvé sur la mauvaise pente et intercepté ?

Dans ce cas, le problème est que nous avons trop simplifié le code pour des raisons de démonstration. Nous n'avons pas normalisé nos données et le paramètre de pente a une caractéristique différente de celle du paramètre d'interception. De minuscules changements de pente peuvent produire des changements massifs de perte, tandis que de minuscules changements d'interception ont très peu d'effet. Cette énorme différence d'échelle des paramètres entraînables conduit à la pente dominant les calculs de gradient, le paramètre d'interception étant presque ignoré.

Ainsi, la descente de gradient trouve efficacement la meilleure pente très proche de l'estimation d'interception initiale. Et comme l'erreur est si proche de l'optimum, les gradients qui l'entourent sont minuscules, de sorte que chaque itération successive ne se déplace que d'un tout petit peu. Normaliser d'abord nos données aurait considérablement amélioré ce phénomène, mais cela ne l'aurait pas éliminé.

C'était un exemple relativement simple, mais nous verrons dans les sections suivantes que cette capacité « d'auto-différenciation » peut gérer des choses assez complexes.

Exemple 2 : vecteurs unitaires à propagation maximale

Exemple 2 Cahier

Cet exemple suivant est basé sur un exercice d'apprentissage en profondeur amusant dans un cours d'apprentissage en profondeur que j'ai suivi l'année dernière.

L'essentiel du problème est que nous avons un "auto-encodeur variationnel" (VAE) qui peut produire des visages réalistes à partir d'un ensemble de 32 nombres normalement distribués. Pour l'identification des suspects, nous voulons utiliser le VAE pour produire un ensemble diversifié de visages (théoriques) parmi lesquels un témoin peut choisir, puis affiner la recherche en produisant plus de visages similaires à ceux qui ont été choisis. Pour cet exercice, il a été suggéré de randomiser l'ensemble initial de vecteurs, mais je voulais trouver un état initial optimal.

Nous pouvons formuler le problème comme ceci : étant donné un espace à 32 dimensions, trouvez un ensemble de vecteurs unitaires X qui sont au maximum écartés. En deux dimensions, c'est facile à calculer exactement. Mais pour trois dimensions (ou 32 dimensions !), il n'y a pas de réponse simple. Cependant, si nous pouvons définir une fonction de perte appropriée qui est à son minimum lorsque nous avons atteint notre état cible, peut-être que la descente de gradient peut nous aider à y parvenir.

Deux graphiques. Le graphique de gauche, État initial pour toutes les expériences, a un point central relié à d'autres points, qui forment presque tous un demi-cercle autour de lui ; un point se trouve à peu près à l'opposé du demi-cercle. Le graphique de droite, État cible, ressemble à une roue, avec des rayons répartis uniformément.

Nous allons commencer avec un ensemble aléatoire de 20 vecteurs comme indiqué ci-dessus et expérimenter avec trois fonctions de perte différentes, chacune avec une complexité croissante, pour démontrer les capacités de TensorFlow.

Définissons d'abord notre boucle d'entraînement. Nous placerons toute la logique TensorFlow sous la méthode self.calc_loss() , puis nous pourrons simplement remplacer cette méthode pour chaque technique, en recyclant cette boucle.

 # 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 première technique à essayer est la plus simple. Nous définissons une métrique de propagation qui est l'angle des vecteurs les plus proches les uns des autres. On veut maximiser la propagation, mais il est classique d'en faire un problème de minimisation. Nous prenons donc simplement le négatif de la métrique de propagation :

 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 peu de magie Matplotlib donnera une visualisation.

Une animation allant de l'état initial à l'état cible. Le seul point reste fixe, et le reste des rayons du demi-cercle oscille à tour de rôle d'avant en arrière, s'étendant lentement et n'atteignant pas l'équidistance même après 1 200 itérations.

C'est maladroit (littéralement !) mais ça marche. Seuls deux des 20 vecteurs sont mis à jour à la fois, augmentant l'espace entre eux jusqu'à ce qu'ils ne soient plus les plus proches, puis passant à l'augmentation de l'angle entre les deux nouveaux vecteurs les plus proches. La chose importante à noter est que cela fonctionne . Nous voyons que TensorFlow a pu passer des gradients via la méthode tf.reduce_min() et la méthode tf.acos() pour faire la bonne chose.

Essayons quelque chose d'un peu plus élaboré. Nous savons qu'à la solution optimale, tous les vecteurs doivent avoir le même angle par rapport à leurs voisins les plus proches. Ajoutons donc la "variance des angles minimaux" à la fonction de perte.

 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 

Une animation allant de l'état initial à l'état cible. Le rayon solitaire ne reste pas fixe, se déplaçant rapidement vers le reste des rayons dans le demi-cercle; au lieu de combler deux espaces de part et d'autre du rayon solitaire, la gigue ferme désormais un grand espace au fil du temps. L'équidistance n'est ici non plus pas tout à fait atteinte après 1 200 itérations.

Ce vecteur solitaire vers le nord rejoint maintenant rapidement ses pairs, car l'angle avec son voisin le plus proche est énorme et augmente le terme de variance qui est maintenant minimisé. Mais il est toujours motivé par l'angle global minimum qui reste lent à monter. Les idées que j'ai pour améliorer cela fonctionnent généralement dans ce cas 2D, mais pas dans des dimensions supérieures.

Mais se concentrer trop sur la qualité de cette tentative mathématique passe à côté de l'essentiel. Regardez combien d'opérations de tenseur sont impliquées dans les calculs de moyenne et de variance, et comment TensorFlow suit et différencie avec succès chaque calcul pour chaque composant de la matrice d'entrée. Et nous n'avons pas eu à faire de calcul manuel. Nous venons de jeter quelques calculs simples ensemble, et TensorFlow a fait le calcul pour nous.

Enfin, essayons encore une chose : une solution basée sur la force. Imaginez que chaque vecteur soit une petite planète attachée à un point central. Chaque planète émet une force qui la repousse des autres planètes. Si nous devions exécuter une simulation physique de ce modèle, nous devrions aboutir à la solution souhaitée.

Mon hypothèse est que la descente de gradient devrait également fonctionner. À la solution optimale, la force tangente sur chaque planète de toutes les autres planètes devrait s'annuler en une force nette nulle (si elle n'était pas nulle, les planètes se déplaceraient). Calculons donc l'amplitude de la force sur chaque vecteur et utilisons la descente de gradient pour la pousser vers zéro.

Tout d'abord, nous devons définir la méthode qui calcule la force à l'aide des méthodes 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

Ensuite, nous définissons notre fonction de perte en utilisant la fonction de force ci-dessus. Nous accumulons la force nette sur chaque vecteur et calculons sa magnitude. À notre solution optimale, toutes les forces devraient s'annuler et nous devrions avoir une force nulle.

 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)) 

Une animation allant de l'état initial à l'état cible. Les premières images voient un mouvement rapide dans tous les rayons, et après seulement 200 itérations environ, l'image globale est déjà assez proche de la cible. Seules 700 itérations sont affichées au total ; après le 300e, les angles ne changent que très légèrement à chaque image.

Non seulement la solution fonctionne à merveille (en plus du chaos dans les premières images), mais le vrai mérite revient à TensorFlow. Cette solution impliquait plusieurs boucles for , une instruction if et un énorme réseau de calculs, et TensorFlow a réussi à tracer des gradients à travers tout cela pour nous.

Exemple 3 : Génération d'entrées d'IA contradictoires

Exemple 3 Cahier

À ce stade, les lecteurs peuvent penser : "Hé ! Ce message n'était pas censé parler d'apprentissage en profondeur !" Mais techniquement, l'introduction fait référence à aller au-delà de "l' entraînement de modèles d'apprentissage en profondeur". Dans ce cas, nous ne formons pas , mais exploitons plutôt certaines propriétés mathématiques d'un réseau de neurones profonds pré-formé pour le tromper en nous donnant les mauvais résultats. Cela s'est avéré beaucoup plus facile et plus efficace qu'on ne l'imaginait. Et tout ce qu'il a fallu, c'est une autre petite goutte de code TensorFlow 2.0.

Nous commençons par trouver un classifieur d'images à attaquer. Nous utiliserons l'une des meilleures solutions pour le concours Dogs vs Cats Kaggle ; plus précisément, la solution présentée par Kaggler "uysimty". Tout leur mérite d'avoir fourni un modèle chat contre chien efficace et d'avoir fourni une excellente documentation. Il s'agit d'un modèle puissant composé de 13 millions de paramètres répartis sur 18 couches de réseaux de neurones. (Les lecteurs sont invités à en savoir plus à ce sujet dans le cahier correspondant.)

Veuillez noter que le but ici n'est pas de mettre en évidence une lacune dans ce réseau particulier, mais de montrer comment tout réseau de neurones standard avec un grand nombre d'entrées est vulnérable.

En relation: Modèles Sound Logic et Monotonic AI

Avec un peu de bricolage, j'ai pu comprendre comment charger le modèle et pré-traiter les images à classer par celui-ci.

Cinq exemples d'images, chacune d'un chien ou d'un chat, avec une classification et un niveau de confiance correspondants. Les niveaux de confiance indiqués vont de 95 % à 100 %.

Cela ressemble à un classificateur vraiment solide ! Toutes les classifications d'échantillons sont correctes et supérieures à 95 % de confiance. Attaquons-le !

Nous voulons produire une image qui est évidemment un chat, mais demander au classificateur de décider qu'il s'agit d'un chien avec une grande confiance. Comment pouvons-nous faire cela?

Commençons par une image de chat qu'il classe correctement, puis découvrons comment de minuscules modifications dans chaque canal de couleur (valeurs 0-255) d'un pixel d'entrée donné affectent la sortie finale du classificateur. La modification d'un pixel ne fera probablement pas grand-chose, mais peut-être que les ajustements cumulatifs de toutes les valeurs de 128x128x3 = 49 152 pixels permettront d'atteindre notre objectif.

Comment savons-nous dans quel sens pousser chaque pixel ? Pendant l'entraînement normal du réseau neuronal, nous essayons de minimiser la perte entre l'étiquette cible et l'étiquette prédite, en utilisant la descente de gradient dans TensorFlow pour mettre à jour simultanément les 13 millions de paramètres libres. Dans ce cas, nous allons plutôt laisser les 13 millions de paramètres fixes et ajuster les valeurs de pixel de l'entrée elle-même.

Quelle est notre fonction de perte ? Eh bien, c'est à quel point l'image ressemble à un chat ! Si nous calculons la dérivée de la valeur du chat par rapport à chaque pixel d'entrée, nous savons dans quelle direction pousser chacun pour minimiser la probabilité de classification du chat.

 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 magie Matplotlib aide à nouveau à visualiser les résultats.

Un échantillon original d'image de chat avec 4 itérations, avec les classifications, "Chat 99,0%", "Chat 67,3%", "Chien 71,7%", "Chien 94,3%" et "Chien 99,4%", respectivement.

Wow! Pour l'œil humain, chacune de ces images est identique. Pourtant, après quatre itérations, nous avons convaincu le classificateur qu'il s'agissait d'un chien, avec une confiance de 99,4 % !

Assurons-nous que ce n'est pas un hasard et que cela fonctionne aussi dans l'autre sens.

Un échantillon original d'image de chien avec 4 itérations, avec les classifications, "Chien 98,4%", "Chien 83,9%", "Chien 54,6%", "Chat 90,4%" et "Chat 99,8%", respectivement. Comme auparavant, les différences sont invisibles à l'œil nu.

Succès! Le classificateur avait initialement prédit cela correctement en tant que chien avec une confiance de 98,4 %, et pense maintenant qu'il s'agit d'un chat avec une confiance de 99,8 %.

Enfin, regardons un exemple de patch d'image et voyons comment il a changé.

Trois grilles de lignes et de colonnes de pixels, affichant des valeurs numériques pour le canal rouge de chaque pixel. Le patch d'image de gauche montre principalement des carrés bleutés, mettant en évidence des valeurs de 218 ou moins, avec quelques carrés rouges (219 et plus) regroupés dans le coin inférieur droit. La page d'image du milieu, "victimisée", montre une disposition colorée et numérotée très similaire. Le patch d'image de droite montre la différence numérique entre les deux autres, avec des différences allant seulement de -4 à +4, et comprenant plusieurs zéros.

Comme prévu, le patch final est très similaire à l'original, chaque pixel ne se déplaçant que de -4 à +4 dans la valeur d'intensité du canal rouge. Ce décalage n'est pas suffisant pour qu'un humain distingue la différence, mais modifie complètement la sortie du classificateur.

Réflexions finales : optimisation de la descente de gradient

Tout au long de cet article, nous avons envisagé d'appliquer manuellement des gradients à nos paramètres entraînables dans un souci de simplicité et de transparence. Cependant, dans le monde réel, les scientifiques des données devraient se lancer directement dans l'utilisation d' optimiseurs , car ils ont tendance à être beaucoup plus efficaces, sans ajouter de gonflement du code.

Il existe de nombreux optimiseurs populaires, notamment RMSprop, Adagrad et Adadelta, mais le plus courant est probablement Adam . Parfois, elles sont appelées « méthodes de taux d'apprentissage adaptatif » car elles maintiennent dynamiquement un taux d'apprentissage différent pour chaque paramètre. Beaucoup d'entre eux utilisent des termes de quantité de mouvement et approximent des dérivés d'ordre supérieur, dans le but d'échapper aux minima locaux et d'obtenir une convergence plus rapide.

Dans une animation empruntée à Sebastian Ruder, on peut voir le parcours de divers optimiseurs descendant une surface de perte. Les techniques manuelles que nous avons démontrées sont les plus comparables à "SGD". L'optimiseur le plus performant ne sera pas le même pour chaque surface de perte ; cependant, les optimiseurs plus avancés fonctionnent généralement mieux que les plus simples.

Une carte de contour animée, montrant le chemin emprunté par six méthodes différentes pour converger vers un point cible. SGD est de loin le plus lent, prenant une courbe régulière à partir de son point de départ. Momentum s'éloigne d'abord de la cible, puis sillonne son propre chemin deux fois avant de se diriger vers elle pas tout à fait directement, et semble la dépasser puis revenir en arrière. NAG est similaire, mais ne s'éloigne pas aussi loin de la cible et ne s'entrecroise qu'une seule fois, atteignant généralement la cible plus rapidement et la dépassant moins. Adagrad commence sur une ligne droite qui est la plus déviée, mais fait très rapidement un virage en épingle à cheveux vers la colline sur laquelle se trouve la cible et se courbe vers elle plus rapidement que les trois premiers. Adadelta a un chemin similaire, mais avec une courbe plus douce ; il dépasse Adagrad et reste devant après la première seconde environ. Enfin, Rmsprop suit un chemin très similaire à Adadelta, mais se penche légèrement plus près de la cible au début ; notamment, son parcours est beaucoup plus régulier, ce qui le place à la traîne par rapport à Adagrad et Adadelta pendant la majeure partie de l'animation; contrairement aux cinq autres, il semble avoir deux sauts soudains et rapides dans deux directions différentes vers la fin de l'animation avant de cesser le mouvement, tandis que les autres, au dernier moment, continuent de ramper lentement le long de la cible.

Cependant, il est rarement utile d'être un expert des optimiseurs, même pour ceux qui souhaitent fournir des services de développement d'intelligence artificielle. C'est une meilleure utilisation du temps des développeurs pour se familiariser avec un couple, juste pour comprendre comment ils améliorent la descente de gradient dans TensorFlow. Après cela, ils peuvent simplement utiliser Adam par défaut et en essayer d'autres uniquement si leurs modèles ne convergent pas.

Pour les lecteurs qui sont vraiment intéressés par comment et pourquoi ces optimiseurs fonctionnent, la vue d'ensemble de Ruder - dans laquelle l'animation apparaît - est l'une des ressources les meilleures et les plus exhaustives sur le sujet.

Mettons à jour notre solution de régression linéaire de la première section pour utiliser des optimiseurs. Ce qui suit est le code de descente de gradient original utilisant des gradients manuels.

 # 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]

Maintenant, voici le même code utilisant un optimiseur à la place. Vous verrez qu'il n'y a pratiquement pas de code supplémentaire (les lignes modifiées sont surlignées en bleu) :

 # 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))

C'est ça! Nous avons défini un optimiseur RMSprop en dehors de la boucle de descente de gradient, puis nous avons utilisé la méthode optimizer.apply_gradients() après chaque calcul de gradient pour mettre à jour les paramètres entraînables. L'optimiseur est défini en dehors de la boucle car il gardera une trace des gradients historiques pour calculer des termes supplémentaires comme le momentum et les dérivés d'ordre supérieur.

Voyons à quoi cela ressemble avec l'optimiseur RMSprop .

Similaire aux paires d'animations synchronisées précédentes ; la ligne ajustée commence au-dessus de son lieu de repos. Le graphique de perte montre qu'il converge presque après seulement cinq itérations.

Ça a l'air génial ! Essayons maintenant avec l'optimiseur Adam .

Un autre diagramme de dispersion synchronisé et une animation de graphique de perte correspondante. Le graphique des pertes se démarque des autres en ce qu'il continue strictement à se rapprocher du minimum ; au lieu de cela, il ressemble au chemin d'une balle qui rebondit. La ligne d'ajustement correspondante sur le nuage de points commence au-dessus des points d'échantillonnage, oscille vers le bas de ceux-ci, puis remonte mais pas aussi haut, et ainsi de suite, chaque changement de direction étant plus proche d'une position centrale.

Waouh, que s'est-il passé ici ? Il semble que la mécanique de l'élan dans Adam l'amène à dépasser la solution optimale et à inverser le cours plusieurs fois. Normalement, ce mécanisme de quantité de mouvement aide avec les surfaces de perte complexes, mais cela nous fait mal dans ce cas simple. Cela met l'accent sur le conseil de faire du choix de l'optimiseur l'un des hyperparamètres à régler lors de l'entraînement de votre modèle.

Quiconque souhaite explorer l'apprentissage en profondeur voudra se familiariser avec ce modèle, car il est largement utilisé dans les architectures TensorFlow personnalisées, où il est nécessaire d'avoir des mécanismes de perte complexes qui ne sont pas facilement intégrés dans le flux de travail standard. Dans cet exemple simple de descente de gradient TensorFlow, il n'y avait que deux paramètres pouvant être entraînés, mais il est nécessaire lorsque vous travaillez avec des architectures contenant des centaines de millions de paramètres à optimiser.

Descente de gradient dans TensorFlow : de la recherche des minimums à l'attaque des systèmes d'IA

Tous les extraits de code et les images ont été produits à partir des blocs-notes du référentiel GitHub correspondant. Il contient également un résumé de toutes les sections, avec des liens vers les cahiers individuels, pour les lecteurs qui souhaitent voir le code complet. Dans un souci de simplification du message, de nombreux détails ont été omis qui peuvent être trouvés dans la documentation en ligne complète.

J'espère que cet article était perspicace et qu'il vous a fait réfléchir à des façons d'utiliser la descente de gradient dans TensorFlow. Même si vous ne l'utilisez pas vous-même, cela clarifie, espérons-le, le fonctionnement de toutes les architectures de réseaux de neurones modernes : créez un modèle, définissez une fonction de perte et utilisez la descente de gradient pour adapter le modèle à votre ensemble de données.


Badge de partenaire Google Cloud.

En tant que partenaire Google Cloud, les experts certifiés Google de Toptal sont à la disposition des entreprises à la demande pour leurs projets les plus importants.