use super::core::{Activation, FuncResult, FunctionalConfig};
use crate::{func_error, validate_inputs};
use torsh_core::error::{Result, TorshError};
use torsh_tensor::Tensor;
pub fn relu(input: &Tensor) -> Result<Tensor> {
input.relu()
}
pub fn relu_inplace(input: &mut Tensor) -> Result<()> {
*input = input.relu()?;
Ok(())
}
pub fn leaky_relu(input: &Tensor, negative_slope: f32) -> Result<Tensor> {
input.leaky_relu(negative_slope)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GeluApproximation {
#[default]
None,
Tanh,
}
impl GeluApproximation {
pub fn from_str_arg(value: &str) -> Result<Self> {
match value {
"none" => Ok(Self::None),
"tanh" => Ok(Self::Tanh),
other => Err(TorshError::InvalidArgument(format!(
"gelu approximate must be \"none\" or \"tanh\", got \"{other}\""
))),
}
}
}
pub fn gelu(input: &Tensor) -> Result<Tensor> {
gelu_with_approximation(input, GeluApproximation::None)
}
pub fn gelu_with_approximation(input: &Tensor, approximate: GeluApproximation) -> Result<Tensor> {
match approximate {
GeluApproximation::None => gelu_exact(input),
GeluApproximation::Tanh => input.gelu(),
}
}
fn gelu_exact(input: &Tensor) -> Result<Tensor> {
use scirs2_core::ndarray::Array1;
use scirs2_core::ndarray_ext::elementwise::erf_simd;
const INV_SQRT_2PI: f32 = 0.398_942_28;
let data = input.to_vec()?;
let dims = input.shape().dims().to_vec();
let scaled: Array1<f32> =
Array1::from_iter(data.iter().map(|&x| x * std::f32::consts::FRAC_1_SQRT_2));
let erf_values = erf_simd(&scaled.view());
let forward_data: Vec<f32> = data
.iter()
.zip(erf_values.iter())
.map(|(&x, &e)| 0.5 * x * (1.0 + e))
.collect();
let forward = Tensor::from_data(forward_data, dims.clone(), input.device())?;
if !input.requires_grad() {
return Ok(forward);
}
let derivative_data: Vec<f32> = data
.iter()
.zip(erf_values.iter())
.map(|(&x, &e)| 0.5 * (1.0 + e) + x * (-0.5 * x * x).exp() * INV_SQRT_2PI)
.collect();
let derivative = Tensor::from_data(derivative_data, dims.clone(), input.device())?;
with_analytic_gradient(input, forward, &derivative, data, dims)
}
fn with_analytic_gradient(
input: &Tensor,
forward: Tensor,
derivative: &Tensor,
values: Vec<f32>,
dims: Vec<usize>,
) -> Result<Tensor> {
let frozen = Tensor::from_data(values, dims, input.device())?;
let residual = input.sub(&frozen)?;
let correction = residual.mul_op(derivative)?;
forward.add(&correction)
}
pub fn sigmoid(input: &Tensor) -> Result<Tensor> {
input.sigmoid()
}
pub fn softmax(input: &Tensor, dim: Option<i32>) -> Result<Tensor> {
let dim = dim.unwrap_or(-1);
let actual_dim = normalize_softmax_dim(input, dim)?;
input.softmax(actual_dim as i32)
}
pub fn log_softmax(input: &Tensor, dim: Option<i32>) -> Result<Tensor> {
let dim = dim.unwrap_or(-1);
let actual_dim = normalize_softmax_dim(input, dim)?;
input.log_softmax(actual_dim as i32)
}
fn normalize_softmax_dim(input: &Tensor, dim: i32) -> Result<usize> {
let shape_binding = input.shape();
let shape = shape_binding.dims();
if shape.is_empty() {
return Err(TorshError::InvalidOperation(
"Cannot compute softmax on a tensor with no dimensions".to_string(),
));
}
let rank = shape.len() as i32;
let actual_dim = if dim < 0 { rank + dim } else { dim };
let dim_size = if actual_dim < 0 {
None
} else {
shape.get(actual_dim as usize).copied()
};
let dim_size = dim_size.ok_or_else(|| {
TorshError::InvalidArgument(format!(
"Dimension {} out of range for a {}-dimensional tensor",
dim, rank
))
})?;
let actual_dim = actual_dim as usize;
if dim_size == 0 {
return Err(TorshError::InvalidOperation(format!(
"Cannot compute softmax along a zero-length dimension {actual_dim}"
)));
}
Ok(actual_dim)
}
pub fn tanh(input: &Tensor) -> Result<Tensor> {
input.tanh()
}
pub fn swish(input: &Tensor) -> Result<Tensor> {
let sigmoid_result = sigmoid(input)?;
input.mul_op(&sigmoid_result)
}
pub fn mish(input: &Tensor) -> Result<Tensor> {
let tanh_result = softplus(input)?.tanh()?;
input.mul_op(&tanh_result)
}
fn softplus(input: &Tensor) -> Result<Tensor> {
let tail = input
.abs()?
.mul_scalar(-1.0)?
.exp()?
.add_scalar(1.0)?
.ln()?;
input.clamp_min(0.0)?.add(&tail)
}
pub fn elu(input: &Tensor, alpha: f32) -> Result<Tensor> {
let data = input.to_vec()?;
let dims = input.shape().dims().to_vec();
let positive: Vec<f32> = data
.iter()
.map(|&x| if x > 0.0 { 1.0 } else { 0.0 })
.collect();
let negative: Vec<f32> = positive.iter().map(|&m| 1.0 - m).collect();
let positive_mask = Tensor::from_data(positive, dims.clone(), input.device())?;
let negative_mask = Tensor::from_data(negative, dims, input.device())?;
let positive_part = input.clamp_min(0.0)?.mul_op(&positive_mask)?;
let negative_part = input
.clamp_max(0.0)?
.exp()?
.add_scalar(-1.0)?
.mul_scalar(alpha)?
.mul_op(&negative_mask)?;
positive_part.add(&negative_part)
}
pub fn selu(input: &Tensor) -> Result<Tensor> {
let alpha = 1.6732632423543772;
let scale = 1.0507009873554805;
let elu_result = elu(input, alpha)?;
let scale_tensor = torsh_tensor::creation::full_like(input, scale)?;
elu_result.mul_op(&scale_tensor)
}
pub fn dropout(input: &Tensor, p: f32, training: bool) -> Result<Tensor> {
use scirs2_core::random::thread_rng;
if !training || p == 0.0 {
return Ok(input.clone());
}
if p == 1.0 {
return input.mul_scalar(0.0);
}
if !(0.0..=1.0).contains(&p) {
return Err(TorshError::InvalidArgument(format!(
"Dropout probability must be between 0 and 1, got {}",
p
)));
}
let numel = input.numel();
let scale = 1.0 / (1.0 - p);
let mut rng = thread_rng();
let mask_data: Vec<f32> = (0..numel)
.map(|_| {
let random_val: f32 = rng.random();
if random_val < p {
0.0 } else {
scale }
})
.collect();
let mask = Tensor::from_data(mask_data, input.shape().dims().to_vec(), input.device())?;
input.mul_op(&mask)
}
pub mod configured {
use super::super::core::validation;
use super::*;
pub fn relu_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(relu(input), "ReLU activation")
}
pub fn sigmoid_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(sigmoid(input), "Sigmoid activation")
}
pub fn tanh_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(tanh(input), "Tanh activation")
}
pub fn softmax_configured(
input: &Tensor,
dim: Option<i32>,
config: &FunctionalConfig,
) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(softmax(input, dim), "Softmax activation")
}
pub fn gelu_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(gelu(input), "GELU activation")
}
pub fn swish_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(swish(input), "Swish activation")
}
pub fn mish_configured(input: &Tensor, config: &FunctionalConfig) -> FuncResult<Tensor> {
validate_inputs!(config, validation::validate_not_empty(input, "input"));
func_error!(mish(input), "Mish activation")
}
}
pub struct ReLU {
inplace: bool,
}
impl ReLU {
pub fn new(inplace: bool) -> Self {
Self { inplace }
}
}
impl Activation for ReLU {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
if self.inplace {
let mut result = input.clone();
relu_inplace(&mut result)?;
Ok(result)
} else {
relu(input).map_err(|e| e.into())
}
}
}
pub struct Sigmoid;
impl Sigmoid {
pub fn new() -> Self {
Self
}
}
impl Default for Sigmoid {
fn default() -> Self {
Self::new()
}
}
impl Activation for Sigmoid {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
sigmoid(input).map_err(|e| e.into())
}
}
pub struct Tanh;
impl Tanh {
pub fn new() -> Self {
Self
}
}
impl Default for Tanh {
fn default() -> Self {
Self::new()
}
}
impl Activation for Tanh {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
tanh(input).map_err(|e| e.into())
}
}
pub struct GELU;
impl GELU {
pub fn new() -> Self {
Self
}
}
impl Default for GELU {
fn default() -> Self {
Self::new()
}
}
impl Activation for GELU {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
gelu(input).map_err(|e| e.into())
}
}
pub struct Swish;
impl Swish {
pub fn new() -> Self {
Self
}
}
impl Default for Swish {
fn default() -> Self {
Self::new()
}
}
impl Activation for Swish {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
swish(input).map_err(|e| e.into())
}
}
pub struct Mish;
impl Mish {
pub fn new() -> Self {
Self
}
}
impl Default for Mish {
fn default() -> Self {
Self::new()
}
}
impl Activation for Mish {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
mish(input).map_err(|e| e.into())
}
}
pub struct ELU {
alpha: f32,
}
impl ELU {
pub fn new(alpha: f32) -> Self {
Self { alpha }
}
}
impl Default for ELU {
fn default() -> Self {
Self::new(1.0)
}
}
impl Activation for ELU {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
elu(input, self.alpha).map_err(|e| e.into())
}
}
pub struct SELU;
impl SELU {
pub fn new() -> Self {
Self
}
}
impl Default for SELU {
fn default() -> Self {
Self::new()
}
}
impl Activation for SELU {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
selu(input).map_err(|e| e.into())
}
}
pub struct LeakyReLU {
negative_slope: f32,
}
impl LeakyReLU {
pub fn new(negative_slope: f32) -> Self {
Self { negative_slope }
}
}
impl Default for LeakyReLU {
fn default() -> Self {
Self::new(0.01)
}
}
impl Activation for LeakyReLU {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
leaky_relu(input, self.negative_slope).map_err(|e| e.into())
}
}
pub struct Softmax {
dim: i32,
}
impl Softmax {
pub fn new(dim: i32) -> Self {
Self { dim }
}
}
impl Default for Softmax {
fn default() -> Self {
Self::new(-1)
}
}
impl Activation for Softmax {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
softmax(input, Some(self.dim)).map_err(|e| e.into())
}
}
pub struct LogSoftmax {
dim: i32,
}
impl LogSoftmax {
pub fn new(dim: i32) -> Self {
Self { dim }
}
}
impl Default for LogSoftmax {
fn default() -> Self {
Self::new(-1)
}
}
impl Activation for LogSoftmax {
fn apply(&self, input: &Tensor) -> FuncResult<Tensor> {
log_softmax(input, Some(self.dim)).map_err(|e| e.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_dropout_training_p_zero() -> Result<()> {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])?;
let output = dropout(&input, 0.0, true)?;
let input_data = input.to_vec()?;
let output_data = output.to_vec()?;
assert_eq!(input_data.len(), output_data.len());
for (i, o) in input_data.iter().zip(output_data.iter()) {
assert_relative_eq!(i, o, epsilon = 1e-6);
}
Ok(())
}
#[test]
fn test_dropout_training_p_one() -> Result<()> {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])?;
let output = dropout(&input, 1.0, true)?;
let output_data = output.to_vec()?;
for &val in output_data.iter() {
assert_relative_eq!(val, 0.0, epsilon = 1e-6);
}
Ok(())
}
#[test]
fn test_dropout_eval_mode() -> Result<()> {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5])?;
let output = dropout(&input, 0.5, false)?;
let input_data = input.to_vec()?;
let output_data = output.to_vec()?;
assert_eq!(input_data.len(), output_data.len());
for (i, o) in input_data.iter().zip(output_data.iter()) {
assert_relative_eq!(i, o, epsilon = 1e-6);
}
Ok(())
}
#[test]
fn test_dropout_training_p_half() -> Result<()> {
let size = 1000;
let input_data: Vec<f32> = (0..size).map(|i| i as f32).collect();
let input = Tensor::from_vec(input_data.clone(), &[size])?;
let output = dropout(&input, 0.5, true)?;
let output_data = output.to_vec()?;
let zeros_count = output_data.iter().filter(|&&x| x == 0.0).count();
assert!(
zeros_count >= 400 && zeros_count <= 600,
"Expected 400-600 zeros, got {}",
zeros_count
);
Ok(())
}
#[test]
fn test_dropout_scaling() -> Result<()> {
let size = 10000;
let input_data: Vec<f32> = vec![1.0; size];
let input = Tensor::from_vec(input_data, &[size])?;
let p = 0.3;
let output = dropout(&input, p, true)?;
let output_data = output.to_vec()?;
let non_zeros: Vec<f32> = output_data.iter().filter(|&&x| x != 0.0).copied().collect();
if !non_zeros.is_empty() {
let mean_non_zero: f32 = non_zeros.iter().sum::<f32>() / non_zeros.len() as f32;
let expected_scale = 1.0 / (1.0 - p);
assert_relative_eq!(mean_non_zero, expected_scale, epsilon = 0.01);
}
let total_mean: f32 = output_data.iter().sum::<f32>() / output_data.len() as f32;
assert_relative_eq!(total_mean, 1.0, epsilon = 0.1);
Ok(())
}
#[test]
fn test_dropout_shape_preservation() -> Result<()> {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], &[2, 4])?;
let output = dropout(&input, 0.5, true)?;
assert_eq!(input.shape().dims(), output.shape().dims());
assert_eq!(input.shape().dims(), &[2, 4]);
Ok(())
}
#[test]
fn test_dropout_invalid_p_negative() {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0], &[3]).expect("Tensor should succeed");
let result = dropout(&input, -0.1, true);
assert!(result.is_err());
if let Err(TorshError::InvalidArgument(msg)) = result {
assert!(msg.contains("Dropout probability must be between 0 and 1"));
} else {
panic!("Expected InvalidArgument error for negative p");
}
}
#[test]
fn test_dropout_invalid_p_too_large() {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0], &[3]).expect("Tensor should succeed");
let result = dropout(&input, 1.5, true);
assert!(result.is_err());
if let Err(TorshError::InvalidArgument(msg)) = result {
assert!(msg.contains("Dropout probability must be between 0 and 1"));
} else {
panic!("Expected InvalidArgument error for p > 1.0");
}
}
#[test]
fn test_dropout_multidimensional() -> Result<()> {
let input = Tensor::from_vec(
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
&[3, 4],
)?;
let output = dropout(&input, 0.5, true)?;
assert_eq!(output.shape().dims(), &[3, 4]);
let output_data = output.to_vec()?;
let has_zeros = output_data.iter().any(|&x| x == 0.0);
let has_nonzeros = output_data.iter().any(|&x| x != 0.0);
assert!(has_zeros, "Should have some dropped (zero) elements");
assert!(has_nonzeros, "Should have some kept (non-zero) elements");
Ok(())
}
#[test]
fn test_dropout_edge_case_empty_like() -> Result<()> {
let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], &[4])?;
let output = dropout(&input, 0.01, true)?;
let output_data = output.to_vec()?;
let non_zeros = output_data.iter().filter(|&&x| x != 0.0).count();
assert!(
non_zeros >= 3,
"Expected at least 3 non-zero elements with p=0.01, got {}",
non_zeros
);
Ok(())
}
}