charlie 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. data/History.txt +14 -0
  2. data/Manifest.txt +13 -22
  3. data/README.txt +3 -3
  4. data/Rakefile +1 -1
  5. data/TODO.txt +11 -21
  6. data/data/BENCHMARK +25 -23
  7. data/data/CROSSOVER +5 -1
  8. data/data/GENOTYPE +6 -6
  9. data/data/MUTATION +19 -7
  10. data/data/SELECTION +2 -1
  11. data/data/template.html +2 -1
  12. data/examples/EXAMPLES_README.txt +70 -0
  13. data/examples/bitstring.rb +72 -0
  14. data/examples/{gladiatorial_sunburn.rb → coevolution.rb} +80 -22
  15. data/examples/function_optimization.rb +113 -0
  16. data/examples/output/{royalroad1_report.html → bitstring_royalroad.html} +822 -655
  17. data/examples/output/function_optimization_sombrero.html +2289 -0
  18. data/examples/output/function_optimization_twopeak.csv +210 -0
  19. data/examples/output/function_optimization_twopeak.html +2477 -0
  20. data/examples/output/string_weasel.html +513 -0
  21. data/examples/output/tsp.html +633 -882
  22. data/examples/{money.rb → permutation.rb} +20 -8
  23. data/examples/string.rb +98 -0
  24. data/examples/tree.rb +37 -12
  25. data/examples/tsp.rb +34 -22
  26. data/lib/charlie.rb +5 -1
  27. data/lib/charlie/1.9fixes.rb +46 -0
  28. data/lib/charlie/crossover.rb +31 -14
  29. data/lib/charlie/etc/minireport.rb +5 -4
  30. data/lib/charlie/etc/monkey.rb +11 -8
  31. data/lib/charlie/gabenchmark.rb +230 -0
  32. data/lib/charlie/genotype.rb +4 -0
  33. data/lib/charlie/list/list_crossover.rb +25 -5
  34. data/lib/charlie/mutate.rb +34 -7
  35. data/lib/charlie/permutation/permutation.rb +34 -6
  36. data/lib/charlie/population.rb +12 -122
  37. data/lib/charlie/selection.rb +1 -0
  38. data/lib/charlie/tree/tree.rb +179 -17
  39. data/test/t_common.rb +1 -1
  40. data/test/test_benchmark.rb +19 -5
  41. data/test/test_cross.rb +23 -1
  42. data/test/test_evolve.rb +14 -1
  43. data/test/test_mutator.rb +28 -2
  44. data/test/test_permutation.rb +23 -1
  45. data/test/test_sel.rb +3 -1
  46. data/test/test_tree.rb +63 -1
  47. metadata +17 -25
  48. data/examples/bit.rb +0 -10
  49. data/examples/function_opt_2peak.rb +0 -24
  50. data/examples/function_opt_sombero.rb +0 -38
  51. data/examples/gladiatorial_simple.rb +0 -17
  52. data/examples/gridwalk.rb +0 -29
  53. data/examples/output/flattened_sombero.html +0 -6400
  54. data/examples/output/flattened_sombero2_.html +0 -3576
  55. data/examples/output/fopt1_dblopt.html +0 -2160
  56. data/examples/output/hill10.html +0 -5816
  57. data/examples/output/hill2.csv +0 -24
  58. data/examples/output/hill2.html +0 -384
  59. data/examples/output/royalroad2_report.html +0 -1076
  60. data/examples/output/royalroadquick_report.html +0 -504
  61. data/examples/output/weasel1_report.html +0 -1076
  62. data/examples/output/weasel2_report.html +0 -240
  63. data/examples/royalroad.rb +0 -26
  64. data/examples/royalroad2.rb +0 -18
  65. data/examples/simple_climb_hill2.rb +0 -47
  66. data/examples/weasel.rb +0 -36
@@ -24,6 +24,10 @@ module Enumerable
24
24
  zip(a2).map(&b)
25
25
  end
26
26
 
