|
|
@ -0,0 +1,515 @@ |
|
|
|
# Live Simulation |
|
|
|
|
|
|
|
<customHTML /> |
|
|
|
|
|
|
|
# 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 an optimization problem. |
|
|
|
Genetic algorithms are often used in high dimensional problems where the optimal solutions are not apparent. |
|
|
|
Genetic algorithms are commonly used to tune the [hyper-parameters](https://en.wikipedia.org/wiki/Hyperparameter) of a program. |
|
|
|
However, this algorithm can be used in any scenario where you have a function which defines how well a solution is. |
|
|
|
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 a genetic algorithm from scratch you only need **very basic** algebra and a general grasp of evolution. |
|
|
|
|
|
|
|
# Genetic Algorithm |
|
|
|
|
|
|
|
All genetic algorithms typically have a single cycle where you continuously mutate, breed, and select the most optimal solutions. |
|
|
|
I will dive into each section of this algorithm using simple JavaScript code snippets. |
|
|
|
The algorithm which I present is very generic and modular so it should be easy to port into other programming languages and applications. |
|
|
|
|
|
|
|
![Genetic Algorithms Flow Chart](media/GA/GAFlowChart.svg) |
|
|
|
|
|
|
|
|
|
|
|
## Population Creation |
|
|
|
|
|
|
|
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 problems using continuous numbers. |
|
|
|
In this approach, every gene will be a single floating point number ranging between zero and one. |
|
|
|
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 value} = (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 original 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 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 the chromosome as a random copy. |
|
|
|
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 easiest way of |
|
|
|
ensuring that all corresponding chromosomes contain genes with identical extrema. |
|
|
|
|
|
|
|
|
|
|
|
```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 a method to create a random clone of a chromosome. |
|
|
|
|
|
|
|
```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. |
|
|
|
After you define what types of genes are found on each chromosome, you can create an entire population. |
|
|
|
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 |
|
|
|
|
|
|
|
Like all optimization problems, you need a way to evaluate the performance of a particular solution. |
|
|
|
The cost function takes in a chromosome and evaluates how close it got to the ideal solution. |
|
|
|
This particular example it is just computing the [Manhattan Distance](https://en.wiktionary.org/wiki/Manhattan_distance) to a random 2D point. |
|
|
|
I chose two dimensions because it is easy to graph, however, real applications may have dozens of genes on each chromosome. |
|
|
|
|
|
|
|
```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 |
|
|
|
|
|
|
|
Selecting the best performing chromosomes is straightforward after you have a function for evaluating the performance. |
|
|
|
This code snippet also computes the average and best chromosome of the population to make it easier to graph and define |
|
|
|
the stopping point for the algorithm's main loop. |
|
|
|
|
|
|
|
```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]); |
|
|
|
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}; |
|
|
|
}; |
|
|
|
``` |
|
|
|
|
|
|
|
You might be wondering how I sorted the list of JSON objects - not a numerical array. |
|
|
|
I used the following function as a comparator for JavaScript's built in sort function. |
|
|
|
This comparator will compare objects based on a specific attribute that you give it. |
|
|
|
This is a very handy function to include in all of your JavaScript projects for easy sorting. |
|
|
|
|
|
|
|
```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; |
|
|
|
} |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
## Reproduction |
|
|
|
|
|
|
|
The process of reproduction can be broken down into Pairing and Mating. |
|
|
|
|
|
|
|
### Pairing |
|
|
|
|
|
|
|
Pairing is the process of selecting mates to produce offspring. |
|
|
|
A typical approach will separate the population into two segments of mothers and fathers. |
|
|
|
You then randomly pick pairs of mothers and fathers to produce offspring. |
|
|
|
It is ok if one chromosome mates more than once. |
|
|
|
It is just important that you keep this process random. |
|
|
|
|
|
|
|
```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]); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
``` |
|
|
|
|
|
|
|
### Mating |
|
|
|
|
|
|
|
Mating is the actual act of forming new chromosomes/organisms based on your previously selected pairs. |
|
|
|
From my research, there are two major forms of mating: blending, crossover. |
|
|
|
|
|
|
|
Blending is typically the most preferred approach to mating when dealing with continuous variables. |
|
|
|
In this approach you combine the genes of both parents based on a random factor. |
|
|
|
|
|
|
|
$$ |
|
|
|
c_{new} = r * c_{mother} + (1-r) * c_{father} |
|
|
|
$$ |
|
|
|
|
|
|
|
The second offspring simply uses (1-r) for their random factor to adjust the chromosomes. |
|
|
|
|
|
|
|
Crossover is the simplest approach to mating. |
|
|
|
In this process you clone the parents and then you randomly swap *n* of their genes. |
|
|
|
This works fine in some scenarios; however, this severely lacks the genetic diversity of the genes because you now have to solely |
|
|
|
rely on mutations for changes. |
|
|
|
|
|
|
|
```javascript |
|
|
|
/** |
|
|
|
* 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 |
|
|
|
|
|
|
|
Mutations are random changes to an organisms DNA. |
|
|
|
In the scope of genetic algorithms, it helps our population converge on the correct solution. |
|
|
|
|
|
|
|
You can either adjust genes by a factor resulting in a smaller change or, you can |
|
|
|
change the value of the gene to be something completely random. |
|
|
|
Since we are using the blending technique for reproduction, we already have small incremental changes. |
|
|
|
I prefer to use mutations to randomly change the entire gene since it helps prevent the algorithm |
|
|
|
from settling on a local minimum rather than the global minimum. |
|
|
|
|
|
|
|
|
|
|
|
```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 |
|
|
|
|
|
|
|
Immigration or "new blood" is the process of dumping random organisms into your population at each generation. |
|
|
|
This prevents us from getting stuck in a local minimum rather than the global minimum. |
|
|
|
There are more advanced techniques to accomplish this same concept. |
|
|
|
My favorite approach (not implemented here) is raising **x** populations simultaneously and every **y** generations |
|
|
|
you take **z** organisms from each population and move them to another population. |
|
|
|
|
|
|
|
```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 |
|
|
|
|
|
|
|
Now that we have all the ingredients for a genetic algorithm we can piece it together in a simple loop. |
|
|
|
|
|
|
|
```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; |
|
|
|
}; |
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
## Running |
|
|
|
|
|
|
|
Running the program is pretty straight forward after you have your genes and cost function defined. |
|
|
|
You might be wondering if there is an optimal configuration of parameters to use with this algorithm. |
|
|
|
The answer is that it varies based on the particular problem. |
|
|
|
Problems like the one graphed by this website perform very well with a low mutation rate and a high population. |
|
|
|
However, some higher dimensional problems won't even converge on a local answer if you set your mutation rate too low. |
|
|
|
|
|
|
|
```javascript |
|
|
|
let gene1 = new Gene(1,10,10); |
|
|
|
... |
|
|
|
let geneN = new Gene(1,10,0.4); |
|
|
|
let geneList = [gene1,..., geneN]; |
|
|
|
|
|
|
|
let exampleOrganism = new Chromosome(geneList); |
|
|
|
|
|
|
|
costFunction = function(chromosome) |
|
|
|
{ |
|
|
|
var d =...; |
|
|
|
//compute cost |
|
|
|
return d; |
|
|
|
} |
|
|
|
|
|
|
|
runGeneticOptimization(exampleOrganism, costFunction, 100, 50, 0.01, 0.3, 20, 10); |
|
|
|
``` |
|
|
|
|
|
|
|
The complete code for the genetic algorithm and the fancy JavaScript graphs can be found in my [Random Scripts GitHub Repository](https://github.com/jrtechs/RandomScripts). |
|
|
|
In the future I may package this into an [npm](https://www.npmjs.com/) package. |