Spaces:
Running
Running
Commit ·
3ddc50c
0
Parent(s):
maj
Browse files- .gitattributes +36 -0
- Dockerfile +22 -0
- README.md +10 -0
- api.py +33 -0
- app.py +192 -0
- logo_gdm.png +0 -0
- requirements.txt +18 -0
- setup.sh +10 -0
- train_V3.py +159 -0
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.plk filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# On ajoute 'curl' qui est parfois nécessaire pour les téléchargements de modèles
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
git \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# On upgrade pip AVANT d'installer le reste
|
| 13 |
+
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
| 14 |
+
|
| 15 |
+
COPY . /app
|
| 16 |
+
|
| 17 |
+
# On installe le requirements
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
+
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: GDM Aide RUN V2
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
api.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from app import predire_ticket # On importe ta fonction depuis app.py
|
| 4 |
+
|
| 5 |
+
# Initialisation de l'API
|
| 6 |
+
app = FastAPI(title="GDM Ticket Classifier API")
|
| 7 |
+
|
| 8 |
+
# Définition du format de donnée attendu (JSON)
|
| 9 |
+
class TicketInput(BaseModel):
|
| 10 |
+
objet: str
|
| 11 |
+
description: str
|
| 12 |
+
|
| 13 |
+
@app.get("/")
|
| 14 |
+
def home():
|
| 15 |
+
return {"message": "API GDM opérationnelle. Utilisez l'endpoint /predict en POST."}
|
| 16 |
+
|
| 17 |
+
@app.post("/predict")
|
| 18 |
+
def api_predict(ticket: TicketInput):
|
| 19 |
+
# Sécurité : Si le modèle n'est pas chargé, on renvoie une erreur propre
|
| 20 |
+
from app import MODELE, MTTR_MOYENS
|
| 21 |
+
if MODELE is None:
|
| 22 |
+
return {"error": "Le modèle n'est pas chargé. Vérifiez les fichiers .pkl"}
|
| 23 |
+
|
| 24 |
+
obj = str(ticket.objet)
|
| 25 |
+
desc = str(ticket.description)
|
| 26 |
+
|
| 27 |
+
categorie, mttr, confiance = predire_ticket(obj, desc)
|
| 28 |
+
|
| 29 |
+
return {
|
| 30 |
+
"pole_expertise": str(categorie),
|
| 31 |
+
"temps_resolution_estime_h": float(mttr) if mttr is not None else 0.0,
|
| 32 |
+
"score_confiance": round(float(confiance), 4)
|
| 33 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pickle
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import os
|
| 7 |
+
import io
|
| 8 |
+
from transformers import AutoTokenizer, AutoModel
|
| 9 |
+
from sklearn.base import BaseEstimator, TransformerMixin
|
| 10 |
+
|
| 11 |
+
# --- 0. DÉFINITION DE LA CLASSE CUSTOM (Indispensable pour charger le PKL) ---
|
| 12 |
+
class LLMFeatureExtractor(BaseEstimator, TransformerMixin):
|
| 13 |
+
def __init__(self, model_name='distilbert-base-multilingual-cased', max_length=128):
|
| 14 |
+
self.model_name = model_name
|
| 15 |
+
self.max_length = max_length
|
| 16 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 17 |
+
self.model = AutoModel.from_pretrained(model_name).to("cpu")
|
| 18 |
+
|
| 19 |
+
def fit(self, X, y=None):
|
| 20 |
+
return self
|
| 21 |
+
|
| 22 |
+
def transform(self, X):
|
| 23 |
+
m_len = getattr(self, 'max_length', 128)
|
| 24 |
+
texts = X.tolist() if hasattr(X, 'tolist') else list(X)
|
| 25 |
+
texts = [str(t)[:1000] for t in texts]
|
| 26 |
+
inputs = self.tokenizer(
|
| 27 |
+
texts,
|
| 28 |
+
padding=True,
|
| 29 |
+
truncation=True,
|
| 30 |
+
max_length=m_len,
|
| 31 |
+
return_tensors="pt"
|
| 32 |
+
).to("cpu")
|
| 33 |
+
with torch.no_grad():
|
| 34 |
+
outputs = self.model(**inputs)
|
| 35 |
+
return outputs.last_hidden_state[:, 0, :].detach().numpy()
|
| 36 |
+
|
| 37 |
+
# Sécurité pour le désarchivage du modèle sur CPU
|
| 38 |
+
class CPU_Unpickler(pickle.Unpickler):
|
| 39 |
+
def find_class(self, module, name):
|
| 40 |
+
# Force Python à chercher LLMFeatureExtractor dans le module app
|
| 41 |
+
if name == 'LLMFeatureExtractor':
|
| 42 |
+
import app
|
| 43 |
+
return app.LLMFeatureExtractor
|
| 44 |
+
if module == 'torch.storage' and name == '_load_from_bytes':
|
| 45 |
+
return lambda b: torch.load(io.BytesIO(b), map_location='cpu')
|
| 46 |
+
return super().find_class(module, name)
|
| 47 |
+
|
| 48 |
+
# --- 1. CHARGEMENT DYNAMIQUE ---
|
| 49 |
+
@st.cache_resource
|
| 50 |
+
def charger_modele_et_data():
|
| 51 |
+
# Chemins robustes pour environnement local et cloud
|
| 52 |
+
base_path = os.path.dirname(__file__)
|
| 53 |
+
MODELE_PATH = os.path.join(base_path, 'modele_classification_taln_llm.pkl')
|
| 54 |
+
MTTR_PATH = os.path.join(base_path, 'mttr_moyennes.pkl')
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
with open(MODELE_PATH, 'rb') as file:
|
| 58 |
+
modele = CPU_Unpickler(file).load()
|
| 59 |
+
|
| 60 |
+
mttr_moyens = {}
|
| 61 |
+
if os.path.exists(MTTR_PATH):
|
| 62 |
+
with open(MTTR_PATH, 'rb') as f:
|
| 63 |
+
mttr_moyens = pickle.load(f)
|
| 64 |
+
|
| 65 |
+
return modele, mttr_moyens
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"Erreur de chargement : {e}")
|
| 68 |
+
return None, None
|
| 69 |
+
|
| 70 |
+
# Chargement global pour être accessible par api.py
|
| 71 |
+
MODELE, MTTR_MOYENS = charger_modele_et_data()
|
| 72 |
+
|
| 73 |
+
# --- 2. FONCTION DE PRÉDICTION ---
|
| 74 |
+
def predire_ticket(objet: str, description: str):
|
| 75 |
+
texte_complet_saisi = str(objet).strip() + " " + str(description).strip()
|
| 76 |
+
if not texte_complet_saisi.strip():
|
| 77 |
+
return "Saisie vide", 0.0, 0.0
|
| 78 |
+
|
| 79 |
+
X_inference = pd.Series([texte_complet_saisi])
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
probabilites = MODELE.predict_proba(X_inference)[0]
|
| 83 |
+
confiance = np.max(probabilites)
|
| 84 |
+
prediction_brute = MODELE.classes_[np.argmax(probabilites)]
|
| 85 |
+
except Exception as e:
|
| 86 |
+
prediction_brute = "Erreur"
|
| 87 |
+
confiance = 0.0
|
| 88 |
+
|
| 89 |
+
# SEUIL DE CONFIANCE GDM
|
| 90 |
+
SEUIL_CRITIQUE = 0.65
|
| 91 |
+
if confiance < SEUIL_CRITIQUE or str(prediction_brute).upper() == "AUTRES":
|
| 92 |
+
categorie_finale = "À qualifier (Hors catalogue)"
|
| 93 |
+
else:
|
| 94 |
+
categorie_finale = prediction_brute
|
| 95 |
+
|
| 96 |
+
mttr_predit = MTTR_MOYENS.get(prediction_brute, 0.0)
|
| 97 |
+
return categorie_finale, mttr_predit, confiance
|
| 98 |
+
|
| 99 |
+
# --- 3. INTERFACE STREAMLIT ---
|
| 100 |
+
# Cette partie ne s'exécute QUE si on lance 'streamlit run app.py'
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
st.set_page_config(page_title="GDM Aide au RUN V4", layout="wide", page_icon="👗")
|
| 103 |
+
|
| 104 |
+
st.markdown("""
|
| 105 |
+
<style>
|
| 106 |
+
.stApp { background-color: #FFFFFF !important; }
|
| 107 |
+
html, body, [data-testid="stWidgetLabel"], .stText, p { color: #1A1A1A !important; }
|
| 108 |
+
.stButton>button { background-color: #E29792 !important; color: white !important; border-radius: 20px; font-weight: bold; }
|
| 109 |
+
.stMetric { background-color: #FDF2F1 !important; padding: 15px; border-radius: 10px; border-left: 5px solid #E29792 !important; }
|
| 110 |
+
[data-testid="stMetricValue"], [data-testid="stMetricLabel"] { color: #1A1A1A !important; }
|
| 111 |
+
</style>
|
| 112 |
+
""", unsafe_allow_html=True)
|
| 113 |
+
|
| 114 |
+
st.title("👗 Grain de Malice - Aide au RUN")
|
| 115 |
+
st.subheader("Optimisation par Pôles d'Expertise Consolidés")
|
| 116 |
+
|
| 117 |
+
with st.sidebar:
|
| 118 |
+
# 1. Logo et En-tête
|
| 119 |
+
if os.path.exists("logo_gdm.png"):
|
| 120 |
+
st.image("logo_gdm.png", width=150)
|
| 121 |
+
st.header("🚀 Spécifications V4")
|
| 122 |
+
st.success("✅ **Modèle :** DistilBERT Multilingual")
|
| 123 |
+
st.info("📊 **Mapping :** 3 Pôles Stratégiques")
|
| 124 |
+
|
| 125 |
+
# 2. Section Expertise (Ton tableau MTTR d'origine)
|
| 126 |
+
st.markdown("---")
|
| 127 |
+
st.subheader("📊 MTTR par Pôle")
|
| 128 |
+
if MTTR_MOYENS:
|
| 129 |
+
df_mttr = pd.DataFrame([
|
| 130 |
+
{"Pôle": k, "Délai Moyen": f"{v:.1f} h"}
|
| 131 |
+
for k, v in MTTR_MOYENS.items() if v is not None
|
| 132 |
+
])
|
| 133 |
+
st.table(df_mttr)
|
| 134 |
+
|
| 135 |
+
# 3. NOUVEAU : Architecture & Industrialisation (Pour la certif)
|
| 136 |
+
st.markdown("---")
|
| 137 |
+
st.subheader("🛠️ Industrialisation")
|
| 138 |
+
|
| 139 |
+
with st.expander("🌐 API & Intégration"):
|
| 140 |
+
st.write("""
|
| 141 |
+
Exposition via **FastAPI** :
|
| 142 |
+
- Endpoint : `/predict`
|
| 143 |
+
- Format : JSON REST
|
| 144 |
+
- Compatible : ServiceNow, Jira, Apps Magasins.
|
| 145 |
+
""")
|
| 146 |
+
|
| 147 |
+
with st.expander("📦 Déploiement Docker"):
|
| 148 |
+
st.write("""
|
| 149 |
+
- Conteneur : `python:3.10-slim`
|
| 150 |
+
- Stockage : Git LFS (Large File Storage)
|
| 151 |
+
- Scalabilité : Prêt pour Kubernetes.
|
| 152 |
+
""")
|
| 153 |
+
|
| 154 |
+
st.caption("Innovation IT - Grain de Malice © 2026")
|
| 155 |
+
|
| 156 |
+
st.markdown("---")
|
| 157 |
+
|
| 158 |
+
col_input1, col_input2 = st.columns(2)
|
| 159 |
+
with col_input1:
|
| 160 |
+
objet_saisi = st.text_input("📍 Objet du ticket :")
|
| 161 |
+
with col_input2:
|
| 162 |
+
description_saisie = st.text_area("📝 Description détaillée :")
|
| 163 |
+
|
| 164 |
+
if st.button("🚀 Lancer le Diagnostic IA"):
|
| 165 |
+
if not objet_saisi and not description_saisie:
|
| 166 |
+
st.warning("Veuillez saisir des informations.")
|
| 167 |
+
else:
|
| 168 |
+
with st.spinner('Analyse sémantique en cours...'):
|
| 169 |
+
categorie, mttr, confiance = predire_ticket(objet_saisi, description_saisie)
|
| 170 |
+
|
| 171 |
+
st.markdown("### ✅ Résultat du Diagnostic")
|
| 172 |
+
res1, res2, res3 = st.columns([2, 1, 1])
|
| 173 |
+
|
| 174 |
+
with res1:
|
| 175 |
+
st.metric("Pôle d'Expertise Prédit", categorie)
|
| 176 |
+
with res2:
|
| 177 |
+
st.metric("MTTR Estimé", f"{mttr:.1f} h" if mttr else "N/A")
|
| 178 |
+
with res3:
|
| 179 |
+
st.metric("Confiance IA", f"{confiance*100:.1f} %")
|
| 180 |
+
|
| 181 |
+
# ALERTES CONTEXTUELLES V4
|
| 182 |
+
if "À qualifier" in categorie:
|
| 183 |
+
st.warning("⚠️ **VÉRIFICATION :** Routage manuel nécessaire (Confiance trop faible).")
|
| 184 |
+
elif "Business & Ventes" in categorie:
|
| 185 |
+
st.success("💰 **COMMERCE :** Ticket orienté vers le pôle Business (Impact CA).")
|
| 186 |
+
elif "Data & Finance" in categorie:
|
| 187 |
+
st.info("📊 **DATA :** Analyse de flux financiers ou reporting BI requis.")
|
| 188 |
+
elif "Opérations & Support" in categorie:
|
| 189 |
+
st.error("🚨 **OPS & RUN :** Incident sur les flux techniques ou le support RUN.")
|
| 190 |
+
|
| 191 |
+
st.info("💡 **Note :** Cette version 3-pôles réduit le bruit et augmente la précision du routage automatique.")
|
| 192 |
+
st.sidebar.caption("🟢 Container Status: Healthy (Port 7860)")
|
logo_gdm.png
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Base ---
|
| 2 |
+
streamlit
|
| 3 |
+
pandas
|
| 4 |
+
numpy
|
| 5 |
+
scikit-learn
|
| 6 |
+
|
| 7 |
+
# --- TALN (Ordre spécifique pour éviter les conflits) ---
|
| 8 |
+
huggingface-hub
|
| 9 |
+
tokenizers
|
| 10 |
+
transformers
|
| 11 |
+
torch
|
| 12 |
+
spacy
|
| 13 |
+
gradio_client
|
| 14 |
+
fastapi
|
| 15 |
+
uvicorn
|
| 16 |
+
|
| 17 |
+
# --- Modèle SpaCy ---
|
| 18 |
+
https://github.com/explosion/spacy-models/releases/download/fr_core_news_sm-3.7.0/fr_core_news_sm-3.7.0.tar.gz
|
setup.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Mise à jour de pip
|
| 4 |
+
pip install --upgrade pip
|
| 5 |
+
|
| 6 |
+
# Installation des dépendances du requirements.txt
|
| 7 |
+
pip install -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Téléchargement explicite du modèle SpaCy français
|
| 10 |
+
python -m spacy download fr_core_news_sm
|
train_V3.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import pickle
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
import os
|
| 6 |
+
import csv
|
| 7 |
+
from transformers import AutoTokenizer, AutoModel
|
| 8 |
+
from sklearn.base import BaseEstimator, TransformerMixin
|
| 9 |
+
from sklearn.pipeline import Pipeline
|
| 10 |
+
from sklearn.linear_model import LogisticRegression
|
| 11 |
+
|
| 12 |
+
# 1. Sécurité pour les gros volumes de texte (évite les erreurs de lecture)
|
| 13 |
+
csv.field_size_limit(10000000)
|
| 14 |
+
|
| 15 |
+
# --- CLASSE D'EXTRACTION LLM (DISTILBERT) ---
|
| 16 |
+
class LLMFeatureExtractor(BaseEstimator, TransformerMixin):
|
| 17 |
+
def __init__(self, model_name='distilbert-base-multilingual-cased', max_length=128, batch_size=32):
|
| 18 |
+
self.model_name = model_name
|
| 19 |
+
self.max_length = max_length
|
| 20 |
+
self.batch_size = batch_size # On traite par petits paquets
|
| 21 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 22 |
+
self.model = AutoModel.from_pretrained(model_name).to("cpu")
|
| 23 |
+
|
| 24 |
+
def fit(self, X, y=None):
|
| 25 |
+
return self
|
| 26 |
+
|
| 27 |
+
def transform(self, X):
|
| 28 |
+
texts = [str(t)[:1000] for t in X]
|
| 29 |
+
all_embeddings = []
|
| 30 |
+
|
| 31 |
+
# Traitement par lots pour économiser la RAM
|
| 32 |
+
for i in range(0, len(texts), self.batch_size):
|
| 33 |
+
batch_texts = texts[i : i + self.batch_size]
|
| 34 |
+
inputs = self.tokenizer(
|
| 35 |
+
batch_texts,
|
| 36 |
+
padding=True,
|
| 37 |
+
truncation=True,
|
| 38 |
+
max_length=self.max_length,
|
| 39 |
+
return_tensors="pt"
|
| 40 |
+
).to("cpu")
|
| 41 |
+
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
outputs = self.model(**inputs)
|
| 44 |
+
# On récupère les embeddings et on les remet sur CPU/Numpy
|
| 45 |
+
embeddings = outputs.last_hidden_state[:, 0, :].detach().numpy()
|
| 46 |
+
all_embeddings.append(embeddings)
|
| 47 |
+
|
| 48 |
+
if i % 512 == 0:
|
| 49 |
+
print(f"⏳ Progression : {i}/{len(texts)} tickets vectorisés...")
|
| 50 |
+
|
| 51 |
+
return np.vstack(all_embeddings)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# --- CONFIGURATION V3 ---
|
| 55 |
+
# Liste tes deux fichiers d'export ici
|
| 56 |
+
FICHIERS_ENTREE = ["Tickets_1.csv", "Tickets_2.csv"]
|
| 57 |
+
NOM_MODELE_GENERE = "modele_classification_taln_llm.pkl"
|
| 58 |
+
NOM_MTTR_GENERE = "mttr_moyennes.pkl"
|
| 59 |
+
|
| 60 |
+
def normaliser(texte):
|
| 61 |
+
""" Nettoyage pour matcher les colonnes peu importe l'encodage """
|
| 62 |
+
return str(texte).strip().lower().replace('é', 'e').replace('è', 'e').replace('ê', 'e')
|
| 63 |
+
|
| 64 |
+
def map_pole_expertise(groupe):
|
| 65 |
+
""" Regroupement sémantique en 3 pôles majeurs pour optimiser l'apprentissage """
|
| 66 |
+
g = str(groupe).upper()
|
| 67 |
+
|
| 68 |
+
# Pôle 1 : COMMERCE & BUSINESS (Le plus gros volume)
|
| 69 |
+
if "COMMERCE" in g:
|
| 70 |
+
return "Business & Ventes"
|
| 71 |
+
|
| 72 |
+
# Pôle 2 : DATA & FINANCE (Gestion de la donnée et des chiffres)
|
| 73 |
+
elif any(keyword in g for keyword in ["DATA", "BI", "FINANCE"]):
|
| 74 |
+
return "Data & Finance"
|
| 75 |
+
|
| 76 |
+
# Pôle 3 : OPS & SUPPORT (Maintenance et flux techniques)
|
| 77 |
+
elif any(keyword in g for keyword in ["RUN", "OPCON", "FLUX", "AUTOMATE"]):
|
| 78 |
+
return "Opérations & Support"
|
| 79 |
+
|
| 80 |
+
else:
|
| 81 |
+
return "AUTRES"
|
| 82 |
+
|
| 83 |
+
def lancer_entrainement_v3():
|
| 84 |
+
print(f"🚀 Initialisation du Pipeline V3...")
|
| 85 |
+
|
| 86 |
+
# 1. CHARGEMENT ET FUSION DES DEUX SOURCES
|
| 87 |
+
liste_df = []
|
| 88 |
+
for f in FICHIERS_ENTREE:
|
| 89 |
+
if os.path.exists(f):
|
| 90 |
+
try:
|
| 91 |
+
temp_df = pd.read_csv(f, sep=None, engine='python', encoding='utf-8-sig')
|
| 92 |
+
liste_df.append(temp_df)
|
| 93 |
+
print(f"📖 Chargé : {f} ({len(temp_df)} lignes)")
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"⚠️ Erreur sur {f} : {e}")
|
| 96 |
+
|
| 97 |
+
if not liste_df:
|
| 98 |
+
print("❌ Aucun fichier trouvé. Vérifiez les noms des CSV.")
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
df = pd.concat(liste_df, ignore_index=True)
|
| 102 |
+
print(f"🔗 Fusion réussie : {len(df)} tickets consolidés.")
|
| 103 |
+
|
| 104 |
+
# 2. DÉTECTION DES COLONNES
|
| 105 |
+
mapping = {normaliser(c): c for c in df.columns}
|
| 106 |
+
col_desc = mapping.get('description')
|
| 107 |
+
col_grp = mapping.get('groupe')
|
| 108 |
+
col_mttr = next((c for norm, c in mapping.items() if 'delai' in norm or 'resolution' in norm), None)
|
| 109 |
+
|
| 110 |
+
if not all([col_desc, col_grp]):
|
| 111 |
+
print(f"❌ Colonnes 'Description' ou 'Groupe' introuvables. Colonnes vues : {list(mapping.keys())}")
|
| 112 |
+
return
|
| 113 |
+
|
| 114 |
+
# 3. NETTOYAGE ET MAPPING DES CLASSES
|
| 115 |
+
df = df.dropna(subset=[col_desc, col_grp])
|
| 116 |
+
df = df.drop_duplicates(subset=[col_desc]) # Supprime les doublons pour un meilleur apprentissage
|
| 117 |
+
df['categorie'] = df[col_grp].apply(map_pole_expertise)
|
| 118 |
+
|
| 119 |
+
print(f"🧹 Nettoyage terminé : {len(df)} tickets uniques.")
|
| 120 |
+
|
| 121 |
+
# 4. CALCUL DU MTTR DYNAMIQUE
|
| 122 |
+
if col_mttr:
|
| 123 |
+
def convertir_en_heures(val):
|
| 124 |
+
try:
|
| 125 |
+
if ":" in str(val): # Gestion format HH:MM:SS
|
| 126 |
+
p = str(val).split(':')
|
| 127 |
+
return float(p[0]) + float(p[1])/60 + float(p[2])/3600
|
| 128 |
+
return float(val) # Gestion format décimal
|
| 129 |
+
except: return np.nan
|
| 130 |
+
|
| 131 |
+
df['mttr_h'] = df[col_mttr].apply(convertir_en_heures)
|
| 132 |
+
# On ignore les valeurs aberrantes (ex: tickets restés ouverts 2 ans)
|
| 133 |
+
df_mttr = df[df['mttr_h'] < 1500].dropna(subset=['mttr_h'])
|
| 134 |
+
|
| 135 |
+
mttr_moyennes = df_mttr.groupby('categorie')['mttr_h'].mean().to_dict()
|
| 136 |
+
with open(NOM_MTTR_GENERE, 'wb') as f:
|
| 137 |
+
pickle.dump(mttr_moyennes, f)
|
| 138 |
+
print(f"📊 Référentiel MTTR généré pour {len(mttr_moyennes)} pôles.")
|
| 139 |
+
|
| 140 |
+
# 5. ENTRAÎNEMENT HYBRIDE (LLM + LOGISTIC REGRESSION)
|
| 141 |
+
print("🧠 Entraînement DistilBERT en cours (cela peut prendre quelques minutes)...")
|
| 142 |
+
X = df[col_desc].astype(str)
|
| 143 |
+
y = df['categorie']
|
| 144 |
+
|
| 145 |
+
pipeline = Pipeline([
|
| 146 |
+
('feature_extractor', LLMFeatureExtractor()),
|
| 147 |
+
('classifier', LogisticRegression(max_iter=1000, solver='lbfgs', multi_class='multinomial'))
|
| 148 |
+
])
|
| 149 |
+
|
| 150 |
+
pipeline.fit(X, y)
|
| 151 |
+
|
| 152 |
+
# 6. SAUVEGARDE FINALE
|
| 153 |
+
with open(NOM_MODELE_GENERE, 'wb') as f:
|
| 154 |
+
pickle.dump(pipeline, f)
|
| 155 |
+
|
| 156 |
+
print(f"✨ MISSION V3 RÉUSSIE ! Modèle et MTTR sauvegardés.")
|
| 157 |
+
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
lancer_entrainement_v3()
|