charlie 0.7.1 → 0.8.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.
@@ -8,6 +8,7 @@ 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
+ raise ArgumentError, "first argument to PMutate should be numeric (probability)." unless p.is_a?(Numeric)
11
12
  return m1 if m1==m2
12
13
  m1_name, m2_name = [m1,m2].map{|c| '_mutate_' + c.to_s.gsub(/[^A-Za-z0-9]/,'') + '!' }
13
14
  Module.new{
@@ -3,34 +3,34 @@
3
3
 
4
4
  # The population class represents an array of genotypes.
5
5
  # Create an instance of this, and call one of the evolve functions to run the genetic algorithm.
6
- class Population
6
+ class Population < Array
7
7
  DEFAULT_MAX_GENS = 100
8
-
9
- attr_reader :size, :population, :genotype_class
10
- def initialize(genotype_class,population_size=20)
11
- @size = population_size
8
+ DEFAULT_POP_SIZE = 20
9
+ attr_reader :genotype_class
10
+ def initialize(genotype_class,population_size=DEFAULT_POP_SIZE)
12
11
  @genotype_class = genotype_class
13
- @population = Array.new(population_size){ genotype_class.new }
12
+ replace Array.new(population_size){ genotype_class.new }
14
13
  end
15
14
 
16
15
  # yields population and generation number to block each generation, for a maximum of max_generations.
17
16
  def evolve_block(max_generations=DEFAULT_MAX_GENS)
18
- yield @population, 0
17
+ yield self, 0
19
18
  (max_generations || DEFAULT_MAX_GENS).times {|generation|
20
- @population = @genotype_class.next_generation(@population){|*parents|
19
+ self.population = @genotype_class.next_generation(self){|*parents|
21
20
  ch = [*@genotype_class.cross(*parents)]
22
21
  ch.each{|c| c.mutate! }
23
22
  ch
24
23
  }
25
- yield @population, generation+1
24
+ yield self, generation+1
26
25
  }
27
- @population
26
+ self
28
27
  end
29
28
 
30
29
  # Runs the genetic algorithm without any output. Returns the population sorted by fitness (unsorted for co-evolution).
31
30
  def evolve_silent(generations=DEFAULT_MAX_GENS)
32
31
  evolve_block(generations){}
33
- @population.sort_by{|x|x.fitness} rescue @population
32
+ sort_by!{|x|x.fitness} rescue nil
33
+ self
34
34
  end
35
35
 
36
36
  # Runs the genetic algorithm with some stats on the console. Returns the population sorted by fitness.
@@ -40,9 +40,9 @@ class Population
40
40
  best = p.max
41
41
  puts "#{g}\t\t#{best.fitness}\t\t#{best.to_s}"
42
42
  }
43
- @population = @population.sort_by{|x|x.fitness}
44
- puts "Finished: Best fitness = #{@population[-1].fitness}"
45
- @population
43
+ self.population = self.sort_by{|x|x.fitness}
44
+ puts "Finished: Best fitness = #{self[-1].fitness}"
45
+ self
46
46
  end
47
47
  alias :evolve :evolve_on_console
48
48
 
@@ -56,7 +56,7 @@ class Population
56
56
  break
57
57
  end
58
58
  }
59
- [@population, tot_gens]
59
+ [self, tot_gens]
60
60
  end
61
61
 
62
62
  # breaks if the block (which is passed the best individual each "check_every" generations) returns true.
@@ -69,16 +69,79 @@ class Population
69
69
  break
70
70
  end
71
71
  }
72
- [@population, tot_gens]
72
+ [self, tot_gens]
73
73
  end
74
74
  alias :evolve_until :evolve_until_best
75
+ alias :best :max
75
76
 
77
+ # backwards compatibility, returns self
78
+ def population; self; end
79
+ # backwards compatibility, replaces population array
80
+ alias :population= :replace
76
81
 
77
- # accessors for population
78
- [:[],:max,:min].each{|m|
79
- define_method(m){|*args| population.send(m,*args) }
80
- }
81
- alias :best :max
82
82
 
