Skip to main content

linfa_bayes/
multinomial_nb.rs

1use linfa::dataset::{AsSingleTargets, DatasetBase, Labels};
2use linfa::traits::{Fit, FitWith, PredictInplace};
3use linfa::{Float, Label};
4use ndarray::{Array1, ArrayBase, ArrayView2, Data, Ix2};
5use std::collections::HashMap;
6use std::hash::Hash;
7
8use crate::base_nb::{NaiveBayes, NaiveBayesValidParams};
9use crate::error::{NaiveBayesError, Result};
10use crate::hyperparams::{MultinomialNbParams, MultinomialNbValidParams};
11use crate::{filter, ClassHistogram};
12
13#[cfg(feature = "serde")]
14use serde_crate::{Deserialize, Serialize};
15
16impl<'a, F, L, D, T> NaiveBayesValidParams<'a, F, L, D, T> for MultinomialNbValidParams<F, L>
17where
18    F: Float,
19    L: Label + 'a,
20    D: Data<Elem = F>,
21    T: AsSingleTargets<Elem = L> + Labels<Elem = L>,
22{
23}
24
25impl<F, L, D, T> Fit<ArrayBase<D, Ix2>, T, NaiveBayesError> for MultinomialNbValidParams<F, L>
26where
27    F: Float,
28    L: Label + Ord,
29    D: Data<Elem = F>,
30    T: AsSingleTargets<Elem = L> + Labels<Elem = L>,
31{
32    type Object = MultinomialNb<F, L>;
33    // Thin wrapper around the corresponding method of NaiveBayesValidParams
34    fn fit(&self, dataset: &DatasetBase<ArrayBase<D, Ix2>, T>) -> Result<Self::Object> {
35        NaiveBayesValidParams::fit(self, dataset, None)
36    }
37}
38
39impl<'a, F, L, D, T> FitWith<'a, ArrayBase<D, Ix2>, T, NaiveBayesError>
40    for MultinomialNbValidParams<F, L>
41where
42    F: Float,
43    L: Label + 'a,
44    D: Data<Elem = F>,
45    T: AsSingleTargets<Elem = L> + Labels<Elem = L>,
46{
47    type ObjectIn = Option<MultinomialNb<F, L>>;
48    type ObjectOut = MultinomialNb<F, L>;
49
50    fn fit_with(
51        &self,
52        model_in: Self::ObjectIn,
53        dataset: &DatasetBase<ArrayBase<D, Ix2>, T>,
54    ) -> Result<Self::ObjectOut> {
55        let x = dataset.records();
56        let y = dataset.as_single_targets();
57
58        let mut model = match model_in {
59            Some(temp) => temp,
60            None => MultinomialNb {
61                class_info: HashMap::new(),
62            },
63        };
64
65        let yunique = dataset.labels();
66
67        for class in yunique {
68            // filter dataset for current class
69            let xclass = filter(x.view(), y.view(), &class);
70
71            // compute feature log probabilities and counts
72            model
73                .class_info
74                .entry(class.clone())
75                .or_insert_with(ClassHistogram::default)
76                .update_with_smoothing(xclass.view(), self.alpha(), false);
77        }
78
79        // update priors
80        let class_count_sum = model
81            .class_info
82            .values()
83            .map(|x| x.class_count)
84            .sum::<usize>();
85
86        for info in model.class_info.values_mut() {
87            info.prior = F::cast(info.class_count) / F::cast(class_count_sum);
88        }
89
90        Ok(model)
91    }
92}
93
94impl<F: Float, L: Label, D> PredictInplace<ArrayBase<D, Ix2>, Array1<L>> for MultinomialNb<F, L>
95where
96    D: Data<Elem = F>,
97{
98    // Thin wrapper around the corresponding method of NaiveBayes
99    fn predict_inplace(&self, x: &ArrayBase<D, Ix2>, y: &mut Array1<L>) {
100        NaiveBayes::predict_inplace(self, x, y);
101    }
102
103    fn default_target(&self, x: &ArrayBase<D, Ix2>) -> Array1<L> {
104        Array1::default(x.nrows())
105    }
106}
107
108/// Fitted Multinomial Naive Bayes classifier.
109///
110/// See [MultinomialNbParams] for more information on the hyper-parameters.
111///
112/// # Model assumptions
113///
114/// The family of Naive Bayes classifiers assume independence between variables. They do not model
115/// moments between variables and lack therefore in modelling capability. The advantage is a linear
116/// fitting time with maximum-likelihood training in a closed form.
117///
118/// # Model usage example
119///
120/// The example below creates a set of hyperparameters, and then uses it to fit a Multinomial Naive
121/// Bayes classifier on provided data.
122///
123/// ```rust
124/// use linfa_bayes::{MultinomialNbParams, MultinomialNbValidParams, Result};
125/// use linfa::prelude::*;
126/// use ndarray::array;
127///
128/// let x = array![
129///     [-2., -1.],
130///     [-1., -1.],
131///     [-1., -2.],
132///     [1., 1.],
133///     [1., 2.],
134///     [2., 1.]
135/// ];
136/// let y = array![1, 1, 1, 2, 2, 2];
137/// let ds = DatasetView::new(x.view(), y.view());
138///
139/// // create a new parameter set with smoothing parameter equals `1`
140/// let unchecked_params = MultinomialNbParams::new()
141///     .alpha(1.0);
142///
143/// // fit model with unchecked parameter set
144/// let model = unchecked_params.fit(&ds)?;
145///
146/// // transform into a verified parameter set
147/// let checked_params = unchecked_params.check()?;
148///
149/// // update model with the verified parameters, this only returns
150/// // errors originating from the fitting process
151/// let model = checked_params.fit_with(Some(model), &ds)?;
152/// # Result::Ok(())
153/// ```
154#[cfg_attr(
155    feature = "serde",
156    derive(Serialize, Deserialize),
157    serde(crate = "serde_crate")
158)]
159#[derive(Debug, Clone, PartialEq)]
160pub struct MultinomialNb<F: PartialEq, L: Eq + Hash> {
161    class_info: HashMap<L, ClassHistogram<F>>,
162}
163
164impl<F: Float, L: Label> MultinomialNb<F, L> {
165    /// Construct a new set of hyperparameters
166    pub fn params() -> MultinomialNbParams<F, L> {
167        MultinomialNbParams::new()
168    }
169}
170
171impl<F, L> NaiveBayes<'_, F, L> for MultinomialNb<F, L>
172where
173    F: Float,
174    L: Label + Ord,
175{
176    // Compute unnormalized posterior log probability
177    fn joint_log_likelihood(&self, x: ArrayView2<F>) -> HashMap<&L, Array1<F>> {
178        let mut joint_log_likelihood = HashMap::new();
179        for (class, info) in self.class_info.iter() {
180            // Combine feature log probabilities and class priors to get log-likelihood for each class
181            let jointi = info.prior.ln();
182            let nij = x.dot(&info.feature_log_prob);
183            joint_log_likelihood.insert(class, nij + jointi);
184        }
185
186        joint_log_likelihood
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{MultinomialNb, NaiveBayes, Result};
193    use linfa::{
194        traits::{Fit, FitWith, Predict},
195        Dataset, DatasetView, Error,
196    };
197
198    use crate::{MultinomialNbParams, MultinomialNbValidParams};
199    use approx::assert_abs_diff_eq;
200    use ndarray::{array, Axis};
201    use std::collections::HashMap;
202
203    #[test]
204    fn autotraits() {
205        fn has_autotraits<T: Send + Sync + Sized + Unpin>() {}
206        has_autotraits::<MultinomialNb<f64, usize>>();
207        has_autotraits::<MultinomialNbValidParams<f64, usize>>();
208        has_autotraits::<MultinomialNbParams<f64, usize>>();
209    }
210
211    #[test]
212    fn test_multinomial_nb() -> Result<()> {
213        let ds = Dataset::new(
214            array![[1., 0.], [2., 0.], [3., 0.], [0., 1.], [0., 2.], [0., 3.]],
215            array![1, 1, 1, 2, 2, 2],
216        );
217
218        let fitted_clf = MultinomialNb::params().fit(&ds)?;
219        let pred = fitted_clf.predict(ds.records());
220
221        assert_abs_diff_eq!(pred, ds.targets());
222
223        let jll = fitted_clf.joint_log_likelihood(ds.records().view());
224        let mut expected = HashMap::new();
225        // Computed with sklearn.naive_bayes.MultinomialNB
226        expected.insert(
227            &1usize,
228            array![
229                -0.82667857,
230                -0.96020997,
231                -1.09374136,
232                -2.77258872,
233                -4.85203026,
234                -6.93147181
235            ],
236        );
237
238        expected.insert(
239            &2usize,
240            array![
241                -2.77258872,
242                -4.85203026,
243                -6.93147181,
244                -0.82667857,
245                -0.96020997,
246                -1.09374136
247            ],
248        );
249
250        for (key, value) in jll.iter() {
251            assert_abs_diff_eq!(value, expected.get(key).unwrap(), epsilon = 1e-6);
252        }
253
254        Ok(())
255    }
256
257    #[test]
258    fn test_mnb_fit_with() -> Result<()> {
259        let x = array![[1., 0.], [2., 0.], [3., 0.], [0., 1.], [0., 2.], [0., 3.]];
260        let y = array![1, 1, 1, 2, 2, 2];
261
262        let clf = MultinomialNb::params();
263
264        let model = x
265            .axis_chunks_iter(Axis(0), 2)
266            .zip(y.axis_chunks_iter(Axis(0), 2))
267            .map(|(a, b)| DatasetView::new(a, b))
268            .try_fold(None, |current, d| clf.fit_with(current, &d).map(Some))?
269            .ok_or(Error::NotEnoughSamples)?;
270
271        let pred = model.predict(&x);
272
273        assert_abs_diff_eq!(pred, y);
274
275        let jll = model.joint_log_likelihood(x.view());
276
277        let mut expected = HashMap::new();
278        // Computed with sklearn.naive_bayes.MultinomialNB
279        expected.insert(
280            &1usize,
281            array![
282                -0.82667857,
283                -0.96020997,
284                -1.09374136,
285                -2.77258872,
286                -4.85203026,
287                -6.93147181
288            ],
289        );
290
291        expected.insert(
292            &2usize,
293            array![
294                -2.77258872,
295                -4.85203026,
296                -6.93147181,
297                -0.82667857,
298                -0.96020997,
299                -1.09374136
300            ],
301        );
302
303        for (key, value) in jll.iter() {
304            assert_abs_diff_eq!(value, expected.get(key).unwrap(), epsilon = 1e-6);
305        }
306
307        Ok(())
308    }
309}