web-dev-qa-db-fra.com

Quel est le moyen le plus rapide Python pour calculer la similarité en cosinus à partir de données matricielles éparses?

Avec une liste de matrice clairsemée, quel est le meilleur moyen de calculer la similarité de cosinus entre chacune des colonnes (ou lignes) de la matrice? Je préférerais ne pas itérer n-choisir-deux fois.

Disons que la matrice d'entrée est:

A= 
[0 1 0 0 1
 0 0 1 1 1
 1 1 0 1 0]

La représentation clairsemée est:

A = 
0, 1
0, 4
1, 2
1, 3
1, 4
2, 0
2, 1
2, 3

En Python, il est facile de travailler avec le format d’entrée matricielle:

import numpy as np
from sklearn.metrics import pairwise_distances
from scipy.spatial.distance import cosine

A = np.array(
[[0, 1, 0, 0, 1],
[0, 0, 1, 1, 1],
[1, 1, 0, 1, 0]])

dist_out = 1-pairwise_distances(A, metric="cosine")
dist_out

Donne:

array([[ 1.        ,  0.40824829,  0.40824829],
       [ 0.40824829,  1.        ,  0.33333333],
       [ 0.40824829,  0.33333333,  1.        ]])

C'est bien pour une entrée de matrice complète, mais je veux vraiment commencer par la représentation clairsemée (en raison de la taille et de la rareté de ma matrice). Des idées sur la meilleure façon d'accomplir cela? Merci d'avance.

50
zbinsd

Vous pouvez calculer la similarité cosinus par paires sur les lignes d'une matrice clairsemée directement à l'aide de sklearn. À partir de la version 0.17, il prend également en charge les sorties fragmentées:

from sklearn.metrics.pairwise import cosine_similarity
from scipy import sparse

A =  np.array([[0, 1, 0, 0, 1], [0, 0, 1, 1, 1],[1, 1, 0, 1, 0]])
A_sparse = sparse.csr_matrix(A)

similarities = cosine_similarity(A_sparse)
print('pairwise dense output:\n {}\n'.format(similarities))

#also can output sparse matrices
similarities_sparse = cosine_similarity(A_sparse,dense_output=False)
print('pairwise sparse output:\n {}\n'.format(similarities_sparse))

Résultats:

pairwise dense output:
[[ 1.          0.40824829  0.40824829]
[ 0.40824829  1.          0.33333333]
[ 0.40824829  0.33333333  1.        ]]

pairwise sparse output:
(0, 1)  0.408248290464
(0, 2)  0.408248290464
(0, 0)  1.0
(1, 0)  0.408248290464
(1, 2)  0.333333333333
(1, 1)  1.0
(2, 1)  0.333333333333
(2, 0)  0.408248290464
(2, 2)  1.0

Si vous voulez des similitudes de cosinus en colonne, transposez simplement votre matrice de saisie à l’avance:

A_sparse.transpose()
52
Jeff

La méthode suivante est environ 30 fois plus rapide que scipy.spatial.distance.pdist. Cela fonctionne assez rapidement sur les grandes matrices (en supposant que vous ayez assez de RAM)

Voir ci-dessous pour une discussion sur la façon d'optimiser pour la clarté.

# base similarity matrix (all dot products)
# replace this with A.dot(A.T).toarray() for sparse representation
similarity = numpy.dot(A, A.T)


# squared magnitude of preference vectors (number of occurrences)
square_mag = numpy.diag(similarity)

# inverse squared magnitude
inv_square_mag = 1 / square_mag

# if it doesn't occur, set it's inverse magnitude to zero (instead of inf)
inv_square_mag[numpy.isinf(inv_square_mag)] = 0

# inverse of the magnitude
inv_mag = numpy.sqrt(inv_square_mag)

# cosine similarity (elementwise multiply by inverse magnitudes)
cosine = similarity * inv_mag
cosine = cosine.T * inv_mag

Si votre problème est typique des problèmes de préférences binaires à grande échelle, vous avez beaucoup plus d'entrées dans une dimension que dans l'autre. En outre, la dimension courte est celle dont vous voulez calculer les similitudes entre les entrées. Appelons cette dimension la dimension 'item'.

Si tel est le cas, indiquez vos éléments dans des lignes et créez A à l'aide de scipy.sparse . Puis remplacez la première ligne comme indiqué.

