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.

793 lines (616 loc) 28.4 kB
//here we have everything for NSGA-II mutliobjective search and neatjs var neatjs = require('neatjs'); var cppnjs = require('cppnjs'); var utilities = cppnjs.utilities; var cppnActivationFactory = cppnjs.cppnActivationFactory; var cppnNode = cppnjs.cppnNode; var neatGenome = neatjs.neatGenome; var neatConnection = neatjs.neatConnection; var neatNode = neatjs.neatNode; var neatParameters = neatjs.neatParameters; var genomeSharpToJS = neatjs.genomeSharpToJS; var novelty = neatjs.novelty; var multiobjective = {}; module.exports = multiobjective; //information to rank each genome multiobjective.RankInfo = function() { var self = this; //when iterating, we count how many genomes dominate other genomes self.dominationCount = 0; //who does this genome dominate self.dominates = []; //what is this genome's rank (i.e. what pareto front is it on) self.rank = 0; //has this genome been ranked self.ranked = false; self.reset = function(){ self.rank = 0; self.ranked = false; self.dominationCount = 0; self.dominates = []; }; return self; }; multiobjective.Help = {}; multiobjective.Help.SortPopulation = function(pop) { //sort genomes by fitness / age -- as genomes are often sorted pop.sort(function(x,y){ var fitnessDelta = y.fitness - x.fitness; if (fitnessDelta < 0.0) return -1; else if (fitnessDelta > 0.0) return 1; var ageDelta = x.age - y.age; // Convert result to an int. if (ageDelta < 0) return -1; else if (ageDelta > 0) return 1; return 0; }); }; //class to assign multiobjective fitness to individuals (fitness based on what pareto front they are on) multiobjective.Multiobjective = function(backone, globalConfig, localConfig, np) { var self = this; self.bb = backbone; self.winFunction = "evolution"; self.bbEmit = self.bb.getEmitter(self); self.log = self.backbone.getLogger(self); //set log level to be whatever configured, or just normal logging self.log.logLevel = localConfig.logLevel || self.log.normal; self.np = np; self.population = []; self.populationIDs = {}; self.ranks = []; self.nov = new novelty.Novelty(10.0); self.doNovelty = false; self.generation = 0; self.localCompetition = false; self.measureNovelty = function() { var count = self.population.length; self.nov.initialize(self.population); //reset locality and competition for each genome for(var i=0; i < count; i++) { var genome = self.population[i]; genome.locality=0.0; genome.competition=0.0; //we measure all objectives locally -- just to make it simpler for(var o=0; o < genome.objectives.length; o++) genome.localObjectivesCompetition[o] = 0.0; } var ng; var max = 0.0, min = 100000000000.0; for (var i = 0; i< count; i++) { ng = self.population[i]; var fit = self.nov.measureNovelty(ng); //reset our fitness value to be local, yeah boyee //the first objective is fitness which is replaced with local fitness -- how many did you beat around you // # won / total number of neighbors = % competitive ng.objectives[0] = ng.competition / ng.nearestNeighbors; ng.objectives[ng.objectives.length - 2] = fit + 0.01; //the last local measure is the genome novelty measure var localGenomeNovelty = ng.localObjectivesCompetition[ng.objectives.length-1]; //genomic novelty is measured locally as well self.log("Genomic Novelty: " + ng.objectives[ng.objectives.length - 1] + " After: " + localGenomeNovelty / ng.nearestNeighbors); //this makes genomic novelty into a local measure ng.objectives[ng.objectives.length - 1] = localGenomeNovelty / ng.nearestNeighbors; if(fit>max) max=fit; if(fit<min) min=fit; } self.log("nov min: "+ min + " max:" + max); }; //if genome x dominates y, increment y's dominated count, add y to x's dominated list self.updateDomination = function( x, y, r1, r2) { if(self.dominates(x,y)) { r1.dominates.push(r2); r2.dominationCount++; } }; //function to check whether genome x dominates genome y, usually defined as being no worse on all //objectives, and better at at least one self.dominates = function( x, y) { var better=false; var objx = x.objectives, objy = y.objectives; var sz = objx.length; //if x is ever worse than y, it cannot dominate y //also check if x is better on at least one for(var i=0;i<sz-1;i++) { if(objx[i]<objy[i]) return false; if(objx[i]>objy[i]) better=true; } //genomic novelty check, disabled for now //threshold set to 0 -- Paul since genome is local var thresh=0.0; if((objx[sz-1]+thresh)<(objy[sz-1])) return false; if((objx[sz-1]>(objy[sz-1]+thresh))) better=true; return better; }; //distance function between two lists of objectives, used to see if two individuals are unique self.distance = function(x, y) { var delta=0.0; var len = x.length; for(var i=0;i<len;i++) { var d=x[i]-y[i]; delta+=d*d; } return delta; }; //Todo: Print to file self.printDistribution = function() { var filename="dist"+ self.generation+".txt"; var content=""; self.log("Print to file disabled for now, todo: write in save to file!"); // XmlDocument archiveout = new XmlDocument(); // XmlPopulationWriter.WriteGenomeList(archiveout, population); // archiveout.Save(filename); }; //currently not used, calculates genomic novelty objective for protecting innovation //uses a rough characterization of topology, i.e. number of connections in the genome self.calculateGenomicNovelty = function() { var sum=0.0; var max_conn = 0; var xx, yy; for(var g=0; g < self.population.length; g++) { xx = self.population[g]; var minDist=10000000.0; var difference=0.0; var delta=0.0; //double array var distances= []; if(xx.connections.length > max_conn) max_conn = xx.connections.length; //int ccount=xx.ConnectionGeneList.Count; for(var g2=0; g2 < self.population.length; g2++) { yy = self.population[g2]; if(g==g2) continue; //measure genomic compatability using neatparams var d = xx.compat(yy, np); //if(d<minDist) // minDist=d; distances.push(d); } //ascending order //want the closest individuals distances.Sort(function(a,b) {return a-b;}); //grab the 10 closest distances var sz=Math.min(distances.length,10); var diversity = 0.0; for(var i=0;i<sz;i++) diversity+=distances[i]; xx.objectives[xx.objectives.length-1] = diversity; sum += diversity; } self.log("Diversity: " + sum/population.length + " " + max_conn); }; //add an existing population from hypersharpNEAT to the multiobjective population maintained in //this class, step taken before evaluating multiobjective population through the rank function self.addPopulation = function(genomes) { for(var i=0;i< genomes.length;i++) { var blacklist=false; //TODO: I'm not sure this is correct, since genomes coming in aren't measured locally yet //so in some sense, we're comparing local measures to global measures and seeing how far //if they are accidentally close, this could be bad news // for(var j=0;j<self.population.length; j++) // { // if(self.distance(genomes[i].behavior.objectives, self.population[j].objectives) < 0.01) // blacklist=true; //reject a genome if it is very similar to existing genomes in pop // } //no duplicates please if(self.populationIDs[genomes[i].gid]) blacklist = true; //TODO: Test if copies are needed, or not? if(!blacklist) { //add genome if it is unique //we might not need to make copies //this will make a copy of the behavior // var copy = new neatGenome.NeatGenome.Copy(genomes[i], genomes[i].gid); // self.population.push(copy); //push directly into population, don't use copy -- should test if this is a good idea? self.population.push(genomes[i]); self.populationIDs[genomes[i].gid] = genomes[i]; } } }; multiobjective.Help.SortPopulation = function(pop) { pop.sort() } self.rankGenomes = function() { var size = self.population.length; self.calculateGenomicNovelty(); if(self.doNovelty) { self.measureNovelty(); } //reset rank information for(var i=0;i<size;i++) { if(self.ranks.length<i+1) self.ranks.push(new multiobjective.RankInfo()); else self.ranks[i].reset(); } //calculate domination by testing each genome against every other genome for(var i=0;i<size;i++) { for(var j=0;j<size;j++) { self.updateDomination(self.population[i], self.population[j],self.ranks[i],self.ranks[j]); } } //successively peel off non-dominated fronts (e.g. those genomes no longer dominated by any in //the remaining population) var front = []; var ranked_count=0; var current_rank=1; while(ranked_count < size) { //search for non-dominated front for(var i=0;i<size;i++) { //continue if already ranked if(self.ranks[i].ranked) continue; //if not dominated, add to front if(self.ranks[i].dominationCount==0) { front.push(i); self.ranks[i].ranked=true; self.ranks[i].rank = current_rank; } } var front_size = front.length; self.log("Front " + current_rank + " size: " + front_size); //now take all the non-dominated individuals, see who they dominated, and decrease //those genomes' domination counts, because we are removing this front from consideration //to find the next front of individuals non-dominated by the remaining individuals in //the population for(var i=0;i<front_size;i++) { var r = self.ranks[front[i]]; for (var dominated in r.dominates) { dominated.dominationCount--; } } ranked_count+=front_size; front = []; current_rank++; } //we save the last objective for potential use as genomic novelty objective var last_obj = self.population[0].objectives.length-1; //fitness = popsize-rank (better way might be maxranks+1-rank), but doesn't matter //because speciation is not used and tournament selection is employed for(var i=0;i<size;i++) { self.population[i].fitness = (size+1)-self.ranks[i].rank;//+population[i].objectives[last_obj]/100000.0; } //sorting based on fitness multiobjective.Help.SortPopulation(self.population); self.generation++; if(self.generation%250==0) self.printDistribution(); }; //when we merge populations together, often the population will overflow, and we need to cut //it down. to do so, we just remove the last x individuals, which will be in the less significant //pareto fronts self.truncatePopulation = function(size) { var toRemove = self.population.length - size; self.log("population size before: " + self.population.length); self.log("removing " + toRemove); //remove the tail after sorting if(toRemove > 0) self.population.splice(size, toRemove); //changes to population, make sure to update our lookup self.populationIDs = neatGenome.Help.CreateGIDLookup(self.population); self.log("population size after: " + self.population.length); return self.population; }; //send it on back yo return self; }; multiobjective.MultiobjectiveSearch = function(seedGenomes, genomeEvaluationFunctions, neatParameters, searchParameters) { var self=this; //functions for evaluating genomes in a population self.genomeEvaluationFunctions = genomeEvaluationFunctions; self.generation = 0; self.np = neatParameters; self.searchParameters = searchParameters; //for now, we just set seed genomes as population //in reality, we should use seed genomes as seeds into population determined by search parameters //i.e. 5 seed genomes -> 50 population size //TODO: Turn seed genomes into full first population self.population = seedGenomes; //create genome lookup once we have population self.populationIDs = neatGenome.Help.CreateGIDLookup(seedGenomes); //see end of multiobjective search declaration for initailization code self.multiobjective= new multiobjective.Multiobjective(neatParameters); self.np.compatibilityThreshold = 100000000.0; //disable speciation w/ multiobjective self.initializePopulation = function() { // The GenomeFactories normally won't bother to ensure that like connections have the same ID // throughout the population (because it's not very easy to do in most cases). Therefore just // run this routine to search for like connections and ensure they have the same ID. // Note. This could also be done periodically as part of the search, remember though that like // connections occuring within a generation are already fixed - using a more efficient scheme. self.matchConnectionIDs(); // Evaluate the whole population. self.evaluatePopulation(); //TODO: Add in some concept of speciation for NSGA algorithm -- other than genomic novelty? //We don't do speciation for NSGA-II algorithm // Now we have fitness scores and no speciated population we can calculate fitness stats for the // population as a whole -- and save best genomes //recall that speciation is NOT part of NSGA-II self.updateFitnessStats(); }; self.matchConnectionIDs = function() { var connectionIdTable = {}; var genomeBound = self.population.length; for(var genomeIdx=0; genomeIdx<genomeBound; genomeIdx++) { var genome = self.population[genomeIdx]; //loop through all the connections for this genome var connectionGeneBound = genome.connections.length; for(var connectionGeneIdx=0; connectionGeneIdx<connectionGeneBound; connectionGeneIdx++) { var connectionGene = genome.connections[connectionGeneIdx]; var ces = connectionGene.sourceID + "," + connectionGene.targetID; var existingID = connectionIdTable[ces]; if(existingID==null) { // No connection withthe same end-points has been registered yet, so // add it to the table. connectionIdTable[ces] = connectionGene.gid; } else { // This connection is already registered. Give our latest connection // the same innovation ID as the one in the table. connectionGene.gid = existingID; } } // The connection genes in this genome may now be out of order. Therefore we must ensure // they are sorted before we continue. genome.connections.sort(function(a,b){ return a.gid - b.gid; }); } }; self.incrementAges = function() { //would normally increment species age as well, but doesn't happen in multiobjective for(var i=0; i < self.population.length; i++) { var ng = self.population[i]; ng.age++; } }; self.updateFitnessStats = function() { self.bestFitness = Number.MIN_VALUE; self.bestGenome = null; self.totalNeuronCount = 0; self.totalConnectionCount = 0; self.totalFitness = 0; self.avgComplexity = 0; self.meanFitness =0; //go through the genomes, find the best genome and the most fit for(var i=0; i < self.population.length; i++) { var ng = self.population[i]; if(ng.realFitness > self.bestFitness) { self.bestFitness = ng.realFitness; self.bestGenome = ng; } self.totalNeuronCount += ng.nodes.length; self.totalConnectionCount += ng.connections.length; self.totalFitness += ng.realFitness; } self.avgComplexity = (self.totalNeuronCount + self.totalConnectionCount)/self.population.length; self.meanFitness = self.totalFitness/self.population.length; }; self.tournamentSelect = function(genomes) { var bestFound= 0.0; var bestGenome=null; var bound = genomes.length; //grab the best of 4 by default, can be more attempts than that for(var i=0;i<self.np.tournamentSize;i++) { var next= genomes[utilities.next(bound)]; if (next.fitness > bestFound) { bestFound=next.fitness; bestGenome=next; } } return bestGenome; }; self.evaluatePopulation= function() { //for each genome, we need to check if we should evaluate the individual, and then evaluate the individual //default everyone is evaluated var shouldEvaluate = self.genomeEvaluationFunctions.shouldEvaluateGenome || function(){return true;}; var defaultFitness = self.genomeEvaluationFunctions.defaultFitness || 0.0001; if(!self.genomeEvaluationFunctions.evaluateGenome) throw new Error("No evaluation function defined, how are you supposed to run evolution?"); var evaluateGenome = self.genomeEvaluationFunctions.evaluateGenome; for(var i=0; i < self.population.length; i++) { var ng = self.population[i]; var fit = defaultFitness; if(shouldEvaluate(ng)) { fit = evaluateGenome(ng, self.np); } ng.fitness = fit; ng.realFitness = fit; } }; self.performOneGeneration = function() { //No speciation in multiobjective //therefore no species to check for removal //----- Stage 1. Create offspring / cull old genomes / add offspring to population. var regenerate = false; self.multiobjective.addPopulation(self.population); self.multiobjective.rankGenomes(); //cut the population down to the desired size self.multiobjective.truncatePopulation(self.population.length); //no speciation necessary //here we can decide if we want to save to WIN self.updateFitnessStats(); if(!regenerate) { self.createOffSpring(); //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 multiobjective.Help.SortPopulation(self.population); var eliteCount = Math.floor(self.np.elitismProportion*self.population.length); //remove everything but the most elite! self.population.splice(eliteCount, self.population.length - eliteCount); // Add offspring to the population. var genomeBound = self.offspringList.length; for(var genomeIdx=0; genomeIdx<genomeBound; genomeIdx++) self.population.push(self.offspringList[genomeIdx]); } //----- Stage 2. Evaluate genomes / Update stats. self.evaluatePopulation(); self.updateFitnessStats(); self.incrementAges(); self.generation++; }; self.createOffSpring = function() { self.offspringList = []; // Create a new lists so that we can track which connections/neurons have been added during this routine. self.newConnectionTable = []; self.newNodeTable = []; //now create chunk of offspring asexually self.createMultipleOffSpring_Asexual(); //then the rest sexually self.createMultipleOffSpring_Sexual(); }; self.createMultipleOffSpring_Asexual = function() { //function for testing if offspring is valid var validOffspring = self.genomeEvaluationFunctions.isValidOffspring || function() {return true;}; var attemptValid = self.genomeEvaluationFunctions.validOffspringAttempts || 5; var eliteCount = Math.floor(self.np.elitismProportion*self.population.length); //how many asexual offspring? Well, the proportion of asexual * total number of desired new individuals var offspringCount = Math.max(1, Math.round((self.population.length - eliteCount)*self.np.pOffspringAsexual)); // Add offspring to a seperate genomeList. We will add the offspring later to prevent corruption of the enumeration loop. for(var i=0; i<offspringCount; i++) { var parent=null; //tournament select in multiobjective search parent = self.tournamentSelect(self.population); var offspring = parent.createOffspringAsexual(self.newNodeTable, self.newConnectionTable, self.np); var testCount = 0, maxTests = attemptValid; //if we have a valid genotype test function, it should be used for generating this individual! while (!validOffspring(offspring, self.np) && testCount++ < maxTests) offspring = parent.createOffspringAsexual(self.newNodeTable, self.newConnectionTable, self.np); //we have a valid offspring, send it away! self.offspringList.push(offspring); } }; self.createMultipleOffSpring_Sexual = function() { //function for testing if offspring is valid var validOffspring = self.genomeEvaluationFunctions.isValidOffspring || function() {return true;}; var attemptValid = self.genomeEvaluationFunctions.validOffspringAttempts || 5; var oneMember=false; var twoMembers=false; if(self.population.length == 1) { // We can't perform sexual reproduction. To give the species a fair chance we call the asexual routine instead. // This keeps the proportions of genomes per species steady. oneMember = true; } else if(self.population.length==2) twoMembers = true; // Determine how many sexual offspring to create. var eliteCount = Math.floor(self.np.elitismProportion*self.population.length); //how many sexual offspring? Well, the proportion of sexual * total number of desired new individuals var matingCount = Math.round((self.population.length - eliteCount)*self.np.pOffspringSexual); for(var i=0; i<matingCount; i++) { var parent1; var parent2=null; var offspring; if(utilities.nextDouble() < self.np.pInterspeciesMating) { // Inter-species mating! //System.Diagnostics.Debug.WriteLine("Inter-species mating!"); if(oneMember) parent1 = self.population[0]; else { //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 an genome // from this species, but that's OK). var j=0; do { parent2 = self.tournamentSelect(self.population); } while(parent1==parent2 && j++ < 4); // Slightly wasteful but not too bad. Limited by j. } else { // Mating within the current species. //System.Diagnostics.Debug.WriteLine("Mating within the current species."); if(oneMember) { // Use asexual reproduction instead. offspring = self.population[0].createOffspringAsexual(self.newNodeTable, self.newConnectionTable, self.np); var testCount = 0; var maxTests = attemptValid; //if we have an assess function, it should be used for generating this individual! while (!validOffspring(offspring) && testCount++ < maxTests) offspring = self.population[0].createOffspringAsexual(self.newNodeTable, self.newConnectionTable, self.np); self.offspringList.push(offspring); continue; } if(twoMembers) { offspring = self.population[0].createOffspringSexual(self.population[1], self.np); var testCount = 0; var maxTests = attemptValid; //if we have an assess function, it should be used for generating this individual! while (!validOffspring(offspring) && testCount++ < maxTests) offspring = self.population[0].createOffspringSexual(self.population[1], self.np); self.offspringList.push(offspring); continue; } parent1 = self.tournamentSelect(self.population); var j=0; do { parent2 = self.tournamentSelect(self.population); } while(parent1==parent2 && j++ < 4); // Slightly wasteful but not too bad. Limited by j. } if(parent1 != parent2) { offspring = parent1.createOffspringSexual(parent2, self.np); var testCount = 0; var maxTests = attemptValid; //if we have an assess function, it should be used for generating this individual! while (!validOffspring(offspring) && testCount++ < maxTests) offspring = parent1.createOffspringSexual(parent2, self.np); self.offspringList.push(offspring); } else { // No mating pair could be found. Fallback to asexual reproduction to keep the population size constant. offspring = parent1.createOffspringAsexual(self.newNodeTable, self.newConnectionTable,self.np); var testCount = 0; var maxTests = attemptValid; //if we have an assess function, it should be used for generating this individual! while (!validOffspring(offspring) && testCount++ < maxTests) offspring = parent1.createOffspringAsexual(self.newNodeTable, self.newConnectionTable,self.np); self.offspringList.push(offspring); } } }; //finishing initalizatgion of object self.initializePopulation(); //send back the full search object return self; }