ffsvm/svm/core/
sparse.rs

1use crate::sparse::{SparseMatrix, SparseVector};
2
3use simd_aligned::traits::Simd;
4use std::convert::TryFrom;
5
6use crate::{
7    errors::Error,
8    parser::ModelFile,
9    svm::{
10        class::Class,
11        features::{FeatureVector, Label},
12        kernel::{KernelSparse, Linear, Poly, Rbf, Sigmoid},
13        predict::Predict,
14        Probabilities, SVMType,
15    },
16    util::{find_max_index, set_all, sigmoid_predict},
17    vectors::Triangular,
18};
19
20/// an SVM optimized for large models with many empty attributes.
21///
22/// # Creating an SVM
23///
24/// This SVM can be created by passing a [`ModelFile`](crate::ModelFile) or [`&str`] into [`ModelFile::try_from`]:
25///
26///
27/// ```
28/// use ffsvm::SparseSVM;
29///
30/// let svm = SparseSVM::try_from("...");
31/// ```
32pub struct SparseSVM {
33    /// Total number of support vectors
34    pub(crate) num_total_sv: usize,
35
36    /// Number of attributes per support vector
37    pub(crate) num_attributes: usize,
38
39    pub(crate) rho: Triangular<f64>,
40
41    pub(crate) probabilities: Option<Probabilities>,
42
43    pub(crate) svm_type: SVMType,
44
45    /// SVM specific data needed for classification
46    pub(crate) kernel: Box<dyn KernelSparse>,
47
48    /// All classes
49    pub(crate) classes: Vec<Class<SparseMatrix<f32>>>,
50}
51
52impl SparseSVM {
53    /// Finds the class index for a given label.
54    ///
55    /// # Description
56    ///
57    /// This method takes a `label` as defined in the libSVM training model
58    /// and returns the internal `index` where this label resides. The index
59    /// equals [`FeatureVector::probabilities`] index where that label's
60    /// probability can be found.
61    ///
62    /// # Returns
63    ///
64    /// If the label was found its index returned in the [`Option`], otherwise `None`
65    /// is returned.
66    #[must_use]
67    pub fn class_index_for_label(&self, label: i32) -> Option<usize> {
68        for (i, class) in self.classes.iter().enumerate() {
69            if class.label != label {
70                continue;
71            }
72
73            return Some(i);
74        }
75
76        None
77    }
78
79    /// Returns the class label for a given index.
80    ///
81    /// # Description
82    ///
83    /// The inverse of [`SparseSVM::class_index_for_label`], this function returns the class label
84    /// associated with a certain internal index. The index equals the [`FeatureVector::probabilities`]
85    /// index where a label's probability can be found.
86    ///
87    /// # Returns
88    ///
89    /// If the index was found it is returned in the [`Option`], otherwise `None`
90    /// is returned.
91    #[must_use]
92    pub fn class_label_for_index(&self, index: usize) -> Option<i32> {
93        if index >= self.classes.len() {
94            None
95        } else {
96            Some(self.classes[index].label)
97        }
98    }
99
100    /// Computes the kernel values for this problem
101    pub(crate) fn compute_kernel_values(&self, problem: &mut FeatureVector<SparseVector<f32>>) {
102        // Get current problem and decision values array
103        let features = &problem.features;
104        let kernel_values = &mut problem.kernel_values;
105
106        // Compute kernel values per class
107        for (i, class) in self.classes.iter().enumerate() {
108            let kvalues = kernel_values.row_as_flat_mut(i);
109
110            self.kernel.compute(&class.support_vectors, features, kvalues);
111        }
112    }
113
114    // This is pretty much copy-paste of `multiclass_probability` from libSVM which we need
115    // to be compatibly for predicting probability for multiclass SVMs. The method is in turn
116    // based on Method 2 from the paper "Probability Estimates for Multi-class
117    // Classification by Pairwise Coupling", Journal of Machine Learning Research 5 (2004) 975-1005,
118    // by Ting-Fan Wu, Chih-Jen Lin and Ruby C. Weng.
119    pub(crate) fn compute_multiclass_probabilities(&self, problem: &mut FeatureVector<SparseVector<f32>>) -> Result<(), Error> {
120        compute_multiclass_probabilities_impl!(self, problem)
121    }
122
123    /// Based on kernel values, computes the decision values for this problem.
124    pub(crate) fn compute_classification_values(&self, problem: &mut FeatureVector<SparseVector<f32>>) {
125        compute_classification_values_impl!(self, problem);
126    }
127
128    /// Based on kernel values, computes the decision values for this problem.
129    pub(crate) fn compute_regression_values(&self, problem: &mut FeatureVector<SparseVector<f32>>) {
130        let class = &self.classes[0];
131        let coef = class.coefficients.row(0);
132        let kvalues = problem.kernel_values.row(0);
133
134        let mut sum = coef.iter().zip(kvalues).map(|(a, b)| (*a * *b).sum()).sum::<f64>();
135
136        sum -= self.rho[0];
137
138        problem.result = Label::Value(sum as f32);
139    }
140
141    /// Returns number of attributes, reflecting the libSVM model.
142    #[must_use]
143    pub const fn attributes(&self) -> usize {
144        self.num_attributes
145    }
146
147    /// Returns number of classes, reflecting the libSVM model.
148    #[must_use]
149    pub fn classes(&self) -> usize {
150        self.classes.len()
151    }
152}
153
154impl Predict<SparseVector<f32>> for SparseSVM {
155    // Predict the value for one problem.
156    fn predict_value(&self, problem: &mut FeatureVector<SparseVector<f32>>) -> Result<(), Error> {
157        match self.svm_type {
158            SVMType::CSvc | SVMType::NuSvc => {
159                // Compute kernel, decision values and eventually the label
160                self.compute_kernel_values(problem);
161                self.compute_classification_values(problem);
162
163                // Compute highest vote
164                let highest_vote = find_max_index(&problem.vote);
165                problem.result = Label::Class(self.classes[highest_vote].label);
166
167                Ok(())
168            }
169            SVMType::ESvr | SVMType::NuSvr => {
170                self.compute_kernel_values(problem);
171                self.compute_regression_values(problem);
172                Ok(())
173            }
174        }
175    }
176
177    fn predict_probability(&self, problem: &mut FeatureVector<SparseVector<f32>>) -> Result<(), Error> {
178        predict_probability_impl!(self, problem)
179    }
180}
181
182impl<'a> TryFrom<&'a str> for SparseSVM {
183    type Error = Error;
184
185    fn try_from(input: &'a str) -> Result<Self, Error> {
186        let raw_model = ModelFile::try_from(input)?;
187        Self::try_from(&raw_model)
188    }
189}
190
191impl<'a> TryFrom<&'a ModelFile<'_>> for SparseSVM {
192    type Error = Error;
193
194    fn try_from(raw_model: &'a ModelFile<'_>) -> Result<Self, Error> {
195        let (mut svm, nr_sv) = prepare_svm!(raw_model, dyn KernelSparse, SparseMatrix<f32>, Self);
196
197        let vectors = &raw_model.vectors();
198
199        // Things down here are a bit ugly as the file format is a bit ugly ...
200        // Now read all vectors and decode stored information
201        let mut start_offset = 0;
202
203        // In the raw file, support vectors are grouped by class
204        for (i, num_sv_per_class) in nr_sv.iter().enumerate() {
205            let stop_offset = start_offset + *num_sv_per_class as usize;
206
207            // Set support vector and coefficients
208            for (i_vector, vector) in vectors[start_offset..stop_offset].iter().enumerate() {
209                // Set support vectors
210                for attribute in &vector.features {
211                    let support_vectors = &mut svm.classes[i].support_vectors;
212                    support_vectors[(i_vector, attribute.index as usize)] = attribute.value;
213                }
214
215                // Set coefficients
216                for (i_coefficient, coefficient) in vector.coefs.iter().enumerate() {
217                    let mut coefficients = svm.classes[i].coefficients.flat_mut();
218                    coefficients[(i_coefficient, i_vector)] = f64::from(*coefficient);
219                }
220            }
221
222            // Update last offset.
223            start_offset = stop_offset;
224        }
225
226        // Return what we have
227        Ok(svm)
228    }
229}