metaheuristics 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +122 -0
- data/examples/example_minimiser.rb +90 -0
- data/lib/metaheuristics/genome_search/alps_ga.rb +229 -0
- data/lib/metaheuristics/genome_search/fgrn_ga.rb +86 -0
- data/lib/metaheuristics/genome_search/tournament_selection.rb +77 -0
- data/lib/metaheuristics/individual_solution.rb +42 -0
- data/lib/metaheuristics/metaheuristic_init_factory.rb +79 -0
- data/lib/metaheuristics/metaheuristic_interface.rb +13 -0
- data/lib/metaheuristics/search_results.rb +44 -0
- metadata +55 -0
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
Metaheustics
|
3
|
+
============
|
4
|
+
|
5
|
+
A ruby library of metaheuristics for search/optimisation.
|
6
|
+
|
7
|
+
|
8
|
+
Usage
|
9
|
+
-----
|
10
|
+
|
11
|
+
For a given problem, the following must be implemented:
|
12
|
+
* a genome class (details below)
|
13
|
+
* a lambda taking no argument and producing a different randomly generated
|
14
|
+
genome each time it is called
|
15
|
+
* an evaluator lambda taking as argument a `genome` object, and returning a
|
16
|
+
fitness object which much implement the `Comparable` interface (usually
|
17
|
+
float is used)
|
18
|
+
|
19
|
+
|
20
|
+
The genome class must implements the following methods:
|
21
|
+
* `clone()` return a deep copy of the genome
|
22
|
+
* `mutate!(mutation_rate)` mutates each gene of the genome with probability `mutation_rate`, return `self`
|
23
|
+
* `crossover(genome)` return a new genome which is a combination of this genome and the `genome` parameter
|
24
|
+
|
25
|
+
|
26
|
+
require 'metaheuristics/metaheuristic_init_factory'
|
27
|
+
|
28
|
+
search_definition = {
|
29
|
+
:name => :tournament_selection,
|
30
|
+
:mutation_rate => 0.1 # depends on the number of genes in a genome
|
31
|
+
}
|
32
|
+
|
33
|
+
search_init = MetaheuristicInitFactory.from_definition(search_definition)
|
34
|
+
|
35
|
+
search = search_init.call(
|
36
|
+
:genome_init => genome_init, # the lambda generating random genomes
|
37
|
+
:evaluator => evaluator_minimiser) # the evaluator lambda
|
38
|
+
|
39
|
+
# run the search until the fitness is over 9000!
|
40
|
+
begin
|
41
|
+
search.run_once
|
42
|
+
end while search.results.best[:fitness] <= 9000
|
43
|
+
|
44
|
+
|
45
|
+
A rule of thumb for the mutation rate is to aim for one mutation per genome, the
|
46
|
+
mutation rate should then be the inverse of the number of genes in a genome.
|
47
|
+
|
48
|
+
The file `examples/example_minimiser.rb` demonstrates usage for a simple minimisation problem.
|
49
|
+
|
50
|
+
|
51
|
+
Implementing new algorithms
|
52
|
+
---------------------------
|
53
|
+
|
54
|
+
New algorithms should implement the `MetaheuristicInterface`, and be given an
|
55
|
+
entry in `MetaheuristicInitFactory::from_definition()`. Then if you are happy
|
56
|
+
with the licensing, please send me a pull request!
|
57
|
+
|
58
|
+
|
59
|
+
Algorithms Details
|
60
|
+
------------------
|
61
|
+
|
62
|
+
At the moment `metaheuristics` contains only Genetic Algorithms (GAs), but has
|
63
|
+
vocation to not only contain genome based methods, but also eventually
|
64
|
+
array-of-values based methods.
|
65
|
+
|
66
|
+
* The traditional tournament selection.
|
67
|
+
* An implementation of the ALPS paradigm using tournament selection in the layers.
|
68
|
+
* FgrnGa, a GA in which fittest individual are kept in the population across several generations.
|
69
|
+
|
70
|
+
If you don't know which one to use, ALPS is recommended.
|
71
|
+
|
72
|
+
For more on metaheuristics, check out [Sean Luke's great
|
73
|
+
book][http://cs.gmu.edu/~sean/book/metaheuristics/], available for download for
|
74
|
+
free, or in printed paper form for money.
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
### Tournament selection
|
79
|
+
|
80
|
+
A simple, basic GA, where each parent is selected by taking a random sample
|
81
|
+
of the population, and choosing the fittest individual in that sample.
|
82
|
+
|
83
|
+
|
84
|
+
### Age-Layered Population Structure (ALPS)
|
85
|
+
|
86
|
+
An implementation of the ALPS paradigm which aims to prevent premature
|
87
|
+
convergence, combined here with tournament selection in the layers.
|
88
|
+
[Greg Hornby's ALPS paper][http://idesign.ucsc.edu/papers/hornby_gecco06.pdf]
|
89
|
+
|
90
|
+
|
91
|
+
### FgrnGa
|
92
|
+
|
93
|
+
An implementation of a GA keeping fit parents across multiple generations.
|
94
|
+
It has notably been used to evolve FGRN genomes, which explains the naming.
|
95
|
+
A description is available in Peter Bentley's thesis.
|
96
|
+
|
97
|
+
|
98
|
+
License
|
99
|
+
-------
|
100
|
+
|
101
|
+
This software is provided under an open source MIT-like license:
|
102
|
+
|
103
|
+
Copyright (c) 2012 Jean-Baptiste Krohn <http://susano.org>
|
104
|
+
|
105
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
106
|
+
this software and associated documentation files (the "Software"), to deal in
|
107
|
+
the Software without restriction, including without limitation the rights to
|
108
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
109
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
110
|
+
so, subject to the following conditions:
|
111
|
+
|
112
|
+
The above copyright notice and this permission notice shall be included in all
|
113
|
+
copies or substantial portions of the Software.
|
114
|
+
|
115
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
116
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
117
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
118
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
119
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
120
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
121
|
+
SOFTWARE.
|
122
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'metaheuristics/metaheuristic_init_factory'
|
2
|
+
|
3
|
+
class GenomeValues
|
4
|
+
|
5
|
+
attr_reader :values
|
6
|
+
|
7
|
+
# randomly generate a new random genome with +size+ components in the real range [-1.0, 1.0]
|
8
|
+
def self.new_random(size)
|
9
|
+
self.new(Array.new(size){ (rand - 0.5) * 2.0 })
|
10
|
+
end
|
11
|
+
|
12
|
+
# ctor
|
13
|
+
def initialize(values)
|
14
|
+
@values = values
|
15
|
+
end
|
16
|
+
|
17
|
+
# clone
|
18
|
+
def clone
|
19
|
+
self.new(values.clone)
|
20
|
+
end
|
21
|
+
|
22
|
+
# mutate this genome, return self
|
23
|
+
def mutate!(mutation_rate)
|
24
|
+
@values.size.times do |i|
|
25
|
+
@values[i] += (rand - 0.5) * 0.05 if rand < mutation_rate
|
26
|
+
end
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# crossover this genome with another one, and return the resulting offspring
|
32
|
+
def crossover(genome)
|
33
|
+
self_values = @values
|
34
|
+
other_values = genome.values
|
35
|
+
|
36
|
+
values =
|
37
|
+
Array.new(self_values.size) do |i|
|
38
|
+
[self_values, other_values][rand(2)][i]
|
39
|
+
end
|
40
|
+
|
41
|
+
GenomeValues.new(values)
|
42
|
+
end
|
43
|
+
end # GenomeValues
|
44
|
+
|
45
|
+
|
46
|
+
# lambda: generate a genome with 8 values randomly generated in the real range [-1.0, 1.0]
|
47
|
+
genome_init = lambda do
|
48
|
+
GenomeValues.new_random(8)
|
49
|
+
end
|
50
|
+
|
51
|
+
search_definition = {
|
52
|
+
:name => :tournament_selection,
|
53
|
+
:population_size => 100,
|
54
|
+
:tournament_size => 10,
|
55
|
+
:elitism => 5,
|
56
|
+
:mutation_rate => 0.2
|
57
|
+
}
|
58
|
+
|
59
|
+
search_init = MetaheuristicInitFactory.from_definition(search_definition)
|
60
|
+
|
61
|
+
# an evaluator which returns a higher fitness for components with lower
|
62
|
+
# distance to the origin point (i.e minimises the absolute value of the
|
63
|
+
# components).
|
64
|
+
evaluator_minimiser = lambda do |genome|
|
65
|
+
sum_of_squares = genome.values.collect{ |v| v * v }.inject(&:+)
|
66
|
+
fitness = 1.0 / (1.0 + sum_of_squares)
|
67
|
+
|
68
|
+
fitness
|
69
|
+
end
|
70
|
+
|
71
|
+
search = search_init.call(
|
72
|
+
:genome_init => genome_init,
|
73
|
+
:evaluator => evaluator_minimiser)
|
74
|
+
|
75
|
+
highest_fitness = 0.0
|
76
|
+
|
77
|
+
while highest_fitness < 0.99999999995
|
78
|
+
search.run_once
|
79
|
+
|
80
|
+
results = search.results
|
81
|
+
best_hash = results.best
|
82
|
+
generation = results.generation
|
83
|
+
|
84
|
+
highest_fitness = best_hash[:fitness]
|
85
|
+
fitness_string = ('%.10f' % highest_fitness).rjust(10)
|
86
|
+
generation_string = generation.to_s.rjust(5)
|
87
|
+
values_string = best_hash[:solution].values.collect{ |v| ('%.6f' % v).rjust(9) }.join(', ')
|
88
|
+
puts " generation #{generation_string} fitness: #{fitness_string} values: #{values_string}"
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'metaheuristics/metaheuristic_interface'
|
2
|
+
require 'metaheuristics/search_results'
|
3
|
+
require 'utils/assert'
|
4
|
+
|
5
|
+
class AlpsGa < MetaheuristicInterface
|
6
|
+
|
7
|
+
# From Hornby 2006 ALPS
|
8
|
+
DEFAULTS = {
|
9
|
+
:layer_size => 100,
|
10
|
+
:max_layer_count => 10,
|
11
|
+
:tournament_size => 7,
|
12
|
+
:age_gap => 10,
|
13
|
+
:aging_scheme => :polynomial,
|
14
|
+
:layer_elitism => 3,
|
15
|
+
:overall_elitism => 0,
|
16
|
+
:parents_from_previous_layer => true
|
17
|
+
}
|
18
|
+
|
19
|
+
attr_reader :results
|
20
|
+
|
21
|
+
class Individual
|
22
|
+
include Comparable
|
23
|
+
|
24
|
+
attr_reader :age, :solution, :fitness
|
25
|
+
|
26
|
+
# ctor
|
27
|
+
def initialize(solution, age = 0)
|
28
|
+
@solution = solution
|
29
|
+
@age = age
|
30
|
+
@used = false
|
31
|
+
@fitness = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# tag as used
|
35
|
+
def tag_as_used
|
36
|
+
@used = true
|
37
|
+
end
|
38
|
+
|
39
|
+
# age
|
40
|
+
def age!
|
41
|
+
if @used
|
42
|
+
@used = false
|
43
|
+
@age += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# evaluate, return fitness
|
48
|
+
def evaluate(evaluator)
|
49
|
+
@fitness = evaluator.call(solution)
|
50
|
+
end
|
51
|
+
|
52
|
+
# <=>
|
53
|
+
def <=>(individual)
|
54
|
+
self.fitness <=> individual.fitness
|
55
|
+
end
|
56
|
+
end # Individual
|
57
|
+
|
58
|
+
# ctor
|
59
|
+
def initialize(options)
|
60
|
+
@layer_size = options[:layer_size ] || DEFAULTS[:layer_size ]
|
61
|
+
@max_layer_count = options[:max_layer_count ] || DEFAULTS[:max_layer_count ]
|
62
|
+
@tournament_size = options[:tournament_size ] || DEFAULTS[:tournament_size ]
|
63
|
+
@age_gap = options[:age_gap ] || DEFAULTS[:age_gap ]
|
64
|
+
@aging_scheme = options[:aging_scheme ] || DEFAULTS[:aging_scheme ]
|
65
|
+
@layer_elitism = options[:layer_elitism ] || DEFAULTS[:layer_elitism ]
|
66
|
+
@overall_elitism = options[:overall_elitism ] || DEFAULTS[:overall_elitism ]
|
67
|
+
@parents_from_previous_layer = options[:parents_from_previous_layer] || DEFAULTS[:parents_from_previous_layer]
|
68
|
+
|
69
|
+
@evaluator = options[:evaluator ] || (raise ArgumentError)
|
70
|
+
@mutation_rate = options[:mutation_rate ] || (raise ArgumentError)
|
71
|
+
@genome_init = options[:genome_init ] || (raise ArgumentError)
|
72
|
+
|
73
|
+
raise ArgumentError unless [:linear, :fibonacci, :polynomial, :exponential].include?(@aging_scheme)
|
74
|
+
|
75
|
+
@results = SearchResults.new
|
76
|
+
@layers = []
|
77
|
+
end
|
78
|
+
|
79
|
+
# run once
|
80
|
+
def run_once
|
81
|
+
generation = @results.generation
|
82
|
+
if generation == 0
|
83
|
+
@layers[0] = Array.new(@layer_size){ Individual.new(@genome_init.call) }
|
84
|
+
evaluate_layer(0)
|
85
|
+
else
|
86
|
+
# all but bottom layer
|
87
|
+
(@layers.size - 1).downto(1) do |i|
|
88
|
+
generate_new_population_layer(i)
|
89
|
+
evaluate_layer(i)
|
90
|
+
end
|
91
|
+
|
92
|
+
# bottom layer
|
93
|
+
if generation % @age_gap == 0
|
94
|
+
move_to_layer(@layers[0], 1)
|
95
|
+
@layers[0] = Array.new(@layer_size){ Individual.new(@genome_init.call) }
|
96
|
+
else
|
97
|
+
generate_new_population_layer(0)
|
98
|
+
end
|
99
|
+
evaluate_layer(0)
|
100
|
+
end
|
101
|
+
|
102
|
+
# age population
|
103
|
+
@layers.each do |l|
|
104
|
+
l.each do |individual|
|
105
|
+
individual.age!
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# promote individuals
|
110
|
+
promote_individuals
|
111
|
+
|
112
|
+
@results.increment_generation
|
113
|
+
|
114
|
+
@layers.each.with_index do |l, idx|
|
115
|
+
l.sort!.reverse!
|
116
|
+
$stderr << "= layer #{idx} : max age #{max_layer_age(idx)} ---------------------------------------------------------------\n"
|
117
|
+
l.each do |i|
|
118
|
+
$stderr << " - #{i.solution.__id__.to_s.rjust(7)}, fitness #{i.fitness.to_i.to_s.rjust(6)}, age #{i.age.to_s.rjust(4)}\n"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
$stderr << "\n"
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# max age for layer +n+
|
127
|
+
def max_layer_age(n)
|
128
|
+
case(@aging_scheme)
|
129
|
+
when :linear ; n + 1
|
130
|
+
when :fibonacci ; fibonacci(n + 1)
|
131
|
+
when :polynomial ; n < 2 ? n + 1 : n ** 2
|
132
|
+
when :exponential; 2 ** n
|
133
|
+
else raise ArgumentError
|
134
|
+
end * @age_gap
|
135
|
+
end
|
136
|
+
|
137
|
+
# fibonacci
|
138
|
+
def fibonacci(n)
|
139
|
+
if n == 0 || n == 1
|
140
|
+
1
|
141
|
+
else
|
142
|
+
fibonacci(n - 1) + fibonacci(n - 2)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# evaluate layer
|
147
|
+
def evaluate_layer(n)
|
148
|
+
@layers[n].each do |individual|
|
149
|
+
fitness = individual.evaluate(@evaluator)
|
150
|
+
@results.add_solution(individual.solution, fitness)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# generate new population layer
|
155
|
+
def generate_new_population_layer(n)
|
156
|
+
current_layer = @layers[n]
|
157
|
+
|
158
|
+
parent_population = ((n == 0 || !@parents_from_previous_layer) ? current_layer : (@layers[n - 1] + current_layer)).sort
|
159
|
+
|
160
|
+
new_population = []
|
161
|
+
|
162
|
+
# elitism
|
163
|
+
elitism = @layer_elitism
|
164
|
+
elitism = @overall_elitism if n == (@layers.size - 1) && @overall_elitism > elitism
|
165
|
+
|
166
|
+
if elitism > 0
|
167
|
+
current_layer.sort.reverse[0, [elitism, current_layer.size].min].each do |i|
|
168
|
+
new_population << i
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
(@layer_size - new_population.size).times do
|
173
|
+
parent1_index = Array.new(@tournament_size){ rand(parent_population.size) }.max
|
174
|
+
parent2_index = Array.new(@tournament_size){ rand(parent_population.size) }.reject{ |v| v == parent1_index }.max
|
175
|
+
while parent2_index == nil || parent2_index == parent1_index
|
176
|
+
parent2_index = rand(parent_population.size)
|
177
|
+
end
|
178
|
+
|
179
|
+
parent1 = parent_population[parent1_index]
|
180
|
+
parent2 = parent_population[parent2_index]
|
181
|
+
|
182
|
+
parent1.tag_as_used
|
183
|
+
parent2.tag_as_used
|
184
|
+
new_solution = parent1.solution.crossover(parent2.solution).mutate!(@mutation_rate)
|
185
|
+
new_population << Individual.new(new_solution, [parent1.age, parent2.age].max + 1)
|
186
|
+
end
|
187
|
+
|
188
|
+
@layers[n] = new_population
|
189
|
+
end
|
190
|
+
|
191
|
+
# promote individuals
|
192
|
+
def promote_individuals
|
193
|
+
layer_count = @layers.size
|
194
|
+
|
195
|
+
((layer_count == @max_layer_count) ? (layer_count - 2) : (layer_count - 1)).downto(0) do |n|
|
196
|
+
layer = @layers[n]
|
197
|
+
# take old ones from current layer
|
198
|
+
max_age = max_layer_age(n)
|
199
|
+
oldies = layer.select{ |i| i.age >= max_age }
|
200
|
+
if !oldies.empty?
|
201
|
+
layer.delete_if{ |i| i.age >= max_age }
|
202
|
+
move_to_layer(oldies, n + 1) # try to fit them in with the layer above
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# try to fit individual within layer
|
208
|
+
def move_to_layer(individuals, n)
|
209
|
+
# add layer
|
210
|
+
if n > (@layers.size - 1)
|
211
|
+
assert{ n < @max_layer_count }
|
212
|
+
assert{ n == @layers.size }
|
213
|
+
@layers[n] = []
|
214
|
+
end
|
215
|
+
|
216
|
+
# add oldies in layer
|
217
|
+
a = (@layers[n] + individuals).sort do |a, b|
|
218
|
+
cmp_fitness = a.fitness <=> b.fitness
|
219
|
+
if cmp_fitness == 0
|
220
|
+
a.age <=> b.age
|
221
|
+
else
|
222
|
+
cmp_fitness
|
223
|
+
end
|
224
|
+
end.reverse
|
225
|
+
|
226
|
+
@layers[n] = a[0, [a.size, @layer_size].min]
|
227
|
+
end
|
228
|
+
end # AlpsGa
|
229
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'metaheuristics/metaheuristic_interface'
|
2
|
+
require 'metaheuristics/individual_solution'
|
3
|
+
|
4
|
+
class FgrnGa < MetaheuristicInterface
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
:age_max => 10,
|
8
|
+
:children_count => 80,
|
9
|
+
:parents_coeff => 0.4,
|
10
|
+
:population_size => 100,
|
11
|
+
:random_parent_coeff => 0.01
|
12
|
+
}
|
13
|
+
|
14
|
+
attr_reader :results, :population
|
15
|
+
|
16
|
+
# ctor
|
17
|
+
def initialize(options)
|
18
|
+
@children_count = options[:children_count ] || DEFAULTS[:children_count ]
|
19
|
+
@parents_coeff = options[:parents_coeff ] || DEFAULTS[:parents_coeff ]
|
20
|
+
@population_size = options[:population_size ] || DEFAULTS[:population_size ]
|
21
|
+
@random_parent_coeff = options[:random_parent_coeff ] || DEFAULTS[:random_parent_coeff]
|
22
|
+
@age_max = options[:age_max ] || DEFAULTS[:age_max ]
|
23
|
+
@evaluate_children_only = options[:evaluate_children_only] || false
|
24
|
+
@evaluator = options[:evaluator ] || (raise ArgumentError)
|
25
|
+
@mutation_rate = options[:mutation_rate ] || (raise ArgumentError)
|
26
|
+
|
27
|
+
genome_init = options[:genome_init] || (raise ArgumentError)
|
28
|
+
@initial_genomes = Array.new(@population_size){ genome_init.call }
|
29
|
+
|
30
|
+
@results = SearchResults.new
|
31
|
+
end
|
32
|
+
|
33
|
+
# run once
|
34
|
+
def run_once
|
35
|
+
if @initial_genomes
|
36
|
+
@population = @initial_genomes.collect{ |genome| IndividualSolution.new(genome, @evaluator, @results) }
|
37
|
+
@initial_genomes = nil
|
38
|
+
else
|
39
|
+
# generate children
|
40
|
+
children =
|
41
|
+
Array.new(@children_count) do
|
42
|
+
parent1 = pick_parent
|
43
|
+
parent2 = pick_parent
|
44
|
+
|
45
|
+
genome = (parent1.crossover(parent2)).mutate!(@mutation_rate)
|
46
|
+
IndividualSolution.new(genome, @evaluator, @results)
|
47
|
+
end
|
48
|
+
|
49
|
+
@population.each{ |individual| individual.increment_age } # increment age
|
50
|
+
@population.reject!{ |individual| individual.age >= @age_max } # reject too old
|
51
|
+
|
52
|
+
# carry over the best
|
53
|
+
carried_over_count = @population_size - @children_count
|
54
|
+
carried_over = @population[0, [carried_over_count, @population.size].min]
|
55
|
+
|
56
|
+
# re-evaluate carried over
|
57
|
+
if !@evaluate_children_only
|
58
|
+
carried_over.each do |individual|
|
59
|
+
individual.reevaluate
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# merge carried over and children
|
64
|
+
@population = carried_over + children
|
65
|
+
end
|
66
|
+
|
67
|
+
@population.sort!.reverse!
|
68
|
+
@results.increment_generation
|
69
|
+
|
70
|
+
# debug: print population
|
71
|
+
@population.each do |i|
|
72
|
+
$stderr << "#{('%d' % i.fitness).rjust(5)} - #{i.__id__.to_s.rjust(10)} - age #{'X' * i.age}\n"
|
73
|
+
end
|
74
|
+
end # run_once
|
75
|
+
|
76
|
+
private
|
77
|
+
# pick parent
|
78
|
+
def pick_parent
|
79
|
+
index =
|
80
|
+
rand < @random_parent_coeff ?
|
81
|
+
rand(@population.size) : # pick a parent at random
|
82
|
+
rand([(@parents_coeff * @population_size).round, @population.size].min) # pick a parent amongst the best individuals
|
83
|
+
@population[index].solution
|
84
|
+
end
|
85
|
+
end # FgrnGa
|
86
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'metaheuristics/metaheuristic_interface'
|
2
|
+
require 'metaheuristics/individual_solution'
|
3
|
+
require 'metaheuristics/search_results'
|
4
|
+
|
5
|
+
class TournamentSelection < MetaheuristicInterface
|
6
|
+
|
7
|
+
DEFAULTS = {
|
8
|
+
:population_size => 100,
|
9
|
+
:tournament_size => 4,
|
10
|
+
:elitism => 0
|
11
|
+
}
|
12
|
+
|
13
|
+
attr_reader :results
|
14
|
+
|
15
|
+
# ctor
|
16
|
+
def initialize(options)
|
17
|
+
@population_size = options[:population_size] || DEFAULTS[:population_size]
|
18
|
+
@tournament_size = options[:tournament_size] || DEFAULTS[:tournament_size]
|
19
|
+
@elitism = options[:elitism ] || DEFAULTS[:elitism ]
|
20
|
+
@evaluator = options[:evaluator ] || (raise ArgumentError)
|
21
|
+
@mutation_rate = options[:mutation_rate ] || (raise ArgumentError)
|
22
|
+
|
23
|
+
# population
|
24
|
+
genome_init = options[:genome_init] || (raise ArgumentError)
|
25
|
+
@initial_genomes = Array.new(@population_size){ genome_init.call }
|
26
|
+
@population = nil
|
27
|
+
|
28
|
+
# results
|
29
|
+
@results = SearchResults.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# run once
|
33
|
+
def run_once
|
34
|
+
|
35
|
+
# genomes
|
36
|
+
genomes =
|
37
|
+
if @initial_genomes # use initial genomes
|
38
|
+
ig = @initial_genomes
|
39
|
+
@initial_genomes = nil
|
40
|
+
ig
|
41
|
+
else # not initial run: generate new genomes
|
42
|
+
array = Array.new(@population_size)
|
43
|
+
|
44
|
+
# elitism
|
45
|
+
@elitism.times do |i|
|
46
|
+
array[i] = @population[i].solution
|
47
|
+
end
|
48
|
+
|
49
|
+
# generate children
|
50
|
+
(@population_size - @elitism).times do |i|
|
51
|
+
parent1 = pick_parent
|
52
|
+
parent2 = pick_parent
|
53
|
+
|
54
|
+
array[@elitism + i] = parent1.crossover(parent2).mutate!(@mutation_rate)
|
55
|
+
end
|
56
|
+
|
57
|
+
array
|
58
|
+
end
|
59
|
+
|
60
|
+
# evaluate genomes into the population
|
61
|
+
@population = genomes.collect do |genome|
|
62
|
+
IndividualSolution.new(genome, @evaluator, @results)
|
63
|
+
end
|
64
|
+
|
65
|
+
@population.sort!.reverse!
|
66
|
+
@results.increment_generation
|
67
|
+
|
68
|
+
self
|
69
|
+
end # run_once
|
70
|
+
|
71
|
+
private
|
72
|
+
# pick parent
|
73
|
+
def pick_parent
|
74
|
+
@population.sample(@tournament_size).max.solution
|
75
|
+
end
|
76
|
+
end # TournamentSelection
|
77
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
class IndividualSolution
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader \
|
6
|
+
:age,
|
7
|
+
:solution,
|
8
|
+
:fitness
|
9
|
+
|
10
|
+
# ctor
|
11
|
+
def initialize(solution, evaluator, results)
|
12
|
+
@solution = solution
|
13
|
+
@evaluator = evaluator
|
14
|
+
@results = results
|
15
|
+
@age = 0
|
16
|
+
evaluate
|
17
|
+
end
|
18
|
+
|
19
|
+
# increment age
|
20
|
+
def increment_age
|
21
|
+
@age += 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# re-evaluate fitness
|
25
|
+
def reevaluate
|
26
|
+
raise RuntimeError if @fitness.nil?
|
27
|
+
evaluate
|
28
|
+
end
|
29
|
+
|
30
|
+
# <=>
|
31
|
+
def <=>(es)
|
32
|
+
@fitness <=> es.fitness
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
# evaluate
|
37
|
+
def evaluate
|
38
|
+
@fitness = @evaluator.call(@solution)
|
39
|
+
@results.add_solution(@solution, @fitness)
|
40
|
+
end
|
41
|
+
end # IndividualSolution
|
42
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'metaheuristics/genome_search/alps_ga'
|
2
|
+
require 'metaheuristics/genome_search/fgrn_ga'
|
3
|
+
require 'metaheuristics/genome_search/tournament_selection'
|
4
|
+
|
5
|
+
class MetaheuristicInitFactory
|
6
|
+
|
7
|
+
# from definition
|
8
|
+
def self.from_definition(definition)
|
9
|
+
name = definition[:name]
|
10
|
+
case(name)
|
11
|
+
|
12
|
+
# alps
|
13
|
+
when :alps
|
14
|
+
args = {
|
15
|
+
:layer_size => definition[:layer_size ],
|
16
|
+
:max_layer_count => definition[:max_layer_count],
|
17
|
+
:tournament_size => definition[:tournament_size],
|
18
|
+
:age_gap => definition[:age_gap ],
|
19
|
+
:aging_scheme => definition[:aging_scheme ],
|
20
|
+
:layer_elitism => definition[:layer_elitism ],
|
21
|
+
:overall_elitism => definition[:overall_elitism],
|
22
|
+
:mutation_rate => definition[:mutation_rate ] || (raise ArgumentError)
|
23
|
+
}
|
24
|
+
|
25
|
+
# init block
|
26
|
+
lambda do |options|
|
27
|
+
genome_init = options[:genome_init] || (raise ArgumentError)
|
28
|
+
evaluator = options[:evaluator] || (raise ArgumentError)
|
29
|
+
|
30
|
+
AlpsGa.new(args.merge(
|
31
|
+
:genome_init => genome_init,
|
32
|
+
:evaluator => evaluator))
|
33
|
+
end
|
34
|
+
|
35
|
+
# fga
|
36
|
+
when :fga
|
37
|
+
args = {
|
38
|
+
:population_size => definition[:population_size ] || (raise ArgumentError),
|
39
|
+
:children_count => definition[:children_count ] || (raise ArgumentError),
|
40
|
+
:mutation_rate => definition[:mutation_rate ] || (raise ArgumentError),
|
41
|
+
:parents_coeff => definition[:parents_coeff ] || (raise ArgumentError),
|
42
|
+
:random_parent_coeff => definition[:random_parent_coeff ] || (raise ArgumentError),
|
43
|
+
:age_max => definition[:age_max ] || (raise ArgumentError),
|
44
|
+
:evaluate_children_only => definition[:evaluate_children_only] || false
|
45
|
+
}
|
46
|
+
|
47
|
+
# init block
|
48
|
+
lambda do |options|
|
49
|
+
genome_init = options[:genome_init] || (raise ArgumentError)
|
50
|
+
evaluator = options[:evaluator] || (raise ArgumentError)
|
51
|
+
|
52
|
+
FgrnGa.new(args.merge(
|
53
|
+
:genome_init => genome_init,
|
54
|
+
:evaluator => evaluator))
|
55
|
+
end
|
56
|
+
|
57
|
+
# tournament selection
|
58
|
+
when :tournament_selection
|
59
|
+
args = {
|
60
|
+
:mutation_rate => definition[:mutation_rate ] || (raise ArgumentError),
|
61
|
+
:population_size => definition[:population_size],
|
62
|
+
:elitism => definition[:elitism ],
|
63
|
+
:tournament_size => definition[:tournament_size]
|
64
|
+
}
|
65
|
+
|
66
|
+
# init block
|
67
|
+
lambda do |options|
|
68
|
+
genome_init = options[:genome_init] || (raise ArgumentError)
|
69
|
+
evaluator = options[:evaluator] || (raise ArgumentError)
|
70
|
+
|
71
|
+
TournamentSelection.new(args.merge(
|
72
|
+
:genome_init => genome_init,
|
73
|
+
:evaluator => evaluator))
|
74
|
+
end
|
75
|
+
|
76
|
+
else raise ArgumentError, name end
|
77
|
+
end # from_definition
|
78
|
+
end # MetaheuristicInitFactory
|
79
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
# The interface to be implemented by metaheuristics algorithms.
|
3
|
+
class MetaheuristicInterface
|
4
|
+
|
5
|
+
# Should return a +SearchResult+ object (or an object with the same interface
|
6
|
+
# as +SearchResult+)
|
7
|
+
def results ; raise NotImplementedError; end
|
8
|
+
|
9
|
+
# Run one search iteration, this should include the evaluation of the fitness
|
10
|
+
# of one or more individuals (e.g. a population generation for a GA)
|
11
|
+
def run_once; raise NotImplementedError; end
|
12
|
+
end # MetaheuristicInterface
|
13
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class SearchResults
|
4
|
+
|
5
|
+
attr_reader \
|
6
|
+
:generation,
|
7
|
+
:evaluation,
|
8
|
+
:best # hash containing the keys : fitness, solution, evaluation, generation
|
9
|
+
|
10
|
+
# ctor
|
11
|
+
def initialize
|
12
|
+
@generation = 0
|
13
|
+
@evaluation = 0
|
14
|
+
@best = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# increment generation
|
18
|
+
def increment_generation
|
19
|
+
@generation += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# add solution
|
23
|
+
def add_solution(solution, fitness)
|
24
|
+
@evaluation += 1
|
25
|
+
if @best.nil? || fitness > @best[:fitness]
|
26
|
+
@best = {
|
27
|
+
:fitness => fitness,
|
28
|
+
:solution => solution,
|
29
|
+
:evaluation => @evaluation,
|
30
|
+
:generation => @generation
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# to hash
|
36
|
+
def to_hash
|
37
|
+
{
|
38
|
+
:generation => @generation,
|
39
|
+
:evaluation => @evaluation,
|
40
|
+
:best => @best.clone
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end # SearchResults
|
44
|
+
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: metaheuristics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jean Krohn
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-29 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Metaheuristics include Genetic Algorithms (GAs), such as ALPS and tournament selection.
|
15
|
+
email: jbk@susano.org
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/metaheuristics/individual_solution.rb
|
22
|
+
- lib/metaheuristics/search_results.rb
|
23
|
+
- lib/metaheuristics/metaheuristic_interface.rb
|
24
|
+
- lib/metaheuristics/metaheuristic_init_factory.rb
|
25
|
+
- lib/metaheuristics/genome_search/alps_ga.rb
|
26
|
+
- lib/metaheuristics/genome_search/fgrn_ga.rb
|
27
|
+
- lib/metaheuristics/genome_search/tournament_selection.rb
|
28
|
+
- examples/example_minimiser.rb
|
29
|
+
homepage: http://github.com/susano/metaheuristics
|
30
|
+
licenses: []
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: !binary |-
|
40
|
+
MA==
|
41
|
+
none: false
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: !binary |-
|
47
|
+
MA==
|
48
|
+
none: false
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 1.8.24
|
52
|
+
signing_key:
|
53
|
+
specification_version: 3
|
54
|
+
summary: Metaheuristic search/optimisation algorithms.
|
55
|
+
test_files: []
|