├── .gitignore ├── README.md ├── hello.js ├── images ├── genetic-programming-1.png ├── genetic-programming-2.png ├── genetic-programming-3.png ├── genetic-programming-4.png ├── genetic-programming-5.png ├── genetic-programming-6.png ├── genetic-programming-7.png ├── genetic-programming-8.png ├── genetic-programming-variable-1.png ├── genetic-programming-variable-2.png ├── genetic-programming-variable-3.png └── genetic-programming-variable-4.png ├── package.json ├── prefix.js └── variable.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Dependency directories 12 | node_modules 13 | 14 | # Optional npm cache directory 15 | .npm 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Genetic Programming 2 | ------------------- 3 | 4 | Read the full article at: 5 | [Creating Self-Assembling Code with Genetic Programming](http://www.primaryobjects.com/2018/09/03/creating-self-assembling-code-with-genetic-programming/) 6 | 7 | Genetic programming is a branch of artificial intelligence that uses evolutionary computation to generate computer programs for solving a particular task. The computer programs are represented by encoded genes and slightly modified at each time-step via cross-over and mutation until a resulting program is found that solves the given task to a degree of accuracy. 8 | 9 | This project hosts a demo of several genetic algorithm and genetic programming examples written in JavaScript with the [genetic-js](https://github.com/subprotocol/genetic-js) library. 10 | 11 | - Hello World 12 | 13 | *A traditional "hello world" example for genetic algorithms, where the goal is to output the text, "Hello World".* 14 | 15 | ![Genetic programming hello world](images/genetic-programming-1.png) 16 | 17 | - Prefix 18 | 19 | *A genetic programming example where a computer program is evolved to represent a specific mathematical expression, in prefix notation format.* 20 | 21 | ![Genetic programming for a mathematical equation](images/genetic-programming-4.png) 22 | 23 | - Variable 24 | 25 | *A genetic programming example where a computer program is evolved to represent a mathematical expression containing both numbers and variables (i.e., formulas ) in prefix notation format.* 26 | 27 | ![Genetic programming for a mathematical equation with variable values](images/genetic-programming-7.png) 28 | 29 | ## Genetic Algorithms in JavaScript 30 | 31 | The [genetic-js](https://github.com/subprotocol/genetic-js) library facilitates the construction of a genetic algorithm in JavaScript. It provides the necessary functions and handlers for defining a genetic algorithm, its initialization, a fitness function, and a means for ranking solutions. In this manner, a genetic algorithm can be developed for a large variety of tasks, and as shown in this project, genetic programming. 32 | 33 | ## Initialization 34 | 35 | Usage of the genetic algorithm library in node.js is relatively straight-forward. You can begin by instantiating the genetic class and selecting an optimization method and selection algorithm. 36 | 37 | ```js 38 | const Genetic = require('genetic-js'); 39 | 40 | const genetic = Genetic.create(); 41 | genetic.optimize = Genetic.Optimize.Maximize; 42 | genetic.select1 = Genetic.Select1.Tournament2; 43 | genetic.select2 = Genetic.Select2.Tournament2; 44 | ``` 45 | 46 | In the above example, we've instantiated the genetic class and selected to maximize the fitness among the pool of genomes. The best performing results will continue on to the next epoch of evolution. 47 | 48 | ## Creating the Initial Population 49 | 50 | After creating the initial genetic algorithm class, you can provide a function of creating the initial pool of genomes to begin evolution from. This set of genomes is usually randomly produced, with some of the genomes performing better than others for solving the desired task. It's these better performing genomes that will rank high and move on to the next epoch of evolution, creating the next generation. 51 | 52 | ```js 53 | genetic.seed = function() { 54 | // Return a random string for this particular genome. 55 | return 'abc'; 56 | } 57 | ``` 58 | 59 | ## Crossover 60 | 61 | When creating the next population of genomes after an epoch, the methods of crossover and mutation are used to slightly modify the existing genomes and create children. Crossover is the process of creating a child genome by taking two parent genomes and mating them together to produce offspring. This is done by taking a series of genes from parent 1 and parent 2 and combining them together to form a child. Additional mutation can be used on the child to slightly tweak the genes further, which has the potential to produce beneficial (or worse) functionality for the child. 62 | 63 | A simplified example for performing crossover on two parent genomes to produce two child genomes is shown below. 64 | 65 | 66 | ```js 67 | genetic.crossover = function(parent1, parent2) { 68 | let index = Math.floor(Math.random() * parent1.length); 69 | 70 | const parent1Left = parent1.substr(0, index); 71 | const parent1Right = parent1.substr(index); 72 | 73 | const parent2Left = parent2.substr(0, index); 74 | const parent2Right = parent2.substr(index); 75 | 76 | // Crossover the left and right side. 77 | let child1 = parent1Left + parent2Right; 78 | let child2 = parent2Left + parent1Right; 79 | 80 | return [child1, child2]; 81 | } 82 | ``` 83 | 84 | In the above code, we're simply splitting the two parent genomes at a specific index. We then take the left portion of one parent and combine it with the right portion of the other parent to produce a child offspring. We repeat this a second time for the second child offspring, by taking the right portion of the first parent and combine it with the left portion of the second parent. 85 | 86 | ## Mutation 87 | 88 | After performing crossover, each child genome can be slightly modified a bit further by mutating specific genes. This may or may not produce beneficial effects that can boost the child's resulting fitness score. Mutation can be performed by randomly altering the value of a gene or by inserting and removing genes at specific indices within the genome. 89 | 90 | ```js 91 | genetic.mutate = function(entity) { 92 | const replaceAt = (str, index, replacement) => { 93 | return str.substr(0, index) + replacement + str.substr(index + replacement.length); 94 | }; 95 | 96 | let index = Math.floor(Math.random() * entity.length); 97 | 98 | // Mutate the instruction up or down by 1 place (according to the direction). 99 | const direction = Math.floor(Math.random() * 2); 100 | return replaceAt(entity, index, String.fromCharCode(entity.charCodeAt(index) + (direction ? 1 : -1))); 101 | } 102 | ``` 103 | 104 | In the above code, we're performing mutation by replacing a randomly selected single gene within the child. We modify the gene by either incrementing or decrementing it by a value of 1. In the case of letters representing the genes, this would select either the previous or next letter within the alphabet. For numbers, this may add or subtract some fraction from the original value. 105 | 106 | ## Fitness 107 | 108 | Scoring the fitness for a genome is the most important task of developing a genetic algorithm, as this contains the core logic for guiding which genomes are to be selected for producing the next generation of genomes and thus, solving a particular task. A granular method of scoring the fitness for each genome is required in order to allow the genetic algorithm to rank genomes accordingly, even if they don't solve the entire given task. By assigning partial credit to those genomes which perform just slightly better than others, the evolution process can select better performing programs in a granular fashion until it arrives at a solution. 109 | 110 | ```js 111 | genetic.fitness = function(entity) { 112 | let score = 0; 113 | 114 | for (let i=0; i { 11 | var text = ""; 12 | var charset = "abcdefghijklmnopqrstuvwxyz0123456789"; 13 | 14 | for (let i=0; i { 26 | return str.substr(0, index) + replacement + str.substr(index + replacement.length); 27 | }; 28 | 29 | let index = Math.floor(Math.random() * entity.length); 30 | 31 | const direction = Math.floor(Math.random() * 2); 32 | return replaceAt(entity, index, String.fromCharCode(entity.charCodeAt(index) + (direction ? 1 : -1))); 33 | } 34 | 35 | genetic.crossover = function(parent1, parent2) { 36 | let index = Math.floor(Math.random() * parent1.length); 37 | 38 | const parent1Left = parent1.substr(0, index); 39 | const parent1Right = parent1.substr(index); 40 | 41 | const parent2Left = parent2.substr(0, index); 42 | const parent2Right = parent2.substr(index); 43 | 44 | // Crossover the left or right side. 45 | let direction = Math.floor(Math.random() * 2); 46 | let child1 = ''; 47 | let child2 = ''; 48 | 49 | if (direction === 0) { 50 | child1 = parent1Left + parent2Right; 51 | child2 = parent2Left + parent1Right; 52 | } 53 | else { 54 | child1 = parent1Right + parent2Left; 55 | child2 = parent2Right + parent1Left; 56 | } 57 | 58 | return [child1, child2]; 59 | } 60 | 61 | genetic.fitness = function(entity) { 62 | let score = 0; 63 | 64 | for (let i=0; i= 0; j--) { 80 | const val = expr[j]; 81 | 82 | // Push operated to stack. 83 | if (!this.isOperator(val)) { 84 | stack.push(parseInt(val)); 85 | } 86 | else { 87 | // Operator found. Pop two elements from the stack. 88 | const a = stack.pop(); 89 | const b = stack.pop(); 90 | stack.push(this.compute(a, this.symbolToOperator(val), b)); 91 | } 92 | } 93 | 94 | return stack[0]; 95 | }, 96 | 97 | replaceAt: function(str, index, replacement) { 98 | return str.substr(0, index) + replacement + str.substr(index + replacement.length); 99 | }, 100 | 101 | replaceAtIndex: function(input, index, search, replace) { 102 | return input.slice(0, index) + input.slice(index).replace(search, replace) 103 | } 104 | } 105 | 106 | genetic.seed = function() { 107 | const getNode = () => { 108 | let isFunction = Math.floor(Math.random() * 2); 109 | return isFunction ? this.userData.manager.operators[Math.floor(Math.random() * this.userData.manager.operators.length)] : this.userData.manager.values[Math.floor(Math.random() * this.userData.manager.values.length)]; 110 | }; 111 | 112 | const tree = () => { 113 | let result = []; 114 | 115 | const node = getNode(); 116 | result.push(node); 117 | 118 | if (this.userData.manager.isOperator(node)) { 119 | // This node is a function, so generate two child nodes. 120 | const left = tree(); 121 | const right = tree(); 122 | 123 | result = result.concat(left); 124 | result = result.concat(right); 125 | } 126 | 127 | return result; 128 | }; 129 | 130 | return tree().join(''); 131 | } 132 | 133 | genetic.mutate = function(entity) { 134 | let result = entity; 135 | let index = Math.floor(Math.random() * entity.length); 136 | 137 | if (this.userData.manager.isOperator(entity[index])) { 138 | // Replace with an operator. 139 | let r = Math.floor(Math.random() * this.userData.manager.operators.length); 140 | result = this.userData.manager.replaceAt(entity, index, this.userData.manager.operators[r]); 141 | } 142 | else { 143 | // Replace with a value. 144 | let r = Math.floor(Math.random() * this.userData.manager.values.length); 145 | result = this.userData.manager.replaceAt(entity, index, this.userData.manager.values[r]); 146 | } 147 | 148 | return result; 149 | } 150 | 151 | genetic.crossover = function(parent1, parent2) { 152 | const index1 = Math.floor(Math.random() * parent1.length); 153 | const index2 = Math.floor(Math.random() * parent2.length); 154 | 155 | const subtree1 = this.userData.manager.subtreePrefix(parent1, index1).expression; 156 | const subtree2 = this.userData.manager.subtreePrefix(parent2, index2).expression; 157 | 158 | // Copy subtree2 to parent1 at index1. 159 | const child1 = this.userData.manager.replaceAtIndex(parent1, index1, subtree1, subtree2); 160 | // Copy subtree1 to parent2 at index2. 161 | const child2 = this.userData.manager.replaceAtIndex(parent2, index2, subtree2, subtree1); 162 | 163 | return [child1, child2]; 164 | } 165 | 166 | genetic.fitness = function(entity) { 167 | const fitness = this.userData.manager.evaluatePrefix(entity); 168 | 169 | return this.userData.solution - Math.abs(this.userData.solution - fitness); 170 | } 171 | 172 | genetic.generation = function(pop, generation, stats) { 173 | return pop[0].fitness !== this.userData.solution; 174 | } 175 | 176 | genetic.notification = function(pop, generation, stats, isDone) { 177 | const value = pop[0].entity; 178 | 179 | console.log(`Generation ${generation}, Best Fitness ${stats.maximum}, Best genome: ${value}`); 180 | 181 | if (isDone) { 182 | console.log(`Result: ${this.userData.manager.evaluatePrefix(value)}`); 183 | } 184 | } 185 | 186 | genetic.evolve({ 187 | iterations: 100000, 188 | size: 100, 189 | crossover: 0.3, 190 | mutation: 0.3, 191 | skip: 50 /* frequency for notifications */ 192 | }, { 193 | solution: 123456, 194 | manager: utilityManager 195 | }) -------------------------------------------------------------------------------- /variable.js: -------------------------------------------------------------------------------- 1 | // https://github.com/subprotocol/genetic-js/blob/master/examples/string-solver.html 2 | const Genetic = require('genetic-js'); 3 | 4 | const genetic = Genetic.create(); 5 | genetic.optimize = Genetic.Optimize.Maximize; 6 | genetic.select1 = Genetic.Select1.Tournament2; 7 | genetic.select2 = Genetic.Select2.Tournament2; 8 | 9 | const utilityManager = { 10 | operators: '+-*/', 11 | values: '0123456789x', 12 | 13 | isOperator: function(val) { 14 | return this.operators.includes(val); 15 | }, 16 | 17 | plus: function(a, b, variables = {}) { return (variables[a] || a) + (variables[b] || b); }, 18 | minus: function(a, b, variables = {}) { return (variables[a] || a) - (variables[b] || b); }, 19 | multiply: function(a, b, variables = {}) { return (variables[a] || a) * (variables[b] || b); }, 20 | divide: function(a, b, variables = {}) { return (variables[a] || a) / (variables[b] || b); }, 21 | 22 | compute: function(a, op, b, variables = {}) { 23 | return op ? op(a, b, variables) : null; 24 | }, 25 | 26 | symbolToOperator: function(symbol) { 27 | switch (symbol) { 28 | case '+': return this.plus; 29 | case '-': return this.minus; 30 | case '*': return this.multiply; 31 | case '/': return this.divide; 32 | } 33 | }, 34 | 35 | subtreePrefix: function(expr, index) { 36 | const parts = expr.split(''); 37 | let val = parts[index]; 38 | const opStack = []; // Start with the node at the index. 39 | const valStack = []; 40 | let valCount = 0; 41 | let i = index + 1; 42 | 43 | if (this.isOperator(val)) { 44 | opStack.push(val); 45 | } 46 | else { 47 | valStack.push(val); 48 | } 49 | 50 | while (opStack.length && i < parts.length) { 51 | val = parts[i]; 52 | 53 | if (!this.isOperator(val) && valCount) { 54 | val = parseInt(val); // Swap variables with the value 1 for subtree extraction, since the actual value doesn't matter. 55 | val = val || 1; 56 | 57 | valStack.push(this.compute(valStack.pop(), this.symbolToOperator(opStack.pop()), val)); 58 | } 59 | else if (this.isOperator(val)) { 60 | opStack.push(val); 61 | valCount = 0; 62 | } 63 | else { 64 | val = parseInt(val); // Swap variables with the value 1 for subtree extraction, since the actual value doesn't matter. 65 | val = val || 1; 66 | 67 | valStack.push(val); 68 | valCount++; 69 | } 70 | 71 | i++; 72 | } 73 | 74 | if (Math.abs(index - i) % 2 === 0) { 75 | i--; 76 | } 77 | 78 | return { expression: expr.substring(index, i), start: index, end: i - 1 }; 79 | }, 80 | 81 | evaluatePrefix: function(expr, variables = {}) { 82 | const parts = expr.split(''); 83 | const stack = []; 84 | 85 | for (let j=expr.length - 1; j >= 0; j--) { 86 | const val = variables[expr[j]] || expr[j]; 87 | 88 | // Push operated to stack. 89 | if (!this.isOperator(val)) { 90 | stack.push(parseInt(val)); 91 | } 92 | else { 93 | // Operator found. Pop two elements from the stack. 94 | const a = stack.pop(); 95 | const b = stack.pop(); 96 | stack.push(this.compute(a, this.symbolToOperator(val), b)); 97 | } 98 | } 99 | 100 | return stack[0]; 101 | }, 102 | 103 | replaceAt: function(str, index, replacement) { 104 | return str.substr(0, index) + replacement + str.substr(index + replacement.length); 105 | }, 106 | 107 | replaceAtIndex: function(input, index, search, replace) { 108 | return input.slice(0, index) + input.slice(index).replace(search, replace) 109 | } 110 | } 111 | 112 | genetic.seed = function() { 113 | const getNode = (isValue) => { 114 | let isFunction = isValue ? 0 : Math.floor(Math.random() * 2); 115 | return isFunction ? this.userData.manager.operators[Math.floor(Math.random() * this.userData.manager.operators.length)] : this.userData.manager.values[Math.floor(Math.random() * this.userData.manager.values.length)]; 116 | }; 117 | 118 | const tree = (maxDepth, depth = 0) => { 119 | let result = []; 120 | 121 | const node = getNode(depth > maxDepth); 122 | result.push(node); 123 | 124 | if (this.userData.manager.isOperator(node)) { 125 | // This node is a function, so generate two child nodes. 126 | const left = tree(maxDepth, depth + 1); 127 | const right = tree(maxDepth, depth + 1); 128 | 129 | result = result.concat(left).concat(right); 130 | } 131 | 132 | return result; 133 | }; 134 | 135 | return tree(this.userData.maxTreeDepth).join(''); 136 | } 137 | 138 | genetic.mutate = function(entity) { 139 | let result = entity; 140 | let index = Math.floor(Math.random() * entity.length); 141 | 142 | if (this.userData.manager.isOperator(entity[index])) { 143 | // Replace with an operator. 144 | let r = Math.floor(Math.random() * this.userData.manager.operators.length); 145 | result = this.userData.manager.replaceAt(entity, index, this.userData.manager.operators[r]); 146 | } 147 | else { 148 | // Replace with a value. 149 | let r = Math.floor(Math.random() * this.userData.manager.values.length); 150 | result = this.userData.manager.replaceAt(entity, index, this.userData.manager.values[r]); 151 | } 152 | 153 | return result; 154 | } 155 | 156 | genetic.crossover = function(parent1, parent2) { 157 | const index1 = Math.floor(Math.random() * parent1.length); 158 | const index2 = Math.floor(Math.random() * parent2.length); 159 | 160 | const subtree1 = this.userData.manager.subtreePrefix(parent1, index1).expression; 161 | const subtree2 = this.userData.manager.subtreePrefix(parent2, index2).expression; 162 | 163 | // Copy subtree2 to parent1 at index1. 164 | let child1 = this.userData.manager.replaceAtIndex(parent1, index1, subtree1, subtree2); 165 | // Copy subtree1 to parent2 at index2. 166 | let child2 = this.userData.manager.replaceAtIndex(parent2, index2, subtree2, subtree1); 167 | 168 | if (child1.length > this.userData.maxLength) { 169 | child1 = parent1; 170 | } 171 | 172 | if (child2.length > this.userData.maxLength) { 173 | child2 = parent2; 174 | } 175 | 176 | return [child1, child2]; 177 | } 178 | 179 | genetic.fitness = function(entity) { 180 | let fitness = 0; 181 | let solution = this.userData.solution; 182 | 183 | if (this.userData.testCases) { 184 | // For each test case, subtract a penalty from a total of 100 for any deviation in the evaluation from the target value. 185 | return this.userData.testCases.map(testCase => { 186 | const target = this.userData.manager.evaluatePrefix(this.userData.solution, testCase); 187 | const actual = this.userData.manager.evaluatePrefix(entity, testCase); 188 | 189 | // Give 100 points for each test case, minus any deviation in the evaluated value. 190 | return (100 - Math.abs(target - actual)); 191 | }).reduce((total, x) => { return total + x; }); 192 | } 193 | else { 194 | fitness = this.userData.manager.evaluatePrefix(entity); 195 | return solution - Math.abs(solution - fitness); 196 | } 197 | } 198 | 199 | genetic.generation = function(pop, generation, stats) { 200 | // If using test cases, give 100 points for each test case. Otherwise, just use the value of the evaluation. 201 | let solution = (this.userData.testCases && this.userData.testCases.length * 100) || this.userData.solution; 202 | return pop[0].fitness !== solution; 203 | } 204 | 205 | genetic.notification = function(pop, generation, stats, isDone) { 206 | const value = pop[0].entity; 207 | 208 | console.log(`Generation ${generation}, Best Fitness ${stats.maximum}, Best genome: ${value}`); 209 | 210 | if (isDone) { 211 | if (this.userData.testCases) { 212 | this.userData.testCases.forEach(testCase => { 213 | const result = this.userData.manager.evaluatePrefix(value, testCase); 214 | console.log(testCase); 215 | console.log(`Result: ${result}`); 216 | }); 217 | } 218 | else { 219 | console.log(`Result: ${this.userData.manager.evaluatePrefix(value)}`); 220 | } 221 | } 222 | } 223 | 224 | genetic.evolve({ 225 | iterations: 100000, 226 | size: 100, 227 | crossover: 0.3, 228 | mutation: 0.3, 229 | skip: 50 /* frequency for notifications */ 230 | }, { 231 | solution: '**xxx', // The function for the GA to learn. 232 | testCases: [ {x: 1 }, {x: 3}, {x: 5}, {x: 9}, {x: 10} ], // Test cases to learn from. 233 | maxTreeDepth: 25, 234 | maxLength: 100, 235 | manager: utilityManager 236 | }) --------------------------------------------------------------------------------