Skip to main content

sklears_multioutput/
sequence.rs

1//! Sequence and structured prediction models
2//!
3//! This module provides algorithms for sequence labeling and structured prediction tasks.
4//! It includes Hidden Markov Models (HMM), Structured Perceptron, and Maximum Entropy
5//! Markov Models (MEMM) for handling sequential and structured data.
6#![allow(non_snake_case)] // Standard ML notation: X for feature matrices, K for kernels
7
8// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
9use scirs2_core::ndarray::{s, Array1, Array2, Array3, ArrayView1};
10use scirs2_core::random::thread_rng;
11use scirs2_core::random::RandNormal;
12use sklears_core::{
13    error::{Result as SklResult, SklearsError},
14    traits::{Estimator, Fit, Predict, Untrained},
15    types::Float,
16};
17use std::collections::{HashMap, HashSet};
18
19/// Structured Perceptron for sequence labeling
20///
21/// The structured perceptron is an extension of the perceptron algorithm for
22/// structured prediction problems. It can handle variable-length sequences and
23/// complex output structures by using structured features and losses.
24///
25/// The structured perceptron extends the classic perceptron to handle structured
26/// outputs by using a feature function that maps input-output pairs to feature
27/// vectors and a loss function that measures the quality of predictions.
28///
29/// # Examples
30///
31/// ```
32/// use sklears_core::traits::{Predict, Fit};
33/// use sklears_multioutput::StructuredPerceptron;
34/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
35/// use scirs2_core::ndarray::array;
36///
37/// let X = array![[[1.0, 2.0], [2.0, 3.0]]]; // One sequence
38/// let y = array![[0, 1]]; // Label sequence
39///
40/// let perceptron = StructuredPerceptron::new().max_iterations(50);
41/// let trained_perceptron = perceptron.fit(&X, &y).unwrap();
42/// let predictions = trained_perceptron.predict(&X).unwrap();
43/// ```
44#[derive(Debug, Clone)]
45pub struct StructuredPerceptron<State = Untrained> {
46    max_iterations: usize,
47    learning_rate: Float,
48    random_state: Option<u64>,
49    state: State,
50}
51
52/// Trained state for Structured Perceptron
53#[derive(Debug, Clone)]
54pub struct StructuredPerceptronTrained {
55    weights: Array1<Float>,
56    n_features: usize,
57    n_classes: usize,
58}
59
60impl Default for StructuredPerceptron<Untrained> {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl StructuredPerceptron<Untrained> {
67    /// Create a new Structured Perceptron
68    pub fn new() -> Self {
69        Self {
70            max_iterations: 100,
71            learning_rate: 1.0,
72            random_state: None,
73            state: Untrained,
74        }
75    }
76
77    /// Set the maximum number of iterations
78    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
79        self.max_iterations = max_iterations;
80        self
81    }
82
83    /// Set the learning rate
84    pub fn learning_rate(mut self, learning_rate: Float) -> Self {
85        self.learning_rate = learning_rate;
86        self
87    }
88
89    /// Set the random state for reproducible results
90    pub fn random_state(mut self, random_state: u64) -> Self {
91        self.random_state = Some(random_state);
92        self
93    }
94}
95
96impl Estimator for StructuredPerceptron<Untrained> {
97    type Config = ();
98    type Error = SklearsError;
99    type Float = Float;
100
101    fn config(&self) -> &Self::Config {
102        &()
103    }
104}
105
106impl Fit<Array3<Float>, Array2<i32>> for StructuredPerceptron<Untrained> {
107    type Fitted = StructuredPerceptron<StructuredPerceptronTrained>;
108
109    fn fit(self, X: &Array3<Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
110        let (n_sequences, max_seq_len, n_features) = X.dim();
111
112        if n_sequences != y.nrows() {
113            return Err(SklearsError::InvalidInput(
114                "X and y must have the same number of sequences".to_string(),
115            ));
116        }
117
118        if y.ncols() != max_seq_len {
119            return Err(SklearsError::InvalidInput(
120                "y sequence length must match X sequence length".to_string(),
121            ));
122        }
123
124        let n_classes = y.iter().max().unwrap_or(&0) + 1;
125        let feature_dim = n_features * n_classes as usize + n_classes as usize * n_classes as usize;
126        let mut weights = Array1::<Float>::zeros(feature_dim);
127
128        let _rng = thread_rng();
129
130        for _iteration in 0..self.max_iterations {
131            let mut updated = false;
132
133            for seq_idx in 0..n_sequences {
134                let sequence = X.slice(s![seq_idx, .., ..]);
135                let true_labels = y.row(seq_idx);
136
137                // Simple prediction: take argmax for each position
138                let mut predicted_labels = Array1::<Float>::zeros(max_seq_len);
139
140                for pos in 0..max_seq_len {
141                    let features = sequence.slice(s![pos, ..]);
142                    let mut best_score = Float::NEG_INFINITY;
143                    let mut best_label = 0;
144
145                    for label in 0..n_classes {
146                        let feature_offset = label as usize * n_features;
147                        let score = features
148                            .iter()
149                            .enumerate()
150                            .map(|(feat_idx, &feat_val)| {
151                                weights[feature_offset + feat_idx] * feat_val
152                            })
153                            .sum::<Float>();
154
155                        if score > best_score {
156                            best_score = score;
157                            best_label = label;
158                        }
159                    }
160                    predicted_labels[pos] = best_label as Float;
161                }
162
163                // Check if prediction is correct
164                let correct = true_labels
165                    .iter()
166                    .zip(predicted_labels.iter())
167                    .all(|(&true_label, &pred_label)| true_label == pred_label as i32);
168
169                if !correct {
170                    // Update weights: add true features, subtract predicted features
171                    for pos in 0..max_seq_len {
172                        let features = sequence.slice(s![pos, ..]);
173                        let true_label = true_labels[pos] as usize;
174                        let pred_label = predicted_labels[pos] as usize;
175
176                        // Add true label features
177                        let true_offset = true_label * n_features;
178                        for (feat_idx, &feat_val) in features.iter().enumerate() {
179                            weights[true_offset + feat_idx] += self.learning_rate * feat_val;
180                        }
181
182                        // Subtract predicted label features
183                        let pred_offset = pred_label * n_features;
184                        for (feat_idx, &feat_val) in features.iter().enumerate() {
185                            weights[pred_offset + feat_idx] -= self.learning_rate * feat_val;
186                        }
187                    }
188                    updated = true;
189                }
190            }
191
192            if !updated {
193                break;
194            }
195        }
196
197        Ok(StructuredPerceptron {
198            max_iterations: self.max_iterations,
199            learning_rate: self.learning_rate,
200            random_state: self.random_state,
201            state: StructuredPerceptronTrained {
202                weights,
203                n_features,
204                n_classes: n_classes as usize,
205            },
206        })
207    }
208}
209
210impl StructuredPerceptron<Untrained> {
211    /// Get the weights of the trained model
212    pub fn weights(&self) -> Option<&Array1<Float>> {
213        None
214    }
215}
216
217impl Predict<Array3<Float>, Array2<i32>> for StructuredPerceptron<StructuredPerceptronTrained> {
218    fn predict(&self, X: &Array3<Float>) -> SklResult<Array2<i32>> {
219        let (n_sequences, max_seq_len, n_features) = X.dim();
220
221        if n_features != self.state.n_features {
222            return Err(SklearsError::InvalidInput(
223                "X has different number of features than training data".to_string(),
224            ));
225        }
226
227        let mut predictions = Array2::<i32>::zeros((n_sequences, max_seq_len));
228
229        for seq_idx in 0..n_sequences {
230            let sequence = X.slice(s![seq_idx, .., ..]);
231
232            for pos in 0..max_seq_len {
233                let features = sequence.slice(s![pos, ..]);
234                let mut best_score = Float::NEG_INFINITY;
235                let mut best_label = 0;
236
237                for label in 0..self.state.n_classes {
238                    let feature_offset = label * n_features;
239                    let score = features
240                        .iter()
241                        .enumerate()
242                        .map(|(feat_idx, &feat_val)| {
243                            self.state.weights[feature_offset + feat_idx] * feat_val
244                        })
245                        .sum::<Float>();
246
247                    if score > best_score {
248                        best_score = score;
249                        best_label = label;
250                    }
251                }
252                predictions[[seq_idx, pos]] = best_label as i32;
253            }
254        }
255
256        Ok(predictions)
257    }
258}
259
260impl StructuredPerceptron<StructuredPerceptronTrained> {
261    /// Get the weights of the trained model
262    pub fn weights(&self) -> &Array1<Float> {
263        &self.state.weights
264    }
265}
266
267/// Hidden Markov Model for sequence modeling
268///
269/// A statistical model that assumes the system being modeled is a Markov process
270/// with unobserved (hidden) states. HMMs are particularly useful for temporal
271/// pattern recognition such as speech recognition, handwriting recognition,
272/// gesture recognition, part-of-speech tagging, and bioinformatics.
273///
274/// # Examples
275///
276/// ```
277/// use sklears_core::traits::{Predict, Fit};
278/// use sklears_multioutput::HiddenMarkovModel;
279/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
280/// use scirs2_core::ndarray::array;
281///
282/// let X = array![[[1.0, 2.0], [2.0, 3.0], [1.5, 2.5]]]; // One sequence
283/// let y = array![[0, 1, 0]]; // State sequence
284///
285/// let hmm = HiddenMarkovModel::new().n_states(2).max_iterations(100);
286/// let trained_hmm = hmm.fit(&X, &y).unwrap();
287/// let predictions = trained_hmm.predict(&X).unwrap();
288/// ```
289#[derive(Debug, Clone)]
290pub struct HiddenMarkovModel<State = Untrained> {
291    n_states: usize,
292    max_iterations: usize,
293    tolerance: Float,
294    random_state: Option<u64>,
295    state: State,
296}
297
298/// Trained state for Hidden Markov Model
299#[derive(Debug, Clone)]
300pub struct HiddenMarkovModelTrained {
301    transition_matrix: Array2<Float>,
302    emission_means: Array2<Float>,
303    #[allow(dead_code)]
304    emission_covariances: Array3<Float>,
305    initial_probs: Array1<Float>,
306    n_features: usize,
307    n_states: usize,
308}
309
310impl Default for HiddenMarkovModel<Untrained> {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316impl HiddenMarkovModel<Untrained> {
317    /// Create a new Hidden Markov Model
318    pub fn new() -> Self {
319        Self {
320            n_states: 2,
321            max_iterations: 100,
322            tolerance: 1e-6,
323            random_state: None,
324            state: Untrained,
325        }
326    }
327
328    /// Set the number of hidden states
329    pub fn n_states(mut self, n_states: usize) -> Self {
330        self.n_states = n_states;
331        self
332    }
333
334    /// Set the maximum number of iterations for EM algorithm
335    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
336        self.max_iterations = max_iterations;
337        self
338    }
339
340    /// Set the convergence tolerance
341    pub fn tolerance(mut self, tolerance: Float) -> Self {
342        self.tolerance = tolerance;
343        self
344    }
345
346    /// Set the random state for reproducible results
347    pub fn random_state(mut self, random_state: u64) -> Self {
348        self.random_state = Some(random_state);
349        self
350    }
351}
352
353impl Estimator for HiddenMarkovModel<Untrained> {
354    type Config = ();
355    type Error = SklearsError;
356    type Float = Float;
357
358    fn config(&self) -> &Self::Config {
359        &()
360    }
361}
362
363impl Fit<Array3<Float>, Array2<i32>> for HiddenMarkovModel<Untrained> {
364    type Fitted = HiddenMarkovModel<HiddenMarkovModelTrained>;
365
366    fn fit(self, X: &Array3<Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
367        let (n_sequences, max_seq_len, n_features) = X.dim();
368
369        if n_sequences != y.nrows() {
370            return Err(SklearsError::InvalidInput(
371                "X and y must have the same number of sequences".to_string(),
372            ));
373        }
374
375        if y.ncols() != max_seq_len {
376            return Err(SklearsError::InvalidInput(
377                "y sequence length must match X sequence length".to_string(),
378            ));
379        }
380
381        let mut rng = thread_rng();
382
383        // Initialize parameters
384        let normal_dist = RandNormal::new(0.0, 1.0).expect("operation should succeed");
385        let mut transition_matrix = Array2::<Float>::zeros((self.n_states, self.n_states));
386        for i in 0..self.n_states {
387            for j in 0..self.n_states {
388                transition_matrix[[i, j]] = rng.sample(normal_dist);
389            }
390        }
391        let mut emission_means = Array2::<Float>::zeros((self.n_states, n_features));
392        for i in 0..self.n_states {
393            for j in 0..n_features {
394                emission_means[[i, j]] = rng.sample(normal_dist);
395            }
396        }
397        let mut emission_covariances =
398            Array3::from_elem((self.n_states, n_features, n_features), 1.0);
399        let mut initial_probs = Array1::from_elem(self.n_states, 1.0 / self.n_states as Float);
400
401        // Normalize transition matrix
402        for i in 0..self.n_states {
403            let row_sum = transition_matrix.row(i).sum();
404            if row_sum > 0.0 {
405                for j in 0..self.n_states {
406                    transition_matrix[[i, j]] /= row_sum;
407                }
408            }
409        }
410
411        // Initialize emission covariances as identity matrices
412        for state in 0..self.n_states {
413            for i in 0..n_features {
414                emission_covariances[[state, i, i]] = 1.0;
415            }
416        }
417
418        let mut prev_likelihood = Float::NEG_INFINITY;
419
420        // EM algorithm
421        for _iteration in 0..self.max_iterations {
422            // E-step: Forward-backward algorithm would go here
423            // For simplicity, we'll use supervised learning with the provided labels
424
425            // M-step: Update parameters based on state assignments
426            let mut state_counts = Array1::<Float>::zeros(self.n_states);
427            let mut transition_counts = Array2::<Float>::zeros((self.n_states, self.n_states));
428            let mut emission_sums = Array2::<Float>::zeros((self.n_states, n_features));
429
430            for seq_idx in 0..n_sequences {
431                let sequence_data = X.slice(s![seq_idx, .., ..]);
432                let sequence_states = y.row(seq_idx);
433
434                for pos in 0..max_seq_len {
435                    let state = sequence_states[pos] as usize;
436                    if state < self.n_states {
437                        state_counts[state] += 1.0;
438
439                        // Update emission parameters
440                        for feat in 0..n_features {
441                            emission_sums[[state, feat]] += sequence_data[[pos, feat]];
442                        }
443
444                        // Update transition parameters
445                        if pos < max_seq_len - 1 {
446                            let next_state = sequence_states[pos + 1] as usize;
447                            if next_state < self.n_states {
448                                transition_counts[[state, next_state]] += 1.0;
449                            }
450                        }
451                    }
452                }
453
454                // Update initial state probabilities
455                let first_state = sequence_states[0] as usize;
456                if first_state < self.n_states {
457                    initial_probs[first_state] += 1.0;
458                }
459            }
460
461            // Normalize and update parameters
462            for state in 0..self.n_states {
463                if state_counts[state] > 0.0 {
464                    for feat in 0..n_features {
465                        emission_means[[state, feat]] =
466                            emission_sums[[state, feat]] / state_counts[state];
467                    }
468                }
469
470                let row_sum = transition_counts.row(state).sum();
471                if row_sum > 0.0 {
472                    for next_state in 0..self.n_states {
473                        transition_matrix[[state, next_state]] =
474                            transition_counts[[state, next_state]] / row_sum;
475                    }
476                }
477            }
478
479            // Normalize initial probabilities
480            let init_sum = initial_probs.sum();
481            if init_sum > 0.0 {
482                initial_probs /= init_sum;
483            }
484
485            // Simple likelihood calculation
486            let total_likelihood = state_counts.sum() as Float;
487
488            if (total_likelihood - prev_likelihood).abs() < self.tolerance {
489                break;
490            }
491            prev_likelihood = total_likelihood;
492        }
493
494        Ok(HiddenMarkovModel {
495            n_states: self.n_states,
496            max_iterations: self.max_iterations,
497            tolerance: self.tolerance,
498            random_state: self.random_state,
499            state: HiddenMarkovModelTrained {
500                transition_matrix,
501                emission_means,
502                emission_covariances,
503                initial_probs,
504                n_features,
505                n_states: self.n_states,
506            },
507        })
508    }
509}
510
511impl HiddenMarkovModel<Untrained> {
512    /// Get the transition matrix (only available after training)
513    pub fn transition_matrix(&self) -> Option<&Array2<Float>> {
514        None
515    }
516
517    /// Get the emission means (only available after training)
518    pub fn emission_means(&self) -> Option<&Array2<Float>> {
519        None
520    }
521
522    /// Get the initial state probabilities (only available after training)
523    pub fn initial_probabilities(&self) -> Option<&Array1<Float>> {
524        None
525    }
526}
527
528impl Predict<Array3<Float>, Array2<i32>> for HiddenMarkovModel<HiddenMarkovModelTrained> {
529    fn predict(&self, X: &Array3<Float>) -> SklResult<Array2<i32>> {
530        let (n_sequences, max_seq_len, n_features) = X.dim();
531
532        if n_features != self.state.n_features {
533            return Err(SklearsError::InvalidInput(
534                "X has different number of features than training data".to_string(),
535            ));
536        }
537
538        let mut predictions = Array2::<i32>::zeros((n_sequences, max_seq_len));
539
540        for seq_idx in 0..n_sequences {
541            let sequence = X.slice(s![seq_idx, .., ..]);
542
543            // Viterbi algorithm for finding most likely state sequence
544            let mut viterbi = Array2::<Float>::zeros((max_seq_len, self.state.n_states));
545            let mut path = Array2::<Float>::zeros((max_seq_len, self.state.n_states));
546
547            // Initialize first time step
548            for state in 0..self.state.n_states {
549                let emission_prob = self.gaussian_probability(&sequence.slice(s![0, ..]), state);
550                viterbi[[0, state]] = self.state.initial_probs[state].ln() + emission_prob.ln();
551            }
552
553            // Forward pass
554            for t in 1..max_seq_len {
555                for state in 0..self.state.n_states {
556                    let emission_prob =
557                        self.gaussian_probability(&sequence.slice(s![t, ..]), state);
558                    let mut best_prob = Float::NEG_INFINITY;
559                    let mut best_prev_state = 0;
560
561                    for prev_state in 0..self.state.n_states {
562                        let prob = viterbi[[t - 1, prev_state]]
563                            + self.state.transition_matrix[[prev_state, state]].ln()
564                            + emission_prob.ln();
565                        if prob > best_prob {
566                            best_prob = prob;
567                            best_prev_state = prev_state;
568                        }
569                    }
570                    viterbi[[t, state]] = best_prob;
571                    path[[t, state]] = best_prev_state as Float;
572                }
573            }
574
575            // Backward pass - find best path
576            let mut states = Array1::<Float>::zeros(max_seq_len);
577
578            // Find best final state
579            let mut best_final_prob = Float::NEG_INFINITY;
580            let mut best_final_state = 0;
581            for state in 0..self.state.n_states {
582                if viterbi[[max_seq_len - 1, state]] > best_final_prob {
583                    best_final_prob = viterbi[[max_seq_len - 1, state]];
584                    best_final_state = state;
585                }
586            }
587
588            states[max_seq_len - 1] = best_final_state as Float;
589
590            // Trace back
591            for t in (0..max_seq_len - 1).rev() {
592                states[t] = path[[t + 1, states[t + 1] as usize]];
593            }
594
595            // Copy to predictions
596            for t in 0..max_seq_len {
597                predictions[[seq_idx, t]] = states[t] as i32;
598            }
599        }
600
601        Ok(predictions)
602    }
603}
604
605impl HiddenMarkovModel<HiddenMarkovModelTrained> {
606    /// Get the transition matrix
607    pub fn transition_matrix(&self) -> &Array2<Float> {
608        &self.state.transition_matrix
609    }
610
611    /// Get the emission means
612    pub fn emission_means(&self) -> &Array2<Float> {
613        &self.state.emission_means
614    }
615
616    /// Get the initial state probabilities
617    pub fn initial_probabilities(&self) -> &Array1<Float> {
618        &self.state.initial_probs
619    }
620
621    /// Calculate Gaussian probability for emission
622    fn gaussian_probability(&self, observation: &ArrayView1<Float>, state: usize) -> Float {
623        let mean = self.state.emission_means.row(state);
624        let diff = observation.to_owned() - &mean.to_owned();
625
626        // Simple Gaussian with identity covariance for now
627        let exponent = -0.5 * diff.mapv(|x| x * x).sum();
628        let normalization =
629            (2.0 * std::f64::consts::PI).powf(self.state.n_features as f64 / 2.0) as Float;
630
631        (exponent.exp() / normalization).max(1e-10)
632    }
633}
634
635/// Maximum Entropy Markov Model (MEMM) for Sequence Labeling
636///
637/// MEMM is a discriminative model for sequence labeling that combines the advantages
638/// of maximum entropy models with Markov assumptions. Unlike CRF, MEMM models the
639/// conditional probability of each label given the previous label and observed features.
640///
641/// The model uses logistic regression at each position to predict the next label
642/// based on features extracted from the current observation and previous label.
643///
644/// # Examples
645///
646/// ```
647/// use sklears_multioutput::MaximumEntropyMarkovModel;
648/// use sklears_core::traits::{Predict, Fit};
649/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
650/// use scirs2_core::ndarray::array;
651///
652/// // Sequence data: each row is a sequence element with features
653/// let X = vec![
654///     array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]], // sequence 1
655///     array![[4.0, 1.0], [1.0, 4.0]]                // sequence 2
656/// ];
657///
658/// // Label sequences
659/// let y = vec![
660///     vec![0, 1, 0], // labels for sequence 1
661///     vec![1, 0]     // labels for sequence 2
662/// ];
663///
664/// let memm = MaximumEntropyMarkovModel::new()
665///     .max_iter(50)
666///     .learning_rate(0.01);
667/// let trained_memm = memm.fit(&X, &y).unwrap();
668/// let predictions = trained_memm.predict(&X).unwrap();
669/// ```
670#[derive(Debug, Clone)]
671pub struct MaximumEntropyMarkovModel<S = Untrained> {
672    state: S,
673    max_iter: usize,
674    learning_rate: Float,
675    l2_reg: Float,
676    tolerance: Float,
677    #[allow(dead_code)]
678    feature_functions: Vec<FeatureFunction>,
679    random_state: Option<u64>,
680}
681
682/// Trained state for MEMM
683#[derive(Debug, Clone)]
684pub struct MaximumEntropyMarkovModelTrained {
685    weights: Array1<Float>,
686    feature_functions: Vec<FeatureFunction>,
687    n_labels: usize,
688    n_features: usize,
689    label_to_idx: HashMap<i32, usize>,
690    #[allow(dead_code)]
691    idx_to_label: HashMap<usize, i32>,
692}
693
694/// Feature function for MEMM
695///
696/// Each feature function extracts a specific type of feature from the input
697/// and previous label combination. The feature value is typically binary
698/// (0 or 1) indicating presence or absence of the feature.
699#[derive(Debug, Clone)]
700pub struct FeatureFunction {
701    /// feature_type
702    pub feature_type: FeatureType,
703    /// weight_index
704    pub weight_index: usize,
705}
706
707/// Types of features in MEMM
708#[derive(Debug, Clone)]
709pub enum FeatureType {
710    /// Current observation feature at given index
711    Observation(usize),
712    /// Previous label feature
713    PreviousLabel(i32),
714    /// Interaction between observation and previous label
715    LabelObservationInteraction(i32, usize),
716}
717
718impl MaximumEntropyMarkovModel<Untrained> {
719    /// Create a new MEMM instance
720    pub fn new() -> Self {
721        Self {
722            state: Untrained,
723            max_iter: 100,
724            learning_rate: 0.01,
725            l2_reg: 0.01,
726            tolerance: 1e-6,
727            feature_functions: Vec::new(),
728            random_state: None,
729        }
730    }
731
732    /// Set maximum number of iterations
733    pub fn max_iter(mut self, max_iter: usize) -> Self {
734        self.max_iter = max_iter;
735        self
736    }
737
738    /// Set learning rate
739    pub fn learning_rate(mut self, learning_rate: Float) -> Self {
740        self.learning_rate = learning_rate;
741        self
742    }
743
744    /// Set L2 regularization strength
745    pub fn l2_regularization(mut self, l2_reg: Float) -> Self {
746        self.l2_reg = l2_reg;
747        self
748    }
749
750    /// Set convergence tolerance
751    pub fn tolerance(mut self, tolerance: Float) -> Self {
752        self.tolerance = tolerance;
753        self
754    }
755
756    /// Set random state for reproducible results
757    pub fn random_state(mut self, random_state: u64) -> Self {
758        self.random_state = Some(random_state);
759        self
760    }
761}
762
763impl Default for MaximumEntropyMarkovModel<Untrained> {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769impl Estimator for MaximumEntropyMarkovModel<Untrained> {
770    type Config = ();
771    type Error = SklearsError;
772    type Float = Float;
773
774    fn config(&self) -> &Self::Config {
775        &()
776    }
777}
778
779impl Fit<Vec<Array2<Float>>, Vec<Vec<i32>>> for MaximumEntropyMarkovModel<Untrained> {
780    type Fitted = MaximumEntropyMarkovModel<MaximumEntropyMarkovModelTrained>;
781
782    fn fit(self, X: &Vec<Array2<Float>>, y: &Vec<Vec<i32>>) -> SklResult<Self::Fitted> {
783        if X.len() != y.len() {
784            return Err(SklearsError::InvalidInput(
785                "X and y must have the same number of sequences".to_string(),
786            ));
787        }
788
789        if X.is_empty() {
790            return Err(SklearsError::InvalidInput(
791                "Cannot fit with 0 sequences".to_string(),
792            ));
793        }
794
795        // Determine dimensions and create label mappings
796        let n_features = X[0].ncols();
797        let mut unique_labels = HashSet::new();
798
799        for sequence_labels in y {
800            for &label in sequence_labels {
801                unique_labels.insert(label);
802            }
803        }
804
805        let mut label_to_idx = HashMap::new();
806        let mut idx_to_label = HashMap::new();
807        // Sort labels to ensure deterministic ordering
808        let mut sorted_labels: Vec<_> = unique_labels.iter().cloned().collect();
809        sorted_labels.sort();
810        for (idx, label) in sorted_labels.iter().enumerate() {
811            label_to_idx.insert(*label, idx);
812            idx_to_label.insert(idx, *label);
813        }
814        let n_labels = unique_labels.len();
815
816        // Create feature functions
817        let mut feature_functions = Vec::new();
818        let mut weight_idx = 0;
819
820        // Observation features
821        for feat_idx in 0..n_features {
822            for &label in &sorted_labels {
823                feature_functions.push(FeatureFunction {
824                    feature_type: FeatureType::LabelObservationInteraction(label, feat_idx),
825                    weight_index: weight_idx,
826                });
827                weight_idx += 1;
828            }
829        }
830
831        // Previous label features
832        for &prev_label in &sorted_labels {
833            for &_curr_label in &sorted_labels {
834                feature_functions.push(FeatureFunction {
835                    feature_type: FeatureType::PreviousLabel(prev_label),
836                    weight_index: weight_idx,
837                });
838                weight_idx += 1;
839            }
840        }
841
842        let n_weights = weight_idx;
843        let mut weights = Array1::<Float>::zeros(n_weights);
844
845        // Initialize weights randomly using random_state for reproducibility
846        let mut rng = if let Some(seed) = self.random_state {
847            scirs2_core::random::seeded_rng(seed)
848        } else {
849            // Use current time as seed for non-deterministic behavior
850            use std::time::{SystemTime, UNIX_EPOCH};
851            let time_seed = SystemTime::now()
852                .duration_since(UNIX_EPOCH)
853                .expect("operation should succeed")
854                .as_secs();
855            scirs2_core::random::seeded_rng(time_seed)
856        };
857
858        for w in weights.iter_mut() {
859            *w = rng.gen_range(-0.1..0.1);
860        }
861
862        // Training loop
863        for _iter in 0..self.max_iter {
864            let mut gradient = Array1::<Float>::zeros(n_weights);
865            let mut _total_loss = 0.0;
866
867            // Process each sequence
868            for (seq_idx, (sequence_x, sequence_y)) in X.iter().zip(y.iter()).enumerate() {
869                let seq_len = sequence_x.nrows();
870
871                if sequence_y.len() != seq_len {
872                    return Err(SklearsError::InvalidInput(format!(
873                        "Sequence {} length mismatch between X and y",
874                        seq_idx
875                    )));
876                }
877
878                // Process each position in the sequence
879                for pos in 0..seq_len {
880                    let current_obs = sequence_x.row(pos);
881                    let true_label = sequence_y[pos];
882                    let prev_label = if pos == 0 { -1 } else { sequence_y[pos - 1] };
883
884                    // Calculate feature vector for current position
885                    let _features =
886                        self.extract_features(&current_obs, prev_label, &feature_functions);
887
888                    // Calculate probabilities for all possible labels
889                    let mut scores = Array1::<Float>::zeros(n_labels);
890                    let mut max_score = Float::NEG_INFINITY;
891
892                    for (label_idx, _label) in unique_labels.iter().enumerate() {
893                        let label_features =
894                            self.extract_features(&current_obs, prev_label, &feature_functions);
895                        scores[label_idx] = label_features.dot(&weights);
896                        max_score = max_score.max(scores[label_idx]);
897                    }
898
899                    // Numerical stability: subtract max score
900                    for score in scores.iter_mut() {
901                        *score -= max_score;
902                    }
903
904                    // Calculate softmax probabilities
905                    let exp_scores: Array1<Float> = scores.mapv(|x| x.exp());
906                    let sum_exp_scores = exp_scores.sum();
907                    let probabilities = exp_scores / sum_exp_scores;
908
909                    // Calculate loss (negative log likelihood)
910                    let true_label_idx = label_to_idx[&true_label];
911                    _total_loss -= probabilities[true_label_idx].ln();
912
913                    // Calculate gradient
914                    for (label_idx, _label) in sorted_labels.iter().enumerate() {
915                        let label_features =
916                            self.extract_features(&current_obs, prev_label, &feature_functions);
917                        let prob = probabilities[label_idx];
918                        let indicator = if label_idx == true_label_idx {
919                            1.0
920                        } else {
921                            0.0
922                        };
923
924                        for (feat_idx, &feat_val) in label_features.iter().enumerate() {
925                            gradient[feat_idx] += feat_val * (prob - indicator);
926                        }
927                    }
928                }
929            }
930
931            // Add L2 regularization to gradient
932            for (i, w) in weights.iter().enumerate() {
933                gradient[i] += self.l2_reg * w;
934            }
935
936            // Update weights
937            let gradient_norm = gradient.mapv(|x| x.abs()).sum();
938            weights = &weights - self.learning_rate * &gradient;
939
940            // Check convergence
941            if gradient_norm < self.tolerance {
942                break;
943            }
944        }
945
946        let trained_state = MaximumEntropyMarkovModelTrained {
947            weights,
948            feature_functions,
949            n_labels,
950            n_features,
951            label_to_idx,
952            idx_to_label,
953        };
954
955        Ok(MaximumEntropyMarkovModel {
956            state: trained_state,
957            max_iter: self.max_iter,
958            learning_rate: self.learning_rate,
959            l2_reg: self.l2_reg,
960            tolerance: self.tolerance,
961            feature_functions: Vec::new(),
962            random_state: self.random_state,
963        })
964    }
965}
966
967impl MaximumEntropyMarkovModel<Untrained> {
968    /// Extract features for a given observation and previous label
969    fn extract_features(
970        &self,
971        observation: &ArrayView1<Float>,
972        prev_label: i32,
973        feature_functions: &[FeatureFunction],
974    ) -> Array1<Float> {
975        let mut features = Array1::<Float>::zeros(feature_functions.len());
976
977        for (i, func) in feature_functions.iter().enumerate() {
978            features[i] = match &func.feature_type {
979                FeatureType::Observation(feat_idx) => observation[*feat_idx],
980                FeatureType::PreviousLabel(label) => {
981                    if prev_label == *label {
982                        1.0
983                    } else {
984                        0.0
985                    }
986                }
987                FeatureType::LabelObservationInteraction(label, feat_idx) => {
988                    if prev_label == *label {
989                        observation[*feat_idx]
990                    } else {
991                        0.0
992                    }
993                }
994            };
995        }
996
997        features
998    }
999}
1000
1001impl Predict<Vec<Array2<Float>>, Vec<Vec<i32>>>
1002    for MaximumEntropyMarkovModel<MaximumEntropyMarkovModelTrained>
1003{
1004    fn predict(&self, X: &Vec<Array2<Float>>) -> SklResult<Vec<Vec<i32>>> {
1005        if X.is_empty() {
1006            return Ok(Vec::new());
1007        }
1008
1009        let mut predictions = Vec::with_capacity(X.len());
1010
1011        for sequence in X {
1012            let seq_len = sequence.nrows();
1013
1014            if sequence.ncols() != self.state.n_features {
1015                return Err(SklearsError::InvalidInput(
1016                    "X has different number of features than training data".to_string(),
1017                ));
1018            }
1019
1020            let mut sequence_predictions = Vec::with_capacity(seq_len);
1021
1022            for pos in 0..seq_len {
1023                let current_obs = sequence.row(pos);
1024                let prev_label = if pos == 0 {
1025                    -1
1026                } else {
1027                    sequence_predictions[pos - 1]
1028                };
1029
1030                // Calculate scores for all possible labels
1031                let mut best_score = Float::NEG_INFINITY;
1032                let mut best_label = 0;
1033
1034                // Sort labels to ensure deterministic iteration order
1035                let mut labels_sorted: Vec<_> = self.state.label_to_idx.iter().collect();
1036                labels_sorted.sort_by_key(|(&label, _)| label);
1037
1038                for (&label, &_label_idx) in labels_sorted {
1039                    let features = self.extract_features(
1040                        &current_obs,
1041                        prev_label,
1042                        &self.state.feature_functions,
1043                    );
1044                    let score = features.dot(&self.state.weights);
1045
1046                    if score > best_score {
1047                        best_score = score;
1048                        best_label = label;
1049                    }
1050                }
1051
1052                sequence_predictions.push(best_label);
1053            }
1054
1055            predictions.push(sequence_predictions);
1056        }
1057
1058        Ok(predictions)
1059    }
1060}
1061
1062impl MaximumEntropyMarkovModel<MaximumEntropyMarkovModelTrained> {
1063    /// Get the learned weights
1064    pub fn weights(&self) -> &Array1<Float> {
1065        &self.state.weights
1066    }
1067
1068    /// Get the number of labels
1069    pub fn n_labels(&self) -> usize {
1070        self.state.n_labels
1071    }
1072
1073    /// Extract features for a given observation and previous label
1074    fn extract_features(
1075        &self,
1076        observation: &ArrayView1<Float>,
1077        prev_label: i32,
1078        feature_functions: &[FeatureFunction],
1079    ) -> Array1<Float> {
1080        let mut features = Array1::<Float>::zeros(feature_functions.len());
1081
1082        for (i, func) in feature_functions.iter().enumerate() {
1083            features[i] = match &func.feature_type {
1084                FeatureType::Observation(feat_idx) => observation[*feat_idx],
1085                FeatureType::PreviousLabel(label) => {
1086                    if prev_label == *label {
1087                        1.0
1088                    } else {
1089                        0.0
1090                    }
1091                }
1092                FeatureType::LabelObservationInteraction(label, feat_idx) => {
1093                    if prev_label == *label {
1094                        observation[*feat_idx]
1095                    } else {
1096                        0.0
1097                    }
1098                }
1099            };
1100        }
1101
1102        features
1103    }
1104}
1105
1106#[allow(non_snake_case)]
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110    // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
1111    use scirs2_core::ndarray::array;
1112
1113    #[test]
1114    #[allow(non_snake_case)]
1115    fn test_structured_perceptron_basic() {
1116        let X = Array3::from_shape_vec((1, 2, 2), vec![1.0, 2.0, 2.0, 3.0])
1117            .expect("shape and data length should match");
1118        let y =
1119            Array2::from_shape_vec((1, 2), vec![0, 1]).expect("shape and data length should match");
1120
1121        let perceptron = StructuredPerceptron::new().max_iterations(10);
1122        let trained = perceptron
1123            .fit(&X, &y)
1124            .expect("model fitting should succeed");
1125        let predictions = trained.predict(&X).expect("prediction should succeed");
1126
1127        assert_eq!(predictions.dim(), (1, 2));
1128    }
1129
1130    #[test]
1131    #[allow(non_snake_case)]
1132    fn test_hidden_markov_model_basic() {
1133        let X = Array3::from_shape_vec((1, 3, 2), vec![1.0, 2.0, 2.0, 3.0, 1.5, 2.5])
1134            .expect("shape and data length should match");
1135        let y = Array2::from_shape_vec((1, 3), vec![0, 1, 0])
1136            .expect("shape and data length should match");
1137
1138        let hmm = HiddenMarkovModel::new().n_states(2).max_iterations(5);
1139        let trained = hmm.fit(&X, &y).expect("model fitting should succeed");
1140        let predictions = trained.predict(&X).expect("prediction should succeed");
1141
1142        assert_eq!(predictions.dim(), (1, 3));
1143    }
1144
1145    #[test]
1146    #[allow(non_snake_case)]
1147    fn test_memm_basic() {
1148        let X = vec![array![[1.0, 2.0], [2.0, 3.0]], array![[3.0, 1.0]]];
1149        let y = vec![vec![0, 1], vec![0]];
1150
1151        let memm = MaximumEntropyMarkovModel::new()
1152            .max_iter(5)
1153            .learning_rate(0.1);
1154        let trained = memm.fit(&X, &y).expect("model fitting should succeed");
1155        let predictions = trained.predict(&X).expect("prediction should succeed");
1156
1157        assert_eq!(predictions.len(), 2);
1158        assert_eq!(predictions[0].len(), 2);
1159        assert_eq!(predictions[1].len(), 1);
1160    }
1161
1162    #[test]
1163    #[allow(non_snake_case)]
1164    fn test_memm_reproducibility() {
1165        let X = vec![
1166            array![[1.0, 2.0], [2.0, 3.0]],
1167            array![[3.0, 1.0], [4.0, 2.0]],
1168        ];
1169        let y = vec![vec![0, 1], vec![1, 0]];
1170
1171        let memm1 = MaximumEntropyMarkovModel::new()
1172            .max_iter(10)
1173            .random_state(42);
1174        let trained_memm1 = memm1.fit(&X, &y).expect("model fitting should succeed");
1175        let pred1 = trained_memm1
1176            .predict(&X)
1177            .expect("prediction should succeed");
1178
1179        let memm2 = MaximumEntropyMarkovModel::new()
1180            .max_iter(10)
1181            .random_state(42);
1182        let trained_memm2 = memm2.fit(&X, &y).expect("model fitting should succeed");
1183        let pred2 = trained_memm2
1184            .predict(&X)
1185            .expect("prediction should succeed");
1186
1187        assert_eq!(pred1, pred2);
1188    }
1189}