Wiele zastosowań opadania gradientu w TensorFlow

Opublikowany: 2022-03-11

TensorFlow firmy Google jest jednym z wiodących narzędzi do szkolenia i wdrażania modeli uczenia głębokiego. Jest w stanie zoptymalizować szalenie złożone architektury sieci neuronowych z setkami milionów parametrów i jest wyposażony w szeroką gamę narzędzi do przyspieszania sprzętowego, szkolenia rozproszonego i przepływów pracy produkcyjnej. Te potężne funkcje mogą sprawiać wrażenie onieśmielających i niepotrzebnych poza domeną głębokiego uczenia się.

Jednak TensorFlow może być zarówno dostępny, jak i użyteczny w przypadku prostszych problemów niezwiązanych bezpośrednio z trenowaniem modeli uczenia głębokiego. W swej istocie TensorFlow jest po prostu zoptymalizowaną biblioteką dla operacji tensorowych (wektory, macierze itp.) oraz operacji rachunku różniczkowego używanych do wykonywania gradientów na dowolnych sekwencjach obliczeń. Doświadczeni naukowcy zajmujący się danymi rozpoznają „opadanie gradientowe” jako podstawowe narzędzie matematyki obliczeniowej, ale zwykle wymaga to zaimplementowania kodu i równań specyficznych dla aplikacji. Jak zobaczymy, tutaj właśnie wkracza nowoczesna architektura „automatycznego różnicowania” TensorFlow.

Przypadki użycia TensorFlow

  • Przykład 1: Regresja liniowa ze spadkiem gradientu w TensorFlow 2.0
    • Co to jest zejście gradientowe?
  • Przykład 2: Maksymalnie rozłożone wektory jednostkowe
  • Przykład 3: Generowanie przeciwstawnych danych wejściowych AI
  • Końcowe myśli: optymalizacja zjazdu gradientowego
  • Gradientowe opadanie w TensorFlow: od znajdowania minimów do atakowania systemów AI

Przykład 1: Regresja liniowa ze spadkiem gradientu w TensorFlow 2.0

Przykład 1 Notatnik

Przed przejściem do kodu TensorFlow ważne jest, aby zapoznać się z opadaniem gradientowym i regresją liniową.

Co to jest zejście gradientowe?

Mówiąc najprościej, jest to numeryczna technika znajdowania danych wejściowych do układu równań, które minimalizują jego wynik. W kontekście uczenia maszynowego, ten układ równań to nasz model , dane wejściowe to nieznane parametry modelu, a dane wyjściowe to funkcja straty , którą należy zminimalizować, która reprezentuje, jak duży jest błąd między modelem a naszymi danymi. W przypadku niektórych problemów (takich jak regresja liniowa) istnieją równania umożliwiające bezpośrednie obliczenie parametrów, które minimalizują nasz błąd, ale w przypadku większości praktycznych zastosowań, aby uzyskać zadowalające rozwiązanie, potrzebujemy technik numerycznych, takich jak opadanie gradientowe.

Najważniejszym punktem tego artykułu jest to, że opadanie gradientu zwykle wymaga ułożenia naszych równań i użycia rachunku różniczkowego, aby wyprowadzić związek między naszą funkcją straty a naszymi parametrami. Dzięki TensorFlow (i dowolnym nowoczesnym narzędziu do autoróżnicowania) rachunek jest obsługiwany za nas, dzięki czemu możemy skupić się na projektowaniu rozwiązania, a nie tracić czasu na jego wdrożenie.

Oto jak wygląda prosty problem regresji liniowej. Mamy próbkę wzrostu (h) i wagi (w) 150 dorosłych mężczyzn i zaczynamy od niedokładnego odgadnięcia nachylenia i odchylenia standardowego tej linii. Po około 15 iteracjach opadania gradientu dochodzimy do prawie optymalnego rozwiązania.

Dwie zsynchronizowane animacje. Lewa strona przedstawia wykres rozrzutu wysokość-waga, z dopasowaną linią, która zaczyna się daleko od danych, a następnie szybko do niej zbliża się, spowalniając, zanim znajdzie ostateczne dopasowanie. Właściwy rozmiar przedstawia wykres straty w funkcji iteracji, przy czym każda klatka dodaje nową iterację do wykresu. Strata zaczyna się powyżej górnej części wykresu przy 2000, ale szybko zbliża się do linii minimalnej straty w ciągu kilku iteracji na czymś, co wydaje się być krzywą logarytmiczną.

