Top Level Namespace

Defined Under Namespace

Modules: Charlie, EdgeRecombinationCrossover, Enumerable, GABenchmark, GPTreeHelper, GladiatorialSelection, InsertionMutator, InversionMutator, NullCrossover, NullMutator, PermutationCrossover, RandomSelection, RotationMutator, RouletteSelection, SinglePointCrossover, TranspositionMutator, TreeChopMutator, TreeCrossover, TreeInsertNodeMutator, TreeRemoveNodeMutator, TreeTerminalMutator, UniformCrossover Classes: Array, Genotype, Module, Numeric, Population, String, Symbol, Table

Constant Summary collapse

BestOnlySelection =

This selection algorithm is basically randomized hill climbing.

TruncationSelection(1)
ScaledRouletteSelection =
ScaledRouletteSelection()
TournamentSelection =
TournamentSelection()
TreeReplaceMutator =
TreeReplaceMutator()
TreePruneMutator =
TreeReplaceMutator(0)
TreeNumTerminalMutator =
TreeNumTerminalMutator()
TreeEvalMutator =
TreeEvalMutator()
MutationStrategies =

The mutation strategies for ListMutator

  • :single_point : Mutates a single element

  • :n_point : Mutates a single element, n times. Example: :n_point, :n_point

  • :probability[ p=0.05 ] : Mutates each element with probability p

  • :expected_n : Mutates each element with probability n/genes.size, i.e. such that the expected # of mutations is n

{  # TODO: change to default parameters in 1.9
  :probability => Proc.new{|genes,pointmut,p| 
    p ||= 0.05
    genes.map!{|e| rand < p ? pointmut.call(e) : e }
  },
  :expected_n => Proc.new{|genes,pointmut,n|
    n ||= 3
    p   = n.to_f / genes.size
    genes.map!{|e| rand < p ? pointmut.call(e) : e }
  },
  :single_point => Proc.new{|genes,pointmut|
    i = genes.rand_index
    genes[i] = pointmut.call(genes[i])
  },
  :n_point => Proc.new{|genes,pointmut,n|
    n ||= 3
    n.times{ i = genes.rand_index; genes[i] = pointmut.call(genes[i]) }
  }
}
PointMutators =

The point mutators for ListMutator

  • :flip : flips bit (x->1-x), use in BitStringGenotype

  • :replace[ c1,c2,…,cn ] : replaces the element with one of the arguments. use in StringGenotype. Example: :replace[ *‘a’..‘z’ ]

  • :uniform[ max_size=0.25 ]: adds a random number in the range [-max_size,+max_size], uniformly distributed.

  • :gaussian[ sigma=0.2 ] : adds a random number, gaussian distributed with standard deviation sigma

{
  :replace => proc{|x,*pos| pos.at_rand },
  :flip    => proc{|x| 1-x },
  :uniform => Proc.new{|x,max_size| max_size ||= 0.25;  x - max_size + max_size * 2 * rand },
  :gaussian=> Proc.new{|x,sigma|
     sigma ||= 0.2;
     delta = Math.sqrt(-2 * Math.log(rand)) * Math.cos(2 * Math::PI * rand)  # Box-Muller transformation
     x + delta * sigma
   }
}

Instance Method Summary collapse

Instance Method Details

#BitStringGenotype(n) ⇒ Object

Genotype of n bits. Individuals are initialized as an array of n random bits.



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/charlie/list/list_genotype.rb', line 24

def BitStringGenotype(n)
 Class.new(Genotype) {
  define_method(:size){ n }
  def initialize
    @genes = Array.new(size){ rand(2) }
  end
  def to_s
    @genes.map(&:to_s).join
  end
  use ListMutator(:expected_n,:flip), SinglePointCrossover.dup
 }
end

#Elitism(sel_module, elite_n = 1) ⇒ Object

