use crate::error::{Context, Error};
use crate::neural_network::Tensor;
use crate::parallel_gates::{cheap_map_parallel_threshold, exp_map_parallel_threshold};
use crate::{Deserialize, Serialize};
use ndarray::{Array2, ArrayView1, ArrayViewMut1, Axis, Zip};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
fn format_shape(shape: &[usize]) -> String {
format!(
"({})",
shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
fn format_output_shape(cached_tensor: &Option<Tensor>) -> String {
match cached_tensor {
Some(tensor) => format_shape(tensor.shape()),
None => "Unknown".to_string(),
}
}
pub mod elu;
pub mod exponential;
pub mod hard_sigmoid;
pub mod leaky_relu;
pub mod linear;
pub mod p_relu;
pub mod relu;
pub mod selu;
pub mod sigmoid;
pub mod softmax;
pub mod softplus;
pub mod softsign;
pub mod tanh;
pub use elu::ELU;
pub use exponential::Exponential;
pub use hard_sigmoid::HardSigmoid;
pub use leaky_relu::LeakyReLU;
pub use linear::Linear;
pub use p_relu::PReLU;
pub use relu::ReLU;
pub use selu::SELU;
pub use sigmoid::Sigmoid;
pub use softmax::Softmax;
pub use softplus::Softplus;
pub use softsign::Softsign;
pub use tanh::Tanh;
const SELU_ALPHA: f32 = 1.673_263_2;
const SELU_SCALE: f32 = 1.050_701;
const SELU_SCALE_ALPHA: f32 = SELU_SCALE * SELU_ALPHA;
const HARD_SIGMOID_SLOPE: f32 = 1.0 / 6.0;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Activation {
Linear,
ReLU,
Sigmoid,
Tanh,
Softmax,
LeakyReLU {
negative_slope: f32,
},
ELU {
alpha: f32,
},
SELU,
Softplus,
Softsign,
HardSigmoid,
Exponential,
}
impl Activation {
pub fn forward(&self, z: &Tensor) -> Result<Tensor, Error> {
match self {
Activation::Linear => Ok(z.clone()),
Activation::ReLU => {
let mut out = z.clone();
let relu = |x: f32| if x <= 0.0 { 0.0 } else { x };
if out.len() >= cheap_map_parallel_threshold() {
out.par_mapv_inplace(relu);
} else {
out.mapv_inplace(relu);
}
Ok(out)
}
Activation::Sigmoid => {
let mut out = z.clone();
let sigmoid = |x: f32| 1.0 / (1.0 + (-x).exp());
if out.len() >= exp_map_parallel_threshold() {
out.par_mapv_inplace(sigmoid);
} else {
out.mapv_inplace(sigmoid);
}
Ok(out)
}
Activation::Tanh => {
let tanh = |x: f32| x.tanh();
let out = if z.len() >= exp_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(tanh);
out
} else {
z.mapv(tanh)
};
Ok(out)
}
Activation::Softmax => softmax_forward(z),
Activation::LeakyReLU { negative_slope } => {
let slope = *negative_slope;
let leaky_relu = |x: f32| if x >= 0.0 { x } else { slope * x };
let out = if z.len() >= cheap_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(leaky_relu);
out
} else {
z.mapv(leaky_relu)
};
Ok(out)
}
Activation::ELU { alpha } => {
let alpha = *alpha;
let elu = |x: f32| if x > 0.0 { x } else { alpha * x.exp_m1() };
let out = if z.len() >= exp_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(elu);
out
} else {
z.mapv(elu)
};
Ok(out)
}
Activation::SELU => {
let selu = |x: f32| {
if x > 0.0 {
SELU_SCALE * x
} else {
SELU_SCALE_ALPHA * x.exp_m1()
}
};
let out = if z.len() >= exp_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(selu);
out
} else {
z.mapv(selu)
};
Ok(out)
}
Activation::Softplus => {
let softplus = |x: f32| {
if x > 0.0 {
x + (-x).exp().ln_1p()
} else {
x.exp().ln_1p()
}
};
let out = if z.len() >= exp_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(softplus);
out
} else {
z.mapv(softplus)
};
Ok(out)
}
Activation::Softsign => {
let softsign = |x: f32| x / (1.0 + x.abs());
let out = if z.len() >= cheap_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(softsign);
out
} else {
z.mapv(softsign)
};
Ok(out)
}
Activation::HardSigmoid => {
let hard_sigmoid = |x: f32| (x + 3.0).clamp(0.0, 6.0) / 6.0;
let out = if z.len() >= cheap_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(hard_sigmoid);
out
} else {
z.mapv(hard_sigmoid)
};
Ok(out)
}
Activation::Exponential => {
let exponential = |x: f32| x.exp();
let out = if z.len() >= exp_map_parallel_threshold() {
let mut out = z.clone();
out.par_mapv_inplace(exponential);
out
} else {
z.mapv(exponential)
};
Ok(out)
}
}
}
pub fn backward(&self, activated: &Tensor, grad_output: &Tensor) -> Result<Tensor, Error> {
match self {
Activation::Linear => Ok(grad_output.clone()),
Activation::ReLU => {
let mut grad = grad_output.clone();
let relu_grad = |g: &mut f32, &a: &f32| {
if a <= 0.0 {
*g = 0.0;
}
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad).and(activated).par_for_each(relu_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(relu_grad);
}
Ok(grad)
}
Activation::Sigmoid => {
let mut grad = grad_output.clone();
let sigmoid_grad = |g: &mut f32, &a: &f32| {
*g *= a * (1.0 - a);
};
if grad.len() >= exp_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(sigmoid_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(sigmoid_grad);
}
Ok(grad)
}
Activation::Tanh => {
let mut grad = grad_output.clone();
let tanh_grad = |g: &mut f32, &a: &f32| {
*g *= 1.0 - a * a;
};
if activated.len() >= exp_map_parallel_threshold() {
Zip::from(&mut grad).and(activated).par_for_each(tanh_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(tanh_grad);
}
Ok(grad)
}
Activation::Softmax => softmax_backward(activated, grad_output),
Activation::LeakyReLU { negative_slope } => {
let slope = *negative_slope;
let mut grad = grad_output.clone();
let leaky_relu_grad = |g: &mut f32, &a: &f32| {
if a < 0.0 {
*g *= slope;
}
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(leaky_relu_grad);
} else {
Zip::from(&mut grad)
.and(activated)
.for_each(leaky_relu_grad);
}
Ok(grad)
}
Activation::ELU { alpha } => {
let alpha = *alpha;
let mut grad = grad_output.clone();
let elu_grad = |g: &mut f32, &a: &f32| {
if a <= 0.0 {
*g *= a + alpha;
}
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad).and(activated).par_for_each(elu_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(elu_grad);
}
Ok(grad)
}
Activation::SELU => {
let mut grad = grad_output.clone();
let selu_grad = |g: &mut f32, &a: &f32| {
*g *= if a > 0.0 {
SELU_SCALE
} else {
a + SELU_SCALE_ALPHA
};
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad).and(activated).par_for_each(selu_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(selu_grad);
}
Ok(grad)
}
Activation::Softplus => {
let mut grad = grad_output.clone();
let softplus_grad = |g: &mut f32, &a: &f32| {
*g *= -(-a).exp_m1();
};
if activated.len() >= exp_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(softplus_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(softplus_grad);
}
Ok(grad)
}
Activation::Softsign => {
let mut grad = grad_output.clone();
let softsign_grad = |g: &mut f32, &a: &f32| {
let t = 1.0 - a.abs();
*g *= t * t;
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(softsign_grad);
} else {
Zip::from(&mut grad).and(activated).for_each(softsign_grad);
}
Ok(grad)
}
Activation::HardSigmoid => {
let mut grad = grad_output.clone();
let hard_sigmoid_grad = |g: &mut f32, &a: &f32| {
*g *= if a > 0.0 && a < 1.0 {
HARD_SIGMOID_SLOPE
} else {
0.0
};
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(hard_sigmoid_grad);
} else {
Zip::from(&mut grad)
.and(activated)
.for_each(hard_sigmoid_grad);
}
Ok(grad)
}
Activation::Exponential => {
let mut grad = grad_output.clone();
let exponential_grad = |g: &mut f32, &a: &f32| {
*g *= a;
};
if activated.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad)
.and(activated)
.par_for_each(exponential_grad);
} else {
Zip::from(&mut grad)
.and(activated)
.for_each(exponential_grad);
}
Ok(grad)
}
}
}
pub fn validate(&self) -> Result<(), Error> {
let (name, value, reason) = match self {
Activation::LeakyReLU { negative_slope } => (
"negative_slope",
*negative_slope,
"must be finite and greater than 0 (use Activation::ReLU for 0)",
),
Activation::ELU { alpha } => ("alpha", *alpha, "must be finite and greater than 0"),
_ => return Ok(()),
};
if !value.is_finite() || value <= 0.0 {
return Err(Error::invalid_parameter(name, reason));
}
Ok(())
}
}
impl From<Linear> for Activation {
#[inline]
fn from(_: Linear) -> Self {
Activation::Linear
}
}
impl From<ReLU> for Activation {
#[inline]
fn from(_: ReLU) -> Self {
Activation::ReLU
}
}
impl From<Sigmoid> for Activation {
#[inline]
fn from(_: Sigmoid) -> Self {
Activation::Sigmoid
}
}
impl From<Tanh> for Activation {
#[inline]
fn from(_: Tanh) -> Self {
Activation::Tanh
}
}
impl From<Softmax> for Activation {
#[inline]
fn from(_: Softmax) -> Self {
Activation::Softmax
}
}
impl From<LeakyReLU> for Activation {
#[inline]
fn from(layer: LeakyReLU) -> Self {
Activation::LeakyReLU {
negative_slope: layer.negative_slope,
}
}
}
impl From<ELU> for Activation {
#[inline]
fn from(layer: ELU) -> Self {
Activation::ELU { alpha: layer.alpha }
}
}
impl From<SELU> for Activation {
#[inline]
fn from(_: SELU) -> Self {
Activation::SELU
}
}
impl From<Softplus> for Activation {
#[inline]
fn from(_: Softplus) -> Self {
Activation::Softplus
}
}
impl From<Softsign> for Activation {
#[inline]
fn from(_: Softsign) -> Self {
Activation::Softsign
}
}
impl From<HardSigmoid> for Activation {
#[inline]
fn from(_: HardSigmoid) -> Self {
Activation::HardSigmoid
}
}
impl From<Exponential> for Activation {
#[inline]
fn from(_: Exponential) -> Self {
Activation::Exponential
}
}
fn softmax_forward(input: &Tensor) -> Result<Tensor, Error> {
let shape = input.shape();
let ndim = shape.len();
if ndim < 2 {
return Err(Error::invalid_input(format!(
"Softmax requires input with at least 2 dimensions, got shape: {:?}",
shape
)));
}
let batch_size: usize = shape[..ndim - 1].iter().product();
let num_features = shape[ndim - 1];
let mut output_2d = input
.as_standard_layout()
.into_owned()
.into_shape_with_order((batch_size, num_features))
.context("Failed to reshape for softmax computation")?;
let apply_softmax = |mut row: ArrayViewMut1<f32>| {
let max_val = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
row.map_inplace(|x| *x = (*x - max_val).exp());
let sum = row.sum();
row.map_inplace(|x| *x /= sum);
};
if batch_size * num_features >= exp_map_parallel_threshold() {
output_2d
.axis_iter_mut(Axis(0))
.into_par_iter()
.for_each(apply_softmax);
} else {
output_2d.axis_iter_mut(Axis(0)).for_each(apply_softmax);
}
Ok(output_2d
.into_shape_with_order(shape)
.context("Failed to reshape back after softmax computation")?
.into_dyn())
}
fn softmax_backward(output: &Tensor, grad_output: &Tensor) -> Result<Tensor, Error> {
let shape = output.shape();
let ndim = shape.len();
let batch_size: usize = shape[..ndim - 1].iter().product();
let num_features = shape[ndim - 1];
let output_2d = output
.to_shape((batch_size, num_features))
.context("Failed to reshape output for backward")?;
let grad_output_2d = grad_output
.to_shape((batch_size, num_features))
.context("Failed to reshape grad_output for backward")?;
let mut grad_input_2d = Array2::<f32>::zeros((batch_size, num_features));
let compute_gradient = |mut grad_row: ArrayViewMut1<f32>,
out_row: ArrayView1<f32>,
grad_out_row: ArrayView1<f32>| {
let dot: f32 = out_row
.iter()
.zip(grad_out_row.iter())
.map(|(&o, &g)| o * g)
.sum();
for j in 0..num_features {
grad_row[j] = out_row[j] * (grad_out_row[j] - dot);
}
};
if batch_size * num_features >= exp_map_parallel_threshold() {
Zip::from(grad_input_2d.axis_iter_mut(Axis(0)))
.and(output_2d.axis_iter(Axis(0)))
.and(grad_output_2d.axis_iter(Axis(0)))
.par_for_each(compute_gradient);
} else {
Zip::from(grad_input_2d.axis_iter_mut(Axis(0)))
.and(output_2d.axis_iter(Axis(0)))
.and(grad_output_2d.axis_iter(Axis(0)))
.for_each(compute_gradient);
}
Ok(grad_input_2d
.into_shape_with_order(shape)
.context("Failed to reshape grad_input back")?
.into_dyn())
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::Array2;
fn tensor2(rows: usize, cols: usize, data: Vec<f32>) -> Tensor {
Array2::from_shape_vec((rows, cols), data)
.expect("shape/data mismatch")
.into_dyn()
}
#[test]
fn softmax_forward_basic_row() {
let input = tensor2(1, 3, vec![0.0, 1.0, 2.0]);
let output = softmax_forward(&input).expect("softmax_forward failed");
let vals = output.as_slice().expect("not contiguous");
assert_abs_diff_eq!(vals[0], 0.09003_f32, epsilon = 1e-4);
assert_abs_diff_eq!(vals[1], 0.24473_f32, epsilon = 1e-4);
assert_abs_diff_eq!(vals[2], 0.66524_f32, epsilon = 1e-4);
}
#[test]
fn softmax_forward_sums_to_one() {
let input = tensor2(1, 3, vec![0.0, 1.0, 2.0]);
let output = softmax_forward(&input).expect("softmax_forward failed");
let sum: f32 = output.iter().sum();
assert_abs_diff_eq!(sum, 1.0_f32, epsilon = 1e-6);
}
#[test]
fn softmax_forward_large_equal_values_stable() {
let input = tensor2(1, 3, vec![1000.0, 1000.0, 1000.0]);
let output = softmax_forward(&input).expect("softmax_forward failed");
let vals = output.as_slice().expect("not contiguous");
let third = 1.0_f32 / 3.0;
assert_abs_diff_eq!(vals[0], third, epsilon = 1e-6);
assert_abs_diff_eq!(vals[1], third, epsilon = 1e-6);
assert_abs_diff_eq!(vals[2], third, epsilon = 1e-6);
}
#[test]
fn softmax_forward_single_element_row() {
let input = tensor2(1, 1, vec![5.0]);
let output = softmax_forward(&input).expect("softmax_forward failed");
let vals = output.as_slice().expect("not contiguous");
assert_abs_diff_eq!(vals[0], 1.0_f32, epsilon = 1e-6);
}
#[test]
fn softmax_forward_accepts_input_that_is_not_in_c_order() {
use ndarray::IxDyn;
let base = tensor2(2, 3, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
let transposed = base.view().permuted_axes(IxDyn(&[1, 0])).to_owned();
assert!(
!transposed.is_standard_layout(),
"the test input must not be in C order"
);
let output = softmax_forward(&transposed).expect("softmax_forward must accept it");
assert_eq!(output.shape(), &[3, 2]);
let c_order: Tensor = transposed.as_standard_layout().into_owned();
let want = softmax_forward(&c_order).expect("softmax_forward failed");
for (got, expected) in output.iter().zip(want.iter()) {
assert_abs_diff_eq!(*got, *expected, epsilon = 1e-6);
}
}
#[test]
fn softmax_forward_rejects_1d_input() {
use ndarray::Array1;
let input = Array1::from_vec(vec![1.0_f32, 2.0, 3.0]).into_dyn();
assert!(
softmax_forward(&input).is_err(),
"1-D input should return Err"
);
}
#[test]
fn softmax_backward_jacobian_vector_product() {
let output = tensor2(1, 3, vec![0.25, 0.25, 0.5]);
let grad_output = tensor2(1, 3, vec![1.0, 0.0, 0.0]);
let grad_input = softmax_backward(&output, &grad_output).expect("softmax_backward failed");
let vals = grad_input.as_slice().expect("not contiguous");
assert_abs_diff_eq!(vals[0], 0.1875_f32, epsilon = 1e-6);
assert_abs_diff_eq!(vals[1], -0.0625_f32, epsilon = 1e-6);
assert_abs_diff_eq!(vals[2], -0.125_f32, epsilon = 1e-6);
}
#[test]
fn softmax_backward_row_sums_to_zero() {
let output = tensor2(1, 3, vec![0.25, 0.25, 0.5]);
let grad_output = tensor2(1, 3, vec![1.0, 0.0, 0.0]);
let grad_input = softmax_backward(&output, &grad_output).expect("softmax_backward failed");
let row_sum: f32 = grad_input.iter().sum();
assert_abs_diff_eq!(row_sum, 0.0_f32, epsilon = 1e-6);
}
#[test]
fn activation_softmax_forward_via_enum() {
let input = tensor2(1, 3, vec![0.0, 1.0, 2.0]);
let output = Activation::Softmax
.forward(&input)
.expect("Activation::Softmax forward failed");
let vals = output.as_slice().expect("not contiguous");
assert_abs_diff_eq!(vals[0], 0.09003_f32, epsilon = 1e-4);
assert_abs_diff_eq!(vals[1], 0.24473_f32, epsilon = 1e-4);
assert_abs_diff_eq!(vals[2], 0.66524_f32, epsilon = 1e-4);
}
#[test]
fn activation_softmax_backward_via_enum() {
let output = tensor2(1, 3, vec![0.25, 0.25, 0.5]);
let grad_output = tensor2(1, 3, vec![1.0, 0.0, 0.0]);
let grad_input = Activation::Softmax
.backward(&output, &grad_output)
.expect("Activation::Softmax backward failed");
let vals = grad_input.as_slice().expect("not contiguous");
assert_abs_diff_eq!(vals[0], 0.1875_f32, epsilon = 1e-6);
assert_abs_diff_eq!(vals[1], -0.0625_f32, epsilon = 1e-6);
assert_abs_diff_eq!(vals[2], -0.125_f32, epsilon = 1e-6);
}
const PROBES: [f32; 11] = [-5.0, -3.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 3.0, 5.0];
fn assert_pinned(name: &str, actual: &Tensor, expected: &[f32]) {
let got: Vec<f32> = actual.iter().cloned().collect();
assert_eq!(got.len(), expected.len(), "{name}: length mismatch");
for (i, (&g, &e)) in got.iter().zip(expected.iter()).enumerate() {
let tol = 1e-5 * e.abs().max(1.0);
assert!(
(g - e).abs() <= tol,
"{name}[{i}] at x = {}: got {g}, want {e}, tolerance {tol}",
PROBES[i]
);
}
}
fn check_against_reference(name: &str, activation: Activation, fwd: &[f32], grad: &[f32]) {
let input = tensor2(1, PROBES.len(), PROBES.to_vec());
let output = activation.forward(&input).expect("forward failed");
assert_pinned(&format!("{name} forward"), &output, fwd);
let ones = tensor2(1, PROBES.len(), vec![1.0; PROBES.len()]);
let derivative = activation
.backward(&output, &ones)
.expect("backward failed");
assert_pinned(&format!("{name} backward"), &derivative, grad);
}
#[test]
fn leaky_relu_matches_reference() {
check_against_reference(
"LeakyReLU(0.3)",
Activation::LeakyReLU {
negative_slope: 0.3,
},
&[
-1.5,
-0.90000004,
-0.6,
-0.3,
-0.15,
0.0,
0.5,
1.0,
2.0,
3.0,
5.0,
],
&[0.3, 0.3, 0.3, 0.3, 0.3, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
);
}
#[test]
fn elu_matches_reference() {
check_against_reference(
"ELU(1.0)",
Activation::ELU { alpha: 1.0 },
&[
-0.99326205,
-0.95021296,
-0.86466473,
-0.63212055,
-0.39346933,
0.0,
0.5,
1.0,
2.0,
3.0,
5.0,
],
&[
0.0067379475,
0.049787045,
0.13533527,
0.36787945,
0.60653067,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
],
);
}
#[test]
fn elu_half_alpha_matches_reference() {
check_against_reference(
"ELU(0.5)",
Activation::ELU { alpha: 0.5 },
&[
-0.49663103,
-0.47510648,
-0.43233237,
-0.31606027,
-0.19673467,
0.0,
0.5,
1.0,
2.0,
3.0,
5.0,
],
&[
0.0033689737,
0.024893522,
0.067_667_63,
0.18393973,
0.30326533,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
],
);
}
#[test]
fn selu_matches_reference() {
check_against_reference(
"SELU",
Activation::SELU,
&[
-1.7462534,
-1.6705688,
-1.5201665,
-1.1113307,
-0.691_758_2,
0.0,
0.525_350_5,
1.050701,
2.101402,
3.152_103,
5.253_505,
],
&[
0.011845981,
0.087_530_57,
0.23793285,
0.646_768_6,
1.0663412,
1.7580993,
1.050701,
1.050701,
1.050701,
1.050701,
1.050701,
],
);
}
#[test]
fn softplus_matches_reference() {
check_against_reference(
"Softplus",
Activation::Softplus,
&[
0.0067153485,
0.048587352,
0.126928,
0.313_261_7,
0.474_077,
std::f32::consts::LN_2,
0.974_077,
1.3132617,
2.126_928,
3.0485873,
5.0067153,
],
&[
0.006692851,
0.047425874,
0.11920291,
0.2689414,
0.37754068,
0.5,
0.62245935,
0.73105854,
0.880_797,
0.95257413,
0.993_307_2,
],
);
}
#[test]
fn softsign_matches_reference() {
check_against_reference(
"Softsign",
Activation::Softsign,
&[
-0.833_333_3,
-0.75,
-0.666_666_7,
-0.5,
-0.33333334,
0.0,
0.33333334,
0.5,
0.666_666_7,
0.75,
0.833_333_3,
],
&[
0.027777776,
0.0625,
0.11111112,
0.25,
0.44444448,
1.0,
0.44444448,
0.25,
0.11111112,
0.0625,
0.027777776,
],
);
}
#[test]
fn hard_sigmoid_matches_reference() {
check_against_reference(
"HardSigmoid",
Activation::HardSigmoid,
&[
0.0,
0.0,
0.16666667,
0.33333334,
0.416_666_7,
0.5,
0.583_333_4,
0.666_666_7,
0.833_333_4,
1.0,
1.0,
],
&[
0.0, 0.0, 0.16666667, 0.16666667, 0.16666667, 0.16666667, 0.16666667, 0.16666667,
0.16666667, 0.0, 0.0,
],
);
}
#[test]
fn exponential_matches_reference() {
let table = [
0.006737947,
0.049787067,
0.13533528,
0.36787945,
0.60653067,
1.0,
1.6487212,
2.7182817,
7.389_056,
20.085537,
148.41316,
];
check_against_reference("Exponential", Activation::Exponential, &table, &table);
}
#[test]
fn softplus_backward_keeps_the_far_negative_tail() {
let input = tensor2(1, 1, vec![-40.0]);
let output = Activation::Softplus.forward(&input).expect("forward");
let ones = tensor2(1, 1, vec![1.0]);
let derivative = Activation::Softplus
.backward(&output, &ones)
.expect("backward");
let got = derivative.iter().next().copied().expect("1 element");
let expected = 4.248_354e-18_f32;
assert!(
(got - expected).abs() <= 1e-5 * expected,
"softplus derivative at x = -40: got {got}, want {expected}"
);
}
#[test]
fn validate_rejects_unusable_leaky_relu_slope() {
for slope in [0.0, -0.1, f32::NAN, f32::INFINITY] {
let result = Activation::LeakyReLU {
negative_slope: slope,
}
.validate();
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"slope {slope} must be rejected, got {result:?}"
);
}
}
#[test]
fn validate_rejects_unusable_elu_alpha() {
for alpha in [0.0, -1.0, f32::NAN, f32::NEG_INFINITY] {
let result = Activation::ELU { alpha }.validate();
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"alpha {alpha} must be rejected, got {result:?}"
);
}
}
#[test]
fn validate_accepts_usable_activations() {
let usable = [
Activation::Linear,
Activation::ReLU,
Activation::Sigmoid,
Activation::Tanh,
Activation::Softmax,
Activation::LeakyReLU {
negative_slope: 0.3,
},
Activation::ELU { alpha: 1.0 },
Activation::SELU,
Activation::Softplus,
Activation::Softsign,
Activation::HardSigmoid,
Activation::Exponential,
];
for activation in usable {
assert!(
activation.validate().is_ok(),
"{activation:?} must pass validation"
);
}
}
}