余弦相似度精度问题引起的偏差

发布时间 2023-05-17 17:23:02作者: 三叶草body

余弦相似度精度问题引起的偏差

余弦相似度值不等于1(实际是等于1)

两个向量\(a\)\(b\)是相同的,余弦相似度值应该是1,但是通过sklearnnumpy计算的结果却不等于1,会出现大于1或者小于1的情况,实际上余弦值应该是在[-1, 1]这个区间内的。

使用sklearn.metrics.pairwise.cosine_similarity方法计算余弦相似度, 存在的问题如下:

import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity


# 情况一 向量方向相同, 值相同
a = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
b = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
similarity = np.diag(cosine_similarity(pd.DataFrame([a]).values, pd.DataFrame([b]).values))[0]
print(similarity)  # 1.0000000000000002

# 情况二 向量方向相同, 值不相同
a = np.array([1, 1])
b = np.array([2, 2])
similarity = np.diag(cosine_similarity(pd.DataFrame([a]).values, pd.DataFrame([b]).values))[0]
print(similarity)  # 0.9999999999999998

# 情况三 向量方向不相同
a = np.array([1, 1])
b = np.array([2, 2])
similarity = np.diag(cosine_similarity(pd.DataFrame([a]).values, pd.DataFrame([b]).values))[0]
print(similarity)  # 0.9999999999999998

使用numpy库计算余弦相似度,也会出现类似情况:

import numpy as np


def cosine_similarity1(a, b):
    """
    计算向量a和向量b之间的cosine similarity
    """
    # 计算分子
    numerator = np.dot(a, b)

    # 计算分母
    denominator = np.linalg.norm(a) * np.linalg.norm(b)

    # 计算cosine similarity
    similarity = numerator / denominator

    return similarity


# 情况一 向量方向相同, 值相同
a = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
b = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity1(a, b)
print(similarity1)  # 1.0000000000000002

# 情况二 向量方向相同, 值不相同
a = np.array([1, 1])
b = np.array([2, 2])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity1(a, b)
print(similarity1)  # 0.9999999999999998

# 情况三  向量方向不同
a = np.array([1, 3])
b = np.array([2, 5])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity1(a, b)
print(similarity1)  # 0.9982743731749958

以上案例,可以看出,在计算两个向量方向相同时,会存在值不等于1的两种情况;计算方向不同的向量,余弦值是有误差的

问题分析

首先从余弦相似度计算公式上分析,公式如下:

img

发现,分母上进行了开根号,因为分母会存在开不尽的情况或者能开尽但是因为精度问题导致了不准确,即使使用双(double)精度也依然存在这个问题。

计算机对于开根计算时,会存在精度丢失,即使提高精度,只是减轻了丢失误差,这种精度误差是无法避免的。

举例说明:\(\sqrt{5} = 2.23606797749979\) \(\sqrt{3} = 1.7320508075688772\) 开根无法开尽,就会出现精度丢失的现象。

问题解决

方式一:首先计算余弦相似度时,分母不进行开根,那么分子就要进行平方处理,这样计算的结果就是余弦相似度的平方,之后再进行开根号处理。

# -*- coding: utf-8 -*-
import numpy as np


def cosine_similarity2(a, b):
    """
    优化方法
    计算向量a和向量b之间的cosine similarity
    """
    similarity = np.sqrt(np.dot(a, b) ** 2 / (np.dot(a, a) * np.dot(b, b)))

    return similarity


# 情况一 向量方向相同, 值相同
a = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
b = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity2(a, b)
print(similarity1)  # 1.0
 
# 情况二 向量方向相同, 值不相同
a = np.array([1, 1])
b = np.array([2, 2])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity2(a, b)
print(similarity1)  # 1.0

# 情况三  向量方向不同
a = np.array([1, 3])
b = np.array([2, 5])
# 利用numpy库提供的函数,自定义封装计算余弦相似度的函数
similarity1 = cosine_similarity2(a, b)
print(similarity1)  # 0.9982743731749959

方法二: 使用scipy.spatial.distance.cosine

# -*- coding: utf-8 -*-
from scipy.spatial.distance import cosine
import pandas as pd


# 情况一 向量方向相同, 值相同
a = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
b = np.array([90.000, 0.000, 0.000, 0.000, 0.000, 0.000, 870.000, 0.000])
similarity3 = 1 - cosine(a, b)
print(similarity3)  # 1

# 情况二 向量方向相同, 值不相同
a = np.array([1, 1])
b = np.array([2, 2])
similarity3 = 1 - cosine(a, b)
print(similarity3)  # 1

# 情况三  向量方向不同
a = np.array([1, 3])
b = np.array([2, 5])
similarity3 = 1 - cosine(a, b)
print(similarity3)  # 0.9982743731749958

通过上面的案例分析,使用第二种方法可以解决余弦相似度计算因为精度问题导致的误差问题,但是计算出来的值精度丢失的问题。

推荐使用scipy.spatial.distance.cosine来计算余弦相似度值。