UNPKG

win-nsga

Version:

Create and manage an evolutionary algorithm that runs the NSGA-II multiobjective search algorithm (bit redundant). This is a WIN module.

504 lines (395 loc) 17.5 kB
var Q = require('q'); var ranking = require('./rank.js'); var wUtils = require('win-utils'); var wMath = wUtils.math; module.exports = evoContainer; function evoContainer(evoProps, localConfig, backEmit, log) { var self = this; //add emit functionality to self self.population = []; self.generation = 0; //don't change popEval dictionary ever //it is passed to ranking object self.popEvaluations = {}; //save geno type and log stuff self.genomeType = evoProps.genomeType; self.defaultFitness = evoProps.defaultFitness || 0.00001; self.tournamentSize = evoProps.tournamentSize || 3; //assumed 10% elitism self.elitismProportion = evoProps.elitismProportion || .1; //assumed 50/50 asexual/sexual self.asexualProportion = evoProps.asexualProportion || (1- (evoProps.sexualProportion || .5)); self.sexualProportion = evoProps.sexualProportion || (1 - self.asexualProportion); //prop sum chekc -- should be 1 exactly var propSum = self.asexualProportion + self.sexualProportion; if(propSum != 1) { //readjust for weird proportion behavior -- resets to being fractions summing to 1! self.asexualProportion = self.asexualProportion/propSum; self.sexualProportion = self.sexualProportion/propSum; } self.log = log; self.backEmit = backEmit; //pass our q function for backbone emit calls -- don't need to keep redefining it self.multiobjective = new ranking(self.popEvaluations, self.backEmit, log); //what do we need to monitor for every object in the population var emptyEvaluation = function() { //some measure of complexity for that encoding type return {age: 0, fitness: 0.000001, realFitness: 0.000001, behaviors: [], complexity: 0}; } var mergeEvalIntoObject = function(fromEval, toEval) { for(var key in fromEval) { toEval[key] = fromEval[key]; } //make sure some defaults exists toEval.fitness = Math.max(toEval.fitness || 0, self.defaultFitness); toEval.realFitness = Math.max(toEval.realFitness || 0, self.defaultFitness); toEval.behaviors = toEval.behaviors || []; toEval.complexity = toEval.complexity || 0; } self.clearSessionObject = function() { self.sessionObject = {}; } self.createOffSpring = function() { //gunna get to this, we swear var defer = Q.defer(); self.clearSessionObject(); //We don't do any of this -- the encodings responsible will handle augmenting the session object //when we clear session info, we're also destroying anything done by other encodings -- it's like reset for everyone // Create a new lists so that we can track which connections/neurons have been added during this routine. // self.sessionObject.newConnectionTable = []; // self.sessionObject.newNodeTable = []; //lets create a list of parents we want for creating artifacts var eliteCount = Math.floor(self.elitismProportion*self.population.length); //how many to create var nonElite = self.population.length - eliteCount; //select how many are asexual var asexual = Math.floor(self.asexualProportion*nonElite); //the rest are sexual offspring var sexual = nonElite - asexual; //life is hard as a single parent -- this is just the offspring from asexual reproduction var singleParents = self.selectSingleParents(asexual); //need parents chosen through tournament selection/some type of mating routine //if for whatever unknown reason you are rocking a pop size of 1 (what are you doing?), we use asexual reproduction -- duh var sexyParents = (self.population.length > 1) ? self.selectSexualParents(sexual) : self.selectSingleParents(sexual); //merge the two parent lists var parentIxs = singleParents.concat(sexyParents); //now we have our desired parents for offspring creation, we return the offspring objects //we must force these parents (and track new nodes/connections), so we pass a session object to create artifacts self.sessionObject.forceParents = parentIxs; //send them to the generator -- more power! //we need to generate as many objects as we have parent lists, we pass in the full population, as well as the force parents object in session self.backEmit.qCall("generator:createArtifacts", self.genomeType, parentIxs.length, self.population, self.sessionObject) .then(function(offspringObject) { //these are our new population of objects! Created from our parents :) var offspring = offspringObject.offspring; //now we have our offspring all ready for next step defer.resolve(offspring); }) .fail(function(err) { defer.reject(err); }); return defer.promise; }; self.selectSingleParents = function(count) { var p = []; var popLength = self.population.length; for(var i=0; i < count; i++) { var inner = [wMath.next(popLength)]; p.push(inner); } //return an array of arrays of length 1 //used for parent selection return p; } self.selectSexualParents = function(count) { var p = []; var popLength = self.population.length; for(var i=0; i < count; i++) { var inner, parent1, parent2; //tournament select in multiobjective search parent1 = self.tournamentSelect(self.population); // Select the 2nd parent from the whole popualtion (there is a chance that this will be the same artifact var j=0; do { parent2 = self.tournamentSelect(self.population); } while(parent1.ix==parent2.ix && j++ < 4); //we found two different objects! if(parent1.ix != parent2.ix) { inner = [parent1.ix, parent2.ix]; } else inner = [parent1.ix]; //either we have a single object selected, or two -- either way, add it to the pile p.push(inner); } //return an array of arrays of length 1 //used for parent selection return p; } self.shutDown = function() { self.isShutDown = true; } self.performOneGeneration = function() { //gunna get to this, we swear var defer = Q.defer(); //No speciation in multiobjective //therefore no species to check for removal //----- Stage 1. Create offspring / cull old genomes / add offspring to population. //send population (along with known eval information) -- can use this for ranking self.multiobjective.addPopulation(self.population, self.popEvaluations); //ranking is now an async process -- we need to call someone to measure our evaluations self.multiobjective.rankGenomes() .then(function() { if(self.isShutDown) return; //finished ranking genomes //cut the population down to the desired size -- use the rankings to sort and cut self.multiobjective.truncatePopulation(self.population.length); //no speciation necessary //keep the numbers in line -- you know how unruly they get self.updateFitnessStats(); //let's make some babies return self.createOffSpring(); }) .then(function(offspring) { if(self.isShutDown) return; //we need to trim population to the elite count, then replace //however, this doesn't affect the multiobjective population -- just the population held in search at the time //sort our pop objects please ranking.SortPopulation(self.population, self.popEvaluations); //how many are kept simply for being good var eliteCount = Math.floor(self.elitismProportion*self.population.length); //remove everything but the most elite! self.population = self.population.slice(0, eliteCount); //now remove the excess evaluations var mergePops = self.population.concat(self.multiobjective.activePopulation); var allKeys = {}; //pull the leftover evaluations from the former eval object (pEvals) for(var i=0; i < mergePops.length; i++) { allKeys[mergePops[i].wid] = true; } //to prevent bloating over many generations, we trim the pop eval dictionary to not have useless eval objects var evalKeys = Object.keys(self.popEvaluations); for(var i=0; i < evalKeys.length; i++) { var key = evalKeys[i]; //if we don't have this key in our current pop, or the multiobjective pop //you can remove it from evaluation objects if(!allKeys[key]) delete self.popEvaluations[key]; } //Add offspring to the population. var genomeBound = offspring.length; for(var genomeIdx=0; genomeIdx<genomeBound; genomeIdx++) { var gObj = offspring[genomeIdx]; self.population.push(gObj); //set the offspring up with an empty evaluation self.popEvaluations[gObj.wid] = emptyEvaluation(); } //----- Stage 2. Evaluate genomes / Update stats. return self.evaluatePopulation(); }) .then(function() { if(self.isShutDown) { defer.resolve(); return; } //update our stats now, all done with evals self.updateFitnessStats(); //we've all gotten just a bit older, wouldn't you say? self.incrementAges(); //increment gen count as well -- happy birthday self.generation++; //also, we're done with this function defer.resolve(); }) .fail(function(err) { defer.reject(err); }) //i will not fail you, probably return defer.promise; }; self.evaluatePopulation= function() { //gunna get to this, we swear var defer = Q.defer(); //default everyone is evaluated if(!self.backEmit.hasListeners("evaluate:evaluateArtifacts")) throw new Error("No evaluation function defined, how are you supposed to run evolution?"); //ask the backbone for some help evaluating -- not the responsibility of evolution, yo self.backEmit.qCall("evaluate:evaluateArtifacts", self.population) .then(function(evalObject) { if(!evalObject || !evalObject.evaluations || evalObject.evaluations.length != self.population.length) throw new Error("Evaluate Artifacts must return an object with evaluations property of type [array] and length [" + self.population.length +"]"); var evaluations = evalObject.evaluations; for(var i=0; i < evaluations.length; i++) { //fetch relevant objects var realEval = evaluations[i]; var pObj = self.population[i]; var pEval = self.popEvaluations[pObj.wid]; // self.log("Real: ", evaluations[i], " obj: ", pObj, " peval: ", pEval); //merge the evaluation object into our existing! mergeEvalIntoObject(realEval, pEval); //our eval work here is done, friend. } //evaluations incorporated into the population -- thank the heavens defer.resolve(); }) .fail(function(err) { self.log("Fail catch: ", err.stack); defer.reject(err); }) //send back a notice that we're really gonna do this eventually, no worries return defer.promise; }; //now lets get started self.createInitialPopulation = function(evoProps, seeds) { var defer = Q.defer(); var popSize = evoProps.populationSize; if(!popSize) throw new Error("Can't initialize population with no size. NSGA-II error.") //Reset session object, for the children's sake. self.clearSessionObject(); //first thing is to take our seeds and create a bunch of new objects //todo: want to send specific request self.backEmit.qCall("generator:createArtifacts", self.genomeType, popSize, seeds) .then(function(offspringObject) { //these are our new population of objects! Created from our seeds :) var offspring = offspringObject.offspring; //We can declare our initial population as these objects self.population = offspring; //need population properties to match our population objects -- we use the id map for(var i=0; i < offspring.length; i++) self.popEvaluations[offspring[i].wid] = (emptyEvaluation()); //we have to evaluate our initial objects before proceeding return self.evaluatePopulation(); }) .fail(function(err) { defer.reject(err); }) .done(function() { //gotta resolve at some point! defer.resolve(); }) return defer.promise; } self.endEvolution = function() { //probably a lot to clean up here -- also, might be interested in doing some saving as well var defer = Q.defer(); //we need to clean up all our junk self.multiobjective.addPopulation(self.population, self.popEvaluations); //we should rank the individuals being sent back as the last population though! self.multiobjective.rankGenomes() .then(function() { //remove excess objects from popEvaluations self.multiobjective.sortPopulation(); // self.multiobjective.truncatePopulation(self.population.length); // for(var i=0; i < self.multiobjective.activePopulation.length; i++) // { // var iPop = self.multiobjective.activePopulation[i]; // self.log("Ending evo: ".rainbow, self.popEvaluations[iPop.wid]); // } //for now, we just return our current population and the evaluations //in reality, we should be searching our archive for most interesting objects //that's outside the scope of the nsga algorithm though defer.resolve({population: self.multiobjective.activePopulation, evaluations: self.popEvaluations}); }) .fail(function(err) { //oops, error ranking genoems before the end defer.reject(err); }) return defer.promise; } self.incrementAges = function() { //would normally increment species age as well, but doesn't happen in multiobjective for(var key in self.popEvaluations) { var ng = self.popEvaluations[key]; ng.age++; } }; self.updateFitnessStats = function() { self.bestFitness = Number.MIN_VALUE; self.bestGenomeID = null; self.totalFitness = 0; self.avgComplexity = 0; self.meanFitness =0; self.totalComplexity = 0; //go through our evaluations -- sum up complexity and choose champ for(var key in self.popEvaluations) { var evalInfo = self.popEvaluations[key]; if(evalInfo.realFitness > self.bestFitness) { self.bestFitness = evalInfo.realFitness; self.bestGenomeID = key; } //add up complexity measure self.totalComplexity += evalInfo.complexity; //pull the fitness sum self.totalFitness += evalInfo.realFitness; } self.avgComplexity = self.totalComplexity/self.population.length; self.meanFitness = self.totalFitness/self.population.length; }; self.tournamentSelect = function(genomes) { var bestFound= 0.0; var bestGenome=null; var selIx = -1; var bound = genomes.length; //grab the best of 4 by default, can be more attempts than that for(var i=0;i<self.tournamentSize;i++) { var genomeIx = wMath.next(bound); var next= genomes[genomeIx]; var evalInfo = self.popEvaluations[next.wid]; if(!evalInfo) throw new Error("tournamentSelect fails without WID information, cannot fetch from evaluation objects."); if (evalInfo.fitness > bestFound) { bestFound=evalInfo.fitness; bestGenome=next; selIx = genomeIx; } } return {genome: bestGenome, ix: selIx}; }; return self; }