Skip to main content

optirs_core/regularizers/
manifold.rs

1// Manifold regularization
2//
3// Manifold regularization assumes that data lies on a low-dimensional manifold
4// and encourages smoothness along this manifold structure.
5
6use scirs2_core::ndarray::{Array, Array2, ArrayBase, Data, Dimension, ScalarOperand};
7use scirs2_core::numeric::{Float, FromPrimitive};
8use std::fmt::Debug;
9
10use crate::error::{OptimError, Result};
11use crate::regularizers::Regularizer;
12
13/// Manifold regularization
14///
15/// Regularizes based on the assumption that data lies on a low-dimensional manifold,
16/// encouraging smooth predictions along the manifold structure.
17///
18/// # Example
19///
20/// ```no_run
21/// use scirs2_core::ndarray::array;
22/// use optirs_core::regularizers::{ManifoldRegularization, Regularizer};
23///
24/// let mut manifold_reg = ManifoldRegularization::new(0.01);
25///
26/// // Set up similarity matrix based on data structure
27/// let similarity = array![[1.0, 0.8], [0.8, 1.0]];
28/// manifold_reg.set_similarity_matrix(similarity).expect("unwrap failed");
29///
30/// let params = array![[1.0, 2.0], [3.0, 4.0]];
31/// let mut gradient = array![[0.1, 0.2], [0.3, 0.4]];
32///
33/// // Apply manifold regularization
34/// let penalty = manifold_reg.apply(&params, &mut gradient).expect("unwrap failed");
35/// ```
36#[derive(Debug, Clone)]
37pub struct ManifoldRegularization<A: Float> {
38    /// Regularization strength
39    lambda: A,
40    /// Similarity/adjacency matrix encoding manifold structure  
41    similarity_matrix: Option<Array2<A>>,
42    /// Degree matrix for normalization
43    degree_matrix: Option<Array2<A>>,
44    /// Laplacian matrix  
45    laplacian: Option<Array2<A>>,
46}
47
48impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> ManifoldRegularization<A> {
49    /// Create a new manifold regularization
50    ///
51    /// # Arguments
52    ///
53    /// * `lambda` - Regularization strength
54    pub fn new(lambda: A) -> Self {
55        Self {
56            lambda,
57            similarity_matrix: None,
58            degree_matrix: None,
59            laplacian: None,
60        }
61    }
62
63    /// Set the similarity matrix encoding manifold structure
64    ///
65    /// # Arguments
66    ///
67    /// * `similarity` - Symmetric similarity/adjacency matrix
68    pub fn set_similarity_matrix(&mut self, similarity: Array2<A>) -> Result<()> {
69        let (rows, cols) = similarity.dim();
70        if rows != cols {
71            return Err(OptimError::InvalidConfig(
72                "Similarity matrix must be square".to_string(),
73            ));
74        }
75
76        // Compute degree matrix (diagonal matrix of row sums)
77        let mut degree = Array2::zeros((rows, rows));
78        for i in 0..rows {
79            let row_sum = similarity.row(i).sum();
80            degree[[i, i]] = row_sum;
81        }
82
83        // Compute Laplacian: L = D - W  (not normalized for simplicity)
84        let laplacian = &degree - &similarity;
85
86        self.similarity_matrix = Some(similarity);
87        self.degree_matrix = Some(degree);
88        self.laplacian = Some(laplacian);
89
90        Ok(())
91    }
92
93    /// Compute manifold regularization penalty
94    pub fn compute_penalty<S>(&self, params: &ArrayBase<S, scirs2_core::ndarray::Ix2>) -> Result<A>
95    where
96        S: Data<Elem = A>,
97    {
98        let laplacian = self
99            .laplacian
100            .as_ref()
101            .ok_or_else(|| OptimError::InvalidConfig("Similarity matrix not set".to_string()))?;
102
103        // Penalty = λ * tr(F^T * L * F) where F are the parameters
104        // For efficiency, compute as λ * sum(F .* (L * F))
105        let lf = laplacian.dot(params);
106        let penalty = params
107            .iter()
108            .zip(lf.iter())
109            .map(|(p, lf)| *p * *lf)
110            .fold(A::zero(), |acc, val| acc + val);
111
112        Ok(self.lambda * penalty)
113    }
114
115    /// Compute gradient of manifold regularization
116    fn compute_gradient<S>(
117        &self,
118        params: &ArrayBase<S, scirs2_core::ndarray::Ix2>,
119    ) -> Result<Array2<A>>
120    where
121        S: Data<Elem = A>,
122    {
123        let laplacian = self
124            .laplacian
125            .as_ref()
126            .ok_or_else(|| OptimError::InvalidConfig("Similarity matrix not set".to_string()))?;
127
128        // Gradient = 2 * λ * L * F
129        let gradient =
130            laplacian.dot(params) * (A::from_f64(2.0).expect("unwrap failed") * self.lambda);
131        Ok(gradient)
132    }
133}
134
135// Implement Regularizer trait
136impl<
137        A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync,
138        D: Dimension + Send + Sync,
139    > Regularizer<A, D> for ManifoldRegularization<A>
140{
141    fn apply(&self, params: &Array<A, D>, gradients: &mut Array<A, D>) -> Result<A> {
142        if params.ndim() != 2 {
143            // Only apply to 2D parameter matrices
144            return Ok(A::zero());
145        }
146
147        // Downcast to 2D
148        let params_2d = params
149            .view()
150            .into_dimensionality::<scirs2_core::ndarray::Ix2>()
151            .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
152
153        let gradient_update = self.compute_gradient(&params_2d)?;
154
155        // Add manifold regularization gradient
156        let mut gradients_2d = gradients
157            .view_mut()
158            .into_dimensionality::<scirs2_core::ndarray::Ix2>()
159            .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
160
161        gradients_2d.zip_mut_with(&gradient_update, |g, &u| *g = *g + u);
162
163        // Return penalty
164        self.compute_penalty(&params_2d)
165    }
166
167    fn penalty(&self, params: &Array<A, D>) -> Result<A> {
168        if params.ndim() != 2 {
169            // Only apply to 2D parameter matrices
170            return Ok(A::zero());
171        }
172
173        // Downcast to 2D
174        let params_2d = params
175            .view()
176            .into_dimensionality::<scirs2_core::ndarray::Ix2>()
177            .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
178
179        self.compute_penalty(&params_2d)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use approx::assert_relative_eq;
187    use scirs2_core::ndarray::array;
188
189    #[test]
190    fn test_manifold_creation() {
191        let manifold = ManifoldRegularization::<f64>::new(0.01);
192        assert_eq!(manifold.lambda, 0.01);
193        assert!(manifold.similarity_matrix.is_none());
194    }
195
196    #[test]
197    fn test_set_similarity_matrix() {
198        let mut manifold = ManifoldRegularization::new(0.01);
199
200        // Simple 2x2 similarity matrix
201        let similarity = array![[1.0, 0.5], [0.5, 1.0]];
202
203        assert!(manifold.set_similarity_matrix(similarity).is_ok());
204        assert!(manifold.laplacian.is_some());
205
206        // Check Laplacian computation
207        let laplacian = manifold.laplacian.as_ref().expect("unwrap failed");
208        // For matrix [[1.0, 0.5], [0.5, 1.0]]:
209        // Degree matrix is [[1.5, 0.0], [0.0, 1.5]]
210        // Laplacian is [[1.5, 0.0], [0.0, 1.5]] - [[1.0, 0.5], [0.5, 1.0]] = [[0.5, -0.5], [-0.5, 0.5]]
211        assert_relative_eq!(laplacian[[0, 0]], 0.5, epsilon = 1e-10);
212        assert_relative_eq!(laplacian[[0, 1]], -0.5, epsilon = 1e-10);
213    }
214
215    #[test]
216    fn test_invalid_similarity_matrix() {
217        let mut manifold = ManifoldRegularization::<f64>::new(0.01);
218
219        // Non-square matrix should fail
220        let similarity = array![[1.0, 0.5, 0.3], [0.5, 1.0, 0.4]];
221        assert!(manifold.set_similarity_matrix(similarity).is_err());
222    }
223
224    #[test]
225    fn test_penalty_without_similarity() {
226        let manifold = ManifoldRegularization::<f64>::new(0.01);
227        let params = array![[1.0, 2.0], [3.0, 4.0]];
228
229        // Should fail without similarity matrix
230        assert!(manifold.compute_penalty(&params).is_err());
231    }
232
233    #[test]
234    fn test_penalty_computation() {
235        let mut manifold = ManifoldRegularization::new(0.1);
236
237        // Set up similarity matrix
238        let similarity = array![[1.0, 0.8], [0.8, 1.0]];
239        manifold
240            .set_similarity_matrix(similarity)
241            .expect("unwrap failed");
242
243        // Test penalty computation
244        let params = array![[1.0, 0.0], [0.0, 1.0]];
245        let penalty = manifold.compute_penalty(&params).expect("unwrap failed");
246
247        // Penalty should be positive for non-similar parameters
248        assert!(penalty > 0.0);
249    }
250
251    #[test]
252    fn test_gradient_computation() {
253        let mut manifold = ManifoldRegularization::new(0.1);
254
255        // Set up similarity matrix
256        let similarity = array![[1.0, 0.8], [0.8, 1.0]];
257        manifold
258            .set_similarity_matrix(similarity)
259            .expect("unwrap failed");
260
261        let params = array![[1.0, 2.0], [3.0, 4.0]];
262        let gradient = manifold.compute_gradient(&params).expect("unwrap failed");
263
264        // Gradient should be non-zero
265        assert!(gradient.abs().sum() > 0.0);
266    }
267
268    #[test]
269    fn test_regularizer_trait() {
270        let mut manifold = ManifoldRegularization::new(0.01);
271
272        // Set up similarity matrix
273        let similarity = array![[1.0, 0.6], [0.6, 1.0]];
274        manifold
275            .set_similarity_matrix(similarity)
276            .expect("unwrap failed");
277
278        let params = array![[1.0, 2.0], [3.0, 4.0]];
279        let mut gradient = array![[0.1, 0.2], [0.3, 0.4]];
280        let original_gradient = gradient.clone();
281
282        let penalty = manifold
283            .apply(&params, &mut gradient)
284            .expect("unwrap failed");
285
286        // Penalty should be positive
287        assert!(penalty > 0.0);
288
289        // Gradient should be modified
290        assert_ne!(gradient, original_gradient);
291    }
292
293    #[test]
294    fn test_identity_similarity() {
295        let mut manifold = ManifoldRegularization::new(0.1);
296
297        // Identity similarity matrix (no connections)
298        let similarity = array![[1.0, 0.0], [0.0, 1.0]];
299        manifold
300            .set_similarity_matrix(similarity)
301            .expect("unwrap failed");
302
303        let params = array![[1.0, 2.0], [3.0, 4.0]];
304        let penalty = manifold.compute_penalty(&params).expect("unwrap failed");
305
306        // With identity similarity, penalty depends on diagonal structure
307        assert!(penalty >= 0.0);
308    }
309}