diff --git a/.gitignore b/.gitignore index b7bca7a..525a71f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,14 +12,12 @@ # Generated by Cargo /target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock # Vim *.swp *.swo +rusty-tags.vi # Fmt **/*.rs.bk diff --git a/.travis.yml b/.travis.yml index bf31bf9..99e23d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ before_script: - | (which cargo-install-update && cargo install-update cargo-update) || cargo install cargo-update && (which cargo-prune && cargo install-update cargo-prune) || cargo install cargo-prune - - rustup component add rustfmt-preview + - rustup component add rustfmt - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export OPENSSL_INCLUDE_DIR=`brew --prefix openssl`/include; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export OPENSSL_LIB_DIR=`brew --prefix openssl`/lib; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export DEP_OPENSSL_INCLUDE=`brew --prefix openssl`/include; fi @@ -25,7 +25,7 @@ script: - if [ "${TRAVIS_RUST_VERSION}" = stable ]; then ( set -x; - cargo fmt -- --write-mode=diff + cargo fmt ); fi - cargo test --verbose --release diff --git a/Cargo.toml b/Cargo.toml index 100a6a2..c3b6fb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,14 +7,15 @@ keywords = ["neuroevolution", "neat", "aumenting-topologies", "genetic", "algori license = "MIT" name = "rustneat" repository = "https://github.com/TLmaK0/rustneat" -version = "0.2.1" +version = "0.3.0" +edition = "2018" [dependencies] conv = "0.3.2" crossbeam = "0.2" lazy_static = "0.2.2" num_cpus = "1.0" -rand = "0.4" +rand = "0.6" rulinalg = "0.3.4" rusty_dashed = { version = "0.2.1", optional = true } @@ -38,3 +39,6 @@ required-features = ["cpython"] [[example]] name = "simple_sample" + +[[example]] +name = "function_approximation" diff --git a/README.md b/README.md index b2f2869..f30c2fb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Working-in-progress Implementation of **NeuroEvolution of Augmenting Topologies NEAT** http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf -This implementations uses a **Continuous-Time Recurrent Neural Network** (**CTRNN**) (Yamauchi and Beer, 1994). +This implementations uses a **CTRNN** based on**On the Dynamics of Small Continuous-Time Recurrent Neural Network** (Beer, 1995) http://www.cs.uvm.edu/~jbongard/2014_CS206/Beer_CTRNNs.pdf ![telemetry](docs/img/rustneat.png) diff --git a/examples/function_approximation.rs b/examples/function_approximation.rs new file mode 100644 index 0000000..0c7117a --- /dev/null +++ b/examples/function_approximation.rs @@ -0,0 +1,72 @@ +extern crate rand; +extern crate rustneat; + +#[cfg(feature = "telemetry")] +#[macro_use] +extern crate rusty_dashed; + +#[cfg(feature = "telemetry")] +mod telemetry_helper; + +use rustneat::{Environment, NeuralNetwork, Organism, Population}; + +static mut BEST_FITNESS: f64 = 0.0; +struct FunctionApproximation; + +impl Environment for FunctionApproximation { + fn test(&self, organism: &mut NeuralNetwork) -> f64 { + let mut output = vec![0f64]; + let mut distance = 0f64; + + let mut outputs = Vec::new(); + + for x in -10..11 { + organism.activate(vec![x as f64 / 10f64], &mut output); + distance += ((x as f64).powf(2f64) - (output[0] * 100f64)).abs(); + outputs.push([x, (output[0] * 100f64) as i64]); + } + + let fitness = 100f64 / (1f64 + (distance / 1000.0)); + unsafe { + if fitness > BEST_FITNESS { + BEST_FITNESS = fitness; + #[cfg(feature = "telemetry")] + telemetry!("approximation1", 1.0, format!("{:?}", outputs)); + } + } + fitness + } +} + +fn main() { + let mut population = Population::create_population(150); + let mut environment = FunctionApproximation; + let mut champion: Option> = None; + + #[cfg(feature = "telemetry")] + telemetry_helper::enable_telemetry("?max_fitness=100&ioNeurons=1,2", true); + + #[cfg(feature = "telemetry")] + std::thread::sleep(std::time::Duration::from_millis(2000)); + + #[cfg(feature = "telemetry")] + telemetry!( + "approximation1", + 1.0, + format!("{:?}", (-10..11).map(|x| [x, x * x]).collect::>()) + ); + + #[cfg(feature = "telemetry")] + std::thread::sleep(std::time::Duration::from_millis(2000)); + + while champion.is_none() { + population.evolve(); + population.evaluate_in(&mut environment); + for organism in &population.get_organisms() { + if organism.fitness >= 96f64 { + champion = Some(organism.clone()); + } + } + } + println!("{:?}", champion.unwrap().genome); +} diff --git a/examples/simple_sample.rs b/examples/simple_sample.rs index 7338c1a..be3c14d 100644 --- a/examples/simple_sample.rs +++ b/examples/simple_sample.rs @@ -1,29 +1,27 @@ extern crate rand; extern crate rustneat; -use rustneat::Environment; -use rustneat::Organism; -use rustneat::Population; +use rustneat::{Environment, NeuralNetwork, Organism, Population}; #[cfg(feature = "telemetry")] mod telemetry_helper; struct XORClassification; -impl Environment for XORClassification { - fn test(&self, organism: &mut Organism) -> f64 { +impl Environment for XORClassification { + fn test(&self, organism: &mut NeuralNetwork) -> f64 { let mut output = vec![0f64]; let mut distance: f64; - organism.activate(&vec![0f64, 0f64], &mut output); - distance = (0f64 - output[0]).abs(); - organism.activate(&vec![0f64, 1f64], &mut output); - distance += (1f64 - output[0]).abs(); - organism.activate(&vec![1f64, 0f64], &mut output); - distance += (1f64 - output[0]).abs(); - organism.activate(&vec![1f64, 1f64], &mut output); - distance += (0f64 - output[0]).abs(); + organism.activate(vec![0f64, 0f64], &mut output); + distance = (0f64 - output[0]).powi(2); + organism.activate(vec![0f64, 1f64], &mut output); + distance += (1f64 - output[0]).powi(2); + organism.activate(vec![1f64, 0f64], &mut output); + distance += (1f64 - output[0]).powi(2); + organism.activate(vec![1f64, 1f64], &mut output); + distance += (0f64 - output[0]).powi(2); - let fitness = (4f64 - distance).powi(2); + let fitness = 16.0 / (1.0 + distance); fitness } @@ -31,19 +29,31 @@ impl Environment for XORClassification { fn main() { #[cfg(feature = "telemetry")] - telemetry_helper::enable_telemetry("?max_fitness=17"); + telemetry_helper::enable_telemetry("?max_fitness=16", true); + + #[cfg(feature = "telemetry")] + std::thread::sleep(std::time::Duration::from_millis(2000)); let mut population = Population::create_population(150); let mut environment = XORClassification; - let mut champion: Option = None; + let mut champion: Option> = None; + let mut i = 0; while champion.is_none() { + i += 1; population.evolve(); population.evaluate_in(&mut environment); + let mut best_fitness = 0.0; for organism in &population.get_organisms() { - if organism.fitness > 15.9f64 { + if organism.fitness > best_fitness { + best_fitness = organism.fitness; + } + if organism.fitness > 15.5 { champion = Some(organism.clone()); } } + if i % 10 == 0 { + println!("Gen {}: {}", i, best_fitness); + } } println!("{:?}", champion.unwrap().genome); } diff --git a/examples/telemetry_helper.rs b/examples/telemetry_helper.rs index 6ef5cbc..9e1ab46 100644 --- a/examples/telemetry_helper.rs +++ b/examples/telemetry_helper.rs @@ -8,17 +8,23 @@ extern crate rusty_dashed; use self::rusty_dashed::Dashboard; #[allow(dead_code)] -pub fn main(){} +pub fn main() {} #[cfg(feature = "telemetry")] -pub fn enable_telemetry(query_string: &str) { +pub fn enable_telemetry(query_string: &str, open: bool) { let mut dashboard = Dashboard::new(); dashboard.add_graph("fitness1", "fitness", 0, 0, 4, 4); dashboard.add_graph("network1", "network", 4, 0, 4, 4); + dashboard.add_graph("approximation1", "approximation", 0, 4, 2, 2); rusty_dashed::Server::serve_dashboard(dashboard); let url = format!("http://localhost:3000{}", query_string); + + if !open { + return; + } + match open::that(url.clone()) { Err(_) => println!( "\nOpen browser and go to {:?} to see how neural network evolves\n", diff --git a/graphs/approximation.css b/graphs/approximation.css new file mode 100644 index 0000000..0c84248 --- /dev/null +++ b/graphs/approximation.css @@ -0,0 +1,8 @@ +#approximation1 svg { + width: 100%; + height: 100%; +} + +#approximation1 path { + fill: none; +} diff --git a/graphs/approximation.js b/graphs/approximation.js new file mode 100644 index 0000000..0d83717 --- /dev/null +++ b/graphs/approximation.js @@ -0,0 +1,40 @@ +var svg, paths = []; + +var lineChart; + +function approximation_init(id){ + svg = d3.select('#' + id).append('svg'); + var width = $('#' + id).children().width() / 2; + var rangeX = [-width, width]; + var height = $('#' + id).children().height(); + var rangeY = [0, height]; + + for(var n=0; n<10; n++ ) { + paths[n] = svg.append('path'); + paths[n].attr("transform", "translate(" + width + ")"); + paths[n].attr("stroke", "rgb(" + (50 + n * 20) + ",0,0)"); + } + paths[0].attr("stroke", "rgb(200,200,200)"); + scaleX = d3.scaleLinear().domain([-10, 10]).range(rangeX); + scaleY = d3.scaleLinear().domain([0, 100]).range(rangeY); + + lineChart = d3.line() + .x(function(d){ return scaleX(d[0]); }) + .y(function(d){ return height - scaleY(d[1]); }); +} + +var functionToApproximate = []; +var functionApproximations = []; + +function approximation(id, value){ + if (functionToApproximate.length == 0) functionToApproximate = value; + if (functionApproximations.length == 9) functionApproximations.shift(); + + functionApproximations.push(value); + + paths[0].attr("d", lineChart(functionToApproximate)); + + for(var n=0; n < functionApproximations.length; n++ ){ + paths[n + 1].attr("d", lineChart(functionApproximations[n])); + } +} diff --git a/graphs/fitness.css b/graphs/fitness.css index d43c9c0..f7185e1 100644 --- a/graphs/fitness.css +++ b/graphs/fitness.css @@ -1,9 +1,9 @@ -svg { +#fitness1 svg { width: 100%; height: 100%; } -svg path { +#fitness1 svg path { stroke: black; fill: none; } diff --git a/graphs/network.js b/graphs/network.js index 1778e41..b9d83c0 100644 --- a/graphs/network.js +++ b/graphs/network.js @@ -1,3 +1,6 @@ +var ioNeurons = (getParameterByName('ioNeurons') || '0,0').split(','); +console.log(ioNeurons); + var simulation; var color; var link; @@ -45,7 +48,10 @@ var allLinks = {}; function addIfNewNode(nodes, newNodes, node_id){ if (nodes.indexOf(node_id) < 0) { - newNodes.push({id: "node" + node_id, group: 0}); + var group = 0; + if (node_id < ioNeurons[0]) group = 3; + else if (node_id < ioNeurons[1]) group = 7; + newNodes.push({id: "node" + node_id, group: group}); nodes.push(node_id); } } @@ -210,3 +216,12 @@ function network(id, genes){ } } +function getParameterByName(name) { + var url = window.location.href; + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")); +} diff --git a/src/ctrnn.rs b/src/ctrnn.rs deleted file mode 100644 index 60297ee..0000000 --- a/src/ctrnn.rs +++ /dev/null @@ -1,181 +0,0 @@ -use rulinalg::matrix::{BaseMatrix, BaseMatrixMut, Matrix}; -#[allow(missing_docs)] -#[derive(Debug)] -pub struct CtrnnNeuralNetwork<'a> { - pub gamma: &'a [f64], - pub delta_t: f64, - pub tau: &'a [f64], - pub wij: &'a (usize, usize, &'a [f64]), - pub theta: &'a [f64], - pub wik: &'a (usize, usize, &'a [f64]), - pub i: &'a [f64], -} - -#[allow(missing_docs)] -#[derive(Default, Clone, Copy, Debug)] -pub struct Ctrnn {} - -impl Ctrnn { - /// Activate the NN - // TODO Not sure steps are required here? - pub fn activate_nn(&self, steps: usize, nn: &CtrnnNeuralNetwork) -> Vec { - let mut state = Ctrnn::matrix_from_vector(nn.gamma); - let theta = Ctrnn::matrix_from_vector(nn.theta); - let wij = Ctrnn::matrix_from_matrix(nn.wij); - let wik = Ctrnn::matrix_from_matrix(nn.wik); - let i = Ctrnn::matrix_from_vector(nn.i); - let tau = Ctrnn::matrix_from_vector(nn.tau); - let delta_t_tau = tau.apply(&(|x| 1.0 / x)) * nn.delta_t; - for _ in 0..steps { - state = &state - + delta_t_tau.elemul( - &((&wij * (&state - &theta).apply(&Ctrnn::sigmoid)) - &state + (&wik * &i)), - ); - } - state - .apply(&(|x| (x - 3.0) * 2.0)) - .apply(&Ctrnn::sigmoid) - .into_vec() - } - - #[allow(missing_docs)] - #[deprecated(since = "0.1.7", note = "please use `activate_nn` instead")] - pub fn activate( - &self, - steps: usize, - gamma: &[f64], - delta_t: f64, - tau: &[f64], - wij: &(usize, usize, Vec), - theta: &[f64], - wik: &(usize, usize, Vec), - i: &[f64], - ) -> Vec { - self.activate_nn( - steps, - &CtrnnNeuralNetwork { - gamma: gamma, - delta_t: delta_t, - tau: tau, - wij: &(wij.0, wij.1, wij.2.as_slice()), - theta: theta, - wik: &(wik.0, wik.1, wik.2.as_slice()), - i: i, - }, - ) - } - - fn sigmoid(y: f64) -> f64 { - 1f64 / (1f64 + (-y).exp()) - } - - fn matrix_from_vector(vector: &[f64]) -> Matrix { - Matrix::new(vector.len(), 1, vector) - } - - fn matrix_from_matrix(matrix: &(usize, usize, &[f64])) -> Matrix { - Matrix::new(matrix.0, matrix.1, matrix.2) - } -} - -#[cfg(test)] -mod tests { - use super::*; - macro_rules! assert_delta_vector { - ($x:expr, $y:expr, $d:expr) => { - for pos in 0..$x.len() { - if !(($x[pos] - $y[pos]).abs() <= $d) { - panic!( - "Element at position {:?} -> {:?} \ - is not equal to {:?}", - pos, $x[pos], $y[pos] - ); - } - } - }; - } - - #[test] - fn neural_network_activation_should_return_correct_values() { - let gamma = vec![0.0, 0.0, 0.0]; - let delta_t = 13.436; - let tau = vec![61.694, 10.149, 16.851]; - let wij = ( - 3, - 3, - vec![ - -2.94737, 2.70665, -0.57046, -3.27553, 3.67193, 1.83218, 2.32476, 0.24739, 0.58587, - ], - ); - let theta = vec![-0.695126, -0.677891, -0.072129]; - let wik = ( - 3, - 2, - vec![-0.10097, -3.04457, -4.86594, 1.79273, -3.45899, -1.27388], - ); - let i = vec![0.98856, 0.31540]; - - let nn = CtrnnNeuralNetwork { - gamma: gamma.as_slice(), - delta_t: delta_t, - tau: tau.as_slice(), - wij: &(wij.0, wij.1, wij.2.as_slice()), - theta: theta.as_slice(), - wik: &(wik.0, wik.1, wik.2.as_slice()), - i: i.as_slice(), - }; - - let ctrnn = Ctrnn::default(); - - assert_delta_vector!( - ctrnn.activate_nn(1, &nn), - vec![ - 0.0012732326259646935, - 0.0000007804325967431104, - 0.00013984620250072583, - ], - 0.00000000000000000001 - ); - - assert_delta_vector!( - ctrnn.activate_nn(2, &nn), - vec![ - 0.00043073019717790323, - 0.000000009937039489593933, - 0.000034080215678448577, - ], - 0.00000000000000000001 - ); - - assert_delta_vector!( - ctrnn.activate_nn(10, &nn), - vec![ - 0.00007325263764065628, - 0.00000012140174814281648, - 0.000004220860839220797, - ], - 0.00000000000000000001 - ); - - assert_delta_vector!( - ctrnn.activate_nn(30, &nn), - vec![ - 0.00006952721528466206, - 0.00000012669416324530944, - 0.000004043510745829741, - ], - 0.00000000000000000001 - ); - - // converges - assert_delta_vector!( - ctrnn.activate_nn(100, &nn), - vec![ - 0.00006952654167069687, - 0.0000001266951605597891, - 0.000004043479141786699, - ], - 0.00000000000000000001 - ); - } -} diff --git a/src/environment.rs b/src/environment.rs index 6c38688..50f6325 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -1,8 +1,8 @@ -use organism::Organism; +use crate::Genome; -/// A trait that is implemented by user to allow test of the Environment. -pub trait Environment: Sync { +/// A trait that is implemented by user to test the fitness of organisms. +pub trait Environment: Sync { /// This test will return the value required by this enviroment to test /// against - fn test(&self, organism: &mut Organism) -> f64; + fn test(&self, organism: &mut G) -> f64; } diff --git a/src/genome.rs b/src/genome.rs index 19cd2e9..4af60ad 100644 --- a/src/genome.rs +++ b/src/genome.rs @@ -1,342 +1,58 @@ -use gene::Gene; -use mutation::Mutation; -use rand::{self, Closed01}; -use std::cmp; +const COMPATIBILITY_THRESHOLD: f64 = 3.0; // used to speciate organisms -/// Vector of Genes -/// Holds a count of last neuron added, similar to Innovation number -#[derive(Default, Debug, Clone)] -pub struct Genome { - genes: Vec, - last_neuron_id: usize, -} - -const COMPATIBILITY_THRESHOLD: f64 = 3f64; -const MUTATE_CONNECTION_WEIGHT: f64 = 0.90f64; -const MUTATE_ADD_CONNECTION: f64 = 0.005f64; -const MUTATE_ADD_NEURON: f64 = 0.004f64; -const MUTATE_TOGGLE_EXPRESSION: f64 = 0.001f64; -const MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY: f64 = 0.90f64; - -impl Genome { - /// May add a connection &| neuron &| mutat connection weight &| - /// enable/disable connection - pub fn mutate(&mut self) { - if rand::random::>().0 < MUTATE_ADD_CONNECTION || self.genes.is_empty() { - self.mutate_add_connection(); - }; - - if rand::random::>().0 < MUTATE_ADD_NEURON { - self.mutate_add_neuron(); - }; - - if rand::random::>().0 < MUTATE_CONNECTION_WEIGHT { - self.mutate_connection_weight(); - }; - - if rand::random::>().0 < MUTATE_TOGGLE_EXPRESSION { - self.mutate_toggle_expression(); - }; - } - - /// Mate two genes - pub fn mate(&self, other: &Genome, fittest: bool) -> Genome { - if self.genes.len() > other.genes.len() { - self.mate_genes(other, fittest) - } else { - other.mate_genes(self, !fittest) - } - } - - fn mate_genes(&self, other: &Genome, fittest: bool) -> Genome { - let mut genome = Genome::default(); - for gene in &self.genes { - genome.add_gene({ - if !fittest || rand::random::() > 0.5f64 { - *gene - } else { - match other.genes.binary_search(gene) { - Ok(position) => other.genes[position], - Err(_) => *gene, - } - } - }); - } - genome - } - /// Get vector of all genes in this genome - pub fn get_genes(&self) -> &Vec { - &self.genes - } - - /// only allow connected nodes - pub fn inject_gene(&mut self, in_neuron_id: usize, out_neuron_id: usize, weight: f64) { - let max_neuron_id = self.last_neuron_id + 1; - - if in_neuron_id == out_neuron_id && in_neuron_id > max_neuron_id { - panic!( - "Try to create a gene neuron unconnected, max neuron id {}, {} -> {}", - max_neuron_id, in_neuron_id, out_neuron_id - ); - } - - assert!( - in_neuron_id <= max_neuron_id, - format!( - "in_neuron_id {} is greater than max allowed id {}", - in_neuron_id, max_neuron_id - ) - ); - assert!( - out_neuron_id <= max_neuron_id, - format!( - "out_neuron_id {} is greater than max allowed id {}", - out_neuron_id, max_neuron_id - ) - ); - - self.create_gene(in_neuron_id, out_neuron_id, weight) - } - /// Number of genes - pub fn len(&self) -> usize { - self.last_neuron_id + 1 // first neuron id is 0 - } - /// is genome empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - fn create_gene(&mut self, in_neuron_id: usize, out_neuron_id: usize, weight: f64) { - let gene = Gene::new(in_neuron_id, out_neuron_id, weight, true); - self.add_gene(gene); - } +/// Implementing `Genome` conceptually means that the implementor "has a +/// genome", and the implementor can be called an "organism". +// (TODO: remove Default?) +pub trait Genome: Clone + Default + Send { + /// Returns a new organism which is a clone of `&self` apart from possible + /// mutations + fn mutate(&self) -> Self; - fn mutate_add_connection(&mut self) { - let mut rng = rand::thread_rng(); - let neuron_ids_to_connect = { - if self.last_neuron_id == 0 { - vec![0, 0] - } else { - rand::seq::sample_iter(&mut rng, 0..self.last_neuron_id + 1, 2).unwrap() - } - }; - self.add_connection(neuron_ids_to_connect[0], neuron_ids_to_connect[1]); - } + /// `fittest` is true if `other` is more fit. + fn mate(&self, other: &Self, fittest: bool) -> Self; - fn mutate_connection_weight(&mut self) { - for gene in &mut self.genes { - Mutation::connection_weight( - gene, - rand::random::() < MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY, - ); - } - } - - fn mutate_toggle_expression(&mut self) { - let mut rng = rand::thread_rng(); - let selected_gene = rand::seq::sample_iter(&mut rng, 0..self.genes.len(), 1).unwrap()[0]; - Mutation::toggle_expression(&mut self.genes[selected_gene]); - } - - fn mutate_add_neuron(&mut self) { - let (gene1, gene2) = { - let mut rng = rand::thread_rng(); - let selected_gene = - rand::seq::sample_iter(&mut rng, 0..self.genes.len(), 1).unwrap()[0]; - let gene = &mut self.genes[selected_gene]; - self.last_neuron_id += 1; - Mutation::add_neuron(gene, self.last_neuron_id) - }; - self.add_gene(gene1); - self.add_gene(gene2); - } - - fn add_connection(&mut self, in_neuron_id: usize, out_neuron_id: usize) { - let gene = Mutation::add_connection(in_neuron_id, out_neuron_id); - self.add_gene(gene); - } - - fn add_gene(&mut self, gene: Gene) { - if gene.in_neuron_id() > self.last_neuron_id { - self.last_neuron_id = gene.in_neuron_id(); - } - if gene.out_neuron_id() > self.last_neuron_id { - self.last_neuron_id = gene.out_neuron_id(); - } - match self.genes.binary_search(&gene) { - Ok(pos) => self.genes[pos].set_enabled(), - Err(_) => self.genes.push(gene), - } - self.genes.sort(); - } + /// TODO: how should it be implemented for e.g. a composed organism? + fn distance(&self, other: &Self) -> f64; /// Compare another Genome for species equality // TODO This should be impl Eq - pub fn is_same_specie(&self, other: &Genome) -> bool { - self.compatibility_distance(other) < COMPATIBILITY_THRESHOLD - } - - /// Total weigths of all genes - pub fn total_weights(&self) -> f64 { - let mut total = 0f64; - for gene in &self.genes { - total += gene.weight(); - } - total - } - - /// Total num genes - // TODO len() is enough - pub fn total_genes(&self) -> usize { - self.genes.len() - } - - // http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf - Pag. 110 - // I have considered disjoint and excess genes as the same - fn compatibility_distance(&self, other: &Genome) -> f64 { - // TODO: optimize this method - let c2 = 1f64; - let c3 = 0.4f64; - - // Number of excess - let n1 = self.genes.len(); - let n2 = other.genes.len(); - let n = cmp::max(n1, n2); - - if n == 0 { - return 0f64; // no genes in any genome, the genomes are equal - } - - let z = if n < 20 { 1f64 } else { n as f64 }; - - let matching_genes = self.genes - .iter() - .filter(|i1_gene| other.genes.contains(i1_gene)) - .collect::>(); - let n3 = matching_genes.len(); - - // Disjoint / excess genes - let d = n1 + n2 - (2 * n3); - - // average weight differences of matching genes - let w1 = matching_genes.iter().fold(0f64, |acc, &m_gene| { - acc + (m_gene.weight() - - &other.genes[other.genes.binary_search(m_gene).unwrap()].weight()) - .abs() - }); - - let w = if n3 == 0 { 0f64 } else { w1 / n3 as f64 }; - - // compatibility distance - (c2 * d as f64 / z) + c3 * w + fn is_same_specie(&self, other: &Self) -> bool { + self.distance(other) < COMPATIBILITY_THRESHOLD } } -#[cfg(test)] -mod tests { - use super::*; - use std::f64::EPSILON; - - #[test] - fn mutation_connection_weight() { - let mut genome = Genome::default(); - genome.inject_gene(0, 0, 1f64); - let orig_gene = genome.genes[0]; - genome.mutate_connection_weight(); - // These should not be same size - assert!((genome.genes[0].weight() - orig_gene.weight()).abs() > EPSILON); - } - - #[test] - fn mutation_add_connection() { - let mut genome = Genome::default(); - genome.add_connection(1, 2); - - assert!(genome.genes[0].in_neuron_id() == 1); - assert!(genome.genes[0].out_neuron_id() == 2); - } - - #[test] - fn mutation_add_neuron() { - let mut genome = Genome::default(); - genome.mutate_add_connection(); - genome.mutate_add_neuron(); - assert!(!genome.genes[0].enabled()); - assert!(genome.genes[1].in_neuron_id() == genome.genes[0].in_neuron_id()); - assert!(genome.genes[1].out_neuron_id() == 1); - assert!(genome.genes[2].in_neuron_id() == 1); - assert!(genome.genes[2].out_neuron_id() == genome.genes[0].out_neuron_id()); - } - - #[test] - #[should_panic(expected = "Try to create a gene neuron unconnected, max neuron id 1, 2 -> 2")] - fn try_to_inject_a_unconnected_neuron_gene_should_panic() { - let mut genome1 = Genome::default(); - genome1.inject_gene(2, 2, 0.5f64); - } - - #[test] - fn two_genomes_without_differences_should_be_in_same_specie() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 1f64); - genome1.inject_gene(0, 1, 1f64); - let mut genome2 = Genome::default(); - genome2.inject_gene(0, 0, 0f64); - genome2.inject_gene(0, 1, 0f64); - genome2.inject_gene(0, 2, 0f64); - assert!(genome1.is_same_specie(&genome2)); - } - - #[test] - fn two_genomes_with_enought_difference_should_be_in_different_species() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 1f64); - genome1.inject_gene(0, 1, 1f64); - let mut genome2 = Genome::default(); - genome2.inject_gene(0, 0, 5f64); - genome2.inject_gene(0, 1, 5f64); - genome2.inject_gene(0, 2, 1f64); - genome2.inject_gene(0, 3, 1f64); - assert!(!genome1.is_same_specie(&genome2)); - } - - #[test] - fn already_existing_gene_should_be_not_duplicated() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 1f64); - genome1.add_connection(0, 0); - assert_eq!(genome1.genes.len(), 1); - assert!((genome1.get_genes()[0].weight() - 1f64).abs() < EPSILON); - } - - #[test] - fn adding_an_existing_gene_disabled_should_enable_original() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 1, 0f64); - genome1.mutate_add_neuron(); - assert!(!genome1.genes[0].enabled()); - assert!(genome1.genes.len() == 3); - genome1.add_connection(0, 1); - assert!(genome1.genes[0].enabled()); - assert!((genome1.genes[0].weight() - 0f64).abs() < EPSILON); - assert_eq!(genome1.genes.len(), 3); - } - - #[test] - fn genomes_with_same_genes_with_little_differences_on_weight_should_be_in_same_specie() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 16f64); - let mut genome2 = Genome::default(); - genome2.inject_gene(0, 0, 16.1f64); - assert!(genome1.is_same_specie(&genome2)); - } - - #[test] - fn genomes_with_same_genes_with_big_differences_on_weight_should_be_in_other_specie() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 5f64); - let mut genome2 = Genome::default(); - genome2.inject_gene(0, 0, 15f64); - assert!(!genome1.is_same_specie(&genome2)); +/// Used in algorithm just to group an organism (genome) with its fitness, and +/// also in the interface to get the fitness of organisms +#[derive(Default, Clone, Debug)] +pub struct Organism { + /// The genome of this organism + pub genome: G, + /// The fitness calculated internally + // TODO: Make fitness private with a getter? + // or Option + pub fitness: f64, +} +impl Organism { + /// Create a new organism with fitness 0.0. + pub fn new(organism: G) -> Organism { + Organism { + genome: organism, + fitness: 0.0, + } + } + /// Returns a cloned `Organism` with a mutated genome + pub fn mutate(&self) -> Organism { + Organism::new(self.genome.mutate()) + } + /// Mate with another organism -- this mates the two genomes. + pub fn mate(&self, other: &Self) -> Organism { + Organism::new( + self.genome + .mate(&other.genome, self.fitness < other.fitness), + ) + } + /// + pub fn distance(&self, other: &Self) -> f64 { + self.genome.distance(&other.genome) } } diff --git a/src/lib.rs b/src/lib.rs index 45c0388..fd7f4b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,17 @@ #![deny( - missing_docs, trivial_casts, trivial_numeric_casts, unsafe_code, unused_import_braces, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unused_import_braces, unused_qualifications )] #![cfg_attr(feature = "clippy", feature(plugin))] #![cfg_attr(feature = "clippy", plugin(clippy))] -#![cfg_attr(feature = "clippy", deny(clippy, unicode_not_nfc, wrong_pub_self_convention))] +#![cfg_attr( + feature = "clippy", + deny(clippy, unicode_not_nfc, wrong_pub_self_convention) +)] #![cfg_attr(feature = "clippy", allow(use_debug, too_many_arguments))] //! Implementation of `NeuroEvolution` of Augmenting Topologies [NEAT] @@ -16,12 +23,6 @@ #[macro_use] extern crate rusty_dashed; -extern crate conv; -extern crate crossbeam; -extern crate num_cpus; -extern crate rand; -extern crate rulinalg; - #[cfg(feature = "telemetry")] #[macro_use] extern crate serde_derive; @@ -29,26 +30,21 @@ extern crate serde_derive; #[cfg(feature = "telemetry")] extern crate serde_json; -pub use self::ctrnn::Ctrnn; pub use self::environment::Environment; -pub use self::gene::Gene; -pub use self::genome::Genome; -pub use self::organism::Organism; +pub use self::genome::*; +pub use self::nn::{Gene, NeuralNetwork}; pub use self::population::Population; pub use self::specie::Specie; pub use self::species_evaluator::SpeciesEvaluator; -pub use ctrnn::CtrnnNeuralNetwork; -mod ctrnn; /// Trait to define test parameter -pub mod environment; -mod gene; +mod environment; /// A collection of genes -pub mod genome; -mod mutation; -/// A genome plus fitness -pub mod organism; +mod genome; +/// Contains the definition of the genome of neural networks, which is the basic +/// building block of an organism (and in many cases, the only building block). +pub mod nn; /// A collection of species with champion -pub mod population; +mod population; mod specie; mod species_evaluator; diff --git a/src/mutation.rs b/src/mutation.rs deleted file mode 100644 index dcbc4f6..0000000 --- a/src/mutation.rs +++ /dev/null @@ -1,51 +0,0 @@ -use gene::Gene; - -pub trait Mutation {} - -impl Mutation { - pub fn connection_weight(gene: &mut Gene, perturbation: bool) { - let mut new_weight = Gene::generate_weight(); - if perturbation { - new_weight += gene.weight(); - } - gene.set_weight(new_weight); - } - - pub fn add_connection(in_neuron_id: usize, out_neuron_id: usize) -> (Gene) { - Gene::new(in_neuron_id, out_neuron_id, Gene::generate_weight(), true) - } - - pub fn add_neuron(gene: &mut Gene, new_neuron_id: usize) -> (Gene, Gene) { - gene.set_disabled(); - - let gen1 = Gene::new(gene.in_neuron_id(), new_neuron_id, 1f64, true); - - let gen2 = Gene::new(new_neuron_id, gene.out_neuron_id(), gene.weight(), true); - (gen1, gen2) - } - - pub fn toggle_expression(gene: &mut Gene) { - if gene.enabled() { - gene.set_disabled() - } else { - gene.set_enabled() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gene::Gene; - - #[test] - fn mutate_toggle_gene_should_toggle() { - let mut gene = Gene::new(0, 1, 1f64, false); - - Mutation::toggle_expression(&mut gene); - assert!(gene.enabled()); - - Mutation::toggle_expression(&mut gene); - assert!(!gene.enabled()); - } -} diff --git a/src/nn/ctrnn.rs b/src/nn/ctrnn.rs new file mode 100644 index 0000000..a5c18e2 --- /dev/null +++ b/src/nn/ctrnn.rs @@ -0,0 +1,67 @@ +use rulinalg::matrix::{BaseMatrix, BaseMatrixMut, Matrix}; + +#[allow(missing_docs)] +#[derive(Debug)] +pub struct Ctrnn<'a> { + pub y: &'a [f64], + pub delta_t: f64, + pub tau: &'a [f64], // time constant + pub wij: &'a [f64], // weights + pub theta: &'a [f64], // bias + pub i: &'a [f64], // sensors +} + +#[allow(missing_docs)] +impl<'a> Ctrnn<'a> { + pub fn activate_nn(&self, steps: usize) -> Vec { + let mut y = Ctrnn::vector_to_column_matrix(self.y); + let theta = Ctrnn::vector_to_column_matrix(self.theta); + let wij = Ctrnn::vector_to_matrix(self.wij); + let i = Ctrnn::vector_to_column_matrix(self.i); + let tau = Ctrnn::vector_to_column_matrix(self.tau); + let delta_t_tau = tau.apply(&(|x| 1.0 / x)) * self.delta_t; + for _ in 0..steps { + let activations = (&y - &theta).apply(&Ctrnn::sigmoid); + y = &y + delta_t_tau.elemul(&((&wij * activations) - &y + &i)); + } + y.into_vec() + } + + fn sigmoid(y: f64) -> f64 { + 1.0 / (1.0 + (-y).exp()) + } + + fn vector_to_column_matrix(vector: &[f64]) -> Matrix { + Matrix::new(vector.len(), 1, vector) + } + + fn vector_to_matrix(vector: &[f64]) -> Matrix { + let width = (vector.len() as f64).sqrt() as usize; + Matrix::new(width, width, vector) + } +} + +#[cfg(test)] +mod tests { + use super::*; + macro_rules! assert_delta_vector { + ($x:expr, $y:expr, $d:expr) => { + for pos in 0..$x.len() { + if !(($x[pos] - $y[pos]).abs() <= $d) { + panic!( + "Element at position {:?} -> {:?} \ + is not equal to {:?}", + pos, $x[pos], $y[pos] + ); + } + } + }; + } + + #[test] + fn neural_network_activation_stability() { + // TODO + // This test should just ensure that a stable neural network + // implementation doesn't change + } +} diff --git a/src/gene.rs b/src/nn/gene.rs similarity index 72% rename from src/gene.rs rename to src/nn/gene.rs index d4742f9..5b18b1a 100644 --- a/src/gene.rs +++ b/src/nn/gene.rs @@ -1,16 +1,18 @@ -extern crate rand; - -use rand::Closed01; +use rand; use std::cmp::Ordering; -/// A connection Gene +/// Gene for a connection in the `NeuralNetwork` #[derive(Debug, Copy, Clone)] #[cfg_attr(feature = "telemetry", derive(Serialize))] pub struct Gene { in_neuron_id: usize, out_neuron_id: usize, - weight: f64, - enabled: bool, + /// Weight of the connection + pub weight: f64, + /// Whether the expression of a gene is enabled. + pub enabled: bool, + /// Whether this gene functions as a bias in the neural network. + pub is_bias: bool, } impl Eq for Gene {} @@ -47,21 +49,24 @@ impl PartialOrd for Gene { impl Gene { /// Create a new gene - pub fn new(in_neuron_id: usize, out_neuron_id: usize, weight: f64, enabled: bool) -> Gene { + pub fn new( + in_neuron_id: usize, + out_neuron_id: usize, + weight: f64, + enabled: bool, + is_bias: bool, + ) -> Gene { Gene { in_neuron_id: in_neuron_id, out_neuron_id: out_neuron_id, weight: weight, enabled: enabled, + is_bias: is_bias, } } /// Generate a weight pub fn generate_weight() -> f64 { - // TODO Weight of nodes perhaps should be between 0 & 1 (closed) - // rand::random::() * 2f64 - 1f64 - rand::random::>().0 * 2f64 - 1f64 - - // rand::thread_rng().next_f64() + rand::random::() * 2.0 - 1.0 } /// Connection in -> pub fn in_neuron_id(&self) -> usize { @@ -71,26 +76,6 @@ impl Gene { pub fn out_neuron_id(&self) -> usize { self.out_neuron_id } - /// getter for the wight of the gene - pub fn weight(&self) -> f64 { - self.weight - } - /// Setter - pub fn set_weight(&mut self, weight: f64) { - self.weight = weight; - } - /// Is gene enabled - pub fn enabled(&self) -> bool { - self.enabled - } - /// Set gene enabled - pub fn set_enabled(&mut self) { - self.enabled = true; - } - /// Set gene disabled - pub fn set_disabled(&mut self) { - self.enabled = false; - } } impl Default for Gene { @@ -100,6 +85,7 @@ impl Default for Gene { out_neuron_id: 1, weight: Gene::generate_weight(), enabled: true, + is_bias: false, } } } diff --git a/src/nn/mod.rs b/src/nn/mod.rs new file mode 100644 index 0000000..23ac09f --- /dev/null +++ b/src/nn/mod.rs @@ -0,0 +1,563 @@ +use crate::Genome; +use rand::{ + self, + distributions::{Distribution, Uniform}, + seq::IteratorRandom, +}; +use std::cmp; + +mod ctrnn; +mod gene; +pub use self::ctrnn::*; +pub use self::gene::*; + +/// Vector of Genes +/// Holds a count of last neuron added, similar to Innovation number +#[derive(Default, Debug, Clone)] +pub struct NeuralNetwork { + genes: Vec, + last_neuron_id: usize, +} + +const MUTATE_CONNECTION_WEIGHT: f64 = 0.90; +const MUTATE_ADD_CONNECTION: f64 = 0.005; +const MUTATE_ADD_NEURON: f64 = 0.004; +const MUTATE_TOGGLE_EXPRESSION: f64 = 0.001; +const MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY: f64 = 0.90; +const MUTATE_TOGGLE_BIAS: f64 = 0.01; + +impl Genome for NeuralNetwork { + // http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf - Pag. 110 + // I have considered disjoint and excess genes as the same + fn distance(&self, other: &NeuralNetwork) -> f64 { + // TODO: optimize this method + let c2 = 1.0; + let c3 = 0.4; + + // Number of excess + let n1 = self.genes.len(); + let n2 = other.genes.len(); + let n = cmp::max(n1, n2); + + if n == 0 { + return 0.0; // no genes in any genome, the genomes are equal + } + + let z = if n < 20 { 1.0 } else { n as f64 }; + + let matching_genes = self + .genes + .iter() + .filter(|i1_gene| other.genes.contains(i1_gene)) + .collect::>(); + let n3 = matching_genes.len(); + + // Disjoint / excess genes + let d = n1 + n2 - (2 * n3); + + // average weight differences of matching genes + let w1 = matching_genes.iter().fold(0.0, |acc, &m_gene| { + acc + (m_gene.weight - &other.genes[other.genes.binary_search(m_gene).unwrap()].weight) + .abs() + }); + + let w = if n3 == 0 { 0.0 } else { w1 / n3 as f64 }; + + // compatibility distance + (c2 * d as f64 / z) + c3 * w + } + /// May add a connection &| neuron &| mutat connection weight &| + /// enable/disable connection + fn mutate(&self) -> Self { + let mut new = self.clone(); + if rand::random::() < MUTATE_ADD_CONNECTION || new.genes.is_empty() { + new.mutate_add_connection(); + }; + + if rand::random::() < MUTATE_ADD_NEURON { + new.mutate_add_neuron(); + }; + + if rand::random::() < MUTATE_CONNECTION_WEIGHT { + new.mutate_connection_weight(); + }; + + if rand::random::() < MUTATE_TOGGLE_EXPRESSION { + new.mutate_toggle_expression(); + }; + + if rand::random::() < MUTATE_TOGGLE_BIAS { + new.mutate_toggle_bias(); + }; + new + } + + /// Mate two genes + fn mate(&self, other: &NeuralNetwork, fittest: bool) -> NeuralNetwork { + if self.genes.len() > other.genes.len() { + self.mate_genes(other, fittest) + } else { + other.mate_genes(self, !fittest) + } + } +} + +impl NeuralNetwork { + /// Creates a network that with no connections, but enough neurons to cover + /// all inputs and outputs. + pub fn with_input_and_output(inputs: usize, outputs: usize) -> NeuralNetwork { + NeuralNetwork { + genes: Vec::new(), + last_neuron_id: inputs + outputs - 1, + } + } + + /// Activate the neural network by sending input `sensors` into its first + /// `sensors.len()` neurons + pub fn activate(&mut self, sensors: Vec, outputs: &mut Vec) { + let neurons_len = self.n_neurons(); + let sensors_len = sensors.len(); + + let tau = vec![1.0; neurons_len]; + let theta = self.get_bias(); + + let mut i = sensors.clone(); + + if neurons_len < sensors_len { + i.truncate(neurons_len); + } else { + i = [i, vec![0.0; neurons_len - sensors_len]].concat(); + } + + let wij = self.get_weights(); + + let activations = Ctrnn { + y: &i, // initial state is the sensors + delta_t: 1.0, + tau: &tau, + wij: &wij, + theta: &theta, + i: &i, + } + .activate_nn(10); + + if sensors_len < neurons_len { + let outputs_activations = activations.split_at(sensors_len).1.to_vec(); + + for n in 0..cmp::min(outputs_activations.len(), outputs.len()) { + outputs[n] = outputs_activations[n]; + } + } + } + + // Helper function for `activate()` + fn get_weights(&self) -> Vec { + let neurons_len = self.n_neurons(); + let mut matrix = vec![0.0; neurons_len * neurons_len]; + for gene in &self.genes { + if gene.enabled { + matrix[(gene.out_neuron_id() * neurons_len) + gene.in_neuron_id()] = gene.weight + } + } + matrix + } + + // Helper function for `activate()` + fn get_bias(&self) -> Vec { + let neurons_len = self.n_neurons(); + let mut matrix = vec![0.0; neurons_len]; + for gene in &self.genes { + if gene.is_bias { + matrix[gene.in_neuron_id()] += 1.0; + } + } + matrix + } + + /// Get vector of all genes in this genome + pub fn get_genes(&self) -> &Vec { + &self.genes + } + + /// only allow connected nodes + #[deprecated(since = "0.3.0", note = "please use `add_gene` instead")] + pub fn inject_gene(&mut self, in_neuron_id: usize, out_neuron_id: usize, weight: f64) { + let gene = Gene::new(in_neuron_id, out_neuron_id, weight, true, false); + self.add_gene(gene); + } + /// Get number of neurons + pub fn n_neurons(&self) -> usize { + self.last_neuron_id + 1 // first neuron id is 0 + } + /// Get number of connections (this equals the number of genes) + pub fn n_connections(&self) -> usize { + self.genes.len() + } + /// is genome empty + pub fn is_empty(&self) -> bool { + self.n_neurons() == 0 + } + + fn mutate_add_connection(&mut self) { + let mut rng = rand::thread_rng(); + let neuron_ids_to_connect = { + if self.last_neuron_id == 0 { + vec![0, 0] + } else { + (0..self.last_neuron_id + 1).choose_multiple(&mut rng, 2) + } + }; + self.add_connection(neuron_ids_to_connect[0], neuron_ids_to_connect[1]); + } + + fn mutate_connection_weight(&mut self) { + for gene in &mut self.genes { + let perturbation = + rand::random::() < MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY; + + let mut new_weight = Gene::generate_weight(); + if perturbation { + new_weight += gene.weight; + } + gene.weight = new_weight; + } + } + + fn mutate_toggle_expression(&mut self) { + let mut rng = rand::thread_rng(); + let selected_gene = Uniform::from(0..self.genes.len()).sample(&mut rng); + self.genes[selected_gene].enabled = !self.genes[selected_gene].enabled; + } + + fn mutate_toggle_bias(&mut self) { + let mut rng = rand::thread_rng(); + let selected_gene = Uniform::from(0..self.genes.len()).sample(&mut rng); + self.genes[selected_gene].is_bias = !self.genes[selected_gene].is_bias; + } + + fn mutate_add_neuron(&mut self) { + // Select a random gene + let mut rng = rand::thread_rng(); + let gene = Uniform::from(0..self.genes.len()).sample(&mut rng); + // Create new neuron + self.last_neuron_id += 1; + // Disable the selected gene ... + self.genes[gene].enabled = false; + // ... And instead make two connections that go through the new neuron + self.add_gene(Gene::new( + self.genes[gene].in_neuron_id(), + self.last_neuron_id, + 1.0, + true, + false, + )); + self.add_gene(Gene::new( + self.last_neuron_id, + self.genes[gene].out_neuron_id(), + self.genes[gene].weight, + true, + false, + )); + } + + fn add_connection(&mut self, in_neuron_id: usize, out_neuron_id: usize) { + self.add_gene(Gene::new( + in_neuron_id, + out_neuron_id, + Gene::generate_weight(), + true, + false, + )); + } + + fn mate_genes(&self, other: &NeuralNetwork, fittest: bool) -> NeuralNetwork { + let mut genome = NeuralNetwork::default(); + genome.last_neuron_id = std::cmp::max(self.last_neuron_id, other.last_neuron_id); + for gene in &self.genes { + genome.add_gene({ + // Only mate half of the genes randomly + if !fittest || rand::random::() > 0.5 { + *gene + } else { + match other.genes.binary_search(gene) { + Ok(position) => other.genes[position], + Err(_) => *gene, + } + } + }); + } + genome + } + + /// Add a new gene and checks if is allowed. Can only connect to the next + /// neuron or already connected neurons. + pub fn add_gene(&mut self, gene: Gene) { + let max_neuron_id = self.last_neuron_id + 1; + + if gene.in_neuron_id() == gene.out_neuron_id() && gene.in_neuron_id() > max_neuron_id { + panic!( + "Try to create a gene neuron unconnected, max neuron id {}, {} -> {}", + max_neuron_id, + gene.in_neuron_id(), + gene.out_neuron_id() + ); + } + + // assert!( + // gene.in_neuron_id() <= max_neuron_id, + // format!( + // "in_neuron_id {} is greater than max allowed id {}", + // gene.in_neuron_id(), max_neuron_id + // ) + //); + // assert!( + // gene.out_neuron_id() <= max_neuron_id, + // format!( + // "out_neuron_id {} is greater than max allowed id {}", + // gene.out_neuron_id(), max_neuron_id + // ) + //); + + if gene.in_neuron_id() > self.last_neuron_id { + self.last_neuron_id = gene.in_neuron_id(); + } + if gene.out_neuron_id() > self.last_neuron_id { + self.last_neuron_id = gene.out_neuron_id(); + } + match self.genes.binary_search(&gene) { + Ok(pos) => self.genes[pos].enabled = true, + Err(_) => self.genes.push(gene), + } + self.genes.sort(); + } + + /// Total weigths of all genes + pub fn total_weights(&self) -> f64 { + let mut total = 0.0; + for gene in &self.genes { + total += gene.weight; + } + total + } + + /// Total num genes + // TODO len() is enough + pub fn total_genes(&self) -> usize { + self.genes.len() + } +} + +#[cfg(test)] +mod tests { + use crate::{nn::Gene, nn::NeuralNetwork, Genome, Organism}; + use std::f64::EPSILON; + + #[test] + fn mutation_connection_weight() { + let mut genome = NeuralNetwork::default(); + genome.add_gene(Gene::new(0, 0, 1.0, true, false)); + let orig_gene = genome.genes[0]; + genome.mutate_connection_weight(); + // These should not be same size + assert!((genome.genes[0].weight - orig_gene.weight).abs() > EPSILON); + } + + #[test] + fn mutation_add_connection() { + let mut genome = NeuralNetwork::default(); + genome.add_connection(1, 2); + + assert!(genome.genes[0].in_neuron_id() == 1); + assert!(genome.genes[0].out_neuron_id() == 2); + } + + #[test] + fn mutation_add_neuron() { + let mut genome = NeuralNetwork::default(); + genome.mutate_add_connection(); + genome.mutate_add_neuron(); + assert!(!genome.genes[0].enabled); + assert!(genome.genes[1].in_neuron_id() == genome.genes[0].in_neuron_id()); + assert!(genome.genes[1].out_neuron_id() == 1); + assert!(genome.genes[2].in_neuron_id() == 1); + assert!(genome.genes[2].out_neuron_id() == genome.genes[0].out_neuron_id()); + } + + #[test] + #[should_panic(expected = "Try to create a gene neuron unconnected, max neuron id 1, 2 -> 2")] + fn try_to_inject_a_unconnected_neuron_gene_should_panic() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(2, 2, 0.5, true, false)); + } + + #[test] + fn two_genomes_without_differences_should_be_in_same_specie() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 1.0, true, false)); + genome1.add_gene(Gene::new(0, 1, 1.0, true, false)); + let mut genome2 = NeuralNetwork::default(); + genome2.add_gene(Gene::new(0, 0, 0.0, true, false)); + genome2.add_gene(Gene::new(0, 1, 0.0, true, false)); + genome2.add_gene(Gene::new(0, 2, 0.0, true, false)); + assert!(genome1.is_same_specie(&genome2)); + } + + #[test] + fn two_genomes_with_enought_difference_should_be_in_different_species() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 1.0, true, false)); + genome1.add_gene(Gene::new(0, 1, 1.0, true, false)); + let mut genome2 = NeuralNetwork::default(); + genome2.add_gene(Gene::new(0, 0, 5.0, true, false)); + genome2.add_gene(Gene::new(0, 1, 5.0, true, false)); + genome2.add_gene(Gene::new(0, 2, 1.0, true, false)); + genome2.add_gene(Gene::new(0, 3, 1.0, true, false)); + assert!(!genome1.is_same_specie(&genome2)); + } + + #[test] + fn already_existing_gene_should_be_not_duplicated() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 1.0, true, false)); + genome1.add_connection(0, 0); + assert_eq!(genome1.genes.len(), 1); + assert!((genome1.get_genes()[0].weight - 1.0).abs() < EPSILON); + } + + #[test] + fn adding_an_existing_gene_disabled_should_enable_original() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 1, 0.0, true, false)); + genome1.mutate_add_neuron(); + assert!(!genome1.genes[0].enabled); + assert!(genome1.genes.len() == 3); + genome1.add_connection(0, 1); + assert!(genome1.genes[0].enabled); + assert!((genome1.genes[0].weight - 0.0).abs() < EPSILON); + assert_eq!(genome1.genes.len(), 3); + } + + #[test] + fn genomes_with_same_genes_with_little_differences_on_weight_should_be_in_same_specie() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 16.0, true, false)); + let mut genome2 = NeuralNetwork::default(); + genome2.add_gene(Gene::new(0, 0, 16.1, true, false)); + assert!(genome1.is_same_specie(&genome2)); + } + + #[test] + fn genomes_with_same_genes_with_big_differences_on_weight_should_be_in_other_specie() { + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 5.0, true, false)); + let mut genome2 = NeuralNetwork::default(); + genome2.add_gene(Gene::new(0, 0, 15.0, true, false)); + assert!(!genome1.is_same_specie(&genome2)); + } + + // From former genome.rs: + + #[test] + fn should_propagate_signal_without_hidden_layers() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 5.0, true, false)); + let sensors = vec![7.5]; + let mut output = vec![0.0]; + organism.activate(sensors, &mut output); + assert!( + output[0] > 0.9, + format!("{:?} is not bigger than 0.9", output[0]) + ); + + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, -2.0, true, false)); + let sensors = vec![1.0]; + let mut output = vec![0.0]; + organism.activate(sensors, &mut output); + assert!( + output[0] < 0.1, + format!("{:?} is not smaller than 0.1", output[0]) + ); + } + + #[test] + fn should_propagate_signal_over_hidden_layers() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 0.0, true, false)); + organism.add_gene(Gene::new(0, 2, 5.0, true, false)); + organism.add_gene(Gene::new(2, 1, 5.0, true, false)); + let sensors = vec![0.0]; + let mut output = vec![0.0]; + organism.activate(sensors, &mut output); + assert!( + output[0] > 0.9, + format!("{:?} is not bigger than 0.9", output[0]) + ); + } + + #[test] + fn should_work_with_cyclic_networks() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 2.0, true, false)); + organism.add_gene(Gene::new(1, 2, 2.0, true, false)); + organism.add_gene(Gene::new(2, 1, 2.0, true, false)); + let mut output = vec![0.0]; + organism.activate(vec![1.0], &mut output); + assert!( + output[0] > 0.9, + format!("{:?} is not bigger than 0.9", output[0]) + ); // <- TODO this fails... -7.14... not bigger than 0.9 + + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, -2.0, true, false)); + organism.add_gene(Gene::new(1, 2, -2.0, true, false)); + organism.add_gene(Gene::new(2, 1, -2.0, true, false)); + let mut output = vec![0.0]; + organism.activate(vec![1.0], &mut output); + assert!( + output[0] < 0.1, + format!("{:?} is not smaller than 0.1", output[0]) + ); + } + + #[test] + fn activate_organims_sensor_without_enough_neurons_should_ignore_it() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 1.0, true, false)); + let sensors = vec![0.0, 0.0, 0.0]; + let mut output = vec![0.0]; + organism.activate(sensors, &mut output); + } + + #[test] + fn should_allow_multiple_output() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 1.0, true, false)); + let sensors = vec![0.0]; + let mut output = vec![0.0, 0.0]; + organism.activate(sensors, &mut output); + } + + #[test] + fn should_be_able_to_get_matrix_representation_of_the_neuron_connections() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 1.0, true, false)); + organism.add_gene(Gene::new(1, 2, 0.5, true, false)); + organism.add_gene(Gene::new(2, 1, 0.5, true, false)); + organism.add_gene(Gene::new(2, 2, 0.75, true, false)); + organism.add_gene(Gene::new(1, 0, 1.0, true, false)); + assert_eq!( + organism.get_weights(), + vec![0.0, 1.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.5, 0.75] + ); + } + + #[test] + fn should_not_raise_exception_if_less_neurons_than_required() { + let mut organism = NeuralNetwork::default(); + organism.add_gene(Gene::new(0, 1, 1.0, true, false)); + let sensors = vec![0.0, 0.0, 0.0]; + let mut output = vec![0.0, 0.0, 0.0]; + organism.activate(sensors, &mut output); + } +} diff --git a/src/organism.rs b/src/organism.rs deleted file mode 100644 index d54f8de..0000000 --- a/src/organism.rs +++ /dev/null @@ -1,187 +0,0 @@ -use ctrnn::{Ctrnn, CtrnnNeuralNetwork}; -use genome::Genome; -use std::cmp; - -/// An organism is a Genome with fitness. -/// Also maitain a fitenss measure of the organism -#[allow(missing_docs)] -#[derive(Debug, Clone)] -pub struct Organism { - pub genome: Genome, - pub fitness: f64, -} - -impl Organism { - /// Create a new organmism form a single genome. - pub fn new(genome: Genome) -> Organism { - Organism { - genome: genome, - fitness: 0f64, - } - } - /// Return a new Orgnaism by mutating this Genome and fitness of zero - pub fn mutate(&self) -> Organism { - let mut new_genome = self.genome.clone(); - new_genome.mutate(); - Organism::new(new_genome) - } - /// Mate this organism with another - pub fn mate(&self, other: &Organism) -> Organism { - Organism::new( - self.genome - .mate(&other.genome, self.fitness < other.fitness), - ) - } - /// Activate this organism in the NN - pub fn activate(&mut self, sensors: &[f64], outputs: &mut Vec) { - let neurons_len = self.genome.len(); - let gamma = vec![0.0; neurons_len]; - let tau = vec![10.0; neurons_len]; - let theta = vec![0.0; neurons_len]; - let wik = vec![1.0; sensors.len() * neurons_len]; - let i = sensors; - let wij = self.get_weights_matrix(); - - let activations = Ctrnn::default().activate_nn( - 30, - &CtrnnNeuralNetwork { - gamma: gamma.as_slice(), - delta_t: 10.0, - tau: tau.as_slice(), - wij: &(wij.0, wij.1, wij.2.as_slice()), - theta: theta.as_slice(), - wik: &(neurons_len, sensors.len(), wik.as_slice()), - i: i, - }, - ); - - if sensors.len() < neurons_len { - let outputs_activations = activations.split_at(sensors.len()).1.to_vec(); - - for n in 0..cmp::min(outputs_activations.len(), outputs.len()) { - outputs[n] = outputs_activations[n]; - } - } - } - - fn get_weights_matrix(&self) -> (usize, usize, Vec) { - let neurons_len = self.genome.len(); - let mut matrix = vec![0.0; neurons_len * neurons_len]; - for gene in self.genome.get_genes() { - if gene.enabled() { - matrix[(gene.out_neuron_id() * neurons_len) + gene.in_neuron_id()] = gene.weight() - } - } - (neurons_len, neurons_len, matrix) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use genome::Genome; - - #[test] - fn should_propagate_signal_without_hidden_layers() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 5f64); - let sensors = vec![1f64]; - let mut output = vec![0f64]; - organism.activate(&sensors, &mut output); - assert!( - output[0] > 0.9f64, - format!("{:?} is not bigger than 0.9", output[0]) - ); - - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, -2f64); - let sensors = vec![1f64]; - let mut output = vec![0f64]; - organism.activate(&sensors, &mut output); - assert!( - output[0] < 0.1f64, - format!("{:?} is not smaller than 0.1", output[0]) - ); - } - - #[test] - fn should_propagate_signal_over_hidden_layers() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 0f64); - organism.genome.inject_gene(0, 2, 5f64); - organism.genome.inject_gene(2, 1, 5f64); - let sensors = vec![0f64]; - let mut output = vec![0f64]; - organism.activate(&sensors, &mut output); - assert!( - output[0] > 0.9f64, - format!("{:?} is not bigger than 0.9", output[0]) - ); - } - - #[test] - fn should_work_with_cyclic_networks() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 2f64); - organism.genome.inject_gene(1, 2, 2f64); - organism.genome.inject_gene(2, 1, 2f64); - let mut output = vec![0f64]; - organism.activate(&[1f64], &mut output); - assert!( - output[0] > 0.9, - format!("{:?} is not bigger than 0.9", output[0]) - ); - - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, -2f64); - organism.genome.inject_gene(1, 2, -2f64); - organism.genome.inject_gene(2, 1, -2f64); - let mut output = vec![0f64]; - organism.activate(&[1f64], &mut output); - assert!( - output[0] < 0.1, - format!("{:?} is not smaller than 0.1", output[0]) - ); - } - - #[test] - fn activate_organims_sensor_without_enough_neurons_should_ignore_it() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 1f64); - let sensors = vec![0f64, 0f64, 0f64]; - let mut output = vec![0f64]; - organism.activate(&sensors, &mut output); - } - - #[test] - fn should_allow_multiple_output() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 1f64); - let sensors = vec![0f64]; - let mut output = vec![0f64, 0f64]; - organism.activate(&sensors, &mut output); - } - - #[test] - fn should_be_able_to_get_matrix_representation_of_the_neuron_connections() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 1f64); - organism.genome.inject_gene(1, 2, 0.5f64); - organism.genome.inject_gene(2, 1, 0.5f64); - organism.genome.inject_gene(2, 2, 0.75f64); - organism.genome.inject_gene(1, 0, 1f64); - assert_eq!( - organism.get_weights_matrix(), - (3, 3, vec![0.0, 1.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.5, 0.75]) - ); - } - - #[test] - fn should_not_raise_exception_if_less_neurons_than_required() { - let mut organism = Organism::new(Genome::default()); - organism.genome.inject_gene(0, 1, 1f64); - let sensors = vec![0f64, 0f64, 0f64]; - let mut output = vec![0f64, 0f64, 0f64]; - organism.activate(&sensors, &mut output); - } -} diff --git a/src/population.rs b/src/population.rs index c18bd2b..eeffae1 100644 --- a/src/population.rs +++ b/src/population.rs @@ -1,7 +1,5 @@ +use crate::{Environment, Genome, Organism, Specie, SpeciesEvaluator}; use conv::prelude::*; -use environment::Environment; -use genome::Genome; -use organism::Organism; #[cfg(feature = "telemetry")] use rusty_dashed; @@ -9,48 +7,59 @@ use rusty_dashed; #[cfg(feature = "telemetry")] use serde_json; -use specie::Specie; -use species_evaluator::SpeciesEvaluator; - -/// All species in the network +/// Contains several species, and a way to evolve these to the next generation. #[derive(Debug)] -pub struct Population { +pub struct Population { /// container of species - pub species: Vec, + pub species: Vec>, champion_fitness: f64, epochs_without_improvements: usize, } const MAX_EPOCHS_WITHOUT_IMPROVEMENTS: usize = 5; -impl Population { - /// Create a new population of size X. - pub fn create_population(population_size: usize) -> Population { - let mut population = Population { - species: vec![], +impl Population { + /// Create a new population with `population_size` organisms. Each organism + /// will have only a single unconnected neuron. + pub fn create_population(population_size: usize) -> Population { + Self::create_population_from(G::default(), population_size) + } + /// Create a new population with `population_size` organisms, + /// where each organism has the same genome given in `genome`. + pub fn create_population_from(genome: G, population_size: usize) -> Population { + let mut organisms = Vec::new(); + while organisms.len() < population_size { + organisms.push(Organism::new(genome.clone())); + } + + let mut specie = Specie::new(organisms.first().unwrap().clone()); + specie.organisms = organisms; + + Population { + species: vec![specie], champion_fitness: 0f64, epochs_without_improvements: 0usize, - }; - - population.create_organisms(population_size); - population + } } - /// Find total of all orgnaisms in the population + + /// Counts the number of organisms in the population pub fn size(&self) -> usize { self.species .iter() - .fold(0usize, |total, specie| total + specie.organisms.len()) + .fold(0, |total, specie| total + specie.organisms.len()) } /// Create offspring by mutation and mating. May create new species. pub fn evolve(&mut self) { self.generate_offspring(); } /// TODO - pub fn evaluate_in(&mut self, environment: &mut Environment) { + pub fn evaluate_in(&mut self, environment: &mut Environment) { let champion = SpeciesEvaluator::new(environment).evaluate(&mut self.species); if self.champion_fitness >= champion.fitness { self.epochs_without_improvements += 1; + #[cfg(feature = "telemetry")] + telemetry!("fitness1", 1.0, format!("{}", self.champion_fitness)); } else { self.champion_fitness = champion.fitness; #[cfg(feature = "telemetry")] @@ -64,12 +73,12 @@ impl Population { self.epochs_without_improvements = 0usize; } } - /// Return all organisms of the population - pub fn get_organisms(&self) -> Vec { + /// Collect all organisms of the population + pub fn get_organisms(&self) -> Vec> { self.species .iter() .flat_map(|specie| specie.organisms.clone()) - .collect::>() + .collect::>() } /// How many iterations without improvement pub fn epochs_without_improvements(&self) -> usize { @@ -95,32 +104,30 @@ impl Population { &organisms, ); } - } else { - let organisms_by_average_fitness = - num_of_organisms.value_as::().unwrap() / total_average_fitness; + self.epochs_without_improvements = 0; + return; + } - for specie in &mut self.species { - let specie_fitness = specie.calculate_average_fitness(); - let offspring_size = if total_average_fitness == 0f64 { - specie.organisms.len() - } else { - (specie_fitness * organisms_by_average_fitness).round() as usize - }; + let organisms_by_average_fitness = + num_of_organisms.value_as::().unwrap() / total_average_fitness; - if offspring_size > 0 { - // TODO: check if offspring is for organisms fitness also, not only by specie - specie.generate_offspring(offspring_size, &organisms); - } else { - specie.remove_organisms(); - } + for specie in &mut self.species { + let specie_fitness = specie.calculate_average_fitness(); + let offspring_size = if total_average_fitness <= 0f64 { + specie.organisms.len() + } else { + (specie_fitness * organisms_by_average_fitness).round() as usize + }; + if offspring_size > 0 { + // TODO: check if offspring is for organisms fitness also, not only by specie + specie.generate_offspring(offspring_size, &organisms); + } else { + specie.remove_organisms(); } } - if self.epochs_without_improvements > MAX_EPOCHS_WITHOUT_IMPROVEMENTS { - self.epochs_without_improvements = 0; - } } - fn get_best_species(&self) -> Vec { + fn get_best_species(&self) -> Vec> { let mut result = vec![]; if self.species.len() < 2 { @@ -154,16 +161,17 @@ impl Population { } for organism in organisms { - let mut new_specie: Option = None; - match self.species + let mut new_specie: Option> = None; + match self + .species .iter_mut() - .find(|specie| specie.match_genome(organism)) + .find(|specie| specie.match_genome(&organism.genome)) { Some(specie) => { specie.add(organism.clone()); } None => { - let mut specie = Specie::new(organism.genome.clone()); + let mut specie = Specie::new(organism.clone()); specie.add(organism.clone()); new_specie = Some(specie); } @@ -173,42 +181,26 @@ impl Population { } } } - - fn create_organisms(&mut self, population_size: usize) { - self.species = vec![]; - let mut organisms = vec![]; - - while organisms.len() < population_size { - organisms.push(Organism::new(Genome::default())); - } - - let mut specie = Specie::new(organisms.first().unwrap().genome.clone()); - specie.organisms = organisms; - self.species.push(specie); - } } #[cfg(test)] mod tests { - use super::*; - use genome::Genome; - use organism::Organism; - use specie::Specie; + use crate::{nn::Gene, nn::NeuralNetwork, Organism, Population, Specie}; #[test] fn population_should_be_able_to_speciate_genomes() { - let mut genome1 = Genome::default(); - genome1.inject_gene(0, 0, 1f64); - genome1.inject_gene(0, 1, 1f64); - let mut genome2 = Genome::default(); - genome1.inject_gene(0, 0, 1f64); - genome1.inject_gene(0, 1, 1f64); - genome2.inject_gene(1, 1, 1f64); - genome2.inject_gene(1, 0, 1f64); + let mut genome1 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 1f64, true, false)); + genome1.add_gene(Gene::new(0, 1, 1f64, true, false)); + let mut genome2 = NeuralNetwork::default(); + genome1.add_gene(Gene::new(0, 0, 1f64, true, false)); + genome1.add_gene(Gene::new(0, 1, 1f64, true, false)); + genome2.add_gene(Gene::new(1, 1, 1f64, true, false)); + genome2.add_gene(Gene::new(1, 0, 1f64, true, false)); let mut population = Population::create_population(2); let organisms = vec![Organism::new(genome1), Organism::new(genome2)]; - let mut specie = Specie::new(organisms.first().unwrap().genome.clone()); + let mut specie = Specie::new(organisms.first().unwrap().clone()); specie.organisms = organisms; population.species = vec![specie]; population.speciate(); @@ -217,7 +209,7 @@ mod tests { #[test] fn after_population_evolve_population_should_be_the_same() { - let mut population = Population::create_population(150); + let mut population = Population::::create_population(150); for _ in 0..150 { population.evolve(); } diff --git a/src/specie.rs b/src/specie.rs index 44df280..bf166df 100644 --- a/src/specie.rs +++ b/src/specie.rs @@ -1,27 +1,28 @@ +use crate::{Genome, Organism}; use conv::prelude::*; -use genome::Genome; -use organism::Organism; -use rand; -use rand::distributions::{IndependentSample, Range}; +use rand::{ + self, + distributions::{Distribution, Uniform}, +}; /// A species (several organisms) and associated fitnesses #[derive(Debug, Clone)] -pub struct Specie { - representative: Genome, +pub struct Specie { + representative: Organism, average_fitness: f64, champion_fitness: f64, age: usize, age_last_improvement: usize, /// All orgnamisms in this species - pub organisms: Vec, + pub organisms: Vec>, } -const MUTATION_PROBABILITY: f64 = 0.25f64; -const INTERSPECIE_MATE_PROBABILITY: f64 = 0.001f64; +const MUTATION_PROBABILITY: f64 = 0.25; +const INTERSPECIE_MATE_PROBABILITY: f64 = 0.001; -impl Specie { - /// Create a new species from a Genome - pub fn new(genome: Genome) -> Specie { +impl Specie { + /// Create a new species from a representative Genome + pub fn new(genome: Organism) -> Specie { Specie { organisms: vec![], representative: genome, @@ -31,13 +32,13 @@ impl Specie { age_last_improvement: 0, } } - /// Add an Organism - pub fn add(&mut self, organism: Organism) { + /// Add an organism to the species + pub fn add(&mut self, organism: Organism) { self.organisms.push(organism); } /// Check if another organism is of the same species as this one. - pub fn match_genome(&self, organism: &Organism) -> bool { - self.representative.is_same_specie(&organism.genome) + pub fn match_genome(&self, organism: &G) -> bool { + self.representative.genome.is_same_specie(&organism) } /// Get the most performant organism pub fn calculate_champion_fitness(&self) -> f64 { @@ -52,13 +53,14 @@ impl Specie { /// Work out average fitness of this species pub fn calculate_average_fitness(&mut self) -> f64 { let organisms_count = self.organisms.len().value_as::().unwrap(); - if organisms_count == 0f64 { - return 0f64; + if organisms_count == 0.0 { + return 0.0; } - let total_fitness = self.organisms + let total_fitness = self + .organisms .iter() - .fold(0f64, |total, organism| total + organism.fitness); + .fold(0.0, |total, organism| total + organism.fitness); let new_fitness = total_fitness / organisms_count; @@ -75,29 +77,31 @@ impl Specie { pub fn generate_offspring( &mut self, num_of_organisms: usize, - population_organisms: &[Organism], + population_organisms: &[Organism], ) { + let mut rng = rand::thread_rng(); self.age += 1; let copy_champion = if num_of_organisms > 5 { 1 } else { 0 }; - let mut rng = rand::thread_rng(); - let mut offspring: Vec = { + // Select `num_of_organisms` organisms in this specie, and make offspring from + // them. + let mut offspring: Vec> = { let mut selected_organisms = vec![]; - let range = Range::new(0, self.organisms.len()); + let uniform = Uniform::from(0..self.organisms.len()); for _ in 0..num_of_organisms - copy_champion { - selected_organisms.push(range.ind_sample(&mut rng)); + selected_organisms.push(uniform.sample(&mut rng)); } selected_organisms .iter() .map(|organism_pos| { self.create_child(&self.organisms[*organism_pos], population_organisms) }) - .collect::>() + .collect::>() }; if copy_champion == 1 { - let champion: Option = + let champion: Option> = self.organisms.iter().fold(None, |champion, organism| { if champion.is_none() || champion.as_ref().unwrap().fitness < organism.fitness { Some(organism.clone()) @@ -111,8 +115,8 @@ impl Specie { self.organisms = offspring; } - /// Get a genome representitive of this species. - pub fn get_representative_genome(&self) -> Genome { + /// Get the representative organism of this species. + pub fn get_representative(&self) -> Organism { self.representative.clone() } /// Clear existing organisms in this species. @@ -127,7 +131,11 @@ impl Specie { } /// Create a new child by mutating and existing one or mating two genomes. - fn create_child(&self, organism: &Organism, population_organisms: &[Organism]) -> Organism { + fn create_child( + &self, + organism: &Organism, + population_organisms: &[Organism], + ) -> Organism { if rand::random::() < MUTATION_PROBABILITY || population_organisms.len() < 2 { self.create_child_by_mutation(organism) } else { @@ -135,23 +143,21 @@ impl Specie { } } - fn create_child_by_mutation(&self, organism: &Organism) -> Organism { + fn create_child_by_mutation(&self, organism: &Organism) -> Organism { organism.mutate() } fn create_child_by_mate( &self, - organism: &Organism, - population_organisms: &[Organism], - ) -> Organism { + organism: &Organism, + population_organisms: &[Organism], + ) -> Organism { let mut rng = rand::thread_rng(); if rand::random::() > INTERSPECIE_MATE_PROBABILITY { - let selected_mate = - rand::seq::sample_iter(&mut rng, 0..self.organisms.len(), 1).unwrap()[0]; + let selected_mate = Uniform::from(0..self.organisms.len()).sample(&mut rng); organism.mate(&self.organisms[selected_mate]) } else { - let selected_mate = - rand::seq::sample_iter(&mut rng, 0..population_organisms.len(), 1).unwrap()[0]; + let selected_mate = Uniform::from(0..population_organisms.len()).sample(&mut rng); organism.mate(&population_organisms[selected_mate]) } } @@ -159,26 +165,24 @@ impl Specie { #[cfg(test)] mod tests { - use super::*; - use genome::Genome; - use organism::Organism; + use crate::{nn::NeuralNetwork, Organism, Specie}; use std::f64::EPSILON; #[test] fn specie_should_return_correct_average_fitness() { - let mut specie = Specie::new(Genome::default()); - let mut organism1 = Organism::new(Genome::default()); + let mut specie = Specie::new(Organism::new(NeuralNetwork::default())); + let mut organism1 = Organism::new(NeuralNetwork::default()); organism1.fitness = 10f64; - let mut organism2 = Organism::new(Genome::default()); + let mut organism2 = Organism::new(NeuralNetwork::default()); organism2.fitness = 15f64; - let mut organism3 = Organism::new(Genome::default()); + let mut organism3 = Organism::new(NeuralNetwork::default()); organism3.fitness = 20f64; - specie.add(organism1); - specie.add(organism2); - specie.add(organism3); + specie.organisms.push(organism1); + specie.organisms.push(organism2); + specie.organisms.push(organism3); assert!((specie.calculate_average_fitness() - 15f64).abs() < EPSILON); } diff --git a/src/species_evaluator.rs b/src/species_evaluator.rs index 1f0d752..9828499 100644 --- a/src/species_evaluator.rs +++ b/src/species_evaluator.rs @@ -1,30 +1,28 @@ +use crate::{Environment, Genome, Organism, Specie}; use crossbeam::{self, Scope}; -use environment::Environment; -use genome::Genome; use num_cpus; -use organism::Organism; -use specie::Specie; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; /// Calculate fitness and champions for a species -pub struct SpeciesEvaluator<'a> { +pub struct SpeciesEvaluator<'a, G> { threads: usize, - environment: &'a mut Environment, + environment: &'a mut Environment, } -impl<'a> SpeciesEvaluator<'a> { +impl<'a, G: Genome + Send> SpeciesEvaluator<'a, G> { /// Take an environment that will test organisms. - pub fn new(environment: &mut Environment) -> SpeciesEvaluator { + pub fn new(environment: &mut Environment) -> SpeciesEvaluator { SpeciesEvaluator { threads: num_cpus::get(), environment: environment, } } - /// return champion fitness - pub fn evaluate(&self, species: &mut Vec) -> Organism { - let mut champion: Organism = Organism::new(Genome::default()); + /// Returns (champion organism, fitness) + pub fn evaluate(&self, species: &mut Vec>) -> Organism { + // The second element in the touple is the fitness of this organism + let mut champion = Organism::::default(); for specie in species { if specie.organisms.is_empty() { @@ -32,7 +30,7 @@ impl<'a> SpeciesEvaluator<'a> { } let organisms_by_thread = (specie.organisms.len() + self.threads - 1) / self.threads; // round up - let (tx, rx): (Sender, Receiver) = mpsc::channel(); + let (tx, rx): (Sender>, Receiver>) = mpsc::channel(); crossbeam::scope(|scope| { let threads_used = self.dispatch_organisms( specie.organisms.as_mut_slice(), @@ -54,10 +52,10 @@ impl<'a> SpeciesEvaluator<'a> { fn dispatch_organisms<'b>( &'b self, - organisms: &'b mut [Organism], + organisms: &'b mut [Organism], organisms_by_thread: usize, threads_used: usize, - tx: &Sender, + tx: &Sender>, scope: &Scope<'b>, ) -> usize { if organisms.len() <= organisms_by_thread { @@ -83,14 +81,18 @@ impl<'a> SpeciesEvaluator<'a> { fn evaluate_organisms<'b>( &'b self, - organisms: &'b mut [Organism], - tx: Sender, + organisms: &'b mut [Organism], + tx: Sender>, scope: &Scope<'b>, ) { scope.spawn(move || { - let mut champion = Organism::new(Genome::default()); + let mut champion = Organism::default(); for organism in &mut organisms.iter_mut() { - organism.fitness = self.environment.test(organism); + organism.fitness = self.environment.test(&mut organism.genome); + if organism.fitness < 0f64 { + eprintln!("Fitness can't be negative: {:?}", organism.fitness); + ::std::process::exit(-1); + } if organism.fitness > champion.fitness { champion = organism.clone(); } diff --git a/tests/rustneat_tests.rs b/tests/rustneat_tests.rs index 3286ca7..c1c52da 100644 --- a/tests/rustneat_tests.rs +++ b/tests/rustneat_tests.rs @@ -1,44 +1,42 @@ -extern crate rustneat; - #[cfg(test)] mod test { - use rustneat::{Environment, Organism, Population}; + use rustneat::{Environment, NeuralNetwork, Organism, Population}; struct MyEnvironment; - impl Environment for MyEnvironment { - fn test(&self, _: &mut Organism) -> f64 { + impl Environment for MyEnvironment { + fn test(&self, _: &mut NeuralNetwork) -> f64 { 0.1234f64 } } struct XORClassification; - impl Environment for XORClassification { - fn test(&self, organism: &mut Organism) -> f64 { + impl Environment for XORClassification { + fn test(&self, organism: &mut NeuralNetwork) -> f64 { let mut output = vec![0f64]; let mut distance: f64; - organism.activate(&vec![0f64, 0f64], &mut output); + organism.activate(vec![0f64, 0f64], &mut output); distance = (0f64 - output[0]).abs(); - organism.activate(&vec![0f64, 1f64], &mut output); + organism.activate(vec![0f64, 1f64], &mut output); distance += (1f64 - output[0]).abs(); - organism.activate(&vec![1f64, 0f64], &mut output); + organism.activate(vec![1f64, 0f64], &mut output); distance += (1f64 - output[0]).abs(); - organism.activate(&vec![1f64, 1f64], &mut output); + organism.activate(vec![1f64, 1f64], &mut output); distance += (0f64 - output[0]).abs(); - (4f64 - distance).powi(2) + 16.0 / (1.0 + distance) } } #[test] fn should_be_able_to_generate_a_population() { - let population = Population::create_population(150); + let population = Population::::create_population(150); assert!(population.size() == 150); } #[test] fn population_can_evolve() { - let mut population = Population::create_population(1); + let mut population = Population::::create_population(1); population.evolve(); let genome = &population.get_organisms()[0].genome; assert_eq!(genome.total_genes(), 1); @@ -47,35 +45,9 @@ mod test { #[test] fn population_can_be_tested_on_environment() { - let mut population = Population::create_population(10); + let mut population = Population::::create_population(10); let mut environment = MyEnvironment; population.evaluate_in(&mut environment); assert!(population.get_organisms()[0].fitness == 0.1234f64); } - - #[test] - fn network_should_be_able_to_solve_xor_classification() { - let mut population = Population::create_population(150); - let mut environment = XORClassification; - let mut champion_option: Option = None; - while champion_option.is_none() { - population.evolve(); - population.evaluate_in(&mut environment); - for organism in &population.get_organisms() { - if organism.fitness > 15.9f64 { - champion_option = Some(organism.clone()); - } - } - } - let champion = champion_option.as_mut().unwrap(); - let mut output = vec![0f64]; - champion.activate(&vec![0f64, 0f64], &mut output); - assert!(output[0] < 0.1f64); - champion.activate(&vec![0f64, 1f64], &mut output); - assert!(output[0] > 0.9f64); - champion.activate(&vec![1f64, 0f64], &mut output); - assert!(output[0] > 0.9f64); - champion.activate(&vec![1f64, 1f64], &mut output); - assert!(output[0] < 0.1f64); - } }