「あああ いいい ううう」が0.604。
ベクトル検索の点数は、なぜ0にならないのか
先日、自社サービスで動いているRAGの検索スコアを公開しました。その中で、無関係な質問には答えさせないためにしきい値0.60を置いている、と書きました。その続きです。
検証のつもりで、意味のない文字列を投げてみました。「あああ いいい ううう えええ おおお」。結果はこうです。
「歯が痛いので歯医者を探しています」 → 0.553 (しきい値割れ・正しく弾く) 「カレーのレシピを教えてください」 → 0.544 (しきい値割れ・正しく弾く) 「あああ いいい ううう えええ おおお」 → 0.604 ← しきい値0.60を通過
意味のない文字列が、意味のある無関係な質問より高い点数を出しました。しかも、しきい値を超えています。
これは実装のバグではありません。ベクトル検索がそういうものだからです。この記事では、なぜこうなるのかを実測しながら分解して、対処法まで書きます。
まず確認:無関係なら0になるはず、は正しい
ここで使っているのはコサイン類似度です。2本のベクトルのなす角のコサインで、向きが同じなら1、直交していれば0になります。つまり「まったく無関係なら0」というのは、数学的には正しい期待です。
しかも、次元が高いとそれは強く成り立ちます。768次元の空間でランダムに2本ベクトルを選ぶと、ほぼ確実に直交します。実際に測りました。乱数で正規化したベクトルを2,000本作り、1,000本×1,000本の総当たりで類似度を計算した結果です。
乱数ベクトル同士(768次元・100万ペア) 平均 -0.0000 標準偏差 0.0361 絶対値の最大 0.1769 理論値 1/√768 = 0.0361 ← 標準偏差とぴたり一致
教科書どおりです。高次元空間では、無関係な2本は0のまわりに±0.036程度の幅で集まります。0.55などという数字はまず出ません。
念のため、乱数ベクトルと実際のコーチの埋め込みも比べました。平均 -0.0014。こちらもきれいに直交しています。
では、なぜ実際の文章同士は0.55も出るのか
答えは、こちらを測ると出ます。コーチ7人の埋め込み同士の類似度です。得意分野は職場の人間関係、思春期の子育て、死別、キャリアと、まるでバラバラの7人です。
コーチ同士のペアワイズ類似度(21ペア) 平均 0.834 / 最小 0.803 / 最大 0.877
0.83です。専門も経歴も違う7人のプロフィールが、互いに0.83も似ている。乱数なら0.00になる指標で、です。
埋め込みは、768次元の空間に散らばっていない。
これが正体です。文章の埋め込みは、空間全体に均等にばらまかれてはいません。「日本語で書かれた自己紹介文」というだけで、非常に狭い一角に固まっています。その狭い錐の中での、さらに小さな差が「意味の違い」です。
ですから点数0.55の中身は、ほとんどが「同じ種類のテキストである」という共通成分で、意味の一致はその上に乗った薄い層でしかありません。
それを数字で確かめる
共通成分がどれくらいを占めているのかは、直接測れます。コーチ7人の埋め込みの平均ベクトルを作り、それと各クエリの類似度を見ればいいのです。
| クエリ | 検索の生スコア(最大) | 平均ベクトルとの類似度 |
|---|---|---|
| 上司の言い方がつらい(当たり) | 0.652 | 0.637 |
| 父を亡くした(当たり) | 0.625 | 0.614 |
| 歯が痛い(外れ) | 0.553 | 0.586 |
| カレーのレシピ(外れ) | 0.544 | 0.573 |
| あああ いいい ううう | 0.604 | 0.629 |
2列目と3列目が、ほとんど同じ値です。検索スコアの中身は、その大半が「平均との近さ」でした。誰と比べたかではなく、そもそも同じ方角を向いているかどうかを測ってしまっている、ということです。
そして「あああ いいい ううう」の行を見てください。平均ベクトルとの類似度が0.629と、当たりの2件(0.637 / 0.614)に匹敵します。意味が無いぶん、意味の層が乗らず、共通成分だけがそのまま出てきたのです。だからしきい値を通過しました。
教科書どおりの対処を試したら、効きませんでした
ここまで分かれば、対処は明らかに見えます。共通成分が邪魔なら、引けばいい。全ベクトルの平均を引いてから測り直す、中心化(centering)という単純な処理です。
実装して、最初の5問で測りました。結果は劇的でした。
そのまま 重心を引いた後 当たり 0.652 → 0.185 当たり 0.625 → 0.199 外れ 0.553 → 0.030 外れ 0.544 → 0.044 あああ 0.604 → 0.081
当たりが外れの2倍から6倍。これで解決した、と書きかけました。
ですが、5問では少なすぎます。当たり6問・外れ7問に増やして測り直しました。
| クエリ | そのまま | 重心を引いた後 | |
|---|---|---|---|
| 当 | 上司の言い方がつらい | 0.652 | 0.185 |
| 当 | 頼まれると断れない | 0.644 | 0.105 |
| 当 | 子どもが口をきかない | 0.653 | 0.261 |
| 当 | 父を亡くした | 0.625 | 0.199 |
| 当 | 転職を決められない | 0.674 | 0.222 |
| 当 | 夫と家事の分担でけんかになる | 0.574 | 0.062 |
| 外 | 歯が痛い | 0.553 | 0.030 |
| 外 | カレーのレシピ | 0.544 | 0.044 |
| 外 | 明日の天気 | 0.525 | 0.038 |
| 外 | 確定申告の書き方 | 0.607 | 0.047 |
| 外 | パソコンが起動しない | 0.548 | 0.090 |
| 外 | おすすめの映画 | 0.566 | 0.061 |
| 外 | あああ いいい ううう | 0.604 | 0.081 |
まとめると、こうなりました。
| 当たりの最小 | 外れの最大 | すき間 | 順位の分離 | |
|---|---|---|---|---|
| そのまま | 0.574 | 0.607 | -0.033 | 40/42 |
| 重心を引いた後 | 0.062 | 0.090 | -0.029 | 40/42 |
数字は小さくなった。判別能力は、変わっていない。
どちらも当たりと外れが重なっています(すき間がマイナス)。当たり×外れの全42ペアで当たりが上に来た割合も、40/42で同じ。中心化がしてくれたのは、目盛りの付け替えだけでした。
なぜ最初は効いたように見えたのか。理由は2つです。5問しか測っていなかったこと。そして比を見ていたこと。0.030が0.185になれば「6倍」ですが、小さい数どうしの比は簡単に大きくなります。見るべきは比ではなく、当たりの下限と外れの上限が離れたかどうかでした。
というわけで、当社はこの中心化を入れていません。キャッシュに平均ベクトルを持ち、しきい値を2種類管理し、件数が少ないときの例外処理を書く——それだけの複雑さに、見合う改善が測れなかったからです。コードには「試したが効かなかった」と実測値つきで残してあります。やらなかった理由を残すのも、設計です。
実際に効いたのは、手前で落とすこと
「あああ いいい ううう」に対して有効だったのは、もっと素朴な方法でした。ベクトルにする前に落とす。
判定は2つだけにしています。
- 日本語・英数字を1文字も含まない(記号や空白だけ)
- 空白で区切ったすべての塊が、同じ文字の繰り返しだけでできている(「あああ」「wwwww」「ああああああ」)
効果は明快でした。「あああ いいい ううう」を投げると、0.1秒で「見つかりませんでした」が返ります。埋め込みのAPIすら呼びません(正常な相談は12秒かかります)。ムダな課金も発生しません。
ここから引き出せる実務の話
- しきい値を一般論で決めてはいけません。「0.7以上なら関連あり」といった数字が出回っていますが、絶対値は使うモデルとデータの偏りで決まります。自社のデータで、当たりの分布と外れの分布を実際に測って、その間を取る以外に方法はありません。
- 外れの分布こそ測ってください。当たりのスコアは誰でも見ます。ですが事故を起こすのは外れのほうです。無関係な質問を10個ほど投げる。それだけで、しきい値の根拠ができます。
- 絶対値では完全に分離できない、と最初から想定しておく。今回、当たりの最小(0.574)より高い外れ(0.607)が実在しました。しきい値1本で切る設計は、必ずどこかで外します。
- 一方で、順位は信用できます。42ペア中40ペアで当たりが上でした。「上位に出すが、確信がなければ言い切らない」という見せ方のほうが、実態に合っています。
- 変な入力は、ベクトルに変換する前に落とす。空間の性質に頼って弾こうとすると、今回のように裏をかかれます。
- そして、5サンプルで結論を出さない。これが今回いちばんの教訓でした。効いたように見えた施策が、測り直したら効いていなかった。
まとめ
- 高次元では、無関係な2本は本当に直交します。768次元の乱数ベクトル100万ペアで、平均0.0000・標準偏差0.0361(=1/√768)でした。
- それでも文章同士が0.55出るのは、埋め込みが空間の狭い一角に固まっているから。専門の違う7人のプロフィールが、互いに0.834似ていました。
- 点数の大半は「意味の一致」ではなく「同じ種類のテキストであること」。だから意味のない文字列がしきい値を突破します。
- 共通成分を引く中心化は、判別を改善しませんでした。13問で実測。目盛りが変わるだけでした。
- 効いたのは、変な入力を手前で落とすこと。ただし短さで切らないこと。
- しきい値1本では分離しきれない。順位のほうを信用する設計にする。
RAGの導入で「精度が出ない」と言われる場面の多くは、モデルの性能ではなく、このしきい値と外れの扱いの設計です。当社は自社サービスでここを運用しているので、うまくいった話だけでなく、試して効かなかった話まで数字でお見せできます。
自社の資料で「外れの分布」を測ってみたい、という段階のご相談も承っています。お問い合わせはこちらからどうぞ。