27
+ def sum
28
+ r=0; each{|e| r+=e }; r
29
+ end
30
+
27
31
  alias_method :enum_slice, :each_slice unless RUBY_VERSION < '1.9' # ruby1.9 replaces enum_* with each_*
28
32
  end
29
33
 
@@ -33,12 +37,10 @@ class Array
33
37
  sort_by{ rand }
34
38
  end if RUBY_VERSION < '1.9'
35
39
 
36
- def sum # TODO 1.9, use :+
37
- inject(0){|a,b|a+b}
38
- end
39
-
40
- def inner_product(v)
41
- zip_with(v){|a,b|a*b}.sum
40
+ def dot_product(v)
41
+ r=0.0
42
+ each_with_index{|e,i| r+=e*v[i] }
43
+ r
42
44
  end
43
45
 
44
46
  def rand_index
@@ -50,6 +52,7 @@ class Array
50
52
  end
51
53
 
52
54
  def stats # TODO 1.9, use minmax
55
+ return transpose.map(&:stats).transpose if at(0).is_a?(Array) # return stats of each component if elements are arrays
53
56
  [min,max,average,stddev]
54
57
  end
55
58
 
@@ -79,9 +82,9 @@ class String
79
82
  self[rand(size)]
80
83
  end
81
84
 
82
- def chars # TODO 1.9
85
+ def chars
83
86
  split('')
84
- end
87
+ end if RUBY_VERSION < '1.9'
85
88
 
86
89
  def each_char(&b)
87
90
  chars.each(&b)
