Genetic Algorithms 2: Girl with a Pearl Earring
20 Mar 2021, Samuel Hinton
A gentle introduction to genetic algorithms by recreating painting classics
This is what we're going to make in this tutorial. Building on from the previous discussion in part one, we now add population and genetic mixing into the algorithm.
In the prior article we evolved a painting by the process of having a single organism that we mutated over time. We aim to improve this algorithm in this step by adding multiple different organisms into the population, and allowing those organisms to mate and produce offspring.
Let's write out our imports now to get them out of the way.
import os
import numpy as np
from numpy.random import choice, random, normal
from colour import Color
import json
import pygame # Pygame for nice fast drawing
As before, we define an organism. You'll note it looks very much like the one from the previous article, except that now I have increased the chance the when a gene mutates, its the hue of the cirlce that changes (hue being located in index 3).
Here lie the main changes from the previous section - our population now acutally has a population. Dramatic stuff. Specifically for changes, we now have:
spawn
spawns a population now, not just one individualsave
andload
allow us to save out and load in a population so I can give my poor laptop a break when it starts to melt.get_child
takes two parents and picks genes from bothmutate_and_pick
which tries several times to mutate the organism to a better versionstep
which now generates childre, mutates them, and then picks the cream of the crop to survive.
With this population, we can create a handy helper function (again) which points to a reference image, sets an output directup up, and then either loads the checkpoint, or starts anew if its not found!
When I feel like I have enough samples I'll terminate the function myself - those are more steps than my laptop could ever generate.
def evolve(rate, scale, add_chance, steps=700000):
pop = Population("genetic2/earring.png")
outdir = f"genetic2/output/"
os.makedirs(outdir, exist_ok=True)
save = outdir + "save.json"
if os.path.exists(save):
pop.load(save)
start = int(sorted(os.listdir(outdir))[-2][:-4]) * 2
else:
pop.spawn(complexity=20)
start = 0
for i in range(start, steps):
pop.step(i, outdir, rate=rate, scale=scale, add=add_chance)
With this all set up, we can now call evolve
and see what we get. Here's our starting point again:

And now lets kick it off:
I also grabbed some snapshots of the population at the start and then as we progress further. The three images are the population at the start, after 40 iterations, and much later one. Notice that even though the populations grow more similar, each organism is still unique.



Using our good friend ffmpeg
to turn some of these PNGs into an animation, we get this:
And heres a side by side:

This still took quite a whil to run on my laptop, and there are existing solutions out there. If you have a serious problem and require an efficient and sophisticated genetic algorithm to help you out, check out the DEAP python package.
Connect to stay in the loop for tutorials and posts.
Samuel Hinton
Astrophysicist & Data Scientist
Here's the full code for convenience: