use burn::tensor::{Tensor, backend::Backend};
pub use crate::train::ppo::loss::{compute_entropy_loss, scalar_f64};
pub fn compute_a2c_policy_loss<B: Backend>(
log_probs: Tensor<B, 1>,
advantages: Tensor<B, 1>,
) -> Tensor<B, 1> {
let advantages = advantages.detach();
(log_probs * advantages).mean().neg()
}
pub fn compute_a2c_value_loss<B: Backend>(
values: Tensor<B, 1>,
returns: Tensor<B, 1>,
) -> Tensor<B, 1> {
let returns = returns.detach();
(values - returns).powf_scalar(2.0_f32).mean()
}
#[cfg(test)]
mod tests {
use burn::{
backend::{Autodiff, NdArray},
tensor::{Tensor, TensorData},
};
use super::*;
type B = Autodiff<NdArray<f32>>;
fn tensor1d(data: &[f32]) -> Tensor<B, 1> {
let device = Default::default();
Tensor::<B, 1>::from_data(TensorData::new(data.to_vec(), [data.len()]), &device)
}
#[test]
fn a2c_policy_loss_is_scalar() {
let log_probs = tensor1d(&[-0.5, -1.0, -0.2]);
let advantages = tensor1d(&[1.0, -1.0, 0.5]);
let loss = compute_a2c_policy_loss(log_probs, advantages);
assert_eq!(loss.dims(), [1]);
assert!(scalar_f64(loss).is_finite());
}
#[test]
fn a2c_policy_loss_sign_with_positive_advantage() {
let log_probs = tensor1d(&[-0.5, -1.0, -0.2]);
let advantages = tensor1d(&[1.0, 2.0, 0.5]);
let loss_val = scalar_f64(compute_a2c_policy_loss(log_probs, advantages));
assert!(loss_val > 0.0, "expected positive PG loss, got {loss_val}");
}
#[test]
fn a2c_policy_loss_decreases_when_good_action_more_likely() {
let advantages = tensor1d(&[1.0, 1.0, 1.0]);
let low =
scalar_f64(compute_a2c_policy_loss(tensor1d(&[-2.0, -2.0, -2.0]), advantages.clone()));
let high = scalar_f64(compute_a2c_policy_loss(tensor1d(&[-0.5, -0.5, -0.5]), advantages));
assert!(high < low, "higher log-prob on good actions should lower loss: {high} !< {low}");
}
#[test]
fn a2c_value_loss_is_plain_mse() {
let values = tensor1d(&[1.0, 2.0, 0.5]);
let returns = tensor1d(&[1.0, 1.5, 0.8]);
let loss = compute_a2c_value_loss(values, returns);
assert_eq!(loss.dims(), [1]);
let expected = (0.0_f64 + 0.25 + 0.09) / 3.0;
let loss_val = scalar_f64(loss);
assert!(
(loss_val - expected).abs() < 1e-5,
"A2C value loss should be plain MSE {expected}, got {loss_val}"
);
}
#[test]
fn a2c_value_loss_zero_when_perfect() {
let values = tensor1d(&[1.0, 2.0, 0.5]);
let returns = tensor1d(&[1.0, 2.0, 0.5]);
let loss_val = scalar_f64(compute_a2c_value_loss(values, returns));
assert!(loss_val.abs() < 1e-6, "perfect predictions should give ~0 loss, got {loss_val}");
}
}