Multe aplicații ale coborârii gradientului în TensorFlow

Publicat: 2022-03-11

TensorFlow de la Google este unul dintre instrumentele de vârf pentru instruirea și implementarea modelelor de învățare profundă. Este capabil să optimizeze arhitecturi de rețele neuronale extrem de complexe cu sute de milioane de parametri și vine cu o gamă largă de instrumente pentru accelerarea hardware, instruire distribuită și fluxuri de lucru de producție. Aceste caracteristici puternice îl pot face să pară intimidant și inutil în afara domeniului învățării profunde.

Dar TensorFlow poate fi atât accesibil, cât și utilizabil pentru probleme mai simple, care nu au legătură directă cu modelele de deep learning. În esență, TensorFlow este doar o bibliotecă optimizată pentru operații cu tensori (vectori, matrice etc.) și operațiunile de calcul utilizate pentru a efectua coborârea gradientului pe secvențe arbitrare de calcule. Oamenii de știință de date cu experiență vor recunoaște „coborârea gradientului” ca un instrument fundamental pentru matematica computațională, dar de obicei necesită implementarea codului și ecuațiilor specifice aplicației. După cum vom vedea, aici intervine arhitectura modernă de „diferențiere automată” a TensorFlow.

Cazuri de utilizare TensorFlow

  • Exemplul 1: Regresie liniară cu coborâre gradient în TensorFlow 2.0
    • Ce este coborârea în gradient?
  • Exemplul 2: Răspândirea maximă a vectorilor unitare
  • Exemplul 3: Generarea de intrări AI adverse
  • Gânduri finale: Optimizarea coborârii în gradient
  • Coborâre gradient în TensorFlow: de la găsirea minimelor până la atacarea sistemelor AI

Exemplul 1: Regresie liniară cu coborâre gradient în TensorFlow 2.0

Exemplul 1 Notebook

Înainte de a ajunge la codul TensorFlow, este important să fii familiarizat cu coborârea gradientului și regresia liniară.

Ce este coborârea în gradient?

În cei mai simpli termeni, este o tehnică numerică pentru găsirea intrărilor într-un sistem de ecuații care minimizează rezultatul acestuia. În contextul învățării automate, acel sistem de ecuații este modelul nostru, intrările sunt parametrii necunoscuți ai modelului, iar rezultatul este o funcție de pierdere care trebuie redusă la minimum, care reprezintă cât de multă eroare există între model și datele noastre. Pentru unele probleme (cum ar fi regresia liniară), există ecuații pentru a calcula direct parametrii care minimizează eroarea noastră, dar pentru majoritatea aplicațiilor practice, avem nevoie de tehnici numerice precum coborârea gradientului pentru a ajunge la o soluție satisfăcătoare.

Cel mai important punct al acestui articol este că coborârea gradientului necesită de obicei stabilirea ecuațiilor noastre și utilizarea calculului pentru a deriva relația dintre funcția noastră de pierdere și parametrii noștri. Cu TensorFlow (și orice instrument modern de diferențiere automată), calculul este gestionat pentru noi, astfel încât să ne putem concentra pe proiectarea soluției și să nu fim nevoiți să petrecem timp implementării acesteia.

Iată cum arată într-o problemă simplă de regresie liniară. Avem un eșantion de înălțimi (h) și greutăți (w) a 150 de masculi adulți și începem cu o estimare imperfectă a pantei și a abaterii standard a acestei linii. După aproximativ 15 iterații de coborâre în gradient, ajungem la o soluție aproape optimă.

Două animații sincronizate. Partea stângă arată o diagramă de dispersie înălțime-greutate, cu o linie adaptată care începe departe de date, apoi se deplasează rapid spre ea, încetinind înainte de a găsi potrivirea finală. Mărimea potrivită arată un grafic de pierdere versus iterație, fiecare cadru adăugând o nouă iterație graficului. Pierderea începe deasupra vârfului graficului la 2.000, dar se apropie rapid de linia pierderii minime în câteva iterații în ceea ce pare a fi o curbă logaritmică.

