use crate::error::Error;
use crate::neural_network::Tensor;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use rayon::slice::{ParallelSlice, ParallelSliceMut};
fn dropout_backward(
grad_output: &Tensor,
mask: &Option<Tensor>,
training: bool,
rate: f32,
layer_name: &'static str,
) -> Result<Tensor, Error> {
if !training || rate == 0.0 {
return Ok(grad_output.clone());
}
if rate == 1.0 {
return Ok(Tensor::zeros(grad_output.raw_dim()));
}
if let Some(mask) = mask {
let scale = 1.0 / (1.0 - rate);
let grad_input = grad_output * mask * scale;
Ok(grad_input)
} else {
Err(Error::forward_pass_not_run(layer_name))
}
}
fn dropout_output_shape(input_shape: &[usize]) -> String {
if !input_shape.is_empty() {
format!(
"({})",
input_shape
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
)
} else {
String::from("Unknown")
}
}
fn apply_spatial_dropout_threshold(mask_2d: &mut Tensor, rate: f32, parallel_threshold: usize) {
let total_elements = mask_2d.len();
if total_elements >= parallel_threshold {
mask_2d.par_mapv_inplace(|x| if x >= rate { 1.0 } else { 0.0 });
} else {
mask_2d.mapv_inplace(|x| if x >= rate { 1.0 } else { 0.0 });
}
}
fn spatial_dropout_scale(
t: &Tensor,
channel_mask: &[f32],
rate: f32,
parallel_threshold: usize,
) -> Tensor {
let n_segments = channel_mask.len();
let total = t.len();
let segment = total / n_segments;
let scale = 1.0 / (1.0 - rate);
let t_std = t.as_standard_layout();
let src = t_std.as_slice().unwrap();
let mut out = Tensor::zeros(t.raw_dim());
let dst = out.as_slice_mut().unwrap();
let task = |((o, x), &m): ((&mut [f32], &[f32]), &f32)| {
let factor = m * scale;
for (o_elem, &x_elem) in o.iter_mut().zip(x) {
*o_elem = x_elem * factor;
}
};
if total >= parallel_threshold {
dst.par_chunks_mut(segment)
.zip(src.par_chunks(segment))
.zip(channel_mask.par_iter())
.for_each(task);
} else {
dst.chunks_mut(segment)
.zip(src.chunks(segment))
.zip(channel_mask.iter())
.for_each(task);
}
out
}
fn spatial_dropout_backward(
grad_output: &Tensor,
mask: &Option<Tensor>,
training: bool,
rate: f32,
layer_name: &'static str,
parallel_threshold: usize,
) -> Result<Tensor, Error> {
if !training || rate == 0.0 {
return Ok(grad_output.clone());
}
if rate == 1.0 {
return Ok(Tensor::zeros(grad_output.raw_dim()));
}
if let Some(mask) = mask {
let channel_mask = mask
.as_slice()
.expect("per-channel dropout mask is contiguous");
Ok(spatial_dropout_scale(
grad_output,
channel_mask,
rate,
parallel_threshold,
))
} else {
Err(Error::forward_pass_not_run(layer_name))
}
}
#[allow(clippy::module_inception)]
pub mod dropout;
pub mod spatial_dropout_1d;
pub mod spatial_dropout_2d;
pub mod spatial_dropout_3d;
pub use dropout::Dropout;
pub use spatial_dropout_1d::SpatialDropout1D;
pub use spatial_dropout_2d::SpatialDropout2D;
pub use spatial_dropout_3d::SpatialDropout3D;
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
#[test]
fn spatial_dropout_scale_parallel_flag_invariant() {
for &(n_seg, seg) in &[(7usize, 5usize), (64, 256), (33, 4096), (512, 17)] {
let total = n_seg * seg;
let t = Tensor::from_shape_vec(
IxDyn(&[n_seg, seg]),
(0..total).map(|i| (i as f32 * 0.013).sin()).collect(),
)
.unwrap();
let channel_mask: Vec<f32> = (0..n_seg).map(|i| (i % 3 != 0) as u8 as f32).collect();
let rate = 0.25f32;
let serial = spatial_dropout_scale(&t, &channel_mask, rate, usize::MAX);
let parallel = spatial_dropout_scale(&t, &channel_mask, rate, 0);
assert_eq!(
serial.as_slice().unwrap(),
parallel.as_slice().unwrap(),
"spatial_dropout_scale parallel flag changed the bits at [{n_seg}x{seg}]"
);
let scale = 1.0 / (1.0 - rate);
let mut expected = vec![0.0f32; total];
for (i, e) in expected.iter_mut().enumerate() {
let m = channel_mask[i / seg];
let x = t.as_slice().unwrap()[i];
*e = (x * m) * scale;
}
assert_eq!(
serial.as_slice().unwrap(),
expected.as_slice(),
"spatial_dropout_scale differs from the explicit two-step form at [{n_seg}x{seg}]"
);
}
}
}