Spaces:
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)
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 lePipeline.BaseEstimatordonneget_params()etset_params().TransformerMixindonnefit_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 faireif "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 évaluerrandom_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). Avecbalanced, 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. |