
距離って本当に二人を離れ離れにするかな?
-リチャード・バック
もし愛する人と一緒にいたいって思ったとしたら
もうその時点で二人の間に距離なんて無いよね?
こんにちは。エンジョイワークス、システム開発部 MLエンジニアのしゅんです。
今日は、レコメンドシステムや、機械学習アルゴリズムにも使われるデータ間の距離を測定する方法、ユークリッド距離とコサイン類似度を比べたいと思います。また、それぞれを、どのようなケースに適用したら良いか、サンプルデータを使って解説します。
ユークリッド距離とコサイン類似度は、ベクトル空間での距離を測る方法です。早速、測定方法の違いを見ていきましょう。わかりやすい具体例として、動物の全長と体重を、特徴データXとして、年齢別に分離したいとします。カテゴリーのラベルは(young = 0, mid = 1, adult = 2)とします。
データを準備する
以下、ランダムなデータを作ります。
import numpy as np
X = np.array([[6.6, 6.2, 1],
[9.7, 9.9, 2],
[8.0, 8.3, 2],
[6.3, 5.4, 1],
[1.3, 2.7, 0],
[2.3, 3.1, 0],
[6.6, 6.0, 1],
[6.5, 6.4, 1],
[6.3, 5.8, 1],
[9.5, 9.9, 2],
[8.9, 8.9, 2],
[8.7, 9.5, 2],
[2.5, 3.8, 0],
[2.0, 3.1, 0],
[1.3, 1.3, 0]])
まずXをDataFrameにセットし、正解ラベルと紐付けます。
import pandas as pd
df = pd.DataFrame(X, columns=['weight', 'length', 'label'])
df
weight length label
0 6.6 6.2 1.0
1 9.7 9.9 2.0
2 8.0 8.3 2.0
3 6.3 5.4 1.0
4 1.3 2.7 0.0
5 2.3 3.1 0.0
6 6.6 6.0 1.0
7 6.5 6.4 1.0
8 6.3 5.8 1.0
9 9.5 9.9 2.0
10 8.9 8.9 2.0
11 8.7 9.5 2.0
12 2.5 3.8 0.0
13 2.0 3.1 0.0
14 1.3 1.3 0.0
データをplotしてみると、3つのグループに分類できますね。
%matplotlib inline
ax = df[df['label'] == 0].plot.scatter(x='weight', y='length', c='blue', label='young')
ax = df[df['label'] == 1].plot.scatter(x='weight', y='length', c='orange', label='mid', ax=ax)
ax = df[df['label'] == 2].plot.scatter(x='weight', y='length', c='red', label='adult', ax=ax)
ax

表を見ると、3つのクラスは、2つの特徴量で上手に分離できました。例えばKNN(k-近傍法)で未知のデータを分類したいとします。アルゴリズムは正解クラスを見つけるために、学習データと何らかの方法で、距離の測定が必要です。ここでユークリッド距離とコサイン類似度が登場します。
測定方法を決める。
わかりやすいように、plot するデータを#0 ,#1,#4の3つだけに絞ります。分類したい未知のデータを#14 をとします。
df2 = pd.DataFrame([df.iloc[0], df.iloc[1], df.iloc[4]], columns=['weight', 'length', 'label'])
df3 = pd.DataFrame([df.iloc[14]], columns=['weight', 'length', 'label'])
ax = df2[df2['label'] == 0].plot.scatter(x='weight', y='length', c='blue', label='young')
ax = df2[df2['label'] == 1].plot.scatter(x='weight', y='length', c='orange', label='mid', ax=ax)
ax = df2[df2['label'] == 2].plot.scatter(x='weight', y='length', c='red', label='adult', ax=ax)
ax = df3.plot.scatter(x='weight', y='length', c='gray', label='?', ax=ax)
ax

ユークリッド距離
ユークリッド距離の方程式は以下になります。

