从距离矩阵创建 dist 对象的内存高效方法
Memory-efficient method to create dist object from distance matrix
我正在尝试从大距离矩阵创建 dist
对象。我 运行 使用 stats::as.dist
内存不足。例如,我在当前机器上有大约 128 Gb 可用,但 as.dist
在处理 73,000 x 73,000 矩阵(约 42Gb)时内存不足。鉴于最终的 dist
对象应该小于矩阵大小的一半(即它是下三角,存储为向量)在我看来应该可以在更多内存中进行此计算- 高效的方式 - 前提是我们注意不要创建大型中间对象,而只是将输入的相关元素直接复制到输出。
查看 getS3method('as.dist', 'default')
的代码,我发现它使用 ans <- m[row(m) > col(m)]
进行计算,这需要创建相同维度的 row
和 col
矩阵作为输入。
我认为我可以使用 中的算法来生成下三角的索引来对此进行改进。这是我使用这种方法的尝试。
as.dist.new = function(dm, diag = FALSE, upper = FALSE) {
n = dim(dm)[1]
stopifnot(is.matrix(dm))
stopifnot(dim(dm)[2] == n)
k = 1:((n^2 - n)/2)
j <- floor(((2 * n + 1) - sqrt((2 * n - 1) ^ 2 - 8 * (k - 1))) / 2)
i <- j + k - (2 * n - j) * (j - 1) / 2
idx = cbind(i,j)
remove(i,j,k)
gc()
d = dm[idx]
class(d) <- "dist"
attr(d, "Size") <- n
attr(d, "call") <- match.call()
attr(d, "Diag") <- diag
attr(d, "Upper") <- upper
d
}
这适用于较小的矩阵。这是一个简单的例子:
N = 10
dm = matrix(runif(N*N), N, N)
diag(dm) = 0
x = as.dist(dm)
y = as.dist.new(dm)
但是,如果我们创建一个更大的距离矩阵,它会遇到与 as.dist
相同的内存问题。
例如此版本在我的系统上崩溃:
N = 73000
dm = matrix(runif(N*N), N, N)
gc()
diag(dm) = 0
gc()
as.dist.new(dm)
有没有人建议如何更有效地执行此操作?欢迎使用 R 或 Rcpp 解决方案。注意 this answer 相关问题(从 2 列位置数据生成完整的距离矩阵)似乎可以使用 RcppArmadillo
来做到这一点,但我没有经验使用那个。
嗯,遍历下三角的逻辑比较简单,
如果你用 C++ 做,那么它也可以很快:
library(Rcpp)
sourceCpp(code='
// [[Rcpp::plugins(cpp11)]]
#include <cstddef> // size_t
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector as_dist(const NumericMatrix& mat) {
std::size_t nrow = mat.nrow();
std::size_t ncol = mat.ncol();
std::size_t size = nrow * (nrow - 1) / 2;
NumericVector ans(size);
if (nrow > 1) {
std::size_t k = 0;
for (std::size_t j = 0; j < ncol; j++) {
for (std::size_t i = j + 1; i < nrow; i++) {
ans[k++] = mat(i,j);
}
}
}
ans.attr("class") = "dist";
ans.attr("Size") = nrow;
ans.attr("Diag") = false;
ans.attr("Upper") = false;
return ans;
}
')
as_dist(matrix(1:100, 10, 10))
1 2 3 4 5 6 7 8 9
2 2
3 3 13
4 4 14 24
5 5 15 25 35
6 6 16 26 36 46
7 7 17 27 37 47 57
8 8 18 28 38 48 58 68
9 9 19 29 39 49 59 69 79
10 10 20 30 40 50 60 70 80 90
我目前的解决方案是直接从lat和lon向量计算dist对象,根本不生成中间距离矩阵。在大型矩阵上,这比 geosphere::mdist()
后跟 stats::as.dist()
的“传统”管道快几百倍,并且只使用存储最终 dist 对象所需的内存。
以下 Rcpp 源代码基于使用 to calculate haversine distance in c++, together with an adaptation of 中的函数迭代 c++ 中的下三角元素。
#include <Rcpp.h>
using namespace Rcpp;
double distanceHaversine(double latf, double lonf, double latt, double lont, double tolerance){
double d;
double dlat = latt - latf;
double dlon = lont - lonf;
d = (sin(dlat * 0.5) * sin(dlat * 0.5)) + (cos(latf) * cos(latt)) * (sin(dlon * 0.5) * sin(dlon * 0.5));
if(d > 1 && d <= tolerance){
d = 1;
}
return 2 * atan2(sqrt(d), sqrt(1 - d)) * 6378137.0;
}
double toRadians(double deg){
return deg * 0.01745329251; // PI / 180;
}
//-----------------------------------------------------------
// [[Rcpp::export]]
NumericVector calc_dist(Rcpp::NumericVector lat,
Rcpp::NumericVector lon,
double tolerance = 10000000000.0) {
std::size_t nlat = lat.size();
std::size_t nlon = lon.size();
if (nlat != nlon) throw std::range_error("lat and lon different lengths");
if (nlat < 2) throw std::range_error("Need at least 2 points");
std::size_t size = nlat * (nlat - 1) / 2;
NumericVector ans(size);
std::size_t k = 0;
double latf;
double latt;
double lonf;
double lont;
for (std::size_t j = 0; j < (nlat-1); j++) {
for (std::size_t i = j + 1; i < nlat; i++) {
latf = toRadians(lat[i]);
lonf = toRadians(lon[i]);
latt = toRadians(lat[j]);
lont = toRadians(lon[j]);
ans[k++] = distanceHaversine(latf, lonf, latt, lont, tolerance);
}
}
return ans;
}
/*** R
as_dist = function(lat, lon, tolerance = 10000000000.0) {
dd = calc_dist(lat, lon, tolerance)
attr(dd, "class") = "dist"
attr(dd, "Size") = length(lat)
attr(dd, "call") = match.call()
attr(dd, "Diag") = FALSE
attr(dd, "Upper") = FALSE
return(dd)
}
*/
我正在尝试从大距离矩阵创建 dist
对象。我 运行 使用 stats::as.dist
内存不足。例如,我在当前机器上有大约 128 Gb 可用,但 as.dist
在处理 73,000 x 73,000 矩阵(约 42Gb)时内存不足。鉴于最终的 dist
对象应该小于矩阵大小的一半(即它是下三角,存储为向量)在我看来应该可以在更多内存中进行此计算- 高效的方式 - 前提是我们注意不要创建大型中间对象,而只是将输入的相关元素直接复制到输出。
查看 getS3method('as.dist', 'default')
的代码,我发现它使用 ans <- m[row(m) > col(m)]
进行计算,这需要创建相同维度的 row
和 col
矩阵作为输入。
我认为我可以使用
as.dist.new = function(dm, diag = FALSE, upper = FALSE) {
n = dim(dm)[1]
stopifnot(is.matrix(dm))
stopifnot(dim(dm)[2] == n)
k = 1:((n^2 - n)/2)
j <- floor(((2 * n + 1) - sqrt((2 * n - 1) ^ 2 - 8 * (k - 1))) / 2)
i <- j + k - (2 * n - j) * (j - 1) / 2
idx = cbind(i,j)
remove(i,j,k)
gc()
d = dm[idx]
class(d) <- "dist"
attr(d, "Size") <- n
attr(d, "call") <- match.call()
attr(d, "Diag") <- diag
attr(d, "Upper") <- upper
d
}
这适用于较小的矩阵。这是一个简单的例子:
N = 10
dm = matrix(runif(N*N), N, N)
diag(dm) = 0
x = as.dist(dm)
y = as.dist.new(dm)
但是,如果我们创建一个更大的距离矩阵,它会遇到与 as.dist
相同的内存问题。
例如此版本在我的系统上崩溃:
N = 73000
dm = matrix(runif(N*N), N, N)
gc()
diag(dm) = 0
gc()
as.dist.new(dm)
有没有人建议如何更有效地执行此操作?欢迎使用 R 或 Rcpp 解决方案。注意 this answer 相关问题(从 2 列位置数据生成完整的距离矩阵)似乎可以使用 RcppArmadillo
来做到这一点,但我没有经验使用那个。
嗯,遍历下三角的逻辑比较简单, 如果你用 C++ 做,那么它也可以很快:
library(Rcpp)
sourceCpp(code='
// [[Rcpp::plugins(cpp11)]]
#include <cstddef> // size_t
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector as_dist(const NumericMatrix& mat) {
std::size_t nrow = mat.nrow();
std::size_t ncol = mat.ncol();
std::size_t size = nrow * (nrow - 1) / 2;
NumericVector ans(size);
if (nrow > 1) {
std::size_t k = 0;
for (std::size_t j = 0; j < ncol; j++) {
for (std::size_t i = j + 1; i < nrow; i++) {
ans[k++] = mat(i,j);
}
}
}
ans.attr("class") = "dist";
ans.attr("Size") = nrow;
ans.attr("Diag") = false;
ans.attr("Upper") = false;
return ans;
}
')
as_dist(matrix(1:100, 10, 10))
1 2 3 4 5 6 7 8 9
2 2
3 3 13
4 4 14 24
5 5 15 25 35
6 6 16 26 36 46
7 7 17 27 37 47 57
8 8 18 28 38 48 58 68
9 9 19 29 39 49 59 69 79
10 10 20 30 40 50 60 70 80 90
我目前的解决方案是直接从lat和lon向量计算dist对象,根本不生成中间距离矩阵。在大型矩阵上,这比 geosphere::mdist()
后跟 stats::as.dist()
的“传统”管道快几百倍,并且只使用存储最终 dist 对象所需的内存。
以下 Rcpp 源代码基于使用
#include <Rcpp.h>
using namespace Rcpp;
double distanceHaversine(double latf, double lonf, double latt, double lont, double tolerance){
double d;
double dlat = latt - latf;
double dlon = lont - lonf;
d = (sin(dlat * 0.5) * sin(dlat * 0.5)) + (cos(latf) * cos(latt)) * (sin(dlon * 0.5) * sin(dlon * 0.5));
if(d > 1 && d <= tolerance){
d = 1;
}
return 2 * atan2(sqrt(d), sqrt(1 - d)) * 6378137.0;
}
double toRadians(double deg){
return deg * 0.01745329251; // PI / 180;
}
//-----------------------------------------------------------
// [[Rcpp::export]]
NumericVector calc_dist(Rcpp::NumericVector lat,
Rcpp::NumericVector lon,
double tolerance = 10000000000.0) {
std::size_t nlat = lat.size();
std::size_t nlon = lon.size();
if (nlat != nlon) throw std::range_error("lat and lon different lengths");
if (nlat < 2) throw std::range_error("Need at least 2 points");
std::size_t size = nlat * (nlat - 1) / 2;
NumericVector ans(size);
std::size_t k = 0;
double latf;
double latt;
double lonf;
double lont;
for (std::size_t j = 0; j < (nlat-1); j++) {
for (std::size_t i = j + 1; i < nlat; i++) {
latf = toRadians(lat[i]);
lonf = toRadians(lon[i]);
latt = toRadians(lat[j]);
lont = toRadians(lon[j]);
ans[k++] = distanceHaversine(latf, lonf, latt, lont, tolerance);
}
}
return ans;
}
/*** R
as_dist = function(lat, lon, tolerance = 10000000000.0) {
dd = calc_dist(lat, lon, tolerance)
attr(dd, "class") = "dist"
attr(dd, "Size") = length(lat)
attr(dd, "call") = match.call()
attr(dd, "Diag") = FALSE
attr(dd, "Upper") = FALSE
return(dd)
}
*/