Ant Colony Optimization...Genetic Programming & Pygame
Using DEAP module to learn effective behavioral function for ants with food in the vicinity. The visual representation is generated using PyGame game engine.
Problem Statement that we solve using genetic programming
For an ant, such that if left in an environment, could follow a given algorithm and easily find food and eat and thrive....What is the algorithm(tree) which can be formed using the basic building components given as a Primitive Set(node).
If you are new to genetic algorithms, please head over to this article for the basics. Then head over to this for the Introduction into genetic Programming using DEAP framework.
Please note, that the code for this article is a derivation of the ant colony optimization example, from DEAP evolutionary programming module. We will modify the code to add the visual representation of an ant and the food using pygame module.
Overview
Let's look at the high level steps we will take in this article,
- Create an Ant class with methods corresponding to moving, eating, etc.
- Setup Pygame:
pygame
to render ant and the food. - Using Deap:
deap
to handle all the genetic programming stuff.
Ant Class
Create a class for ant called AntSimulator
, that will accommodate movement methods which transforms the position of an ant, and if it so happens that the food is where the ant is then it eats the food and food is disappeared.
class AntSimulator(object): direction = ["north","east","south","west"] dir_row = [1, 0, -1, 0] dir_col = [0, 1, 0, -1] def __init__(self, max_moves): self.max_moves = max_moves self.moves = 0 self.eaten = 0 self.routine = None def _reset(self): self.row = self.row_start self.col = self.col_start self.dir = 1 self.moves = 0 self.eaten = 0 self.matrix_exc = copy.deepcopy(self.matrix) @property def position(self): return (self.row, self.col, self.direction[self.dir]) def turn_left(self): if self.moves < self.max_moves: self.moves += 1 self.dir = (self.dir - 1) % 4 def turn_right(self): if self.moves < self.max_moves: self.moves += 1 self.dir = (self.dir + 1) % 4 def move_forward(self): if self.moves < self.max_moves: self.moves += 1 self.row = (self.row + self.dir_row[self.dir]) % self.matrix_row self.col = (self.col + self.dir_col[self.dir]) % self.matrix_col if self.matrix_exc[self.row][self.col] == "food": self.eaten += 1 self.matrix_exc[self.row][self.col] = "passed" def sense_food(self): ahead_row = (self.row + self.dir_row[self.dir]) % self.matrix_row ahead_col = (self.col + self.dir_col[self.dir]) % self.matrix_col return self.matrix_exc[ahead_row][ahead_col] == "food" def if_food_ahead(self, out1, out2): return partial(if_then_else, self.sense_food, out1, out2)
Along with these methods, we also need an ant to play its own algorithm which is constructed using the primitives we have defined(in the section of genetic programming). This is done using run()
method.
def run(self,routine,individual): self._reset() global gen global start_gen if gen >= start_gen: print(individual) while self.moves < self.max_moves: routine() if gen >= start_gen: renderFunction(self) def parse_matrix(self, matrix): self.matrix = list() for i, line in enumerate(matrix): self.matrix.append(list()) for j, col in enumerate(line): if col == "#": self.matrix[-1].append("food") elif col == ".": self.matrix[-1].append("empty") elif col == "S": self.matrix[-1].append("empty") self.row_start = self.row = i self.col_start = self.col = j self.dir = 1 self.matrix_row = len(self.matrix) self.matrix_col = len(self.matrix[0]) self.matrix_exc = copy.deepcopy(self.matrix)
We also need a method to parse our map of the world that has food positions in it. That is why we also created parse_matrix
. The matrix
parameter is obtained by reading a text file called santafe_trail.txt
, which is also taken straight from DEAP module for evolutionary algorithms.
S###............................ ...#............................ ...#.....................###.... ...#....................#....#.. ...#....................#....#.. ...####.#####........##......... ............#................#.. ............#.......#........... ............#.......#........#.. ............#.......#........... ....................#........... ............#................#.. ............#................... ............#.......#.....###... ............#.......#..#........ .................#.............. ................................ ............#...........#....... ............#...#..........#.... ............#...#............... ............#...#............... ............#...#.........#..... ............#..........#........ ............#................... ...##. .#####....#............... .#..............#............... .#..............#............... .#......#######................. .#.....#........................ .......#........................ ..####.......................... ................................
This is the map of the world and #
are the food placed inside the 2-D plane of the ant. Ant has to maximize its eaten
by quickly finding all the food before its lifetime is up, which is 600 steps in my example. Let's initialize our ant.
ant = AntSimulator(600)
In the next step, we shall add code to render the map with food and the ant, along with how well eaten
she is.
Pygame
The code required for Pygame setup incudes,
import pygame pygame.init() scale=10 window = pygame.display.set_mode((800, 600)) def renderFunction(ant): window.fill((255, 255, 255)) for i in range(len(ant.matrix_exc)): for j in range(len(ant.matrix_exc[ant.row])): if ant.matrix_exc[i][j] == "food": pygame.draw.circle(window,(34,34,23),(i*scale,j*scale),5) pygame.draw.circle(window,(244,34,23),(ant.row*scale,ant.col*scale),10) pygame.draw.circle(window,(244,34,23),(ant.row*scale,ant.col*scale),ant.eaten,2) pygame.time.wait(100) pygame.display.update()
This is not a tutorial on Pygame by any angle, but then we do not need that much to go on. Let me explain what we are doing here,
- Importing
pygame
an initializing it by calling itspygame.init()
function. - Creating a window object, setting the resolution to 800x600.
- Create a render function which, in each call, renders all the food as black dots and the ant by its red dot. To denote how eaten the ant is, I have used a red hollow circle which has it's radius as
ant.eaten
. - We then sleep for 100ms in between the loop and then update the display,
pygame.display.update()
.
Now we go ahead and add the genetic programming code.
Deap: Genetic Programming Setup
First add the imports and some basic utility functions to be added as primitives,
from deap import algorithms from deap import base from deap import creator from deap import tools from deap import gp def progn(*args): for arg in args: arg() def prog2(out1, out2): return partial(progn,out1,out2) def prog3(out1, out2, out3): return partial(progn,out1,out2,out3) def if_then_else(condition, out1, out2): out1() if condition() else out2()
Let's construct the primitives set required by the deap
framework.
pset = gp.PrimitiveSet("MAIN", 0) pset.addPrimitive(ant.if_food_ahead, 2) pset.addPrimitive(prog2, 2) pset.addPrimitive(prog3, 3) # also add ant methods to move to our set of primitives. pset.addTerminal(ant.move_forward) pset.addTerminal(ant.turn_left) pset.addTerminal(ant.turn_right)
Creating the toolbar which will be a container for all the functions used during the evolution process.
creator.create("FitnessMax", base.Fitness, weights=(1.0,)) creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMax) toolbox = base.Toolbox() # Attribute generator toolbox.register("expr_init", gp.genFull, pset=pset, min_=1, max_=10) # Structure initializers toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr_init) toolbox.register("population", tools.initRepeat, list, toolbox.individual) # some functions to keep track of the generation, although it does not work as expected. funcCalled=0 gen=0 start_gen=50 def evalArtificialAnt(individual): global funcCalled global gen # Transform the tree expression to functional Python code routine = gp.compile(individual, pset) # Run the generated routine ant.run(routine,individual) #we keep an account of 300 population and then increment the generation counter if funcCalled==300: funcCalled=0 gen+=1 print("gen incremented:",gen) funcCalled+=1 # print(self.moves) return ant.eaten, toolbox.register("evaluate", evalArtificialAnt) toolbox.register("select", tools.selTournament, tournsize=7) toolbox.register("mate", gp.cxOnePoint) toolbox.register("expr_mut", gp.genFull, min_=0, max_2) toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr_mut, pset=pset)
This code is very to the code example present in the Symbolic Regression Article. So now, we give the last remaining main() which will kick start the process.
def main(): random.seed(69) with open("santafe_trail.txt") as trail_file: ant.parse_matrix(trail_file) pop = toolbox.population(n=300) hof = tools.HallOfFame(1) stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("avg", numpy.mean) stats.register("std", numpy.std) stats.register("min", numpy.min) stats.register("max", numpy.max) algorithms.eaSimple(pop, toolbox, 0.5, 0.2, 200, stats, halloffame=hof) return pop, hof, stats if __name__ == "__main__": main()
Now, with tha we are all set to start training, we shall see the screen after 70-80 generations because of a condition inside the renderFrame
function.
Results
Execute the code by saving it to the file ant_gp.py
and let us begin,
$ python3 ant_gp.py
The result is in this video,
One of the corresponding algorithms that is learnt by the ant is shown below,
prog2( prog2( if_food_ahead( prog3( move_forward, move_forward, turn_left ), prog2( turn_right, turn_right ) ), if_food_ahead( move_forward, if_food_ahead( move_forward, turn_right ) ) ), prog2( turn_left, turn_right ) )
Full Source Code: My Github Page!
Conclusion
We started with 300 individual functions which defined the brain of the ants. We recursively called the function again for the given time steps, recording the position the ants move to.
Every time an ant eats a food item, its ant.eaten
value gets incremented and the best eaten value ants are selected for mating for the next generation.
We saw that the 32nd generation ants are capable enough to eat all the food available in the map.