Să vedem cum am produs soluția de mai sus folosind TensorFlow 2.0.

Pentru regresia liniară, spunem că greutățile pot fi prezise printr-o ecuație liniară a înălțimii.

w-indice-i,pred este egal cu produsul punctual alfa h-indice-i plus beta.

Dorim să găsim parametrii α și β (pantă și intersecție) care să minimizeze eroarea pătrată medie (pierderea) dintre predicții și valorile adevărate. Deci, funcția noastră de pierdere (în acest caz, „eroare pătratică medie” sau MSE) arată astfel:

MSE este egal cu unu peste N ori suma de la i este egală cu unu la N din pătratul diferenței dintre w-subscript-i,true și w-subscript-i,pred.

Putem vedea cum arată eroarea medie pătratică pentru câteva linii imperfecte și apoi cu soluția exactă (α=6,04, β=-230,5).

Trei copii ale aceluiași diagramă de dispersie înălțime-greutate, fiecare cu o linie montată diferită. Primul are w = 4,00 * h + -120,0 și o pierdere de 1057,0; linia este sub date și mai puțin abruptă decât aceasta. Al doilea are w = 2,00 * h + 70,0 și o pierdere de 720,8; linia este aproape de partea superioară a punctelor de date și chiar mai puțin abruptă. Tertul are w = 60,4 * h + -230,5 și o pierdere de 127,1; linia trece prin punctele de date astfel încât acestea să apară grupate uniform în jurul ei.

Să punem această idee în acțiune cu TensorFlow. Primul lucru de făcut este să codificați funcția de pierdere folosind tensori și funcții 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

Acest lucru pare destul de simplu. Toți operatorii algebrici standard sunt supraîncărcați pentru tensori, așa că trebuie doar să ne asigurăm că variabilele pe care le optimizăm sunt tensoare și folosim metodele tf.* pentru orice altceva.

Apoi, tot ce trebuie să facem este să punem asta într-o buclă de coborâre în 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]

Să luăm un moment pentru a aprecia cât de îngrijit este. Coborârea gradientului necesită calcularea derivatelor funcției de pierdere în raport cu toate variabilele pe care încercăm să le optimizăm. Ar trebui să fie implicat calculul, dar noi nu am făcut nimic din toate acestea. Magia constă în faptul că:

  1. TensorFlow construiește un grafic de calcul al fiecărui calcul efectuat sub un tf.GradientTape() .
  2. TensorFlow știe cum să calculeze derivatele (gradienții) fiecărei operații, astfel încât să poată determina modul în care orice variabilă din graficul de calcul afectează orice altă variabilă.

Cum arată procesul din diferite puncte de plecare?

Aceleași grafice sincronizate ca înainte, dar și sincronizate cu o pereche similară de grafice de sub ele pentru comparație. Graficul pierderii-iterație al perechii inferioare este similar, dar pare să convergă mai repede; linia sa corespunzătoare începe de deasupra punctelor de date, mai degrabă decât dedesubt, și mai aproape de locul său de odihnă final.

Coborârea în gradient se apropie remarcabil de MSE optimă, dar de fapt converge către o pantă și o intersecție substanțial diferite de cea optimă în ambele exemple. În unele cazuri, aceasta este pur și simplu o coborâre în gradient care converge la minim local, ceea ce este o provocare inerentă cu algoritmii de coborâre a gradientului. Dar regresia liniară are, probabil, doar un minim global. Deci cum am ajuns pe panta greșită și am interceptat?

În acest caz, problema este că am simplificat prea mult codul de dragul demonstrației. Nu ne-am normalizat datele, iar parametrul de pantă are o caracteristică diferită de parametrul de interceptare. Modificările minuscule ale pantei pot produce modificări masive ale pierderilor, în timp ce modificările minuscule ale interceptării au un efect foarte mic. Această diferență uriașă de scară a parametrilor antrenabili duce la dominarea pantei calculelor de gradient, parametrul de interceptare fiind aproape ignorat.

