Dance with Agents

人間はチンパンジーよりマグロに近かった——LLMの意味空間は、生き物の分類でできていない

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

言葉が数字になるまでで、言葉が意味空間の座標に変換される話を書きました。ただ「ベクトル」という言葉は、読んだだけではどうにも掴みどころがありません。

実物は、手元で取り出せます。 公開モデルを1つ落としてくれば、ノートパソコンで動きます。この記事は、実際に取り出した数字をそのまま並べたものです。

同じ道具を使って測った記事を、いくつも書いています(テーマ『手元で測る』にまとめてあります)。この記事は、その入口にあたります。

ベクトル・トークンといった言葉は『LLMと日本語の用語集』に一言ずつまとめてあるので、引っかかったらそちらへ。


準備

Node.jsがインストールされていれば、これだけです。

mkdir vec-demo && cd vec-demo
npm install @huggingface/transformers

初回の実行時に、モデルのダウンロードが走ります(約450MB)。2回目以降はキャッシュから読むので、すぐ動きます。GPUは要りません。

落としてくるモデルの名前は paraphrase-multilingual-MiniLM-L12-v2 です。長い名前ですが、やることは一つだけ——語や文を渡すと、384個の数字の組を返す。 それだけです。100以上の言語をまとめて学習しているので、日本語も扱えます。以下では MiniLM と短く呼びます。

一つ、大事な区別があります。これは文章を書くLLMではありません。 ChatGPTやClaudeのように文章を作る道具とは別物で、「この文とこの文は近いか」を判定するためだけにあります。文書検索や、自分の資料で専門特化のエージェントを作るで書いたRAGで、関係のありそうな箇所を探す工程に使われる側です。

動かすもの

demo.mjs として保存して node demo.mjs で動きます。

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 words = ['人間', 'ヒト', 'チンパンジー', 'ゴリラ', 'イヌ', 'マグロ', '机', '自由']
const raw = []
for (const w of words) raw.push(await emb(w))

console.log(`次元数: ${raw[0].length}`)
console.log(`「人間」の先頭6次元: ${raw[0].slice(0, 6).map(x => x.toFixed(4)).join(', ')} ...`)

// 全体の平均を引いてから、長さを1に揃え直す
const d = raw[0].length
const mean = new Array(d).fill(0)
for (const v of raw) for (let i = 0; i < d; i++) mean[i] += v[i] / raw.length
const vecs = raw.map(v => {
  const u = v.map((x, i) => x - mean[i])
  const n = Math.hypot(...u)
  return u.map(x => x / n)
})

for (const target of ['人間', 'チンパンジー']) {
  const i = words.indexOf(target)
  const near = words
    .map((w, j) => [w, cos(vecs[i], vecs[j])])
    .filter((_, j) => j !== i)
    .sort((a, b) => b[1] - a[1])
    .map(([w, s]) => `${w}(${s.toFixed(2)})`)
  console.log(`【${target}】 ${near.join(' > ')}`)
}

出力です。

次元数: 384
「人間」の先頭6次元: 0.0034, 0.0345, -0.0091, -0.0069, -0.0638, -0.0783 ...
【人間】 ヒト(0.86) > マグロ(0.03) > 机(-0.06) > イヌ(-0.12) > 自由(-0.14) > チンパンジー(-0.45) > ゴリラ(-0.47)
【チンパンジー】 ゴリラ(0.60) > イヌ(-0.09) > 自由(-0.29) > 机(-0.30) > マグロ(-0.42) > 人間(-0.45) > ヒト(-0.48)

1つめ:数字そのものには、意味がない

「人間」は384個の数字でした。この個数を次元数といい、モデルごとに決められた設計値です。384はこのモデルの値で、他のモデルなら別の数になります。

先頭の6個を見ても、何の手がかりもありません。1次元目が何を表しているか、という問いには答えがありません。

意味は、1つ1つの数字ではなく並び全体にあり、しかも他の語との位置関係としてしか取り出せません。だから、比べる相手が要ります。

比べ方——コサイン類似度

上の出力に並んでいる数字が、その「比べた結果」です。コサイン類似度といいます。名前は難しいのですが、中身は単純です。

384個の数字は、384次元の空間に引いた矢印だと思ってください。次元が多すぎて絵には描けませんが、考え方は紙の上の矢印と同じです。コサイン類似度は、2本の矢印がどれだけ同じ方向を向いているかを表します。矢印の長さは見ません。向きだけです。

2本の矢印 角度 値
同じ方向 0度 1
直角 90度 0
正反対 180度 −1

右の列は、その角度のコサインそのものです。cos 0° = 1、cos 90° = 0、cos 180° = −1。名前はここから来ています。角度を、扱いやすい数字に置き換えただけのものです。

