diff --git a/MyIA.AI.Notebooks/GenAI/RAG-et-Memoire-Semantique/02-Retrieval-Avance.ipynb b/MyIA.AI.Notebooks/GenAI/RAG-et-Memoire-Semantique/02-Retrieval-Avance.ipynb index 67ed66a459..30f94ec307 100644 --- a/MyIA.AI.Notebooks/GenAI/RAG-et-Memoire-Semantique/02-Retrieval-Avance.ipynb +++ b/MyIA.AI.Notebooks/GenAI/RAG-et-Memoire-Semantique/02-Retrieval-Avance.ipynb @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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",