""" Classifieur mock pour le développement et les tests UI. Ce classifieur génère des prédictions simulées réalistes sans nécessiter de vrais modèles ML. Il est utilisé pour: - Développer et tester l'interface utilisateur - Démontrer le fonctionnement de l'application - Servir de fallback si les vrais modèles ne sont pas disponibles Supporte plusieurs modèles simulés pour texte et image: - Texte: TF-IDF + SVM, TF-IDF + Random Forest, CamemBERT - Image: ResNet50 + SVM, ResNet50 + Random Forest, VGG16 + SVM """ import hashlib import numpy as np from typing import Optional, Dict, List, Tuple from PIL import Image from dataclasses import dataclass from .model_interface import BaseClassifier, ClassificationResult from .category_mapping import CATEGORY_CODES, get_category_name # ============================================================================= # Configuration des modèles disponibles # ============================================================================= @dataclass class ModelConfig: """Configuration d'un modèle simulé.""" name: str short_name: str description: str base_confidence: float # Confiance de base (moyenne) confidence_std: float # Écart-type de la confiance seed_offset: int # Offset pour différencier les résultats color: str # Couleur pour les graphiques # Modèles de texte disponibles TEXT_MODELS: Dict[str, ModelConfig] = { "tfidf_svm": ModelConfig( name="TF-IDF + SVM", short_name="TF-IDF/SVM", description="Vectorisation TF-IDF avec classifieur SVM linéaire", base_confidence=0.78, confidence_std=0.12, seed_offset=100, color="#2196F3" # Bleu ), "tfidf_rf": ModelConfig( name="TF-IDF + Random Forest", short_name="TF-IDF/RF", description="Vectorisation TF-IDF avec Random Forest (100 arbres)", base_confidence=0.75, confidence_std=0.15, seed_offset=200, color="#4CAF50" # Vert ), "camembert": ModelConfig( name="CamemBERT", short_name="CamemBERT", description="Modèle transformer pré-entraîné sur le français", base_confidence=0.85, confidence_std=0.08, seed_offset=300, color="#9C27B0" # Violet ), } # Modèles d'image disponibles IMAGE_MODELS: Dict[str, ModelConfig] = { "resnet50_svm": ModelConfig( name="ResNet50 + SVM", short_name="ResNet50/SVM", description="Features ResNet50 (2048D) avec classifieur SVM", base_confidence=0.72, confidence_std=0.14, seed_offset=400, color="#FF5722" # Orange ), "resnet50_rf": ModelConfig( name="ResNet50 + Random Forest", short_name="ResNet50/RF", description="Features ResNet50 avec Random Forest (200 arbres)", base_confidence=0.70, confidence_std=0.16, seed_offset=500, color="#795548" # Marron ), "vgg16_svm": ModelConfig( name="VGG16 + SVM", short_name="VGG16/SVM", description="Features VGG16 (4096D) avec classifieur SVM", base_confidence=0.68, confidence_std=0.15, seed_offset=600, color="#607D8B" # Gris-bleu ), } def get_available_text_models() -> Dict[str, ModelConfig]: """Retourne les modèles de texte disponibles.""" return TEXT_MODELS def get_available_image_models() -> Dict[str, ModelConfig]: """Retourne les modèles d'image disponibles.""" return IMAGE_MODELS # ============================================================================= # Classifieur Mock de base # ============================================================================= class MockClassifier(BaseClassifier): """ Classifieur simulé pour le développement de l'interface. Ce classifieur génère des prédictions pseudo-aléatoires mais déterministes basées sur le hash des entrées. """ def __init__(self, seed: int = 42, model_config: Optional[ModelConfig] = None): """ Initialise le classifieur mock. Args: seed: Graine de base pour la génération aléatoire model_config: Configuration du modèle simulé (optionnel) """ self._ready = True self._seed = seed self._model_config = model_config def predict( self, image: Optional[Image.Image] = None, text: Optional[str] = None, top_k: int = 5 ) -> ClassificationResult: """ Génère une prédiction simulée basée sur les entrées. """ if image is None and (text is None or text.strip() == ""): raise ValueError("Au moins une image ou un texte est requis") # Générer une graine déterministe basée sur les entrées seed_offset = self._model_config.seed_offset if self._model_config else 0 hash_input = self._generate_hash(image, text, seed_offset) rng = np.random.RandomState(hash_input) # Générer des probabilités avec les caractéristiques du modèle probabilities = self._generate_probabilities(rng) # Déterminer la source if image is not None and text and text.strip(): source = "mock_multimodal" elif image is not None: source = "mock_image" else: source = "mock_text" # Ajouter le nom du modèle si configuré if self._model_config: source = f"{source}_{self._model_config.short_name}" # Construire le résultat top_predictions = self._probabilities_to_predictions(probabilities, top_k) best_category, best_confidence = top_predictions[0] return ClassificationResult( category=best_category, confidence=best_confidence, top_k_predictions=top_predictions, source=source, raw_probabilities=probabilities ) def _generate_probabilities(self, rng: np.random.RandomState) -> np.ndarray: """Génère des probabilités avec les caractéristiques du modèle.""" alpha = np.ones(self.NUM_CLASSES) * 0.5 peak_indices = rng.choice(self.NUM_CLASSES, size=3, replace=False) if self._model_config: # Ajuster selon la confiance de base du modèle peak_strength = 2.0 + (self._model_config.base_confidence - 0.7) * 10 alpha[peak_indices] = rng.uniform(peak_strength, peak_strength + 3.0, size=3) else: alpha[peak_indices] = rng.uniform(2.0, 5.0, size=3) probabilities = rng.dirichlet(alpha) # Ajuster la confiance maximale selon le modèle if self._model_config: max_idx = np.argmax(probabilities) target_conf = np.clip( rng.normal(self._model_config.base_confidence, self._model_config.confidence_std), 0.4, 0.98 ) # Rescale pour atteindre la confiance cible current_max = probabilities[max_idx] if current_max > 0: scale_factor = target_conf / current_max probabilities[max_idx] = target_conf # Redistribuer le reste other_mask = np.ones(len(probabilities), dtype=bool) other_mask[max_idx] = False remaining = 1.0 - target_conf if probabilities[other_mask].sum() > 0: probabilities[other_mask] *= remaining / probabilities[other_mask].sum() return probabilities def load_model(self, path: str) -> None: """Simule le chargement d'un modèle.""" self._ready = True @property def is_ready(self) -> bool: """Le mock est toujours prêt.""" return self._ready @property def model_config(self) -> Optional[ModelConfig]: """Retourne la configuration du modèle.""" return self._model_config def _generate_hash( self, image: Optional[Image.Image], text: Optional[str], seed_offset: int = 0 ) -> int: """Génère un hash déterministe à partir des entrées.""" hash_parts = [str(self._seed + seed_offset)] if image is not None: hash_parts.append(f"{image.size}") small = image.resize((8, 8)).convert("L") hash_parts.append(small.tobytes().hex()[:32]) if text and text.strip(): hash_parts.append(text.strip()[:200]) combined = "|".join(hash_parts) hash_bytes = hashlib.md5(combined.encode()).digest() return int.from_bytes(hash_bytes[:4], byteorder="big") # ============================================================================= # Classifieur de démonstration avec mots-clés # ============================================================================= class DemoClassifier(MockClassifier): """ Classifieur de démonstration avec des prédictions prédéfinies pour certains mots-clés, permettant des démos contrôlées. """ KEYWORD_PREDICTIONS = { "piscine": ("2583", 0.92), "pool": ("2583", 0.88), "livre": ("2403", 0.85), "book": ("2403", 0.82), "harry potter": ("2403", 0.94), "roman": ("2403", 0.80), "jeu vidéo": ("2462", 0.90), "console": ("2462", 0.87), "playstation": ("2462", 0.95), "xbox": ("2462", 0.94), "nintendo": ("2462", 0.93), "figurine": ("1281", 0.88), "funko": ("1281", 0.91), "pokemon": ("1280", 0.91), "jouet": ("1280", 0.84), "lego": ("1280", 0.93), "bébé": ("1320", 0.86), "meuble": ("1560", 0.83), "jardin": ("2582", 0.89), "tondeuse": ("2585", 0.87), "outil": ("2585", 0.85), "iphone": ("2583", 0.90), "smartphone": ("2583", 0.88), "téléphone": ("2583", 0.85), "coque": ("2583", 0.82), "robe": ("1920", 0.86), "vêtement": ("1920", 0.83), "maquillage": ("1301", 0.89), "parfum": ("1301", 0.87), "bougie": ("1302", 0.84), } def __init__(self, seed: int = 42, model_config: Optional[ModelConfig] = None): super().__init__(seed, model_config) # Ajuster les confiances selon le modèle self._adjusted_predictions = self._adjust_predictions_for_model() def _adjust_predictions_for_model(self) -> Dict[str, Tuple[str, float]]: """Ajuste les prédictions selon les caractéristiques du modèle.""" if not self._model_config: return self.KEYWORD_PREDICTIONS.copy() adjusted = {} rng = np.random.RandomState(self._model_config.seed_offset) for keyword, (category, base_conf) in self.KEYWORD_PREDICTIONS.items(): # Ajuster la confiance avec une variation selon le modèle variation = rng.normal(0, 0.05) model_factor = self._model_config.base_confidence / 0.80 # Normaliser sur 0.80 new_conf = np.clip(base_conf * model_factor + variation, 0.5, 0.98) adjusted[keyword] = (category, new_conf) return adjusted def predict( self, image: Optional[Image.Image] = None, text: Optional[str] = None, top_k: int = 5 ) -> ClassificationResult: """Génère une prédiction basée sur des mots-clés ou le mock standard.""" predictions_to_use = self._adjusted_predictions if self._model_config else self.KEYWORD_PREDICTIONS if text: text_lower = text.lower() for keyword, (category, confidence) in predictions_to_use.items(): if keyword in text_lower: probabilities = self._generate_keyword_probabilities( category, confidence ) top_predictions = self._probabilities_to_predictions( probabilities, top_k ) source = "demo" if self._model_config: source = f"demo_{self._model_config.short_name}" return ClassificationResult( category=category, confidence=confidence, top_k_predictions=top_predictions, source=source, raw_probabilities=probabilities ) return super().predict(image, text, top_k) def _generate_keyword_probabilities( self, main_category: str, main_confidence: float ) -> np.ndarray: """Génère des probabilités cohérentes avec une prédiction principale.""" seed = self._model_config.seed_offset if self._model_config else 42 rng = np.random.RandomState(seed) probabilities = rng.dirichlet(np.ones(self.NUM_CLASSES) * 0.3) main_idx = CATEGORY_CODES.index(main_category) remaining = 1.0 - main_confidence other_sum = probabilities.sum() - probabilities[main_idx] if other_sum > 0: scale = remaining / other_sum probabilities *= scale probabilities[main_idx] = main_confidence return probabilities # ============================================================================= # Factory pour créer des classifieurs avec différents modèles # ============================================================================= class MultiModelClassifier: """ Gestionnaire de plusieurs modèles pour comparaison. Permet de créer et gérer plusieurs classifieurs avec différentes configurations pour comparer leurs performances. """ def __init__(self): self._text_classifiers: Dict[str, DemoClassifier] = {} self._image_classifiers: Dict[str, DemoClassifier] = {} self._initialize_classifiers() def _initialize_classifiers(self): """Initialise tous les classifieurs disponibles.""" for model_id, config in TEXT_MODELS.items(): self._text_classifiers[model_id] = DemoClassifier(model_config=config) for model_id, config in IMAGE_MODELS.items(): self._image_classifiers[model_id] = DemoClassifier(model_config=config) def get_text_classifier(self, model_id: str) -> DemoClassifier: """Retourne un classifieur texte spécifique.""" if model_id not in self._text_classifiers: raise ValueError(f"Modèle texte inconnu: {model_id}") return self._text_classifiers[model_id] def get_image_classifier(self, model_id: str) -> DemoClassifier: """Retourne un classifieur image spécifique.""" if model_id not in self._image_classifiers: raise ValueError(f"Modèle image inconnu: {model_id}") return self._image_classifiers[model_id] def predict_all_text_models( self, text: str, top_k: int = 5 ) -> Dict[str, ClassificationResult]: """ Exécute tous les modèles texte sur la même entrée. Returns: Dict avec model_id -> ClassificationResult """ results = {} for model_id, classifier in self._text_classifiers.items(): results[model_id] = classifier.predict(text=text, top_k=top_k) return results def predict_all_image_models( self, image: Image.Image, top_k: int = 5 ) -> Dict[str, ClassificationResult]: """ Exécute tous les modèles image sur la même entrée. Returns: Dict avec model_id -> ClassificationResult """ results = {} for model_id, classifier in self._image_classifiers.items(): results[model_id] = classifier.predict(image=image, top_k=top_k) return results def get_comparison_metrics( self, results: Dict[str, ClassificationResult] ) -> Dict[str, any]: """ Calcule des métriques de comparaison entre modèles. Returns: Dict avec métriques agrégées """ confidences = {k: r.confidence for k, r in results.items()} categories = {k: r.category for k, r in results.items()} # Trouver le consensus from collections import Counter category_counts = Counter(categories.values()) consensus_category = category_counts.most_common(1)[0][0] agreement_ratio = category_counts[consensus_category] / len(results) return { "confidences": confidences, "categories": categories, "consensus_category": consensus_category, "agreement_ratio": agreement_ratio, "best_model": max(confidences, key=confidences.get), "avg_confidence": np.mean(list(confidences.values())), "std_confidence": np.std(list(confidences.values())), }