From 0ccd824ee67781613be7354daa2625bf3d471a52 Mon Sep 17 00:00:00 2001 From: vandomej Date: Thu, 4 Apr 2024 20:30:08 -0700 Subject: [PATCH] Defining merge operation --- gemla/Cargo.toml | 1 + gemla/src/bin/fighter_nn/mod.rs | 138 +++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/gemla/Cargo.toml b/gemla/Cargo.toml index 161d544..2dd14c4 100644 --- a/gemla/Cargo.toml +++ b/gemla/Cargo.toml @@ -32,3 +32,4 @@ easy-parallel = "3.3.1" fann = "0.1.8" async-trait = "0.1.78" async-recursion = "1.1.0" +lerp = "0.5.0" diff --git a/gemla/src/bin/fighter_nn/mod.rs b/gemla/src/bin/fighter_nn/mod.rs index b7b2c43..e781a9d 100644 --- a/gemla/src/bin/fighter_nn/mod.rs +++ b/gemla/src/bin/fighter_nn/mod.rs @@ -3,10 +3,11 @@ extern crate fann; pub mod neural_network_utility; pub mod fighter_context; -use std::{fs::{self, File}, io::{self, BufRead, BufReader}, ops::Range, path::{Path, PathBuf}, sync::Arc}; +use std::{cmp::max, fs::{self, File}, io::{self, BufRead, BufReader}, ops::Range, path::{Path, PathBuf}}; use fann::{ActivationFunc, Fann}; use futures::future::join_all; use gemla::{core::genetic_node::{GeneticNode, GeneticNodeContext}, error::Error}; +use lerp::Lerp; use rand::prelude::*; use serde::{Deserialize, Serialize}; use anyhow::Context; @@ -148,7 +149,7 @@ impl GeneticNode for FighterNN { let random_nn_index = thread_rng().gen_range(0..self_clone.population_size); let folder = self_clone.folder.clone(); let generation = self_clone.generation; - let semaphore_clone = Arc::clone(&semaphore_clone); + let semaphore_clone = semaphore_clone.clone(); let random_nn = folder.join(format!("{}", generation)).join(self_clone.get_individual_id(random_nn_index as u64)); let nn_clone = nn.clone(); // Clone the path to use in the async block @@ -156,7 +157,7 @@ impl GeneticNode for FighterNN { let future = async move { let permit = semaphore_clone.acquire_owned().await.with_context(|| "Failed to acquire semaphore permit")?; - let score = run_1v1_simulation(&nn_clone, &random_nn).await?; + let (score, _) = run_1v1_simulation(&nn_clone, &random_nn).await?; drop(permit); @@ -262,16 +263,66 @@ impl GeneticNode for FighterNN { Ok(()) } - async fn merge(left: &FighterNN, right: &FighterNN, id: &Uuid, _: Self::Context) -> Result, Error> { + async fn merge(left: &FighterNN, right: &FighterNN, id: &Uuid, gemla_context: Self::Context) -> Result, Error> { let base_path = PathBuf::from(BASE_DIR); let folder = base_path.join(format!("fighter_nn_{:06}", id)); // Ensure the folder exists, including the generation subfolder. fs::create_dir_all(&folder.join("0")) .with_context(|| format!("Failed to create directory {:?}", folder.join("0")))?; + + let get_highest_scores = |fighter: &FighterNN| -> Vec<(u64, f32)> { + let mut sorted_scores: Vec<_> = fighter.scores[fighter.generation as usize].iter().collect(); + sorted_scores.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap()); + sorted_scores.iter().take(fighter.population_size / 2).map(|(k, v)| (**k, **v)).collect() + }; + + let left_scores = get_highest_scores(left); + let right_scores = get_highest_scores(right); + + debug!("Left scores: {:?}", left_scores); + debug!("Right scores: {:?}", right_scores); + + let mut simulations = Vec::new(); + + for _ in 0..max(left.population_size, right.population_size)*SIMULATION_ROUNDS { + let left_nn_id = left_scores[thread_rng().gen_range(0..left_scores.len())].0; + let right_nn_id = right_scores[thread_rng().gen_range(0..right_scores.len())].0; + + let left_nn_path = left.folder.join(left.generation.to_string()).join(left.get_individual_id(left_nn_id)); + let right_nn_path = right.folder.join(right.generation.to_string()).join(right.get_individual_id(right_nn_id)); + let semaphore_clone = gemla_context.shared_semaphore.clone(); + + let future = async move { + let permit = semaphore_clone.acquire_owned().await.with_context(|| "Failed to acquire semaphore permit")?; + + let (left_score, right_score) = run_1v1_simulation(&left_nn_path, &right_nn_path).await?; + + drop(permit); + + Ok::<(f32, f32), Error>((left_score, right_score)) + }; + + simulations.push(future); + } + + let results: Result, Error> = join_all(simulations).await.into_iter().collect(); + let scores = results?; + + let total_left_score = scores.iter().map(|(l, _)| l).sum::(); + let total_right_score = scores.iter().map(|(_, r)| r).sum::(); + + debug!("Total left score: {}", total_left_score); + debug!("Total right score: {}", total_right_score); + + let score_difference = total_right_score - total_left_score; + // Use the sigmoid function to determine lerp amount + let lerp_amount = 1.0 / (1.0 + (-score_difference).exp()); + + let mut nn_shapes = HashMap::new(); // Function to copy NNs from a source FighterNN to the new folder. - let copy_nns = |source: &FighterNN, folder: &PathBuf, id: &Uuid, start_idx: usize| -> Result<(), Error> { + let mut copy_nns = |source: &FighterNN, folder: &PathBuf, id: &Uuid, start_idx: usize| -> Result<(), Error> { let mut sorted_scores: Vec<_> = source.scores[source.generation as usize].iter().collect(); sorted_scores.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap()); let remaining = sorted_scores[(source.population_size / 2)..].iter().map(|(k, _)| *k).collect::>(); @@ -281,26 +332,62 @@ impl GeneticNode for FighterNN { let new_nn_path = folder.join("0").join(format!("{:06}_fighter_nn_{}.net", id, start_idx + i)); fs::copy(&nn_path, &new_nn_path) .with_context(|| format!("Failed to copy nn from {:?} to {:?}", nn_path, new_nn_path))?; + + nn_shapes.insert((start_idx + i) as u64, source.nn_shapes.get(&nn_id).unwrap().clone()); } + Ok(()) }; // Copy the top half of NNs from each parent to the new folder. copy_nns(left, &folder, id, 0)?; copy_nns(right, &folder, id, left.population_size as usize / 2)?; + + debug!("nn_shapes: {:?}", nn_shapes); + + // Lerp the mutation rates and weight ranges + let crossbreed_segments = (left.crossbreed_segments as f32).lerp(right.crossbreed_segments as f32, lerp_amount) as usize; + + let weight_initialization_range_start = left.weight_initialization_range.start.lerp(right.weight_initialization_range.start, lerp_amount); + let weight_initialization_range_end = left.weight_initialization_range.end.lerp(right.weight_initialization_range.end, lerp_amount); + // Have to ensure the range is valid + let weight_initialization_range = if weight_initialization_range_start < weight_initialization_range_end { + weight_initialization_range_start..weight_initialization_range_end + } else { + weight_initialization_range_end..weight_initialization_range_start + }; + + debug!("weight_initialization_range: {:?}", weight_initialization_range); + + let minor_mutation_rate = left.minor_mutation_rate.lerp(right.minor_mutation_rate, lerp_amount); + let major_mutation_rate = left.major_mutation_rate.lerp(right.major_mutation_rate, lerp_amount); + + debug!("minor_mutation_rate: {}", minor_mutation_rate); + debug!("major_mutation_rate: {}", major_mutation_rate); + let mutation_weight_range_start = left.mutation_weight_range.start.lerp(right.mutation_weight_range.start, lerp_amount); + let mutation_weight_range_end = left.mutation_weight_range.end.lerp(right.mutation_weight_range.end, lerp_amount); + // Have to ensure the range is valid + let mutation_weight_range = if mutation_weight_range_start < mutation_weight_range_end { + mutation_weight_range_start..mutation_weight_range_end + } else { + mutation_weight_range_end..mutation_weight_range_start + }; + + debug!("mutation_weight_range: {:?}", mutation_weight_range); + Ok(Box::new(FighterNN { id: *id, folder, generation: 0, - population_size: left.population_size, // Assuming left and right have the same population size. + population_size: nn_shapes.len(), scores: vec![HashMap::new()], - crossbreed_segments: left.crossbreed_segments, - nn_shapes: left.nn_shapes.clone(), // Assuming left and right have the same nn_shapes. - weight_initialization_range: left.weight_initialization_range.clone(), - minor_mutation_rate: left.minor_mutation_rate, - major_mutation_rate: left.major_mutation_rate, - mutation_weight_range: left.mutation_weight_range.clone(), + crossbreed_segments, + nn_shapes, + weight_initialization_range, + minor_mutation_rate, + major_mutation_rate, + mutation_weight_range, })) } } @@ -311,7 +398,7 @@ impl FighterNN { } } -async fn run_1v1_simulation(nn_path_1: &PathBuf, nn_path_2: &PathBuf) -> Result { +async fn run_1v1_simulation(nn_path_1: &PathBuf, nn_path_2: &PathBuf) -> Result<(f32, f32), Error> { // Construct the score file path let base_folder = nn_path_1.parent().unwrap(); let nn_1_id = nn_path_1.file_stem().unwrap().to_str().unwrap(); @@ -323,9 +410,13 @@ async fn run_1v1_simulation(nn_path_1: &PathBuf, nn_path_2: &PathBuf) -> Result< let round_score = read_score_from_file(&score_file, &nn_1_id).await .with_context(|| format!("Failed to read score from file: {:?}", score_file))?; - trace!("{} scored {}", nn_1_id, round_score); + let opposing_score = read_score_from_file(&score_file, &nn_2_id).await + .with_context(|| format!("Failed to read score from file: {:?}", score_file))?; - return Ok::(round_score); + trace!("{} scored {}, while {} scored {}", nn_1_id, round_score, nn_2_id, opposing_score); + + + return Ok((round_score, opposing_score)); } // Check if the opposite round score has been determined @@ -334,9 +425,12 @@ async fn run_1v1_simulation(nn_path_1: &PathBuf, nn_path_2: &PathBuf) -> Result< let round_score = read_score_from_file(&opposite_score_file, &nn_1_id).await .with_context(|| format!("Failed to read score from file: {:?}", opposite_score_file))?; - trace!("{} scored {}", nn_1_id, round_score); + let opposing_score = read_score_from_file(&opposite_score_file, &nn_2_id).await + .with_context(|| format!("Failed to read score from file: {:?}", opposite_score_file))?; - return Ok::(1.0 - round_score); + trace!("{} scored {}, while {} scored {}", nn_1_id, round_score, nn_2_id, opposing_score); + + return Ok((round_score, opposing_score)); } // Run simulation until score file is generated @@ -368,12 +462,16 @@ async fn run_1v1_simulation(nn_path_1: &PathBuf, nn_path_2: &PathBuf) -> Result< let round_score = read_score_from_file(&score_file, &nn_1_id).await .with_context(|| format!("Failed to read score from file: {:?}", score_file))?; - trace!("{} scored {}", nn_1_id, round_score); + let opposing_score = read_score_from_file(&score_file, &nn_2_id).await + .with_context(|| format!("Failed to read score from file: {:?}", score_file))?; - Ok(round_score) + trace!("{} scored {}, while {} scored {}", nn_1_id, round_score, nn_2_id, opposing_score); + + + return Ok((round_score, opposing_score)) } else { warn!("Score file not found: {:?}", score_file); - Ok(0.0) + Ok((0.0, 0.0)) } }