快速非负最小二乘的RCPP实现?

时间:2019-09-19 08:15:07

标签: r constraints rcpp least-squares nnls

我正在寻找R的快速实现(基于活动集)的非负最小二乘算法的快速实现。 Bro, R., & de Jong, S. (1997) A fast non-negativity-constrained least squares algorithm. Journal of Chemometrics, 11, 393-401.multiway package I found this pure R implementation中:

fnnls <- 
  function(XtX,Xty,ntol=NULL){     
    ### initialize variables
    pts <- length(Xty)
    if(is.null(ntol)){
      ntol <- 10*(.Machine$double.eps)*max(colSums(abs(XtX)))*pts
    }
    pvec <- matrix(0,1,pts)
    Zvec <- matrix(1:pts,pts,1)
    beta <- zvec <- t(pvec)
    zz <- Zvec
    wvec <- Xty - XtX%*%beta

    ### iterative procedure
    iter <- 0    
    itmax <- 30*pts

    # outer loop
    while(any(Zvec>0) && any(wvec[zz]>ntol)) {

      tt <- zz[which.max(wvec[zz])]
      pvec[1,tt] <- tt
      Zvec[tt] <- 0
      pp <- which(pvec>0)
      zz <- which(Zvec>0)
      nzz <- length(zz)
      zvec[pp] <- smpower(XtX[pp,pp],-1)%*%Xty[pp]
      zvec[zz] <- matrix(0,nzz,1)

      # inner loop
      while(any(zvec[pp]<=ntol) &&  iter<itmax) {

        iter <- iter + 1
        qq <- which((zvec<=ntol) & t(pvec>0))
        alpha <- min(beta[qq]/(beta[qq]-zvec[qq]))
        beta <- beta + alpha*(zvec-beta)
        indx <- which((abs(beta)<ntol) & t(pvec!=0))
        Zvec[indx] <- t(indx)
        pvec[indx] <- matrix(0,1,length(indx))
        pp <- which(pvec>0)
        zz <- which(Zvec>0)
        nzz <- length(zz)
        if(length(pp)>0){
          zvec[pp] <- smpower(XtX[pp,pp],-1)%*%Xty[pp]
        }
        zvec[zz] <- matrix(0,nzz,1)      

      } # end inner loop

      beta <- zvec
      wvec <- Xty - XtX%*%beta

    } # end outer loop

    beta

  }

但是在我的测试中,尽管算法上fnnls应该更快,但是它比plain nnls function in the nnls package(用fortran编码)要慢得多。我想知道是否有人会提供Rcpp的{​​{1}}端口,理想情况下使用armadillo类并允许fnnls稀疏,也许还支持Y具有多列? >

1 个答案:

答案 0 :(得分:2)

为了研究目的,我在这个问题上花了将近一周的时间。

我还花了将近两天的时间尝试解析 multiway::fnnls 的实现,并且不会在 R 礼节、可解释性和内存使用方面使用选择词。

我不明白为什么 multiway::fnnls 的作者认为他们的实现应该很快。考虑到 fortran Lawson/Hanson 实现,仅 R 实现似乎没用。

这是我编写的 RcppArmadillo 函数(快速近似解轨迹)NNLS,它为条件良好的系统复制 multiway::fnnls

//[[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>

using namespace arma;
typedef unsigned int uint;

// [[Rcpp::export]]
vec fastnnls(mat a, vec b) {

  // initial x is the unbounded least squares solution
  vec x = arma::solve(a, b, arma::solve_opts::likely_sympd + arma::solve_opts::fast);
  
  while (any(x < 0)) {

    // define the feasible set as all values greater than 0
    arma::uvec nz = find(x > 0);
    
    // reset x
    x.zeros();
    
    // solve the least squares solution for values in the feasible set
    x.elem(nz) = solve(a.submat(nz, nz), b.elem(nz), arma::solve_opts::likely_sympd + arma::solve_opts::fast);
  }
  return x;
}

这种方法本质上是 TNT-NN 的前半部分,但没有在每次迭代时尝试从可行集中添加或删除元素的“启发式”。

为了使这种方法超越简单的近似,我们可以添加顺序坐标下降,它接收上面的 FAST 解作为初始化。一般来说,对于大多数小的条件良好的问题,在 99% 的情况下,FAST 给出了准确的解决方案。

上述实现的一个独特属性是它不会给出误报,但有时(在大型或病态系统中)会给出误报。因此,可能比实际解决方案稍微稀疏。请注意,FAST 和精确解之间的损失通常在 1% 以内,因此如果您不追求绝对精确解,那么这是您的最佳选择。

上述算法的运行速度也比 Lawson/Hanson nnls 求解器快得多。这是我刚刚从一个 50 系数系统复制过来的微基准测试,复制了 10000 次:

Unit: microseconds
               expr   min    lq      mean median    uq     max neval
           fastnnls  53.9  56.2  59.32761   58.0  59.5   359.7 10000
 lawson/hanson nnls 112.9 116.7 125.96169  118.6 129.5 11032.4 10000

当然,性能因密度和负性而异。与其他算法相比,我的算法往往随着稀疏度的增加而变得更快,而在正解更少的情况下变得更快。

我尝试过简化 multiway::fnnls 代码并将其运行到 Armadillo 中,但未能成功。

我正在努力将此方法实现为 Rcpp 包,并将在它发布到稳定的 Github 版本时发布。

ps:使用 Eigen 可以加快速度。犰狳求解器使用 Cholesky 分解和直接替换。 Eigen 的 Cholesky 求解器速度更快,因为它可以执行更多的原位运算。