use kopitiam_core::{DType, Error, Result};
use crate::storage::Storage;
use super::Tensor;
impl Tensor {
pub fn softmax(&self, axis: usize) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let rank = self.rank();
if axis >= rank {
return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
}
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let dims = self.shape.dims();
let axis_len = dims[axis];
let outer: usize = dims[..axis].iter().product();
let inner: usize = dims[axis + 1..].iter().product();
let mut out = vec![0f32; contiguous.len()];
for o in 0..outer {
for inn in 0..inner {
let base = o * axis_len * inner + inn;
let at = |a: usize| contiguous[base + a * inner];
let max = (0..axis_len).map(at).fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0f32;
for a in 0..axis_len {
let exp = (at(a) - max).exp();
out[base + a * inner] = exp;
sum += exp;
}
for a in 0..axis_len {
out[base + a * inner] /= sum;
}
}
}
Tensor::from_f32(out, self.shape.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: f32, b: f32) {
assert!((a - b).abs() < 1e-5, "expected {b}, got {a}");
}
#[test]
fn softmax_rows_sum_to_one() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let out = t.softmax(1).unwrap().to_vec_f32().unwrap();
assert_close(out[0] + out[1] + out[2], 1.0);
assert_close(out[3] + out[4] + out[5], 1.0);
}
#[test]
fn softmax_matches_hand_computation_for_a_simple_row() {
let t = Tensor::from_f32(vec![0.0, 0.0], [2]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert_close(out[0], 0.5);
assert_close(out[1], 0.5);
}
#[test]
fn softmax_preserves_relative_order() {
let t = Tensor::from_f32(vec![1.0, 3.0, 2.0], [3]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert!(out[1] > out[2]);
assert!(out[2] > out[0]);
}
#[test]
fn large_magnitude_input_does_not_overflow_to_nan() {
let t = Tensor::from_f32(vec![1000.0, 1001.0], [2]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert!(!out[0].is_nan() && !out[1].is_nan(), "softmax produced NaN: {out:?}");
assert_close(out[0] + out[1], 1.0);
assert_close(out[0], 0.268_941_4);
}
#[test]
fn softmax_operates_along_an_arbitrary_axis_of_a_3d_tensor() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 2, 2]).unwrap();
let out = t.softmax(1).unwrap().to_vec_f32().unwrap();
assert_close(out[0] + out[2], 1.0);
assert_close(out[1] + out[3], 1.0);
}
}