Re:ゼロから始めるML生活

どちらかといえばエミリア派です

コサイン類似度に基づいてANNする際に正規化は必要か?

(雑記です)

表題の件について、ちょっと気になって夜も眠れなくなってしまったのでブログに書いてみます。

最初に結論

個人的理解です、違ったら教えて下さい。

  • いらない
    • 事前にベクトル自体をL2正規化などしてダメなわけではない
    • コサイン類似度の計算以外の部分に影響はあるかもしれないから不安だったらしたら良いと思う

数学的に考える

とりあえず普通に考えます。 コサイン類似度は下記のような式で表されます。

\displaystyle{
cos( \vec{x}, \vec{y} ) = \frac{\vec{x} \cdot \vec{y}}{| \vec{x}| |\vec{y}|}
}

この式の中で、

\displaystyle{
\vec{x}
}, \displaystyle{
\vec{y}
}を何倍してようが、分子分母でキャンセルするので値はおなじになるはずですね。

\displaystyle{
cos( \vec{x}, \vec{y} ) = \frac{n  \vec{x} \cdot m  \vec{y}}{| n \vec{x}| |m \vec{y}|}
}

高校数学みたいですね。

「いやいや、やってみないとわからないじゃん」

ということもありそうですね。百聞は一見にしかず、やってみましょう。

nmslibをつかってやってみます。Colabだとなぜかインストールできなかったので、ローカルで普通にスクリプト書いてやってみました。

import numpy as np
import nmslib

index = nmslib.init(space='cosinesimil')

vs = [
    [1.0, 2.0],
    [2.0, 3.0],
    [4.0, 6.0],
    [6.0, 9.0],
    [2.0, 2.0],
    [3.0, 3.0],
]

vs = np.array(vs, dtype=np.float32)
print(vs)

# インデックス生成
index.addDataPointBatch(vs)
index.createIndex({}, print_progress=False)

# 検索
v = np.array([1.0, 1.0], dtype=np.float32)
ids, dists = index.knnQuery(v, 10)

print('ids: %s' % (ids))
print('dists: %s' % (dists))

# 長さだけ変えて検索し直す
v = np.array([2.0, 2.0], dtype=np.float32)
ids, dists = index.knnQuery(v, 10)

print('ids: %s' % (ids))
print('dists: %s' % (dists))

こんな感じになりました。

root@baa5bc429f36:/home# python3 hello.py 
[[1. 2.]
 [2. 3.]
 [4. 6.]
 [6. 9.]
 [2. 2.]
 [3. 3.]]
ids: [5 4 1 2 3 0]
dists: [0.0000000e+00 5.9604645e-08 1.9419312e-02 1.9419312e-02 1.9419372e-02
 5.1316738e-02]
ids: [5 4 1 2 3 0]
dists: [0.0000000e+00 5.9604645e-08 1.9419312e-02 1.9419312e-02 1.9419372e-02
 5.1316738e-02]
root@baa5bc429f36:/home# 

distsってのがコサイン類似度(nmslibを使っているので厳密には違う)を表してます。*1

わかることとしては、

  • 1個目の検索のスコアからわかること
    • 方向が同じベクトルは距離が0になってる
      • (2,2), (3,3)、0にはなっていないが-8乗オーダーなので一致と言って問題ないだろう
      • (2,3), (4,6), (6,9)も値は一致してないが小さい桁でのズレなので丸め誤差とかそのへんだろう
  • 1個目と2個目を比較してわかること
    • クエリのベクトルを変えても検索結果は同じ

って感じですかね。 使っているベクトルの長さを変えようが(すごい小さい桁で誤差は出てるが)値はおおよそ同じ結果として計算されていますね。

「いやいや、他のライブラリだったら違うかもしれないじゃん」

まあそうですね、違うやつも試してみますか。 というわけでvoygerでやってみます。

先ほどとほぼほぼ同じ結果ですね。 別にベクトルの長さが変わってようが揃ってようが、検索結果には(たいして)影響ないことが確認できました。

(微妙にスコアが変わっていたとしても、実際に近傍探索しようと思ったら、差が付く場合はもっと大きな差がつくし、−8乗の誤差なら一致とみなして問題ないだろうと個人的には思います)

ちなみに検算

試しにANNで計算されているコサイン類似度がどうなのか確認してみます。

import numpy as np

# 2次元ベクトル
vec1 = np.array([1, 1])
vec2 = np.array([2, 3])

# コサイン類似度 = cos(θ) = (A・B) / (||A|| * ||B||)
def cosine_similarity(a, b):
    dot_product = np.dot(a, b)
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    return dot_product / (norm_a * norm_b)

similarity = cosine_similarity(vec1, vec2)
print(f"コサイン類似度: {1-similarity}")

これをやるとコサイン類似度: 0.01941932430908と出てきたので、コサイン類似度としては正しく計算されてそうですね。 (ANNの中では1-similarityとして計算されているんですね)

感想

まあ当たり前っちゃ当たり前なんで、とくになし。