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 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 let xclass = filter(x.view(), y.view(), &class);
70
71 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 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 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#[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 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 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 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 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 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}