set_genome/
lib.rs

1//! This crate is supposed to act as the representation/reproduction aspect in neuroevolution algorithms and may be combined with arbitrary selection mechanisms.
2//!
3//! # What you can do with this crate
4//! ```
5//! # use set_genome::{Genome, Parameters};
6//! # use favannat::{
7//! #   MatrixFeedforwardFabricator, Evaluator, Fabricator,
8//! # };
9//! # use nalgebra::dmatrix;
10//! // Setup a genome context for networks with 10 inputs and 10 outputs.
11//! let parameters = Parameters::basic(10, 10);
12//!
13//! // Initialize a genome.
14//! let mut genome = Genome::initialized(&parameters);
15//!
16//! // Mutate a genome.
17//! genome.mutate(&parameters);
18//!
19//! // Get a phenotype of the genome.
20//! let network = MatrixFeedforwardFabricator::fabricate(&genome).expect("Cool network.");
21//!
22//! // Evaluate a network on an input.
23//! let output = network.evaluate(dmatrix![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
24//! ```
25//!
26//! # SET genome
27//!
28//! SET stands for **S**et **E**ncoded **T**opology and this crate implements a genetic data structure, the [`Genome`], using this set encoding to describe artificial neural networks (ANNs).
29//! Further this crate defines operations on this genome, namely [`Mutations`] and [crossover]. Mutations alter a genome by adding or removing genes, crossover recombines two genomes.
30//! To have an intuitive definition of crossover for network structures the [NEAT algorithm] defined a procedure and has to be understood as a mental predecessor to this SET encoding,
31//! which very much is a formalization and progression of the ideas NEAT introduced regarding the genome.
32//! The thesis describing this genome and other ideas can be found [here], a paper focusing just on the SET encoding will follow soon.
33//!
34//! # Getting started
35//!
36//! We start by defining our parameters:
37//!
38//! Suppose we know our task has ten inputs and two outputs, which translate to the input and output layer of our ANN.
39//! Further we want 100% of our inputs nodes to be initially connected to the outputs and the outputs shall use the [`activations::Activation::Tanh`] function.
40//! Also the weights of our connections are supposed to be capped between \[-1, 1\] and change by deltas sampled from a normal distribution with 0.1 standard deviation.
41//!
42//! ```
43//! use set_genome::{activations::Activation, Parameters, Structure};
44//!
45//! let parameters = Parameters {
46//!     structure: Structure {
47//!         // ten inputs
48//!         number_of_inputs: 10,
49//!         // two outputs
50//!         number_of_outputs: 2,
51//!         // 100% connected
52//!         percent_of_connected_inputs: 1.0,
53//!         // specified output activation
54//!         outputs_activation: Activation::Tanh,
55//!         // seed for initial genome construction
56//!         seed: 42
57//!     },
58//!     mutations: vec![],
59//! };
60//! ```
61//! This allows us to create an initialized genome which conforms to our description above:
62//!
63//! ```
64//! # use set_genome::{Genome, activations::Activation, Parameters, Structure};
65//! #
66//! # let parameters = Parameters {
67//! #     structure: Structure {
68//! #         // ten inputs
69//! #         number_of_inputs: 10,
70//! #         // two outputs
71//! #         number_of_outputs: 2,
72//! #         // 100% connected
73//! #         percent_of_connected_inputs: 1.0,
74//! #         // specified output activation
75//! #         outputs_activation: Activation::Tanh,
76//!           // seed for initial genome construction
77//!           seed: 42
78//! #     },
79//! #     mutations: vec![],
80//! # };
81//! #
82//! let genome_with_connections = Genome::initialized(&parameters);
83//! ```
84//! "Initialized" here means the configured percent of connections have been constructed with random weights.
85//! "Uninitialized" thereby implys no connections have been constructed, such a genome is also available:
86//!
87//! ```
88//! # use set_genome::{Genome, activations::Activation, Parameters, Structure};
89//! #
90//! # let parameters = Parameters {
91//! #     structure: Structure {
92//! #         // ten inputs
93//! #         number_of_inputs: 10,
94//! #         // two outputs
95//! #         number_of_outputs: 2,
96//! #         // 100% connected
97//! #         percent_of_connected_inputs: 1.0,
98//! #         // specified output activation
99//! #         outputs_activation: Activation::Tanh,
100//!           // seed for initial genome construction
101//!           seed: 42
102//!
103//! #     },
104//! #     mutations: vec![],
105//! # };
106//! #
107//! let genome_without_connections = Genome::uninitialized(&parameters);
108//! ```
109//! Setting the `percent_of_connected_inputs` field in the [`parameters::Structure`] parameter to zero makes the
110//! "initialized" and "uninitialized" genome look the same.
111//!
112//! So we got ourselves a genome, let's mutate it: [`Genome::mutate`].
113//!
114//! The possible mutations:
115//!
116//! - [`Mutations::add_connection`]
117//! - [`Mutations::add_node`]
118//! - [`Mutations::add_recurrent_connection`]
119//! - [`Mutations::change_activation`]
120//! - [`Mutations::change_weights`]
121//! - [`Mutations::remove_node`]
122//! - [`Mutations::remove_connection`]
123//! - [`Mutations::remove_recurrent_connection`]
124//!
125//! //! # Features
126//!
127//! This crate exposes the 'favannat' feature. [favannat] is a library to translate the genome into an executable form and also to execute it.
128//! It can be seen as a phenotype of the genome.
129//! The feature is enabled by default as probably you want to evaluate your evolved genomes, but disabling it is as easy as this:
130//!
131//! ```toml
132//! [dependencies]
133//! set-genome = { version = "x.x.x", default-features = false }
134//! ```
135//!
136//! If you are interested how they connect, [see here].
137//! favannat can be used to evaluate other data structures of yours, too, if they are [`favannat::network::NetworkLike`]. ;)
138//!
139//! [thesis]: https://www.silvan.codes/SET-NEAT_Thesis.pdf
140//! [this crate]: https://crates.io/crates/favannat
141//! [crossover]: `Genome::cross_in`
142//! [NEAT algorithm]: http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf
143//! [here]: https://www.silvan.codes/SET-NEAT_Thesis.pdf
144//! [favannat]: https://docs.rs/favannat
145//! [see here]: https://github.com/SilvanCodes/set-genome/blob/main/src/favannat_impl.rs
146
147pub use genes::{activations, Connection, Id, Node};
148pub use genome::{CompatibilityDistance, Genome};
149pub use mutations::{MutationError, MutationResult, Mutations};
150pub use parameters::{Parameters, Structure};
151use rand::{rngs::SmallRng, thread_rng, SeedableRng};
152
153#[cfg(feature = "favannat")]
154mod favannat_impl;
155mod genes;
156mod genome;
157mod mutations;
158mod parameters;
159
160impl Genome {
161    /// Initialization connects the configured percent of inputs nodes to output nodes, i.e. it creates connection genes with random weights.
162    pub fn uninitialized(parameters: &Parameters) -> Self {
163        Self::new(&parameters.structure)
164    }
165
166    pub fn initialized(parameters: &Parameters) -> Self {
167        let mut genome = Genome::new(&parameters.structure);
168        genome.init(&parameters.structure);
169        genome
170    }
171
172    /// Apply all mutations listed in the [`Parameters`] with respect to their chance of happening.
173    /// If a mutation is listed multiple times it is applied multiple times.
174    ///
175    /// This will probably be the most common way to apply mutations to a genome.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use set_genome::{Genome, Parameters};
181    ///
182    /// // Create parameters, usually read from a configuration file.
183    /// let parameters = Parameters::default();
184    ///
185    /// // Create an initialized `Genome`.
186    /// let mut genome = Genome::initialized(&parameters);
187    ///
188    /// // Randomly mutate the genome according to the available mutations listed in the parameters of the context and their corresponding chances .
189    /// genome.mutate(&parameters);
190    /// ```
191    ///
192    pub fn mutate(&mut self, parameters: &Parameters) -> MutationResult {
193        let rng = &mut SmallRng::from_rng(thread_rng()).unwrap();
194
195        for mutation in &parameters.mutations {
196            // gamble for application of mutation right here instead of in mutate() ??
197            mutation.mutate(self, rng)?
198        }
199        Ok(())
200    }
201}