1use std::cmp::Reverse;
12use std::collections::HashMap;
13
14#[cfg(feature = "parallel")]
15#[allow(unused_imports)]
16use rayon::prelude::*;
17use scirs2_core::ndarray::{Array2, ArrayView1};
18
19use crate::kernels::Kernel;
20use sklears_core::error::{Result, SklearsError};
21
22#[derive(Debug, Clone)]
36pub struct NGramKernel {
37 pub n: usize,
39 pub normalize: bool,
41 pub char_level: bool,
43 pub case_sensitive: bool,
45 pub min_freq: usize,
47 pub max_features: Option<usize>,
49}
50
51impl Default for NGramKernel {
52 fn default() -> Self {
53 Self {
54 n: 3,
55 normalize: true,
56 char_level: true,
57 case_sensitive: false,
58 min_freq: 1,
59 max_features: Some(10000),
60 }
61 }
62}
63
64impl NGramKernel {
65 pub fn new(n: usize) -> Self {
67 Self {
68 n,
69 ..Default::default()
70 }
71 }
72
73 pub fn with_normalize(mut self, normalize: bool) -> Self {
75 self.normalize = normalize;
76 self
77 }
78
79 pub fn with_char_level(mut self, char_level: bool) -> Self {
81 self.char_level = char_level;
82 self
83 }
84
85 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
87 self.case_sensitive = case_sensitive;
88 self
89 }
90
91 pub fn with_min_freq(mut self, min_freq: usize) -> Self {
93 self.min_freq = min_freq;
94 self
95 }
96
97 pub fn with_max_features(mut self, max_features: Option<usize>) -> Self {
99 self.max_features = max_features;
100 self
101 }
102
103 pub fn extract_ngrams(&self, text: &str) -> Result<HashMap<String, usize>> {
105 let processed_text = if self.case_sensitive {
106 text.to_string()
107 } else {
108 text.to_lowercase()
109 };
110
111 let mut ngrams = HashMap::new();
112
113 if self.char_level {
114 let chars: Vec<char> = processed_text.chars().collect();
116 for window in chars.windows(self.n) {
117 let ngram: String = window.iter().collect();
118 *ngrams.entry(ngram).or_insert(0) += 1;
119 }
120 } else {
121 let words: Vec<&str> = processed_text.split_whitespace().collect();
123 for window in words.windows(self.n) {
124 let ngram = window.join(" ");
125 *ngrams.entry(ngram).or_insert(0) += 1;
126 }
127 }
128
129 ngrams.retain(|_, &mut count| count >= self.min_freq);
131
132 Ok(ngrams)
133 }
134
135 pub fn compute_text_similarity(&self, text1: &str, text2: &str) -> Result<f64> {
137 let ngrams1 = self.extract_ngrams(text1)?;
138 let ngrams2 = self.extract_ngrams(text2)?;
139
140 let mut dot_product = 0.0;
141 let mut norm1 = 0.0;
142 let mut norm2 = 0.0;
143
144 for (ngram, &count1) in &ngrams1 {
146 norm1 += (count1 as f64).powi(2);
147 if let Some(&count2) = ngrams2.get(ngram) {
148 dot_product += (count1 as f64) * (count2 as f64);
149 }
150 }
151
152 for &count2 in ngrams2.values() {
153 norm2 += (count2 as f64).powi(2);
154 }
155
156 if self.normalize {
157 let norm_product = norm1.sqrt() * norm2.sqrt();
158 if norm_product > 0.0 {
159 Ok(dot_product / norm_product)
160 } else {
161 Ok(0.0)
162 }
163 } else {
164 Ok(dot_product)
165 }
166 }
167
168 pub fn texts_to_features(&self, texts: &[String]) -> Result<(Array2<f64>, Vec<String>)> {
170 let mut all_ngrams = HashMap::new();
172 for text in texts {
173 let ngrams = self.extract_ngrams(text)?;
174 for (ngram, count) in ngrams {
175 *all_ngrams.entry(ngram).or_insert(0) += count;
176 }
177 }
178
179 let mut ngram_features: Vec<(String, usize)> = all_ngrams
181 .into_iter()
182 .filter(|(_, count)| *count >= self.min_freq)
183 .collect();
184
185 ngram_features.sort_by_key(|item| Reverse(item.1));
187 if let Some(max_features) = self.max_features {
188 ngram_features.truncate(max_features);
189 }
190
191 let feature_names: Vec<String> = ngram_features
192 .iter()
193 .map(|(name, _)| name.clone())
194 .collect();
195 let num_features = feature_names.len();
196
197 let mut feature_matrix = Array2::zeros((texts.len(), num_features));
199
200 for (text_idx, text) in texts.iter().enumerate() {
201 let ngrams = self.extract_ngrams(text)?;
202 let mut text_norm = 0.0;
203
204 for (feat_idx, feature_name) in feature_names.iter().enumerate() {
206 let count = ngrams.get(feature_name).copied().unwrap_or(0) as f64;
207 feature_matrix[[text_idx, feat_idx]] = count;
208 text_norm += count * count;
209 }
210
211 if self.normalize && text_norm > 0.0 {
213 let norm = text_norm.sqrt();
214 for feat_idx in 0..num_features {
215 feature_matrix[[text_idx, feat_idx]] /= norm;
216 }
217 }
218 }
219
220 Ok((feature_matrix, feature_names))
221 }
222}
223
224impl Kernel for NGramKernel {
225 fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
226 x.iter().zip(y.iter()).map(|(a, b)| a * b).sum()
229 }
230
231 fn parameters(&self) -> HashMap<String, f64> {
232 let mut params = HashMap::new();
233 params.insert("n".to_string(), self.n as f64);
234 params
235 }
236}
237
238#[derive(Debug, Clone)]
252pub struct StringKernel {
253 pub max_length: usize,
255 pub lambda: f64,
257 pub normalize: bool,
259}
260
261impl Default for StringKernel {
262 fn default() -> Self {
263 Self {
264 max_length: 5,
265 lambda: 0.5,
266 normalize: true,
267 }
268 }
269}
270
271impl StringKernel {
272 pub fn new(max_length: usize, lambda: f64) -> Self {
274 Self {
275 max_length,
276 lambda,
277 normalize: true,
278 }
279 }
280
281 pub fn compute_string_similarity(&self, s1: &str, s2: &str) -> f64 {
283 let chars1: Vec<char> = s1.chars().collect();
284 let chars2: Vec<char> = s2.chars().collect();
285
286 let mut kernel_value = 0.0;
287
288 for length in 1..=self.max_length {
290 kernel_value += self.compute_subsequence_kernel(&chars1, &chars2, length);
291 }
292
293 if self.normalize {
294 let norm1 = self.compute_string_norm(&chars1);
295 let norm2 = self.compute_string_norm(&chars2);
296 let norm_product = norm1 * norm2;
297 if norm_product > 0.0 {
298 kernel_value / norm_product.sqrt()
299 } else {
300 0.0
301 }
302 } else {
303 kernel_value
304 }
305 }
306
307 fn compute_subsequence_kernel(&self, s1: &[char], s2: &[char], length: usize) -> f64 {
309 if length == 0 {
310 return 1.0;
311 }
312
313 let n1 = s1.len();
314 let n2 = s2.len();
315
316 if n1 < length || n2 < length {
317 return 0.0;
318 }
319
320 let mut dp = vec![vec![0.0; n2 + 1]; n1 + 1];
322
323 for i in 0..=n1 {
325 for j in 0..=n2 {
326 if length == 1 {
327 dp[i][j] = if i > 0 && j > 0 && s1[i - 1] == s2[j - 1] {
328 self.lambda.powi(2)
329 } else {
330 0.0
331 };
332 }
333 }
334 }
335
336 if length > 1 {
338 let _prev_kernel = self.compute_subsequence_kernel(s1, s2, length - 1);
339
340 for i in 1..=n1 {
341 for j in 1..=n2 {
342 if s1[i - 1] == s2[j - 1] {
343 let mut sum = 0.0;
345 for ii in 0..i {
346 for jj in 0..j {
347 let dist1 = i - ii - 1;
348 let dist2 = j - jj - 1;
349 let decay = self.lambda.powi((dist1 + dist2 + 2) as i32);
350 sum += decay
351 * self.compute_subsequence_kernel(
352 &s1[0..ii + 1],
353 &s2[0..jj + 1],
354 length - 1,
355 );
356 }
357 }
358 dp[i][j] = sum;
359 }
360 }
361 }
362 }
363
364 dp[n1][n2]
365 }
366
367 fn compute_string_norm(&self, s: &[char]) -> f64 {
369 let mut norm = 0.0;
370 for length in 1..=self.max_length {
371 norm += self.compute_subsequence_kernel(s, s, length);
372 }
373 norm
374 }
375}
376
377#[derive(Debug, Clone)]
389pub struct TfIdfVectorizer {
390 pub min_df: usize,
392 pub max_df: f64,
394 pub max_features: Option<usize>,
396 pub ngram_range: (usize, usize),
398 pub char_level: bool,
400 pub case_sensitive: bool,
402 vocabulary: Option<HashMap<String, usize>>,
404 doc_frequencies: Option<HashMap<String, usize>>,
406 n_docs: usize,
408}
409
410impl Default for TfIdfVectorizer {
411 fn default() -> Self {
412 Self {
413 min_df: 1,
414 max_df: 1.0,
415 max_features: None,
416 ngram_range: (1, 1),
417 char_level: false,
418 case_sensitive: false,
419 vocabulary: None,
420 doc_frequencies: None,
421 n_docs: 0,
422 }
423 }
424}
425
426impl TfIdfVectorizer {
427 pub fn new() -> Self {
429 Self::default()
430 }
431
432 pub fn with_min_df(mut self, min_df: usize) -> Self {
434 self.min_df = min_df;
435 self
436 }
437
438 pub fn with_max_df(mut self, max_df: f64) -> Self {
440 self.max_df = max_df;
441 self
442 }
443
444 pub fn with_max_features(mut self, max_features: Option<usize>) -> Self {
446 self.max_features = max_features;
447 self
448 }
449
450 pub fn with_ngram_range(mut self, min_n: usize, max_n: usize) -> Self {
452 self.ngram_range = (min_n, max_n);
453 self
454 }
455
456 pub fn with_char_level(mut self, char_level: bool) -> Self {
458 self.char_level = char_level;
459 self
460 }
461
462 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
464 self.case_sensitive = case_sensitive;
465 self
466 }
467
468 pub fn fit(&mut self, documents: &[String]) -> Result<()> {
470 self.n_docs = documents.len();
471
472 let mut term_doc_freq = HashMap::new();
474 let mut vocabulary = HashMap::new();
475
476 for document in documents {
477 let terms = self.extract_terms(document)?;
478 let unique_terms: std::collections::HashSet<String> = terms.into_iter().collect();
479
480 for term in unique_terms {
481 *term_doc_freq.entry(term.clone()).or_insert(0) += 1;
482 vocabulary.insert(term, 0); }
484 }
485
486 let max_df_count = (self.max_df * self.n_docs as f64) as usize;
488
489 let filtered_terms: Vec<String> = term_doc_freq
490 .iter()
491 .filter(|(_, &df)| df >= self.min_df && df <= max_df_count)
492 .map(|(term, _)| term.clone())
493 .collect();
494
495 let mut terms_with_freq: Vec<(String, usize)> = filtered_terms
497 .iter()
498 .map(|term| {
499 (
500 term.clone(),
501 *term_doc_freq.get(term).expect("key not found"),
502 )
503 })
504 .collect();
505
506 terms_with_freq.sort_by_key(|item| Reverse(item.1)); if let Some(max_features) = self.max_features {
509 terms_with_freq.truncate(max_features);
510 }
511
512 let mut final_vocabulary = HashMap::new();
514 let mut final_doc_freq = HashMap::new();
515
516 for (idx, (term, freq)) in terms_with_freq.iter().enumerate() {
517 final_vocabulary.insert(term.clone(), idx);
518 final_doc_freq.insert(term.clone(), *freq);
519 }
520
521 self.vocabulary = Some(final_vocabulary);
522 self.doc_frequencies = Some(final_doc_freq);
523
524 Ok(())
525 }
526
527 pub fn transform(&self, documents: &[String]) -> Result<Array2<f64>> {
529 let vocabulary = self
530 .vocabulary
531 .as_ref()
532 .ok_or_else(|| SklearsError::NotFitted {
533 operation: "transform".to_string(),
534 })?;
535
536 let doc_frequencies =
537 self.doc_frequencies
538 .as_ref()
539 .ok_or_else(|| SklearsError::NotFitted {
540 operation: "transform".to_string(),
541 })?;
542
543 let n_features = vocabulary.len();
544 let mut tfidf_matrix = Array2::zeros((documents.len(), n_features));
545
546 for (doc_idx, document) in documents.iter().enumerate() {
547 let terms = self.extract_terms(document)?;
548 let mut term_counts = HashMap::new();
549
550 for term in &terms {
552 if vocabulary.contains_key(term) {
553 *term_counts.entry(term.clone()).or_insert(0) += 1;
554 }
555 }
556
557 let total_terms = terms.len() as f64;
558
559 for (term, count) in term_counts {
561 if let (Some(&term_idx), Some(&doc_freq)) =
562 (vocabulary.get(&term), doc_frequencies.get(&term))
563 {
564 let tf = count as f64 / total_terms;
565 let idf = (self.n_docs as f64 / doc_freq as f64).ln();
566 let tfidf = tf * idf;
567
568 tfidf_matrix[[doc_idx, term_idx]] = tfidf;
569 }
570 }
571 }
572
573 Ok(tfidf_matrix)
574 }
575
576 pub fn fit_transform(&mut self, documents: &[String]) -> Result<Array2<f64>> {
578 self.fit(documents)?;
579 self.transform(documents)
580 }
581
582 fn extract_terms(&self, document: &str) -> Result<Vec<String>> {
584 let processed_doc = if self.case_sensitive {
585 document.to_string()
586 } else {
587 document.to_lowercase()
588 };
589
590 let mut terms = Vec::new();
591
592 if self.char_level {
593 let chars: Vec<char> = processed_doc.chars().collect();
595 for n in self.ngram_range.0..=self.ngram_range.1 {
596 for window in chars.windows(n) {
597 let term: String = window.iter().collect();
598 terms.push(term);
599 }
600 }
601 } else {
602 let words: Vec<&str> = processed_doc.split_whitespace().collect();
604 for n in self.ngram_range.0..=self.ngram_range.1 {
605 for window in words.windows(n) {
606 let term = window.join(" ");
607 terms.push(term);
608 }
609 }
610 }
611
612 Ok(terms)
613 }
614
615 pub fn get_feature_names(&self) -> Result<Vec<String>> {
617 let vocabulary = self
618 .vocabulary
619 .as_ref()
620 .ok_or_else(|| SklearsError::NotFitted {
621 operation: "get_feature_names".to_string(),
622 })?;
623
624 let mut features = vec![String::new(); vocabulary.len()];
625 for (term, &idx) in vocabulary {
626 features[idx] = term.clone();
627 }
628
629 Ok(features)
630 }
631}
632
633#[derive(Debug, Clone)]
640pub struct DocumentSimilarityKernel {
641 pub vectorizer: TfIdfVectorizer,
643 document_vectors: Option<Array2<f64>>,
645 similarity_threshold: f64,
647 normalize_vectors: bool,
649}
650
651impl DocumentSimilarityKernel {
652 pub fn new(vectorizer: TfIdfVectorizer) -> Self {
654 Self {
655 vectorizer,
656 document_vectors: None,
657 similarity_threshold: 0.1,
658 normalize_vectors: true,
659 }
660 }
661
662 pub fn fit(&mut self, documents: &[String]) -> Result<()> {
664 let vectors = self.vectorizer.fit_transform(documents)?;
665 self.document_vectors = Some(vectors);
666 Ok(())
667 }
668
669 pub fn compute_document_similarity(&self, doc1_idx: usize, doc2_idx: usize) -> Result<f64> {
671 let vectors = self
672 .document_vectors
673 .as_ref()
674 .ok_or_else(|| SklearsError::NotFitted {
675 operation: "compute_similarity".to_string(),
676 })?;
677
678 if doc1_idx >= vectors.nrows() || doc2_idx >= vectors.nrows() {
679 return Err(SklearsError::InvalidInput(
680 "Document index out of bounds".to_string(),
681 ));
682 }
683
684 let vec1 = vectors.row(doc1_idx);
685 let vec2 = vectors.row(doc2_idx);
686
687 let dot_product = vec1.dot(&vec2);
688 let norm1 = vec1.dot(&vec1).sqrt();
689 let norm2 = vec2.dot(&vec2).sqrt();
690
691 if norm1 > 0.0 && norm2 > 0.0 {
692 Ok(dot_product / (norm1 * norm2))
693 } else {
694 Ok(0.0)
695 }
696 }
697
698 pub fn get_document_vectors(&self) -> Option<&Array2<f64>> {
700 self.document_vectors.as_ref()
701 }
702}
703
704impl Kernel for DocumentSimilarityKernel {
705 fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
706 let dot_product: f64 = x.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
708 let norm_x: f64 = x.iter().map(|a| a * a).sum::<f64>().sqrt();
709 let norm_y: f64 = y.iter().map(|b| b * b).sum::<f64>().sqrt();
710
711 if norm_x > 0.0 && norm_y > 0.0 {
712 dot_product / (norm_x * norm_y)
713 } else {
714 0.0
715 }
716 }
717
718 fn parameters(&self) -> HashMap<String, f64> {
719 let mut params = HashMap::new();
720 params.insert(
721 "similarity_threshold".to_string(),
722 self.similarity_threshold,
723 );
724 params.insert(
725 "normalize_vectors".to_string(),
726 if self.normalize_vectors { 1.0 } else { 0.0 },
727 );
728 params
729 }
730}
731
732#[allow(non_snake_case)]
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn test_ngram_kernel_basic() {
739 let kernel = NGramKernel::new(2).with_char_level(true);
740
741 let text1 = "hello";
742 let text2 = "hello";
743 let similarity = kernel
744 .compute_text_similarity(text1, text2)
745 .expect("operation should succeed");
746 assert_eq!(similarity, 1.0); let text3 = "world";
749 let similarity2 = kernel
750 .compute_text_similarity(text1, text3)
751 .expect("operation should succeed");
752 assert!(similarity2 < 1.0); }
754
755 #[test]
756 fn test_ngram_extraction() {
757 let kernel = NGramKernel::new(2).with_char_level(true);
758 let ngrams = kernel
759 .extract_ngrams("hello")
760 .expect("operation should succeed");
761
762 assert!(ngrams.contains_key("he"));
763 assert!(ngrams.contains_key("el"));
764 assert!(ngrams.contains_key("ll"));
765 assert!(ngrams.contains_key("lo"));
766 assert_eq!(ngrams.len(), 4);
767 }
768
769 #[test]
770 fn test_word_level_ngrams() {
771 let kernel = NGramKernel::new(2).with_char_level(false);
772 let ngrams = kernel
773 .extract_ngrams("hello world test")
774 .expect("operation should succeed");
775
776 assert!(ngrams.contains_key("hello world"));
777 assert!(ngrams.contains_key("world test"));
778 assert_eq!(ngrams.len(), 2);
779 }
780
781 #[test]
782 fn test_string_kernel() {
783 let kernel = StringKernel::new(3, 0.5);
784
785 let s1 = "abc";
786 let s2 = "abc";
787 let similarity = kernel.compute_string_similarity(s1, s2);
788 assert!(similarity > 0.0);
789
790 let s3 = "xyz";
791 let similarity2 = kernel.compute_string_similarity(s1, s3);
792 assert!(similarity2 < similarity);
793 }
794
795 #[test]
796 fn test_tfidf_vectorizer() {
797 let mut vectorizer = TfIdfVectorizer::new().with_min_df(1).with_ngram_range(1, 1);
798
799 let documents = vec![
800 "hello world".to_string(),
801 "world test".to_string(),
802 "hello test".to_string(),
803 ];
804
805 let result = vectorizer.fit_transform(&documents);
806 assert!(result.is_ok());
807
808 let matrix = result.expect("operation should succeed");
809 assert_eq!(matrix.nrows(), 3);
810 assert!(matrix.ncols() > 0);
811 }
812
813 #[test]
814 fn test_tfidf_feature_names() {
815 let mut vectorizer = TfIdfVectorizer::new().with_min_df(1).with_ngram_range(1, 1);
816
817 let documents = vec!["hello world".to_string(), "world test".to_string()];
818
819 vectorizer
820 .fit(&documents)
821 .expect("model fitting should succeed");
822 let feature_names = vectorizer
823 .get_feature_names()
824 .expect("operation should succeed");
825
826 assert!(feature_names.contains(&"hello".to_string()));
827 assert!(feature_names.contains(&"world".to_string()));
828 assert!(feature_names.contains(&"test".to_string()));
829 }
830
831 #[test]
832 fn test_document_similarity_kernel() {
833 let vectorizer = TfIdfVectorizer::new().with_min_df(1).with_ngram_range(1, 1);
834
835 let mut kernel = DocumentSimilarityKernel::new(vectorizer);
836
837 let documents = vec![
838 "hello world".to_string(),
839 "hello test".to_string(),
840 "completely different text".to_string(),
841 ];
842
843 kernel
844 .fit(&documents)
845 .expect("model fitting should succeed");
846
847 let sim_01 = kernel
849 .compute_document_similarity(0, 1)
850 .expect("operation should succeed");
851 let sim_02 = kernel
852 .compute_document_similarity(0, 2)
853 .expect("operation should succeed");
854
855 assert!(sim_01 > sim_02);
856 }
857
858 #[test]
859 fn test_texts_to_features() {
860 let kernel = NGramKernel::new(2)
861 .with_char_level(true)
862 .with_normalize(true);
863
864 let texts = vec!["hello".to_string(), "world".to_string()];
865
866 let result = kernel.texts_to_features(&texts);
867 assert!(result.is_ok());
868
869 let (features, feature_names) = result.expect("operation should succeed");
870 assert_eq!(features.nrows(), 2);
871 assert!(features.ncols() > 0);
872 assert_eq!(features.ncols(), feature_names.len());
873 }
874
875 #[test]
876 fn test_case_sensitivity() {
877 let kernel_sensitive = NGramKernel::new(2)
878 .with_char_level(true)
879 .with_case_sensitive(true);
880
881 let kernel_insensitive = NGramKernel::new(2)
882 .with_char_level(true)
883 .with_case_sensitive(false);
884
885 let text1 = "Hello";
886 let text2 = "hello";
887
888 let sim_sensitive = kernel_sensitive
889 .compute_text_similarity(text1, text2)
890 .expect("operation should succeed");
891 let sim_insensitive = kernel_insensitive
892 .compute_text_similarity(text1, text2)
893 .expect("operation should succeed");
894
895 assert!(sim_insensitive > sim_sensitive);
896 assert_eq!(sim_insensitive, 1.0); }
898}