learn 0.1.1

A minimal reinforcement learning library written in Rust from scratch.
Documentation
use crate::core::Agent;
use crate::nn::{cross_entropy_grad, softmax, MLP};
use crate::utils::compute_returns;
use rand::rngs::StdRng;
use rand::RngCore;
use rand::SeedableRng;

pub struct Reinforce {
    policy: MLP,
    lr: f32,
    gamma: f32,
    rng: StdRng,
    observations: Vec<Vec<f32>>,
    actions: Vec<usize>,
    rewards: Vec<f32>,
}

impl Reinforce {
    pub fn new(policy: MLP, lr: f32, gamma: f32, seed: u64) -> Self {
        Self {
            policy,
            lr,
            gamma,
            rng: StdRng::seed_from_u64(seed),
            observations: Vec::new(),
            actions: Vec::new(),
            rewards: Vec::new(),
        }
    }
}

impl Agent for Reinforce {
    type Observation = Vec<f32>;
    type Action = usize;

    fn act(&mut self, obs: &Self::Observation, training: bool) -> Self::Action {
        let logits = self.policy.forward(obs);
        if training {
            let probs = softmax(&logits);
            let r = self.rng.next_u32() as f32 / u32::MAX as f32;
            let mut cumsum = 0.0;
            for (i, &p) in probs.iter().enumerate() {
                cumsum += p;
                if r < cumsum {
                    return i;
                }
            }
            probs.len() - 1
        } else {
            logits
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap_or(0)
        }
    }

    fn handle_step(
        &mut self,
        obs: &Self::Observation,
        action: &Self::Action,
        reward: f32,
        _next_obs: &Self::Observation,
        _done: bool,
    ) {
        self.observations.push(obs.clone());
        self.actions.push(*action);
        self.rewards.push(reward);
    }

    fn episode_end(&mut self) {
        if self.observations.is_empty() {
            return;
        }
        let returns = compute_returns(&self.rewards, self.gamma);
        let mut total_grads: Vec<(Vec<f32>, Vec<f32>)> = self
            .policy
            .layers
            .iter()
            .map(|l| (vec![0.0; l.weights.len()], vec![0.0; l.biases.len()]))
            .collect();

        for t in 0..self.observations.len() {
            let logits = self.policy.forward(&self.observations[t]);
            let grad = cross_entropy_grad(&logits, self.actions[t]);
            let scaled_grad: Vec<f32> = grad.iter().map(|&g| g * returns[t]).collect();
            let grads = self.policy.backward(&scaled_grad);
            for (i, (gw, gb)) in grads.iter().enumerate() {
                for j in 0..total_grads[i].0.len() {
                    total_grads[i].0[j] += gw[j];
                }
                for j in 0..total_grads[i].1.len() {
                    total_grads[i].1[j] += gb[j];
                }
            }
        }

        let n = self.observations.len() as f32;
        for (gw, gb) in total_grads.iter_mut() {
            for k in gw.iter_mut() {
                *k /= n;
            }
            for k in gb.iter_mut() {
                *k /= n;
            }
        }

        self.policy.update(&total_grads, self.lr);
        self.observations.clear();
        self.actions.clear();
        self.rewards.clear();
    }
}