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

Podobne dokumenty