Category Classification Engine

API Django de classification automatique des frais annexes de Bons de Commande (BDC) automobile en France.

Le moteur utilise un pipeline à 3 niveaux : règles regex déterministes → modèle SetFit fine-tuné → revue humaine (A_CLASSIFIER). Toute la configuration se fait via Django Admin — aucune modification de code nécessaire.


Onboarding : Créer un moteur de classification

Cette section décrit comment constituer un nouveau jeu de catégories de A à Z.

Étape 1 — Extraire et analyser les données source

À partir des Bons de Commande réels, extraire le tableau des frais annexes (libellés + montants). Analyser la fréquence et la sémantique des libellés pour identifier les regroupements naturels.

Questions à se poser :

Étape 2 — Définir les catégories

Pour chaque catégorie identifiée, créer une entrée dans Django Admin (/admin/classifier/category/) avec :

Minimum recommandé : 5 catégories. Maximum pratique sur un seul modèle : ~20.

Étape 3 — Constituer le jeu d'entraînement

Pour chaque catégorie, collecter 8 à 30 libellés réels et variés issus des BDC. La variété compte plus que la quantité : Carte grise, CG, Certificat immatriculation, Carte Grise + malus sont 4 exemples qui valent mieux que 20 fois Carte grise.

Le jeu d'entraînement peut être fourni de deux manières :

Étape 4 — Importer les données

# Import complet depuis les fichiers JSON (supprime l'existant)
docker compose exec category_web_app python manage.py import_training_data --clear

# Import incrémental (préserve l'existant, ignore les doublons)
docker compose exec category_web_app python manage.py import_training_data

# Preview sans modifier la base
docker compose exec category_web_app python manage.py import_training_data --dry-run

# Exemples uniquement (skip les règles regex)
docker compose exec category_web_app python manage.py import_training_data --skip-rules

Étape 5 — Ajouter les règles regex

Les règles regex capturent les cas déterministes sans passer par le modèle ML (confiance fixe 0.95, <1ms). Elles prennent effet immédiatement après sauvegarde dans l'Admin, sans redémarrage.

Dans Django Admin (/admin/classifier/categoryrule/) :

Règles de priorité importantes :

Étape 6 — Entraîner le modèle

# Entraînement (~90s sur CPU)
docker compose exec category_web_app python manage.py train_setfit

# Avec évaluation de précision sur un holdout de 20%
docker compose exec category_web_app python manage.py train_setfit --evaluate

# OBLIGATOIRE après l'entraînement — le serveur web tourne dans un processus séparé
docker compose restart category_web_app

Étape 7 — Tester avec des données réelles

