Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

R optim(): unexpected behavior when working with parent environments

Consider the function fn() which stores the most recent input x and its return value ret <- x^2 in the parent environment.

makeFn <- function(){
    xx <- ret <- NA
    fn <- function(x){
       if(!is.na(xx) && x==xx){
           cat("x=", xx, ", ret=", ret, " (memory)", fill=TRUE, sep="")
           return(ret)
       }
       xx <<- x; ret <<- sum(x^2)
       cat("x=", xx, ", ret=", ret, " (calculate)", fill=TRUE, sep="")
       ret
    }
    fn
}
fn <- makeFn()

fn() only does the calculation when a different input value is provided. Otherwise, it reads ret from the parent environment.

fn(2)
# x=2, ret=4 (calculate)
# [1] 4
fn(3)
# x=3, ret=9 (calculate)
# [1] 9
fn(3)
# x=3, ret=9 (memory)
# [1] 9

When plugin fn() into optim() to find its minimum, the following unexpected behavior results:

optim(par=10, f=fn, method="L-BFGS-B")
# x=10, ret=100 (calculate)
# x=10.001, ret=100.02 (calculate)
# x=9.999, ret=100.02 (memory)
# $par
# [1] 10
# 
# $value
# [1] 100
#
# (...)

Is this a bug? How can this happen?

Even when using the C-API of R, I have a hard time to imagine how this behavior can be achieved. Any ideas?


Note:

  • works:

    library("optimParallel") # (parallel) wrapper to optim(method="L-BFGS-B")
    cl <- makeCluster(2); setDefaultCluster(cl)
    optimParallel(par=10, f=fn)
    
  • works:

    optimize(f=fn, interval=c(-10, 10))
    
  • works:

    optim(par=10, fn=fn)
    
  • fails:

    optim(par=10, fn=fn, method="BFGS")
    
  • works:

    library("lbfgs"); library("numDeriv")
    lbfgs(call_eval=fn, call_grad=function(x) grad(func=fn, x=x), vars=10)
    
  • works:

    library("memoise")
    fn_mem <- memoise(function(x) x^2)
    optim(par=10, f=fn_mem, method="L-BFGS-B")
    
  • Tested with R version 3.5.0.

like image 520
Nairolf Avatar asked Dec 18 '18 04:12

Nairolf


1 Answers

The problem is happening because the memory address of x is not updated when it is modified on the third iteration of the optimization algorithm under the "BFGS" or "L-BFGS-B" method, as it should.

Instead, the memory address of x is kept the same as the memory address of xx at the third iteration, and this makes xx be updated to the value of x before the fn function runs for the third time, thus making the function return the "memory" value of ret.

You can verify this by yourself if you run the following code that retrieves the memory address of x and xx inside fn() using the address() function of the envnames or data.table package:

library(envnames)

makeFn <- function(){
  xx <- ret <- NA
  fn <- function(x){
    cat("\nAddress of x and xx at start of fn:\n")
    cat("address(x):", address(x), "\n")
    cat("address(xx):", address(xx), "\n")
    if(!is.na(xx) && x==xx){
      cat("x=", xx, ", ret=", ret, " (memory)", fill=TRUE, sep="")
      return(ret)
    }
    xx <<- x; ret <<- sum(x^2)
    cat("x=", xx, ", ret=", ret, " (calculate)", fill=TRUE, sep="")
    ret
  }
  fn
}

fn <- makeFn()

# Run the optimization process
optim(par=0.1, fn=fn, method="L-BFGS-B")

whose partial output (assuming no optimization run was done prior to running this code snippet) would be similar to the following:

Address of x and xx at start of fn:
address(x): 0000000013C89DA8 
address(xx): 00000000192182D0 
x=0.1, ret=0.010201 (calculate)

Address of x and xx at start of fn:
address(x): 0000000013C8A160 
address(xx): 00000000192182D0 
x=0.101, ret=0.010201 (calculate)

Address of x and xx at start of fn:
address(x): 0000000013C8A160 
address(xx): 0000000013C8A160 
x=0.099, ret=0.010201 (memory)

This problem does not happen with other optimization methods available in optim(), such as the default one.

Note: As mentioned, the data.table package can also be used to retrieve the memory address of objects, but here I am taking the opportunity to promote my recently released package envnames (which, other than retrieving an object's memory address, it also retrieves user-defined environment names from their memory address --among other things)

like image 120
mastropi Avatar answered Oct 22 '22 19:10

mastropi