Compare commits
318 Commits
writingInf
...
master
Author | SHA1 | Date | |
---|---|---|---|
c42a1038bc | |||
8ec7efaafa | |||
|
500f051946 | ||
|
5aaba17b2e | ||
|
c5497ff291 | ||
|
e132a1ceaf | ||
|
7f6cec3107 | ||
|
096934f547 | ||
|
00bf015951 | ||
|
7e4f180bb6 | ||
|
f7c5deafda | ||
|
9dc60c7062 | ||
|
2fdbf014aa | ||
|
1b71fe886b | ||
|
f6fa3b461b | ||
|
c8557f3de6 | ||
|
5c8a48e30d | ||
|
14850cd230 | ||
|
cf0a4be82f | ||
|
80c72f1ba0 | ||
|
b63be724f5 | ||
|
80b7e32f47 | ||
|
d363120ef4 | ||
|
87a341744c | ||
|
1eb62c3382 | ||
|
51d50f7cd6 | ||
|
068eb3dfed | ||
|
7906524047 | ||
|
d9d67388e5 | ||
|
3229d9405a | ||
|
f57bc6463b | ||
|
b17221c7c2 | ||
|
a256fc1cfe | ||
|
63b1d36c36 | ||
|
c1b12883f8 | ||
|
f0c77a154b | ||
|
04e3b29c21 | ||
|
29ba3d23c0 | ||
|
d383fdefc4 | ||
|
939c3f9072 | ||
|
1e4b44558c | ||
|
994c509831 | ||
|
326551ad1c | ||
|
e7cd571d95 | ||
|
da119fca37 | ||
|
9825cd9b7d | ||
|
597a709224 | ||
|
34eb74d980 | ||
|
3f2e5550c7 | ||
|
6969a81616 | ||
|
f025df0381 | ||
|
f73ae6290f | ||
|
df70b321ae | ||
|
48560fb3a1 | ||
|
873bfd0d8e | ||
|
98f692a6d8 | ||
|
ed58517b82 | ||
|
8e9d0c1267 | ||
|
9dc74bc599 | ||
|
b8d2a4f379 | ||
|
2c2d865d7f | ||
|
7b6a46188b | ||
|
02df6e01c6 | ||
|
45345cabd8 | ||
|
80946a4246 | ||
|
c88965de38 | ||
|
5f7cd0dce7 | ||
|
7d12c9f59e | ||
|
3fafade1e9 | ||
|
0d6b3e4e84 | ||
|
2fbc877141 | ||
|
93839b3580 | ||
|
05d28c4f5b | ||
|
c4809373a0 | ||
|
3e3fd259dd | ||
|
94ec3c273e | ||
|
f0af76520b | ||
|
1f99669d29 | ||
|
c347c68958 | ||
|
052516540f | ||
|
8bc292f06d | ||
|
8d4cde8eb0 | ||
|
c4d4d6bb52 | ||
|
479137ce74 | ||
|
b0d8872c15 | ||
|
43b654a6a5 | ||
|
44419b5f28 | ||
|
9d1299b162 | ||
|
fd54303dba | ||
|
a3d47b4320 | ||
|
fbd76f31e7 | ||
|
bdba45de08 | ||
|
ad791a299e | ||
|
38ce217db4 | ||
ee9769db6e | |||
c5ff411eba | |||
2f018eae2f | |||
f67a9748f6 | |||
a0da920e39 | |||
b20ac9c356 | |||
ded03f5a3b | |||
3e4f10236d | |||
dc5dc756cb | |||
de68c5e991 | |||
2ff0ce1ddf | |||
f1d616576c | |||
89eb557a2d | |||
04370fb98c | |||
96cf823619 | |||
df16339eef | |||
aa3eea8b39 | |||
1aacc693b9 | |||
3108343513 | |||
b6312df9f3 | |||
d31e03050e | |||
3b294030f4 | |||
d9a4750f7d | |||
878f9b9523 | |||
ada3564713 | |||
d1b6027ffa | |||
eed7d2ea82 | |||
0225eda1eb | |||
7c6cc737e1 | |||
4b75bebf5a | |||
2afbcdaff8 | |||
b9997a0a93 | |||
584d371ec1 | |||
dcc8da6d2a | |||
92490789ea | |||
6725c4ae0c | |||
8c2a3f2bff | |||
1c8b21bf2c | |||
05898f5b8b | |||
052d8ca7ce | |||
de3a0569a9 | |||
66ac27a6fe | |||
ac6a7df4fe | |||
e8e36a4420 | |||
625b6956c9 | |||
f56eedb657 | |||
0cd91fd1a1 | |||
b89822a658 | |||
9484a05dd5 | |||
30e1db94bf | |||
264aa2969c | |||
a45be130e6 | |||
7877cbb239 | |||
5e1e089015 | |||
6ec1c0b2d2 | |||
15babde728 | |||
233ce2462c | |||
5d060ae8a4 | |||
25372c2f34 | |||
b6893379cf | |||
74093de1d3 | |||
b61ffc008b | |||
a0c315a600 | |||
64c9a4979c | |||
fce8eea181 | |||
8cad7be0a5 | |||
2318000476 | |||
454e0ac87d | |||
fa1fe48abb | |||
90746c51d9 | |||
215dd876f7 | |||
8c3bdc3726 | |||
d27986045d | |||
|
d38b2ff7d5 | ||
|
d26703da5b | ||
|
6832d41990 | ||
|
b15382d9bc | ||
|
76de35bc3d | ||
|
4e828de1c7 | ||
|
22355dc960 | ||
|
2cebcd3be6 | ||
|
86394ef71b | ||
|
7d71333558 | ||
|
aab779d4e1 | ||
|
08643c7083 | ||
|
9d1ecf5d90 | ||
|
bb46f56ace | ||
8df63bb607 | |||
d45daadc60 | |||
492f37bda5 | |||
a40a598ffe | |||
dae94576e7 | |||
4fff56bd7d | |||
569128763b | |||
fad3d68b55 | |||
3924023a92 | |||
31811782fb | |||
c806d99d49 | |||
085446ea93 | |||
2b07e7394f | |||
79144c28a3 | |||
d8da2f2625 | |||
154dbaffaa | |||
1610f9c0da | |||
a8ef08b3ef | |||
844397191a | |||
fed30b0ade | |||
cc98db0a96 | |||
776be8fb90 | |||
95972ac969 | |||
065111dee9 | |||
dac8583665 | |||
824a7e33ea | |||
93f4e9f53d | |||
2618bbdf9c | |||
7ed974b194 | |||
269e376756 | |||
107bd0d8f7 | |||
1eb75fc354 | |||
e66b0d4a59 | |||
6ed8423463 | |||
823eefac86 | |||
2bf0ed035b | |||
f224c8c657 | |||
ce50622b89 | |||
57b95e10df | |||
cba207f98c | |||
649b61a84e | |||
b3d38403ad | |||
93472900b9 | |||
48e81e206b | |||
effa48d7c3 | |||
a51661ae6e | |||
28d9013983 | |||
d6cf76f6bc | |||
3ac188103a | |||
549c4f207f | |||
81bed4adeb | |||
25345ae317 | |||
dcdb4ac87e | |||
e270360690 | |||
11b467db32 | |||
45e7c412f8 | |||
16bb2cd311 | |||
f104e90a0f | |||
0eea13331f | |||
01ce5796da | |||
a76965cd91 | |||
752c3daa24 | |||
fc47c697df | |||
|
d537eb089d | ||
|
c96ecc64d8 | ||
389183a4c2 | |||
|
b433b588f5 | ||
|
ddb30859d1 | ||
|
3632a9ffbb | ||
|
ae551703fd | ||
|
b23865b090 | ||
|
a6a8812ace | ||
|
5a4228fcff | ||
|
20c3c269de | ||
|
0d8d9309ae | ||
|
b4624c4c56 | ||
6fbc9184f4 | |||
|
a6b717935f | ||
|
013e8b524d | ||
|
2ba090b2d4 | ||
|
87fc6bea1f | ||
|
08d425b938 | ||
|
1255ab8e16 | ||
|
31d2bf0009 | ||
|
dee9ebb77c | ||
|
b12c969d14 | ||
|
1edaa1f74e | ||
|
f1e0c3c593 | ||
|
b8abacd4cc | ||
|
ab1b4e9efc | ||
|
d5c9f5f801 | ||
|
a5569f0b8f | ||
|
0151413cdd | ||
|
5facbfb2fd | ||
|
53ee1e1f10 | ||
|
deeba7c740 | ||
|
39b689d8f0 | ||
|
8b9d953553 | ||
|
b7f4bc6101 | ||
|
c14ed38df2 | ||
|
7a79ccc9c2 | ||
|
e01dfeb85a | ||
|
2e6f2db543 | ||
|
b6234b1a93 | ||
|
b296fbe492 | ||
|
5af76a1b16 | ||
|
cdcf5fb052 | ||
|
cef686a74f | ||
|
d44274397d | ||
|
91efd32d87 | ||
|
4d6de52e8c | ||
|
7d9dd06d09 | ||
|
e6c36eea8d | ||
|
0084750629 | ||
e37a17cadd | |||
5ca1a0b4d0 | |||
d7e0b37666 | |||
|
28a581bd3e | ||
|
508401d0a3 | ||
|
69313b0984 | ||
1bfa195616 | |||
d32384edde | |||
90cd79b34d | |||
94daee9f79 | |||
|
b47e577356 | ||
|
774d186653 | ||
|
b0891c3e48 | ||
dd16e77c33 | |||
|
10898acc10 | ||
|
ec0c3adc97 | ||
1716cb9190 | |||
a534f2fed3 | |||
89f2a44d43 | |||
ae7c5c64ca | |||
2c60019b41 | |||
|
c09a67dc2f | ||
134d09247e |
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/.idea/
|
||||
__pycache__
|
||||
/venv/
|
||||
/doc/_build/
|
||||
|
214
DecisionTree.md
Normal file
@ -0,0 +1,214 @@
|
||||
# DSZI_Survival - Drzewa Decyzyjne
|
||||
### Autor: Michał Czekański
|
||||
|
||||
## Cel zastosowania w projekcie
|
||||
W projekcie DSZI_Survival drzewo decyzyjne użyte jest do podejmowania decyzji przez agenta, rozbitka na bezludnej wyspie,
|
||||
jaką czynność wykonać w danej chwili.
|
||||
|
||||
Czy:
|
||||
* zdobyć pożywienie
|
||||
* udać się do źródła wody
|
||||
* odpocząć przy ognisku
|
||||
|
||||
## Opis drzewa decyzyjnego
|
||||
|
||||
* **Drzewo decyzyjne** to drzewo reprezentujące jakąś funkcję, Boolowską w najprostszym przypadku.
|
||||
* Drzewo decyzyjne jako **argument** przyjmuje obiekt - sytuację opisaną za pomocą zestawu **atrybutów**
|
||||
* **Wierzchołek** drzewa decyzyjnego odpowiada testowi jednego z atrybutów (np. IsMonday)
|
||||
* Każda **gałąź** wychodząca z wierzchołka jest oznaczona możliwą wartością testu z wierzchołka (np. True)
|
||||
* **Liść** zawiera wartość do zwrócenia (**decyzję, wybór**), gdy liść ten zostanie osiągnięty (np. ShopType.Grocery)
|
||||
|
||||
|
||||
## Metoda uczenia - Algorytm ID3
|
||||
|
||||
Metoda użyta do uczenia drzewa decyzyjnego to metoda **indukcyjnego uczenia drzewa decyzyjnego**.
|
||||
|
||||
### Działanie ID3
|
||||
* Definiujemy atrybuty, które będą posiadały przykłady służące do uczenia drzewa (**atrybuty**)
|
||||
|
||||
```python
|
||||
class AttributeDefinition:
|
||||
def __init__(self, id, name: str, values: List):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.values = values
|
||||
|
||||
class Attribute:
|
||||
def __init__(self, attributeDefinition: AttributeDefinition, value):
|
||||
self.attributeDefinition = attributeDefinition
|
||||
self.value = value
|
||||
```
|
||||
* Tworzymy przykłady z wykorzystaniem atrybutów (**przykłady**)
|
||||
|
||||
```python
|
||||
class DecisionTreeExample:
|
||||
def __init__(self, classification, attributes: List[Attribute]):
|
||||
self.attributes = attributes
|
||||
self.classification = classification
|
||||
```
|
||||
* Ustalamy domyślną wartość do zwrócenia przez drzewo - **klasa domyślna**
|
||||
* Następnie postępujemy indukcyjnie:
|
||||
* Jeżeli liczba przykładów == 0: zwracamy wierzchołek oznaczony klasą domyślną
|
||||
* Jeżeli wszystkie przykłady są tak samo sklasyfikowane: zwracamy wierzchołek oznacz. tą klasą
|
||||
* Jeżeli liczba atrybutów == 0: zwracamy wierzchołek oznacz. klasą, którą posiada większość przykładów
|
||||
* W przeciwnym wypadku **wybieramy atrybut** A (o wyborze atrybutu poniżej) i czynimy go korzeniem drzewa T
|
||||
* **nowa_klasa_domyślna** = wierzchołek oznaczony klasą, która jest przypisana największej liczbie przykładów
|
||||
* Dla każdej wartości W atrybutu A:
|
||||
* nowe_przykłady = przykłady, dla których atrybut A przyjmuje wartość W
|
||||
* Dodajemy do T krawędź oznaczoną przez wartość W, która prowadzi do wierzchołka zwróconego przez wywołanie indukcyjne:
|
||||
*treelearn(nowe_przykłady, atrybuty−A, nowa_klasa_domyślna)*
|
||||
* Zwróć drzewo T
|
||||
```python
|
||||
class DecisionTree(object):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.branches = []
|
||||
self.branchesNum = 0
|
||||
```
|
||||
|
||||
### Wybór atrybutu
|
||||
W trakcie uczenia drzewa decyzyjnego chcemy wybrać jak najlepszy atrybut, dzięki któremu możliwie jak najszybciej będziemy mogli sklasyfikować podane przykłady.
|
||||
|
||||
Miarą porównawczą atrybutów będzie **zysk informacji** dla danego atrybutu (**information gain**).
|
||||
|
||||
Atrybut o największym zysku zostanie wybrany.
|
||||
|
||||
**Implementacja**
|
||||
|
||||
```python
|
||||
def chooseAttribute(attributes: List[AttributeDefinition], examples: List[DecisionTreeExample], classifications):
|
||||
bestAttribute = None
|
||||
bestAttributeGain = -1
|
||||
|
||||
for attribute in attributes:
|
||||
attrInformationGain = calculateInformationGain(attribute, classifications, examples)
|
||||
if attrInformationGain > bestAttributeGain:
|
||||
bestAttribute = attribute
|
||||
bestAttributeGain = attrInformationGain
|
||||
|
||||
return bestAttribute
|
||||
```
|
||||
|
||||
|
||||
#### Obliczanie zysku informacji
|
||||
(Wszelkie obliczenia wedle wzorów podanych na zajęciach)
|
||||
* I(C) - Obliczamy zawartość informacji dla zbioru możliwych klasyfikacji
|
||||
|
||||
![fig](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/raw/master/data/images/reportImages/DT/I%28C%29.png)
|
||||
|
||||
* E(A) - Obliczamy ilość informacji potrzebną do zakończenia klasyfikacji po sprawdzeniu atrybutu
|
||||
|
||||
![fig](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/raw/master/data/images/reportImages/DT/E%28A%29.png)
|
||||
|
||||
* **G(A)** - **Przyrost informacji dla atrybutu A** = I(C) - E(A)
|
||||
|
||||
**Implementacja**
|
||||
|
||||
```python
|
||||
def calculateInformationGain(attribute: AttributeDefinition, classifications, examples: List[DecisionTreeExample]):
|
||||
return calculateEntropy(classifications, examples) - calculateRemainder(attribute, examples, classifications)
|
||||
```
|
||||
|
||||
## Opis implementacji
|
||||
### Definicje atrybutów:
|
||||
* Głód: **[0, 1/4); [1/4, 1/2); [1/2, 3/4); [3/4, 1]**
|
||||
* Pragnienie: **[0, 1/4); [1/4, 1/2); [1/2, 3/4); [3/4, 1]**
|
||||
* Energia: **[0, 1/4); [1/4, 1/2); [1/2, 3/4); [3/4, 1]**
|
||||
* Odległość od jedzenia: **[0, 3); [3, 8); [8, 15); [15, max)**
|
||||
* Odległość od źródła wody: **[0, 3); [3, 8); [8, 15); [15, max)**
|
||||
* Odległość od miejsca spoczynku: **[0, 3); [3, 8); [8, 15); [15, max)**
|
||||
* Odległość pomiędzy wodą a jedzeniem: **[0, 3); [3, 8); [8, 15); [15, max)**
|
||||
|
||||
```python
|
||||
class PlayerStatsValue(Enum):
|
||||
ZERO_TO_QUARTER = 0
|
||||
QUARTER_TO_HALF = 1
|
||||
HALF_TO_THREE_QUARTERS = 2
|
||||
THREE_QUARTERS_TO_FULL = 3
|
||||
|
||||
class DistFromObject(Enum):
|
||||
LT_3 = 0
|
||||
GE_3_LT_8 = 1
|
||||
GE_8_LT_15 = 2
|
||||
GE_15 = 3
|
||||
```
|
||||
### Uczenie drzewa
|
||||
|
||||
```python
|
||||
def inductiveDecisionTreeLearning(examples: List[DecisionTreeExample], attributes: List[AttributeDefinition], default,
|
||||
classifications)
|
||||
```
|
||||
|
||||
### Zwracanie decyzji przez drzewo
|
||||
```python
|
||||
def giveAnswer(self, example: DecisionTreeExample):
|
||||
if self.branchesNum == 0:
|
||||
return self.root
|
||||
|
||||
for attr in example.attributes:
|
||||
if attr.attributeDefinition.id == self.root.id:
|
||||
for branch in self.branches:
|
||||
if branch.label == attr.value:
|
||||
return branch.subtree.giveAnswer(example)
|
||||
```
|
||||
|
||||
### Wybór celu dla agenta
|
||||
```python
|
||||
def pickEntity(self, player, map, pickForGa=False):
|
||||
foods = map.getInteractablesByClassifier(Classifiers.FOOD)
|
||||
waters = map.getInteractablesByClassifier(Classifiers.WATER)
|
||||
rests = map.getInteractablesByClassifier(Classifiers.REST)
|
||||
|
||||
playerStats = DTPlayerStats.dtStatsFromPlayerStats(player.statistics)
|
||||
|
||||
# Get waters sorted by distance from player
|
||||
dtWaters: List[DTSurvivalInteractable] = []
|
||||
for water in waters:
|
||||
dtWater = DTSurvivalInteractable.dtInteractableFromInteractable(water, player.x, player.y)
|
||||
dtWaters.append(dtWater)
|
||||
dtWaters.sort(key=lambda x: x.accurateDistanceFromPlayer)
|
||||
nearestDtWater = dtWaters[0]
|
||||
|
||||
# Get foods sorted by distance from player
|
||||
dtFoods: List[DTSurvivalInteractable] = []
|
||||
for food in foods:
|
||||
dtFood = DTSurvivalInteractable.dtInteractableFromInteractable(food, player.x, player.y)
|
||||
dtFoods.append(dtFood)
|
||||
|
||||
dtFoods.sort(key=lambda x: x.accurateDistanceFromPlayer)
|
||||
# If there is no food on map return nearest water.
|
||||
try:
|
||||
nearestDtFood = dtFoods[0]
|
||||
except IndexError:
|
||||
return nearestDtWater.interactable
|
||||
|
||||
# Get rest places sorted by distance from player
|
||||
dtRestPlaces: List[DTSurvivalInteractable] = []
|
||||
for rest in rests:
|
||||
dtRest = DTSurvivalInteractable.dtInteractableFromInteractable(rest, player.x, player.y)
|
||||
dtRestPlaces.append(dtRest)
|
||||
dtRestPlaces.sort(key=lambda x: x.accurateDistanceFromPlayer)
|
||||
nearestDtRest = dtRestPlaces[0]
|
||||
|
||||
currentSituation = SurvivalDTExample(None, playerStats.hungerAmount, playerStats.thirstAmount,
|
||||
playerStats.staminaAmount,
|
||||
nearestDtFood.dtDistanceFromPlayer, nearestDtWater.dtDistanceFromPlayer,
|
||||
nearestDtRest.dtDistanceFromPlayer,
|
||||
nearestDtFood.getDtDistanceFromOtherInteractable(nearestDtWater.interactable))
|
||||
|
||||
treeDecision, choice = self.__pickEntityAfterTreeDecision__(currentSituation,
|
||||
dtFoods,
|
||||
dtRestPlaces,
|
||||
dtWaters)
|
||||
return choice.interactable
|
||||
```
|
||||
|
||||
## Zestaw uczący, zestaw testowy
|
||||
|
||||
### Zestaw uczący
|
||||
|
||||
Zestaw uczący był generowany poprzez tworzenie losowych przykładów i zapytanie użytkownika o klasyfikację, a następnie zapisywany do pliku.
|
||||
|
||||
### Zestaw testowy
|
||||
|
||||
Przy testowaniu drzewa podajemy ile procent wszystkich, wcześniej wygenerowanych przykładów mają być przykłady testowe.
|
173
GeneticAlgorithm.md
Normal file
@ -0,0 +1,173 @@
|
||||
# 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: 186
|
||||
Worst Fitness: 71
|
||||
```
|
||||
|
||||
* Zestaw najlepszych / najgorszych wartości
|
||||
```
|
||||
Best:
|
||||
Affinities: food=0.9659207331357987, water=1.06794833921562, rest=0.4224083038045297, walking=0.26676612275274836
|
||||
Worst:
|
||||
Affinities: food=0.3927852322929111, water=0.6888704071372844, rest=0.625376993269597, walking=0.5415515638814266
|
||||
```
|
||||
### Przykład symulacji dla najlepszego osobnika:
|
||||
![gif](https://git.wmi.amu.edu.pl/s444409/DSZI_Survival/raw/master/data/images/screenshots/bestFitnessRunExample.gif)
|
||||
|
||||
## 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.
|
70
NeuralNetwork.md
Normal file
@ -0,0 +1,70 @@
|
||||
# DSZI_Survival - Sieć Neuronowa
|
||||
### Autor: Jonathan Spaczyński
|
||||
|
||||
## Cel zastosowania w projekcie
|
||||
W projekcie DSZI_Survival sieć neuronowa użyta jest do podejmowania decyzji przez agenta.
|
||||
Decyzja polega na rozpoznawaniu zdjęć owoców (jabłka i gruszki). W przypadku nie rozpoznania owocu przez
|
||||
agenta, dochodzi do zatrucia i agent umiera/przegrywa.
|
||||
|
||||
## Przygotowanie danych
|
||||
|
||||
* **Krok 1** Przechowywane zdjęcia owoców muszą przejść przez proces zamiany zdjęcia (.jpg) na dane, które
|
||||
mogą być wykorzystane przez sieć neuronową.
|
||||
|
||||
```python
|
||||
CATEGORIES = ["Apple", "Pear"]
|
||||
IMG_SIZE = 64
|
||||
|
||||
training_data = []
|
||||
|
||||
|
||||
def create_training_data():
|
||||
for category in CATEGORIES:
|
||||
path = os.path.join(DATADIR, category)
|
||||
class_num = CATEGORIES.index(category)
|
||||
for img in os.listdir(path):
|
||||
try:
|
||||
img_array = cv2.imread(os.path.join(path, img), cv2.IMREAD_GRAYSCALE)
|
||||
new_array = cv2.resize(img_array, (IMG_SIZE, IMG_SIZE))
|
||||
training_data.append([new_array, class_num])
|
||||
except Exception as e:
|
||||
pass
|
||||
```
|
||||
zdjęcia są przechowywane w tablicy training_data wraz z klasyfikacją (class_num) odpowiadającą jakim typem owocu jest zdjęcie
|
||||
|
||||
* **Krok 2** Bardzo ważnym krokiem jest pomieszanie danych. W przeciwnym wypadku nasz model po ciągłym otrzymywanie danych
|
||||
reprezentujących tylko jedną kategorię owoców mógłby się wyuczyć, aby tylko zgadywać tą kategorię.
|
||||
```python
|
||||
random.shuffle(training_data)
|
||||
```
|
||||
* **Krok 3** Ostatnim krokiem jest zaktualizowanie danych w taki sposób żeby były z przedziału
|
||||
od 0 d 255 (reprezentacja koloru danego pixela)
|
||||
```python
|
||||
X = X / 255.0
|
||||
```
|
||||
## Kilka słów na temat danych
|
||||
* **Ilość Danych** Do trenowania modelu wykorzystałem 8568 zdjęć gruszek i jabłek
|
||||
z czego mniej więcej połowa danych była jednym z typów ww. owoców, a druga połowa
|
||||
reprezentowała pozostałą kategorią
|
||||
* **Dane wykorzystane do obliczenia skutecznośći** stanowiły małą i oddzielną część danych wykorzystanych do trenowania.
|
||||
## Model
|
||||
* **Dane wejściowe:** Dane o kształcie 64x64 reprezentujące pixele w zdjęciach owoców
|
||||
* **Warstwa ukryta:** Składająca się z 128 "neuronów" wykorzystującą sigmoid jako funkcję aktywacyjną
|
||||
* **Warstwa wyjściowa:** Składająca się z 2 "neuronów" reprezentujących gruszkę i jabłko
|
||||
* **Stała ucząca:** 0.001
|
||||
|
||||
```python
|
||||
model = tf.keras.Sequential([
|
||||
tf.keras.layers.Flatten(input_shape=(64, 64)),
|
||||
tf.keras.layers.Dense(128, activation=tf.nn.sigmoid),
|
||||
tf.keras.layers.Dense(2, activation=tf.nn.sigmoid)
|
||||
])
|
||||
|
||||
model.compile(tf.keras.optimizers.Adam(lr=0.001),
|
||||
loss="sparse_categorical_crossentropy",
|
||||
metrics=["accuracy"])
|
||||
```
|
||||
## Osiągniecia modelu
|
||||
* **Trafność:** 86.4%
|
||||
* **Strata:** 0.312
|
||||
|
@ -13,12 +13,18 @@ Python 3.x
|
||||
pygame: 1.9.x
|
||||
```
|
||||
## Uruchomienie
|
||||
Projekt można uruchomić w dwóch trybach, które podajemy jako parametry:
|
||||
* test: Wizualne środowisko agenta, którym możemy sami prouszać
|
||||
* ga: Uruchomienie algorytmu genetycznego w tle. Musimy dodatkowo jako kolejny
|
||||
parametr podać ilość iteracji dla algorytmu. Możemy dodać -t, jeżeli
|
||||
chcemy uruchomić algorytm w wielu wątkach (Nie działa zbyt dobrze)
|
||||
```
|
||||
$ python Run.py
|
||||
$ python Run.py {test|ga} [iter] [-t]
|
||||
```
|
||||
## Konfiguracja
|
||||
Plik z konfiguracją znajduje w ```data/config/mainConfig.json```.
|
||||
|
||||
## Sterowanie
|
||||
* Poruszanie się: *WASD*
|
||||
* A*: ***u*** lub click myszką w jednostkę (np; królik)
|
||||
* Interakcja: *SPACJA*
|
5
Run.py
@ -1,6 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
import sys
|
||||
from src.game.Game import Game
|
||||
|
||||
# TODO: Paths are still retarded
|
||||
programPath = Path(".").resolve()
|
||||
game = Game(programPath)
|
||||
game = Game(programPath, sys.argv)
|
||||
|
BIN
data/AI_data/Nn_images/Apple/1 (115).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (151).jpg
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (27).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (37).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (53).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (8).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/1 (84).jpg
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
data/AI_data/Nn_images/Apple/2 (129).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Apple/2 (19).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/2 (59).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/2 (94).jpg
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
data/AI_data/Nn_images/Apple/3 (10).jpg
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
data/AI_data/Nn_images/Apple/3 (130).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Apple/3 (35).jpg
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
data/AI_data/Nn_images/Apple/3 (69).jpg
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
data/AI_data/Nn_images/Apple/3 (98).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Apple/37_100.jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (129).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (163).jpg
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (36).jpg
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (4).jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (64).jpg
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
data/AI_data/Nn_images/Apple/4 (95).jpg
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
data/AI_data/Nn_images/Apple/5 (129).jpg
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
data/AI_data/Nn_images/Apple/5 (33).jpg
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
data/AI_data/Nn_images/Apple/5 (61).jpg
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
data/AI_data/Nn_images/Apple/5 (92).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (104).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (132).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (163).jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (42).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (5).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Apple/6 (76).jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
data/AI_data/Nn_images/Apple/7 (114).jpg
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
data/AI_data/Nn_images/Apple/7 (145).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/7 (24).jpg
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
data/AI_data/Nn_images/Apple/7 (49).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/7 (77).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (115).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (144).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (21).jpg
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (46).jpg
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (65).jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (84).jpg
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
data/AI_data/Nn_images/Apple/8 (99).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Apple/84_100.jpg
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (123).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (151).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (161).jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (36).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (64).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (8).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Apple/9 (92).jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
data/AI_data/Nn_images/Apple/r_327_100.jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
data/AI_data/Nn_images/Apple/r_5_100.jpg
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
data/AI_data/Nn_images/Apple/r_97_100.jpg
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
data/AI_data/Nn_images/Pear/0_100.jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (12).jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (130).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (164).jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (2).jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (43).jpg
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (71).jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
data/AI_data/Nn_images/Pear/1 (99).jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (116).jpg
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (13).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (144).jpg
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (172).jpg
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (197).jpg
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (225).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (41).jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (54).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Pear/2 (91).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
data/AI_data/Nn_images/Pear/250_100.jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (114).jpg
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (142).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (170).jpg
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (18).jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (204).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (219).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (220).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (52).jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
data/AI_data/Nn_images/Pear/3 (86).jpg
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
data/AI_data/Nn_images/Pear/4 (1).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/4 (133).jpg
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
data/AI_data/Nn_images/Pear/4 (28).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/4 (62).jpg
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
data/AI_data/Nn_images/Pear/4 (99).jpg
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (125).jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (153).jpg
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (190).jpg
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (218).jpg
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (32).jpg
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
data/AI_data/Nn_images/Pear/5 (4).jpg
Normal file
After Width: | Height: | Size: 5.0 KiB |