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 +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
|