diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index fef7ca6..fd849f4 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -2,7 +2,7 @@ name: CI-CD
on:
push:
- branches: [main]
+ branches: [main, dev]
pull_request:
jobs:
diff --git a/.gitignore b/.gitignore
index 1b71596..b2d8069 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/target/
-/.vscode/
\ No newline at end of file
+/.vscode/
+best-agent.json
+fitness-plot.svg
\ No newline at end of file
diff --git a/CONTRIBUTING b/CONTRIBUTING
deleted file mode 100644
index 68c1a8c..0000000
--- a/CONTRIBUTING
+++ /dev/null
@@ -1,6 +0,0 @@
-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.
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8ede4a4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,7 @@
+Thanks for contributing to this project.
+
+To get started, check out the [issues page](https://github.com/hypercodec/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/hypercodec/neat/tree/dev) branch. State your changes and request a review. After all branch rules have been satisfied and the pull request has a valid reason, someone with management permissions on this repository will merge it.
+
+You could also make a draft PR while implementing your features if you want feedback or discussion before finalizing your changes.
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index be4d7b8..0a9a53b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,21 +1,18 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
-name = "bincode"
-version = "1.3.3"
+name = "atomic_float"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
-dependencies = [
- "serde",
-]
+checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
[[package]]
name = "bitflags"
-version = "2.5.0"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "cfg-if"
@@ -56,9 +53,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "genetic-rs"
-version = "0.5.1"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b94601f3db2fb341f71a4470134eb1f71d39f54c2fe264122698eda67cd1c91b"
+checksum = "a68bb62a836f6ea3261d77cfec4012316e206f53e7d0eab519f5f3630e86001f"
dependencies = [
"genetic-rs-common",
"genetic-rs-macros",
@@ -66,9 +63,9 @@ dependencies = [
[[package]]
name = "genetic-rs-common"
-version = "0.5.1"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f41b0e3f6ccb66a00e7fc9170d4e02b1ae80c85f03c67b76b067b3637fd314a"
+checksum = "3be7aaffd4e4dc82d11819d40794f089c37d02595a401f229ed2877d1a4c401d"
dependencies = [
"rand",
"rayon",
@@ -77,9 +74,9 @@ dependencies = [
[[package]]
name = "genetic-rs-macros"
-version = "0.5.1"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d5ec3b9e69a6836bb0f0c8fa6972e6322e0b49108f7b3ed40769feb452c120a"
+checksum = "4e73b1f36ea3e799232e1a3141a2765fa6ee9ed7bb3fed96ccfb3bf272d1832e"
dependencies = [
"genetic-rs-common",
"proc-macro2",
@@ -100,32 +97,38 @@ dependencies = [
[[package]]
name = "itoa"
-version = "1.0.10"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.153"
+version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "neat"
version = "0.5.1"
dependencies = [
- "bincode",
+ "atomic_float",
"bitflags",
"genetic-rs",
"lazy_static",
- "rand",
"rayon",
+ "replace_with",
"serde",
"serde-big-array",
"serde_json",
@@ -139,9 +142,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
-version = "1.0.78"
+version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+checksum = "307e3004becf10f5a6e0d59d20f3cd28231b0e0827a96cd3e0ce6d14bc1e4bb3"
dependencies = [
"unicode-ident",
]
@@ -187,9 +190,9 @@ dependencies = [
[[package]]
name = "rayon"
-version = "1.8.1"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@@ -213,15 +216,15 @@ checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690"
[[package]]
name = "ryu"
-version = "1.0.17"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "serde"
-version = "1.0.197"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
@@ -237,9 +240,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.197"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -248,20 +251,21 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.114"
+version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
+ "memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
-version = "2.0.51"
+version = "2.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c"
+checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index 91247ad..4b26e0f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,9 +3,9 @@ name = "neat"
description = "Crate for working with NEAT in rust"
version = "0.5.1"
edition = "2021"
-authors = ["Inflectrix"]
-repository = "https://github.com/inflectrix/neat"
-homepage = "https://github.com/inflectrix/neat"
+authors = ["HyperCodec"]
+repository = "https://github.com/HyperCodec/neat"
+homepage = "https://github.com/HyperCodec/neat"
readme = "README.md"
keywords = ["genetic", "machine-learning", "ai", "algorithm", "evolution"]
categories = ["algorithms", "science", "simulation"]
@@ -18,22 +18,19 @@ rustdoc-args = ["--cfg", "docsrs"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
-default = ["max-index"]
-crossover = ["genetic-rs/crossover"]
-rayon = ["genetic-rs/rayon", "dep:rayon"]
-max-index = []
+default = []
serde = ["dep:serde", "dep:serde-big-array"]
[dependencies]
-bitflags = "2.5.0"
-genetic-rs = { version = "0.5.1", features = ["derive"] }
-lazy_static = "1.4.0"
-rand = "0.8.5"
-rayon = { version = "1.8.1", optional = true }
-serde = { version = "1.0.197", features = ["derive"], optional = true }
+atomic_float = "1.1.0"
+bitflags = "2.8.0"
+genetic-rs = { version = "0.5.4", features = ["rayon", "derive"] }
+lazy_static = "1.5.0"
+rayon = "1.10.0"
+replace_with = "0.1.7"
+serde = { version = "1.0.217", features = ["derive"], optional = true }
serde-big-array = { version = "0.5.1", optional = true }
[dev-dependencies]
-bincode = "1.3.3"
-serde_json = "1.0.114"
+serde_json = "1.0.138"
\ No newline at end of file
diff --git a/README.md b/README.md
index ad775e2..4e9828b 100644
--- a/README.md
+++ b/README.md
@@ -1,102 +1,17 @@
# neat
-[
](https://github.com/inflectrix/neat)
+[
](https://github.com/hypercodec/neat)
[
](https://crates.io/crates/neat)
[
](https://docs.rs/neat)
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` and adds the `crossover` feature to the `genetic-rs` re-export.
+- serde - Implements `Serialize` and `Deserialize` on most of the types in this crate.
-*Do you like this repo and want to support it? If so, leave a ⭐*
+*Do you like this crate 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
-the use `NeuralNetwork::from` when you finally want to test its performance. The `genetic-rs` crate is also re-exported with the rest of this crate.
-
-Here's an example of how one might use this crate:
-```rust
-use neat::*;
-
-#[derive(Clone, RandomlyMutable, DivisionReproduction)]
-struct MyAgentDNA {
- network: NeuralNetworkTopology<1, 2>,
-}
-
-impl GenerateRandom for MyAgentDNA {
- fn gen_random(rng: &mut impl rand::Rng) -> Self {
- Self {
- network: NeuralNetworkTopology::new(0.01, 3, rng),
- }
- }
-}
-
-struct MyAgent {
- network: NeuralNetwork<1, 2>,
- // ... other state
-}
-
-impl From<&MyAgentDNA> for MyAgent {
- fn from(value: &MyAgentDNA) -> Self {
- Self {
- network: NeuralNetwork::from(&value.network),
- }
- }
-}
-
-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 repeated tests to avoid situational bias and some local maximums, overall providing more accurate score
- for _ in 0..10 {
- let n = rng.gen::();
- 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() {
- let mut rng = rand::thread_rng();
-
- let mut sim = GeneticSim::new(
- Vec::gen_random(&mut rng, 100),
- fitness,
- division_pruning_nextgen,
- );
-
- // 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());
-}
-```
+# How To Use
+TODO
### License
This crate falls under the `MIT` license
diff --git a/examples/basic.rs b/examples/basic.rs
index 9ad0419..85f58cb 100644
--- a/examples/basic.rs
+++ b/examples/basic.rs
@@ -1,130 +1,3 @@
-//! A basic example of NEAT with this crate. Enable the `crossover` feature for it to use crossover reproduction
-
-use neat::*;
-use rand::prelude::*;
-
-#[derive(PartialEq, Clone, Debug, DivisionReproduction, RandomlyMutable)]
-#[cfg_attr(feature = "crossover", derive(CrossoverReproduction))]
-struct AgentDNA {
- network: NeuralNetworkTopology<2, 4>,
-}
-
-impl Prunable for AgentDNA {}
-
-impl GenerateRandom for AgentDNA {
- fn gen_random(rng: &mut impl rand::Rng) -> Self {
- Self {
- network: NeuralNetworkTopology::new(0.01, 3, rng),
- }
- }
-}
-
-#[derive(Debug)]
-struct Agent {
- network: NeuralNetwork<2, 4>,
-}
-
-impl From<&AgentDNA> for Agent {
- fn from(value: &AgentDNA) -> Self {
- Self {
- network: (&value.network).into(),
- }
- }
-}
-
-fn fitness(dna: &AgentDNA) -> f32 {
- let agent = Agent::from(dna);
-
- let mut fitness = 0.;
- let mut rng = rand::thread_rng();
-
- for _ in 0..10 {
- // 10 games
-
- // set up game
- let mut agent_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10));
- let mut food_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10));
-
- while food_pos == agent_pos {
- food_pos = (rng.gen_range(0..10), rng.gen_range(0..10));
- }
-
- let mut step = 0;
-
- loop {
- // perform actions in game
- let action = agent.network.predict([
- (food_pos.0 - agent_pos.0) as f32,
- (food_pos.1 - agent_pos.1) as f32,
- ]);
- let action = action.iter().max_index();
-
- match action {
- 0 => agent_pos.0 += 1,
- 1 => agent_pos.0 -= 1,
- 2 => agent_pos.1 += 1,
- _ => agent_pos.1 -= 1,
- }
-
- step += 1;
-
- if agent_pos == food_pos {
- fitness += 10.;
- break; // new game
- } else {
- // lose fitness for being slow and far away
- fitness -=
- (food_pos.0 - agent_pos.0 + food_pos.1 - agent_pos.1).abs() as f32 * 0.001;
- }
-
- // 50 steps per game
- if step == 50 {
- break;
- }
- }
- }
-
- fitness
-}
-
fn main() {
- #[cfg(not(feature = "rayon"))]
- let mut rng = rand::thread_rng();
-
- let mut sim = GeneticSim::new(
- #[cfg(not(feature = "rayon"))]
- Vec::gen_random(&mut rng, 100),
- #[cfg(feature = "rayon")]
- Vec::gen_random(100),
- fitness,
- #[cfg(not(feature = "crossover"))]
- division_pruning_nextgen,
- #[cfg(feature = "crossover")]
- crossover_pruning_nextgen,
- );
-
- for _ in 0..100 {
- sim.next_generation();
- }
-
- #[cfg(not(feature = "serde"))]
- let mut fits: Vec<_> = sim.genomes.iter().map(fitness).collect();
-
- #[cfg(feature = "serde")]
- let mut fits: Vec<_> = sim.genomes.iter().map(|e| (e, fitness(e))).collect();
-
- #[cfg(not(feature = "serde"))]
- fits.sort_by(|a, b| a.partial_cmp(&b).unwrap());
-
- #[cfg(feature = "serde")]
- fits.sort_by(|(_, a), (_, b)| a.partial_cmp(&b).unwrap());
-
- dbg!(&fits);
-
- #[cfg(feature = "serde")]
- {
- let intermediate = NNTSerde::from(&fits[0].0.network);
- let serialized = serde_json::to_string(&intermediate).unwrap();
- println!("{}", serialized);
- }
+ todo!("use NeuralNetwork as the entire DNA");
}
diff --git a/examples/extra_dna.rs b/examples/extra_dna.rs
new file mode 100644
index 0000000..038709f
--- /dev/null
+++ b/examples/extra_dna.rs
@@ -0,0 +1,3 @@
+fn main() {
+ todo!("use AgentDNA with additional params")
+}
diff --git a/src/topology/activation.rs b/src/activation.rs
similarity index 78%
rename from src/topology/activation.rs
rename to src/activation.rs
index a711851..af9f74e 100644
--- a/src/topology/activation.rs
+++ b/src/activation.rs
@@ -1,7 +1,12 @@
+/// Contains some builtin activation functions ([`sigmoid`], [`relu`], etc.)
+pub mod builtin;
+
+use bitflags::bitflags;
+use builtin::*;
+
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use bitflags::bitflags;
use lazy_static::lazy_static;
use std::{
collections::HashMap,
@@ -15,11 +20,11 @@ use crate::NeuronLocation;
#[macro_export]
macro_rules! activation_fn {
($F: path) => {
- ActivationFn::new(Arc::new($F), ActivationScope::default(), stringify!($F).into())
+ ActivationFn::new(std::sync::Arc::new($F), NeuronScope::default(), stringify!($F).into())
};
($F: path, $S: expr) => {
- ActivationFn::new(Arc::new($F), $S, stringify!($F).into())
+ ActivationFn::new(std::sync::Arc::new($F), $S, stringify!($F).into())
};
{$($F: path),*} => {
@@ -73,11 +78,11 @@ impl ActivationRegistry {
}
/// Gets all activation functions that are valid for a scope.
- pub fn activations_in_scope(&self, scope: ActivationScope) -> Vec {
+ pub fn activations_in_scope(&self, scope: NeuronScope) -> Vec {
let acts = self.activations();
acts.into_iter()
- .filter(|a| a.scope != ActivationScope::NONE && a.scope.contains(scope))
+ .filter(|a| !a.scope.contains(NeuronScope::NONE) && a.scope.contains(scope))
.collect()
}
}
@@ -88,51 +93,18 @@ impl Default for ActivationRegistry {
fns: HashMap::new(),
};
+ // TODO add a way to disable this
s.batch_register(activation_fn! {
- sigmoid => ActivationScope::HIDDEN | ActivationScope::OUTPUT,
- relu => ActivationScope::HIDDEN | ActivationScope::OUTPUT,
- linear_activation => ActivationScope::INPUT | ActivationScope::HIDDEN | ActivationScope::OUTPUT,
- f32::tanh => ActivationScope::HIDDEN | ActivationScope::OUTPUT
+ sigmoid => NeuronScope::HIDDEN | NeuronScope::OUTPUT,
+ relu => NeuronScope::HIDDEN | NeuronScope::OUTPUT,
+ linear_activation => NeuronScope::INPUT | NeuronScope::HIDDEN | NeuronScope::OUTPUT,
+ f32::tanh => NeuronScope::HIDDEN | NeuronScope::OUTPUT
});
s
}
}
-bitflags! {
- /// Specifies where an activation function can occur
- #[derive(Copy, Clone, Debug, Eq, PartialEq)]
- pub struct ActivationScope: u8 {
- /// Whether the activation can be applied to the input layer.
- const INPUT = 0b001;
-
- /// Whether the activation can be applied to the hidden layer.
- const HIDDEN = 0b010;
-
- /// Whether the activation can be applied to the output layer.
- const OUTPUT = 0b100;
-
- /// The activation function will not be randomly placed anywhere
- const NONE = 0b000;
- }
-}
-
-impl Default for ActivationScope {
- fn default() -> Self {
- Self::HIDDEN
- }
-}
-
-impl From<&NeuronLocation> for ActivationScope {
- fn from(value: &NeuronLocation) -> Self {
- match value {
- NeuronLocation::Input(_) => Self::INPUT,
- NeuronLocation::Hidden(_) => Self::HIDDEN,
- NeuronLocation::Output(_) => Self::OUTPUT,
- }
- }
-}
-
/// A trait that represents an activation method.
pub trait Activation {
/// The activation function.
@@ -152,17 +124,13 @@ pub struct ActivationFn {
pub func: Arc,
/// The scope defining where the activation function can appear.
- pub scope: ActivationScope,
+ pub scope: NeuronScope,
pub(crate) name: String,
}
impl ActivationFn {
/// Creates a new ActivationFn object.
- pub fn new(
- func: Arc,
- scope: ActivationScope,
- name: String,
- ) -> Self {
+ pub fn new(func: Arc, scope: NeuronScope, name: String) -> Self {
Self { func, name, scope }
}
}
@@ -206,17 +174,36 @@ impl<'a> Deserialize<'a> for ActivationFn {
}
}
-/// The sigmoid activation function.
-pub fn sigmoid(n: f32) -> f32 {
- 1. / (1. + std::f32::consts::E.powf(-n))
+bitflags! {
+ /// Specifies where an activation function can occur
+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+ pub struct NeuronScope: u8 {
+ /// Whether the activation can be applied to the input layer.
+ const INPUT = 0b001;
+
+ /// Whether the activation can be applied to the hidden layer.
+ const HIDDEN = 0b010;
+
+ /// Whether the activation can be applied to the output layer.
+ const OUTPUT = 0b100;
+
+ /// The activation function will not be randomly placed anywhere
+ const NONE = 0b000;
+ }
}
-/// The ReLU activation function.
-pub fn relu(n: f32) -> f32 {
- n.max(0.)
+impl Default for NeuronScope {
+ fn default() -> Self {
+ Self::HIDDEN
+ }
}
-/// Activation function that does nothing.
-pub fn linear_activation(n: f32) -> f32 {
- n
+impl> From for NeuronScope {
+ fn from(value: L) -> Self {
+ match value.as_ref() {
+ NeuronLocation::Input(_) => Self::INPUT,
+ NeuronLocation::Hidden(_) => Self::HIDDEN,
+ NeuronLocation::Output(_) => Self::OUTPUT,
+ }
+ }
}
diff --git a/src/activation/builtin.rs b/src/activation/builtin.rs
new file mode 100644
index 0000000..fdf7ab7
--- /dev/null
+++ b/src/activation/builtin.rs
@@ -0,0 +1,14 @@
+/// The sigmoid activation function. Scales all values nonlinearly in the range of 1 to -1.
+pub fn sigmoid(n: f32) -> f32 {
+ 1. / (1. + std::f32::consts::E.powf(-n))
+}
+
+/// The ReLU activation function. Equal to `n.max(0)``
+pub fn relu(n: f32) -> f32 {
+ n.max(0.)
+}
+
+/// Activation function that does nothing.
+pub fn linear_activation(n: f32) -> f32 {
+ n
+}
diff --git a/src/lib.rs b/src/lib.rs
index ee9f769..0de7360 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,25 +1,21 @@
-//! A simple crate that implements the Neuroevolution Augmenting Topologies algorithm using [genetic-rs](https://crates.io/crates/genetic-rs)
-//! ### Feature Roadmap:
-//! - [x] base (single-core) crate
-//! - [x] rayon
-//! - [x] serde
-//! - [x] crossover
+//! A crate implementing NeuroEvolution of Augmenting Topologies (NEAT).
//!
-//! You can get started by looking at [genetic-rs docs](https://docs.rs/genetic-rs) and checking the examples for this crate.
+//! The goal is to provide a simple-to-use, very dynamic [`NeuralNetwork`] type that
+//! integrates directly into the [`genetic-rs`](https://crates.io/crates/genetic-rs) ecosystem.
+//!
+//! Look at the README, docs, or examples to learn how to use this crate.
#![warn(missing_docs)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-/// A module containing the [`NeuralNetworkTopology`] struct. This is what you want to use in the DNA of your agent, as it is the thing that goes through nextgens and suppors mutation.
-pub mod topology;
+/// Contains the types surrounding activation functions.
+pub mod activation;
+
+/// Contains the [`NeuralNetwork`] and related types.
+pub mod neuralnet;
-/// A module containing the main [`NeuralNetwork`] struct.
-/// This has state/cache and will run the predictions. Make sure to run [`NeuralNetwork::flush_state`] between uses of [`NeuralNetwork::predict`].
-pub mod runnable;
+pub use neuralnet::*;
-pub use genetic_rs::prelude::*;
-pub use runnable::*;
-pub use topology::*;
+pub use genetic_rs::{self, prelude::*};
-#[cfg(feature = "serde")]
-pub use nnt_serde::*;
+#[cfg(test)]
+mod tests;
diff --git a/src/neuralnet.rs b/src/neuralnet.rs
new file mode 100644
index 0000000..cce0d61
--- /dev/null
+++ b/src/neuralnet.rs
@@ -0,0 +1,860 @@
+use std::{
+ collections::HashSet,
+ sync::{
+ atomic::{AtomicBool, AtomicUsize, Ordering},
+ Arc,
+ },
+};
+
+use atomic_float::AtomicF32;
+use genetic_rs::prelude::*;
+use rand::Rng;
+use replace_with::replace_with_or_abort;
+
+use crate::{
+ activation::{builtin::*, *},
+ activation_fn,
+};
+
+use rayon::prelude::*;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "serde")]
+use serde_big_array::BigArray;
+
+/// The mutation settings for [`NeuralNetwork`].
+/// Does not affect [`NeuralNetwork::mutate`], only [`NeuralNetwork::divide`] and [`NeuralNetwork::crossover`].
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq)]
+pub struct MutationSettings {
+ /// The chance of each mutation type to occur.
+ pub mutation_rate: f32,
+
+ /// The number of times to try to mutate the network.
+ pub mutation_passes: usize,
+
+ /// The maximum amount that the weights will be mutated by.
+ pub weight_mutation_amount: f32,
+}
+
+impl Default for MutationSettings {
+ fn default() -> Self {
+ Self {
+ mutation_rate: 0.01,
+ mutation_passes: 3,
+ weight_mutation_amount: 0.5,
+ }
+ }
+}
+
+/// An abstract neural network type with `I` input neurons and `O` output neurons.
+/// Hidden neurons are not organized into layers, but rather float and link freely
+/// (or at least in any way that doesn't cause a cyclic dependency).
+///
+/// See [`NeuralNetwork::predict`] for usage.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct NeuralNetwork {
+ /// The input layer of neurons. Values specified in [`NeuralNetwork::predict`] will start here.
+ #[cfg_attr(feature = "serde", serde(with = "BigArray"))]
+ pub input_layer: [Neuron; I],
+
+ /// The hidden layer(s) of neurons. They are not actually layered, but rather free-floating.
+ pub hidden_layers: Vec,
+
+ /// The output layer of neurons. Their values will be returned from [`NeuralNetwork::predict`].
+ #[cfg_attr(feature = "serde", serde(with = "BigArray"))]
+ pub output_layer: [Neuron; O],
+
+ /// The mutation settings for the network.
+ pub mutation_settings: MutationSettings,
+}
+
+impl NeuralNetwork {
+ // TODO option to set default output layer activations
+ /// Creates a new random neural network with the given settings.
+ pub fn new(mutation_settings: MutationSettings, rng: &mut impl Rng) -> Self {
+ let mut output_layer = Vec::with_capacity(O);
+
+ for _ in 0..O {
+ output_layer.push(Neuron::new_with_activation(
+ vec![],
+ activation_fn!(sigmoid),
+ rng,
+ ));
+ }
+
+ let mut input_layer = Vec::with_capacity(I);
+
+ for _ in 0..I {
+ let mut already_chosen = Vec::new();
+ let outputs = (0..rng.gen_range(1..=O))
+ .map(|_| {
+ let mut j = rng.gen_range(0..O);
+ while already_chosen.contains(&j) {
+ j = rng.gen_range(0..O);
+ }
+
+ output_layer[j].input_count += 1;
+ already_chosen.push(j);
+
+ (NeuronLocation::Output(j), rng.gen())
+ })
+ .collect();
+
+ input_layer.push(Neuron::new_with_activation(
+ outputs,
+ activation_fn!(linear_activation),
+ rng,
+ ));
+ }
+
+ let input_layer = input_layer.try_into().unwrap();
+ let output_layer = output_layer.try_into().unwrap();
+
+ Self {
+ input_layer,
+ hidden_layers: vec![],
+ output_layer,
+ mutation_settings,
+ }
+ }
+
+ /// Runs the neural network, propagating values from input to output layer.
+ pub fn predict(&self, inputs: [f32; I]) -> [f32; O] {
+ let cache = Arc::new(NeuralNetCache::from(self));
+ cache.prime_inputs(inputs);
+
+ (0..I)
+ .into_par_iter()
+ .for_each(|i| self.eval(NeuronLocation::Input(i), cache.clone()));
+
+ cache.output()
+ }
+
+ fn eval(&self, loc: impl AsRef, cache: Arc>) {
+ let loc = loc.as_ref();
+
+ if !cache.claim(loc) {
+ // some other thread is already
+ // waiting to do this task, currently doing it, or done.
+ // no need to do it again.
+ return;
+ }
+
+ while !cache.is_ready(loc) {
+ // essentially spinlocks until the dependency tasks are complete,
+ // while letting this thread do some work on random tasks.
+ rayon::yield_now();
+ }
+
+ let val = cache.get(loc);
+ let n = self.get_neuron(loc);
+
+ n.outputs.par_iter().for_each(|(loc2, weight)| {
+ cache.add(loc2, n.activate(val * weight));
+ self.eval(loc2, cache.clone());
+ });
+ }
+
+ /// Get a neuron at the specified [`NeuronLocation`].
+ pub fn get_neuron(&self, loc: impl AsRef) -> &Neuron {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => &self.input_layer[*i],
+ NeuronLocation::Hidden(i) => &self.hidden_layers[*i],
+ NeuronLocation::Output(i) => &self.output_layer[*i],
+ }
+ }
+
+ /// Get a mutable reference to the neuron at the specified [`NeuronLocation`].
+ pub fn get_neuron_mut(&mut self, loc: impl AsRef) -> &mut Neuron {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => &mut self.input_layer[*i],
+ NeuronLocation::Hidden(i) => &mut self.hidden_layers[*i],
+ NeuronLocation::Output(i) => &mut self.output_layer[*i],
+ }
+ }
+
+ /// Split a [`Connection`] into two of the same weight, joined by a new [`Neuron`] in the hidden layer(s).
+ pub fn split_connection(&mut self, connection: Connection, rng: &mut impl Rng) {
+ let newloc = NeuronLocation::Hidden(self.hidden_layers.len());
+
+ let a = self.get_neuron_mut(connection.from);
+ let weight = unsafe { a.remove_connection(connection.to) }.unwrap();
+
+ a.outputs.push((newloc, weight));
+
+ let n = Neuron::new(vec![(connection.to, weight)], NeuronScope::HIDDEN, rng);
+ self.hidden_layers.push(n);
+ }
+
+ /// Adds a connection but does not check for cyclic linkages.
+ ///
+ /// # Safety
+ /// This is marked as unsafe because it could cause a hang/livelock when predicting due to cyclic linkage.
+ /// There is no actual UB or unsafe code associated with it.
+ pub unsafe fn add_connection_raw(&mut self, connection: Connection, weight: f32) {
+ let a = self.get_neuron_mut(connection.from);
+ a.outputs.push((connection.to, weight));
+
+ // let b = self.get_neuron_mut(connection.to);
+ // b.inputs.insert(connection.from);
+ }
+
+ /// Returns false if the connection is cyclic.
+ pub fn is_connection_safe(&self, connection: Connection) -> bool {
+ let mut visited = HashSet::from([connection.from]);
+
+ self.dfs(&mut visited, connection.to)
+ }
+
+ // TODO maybe parallelize
+ fn dfs(&self, visited: &mut HashSet, current: NeuronLocation) -> bool {
+ if !visited.insert(current) {
+ return false;
+ }
+
+ let n = self.get_neuron(current);
+ for (loc, _) in &n.outputs {
+ if !self.dfs(visited, *loc) {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ /// Safe, checked add connection method. Returns false if it aborted connecting due to cyclic linkage.
+ pub fn add_connection(&mut self, connection: Connection, weight: f32) -> bool {
+ if !self.is_connection_safe(connection) {
+ return false;
+ }
+
+ unsafe {
+ self.add_connection_raw(connection, weight);
+ }
+
+ true
+ }
+
+ /// Mutates a connection's weight.
+ pub fn mutate_weight(&mut self, connection: Connection, rng: &mut impl Rng) {
+ let rate = self.mutation_settings.weight_mutation_amount;
+ let n = self.get_neuron_mut(connection.from);
+ n.mutate_weight(connection.to, rate, rng).unwrap();
+ }
+
+ /// Get a random valid location within the network.
+ pub fn random_location(&self, rng: &mut impl Rng) -> NeuronLocation {
+ match rng.gen_range(0..3) {
+ 0 => NeuronLocation::Input(rng.gen_range(0..self.input_layer.len())),
+ 1 => NeuronLocation::Hidden(rng.gen_range(0..self.hidden_layers.len())),
+ 2 => NeuronLocation::Output(rng.gen_range(0..self.output_layer.len())),
+ _ => unreachable!(),
+ }
+ }
+
+ /// Get a random valid location within a [`NeuronScope`].
+ pub fn random_location_in_scope(
+ &self,
+ rng: &mut impl Rng,
+ scope: NeuronScope,
+ ) -> NeuronLocation {
+ let loc = self.random_location(rng);
+
+ // this is a lazy and slow way of donig it, TODO better version.
+ if !scope.contains(NeuronScope::from(loc)) {
+ return self.random_location_in_scope(rng, scope);
+ }
+
+ loc
+ }
+
+ /// Remove a connection and any hanging neurons caused by the deletion.
+ /// Returns whether there was a hanging neuron.
+ pub fn remove_connection(&mut self, connection: Connection) -> bool {
+ let a = self.get_neuron_mut(connection.from);
+ unsafe { a.remove_connection(connection.to) }.unwrap();
+
+ let b = self.get_neuron_mut(connection.to);
+ b.input_count -= 1;
+
+ if b.input_count == 0 {
+ self.remove_neuron(connection.to);
+ return true;
+ }
+
+ false
+ }
+
+ /// Remove a neuron and downshift all connection indexes to compensate for it.
+ pub fn remove_neuron(&mut self, loc: impl AsRef) {
+ let loc = loc.as_ref();
+ if !loc.is_hidden() {
+ panic!("Can only remove neurons from hidden layer");
+ }
+
+ unsafe {
+ self.downshift_connections(loc.unwrap());
+ }
+ }
+
+ unsafe fn downshift_connections(&mut self, i: usize) {
+ self.input_layer
+ .par_iter_mut()
+ .for_each(|n| n.downshift_outputs(i));
+
+ self.hidden_layers
+ .par_iter_mut()
+ .for_each(|n| n.downshift_outputs(i));
+ }
+
+ // TODO maybe more parallelism and pass Connection info.
+ /// Runs the `callback` on the weights of the neural network in parallel, allowing it to modify weight values.
+ pub fn map_weights(&mut self, callback: impl Fn(&mut f32) + Sync) {
+ for n in &mut self.input_layer {
+ n.outputs.par_iter_mut().for_each(|(_, w)| callback(w));
+ }
+
+ for n in &mut self.hidden_layers {
+ n.outputs.par_iter_mut().for_each(|(_, w)| callback(w));
+ }
+ }
+
+ unsafe fn clear_input_counts(&mut self) {
+ // not sure whether all this parallelism is necessary or if it will just generate overhead
+ // rayon::scope(|s| {
+ // s.spawn(|_| self.input_layer.par_iter_mut().for_each(|n| n.input_count = 0));
+ // s.spawn(|_| self.hidden_layers.par_iter_mut().for_each(|n| n.input_count = 0));
+ // s.spawn(|_| self.output_layer.par_iter_mut().for_each(|n| n.input_count = 0));
+ // });
+
+ self.input_layer
+ .par_iter_mut()
+ .for_each(|n| n.input_count = 0);
+ self.hidden_layers
+ .par_iter_mut()
+ .for_each(|n| n.input_count = 0);
+ self.output_layer
+ .par_iter_mut()
+ .for_each(|n| n.input_count = 0);
+ }
+
+ /// Recalculates the [`input_count`][`Neuron::input_count`] field for all neurons in the network.
+ pub fn recalculate_input_counts(&mut self) {
+ unsafe { self.clear_input_counts() };
+
+ for i in 0..I {
+ for j in 0..self.input_layer[i].outputs.len() {
+ let (loc, _) = self.input_layer[i].outputs[j];
+ self.get_neuron_mut(loc).input_count += 1;
+ }
+ }
+
+ for i in 0..self.hidden_layers.len() {
+ for j in 0..self.hidden_layers[i].outputs.len() {
+ let (loc, _) = self.hidden_layers[i].outputs[j];
+ self.get_neuron_mut(loc).input_count += 1;
+ }
+ }
+ }
+}
+
+impl RandomlyMutable for NeuralNetwork {
+ fn mutate(&mut self, rate: f32, rng: &mut impl Rng) {
+ if rng.gen::() <= rate {
+ // split connection
+ let from = self.random_location_in_scope(rng, !NeuronScope::OUTPUT);
+ let n = self.get_neuron(from);
+ let (to, _) = n.random_output(rng);
+
+ self.split_connection(Connection { from, to }, rng);
+ }
+
+ if rng.gen::() <= rate {
+ // add connection
+ let weight = rng.gen::();
+
+ let from = self.random_location_in_scope(rng, !NeuronScope::OUTPUT);
+ let to = self.random_location_in_scope(rng, !NeuronScope::INPUT);
+
+ let mut connection = Connection { from, to };
+ while !self.add_connection(connection, weight) {
+ let from = self.random_location_in_scope(rng, !NeuronScope::OUTPUT);
+ let to = self.random_location_in_scope(rng, !NeuronScope::INPUT);
+ connection = Connection { from, to };
+ }
+ }
+
+ if rng.gen::() <= rate {
+ // remove connection
+
+ let from = self.random_location_in_scope(rng, !NeuronScope::OUTPUT);
+ let a = self.get_neuron(from);
+ let (to, _) = a.random_output(rng);
+
+ self.remove_connection(Connection { from, to });
+ }
+
+ self.map_weights(|w| {
+ // TODO maybe `Send`able rng.
+ let mut rng = rand::thread_rng();
+
+ if rng.gen::() <= rate {
+ *w += rng.gen_range(-rate..rate);
+ }
+ });
+ }
+}
+
+impl DivisionReproduction for NeuralNetwork {
+ fn divide(&self, rng: &mut impl Rng) -> Self {
+ let mut child = self.clone();
+
+ for _ in 0..self.mutation_settings.mutation_passes {
+ child.mutate(child.mutation_settings.mutation_rate, rng);
+ }
+
+ child
+ }
+}
+
+#[allow(clippy::needless_range_loop)]
+impl CrossoverReproduction for NeuralNetwork {
+ fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self {
+ let mut output_layer = self.output_layer.clone();
+
+ for (i, n) in output_layer.iter_mut().enumerate() {
+ if rng.gen::() >= 0.5 {
+ *n = other.output_layer[i].clone();
+ }
+ }
+
+ let hidden_len = self.hidden_layers.len().max(other.hidden_layers.len());
+ let mut hidden_layers = Vec::with_capacity(hidden_len);
+
+ for i in 0..hidden_len {
+ if rng.gen::() >= 0.5 {
+ if let Some(n) = self.hidden_layers.get(i) {
+ let mut n = n.clone();
+ n.prune_invalid_outputs(hidden_len, O);
+
+ hidden_layers[i] = n;
+
+ continue;
+ }
+ }
+
+ let mut n = other.hidden_layers[i].clone();
+ n.prune_invalid_outputs(hidden_len, O);
+
+ hidden_layers[i] = n;
+ }
+
+ let mut input_layer = self.input_layer.clone();
+
+ for (i, n) in input_layer.iter_mut().enumerate() {
+ if rng.gen::() >= 0.5 {
+ *n = other.input_layer[i].clone();
+ }
+ n.prune_invalid_outputs(hidden_len, O);
+ }
+
+ // crossover mutation settings just in case.
+ let mutation_settings = if rng.gen::() >= 0.5 {
+ self.mutation_settings.clone()
+ } else {
+ other.mutation_settings.clone()
+ };
+
+ let mut child = Self {
+ input_layer,
+ hidden_layers,
+ output_layer,
+ mutation_settings,
+ };
+
+ // TODO maybe find a way to do this while doing crossover stuff instead of recalculating everything.
+ // would be annoying to implement though.
+ child.recalculate_input_counts();
+
+ for _ in 0..child.mutation_settings.mutation_passes {
+ child.mutate(child.mutation_settings.mutation_rate, rng);
+ }
+
+ child
+ }
+}
+
+fn output_exists(loc: NeuronLocation, hidden_len: usize, output_len: usize) -> bool {
+ match loc {
+ NeuronLocation::Input(_) => false,
+ NeuronLocation::Hidden(i) => i < hidden_len,
+ NeuronLocation::Output(i) => i < output_len,
+ }
+}
+
+/// A helper struct for operations on connections between neurons.
+/// It does not contain information about the weight.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Connection {
+ /// The source of the connection.
+ pub from: NeuronLocation,
+
+ /// The destination of the connection.
+ pub to: NeuronLocation,
+}
+
+/// A stateless neuron. Contains info about bias, activation, and connections.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Neuron {
+ /// The input count used in [`NeuralNetCache`]. Not safe to modify.
+ pub input_count: usize,
+
+ /// The connections and weights to other neurons.
+ pub outputs: Vec<(NeuronLocation, f32)>,
+
+ /// The initial value of the neuron.
+ pub bias: f32,
+
+ /// The activation function applied to the value before propagating to [`outputs`][Neuron::outputs].
+ pub activation_fn: ActivationFn,
+}
+
+impl Neuron {
+ /// Creates a new neuron with a specified activation function and outputs.
+ pub fn new_with_activation(
+ outputs: Vec<(NeuronLocation, f32)>,
+ activation_fn: ActivationFn,
+ rng: &mut impl Rng,
+ ) -> Self {
+ Self {
+ input_count: 0,
+ outputs,
+ bias: rng.gen(),
+ activation_fn,
+ }
+ }
+
+ /// Creates a new neuron with the given output locations.
+ /// Chooses a random activation function within the specified scope.
+ pub fn new(
+ outputs: Vec<(NeuronLocation, f32)>,
+ current_scope: NeuronScope,
+ rng: &mut impl Rng,
+ ) -> Self {
+ let reg = ACTIVATION_REGISTRY.read().unwrap();
+ let activations = reg.activations_in_scope(current_scope);
+
+ Self::new_with_activations(outputs, activations, rng)
+ }
+
+ /// Creates a new neuron with the given outputs.
+ /// Takes a collection of activation functions and chooses a random one from them to use.
+ pub fn new_with_activations(
+ outputs: Vec<(NeuronLocation, f32)>,
+ activations: impl IntoIterator- ,
+ rng: &mut impl Rng,
+ ) -> Self {
+ // TODO get random in iterator form
+ let mut activations: Vec<_> = activations.into_iter().collect();
+
+ // TODO maybe Result instead.
+ if activations.is_empty() {
+ panic!("Empty activations list provided");
+ }
+
+ Self::new_with_activation(
+ outputs,
+ activations.remove(rng.gen_range(0..activations.len())),
+ rng,
+ )
+ }
+
+ /// Runs the [activation function][Neuron::activation_fn] on the given value and returns it.
+ pub fn activate(&self, v: f32) -> f32 {
+ self.activation_fn.func.activate(v)
+ }
+
+ /// Get the weight of the provided output location. Returns `None` if not found.
+ pub fn get_weight(&self, output: impl AsRef) -> Option {
+ let loc = *output.as_ref();
+ for out in &self.outputs {
+ if out.0 == loc {
+ return Some(out.1);
+ }
+ }
+
+ None
+ }
+
+ /// Tries to remove a connection from the neuron and returns the weight if it was found.
+ ///
+ /// # Safety
+ /// This is marked as unsafe because it will not update the destination's [`input_count`][Neuron::input_count].
+ /// Similar to [`add_connection_raw`][NeuralNetwork::add_connection_raw], this does not mean UB or anything.
+ pub unsafe fn remove_connection(&mut self, output: impl AsRef) -> Option {
+ let loc = *output.as_ref();
+ let mut i = 0;
+
+ while i < self.outputs.len() {
+ if self.outputs[i].0 == loc {
+ return Some(self.outputs.remove(i).1);
+ }
+ i += 1;
+ }
+
+ None
+ }
+
+ /// Randomly mutates the specified weight with the rate.
+ pub fn mutate_weight(
+ &mut self,
+ output: impl AsRef,
+ rate: f32,
+ rng: &mut impl Rng,
+ ) -> Option {
+ let loc = *output.as_ref();
+ let mut i = 0;
+
+ while i < self.outputs.len() {
+ let o = &mut self.outputs[i];
+ if o.0 == loc {
+ o.1 += rng.gen_range(-rate..rate);
+
+ return Some(o.1);
+ }
+
+ i += 1;
+ }
+
+ None
+ }
+
+ /// Get a random output location and weight.
+ pub fn random_output(&self, rng: &mut impl Rng) -> (NeuronLocation, f32) {
+ self.outputs[rng.gen_range(0..self.outputs.len())]
+ }
+
+ pub(crate) fn downshift_outputs(&mut self, i: usize) {
+ // TODO par_iter_mut instead of replace
+ replace_with_or_abort(&mut self.outputs, |o| {
+ o.into_par_iter()
+ .map(|(loc, w)| match loc {
+ NeuronLocation::Hidden(j) if j > i => (NeuronLocation::Hidden(j - 1), w),
+ _ => (loc, w),
+ })
+ .collect()
+ });
+ }
+
+ /// Removes any outputs pointing to a nonexistent neuron.
+ pub fn prune_invalid_outputs(&mut self, hidden_len: usize, output_len: usize) {
+ self.outputs
+ .retain(|(loc, _)| output_exists(*loc, hidden_len, output_len));
+ }
+}
+
+/// A pseudo-pointer of sorts that is used for caching.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum NeuronLocation {
+ /// Points to a neuron in the input layer at contained index.
+ Input(usize),
+
+ /// Points to a neuron in the hidden layer at contained index.
+ Hidden(usize),
+
+ /// Points to a neuron in the output layer at contained index.
+ Output(usize),
+}
+
+impl NeuronLocation {
+ /// Returns `true` if it points to the input layer. Otherwise, returns `false`.
+ pub fn is_input(&self) -> bool {
+ matches!(self, Self::Input(_))
+ }
+
+ /// Returns `true` if it points to the hidden layer. Otherwise, returns `false`.
+ pub fn is_hidden(&self) -> bool {
+ matches!(self, Self::Hidden(_))
+ }
+
+ /// Returns `true` if it points to the output layer. Otherwise, returns `false`.
+ pub fn is_output(&self) -> bool {
+ matches!(self, Self::Output(_))
+ }
+
+ /// Retrieves the index value, regardless of layer. Does not consume.
+ pub fn unwrap(&self) -> usize {
+ match self {
+ Self::Input(i) => *i,
+ Self::Hidden(i) => *i,
+ Self::Output(i) => *i,
+ }
+ }
+}
+
+impl AsRef for NeuronLocation {
+ fn as_ref(&self) -> &NeuronLocation {
+ self
+ }
+}
+
+/// Handles the state of a single neuron for [`NeuralNetCache`].
+#[derive(Debug, Default)]
+pub struct NeuronCache {
+ /// The value of the neuron.
+ pub value: AtomicF32,
+
+ /// The expected input count.
+ pub expected_inputs: usize,
+
+ /// The number of inputs that have finished evaluating.
+ pub finished_inputs: AtomicUsize,
+
+ /// Whether or not a thread has claimed this neuron to work on it.
+ pub claimed: AtomicBool,
+}
+
+impl NeuronCache {
+ /// Creates a new [`NeuronCache`] given relevant info.
+ /// Use [`NeuronCache::from`] instead to create cache for a [`Neuron`].
+ pub fn new(bias: f32, expected_inputs: usize) -> Self {
+ Self {
+ value: AtomicF32::new(bias),
+ expected_inputs,
+ ..Default::default()
+ }
+ }
+}
+
+impl From<&Neuron> for NeuronCache {
+ fn from(value: &Neuron) -> Self {
+ Self {
+ value: AtomicF32::new(value.bias),
+ expected_inputs: value.input_count,
+ finished_inputs: AtomicUsize::new(0),
+ claimed: AtomicBool::new(false),
+ }
+ }
+}
+
+/// A cache type used in [`NeuralNetwork::predict`] to track state.
+#[derive(Debug)]
+pub struct NeuralNetCache {
+ /// The input layer cache.
+ pub input_layer: [NeuronCache; I],
+
+ /// The hidden layer(s) cache.
+ pub hidden_layers: Vec,
+
+ /// The output layer cache.
+ pub output_layer: [NeuronCache; O],
+}
+
+impl NeuralNetCache {
+ /// Gets the value of a neuron at the given location.
+ pub fn get(&self, loc: impl AsRef) -> f32 {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => self.input_layer[*i].value.load(Ordering::SeqCst),
+ NeuronLocation::Hidden(i) => self.hidden_layers[*i].value.load(Ordering::SeqCst),
+ NeuronLocation::Output(i) => self.output_layer[*i].value.load(Ordering::SeqCst),
+ }
+ }
+
+ /// Adds a value to the neuron at the specified location and increments [`finished_inputs`][NeuronCache::finished_inputs].
+ pub fn add(&self, loc: impl AsRef, n: f32) -> f32 {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => self.input_layer[*i].value.fetch_add(n, Ordering::SeqCst),
+ NeuronLocation::Hidden(i) => {
+ let c = &self.hidden_layers[*i];
+ let v = c.value.fetch_add(n, Ordering::SeqCst);
+ c.finished_inputs.fetch_add(1, Ordering::SeqCst);
+ v
+ }
+ NeuronLocation::Output(i) => {
+ let c = &self.output_layer[*i];
+ let v = c.value.fetch_add(n, Ordering::SeqCst);
+ c.finished_inputs.fetch_add(1, Ordering::SeqCst);
+ v
+ }
+ }
+ }
+
+ /// Returns whether [`finished_inputs`][NeuronCache::finished_inputs] matches [`expected_inputs`][NeuronCache::expected_inputs].
+ pub fn is_ready(&self, loc: impl AsRef) -> bool {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => {
+ let c = &self.input_layer[*i];
+ c.expected_inputs >= c.finished_inputs.load(Ordering::SeqCst)
+ }
+ NeuronLocation::Hidden(i) => {
+ let c = &self.hidden_layers[*i];
+ c.expected_inputs >= c.finished_inputs.load(Ordering::SeqCst)
+ }
+ NeuronLocation::Output(i) => {
+ let c = &self.output_layer[*i];
+ c.expected_inputs >= c.finished_inputs.load(Ordering::SeqCst)
+ }
+ }
+ }
+
+ /// Adds the input values to the input layer of neurons.
+ pub fn prime_inputs(&self, inputs: [f32; I]) {
+ for (i, v) in inputs.into_iter().enumerate() {
+ self.input_layer[i].value.fetch_add(v, Ordering::SeqCst);
+ }
+ }
+
+ /// Fetches and packs the output layer values into an array.
+ pub fn output(&self) -> [f32; O] {
+ let output: Vec<_> = self
+ .output_layer
+ .par_iter()
+ .map(|c| c.value.load(Ordering::SeqCst))
+ .collect();
+
+ output.try_into().unwrap()
+ }
+
+ /// Attempts to claim a neuron. Returns false if it has already been claimed.
+ pub fn claim(&self, loc: impl AsRef) -> bool {
+ match loc.as_ref() {
+ NeuronLocation::Input(i) => self.input_layer[*i]
+ .claimed
+ .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok(),
+ NeuronLocation::Hidden(i) => self.hidden_layers[*i]
+ .claimed
+ .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok(),
+ NeuronLocation::Output(i) => self.output_layer[*i]
+ .claimed
+ .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok(),
+ }
+ }
+}
+
+impl From<&NeuralNetwork> for NeuralNetCache {
+ fn from(net: &NeuralNetwork) -> Self {
+ let input_layer: Vec<_> = net.input_layer.par_iter().map(|n| n.into()).collect();
+ let input_layer = input_layer.try_into().unwrap();
+
+ let hidden_layers = net.hidden_layers.par_iter().map(|n| n.into()).collect();
+
+ let output_layer: Vec<_> = net.output_layer.par_iter().map(|n| n.into()).collect();
+ let output_layer = output_layer.try_into().unwrap();
+
+ Self {
+ input_layer,
+ hidden_layers,
+ output_layer,
+ }
+ }
+}
diff --git a/src/runnable.rs b/src/runnable.rs
deleted file mode 100644
index 5b28f54..0000000
--- a/src/runnable.rs
+++ /dev/null
@@ -1,300 +0,0 @@
-use crate::topology::*;
-
-#[cfg(not(feature = "rayon"))]
-use std::{cell::RefCell, rc::Rc};
-
-#[cfg(feature = "rayon")]
-use rayon::prelude::*;
-#[cfg(feature = "rayon")]
-use std::sync::{Arc, RwLock};
-
-/// A runnable, stated Neural Network generated from a [NeuralNetworkTopology]. Use [`NeuralNetwork::from`] to go from stateles to runnable.
-/// Because this has state, you need to run [`NeuralNetwork::flush_state`] between [`NeuralNetwork::predict`] calls.
-#[derive(Debug)]
-#[cfg(not(feature = "rayon"))]
-pub struct NeuralNetwork {
- input_layer: [Rc>; I],
- hidden_layers: Vec>>,
- output_layer: [Rc>; O],
-}
-
-/// Parallelized version of the [`NeuralNetwork`] struct.
-#[derive(Debug)]
-#[cfg(feature = "rayon")]
-pub struct NeuralNetwork {
- input_layer: [Arc>; I],
- hidden_layers: Vec>>,
- output_layer: [Arc>; O],
-}
-
-impl NeuralNetwork {
- /// Predicts an output for the given inputs.
- #[cfg(not(feature = "rayon"))]
- pub fn predict(&self, inputs: [f32; I]) -> [f32; O] {
- for (i, v) in inputs.iter().enumerate() {
- let mut nw = self.input_layer[i].borrow_mut();
- nw.state.value = *v;
- nw.state.processed = true;
- }
-
- (0..O)
- .map(NeuronLocation::Output)
- .map(|loc| self.process_neuron(loc))
- .collect::>()
- .try_into()
- .unwrap()
- }
-
- /// Parallelized prediction of outputs from inputs.
- #[cfg(feature = "rayon")]
- pub fn predict(&self, inputs: [f32; I]) -> [f32; O] {
- inputs.par_iter().enumerate().for_each(|(i, v)| {
- let mut nw = self.input_layer[i].write().unwrap();
- nw.state.value = *v;
- nw.state.processed = true;
- });
-
- (0..O)
- .map(NeuronLocation::Output)
- .collect::>()
- .into_par_iter()
- .map(|loc| self.process_neuron(loc))
- .collect::>()
- .try_into()
- .unwrap()
- }
-
- #[cfg(not(feature = "rayon"))]
- fn process_neuron(&self, loc: NeuronLocation) -> f32 {
- let n = self.get_neuron(loc);
-
- {
- let nr = n.borrow();
-
- if nr.state.processed {
- return nr.state.value;
- }
- }
-
- let mut n = n.borrow_mut();
-
- for (l, w) in n.inputs.clone() {
- n.state.value += self.process_neuron(l) * w;
- }
-
- n.activate();
-
- n.state.value
- }
-
- #[cfg(feature = "rayon")]
- fn process_neuron(&self, loc: NeuronLocation) -> f32 {
- let n = self.get_neuron(loc);
-
- {
- let nr = n.read().unwrap();
-
- if nr.state.processed {
- return nr.state.value;
- }
- }
-
- let val: f32 = n
- .read()
- .unwrap()
- .inputs
- .par_iter()
- .map(|&(n2, w)| {
- let processed = self.process_neuron(n2);
- processed * w
- })
- .sum();
-
- let mut nw = n.write().unwrap();
- nw.state.value += val;
- nw.activate();
-
- nw.state.value
- }
-
- #[cfg(not(feature = "rayon"))]
- fn get_neuron(&self, loc: NeuronLocation) -> Rc> {
- match loc {
- NeuronLocation::Input(i) => self.input_layer[i].clone(),
- NeuronLocation::Hidden(i) => self.hidden_layers[i].clone(),
- NeuronLocation::Output(i) => self.output_layer[i].clone(),
- }
- }
-
- #[cfg(feature = "rayon")]
- fn get_neuron(&self, loc: NeuronLocation) -> Arc> {
- match loc {
- NeuronLocation::Input(i) => self.input_layer[i].clone(),
- NeuronLocation::Hidden(i) => self.hidden_layers[i].clone(),
- NeuronLocation::Output(i) => self.output_layer[i].clone(),
- }
- }
-
- /// Flushes the network's state after a [prediction][NeuralNetwork::predict].
- #[cfg(not(feature = "rayon"))]
- pub fn flush_state(&self) {
- for n in &self.input_layer {
- n.borrow_mut().flush_state();
- }
-
- for n in &self.hidden_layers {
- n.borrow_mut().flush_state();
- }
-
- for n in &self.output_layer {
- n.borrow_mut().flush_state();
- }
- }
-
- /// Flushes the neural network's state.
- #[cfg(feature = "rayon")]
- pub fn flush_state(&self) {
- self.input_layer
- .par_iter()
- .for_each(|n| n.write().unwrap().flush_state());
-
- self.hidden_layers
- .par_iter()
- .for_each(|n| n.write().unwrap().flush_state());
-
- self.output_layer
- .par_iter()
- .for_each(|n| n.write().unwrap().flush_state());
- }
-}
-
-impl From<&NeuralNetworkTopology> for NeuralNetwork {
- #[cfg(not(feature = "rayon"))]
- fn from(value: &NeuralNetworkTopology) -> Self {
- let input_layer = value
- .input_layer
- .iter()
- .map(|n| Rc::new(RefCell::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect::>()
- .try_into()
- .unwrap();
-
- let hidden_layers = value
- .hidden_layers
- .iter()
- .map(|n| Rc::new(RefCell::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect();
-
- let output_layer = value
- .output_layer
- .iter()
- .map(|n| Rc::new(RefCell::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect::>()
- .try_into()
- .unwrap();
-
- Self {
- input_layer,
- hidden_layers,
- output_layer,
- }
- }
-
- #[cfg(feature = "rayon")]
- fn from(value: &NeuralNetworkTopology) -> Self {
- let input_layer = value
- .input_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect::>()
- .try_into()
- .unwrap();
-
- let hidden_layers = value
- .hidden_layers
- .iter()
- .map(|n| Arc::new(RwLock::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect();
-
- let output_layer = value
- .output_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(Neuron::from(&n.read().unwrap().clone()))))
- .collect::>()
- .try_into()
- .unwrap();
-
- Self {
- input_layer,
- hidden_layers,
- output_layer,
- }
- }
-}
-
-/// A state-filled neuron.
-#[derive(Clone, Debug)]
-pub struct Neuron {
- inputs: Vec<(NeuronLocation, f32)>,
- bias: f32,
-
- /// The current state of the neuron.
- pub state: NeuronState,
-
- /// The neuron's activation function
- pub activation: ActivationFn,
-}
-
-impl Neuron {
- /// Flushes a neuron's state. Called by [`NeuralNetwork::flush_state`]
- pub fn flush_state(&mut self) {
- self.state.value = self.bias;
- }
-
- /// Applies the activation function to the neuron
- pub fn activate(&mut self) {
- self.state.value = self.activation.func.activate(self.state.value);
- }
-}
-
-impl From<&NeuronTopology> for Neuron {
- fn from(value: &NeuronTopology) -> Self {
- Self {
- inputs: value.inputs.clone(),
- bias: value.bias,
- state: NeuronState {
- value: value.bias,
- ..Default::default()
- },
- activation: value.activation.clone(),
- }
- }
-}
-
-/// A state used in [`Neuron`]s for cache.
-#[derive(Clone, Debug, Default)]
-pub struct NeuronState {
- /// The current value of the neuron. Initialized to a neuron's bias when flushed.
- pub value: f32,
-
- /// Whether or not [`value`][NeuronState::value] has finished processing.
- pub processed: bool,
-}
-
-/// A blanket trait for iterators meant to help with interpreting the output of a [`NeuralNetwork`]
-#[cfg(feature = "max-index")]
-pub trait MaxIndex {
- /// Retrieves the index of the max value.
- fn max_index(self) -> usize;
-}
-
-#[cfg(feature = "max-index")]
-impl, T: PartialOrd> MaxIndex for I {
- // slow and lazy implementation but it works (will prob optimize in the future)
- fn max_index(self) -> usize {
- self.enumerate()
- .max_by(|(_, v), (_, v2)| v.partial_cmp(v2).unwrap())
- .unwrap()
- .0
- }
-}
diff --git a/src/tests.rs b/src/tests.rs
new file mode 100644
index 0000000..825cdee
--- /dev/null
+++ b/src/tests.rs
@@ -0,0 +1,179 @@
+use crate::*;
+use rand::prelude::*;
+
+// no support for tuple structs derive in genetic-rs yet :(
+#[derive(Debug, Clone, PartialEq)]
+struct Agent(NeuralNetwork<4, 1>);
+
+impl Prunable for Agent {}
+
+impl RandomlyMutable for Agent {
+ fn mutate(&mut self, rate: f32, rng: &mut impl Rng) {
+ self.0.mutate(rate, rng);
+ }
+}
+
+impl DivisionReproduction for Agent {
+ fn divide(&self, rng: &mut impl rand::Rng) -> Self {
+ Self(self.0.divide(rng))
+ }
+}
+
+impl CrossoverReproduction for Agent {
+ fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self {
+ Self(self.0.crossover(&other.0, rng))
+ }
+}
+
+struct GuessTheNumber(f32);
+
+impl GuessTheNumber {
+ fn new(rng: &mut impl Rng) -> Self {
+ Self(rng.gen())
+ }
+
+ fn guess(&self, n: f32) -> Option {
+ if n > self.0 + 1.0e-5 {
+ return Some(1.);
+ }
+
+ if n < self.0 - 1.0e-5 {
+ return Some(-1.);
+ }
+
+ // guess was correct (or at least within margin of error).
+ None
+ }
+}
+
+fn fitness(agent: &Agent) -> f32 {
+ let mut rng = rand::thread_rng();
+
+ let mut fitness = 0.;
+
+ // 10 games for consistency
+ for _ in 0..10 {
+ let game = GuessTheNumber::new(&mut rng);
+
+ let mut last_guess = 0.;
+ let mut last_result = 0.;
+
+ let mut last_guess_2 = 0.;
+ let mut last_result_2 = 0.;
+
+ let mut steps = 0;
+ loop {
+ if steps >= 20 {
+ // took too many guesses
+ fitness -= 50.;
+ break;
+ }
+
+ let [cur_guess] =
+ agent
+ .0
+ .predict([last_guess, last_result, last_guess_2, last_result_2]);
+
+ let cur_result = game.guess(cur_guess);
+
+ if let Some(result) = cur_result {
+ last_guess = last_guess_2;
+ last_result = last_result_2;
+
+ last_guess_2 = cur_guess;
+ last_result_2 = result;
+
+ fitness -= 1.;
+ steps += 1;
+
+ continue;
+ }
+
+ fitness += 50.;
+ break;
+ }
+ }
+
+ fitness
+}
+
+#[test]
+fn division() {
+ let mut rng = rand::thread_rng();
+
+ let starting_genomes = (0..100)
+ .map(|_| Agent(NeuralNetwork::new(MutationSettings::default(), &mut rng)))
+ .collect();
+
+ let mut sim = GeneticSim::new(starting_genomes, fitness, division_pruning_nextgen);
+
+ sim.perform_generations(100);
+}
+
+#[test]
+fn crossover() {
+ let mut rng = rand::thread_rng();
+
+ let starting_genomes = (0..100)
+ .map(|_| Agent(NeuralNetwork::new(MutationSettings::default(), &mut rng)))
+ .collect();
+
+ let mut sim = GeneticSim::new(starting_genomes, fitness, crossover_pruning_nextgen);
+
+ sim.perform_generations(100);
+}
+
+#[cfg(feature = "serde")]
+#[test]
+fn serde() {
+ let mut rng = rand::thread_rng();
+ let net: NeuralNetwork<5, 10> = NeuralNetwork::new(MutationSettings::default(), &mut rng);
+
+ let text = serde_json::to_string(&net).unwrap();
+
+ let net2: NeuralNetwork<5, 10> = serde_json::from_str(&text).unwrap();
+
+ assert_eq!(net, net2);
+}
+
+#[test]
+fn neural_net_cache_sync() {
+ let cache = NeuralNetCache {
+ input_layer: [NeuronCache::new(0.3, 0), NeuronCache::new(0.25, 0)],
+ hidden_layers: vec![
+ NeuronCache::new(0.2, 2),
+ NeuronCache::new(0.0, 2),
+ NeuronCache::new(1.5, 2),
+ ],
+ output_layer: [NeuronCache::new(0.0, 3), NeuronCache::new(0.0, 3)],
+ };
+
+ for i in 0..2 {
+ let input_loc = NeuronLocation::Input(i);
+
+ assert!(cache.claim(&input_loc));
+
+ for j in 0..3 {
+ cache.add(
+ NeuronLocation::Hidden(j),
+ f32::tanh(cache.get(&input_loc) * 1.2),
+ );
+ }
+ }
+
+ for i in 0..3 {
+ let hidden_loc = NeuronLocation::Hidden(i);
+
+ assert!(cache.is_ready(&hidden_loc));
+ assert!(cache.claim(&hidden_loc));
+
+ for j in 0..2 {
+ cache.add(
+ NeuronLocation::Output(j),
+ activation::builtin::sigmoid(cache.get(&hidden_loc) * 0.7),
+ );
+ }
+ }
+
+ assert_eq!(cache.output(), [2.0688455, 2.0688455]);
+}
diff --git a/src/topology/mod.rs b/src/topology/mod.rs
deleted file mode 100644
index 02ad296..0000000
--- a/src/topology/mod.rs
+++ /dev/null
@@ -1,626 +0,0 @@
-/// Contains useful structs for serializing/deserializing a [`NeuronTopology`]
-#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
-#[cfg(feature = "serde")]
-pub mod nnt_serde;
-
-/// Contains structs and traits used for activation functions.
-pub mod activation;
-
-pub use activation::*;
-
-use std::{
- collections::HashSet,
- sync::{Arc, RwLock},
-};
-
-use genetic_rs::prelude::*;
-use rand::prelude::*;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-use crate::activation_fn;
-
-/// A stateless neural network topology.
-/// This is the struct you want to use in your agent's inheritance.
-/// See [`NeuralNetwork::from`][crate::NeuralNetwork::from] for how to convert this to a runnable neural network.
-#[derive(Debug)]
-pub struct NeuralNetworkTopology {
- /// The input layer of the neural network. Uses a fixed length of `I`.
- pub input_layer: [Arc>; I],
-
- /// The hidden layers of the neural network. Because neurons have a flexible connection system, all of them exist in the same flat vector.
- pub hidden_layers: Vec>>,
-
- /// The output layer of the neural netowrk. Uses a fixed length of `O`.
- pub output_layer: [Arc>; O],
-
- /// The mutation rate used in [`NeuralNetworkTopology::mutate`] after crossover/division.
- pub mutation_rate: f32,
-
- /// The number of mutation passes (and thus, maximum number of possible mutations that can occur for each entity in the generation).
- pub mutation_passes: usize,
-}
-
-impl NeuralNetworkTopology {
- /// Creates a new [`NeuralNetworkTopology`].
- pub fn new(mutation_rate: f32, mutation_passes: usize, rng: &mut impl Rng) -> Self {
- let input_layer: [Arc>; I] = (0..I)
- .map(|_| {
- Arc::new(RwLock::new(NeuronTopology::new_with_activation(
- vec![],
- activation_fn!(linear_activation),
- rng,
- )))
- })
- .collect::>()
- .try_into()
- .unwrap();
-
- let mut output_layer = Vec::with_capacity(O);
-
- for _ in 0..O {
- // random number of connections to random input neurons.
- let input = (0..rng.gen_range(1..=I))
- .map(|_| {
- let mut already_chosen = Vec::new();
- let mut i = rng.gen_range(0..I);
- while already_chosen.contains(&i) {
- i = rng.gen_range(0..I);
- }
-
- already_chosen.push(i);
-
- NeuronLocation::Input(i)
- })
- .collect();
-
- output_layer.push(Arc::new(RwLock::new(NeuronTopology::new_with_activation(
- input,
- activation_fn!(sigmoid),
- rng,
- ))));
- }
-
- let output_layer = output_layer.try_into().unwrap();
-
- Self {
- input_layer,
- hidden_layers: vec![],
- output_layer,
- mutation_rate,
- mutation_passes,
- }
- }
-
- /// Creates a new connection between the neurons.
- /// If the connection is cyclic, it does not add a connection and returns false.
- /// Otherwise, it returns true.
- pub fn add_connection(
- &mut self,
- from: NeuronLocation,
- to: NeuronLocation,
- weight: f32,
- ) -> bool {
- if self.is_connection_cyclic(from, to) {
- return false;
- }
-
- // Add the connection since it is not cyclic
- self.get_neuron(to)
- .write()
- .unwrap()
- .inputs
- .push((from, weight));
-
- true
- }
-
- fn is_connection_cyclic(&self, from: NeuronLocation, to: NeuronLocation) -> bool {
- if to.is_input() || from.is_output() {
- return true;
- }
-
- let mut visited = HashSet::new();
- self.dfs(from, to, &mut visited)
- }
-
- // TODO rayon implementation
- fn dfs(
- &self,
- current: NeuronLocation,
- target: NeuronLocation,
- visited: &mut HashSet,
- ) -> bool {
- if current == target {
- return true;
- }
-
- visited.insert(current);
-
- let n = self.get_neuron(current);
- let nr = n.read().unwrap();
-
- for &(input, _) in &nr.inputs {
- if !visited.contains(&input) && self.dfs(input, target, visited) {
- return true;
- }
- }
-
- visited.remove(¤t);
- false
- }
-
- /// Gets a neuron pointer from a [`NeuronLocation`].
- /// You shouldn't ever need to directly call this unless you are doing complex custom mutations.
- pub fn get_neuron(&self, loc: NeuronLocation) -> Arc> {
- match loc {
- NeuronLocation::Input(i) => self.input_layer[i].clone(),
- NeuronLocation::Hidden(i) => self.hidden_layers[i].clone(),
- NeuronLocation::Output(i) => self.output_layer[i].clone(),
- }
- }
-
- /// Gets a random neuron and its location.
- pub fn rand_neuron(&self, rng: &mut impl Rng) -> (Arc>, NeuronLocation) {
- match rng.gen_range(0..3) {
- 0 => {
- let i = rng.gen_range(0..self.input_layer.len());
- (self.input_layer[i].clone(), NeuronLocation::Input(i))
- }
- 1 if !self.hidden_layers.is_empty() => {
- let i = rng.gen_range(0..self.hidden_layers.len());
- (self.hidden_layers[i].clone(), NeuronLocation::Hidden(i))
- }
- _ => {
- let i = rng.gen_range(0..self.output_layer.len());
- (self.output_layer[i].clone(), NeuronLocation::Output(i))
- }
- }
- }
-
- fn delete_neuron(&mut self, loc: NeuronLocation) -> NeuronTopology {
- if !loc.is_hidden() {
- panic!("Invalid neuron deletion");
- }
-
- let index = loc.unwrap();
- let neuron = Arc::into_inner(self.hidden_layers.remove(index)).unwrap();
-
- for n in &self.hidden_layers {
- let mut nw = n.write().unwrap();
-
- nw.inputs = nw
- .inputs
- .iter()
- .filter_map(|&(input_loc, w)| {
- if !input_loc.is_hidden() {
- return Some((input_loc, w));
- }
-
- if input_loc.unwrap() == index {
- return None;
- }
-
- if input_loc.unwrap() > index {
- return Some((NeuronLocation::Hidden(input_loc.unwrap() - 1), w));
- }
-
- Some((input_loc, w))
- })
- .collect();
- }
-
- for n2 in &self.output_layer {
- let mut nw = n2.write().unwrap();
- nw.inputs = nw
- .inputs
- .iter()
- .filter_map(|&(input_loc, w)| {
- if !input_loc.is_hidden() {
- return Some((input_loc, w));
- }
-
- if input_loc.unwrap() == index {
- return None;
- }
-
- if input_loc.unwrap() > index {
- return Some((NeuronLocation::Hidden(input_loc.unwrap() - 1), w));
- }
-
- Some((input_loc, w))
- })
- .collect();
- }
-
- neuron.into_inner().unwrap()
- }
-}
-
-// need to do all this manually because Arcs are cringe
-impl Clone for NeuralNetworkTopology {
- fn clone(&self) -> Self {
- let input_layer = self
- .input_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone())))
- .collect::>()
- .try_into()
- .unwrap();
-
- let hidden_layers = self
- .hidden_layers
- .iter()
- .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone())))
- .collect();
-
- let output_layer = self
- .output_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone())))
- .collect::>()
- .try_into()
- .unwrap();
-
- Self {
- input_layer,
- hidden_layers,
- output_layer,
- mutation_rate: self.mutation_rate,
- mutation_passes: self.mutation_passes,
- }
- }
-}
-
-impl RandomlyMutable for NeuralNetworkTopology {
- fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) {
- for _ in 0..self.mutation_passes {
- if rng.gen::() <= rate {
- // split preexisting connection
- let (mut n2, _) = self.rand_neuron(rng);
-
- while n2.read().unwrap().inputs.is_empty() {
- (n2, _) = self.rand_neuron(rng);
- }
-
- let mut n2 = n2.write().unwrap();
- let i = rng.gen_range(0..n2.inputs.len());
- let (loc, w) = n2.inputs.remove(i);
-
- let loc3 = NeuronLocation::Hidden(self.hidden_layers.len());
-
- let n3 = NeuronTopology::new(vec![loc], ActivationScope::HIDDEN, rng);
-
- self.hidden_layers.push(Arc::new(RwLock::new(n3)));
-
- n2.inputs.insert(i, (loc3, w));
- }
-
- if rng.gen::() <= rate {
- // add a connection
- let (_, mut loc1) = self.rand_neuron(rng);
- let (_, mut loc2) = self.rand_neuron(rng);
-
- while loc1.is_output() || !self.add_connection(loc1, loc2, rng.gen::()) {
- (_, loc1) = self.rand_neuron(rng);
- (_, loc2) = self.rand_neuron(rng);
- }
- }
-
- if rng.gen::() <= rate && !self.hidden_layers.is_empty() {
- // remove a neuron
- let (_, mut loc) = self.rand_neuron(rng);
-
- while !loc.is_hidden() {
- (_, loc) = self.rand_neuron(rng);
- }
-
- // delete the neuron
- self.delete_neuron(loc);
- }
-
- if rng.gen::() <= rate {
- // mutate a connection
- let (mut n, _) = self.rand_neuron(rng);
-
- while n.read().unwrap().inputs.is_empty() {
- (n, _) = self.rand_neuron(rng);
- }
-
- let mut n = n.write().unwrap();
- let i = rng.gen_range(0..n.inputs.len());
- let (_, w) = &mut n.inputs[i];
- *w += rng.gen_range(-1.0..1.0) * rate;
- }
-
- if rng.gen::() <= rate {
- // mutate bias
- let (n, _) = self.rand_neuron(rng);
- let mut n = n.write().unwrap();
-
- n.bias += rng.gen_range(-1.0..1.0) * rate;
- }
-
- if rng.gen::() <= rate && !self.hidden_layers.is_empty() {
- // mutate activation function
- let reg = ACTIVATION_REGISTRY.read().unwrap();
- let activations = reg.activations_in_scope(ActivationScope::HIDDEN);
-
- let (mut n, mut loc) = self.rand_neuron(rng);
-
- while !loc.is_hidden() {
- (n, loc) = self.rand_neuron(rng);
- }
-
- let mut nw = n.write().unwrap();
-
- // should probably not clone, but its not a huge efficiency issue anyways
- nw.activation = activations[rng.gen_range(0..activations.len())].clone();
- }
- }
- }
-}
-
-impl DivisionReproduction for NeuralNetworkTopology {
- fn divide(&self, rng: &mut impl rand::Rng) -> Self {
- let mut child = self.clone();
- child.mutate(self.mutation_rate, rng);
- child
- }
-}
-
-impl PartialEq for NeuralNetworkTopology {
- fn eq(&self, other: &Self) -> bool {
- if self.mutation_rate != other.mutation_rate
- || self.mutation_passes != other.mutation_passes
- {
- return false;
- }
-
- for i in 0..I {
- if *self.input_layer[i].read().unwrap() != *other.input_layer[i].read().unwrap() {
- return false;
- }
- }
-
- for i in 0..self.hidden_layers.len().min(other.hidden_layers.len()) {
- if *self.hidden_layers[i].read().unwrap() != *other.hidden_layers[i].read().unwrap() {
- return false;
- }
- }
-
- for i in 0..O {
- if *self.output_layer[i].read().unwrap() != *other.output_layer[i].read().unwrap() {
- return false;
- }
- }
-
- true
- }
-}
-
-#[cfg(feature = "serde")]
-impl From>
- for NeuralNetworkTopology
-{
- fn from(value: nnt_serde::NNTSerde) -> Self {
- let input_layer = value
- .input_layer
- .into_iter()
- .map(|n| Arc::new(RwLock::new(n)))
- .collect::>()
- .try_into()
- .unwrap();
-
- let hidden_layers = value
- .hidden_layers
- .into_iter()
- .map(|n| Arc::new(RwLock::new(n)))
- .collect();
-
- let output_layer = value
- .output_layer
- .into_iter()
- .map(|n| Arc::new(RwLock::new(n)))
- .collect::>()
- .try_into()
- .unwrap();
-
- NeuralNetworkTopology {
- input_layer,
- hidden_layers,
- output_layer,
- mutation_rate: value.mutation_rate,
- mutation_passes: value.mutation_passes,
- }
- }
-}
-
-#[cfg(feature = "crossover")]
-impl CrossoverReproduction for NeuralNetworkTopology {
- fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self {
- let input_layer = self
- .input_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone())))
- .collect::>()
- .try_into()
- .unwrap();
-
- let mut hidden_layers =
- Vec::with_capacity(self.hidden_layers.len().max(other.hidden_layers.len()));
-
- for i in 0..hidden_layers.len() {
- if rng.gen::() <= 0.5 {
- if let Some(n) = self.hidden_layers.get(i) {
- let mut n = n.read().unwrap().clone();
-
- n.inputs
- .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers));
- hidden_layers[i] = Arc::new(RwLock::new(n));
-
- continue;
- }
- }
-
- let mut n = other.hidden_layers[i].read().unwrap().clone();
-
- n.inputs
- .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers));
- hidden_layers[i] = Arc::new(RwLock::new(n));
- }
-
- let mut output_layer: [Arc>; O] = self
- .output_layer
- .iter()
- .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone())))
- .collect::>()
- .try_into()
- .unwrap();
-
- for (i, n) in self.output_layer.iter().enumerate() {
- if rng.gen::() <= 0.5 {
- let mut n = n.read().unwrap().clone();
-
- n.inputs
- .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers));
- output_layer[i] = Arc::new(RwLock::new(n));
-
- continue;
- }
-
- let mut n = other.output_layer[i].read().unwrap().clone();
-
- n.inputs
- .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers));
- output_layer[i] = Arc::new(RwLock::new(n));
- }
-
- let mut child = Self {
- input_layer,
- hidden_layers,
- output_layer,
- mutation_rate: self.mutation_rate,
- mutation_passes: self.mutation_passes,
- };
-
- child.mutate(self.mutation_rate, rng);
-
- child
- }
-}
-
-#[cfg(feature = "crossover")]
-fn input_exists(
- loc: NeuronLocation,
- input: &[Arc>; I],
- hidden: &[Arc>],
-) -> bool {
- match loc {
- NeuronLocation::Input(i) => i < input.len(),
- NeuronLocation::Hidden(i) => i < hidden.len(),
- NeuronLocation::Output(_) => false,
- }
-}
-
-/// A stateless version of [`Neuron`][crate::Neuron].
-#[derive(PartialEq, Debug, Clone)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct NeuronTopology {
- /// The input locations and weights.
- pub inputs: Vec<(NeuronLocation, f32)>,
-
- /// The neuron's bias.
- pub bias: f32,
-
- /// The neuron's activation function.
- pub activation: ActivationFn,
-}
-
-impl NeuronTopology {
- /// Creates a new neuron with the given input locations.
- pub fn new(
- inputs: Vec,
- current_scope: ActivationScope,
- rng: &mut impl Rng,
- ) -> Self {
- let reg = ACTIVATION_REGISTRY.read().unwrap();
- let activations = reg.activations_in_scope(current_scope);
-
- Self::new_with_activations(inputs, activations, rng)
- }
-
- /// Takes a collection of activation functions and chooses a random one to use.
- pub fn new_with_activations(
- inputs: Vec,
- activations: impl IntoIterator
- ,
- rng: &mut impl Rng,
- ) -> Self {
- let mut activations: Vec<_> = activations.into_iter().collect();
-
- Self::new_with_activation(
- inputs,
- activations.remove(rng.gen_range(0..activations.len())),
- rng,
- )
- }
-
- /// Creates a neuron with the activation.
- pub fn new_with_activation(
- inputs: Vec,
- activation: ActivationFn,
- rng: &mut impl Rng,
- ) -> Self {
- let inputs = inputs
- .into_iter()
- .map(|i| (i, rng.gen_range(-1.0..1.0)))
- .collect();
-
- Self {
- inputs,
- bias: rng.gen(),
- activation,
- }
- }
-}
-
-/// A pseudo-pointer of sorts used to make structural conversions very fast and easy to write.
-#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub enum NeuronLocation {
- /// Points to a neuron in the input layer at contained index.
- Input(usize),
-
- /// Points to a neuron in the hidden layer at contained index.
- Hidden(usize),
-
- /// Points to a neuron in the output layer at contained index.
- Output(usize),
-}
-
-impl NeuronLocation {
- /// Returns `true` if it points to the input layer. Otherwise, returns `false`.
- pub fn is_input(&self) -> bool {
- matches!(self, Self::Input(_))
- }
-
- /// Returns `true` if it points to the hidden layer. Otherwise, returns `false`.
- pub fn is_hidden(&self) -> bool {
- matches!(self, Self::Hidden(_))
- }
-
- /// Returns `true` if it points to the output layer. Otherwise, returns `false`.
- pub fn is_output(&self) -> bool {
- matches!(self, Self::Output(_))
- }
-
- /// Retrieves the index value, regardless of layer. Does not consume.
- pub fn unwrap(&self) -> usize {
- match self {
- Self::Input(i) => *i,
- Self::Hidden(i) => *i,
- Self::Output(i) => *i,
- }
- }
-}
diff --git a/src/topology/nnt_serde.rs b/src/topology/nnt_serde.rs
deleted file mode 100644
index 14f392c..0000000
--- a/src/topology/nnt_serde.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-use super::*;
-use serde::{Deserialize, Serialize};
-use serde_big_array::BigArray;
-
-/// A serializable wrapper for [`NeuronTopology`]. See [`NNTSerde::from`] for conversion.
-#[derive(Serialize, Deserialize)]
-pub struct NNTSerde {
- #[serde(with = "BigArray")]
- pub(crate) input_layer: [NeuronTopology; I],
-
- pub(crate) hidden_layers: Vec,
-
- #[serde(with = "BigArray")]
- pub(crate) output_layer: [NeuronTopology; O],
-
- pub(crate) mutation_rate: f32,
- pub(crate) mutation_passes: usize,
-}
-
-impl From<&NeuralNetworkTopology> for NNTSerde {
- fn from(value: &NeuralNetworkTopology) -> Self {
- let input_layer = value
- .input_layer
- .iter()
- .map(|n| n.read().unwrap().clone())
- .collect::>()
- .try_into()
- .unwrap();
-
- let hidden_layers = value
- .hidden_layers
- .iter()
- .map(|n| n.read().unwrap().clone())
- .collect();
-
- let output_layer = value
- .output_layer
- .iter()
- .map(|n| n.read().unwrap().clone())
- .collect::>()
- .try_into()
- .unwrap();
-
- Self {
- input_layer,
- hidden_layers,
- output_layer,
- mutation_rate: value.mutation_rate,
- mutation_passes: value.mutation_passes,
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn serde() {
- let mut rng = rand::thread_rng();
- let nnt = NeuralNetworkTopology::<10, 10>::new(0.1, 3, &mut rng);
- let nnts = NNTSerde::from(&nnt);
-
- let encoded = bincode::serialize(&nnts).unwrap();
-
- if let Some(_) = option_env!("TEST_CREATEFILE") {
- std::fs::write("serde-test.nn", &encoded).unwrap();
- }
-
- let decoded: NNTSerde<10, 10> = bincode::deserialize(&encoded).unwrap();
- let nnt2: NeuralNetworkTopology<10, 10> = decoded.into();
-
- dbg!(nnt, nnt2);
-}