gene_genie 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +88 -15
- data/lib/gene_genie/combiner/one_point_combiner.rb +34 -0
- data/lib/gene_genie/combiner/uniform_combiner.rb +21 -0
- data/lib/gene_genie/combiner/weighted_average_combiner.rb +26 -0
- data/lib/gene_genie/gene.rb +23 -10
- data/lib/gene_genie/gene_factory.rb +15 -7
- data/lib/gene_genie/gene_pool.rb +97 -36
- data/lib/gene_genie/genie.rb +13 -8
- data/lib/gene_genie/listener/logging_listener.rb +18 -0
- data/lib/gene_genie/mutator/nudge_mutator.rb +29 -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 +20 -0
- data/lib/gene_genie/template_evaluator.rb +47 -0
- data/lib/gene_genie/version.rb +1 -1
- metadata +18 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ea71d46d33da67b5dff6976fbcf4aa6dfd73eecd0fd1c2a0fce2fe70ff02de52
|
4
|
+
data.tar.gz: 18034001ed08324f708e0395d1970aae79880f24949c1e954b453737333b04b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19139fef4bb51b4ca40d6f949f716bba442b5fc3490ccc26c0cd555fe0eb0db09ad4bdb8366a1562b63bdc9072a4425bd71989a3af0c4cb7fd22f9ddc247eb78
|
7
|
+
data.tar.gz: 1c7686c788280f0cf40f8abfdb2fee8f267e1b5cbfc087b913042553cde7245fc2f24ce9778c481bee6b19b03e99483add0f1f9d5484be4542f80ab5d318cd47
|
data/README.md
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
-
[![Build Status](https://travis-ci.org/MEHColeman/gene_genie.svg?branch=master)](https://travis-ci.org/MEHColeman/gene_genie)
|
2
1
|
[![Gem Version](https://badge.fury.io/rb/gene_genie.svg)](http://badge.fury.io/rb/gene_genie)
|
3
2
|
[![Code Climate](https://codeclimate.com/github/MEHColeman/gene_genie.png)](https://codeclimate.com/github/MEHColeman/gene_genie)
|
4
3
|
|
5
4
|
# Gene Genie
|
6
5
|
|
7
6
|
Hey, I wrote a genetic algorithm gem. Goals:
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
* Have fun
|
8
|
+
* Be easy and intuitive to use
|
9
|
+
* Be open to extension and experimentation
|
11
10
|
|
12
11
|
## Installation
|
13
12
|
|
@@ -24,31 +23,105 @@ Or install it yourself as:
|
|
24
23
|
$ gem install gene_genie
|
25
24
|
|
26
25
|
## Usage
|
27
|
-
Basic usage is designed to be as simple as possible. You provide two things:
|
28
|
-
|
26
|
+
Basic usage is designed to be as simple as possible. You provide two things: a
|
27
|
+
template and an evaluator.
|
28
|
+
|
29
|
+
A template is an array of hashes, representing a list of variables along with
|
30
|
+
their possible range of values that you wish to optimise.
|
31
|
+
|
29
32
|
An evaluator implements a fitness method that returns a numeric value.
|
30
|
-
The genetic algorithm will then search for the set of values that maximises the fitness.
|
31
33
|
|
32
|
-
|
34
|
+
The genetic algorithm will then search for the set of values that maximises the
|
35
|
+
fitness.
|
36
|
+
|
37
|
+
~~~ruby
|
33
38
|
require 'gene_genie'
|
34
39
|
|
35
|
-
|
40
|
+
template = [{
|
36
41
|
range_of_ints: 1..10,
|
42
|
+
more_ints: 3..100
|
43
|
+
}]
|
44
|
+
~~~
|
45
|
+
|
46
|
+
<!---
|
37
47
|
range_of_floats: 1.0..4.5,
|
38
48
|
set_of_items: [:apple, :banana, :orange],
|
39
49
|
ordered_set_of_items: [:one, :two, :three],
|
40
50
|
circular_ordered_set: [:early_morning, :morning, :noon, :afternoon,
|
41
51
|
:evening, :midnight]
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
-->
|
53
|
+
Typically, your fitness function will create a model represented by the values
|
54
|
+
specified in the template, evaluate the performance of that model, and return a
|
55
|
+
fitness score. But, it can be as complicated or simple as you need.
|
56
|
+
|
57
|
+
A fitness function should return a float or integer
|
58
|
+
~~~ruby
|
59
|
+
class Summer
|
60
|
+
def fitness(params)
|
61
|
+
params.inject(0) {|acc, values| acc + values.each_value.inject(&:+)}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
~~~
|
65
|
+
Then, simply create a Genie, and optimise:
|
66
|
+
~~~ruby
|
67
|
+
genie = GeneGenie::Genie.new(template, Summer.new)
|
68
|
+
genie.optimise
|
69
|
+
|
70
|
+
puts genie.best.inspect
|
71
|
+
~~~
|
72
|
+
If you want to monitor progress of the optimisation algorithm, you can register
|
73
|
+
a listener:
|
74
|
+
~~~ruby
|
75
|
+
genie.register_listener(Proc.new do |g|
|
76
|
+
puts "Best score: '#{g.best.to_hashes.to_s}', Score: #{g.best_fitness}"
|
77
|
+
end)
|
78
|
+
~~~
|
79
|
+
See the examples directory for more details.
|
80
|
+
|
81
|
+
If you use the simple `genie` interface, the genetic algorithm will come up with
|
82
|
+
reasonable best-guesses for various algorithm parameters, but you can dive under
|
83
|
+
the covers to give yourself more flexibility.
|
46
84
|
* Population size
|
47
85
|
* Gene pools
|
48
|
-
*
|
86
|
+
* Initialization
|
49
87
|
* Optimisation Criteria
|
50
88
|
|
51
|
-
|
89
|
+
|
90
|
+
## Advanced Use
|
91
|
+
|
92
|
+
If you want more control over your algorithm, you can skip the `Genie`, and use
|
93
|
+
`GenePool` directly.
|
94
|
+
|
95
|
+
This allows you to create objects that are used to control the methods used for
|
96
|
+
mutation, crossover, gene selection, gene pool parameters like population size
|
97
|
+
and convergence criteria.
|
98
|
+
~~~ruby
|
99
|
+
gene_mutator = CustomMutator.new(gm_args)
|
100
|
+
gene_factory = CustomGeneFactory.new(gf_args)
|
101
|
+
|
102
|
+
template_evaluator = CustomTemplateEvaluator.new(template)
|
103
|
+
size = template_evaluator.recommended_size
|
104
|
+
|
105
|
+
GenePool.new(template: template,
|
106
|
+
fitness_evaluator: fitness_evaluator,
|
107
|
+
gene_factory: gene_factory,
|
108
|
+
size: size,
|
109
|
+
mutator: gene_mutator)
|
110
|
+
~~~
|
111
|
+
The `mutator` operates on a `Gene` to alter it slightly, mimicking natural gene
|
112
|
+
mutations.
|
113
|
+
|
114
|
+
The `gene_factory` creates a population of genes of a given size. Genes are
|
115
|
+
typically generated randomly across the parameter space, but this allows you to
|
116
|
+
have more control over how genes are distributed across this space.
|
117
|
+
|
118
|
+
The `template_evaluator` is used to provide other configuration options to the
|
119
|
+
GenePool, such as recommended size.
|
120
|
+
|
121
|
+
---
|
122
|
+
Note:
|
123
|
+
Due to the non-deterministic nature of the algorithm, some of the tests don't
|
124
|
+
pass every time at the moment! This is a known issue.
|
52
125
|
|
53
126
|
## Contributing
|
54
127
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module GeneGenie
|
2
|
+
module Combiner
|
3
|
+
# Picks alleles from each Gene randomly
|
4
|
+
class OnePointCombiner
|
5
|
+
def call(gene_a, gene_b)
|
6
|
+
if rand >= 0.5
|
7
|
+
first_gene = gene_a
|
8
|
+
second_gene = gene_b
|
9
|
+
else
|
10
|
+
first_gene = gene_b
|
11
|
+
second_gene = gene_a
|
12
|
+
end
|
13
|
+
first_gene_hashes = first_gene.to_hashes
|
14
|
+
second_gene_hashes = second_gene.to_hashes
|
15
|
+
|
16
|
+
total_length = first_gene_hashes.map(&:size).reduce(:+)
|
17
|
+
crossover_point = rand(0..(total_length - 1))
|
18
|
+
|
19
|
+
count = 0
|
20
|
+
new_information = first_gene_hashes.map.with_index do |part, index|
|
21
|
+
new_hash = {}
|
22
|
+
part.each do |k, v|
|
23
|
+
new_hash[k] = (count >= crossover_point) ? second_gene_hashes[index][k] : v
|
24
|
+
count += 1
|
25
|
+
end
|
26
|
+
new_hash
|
27
|
+
end
|
28
|
+
Gene.new(information: new_information,
|
29
|
+
fitness_evaluator: first_gene.fitness_evaluator,
|
30
|
+
gene_combiner: self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module GeneGenie
|
2
|
+
module Combiner
|
3
|
+
# Picks alleles from each Gene randomly
|
4
|
+
class UniformCombiner
|
5
|
+
def call(first_gene, second_gene)
|
6
|
+
first_gene_hashes = first_gene.to_hashes
|
7
|
+
second_gene_hashes = second_gene.to_hashes
|
8
|
+
new_information = first_gene_hashes.map.with_index do |part, index|
|
9
|
+
new_hash = {}
|
10
|
+
part.each do |k, v|
|
11
|
+
new_hash[k] = (rand > 0.5) ? v : second_gene_hashes[index][k]
|
12
|
+
end
|
13
|
+
new_hash
|
14
|
+
end
|
15
|
+
Gene.new(information: new_information,
|
16
|
+
fitness_evaluator: first_gene.fitness_evaluator,
|
17
|
+
gene_combiner: self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GeneGenie
|
2
|
+
module Combiner
|
3
|
+
# Creates new allele value by creating a (random) weighted
|
4
|
+
# average of the two parent genes. Good for genes that represent numeric
|
5
|
+
# scalar values, but not for genes representing discrete info.
|
6
|
+
class WeightedAverageCombiner
|
7
|
+
def call(first_gene, second_gene)
|
8
|
+
first_gene_hashes = first_gene.to_hashes
|
9
|
+
second_gene_hashes = second_gene.to_hashes
|
10
|
+
new_information = first_gene_hashes.map.with_index do |part, index|
|
11
|
+
new_hash = {}
|
12
|
+
part.each do |k, v|
|
13
|
+
p_first = rand(0.0..100.0)
|
14
|
+
p_second = 100 - p_first
|
15
|
+
new_hash[k] = (((p_first * v) +
|
16
|
+
(p_second * second_gene_hashes[index][k]))/100).round
|
17
|
+
end
|
18
|
+
new_hash
|
19
|
+
end
|
20
|
+
Gene.new(information: new_information,
|
21
|
+
fitness_evaluator: first_gene.fitness_evaluator,
|
22
|
+
gene_combiner: self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/gene_genie/gene.rb
CHANGED
@@ -1,15 +1,23 @@
|
|
1
|
+
require_relative 'combiner/one_point_combiner'
|
2
|
+
|
1
3
|
module GeneGenie
|
2
4
|
# A Gene is the basic unit of the genetic algorithm. Genes hold the
|
3
5
|
# information used to evaluate their fitness.
|
4
6
|
# They are combined into new Genes during the optimisation process.
|
5
7
|
# @since 0.0.1
|
6
8
|
class Gene
|
7
|
-
def initialize(information
|
9
|
+
def initialize(information:,
|
10
|
+
fitness_evaluator:,
|
11
|
+
gene_combiner: GeneGenie::Combiner::OnePointCombiner.new)
|
12
|
+
fail ArgumentError, 'information must be Array' unless information.kind_of? Array
|
13
|
+
fail ArgumentError, 'information must be Array of Hashes' unless information[0].kind_of? Hash
|
14
|
+
|
8
15
|
@information = information
|
9
16
|
@fitness_evaluator = fitness_evaluator
|
17
|
+
@combiner = gene_combiner
|
10
18
|
end
|
11
19
|
|
12
|
-
def
|
20
|
+
def to_hashes
|
13
21
|
@information
|
14
22
|
end
|
15
23
|
|
@@ -17,22 +25,27 @@ module GeneGenie
|
|
17
25
|
@fitness ||= @fitness_evaluator.fitness(@information)
|
18
26
|
end
|
19
27
|
|
28
|
+
def fitness_evaluator
|
29
|
+
@fitness_evaluator
|
30
|
+
end
|
31
|
+
|
32
|
+
def normalised_fitness(minimum)
|
33
|
+
@normalised_fitness ||= fitness - minimum
|
34
|
+
end
|
35
|
+
|
20
36
|
def mutate(mutator)
|
21
37
|
@information = mutator.call @information
|
38
|
+
@fitness = nil
|
39
|
+
@normalised_fitness = nil
|
22
40
|
self
|
23
41
|
end
|
24
42
|
|
25
43
|
def combine(other_gene)
|
26
|
-
|
27
|
-
new_hash = {}
|
28
|
-
@information.each do | k, v |
|
29
|
-
new_hash[k] = (rand > 0.5) ? @information[k] : other_gene_hash[k]
|
30
|
-
end
|
31
|
-
Gene.new(new_hash, @fitness_evaluator)
|
44
|
+
@combiner.call(self, other_gene)
|
32
45
|
end
|
33
46
|
|
34
|
-
def <=>(
|
35
|
-
fitness <=>
|
47
|
+
def <=>(other)
|
48
|
+
fitness <=> other.fitness
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'gene'
|
2
|
+
require_relative 'combiner/weighted_average_combiner'
|
2
3
|
|
3
4
|
module GeneGenie
|
4
5
|
# GeneFactory
|
@@ -7,29 +8,36 @@ module GeneGenie
|
|
7
8
|
# The default implementation will produce random genes, but other approaches
|
8
9
|
# could be taken.
|
9
10
|
class GeneFactory
|
10
|
-
def initialize(template, fitness_evaluator)
|
11
|
+
def initialize(template, fitness_evaluator, gene_combiner=nil)
|
11
12
|
@template = template
|
12
13
|
@fitness_evaluator = fitness_evaluator
|
14
|
+
@combiner = gene_combiner || GeneGenie::Combiner::WeightedAverageCombiner.new
|
13
15
|
end
|
14
16
|
|
15
17
|
def create(size = 1)
|
16
18
|
genes = []
|
17
19
|
size.times do
|
18
|
-
|
19
|
-
genes << Gene.new(hash, @fitness_evaluator)
|
20
|
+
genes << create_gene_from_template
|
20
21
|
end
|
21
|
-
|
22
22
|
genes
|
23
23
|
end
|
24
24
|
|
25
25
|
private
|
26
26
|
|
27
|
-
def
|
27
|
+
def create_gene_from_template
|
28
|
+
gene_array = @template.map do |part|
|
29
|
+
create_hash_from_template_part(part)
|
30
|
+
end
|
31
|
+
Gene.new(information: gene_array,
|
32
|
+
fitness_evaluator: @fitness_evaluator,
|
33
|
+
gene_combiner: @combiner)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_hash_from_template_part(part)
|
28
37
|
new_hash = {}
|
29
|
-
|
38
|
+
part.each do |k, v|
|
30
39
|
new_hash[k] = rand(v)
|
31
40
|
end
|
32
|
-
|
33
41
|
new_hash
|
34
42
|
end
|
35
43
|
end
|
data/lib/gene_genie/gene_pool.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
require_relative 'gene_factory'
|
2
|
-
require_relative 'mutator/
|
2
|
+
require_relative 'mutator/nudge_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)
|
28
|
-
|
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
|
37
|
+
gene_mutator = NudgeMutator.new(template, 0.01)
|
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,88 @@ module GeneGenie
|
|
36
55
|
end
|
37
56
|
|
38
57
|
def best
|
39
|
-
@pool.max_by
|
58
|
+
@best ||= @pool.max_by(&:fitness)
|
59
|
+
end
|
60
|
+
|
61
|
+
def best_fitness
|
62
|
+
best.fitness
|
63
|
+
end
|
64
|
+
|
65
|
+
def worst
|
66
|
+
@worst ||= @pool.min_by(&:fitness)
|
67
|
+
end
|
68
|
+
|
69
|
+
def worst_fitness
|
70
|
+
worst.fitness
|
71
|
+
end
|
72
|
+
|
73
|
+
def best_ever
|
74
|
+
@best_ever ||= best
|
40
75
|
end
|
41
76
|
|
42
77
|
def evolve
|
43
78
|
old_best_fitness = best.fitness
|
44
79
|
new_pool = []
|
45
80
|
size.times do
|
46
|
-
|
47
|
-
new_gene = combine_genes(first_gene, second_gene)
|
48
|
-
new_pool << new_gene.mutate(@mutator)
|
81
|
+
new_pool << select_genes_combine_and_mutate
|
49
82
|
end
|
50
83
|
@pool = new_pool
|
84
|
+
update_stats
|
85
|
+
@generation += 1
|
86
|
+
|
87
|
+
@listeners.each { |l| l.call(self) }
|
88
|
+
|
51
89
|
best.fitness > old_best_fitness
|
52
90
|
end
|
53
91
|
|
92
|
+
def generation
|
93
|
+
@generation
|
94
|
+
end
|
95
|
+
|
96
|
+
def average_fitness
|
97
|
+
@average_fitness ||= total_fitness / @pool.size
|
98
|
+
end
|
99
|
+
|
100
|
+
def total_fitness
|
101
|
+
@total_fitness ||= fitness_values.reduce(:+)
|
102
|
+
end
|
103
|
+
|
104
|
+
def total_normalised_fitness
|
105
|
+
@total_normalised_fitness ||= normalised_fitness_values.reduce(:+)
|
106
|
+
end
|
107
|
+
|
108
|
+
def genes
|
109
|
+
@pool
|
110
|
+
end
|
111
|
+
|
54
112
|
private
|
55
|
-
|
56
|
-
|
113
|
+
|
114
|
+
def update_stats
|
115
|
+
@best = nil
|
116
|
+
@worst = nil
|
117
|
+
@total_fitness = nil
|
118
|
+
@total_normalised_fitness = nil
|
119
|
+
@average_fitness = nil
|
120
|
+
|
121
|
+
@best_ever = best if best.fitness > best_ever.fitness
|
122
|
+
end
|
123
|
+
|
57
124
|
def select_genes
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
break
|
68
|
-
else
|
69
|
-
second = s
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
[first, second]
|
125
|
+
@selector.call(self)
|
126
|
+
end
|
127
|
+
|
128
|
+
def fitness_values
|
129
|
+
@pool.map(&:fitness)
|
130
|
+
end
|
131
|
+
|
132
|
+
def normalised_fitness_values
|
133
|
+
@pool.map{ |gene| gene.normalised_fitness(worst_fitness) }
|
75
134
|
end
|
76
135
|
|
77
|
-
def
|
78
|
-
|
136
|
+
def select_genes_combine_and_mutate
|
137
|
+
first_gene, second_gene = select_genes
|
138
|
+
new_gene = first_gene.combine(second_gene)
|
139
|
+
new_gene.mutate(@mutator)
|
79
140
|
end
|
80
141
|
end
|
81
142
|
end
|
data/lib/gene_genie/genie.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
require_relative 'gene_pool'
|
2
|
+
require_relative 'listener/logging_listener'
|
2
3
|
|
3
4
|
# Namespace for GeneGenie genetic algorithm optimisation gem
|
4
5
|
# @since 0.0.1
|
5
6
|
module GeneGenie
|
6
|
-
|
7
7
|
# Top level, basic interface for GA optimisation.
|
8
8
|
# Genie will attempt to optimise based on best-guess defaults if none are
|
9
9
|
# provided
|
10
|
+
# Genie is basically a wrapper around GenePool that lets you get going as
|
11
|
+
# quickly as possible by providing a reasonable set of defaults.
|
12
|
+
# For more control and customisation, go straight to using GenePoo
|
10
13
|
# @since 0.0.1
|
11
14
|
class Genie
|
12
|
-
|
13
15
|
DEFAULT_NO_OF_GENERATIONS = 50
|
14
16
|
IMPROVEMENT_THRESHOLD = 0.1 # %
|
15
17
|
|
@@ -33,34 +35,37 @@ module GeneGenie
|
|
33
35
|
end
|
34
36
|
|
35
37
|
@best_fitness = @fitness_evaluator.fitness(best)
|
36
|
-
|
37
38
|
@best_fitness > previous_best
|
38
39
|
end
|
39
40
|
alias_method :optimize, :optimise
|
40
41
|
|
42
|
+
def register_listener(listener)
|
43
|
+
@gene_pool.register_listener(listener)
|
44
|
+
end
|
45
|
+
|
41
46
|
def best
|
42
|
-
@gene_pool.
|
47
|
+
@gene_pool.best_ever.to_hashes
|
43
48
|
end
|
44
49
|
|
45
50
|
def best_fitness
|
46
|
-
@gene_pool.
|
51
|
+
@gene_pool.best_ever.fitness
|
47
52
|
end
|
48
53
|
|
49
54
|
private
|
55
|
+
|
50
56
|
def evolve_n_times(n)
|
51
57
|
n.times { @gene_pool.evolve }
|
52
58
|
end
|
53
59
|
|
54
60
|
def optimise_by_strategy
|
55
61
|
DEFAULT_NO_OF_GENERATIONS.times do
|
56
|
-
current_fitness = best_fitness
|
57
62
|
@gene_pool.evolve
|
58
63
|
end
|
59
64
|
DEFAULT_NO_OF_GENERATIONS.times do
|
60
65
|
current_fitness = best_fitness
|
61
66
|
@gene_pool.evolve
|
62
|
-
break if best_fitness < current_fitness *
|
63
|
-
(1 + (IMPROVEMENT_THRESHOLD / 100 )
|
67
|
+
break if (best_fitness < current_fitness *
|
68
|
+
(1 + (IMPROVEMENT_THRESHOLD / 100)) && best_fitness > 0)
|
64
69
|
end
|
65
70
|
end
|
66
71
|
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GeneGenie
|
2
|
+
# A NudgeMutator is very similar to a simple nutator, except that it will
|
3
|
+
# only change a value by a small amount, rather than to any valid amount.
|
4
|
+
# So, an allele with a rather specified in the template of 1..100, with a
|
5
|
+
# current value of 50 might change in the range 45..55 instead of 1..100.
|
6
|
+
# @since 0.2.0
|
7
|
+
class NudgeMutator
|
8
|
+
def initialize(template, mutation_rate = 0.04)
|
9
|
+
@template = template
|
10
|
+
@mutation_rate = mutation_rate * 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(genes)
|
14
|
+
genes.each_with_index do |hash, index|
|
15
|
+
hash.each do |k, v|
|
16
|
+
if rand < @mutation_rate
|
17
|
+
nudge_max = (@template[index][k].size * 0.05).ceil
|
18
|
+
hash[k] = rand(
|
19
|
+
[@template[index][k].min, (v - nudge_max).floor].max..
|
20
|
+
[@template[index][k].max, (v + nudge_max).ceil].min
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
genes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -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,20 @@
|
|
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(pool.total_normalised_fitness)
|
13
|
+
total = 0
|
14
|
+
pool.genes.each_with_index do |gene, index|
|
15
|
+
total += gene.normalised_fitness(pool.worst_fitness)
|
16
|
+
return gene if total >= proportional_index || index == (pool.size - 1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module GeneGenie
|
2
|
+
# A Template Evaluator provides certain analysis and useful information
|
3
|
+
# about templates.
|
4
|
+
# A template is always treated internally as an Array of Hashes.
|
5
|
+
# @since 0.0.2
|
6
|
+
class TemplateEvaluator
|
7
|
+
def initialize(template)
|
8
|
+
@template = template
|
9
|
+
end
|
10
|
+
|
11
|
+
def permutations
|
12
|
+
@permutations ||= @template.map { |hash|
|
13
|
+
hash.map { |_, v| v.size }.reduce(:*)
|
14
|
+
}.reduce(:*)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Suggests a recommended GenePool size.
|
18
|
+
# returns a minimum of 6 unless the total number of permutations
|
19
|
+
# is below that.
|
20
|
+
# Otherwise, returns 1/1000th of the number of permutations up to a
|
21
|
+
# maximum of 20000
|
22
|
+
def recommended_size
|
23
|
+
[
|
24
|
+
[((Math.log(permutations))**2).ceil, 20000].min,
|
25
|
+
[6, permutations].min,
|
26
|
+
3
|
27
|
+
].max
|
28
|
+
end
|
29
|
+
|
30
|
+
# Verifies that the given hash conforms to the constraints specified in the
|
31
|
+
# hash template
|
32
|
+
def hash_valid?(hash_under_test)
|
33
|
+
begin
|
34
|
+
@template.each_with_index do |h, index|
|
35
|
+
h.each do |k, v|
|
36
|
+
return false unless hash_under_test[index][k]
|
37
|
+
return false unless hash_under_test[index][k] >= v.min
|
38
|
+
return false unless hash_under_test[index][k] <= v.max
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/gene_genie/version.rb
CHANGED
metadata
CHANGED
@@ -1,31 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gene_genie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Coleman
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.6'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '1.6'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
30
16
|
requirements:
|
31
17
|
- - ">="
|
@@ -39,7 +25,7 @@ dependencies:
|
|
39
25
|
- !ruby/object:Gem::Version
|
40
26
|
version: '0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
28
|
+
name: rake
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
44
30
|
requirements:
|
45
31
|
- - ">="
|
@@ -53,7 +39,7 @@ dependencies:
|
|
53
39
|
- !ruby/object:Gem::Version
|
54
40
|
version: '0'
|
55
41
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
42
|
+
name: rspec
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
58
44
|
requirements:
|
59
45
|
- - ">="
|
@@ -66,8 +52,7 @@ dependencies:
|
|
66
52
|
- - ">="
|
67
53
|
- !ruby/object:Gem::Version
|
68
54
|
version: '0'
|
69
|
-
description:
|
70
|
-
'fitness' and takes a hash
|
55
|
+
description: Optimise anything that responds to 'fitness' and takes a hash
|
71
56
|
email:
|
72
57
|
- m@rkcoleman.co.uk
|
73
58
|
executables: []
|
@@ -76,18 +61,26 @@ extra_rdoc_files: []
|
|
76
61
|
files:
|
77
62
|
- README.md
|
78
63
|
- lib/gene_genie.rb
|
64
|
+
- lib/gene_genie/combiner/one_point_combiner.rb
|
65
|
+
- lib/gene_genie/combiner/uniform_combiner.rb
|
66
|
+
- lib/gene_genie/combiner/weighted_average_combiner.rb
|
79
67
|
- lib/gene_genie/gene.rb
|
80
68
|
- lib/gene_genie/gene_factory.rb
|
81
69
|
- lib/gene_genie/gene_pool.rb
|
82
70
|
- lib/gene_genie/genie.rb
|
71
|
+
- lib/gene_genie/listener/logging_listener.rb
|
72
|
+
- lib/gene_genie/mutator/nudge_mutator.rb
|
83
73
|
- lib/gene_genie/mutator/null_mutator.rb
|
84
74
|
- lib/gene_genie/mutator/simple_gene_mutator.rb
|
75
|
+
- lib/gene_genie/selector/coin_flip_selector.rb
|
76
|
+
- lib/gene_genie/selector/proportional_selector.rb
|
77
|
+
- lib/gene_genie/template_evaluator.rb
|
85
78
|
- lib/gene_genie/version.rb
|
86
79
|
homepage: https://github.com/MEHColeman/gene_genie
|
87
80
|
licenses:
|
88
81
|
- MIT
|
89
82
|
metadata: {}
|
90
|
-
post_install_message:
|
83
|
+
post_install_message:
|
91
84
|
rdoc_options: []
|
92
85
|
require_paths:
|
93
86
|
- lib
|
@@ -95,16 +88,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
88
|
requirements:
|
96
89
|
- - ">="
|
97
90
|
- !ruby/object:Gem::Version
|
98
|
-
version:
|
91
|
+
version: 2.0.0
|
99
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
93
|
requirements:
|
101
94
|
- - ">="
|
102
95
|
- !ruby/object:Gem::Version
|
103
96
|
version: '0'
|
104
97
|
requirements: []
|
105
|
-
|
106
|
-
|
107
|
-
signing_key:
|
98
|
+
rubygems_version: 3.1.6
|
99
|
+
signing_key:
|
108
100
|
specification_version: 4
|
109
101
|
summary: Genetic algorithm optimisation gem
|
110
102
|
test_files: []
|