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.