optirs-nas 0.3.2

OptiRS Neural Architecture Search and hyperparameter optimization
Documentation
//! SearchStrategy implementations: the real RandomStrategy / EvolutionaryStrategy
//! adapters over `crate::search_strategies`.
//!
//! Every other `SearchStrategyType` is served by
//! `crate::nas_engine::strategy_adapters`, which wraps the corresponding real
//! strategy. The five never-instantiated placeholder strategies that used to live
//! here (generated by an `impl_search_strategy!` macro returning an empty
//! candidate list) have been deleted — see the note at the end of the file.

use crate::error::Result;
use crate::nas_engine::config::*;
use crate::nas_engine::results::*;
use scirs2_core::numeric::Float;
use std::collections::{HashMap, VecDeque};
use std::fmt::Debug;

use super::support::SearchStrategy;

/// Evolutionary search strategy wrapper.
///
/// Holds a live [`crate::search_strategies::EvolutionarySearch`] which performs
/// tournament selection, crossover and mutation from the search history (and
/// falls back to random sampling when history is insufficient). Tracks the
/// best score per generation to drive a real convergence criterion.
pub(super) struct EvolutionaryStrategy<
    T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum,
> {
    pub(super) inner: crate::search_strategies::EvolutionarySearch<T>,
    pub(super) search_space: SearchSpaceConfig,
    pub(super) batch_size: usize,
    /// Best overall score observed in each `update_strategy` call, used to
    /// detect a lack of improvement over the most recent generations.
    pub(super) best_score_history: Vec<T>,
    /// Number of trailing generations without improvement that signals
    /// convergence.
    pub(super) convergence_patience: usize,
    /// Whether the caller asked for lack-of-improvement stopping at all.
    ///
    /// `has_converged` is now consulted by `should_stop_search` (it used to be
    /// called from nowhere), and this strategy's version of it is a
    /// no-improvement *heuristic* rather than a structural end of the search. A
    /// caller who set `early_stopping.enabled = false` asked not to be stopped on
    /// that basis, so the heuristic honors the flag; a strategy whose schedule
    /// genuinely runs out (`ProgressiveNAS`) reports completion regardless.
    pub(super) early_stopping_enabled: bool,
    /// Minimum number of generations before this heuristic may stop the search.
    ///
    /// `EarlyStoppingConfig::min_generations` was enforced nowhere in the crate.
    /// Now that `has_converged` actually stops the engine, ignoring it would let a
    /// short patience end a run before the caller's declared minimum — a config of
    /// `patience: 3, min_generations: 50` would have stopped at generation 4.
    /// Structural completion (`ProgressiveNAS`'s exhausted schedule) is deliberately
    /// *not* subject to this floor: a finished schedule is finished.
    pub(super) min_generations: usize,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum>
    EvolutionaryStrategy<T>
{
    /// Construct an evolutionary strategy from a NAS configuration, wrapping a
    /// live [`crate::search_strategies::EvolutionarySearch`] sized to the
    /// configured population.
    pub fn new(config: &NASConfig<T>) -> Result<Self> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        let population_size = config.population_size.max(2);
        let convergence_patience = config.early_stopping.patience.max(5);
        let mut inner =
            crate::search_strategies::EvolutionarySearch::<T>::new(population_size, 0.1, 0.8, 3);
        inner.initialize(&config.search_space)?;
        let batch_size = if config.population_size > 0 {
            config.population_size
        } else {
            8
        };
        Ok(Self {
            inner,
            search_space: config.search_space.clone(),
            batch_size,
            best_score_history: Vec::new(),
            convergence_patience,
            early_stopping_enabled: config.early_stopping.enabled,
            min_generations: config.early_stopping.min_generations,
        })
    }
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum> SearchStrategy<T>
    for EvolutionaryStrategy<T>
{
    fn generate_candidates(
        &mut self,
        history: &VecDeque<SearchResult<T>>,
    ) -> Result<Vec<OptimizerArchitecture<T>>> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        let mut candidates = Vec::with_capacity(self.batch_size);
        for _ in 0..self.batch_size {
            let architecture = self
                .inner
                .generate_architecture(&self.search_space, history)?;
            candidates.push(architecture);
        }
        Ok(candidates)
    }
    fn update_strategy(&mut self, results: &[SearchResult<T>]) -> Result<()> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        if !results.is_empty() {
            let best = results
                .iter()
                .map(|r| r.evaluation_results.overall_score)
                .fold(
                    T::neg_infinity(),
                    |acc, score| {
                        if score > acc {
                            score
                        } else {
                            acc
                        }
                    },
                );
            if best > T::neg_infinity() {
                self.best_score_history.push(best);
            }
        }
        self.inner.update_with_results(results)
    }
    fn has_converged(&self) -> bool {
        if !self.early_stopping_enabled {
            return false;
        }
        // One entry per generation, so the history length is the generation count.
        if self.best_score_history.len() < self.min_generations {
            return false;
        }
        let window = self.convergence_patience;
        if self.best_score_history.len() <= window {
            return false;
        }
        let len = self.best_score_history.len();
        let recent_best = self.best_score_history[len - window..]
            .iter()
            .fold(T::neg_infinity(), |acc, &s| if s > acc { s } else { acc });
        let prior_best = self.best_score_history[..len - window]
            .iter()
            .fold(T::neg_infinity(), |acc, &s| if s > acc { s } else { acc });
        recent_best <= prior_best
    }
    fn strategy_name(&self) -> &str {
        "EvolutionaryStrategy"
    }
}
/// Random search strategy wrapper.
///
/// Holds a live [`crate::search_strategies::RandomSearch`] together with the
/// search space and batch size so that `generate_candidates` produces a real
/// batch of randomly-sampled valid architectures.
pub(super) struct RandomStrategy<
    T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum,