Si votre problème est atypique, vous aurez besoin de plus de modifications. Ceux-ci devraient être des remplacements assez simples des opérations de base numpy avec leurs scipy.sparse équivalents.

40
Waylon Flinn

J'ai essayé quelques méthodes ci-dessus. Cependant, l'expérience de @zbinsd a ses limites. La rareté de la matrice utilisée dans l'expérience est extrêmement faible, alors que la rareté réelle est généralement supérieure à 90%. Dans mon état, le maigre est avec la forme de (7000, 25000) et le maigre de 97%. La méthode 4 est extrêmement lente et je ne peux pas tolérer les résultats. J'utilise la méthode 6 qui se termine en 10 s. Étonnamment, j'ai essayé la méthode ci-dessous et elle est terminée en seulement 0,247 s.

import sklearn.preprocessing as pp

def cosine_similarities(mat):
    col_normed_mat = pp.normalize(mat.tocsc(), axis=0)
    return col_normed_mat.T * col_normed_mat

Cette méthode efficace est liée par entrez la description du lien ici

11
tianshi miao

J'ai pris toutes ces réponses et écrit un script pour 1. valider chacun des résultats (voir assertion ci-dessous) et 2. voir lequel est le plus rapide. Le code et les résultats sont ci-dessous:

# Imports
import numpy as np
import scipy.sparse as sp
from scipy.spatial.distance import squareform, pdist
from sklearn.metrics.pairwise import linear_kernel
from sklearn.preprocessing import normalize
from sklearn.metrics.pairwise import cosine_similarity

# Create an adjacency matrix
np.random.seed(42)
A = np.random.randint(0, 2, (10000, 100)).astype(float).T

# Make it sparse
rows, cols = np.where(A)
data = np.ones(len(rows))
Asp = sp.csr_matrix((data, (rows, cols)), shape = (rows.max()+1, cols.max()+1))

print "Input data shape:", Asp.shape

# Define a function to calculate the cosine similarities a few different ways
def calc_sim(A, method=1):
    if method == 1:
        return 1 - squareform(pdist(A, metric='cosine'))
    if method == 2:
        Anorm = A / np.linalg.norm(A, axis=-1)[:, np.newaxis]
        return np.dot(Anorm, Anorm.T)
    if method == 3:
        Anorm = A / np.linalg.norm(A, axis=-1)[:, np.newaxis]
        return linear_kernel(Anorm)
    if method == 4:
        similarity = np.dot(A, A.T)

        # squared magnitude of preference vectors (number of occurrences)
        square_mag = np.diag(similarity)

        # inverse squared magnitude
        inv_square_mag = 1 / square_mag

        # if it doesn't occur, set it's inverse magnitude to zero (instead of inf)
        inv_square_mag[np.isinf(inv_square_mag)] = 0

        # inverse of the magnitude
        inv_mag = np.sqrt(inv_square_mag)

        # cosine similarity (elementwise multiply by inverse magnitudes)
        cosine = similarity * inv_mag
        return cosine.T * inv_mag
    if method == 5:
        '''
        Just a version of method 4 that takes in sparse arrays
        '''
        similarity = A*A.T
        square_mag = np.array(A.sum(axis=1))
        # inverse squared magnitude
        inv_square_mag = 1 / square_mag

        # if it doesn't occur, set it's inverse magnitude to zero (instead of inf)
        inv_square_mag[np.isinf(inv_square_mag)] = 0

        # inverse of the magnitude
        inv_mag = np.sqrt(inv_square_mag).T

        # cosine similarity (elementwise multiply by inverse magnitudes)
        cosine = np.array(similarity.multiply(inv_mag))
        return cosine * inv_mag.T
    if method == 6:
        return cosine_similarity(A)

# Assert that all results are consistent with the first model ("truth")
for m in range(1, 7):
    if m in [5]: # The sparse case
        np.testing.assert_allclose(calc_sim(A, method=1), calc_sim(Asp, method=m))
    else:
        np.testing.assert_allclose(calc_sim(A, method=1), calc_sim(A, method=m))

# Time them:
print "Method 1"
%timeit calc_sim(A, method=1)
print "Method 2"
%timeit calc_sim(A, method=2)
print "Method 3"
%timeit calc_sim(A, method=3)
print "Method 4"
%timeit calc_sim(A, method=4)
print "Method 5"
%timeit calc_sim(Asp, method=5)
print "Method 6"
%timeit calc_sim(A, method=6)

