Skip to main content

hodu_nn/modules/
norm.rs

1use crate::compat::*;
2use crate::module::Module;
3use crate::state::{get_state, State};
4use hodu_core::{error::HoduResult, scalar::Scalar, tensor::Tensor, types::DType};
5
6#[derive(Module, Clone)]
7pub struct BatchNorm1D {
8    num_features: u32,
9    eps: Scalar,
10    momentum: Scalar,
11    affine: bool,
12    gamma: Option<Tensor>,
13    beta: Option<Tensor>,
14    running_mean: RefCell<Tensor>,
15    running_var: RefCell<Tensor>,
16}
17
18impl BatchNorm1D {
19    pub fn new(
20        num_features: u32,
21        eps: impl Into<Scalar>,
22        momentum: impl Into<Scalar>,
23        affine: bool,
24        dtype: DType,
25    ) -> HoduResult<Self> {
26        let eps = eps.into();
27        let momentum = momentum.into();
28
29        let (gamma, beta) = if affine {
30            let gamma = Tensor::ones([num_features], dtype)?;
31            gamma.set_requires_grad(true)?;
32
33            let beta = Tensor::zeros([num_features], dtype)?;
34            beta.set_requires_grad(true)?;
35
36            (Some(gamma), Some(beta))
37        } else {
38            (None, None)
39        };
40
41        let running_mean = RefCell::new(Tensor::zeros([num_features], dtype)?);
42        let running_var = RefCell::new(Tensor::ones([num_features], dtype)?);
43
44        Ok(Self {
45            num_features,
46            eps,
47            momentum,
48            affine,
49            gamma,
50            beta,
51            running_mean,
52            running_var,
53        })
54    }
55
56    pub fn forward(&self, input: &Tensor) -> HoduResult<Tensor> {
57        let input_shape = input.shape();
58
59        if get_state() == State::Training {
60            // Training mode: compute batch statistics
61            let mean = if input_shape.ndim() == 2 {
62                // [N, C] -> mean over N (axis 0)
63                input.mean(&[0], true)?
64            } else if input_shape.ndim() == 3 {
65                // [N, C, L] -> mean over N, L (axes 0, 2)
66                input.mean(&[0, 2], true)?
67            } else {
68                return Err(hodu_core::error::HoduError::InternalError(format!(
69                    "BatchNorm1d expects 2D [N, C] or 3D [N, C, L] input, got {}D",
70                    input_shape.ndim()
71                )));
72            };
73
74            let var = if input_shape.ndim() == 2 {
75                let centered = input.sub(&mean)?;
76                let squared = centered.mul(&centered)?;
77                squared.mean(&[0], true)?
78            } else {
79                let centered = input.sub(&mean)?;
80                let squared = centered.mul(&centered)?;
81                squared.mean(&[0, 2], true)?
82            };
83
84            // Update running statistics (during training)
85            // running_mean = momentum * running_mean + (1 - momentum) * mean
86            // running_var = momentum * running_var + (1 - momentum) * var
87            {
88                let mut rm = self.running_mean.borrow_mut();
89                let mut rv = self.running_var.borrow_mut();
90
91                let momentum_typed = self.momentum.to_dtype(rm.dtype());
92                let one_minus_momentum = Scalar::from_f32(1.0, rm.dtype()) - momentum_typed;
93
94                // Squeeze mean and var to match running stats shape [C]
95                let mean_squeezed = mean.reshape([self.num_features])?;
96                let var_squeezed = var.reshape([self.num_features])?;
97
98                let new_mean = rm
99                    .mul_scalar(momentum_typed)?
100                    .add(&mean_squeezed.mul_scalar(one_minus_momentum)?)?;
101                let new_var = rv
102                    .mul_scalar(momentum_typed)?
103                    .add(&var_squeezed.mul_scalar(one_minus_momentum)?)?;
104
105                *rm = new_mean;
106                *rv = new_var;
107            }
108
109            // Normalize
110            let normalized = self.normalize(input, &mean, &var)?;
111
112            Ok(normalized)
113        } else {
114            // Evaluation mode: use running statistics
115            let mean = self.running_mean.borrow();
116            let var = self.running_var.borrow();
117
118            // Reshape for broadcasting
119            let mean = if input_shape.ndim() == 2 {
120                mean.reshape([1, self.num_features])?
121            } else {
122                mean.reshape([1, self.num_features, 1])?
123            };
124
125            let var = if input_shape.ndim() == 2 {
126                var.reshape([1, self.num_features])?
127            } else {
128                var.reshape([1, self.num_features, 1])?
129            };
130
131            let normalized = self.normalize(input, &mean, &var)?;
132
133            Ok(normalized)
134        }
135    }
136
137    fn normalize(&self, input: &Tensor, mean: &Tensor, var: &Tensor) -> HoduResult<Tensor> {
138        let input_shape = input.shape();
139
140        // x_normalized = (x - mean) / sqrt(var + eps)
141        let centered = input.sub(mean)?;
142        let eps_typed = self.eps.to_dtype(input.dtype());
143        let std = var.add_scalar(eps_typed)?.sqrt()?;
144        let normalized = centered.div(&std)?;
145
146        // Apply affine transformation if enabled
147        if self.affine {
148            let gamma = self.gamma.as_ref().unwrap();
149            let beta = self.beta.as_ref().unwrap();
150
151            let gamma_reshaped = if input_shape.ndim() == 2 {
152                gamma.reshape([1, self.num_features])?
153            } else {
154                gamma.reshape([1, self.num_features, 1])?
155            };
156
157            let beta_reshaped = if input_shape.ndim() == 2 {
158                beta.reshape([1, self.num_features])?
159            } else {
160                beta.reshape([1, self.num_features, 1])?
161            };
162
163            let scaled = normalized.mul(&gamma_reshaped)?;
164            scaled.add(&beta_reshaped)
165        } else {
166            Ok(normalized)
167        }
168    }
169
170    pub fn parameters(&mut self) -> Vec<&mut Tensor> {
171        let mut params = Vec::new();
172        if self.affine {
173            if let Some(ref mut gamma) = self.gamma {
174                params.push(gamma);
175            }
176            if let Some(ref mut beta) = self.beta {
177                params.push(beta);
178            }
179        }
180        params
181    }
182}
183
184#[derive(Module, Clone)]
185pub struct BatchNorm2D {
186    num_features: u32,
187    eps: Scalar,
188    momentum: Scalar,
189    affine: bool,
190    gamma: Option<Tensor>,
191    beta: Option<Tensor>,
192    running_mean: RefCell<Tensor>,
193    running_var: RefCell<Tensor>,
194}
195
196impl BatchNorm2D {
197    pub fn new(
198        num_features: u32,
199        eps: impl Into<Scalar>,
200        momentum: impl Into<Scalar>,
201        affine: bool,
202        dtype: DType,
203    ) -> HoduResult<Self> {
204        let eps = eps.into();
205        let momentum = momentum.into();
206
207        let (gamma, beta) = if affine {
208            let gamma = Tensor::ones([num_features], dtype)?;
209            gamma.set_requires_grad(true)?;
210
211            let beta = Tensor::zeros([num_features], dtype)?;
212            beta.set_requires_grad(true)?;
213
214            (Some(gamma), Some(beta))
215        } else {
216            (None, None)
217        };
218
219        let running_mean = RefCell::new(Tensor::zeros([num_features], dtype)?);
220        let running_var = RefCell::new(Tensor::ones([num_features], dtype)?);
221
222        Ok(Self {
223            num_features,
224            eps,
225            momentum,
226            affine,
227            gamma,
228            beta,
229            running_mean,
230            running_var,
231        })
232    }
233
234    pub fn forward(&self, input: &Tensor) -> HoduResult<Tensor> {
235        let input_shape = input.shape();
236
237        if input_shape.ndim() != 4 {
238            return Err(hodu_core::error::HoduError::InternalError(format!(
239                "BatchNorm2d expects 4D input [N, C, H, W], got {}D",
240                input_shape.ndim()
241            )));
242        }
243
244        if get_state() == State::Training {
245            // Training mode: compute batch statistics
246            // Mean over [N, H, W] (axes 0, 2, 3)
247            let mean = input.mean(&[0, 2, 3], true)?;
248            let centered = input.sub(&mean)?;
249            let squared = centered.mul(&centered)?;
250            let var = squared.mean(&[0, 2, 3], true)?;
251
252            // Update running statistics (during training)
253            {
254                let mut rm = self.running_mean.borrow_mut();
255                let mut rv = self.running_var.borrow_mut();
256
257                let momentum_typed = self.momentum.to_dtype(rm.dtype());
258                let one_minus_momentum = Scalar::from_f32(1.0, rm.dtype()) - momentum_typed;
259
260                // Squeeze mean and var to match running stats shape [C]
261                let mean_squeezed = mean.reshape([self.num_features])?;
262                let var_squeezed = var.reshape([self.num_features])?;
263
264                let new_mean = rm
265                    .mul_scalar(momentum_typed)?
266                    .add(&mean_squeezed.mul_scalar(one_minus_momentum)?)?;
267                let new_var = rv
268                    .mul_scalar(momentum_typed)?
269                    .add(&var_squeezed.mul_scalar(one_minus_momentum)?)?;
270
271                *rm = new_mean;
272                *rv = new_var;
273            }
274
275            let normalized = self.normalize(input, &mean, &var)?;
276            Ok(normalized)
277        } else {
278            // Evaluation mode: use running statistics
279            let mean = self.running_mean.borrow();
280            let var = self.running_var.borrow();
281
282            let mean = mean.reshape([1, self.num_features, 1, 1])?;
283            let var = var.reshape([1, self.num_features, 1, 1])?;
284
285            let normalized = self.normalize(input, &mean, &var)?;
286            Ok(normalized)
287        }
288    }
289
290    fn normalize(&self, input: &Tensor, mean: &Tensor, var: &Tensor) -> HoduResult<Tensor> {
291        // x_normalized = (x - mean) / sqrt(var + eps)
292        let centered = input.sub(mean)?;
293        let eps_typed = self.eps.to_dtype(input.dtype());
294        let std = var.add_scalar(eps_typed)?.sqrt()?;
295        let normalized = centered.div(&std)?;
296
297        // Apply affine transformation if enabled
298        if self.affine {
299            let gamma = self.gamma.as_ref().unwrap();
300            let beta = self.beta.as_ref().unwrap();
301
302            let gamma_reshaped = gamma.reshape([1, self.num_features, 1, 1])?;
303            let beta_reshaped = beta.reshape([1, self.num_features, 1, 1])?;
304
305            let scaled = normalized.mul(&gamma_reshaped)?;
306            scaled.add(&beta_reshaped)
307        } else {
308            Ok(normalized)
309        }
310    }
311
312    pub fn parameters(&mut self) -> Vec<&mut Tensor> {
313        let mut params = Vec::new();
314        if self.affine {
315            if let Some(ref mut gamma) = self.gamma {
316                params.push(gamma);
317            }
318            if let Some(ref mut beta) = self.beta {
319                params.push(beta);
320            }
321        }
322        params
323    }
324}
325
326#[derive(Module, Clone)]
327pub struct BatchNorm3D {
328    num_features: u32,
329    eps: Scalar,
330    momentum: Scalar,
331    affine: bool,
332    gamma: Option<Tensor>,
333    beta: Option<Tensor>,
334    running_mean: RefCell<Tensor>,
335    running_var: RefCell<Tensor>,
336}
337
338impl BatchNorm3D {
339    pub fn new(
340        num_features: u32,
341        eps: impl Into<Scalar>,
342        momentum: impl Into<Scalar>,
343        affine: bool,
344        dtype: DType,
345    ) -> HoduResult<Self> {
346        let eps = eps.into();
347        let momentum = momentum.into();
348
349        let (gamma, beta) = if affine {
350            let gamma = Tensor::ones([num_features], dtype)?;
351            gamma.set_requires_grad(true)?;
352
353            let beta = Tensor::zeros([num_features], dtype)?;
354            beta.set_requires_grad(true)?;
355
356            (Some(gamma), Some(beta))
357        } else {
358            (None, None)
359        };
360
361        let running_mean = RefCell::new(Tensor::zeros([num_features], dtype)?);
362        let running_var = RefCell::new(Tensor::ones([num_features], dtype)?);
363
364        Ok(Self {
365            num_features,
366            eps,
367            momentum,
368            affine,
369            gamma,
370            beta,
371            running_mean,
372            running_var,
373        })
374    }
375
376    pub fn forward(&self, input: &Tensor) -> HoduResult<Tensor> {
377        let input_shape = input.shape();
378
379        if input_shape.ndim() != 5 {
380            return Err(hodu_core::error::HoduError::InternalError(format!(
381                "BatchNorm3D expects 5D input [N, C, D, H, W], got {}D",
382                input_shape.ndim()
383            )));
384        }
385
386        if get_state() == State::Training {
387            // Training mode: compute batch statistics
388            // Mean over [N, D, H, W] (axes 0, 2, 3, 4)
389            let mean = input.mean(&[0, 2, 3, 4], true)?;
390            let centered = input.sub(&mean)?;
391            let squared = centered.mul(&centered)?;
392            let var = squared.mean(&[0, 2, 3, 4], true)?;
393
394            // Update running statistics (during training)
395            {
396                let mut rm = self.running_mean.borrow_mut();
397                let mut rv = self.running_var.borrow_mut();
398
399                let momentum_typed = self.momentum.to_dtype(rm.dtype());
400                let one_minus_momentum = Scalar::from_f32(1.0, rm.dtype()) - momentum_typed;
401
402                // Squeeze mean and var to match running stats shape [C]
403                let mean_squeezed = mean.reshape([self.num_features])?;
404                let var_squeezed = var.reshape([self.num_features])?;
405
406                let new_mean = rm
407                    .mul_scalar(momentum_typed)?
408                    .add(&mean_squeezed.mul_scalar(one_minus_momentum)?)?;
409                let new_var = rv
410                    .mul_scalar(momentum_typed)?
411                    .add(&var_squeezed.mul_scalar(one_minus_momentum)?)?;
412
413                *rm = new_mean;
414                *rv = new_var;
415            }
416
417            let normalized = self.normalize(input, &mean, &var)?;
418            Ok(normalized)
419        } else {
420            // Evaluation mode: use running statistics
421            let mean = self.running_mean.borrow();
422            let var = self.running_var.borrow();
423
424            let mean = mean.reshape([1, self.num_features, 1, 1, 1])?;
425            let var = var.reshape([1, self.num_features, 1, 1, 1])?;
426
427            let normalized = self.normalize(input, &mean, &var)?;
428            Ok(normalized)
429        }
430    }
431
432    fn normalize(&self, input: &Tensor, mean: &Tensor, var: &Tensor) -> HoduResult<Tensor> {
433        // x_normalized = (x - mean) / sqrt(var + eps)
434        let centered = input.sub(mean)?;
435        let eps_typed = self.eps.to_dtype(input.dtype());
436        let std = var.add_scalar(eps_typed)?.sqrt()?;
437        let normalized = centered.div(&std)?;
438
439        // Apply affine transformation if enabled
440        if self.affine {
441            let gamma = self.gamma.as_ref().unwrap();
442            let beta = self.beta.as_ref().unwrap();
443
444            let gamma_reshaped = gamma.reshape([1, self.num_features, 1, 1, 1])?;
445            let beta_reshaped = beta.reshape([1, self.num_features, 1, 1, 1])?;
446
447            let scaled = normalized.mul(&gamma_reshaped)?;
448            scaled.add(&beta_reshaped)
449        } else {
450            Ok(normalized)
451        }
452    }
453
454    pub fn parameters(&mut self) -> Vec<&mut Tensor> {
455        let mut params = Vec::new();
456        if self.affine {
457            if let Some(ref mut gamma) = self.gamma {
458                params.push(gamma);
459            }
460            if let Some(ref mut beta) = self.beta {
461                params.push(beta);
462            }
463        }
464        params
465    }
466}
467
468#[derive(Module, Clone)]
469pub struct LayerNorm {
470    normalized_shape: Vec<u32>,
471    eps: Scalar,
472    elementwise_affine: bool,
473    gamma: Option<Tensor>,
474    beta: Option<Tensor>,
475}
476
477impl LayerNorm {
478    pub fn new(
479        normalized_shape: Vec<u32>,
480        eps: impl Into<Scalar>,
481        elementwise_affine: bool,
482        dtype: DType,
483    ) -> HoduResult<Self> {
484        let eps = eps.into();
485
486        let (gamma, beta) = if elementwise_affine {
487            let gamma = Tensor::ones(&normalized_shape, dtype)?;
488            gamma.set_requires_grad(true)?;
489
490            let beta = Tensor::zeros(&normalized_shape, dtype)?;
491            beta.set_requires_grad(true)?;
492
493            (Some(gamma), Some(beta))
494        } else {
495            (None, None)
496        };
497
498        Ok(Self {
499            normalized_shape,
500            eps,
501            elementwise_affine,
502            gamma,
503            beta,
504        })
505    }
506
507    pub fn forward(&self, input: &Tensor) -> HoduResult<Tensor> {
508        let input_shape = input.shape();
509        let input_rank = input_shape.ndim();
510        let norm_rank = self.normalized_shape.len() as u32;
511
512        // Verify that the last dimensions match normalized_shape
513        if input_rank < norm_rank {
514            return Err(hodu_core::error::HoduError::InternalError(format!(
515                "Input rank {} is less than normalized_shape rank {}",
516                input_rank, norm_rank
517            )));
518        }
519
520        for i in 0..norm_rank {
521            if input_shape[input_rank - norm_rank + i] != self.normalized_shape[i as usize] {
522                return Err(hodu_core::error::HoduError::InternalError(format!(
523                    "Input shape mismatch at dimension {}",
524                    i
525                )));
526            }
527        }
528
529        // Compute axes to normalize over (last norm_rank dimensions)
530        let axes: Vec<u32> = (0..norm_rank).map(|i| input_rank - norm_rank + i).collect();
531
532        // Compute mean and variance over the normalized dimensions
533        let mean = input.mean(&axes, true)?;
534        let centered = input.sub(&mean)?;
535        let squared = centered.mul(&centered)?;
536        let var = squared.mean(&axes, true)?;
537
538        // Normalize: (x - mean) / sqrt(var + eps)
539        let eps_typed = self.eps.to_dtype(input.dtype());
540        let std = var.add_scalar(eps_typed)?.sqrt()?;
541        let normalized = centered.div(&std)?;
542
543        // Apply elementwise affine transformation if enabled
544        if self.elementwise_affine {
545            let gamma = self.gamma.as_ref().unwrap();
546            let beta = self.beta.as_ref().unwrap();
547
548            let scaled = normalized.mul(gamma)?;
549            scaled.add(beta)
550        } else {
551            Ok(normalized)
552        }
553    }
554
555    pub fn parameters(&mut self) -> Vec<&mut Tensor> {
556        let mut params = Vec::new();
557        if self.elementwise_affine {
558            if let Some(ref mut gamma) = self.gamma {
559                params.push(gamma);
560            }
561            if let Some(ref mut beta) = self.beta {
562                params.push(beta);
563            }
564        }
565        params
566    }
567}