fioriclass commited on
Commit
3633596
·
1 Parent(s): a7790b3

grosse mise à jour sur le train test eval

Browse files
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
- # self.classifier est déjà défini dans BaseTrainer.
40
- # On suppose que 'classifier' sera un modèle cuML (cuml.Base).
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  @abstractmethod
43
  def build_components(self) -> None:
@@ -47,66 +62,152 @@ class CuMLTrainer(BaseTrainer, ABC):
47
  """
48
  pass
49
 
50
- def train(self) -> None:
51
  """
52
- Entraîne le classifieur sur les données vectorisées.
53
- Cette implémentation générique fonctionne pour tous les trainers cuML.
 
54
  """
55
- # Chargement des données
 
 
56
 
 
57
  data = cudf.read_csv(self.data_path)
58
 
59
- # Identification des colonnes de features (toutes sauf la cible)
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. Le dataset doit contenir au moins une colonne en plus de la colonne cible.")
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 # Assurer le bon dtype pour les labels
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- # Vectorisation des textes concaténés
70
- X = self.vectorizer.fit_transform(texts_concatenated)
71
- X_prepared = self._prepare_input_for_fit(X)
72
- # Entraînement du modèle
73
- self.classifier.fit(X_prepared, labels)
 
 
74
 
75
- def evaluate(self) -> dict:
 
 
 
76
  """
77
- Évalue le classifieur et calcule les métriques.
78
- Cette implémentation générique fonctionne pour tous les trainers cuML.
79
  """
80
- # Chargement des données (idéalement un jeu de test séparé)
81
- data = cudf.read_csv(self.data_path)
82
 
83
- # Identification des colonnes de features (toutes sauf la cible)
84
- feature_columns = [col for col in data.columns if col != self.target_column]
85
- if not feature_columns:
86
- raise ValueError("Aucune colonne de feature trouvée pour l'évaluation.")
87
 
88
- # Concaténation des features
89
- texts_concatenated = data[feature_columns].astype(str).agg(' '.join, axis=1)
90
- y_true = data[self.target_column].astype(self._get_label_dtype()).values # Assurer le bon dtype
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- metrics = self.metrics_calculator.calculate_and_log(
100
- y_true=y_true,
101
- y_pred=y_pred,
102
- prefix=prefix
103
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  # Afficher les résultats
106
- print(f"Métriques d'évaluation {prefix}: {metrics}")
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 numpy as np
3
- from typing import Dict, Protocol
4
- import logging
5
- from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
 
 
 
 
 
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 et log les métriques pour un problème binaire.
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 et log les métriques pour un problème multiclasses.
30
  """
31
  pass
32
 
33
- logger = logging.getLogger(__name__)
34
 
35
  class DefaultMetricsCalculator(MetricsCalculator):
36
  """
37
- Implémentation concrète de MetricsCalculator qui calcule
38
- accuracy, f1, precision, recall, et auc-roc.
39
- Fonctionne pour binaire ou multiclasses (avec 'ovr' ou 'macro').
 
 
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 et log les métriques pour un problème binaire
50
- en supposant y_pred est dans {0,1} ou {True,False}.
 
51
  """
52
- y_true_np = cp.asnumpy(y_true)
53
- y_pred_np = cp.asnumpy(y_pred)
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
- auc = roc_auc_score(y_true_np, y_pred_np)
65
- except ValueError:
66
- auc = 0.0
67
-
68
- metrics = {
69
- f"{prefix}_accuracy" : acc,
70
- f"{prefix}_precision" : prec,
71
- f"{prefix}_recall" : rec,
72
- f"{prefix}_f1" : f1,
73
- f"{prefix}_auc_roc" : auc
74
- }
75
- logger.info(f"[{prefix}] Metrics: {metrics}")
76
- return metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 et log les métriques pour un problème multiclasses.
86
- AUC-ROC en mode 'macro' si possible.
 
87
  """
88
- y_true_np = cp.asnumpy(y_true)
89
- y_pred_np = cp.asnumpy(y_pred)
90
-
91
- acc = accuracy_score(y_true_np, y_pred_np)
92
- prec = precision_score(y_true_np, y_pred_np, average="macro", zero_division=0)
93
- rec = recall_score(y_true_np, y_pred_np, average="macro", zero_division=0)
94
- f1 = f1_score(y_true_np, y_pred_np, average="macro", zero_division=0)
95
-
96
- # Pour le multiclasses, la roc_auc_score nécessite des scores proba
97
- # ou "decision_function" => vous ajusterez selon votre cas.
98
- # Ici, on met 0.0 en fallback.
99
- auc = 0.0
100
-
101
- metrics = {
102
- f"{prefix}_accuracy" : acc,
103
- f"{prefix}_precision" : prec,
104
- f"{prefix}_recall" : rec,
105
- f"{prefix}_f1" : f1,
106
- f"{prefix}_auc_roc" : auc
107
- }
108
- logger.info(f"[{prefix}] Multiclass metrics: {metrics}")
109
- return metrics
 
 
 
 
 
 
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
- from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
 
 
 
 
 
 
 
 
 
 
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
- # dans le code de train/evaluate ou un data loader.
52
- self.hf_trainer = Trainer(model=self.model,
53
- args=training_args,
54
- train_dataset=None,
55
- eval_dataset=None)
 
 
 
 
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
- # Tokenization
72
- encodings = self.tokenizer(texts_list, padding=True, truncation=True, return_tensors="pt")
73
- # Création du dataset PyTorch (sous forme de liste de dictionnaires)
74
- dataset = [{
75
- "input_ids": encodings["input_ids"][i],
76
- "attention_mask": encodings["attention_mask"][i],
77
- "labels": torch.tensor(cp.asnumpy(labels)[i])
78
- } for i in range(len(texts_list))]
79
- # Assignation des datasets au Trainer HF
80
- self.hf_trainer.train_dataset = dataset
81
- self.hf_trainer.eval_dataset = dataset
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # À implémenter
93
- return {}
 
 
 
 
 
 
 
 
 
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 à implémenter selon vos besoins."
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
  )