feldtruby 0.3.8 → 0.3.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.md +9 -11
- data/Rakefile +21 -4
- data/lib/feldtruby/array.rb +5 -0
- data/lib/feldtruby/minitest_extensions.rb +10 -0
- data/lib/feldtruby/optimize/objective.rb +168 -125
- data/lib/feldtruby/optimize/search_space.rb +105 -9
- data/lib/feldtruby/optimize/sub_qualities_comparators.rb +73 -0
- data/lib/feldtruby/statistics/array_archive.rb +66 -0
- data/lib/feldtruby/statistics/clustering.rb +31 -0
- data/lib/feldtruby/statistics/distance.rb +35 -0
- data/lib/feldtruby/statistics/euclidean_distance.rb +4 -0
- data/lib/feldtruby/statistics.rb +48 -0
- data/lib/feldtruby/version.rb +1 -1
- data/spikes/zlib_for_short_strings.rb +27 -0
- data/test/skip_test_array_archive.rb +65 -0
- data/test/test_array.rb +6 -0
- data/test/test_clustering.rb +53 -0
- data/test/test_euclidean_distance.rb +28 -0
- data/test/test_optimize_objective.rb +133 -93
- data/test/test_optimize_search_space.rb +54 -0
- data/test/test_sax.rb +14 -1
- data/test/test_sub_qualitites_comparator.rb +109 -0
- metadata +15 -2
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -2,10 +2,18 @@ feldtruby
|
|
2
2
|
=========
|
3
3
|
Robert Feldt's Common Ruby Code lib. I will gradually collect the many generally useful Ruby tidbits I have laying around and clean them up into here. Don't want to rewrite these things again and again... So far this collects a number of generally useful additions to the standard Ruby classes/libs and then includes a simple optimization framework (FeldtRuby::Optimize).
|
4
4
|
|
5
|
+
Note that good documentation is not really a focus here. As things mature in here I will move logically unique/separate sets of functionality into separate Ruby libs/gems of useful code. At that point there will be more focus on documentation.
|
6
|
+
|
5
7
|
email: robert.feldt ((a)) gmail.com
|
6
8
|
|
7
9
|
Contents
|
8
10
|
--------
|
11
|
+
|
12
|
+
### Statistics
|
13
|
+
* Cluster linkage metrics
|
14
|
+
* Access to R from Ruby (extends existing lib so that you can more easily transfer complex objects back to Ruby)
|
15
|
+
* ...
|
16
|
+
|
9
17
|
### Time
|
10
18
|
Time.timestamp() # Get a timestamp string back with the current time
|
11
19
|
|
@@ -47,17 +55,7 @@ numerical optimization using DE:
|
|
47
55
|
(1 - x)**2 + 100*(y - x*x)**2
|
48
56
|
}
|
49
57
|
|
50
|
-
Contributing to feldtruby
|
51
|
-
-------------------------
|
52
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
53
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
54
|
-
* Fork the project.
|
55
|
-
* Start a feature/bugfix branch.
|
56
|
-
* Commit and push until you are happy with your contribution.
|
57
|
-
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
58
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
59
|
-
|
60
58
|
Copyright
|
61
59
|
------------
|
62
|
-
Copyright (c) 2012 Robert Feldt. See LICENSE.txt for
|
60
|
+
Copyright (c) 2012-2013 Robert Feldt. See LICENSE.txt for
|
63
61
|
further details.
|
data/Rakefile
CHANGED
@@ -7,14 +7,31 @@ def psys(str)
|
|
7
7
|
system str
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
task :test do
|
10
|
+
def run_tests(testFiles)
|
12
11
|
helper_files = Dir["test/**/*helper*.rb"]
|
13
|
-
|
14
|
-
require_files = (helper_files + test_files).map {|f| "require \"#{f}\""}.join('; ')
|
12
|
+
require_files = (helper_files + testFiles).map {|f| "require \"#{f}\""}.join('; ')
|
15
13
|
psys "ruby -Ilib:. -e '#{require_files}' --"
|
16
14
|
end
|
17
15
|
|
16
|
+
desc "Run all tests"
|
17
|
+
task :test do
|
18
|
+
run_tests Dir["test/**/test*.rb"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter_latest_changed_files(filenames, numLatestChangedToInclude = 1)
|
22
|
+
filenames.sort_by{ |f| File.mtime(f) }[-numLatestChangedToInclude, numLatestChangedToInclude]
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Run only the latest changed test file"
|
26
|
+
task :t do
|
27
|
+
run_tests filter_latest_changed_files(Dir["test/**/test*.rb"])
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Run only the latest two changed test file"
|
31
|
+
task :t2 do
|
32
|
+
run_tests filter_latest_changed_files(Dir["test/**/test*.rb"], 2)
|
33
|
+
end
|
34
|
+
|
18
35
|
desc "Clean up intermediate/build files"
|
19
36
|
task :clean do
|
20
37
|
FileUtils.rm_rf "pkg"
|
data/lib/feldtruby/array.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'feldtruby/array/basic_stats'
|
2
2
|
|
3
3
|
class Array
|
4
|
+
def map_with_index(&block)
|
5
|
+
index = -1 # Start below zero since we pre-increment in the loop
|
6
|
+
self.map {|v| block.call(v,index+=1)}
|
7
|
+
end
|
8
|
+
|
4
9
|
# Calculate the distance between the elements.
|
5
10
|
def distance_between_elements
|
6
11
|
return nil if self.length == 0
|
@@ -10,8 +10,18 @@ module MiniTest::Assertions
|
|
10
10
|
pvalue = FeldtRuby.chi_squared_test(values)
|
11
11
|
assert(pvalue > expectedPValue, msg || "Proportions differ! p-value is #{pvalue} (<0.05), counts: #{values.counts.inspect}")
|
12
12
|
end
|
13
|
+
|
14
|
+
def assert_falsey(value, msg = nil)
|
15
|
+
assert(value.!, msg || "#{value} is not falsey (it is #{value})")
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_truthy(value, msg = nil)
|
19
|
+
assert(value, msg || "#{value} is not truthy (it is #{value})")
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
23
|
module MiniTest::Expectations
|
16
24
|
infect_an_assertion :assert_similar_proportions, :must_have_similar_proportions
|
25
|
+
infect_an_assertion :assert_falsey, :must_be_falsey
|
26
|
+
infect_an_assertion :assert_truthy, :must_be_truthy
|
17
27
|
end
|
@@ -1,124 +1,128 @@
|
|
1
|
-
|
2
1
|
require 'feldtruby/optimize'
|
3
2
|
require 'feldtruby/float'
|
3
|
+
require 'feldtruby/optimize/sub_qualities_comparators'
|
4
|
+
|
5
|
+
module FeldtRuby::Optimize
|
4
6
|
|
5
|
-
# An Objective captures one or more objectives into a single object
|
7
|
+
# An Objective captures one or more (sub-)objectives into a single object
|
6
8
|
# and supports a large number of ways to utilize basic objective
|
7
9
|
# functions in a single framework. You subclass and add instance
|
8
10
|
# methods named as
|
9
11
|
# objective_min_qualityAspectName (for an objective/aspect to be minimized), or
|
10
12
|
# objective_max_qualityAspectName (for an objective/aspect to be minimized).
|
13
|
+
#
|
11
14
|
# There can be multiple aspects (sub-objectives) for a single objective.
|
15
|
+
#
|
16
|
+
# An objective keeps track of the min and max value that has been seen so far
|
17
|
+
# for each sub-objective.
|
18
|
+
|
12
19
|
# This base class uses mean-weighted-global-ratios (MWGR) as the default mechanism
|
13
|
-
# for handling multi-objectives i.e. with more than one sub-objective.
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
20
|
+
# for handling multi-objectives i.e. with more than one sub-objective.
|
21
|
+
#
|
22
|
+
# An objective has version numbers to indicate the number of times a new min or max
|
23
|
+
# value has been identified for a sub-objective.
|
24
|
+
class Objective
|
25
|
+
# Current version of this objective. Is updated when the min or max values for a sub-objective
|
26
|
+
# has been updated.
|
27
|
+
attr_accessor :current_version
|
28
|
+
|
29
|
+
# For logging changes to the objective.
|
30
|
+
attr_accessor :logger
|
18
31
|
|
19
32
|
def initialize
|
20
33
|
@logger = nil # To avoid getting warnings that logger has not been initialized
|
21
34
|
@current_version = 0
|
22
|
-
@pareto_front = Array.new(num_aspects)
|
35
|
+
@pareto_front = Array.new(num_aspects) # The pareto front is misguided since it only has one best value per sub-objective, not the whole front!
|
23
36
|
end
|
24
37
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
38
|
+
# Return the number of aspects/sub-objectives of this objective.
|
39
|
+
def num_aspects
|
40
|
+
@num_aspects ||= aspect_methods.length
|
41
|
+
end
|
30
42
|
|
31
|
-
|
32
|
-
|
33
|
-
|
43
|
+
def num_sub_objectives
|
44
|
+
num_aspects
|
45
|
+
end
|
34
46
|
|
35
|
-
|
47
|
+
def aspect_methods
|
48
|
+
@aspect_methods ||= self.methods.select {|m| is_aspect_method?(m)}
|
36
49
|
end
|
37
50
|
|
38
|
-
def
|
39
|
-
@
|
40
|
-
@best_qv = candidate._quality_value
|
51
|
+
def is_min_aspect?(aspectIndex)
|
52
|
+
(@is_min_aspect ||= (aspect_methods.map {|m| is_min_aspect_method?(m)}))[aspectIndex]
|
41
53
|
end
|
42
54
|
|
43
|
-
def
|
44
|
-
|
55
|
+
def is_aspect_method?(methodNameAsSymbolOrString)
|
56
|
+
methodNameAsSymbolOrString.to_s =~ /^objective_(min|max)_([\w_]+)$/
|
45
57
|
end
|
46
58
|
|
47
|
-
|
48
|
-
|
49
|
-
@num_aspects ||= aspect_methods.length
|
59
|
+
def is_min_aspect_method?(methodNameAsSymbolOrString)
|
60
|
+
methodNameAsSymbolOrString.to_s =~ /^objective_min_([\w_]+)$/
|
50
61
|
end
|
51
62
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
63
|
+
# The vectors can be mapped to a more complex candidate object before we call
|
64
|
+
# the sub objectives to calc their quality values. Default is no mapping but subclasses
|
65
|
+
# can override this for more complex evaluation schemes.
|
66
|
+
def map_candidate_vector_to_candidate_to_be_evaluated(vector)
|
67
|
+
vector
|
68
|
+
end
|
55
69
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
70
|
+
# Return a vector of the "raw" sub-quality values, i.e. the fitness value for each sub-objective.
|
71
|
+
# The candidate vector is assumed to be a vector of values.
|
72
|
+
def sub_qualities_of(candidateVector)
|
73
|
+
candidate = map_candidate_vector_to_candidate_to_be_evaluated(candidateVector)
|
74
|
+
aspect_methods.map {|omethod| self.send(omethod, candidate)}
|
75
|
+
end
|
60
76
|
|
61
|
-
|
62
|
-
|
63
|
-
|
77
|
+
# Return a single quality value for the whole objective for a given candidate.
|
78
|
+
# By default this uses a variant of Bentley and Wakefield's sum-of-weighted-global-ratios (SWGR)
|
79
|
+
# called mean-of-weighted-global-ratios (MWGR) which always returns a fitness value
|
80
|
+
# in the range (0.0, 1.0) with 1.0 signaling the best fitness seen so far. The scale is adaptive
|
81
|
+
# though so that the best candidate so far always has a fitness value of 1.0.
|
82
|
+
def quality_of(candidate, weights = self.default_weights)
|
83
|
+
return candidate._quality_value_without_check if quality_value_is_up_to_date?(candidate)
|
84
|
+
num_aspects == 1 ? qv_single(candidate) : qv_mwgr(candidate, weights)
|
85
|
+
end
|
64
86
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
87
|
+
# Set the default weights to use when calculating a single quality values from
|
88
|
+
# the vector of sub-qualities.
|
89
|
+
def default_weights=(weights)
|
90
|
+
raise "Must be same number of weights as there are sub-objectives (#{num_aspects}), but is #{weights.length}" unless weights.length == num_aspects
|
91
|
+
@default_weights = weights
|
92
|
+
end
|
71
93
|
|
72
|
-
|
73
|
-
|
74
|
-
pdiff = @qv.ratio_diff_vs(other.qv)
|
75
|
-
subpdiffs = @sub_qvs.zip(other.sub_qvs).map {|s, os| s.ratio_diff_vs(os)}
|
76
|
-
qinspect(pdiff, subpdiffs, "Difference", "SubQ. differences", true) + ", #{report_on_num_differences(subpdiffs)}"
|
77
|
-
else
|
78
|
-
@qv.improvement_in_relation_to(other)
|
79
|
-
end
|
80
|
-
end
|
94
|
+
# Current default weights among the sub-objectives (nil if none have been set)
|
95
|
+
attr_reader :default_weights
|
81
96
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
"#{num_inc} increased, #{num_dec} decreased, #{num_same} same"
|
87
|
-
end
|
97
|
+
# Return the fitness of a candidate. It is the same as the quality value above.
|
98
|
+
def fitness_for(candidate, weights = nil)
|
99
|
+
quality_of(candidate, weights)
|
100
|
+
end
|
88
101
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
s = (Float === sqv ? sqv.round_to_decimals(4) : sqv).inspect
|
93
|
-
s += "%" if subQvsAreRatios
|
94
|
-
s
|
95
|
-
end.join(", ")
|
96
|
-
if qvIsRatio
|
97
|
-
qstr = ("%.4f" % (100.0 * qv)) + "%"
|
98
|
-
else
|
99
|
-
qstr = "%.4f" % qv
|
100
|
-
end
|
101
|
-
"#{qvDesc}: #{qstr}, #{subQvDesc}: [#{sqs}]"
|
102
|
-
end
|
102
|
+
#############################
|
103
|
+
# Sane above here!
|
104
|
+
#############################
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
+
def reset_quality_scale(candidate, aspectIndex, typeOfReset)
|
107
|
+
if (typeOfReset == :min && is_min_aspect?(aspectIndex)) ||
|
108
|
+
(typeOfReset == :max && !is_min_aspect?(aspectIndex))
|
109
|
+
@pareto_front[aspectIndex] = candidate
|
106
110
|
end
|
107
111
|
|
108
|
-
#
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
+
# Reset the best object since we have a new scale
|
113
|
+
@best_candidate = nil
|
114
|
+
@best_qv = nil
|
115
|
+
|
116
|
+
inc_version_number
|
112
117
|
end
|
113
118
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
def
|
120
|
-
|
121
|
-
num_aspects == 1 ? qv_single(candidate) : qv_mwgr(candidate, weights)
|
119
|
+
def update_best_candidate(candidate)
|
120
|
+
@best_candidate = candidate
|
121
|
+
@best_qv = candidate._quality_value
|
122
|
+
end
|
123
|
+
|
124
|
+
def inc_version_number
|
125
|
+
@current_version += 1
|
122
126
|
end
|
123
127
|
|
124
128
|
def quality_value_is_up_to_date?(candidate)
|
@@ -143,7 +147,7 @@ class FeldtRuby::Optimize::Objective
|
|
143
147
|
# Rand candidates from best to worst. NOTE! We do the steps of MWGR separately since we must
|
144
148
|
# update the global mins and maxs before calculating the SWG ratios.
|
145
149
|
def mwgr_rank_candidates(candidates, weights = nil)
|
146
|
-
sub_qvss = candidates.map {|c|
|
150
|
+
sub_qvss = candidates.map {|c| sub_qualities_of(c)}
|
147
151
|
sub_qvss.zip(candidates).each {|sub_qvs, c| update_global_mins_and_maxs(sub_qvs, c)}
|
148
152
|
sub_qvss.each_with_index.map do |sub_qvs, i|
|
149
153
|
qv = mwgr_ratios(sub_qvs).weighted_mean(weights)
|
@@ -191,18 +195,6 @@ class FeldtRuby::Optimize::Objective
|
|
191
195
|
numerator.to_f.protected_division_with(max - min)
|
192
196
|
end
|
193
197
|
|
194
|
-
# The vectors can be mapped to a more complex candidate object before we call
|
195
|
-
# the sub objectives to calc their quality values. Default is no mapping but subclasses
|
196
|
-
# can override this.
|
197
|
-
def map_candidate_vector_to_candidate_to_be_evaluated(vector)
|
198
|
-
vector
|
199
|
-
end
|
200
|
-
|
201
|
-
def sub_objective_values(candidateVector)
|
202
|
-
candidate = map_candidate_vector_to_candidate_to_be_evaluated(candidateVector)
|
203
|
-
aspect_methods.map {|omethod| self.send(omethod, candidate)}
|
204
|
-
end
|
205
|
-
|
206
198
|
def update_global_mins_and_maxs(aspectValues, candidate = nil)
|
207
199
|
aspectValues.each_with_index {|v, i| update_global_min_and_max(i, v, candidate)}
|
208
200
|
end
|
@@ -222,6 +214,68 @@ class FeldtRuby::Optimize::Objective
|
|
222
214
|
end
|
223
215
|
end
|
224
216
|
|
217
|
+
# Class for representing multi-objective qualitites...
|
218
|
+
class QualityValue
|
219
|
+
attr_reader :qv, :sub_qvs, :objective
|
220
|
+
|
221
|
+
def initialize(qv, subQvs, objective)
|
222
|
+
@qv, @sub_qvs, @objective = qv, subQvs, objective
|
223
|
+
@version = objective.current_version
|
224
|
+
end
|
225
|
+
|
226
|
+
def <=>(other)
|
227
|
+
@qv <=> other.qv
|
228
|
+
end
|
229
|
+
|
230
|
+
# Two quality values are the same if they have the same qv, regardless of their
|
231
|
+
# sub qualities.
|
232
|
+
def ==(other)
|
233
|
+
other = other.qv if QualityValue === other
|
234
|
+
@qv == other
|
235
|
+
end
|
236
|
+
|
237
|
+
def improvement_in_relation_to(other)
|
238
|
+
if QualityValue === other
|
239
|
+
pdiff = @qv.ratio_diff_vs(other.qv)
|
240
|
+
subpdiffs = @sub_qvs.zip(other.sub_qvs).map {|s, os| s.ratio_diff_vs(os)}
|
241
|
+
qinspect(pdiff, subpdiffs, "Difference", "SubQ. differences", true) + ", #{report_on_num_differences(subpdiffs)}"
|
242
|
+
else
|
243
|
+
@qv.improvement_in_relation_to(other)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def report_on_num_differences(subQvRatioDiffs)
|
248
|
+
num_inc = subQvRatioDiffs.select {|v| v > 0}.length
|
249
|
+
num_dec = subQvRatioDiffs.select {|v| v < 0}.length
|
250
|
+
num_same = subQvRatioDiffs.length - num_inc - num_dec
|
251
|
+
"#{num_inc} increased, #{num_dec} decreased, #{num_same} same"
|
252
|
+
end
|
253
|
+
|
254
|
+
def qinspect(qv, subQvs, qvDesc = "Quality", subQvDesc = "SubQualities", subQvsAreRatios = false, qvIsRatio = true)
|
255
|
+
subQvs = subQvs.map {|v| v*100.0} if subQvsAreRatios
|
256
|
+
sqs = subQvs.map do |sqv|
|
257
|
+
s = (Float === sqv ? sqv.round_to_decimals(4) : sqv).inspect
|
258
|
+
s += "%" if subQvsAreRatios
|
259
|
+
s
|
260
|
+
end.join(", ")
|
261
|
+
if qvIsRatio
|
262
|
+
qstr = ("%.4f" % (100.0 * qv)) + "%"
|
263
|
+
else
|
264
|
+
qstr = "%.4f" % qv
|
265
|
+
end
|
266
|
+
"#{qvDesc}: #{qstr}, #{subQvDesc}: [#{sqs}]"
|
267
|
+
end
|
268
|
+
|
269
|
+
def inspect
|
270
|
+
qinspect(@qv, @sub_qvs) + ", Obj. version: #{@version}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# Refer all other methods to the main quality value
|
274
|
+
def method_missing(meth, *args, &block)
|
275
|
+
@qv.send(meth, *args, &block)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
225
279
|
def log_new_min_max(index, newValue, oldValue, description)
|
226
280
|
log("New global #{description} for sub-objective #{aspect_methods[index]}",
|
227
281
|
("a %.3f" % (100.0 * (newValue - oldValue).protected_division_with(oldValue))) + "% difference",
|
@@ -247,38 +301,15 @@ class FeldtRuby::Optimize::Objective
|
|
247
301
|
def global_max_values_per_aspect
|
248
302
|
@global_max_values_per_aspect ||= Array.new(num_aspects).map {-Float::INFINITY}
|
249
303
|
end
|
250
|
-
|
251
|
-
private
|
252
|
-
|
253
|
-
def aspect_methods
|
254
|
-
@aspect_methods ||= self.methods.select {|m| is_aspect_method?(m)}
|
255
|
-
end
|
256
|
-
|
257
|
-
def is_min_aspect?(aspectIndex)
|
258
|
-
(@is_min_aspect ||= (aspect_methods.map {|m| is_min_aspect_method?(m)}))[aspectIndex]
|
259
|
-
end
|
260
|
-
|
261
|
-
def is_aspect_method?(methodNameAsSymbolOrString)
|
262
|
-
methodNameAsSymbolOrString.to_s =~ /^objective_(min|max)_([\w_]+)$/
|
263
|
-
end
|
264
|
-
|
265
|
-
def is_min_aspect_method?(methodNameAsSymbolOrString)
|
266
|
-
methodNameAsSymbolOrString.to_s =~ /^objective_min_([\w_]+)$/
|
267
|
-
end
|
268
304
|
end
|
269
305
|
|
270
|
-
#
|
271
|
-
#
|
272
|
-
|
273
|
-
attr_accessor :_quality_value_without_check, :_objective, :_objective_version
|
274
|
-
def _quality_value
|
275
|
-
@_objective.ensure_updated_quality_value(self) if defined?(@_objective) && @_objective
|
276
|
-
@_quality_value_without_check ||= nil # To avoid warning if unset
|
277
|
-
end
|
306
|
+
# The MWGR is a simple way to weigh the fitness values of multiple sub-objectives into a single
|
307
|
+
# fitness value.
|
308
|
+
module MeanWeigthedGlobalRatios
|
278
309
|
end
|
279
310
|
|
280
311
|
# Short hand for when the objective function is given as a block that should be minimized.
|
281
|
-
class
|
312
|
+
class ObjectiveMinimizeBlock < Objective
|
282
313
|
def initialize(&objFunc)
|
283
314
|
super()
|
284
315
|
@objective_function = objFunc
|
@@ -290,7 +321,7 @@ class FeldtRuby::Optimize::ObjectiveMinimizeBlock < FeldtRuby::Optimize::Objecti
|
|
290
321
|
end
|
291
322
|
|
292
323
|
# Short hand for when the objective function is given as a block that should be minimized.
|
293
|
-
class
|
324
|
+
class ObjectiveMaximizeBlock < Objective
|
294
325
|
def initialize(&objFunc)
|
295
326
|
super()
|
296
327
|
@objective_function = objFunc
|
@@ -299,4 +330,16 @@ class FeldtRuby::Optimize::ObjectiveMaximizeBlock < FeldtRuby::Optimize::Objecti
|
|
299
330
|
def objective_max_cost_function(candidate)
|
300
331
|
@objective_function.call(*candidate.to_a)
|
301
332
|
end
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
# We add strangely named accessor methods so we can attach the quality values to objects.
|
338
|
+
# We use strange names to minimize risk of method name conflicts.
|
339
|
+
class Object
|
340
|
+
attr_accessor :_quality_value_without_check, :_objective, :_objective_version
|
341
|
+
def _quality_value
|
342
|
+
@_objective.ensure_updated_quality_value(self) if defined?(@_objective) && @_objective
|
343
|
+
@_quality_value_without_check ||= nil # To avoid warning if unset
|
344
|
+
end
|
302
345
|
end
|
@@ -1,14 +1,18 @@
|
|
1
1
|
require 'feldtruby/optimize'
|
2
2
|
|
3
|
+
module FeldtRuby::Optimize
|
4
|
+
|
3
5
|
# A search space is a set of constraints that limits which values
|
4
6
|
# are searched for. The search space can generate valid candidate
|
5
7
|
# solutions that are inside the space. It can also check if a
|
6
8
|
# given candidate is in the space. The default search space has min
|
7
9
|
# and max values for each element of a continuous vector.
|
8
|
-
class
|
9
|
-
attr_reader :min_values, :max_values
|
10
|
+
class SearchSpace
|
11
|
+
attr_reader :min_values, :max_values, :deltas
|
10
12
|
|
11
|
-
def initialize(minValues, maxValues)
|
13
|
+
def initialize(minValues, maxValues, sampler = LatinHypercubeSampler.new)
|
14
|
+
sampler.search_space = self
|
15
|
+
@sampler = sampler
|
12
16
|
# Check that we have valid min and max values
|
13
17
|
raise "Not same num of min values (#{minValues.length}) as there are max values (#{maxValues.length})" if minValues.length != maxValues.length
|
14
18
|
raise "A search space must have >= 1 variable to be searched, here you specified min values: #{minValues.inspect}" if minValues.length < 1
|
@@ -19,6 +23,15 @@ class FeldtRuby::Optimize::SearchSpace
|
|
19
23
|
@deltas = @min_values.zip(@max_values).map {|min,max| max-min}
|
20
24
|
end
|
21
25
|
|
26
|
+
# Generate a new candidate.
|
27
|
+
def gen_candidate
|
28
|
+
@sampler.sample_candidate
|
29
|
+
end
|
30
|
+
|
31
|
+
def gen_value_for_position(index)
|
32
|
+
@sampler.sample_value_for_dimension(index)
|
33
|
+
end
|
34
|
+
|
22
35
|
# Bound candidate using the min and max values. We randomly generate a new value inside the space
|
23
36
|
# for each element that is outside.
|
24
37
|
def bound(candidate)
|
@@ -48,13 +61,94 @@ class FeldtRuby::Optimize::SearchSpace
|
|
48
61
|
@min_values.length
|
49
62
|
end
|
50
63
|
|
51
|
-
|
52
|
-
|
64
|
+
# A sampler generates a new candidate (or set of candidates) that is (are) within a search space.
|
65
|
+
# This default sampler does uniform random sampling over the whole search space.
|
66
|
+
class Sampler
|
67
|
+
attr_accessor :search_space
|
68
|
+
|
69
|
+
def initialize(searchSpace = nil)
|
70
|
+
self.search_space = searchSpace
|
71
|
+
end
|
72
|
+
|
73
|
+
# Random uniform sampling of a valid value for a given dimension _index_ in the search space.
|
74
|
+
def sample_value_for_dimension(index)
|
75
|
+
min, delta = search_space.min_values[index], search_space.deltas[index]
|
76
|
+
min + delta * rand()
|
77
|
+
end
|
78
|
+
|
79
|
+
# Sample one candidate within the space. Default is to do random uniform sampling.
|
80
|
+
def sample_candidate
|
81
|
+
num_vars = search_space.num_variables
|
82
|
+
(0...num_vars).map {|i| sample_value_for_dimension(i)}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sample multiple candidates from the search space. The default is just to call the method sampling one
|
86
|
+
# candidate mutliple times. But subclasses can implement more sophisticated schemes.
|
87
|
+
def sample_candidates(numCandidates)
|
88
|
+
Array.new(numCandidates) { sample_candidate() }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Set samplers sample many candidates in one go, often to create a better "spread" of genotypes within
|
93
|
+
# the search space. This is an abstract base class so sub-classes ust override the sample_candidates method.
|
94
|
+
class SetSampler < Sampler
|
95
|
+
# The chunk size is the number of candidates that are generated in one go and from which the individual
|
96
|
+
# candidates are then taken. Default is 200 to get a nice initial spread.
|
97
|
+
def initialize(chunkSize = 200)
|
98
|
+
@chunk_size = chunkSize
|
99
|
+
@chunk = []
|
100
|
+
end
|
101
|
+
|
102
|
+
# Sample a set of candidates.
|
103
|
+
def sample_candidates(numCandidates)
|
104
|
+
raise NotImplementedError # Subclasses must implement this
|
105
|
+
end
|
106
|
+
|
107
|
+
def sample_candidate
|
108
|
+
sample_new_chunk() if chunk_empty?
|
109
|
+
pop_candidate_from_chunk()
|
110
|
+
end
|
111
|
+
|
112
|
+
def sample_new_chunk
|
113
|
+
@chunk = sample_candidates(@chunk_size)
|
114
|
+
end
|
115
|
+
|
116
|
+
def chunk_empty?
|
117
|
+
@chunk.nil? || @chunk.length < 1
|
118
|
+
end
|
119
|
+
|
120
|
+
def pop_candidate_from_chunk
|
121
|
+
@chunk.pop
|
122
|
+
end
|
53
123
|
end
|
54
124
|
|
55
|
-
|
56
|
-
|
57
|
-
|
125
|
+
# LatinHypercube sampling is a stratified random sampling in bins over the search space. It is a simple
|
126
|
+
# way to ensure a more even spread over the search space when sampling. It has shown to be generally
|
127
|
+
# useful for seeding initial populations in population based search without requiring any fitness
|
128
|
+
# evaluations. For example used in Deb's OmniOptimizer and in Tiwari's AMGA2 etc.
|
129
|
+
class LatinHypercubeSampler < SetSampler
|
130
|
+
# Sample the latin hypercube evenly for each dimension in the search space and then
|
131
|
+
# use Knuth unbiased shuffling to create individuals from the evenly spread out samples.
|
132
|
+
def sample_candidates(numCandidates)
|
133
|
+
set = Array.new(numCandidates).map {Array.new}
|
134
|
+
(0...(search_space.num_variables)).each do |dimension|
|
135
|
+
samples = latin_samples_for_dimension(dimension, numCandidates)
|
136
|
+
pi = (0...numCandidates).to_a.shuffle # Ruby has Knuth shuffle built in so no need to implement
|
137
|
+
(0...numCandidates).each {|i| set[i] << samples[pi[i]]}
|
138
|
+
end
|
139
|
+
set
|
140
|
+
end
|
141
|
+
|
142
|
+
# Evenly spread _numSamples_ random samples over the search space dimension with _index_.
|
143
|
+
def latin_samples_for_dimension(index, numSamples)
|
144
|
+
low, delta = search_space.min_values[index], search_space.deltas[index]
|
145
|
+
interval_size = delta / numSamples.to_f
|
146
|
+
i = 0
|
147
|
+
Array.new(numSamples).map do
|
148
|
+
i += 1
|
149
|
+
(low + (i-1) * interval_size) + (interval_size * rand())
|
150
|
+
end
|
151
|
+
end
|
58
152
|
end
|
59
153
|
|
60
154
|
def is_candidate?(c)
|
@@ -66,4 +160,6 @@ class FeldtRuby::Optimize::SearchSpace
|
|
66
160
|
end
|
67
161
|
end
|
68
162
|
|
69
|
-
|
163
|
+
DefaultSearchSpace = SearchSpace.new_symmetric(2, 1)
|
164
|
+
|
165
|
+
end
|