@@ -0,0 +1,230 @@
1
+ require 'rbconfig' # for install name
2
+
3
+ module GABenchmark
4
+ extend self
5
+
6
+ # This method generates reports comparing several selection/crossover/mutation methods. Check the examples directory for several examples. See the BENCHMARK documentation file for more information.
7
+ def benchmark(genotype_class, html_outfile='report.html', csv_outfile=nil, &b)
8
+ start = Time.now
9
+
10
+ dsl_obj = StrategiesDSL.new; dsl_obj.instance_eval(&b)
11
+ all_tests = dsl_obj.get_tests
12
+ generations = dsl_obj.generations
13
+ population_size = dsl_obj.population_size
14
+ repeat_tests = dsl_obj.repeat
15
+
16
+ track_stat = dsl_obj.track_stat
17
+
18
+ n_tests = all_tests.size
19
+ tests_done = 0
20
+ puts "#{n_tests} Total tests:"
21
+
22
+ overall_best = [nil, -1.0 / 0.0]
23
+
24
+ data = all_tests.map{|selection_module,crossover_module,mutator_module|
25
+ tests_done += 1
26
+ print "\nRunning test #{tests_done}/#{n_tests} : #{selection_module} / #{crossover_module} / #{mutator_module}\t"
27
+
28
+ gclass = Class.new(genotype_class) { use selection_module,crossover_module,mutator_module }
29
+ start_test = Time.now
30
+
31
+ test_stats = (0...repeat_tests).map{
32
+ print '.'; $stdout.flush
33
+ best = Population.new(gclass,population_size).evolve_silent(generations).last
34
+ stat = track_stat.call(best)
35
+ overall_best = [best, stat] if overall_best[0].nil? || (overall_best[1] <=> stat) < 0 # use <=> to allow arrays
36
+ stat
37
+ }
38
+ [selection_module, crossover_module,mutator_module,
39
+ (Time.now-start_test) / repeat_tests, test_stats]
40
+ }
41
+
42
+ html_output(html_outfile, data, genotype_class, Time.now-start, overall_best, dsl_obj)
43
+ csv_output(csv_outfile , data)
44
+
45
+ puts '',table_details(data).to_s
46
+ return data
47
+ end
48
+
49
+
50
+ private
51
+
52
+ ST_HEADINGS = %w[min max avg stddev time]
53
+
54
+ def format_stat(s)
55
+ if s.is_a?(Array)
56
+ s.map{|x|format_stat(x)}.join("\r\n")
57
+ else
58
+ '%.5f' % s
59
+ end
60
+ end
61
+
62
+ def table_details(data)
63
+ tabledata = data.map{|s,c,m,t,a|
64
+ [s,c,m] + (a.stats << t).map{|f| format_stat(f)}
65
+ }.sort_by{|row| -row[-3].to_f } # sort by avg fitness. highest to lowest. to_f on multiple formatted returns first
66
+ return tabledata.to_table(%w[selection crossover mutation] + ST_HEADINGS)
67
+ end
68
+
69
+ def table_group(datasets,g1_name) # array of title, data rows
70
+ tabledata = datasets.map{|title,dataset|
71
+ joined = dataset.map(&:last).inject{|a,b|a+b}
72
+ avg_time = dataset.map{|r| r[-2] }.average
73
+ [title] + (joined.stats << avg_time).map{|f| format_stat(f)}
74
+ }.sort_by{|row| -row[-3].to_f } # sort by average fitness
75
+ return tabledata.to_table([g1_name]+ST_HEADINGS).to_html
76
+ end
77
+
78
+ def html_output(html_outfile, data, genotype_class, tot_time, overall_best, dsl_obj )
79
+ return unless html_outfile
80
+ # Generate HTML
81
+ html_tables = <<INFO
82
+ <h1>Information</h1>\n
83
+ <table>
84
+ <tr><th colspan=2>Version Info</th></tr>
85
+ <tr><td>Ruby Install Name</td><td>#{Config::CONFIG['ruby_install_name']}</td></tr>
86
+ <tr><td>Ruby Version</td><td>#{RUBY_VERSION}</td></tr>
87
+ <tr><td>Charlie Version</td><td>#{Charlie::VERSION}</td></tr>
88
+ <tr><th colspan=2>Benchmark Info</th></tr>
89
+ <tr><td>Genotype class</td><td>#{genotype_class}</td></tr>
90
+ <tr><td>Population size</td><td>#{dsl_obj.population_size}</td></tr>
91
+ <tr><td>Number of generations per run</td><td>#{dsl_obj.generations}</td></tr>
92
+ <tr><td>Number of tests </td><td>#{data.size}</td></tr>
93
+ <tr><td>Tests repeated </td><td>#{dsl_obj.repeat} times</td></tr>
94
+ <tr><td>Number of runs </td><td>#{data.size * dsl_obj.repeat}</td></tr>
95
+ <tr><td>Total number of generations </td><td>#{data.size * dsl_obj.repeat * dsl_obj.generations}</td></tr>
96
+ <tr><td>Total time</td><td>#{'%.2f' % tot_time} seconds</td></tr>
97
+ <tr><th colspan=2>Best Solution Info</th></tr>
98
+ <tr><td>Fitness</td><td>#{overall_best[1].inspect}</td></tr>
99
+ <tr><td>Solution</td><td><textarea rows=3 cols=40>#{overall_best[0].to_s}</textarea></td></tr>
100
+ </table>
101
+ INFO
102
+
103
+
104
+ # - Combined stats
105
+ html_tables << "<h1>Stats for all</h1>"
106
+ html_tables << table_group([["All",data]],'')
107
+ # - stats grouped by selection, crossover, mutation methods
108
+ ["selection","crossover","mutation"].each_with_index{|title,i|
109
+ html_tables << "<h1>Stats for #{title}</h1>"
110
+ html_tables << table_group(data.group_by{|x|x[i]}, title)
111
+ }
112
+ # - detailed stats
113
+ html_tables << '<h1>Detailed Stats</h1>' << table_details(data).to_html
114
+ # write HTML stats
115
+ File.open(html_outfile,'w'){|f|
116
+ template = File.read(File.dirname(__FILE__)+"/../../data/template.html")
117
+ f << template.sub('{{CONTENT}}',html_tables)
118
+ }
119
+ end
120
+
121
+ def csv_output(csv_outfile,data)
122
+ return unless csv_outfile
123
+ File.open(csv_outfile,'w'){|f|
124
+ f << data.map{|r|r[0..2] << r[-1].inspect }.to_table.to_csv
125
+ }
126
+ end
127
+
128
+
129
+ # Used in the GABenchmark#benchmark function.
130
+ class StrategiesDSL
131
+ class << self
132
+ def attr_dsl(x)
133
+ x = x.to_s
134
+ attr_accessor x
135
+ alias_method 'get_'+x, x # rename reader
136
+ define_method(x) {|*args| # reader with 0 args, write with 1 arg
137
+ return send('get_'+x) if args.empty?
138
+ args.size > 1 ? send(x+'=',args) : send(x+'=',*args)
139
+ }
140
+ end
141
+ end
142
+
143
+ # Number of generations run in each test.
144
+ attr_dsl :generations
145
+ # Population size used.
146
+ attr_dsl :population_size
147
+ # Number of times all tests are run. Default=10. Increase for more accuracy on the benchmark.
148
+ attr_dsl :repeat
149
+
150
+ # Pass several modules to this to test these selection methods.
151
+ attr_dsl :selection
152
+ # Pass several modules to this to test these crossover methods.
153
+ attr_dsl :crossover
154
+ # Pass several modules to this to test these mutation methods.
155
+ attr_dsl :mutator
156
+ alias :mutation :mutator
157
+ alias :mutation= :mutator=
158
+
159
+ def initialize
160
+ @repeat = 10
161
+ @population_size = 20
162
+ @generations = 50
163
+ selection []
164
+ crossover []
165
+ mutator []
166
+ track_stat{|best| best.fitness } # tracks maximum fitness by default
167
+ end
168
+
169
+ # Pass a block that returns one or more statistics to track. Block is passed the individual with the highest fitness after each run.
170
+ # * Can be used to track, for example, training error vs generalization error.
171
+ # * Default is fitness of the best solution.
172
+ # * When returning multiple values, <=> for arrays is used to determine the best individual in the info table (i.e. second elements only for tie-breaking), but min/max/avg/stddev stats are calculated independently for each component
173
+ def track_stat(&b)
174
+ return @track_stat unless block_given?
175
+ @track_stat = b
176
+ end
177
+ alias :track_stats :track_stat
178
+
179
+ # Get all the tests. Basically a cartesian product of all selection, crossover and mutation methods.
180
+ def get_tests
181
+ t = []
182
+
183
+ defmod = Module.new{self.name='default'}
184
+
185
+ selection = [@selection].flatten ; selection = [defmod] if selection.empty?
186
+ crossover = [@crossover].flatten ; crossover = [defmod] if crossover.empty?
187
+ mutator = [@mutator].flatten ; mutator = [defmod] if mutator.empty?
188
+
189
+ selection.each{|s|
190
+ crossover.each{|c|
191
+ mutator.each{|m|
192
+ t << [s,c,m]
193
+ }
194
+ }
195
+ }
196
+ t
197
+ end
198
+ end
199
+
200
+
201
+
202
+ end # GABenchmark
203
+
204
+
205
+
206
+
207
+
208
+ =begin
209
+ class RoyalRoad < BitStringGenotype(64) # Royal Road problem
210
+ def fitness
211
+ 1 + genes.enum_slice(8).find_all{|e| e.all?{|x|x==1} }.size # +1 to avoid all fitness 0 for roulette
212
+ end
213
+ cache_fitness
214
+ end
215
+
216
+ GABenchmark.benchmark(RoyalRoad,'test_bitstring_royalroad.html','o.csv'){
217
+ selection TruncationSelection(0.3),
218
+ Elitism(ScaledRouletteSelection)
219
+
220
+ crossover NullCrossover, UniformCrossover
221
+
222
+ mutator ListMutator(:expected_n[5],:flip)
223
+
224
+ generations 100
225
+ repeat 2 #
226
+ population_size 17
227
+
228
+ track_stat{|b| [b.fitness,b.genes.count{|x|x==1}] }
229
+ }
230
+ =end
@@ -51,6 +51,10 @@ class Genotype
51
51
  self
