今回はコサイン類似度の算出方法をまとめておきたいと思います。
コサイン類似度とは2つのベクトルがどれくらい似ているか表す指標になります。
コサイン類似度とは2つのベクトルが「どのくらい似ているか」という類似性を表す尺度で、具体的には2つのベクトルがなす角のコサイン値のこと。1なら「似ている」を、-1なら「似ていない」を意味する。主に文書同士の類似性を評価するために使われている。
引用: https://atmarkit.itmedia.co.jp/ait/articles/2112/08/news020.html
詳細はITMedia社のコサイン類似度(Cosine Similarity)とは?という記事に説明があります。
基本的にベクトルを作成してコサイン類似度を計算するだけになります。
簡単なリコメンドを作成したいときにコサイン類似度のスコア順に表示するだけでも十分ビジネスに適用可能ではないかと思います。
ちなみに私が実際に利用した時はコールドスタート問題の解決方法として、まだ未販売の商品がどれくらいの売上になりそうか予測するためコサイン類似度で似た属性をもつ商品を探索し分析の補足情報として活用したことがあります。
それでは、ボストンの住宅価格のデータセットを例にやってみたいと思います。(タイトルである商品は住宅と置き換えてください 笑)
データセットの読み込み
# データの読み込み
import pandas as pd
df = pd.read_csv("http://lib.stat.cmu.edu/datasets/boston_corrected.txt",skiprows=9,sep="\t",encoding='Windows-1252')
# infoメソッドで概要を把握
df.info()
RangeIndex: 506 entries, 0 to 505 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 OBS. 506 non-null int64 1 TOWN 506 non-null object 2 TOWN# 506 non-null int64 3 TRACT 506 non-null int64 4 LON 506 non-null float64 5 LAT 506 non-null float64 6 MEDV 506 non-null float64 7 CMEDV 506 non-null float64 8 CRIM 506 non-null float64 9 ZN 506 non-null float64 10 INDUS 506 non-null float64 11 CHAS 506 non-null int64 12 NOX 506 non-null float64 13 RM 506 non-null float64 14 AGE 506 non-null float64 15 DIS 506 non-null float64 16 RAD 506 non-null int64 17 TAX 506 non-null int64 18 PTRATIO 506 non-null float64 19 B 506 non-null float64 20 LSTAT 506 non-null float64 dtypes: float64(14), int64(6), object(1) memory usage: 83.1+ KB
# 上から5件を表示
df.head()
OBS. TOWN TOWN# TRACT LON LAT MEDV CMEDV CRIM ZN ... CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT 0 1 Nahant 0 2011 -70.955 42.2550 24.0 24.0 0.00632 18.0 ... 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98 1 2 Swampscott 1 2021 -70.950 42.2875 21.6 21.6 0.02731 0.0 ... 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14 2 3 Swampscott 1 2022 -70.936 42.2830 34.7 34.7 0.02729 0.0 ... 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03 3 4 Marblehead 2 2031 -70.928 42.2930 33.4 33.4 0.03237 0.0 ... 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94 4 5 Marblehead 2 2032 -70.922 42.2980 36.2 36.2 0.06905 0.0 ... 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33 5 rows × 21 columns
(オプション) デバッグしやすいようにキーを作成。(数値+名称とかだと分かりやすい)
# キーを作成
df["KEY"] = df["OBS."].astype(str) + ":" + + df["TRACT"].astype(str) + ":" + df["TOWN"]
# 最初の1件目を見てみる
df["KEY"].iloc[0]
'1:2011:Nahant'
コサイン類似度の計算
sklearnのcosine_similarityを使って計算します。
# ライブラリのインポート
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from sklearn import preprocessing
# 部屋数、住宅価格、一人当たりの犯罪数 で類似度を計算させる
anacols=[
'RM'
,'CMEDV'
,'CRIM'
]
# 標準化 (そのままだと大きい距離に引っ張られてしまうのでやる)
ss = preprocessing.StandardScaler()
cos_sim_df = ss.fit_transform(df[anacols].fillna(0))
# 行列形式でコサイン類似度を算出 (内部的に正規化してくれてるっぽいので標準化しなくてもいいのかも?)
cos_similarity_matrix = pd.DataFrame(cosine_similarity(cos_sim_df))
# コサイン類似度
cos_similarity_matrix
0 1 2 3 4 5 6 7 8 9 ... 496 497 498 499 500 501 502 503 504 505 0 1.000000 0.831274 0.794872 0.795805 0.762981 0.737662 0.052109 0.358176 -0.440196 -0.143702 ... -0.496160 -0.354028 -0.043342 -0.456986 -0.171561 0.957843 0.136849 0.918292 0.906766 -0.091469 1 0.831274 1.000000 0.324073 0.330033 0.279203 0.379009 0.343320 0.689690 0.089463 0.398219 ... -0.085274 0.168528 0.394906 0.042103 0.402635 0.911625 0.606606 0.685997 0.806016 0.447163 2 0.794872 0.324073 1.000000 0.997586 0.996947 0.860640 -0.255047 -0.120216 -0.832800 -0.666185 ... -0.727768 -0.770294 -0.481194 -0.806270 -0.727181 0.628431 -0.412253 0.800792 0.648947 -0.631034 3 0.795805 0.330033 0.997586 1.000000 0.998548 0.893724 -0.192112 -0.071169 -0.800265 -0.634587 ... -0.678795 -0.733532 -0.430616 -0.767701 -0.711886 0.617587 -0.373439 0.774831 0.622369 -0.599954 4 0.762981 0.279203 0.996947 0.998548 1.000000 0.884390 -0.221409 -0.116234 -0.822584 -0.671200 ... -0.691029 -0.759575 -0.465575 -0.787463 -0.748318 0.578471 -0.417971 0.752668 0.590936 -0.638649 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 501 0.957843 0.911625 0.628431 0.617587 0.578471 0.514325 -0.004914 0.367919 -0.324555 -0.010782 ... -0.479815 -0.250842 -0.005722 -0.372179 0.021113 1.000000 0.227409 0.924298 0.969679 0.043216 502 0.136849 0.606606 -0.412253 -0.373439 -0.417971 -0.039529 0.856839 0.944545 0.828483 0.953918 ... 0.735924 0.877936 0.959815 0.815317 0.886690 0.227409 1.000000 -0.161454 0.025850 0.965643 503 0.918292 0.685997 0.800792 0.774831 0.752668 0.540474 -0.338681 0.003960 -0.654453 -0.385620 ... -0.774099 -0.598835 -0.381249 -0.697093 -0.327998 0.924298 -0.161454 1.000000 0.971938 -0.335484 504 0.906766 0.806016 0.648947 0.622369 0.590936 0.412433 -0.247381 0.139516 -0.473212 -0.180345 ... -0.656927 -0.417827 -0.225447 -0.533547 -0.101018 0.969679 0.025850 0.971938 1.000000 -0.128108 505 -0.091469 0.447163 -0.631034 -0.599954 -0.638649 -0.292493 0.776408 0.829889 0.930855 0.998541 ... 0.806897 0.954552 0.938542 0.907043 0.968738 0.043216 0.965643 -0.335484 -0.128108 1.000000 506 rows × 506 columns
行列だと確認するのが大変なので、各indexごとに最も類似度が高いindexを探索してみます。
コサイン類似度の算出結果から各index毎に最も類似度が高いindexを探索する
# 最終アウトプット格納用
maxindex_list=[]
maxvalue_list=[]
# 行列のデータから各index毎に最も類似度が高いindexを探索する
for j in range(0,len(cos_similarity_matrix)):
print("---- index",j,"と類似するindexを探索 (昇順で表示)")
# 初期設定
max_index=-2
max_val=-2
for i in range(0,len(cos_similarity_matrix)):
# 対角線上はスキップ
if i == j:
continue
# 類似度が1を超える場合はスキップ
elif np.abs(cos_similarity_matrix[i][j]) >= 1.0:
continue
# 重複する名称である場合はスキップ
elif df["KEY"].iloc[i] == df["KEY"].iloc[j]:
print(df["KEY"].iloc[i])
continue
else:
# 類似度が大きいindexとvalueを更新
if cos_similarity_matrix[i][j] >= max_val:
max_val = cos_similarity_matrix[i][j]
max_index = i
print("index =",max_index,"->",max_val)
# 対象index/valueと一番類似度が高いindex/valueを格納
if i == (len(cos_similarity_matrix)-1):
maxindex_list.append(max_index)
maxvalue_list.append(max_val)
else:
pass
---- index 0 と類似するindexを探索 (昇順で表示) index = 1 -> 0.8312739440395608 index = 41 -> 0.9425180497277477 index = 52 -> 0.9725328096870871 index = 81 -> 0.9967538297456999 index = 314 -> 0.9990954456804517 index = 351 -> 0.9997019884255598 ---- index 1 と類似するindexを探索 (昇順で表示) index = 0 -> 0.8312739440395608 index = 62 -> 0.9847946373605043 index = 91 -> 0.9950713979704345 ・・・省略・・・ ---- index 505 と類似するindexを探索 (昇順で表示) index = 0 -> -0.09146897154913142 index = 1 -> 0.4471633696478723 index = 6 -> 0.7764083643742422 index = 7 -> 0.829889262174972 index = 8 -> 0.9308547923887059 index = 9 -> 0.9985405186741478 index = 11 -> 0.9991814496178155 index = 117 -> 0.9992918539565818
# 最終結果出力 (各indexと最も類似度が高いindex番号を表示)
pd.DataFrame(zip(maxindex_list, maxvalue_list))
0 1 0 351 0.999702 1 91 0.995071 2 189 0.999955 3 303 0.999995 4 192 0.999934 ... ... ... 500 47 0.999795 501 301 0.997360 502 51 0.999805 503 88 0.999306 504 111 0.991154 505 rows × 2 columns
index0はindex351、index1はindex91と部屋数、住宅価格、一人当たりの犯罪数が類似しているようです。
# index 0とindex 351の属性情報を確認
df.iloc[[0,351]].transpose()
0 351 OBS. 1 352 TOWN Nahant Marshfield TOWN# 0 71 TRACT 2011 5061 LON -70.955 -70.83 LAT 42.255 42.0775 MEDV 24.0 24.1 CMEDV 24.0 24.1 CRIM 0.00632 0.0795 ZN 18.0 60.0 INDUS 2.31 1.69 CHAS 0 0 NOX 0.538 0.411 RM 6.575 6.579 AGE 65.2 35.9 DIS 4.09 10.7103 RAD 1 4 TAX 296 411 PTRATIO 15.3 18.3 B 396.9 370.78 LSTAT 4.98 5.49 KEY 1:2011:Nahant 352:5061:Marshfield
部屋数と住宅価格がとても似ているようです。CRIMはindex351番の方が数値が高いですね。
同じ地域ではないのに似ている属性をもつレコードを抽出することができました。これはこのままレコメンドシステムに流用できそうですね。