単語ベクトルとは何かを抽象的に説明

この記事では自然言語処理や言語モデルでよく使われる単語ベクトルというものを抽象的に理解できるようにざっくりと説明したいと思います。

:::note
身近な事例で理解しやすくするためにかなり抽象化している部分があります。実際の単語ベクトルとは異なる場合がありますのでご了承ください。
:::

## 単語ベクトルとは？

### 単語の数値化

コンピューターは数値しか扱えないので、文字や単語を扱う際には、それらを数値化する必要が出てきます。たとえば、文字をコンピューターで表現する場合は「文字コード」という形で文字に対応する数値が定義されています。

しかし、自然言語を処理する上では、文字コードのようなただの数字では少し物足りません。単語の「意味」を考慮して数値化する必要があるからです。そこで考えられたのが「単語ベクトル」です。

### プログラム上はただの配列

ベクトルなので、プログラム上は単なる配列として書けます。例えばこんなのです。

```python
[1.0, 1.0, 1.0, 1.0, 1.0]
```

これは、実数が5つ並んでいるので五次元のベクトルと言えるでしょう。このような複数の実数の組み合わせで、単語の意味を数値化したのが単語ベクトルです。

## なぜ数値を並べたら意味が表現できるのか

そうは言っても、なぜ数値を並べただけで意味が表現できるのでしょうか？

### 意味を表現するとはどういうことか

意味を表現するというのは、複数の単語があったときに、**それらが似た意味なのか異なる意味なのかを数値で判断できる**ということです。

例えば、「犬」と「猫」はペットや動物といった共通の文脈で使われることが多いため、それらの単語を近い値のベクトル、つまり近い数値のベクトルで定義することで「似た意味」であることを表現できます。一方、「犬」と「車」では共通点が少ないため、それらの単語を全く異なる数値のベクトルとして定義し、「異なる意味」であることを示すことができるという感じです。

### ベクトルとして定義するメリット

似ているか似ていないかを数値で表現するなら、`猫=1.0、犬=0.9、車=0.1` みたいに単一の数値で定義すればいいんじゃないか？と思うかもしれません。
でも、単語の意味というのは複雑なものです。「ある観点や文脈では似ているけれど、別の観点や文脈では似ていない」ということもあります。
そのため、単一の数値ではなく、多次元のベクトルとして表現することで、さまざまな観点での類似度を同時に表現できるようにしています。（もちろん、これはあくまで簡易的なイメージの説明です）

## 単語ベクトルの具体例

### 単語ベクトルの定義

ふわっとした話で分かりにくいので、具体的に単語ベクトルを使って単語の意味を「計算」する方法を見てみましょう。

たとえば以下のような3つの単語を考えます。

- 猫
- 犬
- 毛布

これらの単語の特徴として、「もふもふ度」「忠実度」「あったかさ」という3つの特徴を考えて、感覚で数字を当てはめてみます。

| 単語  | もふもふ度 | 忠実度 | あったかさ |
|-------|------------|--------|------------|
| 猫    | 1.0         | 0.2      | 0.8          |
| 犬    | 0.3         | 1.0     | 0.8          |
| 毛布  | 0.8          | 0.0      | 1.0         |

これが単語ベクトルのイメージです。
プログラムでこの単語ベクトルを表現すると以下のようになります。

```python
neko = [1.0, 0.2, 0.8]
inu = [0.3, 1.0, 0.8]
moufu = [0.8, 0.0, 1.0]
```


### 類似する概念を調べる例

それぞれの特徴を示す「もふもふ」「忠実」「あったかい」という概念に対しても単語ベクトルを定義してみましょう。
例えば以下のようになるとしましょう。

| 単語  | もふもふ度 | 忠実度 | あったかさ |
|-------|------------|--------|------------|
| もふもふ    | **1.0**         | 0.0      | 0.0          |
| 忠実    | 0.0         | **1.0**     | 0.0          |
| あったかい  | 0.0          | 0.0      | **1.0**         |

ここで、先ほど挙げた猫、犬、毛布という3つの概念の中から最も「忠実」であるものを探したいとします。

その場合、3つの名詞と「忠実」という単語のベクトル(`[0.0, 1.0, 0.0]`)の数値がどれだけ近いのかを計算によって導けばいいです。
単語ベクトルの類似度を計算したい場合は、**コサイン類似度**というもので計算することができます。

コサイン類似度というのは、ベクトル同士の角度の違いを計算する方法です。
これはベクトルの長さ（大きさ）を無視して方向性だけを比較するので、特徴が似てるかを比較するのに使えます。
計算式は以下の通りです。

```
cos(θ) = (A・B) / (|A| * |B|)
```

実際に例に当てはめて計算してみましょう。