83
+ # Effectively runs Population#evolve_block <tt>n_runs</tt> times.
84
+ # Returns the entire population of all results (<tt>n_runs * population_size</tt> elements)
85
+ def self.evolve_multiple(genotype_class,population_size=DEFAULT_POP_SIZE,
86
+ n_runs=25,
87
+ generations=DEFAULT_MAX_GENS)
88
+ r = evolve_multiple_until_population(genotype_class,population_size, n_runs, generations, 10000) {|pop| false }
89
+ r.first
90
+ end
91
+
92
+ # Effectively runs Population#evolve_until_best multiple times.
93
+ # * See Population.evolve_multiple_until_population for arguments
94
+ def self.evolve_multiple_until_best(*args,&b)
95
+ evolve_multiple_until_population(*args) {|pop| b.call(pop.max) }
96
+ end
97
+
98
+ # Runs Population#evolve_until_population multiple times.
99
+ # * genotype_class,population_size are arguments for Population.new
100
+ # * max_tries is the maximum number of restarts.
101
+ # * Returns [population, generations needed] on success where population is the population of the successful run.
102
+ # * Returns [population_all, nil] on failure, where population_all is the combined population of all runs.
103
+ def self.evolve_multiple_until_population(genotype_class,population_size=DEFAULT_POP_SIZE,
104
+ max_tries=25,
105
+ generations=DEFAULT_MAX_GENS,check_every=10)
106
+ tot_gens = 0
107
+ all_pop = []
108
+ max_tries.times{
109
+ pop, gens = Population.new(genotype_class,population_size).evolve_until_population(generations,check_every){|p|
110
+ yield(p)
111
+ }
112
+ all_pop += pop
113
+ tot_gens += gens || generations
114
+ if gens
115
+ return [pop, tot_gens]
116
+ end
117
+ }
118
+ [all_pop, nil]
119
+ end
120
+ class << self; alias :evolve_multiple_until :evolve_multiple_until_best; end
121
+
122
+ end
123
+
124
+
125
+ # Uses GP-style evolution (i.e. crossover OR mutation instead of crossover AND mutation) with a selection method.
126
+ # * Works by discarding and replacing the block given to next_generation in Population#evolve_block
127
+ # * Is not mandatory for GP, and can just as easily be used for GA or not used in GP.
128
+ # * Bit of a strange place to do this, but works nicely with the benchmarking
129
+ def GP(sel_module,crossover_probability=0.75)
130
+ ng_name = sel_module.to_s.gsub(/[^A-Za-z0-9]/,'_')
131
+ Module.new{
132
+ include sel_module.dup
133
+ alias_method ng_name, :next_generation
134
+ define_method(:next_generation){|population|
135
+ send(ng_name,population){|*parents|
136
+ if rand < crossover_probability
137
+ [*parents[0].class.cross(*parents)]
138
+ else
139
+ parents.map{|c| c.mutate }
140
+ end
141
+ }
142
+ }
143
+ self.name= "GP(#{sel_module.to_s},#{crossover_probability})"
144
+ }
83
145
  end
84
146
 
147
+
@@ -37,28 +37,44 @@ def TruncationSelection(best=0.3)
37
37
  }
38
38
  end
39
39
 
40
+ TruncationSelection = TruncationSelection()
41
+
40
42
  # This selection algorithm is basically randomized hill climbing.
41
43
  BestOnlySelection = TruncationSelection(1)
42
44
 
45
+
43
46
  # Roulette selection without replacement. Probability of individual i being selected is fitness(i) / sum fitness(1..population size)
44
47
  module RouletteSelection
45
48
  def next_generation(population)
46
49
  partial_sum = []
47
- sum = population.inject(0){|a,b| cs = a + b.fitness; partial_sum << cs; cs }
50
+ sum = 0
51
+ population.each{|e| partial_sum << (sum += e.fitness) }
48
52
 
53
+ n = population.size
49
54
  new_pop = []
50
- while new_pop.size < population.size
51
- i1 = i2 = nil
52
- i1,i2 = [0,0].map{
53
- r = rand * sum
54
- partial_sum.index partial_sum.find{|x| x > r }
55
- } until i1!=i2 # no replacement, except when this fails
56
- new_pop += yield(population[i1],population[i2])
55
+ i = [0,0]
56
+ while new_pop.size < n
57
+ i[0] = i[1]
58
+ until i[0]!=i[1]
59
+ i.map!{ # binary search
60
+ r = rand * sum
61
+ l = 0; u = n-1;
62
+ while l!=u
63
+ m = (l+u)/2
64
+ if partial_sum[m] < r
65
+ l = m+1
66
+ else
67
+ u = m
68
+ end
69
+ end
70
+ l
71
+ }
72
+ end
73
+ new_pop += yield(population[i[0]],population[i[1]])
57
74
  end
