use std::fmt::Debug;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Float;
use crate::error::{OptimError, Result};
#[derive(Debug, Clone, Copy)]
pub enum OutputTransformation {
Linear,
Tanh,
Sigmoid,
LearnedActivation,
ParameterScaling,
}
#[derive(Debug, Clone)]
pub struct OutputProjectionLayer<T: Float + Debug + Send + Sync + 'static> {
weights: Array2<T>,
bias: Array1<T>,
transformation: OutputTransformation,
}
#[derive(Debug, Clone)]
pub struct InputEmbedding<T: Float + Debug + Send + Sync + 'static> {
weights: Array2<T>,
input_dim: usize,
modeldim: usize,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> OutputProjectionLayer<T> {
pub fn new(input_dim: usize, output_dim: usize) -> Result<Self> {
if input_dim == 0 || output_dim == 0 {
return Err(OptimError::InvalidConfig(
"Output projection dimensions must be positive".to_string(),
));
}
let mut rng = scirs2_core::random::thread_rng();
let mut weights = Array2::zeros((input_dim, output_dim));
let bound = (6.0 / (input_dim + output_dim) as f64).sqrt();
for elem in weights.iter_mut() {
*elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
.unwrap_or_else(|| T::zero());
}
let bias = Array1::zeros(output_dim);
Ok(Self {
weights,
bias,
transformation: OutputTransformation::Linear,
})
}
pub fn new_with_transformation(
input_dim: usize,
output_dim: usize,
transformation: OutputTransformation,
) -> Result<Self> {
let mut layer = Self::new(input_dim, output_dim)?;
layer.transformation = transformation;
Ok(layer)
}
pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
let (seq_len, input_dim) = input.dim();
let (weight_in, weight_out) = self.weights.dim();
if input_dim != weight_in {
return Err(OptimError::InvalidConfig(
"Input dimension doesn't match weight matrix".to_string(),
));
}
let _ = seq_len;
let mut output = input.dot(&self.weights) + &self.bias;
match self.transformation {
OutputTransformation::Linear => {
}
OutputTransformation::Tanh => {
output.mapv_inplace(|x| x.tanh());
}
OutputTransformation::Sigmoid => {
output.mapv_inplace(|x| T::one() / (T::one() + (-x).exp()));
}
OutputTransformation::LearnedActivation => {
output.mapv_inplace(|x| x * (T::one() + (-x.abs()).exp()));
}
OutputTransformation::ParameterScaling => {
for j in 0..weight_out {
let scale: T =
scirs2_core::numeric::NumCast::from(1.0 + 0.1 * (j as f64).sin())
.unwrap_or_else(|| T::one());
for i in 0..output.nrows() {
output[[i, j]] = output[[i, j]] * scale;
}
}
}
}
Ok(output)
}
pub fn transformation(&self) -> OutputTransformation {
self.transformation
}
pub fn set_transformation(&mut self, transformation: OutputTransformation) {
self.transformation = transformation;
}
pub fn weights(&self) -> &Array2<T> {
&self.weights
}
pub fn bias(&self) -> &Array1<T> {
&self.bias
}
pub fn update_parameters(&mut self, weights: Array2<T>, bias: Array1<T>) -> Result<()> {
let (weight_in, weight_out) = weights.dim();
let bias_dim = bias.len();
if weight_out != bias_dim {
return Err(OptimError::InvalidConfig(
"Weight output dimension doesn't match bias dimension".to_string(),
));
}
if (weight_in, weight_out) == self.weights.dim() && bias_dim == self.bias.len() {
self.weights = weights;
self.bias = bias;
Ok(())
} else {
Err(OptimError::InvalidConfig(
"New parameter dimensions don't match current layer dimensions".to_string(),
))
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> InputEmbedding<T> {
pub fn new(input_dim: usize, model_dim: usize) -> Result<Self> {
if input_dim == 0 || model_dim == 0 {
return Err(OptimError::InvalidConfig(
"Input embedding dimensions must be positive".to_string(),
));
}
let mut rng = scirs2_core::random::thread_rng();
let mut weights = Array2::zeros((input_dim, model_dim));
let bound = (6.0 / (input_dim + model_dim) as f64).sqrt();
for elem in weights.iter_mut() {
*elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
.unwrap_or_else(|| T::zero());
}
Ok(Self {
weights,
input_dim,
modeldim: model_dim,
})
}
pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
let (seq_len, input_dim) = input.dim();
if input_dim != self.input_dim {
return Err(OptimError::InvalidConfig(format!(
"Input dimension {} doesn't match embedding input dimension {}",
input_dim, self.input_dim
)));
}
let _ = seq_len;
Ok(input.dot(&self.weights))
}
pub fn input_dim(&self) -> usize {
self.input_dim
}
pub fn model_dim(&self) -> usize {
self.modeldim
}
pub fn weights(&self) -> &Array2<T> {
&self.weights
}
pub fn update_weights(&mut self, weights: Array2<T>) -> Result<()> {
let (weight_in, weight_out) = weights.dim();
if weight_in != self.input_dim || weight_out != self.modeldim {
return Err(OptimError::InvalidConfig(
"New weight dimensions don't match embedding dimensions".to_string(),
));
}
self.weights = weights;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_embedding_produces_nonzero_output() {
let embedding = InputEmbedding::<f64>::new(6, 4).expect("embedding creation");
let input = Array2::<f64>::ones((3, 6));
let output = embedding.forward(&input).expect("forward");
assert_eq!(output.shape(), &[3, 4]);
assert!(
output.iter().any(|&v| v != 0.0),
"Xavier-initialized embedding produced all zeros"
);
}
#[test]
fn output_projection_produces_nonzero_output() {
let projection = OutputProjectionLayer::<f64>::new(6, 4).expect("projection creation");
let input = Array2::<f64>::ones((2, 6));
let output = projection.forward(&input).expect("forward");
assert_eq!(output.shape(), &[2, 4]);
assert!(
output.iter().any(|&v| v != 0.0),
"Xavier-initialized projection produced all zeros"
);
}
#[test]
fn projection_matches_manual_matmul() {
let mut projection = OutputProjectionLayer::<f64>::new(2, 2).expect("projection creation");
let weights =
Array2::<f64>::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape");
let bias = Array1::<f64>::from_vec(vec![0.5, -0.5]);
projection
.update_parameters(weights, bias)
.expect("parameter update");
let input = Array2::<f64>::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("valid shape");
let output = projection.forward(&input).expect("forward");
assert!((output[[0, 0]] - 4.5).abs() < 1e-12);
assert!((output[[0, 1]] - 5.5).abs() < 1e-12);
}
#[test]
fn dimension_mismatches_are_errors() {
let embedding = InputEmbedding::<f64>::new(6, 4).expect("embedding creation");
let input = Array2::<f64>::ones((3, 5));
assert!(embedding.forward(&input).is_err());
assert!(InputEmbedding::<f64>::new(0, 4).is_err());
assert!(OutputProjectionLayer::<f64>::new(4, 0).is_err());
}
#[test]
fn tanh_transformation_bounds_the_output() {
let mut projection = OutputProjectionLayer::<f64>::new(4, 4).expect("projection creation");
projection.set_transformation(OutputTransformation::Tanh);
let input = Array2::<f64>::from_elem((2, 4), 100.0);
let output = projection.forward(&input).expect("forward");
assert!(output.iter().all(|v| v.abs() <= 1.0));
}
}