Учитывая две матрицы и функцию, которая принимает два вектора, как векторизовать среднее значение функции для каждой пары векторов из матриц? - PullRequest
1 голос
/ 11 марта 2020

Я работаю над оценкой рекомендательных алгоритмов (по их ранжированию). Здесь строка в матрице true_scores (двоичная) представляет собой базовые значения для всех элементов пользователя, а строка в матрице predicted_scores (непрерывная) - это прогнозируемые оценки для всех элементов из некоторого алгоритма. sklearn имеет метод average_precision_score, который принимает два массива (true и прогнозируемый), возвращая оценка . Что нужно, так это среднее значение этих баллов для всех пользователей. (BTW true_scores & predicted_scores, очевидно, имеют одинаковую форму)

В настоящее время я использую for l oop для усреднение по пользователям

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 l oop из приведенного выше кода и полностью записать его в numpy? Я в основном хочу ускорить этот код (возможно, с использованием векторизации).

Я понимаю, что нам может понадобиться пользовательская реализация метода average_precision_score. Поэтому я перефразирую вопрос: мне нужна numpy -осознанная реализация среднего значения для любого рейтинга, например NDCG .

Ответы [ 3 ]

2 голосов
/ 02 апреля 2020

Редактировать: Я перечислил три реализации для этой проблемы.

Во-первых, можно полностью исключить циклы, но результирующая функция avg_prec_noloop() довольно требовательна к памяти, поскольку она пытается каждую операцию за одну go. Пока количество предметов находится в пределах 100, оно всегда будет работать довольно быстро. К сожалению, он потребляет слишком много памяти, когда количество предметов стремится к 1000 или более, и вызовет крэ sh. Я включил это только для того, чтобы показать, что это можно сделать без циклов, но я не рекомендую использовать его.

Следуя аналогичной логике c к оригиналу, но добавляя один l oop над элементами у нас есть функция avg_prec_colwise. Мы можем рассчитать точность и вызвать @K для всех пользователей, используя целые пороговые столбцы за раз. Он имеет время, аналогичное предыдущей реализации no l oop, но он не так требователен к памяти и все еще обладает тем свойством, что он немного быстрее, когда элементы <= 100, независимо от количества пользователей. Для 100 000 пользователей и 10 товаров это почти в 300 раз быстрее, чем оригинал; но если items> = 1000, он становится в сто раз медленнее, чем оригинал. Всякий раз, когда у вас есть сценарий с большим количеством пользователей и небольшим количеством элементов, я рекомендую вам использовать это.

Наконец, у меня есть реализация avg_prec_rowwise, которая, возможно, наиболее близка к реализации sklearn. Он не имеет удивительного усиления функций colwise или nol oop, когда элементы ниже, но он последовательно на 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

Некоторый временной сценарий ios: 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)

Out:

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)

Out:

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)

Out:

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)

Out:

Original: 6.912739515304565
Rowwise: 5.9845476150512695
1 голос
/ 05 апреля 2020

Я не особо разбирался в этом вопросе, пока вы не предоставили свой ответ .

Я предполагаю, что код выполняет правильную операцию. Я также предполагаю, что k обычно намного меньше, чем количество баллов. Если это так, вы можете немного оптимизировать свой код, потому что вам не нужно сортировать столько, сколько вы делаете, вас интересуют только самые большие значения k, которые сортируются при выполнении вычисления actual. Для этого вы можете использовать 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 и может быть вычислено более эффективно, если заметить, что единственное релевантное количество - это число 1 с в y_true. К сожалению, последующее вычисление не может быть эффективно выполнено в 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]

В то время как тест на респектабельном наборе данных дает:

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
0 голосов
/ 02 апреля 2020

Мне удалось сделать это для NDCG показателя (линейная релевантность). Код расширен для 2D-версии, аналогичной для кода здесь .

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 l oop, как я ожидал .

...