use burn::tensor::{Int, Tensor, backend::Backend};
pub fn compute_td_target<B: Backend>(
rewards: Tensor<B, 1>,
dones: Tensor<B, 1>,
next_q_target: Tensor<B, 2>,
gamma: f64,
) -> Tensor<B, 1> {
let next_max: Tensor<B, 1> = next_q_target.max_dim(1).squeeze_dim(1);
let one = Tensor::<B, 1>::ones_like(&dones);
let not_done = one - dones;
let target = rewards + (not_done * next_max).mul_scalar(gamma as f32);
target.detach()
}
pub fn compute_td_target_double<B: Backend>(
rewards: Tensor<B, 1>,
dones: Tensor<B, 1>,
next_q_online: Tensor<B, 2>,
next_q_target: Tensor<B, 2>,
gamma: f64,
) -> Tensor<B, 1> {
let a_star: Tensor<B, 2, Int> = next_q_online.argmax(1); let next_q: Tensor<B, 1> = next_q_target.gather(1, a_star).squeeze_dim(1);
let one = Tensor::<B, 1>::ones_like(&dones);
let not_done = one - dones;
let target = rewards + (not_done * next_q).mul_scalar(gamma as f32);
target.detach()
}
pub fn gather_action_q<B: Backend>(
q_online_all: Tensor<B, 2>,
actions: Tensor<B, 1, Int>,
) -> Tensor<B, 1> {
let actions_2d: Tensor<B, 2, Int> = actions.unsqueeze_dim(1); q_online_all.gather(1, actions_2d).squeeze_dim(1)
}
pub fn compute_loss<B: Backend>(
q_online_taken: Tensor<B, 1>,
td_target: Tensor<B, 1>,
) -> Tensor<B, 1> {
huber_per_sample(q_online_taken - td_target, 1.0).mean()
}
pub fn huber_per_sample<B: Backend>(diff: Tensor<B, 1>, delta: f64) -> Tensor<B, 1> {
let delta_f = delta as f32;
let abs_diff = diff.clone().abs();
let quadratic = abs_diff.clone().clamp(0.0, delta);
let linear = abs_diff - quadratic.clone();
quadratic.clone().mul(quadratic).mul_scalar(0.5_f32) + linear.mul_scalar(delta_f)
}
pub fn compute_dqn_loss<B: Backend>(
q_online_all: Tensor<B, 2>,
actions: Tensor<B, 1, Int>,
rewards: Tensor<B, 1>,
next_q_target_all: Tensor<B, 2>,
dones: Tensor<B, 1>,
gamma: f64,
) -> Tensor<B, 1> {
let q_taken = gather_action_q(q_online_all, actions);
let target = compute_td_target(rewards, dones, next_q_target_all, gamma);
compute_loss(q_taken, target)
}
#[allow(clippy::too_many_arguments)]
pub fn compute_dqn_loss_double<B: Backend>(
q_online_all: Tensor<B, 2>,
next_q_online_all: Tensor<B, 2>,
actions: Tensor<B, 1, Int>,
rewards: Tensor<B, 1>,
next_q_target_all: Tensor<B, 2>,
dones: Tensor<B, 1>,
gamma: f64,
) -> Tensor<B, 1> {
let q_taken = gather_action_q(q_online_all, actions);
let target =
compute_td_target_double(rewards, dones, next_q_online_all, next_q_target_all, gamma);
compute_loss(q_taken, target)
}
#[cfg(test)]
mod tests {
use burn::{
backend::{Autodiff, NdArray},
tensor::{Int, 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)
}
fn tensor2d(data: &[f32], rows: usize, cols: usize) -> Tensor<B, 2> {
let device = Default::default();
Tensor::<B, 2>::from_data(TensorData::new(data.to_vec(), [rows, cols]), &device)
}
fn actions1d(data: &[i64]) -> Tensor<B, 1, Int> {
let device = Default::default();
Tensor::<B, 1, Int>::from_data(TensorData::new(data.to_vec(), [data.len()]), &device)
}
fn read_vec(t: Tensor<B, 1>) -> Vec<f32> {
t.into_data().to_vec().unwrap()
}
#[test]
fn test_td_target_terminal_drops_bootstrap() {
let rewards = tensor1d(&[1.0, 2.0]);
let dones = tensor1d(&[0.0, 1.0]);
let next_q = tensor2d(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3);
let target = compute_td_target(rewards, dones, next_q, 0.9);
let vals = read_vec(target);
assert!((vals[0] - 3.7).abs() < 1e-4);
assert!((vals[1] - 2.0).abs() < 1e-4);
}
#[test]
fn test_gather_action_q_burn() {
let q = tensor2d(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2);
let actions = actions1d(&[0, 1, 0]);
let gathered = gather_action_q(q, actions);
let vals = read_vec(gathered);
assert_eq!(vals, vec![1.0, 4.0, 5.0]);
}
#[test]
fn test_double_dqn_target_hand_computed() {
let rewards = tensor1d(&[1.0, 0.5]);
let dones = tensor1d(&[0.0, 1.0]);
let q_online_next = tensor2d(&[0.5, 2.0, 1.0, 0.5], 2, 2);
let q_target_next = tensor2d(&[10.0, 3.0, 7.0, 4.0], 2, 2);
let target = compute_td_target_double(rewards, dones, q_online_next, q_target_next, 0.9);
let vals = read_vec(target);
assert!((vals[0] - 3.7).abs() < 1e-4, "double-Q target sample 0 = {}", vals[0]);
assert!((vals[1] - 0.5).abs() < 1e-4, "double-Q target sample 1 = {}", vals[1]);
}
#[test]
fn test_huber_per_sample_quadratic_branch() {
let diff = tensor1d(&[0.1, -0.2, 0.5]);
let loss = huber_per_sample(diff, 1.0);
let vals = read_vec(loss);
assert!((vals[0] - 0.005).abs() < 1e-4);
assert!((vals[1] - 0.02).abs() < 1e-4);
assert!((vals[2] - 0.125).abs() < 1e-4);
}
#[test]
fn test_huber_per_sample_linear_branch() {
let diff = tensor1d(&[2.0, -3.0]);
let loss = huber_per_sample(diff, 1.0);
let vals = read_vec(loss);
assert!((vals[0] - 1.5).abs() < 1e-4, "got {}", vals[0]);
assert!((vals[1] - 2.5).abs() < 1e-4, "got {}", vals[1]);
}
#[test]
fn test_compute_loss_finite_on_zero_residual() {
let q = tensor1d(&[1.0, 2.0, 3.0]);
let target = tensor1d(&[1.0, 2.0, 3.0]);
let loss = compute_loss(q, target);
let v = loss.into_scalar();
assert!((v - 0.0).abs() < 1e-6);
}
#[test]
fn test_compute_dqn_loss_runs_end_to_end() {
let q_online =
tensor2d(&[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2], 4, 3);
let actions = actions1d(&[0, 1, 2, 1]);
let rewards = tensor1d(&[1.0, 0.0, -1.0, 0.5]);
let dones = tensor1d(&[0.0, 1.0, 0.0, 0.0]);
let next_q_target =
tensor2d(&[0.5, 0.1, 0.0, 0.2, 0.3, 0.4, 1.0, 0.5, 0.7, 0.2, 0.8, 0.6], 4, 3);
let loss = compute_dqn_loss(q_online, actions, rewards, next_q_target, dones, 0.99);
let v = loss.into_scalar();
assert!(v.is_finite());
assert!(v >= 0.0);
}
}