use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnakeCNNInference {
pub grid_width: usize,
pub grid_height: usize,
pub input_channels: usize,
pub num_actions: usize,
pub conv1_weight: Vec<Vec<Vec<Vec<f32>>>>,
pub conv1_bias: Vec<f32>,
pub conv2_weight: Vec<Vec<Vec<Vec<f32>>>>,
pub conv2_bias: Vec<f32>,
pub conv3_weight: Vec<Vec<Vec<Vec<f32>>>>,
pub conv3_bias: Vec<f32>,
pub fc_common_weight: Vec<Vec<f32>>,
pub fc_common_bias: Vec<f32>,
pub fc_policy_weight: Vec<Vec<f32>>,
pub fc_policy_bias: Vec<f32>,
pub fc_value_weight: Vec<Vec<f32>>,
pub fc_value_bias: Vec<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<crate::policy::inference::TrainingMetadata>,
}
impl SnakeCNNInference {
#[allow(clippy::needless_range_loop)]
fn conv2d(
&self,
input: &[Vec<Vec<f32>>], weight: &[Vec<Vec<Vec<f32>>>], bias: &[f32],
) -> Vec<Vec<Vec<f32>>> {
let out_channels = weight.len();
let in_channels = input.len();
let height = input[0].len();
let width = input[0][0].len();
let mut output = vec![vec![vec![0.0; width]; height]; out_channels];
for out_c in 0..out_channels {
for h in 0..height {
for w in 0..width {
let mut sum = bias[out_c];
for in_c in 0..in_channels {
for kh in 0..3 {
for kw in 0..3 {
let ih = h as i32 + kh as i32 - 1;
let iw = w as i32 + kw as i32 - 1;
if ih >= 0 && ih < height as i32 && iw >= 0 && iw < width as i32 {
sum += input[in_c][ih as usize][iw as usize]
* weight[out_c][in_c][kh][kw];
}
}
}
}
output[out_c][h][w] = sum;
}
}
}
output
}
fn relu(&self, input: &mut [Vec<Vec<f32>>]) {
for channel in input.iter_mut() {
for row in channel.iter_mut() {
for val in row.iter_mut() {
if *val < 0.0 {
*val = 0.0;
}
}
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn forward(&self, grid: &[f32]) -> (Vec<f32>, f32) {
let grid_size = self.grid_width * self.grid_height;
assert_eq!(grid.len(), self.input_channels * grid_size);
let mut input =
vec![vec![vec![0.0; self.grid_width]; self.grid_height]; self.input_channels];
for c in 0..self.input_channels {
for h in 0..self.grid_height {
for w in 0..self.grid_width {
let idx = c * grid_size + h * self.grid_width + w;
input[c][h][w] = grid[idx];
}
}
}
let mut x = self.conv2d(&input, &self.conv1_weight, &self.conv1_bias);
self.relu(&mut x);
x = self.conv2d(&x, &self.conv2_weight, &self.conv2_bias);
self.relu(&mut x);
x = self.conv2d(&x, &self.conv3_weight, &self.conv3_bias);
self.relu(&mut x);
let num_conv3_out = self.conv3_weight.len();
let spatial_size = (self.grid_height * self.grid_width) as f32;
let mut flattened = Vec::with_capacity(num_conv3_out);
for channel in &x {
let sum: f32 = channel.iter().flat_map(|row| row.iter().copied()).sum();
flattened.push(sum / spatial_size);
}
let fc_hidden = self.fc_common_weight.len();
let mut features = vec![0.0; fc_hidden];
for (i, row) in self.fc_common_weight.iter().enumerate() {
for (j, &val) in flattened.iter().enumerate() {
features[i] += row[j] * val;
}
features[i] += self.fc_common_bias[i];
if features[i] < 0.0 {
features[i] = 0.0;
}
}
let mut logits = vec![0.0; self.num_actions];
for (i, row) in self.fc_policy_weight.iter().enumerate() {
for (j, &val) in features.iter().enumerate() {
logits[i] += row[j] * val;
}
logits[i] += self.fc_policy_bias[i];
}
let mut value = 0.0;
for (j, &val) in features.iter().enumerate() {
value += self.fc_value_weight[0][j] * val;
}
value += self.fc_value_bias[0];
(logits, value)
}
pub fn get_action(&self, grid: &[f32]) -> usize {
let (logits, _value) = self.forward(grid);
logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap()
}
pub fn save_json(&self, path: &str) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load_json(path: &str) -> anyhow::Result<Self> {
let json = std::fs::read_to_string(path)?;
let model = serde_json::from_str(&json)?;
Ok(model)
}
}