R optim(): неожиданное поведение при работе с родительскими средами

Рассмотрим функцию fn() которая хранит самый последний ввод x и его возвращаемое значение ret <- x^2 в родительской среде.

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() выполняет вычисления только в том случае, если указано другое входное значение. В противном случае он читает ret из родительской среды.

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

Когда плагин fn() в optim() находит его минимум, это приводит к неожиданному поведению:

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
#
# (...)

Это ошибка? Как это может случиться?

Даже когда я использую C-API R, мне трудно представить, как можно добиться такого поведения. Есть идеи?


Замечания:

  • работает:

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

    optimize(f=fn, interval=c(-10, 10))
    
  • работает:

    optim(par=10, fn=fn)
    
  • терпит неудачу:

    optim(par=10, fn=fn, method="BFGS")
    
  • работает:

    library("lbfgs"); library("numDeriv")
    lbfgs(call_eval=fn, call_grad=function(x) grad(func=fn, x=x), vars=10)
    
  • работает:

    library("memoise")
    fn_mem <- memoise(function(x) x^2)
    optim(par=10, f=fn_mem, method="L-BFGS-B")
    
  • Протестировано с версией R 3.5.0.

Ответ 1

Проблема возникает из-за того, что адрес памяти x не обновляется, когда он изменяется на третьей итерации алгоритма оптимизации в соответствии с методом "BFGS" или "L-BFGS-B", как и должно быть.

Вместо этого адрес памяти x сохраняется на том же уровне, что и адрес памяти xx на третьей итерации, и это приводит к тому, что xx обновляется до значения x до того, как функция fn запускается в третий раз, таким образом заставляя функцию возвращать " Память "значение ret.

Вы можете проверить это самостоятельно, запустив следующий код, который извлекает адреса памяти x и xx fn() с помощью функции address() пакета envnames или data.table:

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")

чей частичный вывод (при условии, что перед запуском этого фрагмента кода не было выполнено ни одного прогона оптимизации) будет аналогичен следующему:

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)

Эта проблема не возникает с другими методами оптимизации, доступными в optim(), такими как метод по умолчанию.

Примечание. Как уже упоминалось, пакет data.table также можно использовать для получения адреса памяти объектов, но здесь я пользуюсь возможностью для продвижения моих недавно выпущенных имен пакетов (которые, кроме получения адреса памяти объекта, также извлекают определяемые пользователем имена среды из их адреса памяти --among, другие вещи)