Deci, coborârea în gradient găsește în mod eficient cea mai bună pantă foarte aproape de estimarea inițială a interceptării. Și, deoarece eroarea este atât de aproape de optim, gradienții din jurul ei sunt mici, astfel încât fiecare iterație succesivă se mișcă doar puțin. Normalizarea mai întâi a datelor ar fi îmbunătățit dramatic acest fenomen, dar nu l-ar fi eliminat.

Acesta a fost un exemplu relativ simplu, dar vom vedea în secțiunile următoare că această capacitate de „diferențiere automată” poate face față unor chestii destul de complexe.

Exemplul 2: Răspândirea maximă a vectorilor unitare

Exemplul 2 Notebook

Acest exemplu următor se bazează pe un exercițiu distractiv de învățare profundă într-un curs de învățare profundă pe care l-am urmat anul trecut.

Esența problemei este că avem un „variational auto-encoder” (VAE) care poate produce fețe realiste dintr-un set de 32 de numere distribuite normal. Pentru identificarea suspectului, dorim să folosim VAE pentru a produce un set divers de fețe (teoretice) pentru ca un martor să aleagă, apoi să restrângem căutarea producând mai multe fețe similare cu cele alese. Pentru acest exercițiu, s-a sugerat randomizarea setului inițial de vectori, dar am vrut să găsesc o stare inițială optimă.

Putem formula problema astfel: Având în vedere un spațiu cu 32 de dimensiuni, găsiți un set de X vectori unitari care sunt împrăștiați maxim. În două dimensiuni, acest lucru este ușor de calculat exact. Dar pentru trei dimensiuni (sau 32 de dimensiuni!), nu există un răspuns simplu. Cu toate acestea, dacă putem defini o funcție de pierdere adecvată, care este la minimum atunci când ne-am atins starea țintă, poate că coborârea în gradient ne poate ajuta să ajungem acolo.

Două grafice. Graficul din stânga, Stare inițială pentru toate experimentele, are un punct central conectat la alte puncte, aproape toate formând un semicerc în jurul său; un punct se află aproximativ opus semicercului. Graficul din dreapta, Stare țintă, este ca o roată, cu spițele răspândite uniform.

Vom începe cu un set randomizat de 20 de vectori așa cum se arată mai sus și vom experimenta cu trei funcții de pierdere diferite, fiecare cu o complexitate crescândă, pentru a demonstra capacitățile TensorFlow.

Să definim mai întâi bucla noastră de antrenament. Vom pune toată logica TensorFlow sub metoda self.calc_loss() și apoi putem pur și simplu să suprascriem acea metodă pentru fiecare tehnică, reciclând această buclă.

 # 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

Prima tehnică de încercat este cea mai simplă. Definim o metrică de răspândire care este unghiul vectorilor care sunt cel mai apropiați. Vrem să maximizăm răspândirea, dar este convențional să facem din aceasta o problemă de minimizare. Deci luăm pur și simplu negativul valorii spread-ului:

 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

Unele magie Matplotlib vor produce o vizualizare.

O animație care trece de la starea inițială la starea țintă. Punctul singur rămâne fix, iar restul spițelor din semicerc tremură pe rând înainte și înapoi, răspândindu-se încet și neatingând echidistanța chiar și după 1.200 de iterații.

Acest lucru este ciudat (la propriu!), dar funcționează. Doar doi dintre cei 20 de vectori sunt actualizați simultan, mărind spațiul dintre ei până când nu mai sunt cei mai apropiați, apoi trecând la creșterea unghiului dintre noii doi vectori cei mai apropiați. Important de observat este că funcționează . Vedem că TensorFlow a reușit să treacă gradienți prin metoda tf.reduce_min() și prin metoda tf.acos() pentru a face ceea ce trebuie.