Zobaczmy, jak wyprodukowaliśmy powyższe rozwiązanie przy użyciu TensorFlow 2.0.

W przypadku regresji liniowej mówimy, że wagi można przewidzieć za pomocą liniowego równania wysokości.

w-subscript-i,pred równa się iloczynowi kropki alfa h-subscript-i plus beta.

Chcemy znaleźć parametry α i β (nachylenie i przecięcie), które minimalizują średni kwadratowy błąd (stratę) między przewidywaniami a wartościami rzeczywistymi. Zatem nasza funkcja straty (w tym przypadku „błąd średniokwadratowy” lub MSE) wygląda tak:

MSE równa się jeden przez N razy suma od i równa się jeden do N kwadratu różnicy między w-subscript-i,true i w-subscript-i,pred.

Możemy zobaczyć, jak wygląda błąd średniokwadratowy dla kilku niedoskonałych linii, a następnie z dokładnym rozwiązaniem (α=6,04, β=-230,5).

Trzy kopie tego samego wykresu rozrzutu wysokość-masa, każda z inną dopasowaną linią. Pierwszy ma w = 4,00 * h + -120,0 i stratę 1057,0; linia znajduje się poniżej danych i jest mniej stroma niż ona. Drugi ma w = 2,00 * h + 70,0 i stratę 720,8; linia znajduje się w pobliżu górnej części punktów danych i jest jeszcze mniej stroma. Hird ma w = 60,4 * h + -230,5 i stratę 127,1; linia przechodzi przez punkty danych tak, że wydają się równomiernie skupione wokół niej.

Zrealizujmy ten pomysł z TensorFlow. Pierwszą rzeczą do zrobienia jest zakodowanie funkcji straty przy użyciu tensorów i funkcji 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

To wygląda całkiem prosto. Wszystkie standardowe operatory algebraiczne są przeciążone dla tensorów, więc musimy tylko upewnić się, że zmienne, które optymalizujemy, są tensorami, a do czegokolwiek innego używamy metod tf.* .

Następnie wszystko, co musimy zrobić, to umieścić to w pętli opadania gradientu:

 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]

Poświęćmy chwilę, aby docenić, jakie to jest schludne. Spadek gradientu wymaga obliczenia pochodnych funkcji straty w odniesieniu do wszystkich zmiennych, które próbujemy zoptymalizować. Rachunek powinien być w to zaangażowany, ale tak naprawdę nic z tego nie zrobiliśmy. Magia polega na tym, że:

  1. TensorFlow buduje wykres obliczeniowy każdego obliczenia wykonanego w ramach tf.GradientTape() .
  2. TensorFlow wie, jak obliczyć pochodne (gradienty) każdej operacji, dzięki czemu może określić, w jaki sposób dowolna zmienna na wykresie obliczeń wpływa na dowolną inną zmienną.

Jak wygląda proces z różnych punktów wyjścia?

Te same zsynchronizowane wykresy co poprzednio, ale także zsynchronizowane z podobną parą wykresów pod nimi dla porównania. Wykres iteracji strat niższej pary jest podobny, ale wydaje się, że zbiega się szybciej; odpowiadająca mu dopasowana linia zaczyna się powyżej punktów danych, a nie poniżej i bliżej miejsca ostatecznego spoczynku.

Opadanie gradientowe zbliża się do optymalnego MSE, ale w rzeczywistości zbiega się do znacznie innego nachylenia i przecięcia niż optimum w obu przykładach. W niektórych przypadkach jest to po prostu opadanie gradientowe zbieżne do lokalnego minimum, co jest nieodłącznym wyzwaniem w przypadku algorytmów opadania gradientowego. Ale regresja liniowa ma tylko jedno globalne minimum. Jak więc skończyliśmy na złym nachyleniu i przechwyciliśmy?

W tym przypadku problem polega na tym, że uprościliśmy kod w celach demonstracyjnych. Nie znormalizowaliśmy naszych danych, a parametr nachylenia ma inną charakterystykę niż parametr przecięcia. Niewielkie zmiany nachylenia mogą wywołać ogromne zmiany w stratach, podczas gdy niewielkie zmiany przecięcia mają bardzo mały wpływ. Ta ogromna różnica w skali trenowalnych parametrów prowadzi do tego, że nachylenie dominuje w obliczeniach gradientu, a parametr przecięcia jest prawie ignorowany.

