gene_genie 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f634d03e4f8bce6759898a2048719471aff1e0b
4
- data.tar.gz: ff4ebfd191e17b182149935e1c544b51ceb0f46f
3
+ metadata.gz: ec1b4f9a01e789dfccd2c2ae244d88d024c2a1a1
4
+ data.tar.gz: 23d0def3b38095501ea43abf43eef535b5377cb0
5
5
  SHA512:
6
- metadata.gz: a918e63beb5265d671b2e4b46b28358fb7e57727c590f9b37300eae2b0b57fc7d19abd30b90d0d0a5fea815c6a98bbf1ddb2e65e0b0a2f1e9915fbe88467fa21
7
- data.tar.gz: 86719523c06e1fd0e35c242385a833c236745d3fe43973494d15cdab567bb43467f34bfda9211f5db1d6f1e00f820109c18f66139115e1252875357e0b2c5346
6
+ metadata.gz: 910757fa425a73ab14759642d5f28cbe0535390b0f8735439a38976941944c680de0cf7924164b19eb6f7dbea71c52f59394fe980b34c109ec89815165a1bc34
7
+ data.tar.gz: 727fe8801c9eaed03834693bc4d38601fb66b3ea9ec9bd39f423dddc7c99d39db7e5c3f0b35966020e6baf302bcf9601ea6aa23a9a9b23009676acd29fee316b
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  # Gene Genie
6
6
 
7
7
  Hey, I wrote a genetic algorithm gem. Goals:
8
- * Have fun
9
- * Be easy and intuitive to use
10
- * Be open to extension and experimentation
8
+ * Have fun
9
+ * Be easy and intuitive to use
10
+ * Be open to extension and experimentation
11
11
 
12
12
  ## Installation
13
13
 
@@ -24,32 +24,38 @@ Or install it yourself as:
24
24
  $ gem install gene_genie
25
25
 
26
26
  ## Usage
27
- Basic usage is designed to be as simple as possible. You provide two things: an exemplar and an evaluator.
28
- An exemplar is a list of variables along with their possible range of values.
27
+ Basic usage is designed to be as simple as possible. You provide two things: a template and an evaluator.
28
+ A template is a list of variables along with their possible range of values.
29
29
  An evaluator implements a fitness method that returns a numeric value.
30
30
  The genetic algorithm will then search for the set of values that maximises the fitness.
31
31
 
32
32
  ```ruby
33
33
  require 'gene_genie'
34
34
 
35
- exemplar = {
35
+ template = {
36
36
  range_of_ints: 1..10,
37
+ <!---
37
38
  range_of_floats: 1.0..4.5,
38
- set_of_items: [:apple, :banana, :orange],
39
+ set_of_items: [:apple, :banana, :orange],
39
40
  ordered_set_of_items: [:one, :two, :three],
40
41
  circular_ordered_set: [:early_morning, :morning, :noon, :afternoon,
41
42
  :evening, :midnight]
42
- }
43
+ -->
44
+ }
43
45
  ```
44
46
 
45
47
  If you use the simple genie interface, the genetic algorithm will come up with a reasonable best-guesses for various algorthm parameters, but you can dive under the covers to give yourself more flexibility.
46
48
  * Population size
47
49
  * Gene pools
48
- * Initialisation
50
+ * Initialization
49
51
  * Optimisation Criteria
50
52
 
51
53
  Custom objects for crossover, gene selection, etc.
52
54
 
55
+ Note:
56
+ Due to the non-deterministic nature of the algorithm, some of the tests don't
57
+ pass every time at the moment! This is a known issue.
58
+
53
59
  ## Contributing
54
60
 
