diff --git a/.vscode/launch.json b/.vscode/launch.json index 3b9c326..bc9cad2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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": [], } ] } \ No newline at end of file diff --git a/gemla/build.rs b/gemla/build.rs index e6b8ca6..d50ed3e 100644 --- a/gemla/build.rs +++ b/gemla/build.rs @@ -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: diff --git a/gemla/src/bin/fighter_nn/mod.rs b/gemla/src/bin/fighter_nn/mod.rs index 3baae77..729d234 100644 --- a/gemla/src/bin/fighter_nn/mod.rs +++ b/gemla/src/bin/fighter_nn/mod.rs @@ -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 { + // 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::() - primary_shape.last().unwrap(), secondary_shape.iter().sum::() - 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, 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::>(); + let previous_layer_connections = new_neurons.iter().filter(|(_, _, l, _)| l == &(layer - 1)).collect::>(); + + // 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 > ¤t_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 == ¤t_layer { + new_neurons.push((*neuron_id, is_primary, current_layer, 0)); + } + // Earlier + else if layer < ¤t_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::>(); + // 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::>(); + for (neuron_id, _) in neurons_to_add { + new_neurons.push((*neuron_id, is_primary, current_layer, 0)); + } + } + } + } + // Later + else if layer > ¤t_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 == ¤t_layer).collect::>(); + 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::>(); + 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::>(); + 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::>(); + // 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::>(); + 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::>(); + + // Collect and sort unique layer numbers + let mut unique_layers = new_neurons.iter() + .map(|(_, _, layer, _)| *layer) + .collect::>(); + 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::>(); + + // 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 { let mut attempts = 0; @@ -399,4 +646,95 @@ async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result 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); + } + } \ No newline at end of file