Dzięki temu zniżanie gradientowe skutecznie wyszukuje najlepsze nachylenie bardzo bliskie początkowej przewidywanej wartości przecięcia. A ponieważ błąd jest tak bliski optimum, gradienty wokół niego są bardzo małe, więc każda kolejna iteracja przesuwa się tylko odrobinę. Normalizacja naszych danych w pierwszej kolejności radykalnie poprawiłaby to zjawisko, ale nie wyeliminowałaby go.

Był to stosunkowo prosty przykład, ale w następnych sekcjach zobaczymy, że ta funkcja „automatycznego różnicowania” może poradzić sobie z dość złożonymi sprawami.

Przykład 2: Maksymalnie rozłożone wektory jednostkowe

Przykład 2 Notatnik

Kolejny przykład jest oparty na zabawnym ćwiczeniu z głębokiego uczenia się w kursie głębokiego uczenia się, w którym uczestniczyłem w zeszłym roku.

Istota problemu polega na tym, że mamy „wariacyjny autokoder” (VAE), który może generować realistyczne twarze z zestawu 32 liczb o normalnym rozkładzie. W celu identyfikacji podejrzanego chcemy użyć VAE do stworzenia zróżnicowanego zestawu (teoretycznych) twarzy świadka do wyboru, a następnie zawęzić wyszukiwanie, tworząc więcej twarzy podobnych do wybranych. W tym ćwiczeniu zasugerowano randomizację początkowego zestawu wektorów, ale chciałem znaleźć optymalny stan początkowy.

Możemy sformułować problem w ten sposób: Mając 32-wymiarową przestrzeń, znajdź zbiór X wektorów jednostkowych, które są maksymalnie rozłożone. W dwóch wymiarach łatwo to dokładnie obliczyć. Ale dla trzech wymiarów (lub 32 wymiarów!) nie ma jednoznacznej odpowiedzi. Jeśli jednak uda nam się zdefiniować prawidłową funkcję straty, która jest na poziomie minimum, gdy osiągnęliśmy nasz docelowy stan, być może gradient może nam pomóc w osiągnięciu tego celu.

Dwa wykresy. Lewy wykres, Stan początkowy dla wszystkich eksperymentów, ma centralny punkt połączony z innymi punktami, z których prawie wszystkie tworzą półokrąg wokół niego; jeden punkt stoi mniej więcej naprzeciw półokręgu. Prawy wykres, Stan docelowy, jest jak koło z równomiernie rozłożonymi szprychami.

Zaczniemy od losowego zestawu 20 wektorów, jak pokazano powyżej, i poeksperymentujemy z trzema różnymi funkcjami straty, z których każda ma coraz większą złożoność, aby zademonstrować możliwości TensorFlow.

Najpierw zdefiniujmy naszą pętlę treningową. Umieścimy całą logikę TensorFlow pod self.calc_loss() , a następnie możemy po prostu nadpisać tę metodę dla każdej techniki, przetwarzając tę ​​pętlę.

 # 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

Pierwsza technika do wypróbowania jest najprostsza. Definiujemy metrykę rozproszenia, która jest kątem wektorów, które są najbliżej siebie. Chcemy zmaksymalizować rozprzestrzenianie się, ale konwencjonalnie sprawiamy, że jest to problem minimalizacji. Więc po prostu bierzemy ujemną wartość wskaźnika spreadu:

 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

Niektóre magia Matplotlib dadzą wizualizację.

Animacja przechodząca od stanu początkowego do stanu docelowego. Pojedynczy punkt pozostaje nieruchomy, a pozostałe szprychy w półokręgu na zmianę drgają w przód iw tył, powoli się rozsuwając i nie osiągając równej odległości nawet po 1200 iteracjach.

To jest niezgrabne (dosłownie!), ale działa. Tylko dwa z 20 wektorów są aktualizowane na raz, powiększając odstęp między nimi, aż przestaną być najbliższe, a następnie przełączając się na zwiększanie kąta między nowymi dwoma najbliższymi wektorami. Należy zauważyć, że to działa . Widzimy, że TensorFlow był w stanie przekazać gradienty za pomocą metody tf.reduce_min() i tf.acos() , aby zrobić to, co należy.