Résultats:

Input data shape: (100, 10000)
Method 1
10 loops, best of 3: 71.3 ms per loop
Method 2
100 loops, best of 3: 8.2 ms per loop
Method 3
100 loops, best of 3: 8.6 ms per loop
Method 4
100 loops, best of 3: 2.54 ms per loop
Method 5
10 loops, best of 3: 73.7 ms per loop
Method 6
10 loops, best of 3: 77.3 ms per loop
5
zbinsd

Vous devriez vérifier scipy.sparse _ ( lien ). Vous pouvez appliquer des opérations sur ces matrices éparses, tout comme vous utilisez une matrice normale.

1
cheeyos

Salut tu peux le faire de cette façon

    temp = sp.coo_matrix((data, (row, col)), shape=(3, 59))
    temp1 = temp.tocsr()

    #Cosine similarity
    row_sums = ((temp1.multiply(temp1)).sum(axis=1))
    rows_sums_sqrt = np.array(np.sqrt(row_sums))[:,0]
    row_indices, col_indices = temp1.nonzero()
    temp1.data /= rows_sums_sqrt[row_indices]
    temp2 = temp1.transpose()
    temp3 = temp1*temp2
1
Vaali
def norm(vector):
    return sqrt(sum(x * x for x in vector))    

def cosine_similarity(vec_a, vec_b):
        norm_a = norm(vec_a)
        norm_b = norm(vec_b)
        dot = sum(a * b for a, b in Zip(vec_a, vec_b))
        return dot / (norm_a * norm_b)

Cette méthode semble être un peu plus rapide que d'utiliser l'implémentation de sklearn si vous transmettez une paire de vecteurs à la fois.

0
Daniel

Construire à partir de la solution de Vaali:

def sparse_cosine_similarity(sparse_matrix):
    out = (sparse_matrix.copy() if type(sparse_matrix) is csr_matrix else
           sparse_matrix.tocsr())
    squared = out.multiply(out)
    sqrt_sum_squared_rows = np.array(np.sqrt(squared.sum(axis=1)))[:, 0]
    row_indices, col_indices = out.nonzero()
    out.data /= sqrt_sum_squared_rows[row_indices]
    return out.dot(out.T)

Cela prend une matrice creuse (de préférence un csr_matrix) et retourne un csr_matrix. Les parties les plus intensives doivent être effectuées à l'aide de calculs clairsemés avec une surcharge de mémoire assez minime. Je ne l'ai pas encore testé de façon approfondie, donc attention (Mise à jour: j'ai confiance en cette solution maintenant que je l'ai testée et évaluée)

En outre, voici la version fragmentée de la solution de Waylon au cas où cela aiderait quelqu'un, ne sachant pas quelle solution est réellement la meilleure.

def sparse_cosine_similarity_b(sparse_matrix):
    input_csr_matrix = sparse_matrix.tocsr()
    similarity = input_csr_matrix * input_csr_matrix.T
    square_mag = similarity.diagonal()
    inv_square_mag = 1 / square_mag
    inv_square_mag[np.isinf(inv_square_mag)] = 0
    inv_mag = np.sqrt(inv_square_mag)
    return similarity.multiply(inv_mag).T.multiply(inv_mag)

Les deux solutions semblent avoir la parité avec sklearn.metrics.pairwise.cosine_similarity

:-RÉ

Mise à jour:

Maintenant, j'ai testé les deux solutions par rapport à mon implémentation Cython existante: https://github.com/davidmashburn/sparse_dot/blob/master/test/benchmarks_v3_output_table.txt et il semble que le premier algorithme effectue le meilleur des trois la plupart du temps.

0
David

Je suggère de courir en deux étapes:

1) générer un mappage A qui mappe A: index de colonne-> objets non nuls

2) pour chaque objet i (rangée) avec des occurrences non nulles (colonnes) {k1, .. kn} calcule la similarité cosinus uniquement pour les éléments de l'ensemble d'union A [k1] U A [k2] U .. A [kn]

En supposant une grande matrice clairsemée avec une faible densité, cela donnera un coup de fouet à la force brute

0
moshik