58
75
  new_pop.pop until new_pop.size == population.size
59
76
  new_pop
60
77
  end
61
-
62
78
  end
63
79
 
64
80
  # Scaled Roulette selection without replacement.
@@ -78,16 +94,20 @@ def ScaledRouletteSelection(&block)
78
94
  @@index = []
79
95
  (0...population.size).map(&@@block).each_with_index{|e,i| @@index += Array.new(e.round,i) }
80
96
  end
81
-
82
- population = population.sort_by(&:fitness)
83
- new_pop = []
84
- while new_pop.size < population.size
85
- i1 = i2 = nil
86
- i1,i2 = @@index.at_rand, @@index.at_rand until i1!=i2 # no replacement
87
- new_pop += yield(population[i1],population[i2])
88
- end
89
- new_pop.pop until new_pop.size == population.size
90
- new_pop
97
+ population = population.sort_by(&:fitness)
98
+
99
+ new_pop = []
100
+ index = @@index
101
+ while new_pop.size < population.size
102
+ i1 = index.at_rand
103
+ i2 = index.at_rand
104
+ if i1==i2
105
+ i1,i2 = @@index.at_rand, @@index.at_rand until i1!=i2 # no replacement
106
+ end
107
+ new_pop += yield(population[i1],population[i2])
108
+ end
109
+ new_pop.pop until new_pop.size == population.size
110
+ new_pop
91
111
  end
92
112
 
93
113
  self.name= "ScaledRouletteSelection[#{(0..3).map(&block).map(&:to_s).join(',')},...]"
@@ -95,7 +115,7 @@ def ScaledRouletteSelection(&block)
95
115
  end
96
116
 
97
117
  ScaledRouletteSelection = ScaledRouletteSelection()
98
-
118
+ RankSelection = ScaledRouletteSelection()
99
119
 
100
120
  # Generates a selection module with elitism from a normal selection module.
101
121
  # Elitism is saving the best +elite_n+ individuals each generation, to ensure the best solutions are never lost.
@@ -121,18 +141,16 @@ end
121
141
  # Default: select the 2 individuals with the highest fitness out of a random population with size group_size
122
142
  # and replaces the others with offspring of these 2.
123
143
  # Does this n_times. n_times==nil takes population size / (group_size-2) , i.e. about the same number of new individuals as roulette selection etc.
