Dance with Agents

「犬」と「いぬ」は、別の語として扱われている——表記を変えるだけで、意味ベクトルは遠くなった

ミディアム専門用語を名前のまま使います。初出で解説を添えるので、順に読めば追えます深掘り実践日本語処理手元で測る

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では、表記ゆれがそのまま取りこぼしになる。 資料側を揃えるか、検索語を展開する
  • 強さはモデルによる。本番で使うモデルで測るのが確実

「同じ意味なら同じように扱われるはずだ」という直感は、日本語では通りません。モデルが見ているのは意味ではなく、文字の並びの頻度だからです。

LLMと自然言語の記事

LLMと自然言語のページへ →