1use crate::error::{MLError, Result};
7use crate::optimization::OptimizationMethod;
8use crate::qcnn::PoolingType;
9use crate::qnn::QNNLayerType;
10use crate::qnn::QuantumNeuralNetwork;
11use crate::quantum_transformer::{
12 QuantumAttentionType, QuantumTransformer, QuantumTransformerConfig,
13};
14use quantrs2_circuit::builder::Circuit;
15use scirs2_core::ndarray::*;
16use scirs2_core::random::prelude::*;
17use scirs2_core::random::{ChaCha20Rng, Rng, SeedableRng};
18use scirs2_core::{Complex32, Complex64};
19use std::collections::HashMap;
20use std::f64::consts::PI;
21
22pub trait VisionModel: std::fmt::Debug {
24 fn forward(&self, input: &Array4<f64>) -> Result<Array4<f64>>;
26 fn parameters(&self) -> &Array1<f64>;
28 fn update_parameters(&mut self, params: &Array1<f64>) -> Result<()>;
30 fn num_parameters(&self) -> usize;
32 fn clone_box(&self) -> Box<dyn VisionModel>;
34}
35
36pub trait TaskHead: std::fmt::Debug {
37 fn forward(&self, features: &Array4<f64>) -> Result<TaskOutput>;
39 fn parameters(&self) -> &Array1<f64>;
41 fn update_parameters(&mut self, params: &Array1<f64>) -> Result<()>;
43 fn clone_box(&self) -> Box<dyn TaskHead>;
45}
46
47#[derive(Debug, Clone)]
48pub struct AugmentationConfig {
49 pub horizontal_flip: bool,
51 pub rotation_range: f64,
53 pub zoom_range: (f64, f64),
55 pub brightness_range: (f64, f64),
57 pub quantum_noise: bool,
59}
60impl AugmentationConfig {
61 pub fn default() -> Self {
63 Self {
64 horizontal_flip: true,
65 rotation_range: 15.0,
66 zoom_range: (0.8, 1.2),
67 brightness_range: (0.8, 1.2),
68 quantum_noise: false,
69 }
70 }
71 pub fn detection() -> Self {
73 Self {
74 horizontal_flip: true,
75 rotation_range: 5.0,
76 zoom_range: (0.9, 1.1),
77 brightness_range: (0.9, 1.1),
78 quantum_noise: true,
79 }
80 }
81 pub fn segmentation() -> Self {
83 Self {
84 horizontal_flip: true,
85 rotation_range: 10.0,
86 zoom_range: (0.85, 1.15),
87 brightness_range: (0.85, 1.15),
88 quantum_noise: false,
89 }
90 }
91}
92#[derive(Debug, Clone)]
93pub struct FeatureExtractionHead {
94 pub feature_dim: usize,
95 pub normalize: bool,
96 pub parameters: Array1<f64>,
97}
98impl FeatureExtractionHead {
99 fn new(feature_dim: usize, normalize: bool) -> Result<Self> {
100 Ok(Self {
101 feature_dim,
102 normalize,
103 parameters: Array1::zeros(50),
104 })
105 }
106}
107#[derive(Debug, Clone)]
109pub struct NormalizationParams {
110 pub mean: Array1<f64>,
111 pub std: Array1<f64>,
112}
113#[derive(Debug, Clone)]
115pub enum TaskTarget {
116 Classification {
117 labels: Vec<usize>,
118 },
119 Detection {
120 boxes: Array3<f64>,
121 labels: Array2<usize>,
122 },
123 Segmentation {
124 masks: Array4<usize>,
125 },
126 Features {
127 target_features: Array2<f64>,
128 },
129}
130#[derive(Debug, Clone)]
132pub struct ImagePreprocessor {
133 pub config: PreprocessingConfig,
135 pub norm_params: NormalizationParams,
137}
138impl ImagePreprocessor {
139 pub fn new(config: PreprocessingConfig) -> Self {
141 let norm_params = NormalizationParams {
142 mean: Array1::from_vec(config.mean.clone()),
143 std: Array1::from_vec(config.std.clone()),
144 };
145 Self {
146 config,
147 norm_params,
148 }
149 }
150 pub fn preprocess(&self, images: &Array4<f64>) -> Result<Array4<f64>> {
152 let mut processed = images.clone();
153 if images.dim().2 != self.config.image_size.0 || images.dim().3 != self.config.image_size.1
154 {
155 processed = self.resize(&processed, self.config.image_size)?;
156 }
157 if self.config.normalize {
158 processed = self.normalize(&processed)?;
159 }
160 if self.config.augmentation.horizontal_flip && fastrand::f64() > 0.5 {
161 processed = self.horizontal_flip(&processed)?;
162 }
163 Ok(processed)
164 }
165 fn resize(&self, images: &Array4<f64>, size: (usize, usize)) -> Result<Array4<f64>> {
167 let (batch_size, channels, _, _) = images.dim();
168 let mut resized = Array4::zeros((batch_size, channels, size.0, size.1));
169 for b in 0..batch_size {
170 for c in 0..channels {
171 for h in 0..size.0 {
172 for w in 0..size.1 {
173 let src_h = h * images.dim().2 / size.0;
174 let src_w = w * images.dim().3 / size.1;
175 resized[[b, c, h, w]] = images[[b, c, src_h, src_w]];
176 }
177 }
178 }
179 }
180 Ok(resized)
181 }
182 fn normalize(&self, images: &Array4<f64>) -> Result<Array4<f64>> {
184 let mut normalized = images.clone();
185 let channels = images.dim().1;
186 for c in 0..channels.min(self.norm_params.mean.len()) {
187 let mean = self.norm_params.mean[c];
188 let std = self.norm_params.std[c];
189 normalized
190 .slice_mut(s![.., c, .., ..])
191 .mapv_inplace(|x| (x - mean) / std);
192 }
193 Ok(normalized)
194 }
195 fn horizontal_flip(&self, images: &Array4<f64>) -> Result<Array4<f64>> {
197 let (batch_size, channels, height, width) = images.dim();
198 let mut flipped = Array4::zeros((batch_size, channels, height, width));
199 for b in 0..batch_size {
200 for c in 0..channels {
201 for h in 0..height {
202 for w in 0..width {
203 flipped[[b, c, h, w]] = images[[b, c, h, width - 1 - w]];
204 }
205 }
206 }
207 }
208 Ok(flipped)
209 }
210}
211#[derive(Debug, Clone)]
213pub enum QuantumEnhancement {
214 Low,
216 Medium,
218 High,
220 Custom {
222 quantum_layers: Vec<usize>,
223 entanglement_strength: f64,
224 },
225}
226#[derive(Debug, Clone)]
228pub struct VisionMetrics {
229 pub task_metrics: HashMap<String, f64>,
231 pub quantum_metrics: QuantumMetrics,
233 pub computational_metrics: ComputationalMetrics,
235}
236impl VisionMetrics {
237 pub fn new() -> Self {
239 Self {
240 task_metrics: HashMap::new(),
241 quantum_metrics: QuantumMetrics {
242 circuit_depth: 0,
243 entanglement_entropy: 0.0,
244 quantum_advantage: 1.0,
245 coherence_utilization: 0.8,
246 },
247 computational_metrics: ComputationalMetrics {
248 flops: 0.0,
249 memory_mb: 0.0,
250 inference_ms: 0.0,
251 throughput: 0.0,
252 },
253 }
254 }
255}
256#[derive(Debug, Clone)]
258pub struct QuantumSpatialAttention {
259 pub num_heads: usize,
261 pub attention_dim: usize,
263 pub attention_circuit_params: Vec<Vec<f64>>,
265}
266impl QuantumSpatialAttention {
267 pub fn new(num_heads: usize, attention_dim: usize, num_qubits: usize) -> Result<Self> {
269 let mut attention_circuit_params = Vec::new();
270 for _ in 0..num_heads {
271 let mut params = Vec::new();
272 for _ in 0..num_qubits.min(attention_dim / 8) {
273 params.push(1.0);
274 params.push(0.0);
275 }
276 attention_circuit_params.push(params);
277 }
278 Ok(Self {
279 num_heads,
280 attention_dim,
281 attention_circuit_params,
282 })
283 }
284 pub fn apply(&self, features: &Array4<f64>) -> Result<Array4<f64>> {
286 Ok(features.clone())
287 }
288}
289#[derive(Debug, Clone)]
291pub struct ComputationalMetrics {
292 pub flops: f64,
294 pub memory_mb: f64,
296 pub inference_ms: f64,
298 pub throughput: f64,
300}
301#[derive(Debug, Clone)]
303pub struct QuantumConvolutionalNN {
304 pub num_filters: usize,
306 pub kernel_size: usize,
308 pub num_qubits: usize,
310 pub parameters: Array1<f64>,
312}
313impl QuantumConvolutionalNN {
314 fn new(
315 _layers: Vec<QNNLayerType>,
316 num_qubits: usize,
317 _input_size: usize,
318 num_filters: usize,
319 ) -> Result<Self> {
320 Ok(Self {
321 num_filters,
322 kernel_size: 3,
323 num_qubits,
324 parameters: Array1::zeros(100),
325 })
326 }
327}
328#[derive(Debug, Clone)]
330pub struct QuantumViTBackbone {
331 pub patch_size: usize,
333 pub embed_dim: usize,
335 pub transformer: QuantumTransformer,
337 pub parameters: Array1<f64>,
339}
340impl QuantumViTBackbone {
341 fn new(
342 patch_size: usize,
343 embed_dim: usize,
344 num_heads: usize,
345 depth: usize,
346 num_qubits: usize,
347 ) -> Result<Self> {
348 let config = QuantumTransformerConfig {
349 model_dim: embed_dim,
350 num_heads,
351 ff_dim: embed_dim * 4,
352 num_layers: depth,
353 max_seq_len: 1024,
354 num_qubits,
355 dropout_rate: 0.1,
356 attention_type: QuantumAttentionType::QuantumEnhancedMultiHead,
357 position_encoding: crate::quantum_transformer::PositionEncodingType::LearnableQuantum,
358 };
359 let transformer = QuantumTransformer::new(config)?;
360 Ok(Self {
361 patch_size,
362 embed_dim,
363 transformer,
364 parameters: Array1::zeros(1000),
365 })
366 }
367}
368#[derive(Debug, Clone)]
370pub struct QuantumEfficientNetBackbone {
371 pub width_coefficient: f64,
373 pub depth_coefficient: f64,
375 pub num_qubits: usize,
377 pub parameters: Array1<f64>,
379}
380impl QuantumEfficientNetBackbone {
381 fn new(width_coefficient: f64, depth_coefficient: f64, num_qubits: usize) -> Result<Self> {
382 Ok(Self {
383 width_coefficient,
384 depth_coefficient,
385 num_qubits,
386 parameters: Array1::zeros(800),
387 })
388 }
389}
390#[derive(Debug, Clone)]
392pub struct HybridVisionBackbone {
393 pub cnn_layers: usize,
395 pub transformer_layers: usize,
397 pub num_qubits: usize,
399 pub parameters: Array1<f64>,
401}
402impl HybridVisionBackbone {
403 fn new(cnn_layers: usize, transformer_layers: usize, num_qubits: usize) -> Result<Self> {
404 Ok(Self {
405 cnn_layers,
406 transformer_layers,
407 num_qubits,
408 parameters: Array1::zeros(500),
409 })
410 }
411}
412#[derive(Debug, Clone)]
413pub struct SegmentationHead {
414 pub num_classes: usize,
415 pub output_stride: usize,
416 pub parameters: Array1<f64>,
417}
418impl SegmentationHead {
419 fn new(num_classes: usize, output_stride: usize) -> Result<Self> {
420 Ok(Self {
421 num_classes,
422 output_stride,
423 parameters: Array1::zeros(200),
424 })
425 }
426}
427#[derive(Debug, Clone)]
429pub enum ImageEncodingMethod {
430 AmplitudeEncoding,
432 AngleEncoding { basis: String },
434 FRQI,
436 NEQR { gray_levels: usize },
438 QPIE,
440 HierarchicalEncoding { levels: usize },
442}
443#[derive(Debug, Clone)]
445pub enum ColorSpace {
446 RGB,
447 Grayscale,
448 HSV,
449 LAB,
450 YCbCr,
451}
452#[derive(Debug, Clone)]
453pub struct InstanceSegmentationHead {
454 pub num_classes: usize,
455 pub mask_resolution: (usize, usize),
456 pub parameters: Array1<f64>,
457}
458impl InstanceSegmentationHead {
459 fn new(num_classes: usize, mask_resolution: (usize, usize)) -> Result<Self> {
460 Ok(Self {
461 num_classes,
462 mask_resolution,
463 parameters: Array1::zeros(300),
464 })
465 }
466}
467#[derive(Debug, Clone)]
469pub struct QuantumImageEncoder {
470 pub method: ImageEncodingMethod,
472 pub num_qubits: usize,
474 pub encoding_circuits: Vec<Circuit<16>>,
476 pub parameters: Array1<f64>,
478}
479impl QuantumImageEncoder {
480 pub fn new(method: ImageEncodingMethod, num_qubits: usize) -> Result<Self> {
482 let encoding_circuits = match &method {
483 ImageEncodingMethod::AmplitudeEncoding => {
484 Self::create_amplitude_encoding_circuits(num_qubits)?
485 }
486 ImageEncodingMethod::AngleEncoding { basis } => {
487 Self::create_angle_encoding_circuits(num_qubits, basis)?
488 }
489 ImageEncodingMethod::FRQI => Self::create_frqi_circuits(num_qubits)?,
490 ImageEncodingMethod::NEQR { gray_levels } => {
491 Self::create_neqr_circuits(num_qubits, *gray_levels)?
492 }
493 ImageEncodingMethod::QPIE => Self::create_qpie_circuits(num_qubits)?,
494 ImageEncodingMethod::HierarchicalEncoding { levels } => {
495 Self::create_hierarchical_circuits(num_qubits, *levels)?
496 }
497 };
498 let parameters = Array1::zeros(encoding_circuits.len() * 10);
499 Ok(Self {
500 method,
501 num_qubits,
502 encoding_circuits,
503 parameters,
504 })
505 }
506 pub fn encode(&self, images: &Array4<f64>) -> Result<Array4<f64>> {
508 let (batch_size, channels, height, width) = images.dim();
509 let mut encoded = Array4::zeros((batch_size, channels, height, width));
510 for b in 0..batch_size {
511 for c in 0..channels {
512 let image_slice = images.slice(s![b, c, .., ..]).to_owned();
513 let encoded_slice = self.encode_single_channel(&image_slice)?;
514 encoded.slice_mut(s![b, c, .., ..]).assign(&encoded_slice);
515 }
516 }
517 Ok(encoded)
518 }
519 fn encode_single_channel(&self, channel: &Array2<f64>) -> Result<Array2<f64>> {
521 Ok(channel.mapv(|x| (x * PI).sin()))
522 }
523 fn create_amplitude_encoding_circuits(num_qubits: usize) -> Result<Vec<Circuit<16>>> {
525 let mut circuits = Vec::new();
526 let mut circuit = Circuit::<16>::new();
527 for i in 0..num_qubits.min(16) {
528 circuit.h(i);
529 }
530 for i in 0..num_qubits.min(16) {
531 circuit.ry(i, 0.0);
532 }
533 circuits.push(circuit);
534 Ok(circuits)
535 }
536 fn create_angle_encoding_circuits(num_qubits: usize, basis: &str) -> Result<Vec<Circuit<16>>> {
538 let mut circuits = Vec::new();
539 let mut circuit = Circuit::<16>::new();
540 match basis {
541 "x" => {
542 for i in 0..num_qubits.min(16) {
543 circuit.rx(i, 0.0);
544 }
545 }
546 "y" => {
547 for i in 0..num_qubits.min(16) {
548 circuit.ry(i, 0.0);
549 }
550 }
551 "z" => {
552 for i in 0..num_qubits.min(16) {
553 circuit.rz(i, 0.0);
554 }
555 }
556 _ => {
557 for i in 0..num_qubits.min(16) {
558 circuit.ry(i, 0.0);
559 }
560 }
561 }
562 circuits.push(circuit);
563 Ok(circuits)
564 }
565 fn create_frqi_circuits(num_qubits: usize) -> Result<Vec<Circuit<16>>> {
567 let mut circuits = Vec::new();
568 let mut circuit = Circuit::<16>::new();
569 let position_qubits = (num_qubits - 1).min(15);
570 for i in 0..position_qubits {
571 circuit.h(i);
572 }
573 if position_qubits < 16 {
574 circuit.ry(position_qubits, 0.0);
575 }
576 circuits.push(circuit);
577 Ok(circuits)
578 }
579 fn create_neqr_circuits(num_qubits: usize, gray_levels: usize) -> Result<Vec<Circuit<16>>> {
581 let mut circuits = Vec::new();
582 let gray_qubits = (gray_levels as f64).log2().ceil() as usize;
583 let position_qubits = num_qubits - gray_qubits;
584 let mut circuit = Circuit::<16>::new();
585 for i in 0..position_qubits.min(16) {
586 circuit.h(i);
587 }
588 for i in position_qubits..num_qubits.min(16) {
589 circuit.ry(i, 0.0);
590 }
591 circuits.push(circuit);
592 Ok(circuits)
593 }
594 fn create_qpie_circuits(num_qubits: usize) -> Result<Vec<Circuit<16>>> {
596 let mut circuits = Vec::new();
597 let mut circuit = Circuit::<16>::new();
598 for i in 0..num_qubits.min(16) {
599 circuit.h(i);
600 circuit.ry(i, 0.0);
601 }
602 for i in 0..(num_qubits - 1).min(15) {
603 circuit.cnot(i, i + 1);
604 }
605 circuits.push(circuit);
606 Ok(circuits)
607 }
608 fn create_hierarchical_circuits(num_qubits: usize, levels: usize) -> Result<Vec<Circuit<16>>> {
610 let mut circuits = Vec::new();
611 let qubits_per_level = num_qubits / levels;
612 for level in 0..levels {
613 let mut circuit = Circuit::<16>::new();
614 let start_qubit = level * qubits_per_level;
615 let end_qubit = ((level + 1) * qubits_per_level).min(num_qubits).min(16);
616 for i in start_qubit..end_qubit {
617 circuit.h(i);
618 circuit.ry(i, 0.0);
619 }
620 if level > 0 && start_qubit > 0 && start_qubit < 16 {
621 circuit.cnot(start_qubit - 1, start_qubit);
622 }
623 circuits.push(circuit);
624 }
625 Ok(circuits)
626 }
627}
628#[derive(Debug, Clone)]
630pub struct QuantumVisionConfig {
631 pub num_qubits: usize,
633 pub encoding_method: ImageEncodingMethod,
635 pub backbone: VisionBackbone,
637 pub task_config: VisionTaskConfig,
639 pub preprocessing: PreprocessingConfig,
641 pub quantum_enhancement: QuantumEnhancement,
643}
644impl QuantumVisionConfig {
645 pub fn default() -> Self {
647 Self {
648 num_qubits: 12,
649 encoding_method: ImageEncodingMethod::AmplitudeEncoding,
650 backbone: VisionBackbone::QuantumCNN {
651 conv_layers: vec![
652 ConvolutionalConfig {
653 num_filters: 32,
654 kernel_size: 3,
655 stride: 1,
656 padding: 1,
657 quantum_kernel: true,
658 circuit_depth: 4,
659 },
660 ConvolutionalConfig {
661 num_filters: 64,
662 kernel_size: 3,
663 stride: 2,
664 padding: 1,
665 quantum_kernel: true,
666 circuit_depth: 6,
667 },
668 ],
669 pooling_type: PoolingType::Quantum,
670 },
671 task_config: VisionTaskConfig::Classification {
672 num_classes: 10,
673 multi_label: false,
674 },
675 preprocessing: PreprocessingConfig::default(),
676 quantum_enhancement: QuantumEnhancement::Medium,
677 }
678 }
679 pub fn object_detection(num_classes: usize) -> Self {
681 Self {
682 num_qubits: 16,
683 encoding_method: ImageEncodingMethod::NEQR { gray_levels: 256 },
684 backbone: VisionBackbone::HybridBackbone {
685 cnn_layers: 4,
686 transformer_layers: 2,
687 },
688 task_config: VisionTaskConfig::ObjectDetection {
689 num_classes,
690 anchor_sizes: vec![(32, 32), (64, 64), (128, 128)],
691 iou_threshold: 0.5,
692 },
693 preprocessing: PreprocessingConfig::detection_default(),
694 quantum_enhancement: QuantumEnhancement::High,
695 }
696 }
697 pub fn segmentation(num_classes: usize) -> Self {
699 Self {
700 num_qubits: 14,
701 encoding_method: ImageEncodingMethod::HierarchicalEncoding { levels: 3 },
702 backbone: VisionBackbone::QuantumViT {
703 patch_size: 16,
704 embed_dim: 768,
705 num_heads: 12,
706 depth: 12,
707 },
708 task_config: VisionTaskConfig::Segmentation {
709 num_classes,
710 output_stride: 8,
711 },
712 preprocessing: PreprocessingConfig::segmentation_default(),
713 quantum_enhancement: QuantumEnhancement::High,
714 }
715 }
716}
717#[derive(Debug, Clone)]
719pub struct PreprocessingConfig {
720 pub image_size: (usize, usize),
722 pub normalize: bool,
724 pub mean: Vec<f64>,
725 pub std: Vec<f64>,
726 pub augmentation: AugmentationConfig,
728 pub color_space: ColorSpace,
730}
731impl PreprocessingConfig {
732 pub fn default() -> Self {
734 Self {
735 image_size: (224, 224),
736 normalize: true,
737 mean: vec![0.485, 0.456, 0.406],
738 std: vec![0.229, 0.224, 0.225],
739 augmentation: AugmentationConfig::default(),
740 color_space: ColorSpace::RGB,
741 }
742 }
743 pub fn detection_default() -> Self {
745 Self {
746 image_size: (416, 416),
747 normalize: true,
748 mean: vec![0.5, 0.5, 0.5],
749 std: vec![0.5, 0.5, 0.5],
750 augmentation: AugmentationConfig::detection(),
751 color_space: ColorSpace::RGB,
752 }
753 }
754 pub fn segmentation_default() -> Self {
756 Self {
757 image_size: (512, 512),
758 normalize: true,
759 mean: vec![0.485, 0.456, 0.406],
760 std: vec![0.229, 0.224, 0.225],
761 augmentation: AugmentationConfig::segmentation(),
762 color_space: ColorSpace::RGB,
763 }
764 }
765}
766#[derive(Debug, Clone)]
768pub enum VisionBackbone {
769 QuantumCNN {
771 conv_layers: Vec<ConvolutionalConfig>,
772 pooling_type: PoolingType,
773 },
774 QuantumViT {
776 patch_size: usize,
777 embed_dim: usize,
778 num_heads: usize,
779 depth: usize,
780 },
781 HybridBackbone {
783 cnn_layers: usize,
784 transformer_layers: usize,
785 },
786 QuantumResNet {
788 blocks: Vec<ResidualBlock>,
789 skip_connections: bool,
790 },
791 QuantumEfficientNet {
793 width_coefficient: f64,
794 depth_coefficient: f64,
795 },
796}
797#[derive(Debug, Clone)]
799pub struct QuantumVisionPipeline {
800 pub config: QuantumVisionConfig,
802 pub encoder: QuantumImageEncoder,
804 pub backbone: Box<dyn VisionModel>,
806 pub task_head: Box<dyn TaskHead>,
808 pub feature_extractor: QuantumFeatureExtractor,
810 pub preprocessor: ImagePreprocessor,
812 pub metrics: VisionMetrics,
814 backbone_gradient: Option<Array1<f64>>,
817 task_head_gradient: Option<Array1<f64>>,
820}
821impl QuantumVisionPipeline {
822 pub fn new(config: QuantumVisionConfig) -> Result<Self> {
824 let encoder = QuantumImageEncoder::new(config.encoding_method.clone(), config.num_qubits)?;
825 let backbone: Box<dyn VisionModel> = match &config.backbone {
826 VisionBackbone::QuantumCNN {
827 conv_layers,
828 pooling_type,
829 } => Box::new(QuantumCNNBackbone::new(
830 conv_layers.clone(),
831 pooling_type.clone(),
832 config.num_qubits,
833 )?),
834 VisionBackbone::QuantumViT {
835 patch_size,
836 embed_dim,
837 num_heads,
838 depth,
839 } => Box::new(QuantumViTBackbone::new(
840 *patch_size,
841 *embed_dim,
842 *num_heads,
843 *depth,
844 config.num_qubits,
845 )?),
846 VisionBackbone::HybridBackbone {
847 cnn_layers,
848 transformer_layers,
849 } => Box::new(HybridVisionBackbone::new(
850 *cnn_layers,
851 *transformer_layers,
852 config.num_qubits,
853 )?),
854 VisionBackbone::QuantumResNet {
855 blocks,
856 skip_connections,
857 } => Box::new(QuantumResNetBackbone::new(
858 blocks.clone(),
859 *skip_connections,
860 config.num_qubits,
861 )?),
862 VisionBackbone::QuantumEfficientNet {
863 width_coefficient,
864 depth_coefficient,
865 } => Box::new(QuantumEfficientNetBackbone::new(
866 *width_coefficient,
867 *depth_coefficient,
868 config.num_qubits,
869 )?),
870 };
871 let task_head: Box<dyn TaskHead> = match &config.task_config {
872 VisionTaskConfig::Classification {
873 num_classes,
874 multi_label,
875 } => Box::new(ClassificationHead::new(*num_classes, *multi_label)?),
876 VisionTaskConfig::ObjectDetection {
877 num_classes,
878 anchor_sizes,
879 iou_threshold,
880 } => Box::new(DetectionHead::new(
881 *num_classes,
882 anchor_sizes.clone(),
883 *iou_threshold,
884 )?),
885 VisionTaskConfig::Segmentation {
886 num_classes,
887 output_stride,
888 } => Box::new(SegmentationHead::new(*num_classes, *output_stride)?),
889 VisionTaskConfig::InstanceSegmentation {
890 num_classes,
891 mask_resolution,
892 } => Box::new(InstanceSegmentationHead::new(
893 *num_classes,
894 *mask_resolution,
895 )?),
896 VisionTaskConfig::FeatureExtraction {
897 feature_dim,
898 normalize,
899 } => Box::new(FeatureExtractionHead::new(*feature_dim, *normalize)?),
900 VisionTaskConfig::Generation {
901 latent_dim,
902 output_channels,
903 } => Box::new(GenerationHead::new(*latent_dim, *output_channels)?),
904 };
905 let feature_extractor = QuantumFeatureExtractor::new(512, config.num_qubits)?;
906 let preprocessor = ImagePreprocessor::new(config.preprocessing.clone());
907 let metrics = VisionMetrics::new();
908 Ok(Self {
909 config,
910 encoder,
911 backbone,
912 task_head,
913 feature_extractor,
914 preprocessor,
915 metrics,
916 backbone_gradient: None,
917 task_head_gradient: None,
918 })
919 }
920 pub fn forward(&mut self, images: &Array4<f64>) -> Result<TaskOutput> {
922 let (batch_size, channels, height, width) = images.dim();
923 let processed = self.preprocessor.preprocess(images)?;
924 let encoded = self.encoder.encode(&processed)?;
925 let features = self.backbone.forward(&encoded)?;
926 let quantum_features = self.feature_extractor.extract(&features)?;
927 let output = self.task_head.forward(&quantum_features)?;
928 self.update_metrics(&features, &output);
929 Ok(output)
930 }
931 pub fn train(
933 &mut self,
934 train_data: &[(Array4<f64>, TaskTarget)],
935 val_data: &[(Array4<f64>, TaskTarget)],
936 epochs: usize,
937 optimizer: OptimizationMethod,
938 ) -> Result<TrainingHistory> {
939 let mut history = TrainingHistory::new();
940 for epoch in 0..epochs {
941 let mut train_loss = 0.0;
942 for (images, target) in train_data {
943 let output = self.forward(images)?;
944 let loss = Self::compute_loss(&output, target)?;
945 self.backward(images, target, loss)?;
946 self.update_parameters(&optimizer)?;
947 train_loss += loss;
948 }
949 let mut val_loss = 0.0;
950 let mut val_metrics = HashMap::new();
951 for (images, target) in val_data {
952 let output = self.forward(images)?;
953 let loss = Self::compute_loss(&output, target)?;
954 val_loss += loss;
955 let metrics = self.evaluate_metrics(&output, target)?;
956 for (key, value) in metrics {
957 *val_metrics.entry(key).or_insert(0.0) += value;
958 }
959 }
960 train_loss /= train_data.len() as f64;
961 val_loss /= val_data.len() as f64;
962 for value in val_metrics.values_mut() {
963 *value /= val_data.len() as f64;
964 }
965 history.add_epoch(epoch, train_loss, val_loss, val_metrics);
966 println!(
967 "Epoch {}/{}: train_loss={:.4}, val_loss={:.4}",
968 epoch + 1,
969 epochs,
970 train_loss,
971 val_loss
972 );
973 }
974 Ok(history)
975 }
976 fn compute_loss(output: &TaskOutput, target: &TaskTarget) -> Result<f64> {
978 match (output, target) {
979 (TaskOutput::Classification { logits, .. }, TaskTarget::Classification { labels }) => {
980 let mut loss = 0.0;
981 for (logit_row, &label) in logits.outer_iter().zip(labels.iter()) {
982 let max_logit = logit_row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
983 let exp_logits: Vec<f64> =
984 logit_row.iter().map(|&x| (x - max_logit).exp()).collect();
985 let sum_exp: f64 = exp_logits.iter().sum();
986 let prob = exp_logits[label] / sum_exp;
987 loss -= prob.ln();
988 }
989 Ok(loss / labels.len() as f64)
990 }
991 _ => Ok(0.1),
992 }
993 }
994 fn forward_loss(&self, images: &Array4<f64>, target: &TaskTarget) -> Result<f64> {
999 let processed = self.preprocessor.preprocess(images)?;
1000 let encoded = self.encoder.encode(&processed)?;
1001 let features = self.backbone.forward(&encoded)?;
1002 let quantum_features = self.feature_extractor.extract(&features)?;
1003 let output = self.task_head.forward(&quantum_features)?;
1004 Self::compute_loss(&output, target)
1005 }
1006
1007 fn estimate_backbone_gradient(
1014 &mut self,
1015 images: &Array4<f64>,
1016 target: &TaskTarget,
1017 ) -> Result<Array1<f64>> {
1018 const PERTURBATION_SCALE: f64 = 1e-2;
1019
1020 let base_params = self.backbone.parameters().clone();
1021 let mut rng = thread_rng();
1022 let direction: Array1<f64> =
1023 base_params.mapv(|_| if rng.random::<f64>() < 0.5 { -1.0 } else { 1.0 });
1024
1025 let plus_params = &base_params + &(&direction * PERTURBATION_SCALE);
1026 self.backbone.update_parameters(&plus_params)?;
1027 let loss_plus = self.forward_loss(images, target)?;
1028
1029 let minus_params = &base_params - &(&direction * PERTURBATION_SCALE);
1030 self.backbone.update_parameters(&minus_params)?;
1031 let loss_minus = self.forward_loss(images, target)?;
1032
1033 self.backbone.update_parameters(&base_params)?;
1035
1036 let loss_delta = loss_plus - loss_minus;
1037 Ok(direction.mapv(|d| (loss_delta / (2.0 * PERTURBATION_SCALE)) * d))
1038 }
1039
1040 fn estimate_task_head_gradient(
1043 &mut self,
1044 images: &Array4<f64>,
1045 target: &TaskTarget,
1046 ) -> Result<Array1<f64>> {
1047 const PERTURBATION_SCALE: f64 = 1e-2;
1048
1049 let base_params = self.task_head.parameters().clone();
1050 let mut rng = thread_rng();
1051 let direction: Array1<f64> =
1052 base_params.mapv(|_| if rng.random::<f64>() < 0.5 { -1.0 } else { 1.0 });
1053
1054 let plus_params = &base_params + &(&direction * PERTURBATION_SCALE);
1055 self.task_head.update_parameters(&plus_params)?;
1056 let loss_plus = self.forward_loss(images, target)?;
1057
1058 let minus_params = &base_params - &(&direction * PERTURBATION_SCALE);
1059 self.task_head.update_parameters(&minus_params)?;
1060 let loss_minus = self.forward_loss(images, target)?;
1061
1062 self.task_head.update_parameters(&base_params)?;
1064
1065 let loss_delta = loss_plus - loss_minus;
1066 Ok(direction.mapv(|d| (loss_delta / (2.0 * PERTURBATION_SCALE)) * d))
1067 }
1068
1069 fn backward(&mut self, images: &Array4<f64>, target: &TaskTarget, _loss: f64) -> Result<()> {
1076 self.backbone_gradient = if self.backbone.parameters().is_empty() {
1077 None
1078 } else {
1079 Some(self.estimate_backbone_gradient(images, target)?)
1080 };
1081
1082 self.task_head_gradient = if self.task_head.parameters().is_empty() {
1083 None
1084 } else {
1085 Some(self.estimate_task_head_gradient(images, target)?)
1086 };
1087
1088 Ok(())
1089 }
1090
1091 fn update_parameters(&mut self, optimizer: &OptimizationMethod) -> Result<()> {
1096 let learning_rate = 0.01;
1101 let _ = optimizer;
1102
1103 if let Some(gradient) = self.backbone_gradient.take() {
1104 let updated = self.backbone.parameters() - &(&gradient * learning_rate);
1105 self.backbone.update_parameters(&updated)?;
1106 }
1107 if let Some(gradient) = self.task_head_gradient.take() {
1108 let updated = self.task_head.parameters() - &(&gradient * learning_rate);
1109 self.task_head.update_parameters(&updated)?;
1110 }
1111 Ok(())
1112 }
1113 fn evaluate_metrics(
1115 &self,
1116 output: &TaskOutput,
1117 target: &TaskTarget,
1118 ) -> Result<HashMap<String, f64>> {
1119 let mut metrics = HashMap::new();
1120 match (output, target) {
1121 (
1122 TaskOutput::Classification { probabilities, .. },
1123 TaskTarget::Classification { labels },
1124 ) => {
1125 let mut correct = 0;
1126 for (prob_row, &label) in probabilities.outer_iter().zip(labels.iter()) {
1127 let predicted = prob_row
1128 .iter()
1129 .enumerate()
1130 .max_by(|(_, a), (_, b)| {
1131 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
1132 })
1133 .map(|(i, _)| i)
1134 .unwrap_or(0);
1135 if predicted == label {
1136 correct += 1;
1137 }
1138 }
1139 metrics.insert("accuracy".to_string(), correct as f64 / labels.len() as f64);
1140 }
1141 _ => {}
1142 }
1143 Ok(metrics)
1144 }
1145 fn update_metrics(&mut self, features: &Array4<f64>, output: &TaskOutput) {
1147 self.metrics.quantum_metrics.entanglement_entropy =
1148 self.compute_entanglement_entropy(features);
1149 self.metrics.computational_metrics.inference_ms = 10.0;
1150 self.metrics.computational_metrics.throughput = 100.0;
1151 }
1152 fn compute_entanglement_entropy(&self, features: &Array4<f64>) -> f64 {
1154 let variance = features.var(0.0);
1155 variance.ln()
1156 }
1157 pub fn metrics(&self) -> &VisionMetrics {
1159 &self.metrics
1160 }
1161}
1162#[derive(Debug, Clone)]
1164pub struct DetectionHead {
1165 pub num_classes: usize,
1166 pub anchor_sizes: Vec<(usize, usize)>,
1167 pub iou_threshold: f64,
1168 pub parameters: Array1<f64>,
1169}
1170impl DetectionHead {
1171 fn new(
1172 num_classes: usize,
1173 anchor_sizes: Vec<(usize, usize)>,
1174 iou_threshold: f64,
1175 ) -> Result<Self> {
1176 Ok(Self {
1177 num_classes,
1178 anchor_sizes,
1179 iou_threshold,
1180 parameters: Array1::zeros(100),
1181 })
1182 }
1183}
1184#[derive(Debug, Clone)]
1186pub struct ConvolutionalConfig {
1187 pub num_filters: usize,
1189 pub kernel_size: usize,
1191 pub stride: usize,
1193 pub padding: usize,
1195 pub quantum_kernel: bool,
1197 pub circuit_depth: usize,
1199}
1200#[derive(Debug, Clone)]
1202pub enum TaskOutput {
1203 Classification {
1205 logits: Array2<f64>,
1206 probabilities: Array2<f64>,
1207 },
1208 Detection {
1210 boxes: Array3<f64>,
1211 scores: Array2<f64>,
1212 classes: Array2<usize>,
1213 },
1214 Segmentation {
1216 masks: Array4<f64>,
1217 class_scores: Array4<f64>,
1218 },
1219 Features {
1221 features: Array2<f64>,
1222 attention_maps: Option<Array4<f64>>,
1223 },
1224 Generation {
1226 images: Array4<f64>,
1227 latent_codes: Array2<f64>,
1228 },
1229}
1230#[derive(Debug, Clone)]
1232pub struct QuantumResNetBackbone {
1233 pub blocks: Vec<ResidualBlock>,
1235 pub skip_connections: bool,
1237 pub num_qubits: usize,
1239 pub parameters: Array1<f64>,
1241}
1242impl QuantumResNetBackbone {
1243 fn new(blocks: Vec<ResidualBlock>, skip_connections: bool, num_qubits: usize) -> Result<Self> {
1244 Ok(Self {
1245 blocks,
1246 skip_connections,
1247 num_qubits,
1248 parameters: Array1::zeros(1000),
1249 })
1250 }
1251}
1252#[derive(Debug, Clone)]
1253pub struct GenerationHead {
1254 pub latent_dim: usize,
1255 pub output_channels: usize,
1256 pub parameters: Array1<f64>,
1257}
1258impl GenerationHead {
1259 fn new(latent_dim: usize, output_channels: usize) -> Result<Self> {
1260 Ok(Self {
1261 latent_dim,
1262 output_channels,
1263 parameters: Array1::zeros(400),
1264 })
1265 }
1266}
1267#[derive(Debug, Clone)]
1269pub struct ClassificationHead {
1270 pub num_classes: usize,
1272 pub multi_label: bool,
1274 pub classifier: QuantumNeuralNetwork,
1276}
1277impl ClassificationHead {
1278 fn new(num_classes: usize, multi_label: bool) -> Result<Self> {
1279 let layers = vec![
1280 QNNLayerType::EncodingLayer { num_features: 512 },
1281 QNNLayerType::VariationalLayer { num_params: 256 },
1282 QNNLayerType::VariationalLayer {
1283 num_params: num_classes,
1284 },
1285 QNNLayerType::MeasurementLayer {
1286 measurement_basis: "computational".to_string(),
1287 },
1288 ];
1289 let classifier = QuantumNeuralNetwork::new(layers, 10, 512, num_classes)?;
1290 Ok(Self {
1291 num_classes,
1292 multi_label,
1293 classifier,
1294 })
1295 }
1296}
1297#[derive(Debug, Clone)]
1299pub enum VisionTaskConfig {
1300 Classification {
1302 num_classes: usize,
1303 multi_label: bool,
1304 },
1305 ObjectDetection {
1307 num_classes: usize,
1308 anchor_sizes: Vec<(usize, usize)>,
1309 iou_threshold: f64,
1310 },
1311 Segmentation {
1313 num_classes: usize,
1314 output_stride: usize,
1315 },
1316 InstanceSegmentation {
1318 num_classes: usize,
1319 mask_resolution: (usize, usize),
1320 },
1321 FeatureExtraction { feature_dim: usize, normalize: bool },
1323 Generation {
1325 latent_dim: usize,
1326 output_channels: usize,
1327 },
1328}
1329#[derive(Debug, Clone)]
1331pub struct QuantumCNNBackbone {
1332 pub conv_layers: Vec<QuantumConvolutionalNN>,
1334 pub pooling_type: PoolingType,
1336 pub num_qubits: usize,
1338 pub parameters: Array1<f64>,
1340}
1341impl QuantumCNNBackbone {
1342 fn new(
1343 conv_configs: Vec<ConvolutionalConfig>,
1344 pooling_type: PoolingType,
1345 num_qubits: usize,
1346 ) -> Result<Self> {
1347 let mut conv_layers = Vec::new();
1348 for config in conv_configs {
1349 let qcnn = QuantumConvolutionalNN::new(vec![], num_qubits, 224, config.num_filters)?;
1350 conv_layers.push(qcnn);
1351 }
1352 Ok(Self {
1353 conv_layers,
1354 pooling_type,
1355 num_qubits,
1356 parameters: Array1::zeros(100),
1357 })
1358 }
1359}
1360#[derive(Debug, Clone)]
1362pub struct TrainingHistory {
1363 pub epochs: Vec<usize>,
1364 pub train_losses: Vec<f64>,
1365 pub val_losses: Vec<f64>,
1366 pub metrics: Vec<HashMap<String, f64>>,
1367}
1368impl TrainingHistory {
1369 fn new() -> Self {
1370 Self {
1371 epochs: Vec::new(),
1372 train_losses: Vec::new(),
1373 val_losses: Vec::new(),
1374 metrics: Vec::new(),
1375 }
1376 }
1377 fn add_epoch(
1378 &mut self,
1379 epoch: usize,
1380 train_loss: f64,
1381 val_loss: f64,
1382 metrics: HashMap<String, f64>,
1383 ) {
1384 self.epochs.push(epoch);
1385 self.train_losses.push(train_loss);
1386 self.val_losses.push(val_loss);
1387 self.metrics.push(metrics);
1388 }
1389}
1390#[derive(Debug, Clone)]
1392pub struct ResidualBlock {
1393 pub channels: usize,
1395 pub kernel_size: usize,
1397 pub stride: usize,
1399 pub quantum_conv: bool,
1401}
1402#[derive(Debug, Clone)]
1404pub struct QuantumMetrics {
1405 pub circuit_depth: usize,
1407 pub entanglement_entropy: f64,
1409 pub quantum_advantage: f64,
1411 pub coherence_utilization: f64,
1413}
1414#[derive(Debug, Clone)]
1416pub struct QuantumFeatureExtractor {
1417 pub feature_dim: usize,
1419 pub feature_circuit_params: Vec<Vec<f64>>,
1421 pub transform_network: QuantumNeuralNetwork,
1423 pub attention: QuantumSpatialAttention,
1425}
1426impl QuantumFeatureExtractor {
1427 pub fn new(feature_dim: usize, num_qubits: usize) -> Result<Self> {
1429 let mut feature_circuit_params = Vec::new();
1430 for _ in 0..5 {
1431 let mut params = Vec::new();
1432 for _ in 0..num_qubits {
1433 params.push(1.0);
1434 params.push(0.0);
1435 }
1436 for _ in 0..num_qubits - 1 {
1437 params.push(2.0);
1438 }
1439 feature_circuit_params.push(params);
1440 }
1441 let layers = vec![
1442 QNNLayerType::EncodingLayer { num_features: 256 },
1443 QNNLayerType::VariationalLayer {
1444 num_params: feature_dim,
1445 },
1446 QNNLayerType::MeasurementLayer {
1447 measurement_basis: "computational".to_string(),
1448 },
1449 ];
1450 let transform_network = QuantumNeuralNetwork::new(layers, num_qubits, 256, feature_dim)?;
1451 let attention = QuantumSpatialAttention::new(8, 64, num_qubits)?;
1452 Ok(Self {
1453 feature_dim,
1454 feature_circuit_params,
1455 transform_network,
1456 attention,
1457 })
1458 }
1459 pub fn extract(&self, features: &Array4<f64>) -> Result<Array4<f64>> {
1461 let attended = self.attention.apply(features)?;
1462 Ok(attended)
1463 }
1464}
1465
1466#[cfg(test)]
1467mod backward_regression_tests {
1468 use super::*;
1469
1470 #[derive(Debug, Clone)]
1474 struct ScalingBackbone {
1475 params: Array1<f64>,
1476 }
1477 impl VisionModel for ScalingBackbone {
1478 fn forward(&self, input: &Array4<f64>) -> Result<Array4<f64>> {
1479 let scale = self.params[0];
1480 Ok(input.mapv(|x| x * scale))
1481 }
1482 fn parameters(&self) -> &Array1<f64> {
1483 &self.params
1484 }
1485 fn update_parameters(&mut self, params: &Array1<f64>) -> Result<()> {
1486 self.params = params.clone();
1487 Ok(())
1488 }
1489 fn num_parameters(&self) -> usize {
1490 self.params.len()
1491 }
1492 fn clone_box(&self) -> Box<dyn VisionModel> {
1493 Box::new(self.clone())
1494 }
1495 }
1496
1497 #[derive(Debug, Clone)]
1500 struct LinearClassificationHead {
1501 params: Array1<f64>,
1502 num_classes: usize,
1503 }
1504 impl TaskHead for LinearClassificationHead {
1505 fn forward(&self, features: &Array4<f64>) -> Result<TaskOutput> {
1506 let (batch, _, _, _) = features.dim();
1507 let weight = self.params[0];
1508 let mut logits = Array2::<f64>::zeros((batch, self.num_classes));
1509 for b in 0..batch {
1510 let mean_feature = features.slice(s![b, .., .., ..]).mean().unwrap_or(0.0);
1511 for c in 0..self.num_classes {
1512 logits[[b, c]] = mean_feature * weight * (c as f64);
1521 }
1522 }
1523 Ok(TaskOutput::Classification {
1524 logits: logits.clone(),
1525 probabilities: logits,
1526 })
1527 }
1528 fn parameters(&self) -> &Array1<f64> {
1529 &self.params
1530 }
1531 fn update_parameters(&mut self, params: &Array1<f64>) -> Result<()> {
1532 self.params = params.clone();
1533 Ok(())
1534 }
1535 fn clone_box(&self) -> Box<dyn TaskHead> {
1536 Box::new(self.clone())
1537 }
1538 }
1539
1540 fn build_test_pipeline(backbone_param: f64, head_param: f64) -> QuantumVisionPipeline {
1541 let preprocessing = PreprocessingConfig {
1542 image_size: (2, 2),
1543 normalize: false,
1544 mean: Vec::new(),
1545 std: Vec::new(),
1546 augmentation: AugmentationConfig {
1547 horizontal_flip: false,
1548 rotation_range: 0.0,
1549 zoom_range: (1.0, 1.0),
1550 brightness_range: (1.0, 1.0),
1551 quantum_noise: false,
1552 },
1553 color_space: ColorSpace::Grayscale,
1554 };
1555 let preprocessor = ImagePreprocessor::new(preprocessing);
1556 let encoder = QuantumImageEncoder::new(ImageEncodingMethod::AmplitudeEncoding, 2)
1557 .expect("encoder should construct");
1558 let feature_extractor =
1559 QuantumFeatureExtractor::new(4, 2).expect("feature extractor should construct");
1560
1561 QuantumVisionPipeline {
1562 config: QuantumVisionConfig::default(),
1563 encoder,
1564 backbone: Box::new(ScalingBackbone {
1565 params: Array1::from_vec(vec![backbone_param]),
1566 }),
1567 task_head: Box::new(LinearClassificationHead {
1568 params: Array1::from_vec(vec![head_param]),
1569 num_classes: 2,
1570 }),
1571 feature_extractor,
1572 preprocessor,
1573 metrics: VisionMetrics::new(),
1574 backbone_gradient: None,
1575 task_head_gradient: None,
1576 }
1577 }
1578
1579 #[test]
1583 fn backward_and_update_parameters_actually_change_weights() {
1584 let mut pipeline = build_test_pipeline(2.0, 1.0);
1585 let images = Array4::<f64>::from_elem((1, 1, 2, 2), 0.7);
1586 let target = TaskTarget::Classification { labels: vec![1] };
1587
1588 let backbone_before = pipeline.backbone.parameters().clone();
1589 let head_before = pipeline.task_head.parameters().clone();
1590
1591 pipeline
1592 .backward(&images, &target, 0.0)
1593 .expect("backward should succeed");
1594 assert!(pipeline.backbone_gradient.is_some());
1595 assert!(pipeline.task_head_gradient.is_some());
1596
1597 pipeline
1598 .update_parameters(&OptimizationMethod::GradientDescent)
1599 .expect("update_parameters should succeed");
1600
1601 let backbone_after = pipeline.backbone.parameters().clone();
1602 let head_after = pipeline.task_head.parameters().clone();
1603
1604 assert!(
1605 (backbone_before[0] - backbone_after[0]).abs() > 1e-9,
1606 "backbone parameter should have changed: {} -> {}",
1607 backbone_before[0],
1608 backbone_after[0]
1609 );
1610 assert!(
1611 (head_before[0] - head_after[0]).abs() > 1e-9,
1612 "task head parameter should have changed: {} -> {}",
1613 head_before[0],
1614 head_after[0]
1615 );
1616
1617 assert!(pipeline.backbone_gradient.is_none());
1619 assert!(pipeline.task_head_gradient.is_none());
1620 }
1621
1622 #[test]
1627 fn train_reduces_loss_over_epochs() {
1628 let mut pipeline = build_test_pipeline(0.1, 0.1);
1629 let images = Array4::<f64>::from_elem((1, 1, 2, 2), 0.9);
1630 let target = TaskTarget::Classification { labels: vec![1] };
1631 let train_data = vec![(images.clone(), target.clone())];
1632 let val_data = vec![(images, target)];
1633
1634 let initial_output = pipeline.forward(&train_data[0].0).expect("forward ok");
1635 let initial_loss = QuantumVisionPipeline::compute_loss(&initial_output, &train_data[0].1)
1636 .expect("loss ok");
1637
1638 let history = pipeline
1639 .train(
1640 &train_data,
1641 &val_data,
1642 20,
1643 OptimizationMethod::GradientDescent,
1644 )
1645 .expect("training should succeed");
1646
1647 let final_loss = *history.train_losses.last().expect("has losses");
1648 assert!(
1649 final_loss < initial_loss,
1650 "expected training loss to decrease: initial={initial_loss}, final={final_loss}"
1651 );
1652 }
1653}