【永久保存版】複数のベクトル群同士のコサイン類似度を一気に算出する方法

# はじめに
この記事では，コサイン類似度の概要から，複数のベクトル群同士のコサイン類似度を一気に算出する方法をpythonコードを用いて解説します．


# コサイン類似度とは
コサイン類似度を一言で表すと「**2つのベクトルがどの程度似ているかを表す尺度**」です．この尺度は，**2つのベクトルの内積を2つのベクトルの大きさ（L2ノルム）で割る**ことによって計算できます．
コサイン類似度は，**-1～1の範囲に正規化**され，その値によって以下のように解釈が異なります．

>* **1**なら「2つのベクトルの成す角度が**0度** → **同じ向き**のベクトル → **完全に似ている**」
>* **0**なら「2つのベクトルの成す角度が**90度** → **独立・直行**したベクトル → **似ている/似ていないのどちらにも無関係**」
>* **-1**なら「2つのベクトルの成す角度が**180度** → **反対向き**のベクトル → **完全に似ていない**」

**2つのベクトルの大きさにかかわらず，2つのベクトルの向きが近いほど類似度が高くなる**ということがポイントです．つまり，コサイン類似度は，**2つのベクトル間の数値的な大きさを考慮しない場合**に使用されます．


# 数式
2つのベクトル同士のコサイン類似度の数式は，以下の「**2つのベクトル同士の内積**」の公式から導けます．

```math
\vec{a}・\vec{b} = |\vec{a}| \hspace{2pt} |\vec{b}| \cos\theta \\
 = ||a|| \hspace{2pt} ||b|| \cos(a,b) 
```

なお，|<ベクトル>|は「ベクトルの大きさ」を表し，||<ベクトル>||はベクトル空間における「長さ・距離」を表現する概念であるノルム（norm）を意味し，この公式におけるノルムはユークリッド距離であるL2ノルムを表します．

上記の公式を変形すると以下になります．これがコサイン類似度の数式となり，a = (a1, a2, …, an)，b = (b1, b2, …, bn)のようなn次元に拡張したベクトルのコサイン類似度を算出できます．
```math
\begin{align}
cos(a,b)&= \frac{a・b}{||a|| \hspace{2pt} ||b||}\\
&= \frac{\sum_{i=1}^{n}a_ib_i}{\sqrt{\sum_{i=1}^{n}a_i^2}\sqrt{\sum_{i=1}^{n}b_i^2}}
\end{align}
```

上記の公式をPythonコードで具体例と共に表すと以下になります．

```python:2つのベクトル同士のコサイン類似度
import numpy as np

# データ作成
a = [1, 1, 0]
b = [1, 0, 1]

# コサイン類似度計算
cos = np.dot(a, b)/(np.sqrt(np.dot(a, a))*np.sqrt(np.dot(b, b)))

print(cos)
```
実行結果は，0.49（小数第3位以下切り捨て）となります．

# 複数のベクトル群同士のコサイン類似度
そして，ここからが本題です．
以下のような「おいしさ」「甘さ」「辛さ」という3つの列を持つ「3行×3列」「4行×3列」の食品群行列ベクトル（dataframe）があるとします．

|  | おいしさ | 甘さ | 辛さ |
|---|--:|--:|--:|
| 食品1 |  5|  2|  3|
| 食品2 |  4|  5|  2|
| 食品3 |  1|  2|  5|

|  | おいしさ | 甘さ | 辛さ |
|---|--:|--:|--:|
| 食品4 |  5|  4|  3|
| 食品5 |  3|  5|  2|
| 食品6 |  2|  5|  3|
| 食品7 |  3|  1|  1|

食品群行列ベクトルを作成するためのコードが以下になります．
```python:食品群行列ベクトルを作成
import pandas as pd

df1 = pd.DataFrame({'おいしさ': [5, 4, 1],
                   '甘さ': [2, 5, 2],
                   '辛さ': [3, 2, 5]},
                  index=['食品1', '食品2', '食品3'])

df2 = pd.DataFrame({'おいしさ': [5, 3, 2, 3],
                   '甘さ': [4, 4, 5, 1],
                   '辛さ': [3, 2, 3, 1]},
                  index=['食品4', '食品5', '食品6', '食品7'])
```

今から，この2つの食品群同士のコサイン類似度について一気に算出してみます．算出手順は以下です．
1. 行列の片方を転置（行と列の入れ替え）して積を求める
2. 手順1を2つの行列ベクトルと行列ノルムに適用
3. 手順2の計算結果からコサイン類似度を算出


```python:食品群行列の片方を転置して積を計算
# 行列ベクトルの片方を転置して積を求める
df_dot = df1.dot(df2.T)

# 行列ノルムを求める
df1_norm = pd.DataFrame(np.linalg.norm(df1.values, axis=1), index = df1.index)
df2_norm = pd.DataFrame(np.linalg.norm(df2.values, axis=1), index = df2.index)
# 行列ノルムの片方を転置して積を求める
df_norm = df1_norm.dot(df2_norm.T)

# コサイン類似度を算出
df_cos = df_dot/df_norm

print(df_cos)
```
上記のコードを実行すれば，以下のようにコサイン類似度を算出できます．（小数第3位以下切り捨て）

|  | 食品4 | 食品5 | 食品6 | 食品7 |
|---|--:|--:|--:|--:|
| 食品1 | 0.96| 0.87| 0.76| 0.97|
| 食品2 | 0.96| 0.99| 0.94| 0.85|
| 食品3 | 0.72| 0.71| 0.79| 0.55|

食品2と食品5（0.99）が似ていて，食品3と食品7（0.55）が似ていないことが分かりましたね！

# おまけ
先ほどの結果を用いて，コサイン類似度を高い順にソートするためのコードが以下です．

```python:コサイン類似度が高い順にソート
# コサイン類似度をリストに格納
cos_li = [['{0}×{1}'.format(index, col), value] for index, row in df_cos.iterrows() for col,value in row.iteritems()]

# コサイン類似度の高い順にソート
cos_sort_li = sorted(cos_li, key=lambda x: x[1], reverse=True)

# dataframeに変形
df_cos_sort = pd.DataFrame(cos_sort_li, columns=['ベクトル', 'コサイン類似度'])

print(df_cos_sort)
```
実行結果は以下です．（小数第3位以下を切り捨て）

| ベクトル | コサイン類似度 | 
|---|--:|
| 食品2×食品5 | 0.99 |
| 食品1×食品7 | 0.97 |
| 食品2×食品4 | 0.96 |
| 食品1×食品4 | 0.96 |
| 食品2×食品6 | 0.94 |
| 食品1×食品5 | 0.87 |
| 食品2×食品7 | 0.85 |
| 食品3×食品6 | 0.79 |
| 食品1×食品6 | 0.76 |
| 食品3×食品4 | 0.72 |
| 食品3×食品5 | 0.71 |
| 食品3×食品7 | 0.55 |


コサイン類似度の結果がかなり見やすくなりましたね！

# 最後に
最後までご拝読いただきありがとうございます！少しでもコサイン類似度についての理解が深まったなら幸いです．