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.

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