【问题标题】:Given two matrices and a function that takes two vectors, how to vectorize mean of function for every pair of vectors from the matrices?给定两个矩阵和一个带有两个向量的函数,如何向量化矩阵中每对向量的函数均值?
【发布时间】:2020-06-23 12:57:10
【问题描述】:

我正在评估推荐算法(评估它们的排名表现)。这里,true_scores(二进制)矩阵中的一行是用户所有项目的基本值,而predicted_scores(连续)矩阵中的一行是来自某个算法的所有项目的预测分数。 sklearn 具有方法 average_precision_score,它采用两个数组(真实和预测)返回 score。我们需要的是所有用户的这些分数的平均值。 (顺便说一句true_scores & predicted_scores 显然形状相同)

目前,我正在使用for 循环在用户之间平均

import numpy as np
from sklearn.metrics import average_precision_score as aps

def mean_aps(true_scores, predicted_scores):
    '''Mean Average Precision Score'''
    return np.mean([aps(t, p) for t, p in zip(true_scores, predicted_scores) if t.sum() > 0])

我们可以去掉上面代码中的for循环,完全用numpy写吗?我基本上想加快这段代码的速度(可能使用矢量化)。

我知道我们可能需要方法 average_precision_score 的自定义实现。所以我将重新提出这个问题:我需要一个 numpy-aware 实现 any ranking score like NDCG 的平均分数。

【问题讨论】:

  • average_precision_score 确实支持额外的样本维度,即二维数组。
  • @a_guest 用于多标签分类。它不适用于此用例。
  • 试试numba jit
  • @Faboor:当我在此代码上尝试时,似乎不支持此函数/库。我已经在 NDCG 等其他指标上进行了尝试,我遇到了iteration on ND-arrays isn't supported 的问题。当我通过索引尝试时,性能似乎变得更糟而不是提高。
  • 您可能需要自定义实现平均精度,这有点棘手。真实分数和预测分数的格式是什么?矩阵都是二进制的吗?还是它们的格式是 true_scores 是二进制的,并且 predict_scores 包含决策概率?请给出一个输入的小例子,用于测试目的。

标签: python python-3.x numpy scikit-learn vectorization


【解决方案1】:

我对这个问题没有太多的洞察力,直到你提供自己的answer

我假设关于执行正确操作的代码。 我还假设k 通常比分数的数量小得多。 如果是这种情况,您可以对代码进行相当多的优化,因为您不需要像您所做的那样进行排序,您只对在执行 actual 的计算时排序的最大 k 值感兴趣。 要做,您可以使用np.argpartition()。 此外,在调用np.argsort() 之后执行的索引非常麻烦。 更清洁的方法是使用np.take_along_axis()。 以下代码实现了所有这些。

import numpy as np


def dcg_score_opt_np(values, scores, k):
    idx = np.argsort(scores, axis=1)[:, :-k - 1:-1]
    values = np.take_along_axis(values, idx, 1)
    result = np.sum(values / np.log2(2 + np.arange(values.shape[1])), axis=1)
    return result


def ndcg_score_opt_np(y_true, y_score, k):
    best = dcg_score_opt_np(y_true, y_true, k)
    n = y_score.shape[1]
    if n > k:
        idx = np.argpartition(y_score, n - k, axis=1)[:, -k:]
        y_true = np.take_along_axis(y_true, idx, 1)
        y_score = np.take_along_axis(y_score, idx, 1)
    actual = dcg_score_opt_np(y_true, y_score, k)
    return actual / best

另外,best 的计算独立于y_score,如果注意到唯一相关的数量是y_true1s 的数量,则计算效率会更高。 不幸的是,在 NumPy 中无法有效地完成后续计算。 但如果你愿意使用NumBa,你可以写:

import numpy as np
import numba as nb


@nb.jit
def dcg(scores):
    result = 0
    for i, score in enumerate(scores, 2):
        if score > 0.0:
            result += score / np.log2(i)
    return result


@nb.jit
def idcg(n):
    result = 0
    for i in range(2, n + 2):
        result += 1 / np.log2(i)
    return result