Spróbujmy czegoś bardziej skomplikowanego. Wiemy, że w optymalnym rozwiązaniu wszystkie wektory powinny mieć ten sam kąt do swoich najbliższych sąsiadów. Dodajmy więc „odchylenie minimalnych kątów” do funkcji straty.

 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 

Animacja przechodząca od stanu początkowego do stanu docelowego. Samotna szprycha nie pozostaje nieruchoma, szybko porusza się w kierunku pozostałych szprych w półokręgu; zamiast zamykać dwie luki po obu stronach samotnej szprychy, drganie z czasem zamyka jedną dużą lukę. Równowaga jest tutaj również nie do końca osiągnięta po 1200 iteracjach.

Ten samotny wektor skierowany na północ teraz szybko łączy się ze swoimi rówieśnikami, ponieważ kąt do najbliższego sąsiada jest ogromny i zaostrza składnik wariancji, który jest teraz minimalizowany. Ale ostatecznie jest to ostatecznie napędzane przez globalny minimalny kąt, który powoli rośnie. Pomysły, które muszę ulepszyć to generalnie działają w tym przypadku 2D, ale nie w większych wymiarach.

Ale zbytnie skupianie się na jakości tej matematycznej próby nie ma sensu. Zobacz, ile operacji tensorowych jest zaangażowanych w obliczenia średniej i wariancji oraz jak TensorFlow z powodzeniem śledzi i różnicuje każde obliczenia dla każdego składnika macierzy wejściowej. I nie musieliśmy robić żadnego ręcznego rachunku. Po prostu połączyliśmy trochę prostej matematyki, a TensorFlow zrobił za nas rachunek.

Na koniec spróbujmy jeszcze jednej rzeczy: rozwiązania opartego na sile. Wyobraź sobie, że każdy wektor to mała planeta przywiązana do centralnego punktu. Każda planeta emituje siłę, która odpycha ją od innych planet. Gdybyśmy mieli przeprowadzić symulację fizyki tego modelu, powinniśmy otrzymać pożądane rozwiązanie.

Moja hipoteza jest taka, że ​​zejście gradientowe też powinno działać. W optymalnym rozwiązaniu siła styczna na każdej planecie z każdej innej planety powinna znosić się do zerowej siły netto (gdyby nie była zerem, planety poruszałyby się). Obliczmy więc wielkość siły na każdym wektorze i użyjmy spadku gradientu, aby przesunąć go w kierunku zera.

Najpierw musimy zdefiniować metodę obliczania siły za pomocą metod 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

Następnie definiujemy naszą funkcję straty za pomocą funkcji siły powyżej. Akumulujemy siłę wypadkową na każdym wektorze i obliczamy jej wielkość. Przy naszym optymalnym rozwiązaniu wszystkie siły powinny znosić się i powinniśmy mieć siłę zerową.

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

Animacja przechodząca od stanu początkowego do stanu docelowego. W pierwszych kilku klatkach widać szybki ruch we wszystkich szprychach, a po około 200 iteracjach ogólny obraz jest już dość blisko celu. W sumie pokazano tylko 700 iteracji; po 300 kąty zmieniają się tylko minimalnie z każdą klatką.

Rozwiązanie nie tylko działa pięknie (poza pewnym chaosem w pierwszych kilku klatkach), ale prawdziwa zasługa należy do TensorFlow. To rozwiązanie obejmowało wiele pętli for , instrukcję if i ogromną sieć obliczeń, a TensorFlow z powodzeniem prześledził dla nas gradienty.

Przykład 3: Generowanie przeciwstawnych danych wejściowych AI

Przykład 3 Notatnik

W tym momencie czytelnicy mogą pomyśleć: „Hej! Ten post nie miał dotyczyć głębokiego uczenia się!” Ale technicznie wprowadzenie odnosi się do wyjścia poza „ trening modeli głębokiego uczenia”. W tym przypadku nie trenujemy , ale zamiast tego wykorzystujemy pewne matematyczne właściwości wstępnie wytrenowanej głębokiej sieci neuronowej, aby oszukać ją, aby dała nam złe wyniki. Okazało się to znacznie prostsze i skuteczniejsze niż przypuszczano. Wystarczył kolejny krótki blob kodu TensorFlow 2.0.

