Vypracoval Michal Turek, cvičení Po 12:45-14:15 (Petr Fišer)
Vlastní genetický algoritmus je relativně jednoduchý. Na začátku se inicializuje populace na náhodné hodnoty a poté se vstoupí do while cyklu. V něm se postupně kopírují nejlepší jedinci z předchozí populace, provádějí se rekombinace s mutacemi a z nejlepších výsledků se vytvoří nová populace. Cyklus se zastaví poté, co se určitý počet generací nemění nejlepší známé řešení problému. Aby se výsledek ještě vylepšil, je celý algoritmus spuštěn několikrát nezávisle na sobě a vybírá se nejlepší z nalezených lokálních maxim. Zdrojové kódy v jazyce C++: main.cpp, cknapsack.hpp, cknapsack.cpp, Makefile.
SolveKnapsack() { for(SeveralSeparateRuns) { InitializePopulation(); while(BestSolutionIsChanging()) { CopyBestFromPreviousPopulation(); Recombinations(); Mutations(); Selection(); } } DisplayBest(); }
Přesné chování genetického algoritmu lze upravit následujícími konstantami.
NUM_SEPARATE_RUNS definuje počet nezávislých spuštění genetického algoritmu nad danými daty, které jsou přidány kvůli alespoň částečnému odstranění problémů s lokálními maximy, přesnost výsledku se výrazně zvyšuje. Pokud se přesáhne STOP_BEST_NOT_CHANGED populací a nejlepší známé řešení se už dále nemění, je algoritmus ukončen.
NUM_COPIED_BEST_PARRENTS definuje, kolik nejlepších rodičů se automaticky zkopíruje do nově vytvářené populace. Mohli bychom se ocitnout v situaci, že po aplikování rekombinací a mutací získáme horší výsledek, než byl ten, ze kterého jsme vyšli. Zkopírování nejlepších rodičů zajistí, že v takovém případě populace tolik nezdegraduje.
POPULATION_SIZE udává velikost populace, nad kterou se provádí rekombinace a mutace, které se ukládají do pomocné populace o velikosti NEW_POPULATION_SIZE. Po aplikaci genetických operátorů se z ní vybírají nejlepší jedinci a vytváří se nová populace o původní velikosti.
NUM_MUTATIONS_IN_POPULATION obsahuje počet náhodných mutací, které se provedou při vytváření nové populace.
#define NUM_SEPARATE_RUNS 5 #define STOP_BEST_NOT_CHANGED 20 #define NUM_COPIED_BEST_PARRENTS 3 #define POPULATION_SIZE 20 #define NEW_POPULATION_SIZE 50 #define NUM_MUTATIONS_IN_POPULATION 5
Jistým problémem GA je, že i pro stejná vstupní data vrací při opakovaných spuštěních odlišná řešení, která mohou být i výrazně odlišná. Je to způsobeno tím, že se výchozí populace inicializuje na náhodné hodnoty a i selekce spolu s mutacemi se provádějí náhodně. Nemáme proto naprosto žádnou záruku, že se výsledné řešení bude blížit k optimálnímu nebo že se bude pohybovat v určitém rozsahu. Na druhou stranu níže uvedený graf nejlepší dosažené ceny v závislosti na vytvářených populacích ukazuje, že se ceny postupně vyvíjejí ke stále lepším. Pro výpočty a vynesení grafu byla použita následující instance problému:
Pozn.: Jsem si vědom toho, že linky nejsou v tomto grafu zrovna nejvhodnější, ale při vynášení jednotlivých bodů je výstup z gnuplotu hodně nepřehledný.
Jelikož je GA pro podobné instance problému velice rychlou metodou, je možné algoritmus spouštět vícekrát po sobě a poté uživateli zobrazit nejlepší z dosažených řešení. Jeden průchod pro výše uvedená nastavení a instanci problému, který se ukončil po 35. generaci, trval 4 ms (měřeno pomocí standardního programu time, položka user).
Při vypracovávání této práce jsem s překvapením zjistil, že naprogramovat si vlastní genetický algoritmus není vůbec složité. Velice mi při tom pomohl naprosto skvělý popis genetických algoritmů od Marka Obitka (http://cs.felk.cvut.cz/~xobitko/ga/), který jsem náhodou objevil při pročítání prezentace o genetických algoritmech z University of Extremadura (Spain).
Hlavním problémem při programování GA je správné nastavení konfiguračních parametrů tak, aby algoritmus pracoval co nejlépe. Popravdě jsem nenašel žádný zaručený způsob, jak toho dosáhnout a po určitém čase jsem se jednoduše spokojil s hodnotami, které fungují celkem dobře, ale optimální asi nebudou. Jelikož se v GA používá funkce random() v podstatě všude, je velice obtížné (rychle a jednoznačně) určit, zda je určitá změna daného parametru prospěšná, či nikoliv. Vždy je nutné nad danými daty spustit program vícekrát, průměrovat výsledky a donekonečna porovnávat. A ani poté si člověk nemůže být úplně jistý.
Hodně dobrým příkladem může být parametr NUM_MUTATIONS_IN_POPULATION, který určuje kolik mutací se provede při vytváření každé populace. Na jednu stranu by to mělo být relativně vysoké číslo, abychom se dokázali dostat z lokálního maxima, ale na druhou stranu naprostá většina mutací způsobuje zhoršení a degradaci populace a jen několik málo z nich může být přínosem. Ten se ale opět - kvůli náhodnému výběru rodičů pro rekombinaci - nemusí vůbec projevit. V limitním případě se při příliš vysokém počtu mutací může z GA stát spíše random search, při příliš nízkém se běh zastaví v lokálním maximu.
Jiným parametrem může být velikost populace POPULATION_SIZE. Při čtení různých materiálů o GA, kdy jsem teprve zjišťoval, jak vlastně fungují, jsem několikrát viděl zmíněno, že základní populace by měla být spíše malá cca. 20 až 50 jedinců. To je sice pravda, ale velikost pomocného bufferu NEW_POPULATION_SIZE, do kterého se generují rekombinace a mutace a z nichž se poté selekcí vybírají jedinci nové populace, tak jednoznačná není. Čím bude menší, tím méně kombinací budeme generovat a naopak čím bude větší, tím bude program pomalejší. Opět je nutné najít rozumné optimum.