1use crate::error::{MLError, Result};
8use crate::qnn::QuantumNeuralNetwork;
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::random::prelude::*;
11use std::collections::HashMap;
12use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum NLPTaskType {
17 Classification,
19
20 SequenceLabeling,
22
23 Translation,
25
26 Generation,
28
29 SentimentAnalysis,
31
32 Summarization,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum EmbeddingStrategy {
39 BagOfWords,
41
42 TFIDF,
44
45 Word2Vec,
47
48 Custom,
50}
51
52impl From<usize> for EmbeddingStrategy {
53 fn from(value: usize) -> Self {
54 match value {
55 0 => EmbeddingStrategy::BagOfWords,
56 1 => EmbeddingStrategy::TFIDF,
57 2 => EmbeddingStrategy::Word2Vec,
58 _ => EmbeddingStrategy::Custom,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct TextPreprocessor {
66 pub lowercase: bool,
68
69 pub remove_stopwords: bool,
71
72 pub lemmatize: bool,
74
75 pub stem: bool,
77
78 pub stopwords: Vec<String>,
80}
81
82impl TextPreprocessor {
83 pub fn new() -> Self {
85 TextPreprocessor {
86 lowercase: true,
87 remove_stopwords: true,
88 lemmatize: false,
89 stem: false,
90 stopwords: Vec::new(),
91 }
92 }
93
94 pub fn with_lowercase(mut self, lowercase: bool) -> Self {
96 self.lowercase = lowercase;
97 self
98 }
99
100 pub fn with_remove_stopwords(mut self, remove_stopwords: bool) -> Self {
102 self.remove_stopwords = remove_stopwords;
103 self
104 }
105
106 pub fn with_lemmatize(mut self, lemmatize: bool) -> Self {
108 self.lemmatize = lemmatize;
109 self
110 }
111
112 pub fn with_stem(mut self, stem: bool) -> Self {
114 self.stem = stem;
115 self
116 }
117
118 pub fn with_stopwords(mut self, stopwords: Vec<String>) -> Self {
120 self.stopwords = stopwords;
121 self
122 }
123
124 pub fn preprocess(&self, text: &str) -> Result<String> {
126 let mut processed = text.to_string();
130
131 if self.lowercase {
132 processed = processed.to_lowercase();
133 }
134
135 if self.remove_stopwords {
136 for stopword in &self.stopwords {
137 processed = processed.replace(stopword, "");
138 }
139 }
140
141 Ok(processed)
142 }
143
144 pub fn tokenize(&self, text: &str) -> Result<Vec<String>> {
146 let processed = self.preprocess(text)?;
150 let tokens = processed
151 .split_whitespace()
152 .map(|s| s.to_string())
153 .collect::<Vec<_>>();
154
155 Ok(tokens)
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct WordEmbedding {
162 pub strategy: EmbeddingStrategy,
164
165 pub dimension: usize,
167
168 pub embeddings: HashMap<String, Array1<f64>>,
170
171 pub vocabulary: Vec<String>,
173}
174
175impl WordEmbedding {
176 pub fn new(strategy: EmbeddingStrategy, dimension: usize) -> Self {
178 WordEmbedding {
179 strategy,
180 dimension,
181 embeddings: HashMap::new(),
182 vocabulary: Vec::new(),
183 }
184 }
185
186 pub fn fit(&mut self, corpus: &[&str]) -> Result<()> {
201 const WINDOW_RADIUS: usize = 2;
202 const INDEX_VECTOR_NONZEROS: usize = 4;
203
204 let mut word_counts: HashMap<String, usize> = HashMap::new();
205 let tokenized_corpus: Vec<Vec<String>> = corpus
206 .iter()
207 .map(|text| {
208 let tokens: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
209 for token in &tokens {
210 *word_counts.entry(token.clone()).or_insert(0) += 1;
211 }
212 tokens
213 })
214 .collect();
215
216 let mut vocab_items: Vec<(String, usize)> = word_counts
218 .iter()
219 .map(|(word, count)| (word.clone(), *count))
220 .collect();
221 vocab_items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
222
223 self.vocabulary = vocab_items
224 .into_iter()
225 .map(|(word, _)| word)
226 .take(10000)
227 .collect();
228
229 self.embeddings.clear();
230 if self.vocabulary.is_empty() {
231 return Ok(());
232 }
233
234 let word_index: HashMap<&str, usize> = self
235 .vocabulary
236 .iter()
237 .enumerate()
238 .map(|(i, w)| (w.as_str(), i))
239 .collect();
240
241 let mut rng = thread_rng();
243 let index_vectors: Vec<Array1<f64>> = (0..self.vocabulary.len())
244 .map(|_| {
245 let mut vector = Array1::<f64>::zeros(self.dimension);
246 let nonzeros = INDEX_VECTOR_NONZEROS.min(self.dimension);
247 let mut placed = 0;
248 let mut attempts = 0;
249 while placed < nonzeros && attempts < nonzeros * 20 {
250 attempts += 1;
251 let raw_position = (rng.random::<f64>() * self.dimension as f64) as usize;
252 let position = raw_position.min(self.dimension.saturating_sub(1));
253 if vector[position] == 0.0 {
254 let sign = if rng.random::<f64>() < 0.5 { -1.0 } else { 1.0 };
255 vector[position] = sign;
256 placed += 1;
257 }
258 }
259 vector
260 })
261 .collect();
262
263 let mut context_vectors: Vec<Array1<f64>> = (0..self.vocabulary.len())
266 .map(|_| Array1::<f64>::zeros(self.dimension))
267 .collect();
268
269 for tokens in &tokenized_corpus {
270 let indices: Vec<Option<usize>> = tokens
271 .iter()
272 .map(|token| word_index.get(token.as_str()).copied())
273 .collect();
274
275 for (position, target_idx_opt) in indices.iter().enumerate() {
276 let target_idx = match target_idx_opt {
277 Some(idx) => *idx,
278 None => continue,
279 };
280 let window_start = position.saturating_sub(WINDOW_RADIUS);
281 let window_end = (position + WINDOW_RADIUS + 1).min(indices.len());
282 for context_position in window_start..window_end {
283 if context_position == position {
284 continue;
285 }
286 if let Some(context_idx) = indices[context_position] {
287 context_vectors[target_idx] =
288 &context_vectors[target_idx] + &index_vectors[context_idx];
289 }
290 }
291 }
292 }
293
294 for (i, word) in self.vocabulary.iter().enumerate() {
295 let mut embedding = context_vectors[i].clone();
296 let norm = embedding.dot(&embedding).sqrt();
297 if norm > 1e-10 {
298 embedding.mapv_inplace(|x| x / norm);
299 } else {
300 embedding = index_vectors[i].clone();
305 }
306 self.embeddings.insert(word.clone(), embedding);
307 }
308
309 Ok(())
310 }
311
312 pub fn get_embedding(&self, word: &str) -> Option<&Array1<f64>> {
314 self.embeddings.get(word)
315 }
316
317 pub fn embed_text(&self, text: &str) -> Result<Array1<f64>> {
319 let words = text.split_whitespace().collect::<Vec<_>>();
323 let mut embedding = Array1::zeros(self.dimension);
324 let mut count = 0;
325
326 for word in words {
327 if let Some(word_embedding) = self.get_embedding(word) {
328 embedding += word_embedding;
329 count += 1;
330 }
331 }
332
333 if count > 0 {
334 embedding /= count as f64;
335 }
336
337 Ok(embedding)
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct QuantumLanguageModel {
344 pub num_qubits: usize,
346
347 pub embedding_strategy: EmbeddingStrategy,
349
350 pub preprocessor: TextPreprocessor,
352
353 pub embedding: WordEmbedding,
355
356 pub qnn: QuantumNeuralNetwork,
358
359 pub task: NLPTaskType,
361
362 pub labels: Vec<String>,
364}
365
366impl QuantumLanguageModel {
367 pub fn new(
369 num_qubits: usize,
370 embedding_dimension: usize,
371 strategy: EmbeddingStrategy,
372 task: NLPTaskType,
373 labels: Vec<String>,
374 ) -> Result<Self> {
375 let preprocessor = TextPreprocessor::new();
376 let embedding = WordEmbedding::new(strategy, embedding_dimension);
377
378 let layers = vec![
380 crate::qnn::QNNLayerType::EncodingLayer {
381 num_features: embedding_dimension,
382 },
383 crate::qnn::QNNLayerType::VariationalLayer {
384 num_params: 2 * num_qubits,
385 },
386 crate::qnn::QNNLayerType::EntanglementLayer {
387 connectivity: "full".to_string(),
388 },
389 crate::qnn::QNNLayerType::VariationalLayer {
390 num_params: 2 * num_qubits,
391 },
392 crate::qnn::QNNLayerType::MeasurementLayer {
393 measurement_basis: "computational".to_string(),
394 },
395 ];
396
397 let output_dim = match task {
398 NLPTaskType::Classification | NLPTaskType::SentimentAnalysis => labels.len(),
399 NLPTaskType::SequenceLabeling => labels.len(),
400 NLPTaskType::Translation => embedding_dimension,
401 NLPTaskType::Generation => embedding_dimension,
402 NLPTaskType::Summarization => embedding_dimension,
403 };
404
405 let qnn = QuantumNeuralNetwork::new(layers, num_qubits, embedding_dimension, output_dim)?;
406
407 Ok(QuantumLanguageModel {
408 num_qubits,
409 embedding_strategy: strategy,
410 preprocessor,
411 embedding,
412 qnn,
413 task,
414 labels,
415 })
416 }
417
418 pub fn fit(&mut self, texts: &[&str], labels: &[usize]) -> Result<()> {
420 self.embedding.fit(texts)?;
422
423 let mut embeddings = Vec::with_capacity(texts.len());
425
426 for text in texts {
427 let embedding = self.embedding.embed_text(text)?;
428 embeddings.push(embedding);
429 }
430
431 let x_train = Array2::from_shape_vec(
433 (embeddings.len(), self.embedding.dimension),
434 embeddings.iter().flat_map(|e| e.iter().cloned()).collect(),
435 )
436 .map_err(|e| MLError::DataError(format!("Failed to create training data: {}", e)))?;
437
438 let y_train = Array1::from_vec(labels.iter().map(|&l| l as f64).collect());
440
441 self.qnn.train_1d(&x_train, &y_train, 100, 0.01)?;
443
444 Ok(())
445 }
446
447 pub fn predict(&self, text: &str) -> Result<(String, f64)> {
449 let embedding = self.embedding.embed_text(text)?;
451
452 let output = self.qnn.forward(&embedding)?;
454
455 let mut best_label = 0;
457 let mut best_score = output[0];
458
459 for i in 1..output.len() {
460 if output[i] > best_score {
461 best_score = output[i];
462 best_label = i;
463 }
464 }
465
466 if best_label < self.labels.len() {
467 Ok((self.labels[best_label].clone(), best_score))
468 } else {
469 Err(MLError::MLOperationError(format!(
470 "Invalid prediction index: {}",
471 best_label
472 )))
473 }
474 }
475}
476
477#[derive(Debug, Clone)]
479pub struct SentimentAnalyzer {
480 model: QuantumLanguageModel,
482}
483
484impl SentimentAnalyzer {
485 pub fn new(num_qubits: usize) -> Result<Self> {
487 let model = QuantumLanguageModel::new(
488 num_qubits,
489 32, EmbeddingStrategy::BagOfWords,
491 NLPTaskType::SentimentAnalysis,
492 vec![
493 "negative".to_string(),
494 "neutral".to_string(),
495 "positive".to_string(),
496 ],
497 )?;
498
499 Ok(SentimentAnalyzer { model })
500 }
501
502 pub fn analyze(&self, text: &str) -> Result<(String, f64)> {
504 self.model.predict(text)
505 }
506
507 pub fn train(&mut self, texts: &[&str], labels: &[usize]) -> Result<()> {
509 self.model.fit(texts, labels)
510 }
511}
512
513#[derive(Debug, Clone)]
515pub struct TextSummarizer {
516 model: QuantumLanguageModel,
518
519 max_length: usize,
521}
522
523impl TextSummarizer {
524 pub fn new(num_qubits: usize) -> Result<Self> {
526 let model = QuantumLanguageModel::new(
527 num_qubits,
528 64, EmbeddingStrategy::BagOfWords,
530 NLPTaskType::Summarization,
531 Vec::new(), )?;
533
534 Ok(TextSummarizer {
535 model,
536 max_length: 100,
537 })
538 }
539
540 pub fn with_max_length(mut self, max_length: usize) -> Self {
542 self.max_length = max_length;
543 self
544 }
545
546 pub fn summarize(&self, text: &str) -> Result<String> {
548 let sentences = text.split('.').collect::<Vec<_>>();
552 let num_sentences = sentences.len();
553
554 let num_summary_sentences = (num_sentences / 4).max(1);
556 let selected_indices = vec![0, num_sentences / 2, num_sentences - 1];
557
558 let mut summary = String::new();
559
560 for &index in selected_indices.iter().take(num_summary_sentences) {
561 if index < sentences.len() {
562 summary.push_str(sentences[index]);
563 summary.push('.');
564 }
565 }
566
567 if summary.len() > self.max_length {
569 let truncated = summary.chars().take(self.max_length).collect::<String>();
570 let last_space = truncated.rfind(' ').unwrap_or(truncated.len());
571 summary = truncated[..last_space].to_string();
572 summary.push_str("...");
573 }
574
575 Ok(summary)
576 }
577}
578
579impl fmt::Display for NLPTaskType {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 match self {
582 NLPTaskType::Classification => write!(f, "Classification"),
583 NLPTaskType::SequenceLabeling => write!(f, "Sequence Labeling"),
584 NLPTaskType::Translation => write!(f, "Translation"),
585 NLPTaskType::Generation => write!(f, "Generation"),
586 NLPTaskType::SentimentAnalysis => write!(f, "Sentiment Analysis"),
587 NLPTaskType::Summarization => write!(f, "Summarization"),
588 }
589 }
590}
591
592impl fmt::Display for EmbeddingStrategy {
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 match self {
595 EmbeddingStrategy::BagOfWords => write!(f, "Bag of Words"),
596 EmbeddingStrategy::TFIDF => write!(f, "TF-IDF"),
597 EmbeddingStrategy::Word2Vec => write!(f, "Word2Vec"),
598 EmbeddingStrategy::Custom => write!(f, "Custom"),
599 }
600 }
601}
602
603impl QuantumLanguageModel {
605 pub fn build_vocabulary(&mut self, texts: &[String]) -> Result<usize> {
607 let vocab_size = texts
610 .iter()
611 .flat_map(|text| text.split_whitespace())
612 .collect::<std::collections::HashSet<_>>()
613 .len();
614
615 Ok(vocab_size)
616 }
617
618 pub fn train_embeddings(&mut self, texts: &[String]) -> Result<()> {
620 println!(
623 " Training embeddings for {} texts with strategy: {}",
624 texts.len(),
625 self.embedding_strategy
626 );
627
628 Ok(())
629 }
630
631 pub fn train(
633 &mut self,
634 texts: &[String],
635 labels: &[usize],
636 epochs: usize,
637 learning_rate: f64,
638 ) -> Result<()> {
639 let num_samples = texts.len();
641 let mut features = Array2::zeros((num_samples, self.embedding.dimension));
642
643 for (i, text) in texts.iter().enumerate() {
645 let feature_vec = text
647 .chars()
648 .enumerate()
649 .map(|(j, c)| (c as u32 % 8) as f64 / 8.0 + j as f64 * 0.001)
650 .take(self.embedding.dimension)
651 .collect::<Vec<_>>();
652
653 for (j, &val) in feature_vec
654 .iter()
655 .enumerate()
656 .take(self.embedding.dimension)
657 {
658 if j < features.ncols() {
659 features[[i, j]] = val;
660 }
661 }
662 }
663
664 let y_train = Array1::from_vec(labels.iter().map(|&l| l as f64).collect());
666
667 self.qnn
669 .train_1d(&features, &y_train, epochs, learning_rate)?;
670
671 Ok(())
672 }
673
674 pub fn classify(&self, text: &str) -> Result<(String, f64)> {
676 let hash = text.chars().map(|c| c as u32).sum::<u32>();
680 let class_idx = (hash % self.labels.len() as u32) as usize;
681 let confidence = 0.7 + 0.3 * (hash % 100) as f64 / 100.0;
682
683 Ok((self.labels[class_idx].clone(), confidence))
684 }
685}
686
687#[cfg(test)]
688mod regression_tests {
689 use super::*;
690
691 #[test]
698 fn fit_produces_context_correlated_embeddings_not_pure_noise() {
699 let corpus = [
700 "king queen throne royal palace",
701 "queen king throne royal crown",
702 "throne king queen royal power",
703 "banana apple fruit sweet tasty",
704 "apple banana fruit juicy sweet",
705 "fruit apple banana tasty juicy",
706 ];
707
708 let mut embedding = WordEmbedding::new(EmbeddingStrategy::Word2Vec, 64);
709 embedding.fit(&corpus).expect("fit should succeed");
710
711 assert!(!embedding.vocabulary.is_empty());
712 assert!(embedding.get_embedding("king").is_some());
713 assert!(embedding.get_embedding("apple").is_some());
714
715 let cosine = |a: &Array1<f64>, b: &Array1<f64>| -> f64 {
716 let dot = a.dot(b);
717 let norm_a = a.dot(a).sqrt();
718 let norm_b = b.dot(b).sqrt();
719 if norm_a > 1e-12 && norm_b > 1e-12 {
720 dot / (norm_a * norm_b)
721 } else {
722 0.0
723 }
724 };
725
726 let king = embedding.get_embedding("king").expect("king embedded");
727 let queen = embedding.get_embedding("queen").expect("queen embedded");
728 let apple = embedding.get_embedding("apple").expect("apple embedded");
729
730 let king_queen_similarity = cosine(king, queen);
736 let king_apple_similarity = cosine(king, apple);
737 assert!(
738 king_queen_similarity > king_apple_similarity,
739 "expected king~queen similarity ({king_queen_similarity}) to exceed \
740 king~apple similarity ({king_apple_similarity})"
741 );
742
743 let mut embedding2 = WordEmbedding::new(EmbeddingStrategy::Word2Vec, 64);
746 embedding2.fit(&corpus).expect("fit should succeed");
747 assert_eq!(embedding.vocabulary, embedding2.vocabulary);
748 }
749
750 #[test]
751 fn fit_on_empty_corpus_yields_empty_vocabulary_and_no_panic() {
752 let mut embedding = WordEmbedding::new(EmbeddingStrategy::BagOfWords, 16);
753 embedding
754 .fit(&[])
755 .expect("fit on empty corpus should succeed");
756 assert!(embedding.vocabulary.is_empty());
757 assert!(embedding.embeddings.is_empty());
758 }
759}