Mesurer une recherche : rappel et rang réciproque
Beaucoup de fonctions d’IA cherchent dans un ensemble de documents, puis répondent d’après ce qu’elles trouvent. Quand la réponse est fausse, quelle partie a échoué : la recherche ou la réponse ?
On peut noter la recherche toute seule. Ici, la recherche est un compteur de mots clés écrit en code, sans plongements.
À la fin de cette leçon
Remarque
Je peux noter la recherche à part de la réponse.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Moteur de recherche | la partie qui trouve des documents pour une question | |
| Liste classée | des documents rangés, le meilleur d’abord | |
| Pertinent | un document qui contient vraiment la réponse | |
| Rappel à k | la part des documents pertinents parmi les k premiers | 0,5 |
| Rang réciproque | un divisé par le rang du premier document pertinent | 0,5 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un moteur compte les mots communs
Prenez la question et retirez les petits mots, comme « le » ou « est ». Retirez aussi les accents : « bibliotheque » trouve alors « bibliothèque ». Pour chaque document, comptez combien de mots de la question il contient, puis classez, le meilleur d’abord.
C’est simple, et cela a des limites. Il ne sait pas que « emprunt » et « emprunter » sont presque le même mot, ni que « prolonger » veut dire « renouveler ».
Exemple détaillé : une question, trois documents
| Mots de la question | Document | Mots communs | Rang |
|---|---|---|---|
| prix, emprunter, livres, retard | Le prix des livres en retard est de vingt centimes par jour | 3 | 1 |
| prix, emprunter, livres, retard | Les adhérents peuvent emprunter six livres | 2 | 2 |
| prix, emprunter, livres, retard | Demandez au guichet de renouveler vos livres | 1 | 3 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le rappel et le rang notent la liste
Le rappel à k demande combien de bons documents sont parmi les k premiers. Deux sont bons et un est dans les 2 premiers : le rappel à 2 vaut 0,5.
Le rang réciproque ne regarde que le premier bon document. Au rang 1 il vaut 1. Au rang 2 il vaut 0,5. Au rang 4 il vaut 0,25. Sa moyenne sur plusieurs questions est le rang réciproque moyen.
Exemple détaillé : cinq questions
| Question | Rang du bon document | Rang réciproque | Rappel à 1 |
|---|---|---|---|
| bibliotheque ouvre | 1 | 1 | 1 |
| bibliotheque fermee (deux bons) | 1 et 2 | 1 | 0,5 |
| combien de temps garder les livres avant le retard | 2 | 0,5 | 0 |
| prolonger mon emprunt | introuvable | 0 | 0 |
| imprimer une page | 1 | 1 | 1 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent qu’une réponse fausse montre que le modèle est mauvais. Si le bon document n’a jamais été trouvé, même un modèle parfait ne peut pas répondre. Notez d’abord la recherche, ensuite la réponse.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche par mots clés, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const docs = {' ' d1: \"La bibliothèque ouvre à neuf heures et ferme à dix-sept heures\",' ' d2: \"Les adhérents peuvent emprunter six livres pour trois semaines\",' ' d3: \"Le prix des livres en retard est de vingt centimes par jour\",' ' d4: \"Le café vend de la soupe et du thé\",' ' d5: \"Les ordinateurs sont gratuits pendant une heure\",' ' d6: \"Demandez au guichet de renouveler vos livres en ligne\",' ' d7: \"La bibliothèque est fermée le dimanche\",' ' d8: \"L’impression coûte dix centimes la page\",' '};' > recherche.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche par mots clés, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const vides = [\"le\", \"la\", \"les\", \"l\", \"un\", \"une\", \"des\", \"du\", \"de\", \"d\", \"et\", \"a\", \"au\", \"en\", \"est\", \"sont\", \"pour\", \"par\", \"mon\", \"ma\", \"vos\", \"ce\", \"que\", \"je\"];' 'const avec = \"àâäçéèêëîïôöùûüÿ\";' 'const sans = \"aaaceeeeiioouuuy\";' 'function sansAccent(texte) {' ' let sortie = \"\";' ' for (const c of texte) { const i = avec.indexOf(c); sortie += i >= 0 ? sans[i] : c; }' ' return sortie;' '}' >> recherche.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche par mots clés, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function mots(texte) {' ' const propre = sansAccent(texte.toLowerCase()).split(\"’\").join(\" \").split(\"-\").join(\" \");' ' return propre.split(\" \").filter(function (m) { return m.length > 0 && vides.indexOf(m) < 0; });' '}' 'function chercher(question) {' ' const q = mots(question);' ' const notes = Object.keys(docs).map(function (id) {' ' const a = mots(docs[id]);' ' return { id: id, note: q.filter(function (m) { return a.indexOf(m) >= 0; }).length };' ' }).filter(function (n) { return n.note > 0; });' ' notes.sort(function (x, y) { return y.note - x.note || (x.id < y.id ? -1 : 1); });' ' return notes.map(function (n) { return n.id; });' '}' >> recherche.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche par mots clés, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const questions = [' ' [\"bibliotheque ouvre\", [\"d1\"]], [\"bibliotheque fermee\", [\"d1\", \"d7\"]],' ' [\"combien de temps garder les livres avant le retard\", [\"d2\"]],' ' [\"prolonger mon emprunt\", [\"d6\"]], [\"imprimer une page\", [\"d8\"]],' '];' 'let sommeRr = 0;' 'let sommeRappel = 0;' 'questions.forEach(function (q) {' ' const classes = chercher(q[0]);' ' const place = classes.findIndex(function (id) { return q[1].indexOf(id) >= 0; });' ' const rr = place < 0 ? 0 : 1 / (place + 1);' ' const touche = classes.slice(0, 1).filter(function (id) { return q[1].indexOf(id) >= 0; }).length;' ' sommeRr += rr;' ' sommeRappel += touche / q[1].length;' ' console.log(q[0] + \" -> \" + (classes.join(\",\") || \"rien\") + \" rr \" + rr.toFixed(2));' '});' >> recherche.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche par mots clés, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"rang réciproque moyen : \" + (sommeRr / questions.length).toFixed(2));' ' console.log(\"rappel moyen à 1 : \" + (sommeRappel / questions.length).toFixed(2));' ' const q = \"En utilisant seulement ce texte, répondez en une phrase : \" + docs.d3 + \". Question : combien de livres puis-je emprunter ?\";' ' const echantillon = \"Le prix des livres en retard est de vingt centimes par jour.\";' ' const reponse = await demander([{ role: \"user\", content: q }], echantillon, 60);' ' console.log(\"réponse tirée du premier document : \" + reponse.texte);' '}' 'main();' >> recherche.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer la recherche et la noter
docker exec -w /home/node/project agent node recherche.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| bibliotheque ouvre -> d1,d7 rr 1.00 | La liste classée et sa note de rang |
| combien de temps garder ... -> d3,d2,d6 rr 0.50 | Le bon document est le deuxième |
| prolonger mon emprunt -> rien rr 0.00 | Un synonyme que le compteur ne voit pas |
| rang réciproque moyen : 0.70 | La moyenne sur cinq questions |
| réponse tirée du premier document : ... | Une phrase du labo. Un échantillon enregistré si aucun modèle ne répond |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.