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.

515 lines
17 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 an optimization problem.
  6. Genetic algorithms are often used in high dimensional problems where the optimal solutions are not apparent.
  7. Genetic algorithms are commonly used to tune the [hyper-parameters](https://en.wikipedia.org/wiki/Hyperparameter) of a program.
  8. However, this algorithm can be used in any scenario where you have a function which defines how well a solution is.
  9. Many people have used genetic algorithms in video games to auto learn the weaknesses of players.
  10. The beautiful part about Genetic Algorithms are their simplicity; you need absolutely no knowledge of linear algebra or calculus.
  11. To implement a genetic algorithm from scratch you only need **very basic** algebra and a general grasp of evolution.
  12. # Genetic Algorithm
  13. All genetic algorithms typically have a single cycle where you continuously mutate, breed, and select the most optimal solutions.
  14. I will dive into each section of this algorithm using simple JavaScript code snippets.
  15. The algorithm which I present is very generic and modular so it should be easy to port into other programming languages and applications.
  16. ![Genetic Algorithms Flow Chart](media/GA/GAFlowChart.svg)
  17. ## Population Creation
  18. The very first thing we need to do is specify a data-structure for storing our genetic information.
  19. In biology, chromosomes are composed of sequences of genes.
  20. Many people run genetic algorithms on binary arrays since they more closely represent DNA.
  21. However, as computer scientists, it is often easier to model problems using continuous numbers.
  22. In this approach, every gene will be a single floating point number ranging between zero and one.
  23. Every type of gene will have a max and min value which represents the absolute extremes of that gene.
  24. This works well for optimization because it allows us to easily limit our search space.
  25. For example, we can specify that "height" gene can only vary between 0 and 90.
  26. To get the actual value of the gene from its \[0-1] value we simple de-normalize it.
  27. $$
  28. g_{real value} = (g_{high}- g_{low})g_{norm} + g_{low}
  29. $$
  30. ```javascript
  31. class Gene
  32. {
  33. /**
  34. * Constructs a new Gene to store in a chromosome.
  35. * @param min minimum value that this gene can store
  36. * @param max value this gene can possibly be
  37. * @param value normalized value
  38. */
  39. constructor(min, max, value)
  40. {
  41. this.min = min;
  42. this.max = max;
  43. this.value = value;
  44. }
  45. /**
  46. * De-normalizes the value of the gene
  47. * @returns {*}
  48. */
  49. getRealValue()
  50. {
  51. return (this.max - this.min) * this.value + this.min;
  52. }
  53. getValue()
  54. {
  55. return this.value;
  56. }
  57. setValue(val)
  58. {
  59. this.value = val;
  60. }
  61. makeClone()
  62. {
  63. return new Gene(this.min, this.max, this.value);
  64. }
  65. makeRandomGene()
  66. {
  67. return new Gene(this.min, this.max, Math.random());
  68. }
  69. }
  70. ```
  71. Now that we have genes, we can create chromosomes.
  72. Chromosomes are simply collections of genes.
  73. Whatever language you make this in, make sure that when you create a new chromosome it
  74. is has a [deep copy](https://en.wikipedia.org/wiki/Object_copying) of the original genetic information rather than a shallow copy.
  75. A shallow copy is when you simple copy the object pointer where a deep copy is actually creating a new object.
  76. If you fail to do a deep copy, you will have weird issues where multiple chromosomes will share the same DNA.
  77. In this class I added helper functions to clone the chromosome as a random copy.
  78. You can only create a new chromosome by cloning because I wanted to keep the program generic and make no assumptions about the domain.
  79. Since you only provide the min/max information for the genes once, cloning an existing chromosome is the easiest way of
  80. ensuring that all corresponding chromosomes contain genes with identical extrema.
  81. ```javascript
  82. class Chromosome
  83. {
  84. /**
  85. * Constructs a chromosome by making a copy of
  86. * a list of genes.
  87. * @param geneArray
  88. */
  89. constructor(geneArray)
  90. {
  91. this.genes = [];
  92. for(let i = 0; i < geneArray.length; i++)
  93. {
  94. this.genes.push(geneArray[i].makeClone());
  95. }
  96. }
  97. getGenes()
  98. {
  99. return this.genes;
  100. }
  101. /**
  102. * Mutates a random gene.
  103. */
  104. mutate()
  105. {
  106. this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random());
  107. }
  108. /**
  109. * Creates a totally new chromosome with same
  110. * genetic structure as this chromosome but different
  111. * values.
  112. * @returns {Chromosome}
  113. */
  114. createRandomChromosome()
  115. {
  116. let geneAr = [];
  117. for(let i = 0; i < this.genes.length; i++)
  118. {
  119. geneAr.push(this.genes[i].makeRandomGene());
  120. }
  121. return new Chromosome(geneAr);
  122. }
  123. }
  124. ```
  125. Creating a random population is pretty straight forward if implemented a method to create a random clone of a chromosome.
  126. ```javascript
  127. /**
  128. * Creates a totally random population based on a desired size
  129. * and a prototypical chromosome.
  130. *
  131. * @param geneticChromosome
  132. * @param populationSize
  133. * @returns {Array}
  134. */
  135. const createRandomPopulation = function(geneticChromosome, populationSize)
  136. {
  137. let population = [];
  138. for(let i = 0; i < populationSize; i++)
  139. {
  140. population.push(geneticChromosome.createRandomChromosome());
  141. }
  142. return population;
  143. };
  144. ```
  145. This is where nearly all the domain information is introduced.
  146. After you define what types of genes are found on each chromosome, you can create an entire population.
  147. In this example all genes contain values ranging between one and ten.
  148. ```javascript
  149. let gene1 = new Gene(1,10,10);
  150. let gene2 = new Gene(1,10,0.4);
  151. let geneList = [gene1, gene2];
  152. let exampleOrganism = new Chromosome(geneList);
  153. let population = createRandomPopulation(genericChromosome, 100);
  154. ```
  155. ## Evaluate Fitness
  156. Like all optimization problems, you need a way to evaluate the performance of a particular solution.
  157. The cost function takes in a chromosome and evaluates how close it got to the ideal solution.
  158. This particular example it is just computing the [Manhattan Distance](https://en.wiktionary.org/wiki/Manhattan_distance) to a random 2D point.
  159. I chose two dimensions because it is easy to graph, however, real applications may have dozens of genes on each chromosome.
  160. ```javascript
  161. let costx = Math.random() * 10;
  162. let costy = Math.random() * 10;
  163. /** Defines the cost as the "distance" to a 2-d point.
  164. * @param chromosome
  165. * @returns {number}
  166. */
  167. const basicCostFunction = function(chromosome)
  168. {
  169. return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) +
  170. Math.abs(chromosome.getGenes()[1].getRealValue() - costy);
  171. };
  172. ```
  173. ## Selection
  174. Selecting the best performing chromosomes is straightforward after you have a function for evaluating the performance.
  175. This code snippet also computes the average and best chromosome of the population to make it easier to graph and define
  176. the stopping point for the algorithm's main loop.
  177. ```javascript
  178. /**
  179. * Function which computes the fitness of everyone in the
  180. * population and returns the most fit survivors. Method
  181. * known as elitism.
  182. *
  183. * @param population
  184. * @param keepNumber
  185. * @param fitnessFunction
  186. * @returns {{average: number,
  187. * survivors: Array, bestFit: Chromosome }}
  188. */
  189. const naturalSelection = function(population, keepNumber, fitnessFunction)
  190. {
  191. let fitnessArray = [];
  192. let total = 0;
  193. for(let i = 0; i < population.length; i++)
  194. {
  195. const fitness = fitnessFunction(population[i]);
  196. fitnessArray.push({fit:fitness, chrom: population[i]});
  197. total+= fitness;
  198. }
  199. fitnessArray.sort(predicateBy("fit"));
  200. let survivors = [];
  201. let bestFitness = fitnessArray[0].fit;
  202. let bestChromosome = fitnessArray[0].chrom;
  203. for(let i = 0; i < keepNumber; i++)
  204. {
  205. survivors.push(fitnessArray[i].chrom);
  206. }
  207. return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome};
  208. };
  209. ```
  210. You might be wondering how I sorted the list of JSON objects - not a numerical array.
  211. I used the following function as a comparator for JavaScript's built in sort function.
  212. This comparator will compare objects based on a specific attribute that you give it.
  213. This is a very handy function to include in all of your JavaScript projects for easy sorting.
  214. ```javascript
  215. /**
  216. * Helper function to sort an array
  217. *
  218. * @param prop name of JSON property to sort by
  219. * @returns {function(*, *): number}
  220. */
  221. function predicateBy(prop)
  222. {
  223. return function(a,b)
  224. {
  225. var result;
  226. if(a[prop] > b[prop])
  227. {
  228. result = 1;
  229. }
  230. else if(a[prop] < b[prop])
  231. {
  232. result = -1;
  233. }
  234. return result;
  235. }
  236. }
  237. ```
  238. ## Reproduction
  239. The process of reproduction can be broken down into Pairing and Mating.
  240. ### Pairing
  241. Pairing is the process of selecting mates to produce offspring.
  242. A typical approach will separate the population into two segments of mothers and fathers.
  243. You then randomly pick pairs of mothers and fathers to produce offspring.
  244. It is ok if one chromosome mates more than once.
  245. It is just important that you keep this process random.
  246. ```javascript
  247. /**
  248. * Randomly everyone in the population
  249. *
  250. * @param population
  251. * @param desiredPopulationSize
  252. */
  253. const matePopulation = function(population, desiredPopulationSize)
  254. {
  255. const originalLength = population.length;
  256. while(population.length < desiredPopulationSize)
  257. {
  258. let index1 = Math.round(Math.random() * (originalLength-1));
  259. let index2 = Math.round(Math.random() * (originalLength-1));
  260. if(index1 !== index2)
  261. {
  262. const babies = breed(population[index1], population[index2]);
  263. population.push(babies[0]);
  264. population.push(babies[1]);
  265. }
  266. }
  267. };
  268. ```
  269. ### Mating
  270. Mating is the actual act of forming new chromosomes/organisms based on your previously selected pairs.
  271. From my research, there are two major forms of mating: blending, crossover.
  272. Blending is typically the most preferred approach to mating when dealing with continuous variables.
  273. In this approach you combine the genes of both parents based on a random factor.
  274. $$
  275. c_{new} = r * c_{mother} + (1-r) * c_{father}
  276. $$
  277. The second offspring simply uses (1-r) for their random factor to adjust the chromosomes.
  278. Crossover is the simplest approach to mating.
  279. In this process you clone the parents and then you randomly swap *n* of their genes.
  280. This works fine in some scenarios; however, this severely lacks the genetic diversity of the genes because you now have to solely
  281. rely on mutations for changes.
  282. ```javascript
  283. /**
  284. * Mates two chromosomes using the blending method
  285. * and returns a list of 2 offspring.
  286. * @param father
  287. * @param mother
  288. * @returns {Chromosome[]}
  289. */
  290. const breed = function(father, mother)
  291. {
  292. let son = new Chromosome(father.getGenes());
  293. let daughter = new Chromosome(mother.getGenes());
  294. for(let i = 0;i < son.getGenes().length; i++)
  295. {
  296. let blendCoef = Math.random();
  297. blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef);
  298. }
  299. return [son, daughter];
  300. };
  301. /**
  302. * Blends two genes together based on a random blend
  303. * coefficient.
  304. **/
  305. const blendGene = function(gene1, gene2, blendCoef)
  306. {
  307. let value1 = (blendCoef * gene1.getValue()) +
  308. (gene2.getValue() * (1- blendCoef));
  309. let value2 = ((1-blendCoef) * gene1.getValue()) +
  310. (gene2.getValue() * blendCoef);
  311. gene1.setValue(value1);
  312. gene2.setValue(value2);
  313. };
  314. ```
  315. ## Mutation
  316. Mutations are random changes to an organisms DNA.
  317. In the scope of genetic algorithms, it helps our population converge on the correct solution.
  318. You can either adjust genes by a factor resulting in a smaller change or, you can
  319. change the value of the gene to be something completely random.
  320. Since we are using the blending technique for reproduction, we already have small incremental changes.
  321. I prefer to use mutations to randomly change the entire gene since it helps prevent the algorithm
  322. from settling on a local minimum rather than the global minimum.
  323. ```javascript
  324. /**
  325. * Randomly mutates the population
  326. **/
  327. const mutatePopulation = function(population, mutatePercentage)
  328. {
  329. if(population.length >= 2)
  330. {
  331. let mutations = mutatePercentage *
  332. population.length *
  333. population[0].getGenes().length;
  334. for(let i = 0; i < mutations; i++)
  335. {
  336. population[i].mutate();
  337. }
  338. }
  339. else
  340. {
  341. console.log("Error, population too small to mutate");
  342. }
  343. };
  344. ```
  345. ## Immigration
  346. Immigration or "new blood" is the process of dumping random organisms into your population at each generation.
  347. This prevents us from getting stuck in a local minimum rather than the global minimum.
  348. There are more advanced techniques to accomplish this same concept.
  349. My favorite approach (not implemented here) is raising **x** populations simultaneously and every **y** generations
  350. you take **z** organisms from each population and move them to another population.
  351. ```javascript
  352. /**
  353. * Introduces x random chromosomes to the population.
  354. * @param population
  355. * @param immigrationSize
  356. */
  357. const newBlood = function(population, immigrationSize)
  358. {
  359. for(let i = 0; i < immigrationSize; i++)
  360. {
  361. let geneticChromosome = population[0];
  362. population.push(geneticChromosome.createRandomChromosome());
  363. }
  364. };
  365. ```
  366. ## Putting It All Together
  367. Now that we have all the ingredients for a genetic algorithm we can piece it together in a simple loop.
  368. ```javascript
  369. /**
  370. * Runs the genetic algorithm by going through the processes of
  371. * natural selection, mutation, mating, and immigrations. This
  372. * process will continue until an adequately performing chromosome
  373. * is found or a generation threshold is passed.
  374. *
  375. * @param geneticChromosome Prototypical chromosome: used so algo knows
  376. * what the dna of the population looks like.
  377. * @param costFunction Function which defines how bad a Chromosome is
  378. * @param populationSize Desired population size for population
  379. * @param maxGenerations Cut off level for number of generations to run
  380. * @param desiredCost Sufficient cost to terminate program at
  381. * @param mutationRate Number between [0,1] representing proportion of genes
  382. * to mutate each generation
  383. * @param keepNumber Number of Organisms which survive each generation
  384. * @param newBloodNumber Number of random immigrants to introduce into
  385. * the population each generation.
  386. * @returns {*}
  387. */
  388. const runGeneticOptimization = function(geneticChromosome, costFunction,
  389. populationSize, maxGenerations,
  390. desiredCost, mutationRate, keepNumber,
  391. newBloodNumber)
  392. {
  393. let population = createRandomPopulation(geneticChromosome, populationSize);
  394. let generation = 0;
  395. let bestCost = Number.MAX_VALUE;
  396. let bestChromosome = geneticChromosome;
  397. do
  398. {
  399. matePopulation(population, populationSize);
  400. newBlood(population, newBloodNumber);
  401. mutatePopulation(population, mutationRate);
  402. let generationResult = naturalSelection(population, keepNumber, costFunction);
  403. if(bestCost > generationResult.bestFit)
  404. {
  405. bestChromosome = generationResult.bestChrom;
  406. bestCost = generationResult.bestFit;
  407. }
  408. population = generationResult.survivors;
  409. generation++;
  410. console.log("Generation " + generation + " Best Cost: " + bestCost);
  411. }while(generation < maxGenerations && bestCost > desiredCost);
  412. return bestChromosome;
  413. };
  414. ```
  415. ## Running
  416. Running the program is pretty straight forward after you have your genes and cost function defined.
  417. You might be wondering if there is an optimal configuration of parameters to use with this algorithm.
  418. The answer is that it varies based on the particular problem.
  419. Problems like the one graphed by this website perform very well with a low mutation rate and a high population.
  420. However, some higher dimensional problems won't even converge on a local answer if you set your mutation rate too low.
  421. ```javascript
  422. let gene1 = new Gene(1,10,10);
  423. ...
  424. let geneN = new Gene(1,10,0.4);
  425. let geneList = [gene1,..., geneN];
  426. let exampleOrganism = new Chromosome(geneList);
  427. costFunction = function(chromosome)
  428. {
  429. var d =...;
  430. //compute cost
  431. return d;
  432. }
  433. runGeneticOptimization(exampleOrganism, costFunction, 100, 50, 0.01, 0.3, 20, 10);
  434. ```
  435. 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).
  436. In the future I may package this into an [npm](https://www.npmjs.com/) package.