class NEAT::Evolver

Evolver – Basis of all evolvers.

All evolvers shall derive from this basic evolver (or this one can be
used as is). Here, we'll have many different evolutionary operators
that will perform operations on the various critters in the population.

Attributes

npop[R]

Public Class Methods

new(c) click to toggle source
Calls superclass method
# File lib/rubyneat/evolver.rb, line 13
def initialize(c)
  super
  @critter_op = CritterOp.new self
end

Public Instance Methods

evolve(population) click to toggle source

Here we clone the population and then evolve it on the basis of fitness and novelty, etc.

Returns the newly-evolved population.

# File lib/rubyneat/evolver.rb, line 53
def evolve(population)
  @npop = population.dclone
  
  # Population sorting and evaluation for breeding, mutations, etc.
  prepare_speciation!
  prepare_fitness!
  prepare_novelty!

  mate!

  return @npop
end
gen_initial_genes!(genotype) click to toggle source

Generate the initial genes for a given genotype. We key genes off their innovation numbers.

# File lib/rubyneat/evolver.rb, line 20
def gen_initial_genes!(genotype)
  genotype.genes = {}
  genotype.neural_inputs.each do |s1, input|
    genotype.neural_outputs.each do |s2, output|
      g = Critter::Genotype::Gene[genotype, input, output, NEAT::controller.gaussian]
      genotype.genes[g.innovation] = g
    end
  end
end
mutate!(population) click to toggle source

Here we mutate the population.

# File lib/rubyneat/evolver.rb, line 31
def mutate!(population)
  @npop = population

  if @controller.parms.mate_only_prob.nil? or rand > @controller.parms.mate_only_prob
    log.debug "[[[ Neuron and Gene Giggling!"
    mutate_perturb_gene_weights!
    mutate_change_gene_weights!
    mutate_add_neurons!
    mutate_change_neurons!
    mutate_add_genes!
    mutate_disable_genes!
    mutate_reenable_genes!
    log.debug "]]] End Neuron and Gene Giggling!\n"
  else
    log.debug "*** Mating only!"
  end
end

Protected Instance Methods

mate!() click to toggle source

Here we select candidates for mating. We must look at species and fitness to make the selection for mating.

# File lib/rubyneat/evolver.rb, line 167
def mate!
  parm = @controller.parms
  popsize = parm.population_size
  surv = parm.survival_threshold
  survmin = parm.survival_mininum_per_species
  mlist = [] # list of chosen mating pairs of critters [crit1, crit2], or [:carryover, crit]

  # species list already sorted in descending order of fitness.
  # We will generate the approximate number of  pairs that correspond
  # to the survivial_threshold percentage of the population,
  # then backfill with the most fit out of the top original population.
  @npop.species.each do |k, sp|
    crem = [(sp.size * surv).ceil, survmin].max
    log.warn "Minumum per species hit -- #{survmin}" unless crem > survmin
    spsel = sp[0, crem]
    spsel = sp if spsel.empty?
    crem.times do
      mlist << [spsel[rand spsel.size], spsel[rand spsel.size]]
    end
  end

  # And now for the backfilling
  unless mlist.size >= @npop.critters.size
    mlist += @npop.critters[0, @npop.critters.size - mlist.size].map{|crit| [:carryover, crit]}
  end

  @npop.critters = mlist.map do |crit1, crit2|
    (crit1 == :carryover) ? crit2 : sex(crit1, crit2)
  end
end
mutate_add_genes!() click to toggle source
# File lib/rubyneat/evolver.rb, line 124
def mutate_add_genes!
  @npop.critters.each do |critter|
    if rand < @controller.parms.mutate_add_gene_prob
      log.debug "mutate_add_genes! for #{critter}"
      @critter_op.add_gene! critter
    end
  end
end
mutate_add_neurons!() click to toggle source
# File lib/rubyneat/evolver.rb, line 151
def mutate_add_neurons!
  @npop.critters.each do |critter|
    if rand < @controller.parms.mutate_add_neuron_prob
      log.debug "mutate_add_neurons! for #{critter}"
      @critter_op.add_neuron! critter
    end
  end
end
mutate_change_gene_weights!() click to toggle source

Totally change weights to something completely different

# File lib/rubyneat/evolver.rb, line 112
def mutate_change_gene_weights!
  @gchange = Distribution::Normal::rng(0, @controller.parms.mutate_change_gene_weights_sd) if @gchange.nil?
  @npop.critters.each do |critter|
    critter.genotype.genes.each { |innov, gene|
      if rand < @controller.parms.mutate_change_gene_weights_prob
        gene.weight = chg = @gchange.()
        log.debug { "Change gene #{gene}.#{innov} by #{chg}" }
      end
    }
  end
