feldtruby 0.4.9 → 0.4.10
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/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
|