1use ::ndarray::{Array, Array1, Array4, ArrayD, Axis, Ix1, Ix2, Ix4, IxDyn, Zip};
13
14use rand::{Rng, RngExt, SeedableRng};
15
16use crate::array_protocol::ml_ops::ActivationFunc;
17use crate::array_protocol::operations::OperationError;
18use crate::array_protocol::{ArrayProtocol, NdarrayWrapper};
19
20pub struct LayerGrad {
22 pub grad_input: Box<dyn ArrayProtocol>,
25
26 pub grad_params: Vec<Box<dyn ArrayProtocol>>,
30}
31
32pub trait Layer: Send + Sync {
34 fn layer_type(&self) -> &str;
37
38 fn forward(&self, inputs: &dyn ArrayProtocol)
39 -> Result<Box<dyn ArrayProtocol>, OperationError>;
40
41 fn backward(
57 &self,
58 _input: &dyn ArrayProtocol,
59 _grad_output: &dyn ArrayProtocol,
60 ) -> Result<LayerGrad, OperationError> {
61 Err(OperationError::NotImplemented(format!(
62 "backward() is not implemented for layer type '{layertype}'",
63 layertype = self.layer_type()
64 )))
65 }
66
67 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>>;
69
70 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>>;
72
73 fn update_parameter(
75 &mut self,
76 name: &str,
77 value: Box<dyn ArrayProtocol>,
78 ) -> Result<(), OperationError>;
79
80 fn parameter_names(&self) -> Vec<String>;
82
83 fn train(&mut self);
85
86 fn eval(&mut self);
88
89 fn is_training(&self) -> bool;
91
92 fn name(&self) -> &str;
94}
95
96fn as_f64_array(a: &dyn ArrayProtocol) -> Result<ArrayD<f64>, OperationError> {
101 if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>() {
102 return Ok(w.as_array().clone());
103 }
104 if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix1>>() {
105 return Ok(w.as_array().clone().into_dyn());
106 }
107 if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix2>>() {
108 return Ok(w.as_array().clone().into_dyn());
109 }
110 if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>() {
111 return Ok(w.as_array().clone().into_dyn());
112 }
113 Err(OperationError::TypeMismatch(
114 "Layer::backward currently only supports f64 NdarrayWrapper arrays (Ix1/Ix2/Ix4/IxDyn)"
115 .to_string(),
116 ))
117}
118
119fn wrap_like(
126 reference: &dyn ArrayProtocol,
127 data: ArrayD<f64>,
128) -> Result<Box<dyn ArrayProtocol>, OperationError> {
129 if reference
130 .as_any()
131 .downcast_ref::<NdarrayWrapper<f64, Ix1>>()
132 .is_some()
133 {
134 let arr = data
135 .into_dimensionality::<Ix1>()
136 .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix1): {e}")))?;
137 return Ok(Box::new(NdarrayWrapper::new(arr)));
138 }
139 if reference
140 .as_any()
141 .downcast_ref::<NdarrayWrapper<f64, Ix2>>()
142 .is_some()
143 {
144 let arr = data
145 .into_dimensionality::<Ix2>()
146 .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix2): {e}")))?;
147 return Ok(Box::new(NdarrayWrapper::new(arr)));
148 }
149 if reference
150 .as_any()
151 .downcast_ref::<NdarrayWrapper<f64, Ix4>>()
152 .is_some()
153 {
154 let arr = data
155 .into_dimensionality::<Ix4>()
156 .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix4): {e}")))?;
157 return Ok(Box::new(NdarrayWrapper::new(arr)));
158 }
159 Ok(Box::new(NdarrayWrapper::new(data)))
160}
161
162fn activation_grad(
166 z: &ArrayD<f64>,
167 act: ActivationFunc,
168 grad_y: &ArrayD<f64>,
169) -> Result<ArrayD<f64>, OperationError> {
170 match act {
171 ActivationFunc::ReLU => {
172 Ok(Zip::from(z)
173 .and(grad_y)
174 .map_collect(|&zv, &gy| if zv > 0.0 { gy } else { 0.0 }))
175 }
176 ActivationFunc::Sigmoid => Ok(Zip::from(z).and(grad_y).map_collect(|&zv, &gy| {
177 let s = 1.0 / (1.0 + (-zv).exp());
178 gy * s * (1.0 - s)
179 })),
180 ActivationFunc::Tanh => Ok(Zip::from(z).and(grad_y).map_collect(|&zv, &gy| {
181 let t = zv.tanh();
182 gy * (1.0 - t * t)
183 })),
184 ActivationFunc::LeakyReLU(alpha) => Ok(Zip::from(z)
185 .and(grad_y)
186 .map_collect(|&zv, &gy| if zv > 0.0 { gy } else { gy * alpha })),
187 ActivationFunc::Softmax => Err(OperationError::NotImplemented(
188 "Softmax backward is not implemented: ml_ops::apply_activation's multi-dimensional \
189 Softmax normalizes along the array's last axis regardless of what that axis \
190 semantically represents for a given layer, so a generic gradient here would not \
191 reliably correspond to forward's behavior. Use an element-wise activation (ReLU, \
192 Sigmoid, Tanh, or LeakyReLU) on layers that need to be part of a differentiated model."
193 .to_string(),
194 )),
195 }
196}
197
198pub struct Linear {
200 name: String,
202
203 weights: Box<dyn ArrayProtocol>,
205
206 bias: Option<Box<dyn ArrayProtocol>>,
208
209 activation: Option<ActivationFunc>,
211
212 training: bool,
214}
215
216impl Linear {
217 pub fn new(
219 name: &str,
220 weights: Box<dyn ArrayProtocol>,
221 bias: Option<Box<dyn ArrayProtocol>>,
222 activation: Option<ActivationFunc>,
223 ) -> Self {
224 Self {
225 name: name.to_string(),
226 weights,
227 bias,
228 activation,
229 training: true,
230 }
231 }
232
233 pub fn new_random(
235 name: &str,
236 in_features: usize,
237 out_features: usize,
238 withbias: bool,
239 activation: Option<ActivationFunc>,
240 ) -> Self {
241 let scale = (6.0 / (in_features + out_features) as f64).sqrt();
243 let mut rng = rand::rng();
244 let weights = Array::from_shape_fn((out_features, in_features), |_| {
245 (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
246 });
247
248 let bias = if withbias {
250 let bias_array: Array<f64, Ix1> = Array::zeros(out_features);
251 Some(Box::new(NdarrayWrapper::new(bias_array)) as Box<dyn ArrayProtocol>)
252 } else {
253 None
254 };
255
256 Self {
257 name: name.to_string(),
258 weights: Box::new(NdarrayWrapper::new(weights)),
259 bias,
260 activation,
261 training: true,
262 }
263 }
264}
265
266impl Layer for Linear {
267 fn layer_type(&self) -> &str {
268 "Linear"
269 }
270
271 fn forward(
272 &self,
273 inputs: &dyn ArrayProtocol,
274 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
275 let mut result = crate::array_protocol::matmul(self.weights.as_ref(), inputs)?;
277
278 if let Some(bias) = &self.bias {
280 let intermediate = crate::array_protocol::add(result.as_ref(), bias.as_ref())?;
282 result = intermediate;
283 }
284
285 if let Some(act_fn) = self.activation {
287 let intermediate = crate::array_protocol::ml_ops::activation(result.as_ref(), act_fn)?;
289 result = intermediate;
290 }
291
292 Ok(result)
293 }
294
295 fn backward(
296 &self,
297 input: &dyn ArrayProtocol,
298 grad_output: &dyn ArrayProtocol,
299 ) -> Result<LayerGrad, OperationError> {
300 let x = as_f64_array(input)?
301 .into_dimensionality::<Ix2>()
302 .map_err(|e| {
303 OperationError::ShapeMismatch(format!(
304 "Linear::backward expects a 2D input (in_features, batch): {e}"
305 ))
306 })?;
307 let w = as_f64_array(self.weights.as_ref())?
308 .into_dimensionality::<Ix2>()
309 .map_err(|e| {
310 OperationError::ShapeMismatch(format!("Linear::backward expects 2D weights: {e}"))
311 })?;
312
313 let mut z = w.dot(&x);
316 if let Some(bias) = &self.bias {
317 let b = as_f64_array(bias.as_ref())?
318 .into_dimensionality::<Ix1>()
319 .map_err(|e| {
320 OperationError::ShapeMismatch(format!(
321 "Linear::backward expects a 1D bias: {e}"
322 ))
323 })?;
324 for mut col in z.axis_iter_mut(Axis(1)) {
325 col += &b;
326 }
327 }
328
329 let grad_y = as_f64_array(grad_output)?
330 .into_dimensionality::<Ix2>()
331 .map_err(|e| {
332 OperationError::ShapeMismatch(format!(
333 "Linear::backward expects a 2D grad_output: {e}"
334 ))
335 })?;
336
337 let grad_z = match self.activation {
338 Some(act) => activation_grad(&z.into_dyn(), act, &grad_y.into_dyn())?
339 .into_dimensionality::<Ix2>()
340 .map_err(|e| {
341 OperationError::Other(format!(
342 "internal shape error recovering activation_grad's result: {e}"
343 ))
344 })?,
345 None => grad_y,
346 };
347
348 let grad_w = grad_z.dot(&x.t());
350 let grad_input = w.t().dot(&grad_z);
351
352 let mut grad_params = vec![wrap_like(self.weights.as_ref(), grad_w.into_dyn())?];
353 if let Some(bias) = &self.bias {
354 let grad_b = grad_z.sum_axis(Axis(1));
356 grad_params.push(wrap_like(bias.as_ref(), grad_b.into_dyn())?);
357 }
358
359 Ok(LayerGrad {
360 grad_input: wrap_like(input, grad_input.into_dyn())?,
361 grad_params,
362 })
363 }
364
365 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
366 let mut params = vec![self.weights.clone()];
367 if let Some(bias) = &self.bias {
368 params.push(bias.clone());
369 }
370 params
371 }
372
373 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
374 let mut params = vec![&mut self.weights];
375 if let Some(bias) = &mut self.bias {
376 params.push(bias);
377 }
378 params
379 }
380
381 fn update_parameter(
382 &mut self,
383 name: &str,
384 value: Box<dyn ArrayProtocol>,
385 ) -> Result<(), OperationError> {
386 match name {
387 "weights" => {
388 self.weights = value;
389 Ok(())
390 }
391 "bias" => {
392 self.bias = Some(value);
393 Ok(())
394 }
395 _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
396 }
397 }
398
399 fn parameter_names(&self) -> Vec<String> {
400 let mut names = vec!["weights".to_string()];
401 if self.bias.is_some() {
402 names.push("bias".to_string());
403 }
404 names
405 }
406
407 fn train(&mut self) {
408 self.training = true;
409 }
410
411 fn eval(&mut self) {
412 self.training = false;
413 }
414
415 fn is_training(&self) -> bool {
416 self.training
417 }
418
419 fn name(&self) -> &str {
420 &self.name
421 }
422}
423
424pub struct Conv2D {
426 name: String,
428
429 filters: Box<dyn ArrayProtocol>,
431
432 bias: Option<Box<dyn ArrayProtocol>>,
434
435 stride: (usize, usize),
437
438 padding: (usize, usize),
440
441 activation: Option<ActivationFunc>,
443
444 training: bool,
446}
447
448impl Conv2D {
449 pub fn new(
451 name: &str,
452 filters: Box<dyn ArrayProtocol>,
453 bias: Option<Box<dyn ArrayProtocol>>,
454 stride: (usize, usize),
455 padding: (usize, usize),
456 activation: Option<ActivationFunc>,
457 ) -> Self {
458 Self {
459 name: name.to_string(),
460 filters,
461 bias,
462 stride,
463 padding,
464 activation,
465 training: true,
466 }
467 }
468
469 #[allow(clippy::too_many_arguments)]
471 pub fn withshape(
472 name: &str,
473 filter_height: usize,
474 filter_width: usize,
475 in_channels: usize,
476 out_channels: usize,
477 stride: (usize, usize),
478 padding: (usize, usize),
479 withbias: bool,
480 activation: Option<ActivationFunc>,
481 ) -> Self {
482 let fan_in = filter_height * filter_width * in_channels;
484 let scale = (2.0 / fan_in as f64).sqrt();
485 let mut rng = rand::rng();
486 let filters = Array::from_shape_fn(
487 (filter_height, filter_width, in_channels, out_channels),
488 |_| (rng.random::<f64>() * 2.0_f64 - 1.0) * scale,
489 );
490
491 let bias = if withbias {
493 let bias_array: Array<f64, Ix1> = Array::zeros(out_channels);
494 Some(Box::new(NdarrayWrapper::new(bias_array)) as Box<dyn ArrayProtocol>)
495 } else {
496 None
497 };
498
499 Self {
500 name: name.to_string(),
501 filters: Box::new(NdarrayWrapper::new(filters)),
502 bias,
503 stride,
504 padding,
505 activation,
506 training: true,
507 }
508 }
509}
510
511impl Layer for Conv2D {
512 fn layer_type(&self) -> &str {
513 "Conv2D"
514 }
515
516 fn forward(
517 &self,
518 inputs: &dyn ArrayProtocol,
519 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
520 let mut result = crate::array_protocol::ml_ops::conv2d(
522 inputs,
523 self.filters.as_ref(),
524 self.stride,
525 self.padding,
526 )?;
527
528 if let Some(bias) = &self.bias {
530 result = crate::array_protocol::add(result.as_ref(), bias.as_ref())?;
531 }
532
533 if let Some(act_fn) = self.activation {
535 result = crate::array_protocol::ml_ops::activation(result.as_ref(), act_fn)?;
536 }
537
538 Ok(result)
539 }
540
541 #[allow(clippy::needless_range_loop)]
542 fn backward(
543 &self,
544 input: &dyn ArrayProtocol,
545 grad_output: &dyn ArrayProtocol,
546 ) -> Result<LayerGrad, OperationError> {
547 let inputarr = as_f64_array(input)?
548 .into_dimensionality::<Ix4>()
549 .map_err(|e| {
550 OperationError::ShapeMismatch(format!("Conv2D::backward expects a 4D input: {e}"))
551 })?;
552 let filters = as_f64_array(self.filters.as_ref())?
553 .into_dimensionality::<Ix4>()
554 .map_err(|e| {
555 OperationError::ShapeMismatch(format!("Conv2D::backward expects 4D filters: {e}"))
556 })?;
557
558 let (stride_h, stride_w) = self.stride;
559 let (pad_h, pad_w) = self.padding;
560 let batch_size = inputarr.shape()[0];
561 let input_height = inputarr.shape()[1];
562 let input_width = inputarr.shape()[2];
563 let input_channels = inputarr.shape()[3];
564 let filter_height = filters.shape()[0];
565 let filter_width = filters.shape()[1];
566 let filter_out_channels = filters.shape()[3];
567 let out_height = (input_height - filter_height + 2 * pad_h) / stride_h + 1;
568 let out_width = (input_width - filter_width + 2 * pad_w) / stride_w + 1;
569
570 let mut conv_out =
573 Array4::<f64>::zeros((batch_size, out_height, out_width, filter_out_channels));
574 for b in 0..batch_size {
575 for oc in 0..filter_out_channels {
576 for oh in 0..out_height {
577 for ow in 0..out_width {
578 let mut sum = 0.0;
579 for fh in 0..filter_height {
580 for fw in 0..filter_width {
581 let in_h = (oh * stride_h) as i32 + fh as i32 - pad_h as i32;
582 let in_w = (ow * stride_w) as i32 + fw as i32 - pad_w as i32;
583 if in_h >= 0
584 && in_h < input_height as i32
585 && in_w >= 0
586 && in_w < input_width as i32
587 {
588 for ic in 0..input_channels {
589 sum += inputarr[[b, in_h as usize, in_w as usize, ic]]
590 * filters[[fh, fw, ic, oc]];
591 }
592 }
593 }
594 }
595 conv_out[[b, oh, ow, oc]] = sum;
596 }
597 }
598 }
599 }
600 if let Some(bias) = &self.bias {
601 let bias_arr = as_f64_array(bias.as_ref())?;
602 for b in 0..batch_size {
603 for oh in 0..out_height {
604 for ow in 0..out_width {
605 for oc in 0..filter_out_channels {
606 conv_out[[b, oh, ow, oc]] += bias_arr[[oc]];
607 }
608 }
609 }
610 }
611 }
612
613 let grad_y = as_f64_array(grad_output)?
614 .into_dimensionality::<Ix4>()
615 .map_err(|e| {
616 OperationError::ShapeMismatch(format!(
617 "Conv2D::backward expects a 4D grad_output: {e}"
618 ))
619 })?;
620
621 let grad_preact = match self.activation {
622 Some(act) => activation_grad(&conv_out.into_dyn(), act, &grad_y.into_dyn())?
623 .into_dimensionality::<Ix4>()
624 .map_err(|e| {
625 OperationError::Other(format!(
626 "internal shape error recovering activation_grad's result: {e}"
627 ))
628 })?,
629 None => grad_y,
630 };
631
632 let mut grad_input =
636 Array4::<f64>::zeros((batch_size, input_height, input_width, input_channels));
637 let mut grad_filters = Array4::<f64>::zeros((
638 filter_height,
639 filter_width,
640 input_channels,
641 filter_out_channels,
642 ));
643
644 for b in 0..batch_size {
645 for oc in 0..filter_out_channels {
646 for oh in 0..out_height {
647 for ow in 0..out_width {
648 let g = grad_preact[[b, oh, ow, oc]];
649 for fh in 0..filter_height {
650 for fw in 0..filter_width {
651 let in_h = (oh * stride_h) as i32 + fh as i32 - pad_h as i32;
652 let in_w = (ow * stride_w) as i32 + fw as i32 - pad_w as i32;
653 if in_h >= 0
654 && in_h < input_height as i32
655 && in_w >= 0
656 && in_w < input_width as i32
657 {
658 let (in_h, in_w) = (in_h as usize, in_w as usize);
659 for ic in 0..input_channels {
660 grad_filters[[fh, fw, ic, oc]] +=
661 g * inputarr[[b, in_h, in_w, ic]];
662 grad_input[[b, in_h, in_w, ic]] +=
663 g * filters[[fh, fw, ic, oc]];
664 }
665 }
666 }
667 }
668 }
669 }
670 }
671 }
672
673 let mut grad_params = vec![wrap_like(self.filters.as_ref(), grad_filters.into_dyn())?];
674 if let Some(bias) = &self.bias {
675 let mut grad_bias = Array1::<f64>::zeros(filter_out_channels);
676 for b in 0..batch_size {
677 for oh in 0..out_height {
678 for ow in 0..out_width {
679 for oc in 0..filter_out_channels {
680 grad_bias[oc] += grad_preact[[b, oh, ow, oc]];
681 }
682 }
683 }
684 }
685 grad_params.push(wrap_like(bias.as_ref(), grad_bias.into_dyn())?);
686 }
687
688 Ok(LayerGrad {
689 grad_input: wrap_like(input, grad_input.into_dyn())?,
690 grad_params,
691 })
692 }
693
694 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
695 let mut params = vec![self.filters.clone()];
696 if let Some(bias) = &self.bias {
697 params.push(bias.clone());
698 }
699 params
700 }
701
702 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
703 let mut params = vec![&mut self.filters];
704 if let Some(bias) = &mut self.bias {
705 params.push(bias);
706 }
707 params
708 }
709
710 fn update_parameter(
711 &mut self,
712 name: &str,
713 value: Box<dyn ArrayProtocol>,
714 ) -> Result<(), OperationError> {
715 match name {
716 "filters" => {
717 self.filters = value;
718 Ok(())
719 }
720 "bias" => {
721 self.bias = Some(value);
722 Ok(())
723 }
724 _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
725 }
726 }
727
728 fn parameter_names(&self) -> Vec<String> {
729 let mut names = vec!["filters".to_string()];
730 if self.bias.is_some() {
731 names.push("bias".to_string());
732 }
733 names
734 }
735
736 fn train(&mut self) {
737 self.training = true;
738 }
739
740 fn eval(&mut self) {
741 self.training = false;
742 }
743
744 fn is_training(&self) -> bool {
745 self.training
746 }
747
748 fn name(&self) -> &str {
749 &self.name
750 }
751}
752
753pub struct Conv2DBuilder {
755 name: String,
756 filter_height: usize,
757 filter_width: usize,
758 in_channels: usize,
759 out_channels: usize,
760 stride: (usize, usize),
761 padding: (usize, usize),
762 withbias: bool,
763 activation: Option<ActivationFunc>,
764}
765
766impl Conv2DBuilder {
767 pub fn new(name: &str) -> Self {
769 Self {
770 name: name.to_string(),
771 filter_height: 3,
772 filter_width: 3,
773 in_channels: 1,
774 out_channels: 1,
775 stride: (1, 1),
776 padding: (0, 0),
777 withbias: true,
778 activation: None,
779 }
780 }
781
782 pub const fn filter_size(mut self, height: usize, width: usize) -> Self {
784 self.filter_height = height;
785 self.filter_width = width;
786 self
787 }
788
789 pub const fn channels(mut self, input: usize, output: usize) -> Self {
791 self.in_channels = input;
792 self.out_channels = output;
793 self
794 }
795
796 pub fn stride(mut self, stride: (usize, usize)) -> Self {
798 self.stride = stride;
799 self
800 }
801
802 pub fn padding(mut self, padding: (usize, usize)) -> Self {
804 self.padding = padding;
805 self
806 }
807
808 pub fn withbias(mut self, withbias: bool) -> Self {
810 self.withbias = withbias;
811 self
812 }
813
814 pub fn activation(mut self, activation: ActivationFunc) -> Self {
816 self.activation = Some(activation);
817 self
818 }
819
820 pub fn build(self) -> Conv2D {
822 Conv2D::withshape(
823 &self.name,
824 self.filter_height,
825 self.filter_width,
826 self.in_channels,
827 self.out_channels,
828 self.stride,
829 self.padding,
830 self.withbias,
831 self.activation,
832 )
833 }
834}
835
836#[allow(dead_code)]
838pub struct MaxPool2D {
839 name: String,
841
842 kernel_size: (usize, usize),
844
845 stride: (usize, usize),
847
848 padding: (usize, usize),
850
851 training: bool,
853}
854
855impl MaxPool2D {
856 pub fn new(
858 name: &str,
859 kernel_size: (usize, usize),
860 stride: Option<(usize, usize)>,
861 padding: (usize, usize),
862 ) -> Self {
863 let stride = stride.unwrap_or(kernel_size);
864
865 Self {
866 name: name.to_string(),
867 kernel_size,
868 stride,
869 padding,
870 training: true,
871 }
872 }
873}
874
875impl Layer for MaxPool2D {
876 fn layer_type(&self) -> &str {
877 "MaxPool2D"
878 }
879
880 fn forward(
881 &self,
882 inputs: &dyn ArrayProtocol,
883 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
884 crate::array_protocol::ml_ops::max_pool2d(
886 inputs,
887 self.kernel_size,
888 self.stride,
889 self.padding,
890 )
891 }
892
893 fn backward(
894 &self,
895 input: &dyn ArrayProtocol,
896 grad_output: &dyn ArrayProtocol,
897 ) -> Result<LayerGrad, OperationError> {
898 let inputarr = as_f64_array(input)?
899 .into_dimensionality::<Ix4>()
900 .map_err(|e| {
901 OperationError::ShapeMismatch(format!(
902 "MaxPool2D::backward expects a 4D input: {e}"
903 ))
904 })?;
905 let grad_y = as_f64_array(grad_output)?
906 .into_dimensionality::<Ix4>()
907 .map_err(|e| {
908 OperationError::ShapeMismatch(format!(
909 "MaxPool2D::backward expects a 4D grad_output: {e}"
910 ))
911 })?;
912
913 let (kernel_h, kernel_w) = self.kernel_size;
914 let (stride_h, stride_w) = self.stride;
915 let (pad_h, pad_w) = self.padding;
916
917 let batch_size = inputarr.shape()[0];
918 let input_height = inputarr.shape()[1];
919 let input_width = inputarr.shape()[2];
920 let channels = inputarr.shape()[3];
921 let out_height = grad_y.shape()[1];
922 let out_width = grad_y.shape()[2];
923
924 let mut grad_input =
925 Array4::<f64>::zeros((batch_size, input_height, input_width, channels));
926
927 for b in 0..batch_size {
931 for c in 0..channels {
932 for out_h in 0..out_height {
933 for out_w in 0..out_width {
934 let mut max_val = f64::NEG_INFINITY;
935 let mut argmax: Option<(usize, usize)> = None;
936 for k_h in 0..kernel_h {
937 for k_w in 0..kernel_w {
938 let in_h = (out_h * stride_h) as i32 + k_h as i32 - pad_h as i32;
939 let in_w = (out_w * stride_w) as i32 + k_w as i32 - pad_w as i32;
940 if in_h >= 0
941 && in_h < input_height as i32
942 && in_w >= 0
943 && in_w < input_width as i32
944 {
945 let val = inputarr[[b, in_h as usize, in_w as usize, c]];
946 if val > max_val {
947 max_val = val;
948 argmax = Some((in_h as usize, in_w as usize));
949 }
950 }
951 }
952 }
953 if let Some((in_h, in_w)) = argmax {
954 grad_input[[b, in_h, in_w, c]] += grad_y[[b, out_h, out_w, c]];
955 }
956 }
957 }
958 }
959 }
960
961 Ok(LayerGrad {
962 grad_input: wrap_like(input, grad_input.into_dyn())?,
963 grad_params: Vec::new(),
964 })
965 }
966
967 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
968 Vec::new()
970 }
971
972 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
973 Vec::new()
975 }
976
977 fn update_parameter(
978 &mut self,
979 name: &str,
980 _value: Box<dyn ArrayProtocol>,
981 ) -> Result<(), OperationError> {
982 Err(OperationError::Other(format!(
983 "MaxPool2D has no parameter: {name}"
984 )))
985 }
986
987 fn parameter_names(&self) -> Vec<String> {
988 Vec::new()
990 }
991
992 fn train(&mut self) {
993 self.training = true;
994 }
995
996 fn eval(&mut self) {
997 self.training = false;
998 }
999
1000 fn is_training(&self) -> bool {
1001 self.training
1002 }
1003
1004 fn name(&self) -> &str {
1005 &self.name
1006 }
1007}
1008
1009pub struct BatchNorm {
1011 name: String,
1013
1014 scale: Box<dyn ArrayProtocol>,
1016
1017 offset: Box<dyn ArrayProtocol>,
1019
1020 running_mean: Box<dyn ArrayProtocol>,
1022
1023 running_var: Box<dyn ArrayProtocol>,
1025
1026 epsilon: f64,
1028
1029 training: bool,
1031}
1032
1033impl BatchNorm {
1034 pub fn new(
1036 name: &str,
1037 scale: Box<dyn ArrayProtocol>,
1038 offset: Box<dyn ArrayProtocol>,
1039 running_mean: Box<dyn ArrayProtocol>,
1040 running_var: Box<dyn ArrayProtocol>,
1041 epsilon: f64,
1042 ) -> Self {
1043 Self {
1044 name: name.to_string(),
1045 scale,
1046 offset,
1047 running_mean,
1048 running_var,
1049 epsilon,
1050 training: true,
1051 }
1052 }
1053
1054 pub fn withshape(
1056 name: &str,
1057 num_features: usize,
1058 epsilon: Option<f64>,
1059 _momentum: Option<f64>,
1060 ) -> Self {
1061 let scale: Array<f64, Ix1> = Array::ones(num_features);
1063 let offset: Array<f64, Ix1> = Array::zeros(num_features);
1064 let running_mean: Array<f64, Ix1> = Array::zeros(num_features);
1065 let running_var: Array<f64, Ix1> = Array::ones(num_features);
1066
1067 Self {
1068 name: name.to_string(),
1069 scale: Box::new(NdarrayWrapper::new(scale)),
1070 offset: Box::new(NdarrayWrapper::new(offset)),
1071 running_mean: Box::new(NdarrayWrapper::new(running_mean)),
1072 running_var: Box::new(NdarrayWrapper::new(running_var)),
1073 epsilon: epsilon.unwrap_or(1e-5),
1074 training: true,
1075 }
1076 }
1077}
1078
1079impl Layer for BatchNorm {
1080 fn layer_type(&self) -> &str {
1081 "BatchNorm"
1082 }
1083
1084 fn forward(
1085 &self,
1086 inputs: &dyn ArrayProtocol,
1087 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1088 crate::array_protocol::ml_ops::batch_norm(
1089 inputs,
1090 self.scale.as_ref(),
1091 self.offset.as_ref(),
1092 self.running_mean.as_ref(),
1093 self.running_var.as_ref(),
1094 self.epsilon,
1095 )
1096 }
1097
1098 fn backward(
1099 &self,
1100 input: &dyn ArrayProtocol,
1101 grad_output: &dyn ArrayProtocol,
1102 ) -> Result<LayerGrad, OperationError> {
1103 let x = as_f64_array(input)?
1109 .into_dimensionality::<Ix4>()
1110 .map_err(|e| {
1111 OperationError::ShapeMismatch(format!(
1112 "BatchNorm::backward expects a 4D input: {e}"
1113 ))
1114 })?;
1115 let grad_y = as_f64_array(grad_output)?
1116 .into_dimensionality::<Ix4>()
1117 .map_err(|e| {
1118 OperationError::ShapeMismatch(format!(
1119 "BatchNorm::backward expects a 4D grad_output: {e}"
1120 ))
1121 })?;
1122 let scale = as_f64_array(self.scale.as_ref())?;
1123 let mean = as_f64_array(self.running_mean.as_ref())?;
1124 let variance = as_f64_array(self.running_var.as_ref())?;
1125
1126 let (batch_size, height, width, channels) = x.dim();
1127 let mut grad_input = Array4::<f64>::zeros((batch_size, height, width, channels));
1128 let mut grad_scale = Array1::<f64>::zeros(channels);
1129 let mut grad_offset = Array1::<f64>::zeros(channels);
1130
1131 for c in 0..channels {
1132 let inv_std = 1.0 / (variance[[c]] + self.epsilon).sqrt();
1133 let m = mean[[c]];
1134 let s = scale[[c]];
1135 let mut grad_scale_c = 0.0;
1136 let mut grad_offset_c = 0.0;
1137 for b in 0..batch_size {
1138 for h in 0..height {
1139 for w in 0..width {
1140 let gy = grad_y[[b, h, w, c]];
1141 let normalized = (x[[b, h, w, c]] - m) * inv_std;
1142 grad_scale_c += gy * normalized;
1143 grad_offset_c += gy;
1144 grad_input[[b, h, w, c]] = gy * s * inv_std;
1145 }
1146 }
1147 }
1148 grad_scale[c] = grad_scale_c;
1149 grad_offset[c] = grad_offset_c;
1150 }
1151
1152 Ok(LayerGrad {
1153 grad_input: wrap_like(input, grad_input.into_dyn())?,
1154 grad_params: vec![
1155 wrap_like(self.scale.as_ref(), grad_scale.into_dyn())?,
1156 wrap_like(self.offset.as_ref(), grad_offset.into_dyn())?,
1157 ],
1158 })
1159 }
1160
1161 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1162 vec![self.scale.clone(), self.offset.clone()]
1163 }
1164
1165 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1166 vec![&mut self.scale, &mut self.offset]
1167 }
1168
1169 fn update_parameter(
1170 &mut self,
1171 name: &str,
1172 value: Box<dyn ArrayProtocol>,
1173 ) -> Result<(), OperationError> {
1174 match name {
1175 "scale" => {
1176 self.scale = value;
1177 Ok(())
1178 }
1179 "offset" => {
1180 self.offset = value;
1181 Ok(())
1182 }
1183 _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
1184 }
1185 }
1186
1187 fn parameter_names(&self) -> Vec<String> {
1188 vec!["scale".to_string(), "offset".to_string()]
1189 }
1190
1191 fn train(&mut self) {
1192 self.training = true;
1193 }
1194
1195 fn eval(&mut self) {
1196 self.training = false;
1197 }
1198
1199 fn is_training(&self) -> bool {
1200 self.training
1201 }
1202
1203 fn name(&self) -> &str {
1204 &self.name
1205 }
1206}
1207
1208pub struct Dropout {
1210 name: String,
1212
1213 rate: f64,
1215
1216 seed: Option<u64>,
1218
1219 training: bool,
1221}
1222
1223impl Dropout {
1224 pub fn new(name: &str, rate: f64, seed: Option<u64>) -> Self {
1226 Self {
1227 name: name.to_string(),
1228 rate,
1229 seed,
1230 training: true,
1231 }
1232 }
1233}
1234
1235impl Layer for Dropout {
1236 fn layer_type(&self) -> &str {
1237 "Dropout"
1238 }
1239
1240 fn forward(
1241 &self,
1242 inputs: &dyn ArrayProtocol,
1243 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1244 crate::array_protocol::ml_ops::dropout(inputs, self.rate, self.training, self.seed)
1245 }
1246
1247 fn backward(
1248 &self,
1249 input: &dyn ArrayProtocol,
1250 grad_output: &dyn ArrayProtocol,
1251 ) -> Result<LayerGrad, OperationError> {
1252 if !self.training {
1253 return Ok(LayerGrad {
1255 grad_input: grad_output.box_clone(),
1256 grad_params: Vec::new(),
1257 });
1258 }
1259
1260 let seed = self.seed.ok_or_else(|| {
1264 OperationError::NotImplemented(
1265 "Dropout::backward requires a fixed `seed` to deterministically reproduce the \
1266 forward mask (forward() does not cache it); construct the layer via \
1267 `Dropout::new(name, rate, Some(seed))` to backpropagate through training-mode \
1268 dropout"
1269 .to_string(),
1270 )
1271 })?;
1272
1273 let grad_y = as_f64_array(grad_output)?;
1274 let inputarr = as_f64_array(input)?;
1275 if inputarr.shape() != grad_y.shape() {
1276 return Err(OperationError::ShapeMismatch(format!(
1277 "Dropout::backward: input shape {inputshape:?} != grad_output shape {gradshape:?}",
1278 inputshape = inputarr.shape(),
1279 gradshape = grad_y.shape()
1280 )));
1281 }
1282
1283 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1286 let rate = self.rate;
1287 let mask = Array::from_shape_fn(inputarr.raw_dim(), |_| {
1288 if rng.random::<f64>() >= rate {
1289 1.0
1290 } else {
1291 0.0
1292 }
1293 });
1294 let scale = 1.0 / (1.0 - self.rate);
1295 let grad_input = grad_y * &mask * scale;
1296
1297 Ok(LayerGrad {
1298 grad_input: wrap_like(input, grad_input)?,
1299 grad_params: Vec::new(),
1300 })
1301 }
1302
1303 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1304 Vec::new()
1306 }
1307
1308 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1309 Vec::new()
1311 }
1312
1313 fn update_parameter(
1314 &mut self,
1315 name: &str,
1316 _value: Box<dyn ArrayProtocol>,
1317 ) -> Result<(), OperationError> {
1318 Err(OperationError::Other(format!(
1319 "Dropout has no parameter: {name}"
1320 )))
1321 }
1322
1323 fn parameter_names(&self) -> Vec<String> {
1324 Vec::new()
1326 }
1327
1328 fn train(&mut self) {
1329 self.training = true;
1330 }
1331
1332 fn eval(&mut self) {
1333 self.training = false;
1334 }
1335
1336 fn is_training(&self) -> bool {
1337 self.training
1338 }
1339
1340 fn name(&self) -> &str {
1341 &self.name
1342 }
1343}
1344
1345pub struct MultiHeadAttention {
1347 name: String,
1349
1350 wq: Box<dyn ArrayProtocol>,
1352
1353 wk: Box<dyn ArrayProtocol>,
1355
1356 wv: Box<dyn ArrayProtocol>,
1358
1359 wo: Box<dyn ArrayProtocol>,
1361
1362 num_heads: usize,
1364
1365 dmodel: usize,
1367
1368 training: bool,
1370}
1371
1372impl MultiHeadAttention {
1373 pub fn new(
1375 name: &str,
1376 wq: Box<dyn ArrayProtocol>,
1377 wk: Box<dyn ArrayProtocol>,
1378 wv: Box<dyn ArrayProtocol>,
1379 wo: Box<dyn ArrayProtocol>,
1380 num_heads: usize,
1381 dmodel: usize,
1382 ) -> Self {
1383 Self {
1384 name: name.to_string(),
1385 wq,
1386 wk,
1387 wv,
1388 wo,
1389 num_heads,
1390 dmodel,
1391 training: true,
1392 }
1393 }
1394
1395 pub fn with_params(name: &str, num_heads: usize, dmodel: usize) -> Self {
1397 assert!(
1399 dmodel % num_heads == 0,
1400 "dmodel must be divisible by num_heads"
1401 );
1402
1403 let scale = (1.0_f64 / dmodel as f64).sqrt();
1405 let mut rng = rand::rng();
1406
1407 let wq = Array::from_shape_fn((dmodel, dmodel), |_| {
1408 (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1409 });
1410
1411 let wk = Array::from_shape_fn((dmodel, dmodel), |_| {
1412 (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1413 });
1414
1415 let wv = Array::from_shape_fn((dmodel, dmodel), |_| {
1416 (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1417 });
1418
1419 let wo = Array::from_shape_fn((dmodel, dmodel), |_| {
1420 (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1421 });
1422
1423 Self {
1424 name: name.to_string(),
1425 wq: Box::new(NdarrayWrapper::new(wq)),
1426 wk: Box::new(NdarrayWrapper::new(wk)),
1427 wv: Box::new(NdarrayWrapper::new(wv)),
1428 wo: Box::new(NdarrayWrapper::new(wo)),
1429 num_heads,
1430 dmodel,
1431 training: true,
1432 }
1433 }
1434}
1435
1436impl Layer for MultiHeadAttention {
1437 fn layer_type(&self) -> &str {
1438 "MultiHeadAttention"
1439 }
1440
1441 fn forward(
1442 &self,
1443 inputs: &dyn ArrayProtocol,
1444 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1445 let queries = crate::array_protocol::matmul(self.wq.as_ref(), inputs)?;
1453 let keys = crate::array_protocol::matmul(self.wk.as_ref(), inputs)?;
1454 let values = crate::array_protocol::matmul(self.wv.as_ref(), inputs)?;
1455
1456 let attention = crate::array_protocol::ml_ops::self_attention(
1458 queries.as_ref(),
1459 keys.as_ref(),
1460 values.as_ref(),
1461 None,
1462 Some((self.dmodel / self.num_heads) as f64),
1463 )?;
1464
1465 let output = crate::array_protocol::matmul(self.wo.as_ref(), attention.as_ref())?;
1467
1468 Ok(output)
1469 }
1470
1471 fn backward(
1472 &self,
1473 _input: &dyn ArrayProtocol,
1474 _grad_output: &dyn ArrayProtocol,
1475 ) -> Result<LayerGrad, OperationError> {
1476 Err(OperationError::NotImplemented(
1483 "MultiHeadAttention::backward is not implemented (forward() is itself a documented \
1484 simplified placeholder, not real multi-head attention)"
1485 .to_string(),
1486 ))
1487 }
1488
1489 fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1490 vec![
1491 self.wq.clone(),
1492 self.wk.clone(),
1493 self.wv.clone(),
1494 self.wo.clone(),
1495 ]
1496 }
1497
1498 fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1499 vec![&mut self.wq, &mut self.wk, &mut self.wv, &mut self.wo]
1500 }
1501
1502 fn update_parameter(
1503 &mut self,
1504 name: &str,
1505 value: Box<dyn ArrayProtocol>,
1506 ) -> Result<(), OperationError> {
1507 match name {
1508 "wq" => {
1509 self.wq = value;
1510 Ok(())
1511 }
1512 "wk" => {
1513 self.wk = value;
1514 Ok(())
1515 }
1516 "wv" => {
1517 self.wv = value;
1518 Ok(())
1519 }
1520 "wo" => {
1521 self.wo = value;
1522 Ok(())
1523 }
1524 _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
1525 }
1526 }
1527
1528 fn parameter_names(&self) -> Vec<String> {
1529 vec![
1530 "wq".to_string(),
1531 "wk".to_string(),
1532 "wv".to_string(),
1533 "wo".to_string(),
1534 ]
1535 }
1536
1537 fn train(&mut self) {
1538 self.training = true;
1539 }
1540
1541 fn eval(&mut self) {
1542 self.training = false;
1543 }
1544
1545 fn is_training(&self) -> bool {
1546 self.training
1547 }
1548
1549 fn name(&self) -> &str {
1550 &self.name
1551 }
1552}
1553
1554pub struct Sequential {
1556 name: String,
1558
1559 layers: Vec<Box<dyn Layer>>,
1561
1562 training: bool,
1564}
1565
1566impl Sequential {
1567 pub fn new(name: &str, layers: Vec<Box<dyn Layer>>) -> Self {
1569 Self {
1570 name: name.to_string(),
1571 layers,
1572 training: true,
1573 }
1574 }
1575
1576 pub fn add_layer(&mut self, layer: Box<dyn Layer>) {
1578 self.layers.push(layer);
1579 }
1580
1581 pub fn forward(
1583 &self,
1584 inputs: &dyn ArrayProtocol,
1585 ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1586 let mut x: Box<dyn ArrayProtocol> = inputs.box_clone();
1588
1589 for layer in &self.layers {
1590 let x_ref: &dyn ArrayProtocol = x.as_ref();
1592 x = layer.forward(x_ref)?;
1594 }
1595
1596 Ok(x)
1597 }
1598
1599 pub fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1601 let mut params = Vec::new();
1602
1603 for layer in &self.layers {
1604 params.extend(layer.parameters());
1605 }
1606
1607 params
1608 }
1609
1610 pub fn train(&mut self) {
1612 self.training = true;
1613
1614 for layer in &mut self.layers {
1615 layer.train();
1616 }
1617 }
1618
1619 pub fn eval(&mut self) {
1621 self.training = false;
1622
1623 for layer in &mut self.layers {
1624 layer.eval();
1625 }
1626 }
1627
1628 pub fn name(&self) -> &str {
1630 &self.name
1631 }
1632
1633 pub fn layers(&self) -> &[Box<dyn Layer>] {
1635 &self.layers
1636 }
1637
1638 pub fn layers_mut(&mut self) -> &mut [Box<dyn Layer>] {
1641 &mut self.layers
1642 }
1643
1644 pub fn backward(
1657 &self,
1658 input: &dyn ArrayProtocol,
1659 grad_output: &dyn ArrayProtocol,
1660 ) -> Result<crate::array_protocol::grad::GradientDict, crate::error::CoreError> {
1661 let mut gradients = crate::array_protocol::grad::GradientDict::new();
1662 if self.layers.is_empty() {
1663 return Ok(gradients);
1664 }
1665
1666 let mut activations: Vec<Box<dyn ArrayProtocol>> =
1669 Vec::with_capacity(self.layers.len() + 1);
1670 activations.push(input.box_clone());
1671 for layer in &self.layers {
1672 let layer_input: &dyn ArrayProtocol = activations
1673 .last()
1674 .ok_or_else(|| {
1675 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1676 "internal error: activation cache unexpectedly empty".to_string(),
1677 ))
1678 })?
1679 .as_ref();
1680 let out = layer.forward(layer_input).map_err(|e| {
1681 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1682 "backward(): forward recompute failed in layer '{name}': {e}",
1683 name = layer.name()
1684 )))
1685 })?;
1686 activations.push(out);
1687 }
1688
1689 let mut grad_current: Box<dyn ArrayProtocol> = grad_output.box_clone();
1692 for (layer_idx, layer) in self.layers.iter().enumerate().rev() {
1693 let layer_input: &dyn ArrayProtocol = activations[layer_idx].as_ref();
1694 let layer_grad = layer
1695 .backward(layer_input, grad_current.as_ref())
1696 .map_err(|e| {
1697 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1698 format!(
1699 "backward() failed in layer '{name}' (index {layer_idx}): {e}",
1700 name = layer.name()
1701 ),
1702 ))
1703 })?;
1704
1705 let param_names = layer.parameter_names();
1706 for (param_idx, grad_param) in layer_grad.grad_params.into_iter().enumerate() {
1707 let param_name = param_names.get(param_idx).ok_or_else(|| {
1708 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1709 format!(
1710 "layer '{name}' (index {layer_idx}) returned {ngrads} parameter \
1711 gradient(s) but parameter_names() only has {nnames}",
1712 name = layer.name(),
1713 ngrads = param_idx + 1,
1714 nnames = param_names.len()
1715 ),
1716 ))
1717 })?;
1718 gradients.insert(format!("{layer_idx}.{param_name}"), grad_param);
1719 }
1720
1721 grad_current = layer_grad.grad_input;
1722 }
1723
1724 Ok(gradients)
1725 }
1726
1727 pub fn update_parameter(
1729 &mut self,
1730 param_name: &str,
1731 gradient: &dyn ArrayProtocol,
1732 learningrate: f64,
1733 ) -> Result<(), crate::error::CoreError> {
1734 let parts: Vec<&str> = param_name.split('.').collect();
1736 if parts.len() != 2 {
1737 return Err(crate::error::CoreError::ValueError(
1738 crate::error::ErrorContext::new(format!(
1739 "Invalid parameter name format. Expected 'layer_index.param_name', got: {param_name}"
1740 )),
1741 ));
1742 }
1743
1744 let layer_index: usize = parts[0].parse().map_err(|_| {
1745 crate::error::CoreError::ValueError(crate::error::ErrorContext::new(format!(
1746 "Invalid layer index: {layer_idx}",
1747 layer_idx = parts[0]
1748 )))
1749 })?;
1750
1751 let param_name = parts[1];
1752
1753 if layer_index >= self.layers.len() {
1754 return Err(crate::error::CoreError::ValueError(
1755 crate::error::ErrorContext::new(format!(
1756 "Layer index {layer_index} out of bounds (model has {num_layers} layers)",
1757 num_layers = self.layers.len()
1758 )),
1759 ));
1760 }
1761
1762 let layer = &mut self.layers[layer_index];
1764 let current_params = layer.parameters();
1765 let param_names = layer.parameter_names();
1766
1767 let param_idx = param_names
1769 .iter()
1770 .position(|name| name == param_name)
1771 .ok_or_else(|| {
1772 crate::error::CoreError::ValueError(crate::error::ErrorContext::new(format!(
1773 "Parameter '{param_name}' not found in layer {layer_index}"
1774 )))
1775 })?;
1776
1777 let current_param = ¤t_params[param_idx];
1779
1780 let scaled_gradient =
1782 crate::array_protocol::operations::multiply_by_scalar_f64(gradient, learningrate)
1783 .map_err(|e| {
1784 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1785 format!("Failed to scale gradient: {e}"),
1786 ))
1787 })?;
1788
1789 let updated_param = crate::array_protocol::operations::subtract(
1791 current_param.as_ref(),
1792 scaled_gradient.as_ref(),
1793 )
1794 .map_err(|e| {
1795 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1796 "Failed to update parameter: {e}"
1797 )))
1798 })?;
1799
1800 layer
1802 .update_parameter(param_name, updated_param)
1803 .map_err(|e| {
1804 crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1805 "Failed to set parameter in layer: {e}"
1806 )))
1807 })?;
1808
1809 Ok(())
1810 }
1811
1812 pub fn all_parameter_names(&self) -> Vec<String> {
1814 let mut all_names = Vec::new();
1815 for (layer_idx, layer) in self.layers.iter().enumerate() {
1816 let layer_param_names = layer.parameter_names();
1817 for param_name in layer_param_names {
1818 all_names.push(format!("{layer_idx}.{param_name}"));
1819 }
1820 }
1821 all_names
1822 }
1823
1824 pub fn all_parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1826 let mut all_params = Vec::new();
1827 for layer in &self.layers {
1828 all_params.extend(layer.parameters());
1829 }
1830 all_params
1831 }
1832}
1833
1834#[allow(dead_code)]
1836pub fn create_simple_cnn(inputshape: (usize, usize, usize), num_classes: usize) -> Sequential {
1837 let (height, width, channels) = inputshape;
1838
1839 let mut model = Sequential::new("SimpleCNN", Vec::new());
1840
1841 model.add_layer(Box::new(Conv2D::withshape(
1843 "conv1",
1844 3,
1845 3, channels,
1847 32, (1, 1), (1, 1), true, Some(ActivationFunc::ReLU),
1852 )));
1853
1854 model.add_layer(Box::new(MaxPool2D::new(
1855 "pool1",
1856 (2, 2), None, (0, 0), )));
1860
1861 model.add_layer(Box::new(Conv2D::withshape(
1863 "conv2",
1864 3,
1865 3, 32,
1867 64, (1, 1), (1, 1), true, Some(ActivationFunc::ReLU),
1872 )));
1873
1874 model.add_layer(Box::new(MaxPool2D::new(
1875 "pool2",
1876 (2, 2), None, (0, 0), )));
1880
1881 model.add_layer(Box::new(Linear::new_random(
1885 "fc1",
1886 64 * (height / 4) * (width / 4), 128, true, Some(ActivationFunc::ReLU),
1890 )));
1891
1892 model.add_layer(Box::new(Dropout::new(
1893 "dropout", 0.5, None, )));
1896
1897 model.add_layer(Box::new(Linear::new_random(
1898 "fc2",
1899 128, num_classes, true, None, )));
1904
1905 model
1906}
1907
1908#[cfg(test)]
1909mod tests {
1910 use super::*;
1911 use crate::array_protocol::{self, NdarrayWrapper};
1912 use ndarray::{Array1, Array2};
1913
1914 #[test]
1915 fn test_linear_layer() {
1916 array_protocol::init();
1918
1919 let weights = Array2::<f64>::eye(3);
1921 let bias = Array1::<f64>::ones(3);
1922
1923 let layer = Linear::new(
1924 "linear",
1925 Box::new(NdarrayWrapper::new(weights)),
1926 Some(Box::new(NdarrayWrapper::new(bias))),
1927 Some(ActivationFunc::ReLU),
1928 );
1929
1930 assert_eq!(layer.name(), "linear");
1941 assert!(layer.is_training());
1942 }
1943
1944 #[test]
1945 fn test_sequential_model() {
1946 array_protocol::init();
1948
1949 let mut model = Sequential::new("test_model", Vec::new());
1951
1952 model.add_layer(Box::new(Linear::new_random(
1954 "fc1",
1955 3, 2, true, Some(ActivationFunc::ReLU),
1959 )));
1960
1961 model.add_layer(Box::new(Linear::new_random(
1962 "fc2",
1963 2, 1, true, Some(ActivationFunc::Sigmoid),
1967 )));
1968
1969 assert_eq!(model.name(), "test_model");
1971 assert_eq!(model.layers().len(), 2);
1972 assert!(model.training);
1973 }
1974
1975 #[test]
1976 fn test_simple_cnn_creation() {
1977 array_protocol::init();
1979
1980 let model = create_simple_cnn((28, 28, 1), 10);
1982
1983 assert_eq!(model.layers().len(), 7);
1985 assert_eq!(model.name(), "SimpleCNN");
1986
1987 let params = model.parameters();
1989 assert!(!params.is_empty());
1990 }
1991}
1992
1993#[cfg(test)]
1996#[path = "neural_backward_tests.rs"]
1997mod backward_tests;