Skip to main content
Bounty Ended with 350 reputation awarded by CommunityBot
added 248 characters in body
Source Link
Sextus Empiricus
  • 86.4k
  • 6
  • 115
  • 301
### finding the feasible set
feasible_set <- function(a, b, ind){
  x <- rep(0, length(b))
  x[ind] <- solve(a[ind, ind], b[ind])
  while(any(x < 0)){
    ind <- which(x > 0)
    x <- rep(0, length(b))
    x[ind] <- solve(a[ind, ind], b[ind])
  }
  as.vector(x)
}

### finding the gradients
gradients <- function(b,y,X) {
  current_y <- X %*% b
  d_y <- y-current_y
  gradients <- t(X) %*% d_y
  return(gradients)
}

### The algorithm that repeatedly updates the active set
### The updates are done by removing the variable with the highest positive gradient
fastnnls <- function(y,X) {
  
  ### Initiation
  a <- crossprod(X)
  b <- as.vector(crossprod(X, y))
  current_active <-   rep(TRUE,length(X[1,])) ### start with all variables in active set
  current_s <- rep(0,length(X[1,])) ### initial conditions
  current_y <- X %*% current_s
  current_loss <- sum((y-current_y)^2)

  ### algorithm that stops untill no improvement can be made
  cont <- TRUE
  while (cont) {
    ### add variables based on gradients 
    ### in these four lines the gradients are found and ordered
    gradients <- gradients(current_s,y,X)
    testing <- which(gradients*current_active>0) ### find out which variables are active and have positive gradients
    ord <- order(gradients, decreasing = TRUE)
    ord <- ord[ord %in% testing] ### strip the negative or non-active variables
    
    ### keep adding variables in a loop while this improves the solution
    addition <- 0 ### itterative variable keeping track of the additions
    new_active <- current_active
    for (i in 1:length(ord)) {
      
      ### Try out a new active set with one variable removed
      new_active[ord[i]] <- FALSE  ### remove 'ord[i]' from active set
      new_s <- feasible_set(a,b, ind = which(new_active == FALSE))
      new_y <- X %*% new_s
      new_loss <- sum((y-new_y)^2) 
      
      ### Update the solution if the new trial is better
      if (new_loss < current_loss) {
        addition <- i
        current_active <- new_active
        current_loss <- new_loss
        current_s <- new_s
        current_y <- new_y
      } else {
        break ### skip loop to end
      }
    }
    
    if (addition == 0) { ### quit while when no addition is made
      cont = FALSE
    } else {
      new_active <- new_s == 0 ### in the for loop we had only been decreasing the active set
                               ### but the feasible_set function also increases the active it and we need to adapt accrdingly
    }
    if (sum(current_active) == 0) { ### quit if active set is empty (all variables positive) 
      cont = FALSE
    }
    
    ### The while loop continues by recomputing the gradients 
  }
  
  return(current_s)
}

set.seed(123)
X <- matrix(rnorm(2000),100,20)
y <- X %*% runif(20) + rnorm(100)*5

library(nnls)
library(multiway)


data.frame(multiway = multiway::fnnls(a, b),
           nnls = nnls::nnls(X, y)$x,
           manual = fastnnls(y,X))
### finding the feasible set
feasible_set <- function(a, b, ind){
  x <- rep(0, length(b))
  x[ind] <- solve(a[ind, ind], b[ind])
  while(any(x < 0)){
    ind <- which(x > 0)
    x <- rep(0, length(b))
    x[ind] <- solve(a[ind, ind], b[ind])
  }
  as.vector(x)
}

### finding the gradients
gradients <- function(b,y,X) {
  current_y <- X %*% b
  d_y <- y-current_y
  gradients <- t(X) %*% d_y
  return(gradients)
}

