feldtruby 0.4.9 → 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/lib/feldtruby/optimize/archive.rb +189 -0
- data/lib/feldtruby/optimize/differential_evolution.rb +2 -2
- data/lib/feldtruby/optimize/objective.rb +24 -0
- data/lib/feldtruby/optimize/optimizer.rb +47 -25
- data/lib/feldtruby/version.rb +1 -1
- data/test/test_optimize_archive.rb +218 -0
- metadata +32 -20
- checksums.yaml +0 -7
- data/lib/feldtruby/optimize/elite_archive.rb +0 -91
- data/test/test_optimize_elite_archive.rb +0 -85
data/Gemfile.lock
CHANGED
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'feldtruby/optimize'
|
2
|
+
require 'feldtruby/logger'
|
3
|
+
|
4
|
+
module FeldtRuby::Optimize
|
5
|
+
|
6
|
+
# An archive keeps a record of all "interesting" candidate solutions found
|
7
|
+
# during optimization. It has two purposes: to contribute to the optimization
|
8
|
+
# itself (since seeding a search with previously "good" solutions can improve
|
9
|
+
# convergence speed) but also to provide a current view of the best and most
|
10
|
+
# interesting solutions found.
|
11
|
+
#
|
12
|
+
# Interestingness is hard to define but we posit that two elements are always
|
13
|
+
# important:
|
14
|
+
# 1. Being good, i.e. having good values for one or many of the sub-objectives
|
15
|
+
# 2. Being different, i.e. behaving differently (phenotypic diversity) or
|
16
|
+
# looking different (genotypic diversity), than other candidate solutions.
|
17
|
+
#
|
18
|
+
# Different types of optimization can require different trade-offs between
|
19
|
+
# these elements but all archives should support both types inherently.
|
20
|
+
# The default choices for the main class is to use the quality calculated by the
|
21
|
+
# objective class to judge "being good" and to use a diversity objective to
|
22
|
+
# judge "being different".
|
23
|
+
#
|
24
|
+
# We also posit that it is not enough only to be diverse (although there is
|
25
|
+
# some research showing that novelty search in itself may be as good/important
|
26
|
+
# as directed search) so the default behavior is to only add a solution to the
|
27
|
+
# diversity top lists if it is within a certain percentage of the best solutions
|
28
|
+
# on the other (being good) top list. Thus the basic design is to keep one top
|
29
|
+
# list per fitness goal, one overall aggregate fitness top list and then one
|
30
|
+
# top list per diversity goal.
|
31
|
+
#
|
32
|
+
# We note that diversity is just another type of objective (although a relative
|
33
|
+
# rather than an absolute one) and we can use the existing
|
34
|
+
# classes to implement that. A default diversity objective looks at genotypic
|
35
|
+
# and fitness diversity by default but more elaborate schemes can be used.
|
36
|
+
#
|
37
|
+
# One way to make this model easier to understand is to call the three types:
|
38
|
+
# generalists (overall good, aggregated fitness best)
|
39
|
+
# specialists (doing one thing very, very good, sub-objective fitness best)
|
40
|
+
# weirdos (different but with clear qualitites, ok but diverse)
|
41
|
+
class Archive
|
42
|
+
#include FeldtRuby::Logging
|
43
|
+
|
44
|
+
DefaultParams = {
|
45
|
+
:NumTopPerGoal => 5, # Number of solutions in top list per individual goal
|
46
|
+
:NumTopAggregate => 20, # Number of solutions in top list for aggregate quality
|
47
|
+
|
48
|
+
# Number of solutions in diversity top list. This often needs to
|
49
|
+
# be larger than the other top lists since there are "more" ways to be
|
50
|
+
# diverse than to be good... OTOH it costs CPU power to have large values
|
51
|
+
# here since the quality values needs to be recalculated whenever there is
|
52
|
+
# a new best. So we keep them small.
|
53
|
+
:NumTopDiversityAggregate => 10,
|
54
|
+
|
55
|
+
# Max percent distance (given as a ratio, i.e. 0.05 means 5%) from best
|
56
|
+
# solution (top of Aggregate list) that a solution is allowed to have
|
57
|
+
# to be allowed on the diversity list. If it is more than 5% from best
|
58
|
+
# we don't consider adding it to a diversity list.
|
59
|
+
:MaxPercentDistanceToBestForDiversity => 0.05
|
60
|
+
}
|
61
|
+
|
62
|
+
attr_reader :objective, :diversity_objective
|
63
|
+
|
64
|
+
attr_reader :specialists, :generalists, :weirdos
|
65
|
+
|
66
|
+
def initialize(fitnessObjective, diversityObjective, params = DefaultParams.clone)
|
67
|
+
@objective = fitnessObjective
|
68
|
+
self.diversity_objective = diversityObjective
|
69
|
+
@params = DefaultParams.clone.update(params)
|
70
|
+
init_top_lists
|
71
|
+
end
|
72
|
+
|
73
|
+
def diversity_objective=(diversityObjective)
|
74
|
+
@diversity_objective = diversityObjective
|
75
|
+
@diversity_objective.archive = self if @diversity_objective.respond_to?(:archive=)
|
76
|
+
@diversity_objective.quality_objective = @objective if @diversity_objective.respond_to?(:quality_objective=)
|
77
|
+
end
|
78
|
+
|
79
|
+
def best
|
80
|
+
# The top of the Generalists top list is the overall best
|
81
|
+
@generalists[0]
|
82
|
+
end
|
83
|
+
|
84
|
+
def init_top_lists
|
85
|
+
@specialists = Array.new
|
86
|
+
@objective.num_goals.times do |i|
|
87
|
+
@specialists << GoalTopList.new(@params[:NumTopPerGoal], @objective, i)
|
88
|
+
end
|
89
|
+
|
90
|
+
@generalists = GlobalTopList.new(@params[:NumTopAggregate], @objective)
|
91
|
+
|
92
|
+
@weirdos = GlobalTopList.new(@params[:NumTopDiversityAggregate], @diversity_objective)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Add a candidate to the top lists if it is good enough to be there. Throws
|
96
|
+
# out worse candidates as appropriate.
|
97
|
+
def add(candidate)
|
98
|
+
@specialists.each {|tl| tl.add(candidate)}
|
99
|
+
|
100
|
+
# Detect if we get a new best one by saving the previous best.
|
101
|
+
prev_best = best
|
102
|
+
@generalists.add candidate
|
103
|
+
|
104
|
+
# If there was a new best we invalidate the diversity quality values since
|
105
|
+
# they are relative and must be recalculated. Note that this might incur
|
106
|
+
# a big penalty if there are frequent changes at the top.
|
107
|
+
if prev_best != best
|
108
|
+
# logger.log_data :new_best, {
|
109
|
+
# "New best" => best,
|
110
|
+
# "New quality" => @objective.quality_of(best),
|
111
|
+
# "Old best" => prev_best,
|
112
|
+
# "Old quality" => @objective.quality_of(prev_best)},
|
113
|
+
# "Archive: New best solution found", true
|
114
|
+
|
115
|
+
@weirdos.each {|w| @diversity_objective.invalidate_quality_of(w)}
|
116
|
+
elsif good_enough_quality_to_be_interesting?(candidate)
|
117
|
+
# When we add a new one this will lead to re-calc of the diversity quality
|
118
|
+
# of the previous ones if there has been a new best since last time.
|
119
|
+
@weirdos.add candidate
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Is quality of candidate good enough (within MaxPercentDistanceToBestForDiversity
|
124
|
+
# from quality of best candidate)
|
125
|
+
def good_enough_quality_to_be_interesting?(candidate)
|
126
|
+
qv_best = @objective.quality_of(best).value
|
127
|
+
((qv_best - @objective.quality_of(candidate).value).abs / qv_best) <= @params[:MaxPercentDistanceToBestForDiversity]
|
128
|
+
end
|
129
|
+
|
130
|
+
# A top list is an array of a fixed size that saves the top candidates
|
131
|
+
# based on their (aggregate) quality values.
|
132
|
+
class GlobalTopList
|
133
|
+
def initialize(maxSize, objective)
|
134
|
+
@max_size =maxSize
|
135
|
+
@top_list = Array.new
|
136
|
+
@objective = objective
|
137
|
+
end
|
138
|
+
def length; @top_list.length; end
|
139
|
+
|
140
|
+
def each
|
141
|
+
@top_list.each {|e| yield(e)}
|
142
|
+
end
|
143
|
+
|
144
|
+
def [](index)
|
145
|
+
@top_list[index]
|
146
|
+
end
|
147
|
+
|
148
|
+
def add(candidate)
|
149
|
+
last = @top_list.last
|
150
|
+
#puts "In #{self},\nlast = #{last}, candidate = #{candidate}, top_list = #{@top_list}"
|
151
|
+
if @top_list.length < @max_size || last.nil? || is_better_than?(candidate, last)
|
152
|
+
@top_list.pop if @top_list.length >= @max_size
|
153
|
+
@top_list << candidate
|
154
|
+
@top_list = sort_top_list
|
155
|
+
end
|
156
|
+
#puts "top_list = #{@top_list}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def is_better_than?(candidate1, candidate2)
|
160
|
+
@objective.is_better_than?(candidate1, candidate2)
|
161
|
+
end
|
162
|
+
|
163
|
+
def sort_top_list
|
164
|
+
@top_list.sort_by {|c| @objective.quality_of(c).value}
|
165
|
+
end
|
166
|
+
|
167
|
+
def inspect
|
168
|
+
self.class.inspect + @top_list.inspect
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class GoalTopList < GlobalTopList
|
173
|
+
def initialize(maxSize, objective, goalIndex)
|
174
|
+
super(maxSize, objective)
|
175
|
+
@index = goalIndex
|
176
|
+
end
|
177
|
+
def is_better_than?(candidate1, candidate2)
|
178
|
+
@objective.is_better_than_for_goal?(@index, candidate1, candidate2)
|
179
|
+
end
|
180
|
+
def sort_top_list
|
181
|
+
@top_list.sort_by {|c|
|
182
|
+
qv = @objective.quality_of(c)
|
183
|
+
qv.sub_quality(@index, true) # We want the sub quality value posed as a minimization goal regardless of whether it is a min or max goal
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
@@ -70,11 +70,11 @@ class DEOptimizerBase < EvolutionaryOptimizer
|
|
70
70
|
}, "DE (step #{@num_optimization_steps}): Trial vector was better than target vector"
|
71
71
|
update_candidate_in_population(target_index, trial)
|
72
72
|
feedback_on_trial_vs_target(trial, target, true)
|
73
|
+
[best]
|
73
74
|
else
|
74
75
|
feedback_on_trial_vs_target(trial, target, false)
|
76
|
+
[]
|
75
77
|
end
|
76
|
-
|
77
|
-
[best]
|
78
78
|
end
|
79
79
|
|
80
80
|
#####################################
|
@@ -163,6 +163,13 @@ class Objective
|
|
163
163
|
|
164
164
|
end
|
165
165
|
|
166
|
+
# Invalidate the current quality object to ensure it will be recalculated
|
167
|
+
# the next time quality_of is called with this candidate. This is needed
|
168
|
+
# when there is a new best candidate set in an Archive, for example.
|
169
|
+
def invalidate_quality_of(candidate)
|
170
|
+
update_quality_value_in_object candidate, nil
|
171
|
+
end
|
172
|
+
|
166
173
|
# Rank candidates from best to worst. Candidates that have the same quality
|
167
174
|
# are randomly ordered.
|
168
175
|
def rank_candidates(candidates, weights = self.weights)
|
@@ -510,4 +517,21 @@ class ObjectiveMaximizeBlock < Objective
|
|
510
517
|
end
|
511
518
|
end
|
512
519
|
|
520
|
+
# A diversity objective often is a secondary goal and is measured relative
|
521
|
+
# to quality goals/objectives or relative to candidates in the archive.
|
522
|
+
# Thus it needs to have access to these other two objects.
|
523
|
+
class DiversityObjective < Objective
|
524
|
+
attr_accessor :archive
|
525
|
+
attr_accessor :quality_objective
|
526
|
+
end
|
527
|
+
|
528
|
+
# The standard diversity objective is to just use the Euclidean distance
|
529
|
+
# to the best candidate found so far.
|
530
|
+
class FeldtRuby::Optimize::EuclideanDistanceToBest < FeldtRuby::Optimize::DiversityObjective
|
531
|
+
# Euclidean distance to best candidate. Genotype diversity.
|
532
|
+
def goal_max_euclidean_distance_to_best(candidate)
|
533
|
+
candidate.rms_from(archive.best)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
513
537
|
end
|
@@ -2,6 +2,7 @@ require 'feldtruby/optimize'
|
|
2
2
|
require 'feldtruby/optimize/objective'
|
3
3
|
require 'feldtruby/optimize/search_space'
|
4
4
|
require 'feldtruby/optimize/max_steps_termination_criterion'
|
5
|
+
require 'feldtruby/optimize/archive'
|
5
6
|
require 'feldtruby/math/rand'
|
6
7
|
require 'feldtruby/array'
|
7
8
|
require 'feldtruby/logger'
|
@@ -13,14 +14,15 @@ module FeldtRuby::Optimize
|
|
13
14
|
class Optimizer
|
14
15
|
include FeldtRuby::Logging
|
15
16
|
|
16
|
-
attr_reader :options, :objective, :search_space, :
|
17
|
-
attr_reader :
|
17
|
+
attr_reader :options, :objective, :search_space, :archive
|
18
|
+
attr_reader :num_optimization_steps, :termination_criterion
|
18
19
|
|
19
20
|
def initialize(objective, searchSpace = FeldtRuby::Optimize::DefaultSearchSpace, options = {})
|
20
|
-
@best = nil # To avoid warnings if not set
|
21
21
|
@objective, @search_space = objective, searchSpace
|
22
22
|
@options = FeldtRuby::Optimize.override_default_options_with(options)
|
23
23
|
|
24
|
+
init_archive
|
25
|
+
|
24
26
|
# Must setup logger before setting options since verbosity of logger is
|
25
27
|
# an option!
|
26
28
|
setup_logger_and_distribute_to_instance_variables(options)
|
@@ -28,6 +30,28 @@ class Optimizer
|
|
28
30
|
initialize_options(@options)
|
29
31
|
end
|
30
32
|
|
33
|
+
def init_archive
|
34
|
+
if @options[:archive]
|
35
|
+
|
36
|
+
@archive = @options[:archive]
|
37
|
+
|
38
|
+
else
|
39
|
+
|
40
|
+
if @options[:archiveDiversityObjective]
|
41
|
+
diversity_objective = @options[:archiveDiversityObjective]
|
42
|
+
else
|
43
|
+
diversity_objective = @options[:archiveDiversityObjectiveClass].new
|
44
|
+
end
|
45
|
+
|
46
|
+
@archive = @options[:archiveClass].new(@objective, diversity_objective)
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def best
|
52
|
+
@archive.best
|
53
|
+
end
|
54
|
+
|
31
55
|
def initialize_options(options)
|
32
56
|
self.logger.verbose = options[:verbose]
|
33
57
|
@termination_criterion = options[:terminationCriterion]
|
@@ -37,15 +61,18 @@ class Optimizer
|
|
37
61
|
def optimize()
|
38
62
|
logger.log "Optimization with optimizer #{self.class.inspect} started"
|
39
63
|
@num_optimization_steps = 0
|
40
|
-
|
41
|
-
|
64
|
+
|
65
|
+
# Set up a random best for now that we can later compare to.
|
66
|
+
update_archive [search_space.gen_candidate()]
|
67
|
+
|
42
68
|
begin
|
43
69
|
while !termination_criterion.terminate?(self)
|
44
70
|
new_candidates = optimization_step()
|
45
71
|
@num_optimization_steps += 1
|
46
|
-
|
72
|
+
update_archive new_candidates
|
47
73
|
end
|
48
74
|
rescue Exception => e
|
75
|
+
puts e.inspect
|
49
76
|
logger.log_data :exception, {
|
50
77
|
:exception_class => e.class.inspect,
|
51
78
|
:backtrace => e.backtrace.join("\n")
|
@@ -53,16 +80,18 @@ class Optimizer
|
|
53
80
|
ensure
|
54
81
|
logger.log "!!! - Optimization FINISHED after #{@num_optimization_steps} steps - !!!"
|
55
82
|
end
|
83
|
+
|
56
84
|
@objective.note_end_of_optimization(self)
|
57
85
|
log_end_of_optimization
|
58
|
-
|
86
|
+
|
87
|
+
archive.best # return the best
|
59
88
|
end
|
60
89
|
|
61
90
|
def log_end_of_optimization
|
62
91
|
logger.log("End of optimization\n" +
|
63
92
|
" Optimizer: #{self.class}\n" +
|
64
|
-
" Best found: #{@best}\n" +
|
65
|
-
" Quality of best: #{@objective.quality_of(@best)}\n" +
|
93
|
+
" Best found: #{@archive.best}\n" +
|
94
|
+
" Quality of best: #{@objective.quality_of(@archive.best)}\n" +
|
66
95
|
" Time used = #{Time.human_readable_timestr(logger.elapsed_time)}, " +
|
67
96
|
"Steps performed = #{@num_optimization_steps}, " +
|
68
97
|
"#{Time.human_readable_timestr(time_per_step, true)}/step")
|
@@ -77,21 +106,10 @@ class Optimizer
|
|
77
106
|
def optimization_step()
|
78
107
|
end
|
79
108
|
|
80
|
-
# Update the
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
qb = @best.nil? ? nil : @objective.quality_of(@best)
|
85
|
-
logger.log_data :new_best, {
|
86
|
-
"New best" => best_new,
|
87
|
-
"New quality" => @objective.quality_of(best_new),
|
88
|
-
"Old best" => @best,
|
89
|
-
"Old quality" => qb}, "Optimizer (step #{@num_optimization_steps}): New best solution found", true
|
90
|
-
@best = best_new
|
91
|
-
true
|
92
|
-
else
|
93
|
-
false
|
94
|
-
end
|
109
|
+
# Update the archive with newly found candidates. Array of candidates may be empty if
|
110
|
+
# no new candidates found.
|
111
|
+
def update_archive(candidates)
|
112
|
+
candidates.each {|c| @archive.add(c)}
|
95
113
|
end
|
96
114
|
end
|
97
115
|
|
@@ -211,7 +229,11 @@ DefaultOptimizationOptions = {
|
|
211
229
|
:verbose => true,
|
212
230
|
:populationSize => 100,
|
213
231
|
:samplerClass => FeldtRuby::Optimize::RadiusLimitedPopulationSampler,
|
214
|
-
:samplerRadius => 8 # Max distance between individuals selected in same tournament.
|
232
|
+
:samplerRadius => 8, # Max distance between individuals selected in same tournament.
|
233
|
+
:archive => nil, # If this is set it takes precedence over archiveClass.
|
234
|
+
:archiveClass => FeldtRuby::Optimize::Archive,
|
235
|
+
:archiveDiversityObjective => nil, # If this is set it takes precedence over the class in archiveDiversityObjectiveClass
|
236
|
+
:archiveDiversityObjectiveClass => FeldtRuby::Optimize::EuclideanDistanceToBest,
|
215
237
|
}
|
216
238
|
|
217
239
|
def self.override_default_options_with(options)
|
data/lib/feldtruby/version.rb
CHANGED
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'feldtruby/optimize/archive'
|
2
|
+
|
3
|
+
class TwoMinOneMax < FeldtRuby::Optimize::Objective
|
4
|
+
def objective_min_1(x)
|
5
|
+
x.sum
|
6
|
+
end
|
7
|
+
|
8
|
+
def objective_min_2(x)
|
9
|
+
x.max
|
10
|
+
end
|
11
|
+
|
12
|
+
def objective_max_1(x)
|
13
|
+
x.min
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class DivObj2 < FeldtRuby::Optimize::EuclideanDistanceToBest
|
18
|
+
# Diversity goal 2: AMGA2 crowding distance to nearest neighbours in fitness
|
19
|
+
# space. Phenotype diversity.
|
20
|
+
def goal_max_crowding_distance_to_nearest_neighbours(candidate)
|
21
|
+
# FIX: Below we need to handle the case where the candidate is an extremal
|
22
|
+
# solution and thus only has one nearest neighbour. Give it very high diversity?
|
23
|
+
nn1, nn2 = archive.find_nearest_neighbours_in_fitness(candidate)
|
24
|
+
sqv1 = archive.objective.quality_of(nn1).sub_qualitites
|
25
|
+
sqv2 = archive.objective.quality_of(nn2).sub_qualitites
|
26
|
+
sqvme = archive.objective.quality_of(candidate).sub_qualitites
|
27
|
+
((sqv1 - sqvme) * (sqv2 - sqvme)).abs.sum
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "Archive" do
|
32
|
+
before do
|
33
|
+
@o = TwoMinOneMax.new(FeldtRuby::Optimize::Objective::WeightedSumAggregator.new)
|
34
|
+
@do = FeldtRuby::Optimize::EuclideanDistanceToBest.new(FeldtRuby::Optimize::Objective::WeightedSumAggregator.new)
|
35
|
+
@a = FeldtRuby::Optimize::Archive.new(@o, @do, {
|
36
|
+
:NumTopPerGoal => 2,
|
37
|
+
:NumTopAggregate => 3,
|
38
|
+
:NumTopDiversityAggregate => 2})
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'is adapted to objectives when created' do
|
42
|
+
@a.objective.must_equal @o
|
43
|
+
@a.diversity_objective.must_equal @do
|
44
|
+
@a.specialists.length.must_equal @o.num_goals
|
45
|
+
@a.generalists.must_be_kind_of FeldtRuby::Optimize::Archive::GlobalTopList
|
46
|
+
@a.weirdos.must_be_kind_of FeldtRuby::Optimize::Archive::GlobalTopList
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'does not yet have a best value when created' do
|
50
|
+
@a.best.must_equal nil
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'properly handles additions' do
|
54
|
+
i1 = [1,2,3]
|
55
|
+
@a.add i1 # [6, 3, 1] => 10
|
56
|
+
|
57
|
+
@a.best.must_equal i1
|
58
|
+
|
59
|
+
@a.generalists.length.must_equal 1
|
60
|
+
@a.generalists[0].must_equal i1
|
61
|
+
|
62
|
+
@a.specialists[0].length.must_equal 1
|
63
|
+
@a.specialists[1].length.must_equal 1
|
64
|
+
@a.specialists[2].length.must_equal 1
|
65
|
+
|
66
|
+
@a.weirdos.length.must_equal 0
|
67
|
+
|
68
|
+
i2 = [1,2,4]
|
69
|
+
@a.add i2 # [7, 4, 1] => 12
|
70
|
+
|
71
|
+
@a.best.must_equal i1
|
72
|
+
|
73
|
+
@a.generalists.length.must_equal 2
|
74
|
+
@a.generalists[0].must_equal i1
|
75
|
+
@a.generalists[1].must_equal i2
|
76
|
+
|
77
|
+
@a.specialists[0].length.must_equal 2
|
78
|
+
@a.specialists[1].length.must_equal 2
|
79
|
+
@a.specialists[2].length.must_equal 2
|
80
|
+
@a.specialists[0][0].must_equal i1
|
81
|
+
@a.specialists[0][1].must_equal i2
|
82
|
+
@a.specialists[1][0].must_equal i1
|
83
|
+
@a.specialists[1][1].must_equal i2
|
84
|
+
|
85
|
+
@a.weirdos.length.must_equal 0
|
86
|
+
|
87
|
+
i3 = [1,2,0] # [3, 2, 0] => 5
|
88
|
+
@a.add i3
|
89
|
+
|
90
|
+
@a.best.must_equal i3
|
91
|
+
@a.generalists.length.must_equal 3
|
92
|
+
@a.generalists[0].must_equal i3
|
93
|
+
@a.generalists[1].must_equal i1
|
94
|
+
@a.generalists[2].must_equal i2
|
95
|
+
|
96
|
+
@a.specialists[0].length.must_equal 2
|
97
|
+
@a.specialists[1].length.must_equal 2
|
98
|
+
@a.specialists[2].length.must_equal 2
|
99
|
+
@a.specialists[0][0].must_equal i3
|
100
|
+
@a.specialists[0][1].must_equal i1
|
101
|
+
@a.specialists[1][0].must_equal i3
|
102
|
+
@a.specialists[1][1].must_equal i1
|
103
|
+
|
104
|
+
@a.weirdos.length.must_equal 0
|
105
|
+
|
106
|
+
# Introduce a new baddest one
|
107
|
+
i4 = [5,5,10] # [20, 10, 5] => 35
|
108
|
+
@a.add i4
|
109
|
+
|
110
|
+
@a.best.must_equal i3
|
111
|
+
@a.generalists.length.must_equal 3
|
112
|
+
@a.generalists[0].must_equal i3
|
113
|
+
@a.generalists[1].must_equal i1
|
114
|
+
@a.generalists[2].must_equal i2
|
115
|
+
|
116
|
+
@a.specialists[0].length.must_equal 2
|
117
|
+
@a.specialists[1].length.must_equal 2
|
118
|
+
@a.specialists[2].length.must_equal 2
|
119
|
+
@a.specialists[0][0].must_equal i3
|
120
|
+
@a.specialists[0][1].must_equal i1
|
121
|
+
@a.specialists[1][0].must_equal i3
|
122
|
+
@a.specialists[1][1].must_equal i1
|
123
|
+
|
124
|
+
@a.weirdos.length.must_equal 0
|
125
|
+
|
126
|
+
# Introduce a new 2nd best
|
127
|
+
i5 = [3,0,1] # [4, 3, 0] => 7
|
128
|
+
@a.add i5
|
129
|
+
|
130
|
+
@a.best.must_equal i3
|
131
|
+
@a.generalists.length.must_equal 3
|
132
|
+
@a.generalists[0].must_equal i3
|
133
|
+
@a.generalists[1].must_equal i5
|
134
|
+
@a.generalists[2].must_equal i1
|
135
|
+
|
136
|
+
@a.specialists[0].length.must_equal 2
|
137
|
+
@a.specialists[1].length.must_equal 2
|
138
|
+
@a.specialists[2].length.must_equal 2
|
139
|
+
@a.specialists[0][0].must_equal i3
|
140
|
+
@a.specialists[0][1].must_equal i5
|
141
|
+
@a.specialists[1][0].must_equal i3
|
142
|
+
@a.specialists[1][1].must_equal i1
|
143
|
+
|
144
|
+
@a.weirdos.length.must_equal 0
|
145
|
+
|
146
|
+
# Introduce 2 new that is close to best and goes into top list but also into
|
147
|
+
# weirdos.
|
148
|
+
i6 = [1.05,2.05,0] # [3.1, 2.05, 0] => 5.15
|
149
|
+
@a.add i6
|
150
|
+
|
151
|
+
i7 = [1.01,2.00,0.10] # [3.11, 2.00, 0.1] => 5.01
|
152
|
+
@a.add i7
|
153
|
+
|
154
|
+
@a.best.must_equal i3
|
155
|
+
@a.generalists.length.must_equal 3
|
156
|
+
@a.generalists[0].must_equal i3
|
157
|
+
@a.generalists[1].must_equal i7
|
158
|
+
@a.generalists[2].must_equal i6
|
159
|
+
|
160
|
+
@a.specialists[0].length.must_equal 2
|
161
|
+
@a.specialists[1].length.must_equal 2
|
162
|
+
@a.specialists[2].length.must_equal 2
|
163
|
+
@a.specialists[0][0].must_equal i3
|
164
|
+
@a.specialists[0][1].must_equal i6
|
165
|
+
@a.specialists[1][0].must_equal i3
|
166
|
+
@a.specialists[1][1].must_equal i7
|
167
|
+
|
168
|
+
@a.weirdos.length.must_equal 2
|
169
|
+
@a.weirdos[0].must_equal i7
|
170
|
+
@a.weirdos[1].must_equal i6
|
171
|
+
|
172
|
+
# Introduce one weird which is not good enough to go into the generalists
|
173
|
+
# or specialists but goes on top of weirdos.
|
174
|
+
i8 = [1.03, 2.10, 0.1] # [3.23, 2.10, 0.1] => 5.23
|
175
|
+
@a.good_enough_quality_to_be_interesting?(i8).must_equal true
|
176
|
+
@a.add i8
|
177
|
+
|
178
|
+
@a.best.must_equal i3
|
179
|
+
@a.generalists.length.must_equal 3
|
180
|
+
@a.generalists[0].must_equal i3
|
181
|
+
@a.generalists[1].must_equal i7
|
182
|
+
@a.generalists[2].must_equal i6
|
183
|
+
|
184
|
+
@a.specialists[0].length.must_equal 2
|
185
|
+
@a.specialists[1].length.must_equal 2
|
186
|
+
@a.specialists[2].length.must_equal 2
|
187
|
+
@a.specialists[0][0].must_equal i3
|
188
|
+
@a.specialists[0][1].must_equal i6
|
189
|
+
@a.specialists[1][0].must_equal i3
|
190
|
+
@a.specialists[1][1].must_equal i7
|
191
|
+
|
192
|
+
@a.weirdos.length.must_equal 2
|
193
|
+
@a.weirdos[0].must_equal i8
|
194
|
+
@a.weirdos[1].must_equal i7
|
195
|
+
|
196
|
+
# Less weird than i8 but still goes into weirdos.
|
197
|
+
i9 = [1.04, 2.08, 0.05] # [3.17, 2.08, 0.05] => 5.20
|
198
|
+
@a.add i9
|
199
|
+
|
200
|
+
@a.best.must_equal i3
|
201
|
+
@a.generalists.length.must_equal 3
|
202
|
+
@a.generalists[0].must_equal i3
|
203
|
+
@a.generalists[1].must_equal i7
|
204
|
+
@a.generalists[2].must_equal i6
|
205
|
+
|
206
|
+
@a.specialists[0].length.must_equal 2
|
207
|
+
@a.specialists[1].length.must_equal 2
|
208
|
+
@a.specialists[2].length.must_equal 2
|
209
|
+
@a.specialists[0][0].must_equal i3
|
210
|
+
@a.specialists[0][1].must_equal i6
|
211
|
+
@a.specialists[1][0].must_equal i3
|
212
|
+
@a.specialists[1][1].must_equal i7
|
213
|
+
|
214
|
+
@a.weirdos.length.must_equal 2
|
215
|
+
@a.weirdos[0].must_equal i8
|
216
|
+
@a.weirdos[1].must_equal i9
|
217
|
+
end
|
218
|
+
end
|
metadata
CHANGED
@@ -1,83 +1,94 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feldtruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.10
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Robert Feldt
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-18 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rinruby
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
16
18
|
requirements:
|
17
|
-
- - '>='
|
19
|
+
- - ! '>='
|
18
20
|
- !ruby/object:Gem::Version
|
19
21
|
version: '0'
|
20
22
|
type: :runtime
|
21
23
|
prerelease: false
|
22
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
23
26
|
requirements:
|
24
|
-
- - '>='
|
27
|
+
- - ! '>='
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '0'
|
27
30
|
- !ruby/object:Gem::Dependency
|
28
31
|
name: json
|
29
32
|
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
30
34
|
requirements:
|
31
|
-
- - '>='
|
35
|
+
- - ! '>='
|
32
36
|
- !ruby/object:Gem::Version
|
33
37
|
version: '0'
|
34
38
|
type: :runtime
|
35
39
|
prerelease: false
|
36
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
37
42
|
requirements:
|
38
|
-
- - '>='
|
43
|
+
- - ! '>='
|
39
44
|
- !ruby/object:Gem::Version
|
40
45
|
version: '0'
|
41
46
|
- !ruby/object:Gem::Dependency
|
42
47
|
name: nokogiri
|
43
48
|
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
44
50
|
requirements:
|
45
|
-
- - '>='
|
51
|
+
- - ! '>='
|
46
52
|
- !ruby/object:Gem::Version
|
47
53
|
version: '0'
|
48
54
|
type: :runtime
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
51
58
|
requirements:
|
52
|
-
- - '>='
|
59
|
+
- - ! '>='
|
53
60
|
- !ruby/object:Gem::Version
|
54
61
|
version: '0'
|
55
62
|
- !ruby/object:Gem::Dependency
|
56
63
|
name: mongo
|
57
64
|
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
58
66
|
requirements:
|
59
|
-
- - '>='
|
67
|
+
- - ! '>='
|
60
68
|
- !ruby/object:Gem::Version
|
61
69
|
version: '0'
|
62
70
|
type: :runtime
|
63
71
|
prerelease: false
|
64
72
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
65
74
|
requirements:
|
66
|
-
- - '>='
|
75
|
+
- - ! '>='
|
67
76
|
- !ruby/object:Gem::Version
|
68
77
|
version: '0'
|
69
78
|
- !ruby/object:Gem::Dependency
|
70
79
|
name: bson_ext
|
71
80
|
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
72
82
|
requirements:
|
73
|
-
- - '>='
|
83
|
+
- - ! '>='
|
74
84
|
- !ruby/object:Gem::Version
|
75
85
|
version: '0'
|
76
86
|
type: :runtime
|
77
87
|
prerelease: false
|
78
88
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
79
90
|
requirements:
|
80
|
-
- - '>='
|
91
|
+
- - ! '>='
|
81
92
|
- !ruby/object:Gem::Version
|
82
93
|
version: '0'
|
83
94
|
description: Robert Feldt's Common Ruby Code lib
|
@@ -112,8 +123,8 @@ files:
|
|
112
123
|
- lib/feldtruby/mongodb_logger.rb
|
113
124
|
- lib/feldtruby/net/html_doc_getter.rb
|
114
125
|
- lib/feldtruby/optimize.rb
|
126
|
+
- lib/feldtruby/optimize/archive.rb
|
115
127
|
- lib/feldtruby/optimize/differential_evolution.rb
|
116
|
-
- lib/feldtruby/optimize/elite_archive.rb
|
117
128
|
- lib/feldtruby/optimize/max_steps_termination_criterion.rb
|
118
129
|
- lib/feldtruby/optimize/objective.rb
|
119
130
|
- lib/feldtruby/optimize/optimizer.rb
|
@@ -167,8 +178,8 @@ files:
|
|
167
178
|
- test/test_normalization.rb
|
168
179
|
- test/test_object_annotations.rb
|
169
180
|
- test/test_optimize.rb
|
181
|
+
- test/test_optimize_archive.rb
|
170
182
|
- test/test_optimize_differential_evolution.rb
|
171
|
-
- test/test_optimize_elite_archive.rb
|
172
183
|
- test/test_optimize_objective.rb
|
173
184
|
- test/test_optimize_populationbasedoptimizer.rb
|
174
185
|
- test/test_optimize_random_search.rb
|
@@ -185,26 +196,27 @@ files:
|
|
185
196
|
- test/tmp_shorter.csv
|
186
197
|
homepage: https://github.com/robertfeldt/feldtruby
|
187
198
|
licenses: []
|
188
|
-
metadata: {}
|
189
199
|
post_install_message:
|
190
200
|
rdoc_options: []
|
191
201
|
require_paths:
|
192
202
|
- lib
|
193
203
|
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
none: false
|
194
205
|
requirements:
|
195
|
-
- - '>='
|
206
|
+
- - ! '>='
|
196
207
|
- !ruby/object:Gem::Version
|
197
208
|
version: '0'
|
198
209
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
210
|
+
none: false
|
199
211
|
requirements:
|
200
|
-
- - '>='
|
212
|
+
- - ! '>='
|
201
213
|
- !ruby/object:Gem::Version
|
202
214
|
version: '0'
|
203
215
|
requirements: []
|
204
216
|
rubyforge_project:
|
205
|
-
rubygems_version:
|
217
|
+
rubygems_version: 1.8.25
|
206
218
|
signing_key:
|
207
|
-
specification_version:
|
219
|
+
specification_version: 3
|
208
220
|
summary: Robert Feldt's Common Ruby Code lib
|
209
221
|
test_files:
|
210
222
|
- test/helper.rb
|
@@ -226,8 +238,8 @@ test_files:
|
|
226
238
|
- test/test_normalization.rb
|
227
239
|
- test/test_object_annotations.rb
|
228
240
|
- test/test_optimize.rb
|
241
|
+
- test/test_optimize_archive.rb
|
229
242
|
- test/test_optimize_differential_evolution.rb
|
230
|
-
- test/test_optimize_elite_archive.rb
|
231
243
|
- test/test_optimize_objective.rb
|
232
244
|
- test/test_optimize_populationbasedoptimizer.rb
|
233
245
|
- test/test_optimize_random_search.rb
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 649d3dfa9702ca3b927e1f439220f1aeed6ca970
|
4
|
-
data.tar.gz: 3bc58c5d5c11417907254bc22d4b974b1725bed6
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: e02ff98f8487db6542dc5d9d86c53d7e1a8e228b872d2a1e51f075fcc6b8f4ad2025d5123d1e8f2e84a3ee7efc72b4fce411464696e8181e07a8227fbbd06f0e
|
7
|
-
data.tar.gz: 79e5b1e5ee1cd35181e3675c159d242a9cd1861218f361a6b545f4753b38af329427dd28d1b2d83c690aebe96d915e97c3894d24c71d1ddca8da689c0f39040e
|
@@ -1,91 +0,0 @@
|
|
1
|
-
require 'feldtruby/optimize'
|
2
|
-
|
3
|
-
module FeldtRuby::Optimize
|
4
|
-
|
5
|
-
# This keeps a record of the best/elite candidate solutions found during
|
6
|
-
# an optimization search. It keeps separate top lists per goal being optimized
|
7
|
-
# as well as for the aggregate quality value (fitness) itself. The top lists
|
8
|
-
# are all sorted to allow for fast checks and insertion.
|
9
|
-
class EliteArchive
|
10
|
-
DefaultParams = {
|
11
|
-
:NumTopPerGoal => 10,
|
12
|
-
:NumTopAggregate => 25
|
13
|
-
}
|
14
|
-
|
15
|
-
attr_reader :objective, :top_per_goal, :best
|
16
|
-
|
17
|
-
def initialize(objective, options = DefaultParams.clone)
|
18
|
-
@objective = objective
|
19
|
-
@options = options
|
20
|
-
init_top_lists
|
21
|
-
end
|
22
|
-
|
23
|
-
# A top list is an array of a fixed size that saves the top candidates
|
24
|
-
# based on their quality values.
|
25
|
-
class GlobalTopList
|
26
|
-
def initialize(maxSize, objective)
|
27
|
-
@max_size =maxSize
|
28
|
-
@top_list = Array.new
|
29
|
-
@objective = objective
|
30
|
-
end
|
31
|
-
def length; @top_list.length; end
|
32
|
-
|
33
|
-
def [](index)
|
34
|
-
@top_list[index]
|
35
|
-
end
|
36
|
-
|
37
|
-
def add(candidate)
|
38
|
-
last = @top_list.last
|
39
|
-
#puts "In #{self},\nlast = #{last}, candidate = #{candidate}, top_list = #{@top_list}"
|
40
|
-
if @top_list.length < @max_size || last.nil? || is_better_than?(candidate, last)
|
41
|
-
@top_list.pop if @top_list.length >= @max_size
|
42
|
-
@top_list << candidate
|
43
|
-
@top_list = sort_top_list
|
44
|
-
end
|
45
|
-
#puts "top_list = #{@top_list}"
|
46
|
-
end
|
47
|
-
|
48
|
-
def is_better_than?(candidate1, candidate2)
|
49
|
-
@objective.is_better_than?(candidate1, candidate2)
|
50
|
-
end
|
51
|
-
|
52
|
-
def sort_top_list
|
53
|
-
@top_list.sort_by {|c| @objective.quality_of(c).value}
|
54
|
-
end
|
55
|
-
|
56
|
-
def inspect
|
57
|
-
self.class.inspect + @top_list.inspect
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class GoalTopList < GlobalTopList
|
62
|
-
def initialize(maxSize, objective, goalIndex)
|
63
|
-
super(maxSize, objective)
|
64
|
-
@index = goalIndex
|
65
|
-
end
|
66
|
-
def is_better_than?(candidate1, candidate2)
|
67
|
-
@objective.is_better_than_for_goal?(@index, candidate1, candidate2)
|
68
|
-
end
|
69
|
-
def sort_top_list
|
70
|
-
@top_list.sort_by {|c|
|
71
|
-
qv = @objective.quality_of(c)
|
72
|
-
qv.sub_quality(@index, true) # We want the sub quality value posed as a minimization goal regardless of whether it is a min or max goal
|
73
|
-
}
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def init_top_lists
|
78
|
-
@top_per_goal = Array.new
|
79
|
-
@objective.num_goals.times do |i|
|
80
|
-
@top_per_goal << GoalTopList.new(@options[:NumTopPerGoal], @objective, i)
|
81
|
-
end
|
82
|
-
@best = GlobalTopList.new(@options[:NumTopAggregate], @objective)
|
83
|
-
end
|
84
|
-
|
85
|
-
def add(candidate)
|
86
|
-
@best.add candidate
|
87
|
-
@top_per_goal.each {|tl| tl.add(candidate)}
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
end
|
@@ -1,85 +0,0 @@
|
|
1
|
-
require 'feldtruby/optimize/elite_archive'
|
2
|
-
|
3
|
-
class TwoMinOneMax < FeldtRuby::Optimize::Objective
|
4
|
-
def objective_min_1(x)
|
5
|
-
x.sum
|
6
|
-
end
|
7
|
-
|
8
|
-
def objective_min_2(x)
|
9
|
-
x.max
|
10
|
-
end
|
11
|
-
|
12
|
-
def objective_max_1(x)
|
13
|
-
x.min
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
describe "EliteArchive" do
|
18
|
-
before do
|
19
|
-
@o = TwoMinOneMax.new(FeldtRuby::Optimize::Objective::WeightedSumAggregator.new)
|
20
|
-
@a = FeldtRuby::Optimize::EliteArchive.new(@o, {
|
21
|
-
:NumTopPerGoal => 2,
|
22
|
-
:NumTopAggregate => 3})
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'is adapted to an objective when created' do
|
26
|
-
@a.objective.must_equal @o
|
27
|
-
@a.top_per_goal.length.must_equal @o.num_goals
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'properly handles additions' do
|
31
|
-
i1 = [1,2,3]
|
32
|
-
@a.add i1 # [6, 3, 1] => 8
|
33
|
-
@a.best.length.must_equal 1
|
34
|
-
@a.top_per_goal[0].length.must_equal 1
|
35
|
-
@a.top_per_goal[1].length.must_equal 1
|
36
|
-
@a.top_per_goal[2].length.must_equal 1
|
37
|
-
|
38
|
-
i2 = [1,2,4]
|
39
|
-
@a.add i2 # [7, 4, 1] => 10
|
40
|
-
@a.best.length.must_equal 2
|
41
|
-
@a.top_per_goal[0].length.must_equal 2
|
42
|
-
@a.top_per_goal[1].length.must_equal 2
|
43
|
-
@a.top_per_goal[2].length.must_equal 2
|
44
|
-
@a.top_per_goal[0][0].must_equal i1
|
45
|
-
@a.top_per_goal[0][1].must_equal i2
|
46
|
-
@a.top_per_goal[1][0].must_equal i1
|
47
|
-
@a.top_per_goal[1][1].must_equal i2
|
48
|
-
|
49
|
-
i3 = [1,2,0] # [3, 2, 0] => 5
|
50
|
-
@a.add i3
|
51
|
-
|
52
|
-
@a.best.length.must_equal 3
|
53
|
-
@a.top_per_goal[0].length.must_equal 2
|
54
|
-
@a.top_per_goal[1].length.must_equal 2
|
55
|
-
@a.top_per_goal[2].length.must_equal 2
|
56
|
-
|
57
|
-
@a.best[0].must_equal i3
|
58
|
-
@a.best[1].must_equal i1
|
59
|
-
@a.best[2].must_equal i2
|
60
|
-
|
61
|
-
@a.top_per_goal[0][0].must_equal i3
|
62
|
-
@a.top_per_goal[0][1].must_equal i1
|
63
|
-
|
64
|
-
@a.top_per_goal[1][0].must_equal i3
|
65
|
-
@a.top_per_goal[1][1].must_equal i1
|
66
|
-
|
67
|
-
@a.top_per_goal[2][0].must_equal i1
|
68
|
-
@a.top_per_goal[2][1].must_equal i2
|
69
|
-
|
70
|
-
i4 = [5,5,10] # [20, 10, 5] => 25
|
71
|
-
@a.add i4
|
72
|
-
|
73
|
-
@a.best.length.must_equal 3
|
74
|
-
@a.top_per_goal[0].length.must_equal 2
|
75
|
-
@a.top_per_goal[1].length.must_equal 2
|
76
|
-
@a.top_per_goal[2].length.must_equal 2
|
77
|
-
|
78
|
-
@a.best[0].must_equal i3
|
79
|
-
@a.best[1].must_equal i1
|
80
|
-
@a.best[2].must_equal i2
|
81
|
-
|
82
|
-
@a.top_per_goal[2][0].must_equal i4
|
83
|
-
@a.top_per_goal[2][1].must_equal i1
|
84
|
-
end
|
85
|
-
end
|