Zastosowanie architektury Nvidia CUDA do przyspieszania procesu
Transkrypt
Zastosowanie architektury Nvidia CUDA do przyspieszania procesu
Politechnika Warszawska Wydział Elektroniki i Technik Informacyjnych Instytut Informatyki Rok akademicki 2012/2013 PRACA DYPLOMOWA INŻYNIERSKA Adam Dąbrowski Zastosowanie architektury Nvidia CUDA do przyspieszania procesu uczenia sieci neuronowych Opiekun pracy: mgr inż. Zbigniew Szymański Ocena: ..................................................... ................................................................ Podpis Przewodniczącego Komisji Egzaminu Dyplomowego Kierunek: Informatyka Specjalność: Inżynieria Systemów Informatycznych Data urodzenia: 1989.01.03 Data rozpoczęcia studiów: 2009.02.23 Życiorys Nazywam się Adam Dąbrowski. Urodziłem się 3 stycznia w 1989 roku w Warszawie. W latach 1996-2002 uczyłem się w Szkole Podstawowej nr 3 w Markach. Następnie w latach 2002-2005 w Zespole Szkół nr 1 im. Jana Pawła II. Naukę kontynuowałem w 50 Liceum Ogólnokształcącym im. Ruy Barbosy w Warszawie. Po zdaniu egzaminów maturalnych rozpocząłem studia dzienne I stopnia na wydziale Elektroniki i Technik Informacyjnych Politechniki Warszawskiej. Po dwóch latach wybrałem specjalność Inżynieria Systemów Informatycznych w Instytucie Informatyki, gdzie obecnie kończę pracę dyplomową. ....................................................... Podpis studenta EGZAMIN DYPLOMOWY Złożył egzamin dyplomowy w dniu .................................................................................. 20__ r z wynikiem .................................................................................................................................. Ogólny wynik studiów: ............................................................................................................... Dodatkowe wnioski i uwagi Komisji: ......................................................................................... ...................................................................................................................................................... ...................................................................................................................................................... STRESZCZENIE Praca przedstawia porównanie czasu uczenia sieci neuronowych przy użyciu klasycznego procesora oraz technologii Nvidia CUDA. W tym celu zaimplementowana została biblioteka umożliwiająca uczenie sieci posiadających jedną warstwą ukrytą. Zastosowane zostały przy tym algorytmy gradientów sprzężonych oraz algorytm Levenberga-Marquardta. Testowany był czas uczenia w zależności od złożoności architektury sieci. Dodatkowo zweryfikowana została zbieżność obu algorytmów. Przeprowadzone testy wykazały, że przyspieszenie zależne jest od złożoności sieci oraz wielkości zbioru uczącego. Wykorzystanie GPU pozwala uzyskać przyspieszenie jedynie w przypadku problemów o dużej złożoności. Zredukowanie czasu minimalizacji algorytmem Levenberga-Marquardta sprawia, że jego złożoność obliczeniowa nie stanowi tak dużego problemu jak do tej pory. Słowa kluczowe: Sieci neuronowe, Nvidia CUDA, Levenberg-Marquardt, Metoda gradientu sprzężonego, uczenie. APPLICATION OF NVIDIA CUDA ARCHITECTURE TO ACCELERATE TRAINING PROCESS OF NEURAL NETWORK This thesis presents comparison between training time of neural network while using classic processor and Nvidia CUDA software model. In order to achieve this goal, the library which enable training process of network with one hidden layer have been implemented. The library use conjugate gradient method and Levenberg-Marquardt algorithm. Dependency between time and complexity of network architecture have been tested. In addition, convergence of both algorithm have been verified. Tests indicated that speedup is related to complexity of network and problem size. Application of GPU generate speedup only in case of extensive problem. Reduction of time, during minimization by Levenberg-Marquardt algorithm, make this method more applicable to complex network. Keywords: Neural network, Nvidia CUDA, Levenberg-Marquardt, Conjugate gradient method, training. Spis treści 1 Opis problemu. 1.1 Cel pracy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 2 Opis architektury CUDA. 2.1 Zarys historyczny. . . . . . . . . . . . . . . . . 2.2 Budowa GPU . . . . . . . . . . . . . . . . . . 2.3 Model przetwarzania danych . . . . . . . . . . 2.4 Skalowalność . . . . . . . . . . . . . . . . . . 2.5 Model pamięci . . . . . . . . . . . . . . . . . . 2.5.1 Rejestry + pamięć lokalna . . . . . . . 2.5.2 Pamięć współdzielona . . . . . . . . . 2.5.3 Pamięć stała . . . . . . . . . . . . . . . 2.5.4 Pamięć globalna . . . . . . . . . . . . . 2.6 Struktura programu wykorzystującego CUDA 5 5 5 6 6 7 8 9 9 9 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Opis zastosowanych algorytmów. 11 3.1 Algorytm gradientów sprzężonych. . . . . . . . . . . . . . . . . . . . 11 3.2 Algorytm Levenberga-Marquardta. . . . . . . . . . . . . . . . . . . 12 3.3 Dobór współczynnika uczenia. . . . . . . . . . . . . . . . . . . . . . 13 4 Implementacja. 4.1 Klasa Solver . . . . . . . . . . . . . . . . 4.1.1 Ustawienia konfiguracyjne . . . . 4.2 Klasa Network . . . . . . . . . . . . . . . 4.3 Klasa Examples . . . . . . . . . . . . . . 4.4 Klasy SolverGPU i SolverCPU . . . . . . 4.4.1 Obliczanie sygnałów wyjściowych 4.4.2 Obliczanie gradientu . . . . . . . 4.5 Klasy ConjugateGradient CPU i GPU . 4.6 Klasy LevenbergMarquardt CPU i GPU 4.6.1 Budowa algorytmu . . . . . . . . 4.6.2 Obliczanie aproksymacji hesjanu . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 15 16 18 18 19 19 20 25 26 26 27 4.7 Przykładowe użycie biblioteki . . . . . . . . . . . . . . . . . . . . . 5 Wyniki 5.1 Porównanie czasów. . . . . . . . . . . . . 5.1.1 Porównanie czasów dla algorytmu 5.1.2 Porównanie czasów dla algorytmu 5.2 Porównanie działania algorytmów . . . . . . . . . . . . . . . . . . gradientów sprzężonych. Levenberga Marquardta. . . . . . . . . . . . . . . . . . . 33 35 36 37 40 42 6 Zakończenie 44 7 Załączniki 46 2 1 Opis problemu. Pomimo wielu zalet sieci neuronowych sporo osób podchodzi do tego tematu z dużym dystansem. Rozwiązania oparte o sieci neuronowe są postrzegane jako trudne i wymagające zarówno dobrej znajomości tematyki jak i dużego doświadczenia. Jedną z przyczyn takiego poglądu jest trudny proces uczenia sieci. Dobór odpowiednich wartości wag wiąże się z minimalizacją funkcji błędu. Funkcja ta jest zależna od dużej liczby skorelowanych zmiennych. Charakteryzuje się obfitością fałszywych minimów lokalnych, w które wpadają algorytmy uczące. Drugim poważnym problemem jest dobór odpowiedniej architektury sieci. Zbyt duża liczba neuronów spowoduje, że sieć będzie nadmiernie dopasowywać się do danych uczących. To z kolei prowadzi od zatracania zdolności uogólniania zdobytej wiedzy - jednej z kluczowych cech sieci neuronowych. Poza tym model o niepotrzebnie dużej złożoności będzie podczas pracy wymagał zbędnych obliczeń. Spowoduje to, że uzyskana sieć będzie działała wolniej niż sieć o architekturze zbliżonej do optymalnej. Niestety, nie jest znany uniwersalny sposób pozwalający określić optymalną liczbę neuronów. Można stosować dwa podejścia. W pierwszym zakłada się wstępną liczbę neuronów ukrytych oszacowaną na podstawie rozmiarów problemu. Na przykład, średnia geometryczna liczby wejść i wyjść sieci. W trakcie uczenia redukuje się złożoność sieci. Drugie podejście działa dokładnie na odwrót. Na początku przyjmuje się zbyt małą liczbę, na przykład 0. Następnie zwiększa się złożoność do momentu, aż sieć będzie dawała zadowalające rezultaty. W obu podejściach konieczne jest wielokrotne przeprowadzenie procesu uczenia dla sieci o różnych złożonościach. Ponieważ funkcja błędu cechuje się dużą liczbą minimów lokalnych, nie można jednoznacznie stwierdzić czy dany model ma za mało neuronów, czy jedynie algorytm utknął w jednym z minimów. Powoduje to, że uczenie dla każdej liczby neuronów ukrytych trzeba powtarzać wielokrotnie. O ile w przypadku niedużych sieci i małych zbiorów uczących nie jest to zbyt kłopotliwe, to przy problemach o dużej złożoności może zajmować spore ilości czasu. 3 1.1 Cel pracy. Rozwój komputerów oraz coraz szybsze i obecnie posiadające coraz więcej rdzeni procesory sprawiają, że dobór architektury sieci do problemu staje się coraz mniej czasochłonny. Jedną z najdynamiczniej rozwijających się gałęzi są procesory graficzne. Technologia CUDA (Compute Unified Device Architecture), wprowadzona przez firmę Nvida, umożliwia wykorzystanie GPU do obliczeń niezwiązanych z grafiką. Procesory te przystosowane są do równoległego przetwarzania dużych ilości danych, na których wykonuje się te same operacje. Dzięki równoległemu przetwarzaniu dostępna moc obliczeniowa jest znacznie większa, niż w przypadku procesorów ogólnego przeznaczenia. Celem niniejszej pracy jest zaimplementowanie biblioteki umożliwiającej zbadanie, jakie przyspieszenie można uzyskać dzięki zastosowaniu architektury Nvidia CUDA do obliczeń związanych z sieciami neuronowymi. Zaimplementowana biblioteka umożliwia przeprowadzenie procesu uczenia przy wykorzystaniu CPU oraz GPU algorytmem gradientów sprzężonych lub Levenberga-Marquardta. 4 2 2.1 Opis architektury CUDA. Zarys historyczny. Już od roku 2003 podejmowano próby zastosowania kart graficznych do obliczeń niezwiązanych z grafiką. W tym celu wykorzystywano wysokopoziomowe języki programowania, takie jak DirectX czy OpenGL. Wymagało to od programistów bardzo dobrej znajomości zastosowanego języka oraz architektury GPU. Wykonywane obliczenia musiały być implementowane w taki sposób, aby pasowały do modelu przetwarzania wierzchołków lub cieniowania pikseli. Powodowało to znaczne skomplikowanie tworzonych aplikacji. Dodatkowo brak wsparcia losowego dostępu do pamięci znacznie ograniczał możliwości zastosowań GPU. Pomimo licznych trudności, w aplikacjach, w których zastosowano GPU, osiągnięto znaczne wzrosty wydajności względem CPU [1]. Wprowadzona w 2006 roku architektura G80 rozwiązała wiele wspomnianych trudności. Zastosowano w niej ujednolicone jednostki przetwarzające, które, w zależności od potrzeb, przetwarzają wierzchołki lub wyliczają kolory pikseli. Dzięki temu karty graficzne zaczęły nadawać się do obliczeń ogólnego przeznaczenia, charakteryzujących się dużą równoległością. Równocześnie wprowadzono też technologię CUDA (Compute Unified Device Architecture), która umożliwiła programowanie GPU w rozmaitych, wysoko poziomowych językach [1]. Wyeliminowało to konieczność uczenia się nowego języka programowania. 2.2 Budowa GPU Główne różnice między CPU a GPU pojawiają się już w samej budowie rdzeni. Procesor karty graficznej przeznaczony jest do renderowania grafiki. Jest to intensywny obliczeniowo proces, charakteryzujący się tym, że te same operacje wykonywane są dla dużej liczby danych. Z tego powodu w GPU znacznie więcej tranzystorów zostało przeznaczonych do przetwarzania danych kosztem pamięci podręcznych oraz kontroli wykonywanych instrukcji. Zostało to przedstawione na rysunku 2.1. 5 Rysunek 2.1: Schemat przedstawiający różnicę w budowie CPU i GPU [4]. Procesor karty graficznej zbudowany jest w oparciu o procesory strumieniowe Streaming Multiprocessors (SMs). Każdy procesor posiada własną jednostkę sterującą, rejestry i pamięć podręczną. SM składają się z 32 (lub w nowszych kartach z 48) CUDA Core, które odpowiadają za przetwarzanie danych [4]. 2.3 Model przetwarzania danych Przyjęty model równoległego przetwarzania zbioru danych zakłada tworzenie wielkich ilości równolegle wykonujących się wątków. Każdy wątek odpowiada za obliczenia związane z pojedynczym elementem danych. Wątki tworzą jedno, dwu, lub trzy wymiarowe bloki. Umożliwia to w wygodny sposób przetwarzanie takich struktur jak wektory, macierze, lub inne trójwymiarowe zbiory danych. Wątki podzielone są na grupy i tworzą w ten sposób bloki wątków. W obecnie wykorzystywanych kartach graficznych jeden blok może zawierać do 1024 wątków. Bloki wątków zorganizowane są w jedno, dwu, lub trzy wymiarową strukturę nazywaną grid. Ilość bloków w gridzie wynika zazwyczaj z rozmiaru rozwiązywanego problemu. Grid jest reprezentowany w programie w postaci Kernela. 2.4 Skalowalność W zależności od zastosowanej karty graficznej GPU może posiadać różną liczbą SM. Ponieważ wykonujący się kernel składa się z gridu podzielonego na bloki, możliwe jest dopasowanie się programu do architektury zastosowanego urządzenia. Podczas wykonywania obliczeń poszczególne bloki są przydzielane do SM i wykonywane niezależnie. Przydział bloków do jednostek przetwarzających w przypadku dwóch 6 różnych rdzeni przedstawia rysunek 2.2. Różnica w działaniu polega jedynie na tym, że karty posiadające większą liczbę rdzeni szybciej zakończą wykonywanie kernela. Wynika z tego, że rozwiązywany problem musi być podzielony w taki sposób, aby bloki były od siebie niezależne oraz aby kolejność ich wykonania nie miała wpływu na wynik końcowy. Rysunek 2.2: Automatyczne przyporządkowanie bloków do SM na GPU o różnej liczbie rdzeni [4]. 2.5 Model pamięci W architekturze CUDA można wyszczególnić cztery typy pamięci. Różnią się one od siebie rozmiarami, czasami dostępu oraz możliwościami dostępu. W zależności od potrzeb programista sam musi określić, w jakiej pamięci będzie przechowywał określone dane. Model pamięci został schematycznie przedstawiony na rys. 2.3. Kolorem czerwonym została zaznaczona pamięć znajdująca się w rdzeniu procesora, natomiast żółtym w pamięci DRAM karty graficznej. W kolejnych podpunktach omówiono właściwości każdego typu pamięci. 7 Rysunek 2.3: Schemat przedstawiający model pamięci w architekturze CUDA [2]. 2.5.1 Rejestry + pamięć lokalna Podobnie jak w CPU, rejestry są najszybszym rodzajem pamięci i stanowią pamięć prywatną dla każdego wątku. W rejestrach umieszczane są zmienne lokalne poszczególnych wątków oraz parametry wejściowe. Rejestry stanowią najmniejszy rodzaj pamięci. Jeżeli dane nie mieszczą się w rejestrach, zostaną umieszczone w pamięci lokalnej. Pamięć lokalna, tak jak rejestry, jest prywatna dla każdego wątku. Nie jest jednak tak ograniczona rozmiarami. Jest znacznie wolniejsza, niż rejestry. Spowodowane jest to tym, że znajduje się w pamięci DRAM1 karty graficznej, a nie w rdzeniu GPU. 1 Dynamic Random Access Memory - Półprzewodnikowa pamięć o dostępie swobodnym. 8 2.5.2 Pamięć współdzielona Pamięć współdzielona (Shared Memory) stanowi najszybszą drogę komunikacji pomiędzy wątkami wchodzącymi w skład jednego bloku. Czas dostępu do tej pamięci jest podobny do czasu dostępu do rejestrów. Pamięć współdzielona jest wykorzystywania do zmniejszania opóźnień wynikających z dostępu do pamięci globalnej. Jeżeli wiele wątków potrzebuje tych samych danych, to mogą one być najpierw pobrane do pamięci współdzielonej. Wykorzystanie pamięci współdzielonej bardziej szczegółowo zostało omówione w materiałach[4] oraz[3]. 2.5.3 Pamięć stała Pamięć stała jest szybsza niż pamięć globalna, jednak wolniejsza niż rejestry i pamięć współdzielona. Do tej pamięci dane zapisywać można tylko z poziomu CPU. Program wykonywany przez GPU ma jedynie możliwość odczytu. 2.5.4 Pamięć globalna Jest to największa, ale też najwolniejsza pamięć. Dostęp do niej mają wątki ze wszystkich bloków. Stanowi też jedyną drogę komunikacji pomiędzy wątkami należącymi do różnych bloków. 2.6 Struktura programu wykorzystującego CUDA Program wykorzystujący technologię CUDA składa się z części sekwencyjnych oraz równoległych. Fragmenty sekwencyjne napisane są w sposób klasyczny, tzn. właściwy dla stosowanego języka programowania. W miejscach, w których możliwe jest zrównoleglenie instrukcji za pomocą GPU, umieszczane są kernele. Kernele przypominają nieco zwykłe funkcje, jednak wykonywane są przez kartę graficzną. Napisane są przy użyciu rozszerzenia języka C. Podczas wywoływania kernela, oprócz zwykłych parametrów wejściowych, konieczne jest podanie rozmiaru bloków oraz gridu. Ponieważ wątki składające się na kernel nie maja dostępu do pamięci DRAM znajdującej się na płycie głównej, przed uruchomieniem kernela konieczne jest przekopiowanie danych do pamięci karty graficznej. 9 Dodatkowo poszczególne wywołania kerneli stanowią mechanizm synchronizacyjny pomiędzy wątkami w poszczególnych blokach. Na rysunku 2.4 przedstawiono schemat wykonującego się programu. Rysunek 2.4: Schemat przedstawiający działanie programu wykorzystującego GPU [2]. 10 3 Opis zastosowanych algorytmów. W celu realizacji procesu uczenia wykorzystane zostały dwa popularne algorytmy uczące. Algorytm gradientów sprzężonych oraz algorytm Levenberga-Marquardta. Oba algorytmy zostały zaimplementowane na dwa sposoby: pierwszy wykorzystujący jedynie CPU, drugi wykorzystujący CPU + GPU. 3.1 Algorytm gradientów sprzężonych. Algorytm gradientów sprzężonych, dzięki małym wymaganiom co do pamięci oraz małej złożoności obliczeniowej, jest stosowany do rozwiązywania zadań o dużej liczbie zmiennych. Jego zbieżność zbliżona jest do liniowej. W metodzie tej zrezygnowano z informacji o hesjanie. Zamiast tego kierunki poszukiwania minimum są konstruowane w taki sposób, aby były ortogonalne oraz sprzężone ze wszystkimi poprzednimi kierunkami. Jak podano w [5], założenie to spełnia następujące równanie: pt = gt (W) + t−1 X βt pj (3.1) t=0 Nowy kierunek poszukiwań wyznaczany jest na podstawie wektora gradientu w aktualnym punkcie rozwiązania oraz sumy wszystkich poprzednich kierunków. W algorytmie gradientów sprzężonych stosuje się uproszczoną wersję równania (3.1): pt = gt + βt−1 pt−1 (3.2) Suma wszystkich poprzednich kierunków poszukiwań została zastąpiona poprzednim kierunkiem pomnożonym przez współczynnik sprzężenia βt−1 [5]. Istnieje wiele reguł wyznaczania współczynnika β. Dwie najpopularniejsze to [6]: Metoda Flethera-Reevesa: βt−1 = gtT (gt − gt−1 ) −pt−1 gt−1 (3.3) βt−1 = gtT (gt − gt−1 ) T −gt−1 gt−1 (3.4) Metoda Polaka-Ribiere’a 11 W niniejszej pracy została zastosowana metoda Polaka-Ribiere’a. Ze względu na błędy zaokrągleń algorytm podczas kolejnych iteracji zatraca własność ortogonalności pomiędzy kierunkami poszukiwań. Z tego powodu co n przebiegów następuje restart. Jako kierunek minimalizacji przyjmuje się wówczas kierunek zgodny z algorytmem największego spadku. 3.2 Algorytm Levenberga-Marquardta. Algorytm Levenberga-Marquardta jest jednym z najczęściej wykorzystywanych algorytmów w optymalizacji nieliniowej, a co za tym idzie również w procesie uczenia sieci neuronowych. Łączy ze sobą cechy metody największego spadku oraz GaussaNewtona [5]. Algorytm ten oparty jest na newtonwowskiej metodzie optymalizacji, różni się jednak sposobem obliczania hesjanu. Hesjan został zastąpiony wartością aproksymowaną, określaną na podstawie gradientu oraz czynnika regularyzującego. Wadą tej metody jest kosztowna operacja odwracania hesjanu. W metodzie Levenberga-Marquardta przyjęto następującą postać funkcji celu dla N elementowego zbioru uczącego i sieci z K neuronami wyjściowymi: E(W ) = K X [ek (W)]2 (3.5) k=1 gdzie enk = [yk (W) − dk ] jest błędem k-tego wyjścia sieci dla n-tego przykładu. Jeżeli oznaczymy jakobian jako: ∂e ∂W11 J(W) = .. . ∂eM ∂W1 ... .. . ... ∂e1 ∂WJ .. . ∂eM ∂WJ (3.6) to wektor gradientu wyniesie: g(W) = J(W)T e(W) 12 (3.7) natomiast aproksymowana wartość hesjanu: H(W) = J(W)T J(W) + v1 (3.8) Czynnik regularyzacyjny v początkowo, gdy wektor W jest daleki od rozwiązania, przyjmuje dużą wartość w porównaniu z wartościami własnymi macierzy H(W). Przeszukiwanie odbywa się wówczas zgodnie z metodą największego spadku. W miarę postępu procesu uczenia, gdy wartość błędu maleje, wartość parametru v jest zmniejszana. Pierwszy składnik we wzorze (3.8) zaczyna odgrywać coraz większą rolę, przez co algorytm Levenberga-Marquardta przechodzi w algorytm GaussaNewtona [5]. Kierunek poszukiwań wyznaczany jest zgodnie ze wzorem: pt = [J(W)T J(W) + v1]−1 g(W) 3.3 (3.9) Dobór współczynnika uczenia. Podczas uczenia sieci ważnym czynnikiem wpływającym na efektywność zastosowanego algorytmu jest współczynnik uczenia. Określa on długość kroku, jaki należy wykonać w kierunku wyznaczonym przez przyjęty algorytm. Współczynnik uczenia µ powinien mieć taką wartość, aby nowy wektor wag Wt+1 = Wk + µt pt stanowił minimum funkcji celu E(W) w wyznaczonym kierunku pt [5]. Zbyt duża lub zbyt mała długość kroku spowoduje niewykorzystanie w pełni możliwości minimalizacji w wyznaczonym kierunku. To z kolei spowoduje jego powtórzenie, a co za tym idzie, wzrost liczby iteracji potrzebnych do osiągnięcia minimum. W niniejszej pracy minimalizacja kierunkowa została zrealizowana za pomocą algorytmu Brenta[7]. W pierwszej fazie znajdowane są trzy punkty a, b, c takie, że f (a) > f (b) i f (b) < f (c). Gwarantuje to, że pomiędzy punktami a i c znajduje się jakieś minimum. Następnie rozpoczyna się proces minimalizacji. Stosowana jest przy tym interpolacja za pomocą paraboli. Jeżeli takie podejście nie przynosi zadowalających rezultatów, wykorzystywana jest metoda złotego podziału. 13 4 Implementacja. Biblioteka została napisana w postaci obiektowej. Klasa bazowa Solver stanowi interfejs biblioteki. Zawiera też implementacje metod, które mogą być wspólne dla kilku algorytmów, oraz które są niezależne od stosowanego procesora. Metody, które są wspólne dla więcej niż jednego algorytmu, a których implementacja różni się w zależności od tego, czy obliczenia będą wykonywane na CPU, czy na CPU+GPU, zostały umieszczone odpowiednio w klasach SolverCPU i SolverGPU. Poszczególne algorytmy zostały zaimplementowane w postaci klas dziedziczących z SolverCPU lub SolverGPU. Takie podejście umożliwia dodawanie kolejnych algorytmów w prosty sposób. Poniższy diagram przedstawia powstałą hierarchię klas. <<Interface>> Solver <<Interface>> <<Interface>> SolverCPU SolverGPU ConjugateGradientCPU ConjugateGradientGPU LevenbergMarquardtCPU LevenbergMarquardtGPU Rysunek 4.1: Diagram klas przedstawiający zaimplementowaną bibliotekę. 14 Zbiór przykładów oraz sieć są reprezentowane przez klasy Examples i Network. Poszczególne klasy tworzące bibliotekę zostały omówione w kolejnych punktach. Do realizacji projektu zostały wykorzystane następujące biblioteki: • magma 1.3.0 (odwracanie macierzy) • clapack 3.8.4 (mnożenie i odwracanie macierzy) • cublas 5.0 4.1 Klasa Solver Klasa Solver jest to klasa abstrakcyjna wyznaczająca interfejs biblioteki. Dostępne metody to: • teach(Network&, Examples&, bool init=false) - Metoda czysto wirtualna. Przeprowadza proces uczenia sieci. Jeżeli parametry sieci nie będą odpowiadały przykładom, zostanie zwrócona wartość (-1), (na przykład, gdy liczba wejść jest różna od liczby sygnałów w przykładach). Opcjonalny parametr init określa, czy wagi oraz bias mają zostać zainicjalizowane wartościami losowymi. Funkcja zwraca wartość błędu otrzymanego w ostatniej iteracji algorytmu. • computeTotalError(Network&, Examples&) - Metoda czysto wirtualna. Oblicza wartość funkcji błędu dla podanego zbioru przykładów. Jeżeli sieć nie będzie odpowiadała przykładom, zostanie zwrócona wartość (-1). W przypadku pomyślnego działania funkcja zwraca wartość funkcji błędu. • initRandomWeihgts(Network&, unsigned seed) - Przypisuje wagom oraz biasowi wartości losowe z przedziału [−0.25; 0.25]. W tym celu wykorzystywana jest standardowa funkcja rand(). Generator liczb pseudolosowych jest inicjalizowany wartością podaną w drugim parametrze. • crossValidation (Network&, Examples&, unsigned k, int rep) - Implementacja metody walidacji krzyżowej. Zbiór przykładów jest dzielony na k podzbiorów. Proces uczenia jest powtarzany k razy. Za każdym razem jako zbiór testowy jest brany kolejny utworzony podzbiór, natomiast pozostałe przykłady tworzą zbiór uczący. Wartość zwracana to czteroelementowa struktura zawierająca: 15 – – – – średnią arytmetyczną wartość błędu dla zbiorów testowych średnią arytmetyczną wartość błędu dla zbiorów uczących odchylenie standardowe dla błędów zbiorów uczących odchylenie standardowe dla błędów zbiorów testowych Ostatni parametr rep określa, ile razy dla każdej pary zbiorów zostanie powtórzone uczenie. Na przykład, gdy rep=3, dla każdej pary zbiorów uczenie będzie powtórzone trzy razy. Wybrana zostanie sieć, która osiągnęła najmniejszą wartość błędu, a następnie dla tej sieci będzie obliczony błąd zbioru testowego. • getIterationCount() - Zwraca liczbę iteracji wykonaną przez algorytm. • getTeachingResult() - Zwraca wartość funkcji błędu uzyskaną w wyniku procesu uczenia. • exportErrorTrace(const char *fName) - W trakcie uczenia wartości funkcji błędu uzyskane w poszczególnych iteracjach są zapamiętywane. Funkcja ta umożliwia zapisanie tych wartości do pliku. Każda para wartości: numer iteracji - wartość błędu, jest oddzielana białym znakiem i zapisywana w pojedynczej linii. Ponadto w klasie Solver umieszczono dwie metody: bracketMinimum oraz brent, służące do minimalizacji kierunkowej. Metody te są wykorzystywane przez klasy pochodne i nie są publicznie dostępne. Zostały one zaimplementowane zgodnie ze sposobem podanym w [7]. W metodach tych, w trakcie poszukiwania długości kroku, wykorzystana jest funkcja getValueOfPoint. Ponieważ funkcja ta jest czysto wirtualna, musi być zaimplementowana w klasach pochodnych. W ten sposób metody bracketMinimum oraz brent zostały uniezależnione od stosowanej architektury. 4.1.1 Ustawienia konfiguracyjne Klasa Solver zawiera zmienne określające sposób pracy implementowanych algorytmów. Zostały one wypisane poniżej. • maxIterationNumber - Maksymalna liczba iteracji, jaką może wykonać algorytm w trakcie uczenia. Wartość domyślna - 500. 16 • toLeave - Wartość określająca dokładność algorytmu. Wartość domyślna 10−12 . • beta - Wartość parametru beta używana podczas obliczania funkcji aktywacji. Wartość domyślna - 1. Algorytmy, które po wyznaczeniu kierunku wymagają określania długości kroku, mogą wykorzystać minimalizację kierunkową. W takim wypadku istotne są następujące parametry. • brentMaxIterationNumber - Maksymalna liczba iteracji, jaką może wykonać algorytm Brenta podczas minimalizacji kierunkowej. Wartość domyślna 100. • brentToLeave - Wartość określająca dokładność minimalizacji kierunkowej. Wartość domyślna 10−2 . • maxStepCoefficient - Maksymalna długość kroku, jaki w jednej iteracji może wykonać algorytm Brenta. Wartość domyślna 100. Pobierać oraz modyfikować wartości poszczególnych parametrów można za pomocą odpowiednich metod: • • • • • • • • • • • • setMaxIterationNumber(int) getMaxIterationNumber() setToLeave(double) getToLeave() setBeta(double) getBeta() setBrentMaxIterationNumber(int) getBrentMaxIterationNumber() setBrentToLeave(double) getBrentToLeave() setMaxStepCoefficient(double) getMaxStepCoefficient() 17 4.2 Klasa Network Pojedyncza sieć jest reprezentowana za pomocą instancji klasy Network. Parametry sieci są ustawiane poprzez argumenty konstruktora lub wywołanie metody setParameters. Wartości które można ustawiać to: • • • • liczba wejść liczba neuronów ukrytych liczba neuronów wyjściowych wartość, jaką wagi zostaną zainicjalizowane (opcjonalny) Wszystkie wagi oraz wartości biasu są przechowywane w pojedynczym fragmencie pamięci podzielonym logicznie na cztery części. W pierwszej części znajdują się wagi warstwy ukrytej. W części drugiej umieszczono wartości biasu warstwy ukrytej. Następnie w częściach trzeciej i czwartej znajdują się odpowiednio wagi warstwy wyjściowej oraz wartości biasu. Wagi zostały zapisane w postaci macierzy, przy czym wiersze odpowiadają kolejnym neuronom natomiast kolumny wagom. Klasa Network posiada zdefiniowany operator oraz konstruktor kopiujący. Umożliwia to na przykład utworzenie kilku kopii tej samej sieci, a następnie porównanie rezultatów, jakie da zastosowanie różnych algorytmów uczących. Ponadto obiekty typu Network można przechowywać w strukturach danych ze standardowej biblioteki C++ 2 . 4.3 Klasa Examples Zbiór przykładów reprezentowany jest przez obiekt klasy Examples. tworzenia tego obiektu konieczne jest podanie: • • • • 2 liczby sygnałów wejściowych pojedynczego przykładu liczby sygnałów oczekiwanych na wyjściu sieci liczby przykładów nazwy pliku zawierającego przykłady http://www.cplusplus.com/reference/stl/ 18 Podczas Plik z przykładami musi być zapisany w następujący sposób: • każda linia odpowiada pojedynczemu przykładowi • na początku każdej linii znajdują się wartości sygnałów wejściowych oddzielone białymi znakami • po wartościach sygnałów wejściowych, w tej samej linii, znajdują się wartości oczekiwane oddzielone białymi znakami 4.4 Klasy SolverGPU i SolverCPU Klasy SolverCPU oraz SolverGPU zawierają metody wspólne dla obu zaimplementowanych algorytmów, ale różniące się wykorzystywanym procesorem. W kolejnych punktach omówiono podstawowe funkcje realizowane przez te klasy. Obliczenia zaimplementowane na CPU są wykonywane sekwencyjnie, przez jeden wątek. W przypadku GPU zrównoleglenie polega na takim zapisaniu wykorzystywanych wzorów, aby algorytm operował na macierzach, przetwarzając w ten sposób cały zbiór przykładów. Z tego względu większa uwaga została poświęcona implementacji na GPU. 4.4.1 Obliczanie sygnałów wyjściowych Obliczanie sygnałów wyjściowych jest podstawową, najczęściej wywoływaną funkcją biblioteki. Jest wykorzystywana przez oba algorytmy w trakcie procesu minimalizacji funkcji błędu. Dodatkowo obliczone sygnały wyjściowe obu warstw sieci są wykorzystywane podczas obliczania wektora gradientu. Jej wpływ na czas obliczeń jest szczególnie widoczny w przypadku algorytmu gradientów sprzężonych. Obliczanie sygnałów wyjściowych dla całego zbioru uczącego zostało zrealizowane w dwóch krokach. Najpierw cały zbiór przykładów jest przetwarzany przez pierwszą warstwę sieci. Sprowadza się to do pomnożenia macierzy zawierającej wagi sieci oraz macierzy zawierającej przykłady. Do każdego elementu macierzy wynikowej dodawana jest odpowiednia wartość biasu, a następnie obliczana jest wartość funkcji aktywacji: f (x) = 1 1 + exp(−βx) 19 Cały proces został zrealizowany poprzez jeden kernel goThroughLayerGPU. Kernel ten bardzo przypomina klasyczny przykład mnożenia macierzy z wykorzystaniem pamięci współdzielonej. Różnica polega na tym, że przed zapisaniem wyniku obliczeń dodawany jest bias i obliczana jest wartość funkcji aktywacji. W następnym kroku obliczane są sygnały wyjściowe warstwy drugiej. Proces ten przebiega tak samo, jak w przypadku warstwy ukrytej. Jednak do obliczeń zamiast macierzy przykładów wykorzystywana jest macierz uzyskana w poprzednim kroku. W obu krokach wykorzystany został ten sam kernel. Zaprezentowanie sieci zbioru przykładów powoduje powstanie dwóch macierzy. Mimo, iż pierwsza macierz zawiera jedynie wyniki pośrednie, to są one jednak wykorzystywane podczas obliczania gadientu. Z tego powodu obie macierze zostają zachowane w pamięci. 4.4.2 Obliczanie gradientu Jednym z kluczowych elementów poprawnego działania algorytmów jest obliczenie wektora gradientu. W przypadku sieci neuronowych jest to o tyle trudne zadanie, że z każdą kolejną warstwą sieci wzór staje się coraz bardziej skomplikowany. W niniejszej pracy zastosowano sieć z jedną warstwą ukrytą, dlatego też przedstawione wzory odnoszą się tylko do dwóch warstw. Ostateczne wzory zastosowane podczas implementacji zostały zaznaczone niebieską ramką. W tabeli przedstawiono przyjęte wzory wykorzystane do obliczenia gradientu. Funkcja błędu dla pojedynczego przykładu E= K 1X (yk − dk )2 2 k=1 f (x) = Funkcja aktywacji 20 1 1 + exp(−βx) (4.1) (4.2) f 0 (x) = βf (x)(1 − f (x)) Pochodna funkcji aktywacji Wyjście k-tego neuronu warstwy wyjściowej yk = f (hk ) = f ( Wyjście j-tego neuronu warstwy ukrytej zj = f (hj ) = f ( Wejście k-tego neuronu warstwy wyjściowej hk = J X Wkj zj + bk ) (4.4) Wji xi + bj ) (4.5) j=1 I X i=1 J X Wkj f (hj ) + bk (4.6) j=1 Wejście j-tego neuronu warstwy ukrytej hj = I X Wji xi + bj i=1 Oznaczenia przyjęte we wzorach: dk yk zj hk hj bk bj xi (4.3) - wartość oczekiwana na wyjściu k-tego neuronu warstwy wyjściowej - wyjście k-tego neuronu warstwy wyjściowej - wyjście j-tego neuronu warstwy ukrytej - wejście k-tego neuronu warstwy wyjściowej - wejście j-tego neuronu warstwy ukrytej - bias neuronów warstwy wyjściowej - bias neuronów warstwy ukrytej - sygnał podawany na i-te wejście sieci 21 (4.7) i - indeks wejść sieci j - indeks neuronów ukrytych k - indeks neuronów wyjściowych n - indeks przykładów Pochodna funkcji błędu względem biasu neuronów warstwy wyjściowej: ∂E ∂E ∂yk ∂E ∂yk ∂hk = = ∂bk ∂yk ∂bk ∂yk ∂hk ∂bk (4.8) Pochodne występujące w równaniu (4.8) wynoszą odpowiednio: ∂E = (yk − dk ) ∂yk (4.9) ∂yk = βyk (1 − yk ) ∂hk (4.10) ∂hk =1 ∂bk (4.11) Po podstawieniu zależności (4.9), (4.10) oraz (4.11) do równania (4.8) otrzymamy wzór na pochodną względem biasu warstwy wyjściowej. ∂E = (yk − dk )βyk (1 − yk ) ∂bk (4.12) Pochodna względem biasu różni się od pochodnej względem wag jedynie ostatnim składnikiem. We wzorze na pochodną wag zamiast (4.11) występuje: ∂hk = zj ∂wk j (4.13) Zatem pochodna funkcji błędu względem wag warstwy wyjściowej wynosi: ∂E = (yk − dk )βyk (1 − yk )zj ∂Wkj 22 (4.14) Ponieważ wzory na pochodne biasu oraz wag są do siebie bardzo zbliżone, można najpierw policzyć wektor pochodnych biasu, a następnie wykorzystać go do utworzenia macierzy pochodnych wag. Jak wspomniano w poprzednim punkcie, obliczenie sygnałów wyjściowych dla zbioru przykładów powoduje utworzenie dwóch macierzy. Jedna z nich odpowiada sygnałom otrzymanym na wyjściu sieci, natomiast druga na wyjściu warstwy ukrytej. Dzięki temu podczas obliczania gradientu nie jest konieczne ponowne obliczanie wartości zj . Odpowiednio mnożąc wektor pochodnych biasu oraz odpowiadającą mu kolumnę macierzy z sygnałami wyjściowymi, można uzyskać macierz zawierającą pochodne wag. ∂E ∂b1 .. . ∂E ∂bK " # ∗ z1 , . . . , zJ = ∂E ∂W11 ... ∂E ∂WJ1 .. . .. .. . ∂E ∂W1K ... . ∂E ∂WJK W przypadku procesora graficznego przedstawione obliczenia realizowane są za pomocą dwóch kerneli. Pierwszy z nich oblicza pochodne biasu warstwy wyjściowej dla całego zbioru przykładów. Dodatkowo w tymczasowej macierzy zapisuje pochodne dla poszczególnych próbek. Drugi kernel wykorzystuje tymczasową macierz do obliczenia pochodnych względem wag. W przypadku CPU obliczenia wykonywane są przez jeden wątek. Z tego powodu nie jest konieczne tworzenie macierzy pośrednich. Program iteruje po wszystkich przykładach obliczając pochodne dla każdego przykładu oddzielnie, a rezultat dodaje do wektora wynikowego. Pochodne biasu warstwy ukrytej obliczane są zgodnie z następującymi wzorami: K K K X X X ∂E ∂E ∂yk ∂E ∂yk ∂hk ∂E ∂yk ∂hk ∂hj = = = ∂bj k=1 ∂yk ∂bj k=1 ∂yk ∂hk ∂bj k=1 ∂yk ∂hk ∂hj ∂bj (4.15) Pochodne uzyskane w równaniu (4.15) wynoszą odpowiednio: ∂E = y k − dk ∂yk 23 (4.16) ∂yk = f 0 (hk ) ∂hk (4.17) ∂hk = wkj f 0 (hj ) ∂hj (4.18) ∂hj =1 ∂bj (4.19) Po podstawieniu odpowiednich zależności do równania (4.15) otrzymamy wzór na pochodną względem biasu warstwy ukrytej. Czynnik f 0 (hj ) został od razu wyciągnięty poza nawias. K X ∂E (yk − dk )f 0 (hk )Wkj f 0 (hj ) = ∂bj k=1 ! (4.20) Pochodne względem wag różnią się od pochodnych względem biasu jedynie tym, że w równaniu (4.8) zamiast pochodnej (4.19) znajduje się: ∂hj = xi ∂wji (4.21) Wynika z tego, że podobnie jak w przypadku warstwy wyjściowej, można najpierw policzyć pochodne względem biasu, a następnie wykorzystać je do obliczenia pochodnych wag. Wzór na pochodne wag względem warstwy ukrytej wynosi zatem: K X ∂E = (yk − dk )βf (hk )(1 − f (hk )) Wkj β f (hj )(1 − f (hj ))xi {z } | {z } | {z } ∂wji k=1 | ! pochodna biasu warstwy wyjściowej zj (4.22) zj W implementacji na GPU pierwszym krokiem jest obliczenie macierzy sum występujących we wzorze (4.20). W tym celu wykorzystana została tymczasowa macierz utworzona podczas obliczania pochodnych biasu warstwy wyjściowej. W macierzy tej wiersze odpowiadają kolejnym przykładom, natomiast kolumny pochodnym biasu. Pomnożenie tej macierzy przez macierz wag powoduje powstanie 24 macierzy sum (4.23). Mnożenie zostało wykonane za pomocą funkcji cublasDgemm z biblioteki cuBLAS. Krok ten nie dotyczy algorytmu wykonywanego na CPU. ∂E 1 ∂b1 .. . N ∂E ∂b1 ... .. . ... ∂E 1 ∂bK w11 .. ∗ .. . . N ∂E ∂bK . . . w1J .. . .. = . wK1 . . . wKJ K X ∂E 1 wk1 k=1 ∂bk .. . K X ∂E N k=1 ∂bk wk1 K X K X ∂E 1 ... wkJ ∂b k k=1 . .. .. (4.23) . ∂E N ... wkJ k=1 ∂bk Dalsze obliczenia zrealizowane zostały analogicznie jak w przypadku warstwy wyjściowej - poprzez dwa kernele. Pierwszy kernel oblicza pochodne biasu dla całego zbioru przykładów. W macierzy tymczasowej zostają zapisane pochodne biasu poszczególnych próbek. Drugi kernel wykorzystuje tymczasową macierz do obliczenia pochodnych względem wag. 4.5 Klasy ConjugateGradient CPU i GPU Obie klasy: ConjugateGradientCPU oraz ConjugateGradientGPU realizują algorytm gradientów sprzężonych, przez co ich postać jest do siebie bardzo zbliżona. W obu wypadkach implementacja została umieszczona w metodzie findMin(), która implementuje metodę czysto wirtualną zdefiniowaną w klasie Solver. Zasadnicza różnica polega na tym, że klasa ConjugateGradientGPU operuje na danych znajdujących się w pamięci karty graficznej. Przebieg algorytmu w obu przypadkach jest taki sam. W tych samych miejscach następują wywołania takich metod, jak obliczanie wektora gradientu czy minimalizacja kierunkowa. Ponieważ jednak klasy te mają inne klasy bazowe, wywoływanie tych funkcji powoduje wykonanie kodu programu zaimplementowanego odpowiednio na CPU lub GPU. Klasa ConjugateGradientGPU wykorzystuje trzy dodatkowe kernele do prostych operacji na wektorach. W klasie ConjugateGradientGPU operacje te zostały zaimplementowane bezpośrednio w metodzie findMin(). 25 4.6 Klasy LevenbergMarquardt CPU i GPU Klasy LevenbergMarquardtCPU oraz LevenbergMarquardtGPU realizują algorytm Levenbera-Marquardta. Podobnie jak w przypadku klas odpowiadających za algorytm gradientów sprzężonych, mają one taką samą strukturę. Obie implementują metodę wirtualną findMin() z klasy Solver. Klasy te posiadają własne implementacje metod setNetwork. Jest to spowodowane koniecznością alokowania dodatkowej pamięci na macierze jakobianu i hesjanu. 4.6.1 Budowa algorytmu W algorytmie Levenberga-Marquardta aktualizacja wag oraz biasu odbywa się zgodnie ze wzorem: Wk+1 = Wk+1 − [J(W)T J(W) + v1]−1 g(W) (4.24) W trakcie minimalizacji bardzo ważny jest dobór współczynnika v1. Początkowo przyjmuje on dużą wartość, co sprawia, że znaczący wpływ na wyznaczanie kroku ma wektor gradientu. Algorytm działa wówczas zgodnie z metodą największego spadku (zbieżność liniowa). W miarę postępów uczenia wartość funkcji błędu maleje. Sugeruje to, że aproksymacja kwadratowa funkcji błędu jest poprawna a współczynnik v1 powinien zostać zmniejszony. Przy małych wartościach v1 algorytm charakteryzuje się zbieżnością kwadratową. Jeżeli wartość funkcji błędu wzrośnie, oznacza to, że aproksymacja kwadratowa nie oddaje należycie krzywizny funkcji. Należy wówczas ponownie zwiększyć wpływu gradientu. Do wyznaczania współczynnika v1 zostało przyjęte postępowanie przedstawione w pracy [8]. Zostało ono zilustrowane na rysunku 4.2. Podczas minimalizacji algorytmem Levenberga-Marquardta konieczne jest odwrócenie macierzy hesjanu. W tym celu zostały użyte funkcje clapack_dgetri z biblioteki clapack oraz magma_getri_gpu z biblioteki magma. 26 W k , m=1 Ek W k =W k + 1 m>5 Obliczanie J (W )T J (W ) m=m+1 W k =W k + 1 v1=v1∗0.1 m≤5 W k +1=W k −( J (W )T J (W )+v1)−1 G (W ) v1=v1∗10 E k +1 E k +1 > E k E k +1≤ E k E k +1≤ E stop Rysunek 4.2: Schemat przedstawiający działanie alg. Levenbera-Marquardta. 4.6.2 Obliczanie aproksymacji hesjanu Algorytm Levenberga-Marquardta w trakcie minimalizacji wykorzystuje aproksymację hesjanu. Aby ją uzyskać konieczne jest obliczenie macierzy jacobianu. Każdy wiersz jacobianu odpowiada jednemu neuronowi wyjściowemu sieci, natomiast kolumny odpowiadają pochodnym wag oraz biasu. Powoduje to powstanie macierzy o wymiarach K (liczba neuronów wyjściowych) na L (suma liczby wszystkich wag sieci oraz biasu). 27 Funkcja błędu dla pojedynczego przykładu jest taka sama jak w przypadku algorytmu gradientów sprzężonych. K K 1X 1X 2 E= (yk − dk ) = (ek (W))2 2 k=1 2 k=1 Ponieważ jednak pochodne są liczone względem poszczególnych wyjść sieci, a nie funkcji błędu, wprowadzone zostało dodatkowe oznaczenie ek (W) = yk − dk . e(W) = e1 (W) .. . (4.25) eK (W) Aproksymacja hesjanu obliczana jest następująco: H(W) = J(W)T J(W) + v1 (4.26) Jacobian można podzielić logicznie na cztery części, które zawierają: A B C D - pochodne wag pierwszej warstwy - pochodne biasu pierwszej warstwy - pochodne wag drugiej warstwy - pochodne biasu drugiej warstwy | ∂e1 ∂Wj1 ... ∂e1 ∂WjI ∂e1 ∂b1 .. . .. .. . ∂eK ∂Wj1 ... ∂eK ∂WjI {z A . }| ... ∂e1 ∂bJ ∂e1 ∂W11 ... ∂e1 ∂W1J .. . .. .. . .. . .. .. . 0 .. ∂eK ∂b1 ... ∂eK ∂bJ ∂eK ∂WK1 ... ∂eK ∂WKJ 0 0 . {z B }| 28 . {z C ∂e1 ∂b1 }| 0 0 . {z D 0 ∂eK ∂bK } Wzory pochodnych poszczególnych części macierzy dla pojedynczego przykładu wynoszą kolejno: ∂ek A= = f 0 (hk )Wkj f 0 (hj )xi (4.27) ∂Wji B= ∂ek = f 0 (hk )Wkj f 0 (hj ) ∂bj (4.28) ∂ek = f 0 (hk )zj ∂Wkj (4.29) ∂ek = f 0 (hk ) ∂bk (4.30) C= D= gdzie: dk - wartość oczekiwana na wyjściu k-tego neuronu warstwy wyjściowej yk - wyjście k-tego neuronu warstwy wyjściowej zj - wyjście j-tego neuronu warstwy ukrytej hk - wejście k-tego neuronu warstwy wyjściowej hj - wejście j-tego neuronu warstwy ukrytej bk - bias neuronów warstwy wyjściowej bj - bias neuronów warstwy ukrytej xi - sygnał podawany na i-te wejście sieci i - indeks wejść sieci j - indeks neuronów ukrytych k - indeks neuronów wyjściowych W przypadku implementacji na CPU dla każdego przykładu najpierw jest obliczany jakobian. Następnie wykonywane jest mnożenie, którego wynik jest dodawany do macierzy hesjanu. W przypadku GPU zadanie jest bardziej złożone. Sekwencyjne przetwarzanie przykładów spowodowałoby znaczny wzrost czasu obliczeń. Z tego powodu konieczne jest takie zapisanie problemu, aby możliwe było równoległe przetwarzanie całego zbioru. W tym celu zamiast jakobianu od razu obliczana jest aproksymacja hesjanu J(W)T J(W). Czynnik v1 w obu przypadkach dodawany jest na końcu. 29 Aproksymacje hesjanu można podzielić logicznie na 16 części powstałych w wyniku mnożenia jakobianu. Ponieważ utworzona macierz jest symetryczna, wystarczy obliczyć składniki macierzy trójkątnej, a wyniki zapisać odpowiednio w części górnej oraz dolnej. Przedstawia to rysunek 4.1. AT A AT B AT C AT D BT B BT C BT D CT C CT D DT D Tablica 4.1: Podział hesjanu na części. Obliczenia zostały zrealizowane za pomocą dwóch kerneli. Pierwszy z nich jest odpowiedzialny za część AT A. Jest to najbardziej złożona część, dlatego została zaimplementowana w postaci oddzielnego kernela. Pozwoliło to na zredukowanie liczby bezczynnych wątków, występujących w kernelu obliczającym pozostałe części macierzy (wątki, których pozycja jest poniżej przekątnej). Pierwszy wymiar kernela - X wynosi 32. Odpowiada on za przetwarzanie przykładów. Przy każdym przebiegu pętli iterującej, po przykładach, równolegle obliczane są 32 pochodne. Następnie wartości pochodnych są sumowane i zapisywane w macierzy jakobianu. Wartość 32 wynika z tego, że taki właśnie jest rozmiar wrapu3 (wątki w procesorze strumieniowym do wykonania wybierane są po 32). Większy rozmiar podczas sumowania wyników wymagałby wprowadzenia synchronizacji pomiędzy wątkami oraz dodatkowej pamięci na wyniki pośrednie. W drugim kroku za pomocą jednego kernela obliczane są pozostałe części aproksymacji hesjanu. Wątki na podstawie współrzędnych określają, w której części się znajdują i wywołują odpowiednią funkcję. W tym kernelu również 3 Wątki w bloku dzielą się na grupy liczące po 32 wątki[4]. Taka 32 wątkowa grupa nazywa się wrap. Podczas wykonywania programu jednostka zarządzająca wybiera jedną z grup do wykonania. Wszystkie 32 wątki powinny wykonać tę samą instrukcję. Jeżeli w obrębie wrapu nastąpi rozgałęzienie kodu, część wątków czeka bezczynnie, podczas gdy pozostałe wykonują instrukcje pierwszego wariantu. Następnie wykonywany jest kod drugiego wariantu, przez co znów część wątków czeka bezczynnie. Z tego powodu wykonanie instrukcji przez wrap, zamiast jednego, trwa kilka taktów procesora. 30 pierwszy wymiar - X wynosi 32 i wiąże się z przykładami. Dzięki temu nie nastąpią rozgałęzienia programu w żadnej grupie 32 wątków wybranych do wykonywania. Sposób mapowania wzorów na poszczególne wątki zostanie przedstawiony na przykładzie części AT A. W części tej wzory są najbardziej skomplikowane. Pozostałe fragmenty aproksymacji hesjanu są obliczane analogicznie. W przypadku macierzy AT A wzór przyjmuje następującą postać: AT A = K X ∂ek ∂ek 2 = f 0 (hk ) Wkja Wkjb f 0 (hjb )f 0 (hja )xia xib k=1 k=1 ∂Wja ia ∂Wjb ib K X (4.31) Wartości z macierzy AT zostały oznaczone dodatkowym indeksem a, natomiast z macierzy A indeksem b. W każdym wierszu macierzy A znajdują się pochodne kolejnych wag neuronów ukrytych. Tzn. najpierw wag pierwszego neuronu ukrytego, następnie wag drugiego, potem trzeciego itd. (w macierzy C jest tak samo, tylko są to wagi neuronów wyjściowych). Macierz A można dzięki temu podzielić logicznie na mniejsze części. Każda mniejsza część odpowiada jednemu neuronowi i ma I kolumn (I - liczba wejść sieci). Takich mniejszych macierzy jest J (J - liczba neuronów ukrytych). Przedstawiono to na schemacie 4.3. Schemat ten prezentuje sposób, w jaki wątki ustalają wartości pobierane do obliczeń. Każdy wątek ma dwie współrzędne: z oraz y. W przypadku wątku oznaczonego na schemacie jako X, są to z = 6 oraz y = 0. Na podstawie tych współrzędnych wyznaczane są wartości indeksów występujących we wzorach. k = licznik pętli y ia = I ja = y − ia ∗ I z ib = I jb = z − ib ∗ I 31 Rysunek 4.3: Obliczanie fragmentu aproksymacji hesjanu. Ponieważ w jakobianie części C i D są postaci: | ∂e1 ∂W11 ... ∂e1 ∂W1J 0 0 0 0 ∂ek ∂Wk1 0 0 0 0 0 0 0 ... ∂ek ∂WkJ 0 0 0 0 ... 0 0 ∂eK ∂WK1 ... ∂eK ∂WKI 0 0 {z 0 ∂e1 ∂b1 0 }| C 0 {z D 0 0 ∂eK ∂bK } obliczenia z nimi związane są znacznie prostsze. Nie jest konieczne stosowanie przez wątki dodatkowych pętli. 32 4.7 Przykładowe użycie biblioteki Przykładowe użycie biblioteki zostało przedstawione na wydruku 1. Aby skorzystać z biblioteki, należy utworzyć obiekt typu Network reprezentujący sieć oraz Examples reprezentujący zbiór treningowy. Następnie na obiekcie reprezentującym wybrany algorytm należy wywołać metodę teach(Network, Examples), która przeprowadzi proces uczenia. Tą samą sieć można uczyć wielokrotnie, stosując ten sam bądź inny algorytm. Nowo utworzoną sieć, przed poddaniem uczeniu, warto zainicjalizować wartościami losowymi. Doświadczenie pokazuje, że punkt startowy o niewielkich wartościach daje lepsze wyniki w procesie uczenia. Z tego powodu została zaimplementowana metoda initRandomWeihgts(Network&), która przypisuje wagom oraz biasowi wartości losowe z przedziału [−0.25; 0.25]. 33 1 2 3 4 int int int int inSize = 10; outSize = 3; hiddenNeurons = 1 2 ; samplesCount = 2 0 0 0 ; // // // // Liczba Liczba Liczba Liczba sygnalow sygnalow neuronow probek w wejsciowych . wyjsciowych . ukrytych . z b i o r z e uczacym . 5 6 7 8 Network networkA ( i n S i z e , o u t S i z e , hiddenNeurons ) ; Network networkB ; Examples examples ( i n S i z e , o u t S i z e , samplesCount , " s a m p l e s . t x t " ) ; 9 10 11 12 13 ConjugateGradientCPU ConjugateGradientGPU LevenbergMarquardtCPU LevenbergMarquardtGPU solverCG_CPU ; solverCG_GPU ; solverLM_CPU ; solverLV_GPU ; 14 15 16 double t o t a l E r r o r A ; double t o t a l E r r o r B ; 17 18 19 // Z a i n i c j a l i z u j wagi s i e c i w a r t o s c i a m i losowymi . solverCG_CPU . initRandomWeihgts ( networkA ) ; 20 21 22 // Utoworz k o p i e s i e c i . networkB=networkA ; 23 24 25 26 27 // Ucz o b i e s i e c i metoda g r a d i e n t o w s p r z e z o n y c h . solverCG_CPU . t e a c h ( networkA , examples ) ; solverCG_GPU . t e a c h ( networkB , examples ) ; 28 29 30 31 // Kontynuuj u c z e n i e algorytmem Levenberga−Marquardta . t o t a l E r r o r A = solverLV_GPU . t e a c h ( networkA , examples ) ; t o t a l E r r o r B = solverLM_CPU . t e a c h ( networkB , examples ) ; Wydruk 1: Przykładowe użycie biblioteki. 34 5 Wyniki Głównym celem testów jest zbadanie przyspieszenia, jakie uzyskano dzięki zastosowaniu procesora graficznego. Dodatkowo porównano też działanie samych algorytmów zaimplementowanych w bibliotece. Do testów zostały wykorzystane trzy zbiory testowe zróżnicowane pod względem rozmiarów. Poniżej pokrótce przedstawiono problemy testowe. P arkinson - Zbiór zawiera pomiary parametrów głosu 31 osób, przy czym 23 z nich cierpiały na chorobę parkinsona. Celem jest rozpoznawanie ludzi chorych na podstawie ich głosu. • Liczba próbek 195 • Liczba sygnałów wejściowych 22 • Liczba klas 2 Ripley - Zbiór zawiera współrzędne punktów na płaszczyźnie. Punkty podzielone są na dwa zbiory. Celem jest rozpoznawanie, do którego zbioru należy dany punkt. • Liczba próbek 1250 • Liczba sygnałów wejściowych 2 • Liczba klas 2 M AGIC - Zbiór zawiera dane wygenerowane w celu symulacji detekcji wysokoenergetycznych kwantów gamma za pomocą teleskopu Cherenkova. Celem jest rozpoznanie, czy zarejestrowany sygnał jest wywołany promieniowaniem gamma. • Liczba próbek 19020 • Liczba sygnałów wejściowych 10 • Liczba klas 2 35 Wszystkie testy zostały przeprowadzone na komputerze o następujących parametrach: • • • • R CoreTM 2 Duo E4600 (2M Cache, 2.40 GHz) Procesor: Intel Karta graficzna: Gforce GTX 460 SE Pamięć: 3GB DDR2 System operacyjny: UBUNTU 12.10 (3.5.0-21-generic) Dokładna specyfikacja karty graficznej znajduje się w tabeli 7.1. 5.1 Porównanie czasów. W celu porównania czasu uczenia sieci przy użyciu CPU oraz CPU+GPU zostały wykorzystane trzy zbiory danych różniące się rozmiarami. Dla każdego zbioru przeprowadzono proces uczenia przy różnej liczbie neuronów ukrytych. Pozwoliło to zbadać czasy w zależności od stopnia złożoności sieci oraz od wielkości zbioru testowego. Ponieważ w testach tych istotny jest czas uczenia, a nie jakość uzyskanego klasyfikatora, wszystkie przykłady w poszczególnych problemach testowych zostały wykorzystane jako zbiory uczące. Liczba iteracji nie wpływa na stosunek czasu GPU względem CPU. Powoduje jedynie proporcjonalną zmianę wyników obu algorytmów. Na przykład, jeżeli obliczanie wektora gradientu na CPU zajmuje 3ms, natomiast na GPU 1ms, to stosunek czasów GPU/CPU pozostaje niezmieniony i wynosi 31 . Z tego powodu testy nie obejmują zależności czasu wykonania od liczby iteracji. Parametry konfiguracyjne zostały ustawione w taki sposób, aby każdy algorytm wykonał 300 iteracji. Ustawienia, które przyjęto w testach, zostały przedstawione w tabeli 5.1. 36 Ustawienia algorytmu Maksymalna liczba iteracji: 300 Dokładność: 10−15 Wartość beta stosowana w funkcji aktywacji: 1 Ustawienia minimalizacji kierunkowej Maksymalna liczba iteracji: 100 Dokładność: 10−2 Maksymalna długość kroku: 100 Tablica 5.1: Tabela zawierająca ustawienia algorytmu podczas testów. 5.1.1 Porównanie czasów dla algorytmu gradientów sprzężonych. Na przedstawionych wykresach widać, że w przypadku problemów o małej złożoności obliczeniowej procesor graficzny potrzebuje więcej czasu. Jest to widoczne na wykresie 5.1, gdzie zbiór przykładów liczy 195 próbek. Dla dwóch neuronów ukrytych uczenie trwało ponad dwa razy dłużej niż na CPU (przyspieszenie wyniosło 0.41). Wraz ze wzrostem liczby neuronów wzrasta liczba obliczeń, jakie trzeba wykonać w trakcie uczenia. Uwidacznia się wówczas przewaga GPU. Wykresy dla obu algorytmów wzrastają w przybliżeniu liniowo, jednak wykres związany z CPU jest znacznie bardziej stromy. Wykres dla procesora graficznego ma charakterystykę skokową. Skoki występują co 8 neuronów, co wynika z tego, że bloki wątków są właśnie takiego rozmiaru. Co 8 neuronów konieczne jest zwiększenie rozmiarów gridu o jeden blok. Wykresy wszystkich testów są nieco pofalowane. Jest to spowodowane minimalizacją kierunkową zastosowaną podczas wyznaczania długości kroku. Na wyniki ma też wpływ wielkość zbioru uczącego. Im jest on większy, tym wcześniej uwidacznia się przewaga GPU. W przypadku problemu MAGIC (wykres 5.3) już przy bardzo małej liczbie neuronów algorytm będzie działał szybciej na 37 GPU. W przypadku dwóch neuronów przyspieszenie wyniosło 1.41, natomiast dla 25 neuronów 3.09. W przypadku procesora graficznego gorsze wyniki przy małej złożoności problemu są spowodowane koniecznością kopiowania danych z pamięci RAM, znajdującej się na płycie głównej, do pamięci karty graficznej. Analizowany problem musi być na tyle złożony, aby procesor graficzny był w stanie nadrobić czas stracony na transfer danych. Dodatkowo, aby w pełni wykorzystać możliwości GPU, uruchamiane kernele muszą składać się z dużej liczby wątków. Umożliwia to ukrycie opóźnień w dostępie do pamięci globalnej. Wyniki pomiarów oraz uzyskane przyspieszenia zostały przedstawione w tabeli 7.2. Parkinson - Algorytm gradientów sprzężonych 2 CPU GPU 1.8 1.6 Czas (s) 1.4 1.2 1 0.8 0.6 0.4 0.2 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.1: Parkinson - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. 38 Ripley - Algorytm gradientów sprzezonych 8 CPU GPU 7 Czas (s) 6 5 4 3 2 1 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.2: Ripley - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. MAGIC - Algorytm gradientów sprzezonych 180 CPU GPU 160 140 Czas (s) 120 100 80 60 40 20 0 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.3: MAGIC - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. 39 5.1.2 Porównanie czasów dla algorytmu Levenberga Marquardta. W algorytmie Levenberga Marquardta konieczne jest obliczanie aproksymacji hesjanu. Jego wymiary wynoszą kwadrat długości wektora gradientu. Z tego powodu wykres czasu dla CPU ma kształt paraboli. Czasy osiągane przez CPU są mniejsze jedynie w przypadku małych sieci. Na przykład w przypadku zbioru Ripley’a (rys. 5.5) sieć ma tylko dwa wejścia. Rozmiary hesjanu dla sieci z dwoma neuronami ukrytymi w tym wypadku wynoszą 9 na 9, a przyspieszenie 0.29. Wraz ze wzrostem złożoności sieci gwałtownie rosną wymiary hesjanu, przez co pojawia się duża różnica w stosunku do wyników osiągniętych przez GPU. Dla zbioru „Parkinson”, gdzie sieć przyjmuje największe rozmiary (22 neurony wejściowe), również hesjan jest największy. W tym wypadku przyspieszenie przy 25 neuronach ukrytych wyniosło 6.37. Czynnikiem decydującym o krotności przyspieszenia jest zatem raczej rozmiar sieci, niż liczba przykładów. Wzrost rozmiaru hesjanu w mniejszym stopniu odbija się na GPU niż na CPU Wyniki pomiarów oraz uzyskane przyspieszenia zostały przedstawione w tabeli 7.3 Parkinson - Algorytm Levenberga Marquardta 350 CPU GPU 300 Czas (s) 250 200 150 100 50 0 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.4: Parkinson - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. 40 Ripley - Algorytm Levenberga Marquardta 18 CPU GPU 16 14 Czas (s) 12 10 8 6 4 2 0 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.5: Ripley - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. MAGIC - Algorytm Levenberga Marquardta 2500 CPU GPU Czas (s) 2000 1500 1000 500 0 2 4 6 8 10 12 14 16 18 20 22 24 Liczba neuronów Rysunek 5.6: MAGIC - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. 41 5.2 Porównanie działania algorytmów W celu określenia, czy algorytmy działają poprawnie, zbadana została zależność funkcji błędu od liczby iteracji. Każdy z trzech wykresów prezentuje jeden problem testowy. We wszystkich trzech przypadkach algorytmy rozpoczynały z tego samego punkty startowego (tzn. uczona sieć była zainicjalizowana tymi samymi wartościami). Do testów przyjęto następujące rozmiary sieci: • Zbiór Ripley’a - 4 • Zbiór „Parkinson” - 5 • Zbiór „Magic” - 6 Odpowiadające sobie implementacje algorytmów cechują się identyczną zbieżnością. Pokazuje to, że na wyniki nie ma wpływu zastosowana architektura procesora. Odpowiednie realizacje na GPU i CPU są sobie równoważne. Na wykresach można również zauważyć, że algorytm Levenberga-Marquardta lepiej sobie radzi w okolicach minimum. Wartości gradientu w przypadku płaskich funkcji są niewielkie, przez co zbieżność algorytmu gradientów sprzężonych jest mała. 20 Algorytm Gradientów Sprzężonych CPU Algorytm Gradientów Sprzężonych GPU Algorytm Levenberga-Marquardta CPU Algorytm Levenberga-Marquardta GPU 18 Wartość funkcji błędu 16 14 12 10 8 6 4 2 0 0 50 100 150 200 250 300 Liczba iteracji Rysunek 5.7: Parkinson - porównanie zbieżności algorytmów. 42 110 Algorytm Gradientów Sprzężonych CPU Algorytm Gradientów Sprzężonych GPU Algorytm Levenberga-Marquardta CPU Algorytm Levenberga-Marquardta GPU 100 Wartość funkcji błędu 90 80 70 60 50 40 30 20 0 50 100 150 200 250 300 Liczba iteracji Rysunek 5.8: Ripley - porównanie zbieżności algorytmów. 1500 Algorytm Gradientów Sprzężonych CPU Algorytm Gradientów Sprzężonych GPU Algorytm Levenberga-Marquardta CPU Algorytm Levenberga-Marquardta GPU 1400 Wartość funkcji błędu 1300 1200 1100 1000 900 800 700 600 500 0 50 100 150 200 250 Liczba iteracji Rysunek 5.9: MAGIC - porównanie zbieżności algorytmów. 43 300 6 Zakończenie Jak pokazały testy przeprowadzone w podrozdziale 5.2, wszystkie cztery implementacje algorytmów uczących z powodzeniem mogą zostać wykorzystane do zminimalizowania funkcji błędu. W obu przypadkach realizacja na CPU oraz GPU cechuje się podobną zbieżnością. Można zatem stwierdzić, że cel pracy został osiągnięty. Została zaimplementowana biblioteka umożliwiająca porównanie czasu działania tych samych algorytmów na dwóch różnych architekturach sprzętowych. Porównanie czasu uczenia sieci o różnych złożonościach potwierdziło, że dzięki zastosowaniu GPU możliwe jest osiągnięcie przyspieszenia. Uzyskane wyniki są zależne od rozmiaru zbioru przykładów oraz złożoności sieci. W przypadku algorytmu gradientów sprzężonych warto zastosować kartę graficzną w czasie pracy ze złożonymi problemami. Zarówno rozmiary sieci jak i zbioru uczącego powinny być duże. Jeżeli architektura sieci będzie prosta, wówczas, aby wykorzystanie GPU miało sens, zbiór przykładów musi być większy. Jest to nieco sprzeczne ze statystycznym punktem widzenia, w którym im mniejsza sieć, tym mniej przykładów jest konieczne do uczenia. W przypadku prostych sieci i niedużych zbiorów stosowanie procesora graficznego nie jest opłacalne. Algorytm Levenberga-Marquardta jest stosowany jedynie w przypadku małych i średnich problemów. Z powodu dużej złożoności obliczeniowej nie wykorzystuje się go do uczenia bardziej złożonych sieci. Porównanie czasów działania na CPU i GPU wykazało, że algorytm pracujący na karcie graficznej, znacznie szybciej radzi sobie w przypadku większych sieci. Rozszerza to zakres stosowalności tego algorytmu. Jeżeli uczona sieć ma nieduże rozmiary, zastosowanie GPU powoduje pogorszenie czasu uczenia. W takim wypadku lepiej pozostać przy CPU. Najbardziej złożoną częścią pracy było zaimplementowanie obliczania hesjanu za pomocą karty graficznej. Wiązało się to ze zmianą podejścia stosowanego w przypadku CPU. Stosowane wzory stały się znacznie bardziej skomplikowane, jednak w rezultacie udało się przyspieszyć działania algorytmu. Na pewno nie można uznać, że możliwości zastosowania karty graficznej zostały całkowicie wyczerpane. Optymalizacja programu wykorzystującego technologię CUDA, sama w sobie jest zagadnieniem skomplikowanym i wymagającym 44 dużego doświadczenia. Być może dalsze prace doprowadziłyby do uzyskania jeszcze większych przyspieszeń. Ponadto coraz większe możliwości stwarza obecny dynamiczny rozwój architektury procesorów graficznych. W niniejszej pracy wykorzystana została CUDA w wersji 2.1 (oparta o architekturę Fermi). Kolejne wersje oferują coraz większe możliwości. W wersji 3.0 (architektura Kepler) poprawione zostało wsparcie dla operacji na liczbach podwójnej precyzji. Z kolei w wersji 3.5 wprowadzono możliwość zlecania obliczeń przez GPU. Być może umożliwi to całkowite przeniesienie algorytmu na GPU. Brak konieczności komunikacji z CPU w każdej iteracji może przynieść znaczne zmniejszenie czasu obliczeń, nawet w przypadku sieci o niedużej złożoności. 45 7 Załączniki Tablica 7.1: Tabela przedstawiająca dokładną specyfikacje karty graficznej. Device GeForce GTX 460 SE CUDA Driver Version: 5.0 CUDA Capability version number: 2.1 Total amount of global memory: 1024 MBytes (1073283072 bytes) Multiprocessors: 6 CUDA Cores: 288 CUDA Cores/Multiprocessor: 48 GPU Clock rate: 1460 MHz (1.46 GHz) Memory Clock rate: 1700 Mhz Memory Bus Width: 256-bit L2 Cache Size: 524288 bytes Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 48 KBytes (49152 bytes) Total number of registers available per block: 32768 Warp size: 32 Maximum number of threads per multiprocessor: 1536 Maximum number of threads per block: 1024 Maximum sizes of each dimension of a block: 1024 x 1024 x 64 Maximum sizes of each dimension of a grid: 65535 x 65535 x 65535 46 Texture alignment: 512 bytes Maximum memory pitch: 2147483647 bytes Concurrent copy and kernel execution: Yes with 1 copy engine(s) Run time limit on kernels: Yes Integrated GPU sharing Host Memory: No Support host page-locked memory mapping: Yes Concurrent kernel execution: Yes Alignment requirement for Surfaces: Yes Device supports Unified Addressing (UVA): Yes 47 Tablica 7.2: Czasy uczenia algorytmem gradientów sprzężonych. Ripley GPU 0.41 1.22385 1.12643 1.09 0.534342 0.53 1.37202 1.1807 1.16 0.350913 0.523976 0.67 1.60297 1.06084 1.51 2.63 0.436998 0.541843 0.81 1.90725 1.11088 1.72 17.1638 2.90 0.489257 0.522324 0.94 2.21659 1.09405 2.03 55.0777 17.0535 3.23 0.56104 0.553882 1.01 2.46865 1.0847 2.28 8 61.9812 17.1489 3.61 0.65501 0.560854 1.17 2.66378 1.15228 2.31 9 68.5139 30.4130 2.25 0.692182 0.79384 0.87 3.00089 1.66415 1.80 10 75.7491 30.8174 2.46 0.800248 0.842913 0.95 3.32814 1.71481 1.94 11 81.0657 31.1093 2.61 0.895084 0.803579 1.11 3.70082 1.67172 2.21 12 90.7504 29.5921 3.07 0.964213 0.799211 1.21 3.80519 1.70151 2.24 13 96.8987 30.4331 3.18 1.0159 0.822892 1.23 4.24694 1.68348 2.52 14 101.3680 30.3958 3.33 1.08682 0.824546 1.32 4.59695 1.72183 2.67 15 112.7380 30.9728 3.64 1.15275 0.844323 1.37 4.77467 1.80766 2.64 16 115.2170 30.4999 3.78 1.24073 0.840028 1.48 4.98111 1.6842 2.96 17 119.8180 43.7461 2.74 1.30358 0.90989 1.43 5.25884 2.33379 2.25 18 131.5230 44.0440 2.99 1.38886 0.926263 1.50 5.7186 2.26704 2.52 CPU GPU 1.41 0.219442 0.541141 17.3003 1.72 0.284055 37.8370 17.5278 2.16 5 45.2630 17.2234 6 49.7667 7 Neurony CPU/GPU CPU CPU/GPU Parkinson CPU/GPU MAGIC CPU GPU 2 23.5939 16.7465 3 29.8379 4 48 19 134.3450 43.5601 3.08 1.42381 0.979184 1.45 6.0364 2.38304 2.53 20 139.3440 45.2430 3.08 1.55 0.931699 1.66 6.3016 2.2986 2.74 21 152.6500 44.5519 3.43 1.55952 0.930324 1.68 6.67797 2.35493 2.84 22 154.1390 43.3492 3.56 1.71046 0.946252 1.81 7.35939 2.32692 3.16 23 162.8400 44.4676 3.66 1.73178 0.974365 1.78 7.13267 2.3244 3.07 24 169.9870 43.9721 3.87 1.82139 0.978231 1.86 7.62043 2.32448 3.28 25 176.4330 57.1041 3.09 1.93296 1.19912 1.61 7.86573 2.97788 2.64 Tablica 7.3: Czasy uczenia algorytmem Levenberga-Marquardta. CPU GPU CPU/GPU GPU 2.14 0.743612 1.30984 0.57 0.335454 1.17529 0.29 12.483 2.81 1.61559 2.68915 0.60 0.532597 1.1863 0.45 59.6509 18.9235 3.15 2.73796 3.14319 0.87 0.788368 1.27957 0.62 5 89.5066 27.2963 3.28 4.39932 3.76297 1.17 1.05943 1.36307 0.78 6 129.169 35.8156 3.61 6.42903 4.79826 1.34 1.34858 1.41866 0.95 7 172.146 48.6389 3.54 8.76314 5.84109 1.50 1.70815 1.5428 1.11 8 222.647 63.3104 3.52 11.7934 7.20591 1.64 2.10003 1.70359 1.23 9 276.671 80.3205 3.44 15.2635 8.36204 1.83 2.59068 1.92958 1.34 Neurony CPU Ripley CPU/GPU Parkinson CPU/GPU MAGIC CPU GPU 2 17.8311 8.34065 3 35.1291 4 49 10 339.751 92.8984 3.66 19.3033 10.2926 1.88 3.03591 2.02631 1.50 11 408.601 112.514 3.63 23.5323 12.5564 1.87 3.65796 2.21605 1.65 12 484.014 131.363 3.68 27.5298 13.853 1.99 4.25268 2.39374 1.78 13 562.473 152.536 3.69 33.6632 14.9916 2.25 4.9174 2.63441 1.87 14 653.828 170.538 3.83 40.7945 16.772 2.43 5.62782 2.73811 2.06 15 746.62 196.974 3.79 48.4401 18.5965 2.60 6.508 2.98618 2.18 16 849.126 226.368 3.75 64.8876 21.1652 3.07 7.36497 4.16359 1.77 17 1010.39 256.144 3.94 87.4031 23.2618 3.76 8.26134 4.51085 1.83 18 1063.18 279.928 3.80 104.788 25.008 4.19 9.21358 4.6936 1.96 19 1184.04 314.235 3.77 136.498 28.5044 4.79 10.1417 5.01778 2.02 20 1381.03 349.049 3.96 158.387 31.3067 5.06 11.371 5.29776 2.15 21 1464.83 381.854 3.84 189.999 34.2766 5.54 12.1729 5.61169 2.17 22 1608.75 412.485 3.90 221.142 39.9757 5.53 13.3999 5.84134 2.29 23 1765.01 454.3 3.89 254.373 42.9373 5.92 14.5236 6.22239 2.33 24 1921.52 498.414 3.86 286.097 46.7733 6.12 15.7904 6.65308 2.37 25 2158.28 541.744 3.98 321.043 50.3754 6.37 16.9476 7.14605 2.37 50 Spis rysunków 2.1 2.2 2.3 2.4 4.1 4.2 4.3 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 Schemat przedstawiający różnicę w budowie CPU i GPU [4]. . . . . Automatyczne przyporządkowanie bloków do SM na GPU o różnej liczbie rdzeni [4]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schemat przedstawiający model pamięci w architekturze CUDA [2]. Schemat przedstawiający działanie programu wykorzystującego GPU [2]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Diagram klas przedstawiający zaimplementowaną bibliotekę. . . . . Schemat przedstawiający działanie alg. Levenbera-Marquardta. . . Obliczanie fragmentu aproksymacji hesjanu. . . . . . . . . . . . . . Parkinson - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. . . . . . . . . . Ripley - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. . . . . . . . . . . . . . MAGIC - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm gradientów sprzężonych. . . . . . . . . . . Parkinson - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. . . . . . . . . . Ripley - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. . . . . . . . . . . . . MAGIC - porównanie czasów uczenia w zależności od liczby neuronów ukrytych. Algorytm Levenberga Marquardta. . . . . . . . . . . Parkinson - porównanie zbieżności algorytmów. . . . . . . . . . . . Ripley - porównanie zbieżności algorytmów. . . . . . . . . . . . . . MAGIC - porównanie zbieżności algorytmów. . . . . . . . . . . . . 6 7 8 10 14 27 32 38 39 39 40 41 41 42 43 43 Spis tablic 4.1 5.1 7.1 7.2 Podział hesjanu na części. . . . . . . . . . . . . . . . . . . . . Tabela zawierająca ustawienia algorytmu podczas testów. . . . Tabela przedstawiająca dokładną specyfikacje karty graficznej. Czasy uczenia algorytmem gradientów sprzężonych. . . . . . . 51 . . . . . . . . . . . . 30 37 46 48 7.3 Czasy uczenia algorytmem Levenberga-Marquardta. . . . . . . . . . 49 Literatura [1] Nvidia’s next generation cuda compute architecture: Fermi. http: //www.nvidia.com/content/PDF/fermi_white_papers/NVIDIA_Fermi_ Compute_Architecture_Whitepaper.pdf, 2009. [2] David Kanter. Nvidia’s gt200: Inside a parallel processor. http://www. realworldtech.com/gt200/, 2008. [Online; accessed 7.01.2013]. [3] NVIDIA. CUDA C BEST PRACTICES GUIDE, wydanie 5.0, October 2012. [4] NVIDIA. CUDA C PROGRAMMING GUIDE, wydanie 5.0, October 2012. [5] Stanisław Osowski. Sieci neuronowe w ujęciu algorytmicznym. Wydawnictwa Naukowo-Techniczne, Warszawa, 1996. [6] William T. Vetterling Brian P. Flannery William H. Press, Saul A. Teukolsky. Conjugate gradient methods in multidimensions. Numerical Recipes in C: The Art of Scientific Computing, rozdział 10.6. Cambridge University Press, wydanie drugie, 1992. [7] Brian P. Flannery William H. Press Saul A. Teukolsky, William T. Vetterling. Parabolic interpolation and brent’s method in one dimension. Numerical Recipes in C: The Art of Scientific Computing, rozdział 10.2. Cambridge University Press, wydanie drugie, 1992. [8] Hao Yu, B. M. Wilamowski. Levenberg–marquardt training. Industrial Electronics Handbook, vol. 5 – Intelligent Systems, rozdział 12.3.2. CRC Press, wydanie drugie, 2011. http://www.eng.auburn.edu/~wilambm/pap/2011/K10149_ C012.pdf. 52