use std::fmt::Debug;
use burn::{
module::AutodiffModule,
optim::{AdamWConfig, GradientsParams, Optimizer},
prelude::*,
tensor::backend::AutodiffBackend,
};
use nn::loss::{MseLoss, Reduction};
use crate::{
decay,
env::{Environment, ToTensor},
exploration::{Choice, EpsilonGreedy},
memory::{Exp, ReplayMemory},
};
pub trait DQNModel<B: AutodiffBackend, const D: usize>: AutodiffModule<B> {
fn forward(&self, input: Tensor<B, D>) -> Tensor<B, 2>;
fn soft_update(self, other: &Self, tau: f32) -> Self;
}
pub struct DQNAgentConfig<E, const D: usize>
where
E: Environment,
{
pub memory: ReplayMemory<E>,
pub exploration: EpsilonGreedy<decay::Exponential>,
pub gamma: f32,
pub tau: f32,
pub lr: f32,
}
impl<E, const D: usize> Default for DQNAgentConfig<E, D>
where
E: Environment,
{
fn default() -> Self {
Self {
memory: ReplayMemory::new(50000, 128),
exploration: EpsilonGreedy::new(decay::Exponential::new(1e-3, 1.0, 0.05).unwrap()),
gamma: 0.999,
tau: 5e-3,
lr: 1e-3,
}
}
}
pub struct DQNAgent<B, M, E, const D: usize>
where
B: AutodiffBackend,
E: Environment,
{
policy_net: Option<M>,
target_net: Option<M>,
device: &'static B::Device,
memory: ReplayMemory<E>,
loss: MseLoss<B>, exploration: EpsilonGreedy<decay::Exponential>,
gamma: f32,
tau: f32,
lr: f32,
total_steps: u32,
}
impl<B, M, E, const D: usize> DQNAgent<B, M, E, D>
where
B: AutodiffBackend,
M: DQNModel<B, D>,
E: Environment,
Vec<E::State>: ToTensor<B, D, Float>,
Vec<E::Action>: ToTensor<B, 2, Int>,
E::Action: From<usize>,
B::IntElem: TryInto<usize, Error: Debug>,
{
pub fn new(model: M, config: DQNAgentConfig<E, D>, device: &'static B::Device) -> Self {
let model_clone = model.clone();
Self {
policy_net: Some(model),
target_net: Some(model_clone),
device,
memory: config.memory,
loss: MseLoss::new(),
exploration: config.exploration,
gamma: config.gamma,
tau: config.tau,
lr: config.lr,
total_steps: 0,
}
}
fn act(&self, env: &E, state: E::State) -> E::Action {
match self.exploration.choose(self.total_steps) {
Choice::Explore => env.random_action(),
Choice::Exploit => {
let input = vec![state].to_tensor(self.device);
let output = self
.policy_net
.as_ref()
.unwrap()
.forward(input)
.argmax(1)
.into_scalar();
E::Action::from(output.try_into().unwrap())
}
}
}
fn learn(&mut self, optimizer: &mut impl Optimizer<M, B>) {
let Some(batch) = self.memory.sample_zipped() else {
return;
};
let non_terminal_mask = Tensor::<B, 1, Bool>::from_bool(
batch
.next_states
.iter()
.map(Option::is_some)
.collect::<Vec<_>>()
.as_slice()
.into(),
self.device,
)
.unsqueeze_dim(1);
let states = batch.states.to_tensor(self.device);
let actions = batch.actions.to_tensor(self.device);
let next_states = Tensor::<B, D>::cat(
batch
.next_states
.into_iter()
.flatten()
.map(|ns| vec![ns].to_tensor(self.device)) .collect::<Vec<_>>(),
0,
);
let rewards =
Tensor::<B, 1>::from_floats(batch.rewards.as_slice(), self.device).unsqueeze_dim(1);
let policy_net = self.policy_net.take().unwrap();
let target_net = self.target_net.take().unwrap();
let q_values = policy_net.forward(states).gather(1, actions);
let expected_q_values = Tensor::<B, 2>::zeros([self.memory.batch_size, 1], self.device)
.mask_where(
non_terminal_mask,
target_net.forward(next_states).max_dim(1).detach(),
);
let discounted_expected_return = rewards + (expected_q_values * self.gamma);
let loss = self
.loss
.forward(q_values, discounted_expected_return, Reduction::Mean);
let grads = GradientsParams::from_grads(loss.backward(), &policy_net);
self.policy_net = Some(optimizer.step(self.lr.into(), policy_net, grads));
self.target_net = Some(target_net.soft_update(self.policy_net.as_ref().unwrap(), self.tau));
}
pub fn go(&mut self, env: &mut E) {
let mut optimizer = AdamWConfig::new().init();
let mut next_state = Some(env.reset());
while let Some(state) = next_state {
let action = self.act(env, state.clone());
let (next, reward) = env.step(action.clone());
next_state = next;
self.memory.push(Exp {
state,
action,
next_state: next_state.clone(),
reward,
});
self.learn(&mut optimizer);
self.total_steps += 1;
}
}
}