52
52
  end
53
53
  end
54
+
55
+ # Used by Genotype.cache_fitness. This accessor can be used to clear the cache.
56
+ # Also could be used by niche selection, etc. as a place to change the effective fitness w/o changing the actual selection algorithms.
57
+ attr_accessor :fitness_cache
54
58
 
55
59
  def dup
56
60
  self.class.from_genes(genes.dup)
@@ -1,4 +1,4 @@
1
- # List crossovers: SinglePointCrossover, UniformCrossover
1
+ # List crossovers: SinglePointCrossover, UniformCrossover, NPointCrossover
2
2
 
3
3
 
4
4
  # Simple single point crossover, returns two children.
@@ -10,15 +10,36 @@ module SinglePointCrossover
10
10
  end
11
11
  end
12
12
 
13
+ # n point crossover, returns two children.
14
+ def NPointCrossover(n=2)
15
+ Module.new{
16
+ self.name = "NPointCrossover(#{n})"
17
+ define_method(:cross){|parent1,parent2|
18
+ p1 = parent1.genes; p2 = parent2.genes
19
+ upper_bnd = p1.size + 1
20
+ cross_pts = (0...n).map{rand(upper_bnd)}.sort
21
+
22
+ c1 = []; c2=[]
23
+ ([0] + cross_pts << upper_bnd).each_cons(2){|cp1,cp2|
24
+ c1 += p1[cp1...cp2]
25
+ c2 += p2[cp1...cp2]
26
+ p1,p2 = p2,p1
27
+ }
28
+ [c1,c2].map{|x| from_genes(x) }
29
+ }
30
+ }
31
+ end
32
+
13
33
  # Uniform crossover, returns two children.
