File size: 12,314 Bytes
bf5fb5f
 
 
 
3633596
bf5fb5f
3633596
bf5fb5f
 
3633596
 
 
 
 
 
89f0e63
3633596
 
 
 
bf5fb5f
8ffb539
 
bf5fb5f
 
3633596
 
 
 
 
 
89f0e63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65e5e42
 
89f0e63
 
65e5e42
89f0e63
 
65e5e42
 
 
 
 
 
 
 
89f0e63
3633596
bf5fb5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3633596
bf5fb5f
 
 
3633596
 
 
bf5fb5f
 
 
 
 
 
 
ef3b361
bf5fb5f
 
 
 
 
3633596
 
 
 
 
 
 
 
2040f85
 
3633596
bf5fb5f
 
 
ef3b361
bf5fb5f
ef3b361
 
 
 
 
 
 
 
 
024f027
 
 
 
 
 
 
 
 
 
 
3633596
024f027
 
 
 
3633596
024f027
 
 
 
 
3633596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3b361
3633596
 
 
ef3b361
bf5fb5f
1a758de
bf5fb5f
 
 
1a758de
3633596
bf5fb5f
3633596
 
 
 
 
 
 
 
 
 
 
bf5fb5f
 
 
 
 
 
 
 
 
 
 
 
3633596
bf5fb5f
3633596
bf5fb5f
 
 
 
 
 
 
 
 
ef3b361
 
2e5a32e
 
 
 
 
 
 
2040f85
2e5a32e
2040f85
 
 
ef3b361
2e5a32e
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# ============================================================
# Fichier: trainers/huggingface/huggingface_transformer_trainer.py
# ============================================================

from typing import Optional, Dict, List, Any
import cupy as cp
import numpy as np
import cudf
import torch
import torch.nn.functional as F # Pour softmax
# Utiliser cuml.model_selection
from cuml.model_selection import train_test_split
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments, EvalPrediction
from datasets import Dataset as HFDataset # Utiliser le type Dataset de Hugging Face pour plus de clarté
# Utiliser cuml.metrics
from cuml.metrics import accuracy_score, precision_recall_curve, roc_auc_score
# Importer cupy pour la conversion et argmax
import cupy as cp
# numpy n'est plus nécessaire ici
# import numpy as np

from base_trainer import BaseTrainer
from config import Config


# Fonction pour calculer les métriques en utilisant cuML (sans logs)
def compute_metrics(p: EvalPrediction) -> Dict[str, float]:
    logits = p.predictions
    # Convertir les labels numpy en cupy
    labels_cp = cp.asarray(p.label_ids)
    # Obtenir les prédictions en appliquant argmax aux logits avec cupy
    preds_cp = cp.argmax(cp.asarray(logits), axis=1)

    # Obtenir les probabilités (softmax) et convertir en cupy
    probas_torch = F.softmax(torch.tensor(logits), dim=-1)
    probas_cp = cp.asarray(probas_torch)
    
    # Utiliser les probas de la classe positive
    proba_pos_class = probas_cp[:, 1]

    # 1. Calculer l'accuracy
    acc = accuracy_score(labels_cp, preds_cp)
    
    # 2. Calculer l'AUC ROC
    auc = roc_auc_score(labels_cp.astype(cp.int32), proba_pos_class.astype(cp.float32))
    
    # 3. Utiliser precision_recall_curve pour obtenir les courbes
    precision, recall, thresholds = precision_recall_curve(
        labels_cp.astype(cp.int32), proba_pos_class.astype(cp.float32)
    )
    
    # 4. Calculer la précision, le rappel et le F1 score optimaux
    optimal_precision, optimal_recall, optimal_f1, optimal_threshold = calculate_optimal_f1(
        precision, recall, thresholds
    )

    # Construire le dictionnaire des métriques
    metrics = {
        "accuracy": float(acc),
        "precision": float(optimal_precision),
        "recall": float(optimal_recall),
        "f1": float(optimal_f1),
        "optimal_threshold": float(optimal_threshold),
        "auc_roc": float(auc)
    }
    
    return metrics