> {
    pub(super) inner: crate::search_strategies::RandomSearch<T>,
    pub(super) search_space: SearchSpaceConfig,
    pub(super) batch_size: usize,
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum>
    RandomStrategy<T>
{
    /// Construct a random-search strategy from a NAS configuration. A live
    /// [`crate::search_strategies::RandomSearch`] is seeded deterministically
    /// and initialized with the configured search space.
    pub fn new(config: &NASConfig<T>) -> Result<Self> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        let mut inner = crate::search_strategies::RandomSearch::<T>::new(Some(42));
        inner.initialize(&config.search_space)?;
        let batch_size = if config.population_size > 0 {
            config.population_size
        } else {
            8
        };
        Ok(Self {
            inner,
            search_space: config.search_space.clone(),
            batch_size,
        })
    }
    /// Synthesize a single valid architecture without indexing into the
    /// (possibly empty) component config list. Used when the search space
    /// provides no per-component hyperparameter configs but still declares the
    /// component vocabulary via `component_types`.
    pub(super) fn synthesize_from_component_types(&self, index: usize) -> OptimizerArchitecture<T> {
        let component_label = self
            .search_space
            .component_types
            .first()
            .map(|ct| format!("{:?}", ct))
            .unwrap_or_else(|| "Adam".to_string());
        OptimizerArchitecture {
            components: vec![component_label],
            parameters: HashMap::new(),
            connections: Vec::new(),
            metadata: HashMap::new(),
            hyperparameters: HashMap::new(),
            architecture_id: format!("random_arch_{}", index),
        }
    }
}
impl<T: Float + Debug + Default + Clone + Send + Sync + 'static + std::iter::Sum> SearchStrategy<T>
    for RandomStrategy<T>
{
    fn generate_candidates(
        &mut self,
        history: &VecDeque<SearchResult<T>>,
    ) -> Result<Vec<OptimizerArchitecture<T>>> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        let mut candidates = Vec::with_capacity(self.batch_size);
        for idx in 0..self.batch_size {
            if self.search_space.components.is_empty() {
                candidates.push(self.synthesize_from_component_types(idx));
            } else {
                let architecture = self
                    .inner
                    .generate_architecture(&self.search_space, history)?;
                candidates.push(architecture);
            }
        }
        Ok(candidates)
    }
    fn update_strategy(&mut self, results: &[SearchResult<T>]) -> Result<()> {
        use crate::search_strategies::SearchStrategy as InnerSearchStrategy;
        self.inner.update_with_results(results)
    }
    fn has_converged(&self) -> bool {
        false
    }
    fn strategy_name(&self) -> &str {
        "RandomStrategy"
    }
}
// `BayesianStrategy` / `ReinforcementStrategy` / `DifferentiableStrategy` /
// `ProgressiveStrategy` / `HybridStrategy` used to be defined here and given a
// `SearchStrategy` impl by an `impl_search_strategy!` macro whose
// `generate_candidates` returned `Ok(Vec::new())` — an empty generation — and
// whose `update_strategy` was `Ok(())`.
//
// They were unreachable (nothing constructed them: `create_search_strategy`
// dispatches every `SearchStrategyType` to the real implementations in
// `crate::search_strategies` through `strategy_adapters`), so they were dead
// weight whose only possible effect was to look like a usable strategy to the
// next person wiring one up — and a search that selected one would have run to
// completion having evaluated nothing at all. Deleted rather than left lying
// around; the real strategies are the ones `strategy_adapters` builds.