14
34
  module UniformCrossover
15
35
  def cross(parent1,parent2)
16
36
  c1 = []; c2=[]
17
- parent1.genes.zip(parent2.genes).each{|a,b|
37
+ g1 = parent1.genes; g2 = parent2.genes
38
+ g1.each_with_index{|e,i|
18
39
  if rand(2).zero?
19
- c1 << a; c2 << b
40
+ c1 << e; c2 << g2[i]
20
41
  else
21
- c2 << a; c1 << b
42
+ c2 << e; c1 << g2[i]
22
43
  end
23
44
  }
24
45
  [c1,c2].map{|x| from_genes(x) }
@@ -27,4 +48,3 @@ end
27
48
 
28
49
 
29
50
 
30
-
@@ -8,18 +8,45 @@ end
8
8
 
9
9
  # Takes mutator m1 with probability p, and mutator m2 with probability 1-p
10
10
  def PMutate(p,m1,m2=NullMutator)
11
+ return m1 if m1==m2
12
+ m1_name, m2_name = [m1,m2].map{|c| '_mutate_' + c.to_s.gsub(/[^A-Za-z0-9]/,'') + '!' }
11
13
  Module.new{
12
- @@p = p
13
- include m2
14
- alias :mutate2! :mutate!
15
- include m1
16
- def mutate!(*args)
17
- rand < @@p ? super(*args) : mutate2!(*args)
18
- end
14
+ include m1.dup # dup to avoid bugs on use PMutate(..,m1) .. use m1
15
+ alias_method m1_name, :mutate!
16
+ include m2.dup
17
+ alias_method m2_name, :mutate!
18
+
19
+ define_method(:mutate!) {
20
+ rand < p ? send(m1_name) : send(m2_name)
21
+ }
19
22
  self.name= "PMutate(#{p},#{m1},#{m2})"
20
23
  }
21
24
  end
22
25
 
23
26
 
27
+ # Variant of PMutate for more than 2 mutators
28
+ # * Pass a hash of Module=>probability pairs. If sum(probability) < 1, NullMutator will be used for the remaining probability.
29
+ # * example: PCrossN(SinglePointCrossover=>0.33,UniformCrossover=>0.33) for NullCrossover/SinglePointCrossover/UniformCrossover all with probability 1/3
30
+ def PMutateN(hash)
31
+ tot_p = hash.inject(0){|s,(m,p)| s+p }
32
+ if (tot_p - 1.0).abs > 0.01 # close to 1?
33
+ raise ArgumentError, "PMutateN: sum of probabilities > 1.0" if tot_p > 1.0
34
+ hash[NullMutator] = (hash[NullMutator] || 0.0) + (1.0 - tot_p)
35
+ end
36
+ partial_sums = hash.sort_by{|m,p| -p } # max probability first
37
+ s = 0.0
38
+ partial_sums.map!{|m,p| ['_mutate_' + m.to_s.gsub(/[^A-Za-z0-9]/,'') + '!' , s+=p, m] }
24
39
 
40
+ Module.new{
41
+ partial_sums.each{|name,p,mod|
42
+ include mod.dup
43
+ alias_method name, :mutate!
44
+ }
45
+ define_method(:mutate!) {
46
+ r = rand
47
+ send partial_sums.find{|name,p,mod| p >= r }.first
48
+ }
49
+ self.name= "PMutateN(#{hash.inspect})"
50
+ }
51
+ end
25
52
 
@@ -16,12 +16,12 @@ def PermutationGenotype(n,elements=0...n)
16
16
  def to_s
17
17
  @genes.inspect
18
18
  end
19
- use PermutationMutator.dup , PermutationCrossover.dup
19
+ use TranspositionMutator.dup , PCross(0.75,PermutationCrossover)
20
20
  }
