use std::fmt::Debug;
#[allow(dead_code)]
use scirs2_core::ndarray::{Array1, Array2, Array3};
use scirs2_core::numeric::Float;
use std::collections::{HashMap, HashSet};
use crate::error::{OptimError, Result};
#[derive(Debug, Clone)]
pub struct GradientBasedNASConfig<T: Float + Debug + Send + Sync + 'static> {
pub arch_learning_rate: T,
pub weight_learning_rate: T,
pub arch_weight_decay: T,
pub weight_decay: T,
pub search_epochs: usize,
pub warmup_epochs: usize,
pub temperature: T,
pub temperature_decay: T,
pub min_temperature: T,
pub arch_regularization: T,
pub early_stopping_patience: usize,
pub pruning_threshold: T,
pub num_candidate_ops: usize,
}
#[derive(Debug)]
pub struct DARTSSearcher<T: Float + Debug + Send + Sync + 'static> {
config: GradientBasedNASConfig<T>,
architecture_parameters: Array3<T>,
search_space: SearchSpace<T>,
mixed_operations: HashMap<EdgeId, MixedOperation<T>>,
architecture_history: Vec<ArchitectureCandidate<T>>,
training_stats: DARTSTrainingStats<T>,
current_epoch: usize,
best_architecture: Option<ArchitectureCandidate<T>>,
pruning_tracker: PruningTracker<T>,
}
#[derive(Debug, Clone)]
pub struct SearchSpace<T: Float + Debug + Send + Sync + 'static> {
pub nodes: Vec<SearchNode>,
pub edges: Vec<SearchEdge>,
pub operations: Vec<CandidateOperation<T>>,
pub edge_operations: HashMap<EdgeId, Vec<usize>>,
pub constraints: SearchSpaceConstraints<T>,
}
#[derive(Debug, Clone)]
pub struct SearchNode {
pub id: NodeId,
pub node_type: NodeType,
pub input_dims: Vec<usize>,
pub output_dims: Vec<usize>,
pub parameters: HashMap<String, f64>,
}
#[derive(Debug, Clone)]
pub struct SearchEdge {
pub id: EdgeId,
pub from_node: NodeId,
pub to_node: NodeId,
pub weight: f64,
pub constraints: EdgeConstraints,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeType {
Input,
Intermediate,
Output,
Skip,
}
#[derive(Debug, Clone)]
pub struct EdgeConstraints {
pub max_operations: usize,
pub allowed_ops: HashSet<String>,
pub resource_limits: ResourceLimits,
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_parameters: usize,
pub max_flops: u64,
pub max_memory: usize,
}
#[derive(Debug, Clone)]
pub struct CandidateOperation<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub op_type: OperationType,
pub parameters: HashMap<String, T>,
pub cost: ComputationalCost<T>,
pub implementation: OperationImpl<T>,
}
#[derive(Debug, Clone)]
pub enum OperationType {
Conv { kernel_size: usize, stride: usize },
Dense { units: usize },
Pool { pool_type: PoolType, kernel_size: usize },
Skip,
Zero,
BatchNorm,
Activation { activation: ActivationType },
Attention { heads: usize, dim: usize },
OptimizerOp { optimizer_type: String },
}
#[derive(Debug, Clone, Copy)]
pub enum PoolType {
Max,
Average,
Global,
}
#[derive(Debug, Clone, Copy)]
pub enum ActivationType {
ReLU,
Tanh,
Sigmoid,
GELU,
Swish,
}
#[derive(Debug, Clone)]
pub struct ComputationalCost<T: Float + Debug + Send + Sync + 'static> {
pub parameters: usize,
pub flops: u64,
pub memory: usize,
pub cost_weight: T,
}
#[derive(Debug, Clone)]
pub struct OperationImpl<T: Float + Debug + Send + Sync + 'static> {
pub forward_fn: String,
pub backward_fn: String,
pub init_params: HashMap<String, T>,
}
#[derive(Debug)]
pub struct MixedOperation<T: Float + Debug + Send + Sync + 'static> {
pub edge_id: EdgeId,
pub candidates: Vec<CandidateOperation<T>>,
pub arch_weights: Array1<T>,
pub probabilities: Array1<T>,
pub gumbel_noise: Option<Array1<T>>,
pub temperature: T,
}
#[derive(Debug, Clone)]
pub struct ArchitectureCandidate<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub operations: HashMap<EdgeId, usize>,
pub arch_params: Array3<T>,
pub performance: PerformanceMetrics<T>,
pub complexity: ArchitectureComplexity<T>,
pub generation_method: GenerationMethod,
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics<T: Float + Debug + Send + Sync + 'static> {
pub validation_performance: T,
pub training_performance: T,
pub convergence_speed: T,
pub stability: T,
pub generalization: T,
}
#[derive(Debug, Clone)]
pub struct ArchitectureComplexity<T: Float + Debug + Send + Sync + 'static> {
pub total_parameters: usize,
pub total_flops: u64,
pub memory_usage: usize,
pub depth: usize,
pub connectivity: T,
pub diversity: T,
}
#[derive(Debug, Clone, Copy)]
pub enum GenerationMethod {
Derived,
Sampled,
Manual,
}
#[derive(Debug, Clone)]
pub struct DARTSTrainingStats<T: Float + Debug + Send + Sync + 'static> {
pub arch_grad_norms: Vec<T>,
pub weight_grad_norms: Vec<T>,
pub arch_losses: Vec<T>,
pub weight_losses: Vec<T>,
pub validation_accuracies: Vec<T>,
pub temperature_history: Vec<T>,
pub entropy_history: Vec<T>,
}
#[derive(Debug)]
pub struct PruningTracker<T: Float + Debug + Send + Sync + 'static> {
pub pruned_operations: HashMap<usize, Vec<(EdgeId, usize)>>,
pub pruning_scores: HashMap<EdgeId, Array1<T>>,
pub threshold_history: Vec<T>,
pub remaining_ops: HashMap<EdgeId, usize>,
}
#[derive(Debug, Clone)]
pub struct SearchSpaceConstraints<T: Float + Debug + Send + Sync + 'static> {
pub max_total_parameters: usize,
pub max_depth: usize,
pub max_ops_per_edge: usize,
pub resource_budget: T,
}
pub type NodeId = String;
pub type EdgeId = String;
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> DARTSSearcher<T> {
pub fn new(config: GradientBasedNASConfig<T>, search_space: SearchSpace<T>) -> Result<Self> {
let num_edges = search_space.edges.len();
let num_ops = config.num_candidate_ops;
let architecture_parameters = Array3::from_shape_fn((num_edges, num_ops, 1), |_| {
T::from(scirs2_core::random::random::<f64>() * 0.1).expect("unwrap failed")
});
let mut mixed_operations = HashMap::new();
for edge in &search_space.edges {
if let Some(op_indices) = search_space.edge_operations.get(&edge.id) {
let candidates = op_indices.iter()
.map(|&idx| search_space.operations[idx].clone())
.collect();
let arch_weights = Array1::zeros(candidates.len());
let mixed_op = MixedOperation {
edge_id: edge.id.clone(),
candidates,
arch_weights,
probabilities: Array1::zeros(op_indices.len()),
gumbel_noise: None,
temperature: config.temperature,
};
mixed_operations.insert(edge.id.clone(), mixed_op);
}
}
Ok(Self {
config,
architecture_parameters,
search_space,
mixed_operations,
architecture_history: Vec::new(),
training_stats: DARTSTrainingStats::new(),
current_epoch: 0,
best_architecture: None,
pruning_tracker: PruningTracker::new(),
})
}
pub fn search(&mut self, objective_fn: &dyn Fn(&ArchitectureCandidate<T>) -> Result<T>) -> Result<ArchitectureCandidate<T>> {
println!("Starting DARTS architecture search for {} epochs", self.config.search_epochs);
for epoch in 0..self.config.search_epochs {
self.current_epoch = epoch;
if epoch < self.config.warmup_epochs {
self.warmup_step(objective_fn)?;
} else {
self.joint_optimization_step(objective_fn)?;
}
self.update_temperature();
if epoch > self.config.warmup_epochs {
self.progressive_pruning()?;
}
let current_arch = self.derive_architecture()?;
let performance = objective_fn(¤t_arch)?;
if let Some(ref best) = self.best_architecture {
if performance > best.performance.validation_performance {
self.best_architecture = Some(current_arch.clone());
}
} else {
self.best_architecture = Some(current_arch.clone());
}
self.record_epoch_stats(performance)?;
if self.should_early_stop() {
println!("Early stopping at epoch {} due to convergence", epoch);
break;
}
if epoch % 10 == 0 {
println!("DARTS Epoch {}: Performance = {:.4}, Temperature = {:.4}",
epoch,
performance.to_f64().unwrap_or(0.0),
self.config.temperature.to_f64().unwrap_or(1.0)
);
}
}
self.best_architecture.clone().ok_or_else(||
OptimError::SearchFailed("No valid architecture found".to_string())
)
}
fn warmup_step(&mut self, objective_fn: &dyn Fn(&ArchitectureCandidate<T>) -> Result<T>) -> Result<()> {
let sampled_arch = self.sample_architecture()?;
let performance = objective_fn(&sampled_arch)?;
self.update_weights(performance)?;
Ok(())
}
fn joint_optimization_step(&mut self, objective_fn: &dyn Fn(&ArchitectureCandidate<T>) -> Result<T>) -> Result<()> {
let arch_gradient = self.compute_architecture_gradient(objective_fn)?;
self.update_architecture_parameters(arch_gradient)?;
let weight_gradient = self.compute_weight_gradient(objective_fn)?;
self.update_weight_parameters(weight_gradient)?;
Ok(())
}
fn compute_architecture_gradient(&self, objective_fn: &dyn Fn(&ArchitectureCandidate<T>) -> Result<T>) -> Result<Array3<T>> {
let mut gradient = Array3::zeros(self.architecture_parameters.dim());
let epsilon = scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero());
for i in 0..gradient.shape()[0] {
for j in 0..gradient.shape()[1] {
for k in 0..gradient.shape()[2] {
let mut params_plus = self.architecture_parameters.clone();
params_plus[[i, j, k]] = params_plus[[i, j, k]] + epsilon;
let arch_plus = self.params_to_architecture(¶ms_plus)?;
let perf_plus = objective_fn(&arch_plus)?;
let mut params_minus = self.architecture_parameters.clone();
params_minus[[i, j, k]] = params_minus[[i, j, k]] - epsilon;
let arch_minus = self.params_to_architecture(¶ms_minus)?;
let perf_minus = objective_fn(&arch_minus)?;
gradient[[i, j, k]] = (perf_plus - perf_minus) / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * epsilon);
}
}
}
Ok(gradient)
}
fn compute_weight_gradient(&self, objective_fn: &dyn Fn(&ArchitectureCandidate<T>) -> Result<T>) -> Result<T> {
let current_arch = self.derive_architecture()?;
let performance = objective_fn(¤t_arch)?;
Ok(performance * scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero()))
}
fn update_architecture_parameters(&mut self, gradient: Array3<T>) -> Result<()> {
let learning_rate = self.config.arch_learning_rate;
let weight_decay = self.config.arch_weight_decay;
for ((i, j, k), &grad) in gradient.indexed_iter() {
let current_param = self.architecture_parameters[[i, j, k]];
let update = learning_rate * grad + weight_decay * current_param;
self.architecture_parameters[[i, j, k]] = current_param - update;
}
Ok(())
}
fn update_weight_parameters(&mut self, gradient: T) -> Result<()> {
Ok(())
}
fn update_mixed_operations(&mut self) -> Result<()> {
for (edge_id, mixed_op) in &mut self.mixed_operations {
if let Some(edge_idx) = self.get_edge_index(edge_id) {
for i in 0..mixed_op.arch_weights.len() {
mixed_op.arch_weights[i] = self.architecture_parameters[[edge_idx, i, 0]];
}
self.compute_softmax_probabilities(mixed_op)?;
}
}
Ok(())
}
fn compute_softmax_probabilities(&self, mixed_op: &mut MixedOperation<T>) -> Result<()> {
let temperature = mixed_op.temperature;
let weights = &mixed_op.arch_weights;
let scaled_weights = weights.mapv(|w| w / temperature);
let max_weight = scaled_weights.iter().cloned().fold(scirs2_core::numeric::NumCast::from(f64::NEG_INFINITY).unwrap_or_else(|| T::zero()), T::max);
let exp_weights = scaled_weights.mapv(|w| (w - max_weight).exp());
let sum_exp = exp_weights.sum();
mixed_op.probabilities = exp_weights / sum_exp;
Ok(())
}
fn sample_architecture(&mut self) -> Result<ArchitectureCandidate<T>> {
self.update_mixed_operations()?;
let mut operations = HashMap::new();
let mut arch_params = self.architecture_parameters.clone();
for (edge_id, mixed_op) in &mut self.mixed_operations {
let gumbel_noise = self.sample_gumbel_noise(mixed_op.probabilities.len())?;
mixed_op.gumbel_noise = Some(gumbel_noise.clone());
let gumbel_softmax = &mixed_op.probabilities + &gumbel_noise;
let argmax = gumbel_softmax.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
operations.insert(edge_id.clone(), argmax);
}
Ok(ArchitectureCandidate {
id: format!("sampled_epoch_{}", self.current_epoch),
operations,
arch_params,
performance: PerformanceMetrics::default(),
complexity: self.compute_architecture_complexity(&HashMap::new())?,
generation_method: GenerationMethod::Sampled,
})
}
fn derive_architecture(&self) -> Result<ArchitectureCandidate<T>> {
let mut operations = HashMap::new();
for (edge_id, mixed_op) in &self.mixed_operations {
let best_op_idx = mixed_op.probabilities.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
operations.insert(edge_id.clone(), best_op_idx);
}
Ok(ArchitectureCandidate {
id: format!("derived_epoch_{}", self.current_epoch),
operations: operations.clone(),
arch_params: self.architecture_parameters.clone(),
performance: PerformanceMetrics::default(),
complexity: self.compute_architecture_complexity(&operations)?,
generation_method: GenerationMethod::Derived,
})
}
fn params_to_architecture(&self, params: &Array3<T>) -> Result<ArchitectureCandidate<T>> {
let mut operations = HashMap::new();
for (i, (edge_id, mixed_op)) in self.mixed_operations.iter().enumerate() {
if i < params.shape()[0] {
let edge_params = params.slice(scirs2_core::ndarray::s![i, .., 0]);
let best_idx = edge_params.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
operations.insert(edge_id.clone(), best_idx);
}
}
Ok(ArchitectureCandidate {
id: format!("temp_arch_{}", self.current_epoch),
operations: operations.clone(),
arch_params: params.clone(),
performance: PerformanceMetrics::default(),
complexity: self.compute_architecture_complexity(&operations)?,
generation_method: GenerationMethod::Manual,
})
}
fn sample_gumbel_noise(&self, size: usize) -> Result<Array1<T>> {
use scirs2_core::random::Rng;
let mut rng = scirs2_core::random::thread_rng();
let noise = Array1::from_shape_fn(size, |_| {
let u: f64 = rng.gen_range(1e-10..1.0);
let gumbel = -(-u.ln()).ln();
scirs2_core::numeric::NumCast::from(gumbel).unwrap_or_else(|| T::zero())
});
Ok(noise)
}
fn update_temperature(&mut self) {
self.config.temperature = (self.config.temperature * self.config.temperature_decay)
.max(self.config.min_temperature);
for mixed_op in self.mixed_operations.values_mut() {
mixed_op.temperature = self.config.temperature;
}
}
fn progressive_pruning(&mut self) -> Result<()> {
let threshold = self.config.pruning_threshold;
for (edge_id, mixed_op) in &mut self.mixed_operations {
let mut pruned_ops = Vec::new();
for (i, &prob) in mixed_op.probabilities.iter().enumerate() {
if prob < threshold && mixed_op.candidates.len() > 1 {
pruned_ops.push(i);
}
}
self.pruning_tracker.pruned_operations
.entry(self.current_epoch)
.or_insert_with(Vec::new)
.extend(pruned_ops.iter().map(|&i| (edge_id.clone(), i)));
}
Ok(())
}
fn should_early_stop(&self) -> bool {
if self.training_stats.validation_accuracies.len() < self.config.early_stopping_patience {
return false;
}
let recent_accs = &self.training_stats.validation_accuracies[
self.training_stats.validation_accuracies.len().saturating_sub(self.config.early_stopping_patience)..
];
let mut is_improving = false;
for i in 1..recent_accs.len() {
if recent_accs[i] > recent_accs[i-1] {
is_improving = true;
break;
}
}
!is_improving
}
fn record_epoch_stats(&mut self, performance: T) -> Result<()> {
self.training_stats.validation_accuracies.push(performance);
self.training_stats.temperature_history.push(self.config.temperature);
let entropy = self.compute_architecture_entropy()?;
self.training_stats.entropy_history.push(entropy);
Ok(())
}
fn compute_architecture_entropy(&self) -> Result<T> {
let mut total_entropy = T::zero();
let mut num_edges = 0;
for mixed_op in self.mixed_operations.values() {
let mut edge_entropy = T::zero();
for &p in mixed_op.probabilities.iter() {
if p > T::zero() {
edge_entropy = edge_entropy - p * p.ln();
}
}
total_entropy = total_entropy + edge_entropy;
num_edges += 1;
}
Ok(total_entropy / scirs2_core::numeric::NumCast::from(num_edges as f64).unwrap_or_else(|| T::zero()))
}
fn compute_architecture_complexity(&self, operations: &HashMap<EdgeId, usize>) -> Result<ArchitectureComplexity<T>> {
let mut total_parameters = 0;
let mut total_flops = 0;
let mut total_memory = 0;
let mut operation_types = HashSet::new();
for (edge_id, &op_idx) in operations {
if let Some(mixed_op) = self.mixed_operations.get(edge_id) {
if op_idx < mixed_op.candidates.len() {
let op = &mixed_op.candidates[op_idx];
total_parameters += op.cost.parameters;
total_flops += op.cost.flops;
total_memory += op.cost.memory;
operation_types.insert(op.id.clone());
}
}
}
let diversity = T::from(operation_types.len() as f64).expect("unwrap failed");
let depth = operations.len();
Ok(ArchitectureComplexity {
total_parameters,
total_flops,
memory_usage: total_memory,
depth,
connectivity: T::from(operations.len() as f64).expect("unwrap failed"),
diversity,
})
}
fn get_edge_index(&self, edge_id: &EdgeId) -> Option<usize> {
self.search_space.edges.iter()
.position(|edge| &edge.id == edge_id)
}
fn update_weights(&mut self, performance: T) -> Result<()> {
Ok(())
}
pub fn best_architecture(&self) -> Option<&ArchitectureCandidate<T>> {
self.best_architecture.as_ref()
}
pub fn training_statistics(&self) -> &DARTSTrainingStats<T> {
&self.training_stats
}
pub fn search_space(&self) -> &SearchSpace<T> {
&self.search_space
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> DARTSTrainingStats<T> {
fn new() -> Self {
Self {
arch_grad_norms: Vec::new(),
weight_grad_norms: Vec::new(),
arch_losses: Vec::new(),
weight_losses: Vec::new(),
validation_accuracies: Vec::new(),
temperature_history: Vec::new(),
entropy_history: Vec::new(),
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> PruningTracker<T> {
fn new() -> Self {
Self {
pruned_operations: HashMap::new(),
pruning_scores: HashMap::new(),
threshold_history: Vec::new(),
remaining_ops: HashMap::new(),
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for PerformanceMetrics<T> {
fn default() -> Self {
Self {
validation_performance: T::zero(),
training_performance: T::zero(),
convergence_speed: T::zero(),
stability: T::zero(),
generalization: T::zero(),
}
}
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for GradientBasedNASConfig<T> {
fn default() -> Self {
Self {
arch_learning_rate: scirs2_core::numeric::NumCast::from(3e-4).unwrap_or_else(|| T::zero()),
weight_learning_rate: scirs2_core::numeric::NumCast::from(0.025).unwrap_or_else(|| T::zero()),
arch_weight_decay: scirs2_core::numeric::NumCast::from(1e-3).unwrap_or_else(|| T::zero()),
weight_decay: scirs2_core::numeric::NumCast::from(3e-4).unwrap_or_else(|| T::zero()),
search_epochs: 50,
warmup_epochs: 15,
temperature: scirs2_core::numeric::NumCast::from(1.0).unwrap_or_else(|| T::zero()),
temperature_decay: scirs2_core::numeric::NumCast::from(0.96).unwrap_or_else(|| T::zero()),
min_temperature: scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero()),
arch_regularization: scirs2_core::numeric::NumCast::from(1e-3).unwrap_or_else(|| T::zero()),
early_stopping_patience: 10,
pruning_threshold: scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero()),
num_candidate_ops: 8,
}
}
}
impl Default for EdgeConstraints {
fn default() -> Self {
Self {
max_operations: 8,
allowed_ops: HashSet::new(),
resource_limits: ResourceLimits {
max_parameters: 1_000_000,
max_flops: 1_000_000_000,
max_memory: 1_000_000,
},
}
}
}