```python
import numpy as np

# 3つの単語のベクトルを定義
neko = np.array([1.0, 0.2, 0.8])
inu = np.array([0.3, 1.0, 0.8])
moufu = np.array([0.8, 0.0, 1.0])

# 「忠実」の単語ベクトルを定義
chujitsu = np.array([0.0, 1.0, 0.0])

# コサイン類似度の計算
def similarity(vec_a, vec_b):
    dot_product = np.dot(vec_a, vec_b)
    norm_a = np.linalg.norm(vec_a)
    norm_b = np.linalg.norm(vec_b)
    return dot_product / (norm_a * norm_b)

print(f"chujitsuとnekoの類似度: {similarity(chujitsu, neko):.2f}")    # 約0.15
print(f"chujitsuとinuの類似度: {similarity(chujitsu, inu):.2f}")      # 約0.76 <- 最も高い
print(f"chujitsuとmoufuの類似度: {similarity(chujitsu, moufu):.2f}")  # 0.00
```

- 「猫」と「忠実」の類似度: `0.15`
- 「犬」と「忠実」の類似度: **`0.76`**
- 「毛布」と「忠実」の類似度: `0.00`

このことから、もっとも「忠実」であるのは「犬」であることがわかります。

### 意味を足し引きする例

単語ベクトルはただの数字の集まりなので、当然「足し引き」することもできます。
足し引きをすることで、新しい単語の意味を推測することができます。

めちゃくちゃよくある例ですが、例としてこんな風に各単語を定義します。

```python
king = [0.8, 0.6, 0.7]
man = [0.6, 0.4, 0.5]
woman = [0.5, 0.6, 0.8]
```

「王」から「男性」を引き、「女性」を加える操作をすることができます。

```python
queen = king - man + woman
print(queen) # [0.7, 0.8, 1.0]
```

さて、何になるでしょうか？

変数名でネタバレしてますが、この結果は「女王」のベクトルに近いものになるはずです。

これを応用すると、「フランス」と「パリ」の関係から「日本」に対応する「東京」を導き出したりするようなこともできるかもしれません。

## 実際の単語ベクトル

これまでの例はちょっと抽象化して説明した例ですが、実際の単語ベクトルはちょっと違います。

### 特徴量は人間が決めているわけではない

上記の例では単語の特徴量として使う基準を私が決めました。でも実際の単語ベクトルでは、このように人間が特徴の基準を決めているわけではありません。プログラムによって、自動的にどのような特徴があるかが計算されます。

実際の単語ベクトルは96次元とかの高次元のベクトルですが、それぞれの数字がどのような特徴を示しているかは人間には理解できません。
もちろん単語同士を比較することでそれぞれの数値がどのような特徴を示しているかを「推定」することはできるかもしれません。しかし、あくまで人間が恣意的に決めたものではないので、本当は何を示すのかは誰にもわからないのです。
ちょっと不思議な話ですよね。

### 実は単語単位でもない

単語ベクトルというからには単語ごとにベクトル化していると思いがちですが、実際の自然言語ではマイナーな言い回しや造語、新語、語形変化など様々なイレギュラーがあり、単語単位のベクトルではすべての単語をとうてい網羅できません。

そのため実際の言語モデルでは、単語そのものではなく「トークン」という単位でテキストを処理することが多いです。トークンというのは、単語をさらに細かく分割したサブワードや、場合によっては文字レベルまで分解したものです。このトークン単位でベクトルを計算しています。

::: note
LLMが英語の文章を出力するところをスローで見てみると、例えば`eating`という単語が1度に出力されずに`eat`と`ing`の2段階で出力されるみたいな現象を見ることができます。これは`eating`という単語が2つのサブワードに分割されているという分かりやすい例です。
:::

なので、実際には「単語ベクトル」ではなく「トークンベクトル」や「トークン埋め込み」というのが正確というわけですね。
(単語を実際にどんな単位でトークンに分割するのかについては、これも人間が恣意的に決めるのではなくプログラムが統計的に決めています。)

## 単語ベクトルを計算する方法

では実際にどのように単語ベクトルというのは計算されているのでしょうか？

単語ベクトルを作成するための方法はいくつかありますが、ざっくり言うと「その単語が出てきたときに周りにどのような単語があったか」ということを調べることによって計算されています。

例えば、以下のように3つの単語があったとします。

- 日本一高い山は______である。
- ______はとても高い山である。
- ______は日本一の標高を誇る。

自然言語を解する皆さんは、空欄に入る単語が「富士山」であることがすぐにわかるでしょう。

有名な手法であるWord2Vecでは、CBOW（Continuous Bag-of-Words）モデルを用いて、周囲の単語から中心の単語を予測することで、単語の意味的な関係を捉えます。

先ほどの例で言えば、空欄に入る単語を予測するために、その周辺の単語を使います。

`日本一` `高い` `山` は ______ である。
______ はとても `高い` `山` である。
______ は `日本一` の `標高` を誇る。

`富士山`の周りには、`日本一`とか`高い`とか`山`がたくさん出てくることが分かりますので、他の「高い山」 (例：エベレスト)や他の「日本一高い」もの(例：スカイツリー)と類似した単語ベクトルを持つように調整していきます。
CBOWではターゲットとなる単語の周囲の単語を入力にしてニューラルネットワークによる学習を行うことで、似た文脈で使われがちな単語は似たベクトル表現になるように調整されていきます。(らしいです)

## まとめ

このように定義された単語ベクトルは、さまざまな言語モデルが言語を処理する際の基礎的な考え方となっています。
単語ベクトルのイメージを使うことで、言語モデルを使った処理の理解も深まると思います。