Pracownia problemowa 2009/2010 Prowadzący: mgr inż. Michał Pryczek środa, 8:15



Podobne dokumenty
Obiekt klasy jest definiowany poprzez jej składniki. Składnikami są różne zmienne oraz funkcje. Składniki opisują rzeczywisty stan obiektu.

Programowanie obiektowe

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Czym jest Java? Rozumiana jako środowisko do uruchamiania programów Platforma software owa

Programowanie obiektowe

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Czym są właściwości. Poprawne projektowanie klas

Dziedziczenie. Streszczenie Celem wykładu jest omówienie tematyki dziedziczenia klas. Czas wykładu 45 minut.

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Wykład 8: klasy cz. 4

Wyjątki. Streszczenie Celem wykładu jest omówienie tematyki wyjątków w Javie. Czas wykładu 45 minut.

Kurs programowania. Wstęp - wykład 0. Wojciech Macyna. 22 lutego 2016

Wprowadzenie do projektu QualitySpy

Konstruktory. Streszczenie Celem wykładu jest zaprezentowanie konstruktorów w Javie, syntaktyki oraz zalet ich stosowania. Czas wykładu 45 minut.

Programowanie obiektowe. Literatura: Autor: dr inŝ. Zofia Kruczkiewicz

XQTav - reprezentacja diagramów przepływu prac w formacie SCUFL przy pomocy XQuery

Java - wprowadzenie. Programowanie Obiektowe Mateusz Cicheński

Programowanie obiektowe - 1.

Paradygmaty programowania

8. Neuron z ciągłą funkcją aktywacji.

Jeśli chcesz łatwo i szybko opanować podstawy C++, sięgnij po tę książkę.

Zapisywanie algorytmów w języku programowania

Zadanie polega na stworzeniu bazy danych w pamięci zapewniającej efektywny dostęp do danych baza osób.

Wykład 5: Klasy cz. 3

Scala - programowanie obiektowo-funkcyjne

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Dokumentacja do API Javy.

IMPLEMENTACJA SIECI NEURONOWYCH MLP Z WALIDACJĄ KRZYŻOWĄ

Pierwsze kroki. Algorytmy, niektóre zasady programowania, kompilacja, pierwszy program i jego struktura

Temat: Sieci neuronowe oraz technologia CUDA

Podczas dziedziczenia obiekt klasy pochodnej może być wskazywany przez wskaźnik typu klasy bazowej.

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Rozdział 4 KLASY, OBIEKTY, METODY

Elżbieta Kula - wprowadzenie do Turbo Pascala i algorytmiki

JAVA. Java jest wszechstronnym językiem programowania, zorientowanym. apletów oraz samodzielnych aplikacji.

Programowanie obiektowe zastosowanie języka Java SE

Sztuczna Inteligencja Tematy projektów Sieci Neuronowe

Aplikacje w środowisku Java

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Wątek - definicja. Wykorzystanie kilku rdzeni procesora jednocześnie Zrównoleglenie obliczeń Jednoczesna obsługa ekranu i procesu obliczeniowego

Wykład 9: Polimorfizm i klasy wirtualne

Programowanie obiektowe

Programowanie w języku Java - Wyjątki, obsługa wyjątków, generowanie wyjątków

PHP 5 język obiektowy

Wyjątki (exceptions)

Dziedziczenie. Tomasz Borzyszkowski

Podstawy programowania skrót z wykładów:

Jak zawsze wyjdziemy od terminologii. While oznacza dopóki, podczas gdy. Pętla while jest

4. Funkcje. Przykłady

Algorytm. a programowanie -

4. Procesy pojęcia podstawowe

Typy, klasy typów, składnie w funkcji

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Klasy abstrakcyjne i interfejsy

Szablony funkcji i szablony klas

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Programowanie i techniki algorytmiczne

Współbieżność i równoległość w środowiskach obiektowych. Krzysztof Banaś Obliczenia równoległe 1

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Komputer nie myśli. On tylko wykonuje nasze polecenia. Nauczmy się więc wydawać mu rozkazy

Szablony funkcji i klas (templates)

Michał Olejnik. 22 grudnia 2009

Java jako język programowania

SYSTEMY OPERACYJNE: STRUKTURY I FUNKCJE (opracowano na podstawie skryptu PP: Królikowski Z., Sajkowski M. 1992: Użytkowanie systemu operacyjnego UNIX)

JAVA W SUPER EXPRESOWEJ PIGUŁCE

Programowanie Strukturalne i Obiektowe Słownik podstawowych pojęć 1 z 5 Opracował Jan T. Biernat

1 Podstawy c++ w pigułce.

Być może jesteś doświadczonym programistą, biegle programujesz w Javie,

Analiza i projektowanie obiektowe 2016/2017. Wykład 10: Tworzenie projektowego diagramu klas

KOTLIN. Język programowania dla Androida

OSGi Agata Hejmej

Ćwiczenie numer 4 JESS PRZYKŁADOWY SYSTEM EKSPERTOWY.

Wstęp do programowania 2

Myśl w języku Python! : nauka programowania / Allen B. Downey. Gliwice, cop Spis treści

Temat 20. Techniki algorytmiczne

C# 6.0 : kompletny przewodnik dla praktyków / Mark Michaelis, Eric Lippert. Gliwice, cop Spis treści

Informatyka I. Dziedziczenie. Nadpisanie metod. Klasy abstrakcyjne. Wskaźnik this. Metody i pola statyczne. dr inż. Andrzej Czerepicki

Podstawy i języki programowania

Informatyka I: Instrukcja 4.2

Rok akademicki: 2012/2013 Kod: ZIE s Punkty ECTS: 3. Poziom studiów: Studia I stopnia Forma i tryb studiów: -

C++ Przeładowanie operatorów i wzorce w klasach

7. Pętle for. Przykłady

PyPy's Approach to Virtual Machine Construction

JAK DZIAŁAJĄ FUNKCJE PODZIAŁ PAMIĘCI

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Technologie i usługi internetowe cz. 2

Programowanie Obiektowe Ćwiczenie 4

Przykład 1: Funkcja jest obiektem, przypisanie funkcji o nazwie function() do zmiennej o nazwie funkcja1

Tworzenie oprogramowania

PARADYGMATY PROGRAMOWANIA Wykład 4

Zadanie nr 3: Sprawdzanie testu z arytmetyki

1 Podstawy c++ w pigułce.

Język JAVA podstawy. wykład 2, część 1. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

Programowanie dla początkujących w 24 godziny / Greg Perry, Dean Miller. Gliwice, cop Spis treści

Diagramy klas. dr Jarosław Skaruz

Struktura systemu operacyjnego. Opracował: mgr Marek Kwiatkowski

Wstęp do programowania

Uwagi dotyczące notacji kodu! Moduły. Struktura modułu. Procedury. Opcje modułu (niektóre)

Spis treści. 1 Java T M

Transkrypt:

Informatyka, studia dzienne, mgr jednolite semestr IX Pracownia problemowa 2009/2010 Prowadzący: mgr inż. Michał Pryczek środa, 8:15 Data oddania: Ocena: Artur Ziółkowski 133901 Mariusz Seklecki 133853 Krzysztof Jarocki 133769 1

Spis treści Lista algorytmów............................... 2 1. Wstęp..................................... 4 Środowiska uruchomieniowe i IDE..................... 4 2. Model aktora................................ 5 2.1. Erlang.................................. 5 2.1.1. Aktorzy w Erlangu...................... 6 2.1.2. Praktyczne zapoznanie z językiem Erlang.......... 7 2.2. Scala.................................. 10 2.2.1. Aktorzy w Scali........................ 11 2.3. Uzasadnienie wyboru języka do rozwiązania problemu praktycznego 15 3. Problem praktyczny........................... 17 3.1. Przedstawienie problemu....................... 17 3.2. Architektura rozwiązania....................... 17 3.2.1. Budowa sieci.......................... 17 3.2.2. Klasy i komunikaty...................... 18 3.3. Opis interfejsu............................. 22 3.4. Prezentacja działania......................... 24 4. Skalowalność rozwiązania........................ 27 4.1. Opis idei testów............................ 27 4.2. Wyniki................................. 28 5. Wnioski................................... 28 Literatura.................................... 30 Lista algorytmów 1 Realizacja funkcji silnia w Erlangu............... 6 2 Przykład użycia modułu matematyka w środowisku konsolowym Werl......................... 6 3 Kod modułu koordynatora napisany w języku Erlang z wykorzystaniem mechanizmu aktorów.............. 8 4 Kod modułu zadania napisany w języku Erlang z wykorzystaniem mechanizmu aktorów.............. 8 5 Wykorzystanie zaimplementowanych modułów: koordynatora i zadania.............................. 9 6 Klasa i singleton w Scali. Wywołanie metod........... 10 7 Deklaracja klasy i singletonu z listingu 6 z jawną deklaracją typów zwracanych przez funkcje................. 11 8 Przykładowy program prezentujący komunikację pomiędzy thread-based actors........................ 12 9 Event-based actors - NIEPOPRAWNA konstrukcja...... 15 10 Event-based actors - poprawna konstrukcja wykorzystująca rekurencję............................. 15 11 Tekst pomocy stworzonej aplikacji................ 22 12 Format pliku opisujący strukturę sieci do wygenerowania... 24 2