Zaczynamy od znalezienia klasyfikatora obrazu do ataku. Wykorzystamy jedno z najlepszych rozwiązań w konkursie Kaggle Dogs vs. Cats; konkretnie rozwiązanie zaprezentowane przez Kagglera „uysimty”. Wszyscy zasługują na to, że dostarczyli skuteczny model kot kontra pies i dostarczyli świetną dokumentację. Jest to potężny model składający się z 13 milionów parametrów w 18 warstwach sieci neuronowej. (Czytelnicy mogą przeczytać więcej na ten temat w odpowiednim zeszycie.)

Proszę zauważyć, że celem nie jest podkreślenie jakichkolwiek niedociągnięć w tej konkretnej sieci, ale pokazanie, jak podatna jest każda standardowa sieć neuronowa z dużą liczbą wejść.

Powiązane: Modele logiki dźwiękowej i monotonicznej sztucznej inteligencji

Przy odrobinie majsterkowania udało mi się wymyślić, jak załadować model i wstępnie przetworzyć obrazy, które mają zostać przez niego sklasyfikowane.

Pięć przykładowych obrazów, każdego psa lub kota, z odpowiednią klasyfikacją i poziomem ufności. Pokazane poziomy ufności wahają się od 95 do 100 procent.

To wygląda na naprawdę solidny klasyfikator! Wszystkie klasyfikacje próbek są prawidłowe i mają ufność powyżej 95%. Zaatakujmy to!

Chcemy stworzyć obraz, który jest oczywiście kotem, ale niech klasyfikator zdecyduje, że jest to pies z dużą pewnością siebie. Jak możemy to zrobić?

Zacznijmy od obrazu kota, który jest poprawnie klasyfikowany, a następnie zastanówmy się, jak drobne modyfikacje w każdym kanale koloru (wartości 0-255) danego piksela wejściowego wpływają na końcowy wynik klasyfikatora. Modyfikacja jednego piksela prawdopodobnie nie da wiele, ale być może skumulowane poprawki wszystkich wartości 128x128x3 = 49 152 pikseli osiągną nasz cel.

Skąd wiemy, w jaki sposób wypchnąć każdy piksel? Podczas normalnego treningu sieci neuronowych staramy się zminimalizować utratę między etykietą docelową a etykietą przewidywaną, używając gradientu w TensorFlow do jednoczesnej aktualizacji wszystkich 13 milionów wolnych parametrów. W tym przypadku zamiast tego zostawimy 13 milionów parametrów ustalonych i dostosujemy wartości pikseli samego wejścia.

Jaka jest nasza funkcja straty? Cóż, tak bardzo obraz wygląda jak kot! Jeśli obliczymy pochodną wartości kota w odniesieniu do każdego piksela wejściowego, wiemy, w jaki sposób popchnąć każdy z nich, aby zminimalizować prawdopodobieństwo klasyfikacji kota.

 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 ponownie pomaga zwizualizować wyniki.

Oryginalny przykładowy obraz kota wraz z 4 iteracjami, z klasyfikacjami odpowiednio „Kot 99,0%”, „Kot 67,3%”, „Pies 71,7%”, „Pies 94,3%” i „Pies 99,4%”.

Wow! Dla ludzkiego oka każdy z tych obrazów jest identyczny. Jednak po czterech iteracjach przekonaliśmy klasyfikatora, że ​​to pies, z 99,4% pewnością!

Upewnijmy się, że to nie jest przypadek i działa też w drugą stronę.

Oryginalny przykładowy obraz psa wraz z 4 iteracjami, z klasyfikacjami odpowiednio „Pies 98,4%”, „Pies 83,9%”, „Pies 54,6%”, „Kot 90,4%” i „Kot 99,8%”. Tak jak poprzednio, różnice są niewidoczne gołym okiem.

Sukces! Klasyfikator pierwotnie przewidział to poprawnie jako pies z 98,4 procentową pewnością, a teraz uważa, że ​​jest to kot z 99,8 procentową pewnością.

Na koniec spójrzmy na przykładową łatkę obrazu i zobaczmy, jak się zmieniła.

