【问题标题】:Clustering a large, very sparse, binary matrix in R在 R 中聚类一个大的、非常稀疏的二进制矩阵
【发布时间】:2015-09-05 19:46:23
【问题描述】:

我有一个大而稀疏的二进制矩阵(大约 39,000 x 14,000;大多数行只有一个“1”条目)。我想将相似的行聚集在一起,但我最初的计划需要很长时间才能完成:

d <- dist(inputMatrix, method="binary")
hc <- hclust(d, method="complete")

第一步没有完成,所以我不确定第二步会怎样。有哪些方法可以有效地将 R 中大型稀疏二进制矩阵的相似行分组?

【问题讨论】:

  • 如何定义行之间的距离?如果大多数行只有一个 1,是 1 之间的距离吗?
  • 我想使用 Jaccard 距离,我相信这是 distmethod 设置为“二进制”时使用的距离。
  • 如问题中所述,正在使用二进制方法:任一向量中为 1 的任何条目的重叠 1 的比例。
  • 您是否尝试过折叠重复的行?这可能会使您获得更易于管理的观察次数。然后,当您最后清楚地返回时,已删除的条目将与其唯一的行伙伴一起返回。
  • @cr1msonB1ade 我看到了,但如果大多数行只有一个,那么大多数行比较的距离将为 1,所以我想知道是否还有其他意图。

标签: r performance matrix cluster-analysis sparse-matrix


【解决方案1】:

我已经编写了一些 Rcpp 代码和 R 代码,它们可以计算出二进制矩阵的二进制/Jaccard 距离。比 dist(x, method = "binary") 快 80 倍。它将输入矩阵转换为原始矩阵,这是输入的转置(以便位模式在内部处于正确的顺序)。然后在一些 C++ 代码中使用它,将数据作为 64 位无符号整数处理以提高速度。两个向量 x 和 y 的 Jaccard 距离等于 x ^ y / (x | y),其中 ^ 是 xor 运算符。如果xoror 的结果非零,则Hamming Weight 计算用于计算设置的位数。

我已将代码放在 github 上 https://github.com/NikNakk/binaryDist/ 上,并复制了下面的两个文件。我已经确认,一些随机数据集的结果与dist(x, method = "binary") 相同。

在 39000 行 x 14000 列(每行 1-5 个)的数据集上,大约需要 11 分钟。输出距离矩阵为 5.7 GB。

bDist.cpp

#include <Rcpp.h>
using namespace Rcpp;

//countBits function taken from https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation

const uint64_t m1  = 0x5555555555555555; //binary: 0101...
const uint64_t m2  = 0x3333333333333333; //binary: 00110011..
const uint64_t m4  = 0x0f0f0f0f0f0f0f0f; //binary:  4 zeros,  4 ones ...
const uint64_t h01 = 0x0101010101010101; //the sum of 256 to the power of 0,1,2,3...

int countBits(uint64_t x) {
  x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
  x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits 
  x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits 
  return (x * h01)>>56;  //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... 
}

// [[Rcpp::export]]
int countBitsFromRaw(RawVector rv) {
  uint64_t* x = (uint64_t*)RAW(rv);
  return(countBits(*x));
}

// [[Rcpp::export]]
NumericVector bDist(RawMatrix mat) {
  int nr(mat.nrow()), nc(mat.ncol());
  int nw = nr / 8;
  NumericVector res(nc * (nc - 1) / 2);
  // Access the raw data as unsigned 64 bit integers
  uint64_t* data = (uint64_t*)RAW(mat);
  uint64_t a(0);
  // Work through each possible combination of columns (rows in the original integer matrix)
  for (int i = 0; i < nc - 1; i++) {
    for (int j = i + 1; j < nc; j++) {
      uint64_t sx = 0;
      uint64_t so = 0;
      // Work through each 64 bit integer and calculate the sum of (x ^ y) and (x | y)
      for (int k = 0; k < nw; k++) {
        uint64_t o = data[nw * i + k] | data[nw * j + k];
        // If (x | y == 0) then (x ^ y) will also be 0
        if (o) {
          // Use Hamming weight method to calculate number of set bits
          so = so + countBits(o);
          uint64_t x = data[nw * i + k] ^ data[nw * j + k];
          if (x) {
            sx = sx + countBits(x);
          }
        }
      }
      res(a++) = (double)sx / so;
    }
  }
  return (res);
}

R 源码

library("Rcpp")
library("plyr")
sourceCpp("bDist.cpp")

# Converts a binary integer vector into a packed raw vector,
# padding out at the end to make the input length a multiple of packWidth
packRow <- function(row, packWidth = 64L) {
  packBits(as.raw(c(row, rep(0, (packWidth - length(row)) %% packWidth))))
}

as.PackedMatrix <- function(x, packWidth = 64L) {
  UseMethod("as.PackedMatrix")
}

# Converts a binary integer matrix into a packed raw matrix
# padding out at the end to make the input length a multiple of packWidth
as.PackedMatrix.matrix <- function(x, packWidth = 64L) {
  stopifnot(packWidth %% 8 == 0, class(x) %in% c("matrix", "Matrix"))
  storage.mode(x) <- "raw"
  if (ncol(x) %% packWidth != 0) {
    x <- cbind(x, matrix(0L, nrow = nrow(x), ncol = packWidth - (ncol(x) %% packWidth)))
  }
  out <- packBits(t(x))
  dim(out) <- c(ncol(x) %/% 8, nrow(x))
  class(out) <- "PackedMatrix"
  out
}

# Converts back to an integer matrix
as.matrix.PackedMatrix <- function(x) {
  out <- rawToBits(x)
  dim(out) <- c(nrow(x) * 8L, ncol(x))
  storage.mode(out) <- "integer"
  t(out)
}