Generates a selection module with elitism from a normal selection module. Elitism is saving the best elite_n individuals each generation, to ensure the best solutions are never lost.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/charlie/1.9fixes.rb', line 6

def Elitism(sel_module,elite_n=1) # :nodoc:
 ng_name = sel_module.to_s.gsub(/[^A-Za-z0-9]/,'_')
 Module.new{
  include sel_module
  alias_method ng_name, :next_generation
  @@elite_n = elite_n
  define_method :next_generation, ->(population,&block){
    population = population.sort_by(&:fitness)
    best = population[-@@elite_n..-1]
    population = send(ng_name,population,&block)
    # reset old best elite_n, but don't overwrite better ones
    population[-@@elite_n..-1] = best.zip_with(population[-@@elite_n..-1]){|old,new| [old,new].max }
    population
  }
  self.name= "Elitism(#{sel_module.to_s},#{elite_n})"
 }
end

#FloatListGenotype(n, range = 0..1) ⇒ Object

Genotype of n floats in the range range. Individuals are initialized as an array of n random numbers in this range. Note that mutations may cross range min/max.



8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/charlie/list/list_genotype.rb', line 8

def FloatListGenotype(n,range=0..1)
 Class.new(Genotype) {
  @@range = range
  define_method(:size){ n }
  def initialize
    @genes = Array.new(size){ rand * (@@range.end - @@range.begin) + @@range.begin }
  end
  def to_s
    @genes.inspect
  end
  use ListMutator(), SinglePointCrossover.dup
 }
end

#ListMutator(strategy = :expected_n, point_mutator = :uniform) ⇒ Object

Generates a module which can be used as a mutator for array-based genotypes like FloatListGenotype, BitStringGenotype and StringGenotype

  • strategy should be one of the MutationStrategies, or a proc

  • point_mutator should be one of the PointMutators, or a proc

  • nil is equivalent to proc{} for the point mutator

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/charlie/list/list_mutate.rb', line 48

def ListMutator(strategy=:expected_n ,point_mutator=:uniform)
  strat, *strat_args = *strategy
  pm   , *pm_args    = *point_mutator

  pm ||= proc{}

  strat = MutationStrategies[strat.intern] unless strat.is_a? Proc
  pm    = PointMutators[pm.intern]         unless pm.is_a? Proc
  
  raise ArgumentError,"Invalid mutation strategy" if strat.nil?
  raise ArgumentError,"Invalid point mutator"     if point_mutator.nil?

  if pm_args.empty?
    point_mutator_with_args = pm    
  else
    point_mutator_with_args = proc{|*args| pm.call(*(args+pm_args) ) }
  end

  Module.new{
    define_method(:mutate!) {
      strat.call(@genes,point_mutator_with_args,*strat_args)
      self
    }
    self.name= "ListMutator(#{strategy.inspect},#{point_mutator.inspect})"
  }
end

#NPointCrossover(n = 2) ⇒ Object

n point crossover, returns two children.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/charlie/list/list_crossover.rb', line 14

def NPointCrossover(n=2)
 Module.new{
  self.name = "NPointCrossover(#{n})"
  define_method(:cross){|parent1,parent2|
    p1 = parent1.genes; p2 = parent2.genes
    upper_bnd = p1.size + 1
    cross_pts = (0...n).map{rand(upper_bnd)}.sort

    c1 = []; c2=[]
    ([0] + cross_pts << upper_bnd).each_cons(2){|cp1,cp2|
      c1 += p1[cp1...cp2]
      c2 += p2[cp1...cp2]
      p1,p2 = p2,p1
    }
    [c1,c2].map{|x| from_genes(x) }
  }
 }
end

#PCross(p, c1, c2 = NullCrossover) ⇒ Object

Takes crossover c1 with probability p, and crossover c2 with probability 1-p



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/charlie/crossover.rb', line 25