def calculate_optimal_f1(precision: cp.ndarray, recall: cp.ndarray, thresholds: cp.ndarray):
    """
    Calcule le F1 score optimal à partir des courbes de précision et de rappel.
    
    Args:
        precision: Tableau de précisions pour différents seuils
        recall: Tableau de rappels pour différents seuils
        thresholds: Tableau de seuils correspondants
        
    Returns:
        Tuple contenant (précision optimale, rappel optimal, F1 score optimal, seuil optimal)
    """
    # Ajouter le seuil 1.0 à thresholds (qui n'est pas inclus par défaut dans precision_recall_curve)
    thresholds_with_one = cp.append(thresholds, cp.array([1.0]))
    
    # Calculer le F1 score pour chaque point de la courbe
    # F1 = 2 * (precision * recall) / (precision + recall)
    f1_scores = 2 * (precision * recall) / (precision + recall)
    
    # Trouver l'indice du F1 score maximal
    best_idx = cp.argmax(f1_scores)
    best_precision = float(precision[best_idx])
    best_recall = float(recall[best_idx])
    best_f1 = float(f1_scores[best_idx])
    
    # Obtenir le seuil optimal
    best_threshold = float(thresholds_with_one[best_idx])
    
    return best_precision, best_recall, best_f1, best_threshold

