as.matrix на объекте расстояния чрезвычайно медленный; как сделать это быстрее?
Я нашел пакет R Rlof
который использует многопоточность для вычисления матрицы расстояний, и это делает замечательную работу.
Тем не менее, вывод функции distmc
это вектор, а не матрица. применение as.matrix
Для этого "дистального" объекта получается гораздо дороже, чем многопоточный расчет расстояний.
Глядя на справку по функции, есть варианты печати диагонали и верхнего треугольника, но я не понимаю, где они должны использоваться.
Есть ли способ сэкономить время as.matrix
каким-то образом?
Воспроизводимый пример:
set.seed(42)
M1 = matrix(rnorm(15000*20), nrow = 15000, ncol =20)
system.time({dA = distmc(M1, method = "euclidean", diag = TRUE,
upper = TRUE, p = 2)})
system.time(A = as.matrix(dA))
1 ответ
Что значит dist
вернуть?
Эта функция всегда возвращает вектор, содержащий нижнюю треугольную часть (по столбцу) полной матрицы. diag
или же upper
аргумент влияет только на печать, т. е. stats:::print.dist
, Эта функция может печатать этот вектор как матрицу; но это действительно не так.
Почему as.matrix
плохо для "dist" объекта?
Трудно эффективно работать с треугольными матрицами или сделать их симметричными в R ядре. lower.tri
а также upper.tri
может быть занимать память, если ваша матрица велика: R: преобразовать верхнюю треугольную часть матрицы в симметричную матрицу.
Преобразование объекта dist в матрицу хуже. Посмотрите на код stats:::as.matrix.dist
:
function (x, ...)
{
size <- attr(x, "Size")
df <- matrix(0, size, size)
df[row(df) > col(df)] <- x
df <- df + t(df)
labels <- attr(x, "Labels")
dimnames(df) <- if (is.null(labels))
list(seq_len(size), seq_len(size))
else list(labels, labels)
df
}
Использование row
, col
а также t
это кошмар. И финал "dimnames<-"
генерирует еще один большой временный матричный объект. Когда память становится узким местом, все идет медленно.
Но нам все еще, возможно, нужна полная матрица, поскольку она проста в использовании.
Неловко то, что проще работать с полной матрицей, поэтому мы этого хотим. Рассмотрим этот пример: R - Как получить индексы строк и столбцов совпадающих элементов из матрицы расстояний. Операции сложны, если мы пытаемся избежать формирования полной матрицы.
Решение Rcpp
Я пишу функцию Rcpp dist2mat
(см. исходный файл "dist2mat.cpp" в конце этого ответа).
Функция имеет два входа: объект dist x
и (целочисленный) коэффициент блокировки кэша bf
, Функция работает, сначала создав матрицу и заполнив ее нижнюю треугольную часть, затем скопировав нижнюю треугольную часть в верхнюю треугольную, чтобы сделать ее симметричной. Второй шаг - это типичная операция транспонирования, и для крупномасштабной матричной кэш-памяти стоит блокировать. Производительность должна быть нечувствительной к фактору блокировки кэша, если она не слишком мала или не слишком велика. 128 или 256, как правило, хороший выбор.
Это моя первая попытка с Rcpp. Я был программистом C, использующим традиционный интерфейс C. Но я уже собираюсь познакомиться с Rcpp. Учитывая, что вы не знаете, как писать скомпилированный код, вы, вероятно, также не знаете, как запускать функции Rcpp. Вам нужно
- устанавливать
Rcpp
пакет (не уверен, нужен ли вам Rtools, если вы работаете в Windows); - скопируйте мой "dist2mat.cpp" в файл в текущем рабочем каталоге вашего R (вы можете получить его из
getwd()
в вашей R сессии). Файл.cpp - это простой текстовый файл, поэтому вы можете создавать, редактировать и сохранять его в любом текстовом редакторе.
Теперь давайте начнем витрину.
library(Rcpp)
sourceCpp("dist2mat.cpp") ## this takes some time; be patient
## a simple test with `dist2mat`
set.seed(0)
x <- dist(matrix(runif(10), nrow = 5, dimnames = list(letters[1:5], NULL)))
A <- dist2mat(x, 128) ## cache blocking factor = 128
A
# a b c d e
#a 0.0000000 0.9401067 0.9095143 0.5618382 0.4275871
#b 0.9401067 0.0000000 0.1162289 0.3884722 0.6968296
#c 0.9095143 0.1162289 0.0000000 0.3476762 0.6220650
#d 0.5618382 0.3884722 0.3476762 0.0000000 0.3368478
#e 0.4275871 0.6968296 0.6220650 0.3368478 0.0000000
Полученная матрица сохраняет имена строк вашей исходной матрицы / фрейма данных, переданных в dist
,
Вы можете настроить фактор блокировки кэша на вашем компьютере. Обратите внимание, что эффект блокировки кэша не проявляется для небольших матриц. Здесь я попробовал один 10000 х 10000.
## mimic a "dist" object without actually calling `dist`
n <- 10000
x <- structure(numeric(n * (n - 1) / 2), class = "dist", Size = n)
system.time(A <- dist2mat(x, 64))
# user system elapsed
# 0.676 0.424 1.113
system.time(A <- dist2mat(x, 128))
# user system elapsed
# 0.532 0.140 0.672
system.time(A <- dist2mat(x, 256))
# user system elapsed
# 0.616 0.140 0.759
Мы можем измерять dist2mat
с as.matrix
, Как as.matrix
занимает много оперативной памяти, здесь я приведу небольшой пример.
## mimic a "dist" object without actually calling `dist`
n <- 2000
x <- structure(numeric(n * (n - 1) / 2), class = "dist", Size = n)
library(bench)
bench::mark(dist2mat(x, 128), as.matrix(x), check = FALSE)
## A tibble: 2 x 14
# expression min mean median max `itr/sec` mem_alloc n_gc n_itr
# <chr> <bch:tm> <bch:> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int>
#1 dist2mat(x, … 24.6ms 26ms 25.8ms 37.1ms 38.4 30.5MB 0 20
#2 as.matrix(x) 154.5ms 155ms 154.8ms 154.9ms 6.46 160.3MB 0 4
## ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
## time <list>, gc <list>
Обратите внимание, как dist2mat
быстрее (см. "mean", "median"), а также насколько меньше оперативной памяти (см. "mem_alloc") ему нужно. Я поставил check = FALSE
отключить проверку результатов, потому что dist2mat
не возвращает атрибут "dimnames" (у объекта "dist" такой информации нет), но as.matrix
делает (это устанавливает 1:2000
как "dimnames"), поэтому они не совсем равны. Но вы можете убедиться, что они оба верны.
A <- dist2mat(x, 128)
B <- as.matrix(x)
range(A - B)
#[1] 0 0
"Dist2mat.cpp"
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericMatrix dist2mat(NumericVector& x, int bf) {
/* input validation */
if (!x.inherits("dist")) stop("Input must be a 'dist' object");
int n = x.attr("Size");
if (n > 65536) stop("R cannot create a square matrix larger than 65536 x 65536");
/* initialization */
NumericMatrix A(n, n);
/* use pointers */
size_t j, i, jj, ni, nj; double *ptr_x = &x[0];
double *A_jj, *A_ij, *A_ji, *col, *row, *end;
/* fill in lower triangular part */
for (j = 0; j < n; j++) {
col = &A(j + 1, j); end = &A(n, j);
while (col < end) *col++ = *ptr_x++;
}
/* cache blocking factor */
size_t b = (size_t)bf;
/* copy lower triangular to upper triangular; cache blocking applied */
for (j = 0; j < n; j += b) {
nj = n - j; if (nj > b) nj = b;
/* diagonal block has size nj x nj */
A_jj = &A(j, j);
for (jj = nj - 1; jj > 0; jj--, A_jj += n + 1) {
/* copy a column segment to a row segment */
col = A_jj + 1; row = A_jj + n;
for (end = col + jj; col < end; col++, row += n) *row = *col;
}
/* off-diagonal blocks */
for (i = j + nj; i < n; i += b) {
ni = n - i; if (ni > b) ni = b;
/* off-diagonal block has size ni x nj */
A_ij = &A(i, j); A_ji = &A(j, i);
for (jj = 0; jj < nj; jj++) {
/* copy a column segment to a row segment */
col = A_ij + jj * n; row = A_ji + jj;
for (end = col + ni; col < end; col++, row += n) *row = *col;
}
}
}
/* add row names and column names */
A.attr("dimnames") = List::create(x.attr("Labels"), x.attr("Labels"));
return A;
}