xとyはベクトルの成分ですね。
def euclidean_distance(x, y):
return np.sqrt(np.sum((x - y) ** 2))
全てのベクトルを計算してみましょう。
x0 = X[0][:-1]
x1 = X[1][:-1]
x4 = X[4][:-1]
x14 = X[14][:-1]
print(" x0:", x0, "\n x1:", x1, "\n x4:", x4, "\nx14:", x14)
x0: [ 6.6 6.2]
x1: [ 9.7 9.9]
x4: [ 1.3 2.7]
x14: [ 1.3 1.3]
計算すると
print(" x14 and x0:", euclidean_distance(x14, x0), "\n",
"x14 and x1:", euclidean_distance(x14, x1), "\n",
"x14 and x4:", euclidean_distance(x14, x4))
x14 and x0: 7.21803297305
x14 and x1: 12.0216471417
x14 and x4: 1.4
結果は、ユークリッド距離の場合、#14のデータに近いデータは#4となります。(数字が小さいほど、距離が近い。)
正解ラベルと確認すると「0 」で「young:若い」ですね。正しく分類されています。
X[4]
array([ 1.3, 2.7, 0. ])
一方て、コサイン類似度だと、どうなるでしょうか?
コサイン類似度
コサイン類似度の方程式は以下になります

xとyはベクトルの成分ですね。
def cosine_similarity(x, y):
return np.dot(x, y) / (np.sqrt(np.dot(x, x)) * np.sqrt(np.dot(y, y)))
測定すると、今度は#14に近いのは#1となりました。(数字が大きほど近い。1だと同じベクトル。)
print(" x14 and x0:", cosine_similarity(x14, x0), "\n",
"x14 and x1:", cosine_similarity(x14, x1), "\n",
"x14 and x4:", cosine_similarity(x14, x4))
x14 and x0: 0.999512076087
x14 and x1: 0.999947942424
x14 and x4: 0.943858356366
X[1]
array([ 9.7, 9.9, 2. ])
正解ラベルを見ると「2」で「adult:大人」ですね。これは想定とは違いますね。
結局何が起きたのか?

