├── .npmrc ├── .gitignore ├── index.js ├── CHANGELOG.md ├── .editorconfig ├── .travis.yml ├── utils ├── selection.js ├── graph.js └── selection.spec.js ├── examples ├── showSpecies.js ├── largeGenome.js └── xor.js ├── src ├── node.class.js ├── node.spec.js ├── connection.class.js ├── innovation.class.js ├── network.class.js ├── connection.spec.js ├── network.spec.js ├── innovation.spec.js ├── population.spec.js ├── population.class.js ├── genome.class.js └── genome.spec.js ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .idea 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Population } = require('./src/population.class') 2 | 3 | module.exports = { Population } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## :scroll: Changelog 2 | 3 | ### :passenger_ship: 0.1.1 (2019-08-24) 4 | - `New` Added Xor example 5 | - `New` Updated Readme and licence 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - '6' 3 | - '8' 4 | - '10' 5 | sudo: false 6 | language: node_js 7 | install: 8 | - npm install 9 | script: 10 | - npm run test 11 | after_success: npm run coverage 12 | -------------------------------------------------------------------------------- /utils/selection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Returning a random item from an array 5 | * @return {Array} array an array 6 | * @return {item} item a random item from the array 7 | */ 8 | const getRandomItem = (array) => array[Math.floor(Math.random() * array.length)] 9 | 10 | module.exports = { getRandomItem } 11 | -------------------------------------------------------------------------------- /examples/showSpecies.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Population } = require('../index') 4 | const { showSpecies } = require('../utils/graph') 5 | 6 | const fitnessFunction = () => Math.random() 7 | 8 | const population = new Population(10, 2, 2, false) 9 | 10 | for (let i = 1; i < 30; ++i) { 11 | population.evolve(1, fitnessFunction) 12 | showSpecies(population) 13 | } 14 | -------------------------------------------------------------------------------- /examples/largeGenome.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Population } = require('../index') 4 | 5 | // This fitness function will make genomes grow larger and larger 6 | const fitnessFunction = genome => genome.connections.filter(c => !c.disabled).length 7 | 8 | const population = new Population(30, 3, 3) 9 | 10 | for (let i = 1; i < 10; ++i) { 11 | population.evolve(20, fitnessFunction) 12 | } 13 | -------------------------------------------------------------------------------- /src/node.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nodeTypes = ['input', 'hidden', 'output'] 4 | 5 | /** 6 | * Node gene is part of a genome 7 | */ 8 | class Node { 9 | /** 10 | * Create a new Node gene 11 | * @param {number} number Node number can be set 12 | * @param {number} type Type of node created 13 | */ 14 | constructor (number, type = 'hidden') { 15 | if (!number) throw new Error('Node should have a node number') 16 | if (type && !nodeTypes.includes(type)) throw new Error('Node type is not valid') 17 | this.number = number 18 | this.type = type 19 | } 20 | } 21 | 22 | module.exports = { Node } 23 | -------------------------------------------------------------------------------- /utils/graph.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const s = { 4 | 0: '⓿', 5 | 1: '❶', 6 | 2: '❷', 7 | 3: '❸', 8 | 4: '❹', 9 | 5: '❺', 10 | 6: '❻', 11 | 7: '❼', 12 | 8: '❽', 13 | 9: '❾', 14 | 10: '❿', 15 | 11: '⓫', 16 | 12: '⓬', 17 | 13: '⓭', 18 | 14: '⓮', 19 | 15: '⓯', 20 | 16: '⓰', 21 | 17: '⓱', 22 | 18: '⓲', 23 | 19: '⓳', 24 | 20: '⓴' 25 | } 26 | 27 | /** 28 | * Plots the differents species existing in the population 29 | * @return {Population} population Population 30 | */ 31 | const showSpecies = population => { 32 | let species = ` Gen ${population.generation} - ${population.species.length} species ` 33 | for (const i in population.species) { 34 | species += (s[i] ? s[i] : `[${i}]`).repeat(population.species[i].length) + ' ' 35 | } 36 | console.log(species) 37 | return species 38 | } 39 | 40 | module.exports = { showSpecies } 41 | -------------------------------------------------------------------------------- /examples/xor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Population } = require('../index') 4 | const population = new Population(100, 2, 1, false) 5 | const xor = [ 6 | [[0, 0], 0], 7 | [[0, 1], 1], 8 | [[1, 0], 1], 9 | [[1, 1], 0], 10 | ] 11 | 12 | let bestScore = 0 13 | const display = (population, score) => { 14 | if (score <= bestScore) return 15 | bestScore = score 16 | console.log(`[Generation ${population.generation}] fitness score = ${score}`) 17 | } 18 | 19 | population.evolve(10000, genome => { 20 | const network = genome.generateNetwork() 21 | let error = 0; 22 | let predictions = [] 23 | for (const [input, output] of xor) { 24 | const [prediction] = network.predict(input) 25 | error += Math.abs(prediction - output) 26 | predictions.push(prediction) 27 | } 28 | const score = 1 - (error / xor.length) 29 | display(population, score) 30 | return score 31 | }) 32 | -------------------------------------------------------------------------------- /src/node.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Node } = require('./node.class') 5 | 6 | describe('Node gene', () => { 7 | describe('creation', () => { 8 | it('should create a new Node Gene with all properties', () => { 9 | const gene = new Node(42, 'input') 10 | expect(gene).to.be.an('object') 11 | expect(gene).to.have.all.keys('number', 'type') 12 | expect(gene.type).to.equal('input') 13 | expect(gene.number).to.equal(42) 14 | }) 15 | it('should reject invalid node type', () => { 16 | expect(() => new Node(1, 'invalidtype')).to.throw('Node type is not valid') 17 | }) 18 | it('should reject node without node number', () => { 19 | expect(() => new Node(null, 'input')).to.throw('Node should have a node number') 20 | }) 21 | it('should create new node with hidden type', () => { 22 | const gene = new Node(2) 23 | expect(gene.type).to.equal('hidden') 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /utils/selection.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Node } = require('../src/node.class') 5 | const { getRandomItem } = require('./selection') 6 | 7 | describe('Selection', () => { 8 | describe('getRandomItem', () => { 9 | it('should get a random number from array', () => { 10 | const items = [1, 2, 3, 4, 5, 6, 7, 8, 10] 11 | const random = getRandomItem(items) 12 | expect(random).to.be.a('number') 13 | expect(random).to.be.within(1, 10) 14 | }) 15 | it('should get a random string from array', () => { 16 | const items = ['input', 'hidden', 'output'] 17 | const random = getRandomItem(items) 18 | expect(random).to.be.oneOf(items) 19 | }) 20 | it('should get a random item from node', () => { 21 | const items = [new Node(1, 'input'), new Node(2, 'output')] 22 | const random = getRandomItem(items) 23 | expect(random).to.be.an('object') 24 | expect(random).to.be.oneOf(items) 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/connection.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Innovation } = require('./innovation.class') 4 | const innovation = new Innovation() 5 | 6 | /** 7 | * Connection gene is part of a genome 8 | * We keep track of similar connections with an innovation number 9 | */ 10 | class Connection { 11 | /** 12 | * Create a new Connection gene 13 | * @param {number} inputNode Reference number of input node 14 | * @param {number} outputNode Reference number of output node 15 | * @param {number} weight Weight of connection [0, 1] 16 | */ 17 | constructor (inputNode, outputNode, weight) { 18 | if (!inputNode || !outputNode) throw new Error('Connection should have an input and output node') 19 | this.inputNode = inputNode 20 | this.outputNode = outputNode 21 | this.innovationNumber = innovation.getNumber(inputNode, outputNode) 22 | this.disabled = false 23 | this.weight = weight || Math.random() 24 | } 25 | 26 | /** 27 | * Disable a connection gene 28 | */ 29 | disable () { 30 | this.disabled = true 31 | } 32 | } 33 | 34 | module.exports = { Connection } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Romain SIMON 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neuroevolution", 3 | "version": "0.1.1", 4 | "description": "NeuroEvolution of Augmenting Topologies (NEAT) implementation with Tensorflow.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && nyc --reporter=html --reporter=text mocha \"./{,!(node_modules)/**/}*.spec.js\"", 8 | "coverage": "nyc report --reporter=text-lcov | coveralls", 9 | "lint:fix": "standard --fix" 10 | }, 11 | "author": "Romain Simon ", 12 | "license": "MIT", 13 | "keywords": [ 14 | "neat", 15 | "neuroevolution", 16 | "tensorflow", 17 | "genetic", 18 | "algorithm", 19 | "evolutionary" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/romainsimon/neuroevolution" 24 | }, 25 | "standard": { 26 | "env": { 27 | "mocha": true 28 | } 29 | }, 30 | "dependencies": { 31 | "@tensorflow/tfjs": "^0.13.4" 32 | }, 33 | "devDependencies": { 34 | "chai": "^4.1.2", 35 | "coveralls": "^3.0.2", 36 | "mocha": "^5.2.0", 37 | "mocha-lcov-reporter": "^1.3.0", 38 | "nyc": "^12.0.2", 39 | "standard": "^12.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/innovation.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let globalInnovationNumber = 0 4 | let innovations = {} 5 | 6 | /** 7 | * Innovation Generator generates innovation numbers 8 | * and keeps track of existing innovations 9 | * This allows historical markings of node/connection genes 10 | */ 11 | class Innovation { 12 | /** 13 | * Returns the last innovation number 14 | * @return {number} number last innovation number 15 | */ 16 | getLast () { 17 | return globalInnovationNumber 18 | } 19 | 20 | /** 21 | * Generates an increasing innovation number 22 | * or returns the existing innovation number for the connection 23 | * @param {number} inputNode Reference number of input node 24 | * @param {number} outputNode Reference number of output node 25 | * @return {number} number Innovation number 26 | */ 27 | getNumber (inputNode, outputNode) { 28 | if (!inputNode || !Number(inputNode)) throw new Error('You must specify an input node number') 29 | if (!outputNode || !Number(outputNode)) throw new Error('You must specify an output node number') 30 | const connection = outputNode > inputNode ? `${inputNode}>${outputNode}` : `${outputNode}>${inputNode}` 31 | if (!innovations[connection]) { 32 | ++globalInnovationNumber 33 | innovations[connection] = globalInnovationNumber 34 | } 35 | return innovations[connection] 36 | } 37 | 38 | /** 39 | * Reset innovation number to zero 40 | */ 41 | reset () { 42 | globalInnovationNumber = 0 43 | innovations = {} 44 | } 45 | } 46 | 47 | module.exports = { Innovation } 48 | -------------------------------------------------------------------------------- /src/network.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tf = require('@tensorflow/tfjs') 4 | 5 | /** 6 | * Neural Network 7 | * @TODO This does not reflect the structure from genome yet 8 | */ 9 | class Network { 10 | /** 11 | * Create a new Neural Network 12 | * @param {number} nbInput Number of input neurons 13 | * @param {number} nbHidden Number of hidden neurons 14 | * @param {number} nbOutput Number of output neurons 15 | */ 16 | constructor (nbInput, nbHidden, nbOutput) { 17 | this.nbInput = nbInput 18 | this.nbHidden = nbHidden 19 | this.nbOutput = nbOutput 20 | this.inputWeights = tf.randomNormal([this.nbInput, this.nbHidden]) 21 | this.outputWeights = tf.randomNormal([this.nbHidden, this.nbOutput]) 22 | } 23 | 24 | /** 25 | * Predict outut from input 26 | * @param {Array} input One hot encoded input 27 | * @return {Array} output One hot encoded output 28 | */ 29 | predict (input) { 30 | let output 31 | tf.tidy(() => { 32 | let inputLayer = tf.tensor(input, [1, this.nbInput]) 33 | let hiddenLayer = inputLayer.matMul(this.inputWeights).sigmoid() 34 | let outputLayer = hiddenLayer.matMul(this.outputWeights).sigmoid() 35 | output = outputLayer.dataSync() 36 | }) 37 | return output 38 | } 39 | 40 | /** 41 | * Create a clone a this neural network 42 | * @return {NeuralNetwork} cloned neural network 43 | */ 44 | clone () { 45 | let clone = new Network(this.nbInput, this.nbHidden, this.nbOutput) 46 | clone.dispose() 47 | clone.inputWeights = tf.clone(this.inputWeights) 48 | clone.outputWeights = tf.clone(this.outputWeights) 49 | return clone 50 | } 51 | 52 | /** 53 | * Dispose memory 54 | */ 55 | dispose () { 56 | this.inputWeights.dispose() 57 | this.outputWeights.dispose() 58 | } 59 | } 60 | 61 | module.exports = { Network } 62 | -------------------------------------------------------------------------------- /src/connection.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Connection } = require('./connection.class') 5 | 6 | describe('Connection gene', () => { 7 | describe('creation', () => { 8 | it('should create a new Connection Gene with all properties', () => { 9 | const gene = new Connection(4, 5) 10 | expect(gene).to.be.an('object') 11 | expect(gene).to.have.all.keys('inputNode', 'outputNode', 'disabled', 'weight', 'innovationNumber') 12 | expect(gene.inputNode).to.equal(4) 13 | expect(gene.outputNode).to.equal(5) 14 | expect(gene.disabled).to.equal(false) 15 | expect(gene.weight).to.be.within(0, 1) 16 | expect(gene.innovationNumber).to.equal(1) 17 | }) 18 | it('should increase innovation number from new connections', () => { 19 | const gene1 = new Connection(1, 4) 20 | const gene2 = new Connection(1, 8) 21 | expect(gene2.innovationNumber).to.be.above(gene1.innovationNumber) 22 | }) 23 | it('should generate the same innovation number for the same connection', () => { 24 | const gene1 = new Connection(2, 8) 25 | const gene2 = new Connection(2, 8) 26 | expect(gene2.innovationNumber).to.equal(gene1.innovationNumber) 27 | }) 28 | it('should get the same innovation number when copied ', () => { 29 | const gene1 = new Connection(1, 2) 30 | const gene2 = gene1 31 | expect(gene2.innovationNumber).to.equal(gene1.innovationNumber) 32 | }) 33 | it('should reject node without input or output nodes', () => { 34 | expect(() => new Connection(null, null)).to.throw('Connection should have an input and output node') 35 | }) 36 | }) 37 | 38 | describe('disable', () => { 39 | it('should disable connection', () => { 40 | const gene = new Connection(1, 2) 41 | gene.disable() 42 | expect(gene.disabled).to.equal(true) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/network.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Network } = require('./network.class') 5 | 6 | describe('Neural Network', () => { 7 | describe('creation', () => { 8 | it('should create a new Neural Network with all properties', () => { 9 | const ann = new Network(1, 6, 3) 10 | expect(ann).to.be.an('object') 11 | expect(ann).to.have.all.keys('nbInput', 'nbHidden', 'nbOutput', 'inputWeights', 'outputWeights') 12 | }) 13 | }) 14 | 15 | describe('creation', () => { 16 | it('should predict an output from input', () => { 17 | const ann = new Network(1, 1, 2) 18 | const input = [Math.random()] 19 | const output = ann.predict(input) 20 | expect(output).to.be.have.lengthOf(2) 21 | expect(output[0]).to.be.within(0, 1) 22 | }) 23 | }) 24 | 25 | describe('clone', () => { 26 | it('should clone neural network', () => { 27 | const ann1 = new Network(3, 4, 5) 28 | const ann2 = ann1.clone() 29 | expect(ann2).to.be.an('object') 30 | expect(ann2).to.have.all.keys('nbInput', 'nbHidden', 'nbOutput', 'inputWeights', 'outputWeights') 31 | expect(ann2.nbInput).to.equal(ann1.nbInput) 32 | expect(ann2.nbHidden).to.equal(ann1.nbHidden) 33 | expect(ann2.nbOutput).to.equal(ann1.nbOutput) 34 | }) 35 | }) 36 | 37 | describe('dispose', () => { 38 | it('should dispose input and output weights', () => { 39 | const ann = new Network(3, 4, 5) 40 | expect(ann.inputWeights.isDisposedInternal).to.equal(false) 41 | expect(ann.outputWeights.isDisposedInternal).to.equal(false) 42 | ann.dispose() 43 | expect(ann).to.be.an('object') 44 | expect(ann).to.have.all.keys('nbInput', 'nbHidden', 'nbOutput', 'inputWeights', 'outputWeights') 45 | expect(ann.inputWeights.isDisposedInternal).to.equal(true) 46 | expect(ann.outputWeights.isDisposedInternal).to.equal(true) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/innovation.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Innovation } = require('./innovation.class') 5 | 6 | describe('Innovation', () => { 7 | describe('getNumber', () => { 8 | it('should throw an error if from node is not present', () => { 9 | const innovation = new Innovation() 10 | expect(() => innovation.getNumber()).to.throw('You must specify an input node number') 11 | }) 12 | it('should throw an error if to node is not present', () => { 13 | const innovation = new Innovation() 14 | expect(() => innovation.getNumber(1)).to.throw('You must specify an output node number') 15 | }) 16 | it('should get an increasing innovation number for different connections', () => { 17 | const innovation = new Innovation() 18 | const innovation1 = innovation.getNumber(1, 2) 19 | const innovation2 = innovation.getNumber(1, 4) 20 | expect(innovation2).to.be.above(innovation1) 21 | }) 22 | it('should get the same innovation number for the same connection', () => { 23 | const innovation = new Innovation() 24 | const innovation1 = innovation.getNumber(1, 2) 25 | const innovation2 = innovation.getNumber(1, 2) 26 | expect(innovation2).to.equal(innovation1) 27 | }) 28 | it('should get the same innovation number for the same even if inverted', () => { 29 | const innovation = new Innovation() 30 | const innovation1 = innovation.getNumber(1, 2) 31 | const innovation2 = innovation.getNumber(2, 1) 32 | expect(innovation2).to.equal(innovation1) 33 | }) 34 | }) 35 | 36 | describe('reset', () => { 37 | it('should reset innovation number generator', () => { 38 | const innovation = new Innovation() 39 | innovation.getNumber(1, 2) 40 | innovation.reset() 41 | const innovation2 = innovation.getNumber(1, 4) 42 | expect(innovation2).to.be.equal(1) 43 | }) 44 | }) 45 | 46 | describe('getLast', () => { 47 | it('should get the last innovation number', () => { 48 | const innovation = new Innovation() 49 | innovation.reset() 50 | innovation.getNumber(1, 2) 51 | innovation.getNumber(1, 4) 52 | const last = innovation.getLast() 53 | expect(last).to.be.equal(2) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NeuroEvolution of Augmenting Topologies (NEAT)

