Spaces:
Running
Running
| """ | |
| 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 | |
| # ============================================================================= | |
| 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 | |
| def is_ready(self) -> bool: | |
| """Le mock est toujours prêt.""" | |
| return self._ready | |
| 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())), | |
| } | |