なぜ長さを見ないのか。 矢印の長さは、その語がどれだけ文章に登場するかといった、意味とは関係のない事情で変わります。 意味を担っているのは向きのほうなので、先に長さを1に揃えてしまいます。スクリプトの normalize: true がその処理です。

長さが揃っているおかげで、計算はこれだけで済みます。

const cos = (a, b) => a.reduce((s, x, i) => s + x * b[i], 0)

同じ位置の数字どうしを掛けて、全部足しているだけです。384組を掛けて足す。それで向きの一致度が出ます。

2つめ:生の数字は、そのままでは読めない

スクリプトの真ん中に「全体の平均を引く」という処理があります。これを外すと、結果はこうなります。

【人間】 ヒト(0.97) > マグロ(0.75) > イヌ(0.67) > 机(0.63) > 自由(0.55) > チンパンジー(0.50) > ゴリラ(0.47)

何と何を比べても0.4〜0.9に収まってしまい、差が読めません。 これは意味の話ではなく、ベクトルが空間の狭い一角に固まっているために起きます。全部が同じ方向を向いているので、どれとどれも「そこそこ似ている」ように見える。

全体の平均を引くと、その共通成分が消えて、差だけが残ります。 上の処理はそのためのものです。似ていないものはマイナスになります。

3つめ:人間は、類人猿の隣にいない

本題です。チンパンジーとゴリラは0.60でぴったり近い。ところが人間は、どちらからも−0.45前後で、はっきり遠い。 人間のほうに近いのは、マグロや机のほうでした。

生物学的な分類は、ここには含まれていません。 含まれているのは文章の中での使われ方の近さだけです。「人間」という語は「人間とは何か」「人間関係」のような文脈で使われ、「ゴリラ」は生態や動物園の文脈で使われる。別の場所で使われる語は、別の場所に置かれます。

ただし、ここまでは推測です。 もっともらしい説明ではありますが、この記事の中では確かめていません。

そこで別途、先に予言を立てて測り直しました——クジラのほうが、人間より哺乳類だった。推測どおりだった部分と、外れた部分の両方が載っています。

英語で同じことをやっても、結果は変わりませんでした。

【human】      freedom(0.02) > desk(-0.10) > dog(-0.14) > gorilla(-0.22) > chimpanzee(-0.26)
【chimpanzee】 gorilla(0.38) > dog(-0.16) > desk(-0.19) > human(-0.26) > freedom(-0.27)

humanのいちばん近くにいるのは freedom です。 日本語固有の癖ではなく、そういう作りだということになります。

4つめ:足し算引き算が効く

意味空間という言い方が誇張でないことは、演算で確かめられます。単語のベクトルを足し引きしてから、いちばん近い語を探します。

const u = V['王'].map((x, i) => x - V['男'][i] + V['女'][i])

結果です。

王   - 男 + 女 → 女王(0.84) > 王女(0.80) > 女(0.76)
父   - 男 + 女 → 母(0.82) > 女(0.72) > 父(0.64)
王子 - 男 + 女 → 王女(0.79) > 女王(0.75) > 女(0.69)

「男から女への向き」が、空間の中で一定の方向として存在しています。 王からその方向に進むと女王に着く。父から進むと母に着く。教えられた対応表ではなく、位置関係として持っていることが、ここで見えます。

但し書き

  • ここで使ったのは384次元の小さな公開モデルで、Claudeではありません。 Anthropicはモデルの中身を公開していないので、同じことはできません
  • 単語1つを与えていますが、このモデルは本来文を扱うものです。単語単体は本来の用途から外れた使い方で、結果もその分ぶれます
  • 語の選び方で結果は動きます。8語の中での相対的な近さを見ている、という点は外さないでください

まとめ

  • 意味のベクトルは、公開モデルを落とせば手元で実際に取り出せる。GPUは要らない
  • 中身は数字の並びで、1つ1つには意味がない。意味は他の語との位置関係としてしか現れない。個数(次元数)はモデルごとの設計値で、384個はこのモデルの場合
  • 生のコサイン類似度は読めない。 全部が0.4〜0.9に張り付くので、全体の平均を引いてから見る
  • 人間はチンパンジーやゴリラから遠い。 意味空間にあるのは生物学的分類ではなく、文章中での使われ方の近さ——ただしこれは本記事では推測にとどまり、別記事で測り直している
  • 足し引きが成り立つ。「王 − 男 + 女」は女王に着く。日本語でも再現する

「ベクトル」という言葉が曖昧に感じられるのは当然で、実物も曖昧です。 1つ1つの数字は説明できません。それでも、並べて比べると構造が見える。 そこだけが確かなところです。

なお、この記事で使った語のうち「イヌ」の扱いには別の問題があります。それは「犬」と「いぬ」は別の語として扱われているで扱います。

LLMと自然言語の記事

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