# 🧠 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) ```python 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 ``` > [!TIP] > **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) ```python 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) ```python 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" ``` > [!NOTE] > **`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) ```python 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é ```python 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 ``` > [!IMPORTANT] > **`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) ```python 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) ```python 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" # ... ``` > [!NOTE] > **`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) ```python 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) ```python 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) ```python # 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) ```python # 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) ```python # 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 ```python 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 )) ]) ``` > [!IMPORTANT] > **`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. ```python 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) ```python 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. |