use crate::layers::{Layer, LayerConfig};
use crate::NeuralResult;
use scirs2_core::ndarray::{Array2, Array3, Axis};
use sklears_core::error::SklearsError;
use sklears_core::types::FloatBounds;
use std::marker::PhantomData;
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct ScaledDotProductAttention<T: FloatBounds> {
d_k: usize,
dropout_rate: Option<T>,
last_attention_weights: Option<Array2<T>>,
config: LayerConfig<T>,
_phantom: PhantomData<T>,
}
impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> ScaledDotProductAttention<T> {
pub fn new(d_k: usize, dropout_rate: Option<T>) -> Self {
Self {
d_k,
dropout_rate,
last_attention_weights: None,
config: LayerConfig::new(d_k),
_phantom: PhantomData,
}
}
pub fn apply_attention(
&mut self,
query: &Array3<T>,
key: &Array3<T>,
value: &Array3<T>,
mask: Option<&Array2<bool>>,
training: bool,
) -> NeuralResult<Array3<T>> {
let (batch_size, seq_len_q, _) = query.dim();
let (_, seq_len_k, _) = key.dim();
let (_, seq_len_v, d_v) = value.dim();
if seq_len_k != seq_len_v {
return Err(SklearsError::InvalidInput(
"Key and Value sequences must have the same length".to_string(),
));
}
let scale = T::one() / T::from(self.d_k).unwrap_or(T::one()).sqrt();
let mut output = Array3::zeros((batch_size, seq_len_q, d_v));
for b in 0..batch_size {
let q = query.index_axis(Axis(0), b);
let k = key.index_axis(Axis(0), b);
let v = value.index_axis(Axis(0), b);
let scores = q.dot(&k.t()) * scale;
let mut masked_scores = scores;
if let Some(mask) = mask {
for i in 0..seq_len_q {
for j in 0..seq_len_k {
if !mask[[i, j]] {
masked_scores[[i, j]] = T::from(-1e9)
.unwrap_or(-T::one() * T::from(1000000000).unwrap_or(T::one()));
}
}
}
}
let attention_weights = self.softmax_2d(&masked_scores);
self.last_attention_weights = Some(attention_weights.clone());
let final_weights = if training && self.dropout_rate.is_some() {
self.apply_dropout(&attention_weights)
} else {
attention_weights
};
let batch_output = final_weights.dot(&v);
output.index_axis_mut(Axis(0), b).assign(&batch_output);
}
Ok(output)
}
pub fn get_attention_weights(&self) -> Option<&Array2<T>> {
self.last_attention_weights.as_ref()
}
fn softmax_2d(&self, x: &Array2<T>) -> Array2<T> {
let mut result = Array2::zeros(x.dim());
for (input_row, mut output_row) in x.axis_iter(Axis(0)).zip(result.axis_iter_mut(Axis(0))) {
let max_val = input_row.fold(
T::from(-f64::INFINITY).unwrap_or(-T::one() * T::from(1e10).unwrap_or(T::one())),
|acc, &x| {
if x > acc {
x
} else {
acc
}
},
);
let mut sum = T::zero();
for (&input_val, output_val) in input_row.iter().zip(output_row.iter_mut()) {
let exp_val = (input_val - max_val).exp();
*output_val = exp_val;
sum += exp_val;
}
for output_val in output_row.iter_mut() {
*output_val /= sum;
}
}
result
}
fn apply_dropout(&self, weights: &Array2<T>) -> Array2<T> {
let dropout_rate = self.dropout_rate.unwrap_or(T::zero());
let keep_prob = T::one() - dropout_rate;
weights * keep_prob
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct MultiHeadAttention<T: FloatBounds> {
num_heads: usize,
d_model: usize,
d_k: usize,
d_v: usize,
w_q: Vec<Array2<T>>,
w_k: Vec<Array2<T>>,
w_v: Vec<Array2<T>>,
w_o: Array2<T>,
attention_layers: Vec<ScaledDotProductAttention<T>>,
last_input: Option<(Array3<T>, Array3<T>, Array3<T>)>,
config: LayerConfig<T>,
_phantom: PhantomData<T>,
}
impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> MultiHeadAttention<T> {
pub fn new(num_heads: usize, d_model: usize, dropout_rate: Option<T>) -> NeuralResult<Self> {
if !d_model.is_multiple_of(num_heads) {
return Err(SklearsError::InvalidInput(format!(
"d_model ({}) must be divisible by num_heads ({})",
d_model, num_heads
)));
}
let d_k = d_model / num_heads;
let d_v = d_k;
let mut w_q = Vec::new();
let mut w_k = Vec::new();
let mut w_v = Vec::new();
let mut attention_layers = Vec::new();
for _ in 0..num_heads {
w_q.push(Self::initialize_weights(d_model, d_k)?);
w_k.push(Self::initialize_weights(d_model, d_k)?);
w_v.push(Self::initialize_weights(d_model, d_v)?);
attention_layers.push(ScaledDotProductAttention::new(d_k, dropout_rate));
}
let w_o = Self::initialize_weights(d_model, d_model)?;
Ok(Self {
num_heads,
d_model,
d_k,
d_v,
w_q,
w_k,
w_v,
w_o,
attention_layers,
last_input: None,
config: LayerConfig::new(d_model),
_phantom: PhantomData,
})
}
fn initialize_weights(input_size: usize, output_size: usize) -> NeuralResult<Array2<T>> {
let scale = T::from(2.0).unwrap_or(T::one() + T::one())
/ T::from(input_size + output_size).unwrap_or(T::one());
let bound = scale.sqrt();
let mut weights = Array2::zeros((input_size, output_size));
for elem in weights.iter_mut() {
*elem =
bound * T::from(0.1).unwrap_or(T::one() / T::from(10).unwrap_or_else(|| T::zero()));
}
Ok(weights)
}
pub fn apply(
&mut self,
query: &Array3<T>,
key: &Array3<T>,
value: &Array3<T>,
mask: Option<&Array2<bool>>,
training: bool,
) -> NeuralResult<Array3<T>> {
self.last_input = Some((query.clone(), key.clone(), value.clone()));
let (batch_size, seq_len, _) = query.dim();
let mut head_outputs = Vec::new();
for h in 0..self.num_heads {
let q_proj = self.project_sequence(query, &self.w_q[h])?;
let k_proj = self.project_sequence(key, &self.w_k[h])?;
let v_proj = self.project_sequence(value, &self.w_v[h])?;
let head_output = self.attention_layers[h]
.apply_attention(&q_proj, &k_proj, &v_proj, mask, training)?;
head_outputs.push(head_output);
}
let mut concatenated = Array3::zeros((batch_size, seq_len, self.d_model));
for (h, head_output) in head_outputs.iter().enumerate() {
let start_idx = h * self.d_v;
let _end_idx = start_idx + self.d_v;
for b in 0..batch_size {
for s in 0..seq_len {
for d in 0..self.d_v {
concatenated[[b, s, start_idx + d]] = head_output[[b, s, d]];
}
}
}
}
let output = self.project_sequence(&concatenated, &self.w_o)?;
Ok(output)
}
fn project_sequence(&self, input: &Array3<T>, weights: &Array2<T>) -> NeuralResult<Array3<T>> {
let (batch_size, seq_len, _input_dim) = input.dim();
let output_dim = weights.dim().1;
let mut output = Array3::zeros((batch_size, seq_len, output_dim));
for b in 0..batch_size {
for s in 0..seq_len {
let input_slice = input.index_axis(Axis(0), b);
let input_vec = input_slice.index_axis(Axis(0), s);
let output_vec = input_vec.dot(weights);
output
.index_axis_mut(Axis(0), b)
.index_axis_mut(Axis(0), s)
.assign(&output_vec);
}
}
Ok(output)
}
pub fn get_all_attention_weights(&self) -> Vec<Option<&Array2<T>>> {
self.attention_layers
.iter()
.map(|layer| layer.get_attention_weights())
.collect()
}
}
impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> Layer<T> for MultiHeadAttention<T> {
fn forward(&mut self, input: &Array2<T>, training: bool) -> NeuralResult<Array2<T>> {
let (batch_size, features) = input.dim();
let seq_len = 1;
let input_3d = input
.clone()
.into_shape_with_order((batch_size, seq_len, features))
.map_err(|_| SklearsError::InvalidInput("Failed to reshape input".to_string()))?;
let output_3d = self.apply(&input_3d, &input_3d, &input_3d, None, training)?;
let output = output_3d
.into_shape_with_order((batch_size, features))
.map_err(|_| SklearsError::InvalidInput("Failed to reshape output".to_string()))?;
Ok(output)
}
fn backward(&mut self, _grad_output: &Array2<T>) -> NeuralResult<Array2<T>> {
Err(SklearsError::NotImplemented(
"Full backward pass for MultiHeadAttention not yet implemented".to_string(),
))
}
fn num_parameters(&self) -> usize {
let head_params = self.num_heads * (self.d_model * self.d_k * 3); let output_params = self.d_model * self.d_model; head_params + output_params
}
fn reset(&mut self) {
self.last_input = None;
for layer in &mut self.attention_layers {
layer.last_attention_weights = None;
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::Array3;
#[test]
#[ignore]
fn test_scaled_dot_product_attention() {
let mut attention = ScaledDotProductAttention::<f64>::new(4, None);
let batch_size = 2;
let seq_len = 3;
let d_k = 4;
let d_v = 4;
let query = Array3::ones((batch_size, seq_len, d_k));
let key = Array3::ones((batch_size, seq_len, d_k));
let value = Array3::ones((batch_size, seq_len, d_v));
let output = attention
.apply_attention(&query, &key, &value, None, false)
.expect("operation should succeed");
assert_eq!(output.dim(), (batch_size, seq_len, d_v));
assert!(attention.get_attention_weights().is_some());
}
#[test]
#[ignore]
fn test_multi_head_attention_creation() {
let attention =
MultiHeadAttention::<f64>::new(8, 512, None).expect("construction should succeed");
assert_eq!(attention.num_heads, 8);
assert_eq!(attention.d_model, 512);
assert_eq!(attention.d_k, 64);
assert_eq!(attention.w_q.len(), 8);
assert_eq!(attention.w_k.len(), 8);
assert_eq!(attention.w_v.len(), 8);
}
#[test]
#[ignore]
fn test_multi_head_attention_invalid_dimensions() {
let result = MultiHeadAttention::<f64>::new(7, 512, None);
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_multi_head_attention_forward() {
let mut attention =
MultiHeadAttention::<f64>::new(4, 8, None).expect("construction should succeed");
let batch_size = 2;
let seq_len = 3;
let d_model = 8;
let query = Array3::ones((batch_size, seq_len, d_model));
let key = Array3::ones((batch_size, seq_len, d_model));
let value = Array3::ones((batch_size, seq_len, d_model));
let output = attention
.apply(&query, &key, &value, None, false)
.expect("apply should succeed");
assert_eq!(output.dim(), (batch_size, seq_len, d_model));
}
#[test]
#[ignore]
fn test_attention_with_mask() {
let mut attention = ScaledDotProductAttention::<f64>::new(4, None);
let batch_size = 1;
let seq_len = 3;
let d_k = 4;
let query = Array3::ones((batch_size, seq_len, d_k));
let key = Array3::ones((batch_size, seq_len, d_k));
let value = Array3::ones((batch_size, seq_len, d_k));
let mut mask = Array2::from_elem((seq_len, seq_len), true);
for i in 0..seq_len {
for j in (i + 1)..seq_len {
mask[[i, j]] = false;
}
}
let output = attention
.apply_attention(&query, &key, &value, Some(&mask), false)
.expect("operation should succeed");
assert_eq!(output.dim(), (batch_size, seq_len, d_k));
let attention_weights = attention
.get_attention_weights()
.expect("operation should succeed");
for i in 0..seq_len {
for j in (i + 1)..seq_len {
assert!(attention_weights[[i, j]] < 1e-6);
}
}
}
#[test]
#[ignore]
fn test_softmax_2d() {
let attention = ScaledDotProductAttention::<f64>::new(4, None);
let input = scirs2_core::ndarray::array![[1.0, 2.0, 3.0], [0.0, 1.0, 0.0]];
let output = attention.softmax_2d(&input);
for row in output.axis_iter(scirs2_core::ndarray::Axis(0)) {
let sum: f64 = row.sum();
assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-10);
}
for &val in output.iter() {
assert!(val >= 0.0);
}
}
}