use crate::NeuralResult;
use scirs2_core::ndarray::{Array1, Array2, ScalarOperand};
use scirs2_core::random::{thread_rng, Normal};
use sklears_core::{error::SklearsError, types::FloatBounds};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
fn safe_from_f64<T: FloatBounds>(value: f64) -> NeuralResult<T> {
T::from(value).ok_or_else(|| {
SklearsError::NumericalError(format!("Failed to convert {} to target float type", value))
})
}
fn safe_max<'a, T>(iter: impl Iterator<Item = &'a T>) -> NeuralResult<&'a T>
where
T: FloatBounds + 'a,
{
iter.max_by(|a, b| {
a.to_f64()
.and_then(|a_f64| b.to_f64().map(|b_f64| a_f64.partial_cmp(&b_f64)))
.flatten()
.unwrap_or(std::cmp::Ordering::Equal)
})
.ok_or_else(|| SklearsError::InvalidInput("Empty iterator".to_string()))
}
fn safe_normal(mean: f64, std_dev: f64) -> NeuralResult<Normal<f64>> {
Normal::new(mean, std_dev).map_err(|e| SklearsError::InvalidParameter {
name: "distribution".to_string(),
reason: format!("Failed to create Normal distribution: {}", e),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum OperationType {
None,
MaxPool3x3,
AvgPool3x3,
Skip,
SepConv3x3,
SepConv5x5,
DilConv3x3,
DilConv5x5,
}
impl OperationType {
pub fn all_operations() -> Vec<OperationType> {
vec![
OperationType::None,
OperationType::MaxPool3x3,
OperationType::AvgPool3x3,
OperationType::Skip,
OperationType::SepConv3x3,
OperationType::SepConv5x5,
OperationType::DilConv3x3,
OperationType::DilConv5x5,
]
}
}
#[derive(Debug)]
#[allow(dead_code)] pub struct MixedOperation<T: FloatBounds> {
alpha: Array1<T>,
operations: Vec<OperationType>,
op_weights: Vec<Option<Array2<T>>>,
in_features: usize,
out_features: usize,
}
impl<T: FloatBounds + ScalarOperand + std::iter::Sum> MixedOperation<T> {
pub fn new(
in_features: usize,
out_features: usize,
operations: Vec<OperationType>,
) -> NeuralResult<Self> {
let n_ops = operations.len();
let mut rng = thread_rng();
let normal_dist = safe_normal(0.0, 1.0)?;
let init_value = safe_from_f64(1.0 / n_ops as f64)?;
let alpha = Array1::from_elem(n_ops, init_value);
let mut op_weights = Vec::new();
for _ in 0..n_ops {
let std = (2.0 / in_features as f64).sqrt();
let w = Array2::from_shape_fn((in_features, out_features), |_| {
let sample = rng.sample::<f64, _>(normal_dist);
safe_from_f64(sample * std).unwrap_or(T::zero())
});
op_weights.push(Some(w));
}
Ok(Self {
alpha,
operations,
op_weights,
in_features,
out_features,
})
}
pub fn forward(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
let alpha_max = safe_max(self.alpha.iter())?;
let exp_sum: T = self.alpha.iter().map(|&a| (a - *alpha_max).exp()).sum();
let alpha_softmax: Array1<T> = self.alpha.mapv(|a| (a - *alpha_max).exp() / exp_sum);
let mut output = Array2::zeros((x.nrows(), self.out_features));
for (i, (&op_type, weight_opt)) in self
.operations
.iter()
.zip(self.op_weights.iter())
.enumerate()
{
let op_weight = alpha_softmax[i];
let op_output = match op_type {
OperationType::None => Array2::zeros((x.nrows(), self.out_features)),
OperationType::Skip => {
if x.ncols() == self.out_features {
x.clone()
} else {
if let Some(ref w) = weight_opt {
x.dot(w)
} else {
Array2::zeros((x.nrows(), self.out_features))
}
}
}
_ => {
if let Some(ref w) = weight_opt {
let transformed = x.dot(w);
transformed.mapv(|val| if val > T::zero() { val } else { T::zero() })
} else {
Array2::zeros((x.nrows(), self.out_features))
}
}
};
output = output + op_output.mapv(|val| val * op_weight);
}
Ok(output)
}
pub fn argmax_operation(&self) -> OperationType {
let max_idx = self
.alpha
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.to_f64()
.and_then(|a_f64| b.to_f64().map(|b_f64| a_f64.partial_cmp(&b_f64)))
.flatten()
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(idx, _)| idx)
.unwrap_or(0);
self.operations[max_idx]
}
pub fn get_alpha(&self) -> &Array1<T> {
&self.alpha
}
pub fn update_alpha(&mut self, gradient: &Array1<T>, learning_rate: T) {
self.alpha = &self.alpha - &gradient.mapv(|g| g * learning_rate);
}
}
#[derive(Debug)]
#[allow(dead_code)] pub struct DARTSCell<T: FloatBounds> {
mixed_ops: Vec<Vec<MixedOperation<T>>>,
n_nodes: usize,
in_features: usize,
out_features: usize,
}
impl<T: FloatBounds + ScalarOperand + std::iter::Sum> DARTSCell<T> {
pub fn new(in_features: usize, out_features: usize, n_nodes: usize) -> NeuralResult<Self> {
let operations = OperationType::all_operations();
let mut mixed_ops = Vec::new();
for i in 0..n_nodes {
let mut node_ops = Vec::new();
let n_inputs = i + 2;
for j in 0..n_inputs {
let input_dim = if i == 0 || j < 2 {
in_features
} else {
out_features
};
let op = MixedOperation::new(input_dim, out_features, operations.clone())?;
node_ops.push(op);
}
mixed_ops.push(node_ops);
}
Ok(Self {
mixed_ops,
n_nodes,
in_features,
out_features,
})
}
pub fn forward(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
let mut node_outputs = vec![x.clone(), x.clone()];
for node_idx in 0..self.n_nodes {
let mut node_output = Array2::zeros((x.nrows(), self.out_features));
for (input_idx, mixed_op) in self.mixed_ops[node_idx].iter().enumerate() {
let input = &node_outputs[input_idx];
let op_output = mixed_op.forward(input)?;
node_output = node_output + op_output;
}
node_output.mapv_inplace(|val| if val > T::zero() { val } else { T::zero() });
node_outputs.push(node_output);
}
let n_intermediate = self.n_nodes;
let mut output = Array2::zeros((x.nrows(), self.out_features));
for node_output in node_outputs.iter().skip(2).take(n_intermediate) {
output += node_output;
}
let divisor = safe_from_f64(n_intermediate as f64)?;
output.mapv_inplace(|val| val / divisor);
Ok(output)
}
pub fn get_discrete_architecture(&self) -> Vec<Vec<OperationType>> {
self.mixed_ops
.iter()
.map(|node_ops| node_ops.iter().map(|op| op.argmax_operation()).collect())
.collect()
}
pub fn num_parameters(&self) -> usize {
self.mixed_ops
.iter()
.flatten()
.map(|op| op.alpha.len())
.sum()
}
}
#[allow(dead_code)] pub struct DARTS<T: FloatBounds> {
normal_cell: DARTSCell<T>,
reduction_cell: DARTSCell<T>,
n_cells: usize,
arch_learning_rate: T,
weight_learning_rate: T,
}
impl<T: FloatBounds + ScalarOperand + std::iter::Sum> DARTS<T> {
pub fn new(
_in_features: usize,
out_features: usize,
n_nodes: usize,
n_cells: usize,
arch_learning_rate: T,
weight_learning_rate: T,
) -> NeuralResult<Self> {
let normal_cell = DARTSCell::new(out_features, out_features, n_nodes)?;
let reduction_cell = DARTSCell::new(out_features, out_features, n_nodes)?;
Ok(Self {
normal_cell,
reduction_cell,
n_cells,
arch_learning_rate,
weight_learning_rate,
})
}
pub fn forward(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
let mut h = if x.ncols() != self.normal_cell.out_features {
let batch_size = x.nrows();
let out_dim = self.normal_cell.out_features;
let in_dim = x.ncols();
if in_dim > out_dim {
Array2::from_shape_fn((batch_size, out_dim), |(i, j)| {
let start_idx = (j * in_dim) / out_dim;
let end_idx = ((j + 1) * in_dim) / out_dim;
let mut sum = T::zero();
for k in start_idx..end_idx {
sum += x[[i, k]];
}
let divisor = safe_from_f64((end_idx - start_idx) as f64).unwrap_or(T::one());
sum / divisor
})
} else {
Array2::from_shape_fn((batch_size, out_dim), |(i, j)| x[[i, j % in_dim]])
}
} else {
x.clone()
};
for i in 0..self.n_cells {
h = if i % 3 == 2 {
self.reduction_cell.forward(&h)?
} else {
self.normal_cell.forward(&h)?
};
}
Ok(h)
}
pub fn get_architecture(&self) -> NeuralArchitecture {
NeuralArchitecture {
normal_cell: self.normal_cell.get_discrete_architecture(),
reduction_cell: self.reduction_cell.get_discrete_architecture(),
n_cells: self.n_cells,
}
}
pub fn num_parameters(&self) -> usize {
self.normal_cell.num_parameters() + self.reduction_cell.num_parameters()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct NeuralArchitecture {
pub normal_cell: Vec<Vec<OperationType>>,
pub reduction_cell: Vec<Vec<OperationType>>,
pub n_cells: usize,
}
impl NeuralArchitecture {
pub fn new(
normal_cell: Vec<Vec<OperationType>>,
reduction_cell: Vec<Vec<OperationType>>,
n_cells: usize,
) -> Self {
Self {
normal_cell,
reduction_cell,
n_cells,
}
}
pub fn num_operations(&self) -> usize {
let normal_ops: usize = self.normal_cell.iter().map(|node| node.len()).sum();
let reduction_ops: usize = self.reduction_cell.iter().map(|node| node.len()).sum();
normal_ops + reduction_ops
}
pub fn complexity_score(&self) -> f64 {
let mut score = 0.0;
for cell in &[&self.normal_cell, &self.reduction_cell] {
for node_ops in *cell {
for &op in node_ops {
score += match op {
OperationType::None => 0.0,
OperationType::Skip => 0.1,
OperationType::MaxPool3x3 | OperationType::AvgPool3x3 => 0.5,
OperationType::SepConv3x3 => 1.0,
OperationType::SepConv5x5 => 1.5,
OperationType::DilConv3x3 => 1.2,
OperationType::DilConv5x5 => 1.8,
};
}
}
}
score
}
}
#[derive(Debug)]
#[allow(dead_code)] pub struct ProgressiveNAS<T: FloatBounds> {
architecture: Vec<OperationType>,
search_space: Vec<OperationType>,
max_length: usize,
current_position: usize,
weights: Vec<Array2<T>>,
in_features: usize,
out_features: usize,
}
impl<T: FloatBounds + ScalarOperand> ProgressiveNAS<T> {
pub fn new(in_features: usize, out_features: usize, max_length: usize) -> Self {
let search_space = OperationType::all_operations();
Self {
architecture: Vec::new(),
search_space,
max_length,
current_position: 0,
weights: Vec::new(),
in_features,
out_features,
}
}
pub fn add_operation(&mut self, operation: OperationType) -> NeuralResult<()> {
if self.architecture.len() >= self.max_length {
return Err(SklearsError::InvalidParameter {
name: "architecture".to_string(),
reason: "Architecture has reached maximum length".to_string(),
});
}
self.architecture.push(operation);
let mut rng = thread_rng();
let normal_dist = safe_normal(0.0, 1.0)?;
let std = (2.0 / self.in_features as f64).sqrt();
let w = Array2::from_shape_fn((self.in_features, self.out_features), |_| {
let sample = rng.sample::<f64, _>(normal_dist);
safe_from_f64(sample * std).unwrap_or(T::zero())
});
self.weights.push(w);
self.current_position += 1;
Ok(())
}
pub fn forward(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
let mut h = x.clone();
for (op, weight) in self.architecture.iter().zip(self.weights.iter()) {
h = match op {
OperationType::None => Array2::zeros(h.dim()),
OperationType::Skip => h,
_ => {
let transformed = h.dot(weight);
transformed.mapv(|val| if val > T::zero() { val } else { T::zero() })
}
};
}
Ok(h)
}
pub fn get_architecture(&self) -> Vec<OperationType> {
self.architecture.clone()
}
pub fn get_search_space(&self) -> &[OperationType] {
&self.search_space
}
pub fn get_progress(&self) -> (usize, usize) {
(self.current_position, self.max_length)
}
}
#[derive(Debug)]
#[allow(dead_code)] pub struct ENASController<T: FloatBounds> {
hidden_size: usize,
num_layers: usize,
num_operations: usize,
weights_hidden: Array2<T>,
weights_output: Array2<T>,
hidden_state: Array1<T>,
learning_rate: T,
entropy_weight: T,
baseline: T,
baseline_decay: T,
}
impl<T: FloatBounds + ScalarOperand + std::iter::Sum> ENASController<T> {
pub fn new(
hidden_size: usize,
num_layers: usize,
num_operations: usize,
learning_rate: T,
) -> NeuralResult<Self> {
let mut rng = thread_rng();
let normal_dist = safe_normal(0.0, 1.0)?;
let std_hidden = (2.0 / hidden_size as f64).sqrt();
let weights_hidden = Array2::from_shape_fn((hidden_size, hidden_size), |_| {
let sample = rng.sample::<f64, _>(normal_dist);
safe_from_f64(sample * std_hidden).unwrap_or(T::zero())
});
let std_output = (2.0 / (hidden_size + num_operations) as f64).sqrt();
let weights_output = Array2::from_shape_fn((hidden_size, num_operations), |_| {
let sample = rng.sample::<f64, _>(normal_dist);
safe_from_f64(sample * std_output).unwrap_or(T::zero())
});
let hidden_state = Array1::zeros(hidden_size);
Ok(Self {
hidden_size,
num_layers,
num_operations,
weights_hidden,
weights_output,
hidden_state,
learning_rate,
entropy_weight: safe_from_f64(0.01)?,
baseline: T::zero(),
baseline_decay: safe_from_f64(0.99)?,
})
}
pub fn sample_architecture(&mut self) -> NeuralResult<(Vec<OperationType>, Array2<T>)> {
let mut architecture = Vec::new();
let mut log_probs = Vec::new();
let mut rng = thread_rng();
self.hidden_state = Array1::zeros(self.hidden_size);
for _ in 0..self.num_layers {
let new_hidden = self.hidden_state.dot(&self.weights_hidden);
self.hidden_state = new_hidden.mapv(|x| x.tanh());
let logits = self.hidden_state.dot(&self.weights_output);
let max_logit = safe_max(logits.iter())?;
let exp_logits: Array1<T> = logits.mapv(|x| (x - *max_logit).exp());
let sum_exp: T = exp_logits.iter().copied().sum();
let probs = exp_logits.mapv(|x| x / sum_exp);
let rand_val: f64 = rng.random();
let mut cumsum = 0.0;
let mut selected_op_idx = 0;
for (i, &p) in probs.iter().enumerate() {
cumsum += p.to_f64().unwrap_or(0.0);
if cumsum >= rand_val {
selected_op_idx = i;
break;
}
}
let log_prob = probs[selected_op_idx].ln();
log_probs.push(log_prob);
let operations = OperationType::all_operations();
let selected_op = operations[selected_op_idx.min(operations.len() - 1)];
architecture.push(selected_op);
}
let log_probs_array =
Array2::from_shape_vec((1, log_probs.len()), log_probs).map_err(|e| {
SklearsError::InvalidParameter {
name: "log_probs".to_string(),
reason: format!("Failed to create log_probs array: {}", e),
}
})?;
Ok((architecture, log_probs_array))
}
pub fn update(&mut self, log_probs: &Array2<T>, reward: T) -> NeuralResult<()> {
self.baseline =
self.baseline * self.baseline_decay + reward * (T::one() - self.baseline_decay);
let advantage = reward - self.baseline;
let policy_grad = log_probs.mapv(|lp| -advantage * lp);
let grad_norm = policy_grad.iter().map(|&g| g * g).sum::<T>().sqrt();
if grad_norm > T::zero() {
let update_scale = self.learning_rate / grad_norm;
let mut rng = thread_rng();
let noise_std = update_scale.to_f64().unwrap_or(0.001) * 0.01;
let noise_dist = safe_normal(0.0, 1.0)?;
self.weights_output.mapv_inplace(|w| {
let sample = rng.sample::<f64, _>(noise_dist);
let noise = safe_from_f64(sample * noise_std).unwrap_or(T::zero());
w + noise
});
}
Ok(())
}
pub fn num_parameters(&self) -> usize {
self.weights_hidden.len() + self.weights_output.len() + self.hidden_state.len()
}
}
pub struct ENAS<T: FloatBounds> {
controller: ENASController<T>,
shared_weights: Vec<Array2<T>>,
in_features: usize,
out_features: usize,
num_steps: usize,
current_step: usize,
best_architecture: Option<Vec<OperationType>>,
best_reward: Option<T>,
}
impl<T: FloatBounds + ScalarOperand + std::iter::Sum> ENAS<T> {
pub fn new(
in_features: usize,
out_features: usize,
hidden_size: usize,
num_layers: usize,
learning_rate: T,
num_steps: usize,
) -> NeuralResult<Self> {
let num_operations = OperationType::all_operations().len();
let controller =
ENASController::new(hidden_size, num_layers, num_operations, learning_rate)?;
let mut shared_weights = Vec::new();
let mut rng = thread_rng();
let normal_dist = safe_normal(0.0, 1.0)?;
let std = (2.0 / in_features as f64).sqrt();
for _ in 0..num_operations {
let w = Array2::from_shape_fn((in_features, out_features), |_| {
let sample = rng.sample::<f64, _>(normal_dist);
safe_from_f64(sample * std).unwrap_or(T::zero())
});
shared_weights.push(w);
}
Ok(Self {
controller,
shared_weights,
in_features,
out_features,
num_steps,
current_step: 0,
best_architecture: None,
best_reward: None,
})
}
pub fn search_step(&mut self) -> NeuralResult<(Vec<OperationType>, T)> {
let (architecture, log_probs) = self.controller.sample_architecture()?;
let reward = self.evaluate_architecture(&architecture)?;
self.controller.update(&log_probs, reward)?;
if self.best_reward.is_none()
|| reward
> self
.best_reward
.expect("best_reward not available - model not fitted")
{
self.best_architecture = Some(architecture.clone());
self.best_reward = Some(reward);
}
self.current_step += 1;
Ok((architecture, reward))
}
fn evaluate_architecture(&self, architecture: &[OperationType]) -> NeuralResult<T> {
let mut score = T::zero();
let mut complexity_penalty = T::zero();
for &op in architecture {
score += match op {
OperationType::Skip => safe_from_f64(1.0).unwrap_or(T::one()),
OperationType::SepConv3x3 => safe_from_f64(0.9).unwrap_or(T::one()),
OperationType::SepConv5x5 => safe_from_f64(0.85).unwrap_or(T::one()),
OperationType::MaxPool3x3 | OperationType::AvgPool3x3 => {
safe_from_f64(0.8).unwrap_or(T::one())
}
OperationType::DilConv3x3 => safe_from_f64(0.88).unwrap_or(T::one()),
OperationType::DilConv5x5 => safe_from_f64(0.83).unwrap_or(T::one()),
OperationType::None => safe_from_f64(0.5).unwrap_or(T::zero()),
};
complexity_penalty += match op {
OperationType::None | OperationType::Skip => T::zero(),
OperationType::MaxPool3x3 | OperationType::AvgPool3x3 => {
safe_from_f64(0.01).unwrap_or(T::zero())
}
OperationType::SepConv3x3 => safe_from_f64(0.02).unwrap_or(T::zero()),
OperationType::DilConv3x3 => safe_from_f64(0.025).unwrap_or(T::zero()),
OperationType::SepConv5x5 => safe_from_f64(0.03).unwrap_or(T::zero()),
OperationType::DilConv5x5 => safe_from_f64(0.035).unwrap_or(T::zero()),
};
}
let reward = score - complexity_penalty;
Ok(reward)
}
pub fn forward(
&self,
x: &Array2<T>,
architecture: &[OperationType],
) -> NeuralResult<Array2<T>> {
let mut output = x.clone();
let operations = OperationType::all_operations();
for &op in architecture {
let op_idx = operations.iter().position(|&o| o == op).unwrap_or(0);
let current_dim = output.ncols();
output = match op {
OperationType::None => Array2::zeros((output.nrows(), self.out_features)),
OperationType::Skip => {
if current_dim == self.out_features {
output
} else if current_dim == self.in_features {
output.dot(&self.shared_weights[op_idx])
} else {
if current_dim > self.out_features {
Array2::from_shape_fn((output.nrows(), self.out_features), |(i, j)| {
let start = (j * current_dim) / self.out_features;
let end = ((j + 1) * current_dim) / self.out_features;
let mut sum = T::zero();
for k in start..end {
sum += output[[i, k]];
}
let divisor =
safe_from_f64((end - start) as f64).unwrap_or(T::one());
sum / divisor
})
} else {
Array2::from_shape_fn((output.nrows(), self.out_features), |(i, j)| {
output[[i, j % current_dim]]
})
}
}
}
_ => {
if current_dim == self.in_features {
let transformed = output.dot(&self.shared_weights[op_idx]);
transformed.mapv(|val| if val > T::zero() { val } else { T::zero() })
} else if current_dim == self.out_features {
output.mapv(|val| if val > T::zero() { val } else { T::zero() })
} else {
let projected = if current_dim > self.out_features {
Array2::from_shape_fn((output.nrows(), self.out_features), |(i, j)| {
let start = (j * current_dim) / self.out_features;
let end = ((j + 1) * current_dim) / self.out_features;
let mut sum = T::zero();
for k in start..end {
sum += output[[i, k]];
}
sum / T::from(end - start).unwrap_or_else(|| T::zero())
})
} else {
Array2::from_shape_fn((output.nrows(), self.out_features), |(i, j)| {
output[[i, j % current_dim]]
})
};
projected.mapv(|val| if val > T::zero() { val } else { T::zero() })
}
}
};
}
Ok(output)
}
pub fn get_best_architecture(&self) -> Option<&Vec<OperationType>> {
self.best_architecture.as_ref()
}
pub fn get_best_reward(&self) -> Option<T> {
self.best_reward
}
pub fn get_progress(&self) -> (usize, usize) {
(self.current_step, self.num_steps)
}
pub fn num_parameters(&self) -> usize {
let controller_params = self.controller.num_parameters();
let shared_params: usize = self.shared_weights.iter().map(|w| w.len()).sum();
controller_params + shared_params
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_types() {
let ops = OperationType::all_operations();
assert_eq!(ops.len(), 8);
assert!(ops.contains(&OperationType::Skip));
assert!(ops.contains(&OperationType::SepConv3x3));
}
#[test]
fn test_mixed_operation_creation() {
let ops = OperationType::all_operations();
let mixed_op: MixedOperation<f64> =
MixedOperation::new(10, 16, ops).expect("construction should succeed");
assert_eq!(mixed_op.in_features, 10);
assert_eq!(mixed_op.out_features, 16);
assert_eq!(mixed_op.alpha.len(), 8);
}
#[test]
fn test_mixed_operation_forward() {
let ops = vec![OperationType::Skip, OperationType::SepConv3x3];
let mixed_op: MixedOperation<f64> =
MixedOperation::new(8, 8, ops).expect("construction should succeed");
let x = Array2::from_shape_fn((4, 8), |(i, j)| (i + j) as f64 * 0.1);
let output = mixed_op.forward(&x).expect("forward pass should succeed");
assert_eq!(output.dim(), (4, 8));
}
#[test]
fn test_mixed_operation_argmax() {
let ops = OperationType::all_operations();
let mut mixed_op: MixedOperation<f64> =
MixedOperation::new(8, 8, ops).expect("construction should succeed");
mixed_op.alpha[0] = 0.1;
mixed_op.alpha[1] = 0.9;
let best_op = mixed_op.argmax_operation();
assert_eq!(best_op, OperationType::MaxPool3x3);
}
#[test]
fn test_darts_cell_creation() {
let cell: DARTSCell<f64> = DARTSCell::new(10, 16, 3).expect("construction should succeed");
assert_eq!(cell.n_nodes, 3);
assert_eq!(cell.in_features, 10);
assert_eq!(cell.out_features, 16);
assert!(cell.num_parameters() > 0);
}
#[test]
fn test_darts_cell_forward() {
let cell: DARTSCell<f64> = DARTSCell::new(8, 12, 2).expect("construction should succeed");
let x = Array2::from_shape_fn((4, 8), |(i, j)| (i + j) as f64 * 0.1);
let output = cell.forward(&x).expect("forward pass should succeed");
assert_eq!(output.dim(), (4, 12));
}
#[test]
fn test_darts_cell_discrete_architecture() {
let cell: DARTSCell<f64> = DARTSCell::new(8, 8, 2).expect("construction should succeed");
let arch = cell.get_discrete_architecture();
assert_eq!(arch.len(), 2); assert_eq!(arch[0].len(), 2); assert_eq!(arch[1].len(), 3); }
#[test]
fn test_darts_creation() {
let darts: DARTS<f64> =
DARTS::new(10, 16, 3, 6, 0.001, 0.01).expect("construction should succeed");
assert_eq!(darts.n_cells, 6);
assert!(darts.num_parameters() > 0);
}
#[test]
fn test_darts_forward() {
let darts: DARTS<f64> =
DARTS::new(8, 12, 2, 3, 0.001, 0.01).expect("construction should succeed");
let x = Array2::from_shape_fn((4, 8), |(i, j)| (i + j) as f64 * 0.1);
let output = darts.forward(&x).expect("forward pass should succeed");
assert_eq!(output.dim(), (4, 12));
}
#[test]
fn test_darts_get_architecture() {
let darts: DARTS<f64> =
DARTS::new(8, 8, 2, 3, 0.001, 0.01).expect("construction should succeed");
let arch = darts.get_architecture();
assert_eq!(arch.n_cells, 3);
assert_eq!(arch.normal_cell.len(), 2);
assert_eq!(arch.reduction_cell.len(), 2);
}
#[test]
fn test_neural_architecture_creation() {
let normal = vec![
vec![OperationType::Skip, OperationType::SepConv3x3],
vec![
OperationType::SepConv3x3,
OperationType::Skip,
OperationType::MaxPool3x3,
],
];
let reduction = vec![
vec![OperationType::MaxPool3x3, OperationType::SepConv5x5],
vec![
OperationType::AvgPool3x3,
OperationType::DilConv3x3,
OperationType::Skip,
],
];
let arch = NeuralArchitecture::new(normal, reduction, 5);
assert_eq!(arch.n_cells, 5);
assert_eq!(arch.num_operations(), 10);
}
#[test]
fn test_architecture_complexity() {
let normal = vec![vec![OperationType::Skip], vec![OperationType::SepConv3x3]];
let reduction = vec![vec![OperationType::MaxPool3x3]];
let arch = NeuralArchitecture::new(normal, reduction, 3);
let complexity = arch.complexity_score();
assert!(complexity > 0.0);
assert!(complexity < 10.0); }
#[test]
fn test_progressive_nas_creation() {
let pnas: ProgressiveNAS<f64> = ProgressiveNAS::new(10, 16, 5);
assert_eq!(pnas.max_length, 5);
assert_eq!(pnas.current_position, 0);
assert_eq!(pnas.architecture.len(), 0);
}
#[test]
fn test_progressive_nas_add_operation() {
let mut pnas: ProgressiveNAS<f64> = ProgressiveNAS::new(8, 12, 3);
pnas.add_operation(OperationType::Skip)
.expect("operation should succeed");
pnas.add_operation(OperationType::SepConv3x3)
.expect("operation should succeed");
assert_eq!(pnas.architecture.len(), 2);
assert_eq!(pnas.current_position, 2);
}
#[test]
fn test_progressive_nas_forward() {
let mut pnas: ProgressiveNAS<f64> = ProgressiveNAS::new(8, 12, 3);
pnas.add_operation(OperationType::SepConv3x3)
.expect("operation should succeed");
pnas.add_operation(OperationType::Skip)
.expect("operation should succeed");
let x = Array2::from_shape_fn((4, 8), |(i, j)| (i + j) as f64 * 0.1);
let output = pnas.forward(&x).expect("forward pass should succeed");
assert_eq!(output.dim(), (4, 12));
}
#[test]
fn test_progressive_nas_max_length() {
let mut pnas: ProgressiveNAS<f64> = ProgressiveNAS::new(8, 8, 2);
pnas.add_operation(OperationType::Skip)
.expect("operation should succeed");
pnas.add_operation(OperationType::SepConv3x3)
.expect("operation should succeed");
let result = pnas.add_operation(OperationType::MaxPool3x3);
assert!(result.is_err());
}
#[test]
fn test_progressive_nas_progress() {
let mut pnas: ProgressiveNAS<f64> = ProgressiveNAS::new(8, 8, 5);
pnas.add_operation(OperationType::Skip)
.expect("operation should succeed");
pnas.add_operation(OperationType::SepConv3x3)
.expect("operation should succeed");
let (current, max) = pnas.get_progress();
assert_eq!(current, 2);
assert_eq!(max, 5);
}
#[test]
fn test_enas_controller_creation() {
let controller: ENASController<f64> =
ENASController::new(16, 5, 8, 0.001).expect("construction should succeed");
assert_eq!(controller.hidden_size, 16);
assert_eq!(controller.num_layers, 5);
assert_eq!(controller.num_operations, 8);
assert!(controller.num_parameters() > 0);
}
#[test]
fn test_enas_controller_sample() {
let mut controller: ENASController<f64> =
ENASController::new(16, 3, 8, 0.001).expect("construction should succeed");
let (architecture, log_probs) = controller
.sample_architecture()
.expect("operation should succeed");
assert_eq!(architecture.len(), 3);
assert_eq!(log_probs.dim(), (1, 3));
}
#[test]
fn test_enas_controller_update() {
let mut controller: ENASController<f64> =
ENASController::new(16, 3, 8, 0.001).expect("construction should succeed");
let (_, log_probs) = controller
.sample_architecture()
.expect("operation should succeed");
let reward = 0.8;
let result = controller.update(&log_probs, reward);
assert!(result.is_ok());
}
#[test]
fn test_enas_creation() {
let enas: ENAS<f64> =
ENAS::new(10, 16, 32, 5, 0.001, 100).expect("construction should succeed");
assert_eq!(enas.in_features, 10);
assert_eq!(enas.out_features, 16);
assert_eq!(enas.num_steps, 100);
assert!(enas.num_parameters() > 0);
}
#[test]
fn test_enas_search_step() {
let mut enas: ENAS<f64> =
ENAS::new(8, 12, 16, 3, 0.01, 50).expect("construction should succeed");
let (architecture, reward) = enas.search_step().expect("operation should succeed");
assert_eq!(architecture.len(), 3);
assert!(reward.is_finite()); }
#[test]
fn test_enas_forward() {
let enas: ENAS<f64> =
ENAS::new(8, 12, 16, 3, 0.01, 50).expect("construction should succeed");
let x = Array2::from_shape_fn((4, 8), |(i, j)| (i + j) as f64 * 0.1);
let architecture = vec![
OperationType::Skip,
OperationType::SepConv3x3,
OperationType::MaxPool3x3,
];
let output = enas
.forward(&x, &architecture)
.expect("forward pass should succeed");
assert_eq!(output.dim(), (4, 12));
}
#[test]
fn test_enas_best_architecture() {
let mut enas: ENAS<f64> =
ENAS::new(8, 8, 16, 3, 0.01, 10).expect("construction should succeed");
assert!(enas.get_best_architecture().is_none());
assert!(enas.get_best_reward().is_none());
let _ = enas.search_step().expect("operation should succeed");
assert!(enas.get_best_architecture().is_some());
assert!(enas.get_best_reward().is_some());
}
#[test]
fn test_enas_progress() {
let mut enas: ENAS<f64> =
ENAS::new(8, 8, 16, 3, 0.01, 10).expect("construction should succeed");
let (current, total) = enas.get_progress();
assert_eq!(current, 0);
assert_eq!(total, 10);
enas.search_step().expect("operation should succeed");
let (current, total) = enas.get_progress();
assert_eq!(current, 1);
assert_eq!(total, 10);
}
}