Skip to main content

quantrs2_ml/
qsvm.rs

1//! Quantum Support Vector Machine (QSVM) implementation
2//!
3//! This module implements quantum-enhanced support vector machines for
4//! classification tasks using quantum feature maps and kernel methods.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::Complex64;
8use std::collections::HashMap;
9use std::f64::consts::PI;
10
11/// Quantum feature map types
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum FeatureMapType {
14    /// Pauli-Z feature map: exp(i·φ(x)·Z)
15    ZFeatureMap,
16    /// Pauli-ZZ feature map: exp(i·φ(x)·ZZ)
17    ZZFeatureMap,
18    /// Pauli feature map (general)
19    PauliFeatureMap,
20    /// Custom angle encoding
21    AngleEncoding,
22    /// Amplitude encoding
23    AmplitudeEncoding,
24}
25
26/// Parameters for QSVM
27#[derive(Debug, Clone)]
28pub struct QSVMParams {
29    /// Type of quantum feature map
30    pub feature_map: FeatureMapType,
31    /// Number of repetitions of the feature map circuit
32    pub reps: usize,
33    /// Regularization parameter (also accessible as c_parameter for compatibility)
34    pub c: f64,
35    /// Tolerance for convergence
36    pub tolerance: f64,
37    /// Number of qubits
38    pub num_qubits: usize,
39    /// Circuit depth
40    pub depth: usize,
41    /// Gamma parameter for RBF-like kernels
42    pub gamma: Option<f64>,
43    /// Regularization parameter (alias for c)
44    pub regularization: f64,
45    /// Maximum iterations for optimization
46    pub max_iterations: usize,
47    /// Random seed for reproducibility
48    pub seed: Option<u64>,
49}
50
51impl Default for QSVMParams {
52    fn default() -> Self {
53        Self {
54            feature_map: FeatureMapType::ZZFeatureMap,
55            reps: 2,
56            c: 1.0,
57            tolerance: 1e-3,
58            num_qubits: 4,
59            depth: 2,
60            gamma: None,
61            regularization: 1.0,
62            max_iterations: 1000,
63            seed: None,
64        }
65    }
66}
67
68impl QSVMParams {
69    /// Get c_parameter (alias for c)
70    pub fn c_parameter(&self) -> f64 {
71        self.c
72    }
73
74    /// Set c_parameter (updates both c and regularization)
75    pub fn set_c_parameter(&mut self, value: f64) {
76        self.c = value;
77        self.regularization = value;
78    }
79}
80
81/// Quantum kernel computation
82pub struct QuantumKernel {
83    feature_map: FeatureMapType,
84    reps: usize,
85}
86
87impl QuantumKernel {
88    /// Create a new quantum kernel
89    pub fn new(feature_map: FeatureMapType, reps: usize) -> Self {
90        Self { feature_map, reps }
91    }
92
93    /// Compute the quantum kernel between two data points
94    pub fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
95        match self.feature_map {
96            FeatureMapType::ZFeatureMap => self.z_feature_map_kernel(x1, x2),
97            FeatureMapType::ZZFeatureMap => self.zz_feature_map_kernel(x1, x2),
98            FeatureMapType::PauliFeatureMap => self.zz_feature_map_kernel(x1, x2), // Use ZZ as fallback
99            FeatureMapType::AngleEncoding => self.angle_encoding_kernel(x1, x2),
100            FeatureMapType::AmplitudeEncoding => self.amplitude_encoding_kernel(x1, x2),
101        }
102    }
103
104    /// Z feature map kernel
105    fn z_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
106        let n = x1.len();
107        let mut kernel_val = 1.0;
108
109        for _ in 0..self.reps {
110            for i in 0..n {
111                let phase_diff = (x1[i] - x2[i]) * PI;
112                kernel_val *= phase_diff.cos();
113            }
114        }
115
116        kernel_val
117    }
118
119    /// ZZ feature map kernel (includes entanglement)
120    fn zz_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
121        let n = x1.len();
122        let mut kernel_val = 1.0;
123
124        for rep in 0..self.reps {
125            // Single-qubit rotations
126            for i in 0..n {
127                let phase_diff = (x1[i] - x2[i]) * PI * (rep + 1) as f64;
128                kernel_val *= phase_diff.cos();
129            }
130
131            // Two-qubit interactions
132            for i in 0..n - 1 {
133                let interaction = (x1[i] - x2[i]) * (x1[i + 1] - x2[i + 1]) * PI;
134                kernel_val *= interaction.cos();
135            }
136        }
137
138        kernel_val
139    }
140
141    /// Angle encoding kernel
142    fn angle_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
143        let mut sum = 0.0;
144        for i in 0..x1.len() {
145            sum += (x1[i] - x2[i]).powi(2);
146        }
147        (-sum / 2.0).exp()
148    }
149
150    /// Amplitude encoding kernel
151    fn amplitude_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
152        // Normalize vectors
153        let norm1 = x1.dot(x1).sqrt();
154        let norm2 = x2.dot(x2).sqrt();
155
156        if norm1 < 1e-10 || norm2 < 1e-10 {
157            return 0.0;
158        }
159
160        let x1_norm = x1 / norm1;
161        let x2_norm = x2 / norm2;
162
163        // Inner product gives fidelity
164        x1_norm.dot(&x2_norm).powi(2)
165    }
166
167    /// Compute the full kernel matrix for a dataset
168    pub fn compute_kernel_matrix(&self, data: &Array2<f64>) -> Array2<f64> {
169        let n_samples = data.nrows();
170        let mut kernel_matrix = Array2::zeros((n_samples, n_samples));
171
172        for i in 0..n_samples {
173            for j in i..n_samples {
174                let kernel_val = self.compute(&data.row(i).to_owned(), &data.row(j).to_owned());
175                kernel_matrix[[i, j]] = kernel_val;
176                kernel_matrix[[j, i]] = kernel_val; // Symmetric
177            }
178        }
179
180        kernel_matrix
181    }
182}
183
184/// Quantum Support Vector Machine classifier
185pub struct QSVM {
186    params: QSVMParams,
187    kernel: QuantumKernel,
188    support_vectors: Option<Array2<f64>>,
189    support_labels: Option<Array1<i32>>,
190    alphas: Option<Array1<f64>>,
191    bias: f64,
192    kernel_matrix_cache: HashMap<(usize, usize), f64>,
193}
194
195impl QSVM {
196    /// Create a new QSVM classifier
197    pub fn new(params: QSVMParams) -> Self {
198        let kernel = QuantumKernel::new(params.feature_map, params.reps);
199        Self {
200            params,
201            kernel,
202            support_vectors: None,
203            support_labels: None,
204            alphas: None,
205            bias: 0.0,
206            kernel_matrix_cache: HashMap::new(),
207        }
208    }
209
210    /// Train the QSVM using SMO (Sequential Minimal Optimization)
211    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<i32>) -> Result<(), String> {
212        let n_samples = x.nrows();
213
214        // Validate labels are binary (-1 or 1)
215        for &label in y.iter() {
216            if label != -1 && label != 1 {
217                return Err("QSVM requires binary labels: -1 or 1".to_string());
218            }
219        }
220
221        // Initialize alphas
222        let mut alphas = Array1::zeros(n_samples);
223
224        // Precompute kernel matrix
225        let kernel_matrix = self.kernel.compute_kernel_matrix(x);
226
227        // SMO optimization
228        let mut converged = false;
229        let mut iter = 0;
230
231        while !converged && iter < self.params.max_iterations {
232            let old_alphas = alphas.clone();
233
234            // Select working set (simplified SMO)
235            for i in 0..n_samples {
236                // Compute error for i
237                let ei = self.compute_error(&kernel_matrix, &alphas, y, i);
238
239                // Check KKT conditions
240                if !self.check_kkt(alphas[i], y[i], ei) {
241                    // Select j != i
242                    let j = self.select_second_alpha(i, n_samples, &kernel_matrix, &alphas, y);
243                    if i == j {
244                        continue;
245                    }
246
247                    // Compute error for j
248                    let ej = self.compute_error(&kernel_matrix, &alphas, y, j);
249
250                    // Save old alphas
251                    let alpha_i_old = alphas[i];
252                    let alpha_j_old = alphas[j];
253
254                    // Compute bounds
255                    let (l, h) = self.compute_bounds(y[i], y[j], alphas[i], alphas[j]);
256
257                    if (h - l).abs() < 1e-10 {
258                        continue;
259                    }
260
261                    // Compute eta
262                    let eta =
263                        kernel_matrix[[i, i]] + kernel_matrix[[j, j]] - 2.0 * kernel_matrix[[i, j]];
264
265                    if eta <= 0.0 {
266                        continue;
267                    }
268
269                    // Update alpha_j
270                    alphas[j] += y[j] as f64 * (ei - ej) / eta;
271                    alphas[j] = alphas[j].clamp(l, h);
272
273                    if (alphas[j] - alpha_j_old).abs() < 1e-5 {
274                        continue;
275                    }
276
277                    // Update alpha_i
278                    alphas[i] += y[i] as f64 * y[j] as f64 * (alpha_j_old - alphas[j]);
279                }
280            }
281
282            // Check convergence
283            let alpha_change: f64 = (&alphas - &old_alphas).mapv(|a| a.abs()).sum();
284            converged = alpha_change < self.params.tolerance;
285            iter += 1;
286        }
287
288        // Extract support vectors
289        let mut support_indices = Vec::new();
290        let mut support_alphas = Vec::new();
291
292        for i in 0..n_samples {
293            if alphas[i] > 1e-5 {
294                support_indices.push(i);
295                support_alphas.push(alphas[i]);
296            }
297        }
298
299        if support_indices.is_empty() {
300            return Err("No support vectors found".to_string());
301        }
302
303        // Store support vectors
304        let n_support = support_indices.len();
305        let n_features = x.ncols();
306        let mut support_vectors = Array2::zeros((n_support, n_features));
307        let mut support_labels = Array1::zeros(n_support);
308
309        for (idx, &i) in support_indices.iter().enumerate() {
310            support_vectors.row_mut(idx).assign(&x.row(i));
311            support_labels[idx] = y[i];
312        }
313
314        self.support_vectors = Some(support_vectors);
315        self.support_labels = Some(support_labels);
316        self.alphas = Some(Array1::from_vec(support_alphas));
317
318        // Compute bias
319        self.compute_bias(&kernel_matrix, &alphas, y, &support_indices);
320
321        Ok(())
322    }
323
324    /// Compute error for a given sample
325    fn compute_error(
326        &self,
327        kernel_matrix: &Array2<f64>,
328        alphas: &Array1<f64>,
329        y: &Array1<i32>,
330        i: usize,
331    ) -> f64 {
332        let mut sum = self.bias;
333        for j in 0..alphas.len() {
334            if alphas[j] > 0.0 {
335                sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
336            }
337        }
338        sum - y[i] as f64
339    }
340
341    /// Check KKT conditions
342    fn check_kkt(&self, alpha: f64, y: i32, error: f64) -> bool {
343        let y_error = y as f64 * error;
344
345        if alpha < 1e-5 {
346            y_error >= -self.params.tolerance
347        } else if alpha > self.params.c - 1e-5 {
348            y_error <= self.params.tolerance
349        } else {
350            (y_error).abs() <= self.params.tolerance
351        }
352    }
353
354    /// Select second alpha using maximum step heuristic
355    fn select_second_alpha(
356        &self,
357        i: usize,
358        n_samples: usize,
359        kernel_matrix: &Array2<f64>,
360        alphas: &Array1<f64>,
361        y: &Array1<i32>,
362    ) -> usize {
363        let ei = self.compute_error(kernel_matrix, alphas, y, i);
364        let mut max_step = 0.0;
365        let mut best_j = i;
366
367        for j in 0..n_samples {
368            if i == j {
369                continue;
370            }
371
372            let ej = self.compute_error(kernel_matrix, alphas, y, j);
373            let step = (ei - ej).abs();
374
375            if step > max_step {
376                max_step = step;
377                best_j = j;
378            }
379        }
380
381        best_j
382    }
383
384    /// Compute bounds for alpha updates
385    fn compute_bounds(&self, yi: i32, yj: i32, alpha_i: f64, alpha_j: f64) -> (f64, f64) {
386        if yi != yj {
387            let l = (alpha_j - alpha_i).max(0.0);
388            let h = (self.params.c + alpha_j - alpha_i).min(self.params.c);
389            (l, h)
390        } else {
391            let l = (alpha_i + alpha_j - self.params.c).max(0.0);
392            let h = (alpha_i + alpha_j).min(self.params.c);
393            (l, h)
394        }
395    }
396
397    /// Compute bias term
398    fn compute_bias(
399        &mut self,
400        kernel_matrix: &Array2<f64>,
401        alphas: &Array1<f64>,
402        y: &Array1<i32>,
403        support_indices: &[usize],
404    ) {
405        let mut bias_sum = 0.0;
406        let mut count = 0;
407
408        for &i in support_indices {
409            if alphas[i] > 1e-5 && alphas[i] < self.params.c - 1e-5 {
410                let mut sum = 0.0;
411                for j in 0..alphas.len() {
412                    if alphas[j] > 1e-5 {
413                        sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
414                    }
415                }
416                bias_sum += y[i] as f64 - sum;
417                count += 1;
418            }
419        }
420
421        self.bias = if count > 0 {
422            bias_sum / count as f64
423        } else {
424            0.0
425        };
426    }
427
428    /// Predict labels for new data
429    pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<i32>, String> {
430        let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
431        let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
432        let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
433
434        let n_samples = x.nrows();
435        let mut predictions = Array1::zeros(n_samples);
436
437        for i in 0..n_samples {
438            let mut score = self.bias;
439
440            for (j, sv) in support_vectors.rows().into_iter().enumerate() {
441                let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
442                score += alphas[j] * support_labels[j] as f64 * kernel_val;
443            }
444
445            predictions[i] = if score >= 0.0 { 1 } else { -1 };
446        }
447
448        Ok(predictions)
449    }
450
451    /// Get decision function values
452    pub fn decision_function(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
453        let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
454        let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
455        let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
456
457        let n_samples = x.nrows();
458        let mut scores = Array1::zeros(n_samples);
459
460        for i in 0..n_samples {
461            let mut score = self.bias;
462
463            for (j, sv) in support_vectors.rows().into_iter().enumerate() {
464                let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
465                score += alphas[j] * support_labels[j] as f64 * kernel_val;
466            }
467
468            scores[i] = score;
469        }
470
471        Ok(scores)
472    }
473
474    /// Get the number of support vectors
475    pub fn n_support_vectors(&self) -> usize {
476        self.support_vectors
477            .as_ref()
478            .map(|sv| sv.nrows())
479            .unwrap_or(0)
480    }
481}
482
483/// Quantum kernel ridge regression for comparison
484pub struct QuantumKernelRidge {
485    kernel: QuantumKernel,
486    alpha: f64,
487    training_data: Option<Array2<f64>>,
488    coefficients: Option<Array1<f64>>,
489}
490
491impl QuantumKernelRidge {
492    /// Create new quantum kernel ridge regression
493    pub fn new(feature_map: FeatureMapType, reps: usize, alpha: f64) -> Self {
494        Self {
495            kernel: QuantumKernel::new(feature_map, reps),
496            alpha,
497            training_data: None,
498            coefficients: None,
499        }
500    }
501
502    /// Fit the model
503    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<(), String> {
504        // Compute kernel matrix
505        let mut k = self.kernel.compute_kernel_matrix(x);
506
507        // Add regularization to diagonal
508        let n = k.nrows();
509        for i in 0..n {
510            k[[i, i]] += self.alpha;
511        }
512
513        // Solve K * coefficients = y
514        // Using simple matrix inversion (in practice, use Cholesky decomposition)
515        match Self::solve_linear_system(&k, y) {
516            Ok(coeffs) => {
517                self.training_data = Some(x.clone());
518                self.coefficients = Some(coeffs);
519                Ok(())
520            }
521            Err(e) => Err(format!("Failed to solve linear system: {}", e)),
522        }
523    }
524
525    /// Solve the linear system `A x = b` using Cholesky decomposition (LL^T factorisation).
526    ///
527    /// `A` is assumed to be symmetric positive-definite (the kernel matrix + regularisation
528    /// diagonal satisfies this by construction).
529    ///
530    /// Algorithm:
531    /// 1. Compute the lower-triangular Cholesky factor `L` such that `A = L Lᵀ`.
532    /// 2. Forward-substitute: solve `L y = b`.
533    /// 3. Backward-substitute: solve `Lᵀ x = y`.
534    fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
535        let n = a.nrows();
536        if n == 0 {
537            return Err("Empty matrix".to_string());
538        }
539        if n != a.ncols() {
540            return Err(format!("Matrix is not square: {}×{}", n, a.ncols()));
541        }
542        if n != b.len() {
543            return Err(format!(
544                "Dimension mismatch: matrix is {}×{} but rhs has {} elements",
545                n,
546                n,
547                b.len()
548            ));
549        }
550
551        // ── Step 1: Cholesky factorisation A = L Lᵀ ─────────────────────────
552        // L is stored as a flat row-major lower triangular matrix.
553        let mut l = Array2::<f64>::zeros((n, n));
554
555        for i in 0..n {
556            // Diagonal element
557            let mut sum_sq = a[[i, i]];
558            for k in 0..i {
559                sum_sq -= l[[i, k]] * l[[i, k]];
560            }
561            if sum_sq <= 0.0 {
562                // Matrix is not positive-definite; fall back to Gaussian elimination.
563                return Self::solve_gaussian_elimination(a, b);
564            }
565            l[[i, i]] = sum_sq.sqrt();
566
567            // Sub-diagonal elements for column i
568            for j in (i + 1)..n {
569                let mut sum_prod = a[[j, i]];
570                for k in 0..i {
571                    sum_prod -= l[[j, k]] * l[[i, k]];
572                }
573                l[[j, i]] = sum_prod / l[[i, i]];
574            }
575        }
576
577        // ── Step 2: Forward substitution — solve L y = b ─────────────────────
578        let mut y = Array1::<f64>::zeros(n);
579        for i in 0..n {
580            let mut s = b[i];
581            for k in 0..i {
582                s -= l[[i, k]] * y[k];
583            }
584            let diag = l[[i, i]];
585            if diag.abs() < 1e-14 {
586                return Err(format!("Singular Cholesky factor at row {i}"));
587            }
588            y[i] = s / diag;
589        }
590
591        // ── Step 3: Backward substitution — solve Lᵀ x = y ──────────────────
592        let mut x = Array1::<f64>::zeros(n);
593        for i in (0..n).rev() {
594            let mut s = y[i];
595            for k in (i + 1)..n {
596                s -= l[[k, i]] * x[k]; // l[[k,i]] = Lᵀ[[i,k]]
597            }
598            let diag = l[[i, i]];
599            if diag.abs() < 1e-14 {
600                return Err(format!("Singular Cholesky factor at column {i}"));
601            }
602            x[i] = s / diag;
603        }
604
605        Ok(x)
606    }
607
608    /// Fallback linear solver using Gaussian elimination with partial pivoting.
609    ///
610    /// Used when the matrix is not numerically positive-definite.
611    fn solve_gaussian_elimination(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
612        let n = a.nrows();
613
614        // Build augmented matrix [A | b]
615        let mut aug: Vec<Vec<f64>> = (0..n)
616            .map(|i| {
617                let mut row: Vec<f64> = (0..n).map(|j| a[[i, j]]).collect();
618                row.push(b[i]);
619                row
620            })
621            .collect();
622
623        // Forward elimination with partial pivoting
624        for col in 0..n {
625            // Find pivot
626            let pivot_row = (col..n)
627                .max_by(|&r1, &r2| {
628                    aug[r1][col]
629                        .abs()
630                        .partial_cmp(&aug[r2][col].abs())
631                        .unwrap_or(std::cmp::Ordering::Equal)
632                })
633                .ok_or("Empty column range during elimination")?;
634
635            aug.swap(col, pivot_row);
636
637            let pivot = aug[col][col];
638            if pivot.abs() < 1e-14 {
639                return Err(format!("Singular or near-singular matrix at column {col}"));
640            }
641
642            // Eliminate column entries below the pivot
643            for row in (col + 1)..n {
644                let factor = aug[row][col] / pivot;
645                for j in col..=n {
646                    let val = aug[col][j];
647                    aug[row][j] -= factor * val;
648                }
649            }
650        }
651
652        // Back-substitution
653        let mut x = vec![0.0_f64; n];
654        for i in (0..n).rev() {
655            let mut s = aug[i][n];
656            for j in (i + 1)..n {
657                s -= aug[i][j] * x[j];
658            }
659            let diag = aug[i][i];
660            if diag.abs() < 1e-14 {
661                return Err(format!(
662                    "Singular matrix during back-substitution at row {i}"
663                ));
664            }
665            x[i] = s / diag;
666        }
667
668        Ok(Array1::from_vec(x))
669    }
670
671    /// Predict values for new data
672    pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
673        let training_data = self.training_data.as_ref().ok_or("Model not trained")?;
674        let coefficients = self.coefficients.as_ref().ok_or("Model not trained")?;
675
676        let n_samples = x.nrows();
677        let mut predictions = Array1::zeros(n_samples);
678
679        for i in 0..n_samples {
680            let mut sum = 0.0;
681            for (j, coeff) in coefficients.iter().enumerate() {
682                let kernel_val = self
683                    .kernel
684                    .compute(&x.row(i).to_owned(), &training_data.row(j).to_owned());
685                sum += coeff * kernel_val;
686            }
687            predictions[i] = sum;
688        }
689
690        Ok(predictions)
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use scirs2_core::ndarray::array;
698
699    #[test]
700    fn test_quantum_kernel_computation() {
701        let kernel = QuantumKernel::new(FeatureMapType::ZFeatureMap, 1);
702
703        let x1 = array![0.5, 0.5];
704        let x2 = array![0.5, 0.5];
705
706        let kernel_val = kernel.compute(&x1, &x2);
707        assert!((kernel_val - 1.0).abs() < 1e-6); // Same vectors should give 1
708
709        let x3 = array![0.0, 1.0];
710        let kernel_val2 = kernel.compute(&x1, &x3);
711        assert!(kernel_val2 < 1.0); // Different vectors should give < 1
712    }
713
714    #[test]
715    fn test_qsvm_basic() {
716        // Create simple linearly separable dataset
717        let x = array![[0.0, 0.0], [0.1, 0.1], [1.0, 1.0], [0.9, 0.9],];
718
719        let y = array![-1, -1, 1, 1];
720
721        let params = QSVMParams::default();
722        let mut qsvm = QSVM::new(params);
723
724        // Train
725        qsvm.fit(&x, &y).expect("fit should succeed");
726
727        // Check that we have support vectors
728        assert!(qsvm.n_support_vectors() > 0);
729
730        // Predict on training data
731        let predictions = qsvm.predict(&x).expect("predict should succeed");
732
733        // Should classify training data correctly
734        for i in 0..y.len() {
735            assert_eq!(predictions[i], y[i]);
736        }
737    }
738}