def PCross(p,c1,c2=NullCrossover)
  return c1 if c1==c2
  c1_name, c2_name = [c1,c2].map{|c| '_cross_' + c.to_s.gsub(/[^A-Za-z0-9]/,'') }
  Module.new{
    include c1.dup # dup to avoid bugs on use PCross(..,c1) .. use c1
    alias_method c1_name, :cross
    include c2.dup
    alias_method c2_name, :cross

    define_method(:cross) {|*args|
      rand < p ? send(c1_name,*args) : send(c2_name,*args)
    }
    self.name= "PCross(#{p},#{c1},#{c2})"
  }
end

#PCrossN(hash) ⇒ Object

Variant of PCross for more than 2 crossovers.

  • Pass a hash of Module=>probability pairs. If sum(probability) < 1, NullCrossover will be used for the remaining probability.

  • example: PCrossN(SinglePointCrossover=>0.33,UniformCrossover=>0.33) for NullCrossover/SinglePointCrossover/UniformCrossover all with probability 1/3



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/charlie/crossover.rb', line 44

def PCrossN(hash)
  tot_p = hash.inject(0){|s,(m,p)| s+p }
  if (tot_p - 1.0).abs > 0.01 # close to 1?
    raise ArgumentError, "PCrossN: sum of probabilities > 1.0" if tot_p > 1.0
    hash[NullCrossover] = (hash[NullCrossover] || 0.0) + (1.0 - tot_p)
  end
  partial_sums = hash.sort_by{|m,p| -p } # max probability first
  s = 0.0
  partial_sums.map!{|m,p| ['_cross_' + m.to_s.gsub(/[^A-Za-z0-9]/,'') , s+=p, m] }

  Module.new{
    partial_sums.each{|name,p,mod|
      include mod.dup
      alias_method name, :cross
    }
    define_method(:cross) {|*args|
      r = rand
      c_name = partial_sums.find{|name,p,mod| p >= r }.first
      send(c_name,*args)
    }
    self.name= "PCrossN(#{hash.inspect})"
  }
end

#PermutationGenotype(n, elements = 0...n) ⇒ Object

Generates a genotype class which represents a permutation of elements Includes the PermutationMutator and PermutationCrossover by default



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/charlie/permutation/permutation.rb', line 5

def PermutationGenotype(n,elements=0...n)
 elements = elements.chars if elements.is_a? String # string to array of chars
 elements = elements.to_a
 Class.new(Genotype) {
  define_method(:size)    { n }
  define_method(:elements){ elements }

  def initialize
    @genes = elements.dup.shuffle
  end

  def to_s
    @genes.inspect
  end
  use TranspositionMutator.dup , PCross(0.75,PermutationCrossover)
 }
end

#PMutate(p, m1, m2 = NullMutator) ⇒ Object

Takes mutator m1 with probability p, and mutator m2 with probability 1-p



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/charlie/mutate.rb', line 10

def PMutate(p,m1,m2=NullMutator)
  return m1 if m1==m2
  m1_name, m2_name = [m1,m2].map{|c| '_mutate_' + c.to_s.gsub(/[^A-Za-z0-9]/,'') + '!' }
  Module.new{
    include m1.dup # dup to avoid bugs on use PMutate(..,m1) .. use m1
    alias_method m1_name, :mutate!
    include m2.dup
    alias_method m2_name, :mutate!

    define_method(:mutate!) {
      rand < p ? send(m1_name) : send(m2_name)
    }
    self.name= "PMutate(#{p},#{m1},#{m2})"
  }
end

#PMutateN(hash) ⇒ Object

Variant of PMutate for more than 2 mutators

  • Pass a hash of Module=>probability pairs. If sum(probability) < 1, NullMutator will be used for the remaining probability.

  • example: PCrossN(SinglePointCrossover=>0.33,UniformCrossover=>0.33) for NullCrossover/SinglePointCrossover/UniformCrossover all with probability 1/3



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/charlie/mutate.rb', line 30

