Spaces:
Sleeping
Sleeping
Commit
·
3633596
1
Parent(s):
a7790b3
grosse mise à jour sur le train test eval
Browse files- src/cuml_trainer.py +143 -42
- src/interfaces/metrics_calculator.py +109 -61
- src/trainers/huggingface/huggingface_transformer_trainer.py +154 -23
src/cuml_trainer.py
CHANGED
@@ -3,10 +3,12 @@
|
|
3 |
# ===========================
|
4 |
|
5 |
from abc import ABC, abstractmethod
|
6 |
-
from typing import Union
|
7 |
import cupy as cp
|
8 |
from scipy.sparse import csr_matrix
|
9 |
import cudf
|
|
|
|
|
10 |
|
11 |
from config import Config
|
12 |
from base_trainer import BaseTrainer
|
@@ -36,8 +38,21 @@ class CuMLTrainer(BaseTrainer, ABC):
|
|
36 |
"""
|
37 |
super().__init__(config, data_path, target_column)
|
38 |
self.vectorizer: Vectorizer = None
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
@abstractmethod
|
43 |
def build_components(self) -> None:
|
@@ -47,66 +62,152 @@ class CuMLTrainer(BaseTrainer, ABC):
|
|
47 |
"""
|
48 |
pass
|
49 |
|
50 |
-
def
|
51 |
"""
|
52 |
-
|
53 |
-
|
|
|
54 |
"""
|
55 |
-
#
|
|
|
|
|
56 |
|
|
|
57 |
data = cudf.read_csv(self.data_path)
|
58 |
|
59 |
-
# Identification
|
60 |
feature_columns = [col for col in data.columns if col != self.target_column]
|
61 |
if not feature_columns:
|
62 |
-
raise ValueError("Aucune colonne de feature trouvée.
|
63 |
-
|
64 |
-
# Concaténation des features (même si une seule, pour la cohérence et l'évolutivité)
|
65 |
-
# Convertit en string et ajoute un espace comme séparateur si plusieurs colonnes existent.
|
66 |
texts_concatenated = data[feature_columns].astype(str).agg(' '.join, axis=1)
|
67 |
-
labels = data[self.target_column].astype(self._get_label_dtype()).values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
-
#
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
self.
|
|
|
|
|
74 |
|
75 |
-
|
|
|
|
|
|
|
76 |
"""
|
77 |
-
|
78 |
-
|
79 |
"""
|
80 |
-
#
|
81 |
-
data = cudf.read_csv(self.data_path)
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
if not feature_columns:
|
86 |
-
raise ValueError("Aucune colonne de feature trouvée pour l'évaluation.")
|
87 |
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
-
# Vectorisation et prédiction
|
93 |
-
X = self.vectorizer.transform(texts_concatenated)
|
94 |
-
X_prepared = self._prepare_input_for_predict(X)
|
95 |
-
y_pred = self.classifier.predict(X_prepared)
|
96 |
|
97 |
# Calcul et logging des métriques
|
98 |
-
prefix = self.config.model.type.lower()
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
|
105 |
# Afficher les résultats
|
106 |
-
|
107 |
-
|
108 |
return metrics
|
109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
def _prepare_input_for_fit(self, X: Union[cp.ndarray,
|
111 |
csr_matrix]) -> cp.ndarray:
|
112 |
"""
|
|
|
3 |
# ===========================
|
4 |
|
5 |
from abc import ABC, abstractmethod
|
6 |
+
from typing import Optional, Union, Tuple
|
7 |
import cupy as cp
|
8 |
from scipy.sparse import csr_matrix
|
9 |
import cudf
|
10 |
+
from cuml.model_selection import train_test_split
|
11 |
+
import logging
|
12 |
|
13 |
from config import Config
|
14 |
from base_trainer import BaseTrainer
|
|
|
38 |
"""
|
39 |
super().__init__(config, data_path, target_column)
|
40 |
self.vectorizer: Vectorizer = None
|
41 |
+
self.classifier: object = None # Déjà dans BaseTrainer, mais redéfini pour clarté
|
42 |
+
self.logger = logging.getLogger(__name__)
|
43 |
+
|
44 |
+
# Attributs pour stocker les données splittées (texte brut)
|
45 |
+
self.X_train_text: Optional[cudf.Series] = None
|
46 |
+
self.X_val_text: Optional[cudf.Series] = None
|
47 |
+
self.X_test_text: Optional[cudf.Series] = None
|
48 |
+
self.y_train: Optional[cp.ndarray] = None
|
49 |
+
self.y_val: Optional[cp.ndarray] = None
|
50 |
+
self.y_test: Optional[cp.ndarray] = None
|
51 |
+
|
52 |
+
# Attributs pour stocker les données vectorisées
|
53 |
+
self.X_train_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
|
54 |
+
self.X_val_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
|
55 |
+
self.X_test_vec: Optional[Union[cp.ndarray, csr_matrix]] = None
|
56 |
|
57 |
@abstractmethod
|
58 |
def build_components(self) -> None:
|
|
|
62 |
"""
|
63 |
pass
|
64 |
|
65 |
+
def _load_and_split_data(self, test_size=0.2, val_size=0.5, random_state=42) -> None:
|
66 |
"""
|
67 |
+
Charge les données depuis data_path, les sépare en features/labels,
|
68 |
+
et les divise en ensembles train/validation/test (80/10/10 par défaut).
|
69 |
+
Stocke les résultats dans les attributs de l'instance.
|
70 |
"""
|
71 |
+
if self.X_train_text is not None: # Évite de recharger/resplitter
|
72 |
+
self.logger.info("Données déjà chargées et splittées.")
|
73 |
+
return
|
74 |
|
75 |
+
self.logger.info(f"Chargement des données depuis {self.data_path}...")
|
76 |
data = cudf.read_csv(self.data_path)
|
77 |
|
78 |
+
# Identification et concaténation des features
|
79 |
feature_columns = [col for col in data.columns if col != self.target_column]
|
80 |
if not feature_columns:
|
81 |
+
raise ValueError("Aucune colonne de feature trouvée.")
|
|
|
|
|
|
|
82 |
texts_concatenated = data[feature_columns].astype(str).agg(' '.join, axis=1)
|
83 |
+
labels = data[self.target_column].astype(self._get_label_dtype()).values
|
84 |
+
|
85 |
+
self.logger.info("Séparation des données en train/validation/test (80/10/10)...")
|
86 |
+
# Premier split: 80% train, 20% temp (pour val+test)
|
87 |
+
X_train, X_temp, y_train, y_temp = train_test_split(
|
88 |
+
texts_concatenated, labels, test_size=test_size, random_state=random_state, stratify=labels
|
89 |
+
)
|
90 |
+
|
91 |
+
# Deuxième split: 50% validation, 50% test sur l'ensemble temp
|
92 |
+
# (val_size=0.5 sur 20% donne 10% du total pour val et 10% pour test)
|
93 |
+
# Utilisation de stratify=y_temp pour maintenir la distribution des classes
|
94 |
+
X_val, X_test, y_val, y_test = train_test_split(
|
95 |
+
X_temp, y_temp, test_size=val_size, random_state=random_state, stratify=y_temp
|
96 |
+
)
|
97 |
|
98 |
+
# Stockage des résultats
|
99 |
+
self.X_train_text = X_train
|
100 |
+
self.X_val_text = X_val
|
101 |
+
self.X_test_text = X_test
|
102 |
+
self.y_train = y_train
|
103 |
+
self.y_val = y_val
|
104 |
+
self.y_test = y_test
|
105 |
|
106 |
+
self.logger.info(f"Taille Train: {len(self.X_train_text)}, Val: {len(self.X_val_text)}, Test: {len(self.X_test_text)}")
|
107 |
+
|
108 |
+
|
109 |
+
def train(self) -> None:
|
110 |
"""
|
111 |
+
Entraîne le classifieur sur l'ensemble d'entraînement après vectorisation.
|
112 |
+
Vectorise également les ensembles de validation et de test.
|
113 |
"""
|
114 |
+
self._load_and_split_data() # Assure que les données sont chargées et splittées
|
|
|
115 |
|
116 |
+
if self.vectorizer is None or self.classifier is None:
|
117 |
+
raise RuntimeError("Les composants (vectorizer, classifier) doivent être construits avant l'entraînement. Appelez build_components().")
|
|
|
|
|
118 |
|
119 |
+
self.logger.info("Vectorisation des données textuelles...")
|
120 |
+
# fit_transform sur l'entraînement
|
121 |
+
self.X_train_vec = self.vectorizer.fit_transform(self.X_train_text)
|
122 |
+
# transform sur validation et test
|
123 |
+
self.X_val_vec = self.vectorizer.transform(self.X_val_text)
|
124 |
+
self.X_test_vec = self.vectorizer.transform(self.X_test_text)
|
125 |
+
|
126 |
+
# Préparation pour cuML (conversion en dense si nécessaire)
|
127 |
+
X_train_prepared = self._prepare_input_for_fit(self.X_train_vec)
|
128 |
+
|
129 |
+
self.logger.info("Entraînement du modèle...")
|
130 |
+
self.classifier.fit(X_train_prepared, self.y_train)
|
131 |
+
self.logger.info("Entraînement terminé.")
|
132 |
+
|
133 |
+
|
134 |
+
def evaluate(self, use_validation_set=False) -> dict:
|
135 |
+
"""
|
136 |
+
Évalue le classifieur sur l'ensemble de test (par défaut) ou de validation.
|
137 |
+
|
138 |
+
:param use_validation_set: Si True, évalue sur l'ensemble de validation.
|
139 |
+
Sinon (défaut), évalue sur l'ensemble de test.
|
140 |
+
:return: Dictionnaire de métriques.
|
141 |
+
"""
|
142 |
+
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:
|
143 |
+
raise RuntimeError("Les données doivent être chargées, splittées et vectorisées avant l'évaluation. Appelez train().")
|
144 |
+
if self.classifier is None:
|
145 |
+
raise RuntimeError("Le classifieur doit être entraîné avant l'évaluation. Appelez train().")
|
146 |
+
|
147 |
+
if use_validation_set:
|
148 |
+
self.logger.info("Évaluation sur l'ensemble de validation...")
|
149 |
+
X_eval_vec = self.X_val_vec
|
150 |
+
y_true = self.y_val
|
151 |
+
dataset_name = "validation"
|
152 |
+
else:
|
153 |
+
self.logger.info("Évaluation sur l'ensemble de test...")
|
154 |
+
X_eval_vec = self.X_test_vec
|
155 |
+
y_true = self.y_test
|
156 |
+
dataset_name = "test"
|
157 |
+
|
158 |
+
# Préparation pour cuML et prédiction
|
159 |
+
X_eval_prepared = self._prepare_input_for_predict(X_eval_vec)
|
160 |
+
y_pred = self.classifier.predict(X_eval_prepared)
|
161 |
+
|
162 |
+
# Essayer de récupérer les probabilités
|
163 |
+
y_proba = None
|
164 |
+
try:
|
165 |
+
# Utilise la méthode _get_positive_probabilities définie dans BaseTrainer
|
166 |
+
# et potentiellement surchargée dans les sous-classes (comme SvmTrainer)
|
167 |
+
y_proba = self._get_positive_probabilities(X_eval_prepared)
|
168 |
+
except NotImplementedError:
|
169 |
+
self.logger.warning("La méthode _get_positive_probabilities n'est pas implémentée pour ce modèle, AUC pourrait être moins précis ou indisponible.")
|
170 |
+
except Exception as e:
|
171 |
+
self.logger.warning(f"Erreur lors de la récupération des probabilités : {e}")
|
172 |
|
|
|
|
|
|
|
|
|
173 |
|
174 |
# Calcul et logging des métriques
|
175 |
+
prefix = f"{self.config.model.type.lower()}_{dataset_name}"
|
176 |
+
if self.metrics_calculator is None:
|
177 |
+
# Initialisation par défaut si non fait ailleurs
|
178 |
+
from interfaces.metrics_calculator import DefaultMetricsCalculator # Utiliser l'implémentation par défaut
|
179 |
+
self.metrics_calculator = DefaultMetricsCalculator()
|
180 |
+
|
181 |
+
# Déterminer si binaire ou multiclasse
|
182 |
+
num_classes = len(cp.unique(y_true))
|
183 |
+
self.logger.info(f"Nombre de classes détectées dans y_true ({dataset_name}): {num_classes}")
|
184 |
+
|
185 |
+
if num_classes <= 2:
|
186 |
+
metrics = self.metrics_calculator.calculate_and_log(
|
187 |
+
y_true=y_true,
|
188 |
+
y_pred=y_pred,
|
189 |
+
y_proba=y_proba, # Passer les probas
|
190 |
+
prefix=prefix
|
191 |
+
)
|
192 |
+
else:
|
193 |
+
metrics = self.metrics_calculator.calculate_and_log_multiclass(
|
194 |
+
y_true=y_true,
|
195 |
+
y_pred=y_pred,
|
196 |
+
y_proba=y_proba, # Passer les probas
|
197 |
+
prefix=prefix
|
198 |
+
)
|
199 |
|
200 |
# Afficher les résultats
|
201 |
+
self.logger.info(f"Métriques d'évaluation ({dataset_name}): {metrics}")
|
202 |
+
|
203 |
return metrics
|
204 |
|
205 |
+
# Note: La méthode optimize_if_needed appelle l'optimiseur qui, à son tour,
|
206 |
+
# peut appeler train() et evaluate(). Il faudra s'assurer que l'optimiseur
|
207 |
+
# utilise evaluate(use_validation_set=True) pour l'évaluation des hyperparamètres.
|
208 |
+
# Cela pourrait nécessiter une modification des classes Optimizer ou de la façon
|
209 |
+
# dont la fonction objectif est définie dans l'optimiseur.
|
210 |
+
|
211 |
def _prepare_input_for_fit(self, X: Union[cp.ndarray,
|
212 |
csr_matrix]) -> cp.ndarray:
|
213 |
"""
|
src/interfaces/metrics_calculator.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1 |
import cupy as cp
|
2 |
-
import
|
3 |
-
|
4 |
-
|
5 |
-
from
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
class MetricsCalculator(Protocol):
|
8 |
"""
|
@@ -12,98 +17,141 @@ class MetricsCalculator(Protocol):
|
|
12 |
self,
|
13 |
y_true: cp.ndarray,
|
14 |
y_pred: cp.ndarray,
|
|
|
15 |
prefix: str
|
16 |
) -> Dict[str, float]:
|
17 |
"""
|
18 |
-
Calcule
|
19 |
"""
|
20 |
pass
|
21 |
-
|
22 |
def calculate_and_log_multiclass(
|
23 |
self,
|
24 |
y_true: cp.ndarray,
|
25 |
y_pred: cp.ndarray,
|
|
|
26 |
prefix: str
|
27 |
) -> Dict[str, float]:
|
28 |
"""
|
29 |
-
Calcule
|
30 |
"""
|
31 |
pass
|
32 |
|
33 |
-
logger = logging.getLogger(__name__)
|
34 |
|
35 |
class DefaultMetricsCalculator(MetricsCalculator):
|
36 |
"""
|
37 |
-
Implémentation concrète de MetricsCalculator
|
38 |
-
accuracy,
|
39 |
-
|
|
|
|
|
40 |
"""
|
41 |
|
42 |
def calculate_and_log(
|
43 |
self,
|
44 |
y_true: cp.ndarray,
|
45 |
y_pred: cp.ndarray,
|
|
|
46 |
prefix: str
|
47 |
) -> Dict[str, float]:
|
48 |
"""
|
49 |
-
Calcule
|
50 |
-
|
|
|
51 |
"""
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
acc = accuracy_score(y_true_np, y_pred_np)
|
56 |
-
prec = precision_score(y_true_np, y_pred_np, zero_division=0)
|
57 |
-
rec = recall_score(y_true_np, y_pred_np, zero_division=0)
|
58 |
-
f1 = f1_score(y_true_np, y_pred_np, zero_division=0)
|
59 |
-
|
60 |
-
# Calcul AUC pour un problème binaire (si y_pred est 0/1)
|
61 |
-
# On treat y_pred_np as our "probabilities" only if truly 0/1.
|
62 |
-
# In a real pipeline you might store probabilities separately.
|
63 |
try:
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
f"{prefix}
|
73 |
-
f"{prefix}
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
def calculate_and_log_multiclass(
|
79 |
self,
|
80 |
y_true: cp.ndarray,
|
81 |
y_pred: cp.ndarray,
|
|
|
82 |
prefix: str
|
83 |
) -> Dict[str, float]:
|
84 |
"""
|
85 |
-
Calcule
|
86 |
-
AUC-ROC
|
|
|
87 |
"""
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
f"{prefix}
|
106 |
-
f"{prefix}
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import cupy as cp
|
2 |
+
from typing import Dict, Protocol, Optional
|
3 |
+
import warnings
|
4 |
+
# Utiliser cuml.metrics
|
5 |
+
from cuml.metrics import accuracy_score, precision_recall_fscore_support, roc_auc_score
|
6 |
+
|
7 |
+
# Ignorer les avertissements (peut nécessiter ajustement si cuML utilise d'autres types)
|
8 |
+
# Attention: Masquer tous les warnings peut cacher des problèmes potentiels.
|
9 |
+
warnings.filterwarnings("ignore", category=Warning)
|
10 |
+
|
11 |
|
12 |
class MetricsCalculator(Protocol):
|
13 |
"""
|
|
|
17 |
self,
|
18 |
y_true: cp.ndarray,
|
19 |
y_pred: cp.ndarray,
|
20 |
+
y_proba: Optional[cp.ndarray], # Probabilités pour AUC
|
21 |
prefix: str
|
22 |
) -> Dict[str, float]:
|
23 |
"""
|
24 |
+
Calcule les métriques pour un problème binaire.
|
25 |
"""
|
26 |
pass
|
27 |
+
|
28 |
def calculate_and_log_multiclass(
|
29 |
self,
|
30 |
y_true: cp.ndarray,
|
31 |
y_pred: cp.ndarray,
|
32 |
+
y_proba: Optional[cp.ndarray], # Probabilités (potentiellement pour futures métriques)
|
33 |
prefix: str
|
34 |
) -> Dict[str, float]:
|
35 |
"""
|
36 |
+
Calcule les métriques pour un problème multiclasses.
|
37 |
"""
|
38 |
pass
|
39 |
|
|
|
40 |
|
41 |
class DefaultMetricsCalculator(MetricsCalculator):
|
42 |
"""
|
43 |
+
Implémentation concrète de MetricsCalculator utilisant cuML.
|
44 |
+
Calcule accuracy, et F1/precision/recall pondérés.
|
45 |
+
Calcule AUC-ROC pour les problèmes binaires *uniquement* si les probabilités sont fournies.
|
46 |
+
Ne calcule pas l'AUC-ROC pour les problèmes multiclasses (non supporté par cuml.metrics.roc_auc_score).
|
47 |
+
Retourne NaN pour les métriques non calculables.
|
48 |
"""
|
49 |
|
50 |
def calculate_and_log(
|
51 |
self,
|
52 |
y_true: cp.ndarray,
|
53 |
y_pred: cp.ndarray,
|
54 |
+
y_proba: Optional[cp.ndarray], # Probabilités requises pour AUC
|
55 |
prefix: str
|
56 |
) -> Dict[str, float]:
|
57 |
"""
|
58 |
+
Calcule les métriques pour un problème binaire.
|
59 |
+
Utilise y_proba pour AUC si disponible.
|
60 |
+
Utilise average='weighted' pour precision/recall/f1.
|
61 |
"""
|
62 |
+
metrics: Dict[str, float] = {}
|
63 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
try:
|
65 |
+
# Accuracy
|
66 |
+
acc = accuracy_score(y_true, y_pred)
|
67 |
+
metrics[f"{prefix}_accuracy"] = float(acc)
|
68 |
+
|
69 |
+
# Precision, Recall, F1 (Weighted)
|
70 |
+
prec, rec, f1, _ = precision_recall_fscore_support(
|
71 |
+
y_true, y_pred, average='weighted'
|
72 |
+
)
|
73 |
+
metrics[f"{prefix}_precision_weighted"] = float(prec)
|
74 |
+
metrics[f"{prefix}_recall_weighted"] = float(rec)
|
75 |
+
metrics[f"{prefix}_f1_weighted"] = float(f1)
|
76 |
+
|
77 |
+
except Exception:
|
78 |
+
# En cas d'erreur sur les métriques de base, remplir avec NaN
|
79 |
+
metrics.setdefault(f"{prefix}_accuracy", float('nan'))
|
80 |
+
metrics.setdefault(f"{prefix}_precision_weighted", float('nan'))
|
81 |
+
metrics.setdefault(f"{prefix}_recall_weighted", float('nan'))
|
82 |
+
metrics.setdefault(f"{prefix}_f1_weighted", float('nan'))
|
83 |
+
|
84 |
+
# AUC-ROC (Binary only, requires probabilities)
|
85 |
+
auc: float = float('nan') # Default to NaN
|
86 |
+
if y_proba is not None:
|
87 |
+
try:
|
88 |
+
# Ensure y_true and y_proba have compatible shapes and types
|
89 |
+
if y_true.dtype != cp.int32 and y_true.dtype != cp.int64:
|
90 |
+
y_true = y_true.astype(cp.int32)
|
91 |
+
|
92 |
+
# roc_auc_score expects probabilities of the positive class
|
93 |
+
if y_proba.ndim == 2 and y_proba.shape[1] == 2:
|
94 |
+
proba_pos_class = y_proba[:, 1]
|
95 |
+
elif y_proba.ndim == 1:
|
96 |
+
proba_pos_class = y_proba # Assume already positive class proba
|
97 |
+
else:
|
98 |
+
# Forme inattendue, ne peut pas calculer l'AUC
|
99 |
+
raise ValueError("y_proba a une forme inattendue pour le calcul AUC binaire.")
|
100 |
+
|
101 |
+
if proba_pos_class.dtype != cp.float32 and proba_pos_class.dtype != cp.float64:
|
102 |
+
proba_pos_class = proba_pos_class.astype(cp.float32)
|
103 |
+
|
104 |
+
# Check if y_true contains more than one class before calculating AUC
|
105 |
+
unique_labels = cp.unique(y_true)
|
106 |
+
if len(unique_labels) >= 2:
|
107 |
+
auc_score = roc_auc_score(y_true, proba_pos_class)
|
108 |
+
auc = float(auc_score) # Cast to float
|
109 |
+
|
110 |
+
except (ValueError, TypeError, Exception):
|
111 |
+
# Si une erreur se produit (ex: une seule classe, type incorrect, autre), AUC reste NaN
|
112 |
+
pass # auc est déjà float('nan')
|
113 |
+
|
114 |
+
metrics[f"{prefix}_auc_roc"] = auc
|
115 |
+
# Ensure all values in the returned dict are standard floats
|
116 |
+
return {k: float(v) for k, v in metrics.items()}
|
117 |
+
|
118 |
|
119 |
def calculate_and_log_multiclass(
|
120 |
self,
|
121 |
y_true: cp.ndarray,
|
122 |
y_pred: cp.ndarray,
|
123 |
+
y_proba: Optional[cp.ndarray], # Gardé pour cohérence d'interface
|
124 |
prefix: str
|
125 |
) -> Dict[str, float]:
|
126 |
"""
|
127 |
+
Calcule les métriques pour un problème multiclasses.
|
128 |
+
AUC-ROC n'est pas calculé (retourne NaN) car non supporté par cuml.metrics.roc_auc_score.
|
129 |
+
Utilise average='weighted' pour precision/recall/f1.
|
130 |
"""
|
131 |
+
metrics: Dict[str, float] = {}
|
132 |
+
|
133 |
+
try:
|
134 |
+
# Accuracy
|
135 |
+
acc = accuracy_score(y_true, y_pred)
|
136 |
+
metrics[f"{prefix}_accuracy"] = float(acc)
|
137 |
+
|
138 |
+
# Precision, Recall, F1 (Weighted)
|
139 |
+
prec, rec, f1, _ = precision_recall_fscore_support(
|
140 |
+
y_true, y_pred, average="weighted"
|
141 |
+
)
|
142 |
+
metrics[f"{prefix}_precision_weighted"] = float(prec)
|
143 |
+
metrics[f"{prefix}_recall_weighted"] = float(rec)
|
144 |
+
metrics[f"{prefix}_f1_weighted"] = float(f1)
|
145 |
+
|
146 |
+
except Exception:
|
147 |
+
# En cas d'erreur sur les métriques de base, remplir avec NaN
|
148 |
+
metrics.setdefault(f"{prefix}_accuracy", float('nan'))
|
149 |
+
metrics.setdefault(f"{prefix}_precision_weighted", float('nan'))
|
150 |
+
metrics.setdefault(f"{prefix}_recall_weighted", float('nan'))
|
151 |
+
metrics.setdefault(f"{prefix}_f1_weighted", float('nan'))
|
152 |
+
|
153 |
+
# AUC Multiclasse non supporté, retourner NaN
|
154 |
+
metrics[f"{prefix}_auc_roc"] = float('nan')
|
155 |
+
|
156 |
+
# Ensure all values in the returned dict are standard floats
|
157 |
+
return {k: float(v) for k, v in metrics.items()}
|
src/trainers/huggingface/huggingface_transformer_trainer.py
CHANGED
@@ -2,16 +2,94 @@
|
|
2 |
# Fichier: trainers/huggingface/huggingface_transformer_trainer.py
|
3 |
# ============================================================
|
4 |
|
5 |
-
from typing import Optional
|
6 |
import cupy as cp
|
|
|
7 |
import cudf
|
8 |
import torch
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
from base_trainer import BaseTrainer
|
12 |
from config import Config
|
13 |
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
class HuggingFaceTransformerTrainer(BaseTrainer):
|
16 |
"""
|
17 |
Entraîneur spécifique Hugging Face, utilisant un tokenizer,
|
@@ -31,9 +109,13 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
31 |
:param target_column: Nom de la colonne cible dans vos données.
|
32 |
"""
|
33 |
super().__init__(config, data_path, target_column)
|
|
|
34 |
self.tokenizer: Optional[AutoTokenizer] = None
|
35 |
self.model: Optional[AutoModelForSequenceClassification] = None
|
36 |
self.hf_trainer: Optional[Trainer] = None
|
|
|
|
|
|
|
37 |
|
38 |
def build_components(self) -> None:
|
39 |
"""
|
@@ -47,12 +129,16 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
47 |
model_name)
|
48 |
training_args = self._prepare_training_args()
|
49 |
|
50 |
-
# Le HF Trainer a besoin de datasets, qui sont construits
|
51 |
-
#
|
52 |
-
self.hf_trainer = Trainer(
|
53 |
-
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
56 |
|
57 |
def train(self) -> None:
|
58 |
"""
|
@@ -68,18 +154,46 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
68 |
for col in features_df.columns[1:]:
|
69 |
texts = texts.str.cat(features_df[col], sep=' ')
|
70 |
texts_list = texts.to_arrow().to_pylist()
|
71 |
-
#
|
72 |
-
|
73 |
-
#
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
# Lancement du fine‑tuning
|
|
|
|
|
|
|
83 |
self.hf_trainer.train()
|
84 |
|
85 |
def evaluate(self) -> dict:
|
@@ -87,10 +201,19 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
87 |
Évalue le modèle Hugging Face; la logique de calcul
|
88 |
des métriques est en partie assurée par le HF Trainer.
|
89 |
|
90 |
-
:return: Dictionnaire contenant les métriques calculées.
|
91 |
"""
|
92 |
-
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
def _create_torch_dataset(self, texts: cudf.Series,
|
96 |
labels: cp.ndarray) -> torch.utils.data.Dataset:
|
@@ -103,8 +226,9 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
103 |
:return: Un Dataset PyTorch utilisable par Trainer.
|
104 |
"""
|
105 |
# Implémentation possible : tokenization + construction d'un dataset custom.
|
|
|
106 |
raise NotImplementedError(
|
107 |
-
"La méthode _create_torch_dataset est
|
108 |
)
|
109 |
|
110 |
def _prepare_training_args(self) -> TrainingArguments:
|
@@ -124,4 +248,11 @@ class HuggingFaceTransformerTrainer(BaseTrainer):
|
|
124 |
warmup_steps=params.get("warmup_steps"),
|
125 |
weight_decay=params.get("weight_decay"),
|
126 |
adam_epsilon=params.get("adam_epsilon"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
)
|
|
|
2 |
# Fichier: trainers/huggingface/huggingface_transformer_trainer.py
|
3 |
# ============================================================
|
4 |
|
5 |
+
from typing import Optional, Dict, List, Any
|
6 |
import cupy as cp
|
7 |
+
import numpy as np
|
8 |
import cudf
|
9 |
import torch
|
10 |
+
import torch.nn.functional as F # Pour softmax
|
11 |
+
# Utiliser cuml.model_selection
|
12 |
+
from cuml.model_selection import train_test_split
|
13 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments, EvalPrediction
|
14 |
+
from datasets import Dataset as HFDataset # Utiliser le type Dataset de Hugging Face pour plus de clarté
|
15 |
+
# Utiliser cuml.metrics
|
16 |
+
from cuml.metrics import accuracy_score, precision_recall_fscore_support, roc_auc_score
|
17 |
+
# Importer cupy pour la conversion et argmax
|
18 |
+
import cupy as cp
|
19 |
+
# numpy n'est plus nécessaire ici
|
20 |
+
# import numpy as np
|
21 |
|
22 |
from base_trainer import BaseTrainer
|
23 |
from config import Config
|
24 |
|
25 |
|
26 |
+
# Fonction pour calculer les métriques en utilisant cuML (sans logs)
|
27 |
+
def compute_metrics(p: EvalPrediction) -> Dict[str, float]:
|
28 |
+
logits = p.predictions
|
29 |
+
# Convertir les labels numpy en cupy
|
30 |
+
labels_cp = cp.asarray(p.label_ids)
|
31 |
+
# Obtenir les prédictions en appliquant argmax aux logits avec cupy
|
32 |
+
preds_cp = cp.argmax(cp.asarray(logits), axis=1) # Utilisation de cp.argmax
|
33 |
+
|
34 |
+
metrics: Dict[str, float] = {}
|
35 |
+
|
36 |
+
try:
|
37 |
+
# Accuracy avec cuML
|
38 |
+
acc = accuracy_score(labels_cp, preds_cp)
|
39 |
+
metrics["accuracy"] = float(acc)
|
40 |
+
|
41 |
+
# Precision, Recall, F1 (Weighted) avec cuML
|
42 |
+
prec, rec, f1, _ = precision_recall_fscore_support(
|
43 |
+
labels_cp, preds_cp, average='weighted'
|
44 |
+
)
|
45 |
+
metrics["precision_weighted"] = float(prec)
|
46 |
+
metrics["recall_weighted"] = float(rec)
|
47 |
+
metrics["f1_weighted"] = float(f1)
|
48 |
+
|
49 |
+
except Exception:
|
50 |
+
# Remplir avec NaN si erreur
|
51 |
+
metrics.setdefault("accuracy", float('nan'))
|
52 |
+
metrics.setdefault("precision_weighted", float('nan'))
|
53 |
+
metrics.setdefault("recall_weighted", float('nan'))
|
54 |
+
metrics.setdefault("f1_weighted", float('nan'))
|
55 |
+
|
56 |
+
# Calcul AUC (binaire seulement avec cuML)
|
57 |
+
auc: float = float('nan') # Default NaN
|
58 |
+
num_classes = logits.shape[1]
|
59 |
+
|
60 |
+
if num_classes == 2:
|
61 |
+
try:
|
62 |
+
# Obtenir les probabilités (softmax) et convertir en cupy
|
63 |
+
probas_torch = F.softmax(torch.tensor(logits), dim=-1)
|
64 |
+
probas_cp = cp.asarray(probas_torch)
|
65 |
+
|
66 |
+
# Utiliser les probas de la classe positive
|
67 |
+
proba_pos_class = probas_cp[:, 1]
|
68 |
+
|
69 |
+
# S'assurer que les types sont corrects pour cuML roc_auc_score
|
70 |
+
if labels_cp.dtype != cp.int32 and labels_cp.dtype != cp.int64:
|
71 |
+
labels_cp = labels_cp.astype(cp.int32)
|
72 |
+
if proba_pos_class.dtype != cp.float32 and proba_pos_class.dtype != cp.float64:
|
73 |
+
proba_pos_class = proba_pos_class.astype(cp.float32)
|
74 |
+
|
75 |
+
# Vérifier qu'il y a plus d'une classe dans les labels réels
|
76 |
+
unique_labels = cp.unique(labels_cp)
|
77 |
+
if len(unique_labels) >= 2:
|
78 |
+
auc_score = roc_auc_score(labels_cp, proba_pos_class)
|
79 |
+
auc = float(auc_score) # Cast to float
|
80 |
+
# else: # Pas de log
|
81 |
+
|
82 |
+
except (ValueError, TypeError, Exception):
|
83 |
+
# auc reste NaN en cas d'erreur, pas de log
|
84 |
+
pass
|
85 |
+
# else: # Pas de log pour le cas multiclasse
|
86 |
+
# auc reste NaN
|
87 |
+
|
88 |
+
metrics["auc_roc"] = auc
|
89 |
+
|
90 |
+
# Retourner les métriques avec les noms de base
|
91 |
+
return {k: float(v) for k, v in metrics.items()} # Assurer float standard
|
92 |
+
|
93 |
class HuggingFaceTransformerTrainer(BaseTrainer):
|
94 |
"""
|
95 |
Entraîneur spécifique Hugging Face, utilisant un tokenizer,
|
|
|
109 |
:param target_column: Nom de la colonne cible dans vos données.
|
110 |
"""
|
111 |
super().__init__(config, data_path, target_column)
|
112 |
+
super().__init__(config, data_path, target_column)
|
113 |
self.tokenizer: Optional[AutoTokenizer] = None
|
114 |
self.model: Optional[AutoModelForSequenceClassification] = None
|
115 |
self.hf_trainer: Optional[Trainer] = None
|
116 |
+
self.train_dataset: Optional[HFDataset] = None
|
117 |
+
self.eval_dataset: Optional[HFDataset] = None
|
118 |
+
self.test_dataset: Optional[HFDataset] = None
|
119 |
|
120 |
def build_components(self) -> None:
|
121 |
"""
|
|
|
129 |
model_name)
|
130 |
training_args = self._prepare_training_args()
|
131 |
|
132 |
+
# Le HF Trainer a besoin de datasets, qui sont construits dans train()
|
133 |
+
# On ajoute compute_metrics ici
|
134 |
+
self.hf_trainer = Trainer(
|
135 |
+
model=self.model,
|
136 |
+
args=training_args,
|
137 |
+
train_dataset=self.train_dataset, # Sera défini dans train()
|
138 |
+
eval_dataset=self.eval_dataset, # Sera défini dans train()
|
139 |
+
compute_metrics=compute_metrics,
|
140 |
+
tokenizer=self.tokenizer # Ajout du tokenizer pour le padding dynamique si besoin
|
141 |
+
)
|
142 |
|
143 |
def train(self) -> None:
|
144 |
"""
|
|
|
154 |
for col in features_df.columns[1:]:
|
155 |
texts = texts.str.cat(features_df[col], sep=' ')
|
156 |
texts_list = texts.to_arrow().to_pylist()
|
157 |
+
# texts est une cudf.Series, labels est un cp.ndarray
|
158 |
+
# Utiliser cuml.model_selection.train_test_split directement
|
159 |
+
# Premier split: 80% train, 20% temp
|
160 |
+
X_train_text, X_temp_text, y_train, y_temp = train_test_split(
|
161 |
+
texts, labels, test_size=0.2, random_state=42, stratify=labels
|
162 |
+
)
|
163 |
+
# Deuxième split: 50% validation, 50% test sur temp (donne 10% val, 10% test du total)
|
164 |
+
X_val_text, X_test_text, y_val, y_test = train_test_split(
|
165 |
+
X_temp_text, y_temp, test_size=0.5, random_state=42, stratify=y_temp
|
166 |
+
)
|
167 |
+
|
168 |
+
# Fonction pour créer un dataset Hugging Face à partir de cudf.Series et cp.ndarray
|
169 |
+
def create_hf_dataset(text_series: cudf.Series, label_array: cp.ndarray) -> HFDataset:
|
170 |
+
# Convertir en listes Python pour le tokenizer et HF Dataset
|
171 |
+
texts_list = text_series.to_arrow().to_pylist()
|
172 |
+
# Convertir cupy array en numpy puis en liste pour HF Dataset
|
173 |
+
labels_list = cp.asnumpy(label_array).tolist()
|
174 |
+
|
175 |
+
encodings = self.tokenizer(texts_list, padding=True, truncation=True) # Pas de return_tensors="pt" ici
|
176 |
+
# Crée un dictionnaire compatible avec Dataset.from_dict
|
177 |
+
data_dict = {
|
178 |
+
"input_ids": encodings["input_ids"],
|
179 |
+
"attention_mask": encodings["attention_mask"],
|
180 |
+
"labels": labels_list
|
181 |
+
}
|
182 |
+
return HFDataset.from_dict(data_dict)
|
183 |
+
|
184 |
+
# Création des datasets
|
185 |
+
self.train_dataset = create_hf_dataset(X_train_text, y_train)
|
186 |
+
self.eval_dataset = create_hf_dataset(X_val_text, y_val)
|
187 |
+
self.test_dataset = create_hf_dataset(X_test_text, y_test) # Garder pour evaluate()
|
188 |
+
|
189 |
+
# Assignation des datasets au Trainer HF (déjà fait dans build_components mais on réassigne ici)
|
190 |
+
self.hf_trainer.train_dataset = self.train_dataset
|
191 |
+
self.hf_trainer.eval_dataset = self.eval_dataset
|
192 |
+
|
193 |
# Lancement du fine‑tuning
|
194 |
+
print(f"Starting training with {len(self.train_dataset)} samples.")
|
195 |
+
print(f"Validation during training with {len(self.eval_dataset)} samples.")
|
196 |
+
print(f"Test set prepared with {len(self.test_dataset)} samples.")
|
197 |
self.hf_trainer.train()
|
198 |
|
199 |
def evaluate(self) -> dict:
|
|
|
201 |
Évalue le modèle Hugging Face; la logique de calcul
|
202 |
des métriques est en partie assurée par le HF Trainer.
|
203 |
|
204 |
+
:return: Dictionnaire contenant les métriques calculées sur l'ensemble de test.
|
205 |
"""
|
206 |
+
if self.hf_trainer is None or self.test_dataset is None:
|
207 |
+
raise ValueError("Trainer or test dataset not initialized. Run train() first.")
|
208 |
+
|
209 |
+
print(f"Evaluating on the test set ({len(self.test_dataset)} samples)...")
|
210 |
+
# Utiliser predict pour obtenir les métriques sur le jeu de test
|
211 |
+
results = self.hf_trainer.predict(self.test_dataset)
|
212 |
+
|
213 |
+
# results.metrics contient déjà les métriques calculées par compute_metrics
|
214 |
+
# sur le test_dataset fourni.
|
215 |
+
print("Evaluation results:", results.metrics)
|
216 |
+
return results.metrics
|
217 |
|
218 |
def _create_torch_dataset(self, texts: cudf.Series,
|
219 |
labels: cp.ndarray) -> torch.utils.data.Dataset:
|
|
|
226 |
:return: Un Dataset PyTorch utilisable par Trainer.
|
227 |
"""
|
228 |
# Implémentation possible : tokenization + construction d'un dataset custom.
|
229 |
+
# Cette méthode n'est plus directement utilisée car on crée les HFDatasets dans train()
|
230 |
raise NotImplementedError(
|
231 |
+
"La méthode _create_torch_dataset n'est plus utilisée directement."
|
232 |
)
|
233 |
|
234 |
def _prepare_training_args(self) -> TrainingArguments:
|
|
|
248 |
warmup_steps=params.get("warmup_steps"),
|
249 |
weight_decay=params.get("weight_decay"),
|
250 |
adam_epsilon=params.get("adam_epsilon"),
|
251 |
+
# Ajout de paramètres importants pour l'évaluation
|
252 |
+
evaluation_strategy="epoch", # Évaluer à chaque époque
|
253 |
+
save_strategy="epoch", # Sauvegarder le modèle à chaque époque
|
254 |
+
load_best_model_at_end=True, # Charger le meilleur modèle à la fin
|
255 |
+
metric_for_best_model="f1", # Utiliser F1 pour déterminer le meilleur modèle (ou 'accuracy')
|
256 |
+
logging_dir=params.get("logging_dir", "./logs"), # Pour les logs TensorBoard
|
257 |
+
logging_steps=params.get("logging_steps", 10), # Fréquence des logs
|
258 |
)
|