Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,34 @@
"print(\"Modèles locaux : bi-encoder multilingue, FLAN-T5-small, cross-encoder mMARCO\")\n"
]
},
{
"cell_type": "markdown",
"id": "91fd3f97",
"metadata": {},
"source": [
"### Lecture : l'environnement déclaré — trois modèles locaux, zéro appel réseau\n",
"\n",
"La sortie `PyTorch : 2.11.0+cpu | device : CPU` puis la liste des trois\n",
"modèles locaux méritent d'être lues comme un choix de protocole, pas comme un\n",
"journal d'installation. Trois décisions s'y trouvent. D'abord **le CPU est\n",
"explicite** (`device=\"cpu\"` sur les trois modèles) : le notebook ne dépend\n",
"d'aucun GPU, donc il s'exécute partout avec le même résultat — le coût se\n",
"paie en temps de calcul, jamais en matériel. Ensuite **tout est local et rien\n",
"ne sort de la machine** : bi-encoder multilingue, `google/flan-t5-small`,\n",
"cross-encoder mMARCO sont chargés depuis le cache local, sans clé d'API ni\n",
"quota — contraste direct avec un notebook d'API distante où chaque score\n",
"dépend d'un service tiers. Le prix de cette clôture : les scores sont\n",
"reproductibles et comparables entre configurations, ce qui est précisément\n",
"la condition d'un benchmark contrôlé comme celui de la section 5. Enfin, ces\n",
"modèles sont **petits par construction** — `flan-t5-small` compte ~77 M de\n",
"paramètres — et cette petitesse est une hypothèse à tester, pas une garantie\n",
"de qualité : la lecture du document hypothétique (section 3) montrera ce\n",
"qu'un générateur non affiné produit réellement. Le `Seed : 42` et le\n",
"plafonnement `threads PyTorch : 4` complètent la démarche : on privilégie la\n",
"prévisibilité à la vitesse, car un benchmark n'a de valeur que si deux runs\n",
"donnent le même tableau."
]
},
{
"cell_type": "markdown",
"id": "fa3463d5",
Expand Down Expand Up @@ -231,6 +259,32 @@
"print(\"Exemple :\", documents[0])\n"
]
},
{
"cell_type": "markdown",
"id": "2f227050",
"metadata": {},
"source": [
"### Lecture : le corpus est un dispositif de mesure, pas un jeu de données\n",
"\n",
"`Corpus : 60 documents | thèmes : 20` : le corpus est **synthétique et\n",
"volontairement régulier** — trois variantes par thème (`définition`,\n",
"`pratique`, `décision`), toutes construites sur la même matrice de phrases.\n",
"Ce n'est pas de la paresse : c'est ce qui rend le benchmark INTERPRÉTABLE.\n",
"Sur un corpus réel, on ne sait jamais si une chute de nDCG vient du\n",
"retrieval ou d'une vérité terrain bruitée ; ici, la perturbation est la seule\n",
"variable. Le second nombre est plus subtil : `Gold QA : 24 questions (20\n",
"in-corpus, 4 hors corpus)`. Les 4 questions hors corpus ne sont **pas un\n",
"oubli ni une erreur d'annotation** — c'est le groupe de CONTRÔLE du\n",
"notebook, celui qui mesure le comportement quand la réponse n'existe pas.\n",
"La section 5 les interroge séparément, et l'exercice 3 en fait le cœur du\n",
"sujet (règle d'abstention). Noter enfin ce que la vérité terrain contient :\n",
"des **identifiants de documents** (`{'id': 'python-0', ...}`), c'est-à-dire\n",
"une pertinence BINAIRE et non graduée. Conséquence directe sur la métrique\n",
"choisie plus bas : nDCG dégénère ici en une mesure de position du bon\n",
"document, sans degrés de pertinence à pondérer. Un tel corpus valide les\n",
"deltas, pas la performance absolue."
]
},
{
"cell_type": "markdown",
"id": "ef63017d",
Expand Down Expand Up @@ -319,6 +373,31 @@
"print(f\"Exemple dégradé — Recall@3={recall_at_k(['x', 'a', 'b'], relevant, 3):.2f}, nDCG@3={ndcg_at_k(['x', 'a', 'b'], relevant, 3):.3f}\")\n"
]
},
{
"cell_type": "markdown",
"id": "5cd7646e",
"metadata": {},
"source": [
"### Lecture : deux métriques, deux questions — et les nombres se recomptent\n",
"\n",
"La sortie donne `Tests métriques : PASS` puis l'exemple dégradé\n",
"`Recall@3=1.00, nDCG@3=0.693`. Le ranking `['x', 'a', 'b']` contre les\n",
"documents pertinents `{a, b}` est précisément le cas qui sépare les deux\n",
"métriques : le document non pertinent est **en tête**, mais les deux\n",
"pertinents restent dans la fenêtre de 3. Résultat, `Recall@3 = 2/2 = 1.00`\n",
"— parfait — tandis que nDCG chute. Ce n'est pas une incohérence : les deux\n",
"mesurent des choses différentes. Recall répond à « le bon document est-il\n",
"DANS la fenêtre ? », nDCG à « À QUELLE HAUTEUR ? ». Le 0.693 se recompte à\n",
"la main. Le DCG idéal place les deux pertinents aux rangs 1 et 2 :\n",
"`1/log2(2) + 1/log2(3) = 1 + 0,6309 = 1,6309`. Le DCG réel, avec `x` en tête,\n",
"les place aux rangs 2 et 3 : `0 + 1/log2(3) + 1/log2(4) = 0,6309 + 0,5 =\n",
"1,1309`. Le rapport vaut `1,1309 / 1,6309 = 0,693` — exactement la valeur\n",
"affichée. La décote logarithmique est la clé : occuper le rang 1 rapporte\n",
"`1,0`, le rang 3 seulement `0,5`. C'est ce qui rend nDCG sensible au\n",
"reranking de la section 4 : déplacer un document pertinent du rang 4 au rang\n",
"3 améliore le score sans changer le Recall d'un iota."
]
},
{
"cell_type": "markdown",
"id": "44a5a24f",
Expand Down Expand Up @@ -463,6 +542,34 @@
" print(f\" {doc_id:<18} score={score:.3f}\")\n"
]
},
{
"cell_type": "markdown",
"id": "5e546075",
"metadata": {},
"source": [
"### Lecture : le bi-encoder factorise — et laisse un intrus au rang 3\n",
"\n",
"`Matrice corpus : (60, 384)` dit l'essentiel de l'architecture : les 60\n",
"documents sont encodés **une fois pour toutes** en 384 dimensions (la taille\n",
"de `MiniLM-L12`), et une requête se réduit à un vecteur unique, comparé par\n",
"un simple produit matriciel `(60, 384) · (384,)` → 60 scores. C'est la\n",
"factorisation qui fait la vitesse du bi-encoder : aucun des 60 documents\n",
"n'est relu au moment de la question. Mais cette économie a un prix, et il est\n",
"visible dans la sortie. Le top-5 affiche `rag-0 score=0.517`, `rag-2\n",
"score=0.467`, puis **`overfit-1 score=0.403`** — un document d'un AUTRE thème\n",
"s'insère au rang 3, devant `rag-1` (`0.303`). Le diagnostic se lit en\n",
"relisant les textes du corpus : le thème *surapprentissage* est défini par\n",
"« l'écart entre performance d'entraînement et de validation », vocabulaire\n",
"qui recoupe (`validation`, `généralisation`, `jeu de données`) celui du\n",
"document RAG. La proximité est **sémantique de surface** — elle porte sur des\n",
"mots partagés, pas sur le sujet — et le bi-encoder, qui encode question et\n",
"document SÉPARÉMENT, n'a aucun moyen de la distinguer d'une vraie\n",
"correspondance : c'est exactement la limite que le cross-encoder de la\n",
"section 4 est conçu pour lever. Noter aussi l'amplitude : de 0,517 à 0,299,\n",
"la queue est serrée, donc la marge entre rangs voisins est faible — de la\n",
"matière pour un réordonnanceur, peu de certitude pour un seuil."
]
},
{
"cell_type": "markdown",
"id": "4f500177",
Expand Down Expand Up @@ -548,6 +655,42 @@
"print(\"Top-5 HyDE :\", [doc_id for doc_id, _ in hyde_example])\n"
]
},
{
"cell_type": "markdown",
"id": "18fe0a26",
"metadata": {},
"source": [
"### Lecture : le document hypothétique est un texte DÉGÉNÉRÉ — et c'est l'expérience\n",
"\n",
"La sortie doit être lue sans complaisance :\n",
"\n",
"> `In French, a current technique of two phrases that responds to this\n",
"> question is not a reference to this question. N'invente pas de référence :\n",
"> Comment ancrear a generation in documents récupérés ?`\n",
"\n",
"Les trois symptômes de dégénérescence sont simultanés. (1) Le modèle\n",
"**recopie la consigne au lieu d'y répondre** : « N'invente pas de référence »\n",
"appartient au prompt, pas à une réponse — le modèle paraphrase l'instruction\n",
"qu'il a reçue. (2) Il **mélange deux langues** (« In French… » en anglais,\n",
"puis la suite en français) alors que la question est entièrement française.\n",
"(3) Il **fabrique un mot** (`ancrear`, hybride français/espagnol de\n",
"« ancrer »). Le mécanisme de HyDE reste pourtant intact : on n'encode plus la\n",
"question, on encode **le texte généré**, quelle que soit sa qualité — le\n",
"`retrieve_hyde` ne vérifie rien. Le classement qui en résulte\n",
"(`['rag-1', 'rag-2', 'rag-0', 'kubernetes-0', 'transformer-1']`) est lu\n",
"contre la baseline bi-encoder de la section précédente\n",
"(`['rag-0', 'rag-2', 'overfit-1', 'rag-1', 'transformer-1']`) : deux\n",
"mouvements opposés s'annulent presque. Gain — `rag-1` remonte du rang 4 au\n",
"rang 1 et l'intrus `overfit-1` est éliminé. Perte — `rag-0`, le meilleur\n",
"document, **recule du rang 1 au rang 3**, et un intrus NOUVEAU apparaît\n",
"(`kubernetes-0`). Le solde est un déplacement de bruit, pas une\n",
"amélioration : HyDE, tel que publié (Gao *et al.*, 2022), suppose un modèle\n",
"instruct capable de rédiger un passage plausible ; `flan-t5-small` sans\n",
"affinage ne satisfait pas cette hypothèse, et le notebook l'expose au lieu de\n",
"la masquer. C'est le point de méthode : une technique ne se juge pas sur son\n",
"nom, mais sur la qualité de son entrée."
]
},
{
"cell_type": "markdown",
"id": "3de0f45a",
Expand Down Expand Up @@ -681,6 +824,41 @@
"print(\"Après reranking :\", [doc_id for doc_id, _ in reranked_example])\n"
]
},
{
"cell_type": "markdown",
"id": "04abe986",
"metadata": {},
"source": [
"### Lecture : le reranker corrige l'intrus — mais ne récupère rien\n",
"\n",
"Le tableau avant/après est le verdict d'un réordonnanceur qui a bien\n",
"fonctionné :\n",
"\n",
"- avant — `['rag-0', 'rag-2', 'overfit-1', 'rag-1', 'transformer-1']`\n",
"- après — `['rag-2', 'rag-0', 'rag-1', 'overfit-1', 'transformer-1']`\n",
"\n",
"Trois mouvements, tous conformes à la limite diagnostiquée sur le\n",
"bi-encoder. `rag-1` remonte du rang 4 au rang 3 en **passant devant\n",
"`overfit-1`** : l'intrus lexical qui s'était glissé au rang 3 est repoussé au\n",
"rang 4, et les trois documents du thème `rag` occupent désormais les rangs 1\n",
"à 3. `rag-2` et `rag-0` permutent en tête. C'est la démonstration que le\n",
"cross-encoder fait ce que le bi-encoder ne pouvait pas : il voit la paire\n",
"`(question, document)` dans une seule séquence et peut donc modéliser les\n",
"interactions entre les termes, là où deux encodages séparés ne comparent que\n",
"des directions de vecteurs. Mais la composition du top-5 est **identique** :\n",
"aucun document n'entre, aucun ne sort. C'est structurel, pas accidentel — le\n",
"`rerank` ne reçoit que la fenêtre `baseline_example` et **ne peut choisir que\n",
"parmi ses candidats** ; un document absent de la liste du bi-encoder est\n",
"définitivement hors de portée. D'où le coût, qui explique l'ordre\n",
"d'application : le cross-encoder exécute un passage avant par PAIRE\n",
"(question, document), soit 5 ici mais 50 à 100 en production — on rappelle\n",
"donc large et pas cher avec le bi-encoder, puis on reranke étroit et cher.\n",
"Dernier détail de lecture : les scores du cross-encoder sont des **logits non\n",
"bornés**, non comparables aux cosinus du bi-encoder ; seuls les RANGS se\n",
"comparent entre colonnes, ce que le notebook respecte en ne joignant jamais\n",
"les deux échelles dans un même tableau."
]
},
{
"cell_type": "markdown",
"id": "1607fc8a",
Expand Down Expand Up @@ -903,6 +1081,45 @@
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "e21df0c4",
"metadata": {},
"source": [
"### Lecture : -1.000 n'est pas une baisse, c'est une disparition — et le signe compte\n",
"\n",
"`delta_nDCG -1.000` répété cinq fois (q-python, q-rest, q-overfit sous HyDE\n",
"et HyDE + rerank) demande une lecture précise : le nDCG@10 ne baisse pas de\n",
"1, il **tombe à zéro**. Traduit en langage de retrieval : sur ces questions,\n",
"le document pertinent n'est plus dans les 10 premiers du tout. L'échelle\n",
"d'aide à mesurer la gravité — le corpus ne compte que 60 documents et la\n",
"fenêtre en couvre 10, soit un sixième ; perdre le bon document dans ces\n",
"conditions signale une dérive complète de la représentation de requête, pas\n",
"un réordonnancement malheureux. C'est la conséquence directe du texte\n",
"dégénéré de la section 3 : quand la requête encodée ne parle plus du sujet,\n",
"le classement n'a plus de raison de contenir la réponse. Le résultat le plus\n",
"instructif est ailleurs : **`HyDE + rerank` est aussi à -1.000** sur q-python\n",
"et q-rest. Le cross-encoder, dont on vient de vérifier qu'il corrige\n",
"proprement les intrus quand le bon document est dans la fenêtre, est ici\n",
"**impuissant — non par faiblesse, mais parce qu'il ne reçoit pas le document\n",
"pertinent**. Un réordonnanceur ne peut pas réparer un rappel manquant ; c'est\n",
"la démonstration expérimentale de la limite énoncée après la cellule du\n",
"cross-encoder, et la raison pour laquelle l'ordre rappel-large → rerank-étroit\n",
"est une contrainte d'architecture, pas une préférence. Le second tableau\n",
"apporte la pièce manquante sur les questions **hors corpus**, et c'est\n",
"l'observation la plus utile du notebook. La baseline retourne un top-1 avec\n",
"des scores **faibles mais POSITIFS** (`docker-0 0.254`, `kubernetes-0 0.307`,\n",
"`0.233`, `0.278`) ; `HyDE + rerank` retourne des scores **négatifs**\n",
"(`-6.658`, `-4.874`, `-4.719`, `-4.043`). Le signe est une information\n",
"exploitable : le cross-encoder affirme explicitement la NON-pertinence de la\n",
"paire, alors qu'un cosinus — borné par le bas à -1 et structurellement\n",
"incapable de dire « aucun rapport » — laisse 0,25 ressembler à un score\n",
"acceptable. Il n'y a pas de document pertinent à trouver dans ces quatre cas ;\n",
"un système qui répond quand même avec aplomb est exactement le défaut que\n",
"l'exercice 3 (`should_abstain`) est chargé de corriger : le reranker fournit\n",
"le signal (un logit négatif), le seuil reste la décision à prendre."
]
},
{
"cell_type": "markdown",
"id": "34885014",
Expand Down
Loading