diff --git a/examples/function_approximation.rs b/examples/function_approximation.rs index 0354751..0c7117a 100644 --- a/examples/function_approximation.rs +++ b/examples/function_approximation.rs @@ -8,34 +8,34 @@ extern crate rusty_dashed; #[cfg(feature = "telemetry")] mod telemetry_helper; -use rustneat::{Environment, Organism, Population, NeuralNetwork}; +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; + fn test(&self, organism: &mut NeuralNetwork) -> f64 { + let mut output = vec![0f64]; + let mut distance = 0f64; - let mut outputs = Vec::new(); + 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]); - } + 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 { + let fitness = 100f64 / (1f64 + (distance / 1000.0)); + unsafe { if fitness > BEST_FITNESS { BEST_FITNESS = fitness; - #[cfg(feature = "telemetry")] - telemetry!("approximation1", 1.0, format!("{:?}", outputs)); + #[cfg(feature = "telemetry")] + telemetry!("approximation1", 1.0, format!("{:?}", outputs)); } - } - fitness - } + } + fitness + } } fn main() { @@ -50,7 +50,11 @@ fn main() { 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::>())); + 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)); diff --git a/examples/simple_sample.rs b/examples/simple_sample.rs index 60214f0..be3c14d 100644 --- a/examples/simple_sample.rs +++ b/examples/simple_sample.rs @@ -1,7 +1,7 @@ extern crate rand; extern crate rustneat; -use rustneat::{Environment, Organism, Population, NeuralNetwork}; +use rustneat::{Environment, NeuralNetwork, Organism, Population}; #[cfg(feature = "telemetry")] mod telemetry_helper; diff --git a/examples/telemetry_helper.rs b/examples/telemetry_helper.rs index 4d23f09..9e1ab46 100644 --- a/examples/telemetry_helper.rs +++ b/examples/telemetry_helper.rs @@ -8,7 +8,7 @@ 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, open: bool) { diff --git a/src/genome.rs b/src/genome.rs index 1cd8680..4af60ad 100644 --- a/src/genome.rs +++ b/src/genome.rs @@ -1,10 +1,11 @@ -const COMPATIBILITY_THRESHOLD: f64 = 3.0; //used to speciate organisms +const COMPATIBILITY_THRESHOLD: f64 = 3.0; // used to speciate organisms -/// Implementing `Genome` conceptually means that the implementor "has a genome", and the -/// implementor can be called an "organism". +/// 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 + /// Returns a new organism which is a clone of `&self` apart from possible + /// mutations fn mutate(&self) -> Self; /// `fittest` is true if `other` is more fit. @@ -13,7 +14,6 @@ pub trait Genome: Clone + Default + Send { /// 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 fn is_same_specie(&self, other: &Self) -> bool { @@ -21,8 +21,8 @@ pub trait Genome: Clone + Default + Send { } } -/// Used in algorithm just to group an organism (genome) with its fitness, and also in the -/// interface to get the fitness of organisms +/// 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 @@ -48,9 +48,10 @@ impl Organism { pub fn mate(&self, other: &Self) -> Organism { Organism::new( self.genome - .mate(&other.genome, self.fitness < other.fitness)) + .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 b887bc9..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] @@ -23,20 +30,20 @@ extern crate serde_derive; #[cfg(feature = "telemetry")] extern crate serde_json; -pub use self::genome::*; pub use self::environment::Environment; +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 self::nn::{NeuralNetwork, Gene}; -/// 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; /// Trait to define test parameter mod environment; /// A collection of genes 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 mod population; mod specie; diff --git a/src/nn/ctrnn.rs b/src/nn/ctrnn.rs index 56982c7..a5c18e2 100644 --- a/src/nn/ctrnn.rs +++ b/src/nn/ctrnn.rs @@ -5,13 +5,12 @@ use rulinalg::matrix::{BaseMatrix, BaseMatrixMut, Matrix}; 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 + 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 { @@ -23,10 +22,8 @@ impl<'a> Ctrnn<'a> { 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 = &y + delta_t_tau.elemul(&((&wij * activations) - &y + &i)); + } y.into_vec() } @@ -64,6 +61,7 @@ mod tests { #[test] fn neural_network_activation_stability() { // TODO - // This test should just ensure that a stable neural network implementation doesn't change + // This test should just ensure that a stable neural network + // implementation doesn't change } } diff --git a/src/nn/gene.rs b/src/nn/gene.rs index a7b427a..5b18b1a 100644 --- a/src/nn/gene.rs +++ b/src/nn/gene.rs @@ -12,7 +12,7 @@ pub struct Gene { /// 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 + pub is_bias: bool, } impl Eq for Gene {} @@ -49,13 +49,19 @@ 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, is_bias: 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 + is_bias: is_bias, } } /// Generate a weight @@ -79,7 +85,7 @@ impl Default for Gene { out_neuron_id: 1, weight: Gene::generate_weight(), enabled: true, - is_bias: false + is_bias: false, } } } diff --git a/src/nn/mod.rs b/src/nn/mod.rs index c68b4c5..23ac09f 100644 --- a/src/nn/mod.rs +++ b/src/nn/mod.rs @@ -1,6 +1,10 @@ -use rand::{self, seq::IteratorRandom, distributions::{Distribution, Uniform}}; -use std::cmp; use crate::Genome; +use rand::{ + self, + distributions::{Distribution, Uniform}, + seq::IteratorRandom, +}; +use std::cmp; mod ctrnn; mod gene; @@ -41,7 +45,8 @@ impl Genome for NeuralNetwork { let z = if n < 20 { 1.0 } else { n as f64 }; - let matching_genes = self.genes + let matching_genes = self + .genes .iter() .filter(|i1_gene| other.genes.contains(i1_gene)) .collect::>(); @@ -52,8 +57,7 @@ impl Genome for NeuralNetwork { // 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) + acc + (m_gene.weight - &other.genes[other.genes.binary_search(m_gene).unwrap()].weight) .abs() }); @@ -96,12 +100,11 @@ impl Genome for NeuralNetwork { other.mate_genes(self, !fittest) } } - } impl NeuralNetwork { - /// Creates a network that with no connections, but enough neurons to cover all inputs and - /// outputs. + /// 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(), @@ -109,14 +112,14 @@ impl NeuralNetwork { } } - /// Activate the neural network by sending input `sensors` into its first `sensors.len()` - /// neurons + /// 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 theta = self.get_bias(); let mut i = sensors.clone(); @@ -128,15 +131,15 @@ impl NeuralNetwork { 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); + 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(); @@ -165,7 +168,7 @@ impl NeuralNetwork { 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[gene.in_neuron_id()] += 1.0; } } matrix @@ -209,7 +212,8 @@ impl NeuralNetwork { fn mutate_connection_weight(&mut self) { for gene in &mut self.genes { - let perturbation = rand::random::() < MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY; + let perturbation = + rand::random::() < MUTATE_CONNECTION_WEIGHT_PERTURBED_PROBABILITY; let mut new_weight = Gene::generate_weight(); if perturbation { @@ -240,14 +244,30 @@ impl NeuralNetwork { // 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)); + 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)); + 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 { @@ -255,7 +275,7 @@ impl NeuralNetwork { 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 + // Only mate half of the genes randomly if !fittest || rand::random::() > 0.5 { *gene } else { @@ -269,27 +289,28 @@ impl NeuralNetwork { genome } - - /// Add a new gene and checks if is allowed. Can only connect to the next neuron or already connected - /// neurons. + /// 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() + max_neuron_id, + gene.in_neuron_id(), + gene.out_neuron_id() ); } - //assert!( + // 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!( + // assert!( // gene.out_neuron_id() <= max_neuron_id, // format!( // "out_neuron_id {} is greater than max allowed id {}", @@ -310,7 +331,6 @@ impl NeuralNetwork { self.genes.sort(); } - /// Total weigths of all genes pub fn total_weights(&self) -> f64 { let mut total = 0.0; @@ -325,13 +345,12 @@ impl NeuralNetwork { 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; - use crate::{nn::NeuralNetwork, nn::Gene, Genome, Organism}; #[test] fn mutation_connection_weight() { @@ -436,7 +455,6 @@ mod tests { assert!(!genome1.is_same_specie(&genome2)); } - // From former genome.rs: #[test] @@ -543,4 +561,3 @@ mod tests { organism.activate(sensors, &mut output); } } - diff --git a/src/population.rs b/src/population.rs index 5fca589..eeffae1 100644 --- a/src/population.rs +++ b/src/population.rs @@ -1,5 +1,5 @@ +use crate::{Environment, Genome, Organism, Specie, SpeciesEvaluator}; use conv::prelude::*; -use crate::{Genome, Organism, Environment, Specie, SpeciesEvaluator}; #[cfg(feature = "telemetry")] use rusty_dashed; @@ -7,7 +7,6 @@ use rusty_dashed; #[cfg(feature = "telemetry")] use serde_json; - /// Contains several species, and a way to evolve these to the next generation. #[derive(Debug)] pub struct Population { @@ -20,8 +19,8 @@ pub struct Population { const MAX_EPOCHS_WITHOUT_IMPROVEMENTS: usize = 5; impl Population { - /// Create a new population with `population_size` organisms. Each organism will have only a single unconnected - /// neuron. + /// 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) } @@ -163,7 +162,8 @@ impl Population { for organism in organisms { let mut new_specie: Option> = None; - match self.species + match self + .species .iter_mut() .find(|specie| specie.match_genome(&organism.genome)) { @@ -185,7 +185,7 @@ impl Population { #[cfg(test)] mod tests { - use crate::{nn::Gene, Organism, Specie, nn::NeuralNetwork, Population}; + use crate::{nn::Gene, nn::NeuralNetwork, Organism, Population, Specie}; #[test] fn population_should_be_able_to_speciate_genomes() { diff --git a/src/specie.rs b/src/specie.rs index 1a7093c..bf166df 100644 --- a/src/specie.rs +++ b/src/specie.rs @@ -1,6 +1,9 @@ -use conv::prelude::*; -use rand::{self, distributions::{Distribution, Uniform}}; use crate::{Genome, Organism}; +use conv::prelude::*; +use rand::{ + self, + distributions::{Distribution, Uniform}, +}; /// A species (several organisms) and associated fitnesses #[derive(Debug, Clone)] @@ -54,7 +57,8 @@ impl Specie { return 0.0; } - let total_fitness = self.organisms + let total_fitness = self + .organisms .iter() .fold(0.0, |total, organism| total + organism.fitness); @@ -80,14 +84,16 @@ impl Specie { let copy_champion = if num_of_organisms > 5 { 1 } else { 0 }; - // Select `num_of_organisms` organisms in this specie, and make offspring from them. + // Select `num_of_organisms` organisms in this specie, and make offspring from + // them. let mut offspring: Vec> = { let mut selected_organisms = vec![]; let uniform = Uniform::from(0..self.organisms.len()); for _ in 0..num_of_organisms - copy_champion { selected_organisms.push(uniform.sample(&mut rng)); } - selected_organisms.iter() + selected_organisms + .iter() .map(|organism_pos| { self.create_child(&self.organisms[*organism_pos], population_organisms) }) @@ -125,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 { diff --git a/src/species_evaluator.rs b/src/species_evaluator.rs index de7803f..9828499 100644 --- a/src/species_evaluator.rs +++ b/src/species_evaluator.rs @@ -1,10 +1,9 @@ -use crossbeam::{self, Scope}; use crate::{Environment, Genome, Organism, Specie}; +use crossbeam::{self, Scope}; use num_cpus; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; - /// Calculate fitness and champions for a species pub struct SpeciesEvaluator<'a, G> { threads: usize, diff --git a/tests/rustneat_tests.rs b/tests/rustneat_tests.rs index af5e92a..c1c52da 100644 --- a/tests/rustneat_tests.rs +++ b/tests/rustneat_tests.rs @@ -1,7 +1,6 @@ - #[cfg(test)] mod test { - use rustneat::{Environment, Organism, Population, NeuralNetwork}; + use rustneat::{Environment, NeuralNetwork, Organism, Population}; struct MyEnvironment; @@ -51,31 +50,4 @@ mod test { 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 Organism {genome: champion, fitness: _} = champion_option.as_mut().unwrap(); - let mut output = vec![0f64]; - champion.activate(vec![0f64, 0f64], &mut output); - // println!("Output[0] = {}", output[0]); - 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); - } }