Debugging crossbreeding logic

This commit is contained in:
vandomej 2024-03-31 15:53:47 -07:00
parent 1301d457a9
commit 5670649227
3 changed files with 393 additions and 39 deletions

18
.vscode/launch.json vendored
View file

@ -10,7 +10,23 @@
"name": "Debug",
"program": "${workspaceFolder}/gemla/target/debug/gemla.exe",
"args": ["./gemla/temp/"],
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder}/gemla"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug Rust Tests",
"cargo": {
"args": [
"test",
"--manifest-path", "${workspaceFolder}/gemla/Cargo.toml",
"--no-run", // Compiles the tests without running them
"--package=gemla", // Specify your package name if necessary
"--bin=bin"
],
"filter": { }
},
"args": [],
}
]
}

View file

@ -1,9 +1,9 @@
fn main() {
// Replace this with the path to the directory containing `fann.lib`
let lib_dir = "F://vandomej/Downloads/vcpkg/packages/fann_x64-windows/lib";
let lib_dir = "/opt/homebrew/Cellar/fann/2.2.0/lib";
println!("cargo:rustc-link-search=native={}", lib_dir);
println!("cargo:rustc-link-lib=static=fann");
println!("cargo:rustc-link-lib=dylib=fann");
// Use `dylib=fann` instead of `static=fann` if you're linking dynamically
// If there are any additional directories where the compiler can find header files, you can specify them like this:

View file

@ -1,6 +1,6 @@
extern crate fann;
use std::{fs::{self, File}, io::{self, BufRead, BufReader}, path::{Path, PathBuf}, sync::Arc};
use std::{cmp::min, fs::{self, File}, io::{self, BufRead, BufReader}, path::{Path, PathBuf}, sync::Arc};
use fann::{ActivationFunc, Fann};
use futures::future::join_all;
use gemla::{core::genetic_node::{GeneticNode, GeneticNodeContext}, error::Error};
@ -24,7 +24,7 @@ const NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MIN: usize = 3;
const NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX: usize = 35;
const NEURAL_NETWORK_INITIAL_WEIGHT_MIN: f32 = -2.0;
const NEURAL_NETWORK_INITIAL_WEIGHT_MAX: f32 = 2.0;
const NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN: usize = 1;
const NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN: usize = 2;
const NEURAL_NETWORK_CROSSBREED_SEGMENTS_MAX: usize = 20;
const SIMULATION_ROUNDS: usize = 5;
@ -70,7 +70,7 @@ impl GeneticNode for FighterNN {
fs::create_dir_all(&gen_folder)
.with_context(|| format!("Failed to create or access the generation folder: {:?}", gen_folder))?;
let nn_shapes = HashMap::new();
let mut nn_shapes = HashMap::new();
// Create the first generation in this folder
for i in 0..POPULATION {
@ -95,6 +95,11 @@ impl GeneticNode for FighterNN {
fann.save(&nn)
.with_context(|| format!("Failed to save nn at {:?}", nn))?;
}
let mut crossbreed_segments = thread_rng().gen_range(NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN..NEURAL_NETWORK_CROSSBREED_SEGMENTS_MAX);
if crossbreed_segments % 2 != 0 {
crossbreed_segments += 1;
}
Ok(Box::new(FighterNN {
id: context.id,
@ -103,7 +108,8 @@ impl GeneticNode for FighterNN {
generation: 0,
scores: vec![HashMap::new()],
nn_shapes,
crossbreed_segments: thread_rng().gen_range(NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN..NEURAL_NETWORK_CROSSBREED_SEGMENTS_MAX)
// we need crossbreed segments to be even
crossbreed_segments,
}))
}
@ -266,45 +272,16 @@ impl GeneticNode for FighterNN {
let fann = Fann::from_file(&nn)
.with_context(|| format!("Failed to load nn"))?;
let new_fann = fann.try_clone();
// Load another nn from the current generation and cross breed it with the current nn
let cross_nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, to_keep[thread_rng().gen_range(0..survivor_count)]));
let cross_fann = Fann::from_file(&cross_nn)
.with_context(|| format!("Failed to load cross nn"))?;
let mut connections = fann.get_connections(); // Vector of connections
let cross_connections = cross_fann.get_connections(); // Vector of connections
let segment_count: usize = 3; // For example, choose 3 segments to swap
let segment_distribution = Uniform::from(1..connections.len() / segment_count); // Ensure segments are not too small
let mut start_points = vec![];
for _ in 0..segment_count {
let start_point = segment_distribution.sample(&mut rand::thread_rng());
start_points.push(start_point);
}
start_points.sort_unstable(); // Ensure segments are in order
trace!("Crossbreeding Start points: {:?}", start_points);
for (j, &start) in start_points.iter().enumerate() {
let end = if j < segment_count - 1 {
start_points[j + 1]
} else {
connections.len()
};
// Swap segments
for k in start..end {
connections[k] = cross_connections[k].clone();
}
}
fann.set_connections(&connections);
let mut new_fann = crossbreed(&fann, &cross_fann, self.crossbreed_segments)?;
// For each weight in the 5 new nn's there is a 20% chance of a minor mutation (a random number between -0.1 and 0.1 is added to the weight)
// And a 5% chance of a major mutation (a random number between -0.3 and 0.3 is added to the weight)
let mut connections = fann.get_connections(); // Vector of connections
let mut connections = new_fann.get_connections(); // Vector of connections
for c in &mut connections {
if thread_rng().gen_range(0..100) < 20 {
c.weight += thread_rng().gen_range(-0.1..0.1);
@ -313,7 +290,7 @@ impl GeneticNode for FighterNN {
c.weight += thread_rng().gen_range(-0.3..0.3);
}
}
fann.set_connections(&connections);
new_fann.set_connections(&connections);
// Save the new nn's to the new generation folder
let new_nn = new_gen_folder.join(format!("{:06}_fighter_nn_{}.net", self.id, i + survivor_count));
@ -360,10 +337,280 @@ impl GeneticNode for FighterNN {
generation: 0,
population_size: left.population_size, // Assuming left and right have the same population size.
scores: vec![HashMap::new()],
crossbreed_segments: left.crossbreed_segments,
nn_shapes: HashMap::new(),
}))
}
}
/// Crossbreeds two neural networks of different shapes by finding cut points, and swapping neurons between the two networks.
/// Algorithm tries to ensure similar functionality is maintained between the two networks.
/// It does this by preserving connections between the same neurons from the original to the new network, and if a connection cannot be found
/// it will create a new connection with a random weight.
fn crossbreed(primary: &Fann, secondary: &Fann, crossbreed_segments: usize) -> Result<Fann, Error> {
// First we need to get the shape of the networks and transform this into a format that is easier to work with
// We want a list of every neuron id, and the layer it is in
let primary_shape = primary.get_layer_sizes();
let secondary_shape = secondary.get_layer_sizes();
let primary_neurons = generate_neuron_datastructure(&primary_shape);
let secondary_neurons = generate_neuron_datastructure(&secondary_shape);
// Now we need to find the cut points for the crossbreed
let start = primary_shape[0] + 1; // Start at the first hidden layer
let end = min(primary_shape.iter().sum::<u32>() - primary_shape.last().unwrap(), secondary_shape.iter().sum::<u32>() - secondary_shape.last().unwrap()); // End at the last hidden layer
let segment_distribution = Uniform::from(start..end); // Ensure segments are not too small
let mut cut_points = Vec::new();
for _ in 0..crossbreed_segments {
let cut_point = segment_distribution.sample(&mut thread_rng());
if !cut_points.contains(&cut_point) {
cut_points.push(cut_point);
}
}
// Sort the cut points to make it easier to iterate over them
cut_points.sort_unstable();
// We need to transform the cut_points vector to a vector of tuples that contain the start and end of each segment
let mut segments = Vec::new();
let mut previous = 0;
for &cut_point in cut_points.iter() {
segments.push((previous, cut_point));
previous = cut_point;
}
let new_neurons = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons);
// Now we need to create the new network with the shape we've determined
let mut new_shape = vec![];
for (_, _, layer, _) in new_neurons.iter() {
// Check if new_shape has an entry for layer in it
if new_shape.len() <= *layer as usize {
new_shape.push(1);
}
else {
new_shape[*layer as usize] += 1;
}
}
let mut new_fann = Fann::new(new_shape.as_slice())
.with_context(|| "Failed to create new fann")?;
// We need to randomize the weights to a small value
new_fann.randomize_weights(-0.1, 0.1);
new_fann.set_activation_func_hidden(ActivationFunc::SigmoidSymmetric);
new_fann.set_activation_func_output(ActivationFunc::SigmoidSymmetric);
consolidate_old_connections(primary, secondary, new_shape, new_neurons, &mut new_fann);
Ok(new_fann)
}
fn consolidate_old_connections(primary: &Fann, secondary: &Fann, new_shape: Vec<u32>, new_neurons: Vec<(u32, bool, usize, u32)>, new_fann: &mut Fann) {
// Now we need to copy the connections from the original networks to the new network
// We can do this by referencing our connections array, it will contain the original id's of the neurons
// and their new id as well as their layer. We can iterate one layer at a time and copy the connections
// Start by iterating layer by later
let primary_connections = primary.get_connections();
let secondary_connections = secondary.get_connections();
for layer in 1..new_shape.len() {
// filter out the connections that are in the current layer and previous layer
let current_layer_connections = new_neurons.iter().filter(|(_, _, l, _)| l == &layer).collect::<Vec<_>>();
let previous_layer_connections = new_neurons.iter().filter(|(_, _, l, _)| l == &(layer - 1)).collect::<Vec<_>>();
// Now we need to iterate over the connections in the current layer
for (neuron_id, is_primary, _, new_id) in current_layer_connections.iter() {
// We need to find the connections from the previous layer to this neuron
for (previous_neuron_id, _, _, previous_new_id) in previous_layer_connections.iter() {
// First we use primary to and check the correct connections array to see if the connection exists
// If it does, we add it to the new network
let connection = if *is_primary {
primary_connections.iter().find(|connection| &connection.from_neuron == previous_neuron_id && &connection.to_neuron == neuron_id)
}
else {
secondary_connections.iter().find(|connection| &connection.from_neuron == previous_neuron_id && &connection.to_neuron == neuron_id)
};
// If the connection exists, we need to add it to the new network
if let Some(connection) = connection {
new_fann.set_weight(*previous_new_id, *new_id, connection.weight);
}
}
}
}
}
fn crossbreed_neuron_arrays(segments: Vec<(u32, u32)>, primary_neurons: Vec<(u32, usize)>, secondary_neurons: Vec<(u32, usize)>) -> Vec<(u32, bool, usize, u32)> {
// We now need to determine the resulting location of the neurons in the new network.
// To do this we need a new structure that keeps track of the following information:
// - The neuron id from the original network
// - Which network it originated from (primary or secondary)
// - The layer the neuron is in
// - The resulting neuron id in the new network which will be calculated after the fact
let mut new_neurons = Vec::new();
let mut current_layer = 0;
let mut is_primary = true;
for (i, &segment) in segments.iter().enumerate() {
// If it's the first slice, copy neurons from the primary network up to the cut_point
if i == 0 {
for (neuron_id, layer) in primary_neurons.iter() {
if neuron_id <= &segment.1 {
if layer > &current_layer {
current_layer += 1;
}
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
else {
break;
}
}
}
else {
let target_neurons = if is_primary { &primary_neurons } else { &secondary_neurons };
for (neuron_id, layer) in target_neurons.iter() {
// Iterate until neuron_id equals the cut_point
if neuron_id >= &segment.0 && neuron_id <= &segment.1 {
// We need to do something different depending on whether the neuron layer is, lower, higher or equal to the target layer
// Equal
if layer == &current_layer {
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
// Earlier
else if layer < &current_layer {
// If it's in an earlier layer, add it to the earlier layer
// Check if there's a lower id from the same individual in that earlier layer
// As long as there isn't a neuron from the other individual in between the lower id and current id, add the id values from the same individual
let earlier_layer_neurons = new_neurons.iter().filter(|(_, _, l, _)| l == layer).collect::<Vec<_>>();
// get max id from that layer
let highest_id = earlier_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
if let Some(highest_id) = highest_id {
if highest_id.1 == is_primary {
let neurons_to_add = target_neurons.iter().filter(|(id, l)| id > &highest_id.0 && id <= neuron_id && l == layer).collect::<Vec<_>>();
for (neuron_id, _) in neurons_to_add {
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
}
}
}
// Later
else if layer > &current_layer {
// If the highest id in the current layer is from the same individual, add anything with a higher id to the current layer before moving to the next layer
// First filter new_neurons to look at neurons from the current layer
let current_layer_neurons = new_neurons.iter().filter(|(_, _, l, _)| l == &current_layer).collect::<Vec<_>>();
let highest_id = current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
if let Some(highest_id) = highest_id {
if highest_id.1 == is_primary {
let neurons_to_add = target_neurons.iter().filter(|(id, l)| id > &highest_id.0 && *l == layer - 1).collect::<Vec<_>>();
for (neuron_id, _) in neurons_to_add {
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
}
}
// If it's in a future layer, move to the next layer
current_layer += 1;
// Add the neuron to the new network
// Along with any neurons that have a lower id in the future layer
let neurons_to_add = target_neurons.iter().filter(|(id, l)| id <= &neuron_id && l == layer).collect::<Vec<_>>();
for (neuron_id, _) in neurons_to_add {
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
}
}
else if neuron_id >= &segment.1 {
break;
}
}
}
// Switch to the other network
is_primary = !is_primary;
}
// For the last segment, copy the remaining neurons
let target_neurons = if is_primary { &primary_neurons } else { &secondary_neurons };
// Get output layer number
let output_layer = target_neurons.iter().max_by_key(|(_, l)| l).unwrap().1;
// For the last segment, copy the remaining neurons from the target network
// But when we reach the output layer, we need to add a new layer to the end of new_neurons regardless of it's length
// and copy the output neurons to that layer
for (neuron_id, layer) in target_neurons.iter() {
if neuron_id > &segments.last().unwrap().1 {
if layer == &output_layer {
// Calculate which layer the neurons should be in
current_layer = new_neurons.iter().max_by_key(|(_, _, l, _)| l).unwrap().2 + 1;
for (neuron_id, _) in target_neurons.iter().filter(|(_, l)| l == &output_layer) {
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
}
break;
}
else if *neuron_id == &segments.last().unwrap().1 + 1 {
let earlier_layer_neurons = new_neurons.iter().filter(|(_, _, l, _)| l == layer).collect::<Vec<_>>();
// get max id from that layer
let highest_id = earlier_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
if let Some(highest_id) = highest_id {
if highest_id.1 == is_primary {
let neurons_to_add = target_neurons.iter().filter(|(id, l)| id > &highest_id.0 && id <= neuron_id && l == layer).collect::<Vec<_>>();
for (neuron_id, _) in neurons_to_add {
new_neurons.push((*neuron_id, is_primary, *layer, 0));
}
}
}
}
else {
new_neurons.push((*neuron_id, is_primary, *layer, 0));
}
}
}
// Filtering layers with too few neurons, if necessary
let layer_counts = new_neurons.iter().fold(vec![0; current_layer + 1], |mut counts, &(_, _, layer, _)| {
counts[layer] += 1;
counts
});
// Filter out layers based on the minimum number of neurons per layer
new_neurons = new_neurons.into_iter()
.filter(|&(_, _, layer, _)| layer_counts[layer] >= NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MIN)
.collect::<Vec<_>>();
// Collect and sort unique layer numbers
let mut unique_layers = new_neurons.iter()
.map(|(_, _, layer, _)| *layer)
.collect::<Vec<_>>();
unique_layers.sort();
unique_layers.dedup(); // Removes duplicates, keeping only unique layer numbers
// Create a mapping from old layer numbers to new (gap-less) layer numbers
let layer_mapping = unique_layers.iter().enumerate()
.map(|(new_layer, &old_layer)| (old_layer, new_layer))
.collect::<HashMap<usize, usize>>();
// Apply the mapping to renumber layers in new_neurons
new_neurons.iter_mut().for_each(|(_, _, layer, _)| {
*layer = *layer_mapping.get(layer).unwrap_or(layer); // Fallback to original layer if not found, though it should always find a match
});
// Assign new IDs
// new_neurons must be sorted by layer, then by neuron ID within the layer
new_neurons.sort_unstable_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
new_neurons.iter_mut().enumerate().for_each(|(new_id, neuron)| {
neuron.3 = new_id as u32;
});
new_neurons
}
fn generate_neuron_datastructure(shape: &[u32]) -> Vec<(u32, usize)> {
shape.iter().enumerate().flat_map(|(i, &size)| {
(0..size).enumerate().map(move |(j, _)| (j as u32, i))
}).collect()
}
async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error> {
let mut attempts = 0;
@ -399,4 +646,95 @@ async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::
Err(e) => return Err(e),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn crossbreed_neuron_arrays_test() {
// Assign
let segments = vec![(0, 3), (4, 6), (7, 8), (9, 10)];
let primary_network = vec![
// Input layer
(0, 0), (1, 0), (2, 0), (3, 0),
// Hidden layer 1
(4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1), (10, 1), (11, 1),
// Hidden layer 2
(12, 2), (13, 2), (14, 2), (15, 2), (16, 2), (17, 2),
// Output layer
(18, 3), (19, 3), (20, 3), (21, 3),
];
let secondary_network = vec![
// Input layer
(0, 0), (1, 0), (2, 0), (3, 0),
// Hidden layer 1
(4, 1), (5, 1), (6, 1),
// Hidden layer 2
(7, 2), (8, 2), (9, 2),
// Hiden Layer 3
(10, 3), (11, 3), (12, 3),
// Hidden Layer 4
(13, 4), (14, 4), (15, 4),
// Hiden Layer 5
(16, 5), (17, 5), (18, 5),
// Output layer
(19, 6), (20, 6), (21, 6), (22, 6),
];
// Act
let result = crossbreed_neuron_arrays(segments.clone(), primary_network.clone(), secondary_network.clone());
// Expected Result Set
let expected: HashSet<(u32, bool, usize, u32)> = vec![
// Input layer: Expect 4
(0, true, 0, 0), (1, true, 0, 1), (2, true, 0, 2), (3, true, 0, 3),
// Hidden Layer 1: Expect 8
(4, false, 1, 4), (5, false, 1, 5), (6, false, 1, 6), (7, true, 1, 7), (8, true, 1, 8), (9, true, 1, 9), (10, true, 1, 10), (11, true, 1, 11),
// Hidden Layer 2: Expect 9
(7, false, 2, 12), (8, false, 2, 13), (9, false, 2, 14), (12, true, 2, 15), (13, true, 2, 16), (14, true, 2, 17), (15, true, 2, 18), (16, true, 2, 19), (17, true, 2, 20),
// Output Layer: Expect 4
(18, true, 3, 21), (19, true, 3, 22), (20, true, 3, 23), (21, true, 3, 24),
].into_iter().collect();
// Convert Result to HashSet for Comparison
let result_set: HashSet<(u32, bool, usize, u32)> = result.into_iter().collect();
// Assert
assert_eq!(result_set, expected);
// Now we test the ooposite case
// Act
let result = crossbreed_neuron_arrays(segments.clone(), secondary_network.clone(), primary_network.clone());
// Expected Result Set
let expected: HashSet<(u32, bool, usize, u32)> = vec![
// Input layer: Expect 4
(0, true, 0, 0), (1, true, 0, 1), (2, true, 0, 2), (3, true, 0, 3),
// Hidden Layer 1: Expect 7
(4, false, 1, 4), (5, false, 1, 5), (6, false, 1, 6), (7, false, 1, 7), (8, false, 1, 8), (9, false, 1, 9), (10, false, 1, 10),
// Hidden Layer 2: Expect 3
(7, true, 2, 11), (8, true, 2, 12), (9, true, 2, 13),
// Hidden Layer 3: Expect 3
(10, true, 3, 14), (11, true, 3, 15), (12, true, 3, 16),
// Hidden Layer 4: Expect 3
(13, true, 4, 17), (14, true, 4, 18), (15, true, 4, 19),
// Hidden Layer 5: Expect 3
(16, true, 5, 20), (17, true, 5, 21), (18, true, 5, 22),
// Output Layer: Expect 4
(19, true, 6, 23), (20, true, 6, 24), (21, true, 6, 25), (22, true, 6, 26),
].into_iter().collect();
// Convert Result to HashSet for Comparison
let result_set: HashSet<(u32, bool, usize, u32)> = result.into_iter().collect();
// Assert
assert_eq!(result_set, expected);
}
}