1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
//! This crate is supposed to act as the representation/reproduction aspect in neuroevolution algorithms and may be combined with arbitrary selection mechanisms. //! //! # Getting started //! Head over to [`GenomeContext`] to understand how to use this crate. //! //! # SET genome //! //! 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). //! 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. //! 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, //! which very much is a formalization and progression of the ideas NEAT introduced regarding the genome. //! The thesis describing this genome and other ideas can be found [here], a paper focusing just on the SET encoding will follow soon. //! //! [crossover]: `Genome::cross_in` //! [NEAT algorithm]: http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf //! [here]: https://www.silvan.codes/SET-NEAT_Thesis.pdf use genes::Connection; pub use genes::{activations, Id, IdGenerator}; pub use genome::Genome; pub use mutations::Mutations; pub use parameters::{Parameters, Structure}; pub use rng::GenomeRng; mod favannat_impl; mod genes; mod genome; mod mutations; mod parameters; mod rng; /// This struct simplifies operations on the [`Genome`]. /// /// The [`GenomeContext`] wraps all required building blocks to create and initialize genomes while maintaining consistent identities of their parts across operations. /// It is used in a simplified API to perform operations on genomes, as it handles all necessary moving parts for you. /// /// # Examples /// /// Creating a default genome context: /// ``` /// use set_genome::GenomeContext; /// /// let genome_context = GenomeContext::default(); /// ``` /// /// Creating the context like this is unusual as we most likely want to pass it parameters fitting our situation. /// /// Suppose we know our task has ten inputs and two outputs, which translate to the input and output layer of our ANN. /// 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. /// 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. /// /// ``` /// use set_genome::{GenomeContext, activations::Activation, Parameters, Structure}; /// /// let parameters = Parameters { /// seed: None, /// structure: Structure { /// // ten inputs /// inputs: 10, /// // 100% connected /// inputs_connected_percent: 1.0, /// // two outputs /// outputs: 2, /// // specified output activation /// outputs_activation: Activation::Tanh, /// // delta distribution /// weight_std_dev: 0.1, /// // intervall constraint, applies as [-weight_cap, weight_cap] /// weight_cap: 1.0, /// }, /// mutations: vec![], /// }; /// /// let genome_context = GenomeContext::new(parameters); /// ``` /// This allows us to ask this context for an initialized genome which conforms to our description above: /// /// ``` /// # use set_genome::{GenomeContext, activations::Activation, Parameters, Structure}; /// # /// # let parameters = Parameters { /// # seed: None, /// # structure: Structure { /// # // ten inputs /// # inputs: 10, /// # // 100% connected /// # inputs_connected_percent: 1.0, /// # // two outputs /// # outputs: 2, /// # // specified output activation /// # outputs_activation: Activation::Tanh, /// # // delta distribution /// # weight_std_dev: 0.1, /// # // intervall constraint, applies as [-weight_cap, weight_cap] /// # weight_cap: 1.0, /// # }, /// # mutations: vec![], /// # }; /// # /// # let genome_context = GenomeContext::new(parameters); /// let genome_with_connections = genome_context.initialized_genome(); /// ``` /// "Initialized" here means the configured percent of connections have been constructed with random weights. /// "Uninitialized" thereby implys no connections have been constructed, such a genome is also available: /// /// ``` /// # use set_genome::{GenomeContext, activations::Activation, Parameters, Structure}; /// # /// # let parameters = Parameters { /// # seed: None, /// # structure: Structure { /// # // ten inputs /// # inputs: 10, /// # // 100% connected /// # inputs_connected_percent: 1.0, /// # // two outputs /// # outputs: 2, /// # // specified output activation /// # outputs_activation: Activation::Tanh, /// # // delta distribution /// # weight_std_dev: 0.1, /// # // intervall constraint, applies as [-weight_cap, weight_cap] /// # weight_cap: 1.0, /// # }, /// # mutations: vec![], /// # }; /// # /// # let genome_context = GenomeContext::new(parameters); /// let genome_without_connections = genome_context.uninitialized_genome(); /// ``` /// Setting the `inputs_connected_percent` field in the [`parameters::Structure`] parameter to zero makes the /// "initialized" and "uninitialized" genome look the same. /// /// So we got ourselves a genome, let's mutate it: [`Genome::mutate_with_context`]. /// /// The possible mutations: /// /// - [`Mutations::add_connection`] /// - [`Mutations::add_node`] /// - [`Mutations::add_recurrent_connection`] /// - [`Mutations::change_activation`] /// - [`Mutations::change_weights`] /// - [`Mutations::remove_node`] /// /// To evaluate the function encoded in the genome check [this crate]. /// /// [thesis]: https://www.silvan.codes/SET-NEAT_Thesis.pdf /// [this crate]: https://crates.io/crates/favannat /// pub struct GenomeContext { pub id_gen: IdGenerator, pub rng: GenomeRng, pub parameters: Parameters, initialized_genome: Genome, uninitialized_genome: Genome, } impl GenomeContext { /// Returns a new `GenomeContext` from the parameters. pub fn new(parameters: Parameters) -> Self { let mut id_gen = IdGenerator::default(); let mut rng = GenomeRng::new( parameters.seed.unwrap_or(42), parameters.structure.weight_std_dev, parameters.structure.weight_cap, ); let uninitialized_genome = Genome::new(&mut id_gen, ¶meters.structure); let mut initialized_genome = uninitialized_genome.clone(); initialized_genome.init(&mut rng, ¶meters.structure); Self { id_gen, rng, parameters, initialized_genome, uninitialized_genome, } } /// Returns an initialized genome, see [`Genome::init_with_context`]. pub fn initialized_genome(&self) -> Genome { self.initialized_genome.clone() } /// Returns an uninitialized genome, see [`Genome::init_with_context`]. pub fn uninitialized_genome(&self) -> Genome { self.uninitialized_genome.clone() } } impl Default for GenomeContext { fn default() -> Self { Self::new(Parameters::default()) } } impl Genome { /// Initialization connects the configured percent of inputs nodes to output nodes, i.e. it creates connection genes with random weights. pub fn init_with_context(&mut self, context: &mut GenomeContext) { for input in self .inputs .iterate_with_random_offset(&mut context.rng) .take( (context.parameters.structure.inputs_connected_percent * context.parameters.structure.inputs as f64) .ceil() as usize, ) { // connect to every output for output in self.outputs.iter() { assert!(self.feed_forward.insert(Connection::new( input.id, context.rng.weight_perturbation(0.0), output.id ))); } } } /// Apply all mutations listed in the [parameters of the context] with respect to their chance of happening. /// /// This will probably be the most common way to apply mutations to a genome. /// /// # Examples /// /// ``` /// use set_genome::GenomeContext; /// /// // Create a `GenomeContext`. /// let mut genome_context = GenomeContext::default(); /// /// // Create an initialized `Genome`. /// let mut genome = genome_context.initialized_genome(); /// /// // Randomly mutate the genome according to the available mutations listed in the parameters of the context and their corresponding chances . /// genome.mutate_with_context(&mut genome_context); /// ``` /// /// [parameters of the context]: `Parameters` /// pub fn mutate_with_context(&mut self, context: &mut GenomeContext) { for mutation in &context.parameters.mutations { mutation.mutate(self, &mut context.rng, &mut context.id_gen); } } /// Calls [`Mutations::add_node`] with `self`, should [`Mutations::AddNode`] be listed in the context. /// It needs to be listed as it provides parameters. pub fn add_node_with_context(&mut self, context: &mut GenomeContext) { for mutation in &context.parameters.mutations { if let Mutations::AddNode { activation_pool, .. } = mutation { Mutations::add_node(activation_pool, self, &mut context.rng, &mut context.id_gen) } } } /// Calls the [`Mutations::remove_node`] with `self`. pub fn remove_node_with_context( &mut self, context: &mut GenomeContext, ) -> Result<(), &'static str> { Mutations::remove_node(self, &mut context.rng) } /// Calls the [`Mutations::add_connection`] with `self`. pub fn add_connection_with_context( &mut self, context: &mut GenomeContext, ) -> Result<(), &'static str> { Mutations::add_connection(self, &mut context.rng) } /// Calls the [`Mutations::add_recurrent_connection`] with `self`. pub fn add_recurrent_connection_with_context( &mut self, context: &mut GenomeContext, ) -> Result<(), &'static str> { Mutations::add_recurrent_connection(self, &mut context.rng) } /// Calls [`Mutations::change_activation`] with `self`, should [`Mutations::ChangeActivation`] be listed in the context. /// It needs to be listed as it provides parameters. pub fn change_activation_with_context(&mut self, context: &mut GenomeContext) { for mutation in &context.parameters.mutations { if let Mutations::ChangeActivation { activation_pool, .. } = mutation { Mutations::change_activation(activation_pool, self, &mut context.rng) } } } /// Calls [`Mutations::change_weights`] with `self`, should [`Mutations::ChangeWeights`] be listed in the context. /// It needs to be listed as it provides parameters. pub fn change_weights_with_context(&mut self, context: &mut GenomeContext) { for mutation in &context.parameters.mutations { if let Mutations::ChangeWeights { percent_perturbed, .. } = *mutation { Mutations::change_weights(percent_perturbed, self, &mut context.rng) } } } }