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
Public Class Methods
# File lib/rubyneat/evolver.rb, line 13 def initialize(c) super @critter_op = CritterOp.new self end
Public Instance Methods
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
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
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
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
# 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
# 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
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
TODO Finish mutate_change_neurons!
# File lib/rubyneat/evolver.rb, line 161 def mutate_change_neurons! log.error "mutate_change_neurons! NIY" end
# 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
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
# 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
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
TODO: write novelty code
# File lib/rubyneat/evolver.rb, line 95 def prepare_novelty! end
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
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