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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With