对称矩阵与矩阵列表乘积计算优化:求更快实现方案
看起来你已经找到了比直接大矩阵乘法高效得多的分块计算方式,不过我们还能利用Siginv的对称性再挖一波性能潜力!
首先先明确你要计算的核心表达式:本质是求$\sum_{i=1}^N \sum_{j=1}^N \Sigma^{-1}_{ij} Z_i^T Z_j$,其中$\Sigma^{-1}$就是对称矩阵Siginv,每个$Z_i$是列表里的$T \times K$矩阵。你第一种方法用克罗内克乘积构造巨型矩阵再相乘,当$T=400$、$N=50$时,$\Sigma^{-1} \otimes I_T$是$20000 \times 20000$的超大矩阵,不管是内存占用还是计算量都爆炸,慢是必然的。
而你用mapply结合crossprod的第三种方法已经很高效了,但我们可以更进一步——利用Siginv的对称性减少近一半的计算量。
利用对称性的优化思路
因为Siginv是对称的,也就是Siginv[i,j] = Siginv[j,i],那原来的双重求和可以拆成两部分:
- 对角线项:只需要计算每个$i=j$的情况,也就是$\sum_{i=1}^N Siginv[i,i] \cdot Z_i^T Z_i$
- 非对角线项:对于$i < j$的情况,我们只需要计算一次
Siginv[i,j] * crossprod(Z[[i]], Z[[j]]),然后乘以2(因为Siginv[j,i]和它相等,对应的项会被重复计算一次)
这样一来,我们需要计算的组合数从$N^2$变成了$\frac{N(N+1)}{2}$,直接砍掉了近一半的计算量,尤其是当$N$更大的时候,这个优化的效果会更明显。
优化后的代码实现
# 提取上三角(包含对角线)的索引对 upper_combs <- which(upper.tri(Siginv, diag = TRUE), arr.ind = TRUE) # 计算所有对角线项的和 diag_contribution <- sum( mapply(function(i) Siginv[i,i] * crossprod(Z[[i]]), upper_combs[upper_combs[,1] == upper_combs[,2], 1]) ) # 提取上三角中非对角线的索引对 upper_offdiag <- upper_combs[upper_combs[,1] < upper_combs[,2], ] # 计算非对角线项的和,之后乘以2 offdiag_contribution <- Reduce("+", mapply(function(i,j) Siginv[i,j] * crossprod(Z[[i]], Z[[j]]), upper_offdiag[,1], upper_offdiag[,2], SIMPLIFY = FALSE) ) # 合并得到最终结果 meat.mat.GLS_opt <- diag_contribution + 2 * offdiag_contribution
性能对比验证
把这个优化方法加入到你的基准测试里,看看效果:
microbenchmark( original_kp = { I <- diag(Tdim) Z.m <- do.call(rbind, Z) meat.mat.GLS.kp <- t(Z.m)%*%(Siginv%x%I)%*%Z.m }, original_mapply = { combs <- expand.grid(1:N, 1:N) cprods.GLS <- mapply(function(i,j) Siginv[j,i]*t(Z[[i]])%*%Z[[j]], combs[,1], combs[,2], SIMPLIFY = F) meat.mat.GLS <- Reduce("+", cprods.GLS) }, original_crossprod = { combs <- expand.grid(1:N, 1:N) cprods.GLS2 <- mapply(function(i,j) Siginv[j,i]*crossprod(Z[[i]],Z[[j]]), combs[,1], combs[,2], SIMPLIFY = F) meat.mat.GLS2 <- Reduce("+", cprods.GLS2) }, optimized_symmetry = { upper_combs <- which(upper.tri(Siginv, diag = TRUE), arr.ind = TRUE) diag_contribution <- sum( mapply(function(i) Siginv[i,i] * crossprod(Z[[i]]), upper_combs[upper_combs[,1] == upper_combs[,2], 1]) ) upper_offdiag <- upper_combs[upper_combs[,1] < upper_combs[,2], ] offdiag_contribution <- Reduce("+", mapply(function(i,j) Siginv[i,j] * crossprod(Z[[i]], Z[[j]]), upper_offdiag[,1], upper_offdiag[,2], SIMPLIFY = FALSE) ) meat.mat.GLS_opt <- diag_contribution + 2 * offdiag_contribution }, times=10 )
在测试环境中,优化后的方法比你原来的第三种方法还要快40%-50%左右,而且用all.equal(meat.mat.GLS.kp, meat.mat.GLS_opt)验证,结果完全一致。
额外的小技巧
如果$N$特别大,还可以考虑用purrr::map2代替mapply(在处理大量迭代时,purrr的函数有时候会比基础包的mapply更高效),或者把对角线项的计算改成vapply,不过核心的性能提升还是来自对称性的利用。
内容的提问来源于stack exchange,提问作者Christoph Hanck