ユークリッド距離とコサイン類似度を、図表にしてみましょう。コサイン類似度(θ)は、ベクトル間の角度を測定するのに対して(x,y方向の大きさは考慮せず。)ユークリッド距離(d)は定規で計ったように、ベクトル間の距離を算出しています。上記の具体例だと、x14とx4の距離は近いにも関わらず、角度は他のベクトルより大きかったのです。
コサイン類似度はどんな時に使うのか?
コサイン類似度は、一般的にベクトル間の数値的な大きさを考慮しない場合に使用する測定方法です。
典型的な使用方法は、テキストデータを扱う場合です。ベクトルの大きさや重みを無視して、より単語の類似度を重視したい場合に用います。
例えば、文章Aは「科学」という言葉が文書Bより、頻繁に登場するとします。その場合、文章Aは文章Bより「科学」について言及しているとわかります。しかしながら、比較する文章の長さが揃ってない場合、より長い文章のほうが、たまたま単語が登場する確率が増えてしまいます。
例えば、文章Aの方が長く、「科学」とは関係ないにも関わらず、たまたま「科学」という言葉が多く登場したとします。一方で、短い文章Bが実際に科学の文章だとしたら。このよう場合に、コサイン類似度を使って評価します。
ユークリッド距離とコサイン類似度の関係
ユークリッド距離とコサイン類似度を、具体例で比べてみましょう。
print("vectors \t", x0, x1, "\n"
"euclidean \t", euclidean_distance(x0, x1), "\n"
"cosine \t\t", cosine_similarity(x0, x1))
vectors [ 6.6 6.2] [ 9.7 9.9]
euclidean 4.82700735446
cosine 0.99914133854
コサイン類似度は、単位ベクトルの内積を取っています。ユークリッド距離で同じことをしたら、どうなるのでしょうか?(つまり、単位ベクトルで計算する。)
単位ベクトルとは、ベクトルは方向を維持しつつ、ベクトルの大きさを「1」にします。つまり大きさを揃えます。
L1,L2ノルムを求めて、単位ベクトルを算出してみます。
def l1_normalize(v):
norm = np.sum(v)
return v / norm
def l2_normalize(v):
norm = np.sqrt(np.sum(np.square(v)))
return v / norm
x0_n = l1_normalize(x0)
x1_n = l1_normalize(x1)
print(x0_n, x1_n)
[ 0.515625 0.484375] [ 0.49489796 0.50510204]
上記を足し合わせると1になります。
単位ベクトルを、求めなかった場合と比べてみましょう。
print("vectors \t", x0_n, x1_n, "\n"
"euclidean \t", euclidean_distance(x0_n, x1_n), "\n"
"cosine \t\t", cosine_similarity(x0_n, x1_n))
vectors [ 0.515625 0.484375] [ 0.49489796 0.50510204]
euclidean 0.0293124622303
cosine 0.99914133854
正規化する前は距離が開いており、かつ類似度も近いですね。ベクトルを正規化したあとを見ると、距離が縮まっています。同じパターンがベクトル#4でも起こります
print("vectors \t", x0, x4, "\n"
"euclidean \t", euclidean_distance(x0, x4), "\n"
"cosine \t\t", cosine_similarity(x0, x4))
vectors [ 6.6 6.2] [ 1.3 2.7]
euclidean 6.35137780328
cosine 0.933079411589
↑正規化なし
x4_n = l1_norm(x4)
print("vectors \t", x0_n, x4_n, "\n"
"euclidean \t", euclidean_distance(x0_n, x4_n), "\n"
"cosine \t\t", cosine_similarity(x0_n, x4_n))
vectors [ 0.515625 0.484375] [ 0.325 0.675]
euclidean 0.269584460327
cosine 0.933079411589
↑正規化
x0,x1はx0,x4と比べるとコサイン類似度は同じですが、ユークリッド距離は大きいです。
以上を考慮して、新たなベクトルを作ってみましょう。そこそこ距離があり、今度は角度は大きくとります。
x00 = np.array([0.1, 6])
print("vectors \t", x0, x00, "\n"
"euclidean \t", euclidean_distance(x0, x00), "\n"
"cosine \t\t", cosine_similarity(x0, x00))
vectors [ 6.6 6.2] [ 0.1 6. ]
euclidean 6.50307619516
cosine 0.696726168728
正規化すると、今までと同じパターンですね。距離は縮むが、角度は影響を受けません。
x00_n = l1_normalize(x00)
print("vectors \t", x0_n, x00_n, "\n"
"euclidean \t", euclidean_distance(x0_n, x00_n), "\n"
"cosine \t\t", cosine_similarity(x0_n, x00_n))
vectors [ 0.515625 0.484375] [ 0.01639344 0.98360656]
euclidean 0.706020039207
cosine 0.696726168728
つまり、何がわかるかというと、コサイン類似度は、ベクトルの大きさに影響を受けないということです。文章の場合、単語の総量にかからわず、内容を評価できます。
コサイン類似度 イン アクション
さて、コサイン類似度を使うと何が嬉しいのか?Wikipediaの記事で、遊んでみましょう。Wikipedia Apiを使って文書にアクセスしてみます。(本記事では英語のデータを使います。)
import wikipedia
q1 = wikipedia.page('Machine Learning')
q2 = wikipedia.page('Artifical Intelligence')
q3 = wikipedia.page('Soccer')
q4 = wikipedia.page('Tennis')
単語の出現頻度をベクトル化します。各インスタンスが、文章となり、単語が特徴量となります。ここでの、特徴量は文章での単語の出現頻度となります。
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer()
X = np.array(cv.fit_transform([q1.content, q2.content, q3.content, q4.content]).todense())
例えば、AIや機械学習の文章では「ball」の出現回数は0となるはずです。一方で、サッカーやテニスの記事では、高頻度で出現するはずです。
scikit-learnのCountVectorizerは、スペースで文章を単語に分割します。各文章の単語の総数を見てみましょう。
print("ML \t", len(q1.content.split()), "\n"
"AI \t", len(q2.content.split()), "\n"
"soccer \t", len(q3.content.split()), "\n"
"tennis \t", len(q4.content.split()))
ML 3694
AI 10844
soccer 6134
tennis 9715
AIの記事は、機械学習の記事と比べて、ずっと単語が多いですね。単語数が多いAIは、正規化しないと機械学習との距離が離れたものになります。機械学習は、単語数が同じぐらいの記事と近くなるはずです。試してみましょう。
print("ML - AI \t", euclidean_distance(X[0], X[1]), "\n"
"ML - soccer \t", euclidean_distance(X[0], X[2]), "\n"
"ML - tennis \t", euclidean_distance(X[0], X[3]))
ML - AI 661.102110116
ML - soccer 459.307086817
ML - tennis 805.405487938
事前に想定した通りですね。ML(機械学習)はサッカーの記事と近くなってしまいました。。次にコサイン類似度で比べてみましょうか。(ベクトルを正規化)
print("ML - AI \t", cosine_similarity(X[0], X[1]), "\n"
"ML - soccer \t", cosine_similarity(X[0], X[2]), "\n"
"ML - tennis \t", cosine_similarity(X[0], X[3]))
ML - AI 0.886729067818
ML - soccer 0.785735757699
ML - tennis 0.797450973312
今度はAIと近くなりましたね!ただ、スコアを見るかぎりまだサッカーとテニスに近いですね。考慮していただきたいのは、言葉の出現頻度は、必ずしも文章の文脈までを、厳密に評価していないということです。
ツイートをカテゴライズする。
ツイートをカテゴライズして遊んでみましょう。
ml_tweet = "New research release: overcoming many of Reinforcement Learning's limitations with Evolution Strategies."
x = np.array(cv.transform([ml_tweet]).todense())[0]
ここでもツイートを単語ベクトルとします。ツイートが、先ほどのWikipedia記事のどの分野にカテゴライズされるか見てみましょう。
print("tweet - ML \t", euclidean_distance(x[0], X[0]), "\n"
"tweet - AI \t", euclidean_distance(x[0], X[1]), "\n"
"tweet - soccer \t", euclidean_distance(x[0], X[2]), "\n"
"tweet - tennis \t", euclidean_distance(x[0], X[3]))
tweet - ML 342.575539115
tweet - AI 945.624661269
tweet - soccer 676.677914521
tweet - tennis 1051.61589946
機械学習にカテゴライズされましたね。しかし、よく見ると、AIよりもサッカーに近い??次元が多いので、厳密になぜそうなったか検証するのは難しいですが、サッカー記事が2番目に小さい文章であることが関係してそうです。コサイン類似度で評価してみましょう。
print("tweet - ML \t", cosine_similarity(x, X[0]), "\n"
"tweet - AI \t", cosine_similarity(x, X[1]), "\n"
"tweet - soccer \t", cosine_similarity(x, X[2]), "\n"
"tweet - tennis \t", cosine_similarity(x, X[3]))
tweet - ML 0.299293065515
tweet - AI 0.215356854916
tweet - soccer 0.135323358719
tweet - tennis 0.129773245953
近くなりましたね!直感に近い結果です。ユークリッド距離が、より文章の大きさに相関することが見て取れます。同じことサッカーのツイートで試してみましょう。
so_tweet = "#LegendsDownUnder The Reds are out for the warm up at the @nibStadium. Not long now until kick-off in Perth."
x2 = np.array(cv.transform([so_tweet]).todense())[0]
違うツイートで、同じことをします。
print("tweet - ML \t", euclidean_distance(x2, X[0]), "\n"
"tweet - AI \t", euclidean_distance(x2, X[1]), "\n"
"tweet - soccer \t", euclidean_distance(x2, X[2]), "\n"
"tweet - tennis \t", euclidean_distance(x2, X[3]))
tweet - ML 340.549555865
tweet - AI 943.455351355
tweet - soccer 673.818224746
tweet - tennis 1048.82124311
メチャクチャな結果ですね。コサイン類似度で測定しますよう。
print("tweet - ML \t", cosine_similarity(x2, X[0]), "\n"
"tweet - AI \t", cosine_similarity(x2, X[1]), "\n"
"tweet - soccer \t", cosine_similarity(x2, X[2]), "\n"
"tweet - tennis \t", cosine_similarity(x2, X[3]))
tweet - ML 0.437509648194
tweet - AI 0.464447992614
tweet - soccer 0.611865382744
tweet - tennis 0.597261139457
完璧ですね!
ベクトルを正規化したことによって、結果に違いが出るということが理解できたかと思います。
エンジョイワークスでは、バックエンドエンジニアを募集しております。空き家問題を、自分のスキルで解決したいエンジニアは、是非ご応募ください!
リクルート情報はこちら!
https://enjoyworks.jp/recruit