Să încercăm ceva un pic mai elaborat. Știm că la soluția optimă, toți vectorii ar trebui să aibă același unghi față de vecinii lor cei mai apropiați. Deci, să adăugăm „varianța unghiurilor minime” la funcția de pierdere.

 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 

O animație care trece de la starea inițială la starea țintă. Spița singură nu rămâne fixă, mișcându-se rapid spre restul spițelor din semicerc; în loc să închidă două goluri de fiecare parte a spiței singure, tremurul acum închide un gol mare în timp. Echidistanța nu este, de asemenea, atinsă aici după 1.200 de iterații.

Acest vector singuratic spre nord se alătură acum rapid egalilor săi, deoarece unghiul față de cel mai apropiat vecin este uriaș și crește termenul de varianță care este acum redus la minimum. Dar este încă condus în cele din urmă de unghiul minim global, care rămâne lent să crească. Ideile pe care le am pentru a îmbunătăți acest lucru, în general, funcționează în acest caz 2D, dar nu în dimensiuni mai mari.

Dar concentrarea prea mult pe calitatea acestei încercări matematice este ratată. Uitați-vă la câte operații tensoare sunt implicate în calculele de medie și varianță și cum TensorFlow urmărește și diferențiază cu succes fiecare calcul pentru fiecare componentă din matricea de intrare. Și nu a trebuit să facem niciun calcul manual. Am pus niște calcule simple împreună, iar TensorFlow a făcut calculul pentru noi.

În sfârșit, să mai încercăm un lucru: o soluție bazată pe forță. Imaginează-ți că fiecare vector este o planetă mică legată de un punct central. Fiecare planetă emite o forță care o respinge de pe celelalte planete. Dacă ar fi să rulăm o simulare fizică a acestui model, ar trebui să ajungem la soluția dorită.

Ipoteza mea este că și coborârea în gradient ar trebui să funcționeze. La soluția optimă, forța tangentă de pe fiecare planetă de pe orice altă planetă ar trebui să se anuleze la o forță netă zero (dacă nu ar fi zero, planetele s-ar mișca). Deci, să calculăm mărimea forței pe fiecare vector și să folosim coborârea gradientului pentru a-l împinge spre zero.

Mai întâi, trebuie să definim metoda care calculează forța folosind metodele 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

Apoi, definim funcția noastră de pierdere folosind funcția de forță de mai sus. Acumulăm forța netă asupra fiecărui vector și calculăm mărimea acestuia. La soluția noastră optimă, toate forțele ar trebui să se anuleze și ar trebui să avem forță zero.

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

O animație care trece de la starea inițială la starea țintă. Primele cadre văd mișcare rapidă în toate spițele și, după doar 200 de iterații, imaginea de ansamblu este deja destul de aproape de țintă. Doar 700 de iterații sunt afișate în total; după 300, unghiurile se schimbă doar minut cu fiecare cadru.

Nu numai că soluția funcționează frumos (pe lângă ceva haos în primele cadre), dar meritul real revine TensorFlow. Această soluție a implicat mai multe bucle for , o declarație if și o rețea uriașă de calcule, iar TensorFlow a urmărit cu succes gradienți prin toate acestea pentru noi.

Exemplul 3: Generarea de intrări AI adverse

Exemplul 3 Notebook

În acest moment, cititorii se pot gândi: „Hei! Această postare nu trebuia să fie despre învățarea profundă!” Dar din punct de vedere tehnic, introducerea se referă la depășirea „ formarii modelelor de învățare profundă”. În acest caz, nu ne antrenăm , ci exploatăm unele proprietăți matematice ale unei rețele neuronale profunde pre-antrenate pentru a o păcăli să ne dea rezultate greșite. Acest lucru s-a dovedit a fi mult mai ușor și mai eficient decât ne-am imaginat. Și tot ce a fost nevoie a fost încă o scurtă pată de cod TensorFlow 2.0.

