GDM-Aide-RUN-V2 / guide_train_v3.md
Elkristobal59's picture
feat: optimize model loading, clean imports, run V4 training and fix tests
187f074
|
Raw
History Blame Contribute Delete
11.4 kB

🧠 Guide Ligne par Ligne — train_V3.py

Ce document explique chaque ligne de ton script d'entraînement pour que tu puisses défendre ton code devant le jury avec assurance.


Bloc 1 : Les Imports (Lignes 1-12)

import pandas as pd                          # Manipulation de données tabulaires (DataFrames)
import pickle                                 # Sérialisation : sauvegarder ton modèle sur disque
import numpy as np                            # Calcul numérique (matrices, moyennes)
import torch                                  # PyTorch : le moteur de Deep Learning (GPU)
import os                                     # Gestion des fichiers (vérifier si un CSV existe)
import csv                                    # Gestion bas niveau des CSV (pour augmenter la limite)
from transformers import AutoTokenizer, AutoModel   # Hugging Face : charger DistilBERT
from sklearn.base import BaseEstimator, TransformerMixin  # Interfaces Scikit-Learn
from sklearn.pipeline import Pipeline                     # Chaîner les étapes du ML
from sklearn.linear_model import LogisticRegression       # L'algorithme de classification
from sklearn.model_selection import train_test_split      # Séparer Train / Test
from sklearn.metrics import classification_report         # Rapport Precision/Recall/F1

Si le jury te demande "Pourquoi Scikit-Learn ET PyTorch ?" PyTorch sert uniquement à faire tourner DistilBERT (le modèle de langage). Scikit-Learn sert à tout le reste : le pipeline, la régression logistique, l'évaluation. Les deux se complètent parfaitement.


Bloc 2 : Sécurité CSV (Ligne 14-15)

csv.field_size_limit(10000000)

Pourquoi ? Par défaut, Python refuse de lire un champ CSV de plus de 131 072 caractères. Certaines descriptions de tickets Freshservice peuvent être extrêmement longues (logs Opcon copiés-collés). Sans cette ligne, le script crashe avec _csv.Error: field larger than field limit.


Bloc 3 : La Classe LLMFeatureExtractor (Lignes 17-71)

C'est le cœur technique de ton projet. Cette classe transforme du texte brut en vecteurs numériques que la Régression Logistique peut comprendre.

Le Constructeur __init__ (Lignes 19-27)

class LLMFeatureExtractor(BaseEstimator, TransformerMixin):
    def __init__(self, model_name='distilbert-base-multilingual-cased', 
                 max_length=128, batch_size=32):
        self.model_name = model_name     # Nom du modèle Hugging Face à utiliser
        self.max_length = max_length     # Nombre max de tokens par phrase (128 ≈ 2-3 phrases)
        self.batch_size = batch_size     # Nombre de tickets traités simultanément sur le GPU
        self.device = "cuda" if torch.cuda.is_available() else "cpu"  # Détection GPU
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)    # Le "dictionnaire"
        self.model = AutoModel.from_pretrained(model_name).to(self.device)  # Le "cerveau"

BaseEstimator, TransformerMixin : En héritant de ces deux classes Scikit-Learn, ta classe devient compatible avec le Pipeline. BaseEstimator donne get_params() et set_params(). TransformerMixin donne fit_transform() automatiquement.

La Méthode fit (Ligne 29-30)

def fit(self, X, y=None):
    return self

Pourquoi elle ne fait rien ? DistilBERT est un modèle pré-entraîné. Il a déjà appris le français/anglais sur des milliards de documents. On n'a pas besoin de le ré-entraîner. Il est "prêt à l'emploi" (c'est le principe du Transfer Learning).

La Méthode transform (Lignes 32-55) — L'Étape Clé

