diff --git a/.gitignore b/.gitignore index 51d5dfe..b7bca7a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ Cargo.lock # Vim *.swp *.swo + +# Fmt +**/*.rs.bk diff --git a/.travis.yml b/.travis.yml index 24cbaf3..44d49b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,26 @@ sudo: false language: rust rust: - - stable - - beta + - stable + - beta os: - - linux - - osx + - linux + - osx cache: cargo before_cache: - - cargo prune + - cargo prune env: global: - PATH=$PATH:$HOME/.cargo/bin - RUST_BACKTRACE=1 before_script: -- | - (which cargo-install-update && cargo install-update cargo-update) || cargo install cargo-update && - (which rustfmt && cargo install-update rustfmt) || cargo install rustfmt && - (which cargo-prune && cargo install-update cargo-prune) || cargo install cargo-prune - + - | + (which cargo-install-update && cargo install-update cargo-update) || cargo install cargo-update && + (which rustfmt && cargo install-update rustfmt) || cargo install rustfmt && + (which cargo-prune && cargo install-update cargo-prune) || cargo install cargo-prune + - 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 script: - if [ "${TRAVIS_RUST_VERSION}" = stable ]; then ( diff --git a/Cargo.toml b/Cargo.toml index 3139fd1..ff38ee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,13 @@ lazy_static = "0.2.2" num_cpus = "1.0" rand = "0.3" rulinalg = "0.3.4" +rusty_dashed = "0.1.2" +rustc-serialize = "0.3.21" [dependencies.clippy] optional = true version = "0.0.103" + +[features] +default = [] +telemetry = [] diff --git a/README.md b/README.md index ede0741..ef669b1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ To speed up tests, run them with `--release` (XOR classification/simple_sample s ``` ## Run example -`cargo run --release --example simple_sample` +`cargo run --release --example simple_sample --features=telemetry` + +then go to `http://localhost:3000` to see how neural network evolves ## Sample usage @@ -70,3 +72,9 @@ fn main() { } ``` + +# Develop +Check style guidelines with: + +`cargo install rustfmt` +`cargo fmt -- --write-mode=diff` diff --git a/examples/simple_sample.rs b/examples/simple_sample.rs index c00a395..da17ac3 100644 --- a/examples/simple_sample.rs +++ b/examples/simple_sample.rs @@ -1,7 +1,14 @@ extern crate rustneat; +#[macro_use] +extern crate rusty_dashed; + +extern crate rand; + use rustneat::Environment; use rustneat::Organism; use rustneat::Population; +use rusty_dashed::Dashboard; + struct XORClassification; @@ -17,11 +24,24 @@ impl Environment for XORClassification { distance += (1f64 - output[0]).abs(); organism.activate(&vec![1f64, 1f64], &mut output); distance += (0f64 - output[0]).abs(); - (4f64 - distance).powi(2) + + let fitness = (4f64 - distance).powi(2); + + fitness } } fn main() { + let mut dashboard = Dashboard::new(); + dashboard.add_graph("fitness1", "fitness", 0, 0, 4, 4); + dashboard.add_graph("network1", "network", 4, 0, 4, 4); + + rusty_dashed::Server::serve_dashboard(dashboard); + + + #[cfg(feature = "telemetry")] + println!("\nGo to http://localhost:3000 to see how neural network evolves\n"); + let mut population = Population::create_population(150); let mut environment = XORClassification; let mut champion: Option = None; diff --git a/graphs/fitness.css b/graphs/fitness.css new file mode 100644 index 0000000..d43c9c0 --- /dev/null +++ b/graphs/fitness.css @@ -0,0 +1,9 @@ +svg { + width: 100%; + height: 100%; +} + +svg path { + stroke: black; + fill: none; +} diff --git a/graphs/fitness.js b/graphs/fitness.js new file mode 100644 index 0000000..16b1a75 --- /dev/null +++ b/graphs/fitness.js @@ -0,0 +1,51 @@ +var fitnessAxis = {}; +function fitness_init(id){ + var svg = d3.select('#' + id).append('svg'); + svg.append('path'); + + fitnessAxis[id+"y"] = svg.append("g"); + fitnessAxis[id+"x"] = svg.append("g"); +} + +var fitnessData = {}; + +function fitness(id, value){ + if (!fitnessData[id]){ + fitnessData[id] = []; + for(i = 0; i < 100; i++){ + fitnessData[id].push(0); + } + } + + fitnessData[id].shift(); + fitnessData[id].push(value); + + var data = fitnessData[id]; + + var svg = d3.select('#' + id).select('svg'); + var path = svg.select('path'); + + var width = $('#' + id).children().width(), + height = $('#' + id).children().height(); + + var y = d3.scaleLinear() + .range([height, 0]) + .domain([-1, 17]); + + var x = d3.scaleLinear() + .range([0, width]) + .domain([data.length, 0]); + + + var lineChart = d3.line() + .x(function(d, i){ return x(i) }) + .y(function(d){ return y(d) }); + + var yAxis = d3.axisLeft(y); + var xAxis = d3.axisBottom(x); + + fitnessAxis[id+'y'].attr("transform", "translate(30,0)").call(yAxis); + fitnessAxis[id+'x'].attr("transform", "translate(30,"+ (height - 19) + ")").call(xAxis); + + path.attr("transform", "translate(30,0)").attr("d", lineChart(data)); +} diff --git a/graphs/network.css b/graphs/network.css new file mode 100644 index 0000000..90b24fe --- /dev/null +++ b/graphs/network.css @@ -0,0 +1,15 @@ +.links line { + stroke: #999; + stroke-opacity: 0.6; +} + +.nodes circle { + stroke: #fff; + stroke-width: 1.5px; +} + +svg { + width: 100%; + height: 100%; +} + diff --git a/graphs/network.js b/graphs/network.js new file mode 100644 index 0000000..1778e41 --- /dev/null +++ b/graphs/network.js @@ -0,0 +1,212 @@ +var simulation; +var color; +var link; +var node; +var graph={nodes:[], links:[]}; + +function ticked() { + link + .attr("x1", function(d) { return d.source.x; }) + .attr("y1", function(d) { return d.source.y; }) + .attr("x2", function(d) { return d.target.x; }) + .attr("y2", function(d) { return d.target.y; }); + + node + .attr("cx", function(d) { return d.x; }) + .attr("cy", function(d) { return d.y; }); +} + +function network_init(id){ + var svg = d3.select('#' + id).append('svg'); + svg.append("g").attr("class", "nodes"); + svg.append("g").attr("class", "links"); + + var width = $('#' + id).children().width(), + height = $('#' + id).children().height(); + + color = d3.scaleOrdinal(d3.schemeCategory20); + + simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(width / 2, height / 2)) + .alphaTarget(1).on("tick", ticked); + + link = svg.select(".links") + .selectAll("line"); + + node = svg.select(".nodes") + .selectAll("circle"); + +} + +var allNodes = {}; +var allLinks = {}; + +function addIfNewNode(nodes, newNodes, node_id){ + if (nodes.indexOf(node_id) < 0) { + newNodes.push({id: "node" + node_id, group: 0}); + nodes.push(node_id); + } +} + +function createLink(node_in, node_out, weight){ + return {source: "node" + node_in, target: "node" + node_out, value: weight }; +} + +function addIfNewLink(links, newLinks, updateLinks, node_in, node_out, weight){ + var link_id = node_in + "_" + node_out; + if (links.indexOf(link_id) < 0) { + newLinks.push(createLink(node_in, node_out, weight)); + links.push(link_id); + } else { + updateLinks.push(createLink(node_in, node_out, weight)); + } +} + +function getNodesLinks(id, genes){ + var newNodes = []; + var newLinks = []; + var updateLinks = []; + var removeLinks = []; + var removeNodes = []; + + var nodes = allNodes[id]; + var links = allLinks[id]; + + if (!nodes) { allNodes[id] = []; nodes = allNodes[id]; }; + if (!links) { allLinks[id] = []; links = allLinks[id]; }; + + genes.forEach(function(gene){ + if (gene.enabled) { + addIfNewNode(nodes, newNodes, gene.in_neuron_id); + addIfNewNode(nodes, newNodes, gene.out_neuron_id); + addIfNewLink(links, newLinks, updateLinks, gene.in_neuron_id, gene.out_neuron_id, gene.weight); + } + }); + + links.forEach(function(link){ + var found = false; + genes.forEach(function(gene){ + if (gene.in_neuron_id + "_" + gene.out_neuron_id == link) found = true; + }); + var link_ids = link.split("_"); + var linkObj = createLink(link_ids[0], link_ids[1], 0); + if (!found) removeLinks.push(linkObj); + }); + + nodes.forEach(function(node_id){ + var found = false; + genes.forEach(function(gene){ + if (gene.in_neuron_id == node_id || gene.out_neuron_id == node_id) found = true; + }); + if (!found) removeNodes.push("node" + node_id); + }); + + removeLinks.forEach(function(link){ + var linkId = link.source.split("node")[1] + "_" + link.target.split("node")[1]; + var linkPos = links.indexOf(linkId); + links.splice(linkPos, 1); + }); + + removeNodes.forEach(function(node_id){ + nodes.splice(nodes.indexOf(node_id), 1); + }); + + return {delete:{links: removeLinks, nodes: removeNodes} , update:{links: updateLinks}, add:{nodes: newNodes, links: newLinks}}; +} + +function network(id, genes){ + var nodesLinksUpdate = getNodesLinks(id, genes); + var newNodesLinks = nodesLinksUpdate.add; + var updateLinks = nodesLinksUpdate.update.links; + var deleteLinks = nodesLinksUpdate.delete.links; + var deleteNodes = nodesLinksUpdate.delete.nodes; + + var svg = d3.select('#' + id).select('svg'); + + deleteLinks.forEach(function(linkDelete){ + var index = 0; + var deleteIndex = -1; + graph.links.forEach(function(link){ + if (linkDelete.source == link.source.id && linkDelete.target == link.target.id) { + deleteIndex = index; + } + index++; + }); + if (deleteIndex >= 0) { + graph.links.splice(deleteIndex, 1); + } + }); + + deleteNodes.forEach(function(nodeDelete){ + var index = 0; + var deleteIndex = -1; + graph.nodes.forEach(function(node){ + if (nodeDelete == node.id) { + deleteIndex = index; + } + index++; + }); + if (deleteIndex >= 0) { + graph.nodes.splice(deleteIndex, 1); + } + }); + + updateLinks.forEach(function(linkUpdate){ + graph.links.forEach(function(link){ + if (link.source.id == linkUpdate.source && link.target.id == linkUpdate.target){ + link.value = linkUpdate.value; + } + }); + }); + + newNodesLinks.nodes.forEach(function(node){ + graph.nodes.push(node); + }); + + newNodesLinks.links.forEach(function(link){ + graph.links.push(link); + }); + + + node = node.data(graph.nodes); + node.exit().remove(); + + node = node.enter().append("circle") + .attr("r", 5) + .attr("fill", function(d) { return color(d.group); }) + .merge(node) + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + link = link.data(graph.links) + link.exit().remove(); + link = link.enter().append("line") + .attr("stroke-width", function(d) { return Math.sqrt(d.value * 10); }) + .merge(link); + + simulation.nodes(graph.nodes); + simulation.force("link").links(graph.links); + simulation.alpha(1).restart(); + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } +} + diff --git a/src/gene.rs b/src/gene.rs index b1d1ef8..d9bbd44 100644 --- a/src/gene.rs +++ b/src/gene.rs @@ -4,7 +4,7 @@ use rand::Closed01; use std::cmp::Ordering; /// A connection Gene -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, RustcEncodable)] pub struct Gene { in_neuron_id: usize, out_neuron_id: usize, diff --git a/src/lib.rs b/src/lib.rs index 2c2edc0..257b6c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,11 +15,15 @@ #[macro_use] extern crate lazy_static; +#[macro_use] +extern crate rusty_dashed; + extern crate conv; extern crate rand; extern crate rulinalg; extern crate num_cpus; extern crate crossbeam; +extern crate rustc_serialize; pub use ctrnn::CtrnnNeuralNetwork; pub use self::ctrnn::Ctrnn; diff --git a/src/population.rs b/src/population.rs index d102005..153f8b2 100644 --- a/src/population.rs +++ b/src/population.rs @@ -2,6 +2,12 @@ use conv::prelude::*; use environment::Environment; use genome::Genome; use organism::Organism; +#[cfg(feature = "telemetry")] +use rustc_serialize::json; + +#[cfg(feature = "telemetry")] +use rusty_dashed; + use specie::Specie; use species_evaluator::SpeciesEvaluator; @@ -38,12 +44,16 @@ impl Population { } /// TODO pub fn evaluate_in(&mut self, environment: &mut Environment) { - let champion_fitness = SpeciesEvaluator::new(environment).evaluate(&mut self.species); + let champion = SpeciesEvaluator::new(environment).evaluate(&mut self.species); - if self.champion_fitness > champion_fitness { + if self.champion_fitness >= champion.fitness { self.epochs_without_improvements += 1; } else { - self.champion_fitness = champion_fitness; + self.champion_fitness = champion.fitness; + telemetry!("fitness1", 1.0, format!("{}", self.champion_fitness)); + telemetry!("network1", + 1.0, + json::encode(&champion.genome.get_genes()).unwrap()); self.epochs_without_improvements = 0usize; } } diff --git a/src/species_evaluator.rs b/src/species_evaluator.rs index 43a7afa..0907cec 100644 --- a/src/species_evaluator.rs +++ b/src/species_evaluator.rs @@ -1,5 +1,6 @@ use crossbeam::{self, Scope}; use environment::Environment; +use genome::Genome; use num_cpus; use organism::Organism; use specie::Specie; @@ -22,13 +23,14 @@ impl<'a> SpeciesEvaluator<'a> { } /// return champion fitness - pub fn evaluate(&self, species: &mut Vec) -> f64 { - let mut champion_fitness = 0f64; + pub fn evaluate(&self, species: &mut Vec) -> Organism { + let mut champion: Organism = Organism::new(Genome::default()); + for specie in species { if !specie.organisms.is_empty() { 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(), organisms_by_thread, @@ -36,22 +38,22 @@ impl<'a> SpeciesEvaluator<'a> { &tx, scope); for _ in 0..threads_used { - let max_fitness = rx.recv().unwrap(); - if max_fitness > champion_fitness { - champion_fitness = max_fitness; + let champion_candidate = rx.recv().unwrap(); + if champion_candidate.fitness > champion.fitness { + champion = champion_candidate; } } }); } } - champion_fitness + champion } fn dispatch_organisms<'b>(&'b self, 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 { @@ -75,17 +77,17 @@ impl<'a> SpeciesEvaluator<'a> { fn evaluate_organisms<'b>(&'b self, organisms: &'b mut [Organism], - tx: Sender, + tx: Sender, scope: &Scope<'b>) { scope.spawn(move || { - let mut max_fitness = 0f64; + let mut champion = Organism::new(Genome::default()); for organism in &mut organisms.iter_mut() { organism.fitness = self.environment.test(organism); - if organism.fitness > max_fitness { - max_fitness = organism.fitness + if organism.fitness > champion.fitness { + champion = organism.clone(); } } - tx.send(max_fitness).unwrap(); + tx.send(champion).unwrap(); }); } }