# Obtenir un token JWT
TOKEN=$(curl -s -X POST https://categoryclassification.aiichaa.com/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"YOUR_PASSWORD"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access'])")

# Classifier des frais annexes
curl -s https://categoryclassification.aiichaa.com/api/v1/classify/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"frais_annexes": [{"name": "Carte Grise", "prix": "238"}, {"name": "Waxoyl", "prix": "219"}]}'

Analyser les résultats :

Étape 8 — Boucle feedback → révision → réentraînement

  1. L'application cliente collecte les items A_CLASSIFIER et envoie des corrections à POST /api/v1/feedback/
  2. Un ticket Mantis est automatiquement créé pour chaque feedback soumis
  3. Le responsable révise depuis l'Admin Django (lien direct dans le ticket Mantis) — il peut uniquement modifier correct_category et le statut d'approbation
  4. Une fois les feedbacks traités, appliquer les corrections et réentraîner :
docker compose exec category_web_app python manage.py apply_feedback --retrain
docker compose restart category_web_app

Pipeline de Classification

POST /api/v1/classify/  (JWT requis)
        │
        ▼
┌─────────────────────────────────────────┐
│ Niveau 1 : RÈGLES REGEX (depuis la DB)  │
│ Déterministe, <1ms, confiance 0.95      │
│ Match unique  → catégorie               │
│ Multi-match   → A_CLASSIFIER            │
└────────────────┬────────────────────────┘
                 │ aucun match
                 ▼
┌─────────────────────────────────────────┐
│ Niveau 2 : MODÈLE SETFIT                │
│ Fine-tuné sur vos données, ~50ms/item   │
│ conf >= seuil → catégorie               │
│ conf < seuil  → A_CLASSIFIER            │
└────────────────┬────────────────────────┘
                 │ faible confiance
                 ▼
┌─────────────────────────────────────────┐
│ Niveau 3 : A_CLASSIFIER (revue humaine) │
└─────────────────────────────────────────┘

Réentraînement du modèle : ce qui se passe exactement

L'entraînement est toujours de zéro (non incrémental). Voici exactement ce que fait train_setfit :

  1. Chargement de toutes les catégories actives et leurs exemples depuis la DB (sources : import initial + feedbacks appliqués + saisies manuelles)
  2. Suppression de l'ancien modèle sur disque (src/classifier/fixtures/setfit_model/)
  3. Chargement du modèle de base depuis le cache HuggingFace local (déjà dans l'image Docker — aucun téléchargement réseau)
  4. Fine-tuning contrastif : SetFit génère des paires positives/négatives et ajuste les poids du SentenceTransformer pour que vos catégories soient bien séparées dans l'espace vectoriel
  5. Entraînement de la tête de classification (LogisticRegression) sur les nouveaux embeddings
  6. Sauvegarde du modèle complet sur disque
  7. Mise à jour du cache en mémoire — mais uniquement dans le processus du management command
Le serveur web tourne dans un processus séparé. Il conserve l'ancien modèle en mémoire jusqu'au redémarrage. C'est pourquoi docker compose restart category_web_app est obligatoire après chaque entraînement.

Points importants :


Boucle d'amélioration continue

  ┌────── API classifie les frais ◄──────────────────┐
  │                                                   │
  ▼                                                   │
  Items A_CLASSIFIER → client envoie POST /feedback/ │
  │                                                   │
  ▼                                                   │
  Ticket Mantis créé automatiquement                  │
  │                                                   │
  ▼                                                   │
  Responsable révise dans Django Admin                │
  (approuve / refuse / corrige la catégorie)          │
  │                                                   │
  ▼                                                   │
  manage.py apply_feedback --retrain                  │
  │                                                   │
  └───────────────────────────────────────────────────┘

Quick Start

# 1. Configurer l'environnement
cp .env.example .env
# Éditer .env avec vos valeurs (voir section Configuration)

# 2. Build et démarrage
docker compose up --build -d

# 3. Migrations
docker compose exec category_web_app python manage.py makemigrations classifier
docker compose exec category_web_app python manage.py migrate

# 4. Importer catégories, règles regex et exemples d'entraînement
docker compose exec category_web_app python manage.py import_training_data --clear

# 5. Entraîner le modèle SetFit (~90s sur CPU)
docker compose exec category_web_app python manage.py train_setfit

# 6. Redémarrer pour charger le modèle en mémoire
docker compose restart category_web_app

# 7. Obtenir un token JWT
curl -X POST http://localhost:8042/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "adminPass"}'

Configuration

Variables d'environnement (.env)

VariableDéfautDescription
SECRET_KEYClé secrète Django (obligatoire en prod)
DEBUG1Mode debug (0 en production)
ALLOWED_HOSTS*Hôtes autorisés, séparés par virgules
POSTGRES_DBcategory_dbNom de la base de données
POSTGRES_USERappuserUtilisateur PostgreSQL
POSTGRES_PASSWORDMot de passe PostgreSQL
DATABASE_URLURL complète (prioritaire sur les vars individuelles)
WEB_PORT8042Port externe Django
DB_PORT5434Port externe PostgreSQL
MANTIS_ENABLED0Activer l'intégration Mantis (1/0)
MANTIS_URLURL de l'instance Mantis (ex: https://mantis.example.com)
MANTIS_TOKENToken API Mantis
MANTIS_PROJECT_NAMENom exact du projet dans Mantis
Important : après toute modification du .env, utiliser docker compose up -d (pas restart) pour que les nouvelles variables soient injectées dans le conteneur.

Ports

ServicePort interne (conteneur)Port externe (hôte)
Django80008042 (configurable via WEB_PORT)
PostgreSQL54325434 (configurable via DB_PORT)

Authentification (JWT)

Tous les endpoints sauf /api/v1/status/ nécessitent un token Bearer JWT.

# Obtenir un token
curl -X POST http://localhost:8042/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "adminPass"}'

# Utiliser le token
curl http://localhost:8042/api/v1/categories/ \
  -H "Authorization: Bearer eyJ..."

# Rafraîchir un token expiré (expire après 5 minutes)
curl -X POST http://localhost:8042/api/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJ..."}'

Throttling : 100 req/min pour les accès anonymes, 1000 req/min pour les accès authentifiés.


API Endpoints

EndpointMéthodeAuthDescription
/GETNonHomepage (ce document)
/api/token/POSTNonObtenir un JWT
/api/token/refresh/POSTNonRafraîchir un JWT
/api/v1/status/GETNonStatut du moteur (règles, modèle, stats)
/api/v1/categories/GETBearerListe des catégories actives
/api/v1/classify/POSTBearerClassifier des frais annexes
/api/v1/feedback/POSTBearerSoumettre une correction

Classifier des frais (POST /api/v1/classify/)

{
  "frais_annexes": [
    {"name": "Carte Grise (hors malus éventuel)", "prix": "238,00"},
    {"name": "Waxoyl", "prix": "219,00"},
    {"name": "immat + transport", "prix": "450,00"}
  ],
  "confidence_threshold": 0.55
}

Réponse :

{
  "frais_annexes": {
    "carte_grise": [
      {"name": "carte_grise_1", "code": "Carte Grise (hors malus éventuel)",
       "prix": "238,00", "confidence": 0.95, "method": "rules"}
    ],
    "traitement_carrosserie": [
      {"name": "traitement_carrosserie_1", "code": "Waxoyl",
       "prix": "219,00", "confidence": 0.95, "method": "rules"}
    ],
    "A_CLASSIFIER": [
      {"name": "A_CLASSIFIER_1", "code": "immat + transport",
       "prix": "450,00", "confidence": 0.0, "method": "rules_multi_match",
       "matched_categories": ["frais_administratif", "transport_livraison"],
       "needs_review": true}
    ]
  },
  "meta": {
    "total_items": 3, "classified": 2, "needs_review": 1,
    "processing_time_ms": 14.2,
    "methods": {"rules": 2, "rules_multi_match": 1, "setfit": 0}
  }
}

Soumettre un feedback (POST /api/v1/feedback/)

{
  "original_libelle": "Frais Aménagement DANGEL",
  "predicted_category": "A_CLASSIFIER",
  "correct_category": "options_vehicule",
  "notes": "DANGEL = kit aménagement 4x4"
}

Les 15 catégories (+ A_CLASSIFIER)

CatégorieDescriptionFréquence
carte_griseCertificat d'immatriculation, taxes, redevances~45%
carburantPlein essence/diesel, charge électrique, câble~11%
accessoiresTapis, plaques luxe, Fix&Go, attelage~8%
frais_administratifDémarches, courtage, immatriculation hors CG~6.5%
garantie_extensionRoole Confort, Coyote, Forfait Sérénité~5.5%
gravageNomblot, Eurodatacar, marquage antivol~5.5%
traitement_carrosserieWaxoyl, PowerShine, céramique, lustrage~5.5%
aides_gouvernementalesMalus CO2, bonus écologique, prime CEE~4%
pack_securiteGilet + triangle, extincteur, Crit'Air~3.5%
mise_a_la_routePréparation véhicule avant livraison~2.3%
autres_prestationsNettoyage, installation, services divers~1%
transport_livraisonTransport, convoyage, mise à disposition~0.3%
options_vehiculePeinture métallisée, toit ouvrant, packs constructeur~0.2%
remise_commercialePéréquation, remise, renfort LOA~0.1%
contrat_entretienMaintenance Plus, Allure Care, FlexCare<0.1%
A_CLASSIFIERRevue humaine obligatoire~2-5%

Chaque catégorie porte des métadonnées comptables gérées dans l'Admin :


Management Commands

import_training_data — Import des données initiales

Importe catégories, exemples d'entraînement et règles regex depuis les fichiers JSON de src/classifier/fixtures/.

docker compose exec category_web_app python manage.py import_training_data --clear    # clean install
docker compose exec category_web_app python manage.py import_training_data            # incremental
docker compose exec category_web_app python manage.py import_training_data --dry-run  # preview
docker compose exec category_web_app python manage.py import_training_data --skip-rules

train_setfit — Entraîner le modèle SetFit

docker compose exec category_web_app python manage.py train_setfit             # entraînement
docker compose exec category_web_app python manage.py train_setfit --evaluate  # + évaluation
docker compose exec category_web_app python manage.py train_setfit --dry-run   # preview données
docker compose restart category_web_app  # OBLIGATOIRE après chaque entraînement

apply_feedback — Appliquer les corrections humaines

Traite les feedbacks approuvés : ajoute les libellés comme exemples d'entraînement dans la bonne catégorie, les retire de la mauvaise, et signale les conflits avec les règles regex.

docker compose exec category_web_app python manage.py apply_feedback --dry-run   # preview
docker compose exec category_web_app python manage.py apply_feedback --retrain   # appliquer + réentraîner
docker compose restart category_web_app

Modèles de données

ModèleRôle
CategoryCatégories de classification (clé, nom, TVA, code PCG)
CategoryRuleRègles regex par catégorie (pattern, priorité, description)
TrainingExampleLibellés d'entraînement par catégorie (source : import / feedback / manuel)
ClassificationLogJournal de chaque appel API de classification (traçabilité)
ClassificationFeedbackCorrections humaines — statut : en attente / approuvé / refusé
TrainingRunHistorique des entraînements (précision, durée, nombre d'exemples)

Prérequis système

ComposantTaille
Image Docker (Python 3.12 + PyTorch CPU + transformers + SetFit)~2.5 GB
Modèle de base HuggingFace (paraphrase-multilingual-MiniLM-L12-v2)~500 MB
Modèle fine-tuné (setfit_model/)~500 MB
Image PostgreSQL 16~400 MB
Marge (logs, cache pip, rebuilds)~2 GB
Total recommandé~6-8 GB

RAM : ~1.5 GB en inférence, ~2-3 GB pendant l'entraînement.

PyTorch est installé en variante CPU uniquement (index https://download.pytorch.org/whl/cpu) pour éviter de tirer la version CUDA de 8 GB. Cette installation se fait dans une couche Docker séparée, avant la copie de requirements.txt, ce qui évite de re-télécharger PyTorch à chaque changement de dépendances.


Architecture des fichiers

src/
├── config/
│   ├── settings.py     # JWT, throttling, DB, Mantis
│   ├── urls.py         # Routes : homepage, admin, JWT, api/v1/
│   └── views.py        # Homepage — rendu de ce README en HTML
└── classifier/
    ├── engine/
    │   ├── categories.py   # Cache des catégories depuis la DB
    │   ├── rules.py        # Niveau 1 : regex + détection multi-match
    │   ├── embeddings.py   # Niveau 2 : SetFit (chargement, inférence, entraînement)
    │   └── classifier.py   # Orchestrateur du pipeline (règles → SetFit → fallback)
    ├── fixtures/
    │   ├── training_data.json   # Snapshot des exemples d'entraînement
    │   ├── regex_rules.json     # Snapshot des règles regex
    │   └── setfit_model/        # Modèle fine-tuné (généré par train_setfit, gitignored)
    ├── management/commands/
    │   ├── import_training_data.py  # JSON fixtures → DB
    │   ├── train_setfit.py          # Entraînement SetFit depuis la DB
    │   └── apply_feedback.py        # Feedbacks approuvés → exemples d'entraînement
    ├── models.py        # Category, CategoryRule, TrainingExample, logs, feedbacks
    ├── serializers.py
    ├── views.py         # Endpoints API (JWT-protected)
    ├── admin.py         # Interface d'administration Django
    └── services.py      # Intégration Mantis Bug Tracker

Tests

docker compose exec category_web_app python manage.py test classifier -v2

Un jeu de test complet de 86 items couvrant toutes les catégories et les cas limites est disponible dans postman/test_classify_body.json.