Skip to main content

sklears_neural/
utils.rs

1//! Neural network utility functions and helper modules.
2//!
3//! This module provides core neural network utility functions including
4//! weight initialization, batch processing, early stopping, encoding utilities,
5//! and accuracy metrics.
6
7use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::RngExt;
9
10/// Type alias for neural network weights and biases
11pub type WeightsAndBiases = (Vec<Array2<f64>>, Vec<Array1<f64>>);
12
13/// Weight initialization strategies
14#[derive(Debug, Clone, PartialEq)]
15pub enum WeightInit {
16    /// Initialize all weights to zero (not recommended for most layers)
17    Zero,
18    /// Uniform random initialization in `(-1, 1)` without any scaling
19    Random,
20    /// Xavier/Glorot initialization: scales by `sqrt(2 / (fan_in + fan_out))`
21    Xavier,
22    /// He/Kaiming initialization: scales by `sqrt(2 / fan_in)`, suited for ReLU activations
23    He,
24    /// Uniform distribution over `[low, high)`
25    Uniform {
26        /// Lower bound of the uniform distribution
27        low: f64,
28        /// Upper bound of the uniform distribution
29        high: f64,
30    },
31    /// Gaussian distribution with specified mean and standard deviation
32    Normal {
33        /// Mean of the Gaussian distribution
34        mean: f64,
35        /// Standard deviation of the Gaussian distribution
36        std: f64,
37    },
38}
39
40/// Batch configuration for training
41#[derive(Debug, Clone)]
42pub struct BatchConfig {
43    /// Number of samples per mini-batch
44    pub batch_size: usize,
45    /// Whether to shuffle training samples at the start of each epoch
46    pub shuffle: bool,
47}
48
49impl Default for BatchConfig {
50    fn default() -> Self {
51        Self {
52            batch_size: 32,
53            shuffle: true,
54        }
55    }
56}
57
58/// Early stopping configuration and implementation
59#[derive(Debug, Clone)]
60pub struct EarlyStopping {
61    /// Number of epochs with no improvement above `min_delta` before stopping
62    pub patience: usize,
63    /// Minimum decrease in loss required to be counted as an improvement
64    pub min_delta: f64,
65    /// Whether to restore the weights from the best epoch when stopping
66    pub restore_best_weights: bool,
67    current_patience: usize,
68    best_loss: f64,
69    best_weights: Option<WeightsAndBiases>,
70}
71
72impl EarlyStopping {
73    /// Create a new `EarlyStopping` monitor with the given patience, minimum improvement delta, and weight-restoration flag
74    pub fn new(patience: usize, min_delta: f64, restore_best_weights: bool) -> Self {
75        Self {
76            patience,
77            min_delta,
78            restore_best_weights,
79            current_patience: 0,
80            best_loss: f64::INFINITY,
81            best_weights: None,
82        }
83    }
84
85    /// Record the current `loss` and return `true` when patience has been exhausted
86    pub fn should_stop(
87        &mut self,
88        loss: f64,
89        weights: &[Array2<f64>],
90        biases: &[Array1<f64>],
91    ) -> bool {
92        if loss < self.best_loss - self.min_delta {
93            self.best_loss = loss;
94            self.current_patience = 0;
95            if self.restore_best_weights {
96                self.best_weights = Some((weights.to_vec(), biases.to_vec()));
97            }
98            false
99        } else {
100            self.current_patience += 1;
101            self.current_patience >= self.patience
102        }
103    }
104
105    /// Return a reference to the weights saved at the best epoch, if any were recorded
106    pub fn get_best_weights(&self) -> Option<&WeightsAndBiases> {
107        self.best_weights.as_ref()
108    }
109}
110
111/// One-hot encoding utility functions
112pub fn one_hot_encode(labels: &[usize], num_classes: Option<usize>) -> Array2<f64> {
113    let n_samples = labels.len();
114    let n_classes = num_classes.unwrap_or_else(|| labels.iter().max().unwrap_or(&0) + 1);
115
116    let mut encoded = Array2::zeros((n_samples, n_classes));
117    for (i, &label) in labels.iter().enumerate() {
118        if label < n_classes {
119            encoded[[i, label]] = 1.0;
120        }
121    }
122    encoded
123}
124
125/// Decode one-hot encoded data back to labels
126pub fn one_hot_decode(encoded: &Array2<f64>) -> Vec<usize> {
127    encoded
128        .rows()
129        .into_iter()
130        .map(|row| {
131            row.iter()
132                .enumerate()
133                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
134                .map(|(idx, _)| idx)
135                .unwrap_or(0)
136        })
137        .collect()
138}
139
140/// Calculate accuracy between predictions and true labels
141pub fn accuracy(y_true: &[usize], y_pred: &[usize]) -> f64 {
142    if y_true.len() != y_pred.len() {
143        return 0.0;
144    }
145
146    let correct = y_true
147        .iter()
148        .zip(y_pred.iter())
149        .filter(|(true_val, pred_val)| true_val == pred_val)
150        .count();
151
152    correct as f64 / y_true.len() as f64
153}
154
155/// Create batches for training data
156pub fn create_batches<R: scirs2_core::random::Rng>(
157    x: &Array2<f64>,
158    y: &[usize],
159    batch_size: usize,
160    shuffle: bool,
161    rng: &mut R,
162) -> Vec<(Array2<f64>, Vec<usize>)> {
163    let n_samples = x.nrows();
164    let mut indices: Vec<usize> = (0..n_samples).collect();
165
166    if shuffle {
167        use scirs2_core::random::seq::SliceRandom;
168        indices.shuffle(rng);
169    }
170
171    let mut batches = Vec::new();
172    for chunk in indices.chunks(batch_size) {
173        let batch_x = Array2::from_shape_vec(
174            (chunk.len(), x.ncols()),
175            chunk.iter().flat_map(|&i| x.row(i).to_vec()).collect(),
176        )
177        .expect("value should be present");
178
179        let batch_y: Vec<usize> = chunk.iter().map(|&i| y[i]).collect();
180        batches.push((batch_x, batch_y));
181    }
182
183    batches
184}
185
186/// Create batches for regression data
187pub fn create_batches_regression<R: scirs2_core::random::Rng>(
188    x: &Array2<f64>,
189    y: &Array2<f64>,
190    batch_size: usize,
191    shuffle: bool,
192    rng: &mut R,
193) -> Vec<(Array2<f64>, Array2<f64>)> {
194    let n_samples = x.nrows();
195    let mut indices: Vec<usize> = (0..n_samples).collect();
196
197    if shuffle {
198        use scirs2_core::random::seq::SliceRandom;
199        indices.shuffle(rng);
200    }
201
202    let mut batches = Vec::new();
203    for chunk in indices.chunks(batch_size) {
204        let batch_x = Array2::from_shape_vec(
205            (chunk.len(), x.ncols()),
206            chunk.iter().flat_map(|&i| x.row(i).to_vec()).collect(),
207        )
208        .expect("value should be present");
209
210        let batch_y = Array2::from_shape_vec(
211            (chunk.len(), y.ncols()),
212            chunk.iter().flat_map(|&i| y.row(i).to_vec()).collect(),
213        )
214        .expect("value should be present");
215        batches.push((batch_x, batch_y));
216    }
217
218    batches
219}
220
221/// Initialize weights based on initialization strategy
222pub fn initialize_weights<R: scirs2_core::random::Rng>(
223    rows: usize,
224    cols: usize,
225    init: &WeightInit,
226    rng: &mut R,
227) -> Array2<f64> {
228    use scirs2_core::random::essentials::{Normal, Uniform};
229
230    match init {
231        WeightInit::Zero => Array2::zeros((rows, cols)),
232        WeightInit::Random => {
233            let dist = Uniform::new(-1.0, 1.0).expect("valid distribution params");
234            Array2::from_shape_simple_fn((rows, cols), || rng.sample(dist))
235        }
236        WeightInit::Xavier => {
237            let fan_avg = (rows + cols) as f64 / 2.0;
238            let bound = (6.0 / fan_avg).sqrt();
239            let dist = Uniform::new(-bound, bound).expect("valid distribution params");
240            Array2::from_shape_simple_fn((rows, cols), || rng.sample(dist))
241        }
242        WeightInit::He => {
243            let std = (2.0 / rows as f64).sqrt();
244            let dist = Normal::new(0.0, std).expect("valid distribution params");
245            Array2::from_shape_simple_fn((rows, cols), || rng.sample(dist))
246        }
247        WeightInit::Uniform { low, high } => {
248            let dist = Uniform::new(*low, *high).expect("valid distribution params");
249            Array2::from_shape_simple_fn((rows, cols), || rng.sample(dist))
250        }
251        WeightInit::Normal { mean, std } => {
252            let dist = Normal::new(*mean, *std).expect("valid distribution params");
253            Array2::from_shape_simple_fn((rows, cols), || rng.sample(dist))
254        }
255    }
256}
257
258/// Initialize biases
259pub fn initialize_biases<R: scirs2_core::random::Rng>(
260    size: usize,
261    init: &WeightInit,
262    rng: &mut R,
263) -> Array1<f64> {
264    use scirs2_core::random::essentials::{Normal, Uniform};
265
266    match init {
267        WeightInit::Zero => Array1::zeros(size),
268        WeightInit::Random => {
269            let dist = Uniform::new(-1.0, 1.0).expect("valid distribution params");
270            Array1::from_shape_simple_fn(size, || rng.sample(dist))
271        }
272        WeightInit::Xavier => {
273            let bound = (6.0 / size as f64).sqrt();
274            let dist = Uniform::new(-bound, bound).expect("valid distribution params");
275            Array1::from_shape_simple_fn(size, || rng.sample(dist))
276        }
277        WeightInit::He => {
278            let std = (2.0 / size as f64).sqrt();
279            let dist = Normal::new(0.0, std).expect("valid distribution params");
280            Array1::from_shape_simple_fn(size, || rng.sample(dist))
281        }
282        WeightInit::Uniform { low, high } => {
283            let dist = Uniform::new(*low, *high).expect("valid distribution params");
284            Array1::from_shape_simple_fn(size, || rng.sample(dist))
285        }
286        WeightInit::Normal { mean, std } => {
287            let dist = Normal::new(*mean, *std).expect("valid distribution params");
288            Array1::from_shape_simple_fn(size, || rng.sample(dist))
289        }
290    }
291}