はじめに|レポート
私が毎月開催している桜井進のPython・UNIX教室【数学プログラミングコース】3月と桜井進の数学浪漫紀行5月のテーマが「コサイン類似度」でした。
前回はPythonのNumPyライブラリによるベクトルを扱う方法を紹介しました。今回はベクトルの応用としてコサイン類似度を紹介します。
目次
2つのベクトルのなす角
2つのベクトルに対して、その角を考えることができます。2つのベクトルのなす角をθとすると、その取り得る範囲は0°≦θ≦180°です。
余弦cosθを2つのベクトルの成分を用いて次の図のように求めることができます。よってθはアークコサイン(逆余弦関数)を用いて得られます。
この計算をPythonでも行ってみます。
>>> import numpy as np
>>> x = np.array([2, -1, 3, -2])
>>> y = np.array([-3, 2, 1, 5])
>>> theta = np.dot(x, y)/(np.linalg.norm(x) * np.linalg.norm(y))
>>> print(np.degrees(np.arccos(theta)))
>>> 124.48138937198158
たしかに同じ結果です。この計算に登場するnp.dot(x, y)がベクトルの内積です。
ベクトルの内積とコサイン類似度
ベクトルxとyの内積をx・yのように表します。次の図のように内積x・yは2通りで表すことができます。
その1はベクトルのノルム(大きさ)となす角を用いた式、その2はベクトルの成分による式です。
これら2つの内積の式およびベクトルのノルムの式よりcosθが成分だけで表されます。
このcosθがコサイン類似度の正体です。
データをベクトル化することで、2つのデータの比較をベクトルのなす角によって行えるようになります。
θが0°(cos0°=1)の場合すなわち2つのベクトルが平行の場合は、2つのデータは“完全に似ている”とし、θが180°(cos180°=-1)の場合すなわち2つのベクトルが真反対の場合は、2つのデータは“全く似ていない”とする解釈です。
実際にはθではなくcosθで比べます。
cos0°が最大値1である場合が、データは“完全に似ている”、cos0°が最小値-1である場合が、データは“全く似ていない”と対応します。
つまり類似度はcosθの大小に一致します。これをコサイン類似度(Cosine Similarity)と呼び、cos(x,y)と表したりします。
コサイン類似度の実例
6人それぞれに10の音楽ジャンルごとに好き嫌いのアンケートをとり、そのデータをもとに音楽の趣味が合う2人と逆に趣味が合わない2人を探し出してみます。
好きは○、嫌いは●、どちらでもないは空欄とします。6人の結果を数値化します。
○は1、●は-1、そして空間は0として、まず一人一人リスト化します。コサイン類似度を計算するために、このリストをベクトル化(1次元配列)します。例えば、A(1佐藤さん)の場合には、A = [1,1,-1,0,1,-1,0,-1,1,-1]とします。
プログラム「cossim.py」コサイン類似度の計算プログラム
>>> import numpy as np
>>>
>>> # 好き嫌いデータ
>>> A = [1,1,-1,0,1,-1,0,-1,1,-1]
>>> B = [-1,1,-1,-1,1,1,1,-1,1,1]
>>> C = [1,0,0,0,0,0,-1,1,0,0]
>>> D = [0,1,1,1,0,-1,0,1,0,1]
>>> E = [-1,-1,-1,-1,-1,-1,1,-1,-1,-1]
>>> F = [1,1,1,0,1,1,1,1,-1,1]
>>> people = [A, B, C, D, E, F]
>>> name = ["1佐藤さん", "2鈴木さん", "3武田さん", "4加藤さん", "5田中さん", "6岡田さん"]
>>>
>>> # コサイン類似度の関数定義
>>> def cossim(list1, list2):
>>> vec1 = np.array(list1)
>>> vec2 = np.array(list2)
>>> if (np.linalg.norm(vec1) != 0 and np.linalg.norm(vec2) != 0):
>>> cos_theta = np.dot(vec1, vec2)/(np.linalg.norm(vec1)*np.linalg.norm(vec2))
>>> else:
>>> cos_theta = 0
>>> return cos_theta
>>>
>>> people_ijcos_list = [] # people[i]とpeople[j]:コサイン類似度 cos_theta のリスト
>>> cos_theta_list = [] # cos_theta のリスト
>>> n = len(people) # 人数
>>>
>>> for i in range(n-1):
>>> for j in range(i+1, n):
>>> cos_theta = cossim(people[i], people[j]) # コサイン類似度cos_theta
>>> pair_ijcos = name[i]+"と"+name[j]+":コサイン類似度 "+str(round(cos_theta, 4))
>>> people_ijcos_list.append(pair_ijcos)
>>> cos_theta_list.append(cos_theta)
>>> print(pair_ijcos)
>>>
>>> maxpair = cos_theta_list.index(max(cos_theta_list)) # コサイン類似度最大値のペアのインデックス
>>> minpair = cos_theta_list.index(min(cos_theta_list)) # コサイン類似度最小値のペアのインデックス
>>> print("\n音楽の趣味が最も類似する二人 ", people_ijcos_list[maxpair])
>>> print("音楽の趣味が最も類似しない二人", people_ijcos_list[minpair])
このプログラムファイルは次からダウンロードできます。
https://drive.google.com/file/d/1KIs09O3TfoUcmNioFsZ_F19ZbXjKj1CH/view?usp=sharing
プログラム実行結果
>>> 1佐藤さんと2鈴木さん:コサイン類似度 0.2236
>>> 1佐藤さんと3武田さん:コサイン類似度 0.0
>>> 1佐藤さんと4加藤さん:コサイン類似度 -0.1443
>>> 1佐藤さんと5田中さん:コサイン類似度 0.0
>>> 1佐藤さんと6岡田さん:コサイン類似度 -0.2357
>>> 2鈴木さんと3武田さん:コサイン類似度 -0.5477
>>> 2鈴木さんと4加藤さん:コサイン類似度 -0.2582
>>> 2鈴木さんと5田中さん:コサイン類似度 0.0
>>> 2鈴木さんと6岡田さん:コサイン類似度 0.1054
>>> 3武田さんと4加藤さん:コサイン類似度 0.2357
>>> 3武田さんと5田中さん:コサイン類似度 -0.5477
>>> 3武田さんと6岡田さん:コサイン類似度 0.1925
>>> 4加藤さんと5田中さん:コサイン類似度 -0.5164
>>> 4加藤さんと6岡田さん:コサイン類似度 0.4082
>>> 5田中さんと6岡田さん:コサイン類似度 -0.527
>>>
>>> 音楽の趣味が最も類似する二人 4加藤さんと6岡田さん:コサイン類似度 0.4082
>>> 音楽の趣味が最も類似しない二人 2鈴木さんと3武田さん:コサイン類似度 -0.5477
6人から2人選ぶすべての組合せにおいて、コサイン類似度を計算します。そして、最大値と最小値を検索します。
結果は、4加藤さんと6岡田さんの相性が最も良く、2鈴木さんと3武田さんの相性が最も良くないとなりました。
それぞれの組合せを比べてみると、それなりに納得できる結果のようにも思えます。冒頭の「# 好き嫌いデータ部分」をいじってみてコサイン類似度がどのように変化するのかをためしてみましょう。