mod attention;
mod callbacks;
mod conv;
mod layers;
mod quantum_layers;
mod rnn;
mod schedules;
pub use attention::*;
pub use callbacks::*;
pub use conv::*;
pub use layers::*;
pub use quantum_layers::*;
pub use rnn::*;
pub use schedules::*;
use crate::error::{MLError, Result};
use scirs2_core::ndarray::{s, ArrayD, Axis, IxDyn};
use std::collections::HashMap;
pub trait KerasLayer: Send + Sync {
fn build(&mut self, input_shape: &[usize]) -> Result<()>;
fn call(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>>;
fn compute_output_shape(&self, input_shape: &[usize]) -> Vec<usize>;
fn name(&self) -> &str;
fn get_weights(&self) -> Vec<ArrayD<f64>>;
fn set_weights(&mut self, weights: Vec<ArrayD<f64>>) -> Result<()>;
fn count_params(&self) -> usize {
self.get_weights().iter().map(|w| w.len()).sum()
}
fn built(&self) -> bool;
}
#[derive(Debug, Clone)]
pub enum ActivationFunction {
Linear,
ReLU,
Sigmoid,
Tanh,
Softmax,
LeakyReLU(f64),
ELU(f64),
}
#[derive(Debug, Clone)]
pub enum InitializerType {
Zeros,
Ones,
GlorotUniform,
GlorotNormal,
HeUniform,
}
pub struct Sequential {
layers: Vec<Box<dyn KerasLayer>>,
name: String,
built: bool,
compiled: bool,
input_shape: Option<Vec<usize>>,
loss: Option<LossFunction>,
optimizer: Option<OptimizerType>,
metrics: Vec<MetricType>,
}
impl Sequential {
pub fn new() -> Self {
Self {
layers: Vec::new(),
name: format!("sequential_{}", fastrand::u32(..)),
built: false,
compiled: false,
input_shape: None,
loss: None,
optimizer: None,
metrics: Vec::new(),
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn add(&mut self, layer: Box<dyn KerasLayer>) {
self.layers.push(layer);
self.built = false;
}
pub fn build(&mut self, input_shape: Vec<usize>) -> Result<()> {
self.input_shape = Some(input_shape.clone());
let mut current_shape = input_shape;
for layer in &mut self.layers {
layer.build(¤t_shape)?;
current_shape = layer.compute_output_shape(¤t_shape);
}
self.built = true;
Ok(())
}
pub fn compile(
mut self,
loss: LossFunction,
optimizer: OptimizerType,
metrics: Vec<MetricType>,
) -> Self {
self.loss = Some(loss);
self.optimizer = Some(optimizer);
self.metrics = metrics;
self.compiled = true;
self
}
pub fn summary(&self) -> ModelSummary {
let mut layers_info = Vec::new();
let mut total_params = 0;
let mut trainable_params = 0;
let mut current_shape = self.input_shape.clone().unwrap_or_default();
for layer in &self.layers {
let output_shape = layer.compute_output_shape(¤t_shape);
let params = layer.count_params();
layers_info.push(LayerInfo {
name: layer.name().to_string(),
layer_type: "Layer".to_string(),
output_shape: output_shape.clone(),
param_count: params,
});
total_params += params;
trainable_params += params;
current_shape = output_shape;
}
ModelSummary {
layers: layers_info,
total_params,
trainable_params,
non_trainable_params: 0,
}
}
pub fn predict(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>> {
if !self.built {
return Err(MLError::InvalidConfiguration(
"Model must be built before prediction".to_string(),
));
}
let mut current = inputs.clone();
for layer in &self.layers {
current = layer.call(¤t)?;
}
Ok(current)
}
#[allow(non_snake_case)]
pub fn fit(
&mut self,
X: &ArrayD<f64>,
y: &ArrayD<f64>,
epochs: usize,
batch_size: Option<usize>,
validation_data: Option<(&ArrayD<f64>, &ArrayD<f64>)>,
callbacks: Vec<Box<dyn Callback>>,
) -> Result<TrainingHistory> {
if !self.compiled {
return Err(MLError::InvalidConfiguration(
"Model must be compiled before training".to_string(),
));
}
let batch_size = batch_size.unwrap_or(32);
let n_samples = X.shape()[0];
let n_batches = (n_samples + batch_size - 1) / batch_size;
let mut history = TrainingHistory::new();
for epoch in 0..epochs {
let mut epoch_loss = 0.0;
let mut epoch_metrics: HashMap<String, f64> = HashMap::new();
for metric in &self.metrics {
epoch_metrics.insert(metric.name(), 0.0);
}
for batch_idx in 0..n_batches {
let start_idx = batch_idx * batch_size;
let end_idx = ((batch_idx + 1) * batch_size).min(n_samples);
let X_batch = X.slice(s![start_idx..end_idx, ..]);
let y_batch = y.slice(s![start_idx..end_idx, ..]);
let predictions = self.predict(&X_batch.to_owned().into_dyn())?;
let loss = self.compute_loss(&predictions, &y_batch.to_owned().into_dyn())?;
epoch_loss += loss;
self.backward_pass(&predictions, &y_batch.to_owned().into_dyn())?;
for metric in &self.metrics {
let metric_value =
metric.compute(&predictions, &y_batch.to_owned().into_dyn())?;
*epoch_metrics.entry(metric.name()).or_insert(0.0) += metric_value;
}
}
epoch_loss /= n_batches as f64;
for value in epoch_metrics.values_mut() {
*value /= n_batches as f64;
}
let (val_loss, val_metrics) = if let Some((X_val, y_val)) = validation_data {
let val_predictions = self.predict(X_val)?;
let val_loss = self.compute_loss(&val_predictions, y_val)?;
let mut val_metrics = HashMap::new();
for metric in &self.metrics {
let metric_value = metric.compute(&val_predictions, y_val)?;
val_metrics.insert(format!("val_{}", metric.name()), metric_value);
}
(Some(val_loss), val_metrics)
} else {
(None, HashMap::new())
};
history.add_epoch(epoch_loss, epoch_metrics, val_loss, val_metrics);
for callback in &callbacks {
callback.on_epoch_end(epoch, &history)?;
}
println!("Epoch {}/{} - loss: {:.4}", epoch + 1, epochs, epoch_loss);
}
Ok(history)
}
#[allow(non_snake_case)]
pub fn evaluate(
&self,
X: &ArrayD<f64>,
y: &ArrayD<f64>,
_batch_size: Option<usize>,
) -> Result<HashMap<String, f64>> {
let predictions = self.predict(X)?;
let loss = self.compute_loss(&predictions, y)?;
let mut results = HashMap::new();
results.insert("loss".to_string(), loss);
for metric in &self.metrics {
let metric_value = metric.compute(&predictions, y)?;
results.insert(metric.name(), metric_value);
}
Ok(results)
}
fn compute_loss(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
if let Some(ref loss_fn) = self.loss {
loss_fn.compute(predictions, targets)
} else {
Err(MLError::InvalidConfiguration(
"Loss function not specified".to_string(),
))
}
}
fn backward_pass(&mut self, _predictions: &ArrayD<f64>, _targets: &ArrayD<f64>) -> Result<()> {
Ok(())
}
}
impl Default for Sequential {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum LossFunction {
MeanSquaredError,
BinaryCrossentropy,
CategoricalCrossentropy,
SparseCategoricalCrossentropy,
MeanAbsoluteError,
Huber(f64),
}
impl LossFunction {
pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
match self {
LossFunction::MeanSquaredError => {
let diff = predictions - targets;
diff.mapv(|x| x * x).mean().ok_or_else(|| {
MLError::ComputationError("Failed to compute mean of empty array".to_string())
})
}
LossFunction::BinaryCrossentropy => {
let epsilon = 1e-15;
let clipped_preds = predictions.mapv(|x| x.max(epsilon).min(1.0 - epsilon));
let loss = targets * clipped_preds.mapv(|x| x.ln())
+ (1.0 - targets) * clipped_preds.mapv(|x| (1.0 - x).ln());
loss.mean().map(|m| -m).ok_or_else(|| {
MLError::ComputationError("Failed to compute mean of empty array".to_string())
})
}
LossFunction::MeanAbsoluteError => {
let diff = predictions - targets;
diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
MLError::ComputationError("Failed to compute mean of empty array".to_string())
})
}
_ => Err(MLError::InvalidConfiguration(
"Loss function not implemented".to_string(),
)),
}
}
}
#[derive(Debug, Clone)]
pub enum OptimizerType {
SGD { learning_rate: f64, momentum: f64 },
Adam {
learning_rate: f64,
beta1: f64,
beta2: f64,
epsilon: f64,
},
RMSprop {
learning_rate: f64,
rho: f64,
epsilon: f64,
},
AdaGrad { learning_rate: f64, epsilon: f64 },
}
#[derive(Debug, Clone)]
pub enum MetricType {
Accuracy,
Precision,
Recall,
F1Score,
MeanAbsoluteError,
MeanSquaredError,
}
impl MetricType {
pub fn name(&self) -> String {
match self {
MetricType::Accuracy => "accuracy".to_string(),
MetricType::Precision => "precision".to_string(),
MetricType::Recall => "recall".to_string(),
MetricType::F1Score => "f1_score".to_string(),
MetricType::MeanAbsoluteError => "mean_absolute_error".to_string(),
MetricType::MeanSquaredError => "mean_squared_error".to_string(),
}
}
pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
match self {
MetricType::Accuracy => {
let pred_classes = predictions.mapv(|x| if x > 0.5 { 1.0 } else { 0.0 });
let correct = pred_classes
.iter()
.zip(targets.iter())
.filter(|(&pred, &target)| (pred - target).abs() < 1e-6)
.count();
Ok(correct as f64 / targets.len() as f64)
}
MetricType::MeanAbsoluteError => {
let diff = predictions - targets;
diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
MLError::ComputationError("Failed to compute mean of empty array".to_string())
})
}
MetricType::MeanSquaredError => {
let diff = predictions - targets;
diff.mapv(|x| x * x).mean().ok_or_else(|| {
MLError::ComputationError("Failed to compute mean of empty array".to_string())
})
}
_ => Err(MLError::InvalidConfiguration(
"Metric not implemented".to_string(),
)),
}
}
}
#[derive(Debug, Clone)]
pub struct TrainingHistory {
pub loss: Vec<f64>,
pub metrics: Vec<HashMap<String, f64>>,
pub val_loss: Vec<f64>,
pub val_metrics: Vec<HashMap<String, f64>>,
}
impl TrainingHistory {
pub fn new() -> Self {
Self {
loss: Vec::new(),
metrics: Vec::new(),
val_loss: Vec::new(),
val_metrics: Vec::new(),
}
}
pub fn add_epoch(
&mut self,
loss: f64,
metrics: HashMap<String, f64>,
val_loss: Option<f64>,
val_metrics: HashMap<String, f64>,
) {
self.loss.push(loss);
self.metrics.push(metrics);
if let Some(val_loss) = val_loss {
self.val_loss.push(val_loss);
}
self.val_metrics.push(val_metrics);
}
}
impl Default for TrainingHistory {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ModelSummary {
pub layers: Vec<LayerInfo>,
pub total_params: usize,
pub trainable_params: usize,
pub non_trainable_params: usize,
}
#[derive(Debug)]
pub struct LayerInfo {
pub name: String,
pub layer_type: String,
pub output_shape: Vec<usize>,
pub param_count: usize,
}
pub struct Input {
pub shape: Vec<usize>,
pub name: Option<String>,
pub dtype: DataType,
}
impl Input {
pub fn new(shape: Vec<usize>) -> Self {
Self {
shape,
name: None,
dtype: DataType::Float64,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn dtype(mut self, dtype: DataType) -> Self {
self.dtype = dtype;
self
}
}
#[derive(Debug, Clone)]
pub enum DataType {
Float32,
Float64,
Int32,
Int64,
}
pub mod utils {
use super::*;
pub fn create_classification_model(
_input_dim: usize,
num_classes: usize,
hidden_layers: Vec<usize>,
) -> Sequential {
let mut model = Sequential::new();
for (i, &units) in hidden_layers.iter().enumerate() {
model.add(Box::new(
Dense::new(units)
.activation(ActivationFunction::ReLU)
.name(format!("dense_{}", i)),
));
}
let output_activation = if num_classes == 2 {
ActivationFunction::Sigmoid
} else {
ActivationFunction::Softmax
};
model.add(Box::new(
Dense::new(num_classes)
.activation(output_activation)
.name("output"),
));
model
}
pub fn create_quantum_model(
num_qubits: usize,
num_classes: usize,
num_layers: usize,
) -> Sequential {
let mut model = Sequential::new();
model.add(Box::new(
QuantumDense::new(num_qubits, num_classes)
.num_layers(num_layers)
.ansatz_type(QuantumAnsatzType::HardwareEfficient)
.name("quantum_layer"),
));
if num_classes > 1 {
model.add(Box::new(
Activation::new(ActivationFunction::Softmax).name("softmax"),
));
}
model
}
pub fn create_hybrid_model(
_input_dim: usize,
num_qubits: usize,
num_classes: usize,
classical_hidden: Vec<usize>,
) -> Sequential {
let mut model = Sequential::new();
for (i, &units) in classical_hidden.iter().enumerate() {
model.add(Box::new(
Dense::new(units)
.activation(ActivationFunction::ReLU)
.name(format!("classical_{}", i)),
));
}
model.add(Box::new(
QuantumDense::new(num_qubits, 64)
.num_layers(2)
.ansatz_type(QuantumAnsatzType::HardwareEfficient)
.name("quantum_layer"),
));
model.add(Box::new(
Dense::new(num_classes)
.activation(if num_classes == 2 {
ActivationFunction::Sigmoid
} else {
ActivationFunction::Softmax
})
.name("output"),
));
model
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array;
#[test]
fn test_dense_layer() {
let mut dense = Dense::new(10)
.activation(ActivationFunction::ReLU)
.name("test_dense");
assert!(!dense.built());
dense.build(&[5]).expect("Should build successfully");
assert!(dense.built());
assert_eq!(dense.compute_output_shape(&[32, 5]), vec![32, 10]);
}
#[test]
fn test_sequential_model() {
let mut model = Sequential::new();
model.add(Box::new(Dense::new(10)));
model.add(Box::new(Activation::new(ActivationFunction::ReLU)));
model.add(Box::new(Dense::new(5)));
model
.build(vec![32, 20])
.expect("Should build successfully");
let summary = model.summary();
assert_eq!(summary.layers.len(), 3);
}
#[test]
fn test_activation_functions() {
let relu = ActivationFunction::ReLU;
let sigmoid = ActivationFunction::Sigmoid;
let _tanh = ActivationFunction::Tanh;
let mut act_relu = Activation::new(relu);
act_relu.build(&[10]).expect("Should build");
let mut act_sigmoid = Activation::new(sigmoid);
act_sigmoid.build(&[10]).expect("Should build");
}
}