Вычисление сходства Jaccard в Python

У меня есть 20 000 документов, для которых я хочу вычислить истинное сходство Джаккарда, чтобы позже я мог проверить, насколько точно минимальное хеширование приближается к нему.

каждый документ представлен в виде столбца в матрице numpy, где каждая строка-это слово, которое либо появляется в документе (запись=1), либо нет (запись = 0). Есть ~600 слов (строк).

Так, например, столбец 1 будет [1 0 0 0 0 0 1 0 0 0 1 0], что означает слова 1,7,11 появились в нем и никакие другие.

есть ли более эффективный способ вычислить сходство, кроме моего подхода сравнения по элементам? Я не вижу, как я мог бы использовать наборы для улучшения скорости, так как наборы просто становятся (0,1), но в настоящее время код невозможно медленный.

import numpy as np

#load file into python
rawdata = np.loadtxt("myfile.csv",delimiter="t")
#Convert the documents from rows to columns
rawdata = np.transpose(rawdata)
#compute true jacard similarity
ndocs = rawdata.shape[1]
nwords = rawdata.shape[0]
tru_sim = np.zeros((ndocs,ndocs))

#computes jaccard similarity of 2 documents
def jaccard(c1, c2):
    n11 = sum((c1==1)&(c2==1))
    n00 = sum((c1==0)&(c2==0))
    jac = n11 / (nfeats-n00)
    return (jac)

for i in range(0,ndocs):
    tru_sim[i,i]=1
    for j in range(i+1,ndocs):
        tru_sim[i,j] = jaccard(rawdata[:,i],rawdata[:,j])

2 ответов


вот векторизованный подход -

# Get the row, col indices that are to be set in output array        
r,c = np.tril_indices(ndocs,-1)

# Use those indicees to slice out respective columns 
p1 = rawdata[:,c]
p2 = rawdata[:,r]

# Perform n11 and n00 vectorized computations across all indexed columns
n11v = ((p1==1) & (p2==1)).sum(0)
n00v = ((p1==0) & (p2==0)).sum(0)

# Finally, setup output array and set final division computations
out = np.eye(ndocs)
out[c,r] = n11v / (nfeats-n00v)

альтернативный способ вычисления n11v и n00v С np.einsum -

n11v = np.einsum('ij,ij->j',(p1==1),(p2==1).astype(int))
n00v = np.einsum('ij,ij->j',(p1==0),(p2==0).astype(int))

если rawdata состоит из 0s и 1s только, более простой способ получить их будет -

n11v = np.einsum('ij,ij->j',p1,p2)
n00v = np.einsum('ij,ij->j',1-p1,1-p2)

бенчмаркинг

функции определения -

def original_app(rawdata, ndocs, nfeats):
    tru_sim = np.zeros((ndocs,ndocs))
    for i in range(0,ndocs):
        tru_sim[i,i]=1
        for j in range(i+1,ndocs):
            tru_sim[i,j] = jaccard(rawdata[:,i],rawdata[:,j])
    return tru_sim

def vectorized_app(rawdata, ndocs, nfeats):
    r,c = np.tril_indices(ndocs,-1)
    p1 = rawdata[:,c]
    p2 = rawdata[:,r]
    n11v = ((p1==1) & (p2==1)).sum(0)
    n00v = ((p1==0) & (p2==0)).sum(0)
    out = np.eye(ndocs)
    out[c,r] = n11v / (nfeats-n00v)
    return out

проверка и тайминги -

In [6]: # Setup inputs
   ...: rawdata = (np.random.rand(20,10000)>0.2).astype(int)
   ...: rawdata = np.transpose(rawdata)
   ...: ndocs = rawdata.shape[1]
   ...: nwords = rawdata.shape[0]
   ...: nfeats = 5
   ...: 

In [7]: # Verify results
   ...: out1 = original_app(rawdata, ndocs, nfeats)
   ...: out2 = vectorized_app(rawdata, ndocs, nfeats)
   ...: print np.allclose(out1,out2)
   ...: 
True

In [8]: %timeit original_app(rawdata, ndocs, nfeats)
1 loops, best of 3: 8.72 s per loop

In [9]: %timeit vectorized_app(rawdata, ndocs, nfeats)
10 loops, best of 3: 27.6 ms per loop

каким-то волшебным 300x+ ускорение есть!

Итак, почему это так быстро? Ну, есть много факторов, наиболее важным из которых является тот факт, что массивы NumPy построены для производительности и оптимизированы для векторизованных вычислений. С предложенным подходом мы используем его довольно хорошо и, таким образом, видим такие ускорения.

вот related Q&A что говорить об этих критериях производительности подробно.


чтобы вычислить Jaccard, используйте:

def jaccard(x,y):
  x = np.asarray(x, np.bool) # Not necessary, if you keep your data
  y = np.asarray(y, np.bool) # in a boolean array already!
  return np.double(np.bitwise_and(x, y).sum()) / np.double(np.bitwise_or(x, y).sum())

print jaccard([1,1,0,0,0],[0,1,0,0,1])
>>> 0.33333333333333331