Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTRIBUTING
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Thanks for contributing to this project.

To get started, check out the [issues page](https://github.com/inflectrix/neat). You can either find a feature/fix from there or start a new issue, then begin implementing it in your own fork of this repo.

Once you are done making the changes you'd like the make, start a pull request to the [dev](https://github.com/inflectrix/neat/tree/dev) branch. State your changes and request a review. After all branch rules have been satisfied, someone with management permissions on this repository will merge it.

56 changes: 51 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "neat"
description = "Crate for working with NEAT in rust"
version = "0.2.1"
version = "0.4.0"
edition = "2021"
authors = ["Inflectrix"]
repository = "https://github.com/inflectrix/neat"
Expand All @@ -26,11 +26,12 @@ serde = ["dep:serde", "dep:serde-big-array"]


[dependencies]
genetic-rs = "0.3"
genetic-rs = { version = "0.5.1", features = ["derive"] }
rand = "0.8.5"
rayon = { version = "1.8.1", optional = true }
serde = { version = "1.0.197", features = ["derive"], optional = true }
serde-big-array = { version = "0.5.1", optional = true }

[dev-dependencies]
bincode = "1.3.3"
bincode = "1.3.3"
serde_json = "1.0.114"
68 changes: 42 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
[<img alt="crates.io" src="https://img.shields.io/crates/d/neat" height="20">](https://crates.io/crates/neat)
[<img alt="docs.rs" src="https://img.shields.io/docsrs/neat" height="20">](https://docs.rs/neat)

Implementation of the NEAT algorithm using `genetic-rs`
Implementation of the NEAT algorithm using `genetic-rs`.

### Features
- rayon - Uses parallelization on the `NeuralNetwork` struct and adds the `rayon` feature to the `genetic-rs` re-export.
- serde - Adds the NNTSerde struct and allows for serialization of `NeuralNetworkTopology`
- crossover - Implements the `CrossoverReproduction` trait on `NeuralNetworkTopology`
- crossover - Implements the `CrossoverReproduction` trait on `NeuralNetworkTopology` and adds the `crossover` feature to the `genetic-rs` re-export.

*Do you like this repo and want to support it? If so, leave a ⭐*

### How To Use
When working with this crate, you'll want to use the `NeuralNetworkTopology` struct in your agent's DNA and
Expand All @@ -18,55 +20,58 @@ Here's an example of how one might use this crate:
```rust
use neat::*;

#[derive(Clone)]
#[derive(Clone, RandomlyMutable, DivisionReproduction)]
struct MyAgentDNA {
network: NeuralNetworkTopology<1, 2>,
other_stuff: Foo,
}

impl RandomlyMutable for MyAgentDNA {
fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) {
self.network.mutate(rate, rng);
self.other_stuff.mutate(rate, rng);
}
}

impl DivisionReproduction for MyAgentDNA {
fn spawn_child(&self, rng: &mut impl rand::Rng) -> Self {
Self {
network: self.network.spawn_child(rng),
// ...
}
}
}

impl GenerateRandom for MyAgentDNA {
fn gen_random(rng: &mut impl rand::Rng) -> Self {
Self {
network: NeuralNetworkTopology::new(0.01, 3, rng),
other_stuff: Foo::gen_random(rng),
}
}
}

struct MyAgent {
network: NeuralNetwork<1, 2>,
some_other_state: Bar,
// ... other state
}

impl From<&MyAgentDNA> for MyAgent {
fn from(value: &MyAgentDNA) -> Self {
Self {
network: NeuralNetwork::from(&value.network),
some_other_state: Bar::default(),
}
}
}

fn fitness(dna: &MyAgentDNA) -> f32 {
// agent will simply try to predict whether a number is greater than 0.5
let mut agent = MyAgent::from(dna);
let mut rng = rand::thread_rng();
let mut fitness = 0;

// ... use agent.network.predict() and agent.network.flush() throughout multiple iterations
// use repeated tests to avoid situational bias and some local maximums, overall providing more accurate score
for _ in 0..10 {
let n = rng.gen::<f32>();
let above = n > 0.5;

let res = agent.network.predict([n]);
let resi = res.iter().max_index();

if resi == 0 ^ above {
// agent did not guess correctly, punish slightly (too much will hinder exploration)
fitness -= 0.5;

continue;
}

// agent guessed correctly, they become more fit.
fitness += 3.;
}

fitness
}

fn main() {
Expand All @@ -78,9 +83,20 @@ fn main() {
division_pruning_nextgen,
);

// ... simulate generations, etc.
// simulate 100 generations
for _ in 0..100 {
sim.next_generation();
}

// display fitness results
let fits: Vec<_> = sim.entities
.iter()
.map(fitness)
.collect();

dbg!(&fits, fits.iter().max());
}
```

### License
This crate falls under the `MIT` license
This crate falls under the `MIT` license
56 changes: 20 additions & 36 deletions examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,14 @@
use neat::*;
use rand::prelude::*;

#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, DivisionReproduction, RandomlyMutable)]
#[cfg_attr(feature = "crossover", derive(CrossoverReproduction))]
struct AgentDNA {
network: NeuralNetworkTopology<2, 4>,
}

impl RandomlyMutable for AgentDNA {
fn mutate(&mut self, rate: f32, rng: &mut impl Rng) {
self.network.mutate(rate, rng);
}
}

impl Prunable for AgentDNA {}

impl DivisionReproduction for AgentDNA {
fn divide(&self, rng: &mut impl Rng) -> Self {
let mut child = self.clone();
child.mutate(self.network.mutation_rate, rng);
child
}
}

#[cfg(feature = "crossover")]
impl CrossoverReproduction for AgentDNA {
fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self {
Self {
network: self.network.crossover(&other.network, rng),
}
}
}

impl GenerateRandom for AgentDNA {
fn gen_random(rng: &mut impl rand::Rng) -> Self {
Self {
Expand Down Expand Up @@ -165,14 +143,17 @@ fn main() {
sim.next_generation();
}

let fits: Vec<_> = sim.genomes.iter().map(fitness).collect();
let mut fits: Vec<_> = sim.genomes.iter().map(|e| (e, fitness(e))).collect();

let maxfit = fits
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
fits.sort_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap());

dbg!(&fits, maxfit);
dbg!(&fits);

if cfg!(feature = "serde") {
let intermediate = NNTSerde::from(&fits[0].0.network);
let serialized = serde_json::to_string(&intermediate).unwrap();
println!("{}", serialized);
}
}

#[cfg(all(feature = "crossover", feature = "rayon"))]
Expand All @@ -183,12 +164,15 @@ fn main() {
sim.next_generation();
}

let fits: Vec<_> = sim.genomes.iter().map(fitness).collect();
let mut fits: Vec<_> = sim.genomes.iter().map(|e| (e, fitness(e))).collect();

let maxfit = fits
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
fits.sort_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap());

dbg!(&fits, maxfit);
dbg!(&fits);

if cfg!(feature = "serde") {
let intermediate = NNTSerde::from(&fits[0].0.network);
let serialized = serde_json::to_string(&intermediate).unwrap();
println!("serialized: {}", serialized);
}
}