@nb.jit
def dcg_score(result, scores, k):
    for i in range(result.shape[0]):
        result[i] = dcg(scores[i, :k])


@nb.jit
def idcg_score(result, counts, k):
    for i in range(result.shape[0]):
        n = counts[i] if counts[i] < k else k
        result[i] = idcg(n)


def idcg_score_opt_nb(values, k):
    counts = np.count_nonzero(values, axis=1)
    result = np.empty(values.shape[0])
    idcg_score(result, counts, k)
    return result


def dcg_score_opt_nb(values, scores, k):
    idx = np.argsort(scores, axis=1)[:, :-k - 1:-1]
    values = np.take_along_axis(values, idx, 1)
    result = np.empty(scores.shape[0])
    dcg_score(result, values, k)
    return result


def ndcg_score_opt_nb(y_true, y_score, k):
    best = idcg_score_opt_nb(y_true, k)
    n = y_score.shape[1]
    if n > k:
        idx = np.argpartition(y_score, n - k, axis=1)[:, -k:]
        y_true = np.take_along_axis(y_true, idx, 1)
        y_score = np.take_along_axis(y_score, idx, 1)
    actual = dcg_score_opt_nb(y_true, y_score, k)
    return actual / best

注意,dcg_score_opt_nb()不是真正必要的,它似乎与dcg_score_opt_np() @ @。 Numba 增强实现的另一个优点是,它们的内存效率更高,因为临时数组的使用保持在最低限度。


一个简单的测试表明我们一直得到相同的结果:

N_USERS = 6
N_ITEMS = 4
k = 3
np.random.seed(0)
y_true = np.random.choice(2, (N_USERS, N_ITEMS))
y_score = np.random.random(size=(N_USERS, N_ITEMS))
print(ndcg_score_2d(y_true, y_score, k))
# [0.61314719 1.         0.76536064 0.63092975 1.         0.38685281]
print(ndcg_score_opt_np(y_true, y_score, k))
# [0.61314719 1.         0.76536064 0.63092975 1.         0.38685281]
print(ndcg_score_opt_nb(y_true, y_score, k))
# [0.61314719 1.         0.76536064 0.63092975 1.         0.38685281]

虽然Apparable DataSet上的基准标记给出:

N_USERS = 3000
N_ITEMS = 2000
k = 100
y_true = np.random.choice(2, (N_USERS, N_ITEMS))
y_score = np.random.random(size=(N_USERS, N_ITEMS))

%timeit ndcg_score_2d(y_true, y_score, k)
# 1 loop, best of 3: 540 ms per loop
%timeit ndcg_score_opt_np(y_true, y_score, k)
# 1 loop, best of 3: 226 ms per loop
%timeit ndcg_score_opt_nb(y_true, y_score, k)
# 10 loops, best of 3: 138 ms per loop

【讨论】:

  • @ 987654340的建议令人敬畏! ndcg_score_opt_np与上面的工作相同,ndcg_score_2d并大约需要一半。谢谢你的精彩想法:)然而, numba i>基于ndcg_score_opt_nb似乎根据我的测试提供错误的结果。 span>
  • @kamalbanga 你能缩小问题的范围吗?如果它在dcg_score_opt_nb(),也许你可以刚使用dcg_score_opt_np(),因为使用numba的主要原因是计算idcg_score()。在任何情况下,您可以提供NumBA代码失败的示例吗? span>
  • 我意识到我犯了一个愚蠢的错误:我说true_scores 是二进制的,这对于average_precision 是必需的,但由于ndcg 不需要,所以我使用的是非二进制true_scores 其中真实分数是用户在该项目上的相关性。也许这是原因,ndcg_score_2dndcg_score_opt_np在@ 987654353 not(我的数据)上对齐。 span>
  • @kamalbanga 确实 idcg_score_opt_nb() 假定二元真实分数。如果不再是这种情况,那么 Numba 版本确实不会提供任何有意义的速度优势,但是通过适当的修复(即使用 dcg_score_opt_nb() 而不是 idcg_score_opt_nb())应该具有更小的内存占用。
