4

I am trying to implement NMF in R based on the following formula :
H is initially guess and then iteratively update based on this formula. I wrote this code but it takes like ever to execute. How can I rewrite this code? W is similarity matrix.

sym.nmf <- function ( W )
{
        N <- ncol(W)
        set.seed(1234)
        H <- matrix(runif(N * k, 0, 1),N,k)

        J1 <- 0

        while (0 < 1)
        {
                HT <- t(H)
                A <- W %*% H
                B <- H %*% HT %*% H
                H <- 0.5 * ( H * ( 1 + ( A / B )))
                J = W - (H %*% t(H))
                J = sum (J^2)
                if ( (J1 != 0 ) && (J > J1) )
                        return (H1)
                H1 <- H
                J1 <- J
        }

}
Sahar
  • 177
  • 2
  • 11
  • 6
    There's an [R package](https://cran.r-project.org/web/packages/NMF/index.html) `NMF` which does this, if you don't want to try to re-invent the wheel – TomNash Feb 11 '16 at 18:45
  • 3
    And if you're interested in methods, [you could check out their implementation](https://github.com/renozao/NMF). – Gregor Thomas Feb 11 '16 at 18:46
  • @TomNash Unfortunately it is a new method and there is not any implementation for it – Sahar Feb 11 '16 at 18:52
  • 3
    I don't see anything obviously slow, so if you want more speed it might be time to look to `rcpp` or the like. – Gregor Thomas Feb 11 '16 at 18:55
  • @Gregor It is slow when the dimension of matrix W is large. In my case it is 1500* 1500. – Sahar Feb 11 '16 at 19:07
  • I'm not sure why you tagged me in that, it doesn't seem to be related to my comment. I'll say it again: *if you want more speed it might be time to look to `rcpp` or the like.* – Gregor Thomas Feb 11 '16 at 19:11
  • I used tcrossprod and I found it quite useful – Sahar Feb 11 '16 at 20:45
  • To be clear, symmetric NMF is not implemented in NMF as stated in the upvoted comments above. While NMF will approximate SymNMF, it does not solve the above equation and is neither a theoretically proper nor precise solution if the input is a similarity matrix. – zdebruine Oct 31 '20 at 14:51
  • @Sahar I suspect you've moved on long ago, but there are many points in your code where parallelization is possible, where you can avoid transferring variables to memory, and where Rcpp will speed things up. I'm having a go at it right now and if I can beat NNLM approximations of SymNMF in Rbenchmark tests I'll post up. But NNLM take a similar approach (alternating least squares) to solve for W and H, so it naturally approximates this algorithm you've posted above. – zdebruine Oct 31 '20 at 14:52

1 Answers1

2

Here's a rework of the sym.nmf function with some statistically important improvements and speed gains along the way.

  1. Add a relative tolerance (rel.tol) parameter to break the loop when J[i] is within rel.tol percent of J[i-1]. The way you have it set up, you will only stop the loop when 0 == 1 or machine precision becomes more variable than the fit itself. In theory, your function will never converge.

  2. Add a seed, because reproducibility is important. Along this line, you may think of initializing with Non-negative double SVD to get a head start. However, depending on your application, this may drive your NMF into a local minima that is not representative of the global minima so it can be dangerous. In my case I get locked into a SVD-like minima and the NMF ends up converging at a state totally unlike the factorization from a random initialization.

  3. Add a maximum number of iterations (max.iter), because sometimes you don't want to run a million iterations to reach your tolerance threshold.

  4. Substitute in the crossprod and tcrossprod functions for the base %*% function. This nets about a 2x speed gain depending on matrix size.

  5. Reduce the number of times convergence is checked, because calculating the residual signal in W after subtraction of HH^T takes nearly half of the computing time. You can assume it's going to take hundreds to thousands of iterations to converge, so just check for convergence every 100 cycles.

Updated function:

sym.nmf <- function (W, k, seed = 123, max.iter = 10000, rel.tol = 1e-10) {
  set.seed(seed)
  H <- matrix(runif(ncol(W) * k, 0, 1),ncol(W),k)
  J <- c()
  for(i in 1:max.iter){
    H <- 0.5*(H*(1+(crossprod(W,H)/tcrossprod(H,crossprod(H)))))

    # check for convergence every 100 iterations
    if(i %% 100 == 0){
      J <- c(J,sum((W - tcrossprod(H))^2))
      plot(J, xlab = "iteration", ylab = "total residual signal", log = 'y')
      cat("Iteration ",i,": J =",tail(J)[1],"\n")
      if(length(J) > 3 && (1 - tail(J, 1)/tail(J, 2)[1]) < rel.tol){
        return(H)
      }    
    }
    if(i == max.iter){
      warning("Max.iter was reached before convergence\n")
      return(H)
    }
  }
}

The objective function can be isolated as well, and Rfast can be used for parallelized computing of Rfast::Crossprod() and Rfast::Tcrossprod() as well.

sym.nmf <- function (W, k, seed = 123, max.iter = 100, rel.tol = 1e-10) {
  set.seed(seed)
  require(Rfast)
  H <- matrix(runif(ncol(W) * k, 0, 1),ncol(W),k)
  J <- c()
  for(i in 1:max.iter){
    H <- 0.5 * fit_H(W,H, num.iter = 100)
    J <- c(J,sum((W - tcrossprod(H))^2))
    plot(J, xlab = "iteration", ylab = "total residual signal", log = 'y')
    cat("Iteration ",i,": J =",tail(J, n = 1),"\n")
    if(length(J) > 3 && (1 - tail(J, 1)/tail(J, 2)[1]) < rel.tol){
      return(H)
    }
    if(i == max.iter){
      warning("Max.iter was reached before convergence\n")
      return(H)
    }
  }
}

fit_H <- function(W,H, num.iter){
  for(i in 1:num.iter){
    H <- 0.5*(H*(1+(Rfast::Crossprod(W,H)/Rfast::Tcrossprod(H,Rfast::Crossprod(H,H)))))
  }
  H
}

Now this objective function can be translated to Rcpp for further speed gains. Parallelization may also achieve further gains, either within the objective function (parallelized crossprod and tcrossprod) or by running multiple factorizations in parallel (since multiple restarts are often required to discover a robust solution).

zdebruine
  • 3,687
  • 6
  • 31
  • 50