Aller au contenu principal

Classification de texte par embeddings

Objectifs

  • Classifier des textes sans entraîner de modèle
  • Implémenter la classification zero-shot et few-shot par embeddings
  • Comparer avec un classifieur supervisé léger

Classification zero-shot

L’idée est simple : comparez l’embedding du texte à classer avec les embeddings des labels. Le label le plus similaire est la prédiction.

from openai import OpenAI
import numpy as np

client = OpenAI()

def classifier_zero_shot(
    texte: str,
    categories: list[str],
    model: str = "text-embedding-3-large"
) -> dict:
    """Classifie un texte parmi des catégories sans entraînement."""
    # Embeddings des catégories
    all_inputs = [texte] + categories
    response = client.embeddings.create(
        input=all_inputs,
        model=model
    )
    embs = [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
    texte_emb = np.array(embs[0])
    cat_embs = np.array(embs[1:])

    # Similarité avec chaque catégorie
    scores = cat_embs @ texte_emb

    # Softmax pour avoir des probabilités
    exp_scores = np.exp(scores - np.max(scores))
    probas = exp_scores / exp_scores.sum()

    resultats = sorted(
        zip(categories, probas),
        key=lambda x: x[1], reverse=True
    )

    return {
        "prediction": resultats[0][0],
        "confiance": float(resultats[0][1]),
        "scores": {cat: float(p) for cat, p in resultats}
    }

# Utilisation
texte = "Le cours de l'action a chuté de 15% après l'annonce"
categories = ["Finance", "Sport", "Technologie", "Politique", "Santé"]

resultat = classifier_zero_shot(texte, categories)
print(f"Prédiction : {resultat['prediction']} "
      f"({resultat['confiance']:.1%})")
print("Scores :", resultat["scores"])

Classification few-shot

Utilisez des exemples labellisés pour améliorer la précision. Chaque catégorie est représentée par la moyenne de ses exemples :

def classifier_few_shot(
    texte: str,
    exemples: dict[str, list[str]],
    model: str = "text-embedding-3-large"
) -> dict:
    """Classifie par similarité avec des exemples labellisés.

    Args:
        exemples: {"catégorie": ["exemple1", "exemple2", ...]}
    """
    # Préparer tous les textes
    tous_textes = [texte]
    mapping = []
    for cat, txts in exemples.items():
        for t in txts:
            tous_textes.append(t)
            mapping.append(cat)

    # Embeddings en un seul appel
    response = client.embeddings.create(
        input=tous_textes, model=model
    )
    embs = [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
    texte_emb = np.array(embs[0])

    # Moyenne des embeddings par catégorie
    cat_embeddings = {}
    for i, cat in enumerate(mapping):
        emb = np.array(embs[i + 1])
        if cat not in cat_embeddings:
            cat_embeddings[cat] = []
        cat_embeddings[cat].append(emb)

    cat_moyennes = {
        cat: np.mean(embs_list, axis=0)
        for cat, embs_list in cat_embeddings.items()
    }

    # Similarité
    scores = {
        cat: float(np.dot(texte_emb, emb_moy) /
                    (np.linalg.norm(texte_emb) * np.linalg.norm(emb_moy)))
        for cat, emb_moy in cat_moyennes.items()
    }

    prediction = max(scores, key=scores.get)
    return {"prediction": prediction, "scores": scores}

# Utilisation
exemples = {
    "bug": [
        "L'application crash au démarrage",
        "Erreur 500 sur la page de connexion",
        "Le bouton ne fonctionne pas sur mobile",
    ],
    "feature": [
        "Pourriez-vous ajouter un mode sombre ?",
        "Il serait utile d'exporter en PDF",
        "Suggestion : intégrer un calendrier",
    ],
    "question": [
        "Comment changer mon mot de passe ?",
        "Où trouver les paramètres de notification ?",
        "Quelle est la limite de stockage ?",
    ],
}

texte = "L'export CSV ne contient pas toutes les colonnes"
resultat = classifier_few_shot(texte, exemples)
print(f"Catégorie : {resultat['prediction']}")

Classifieur supervisé léger

Pour de meilleures performances, entraînez un classifieur scikit-learn sur les embeddings :

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
import numpy as np

def entrainer_classifieur(
    textes: list[str],
    labels: list[str],
    model: str = "text-embedding-3-large"
) -> LogisticRegression:
    """Entraîne un classifieur sur des embeddings."""
    # Générer les embeddings
    response = client.embeddings.create(
        input=textes, model=model
    )
    X = np.array([d.embedding for d in sorted(response.data, key=lambda x: x.index)])

    # Entraîner un classifieur
    clf = LogisticRegression(max_iter=1000)
    
    # Validation croisée
    scores = cross_val_score(clf, X, labels, cv=5)
    print(f"Accuracy (5-fold CV) : {scores.mean():.3f} ± {scores.std():.3f}")

    # Entraîner sur tout le dataset
    clf.fit(X, labels)
    return clf

def predire(clf, texte: str, model: str = "text-embedding-3-large") -> str:
    """Prédit la catégorie d'un texte."""
    emb = client.embeddings.create(
        input=texte, model=model
    ).data[0].embedding
    return clf.predict([emb])[0]

Comparaison des approches

ApprocheExemples requisPrécisionLatence
Zero-shot0~70-80 %1 appel API
Few-shot (3-5/classe)15-25~80-90 %1 appel API
Supervisé (50+/classe)250+~90-95 %1 appel API + inference

Résumé

  • La classification zero-shot compare le texte aux labels via embeddings
  • Le few-shot utilise des exemples pour définir chaque catégorie
  • Un classifieur supervisé (LogisticRegression) atteint les meilleures performances
  • Commencez par le zero-shot, ajoutez des exemples si la précision est insuffisante