From 47a003f681b3115a927a2efb922fa50aa0e8210c Mon Sep 17 00:00:00 2001 From: Eric Matte <14026234@usherbrooke.ca> Date: Sun, 17 Nov 2019 11:45:33 -0500 Subject: [PATCH] Add the Shakespeare monkey test --- lib/genetic/algorithm/fitness_recorder.rb | 4 ++ .../algorithm/{optimiser.rb => optimizer.rb} | 25 +++---- lib/genetic/algorithm/resolver.rb | 15 ---- .../{optimiser_test.rb => optimizer_test.rb} | 71 ++++++++++--------- .../algorithm/shakespeare_monkey_test.rb | 57 +++++++++++++++ .../{resolver_test.rb => version_test.rb} | 2 +- 6 files changed, 108 insertions(+), 66 deletions(-) rename lib/genetic/algorithm/{optimiser.rb => optimizer.rb} (77%) delete mode 100644 lib/genetic/algorithm/resolver.rb rename test/genetic/algorithm/{optimiser_test.rb => optimizer_test.rb} (52%) create mode 100644 test/genetic/algorithm/shakespeare_monkey_test.rb rename test/genetic/algorithm/{resolver_test.rb => version_test.rb} (67%) diff --git a/lib/genetic/algorithm/fitness_recorder.rb b/lib/genetic/algorithm/fitness_recorder.rb index bc14ea3..ecc23d8 100644 --- a/lib/genetic/algorithm/fitness_recorder.rb +++ b/lib/genetic/algorithm/fitness_recorder.rb @@ -47,4 +47,8 @@ def display_best_individual def save_best_individual_genes raise "not implemented".freeze end + + def debug + false + end end diff --git a/lib/genetic/algorithm/optimiser.rb b/lib/genetic/algorithm/optimizer.rb similarity index 77% rename from lib/genetic/algorithm/optimiser.rb rename to lib/genetic/algorithm/optimizer.rb index bcf8821..d4c6abc 100644 --- a/lib/genetic/algorithm/optimiser.rb +++ b/lib/genetic/algorithm/optimizer.rb @@ -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 if index.nil? attr_accessor :fitness_calculator # (genes: Array) -> 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 @@ -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) @@ -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) diff --git a/lib/genetic/algorithm/resolver.rb b/lib/genetic/algorithm/resolver.rb deleted file mode 100644 index 55eefb5..0000000 --- a/lib/genetic/algorithm/resolver.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Genetic - module Algorithm - module Resolver - class Error < StandardError; end - # Your code goes here... - - class Ai - def tester - a = 12 + 3 - a - end - end - end - end -end diff --git a/test/genetic/algorithm/optimiser_test.rb b/test/genetic/algorithm/optimizer_test.rb similarity index 52% rename from test/genetic/algorithm/optimiser_test.rb rename to test/genetic/algorithm/optimizer_test.rb index d710cd0..8e25b62 100644 --- a/test/genetic/algorithm/optimiser_test.rb +++ b/test/genetic/algorithm/optimizer_test.rb @@ -1,51 +1,52 @@ 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) @@ -53,15 +54,15 @@ def test_mate_with_0_proability_returns_same_chromosomes 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) @@ -69,15 +70,15 @@ def test_mate_crossover 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) diff --git a/test/genetic/algorithm/shakespeare_monkey_test.rb b/test/genetic/algorithm/shakespeare_monkey_test.rb new file mode 100644 index 0000000..24c7895 --- /dev/null +++ b/test/genetic/algorithm/shakespeare_monkey_test.rb @@ -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 diff --git a/test/genetic/algorithm/resolver_test.rb b/test/genetic/algorithm/version_test.rb similarity index 67% rename from test/genetic/algorithm/resolver_test.rb rename to test/genetic/algorithm/version_test.rb index 34237eb..58a02ad 100644 --- a/test/genetic/algorithm/resolver_test.rb +++ b/test/genetic/algorithm/version_test.rb @@ -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