Distance et similarité cosinus
Mis à jour le 29 juillet 2026
Objectifs
- Comprendre la similarité cosinus et pourquoi elle est préférée
- Implémenter la comparaison de vecteurs en Python
- Interpréter les scores de similarité
Mesurer la proximité entre vecteurs
Une fois vos textes convertis en vecteurs, tout repose sur une question : comment décide-t-on que deux vecteurs sont « proches » ? Trois réponses coexistent, et le choix n’est pas neutre.
La distance euclidienne est la distance géométrique classique, celle qu’on mesure à la règle entre deux points. Elle est intuitive, mais elle dépend de la magnitude des vecteurs : deux textes de même sens dont l’un serait encodé avec une amplitude plus forte apparaîtraient artificiellement éloignés.
import numpy as np
def distance_euclidienne(a: list[float], b: list[float]) -> float:
return np.linalg.norm(np.array(a) - np.array(b))
Le produit scalaire additionne les produits élément par élément. C’est l’opération la plus rapide de la famille, celle que le processeur exécute le plus efficacement sur de grandes matrices.
def produit_scalaire(a: list[float], b: list[float]) -> float:
return np.dot(a, b)
La similarité cosinus, enfin, mesure l’angle entre deux vecteurs indépendamment de leur longueur, ce qui en fait la métrique standard pour les embeddings : seule la direction du sens compte, pas son intensité.
def similarite_cosinus(a: list[float], b: list[float]) -> float:
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
Elle varie de -1, pour deux directions opposées, à +1 pour deux directions identiques. Un détail pratique a son importance : les embeddings OpenAI sont déjà normalisés, c’est-à-dire de longueur 1. Le dénominateur vaut donc 1, et le produit scalaire donne exactement le même résultat que la similarité cosinus — pour un coût de calcul moindre. C’est ce raccourci que nous utiliserons partout dans ce cours.
Comparaison pratique
Rien ne vaut une mesure sur des phrases réelles. Comparons une phrase, sa paraphrase intégrale et une phrase hors sujet :
from openai import OpenAI
client = OpenAI()
def obtenir_embedding(texte: str) -> list[float]:
response = client.embeddings.create(
input=texte,
model="text-embedding-3-large"
)
return response.data[0].embedding
# Phrases à comparer
phrases = {
"a": "Le chat dort sur le canapé",
"b": "Le félin sommeille sur le sofa",
"c": "Python est un langage de programmation",
}
embeddings = {k: obtenir_embedding(v) for k, v in phrases.items()}
# Comparer toutes les paires
from itertools import combinations
for (k1, v1), (k2, v2) in combinations(phrases.items(), 2):
sim = similarite_cosinus(embeddings[k1], embeddings[k2])
print(f"sim({k1}, {k2}) = {sim:.4f}")
print(f" {v1}")
print(f" {v2}")
print()
Résultat typique :
sim(a, b) = 0.8923 ← sens très proche
sim(a, c) = 0.1247 ← sujets différents
sim(b, c) = 0.1189 ← sujets différents
Les phrases a et b n’ont aucun mot en commun — chat/félin, dort/sommeille, canapé/sofa — et pourtant elles obtiennent 0,89. C’est exactement ce qu’aucune méthode lexicale ne sait faire.
Interpréter les scores
Les seuils dépendent de votre cas d’usage, mais le tableau ci-dessous donne des repères pour lire un score sans se tromper d’un ordre de grandeur.
| Score | Interprétation |
|---|---|
| > 0.85 | Très similaire (paraphrases, synonymes) |
| 0.70 – 0.85 | Thématiquement liés |
| 0.40 – 0.70 | Vaguement liés |
| < 0.40 | Sujets différents |
Ces valeurs varient selon le modèle et selon le domaine : un corpus juridique, où tous les textes partagent le même vocabulaire formel, produit des scores globalement plus élevés qu’un corpus généraliste, au point qu’un seuil de 0,70 y devient trop permissif. Calibrez toujours sur vos propres données, en observant une trentaine de paires dont vous connaissez la réponse attendue.
Recherche du plus proche voisin
Chercher un document revient à calculer la similarité entre le vecteur de la requête et tous ceux du corpus, puis à garder les k meilleurs. L’astuce est de ne pas boucler en Python : une multiplication matricielle NumPy calcule toutes les similarités d’un coup, en une fraction du temps.
def rechercher_similaires(
query_embedding: list[float],
corpus_embeddings: np.ndarray,
k: int = 5
) -> list[tuple[int, float]]:
"""Retourne les k documents les plus similaires."""
query = np.array(query_embedding)
# Calcul vectorisé de toutes les similarités
similarites = corpus_embeddings @ query
# (fonctionne car les embeddings OpenAI sont normalisés)
# Indices des k meilleurs scores
top_k_indices = np.argsort(similarites)[-k:][::-1]
return [(int(i), float(similarites[i])) for i in top_k_indices]
# Utilisation
corpus = np.load("embeddings.npy") # matrice (n, 3072)
query_emb = obtenir_embedding("Comment installer Python ?")
resultats = rechercher_similaires(query_emb, corpus, k=5)
for idx, score in resultats:
print(f"Document {idx}: score = {score:.4f}")
Le [-k:][::-1] mérite une seconde de lecture : argsort trie par ordre croissant, on prend donc la fin du tableau pour obtenir les meilleurs scores, puis on inverse pour les classer du plus pertinent au moins pertinent.
Avec scikit-learn
Quand vous ne cherchez pas un document mais que vous voulez comparer tout le corpus avec lui-même — pour détecter des doublons, par exemple, ou préparer un clustering — scikit-learn fournit une fonction optimisée qui renvoie la matrice complète.
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# Matrice de similarité entre tous les documents
corpus = np.array(embeddings_list)
matrice_sim = cosine_similarity(corpus)
print(f"Shape : {matrice_sim.shape}") # (n, n)
print(f"Similarité doc 0 ↔ doc 1 : {matrice_sim[0][1]:.4f}")
Gardez en tête que cette matrice est de taille n × n : dix mille documents produisent cent millions de valeurs, soit environ 400 Mo en float32. Au-delà de quelques milliers d’éléments, procédez par blocs plutôt que d’un seul appel.
Résumé
- La similarité cosinus mesure l’angle entre vecteurs, indépendamment de leur magnitude
- Les embeddings OpenAI sont normalisés : dot product = cosinus
- Un score > 0.85 indique des textes très similaires
- La recherche de voisins proches se fait efficacement avec NumPy ou scikit-learn