Skip to content

Commit

Permalink
Add the Shakespeare monkey test
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Matte committed Nov 17, 2019
1 parent 34dcccc commit 47a003f
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 66 deletions.
4 changes: 4 additions & 0 deletions lib/genetic/algorithm/fitness_recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ def display_best_individual
def save_best_individual_genes
raise "not implemented".freeze
end

def debug
false
end
end
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
module Genetic
module Algorithm
class Optimiser
class Optimizer
attr_accessor :crossover_probability # Int
attr_accessor :mutation_probability # Int
attr_accessor :population_size # Int
attr_accessor :number_of_generations # Int
attr_accessor :genes_generator # (index: Int?) -> Array<Gene> | Gene if index.nil?
attr_accessor :fitness_calculator # (genes: Array<Gene>) -> Int

def execute
population = initialize_population

fitness_recorder = FitnessRecorder.new
@number_of_generations.times.each do |generation|
if generation != @number_of_generations
population = do_natural_selection(population)
end
fitness_recorder.record_generation(generation, population)
end

fitness_recorder.display_best_individual
def execute(population = nil)
population = initialize_population if population.nil?
do_natural_selection(population)
end

private
Expand All @@ -34,6 +24,7 @@ def initialize_population

def do_natural_selection(population)
total_fitness = population.map(&:fitness).inject(:+)

new_population = []
(@population_size / 2).times.each do
parent1 = weighted_random_sampling(population, total_fitness)
Expand All @@ -49,7 +40,11 @@ def do_natural_selection(population)
end

def weighted_random_sampling(population, total_fitness)
population.max_by { |chromosome| rand**(total_fitness / chromosome.fitness) }
if total_fitness.zero?
population.sample
else
population.max_by { |chromosome| rand * (chromosome.fitness / total_fitness) }
end
end

def mate(parent1, parent2)
Expand Down
15 changes: 0 additions & 15 deletions lib/genetic/algorithm/resolver.rb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,83 +1,84 @@
require "test_helper"
require "genetic/algorithm/optimiser"
require "genetic/algorithm/optimizer"

class Genetic::Algorithm::OptimiserTest < Minitest::Test
class Genetic::Algorithm::OptimizerTest < Minitest::Test
def test_it_initialize_population
ga_optimiser = Genetic::Algorithm::Optimiser.new
ga_optimiser.population_size = population_size
ga_optimiser.genes_generator = method(:genes_generator)
ga_optimiser.fitness_calculator = method(:fitness_calculator)
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.population_size = population_size
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)

test_population = ga_optimiser.send(:initialize_population)
test_population = ga_optimizer.send(:initialize_population)

assert(test_population.length == population_size)
assert(test_population.map(&:fitness).inject(:+) >= population_size)
end

def test_it_do_natural_selection
ga_optimiser = Genetic::Algorithm::Optimiser.new
ga_optimiser.crossover_probability = 0.7
ga_optimiser.mutation_probability = 0.1
ga_optimiser.population_size = population_size
ga_optimiser.genes_generator = method(:genes_generator)
ga_optimiser.fitness_calculator = method(:fitness_calculator)
first_population = ga_optimiser.send(:initialize_population)
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.crossover_probability = 0.7
ga_optimizer.mutation_probability = 0.5
ga_optimizer.population_size = population_size
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)
first_population = ga_optimizer.send(:initialize_population)
last_population = first_population

20.times do
last_population = ga_optimiser.send(:do_natural_selection, last_population)
100.times do
last_population = ga_optimizer.send(:do_natural_selection, last_population)
end

assert(first_population.length == population_size)
assert(last_population.length == population_size)

assert(first_population != last_population)
first_gen_fitness = first_population.map(&:fitness).inject(:+)
last_gen_fitness = last_population.map(&:fitness).inject(:+)
assert(first_gen_fitness < last_gen_fitness)
assert(first_gen_fitness != last_gen_fitness)
end

