LLMは日本語を単語に割っていないで、分割の切れ目が文法の切れ目と一致しないことを見ました。ではその結果、何が起きるのか。
日本語では、書き方を変えただけで語が遠くなります。 手元で測れる話なので、測りました。
測る
使うのは 『人間はチンパンジーよりマグロに近かった』 と同じ paraphrase-multilingual-MiniLM-L12-v2(以下 MiniLM)という公開モデルです。語や文を渡すと384個の数字の組——意味の座標——を返すだけの小さな道具で、ノートパソコンで動きます。文章を書くLLMではなく、文書検索やRAGで「この文とこの文は近いか」を判定する側にあたります。
準備もその記事と同じで、npm install @huggingface/transformers だけです。
import { pipeline, AutoTokenizer } from '@huggingface/transformers'
const MODEL = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2'
const tok = await AutoTokenizer.from_pretrained(MODEL)
const ex = await pipeline('feature-extraction', MODEL)
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 words = ['犬', 'イヌ', 'いぬ', 'ドッグ']
for (const w of words) console.log(`${w} → ${JSON.stringify(tok.tokenize(w))}`)
const vecs = []
for (const w of words) vecs.push(await emb(w))
console.log('')
for (let i = 0; i < words.length; i++) {
for (let j = i + 1; j < words.length; j++) {
console.log(`${words[i]} ↔ ${words[j]}: ${cos(vecs[i], vecs[j]).toFixed(3)}`)
}
}
出力です。
犬 → ["▁","犬"]
イヌ → ["▁","イ","ヌ"]
いぬ → ["▁","い","ぬ"]
ドッグ → ["▁","ド","ッ","グ"]
犬 ↔ イヌ: 0.928
犬 ↔ いぬ: 0.423
犬 ↔ ドッグ: 0.551
イヌ ↔ いぬ: 0.631
イヌ ↔ ドッグ: 0.721
いぬ ↔ ドッグ: 0.732
同じ語が、書き方で0.42まで落ちる
すべて同じ動物を指す語です。 数字は1に近いほど意味が近いという見方をします(この数え方の中身は『人間はチンパンジーよりマグロに近かった』で説明しています)。
それなのに、こうなりました。
| 組 | 近さ |
|---|---|
| 犬 ↔ イヌ | 0.928 |
| 犬 ↔ ドッグ | 0.551 |
| 犬 ↔ いぬ | 0.423 |
「犬」と「いぬ」の距離が、「犬」と「ドッグ」より遠い。 意味の上では完全に同じ語なのに、です。
何が起きているのか
分割を見ると、事情が見えてきます。
犬 → ▁ | 犬 (1つの塊)
いぬ → ▁ | い | ぬ (断片2つ)
「犬」は1つの単位として扱われています。「犬」という並びが学習データに大量に登場するので、専用の単位が割り当てられている。
「いぬ」はそうではありません。「い」と「ぬ」という、それ自体では何も意味しない断片に割られます。ひらがな2文字は、日本語のあらゆる場所に登場する汎用の部品です。「い」も「ぬ」も、犬とは無関係な文脈で無数に登場する。その2つを合成したところで、犬という意味は立ち上がりにくい。
考えられる理由は2つあり、どちらが主かをこの実験だけでは切り分けていません。
- 分割の違い:1つの塊か、汎用の断片の寄せ集めか
- 出現頻度の違い:そもそも「いぬ」という表記が、テキスト中に少ない
ただしどちらであっても、行き着く先は同じです。 よく使われる表記ほど固有の単位を持ち、意味も強く宿る。有名人のことは詳しくで書いた「頻度がすべてを決める」という構図の、表記版ということになります。
英語も無傷ではない
念のため、英語でも分割を見ました。
human → ▁human (1つ)
dog → ▁dog (1つ)
gorilla → ▁gor | illa
chimpanzee → ▁chi | mpan | ze | e
日常語は1つの単位を持ち、そうでない語は砕けます。 chimpanzee が4断片になるのは、日本語の「いぬ」と同じ扱いです。空白があるかどうかとは、別の軸の問題でした。
実務では、表記を揃える
この話が関わってくるのは、意味の近さで検索する仕組みを作るときです。自分の資料で専門特化のエージェントを作るで書いたような、いわゆるRAGがそれにあたります。
資料の中で「犬」と書いてあり、検索する側が「いぬ」と打った場合、近さが0.42しかないということは、引っかからない可能性があるということです。同じことは「引越し/引っ越し」「サーバ/サーバー」「Web/ウェブ」でも起きます。
対処は単純で、資料の側の表記を揃えておくか、検索語のほうを複数の表記に展開して投げるかです。
ただし、ここで測ったのはMiniLMという384次元の小さな公開モデル1つです。表記ゆれへの強さはモデルによって違うので、本番で使うモデルで同じ測定をやってみるのが確実です。上のスクリプトは、語を差し替えるだけでそのまま使えます。
まとめ
- 同じ語でも、表記を変えると意味ベクトルが離れる。「犬 ↔ いぬ」は0.42まで落ちた
- 原因は分割の違いと出現頻度の違い。「犬」は専用の単位を持ち、「いぬ」は汎用の断片に砕ける
- 英語も同じ構造を持つ。日常語は1つの単位、そうでない語は断片になる
- 検索やRAGでは、表記ゆれがそのまま取りこぼしになる。 資料側を揃えるか、検索語を展開する
- 強さはモデルによる。本番で使うモデルで測るのが確実
「同じ意味なら同じように扱われるはずだ」という直感は、日本語では通りません。モデルが見ているのは意味ではなく、文字の並びの頻度だからです。