Setting up dynamic nn shapes

This commit is contained in:
vandomej 2024-03-30 21:08:56 -07:00
parent ac71b28c7c
commit 1301d457a9
3 changed files with 111 additions and 47 deletions

View file

@ -14,7 +14,7 @@ def hierarchy_pos(G, root=None, width=1., vert_gap=0.2, vert_loc=0, xcenter=0.5)
else: else:
root = random.choice(list(G.nodes)) root = random.choice(list(G.nodes))
def _hierarchy_pos(G, root, width=1., vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None): def _hierarchy_pos(G, root, width=2., vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None):
if pos is None: if pos is None:
pos = {root: (xcenter, vert_loc)} pos = {root: (xcenter, vert_loc)}
else: else:
@ -28,7 +28,7 @@ def hierarchy_pos(G, root=None, width=1., vert_gap=0.2, vert_loc=0, xcenter=0.5)
nextx = xcenter - width / 2 - dx / 2 nextx = xcenter - width / 2 - dx / 2
for child in children: for child in children:
nextx += dx nextx += dx
pos = _hierarchy_pos(G, child, width=dx, vert_gap=vert_gap, pos = _hierarchy_pos(G, child, width=dx*2.0, vert_gap=vert_gap,
vert_loc=vert_loc - vert_gap, xcenter=nextx, vert_loc=vert_loc - vert_gap, xcenter=nextx,
pos=pos, parent=root) pos=pos, parent=root)
return pos return pos

View file

@ -45,9 +45,9 @@ fn main() -> Result<()> {
let mut gemla = log_error(Gemla::<FighterNN>::new( let mut gemla = log_error(Gemla::<FighterNN>::new(
&PathBuf::from(args.file), &PathBuf::from(args.file),
GemlaConfig { GemlaConfig {
generations_per_height: 10, generations_per_height: 5,
overwrite: false, overwrite: false,
shared_semaphore_concurrency_limit: 30, shared_semaphore_concurrency_limit: 50,
}, },
DataFormat::Json, DataFormat::Json,
))?; ))?;
@ -59,7 +59,7 @@ fn main() -> Result<()> {
// Example placeholder loop to continuously run simulate // Example placeholder loop to continuously run simulate
loop { // Arbitrary loop count for demonstration loop { // Arbitrary loop count for demonstration
gemla.simulate(5).await?; gemla.simulate(1).await?;
} }
}); });

View file