【解决方案2】:

编辑:我列出了该问题的三个实现。

首先,完全消除循环是可能的,但是生成的函数avg_prec_noloop() 非常消耗内存,因为它试图一次性完成每个 操作。只要项目的数量在 100 以内,它总是会很快工作。不幸的是,当项目数趋于 1000 或更多时,它会消耗过多的内存,并且会导致崩溃。我包括这个只是为了表明它可以在没有循环的情况下完成,但我不建议使用它。

遵循与原始类似的逻辑,但通过在项目上添加单个循环,我们有函数avg_prec_colwise。我们可以通过一次获取整个阈值列来计算所有用户的精度和召回@K。它与之前的无循环实现有相似的时间,但它并不像内存消耗那么大,并且仍然具有当 items=1000,它会比原来慢一百倍。每当你有大量用户和少量物品的场景时,我建议你使用它。

最后,我有一个实现avg_prec_rowwise,它可能是最接近 sklearn 的实现。当项目较少时,它没有 colwise 或 noloop 函数的惊人增益,但无论项目或用户数量如何,它始终比使用原始快 10-20%。出于一般目的,我建议您使用这个。

import numpy as np
from sklearn.metrics import average_precision_score as aps
from sklearn.metrics import precision_recall_curve as prc
import warnings
warnings.filterwarnings('ignore')

def mean_aps(true_scores, predicted_scores):
    '''Mean Average Precision Score'''
    return np.mean([aps(t, p) for t, p in zip(true_scores, predicted_scores) if t.sum() > 0])

def avg_prec_noloop(yt, yp): 
    valid = yt.sum(axis=1) != 0
    yt, yp = yt[valid], yp[valid] 

    THRESH = np.sort(yp).T
    yp = yp.reshape(1, yp.shape[0], yp.shape[1]) >= THRESH.reshape(THRESH.shape[0], THRESH.shape[1], 1)

    a = (yt*(yt==yp)).sum(axis=2)
    b = yp.sum(axis=2)
    c = yt.sum(axis=1)

    p = (np.where(b==0,0,a/b))
    r = a/c
    rdif = np.vstack((r[:-1]-r[1:],r[-1]))

    return (rdif*p).sum()/yt.shape[0]

def avg_prec_colwise(yt, yp):
    valid = yt.sum(axis=1) != 0
    yt, yp = yt[valid], yp[valid] 
    N_USER, N_ITEM = yt.shape

    THRESH = np.sort(yp)
    p, r = np.zeros((N_USER, N_ITEM)), np.zeros((N_USER, N_ITEM))
    c = yt.sum(axis=1)

    for i in range(N_ITEM):
        ypt = yp >= THRESH[:,i].reshape(-1,1)
        a = (yt*(yt==ypt)).sum(axis=1)
        b = ypt.sum(axis=1)        
        p[:,i] = np.where(b==0,0,a/b).reshape(-1)
        r[:,i] = a/c

    rdif = np.hstack((r[:,:-1]-r[:,1:],r[:,-1].reshape(-1,1)))

    return (rdif*p).sum()/N_USER

def avg_prec_rowwise(yt, yp):
    valid = yt.sum(axis=1) != 0
    yt, yp = yt[valid], yp[valid] 
    N_USER, N_ITEM = yt.shape

    p, r = np.zeros((N_USER, N_ITEM)), np.zeros((N_USER, N_ITEM))
    for i in range(N_USER):
        a, b, _ = prc(yt[i,:], yp[i,:])
        p[i,:len(a)-1] = a[:-1]
        r[i,:len(b)-1] = b[:-1]
    rdif = np.hstack((r[:,:-1]-r[:,1:],r[:,-1].reshape(-1,1)))

    return (rdif*p).sum()/N_USER

一些时间场景:1)项目真的少

N_USERS = 10000
N_ITEMS = 10
a = np.random.choice(2,(N_USERS, N_ITEMS))
b = np.random.random(size=(N_USERS, N_ITEMS))

start = time.time()
for i in range(10):
    mean_aps(a,b)