class HuggingFaceTransformerTrainer(BaseTrainer):
    """
    Entraîneur spécifique Hugging Face, utilisant un tokenizer,
    un modèle AutoModelForSequenceClassification et un HF Trainer.
    Ne dépend pas d'un vectorizer cuML d'après l'UML.
    """

    def __init__(self, config: Config, data_path: str,
                 target_column: str) -> None:
        """
        Initialise un HuggingFaceTransformerTrainer avec la configuration
        et les paramètres du parent BaseTrainer.

        :param config: Configuration globale du système.
                       (La config.vectorizer n'est pas utilisée ici.)
        :param data_path: Chemin vers le fichier de données.
        :param target_column: Nom de la colonne cible dans vos données.
        """
        super().__init__(config, data_path, target_column)
        super().__init__(config, data_path, target_column)
        self.tokenizer: Optional[AutoTokenizer] = None
        self.model: Optional[AutoModelForSequenceClassification] = None
        self.hf_trainer: Optional[Trainer] = None
        self.train_dataset: Optional[HFDataset] = None
        self.eval_dataset: Optional[HFDataset] = None
        self.test_dataset: Optional[HFDataset] = None

    def build_components(self) -> None:
        """
        Instancie le tokenizer et le modèle Hugging Face
        AutoModelForSequenceClassification, puis crée un Trainer
        avec des TrainingArguments par défaut.
        """
        model_name = self.config.model.params.get("model_name")
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForSequenceClassification.from_pretrained(
            model_name)
        training_args = self._prepare_training_args()

        # Le HF Trainer a besoin de datasets, qui sont construits dans train()
        # On ajoute compute_metrics ici
        self.hf_trainer = Trainer(
            model=self.model,
            args=training_args,
            train_dataset=self.train_dataset, # Sera défini dans train()
            eval_dataset=self.eval_dataset,   # Sera défini dans train()
            compute_metrics=compute_metrics,
            tokenizer=self.tokenizer, # Ajout du tokenizer pour le padding dynamique si besoin
            callbacks=[]
        )

    def train(self) -> None:
        """
        Entraîne le modèle Hugging Face sur le jeu de données.
        """
        # Chargement des données avec cuDF
        df = cudf.read_csv(self.data_path)
        # Séparation des labels
        labels = cp.asarray(df[self.target_column].astype(int))
        # Création du texte en concaténant toutes les colonnes sauf la cible
        features_df = df.drop(columns=[self.target_column]).astype(str)
        texts = features_df[features_df.columns[0]]
        for col in features_df.columns[1:]:
            texts = texts.str.cat(features_df[col], sep=' ')
            
        # Créer une copie des textes pour le stockage
        texts_for_storage = texts.copy()
        
        # Utiliser des indices numériques pour le split au lieu des textes directement
        # Cette approche évite les problèmes de conversion des objets string en tableaux CUDA
        indices = cp.arange(len(texts))
        
        # Premier split: 80% train, 20% temp en utilisant les indices
        train_indices, temp_indices, y_train, y_temp = train_test_split(
            indices, labels, test_size=0.2, random_state=42, stratify=labels
        )
        
        # Deuxième split: 50% validation, 50% test sur temp
        val_indices, test_indices, y_val, y_test = train_test_split(
            temp_indices, y_temp, test_size=0.5, random_state=42, stratify=y_temp
        )
        
        # Récupérer les textes correspondant aux indices
        X_train_text = texts_for_storage.iloc[train_indices.get()]
        X_val_text = texts_for_storage.iloc[val_indices.get()]
        X_test_text = texts_for_storage.iloc[test_indices.get()]

        # Fonction pour créer un dataset Hugging Face à partir de cudf.Series et cp.ndarray
        def create_hf_dataset(text_series: cudf.Series, label_array: cp.ndarray) -> HFDataset:
            # Convertir en listes Python pour le tokenizer et HF Dataset
            texts_list = text_series.to_arrow().to_pylist()
            # Convertir cupy array en numpy puis en liste pour HF Dataset
            labels_list = cp.asnumpy(label_array).tolist()

            encodings = self.tokenizer(texts_list, padding=True, truncation=True) # Pas de return_tensors="pt" ici
            # Crée un dictionnaire compatible avec Dataset.from_dict
            data_dict = {
                "input_ids": encodings["input_ids"],
                "attention_mask": encodings["attention_mask"],
                "labels": labels_list
            }
            return HFDataset.from_dict(data_dict)

        # Création des datasets
        self.train_dataset = create_hf_dataset(X_train_text, y_train)
        self.eval_dataset = create_hf_dataset(X_val_text, y_val)
        self.test_dataset = create_hf_dataset(X_test_text, y_test) # Garder pour evaluate()

        # Assignation des datasets au Trainer HF (déjà fait dans build_components mais on réassigne ici)
        self.hf_trainer.train_dataset = self.train_dataset
        self.hf_trainer.eval_dataset = self.eval_dataset

        # Lancement du fine‑tuning
        print(f"Starting training with {len(self.train_dataset)} samples.")
        print(f"Validation during training with {len(self.eval_dataset)} samples.")
        print(f"Test set prepared with {len(self.test_dataset)} samples.")
        self.hf_trainer.train()

    def evaluate(self) -> dict:
        """
        Évalue le modèle Hugging Face; la logique de calcul
        des métriques est en partie assurée par le HF Trainer.
        
        :return: Dictionnaire contenant les métriques calculées sur l'ensemble de test.
        """
        if self.hf_trainer is None or self.test_dataset is None:
            raise ValueError("Trainer or test dataset not initialized. Run train() first.")

        print(f"Evaluating on the test set ({len(self.test_dataset)} samples)...")
        # Utiliser predict pour obtenir les métriques sur le jeu de test
        results = self.hf_trainer.predict(self.test_dataset)

        # results.metrics contient déjà les métriques calculées par compute_metrics
        # sur le test_dataset fourni.
        print("Evaluation results:", results.metrics)
        return results.metrics

    def _create_torch_dataset(self, texts: cudf.Series,
                              labels: cp.ndarray) -> torch.utils.data.Dataset:
        """
        Convertit un cudf.Series de textes et un tableau cupy de labels
        en un Dataset PyTorch.

        :param texts: Série cudf contenant les textes.
        :param labels: Vecteur cupy des labels (ex. classification binaire ou multiclasses).
        :return: Un Dataset PyTorch utilisable par Trainer.
        """
        # Implémentation possible : tokenization + construction d'un dataset custom.
        # Cette méthode n'est plus directement utilisée car on crée les HFDatasets dans train()
        raise NotImplementedError(
            "La méthode _create_torch_dataset n'est plus utilisée directement."
        )

    def _prepare_training_args(self) -> TrainingArguments:
        """
        Construit un objet TrainingArguments Hugging Face,
        par exemple pour définir l'output_dir, le batch_size, etc.

        :return: Instance de TrainingArguments configurée.
        """
        params = self.config.model.params
        return TrainingArguments(
            output_dir="./results",
            num_train_epochs=float(params.get("epochs")),
            per_device_train_batch_size=int(params.get("batch_size")),
            per_device_eval_batch_size=int(params.get("batch_size")),
            learning_rate=float(params.get("learning_rate")),
            warmup_steps=int(params.get("warmup_steps")),
            weight_decay=float(params.get("weight_decay")),
            save_steps=50,
            logging_dir="./logs",
            logging_strategy="no",
            save_strategy="epoch",
            report_to="mlflow"
        )
        
    def optimize_if_needed(self) -> None:
        """
        Surcharge la méthode optimize_if_needed de BaseTrainer pour désactiver
        l'optimisation des hyperparamètres pour les modèles transformers.
        """
        # Ne rien faire, ce qui désactive l'optimisation des hyperparamètres
        return