use std::rc::Rc;
use crate::{DType, Engine, Error, Matrix, Result, matrix as core_matrix};
use super::{Module, ModuleRegistry, matrix, nn::Linear};
pub struct ActorCriticOutput {
pub logits: Matrix,
pub value: Matrix,
}
pub trait ActorCritic: Module {
fn evaluate(&self, observation: &Matrix) -> Result<ActorCriticOutput>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CategoricalActorCriticConfig {
pub observation_size: usize,
pub action_count: usize,
pub hidden_size: usize,
pub seed: u64,
}
pub struct CategoricalActorCritic {
config: CategoricalActorCriticConfig,
policy_0: Rc<Linear>,
policy_1: Rc<Linear>,
policy: Rc<Linear>,
value_0: Rc<Linear>,
value_1: Rc<Linear>,
value: Rc<Linear>,
registry: ModuleRegistry,
}
impl CategoricalActorCritic {
pub fn new(engine: &Engine, config: CategoricalActorCriticConfig) -> Result<Self> {
if config.observation_size == 0 || config.action_count < 2 || config.hidden_size == 0 {
return Err(Error::invalid_argument(
"categorical actor-critic requires positive observation/hidden sizes and at least two actions",
));
}
let policy_0 = Rc::new(Linear::with_seed(
engine,
config.observation_size,
config.hidden_size,
config.seed,
)?);
let policy_1 = Rc::new(Linear::with_seed(
engine,
config.hidden_size,
config.hidden_size,
config.seed.wrapping_add(1),
)?);
let policy = Rc::new(Linear::with_seed(
engine,
config.hidden_size,
config.action_count,
config.seed.wrapping_add(2),
)?);
let value_0 = Rc::new(Linear::with_seed(
engine,
config.observation_size,
config.hidden_size,
config.seed.wrapping_add(3),
)?);
let value_1 = Rc::new(Linear::with_seed(
engine,
config.hidden_size,
config.hidden_size,
config.seed.wrapping_add(4),
)?);
let value = Rc::new(Linear::with_seed(
engine,
config.hidden_size,
1,
config.seed.wrapping_add(5),
)?);
let mut registry = ModuleRegistry::new();
registry.register_module("policy_0", policy_0.clone())?;
registry.register_module("policy_1", policy_1.clone())?;
registry.register_module("policy", policy.clone())?;
registry.register_module("value_0", value_0.clone())?;
registry.register_module("value_1", value_1.clone())?;
registry.register_module("value", value.clone())?;
Ok(Self {
config,
policy_0,
policy_1,
policy,
value_0,
value_1,
value,
registry,
})
}
pub fn evaluate(&self, observation: &Matrix) -> Result<ActorCriticOutput> {
let [batch, features] = observation.shape() else {
return Err(Error::invalid_argument(
"categorical actor-critic observations must have rank two",
));
};
if *batch == 0 || *features != self.config.observation_size || observation.dtype() != DType::F32
{
return Err(Error::invalid_argument(format!(
"categorical actor-critic requires nonempty FP32 observations [batch, {}]",
self.config.observation_size
)));
}
let policy_hidden = matrix::relu(&self.policy_0.forward(observation)?)?;
let policy_hidden = matrix::relu(&self.policy_1.forward(&policy_hidden)?)?;
let value_hidden = matrix::relu(&self.value_0.forward(observation)?)?;
let value_hidden = matrix::relu(&self.value_1.forward(&value_hidden)?)?;
let value = self.value.forward(&value_hidden)?;
Ok(ActorCriticOutput {
logits: self.policy.forward(&policy_hidden)?,
value: core_matrix::reshape(&value, [*batch])?,
})
}
pub const fn config(&self) -> CategoricalActorCriticConfig {
self.config
}
}
impl Module for CategoricalActorCritic {
fn forward(&self, input: &Matrix) -> Result<Matrix> {
Ok(self.evaluate(input)?.logits)
}
fn registry(&self) -> &ModuleRegistry {
&self.registry
}
}
impl ActorCritic for CategoricalActorCritic {
fn evaluate(&self, observation: &Matrix) -> Result<ActorCriticOutput> {
CategoricalActorCritic::evaluate(self, observation)
}
}