├── organism_v1.gif ├── requirements.txt ├── Pipfile ├── README.md ├── LICENSE ├── plotting.py └── organism_v1.py /organism_v1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanrooy/evolving-simple-organisms/HEAD/organism_v1.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanrooy/evolving-simple-organisms/HEAD/requirements.txt -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | matplotlib = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.6" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evolving-simple-organisms 2 | Evolving simple organisms using a genetic algorithm and deep learning from scratch with python. 3 | 4 | ![alt text](organism_v1.gif) 5 | 6 | The tutorial for this repo is located here -> https://nathanrooy.github.io/posts/2017-11-30/evolving-simple-organisms-using-a-genetic-algorithm-and-deep-learning/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nathan Rooy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plotting.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | from matplotlib.patches import Circle 3 | import matplotlib.lines as lines 4 | 5 | from math import sin 6 | from math import cos 7 | from math import radians 8 | 9 | #--- FUNCTIONS ----------------------------------------------------------------+ 10 | 11 | def plot_organism(x1, y1, theta, ax): 12 | 13 | circle = Circle([x1,y1], 0.05, edgecolor = 'g', facecolor = 'lightgreen', zorder=8) 14 | ax.add_artist(circle) 15 | 16 | edge = Circle([x1,y1], 0.05, facecolor='None', edgecolor = 'darkgreen', zorder=8) 17 | ax.add_artist(edge) 18 | 19 | tail_len = 0.075 20 | 21 | x2 = cos(radians(theta)) * tail_len + x1 22 | y2 = sin(radians(theta)) * tail_len + y1 23 | 24 | ax.add_line(lines.Line2D([x1,x2],[y1,y2], color='darkgreen', linewidth=1, zorder=10)) 25 | 26 | pass 27 | 28 | 29 | def plot_food(x1, y1, ax): 30 | 31 | circle = Circle([x1,y1], 0.03, edgecolor = 'darkslateblue', facecolor = 'mediumslateblue', zorder=5) 32 | ax.add_artist(circle) 33 | 34 | pass 35 | 36 | #--- END ----------------------------------------------------------------------+ 37 | -------------------------------------------------------------------------------- /organism_v1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------+ 2 | # 3 | # Nathan A. Rooy 4 | # Evolving Simple Organisms 5 | # 2017-Nov. 6 | # 7 | #------------------------------------------------------------------------------+ 8 | 9 | #--- IMPORT DEPENDENCIES ------------------------------------------------------+ 10 | 11 | from __future__ import division, print_function 12 | from collections import defaultdict 13 | 14 | from matplotlib import pyplot as plt 15 | from matplotlib.patches import Circle 16 | import matplotlib.lines as lines 17 | 18 | from plotting import plot_food 19 | from plotting import plot_organism 20 | 21 | import numpy as np 22 | import operator 23 | 24 | from math import atan2 25 | from math import cos 26 | from math import degrees 27 | from math import floor 28 | from math import radians 29 | from math import sin 30 | from math import sqrt 31 | from random import randint 32 | from random import random 33 | from random import sample 34 | from random import uniform 35 | 36 | #--- CONSTANTS ----------------------------------------------------------------+ 37 | 38 | settings = {} 39 | 40 | # EVOLUTION SETTINGS 41 | settings['pop_size'] = 50 # number of organisms 42 | settings['food_num'] = 100 # number of food particles 43 | settings['gens'] = 50 # number of generations 44 | settings['elitism'] = 0.20 # elitism (selection bias) 45 | settings['mutate'] = 0.10 # mutation rate 46 | 47 | # SIMULATION SETTINGS 48 | settings['gen_time'] = 100 # generation length (seconds) 49 | settings['dt'] = 0.04 # simulation time step (dt) 50 | settings['dr_max'] = 720 # max rotational speed (degrees per second) 51 | settings['v_max'] = 0.5 # max velocity (units per second) 52 | settings['dv_max'] = 0.25 # max acceleration (+/-) (units per second^2) 53 | 54 | settings['x_min'] = -2.0 # arena western border 55 | settings['x_max'] = 2.0 # arena eastern border 56 | settings['y_min'] = -2.0 # arena southern border 57 | settings['y_max'] = 2.0 # arena northern border 58 | 59 | settings['plot'] = False # plot final generation? 60 | 61 | # ORGANISM NEURAL NET SETTINGS 62 | settings['inodes'] = 1 # number of input nodes 63 | settings['hnodes'] = 5 # number of hidden nodes 64 | settings['onodes'] = 2 # number of output nodes 65 | 66 | #--- FUNCTIONS ----------------------------------------------------------------+ 67 | 68 | def dist(x1,y1,x2,y2): 69 | return sqrt((x2-x1)**2 + (y2-y1)**2) 70 | 71 | 72 | def calc_heading(org, food): 73 | d_x = food.x - org.x 74 | d_y = food.y - org.y 75 | theta_d = degrees(atan2(d_y, d_x)) - org.r 76 | if abs(theta_d) > 180: theta_d += 360 77 | return theta_d / 180 78 | 79 | 80 | def plot_frame(settings, organisms, foods, gen, time): 81 | fig, ax = plt.subplots() 82 | fig.set_size_inches(9.6, 5.4) 83 | 84 | plt.xlim([settings['x_min'] + settings['x_min'] * 0.25, settings['x_max'] + settings['x_max'] * 0.25]) 85 | plt.ylim([settings['y_min'] + settings['y_min'] * 0.25, settings['y_max'] + settings['y_max'] * 0.25]) 86 | 87 | # PLOT ORGANISMS 88 | for organism in organisms: 89 | plot_organism(organism.x, organism.y, organism.r, ax) 90 | 91 | # PLOT FOOD PARTICLES 92 | for food in foods: 93 | plot_food(food.x, food.y, ax) 94 | 95 | # MISC PLOT SETTINGS 96 | ax.set_aspect('equal') 97 | frame = plt.gca() 98 | frame.axes.get_xaxis().set_ticks([]) 99 | frame.axes.get_yaxis().set_ticks([]) 100 | 101 | plt.figtext(0.025, 0.95,r'GENERATION: '+str(gen)) 102 | plt.figtext(0.025, 0.90,r'T_STEP: '+str(time)) 103 | 104 | plt.savefig(str(gen)+'-'+str(time)+'.png', dpi=100) 105 | ## plt.show() 106 | 107 | 108 | def evolve(settings, organisms_old, gen): 109 | 110 | elitism_num = int(floor(settings['elitism'] * settings['pop_size'])) 111 | new_orgs = settings['pop_size'] - elitism_num 112 | 113 | #--- GET STATS FROM CURRENT GENERATION ----------------+ 114 | stats = defaultdict(int) 115 | for org in organisms_old: 116 | if org.fitness > stats['BEST'] or stats['BEST'] == 0: 117 | stats['BEST'] = org.fitness 118 | 119 | if org.fitness < stats['WORST'] or stats['WORST'] == 0: 120 | stats['WORST'] = org.fitness 121 | 122 | stats['SUM'] += org.fitness 123 | stats['COUNT'] += 1 124 | 125 | stats['AVG'] = stats['SUM'] / stats['COUNT'] 126 | 127 | 128 | #--- ELITISM (KEEP BEST PERFORMING ORGANISMS) ---------+ 129 | orgs_sorted = sorted(organisms_old, key=operator.attrgetter('fitness'), reverse=True) 130 | organisms_new = [] 131 | for i in range(0, elitism_num): 132 | organisms_new.append(organism(settings, wih=orgs_sorted[i].wih, who=orgs_sorted[i].who, name=orgs_sorted[i].name)) 133 | 134 | 135 | #--- GENERATE NEW ORGANISMS ---------------------------+ 136 | for w in range(0, new_orgs): 137 | 138 | # SELECTION (TRUNCATION SELECTION) 139 | canidates = range(0, elitism_num) 140 | random_index = sample(canidates, 2) 141 | org_1 = orgs_sorted[random_index[0]] 142 | org_2 = orgs_sorted[random_index[1]] 143 | 144 | # CROSSOVER 145 | crossover_weight = random() 146 | wih_new = (crossover_weight * org_1.wih) + ((1 - crossover_weight) * org_2.wih) 147 | who_new = (crossover_weight * org_1.who) + ((1 - crossover_weight) * org_2.who) 148 | 149 | # MUTATION 150 | mutate = random() 151 | if mutate <= settings['mutate']: 152 | 153 | # PICK WHICH WEIGHT MATRIX TO MUTATE 154 | mat_pick = randint(0,1) 155 | 156 | # MUTATE: WIH WEIGHTS 157 | if mat_pick == 0: 158 | index_row = randint(0,settings['hnodes']-1) 159 | wih_new[index_row] = wih_new[index_row] * uniform(0.9, 1.1) 160 | if wih_new[index_row] > 1: wih_new[index_row] = 1 161 | if wih_new[index_row] < -1: wih_new[index_row] = -1 162 | 163 | # MUTATE: WHO WEIGHTS 164 | if mat_pick == 1: 165 | index_row = randint(0,settings['onodes']-1) 166 | index_col = randint(0,settings['hnodes']-1) 167 | who_new[index_row][index_col] = who_new[index_row][index_col] * uniform(0.9, 1.1) 168 | if who_new[index_row][index_col] > 1: who_new[index_row][index_col] = 1 169 | if who_new[index_row][index_col] < -1: who_new[index_row][index_col] = -1 170 | 171 | organisms_new.append(organism(settings, wih=wih_new, who=who_new, name='gen['+str(gen)+']-org['+str(w)+']')) 172 | 173 | return organisms_new, stats 174 | 175 | 176 | def simulate(settings, organisms, foods, gen): 177 | 178 | total_time_steps = int(settings['gen_time'] / settings['dt']) 179 | 180 | #--- CYCLE THROUGH EACH TIME STEP ---------------------+ 181 | for t_step in range(0, total_time_steps, 1): 182 | 183 | # PLOT SIMULATION FRAME 184 | if settings['plot']==True and gen==settings['gens']-1: 185 | plot_frame(settings, organisms, foods, gen, t_step) 186 | 187 | # UPDATE FITNESS FUNCTION 188 | for food in foods: 189 | for org in organisms: 190 | food_org_dist = dist(org.x, org.y, food.x, food.y) 191 | 192 | # UPDATE FITNESS FUNCTION 193 | if food_org_dist <= 0.075: 194 | org.fitness += food.energy 195 | food.respawn(settings) 196 | 197 | # RESET DISTANCE AND HEADING TO NEAREST FOOD SOURCE 198 | org.d_food = 100 199 | org.r_food = 0 200 | 201 | # CALCULATE HEADING TO NEAREST FOOD SOURCE 202 | for food in foods: 203 | for org in organisms: 204 | 205 | # CALCULATE DISTANCE TO SELECTED FOOD PARTICLE 206 | food_org_dist = dist(org.x, org.y, food.x, food.y) 207 | 208 | # DETERMINE IF THIS IS THE CLOSEST FOOD PARTICLE 209 | if food_org_dist < org.d_food: 210 | org.d_food = food_org_dist 211 | org.r_food = calc_heading(org, food) 212 | 213 | # GET ORGANISM RESPONSE 214 | for org in organisms: 215 | org.think() 216 | 217 | # UPDATE ORGANISMS POSITION AND VELOCITY 218 | for org in organisms: 219 | org.update_r(settings) 220 | org.update_vel(settings) 221 | org.update_pos(settings) 222 | 223 | return organisms 224 | 225 | 226 | #--- CLASSES ------------------------------------------------------------------+ 227 | 228 | 229 | class food(): 230 | def __init__(self, settings): 231 | self.x = uniform(settings['x_min'], settings['x_max']) 232 | self.y = uniform(settings['y_min'], settings['y_max']) 233 | self.energy = 1 234 | 235 | 236 | def respawn(self,settings): 237 | self.x = uniform(settings['x_min'], settings['x_max']) 238 | self.y = uniform(settings['y_min'], settings['y_max']) 239 | self.energy = 1 240 | 241 | 242 | class organism(): 243 | def __init__(self, settings, wih=None, who=None, name=None): 244 | 245 | self.x = uniform(settings['x_min'], settings['x_max']) # position (x) 246 | self.y = uniform(settings['y_min'], settings['y_max']) # position (y) 247 | 248 | self.r = uniform(0,360) # orientation [0, 360] 249 | self.v = uniform(0,settings['v_max']) # velocity [0, v_max] 250 | self.dv = uniform(-settings['dv_max'], settings['dv_max']) # dv 251 | 252 | self.d_food = 100 # distance to nearest food 253 | self.r_food = 0 # orientation to nearest food 254 | self.fitness = 0 # fitness (food count) 255 | 256 | self.wih = wih 257 | self.who = who 258 | 259 | self.name = name 260 | 261 | 262 | # NEURAL NETWORK 263 | def think(self): 264 | 265 | # SIMPLE MLP 266 | af = lambda x: np.tanh(x) # activation function 267 | h1 = af(np.dot(self.wih, self.r_food)) # hidden layer 268 | out = af(np.dot(self.who, h1)) # output layer 269 | 270 | # UPDATE dv AND dr WITH MLP RESPONSE 271 | self.nn_dv = float(out[0]) # [-1, 1] (accelerate=1, deaccelerate=-1) 272 | self.nn_dr = float(out[1]) # [-1, 1] (left=1, right=-1) 273 | 274 | 275 | # UPDATE HEADING 276 | def update_r(self, settings): 277 | self.r += self.nn_dr * settings['dr_max'] * settings['dt'] 278 | self.r = self.r % 360 279 | 280 | 281 | # UPDATE VELOCITY 282 | def update_vel(self, settings): 283 | self.v += self.nn_dv * settings['dv_max'] * settings['dt'] 284 | if self.v < 0: self.v = 0 285 | if self.v > settings['v_max']: self.v = settings['v_max'] 286 | 287 | 288 | # UPDATE POSITION 289 | def update_pos(self, settings): 290 | dx = self.v * cos(radians(self.r)) * settings['dt'] 291 | dy = self.v * sin(radians(self.r)) * settings['dt'] 292 | self.x += dx 293 | self.y += dy 294 | 295 | 296 | #--- MAIN ---------------------------------------------------------------------+ 297 | 298 | 299 | def run(settings): 300 | 301 | #--- POPULATE THE ENVIRONMENT WITH FOOD ---------------+ 302 | foods = [] 303 | for i in range(0,settings['food_num']): 304 | foods.append(food(settings)) 305 | 306 | #--- POPULATE THE ENVIRONMENT WITH ORGANISMS ----------+ 307 | organisms = [] 308 | for i in range(0,settings['pop_size']): 309 | wih_init = np.random.uniform(-1, 1, (settings['hnodes'], settings['inodes'])) # mlp weights (input -> hidden) 310 | who_init = np.random.uniform(-1, 1, (settings['onodes'], settings['hnodes'])) # mlp weights (hidden -> output) 311 | 312 | organisms.append(organism(settings, wih_init, who_init, name='gen[x]-org['+str(i)+']')) 313 | 314 | #--- CYCLE THROUGH EACH GENERATION --------------------+ 315 | for gen in range(0, settings['gens']): 316 | 317 | # SIMULATE 318 | organisms = simulate(settings, organisms, foods, gen) 319 | 320 | # EVOLVE 321 | organisms, stats = evolve(settings, organisms, gen) 322 | print('> GEN:',gen,'BEST:',stats['BEST'],'AVG:',stats['AVG'],'WORST:',stats['WORST']) 323 | 324 | pass 325 | 326 | 327 | #--- RUN ----------------------------------------------------------------------+ 328 | 329 | run(settings) 330 | 331 | #--- END ----------------------------------------------------------------------+ 332 | --------------------------------------------------------------------------------