### The algorithm that repeatedly updates the active set
### The updates are done by removing the variable with the highest positive gradient
fastnnls <- function(y,X) {
  
  ### Initiation
  a <- crossprod(X)
  b <- as.vector(crossprod(X, y))
  current_active <-   rep(TRUE,length(X[1,])) ### start with all variables in active set
  current_s <- rep(0,length(X[1,])) ### initial conditions
  current_y <- X %*% current_s
  current_loss <- sum((y-current_y)^2)

  ### algorithm that stops untill no improvement can be made
  cont <- TRUE
  while (cont) {
    ### add variables based on gradients 
    ### in these four lines the gradients are found and ordered
    gradients <- gradients(current_s,y,X)
    testing <- which(gradients*current_active>0) ### find out which variables are active and have positive gradients
    ord <- order(gradients, decreasing = TRUE)
    ord <- ord[ord %in% testing] ### strip the negative or non-active variables
    
    ### keep adding variables in a loop while this improves the solution
    addition <- 0 ### itterative variable keeping track of the additions
    new_active <- current_active
    for (i in 1:length(ord)) {
      
      ### Try out a new active set with one variable removed
      new_active[ord[i]] <- FALSE  ### remove 'ord[i]' from active set
      new_s <- feasible_set(a,b, ind = which(new_active == FALSE))
      new_y <- X %*% new_s
      new_loss <- sum((y-new_y)^2) 
      
      ### Update the solution if the new trial is better
      if (new_loss < current_loss) {
        addition <- i
        current_active <- new_active
        current_loss <- new_loss
        current_s <- new_s
        current_y <- new_y
      } else {
        break ### skip loop to end
      }
    }
    
    if (addition == 0) { ### quit while when no addition is made
      cont = FALSE
    }
    if (sum(current_active) == 0) { ### quit if active set is empty (all variables positive) 
      cont = FALSE
    }
    
    ### The while loop continues by recomputing the gradients 
  }
  
  return(current_s)
}

set.seed(123)
X <- matrix(rnorm(2000),100,20)
y <- X %*% runif(20) + rnorm(100)*5

library(nnls)
library(multiway)


data.frame(multiway = multiway::fnnls(a, b),
           nnls = nnls::nnls(X, y)$x,
           manual = fastnnls(y,X))
### finding the feasible set
feasible_set <- function(a, b, ind){
  x <- rep(0, length(b))
  x[ind] <- solve(a[ind, ind], b[ind])
  while(any(x < 0)){
    ind <- which(x > 0)
    x <- rep(0, length(b))
    x[ind] <- solve(a[ind, ind], b[ind])
  }
  as.vector(x)
}

### finding the gradients
gradients <- function(b,y,X) {
  current_y <- X %*% b
  d_y <- y-current_y
  gradients <- t(X) %*% d_y
  return(gradients)
}

### The algorithm that repeatedly updates the active set
### The updates are done by removing the variable with the highest positive gradient
fastnnls <- function(y,X) {
  
  ### Initiation
  a <- crossprod(X)
  b <- as.vector(crossprod(X, y))
  current_active <-   rep(TRUE,length(X[1,])) ### start with all variables in active set
  current_s <- rep(0,length(X[1,])) ### initial conditions
  current_y <- X %*% current_s
  current_loss <- sum((y-current_y)^2)

  ### algorithm that stops untill no improvement can be made
  cont <- TRUE
  while (cont) {
    ### add variables based on gradients 
    ### in these four lines the gradients are found and ordered
    gradients <- gradients(current_s,y,X)
    testing <- which(gradients*current_active>0) ### find out which variables are active and have positive gradients
    ord <- order(gradients, decreasing = TRUE)
    ord <- ord[ord %in% testing] ### strip the negative or non-active variables
    
    ### keep adding variables in a loop while this improves the solution
    addition <- 0 ### itterative variable keeping track of the additions
    new_active <- current_active
    for (i in 1:length(ord)) {
      
      ### Try out a new active set with one variable removed
      new_active[ord[i]] <- FALSE  ### remove 'ord[i]' from active set
      new_s <- feasible_set(a,b, ind = which(new_active == FALSE))
      new_y <- X %*% new_s
      new_loss <- sum((y-new_y)^2) 
      
      ### Update the solution if the new trial is better
      if (new_loss < current_loss) {
        addition <- i
        current_active <- new_active
        current_loss <- new_loss
        current_s <- new_s
        current_y <- new_y
      } else {
        break ### skip loop to end
      }
    }
    
    if (addition == 0) { ### quit while when no addition is made
      cont = FALSE
    } else {
      new_active <- new_s == 0 ### in the for loop we had only been decreasing the active set
                               ### but the feasible_set function also increases the active it and we need to adapt accrdingly
    }
    if (sum(current_active) == 0) { ### quit if active set is empty (all variables positive) 
      cont = FALSE
    }
    
    ### The while loop continues by recomputing the gradients 
  }
  
  return(current_s)
}