def transform(self, X):
    texts = [str(t)[:1000] for t in X]     # Tronquer les textes trop longs (sécurité)
    all_embeddings = []
    
    for i in range(0, len(texts), self.batch_size):   # On traite par paquets de 32
        batch_texts = texts[i : i + self.batch_size]
        
        # 1. TOKENISATION : Transformer le texte en nombres (IDs de tokens)
        inputs = self.tokenizer(
            batch_texts, 
            padding=True,           # Aligner tous les textes sur la même longueur
            truncation=True,        # Couper si trop long
            max_length=128,         # Maximum 128 tokens
            return_tensors="pt"     # Format PyTorch (tenseurs)
        ).to(self.device)           # Envoyer sur le GPU
        
        # 2. INFÉRENCE : DistilBERT lit les tokens et génère un vecteur de 768 dimensions
        with torch.no_grad():       # Désactiver le calcul de gradient (on ne ré-entraîne pas)
            outputs = self.model(**inputs)
            # On prend le vecteur du token [CLS] (résumé sémantique de toute la phrase)
            embeddings = outputs.last_hidden_state[:, 0, :].cpu().detach().numpy()
            all_embeddings.append(embeddings)

    return np.vstack(all_embeddings)  # Empiler tous les vecteurs en une grande matrice

outputs.last_hidden_state[:, 0, :] : C'est la ligne la plus importante. DistilBERT génère un vecteur pour CHAQUE mot de la phrase. Le token [CLS] (position 0) est un vecteur spécial qui résume le sens global de toute la phrase en 768 nombres. C'est lui qu'on donne à la Régression Logistique.

Les Méthodes __getstate__ et __setstate__ (Lignes 57-71)

def __getstate__(self):
    state = self.__dict__.copy()
    if 'model' in state: del state['model']       # On retire le gros cerveau (540 Mo)
    if 'tokenizer' in state: del state['tokenizer'] # On retire le dictionnaire
    return state   # Il ne reste que les hyperparamètres (model_name, max_length, etc.)

def __setstate__(self, state):
    self.__dict__.update(state)
    from transformers import AutoTokenizer, AutoModel
    self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)  # Retéléchargé !
    device = "cuda" if torch.cuda.is_available() else "cpu"
    self.model = AutoModel.from_pretrained(self.model_name).to(device)  # Retéléchargé !

Explication : Quand Python exécute pickle.dump(pipeline), il appelle __getstate__ pour savoir quoi sauvegarder. On lui dit : "Sauvegarde tout SAUF le modèle DistilBERT". Résultat : le fichier .pkl passe de 540 Mo à 44 Ko.

Quand l'application Streamlit fait pickle.load() sur Hugging Face, Python appelle __setstate__ et retélécharge DistilBERT en direct.


Bloc 4 : Le Mapping Métier (Lignes 83-108)

def map_groupe_to_pole(groupe):
    g = str(groupe).upper().strip()
    
    if any(k in g for k in ["OPCON", "CHAINE DE NUIT", "CRITICITE", "CRITICIT"]):
        return "MONITORING"
    elif g == "RUN":
        return "RUN"
    # ...

any(k in g for k in [...]) : C'est une compréhension Python qui vérifie si AU MOINS UN des mots-clés apparaît dans le nom du groupe. C'est l'équivalent de faire if "OPCON" in g or "CHAINE" in g or ... mais en beaucoup plus lisible et maintenable.


Bloc 5 : Le Pipeline d'Entraînement (Lignes 110-214)

5.1 — Chargement des données (Lignes 113-128)

liste_df = []
for f in FICHIERS_ENTREE:
    temp_df = pd.read_csv(f, sep=None, engine='python', encoding='utf-8-sig')
    liste_df.append(temp_df)

df = pd.concat(liste_df, ignore_index=True)  # Fusionner les 2 CSV en un seul DataFrame

encoding='utf-8-sig' : Gère le BOM (Byte Order Mark) que Windows ajoute parfois en tête des fichiers CSV exportés depuis Excel.

