use burn::backend::Flex;
use burn::module::Module;
use burn::nn::{Linear, LinearConfig};
use burn::tensor::{Tensor, TensorData, activation, backend::Backend};
use rlevo_core::rate::NonNegativeRate;
use rlevo_evolution::WeightOnly;
use rlevo_evolution::algorithms::ga::{GaConfig, GeneticAlgorithm};
use rlevo_evolution::module_eval_fn::ModuleEvalFn;
use rlevo_evolution::param_reshaper::ModuleReshaper;
use rlevo_evolution::strategy::EvolutionaryHarness;
type TestBackend = Flex;
#[derive(Module, Debug)]
struct SineMlp<B: Backend> {
l1: Linear<B>,
l2: Linear<B>,
}
impl<B: Backend> SineMlp<B> {
fn new(device: &B::Device) -> Self {
Self {
l1: LinearConfig::new(1, 16).init(device),
l2: LinearConfig::new(16, 1).init(device),
}
}
fn forward(&self, x: Tensor<B, 2>) -> Tensor<B, 2> {
let h = activation::tanh(self.l1.forward(x));
self.l2.forward(h)
}
}
fn dataset(
device: &<TestBackend as burn::tensor::backend::BackendTypes>::Device,
n: usize,
) -> (Tensor<TestBackend, 2>, Tensor<TestBackend, 2>) {
#![allow(clippy::trivially_copy_pass_by_ref)]
let mut xs = Vec::with_capacity(n);
let mut ys = Vec::with_capacity(n);
#[allow(clippy::cast_precision_loss)]
for i in 0..n {
let t = i as f32 / (n - 1) as f32;
let x = -std::f32::consts::PI + t * 2.0 * std::f32::consts::PI;
let noise = 0.05 * (12.9898 * x).sin();
xs.push(x);
ys.push(x.sin() + noise);
}
let inputs = Tensor::<TestBackend, 2>::from_data(TensorData::new(xs, [n, 1]), device);
let targets = Tensor::<TestBackend, 2>::from_data(TensorData::new(ys, [n, 1]), device);
(inputs, targets)
}
#[test]
fn weight_only_ga_fits_noisy_sine_directional() {
let device = Default::default();
let n = 32;
let (inputs, targets) = dataset(&device, n);
let template = SineMlp::<TestBackend>::new(&device);
let reshaper = ModuleReshaper::new(template.clone());
let num_params = reshaper.num_params();
assert_eq!(num_params, 49);
let mse_inputs = inputs;
let mse_targets = targets;
let scorer = move |m: &SineMlp<TestBackend>| -> f32 {
let preds = m.forward(mse_inputs.clone());
let diff = preds - mse_targets.clone();
let mse = diff.clone().mul(diff).mean();
mse.into_data().into_vec::<f32>().unwrap()[0]
};
let eval = ModuleEvalFn::with_sense(
reshaper,
scorer,
rlevo_core::objective::ObjectiveSense::Minimize,
);
let mut params = GaConfig::default_for(64, num_params);
params.mutation_sigma = NonNegativeRate::new(0.3);
let strategy = WeightOnly::new(GeneticAlgorithm::<TestBackend>::new(), template);
let mut harness =
EvolutionaryHarness::<TestBackend, _, _>::new(strategy, params, eval, 42, device, 50)
.expect("valid params");
harness.reset();
harness.step(());
let initial_best = harness.latest_metrics().unwrap().best_fitness_ever();
loop {
if harness.step(()).done {
break;
}
}
let final_best = harness.latest_metrics().unwrap().best_fitness_ever();
assert!(
final_best < initial_best,
"expected directional improvement: final best MSE {final_best} \
should be < generation-0 best MSE {initial_best}"
);
assert_eq!(harness.generation(), 50);
}