pub mod nn;
pub mod snake;
pub mod weights;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportedModel {
pub input_dim: usize,
pub output_dim: usize,
pub hidden_dim: usize,
pub feature_extractor: LayerWeights,
pub policy_head: LayerWeights,
pub value_head: Option<LayerWeights>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerWeights {
pub weights: Vec<f32>,
pub biases: Vec<f32>,
pub in_features: usize,
pub out_features: usize,
}
impl ExportedModel {
pub fn new(
input_dim: usize,
output_dim: usize,
hidden_dim: usize,
feature_extractor: LayerWeights,
policy_head: LayerWeights,
value_head: Option<LayerWeights>,
) -> Self {
Self { input_dim, output_dim, hidden_dim, feature_extractor, policy_head, value_head }
}
pub fn predict(&self, observation: &[f32]) -> Vec<f32> {
assert_eq!(observation.len(), self.input_dim, "Input dimension mismatch");
let features = self.feature_extractor.forward(observation);
let features_relu: Vec<f32> = features.iter().map(|&x| x.max(0.0)).collect();
let logits = self.policy_head.forward(&features_relu);
softmax(&logits)
}
pub fn get_action(&self, observation: &[f32]) -> usize {
let probs = self.predict(observation);
probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap()
}
pub fn get_action_probs(&self, observation: &[f32]) -> Vec<f32> {
self.predict(observation)
}
}
impl LayerWeights {
pub fn new(
weights: Vec<f32>,
biases: Vec<f32>,
in_features: usize,
out_features: usize,
) -> Self {
assert_eq!(weights.len(), in_features * out_features, "Weight matrix size mismatch");
assert_eq!(biases.len(), out_features, "Bias vector size mismatch");
Self { weights, biases, in_features, out_features }
}
#[allow(clippy::needless_range_loop)]
pub fn forward(&self, input: &[f32]) -> Vec<f32> {
assert_eq!(input.len(), self.in_features, "Input size mismatch");
let mut output = vec![0.0; self.out_features];
for i in 0..self.out_features {
let mut sum = self.biases[i];
for j in 0..self.in_features {
sum += self.weights[i * self.in_features + j] * input[j];
}
output[i] = sum;
}
output
}
}
fn softmax(logits: &[f32]) -> Vec<f32> {
let max_logit = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&x| (x - max_logit).exp()).collect();
let sum: f32 = exps.iter().sum();
exps.iter().map(|&x| x / sum).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layer_forward() {
let weights = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ];
let biases = vec![0.1, 0.2, 0.3];
let layer = LayerWeights::new(weights, biases, 2, 3);
let input = vec![1.0, 2.0];
let output = layer.forward(&input);
assert!((output[0] - 5.1).abs() < 1e-5);
assert!((output[1] - 11.2).abs() < 1e-5);
assert!((output[2] - 17.3).abs() < 1e-5);
}
#[test]
fn test_softmax() {
let logits = vec![1.0, 2.0, 3.0];
let probs = softmax(&logits);
let sum: f32 = probs.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
assert!(probs[0] < probs[1]);
assert!(probs[1] < probs[2]);
}
#[test]
fn test_exported_model() {
let feature_weights = LayerWeights::new(
vec![1.0, 0.0, 0.0, 1.0], vec![0.0, 0.0],
2,
2,
);
let policy_weights = LayerWeights::new(
vec![1.0, -1.0, -1.0, 1.0], vec![0.0, 0.0],
2,
2,
);
let model = ExportedModel::new(2, 2, 2, feature_weights, policy_weights, None);
let obs = vec![1.0, 0.5];
let action = model.get_action(&obs);
assert!(action < 2);
}
}