1use burn::{
9 module::Module,
10 nn::{Initializer, Linear},
11 tensor::{Int, Tensor, activation, backend::Backend},
12};
13
14use super::mlp::{
15 BurnActivation, MlpBurnConfig, derive_layer_seed, linear_from_weights, linear_with_init,
16 seeded_layer_weights,
17};
18
19struct MultiDiscreteHostDist {
30 batch: usize,
31 num_dims: usize,
32 probs_per_dim: Vec<(usize, Vec<f32>, Vec<f32>)>,
33 values_host: Vec<f32>,
34}
35
36impl MultiDiscreteHostDist {
37 fn sample_actions(&self, rng: &mut rand::rngs::StdRng) -> (Vec<i64>, Vec<f32>, Vec<f32>) {
46 use rand::Rng;
47 let mut actions = vec![0_i64; self.batch * self.num_dims];
48 let mut log_probs = vec![0.0_f32; self.batch];
49 for row in 0..self.batch {
50 let mut joint_lp = 0.0_f32;
51 for (d, (n_actions, probs_flat, log_probs_flat)) in
52 self.probs_per_dim.iter().enumerate()
53 {
54 let u: f32 = rng.random();
55 let mut cum = 0.0;
56 let mut chosen = (*n_actions - 1) as i64;
57 for j in 0..*n_actions {
58 cum += probs_flat[row * n_actions + j];
59 if u < cum {
60 chosen = j as i64;
61 break;
62 }
63 }
64 actions[row * self.num_dims + d] = chosen;
65 joint_lp += log_probs_flat[row * n_actions + chosen as usize];
66 }
67 log_probs[row] = joint_lp;
68 }
69 (actions, log_probs, self.values_host.clone())
70 }
71}
72
73#[derive(Module, Debug)]
81pub struct MultiDiscreteMlpBurnPolicy<B: Backend> {
82 fc1: Linear<B>,
83 fc2: Linear<B>,
84 fc3: Option<Linear<B>>,
85 action_heads: Vec<Linear<B>>,
86 value_head: Linear<B>,
87 activation: BurnActivation,
88}
89
90impl<B: Backend> MultiDiscreteMlpBurnPolicy<B> {
91 pub fn new(
95 obs_dim: usize,
96 action_dims: Vec<usize>,
97 hidden_dim: usize,
98 device: &B::Device,
99 ) -> Self {
100 let config = MlpBurnConfig { hidden_dim, ..Default::default() };
101 Self::with_config(obs_dim, action_dims, config, device)
102 }
103
104 pub fn new_seeded(
110 obs_dim: usize,
111 action_dims: Vec<usize>,
112 hidden_dim: usize,
113 seed: u64,
114 device: &B::Device,
115 ) -> Self {
116 let config = MlpBurnConfig { hidden_dim, ..Default::default() }.with_seed(seed);
117 Self::with_config(obs_dim, action_dims, config, device)
118 }
119
120 pub fn with_config(
122 obs_dim: usize,
123 action_dims: Vec<usize>,
124 config: MlpBurnConfig,
125 device: &B::Device,
126 ) -> Self {
127 assert!(!action_dims.is_empty(), "action_dims must have at least one element");
128 for (i, d) in action_dims.iter().enumerate() {
129 assert!(*d >= 1, "action_dims[{i}] = {d}; must be >= 1");
130 }
131
132 if let Some(seed) = config.seed {
138 let orth = config.use_orthogonal_init;
139 let mk = |idx: u64, d_in: usize, d_out: usize, is_head: bool| {
140 let s = derive_layer_seed(seed, idx);
141 let w = seeded_layer_weights(s, d_in, d_out, orth, is_head);
142 linear_from_weights::<B>(d_in, d_out, &w, device)
143 };
144 let fc1 = mk(0, obs_dim, config.hidden_dim, false);
145 let fc2 = mk(1, config.hidden_dim, config.hidden_dim, false);
146 let fc3 = if config.num_layers >= 3 {
147 Some(mk(2, config.hidden_dim, config.hidden_dim, false))
148 } else {
149 None
150 };
151 let value_head = mk(3, config.hidden_dim, 1, true);
152 let action_heads: Vec<Linear<B>> = action_dims
153 .iter()
154 .enumerate()
155 .map(|(i, &dim)| mk(100 + i as u64, config.hidden_dim, dim, true))
156 .collect();
157 return Self { fc1, fc2, fc3, action_heads, value_head, activation: config.activation };
158 }
159
160 let hidden_init = if config.use_orthogonal_init {
161 Initializer::Orthogonal { gain: 2.0_f64.sqrt() }
162 } else {
163 Initializer::KaimingUniform { gain: 1.0_f64 / 3.0_f64.sqrt(), fan_out_only: false }
164 };
165 let output_init = if config.use_orthogonal_init {
166 Initializer::Orthogonal { gain: 0.01 }
167 } else {
168 Initializer::KaimingUniform { gain: 1.0_f64 / 3.0_f64.sqrt(), fan_out_only: false }
169 };
170
171 let fc1 = linear_with_init::<B>(obs_dim, config.hidden_dim, hidden_init.clone(), device);
172 let fc2 = linear_with_init::<B>(
173 config.hidden_dim,
174 config.hidden_dim,
175 hidden_init.clone(),
176 device,
177 );
178 let fc3 = if config.num_layers >= 3 {
179 Some(linear_with_init::<B>(config.hidden_dim, config.hidden_dim, hidden_init, device))
180 } else {
181 None
182 };
183
184 let action_heads: Vec<Linear<B>> = action_dims
185 .iter()
186 .map(|&dim| linear_with_init::<B>(config.hidden_dim, dim, output_init.clone(), device))
187 .collect();
188 let value_head = linear_with_init::<B>(config.hidden_dim, 1, output_init, device);
189
190 Self { fc1, fc2, fc3, action_heads, value_head, activation: config.activation }
191 }
192
193 fn apply_activation<const D: usize>(&self, x: Tensor<B, D>) -> Tensor<B, D> {
194 match self.activation {
195 BurnActivation::ReLU => activation::relu(x),
196 BurnActivation::Tanh => activation::tanh(x),
197 }
198 }
199
200 pub fn encoder_features(&self, obs: Tensor<B, 2>) -> Tensor<B, 2> {
203 let h = self.apply_activation(self.fc1.forward(obs));
204 let h = self.apply_activation(self.fc2.forward(h));
205 if let Some(fc3) = &self.fc3 {
206 self.apply_activation(fc3.forward(h))
207 } else {
208 h
209 }
210 }
211
212 pub fn forward(&self, obs: Tensor<B, 2>) -> (Vec<Tensor<B, 2>>, Tensor<B, 1>) {
217 let features = self.encoder_features(obs);
218 let logits: Vec<Tensor<B, 2>> =
219 self.action_heads.iter().map(|h| h.forward(features.clone())).collect();
220 let value = self.value_head.forward(features).squeeze_dim::<1>(1);
221 (logits, value)
222 }
223
224 pub fn num_action_dims(&self) -> usize {
226 self.action_heads.len()
227 }
228
229 pub fn action_dim_cardinalities(&self) -> Vec<usize> {
240 self.action_heads.iter().map(|h| h.weight.val().dims()[1]).collect()
241 }
242
243 pub fn get_action_host(&self, obs: Tensor<B, 2>) -> (Vec<i64>, Vec<f32>, Vec<f32>) {
258 use rand::SeedableRng;
259 let mut rng = rand::rngs::StdRng::from_os_rng();
263 self.get_action_host_seeded(obs, &mut rng)
264 }
265
266 pub fn get_action_host_seeded(
290 &self,
291 obs: Tensor<B, 2>,
292 rng: &mut rand::rngs::StdRng,
293 ) -> (Vec<i64>, Vec<f32>, Vec<f32>) {
294 self.forward_to_host_dist(obs).sample_actions(rng)
302 }
303
304 fn forward_to_host_dist(&self, obs: Tensor<B, 2>) -> MultiDiscreteHostDist {
308 let (logits_per_dim, value) = self.forward(obs);
309 let num_dims = logits_per_dim.len();
310 assert!(num_dims > 0, "at least one action dim");
311
312 let mut probs_per_dim: Vec<(usize, Vec<f32>, Vec<f32>)> = Vec::with_capacity(num_dims);
315 let mut batch_opt: Option<usize> = None;
316 for logits in logits_per_dim.into_iter() {
317 let dims = logits.dims();
318 let batch = dims[0];
319 let n_actions = dims[1];
320 batch_opt.get_or_insert(batch);
321 let probs = activation::softmax(logits.clone(), 1);
322 let log_probs = activation::log_softmax(logits, 1);
323 let probs_flat: Vec<f32> = probs.into_data().to_vec().expect("probs to_vec");
324 let log_probs_flat: Vec<f32> =
325 log_probs.into_data().to_vec().expect("log_probs to_vec");
326 probs_per_dim.push((n_actions, probs_flat, log_probs_flat));
327 }
328 let batch = batch_opt.unwrap_or(0);
329 let values_host: Vec<f32> = value.into_data().to_vec().expect("values to_vec");
330
331 MultiDiscreteHostDist { batch, num_dims, probs_per_dim, values_host }
332 }
333
334 pub fn get_actions_host_seeded_batched(
346 &self,
347 obs: Tensor<B, 2>,
348 rng: &mut rand::rngs::StdRng,
349 ) -> (Vec<i64>, Vec<f32>, Vec<f32>) {
350 self.forward_to_host_dist(obs).sample_actions(rng)
351 }
352
353 pub fn evaluate_actions(
365 &self,
366 obs: Tensor<B, 2>,
367 actions: Tensor<B, 2, Int>,
368 ) -> (Tensor<B, 1>, Tensor<B, 1>, Tensor<B, 1>) {
369 let (logits_per_dim, value) = self.forward(obs);
370
371 let num_dims = logits_per_dim.len();
372 assert!(num_dims > 0, "logits_per_dim must be non-empty");
373
374 let mut summed_log_probs: Option<Tensor<B, 1>> = None;
375 let mut summed_entropy: Option<Tensor<B, 1>> = None;
376
377 for (i, logits) in logits_per_dim.into_iter().enumerate() {
378 let log_probs = activation::log_softmax(logits, 1);
379 let probs = log_probs.clone().exp();
380 let per_dim_entropy: Tensor<B, 1> =
381 -(probs * log_probs.clone()).sum_dim(1).squeeze_dim::<1>(1);
382
383 let actions_i: Tensor<B, 1, Int> =
385 actions.clone().slice([0..actions.dims()[0], i..i + 1]).squeeze_dim::<1>(1);
386 let per_dim_log_p: Tensor<B, 1> =
387 log_probs.gather(1, actions_i.unsqueeze_dim::<2>(1)).squeeze_dim::<1>(1);
388
389 summed_log_probs = Some(match summed_log_probs.take() {
390 Some(acc) => acc + per_dim_log_p,
391 None => per_dim_log_p,
392 });
393 summed_entropy = Some(match summed_entropy.take() {
394 Some(acc) => acc + per_dim_entropy,
395 None => per_dim_entropy,
396 });
397 }
398
399 let log_probs = summed_log_probs.expect("at least one dim");
400 let entropy = summed_entropy.expect("at least one dim").div_scalar(num_dims as f32);
402
403 (log_probs, entropy, value)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use burn::backend::{Autodiff, NdArray};
410
411 use super::*;
412
413 type B = Autodiff<NdArray<f32>>;
414
415 #[test]
416 fn test_creation_default() {
417 let device = Default::default();
418 let _policy = MultiDiscreteMlpBurnPolicy::<B>::new(4, vec![10, 2], 32, &device);
419 }
420
421 #[test]
422 fn test_forward_shapes() {
423 let device = Default::default();
424 let policy = MultiDiscreteMlpBurnPolicy::<B>::with_config(
425 4,
426 vec![10, 2],
427 MlpBurnConfig::default(),
428 &device,
429 );
430 let obs = Tensor::<B, 2>::zeros([3, 4], &device);
431 let (logits, value) = policy.forward(obs);
432 assert_eq!(logits.len(), 2);
433 assert_eq!(logits[0].dims(), [3, 10]);
434 assert_eq!(logits[1].dims(), [3, 2]);
435 assert_eq!(value.dims(), [3]);
436 }
437
438 #[test]
439 fn test_evaluate_actions_shapes() {
440 let device = Default::default();
441 let policy = MultiDiscreteMlpBurnPolicy::<B>::new(4, vec![3, 4], 16, &device);
442 let obs = Tensor::<B, 2>::zeros([5, 4], &device);
443 let actions_data: Vec<i64> = vec![0, 1, 1, 2, 2, 0, 0, 3, 1, 2];
444 let actions = Tensor::<B, 2, Int>::from_data(
445 burn::tensor::TensorData::new(actions_data, [5, 2]),
446 &device,
447 );
448 let (log_probs, entropy, values) = policy.evaluate_actions(obs, actions);
449 assert_eq!(log_probs.dims(), [5]);
450 assert_eq!(entropy.dims(), [5]);
451 assert_eq!(values.dims(), [5]);
452 }
453
454 #[test]
455 fn test_num_action_dims() {
456 let device = Default::default();
457 let policy = MultiDiscreteMlpBurnPolicy::<B>::new(4, vec![10, 2, 5], 32, &device);
458 assert_eq!(policy.num_action_dims(), 3);
459 }
460
461 #[test]
471 fn test_get_action_host_seeded_is_bit_exact() {
472 use rand::{SeedableRng, rngs::StdRng};
473
474 let device = Default::default();
475 let policy = MultiDiscreteMlpBurnPolicy::<B>::new(4, vec![3, 4], 16, &device);
476
477 let obs_data: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2];
479 let obs_a = Tensor::<B, 2>::from_data(
480 burn::tensor::TensorData::new(obs_data.clone(), [3, 4]),
481 &device,
482 );
483 let obs_b =
484 Tensor::<B, 2>::from_data(burn::tensor::TensorData::new(obs_data, [3, 4]), &device);
485
486 let mut rng_a = StdRng::seed_from_u64(123);
487 let mut rng_b = StdRng::seed_from_u64(123);
488 let (a_a, lp_a, v_a) = policy.get_action_host_seeded(obs_a, &mut rng_a);
489 let (a_b, lp_b, v_b) = policy.get_action_host_seeded(obs_b, &mut rng_b);
490 assert_eq!(a_a.len(), 6, "row-major (row, dim) layout = 3*2 entries");
492 assert_eq!(a_a, a_b, "same-seed actions must be bit-identical");
493 assert_eq!(lp_a, lp_b, "same-seed log_probs must be bit-identical");
494 assert_eq!(v_a, v_b, "same-seed values must be bit-identical");
495 }
496
497 fn collect_params(p: &MultiDiscreteMlpBurnPolicy<B>) -> Vec<f32> {
500 let mut out = Vec::new();
501 let mut push = |lin: &Linear<B>| {
502 out.extend::<Vec<f32>>(lin.weight.val().into_data().to_vec().unwrap());
503 if let Some(b) = &lin.bias {
504 out.extend::<Vec<f32>>(b.val().into_data().to_vec().unwrap());
505 }
506 };
507 push(&p.fc1);
508 push(&p.fc2);
509 if let Some(fc3) = &p.fc3 {
510 push(fc3);
511 }
512 push(&p.value_head);
513 for h in &p.action_heads {
514 push(h);
515 }
516 out
517 }
518
519 #[test]
523 fn test_new_seeded_is_bit_identical() {
524 let device = Default::default();
525 let a = MultiDiscreteMlpBurnPolicy::<B>::new_seeded(4, vec![3, 4, 2], 16, 42, &device);
526 let b = MultiDiscreteMlpBurnPolicy::<B>::new_seeded(4, vec![3, 4, 2], 16, 42, &device);
527 assert_eq!(collect_params(&a), collect_params(&b), "same seed must be bit-identical");
528 let c = MultiDiscreteMlpBurnPolicy::<B>::new_seeded(4, vec![3, 4, 2], 16, 43, &device);
529 assert_ne!(collect_params(&a), collect_params(&c), "different seed must differ");
530 }
531
532 #[test]
535 fn test_seeded_action_heads_are_distinct() {
536 let device = Default::default();
537 let p = MultiDiscreteMlpBurnPolicy::<B>::new_seeded(4, vec![2, 2], 8, 5, &device);
539 let h0: Vec<f32> = p.action_heads[0].weight.val().into_data().to_vec().unwrap();
540 let h1: Vec<f32> = p.action_heads[1].weight.val().into_data().to_vec().unwrap();
541 assert_ne!(h0, h1, "same-cardinality action heads must get distinct seeded weights");
542 }
543}