二つの検索、二つの死角
ハイブリッド検索は、キーワード検索と意味検索を両方走らせ、二つの順位表を一つにまとめます。片方が見落とすものをもう片方が拾うので、役に立ちます。
「ユーザー名 忘れた」と入力した人がいます。ヘルプページに「ユーザー名」はなく、キーワードでは何も出ません。「注文番号 4417」はその逆で、意味検索には 4417 が何か分かりません。
このレッスンのゴール
メモ
キーワード検索と意味検索を一つの順位表にまとめ、どちらが勝つ場面かを説明できる。
キーワード
| ことば | いみ |
|---|---|
| BM25 | 珍しい語と短い文書に高い点を付けるキーワード点 |
| 語の頻度 | 一つの文書にその語が出る回数 |
| 逆文書頻度 | 含む文書が少ない語ほど大きくなる重み |
| 順位 | リストの中の位置。1 から数える |
| 逆順位融合 | 1 を k と順位の和で割った値を足して統合する方法 |
| 語彙検索 | 語そのものを照合する検索 |
学生が入力するセルは、このプレビューでは空白です。
BM25 は語そのものを採点する
キーワード検索は、質問の語が各文書に何回出るかを数えます。BM25 は、この単純な数え方を三つ改良します。含む文書が少ない語ほど重くなります。繰り返しの効果は、回を追うごとに小さくなります。回数が同じなら、短い文書が長い文書に勝ちます。
結果は正確です。ない語は一点にもなりません。これが強みであり、死角でもあります。
意味検索はベクトルを比べる
検索拡張生成のモジュールでは、テキストをベクトルにして向きを比べました。ここでは各ヘルプページに三つの作り物の数を付けます。アカウント、配送、お金にどれだけ関わるかです。
質問にも数を付け、近いページが勝ちます。「ユーザー名 忘れた」はアカウントの話なので、共通の語がなくてもアカウントのページが上に来ます。本物は embeddings モデルが数百個の数を作ります。
点ではなく順位を統合する
BM25 の点は 0 から十以上まで動きます。コサインの点は 0 から 1 の間です。足すと、片方のリストがもう片方をかき消します。
逆順位融合は点を使わず、順位を使います。要素は、それを含む各リストから 1 / (60 + 順位) 点をもらい、合計します。両方のリストが選ぶ要素は、片方だけが選ぶ要素に勝ちます。
例: 注文番号 4417
| ページ | キーワードの順位 | 意味の順位 | 統合の点 |
|---|---|---|---|
| d4 荷物の追跡 | 1 | 3 | 0.0323 |
| d3 返品と返金 | なし | 1 | 0.0164 |
| d5 支払い | なし | 2 | 0.0161 |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
ハイブリッド検索は二つの点を足すことだと考える人がいます。目盛りが違うので、いつも同じリストが勝ちます。代わりに 1 / (k + 順位) で順位を足せば、二つのリストの発言力は同じになります。
ハイブリッド検索を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const docs = [' ' { id: \"d1\", text: \"パスワード 再設定 ログイン 画面\", v: [9, 0, 1] },' ' { id: \"d2\", text: \"メールアドレス 変更 アカウント\", v: [8, 1, 0] },' ' { id: \"d3\", text: \"返品 荷物 返金 規約\", v: [1, 5, 8] },' ' { id: \"d4\", text: \"荷物 追跡 注文番号\", v: [0, 9, 1] },' ' { id: \"d5\", text: \"支払い クレジットカード 銀行振込\", v: [1, 1, 9] },' ' { id: \"d6\", text: \"アカウント 削除 データ\", v: [7, 0, 2] },' '];' 'const K1 = 1.5, B = 0.75;' 'function words(t) { return t.toLowerCase().split(\" \"); }' 'const avg = docs.reduce(function (s, d) { return s + words(d.text).length; }, 0) / docs.length;' > hybrid1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハイブリッド検索を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function bm25(query, doc) {' ' let score = 0;' ' words(query).forEach(function (w) {' ' const n = docs.filter(function (d) { return words(d.text).includes(w); }).length;' ' if (n === 0) return;' ' const idf = Math.log(1 + (docs.length - n + 0.5) / (n + 0.5));' ' const f = words(doc.text).filter(function (x) { return x === w; }).length;' ' const len = words(doc.text).length;' ' score += idf * f * (K1 + 1) / (f + K1 * (1 - B + B * len / avg));' ' });' ' return score;' '}' >> hybrid1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハイブリッド検索を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function cosine(a, b) {' ' let dot = 0, sa = 0, sb = 0;' ' for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; sa += a[i] * a[i]; sb += b[i] * b[i]; }' ' return dot / Math.sqrt(sa * sb);' '}' 'function ranked(scores) {' ' return docs.map(function (d, i) { return { id: d.id, s: scores[i] }; })' ' .filter(function (x) { return x.s > 0.01; })' ' .sort(function (x, y) { return y.s - x.s; })' ' .map(function (x) { return x.id; });' '}' 'function fuse(lists) {' ' const total = {};' ' lists.forEach(function (list) {' ' list.forEach(function (id, at) { total[id] = (total[id] || 0) + 1 / (60 + at + 1); });' ' });' ' return Object.keys(total).sort(function (x, y) { return total[y] - total[x]; });' '}' >> hybrid1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ハイブリッド検索を書く(4)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const queries = [' ' { text: \"ユーザー名 忘れた\", v: [9, 0, 0] },' ' { text: \"注文番号 4417\", v: [2, 3, 3] },' '];' 'queries.forEach(function (q) {' ' const a = ranked(docs.map(function (d) { return bm25(q.text, d); }));' ' const b = ranked(docs.map(function (d) { return cosine(q.v, d.v); })).slice(0, 3);' ' console.log(q.text);' ' console.log(\" キーワード:\", a.join(\" \") || \"なし\");' ' console.log(\" 意味:\", b.join(\" \"));' ' console.log(\" 統合:\", fuse([a, b]).join(\" \"));' '});' >> hybrid1.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
実行してリストを比べる
docker exec -w /home/node/project agent node hybrid1.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
これは練習用の代用品です
Run はモデルを呼びません。ベクトルは三つの作り物の数で、六ページが索引の代わりです。語はスペースで区切り、助詞は除いてあります。実際は形態素解析器が語に分けます。
本物は数百万ページを持ち、ベクトルは embeddings モデルから来ます。形は同じで、二つの順位表から一つの統合リストを作ります。
Run の出力
| 質問 | キーワード | 意味 | 統合 |
|---|---|---|---|
| ユーザー名 忘れた | なし | d1 d2 d6 | d1 d2 d6 |
| 注文番号 4417 | d4 | d3 d5 d4 | d4 d3 d5 |
学生が入力するセルは、このプレビューでは空白です。