Les embeddings : transformer le sens en nombres
Transformer des phrases en vecteurs en local avec un modèle d'embedding chinois open source, comparer leur sens par similarité cosinus, et voir ce qu'il fait bien et ce qu'il ne sait pas distinguer. C'est la base de la recherche sémantique et du RAG qui viendront ensuite.
- Environ 35 minutes
- Niveau : Débutant
- Testé : 2026-09-14 bge-small-zh-v1.5, sentence-transformers
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
Imaginons que vous construisiez l'assistant de questions-réponses pour httpx. L'utilisateur demande « httpx 怎么设置请求超时? » (comment définir le délai d'expiration d'une requête avec httpx ?), alors que la documentation parle de « timeout configuration » ; un autre demande « 怎么让请求等久一点再报错 » (comment faire attendre la requête plus longtemps avant l'erreur), et la documentation ne contient pas une seule fois le mot « 超时 » (délai d'expiration). Avec une recherche par mots-clés, aucune de ces formulations ne trouve le bon paragraphe.
Il vous faut une manière de comparer le « sens » plutôt que la « lettre ». C'est à cela que servent les embeddings (vecteurs de représentation).
Qu'est-ce qu'un embedding
Un modèle d'embedding est un autre type de modèle. Il ne génère pas de texte et ne fait qu'une chose : il reçoit un texte et produit une suite de nombres de longueur fixe, c'est-à-dire un vecteur. Il est entraîné pour que des textes de sens proche donnent des vecteurs proches.
La « proximité » se mesure par un nombre, le plus souvent la similarité cosinus : plus deux vecteurs pointent dans la même direction, plus la valeur est proche de 1 ; sans rapport entre eux, elle est proche de 0. Un exemple en deux dimensions pour le sentir :
import numpy as np
def cosine(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
print(cosine(np.array([1, 2]), np.array([2, 4]))) # 方向完全相同,长度不同
print(cosine(np.array([1, 2]), np.array([2, -1]))) # 互相垂直
0.9999999999999998
0.0
[1, 2] et [2, 4] n'ont pas la même longueur mais exactement la même direction, donc leur similarité vaut 1 (le 0.9999999999999998 affiché est une infime erreur de calcul en virgule flottante ; on peut le lire comme 1). La similarité cosinus ne regarde que la direction, pas la longueur, et c'est exactement ce que nous voulons : qu'une phrase soit un peu plus longue ou plus courte ne doit pas changer son sens.
Les vrais vecteurs d'embedding ont des centaines ou des milliers de dimensions, impossibles à dessiner, mais le calcul est exactement le même.
DeepSeek n'a pas d'interface d'embedding
En septembre 2026, l'API de DeepSeek ne propose que des modèles de conversation, pas de modèle d'embedding. En appelant son interface d'embedding avec le SDK d'OpenAI, j'obtiens directement une erreur 404.
Deux solutions :
- Utiliser l'API d'embedding d'un autre fournisseur. Alibaba Cloud Bailian, Zhipu, OpenAI et d'autres proposent une interface d'embedding, qui s'appelle comme l'interface de conversation via le SDK d'OpenAI (
client.embeddings.create(...)) ; les noms de modèles sont dans leur documentation. - Faire tourner un modèle d'embedding open source en local. Les modèles d'embedding sont bien plus petits que les modèles de conversation ; le CPU d'un ordinateur ordinaire suffit.
Ce cours choisit la seconde solution, avec bge-small-zh-v1.5, publié en open source par l'Académie d'intelligence artificielle de Pékin (BAAI) : entraîné spécifiquement pour le chinois, seulement 24 millions de paramètres, 92 Mo à télécharger, entièrement gratuit, utilisable sans réseau, et sans risque de fuite de vos documents vers un tiers.
En pratique : comparer le sens de quelques phrases
Installez un paquet :
uv add sentence-transformers
sentence-transformers est une bibliothèque dédiée à l'exécution de modèles d'embedding. Au premier lancement, elle télécharge le modèle depuis Hugging Face ; si le téléchargement est lent (depuis la Chine notamment), définissez d'abord un miroir :
export HF_ENDPOINT=https://hf-mirror.com
Puis lancez ce script :
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
query = "httpx 怎么设置请求超时?"
sentences = [
"How do I set a timeout for requests in httpx?",
"httpx 的 timeout 参数默认是 5 秒。",
"给 httpx 客户端配置代理服务器",
"requests 库如何设置超时时间",
"今天中午吃了一碗牛肉面",
]
# normalize_embeddings=True 把每个向量的长度缩放成 1,这样点积就等于余弦相似度
vectors = model.encode([query] + sentences, normalize_embeddings=True)
print(f"每句话变成了一个 {vectors.shape[1]} 维的向量")
print(f"第一句的前 5 个数:{np.round(vectors[0][:5], 4)}")
print()
q, rest = vectors[0], vectors[1:]
scores = rest @ q
print(f"和「{query}」的相似度:")
for score, text in sorted(zip(scores, sentences), reverse=True):
print(f" {score:.3f} {text}")
Résultat (ce code n'appelle aucune API, vous devriez obtenir les mêmes nombres) :
每句话变成了一个 512 维的向量
第一句的前 5 个数:[-0.0232 0.0017 0.0812 0.0058 0.0116]
和「httpx 怎么设置请求超时?」的相似度:
0.777 requests 库如何设置超时时间
0.673 httpx 的 timeout 参数默认是 5 秒。
0.669 How do I set a timeout for requests in httpx?
0.659 给 httpx 客户端配置代理服务器
0.202 今天中午吃了一碗牛肉面
Chaque phrase devient 512 nombres. Pris isolément, ces nombres n'ont pas de sens ; ce qui compte, c'est la similarité entre les vecteurs.
Ce que montre le résultat
Les phrases de sens proche ont un score élevé, les autres un score bas. Les quatre phrases liées aux requêtes HTTP dépassent toutes 0,65, tandis que « 今天中午吃了一碗牛肉面 » (à midi, j'ai mangé un bol de nouilles au bœuf) n'a que 0,2. La séparation est nette.
La correspondance fonctionne même entre langues. L'anglais « How do I set a timeout for requests in httpx? » n'a aucun caractère en commun avec la question chinoise, et obtient 0,669. C'est là que les embeddings surpassent la recherche par mots-clés.
Mais il ne distingue pas de quelle bibliothèque il s'agit. Le meilleur score revient à « requests 库如何设置超时时间 » (comment définir le délai d'expiration avec la bibliothèque requests), plus haut que les deux phrases sur httpx. Pour le modèle d'embedding, la structure « comment définir le délai d'expiration avec la bibliothèque XX » et le sens général comptent le plus ; que ce soit requests ou httpx n'est qu'une différence secondaire. Or, pour un assistant de questions-réponses, c'est justement la différence décisive : répondre à une question sur httpx avec la documentation de requests, c'est donner une mauvaise réponse.
De même, « 给 httpx 客户端配置代理服务器 » (configurer un serveur proxy pour le client httpx) parle de proxy, pas de délai, et obtient pourtant 0,659, à peine moins que les phrases sur le délai.
Cela montre que les embeddings saisissent bien « l'idée générale », mais sont peu sensibles aux choses exactes comme les noms propres, les noms de fonctions ou les numéros de version. La leçon 4 du module 04 combine embeddings et recherche par mots-clés pour résoudre précisément ce problème.
Les scores n'ont pas de sens absolu. 0,67, est-ce haut ou bas ? Il n'y a pas de réponse type. Chaque modèle d'embedding a sa propre plage de scores, et un même modèle varie selon le domaine. Ce qui est utile, c'est la comparaison : pour une même question, quels passages arrivent en tête. N'écrivez pas en dur dans votre code une règle du type « pertinent seulement si la similarité dépasse 0,7 », sauf si vous avez validé ce seuil sur vos propres données.
À quoi servent les embeddings
- Recherche sémantique : calculer à l'avance et stocker les vecteurs de tous les passages de documentation ; quand l'utilisateur pose une question, calculer le vecteur de la question et trouver les passages les plus proches. C'est le cœur du RAG ; la leçon 3 du module 04 en écrit un de zéro.
- Dédoublonnage : deux textes aux vecteurs presque identiques sont très probablement du contenu dupliqué.
- Classification et regroupement : regrouper automatiquement retours d'utilisateurs ou issues selon leur sens.
- Recommandation : trouver d'autres contenus proches par le sens de ce que l'utilisateur a déjà consulté.
Quelques pièges à l'usage
La question et les documents doivent utiliser le même modèle. Les vecteurs de modèles d'embedding différents sont totalement incompatibles, comme deux systèmes de coordonnées différents. Changer de modèle d'embedding oblige à recalculer tous les documents.
Il y a une limite de longueur. bge-small-zh-v1.5 ne lit au plus que 512 tokens ; au-delà, le texte est simplement jeté, sans erreur. Les longs documents doivent donc d'abord être découpés en petits passages dont on calcule les vecteurs séparément ; c'est le sujet de la leçon 2 du module 04.
Certains modèles demandent un préfixe pour la question. Certains modèles d'embedding recommandent d'ajouter une phrase fixe avant la requête, par exemple « 为这个句子生成表示以用于检索相关文章: » (générer une représentation de cette phrase pour rechercher des articles pertinents :), ce qui améliore la recherche. Faut-il en ajouter une, et laquelle ? Voyez la documentation du modèle. Par simplicité, cette leçon n'en ajoute pas.
Exercices
- Ajoutez à
sentencesquelques phrases de votre cru : une formulée autrement mais de sens identique (« httpx 请求等太久怎么办 », que faire si une requête httpx attend trop longtemps), une avec les mêmes mots-clés mais un autre sens (« 超时费用怎么计算 », comment calculer les frais de dépassement). Où se classent-elles ? - Remplacez la requête par l'anglais « How to configure a proxy in httpx? » et voyez comment le classement change.
- Si vous avez une clé Bailian, Zhipu ou OpenAI, appelez leur interface d'embedding avec
client.embeddings.create(model=..., input=[...]), calculez la similarité des mêmes phrases et comparez avec le modèle local. La plage des scores peut être complètement différente ; comparez les classements.
Auto-test
1. Pourquoi la similarité cosinus ne regarde-t-elle que la direction des vecteurs, et pas leur longueur ?
Ce qui nous intéresse, c'est de savoir si deux textes ont un sens proche, pas leur longueur ou d'autres grandeurs sans rapport. La similarité cosinus divise par la longueur des deux vecteurs et ne garde que l'information de direction. Si l'on normalise les vecteurs à une longueur de 1 à l'avance, le produit scalaire est directement égal à la similarité cosinus, ce qui accélère le calcul.
2. L'utilisateur pose une question sur httpx, et la recherche par embedding classe en premier la documentation de la bibliothèque requests. Pourquoi ? Que faire ?
Le modèle d'embedding saisit le sens global : « comment définir le délai avec telle bibliothèque » est très proche, et le nom de la bibliothèque n'est qu'une différence secondaire, d'où deux scores élevés. La solution consiste à combiner recherche par embedding et recherche par mots-clés (très sensible à un mot exact comme « httpx »), ou à limiter la recherche à la documentation de httpx.
3. Que se passe-t-il si vous donnez directement un article de 5000 caractères à bge-small-zh-v1.5 pour calculer son vecteur ?
Il ne lit que les 512 premiers tokens ; la suite est tronquée et jetée, sans erreur. Le vecteur obtenu ne représente que le sens du début de l'article. Les longs documents doivent donc d'abord être découpés en petits passages, dont on calcule les vecteurs séparément.
Questions et discussion
Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.
Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.
Chargement de la discussion…