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:
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:
pos = {root: (xcenter, vert_loc)}
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
for child in children:
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,
pos=pos, parent=root)
return pos

View file

@ -45,9 +45,9 @@ fn main() -> Result<()> {
let mut gemla = log_error(Gemla::<FighterNN>::new(
&PathBuf::from(args.file),
GemlaConfig {
generations_per_height: 10,
generations_per_height: 5,
overwrite: false,
shared_semaphore_concurrency_limit: 30,
shared_semaphore_concurrency_limit: 50,
},
DataFormat::Json,
))?;
@ -59,7 +59,7 @@ fn main() -> Result<()> {
// Example placeholder loop to continuously run simulate
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 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 SURVIVAL_RATE: f32 = 0.5;
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,
// A map of each nn identifier in a generation and their physics score
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]
@ -55,14 +69,26 @@ impl GeneticNode for FighterNN {
let gen_folder = folder.join("0");
fs::create_dir_all(&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
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
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")?;
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_output(ActivationFunc::SigmoidSymmetric);
// This will overwrite any existing file with the same name
@ -76,6 +102,8 @@ impl GeneticNode for FighterNN {
population_size: POPULATION,
generation: 0,
scores: vec![HashMap::new()],
nn_shapes,
crossbreed_segments: thread_rng().gen_range(NEURAL_NETWORK_CROSSBREED_SEGMENTS_MIN..NEURAL_NETWORK_CROSSBREED_SEGMENTS_MAX)
}))
}
@ -120,43 +148,59 @@ impl GeneticNode for FighterNN {
// Check if score file already exists before running the simulation
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))?;
trace!("{} scored {}", nn_id, round_score);
return Ok::<f32, Error>(round_score);
}
// 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));
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))?;
trace!("{} scored {}", nn_id, round_score);
return Ok::<f32, Error>(1.0 - round_score);
}
let _output = if thread_rng().gen_range(0..100) < 0 {
Command::new(GAME_EXECUTABLE_PATH)
.arg(&config1_arg)
.arg(&config2_arg)
.output()
.await
.expect("Failed to execute game")
} else {
Command::new(GAME_EXECUTABLE_PATH)
.arg(&config1_arg)
.arg(&config2_arg)
.arg(&disable_unreal_rendering_arg)
.output()
.await
.expect("Failed to execute game")
};
// 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)
.arg(&config1_arg)
.arg(&config2_arg)
.output()
.await
.expect("Failed to execute game")
} else {
Command::new(GAME_EXECUTABLE_PATH)
.arg(&config1_arg)
.arg(&config2_arg)
.arg(&disable_unreal_rendering_arg)
.output()
.await
.expect("Failed to execute game")
};
}
drop(permit);
// Read the score from the file
let round_score = read_score_from_file(&score_file, &nn_id)
.with_context(|| format!("Failed to read score from file: {:?}", score_file_name))?;
Ok::<f32, Error>(round_score)
// Read the score from the file
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))?;
trace!("{} scored {}", nn_id, round_score);
Ok(round_score)
} else {
trace!("Score file not found: {:?}", score_file_name);
Ok(0.0)
}
};
simulations.push(future);
@ -219,9 +263,11 @@ impl GeneticNode for FighterNN {
for i in 0..survivor_count {
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 mut fann = Fann::from_file(&nn)
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)
@ -239,6 +285,7 @@ impl GeneticNode for FighterNN {
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 {
@ -317,22 +364,39 @@ impl GeneticNode for FighterNN {
}
}
fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::Error> {
let mut attempts = 0;
for line in reader.lines() {
let line = line?;
if line.starts_with(nn_id) {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() == 2 {
return parts[1].trim().parse::<f32>().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
}
loop {
match File::open(file_path) {
Ok(file) => {
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with(nn_id) {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() == 2 {
return parts[1].trim().parse::<f32>().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
}
}
}
return Err(io::Error::new(
io::ErrorKind::NotFound,
"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),
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"NN ID not found in scores file",
))
}