検索の指標: 再現率と逆順位
多くのAI機能は、文書の集まりを検索し、見つけたものから答えます。答えが間違っていたとき、失敗したのは検索と答えのどちらでしょうか。
検索だけに点を付けることができます。ここでの検索は、コードで書いたキーワードの数え役で、埋め込みは使いません。
このレッスンのゴール
メモ
検索の点数を、答えとは別に付けられる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 検索役 | 質問に合う文書を見つける部分 | |
| 順位つきのリスト | 文書を、良い順に並べたもの | |
| 正しい文書 | 本当に答えを持っている文書 | |
| 上位 k 件の再現率 | 上位 k 件に入った、正しい文書の割合 | 0.5 |
| 逆順位 | 最初の正しい文書の順位の逆数 | 0.5 |
学生が入力するセルは、このプレビューでは空白です。
キーワード検索は、共通する文字の組を数える
日本語には言葉の間の空白がないので、文字を 2 文字ずつ組にして比べます。質問の組のうち、何個を文書が持っているかが、点数です。点数の高い順に並べます。
単純なので、限界があります。開き と 開く は別の組で、延長 と 更新 が同じ意味だとは知りません。
決めた四つのこと
| 手順 | すること | 例 |
|---|---|---|
| 1 そろえる | 全角を半角に、大文字を小文字に | 1ページ から 1ページ |
| 2 区切る | 助詞と、です、ます、から、まで、ください、ですか、ますか で切る | 図書館は何時に から 図書館|何時| |
| 3 組にする | 区切りの中を 2 文字ずつ重ねる。一文字の区切りは捨てる | 図書館 から 図書、書館 |
| 4 数える | 同じ組は一度だけ数える。語幹は処理しない | 開き と 開く は別 |
学生が入力するセルは、このプレビューでは空白です。
例: 一つの質問と三つの文書
| 質問 | 文書 | 共通する組の数 | 順位 |
|---|---|---|---|
| 遅れて返すと、いくらかかりますか | 返すのが遅れた本は、1日20円かかります | 4 | 1 |
| 遅れて返すと、いくらかかりますか | 会員は6冊を3週間まで借りられます | 0 | - |
| 遅れて返すと、いくらかかりますか | カフェではスープとお茶を売っています | 0 | - |
学生が入力するセルは、このプレビューでは空白です。
再現率と順位が、リストを採点する
上位 k 件の再現率は、正しい文書のうち、上位 k 件に入ったものの割合です。正しい文書が二つあり、上位 2 件に一つなら、0.5 です。
逆順位は、最初の正しい文書だけを見ます。1 位なら 1、2 位なら 0.5、4 位なら 0.25 です。多くの質問での平均が、平均の逆順位です。
例: 五つの質問
| 質問 | 正しい文書の順位 | 逆順位 | 1 位までの再現率 |
|---|---|---|---|
| 図書館は何時に開くか | 1 | 1 | 1 |
| 図書館はいつ閉まるか(正しい文書が二つ) | 1 と 2 | 1 | 0.5 |
| 借りた本は、いつまでに返せばよいか | 2 | 0.5 | 0 |
| 貸出の延長 | 見つからない | 0 | 0 |
| 印刷はいくらか | 1 | 1 | 1 |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
答えが間違っていたら、モデルが悪いと考える人が多いです。正しい文書が見つからなかったなら、完璧なモデルにも答えられません。先に検索に点を付け、次に答えに点を付けましょう。
助っ人 model.js を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答のトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
助っ人 model.js を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, 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(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const docs = {' ' d1: \"図書館は9時に開き、17時に閉まります\",' ' d2: \"会員は6冊を3週間まで借りられます\",' ' d3: \"返すのが遅れた本は、1日20円かかります\",' ' d4: \"カフェではスープとお茶を売っています\",' ' d5: \"パソコンは1時間まで無料で使えます\",' ' d6: \"本の更新は、カウンターかネットでできます\",' ' d7: \"図書館は日曜日は休みです\",' ' d8: \"印刷は1ページ10円です\",' '};' > search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const stop = [\"ですか\", \"ますか\", \"ください\", \"です\", \"ます\", \"から\", \"まで\", \"は\", \"が\", \"を\", \"に\", \"で\", \"の\", \"と\", \"も\", \"へ\", \"や\"];' 'function toHalf(t) {' ' return t.split(\"\").map(function (c) {' ' const n = c.charCodeAt(0);' ' if (n >= 0xFF01 && n <= 0xFF5E) return String.fromCharCode(n - 0xFEE0);' ' return n === 0x3000 ? \" \" : c;' ' }).join(\"\");' '}' >> search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function tokens(text) {' ' let t = toHalf(text).toLowerCase();' ' stop.forEach(function (s) { t = t.split(s).join(\"|\"); });' ' const out = [];' ' t.split(/[|、。 ?,.]+/).forEach(function (seg) {' ' for (let i = 0; i + 1 < seg.length; i++) {' ' const pair = seg.slice(i, i + 2);' ' if (out.indexOf(pair) < 0) out.push(pair);' ' }' ' });' ' return out;' '}' >> search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function search(query) {' ' const q = tokens(query);' ' const scored = Object.keys(docs).map(function (id) {' ' const have = tokens(docs[id]);' ' return { id: id, score: q.filter(function (w) { return have.indexOf(w) >= 0; }).length };' ' }).filter(function (s) { return s.score > 0; });' ' scored.sort(function (a, b) { return b.score - a.score || (a.id < b.id ? -1 : 1); });' ' return scored.map(function (s) { return s.id; });' '}' 'const queries = [' ' [\"図書館は何時に開きますか\", [\"d1\"]],' ' [\"図書館が閉まるのはいつですか\", [\"d1\", \"d7\"]],' ' [\"借りた本を遅れずに返すには、いつまでに返せばよいですか\", [\"d2\"]],' ' [\"貸出の延長はできますか\", [\"d6\"]],' ' [\"印刷は1ページいくらですか\", [\"d8\"]],' '];' >> search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(5)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'let sumRr = 0;' 'let sumRecall = 0;' 'queries.forEach(function (item) {' ' const ranked = search(item[0]);' ' const at = ranked.findIndex(function (id) { return item[1].indexOf(id) >= 0; });' ' const rr = at < 0 ? 0 : 1 / (at + 1);' ' const hit = ranked.slice(0, 1).filter(function (id) { return item[1].indexOf(id) >= 0; }).length;' ' sumRr += rr;' ' sumRecall += hit / item[1].length;' ' console.log(item[0] + \" -> \" + (ranked.join(\",\") || \"なし\") + \" 逆順位 \" + rr.toFixed(2));' '});' >> search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
キーワード検索を書く(6)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"平均の逆順位: \" + (sumRr / queries.length).toFixed(2));' ' console.log(\"1 位までの平均の再現率: \" + (sumRecall / queries.length).toFixed(2));' ' const q = \"次の文だけを使って、一文で答えてください。文: \" + docs.d3 + \"。質問: 借りた本は、いつまでに返せばよいですか。\";' ' const sample = \"この文には書かれていません。遅れた本は1日20円かかる、とだけあります。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 80);' ' console.log(\"一番上の文書からの答え: \" + reply.text);' '}' 'main();' >> search.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
検索を動かして採点する
docker exec -w /home/node/project agent node search.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| 図書館は何時に開きますか -> d1,d7 逆順位 1.00 | 順位つきのリストと、その順位の点 |
| 借りた本を遅れずに返すには、... -> d3,d2 逆順位 0.50 | 正しい文書は 2 番目 |
| 貸出の延長はできますか -> なし 逆順位 0.00 | 数え役には見えない言い換え |
| 平均の逆順位: 0.70 | 五つの質問の平均 |
| 一番上の文書からの答え: ... | ラボからの一文。答えがなければ記録済みサンプル |
学生が入力するセルは、このプレビューでは空白です。