use crate::activation::Activation;
use crate::weight_init::InitStrategy;
use crate::NeuralResult;
use sklears_core::error::SklearsError;
use sklears_core::types::FloatBounds;
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConfigFormat {
JSON,
YAML,
}
impl ConfigFormat {
pub fn from_path<P: AsRef<Path>>(path: P) -> ConfigFormat {
let path = path.as_ref();
match path.extension().and_then(|s| s.to_str()) {
Some("yaml") | Some("yml") => ConfigFormat::YAML,
Some("json") => ConfigFormat::JSON,
_ => ConfigFormat::JSON, }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct NeuralNetworkConfig<T: FloatBounds> {
pub model: ModelConfig<T>,
pub training: TrainingConfig<T>,
pub optimizer: OptimizerConfig<T>,
pub data: DataConfig,
pub evaluation: EvaluationConfig,
pub misc: MiscConfig,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct ModelConfig<T: FloatBounds> {
pub model_type: String,
pub input_dim: usize,
pub output_dim: usize,
pub hidden_layers: Vec<usize>,
pub activation: String,
pub dropout: Option<T>,
pub batch_norm: bool,
pub layer_norm: bool,
pub weight_init: String,
pub arch_params: HashMap<String, String>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct TrainingConfig<T: FloatBounds> {
pub epochs: usize,
pub batch_size: usize,
pub learning_rate: T,
pub lr_schedule: LearningRateSchedule<T>,
pub early_stopping: Option<EarlyStoppingConfig<T>>,
pub validation_split: Option<T>,
pub random_seed: Option<u64>,
pub mixed_precision: bool,
pub gradient_clipping: Option<T>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct LearningRateSchedule<T: FloatBounds> {
pub schedule_type: String,
pub step_size: Option<usize>,
pub decay_factor: Option<T>,
pub min_lr: Option<T>,
pub max_lr: Option<T>,
pub warmup_steps: Option<usize>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct EarlyStoppingConfig<T: FloatBounds> {
pub monitor: String,
pub patience: usize,
pub min_delta: T,
pub mode: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound = "T: FloatBounds + serde::Serialize + serde::de::DeserializeOwned")
)]
pub struct OptimizerConfig<T: FloatBounds> {
pub optimizer_type: String,
pub momentum: Option<T>,
pub beta1: Option<T>,
pub beta2: Option<T>,
pub weight_decay: Option<T>,
pub epsilon: Option<T>,
pub nesterov: bool,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct DataConfig {
pub normalization: Option<String>,
pub augmentation: Vec<DataAugmentationConfig>,
pub shuffle: bool,
pub num_workers: usize,
pub pin_memory: bool,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct DataAugmentationConfig {
pub aug_type: String,
pub probability: f64,
pub params: HashMap<String, String>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct EvaluationConfig {
pub metrics: Vec<String>,
pub confusion_matrix: bool,
pub classification_report: bool,
pub test_batch_size: Option<usize>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct MiscConfig {
pub verbose: bool,
pub log_frequency: usize,
pub checkpoint_frequency: Option<usize>,
pub checkpoint_dir: Option<String>,
pub device: String,
pub deterministic: bool,
}
impl<T: FloatBounds> Default for NeuralNetworkConfig<T> {
fn default() -> Self {
Self {
model: ModelConfig::default(),
training: TrainingConfig::default(),
optimizer: OptimizerConfig::default(),
data: DataConfig::default(),
evaluation: EvaluationConfig::default(),
misc: MiscConfig::default(),
}
}
}
impl<T: FloatBounds> Default for ModelConfig<T> {
fn default() -> Self {
Self {
model_type: "mlp".to_string(),
input_dim: 784,
output_dim: 10,
hidden_layers: vec![128, 64],
activation: "relu".to_string(),
dropout: Some(T::from(0.2).unwrap_or_else(|| T::zero())),
batch_norm: false,
layer_norm: false,
weight_init: "xavier_uniform".to_string(),
arch_params: HashMap::new(),
}
}
}
impl<T: FloatBounds> Default for TrainingConfig<T> {
fn default() -> Self {
Self {
epochs: 100,
batch_size: 32,
learning_rate: T::from(0.001).unwrap_or_else(|| T::zero()),
lr_schedule: LearningRateSchedule::default(),
early_stopping: None,
validation_split: Some(T::from(0.2).unwrap_or_else(|| T::zero())),
random_seed: Some(42),
mixed_precision: false,
gradient_clipping: None,
}
}
}
impl<T: FloatBounds> Default for LearningRateSchedule<T> {
fn default() -> Self {
Self {
schedule_type: "constant".to_string(),
step_size: None,
decay_factor: None,
min_lr: None,
max_lr: None,
warmup_steps: None,
}
}
}
impl<T: FloatBounds> Default for OptimizerConfig<T> {
fn default() -> Self {
Self {
optimizer_type: "adam".to_string(),
momentum: None,
beta1: Some(T::from(0.9).unwrap_or_else(|| T::zero())),
beta2: Some(T::from(0.999).unwrap_or_else(|| T::zero())),
weight_decay: Some(T::from(1e-4).unwrap_or_else(|| T::zero())),
epsilon: Some(T::from(1e-8).unwrap_or_else(|| T::zero())),
nesterov: false,
}
}
}
impl Default for DataConfig {
fn default() -> Self {
Self {
normalization: Some("standard".to_string()),
augmentation: Vec::new(),
shuffle: true,
num_workers: 1,
pin_memory: false,
}
}
}
impl Default for EvaluationConfig {
fn default() -> Self {
Self {
metrics: vec!["accuracy".to_string(), "loss".to_string()],
confusion_matrix: false,
classification_report: false,
test_batch_size: None,
}
}
}
impl Default for MiscConfig {
fn default() -> Self {
Self {
verbose: true,
log_frequency: 10,
checkpoint_frequency: None,
checkpoint_dir: None,
device: "cpu".to_string(),
deterministic: false,
}
}
}
pub struct ConfigManager;
impl ConfigManager {
#[cfg(feature = "serde")]
pub fn load_config<T: FloatBounds + for<'de> serde::Deserialize<'de>>(
path: &str,
) -> NeuralResult<NeuralNetworkConfig<T>> {
let format = ConfigFormat::from_path(path);
let content = std::fs::read_to_string(path).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to read config file: {}", e))
})?;
match format {
ConfigFormat::JSON => serde_json::from_str(&content)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to parse JSON: {}", e))),
ConfigFormat::YAML => serde_yaml::from_str(&content)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to parse YAML: {}", e))),
}
}
#[cfg(feature = "serde")]
pub fn save_config<T: FloatBounds + serde::Serialize>(
config: &NeuralNetworkConfig<T>,
path: &str,
) -> NeuralResult<()> {
let format = ConfigFormat::from_path(path);
let content = match format {
ConfigFormat::JSON => serde_json::to_string_pretty(config).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to serialize to JSON: {}", e))
})?,
ConfigFormat::YAML => serde_yaml::to_string(config).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to serialize to YAML: {}", e))
})?,
};
std::fs::write(path, content).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to write config file: {}", e))
})?;
Ok(())
}
#[cfg(feature = "serde")]
pub fn create_template<T: FloatBounds + serde::Serialize + Default>(
path: &str,
model_type: &str,
) -> NeuralResult<()> {
let mut config = NeuralNetworkConfig::<T>::default();
config.model.model_type = model_type.to_string();
match model_type {
"transformer" => {
config
.model
.arch_params
.insert("num_heads".to_string(), "8".to_string());
config
.model
.arch_params
.insert("d_model".to_string(), "512".to_string());
config
.model
.arch_params
.insert("num_layers".to_string(), "6".to_string());
config.training.learning_rate = T::from(1e-4).unwrap_or_else(|| T::zero());
}
"rnn" | "lstm" | "gru" => {
config
.model
.arch_params
.insert("sequence_length".to_string(), "50".to_string());
config
.model
.arch_params
.insert("bidirectional".to_string(), "false".to_string());
}
"cnn" => {
config
.model
.arch_params
.insert("kernel_size".to_string(), "3".to_string());
config
.model
.arch_params
.insert("stride".to_string(), "1".to_string());
config
.model
.arch_params
.insert("padding".to_string(), "1".to_string());
}
_ => {} }
Self::save_config(&config, path)
}
pub fn validate_config<T: FloatBounds>(config: &NeuralNetworkConfig<T>) -> NeuralResult<()> {
if config.model.input_dim == 0 {
return Err(SklearsError::InvalidParameter {
name: "input_dim".to_string(),
reason: "must be greater than 0".to_string(),
});
}
if config.model.output_dim == 0 {
return Err(SklearsError::InvalidParameter {
name: "output_dim".to_string(),
reason: "must be greater than 0".to_string(),
});
}
if config.training.epochs == 0 {
return Err(SklearsError::InvalidParameter {
name: "epochs".to_string(),
reason: "must be greater than 0".to_string(),
});
}
if config.training.batch_size == 0 {
return Err(SklearsError::InvalidParameter {
name: "batch_size".to_string(),
reason: "must be greater than 0".to_string(),
});
}
if config.training.learning_rate <= T::zero() {
return Err(SklearsError::InvalidParameter {
name: "learning_rate".to_string(),
reason: "must be greater than 0".to_string(),
});
}
if let Some(beta1) = config.optimizer.beta1 {
if beta1 < T::zero() || beta1 >= T::one() {
return Err(SklearsError::InvalidParameter {
name: "beta1".to_string(),
reason: "must be in range [0, 1)".to_string(),
});
}
}
if let Some(beta2) = config.optimizer.beta2 {
if beta2 < T::zero() || beta2 >= T::one() {
return Err(SklearsError::InvalidParameter {
name: "beta2".to_string(),
reason: "must be in range [0, 1)".to_string(),
});
}
}
Ok(())
}
pub fn parse_activation(activation_str: &str) -> NeuralResult<Activation> {
match activation_str.to_lowercase().as_str() {
"relu" => Ok(Activation::Relu),
"sigmoid" => Ok(Activation::Logistic),
"tanh" => Ok(Activation::Tanh),
"identity" | "linear" => Ok(Activation::Identity),
"elu" => Ok(Activation::Elu),
"leakyrelu" => Ok(Activation::LeakyRelu),
"swish" | "silu" => Ok(Activation::Swish),
"gelu" => Ok(Activation::Gelu),
"mish" => Ok(Activation::Mish),
_ => Err(SklearsError::InvalidInput(format!(
"Unknown activation function: {}",
activation_str
))),
}
}
pub fn parse_weight_init(init_str: &str) -> NeuralResult<InitStrategy> {
match init_str.to_lowercase().as_str() {
"zeros" => Ok(InitStrategy::Zeros),
"uniform" => Ok(InitStrategy::Uniform {
low: -0.1,
high: 0.1,
}),
"normal" => Ok(InitStrategy::Normal {
mean: 0.0,
std: 0.1,
}),
"xavier_uniform" | "glorot_uniform" => Ok(InitStrategy::XavierUniform),
"xavier_normal" | "glorot_normal" => Ok(InitStrategy::XavierNormal),
"he_uniform" => Ok(InitStrategy::HeUniform),
"he_normal" => Ok(InitStrategy::HeNormal),
"lecun_uniform" => Ok(InitStrategy::LeCunUniform),
"lecun_normal" => Ok(InitStrategy::LeCunNormal),
"orthogonal" => Ok(InitStrategy::Orthogonal { gain: 1.0 }),
_ => Err(SklearsError::InvalidInput(format!(
"Unknown weight initialization: {}",
init_str
))),
}
}
}
pub fn create_example_configs() -> NeuralResult<()> {
std::fs::create_dir_all("configs/examples")
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create directory: {}", e)))?;
#[cfg(feature = "serde")]
{
ConfigManager::create_template::<f32>("configs/examples/mlp_config.yaml", "mlp")?;
ConfigManager::create_template::<f32>("configs/examples/mlp_config.json", "mlp")?;
ConfigManager::create_template::<f32>(
"configs/examples/transformer_config.yaml",
"transformer",
)?;
ConfigManager::create_template::<f32>("configs/examples/lstm_config.yaml", "lstm")?;
ConfigManager::create_template::<f32>("configs/examples/cnn_config.yaml", "cnn")?;
}
println!("Example configuration files created in configs/examples/");
Ok(())
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_format_from_path() {
assert_eq!(ConfigFormat::from_path("config.yaml"), ConfigFormat::YAML);
assert_eq!(ConfigFormat::from_path("config.yml"), ConfigFormat::YAML);
assert_eq!(ConfigFormat::from_path("config.json"), ConfigFormat::JSON);
assert_eq!(ConfigFormat::from_path("config.txt"), ConfigFormat::JSON); }
#[test]
fn test_default_configs() {
let config = NeuralNetworkConfig::<f32>::default();
assert_eq!(config.model.model_type, "mlp");
assert_eq!(config.training.epochs, 100);
assert_eq!(config.optimizer.optimizer_type, "adam");
}
#[test]
fn test_config_validation() {
let mut config = NeuralNetworkConfig::<f32>::default();
assert!(ConfigManager::validate_config(&config).is_ok());
config.model.input_dim = 0;
assert!(ConfigManager::validate_config(&config).is_err());
}
#[test]
fn test_activation_parsing() {
assert!(matches!(
ConfigManager::parse_activation("relu"),
Ok(Activation::Relu)
));
assert!(matches!(
ConfigManager::parse_activation("ReLU"),
Ok(Activation::Relu)
));
assert!(ConfigManager::parse_activation("unknown").is_err());
}
#[test]
fn test_weight_init_parsing() {
assert!(matches!(
ConfigManager::parse_weight_init("xavier_uniform"),
Ok(InitStrategy::XavierUniform)
));
assert!(ConfigManager::parse_weight_init("unknown").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_config_serialization() {
let config = NeuralNetworkConfig::<f32>::default();
let json_str = serde_json::to_string(&config).expect("operation should succeed");
let parsed: NeuralNetworkConfig<f32> =
serde_json::from_str(&json_str).expect("operation should succeed");
assert_eq!(parsed.model.model_type, config.model.model_type);
let yaml_str = serde_yaml::to_string(&config).expect("operation should succeed");
let parsed: NeuralNetworkConfig<f32> =
serde_yaml::from_str(&yaml_str).expect("operation should succeed");
assert_eq!(parsed.model.model_type, config.model.model_type);
}
}