在 python 中创建 NxN similarity/distance 矩阵的有效方法
Efficient method to create NxN similarity/distance matrix in python
我需要在 python 中创建一个 NxN 相似度矩阵,其中 N = 943。
我最初使用 cosine_similarity 的 sklearns 实现,但现在我需要使用更复杂和非标准的距离度量。
下午好,
我有一个用户电影数据框(table 中的 NaN 表示用户没有对这些电影进行评分)
| movie_id | 1 | 2 | 3 | 4 | 5 |
|----------|---|---|---|---|---|
| user_id | | | | | |
| 1 | 1 | 1 | NaN | 4 | 5 |
| 2 | NaN | 1 | 1 | 5 | 5 |
| 3 | 4 | NaN | 4 | 1 | 2 |
我需要将 3 个单独的函数应用于用户电影数据框:接近度、影响力和流行度。
2 个用户之间的最终相似性由接近度、影响力和流行度的乘积给出。
现在棘手的部分是我只需要为每个用户的“共同评级”项目应用上述 3 个函数。所以比如在计算user1和user2的相似度时,我们应该只考虑movie_ids 2, 4 and 5.
现在我将准确定义这 3 个函数应该做什么。
- 首先我定义了一个名为“agreement”的辅助方法
给定来自 2 个用户的 2 个评分,此函数 returns 为真当且仅当两个评分都在中位数的同一侧。在我们的例子中,中位数是 2.5。否则为假。
def agreement(rating1: int, rating2: int) -> bool:
if ((rating1 > 2.5 and rating2 < 2.5) or (rating1 < 2.5 and rating2 > 2.5)):
return False
else:
True
- 距离
给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将简单地计算绝对差异。如果评分不一致,则会受到处罚。
def proximity(rating1: int, rating2: int) -> float:
if(agreement(rating1, rating2)):
dist = np.absolute(rating1 - rating2)
else:
dist = 2 * np.absolute(rating1 - rating2)
prox = ((2*(rating_max - rating_min) + 1) - dist) ** 2
return prox
- 影响
给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将计算 impact_score。如果 2 个评分不一致,则 returns1/impact_score
def impact(rating1: int, rating2: int) -> float:
impact_score = (np.absolute(rating1 - rating_median) + 1) * (np.absolute(rating2 - rating_median) + 1)
if(agreement(rating1, rating2)):
return impact_score
else:
return 1/impact_score
- 人气。
给定来自 2 个用户的 2 个评分和给定 movie_id(mu_k) 的平均评分,此方法计算 pop_score 当且仅当这 2 个评分都大于(或小于)平均评分给定电影的。
def popularity(rating1: int, rating2: int, mu_k) -> float:
pop = 1
if((rating1 > mu_k and rating2 > mu_k) or (rating1 < mu_k and rating2 < mu_k)):
pop = 1 + ((rating1 + rating2)/2 - mu_k)**2
return pop
最终的相似度矩阵应该是这样的:
# 0 1 2
#0 1.000000 60.972245 12.761905
#1 60.972245 1.000000 9.790476
#2 12.761905 9.790476 1.000000
问题是我目前的实施速度非常慢。我大约需要 1.5 小时来计算 N=943 的矩阵。
我目前遍历 NxN 矩阵的每个单元格并单独应用所有 3 个函数(当前实现代码:https://pastebin.com/zfcyBhJz)。
所以我想知道在给定要使用的 3 个函数的情况下,是否有更快更有效的方法来生成所需的相似度矩阵?
使用numpyp.ma.MaskedArray
,同时充分发挥广播的作用,可以获得非常好的性能。
先获取df的values
:
import numpy as np
from numpy import nan
ratings = np.array([[1., 1., nan, 4., 5.],
[nan, 1., 1., 5., 5.],
[4., nan, 4., 1., 2.]])
# ratings = df_ratings.values
转换为MaskedArray
:
from numpy.ma import masked_invalid
ratings = masked_invalid(ratings)
# masked_array(
# data=[[1.0, 1.0, --, 4.0, 5.0],
# [--, 1.0, 1.0, 5.0, 5.0],
# [4.0, --, 4.0, 1.0, 2.0]],
# mask=[[False, False, True, False, False],
# [ True, False, False, False, False],
# [False, True, False, False, False]],
# fill_value=1e+20)
计算每对用户之间所有评分的agrement
的负数:
temp = ratings - 2.5
not_agreements = temp[:, None] * temp[None] < 0
# Equivalent to
# from numpy.ma import masked_array
# not_argeements = masked_array([masked_array([(i - 2.5) * (j - 2.5) < 0 for j in ratings]) for i in ratings])
同理计算所有proximity
、impact
和popularity
,这里我假设rating_max
、rating_min
和rating_median
都是标量:
dist = np.abs(ratings[:, None] - ratings[None])
dist[not_agreements] *= 2
prox = ((2 * (rating_max - rating_min) + 1) - dist) ** 2
temp = np.abs(ratings - rating_median) + 1
impact_score = temp[:, None] * temp[None]
impact_score[not_agreements] = 1 / impact_score[not_agreements]
mu_k = ratings.mean(0)
temp = ratings - mu_k
shape = ratings.shape
pop = np.ones(shape[:1] + shape)
mask = temp[:, None] * temp[None] > 0
pop[mask] += ((temp[:, None] + temp[None]) / 2)[mask] ** 2
将它们相乘并沿最后一个轴求和,然后将对角线上的值设置为1,最后得到你想要的结果:
similarity_matrix = (prox * impact_score * pop).sum(-1)
similarity_matrix[np.diag_indices_from(similarity_matrix)] = 1
similarity_matrix_df = pd.DataFrame(similarity_matrix, index=df_ratings.index, columns=df_ratings.index)
经测试,你的遍历方法的运行时间和你例子中我的方法差不多,但是随着数组的扩大,你的方法的运行时间增加了很多快速地。当数组的形状达到(48, 50)时,最多需要10s,而我的向量化方法只需要0.06s。
我需要在 python 中创建一个 NxN 相似度矩阵,其中 N = 943。 我最初使用 cosine_similarity 的 sklearns 实现,但现在我需要使用更复杂和非标准的距离度量。
下午好, 我有一个用户电影数据框(table 中的 NaN 表示用户没有对这些电影进行评分)
| movie_id | 1 | 2 | 3 | 4 | 5 |
|----------|---|---|---|---|---|
| user_id | | | | | |
| 1 | 1 | 1 | NaN | 4 | 5 |
| 2 | NaN | 1 | 1 | 5 | 5 |
| 3 | 4 | NaN | 4 | 1 | 2 |
我需要将 3 个单独的函数应用于用户电影数据框:接近度、影响力和流行度。
2 个用户之间的最终相似性由接近度、影响力和流行度的乘积给出。
现在棘手的部分是我只需要为每个用户的“共同评级”项目应用上述 3 个函数。所以比如在计算user1和user2的相似度时,我们应该只考虑movie_ids 2, 4 and 5.
现在我将准确定义这 3 个函数应该做什么。
- 首先我定义了一个名为“agreement”的辅助方法
给定来自 2 个用户的 2 个评分,此函数 returns 为真当且仅当两个评分都在中位数的同一侧。在我们的例子中,中位数是 2.5。否则为假。
def agreement(rating1: int, rating2: int) -> bool:
if ((rating1 > 2.5 and rating2 < 2.5) or (rating1 < 2.5 and rating2 > 2.5)):
return False
else:
True
- 距离
给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将简单地计算绝对差异。如果评分不一致,则会受到处罚。
def proximity(rating1: int, rating2: int) -> float:
if(agreement(rating1, rating2)):
dist = np.absolute(rating1 - rating2)
else:
dist = 2 * np.absolute(rating1 - rating2)
prox = ((2*(rating_max - rating_min) + 1) - dist) ** 2
return prox
- 影响
给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将计算 impact_score。如果 2 个评分不一致,则 returns1/impact_score
def impact(rating1: int, rating2: int) -> float:
impact_score = (np.absolute(rating1 - rating_median) + 1) * (np.absolute(rating2 - rating_median) + 1)
if(agreement(rating1, rating2)):
return impact_score
else:
return 1/impact_score
- 人气。
给定来自 2 个用户的 2 个评分和给定 movie_id(mu_k) 的平均评分,此方法计算 pop_score 当且仅当这 2 个评分都大于(或小于)平均评分给定电影的。
def popularity(rating1: int, rating2: int, mu_k) -> float:
pop = 1
if((rating1 > mu_k and rating2 > mu_k) or (rating1 < mu_k and rating2 < mu_k)):
pop = 1 + ((rating1 + rating2)/2 - mu_k)**2
return pop
最终的相似度矩阵应该是这样的:
# 0 1 2
#0 1.000000 60.972245 12.761905
#1 60.972245 1.000000 9.790476
#2 12.761905 9.790476 1.000000
问题是我目前的实施速度非常慢。我大约需要 1.5 小时来计算 N=943 的矩阵。
我目前遍历 NxN 矩阵的每个单元格并单独应用所有 3 个函数(当前实现代码:https://pastebin.com/zfcyBhJz)。
所以我想知道在给定要使用的 3 个函数的情况下,是否有更快更有效的方法来生成所需的相似度矩阵?
使用numpyp.ma.MaskedArray
,同时充分发挥广播的作用,可以获得非常好的性能。
先获取df的values
:
import numpy as np
from numpy import nan
ratings = np.array([[1., 1., nan, 4., 5.],
[nan, 1., 1., 5., 5.],
[4., nan, 4., 1., 2.]])
# ratings = df_ratings.values
转换为MaskedArray
:
from numpy.ma import masked_invalid
ratings = masked_invalid(ratings)
# masked_array(
# data=[[1.0, 1.0, --, 4.0, 5.0],
# [--, 1.0, 1.0, 5.0, 5.0],
# [4.0, --, 4.0, 1.0, 2.0]],
# mask=[[False, False, True, False, False],
# [ True, False, False, False, False],
# [False, True, False, False, False]],
# fill_value=1e+20)
计算每对用户之间所有评分的agrement
的负数:
temp = ratings - 2.5
not_agreements = temp[:, None] * temp[None] < 0
# Equivalent to
# from numpy.ma import masked_array
# not_argeements = masked_array([masked_array([(i - 2.5) * (j - 2.5) < 0 for j in ratings]) for i in ratings])
同理计算所有proximity
、impact
和popularity
,这里我假设rating_max
、rating_min
和rating_median
都是标量:
dist = np.abs(ratings[:, None] - ratings[None])
dist[not_agreements] *= 2
prox = ((2 * (rating_max - rating_min) + 1) - dist) ** 2
temp = np.abs(ratings - rating_median) + 1
impact_score = temp[:, None] * temp[None]
impact_score[not_agreements] = 1 / impact_score[not_agreements]
mu_k = ratings.mean(0)
temp = ratings - mu_k
shape = ratings.shape
pop = np.ones(shape[:1] + shape)
mask = temp[:, None] * temp[None] > 0
pop[mask] += ((temp[:, None] + temp[None]) / 2)[mask] ** 2
将它们相乘并沿最后一个轴求和,然后将对角线上的值设置为1,最后得到你想要的结果:
similarity_matrix = (prox * impact_score * pop).sum(-1)
similarity_matrix[np.diag_indices_from(similarity_matrix)] = 1
similarity_matrix_df = pd.DataFrame(similarity_matrix, index=df_ratings.index, columns=df_ratings.index)
经测试,你的遍历方法的运行时间和你例子中我的方法差不多,但是随着数组的扩大,你的方法的运行时间增加了很多快速地。当数组的形状达到(48, 50)时,最多需要10s,而我的向量化方法只需要0.06s。