171 lines
6.7 KiB
Markdown
171 lines
6.7 KiB
Markdown
|
# Algorythm Genetyczny w projekcie DSZI_Survival
|
||
|
**Autor:** Marcin Kostrzewski
|
||
|
|
||
|
---
|
||
|
## Cel
|
||
|
Celem algorytmu jest znalezienie czterech optymalnych wartości, według których
|
||
|
agent podejmuje decyzję, co zrobić dalej. Te cztery cechy to:
|
||
|
* Priorytet (chęć) zaspokajania głodu,
|
||
|
* Zaspokajanie pragnienia,
|
||
|
* Odpoczynek,
|
||
|
* Jak odległość od obiektu wpływa na podjętą decyzję.
|
||
|
|
||
|
Zestaw tych cech reprezentuje klasa-struktura **[*Affinities*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/Affinities.py)**:
|
||
|
```python
|
||
|
class Affinities:
|
||
|
def __init__(self, food, water, rest, walking):
|
||
|
"""
|
||
|
Create a container of affinities. Affinities describe, what type of entities a player prioritizes.
|
||
|
:param food: Food affinity
|
||
|
:param water: Freshwater affinity
|
||
|
:param rest: Firepit affinity
|
||
|
:param walking: How distances determine choices
|
||
|
"""
|
||
|
self.food = food
|
||
|
self.water = water
|
||
|
self.rest = rest
|
||
|
self.walking = walking
|
||
|
```
|
||
|
|
||
|
Oczywiście agent (gracz) posiada w swojej klasie pole ``self.affinities``.
|
||
|
|
||
|
## Podejmowanie decyzji
|
||
|
|
||
|
Gracz podejmuje decyzję o wyborze celu według następującej formuły:
|
||
|
```python
|
||
|
typeWeight / (distance / walkingAffinity) * affectedStat * multiplier
|
||
|
```
|
||
|
gdzie:
|
||
|
* *typeWeight* - wartość cechy odpowiadającej typowi celu,
|
||
|
* *distance* - odległość od celu,
|
||
|
* *walkingAffinity* - waga odległości,
|
||
|
* *affectedStat* - aktualna wartość odpowiadającej statystyki agenta,
|
||
|
* *multiplier* - mnożnik redukujący wpływ obecnych statystyk na wybór.
|
||
|
|
||
|
Implementacja w **[*GA.py/pickEntity()*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/GA.py)** (przykładowo dla jedzenia):
|
||
|
```python
|
||
|
watersWeights = []
|
||
|
thirst = player.statistics.thirst
|
||
|
for water in waters:
|
||
|
typeWeight = weights[1]
|
||
|
distance = abs(player.x - water.x) + abs(player.x - water.y)
|
||
|
watersWeights.append(typeWeight / (distance * walkingAffinity) * thirst * 0.01)
|
||
|
```
|
||
|
|
||
|
Dla każdego obiektu, z którym agent może podjąć interakcję wyliczana jest ta wartość
|
||
|
i wybierany jest obiekt, dla którego jest największa.
|
||
|
|
||
|
## Implementacja algorytmu genetycznego
|
||
|
|
||
|
Za realizację algorytmu odpowiada funkcja *geneticAlgorithm()* w **[*GA.py*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/GA.py)** (Skrócona wersja):
|
||
|
```python
|
||
|
def geneticAlgorithm(map, iter, solutions, mutationAmount=0.05):
|
||
|
# Based on 4 weights, that are affinities tied to the player
|
||
|
weightsCount = 4
|
||
|
|
||
|
# Initialize the first population with random values
|
||
|
initialPopulation = numpy.random.uniform(low=0.0, high=1.0, size=(solutions, weightsCount))
|
||
|
population = initialPopulation
|
||
|
|
||
|
for i in range(iter):
|
||
|
fitness = []
|
||
|
for player in population:
|
||
|
fitness.append(doSimulation(player, map))
|
||
|
|
||
|
parents = selectMatingPool(population, fitness, int(solutions / 2))
|
||
|
|
||
|
offspring = mating(parents, solutions, mutationAmount)
|
||
|
|
||
|
population = offspring
|
||
|
```
|
||
|
|
||
|
#### Omówienie:
|
||
|
|
||
|
##### Pierwsza populacja
|
||
|
Pierwsza populacja inicjalizowana jest losowymi wartościami. Szukamy
|
||
|
czterech najlepszych wag; każdy osobnik z gatunku jest reprezentowany przez
|
||
|
listę 4-elementową wag.
|
||
|
|
||
|
```python
|
||
|
initialPopulation = numpy.random.uniform(low=0.0, high=1.0, size=(solutions, weightsCount))
|
||
|
```
|
||
|
|
||
|
Rozpoczyna się pętla, która stworzy tyle generacji, ile sprecyzujemy w parametrze.
|
||
|
|
||
|
##### Symulacja i *fitness*
|
||
|
|
||
|
Dla każdego osobnika z populacji uruchamiana jest symulacja. Symulacja dzieje się w tle,
|
||
|
żeby zminimializować czas potrzebny do wykonania pełnej symulacji. Jej koniec następuje w momencie,
|
||
|
gdy agent umrze.
|
||
|
```python
|
||
|
fitness.append(doSimulation(player, map))
|
||
|
```
|
||
|
|
||
|
Wartością zwracaną przez funkcję symulacji jest tzw. *fitness*. W tym wypadku,
|
||
|
wartością tą jest ilość kroków, jakie pokonał agent przez cykl życia.
|
||
|
|
||
|
##### Wybór rodziców
|
||
|
|
||
|
Rodzice dla dzieci przyszłego pokolenia wybierani są na podstawie wartości
|
||
|
*fitness*. W tym wypadku wybirana jest połowa populacji z najwyższymi wartościami przeżywalności.
|
||
|
```python
|
||
|
parents = selectMatingPool(population, fitness, int(solutions / 2))
|
||
|
```
|
||
|
|
||
|
##### Potomstwo, czyli rozmnażanie i mutacje
|
||
|
|
||
|
Za wyliczanie wartości dla nowego pokolenia odpowiada funkcja ``mating``. Przekazujemy do niej rodziców, ilość potomstwa
|
||
|
i siłę mutacji. Z **[*GA.py/mating()*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/GA.py)**:
|
||
|
```python
|
||
|
for i in range(offspringCount):
|
||
|
parent1 = i % len(parents)
|
||
|
parent2 = (i + 1) % len(parents)
|
||
|
offspring.append(crossover(parents[parent1], parents[parent2]))
|
||
|
```
|
||
|
|
||
|
Do stworzenia potomstwa używana jest funkcja ``crossover``, która wylicza wartości, jakie przyjmie nowe potomstwo.
|
||
|
Wartośc ta to mediana wartości obu rodziców. Z **[*GA.py/crossover()*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/GA.py)**:
|
||
|
```python
|
||
|
for gene1, gene2 in zip(genes1, genes2):
|
||
|
result.append((gene1 + gene2) / 2)
|
||
|
```
|
||
|
Po zastosowaniu krzyżówki, jeden losowo wybrany gen jest alterowany o niewielką wartość (mutacja). Z **[*GA.py/mutation()*](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/src/master/src/AI/GA.py)**:
|
||
|
```python
|
||
|
for player in offspring:
|
||
|
randomGeneIdx = random.randrange(0, len(player))
|
||
|
player[randomGeneIdx] = player[randomGeneIdx] + random.uniform(-1.0, 1.0) * mutationAmount
|
||
|
```
|
||
|
|
||
|
Nowe potomstwo zastępuje obecną populacje i algorytm wchodzi w kolejną pętle:
|
||
|
```python
|
||
|
population = offspring
|
||
|
```
|
||
|
|
||
|
## Skuteczność algorytmu
|
||
|
|
||
|
Zastosowanie algorytmu przynosi niezbyt spektakularne, lecz oczekiwane wyniki. Po uruchomieniu symulacji
|
||
|
dla 1000 generacji:
|
||
|
* Wykres wartości fitness od generacji:
|
||
|
![fig](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/raw/master/data/images/exampleFitness.png)
|
||
|
|
||
|
* Najlepsze / najgorsze fitness:
|
||
|
```
|
||
|
Best Fitness: 188
|
||
|
Worst Fitness: 66
|
||
|
```
|
||
|
|
||
|
* Zestaw najlepszych / najgorszych wartości
|
||
|
```
|
||
|
Best:
|
||
|
Affinities: food=0.6246050649543714, water=0.7464600395102128, rest=0.09747026036072276, walking=0.8427814969457226
|
||
|
Worst:
|
||
|
Affinities: food=0.5010343683602108, water=0.5248573924802746, rest=0.2191880576417362, walking=0.7195556582421176
|
||
|
```
|
||
|
|
||
|
## Zastosowanie w całości projektu
|
||
|
Dzięki wyliczonym przez algorytm wagom, gracz poruszający się w środowisku będzie znał swoje priorytety i będzie w stanie
|
||
|
przeżyć jak najdłużej. Obecnie, wybór obiektu jest dość statyczny i niezbyt "mądry", został napisany jedynie
|
||
|
na potrzeby tego projektu. W przyszłości algorytm może być trenowany według inteligentnych wyborów obiektów np. poprzez zastosowanie
|
||
|
drzewa decyzyjnego. Każdy obiekt ma zdefiniowany swój skutek, czyli gracz z góry wie, czym jest dany obiekt. W przyszłości
|
||
|
gracz może nie znać informacji o obiektach, może być do tego używany jakiś inny algorytm, który oceni,
|
||
|
czym jest dany obiekt.
|