21
21
  end
22
22
 
23
- # Transposition mutator for PermutationGenotype
24
- module PermutationMutator
23
+ # Transposition mutator for PermutationGenotype. Interchanges two elements and leaves the remaining elements in their original positions.
24
+ module TranspositionMutator
25
25
  # Transposes two elements
26
26
  def mutate!
27
27
  i1, i2 = @genes.rand_index,@genes.rand_index
@@ -30,7 +30,7 @@ module PermutationMutator
30
30
  end
31
31
  end
32
32
 
33
- # Inversion mutator for PermutationGenotype. May work on other array/string-based genotypes as well, but this is untested.
33
+ # Inversion mutator for PermutationGenotype.
34
34
  # Takes two random indices, and reverses the elements in between (includes possible wrapping if index2 < index1)
35
35
  module InversionMutator
36
36
  # Inverts parts of the genes
@@ -48,8 +48,36 @@ module InversionMutator
48
48
  end
49
49
 
50
50
 
51
- # One point partial preservation crossover for PermutationGenotype
52
- # Child 1 is identical to parent 1 up to the cross point, and contains the remaining elements in the same order as parent 2.
51
+ # Takes a random element of the permutation, and inserts it at a random position.
52
+ # * Example: [1 2 3 4 5] to [1 4 2 3 5]
53
+ module InsertionMutator
54
+ def mutate!
55
+ from, to = @genes.rand_index, @genes.rand_index
56
+ @genes = if to >= from
57
+ to += 1 # add end of array as possibility
58
+ (@genes[0...from] + @genes[from+1...to] << @genes[from]) + @genes[to..-1]
59
+ else
60
+ (@genes[0...to] << @genes[from]) + @genes[to...from] + @genes[from+1..-1]
61
+ end
62
+ self
63
+ end
64
+ end
65
+
66
+
67
+ # Rotates the representation of the permutation (i.e. effectively does nothing if it represents a cycle)
68
+ # * Example: [1 2 3 4] to [3 4 1 2]
69
+ module RotationMutator
70
+ def mutate!
71
+ new_start = @genes.rand_index
72
+ @genes = @genes[new_start..-1] + @genes[0...new_start]
73
+ self
74
+ end
75
+ end
76
+
77
+
78
+ # * One point partial preservation crossover for PermutationGenotype
79
+ # * Also known as partial recombination crossover (PRX).
80
+ # * Child 1 is identical to parent 1 up to the cross point, and contains the remaining elements in the same order as parent 2.
53
81
  module PermutationCrossover
54
82
  def cross(parent1,parent2)
55
83
  p1, p2 = parent1.genes, parent2.genes