use scirs2_core::ndarray_ext::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
use scirs2_core::random::Random;
use sklears_core::error::SklearsError;
use sklears_core::traits::{Fit, Predict, PredictProba};
#[derive(Debug, Clone)]
pub struct AutoregressiveModel {
hidden_dims: Vec<usize>,
n_classes: usize,
input_dim: usize,
sequence_length: usize,
learning_rate: f64,
epochs: usize,
regularization: f64,
temperature: f64,
classification_weight: f64,
weights: Vec<Array2<f64>>,
biases: Vec<Array1<f64>>,
class_weights: Array2<f64>,
class_bias: Array1<f64>,
fitted: bool,
}
impl Default for AutoregressiveModel {
fn default() -> Self {
Self::new()
}
}
impl AutoregressiveModel {
pub fn new() -> Self {
Self {
hidden_dims: vec![64, 32],
n_classes: 2,
input_dim: 10,
sequence_length: 10,
learning_rate: 0.001,
epochs: 100,
regularization: 0.01,
temperature: 1.0,
classification_weight: 1.0,
weights: Vec::new(),
biases: Vec::new(),
class_weights: Array2::zeros((0, 0)),
class_bias: Array1::zeros(0),
fitted: false,
}
}
pub fn hidden_dims(mut self, dims: Vec<usize>) -> Self {
self.hidden_dims = dims;
self
}
pub fn n_classes(mut self, n_classes: usize) -> Self {
self.n_classes = n_classes;
self
}
pub fn input_dim(mut self, input_dim: usize) -> Self {
self.input_dim = input_dim;
self
}
pub fn sequence_length(mut self, length: usize) -> Self {
self.sequence_length = length;
self
}
pub fn learning_rate(mut self, lr: f64) -> Self {
self.learning_rate = lr;
self
}
pub fn epochs(mut self, epochs: usize) -> Self {
self.epochs = epochs;
self
}
pub fn regularization(mut self, reg: f64) -> Self {
self.regularization = reg;
self
}
pub fn temperature(mut self, temp: f64) -> Self {
self.temperature = temp;
self
}
pub fn classification_weight(mut self, weight: f64) -> Self {
self.classification_weight = weight;
self
}
fn initialize_parameters(&mut self) -> Result<(), SklearsError> {
let mut layer_dims = vec![self.input_dim];
layer_dims.extend_from_slice(&self.hidden_dims);
layer_dims.push(self.input_dim);
self.weights.clear();
self.biases.clear();
for i in 0..layer_dims.len() - 1 {
let fan_in = layer_dims[i];
let fan_out = layer_dims[i + 1];
let scale = (6.0 / (fan_in + fan_out) as f64).sqrt();
let mut rng = Random::default();
let mut weight = Array2::<f64>::zeros((fan_in, fan_out));
for i in 0..fan_in {
for j in 0..fan_out {
let u: f64 = rng.random_range(0.0..1.0);
weight[(i, j)] = u * (2.0 * scale) - scale;
}
}
let bias = Array1::zeros(fan_out);
self.weights.push(weight);
self.biases.push(bias);
}
let last_hidden_dim = self.hidden_dims.last().unwrap_or(&self.input_dim);
let class_scale = (6.0 / (last_hidden_dim + self.n_classes) as f64).sqrt();
let mut rng = Random::default();
let mut class_weights = Array2::<f64>::zeros((*last_hidden_dim, self.n_classes));
for i in 0..*last_hidden_dim {
for j in 0..self.n_classes {
let u: f64 = rng.random_range(0.0..1.0);
class_weights[(i, j)] = u * (2.0 * class_scale) - class_scale;
}
}
self.class_weights = class_weights;
self.class_bias = Array1::zeros(self.n_classes);
Ok(())
}
fn relu(&self, x: &Array1<f64>) -> Array1<f64> {
x.mapv(|v| v.max(0.0))
}
fn softmax(&self, x: &Array1<f64>) -> Array1<f64> {
let max_val = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let exp_x = x.mapv(|v| ((v - max_val) / self.temperature).exp());
let sum_exp = exp_x.sum();
exp_x / sum_exp
}
fn forward(&self, input: &ArrayView1<f64>) -> Result<(Array1<f64>, Array1<f64>), SklearsError> {
let mut activation = input.to_owned();
let mut activations = vec![activation.clone()];
for (i, (weight, bias)) in self.weights.iter().zip(self.biases.iter()).enumerate() {
let linear = activation.dot(weight) + bias;
if i < self.weights.len() - 1 {
activation = self.relu(&linear);
} else {
activation = linear;
}
activations.push(activation.clone());
}
let feature_layer_idx = self.weights.len() - 1;
let features = &activations[feature_layer_idx];
let class_logits = features.dot(&self.class_weights) + &self.class_bias;
let class_probs = self.softmax(&class_logits);
Ok((activation, class_probs))
}
fn autoregressive_loss(&self, sequence: &ArrayView1<f64>) -> Result<f64, SklearsError> {
let mut total_loss = 0.0;
let seq_len = sequence.len();
if seq_len < 2 {
return Err(SklearsError::InvalidInput(
"Sequence too short for autoregressive modeling".to_string(),
));
}
for i in 1..seq_len {
let context = sequence.slice(s![..i]);
let target = sequence[i];
let mut padded_context = Array1::zeros(self.input_dim);
let copy_len = context.len().min(self.input_dim);
padded_context
.slice_mut(s![..copy_len])
.assign(&context.slice(s![..copy_len]));
let (reconstruction, _) = self.forward(&padded_context.view())?;
let prediction = reconstruction[i % self.input_dim];
total_loss += (prediction - target).powi(2);
}
Ok(total_loss / (seq_len - 1) as f64)
}
pub fn generate_sequence(
&self,
initial_context: &ArrayView1<f64>,
length: usize,
) -> Result<Array1<f64>, SklearsError> {
if !self.fitted {
return Err(SklearsError::NotFitted {
operation: "generating sequences".to_string(),
});
}
let mut sequence = Vec::new();
let mut context = initial_context.to_owned();
for _ in 0..length {
let mut padded_context = Array1::zeros(self.input_dim);
let copy_len = context.len().min(self.input_dim);
padded_context
.slice_mut(s![..copy_len])
.assign(&context.slice(s![..copy_len]));
let (reconstruction, _) = self.forward(&padded_context.view())?;
let next_value = reconstruction[sequence.len() % self.input_dim];
sequence.push(next_value);
if context.len() >= self.sequence_length {
for i in 0..context.len() - 1 {
context[i] = context[i + 1];
}
let context_len = context.len();
context[context_len - 1] = next_value;
} else {
let mut new_context = Array1::zeros(context.len() + 1);
new_context.slice_mut(s![..context.len()]).assign(&context);
new_context[context.len()] = next_value;
context = new_context;
}
}
Ok(Array1::from_vec(sequence))
}
pub fn log_likelihood(&self, sequence: &ArrayView1<f64>) -> Result<f64, SklearsError> {
if !self.fitted {
return Err(SklearsError::NotFitted {
operation: "computing log-likelihood".to_string(),
});
}
let mut log_likelihood = 0.0;
let seq_len = sequence.len();
if seq_len < 2 {
return Err(SklearsError::InvalidInput(
"Sequence too short for log-likelihood computation".to_string(),
));
}
for i in 1..seq_len {
let context = sequence.slice(s![..i]);
let target = sequence[i];
let mut padded_context = Array1::zeros(self.input_dim);
let copy_len = context.len().min(self.input_dim);
padded_context
.slice_mut(s![..copy_len])
.assign(&context.slice(s![..copy_len]));
let (reconstruction, _) = self.forward(&padded_context.view())?;
let prediction = reconstruction[i % self.input_dim];
let diff = prediction - target;
log_likelihood -= 0.5 * diff * diff + 0.5 * (2.0 * std::f64::consts::PI).ln();
}
Ok(log_likelihood)
}
}
impl Fit<ArrayView2<'_, f64>, ArrayView1<'_, i32>> for AutoregressiveModel {
type Fitted = AutoregressiveModel;
#[allow(non_snake_case)] fn fit(self, X: &ArrayView2<f64>, y: &ArrayView1<i32>) -> Result<Self::Fitted, SklearsError> {
if X.nrows() != y.len() {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
let mut model = self;
model.input_dim = X.ncols();
model.initialize_parameters()?;
let n_samples = X.nrows();
let labeled_mask: Vec<bool> = y.iter().map(|&label| label != -1).collect();
let n_labeled = labeled_mask.iter().filter(|&&labeled| labeled).count();
if n_labeled == 0 {
return Err(SklearsError::InvalidInput(
"At least one labeled sample required".to_string(),
));
}
for epoch in 0..model.epochs {
let mut total_loss = 0.0;
let mut n_processed = 0;
for i in 0..n_samples {
let sample = X.row(i);
let label = y[i];
let reconstruction_loss = model.autoregressive_loss(&sample)?;
total_loss += reconstruction_loss;
if labeled_mask[i] {
let (_, class_probs) = model.forward(&sample)?;
let target_class = label as usize;
if target_class >= model.n_classes {
return Err(SklearsError::InvalidInput(format!(
"Label {} exceeds number of classes {}",
target_class, model.n_classes
)));
}
let class_loss = -class_probs[target_class].ln();
total_loss += model.classification_weight * class_loss;
}
n_processed += 1;
}
if epoch % 10 == 0 {
println!(
"Epoch {}: Average loss = {:.4}",
epoch,
total_loss / n_processed as f64
);
}
for weight in &mut model.weights {
weight.mapv_inplace(|w| w * (1.0 - model.learning_rate * model.regularization));
}
}
model.fitted = true;
Ok(model)
}
}
impl Predict<ArrayView2<'_, f64>, Array1<i32>> for AutoregressiveModel {
#[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<f64>) -> Result<Array1<i32>, SklearsError> {
if !self.fitted {
return Err(SklearsError::NotFitted {
operation: "making predictions".to_string(),
});
}
let mut predictions = Array1::zeros(X.nrows());
for (i, sample) in X.axis_iter(Axis(0)).enumerate() {
let (_, class_probs) = self.forward(&sample)?;
let predicted_class = class_probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
.expect("operation should succeed")
.0;
predictions[i] = predicted_class as i32;
}
Ok(predictions)
}
}
impl PredictProba<ArrayView2<'_, f64>, Array2<f64>> for AutoregressiveModel {
#[allow(non_snake_case)] fn predict_proba(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>, SklearsError> {
if !self.fitted {
return Err(SklearsError::NotFitted {
operation: "making predictions".to_string(),
});
}
let mut probabilities = Array2::zeros((X.nrows(), self.n_classes));
for (i, sample) in X.axis_iter(Axis(0)).enumerate() {
let (_, class_probs) = self.forward(&sample)?;
probabilities.row_mut(i).assign(&class_probs);
}
Ok(probabilities)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::array;
#[test]
fn test_autoregressive_model_creation() {
let model = AutoregressiveModel::new()
.hidden_dims(vec![32, 16])
.n_classes(3)
.input_dim(5)
.sequence_length(8)
.learning_rate(0.01)
.epochs(50)
.regularization(0.1)
.temperature(0.8)
.classification_weight(2.0);
assert_eq!(model.hidden_dims, vec![32, 16]);
assert_eq!(model.n_classes, 3);
assert_eq!(model.input_dim, 5);
assert_eq!(model.sequence_length, 8);
assert_eq!(model.learning_rate, 0.01);
assert_eq!(model.epochs, 50);
assert_eq!(model.regularization, 0.1);
assert_eq!(model.temperature, 0.8);
assert_eq!(model.classification_weight, 2.0);
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_fit_predict() {
let X = array![
[1.0, 2.0, 3.0],
[2.0, 3.0, 4.0],
[3.0, 4.0, 5.0],
[4.0, 5.0, 6.0]
];
let y = array![0, 1, -1, 0];
let model = AutoregressiveModel::new()
.n_classes(2)
.input_dim(3)
.epochs(10)
.learning_rate(0.01);
let fitted_model = model
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted_model
.predict(&X.view())
.expect("operation should succeed");
let probabilities = fitted_model
.predict_proba(&X.view())
.expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert_eq!(probabilities.dim(), (4, 2));
for i in 0..4 {
let sum: f64 = probabilities.row(i).sum();
assert!((sum - 1.0).abs() < 1e-6);
}
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_insufficient_labeled_samples() {
let X = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0]];
let y = array![-1, -1];
let model = AutoregressiveModel::new()
.n_classes(2)
.input_dim(3)
.epochs(10);
let result = model.fit(&X.view(), &y.view());
assert!(result.is_err());
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_invalid_dimensions() {
let X = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0]];
let y = array![0];
let model = AutoregressiveModel::new();
let result = model.fit(&X.view(), &y.view());
assert!(result.is_err());
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_generate_sequence() {
let X = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0], [3.0, 4.0, 5.0]];
let y = array![0, 1, 0];
let model = AutoregressiveModel::new()
.n_classes(2)
.input_dim(3)
.epochs(5);
let fitted_model = model
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let initial_context = array![1.0, 2.0];
let sequence = fitted_model
.generate_sequence(&initial_context.view(), 5)
.expect("operation should succeed");
assert_eq!(sequence.len(), 5);
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_log_likelihood() {
let X = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0]];
let y = array![0, 1];
let model = AutoregressiveModel::new()
.n_classes(2)
.input_dim(3)
.epochs(5);
let fitted_model = model
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let sequence = array![1.0, 2.0, 3.0, 4.0];
let log_likelihood = fitted_model
.log_likelihood(&sequence.view())
.expect("operation should succeed");
assert!(log_likelihood.is_finite());
}
#[test]
fn test_softmax_computation() {
let model = AutoregressiveModel::new().temperature(1.0);
let logits = array![1.0, 2.0, 3.0];
let probs = model.softmax(&logits);
let sum: f64 = probs.sum();
assert!((sum - 1.0).abs() < 1e-10);
assert!(probs[0] < probs[1]);
assert!(probs[1] < probs[2]);
}
#[test]
fn test_relu_activation() {
let model = AutoregressiveModel::new();
let input = array![-1.0, 0.0, 1.0, 2.0];
let output = model.relu(&input);
assert_eq!(output, array![0.0, 0.0, 1.0, 2.0]);
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_not_fitted_error() {
let model = AutoregressiveModel::new();
let X = array![[1.0, 2.0, 3.0]];
let result = model.predict(&X.view());
assert!(result.is_err());
let result = model.predict_proba(&X.view());
assert!(result.is_err());
let sequence = array![1.0, 2.0, 3.0];
let result = model.generate_sequence(&sequence.view(), 5);
assert!(result.is_err());
let result = model.log_likelihood(&sequence.view());
assert!(result.is_err());
}
#[test]
#[allow(non_snake_case)]
fn test_autoregressive_model_with_different_parameters() {
let X = array![
[1.0, 2.0, 3.0, 4.0],
[2.0, 3.0, 4.0, 5.0],
[3.0, 4.0, 5.0, 6.0]
];
let y = array![0, 1, 2];
let model = AutoregressiveModel::new()
.hidden_dims(vec![8, 4])
.n_classes(3)
.input_dim(4)
.sequence_length(6)
.learning_rate(0.1)
.epochs(3)
.regularization(0.01)
.temperature(0.5)
.classification_weight(0.5);
let fitted_model = model
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted_model
.predict(&X.view())
.expect("operation should succeed");
let probabilities = fitted_model
.predict_proba(&X.view())
.expect("operation should succeed");
assert_eq!(predictions.len(), 3);
assert_eq!(probabilities.dim(), (3, 3));
}
#[test]
fn test_autoregressive_loss_computation() {
let model = AutoregressiveModel::new().input_dim(3).hidden_dims(vec![4]);
let mut model = model.clone();
model
.initialize_parameters()
.expect("operation should succeed");
let sequence = array![1.0, 2.0, 3.0, 4.0];
let loss = model
.autoregressive_loss(&sequence.view())
.expect("operation should succeed");
assert!(loss >= 0.0);
assert!(loss.is_finite());
}
#[test]
fn test_autoregressive_loss_short_sequence() {
let model = AutoregressiveModel::new().input_dim(3).hidden_dims(vec![4]);
let mut model = model.clone();
model
.initialize_parameters()
.expect("operation should succeed");
let sequence = array![1.0]; let result = model.autoregressive_loss(&sequence.view());
assert!(result.is_err());
}
}