124
- def TournamentSelection(group_size=4,n_times=nil) # TODO: maybe expand for probabilistic selection(?) Or in a new module
144
+ def TournamentSelection(group_size=4,n_times=nil)
125
145
  Module::new{
126
146
  @@group_size = group_size
127
147
  @@n_times = n_times
128
148
  def next_generation(population)
129
- @@n_times ||= population.size / (@@group_size-2)
130
- @@n_times.times{
131
- ix=[]
132
- begin
133
- ix = (0...@@group_size).map{ population.rand_index }
134
- end while ix.uniq.size != @@group_size
135
- ix=ix.sort_by{|i| population[i].fitness }
149
+ psz = population.size
150
+ n_times = @@n_times || (psz / (@@group_size-2))
151
+ n_times.times{
152
+ population.shuffle!
153
+ ix = (0...@@group_size).sort_by{|i| population[i].fitness }
136
154
  p1,p2 = population[ix[-1]],population[ix[-2]]
137
155
  nw = [];
138
156
  nw += yield(p1,p2) while nw.size < @@group_size-2
@@ -146,8 +164,7 @@ end
146
164
  TournamentSelection = TournamentSelection()
147
165
 
148
166
 
149
-
150
- # Direct competition (gladiatorial) selection
167
+ # Co-evolution: Direct competition (gladiatorial) selection. Define a Genotype#fight function to use this
151
168
  module GladiatorialSelection
152
169
  def next_generation(population)
153
170
  (population.size/2).times{
@@ -163,3 +180,44 @@ module GladiatorialSelection
163
180
  end
164
181
  end
165
182
 
183
+ # Co-evolution: competition in tournaments selection.
184
+ # * Define a fight_points(other) function in your genotype to use this. The function should return [points for self,points for other]
185
+ # * Point-proportial selection is used within tournaments. Entire groups are replaced.
186
+ # * full_tournament = true calls both fight_points(population[i],population[j]) AND fight_points(population[j],population[i]) instead of only i < j
187
+ # Does this n_times. n_times==nil takes population size / group_size , i.e. about the same number of new individuals as roulette selection etc.
188
+ def CoTournamentSelection(group_size=4,full_tournament=false,n_times=nil)
189
+ Module::new{
190
+ @@group_size = group_size
191
+ @@full_tournament = full_tournament
192
+ @@n_times = n_times
193
+ def next_generation(population)
194
+ psz = population.size
195
+ n_times = @@n_times || (psz / @@group_size) #(psz / (@@group_size-2))
196
+ n_times.times{
197
+ population.shuffle!
198
+ points = Array.new(@@group_size,0.0)
199
+ for i in 0...@@group_size
200
+ for j in 0...@@group_size
201
+ next if j==i || (i > j && !@@full_tournament)
202
+ r = population[i].fight_points(population[j])
203
+ points[i] += r[0]
204
+ points[j] += r[1]
205
+ end
206
+ end
207
+ # points-proportional selection, with replacement b.c. 1 individual with 100% of the points is not implausible.
208
+
209
+ partial_sum = []; sum = 0; points.each{|p| partial_sum << (sum += p) }
210
+ partial_sum.map!{|x|x+1} if sum.abs < 1e-10 # sum==0 -> random
211
+ newgroup = [];
212
+ (@@group_size / 2.0).ceil.times{
213
+ sel_ix = [0,0].map{ r=rand*sum; partial_sum.find_index{|ps| ps >= r } }
214
+ newgroup += yield(population[sel_ix[0]],population[sel_ix[1]])
215
+ }
216
+ population[0...@@group_size] = newgroup[0...@@group_size]
217
+ }
218
+ population
219
+ end
220
+ self.name= "CoTournamentSelection(#{group_size},#{full_tournament},#{n_times.inspect})"
221
+ }
222
+ end
223
+ CoTournamentSelection = CoTournamentSelection()
@@ -278,6 +278,7 @@ TreeNumTerminalMutator = TreeNumTerminalMutator()
278
278
  # Replaces a random subtree by the result of its evaluation. value_hash is passed to eval_tree.
279
279
  def TreeEvalMutator(value_hash=Hash.new{0})
280
280
  Module.new{
281
+ self.name = "TreeEvalMutator(#{value_hash.inspect})"
281
282
  define_method(:mutate!) {
282
283
  st = random_subtree
283
284
  st.replace [:term,eval_tree(st,value_hash)]
@@ -3,7 +3,7 @@
3
3
  require File.dirname(__FILE__) + '/../lib/charlie' unless Object.const_defined?('Charlie')
4
4
  require 'test/unit'
5
5
 
6
- $crs_mth = [NullCrossover, SinglePointCrossover, UniformCrossover, NPointCrossover(1), NPointCrossover(2), NPointCrossover(24)]
6
+ $crs_mth = [NullCrossover, SinglePointCrossover, UniformCrossover, NPointCrossover(1), NPointCrossover(2), NPointCrossover(24),BlendingCrossover(0.1,:line),BlendingCrossover(1.3,:cube)]
7
7
 
8
8
 
9
9
  class TestProblem < FloatListGenotype(2,0..1)
@@ -69,4 +69,22 @@ class BMTest < Test::Unit::TestCase
69
69
  assert_equal 11,d[0][-1].size
70
70
  assert d.all?{|r| r[-1].all?{|e| e.size==2 } }
71
71
  end
72
+
73
+
74
+ def test_setup_teardown
75
+ d = nil
76
+ s = t = 0 # count setup/teardown calls/args
77
+ assert_nothing_raised {
78
+ d = GABenchmark.benchmark(StringA,nil,nil){
79
+ selection TruncationSelection, TournamentSelection
80
+ setup{ s+=1 }
81
+ teardown{|p| t += p.size }
82
+ population_size 7
83
+ repeat 10
84
+ }
85
+ }
86
+ assert_equal 2, d.size
87
+ assert_equal 10 * 7 * 2,t
88
+ assert_equal 10 * 2, s
89
+ end
72
90
  end
@@ -8,6 +8,26 @@ class TestCrossover < Test::Unit::TestCase
8
8
  }
9
9
  end
10
10
 
11
+ def test_blend
12
+ klass = FloatListGenotype(100)
13
+ klass.use BlendingCrossover(0.0)
14
+ p = Array.new(2){klass.new}
15
+ p[0].genes.map!{0.0}
16
+ p[1].genes.map!{1.0}
17
+ 10.times{
18
+ ch = klass.cross(p[0],p[1])
19
+ assert ch.all?{|c| c.genes.all?{|g| g > 0.0 && g < 1.0 } }
20
+ }
21
+ assert p[0].genes.all?{|g|g==0.0}
22
+ assert p[1].genes.all?{|g|g==1.0}
23
+ klass.use BlendingCrossover(0.0,:line)
24
+ 3.times{
25
+ ch = klass.cross(p[0],p[1])
26
+ assert ch.all?{|c| gn = c.genes; gn[0] > 0.0 && gn[0] < 1.0 && gn.all?{|g| g==gn[0] } }
27
+ }
28
+ assert_raises(ArgumentError) { BlendingCrossover(0.0,:foo) }
29
+ end
30
+
11
31
  def test_singlechild
12
32
  $crs_mth.map{|c| SingleChild(c) }.each{|s|
13
33
  klass = TestClass(s)
@@ -91,10 +91,34 @@ class EvolveTest < Test::Unit::TestCase
91
91
  end
92
92
 
93
93
 
94
- def test_acc # test accessors
94
+ def test_acc # test accessors. backward compatibility
95
95
  p = Population.new(RRTest,10)
96
96
  assert_equal p.max, p.population.max
97
97
  assert_equal p.max, p.best
98
98
  assert_equal p[6], p.population[6]
99
99
  end
100
+
101
+
102
+
103
+ def test_multiple_until_best # test evolve_multiple_until
104
+ r,g = Population.evolve_multiple_until(RRTest,5,5){false}
105
+ assert_equal 5*5, r.size
106
+ assert_nil g
107
+
108
+ r,g = Population.evolve_multiple_until(RRTest,10,500){|b| b.fitness == 0}
109
+ assert_equal 10, r.size
110
+ assert_not_nil g
111
+ assert_equal r.max.fitness, 0 # actually converged
112
+ assert_respond_to r, :[]
113
+ assert_respond_to r[-1], :fitness
114
+ end
115
+
116
+
117
+ def test_multiple # test evolve_multiple_until
118
+ r = Population.evolve_multiple(RRTest,5,5)
119
+ assert_equal 5*5, r.size
120
+ assert_respond_to r, :[]
121
+ assert_respond_to r[-1], :fitness
122
+ end
123
+
100
124
  end
@@ -0,0 +1,36 @@
1
+ require 't_common'
2
+
3
+ class BMatrixTest < BitMatrixGenotype(16,4)
4
+ def fitness
5
+ genes.flatten.count{|b| b==1 }
6
+ end
7
+ end
8
+
9
+ class MatrixTests < Test::Unit::TestCase
10
+ def test_evolve
11
+ p=nil
12
+ assert_nothing_raised{
13
+ p=Population.new(BMatrixTest,20).evolve_silent(20)
14
+ }
15
+ assert p.all?{|s| s.fitness > 32 }, "Insufficient fitness gain, can be extremely bad luck but probably a sign of a bug in the dup code"
16
+ p.each{|s|
17
+ assert s.genes.is_a?(Array) && s.genes.size == 16
18
+ assert s.genes[0].is_a?(Array) && s.genes.all?{|a| a.size==4 }
19
+ }
20
+ end
21
+
22
+ def test_listxover
23
+ p=nil
24
+ klass = Class.new(BMatrixTest) {use NPointCrossover(3) }
25
+ assert_nothing_raised{
26
+ p=Population.new(klass,20).evolve_silent(20)
27
+ }
28
+ assert p.all?{|s| s.fitness > 32 }, "Insufficient fitness gain, can be extremely bad luck but probably a sign of a bug in the dup code"
29
+ p.each{|s|
30
+ assert s.genes.is_a?(Array) && s.genes.size == 16
31
+ assert s.genes[0].is_a?(Array) && s.genes.all?{|a| a.size==4 }
32
+ }
33
+
34
+ end
35
+
36
+ end