def test_mate_with_0_proability_returns_same_chromosomes
ga_optimiser = Genetic::Algorithm::Optimiser.new
ga_optimiser.crossover_probability = 0.0
ga_optimiser.mutation_probability = 0.0
ga_optimiser.genes_generator = method(:genes_generator)
ga_optimiser.fitness_calculator = method(:fitness_calculator)
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.crossover_probability = 0.0
ga_optimizer.mutation_probability = 0.0
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)

parent1 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
parent2 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
child1, child2 = ga_optimiser.send(:mate, parent1, parent2)
child1, child2 = ga_optimizer.send(:mate, parent1, parent2)

assert(parent1.genes != parent2.genes)
assert(child1.genes == parent1.genes)
assert(child2.genes == parent2.genes)
end

def test_mate_crossover
ga_optimiser = Genetic::Algorithm::Optimiser.new
ga_optimiser.crossover_probability = 1.0
ga_optimiser.mutation_probability = 0.0
ga_optimiser.genes_generator = method(:genes_generator)
ga_optimiser.fitness_calculator = method(:fitness_calculator)
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.crossover_probability = 1.0
ga_optimizer.mutation_probability = 0.0
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)

parent1 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
parent2 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
child1, child2 = ga_optimiser.send(:mate, parent1, parent2)
child1, child2 = ga_optimizer.send(:mate, parent1, parent2)

assert(parent1.genes != parent2.genes)
assert(child1.genes != parent1.genes)
assert(child2.genes != parent2.genes)
end

def test_mate_make_one_mutation_per_child
ga_optimiser = Genetic::Algorithm::Optimiser.new
ga_optimiser.crossover_probability = 0.0
ga_optimiser.mutation_probability = 1.0
ga_optimiser.genes_generator = method(:genes_generator)
ga_optimiser.fitness_calculator = method(:fitness_calculator)
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.crossover_probability = 0.0
ga_optimizer.mutation_probability = 1.0
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)

parent1 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
parent2 = Genetic::Algorithm::Chromosome.new(genes_generator, method(:fitness_calculator))
child1, child2 = ga_optimiser.send(:mate, parent1, parent2)
child1, child2 = ga_optimizer.send(:mate, parent1, parent2)

assert(parent1.genes != parent2.genes)
assert(child1.genes != parent1.genes)
Expand Down
57 changes: 57 additions & 0 deletions test/genetic/algorithm/shakespeare_monkey_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "test_helper"

class Genetic::Algorithm::ShakespeareMonkeyTest < Minitest::Test
def test_that_it_resolves_the_monkey_problem
ga_optimizer = Genetic::Algorithm::Optimizer.new
ga_optimizer.population_size = 200
ga_optimizer.crossover_probability = 0.7
ga_optimizer.mutation_probability = 0.5
ga_optimizer.genes_generator = method(:genes_generator)
ga_optimizer.fitness_calculator = method(:fitness_calculator)

goal_phrase = goal.join

generation = 0
last_population = nil
while generation < 500
new_population = ga_optimizer.execute(last_population)

best_one = new_population.max_by(&:fitness)
best_phrase = best_one.genes.join

if best_phrase == goal_phrase
puts "Goal found! \"#{goal_phrase}\" in #{generation} generations."
break
else
puts "#{best_phrase} - #{best_one.fitness * 100}% (G-#{generation})"
end

generation += 1
last_population = new_population
end

assert(true, generation < 500)
end

private

def goal
"Make Time Count.".split("")
end

def letters
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.!? ".split("")
end

def genes_generator(index = nil)
index.nil? ? Array.new(goal.length) { letters.sample } : letters.sample
end

def fitness_calculator(genes)
fitness = 0.0
goal.each_with_index do |letter, i|
fitness += 1 if letter == genes[i]
end
fitness / goal.length
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "test_helper"

class Genetic::Algorithm::ResolverTest < Minitest::Test
class Genetic::Algorithm::VersionTest < Minitest::Test
def test_that_it_has_a_version_number
refute_nil Genetic::Algorithm::VERSION
end
Expand Down

0 comments on commit 47a003f

Please sign in to comment.