DSZI_Survival/gaTraveling.md

216 lines
10 KiB
Markdown
Raw Normal View History

2020-05-30 20:31:10 +02:00
# Wyznaczanie trasy algorytmem genetycznym
2020-05-31 19:54:27 +02:00
**Autor:** *Mateusz Tylka*
## Cel algorytmu
Celem tego algorytmu jest wyznaczenie optymalnej trasy w zbieraniu ziół o konkretnych pozycjach, które
są generowane losowo. Algorytm decyduje po które zioło udać się najpierw, starając się, aby końcowa suma odległości
2020-06-01 01:20:16 +02:00
pomiędzy odwiedzonymi pozycjami była jak najmniejsza. Gdy agent zbierze wszystkie zioła i dojdzie do ogniska, aby
odpocząć, utworzy również eliksir, który odnowi mu wszystkie statystyki pierwotnego (pełnego) stanu.
2020-05-31 19:54:27 +02:00
## Osobnik Traveling
Osobnik jest to jednostka polegająca ewolucji za pomocą operacji genetycznych.
2020-06-01 01:20:16 +02:00
W mojej implementacji osobnika reprezentuje obiekt [Traveling.py](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/wirus_dev/src/AI/GaTravelingForHerbs/Traveling.py). Ten obiekt przechowuje następujące metody:
2020-05-31 19:54:27 +02:00
```python
class Traveling:
def __init__(self, coords):
self.coords = coords
self.fitness = self.evaluate()
```
* W konstruktorze przyjmowany jako parametr jest zestaw koordynatów, który zostaje zapisany jako atrybut,
następnie tworzymy atrybut reprezentujący sprawność danego osobnika, który jest wynikiem metody określającej
poprawność danej trasy.
```python
def evaluate(self):
sum = 0
for i, c in enumerate(self.coords):
if i + 1 > len(self.coords) - 1:
break
nextCoord = self.coords[i + 1]
# calculate distance
sum += sqrt((nextCoord[0] - c[0]) ** 2 + (nextCoord[1] - c[1]) ** 2)
return sum
```
* Metoda **evaluate** odpowiedzialna jest za ocenę osobnika. Liczymy w niej odległość od punktu startu do
pierwszego punktu, następnie odległość między drugim a trzecim miejscem i tak dalej..., aż do końca listy pozycji
ziół z rozważanej trasy. Uzyskane wyniki sumujemy, czyli uzyskujemy długość konretnej drogi.
```python
def crossover(self, parentCoords):
childCoords = self.coords[1:int(len(self.coords) / 2)]
for coord in parentCoords.coords:
if coord not in childCoords and coord not in END_COORD + START_COORD:
childCoords.append(coord)
if len(childCoords) == len(parentCoords.coords):
break
return Traveling(START_COORD + childCoords + END_COORD)
```
* Metoda **crossover** reprezentuję operację genetyczną krzyżowania osobników. Bierzemy w niej z pierwszego osobnika
część punktów jego trasy (w naszym przypadku połowę) i dobieramy w pętli kolejne koordynaty z drugiego osobnika
tak, aby się one nie powtarzały. Gdy już osiągniemy odpowiednią długość nowego osobnika kończymy pętlę i zwracamy go.
```python
def mutation(self):
first = randint(1, len(self.coords) - 2)
second = randint(1, len(self.coords) - 2)
self.coords[first], self.coords[second] = self.coords[second], self.coords[first]
self.fitness = self.evaluate()
```
* Ta metoda przedstawia proces mutacji. Polega on po prostu na zamianę miejscami dwóch losowych koordynatów
na trasie.
```python
def __repr__(self):
return str(self.coords)
```
* Obiekt ten zwracany jest w formie tekstowej listy koordynatów.
## Obiekt GeneticAlgorithm
2020-06-01 01:20:16 +02:00
W pliku [GeneticAlgorithm.py](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/wirus_dev/src/AI/GaTravelingForHerbs/GeneticAlgorithm.py) znajduje się model selekcji osobników, warunek stopu, oraz główna pętla
2020-05-31 19:54:27 +02:00
algorytmu.
```python
class GeneticAlgorithm:
def __init__(self, firstPopulation, mutationProbability):
self.firstPopulation = firstPopulation
self.mutationProbability = mutationProbability
```
* Obiekt ten przyjmuje pierwszą populację oraz prawdopodobieństwo mutacji jako parametry i zapisuje je
w odpowiednich atrybutach.
```python
def selectionModel(self, generation):
max_selected = int(len(generation) / 10)
sorted_by_assess = sorted(generation, key=lambda x: x.fitness)
return sorted_by_assess[:max_selected]
```
* Model w mojej implementacji opiera się na elitaryzmie - czyli wybraniu pewnej ilości najlepszych chromosomów,
które z pewnością przetrwają do następnej generacji. Definiujemy w niej 10% spośród przyjętej generacji jako parametr.
Sortujemy naszą generację według odległości (metody *evaluate*) czyli wartości atrybutu **fitness**.
```python
def stopCondition(self, i):
return i == 64
```
* Warunkiem końca algorytmu jest osiągnięcie 64 generacji.
```python
def run(self):
population = self.firstPopulation
population.sort(key=lambda x: x.fitness)
populationLen = len(population)
i = 0
while True:
selected = self.selectionModel(population)
newPopulation = selected.copy()
while len(newPopulation) != populationLen:
child = choice(population).crossover(choice(population))
if random() <= self.mutationProbability:
child.mutation()
newPopulation.append(child)
population = newPopulation
theBestMatch = min(population, key=lambda x: x.fitness)
print("Generation: {} S: {} fitness: {}".format(i+1, theBestMatch, theBestMatch.fitness))
i += 1
if self.stopCondition(i):
return str(theBestMatch)
```
* W metodzie **run** zaimplementowana jest główna pętla algorytmu
genetycznego. Na początku wskazujemy pierwszą populację i sortujemy ją według dopasowania **fitness**,
a następnie obliczamy długość populacji i deklarujemy iterator pętli, która przebiega w następujących krokach;
* Wybieramy najlepszych osobników według modelu selekcji (metody **selectionModel**)
* Tworzymy nową populację z najlepszych wybranych osobników, jednak do pełnej populacji brakuje nam kilku chromosomów
* Dopełniamy do pełnej liczności populacji, poprzez operację krzyżowania (metoda **crossover**), oraz
ewentualną mutację (metodą **mutation**).
* Wybieramy najlepszego osobnika z populacji po minimalnej odległości, oraz wyświetlamy wynik.
2020-06-01 01:20:16 +02:00
* Przeprowadzamy w ten sposób kolejną generację dopóki nie będzie ich 64.
```python
def listOfTravel(self):
strTravel = self.run()
import ast
return ast.literal_eval(strTravel)
```
* Ta metoda, odpowiada za uruchomienie algorytmu oraz zwrócenie najlepszego wyniku w postaci listy koordynatów
(nie jako string).
## Inicjalizacja pierwszej populacji i uruchomienie algorytmu
Uruchamiając projekt za pomocą [Run.py](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/wirus_dev/Run.py) z użyciem parametru **ga_travel**, projekt uruchomi się tak jak w przypadku
testowej wersji z dodatkiem kodu (znajduje się on w [Game.py](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/wirus_dev/src/game/Game.py)) zaprezentowanego poniżej;
```python
# Generate random travel list
self.travelCoords = random.sample(self.map.movableList(), 10)
import ast
self.travelCoords = ast.literal_eval(str(self.travelCoords))
# Insert herbs on random travel coordinates
self.map.insertHerbs(self.travelCoords)
# Initialize genetic algorithm
firstGeneration = [Traveling(START_COORD + sample(self.travelCoords, len(self.travelCoords)) + END_COORD) for _
in range(100)]
mutationProbability = float(0.1)
ga = GeneticAlgorithm(firstGeneration, mutationProbability)
self.movementList = ga.listOfTravel()
# Define list of entities which player should pass to collect herbs
self.entityToVisitList = []
for i in self.movementList:
self.entityToVisitList.append(self.map.getEntityOnCoord(i))
# Remove first element, because start coordinates is None
self.entityToVisitList.remove(self.entityToVisitList[0])
self.screen.ui.console.printToConsole("First generation: " + str(firstGeneration[0]))
self.screen.ui.console.printToConsole("The best generation: " + str(self.entityToVisitList))
self.mainLoop()
```
* Generujemy losową listę 10 koordynatów na mapie wolnych od entity za pomocą metody **map.movableList()** i konwertujemy
ją na normalną listę (nie *string*).
* Umieszczamy entity ziół w miejscach wygenerowanych koordynatów.
* Tworzymy pierwszą generację w postaci 100-elementowej listy, za pomocą konstruktora obiektu **Traveling** o koordynatach ziół plus startowa pozycja gracza
i pozycja ogniska, gdzie agent będzie w stanie sporządzić miksturę odnawiającą jego statystyki.
* Deklarujemy algorytm genetyczny przekazując pierwszą generację oraz prawdopodobieństwo mutacji wynoszące 10%
* Tworzymy listę kordynatów na których będziemy się poruszać, gdzie jej wartość co zwrócona lista przez
metodę **ga.listOfTravel()**
* Aby udać się po odpowiednie cele, tworzymy listę entity, które musimy zebrać, wykorzystując przy tym
wcześniej stworzoną listę **movementList** oraz metodę **map.getEntityOnCoord**.
* Usuwamy z listy **entityToVisitList** pierwszy element, gdyż jest to startowa pozycja gracza, na której nie
ma żadnego entity.
## Poruszanie się
*Zdefiniowane jest w pliku/klasie [EventManager.py](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/wirus_dev/src/game/EventManager.py).*
```python
if keys[pygame.K_t]: # Handle traveling movement to collect herbs
if self.player.movementTarget is None and self.iterator <= 10:
target = self.game.entityToVisitList[self.iterator]
self.player.gotoToTarget(target, self.game.map)
self.iterator += 1
if self.player.herbs > self.takenHerbs: # Console log when player collect herb
self.game.screen.ui.console.printToConsole("Ziele zebrane! Ilość: " + str(self.player.herbs))
self.takenHerbs = self.player.herbs
if self.player.readyToCrafting: # Console log and reset statistics because of collect all herbs
self.game.screen.ui.console.printToConsole("Eliksir został utworzony i spożyty!")
self.player.statistics.set_hp(100)
self.player.statistics.set_stamina(100)
self.player.statistics.set_thirst(-100)
self.player.statistics.set_hunger(-100)
self.player.readyToCrafting = False
```
Po kliknięciu przycisku **t** agent uda się po kolejne ziele do zebranie w
kolejce za pomocą algorytmu **A***. Obok jest również zaimplementowane wypisywanie ilości zebranych ziół,
oraz odnawianie statystyk po spożyciu eliksiru.