use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::validation::{start_build, validate_weight_shape};
use crate::neural_network::layers::{built_layer_shape_functions, named_weight_layer_functions};
use crate::neural_network::traits::{LayerBase, ParamRef, UnaryLayer};
use crate::neural_network::{Ctx, Shape, Tensor};
use crate::parallel_gates::cheap_map_parallel_threshold;
use ndarray::{ArrayD, Axis, Zip};
#[derive(Debug)]
pub struct PReLU {
input_shape: Vec<usize>,
shared_axes: Vec<usize>,
alpha_init: f32,
alpha: ArrayD<f32>,
built: Option<Shape>,
}
impl PReLU {
pub fn new(alpha: f32) -> Result<Self, Error> {
if !alpha.is_finite() {
return Err(Error::invalid_parameter(
"alpha",
"must be finite, because a non-finite slope makes every negative element \
non-finite",
));
}
Ok(Self {
input_shape: Vec::new(),
shared_axes: Vec::new(),
alpha_init: alpha,
alpha: ArrayD::from_elem(Vec::new(), alpha),
built: None,
})
}
fn allocate_alpha(&mut self) {
let mut param_shape = self.input_shape[1..].to_vec();
for &axis in &self.shared_axes {
param_shape[axis - 1] = 1;
}
self.alpha = ArrayD::from_elem(param_shape, self.alpha_init);
}
pub fn with_shared_axes(mut self, shared_axes: Vec<usize>) -> Result<Self, Error> {
let mut sorted = shared_axes;
sorted.sort_unstable();
for (position, &axis) in sorted.iter().enumerate() {
if axis == 0 {
return Err(Error::invalid_parameter(
"shared_axes",
"holds axis 0, which is the batch axis. Every slope is already shared over \
the batch",
));
}
if position > 0 && sorted[position - 1] == axis {
return Err(Error::invalid_parameter(
"shared_axes",
format!("holds axis {axis} more than 1 time"),
));
}
}
self.shared_axes = sorted;
if self.built.is_some() {
self.allocate_alpha();
}
Ok(self)
}
pub fn set_weights(&mut self, alpha: ArrayD<f32>) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("PReLU"));
}
validate_weight_shape("alpha", self.alpha.shape(), alpha.shape())?;
self.alpha = alpha.as_standard_layout().into_owned();
Ok(())
}
fn validate_input(&self, input: &Tensor) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("PReLU"));
}
if input.is_empty() {
return Err(Error::empty_input("input tensor"));
}
if input.ndim() != self.input_shape.len() {
return Err(Error::invalid_input(format!(
"PReLU layer expects an input of rank {}, got rank {}",
self.input_shape.len(),
input.ndim()
)));
}
for axis in 1..self.input_shape.len() {
if self.shared_axes.contains(&axis) {
continue;
}
if input.shape()[axis] != self.input_shape[axis] {
return Err(Error::invalid_input(format!(
"PReLU layer holds 1 slope per position of axis {axis}, so that axis must \
have extent {}, got {}. Add the axis to shared_axes to accept any extent",
self.input_shape[axis],
input.shape()[axis]
)));
}
}
Ok(())
}
fn activate(&self, input: &Tensor) -> Result<Tensor, Error> {
self.validate_input(input)?;
let slopes = self
.alpha
.broadcast(input.raw_dim())
.expect("validate_input accepts only a shape the slope array covers");
let mut output = Tensor::zeros(input.raw_dim());
let p_relu = |out: &mut f32, &x: &f32, &a: &f32| {
*out = if x >= 0.0 { x } else { a * x };
};
if input.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut output)
.and(input)
.and(&slopes)
.par_for_each(p_relu);
} else {
Zip::from(&mut output)
.and(input)
.and(&slopes)
.for_each(p_relu);
}
Ok(output)
}
}
impl LayerBase for PReLU {
fn layer_type(&self) -> &str {
"PReLU"
}
fn param_count(&self) -> ParamCounts {
ParamCounts::trainable(self.alpha.len())
}
fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
let Self { alpha, .. } = self;
vec![ParamRef::no_decay(
"alpha",
alpha
.as_slice_mut()
.expect("the slopes are kept in C order"),
)]
}
built_layer_shape_functions!();
named_weight_layer_functions!(
trainable "alpha" => alpha,
);
}
impl UnaryLayer for PReLU {
fn build(&mut self, input: &Shape) -> Result<(), Error> {
let Some(built) = start_build(&self.built, "PReLU", input)? else {
return Ok(());
};
built.check_min_rank("PReLU", 2)?;
let (_, tail) = built.split_batch("PReLU")?;
if let Some(axis) = tail.iter().position(|&extent| extent == 0) {
return Err(Error::invalid_input(format!(
"PReLU layer expects every dimension of the input shape to be 1 or more: axis \
{} has extent 0",
axis + 1
)));
}
if let Some(&axis) = self.shared_axes.iter().find(|&&axis| axis > tail.len()) {
return Err(Error::invalid_parameter(
"shared_axes",
format!(
"holds axis {axis}, and the input has rank {}",
tail.len() + 1
),
));
}
let mut dims = vec![1];
dims.extend(tail);
self.input_shape = dims;
self.built = Some(built);
self.allocate_alpha();
Ok(())
}
fn forward(&self, input: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
if self.built.is_none() {
return Err(Error::not_built("PReLU"));
}
let output = self.activate(input)?;
if ctx.is_training() {
ctx.push_cache("PReLU", input.clone());
}
Ok(output)
}
fn backward(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
let Self {
shared_axes, alpha, ..
} = self;
let input: Tensor = ctx.pop_cache("PReLU")?;
if grad_output.shape() != input.shape() {
return Err(Error::shape_mismatch(input.shape(), grad_output.shape()));
}
let slopes = alpha
.broadcast(input.raw_dim())
.expect("the cached input has a shape the slope array covers");
let mut grad_input = Tensor::zeros(input.raw_dim());
let mut contribution = Tensor::zeros(input.raw_dim());
let split = |dx: &mut f32, share: &mut f32, &x: &f32, &g: &f32, &a: &f32| {
if x > 0.0 {
*dx = g;
} else if x < 0.0 {
*dx = g * a;
*share = g * x;
}
};
if input.len() >= cheap_map_parallel_threshold() {
Zip::from(&mut grad_input)
.and(&mut contribution)
.and(&input)
.and(grad_output)
.and(&slopes)
.par_for_each(split);
} else {
Zip::from(&mut grad_input)
.and(&mut contribution)
.and(&input)
.and(grad_output)
.and(&slopes)
.for_each(split);
}
let mut reduced = contribution.sum_axis(Axis(0));
for &axis in shared_axes.iter() {
let target = Axis(axis - 1);
reduced = reduced.sum_axis(target).insert_axis(target);
}
ctx.add_grad("alpha", reduced.as_standard_layout().to_owned().into_dyn())?;
Ok(grad_input)
}
fn compute_output_shape(&self, input: &Shape) -> Result<Shape, Error> {
if self.built.is_none() {
input.check_min_rank("PReLU", 2)?;
if let Some(&axis) = self.shared_axes.iter().find(|&&axis| axis >= input.rank()) {
return Err(Error::invalid_parameter(
"shared_axes",
format!("holds axis {axis}, and the input has rank {}", input.rank()),
));
}
return Ok(input.clone());
}
input.check_rank("PReLU", self.input_shape.len())?;
for (axis, extent) in input.axes().iter().enumerate().skip(1) {
if self.shared_axes.contains(&axis) {
continue;
}
let wanted = self.input_shape[axis];
match extent {
Some(extent) if *extent == wanted => {}
Some(extent) => {
return Err(Error::invalid_input(format!(
"PReLU layer holds 1 slope per position of axis {axis}, so that axis \
must have extent {wanted}, got {extent}. Add the axis to shared_axes \
to accept any extent"
)));
}
None => {
return Err(Error::invalid_input(format!(
"PReLU layer holds 1 slope per position of axis {axis}, so that axis \
needs a fixed extent. Add the axis to shared_axes to accept any extent"
)));
}
}
}
Ok(input.clone())
}
}