set.seed(123)
X <- matrix(rnorm(2000),100,20)
y <- X %*% runif(20) + rnorm(100)*5

library(nnls)
library(multiway)


data.frame(multiway = multiway::fnnls(a, b),
           nnls = nnls::nnls(X, y)$x,
           manual = fastnnls(y,X))
added 77 characters in body
Source Link
Sextus Empiricus
  • 86.4k
  • 6
  • 115
  • 301

You got a difference because of a small error in your comparison. The one function requires the matrix $X$ and vector $Y$, the other function requires the matrix $X^TX$ and vector $X^Ty$. You have used the latter for both functions. In the code below I give an example output.

You got a difference because of a small error in your comparison. The one function requires the matrix $X$ and You have used the latter for both functions. In the code below I give an example output.

You got a difference because of a small error in your comparison. The one function requires the matrix $X$ and vector $Y$, the other function requires the matrix $X^TX$ and vector $X^Ty$. You have used the latter for both functions. In the code below I give an example output.

Source Link
Sextus Empiricus
  • 86.4k
  • 6
  • 115
  • 301

TNT-NN

For the TNT-NN see:

Myre, Joe M., et al. "TNT-NN: a fast active set method for solving large non-negative least squares problems." Procedia Computer Science 108 (2017): 755-764.

https://doi.org/10.1016/j.procs.2017.05.194

To form the active set, TNT-NN first solves an unconstrained least squares problem. Variables that violate the non-negativity constraint are added to the active set. Once a feasible solution is found, where none of the non-negativity constraints are violated, the 2-norm of the residual is used as a measure of fitness and the solution is saved as the current “best” solution.

TNT-NN attempts to modify the active set by iteratively moving some of the variables from the active set back into the unconstrained set. The active set variables are sorted based on their components of the gradient. Variables that show the largest positive gradient components are tested by moving some of them from the active set into the unconstrained set. It is important to note that initially large groups of variables can be moved in a single test. If the new solution does not improve in fitness, then the solution is rejected and a smaller set of variables is tested. If a group of the variables can be removed from the active set and a new feasible solution is found that is “better”, the solution is saved and the algorithm begins a new iteration. The algorithm reaches convergence when the active set can no longer be modified.

Your implementation is only half part of this solution. It is the part where the new feasible set is searched to test the removal of a variable from the active set. In the code below it is demonstrated. I have copied your fastnnls function and turned it into feasible_set which is only part of the algorithm.

In the article Myre et al speak of " by moving some of them from the active set into the unconstrained set. It is important to note that initially large groups of variables can be moved in a single test". But I am not sure how they do that so in the code, I have been adding them one by one. Probably there are some additional tricks to make faster selections of large groups to be added at once instead of my for loop that tries all variables.

The difference between multiway::fnnls and nnls::nnls

You got a difference because of a small error in your comparison. The one function requires the matrix $X$ and You have used the latter for both functions. In the code below I give an example output.

Example code

