gene_genie 0.0.1 → 0.1.1
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 +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
|
-
[](https://travis-ci.org/MEHColeman/gene_genie)
|
2
1
|
[](http://badge.fury.io/rb/gene_genie)
|
3
2
|
[](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: []
|