optirs_core/regularizers/
weight_standardization.rs1use scirs2_core::ndarray::{Array, Array2, Array4, ArrayBase, Data, Dimension, ScalarOperand};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use crate::error::{OptimError, Result};
12use crate::regularizers::Regularizer;
13
14#[derive(Debug, Clone)]
37pub struct WeightStandardization<A: Float> {
38 eps: A,
40}
41
42impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> WeightStandardization<A> {
43 pub fn new(eps: f64) -> Self {
49 Self {
50 eps: A::from_f64(eps).expect(
51 "WeightStandardization: eps must fit in A (f32/f64 never rejects a finite f64)",
52 ),
53 }
54 }
55
56 pub fn standardize(&self, weights: &Array2<A>) -> Result<Array2<A>> {
68 let n_cols = weights.ncols();
70 let n_cols_f: A = crate::regularizers::cast_scalar(n_cols)?;
71
72 let means = weights.sum_axis(scirs2_core::ndarray::Axis(1)) / n_cols_f;
74
75 let mut centered = weights.clone();
77 for i in 0..weights.nrows() {
78 for j in 0..weights.ncols() {
79 centered[[i, j]] = centered[[i, j]] - means[i];
80 }
81 }
82
83 let mut var = Array::zeros(weights.nrows());
85 for i in 0..weights.nrows() {
86 let mut sum_sq = A::zero();
87 for j in 0..weights.ncols() {
88 sum_sq = sum_sq + centered[[i, j]] * centered[[i, j]];
89 }
90 var[i] = sum_sq / n_cols_f;
91 }
92
93 let mut standardized = centered.clone();
95 for i in 0..weights.nrows() {
96 let denom = (var[i] + self.eps).sqrt();
97 for j in 0..weights.ncols() {
98 standardized[[i, j]] = centered[[i, j]] / denom;
99 }
100 }
101
102 Ok(standardized)
103 }
104
105 pub fn standardize_conv4d(&self, weights: &Array4<A>) -> Result<Array4<A>> {
115 let shape = weights.shape();
116 if shape.len() != 4 {
117 return Err(OptimError::InvalidConfig(
118 "Expected 4D weights for conv4d standardization".to_string(),
119 ));
120 }
121
122 let out_channels = shape[0];
123 let in_channels = shape[1];
124 let kernel_h = shape[2];
125 let kernel_w = shape[3];
126 let n_elements = in_channels * kernel_h * kernel_w;
127 let n_elements_f: A = crate::regularizers::cast_scalar(n_elements)?;
128
129 let mut means = Array::zeros(out_channels);
131
132 for c_out in 0..out_channels {
133 let mut sum = A::zero();
134 for c_in in 0..in_channels {
135 for h in 0..kernel_h {
136 for w in 0..kernel_w {
137 sum = sum + weights[[c_out, c_in, h, w]];
138 }
139 }
140 }
141 means[c_out] = sum / n_elements_f;
142 }
143
144 let mut centered = weights.clone();
146
147 for c_out in 0..out_channels {
148 for c_in in 0..in_channels {
149 for h in 0..kernel_h {
150 for w in 0..kernel_w {
151 centered[[c_out, c_in, h, w]] = weights[[c_out, c_in, h, w]] - means[c_out];
152 }
153 }
154 }
155 }
156
157 let mut vars = Array::zeros(out_channels);
159
160 for c_out in 0..out_channels {
161 let mut sum_sq = A::zero();
162 for c_in in 0..in_channels {
163 for h in 0..kernel_h {
164 for w in 0..kernel_w {
165 sum_sq =
166 sum_sq + centered[[c_out, c_in, h, w]] * centered[[c_out, c_in, h, w]];
167 }
168 }
169 }
170 vars[c_out] = sum_sq / n_elements_f;
171 }
172
173 let mut standardized = centered.clone();
175
176 for c_out in 0..out_channels {
177 let std_dev = (vars[c_out] + self.eps).sqrt();
178 for c_in in 0..in_channels {
179 for h in 0..kernel_h {
180 for w in 0..kernel_w {
181 standardized[[c_out, c_in, h, w]] = centered[[c_out, c_in, h, w]] / std_dev;
182 }
183 }
184 }
185 }
186
187 Ok(standardized)
188 }
189
190 fn compute_gradients<S1, S2>(
201 &self,
202 weights: &ArrayBase<S1, scirs2_core::ndarray::Ix2>,
203 grad_output: &ArrayBase<S2, scirs2_core::ndarray::Ix2>,
204 ) -> Result<Array2<A>>
205 where
206 S1: Data<Elem = A>,
207 S2: Data<Elem = A>,
208 {
209 let weights = weights.to_owned();
214 let grad_output = grad_output.to_owned();
215
216 let n_rows = weights.nrows();
217 let n_cols = weights.ncols();
218 let epsilon: A = crate::regularizers::cast_scalar(1e-6)?;
219
220 let mut gradients = Array2::zeros((n_rows, n_cols));
221 let standardized = self.standardize(&weights)?;
222
223 for i in 0..n_rows {
225 for j in 0..n_cols {
226 let mut weights_plus = weights.clone();
227 weights_plus[[i, j]] = weights_plus[[i, j]] + epsilon;
228
229 let standardized_plus = self.standardize(&weights_plus)?;
230
231 let diff = &standardized_plus - &standardized;
233
234 let mut grad_sum = A::zero();
236 for r in 0..n_rows {
237 for c in 0..n_cols {
238 grad_sum = grad_sum + diff[[r, c]] * grad_output[[r, c]];
239 }
240 }
241
242 gradients[[i, j]] = grad_sum / epsilon;
243 }
244 }
245
246 Ok(gradients)
247 }
248}
249
250impl<
251 A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync,
252 D: Dimension + Send + Sync,
253 > Regularizer<A, D> for WeightStandardization<A>
254{
255 fn apply(&self, params: &Array<A, D>, gradients: &mut Array<A, D>) -> Result<A> {
256 if params.ndim() != 2 {
258 return Ok(A::zero());
261 }
262
263 let params_2d = params
265 .view()
266 .into_dimensionality::<scirs2_core::ndarray::Ix2>()
267 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
268 let gradients_2d = gradients
269 .view()
270 .into_dimensionality::<scirs2_core::ndarray::Ix2>()
271 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
272
273 let corrections = self.compute_gradients(¶ms_2d, &gradients_2d)?;
275
276 let mut grad_mut = gradients
278 .view_mut()
279 .into_dimensionality::<scirs2_core::ndarray::Ix2>()
280 .map_err(|_| OptimError::InvalidConfig("Expected 2D array".to_string()))?;
281
282 grad_mut.zip_mut_with(&corrections, |g, &c| *g = *g + c);
284
285 Ok(A::zero())
287 }
288
289 fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
290 Ok(A::zero())
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use approx::assert_relative_eq;
299 use scirs2_core::ndarray::array;
300
301 #[test]
302 fn test_weight_standardization_creation() {
303 let ws = WeightStandardization::<f64>::new(1e-5);
304 assert_eq!(ws.eps, 1e-5);
305 }
306
307 #[test]
308 fn test_standardize_2d() {
309 let ws = WeightStandardization::new(1e-5);
310
311 let weights = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
313
314 let standardized = ws
315 .standardize(&weights)
316 .expect("ws.standardize succeeds in test_standardize_2d");
317
318 assert_eq!(standardized.shape(), weights.shape());
320
321 let mean1 = standardized.row(0).sum() / 3.0;
323 let mean2 = standardized.row(1).sum() / 3.0;
324
325 assert_relative_eq!(mean1, 0.0, epsilon = 1e-10);
326 assert_relative_eq!(mean2, 0.0, epsilon = 1e-10);
327
328 let var1 = standardized.row(0).mapv(|x| x * x).sum() / 3.0;
330 let var2 = standardized.row(1).mapv(|x| x * x).sum() / 3.0;
331
332 println!("var1 = {}, var2 = {}", var1, var2);
333
334 assert!((var1 - 1.0).abs() < 2e-4);
336 assert!((var2 - 1.0).abs() < 2e-4);
337 }
338
339 #[test]
340 fn test_standardize_conv4d() {
341 let ws = WeightStandardization::new(1e-5);
342
343 let weights = Array4::from_shape_fn((2, 2, 2, 2), |idx| {
345 let (a, b, c, d) = (idx.0, idx.1, idx.2, idx.3);
346 (a * 8 + b * 4 + c * 2 + d) as f64
347 });
348
349 let standardized = ws
350 .standardize_conv4d(&weights)
351 .expect("ws.standardize_conv4d succeeds in test_standardize_conv4d");
352
353 assert_eq!(standardized.shape(), weights.shape());
355
356 let mut sum1 = 0.0;
358 let mut sum2 = 0.0;
359
360 for c_in in 0..2 {
361 for h in 0..2 {
362 for w in 0..2 {
363 sum1 += standardized[[0, c_in, h, w]];
364 sum2 += standardized[[1, c_in, h, w]];
365 }
366 }
367 }
368
369 let mean1 = sum1 / 8.0;
370 let mean2 = sum2 / 8.0;
371
372 assert_relative_eq!(mean1, 0.0, epsilon = 1e-10);
373 assert_relative_eq!(mean2, 0.0, epsilon = 1e-10);
374
375 let mut sum_sq1 = 0.0;
377 let mut sum_sq2 = 0.0;
378
379 for c_in in 0..2 {
380 for h in 0..2 {
381 for w in 0..2 {
382 sum_sq1 += standardized[[0, c_in, h, w]] * standardized[[0, c_in, h, w]];
383 sum_sq2 += standardized[[1, c_in, h, w]] * standardized[[1, c_in, h, w]];
384 }
385 }
386 }
387
388 let var1 = sum_sq1 / 8.0;
389 let var2 = sum_sq2 / 8.0;
390
391 assert!((var1 - 1.0).abs() < 1e-5);
392 assert!((var2 - 1.0).abs() < 1e-5);
393 }
394
395 #[test]
396 fn test_regularizer_trait() {
397 let ws = WeightStandardization::new(1e-5);
398 let params = array![[1.0, 2.0], [3.0, 4.0]];
399 let mut gradients = array![[0.1, 0.2], [0.3, 0.4]];
400 let orig_gradients = gradients.clone();
401
402 let penalty = ws
403 .apply(¶ms, &mut gradients)
404 .expect("ws.apply succeeds in test_regularizer_trait");
405
406 assert_eq!(penalty, 0.0);
408
409 assert_ne!(gradients, orig_gradients);
411 }
412}