end
mutate_change_neurons!() click to toggle source

TODO Finish mutate_change_neurons!

# File lib/rubyneat/evolver.rb, line 161
def mutate_change_neurons!
  log.error "mutate_change_neurons! NIY"
end
mutate_disable_genes!() click to toggle source
# File lib/rubyneat/evolver.rb, line 133
def mutate_disable_genes!
  @npop.critters.each do |critter|
    if rand < @controller.parms.mutate_gene_disable_prob
      log.debug "mutate_disable_genes! for #{critter}"
      @critter_op.disable_gene! critter
    end
  end
end
mutate_perturb_gene_weights!() click to toggle source

Perturb existing gene weights by adding a guassian to them.

# File lib/rubyneat/evolver.rb, line 99
def mutate_perturb_gene_weights!
  @gperturb = Distribution::Normal::rng(0, @controller.parms.mutate_perturb_gene_weights_sd) if @gperturb.nil?
  @npop.critters.each do |critter|
    critter.genotype.genes.each { |innov, gene|
      if rand < @controller.parms.mutate_perturb_gene_weights_prob
        gene.weight += per = @gperturb.()
        log.debug { "Peturbed gene #{gene}.#{innov} by #{per}" }
      end
    }
  end
end
mutate_reenable_genes!() click to toggle source
# File lib/rubyneat/evolver.rb, line 142
def mutate_reenable_genes!
  @npop.critters.each do |critter|
    if rand < @controller.parms.mutate_gene_reenable_prob
      log.debug "mutate_reenable_genes! for #{critter}"
      @critter_op.reenable_gene! critter
    end
  end
end
prepare_fitness!() click to toggle source

Sort species within the basis of fitness. Think of the fitness as an error / cost function. The better fit, the closer to zero the fitness parameter will be.

If a compare block is specified in the DSL, then that function is called with the *fitness values* from critters c1 and c2. The default valuation is c1.fitness <=> c2.fitness. You may elect to evaluate them differently.

# File lib/rubyneat/evolver.rb, line 82
def prepare_fitness!
  @npop.species.each do |k, sp|
    sp.sort!{|c1, c2|
      unless @controller.compare_func.nil?
        @controller.compare_func.(c1.fitness, c2.fitness)
      else
        c1.fitness <=> c2.fitness
      end
    }
  end
end
prepare_novelty!() click to toggle source

TODO: write novelty code

# File lib/rubyneat/evolver.rb, line 95
def prepare_novelty!
end
prepare_speciation!() click to toggle source

Here we specify evolutionary operators.

# File lib/rubyneat/evolver.rb, line 69
def prepare_speciation!
  @npop.speciate!
  log.debug "SPECIES:"
  NEAT::dpp @npop.species
end
sex(crit1, crit2) click to toggle source

Mate the given critters and return a baby. This is rather involved, and relies heavily on the Innovation Numbers.

Some definitions:

Matching Gene
 2 genes with matching innovation numbers.

Disjoint Gene
 A gene in one has an innovation number in the range of innovation numbers
 of the other.

Excess Gene
 Gene in one critter that has an innovation number outside of the range
 of innovation numbers of the other.

Neurons
 Distinct Neurons from both crit1 and crit2 must be present in
 the baby.

Excess and Disjoint genes are always included from the more fit parent. Matching genes are randomly chosen. For now, we make it 50/50.

# File lib/rubyneat/evolver.rb, line 221
def sex(crit1, crit2)
  Critter.new(@npop, true) do |baby|
    fitcrit = if crit1.fitness > crit2.fitness
                crit1
              elsif crit2.fitness > crit1.fitness
                crit2
              else
                (rand(2) == 1) ? crit1 : crit2
              end
    a = crit1.genotype.genes.keys.to_set
    b = crit2.genotype.genes.keys.to_set
    disjoint = (a - b) + (b - a)
    joint = (a + b) - disjoint
    baby.genotype.neucleate { |gtype|
      joint.map { |innov|
        g1 = crit1.genotype.genes[innov]
        g2 = crit2.genotype.genes[innov]
        Critter::Genotype::Gene[gtype,
                                g1.in_neuron, g1.out_neuron,
                                (rand(2) == 1) ? g1.weight : g2.weight,
                                innov]
      } + disjoint.map { |innov|
        fitcrit.genotype.genes[innov].clone unless fitcrit.genotype.genes[innov].nil?
      }.reject{|i| i.nil? }
    }
    baby.genotype.innervate! crit1.genotype.neurons, crit2.genotype.neurons
    baby.genotype.prune!
    baby.genotype.wire!
  end
end