def PMutateN(hash)
  tot_p = hash.inject(0){|s,(m,p)| s+p }
  if (tot_p - 1.0).abs > 0.01 # close to 1?
    raise ArgumentError, "PMutateN: sum of probabilities > 1.0" if tot_p > 1.0
    hash[NullMutator] = (hash[NullMutator] || 0.0) + (1.0 - tot_p)
  end
  partial_sums = hash.sort_by{|m,p| -p } # max probability first
  s = 0.0
  partial_sums.map!{|m,p| ['_mutate_' + m.to_s.gsub(/[^A-Za-z0-9]/,'') + '!' , s+=p, m] }

  Module.new{
    partial_sums.each{|name,p,mod|
      include mod.dup
      alias_method name, :mutate!
    }
    define_method(:mutate!) {
      r = rand
      send partial_sums.find{|name,p,mod| p >= r }.first
    }
    self.name= "PMutateN(#{hash.inspect})"
  }
end

#ScaledRouletteSelection(&block) ⇒ Object

Scaled Roulette selection without replacement. Sorts by fitness, scales fitness by the values the given block yields for its index, then applies Roulette selection to the resulting fitness values. Default is Rank selection: {|x| x+1 }



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/charlie/selection.rb', line 66

def ScaledRouletteSelection(&block)
 Module.new{
  block = proc{|x|x+1} if block.nil?
  @@block = block
  @@index = nil
  @@index_size = 0 # population size used to generate index

  def next_generation(population)
 
   if @@index_size != population.size # build index, cache for constant population size
     @@index_size = population.size
     @@index = []
     (0...population.size).map(&@@block).each_with_index{|e,i| @@index += Array.new(e.round,i)  }
   end

    population = population.sort_by(&:fitness)
    new_pop = []
    while new_pop.size < population.size
      i1,i2 = @@index.at_rand, @@index.at_rand until i1!=i2 # no replacement
      new_pop += yield(population[i1],population[i2])
    end
    new_pop.pop until new_pop.size == population.size
    new_pop
  end

  self.name= "ScaledRouletteSelection[#{(0..3).map(&block).map(&:to_s).join(',')},...]"
 }
end

#SingleChild(crossover_module) ⇒ Object

Turns a two-children crossover module into a single-child crossover module. Usage: use SingleChild(UniformCrossover)



25
26
27
28
29
30
31
32
33
34
# File 'lib/charlie/1.9fixes.rb', line 25

def SingleChild(crossover_module) # :nodoc:
  crs_name =  '_cross_' + crossover_module.to_s.gsub(/[^A-Za-z0-9]/,'')
  Module.new{
    include crossover_module
    alias_method crs_name, :cross

    define_method(:cross){|*args| send(crs_name,*args).at_rand }
    self.name= "SingleChild(#{crossover_module})"
  }
end

#StringGenotype(n, elements) ⇒ Object

Genotype of n elements (not necessarily chars). Individuals are initialized as an array of n elements, each randomly chosen from the elements array.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/charlie/list/list_genotype.rb', line 39

def StringGenotype(n,elements)
 elements = elements.chars if elements.is_a? String # string to array of chars
 elements = elements.to_a
 Class.new(Genotype) {
  define_method(:size){ n }
  define_method(:elements){ elements }
  def initialize
    @genes = Array.new(size){ elements.at_rand }
  end
  def to_s
    @genes.map(&:to_s).join
  end
  use ListMutator(:expected_n[2],:replace[*elements]), SinglePointCrossover.dup
 }
end

#TournamentSelection(group_size = 4, n_times = nil) ⇒ Object

Tournament selection. Default: select the 2 individuals with the highest fitness out of a random population with size group_size and replaces the others with offspring of these 2. 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.



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/charlie/selection.rb', line 122

