Personal blog written from scratch using Node.js, Bootstrap, and MySQL. https://jrtechs.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

427 lines
12 KiB

  1. # Live Simulation
  2. <customHTML />
  3. # Background and Theory
  4. Since you stumbled upon this article, you might be wondering what the heck genetic algorithms are.
  5. To put it simply: genetic algorithms employ the same tactics used in natural selection to find an optimal solution to a problem.
  6. Genetic algorithms are often used in high dimensional problems where the optimal solution is not apparent.
  7. A common use case of genetic algorithms are to tune [hyper-parameters](https://en.wikipedia.org/wiki/Hyperparameter) in a program.
  8. However, this algorithm can be used in any scenario where you have a function which defines how well something performed and you would
  9. like to find the absolute minimum.
  10. Many people have used genetic algorithms in video games to auto learn the weaknesses of players.
  11. The beautiful part about Genetic Algorithms are their simplicity; you need absolutely no knowledge of linear algebra or calculus.
  12. To implement one all you need to know is **very basic** algebra and a general grasp of evolution.
  13. # Genetic Algorithm
  14. All genetic algorithms typically follow the pattern of evaluation mating, mutation, and selection.
  15. I will dive into each section of this algorithm using JavaScript code snippets.
  16. The algorithm which I present is very generic and modular so it should be easy to port into other applications and programming languages.
  17. ![Genetic Algorithms Flow Chart](media/GA/GAFlowChart.svg)
  18. ## Create Initial Population
  19. The very first thing we need to do is specify a data-structure for storing our genetic information.
  20. In biology chromosomes are composed of sequences of genes.
  21. Many people run genetic algorithms on binary arrays since they more closely represent DNA.
  22. However, as computer scientists, it is often easier to model things as continuous numbers.
  23. In this approach, every Gene will be a single floating point number ranging between zero and one.
  24. This keeps things really easy because it is very simple perform any genetic "operation".
  25. Every type of Gene will have a max and min value which represents the absolute extremes of that gene.
  26. This works well for optimization because it allows us to easily limit our search space.
  27. For example, we can specify that "height" gene can only vary between 0 and 90.
  28. To get the actual value of the gene from its \[0-1] value we simple de-normalize it.
  29. $$
  30. g_{real ralue} = (g_{high}- g_{low})g_{norm} + g_{low}
  31. $$
  32. ```javascript
  33. class Gene
  34. {
  35. /**
  36. * Constructs a new Gene to store in a chromosome.
  37. * @param min minimum value that this gene can store
  38. * @param max value this gene can possibly be
  39. * @param value normalized value
  40. */
  41. constructor(min, max, value)
  42. {
  43. this.min = min;
  44. this.max = max;
  45. this.value = value;
  46. }
  47. /**
  48. * De-normalizes the value of the gene
  49. * @returns {*}
  50. */
  51. getRealValue()
  52. {
  53. return (this.max - this.min) * this.value + this.min;
  54. }
  55. getValue()
  56. {
  57. return this.value;
  58. }
  59. setValue(val)
  60. {
  61. this.value = val;
  62. }
  63. makeClone()
  64. {
  65. return new Gene(this.min, this.max, this.value);
  66. }
  67. makeRandomGene()
  68. {
  69. return new Gene(this.min, this.max, Math.random());
  70. }
  71. }
  72. ```
  73. Now that we have genes, we can create Chromosomes.
  74. Chromosomes are simply collections of genes.
  75. Whatever language you make this in, make sure that when you create a new Chromosome it
  76. is has a [deep copy](https://en.wikipedia.org/wiki/Object_copying) of the genetic information rather than a shallow copy.
  77. A shallow copy is when you simple copy the object pointer where a deep copy is actually creating a full new object.
  78. If you fail to do a deep copy, you will have weird issues where multiple Chromosomes will share the same DNA.
  79. In this class I added helper functions to clone and create a new Chromosome.
  80. You can only create a new chromosome by cloning because I wanted to keep the program generic and make no assumptions about the domain.
  81. 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.
  82. ```javascript
  83. class Chromosome
  84. {
  85. /**
  86. * Constructs a chromosome by making a copy of
  87. * a list of genes.
  88. * @param geneArray
  89. */
  90. constructor(geneArray)
  91. {
  92. this.genes = [];
  93. for(let i = 0; i < geneArray.length; i++)
  94. {
  95. this.genes.push(geneArray[i].makeClone());
  96. }
  97. }
  98. getGenes()
  99. {
  100. return this.genes;
  101. }
  102. /**
  103. * Mutates a random gene.
  104. */
  105. mutate()
  106. {
  107. this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random());
  108. }
  109. /**
  110. * Creates a totally new chromosome with same
  111. * genetic structure as this chromosome but different
  112. * values.
  113. * @returns {Chromosome}
  114. */
  115. createRandomChromosome()
  116. {
  117. let geneAr = [];
  118. for(let i = 0; i < this.genes.length; i++)
  119. {
  120. geneAr.push(this.genes[i].makeRandomGene());
  121. }
  122. return new Chromosome(geneAr);
  123. }
  124. }
  125. ```
  126. Creating a random population is pretty straight forward if implemented this method of random cloning in the Chromosome and Gene class.
  127. ```javascript
  128. /**
  129. * Creates a totally random population based on a desired size
  130. * and a prototypical chromosome.
  131. *
  132. * @param geneticChromosome
  133. * @param populationSize
  134. * @returns {Array}
  135. */
  136. const createRandomPopulation = function(geneticChromosome, populationSize)
  137. {
  138. let population = [];
  139. for(let i = 0; i < populationSize; i++)
  140. {
  141. population.push(geneticChromosome.createRandomChromosome());
  142. }
  143. return population;
  144. };
  145. ```
  146. This is where nearly all the domain information is introduced.
  147. To create an entire population, you simply need to define what types of genes are found on each chromosome.
  148. In this example all genes contain values ranging between one and ten.
  149. ```javascript
  150. let gene1 = new Gene(1,10,10);
  151. let gene2 = new Gene(1,10,0.4);
  152. let geneList = [gene1, gene2];
  153. let exampleOrganism = new Chromosome(geneList);
  154. let population = createRandomPopulation(genericChromosome, 100);
  155. ```
  156. ## Evaluate Fitness
  157. ```javascript
  158. let costx = Math.random() * 10;
  159. let costy = Math.random() * 10;
  160. /** Defines the cost as the "distance" to a 2-d point.
  161. * @param chromosome
  162. * @returns {number}
  163. */
  164. const basicCostFunction = function(chromosome)
  165. {
  166. return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) +
  167. Math.abs(chromosome.getGenes()[1].getRealValue() - costy);
  168. };
  169. ```
  170. ## Selection
  171. ```javascript
  172. /**
  173. * Function which computes the fitness of everyone in the
  174. * population and returns the most fit survivors. Method
  175. * known as elitism.
  176. *
  177. * @param population
  178. * @param keepNumber
  179. * @param fitnessFunction
  180. * @returns {{average: number,
  181. * survivors: Array, bestFit: Chromosome }}
  182. */
  183. const naturalSelection = function(population, keepNumber, fitnessFunction)
  184. {
  185. let fitnessArray = [];
  186. let total = 0;
  187. for(let i = 0; i < population.length; i++)
  188. {
  189. const fitness = fitnessFunction(population[i]);
  190. console.log(fitness);
  191. fitnessArray.push({fit:fitness, chrom: population[i]});
  192. total+= fitness;
  193. }
  194. fitnessArray.sort(predicateBy("fit"));
  195. let survivors = [];
  196. let bestFitness = fitnessArray[0].fit;
  197. let bestChromosome = fitnessArray[0].chrom;
  198. for(let i = 0; i < keepNumber; i++)
  199. {
  200. survivors.push(fitnessArray[i].chrom);
  201. }
  202. return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome};
  203. };
  204. ```
  205. ```javascript
  206. /**
  207. * Helper function to sort an array
  208. *
  209. * @param prop name of JSON property to sort by
  210. * @returns {function(*, *): number}
  211. */
  212. function predicateBy(prop)
  213. {
  214. return function(a,b)
  215. {
  216. var result;
  217. if(a[prop] > b[prop])
  218. {
  219. result = 1;
  220. }
  221. else if(a[prop] < b[prop])
  222. {
  223. result = -1;
  224. }
  225. return result;
  226. }
  227. }
  228. ```
  229. ## Mating
  230. ```javascript
  231. /**
  232. * Randomly everyone in the population
  233. *
  234. * @param population
  235. * @param desiredPopulationSize
  236. */
  237. const matePopulation = function(population, desiredPopulationSize)
  238. {
  239. const originalLength = population.length;
  240. while(population.length < desiredPopulationSize)
  241. {
  242. let index1 = Math.round(Math.random() * (originalLength-1));
  243. let index2 = Math.round(Math.random() * (originalLength-1));
  244. if(index1 !== index2)
  245. {
  246. const babies = breed(population[index1], population[index2]);
  247. population.push(babies[0]);
  248. population.push(babies[1]);
  249. }
  250. }
  251. };
  252. /**
  253. * Mates two chromosomes using the blending method
  254. * and returns a list of 2 offspring.
  255. * @param father
  256. * @param mother
  257. * @returns {Chromosome[]}
  258. */
  259. const breed = function(father, mother)
  260. {
  261. let son = new Chromosome(father.getGenes());
  262. let daughter = new Chromosome(mother.getGenes());
  263. for(let i = 0;i < son.getGenes().length; i++)
  264. {
  265. let blendCoef = Math.random();
  266. blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef);
  267. }
  268. return [son, daughter];
  269. };
  270. /**
  271. * Blends two genes together based on a random blend
  272. * coefficient.
  273. **/
  274. const blendGene = function(gene1, gene2, blendCoef)
  275. {
  276. let value1 = (blendCoef * gene1.getValue()) +
  277. (gene2.getValue() * (1- blendCoef));
  278. let value2 = ((1-blendCoef) * gene1.getValue()) +
  279. (gene2.getValue() * blendCoef);
  280. gene1.setValue(value1);
  281. gene2.setValue(value2);
  282. };
  283. ```
  284. ## Mutation
  285. ```javascript
  286. /**
  287. * Randomly mutates the population
  288. **/
  289. const mutatePopulation = function(population, mutatePercentage)
  290. {
  291. if(population.length >= 2)
  292. {
  293. let mutations = mutatePercentage *
  294. population.length *
  295. population[0].getGenes().length;
  296. for(let i = 0; i < mutations; i++)
  297. {
  298. population[i].mutate();
  299. }
  300. }
  301. else
  302. {
  303. console.log("Error, population too small to mutate");
  304. }
  305. };
  306. ```
  307. ## Immigration
  308. ```javascript
  309. /**
  310. * Introduces x random chromosomes to the population.
  311. * @param population
  312. * @param immigrationSize
  313. */
  314. const newBlood = function(population, immigrationSize)
  315. {
  316. for(let i = 0; i < immigrationSize; i++)
  317. {
  318. let geneticChromosome = population[0];
  319. population.push(geneticChromosome.createRandomChromosome());
  320. }
  321. };
  322. ```
  323. ## Putting It All Together
  324. ```javascript
  325. /**
  326. * Runs the genetic algorithm by going through the processes of
  327. * natural selection, mutation, mating, and immigrations. This
  328. * process will continue until an adequately performing chromosome
  329. * is found or a generation threshold is passed.
  330. *
  331. * @param geneticChromosome Prototypical chromosome: used so algo knows
  332. * what the dna of the population looks like.
  333. * @param costFunction Function which defines how bad a Chromosome is
  334. * @param populationSize Desired population size for population
  335. * @param maxGenerations Cut off level for number of generations to run
  336. * @param desiredCost Sufficient cost to terminate program at
  337. * @param mutationRate Number between [0,1] representing proportion of genes
  338. * to mutate each generation
  339. * @param keepNumber Number of Organisms which survive each generation
  340. * @param newBloodNumber Number of random immigrants to introduce into
  341. * the population each generation.
  342. * @returns {*}
  343. */
  344. const runGeneticOptimization = function(geneticChromosome, costFunction,
  345. populationSize, maxGenerations,
  346. desiredCost, mutationRate, keepNumber,
  347. newBloodNumber)
  348. {
  349. let population = createRandomPopulation(geneticChromosome, populationSize);
  350. let generation = 0;
  351. let bestCost = Number.MAX_VALUE;
  352. let bestChromosome = geneticChromosome;
  353. do
  354. {
  355. matePopulation(population, populationSize);
  356. newBlood(population, newBloodNumber);
  357. mutatePopulation(population, mutationRate);
  358. let generationResult = naturalSelection(population, keepNumber, costFunction);
  359. if(bestCost > generationResult.bestFit)
  360. {
  361. bestChromosome = generationResult.bestChrom;
  362. bestCost = generationResult.bestFit;
  363. }
  364. population = generationResult.survivors;
  365. generation++;
  366. console.log("Generation " + generation + " Best Cost: " + bestCost);
  367. }while(generation < maxGenerations && bestCost > desiredCost);
  368. return bestChromosome;
  369. };
  370. ```
  371. # Conclusion