静かなる名辞

pythonとプログラミングのこと


コサイン距離は距離じゃないんだから、勘違いしないでよねっ!

き、記事タイトルに意味なんてないんだからねっ!

 自然言語処理などでお馴染みのコサイン類似度。これを1から引いたものを「コサイン距離」と称している文献も散見されますが、この「コサイン距離」は距離としての性質を満たしません。

 それがどういうことなのかをこの記事で説明していきます。

コサイン類似度のことくらい自分で調べなさいっ!

 まず前提となるコサイン類似度については、親切に解説しているサイトが他にたくさんあるので、そちらに譲ります。

 たとえばここなどがいいでしょう。

コサイン類似度

 コサイン類似度はベクトル同士の類似度であり、要するに単なる内積(をノルムで正規化したもの)です。これは-1から1の区間を取ります。1なら「最も似ている(同じベクトル)」、-1なら「最も似ていない(反対向き)」という性質を持ちます。

 これを1から引くことで、0なら「最も似ている」、2なら「最も似ていない」に変換したものが「コサイン距離」です。

距離の定義を知らないの? しょ、しょうがないから教えてあげるわ

 さて、距離という言葉というか概念は実は数学的にちゃんと定義できます。かいつまんで書くと、関数 dが以下の条件(距離の公理といいます)を満たすとき、その関数を距離関数あるいは距離と言えます。

\begin{align}
d(x,y) &>& 0\\
x&=&y\Leftrightarrow d(x, y) = 0\\
d(x, y) &=& d(y, x)\\
d(x, z) &\leq& d(x, y) + d(y, z)
\end{align}

参考
距離空間 - Wikipedia
第6回 距離の公理:ねむねこ幻想郷:So-netブログ
距離とは (キョリとは) [単語記事] - ニコニコ大百科

 数式で見ると難しく見えるかもしれませんが、この式はそれぞれ

  • 距離は負にはならない(非負性)
  • 同じ点同士の距離は0、距離が0の点は同じ点
  • x,yの間の距離について、距離を測る起点を逆にしても距離は変わらない(対称性)
  • x,zとまっすぐ行くときと比べて、yに寄り道すると必ずトータルの経路は長くなる(三角不等式)

 ということを言っているだけなので、概念的には簡単です。

 こういうものを満たすと距離と呼べる、ということですね。

 「コサイン距離」はどれを満たさないのでしょうか?

わからないの? ……ばか

 「コサイン距離」は2番目の x=y\Leftrightarrow d(x, y) = 0と、4番目の三角不等式を満たしません。

 2番目を説明するのは簡単で、元のコサイン類似度はベクトル間の角度にしか興味を持たない性質があります。なので、たとえば二次元ベクトル (1,0) (2,0)とか、 (42,1) (84,2)は同じ距離になります。

 4番目については、反例を挙げてみましょう。

すごく単純な例
すごく単純な例

 特に凝ったことはしていません。この図において、単純なユークリッド距離を考えると、

  • A-B間, B-C間の距離:1
  • A-C間の距離: \sqrt{2}\simeq 1.414

 となり、こういうのが三角不等式を満たしている場合です。A-B-Cとたどる経路の長さは2になるので、A-Cとたどるより長い距離をたどることになります。

 では、「コサイン距離」では? というと、

  • A-B間, B-C間の「コサイン距離」:約0.293
  • A-C間の距離:1

 となり、A-B-Cとたどることで約0.586になりますからA-Cと直接たどるより短い距離で行けてしまうことになります。つまり、三角不等式を満たさないので、「コサイン距離」は距離ではないということになります。

距離として扱うと困るのかって? ……困るに決まってるじゃないっ、わからずや!

 データ分析などで、距離を使うことを前提としている手法で「コサイン距離」を使うと、不都合なことが起きる可能性があります。

 みんなが大好きなirisのデータを多次元尺度構成法、MDSで可視化してみましょう。Pythonで書くとこうなります。

import matplotlib.pyplot as plt
from scipy.spatial.distance import pdist, squareform
from sklearn.datasets import load_iris
from sklearn.manifold import MDS

def main():
    iris = load_iris()

    A = squareform(pdist(iris.data, "euclidean"))
    mds = MDS(n_components=2, dissimilarity="precomputed",
              n_init=10, max_iter=500)
    X_2d = mds.fit_transform(A)

    for i, target in enumerate(iris.target_names):
        mask = iris.target == i
        plt.scatter(X_2d[mask,0], X_2d[mask,1], label=target)

    plt.xlim(X_2d[:,0].min() - 1, X_2d[:,0].max() + 1)
    plt.ylim(X_2d[:,1].min() - 1, X_2d[:,1].max() + 1)
    plt.title("MDS stress:{:.4f}".format(mds.stress_))
    plt.legend()
    plt.savefig("iris_euclidean_mds.png")
   
if __name__ == "__main__":
    main()

iris_euclidean_mds.png
iris_euclidean_mds.png

 そんなに申し分はなさそうな結果ですね。

 「コサイン距離」でもやってみます。といっても、親切なことにscipyが「コサイン距離」を標準でサポートしているので、

    A = squareform(pdist(iris.data, "cosine"))

 とすれば一発でできます。あとはせいぜい出力ファイル名を変えておきます。
(plt.savefig("iris_cosine_mds.png")としました。)

scipy.spatial.distance.pdist — SciPy v1.3.0 Reference Guide

iris_cosine_mds.png
iris_cosine_mds.png

 なんかよくわからないことになりました。念のために中心付近にズームしてみます(plt.xlimとplt.ylimで調整)。

iris_cosine_mds_zoomed.png
iris_cosine_mds_zoomed.png

 考えてみれば当然の結果で、コサイン類似度は-1から1のレンジを取ります。ということは、「コサイン距離」の最大値は2にしかならないのです。なので、遠い点が表現できなくなり、とても小さい範囲に押し込められます。

 また、「コサイン距離」では向きが同じで長さの違うベクトル同士を区別できません。昔作ったirisの主成分分析のバイプロットを持ってくると、

irisのバイプロット
irisのバイプロット

【python】pythonで主成分分析のバイプロット - 静かなる名辞

 グループ間の差異は概ね第一主成分に、グループ内での差異は第二主成分にあらわれています。そして、第一主成分とほぼ同じ方向を向いている2つの変数、そうでもない2つの変数があることがわかります。

 品種が違うと各変数の相対的な比率が変わる反面、同じ品種同士では各変数の相対的な比率はさほど変わらない(全体的に大きかったり小さかったりという個体差があるだけ)と想定すれば、結果が一直線上に並ぶのもなんとなく理解できる気がします。

「じゃあどう呼べば良いのか」って? そんなの自分で考えてよね!

 「コサイン距離」に変わる呼称方法ですが……

 ま、常識的に考えると、コサイン非類似度でいいのではないでしょうか。

わかったなら感謝しなさい。……え、ありがとう? べ、べつに喜ばれても嬉しくなかんないんだからっ!

 安易に「コサイン距離」という言葉を使ってはいけないこと、また、距離として扱うと問題になるというか、イマイチな結果を招く可能性があることがこの記事でわかっていただけたら、嬉しいです。

 あと、ツンデレ風の章タイトルにしたことに対して今更ながら後悔の念を感じ始めているのですが(自分で見返してもかなり痛い)、下書きに放り込んで一晩寝たらたぶん投稿する勇気がなくなっていると思うので、蛮勇を奮ってこのまま後悔公開することにします。