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 +4 -4
- data/README.md +15 -9
- data/lib/gene_genie/gene.rb +15 -8
- data/lib/gene_genie/gene_factory.rb +10 -6
- data/lib/gene_genie/gene_pool.rb +83 -32
- data/lib/gene_genie/genie.rb +4 -7
- data/lib/gene_genie/listener/logging_listener.rb +18 -0
- data/lib/gene_genie/mutator/simple_gene_mutator.rb +7 -5
- data/lib/gene_genie/selector/coin_flip_selector.rb +31 -0
- data/lib/gene_genie/selector/proportional_selector.rb +29 -0
- data/lib/gene_genie/template_evaluator.rb +27 -0
- data/lib/gene_genie/version.rb +1 -1
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec1b4f9a01e789dfccd2c2ae244d88d024c2a1a1
|
4
|
+
data.tar.gz: 23d0def3b38095501ea43abf43eef535b5377cb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
9
|
-
|
10
|
-
|
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:
|
28
|
-
|
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
|
-
|
35
|
+
template = {
|
36
36
|
range_of_ints: 1..10,
|
37
|
+
<!---
|
37
38
|
range_of_floats: 1.0..4.5,
|
38
|
-
|
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
|
-
*
|
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 )
|
data/lib/gene_genie/gene.rb
CHANGED
@@ -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
|
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.
|
27
|
-
|
28
|
-
|
29
|
-
|
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(
|
38
|
+
Gene.new(new_information, @fitness_evaluator)
|
32
39
|
end
|
33
40
|
|
34
|
-
def <=>(
|
35
|
-
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/gene_genie/gene_pool.rb
CHANGED
@@ -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
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
31
|
-
|
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
|
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
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
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
|
data/lib/gene_genie/genie.rb
CHANGED
@@ -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.
|
39
|
+
@gene_pool.best_ever.to_hashes
|
43
40
|
end
|
44
41
|
|
45
42
|
def best_fitness
|
46
|
-
@gene_pool.
|
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(
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
data/lib/gene_genie/version.rb
CHANGED
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
|
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:
|
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:
|
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:
|
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.
|
109
|
+
rubygems_version: 2.5.1
|
107
110
|
signing_key:
|
108
111
|
specification_version: 4
|
109
112
|
summary: Genetic algorithm optimisation gem
|