| @ -1,5 +1,428 @@ | |||
| # Background | |||
| # Live Simulation | |||
| <customHTML /> | |||
| # Set Up | |||
| # Background and Theory | |||
| Since you stumbled upon this article, you might be wondering what the heck genetic algorithms are. | |||
| To put it simply: genetic algorithms employ the same tactics used in natural selection to find an optimal solution to a problem. | |||
| Genetic algorithms are often used in high dimensional problems where the optimal solution is not apparent. | |||
| A common use case of genetic algorithms are to tune [hyper-parameters](https://en.wikipedia.org/wiki/Hyperparameter) in a program. | |||
| However, this algorithm can be used in any scenario where you have a function which defines how well something performed and you would | |||
| like to find the absolute minimum. | |||
| Many people have used genetic algorithms in video games to auto learn the weaknesses of players. | |||
| The beautiful part about Genetic Algorithms are their simplicity; you need absolutely no knowledge of linear algebra or calculus. | |||
| To implement one all you need to know is **very basic** algebra and a general grasp of evolution. | |||
| # Genetic Algorithm | |||
| All genetic algorithms typically follow the pattern of evaluation mating, mutation, and selection. | |||
| I will dive into each section of this algorithm using JavaScript code snippets. | |||
| The algorithm which I present is very generic and modular so it should be easy to port into other applications and programming languages. | |||
|  | |||
| ## Create Initial Population | |||
| The very first thing we need to do is specify a data-structure for storing our genetic information. | |||
| In biology chromosomes are composed of sequences of genes. | |||
| Many people run genetic algorithms on binary arrays since they more closely represent DNA. | |||
| However, as computer scientists, it is often easier to model things as continuous numbers. | |||
| In this approach, every Gene will be a single floating point number ranging between zero and one. | |||
| This keeps things really easy because it is very simple perform any genetic "operation". | |||
| Every type of Gene will have a max and min value which represents the absolute extremes of that gene. | |||
| This works well for optimization because it allows us to easily limit our search space. | |||
| For example, we can specify that "height" gene can only vary between 0 and 90. | |||
| To get the actual value of the gene from its \[0-1] value we simple de-normalize it. | |||
| $$ | |||
| g_{real ralue} = (g_{high}- g_{low})g_{norm} + g_{low} | |||
| $$ | |||
| ```javascript | |||
| class Gene | |||
| { | |||
| /** | |||
| * Constructs a new Gene to store in a chromosome. | |||
| * @param min minimum value that this gene can store | |||
| * @param max value this gene can possibly be | |||
| * @param value normalized value | |||
| */ | |||
| constructor(min, max, value) | |||
| { | |||
| this.min = min; | |||
| this.max = max; | |||
| this.value = value; | |||
| } | |||
| /** | |||
| * De-normalizes the value of the gene | |||
| * @returns {*} | |||
| */ | |||
| getRealValue() | |||
| { | |||
| return (this.max - this.min) * this.value + this.min; | |||
| } | |||
| getValue() | |||
| { | |||
| return this.value; | |||
| } | |||
| setValue(val) | |||
| { | |||
| this.value = val; | |||
| } | |||
| makeClone() | |||
| { | |||
| return new Gene(this.min, this.max, this.value); | |||
| } | |||
| makeRandomGene() | |||
| { | |||
| return new Gene(this.min, this.max, Math.random()); | |||
| } | |||
| } | |||
| ``` | |||
| Now that we have genes, we can create Chromosomes. | |||
| Chromosomes are simply collections of genes. | |||
| Whatever language you make this in, make sure that when you create a new Chromosome it | |||
| is has a [deep copy](https://en.wikipedia.org/wiki/Object_copying) of the genetic information rather than a shallow copy. | |||
| A shallow copy is when you simple copy the object pointer where a deep copy is actually creating a full new object. | |||
| If you fail to do a deep copy, you will have weird issues where multiple Chromosomes will share the same DNA. | |||
| In this class I added helper functions to clone and create a new Chromosome. | |||
| You can only create a new chromosome by cloning because I wanted to keep the program generic and make no assumptions about the domain. | |||
| Since you only provide the min/max information for the genes once, cloning an existing chromosome is the easy way of ensuring that all chromosomes contain similarly behaved genes. | |||
| ```javascript | |||
| class Chromosome | |||
| { | |||
| /** | |||
| * Constructs a chromosome by making a copy of | |||
| * a list of genes. | |||
| * @param geneArray | |||
| */ | |||
| constructor(geneArray) | |||
| { | |||
| this.genes = []; | |||
| for(let i = 0; i < geneArray.length; i++) | |||
| { | |||
| this.genes.push(geneArray[i].makeClone()); | |||
| } | |||
| } | |||
| getGenes() | |||
| { | |||
| return this.genes; | |||
| } | |||
| /** | |||
| * Mutates a random gene. | |||
| */ | |||
| mutate() | |||
| { | |||
| this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random()); | |||
| } | |||
| /** | |||
| * Creates a totally new chromosome with same | |||
| * genetic structure as this chromosome but different | |||
| * values. | |||
| * @returns {Chromosome} | |||
| */ | |||
| createRandomChromosome() | |||
| { | |||
| let geneAr = []; | |||
| for(let i = 0; i < this.genes.length; i++) | |||
| { | |||
| geneAr.push(this.genes[i].makeRandomGene()); | |||
| } | |||
| return new Chromosome(geneAr); | |||
| } | |||
| } | |||
| ``` | |||
| Creating a random population is pretty straight forward if implemented this method of random cloning in the Chromosome and Gene class. | |||
| ```javascript | |||
| /** | |||
| * Creates a totally random population based on a desired size | |||
| * and a prototypical chromosome. | |||
| * | |||
| * @param geneticChromosome | |||
| * @param populationSize | |||
| * @returns {Array} | |||
| */ | |||
| const createRandomPopulation = function(geneticChromosome, populationSize) | |||
| { | |||
| let population = []; | |||
| for(let i = 0; i < populationSize; i++) | |||
| { | |||
| population.push(geneticChromosome.createRandomChromosome()); | |||
| } | |||
| return population; | |||
| }; | |||
| ``` | |||
| This is where nearly all the domain information is introduced. | |||
| To create an entire population, you simply need to define what types of genes are found on each chromosome. | |||
| In this example all genes contain values ranging between one and ten. | |||
| ```javascript | |||
| let gene1 = new Gene(1,10,10); | |||
| let gene2 = new Gene(1,10,0.4); | |||
| let geneList = [gene1, gene2]; | |||
| let exampleOrganism = new Chromosome(geneList); | |||
| let population = createRandomPopulation(genericChromosome, 100); | |||
| ``` | |||
| ## Evaluate Fitness | |||
| ```javascript | |||
| let costx = Math.random() * 10; | |||
| let costy = Math.random() * 10; | |||
| /** Defines the cost as the "distance" to a 2-d point. | |||
| * @param chromosome | |||
| * @returns {number} | |||
| */ | |||
| const basicCostFunction = function(chromosome) | |||
| { | |||
| return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) + | |||
| Math.abs(chromosome.getGenes()[1].getRealValue() - costy); | |||
| }; | |||
| ``` | |||
| ## Selection | |||
| ```javascript | |||
| /** | |||
| * Function which computes the fitness of everyone in the | |||
| * population and returns the most fit survivors. Method | |||
| * known as elitism. | |||
| * | |||
| * @param population | |||
| * @param keepNumber | |||
| * @param fitnessFunction | |||
| * @returns {{average: number, | |||
| * survivors: Array, bestFit: Chromosome }} | |||
| */ | |||
| const naturalSelection = function(population, keepNumber, fitnessFunction) | |||
| { | |||
| let fitnessArray = []; | |||
| let total = 0; | |||
| for(let i = 0; i < population.length; i++) | |||
| { | |||
| const fitness = fitnessFunction(population[i]); | |||
| console.log(fitness); | |||
| fitnessArray.push({fit:fitness, chrom: population[i]}); | |||
| total+= fitness; | |||
| } | |||
| fitnessArray.sort(predicateBy("fit")); | |||
| let survivors = []; | |||
| let bestFitness = fitnessArray[0].fit; | |||
| let bestChromosome = fitnessArray[0].chrom; | |||
| for(let i = 0; i < keepNumber; i++) | |||
| { | |||
| survivors.push(fitnessArray[i].chrom); | |||
| } | |||
| return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome}; | |||
| }; | |||
| ``` | |||
| ```javascript | |||
| /** | |||
| * Helper function to sort an array | |||
| * | |||
| * @param prop name of JSON property to sort by | |||
| * @returns {function(*, *): number} | |||
| */ | |||
| function predicateBy(prop) | |||
| { | |||
| return function(a,b) | |||
| { | |||
| var result; | |||
| if(a[prop] > b[prop]) | |||
| { | |||
| result = 1; | |||
| } | |||
| else if(a[prop] < b[prop]) | |||
| { | |||
| result = -1; | |||
| } | |||
| return result; | |||
| } | |||
| } | |||
| ``` | |||
| ## Mating | |||
| ```javascript | |||
| /** | |||
| * Randomly everyone in the population | |||
| * | |||
| * @param population | |||
| * @param desiredPopulationSize | |||
| */ | |||
| const matePopulation = function(population, desiredPopulationSize) | |||
| { | |||
| const originalLength = population.length; | |||
| while(population.length < desiredPopulationSize) | |||
| { | |||
| let index1 = Math.round(Math.random() * (originalLength-1)); | |||
| let index2 = Math.round(Math.random() * (originalLength-1)); | |||
| if(index1 !== index2) | |||
| { | |||
| const babies = breed(population[index1], population[index2]); | |||
| population.push(babies[0]); | |||
| population.push(babies[1]); | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * Mates two chromosomes using the blending method | |||
| * and returns a list of 2 offspring. | |||
| * @param father | |||
| * @param mother | |||
| * @returns {Chromosome[]} | |||
| */ | |||
| const breed = function(father, mother) | |||
| { | |||
| let son = new Chromosome(father.getGenes()); | |||
| let daughter = new Chromosome(mother.getGenes()); | |||
| for(let i = 0;i < son.getGenes().length; i++) | |||
| { | |||
| let blendCoef = Math.random(); | |||
| blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef); | |||
| } | |||
| return [son, daughter]; | |||
| }; | |||
| /** | |||
| * Blends two genes together based on a random blend | |||
| * coefficient. | |||
| **/ | |||
| const blendGene = function(gene1, gene2, blendCoef) | |||
| { | |||
| let value1 = (blendCoef * gene1.getValue()) + | |||
| (gene2.getValue() * (1- blendCoef)); | |||
| let value2 = ((1-blendCoef) * gene1.getValue()) + | |||
| (gene2.getValue() * blendCoef); | |||
| gene1.setValue(value1); | |||
| gene2.setValue(value2); | |||
| }; | |||
| ``` | |||
| ## Mutation | |||
| ```javascript | |||
| /** | |||
| * Randomly mutates the population | |||
| **/ | |||
| const mutatePopulation = function(population, mutatePercentage) | |||
| { | |||
| if(population.length >= 2) | |||
| { | |||
| let mutations = mutatePercentage * | |||
| population.length * | |||
| population[0].getGenes().length; | |||
| for(let i = 0; i < mutations; i++) | |||
| { | |||
| population[i].mutate(); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| console.log("Error, population too small to mutate"); | |||
| } | |||
| }; | |||
| ``` | |||
| ## Immigration | |||
| ```javascript | |||
| /** | |||
| * Introduces x random chromosomes to the population. | |||
| * @param population | |||
| * @param immigrationSize | |||
| */ | |||
| const newBlood = function(population, immigrationSize) | |||
| { | |||
| for(let i = 0; i < immigrationSize; i++) | |||
| { | |||
| let geneticChromosome = population[0]; | |||
| population.push(geneticChromosome.createRandomChromosome()); | |||
| } | |||
| }; | |||
| ``` | |||
| ## Putting It All Together | |||
| ```javascript | |||
| /** | |||
| * Runs the genetic algorithm by going through the processes of | |||
| * natural selection, mutation, mating, and immigrations. This | |||
| * process will continue until an adequately performing chromosome | |||
| * is found or a generation threshold is passed. | |||
| * | |||
| * @param geneticChromosome Prototypical chromosome: used so algo knows | |||
| * what the dna of the population looks like. | |||
| * @param costFunction Function which defines how bad a Chromosome is | |||
| * @param populationSize Desired population size for population | |||
| * @param maxGenerations Cut off level for number of generations to run | |||
| * @param desiredCost Sufficient cost to terminate program at | |||
| * @param mutationRate Number between [0,1] representing proportion of genes | |||
| * to mutate each generation | |||
| * @param keepNumber Number of Organisms which survive each generation | |||
| * @param newBloodNumber Number of random immigrants to introduce into | |||
| * the population each generation. | |||
| * @returns {*} | |||
| */ | |||
| const runGeneticOptimization = function(geneticChromosome, costFunction, | |||
| populationSize, maxGenerations, | |||
| desiredCost, mutationRate, keepNumber, | |||
| newBloodNumber) | |||
| { | |||
| let population = createRandomPopulation(geneticChromosome, populationSize); | |||
| let generation = 0; | |||
| let bestCost = Number.MAX_VALUE; | |||
| let bestChromosome = geneticChromosome; | |||
| do | |||
| { | |||
| matePopulation(population, populationSize); | |||
| newBlood(population, newBloodNumber); | |||
| mutatePopulation(population, mutationRate); | |||
| let generationResult = naturalSelection(population, keepNumber, costFunction); | |||
| if(bestCost > generationResult.bestFit) | |||
| { | |||
| bestChromosome = generationResult.bestChrom; | |||
| bestCost = generationResult.bestFit; | |||
| } | |||
| population = generationResult.survivors; | |||
| generation++; | |||
| console.log("Generation " + generation + " Best Cost: " + bestCost); | |||
| }while(generation < maxGenerations && bestCost > desiredCost); | |||
| return bestChromosome; | |||
| }; | |||
| ``` | |||
| # Conclusion | |||