def TournamentSelection(group_size=4,n_times=nil) # TODO: maybe expand for probabilistic selection(?) Or in a new module
  Module::new{
    @@group_size = group_size
    @@n_times    = n_times
    def next_generation(population)
      @@n_times ||= population.size / (@@group_size-2)
      @@n_times.times{
        ix=[]
        begin
          ix = (0...@@group_size).map{ population.rand_index }
        end while ix.uniq.size != @@group_size
        ix=ix.sort_by{|i| population[i].fitness }
        p1,p2 = population[ix[-1]],population[ix[-2]]
        nw = []; 
        nw +=  yield(p1,p2) while nw.size < @@group_size-2
        (@@group_size-2).times{|i| population[ix[i]] = nw[i] }
      }
      population
    end
    self.name= "TournamentSelection(#{group_size},#{n_times.inspect})"
  }
end

#TreeEvalMutator(value_hash = Hash.new{0}) ⇒ Object

Replaces a random subtree by the result of its evaluation. value_hash is passed to eval_tree.



279
280
281
282
283
284
285
286
287
# File 'lib/charlie/tree/tree.rb', line 279

def TreeEvalMutator(value_hash=Hash.new{0})
 Module.new{
  define_method(:mutate!) {
    st = random_subtree
    st.replace [:term,eval_tree(st,value_hash)]
    self
  }
 }
end

#TreeGenotype(terminals, unary_ops, binary_ops, init_depth = 3, init_type = :half) ⇒ Object

Tree genotype, for genetic programming etc.

  • Pass arrays of terminals/unary operators/binary operators to this function to generate a class.

  • terminals can be procs (eval’d on initialization), symbols (replaced by values in calls to eval_genes) or anything else (not changed, so make sure all operators are defined for these)

  • This needs more options. Depth of initial trees, etc. Also needs a better mutator.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/charlie/tree/tree.rb', line 73

def TreeGenotype(terminals, unary_ops, binary_ops, init_depth = 3, init_type = :half)
  unary_ops  = nil if unary_ops.empty?
  Class.new(Genotype) {

    define_method(:unary_ops)  {unary_ops }
    define_method(:binary_ops) {binary_ops}
    define_method(:terminals)  {terminals }
    define_method(:init_depth) {init_depth}
    define_method(:init_type)  {init_type }

    def initialize
      self.genes = generate_random_tree(init_depth,init_type)
    end

    def genes=(g)
      class << g # ensures a genes.dup call is a deep copy
        def dup
          GPTreeHelper.dup_tree(self)
        end
      end
      @genes = g
    end

    def size
      tree_size(@genes)
    end

    def depth
      tree_depth(@genes)
    end


    # Generates a random tree.
    # * type = grow uses generate_random_tree_grow
    # * type = full uses generate_random_tree_full
    # * type = half uses one of them, with 50% probability each.
    def generate_random_tree(depth, type=:half)
      if type==:full || ( type == :half && rand < 0.5 )
        generate_random_tree_full(depth)
      else
        generate_random_tree_grow(depth,true)
      end
    end

    # Generates a random tree of a certain maximum depth.
    # * <tt>no_term=true</tt> makes sure the root node is a function.
    def generate_random_tree_grow(depth,no_term=nil)
      if depth.zero? || (rand(3).zero?  && !no_term)
        e = terminals.at_rand
        [:term, e.is_a?(Proc) ? e.call : e]
      else
        if unary_ops.nil? || rand(2).zero?
          [binary_ops.at_rand,generate_random_tree_grow(depth-1),generate_random_tree_grow(depth-1)]
        else
          [unary_ops.at_rand,generate_random_tree_grow(depth-1)]
        end
       end
    end

    # Generates a random tree, with all terminals at 'depth'.
    def generate_random_tree_full(depth)
      if depth.zero?
        e = terminals.at_rand
        [:term, e.is_a?(Proc) ? e.call : e]
      else
        if unary_ops.nil? || rand(2).zero?
          [binary_ops.at_rand,generate_random_tree_full(depth-1),generate_random_tree_full(depth-1)]
        else
          [unary_ops.at_rand,generate_random_tree_full(depth-1)]
        end
       end
    end

    def eval_genes(terminals_value_hash = {})
      eval_tree(@genes,terminals_value_hash)
    end

    def to_s
      @genes.inspect
    end

    use PCross(0.7,TreeCrossover), PMutate(0.5,TreeReplaceMutator) # TODO: test what options are best -- benchmark show that these are ok for simple settings.

    # make helper functions available at both class and instance level
    include GPTreeHelper
    class << self
      include GPTreeHelper
    end
  }