Trzy siatki rzędów i kolumn pikseli, przedstawiające wartości liczbowe dla czerwonego kanału każdego piksela. Obszar po lewej stronie przedstawia głównie niebieskawe kwadraty, podkreślając wartości 218 lub niższe, z kilkoma czerwonymi kwadratami (219 i więcej) skupionymi w prawym dolnym rogu. Środkowa strona z obrazem „ofiarowanym” przedstawia układ o bardzo podobnym kolorze i numerach. Plaster obrazu po prawej stronie pokazuje różnicę liczbową między pozostałymi dwoma, przy czym różnice wahają się tylko od -4 do +4 i zawierają kilka zer.

Zgodnie z oczekiwaniami, ostateczna łata jest bardzo podobna do oryginału, a każdy piksel przesuwa się tylko z -4 do +4 w wartości intensywności kanału czerwonego. Ta zmiana nie wystarczy, aby człowiek mógł odróżnić różnicę, ale całkowicie zmienia wyjście klasyfikatora.

Końcowe myśli: optymalizacja zjazdu gradientowego

W tym artykule przyjrzeliśmy się ręcznym stosowaniu gradientów do naszych parametrów, które można trenować, ze względu na prostotę i przejrzystość. Jednak w prawdziwym świecie naukowcy zajmujący się danymi powinni od razu zacząć korzystać z optymalizatorów , ponieważ są one zwykle znacznie bardziej efektywne bez dodawania rozrostu kodu.

Istnieje wiele popularnych optymalizatorów, w tym RMSprop, Adagrad i Adadelta, ale najpopularniejszym jest prawdopodobnie Adam . Czasami nazywa się je „metodami adaptacyjnego tempa uczenia się”, ponieważ dynamicznie utrzymują różne tempo uczenia się dla każdego parametru. Wiele z nich używa terminów momentum i przybliża pochodne wyższego rzędu w celu uniknięcia lokalnych minimów i osiągnięcia szybszej zbieżności.

W animacji zapożyczonej od Sebastiana Rudera widzimy ścieżkę różnych optymalizatorów schodzących po powierzchni strat. Zademonstrowane przez nas techniki manualne są najbardziej porównywalne do „SGD”. Najskuteczniejszy optymalizator nie będzie taki sam dla każdej powierzchni strat; jednak bardziej zaawansowane optymalizatory zazwyczaj działają lepiej niż te prostsze.

Animowana mapa konturowa, pokazująca ścieżkę przebytą sześcioma różnymi metodami w celu zbieżności w punkcie docelowym. SGD jest zdecydowanie najwolniejszy, biorąc stabilny krzywą od punktu początkowego. Pęd początkowo oddala się od celu, a następnie dwukrotnie przecina własną ścieżkę, po czym kieruje się w jego stronę nie całkowicie bezpośrednio i wydaje się, że go przestrzeliwuje, a następnie cofa się. NAG jest podobny, ale nie oddala się aż tak daleko od celu i krzyżuje się tylko raz, generalnie osiągając cel szybciej i mniej go przestrzeliwując. Adagrad zaczyna się w linii prostej, która jest najbardziej zboczona z kursu, ale bardzo szybko skręca w kierunku wzgórza, na którym znajduje się cel, i skręca w jego kierunku szybciej niż pierwsze trzy. Adadelta ma podobną ścieżkę, ale z gładszą krzywą; wyprzedza Adagrad i wyprzedza go po mniej więcej pierwszej sekundzie. Wreszcie, Rmsprop podąża bardzo podobną ścieżką do Adadelty, ale na początku pochyla się nieco bliżej celu; w szczególności jego kurs jest znacznie bardziej stabilny, przez co pozostaje w tyle za Adagradem i Adadeltą przez większość animacji; w przeciwieństwie do pozostałych pięciu, wydaje się, że ma dwa nagłe, szybkie skoki w dwóch różnych kierunkach pod koniec animacji, zanim przestaną się poruszać, podczas gdy pozostali, w ostatniej chwili, powoli pełzają obok celu.

Jednak bycie ekspertem od optymalizatorów rzadko jest przydatne — nawet dla tych, którzy chcą świadczyć usługi rozwoju sztucznej inteligencji. Lepiej jest wykorzystać czas programistów na zapoznanie się z parą, tylko po to, aby zrozumieć, jak poprawiają one gradient w TensorFlow. Następnie mogą domyślnie używać Adama i wypróbowywać różne tylko wtedy, gdy ich modele nie są zbieżne.

