Teoria Obliczeń i Złożoności Obliczeniowej
Transkrypt
Teoria Obliczeń i Złożoności Obliczeniowej
Teoria Obliczeń i Złożoności Obliczeniowej Laboratorium: Analiza programu genetycznego. Analiza złożoności czasowej i pamięciowej algorytmu iteracyjnego. Jedną z bardzo interesujących prób podejścia do problemu optymalizacji są algorytmy genetyczne. Znajdują one wiele zastosowań, zwłaszcza w przypadkach nierozwiązywalnych metodą "brute force", gdy przejrzenie całej przestrzeni rozwiązań jest niewykonalne ze względu na złożoność obliczeń. Realne przykłady zastosowań to problemy kombinatoryczne. Klasycznym takim zadaniem jest właśnie problem komiwojażera. Dla kilku miast problem jest prosty, natomiast bardzo się komplikuje przy większej ich ilości. Problem komiwojażera jest NP-zupełny. Dodanie do jednego miasta komplikuje problem wielokrotnie, proporcjonalnie do liczby wszystkich miast. Nie liczymy na to, że algorytm genetyczny będzie nam znajdował rozwiązanie optymalne. W obliczu NP-zupełności problemu chcemy za pomocą algorytmu genetycznego znaleźć rozwiązanie możliwie jak najlepsze. W algorytmach genetycznych definiujemy środowisko, w którym żyje pewna populacja osobników. Każdy z osobników ma przypisany pewien zbiór informacji stanowiących jego genotyp, będący podstawą do utworzenia fenotypu. Fenotyp to zbiór indywidualnych cech osobnika. Fenotyp to złożona struktura, mniej lub bardziej przystosowana do rozwiązywania postawionych przez otoczenie problemów, i w zależności od stopnia tego przystosowania ma większą lub mniejszą możliwość przekazywania swych cech kolejnym pokoleniom. Genotyp opisuje proponowane rozwiązanie problemu a funkcja przystosowania ocenia jak dobre jest to rozwiązanie. Genotyp składa się z chromosomów, gdzie zakodowany jest fenotyp i ewentualnie pewne informacje pomocnicze dla algorytmu genetycznego. Jak działają algorytmy genetyczne w praktyce? Najczęściej działanie algorytmu przebiega następująco: losowana jest pewna populacja początkowa (selekcja) i poddawana jest ocenie. Najlepiej przystosowane osobniki biorą udział w procesie reprodukcji. Ich genotypy są ze sobą kojarzone poprzez złączanie genotypów rodziców (krzyżowanie). Następnie przeprowadzana jest mutacja, czyli wprowadzenie drobnych losowych zmian. Rodzi się drugie pokolenie które stanowi bazę do wykonania analogicznego, kolejnego kroku. I tak iteracyjnie algorytm zmierza do rozwiązania. W skrócie: operator krzyżowania ma za zadanie łączyć w różnych kombinacjach cechy pochodzące z różnych osobników populacji (optymalnie jest gdy są to najlepsze cechy osobników), zaś operator mutacji ma za zadanie zwiększać różnorodność tych osobników. Ideą ewolucji jest utrzymywanie dużego i różnorodnego zbioru osobników (populacji), który ewoluuje jako całość, a sam proces ewolucji opiera się na prostych zasadach wymiany cech między osobnikami. Podobnie jak w środowisku naturalnym słabe osobniki giną, a silne żyją przekazując swoje najlepsze cechy potomstwu. Reprezentacja genetyczna problemu komiwojażera Fenotypem w naszym zadaniu jest mapa z zaznaczonymi drogami, które dają przykładową drogę komiwojażera. Możemy ją narysować w postaci grafu z jednym, dużym cyklem spinającym wszystkie miasta. Fenotyp, czyli pewne rozwiązanie naszego problemu jest dozwolony, jeśli spina wszystkie miasta jednym cyklem, ale każde miasto tylko raz. Funkcją oceny jest po prostu droga, czyli długość całego cyklu w grafie. Jak widać, funkcja oceny jest bardzo prosta: po prostu liczymy długość całej drogi i otrzymujemy konkretną liczbę. Im mniejszą, tym lepiej. Złożoność obliczeniowa algorytmów genetycznych Działanie algorytmów genetycznych zostało wyjaśnione powyżej. Ponieważ program iteracyjnie przetwarza kolejne populacje trudno przewidzieć czas zakończenia algorytmu. Zwykle przyjmujemy pewne kryterium zakończenia iteracji. W tym wypadku możemy kończyć proces obliczeniowy gdy algorytm zbiegł się do jakiegoś rozwiązania i kolejne iteracje niewiele wnoszą do wyniku (nie poprawiają już osiągniętej długości trasy). Czas w jakim algorytm osiągnie rozwiązanie zależy od wielu czynników: – przede wszystkim od szybkości z jaką komputer liczy jeden krok iteracji (o tym później...) – populacja początkowa – zależnie od tego czy generowana jest losowo, czy też przez szybkie ale mało wydajne algorytmy typu „najbliższe miasto”, na zasadnicze znaczenie w procesie obliczeniowym – parametry operatorów genetycznych – w zależności od różnych ustawień prawdopodobieństwa krzyżowania i mutacji program może sobie dobrze radzić z początkową fazą obliczeń (gdzie dużo jest jeszcze do zrobienia) lub w fazie końcowej (ale kluczowej!) gdzie likwidowane są drobne ale nieoptymalne części tras. Mówiąc o czasowej złożoności obliczeniowej warto zastanowić się nad złożonością kolejnych kroków iteracji, gdyż to czas ich wykonania wpływa bezpośrednio na czas zakończenia obliczeń. Najważniejszymi czynnikami decydującymi o złożoności czasowej jednego kroku programu są: – ilość miast – złożoność problemu w pierwszej kolejności zależy oczywiście od ilości miast – wielkość populacji – im większa populacja tym większy czas obliczeń – jakość generatora liczb losowych - ponieważ operacje krzyżowania i mutacji korzystają z wartości losowych, kluczową rolę odgrywa jakość generatora liczb pseudolosowych – parametry operatorów genetycznych – wymienione już wcześniej, ale tym razem w innym sensie, odgrywają kluczową rolę w złożoności obliczeniowej jednego kroku algorytmu, duże prawdopodobieństwo mutacji znacznie zwiększa potrzebę generowania liczb losowych, natomiast częste wykonywanie krzyżowania znacząco zwiększa złożoność obliczeniową przez konieczność wykonywania operacji zamiany łańcuchów oraz również losowania osobników, w których to ta zamiana nastąpi. Złożoność pamięciowa zależy oczywiście od wielkości populacji i struktur danych użytych do reprezentowania osobników. Pewna dodatkowa ilość pamięci zapewne potrzebna jest dla sprawnego działania programu, ale ogólnie dla poprawnie napisanego programu złożoność pamięciowa będzie liniowo wzrastała wraz z wielkością populacji. Tak przedstawia się ogólnikowo kwestia złożoności. Dalsze spostrzeżenia przedstawione zostaną na przykładach. Analiza Spróbujemy przeanalizować problem korzystając z programu „Visual Genetic - TSP” i stwierdzić które parametry algorytmu genetycznego wpływają na szybkość osiągnięcia rozwiązania oraz jego jakość. 1. Populacja początkowa. Co wydaje się być oczywiste już na pierwszy rzut oka bardzo dużą rolę odgrywa początkowa populacja. Wygenerowanie jej losowo sprawia, że zaczynamy od zupełnie nie-optymalnego rozwiązania. Użycie prostego algorytmu takiego jak „najbliższe miasto” powoduje znaczne skrócenie obliczeń, gdyż od początku dysponujemy nienajgorszym rozwiązaniem. Algorytm genetyczny bierze je za punkt startowy i poprawia znanymi metodami. Oczywiście tracimy na początku czas na wygenerowanie trasy przy użyciu wybranego algorytmu, ale znacznie skraca to czas osiągnięcia rozwiązania. Test Liczba miast: 100 Populacja startowa: 10 osobników Prawdopodobieństwo krzyżowania: 0,6 Prawdopodobieństwo mutacji: 0,1 Wynik osiągnięty po 5000 iteracjach: (najbliższe miasto) (losowy rozkład) W pierwszym przypadku osiągnęliśmy trasę o długości 8095 która wygląda na dość optymalną. W drugim przypadku – 10546. To duża różnica, ale trzeba zauważyć, że w pierwszym przypadku już na starcie dysponowaliśmy trasą o długości około 8600. Prawie zawsze więc warto poświęcić trochę mocy obliczeniowej na wygenerowanie populacji początkowej. 2. Wielkość populacji. Krótki test doprowadził mnie do prostego wniosku: nie warto tworzyć dużych populacji. Kilka lub kilkanaście osobników to wystarczająca liczba. Poniżej widzimy wykresy dla obliczeń przeprowadzonych przy różnej wielkości populacjach: (100 osobników) długość trasy: 8264 (10 osobników) długość trasy: 8522 Jak widać dysponując populacją 100 osobników otrzymaliśmy wynik lepszy, ale kosztem dużych strat czasowych. Algorytm szukając rozwiązania 2 sposobem po kolejnych 2, 3 tysiącach iteracji poprawił wynik z 1 sposobu z czasem w dalszym ciągu lepszym niż pierwszy czas. Nic dziwnego gdyż w 1 przypadku program za każdym razem przetwarzał całą populację o liczebności 100 osobników. Tworzenie dużych populacji wydaje się więc być niekorzystne ze względu na czas obliczeń – lepiej wykonać po prostu więcej iteracji. Uwaga: Obserwowanie działającego programu może prowadzić do błędnych wniosków, gdyż program traci ogromną ilość czasu na odświeżanie wykresów po każdej iteracji! W dalszych testach będziemy więc posługiwać się wcześniejszymi spostrzeżeniami i używać populacji o wielkości 10 osobników tworzonej przez algorytm „najbliższe miasto”. 3. Prawdopodobieństwo krzyżowania. Krzyżowanie jest główną ideą algorytmów genetycznych, ale zmiana prawdopodobieństwa tego parametru nie odgrywa kluczowej roli. Co więcej Jak łatwo policzyć duże prawdopodobieństwo krzyżowania prowadzi do ogromnych narzutów czasowych! Operacje losowania osobników do skrzyżowania, losowania długości łańcuchów podlegających wymianie i przed wszystkim operacje zamiany tych łańcuchów są bardzo kosztowne. Współczynniki prawdopodobieństwa przeprowadzenia krzyżowania powinien więc w przypadku tego algorytmu być utrzymywany na raczej niskim poziomie. 4. Prawdopodobieństwo mutacji. Mutacja może znacznie przyczynić się do polepszenia rozwiązania jak i do jego zniszczenia. Z przeprowadzonych testów wynika, że mały współczynnik mutacji może prowadzić do sytuacji, gdzie algorytm utknie przy pewnej trasie i nie będzie mógł go już poprawić. Co więcej: niemożliwe jest w zasadzie prowadzenie obliczeń bez mutacji! Mutacja daje nam nowe rozwiązania, które ewentualnie mogą być poprawiane na drodze krzyżowania. Przy zbyt dużej mutacji wynik zatracany co na wykresie objawia się generowaniem absurdalnie długich tras. Współczynnik mutacji powinien być jednak odpowiednio wysoki, aby potrafił wyprowadzić populację z sytuacji w której utworzyło się wiele bardzo podobnych osobników. Zastosowana w programie forma mutacji to tzw. „mutacja inwersji”, która "obraca" dany odcinek trasy i jest dość skuteczna w przypadku rozwiązywania podobnych problemów. Wydaje mi się, że dość dobrym doborem parametrów do rozwiązywania problemu komiwojażera jest populacja 10 osobników, która krzyżuje się z prawdopodobieństwem 0,2, mutuje z prawdopodobieństwem 0,4, a populacja początkowa generowana jest przez szybki algorytm, taki jak „najbliższe miasto”. Dla układu miast jak wcześniej możemy dość szybko uzyskać optimum. Ostatecznie udało mi się uzyskać wynik 7893. Program posiada jeszcze wiele ciekawych opcji. Można na przykład przy krzyżowaniu zastosować heurystykę, ale odbija się to bardzo niekorzystnie na czasie obliczeń. Lepiej więc po prostu użyć prostszych metod, a wykonać więcej iteracji. To daje najlepsze rezultaty.