5.2 — Détection dynamique des colonnes (Lignes 130-139)

mapping = {normaliser(c): c for c in df.columns}
col_desc = mapping.get('description')
col_groupe = mapping.get('groupe')

Pourquoi normaliser ? Les noms de colonnes Freshservice contiennent des accents (Catégorie, Département). En normalisant (categorie, departement), on évite les bugs d'encodage.

5.3 — Préparation des features (Lignes 141-159)

# Fusion Objet + Description → plus de contexte pour l'IA
df['text_brut'] = df[col_objet].fillna('') + " " + df[col_desc].fillna('')

# Mapping vers les 5 pôles
df['pole'] = df[col_groupe].apply(map_groupe_to_pole)

# Suppression des doublons (un même ticket apparaît parfois 2 fois)
df = df.drop_duplicates(subset=['text_brut'])

5.4 — Calcul du MTTR (Lignes 172-190)

# Conversion "04:13:00" → 4.2167 heures
def convertir_en_heures(val):
    p = str(val).split(':')
    return float(p[0]) + float(p[1])/60 + float(p[2])/3600

# Médiane par pôle (robuste aux valeurs extrêmes)
mttr_moyennes = df_mttr.groupby('pole')['mttr_h'].median().to_dict()

# Sauvegarde dans un fichier séparé
pickle.dump(mttr_moyennes, f)

5.5 — Entraînement et Évaluation (Lignes 192-211)

# Séparation 80% train / 20% test, avec stratification
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
  • test_size=0.2 : 80% pour apprendre, 20% pour évaluer
  • random_state=42 : Reproductibilité (même split à chaque exécution)
  • stratify=y : Garantit que chaque pôle est représenté proportionnellement dans le train ET le test
pipeline = Pipeline([
    ('feature_extractor', LLMFeatureExtractor()),  # Étape 1 : Texte → Vecteur 768D
    ('classifier', LogisticRegression(
        max_iter=2000,          # Nombre max d'itérations pour converger
        solver='lbfgs',         # Algorithme d'optimisation (standard pour multi-classes)
        class_weight='balanced' # Compense le déséquilibre des classes automatiquement
    ))
])

class_weight='balanced' : C'est crucial ! Sans ça, le modèle favoriserait toujours la classe majoritaire (MONITORING avec ~7800 tickets). Avec balanced, Scikit-Learn augmente automatiquement le "poids" des classes minoritaires (COMMERCE avec ~600 tickets) dans la fonction de coût.

pipeline.fit(X_train, y_train)    # Entraînement
y_pred = pipeline.predict(X_test) # Prédiction sur les données cachées
print(classification_report(y_test, y_pred))  # Rapport Precision/Recall/F1

5.6 — Sauvegarde du modèle (Lignes 208-211)

with open('modele_classification_taln_llm.pkl', 'wb') as f:
    pickle.dump(pipeline, f)

Grâce à notre __getstate__, ce fichier ne pèse que 44 Ko au lieu de 540 Mo.


Glossaire Express (Pour le Jury)

Terme Définition simple
DistilBERT Version allégée de BERT (Google). Comprend 104 langues. 66M de paramètres.
Tokenisation Découper une phrase en morceaux (tokens) et leur attribuer un numéro unique.
Embedding Vecteur de 768 nombres qui représente le "sens" d'une phrase.
Transfer Learning Réutiliser un modèle déjà entraîné (DistilBERT) pour une nouvelle tâche.
Pipeline Chaîne automatisée : Texte → Vecteur → Prédiction.
Stratify Garantir la même proportion de chaque classe dans le train et le test.
Class Weight Balanced Donner plus d'importance aux classes rares pendant l'entraînement.
MTTR Mean Time To Resolution. Temps médian pour résoudre un incident.
Pickle Format Python pour sauvegarder un objet (modèle) sur le disque dur.
__getstate__ Méthode Python qui contrôle ce qui est sauvegardé par Pickle.