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
| Approche | Exemples requis | Précision | Latence |
|---|---|---|---|
| Zero-shot | 0 | ~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