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 linear;
pub mod relu;
pub mod sigmoid;
pub mod softmax;
pub mod tanh;
pub use linear::Linear;
pub use relu::ReLU;
pub use sigmoid::Sigmoid;
pub use softmax::Softmax;
pub use tanh::Tanh;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Activation {
Linear,
ReLU,
Sigmoid,
Tanh,
Softmax,
}
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),
}
}
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),
}
}
}
impl From<Linear> for Activation {
fn from(_: Linear) -> Self {
Activation::Linear
}
}
impl From<ReLU> for Activation {
fn from(_: ReLU) -> Self {
Activation::ReLU
}
}
impl From<Sigmoid> for Activation {
fn from(_: Sigmoid) -> Self {
Activation::Sigmoid
}
}
impl From<Tanh> for Activation {
fn from(_: Tanh) -> Self {
Activation::Tanh
}
}
impl From<Softmax> for Activation {
fn from(_: Softmax) -> Self {
Activation::Softmax
}
}
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
.to_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_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);
}
}