use burn::{
module::Module,
nn::{
Initializer, Linear,
conv::{Conv2d, Conv2dConfig},
},
tensor::{Int, Tensor, activation, backend::Backend},
};
use super::mlp::{derive_layer_seed, linear_from_weights, linear_with_init, seeded_layer_weights};
const INPUT_CHANNELS: usize = 4;
const FLAT_SIZE: usize = 64 * 7 * 7;
const FC_HIDDEN: usize = 512;
#[derive(Debug, Clone, Copy, Default)]
pub struct NatureDqnConfig {
pub seed: Option<u64>,
}
impl NatureDqnConfig {
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
}
fn build_convs<B: Backend>(device: &B::Device) -> (Conv2d<B>, Conv2d<B>, Conv2d<B>) {
let conv1 = Conv2dConfig::new([INPUT_CHANNELS, 32], [8, 8]).with_stride([4, 4]).init(device);
let conv2 = Conv2dConfig::new([32, 64], [4, 4]).with_stride([2, 2]).init(device);
let conv3 = Conv2dConfig::new([64, 64], [3, 3]).with_stride([1, 1]).init(device);
(conv1, conv2, conv3)
}
fn default_fc_init() -> Initializer {
Initializer::KaimingUniform { gain: 1.0_f64 / 3.0_f64.sqrt(), fan_out_only: false }
}
fn conv_features<B: Backend>(
conv1: &Conv2d<B>,
conv2: &Conv2d<B>,
conv3: &Conv2d<B>,
flat_size: usize,
obs: Tensor<B, 4>,
) -> Tensor<B, 2> {
let x = activation::relu(conv1.forward(obs));
let x = activation::relu(conv2.forward(x));
let x = activation::relu(conv3.forward(x));
let batch = x.dims()[0];
x.reshape([batch, flat_size])
}
#[derive(Module, Debug)]
pub struct NatureDqnBurnPolicy<B: Backend> {
conv1: Conv2d<B>,
conv2: Conv2d<B>,
conv3: Conv2d<B>,
fc_common: Linear<B>,
policy_head: Linear<B>,
value_head: Linear<B>,
flat_size: usize,
}
impl<B: Backend> NatureDqnBurnPolicy<B> {
pub fn new(n_actions: usize, device: &B::Device) -> Self {
Self::with_config(n_actions, NatureDqnConfig::default(), device)
}
pub fn with_config(n_actions: usize, config: NatureDqnConfig, device: &B::Device) -> Self {
let (conv1, conv2, conv3) = build_convs::<B>(device);
let (fc_common, policy_head, value_head) = if let Some(base_seed) = config.seed {
let mut layer_idx = 0u64;
let mut next = || {
let s = derive_layer_seed(base_seed, layer_idx);
layer_idx += 1;
s
};
let wc = seeded_layer_weights(next(), FLAT_SIZE, FC_HIDDEN, false, false);
let fc_common = linear_from_weights::<B>(FLAT_SIZE, FC_HIDDEN, &wc, device);
let wp = seeded_layer_weights(next(), FC_HIDDEN, n_actions, false, true);
let policy_head = linear_from_weights::<B>(FC_HIDDEN, n_actions, &wp, device);
let wv = seeded_layer_weights(next(), FC_HIDDEN, 1, false, true);
let value_head = linear_from_weights::<B>(FC_HIDDEN, 1, &wv, device);
(fc_common, policy_head, value_head)
} else {
let init = default_fc_init();
let fc_common = linear_with_init::<B>(FLAT_SIZE, FC_HIDDEN, init.clone(), device);
let policy_head = linear_with_init::<B>(FC_HIDDEN, n_actions, init.clone(), device);
let value_head = linear_with_init::<B>(FC_HIDDEN, 1, init, device);
(fc_common, policy_head, value_head)
};
Self { conv1, conv2, conv3, fc_common, policy_head, value_head, flat_size: FLAT_SIZE }
}
pub fn forward(&self, obs: Tensor<B, 4>) -> (Tensor<B, 2>, Tensor<B, 2>) {
let flat = conv_features(&self.conv1, &self.conv2, &self.conv3, self.flat_size, obs);
let features = activation::relu(self.fc_common.forward(flat));
let logits = self.policy_head.forward(features.clone());
let values = self.value_head.forward(features);
(logits, values)
}
pub fn evaluate_actions(
&self,
obs: Tensor<B, 4>,
actions: Tensor<B, 1, Int>,
) -> (Tensor<B, 1>, Tensor<B, 1>, Tensor<B, 1>) {
let (logits, values) = self.forward(obs);
let log_probs = activation::log_softmax(logits, 1);
let probs = log_probs.clone().exp();
let action_log_probs =
log_probs.clone().gather(1, actions.unsqueeze_dim::<2>(1)).squeeze_dim::<1>(1);
let entropy = -(probs * log_probs).sum_dim(1).squeeze_dim::<1>(1);
let values = values.squeeze_dim::<1>(1);
(action_log_probs, entropy, values)
}
}
#[derive(Module, Debug)]
pub struct NatureDqnQNetwork<B: Backend> {
conv1: Conv2d<B>,
conv2: Conv2d<B>,
conv3: Conv2d<B>,
fc_common: Linear<B>,
q_head: Linear<B>,
flat_size: usize,
}
impl<B: Backend> NatureDqnQNetwork<B> {
pub fn new(n_actions: usize, device: &B::Device) -> Self {
Self::with_config(n_actions, NatureDqnConfig::default(), device)
}
pub fn with_config(n_actions: usize, config: NatureDqnConfig, device: &B::Device) -> Self {
let (conv1, conv2, conv3) = build_convs::<B>(device);
let (fc_common, q_head) = if let Some(base_seed) = config.seed {
let mut layer_idx = 0u64;
let mut next = || {
let s = derive_layer_seed(base_seed, layer_idx);
layer_idx += 1;
s
};
let wc = seeded_layer_weights(next(), FLAT_SIZE, FC_HIDDEN, false, false);
let fc_common = linear_from_weights::<B>(FLAT_SIZE, FC_HIDDEN, &wc, device);
let wq = seeded_layer_weights(next(), FC_HIDDEN, n_actions, false, true);
let q_head = linear_from_weights::<B>(FC_HIDDEN, n_actions, &wq, device);
(fc_common, q_head)
} else {
let init = default_fc_init();
let fc_common = linear_with_init::<B>(FLAT_SIZE, FC_HIDDEN, init.clone(), device);
let q_head = linear_with_init::<B>(FC_HIDDEN, n_actions, init, device);
(fc_common, q_head)
};
Self { conv1, conv2, conv3, fc_common, q_head, flat_size: FLAT_SIZE }
}
pub fn forward(&self, obs: Tensor<B, 4>) -> Tensor<B, 2> {
let flat = conv_features(&self.conv1, &self.conv2, &self.conv3, self.flat_size, obs);
let features = activation::relu(self.fc_common.forward(flat));
self.q_head.forward(features)
}
pub fn copy_params_from(self, source: &NatureDqnQNetwork<B>) -> NatureDqnQNetwork<B> {
self.load_record(source.clone().into_record())
}
}
#[cfg(test)]
mod tests {
use burn::{
backend::{Autodiff, NdArray},
module::Module,
};
use super::*;
type B = Autodiff<NdArray<f32>>;
fn count_params<M: Module<B>>(module: &M) -> usize {
module.num_params()
}
#[test]
fn test_nature_dqn_ac_forward_single() {
let device = Default::default();
let policy = NatureDqnBurnPolicy::<B>::new(4, &device);
let obs = Tensor::<B, 4>::zeros([1, 4, 84, 84], &device);
let (logits, values) = policy.forward(obs);
assert_eq!(logits.dims(), [1, 4]);
assert_eq!(values.dims(), [1, 1]);
}
#[test]
fn test_nature_dqn_ac_forward_batch() {
let device = Default::default();
let policy = NatureDqnBurnPolicy::<B>::new(4, &device);
let obs = Tensor::<B, 4>::zeros([32, 4, 84, 84], &device);
let (logits, values) = policy.forward(obs);
assert_eq!(logits.dims(), [32, 4]);
assert_eq!(values.dims(), [32, 1]);
}
#[test]
fn test_nature_dqn_q_forward() {
let device = Default::default();
let q_net = NatureDqnQNetwork::<B>::new(4, &device);
let obs = Tensor::<B, 4>::zeros([1, 4, 84, 84], &device);
let q_values = q_net.forward(obs);
assert_eq!(q_values.dims(), [1, 4]);
}
#[test]
fn test_nature_dqn_evaluate_actions_shapes() {
let device = Default::default();
let policy = NatureDqnBurnPolicy::<B>::new(4, &device);
let obs = Tensor::<B, 4>::zeros([8, 4, 84, 84], &device);
let actions = Tensor::<B, 1, Int>::from_data(
burn::tensor::TensorData::new(vec![0i64, 1, 2, 3, 0, 1, 2, 3], [8]),
&device,
);
let (log_probs, entropy, values) = policy.evaluate_actions(obs, actions);
assert_eq!(log_probs.dims(), [8]);
assert_eq!(entropy.dims(), [8]);
assert_eq!(values.dims(), [8]);
}
#[test]
fn test_nature_dqn_seeded_fc_identical() {
let device = Default::default();
let cfg = NatureDqnConfig::default().with_seed(42);
let a = NatureDqnBurnPolicy::<B>::with_config(4, cfg, &device);
let b = NatureDqnBurnPolicy::<B>::with_config(4, cfg, &device);
let c = NatureDqnBurnPolicy::<B>::with_config(
4,
NatureDqnConfig::default().with_seed(43),
&device,
);
let fc_a: Vec<f32> = a.fc_common.weight.val().into_data().to_vec().unwrap();
let fc_b: Vec<f32> = b.fc_common.weight.val().into_data().to_vec().unwrap();
let fc_c: Vec<f32> = c.fc_common.weight.val().into_data().to_vec().unwrap();
assert_eq!(fc_a, fc_b, "same seed must yield identical fc_common weights");
assert!(fc_a != fc_c, "different seed must yield different fc_common weights");
let ph_a: Vec<f32> = a.policy_head.weight.val().into_data().to_vec().unwrap();
let ph_b: Vec<f32> = b.policy_head.weight.val().into_data().to_vec().unwrap();
assert_eq!(ph_a, ph_b, "same seed must yield identical policy_head weights");
let vh_a: Vec<f32> = a.value_head.weight.val().into_data().to_vec().unwrap();
let vh_b: Vec<f32> = b.value_head.weight.val().into_data().to_vec().unwrap();
assert_eq!(vh_a, vh_b, "same seed must yield identical value_head weights");
}
#[test]
fn test_nature_dqn_seeded_layers_decorrelated() {
let device = Default::default();
let cfg = NatureDqnConfig::default().with_seed(7);
let policy = NatureDqnBurnPolicy::<B>::with_config(4, cfg, &device);
let fc: Vec<f32> = policy.fc_common.weight.val().into_data().to_vec().unwrap();
let ph: Vec<f32> = policy.policy_head.weight.val().into_data().to_vec().unwrap();
let n = ph.len().min(fc.len());
assert!(
fc[..n].iter().zip(&ph[..n]).any(|(x, y)| (x - y).abs() > 1e-9),
"fc_common and policy_head must not share weights within one seeded construction"
);
}
#[test]
fn test_nature_dqn_q_copy_params_from() {
let device = Default::default();
let source = NatureDqnQNetwork::<B>::with_config(
4,
NatureDqnConfig::default().with_seed(11),
&device,
);
let target = NatureDqnQNetwork::<B>::with_config(
4,
NatureDqnConfig::default().with_seed(99),
&device,
);
let obs = Tensor::<B, 4>::ones([2, 4, 84, 84], &device) * 0.5;
let q_source_before: Vec<f32> = source.forward(obs.clone()).into_data().to_vec().unwrap();
let q_target_before: Vec<f32> = target.forward(obs.clone()).into_data().to_vec().unwrap();
assert!(
q_source_before.iter().zip(&q_target_before).any(|(a, b)| (a - b).abs() > 1e-6),
"expected fresh nets to disagree before copy"
);
let target_copied = target.copy_params_from(&source);
let q_source_after: Vec<f32> = source.forward(obs.clone()).into_data().to_vec().unwrap();
let q_target_after: Vec<f32> = target_copied.forward(obs).into_data().to_vec().unwrap();
for (a, b) in q_source_after.iter().zip(&q_target_after) {
assert!(
(a - b).abs() < 1e-6,
"Q output mismatch after copy_params_from: source={a} target={b}"
);
}
}
#[test]
fn test_nature_dqn_param_count_ac() {
let device = Default::default();
let policy = NatureDqnBurnPolicy::<B>::new(4, &device);
assert_eq!(count_params(&policy), 1_686_693);
}
#[test]
fn test_nature_dqn_param_count_q() {
let device = Default::default();
let q_net = NatureDqnQNetwork::<B>::new(4, &device);
assert_eq!(count_params(&q_net), 1_686_180);
}
}