# Generates random sparse data for testing the main function
makeRandomData <- function(nObs, nVariables, maxBits, packed = FALSE) {
  x <- replicate(nObs, {
    y <- integer(nVariables)
    y[sample(nVariables, sample(maxBits, 1))] <- 1L
    if (packed) {
      packRow(y, 64L)
    } else {
      y
    }
  })
  if (packed) {
    class(x) <- "PackedMatrix"
    x
  } else {
    t(x)
  }
}

# Reads a binary matrix from file or character vector
# Borrows the first bit of code from read.table
readPackedMatrix <- function(file = NULL, text = NULL, packWidth = 64L) {
  if (missing(file) && !missing(text)) {
    file <- textConnection(text)
    on.exit(close(file))
  }
  if (is.character(file)) {
    file <- file(file, "rt")
    on.exit(close(file))
  }
  if (!inherits(file, "connection")) 
    stop("'file' must be a character string or connection")
  if (!isOpen(file, "rt")) {
    open(file, "rt")
    on.exit(close(file))
  }
  lst <- list()
  i <- 1
  while(length(line <- readLines(file, n = 1)) > 0) {
    lst[[i]] <- packRow(as.integer(strsplit(line, "", fixed = TRUE)[[1]]), packWidth = packWidth)
    i <- i + 1
  }
  out <- do.call("cbind", lst)
  class(out) <- "PackedMatrix"
  out
}

# Wrapper for the C++ code which 
binaryDist <- function(x) {
  if (class(x) != "PackedMatrix") {
    x <- as.PackedMatrix(x)
  }
  dst <- bDist(x)
  attr(dst, "Size") <- ncol(x)
  attr(dst, "Diag") <- attr(dst, "Upper") <- FALSE
  attr(dst, "method") <- "binary"
  attr(dst, "call") <- match.call()
  class(dst) <- "dist"
  dst
}

x <- makeRandomData(2000, 400, maxBits = 5, packed = TRUE)

system.time(bd <- binaryDist(x))

来自原始答案:

要考虑的其他事情是对两行与单行之间的比较进行一些预过滤,因为距离将是 0 表示重复或 1 表示任何其他可能性。

vegan 包中的vegdist 函数和amap 包。如果您有多个内核并利用它支持并行化的事实,后者可能只会更快。

【讨论】:

  • 这行得通 - 谢谢!一件快速的事情 - bddist() 的输出略有不同,bd 缺少“标签”字段。看起来像在通过names(cutClusterObject) &lt;- rownames(x) 进行聚类后将它们重新添加,但您认为有办法维护bd 对象中的标签吗?
  • @MattLaFave 老实说,最简单的做法是按照您的建议进行操作 - 如果需要,可以将其添加到上面的代码中。
  • 这太棒了。感谢发帖!
  • 我在我的研究中使用了你的代码,我想在我的论文中引用它。您能告诉我您希望如何引用它吗?
  • @Shamsa 谢谢,很高兴听到它很有用!该代码未在除此处以外的任何地方发布,因此最好引用此 Stack Overflow 帖子和我的名字。如果您乐意这样做,那么当您准备好与全世界分享您的论文时,看到您的论文会非常有趣。
【解决方案2】:
  1. 您有重复的行吗?无需计算它们的距离两次。

  2. 带有单个 1 的所有行将与所有带有单个 1 在不同位置的行 100% 不同。

因此,对此类数据进行聚类是没有意义的。输出是相当可预测的,归结为找到 1。

尝试将您的数据集限制为具有多个的对象。除非您只能在这些方面获得有趣的结果,否则无需继续。二进制数据的信息太少。

【讨论】:

    【解决方案3】:

    计算需要这么长时间的原因是对 dist 的调用正在计算和存储超过 7.6 亿个成对距离。如果您的数据存储稀疏,这将需要很长时间和大量存储。如果您的数据不是稀疏存储的,那么每次距离计算至少需要 14,000 次操作,总操作数超过 1 万亿!

    一种更快的方法是 k-means 聚类,因为它不需要预先计算距离矩阵;在每次迭代中,您只需要 39000*k 距离计算,其中 k 是集群的数量。要获得类似于 Jaccard 索引的成对距离(如果相同,则为 0,如果没有索引重合,则为 1,如果某些但不是所有索引重合,则介于两者之间),您可以将每一行 x 除以 sqrt(2*sum(x^2))。例如,如果您有以下输入矩阵:

    (mat <- rbind(c(1, 0, 0, 0, 0), c(0, 0, 0, 1, 1)))
    #      [,1] [,2] [,3] [,4] [,5]
    # [1,]    1    0    0    0    0
    # [2,]    0    0    0    1    1
    

    标准化版本将是(假设仅在矩阵中存在二进制值;如果不是这种情况,您将使用rowSums(mat^2)):

    (mat.norm <- mat / sqrt(2*rowSums(mat)))
    #           [,1] [,2] [,3] [,4] [,5]
    # [1,] 0.7071068    0    0  0.0  0.0
    # [2,] 0.0000000    0    0  0.5  0.5
    

    这两个观测值(没有共同的索引)的欧几里得距离为 1,与本例的 Jaccard 距离一致。

    dist(mat.norm, "euclidean")
    #   1
    # 2 1
    

    此外,相同的观测值显然具有欧几里得距离 0,同样对应于 Jaccard 距离。

    【讨论】:

      猜你喜欢
      • 2011-03-11
      • 2023-03-05
      • 2011-03-03
      • 1970-01-01
      • 2018-02-09
      • 2019-07-09
      • 2012-09-05
      • 1970-01-01
      • 2014-09-07
      相关资源
      最近更新 更多