end = time.time()
print('Original:',end-start)

start = time.time()
for i in range(10):
    avg_prec_colwise(a,b)
end = time.time()
print('Colwise:',end-start)

start = time.time()
for i in range(10):
    avg_prec_rowwise(a,b)
end = time.time()
print('Rowwise:',end-start)

输出:

Original: 47.91176509857178
Colwise: 0.16370844841003418
Rowwise: 37.96852993965149

2) 更多项目:

N_USERS = 3000
N_ITEMS = 100
a = np.random.choice(2,(N_USERS, N_ITEMS))
b = np.random.random(size=(N_USERS, N_ITEMS))

start = time.time()
for i in range(10):
    mean_aps(a,b)
end = time.time()
print('Original:',end-start)

start = time.time()
for i in range(10):
    avg_prec_colwise(a,b)
end = time.time()
print('Colwise:',end-start)

start = time.time()
for i in range(10):
    avg_prec_rowwise(a,b)
end = time.time()
print('Rowwise:',end-start)

输出:

Original: 14.943019151687622
Colwise: 2.0997579097747803
Rowwise: 11.798128604888916

3) 很多项目:

N_USERS = 3000
N_ITEMS = 1000
a = np.random.choice(2,(N_USERS, N_ITEMS))
b = np.random.random(size=(N_USERS, N_ITEMS))

start = time.time()
for i in range(10):
    mean_aps(a,b)
end = time.time()
print('Original:',end-start)

start = time.time()
for i in range(10):
    avg_prec_colwise(a,b)
end = time.time()
print('Colwise:',end-start)

start = time.time()
for i in range(10):
    avg_prec_rowwise(a,b)
end = time.time()
print('Rowwise:',end-start)

输出:

Original: 20.760642051696777
Colwise: 248.5634708404541
Rowwise: 17.940539121627808

4) 原始和逐行之间的最后比较,没有任何循环:

N_USERS = 10000
N_ITEMS = 1000
a = np.random.choice(2,(N_USERS, N_ITEMS))
b = np.random.random(size=(N_USERS, N_ITEMS))

start = time.time()
mean_aps(a,b)
end = time.time()
print('Original:',end-start)

start = time.time()
avg_prec_rowwise(a,b)
end = time.time()
print('Rowwise:',end-start)

输出:

Original: 6.912739515304565
Rowwise: 5.9845476150512695

【讨论】:

  • 哇,你看起来像个麻木的巫师!您的自定义函数确实给出了与mean_aps 相同的结果,但不幸的是它的扩展性很差。对于 3000 个用户和 1000 个项目,它大约是 mean_aps 的 50 倍。对于 10000 个用户,它会杀死我的 jupyter。
  • 是的,你是对的,它的扩展性很差。阈值化后,yp变成1000x3000x1000大小的数组,太大了。
  • @kamalbanga 我在三个不同的实现中进行了编辑。 noloop 函数表明可以在没有任何循环的情况下执行此操作,但它会使您的 jupyter 崩溃。 colwise 不会让你的 jupyter 崩溃,但对项目的扩展性很差;当 items
【解决方案3】:

我能够为 NDCG 分数(线性相关性)做到这一点。代码扩展为类似于to the code here 的二维版本。

import numpy as np

def dcg_score_2d(y_true, y_score, k=100):
    order = np.argsort(y_score)[:, ::-1]
    y_true = y_true[np.arange(y_true.shape[0])[:,np.newaxis], order[:, :k]]
    discounts = np.log2(np.arange(y_true.shape[-1]) + 2)
    return np.sum(y_true / discounts, axis=1)

def ndcg_score_2d(y_true, y_score, k=100):
    best = dcg_score_2d(y_true, y_true, k=k)
    actual = dcg_score_2d(y_true, y_score, k=k)
    return actual / best

虽然它并没有像我预期的那样在for 循环中给我带来太多的性能提升。

【讨论】:

    猜你喜欢
    • 2011-06-20
    • 1970-01-01
    • 1970-01-01
    • 2018-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-23
    • 2020-07-28
    相关资源
    最近更新 更多