Dla czytelników, którzy są naprawdę zainteresowani tym, jak i dlaczego działają te optymalizatory, przegląd Rudera — w którym pojawia się animacja — jest jednym z najlepszych i najbardziej wyczerpujących zasobów na ten temat.

Zaktualizujmy nasze rozwiązanie regresji liniowej z pierwszej sekcji, aby korzystać z optymalizatorów. Poniżej znajduje się oryginalny kod spadku gradientu przy użyciu gradientów ręcznych.

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

Oto ten sam kod, który używa zamiast tego optymalizatora. Zobaczysz, że to prawie żaden dodatkowy kod (zmienione linie są podświetlone na niebiesko):

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

Otóż ​​to! Zdefiniowaliśmy optymalizator RMSprop poza pętlą opadania gradientu, a następnie po każdym obliczeniu gradientu użyliśmy metody optimizer.apply_gradients() w celu zaktualizowania parametrów, które można trenować. Optymalizator jest zdefiniowany poza pętlą, ponieważ będzie śledził historyczne gradienty w celu obliczenia dodatkowych warunków, takich jak pęd i pochodne wyższego rzędu.

Zobaczmy, jak to wygląda z optymalizatorem RMSprop .

Podobny do poprzednich zsynchronizowanych par animacji; dopasowana linia zaczyna się powyżej miejsca spoczynku. Wykres strat pokazuje prawie zbieżność po zaledwie pięciu iteracjach.

Wygląda świetnie! Teraz spróbujmy z optymalizatorem Adama .

Kolejny zsynchronizowany wykres rozrzutu i odpowiadająca mu animacja wykresu strat. Wykres strat wyróżnia się na tle innych tym, że nie zbliża się ściśle do minimum; zamiast tego przypomina ścieżkę odbijającej się piłki. Odpowiednia dopasowana linia na wykresie rozrzutu zaczyna się powyżej punktów próbkowania, odchyla się w kierunku ich dołu, a następnie cofa się, ale nie tak wysoko, i tak dalej, przy czym każda zmiana kierunku zbliża się do położenia centralnego.

Whoa, co się tutaj stało? Wygląda na to, że mechanika pędu u Adama powoduje, że wielokrotnie przeskakuje on nad optymalne rozwiązanie i odwraca kurs. Zwykle ta mechanika pędu pomaga w przypadku złożonych powierzchni strat, ale w tym prostym przypadku szkodzi nam. Podkreśla to radę, aby wybór optymalizatora był jednym z hiperparametrów do dostrojenia podczas uczenia modelu.

Każdy, kto chce zbadać głębokie uczenie, będzie chciał zapoznać się z tym wzorcem, ponieważ jest on szeroko stosowany w niestandardowych architekturach TensorFlow, gdzie istnieje potrzeba posiadania złożonej mechaniki strat, która nie jest łatwa do włączenia w standardowy przepływ pracy. W tym prostym przykładzie ze spadkiem gradientu TensorFlow były tylko dwa parametry, które można trenować, ale jest to konieczne podczas pracy z architekturami zawierającymi setki milionów parametrów do optymalizacji.

Gradientowe opadanie w TensorFlow: od znajdowania minimów do atakowania systemów AI

Wszystkie fragmenty kodu i obrazy zostały utworzone z notebooków w odpowiednim repozytorium GitHub. Zawiera również podsumowanie wszystkich sekcji, z linkami do poszczególnych notatników, dla czytelników, którzy chcą zobaczyć pełny kod. W celu uproszczenia przekazu pominięto wiele szczegółów, które można znaleźć w obszernej dokumentacji wbudowanej.

Mam nadzieję, że ten artykuł był wnikliwy i skłonił Cię do zastanowienia się nad sposobami wykorzystania gradientu w TensorFlow. Nawet jeśli nie używasz go samodzielnie, miejmy nadzieję, że wyjaśni to, jak działają wszystkie nowoczesne architektury sieci neuronowych — utwórz model, zdefiniuj funkcję straty i użyj gradientu, aby dopasować model do zestawu danych.


Status Partnera Google Cloud.

Jako Partner Google Cloud, certyfikowani przez Google eksperci Toptal są dostępni dla firm na żądanie w ich najważniejszych projektach.