#![cfg(feature = "burn")]
use burn_tensor::activation;
use burn_tensor::{Tensor, TensorData};
use gradcheck::{adapters::burn::Burn, gradcheck, gradcheck_corrupted, Config, Verdict};
fn device() -> burn_tensor::Device {
burn_tensor::Device::default().autodiff()
}
fn weights(kind: &str, n: usize) -> Vec<f32> {
match kind {
"ones" => vec![1.0; n],
"ramp" => (0..n).map(|i| (i + 1) as f32 / n as f32).collect(),
"rademacher" => (0..n)
.map(|i| {
if (i * 2654435761usize) % 2 == 0 {
1.0
} else {
-1.0
}
})
.collect(),
"zeros3" => (0..n)
.map(|i| {
if i % 3 == 0 {
0.0
} else if i % 2 == 0 {
1.0
} else {
-1.0
}
})
.collect(),
_ => unreachable!(),
}
}
fn ramp_data(n: usize) -> Vec<f64> {
(0..n)
.map(|i| ((i as f64) * 0.613).sin() * 1.3 + ((i as f64) * 0.271).cos() * 0.7 + 0.11)
.collect()
}
fn weighted_softmax(kind: &'static str, shape: [usize; 2]) -> impl Fn(Tensor<2>) -> Tensor<2> {
move |t: Tensor<2>| {
let n = shape[0] * shape[1];
let w =
Tensor::<2>::from_data(TensorData::new(weights(kind, n), shape.to_vec()), &device());
activation::softmax(t, 1) * w
}
}
#[test]
fn step_1_reproduce_the_defect() {
let shape = [4usize, 8];
let d = ramp_data(32);
let cfg = Config::f32_defaults();
let r = gradcheck::<Burn<2>, _>(
"softmax_summed",
&d,
&shape,
|t| activation::softmax(t, 1),
&cfg,
);
let (checked, total) = r.checked_fraction();
let max_abs = r.analytic.iter().fold(0.0f64, |a, v| a.max(v.abs()));
println!(
"OBS summed verdict={:?} checked={}/{} max|analytic|={:.3e}",
r.verdict, checked, total, max_abs
);
assert!(
max_abs < 1e-6,
"the summed objective should give an identically-zero gradient, got {max_abs:e}"
);
assert!(
!r.passed(),
"an unobservable objective must not certify; got {:?}",
r.verdict
);
}
#[test]
fn step_2_weighted_objective_restores_observability() {
let shape = [4usize, 8];
let d = ramp_data(32);
let cfg = Config::f32_defaults();
for kind in ["ones", "ramp", "rademacher", "zeros3"] {
let r = gradcheck::<Burn<2>, _>(
&format!("softmax_w_{kind}"),
&d,
&shape,
weighted_softmax(kind, shape),
&cfg,
);
let (checked, total) = r.checked_fraction();
let max_abs = r.analytic.iter().fold(0.0f64, |a, v| a.max(v.abs()));
println!(
"OBS w={:<11} verdict={:?} checked={}/{} max|analytic|={:.3e}",
kind, r.verdict, checked, total, max_abs
);
if kind == "ones" {
assert!(
max_abs < 1e-6,
"w=ones IS the summed objective; it must stay unobservable"
);
} else {
assert!(
max_abs > 1e-3,
"w={kind} should produce a non-trivial gradient, got {max_abs:e}"
);
assert!(
checked > 0,
"w={kind} should adjudicate at least one component"
);
}
}
}
#[test]
fn step_3_the_weighted_objective_can_actually_reject() {
let shape = [4usize, 8];
let d = ramp_data(32);
let cfg = Config::f32_defaults();
let summed = gradcheck_corrupted::<Burn<2>, _>(
"softmax_summed_corrupted",
&d,
&shape,
|t| activation::softmax(t, 1),
&cfg,
1.5,
);
println!("OBS corrupt summed verdict={:?}", summed.verdict);
assert_ne!(
summed.verdict,
Verdict::Mismatch,
"a corruption SHOULD be invisible under the unobservable objective -- \
if this now rejects, the premise of the design note is wrong"
);
for kind in ["ramp", "rademacher", "zeros3"] {
let r = gradcheck_corrupted::<Burn<2>, _>(
&format!("softmax_w_{kind}_corrupted"),
&d,
&shape,
weighted_softmax(kind, shape),
&cfg,
1.5,
);
println!("OBS corrupt w={kind:<11} verdict={:?}", r.verdict);
assert_eq!(
r.verdict,
Verdict::Mismatch,
"w={kind}: the weighted objective must REJECT a 1.5x corruption, else it \
produces numbers without producing detection"
);
}
}