use crate::error::Error;
use crate::neural_network::Tensor;
use rayon::iter::{IndexedParallelIterator, 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 channels = t.shape()[t.ndim() - 1].max(1);
let batch = (channel_mask.len() / channels).max(1);
let total = t.len();
let item = total / batch;
let positions = (item / channels).max(1);
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 mut reps = (1024 / channels).clamp(1, positions);
while reps > 1 && !positions.is_multiple_of(reps) {
reps -= 1;
}
let tile_len = reps * channels;
let mut tiles = Vec::with_capacity(batch * tile_len);
for b in 0..batch {
let mask = &channel_mask[b * channels..(b + 1) * channels];
for _ in 0..reps {
tiles.extend(mask.iter().map(|&m| m * scale));
}
}
let apply = |(ci, (o, x)): (usize, (&mut [f32], &[f32]))| {
let b = ci * tile_len / item;
let tile = &tiles[b * tile_len..(b + 1) * tile_len];
for ((o_elem, &x_elem), &f) in o.iter_mut().zip(x).zip(tile) {
*o_elem = x_elem * f;
}
};
if total >= parallel_threshold {
dst.par_chunks_mut(tile_len)
.zip(src.par_chunks(tile_len))
.enumerate()
.for_each(apply);
} else {
dst.chunks_mut(tile_len)
.zip(src.chunks(tile_len))
.enumerate()
.for_each(apply);
}
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_drops_whole_channels() {
let t = Tensor::from_shape_vec(IxDyn(&[1, 3, 4]), vec![1.0f32; 12]).unwrap();
let channel_mask = [1.0f32, 0.0, 1.0, 0.0];
let out = spatial_dropout_scale(&t, &channel_mask, 0.5, usize::MAX);
assert_eq!(
out.iter().copied().collect::<Vec<f32>>(),
vec![2.0, 0.0, 2.0, 0.0, 2.0, 0.0, 2.0, 0.0, 2.0, 0.0, 2.0, 0.0]
);
}
#[test]
fn spatial_dropout_scale_masks_are_per_batch_item() {
let t = Tensor::from_shape_vec(IxDyn(&[2, 1, 2]), vec![1.0f32; 4]).unwrap();
let channel_mask = [1.0f32, 0.0, 0.0, 1.0];
let out = spatial_dropout_scale(&t, &channel_mask, 0.5, usize::MAX);
assert_eq!(
out.iter().copied().collect::<Vec<f32>>(),
vec![2.0, 0.0, 0.0, 2.0]
);
}
#[test]
fn spatial_dropout_scale_parallel_flag_invariant() {
for &(batch, positions, channels) in &[
(1usize, 5usize, 7usize),
(2, 256, 64),
(3, 4096, 1),
(4, 17, 512),
(1, 4093, 3),
] {
let total = batch * positions * channels;
let t = Tensor::from_shape_vec(
IxDyn(&[batch, positions, channels]),
(0..total).map(|i| (i as f32 * 0.013).sin()).collect(),
)
.unwrap();
let channel_mask: Vec<f32> = (0..batch * channels)
.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(),
"parallel flag changed the bits at [{batch}, {positions}, {channels}]"
);
let scale = 1.0 / (1.0 - rate);
let item = positions * channels;
let mut expected = vec![0.0f32; total];
for (i, e) in expected.iter_mut().enumerate() {
let m = channel_mask[(i / item) * channels + i % channels];
let x = t.as_slice().unwrap()[i];
*e = (x * m) * scale;
}
assert_eq!(
serial.as_slice().unwrap(),
expected.as_slice(),
"differs from the explicit two-step form at [{batch}, {positions}, {channels}]"
);
}
}
}