13 Przykładowy plik ze strukturą sieci dla rozpoznawania cyfr z losowanymi wagami........................ 24 14 Przykład sieci z określonymi wagami............... 25 15 Wektory wejściowe i wektory oczekiwane dla cyfr od 0 do 9.. 25 16 Zaburzone wektory wejściowe.................. 27 17 Wyjścia sieci dla zaburzonych wzorców (zaokrągliliśmy wartości do trzech miejsc po przecinku)............. 27 3

1. Wstęp W ramach pracowni problemowej sztucznej inteligencji wybraliśmy temat o współbieżności w językach funkcyjnych. Naszym głównym celem było zapoznanie się z modelem aktora, jako jednym ze sposobów na współbieżność. Zadanie było też dobrą okazją do zapoznania się z ideą programowania funkcyjnego. Programowanie funkcyjne różni się od programowania imperatywnego dość znacznie. Przede wszystkim w językach czysto funkcyjnych nie ma zmiennych, a tylko nazwane wartości. Najważniejszym zaś elementem programowania funkcyjnego jest utożsamianie każdej funkcji ze zwracaną przez nią wartością. Funkcje można przekazywać jako parametry innych funkcji i funkcje nie mogą mieć efektów ubocznych. Jako programiści piszący dotąd wyłącznie w językach imperatywnych, mieliśmy na początku pewien kłopot z przestawieniem się na całkiem inny styl programowania. Poznawaliśmy dwa języki funkcyjne (a przynajmniej częściowo funkcyjne): Erlang i Scalę. O ile możliwość przekazania funkcji jako parametru innej funkcji powitaliśmy z wielką aprobatą (każdy miał już doświadczenia z językiem C#, który, jako zbiór coraz większej ilości udogodnień dla programisty, ma delegaty i na dodatek można je podawać za pomocą tzw. wyrażeń lambda), o tyle brak zmiennych i iteracji w Erlangu był sporym zaskoczeniem i nie był łatwy do opanowania. Uznaliśmy, że na początku zapoznamy się z oboma językami, tworząc w nich małe, proste programy, a następnie wybierzemy jeden z nich do rozwiązania jakiegoś konkretnego problemu. Dlatego pierwsza część sprawozdania będzie dotyczyła omówienia modelu aktora w obu językach, natomiast w drugiej części skupimy się już tylko na jednym z tych języków. Środowiska uruchomieniowe i IDE Warto jeszcze wspomnieć o środowiskach uruchomieniowych i IDE, jakie można znaleźć dla Erlanga i Scali. Erlang: Erlang/OTP R13B02 - interaktywna powłoka Erlanga dla Windows. Pozwala wygenerować bajtkod na podstawie kodu pisanego w zewnętrznym edytorze i uruchamiać aplikacje. To środowisko działało u całej naszej trójki bez problemów. Erlang IDE dla Eclipse (repozytorium: http://erlide.org/update) - wtyczka pozwalająca pisać w Erlangu pod Eclipse. Działała poprawnie tylko u jednego z nas. Próby wprowadzenia analogicznej konfiguracji u pozostałych nie zdały egzaminu. Scala: Scala REPL - interaktywne środowisko programistyczne Scali. Nie mieliśmy okazji z niego korzystać. Scala Eclipse Plugin (repozytorium: http://www.scala-lang.org/scala-eclipse-plugin-nightly) - wtyczka pozwalająca pisać w Scali pod Eclipse. Podczas pracy korzystaliśmy właśnie z tej wtyczki. 4

2. Model aktora Ze względu na ograniczone możliwości dalszego zwiększania mocy obliczeniowej pojedynczych procesorów nastąpiła zmiana podejścia do tego problemu. Obecnie zwiększa się moc komputerów, poprzez zwiększenie ilości rdzeni procesorów oraz ilości procesorów. Niestety, aby dobrze zagospodarować możliwości przerobowe takiej jednostki, potrzebne jest oprogramowanie współbieżne. Tradycyjnie współbieżność w oprogramowaniu uzyskuje się poprzez operowanie bezpośrednio na wątkach. Współdzielą one pamięć, co może prowadzić do powstania wielu trudnych do wykrycia błędów. Przykładowo w programie działają dwa wątki, które modyfikują tą samą zmienną. Nie jest to operacja atomowa (składa się z odczytania wartość zmiennej, wyliczenia nowej wartości i zapisu jej do zmiennej). Czasami może dojść do przeplecenia operacji różnych wątków i w efekcie otrzymania nieprawidłowej wartości zmiennej. Zapobiega się temu poprzez stosowanie blokad, które z kolei mogą prowadzić do zakleszczenia się wątków. W naszej pracy zajęliśmy się alternatywnym rozwiązaniem problemu współbieżności opartym o model aktora. W modelu tym każdy obiekt jest aktorem, który posiada swoją skrzynkę na komunikaty oraz definicje zachowań w razie otrzymania komunikatów różnych typów. Zależnie od otrzymanej wiadomości aktor może podejmować różne działania, np. wysyłać komunikaty do innych aktorów oraz tworzyć nowych. Komunikacja między aktorami odbywa się asynchronicznie. Pozwala to na natychmiastowe kontynuowanie działania zaraz po wysłaniu komunikatu, bez oczekiwania na potwierdzenie ze strony odbiorcy. Dostarczenie wiadomości jest zagwarantowane, ale kolejność ich odbioru po drugiej stronie już nie. Dzięki takiemu sposobowi komunikacji nie mamy do czynienia ze współdzieleniem pamięci i związanymi z tym problemami. Aktorzy mogą przesyłać informacje oraz wpływać na siebie jedynie poprzez przesyłanie komunikatów. Możemy postrzegać aktorów jako małe niezależne procesy komunikujące się ze sobą w celu realizacji jakiegoś złożonego zadania. 2.1. Erlang Erlang jest językiem programowania współbieżnego stworzonym przez Ericssona. Został zaprojektowany jako język rozproszony, który dostarcza możliwość tworzenia aplikacji odpornych na błędy, działających non-stop. Pozwala nawet na częściową wymianę kodu w działających aplikacjach, co jest zaletą w przypadku bardzo złożonych systemów, w których ponowne uruchomienie wymaga synchronizacji wielu skomplikowanych procesów poprzedzających. Znalazł swoje zastosowanie głównie w dziedzinie telekomunikacji, ale jest również językiem ogólnego zastosowania. Jednym z przykładów może być napisany w Erlangu komunikator popularnego w Polsce portalu społecznościowego Nasza-Klasa. Komunikator nazywa się NKtalk. Oparcie aplikacji komunikatora na technologii Erlanga umożliwiło autorom efektywne skalowanie systemu i przygotowanie całego serwisu na ogromny ruch sieciowy generowany przez użytkowników i przesył danych. 5

Erlang jest językiem ściśle funkcjonalnym. Cechuje go pojedyncze przypisywanie oraz gorliwa ewaluacja. Posiada wbudowane struktury dla mechanizmów rozproszenia i współbieżności. Posiada dynamiczne typowanie danych, w którym sprawdzanie typu wyrażenia jest wykonywane w czasie jego użycia. Dynamiczne sprawdzanie typu jest oparte na zawartości samych danych. Pozwala to działać aplikacji poprawnie nawet, kiedy w procesie przetwarzania pojawią się nieoczekiwane dane o innym typie. Działanie Erlanga opiera się na istnieniu wirtualnej maszyny. Wirtualna maszyna posiada własną implementację procesów, która jest niezależna od procesów i wątków systemu operacyjnego gospodarza. Dzięki takiemu rozwiązaniu zarządzanie procesami jest bardzo wydajne. Czas tworzenia, przełączania czy niszczenia procesów w Erlangu jest dużo krótszy niż w systemie operacyjnym. Procesy wirtualnej maszyny nie współdzielą między sobą żadnych danych natomiast komunikacja pomiędzy nimi jest przeprowadzana za pomocą wymiany komunikatów. Na listingu 1 chcieliśmy zaprezentować bardzo prosty i popularny przykład działania Erlanga. Listing 1 Realizacja funkcji silnia w Erlangu -module(matematyka). -export([silnia/1]) silnia(0) -> 1; silnia(n) when N > 0 -> N * silnia(n-1). Przykład prezentuje realizację funkcji silnia. Pierwszym krokiem, jaki musi zostać wykonany jest utworzenie i nazwanie modułu. Wykonuje się to za pomocą słowa kluczowego module. Następnie należy zdefiniować funkcjonalność modułu, czyli określić za pomocą słowa kluczowego export jakie metody udostępnia moduł oraz ile parametrów mogą one przyjmować. Po spełnieniu tych podstawowych wymagań można już pisać ciało metod. To, która metoda zostanie wykonana zostanie określone w czasie jej wywołania na podstawie przekazywanych parametrów. Każdy możliwy scenariusz musi zostać obsłużony. Wykorzystanie modułu z listingu 1 prezentuje listing 2. Listing 2 Przykład użycia modułu matematyka w środowisku konsolowym Werl Erlang R13B03 (erts-5.7.4) [smp:2:2] [rq:2] [async-threads:0] Eshell V5.7.4 (abort with ^G) 1> cd("e:/ftims/projekty Eclipse/Erlang"). E:/FTIMS/Projekty Eclipse/Erlang ok 2> c(matematyka). {ok,matematyka} 3> matematyka:silnia(5). 120 2.1.1. Aktorzy w Erlangu Erlang został zaprojektowany pod kątem współbieżnego i rozproszonego działania aplikacji, które w dodatku mają być skalowalne. Z tego powodu aktorzy są jego integralną częścią. Nazywa się ich procesami, a każdy taki proces 6

uruchamiany jest za pomocą wbudowanej funkcji spawn. Jak wspomnieliśmy wcześniej aktorzy nie współdzielą ze sobą żadnych danych. Dla niektórych może to być wada, ale z drugiej strony takie rozwiązanie zwalnia programistę z dbania o synchronizowany dostęp do współdzielonych danych. Wszystkie informacje aktorzy wymieniają pomiędzy sobą za pomocą mechanizmu komunikatów asynchronicznych. Wysłanie pojedynczego komunikatu wymaga użycia specjalnego operatora!. Każdy aktor posiada swoją skrzynkę, do której trafiają komunikaty. Cała funkcjonalność związana ze współbieżnością znajduje się w bloku kodu pomiędzy znacznikami receive, a end. W tej części kodu skrzynka aktora jest odczytywana, a wiadomości w niej przechowywane są dopasowywane do obsługiwanych przez aktora wzorców komunikatów, w kolejności w jakiej zostały zapisane przez programistę. Jeśli został znaleziony odpowiedni wzorzec wiadomości, to wykonywany zostaje odpowiedni blok funkcjonalny, natomiast jeśli dopasowanie nie zostanie uzyskane, to nic się nie dzieje, a nadawca nie zostaje o takiej sytuacji powiadomiony. Programista możne jednak zdefiniować wzorzec, do którego zostaną dopasowane wszystkie możliwe komunikaty. W taki sposób może poinformować nadawcę o błędzie. 2.1.2. Praktyczne zapoznanie z językiem Erlang W ramach praktycznego zapoznania się z językiem Erlang każdy z nas napisał prosty program, w którym wykorzystał mechanizm aktorów. Nie będziemy przedstawiali wszystkich rozwiązań. Wybraliśmy jedno, które posłuży jako przykład wykorzystania mechanizmu współbieżności oferowanego przez język Erlang. Program składa się z dwóch modułów: koordynatora oraz zadania. Najpierw musi zostać utworzony koordynator. Jest on rejestrowany jako proces pod tą samą nazwą. Pozwala to na posługiwanie się nazwą procesu koordynatora w drugim module. Można utworzyć wiele procesów zadań. Zamysłem było, aby każde zadanie wykonywało jakieś cząstkowe obliczenia. W ramach zabawy językiem i testów jest to zwykła funkcja f(x) = x 2, ale nic nie stoi na przeszkodzie, aby stworzyć więcej modułów zadania już wyspecjalizowanych w jakichś obliczeniach. Ważne tylko aby miały taki sam interfejs. Podczas tworzenia zadania wywołana zostaje funkcja koordynatora o nazwie dodajzadanie(pidzadania, NazwaZadania), która dodaje identyfikator procesu zadania do listy przechowywanej w koordynatorze. Dodanie do listy jest potwierdzone odpowiednim komunikatem. Jeśli wszystkie zadania zostały utworzone, to możemy zlecić koordynatorowi ich wykonanie. Dokonuje się tego wywołując funkcję koordynator:wykonajzadania(argument). Do wszystkich zarejestrowanych procesów-zadań zostanie wysłany nakaz wykonania obliczeń dla podanego argumentu. Po wykonaniu obliczeń procesy zadań zwracają wyniki do procesu koordynatora i oczekują na dalsze akcje. Koordynator otrzymując komunikat zakończenia wykonywania obliczeń od procesu zadania uaktualnia wartość końcowego wyniku obliczeń. Kod obydwu modułów jest prezentowany na listingach 3 oraz 4 natomiast przykładowe użycie opisanych modułów przedstawiamy na listingu 5. 7

Listing 3 Kod modułu koordynatora napisany w języku Erlang z wykorzystaniem mechanizmu aktorów. -module(koordynator). -export([utworz/0]). -export([dodajzadanie/2]). -export([wykonajzadania/1]). -export([akcja/1]). -export([wynik/0]). utworz() -> register(koordynator,spawn(fun() -> akcja({[],0}) end)). dodajzadanie(pidzadania, NazwaZadania) -> koordynator! { dodajdolisty, PidZadania, NazwaZadania}. wykonajzadania(argument) -> koordynator! { zleczadanie, "SERWER", Argument }. wynik() -> koordynator! {wypiszwynik}. akcja({listaelementow, WynikSumy}) -> receive {zleczadanie, Zlecajacy, Argument} -> [ Element! { policz, Zlecajacy, Argument } Element <- ListaElementow], akcja({listaelementow,wyniksumy}); {dodajdolisty, IdZadania, NazwaZadania } -> io:format("koordynator: Dodano nowe zadanie do listy: ~s~n",[nazwazadania]), akcja({[idzadania ListaElementow],WynikSumy}); {wynik,nazwazadania, Wartosc} -> erlang:display(["koordynator: zadanie o nazwie ",NazwaZadania, "zakonczylo prace. Otrzymany wynik = ",Wartosc]), akcja({[listaelementow],wyniksumy+wartosc}); {wypiszwynik} -> erlang:display(["koordynator: Suma z zadan = ",WynikSumy]), akcja({listaelementow,wyniksumy}) end. Listing 4 Kod modułu zadania napisany w języku Erlang z wykorzystaniem mechanizmu aktorów. -module(zadanie). -export([akcja/1]). -export([utworz/1]). utworz(nazwazadania) -> Pid = spawn(fun() -> akcja(nazwazadania) end), koordynator:dodajzadanie(pid,nazwazadania), Pid. akcja(nazwazadania) -> receive { policz, NazwaZlecajacego, ArgumentZadania } -> erlang:display([nazwazadania,"otrzymalem zlecenie wykonania zadania od: ",NazwaZlecajacego, " argument: ",ArgumentZadania]), erlang:display([nazwazadania,"wykonuje bardzo trudne zadanie..."]), Funkcja = fun(x) -> X * X end, Wartosc = Funkcja(ArgumentZadania), erlang:display([nazwazadania,"zakonczylem wykonywanie zadania. zwracam wynik = ",Wartosc]), koordynator! {wynik, NazwaZadania, Wartosc }, akcja(nazwazadania) end. 8

Listing 5 Wykorzystanie zaimplementowanych modułów: koordynatora i zadania Erlang R13B03 (erts-5.7.4) [smp:2:2] [rq:2] [async-threads:0] Eshell V5.7.4 (abort with ^G) 1> cd("e:/ftims/9 sem/pp/erlang/obliczenia"). E:/FTIMS/9 sem/pp/erlang/obliczenia ok 2> c(koordynator). {ok,koordynator} 3> c(zadanie). {ok,zadanie} 4> koordynator:utworz(). true 5> Z1 = zadanie:utworz("zad1"). KOORDYNATOR: Dodano nowe zadanie do listy: Zad1 <0.47.0> 6> Z2 = zadanie:utworz("zad2"). KOORDYNATOR: Dodano nowe zadanie do listy: Zad2 <0.49.0> 7> Z3 = zadanie:utworz("zad3"). KOORDYNATOR: Dodano nowe zadanie do listy: Zad3 <0.51.0> 8> koordynator:wykonajzadania(10). ["Zad3","Otrzymalem zlecenie wykonania zadania od: ","SERWER"," argument: ",10] ["Zad3","Wykonuje bardzo trudne zadanie..."] ["Zad3","Zakonczylem wykonywanie zadania. zwracam wynik = ",100] ["Zad2","Otrzymalem zlecenie wykonania zadania od: ","SERWER"," argument: ",10] ["Zad2","Wykonuje bardzo trudne zadanie..."] ["Zad2","Zakonczylem wykonywanie zadania. zwracam wynik = ",100] ["Zad1","Otrzymalem zlecenie wykonania zadania od: ","SERWER"," argument: ",10] ["Zad1","Wykonuje bardzo trudne zadanie..."] ["Zad1","Zakonczylem wykonywanie zadania. zwracam wynik = ",100] ["KOORDYNATOR: zadanie o nazwie ","Zad3"," zakonczylo prace. Otrzymany wynik = ",100] ["KOORDYNATOR: zadanie o nazwie ","Zad2"," zakonczylo prace. Otrzymany wynik = ",100] ["KOORDYNATOR: zadanie o nazwie ","Zad1"," zakonczylo prace. Otrzymany wynik = ",100] {zleczadanie,"serwer",10} 9> koordynator:wynik(). ["KOORDYNATOR: Suma z zadan = ",300] {wypiszwynik} 9

Listing 6 Klasa i singleton w Scali. Wywołanie metod. class C { def metoda_instancji = "Jestem metod instancji" } object C { def metoda_klasowa = "Jestem metod klasow" } (new C).metoda_instancji C.metoda_klasowa 2.2. Scala Scala (od Scalable Language) jest językiem programowania, w którym łączą się cechy programowania funkcyjnego i obiektowego. Od stylu programisty zależy, czego w jego kodzie będzie więcej. Istnieją implementacje Scali dla maszyny wirtualnej Javy (JVM) lub platformy.net, a także dla kilku innych platform. Składnia Scali jest prosta i większość jej funkcjonalności jest oparta o biblioteki, a nie o konstrukcje składniowe. Dzięki temu Scala nie ma żadnych ograniczeń rozwojowych i doskonale się skaluje - w końcu właśnie po to została stworzona. Jest językiem, który można łatwo rozszerzać (tworząc nowe typy i obiekty, które wyglądają jak nowa składnia języka) i wygodnie pisać w nim współbieżne programy, które będą w pełni korzystać z wieloprocesorowych maszyn. Co więcej, w Scali można bez problemu wykorzystywać klasy napisane w Javie, co przy bogactwie bibliotek Javy, sprawia, że Scala jest jeszcze potężniejszym narzędziem. W Scali każda wartość jest obiektem i każda funkcja zwraca wartość, a co się z tym wiąże, funkcja też jest obiektem. Funkcje można przekazywać w parametrach innych funkcji czy konstruktorów, zagnieżdżać a nawet wykorzystywać w ich przypadku mechanizm dziedziczenia. Nawet operatory w Scali są funkcjami, czyli kiedy piszemy 3+4 to tak naprawdę jest to rozumiane jako 3.+(4). Na dodatek w przypadku metod przyjmujących jeden parametr, podczas wywołania można pominąć kropki i nawiasy. Zamiast pisać studentslist.add(student), można napisać studentslist add student, co przypomina naturalny język, w jakim mówimy na co dzień. Scala wprowadza podział na obiekty typu class i typu object. Class to zwykła klasa, której instancje możemy tworzyć w dowolnych ilościach i wywoływać metody na ich rzecz. Object to singleton, w programie powoływana jest tylko jedna instancja danego typu i na jej rzecz wywoływane są metody. Metody singletonu nazywają się metodami klasowymi. Listing 6 przedstawia deklaracje klasy i singletonu oraz wywołanie metody instancji i metody klasowej. Jak przy okazji widać, podczas tworzenia obiektu za pomocą konstruktora domyślnego, możemy pominąć nawiasy po nazwie typu. Na dodatek Scala jest językiem statycznie typowanym i ma rozbudowaną inferencję typów (tam, gdzie kompilator potrafi sam wywnioskować, jakiego typu są dane, to nie trzeba tego typu jawnie deklarować). Pełna deklaracja klasy i singletonu z listingu 6 jest przedstawiona na listingu 7. Jak widać, nawet średniki można pomijać, jeśli ktoś bardzo dba o swój mały palec prawej ręki. To samo tyczy się nawiasów w metodach nie przyjmujących żadnych 10

Listing 7 Deklaracja klasy i singletonu z listingu 6 z jawną deklaracją typów zwracanych przez funkcje. class C { def metoda_instancji(): String = return "Jestem metod instancji"; } object C { def metoda_klasowa(): String = return = "Jestem metod klasow"; } parametrów oraz słowa kluczowego return - zostanie zwrócona wartość ostatniego wyrażenia w deklaracji metody. Kolejnym przyjemnym aspektem Scali są tzw. traits. Można je uznać za bardzo zręczne połączenie javowych interfejsów i klas abstrakcyjnych. Trait nie może mieć konstruktora i nie można utworzyć jego instancji, jednak może posiadać zadeklarowane zmienne, częściową lub pełną implementację metod oraz podlega dziedziczeniu. Dzięki temu klasy mogą dziedziczyć po wielu traitach, ale nie występuje tutaj problem (znany z innych języków pozwalających na wielobazowe dziedziczenie) kolejności wywołania konstruktorów - bo traity nie mają konstruktorów. 2.2.1. Aktorzy w Scali Aktorzy w Scali są dostępni za pomocą biblioteki scala.actors. Jest to świetny przykład na łatwość rozszerzania składni Scali. Aktorzy są napisani w czystej Scali i załączeni jako biblioteka, bez żadnych zmian w samej czystej Scali. A korzysta się z nich tak, jakby rzeczywiście wchodzili w skład podstawowej składni Scali (do operowania na aktorach wykorzystuje się kilka nowych operatorów, których nie ma w czystej Scali, ale programista jakoś tego nie zauważa). Aby zostać aktorem, dana klasa musi dziedziczyć z traita Actor. Zawartość metody act() odpowiada za to, co będzie wykonywała klasa jako aktor. Komunikacja pomiędzy aktorami odbywa się za pomocą komunikatów. Aktor może posłać drugiemu aktorowi komunikat za pomocą operatora!. Może również od razu o oczekiwać na odpowiedź za pomocą operatora!?. W Scali istnieją dwa rodzaje aktorów: thread-based actors oraz event-based actors. Podstawy przedstawimy na przykładzie tych pierwszych, a potem omówimy różnice pomiędzy pierwszymi i drugimi. Listing 8 przedstawia przykład komunikacji pomiędzy aktorami. Posłuży on nam do przestawienia kilku aspektów na raz. Przy okazji można zobaczyć, że bezpośrednio w ciele klasy możemy pisać nie tylko deklaracje zmiennych, ale również instrukcje do wykonania - bez zamykania ich w ciało metody (widać to w singletonie TestAktorow). Powiedzmy też od razu, czym są klasy case. Są to klasy wykorzystywane na ogół do dopasowywania wzorców w Scali, aby tworzyć instancje takich klas, nie trzeba używać słowa kluczowego new. W modelu aktorów case classes wykorzystuje się jako komunikaty. Prześledźmy teraz, co po kolei stanie się w programie z listingu 8. Program rozpocznie się w singletonie TestAktorow dziedziczącym z traita Application. Najpierw tworzymy obiekty obu aktorów (w Scali są dwa podobne słowa kluczowe: val i var. Za pomocą val deklarujemy zmienne, które 11

Listing 8 Przykładowy program prezentujący komunikację pomiędzy thread-based actors. 1 import scala.actors.actor 2 import scala.actors.actor._ 3 4 case class LubieCie() 5 case class IluMaszFanow(pytajacy: Actor) 6 case class DajAutograf() 7 case class NieZaczepiajMnie() 8 9 class PierwszyAktor extends Actor { 10 var iloscmoichfanow = 0 11 12 def act() = { 13 while(true){ 14 receive{ 15 case LubieCie() => iloscmoichfanow += 1 16 case IluMaszFanow(pytajacy) => pytajacy! iloscmoichfanow 17 case _ => reply(niezaczepiajmnie()) 18 } 19 } 20 } 21 22 class DrugiAktor(gwiazda : Actor) extends Actor { 23 def act() ={ 24 gwiazda! LubieCie() 25 var iloscfanow : Int = gwiazda!? IluMaszFanow(self) 26 gwiazda! DajAutograf() 27 while(true){ 28 receive{ 29 case NieZaczepiajMnie() => exit() 30 } 31 } 32 } 33 } 34 35 object TestAktorow extends Application{ 36 val pierwszy: Actor = new PierwszyAktor 37 val drugi: Actor = new DrugiAktor(pierwszy) 38 39 pierwszy.start() 40 41 pierwszy! LubieCie() 42 43 drugi.start() 44 } 12

są niezmienne, czyli takiej zmiennej można przypisać wartość tylko raz. Za pomocą var deklarujemy prawdziwe zmienne, które mogą przyjmować różne wartości w czasie). Następnie startujemy pierwszego aktora (metoda start() dla typu Actor powoduje wywołanie metody act()). Wchodzi on w blok receive, w którym oczekuje na przychodzące do niego komunikaty. Linia 41 pokazuje, że obiekt nie będący aktorem może posłać aktorowi komunikat (jednak nie może żadnego komunikatu odebrać). Pierwszy aktor dopasuje komunikat LubieCie() w linii 15 i zwiększy o jeden licznik swoich fanów. Skończy przetwarzanie bloku receive i, na skutek pętli while, wejdzie ponownie w blok receive, oczekując na kolejny komunikat. Teraz startujemy drugiego aktora. Jego pierwszą akcją jest posłanie pierwszemu aktorowi (jego gwieździe ) wyrazów uznania. Pierwszy znowu zinkrementuje swój licznik fanów i znowu wejdzie w blok receive. W tym czasie drugi aktor zapyta pierwszego o ilość jego fanów. Wykorzystany tutaj został operator!?, który powoduje oczekiwanie na odpowiedź od odbiorcy i po jej otrzymaniu zwraca jej wartość. Pierwszy odeśle nadawcy (podanemu z parametrze case class IluMaszFanow(Actor)) ilość swoich fanów (widzimy, że nie tylko case class mogą być komunikatami), którą drugi przypisze sobie do zmiennej. Następnie drugi pośle pierwszemu komunikat DajAutograf() i wejdzie we własny blok receive, aby oczekiwać na odpowiedź. Pierwszy posiada akcję wykonywaną dla wszystkich wzorców, których dotąd nie dopasowano (wszystkie wzorce określa się znakiem _ ). W tej akcji odpowiada nadawcy komunikatem NieZaczepiajMnie() wykorzystując instrukcję reply() - zaraz wyjaśnimy czym ona jest, ale najpierw dojdźmy do końca programu. Drugi aktor oczekiwał w swoim bloku receive na komunikat NieZaczepiajMnie() i po jego otrzymaniu zakończył swoją pracę za pomocą instrukcji exit() (wszelkie komunikaty wysłane teraz do drugiego aktora już do niego nie dotrą). Drugi aktor nie miał akcji dla dowolnego komunikatu _. Jeśli dostałby jakiś inny komunikat (nie NieZaczepiajMnie()), zakończyłby blok receive bez wykonania żadnej akcji. Dlatego umieściliśmy blok receive w nieskończonej pętli while. Trzeba jeszcze dodać, że po wykonaniu wszystkich tych instrukcji nasz program wcale się nie zakończył. Drugi aktor wykonał instrukcję exit(), więc skończył swoje działanie, TestAktorów również wykonał wszystkie swoje instrukcje. Jednak pierwszy aktor nigdzie nie wykonał instrukcji exit(), i wykonuje blok receive w nieskończonej pętli, więc nadal będzie bez przerwy oczekiwał na komunikaty. Świadomie pozostawiliśmy go w stanie ciągłego nasłuchu. Instrukcja reply(). W Scali każdy komunikat zawiera wskazanie na swojego nadawcę. W Erlangu, jeśli potrzebujemy odesłać odpowiedź, musimy podać nadawcę jawnie w parametrze komunikatu, w Scali wskazanie na nadawcę jest zawsze załączone do komunikatu i dzięki temu na każdy komunikat można odesłać w odpowiedzi dowolny komunikat za pomocą instrukcji reply(). W programie z listingu 8 pokazaliśmy odpowiedź zarówno w stylu Erlanga, jak i z wykorzystaniem instrukcji reply(). 13

Event-based actors i thread-based actors. Ponieważ Scala działa na JVM, nie można było dla niej zaimplementować tak wysoce efektywnej współbieżności, jak w przypadku Erlanga (którego wirtualna maszyna ma lekkie wątki i efektywne metody ich przełączania). Aktorzy w Scali są powiązani z ciężkimi wątkami i zarządzaniem nimi przez JVM, co jest mało efektywne. I od teraz prosimy traktować ten akapit, jako domysły i niepewne rozważania, ponieważ nie mogliśmy znaleźć wiarygodnych źródeł, które jednoznacznie i przejrzyście wyjaśniłyby nam temat zarządzania wątkami w przypadku aktorów typu thread i aktorów typu event. Albo trafialiśmy na źródła mówiące o temacie bardzo ogólnikowo i często były sprzeczne z innymi takimi źródłami, albo próbowaliśmy przebrnąć przez notatki twórców modelu aktorów w Scali, które były utrzymane w tonie naukowego wywodu, czytało się je, jak serie dowodów matematycznych a na końcu i tak nie wynikało z nich nic przydatnego do krótkiego zdefiniowania różnic pomiędzy dwoma typami aktorów. Chodzi nam, oczywiście, o różnice w zarządzaniu wątkami, bo odnośnie samego wykorzystania tych aktorów źródła są akurat nie najgorsze. Po lekturze niektórych źródeł można dojść do wniosku, że oba typy aktorów operują na takiej samej puli wątków, jednak cała różnica polega na przełączaniu tych wątków. W przypadku thread-based actors każde przełączenie wątku wiąże się z jego pełnym zablokowaniem i wieloma operacjami w pamięci. Aktorzy typu event są w lekki sposób zawieszani (reprezentowani przez magiczne słowo closure ) w momencie, kiedy nie mogą wykonać dalszych obliczeń, bo np. czekają na komunikat i wznawiani w momencie wystąpienia odpowiedniego zdarzenia - czyli pojawienia się komunikatu w skrzynce danego aktora. Według innych źródeł aktorzy typu event w ogóle działają w tylko jednym wątku (bardzo często byli określani jako threadless), co ma w jakiś, najwyraźniej magiczny, sposób przyspieszać działanie w przypadku dużej liczby aktorów. Mówimy o magicznym sposobie, bo przecież nadal mamy tutaj czerpać korzyści ze skalowalności programu na wiele procesorów - a jak to osiągnąć przy jednym wątku? Dowiedzieliśmy się również o tym, że zarówno JVM, jak i wirtualna maszyna.net (CLR) bronią bezpośredniego dostępu do swojego stosu, niczym niepodległości. Powoduje to, że każda implementacja jakiegoś języka na te maszyny, która chciałaby w jakiś sposób przedefiniować stos, musi sobie ten stos emulować na stercie JVM (tudzież CLR). To powoduje obniżenie wydajności oraz problemy np. standardowych debuggerów Javy, dla których stos debugowanego programu jest tożsamy ze stosem JVM - a tu niespodzianka, nasz program korzysta ze stosu emulowanego na stercie... Darujmy więc może dalsze rozważania na temat tego, jak działają różne typy aktorów w Scali, potraktujmy to jako czarną skrzynkę (w której najlepiej krasnoludki dbają, żeby wszystko było dobrze, a programista zadowolony popijał kawę, napawając się niespotykanie szybkim działaniem swojego dzieła). Źródła są zgodne co do tego, kiedy używać aktorów typu event. Otóż, należy ich używać przede wszystkim wtedy, kiedy wielka ilość aktorów (a co za tym idzie, ciężkich wątków) może doprowadzić do poważnego spadku wydajności (ze względu na sposób zarządzania wątkami przez JVM) lub wręcz kiedy braknie zasobów dla kolejnych aktorów. Wtedy z pomocą przychodzą nam aktorzy typu event, którzy są uważani za lżejsze rozwiązanie. Niektórzy 14

Listing 9 Event-based actors - NIEPOPRAWNA konstrukcja... react{ case Komunikat() => println("dostalem komunikat") } println("wyszedlem z bloku react")... Listing 10 Event-based actors - poprawna konstrukcja wykorzystująca rekurencję... def nasluch() = { react{ case Komunikat() => println("dostalem komunikat") nasluch() } }... nawet uważają, że użycie aktorów typu event daje niesamowity wzrost wydajności względem aktorów typu thread. W naszym wypadku było jednak całkiem odwrotnie, ale nie uprzedzajmy faktów - ten temat poruszymy w kolejnych sekcjach. Jeszcze tylko słowo o ograniczeniach i składni wykorzystującej event-based actors. Jeśli chodzi o składnię, to najkrócej mówiąc, zastępujemy blok receive blokiem react i już mamy aktora opartego na zdarzeniach. Jednak są właśnie pewne ograniczenia. Aktor, który wszedł w blok react, nie może już przekazać sterowania poza ten blok (z bloku react się nie wraca). Dlatego niepoprawna jest konstrukcja taka jak na listingu 9 Z tego samego powodu nie możemy zamknąć bloku react w nieskończonej pętli while. Jeśli chcemy, żeby po odebraniu jednego komunikatu nasz aktor dalej oczekiwał na kolejne, musimy użyć specjalnie do tego przygotowanej pętli loop (umieszczamy ją zamiast pętli while(true)), albo za pomocą wywołania jakieś funkcji (np. rekurencyjnego wywołania tej samej funkcji) zapewnić wejście w kolejny blok react (pokazuje to listing 10). 2.3. Uzasadnienie wyboru języka do rozwiązania problemu praktycznego Po tym, co napisaliśmy o aktorach w Scali, lekkim zaskoczeniem może być, że jednak właśnie Scalę wybraliśmy do dalszej pracy. Jednak jest ku temu kilka powodów: Scala pozwala programować nie tylko funkcyjnie, ale również imperatywnie. Dzięki temu mogliśmy zmniejszyć szok związany z przesiadką na programowanie funkcyjne. W razie braku pomysłu, jak coś rozwiązać funkcyjnie, można to napisać w standardowy, imperatywny sposób i też będzie działało. Jak się potem okazało, niektóre fragmenty kodu zaimplementowane imperatywnie przepisywaliśmy potem na styl funkcjonalny - kiedy pojawiły się pomysły na takie rozwiązania i chcieliśmy poćwiczyć nowy styl. Erlang skazałby nas na pisanie całej aplikacji w czysto funkcyj- 15

ny sposób. Ciężko jest sobie wyobrazić implementację wielowarstwowego perceptronu bez wykorzystania żadnych zmiennych o zasięgu większym niż jedna funkcja. W Scali można korzystać z klas i bibliotek napisanych w Javie. Dzięki temu mieliśmy dostęp do bogatych bibliotek standardowych Javy, a nawet mogliśmy sami napisać w Javie fragmenty aplikacji, które nie wymagały współbieżności - np. wczytywanie i parsowanie plików tekstowych. Co prawda, nie napisaliśmy jednak w Javie żadnego kawałka naszego programu, ale wykorzystaliśmy javową bibliotekę Apache Commons CLI do parsowania linii poleceń. W Erlangu nawet takie wczytywanie musielibyśmy napisać funkcyjnie (co ciężko nam sobie nawet wyobrazić) i prawdopodobnie nie udałoby nam się znaleźć odpowiedniej biblioteki, która parsowałaby polecenia za nas. A nawet jeśli, to byłaby to dla nas nowość, a z biblioteką Commons CLI mieliśmy już do czynienia. Architektura aplikacji w Scali jest podobna do architektury Javy. Też mamy biblioteki, pakiety i nadal można utrzymywać javową zasadę: jedna klasa - jeden plik. Projekt scalowy w Eclipse do złudzenia przypomina projekt javowy. Nawet kolorowanie składni jest podobne (to prawda, że to akurat można sobie samemu ustawić, ale w przypadku Scali mamy takie kolorowanie od zainstalowania wtyczki). Zrozumienie erlangowych modułów sprawiło nam nieco trudności. Sam plik z kodem danego modułu również wygląda nieznajomo. W Scali mamy programowanie obiektowe bardzo przypominające Javę. Wystarczy porównać załączone przez nas listingi Erlanga i Scali, żeby zauważyć, które wyglądają przyjemniej i znajomo - oczywiście, zakładając, że patrzy na nie programista Java. Mieliśmy problemy ze zmuszeniem do pracy eclipsowej wtyczki dla Erlanga. Udało się to tylko na jednym komputerze, po dłuższych bojach. Natomiast pisanie wszystkiego w notatniku i ręczne kompilowanie w środowisku Erlang/OTP jest nieprzyjemne i męczące. Zaś wtyczka Scali zadziałała od razu, nawet nie musieliśmy instalować ręcznie Scali (przez co nie mamy Scala REPL). Kompilacja i uruchamianie programów jest w niej szybkie i wygodne - jak w Javie. Programy erlangowe pod Eclipse również uruchamiają się wygodnie, ale, jak już wspominaliśmy, nie każdemu z nas dana była możność korzystania z tej wtyczki. Poza tym w Erlangu napotkaliśmy pewne dziwne problemy. Program potrafił po prostu nie działać, mimo, że wszystko wydawało się identyczne, jak w przykładzie. Czasami jakieś moduły nie były razem z innymi kompilowane od nowa i nasze poprawki w tych modułach nie były uwzględniane przy uruchamianiu programu (to bardzo frustrujące, jak program już naprawdę powinien działać, a nadal, pomimo kilku zmian kodu, mamy ten sam błąd). Tego powodu nie wypunktowaliśmy, ponieważ akurat równoważy się on z tym, że sprawdzanie składni w eclipsowej wtyczce dla Scali płatało nam figle - Eclipse podkreślał kod jako błędny, ale program się kompilował i - co najdziwniejsze - działał poprawnie. Zdarzało się też, że program w Scali po prostu się zatrzymywał. Nie było żadnego wyjątku, program się nie kończył, po prostu się zawieszał. Po kilku przeprawach z debuggerem doszliśmy do wniosku, że Scala czasami po prostu połyka wyjątki. Podczas debugowania 16

bywaliśmy przenoszeni nagle do kodu bibliotek Scali, gdzie okazywało się, że jakiś wyjątek jest łapany i na tym kończy się jego obsługa. Wszystko byłoby w porządku, gdyby program dalej działał, a nie się zawieszał... Ale to już tylko takie uwagi, które zamieściliśmy tylko z powodu wrodzonego poczucia sprawiedliwości. Przedstawiliśmy wystarczająco dużo poważnych powodów, dla których wybraliśmy Scalę, a nie Erlanga i możemy teraz przejść do problemu praktycznego, jaki postanowiliśmy rozwiązać. 3. Problem praktyczny 3.1. Przedstawienie problemu Dość długo zastanawialiśmy się nad wyborem problemu, nad którym chcielibyśmy pracować. Przez nasze głowy przelatywało sporo pomysłów, ale tylko jeden z nich szczególnie nas zainteresował. Po kilku rozmowach i jednej burzy mózgów zdecydowaliśmy, że w ramach pracowni problemowej będziemy zajmowali się implementowaniem współbieżnej, rozproszonej sieci neuronowej opartej o wielowarstwowy perceptron z uczeniem za pomocą wstecznej propagacji błędu. Za wyborem tego zagadnienia przemawiało kilka argumentów. Po pierwsze z sieciami neuronowymi mieliśmy wszyscy już styczność i rozumieliśmy ich działanie. Pozwoliło nam to skupić się na nowych problemach, które napotkaliśmy podczas pisania programu, takich jak choćby synchronizacja propagacji sygnałów przez neurony. Po drugie uznaliśmy, że bardzo ciekawym doświadczeniem będzie pisanie czegoś, co działa w sposób o wiele bardziej zbliżony do ludzkiego mózgu niż cokolwiek nad czym pracowaliśmy do tej pory. Mamy tu na myśli to, że poszczególne neurony nie są świadome tego co w tej chwili robią. Wykonują tylko część zadania, które zyskuje sens dopiero jako całość. Kolejnym argumentem, który przeważył szalę było przyspieszenie procesu nauki. Wiadomym jest, że nauka prostych wzorców (z odpowiednimi parametrami) nie zajmuje sieci neuronowej wiele czasu, ale kiedy chcielibyśmy, aby to narzędzie mogło być wykorzystane do trudniejszych zadań, to proces nauki wymaga czasu. Z tego powodu warto jest skorzystać z technik, które pozwolą przyspieszyć naukę. Współbieżne wykonywanie obliczeń w czasach dynamicznego rozwoju procesorów wielordzeniowych jest godne zainteresowania i zyskuje coraz większą popularność. Jeszcze ciekawsze jest rozproszenie obliczeń po wielu maszynach. Sieć maszyn realizujących obliczenia neuronowe, w szczególności, gdy każdy neuron byłby na oddzielnej maszynie, można by utożsamiać właśnie z mózgiem. 3.2. Architektura rozwiązania 3.2.1. Budowa sieci Nasze rozwiązanie ma strukturę hierarchiczną. Najwyżej jest klasa Siec, a najniżej Neuron. Sieć jest aktorem pozwalającym na uczenie sieci i testowanie wzroców. Do tego celu wykorzystuje aktorów KoordynatorUczenia i KoordynatorTestujący. Koordynatorzy z kolei komunikują się z obiektami ZarzadcaWyjscia i ZarządcaWejscia, którzy stanowią pomost pomiędzy sie- 17

cią połączonych obiektów klasy Neuron, a koordynatorami. Taki podział na różne poziomy abstrakcji miał na celu uproszczenie mechanizmu sterowania siecią jako całością poprzez przesyłanie komunikatów. Przepływ komunikatów w celu nauczenia pojedynczego wzorca przedstawiony jest na rysunku 1. 3.2.2. Klasy i komunikaty Poniżej przedstawiamy najważniejsze klasy składające się na rozwiązanie naszego problemu. Polaczenie odpowiedzialność: Przechowuje wagę połączenia między dwoma konkretnymi neuronami oraz ostatnią jej zmianę na potrzebę nauki z momentum. konstruktor: Polaczenie(Actor wejsciowy, Actor wyjsciowy, waga) pola: waga ostatniprzyrost aktorwejsciowy aktorwyjsciowy metody: zmienwage(zmiana:double) ustawwage(waga:double) Neuron (Actor) odpowiedzialność: Podstawowy element liczący w sieci. Sumuje sygnał z wejść i przesyła dalej po przetworzeniu przez funkcję aktywacji. Oblicza i propaguje wstecz błąd. pola: Polaczenie[] polaczeniawejsciowe Polaczenie[] polaczeniawyjsciowe funkcjaaktywacji id blad sygnal komunikaty wychodzące: ZarzadcaWyjscia <- Sygnal(int id, double wartosc) Neuron <- Sygnal(int id, double wartosc) ZarzadcaWejscia <- Blad(id, delta) Neuron <- Blad(id, delta) 18

Neuron <- PoprawWage(int id, ni, mi, wyjsciepoprzedniego) ZarzadcaWyjscia <- PoprawWage(int id, ni, mi, wyjsciepoprzedniego) komunikaty przychodzące: ZarzadcaWejscia -> Sygnal(int id, double wartosc) Neuron -> Sygnal(int id, double wartosc) ZarzadcaWyjscia -> Blad(id, delta) Neuron -> Blad(id, delta) Neuron -> PoprawWage(int id, ni, mi, wyjsciepoprzedniego) ZarzadcaWejscia -> PoprawWage(int id, ni, mi, wyjsciepoprzedniego) Warstwa odpowiedzialność: Grupuje neurony w warstwy Pozwala na ich łatwe łączenie i budowanie sieci o określonej strukturze. konstruktor: Warstwa(int ileneuronow, funkcja_aktywacji) pola: następna warstwa metody: polacz(warstwa kolejnawarstwa) polacz(warstwa kolejnawarstwa, (Int,Int)=>Double) zbierzwagi() ustawwagi((int,int)=>double) ustawbiasy((int)=>double) Siec (Actor) odpowiedzialność: Zarządza całością. Wczytuje i podaje koordynatorom wzorce. konstruktor: Siec(rozmiarWektoraWejsciowego, warstwawejsciowa) pola: ZarzadcaWejscia ZarzadcaWyjscia warstwawejsciowa metody: init() startsieci() stop() 19

test(wzorzec) ucz(tablica_wzorcow_uczacych, ni, mi, limitepok, dopuszczalnyblad) komunikaty wychodzące: KoordynatorTestujacy <- Testuj(wektor) KoordynatorUczacy <- Ucz(wektor, ni, mi) komunikaty przychodzące: KoordynatorTestujacy -> reply(wektorywynikowy) KoordynatorUczacy -> reply(bladwzorca) ZarzadcaWejscia (Actor) odpowiedzialność: Podaje sygnał neuronom warstwy ukrytej (pełni rolę warstwy wejściowej kopiujacej) Informuje koordynatora uczącego o policzeniu błędów w neuronach. Wysyła do neuronów komunikat nakazujący poprawę wag. konstruktor: ZarzadcaWejscia(warstwaWejsciowa, rozmiarwektorawejsciowego) pola: Polaczenie[] polaczenia bool[] potwierdzeniabledu WektorWzorca[] wzorzec Actor koordynator komunikaty wychodzące: KoordynatorTestujacy <- reply(jesteskoordynatorem()) KoordynatorUczacy <- reply(jesteskoordynatorem()) Neuron <- Sygnal(int nr_wejscia, double wartosc) KoordynatorUczacy <- PoliczonoBledy() Neuron <- PoprawWage(int id, ni, mi, wyjsciepoprzedniego_czyli_pole_wzorca) komunikaty przychodzące: KoordynatorTestujacy -> JestemKoordynatorem(Actor) KoordynatorUczacy -> JestemKoordynatorem(Actor) KoordynatorTestujacy -> Wzorzec(wzorzec) KoordynatorUczacy -> Wzorzec(wzorzec) Neuron -> Blad(id, delta) KoordynatorUczacy -> PoprawWagi(ni, mi) ZarzadcaWyjscia (Actor) odpowiedzialność: Odbiera sygnał wyjściowy od ostatniej warstwy neuronów i przekazuje do koordynatorów. 20

Podaje błąd neuronom wyjściowym poprzez komunikat propagacji błędu. Informuje koordynatora uczącego o zakończeniu korekcji wag neuronów. konstruktor: ZarzadcaWyjscia(warstwaWyjsciowa) pola: Polaczenie[] polaczenia bool[] potwierdzeniasygnalow bool[] potwierdzeniapoprawywag double[] wynikisieci Actor koordynator komunikaty wychodzące: KoordynatorTestujacy <- reply(jesteskoordynatorem()) KoordynatorUczacy <- reply(jesteskoordynatorem()) KoordynatorTestujacy <- Wynik(wzorzecWynikowy) KoordynatorUczacy <- Wynik(wzorzecWynikowy) Neuron <- Blad(id, delta) KoordynatorUczacy <- SkorygowanoWagi() komunikaty przychodzące: KoordynatorTestujacy -> JestemKoordynatorem(Actor) KoordynatorUczacy -> JestemKoordynatorem(Actor) Neuron -> Sygnal(int id, double wartosc) KoordynatorUczacy -> PropagujBlad(wzorzecOczekiwany) Neuron -> PoprawWage(int id, ni, mi, wyjsciepoprzedniego) KoordynatorTestujacy(Actor) odpowiedzialność: Przeprowadza proces propagacji wzorca bez uczenia. konstruktor: KoordynatorTestujacy(ZarzadcaWejscia, ZarzadcaWyjscia) komunikaty wychodzące: ZarzadcaWejscia <- JestemKoordynatorem(self) ZarzadcaWyjscia <- JestemKoordynatorem(self) ZarzadcaWejscia <- Wzorzec(wzorzec) Siec <- reply(wzorzecwynikowy) komunikaty przychodzące: Siec -> Testuj(wzorzec) ZarzadcaWejscia -> reply(jesteskoordynatorem() ) ZarzadcaWyjscia -> reply(jesteskoordynatorem() ) ZarzadcaWyjscia -> Wynik(wzorzecWynikowy) 21

Listing 11 Tekst pomocy stworzonej aplikacji. usage: java -jar program [-e <liczba>] [-i <liczba>] [-l <sciezka>] [-m <liczba>] [-n <liczba>] [-p] [-s <sciezka>] [-t <sciezka>] [-u <sciezka>] [-z <sciezka>] -e,--blad <liczba> Warunek stopu nauki, maksymalny bledu (domyslnie e=0) -i,--iteracje <liczba> Warunek stopu nauki, ilosc iteracji (domyslnie i=100) -l,--loguj <sciezka> Logowanie do pliku dzialania sieci -m,--mi <liczba> Wspolczynnik momentum nauki (domyslnie m=0) -n,--ni <liczba> Wspolczynnik nauki sieci (domyslnie n=0.1) -p,--pomoc Wyswietla pomoc -s,--siec <sciezka> Plik z siecia do wczytania -t,--test <sciezka> Testowanie sieci danymi z pliku -u,--ucz <sciezka> Uczenie sieci danymi z pliku -z,--zapisz-siec <sciezka> Plik do zapisania sieci KoordynatorUczacy (Actor) odpowiedzialność: Przeprowadza proces propagacji wzorca wraz z nauką; konstruktor: KoordynatorUczacy(ZarzadcaWejscia, ZarzadcaWyjscia) komunikaty wychodzące: ZarzadcaWejscia <- JestemKoordynatorem(self) ZarzadcaWyjscia <- JestemKoordynatorem(self) ZarzadcaWejscia <- Wzorzec(wzorzec) ZarzadcaWyjscia <- PropagujBlad(wzorzecOczekiwany) ZarzadcaWejscia <- PoprawWagi(ni,mi) Siec <- reply(bladwzorca) komunikaty przychodzące: Siec -> Ucz(wzorzec, ni, mi) ZarzadcaWejscia -> reply(jesteskoordynatorem() ) ZarzadcaWyjscia -> reply(jesteskoordynatorem() ) ZarzadcaWyjscia -> Wynik(wynik) ZarzadcaWejscia -> PoliczonoBledy() ZarzadcaWyjscia -> SkorygowanoWagi() 3.3. Opis interfejsu Programem steruje się z wiersza poleceń. Aby uzyskać opis możliwych parametrów należy wywołać program z parametrem -p. Na listingu 11przedstawiono efekt takiego wywołania. Możliwe są dwa tryby pracy aplikacji: uczenie(-u) i testowanie(-t). Możemy podać dwa parametry określające warunek stopu (-e, -i), spełnienie któregokolwiek z nich powoduje zakończenie procesu nauki. W celu uczenia sieci musimy podać plik ze struktura sieci (-s) oraz plik z wzorcami. Nauczoną sieć można zapisać do pliku (-z). Format pliku z siecią do wygenerowania przestawiony jest na listingu 12. Pole liczba_wejść_sieci musi byc równe rozmiarowi wektora wejściowego, który ma być podany sieci. W miejscu oznaczonym jako liczba_neuronów_w_warstwie 22

Rysunek 1. Diagram prezentujący przepływ sterowania podczas nauki pojedynczego wzorca 23

Listing 12 Format pliku opisujący strukturę sieci do wygenerowania <l i c z b a _ w e j _ s i e c i > warstwa<liczba_neuronów_w_warstwie >:<funkcja_ aktywacji >:R<liczba_ wej > Listing 13 Przykładowy plik ze strukturą sieci dla rozpoznawania cyfr z losowanymi wagami. <20> warstwa <15>:<LOGISTYCZNA>:R<20> warstwa <10>:<LOGISTYCZNA>:R<15> należy podać liczbę neuronów do wygenerowania. Pole funkcja_aktywacji musi posiadać jedną z dwóch wartości: LOGISTYCZNA - w przypadku, gdy funkcją aktywacji w neuronach warstwy ma być funkcja logistyczna lub IDENTYCZNOSCIOWA - jeśli ma nią być funkcja identycznościowa. Liczba_wejść mówi ile dla każdego neuronu należy wygenerować wejść o losowej wadze. Wartość tego pola powinna odpowiadać ilości neuronów w warstwie poprzedniej. Przykład poprawnego pliku w tym formacie można zobaczyć na listingu 13. Możliwe jest również wczytywanie sieci o ustalonych wagach w neuronach. Umożliwia to wczytywanie nauczonych i zapisanych wcześniej sieci. Zawartość pliku z siecią w takim formacie przedstawiona została na listingu 14. Format podobny jest do wcześniej opisanego z tą różnicą, że nie występuje człon :R<liczba_wejść>, gdyż wagi nie są losowane, lecz podane w pliku. Opis kolejnych neuronów w warstwie ma postać: [waga_biasu]:[waga_wejscia(1);waga_wejscia(2);..;waga_wejscia(n)] Zawartość przykładowego pliku z wzorcami można zobaczyć na listingu. Wzorce umieszczane są w kolejnych liniach. Przed znakiem : podane są wartości wejścia sieci, natomiast w drugiej części oczekiwane wyjście sieci. 3.4. Prezentacja działania Zbudowaliśmy sieć o 20 wejściach, 15 neuronach ukrytych z logistyczną funkcją aktywacji oraz 10 neuronach wyjściowych z logistyczną funkcją aktywacji. Do nauki tej sieci wykorzystaliśmy wzorce reprezentujące cyfry w następującym formacie prezentowanym na rysunku 2. Przekształciliśmy je na wektory 20-elementowe, w których wartości 0 odpowiadają pustym polom w matrycach, a wartości 1 polom pełnym. Wektory wejściowe dla wszystkich dziesięciu cyfr prezentuje listing 15. Wykonaliśmy kilka prób po 1000 epok każda. W najlepszej udało nam się nauczyć sieć z końcowym błędem równym 0,0044. Trwało to ok. 90 se- Rysunek 2. Przykładowe wzorce cyfr (0, 2, 4, 6, 8) 24

Listing 14 Przykład sieci z określonymi wagami. <4> warstwa<5>:<logistyczna> [ 1. 342294741136212]:[ 1. 917032346612691; 1. 8490295512233808; 2. 9981651357971195 ; 5. 9 8 0 3 0 1 6 8 8 7 2 7 4 3 8 5 ] [ 1. 1 0 4 3 8 7 8 5 2 6 1 9 2 9 8 ] : [ 6. 8 4 6 3 3 3 7 0 8 9 4 0 2 5 8 ; 4. 1 7 8 3 8 6 0 4 6 5 1 4 5 2 4 6E 4 ; 3. 7953895061270764; 4. 057602167830132] [ 1. 9 2 2 6 2 1 0 5 6 7 0 5 8 5 9 3 ] : [ 1. 7 6 9 8 6 2 4 6 0 1 5 2 7 5 7 5 ; 2. 3 2 0 5 0 9 7 5 7 5 2 7 0 8 1 4 ; 2. 5 8 6 5 4 0 0 4 1 8 8 1 6 2 4 ; 5. 669931090849579] [ 0. 23389647671487618]:[ 4. 785751893055408; 5. 231403621472267; 2. 9546134394722547 ; 6. 2 3 8 0 9 7 8 9 3 5 9 0 0 2 2 ] [ 0. 4622113862290239]:[ 5. 1 83115789853775; 4. 499212303247797; 4. 9 980985951716965 ; 4. 1 7 9 2 7 9 9 6 7 2 5 1 2 2 5 ] warstwa<4>:<logistyczna> [ 1. 4483364399297056]:[ 1. 083054713436979; 3. 8698795307109166; 0. 551878391553109 ; 1. 9276697905624054; 2. 737168074065042] [ 0. 9267264878295934]:[ 1. 3589439846041473; 0. 7542583138720378; 0. 058971008185474016 ; 3. 6484190588862404; 2. 6146152152558844] [ 0. 6011002614046471]:[ 1. 593137303295829; 2. 161588070732628; 0. 3984113844805032 ; 2. 034470704471459; 2. 8511400866235395] [ 1. 3776844608747894]:[ 1. 7666984154716596; 2. 7918720119472793; 2. 8644554170447476 ; 2. 8 8 5 3 4 4 5 1 7 2 0 8 1 9 6 ; 0. 9 4 9 6 3 1 9 3 1 1 6 3 7 1 2 ] Listing 15 Wektory wejściowe i wektory oczekiwane dla cyfr od 0 do 9 1 (0,1,1,0,1,0,0,1,1,0,0,1,1,0,0,1,0,1,1,0):(1,0,0,0,0,0,0,0,0,0) 2 (0,0,0,1,0,0,1,1,0,1,0,1,0,0,0,1,0,0,0,1):(0,1,0,0,0,0,0,0,0,0) 3 (0,1,1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,1,1,1):(0,0,1,0,0,0,0,0,0,0) 4 (0,1,1,0,1,0,0,1,0,0,1,0,1,0,0,1,0,1,1,0):(0,0,0,1,0,0,0,0,0,0) 5 (1,0,0,1,1,0,0,1,1,1,1,1,0,0,0,1,0,0,0,1):(0,0,0,0,1,0,0,0,0,0) 6 (1,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,0,1,1,0):(0,0,0,0,0,1,0,0,0,0) 7 (0,1,1,0,1,0,0,0,1,1,1,0,1,0,0,1,0,1,1,0):(0,0,0,0,0,0,1,0,0,0) 8 (1,1,1,1,0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0):(0,0,0,0,0,0,0,1,0,0) 9 (0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0):(0,0,0,0,0,0,0,0,1,0) 10 (0,1,1,0,1,0,0,1,0,1,1,1,0,0,0,1,0,1,1,0):(0,0,0,0,0,0,0,0,0,1) 25