コンテンツへスキップ

「あああ いいい ううう」が0.604。ベクトル検索の点数は、なぜ0にならないのか

公開日
POSII COLUMN

「あああ いいい ううう」が0.604。
ベクトル検索の点数は、なぜ0にならないのか

——しきい値を「なんとなく0.7」で決めてはいけない理由を、実測で示します

先日、自社サービスで動いている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倍」ですが、小さい数どうしの比は簡単に大きくなります。見るべきは比ではなく、当たりの下限と外れの上限が離れたかどうかでした。

表をよく見ると、中心化で良くなったものと悪くなったものが両方あるのが分かります。「確定申告」は0.607→0.047で大きく下がった(改善)。一方「パソコンが起動しない」は0.548→0.090で相対的に上がった(悪化)。差し引きゼロ、というのが実態でした。

というわけで、当社はこの中心化を入れていません。キャッシュに平均ベクトルを持ち、しきい値を2種類管理し、件数が少ないときの例外処理を書く——それだけの複雑さに、見合う改善が測れなかったからです。コードには「試したが効かなかった」と実測値つきで残してあります。やらなかった理由を残すのも、設計です。

◇

実際に効いたのは、手前で落とすこと

「あああ いいい ううう」に対して有効だったのは、もっと素朴な方法でした。ベクトルにする前に落とす。

判定は2つだけにしています。

  • 日本語・英数字を1文字も含まない(記号や空白だけ)
  • 空白で区切ったすべての塊が、同じ文字の繰り返しだけでできている(「あああ」「wwwww」「ああああああ」)
⚠️ 短さでは絶対に切りません。「つらい」「死にたい」「助けて」は短いですが、本物の相談です。文字数で足切りする実装は、いちばん助けが必要な人を最初に落とします。当社のテストには「絶対に落としてはいけない入力」の一覧を置いて、そこが1つでも落ちたら失敗するようにしてあります。

効果は明快でした。「あああ いいい ううう」を投げると、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の導入で「精度が出ない」と言われる場面の多くは、モデルの性能ではなく、このしきい値と外れの扱いの設計です。当社は自社サービスでここを運用しているので、うまくいった話だけでなく、試して効かなかった話まで数字でお見せできます。

自社の資料で「外れの分布」を測ってみたい、という段階のご相談も承っています。お問い合わせはこちらからどうぞ。

最後までお読みいただき、心より感謝申し上げます。

大河原潤

大河原 潤

AI開発専門家

ブーム以前からAI研究に携わる、本物の専門家。「AIに使われる」のではなく、「AIを使いこなす」確かな技術力を提供します。

【アカデミックな裏付け】

  • カリフォルニア大学リバーサイド校 博士前期課程修了(研究分野:測度論、経路積分)
  • アメリカ数学会のジャーナルに論文発表

【社会的に認められた専門性】

  • AI関連書籍:『誤解だらけの人工知能』(2018年)、『AI×Web3の未来』(2023年)
  • プログラミング専門書:実務的な技術書を2冊出版(確かな実装力の証明)
  • 100社以上のAI導入コンサルティング実績、特許売却経験あり