Începem prin a găsi un clasificator de imagini pe care să îl atacăm. Vom folosi una dintre soluțiile de top pentru Concursul Dogs vs Cats Kaggle; mai exact, soluția prezentată de Kaggler „uysimty”. Tot meritul lor pentru că au furnizat un model eficient pisică-vs-câine și au furnizat documentație excelentă. Acesta este un model puternic format din 13 milioane de parametri în 18 straturi de rețea neuronală. (Cititorii sunt bineveniți să citească mai multe despre aceasta în caietul corespunzător.)

Vă rugăm să rețineți că scopul aici nu este de a evidenția vreo deficiență în această rețea particulară, ci de a arăta cum este vulnerabilă orice rețea neuronală standard cu un număr mare de intrări.

Înrudit: logica sunetului și modelele AI monotone

Cu puțină reparație, am reușit să-mi dau seama cum să încarc modelul și să preprocesez imaginile pentru a fi clasificate după el.

Cinci imagini eșantion, fiecare cu un câine sau o pisică, cu o clasificare și un nivel de încredere corespunzător. Nivelurile de încredere afișate variază de la 95% la 100%.

Acesta pare un clasificator foarte solid! Toate clasificările eșantioanelor sunt corecte și au o încredere peste 95%. Să-l atacăm!

Dorim să producem o imagine care este în mod evident o pisică, dar ca clasificatorul să decidă că este un câine cu mare încredere. Cum putem face asta?

Să începem cu o imagine a pisicii pe care o clasifică corect, apoi să ne dăm seama cum modificările mici în fiecare canal de culoare (valori 0-255) ale unui pixel de intrare dat afectează rezultatul final al clasificatorului. Modificarea unui pixel probabil nu va face mare lucru, dar poate că modificările cumulate ale tuturor valorilor 128x128x3 = 49.152 pixeli ne vor atinge obiectivul.

Cum știm în ce mod să împingem fiecare pixel? În timpul antrenamentului normal al rețelei neuronale, încercăm să minimizăm pierderea dintre eticheta țintă și eticheta prezisă, folosind coborârea gradientului în TensorFlow pentru a actualiza simultan toate cele 13 milioane de parametri liberi. În acest caz, vom lăsa fix cei 13 milioane de parametri și vom ajusta valorile pixelilor intrării în sine.

Care este funcția noastră de pierdere? Ei bine, cât de mult arată imaginea cu o pisică! Dacă calculăm derivata valorii pisicii în raport cu fiecare pixel de intrare, știm în ce mod să împingem fiecare pentru a minimiza probabilitatea de clasificare a pisicii.

 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

Magia Matplotlib ajută din nou la vizualizarea rezultatelor.

Un eșantion de imagine original de pisică împreună cu 4 iterații, cu clasificări, „Pisică 99,0%”, „Pisică 67,3%”, „Câine 71,7%”, „Câine 94,3%” și, respectiv, „Câine 99,4%”.

Wow! Pentru ochiul uman, fiecare dintre aceste imagini este identică. Cu toate acestea, după patru iterații, l-am convins pe clasificator că este un câine, cu o încredere de 99,4%!

Să ne asigurăm că nu este o întâmplare și că funcționează și în cealaltă direcție.

O imagine originală a câinelui, împreună cu 4 iterații, cu clasificări, „Câine 98,4%”, „Câine 83,9%”, „Câine 54,6%”, „Pisică 90,4%” și, respectiv, „Pisică 99,8%”. Ca și înainte, diferențele sunt invizibile cu ochiul liber.

Succes! Clasificatorul a prezis inițial acest lucru corect ca un câine cu 98,4% încredere, iar acum crede că este o pisică cu 99,8% încredere.

În cele din urmă, să ne uităm la un eșantion de patch de imagine și să vedem cum s-a schimbat.

