Bi-Encoder
Un bi-encoder est une architecture neuronale qui encode séparément deux textes (une requête et un document) en vecteurs indépendants (embeddings), puis compare ces vecteurs par une métrique de similarité comme la similarité cosinus. C’est l’architecture qui rend la recherche sémantique à grande échelle possible.
Le principe est d’une simplicité élégante. Chaque texte passe par un modèle Transformer (comme BERT) suivi d’une couche de pooling, qui produit un vecteur dense de dimensions fixes (typiquement 384, 768 ou 1024). Les documents sont encodés une seule fois et stockés dans une base de données vectorielle. Quand une requête arrive, seul son vecteur est calculé à la volée, puis comparé à tous les vecteurs pré-calculés. Le résultat : une recherche sémantique en millisecondes sur des millions de documents. C’est cette architecture qui propulse tous les systèmes de RAG, de recherche sémantique et de recommandation en production.
- Catégorie
- NLP / Information Retrieval
- Aussi appelé
- Dual encoder, twin encoder, sentence transformer
- Architecture
- Réseau siamois (deux Transformers à poids partagés) + couche de pooling
- Sortie
- Un vecteur (embedding) par texte
- Usage principal
- Retrieval à grande échelle (semantic search, RAG, recommandation)
- Bibliothèque de référence
- Sentence Transformers (SBERT), 15 000+ modèles pré-entraînés sur HuggingFace
Architecture du bi-encoder
Le réseau siamois (Siamese Network)
Le bi-encoder repose sur une architecture siamoise : deux réseaux Transformer identiques (partageant les mêmes poids) encodent chacun un texte différent. Le premier encode la requête Q en un vecteur u. Le second encode le document D en un vecteur v. Les deux réseaux sont strictement indépendants : à aucun moment les tokens de la requête ne « voient » les tokens du document.
SBERT (Sentence-BERT), publié par Nils Reimers et Iryna Gurevych en 2019, est le modèle fondateur de cette approche. Il a résolu le problème principal de BERT pour la similarité de phrases : BERT nécessitait de passer les deux phrases ensemble en entrée (comme un cross-encoder), ce qui rendait la comparaison de 10 000 phrases entre elles ingérable (50 millions de paires, soit ~65 heures de calcul). SBERT réduit ce temps à 5 secondes en encodant chaque phrase indépendamment, puis en comparant les vecteurs.
Couche de pooling
Un modèle BERT produit un vecteur de 768 dimensions pour chaque token de l’entrée. Pour obtenir un seul vecteur représentant tout le texte, il faut agréger ces vecteurs de tokens. C’est le rôle de la couche de pooling. Trois stratégies existent :
Mean pooling (la plus courante) : Calcule la moyenne de tous les vecteurs de tokens, pondérée par le masque d’attention. C’est la méthode recommandée par SBERT et la plus utilisée en production. Elle donne les meilleurs résultats sur la majorité des benchmarks.
Max pooling : Prend la valeur maximale de chaque dimension à travers tous les tokens. Moins utilisé, peut être utile pour capturer les signaux les plus forts.
[CLS] token : Utilise uniquement le vecteur de sortie du token spécial [CLS]. C’est la stratégie native de BERT, mais elle donne généralement des résultats inférieurs au mean pooling pour les embeddings de phrases.
Comparaison des vecteurs
Une fois les vecteurs u (requête) et v (document) calculés, leur similarité est évaluée par une métrique de distance :
Similarité cosinus : Mesure l’angle entre les deux vecteurs, indépendamment de leur norme. Produit un score entre -1 et 1 (1 = identique, 0 = orthogonal). C’est la métrique la plus utilisée pour les embeddings de texte.
Produit scalaire (dot product) : Plus rapide à calculer, mais sensible à la norme des vecteurs. Utilisé quand les embeddings sont normalisés (ce qui rend le résultat équivalent à la similarité cosinus).
Distance euclidienne : Mesure la distance géométrique entre les deux points dans l’espace vectoriel. Moins courante pour le texte, plus utilisée pour les images.
Comment un bi-encoder est entraîné
L’entraînement d’un bi-encoder vise à apprendre des vecteurs où les paires pertinentes (requête, document pertinent) sont proches et les paires non pertinentes sont éloignées dans l’espace vectoriel.
Apprentissage contrastif
La méthode dominante est l’apprentissage contrastif (contrastive learning). Pour chaque requête (anchor), on dispose d’un exemple positif (document pertinent) et de plusieurs exemples négatifs (documents non pertinents). Le modèle est entraîné à maximiser la similarité entre l’anchor et le positif tout en minimisant la similarité avec les négatifs.
La perte la plus courante est la Multiple Negatives Ranking Loss (MNRL) de Sentence Transformers. Elle exploite les « in-batch negatives » : dans un batch de B paires positives, chaque positif des autres paires sert de négatif. Avec un batch de 64 paires, chaque exemple est contrasté avec 63 négatifs sans coût supplémentaire. Plus le batch est grand, plus l’entraînement est efficace.
Hard negatives
Les négatifs les plus informatifs sont les « hard negatives » : des documents sémantiquement proches de la requête mais pas réellement pertinents. Par exemple, pour la requête « impact du recyclage sur la santé publique », un hard negative serait un article sur le recyclage sans mention de la santé. Ces exemples forcent le modèle à apprendre des distinctions fines. Les techniques comme DPR (Dense Passage Retrieval) ou RocketQA utilisent BM25 ou un bi-encoder pré-entraîné pour miner ces hard negatives à grande échelle.
Distillation depuis un cross-encoder
Une approche puissante consiste à utiliser un cross-encoder plus précis pour générer des labels de pertinence, puis entraîner le bi-encoder à reproduire ces scores. Le cross-encoder fournit des signaux de supervision plus fins que les labels binaires (pertinent/non pertinent). C’est le principe d’Augmented SBERT et des techniques de knowledge distillation qui permettent de fermer l’écart de précision entre bi-encoder et cross-encoder.
Les principaux modèles de bi-encoder
| Modèle | Dimensions | Fournisseur | Caractéristiques |
|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | Sentence Transformers | Léger, rapide, baseline de prototypage. Open source. |
| all-mpnet-base-v2 | 768 | Sentence Transformers | Plus précis que MiniLM, bonne qualité généraliste. Open source. |
| multilingual-e5-large | 1024 | Microsoft | Multilingue, performant sur le français. Open source. |
| text-embedding-3-large | 256 / 1024 / 3072 | OpenAI | État de l’art commercial, dimensions ajustables (Matryoshka). API. |
| text-embedding-3-small | 512 / 1536 | OpenAI | Bon rapport qualité-coût pour l’API OpenAI. |
| Cohere Embed v3 | 1024 | Cohere | Multilingue (100+ langues), optimisé pour la recherche. API. |
| voyage-3 | 1024 | Voyage AI | Spécialisé code et texte technique. API. |
| BGE-large-en-v1.5 | 1024 | BAAI | Performant sur MTEB, open source. Version multilingue : bge-m3. |
| jina-embeddings-v3 | Variable (Matryoshka) | Jina AI | Multilingue (89 langues), contexte 8192 tokens, dimensions ajustables. |
all-MiniLM-L6-v2 (gratuit, 5x plus rapide que les gros modèles). Pour du français en production : multilingual-e5-large ou Cohere Embed v3. Pour le meilleur score absolu : text-embedding-3-large (OpenAI) ou jina-embeddings-v3. Plus les dimensions sont élevées, meilleure est la capture sémantique, mais plus le stockage et le calcul sont coûteux. Le benchmark MTEB (Massive Text Embedding Benchmark) est la référence pour comparer objectivement les modèles.
Bi-encoder vs cross-encoder : le compromis fondamental
Le bi-encoder et le cross-encoder incarnent le compromis vitesse-précision le plus important en information retrieval.
| Critère | Bi-encoder | Cross-encoder |
|---|---|---|
| Encodage | Séparé (requête et document indépendants) | Conjoint (requête + document ensemble) |
| Sortie | Un vecteur par texte (réutilisable, stockable) | Un score par paire (non réutilisable) |
| Pré-calcul | Oui (les documents sont encodés une seule fois) | Non (chaque paire nécessite une inférence) |
| Scalabilité | Millions à milliards de documents | 20 à 100 candidats maximum |
| Latence (1M docs) | 5 à 50 ms (recherche ANN) | Impossible (heures de calcul) |
| Précision | Bonne (perte d’info due à la compression) | Excellente (+4 points nDCG@10) |
| Rôle | Retriever (étage 1) | Reranker (étage 2) |
En production, le pattern standard est : bi-encoder pour le retrieval initial (maximiser le recall sur tout le corpus), puis cross-encoder pour le reranking (maximiser la précision sur les top candidats). Les deux architectures ne sont pas en compétition mais forment un pipeline à deux étages complémentaire.
Implémentation en Python
Avec Sentence Transformers
from sentence_transformers import SentenceTransformer, util
# 1. Charger un modèle bi-encoder pré-entraîné
model = SentenceTransformer('all-MiniLM-L6-v2')
# 2. Encoder des phrases en embeddings
sentences = [
"Le recyclage des plastiques réduit la pollution.",
"L'énergie solaire est une alternative renouvelable.",
"La réduction des déchets améliore la santé publique.",
]
embeddings = model.encode(sentences)
print(embeddings.shape) # (3, 384) : 3 phrases, 384 dimensions
# 3. Calculer la similarité entre toutes les paires
similarities = model.similarity(embeddings, embeddings)
print(similarities)
# tensor([[1.0000, 0.1523, 0.5847],
# [0.1523, 1.0000, 0.1102],
# [0.5847, 0.1102, 1.0000]])
La phrase 1 (recyclage + pollution) et la phrase 3 (déchets + santé) ont une similarité plus élevée (0,58) que les autres paires, car elles partagent un champ sémantique lié à l’environnement et la santé.
Recherche sémantique complète
from sentence_transformers import SentenceTransformer, util
import torch
model = SentenceTransformer('all-MiniLM-L6-v2')
# Corpus de documents (encodé une seule fois)
corpus = [
"Guide pour protéger vos plantes du gel hivernal",
"Les meilleures variétés de tomates pour l'été",
"Comment installer un système d'arrosage automatique",
"Protéger le jardin contre le froid et le gel",
"Recette de confiture de fraises maison",
]
corpus_embeddings = model.encode(corpus, convert_to_tensor=True)
# Requête (encodée à chaque recherche)
query = "protéger mon jardin du gel"
query_embedding = model.encode(query, convert_to_tensor=True)
# Recherche des documents les plus similaires
hits = util.semantic_search(query_embedding, corpus_embeddings, top_k=3)[0]
for hit in hits:
print(f"Score: {hit['score']:.4f} | {corpus[hit['corpus_id']]}")
# Score: 0.7234 | Protéger le jardin contre le froid et le gel
# Score: 0.6891 | Guide pour protéger vos plantes du gel hivernal
# Score: 0.1456 | Comment installer un système d'arrosage automatique
Ce code illustre le cœur du bi-encoder : les documents sont encodés une seule fois (corpus_embeddings), puis chaque nouvelle requête ne nécessite que l’encodage d’un seul vecteur et une recherche par similarité. En production, remplacez util.semantic_search par une base vectorielle (Pinecone, Weaviate, Qdrant) pour des recherches en millisecondes sur des millions de documents.
Fine-tuning d’un bi-encoder
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
# Charger un modèle pré-entraîné comme point de départ
model = SentenceTransformer('all-MiniLM-L6-v2')
# Préparer les données d'entraînement (paires positives)
train_examples = [
InputExample(texts=["protéger plantes gel", "guide protection hivernale jardin"]),
InputExample(texts=["arrosage automatique", "système irrigation programmable"]),
# ... plus de paires
]
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# Multiple Negatives Ranking Loss (in-batch negatives)
train_loss = losses.MultipleNegativesRankingLoss(model)
# Entraîner
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=3,
warmup_steps=100,
output_path="./mon-modele-finetune"
)
Le fine-tuning d’un bi-encoder sur votre domaine spécifique améliore significativement la qualité du retrieval. Quelques centaines de paires pertinentes suffisent souvent pour un gain notable, surtout si votre domaine utilise un vocabulaire spécialisé.
Cas d’usage du bi-encoder
Recherche sémantique : Le cas d’usage principal. Retrouver des documents pertinents par le sens, pas seulement par les mots-clés. C’est le cœur de la recherche sémantique dans les moteurs de recherche, les systèmes documentaires et les FAQ intelligentes.
Retrieval dans les pipelines RAG : Le bi-encoder est le retriever de premier étage dans tous les systèmes de RAG. Il récupère rapidement les chunks pertinents d’une base documentaire, qui sont ensuite rerankés par un cross-encoder avant d’être envoyés au LLM.
Clustering de textes : Les embeddings d’un bi-encoder peuvent être utilisés pour regrouper des textes similaires. Les algorithmes comme K-means ou HDBSCAN opèrent directement dans l’espace vectoriel pour identifier des clusters thématiques dans un corpus.
Détection de doublons : Comparer les embeddings de tous les textes d’une base pour identifier les doublons sémantiques (même idée, mots différents). Utilisé dans les FAQ, les bases de tickets support, et les systèmes de déduplication.
Recommandation : Les embeddings de produits, d’articles ou de contenus permettent de calculer des similarités et de recommander des éléments proches de ce qu’un utilisateur a consulté ou aimé.
Limites du bi-encoder
Compression en un seul vecteur. Tout le sens d’un texte de 500 mots est compressé en un vecteur de 768 dimensions. Inévitablement, de l’information est perdue. Un document qui traite de multiples sujets sera représenté par une « moyenne » qui peut ne correspondre précisément à aucun des sujets individuels. C’est ce qui cause les faux positifs (documents vaguement liés classés haut) et les faux négatifs (documents très pertinents ratés).
Pas d’interaction requête-document. Le bi-encoder encode le document sans connaître la requête (les embeddings sont pré-calculés). Le vecteur du document est donc « générique », optimisé pour toutes les requêtes possibles. Le cross-encoder, en revanche, adapte son analyse du document à la requête spécifique, ce qui explique sa meilleure précision.
Généralisation hors domaine limitée. Les études montrent que les bi-encoders ne généralisent pas toujours bien sur des domaines non vus à l’entraînement (zero-shot). Sur certains benchmarks, un simple BM25 surpasse les bi-encoders en zero-shot. Le fine-tuning sur votre domaine est souvent nécessaire pour obtenir de bons résultats.
Dépendance au modèle d’embedding. Les vecteurs de deux modèles différents ne sont pas comparables. Si vous changez de modèle, tous les documents doivent être ré-encodés. C’est un coût non négligeable pour des corpus de millions de documents.
Évolutions récentes
Matryoshka Representation Learning (MRL) : Technique permettant de produire des embeddings dont les premières N dimensions sont aussi informatives qu’un embedding complet de N dimensions. Vous pouvez utiliser seulement les 256 premières dimensions d’un embedding de 3072 dimensions pour un stockage réduit, avec une perte de qualité minimale. OpenAI (text-embedding-3) et Jina (jina-embeddings-v3) implémentent cette approche.
Sparse Encoders (SPLADE) : Des modèles qui génèrent des vecteurs creux (sparse) plutôt que denses. Ils combinent la structure d’un index inversé (comme BM25) avec l’apprentissage neural. Sentence Transformers supporte maintenant les sparse encoders via la classe SparseEncoder. C’est un troisième signal complémentaire aux vecteurs denses et à BM25.
Quantification : Les embeddings peuvent être quantifiés (passage de float32 à int8 ou binaire) pour réduire l’espace mémoire de 4x à 32x avec une perte de qualité minime. Les bases vectorielles comme Qdrant et Milvus supportent nativement cette optimisation.
Verdict
Le bi-encoder est la brique fondamentale de tout système de recherche sémantique. Sans lui, pas de retrieval vectoriel, pas de RAG, pas de recommandation par similarité. Sa force est la scalabilité : encoder des millions de documents une seule fois, puis chercher en millisecondes. Sa faiblesse est la précision, limitée par la compression du sens en un seul vecteur.
En production, le bi-encoder ne doit jamais être utilisé seul. Le pipeline standard combine un bi-encoder (retrieval rapide) avec BM25 (hybrid search) et un cross-encoder (reranking). Pour le choix du modèle, commencez avec all-MiniLM-L6-v2 pour prototyper, puis migrez vers multilingual-e5-large, Cohere Embed v3 ou text-embedding-3-large pour la production. Et si votre domaine est spécialisé, le fine-tuning du bi-encoder est souvent le levier d’amélioration le plus rentable.
Questions fréquentes sur le bi-encoder
Quelle est la différence entre un bi-encoder et un cross-encoder ?
Le bi-encoder encode la requête et le document séparément en vecteurs indépendants, puis compare ces vecteurs par similarité cosinus. C’est rapide (les documents sont pré-encodés) et scalable (millions de documents), mais moins précis car aucune interaction directe n’existe entre requête et document. Le cross-encoder concatène requête et document, les encode ensemble dans un seul Transformer, et produit un score de pertinence avec une cross-attention complète. C’est plus précis (+4 points nDCG@10) mais beaucoup plus lent (chaque paire nécessite une inférence). En production, le bi-encoder sert de retriever (étage 1) et le cross-encoder de reranker (étage 2).
Qu’est-ce que SBERT et quel est son rapport avec le bi-encoder ?
SBERT (Sentence-BERT) est le modèle fondateur de l’architecture bi-encoder pour les embeddings de phrases. Publié en 2019 par Nils Reimers et Iryna Gurevych, il adapte BERT en réseau siamois : deux BERT identiques encodent séparément deux phrases, une couche de pooling produit un vecteur par phrase, et ces vecteurs sont comparés par similarité. La bibliothèque Python sentence-transformers (maintenue par HuggingFace) est le framework standard pour utiliser et entraîner des bi-encoders. Plus de 15 000 modèles pré-entraînés sont disponibles sur HuggingFace.
Comment choisir le nombre de dimensions d’un embedding ?
Plus les dimensions sont élevées, meilleure est la capture sémantique, mais plus le stockage et le calcul sont coûteux. Pour un prototype : 384 dimensions (MiniLM) suffisent. Pour la production : 768 à 1024 dimensions offrent le meilleur compromis. Pour la précision maximale : 3072 dimensions (text-embedding-3-large d’OpenAI). Les modèles Matryoshka (OpenAI, Jina) permettent d’utiliser un sous-ensemble des dimensions (ex. 256 sur 3072) pour réduire les coûts avec une perte minime. Le choix dépend de votre volume de données, de votre budget de stockage et de vos contraintes de latence.
Faut-il fine-tuner un bi-encoder sur son domaine ?
Oui, si votre domaine utilise un vocabulaire spécialisé (médical, juridique, technique). Les modèles pré-entraînés généralistes fonctionnent bien sur du texte courant mais dégradent sur des termes spécifiques non vus à l’entraînement. Le fine-tuning avec quelques centaines à quelques milliers de paires (requête, document pertinent) améliore significativement le recall. La technique la plus simple : Multiple Negatives Ranking Loss avec des paires positives de votre domaine. Pour des résultats encore meilleurs, minez des hard negatives avec BM25 et distillez les scores d’un cross-encoder.
Le bi-encoder suffit-il pour construire un système RAG ?
Un bi-encoder seul donne des résultats corrects mais pas optimaux. Le pipeline RAG standard en production combine : un bi-encoder + BM25 en hybrid search (pour maximiser le recall), une fusion RRF des résultats, un cross-encoder pour le reranking (pour maximiser la précision), et enfin l’injection des top chunks dans le prompt du LLM. Chaque composant apporte une amélioration mesurable. Le bi-encoder est la fondation, mais les étages supplémentaires font la différence entre un RAG « acceptable » et un RAG de qualité production.