Как эффективно рассчитать PPMI на разреженной матрице в R?
Я бы подумал, что между R
пакеты text2vec
, tm
, quanteda
, svs
, qlcMatrix
а также wordspace
была бы функция для вычисления PPMI (положительная точечная взаимная информация) между терминами и контекстами (основанная на матрице совпадений термин-термин (контекст)) - но, очевидно, нет, поэтому я решил написать ее сам. Проблема в том, что он медленный, как патока, возможно потому, что я не очень хорош с разреженными матрицами - и мои tcms имеют порядок 10k*20k, поэтому они должны быть разреженными.
Из того, что я понимаю, PMI = log( p(word, context) / (p(word)*p(context)) )
следовательно, я считаю, что:
count(word_context_co-occurrence) / N
PMI = log( ------------------------------------- )
count(word)/N * count(context)/N
куда N
является суммой всех совпадений в матрице совпадений. И PPMI просто заставляет все значения <0 быть равными 0. (Это пока правильно, верно?)
Имея это в виду, вот попытка реализации:
library(Matrix)
set.seed(1)
pmat = matrix(sample(c(0,0,0,0,0,0,1,10),5*10,T), 5,10, byrow=T) # tiny example matrix;
# rows are words, columns are contexts (words the row-words co-occur with, in a certain window in the text)
pmat = Matrix(pmat, sparse=T) # make it sparse
# calculate some things beforehand to make it faster
N = sum(pmat)
contextp = Matrix::colSums(pmat)/N # probabilities of contexts
wordp = Matrix::rowSums(pmat)/N # probabilities of terms
# here goes nothing...
pmat2 = pmat
for(r in 1:nrow(pmat)){ # go term by term, calculate PPMI association with each of its contexts
not0 = which(pmat[r, ] > 0) # no need to consider 0 values (no co-occurrence)
tmp = log( (pmat[r,not0] / N) / (wordp[r] * contextp[not0] )) # PMI
tmp = ifelse(tmp < 0, 0, tmp) # PPMI
pmat2[r, not0] = tmp # <-- THIS here is the slow part, replacing the old frequency values with the new PPMI weighted ones.
}
# take a look:
round(pmat2,2)
То, что кажется медленным, это не сама калькуляция, а помещение новых вычисленных значений в разреженную матрицу (на этом крошечном примере это неплохо, но если вы сделаете это тысячами строк на тысячи строк, даже одна итерация этого цикла будет длиться вечно построение новой матрицы с rBind
кажется, идея хуже).
Как эффективнее заменить старые значения в такой разреженной матрице новыми взвешенными значениями PPMI? Либо предложения изменить этот код, либо использовать какую-то существующую функцию в каком-то пакете, который я как-то пропустил - все в порядке.
1 ответ
Тем временем, вроде как, это работает довольно быстро. Я оставлю это здесь на случай, если кто-нибудь еще столкнется с той же проблемой. Также, похоже, очень похоже на подход, связанный в комментарии к вопросу (спасибо!).
# this is for a column-oriented sparse matrix; transpose if necessary
tcmrs = Matrix::rowSums(pmat)
tcmcs = Matrix::colSums(pmat)
N = sum(tcmrs)
colp = tcmcs/N
rowp = tcmrs/N
pp = pmat@p+1
ip = pmat@i+1
tmpx = rep(0,length(pmat@x)) # new values go here, just a numeric vector
# iterate through sparse matrix:
for(i in 1:(length(pmat@p)-1) ){
ind = pp[i]:(pp[i+1]-1)
not0 = ip[ind]
icol = pmat@x[ind]
tmp = log( (icol/N) / (rowp[not0] * colp[i] )) # PMI
tmpx[ind] = tmp
}
pmat@x = tmpx
# to convert to PPMI, replace <0 values with 0 and do a Matrix::drop0() on the object.