『「喜ぶ」と「嬉しい」は、品詞が違うのに近かった』で、意味の座標を決めているのは品詞ではなく「何のことを言っているか」だと分かりました。
では、話題が同じで、言っていることが正反対だったら。 これも測れるので、測りました。結果は、実務で使う側にとってかなり具合の悪いものでした。
何で測るのか
使うのは paraphrase-multilingual-MiniLM-L12-v2(以下 MiniLM)という公開モデルです。語や文を渡すと384個の数字の組——意味の座標——を返すだけの小さな道具で、ノートパソコンで動きます。
これは文章を書くLLMではありません。 ChatGPTやClaudeのような、文章を作る道具とは別物です。MiniLMがやるのは「この文とこの文は近いか」の判定だけで、文書検索やRAGで関係のありそうな箇所を探す工程に使われます。今回の話が実務に直結するのは、そのためです。道具そのものの詳しい説明は『「喜ぶ」と「嬉しい」は、品詞が違うのに近かった』にあります。
用語で引っかかったら、『LLMと日本語の用語集』に一言ずつまとめてあります。
活用形の比較では、否定は区別されているように見える
まず動詞の活用形から測ります。基準はすべて「走る」です。数字は1に近いほど意味が近いという見方をします。
| 組 | 近さ |
|---|---|
| 走る ↔ 走ります(丁寧にしただけ) | 0.956 |
| 走る ↔ 走った(時制を変えた) | 0.952 |
| 走る ↔ 走らない(否定にした) | 0.554 |
| 走る ↔ 走らなかった | 0.526 |
| 走る ↔ 食べる(別の動詞) | 0.218 |
丁寧さと時制は、ほとんど座標を動かしません。 「走ります」も「走った」も、「走る」の真横にあります。
いっぽう否定にすると0.554まで落ちる。ここだけ見れば、否定はきちんと区別されているように見えます。
文の単位では、正反対の指示でも近かった
ところが、単語ではなく文で測ると景色が変わります。
| 組 | 近さ |
|---|---|
| この薬は飲んでよい ↔ この薬は飲んではいけない | 0.764 |
| この薬は飲んでよい ↔ この薬は服用可能だ(言い換え) | 0.979 |
正反対の指示が0.764で、同じ意味の言い換えが0.979。 数字としては離れています。だからこれを見て「0.76なら十分遠いので区別できている」と読むこともできます。
その読み方が通らないことを示すには、順位で見る必要があります。 実際の検索は「0.76は遠いか近いか」を判定する仕組みではなく、候補を並べて上から拾う仕組みだからです。
近い順の1位は、正反対の文だった
「この薬は飲んでも大丈夫ですか」という質問を投げて、5つの文を近い順に並べます。
import { pipeline } from '@huggingface/transformers'
const ex = await pipeline('feature-extraction', 'Xenova/paraphrase-multilingual-MiniLM-L12-v2')
const emb = async w => Array.from((await ex(w, { pooling: 'mean', normalize: true })).data)
const cos = (a, b) => a.reduce((s, x, i) => s + x * b[i], 0)
const q = 'この薬は飲んでも大丈夫ですか'
const docs = [
'この薬は服用しても問題ありません',
'この薬は服用してはいけません',
'この薬は妊娠中の方は服用できません',
'この薬は食後に服用してください',
'本日の天気は晴れのち曇りです',
]
const qv = await emb(q)
const scored = []
for (const d of docs) scored.push([d, cos(qv, await emb(d))])
scored.sort((a, b) => b[1] - a[1])
for (const [d, s] of scored) console.log(`${s.toFixed(3)} ${d}`)
出力です。
0.753 この薬は服用してはいけません
0.750 この薬は食後に服用してください
0.737 この薬は服用しても問題ありません
0.652 この薬は妊娠中の方は服用できません
-0.012 本日の天気は晴れのち曇りです
1位が、質問への答えとして正反対の文でした。 正しい答えの「服用しても問題ありません」は3位。しかも1位との差は0.016しかありません。
いっぽう無関係な天気の文は -0.012 で、はるか下に落ちています。話題の切り分けは完璧でした。 薬の話か天気の話かは、一発で分かれている。
つまりこの座標が捉えているのは、「何について書かれているか」であって、「それが許可なのか禁止なのか」ではありません。 肯定と否定の差は、上位4件が固まった0.65〜0.75という帯の中に埋もれています。
なぜ否定が薄まるのか
文をベクトルにするとき、MiniLMは文を細かい断片に割り、断片ごとに座標を求め、それを平均して1つの座標にしています。 上のコードの pooling: 'mean' が、その「平均する」という指定です。
「この薬は服用しても問題ありません」と「この薬は服用してはいけません」は、大半の断片を共有しています。 違うのは末尾のわずかな部分だけ。それを平均すれば、差は薄まります。否定は、文全体のたった一箇所をひっくり返すことで意味を反転させる仕掛けなので、平均という操作とは相性が悪い。
この説明が正しければ、文が長いほど否定は薄まるはずです。同じ否定を、長さを変えた文に置いて測りました。
| 文 | 近さ |
|---|---|
| 行く ↔ 行かない | 0.732 |
| 私は行く ↔ 私は行かない | 0.693 |
| 私は明日そこへ行く ↔ 私は明日そこへ行かない | 0.700 |
| 私は明日の午後、駅前の会場へ行く ↔ (同・否定) | 0.834 |
| 私は明日の午後、駅前の会場で開かれる説明会へ行く ↔ (同・否定) | 0.806 |
きれいには出ませんでした。 長い側が高いという大まかな傾向はありますが、単調に上がってはおらず、0.69〜0.83のあいだで揺れています。「長い文ほど否定が消える」とまでは、この測定では言えません。 平均で薄まるという説明は仕組みとしては筋が通りますが、裏づけとしてはここまでです。
実務では、否定の見落としが事故になる
『自分の資料で専門特化のエージェントを作る』で書いたような、手持ちの資料から関係のありそうな箇所を探して読ませる仕組み——いわゆるRAGでは、この性質がそのまま事故につながります。
「〇〇してよいか」と質問すると、「〇〇してはいけません」と書かれた箇所が上位で拾われる。拾われた箇所は、そのままLLMに渡されます。 渡された側が正しく読めば「いけない、と書いてあります」と答えますが、上位3件をまとめて渡した場合、肯定と否定が混ざった資料を見せることになります。
対策として現実的なのは、次のあたりです。
- 拾ったあとに、もう一度並べ替える工程を挟む(リランカーと呼ばれる、質問と文書を一緒に読んで関連度を算出する別のモデル。断片の平均ではないので、否定を拾える見込みがある)
- 禁止・不可・注意にあたる記述を、資料の側であらかじめ目印付きで管理する
- 上位1件だけを答えの根拠にしない。 上位数件を渡して、LLMの側に読み比べさせる
そして何より、許可か禁止かが結果を左右する領域——薬、法令、契約、安全手順——では、意味の近さだけで拾った結果をそのまま答えにしないことです。
なお、ここで測ったのはMiniLMという384次元の小さな公開モデル1つと、5つの文です。否定への強さはモデルによって違うので、上のスクリプトの docs を実際の資料に差し替えて、使う予定のモデルで同じことを試すのが確実です。
まとめ
- 単語では、否定は区別されて見える。走る ↔ 走らないは0.554。丁寧さや時制(0.95台)とは比較にならない
- 文になると、正反対でも0.764まで戻る
- 近い順に並べると、正反対の文が1位、正しい答えが3位、差は0.016だった。いっぽう無関係な話題は -0.012。話題は完璧に分かれ、肯定と否定は分かれていない
- 仕組みとしては、文の断片を平均するやり方が否定と相性が悪い。ただし文の長さを変えた測定では、この説明はきれいには裏づけられなかった
- 実務では並べ替えの工程を足す・上位1件で決めない。薬・法令・契約・安全手順では、近さだけで拾った結果を答えにしない
意味の近さで探す仕組みは、「その話をしているか」を探す道具です。「その話について、何と言っているか」を探す道具ではありませんでした。