gene_genie 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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