emotion_classifier / src /cuml_trainer.py
fioriclass's picture
correction bug metrics bien implémentés
89f0e63
raw
history blame
8.51 kB
# ===========================
# Fichier: cuml_trainer.py
# ===========================
from abc import ABC, abstractmethod
from typing import Optional, Union, Tuple
import cupy as cp
from scipy.sparse import csr_matrix
import cudf
from cuml.model_selection import train_test_split
from config import Config
from base_trainer import BaseTrainer
from interfaces.vectorizer import Vectorizer
class CuMLTrainer(BaseTrainer, ABC):
"""
Classe abstraite, héritée de BaseTrainer, représentant un entraîneur
basé sur la librairie cuML. Elle ajoute notamment le concept de vectoriseur
et force le passage de la matrice d'entrée en cupy.ndarray pour la plupart
des opérations.
Attributs:
vectorizer (Vectorizer): Objet responsable de la vectorisation du texte.
"""
def __init__(self, config: Config, data_path: str,
target_column: str) -> None:
"""
Initialise un CuMLTrainer avec la configuration.
Appelle également le constructeur de BaseTrainer.
:param config: Configuration globale du système.
:param data_path: Chemin vers le fichier de données.
:param target_column: Nom de la colonne cible dans les données.
"""
super().__init__(config, data_path, target_column)
self.vectorizer: Vectorizer = None
self.classifier: object = None # Déjà dans BaseTrainer, mais redéfini pour clarté
# Attributs pour stocker les données splittées (texte brut)
self.X_train_text: Optional[cudf.Series] = None
self.X_val_text: Optional[cudf.Series] = None
self.X_test_text: Optional[cudf.Series] = None
self.y_train: Optional[cp.ndarray] = None
self.y_val: Optional[cp.ndarray] = None
self.y_test: Optional[cp.ndarray] = None
# Attributs pour stocker les données vectorisées
self.X_train_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
self.X_val_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
self.X_test_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
@abstractmethod
def build_components(self) -> None:
"""
Méthode abstraite. Instancie concrètement le vectorizer et le classifieur,
selon la configuration (ex. 'svm', 'random_forest', etc.).
"""
pass
def _load_and_split_data(self, test_size=0.2, val_size=0.5, random_state=42) -> None:
"""
Charge les données depuis data_path, les sépare en features/labels,
et les divise en ensembles train/validation/test (80/10/10 par défaut).
Stocke les résultats dans les attributs de l'instance.
"""
if self.X_train_text is not None: # Évite de recharger/resplitter
return
data = cudf.read_csv(self.data_path)
# Identification et concaténation des features
feature_columns = [col for col in data.columns if col != self.target_column]
if not feature_columns:
raise ValueError("Aucune colonne de feature trouvée.")
texts_concatenated = data[feature_columns].astype(str).agg(' '.join, axis=1)
labels = data[self.target_column].astype(self._get_label_dtype()).values
# Premier split: 80% train, 20% temp (pour val+test)
X_train, X_temp, y_train, y_temp = train_test_split(
texts_concatenated, labels, test_size=test_size, random_state=random_state, stratify=labels
)
# Deuxième split: 50% validation, 50% test sur l'ensemble temp
# (val_size=0.5 sur 20% donne 10% du total pour val et 10% pour test)
# Utilisation de stratify=y_temp pour maintenir la distribution des classes
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=val_size, random_state=random_state, stratify=y_temp
)
# Stockage des résultats
self.X_train_text = X_train
self.X_val_text = X_val
self.X_test_text = X_test
self.y_train = y_train
self.y_val = y_val
self.y_test = y_test
def train(self) -> None:
"""
Entraîne le classifieur sur l'ensemble d'entraînement après vectorisation.
Vectorise également les ensembles de validation et de test.
"""
self._load_and_split_data() # Assure que les données sont chargées et splittées
if self.vectorizer is None or self.classifier is None:
raise RuntimeError("Les composants (vectorizer, classifier) doivent être construits avant l'entraînement. Appelez build_components().")
# fit_transform sur l'entraînement
self.X_train_vec = self.vectorizer.fit_transform(self.X_train_text)
# transform sur validation et test
self.X_val_vec = self.vectorizer.transform(self.X_val_text)
self.X_test_vec = self.vectorizer.transform(self.X_test_text)
# Préparation pour cuML (conversion en dense si nécessaire)
X_train_prepared = self._prepare_input_for_fit(self.X_train_vec)
self.classifier.fit(X_train_prepared, self.y_train)
def evaluate(self, use_validation_set=False) -> dict:
"""
Évalue le classifieur sur l'ensemble de test (par défaut) ou de validation.
:param use_validation_set: Si True, évalue sur l'ensemble de validation.
Sinon (défaut), évalue sur l'ensemble de test.
:return: Dictionnaire de métriques.
"""
if self.X_test_vec is None or self.y_test is None or self.X_val_vec is None or self.y_val is None:
raise RuntimeError("Les données doivent être chargées, splittées et vectorisées avant l'évaluation. Appelez train().")
if self.classifier is None:
raise RuntimeError("Le classifieur doit être entraîné avant l'évaluation. Appelez train().")
if use_validation_set:
X_eval_vec = self.X_val_vec
y_true = self.y_val
dataset_name = "validation"
else:
X_eval_vec = self.X_test_vec
y_true = self.y_test
dataset_name = "test"
# Préparation pour cuML et prédiction
X_eval_prepared = self._prepare_input_for_predict(X_eval_vec)
y_pred = self.classifier.predict(X_eval_prepared)
# Récupérer les probabilités pour la classe positive
y_proba = self._get_positive_probabilities(X_eval_prepared)
# Calcul des métriques
prefix = f"{self.config.model.type.lower()}_{dataset_name}"
if self.metrics_calculator is None:
# Initialisation par défaut si non fait ailleurs
from interfaces.metrics_calculator import DefaultMetricsCalculator
self.metrics_calculator = DefaultMetricsCalculator()
# Utiliser la méthode calculate_and_log pour la classification binaire
metrics = self.metrics_calculator.calculate_and_log(
y_true=y_true,
y_pred=y_pred,
y_proba=y_proba,
prefix=prefix
)
return metrics
# Note: La méthode optimize_if_needed appelle l'optimiseur qui, à son tour,
# peut appeler train() et evaluate(). Il faudra s'assurer que l'optimiseur
# utilise evaluate(use_validation_set=True) pour l'évaluation des hyperparamètres.
# Cela pourrait nécessiter une modification des classes Optimizer ou de la façon
# dont la fonction objectif est définie dans l'optimiseur.
def _prepare_input_for_fit(self, X: Union[cp.ndarray,
csr_matrix]) -> cp.ndarray:
"""
Convertit, si nécessaire, la matrice en cupy.ndarray pour l'entraînement.
:param X: Données d'entraînement (cupy.ndarray ou scipy.sparse.csr_matrix).
:return: Données converties en cupy.ndarray, pour compatibilité cuML.
"""
if isinstance(X, csr_matrix):
return cp.asarray(X.toarray())
return X # c'est déjà cupy.ndarray
def _prepare_input_for_predict(
self, X: Union[cp.ndarray, csr_matrix]) -> cp.ndarray:
"""
Convertit, si nécessaire, la matrice en cupy.ndarray pour la prédiction.
:param X: Données de prédiction (cupy.ndarray ou scipy.sparse.csr_matrix).
:return: Données converties en cupy.ndarray, pour compatibilité cuML.
"""
if isinstance(X, csr_matrix):
return cp.asarray(X.toarray())
return X