end

#TreeNumTerminalMutator(mutate = , &b) ⇒ Object

mutates a random numeric terminal using a point mutator (cf. ListMutator) or a block (e.g. {|x| x-rand+0.5}



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/charlie/tree/tree.rb', line 255

def TreeNumTerminalMutator(mutate=:uniform[0.1], &b)
 if block_given?
   mutate_proc = b
 else
  mut_name, *mut_arg = mutate
  mut_fn = PointMutators[mut_name]
  mutate_proc = proc{|x| mut_fn.call(x,*mut_arg) }
 end

 Module.new{
  self.name = "TreeNumTerminalMutator(#{mutate.inspect})"
  define_method(:mutate!) {
    numterms = all_terminals.select{|x| x[1].is_a? Numeric }
    unless numterms.empty?
      random_term = numterms.at_rand
      random_term[1] = mutate_proc.call(random_term[1])
    end
    self
  }
 }
end

#TreeReplaceMutator(depth = 2, type = :half) ⇒ Object

TreeReplaceMutator replaces a randomly chosen subtree with a new, randomly generated, subtree.

  • depth and type are arguments for TreeGenotype#generate_random_tree

  • depth == [1,2,3,..] or depth==(1..3) uses one of the elements in the range for the depth.

  • depth == :same to use the depth of the replaced subtree.

  • depth == :same for depth of the replaced subtree plus a random offset between min and max.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/charlie/tree/tree.rb', line 183

def TreeReplaceMutator(depth=2,type=:half)
  Module.new{
     self.name = "TreeReplaceMutator(#{depth.inspect},#{type.inspect})"
    if depth.is_a? Numeric
      define_method(:mutate!) {
        random_subtree.replace generate_random_tree(depth,type)
        self
      }
    elsif depth==:same || (depth.is_a?(Array) && depth[0]==:same)
      s, dd_min, dd_max = *depth
      possible_deltas = (dd_min||0..dd_max||0).to_a
      define_method(:mutate!) {
        st = random_subtree
        st.replace generate_random_tree([tree_depth(st) + possible_deltas.at_rand,0].max, type)
        self
      }
    elsif depth.respond_to?(:to_a)
      possible_depths = depth.to_a
      define_method(:mutate!) {
        random_subtree.replace generate_random_tree(possible_depths.at_rand,type)
        self
      }
    else
      raise ArgumentError, "invalid option for depth"
    end
  }
end

#TruncationSelection(best = 0.3) ⇒ Object

Truncation selection takes the best invididuals with the highest fitness and crosses them randomly to replace all the others. best can be an float < 1 (fraction of population size), or a number >=1 (number of individuals)



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/charlie/selection.rb', line 17

def TruncationSelection(best=0.3)
  Module.new{
    @@best = best
    def next_generation(population)
      k = if @@best >= 1  # best is an integer >= 1 -> select this much
            @@best.round
          else             # best is a float < 1 -> select this percentage
            [1,(@@best * population.size).round].max
          end
      n_new = population.size - k

      population = population.sort_by(&:fitness)
      best = population[-k..-1]
      new_pop = []
      new_pop += yield(best.at_rand,best.at_rand) while new_pop.size < n_new
      new_pop.pop until new_pop.size == n_new

      new_pop + best
    end
    self.name = "TruncationSelection(#{best})"
  }
end