55
61
  1. Fork it ( https://github.com/MEHColeman/gene_genie/fork )
@@ -5,11 +5,14 @@ module GeneGenie
5
5
  # @since 0.0.1
6
6
  class Gene
7
7
  def initialize(information, fitness_evaluator)
8
+ fail ArgumentError, 'information must be Array' unless information.kind_of? Array
9
+ fail ArgumentError, 'information must be Array of Hashes' unless information[0].kind_of? Hash
10
+
8
11
  @information = information
9
12
  @fitness_evaluator = fitness_evaluator
10
13
  end
11
14
 
12
- def to_hash
15
+ def to_hashes
13
16
  @information
14
17
  end
15
18
 
@@ -19,20 +22,24 @@ module GeneGenie
19
22
 
20
23
  def mutate(mutator)
21
24
  @information = mutator.call @information
25
+ @fitness = nil
22
26
  self
23
27
  end
24
28
 
25
29
  def combine(other_gene)
26
- other_gene_hash = other_gene.to_hash
27
- new_hash = {}
28
- @information.each do | k, v |
29
- new_hash[k] = (rand > 0.5) ? @information[k] : other_gene_hash[k]
30
+ other_gene_hash = other_gene.to_hashes
31
+ new_information = @information.map.with_index do |part, index|
32
+ new_hash = {}
33
+ part.each do |k, v|
34
+ new_hash[k] = (rand > 0.5) ? v : other_gene_hash[index][k]
35
+ end
36
+ new_hash
30
37
  end
31
- Gene.new(new_hash, @fitness_evaluator)
38
+ Gene.new(new_information, @fitness_evaluator)
32
39
  end
33
40
 
34
- def <=>(gene)
35
- fitness <=> gene.fitness
41
+ def <=>(other)
42
+ fitness <=> other.fitness
36
43
  end
37
44
  end
38
45
  end
@@ -15,21 +15,25 @@ module GeneGenie
15
15
  def create(size = 1)
16
16
  genes = []
17
17
  size.times do
18
- hash = create_hash_from_template
19
- genes << Gene.new(hash, @fitness_evaluator)
18
+ genes << create_gene_from_template
20
19
  end
21
-
22
20
  genes
23
21
  end
24
22
 
25
23
  private
26
24
 
27
- def create_hash_from_template
25
+ def create_gene_from_template
26
+ gene_array = @template.map do |part|
27
+ create_hash_from_template_part(part)
28
+ end
29
+ Gene.new(gene_array, @fitness_evaluator)
30
+ end
31
+
32
+ def create_hash_from_template_part(part)
28
33
  new_hash = {}
29
- @template.each do |k, v|
34
+ part.each do |k, v|
30
35
  new_hash[k] = rand(v)
31
36
  end
32
-
33
37
  new_hash
34
38
  end
35
39
  end
@@ -1,13 +1,19 @@
1
1
  require_relative 'gene_factory'
2
2
  require_relative 'mutator/simple_gene_mutator'
3
3
  require_relative 'mutator/null_mutator'
4
+ require_relative 'selector/proportional_selector'
5
+ require_relative 'template_evaluator'
4
6
 
5
7
  module GeneGenie
6
8
  class GenePool
7
- def initialize(template, fitness_evaluator, gene_factory,
8
- mutator = NullMutator.new)
9
- unless template.instance_of? Hash
10
- fail ArgumentError, 'template must be a hash of ranges'
9
+ def initialize(template:,
10
+ fitness_evaluator:,
11
+ gene_factory:,
12
+ size: 10,
13
+ mutator: NullMutator.new,
14
+ selector: ProportionalSelector.new)
15
+ unless (template.instance_of? Array) && (template[0].instance_of? Hash)
16
+ fail ArgumentError, 'template must be an array of hashes of ranges'
11
17
  end
12
18
  unless fitness_evaluator.respond_to?(:fitness)
13
19
  fail ArgumentError, 'fitness_evaluator must respond to fitness'
@@ -16,19 +22,32 @@ module GeneGenie
16
22
  @template = template
17
23
  @fitness_evaluator = fitness_evaluator
18
24
  @mutator = mutator
19
-
20
- #size = template_evaluator.recommended_size
21
- size ||= 10
25
+ @selector = selector
22
26
  @pool = gene_factory.create(size)
27
+ @generation = 0
28
+ @listeners = []
23
29
  end
24
30
 
25
31
  # build a GenePool with a reasonable set of defaults.
26
32
  # You only need to specily the minimum no. of parameters
27
33
  def self.build(template, fitness_evaluator)
34
+ unless (template.instance_of? Array) && (template[0].instance_of? Hash)
35
+ fail ArgumentError, 'template must be an array of hashes of ranges'
36
+ end
28
37
  gene_mutator = SimpleGeneMutator.new(template)
29
38
  gene_factory = GeneFactory.new(template, fitness_evaluator)
30
- GenePool.new(template, fitness_evaluator, gene_factory,
31
- gene_mutator)
39
+
40
+ template_evaluator = TemplateEvaluator.new(template)
41
+ size = template_evaluator.recommended_size
42
+ GenePool.new(template: template,
43
+ fitness_evaluator: fitness_evaluator,
44
+ gene_factory: gene_factory,
45
+ size: size,
46
+ mutator: gene_mutator)
47
+ end
48
+
49
+ def register_listener(listener)
50
+ @listeners << listener
32
51
  end
33
52
 
34
53
  def size
@@ -36,46 +55,78 @@ module GeneGenie
36
55
  end
37
56
 
38
57
  def best
39
- @pool.max_by { |gene| gene.fitness }
58
+ @pool.max_by(&:fitness)
59
+ end
60
+
61
+ def best_fitness
62
+ best.fitness
63
+ end
64
+
65
+ def best_ever
66
+ @best_ever ||= best
40
67
  end
41
68
 
42
69
  def evolve
43
70
  old_best_fitness = best.fitness
44
71
  new_pool = []
45
72
  size.times do
46
- first_gene, second_gene = select_genes
47
- new_gene = combine_genes(first_gene, second_gene)
48
- new_pool << new_gene.mutate(@mutator)
73
+ new_pool << select_genes_combine_and_mutate
49
74
  end
50
75
  @pool = new_pool
76
+ check_best_ever
77
+ @generation += 1
78
+
79
+ @listeners.each { |l| l.call(self) }
80
+
51
81
  best.fitness > old_best_fitness
52
82
  end
53
83
 
84
+ def generation
85
+ @generation
86
+ end
87
+
88
+ def average_fitness
89
+ total_fitness / @pool.size
90
+ end
91
+
92
+ def total_fitness
93
+ fitness_values.reduce(:+)
94
+ end
95
+
96
+ def genes
97
+ @pool
98
+ end
99
+
100
+ def worst
101
+ @pool.min_by(&:fitness)
102
+ end
103
+
104
+ def worst_fitness
105
+ worst.fitness
106
+ end
107
+
54
108
  private
55
- # a very simple selection - pick by sorted order
56
- # pick two different genes
109
+
110
+ def check_best_ever
111
+ @best_ever = best if best.fitness > best_ever.fitness
112
+ end
113
+
57
114
  def select_genes
58
- selectees = @pool.sort.reverse
59
- first, second = nil, nil
60
- probability = [(( 1.0/size ) * 3), 0.8].min
61
- while !first || !second do
62
- selectees.each do |s|
63
- if rand < probability
64
- selectees.delete(s)
65
- if !first
66
- first = s
67
- break
68
- else
69
- second = s
70
- end
71
- end
72
- end
73
- end
74
- [first, second]
115
+ @selector.call(self)
75
116
  end
76
117
 
77
118
  def combine_genes(first, second)
78
119
  first.combine(second)
79
120
  end
121
+
122
+ def fitness_values
123
+ @pool.map(&:fitness)
124
+ end
125
+
126
+ def select_genes_combine_and_mutate
127
+ first_gene, second_gene = select_genes
128
+ new_gene = combine_genes(first_gene, second_gene)
129
+ new_gene.mutate(@mutator)
130
+ end
80
131
  end
81
132
  end
@@ -3,13 +3,11 @@ require_relative 'gene_pool'
3
3
  # Namespace for GeneGenie genetic algorithm optimisation gem
4
4
  # @since 0.0.1
5
5
  module GeneGenie
6
-
7
6
  # Top level, basic interface for GA optimisation.
8
7
  # Genie will attempt to optimise based on best-guess defaults if none are
9
8
  # provided
10
9
  # @since 0.0.1
11
10
  class Genie
12
-
13
11
  DEFAULT_NO_OF_GENERATIONS = 50
14
12
  IMPROVEMENT_THRESHOLD = 0.1 # %
15
13
 
@@ -33,34 +31,33 @@ module GeneGenie
33
31
  end
34
32
 
35
33
  @best_fitness = @fitness_evaluator.fitness(best)
36
-
37
34
  @best_fitness > previous_best
38
35
  end
39
36
  alias_method :optimize, :optimise
40
37
 
41
38
  def best
42
- @gene_pool.best.to_hash
39
+ @gene_pool.best_ever.to_hashes
43
40
  end
44
41
 
45
42
  def best_fitness
46
- @gene_pool.best.fitness
43
+ @gene_pool.best_ever.fitness
47
44
  end
48
45
 
49
46
  private
47
+
50
48
  def evolve_n_times(n)
51
49
  n.times { @gene_pool.evolve }
52
50
  end
53
51
 
54
52
  def optimise_by_strategy
55
53
  DEFAULT_NO_OF_GENERATIONS.times do
56
- current_fitness = best_fitness
57
54
  @gene_pool.evolve
58
55
  end
59
56
  DEFAULT_NO_OF_GENERATIONS.times do
60
57
  current_fitness = best_fitness
61
58
  @gene_pool.evolve
62
59
  break if best_fitness < current_fitness *
63
- (1 + (IMPROVEMENT_THRESHOLD / 100 ))
60
+ (1 + (IMPROVEMENT_THRESHOLD / 100))
64
61
  end
65
62
  end
66
63
  end
@@ -0,0 +1,18 @@
1
+ module GeneGenie
2
+ module Listener
3
+ class LoggingListener
4
+ def initialize(logger)
5
+ @logger = logger
6
+ @last_time = Time.now
7
+ end
8
+
9
+ def call(pool)
10
+ @logger.info "Pool Generation ##{pool.generation}"
11
+ @logger.info "Average Fitness: #{pool.average_fitness}"
12
+ @logger.info "Best Fitness: #{pool.best_fitness}"
13
+ @logger.info "Time elapsed: #{Time.now - @last_time}"
14
+ @last_time = Time.now
15
+ end
16
+ end
17
+ end
18
+ end
@@ -9,13 +9,15 @@ module GeneGenie
9
9
  @mutation_rate = mutation_rate
10
10
  end
11
11
 
12
- def call(hash)
13
- hash.each do |k, v|
14
- if rand < @mutation_rate
15
- hash[k] = rand(@template[k])
12
+ def call(genes)
13
+ genes.each_with_index do |hash, index|
14
+ hash.each do |k, v|
15
+ if rand < @mutation_rate
16
+ hash[k] = rand(@template[index][k])
17
+ end
16
18
  end
17
19
  end
18
- hash
20
+ genes
19
21
  end
20
22
  end
21
23
  end
@@ -0,0 +1,31 @@
1
+ module GeneGenie
2
+ # A simple gene selection algorithm.
3
+ # Genes are ordered by score.
4
+ # Effectively, a loaded coin is tossed. If it's heads, the top gene is
5
+ # selected, otherwise, continue down the list until a head comes up.
6
+ # The coin is loaded, so that there is a reasonable spread throughout the
7
+ # gene population, but the better the gene, the more likely it is to turn up.
8
+ class CoinFlipSelector
9
+ def call(pool)
10
+ # a very simple selection - pick by sorted order
11
+ # pick two different genes
12
+ selectees = pool.genes.sort.reverse
13
+ first, second = nil, nil
14
+ probability = [((1.0 / pool.size) * 3), 0.8].min
15
+ while !first || !second do
16
+ selectees.each do |s|
17
+ if rand < probability
18
+ selectees.delete(s)
19
+ if !first
20
+ first = s
21
+ break
22
+ else
23
+ second = s
24
+ end
25
+ end
26
+ end
27
+ end
28
+ [first, second]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ module GeneGenie
2
+ # A proportional gene selection algorithm.
3
+ # Genes are picked in proportion to thier normalised score.
4
+ class ProportionalSelector
5
+ def call(pool)
6
+ [pick_one(pool), pick_one(pool)]
7
+ end
8
+
9
+ private
10
+
11
+ def pick_one(pool)
12
+ proportional_index = rand(total_normalised_fitness(pool))
13
+ total = 0
14
+ pool.genes.each_with_index do |gene, index|
15
+ total += normalised_fitness(gene,pool)
16
+ return gene if total >= proportional_index || index == (pool.size - 1)
17
+ end
18
+ end
19
+
20
+ def total_normalised_fitness(pool)
21
+ pool.genes.map { |gene| normalised_fitness(gene,pool) }.reduce(:+)
22
+ end
23
+
24
+ def normalised_fitness(gene,pool)
25
+ gene.fitness -
26
+ pool.worst_fitness + 1
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ module GeneGenie
2
+ # A Template Evaluator provides certain analysis and useful information
3
+ # about templates
4
+ # @since 0.0.2
5
+ class TemplateEvaluator
6
+ def initialize(template)
7
+ @template = template
8
+ end
9
+
10
+ def permutations
11
+ @permutations ||= @template.map { |hash|
12
+ hash.map { |_, v| v.size }.reduce(:*)
13
+ }.reduce(:*)
14
+ end
15
+
16
+ # returns a minimum of 10 unless the total number of permutations
17
+ # is below that
18
+ # otherwise, returns 1/1000th of the number of permutations up to a
19
+ # maximum of 1000
20
+ def recommended_size
21
+ [
22
+ [(permutations / 100_000), 5000].min,
23
+ [10, permutations].min,
24
+ ].max
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module GeneGenie
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gene_genie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Coleman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-28 00:00:00.000000000 Z
11
+ date: 2016-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,8 +66,7 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- description: JUST A PROTOTYPE WORK IN PROGRESS! Optimise anything that responds to
70
- 'fitness' and takes a hash
69
+ description: Optimise anything that responds to 'fitness' and takes a hash
71
70
  email:
72
71
  - m@rkcoleman.co.uk
73
72
  executables: []
@@ -80,8 +79,12 @@ files:
80
79
  - lib/gene_genie/gene_factory.rb
81
80
  - lib/gene_genie/gene_pool.rb
82
81
  - lib/gene_genie/genie.rb
82
+ - lib/gene_genie/listener/logging_listener.rb
83
83
  - lib/gene_genie/mutator/null_mutator.rb
84
84
  - lib/gene_genie/mutator/simple_gene_mutator.rb
85
+ - lib/gene_genie/selector/coin_flip_selector.rb
86
+ - lib/gene_genie/selector/proportional_selector.rb
87
+ - lib/gene_genie/template_evaluator.rb
85
88
  - lib/gene_genie/version.rb
86
89
  homepage: https://github.com/MEHColeman/gene_genie
87
90
  licenses:
@@ -95,7 +98,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
98
  requirements:
96
99
  - - ">="
97
100
  - !ruby/object:Gem::Version
98
- version: '0'
101
+ version: 2.0.0
99
102
  required_rubygems_version: !ruby/object:Gem::Requirement
100
103
  requirements:
101
104
  - - ">="
@@ -103,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
106
  version: '0'
104
107
  requirements: []
105
108
  rubyforge_project:
106
- rubygems_version: 2.4.5
109
+ rubygems_version: 2.5.1
107
110
  signing_key:
108
111
  specification_version: 4
109
112
  summary: Genetic algorithm optimisation gem