Trei grile de rânduri și coloane de pixeli, care arată valorile numerice pentru canalul roșu al fiecărui pixel. Patch-ul din stânga imaginii prezintă în mare parte pătrate albăstrui, evidențiind valori de 218 sau mai jos, cu unele pătrate roșii (219 și mai sus) grupate în colțul din dreapta jos. Pagina de imagine din mijloc, „victimizată”, arată un aspect foarte asemănător colorat și numerotat. Patch-ul de imagine din dreapta arată diferența numerică dintre celelalte două, cu diferențe variind doar de la -4 la +4 și incluzând câteva zerouri.

După cum era de așteptat, patch-ul final este foarte asemănător cu originalul, fiecare pixel deplasându-se doar de la -4 la +4 în valoarea intensității canalului roșu. Această schimbare nu este suficientă pentru ca un om să distingă diferența, dar schimbă complet rezultatul clasificatorului.

Gânduri finale: Optimizarea coborârii în gradient

Pe parcursul acestui articol, ne-am uitat la aplicarea manuală a gradienților parametrilor noștri antrenabili de dragul simplității și transparenței. Cu toate acestea, în lumea reală, oamenii de știință de date ar trebui să treacă direct la utilizarea optimizatorilor , deoarece acestea tind să fie mult mai eficiente, fără a adăuga niciun cod de umflare.

Există multe optimizatoare populare, inclusiv RMSprop, Adagrad și Adadelta, dar cel mai comun este probabil Adam . Uneori, ele sunt numite „metode adaptative ale ratei de învățare” deoarece mențin în mod dinamic o rată de învățare diferită pentru fiecare parametru. Mulți dintre ei folosesc termeni de impuls și aproximează derivate de ordin superior, cu scopul de a scăpa de minimele locale și de a obține o convergență mai rapidă.

Într-o animație împrumutată de la Sebastian Ruder, putem vedea calea diferiților optimizatori coborând o suprafață de pierdere. Tehnicile manuale pe care le-am demonstrat sunt cel mai comparabile cu „SGD”. Optimizatorul cu cele mai bune performanțe nu va fi același pentru fiecare suprafață de pierdere; cu toate acestea, optimizatorii mai avansati au de obicei rezultate mai bune decât cele mai simple.

O hartă de contur animată, care arată calea urmată de șase metode diferite pentru a converge către un punct țintă. SGD este de departe cel mai lent, luând o curbă constantă de la punctul său de pornire. Momentum se îndepărtează inițial de țintă, apoi își încrucișează propriul drum de două ori înainte de a se îndrepta spre ea nu în totalitate direct, părând că o depășește și apoi da înapoi. NAG este similar, dar nu se îndepărtează atât de departe de țintă și se încrucișează o singură dată, în general atingând ținta mai repede și depășindu-o mai puțin. Adagrad pornește într-o linie dreaptă, care este cea mai îndepărtată de curs, dar se întoarce foarte repede în ac de păr spre dealul pe care se află ținta și se curbează spre el mai repede decât primele trei. Adadelta are o cale similară, dar cu o curbă mai netedă; îl depășește pe Adagrad și rămâne înaintea lui după prima secundă și ceva. În cele din urmă, Rmsprop urmează o cale foarte asemănătoare cu Adadelta, dar se înclină puțin mai aproape de țintă devreme; în special, cursul său este mult mai stabil, făcându-l să rămână în urmă cu Adagrad și Adadelta pentru cea mai mare parte a animației; spre deosebire de celelalte cinci, pare să aibă două sărituri bruște și rapide în două direcții diferite aproape de sfârșitul animației înainte de a înceta mișcarea, în timp ce ceilalți, în ultimul moment, continuă să se strecoare încet pe lângă țintă.

Cu toate acestea, este rareori util să fii un expert în optimizatori, chiar și pentru cei dornici să ofere servicii de dezvoltare a inteligenței artificiale. Este o utilizare mai bună a timpului dezvoltatorilor pentru a se familiariza cu un cuplu, doar pentru a înțelege cum îmbunătățesc coborârea gradientului în TensorFlow. După aceea, pot folosi Adam în mod implicit și pot încerca altele diferite numai dacă modelele lor nu converg.