2 | 3 |
4 | :hatching_chick: Evolving a population of Neural Networks using Tensorflow.js and Genetic Algorithm in ES6. This is an implementation of NEAT (Neuro Evolution of Augmenting topologies) 5 |
6 | 7 |
8 | 9 |
10 | 11 | Build Status 13 | 14 | Test Coverage 16 | 17 | 18 | Standard 20 | 21 |
22 | 23 | 24 | ## Table of Contents 25 | 26 | - [Installation](#installation) 27 | - [Usage](#usage) 28 | - [1. Initialize a new population](#1-initialize-a-new-population) 29 | - [2. Evolve the population](#2-evolve-the-population) 30 | - [Examples](#examples) 31 | - [Xor](#xor) 32 | - [License](#license) 33 | 34 | ## Installation 35 | 36 | Install using [`npm`](https://www.npmjs.com/package/neuroevolution): 37 | 38 | ```bash 39 | npm install neuroevolution 40 | ``` 41 | 42 | or Yarn [`yarn`](https://yarnpkg.com/en/package/neuroevolution): 43 | 44 | ```bash 45 | yarn add neuroevolution 46 | ``` 47 | 48 | ## Usage 49 | 50 | ### 1. Initialize a new population 51 | 52 | The first step should be to initialize a new population. 53 | 54 | A population accepts 3 main parameters: 55 | - `populationSize` Total size of the genomes population 56 | - `nbInput` Number of input nodes 57 | - `nbOutput` Number of output node 58 | 59 | ```javascript 60 | const { Population } = require('neuroevolution') 61 | const population = new Population(100, 2, 4) 62 | ``` 63 | 64 | This will create a population of 100 neural networks with 2 inputs and 4 outputs. 65 | 66 | ### 2. Evolve the population 67 | 68 | You can start evolving the population using `evolve` with 2 parameters : 69 | - `iterations` How many iterations to evolve 70 | - `fitnessFunction` You fitness function that has access to your genome 71 | 72 | ```javascript 73 | population.evolve(40, genome => { 74 | const network = genome.generateNetwork() 75 | const prediction = network.predict(input) 76 | // ... return a fitness score according to the accuracy of prediction 77 | }) 78 | ``` 79 | 80 | This will evolve the population, keeping the fittest neural networks according to your 81 | fitness function accross 40 generations. The number of iterations is cumulative, this means 82 | if your population current generation is 12, evolving with 40 iterations will transform the 83 | population to generation 52. 84 | 85 | 86 | ## Examples 87 | 88 | ### Xor 89 | 90 | To run this example : 91 | 92 | ```bash 93 | node examples/xor 94 | ``` 95 | 96 | Sample code : 97 | 98 | ```javascript 99 | const { Population } = require('neuroevolution') 100 | const population = new Population(50, 2, 1, false) 101 | const xor = [ 102 | [[0, 0], 0], 103 | [[0, 1], 1], 104 | [[1, 0], 1], 105 | [[1, 1], 0], 106 | ] 107 | 108 | population.evolve(1000, genome => { 109 | const network = genome.generateNetwork() 110 | let error = 0; 111 | for (const [input, output] of xor) { 112 | const [prediction] = network.predict(input) 113 | error += Math.abs(prediction - output) 114 | } 115 | return 1 - (error / xor.length) 116 | }) 117 | ``` 118 | 119 | ## License 120 | 121 | Neuroevolution is [MIT licensed](./LICENSE). -------------------------------------------------------------------------------- /src/population.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Population } = require('./population.class') 5 | 6 | describe('Population', () => { 7 | describe('creation', () => { 8 | it('should create a new Population with all properties', () => { 9 | const population = new Population() 10 | expect(population).to.be.an('object') 11 | expect(population).to.have.all.keys('generation', 'nbInput', 'nbOutput', 'species', 'populationSize', 'showLogs', 'currentPopulation') 12 | }) 13 | 14 | it('should create a new Population with generation 1', () => { 15 | const population = new Population() 16 | expect(population.generation).to.equal(1) 17 | }) 18 | 19 | it('should create a new Population with population size', () => { 20 | const size = 18 21 | const population = new Population(size) 22 | expect(population.populationSize).to.equal(size) 23 | expect(population.currentPopulation).to.have.lengthOf(size) 24 | }) 25 | }) 26 | 27 | describe('speciate', () => { 28 | it('should create an initial species with all population', () => { 29 | const population = new Population(20, 3, 4) 30 | expect(population.species).to.have.lengthOf(1) 31 | expect(population.species[0]).to.have.lengthOf(20) 32 | }) 33 | 34 | it('should group similar genomes into the same species', () => { 35 | const population = new Population(20, 3, 4) 36 | population.speciate() 37 | expect(population.species).to.have.lengthOf(1) 38 | expect(population.species[0]).to.have.lengthOf(20) 39 | }) 40 | 41 | it('should create new species when threshold is too low', () => { 42 | const population = new Population(20, 3, 4) 43 | population.speciate(0) 44 | expect(population.species).to.have.lengthOf(20) 45 | }) 46 | }) 47 | 48 | describe('evaluate', () => { 49 | it('should calculate fitness score for all genomes', () => { 50 | const population = new Population() 51 | const dumbFitness = () => 0.42 52 | population.evaluate(dumbFitness) 53 | for (const chromosome of population.currentPopulation) { 54 | expect(chromosome.fitness).to.be.a('number') 55 | expect(chromosome.fitness).to.equal(0.42) 56 | } 57 | }) 58 | }) 59 | 60 | describe('select', () => { 61 | it('should select only genomes with top fitness', () => { 62 | const population = new Population(100) 63 | const dumbFitness = () => Math.random() 64 | population.evaluate(dumbFitness) 65 | population.select(0.1) 66 | expect(population.currentPopulation.length).to.equal(10) 67 | // expect(population.currentPopulation[0].fitness).be.at.least(population.currentPopulation[9].fitness) 68 | }) 69 | 70 | it('should select genomes when no fitness score', () => { 71 | const population = new Population(100) 72 | population.select(0.1) 73 | expect(population.currentPopulation.length).to.equal(10) 74 | }) 75 | }) 76 | 77 | describe('reproduce', () => { 78 | it('should create new children genomes in the population', () => { 79 | const population = new Population(5) 80 | population.reproduce() 81 | expect(population.currentPopulation.length).to.equal(5 + 4 + 3 + 2 + 1) 82 | }) 83 | }) 84 | 85 | describe('repopulate', () => { 86 | it('should repopulate with new chromosomes', () => { 87 | const population = new Population(50) 88 | population.select(0.1) 89 | population.repopulate() 90 | expect(population.currentPopulation.length).to.equal(50) 91 | }) 92 | }) 93 | 94 | describe('evolve', () => { 95 | it('should evolve population to target generation', () => { 96 | const dumbFitness = () => Math.random() 97 | const population = new Population(10, 1, 1, false) 98 | population.evolve(10, dumbFitness) 99 | expect(population.generation).to.equal(11) 100 | population.evolve(10, dumbFitness) 101 | expect(population.generation).to.equal(21) 102 | }) 103 | 104 | it('should increase fitness over generations', () => { 105 | const generationFitness = genome => genome.connections.length 106 | const population = new Population(10, 1, 1) 107 | population.evaluate(generationFitness) 108 | const firstFitness = population.currentPopulation[0].fitness 109 | population.evolve(10, generationFitness) 110 | const lastFitness = population.currentPopulation[0].fitness 111 | expect(lastFitness).to.be.at.least(firstFitness) 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/population.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Genome } = require('./genome.class') 4 | const { getRandomItem } = require('../utils/selection') 5 | 6 | /** 7 | * Population contains different genomes that are 8 | * grouped into different species according to their distance 9 | */ 10 | class Population { 11 | /** 12 | * Create a new population of genomes 13 | * @param {Number} populationSize Total size of the genomes population 14 | * @param {number} nbInput Number of input nodes 15 | * @param {number} nbOutput Number of output node 16 | * @param {Boolean} showLogs Will display logs if true 17 | */ 18 | constructor (populationSize = 100, nbInput = 1, nbOutput = 1, showLogs = true) { 19 | this.generation = 1 20 | this.populationSize = populationSize 21 | this.nbInput = nbInput 22 | this.nbOutput = nbOutput 23 | this.showLogs = showLogs 24 | this.currentPopulation = [...Array(this.populationSize)].map(genome => new Genome(nbInput, nbOutput)) 25 | this.species = [ this.currentPopulation ] 26 | } 27 | 28 | /** 29 | * Speciation creates and ordered list of species 30 | * Two genomes are considered as from the same species 31 | * if their distance is below the distance threshold 32 | * @param {number} threshold Threshold distance 33 | */ 34 | speciate (threshold = 1) { 35 | const speciesRepresentation = this.species.map(s => getRandomItem(s)) 36 | this.species = new Array(speciesRepresentation.length).fill([]) 37 | for (const g of this.currentPopulation) { 38 | for (const i in speciesRepresentation) { 39 | if (g.distance(speciesRepresentation[i]) <= threshold) { 40 | this.species[i].push(g) 41 | break 42 | } 43 | if (speciesRepresentation.length - i === 1) this.species.push([g]) 44 | } 45 | } 46 | this.species = this.species.filter(s => s.length > 0) 47 | return this.species 48 | } 49 | 50 | /** 51 | * Evaluate the fitness of entire population according to fitness function 52 | * Sorts the population from highest fitness score to lowest 53 | * @param {Function} fitnessFunction Fitness function used to score genomes 54 | */ 55 | evaluate (fitnessFunction) { 56 | for (const genome of this.currentPopulation) genome.calculateFitness(fitnessFunction) 57 | this.currentPopulation.sort((genomeA, genomeB) => genomeB.fitness - genomeA.fitness) 58 | } 59 | 60 | /** 61 | * Select the best genomes in the population according to survival rate 62 | * Kills all other genomes (sorry guys) 63 | * @param {number} survivalRate Percent of population that survives [0-1] 64 | */ 65 | select (survivalRate = 0.2) { 66 | this.speciate() 67 | // @TODO : explicit fitness sharing 68 | const nbSelected = Math.ceil(this.populationSize * survivalRate) 69 | const newPopulation = [] 70 | for (const i in this.currentPopulation) if (i < nbSelected) newPopulation.push(this.currentPopulation[i]) 71 | this.currentPopulation = newPopulation 72 | } 73 | 74 | /** 75 | * Reproduce existing genomes in population via crossover 76 | * Mutates children and adds them to population 77 | * This uses explicit fitness sharing to adjust the fitness 78 | * according to species 79 | */ 80 | reproduce () { 81 | const children = [] 82 | for (let i = 0; i < this.currentPopulation.length; i++) { 83 | for (let j = i + 1; j < this.currentPopulation.length; j++) { 84 | const parentA = this.currentPopulation[i] 85 | const parentB = this.currentPopulation[j] 86 | const child = parentA.crossover(parentB) 87 | child.mutate() 88 | children.push(child) 89 | } 90 | } 91 | this.generation++ 92 | this.currentPopulation = [...this.currentPopulation, ...children] 93 | } 94 | 95 | /** 96 | * Create new random genomes to match the max population size 97 | * It does not do crossover or mutation, but simply repopulates 98 | */ 99 | repopulate () { 100 | const nbToGenerate = this.populationSize - this.currentPopulation.length 101 | const newGenomes = Array(nbToGenerate).fill('').map(genome => new Genome(this.nbInput, this.nbOutput)) 102 | this.currentPopulation = [...this.currentPopulation, ...newGenomes] 103 | } 104 | 105 | /** 106 | * Evolves the population via different steps: 107 | * selection, crossover, mutation 108 | * @param {number} iterations Number of iterations 109 | * @param {Function} FitnessFunction Fitness function used for evaluation 110 | * It has access to the genome context 111 | */ 112 | evolve (iterations = 1000, fitnessFunction) { 113 | const startGeneration = this.generation 114 | const maxGen = startGeneration + iterations - 1 115 | while (this.generation <= maxGen) { 116 | if (this.showLogs) { 117 | let terminalWidth = process.stdout.columns / 2 118 | let progress = '' 119 | for (let p = 0; p < terminalWidth; ++p) progress += (p >= Math.round((this.generation - startGeneration) / (maxGen - startGeneration) * terminalWidth)) ? '░' : '█' 120 | process.stdout.write(` Generation ${this.generation}/${maxGen} ${progress} ${Math.round(this.generation / maxGen * 100)}% ${this.generation === maxGen ? '\n' : '\r'}`) 121 | } 122 | this.evaluate(fitnessFunction) 123 | this.select() 124 | this.reproduce() 125 | } 126 | if (this.showLogs) { 127 | console.log(` - Fittest genome: ${this.currentPopulation[0].dna(false)}`) 128 | console.log(` - Species: ${this.species.length}`) 129 | console.log(` - Population: ${this.currentPopulation.length}`) 130 | console.log(` - Max fitness: ${this.currentPopulation[0].fitness}`) 131 | } 132 | return this.currentPopulation[0] 133 | } 134 | } 135 | 136 | module.exports = { Population } 137 | -------------------------------------------------------------------------------- /src/genome.class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Node } = require('./node.class') 4 | const { Connection } = require('./connection.class') 5 | const { Network } = require('./network.class') 6 | const { getRandomItem } = require('../utils/selection') 7 | 8 | /** 9 | * Genome is combination of genes 10 | * It can be mutated or combined with another genome (crossover) 11 | */ 12 | class Genome { 13 | /** 14 | * Create a new Genome 15 | * @param {number} nbInput Number of input nodes 16 | * @param {number} nbOutput Number of output nodes 17 | * @param {Object[]} nodes Array of existing nodes 18 | * @param {Object[]} connections Array of existing connections 19 | */ 20 | constructor (nbInput = 1, nbOutput = 1, nodes, connections) { 21 | if (!nodes) { 22 | this.nodeCount = 0 23 | this.nodes = [] 24 | for (let i = 0; i < nbInput; ++i) this.nodes.push(new Node(++this.nodeCount, 'input')) 25 | for (let o = 0; o < nbOutput; ++o) this.nodes.push(new Node(++this.nodeCount, 'output')) 26 | } else { 27 | this.nodes = nodes 28 | this.nodeCount = nodes.length 29 | } 30 | this.connections = connections || [] 31 | this.nbInput = nbInput 32 | this.nbOutput = nbOutput 33 | this.fitness = 0 34 | this.sharedFitness = 0 35 | } 36 | 37 | /** 38 | * Shows a text representation of DNA/connections 39 | * @param {Boolean} showDisabled Displays disabled connections 40 | * @return {string} DNA respresenting all connections 41 | */ 42 | dna (showDisabled = true) { 43 | return this.connections 44 | .filter(c => showDisabled ? true : !c.disabled) 45 | .map(c => `${c.innovationNumber}[${c.inputNode}${c.disabled ? 'X' : '>'}${c.outputNode}]`) 46 | .join() 47 | } 48 | 49 | /** 50 | * Get a node by its innovation number 51 | * @param {number} number Innovation number 52 | */ 53 | getLastInnovation () { 54 | return this.connections.length ? this.connections.reduce((a, b) => a.innovationNumber > b.innovationNumber ? a : b, 0).innovationNumber : 0 55 | } 56 | 57 | /** 58 | * Get a node by its node number 59 | * @param {number} number node number 60 | */ 61 | getNode (number) { 62 | const node = this.nodes.filter(n => n.number === number) 63 | return node.length ? node[0] : null 64 | } 65 | 66 | /** 67 | * Get a connection by its innovation number 68 | * @param {number} number Innovation number 69 | */ 70 | getConnection (number) { 71 | const connection = this.connections.filter(c => c.innovationNumber === number) 72 | return connection.length ? connection[0] : null 73 | } 74 | 75 | /** 76 | * Lists all possible new connections 77 | * @return {Array} possibleConnections An array of possible new connections that can be created 78 | */ 79 | possibleNewConnections () { 80 | const existingConnections = this.connections 81 | .filter(conn => !conn.disabled) 82 | .map(conn => conn.inputNode + '>' + conn.outputNode) 83 | 84 | const possibleConnections = this.nodes.reduce((acc, input, i) => 85 | acc.concat(this.nodes.slice(i + 1).map(output => [input.number, output.number])), 86 | []).filter(gene => !existingConnections.includes(gene[0] + '>' + gene[1]) && 87 | !(gene[0].type === 'input' && gene[1].type === 'input') && 88 | !(gene[0].type === 'output' && gene[1].type === 'output')) 89 | 90 | return possibleConnections 91 | } 92 | 93 | /** 94 | * Evalutate the fitness of genome 95 | * @param {Function} fitFunction Fitness function used to score genomes 96 | * @return {number} Fitness score 97 | */ 98 | calculateFitness (fitFunction) { 99 | this.fitness = fitFunction(this) 100 | return this.fitness 101 | } 102 | 103 | /** 104 | * Calculates a distance between this genome and another genome 105 | * @param {Genome} genomeB Another genome 106 | * @return {number} Distance between two genomes 107 | */ 108 | distance (genomeB) { 109 | const weights = { excess: 1, disjoint: 1, weight: 0.4 } 110 | const totalGenes = Math.max(this.connections.length, genomeB.connections.length) 111 | const N = totalGenes > 20 ? totalGenes : 1 112 | 113 | let nbExcess = 0 114 | let nbDisjoint = 0 115 | let nbMatching = 0 116 | let weightDiff = 0 117 | let c = 1 118 | 119 | const maxInnovationA = this.getLastInnovation() 120 | const maxInnovationB = genomeB.getLastInnovation() 121 | const maxInnovation = Math.max(maxInnovationA, maxInnovationB) 122 | 123 | while (c <= maxInnovation) { 124 | const aConn = this.getConnection(c) 125 | const bConn = genomeB.getConnection(c) 126 | if (aConn && !bConn) { 127 | if (c > maxInnovationB) nbExcess++ 128 | else nbDisjoint++ 129 | } else if (!aConn && bConn) { 130 | if (c > maxInnovationA) nbExcess++ 131 | else nbDisjoint++ 132 | } else if (aConn && bConn) { 133 | nbMatching++ 134 | weightDiff += Math.abs(aConn.weight - bConn.weight) 135 | } 136 | c++ 137 | } 138 | const avgWeightDiff = nbMatching > 0 ? weightDiff / nbMatching : 1 139 | const distance = weights.excess * nbExcess / N + 140 | weights.disjoint * nbDisjoint / N + 141 | weights.weight * avgWeightDiff 142 | 143 | return distance 144 | } 145 | 146 | /** 147 | * Creates a child genome. 148 | * This genome is combined with another one 149 | * Matching genes are chosen randomly between the two parents 150 | * Disjoint or excess genes are chosen in the more fit parent 151 | * @param {Genome} genomeB Another genome 152 | * @return {Genome} Child genome 153 | */ 154 | crossover (genomeB) { 155 | const childNodes = [] 156 | const childConnections = [] 157 | let n = 1 158 | let c = 1 159 | const maxNode = Math.max(this.nodeCount, genomeB.nodeCount) 160 | while (n <= maxNode) { 161 | const aNode = this.getNode(n) 162 | const bNode = genomeB.getNode(n) 163 | if (aNode && bNode) { childNodes.push(Math.random() > 0.5 ? aNode : bNode) } else if (aNode && this.fitness > genomeB.fitness) { childNodes.push(aNode) } else if (bNode && this.fitness < genomeB.fitness) { childNodes.push(bNode) } else if (aNode && bNode) { childNodes.push(aNode || bNode) } 164 | n++ 165 | } 166 | const maxConnection = Math.max(this.getLastInnovation(), genomeB.getLastInnovation()) 167 | while (c <= maxConnection) { 168 | const aConn = this.getConnection(c) 169 | const bConn = genomeB.getConnection(c) 170 | if (aConn && bConn) { childConnections.push(Math.random() > 0.5 ? aConn : bConn) } else if (aConn && this.fitness > genomeB.fitness) { childConnections.push(aConn) } else if (bConn && this.fitness < genomeB.fitness) { childConnections.push(bConn) } else if (aConn && bConn) { childConnections.push(aConn || bConn) } 171 | c++ 172 | } 173 | return new Genome(this.nbInput, this.nbOutput, childNodes, childConnections) 174 | } 175 | 176 | /** 177 | * Mutates genome by structural or non-structural mutation 178 | * - Add a new node 179 | * - Add a new connection 180 | * - Change weight of a connection 181 | */ 182 | mutate () { 183 | const mutations = [ 184 | 'addConnection', 185 | 'addNode', 186 | 'updateConnectionWeight' 187 | ] 188 | this[getRandomItem(mutations)]() 189 | } 190 | 191 | /** 192 | * Mutates genome by adding a new connection 193 | */ 194 | addConnection () { 195 | const randomConnection = getRandomItem(this.possibleNewConnections()) 196 | if (!randomConnection) return false 197 | const newConnection = new Connection(randomConnection[0], randomConnection[1]) 198 | this.connections.push(newConnection) 199 | } 200 | 201 | /** 202 | * Mutates genome by adding a new node 203 | * The connection gene being split is disabled, and two new connection genes are created. 204 | * The new node is between the two new connections. 205 | */ 206 | addNode () { 207 | const randomConnection = getRandomItem(this.connections.filter(gene => !gene.disabled)) 208 | if (!randomConnection) { return false } 209 | const newNode = new Node(++this.nodeCount, 'hidden') 210 | this.nodes.push(newNode) 211 | this.connections.push(new Connection(randomConnection.inputNode, newNode.number, 1)) 212 | this.connections.push(new Connection(newNode.number, randomConnection.outputNode, randomConnection.weight)) 213 | randomConnection.disable() 214 | } 215 | 216 | /** 217 | * Mutates genome by updating connection weight 218 | */ 219 | updateConnectionWeight () { 220 | if (!this.connections.length) { return false } 221 | this.connections[Math.floor(Math.random() * this.connections.length)].weight = Math.random() 222 | } 223 | 224 | /** 225 | * Generates the corresponding Neural Network 226 | * @TODO : Kahn's algorithm ? 227 | */ 228 | generateNetwork () { 229 | const nbHidden = this.nodes.filter(node => node.type === 'hidden').length 230 | const network = new Network(this.nbInput, nbHidden, this.nbOutput) 231 | return network 232 | } 233 | } 234 | 235 | module.exports = { Genome } 236 | -------------------------------------------------------------------------------- /src/genome.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { Genome } = require('./genome.class') 5 | const { Node } = require('./node.class') 6 | const { Connection } = require('./connection.class') 7 | const { Innovation } = require('./innovation.class') 8 | 9 | const innovation = new Innovation() 10 | 11 | describe('Genome', () => { 12 | beforeEach(() => innovation.reset()) 13 | describe('creation', () => { 14 | it('should create a new Genome with all properties', () => { 15 | const genome = new Genome(2, 3) 16 | expect(genome).to.be.an('object') 17 | expect(genome).to.have.all.keys('nodes', 'connections', 'nbInput', 'nbOutput', 'nodeCount', 'fitness', 'sharedFitness') 18 | expect(genome.nodes).to.have.lengthOf(2 + 3) 19 | }) 20 | it('should create a new Genome with increasing node numbers in initial nodes', () => { 21 | const genome = new Genome(2, 3) 22 | expect(genome).to.be.an('object') 23 | expect(genome.nodes[1].number).to.be.above(genome.nodes[0].number) 24 | }) 25 | it('should preserve node numbers when copying a genome', () => { 26 | const genome1 = new Genome(2, 2) 27 | genome1.addConnection() 28 | const genome2 = genome1 29 | expect(genome2.nodes[0].number).to.equal(genome1.nodes[0].number) 30 | expect(genome2.nodes[1].number).to.equal(genome1.nodes[1].number) 31 | expect(genome2.connections[0].number).to.equal(genome1.connections[0].number) 32 | }) 33 | }) 34 | 35 | describe('dna', () => { 36 | it('should return a valid dna string representing genome', () => { 37 | const genome = new Genome(2, 3, null, [ 38 | new Connection(1, 4), 39 | new Connection(2, 3), 40 | new Connection(2, 4) 41 | ]) 42 | expect(genome.dna()).to.be.a('string') 43 | expect(genome.dna()).to.equal('1[1>4],2[2>3],3[2>4]') 44 | }) 45 | }) 46 | 47 | describe('getLastInnovation', () => { 48 | it('should return 0 if no innovation occured', () => { 49 | const genome = new Genome(1, 2) 50 | expect(genome.getLastInnovation()).to.equal(0) 51 | }) 52 | it('should return the last innovation number', () => { 53 | const genome = new Genome(2, 3, null, [ 54 | new Connection(1, 4), 55 | new Connection(2, 3), 56 | new Connection(2, 4) 57 | ]) 58 | expect(genome.getLastInnovation()).to.equal(3) 59 | }) 60 | }) 61 | 62 | describe('getNode', () => { 63 | it('should get a node by its number', () => { 64 | const genome = new Genome(1, 2, [ 65 | new Node(1, 'input'), 66 | new Node(2, 'output') 67 | ]) 68 | const node1 = genome.getNode(1) 69 | expect(node1).to.be.an('object') 70 | expect(node1.number).to.equal(1) 71 | expect(node1.type).to.equal('input') 72 | const node2 = genome.getNode(2) 73 | expect(node2).to.be.an('object') 74 | expect(node2.number).to.equal(2) 75 | expect(node2.type).to.equal('output') 76 | const node32 = genome.getNode(42) 77 | expect(node32).to.equal(null) 78 | }) 79 | }) 80 | 81 | describe('getConnection', () => { 82 | it('should get a connection by its innovation number', () => { 83 | const genome = new Genome(2, 3, null, [ 84 | new Connection(1, 4, 1), 85 | new Connection(2, 3, 2) 86 | ]) 87 | const conn1 = genome.getConnection(1) 88 | expect(conn1).to.be.an('object') 89 | expect(conn1.innovationNumber).to.equal(1) 90 | const conn2 = genome.getConnection(2) 91 | expect(conn2).to.be.an('object') 92 | expect(conn2.innovationNumber).to.equal(2) 93 | const conn32 = genome.getConnection(42) 94 | expect(conn32).to.equal(null) 95 | }) 96 | }) 97 | 98 | describe('possibleNewConnections', () => { 99 | it('should create all combinations of possible connections without existing and input-input or output-output', () => { 100 | const genome = new Genome(1, 3, [ 101 | new Node(1, 'input'), 102 | new Node(2, 'hidden'), 103 | new Node(3, 'hidden'), 104 | new Node(4, 'output') 105 | ], [ 106 | new Connection(1, 2), 107 | new Connection(1, 3), 108 | new Connection(1, 4), 109 | new Connection(2, 4) 110 | ]) 111 | const newConn = genome.possibleNewConnections() 112 | expect(newConn).to.be.an('array') 113 | expect(newConn).to.have.lengthOf(2) 114 | expect(newConn[0]).to.be.an('array') 115 | expect(newConn[0][0]).to.equal(2) 116 | expect(newConn[0][1]).to.equal(3) 117 | expect(newConn[1]).to.be.an('array') 118 | expect(newConn[1][0]).to.equal(3) 119 | expect(newConn[1][1]).to.equal(4) 120 | }) 121 | }) 122 | 123 | describe('distance', () => { 124 | it('should calculate distance of 0 between same genome', () => { 125 | const genomeA = new Genome(1, 3, [ 126 | new Node(1, 'input'), 127 | new Node(2, 'hidden'), 128 | new Node(3, 'hidden'), 129 | new Node(4, 'output') 130 | ], [ 131 | new Connection(1, 2), 132 | new Connection(1, 3), 133 | new Connection(1, 4), 134 | new Connection(2, 4) 135 | ]) 136 | const distance = genomeA.distance(genomeA) 137 | expect(distance).to.equal(0) 138 | }) 139 | it('should calculate distance between two genomes', () => { 140 | const genomeA = new Genome(1, 3, [ 141 | new Node(1, 'input'), 142 | new Node(2, 'hidden'), 143 | new Node(3, 'hidden'), 144 | new Node(4, 'output') 145 | ], [ 146 | new Connection(1, 2), 147 | new Connection(1, 3), 148 | new Connection(1, 4), 149 | new Connection(2, 4) 150 | ]) 151 | const genomeB = genomeA 152 | const distance = genomeA.distance(genomeB) 153 | const distance2 = genomeB.distance(genomeA) 154 | expect(distance).to.be.a('number') 155 | expect(distance).to.equal(distance2) 156 | }) 157 | it('should increase distance when mutation occurs', () => { 158 | const genomeA = new Genome(1, 3, [ 159 | new Node(1, 'input'), 160 | new Node(2, 'hidden'), 161 | new Node(3, 'hidden'), 162 | new Node(4, 'output') 163 | ], [ 164 | new Connection(1, 2), 165 | new Connection(1, 3), 166 | new Connection(1, 4), 167 | new Connection(2, 4) 168 | ]) 169 | const genomeB = new Genome(1, 3, [ 170 | new Node(1, 'input'), 171 | new Node(2, 'hidden'), 172 | new Node(3, 'hidden'), 173 | new Node(4, 'output') 174 | ], [ 175 | new Connection(1, 2), 176 | new Connection(1, 3), 177 | new Connection(1, 4), 178 | new Connection(2, 4) 179 | ]) 180 | const distance1 = genomeA.distance(genomeB) 181 | genomeB.mutate() 182 | genomeB.mutate() 183 | genomeB.mutate() 184 | genomeB.mutate() 185 | const distance2 = genomeA.distance(genomeB) 186 | expect(distance2).to.be.above(distance1) 187 | }) 188 | }) 189 | 190 | describe('mutate', () => { 191 | it('should either add node, connection or weight', () => { 192 | const genome = new Genome(1, 2) 193 | genome.addConnection() 194 | genome.mutate() 195 | expect(genome.connections.length + genome.nodes.length).to.be.above(3) 196 | }) 197 | }) 198 | 199 | describe('addConnection', () => { 200 | it('should create new connection with increasing innovation numbers', () => { 201 | const genome = new Genome(1, 3, [ 202 | new Node(1, 'input'), 203 | new Node(3, 'hidden'), 204 | new Node(4, 'hidden'), 205 | new Node(2, 'output') 206 | ], null) 207 | genome.addConnection() 208 | genome.addConnection() 209 | expect(genome.connections).to.be.an('array') 210 | expect(genome.connections).to.have.lengthOf(2) 211 | expect(genome.connections[1].innovationNumber).to.be.above(genome.connections[0].innovationNumber) 212 | }) 213 | }) 214 | 215 | describe('crossover', () => { 216 | it('should create a child genome with all properties', () => { 217 | const genome1 = new Genome() 218 | const genome2 = new Genome() 219 | const children = genome1.crossover(genome2) 220 | expect(children).to.be.an('object') 221 | expect(children).to.have.all.keys('nodes', 'nodeCount', 'connections', 'nbInput', 'nbOutput', 'fitness', 'sharedFitness') 222 | }) 223 | 224 | it('should create a child with genes from fitest parent', () => { 225 | const genome1 = new Genome(1, 2) 226 | const genome2 = genome1 227 | genome2.addConnection() 228 | genome2.addConnection() 229 | const smallFitness = () => 0.4 230 | genome1.calculateFitness(smallFitness) 231 | const betterFitness = () => 0.8 232 | genome2.calculateFitness(betterFitness) 233 | const children = genome1.crossover(genome2) 234 | expect(children.nodes).to.have.lengthOf(3) 235 | expect(children.connections).to.have.lengthOf(2) 236 | }) 237 | }) 238 | 239 | describe('generateNetwork', () => { 240 | it('should generate network', () => { 241 | const genome = new Genome(1, 2) 242 | const network = genome.generateNetwork() 243 | expect(network).to.be.an('object') 244 | // @TODO 245 | }) 246 | }) 247 | }) 248 | --------------------------------------------------------------------------------