optirs_core/regularizers/
orthogonal.rs1use scirs2_core::ndarray::{Array, ArrayBase, Data, Dimension, Ix2, ScalarOperand};
7use scirs2_core::numeric::{Float, FromPrimitive};
8use std::fmt::Debug;
9
10use crate::error::{OptimError, Result};
11use crate::regularizers::Regularizer;
12
13#[derive(Debug, Clone)]
32pub struct OrthogonalRegularization<A: Float> {
33 lambda: A,
35}
36
37impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> OrthogonalRegularization<A> {
38 pub fn new(lambda: A) -> Self {
44 Self { lambda }
45 }
46
47 pub fn compute_penalty_2d<S: Data<Elem = A>>(&self, weights: &ArrayBase<S, Ix2>) -> A {
49 let n = weights.nrows().min(weights.ncols());
50 let eye = Array::<A, Ix2>::eye(n);
51
52 let wtw = weights.t().dot(weights);
54
55 let mut penalty = A::zero();
57 for i in 0..n {
58 for j in 0..n {
59 let diff = wtw[[i, j]] - eye[[i, j]];
60 penalty = penalty + diff * diff;
61 }
62 }
63
64 if weights.nrows() != weights.ncols() {
66 let (rows, cols) = wtw.dim();
67 for i in 0..rows {
68 for j in 0..cols {
69 if i >= n || j >= n {
70 penalty = penalty + wtw[[i, j]] * wtw[[i, j]];
71 }
72 }
73 }
74 }
75
76 self.lambda * penalty
77 }
78
79 fn compute_gradient_2d<S: Data<Elem = A>>(&self, weights: &ArrayBase<S, Ix2>) -> Array<A, Ix2> {
81 let n = weights.nrows().min(weights.ncols());
82
83 let wtw = weights.t().dot(weights);
85
86 let mut diff = wtw.clone();
88 for i in 0..n {
89 diff[[i, i]] = diff[[i, i]] - A::one();
90 }
91
92 let two =
93 A::from_f64(2.0).expect("OrthogonalRegularization: integer literal 2.0 must fit in A");
94 weights.dot(&diff) * (two * self.lambda)
95 }
96}
97
98impl<
100 A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync,
101 D: Dimension + Send + Sync,
102 > Regularizer<A, D> for OrthogonalRegularization<A>
103{
104 fn apply(&self, params: &Array<A, D>, gradients: &mut Array<A, D>) -> Result<A> {
105 if params.ndim() != 2 {
106 return Ok(A::zero());
108 }
109
110 let params_2d = params
112 .view()
113 .into_dimensionality::<Ix2>()
114 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
115
116 let gradient_update = self.compute_gradient_2d(¶ms_2d);
117
118 let mut gradients_2d = gradients
120 .view_mut()
121 .into_dimensionality::<Ix2>()
122 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
123
124 gradients_2d.zip_mut_with(&gradient_update, |g, &u| *g = *g + u);
126
127 Ok(self.compute_penalty_2d(¶ms_2d))
129 }
130
131 fn penalty(&self, params: &Array<A, D>) -> Result<A> {
132 if params.ndim() != 2 {
133 return Ok(A::zero());
135 }
136
137 let params_2d = params
139 .view()
140 .into_dimensionality::<Ix2>()
141 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
142
143 Ok(self.compute_penalty_2d(¶ms_2d))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use approx::assert_relative_eq;
151 use scirs2_core::ndarray::{array, Array3};
152
153 #[test]
154 fn test_orthogonal_creation() {
155 let ortho = OrthogonalRegularization::<f64>::new(0.01);
156 assert_eq!(ortho.lambda, 0.01);
157 }
158
159 #[test]
160 fn test_identity_matrix_penalty() {
161 let ortho = OrthogonalRegularization::new(0.01);
162
163 let weights = array![[1.0, 0.0], [0.0, 1.0]];
165 let penalty = ortho.compute_penalty_2d(&weights);
166
167 assert_relative_eq!(penalty, 0.0, epsilon = 1e-10);
168 }
169
170 #[test]
171 fn test_non_orthogonal_penalty() {
172 let ortho = OrthogonalRegularization::new(0.01);
173
174 let weights = array![[1.0, 0.5], [0.5, 1.0]];
176 let penalty = ortho.compute_penalty_2d(&weights);
177
178 assert!(penalty > 0.0);
179 }
180
181 #[test]
182 fn test_rectangular_matrix() {
183 let ortho = OrthogonalRegularization::new(0.01);
184
185 let weights = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
187 let penalty = ortho.compute_penalty_2d(&weights);
188
189 assert!(penalty >= 0.0);
191 }
192
193 #[test]
194 fn test_gradient_computation() {
195 let ortho = OrthogonalRegularization::new(0.1);
196
197 let weights = array![[1.0, 0.5], [0.5, 1.0]];
198 let gradient = ortho.compute_gradient_2d(&weights);
199
200 assert!(gradient.abs().sum() > 0.0);
202 }
203
204 #[test]
205 fn test_regularizer_trait() {
206 let ortho = OrthogonalRegularization::new(0.01);
207
208 let params = array![[1.0, 0.5], [0.5, 1.0]];
209 let mut gradient = array![[0.1, 0.2], [0.3, 0.4]];
210 let original_gradient = gradient.clone();
211
212 let penalty = ortho
213 .apply(¶ms, &mut gradient)
214 .expect("ortho.apply succeeds in test_regularizer_trait");
215
216 assert!(penalty > 0.0);
218
219 assert_ne!(gradient, original_gradient);
221
222 let penalty2 = ortho
224 .penalty(¶ms)
225 .expect("ortho.penalty succeeds in test_regularizer_trait");
226 assert_relative_eq!(penalty, penalty2, epsilon = 1e-10);
227 }
228
229 #[test]
230 fn test_non_2d_array() {
231 let ortho = OrthogonalRegularization::new(0.01);
232
233 let params = Array3::<f64>::zeros((2, 2, 2));
235 let mut gradient = Array3::<f64>::zeros((2, 2, 2));
236
237 let penalty = ortho
238 .apply(¶ms, &mut gradient)
239 .expect("ortho.apply succeeds in test_non_2d_array");
240 assert_eq!(penalty, 0.0);
241
242 assert_eq!(gradient, Array3::<f64>::zeros((2, 2, 2)));
244 }
245}