Pentru cititorii care sunt cu adevărat interesați de cum și de ce funcționează aceste optimizatoare, prezentarea generală a lui Ruder, în care apare animația, este una dintre cele mai bune și mai exhaustive resurse pe această temă.

Să actualizăm soluția noastră de regresie liniară din prima secțiune pentru a folosi optimizatori. Următorul este codul original de coborâre a gradientului folosind gradienți manuali.

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

Acum, aici este același cod folosind un optimizator. Veți vedea că nu este deloc cod suplimentar (liniile modificate sunt evidențiate cu albastru):

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

Asta e! Am definit un optimizator RMSprop în afara buclei de coborâre a gradientului și apoi am folosit metoda optimizer.apply_gradients() după fiecare calcul de gradient pentru a actualiza parametrii antrenabili. Optimizatorul este definit în afara buclei, deoarece va ține evidența gradienților istorici pentru calcularea termenilor suplimentari, cum ar fi impulsul și derivatele de ordin superior.

Să vedem cum arată cu optimizatorul RMSprop .

Similar cu perechile de animații sincronizate anterioare; linia montată începe deasupra locului său de odihnă. Graficul pierderii arată că aproape converge după doar cinci iterații.

Arata bine! Acum haideți să încercăm cu optimizatorul Adam .

Un alt grafic de dispersie sincronizat și animație corespunzătoare graficului de pierderi. Graficul pierderilor iese în evidență de celelalte prin faptul că nu continuă strict să se apropie de minim; în schimb, seamănă cu calea unei mingi care sări. Linia potrivită corespunzătoare pe diagrama de dispersie începe deasupra punctelor eșantionului, se balansează spre partea de jos a acestora, apoi înapoi, dar nu la fel de sus, și așa mai departe, fiecare schimbare de direcție fiind mai aproape de o poziție centrală.

Ce sa întâmplat aici? Se pare că mecanica impulsului din Adam îl face să depășească soluția optimă și să inverseze cursul de mai multe ori. În mod normal, această mecanică a impulsului ajută la suprafețele complexe de pierdere, dar ne doare în acest caz simplu. Acest lucru subliniază sfatul de a face din alegerea optimizatorului unul dintre hiperparametrii de reglat atunci când vă antrenezați modelul.

Oricine dorește să exploreze învățarea profundă va dori să se familiarizeze cu acest model, deoarece este utilizat pe scară largă în arhitecturile personalizate TensorFlow, unde este nevoie de mecanisme complexe de pierdere care nu sunt ușor de înglobat în fluxul de lucru standard. În acest exemplu simplu de coborâre a gradientului TensorFlow, au existat doar doi parametri antrenabili, dar este necesar atunci când lucrați cu arhitecturi care conțin sute de milioane de parametri pentru optimizare.

Coborâre gradient în TensorFlow: de la găsirea minimelor până la atacarea sistemelor AI

Toate fragmentele de cod și imaginile au fost produse din notebook-uri în depozitul GitHub corespunzător. Conține și un rezumat al tuturor secțiunilor, cu link-uri către caietele individuale, pentru cititorii care doresc să vadă codul complet. De dragul simplificării mesajului, au fost omise o mulțime de detalii care pot fi găsite în documentația extinsă inline.

Sper că acest articol a fost perspicace și te-a făcut să te gândești la modalități de a folosi coborârea gradientului în TensorFlow. Chiar dacă nu îl utilizați singur, sperăm că va face mai clar cum funcționează toate arhitecturile moderne de rețele neuronale - creați un model, definiți o funcție de pierdere și utilizați coborârea gradientului pentru a se potrivi modelului la setul dvs. de date.


Insigna Google Cloud Partner.

În calitate de partener Google Cloud, experții Toptal certificați de Google sunt disponibili companiilor la cerere pentru cele mai importante proiecte ale acestora.