### finding the feasible set
feasible_set <- function(a, b, ind){
  x <- rep(0, length(b))
  x[ind] <- solve(a[ind, ind], b[ind])
  while(any(x < 0)){
    ind <- which(x > 0)
    x <- rep(0, length(b))
    x[ind] <- solve(a[ind, ind], b[ind])
  }
  as.vector(x)
}

### finding the gradients
gradients <- function(b,y,X) {
  current_y <- X %*% b
  d_y <- y-current_y
  gradients <- t(X) %*% d_y
  return(gradients)
}

### The algorithm that repeatedly updates the active set
### The updates are done by removing the variable with the highest positive gradient
fastnnls <- function(y,X) {
  
  ### Initiation
  a <- crossprod(X)
  b <- as.vector(crossprod(X, y))
  current_active <-   rep(TRUE,length(X[1,])) ### start with all variables in active set
  current_s <- rep(0,length(X[1,])) ### initial conditions
  current_y <- X %*% current_s
  current_loss <- sum((y-current_y)^2)

  ### algorithm that stops untill no improvement can be made
  cont <- TRUE
  while (cont) {
    ### add variables based on gradients 
    ### in these four lines the gradients are found and ordered
    gradients <- gradients(current_s,y,X)
    testing <- which(gradients*current_active>0) ### find out which variables are active and have positive gradients
    ord <- order(gradients, decreasing = TRUE)
    ord <- ord[ord %in% testing] ### strip the negative or non-active variables
    
    ### keep adding variables in a loop while this improves the solution
    addition <- 0 ### itterative variable keeping track of the additions
    new_active <- current_active
    for (i in 1:length(ord)) {
      
      ### Try out a new active set with one variable removed
      new_active[ord[i]] <- FALSE  ### remove 'ord[i]' from active set
      new_s <- feasible_set(a,b, ind = which(new_active == FALSE))
      new_y <- X %*% new_s
      new_loss <- sum((y-new_y)^2) 
      
      ### Update the solution if the new trial is better
      if (new_loss < current_loss) {
        addition <- i
        current_active <- new_active
        current_loss <- new_loss
        current_s <- new_s
        current_y <- new_y
      } else {
        break ### skip loop to end
      }
    }
    
    if (addition == 0) { ### quit while when no addition is made
      cont = FALSE
    }
    if (sum(current_active) == 0) { ### quit if active set is empty (all variables positive) 
      cont = FALSE
    }
    
    ### The while loop continues by recomputing the gradients 
  }
  
  return(current_s)
}

set.seed(123)
X <- matrix(rnorm(2000),100,20)
y <- X %*% runif(20) + rnorm(100)*5

library(nnls)
library(multiway)


data.frame(multiway = multiway::fnnls(a, b),
           nnls = nnls::nnls(X, y)$x,
           manual = fastnnls(y,X))

Output

> data.frame(multiway = multiway::fnnls(a, b),
+            nnls = nnls::nnls(X, y)$x,
+            manual = fastnnls(y,X))
      multiway        nnls      manual
1  0.610802720 0.610802720 0.610802720
2  0.146121047 0.146121047 0.146121047
3  0.841809005 0.841809005 0.841809005
4  1.131040740 1.131040740 1.131040740
5  0.000000000 0.000000000 0.000000000
6  1.093652478 1.093652478 1.093652478
7  0.725590111 0.725590111 0.725590111
8  0.211525228 0.211525228 0.211525228
9  0.000000000 0.000000000 0.000000000
10 1.472333600 1.472333600 1.472333600
11 0.005740395 0.005740395 0.005740395
12 2.131277775 2.131277775 2.131277775
13 0.000000000 0.000000000 0.000000000
14 0.590923989 0.590923989 0.590923989
15 0.652530944 0.652530944 0.652530944
16 0.717713755 0.717713755 0.717713755
17 1.115162378 1.115162378 1.115162378
18 0.603304661 0.603304661 0.603304661
19 0.000000000 0.000000000 0.000000000
20 0.218073317 0.218073317 0.218073317