@ -15,7 +15,18 @@ use async_trait::async_trait;
const BASE_DIR: &str = "F:\\\\vandomej\\Projects\\dootcamp-AI-Simulation\\Simulations"; const BASE_DIR: &str = "F:\\\\vandomej\\Projects\\dootcamp-AI-Simulation\\Simulations";
const POPULATION: usize = 50; const POPULATION: usize = 50;
const NEURAL_NETWORK_SHAPE: &[u32; 5] = &[14, 20, 20, 12, 8];
const NEURAL_NETWORK_INPUTS: usize = 14;
const NEURAL_NETWORK_OUTPUTS: usize = 8;
const NEURAL_NETWORK_HIDDEN_LAYERS_MIN: usize = 1;
const NEURAL_NETWORK_HIDDEN_LAYERS_MAX: usize = 10;
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_MAX: usize = 20;
const SIMULATION_ROUNDS: usize = 5; const SIMULATION_ROUNDS: usize = 5;
const SURVIVAL_RATE: f32 = 0.5; const SURVIVAL_RATE: f32 = 0.5;
const GAME_EXECUTABLE_PATH: &str = "F:\\\\vandomej\\Projects\\dootcamp-AI-Simulation\\Package\\Windows\\AI_Fight_Sim.exe"; const GAME_EXECUTABLE_PATH: &str = "F:\\\\vandomej\\Projects\\dootcamp-AI-Simulation\\Package\\Windows\\AI_Fight_Sim.exe";
@ -38,6 +49,9 @@ pub struct FighterNN {
pub generation: u64, pub generation: u64,
// A map of each nn identifier in a generation and their physics score // A map of each nn identifier in a generation and their physics score
pub scores: Vec<HashMap<u64, f32>>, pub scores: Vec<HashMap<u64, f32>>,
// A map of the id of the nn in the current generation and their neural network shape
pub nn_shapes: HashMap<u64, Vec<u32>>,
pub crossbreed_segments: usize
} }
#[async_trait] #[async_trait]
@ -56,13 +70,25 @@ impl GeneticNode for FighterNN {
fs::create_dir_all(&gen_folder) fs::create_dir_all(&gen_folder)
.with_context(|| format!("Failed to create or access the generation folder: {:?}", gen_folder))?; .with_context(|| format!("Failed to create or access the generation folder: {:?}", gen_folder))?;
let nn_shapes = HashMap::new();
// Create the first generation in this folder // Create the first generation in this folder
for i in 0..POPULATION { for i in 0..POPULATION {
// Filenames are stored in the format of "xxxxxx_fighter_nn_0.net", "xxxxxx_fighter_nn_1.net", etc. Where xxxxxx is the folder name // Filenames are stored in the format of "xxxxxx_fighter_nn_0.net", "xxxxxx_fighter_nn_1.net", etc. Where xxxxxx is the folder name
let nn = gen_folder.join(format!("{:06}_fighter_nn_{}.net", context.id, i)); let nn = gen_folder.join(format!("{:06}_fighter_nn_{}.net", context.id, i));
let mut fann = Fann::new(NEURAL_NETWORK_SHAPE)
// Randomly generate a neural network shape based on constants
let hidden_layers = thread_rng().gen_range(NEURAL_NETWORK_HIDDEN_LAYERS_MIN..NEURAL_NETWORK_HIDDEN_LAYERS_MAX);
let mut nn_shape = vec![NEURAL_NETWORK_INPUTS as u32];
for _ in 0..hidden_layers {
nn_shape.push(thread_rng().gen_range(NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MIN..NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX) as u32);
}
nn_shape.push(NEURAL_NETWORK_OUTPUTS as u32);
nn_shapes.insert(i as u64, nn_shape.clone());
let mut fann = Fann::new(nn_shape.as_slice())
.with_context(|| "Failed to create nn")?; .with_context(|| "Failed to create nn")?;
fann.randomize_weights(-0.8, 0.8); fann.randomize_weights(thread_rng().gen_range(NEURAL_NETWORK_INITIAL_WEIGHT_MIN..0.0), thread_rng().gen_range(0.0..=NEURAL_NETWORK_INITIAL_WEIGHT_MAX));
fann.set_activation_func_hidden(ActivationFunc::SigmoidSymmetric); fann.set_activation_func_hidden(ActivationFunc::SigmoidSymmetric);
fann.set_activation_func_output(ActivationFunc::SigmoidSymmetric); fann.set_activation_func_output(ActivationFunc::SigmoidSymmetric);
// This will overwrite any existing file with the same name // This will overwrite any existing file with the same name
@ -76,6 +102,8 @@ impl GeneticNode for FighterNN {
population_size: POPULATION, population_size: POPULATION,
generation: 0, generation: 0,
scores: vec![HashMap::new()], scores: vec![HashMap::new()],
nn_shapes,
crossbreed_segments: thread_rng().gen_range(NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN..NEURAL_NETWORK_CROSSBREED_SEGMENTS_MAX)
})) }))
} }
@ -120,20 +148,28 @@ impl GeneticNode for FighterNN {
// Check if score file already exists before running the simulation // Check if score file already exists before running the simulation
if score_file.exists() { if score_file.exists() {
let round_score = read_score_from_file(&score_file, &nn_id) let round_score = read_score_from_file(&score_file, &nn_id).await
.with_context(|| format!("Failed to read score from file: {:?}", score_file_name))?; .with_context(|| format!("Failed to read score from file: {:?}", score_file_name))?;
trace!("{} scored {}", nn_id, round_score);
return Ok::<f32, Error>(round_score); return Ok::<f32, Error>(round_score);
} }
// Check if the opposite round score has been determined // Check if the opposite round score has been determined
let opposite_score_file = folder.join(format!("{}", generation)).join(format!("{}_vs_{}.txt", random_nn_id, nn_id)); let opposite_score_file = folder.join(format!("{}", generation)).join(format!("{}_vs_{}.txt", random_nn_id, nn_id));
if opposite_score_file.exists() { if opposite_score_file.exists() {
let round_score = read_score_from_file(&opposite_score_file, &nn_id) let round_score = read_score_from_file(&opposite_score_file, &nn_id).await
.with_context(|| format!("Failed to read score from file: {:?}", opposite_score_file))?; .with_context(|| format!("Failed to read score from file: {:?}", opposite_score_file))?;
trace!("{} scored {}", nn_id, round_score);
return Ok::<f32, Error>(1.0 - round_score); return Ok::<f32, Error>(1.0 - round_score);
} }
let _output = if thread_rng().gen_range(0..100) < 0 { // Run simulation until score file is generated
while !score_file.exists() {
let _output = if thread_rng().gen_range(0..100) < 1 {
Command::new(GAME_EXECUTABLE_PATH) Command::new(GAME_EXECUTABLE_PATH)
.arg(&config1_arg) .arg(&config1_arg)
.arg(&config2_arg) .arg(&config2_arg)
@ -149,14 +185,22 @@ impl GeneticNode for FighterNN {
.await .await
.expect("Failed to execute game") .expect("Failed to execute game")
}; };
}
drop(permit); drop(permit);
// Read the score from the file // Read the score from the file
let round_score = read_score_from_file(&score_file, &nn_id) if score_file.exists() {
let round_score = read_score_from_file(&score_file, &nn_id).await
.with_context(|| format!("Failed to read score from file: {:?}", score_file_name))?; .with_context(|| format!("Failed to read score from file: {:?}", score_file_name))?;
Ok::<f32, Error>(round_score) trace!("{} scored {}", nn_id, round_score);
Ok(round_score)
} else {
trace!("Score file not found: {:?}", score_file_name);
Ok(0.0)
}
}; };
simulations.push(future); simulations.push(future);
@ -219,9 +263,11 @@ impl GeneticNode for FighterNN {
for i in 0..survivor_count { for i in 0..survivor_count {
let nn_id = to_keep[i]; let nn_id = to_keep[i];
let nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, nn_id)); let nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, nn_id));
let mut fann = Fann::from_file(&nn) let fann = Fann::from_file(&nn)
.with_context(|| format!("Failed to load 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 // 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_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) let cross_fann = Fann::from_file(&cross_nn)
@ -239,6 +285,7 @@ impl GeneticNode for FighterNN {
start_points.push(start_point); start_points.push(start_point);
} }
start_points.sort_unstable(); // Ensure segments are in order start_points.sort_unstable(); // Ensure segments are in order
trace!("Crossbreeding Start points: {:?}", start_points);
for (j, &start) in start_points.iter().enumerate() { for (j, &start) in start_points.iter().enumerate() {
let end = if j < segment_count - 1 { let end = if j < segment_count - 1 {
@ -317,8 +364,12 @@ impl GeneticNode for FighterNN {
} }
} }
fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error> { async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error> {
let file = File::open(file_path)?; let mut attempts = 0;
loop {
match File::open(file_path) {
Ok(file) => {
let reader = BufReader::new(file); let reader = BufReader::new(file);
for line in reader.lines() { for line in reader.lines() {
@ -331,8 +382,21 @@ fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error>
} }
} }
Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::NotFound, io::ErrorKind::NotFound,
"NN ID not found in scores file", "NN ID not found in scores file",
)) ));
},
Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::Other => {
if attempts >= 5 { // Attempt 5 times before giving up.
return Err(e);
}
attempts += 1;
// wait 1 second to ensure the file is written
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
},
Err(e) => return Err(e),
}
}
} }