R высокоэффективная рекурсия

#r #recursion #memory-efficient

#r #рекурсия #экономия памяти

Вопрос:

Предположим, что мы ежемесячно наблюдаем данные за горизонтом в 1 год (k = 12). Давайте также предположим следующую рекурсивную структуру (пример ниже).

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

  • Период в 1 год указан для простоты и обычно не фиксируется при этом значении. Соответственно, это значение должно переопределяться каждый раз,
  • Ежемесячные вычисления должны быть сохранены (я подумал о списке?) потому что мне нужны конкретные значения для дальнейших вычислений.

MWE:

 beta = c(0.95, 0.89, 0.23, 0.96, 0.98, 0.79, 0.99, 0.85, 0.97) 
dim(beta) <- c(3, 3)
phi = c(0.45, 0.12, 0.98, 0.66, 0.79, 0.24, 0.33, 0.19, 0.27)
dim(phi) <- c(3, 3)  
 

(В общем случае параметры beta и phi не наблюдаются, но должны быть оценены, так что это всего лишь предположение для простоты.)

В k-1 мы вычисляем матрицу следующего вида:

 K1 <- beta %*% t(beta)
 

В k-2 мы имеем аналогичное выражение, но теперь оно также зависит от предыдущего значения, умноженного на phi:

 K2 <- beta %*% t(beta)   phi %*% K1
 

В периоде k-3 это то же самое, что и в k-2, только K1 меняется на K2. Итак, с этого момента он является рекурсивным и повторяется. Итак, для последнего наблюдения (k-11) это та же формула, что и для K10.

Ответ №1:

Прежде всего, вам всегда нужен термин beta %*% t(beta) , поэтому сохраняйте его вместо вычисления на каждой итерации:

 btb <- beta %*% t(beta)
 

Теперь вы можете использовать функцию Reduce — .

 Reduce(f = function(kPrevious,someOther) {btb   phi %*% kPrevious},
       x = 1:10,
       init = btb,
       accumulate = TRUE
)
 

Аргумент accumulate=TRUE сохраняет все промежуточные результаты.

Альтернативой может быть использование простого for цикла. Но это не так эффективно, как Reduce . (Хотя он делает «то же самое»):

   history <- list(btb)
  for(k in 1:10) {
    history <- c(history,list(btb   phi %*% history[[length(history)]]))
  }
  history
 

Для вычисления некоторых тестов давайте поместим все в функцию, зависящую от количества итераций:

 ff0 <- function(n=10) {
  Reduce(f = function(kPrevious,someOther) {btb   phi %*% kPrevious},
         x = 1:n,
         init = btb,
         accumulate = TRUE
  )
}

ff <- function(n=10) {
  history <- list(btb)
  for(k in 1:n) {
    history <- c(history,list(btb   phi %*% history[[length(history)]]))
  }
  history
}
 

Теперь давайте посмотрим, дают ли две функции одинаковый результат:

 all(mapply(FUN = function(x,y) {all(x==y)},ff0(),ff()))
 

Чтобы получить эталон, давайте используем microbenchmark :

 microbenchmark::microbenchmark(ff0(),ff())
# Unit: microseconds
#  expr  min    lq   mean median    uq  max neval
# ff0() 20.6 22.80 25.388   24.1 25.20 79.2   100
#  ff() 11.4 12.25 13.922   13.0 13.65 80.0   100

microbenchmark::microbenchmark(ff0(100),ff(100))
#Unit: microseconds
#     expr   min     lq    mean median     uq   max neval
# ff0(100) 163.1 172.50 191.193 186.35 198.00 320.6   100
#  ff(100) 143.0 151.45 169.125 164.75 174.05 296.1   100

microbenchmark::microbenchmark(ff0(1000),ff(1000))
#Unit: milliseconds
#      expr    min      lq     mean  median      uq     max neval
# ff0(1000) 1.5680 1.62505 1.698665 1.67135 1.74185  2.6058   100
#  ff(1000) 4.6626 4.77065 5.293329 4.87640 5.10260 11.2255   100

microbenchmark::microbenchmark(ff0(10000),ff(10000))
#Unit: milliseconds
#       expr      min       lq      mean   median        uq      max neval
# ff0(10000)  15.9035  16.4428  17.28437  17.0618  17.71085  22.1506   100
#  ff(10000) 468.8924 484.7448 494.79808 489.4221 496.60075 581.0389   100

 

Как мы можем видеть, реализация for -loop, по-видимому, лучше подходит для вычислений с относительно небольшим количеством итераций. Но по мере увеличения итераций Reduce становится быстрее. Я думаю, причина в том, что это Reduce создает больше накладных расходов, чем простой foor цикл, но эти накладные расходы не имеют большого веса после его создания.
Так что вам решать, какую реализацию вы будете использовать.

Комментарии:

1. если бета-матрица огромна, то tcrossprod(beta) вместо beta %*% t(beta) будет намного быстрее