有几种方法可以优化 R 的性能,例如使用 BrodieG 进行矢量化。
或者,您可以利用现有计算模式的性能优势,例如矩阵乘法、排序。进一步阅读本书中的模式
[Michael McCool 等,结构化并行编程 - 高效计算模式]。
我们还可以从多核 CPU 或 GPU 的这些现有模式的并行库中获得进一步的性能优势。在这种情况下,我们可以用矩阵运算或 KNN 来表示计算。
1.分析
通过system.time对大数据集输入(1e6)分析这段代码,我们可以看到第一个循环计算两个向量之间的距离,占总计算时间的95%。所以,我们的优化将从这里开始。
# Original code
vals <- 50
clusts <- 6
ClusterCenters <- matrix(runif(vals * clusts), nrow=clusts)
data.count <- 1e6 # large number
calcData <- matrix(runif(data.count * vals), nrow=data.count)
system.time({
for(i in 1:nrow(ClusterCenters)) {
dists[i,] <- (rowSums((matrix(unlist(apply(calcData, 1, function(x) {x ClusterCenters[i,]})), ncol = ncol(calcData), byrow = TRUE))^2))^0.5
}
})
user system elapsed
71.62 1.13 73.13
system.time({
for(i in 1: nrow(calcData)) {
ClusterMemberships[i] <- which.min(dists[,i])
}
})
user system elapsed
5.29 0.00 5.31
2。矢量化
向量化是加速 R 代码的绝对有用的方法,尤其是对于“循环”,如 @BrodieG 所示。顺便说一句,我对他的解决方案进行了一些修改以获得正确的结果,如下所示,它可以比原始代码获得大约 3-5X 的加速。
#Vectorization: From BrodieG
dists1 <-matrix(NA, nrow = nrow(ClusterCenters), ncol = nrow(calcData)) system.time({
dists1 <- apply(ClusterCenters, 1, function(x) rowSums(sweep(calcData, 2,x, '-') ^ 2) ^ .5)
min.dist.vec <- max.col(-dists1, ties.method="first")
})
user system elapsed
16.13 1.42 17.61
all.equal(ClusterMemberships, min.dist.vec)
[1] TRUE
3.矩阵模式
然后,让我们回顾一下第一个循环,它通过汇总 (calcData[i,] – ClusterCenters[j,])^2 的列来计算距离。
所以,我们可以通过展开这个等式,将这个运算转化为矩阵:
calcData[i, ]^2 – 2 * calcData[i, ] * ClusterCenters[j, ] +
集群中心[j,]^2
因此,对于第一部分和第三部分,我们可以做简单的矩阵扫描乘法,例如
计算数据 * 计算数据
而对于第二项,我们需要一个矩阵转移的技巧,然后它变成一个矩阵乘法
ClusterCenters %*% t(calcData)
最后,矩阵运算的整个代码如下:
# Pattern Representation 1: Matrix
dists2 <-matrix(NA, nrow = nrow(ClusterCenters), ncol = nrow(calcData))
system.time({
data2 <- rowSums(calcData*calcData)
clusters2 <- rowSums(ClusterCenters*ClusterCenters)
# NVIDIA GPU: nvBLAS can speedup this step
# Futher Info on ParallelR.com
ClustersXdata <- calcData %*% t(ClusterCenters)
# compute distance
dists2 <- sweep(data2 - 2 * ClustersXdata, 2, clusters2, '+') ^0.5
min.dist.matrix <- max.col(-dists2, ties.method="first")
})
user system elapsed
1.17 0.09 1.28
all.equal(ClusterMemberships, min.dist.matrix)
[1] TRUE
现在,我们可以看到所有这三个部分都可以通过矩阵运算来完成。通过这种方法,计算时间从 10^3 到 10^7 几乎是线性的,并且它比 1e6 calcData 集的原始代码快大约 50X。
4. KNN
在这种情况下,计算可以看作是在集群数据集中找到第一个最近邻,所以它是一种最简单的 KNN,k=1。而且我们可以很容易地使用C编写的类包中的knn函数。同时,它也将非常高效。示例代码如下:
# Pattern Representation 2: KNN
library("class")
system.time(
min.dist.knn <- knn(ClusterCenters, calcData, cl = 1:nrow(ClusterCenters), k = 1)
)
user system elapsed
1.21 0.12 1.35
all.equal(ClusterMemberships, as.integer(min.dist.knn))
[1] TRUE
KNN的运行时间和我们1e6的矩阵运算代码差不多,但是如果我们对这两个算法应用更多的大数据,我们可以看到矩阵算法还是赢了,矩阵算法是2X 比 KNN 快(15.9 .vs. 29.1)。
最后,在这篇文章中我只是展示了一些性能调优的想法,我们可以继续微调这段代码,包括架构指定的优化和使用 c/c++ 重写它。甚至在多核CPU和NVIDIA GPU上并行化矩阵运算和KNN,可以参考ParallelR