metaheuristics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|