Crate perestroika

source ·
Expand description

Perestroika - A library for simulating evolution.

This crate aims to help the users to create “Brains”, or in Perestroika’s terminology, Genomes. Genomes are essentially self contained decision making machines - given an input, they will propagate it, and return a decision.

Genomes consist of NodeGenes split between (or aggregated into) different Layers. In order to be able to propagate the inputs the NodeGenes must be connected via ConnectionGenes. As in a real brain, not all NodeGenes must be connected to each other or the next layer.

Genomes also have the ability to mutate. If NodeGenes and ConnectionGenes are the building blocks of the brain, then mutation is the engine of evolution.

Since the crate relies heavily on randomness, in many examples a specific seed is used for the sake of reproducibility, this is an intended use case of the crate as well.

§Quick start

§Minimal

To generate the smallest useful Genome with an input and an output Layers and a ConnectionGene:

use perestroika::errors::PerestroikaError;
use perestroika::genome::Genome;
use perestroika::genome_builder::GenomeBuilder;
use perestroika::node_gene::DepthType;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;

// This genome uses ChaCha8Rng as the random number generator.
// Let's also set a seed for reproducibility:
let rng: ChaCha8Rng = ChaCha8Rng::seed_from_u64(42);

// It is a very simple genome, consisting of a single input NodeGene a single output NodeGene
// and a ConnectionGene which connects them.
let mut genome: Genome = GenomeBuilder::new()
    .with_shape(&[1, 1])?
    .with_fully_connected_layers(&[[DepthType::Input, DepthType::Output]], true)?
    .build()?;

// NodeGenes by default use the Identity activation function and have a bias of 0.
// This means, they pass the input onward to the next one, without changing it.
let output = genome.propagate(&[0.42])?;
assert_ne!(output, &[0.0]);

§More practical

Generating a more complex Genome and propagating through it.

 use rand::Rng;
 use rand::SeedableRng;
 use rand_chacha::ChaCha8Rng;

 use perestroika::errors::PerestroikaError;
 use perestroika::genome::Genome;
 use perestroika::genome_builder::GenomeBuilder;

 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1234567);

 let mut genome: Genome = GenomeBuilder::new()
     .with_shape(&vec![8, 6, 4, 2, 4, 6, 4])?
     .with_cnn_structure()?
     .build()?;

 // Let's create an input vector:
 let input_vector: Vec<f64> = vec![0.1, 0.2, 0.3, 0.31428, 0.424242, 0.999, -0.17, 0.5];
 // Be sure that the input vector and the InputLayer have matching dimensions.

 // Propagate it through the genome to get the output:
 let output: Vec<f64> = genome.propagate(&input_vector)?;

 // Output: [1.0, 0.20511543379524064, -1.0, 0.15121278824891932].

§Using different random number generators

It is possible to use other random number generators, as long as they impl Rng +?Sized + Clone. A possible place to use the generator is in the mutate_randomly method.

 use rand::rngs::StdRng;
 use rand::Rng;
 use rand::SeedableRng;
 use rand_chacha::ChaCha8Rng;

 use perestroika::errors::PerestroikaError;
 use perestroika::genome::Genome;
 use perestroika::genome_builder::GenomeBuilder;

 let mut std_rng = StdRng::seed_from_u64(42);
 let mut chacha8_rng = rand_chacha::ChaCha8Rng::seed_from_u64(17);

 let mut genome: Genome = GenomeBuilder::new()
     .with_shape(&vec![4, 16, 4])?
     .with_cnn_structure()?
     .build()?;

 // Mutate the Genome.
 genome.mutate_randomly(&mut std_rng)?;
 genome.mutate_randomly(&mut chacha8_rng)?;

Re-exports§

Modules§