Spaces:
Running
Running
| # 🧠 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. | | |